refactor #22
@@ -255,10 +255,11 @@ After callback:
|
||||
[Discussion Idle]
|
||||
|
||||
No agent responded in the latest discussion round.
|
||||
If the discussion goal has been achieved, the initiator should now:
|
||||
If the discussion goal has already 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
|
||||
|
||||
This reminder does not mean the discussion was automatically summarized or closed.
|
||||
If more discussion is still needed, continue the discussion in this channel.
|
||||
```
|
||||
|
||||
@@ -284,6 +285,7 @@ If more discussion is still needed, continue the discussion in this channel.
|
||||
This discussion channel has been closed.
|
||||
It is now kept for archive/reference only.
|
||||
Further discussion in this channel is ignored.
|
||||
If follow-up work is needed, continue it from the origin work channel instead.
|
||||
```
|
||||
|
||||
这部分实现明确采用已有“在指定 session 上临时覆盖为 no-reply 模型”的方式,而不是修改 Agent 的全局默认模型。
|
||||
|
||||
@@ -152,9 +152,9 @@
|
||||
- [x] 模板中明确 `discuss-callback(summaryPath)` 调用要求
|
||||
|
||||
#### A10.2 idle reminder
|
||||
- [. ] 定稿 discussion idle 模板
|
||||
- [. ] 模板中提醒 initiator:写总结文件并 callback
|
||||
- [. ] 避免提醒文案歧义或像自动总结器
|
||||
- [x] 定稿 discussion idle 模板
|
||||
- [x] 模板中提醒 initiator:写总结文件并 callback
|
||||
- [x] 避免提醒文案歧义或像自动总结器
|
||||
|
||||
#### A10.3 origin callback message
|
||||
- [x] 定稿发回原工作 channel 的结果通知模板
|
||||
@@ -163,8 +163,8 @@
|
||||
- [x] 模板中明确“继续基于该总结文件推进原任务”
|
||||
|
||||
#### A10.4 closed reply
|
||||
- [. ] 定稿 closed channel 固定回复模板
|
||||
- [. ] 明确 channel 已关闭,仅做留档使用
|
||||
- [x] 定稿 closed channel 固定回复模板
|
||||
- [x] 明确 channel 已关闭,仅做留档使用
|
||||
|
||||
### A11. `discuss-callback` 详细校验任务
|
||||
- [x] 校验当前 channel 必须是 discussion channel
|
||||
@@ -212,9 +212,9 @@
|
||||
- [x] 测试绝对路径越界失败
|
||||
|
||||
#### A13.5 回调链路测试
|
||||
- [. ] 测试 callback 成功后 moderator 在 origin channel 发出通知
|
||||
- [. ] 测试 origin channel 收到路径后能继续原工作流
|
||||
- [. ] 测试 discussion channel 后续只保留留档行为
|
||||
- [x] 测试 callback 成功后 moderator 在 origin channel 发出通知
|
||||
- [ ] 测试 origin channel 收到路径后能继续原工作流
|
||||
- [x] 测试 discussion channel 后续只保留留档行为
|
||||
|
||||
#### A13.6 文档交付
|
||||
- [. ] 根据最终代码实现更新 `plans/CSM.md`
|
||||
|
||||
@@ -31,10 +31,11 @@ export function buildDiscussionIdleReminderMessage(): string {
|
||||
"[Discussion Idle]",
|
||||
"",
|
||||
"No agent responded in the latest discussion round.",
|
||||
"If the discussion goal has been achieved, the initiator should now:",
|
||||
"If the discussion goal has already 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",
|
||||
"",
|
||||
"This reminder does not mean the discussion was automatically summarized or closed.",
|
||||
"If more discussion is still needed, continue the discussion in this channel.",
|
||||
].join("\n");
|
||||
}
|
||||
@@ -46,6 +47,7 @@ export function buildDiscussionClosedMessage(): string {
|
||||
"This discussion channel has been closed.",
|
||||
"It is now kept for archive/reference only.",
|
||||
"Further discussion in this channel is ignored.",
|
||||
"If follow-up work is needed, continue it from the origin work channel instead.",
|
||||
].join("\n");
|
||||
}
|
||||
|
||||
|
||||
@@ -5,6 +5,7 @@ import os from 'node:os';
|
||||
import path from 'node:path';
|
||||
|
||||
import { createDiscussionService } from '../plugin/core/discussion-service.ts';
|
||||
import { buildDiscussionClosedMessage, buildDiscussionOriginCallbackMessage } from '../plugin/core/discussion-messages.ts';
|
||||
|
||||
function makeLogger() {
|
||||
return {
|
||||
@@ -244,3 +245,100 @@ test('handleCallback rejects an absolute path outside the initiator workspace',
|
||||
/summaryPath must stay inside the initiator workspace/,
|
||||
);
|
||||
});
|
||||
|
||||
test('handleCallback notifies the origin channel with the resolved summary path', async () => {
|
||||
const workspace = makeWorkspace();
|
||||
const summaryRelPath = path.join('plans', 'discussion-summary.md');
|
||||
const summaryAbsPath = path.join(workspace, summaryRelPath);
|
||||
fs.mkdirSync(path.dirname(summaryAbsPath), { recursive: true });
|
||||
fs.writeFileSync(summaryAbsPath, '# done\n');
|
||||
|
||||
const fetchCalls: Array<{ url: string; body: any }> = [];
|
||||
const originalFetch = globalThis.fetch;
|
||||
globalThis.fetch = (async (url: string | URL | Request, init?: RequestInit) => {
|
||||
const body = init?.body ? JSON.parse(String(init.body)) : undefined;
|
||||
fetchCalls.push({ url: String(url), body });
|
||||
return new Response(JSON.stringify({ id: `msg-${fetchCalls.length}` }), { status: 200 });
|
||||
}) as typeof fetch;
|
||||
|
||||
try {
|
||||
const service = createDiscussionService({
|
||||
api: makeApi() as any,
|
||||
workspaceRoot: workspace,
|
||||
moderatorBotToken: 'bot-token',
|
||||
forceNoReplyForSession: () => {},
|
||||
});
|
||||
|
||||
await service.initDiscussion({
|
||||
discussionChannelId: 'discussion-origin-1',
|
||||
originChannelId: 'origin-8',
|
||||
initiatorAgentId: 'agent-theta',
|
||||
initiatorSessionId: 'session-theta',
|
||||
discussGuide: 'Notify the origin channel.',
|
||||
});
|
||||
|
||||
await service.handleCallback({
|
||||
channelId: 'discussion-origin-1',
|
||||
summaryPath: summaryRelPath,
|
||||
callerAgentId: 'agent-theta',
|
||||
callerSessionKey: 'session-theta',
|
||||
});
|
||||
|
||||
assert.equal(fetchCalls.length, 2);
|
||||
assert.equal(fetchCalls[1]?.url, 'https://discord.com/api/v10/channels/origin-8/messages');
|
||||
assert.equal(
|
||||
fetchCalls[1]?.body?.content,
|
||||
buildDiscussionOriginCallbackMessage(fs.realpathSync.native(summaryAbsPath), 'discussion-origin-1'),
|
||||
);
|
||||
} finally {
|
||||
globalThis.fetch = originalFetch;
|
||||
}
|
||||
});
|
||||
|
||||
test('maybeReplyClosedChannel sends the archive-only closed message for later channel activity', async () => {
|
||||
const workspace = makeWorkspace();
|
||||
const summaryRelPath = 'summary.md';
|
||||
const summaryAbsPath = path.join(workspace, summaryRelPath);
|
||||
fs.writeFileSync(summaryAbsPath, 'closed\n');
|
||||
|
||||
const fetchCalls: Array<{ url: string; body: any }> = [];
|
||||
const originalFetch = globalThis.fetch;
|
||||
globalThis.fetch = (async (url: string | URL | Request, init?: RequestInit) => {
|
||||
const body = init?.body ? JSON.parse(String(init.body)) : undefined;
|
||||
fetchCalls.push({ url: String(url), body });
|
||||
return new Response(JSON.stringify({ id: `msg-${fetchCalls.length}` }), { status: 200 });
|
||||
}) as typeof fetch;
|
||||
|
||||
try {
|
||||
const service = createDiscussionService({
|
||||
api: makeApi() as any,
|
||||
workspaceRoot: workspace,
|
||||
moderatorBotToken: 'bot-token',
|
||||
moderatorUserId: 'moderator-user',
|
||||
forceNoReplyForSession: () => {},
|
||||
});
|
||||
|
||||
await service.initDiscussion({
|
||||
discussionChannelId: 'discussion-closed-1',
|
||||
originChannelId: 'origin-9',
|
||||
initiatorAgentId: 'agent-iota',
|
||||
initiatorSessionId: 'session-iota',
|
||||
discussGuide: 'Close and archive.',
|
||||
});
|
||||
|
||||
await service.handleCallback({
|
||||
channelId: 'discussion-closed-1',
|
||||
summaryPath: summaryRelPath,
|
||||
callerAgentId: 'agent-iota',
|
||||
callerSessionKey: 'session-iota',
|
||||
});
|
||||
|
||||
const handled = await service.maybeReplyClosedChannel('discussion-closed-1', 'human-user');
|
||||
assert.equal(handled, true);
|
||||
assert.equal(fetchCalls.length, 3);
|
||||
assert.equal(fetchCalls[2]?.url, 'https://discord.com/api/v10/channels/discussion-closed-1/messages');
|
||||
assert.equal(fetchCalls[2]?.body?.content, buildDiscussionClosedMessage());
|
||||
} finally {
|
||||
globalThis.fetch = originalFetch;
|
||||
}
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user