refactor #22
@@ -22,53 +22,53 @@
|
|||||||
|
|
||||||
### A3. `plugin/tools/register-tools.ts`
|
### A3. `plugin/tools/register-tools.ts`
|
||||||
#### A3.1 扩展 `discord_channel_create`
|
#### A3.1 扩展 `discord_channel_create`
|
||||||
- [ ] 阅读当前 `discord_channel_create` 的参数定义与执行逻辑
|
- [x] 阅读当前 `discord_channel_create` 的参数定义与执行逻辑
|
||||||
- [ ] 为 `discord_channel_create` 增加可选参数 `callbackChannelId`
|
- [x] 为 `discord_channel_create` 增加可选参数 `callbackChannelId`
|
||||||
- [ ] 为 `discord_channel_create` 增加可选参数 `discussGuide`
|
- [x] 为 `discord_channel_create` 增加可选参数 `discussGuide`
|
||||||
- [ ] 添加联动校验:若传 `callbackChannelId`,则必须传 `discussGuide`
|
- [x] 添加联动校验:若传 `callbackChannelId`,则必须传 `discussGuide`
|
||||||
- [ ] 保证不传 `callbackChannelId` 时,行为与当前版本完全一致
|
- [x] 保证不传 `callbackChannelId` 时,行为与当前版本完全一致
|
||||||
- [ ] 创建成功后识别是否为 discussion 模式 channel
|
- [x] 创建成功后识别是否为 discussion 模式 channel
|
||||||
- [ ] 在 discussion 模式下调用 metadata 初始化逻辑
|
- [x] 在 discussion 模式下调用 metadata 初始化逻辑
|
||||||
- [ ] 在 discussion 模式下调用 moderator kickoff 发送逻辑
|
- [x] 在 discussion 模式下调用 moderator kickoff 发送逻辑
|
||||||
|
|
||||||
#### A3.2 注册 `discuss-callback`
|
#### A3.2 注册 `discuss-callback`
|
||||||
- [ ] 定义 `discuss-callback` 的 parameters schema
|
- [x] 定义 `discuss-callback` 的 parameters schema
|
||||||
- [ ] 注册新工具 `discuss-callback`
|
- [x] 注册新工具 `discuss-callback`
|
||||||
- [ ] 将 `discuss-callback` 执行逻辑接到 discussion service / manager
|
- [x] 将 `discuss-callback` 执行逻辑接到 discussion service / manager
|
||||||
- [ ] 为工具失败场景返回可读错误信息
|
- [x] 为工具失败场景返回可读错误信息
|
||||||
|
|
||||||
### A4. `plugin/core/` 新增 discussion metadata/service 模块
|
### A4. `plugin/core/` 新增 discussion metadata/service 模块
|
||||||
#### A4.1 新建 metadata/state 模块
|
#### A4.1 新建 metadata/state 模块
|
||||||
- [ ] 新建 discussion state 类型定义文件(如 `plugin/core/discussion-state.ts`)
|
- [x] 新建 discussion state 类型定义文件(如 `plugin/core/discussion-state.ts`)
|
||||||
- [ ] 定义 discussion metadata 类型:
|
- [x] 定义 discussion metadata 类型:
|
||||||
- [ ] `mode`
|
- [x] `mode`
|
||||||
- [ ] `discussionChannelId`
|
- [x] `discussionChannelId`
|
||||||
- [ ] `originChannelId`
|
- [x] `originChannelId`
|
||||||
- [ ] `initiatorAgentId`
|
- [x] `initiatorAgentId`
|
||||||
- [ ] `initiatorSessionId`
|
- [x] `initiatorSessionId`
|
||||||
- [ ] `discussGuide`
|
- [x] `discussGuide`
|
||||||
- [ ] `status`
|
- [x] `status`
|
||||||
- [ ] `createdAt`
|
- [x] `createdAt`
|
||||||
- [ ] `completedAt`
|
- [x] `completedAt`
|
||||||
- [ ] `summaryPath`
|
- [x] `summaryPath`
|
||||||
- [ ] 提供按 `discussionChannelId` 查询 metadata 的方法
|
- [x] 提供按 `discussionChannelId` 查询 metadata 的方法
|
||||||
- [ ] 提供创建 metadata 的方法
|
- [x] 提供创建 metadata 的方法
|
||||||
- [ ] 提供更新状态的方法
|
- [x] 提供更新状态的方法
|
||||||
- [ ] 提供关闭 discussion channel 的状态写入方法
|
- [x] 提供关闭 discussion channel 的状态写入方法
|
||||||
|
|
||||||
#### A4.2 新建 discussion service 模块
|
#### A4.2 新建 discussion service 模块
|
||||||
- [ ] 新建 discussion service(如 `plugin/core/discussion-service.ts`)
|
- [x] 新建 discussion service(如 `plugin/core/discussion-service.ts`)
|
||||||
- [ ] 封装 discussion channel 创建后的初始化逻辑
|
- [x] 封装 discussion channel 创建后的初始化逻辑
|
||||||
- [ ] 封装 callback 校验逻辑
|
- [x] 封装 callback 校验逻辑
|
||||||
- [ ] 封装 callback 成功后的收尾逻辑
|
- [x] 封装 callback 成功后的收尾逻辑
|
||||||
- [ ] 封装 origin channel moderator 通知逻辑
|
- [x] 封装 origin channel moderator 通知逻辑
|
||||||
- [ ] 封装“channel 已关闭,仅做留档使用”的统一回复逻辑
|
- [x] 封装“channel 已关闭,仅做留档使用”的统一回复逻辑
|
||||||
|
|
||||||
#### A4.3 workspace 路径校验
|
#### A4.3 workspace 路径校验
|
||||||
- [ ] 新增 path 校验辅助函数
|
- [x] 新增 path 校验辅助函数
|
||||||
- [ ] 校验 `summaryPath` 文件存在
|
- [x] 校验 `summaryPath` 文件存在
|
||||||
- [ ] 校验 `summaryPath` 位于 initiator workspace 下
|
- [x] 校验 `summaryPath` 位于 initiator workspace 下
|
||||||
- [ ] 防止 `..`、绝对路径越界、软链接绕过等路径逃逸问题
|
- [x] 防止 `..`、绝对路径越界、软链接绕过等路径逃逸问题
|
||||||
|
|
||||||
### A5. `plugin/core/moderator-discord.ts`
|
### A5. `plugin/core/moderator-discord.ts`
|
||||||
- [x] 确认当前 `sendModeratorMessage(...)` 是否已足够支撑新流程
|
- [x] 确认当前 `sendModeratorMessage(...)` 是否已足够支撑新流程
|
||||||
@@ -236,10 +236,10 @@
|
|||||||
|
|
||||||
### B2. 配置与 schema
|
### B2. 配置与 schema
|
||||||
#### B2.1 `plugin/openclaw.plugin.json`
|
#### B2.1 `plugin/openclaw.plugin.json`
|
||||||
- [ ] 增加 `multiMessageStartMarker`
|
- [x] 增加 `multiMessageStartMarker`
|
||||||
- [ ] 增加 `multiMessageEndMarker`
|
- [x] 增加 `multiMessageEndMarker`
|
||||||
- [ ] 增加 `multiMessagePromptMarker`
|
- [x] 增加 `multiMessagePromptMarker`
|
||||||
- [ ] 为新增配置设置默认值:`↗️` / `↙️` / `⤵️`
|
- [x] 为新增配置设置默认值:`↗️` / `↙️` / `⤵️`
|
||||||
- [ ] 评估是否需要增加 shuffle 默认配置项
|
- [ ] 评估是否需要增加 shuffle 默认配置项
|
||||||
|
|
||||||
#### B2.2 `plugin/rules.ts` / config 类型
|
#### B2.2 `plugin/rules.ts` / config 类型
|
||||||
@@ -248,36 +248,36 @@
|
|||||||
- [ ] 确保运行时读取配置逻辑可访问新增字段
|
- [ ] 确保运行时读取配置逻辑可访问新增字段
|
||||||
|
|
||||||
### B3. `plugin/core/` 新增 channel mode / shuffle state 模块
|
### B3. `plugin/core/` 新增 channel mode / shuffle state 模块
|
||||||
- [ ] 新增 channel mode state 模块(如 `plugin/core/channel-modes.ts`)
|
- [x] 新增 channel mode state 模块(如 `plugin/core/channel-modes.ts`)
|
||||||
- [ ] 定义 channel mode:`normal` / `multi-message`
|
- [x] 定义 channel mode:`normal` / `multi-message`
|
||||||
- [ ] 提供 `enterMultiMessageMode(channelId)`
|
- [x] 提供 `enterMultiMessageMode(channelId)`
|
||||||
- [ ] 提供 `exitMultiMessageMode(channelId)`
|
- [x] 提供 `exitMultiMessageMode(channelId)`
|
||||||
- [ ] 提供 `isMultiMessageMode(channelId)`
|
- [x] 提供 `isMultiMessageMode(channelId)`
|
||||||
- [ ] 提供 shuffle 开关状态存取方法
|
- [x] 提供 shuffle 开关状态存取方法
|
||||||
- [ ] 评估 shuffle state 是否应并入 turn-manager 内部状态
|
- [x] 评估 shuffle state 是否应并入 turn-manager 内部状态
|
||||||
|
|
||||||
### B4. `plugin/hooks/message-received.ts`
|
### B4. `plugin/hooks/message-received.ts`
|
||||||
#### B4.1 Multi-Message Mode 入口/出口
|
#### B4.1 Multi-Message Mode 入口/出口
|
||||||
- [ ] 检测 human 消息中的 multi-message start marker
|
- [.] 检测 human 消息中的 multi-message start marker
|
||||||
- [ ] start marker 命中时,将 channel 切换到 multi-message mode
|
- [.] start marker 命中时,将 channel 切换到 multi-message mode
|
||||||
- [ ] 检测 human 消息中的 multi-message end marker
|
- [.] 检测 human 消息中的 multi-message end marker
|
||||||
- [ ] end marker 命中时,将 channel 退出 multi-message mode
|
- [.] end marker 命中时,将 channel 退出 multi-message mode
|
||||||
- [ ] 避免 moderator 自己的 prompt marker 消息触发 mode 切换
|
- [.] 避免 moderator 自己的 prompt marker 消息触发 mode 切换
|
||||||
|
|
||||||
#### B4.2 Multi-Message Mode 中的 moderator 提示
|
#### B4.2 Multi-Message Mode 中的 moderator 提示
|
||||||
- [ ] 当 channel 处于 multi-message mode 时,人类每发一条消息触发 moderator prompt marker
|
- [.] 当 channel 处于 multi-message mode 时,人类每发一条消息触发 moderator prompt marker
|
||||||
- [ ] prompt marker 文案/内容使用配置项 `multiMessagePromptMarker`
|
- [.] prompt marker 文案/内容使用配置项 `multiMessagePromptMarker`
|
||||||
- [ ] 避免重复触发或回环
|
- [.] 避免重复触发或回环
|
||||||
|
|
||||||
#### B4.3 与现有 mention override 的兼容
|
#### B4.3 与现有 mention override 的兼容
|
||||||
- [ ] 明确 multi-message mode 下 human @mention 是否忽略
|
- [ ] 明确 multi-message mode 下 human @mention 是否忽略
|
||||||
- [ ] 避免 multi-message mode 与 mention override 冲突
|
- [ ] 避免 multi-message mode 与 mention override 冲突
|
||||||
|
|
||||||
### B5. `plugin/hooks/before-model-resolve.ts`
|
### B5. `plugin/hooks/before-model-resolve.ts`
|
||||||
- [ ] 当 channel 处于 multi-message mode 时,强制相关 session 走 `noReplyProvider` / `noReplyModel`
|
- [.] 当 channel 处于 multi-message mode 时,强制相关 session 走 `noReplyProvider` / `noReplyModel`
|
||||||
- [ ] 确保 multi-message mode 的 no-reply 覆盖优先于普通 turn 决策
|
- [.] 确保 multi-message mode 的 no-reply 覆盖优先于普通 turn 决策
|
||||||
- [ ] 确保退出 multi-message mode 后恢复正常 turn 逻辑
|
- [.] 确保退出 multi-message mode 后恢复正常 turn 逻辑
|
||||||
- [ ] 补充必要调试日志
|
- [.] 补充必要调试日志
|
||||||
|
|
||||||
### B6. `plugin/turn-manager.ts`
|
### B6. `plugin/turn-manager.ts`
|
||||||
#### B6.1 Multi-Message Mode 与 turn pause/resume
|
#### B6.1 Multi-Message Mode 与 turn pause/resume
|
||||||
@@ -287,22 +287,22 @@
|
|||||||
- [ ] 退出时确定下一位 speaker 的选择逻辑
|
- [ ] 退出时确定下一位 speaker 的选择逻辑
|
||||||
|
|
||||||
#### B6.2 Shuffle Mode
|
#### B6.2 Shuffle Mode
|
||||||
- [ ] 为每个 channel 增加 `shuffling` 开关状态
|
- [.] 为每个 channel 增加 `shuffling` 开关状态
|
||||||
- [ ] 识别“一轮最后一位 speaker 发言完成”的边界点
|
- [.] 识别“一轮最后位 speaker 发言完成”的边界点
|
||||||
- [ ] 在进入下一轮前执行 reshuffle
|
- [.] 在进入下一轮前执行 reshuffle
|
||||||
- [ ] 保证上一轮最后 speaker 不会成为新一轮第一位
|
- [.] 保证上一轮最后 speaker 不会成为新一轮第一位
|
||||||
- [ ] 处理单 Agent 场景
|
- [.] 处理单 Agent 场景
|
||||||
- [ ] 处理双 Agent 场景
|
- [.] 处理双 Agent 场景
|
||||||
- [ ] 处理 mention override / waiting-for-human / dormant 状态下的 reshape 边界
|
- [.] 处理 mention override / waiting-for-human / dormant 状态下的 reshape 边界
|
||||||
|
|
||||||
### B7. `plugin/commands/dirigent-command.ts`
|
### B7. `plugin/commands/dirigent-command.ts`
|
||||||
- [ ] 新增 `/turn-shuffling` 子命令
|
- [x] 新增 `/turn-shuffling` 子命令
|
||||||
- [ ] 支持:
|
- [x] 支持:
|
||||||
- [ ] `/turn-shuffling`
|
- [x] `/turn-shuffling`
|
||||||
- [ ] `/turn-shuffling on`
|
- [x] `/turn-shuffling on`
|
||||||
- [ ] `/turn-shuffling off`
|
- [x] `/turn-shuffling off`
|
||||||
- [ ] 命令返回当前 channel 的 shuffling 状态
|
- [x] 命令返回当前 channel 的 shuffling 状态
|
||||||
- [ ] 命令帮助文本补充说明
|
- [x] 命令帮助文本补充说明
|
||||||
|
|
||||||
### B8. `plugin/index.ts`
|
### B8. `plugin/index.ts`
|
||||||
- [ ] 注入 channel mode / shuffle state 模块依赖
|
- [ ] 注入 channel mode / shuffle state 模块依赖
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import type { OpenClawPluginApi } from "openclaw/plugin-sdk";
|
import type { OpenClawPluginApi } from "openclaw/plugin-sdk";
|
||||||
import { advanceTurn, getTurnDebugInfo, resetTurn } from "../turn-manager.js";
|
import { advanceTurn, getTurnDebugInfo, resetTurn } from "../turn-manager.js";
|
||||||
import type { DirigentConfig } from "../rules.js";
|
import type { DirigentConfig } from "../rules.js";
|
||||||
|
import { setChannelShuffling, getChannelShuffling } from "../core/channel-modes.js";
|
||||||
|
|
||||||
type CommandDeps = {
|
type CommandDeps = {
|
||||||
api: OpenClawPluginApi;
|
api: OpenClawPluginApi;
|
||||||
@@ -30,6 +31,7 @@ export function registerDirigentCommand(deps: CommandDeps): void {
|
|||||||
`/dirigent turn-status - Show turn-based speaking status\n` +
|
`/dirigent turn-status - Show turn-based speaking status\n` +
|
||||||
`/dirigent turn-advance - Manually advance turn\n` +
|
`/dirigent turn-advance - Manually advance turn\n` +
|
||||||
`/dirigent turn-reset - Reset turn order\n` +
|
`/dirigent turn-reset - Reset turn order\n` +
|
||||||
|
`/dirigent turn-shuffling [on|off] - Enable/disable turn order shuffling\n` +
|
||||||
`/dirigent_policy get <discordChannelId>\n` +
|
`/dirigent_policy get <discordChannelId>\n` +
|
||||||
`/dirigent_policy set <discordChannelId> <policy-json>\n` +
|
`/dirigent_policy set <discordChannelId> <policy-json>\n` +
|
||||||
`/dirigent_policy delete <discordChannelId>`,
|
`/dirigent_policy delete <discordChannelId>`,
|
||||||
@@ -60,6 +62,25 @@ export function registerDirigentCommand(deps: CommandDeps): void {
|
|||||||
return { text: JSON.stringify({ ok: true }) };
|
return { text: JSON.stringify({ ok: true }) };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (subCmd === "turn-shuffling") {
|
||||||
|
const channelId = cmdCtx.channelId;
|
||||||
|
if (!channelId) return { text: "Cannot get channel ID", isError: true };
|
||||||
|
|
||||||
|
const arg = parts[1]?.toLowerCase();
|
||||||
|
if (arg === "on") {
|
||||||
|
setChannelShuffling(channelId, true);
|
||||||
|
return { text: JSON.stringify({ ok: true, channelId, shuffling: true }) };
|
||||||
|
} else if (arg === "off") {
|
||||||
|
setChannelShuffling(channelId, false);
|
||||||
|
return { text: JSON.stringify({ ok: true, channelId, shuffling: false }) };
|
||||||
|
} else if (!arg) {
|
||||||
|
const isShuffling = getChannelShuffling(channelId);
|
||||||
|
return { text: JSON.stringify({ ok: true, channelId, shuffling: isShuffling }) };
|
||||||
|
} else {
|
||||||
|
return { text: "Invalid argument. Use: /dirigent turn-shuffling [on|off]", isError: true };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return { text: `Unknown subcommand: ${subCmd}`, isError: true };
|
return { text: `Unknown subcommand: ${subCmd}`, isError: true };
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|||||||
51
plugin/core/channel-modes.ts
Normal file
51
plugin/core/channel-modes.ts
Normal file
@@ -0,0 +1,51 @@
|
|||||||
|
export type ChannelMode = "normal" | "multi-message";
|
||||||
|
|
||||||
|
export type ChannelModesState = {
|
||||||
|
mode: ChannelMode;
|
||||||
|
shuffling: boolean;
|
||||||
|
lastShuffledAt?: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
const channelStates = new Map<string, ChannelModesState>();
|
||||||
|
|
||||||
|
export function getChannelState(channelId: string): ChannelModesState {
|
||||||
|
if (!channelStates.has(channelId)) {
|
||||||
|
channelStates.set(channelId, {
|
||||||
|
mode: "normal",
|
||||||
|
shuffling: false,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return channelStates.get(channelId)!;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function enterMultiMessageMode(channelId: string): void {
|
||||||
|
const state = getChannelState(channelId);
|
||||||
|
state.mode = "multi-message";
|
||||||
|
channelStates.set(channelId, state);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function exitMultiMessageMode(channelId: string): void {
|
||||||
|
const state = getChannelState(channelId);
|
||||||
|
state.mode = "normal";
|
||||||
|
channelStates.set(channelId, state);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function isMultiMessageMode(channelId: string): boolean {
|
||||||
|
return getChannelState(channelId).mode === "multi-message";
|
||||||
|
}
|
||||||
|
|
||||||
|
export function setChannelShuffling(channelId: string, enabled: boolean): void {
|
||||||
|
const state = getChannelState(channelId);
|
||||||
|
state.shuffling = enabled;
|
||||||
|
channelStates.set(channelId, state);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getChannelShuffling(channelId: string): boolean {
|
||||||
|
return getChannelState(channelId).shuffling;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function markLastShuffled(channelId: string): void {
|
||||||
|
const state = getChannelState(channelId);
|
||||||
|
state.lastShuffledAt = Date.now();
|
||||||
|
channelStates.set(channelId, state);
|
||||||
|
}
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
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;
|
||||||
@@ -181,15 +182,23 @@ export function registerBeforeMessageWriteHook(deps: BeforeMessageWriteDeps): vo
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (live.moderatorBotToken) {
|
if (live.moderatorBotToken) {
|
||||||
const nextUserId = resolveDiscordUserId(api, nextSpeaker);
|
if (isMultiMessageMode(channelId)) {
|
||||||
if (nextUserId) {
|
// In multi-message mode, send the prompt marker instead of scheduling identifier
|
||||||
const schedulingId = live.schedulingIdentifier || "➡️";
|
const promptMarker = live.multiMessagePromptMarker || "⤵️";
|
||||||
const handoffMsg = `<@${nextUserId}>${schedulingId}`;
|
void sendModeratorMessage(live.moderatorBotToken, channelId, promptMarker, api.logger).catch((err) => {
|
||||||
void sendModeratorMessage(live.moderatorBotToken, channelId, handoffMsg, api.logger).catch((err) => {
|
api.logger.warn(`dirigent: before_message_write multi-message prompt marker failed: ${String(err)}`);
|
||||||
api.logger.warn(`dirigent: before_message_write handoff failed: ${String(err)}`);
|
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
api.logger.warn(`dirigent: cannot resolve Discord userId for next speaker accountId=${nextSpeaker}`);
|
const nextUserId = resolveDiscordUserId(api, nextSpeaker);
|
||||||
|
if (nextUserId) {
|
||||||
|
const schedulingId = live.schedulingIdentifier || "➡️";
|
||||||
|
const handoffMsg = `<@${nextUserId}>${schedulingId}`;
|
||||||
|
void sendModeratorMessage(live.moderatorBotToken, channelId, handoffMsg, api.logger).catch((err) => {
|
||||||
|
api.logger.warn(`dirigent: before_message_write handoff failed: ${String(err)}`);
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
api.logger.warn(`dirigent: cannot resolve Discord userId for next speaker accountId=${nextSpeaker}`);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else if (hasEndSymbol) {
|
} else if (hasEndSymbol) {
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ 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;
|
||||||
@@ -99,6 +100,16 @@ export function registerBeforeModelResolveHook(deps: BeforeModelResolveDeps): vo
|
|||||||
noReply: true,
|
noReply: true,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (isMultiMessageMode(derived.channelId)) {
|
||||||
|
sessionAllowed.set(key, false);
|
||||||
|
api.logger.info(`dirigent: before_model_resolve forcing no-reply for multi-message mode channel=${derived.channelId} session=${key}`);
|
||||||
|
return {
|
||||||
|
model: ctx.model,
|
||||||
|
provider: ctx.provider,
|
||||||
|
noReply: true,
|
||||||
|
};
|
||||||
|
}
|
||||||
}
|
}
|
||||||
const resolvedAccountId = resolveAccountId(api, ctx.agentId || "");
|
const resolvedAccountId = resolveAccountId(api, ctx.agentId || "");
|
||||||
if (resolvedAccountId) {
|
if (resolvedAccountId) {
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ 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;
|
||||||
@@ -79,21 +80,38 @@ export function registerMessageReceivedHook(deps: MessageReceivedDeps): void {
|
|||||||
|
|
||||||
if (isHuman) {
|
if (isHuman) {
|
||||||
const messageContent = ((e as Record<string, unknown>).content as string) || ((e as Record<string, unknown>).text as string) || "";
|
const messageContent = ((e as Record<string, unknown>).content as string) || ((e as Record<string, unknown>).text as string) || "";
|
||||||
const mentionedUserIds = extractMentionedUserIds(messageContent);
|
|
||||||
|
// Handle multi-message mode markers
|
||||||
|
const startMarker = livePre.multiMessageStartMarker || "↗️";
|
||||||
|
const endMarker = livePre.multiMessageEndMarker || "↙️";
|
||||||
|
|
||||||
|
if (messageContent.includes(startMarker)) {
|
||||||
|
enterMultiMessageMode(preChannelId);
|
||||||
|
api.logger.info(`dirigent: entered multi-message mode channel=${preChannelId}`);
|
||||||
|
} else if (messageContent.includes(endMarker)) {
|
||||||
|
exitMultiMessageMode(preChannelId);
|
||||||
|
api.logger.info(`dirigent: exited multi-message mode channel=${preChannelId}`);
|
||||||
|
// After exiting multi-message mode, activate the turn system
|
||||||
|
onNewMessage(preChannelId, senderAccountId, isHuman);
|
||||||
|
} else {
|
||||||
|
const mentionedUserIds = extractMentionedUserIds(messageContent);
|
||||||
|
|
||||||
if (mentionedUserIds.length > 0) {
|
if (mentionedUserIds.length > 0) {
|
||||||
const userIdMap = buildUserIdToAccountIdMap(api);
|
const userIdMap = buildUserIdToAccountIdMap(api);
|
||||||
const mentionedAccountIds = mentionedUserIds.map((uid) => userIdMap.get(uid)).filter((aid): aid is string => !!aid);
|
const mentionedAccountIds = mentionedUserIds.map((uid) => userIdMap.get(uid)).filter((aid): aid is string => !!aid);
|
||||||
|
|
||||||
if (mentionedAccountIds.length > 0) {
|
if (mentionedAccountIds.length > 0) {
|
||||||
await ensureTurnOrder(api, preChannelId);
|
await ensureTurnOrder(api, preChannelId);
|
||||||
const overrideSet = setMentionOverride(preChannelId, mentionedAccountIds);
|
const overrideSet = setMentionOverride(preChannelId, mentionedAccountIds);
|
||||||
if (overrideSet) {
|
if (overrideSet) {
|
||||||
api.logger.info(
|
api.logger.info(
|
||||||
`dirigent: mention override set channel=${preChannelId} mentionedAgents=${JSON.stringify(mentionedAccountIds)}`,
|
`dirigent: mention override set channel=${preChannelId} mentionedAgents=${JSON.stringify(mentionedAccountIds)}`,
|
||||||
);
|
);
|
||||||
if (shouldDebugLog(livePre, preChannelId)) {
|
if (shouldDebugLog(livePre, preChannelId)) {
|
||||||
api.logger.info(`dirigent: turn state after override: ${JSON.stringify(getTurnDebugInfo(preChannelId))}`);
|
api.logger.info(`dirigent: turn state after override: ${JSON.stringify(getTurnDebugInfo(preChannelId))}`);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
onNewMessage(preChannelId, senderAccountId, isHuman);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
onNewMessage(preChannelId, senderAccountId, isHuman);
|
onNewMessage(preChannelId, senderAccountId, isHuman);
|
||||||
@@ -101,8 +119,6 @@ export function registerMessageReceivedHook(deps: MessageReceivedDeps): void {
|
|||||||
} else {
|
} else {
|
||||||
onNewMessage(preChannelId, senderAccountId, isHuman);
|
onNewMessage(preChannelId, senderAccountId, isHuman);
|
||||||
}
|
}
|
||||||
} else {
|
|
||||||
onNewMessage(preChannelId, senderAccountId, isHuman);
|
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
onNewMessage(preChannelId, senderAccountId, isHuman);
|
onNewMessage(preChannelId, senderAccountId, isHuman);
|
||||||
|
|||||||
@@ -25,7 +25,10 @@
|
|||||||
"enableDirigentPolicyTool": { "type": "boolean", "default": true },
|
"enableDirigentPolicyTool": { "type": "boolean", "default": true },
|
||||||
"enableDebugLogs": { "type": "boolean", "default": false },
|
"enableDebugLogs": { "type": "boolean", "default": false },
|
||||||
"debugLogChannelIds": { "type": "array", "items": { "type": "string" }, "default": [] },
|
"debugLogChannelIds": { "type": "array", "items": { "type": "string" }, "default": [] },
|
||||||
"moderatorBotToken": { "type": "string" }
|
"moderatorBotToken": { "type": "string" },
|
||||||
|
"multiMessageStartMarker": { "type": "string", "default": "↗️" },
|
||||||
|
"multiMessageEndMarker": { "type": "string", "default": "↙️" },
|
||||||
|
"multiMessagePromptMarker": { "type": "string", "default": "⤵️" }
|
||||||
},
|
},
|
||||||
"required": ["noReplyProvider", "noReplyModel"]
|
"required": ["noReplyProvider", "noReplyModel"]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,6 +11,8 @@
|
|||||||
* - 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";
|
||||||
|
|
||||||
export type ChannelTurnState = {
|
export type ChannelTurnState = {
|
||||||
/** Ordered accountIds for this channel (auto-populated, shuffled) */
|
/** Ordered accountIds for this channel (auto-populated, shuffled) */
|
||||||
turnOrder: string[];
|
turnOrder: string[];
|
||||||
@@ -46,6 +48,26 @@ function shuffleArray<T>(arr: T[]): T[] {
|
|||||||
return a;
|
return a;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function reshuffleTurnOrder(channelId: string, currentOrder: string[], lastSpeaker?: string): string[] {
|
||||||
|
const shufflingEnabled = getChannelShuffling(channelId);
|
||||||
|
if (!shufflingEnabled) return currentOrder;
|
||||||
|
|
||||||
|
const shuffled = shuffleArray(currentOrder);
|
||||||
|
|
||||||
|
// If there's a last speaker and they're in the order, ensure they're not first
|
||||||
|
if (lastSpeaker && shuffled.length > 1 && shuffled[0] === lastSpeaker) {
|
||||||
|
// Find another speaker to swap with
|
||||||
|
for (let i = 1; i < shuffled.length; i++) {
|
||||||
|
if (shuffled[i] !== lastSpeaker) {
|
||||||
|
[shuffled[0], shuffled[i]] = [shuffled[i], shuffled[0]];
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return shuffled;
|
||||||
|
}
|
||||||
|
|
||||||
// --- public API ---
|
// --- public API ---
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -176,6 +198,13 @@ export function onNewMessage(channelId: string, senderAccountId: string | undefi
|
|||||||
const state = channelTurns.get(channelId);
|
const state = channelTurns.get(channelId);
|
||||||
if (!state || state.turnOrder.length === 0) return;
|
if (!state || state.turnOrder.length === 0) return;
|
||||||
|
|
||||||
|
// Check for multi-message mode exit condition
|
||||||
|
if (isMultiMessageMode(channelId) && isHuman) {
|
||||||
|
// In multi-message mode, human messages don't trigger turn activation
|
||||||
|
// We only exit multi-message mode if the end marker is detected in a higher-level hook
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (isHuman) {
|
if (isHuman) {
|
||||||
// Human message: clear wait-for-human, restore original order if overridden, activate from first
|
// Human message: clear wait-for-human, restore original order if overridden, activate from first
|
||||||
state.waitingForHuman = false;
|
state.waitingForHuman = false;
|
||||||
@@ -330,6 +359,7 @@ export function onSpeakerDone(channelId: string, accountId: string, wasNoReply:
|
|||||||
state.noRepliedThisCycle = new Set();
|
state.noRepliedThisCycle = new Set();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const prevSpeaker = state.currentSpeaker;
|
||||||
const next = advanceTurn(channelId);
|
const next = advanceTurn(channelId);
|
||||||
|
|
||||||
// Check if override cycle completed (returned to first agent)
|
// Check if override cycle completed (returned to first agent)
|
||||||
@@ -341,6 +371,18 @@ export function onSpeakerDone(channelId: string, accountId: string, wasNoReply:
|
|||||||
return null; // go dormant after override cycle completes
|
return null; // go dormant after override cycle completes
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Check if we've completed a full cycle (all agents spoke once)
|
||||||
|
// This happens when we're back to the first agent in the turn order
|
||||||
|
const isFirstSpeakerAgain = next === state.turnOrder[0];
|
||||||
|
if (!wasNoReply && !state.overrideFirstAgent && next && isFirstSpeakerAgain && state.noRepliedThisCycle.size === 0) {
|
||||||
|
// Completed a full cycle without anyone NO_REPLYing - reshuffle if enabled
|
||||||
|
const newOrder = reshuffleTurnOrder(channelId, state.turnOrder, prevSpeaker);
|
||||||
|
if (newOrder !== state.turnOrder) {
|
||||||
|
state.turnOrder = newOrder;
|
||||||
|
console.log(`[dirigent][turn-debug] reshuffled turn order for channel=${channelId} newOrder=${JSON.stringify(newOrder)}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return next;
|
return next;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user