Files
Dirigent/NEW_FEAT.md

6.1 KiB
Raw Blame History

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

结构示例:

{
  "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):<id>:channel:<id>
    3. prompt 中 untrusted metadatachat_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. 增加调试日志与回归测试(新频道首条消息场景)