diff --git a/plans/TASKLIST.md b/plans/TASKLIST.md index 4684529..dfe8bf2 100644 --- a/plans/TASKLIST.md +++ b/plans/TASKLIST.md @@ -119,12 +119,12 @@ - [ ] 确保 callback 完成后的 closed channel 不会继续触发 handoff #### A7.4 `plugin/hooks/message-received.ts` -- [ ] 梳理 moderator bot 消息当前是否已被过滤,避免 moderator 自己再次触发讨论链路 -- [ ] 对 closed discussion channel 的新消息增加统一处理入口 -- [ ] 若 closed discussion channel 收到新消息: - - [ ] 不再唤醒任何 Agent 正常讨论 - - [ ] 由 moderator 回复“channel 已关闭,仅做留档使用” -- [ ] 避免 moderator 的 closed 提示消息反复触发自身处理 +- [x] 梳理 moderator bot 消息当前是否已被过滤,避免 moderator 自己再次触发讨论链路 +- [x] 对 closed discussion channel 的新消息增加统一处理入口 +- [x] 若 closed discussion channel 收到新消息: + - [x] 不再唤醒任何 Agent 正常讨论 + - [x] 由 moderator 回复“channel 已关闭,仅做留档使用” +- [x] 避免 moderator 的 closed 提示消息反复触发自身处理 #### A7.5 `plugin/core/session-state.ts`(如需) - [ ] 检查现有 session 相关缓存是否适合扩展 discussion 状态 @@ -167,23 +167,23 @@ - [ ] 明确 channel 已关闭,仅做留档使用 ### A11. `discuss-callback` 详细校验任务 -- [ ] 校验当前 channel 必须是 discussion channel -- [ ] 校验当前 discussion 状态必须是 `active` -- [ ] 校验调用者必须是 initiator -- [ ] 校验 `summaryPath` 非空 -- [ ] 校验 `summaryPath` 文件存在 -- [ ] 校验 `summaryPath` 路径在 initiator workspace 内 -- [ ] 校验 callback 未重复执行 -- [ ] callback 成功后写入 `completedAt` -- [ ] callback 成功后记录 `summaryPath` -- [ ] callback 成功后切换 discussion 状态为 `completed` / `closed` +- [x] 校验当前 channel 必须是 discussion channel +- [x] 校验当前 discussion 状态必须是 `active` +- [x] 校验调用者必须是 initiator +- [x] 校验 `summaryPath` 非空 +- [x] 校验 `summaryPath` 文件存在 +- [x] 校验 `summaryPath` 路径在 initiator workspace 内 +- [x] 校验 callback 未重复执行 +- [x] callback 成功后写入 `completedAt` +- [x] callback 成功后记录 `summaryPath` +- [x] callback 成功后切换 discussion 状态为 `completed` / `closed` ### A12. 关闭后的行为封口 -- [ ] closed discussion channel 中所有旧 session 继续使用 no-reply 覆盖 -- [ ] closed discussion channel 中任何新消息都不再进入真实讨论 -- [ ] closed discussion channel 的任何新消息统一走 moderator 固定回复 -- [ ] 防止 closed channel 中 moderator 自己的回复再次触发回环 -- [ ] 明确 archived-only 的最终行为与边界 +- [x] closed discussion channel 中所有旧 session 继续使用 no-reply 覆盖 +- [x] closed discussion channel 中任何新消息都不再进入真实讨论 +- [x] closed discussion channel 的任何新消息统一走 moderator 固定回复 +- [x] 防止 closed channel 中 moderator 自己的回复再次触发回环 +- [x] 明确 archived-only 的最终行为与边界 ### A13. 测试与文档收尾 #### A13.1 工具层测试 diff --git a/plugin/core/discussion-service.ts b/plugin/core/discussion-service.ts index aa65438..0ed8a0e 100644 --- a/plugin/core/discussion-service.ts +++ b/plugin/core/discussion-service.ts @@ -7,6 +7,7 @@ import { sendModeratorMessage } from "./moderator-discord.js"; type DiscussionServiceDeps = { api: OpenClawPluginApi; moderatorBotToken?: string; + moderatorUserId?: string; workspaceRoot?: string; forceNoReplyForSession: (sessionKey: string) => void; }; @@ -171,6 +172,7 @@ export function createDiscussionService(deps: DiscussionServiceDeps) { async function maybeReplyClosedChannel(channelId: string, senderId?: string): Promise { const metadata = getDiscussion(channelId); if (!metadata || metadata.status !== "closed") return false; + if (deps.moderatorUserId && senderId && senderId === deps.moderatorUserId) return true; if (!deps.moderatorBotToken) return true; await sendModeratorMessage(deps.moderatorBotToken, channelId, buildClosedMessage(), deps.api.logger); return true; @@ -179,6 +181,9 @@ export function createDiscussionService(deps: DiscussionServiceDeps) { return { initDiscussion, getDiscussion, + isClosedDiscussion(channelId: string): boolean { + return isDiscussionClosed(channelId); + }, maybeSendIdleReminder, maybeReplyClosedChannel, handleCallback, diff --git a/plugin/hooks/before-model-resolve.ts b/plugin/hooks/before-model-resolve.ts index 04fa65f..71d4dc6 100644 --- a/plugin/hooks/before-model-resolve.ts +++ b/plugin/hooks/before-model-resolve.ts @@ -29,6 +29,9 @@ type BeforeModelResolveDeps = { pruneDecisionMap: () => void; shouldDebugLog: (config: DirigentConfig & DebugConfig, channelId?: string) => boolean; ensureTurnOrder: (api: OpenClawPluginApi, channelId: string) => Promise | void; + discussionService?: { + isClosedDiscussion: (channelId: string) => boolean; + }; }; export function registerBeforeModelResolveHook(deps: BeforeModelResolveDeps): void { @@ -47,6 +50,7 @@ export function registerBeforeModelResolveHook(deps: BeforeModelResolveDeps): vo pruneDecisionMap, shouldDebugLog, ensureTurnOrder, + discussionService, } = deps; api.on("before_model_resolve", async (event, ctx) => { @@ -86,6 +90,15 @@ export function registerBeforeModelResolveHook(deps: BeforeModelResolveDeps): vo if (derived.channelId) { sessionChannelId.set(key, derived.channelId); + if (discussionService?.isClosedDiscussion(derived.channelId)) { + sessionAllowed.set(key, false); + api.logger.info(`dirigent: before_model_resolve forcing no-reply for closed discussion channel=${derived.channelId} session=${key}`); + return { + model: ctx.model, + provider: ctx.provider, + noReply: true, + }; + } } const resolvedAccountId = resolveAccountId(api, ctx.agentId || ""); if (resolvedAccountId) { diff --git a/plugin/index.ts b/plugin/index.ts index 0f19715..83dcd95 100644 --- a/plugin/index.ts +++ b/plugin/index.ts @@ -118,6 +118,7 @@ export default { const discussionService = createDiscussionService({ api, moderatorBotToken: baseConfig.moderatorBotToken, + moderatorUserId: getModeratorUserId(baseConfig), workspaceRoot: process.cwd(), forceNoReplyForSession: (sessionKey: string) => { if (sessionKey) forceNoReplySessions.add(sessionKey); @@ -163,6 +164,7 @@ export default { pruneDecisionMap, shouldDebugLog, ensureTurnOrder, + discussionService, }); registerBeforePromptBuildHook({