refactor #22

Merged
hzhang merged 33 commits from refactor into main 2026-04-10 07:49:57 +00:00
9 changed files with 60 additions and 20 deletions
Showing only changes of commit d44204fabf - Show all commits

View File

@@ -243,9 +243,9 @@
- [ ] 评估是否需要增加 shuffle 默认配置项 - [ ] 评估是否需要增加 shuffle 默认配置项
#### B2.2 `plugin/rules.ts` / config 类型 #### B2.2 `plugin/rules.ts` / config 类型
- [ ] 为 multi-message mode 相关配置补类型定义 - [x] 为 multi-message mode 相关配置补类型定义
- [ ] 为 shuffle mode 相关 channel state / config 补类型定义 - [x] 为 shuffle mode 相关 channel state / config 补类型定义
- [ ] 确保运行时读取配置逻辑可访问新增字段 - [x] 确保运行时读取配置逻辑可访问新增字段
### B3. `plugin/core/` 新增 channel mode / shuffle state 模块 ### B3. `plugin/core/` 新增 channel mode / shuffle state 模块
- [x] 新增 channel mode state 模块(如 `plugin/core/channel-modes.ts` - [x] 新增 channel mode state 模块(如 `plugin/core/channel-modes.ts`
@@ -305,14 +305,14 @@
- [x] 命令帮助文本补充说明 - [x] 命令帮助文本补充说明
### B8. `plugin/index.ts` ### B8. `plugin/index.ts`
- [ ] 注入 channel mode / shuffle state 模块依赖 - [x] 注入 channel mode / shuffle state 模块依赖
- [ ] 将新状态能力传给相关 hooks / turn-manager - [x] 将新状态能力传给相关 hooks / turn-manager
- [ ] 保持初始化关系清晰,避免 mode 逻辑散落 - [x] 保持初始化关系清晰,避免 mode 逻辑散落
### B9. moderator 消息模板 ### B9. moderator 消息模板
- [ ] 定义 multi-message mode 下的 prompt marker 发送规则 - [x] 定义 multi-message mode 下的 prompt marker 发送规则
- [ ] 明确是否需要 start / end 的 moderator 确认消息 - [x] 明确是否需要 start / end 的 moderator 确认消息
- [ ] 定义退出 multi-message mode 后的 scheduling handoff 触发格式 - [x] 定义退出 multi-message mode 后的 scheduling handoff 触发格式
### B10. 测试 ### B10. 测试
#### B10.1 Multi-Message Mode #### B10.1 Multi-Message Mode

View File

@@ -30,6 +30,9 @@ Optional:
- `channelPoliciesFile` (per-channel overrides in a standalone JSON file) - `channelPoliciesFile` (per-channel overrides in a standalone JSON file)
- `schedulingIdentifier` (default `➡️`) — moderator handoff identifier - `schedulingIdentifier` (default `➡️`) — moderator handoff identifier
- `enableDirigentPolicyTool` (default true) - `enableDirigentPolicyTool` (default true)
- `multiMessageStartMarker` (default `↗️`)
- `multiMessageEndMarker` (default `↙️`)
- `multiMessagePromptMarker` (default `⤵️`)
Unified optional tool: Unified optional tool:
- `dirigent_tools` - `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. 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) ## Slash command (Discord)
``` ```
@@ -66,6 +78,9 @@ This is a non-semantic scheduling message. The scheduling identifier (`➡️` b
/dirigent turn-status /dirigent turn-status
/dirigent turn-advance /dirigent turn-advance
/dirigent turn-reset /dirigent turn-reset
/dirigent turn-shuffling
/dirigent turn-shuffling on
/dirigent turn-shuffling off
``` ```
Debug logging: Debug logging:

View File

@@ -1,10 +1,7 @@
export type ChannelMode = "normal" | "multi-message"; import type { ChannelRuntimeMode, ChannelRuntimeState } from "../rules.js";
export type ChannelModesState = { export type ChannelMode = ChannelRuntimeMode;
mode: ChannelMode; export type ChannelModesState = ChannelRuntimeState;
shuffling: boolean;
lastShuffledAt?: number;
};
const channelStates = new Map<string, ChannelModesState>(); const channelStates = new Map<string, ChannelModesState>();
@@ -48,4 +45,4 @@ export function markLastShuffled(channelId: string): void {
const state = getChannelState(channelId); const state = getChannelState(channelId);
state.lastShuffledAt = Date.now(); state.lastShuffledAt = Date.now();
channelStates.set(channelId, state); channelStates.set(channelId, state);
} }

View File

@@ -1,7 +1,6 @@
import type { OpenClawPluginApi } from "openclaw/plugin-sdk"; import type { OpenClawPluginApi } from "openclaw/plugin-sdk";
import { resolvePolicy, type DirigentConfig } from "../rules.js"; import { resolvePolicy, type DirigentConfig } from "../rules.js";
import { getTurnDebugInfo, onSpeakerDone, setWaitingForHuman } from "../turn-manager.js"; import { getTurnDebugInfo, onSpeakerDone, setWaitingForHuman } from "../turn-manager.js";
import { isMultiMessageMode } from "../core/channel-modes.js";
type DebugConfig = { type DebugConfig = {
enableDebugLogs?: boolean; enableDebugLogs?: boolean;
@@ -20,6 +19,7 @@ type BeforeMessageWriteDeps = {
shouldDebugLog: (config: DirigentConfig & DebugConfig, channelId?: string) => boolean; shouldDebugLog: (config: DirigentConfig & DebugConfig, channelId?: string) => boolean;
ensureTurnOrder: (api: OpenClawPluginApi, channelId: string) => Promise<void> | void; ensureTurnOrder: (api: OpenClawPluginApi, channelId: string) => Promise<void> | void;
resolveDiscordUserId: (api: OpenClawPluginApi, accountId: string) => string | undefined; resolveDiscordUserId: (api: OpenClawPluginApi, accountId: string) => string | undefined;
isMultiMessageMode: (channelId: string) => boolean;
sendModeratorMessage: ( sendModeratorMessage: (
botToken: string, botToken: string,
channelId: string, channelId: string,
@@ -45,6 +45,7 @@ export function registerBeforeMessageWriteHook(deps: BeforeMessageWriteDeps): vo
shouldDebugLog, shouldDebugLog,
ensureTurnOrder, ensureTurnOrder,
resolveDiscordUserId, resolveDiscordUserId,
isMultiMessageMode,
sendModeratorMessage, sendModeratorMessage,
discussionService, discussionService,
} = deps; } = deps;

View File

@@ -2,7 +2,6 @@ import type { OpenClawPluginApi } from "openclaw/plugin-sdk";
import { evaluateDecision, type Decision, type DirigentConfig } from "../rules.js"; import { evaluateDecision, type Decision, type DirigentConfig } from "../rules.js";
import { checkTurn } from "../turn-manager.js"; import { checkTurn } from "../turn-manager.js";
import { deriveDecisionInputFromPrompt } from "../decision-input.js"; import { deriveDecisionInputFromPrompt } from "../decision-input.js";
import { isMultiMessageMode } from "../core/channel-modes.js";
type DebugConfig = { type DebugConfig = {
enableDebugLogs?: boolean; enableDebugLogs?: boolean;
@@ -30,6 +29,7 @@ type BeforeModelResolveDeps = {
pruneDecisionMap: () => void; pruneDecisionMap: () => void;
shouldDebugLog: (config: DirigentConfig & DebugConfig, channelId?: string) => boolean; shouldDebugLog: (config: DirigentConfig & DebugConfig, channelId?: string) => boolean;
ensureTurnOrder: (api: OpenClawPluginApi, channelId: string) => Promise<void> | void; ensureTurnOrder: (api: OpenClawPluginApi, channelId: string) => Promise<void> | void;
isMultiMessageMode: (channelId: string) => boolean;
discussionService?: { discussionService?: {
isClosedDiscussion: (channelId: string) => boolean; isClosedDiscussion: (channelId: string) => boolean;
}; };
@@ -51,6 +51,7 @@ export function registerBeforeModelResolveHook(deps: BeforeModelResolveDeps): vo
pruneDecisionMap, pruneDecisionMap,
shouldDebugLog, shouldDebugLog,
ensureTurnOrder, ensureTurnOrder,
isMultiMessageMode,
discussionService, discussionService,
} = deps; } = deps;

View File

@@ -2,7 +2,6 @@ import type { OpenClawPluginApi } from "openclaw/plugin-sdk";
import { onNewMessage, setMentionOverride, getTurnDebugInfo } from "../turn-manager.js"; import { onNewMessage, setMentionOverride, getTurnDebugInfo } from "../turn-manager.js";
import { extractDiscordChannelId } from "../channel-resolver.js"; import { extractDiscordChannelId } from "../channel-resolver.js";
import type { DirigentConfig } from "../rules.js"; import type { DirigentConfig } from "../rules.js";
import { enterMultiMessageMode, exitMultiMessageMode, isMultiMessageMode } from "../core/channel-modes.js";
type DebugConfig = { type DebugConfig = {
enableDebugLogs?: boolean; enableDebugLogs?: boolean;
@@ -19,6 +18,8 @@ type MessageReceivedDeps = {
recordChannelAccount: (api: OpenClawPluginApi, channelId: string, accountId: string) => boolean; recordChannelAccount: (api: OpenClawPluginApi, channelId: string, accountId: string) => boolean;
extractMentionedUserIds: (content: string) => string[]; extractMentionedUserIds: (content: string) => string[];
buildUserIdToAccountIdMap: (api: OpenClawPluginApi) => Map<string, string>; buildUserIdToAccountIdMap: (api: OpenClawPluginApi) => Map<string, string>;
enterMultiMessageMode: (channelId: string) => void;
exitMultiMessageMode: (channelId: string) => void;
discussionService?: { discussionService?: {
maybeReplyClosedChannel: (channelId: string, senderId?: string) => Promise<boolean>; maybeReplyClosedChannel: (channelId: string, senderId?: string) => Promise<boolean>;
}; };
@@ -35,6 +36,8 @@ export function registerMessageReceivedHook(deps: MessageReceivedDeps): void {
recordChannelAccount, recordChannelAccount,
extractMentionedUserIds, extractMentionedUserIds,
buildUserIdToAccountIdMap, buildUserIdToAccountIdMap,
enterMultiMessageMode,
exitMultiMessageMode,
discussionService, discussionService,
} = deps; } = deps;

View File

@@ -18,6 +18,7 @@ import { debugCtxSummary, pickDefined, shouldDebugLog } from "./core/utils.js";
import { resolveDiscordUserId, sendModeratorMessage } from "./core/moderator-discord.js"; import { resolveDiscordUserId, sendModeratorMessage } from "./core/moderator-discord.js";
import { startNoReplyApi, stopNoReplyApi } from "./core/no-reply-process.js"; import { startNoReplyApi, stopNoReplyApi } from "./core/no-reply-process.js";
import { createDiscussionService } from "./core/discussion-service.js"; import { createDiscussionService } from "./core/discussion-service.js";
import { enterMultiMessageMode, exitMultiMessageMode, isMultiMessageMode } from "./core/channel-modes.js";
import { import {
DECISION_TTL_MS, DECISION_TTL_MS,
forceNoReplySessions, forceNoReplySessions,
@@ -49,6 +50,9 @@ function normalizePluginConfig(api: OpenClawPluginApi): NormalizedDirigentConfig
noReplyPort: 8787, noReplyPort: 8787,
schedulingIdentifier: "➡️", schedulingIdentifier: "➡️",
waitIdentifier: "👤", waitIdentifier: "👤",
multiMessageStartMarker: "↗️",
multiMessageEndMarker: "↙️",
multiMessagePromptMarker: "⤵️",
...(api.pluginConfig || {}), ...(api.pluginConfig || {}),
} as NormalizedDirigentConfig; } as NormalizedDirigentConfig;
} }
@@ -146,6 +150,8 @@ export default {
recordChannelAccount, recordChannelAccount,
extractMentionedUserIds, extractMentionedUserIds,
buildUserIdToAccountIdMap, buildUserIdToAccountIdMap,
enterMultiMessageMode,
exitMultiMessageMode,
discussionService, discussionService,
}); });
@@ -164,6 +170,7 @@ export default {
pruneDecisionMap, pruneDecisionMap,
shouldDebugLog, shouldDebugLog,
ensureTurnOrder, ensureTurnOrder,
isMultiMessageMode,
discussionService, discussionService,
}); });
@@ -203,6 +210,7 @@ export default {
shouldDebugLog, shouldDebugLog,
ensureTurnOrder, ensureTurnOrder,
resolveDiscordUserId, resolveDiscordUserId,
isMultiMessageMode,
sendModeratorMessage, sendModeratorMessage,
discussionService, discussionService,
}); });

View File

@@ -12,6 +12,12 @@ export type DirigentConfig = {
schedulingIdentifier?: string; schedulingIdentifier?: string;
/** Wait identifier: agent ends with this when waiting for a human reply (default: 👤) */ /** Wait identifier: agent ends with this when waiting for a human reply (default: 👤) */
waitIdentifier?: string; 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; noReplyProvider: string;
noReplyModel: string; noReplyModel: string;
noReplyPort?: number; noReplyPort?: number;
@@ -19,6 +25,14 @@ export type DirigentConfig = {
moderatorBotToken?: string; moderatorBotToken?: string;
}; };
export type ChannelRuntimeMode = "normal" | "multi-message";
export type ChannelRuntimeState = {
mode: ChannelRuntimeMode;
shuffling: boolean;
lastShuffledAt?: number;
};
export type ChannelPolicy = { export type ChannelPolicy = {
listMode?: "human-list" | "agent-list"; listMode?: "human-list" | "agent-list";
humanList?: string[]; humanList?: string[];

View File

@@ -11,7 +11,7 @@
* - If sender IS in turn order → current = next after sender * - 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 = { export type ChannelTurnState = {
/** Ordered accountIds for this channel (auto-populated, shuffled) */ /** 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); const newOrder = reshuffleTurnOrder(channelId, state.turnOrder, prevSpeaker);
if (newOrder !== state.turnOrder) { if (newOrder !== state.turnOrder) {
state.turnOrder = newOrder; state.turnOrder = newOrder;
markLastShuffled(channelId);
console.log(`[dirigent][turn-debug] reshuffled turn order for channel=${channelId} newOrder=${JSON.stringify(newOrder)}`); console.log(`[dirigent][turn-debug] reshuffled turn order for channel=${channelId} newOrder=${JSON.stringify(newOrder)}`);
} }
} }