diff --git a/FEAT.md b/FEAT.md index 4d3fefa..56b0fd2 100644 --- a/FEAT.md +++ b/FEAT.md @@ -121,3 +121,38 @@ Turn management is handled entirely by the plugin. Manual control via slash comm - Member list with pagination + field projection - Guardrails: action validation, id-list limits, response-size limit - (Migrated) Discord control now runs in-plugin via direct Discord REST (no companion service) + +--- + +## NEW_FEAT 合并记录(原 NEW_FEAT.md) + +### 背景与目标 +- 解决 turn 初始化依赖被动观察(`recordChannelAccount`)导致 `currentSpeaker` 空值的问题。 +- 将 Discord control 从 sidecar 迁移到插件内模块。 +- 采用 channel 成员缓存(内存 + 本地持久化),避免轮询。 + +### 关键实现方向 +- 统一 channelId 解析链路,避免 `channel=discord` 错位。 +- `before_model_resolve / before_prompt_build` 与消息 hook 使用一致解析策略。 +- 清理未使用函数,降低排障噪音。 +- 模块化重构:`index.ts` 作为 wiring,逻辑拆入 `hooks/core/tools/policy/commands`。 + +### Channel 成员缓存 +- 缓存文件:`~/.openclaw/dirigent-channel-members.json` +- 启动加载、运行时原子写盘。 +- 记录字段包含 `botAccountIds/updatedAt/source/guildId`。 +- 首次无缓存时允许 bootstrap 拉取,随后走本地缓存。 + +### Turn 初始化改造 +- `ensureTurnOrder(channelId)` 基于缓存中的 botAccountIds 初始化。 +- 不再仅依赖“已见账号”被动记录。 +- 提升新频道首条消息场景的稳定性。 + +### 权限计算(频道可见成员) +- 通过 guild 成员 + roles + channel overwrites 计算 `VIEW_CHANNEL` 可见性。 +- 用于内部 turn bootstrap,不对外暴露为公共工具。 + +### 风险与注意 +- 权限位计算必须严格按 Discord 规则。 +- 缓存读写需原子化,防并发损坏。 +- 通过 `updatedAt/source/guildId` 提高可观测性与排障效率。 diff --git a/NEW_FEAT.md b/NEW_FEAT.md deleted file mode 100644 index d57a957..0000000 --- a/NEW_FEAT.md +++ /dev/null @@ -1,185 +0,0 @@ -# NEW_FEAT — Dirigent 设计草案(channel 成员缓存驱动 turn 初始化) - -## 背景 - -当前 turn manager 初始化依赖 `recordChannelAccount`(被动观察频道里出现过哪些 bot account)。 -在新频道/低活跃频道中,可能出现 turn state 未及时建立,导致 `currentSpeaker` 为空(`undefined/null`)并引发调度异常。 - -(已完成迁移)原 `discord-control-api` sidecar 带来部署与运维复杂度,现已改为插件内直连 Discord REST。 - ---- - -## 目标 - -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. 增加调试日志与回归测试(新频道首条消息场景)