# NEW_FEAT — Dirigent 设计草案(channel 成员缓存驱动 turn 初始化) ## 背景 当前 turn manager 初始化依赖 `recordChannelAccount`(被动观察频道里出现过哪些 bot account)。 在新频道/低活跃频道中,可能出现 turn state 未及时建立,导致 `currentSpeaker` 为空(`undefined/null`)并引发调度异常。 同时,`discord-control-api` 以独立 sidecar 运行增加了部署与运维复杂度。 --- ## 目标 1. 将 Discord control 能力从独立 API server 收敛为插件内模块(类似 turn-manager.ts 的内部模块化)。 2. 引入 **channel 成员列表缓存(内存 + 本地文件持久化)**,作为 turn 初始化的权威输入。 3. 成员列表采用 **事件/工具触发更新**,不轮询。 --- ## 方案总览 ### A. 架构调整 - 新增模块:`plugin/discord-control.ts` - 迁移当前 `discord-control-api/server.mjs` 的核心能力为模块函数: - `channelPrivateCreate` - `channelPrivateUpdate` - `guildMemberList` - `channelMemberList`(新增) > 说明:`channelMemberList` 通过 Discord 权限计算(guild 成员 + channel overwrite)得到可见成员。 --- ### B. channel 成员缓存(内存 + 本地持久化) 建议新增缓存文件(示例): - `~/.openclaw/dirigent-channel-members.json` 结构示例: ```json { "1479928646830391346": { "guildId": "1368531017534537779", "memberUserIds": ["..."], "botAccountIds": ["neon", "nav", "lyn"], "updatedAt": "2026-03-07T21:40:00Z", "source": "tool|bootstrap|channel-private-update" } } ``` 插件启动时加载到内存;更新时先改内存再原子写盘。 --- ### C. 更新策略(不轮询) 只在以下时机刷新某 channel 成员: 1. 通过指定工具显式更新(手动触发) 2. `channel-private-create` 成功后刷新该 channel 3. `channel-private-update` 成功后刷新该 channel 4. (建议兜底)首次遇到无缓存 channel 时允许一次 bootstrap 拉取并落盘 > 除上述触发外,不做轮询保活。 --- ### D. turn manager 初始化改造 当前: - `ensureTurnOrder()` 依赖 `recordChannelAccount` 的被动观察结果 改造后: - `ensureTurnOrder(channelId)` 直接读取 channel 成员缓存中的 `botAccountIds` - 有可用 bot 列表即 `initTurnOrder(channelId, botAccountIds)` - 不再把 `recordChannelAccount` 作为主初始化来源(可保留为辅助观测) 预期效果: - 新频道首次消息即可建立 turn state - 避免 `currentSpeaker` 长时间空值导致的并发生成异常 --- ### E. 统一 channelId 解析(`deriveDecisionInputFromPrompt` 对齐 `extractDiscordChannelId`) 问题点: - `deriveDecisionInputFromPrompt(prompt, messageProvider, ctx.channelId)` 目前可能优先使用 `ctx.channelId`。 - 在 OpenClaw 的 Discord 场景中,`ctx.channelId` 常是平台名(`"discord"`),不是 snowflake。 改造建议: - 在 `before_model_resolve / before_prompt_build` 路径中,先走与消息钩子一致的解析逻辑: 1. `extractDiscordChannelId(ctx, event)`(优先) 2. `sessionKey` 正则兜底(`discord:(channel|group):` 或 `:channel:`) 3. prompt 中 untrusted metadata(`chat_id` / `conversation_label` / `channel_id`)作为最后兜底 - `ctx.channelId` 仅作为末级 fallback,不再作为高优先级输入。 目标: - 让 `message_received / message_sent / before_model_resolve / before_prompt_build` 使用一致的 channelId 来源,避免出现 `channel=discord` 导致 turn/policy 失配。 --- ### F. 清理未使用函数(减小维护噪音) 当前检查到 `plugin/index.ts` 中存在未被引用的函数: - `normalizeSender(...)` - `normalizeChannel(...)` 计划: - 在完成 channelId 解析统一改造后,移除上述未使用函数与相关无效注释。 - 如后续确实需要其语义,改为在统一解析模块内落地实际调用,不保留“仅定义不使用”的中间状态。 目标: - 降低代码噪音,避免误导排障(看起来像在用,实际没走到)。 - 保持关键路径(sender/channel 解析)只有一套可追踪实现。 --- ## Discord 权限计算要点(channelMemberList) 按 Discord 规则计算 `VIEW_CHANNEL` 可见性: 1. 基于 guild 基础权限(@everyone + 角色) 2. 叠加 channel `permission_overwrites`: - @everyone overwrite - role overwrites(合并) - member overwrite(最终) 3. 最终判定是否可见频道 > 私密线程与普通私密文本频道语义不同,需分路径处理。 --- ## 风险与注意 - 权限位计算需严格按 Discord bitfield 规则实现,否则成员集会偏差。 - 缓存文件读写要原子化,防止并发写损坏。 - 建议记录 `updatedAt/source` 便于排查“成员集为何变化”。 --- ### G. 模块化重构(拆分 `plugin/index.ts`) 现状: - `plugin/index.ts` 体积过大,hook、工具、状态、解析逻辑耦合在一起,排障成本高。 重构方向: - `plugin/hooks/` - `message-received.ts` - `before-model-resolve.ts` - `before-prompt-build.ts` - `before-message-write.ts` - `message-sent.ts` - `plugin/core/` - `decision-input.ts` - `channel-resolver.ts` - `session-state.ts` - `plugin/policy/` - `policy-store.ts` - `policy-resolver.ts` - `plugin/tools/` - `discord-control-tools.ts` - `policy-tools.ts` - `plugin/index.ts` - 仅保留 wiring(注册工具 + 注册 hooks + 生命周期管理) 目标: - 单文件职责清晰,便于定位问题与编写回归测试。 - 关键路径(channel/sender/session/turn)统一入口,避免同逻辑多处实现漂移。 --- ## 里程碑建议 1. 抽离 `discord-control.ts`(功能等价迁移) 2. 增加成员缓存存储与读写 3. 实现 `channelMemberList` 权限计算 4. 打通 create/update 工具触发刷新 5. 改造 turn 初始化读取缓存 6. 统一 channelId 解析到 `extractDiscordChannelId` 路径 7. 清理未使用函数(`normalizeSender` / `normalizeChannel`) 8. 模块化拆分 `plugin/index.ts`(最少先拆 core 解析层) 9. 增加调试日志与回归测试(新频道首条消息场景)