From d44204fabfe73cb69264e1f225a02330ee522800 Mon Sep 17 00:00:00 2001 From: zhi Date: Thu, 2 Apr 2026 04:48:20 +0000 Subject: [PATCH] feat: wire channel mode runtime config and docs --- plans/TASKLIST.md | 18 +++++++++--------- plugin/README.md | 15 +++++++++++++++ plugin/core/channel-modes.ts | 11 ++++------- plugin/hooks/before-message-write.ts | 3 ++- plugin/hooks/before-model-resolve.ts | 3 ++- plugin/hooks/message-received.ts | 5 ++++- plugin/index.ts | 8 ++++++++ plugin/rules.ts | 14 ++++++++++++++ plugin/turn-manager.ts | 3 ++- 9 files changed, 60 insertions(+), 20 deletions(-) diff --git a/plans/TASKLIST.md b/plans/TASKLIST.md index d57f7df..dd60f97 100644 --- a/plans/TASKLIST.md +++ b/plans/TASKLIST.md @@ -243,9 +243,9 @@ - [ ] 评估是否需要增加 shuffle 默认配置项 #### B2.2 `plugin/rules.ts` / config 类型 -- [ ] 为 multi-message mode 相关配置补类型定义 -- [ ] 为 shuffle mode 相关 channel state / config 补类型定义 -- [ ] 确保运行时读取配置逻辑可访问新增字段 +- [x] 为 multi-message mode 相关配置补类型定义 +- [x] 为 shuffle mode 相关 channel state / config 补类型定义 +- [x] 确保运行时读取配置逻辑可访问新增字段 ### B3. `plugin/core/` 新增 channel mode / shuffle state 模块 - [x] 新增 channel mode state 模块(如 `plugin/core/channel-modes.ts`) @@ -305,14 +305,14 @@ - [x] 命令帮助文本补充说明 ### B8. `plugin/index.ts` -- [ ] 注入 channel mode / shuffle state 模块依赖 -- [ ] 将新状态能力传给相关 hooks / turn-manager -- [ ] 保持初始化关系清晰,避免 mode 逻辑散落 +- [x] 注入 channel mode / shuffle state 模块依赖 +- [x] 将新状态能力传给相关 hooks / turn-manager +- [x] 保持初始化关系清晰,避免 mode 逻辑散落 ### B9. moderator 消息模板 -- [ ] 定义 multi-message mode 下的 prompt marker 发送规则 -- [ ] 明确是否需要 start / end 的 moderator 确认消息 -- [ ] 定义退出 multi-message mode 后的 scheduling handoff 触发格式 +- [x] 定义 multi-message mode 下的 prompt marker 发送规则 +- [x] 明确是否需要 start / end 的 moderator 确认消息 +- [x] 定义退出 multi-message mode 后的 scheduling handoff 触发格式 ### B10. 测试 #### B10.1 Multi-Message Mode diff --git a/plugin/README.md b/plugin/README.md index add6333..397cd9d 100644 --- a/plugin/README.md +++ b/plugin/README.md @@ -30,6 +30,9 @@ Optional: - `channelPoliciesFile` (per-channel overrides in a standalone JSON file) - `schedulingIdentifier` (default `➡️`) — moderator handoff identifier - `enableDirigentPolicyTool` (default true) +- `multiMessageStartMarker` (default `↗️`) +- `multiMessageEndMarker` (default `↙️`) +- `multiMessagePromptMarker` (default `⤵️`) Unified optional tool: - `dirigent_tools` @@ -59,6 +62,15 @@ When the current speaker NO_REPLYs, the moderator bot sends: `<@NEXT_USER_ID>➡ This is a non-semantic scheduling message. The scheduling identifier (`➡️` by default) carries no meaning — it simply signals the next agent to check chat history and decide whether to speak. +## Multi-message mode / shuffle mode + +- Human sends the configured start marker (default `↗️`) → channel enters multi-message mode. +- While active, agents are forced to no-reply and the moderator sends only the configured prompt marker (default `⤵️`) after each additional human message. +- Human sends the configured end marker (default `↙️`) → channel exits multi-message mode and normal scheduling resumes. +- No separate moderator "entered/exited mode" confirmation message is sent; the markers themselves are the protocol. +- The first moderator message after exit uses the normal scheduling handoff format: `<@NEXT_USER_ID>➡️`. +- `/dirigent turn-shuffling`, `/dirigent turn-shuffling on`, and `/dirigent turn-shuffling off` control per-channel reshuffling between completed rounds. + ## Slash command (Discord) ``` @@ -66,6 +78,9 @@ This is a non-semantic scheduling message. The scheduling identifier (`➡️` b /dirigent turn-status /dirigent turn-advance /dirigent turn-reset +/dirigent turn-shuffling +/dirigent turn-shuffling on +/dirigent turn-shuffling off ``` Debug logging: diff --git a/plugin/core/channel-modes.ts b/plugin/core/channel-modes.ts index e41cedd..620bc54 100644 --- a/plugin/core/channel-modes.ts +++ b/plugin/core/channel-modes.ts @@ -1,10 +1,7 @@ -export type ChannelMode = "normal" | "multi-message"; +import type { ChannelRuntimeMode, ChannelRuntimeState } from "../rules.js"; -export type ChannelModesState = { - mode: ChannelMode; - shuffling: boolean; - lastShuffledAt?: number; -}; +export type ChannelMode = ChannelRuntimeMode; +export type ChannelModesState = ChannelRuntimeState; const channelStates = new Map(); @@ -48,4 +45,4 @@ export function markLastShuffled(channelId: string): void { const state = getChannelState(channelId); state.lastShuffledAt = Date.now(); channelStates.set(channelId, state); -} \ No newline at end of file +} diff --git a/plugin/hooks/before-message-write.ts b/plugin/hooks/before-message-write.ts index f0d0e4d..810ffe1 100644 --- a/plugin/hooks/before-message-write.ts +++ b/plugin/hooks/before-message-write.ts @@ -1,7 +1,6 @@ import type { OpenClawPluginApi } from "openclaw/plugin-sdk"; import { resolvePolicy, type DirigentConfig } from "../rules.js"; import { getTurnDebugInfo, onSpeakerDone, setWaitingForHuman } from "../turn-manager.js"; -import { isMultiMessageMode } from "../core/channel-modes.js"; type DebugConfig = { enableDebugLogs?: boolean; @@ -20,6 +19,7 @@ type BeforeMessageWriteDeps = { shouldDebugLog: (config: DirigentConfig & DebugConfig, channelId?: string) => boolean; ensureTurnOrder: (api: OpenClawPluginApi, channelId: string) => Promise | void; resolveDiscordUserId: (api: OpenClawPluginApi, accountId: string) => string | undefined; + isMultiMessageMode: (channelId: string) => boolean; sendModeratorMessage: ( botToken: string, channelId: string, @@ -45,6 +45,7 @@ export function registerBeforeMessageWriteHook(deps: BeforeMessageWriteDeps): vo shouldDebugLog, ensureTurnOrder, resolveDiscordUserId, + isMultiMessageMode, sendModeratorMessage, discussionService, } = deps; diff --git a/plugin/hooks/before-model-resolve.ts b/plugin/hooks/before-model-resolve.ts index 868a038..48b8283 100644 --- a/plugin/hooks/before-model-resolve.ts +++ b/plugin/hooks/before-model-resolve.ts @@ -2,7 +2,6 @@ import type { OpenClawPluginApi } from "openclaw/plugin-sdk"; import { evaluateDecision, type Decision, type DirigentConfig } from "../rules.js"; import { checkTurn } from "../turn-manager.js"; import { deriveDecisionInputFromPrompt } from "../decision-input.js"; -import { isMultiMessageMode } from "../core/channel-modes.js"; type DebugConfig = { enableDebugLogs?: boolean; @@ -30,6 +29,7 @@ type BeforeModelResolveDeps = { pruneDecisionMap: () => void; shouldDebugLog: (config: DirigentConfig & DebugConfig, channelId?: string) => boolean; ensureTurnOrder: (api: OpenClawPluginApi, channelId: string) => Promise | void; + isMultiMessageMode: (channelId: string) => boolean; discussionService?: { isClosedDiscussion: (channelId: string) => boolean; }; @@ -51,6 +51,7 @@ export function registerBeforeModelResolveHook(deps: BeforeModelResolveDeps): vo pruneDecisionMap, shouldDebugLog, ensureTurnOrder, + isMultiMessageMode, discussionService, } = deps; diff --git a/plugin/hooks/message-received.ts b/plugin/hooks/message-received.ts index 76f66bf..226c437 100644 --- a/plugin/hooks/message-received.ts +++ b/plugin/hooks/message-received.ts @@ -2,7 +2,6 @@ import type { OpenClawPluginApi } from "openclaw/plugin-sdk"; import { onNewMessage, setMentionOverride, getTurnDebugInfo } from "../turn-manager.js"; import { extractDiscordChannelId } from "../channel-resolver.js"; import type { DirigentConfig } from "../rules.js"; -import { enterMultiMessageMode, exitMultiMessageMode, isMultiMessageMode } from "../core/channel-modes.js"; type DebugConfig = { enableDebugLogs?: boolean; @@ -19,6 +18,8 @@ type MessageReceivedDeps = { recordChannelAccount: (api: OpenClawPluginApi, channelId: string, accountId: string) => boolean; extractMentionedUserIds: (content: string) => string[]; buildUserIdToAccountIdMap: (api: OpenClawPluginApi) => Map; + enterMultiMessageMode: (channelId: string) => void; + exitMultiMessageMode: (channelId: string) => void; discussionService?: { maybeReplyClosedChannel: (channelId: string, senderId?: string) => Promise; }; @@ -35,6 +36,8 @@ export function registerMessageReceivedHook(deps: MessageReceivedDeps): void { recordChannelAccount, extractMentionedUserIds, buildUserIdToAccountIdMap, + enterMultiMessageMode, + exitMultiMessageMode, discussionService, } = deps; diff --git a/plugin/index.ts b/plugin/index.ts index 83dcd95..0ea62ce 100644 --- a/plugin/index.ts +++ b/plugin/index.ts @@ -18,6 +18,7 @@ import { debugCtxSummary, pickDefined, shouldDebugLog } from "./core/utils.js"; import { resolveDiscordUserId, sendModeratorMessage } from "./core/moderator-discord.js"; import { startNoReplyApi, stopNoReplyApi } from "./core/no-reply-process.js"; import { createDiscussionService } from "./core/discussion-service.js"; +import { enterMultiMessageMode, exitMultiMessageMode, isMultiMessageMode } from "./core/channel-modes.js"; import { DECISION_TTL_MS, forceNoReplySessions, @@ -49,6 +50,9 @@ function normalizePluginConfig(api: OpenClawPluginApi): NormalizedDirigentConfig noReplyPort: 8787, schedulingIdentifier: "➡️", waitIdentifier: "👤", + multiMessageStartMarker: "↗️", + multiMessageEndMarker: "↙️", + multiMessagePromptMarker: "⤵️", ...(api.pluginConfig || {}), } as NormalizedDirigentConfig; } @@ -146,6 +150,8 @@ export default { recordChannelAccount, extractMentionedUserIds, buildUserIdToAccountIdMap, + enterMultiMessageMode, + exitMultiMessageMode, discussionService, }); @@ -164,6 +170,7 @@ export default { pruneDecisionMap, shouldDebugLog, ensureTurnOrder, + isMultiMessageMode, discussionService, }); @@ -203,6 +210,7 @@ export default { shouldDebugLog, ensureTurnOrder, resolveDiscordUserId, + isMultiMessageMode, sendModeratorMessage, discussionService, }); diff --git a/plugin/rules.ts b/plugin/rules.ts index 3750562..06c5036 100644 --- a/plugin/rules.ts +++ b/plugin/rules.ts @@ -12,6 +12,12 @@ export type DirigentConfig = { schedulingIdentifier?: string; /** Wait identifier: agent ends with this when waiting for a human reply (default: 👤) */ waitIdentifier?: string; + /** Human-visible marker that enters multi-message mode for a channel (default: ↗️) */ + multiMessageStartMarker?: string; + /** Human-visible marker that exits multi-message mode for a channel (default: ↙️) */ + multiMessageEndMarker?: string; + /** Moderator marker sent after each human message while multi-message mode is active (default: ⤵️) */ + multiMessagePromptMarker?: string; noReplyProvider: string; noReplyModel: string; noReplyPort?: number; @@ -19,6 +25,14 @@ export type DirigentConfig = { moderatorBotToken?: string; }; +export type ChannelRuntimeMode = "normal" | "multi-message"; + +export type ChannelRuntimeState = { + mode: ChannelRuntimeMode; + shuffling: boolean; + lastShuffledAt?: number; +}; + export type ChannelPolicy = { listMode?: "human-list" | "agent-list"; humanList?: string[]; diff --git a/plugin/turn-manager.ts b/plugin/turn-manager.ts index 883678a..5961fef 100644 --- a/plugin/turn-manager.ts +++ b/plugin/turn-manager.ts @@ -11,7 +11,7 @@ * - If sender IS in turn order → current = next after sender */ -import { isMultiMessageMode, exitMultiMessageMode } from "./core/channel-modes.js"; +import { getChannelShuffling, isMultiMessageMode, markLastShuffled } from "./core/channel-modes.js"; export type ChannelTurnState = { /** Ordered accountIds for this channel (auto-populated, shuffled) */ @@ -379,6 +379,7 @@ export function onSpeakerDone(channelId: string, accountId: string, wasNoReply: const newOrder = reshuffleTurnOrder(channelId, state.turnOrder, prevSpeaker); if (newOrder !== state.turnOrder) { state.turnOrder = newOrder; + markLastShuffled(channelId); console.log(`[dirigent][turn-debug] reshuffled turn order for channel=${channelId} newOrder=${JSON.stringify(newOrder)}`); } }