Refine discussion moderator messaging flow
This commit is contained in:
@@ -195,7 +195,7 @@ moderator bot 的工作流完全不使用模型,所有输出均由模板字符
|
|||||||
|
|
||||||
当检测到某 channel 为讨论模式 channel 时,moderator bot 自动发 kickoff message。
|
当检测到某 channel 为讨论模式 channel 时,moderator bot 自动发 kickoff message。
|
||||||
|
|
||||||
建议内容结构如下:
|
建议内容结构如下(当前实现中统一收敛在 `plugin/core/discussion-messages.ts`):
|
||||||
|
|
||||||
```text
|
```text
|
||||||
[Discussion Started]
|
[Discussion Started]
|
||||||
@@ -210,7 +210,7 @@ Instructions:
|
|||||||
2. Work toward a concrete conclusion.
|
2. Work toward a concrete conclusion.
|
||||||
3. When the initiator decides the goal has been achieved, the initiator must:
|
3. When the initiator decides the goal has been achieved, the initiator must:
|
||||||
- write a summary document to a file
|
- write a summary document to a file
|
||||||
- call the tool: discuss-callback
|
- call the tool: discuss-callback(summaryPath)
|
||||||
- provide the summary document path
|
- provide the summary document path
|
||||||
|
|
||||||
Completion rule:
|
Completion rule:
|
||||||
@@ -331,7 +331,7 @@ Further discussion in this channel is ignored.
|
|||||||
- 提供结果文档路径
|
- 提供结果文档路径
|
||||||
- 用新消息唤醒原工作 channel 上的 Agent 继续执行
|
- 用新消息唤醒原工作 channel 上的 Agent 继续执行
|
||||||
|
|
||||||
建议模板:
|
建议模板(当前实现中统一收敛在 `plugin/core/discussion-messages.ts`):
|
||||||
|
|
||||||
```text
|
```text
|
||||||
[Discussion Result Ready]
|
[Discussion Result Ready]
|
||||||
|
|||||||
@@ -71,13 +71,13 @@
|
|||||||
- [ ] 防止 `..`、绝对路径越界、软链接绕过等路径逃逸问题
|
- [ ] 防止 `..`、绝对路径越界、软链接绕过等路径逃逸问题
|
||||||
|
|
||||||
### A5. `plugin/core/moderator-discord.ts`
|
### A5. `plugin/core/moderator-discord.ts`
|
||||||
- [ ] 确认当前 `sendModeratorMessage(...)` 是否已足够支撑新流程
|
- [x] 确认当前 `sendModeratorMessage(...)` 是否已足够支撑新流程
|
||||||
- [ ] 如有必要,补充统一错误日志和返回值处理
|
- [x] 如有必要,补充统一错误日志和返回值处理
|
||||||
- [ ] 确认可被 discussion service 复用发送:
|
- [x] 确认可被 discussion service 复用发送:
|
||||||
- [ ] kickoff message
|
- [x] kickoff message
|
||||||
- [ ] idle reminder
|
- [x] idle reminder
|
||||||
- [ ] callback 完成通知
|
- [x] callback 完成通知
|
||||||
- [ ] channel closed 固定回复
|
- [x] channel closed 固定回复
|
||||||
|
|
||||||
### A6. `plugin/turn-manager.ts`
|
### A6. `plugin/turn-manager.ts`
|
||||||
#### A6.1 理解现有轮转机制
|
#### A6.1 理解现有轮转机制
|
||||||
@@ -146,10 +146,10 @@
|
|||||||
|
|
||||||
### A10. moderator 消息模板整理
|
### A10. moderator 消息模板整理
|
||||||
#### A10.1 kickoff message
|
#### A10.1 kickoff message
|
||||||
- [ ] 定稿 discussion started 模板
|
- [x] 定稿 discussion started 模板
|
||||||
- [ ] 模板中包含 `discussGuide`
|
- [x] 模板中包含 `discussGuide`
|
||||||
- [ ] 模板中明确 initiator 结束责任
|
- [x] 模板中明确 initiator 结束责任
|
||||||
- [ ] 模板中明确 `discuss-callback(summaryPath)` 调用要求
|
- [x] 模板中明确 `discuss-callback(summaryPath)` 调用要求
|
||||||
|
|
||||||
#### A10.2 idle reminder
|
#### A10.2 idle reminder
|
||||||
- [ ] 定稿 discussion idle 模板
|
- [ ] 定稿 discussion idle 模板
|
||||||
@@ -157,10 +157,10 @@
|
|||||||
- [ ] 避免提醒文案歧义或像自动总结器
|
- [ ] 避免提醒文案歧义或像自动总结器
|
||||||
|
|
||||||
#### A10.3 origin callback message
|
#### A10.3 origin callback message
|
||||||
- [ ] 定稿发回原工作 channel 的结果通知模板
|
- [x] 定稿发回原工作 channel 的结果通知模板
|
||||||
- [ ] 模板中包含 `summaryPath`
|
- [x] 模板中包含 `summaryPath`
|
||||||
- [ ] 模板中包含来源 discussion channel
|
- [x] 模板中包含来源 discussion channel
|
||||||
- [ ] 模板中明确“继续基于该总结文件推进原任务”
|
- [x] 模板中明确“继续基于该总结文件推进原任务”
|
||||||
|
|
||||||
#### A10.4 closed reply
|
#### A10.4 closed reply
|
||||||
- [ ] 定稿 closed channel 固定回复模板
|
- [ ] 定稿 closed channel 固定回复模板
|
||||||
|
|||||||
69
plugin/core/discussion-messages.ts
Normal file
69
plugin/core/discussion-messages.ts
Normal file
@@ -0,0 +1,69 @@
|
|||||||
|
export function buildDiscussionKickoffMessage(discussGuide: string): string {
|
||||||
|
return [
|
||||||
|
"[Discussion Started]",
|
||||||
|
"",
|
||||||
|
"This channel was created for a temporary agent discussion.",
|
||||||
|
"",
|
||||||
|
"Goal:",
|
||||||
|
discussGuide,
|
||||||
|
"",
|
||||||
|
"Instructions:",
|
||||||
|
"1. Discuss only the topic above.",
|
||||||
|
"2. Work toward a concrete conclusion.",
|
||||||
|
"3. When the initiator decides the goal has been achieved, the initiator must:",
|
||||||
|
" - write a summary document to a file",
|
||||||
|
" - call the tool: discuss-callback(summaryPath)",
|
||||||
|
" - provide the summary document path",
|
||||||
|
"",
|
||||||
|
"Completion rule:",
|
||||||
|
"Only the discussion initiator may finish this discussion.",
|
||||||
|
"",
|
||||||
|
"After callback:",
|
||||||
|
"- this channel will be closed",
|
||||||
|
"- further discussion messages will be ignored",
|
||||||
|
"- this channel will remain only for archive/reference",
|
||||||
|
"- the original work channel will be notified with the summary file path",
|
||||||
|
].join("\n");
|
||||||
|
}
|
||||||
|
|
||||||
|
export function buildDiscussionIdleReminderMessage(): string {
|
||||||
|
return [
|
||||||
|
"[Discussion Idle]",
|
||||||
|
"",
|
||||||
|
"No agent responded in the latest discussion round.",
|
||||||
|
"If the discussion goal has been achieved, the initiator should now:",
|
||||||
|
"1. write the discussion summary to a file in the workspace",
|
||||||
|
"2. call discuss-callback with the summary file path",
|
||||||
|
"",
|
||||||
|
"If more discussion is still needed, continue the discussion in this channel.",
|
||||||
|
].join("\n");
|
||||||
|
}
|
||||||
|
|
||||||
|
export function buildDiscussionClosedMessage(): string {
|
||||||
|
return [
|
||||||
|
"[Channel Closed]",
|
||||||
|
"",
|
||||||
|
"This discussion channel has been closed.",
|
||||||
|
"It is now kept for archive/reference only.",
|
||||||
|
"Further discussion in this channel is ignored.",
|
||||||
|
].join("\n");
|
||||||
|
}
|
||||||
|
|
||||||
|
export function buildDiscussionOriginCallbackMessage(summaryPath: string, discussionChannelId: string): string {
|
||||||
|
return [
|
||||||
|
"[Discussion Result Ready]",
|
||||||
|
"",
|
||||||
|
"A temporary discussion has completed.",
|
||||||
|
"",
|
||||||
|
"Summary file:",
|
||||||
|
summaryPath,
|
||||||
|
"",
|
||||||
|
"Source discussion channel:",
|
||||||
|
`<#${discussionChannelId}>`,
|
||||||
|
"",
|
||||||
|
"Status:",
|
||||||
|
"completed",
|
||||||
|
"",
|
||||||
|
"Continue the original task using the summary file above.",
|
||||||
|
].join("\n");
|
||||||
|
}
|
||||||
@@ -2,6 +2,12 @@ import fs from "node:fs";
|
|||||||
import path from "node:path";
|
import path from "node:path";
|
||||||
import type { OpenClawPluginApi } from "openclaw/plugin-sdk";
|
import type { OpenClawPluginApi } from "openclaw/plugin-sdk";
|
||||||
import { closeDiscussion, createDiscussion, getDiscussion, isDiscussionClosed, markDiscussionIdleReminderSent, type DiscussionMetadata } from "./discussion-state.js";
|
import { closeDiscussion, createDiscussion, getDiscussion, isDiscussionClosed, markDiscussionIdleReminderSent, type DiscussionMetadata } from "./discussion-state.js";
|
||||||
|
import {
|
||||||
|
buildDiscussionClosedMessage,
|
||||||
|
buildDiscussionIdleReminderMessage,
|
||||||
|
buildDiscussionKickoffMessage,
|
||||||
|
buildDiscussionOriginCallbackMessage,
|
||||||
|
} from "./discussion-messages.js";
|
||||||
import { sendModeratorMessage } from "./moderator-discord.js";
|
import { sendModeratorMessage } from "./moderator-discord.js";
|
||||||
|
|
||||||
type DiscussionServiceDeps = {
|
type DiscussionServiceDeps = {
|
||||||
@@ -15,76 +21,6 @@ type DiscussionServiceDeps = {
|
|||||||
export function createDiscussionService(deps: DiscussionServiceDeps) {
|
export function createDiscussionService(deps: DiscussionServiceDeps) {
|
||||||
const workspaceRoot = path.resolve(deps.workspaceRoot || process.cwd());
|
const workspaceRoot = path.resolve(deps.workspaceRoot || process.cwd());
|
||||||
|
|
||||||
function buildKickoffMessage(discussGuide: string): string {
|
|
||||||
return [
|
|
||||||
"[Discussion Started]",
|
|
||||||
"",
|
|
||||||
"This channel was created for a temporary agent discussion.",
|
|
||||||
"",
|
|
||||||
"Goal:",
|
|
||||||
discussGuide,
|
|
||||||
"",
|
|
||||||
"Instructions:",
|
|
||||||
"1. Discuss only the topic above.",
|
|
||||||
"2. Work toward a concrete conclusion.",
|
|
||||||
"3. When the initiator decides the goal has been achieved, the initiator must:",
|
|
||||||
" - write a summary document to a file",
|
|
||||||
" - call the tool: discuss-callback",
|
|
||||||
" - provide the summary document path",
|
|
||||||
"",
|
|
||||||
"Completion rule:",
|
|
||||||
"Only the discussion initiator may finish this discussion.",
|
|
||||||
"",
|
|
||||||
"After callback:",
|
|
||||||
"- this channel will be closed",
|
|
||||||
"- further discussion messages will be ignored",
|
|
||||||
"- this channel will remain only for archive/reference",
|
|
||||||
"- the original work channel will be notified with the summary file path",
|
|
||||||
].join("\n");
|
|
||||||
}
|
|
||||||
|
|
||||||
function buildIdleReminderMessage(): string {
|
|
||||||
return [
|
|
||||||
"[Discussion Idle]",
|
|
||||||
"",
|
|
||||||
"No agent responded in the latest discussion round.",
|
|
||||||
"If the discussion goal has been achieved, the initiator should now:",
|
|
||||||
"1. write the discussion summary to a file in the workspace",
|
|
||||||
"2. call discuss-callback with the summary file path",
|
|
||||||
"",
|
|
||||||
"If more discussion is still needed, continue the discussion in this channel.",
|
|
||||||
].join("\n");
|
|
||||||
}
|
|
||||||
|
|
||||||
function buildClosedMessage(): string {
|
|
||||||
return [
|
|
||||||
"[Channel Closed]",
|
|
||||||
"",
|
|
||||||
"This discussion channel has been closed.",
|
|
||||||
"It is now kept for archive/reference only.",
|
|
||||||
"Further discussion in this channel is ignored.",
|
|
||||||
].join("\n");
|
|
||||||
}
|
|
||||||
|
|
||||||
function buildOriginCallbackMessage(summaryPath: string, discussionChannelId: string): string {
|
|
||||||
return [
|
|
||||||
"[Discussion Result Ready]",
|
|
||||||
"",
|
|
||||||
"A temporary discussion has completed.",
|
|
||||||
"",
|
|
||||||
"Summary file:",
|
|
||||||
summaryPath,
|
|
||||||
"",
|
|
||||||
"Source discussion channel:",
|
|
||||||
`<#${discussionChannelId}>`,
|
|
||||||
"",
|
|
||||||
"Status:",
|
|
||||||
"completed",
|
|
||||||
"",
|
|
||||||
"Continue the original task using the summary file above.",
|
|
||||||
].join("\n");
|
|
||||||
}
|
|
||||||
|
|
||||||
async function initDiscussion(params: {
|
async function initDiscussion(params: {
|
||||||
discussionChannelId: string;
|
discussionChannelId: string;
|
||||||
originChannelId: string;
|
originChannelId: string;
|
||||||
@@ -104,7 +40,15 @@ export function createDiscussionService(deps: DiscussionServiceDeps) {
|
|||||||
});
|
});
|
||||||
|
|
||||||
if (deps.moderatorBotToken) {
|
if (deps.moderatorBotToken) {
|
||||||
await sendModeratorMessage(deps.moderatorBotToken, params.discussionChannelId, buildKickoffMessage(params.discussGuide), deps.api.logger);
|
const result = await sendModeratorMessage(
|
||||||
|
deps.moderatorBotToken,
|
||||||
|
params.discussionChannelId,
|
||||||
|
buildDiscussionKickoffMessage(params.discussGuide),
|
||||||
|
deps.api.logger,
|
||||||
|
);
|
||||||
|
if (!result.ok) {
|
||||||
|
deps.api.logger.warn(`dirigent: discussion kickoff message failed channel=${params.discussionChannelId} error=${result.error}`);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return metadata;
|
return metadata;
|
||||||
@@ -115,7 +59,15 @@ export function createDiscussionService(deps: DiscussionServiceDeps) {
|
|||||||
if (!metadata || metadata.status !== "active" || metadata.idleReminderSent) return;
|
if (!metadata || metadata.status !== "active" || metadata.idleReminderSent) return;
|
||||||
markDiscussionIdleReminderSent(channelId);
|
markDiscussionIdleReminderSent(channelId);
|
||||||
if (deps.moderatorBotToken) {
|
if (deps.moderatorBotToken) {
|
||||||
await sendModeratorMessage(deps.moderatorBotToken, channelId, buildIdleReminderMessage(), deps.api.logger);
|
const result = await sendModeratorMessage(
|
||||||
|
deps.moderatorBotToken,
|
||||||
|
channelId,
|
||||||
|
buildDiscussionIdleReminderMessage(),
|
||||||
|
deps.api.logger,
|
||||||
|
);
|
||||||
|
if (!result.ok) {
|
||||||
|
deps.api.logger.warn(`dirigent: discussion idle reminder failed channel=${channelId} error=${result.error}`);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -163,7 +115,17 @@ export function createDiscussionService(deps: DiscussionServiceDeps) {
|
|||||||
deps.forceNoReplyForSession(metadata.initiatorSessionId);
|
deps.forceNoReplyForSession(metadata.initiatorSessionId);
|
||||||
|
|
||||||
if (deps.moderatorBotToken) {
|
if (deps.moderatorBotToken) {
|
||||||
await sendModeratorMessage(deps.moderatorBotToken, metadata.originChannelId, buildOriginCallbackMessage(realPath, metadata.discussionChannelId), deps.api.logger);
|
const result = await sendModeratorMessage(
|
||||||
|
deps.moderatorBotToken,
|
||||||
|
metadata.originChannelId,
|
||||||
|
buildDiscussionOriginCallbackMessage(realPath, metadata.discussionChannelId),
|
||||||
|
deps.api.logger,
|
||||||
|
);
|
||||||
|
if (!result.ok) {
|
||||||
|
deps.api.logger.warn(
|
||||||
|
`dirigent: discussion origin callback notification failed originChannel=${metadata.originChannelId} error=${result.error}`,
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return { ok: true, summaryPath: realPath, discussion: closed };
|
return { ok: true, summaryPath: realPath, discussion: closed };
|
||||||
@@ -174,7 +136,10 @@ export function createDiscussionService(deps: DiscussionServiceDeps) {
|
|||||||
if (!metadata || metadata.status !== "closed") return false;
|
if (!metadata || metadata.status !== "closed") return false;
|
||||||
if (deps.moderatorUserId && senderId && senderId === deps.moderatorUserId) return true;
|
if (deps.moderatorUserId && senderId && senderId === deps.moderatorUserId) return true;
|
||||||
if (!deps.moderatorBotToken) return true;
|
if (!deps.moderatorBotToken) return true;
|
||||||
await sendModeratorMessage(deps.moderatorBotToken, channelId, buildClosedMessage(), deps.api.logger);
|
const result = await sendModeratorMessage(deps.moderatorBotToken, channelId, buildDiscussionClosedMessage(), deps.api.logger);
|
||||||
|
if (!result.ok) {
|
||||||
|
deps.api.logger.warn(`dirigent: discussion closed reply failed channel=${channelId} error=${result.error}`);
|
||||||
|
}
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -20,12 +20,16 @@ export function resolveDiscordUserId(api: OpenClawPluginApi, accountId: string):
|
|||||||
return userIdFromToken(acct.token);
|
return userIdFromToken(acct.token);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type ModeratorMessageResult =
|
||||||
|
| { ok: true; status: number; channelId: string; messageId?: string }
|
||||||
|
| { ok: false; status?: number; channelId: string; error: string };
|
||||||
|
|
||||||
export async function sendModeratorMessage(
|
export async function sendModeratorMessage(
|
||||||
token: string,
|
token: string,
|
||||||
channelId: string,
|
channelId: string,
|
||||||
content: string,
|
content: string,
|
||||||
logger: { info: (msg: string) => void; warn: (msg: string) => void },
|
logger: { info: (msg: string) => void; warn: (msg: string) => void },
|
||||||
): Promise<boolean> {
|
): Promise<ModeratorMessageResult> {
|
||||||
try {
|
try {
|
||||||
const r = await fetch(`https://discord.com/api/v10/channels/${channelId}/messages`, {
|
const r = await fetch(`https://discord.com/api/v10/channels/${channelId}/messages`, {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
@@ -35,15 +39,27 @@ export async function sendModeratorMessage(
|
|||||||
},
|
},
|
||||||
body: JSON.stringify({ content }),
|
body: JSON.stringify({ content }),
|
||||||
});
|
});
|
||||||
if (!r.ok) {
|
|
||||||
const text = await r.text();
|
const text = await r.text();
|
||||||
logger.warn(`dirigent: moderator send failed (${r.status}): ${text}`);
|
let json: Record<string, unknown> | null = null;
|
||||||
return false;
|
try {
|
||||||
|
json = text ? (JSON.parse(text) as Record<string, unknown>) : null;
|
||||||
|
} catch {
|
||||||
|
json = null;
|
||||||
}
|
}
|
||||||
logger.info(`dirigent: moderator message sent to channel=${channelId}`);
|
|
||||||
return true;
|
if (!r.ok) {
|
||||||
|
const error = `discord api error (${r.status}): ${text || "<empty response>"}`;
|
||||||
|
logger.warn(`dirigent: moderator send failed channel=${channelId} ${error}`);
|
||||||
|
return { ok: false, status: r.status, channelId, error };
|
||||||
|
}
|
||||||
|
|
||||||
|
const messageId = typeof json?.id === "string" ? json.id : undefined;
|
||||||
|
logger.info(`dirigent: moderator message sent to channel=${channelId} messageId=${messageId ?? "unknown"}`);
|
||||||
|
return { ok: true, status: r.status, channelId, messageId };
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
logger.warn(`dirigent: moderator send error: ${String(err)}`);
|
const error = String(err);
|
||||||
return false;
|
logger.warn(`dirigent: moderator send error channel=${channelId}: ${error}`);
|
||||||
|
return { ok: false, channelId, error };
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -24,7 +24,7 @@ type BeforeMessageWriteDeps = {
|
|||||||
channelId: string,
|
channelId: string,
|
||||||
content: string,
|
content: string,
|
||||||
logger: { info: (m: string) => void; warn: (m: string) => void },
|
logger: { info: (m: string) => void; warn: (m: string) => void },
|
||||||
) => Promise<void>;
|
) => Promise<unknown>;
|
||||||
discussionService?: {
|
discussionService?: {
|
||||||
maybeSendIdleReminder: (channelId: string) => Promise<void>;
|
maybeSendIdleReminder: (channelId: string) => Promise<void>;
|
||||||
getDiscussion: (channelId: string) => { status: string } | undefined;
|
getDiscussion: (channelId: string) => { status: string } | undefined;
|
||||||
|
|||||||
@@ -22,7 +22,7 @@ type MessageSentDeps = {
|
|||||||
channelId: string,
|
channelId: string,
|
||||||
content: string,
|
content: string,
|
||||||
logger: { info: (m: string) => void; warn: (m: string) => void },
|
logger: { info: (m: string) => void; warn: (m: string) => void },
|
||||||
) => Promise<void>;
|
) => Promise<unknown>;
|
||||||
};
|
};
|
||||||
|
|
||||||
export function registerMessageSentHook(deps: MessageSentDeps): void {
|
export function registerMessageSentHook(deps: MessageSentDeps): void {
|
||||||
|
|||||||
Reference in New Issue
Block a user