6.1 KiB
6.1 KiB
NEW_FEAT — Dirigent 设计草案(channel 成员缓存驱动 turn 初始化)
背景
当前 turn manager 初始化依赖 recordChannelAccount(被动观察频道里出现过哪些 bot account)。
在新频道/低活跃频道中,可能出现 turn state 未及时建立,导致 currentSpeaker 为空(undefined/null)并引发调度异常。
(已完成迁移)原 discord-control-api sidecar 带来部署与运维复杂度,现已改为插件内直连 Discord REST。
目标
- 将 Discord control 能力从独立 API server 收敛为插件内模块(类似 turn-manager.ts 的内部模块化)。
- 引入 channel 成员列表缓存(内存 + 本地文件持久化),作为 turn 初始化的权威输入。
- 成员列表采用 事件/工具触发更新,不轮询。
方案总览
A. 架构调整
- 新增模块:
plugin/discord-control.ts - (已迁移)将原
discord-control-api/server.mjs核心能力迁移为插件模块函数:channelPrivateCreatechannelPrivateUpdateguildMemberListchannelMemberList(新增)
说明:
channelMemberList通过 Discord 权限计算(guild 成员 + channel overwrite)得到可见成员。
B. channel 成员缓存(内存 + 本地持久化)
建议新增缓存文件(示例):
~/.openclaw/dirigent-channel-members.json
结构示例:
{
"1479928646830391346": {
"guildId": "1368531017534537779",
"memberUserIds": ["..."],
"botAccountIds": ["neon", "nav", "lyn"],
"updatedAt": "2026-03-07T21:40:00Z",
"source": "tool|bootstrap|channel-private-update"
}
}
插件启动时加载到内存;更新时先改内存再原子写盘。
C. 更新策略(不轮询)
只在以下时机刷新某 channel 成员:
- 通过指定工具显式更新(手动触发)
channel-private-create成功后刷新该 channelchannel-private-update成功后刷新该 channel- (建议兜底)首次遇到无缓存 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路径中,先走与消息钩子一致的解析逻辑:extractDiscordChannelId(ctx, event)(优先)sessionKey正则兜底(discord:(channel|group):<id>或:channel:<id>)- 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 可见性:
- 基于 guild 基础权限(@everyone + 角色)
- 叠加 channel
permission_overwrites:- @everyone overwrite
- role overwrites(合并)
- member overwrite(最终)
- 最终判定是否可见频道
私密线程与普通私密文本频道语义不同,需分路径处理。
风险与注意
- 权限位计算需严格按 Discord bitfield 规则实现,否则成员集会偏差。
- 缓存文件读写要原子化,防止并发写损坏。
- 建议记录
updatedAt/source便于排查“成员集为何变化”。
G. 模块化重构(拆分 plugin/index.ts)
现状:
plugin/index.ts体积过大,hook、工具、状态、解析逻辑耦合在一起,排障成本高。
重构方向:
plugin/hooks/message-received.tsbefore-model-resolve.tsbefore-prompt-build.tsbefore-message-write.tsmessage-sent.ts
plugin/core/decision-input.tschannel-resolver.tssession-state.ts
plugin/policy/policy-store.tspolicy-resolver.ts
plugin/tools/discord-control-tools.tspolicy-tools.ts
plugin/index.ts- 仅保留 wiring(注册工具 + 注册 hooks + 生命周期管理)
目标:
- 单文件职责清晰,便于定位问题与编写回归测试。
- 关键路径(channel/sender/session/turn)统一入口,避免同逻辑多处实现漂移。
里程碑建议
- 抽离
discord-control.ts(功能等价迁移) - 增加成员缓存存储与读写
- 实现
channelMemberList权限计算 - 打通 create/update 工具触发刷新
- 改造 turn 初始化读取缓存
- 统一 channelId 解析到
extractDiscordChannelId路径 - 清理未使用函数(
normalizeSender/normalizeChannel) - 模块化拆分
plugin/index.ts(最少先拆 core 解析层) - 增加调试日志与回归测试(新频道首条消息场景)