chatccc 0.2.267 → 0.2.269
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +11 -5
- package/dist/src/agent-delegate-task-rpc.js +40 -27
- package/dist/src/agent-delegate-task.js +8 -3
- package/dist/src/agent-set-cwd-rpc.js +73 -0
- package/dist/src/agent-team/application/main-agent-service.js +11 -2
- package/dist/src/agent-team/application/task-execution-service.js +34 -5
- package/dist/src/agent-team/domain/task-run.js +4 -0
- package/dist/src/agent-team/http/board-routes.js +6 -0
- package/dist/src/agent-team/infrastructure/task-execution-runtime.js +4 -0
- package/dist/src/agent-team/web/agent-team-page.js +243 -229
- package/dist/src/cards.js +3 -0
- package/dist/src/engines/engine-manager.js +7 -0
- package/dist/src/execution-transcript.js +110 -0
- package/dist/src/im-skills.js +2 -2
- package/dist/src/index.js +9 -2
- package/dist/src/orchestrator.js +194 -6
- package/dist/src/safe-maintenance.js +271 -0
- package/dist/src/session-chat-binding.js +12 -0
- package/dist/src/session.js +16 -2
- package/dist/src/stream-state.js +1 -0
- package/dist/src/web-ui.js +5 -0
- package/im-skills/feishu-skill/skill.md +18 -4
- package/package.json +76 -76
package/README.md
CHANGED
|
@@ -155,11 +155,11 @@ chatccc
|
|
|
155
155
|
|
|
156
156
|
#### 本地任务看板(Agent Team)
|
|
157
157
|
|
|
158
|
-
Agent Team 提供本地任务看板和项目主 Agent 入口。每个规范化后的本机目录对应一个独立项目和看板,固定包含“头脑风暴、Todo、Doing、Done、搁置”五列,支持新增、单击卡片编辑、拖动排序和删除任务。页面会自动保存,不需要手动提交。当前按路径识别项目,不检查 Git 仓库或 worktree 关系,任意子目录都可以作为独立项目。
|
|
158
|
+
Agent Team 提供本地任务看板和项目主 Agent 入口。每个规范化后的本机目录对应一个独立项目和看板,固定包含“头脑风暴、Todo、Doing、Done、搁置”五列,支持新增、单击卡片编辑、拖动排序和删除任务。页面会自动保存,不需要手动提交。当前按路径识别项目,不检查 Git 仓库或 worktree 关系,任意子目录都可以作为独立项目。
|
|
159
159
|
|
|
160
|
-
打开项目后可以选择 CCC、Claude、Cursor 或 Codex 作为主 Agent。首次设置会创建固定命名为 `主Agent-<目录短名>` 的飞书群和空 Agent Session;后续切换 Agent 或重新关联目录会复用原群,运行中的主 Agent 不允许切换。建群成员取自机器人最近一次收到的飞书私聊;如果尚无私聊记录,网页会提示先给机器人发送任意私聊消息并自动检测。项目群名不会随第一句话或 `/forget` 改变。
|
|
161
|
-
|
|
162
|
-
任务卡片可通过“交给主 Agent”启动真实执行:任务会从 Todo 移到 Doing,成功后自动移到 Done,失败则保留在 Doing 并显示错误,支持停止与重试。同一项目同一时间只运行一个看板任务;运行记录持久化在本地 JSON 中,服务异常退出后会把未完成执行标记为中断,避免错误显示为仍在运行。
|
|
160
|
+
打开项目后可以选择 CCC、Claude、Cursor 或 Codex 作为主 Agent。首次设置会创建固定命名为 `主Agent-<目录短名>` 的飞书群和空 Agent Session;后续切换 Agent 或重新关联目录会复用原群,运行中的主 Agent 不允许切换。建群成员取自机器人最近一次收到的飞书私聊;如果尚无私聊记录,网页会提示先给机器人发送任意私聊消息并自动检测。项目群名不会随第一句话或 `/forget` 改变。
|
|
161
|
+
|
|
162
|
+
任务卡片可通过“交给主 Agent”启动真实执行:任务会从 Todo 移到 Doing,成功后自动移到 Done,失败则保留在 Doing 并显示错误,支持停止与重试。同一项目同一时间只运行一个看板任务;运行记录持久化在本地 JSON 中,服务异常退出后会把未完成执行标记为中断,避免错误显示为仍在运行。
|
|
163
163
|
|
|
164
164
|
看板数据默认保存在 `~/.chatccc/agent-team/`,其中 `workspaces.json` 保存最近工作目录索引,`boards/` 保存按稳定 ID 分隔的看板 JSON,`main-agent-bindings/` 单独保存本机飞书群与 Session 绑定。目录移动或重命名后,可从最近目录列表重新关联。数据访问通过仓储接口隔离,后续接入飞书多维表格时可增加双向同步适配器,无需把本机群聊和 Session 状态混入任务同步模型。选择目录、创建群聊和启动 Agent 等操作都由本机 Node 后端执行;当前实现不依赖 Electron。
|
|
165
165
|
|
|
@@ -427,10 +427,16 @@ Codex 的默认模型和推理强度可继续由 `~/.codex/config.toml` 管理
|
|
|
427
427
|
| `/plan <内容>` | 只读计划模式:仅允许读文件和 stop-stuck-loop 请求,不执行任何写操作 |
|
|
428
428
|
| `/ask <内容>` | 只读问答模式:与 /plan 相同,仅允许读文件和 stop-stuck-loop 请求 |
|
|
429
429
|
| `/restart` | 重启机器人进程 |
|
|
430
|
+
| `/restart safe` | 停止接受新任务,等待现有会话、缓存消息和依赖安装完成后重启 |
|
|
430
431
|
| `/update` | 更新 npm 全局包并重启(仅限 `npm install -g chatccc` 安装的全局进程;同一飞书事件跨重启去重) |
|
|
432
|
+
| `/update safe` | 停止接受新任务,排空现有工作后更新并重启 |
|
|
433
|
+
| `/safestatus` | 查看安全重启/更新的等待状态 |
|
|
434
|
+
| `/cancelsf` | 取消尚未开始执行的安全重启/更新预约 |
|
|
431
435
|
| `/deleteg` | 解散当前飞书会话群;Agent 会话记录保留 |
|
|
432
436
|
|
|
433
|
-
`/update`
|
|
437
|
+
`/update` 与 `/update safe` 会把飞书消息或按钮事件 ID 原子写入 `~/.chatccc/state/update-command-guard.json`。同一 ID 跨重启重投时会静默忽略;用户主动发送的新更新指令因事件 ID 不同,仍可执行。该保护不改变普通消息与重启指令的处理方式。
|
|
438
|
+
|
|
439
|
+
`/restart safe` 与 `/update safe` 会先建立全局准入门禁:指令到达前已经运行或进入单会话缓存队列的工作会继续完成,之后到达的新普通任务会被提示在维护完成后重发。维护任务持久化到 `~/.chatccc/state/safe-maintenance.json`,进程意外退出后可继续排空;依赖安装、会话收尾、自动恢复和 Agent Teams 执行也计入等待条件。内存缓存随重启自然重建,磁盘会话、看板、图片等持久数据不会被清理。
|
|
434
440
|
|
|
435
441
|
> **模型切换**:`/model` 查看当前会话 Agent 的可选模型清单,`/model <名称>` 模糊匹配切换,`/model clear` 恢复默认。可选模型来自当前 Agent 的配置:Claude 使用 `claude.model` / `claude.subagentModel`;Cursor、Codex、CCC Agent 和 DSH 使用各自的 `model` / `alternativeModel`。
|
|
436
442
|
|
|
@@ -1,11 +1,13 @@
|
|
|
1
1
|
import { resolve } from "node:path";
|
|
2
|
+
import { homedir } from "node:os";
|
|
2
3
|
import { resolveDefaultAgentTool } from "./config.js";
|
|
3
4
|
import { readUtf8JsonBody } from "./agent-rpc-body.js";
|
|
4
5
|
import { delegateAgentTask } from "./agent-delegate-task.js";
|
|
5
6
|
import { applySharedPrefix } from "./shared-prefix.js";
|
|
7
|
+
import { beginSafeMaintenanceTrackedWork, isSafeMaintenanceAdmissionClosed, } from "./safe-maintenance.js";
|
|
6
8
|
export const AGENT_DELEGATE_TASK_PATH = "/api/agent/delegate-task";
|
|
7
9
|
const MAX_REQUEST_BYTES = 128 * 1024;
|
|
8
|
-
const VALID_TOOLS = new Set(["claude", "cursor", "codex"]);
|
|
10
|
+
const VALID_TOOLS = new Set(["claude", "cursor", "codex", "ccc", "dsh"]);
|
|
9
11
|
function jsonReply(res, status, data) {
|
|
10
12
|
res.writeHead(status, { "Content-Type": "application/json; charset=utf-8" });
|
|
11
13
|
res.end(JSON.stringify(data));
|
|
@@ -37,7 +39,7 @@ function validateTool(rawTool) {
|
|
|
37
39
|
function validateCwd(rawCwd) {
|
|
38
40
|
const cwd = stringValue(rawCwd);
|
|
39
41
|
if (!cwd)
|
|
40
|
-
|
|
42
|
+
return homedir();
|
|
41
43
|
return resolve(cwd);
|
|
42
44
|
}
|
|
43
45
|
export async function handleAgentDelegateTaskRequest(req, res, platform) {
|
|
@@ -69,8 +71,6 @@ export async function handleAgentDelegateTaskRequest(req, res, platform) {
|
|
|
69
71
|
tool = validateTool(payload.tool);
|
|
70
72
|
cwd = validateCwd(payload.cwd);
|
|
71
73
|
const rawPrompt = promptFromPayload(payload);
|
|
72
|
-
if (!rawPrompt)
|
|
73
|
-
throw new Error("prompt must be a non-empty string");
|
|
74
74
|
const sharedPrefix = applySharedPrefix(rawPrompt);
|
|
75
75
|
promptText = sharedPrefix.text;
|
|
76
76
|
promptNamePrefix = sharedPrefix.body || rawPrompt;
|
|
@@ -83,21 +83,31 @@ export async function handleAgentDelegateTaskRequest(req, res, platform) {
|
|
|
83
83
|
return true;
|
|
84
84
|
}
|
|
85
85
|
try {
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
86
|
+
if (isSafeMaintenanceAdmissionClosed()) {
|
|
87
|
+
jsonReply(res, 409, { ok: false, error: "ChatCCC 正在等待安全维护,暂不接受新的委派任务。" });
|
|
88
|
+
return true;
|
|
89
|
+
}
|
|
90
|
+
const release = beginSafeMaintenanceTrackedWork("agent-delegate-task");
|
|
91
|
+
try {
|
|
92
|
+
const result = await delegateAgentTask({
|
|
93
|
+
platform,
|
|
94
|
+
tool,
|
|
95
|
+
cwd,
|
|
96
|
+
promptText,
|
|
97
|
+
openIds,
|
|
98
|
+
chatNamePrefix: stringValue(payload.chat_name) || promptNamePrefix.slice(0, 10),
|
|
99
|
+
});
|
|
100
|
+
jsonReply(res, 200, {
|
|
101
|
+
ok: true,
|
|
102
|
+
chat_id: result.chatId,
|
|
103
|
+
session_id: result.sessionId,
|
|
104
|
+
tool: result.tool,
|
|
105
|
+
cwd: result.cwd,
|
|
106
|
+
});
|
|
107
|
+
}
|
|
108
|
+
finally {
|
|
109
|
+
release();
|
|
110
|
+
}
|
|
101
111
|
}
|
|
102
112
|
catch (err) {
|
|
103
113
|
jsonReply(res, 500, { ok: false, error: err.message });
|
|
@@ -106,24 +116,27 @@ export async function handleAgentDelegateTaskRequest(req, res, platform) {
|
|
|
106
116
|
}
|
|
107
117
|
export function buildAgentDelegateTaskCapabilityPrompt(input) {
|
|
108
118
|
const lines = [
|
|
109
|
-
"[ChatCCC local capability:
|
|
110
|
-
"You can create a separate Feishu ChatCCC agent session and assign its first task by calling this local endpoint.",
|
|
119
|
+
"[ChatCCC local capability: new session]",
|
|
120
|
+
"You can create a separate Feishu ChatCCC agent session (a new group) and optionally assign its first task by calling this local endpoint.",
|
|
111
121
|
"",
|
|
112
122
|
`POST ${input.url}`,
|
|
113
123
|
"Content-Type: application/json; charset=utf-8",
|
|
114
124
|
"",
|
|
115
|
-
'Body: {"tool":"codex|
|
|
125
|
+
'Body: {"tool":"claude|cursor|codex|ccc|dsh","cwd":"absolute working directory","open_id":"initiator open_id","prompt":"optional first task"}',
|
|
116
126
|
"",
|
|
117
127
|
"Rules:",
|
|
118
|
-
"- Use this
|
|
119
|
-
"- Pass
|
|
120
|
-
"- Pass
|
|
121
|
-
"-
|
|
128
|
+
"- Use this when the user asks to start a new conversation/session, optionally in a specific directory.",
|
|
129
|
+
"- Pass open_id exactly as provided to you; the new group will only include that requester.",
|
|
130
|
+
"- Pass cwd as an absolute local path. When the user does not name a directory, use your current working directory.",
|
|
131
|
+
"- tool is optional; omit it to use the default agent, or pass one of claude/cursor/codex/ccc/dsh when the user names a tool.",
|
|
132
|
+
"- prompt is optional; omit it to just create the session without sending a first task.",
|
|
122
133
|
"- The prompt is sent through the normal ChatCCC prompt path, so project prompt injection and IM skills still apply.",
|
|
123
134
|
"- Request body must be UTF-8 encoded JSON bytes. Do not call Feishu Open Platform directly.",
|
|
124
|
-
"[/ChatCCC local capability:
|
|
135
|
+
"[/ChatCCC local capability: new session]",
|
|
125
136
|
];
|
|
126
137
|
if (input.cwd)
|
|
127
138
|
lines.splice(2, 0, `Current working directory: ${input.cwd}`);
|
|
139
|
+
if (input.openId)
|
|
140
|
+
lines.splice(2, 0, `Initiator open_id: ${input.openId}`);
|
|
128
141
|
return lines.join("\n");
|
|
129
142
|
}
|
|
@@ -7,9 +7,10 @@ import { sessionChatName } from "./session-name.js";
|
|
|
7
7
|
export async function delegateAgentTask(input) {
|
|
8
8
|
const cwd = resolve(input.cwd);
|
|
9
9
|
const toolLabel = toolDisplayName(input.tool);
|
|
10
|
+
const hasPrompt = input.promptText.trim().length > 0;
|
|
10
11
|
const init = await initClaudeSession(input.tool, cwd);
|
|
11
12
|
const sessionId = init.sessionId;
|
|
12
|
-
const chatNamePrefix = input.chatNamePrefix?.trim() || input.promptText.slice(0, 10)
|
|
13
|
+
const chatNamePrefix = input.chatNamePrefix?.trim() || (hasPrompt ? input.promptText.slice(0, 10) : "新会话");
|
|
13
14
|
const chatName = sessionChatName(chatNamePrefix, cwd);
|
|
14
15
|
let chatId;
|
|
15
16
|
try {
|
|
@@ -36,13 +37,17 @@ export async function delegateAgentTask(input) {
|
|
|
36
37
|
await input.platform.sendCard(chatId, `${toolLabel} Session Ready`, `已创建 **${toolLabel}** 会话群。\n\n` +
|
|
37
38
|
`**Session ID:** ${sessionId}\n` +
|
|
38
39
|
`**工作目录:** \`${cwd}\`\n\n` +
|
|
39
|
-
|
|
40
|
+
(hasPrompt
|
|
41
|
+
? `下面会自动把任务作为第一句话发送给 ${toolLabel}。`
|
|
42
|
+
: `直接在这里发消息即可与 ${toolLabel} 对话。`), "green").catch(() => { });
|
|
40
43
|
const fastMode = getEffectiveFastModeForTool(input.tool, sessionId);
|
|
41
44
|
const avatarUpdate = fastMode
|
|
42
45
|
? input.platform.setChatAvatar(chatId, input.tool, "new", { fastMode: true })
|
|
43
46
|
: input.platform.setChatAvatar(chatId, input.tool, "new");
|
|
44
47
|
avatarUpdate.catch(() => { });
|
|
45
|
-
|
|
48
|
+
if (hasPrompt) {
|
|
49
|
+
await resumeAndPrompt(sessionId, input.promptText, input.platform, chatId, input.msgTimestamp ?? Date.now(), input.tool, input.traceId, input.openIds?.[0]);
|
|
50
|
+
}
|
|
46
51
|
console.log(`[${ts()}] [AGENT-DELEGATE-TASK] created ${toolLabel} session=${sessionId} chat=${chatId} cwd=${cwd}`);
|
|
47
52
|
return { chatId, sessionId, tool: input.tool, cwd };
|
|
48
53
|
}
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import { resolve } from "node:path";
|
|
2
|
+
import { stat } from "node:fs/promises";
|
|
3
|
+
import { addRecentDir, setDefaultCwd } from "./config.js";
|
|
4
|
+
import { readUtf8JsonBody } from "./agent-rpc-body.js";
|
|
5
|
+
import { getChatsForSession } from "./session-chat-binding.js";
|
|
6
|
+
export const AGENT_SET_CWD_PATH = "/api/agent/set-cwd";
|
|
7
|
+
const MAX_REQUEST_BYTES = 64 * 1024;
|
|
8
|
+
function jsonReply(res, status, data) {
|
|
9
|
+
res.writeHead(status, { "Content-Type": "application/json; charset=utf-8" });
|
|
10
|
+
res.end(JSON.stringify(data));
|
|
11
|
+
}
|
|
12
|
+
function stringValue(value) {
|
|
13
|
+
return typeof value === "string" ? value.trim() : "";
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* Agent 本地能力:设置后续新建会话的默认工作目录(等价于 /cd,不改变当前会话)。
|
|
17
|
+
* 请求携带发起者当前 session_id,主进程据此反查 chatId,并持久化到该 chat 的
|
|
18
|
+
* working_dir 文件,同时写入最近目录记录。
|
|
19
|
+
*/
|
|
20
|
+
export async function handleAgentSetCwdRequest(req, res) {
|
|
21
|
+
const url = new URL(req.url ?? "/", "http://127.0.0.1");
|
|
22
|
+
if (url.pathname !== AGENT_SET_CWD_PATH)
|
|
23
|
+
return false;
|
|
24
|
+
if (req.method !== "POST") {
|
|
25
|
+
jsonReply(res, 405, { ok: false, error: "Method not allowed" });
|
|
26
|
+
return true;
|
|
27
|
+
}
|
|
28
|
+
let payload;
|
|
29
|
+
try {
|
|
30
|
+
payload = await readUtf8JsonBody(req, MAX_REQUEST_BYTES);
|
|
31
|
+
}
|
|
32
|
+
catch (err) {
|
|
33
|
+
jsonReply(res, 400, { ok: false, error: err.message || "Invalid JSON" });
|
|
34
|
+
return true;
|
|
35
|
+
}
|
|
36
|
+
const sessionId = stringValue(payload.session_id);
|
|
37
|
+
if (!sessionId) {
|
|
38
|
+
jsonReply(res, 400, { ok: false, error: "Missing session_id" });
|
|
39
|
+
return true;
|
|
40
|
+
}
|
|
41
|
+
const rawDir = stringValue(payload.dir) || stringValue(payload.path);
|
|
42
|
+
if (!rawDir) {
|
|
43
|
+
jsonReply(res, 400, { ok: false, error: "dir must be a non-empty string" });
|
|
44
|
+
return true;
|
|
45
|
+
}
|
|
46
|
+
const dir = resolve(rawDir);
|
|
47
|
+
try {
|
|
48
|
+
const st = await stat(dir);
|
|
49
|
+
if (!st.isDirectory()) {
|
|
50
|
+
jsonReply(res, 400, { ok: false, error: `path is not a directory: ${dir}` });
|
|
51
|
+
return true;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
catch {
|
|
55
|
+
jsonReply(res, 400, { ok: false, error: `directory does not exist: ${dir}` });
|
|
56
|
+
return true;
|
|
57
|
+
}
|
|
58
|
+
const chatIds = getChatsForSession(sessionId);
|
|
59
|
+
const chatId = chatIds[0];
|
|
60
|
+
if (!chatId) {
|
|
61
|
+
jsonReply(res, 404, { ok: false, error: "No chat bound to this session; cannot set working directory." });
|
|
62
|
+
return true;
|
|
63
|
+
}
|
|
64
|
+
try {
|
|
65
|
+
await setDefaultCwd(dir, chatId);
|
|
66
|
+
await addRecentDir(dir);
|
|
67
|
+
jsonReply(res, 200, { ok: true, dir, chat_id: chatId });
|
|
68
|
+
}
|
|
69
|
+
catch (err) {
|
|
70
|
+
jsonReply(res, 500, { ok: false, error: err.message });
|
|
71
|
+
}
|
|
72
|
+
return true;
|
|
73
|
+
}
|
|
@@ -2,6 +2,7 @@ import { isAgentTool } from "../../agent-tool.js";
|
|
|
2
2
|
import { sessionChatName } from "../../session-name.js";
|
|
3
3
|
import { isValidFeishuOpenId, } from "../repositories/feishu-p2p-contact-store.js";
|
|
4
4
|
import { BoardStoreError } from "../repositories/board-repository.js";
|
|
5
|
+
import { beginSafeMaintenanceTrackedWork, isSafeMaintenanceAdmissionClosed, } from "../../safe-maintenance.js";
|
|
5
6
|
export class MainAgentService {
|
|
6
7
|
options;
|
|
7
8
|
now;
|
|
@@ -17,18 +18,26 @@ export class MainAgentService {
|
|
|
17
18
|
return this.options.bindingRepository.get(projectId);
|
|
18
19
|
}
|
|
19
20
|
setPrimaryAgent(projectId, agentId, expectedRevision) {
|
|
21
|
+
if (isSafeMaintenanceAdmissionClosed()) {
|
|
22
|
+
throw new BoardStoreError("safe_maintenance_draining", "ChatCCC 正在等待安全维护,暂不接受新的主 Agent 设置。", 409);
|
|
23
|
+
}
|
|
20
24
|
if (!isAgentTool(agentId)) {
|
|
21
25
|
throw new BoardStoreError("invalid_request", `Unsupported primary Agent: ${String(agentId)}`, 400);
|
|
22
26
|
}
|
|
27
|
+
const release = beginSafeMaintenanceTrackedWork("agent-team-main-agent");
|
|
23
28
|
return this.exclusive(projectId, async () => {
|
|
24
29
|
const board = await this.options.boardService.getBoard(projectId);
|
|
25
30
|
if (board.revision !== expectedRevision) {
|
|
26
31
|
throw new BoardStoreError("revision_conflict", `Board changed in another page (expected revision ${expectedRevision}, current ${board.revision})`, 409);
|
|
27
32
|
}
|
|
28
33
|
return this.provision(board, agentId, false);
|
|
29
|
-
});
|
|
34
|
+
}).finally(release);
|
|
30
35
|
}
|
|
31
36
|
relinkWorkspace(projectId, workspacePath, expectedRevision) {
|
|
37
|
+
if (isSafeMaintenanceAdmissionClosed()) {
|
|
38
|
+
throw new BoardStoreError("safe_maintenance_draining", "ChatCCC 正在等待安全维护,暂不接受目录重新关联。", 409);
|
|
39
|
+
}
|
|
40
|
+
const release = beginSafeMaintenanceTrackedWork("agent-team-relink");
|
|
32
41
|
return this.exclusive(projectId, async () => {
|
|
33
42
|
const existing = await this.options.bindingRepository.get(projectId);
|
|
34
43
|
const currentSessionId = existing ? (await this.resolveCurrentSession(existing)).sessionId : null;
|
|
@@ -39,7 +48,7 @@ export class MainAgentService {
|
|
|
39
48
|
if (!existing || !board.primaryAgentId)
|
|
40
49
|
return { board, binding: null };
|
|
41
50
|
return this.provision(board, board.primaryAgentId, true);
|
|
42
|
-
});
|
|
51
|
+
}).finally(release);
|
|
43
52
|
}
|
|
44
53
|
async provision(board, agentId, forceNewSession) {
|
|
45
54
|
const existing = await this.options.bindingRepository.get(board.boardId);
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { randomUUID } from "node:crypto";
|
|
2
2
|
import { isActiveTaskRun } from "../domain/task-run.js";
|
|
3
3
|
import { BoardStoreError } from "../repositories/board-repository.js";
|
|
4
|
+
import { beginSafeMaintenanceTrackedWork, isSafeMaintenanceAdmissionClosed, } from "../../safe-maintenance.js";
|
|
4
5
|
export class TaskExecutionService {
|
|
5
6
|
options;
|
|
6
7
|
now;
|
|
@@ -12,10 +13,25 @@ export class TaskExecutionService {
|
|
|
12
13
|
this.now = options.now ?? (() => new Date());
|
|
13
14
|
this.idFactory = options.idFactory ?? randomUUID;
|
|
14
15
|
}
|
|
15
|
-
listRuns(projectId) {
|
|
16
|
-
|
|
16
|
+
async listRuns(projectId) {
|
|
17
|
+
const runs = await this.options.repository.listByProject(projectId);
|
|
18
|
+
return runs.map(withoutTranscript);
|
|
19
|
+
}
|
|
20
|
+
async getRun(projectId, runId) {
|
|
21
|
+
const run = await this.options.repository.get(runId);
|
|
22
|
+
if (!run || run.projectId !== projectId) {
|
|
23
|
+
throw new BoardStoreError("task_run_not_found", "找不到这次任务执行记录", 404);
|
|
24
|
+
}
|
|
25
|
+
if (!isActiveTaskRun(run) || !this.options.runtime.getTranscript)
|
|
26
|
+
return run;
|
|
27
|
+
const live = await this.options.runtime.getTranscript(run.sessionId).catch(() => []);
|
|
28
|
+
return live.length ? { ...run, transcript: mergeTranscript(run.transcript, live) } : run;
|
|
17
29
|
}
|
|
18
30
|
async startTask(projectId, taskId, expectedRevision) {
|
|
31
|
+
if (isSafeMaintenanceAdmissionClosed()) {
|
|
32
|
+
throw new BoardStoreError("safe_maintenance_draining", "ChatCCC 正在等待安全维护,暂不接受新的 Agent Team 任务。", 409);
|
|
33
|
+
}
|
|
34
|
+
const release = beginSafeMaintenanceTrackedWork("agent-team-task-start");
|
|
19
35
|
return this.exclusive(projectId, async () => {
|
|
20
36
|
let board = await this.options.boardService.getBoard(projectId);
|
|
21
37
|
const active = (await this.options.repository.listByProject(projectId)).find(isActiveTaskRun);
|
|
@@ -54,6 +70,10 @@ export class TaskExecutionService {
|
|
|
54
70
|
createdAt: timestamp,
|
|
55
71
|
updatedAt: timestamp,
|
|
56
72
|
};
|
|
73
|
+
run = {
|
|
74
|
+
...run,
|
|
75
|
+
transcript: [{ type: "prompt", at: timestamp, text: taskPrompt(run) }],
|
|
76
|
+
};
|
|
57
77
|
await this.options.repository.save(run);
|
|
58
78
|
try {
|
|
59
79
|
if (task.columnId !== "doing") {
|
|
@@ -80,7 +100,7 @@ export class TaskExecutionService {
|
|
|
80
100
|
console.error(`[Agent Team] Task run ${run.runId} failed to persist its terminal state: ${err.message}`);
|
|
81
101
|
});
|
|
82
102
|
return { board, run };
|
|
83
|
-
});
|
|
103
|
+
}).finally(release);
|
|
84
104
|
}
|
|
85
105
|
async stopRun(projectId, runId) {
|
|
86
106
|
return this.exclusive(projectId, async () => {
|
|
@@ -125,14 +145,15 @@ export class TaskExecutionService {
|
|
|
125
145
|
prompt: taskPrompt(run),
|
|
126
146
|
});
|
|
127
147
|
if (result.outcome === "done") {
|
|
128
|
-
return this.finishRun(run, "succeeded", { result: result.result, moveTask: true });
|
|
148
|
+
return this.finishRun(run, "succeeded", { result: result.result, transcript: result.transcript, moveTask: true });
|
|
129
149
|
}
|
|
130
150
|
if (result.outcome === "stopped") {
|
|
131
|
-
return this.finishRun(run, "canceled", { error: result.error, moveTask: true });
|
|
151
|
+
return this.finishRun(run, "canceled", { error: result.error, transcript: result.transcript, moveTask: true });
|
|
132
152
|
}
|
|
133
153
|
return this.finishRun(run, "failed", {
|
|
134
154
|
error: result.error || (result.outcome === "auto_ended" ? "Agent response timed out" : "Agent execution failed"),
|
|
135
155
|
result: result.result,
|
|
156
|
+
transcript: result.transcript,
|
|
136
157
|
moveTask: true,
|
|
137
158
|
});
|
|
138
159
|
}
|
|
@@ -150,6 +171,7 @@ export class TaskExecutionService {
|
|
|
150
171
|
state,
|
|
151
172
|
updatedAt: now,
|
|
152
173
|
finishedAt: now,
|
|
174
|
+
...(details.transcript?.length ? { transcript: mergeTranscript(latest.transcript, details.transcript) } : {}),
|
|
153
175
|
...(details.result ? { result: details.result } : {}),
|
|
154
176
|
...(details.error ? { error: details.error } : {}),
|
|
155
177
|
};
|
|
@@ -195,6 +217,13 @@ export class TaskExecutionService {
|
|
|
195
217
|
}
|
|
196
218
|
}
|
|
197
219
|
}
|
|
220
|
+
function mergeTranscript(existing, appended) {
|
|
221
|
+
return [...(existing ?? []), ...appended];
|
|
222
|
+
}
|
|
223
|
+
function withoutTranscript(run) {
|
|
224
|
+
const { transcript: _transcript, ...summary } = run;
|
|
225
|
+
return summary;
|
|
226
|
+
}
|
|
198
227
|
function taskPrompt(run) {
|
|
199
228
|
return [
|
|
200
229
|
"你正在执行 Agent Team 看板中的一个明确任务。请在当前工作目录完成任务,进行必要验证,并用简洁结果说明完成内容。",
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { isAgentTool } from "../../agent-tool.js";
|
|
2
|
+
import { isExecutionTranscriptEntry } from "../../execution-transcript.js";
|
|
2
3
|
export const TASK_RUN_SCHEMA_VERSION = 1;
|
|
3
4
|
export function isActiveTaskRun(run) {
|
|
4
5
|
return run.state === "queued" || run.state === "running";
|
|
@@ -25,5 +26,8 @@ export function parseTaskRun(value) {
|
|
|
25
26
|
if (run[field] !== undefined && typeof run[field] !== "string")
|
|
26
27
|
throw new Error(`Invalid task run ${field}`);
|
|
27
28
|
}
|
|
29
|
+
if (run.transcript !== undefined && (!Array.isArray(run.transcript) || !run.transcript.every(isExecutionTranscriptEntry))) {
|
|
30
|
+
throw new Error("Invalid task run transcript");
|
|
31
|
+
}
|
|
28
32
|
return run;
|
|
29
33
|
}
|
|
@@ -109,6 +109,12 @@ export function createAgentTeamRequestHandler(options) {
|
|
|
109
109
|
jsonReply(res, 200, { ok: true, runs });
|
|
110
110
|
return true;
|
|
111
111
|
}
|
|
112
|
+
const runDetailMatch = pathname.match(/^\/api\/agent-team\/boards\/([^/]+)\/runs\/([^/]+)$/);
|
|
113
|
+
if (runDetailMatch && method === "GET") {
|
|
114
|
+
const run = await requireTaskExecutionService(taskExecutionService()).getRun(decodeURIComponent(runDetailMatch[1]), decodeURIComponent(runDetailMatch[2]));
|
|
115
|
+
jsonReply(res, 200, { ok: true, run });
|
|
116
|
+
return true;
|
|
117
|
+
}
|
|
112
118
|
const runTaskMatch = pathname.match(/^\/api\/agent-team\/boards\/([^/]+)\/tasks\/([^/]+)\/run$/);
|
|
113
119
|
if (runTaskMatch && method === "POST") {
|
|
114
120
|
const body = await bodyJson(req);
|
|
@@ -11,10 +11,14 @@ export function createTaskExecutionRuntime(platform) {
|
|
|
11
11
|
}
|
|
12
12
|
return {
|
|
13
13
|
outcome,
|
|
14
|
+
...(stream?.transcript?.length ? { transcript: stream.transcript } : {}),
|
|
14
15
|
...(stream?.finalReply ? { result: stream.finalReply } : {}),
|
|
15
16
|
...(stream?.terminalError?.message ? { error: stream.terminalError.message } : {}),
|
|
16
17
|
};
|
|
17
18
|
},
|
|
19
|
+
async getTranscript(sessionId) {
|
|
20
|
+
return (await readStreamState(sessionId))?.transcript ?? [];
|
|
21
|
+
},
|
|
18
22
|
stop: stopSession,
|
|
19
23
|
isSessionRunning,
|
|
20
24
|
};
|