chatccc 0.2.267 → 0.2.268
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 +9 -3
- package/dist/src/agent-delegate-task-rpc.js +26 -15
- 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 +28 -14
- 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/index.js +6 -2
- package/dist/src/orchestrator.js +192 -4
- package/dist/src/safe-maintenance.js +271 -0
- package/dist/src/session-chat-binding.js +12 -0
- package/dist/src/session.js +10 -0
- package/dist/src/stream-state.js +1 -0
- package/dist/src/web-ui.js +5 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -426,11 +426,17 @@ Codex 的默认模型和推理强度可继续由 `~/.codex/config.toml` 管理
|
|
|
426
426
|
| `/abd<内容>` | 去掉 `/abd` 前缀后把内容发给 Agent,并在消息末尾追加第一性原理需求澄清提示 |
|
|
427
427
|
| `/plan <内容>` | 只读计划模式:仅允许读文件和 stop-stuck-loop 请求,不执行任何写操作 |
|
|
428
428
|
| `/ask <内容>` | 只读问答模式:与 /plan 相同,仅允许读文件和 stop-stuck-loop 请求 |
|
|
429
|
-
| `/restart` | 重启机器人进程 |
|
|
430
|
-
| `/
|
|
429
|
+
| `/restart` | 重启机器人进程 |
|
|
430
|
+
| `/restart safe` | 停止接受新任务,等待现有会话、缓存消息和依赖安装完成后重启 |
|
|
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
|
|
|
@@ -3,6 +3,7 @@ import { resolveDefaultAgentTool } from "./config.js";
|
|
|
3
3
|
import { readUtf8JsonBody } from "./agent-rpc-body.js";
|
|
4
4
|
import { delegateAgentTask } from "./agent-delegate-task.js";
|
|
5
5
|
import { applySharedPrefix } from "./shared-prefix.js";
|
|
6
|
+
import { beginSafeMaintenanceTrackedWork, isSafeMaintenanceAdmissionClosed, } from "./safe-maintenance.js";
|
|
6
7
|
export const AGENT_DELEGATE_TASK_PATH = "/api/agent/delegate-task";
|
|
7
8
|
const MAX_REQUEST_BYTES = 128 * 1024;
|
|
8
9
|
const VALID_TOOLS = new Set(["claude", "cursor", "codex"]);
|
|
@@ -83,21 +84,31 @@ export async function handleAgentDelegateTaskRequest(req, res, platform) {
|
|
|
83
84
|
return true;
|
|
84
85
|
}
|
|
85
86
|
try {
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
87
|
+
if (isSafeMaintenanceAdmissionClosed()) {
|
|
88
|
+
jsonReply(res, 409, { ok: false, error: "ChatCCC 正在等待安全维护,暂不接受新的委派任务。" });
|
|
89
|
+
return true;
|
|
90
|
+
}
|
|
91
|
+
const release = beginSafeMaintenanceTrackedWork("agent-delegate-task");
|
|
92
|
+
try {
|
|
93
|
+
const result = await delegateAgentTask({
|
|
94
|
+
platform,
|
|
95
|
+
tool,
|
|
96
|
+
cwd,
|
|
97
|
+
promptText,
|
|
98
|
+
openIds,
|
|
99
|
+
chatNamePrefix: stringValue(payload.chat_name) || promptNamePrefix.slice(0, 10),
|
|
100
|
+
});
|
|
101
|
+
jsonReply(res, 200, {
|
|
102
|
+
ok: true,
|
|
103
|
+
chat_id: result.chatId,
|
|
104
|
+
session_id: result.sessionId,
|
|
105
|
+
tool: result.tool,
|
|
106
|
+
cwd: result.cwd,
|
|
107
|
+
});
|
|
108
|
+
}
|
|
109
|
+
finally {
|
|
110
|
+
release();
|
|
111
|
+
}
|
|
101
112
|
}
|
|
102
113
|
catch (err) {
|
|
103
114
|
jsonReply(res, 500, { ok: false, error: err.message });
|
|
@@ -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
|
};
|
|
@@ -14,9 +14,9 @@ export const AGENT_TEAM_PAGE_HTML = `<!DOCTYPE html>
|
|
|
14
14
|
.status-row{display:flex;align-items:center;justify-content:space-between;gap:12px;margin:0 3px 10px;color:var(--muted);font-size:11px}.workspace-name{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.save-state{min-width:78px;text-align:right;color:var(--success);font-weight:700}.save-state::before{content:"● ";font-size:9px}.save-state.saving{color:#b7791f}.save-state.error{color:var(--danger)}
|
|
15
15
|
.board-scroll{overflow-x:auto;padding-bottom:12px}.board{display:grid;grid-template-columns:repeat(5,minmax(230px,1fr));gap:11px;min-width:1200px}.column{display:flex;flex-direction:column;min-height:calc(100vh - 230px);background:var(--soft);border:1px solid var(--line);border-radius:15px;overflow:hidden}.column.drag-over{border-color:var(--accent);box-shadow:0 0 0 3px rgba(103,88,232,.13)}.column-head{display:flex;align-items:center;gap:8px;padding:11px 10px 7px 13px}.column-dot{width:7px;height:7px;border-radius:50%;background:var(--accent)}.column-title{font-size:13px;font-weight:800}.count{margin-left:auto;display:inline-grid;place-items:center;min-width:23px;height:23px;padding:0 7px;border-radius:999px;background:var(--panel);border:1px solid var(--line);color:var(--muted);font-size:10px}.column-add{width:27px;height:27px;display:grid;place-items:center;padding:0;border-color:transparent;border-radius:8px;background:transparent;color:var(--muted);font-size:19px;font-weight:500;line-height:1}.column-add:hover{border-color:var(--line);background:var(--panel);color:var(--accent)}.task-list{flex:1;min-height:100px;padding:4px 10px 10px}.task{position:relative;background:var(--panel2);border:1px solid var(--line);border-radius:11px;padding:12px;margin:8px 0;box-shadow:0 4px 15px rgba(34,42,65,.045);cursor:pointer}.task:hover{border-color:#d1d5e1;box-shadow:0 8px 22px rgba(34,42,65,.08)}.task.dragging{opacity:.35;cursor:grabbing}.task h3{font-size:13px;line-height:1.45;margin:0 42px 6px 0}.task p{font-size:11px;line-height:1.55;color:var(--muted);margin:0;white-space:pre-wrap;display:-webkit-box;-webkit-line-clamp:4;-webkit-box-orient:vertical;overflow:hidden}.task-actions{display:flex;gap:4px;position:absolute;right:7px;top:7px;opacity:0;transition:opacity .15s}.task:hover .task-actions,.task:focus-within .task-actions{opacity:1}.icon-btn{padding:3px 6px;border-radius:6px;background:var(--soft);color:var(--muted);font-size:10px}.empty{padding:20px 8px;text-align:center;color:#9aa3b4;font-size:11px}
|
|
16
16
|
.task.running{cursor:default;border-color:#cfc8ff}.task-run{margin-top:10px;padding-top:9px;border-top:1px solid var(--line)}.task-run-head{display:flex;align-items:center;justify-content:space-between;gap:7px}.run-status{font-size:10px;font-weight:800;color:var(--muted)}.run-status.running,.run-status.queued{color:var(--accent)}.run-status.succeeded{color:var(--success)}.run-status.failed,.run-status.interrupted{color:var(--danger)}.run-result{margin-top:6px!important;padding:7px 8px;border-radius:7px;background:var(--soft);display:-webkit-box!important;-webkit-line-clamp:3!important}.run-button{padding:5px 8px;font-size:10px;border-radius:7px}.run-button.primary{box-shadow:none}
|
|
17
|
-
.modal-backdrop{position:fixed;inset:0;z-index:20;background:rgba(24,32,51,.36);display:grid;place-items:center;padding:20px;backdrop-filter:blur(3px)}.modal{width:min(560px,100%);background:var(--panel);border:1px solid var(--line);border-radius:16px;padding:20px;box-shadow:0 28px 90px rgba(35,43,65,.24)}.modal h2{margin:0 0 16px}.field{display:grid;gap:7px;margin-bottom:14px}.field label{font-size:13px;color:var(--text);font-weight:650}.field textarea{min-height:150px;resize:vertical}.modal-actions{display:flex;justify-content:flex-end;gap:9px}.directory-modal{width:min(820px,100%)}.directory-layout{display:grid;grid-template-columns:180px minmax(0,1fr);min-height:430px;border:1px solid var(--line);border-radius:12px;overflow:hidden;margin-bottom:16px}.directory-sidebar{padding:12px;background:var(--soft);border-right:1px solid var(--line)}.directory-label{margin:2px 6px 9px;color:var(--muted);font-size:11px;font-weight:800;letter-spacing:.05em}.directory-locations{display:grid;gap:4px}.directory-location{width:100%;padding:8px 9px;border:0;background:transparent;text-align:left;font-size:12px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.directory-location:hover{border:0;background:var(--accent-soft);color:var(--accent)}.directory-main{min-width:0;display:flex;flex-direction:column;padding:12px}.directory-toolbar{display:grid;grid-template-columns:auto auto minmax(0,1fr);gap:7px}.directory-toolbar button{padding:8px 11px}.directory-toolbar input{min-width:0;padding:8px 10px}.directory-list{flex:1;height:310px;overflow:auto;margin-top:10px;border:1px solid var(--line);border-radius:10px;background:var(--soft);padding:6px}.directory-row{width:100%;display:flex;align-items:center;gap:9px;padding:9px 10px;border:0;background:transparent;text-align:left;font-size:12px}.directory-row:hover{border:0;background:#fff}.directory-row.selected{border:0;background:var(--accent-soft);color:var(--accent)}.directory-folder-icon{color:#b7791f;font-size:15px}.directory-empty{padding:46px 16px;text-align:center;color:var(--muted);font-size:12px}.directory-footer{display:flex;align-items:center;justify-content:space-between;gap:12px;margin-top:10px;color:var(--muted);font-size:11px}.directory-footer label{display:flex;align-items:center;gap:6px;white-space:nowrap}.directory-footer input{width:15px;height:15px;margin:0}.directory-selected{min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;text-align:right}.toast{position:fixed;right:22px;bottom:22px;z-index:30;max-width:min(420px,calc(100vw - 44px));padding:12px 15px;border-radius:10px;background:#253047;color:#fff;border:1px solid #35415a;box-shadow:0 16px 50px rgba(35,43,65,.25)}.toast.error{border-color:#ef9bad;background:#9f2942}
|
|
17
|
+
.modal-backdrop{position:fixed;inset:0;z-index:20;background:rgba(24,32,51,.36);display:grid;place-items:center;padding:20px;backdrop-filter:blur(3px)}.modal{width:min(560px,100%);background:var(--panel);border:1px solid var(--line);border-radius:16px;padding:20px;box-shadow:0 28px 90px rgba(35,43,65,.24)}.modal h2{margin:0 0 16px}.field{display:grid;gap:7px;margin-bottom:14px}.field label{font-size:13px;color:var(--text);font-weight:650}.field textarea{min-height:150px;resize:vertical}.modal-actions{display:flex;justify-content:flex-end;gap:9px}.task-detail-modal{width:min(980px,100%);max-height:calc(100vh - 40px);padding:0;display:flex;flex-direction:column;overflow:hidden}.task-detail-scroll{min-height:0;overflow-y:auto;padding:22px 24px}.task-detail-modal>.modal-actions{flex:none;padding:14px 24px;border-top:1px solid var(--line);background:var(--panel)}.task-execution-details{margin-top:22px;padding-top:20px;border-top:1px solid var(--line)}.task-execution-title{display:flex;align-items:center;justify-content:space-between;gap:12px;margin-bottom:12px}.task-execution-title h3{margin:0;font-size:16px}.run-meta{display:grid;grid-template-columns:repeat(4,minmax(0,1fr));gap:8px;margin-bottom:14px}.run-meta-item{padding:9px 10px;border:1px solid var(--line);border-radius:9px;background:var(--soft);min-width:0}.run-meta-item span{display:block;color:var(--muted);font-size:10px;margin-bottom:4px}.run-meta-item strong{display:block;font-size:11px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.execution-timeline{display:grid;gap:9px}.timeline-entry{border:1px solid var(--line);border-radius:10px;background:var(--soft);overflow:hidden}.timeline-entry.tool_use{border-left:3px solid var(--accent)}.timeline-entry.tool_result{border-left:3px solid var(--success)}.timeline-entry.error{border-left-color:var(--danger)}.timeline-head{display:flex;justify-content:space-between;gap:12px;padding:8px 10px;border-bottom:1px solid var(--line);color:var(--muted);font-size:10px;font-weight:800}.timeline-entry pre{margin:0;padding:11px 12px;max-height:none;overflow:visible;white-space:pre-wrap;overflow-wrap:anywhere;font:11px/1.65 ui-monospace,SFMono-Regular,Consolas,monospace;color:var(--text)}.timeline-empty{padding:22px;border:1px dashed var(--line);border-radius:10px;text-align:center;color:var(--muted);font-size:12px}.directory-modal{width:min(820px,100%)}.directory-layout{display:grid;grid-template-columns:180px minmax(0,1fr);min-height:430px;border:1px solid var(--line);border-radius:12px;overflow:hidden;margin-bottom:16px}.directory-sidebar{padding:12px;background:var(--soft);border-right:1px solid var(--line)}.directory-label{margin:2px 6px 9px;color:var(--muted);font-size:11px;font-weight:800;letter-spacing:.05em}.directory-locations{display:grid;gap:4px}.directory-location{width:100%;padding:8px 9px;border:0;background:transparent;text-align:left;font-size:12px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.directory-location:hover{border:0;background:var(--accent-soft);color:var(--accent)}.directory-main{min-width:0;display:flex;flex-direction:column;padding:12px}.directory-toolbar{display:grid;grid-template-columns:auto auto minmax(0,1fr);gap:7px}.directory-toolbar button{padding:8px 11px}.directory-toolbar input{min-width:0;padding:8px 10px}.directory-list{flex:1;height:310px;overflow:auto;margin-top:10px;border:1px solid var(--line);border-radius:10px;background:var(--soft);padding:6px}.directory-row{width:100%;display:flex;align-items:center;gap:9px;padding:9px 10px;border:0;background:transparent;text-align:left;font-size:12px}.directory-row:hover{border:0;background:#fff}.directory-row.selected{border:0;background:var(--accent-soft);color:var(--accent)}.directory-folder-icon{color:#b7791f;font-size:15px}.directory-empty{padding:46px 16px;text-align:center;color:var(--muted);font-size:12px}.directory-footer{display:flex;align-items:center;justify-content:space-between;gap:12px;margin-top:10px;color:var(--muted);font-size:11px}.directory-footer label{display:flex;align-items:center;gap:6px;white-space:nowrap}.directory-footer input{width:15px;height:15px;margin:0}.directory-selected{min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;text-align:right}.toast{position:fixed;right:22px;bottom:22px;z-index:30;max-width:min(420px,calc(100vw - 44px));padding:12px 15px;border-radius:10px;background:#253047;color:#fff;border:1px solid #35415a;box-shadow:0 16px 50px rgba(35,43,65,.25)}.toast.error{border-color:#ef9bad;background:#9f2942}
|
|
18
18
|
@media(max-width:1100px){.app-shell{grid-template-columns:78px minmax(0,1fr)}.sidebar{padding:22px 12px;align-items:center}.brand span:last-child,.nav-item span:last-child,.side-note{display:none}.nav-item{width:45px;height:45px;justify-content:center;padding:0}.workspace-bar{grid-template-columns:1fr 1fr}.workspace-bar input{grid-column:1/-1}.main-agent-bar{grid-template-columns:1fr}.main-agent-controls{justify-content:flex-end}.board{min-width:1200px}}
|
|
19
|
-
@media(max-width:700px){.app-shell{display:block}.sidebar{display:none}.shell{padding:14px}.top{align-items:flex-start}.workspace-bar{grid-template-columns:1fr}.workspace-bar input{grid-column:auto}.main-agent-controls{display:grid;grid-template-columns:minmax(0,1fr) auto;justify-content:stretch}.main-agent-controls select{min-width:0}.board{min-width:1180px}.directory-layout{grid-template-columns:1fr}.directory-sidebar{border-right:0;border-bottom:1px solid var(--line)}.directory-locations{display:flex;overflow-x:auto}.directory-location{width:auto;min-width:max-content}.directory-toolbar{grid-template-columns:auto auto 1fr}.directory-list{height:270px}}
|
|
19
|
+
@media(max-width:700px){.app-shell{display:block}.sidebar{display:none}.shell{padding:14px}.top{align-items:flex-start}.workspace-bar{grid-template-columns:1fr}.workspace-bar input{grid-column:auto}.main-agent-controls{display:grid;grid-template-columns:minmax(0,1fr) auto;justify-content:stretch}.main-agent-controls select{min-width:0}.board{min-width:1180px}.task-detail-scroll{padding:18px}.task-detail-modal>.modal-actions{padding:12px 18px}.run-meta{grid-template-columns:repeat(2,minmax(0,1fr))}.directory-layout{grid-template-columns:1fr}.directory-sidebar{border-right:0;border-bottom:1px solid var(--line)}.directory-locations{display:flex;overflow-x:auto}.directory-location{width:auto;min-width:max-content}.directory-toolbar{grid-template-columns:auto auto 1fr}.directory-list{height:270px}}
|
|
20
20
|
</style>
|
|
21
21
|
</head>
|
|
22
22
|
<body>
|
|
@@ -53,11 +53,18 @@ export const AGENT_TEAM_PAGE_HTML = `<!DOCTYPE html>
|
|
|
53
53
|
</main>
|
|
54
54
|
</div>
|
|
55
55
|
<div id="modal" class="modal-backdrop hidden" role="dialog" aria-modal="true">
|
|
56
|
-
<form class="modal" id="task-form">
|
|
57
|
-
<
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
56
|
+
<form class="modal task-detail-modal" id="task-form">
|
|
57
|
+
<div class="task-detail-scroll">
|
|
58
|
+
<h2 id="modal-title">添加任务</h2>
|
|
59
|
+
<div class="field"><label for="task-title">标题</label><input id="task-title" maxlength="200" required></div>
|
|
60
|
+
<div class="field"><label for="task-description">描述</label><textarea id="task-description" maxlength="20000"></textarea></div>
|
|
61
|
+
<section id="task-execution-details" class="task-execution-details hidden">
|
|
62
|
+
<div class="task-execution-title"><h3>完整执行过程</h3><span id="task-run-status" class="run-status"></span></div>
|
|
63
|
+
<div id="task-run-meta" class="run-meta"></div>
|
|
64
|
+
<div id="task-transcript" class="execution-timeline"></div>
|
|
65
|
+
</section>
|
|
66
|
+
</div>
|
|
67
|
+
<div class="modal-actions"><button type="button" id="cancel" class="ghost">取消</button><button type="submit" id="task-save" class="primary">保存</button></div>
|
|
61
68
|
</form>
|
|
62
69
|
</div>
|
|
63
70
|
<div id="dm-modal" class="modal-backdrop hidden" role="dialog" aria-modal="true" aria-labelledby="dm-modal-title">
|
|
@@ -100,7 +107,7 @@ export const AGENT_TEAM_PAGE_HTML = `<!DOCTYPE html>
|
|
|
100
107
|
<div id="toast" class="toast hidden"></div>
|
|
101
108
|
<script>
|
|
102
109
|
(function(){
|
|
103
|
-
var state={board:null,binding:null,runs:[],runPollTimer:null,runPollBusy:false,workspaces:[],agentOptions:[],dragTaskId:null,modalTaskId:null,modalColumnId:null,relinkBoardId:null,pendingAgentId:null,pendingWorkspacePath:null,workspaceSelectionVersion:0,directoryLocations:[],directoryPath:null,directoryParentPath:null,directorySelectedPath:null,directoryBrowseVersion:0,contactTimer:null,contactChecking:false,mutationQueue:Promise.resolve()};
|
|
110
|
+
var state={board:null,binding:null,runs:[],runPollTimer:null,runPollBusy:false,workspaces:[],agentOptions:[],dragTaskId:null,modalTaskId:null,modalColumnId:null,modalDetailVersion:0,relinkBoardId:null,pendingAgentId:null,pendingWorkspacePath:null,workspaceSelectionVersion:0,directoryLocations:[],directoryPath:null,directoryParentPath:null,directorySelectedPath:null,directoryBrowseVersion:0,contactTimer:null,contactChecking:false,mutationQueue:Promise.resolve()};
|
|
104
111
|
var columns=['brainstorm','todo','doing','done','on_hold'];
|
|
105
112
|
var recent=document.getElementById('recent');
|
|
106
113
|
var pathInput=document.getElementById('workspace-path');
|
|
@@ -165,7 +172,7 @@ export const AGENT_TEAM_PAGE_HTML = `<!DOCTYPE html>
|
|
|
165
172
|
function isActiveRun(run){return !!run&&(run.state==='queued'||run.state==='running');}
|
|
166
173
|
function runLabel(run){var labels={queued:'等待执行',running:run.stopRequestedAt?'正在停止':'主 Agent 执行中',succeeded:'执行成功',failed:'执行失败',canceled:'已停止',interrupted:'重启时中断'};return labels[run.state]||run.state;}
|
|
167
174
|
function scheduleRunPoll(){if(state.runPollTimer){clearTimeout(state.runPollTimer);state.runPollTimer=null;}if(!state.runs.some(isActiveRun))return;state.runPollTimer=setTimeout(pollRuns,1500);}
|
|
168
|
-
async function pollRuns(){if(state.runPollBusy||!state.board)return scheduleRunPoll();state.runPollBusy=true;var boardId=state.board.boardId;try{var results=await Promise.all([api('/api/agent-team/boards/'+encodeURIComponent(boardId)),api('/api/agent-team/boards/'+encodeURIComponent(boardId)+'/runs')]);if(state.board&&state.board.boardId===boardId){state.board=results[0].board;state.binding=results[0].binding||null;state.runs=results[1].runs||[];renderBoard();if(!state.runs.some(isActiveRun))setSaving('已保存','');}}catch(error){toast(error.message,true);}finally{state.runPollBusy=false;scheduleRunPoll();}}
|
|
175
|
+
async function pollRuns(){if(state.runPollBusy||!state.board)return scheduleRunPoll();state.runPollBusy=true;var boardId=state.board.boardId;try{var results=await Promise.all([api('/api/agent-team/boards/'+encodeURIComponent(boardId)),api('/api/agent-team/boards/'+encodeURIComponent(boardId)+'/runs')]);if(state.board&&state.board.boardId===boardId){state.board=results[0].board;state.binding=results[0].binding||null;state.runs=results[1].runs||[];renderBoard();if(state.modalTaskId)await loadOpenTaskRunDetails(false);if(!state.runs.some(isActiveRun))setSaving('已保存','');}}catch(error){toast(error.message,true);}finally{state.runPollBusy=false;scheduleRunPoll();}}
|
|
169
176
|
async function startTaskRun(task){if(!state.board)return;if(!state.binding||state.binding.status!=='ready')return toast('请先设置可用的主 Agent',true);try{setSaving('正在交给主 Agent…','saving');var result=await request('POST','/api/agent-team/boards/'+encodeURIComponent(state.board.boardId)+'/tasks/'+encodeURIComponent(task.id)+'/run',{expectedRevision:state.board.revision});state.board=result.board;state.runs=[result.run].concat(state.runs.filter(function(run){return run.runId!==result.run.runId;}));renderBoard();scheduleRunPoll();setSaving('任务执行中','saving');}catch(error){if(error.code==='revision_conflict')await reloadBoard();toast(error.message,true);setSaving('执行失败','error');}}
|
|
170
177
|
async function stopTaskRun(run){if(!state.board)return;try{var result=await request('POST','/api/agent-team/boards/'+encodeURIComponent(state.board.boardId)+'/runs/'+encodeURIComponent(run.runId)+'/stop',{});state.runs=state.runs.map(function(item){return item.runId===result.run.runId?result.run:item;});renderBoard();scheduleRunPoll();toast('已请求主 Agent 停止任务');}catch(error){toast(error.message,true);}}
|
|
171
178
|
function activeTasks(columnId){return state.board?state.board.tasks.filter(function(task){return !task.deletedAt&&task.columnId===columnId;}).sort(function(a,b){return a.order-b.order;}):[];}
|
|
@@ -176,9 +183,9 @@ export const AGENT_TEAM_PAGE_HTML = `<!DOCTYPE html>
|
|
|
176
183
|
if(!tasks.length){var empty=document.createElement('div');empty.className='empty';empty.textContent='暂无任务';list.appendChild(empty);}
|
|
177
184
|
tasks.forEach(function(task){var run=latestRun(task.id);var active=isActiveRun(run);var dragged=false;var card=document.createElement('article');card.className='task'+(active?' running':'');card.draggable=!active;card.dataset.taskId=task.id;
|
|
178
185
|
var title=document.createElement('h3');title.textContent=task.title;card.appendChild(title);if(task.description){var desc=document.createElement('p');desc.textContent=task.description;card.appendChild(desc);}
|
|
179
|
-
var actions=document.createElement('div');actions.className='task-actions';var edit=document.createElement('button');edit.type='button';edit.className='icon-btn';edit.textContent='编辑';edit.
|
|
186
|
+
var actions=document.createElement('div');actions.className='task-actions';var edit=document.createElement('button');edit.type='button';edit.className='icon-btn';edit.textContent='编辑';edit.addEventListener('click',function(event){event.stopPropagation();showEdit(task);});var del=document.createElement('button');del.type='button';del.className='icon-btn';del.textContent='删除';del.disabled=active;del.addEventListener('click',function(event){event.stopPropagation();removeTask(task);});actions.append(edit,del);card.appendChild(actions);
|
|
180
187
|
var runBox=document.createElement('div');runBox.className='task-run';var runHead=document.createElement('div');runHead.className='task-run-head';var status=document.createElement('span');status.className='run-status '+(run?run.state:'');status.textContent=run?runLabel(run)+(run.attempt>1?' · 第 '+run.attempt+' 次':''):'尚未执行';var runButton=document.createElement('button');runButton.type='button';runButton.className='run-button '+(!run||!active?'primary':'danger');runButton.textContent=active?'停止':(run?'重试':'交给主 Agent');runButton.addEventListener('click',function(event){event.stopPropagation();if(active)stopTaskRun(run);else startTaskRun(task);});runHead.append(status,runButton);runBox.appendChild(runHead);if(run&&(run.result||run.error)){var resultText=document.createElement('p');resultText.className='run-result';resultText.textContent=run.error||run.result;runBox.appendChild(resultText);}card.appendChild(runBox);
|
|
181
|
-
card.addEventListener('click',function(){if(!
|
|
188
|
+
card.addEventListener('click',function(){if(!dragged)showEdit(task);});card.addEventListener('dragstart',function(event){if(active){event.preventDefault();return;}dragged=true;state.dragTaskId=task.id;card.classList.add('dragging');event.dataTransfer.effectAllowed='move';});card.addEventListener('dragend',function(){state.dragTaskId=null;card.classList.remove('dragging');document.querySelectorAll('.column').forEach(function(el){el.classList.remove('drag-over');});setTimeout(function(){dragged=false;},0);});list.appendChild(card);
|
|
182
189
|
});
|
|
183
190
|
});
|
|
184
191
|
}
|
|
@@ -201,9 +208,16 @@ export const AGENT_TEAM_PAGE_HTML = `<!DOCTYPE html>
|
|
|
201
208
|
if(!state.board||!agentId)return;state.pendingAgentId=agentId;
|
|
202
209
|
state.mutationQueue=state.mutationQueue.then(async function(){setSaving('创建主 Agent…','saving');mainAgentButton.disabled=true;try{var result=await request('POST','/api/agent-team/boards/'+encodeURIComponent(state.board.boardId)+'/main-agent',{expectedRevision:state.board.revision,agentId:agentId});state.board=result.board;state.binding=result.binding;state.pendingAgentId=null;renderBoard();setSaving('已保存','');toast('主 Agent 群已就绪');return true;}catch(error){setSaving('设置失败','error');renderBoard();if(error.code==='feishu_dm_required')showDmModal();else if(error.code==='revision_conflict'){try{await reloadBoard();toast('其他页面修改了项目,已载入最新版本',true);}catch(reloadError){toast(reloadError.message,true);}}else toast(error.message,true);return false;}});return state.mutationQueue;
|
|
203
210
|
}
|
|
204
|
-
function
|
|
205
|
-
function
|
|
206
|
-
function
|
|
211
|
+
function formatRunTime(value){if(!value)return '—';var date=new Date(value);return Number.isNaN(date.getTime())?String(value):date.toLocaleString();}
|
|
212
|
+
function addRunMeta(root,label,value){var item=document.createElement('div');item.className='run-meta-item';var key=document.createElement('span');key.textContent=label;var content=document.createElement('strong');content.textContent=value||'—';content.title=value||'—';item.append(key,content);root.appendChild(item);}
|
|
213
|
+
function transcriptLabel(entry){var labels={prompt:'任务指令',thinking:'思考',text:'主 Agent 回复',tool_use:'调用工具',tool_result:'工具结果',search:'联网搜索',compact:'上下文压缩',status:'状态变化',notice:'提示'};var label=labels[entry.type]||entry.type;if(entry.name)label+=' · '+entry.name;return label;}
|
|
214
|
+
function transcriptContent(entry){if(entry.type==='tool_use')return entry.input||'';if(entry.type==='tool_result')return entry.output||'';return entry.text||'';}
|
|
215
|
+
function addTranscriptEntry(root,entry){var row=document.createElement('article');row.className='timeline-entry '+entry.type+(entry.isError?' error':'');var head=document.createElement('div');head.className='timeline-head';var label=document.createElement('span');label.textContent=transcriptLabel(entry);var time=document.createElement('span');time.textContent=formatRunTime(entry.at);head.append(label,time);var body=document.createElement('pre');body.textContent=transcriptContent(entry)||'(无文本内容)';row.append(head,body);root.appendChild(row);}
|
|
216
|
+
function renderTaskExecution(task,run){var details=document.getElementById('task-execution-details');var title=document.getElementById('task-title');var description=document.getElementById('task-description');var save=document.getElementById('task-save');var cancel=document.getElementById('cancel');var active=isActiveRun(run);title.disabled=active;description.disabled=active;save.classList.toggle('hidden',active);cancel.textContent=active?'关闭':'取消';details.classList.remove('hidden');var status=document.getElementById('task-run-status');status.className='run-status '+(run?run.state:'');status.textContent=run?runLabel(run):'尚未执行';var meta=document.getElementById('task-run-meta');meta.innerHTML='';if(run){addRunMeta(meta,'执行次数','第 '+run.attempt+' 次');addRunMeta(meta,'执行 Agent',run.agentId);addRunMeta(meta,'开始时间',formatRunTime(run.startedAt||run.createdAt));addRunMeta(meta,'结束时间',formatRunTime(run.finishedAt));}else{addRunMeta(meta,'任务状态','尚未交给主 Agent');addRunMeta(meta,'创建时间',formatRunTime(task.createdAt));addRunMeta(meta,'更新时间',formatRunTime(task.updatedAt));}var transcript=document.getElementById('task-transcript');transcript.innerHTML='';var entries=run&&Array.isArray(run.transcript)?run.transcript.slice():[];var hasText=entries.some(function(entry){return entry.type==='text';});if(run&&run.result&&!hasText)entries.push({type:'text',at:run.finishedAt||run.updatedAt,text:run.result});if(run&&run.error)entries.push({type:'notice',at:run.finishedAt||run.updatedAt,text:run.error,isError:true});if(!entries.length){var empty=document.createElement('div');empty.className='timeline-empty';empty.textContent=run?'暂时还没有可显示的执行事件':'任务尚未执行,交给主 Agent 后会在这里记录完整过程。';transcript.appendChild(empty);return;}entries.forEach(function(entry){addTranscriptEntry(transcript,entry);});}
|
|
217
|
+
async function loadOpenTaskRunDetails(showError){if(!state.board||!state.modalTaskId)return;var task=state.board.tasks.find(function(candidate){return candidate.id===state.modalTaskId&&!candidate.deletedAt;});if(!task)return;var summary=latestRun(task.id);if(!summary){renderTaskExecution(task,null);return;}var version=++state.modalDetailVersion;try{var result=await api('/api/agent-team/boards/'+encodeURIComponent(state.board.boardId)+'/runs/'+encodeURIComponent(summary.runId));if(version!==state.modalDetailVersion||state.modalTaskId!==task.id)return;renderTaskExecution(task,result.run);}catch(error){if(showError!==false)toast(error.message,true);}}
|
|
218
|
+
function showCreate(columnId){if(!state.board)return toast('请先打开一个工作目录',true);state.modalTaskId=null;state.modalColumnId=columnId;document.getElementById('modal-title').textContent='添加任务';document.getElementById('task-title').value='';document.getElementById('task-description').value='';document.getElementById('task-title').disabled=false;document.getElementById('task-description').disabled=false;document.getElementById('task-save').classList.remove('hidden');document.getElementById('cancel').textContent='取消';document.getElementById('task-execution-details').classList.add('hidden');document.getElementById('modal').classList.remove('hidden');setTimeout(function(){document.getElementById('task-title').focus();},0);}
|
|
219
|
+
function showEdit(task){state.modalTaskId=task.id;state.modalColumnId=task.columnId;document.getElementById('modal-title').textContent='任务详情';document.getElementById('task-title').value=task.title;document.getElementById('task-description').value=task.description;renderTaskExecution(task,latestRun(task.id));document.getElementById('modal').classList.remove('hidden');loadOpenTaskRunDetails(true);}
|
|
220
|
+
function closeModal(){state.modalDetailVersion++;document.getElementById('modal').classList.add('hidden');state.modalTaskId=null;}
|
|
207
221
|
function removeTask(task){if(!confirm('删除任务“'+task.title+'”?'))return;mutate(function(){return request('DELETE','/api/agent-team/boards/'+encodeURIComponent(state.board.boardId)+'/tasks/'+encodeURIComponent(task.id),{expectedRevision:state.board.revision});});}
|
|
208
222
|
|
|
209
223
|
document.querySelectorAll('[data-add]').forEach(function(button){button.addEventListener('click',function(){showCreate(button.dataset.add);});});
|
package/dist/src/cards.js
CHANGED
|
@@ -143,7 +143,10 @@ export function buildHelpCard(userText, opts = {}) {
|
|
|
143
143
|
"发送 **/ask** 以问答模式提问(只读,不执行写操作)",
|
|
144
144
|
"发送 **/usage** 查看当前 Agent 的用量或余额",
|
|
145
145
|
"发送 **/restart** 重启 ChatCCC 进程",
|
|
146
|
+
"发送 **/restart safe** 等待现有任务完成后安全重启",
|
|
146
147
|
"发送 **/update** 更新并重启(仅 npm 全局安装可用)",
|
|
148
|
+
"发送 **/update safe** 等待现有任务完成后安全更新",
|
|
149
|
+
"发送 **/safestatus** 查看安全维护状态,**/cancelsf** 取消等待中的预约",
|
|
147
150
|
ABD_HELP_LINE,
|
|
148
151
|
].join("\n");
|
|
149
152
|
return JSON.stringify({
|
|
@@ -4,6 +4,7 @@ import { existsSync } from "node:fs";
|
|
|
4
4
|
import { mkdir, readFile, readdir, rename, rm, stat, writeFile } from "node:fs/promises";
|
|
5
5
|
import { homedir } from "node:os";
|
|
6
6
|
import { delimiter, dirname, join, relative, resolve } from "node:path";
|
|
7
|
+
import { isSafeMaintenanceAdmissionClosed } from "../safe-maintenance.js";
|
|
7
8
|
const STEP_DEFINITIONS = [
|
|
8
9
|
["preflight", "检查运行环境"],
|
|
9
10
|
["prepare", "准备临时目录"],
|
|
@@ -100,6 +101,9 @@ export class EngineManager {
|
|
|
100
101
|
listSpecs() {
|
|
101
102
|
return [...this.specs.values()];
|
|
102
103
|
}
|
|
104
|
+
getActiveInstallIds() {
|
|
105
|
+
return [...this.activeInstalls.keys()].sort();
|
|
106
|
+
}
|
|
103
107
|
getSpec(engineId) {
|
|
104
108
|
const spec = this.specs.get(engineId);
|
|
105
109
|
if (!spec)
|
|
@@ -133,6 +137,9 @@ export class EngineManager {
|
|
|
133
137
|
const running = this.activeInstalls.get(engineId);
|
|
134
138
|
if (running)
|
|
135
139
|
return this.readJob(this.getSpec(engineId)).then((job) => job ?? this.newJob(this.getSpec(engineId)));
|
|
140
|
+
if (isSafeMaintenanceAdmissionClosed()) {
|
|
141
|
+
throw new Error("ChatCCC 正在等待安全维护,暂不接受新的依赖安装任务。");
|
|
142
|
+
}
|
|
136
143
|
const spec = this.getSpec(engineId);
|
|
137
144
|
const job = this.newJob(spec);
|
|
138
145
|
await this.persistJob(spec, job);
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
export function appendExecutionTranscriptBlock(block, state, at = new Date().toISOString()) {
|
|
2
|
+
switch (block.type) {
|
|
3
|
+
case "text_reset":
|
|
4
|
+
state.transcript = [];
|
|
5
|
+
return;
|
|
6
|
+
case "text":
|
|
7
|
+
appendText(state, block.text, at);
|
|
8
|
+
return;
|
|
9
|
+
case "text_final":
|
|
10
|
+
// Some adapters emit both deltas and an authoritative final snapshot. Keep the
|
|
11
|
+
// ordered deltas when present, otherwise retain the snapshot as the full reply.
|
|
12
|
+
if (!state.transcript.some((entry) => entry.type === "text"))
|
|
13
|
+
appendText(state, block.text, at);
|
|
14
|
+
return;
|
|
15
|
+
case "thinking":
|
|
16
|
+
appendEntry(state, { type: "thinking", at, text: block.thinking });
|
|
17
|
+
return;
|
|
18
|
+
case "tool_use":
|
|
19
|
+
appendEntry(state, {
|
|
20
|
+
type: "tool_use",
|
|
21
|
+
at,
|
|
22
|
+
name: block.name,
|
|
23
|
+
...(block.id ? { toolUseId: block.id } : {}),
|
|
24
|
+
input: stringifyTranscriptValue(block.input),
|
|
25
|
+
});
|
|
26
|
+
return;
|
|
27
|
+
case "tool_result":
|
|
28
|
+
appendEntry(state, {
|
|
29
|
+
type: "tool_result",
|
|
30
|
+
at,
|
|
31
|
+
...(findToolName(state.transcript, block.tool_use_id) ? { name: findToolName(state.transcript, block.tool_use_id) } : {}),
|
|
32
|
+
toolUseId: block.tool_use_id,
|
|
33
|
+
output: stringifyTranscriptValue(block.content),
|
|
34
|
+
...(block.is_error !== undefined ? { isError: block.is_error } : {}),
|
|
35
|
+
});
|
|
36
|
+
return;
|
|
37
|
+
case "search_result":
|
|
38
|
+
appendEntry(state, { type: "search", at, text: block.query });
|
|
39
|
+
return;
|
|
40
|
+
case "compact_boundary":
|
|
41
|
+
appendEntry(state, {
|
|
42
|
+
type: "compact",
|
|
43
|
+
at,
|
|
44
|
+
text: `${block.trigger === "manual" ? "手动" : "自动"}压缩:${block.pre_tokens} → ${block.post_tokens ?? "?"} tokens`,
|
|
45
|
+
});
|
|
46
|
+
return;
|
|
47
|
+
case "agent_status": {
|
|
48
|
+
const text = block.status === "compacting" ? "正在压缩上下文" : "正在生成回复";
|
|
49
|
+
const previous = state.transcript.at(-1);
|
|
50
|
+
if (previous?.type !== "status" || previous.text !== text)
|
|
51
|
+
appendEntry(state, { type: "status", at, text });
|
|
52
|
+
return;
|
|
53
|
+
}
|
|
54
|
+
case "redacted_thinking":
|
|
55
|
+
appendEntry(state, { type: "notice", at, text: "部分思考内容已被安全过滤" });
|
|
56
|
+
return;
|
|
57
|
+
case "agent_progress":
|
|
58
|
+
// Heartbeats carry no content and can occur very frequently. Persisting them
|
|
59
|
+
// would add noise without helping users reconstruct what happened.
|
|
60
|
+
return;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
export function isExecutionTranscriptEntry(value) {
|
|
64
|
+
if (!value || typeof value !== "object" || Array.isArray(value))
|
|
65
|
+
return false;
|
|
66
|
+
const entry = value;
|
|
67
|
+
const types = [
|
|
68
|
+
"prompt", "thinking", "text", "tool_use", "tool_result", "search", "compact", "status", "notice",
|
|
69
|
+
];
|
|
70
|
+
if (!types.includes(entry.type) || typeof entry.at !== "string")
|
|
71
|
+
return false;
|
|
72
|
+
for (const field of ["text", "name", "toolUseId", "input", "output"]) {
|
|
73
|
+
if (entry[field] !== undefined && typeof entry[field] !== "string")
|
|
74
|
+
return false;
|
|
75
|
+
}
|
|
76
|
+
return entry.isError === undefined || typeof entry.isError === "boolean";
|
|
77
|
+
}
|
|
78
|
+
function findToolName(entries, toolUseId) {
|
|
79
|
+
for (let index = entries.length - 1; index >= 0; index--) {
|
|
80
|
+
const entry = entries[index];
|
|
81
|
+
if (entry?.type === "tool_use" && entry.toolUseId === toolUseId)
|
|
82
|
+
return entry.name;
|
|
83
|
+
}
|
|
84
|
+
return undefined;
|
|
85
|
+
}
|
|
86
|
+
function appendText(state, text, at) {
|
|
87
|
+
if (!text)
|
|
88
|
+
return;
|
|
89
|
+
const previous = state.transcript.at(-1);
|
|
90
|
+
if (previous?.type === "text") {
|
|
91
|
+
previous.text = (previous.text ?? "") + text;
|
|
92
|
+
return;
|
|
93
|
+
}
|
|
94
|
+
appendEntry(state, { type: "text", at, text });
|
|
95
|
+
}
|
|
96
|
+
function appendEntry(state, entry) {
|
|
97
|
+
state.transcript.push(entry);
|
|
98
|
+
}
|
|
99
|
+
function stringifyTranscriptValue(value) {
|
|
100
|
+
if (typeof value === "string")
|
|
101
|
+
return value;
|
|
102
|
+
if (value === undefined)
|
|
103
|
+
return "";
|
|
104
|
+
try {
|
|
105
|
+
return JSON.stringify(value, null, 2);
|
|
106
|
+
}
|
|
107
|
+
catch {
|
|
108
|
+
return String(value);
|
|
109
|
+
}
|
|
110
|
+
}
|
package/dist/src/index.js
CHANGED
|
@@ -47,7 +47,7 @@ import { loadSessionRegistryForBinding, rebuildBindingsFromRegistry, resetState,
|
|
|
47
47
|
import { startChromeDevtoolsGuard, stopChromeDevtoolsGuard } from "./chrome-devtools-guard.js";
|
|
48
48
|
import { rebuildSessionChatsFromRegistry, setQueueConsumer, } from "./session-chat-binding.js";
|
|
49
49
|
import { fixStaleStreamStates } from "./stream-state.js";
|
|
50
|
-
import { handleCommand } from "./orchestrator.js";
|
|
50
|
+
import { configureSafeMaintenanceRuntime, handleCommand, recoverSafeMaintenanceAfterStartup, } from "./orchestrator.js";
|
|
51
51
|
import { createWechatAdapter, startWechatPlatform } from "./wechat-platform.js";
|
|
52
52
|
import { handleCodexResetCardAction } from "./codex-reset-actions.js";
|
|
53
53
|
import { resolveFeishuCardActionChatType } from "./card-action-routing.js";
|
|
@@ -103,11 +103,12 @@ function createFeishuAdapter() {
|
|
|
103
103
|
}
|
|
104
104
|
const feishuPlatform = createFeishuAdapter();
|
|
105
105
|
const wechatPlatform = createWechatAdapter();
|
|
106
|
+
configureSafeMaintenanceRuntime([feishuPlatform, wechatPlatform]);
|
|
106
107
|
setSessionPlatform(feishuPlatform);
|
|
107
108
|
configureAgentTeamMainAgent(feishuPlatform);
|
|
108
109
|
// 注册队列消费回调:session 生成完成后自动处理缓存消息
|
|
109
110
|
setQueueConsumer((platform, msg) => {
|
|
110
|
-
handleCommand(platform, msg.text, msg.chatId, msg.openId, msg.msgTimestamp, msg.chatType, msg.traceId).catch(err => console.error(`[${ts()}] Queue consume failed: ${err.message}`));
|
|
111
|
+
handleCommand(platform, msg.text, msg.chatId, msg.openId, msg.msgTimestamp, msg.chatType, msg.traceId, undefined, true).catch(err => console.error(`[${ts()}] Queue consume failed: ${err.message}`));
|
|
111
112
|
});
|
|
112
113
|
function getInnerEvent(data) {
|
|
113
114
|
return (data.event ?? data);
|
|
@@ -596,6 +597,7 @@ async function main() {
|
|
|
596
597
|
appendStartupTrace(opened ? "web-ui: opening simulate browser" : "web-ui: simulate browser unavailable", { url });
|
|
597
598
|
}
|
|
598
599
|
installShutdownHandlers(simServer, serviceLifecycle);
|
|
600
|
+
await recoverSafeMaintenanceAfterStartup();
|
|
599
601
|
return;
|
|
600
602
|
}
|
|
601
603
|
if (Number.isNaN(CHATCCC_PORT) || CHATCCC_PORT < 1 || CHATCCC_PORT > 65535) {
|
|
@@ -647,6 +649,7 @@ async function main() {
|
|
|
647
649
|
});
|
|
648
650
|
try {
|
|
649
651
|
await startConfiguredPlatforms(httpServer, { failOnFeishuError: true });
|
|
652
|
+
await recoverSafeMaintenanceAfterStartup();
|
|
650
653
|
return { ok: true };
|
|
651
654
|
}
|
|
652
655
|
catch (err) {
|
|
@@ -714,6 +717,7 @@ async function main() {
|
|
|
714
717
|
});
|
|
715
718
|
}
|
|
716
719
|
await startConfiguredPlatforms(httpServer, { failOnFeishuError: false });
|
|
720
|
+
await recoverSafeMaintenanceAfterStartup();
|
|
717
721
|
}
|
|
718
722
|
/**
|
|
719
723
|
* 生命周期健康检查发现 HTTP Server 已停止监听时,优先原地恢复同一个 Server。
|
package/dist/src/orchestrator.js
CHANGED
|
@@ -17,7 +17,7 @@ import { CLAUDE_MODEL, GIT_TIMEOUT_MS, PROJECT_ROOT, anthropicConfigDisplay, con
|
|
|
17
17
|
import { buildHelpCard, buildEffortCard, buildFastModeCard, buildModelCard, buildStatusCard, buildCdContent, buildCdCard, buildSessionsCard, buildQueuedCard, buildQueueFullCard, buildCodexUsageCard, } from "./cards.js";
|
|
18
18
|
import { formatGitResult, gitResultHeaderTemplate, runGitCommand, } from "./git-command.js";
|
|
19
19
|
import { clearSessionModelOverride, clearSessionEffortOverride, getSessionStatus, getAllSessionsStatus, initClaudeSession, lastMsgTimestamps, resumeAndPrompt, sessionInfoMap, setSessionModelOverride, setSessionEffortOverride, switchChatBinding, recordSessionRegistry, getAdapterForTool, getEffectiveModelForTool, getEffectiveEffortForTool, getEffectiveFastModeForTool, setSessionFastModeOverride, stopSession, loadSessionRegistryForBinding, removeSessionRegistryRecord, saveSessionTool, recordChatPlatform, } from "./session.js";
|
|
20
|
-
import { bindChatToSession, unbindChatFromSession, isSessionRunning, displayCards, enqueueMessage, cancelQueuedMessage, } from "./session-chat-binding.js";
|
|
20
|
+
import { bindChatToSession, unbindChatFromSession, isSessionRunning, displayCards, enqueueMessage, cancelQueuedMessage, getSessionDrainSnapshot, } from "./session-chat-binding.js";
|
|
21
21
|
import { getCodexUsageSummary, getTenantAccessToken, sendPostMessage } from "./feishu-platform.js";
|
|
22
22
|
import { getCursorUsageSummary } from "./cursor-usage.js";
|
|
23
23
|
import { getChatGptSubscriptionStatus } from "./chatgpt-subscription.js";
|
|
@@ -25,8 +25,10 @@ import { applySharedPrefix } from "./shared-prefix.js";
|
|
|
25
25
|
import { sessionChatName } from "./session-name.js";
|
|
26
26
|
import { reloadRuntimeConfig } from "./runtime-reload.js";
|
|
27
27
|
import { acquireUpdateCommandGuard } from "./update-command-guard.js";
|
|
28
|
-
import { createInternalRestartEnv } from "./startup-lifecycle.js";
|
|
28
|
+
import { createInternalRestartEnv, INTERNAL_RESTART_ENV_VAR } from "./startup-lifecycle.js";
|
|
29
29
|
import { resolveChatCccRuntimeSpawnSpec } from "./runtime-entry.js";
|
|
30
|
+
import { engineManager } from "./engines/engine-specs.js";
|
|
31
|
+
import { beginSafeMaintenanceTrackedWork, isSafeMaintenanceAdmissionClosed, safeMaintenanceCoordinator, } from "./safe-maintenance.js";
|
|
30
32
|
import { feishuP2pContactStore, isValidFeishuOpenId, } from "./agent-team/repositories/feishu-p2p-contact-store.js";
|
|
31
33
|
// ---------------------------------------------------------------------------
|
|
32
34
|
// 辅助函数
|
|
@@ -577,10 +579,11 @@ function updLog(msg) {
|
|
|
577
579
|
catch { }
|
|
578
580
|
}
|
|
579
581
|
/** 同步更新 npm 全局包并 spawn 新进程重启。不依赖 systemd 或任何服务管理器。 */
|
|
580
|
-
function syncUpdateAndRestart() {
|
|
582
|
+
function syncUpdateAndRestart(options = {}) {
|
|
581
583
|
updLog(`sync update start, pid=${process.pid}`);
|
|
582
584
|
appendStartupTrace("update: sync update start", { pid: process.pid });
|
|
583
585
|
const npmCmd = process.platform === "win32" ? "npm.cmd" : "npm";
|
|
586
|
+
let updateSucceeded = false;
|
|
584
587
|
// 1. npm update
|
|
585
588
|
updLog(`running: ${npmCmd} update -g chatccc`);
|
|
586
589
|
appendStartupTrace("update: npm update begin", { npmCmd });
|
|
@@ -590,6 +593,7 @@ function syncUpdateAndRestart() {
|
|
|
590
593
|
const elapsed = Date.now() - t0;
|
|
591
594
|
updLog(`npm update OK (${elapsed}ms): ${out.slice(0, 500)}`);
|
|
592
595
|
appendStartupTrace("update: npm update OK", { elapsedMs: elapsed, outputLen: out.length });
|
|
596
|
+
updateSucceeded = true;
|
|
593
597
|
}
|
|
594
598
|
catch (e) {
|
|
595
599
|
const elapsed = Date.now() - t0;
|
|
@@ -605,6 +609,7 @@ function syncUpdateAndRestart() {
|
|
|
605
609
|
const elapsed2 = Date.now() - t1;
|
|
606
610
|
updLog(`npm install fallback OK (${elapsed2}ms): ${out2.slice(0, 500)}`);
|
|
607
611
|
appendStartupTrace("update: npm install fallback OK", { elapsedMs: elapsed2, outputLen: out2.length });
|
|
612
|
+
updateSucceeded = true;
|
|
608
613
|
}
|
|
609
614
|
catch (e2) {
|
|
610
615
|
const elapsed2 = Date.now() - t1;
|
|
@@ -613,6 +618,11 @@ function syncUpdateAndRestart() {
|
|
|
613
618
|
appendStartupTrace("update: npm install fallback failed", { elapsedMs: elapsed2, message: err2.message });
|
|
614
619
|
}
|
|
615
620
|
}
|
|
621
|
+
if (!updateSucceeded && options.spawnOnUpdateFailure === false) {
|
|
622
|
+
updLog("safe update aborted: both npm update and fallback install failed");
|
|
623
|
+
appendStartupTrace("update: safe update aborted before restart", {});
|
|
624
|
+
return undefined;
|
|
625
|
+
}
|
|
616
626
|
// 2. resolve bin path
|
|
617
627
|
const npmPrefix = process.env.NPM_PREFIX || "";
|
|
618
628
|
const binName = process.platform === "win32" ? "chatccc.cmd" : "chatccc";
|
|
@@ -771,15 +781,106 @@ export async function decideRestartParentExit(child, timeoutMs, pollMs = 500, tr
|
|
|
771
781
|
trace("restart: child alive after window, parent exiting", { childPid: child.pid });
|
|
772
782
|
return true;
|
|
773
783
|
}
|
|
784
|
+
const safeMaintenancePlatforms = new Map();
|
|
785
|
+
export function configureSafeMaintenanceRuntime(platforms) {
|
|
786
|
+
safeMaintenancePlatforms.clear();
|
|
787
|
+
for (const platform of platforms) {
|
|
788
|
+
if (platform.kind)
|
|
789
|
+
safeMaintenancePlatforms.set(platform.kind, platform);
|
|
790
|
+
}
|
|
791
|
+
safeMaintenanceCoordinator.configure({
|
|
792
|
+
getSnapshot() {
|
|
793
|
+
const sessions = getSessionDrainSnapshot();
|
|
794
|
+
return {
|
|
795
|
+
...sessions,
|
|
796
|
+
activeEngineIds: engineManager.getActiveInstallIds(),
|
|
797
|
+
activeWorkLabels: [],
|
|
798
|
+
};
|
|
799
|
+
},
|
|
800
|
+
execute(kind) {
|
|
801
|
+
return kind === "update" ? executeSafeUpdate() : executeSafeRestart();
|
|
802
|
+
},
|
|
803
|
+
async notify(requester, message) {
|
|
804
|
+
const platform = safeMaintenancePlatforms.get(requester.platform);
|
|
805
|
+
if (!platform)
|
|
806
|
+
throw new Error(`Unavailable platform: ${requester.platform}`);
|
|
807
|
+
await platform.sendText(requester.chatId, message);
|
|
808
|
+
},
|
|
809
|
+
});
|
|
810
|
+
}
|
|
811
|
+
export function recoverSafeMaintenanceAfterStartup() {
|
|
812
|
+
return safeMaintenanceCoordinator.recoverAfterStartup(process.env[INTERNAL_RESTART_ENV_VAR] === "1");
|
|
813
|
+
}
|
|
814
|
+
async function executeSafeRestart() {
|
|
815
|
+
fileLog.flush();
|
|
816
|
+
appendStartupTrace("safe-maintenance: restart spawn begin", { fromPid: process.pid });
|
|
817
|
+
const child = spawnRestartChild();
|
|
818
|
+
child.unref();
|
|
819
|
+
const shouldExit = await decideRestartParentExit(child, RESTART_CHILD_READY_MS);
|
|
820
|
+
if (!shouldExit)
|
|
821
|
+
return false;
|
|
822
|
+
appendStartupTrace("safe-maintenance: restart parent exit", { childPid: child.pid });
|
|
823
|
+
process.exit(0);
|
|
824
|
+
}
|
|
825
|
+
async function executeSafeUpdate() {
|
|
826
|
+
fileLog.flush();
|
|
827
|
+
appendStartupTrace("safe-maintenance: update begin", { fromPid: process.pid });
|
|
828
|
+
const child = syncUpdateAndRestart({ spawnOnUpdateFailure: false });
|
|
829
|
+
if (!child)
|
|
830
|
+
return false;
|
|
831
|
+
child.unref();
|
|
832
|
+
const shouldExit = await decideRestartParentExit(child, RESTART_CHILD_READY_MS);
|
|
833
|
+
if (!shouldExit)
|
|
834
|
+
return false;
|
|
835
|
+
appendStartupTrace("safe-maintenance: update parent exit", { childPid: child.pid });
|
|
836
|
+
process.exit(0);
|
|
837
|
+
}
|
|
838
|
+
function safeMaintenanceRequester(platform, chatId, openId) {
|
|
839
|
+
return { platform: platform.kind ?? "feishu", chatId, openId };
|
|
840
|
+
}
|
|
841
|
+
function safeMaintenancePhaseLabel(phase) {
|
|
842
|
+
const labels = {
|
|
843
|
+
draining: "等待现有任务结束",
|
|
844
|
+
executing: "正在执行维护",
|
|
845
|
+
completed: "已完成",
|
|
846
|
+
failed: "执行失败",
|
|
847
|
+
};
|
|
848
|
+
return labels[phase] ?? phase;
|
|
849
|
+
}
|
|
850
|
+
async function safeMaintenanceStatusText() {
|
|
851
|
+
const status = await safeMaintenanceCoordinator.status();
|
|
852
|
+
if (!status.job)
|
|
853
|
+
return "当前没有安全维护预约。";
|
|
854
|
+
const snapshot = status.snapshot;
|
|
855
|
+
return [
|
|
856
|
+
`安全维护:${status.job.kind === "update" ? "更新并重启" : "重启"}`,
|
|
857
|
+
`状态:${safeMaintenancePhaseLabel(status.job.phase)}`,
|
|
858
|
+
`执行中/收尾会话:${snapshot.activeSessionIds.length}`,
|
|
859
|
+
`已接受的缓存消息:${snapshot.queuedSessionIds.length}`,
|
|
860
|
+
`依赖安装任务:${snapshot.activeEngineIds.length}`,
|
|
861
|
+
`其他处理中入口:${Math.max(0, snapshot.activeWorkLabels.length - 1)}`,
|
|
862
|
+
...(status.job.lastError ? [`错误:${status.job.lastError}`] : []),
|
|
863
|
+
].join("\n");
|
|
864
|
+
}
|
|
774
865
|
// ---------------------------------------------------------------------------
|
|
775
866
|
// handleCommand — 平台无关的命令分发
|
|
776
867
|
// ---------------------------------------------------------------------------
|
|
777
|
-
export async function handleCommand(platform, text, chatId, openId, msgTimestamp, chatType = "group", traceId, commandId) {
|
|
868
|
+
export async function handleCommand(platform, text, chatId, openId, msgTimestamp, chatType = "group", traceId, commandId, acceptedBeforeSafeMaintenance = false) {
|
|
869
|
+
const release = beginSafeMaintenanceTrackedWork(acceptedBeforeSafeMaintenance ? "accepted-queued-message" : "chat-command");
|
|
870
|
+
try {
|
|
871
|
+
await handleCommandInternal(platform, text, chatId, openId, msgTimestamp, chatType, traceId, commandId, acceptedBeforeSafeMaintenance);
|
|
872
|
+
}
|
|
873
|
+
finally {
|
|
874
|
+
release();
|
|
875
|
+
}
|
|
876
|
+
}
|
|
877
|
+
async function handleCommandInternal(platform, text, chatId, openId, msgTimestamp, chatType = "group", traceId, commandId, acceptedBeforeSafeMaintenance = false) {
|
|
778
878
|
const tid = traceId ?? makeTraceId();
|
|
779
879
|
const sharedPrefix = applySharedPrefix(text);
|
|
780
880
|
const promptText = sharedPrefix.text;
|
|
781
881
|
text = sharedPrefix.body;
|
|
782
882
|
const textLower = text.toLowerCase();
|
|
883
|
+
const normalizedCommandText = textLower.trim().replace(/\s+/g, " ");
|
|
783
884
|
const isCommandText = !sharedPrefix.matched && textLower.startsWith("/");
|
|
784
885
|
recordChatPlatform(chatId, platform);
|
|
785
886
|
if (platform.kind === "feishu" && chatType === "p2p" && isValidFeishuOpenId(openId)) {
|
|
@@ -788,6 +889,77 @@ export async function handleCommand(platform, text, chatId, openId, msgTimestamp
|
|
|
788
889
|
console.error(`[${ts()}] [AGENT-TEAM] Failed to remember Feishu private contact: ${err.message}`);
|
|
789
890
|
});
|
|
790
891
|
}
|
|
892
|
+
if (isCommandText && normalizedCommandText === "/safestatus") {
|
|
893
|
+
logTrace(tid, "BRANCH", { cmd: "/safestatus" });
|
|
894
|
+
await platform.sendText(chatId, await safeMaintenanceStatusText()).catch(() => { });
|
|
895
|
+
logTrace(tid, "DONE", { outcome: "safe_maintenance_status" });
|
|
896
|
+
return;
|
|
897
|
+
}
|
|
898
|
+
if (isCommandText && normalizedCommandText === "/cancelsf") {
|
|
899
|
+
logTrace(tid, "BRANCH", { cmd: "/cancelsf" });
|
|
900
|
+
const canceled = await safeMaintenanceCoordinator.cancel();
|
|
901
|
+
if (!canceled) {
|
|
902
|
+
await platform.sendText(chatId, "当前没有可取消的安全维护预约;已经开始执行的维护也不能取消。").catch(() => { });
|
|
903
|
+
}
|
|
904
|
+
logTrace(tid, "DONE", { outcome: canceled ? "safe_maintenance_canceled" : "safe_maintenance_cancel_unavailable" });
|
|
905
|
+
return;
|
|
906
|
+
}
|
|
907
|
+
if (isCommandText && normalizedCommandText === "/restart safe") {
|
|
908
|
+
logTrace(tid, "BRANCH", { cmd: "/restart safe" });
|
|
909
|
+
try {
|
|
910
|
+
const job = await safeMaintenanceCoordinator.schedule("restart", safeMaintenanceRequester(platform, chatId, openId));
|
|
911
|
+
await platform.sendText(chatId, job.phase === "executing"
|
|
912
|
+
? "安全维护已经开始执行,无法重复预约。"
|
|
913
|
+
: job.kind === "update"
|
|
914
|
+
? "已经存在优先级更高的安全更新预约,将继续等待现有任务完成。发送 /safestatus 查看状态,/cancelsf 取消预约。"
|
|
915
|
+
: "已预约安全重启。现有会话、缓存消息和依赖安装会先自然完成;现在起不再接受新的普通任务。发送 /safestatus 查看状态,/cancelsf 取消预约。").catch(() => { });
|
|
916
|
+
logTrace(tid, "DONE", { outcome: "safe_restart_scheduled", jobId: job.jobId });
|
|
917
|
+
}
|
|
918
|
+
catch (error) {
|
|
919
|
+
await platform.sendText(chatId, `安全重启预约失败:${error.message}`).catch(() => { });
|
|
920
|
+
logTrace(tid, "DONE", { outcome: "safe_restart_schedule_failed", error: error.message });
|
|
921
|
+
}
|
|
922
|
+
return;
|
|
923
|
+
}
|
|
924
|
+
if (isCommandText && normalizedCommandText === "/update safe") {
|
|
925
|
+
logTrace(tid, "BRANCH", { cmd: "/update safe" });
|
|
926
|
+
const isGlobal = isRunningFromGlobalNpm();
|
|
927
|
+
if (!isGlobal) {
|
|
928
|
+
await platform.sendText(chatId, "当前进程非 npm 全局安装,无法使用 /update safe。请通过 npm install -g chatccc 安装后使用。").catch(() => { });
|
|
929
|
+
logTrace(tid, "DONE", { outcome: "safe_update_not_global" });
|
|
930
|
+
return;
|
|
931
|
+
}
|
|
932
|
+
const updateGuard = acquireUpdateCommandGuard({ commandId });
|
|
933
|
+
if (!updateGuard.allowed) {
|
|
934
|
+
if (updateGuard.reason !== "duplicate_id") {
|
|
935
|
+
await platform.sendText(chatId, "无法写入更新保护状态。为避免连续更新和重启,本次 /update safe 未预约。").catch(() => { });
|
|
936
|
+
}
|
|
937
|
+
logTrace(tid, "DONE", { outcome: updateGuard.reason === "duplicate_id" ? "safe_update_duplicate_id" : "safe_update_guard_failed" });
|
|
938
|
+
return;
|
|
939
|
+
}
|
|
940
|
+
try {
|
|
941
|
+
const job = await safeMaintenanceCoordinator.schedule("update", safeMaintenanceRequester(platform, chatId, openId));
|
|
942
|
+
await platform.sendText(chatId, job.phase === "executing"
|
|
943
|
+
? "安全维护已经开始执行,无法重复预约。"
|
|
944
|
+
: "已预约安全更新。现有会话、缓存消息和依赖安装会先自然完成;现在起不再接受新的普通任务。发送 /safestatus 查看状态,/cancelsf 取消预约。").catch(() => { });
|
|
945
|
+
logTrace(tid, "DONE", { outcome: "safe_update_scheduled", jobId: job.jobId });
|
|
946
|
+
}
|
|
947
|
+
catch (error) {
|
|
948
|
+
await platform.sendText(chatId, `安全更新预约失败:${error.message}`).catch(() => { });
|
|
949
|
+
logTrace(tid, "DONE", { outcome: "safe_update_schedule_failed", error: error.message });
|
|
950
|
+
}
|
|
951
|
+
return;
|
|
952
|
+
}
|
|
953
|
+
const maintenanceAllowedCommands = new Set([
|
|
954
|
+
"/stop", "/cancel", "/state", "/sessions", "/usage", "/safestatus", "/cancelsf", "/restart", "/update", "/reload", "/help",
|
|
955
|
+
]);
|
|
956
|
+
if (isSafeMaintenanceAdmissionClosed()
|
|
957
|
+
&& !acceptedBeforeSafeMaintenance
|
|
958
|
+
&& (!isCommandText || !maintenanceAllowedCommands.has(normalizedCommandText))) {
|
|
959
|
+
await platform.sendText(chatId, "ChatCCC 正在等待安全重启或更新,当前不接受新的任务。请在维护完成后重新发送;可用 /safestatus 查看状态。").catch(() => { });
|
|
960
|
+
logTrace(tid, "DONE", { outcome: "safe_maintenance_admission_closed" });
|
|
961
|
+
return;
|
|
962
|
+
}
|
|
791
963
|
if (isCommandText && textLower === "/reload") {
|
|
792
964
|
logTrace(tid, "BRANCH", { cmd: "/reload" });
|
|
793
965
|
try {
|
|
@@ -808,6 +980,14 @@ export async function handleCommand(platform, text, chatId, openId, msgTimestamp
|
|
|
808
980
|
}
|
|
809
981
|
if (isCommandText && textLower === "/restart") {
|
|
810
982
|
logTrace(tid, "BRANCH", { cmd: "/restart" });
|
|
983
|
+
const safeStatus = await safeMaintenanceCoordinator.status();
|
|
984
|
+
if (safeStatus.job?.phase === "executing") {
|
|
985
|
+
await platform.sendText(chatId, "安全维护已经开始执行,请勿重复重启。").catch(() => { });
|
|
986
|
+
logTrace(tid, "DONE", { outcome: "restart_blocked_by_executing_safe_maintenance" });
|
|
987
|
+
return;
|
|
988
|
+
}
|
|
989
|
+
if (safeStatus.job?.phase === "draining")
|
|
990
|
+
await safeMaintenanceCoordinator.cancel(false);
|
|
811
991
|
await platform.sendText(chatId, "重启中...请几秒后发消息唤醒我").catch(() => { });
|
|
812
992
|
logTrace(tid, "DONE", { outcome: "restart" });
|
|
813
993
|
appendStartupTrace("restart: spawn begin", { fromPid: process.pid });
|
|
@@ -825,6 +1005,12 @@ export async function handleCommand(platform, text, chatId, openId, msgTimestamp
|
|
|
825
1005
|
}
|
|
826
1006
|
if (isCommandText && textLower === "/update") {
|
|
827
1007
|
logTrace(tid, "BRANCH", { cmd: "/update" });
|
|
1008
|
+
const safeStatus = await safeMaintenanceCoordinator.status();
|
|
1009
|
+
if (safeStatus.job?.phase === "executing") {
|
|
1010
|
+
await platform.sendText(chatId, "安全维护已经开始执行,请勿重复更新。").catch(() => { });
|
|
1011
|
+
logTrace(tid, "DONE", { outcome: "update_blocked_by_executing_safe_maintenance" });
|
|
1012
|
+
return;
|
|
1013
|
+
}
|
|
828
1014
|
const isGlobal = isRunningFromGlobalNpm();
|
|
829
1015
|
appendStartupTrace("update: command received", { isGlobal, chatId });
|
|
830
1016
|
if (!isGlobal) {
|
|
@@ -851,6 +1037,8 @@ export async function handleCommand(platform, text, chatId, openId, msgTimestamp
|
|
|
851
1037
|
logTrace(tid, "DONE", { outcome: "update_guard_write_failed" });
|
|
852
1038
|
return;
|
|
853
1039
|
}
|
|
1040
|
+
if (safeStatus.job?.phase === "draining")
|
|
1041
|
+
await safeMaintenanceCoordinator.cancel(false);
|
|
854
1042
|
await platform.sendText(chatId, "正在更新并重启,请稍候...").catch(() => { });
|
|
855
1043
|
logTrace(tid, "DONE", { outcome: "update" });
|
|
856
1044
|
appendStartupTrace("update: sync update begin", { fromPid: process.pid });
|
|
@@ -0,0 +1,271 @@
|
|
|
1
|
+
import { randomUUID } from "node:crypto";
|
|
2
|
+
import { existsSync, mkdirSync, readFileSync, renameSync, rmSync, writeFileSync } from "node:fs";
|
|
3
|
+
import { dirname, join } from "node:path";
|
|
4
|
+
import { USER_DATA_DIR } from "./config.js";
|
|
5
|
+
export const SAFE_MAINTENANCE_FILE = join(USER_DATA_DIR, "state", "safe-maintenance.json");
|
|
6
|
+
export const SAFE_MAINTENANCE_STABLE_IDLE_MS = 1_000;
|
|
7
|
+
const EMPTY_SNAPSHOT = {
|
|
8
|
+
activeSessionIds: [],
|
|
9
|
+
queuedSessionIds: [],
|
|
10
|
+
activeEngineIds: [],
|
|
11
|
+
activeWorkLabels: [],
|
|
12
|
+
};
|
|
13
|
+
export class SafeMaintenanceCoordinator {
|
|
14
|
+
filePath;
|
|
15
|
+
now;
|
|
16
|
+
idFactory;
|
|
17
|
+
stableIdleMs;
|
|
18
|
+
pollMs;
|
|
19
|
+
autoPoll;
|
|
20
|
+
trackedWork = new Map();
|
|
21
|
+
runtime = null;
|
|
22
|
+
job;
|
|
23
|
+
snapshot = EMPTY_SNAPSHOT;
|
|
24
|
+
stableSince = null;
|
|
25
|
+
timer = null;
|
|
26
|
+
tickRunning = false;
|
|
27
|
+
constructor(options = {}) {
|
|
28
|
+
this.filePath = options.filePath ?? SAFE_MAINTENANCE_FILE;
|
|
29
|
+
this.now = options.now ?? (() => new Date());
|
|
30
|
+
this.idFactory = options.idFactory ?? randomUUID;
|
|
31
|
+
this.stableIdleMs = options.stableIdleMs ?? SAFE_MAINTENANCE_STABLE_IDLE_MS;
|
|
32
|
+
this.pollMs = options.pollMs ?? 500;
|
|
33
|
+
this.autoPoll = options.autoPoll ?? true;
|
|
34
|
+
this.job = readJob(this.filePath);
|
|
35
|
+
}
|
|
36
|
+
configure(runtime) {
|
|
37
|
+
this.runtime = runtime;
|
|
38
|
+
}
|
|
39
|
+
isAdmissionClosed() {
|
|
40
|
+
return this.job?.phase === "draining" || this.job?.phase === "executing";
|
|
41
|
+
}
|
|
42
|
+
beginTrackedWork(label) {
|
|
43
|
+
const id = this.idFactory();
|
|
44
|
+
this.trackedWork.set(id, label);
|
|
45
|
+
let released = false;
|
|
46
|
+
return () => {
|
|
47
|
+
if (released)
|
|
48
|
+
return;
|
|
49
|
+
released = true;
|
|
50
|
+
this.trackedWork.delete(id);
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
async schedule(kind, requester) {
|
|
54
|
+
const now = this.now().toISOString();
|
|
55
|
+
if (this.job?.phase === "executing")
|
|
56
|
+
return structuredClone(this.job);
|
|
57
|
+
const previous = this.job;
|
|
58
|
+
if (this.job?.phase === "draining") {
|
|
59
|
+
const requesters = addRequester(this.job.requesters, requester);
|
|
60
|
+
this.job = {
|
|
61
|
+
...this.job,
|
|
62
|
+
kind: this.job.kind === "update" || kind === "update" ? "update" : "restart",
|
|
63
|
+
requesters,
|
|
64
|
+
updatedAt: now,
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
else {
|
|
68
|
+
this.job = {
|
|
69
|
+
schemaVersion: 1,
|
|
70
|
+
jobId: this.idFactory(),
|
|
71
|
+
kind,
|
|
72
|
+
phase: "draining",
|
|
73
|
+
requestedAt: now,
|
|
74
|
+
updatedAt: now,
|
|
75
|
+
requesters: [requester],
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
this.stableSince = null;
|
|
79
|
+
try {
|
|
80
|
+
writeJob(this.filePath, this.job);
|
|
81
|
+
}
|
|
82
|
+
catch (error) {
|
|
83
|
+
this.job = previous;
|
|
84
|
+
throw error;
|
|
85
|
+
}
|
|
86
|
+
this.startPolling();
|
|
87
|
+
return structuredClone(this.job);
|
|
88
|
+
}
|
|
89
|
+
async cancel(notify = true) {
|
|
90
|
+
if (this.job?.phase !== "draining")
|
|
91
|
+
return false;
|
|
92
|
+
const requesters = this.job.requesters;
|
|
93
|
+
removeJob(this.filePath);
|
|
94
|
+
this.job = null;
|
|
95
|
+
this.snapshot = EMPTY_SNAPSHOT;
|
|
96
|
+
this.stableSince = null;
|
|
97
|
+
this.stopPolling();
|
|
98
|
+
if (notify)
|
|
99
|
+
await this.notifyAll(requesters, "已取消安全维护预约,ChatCCC 恢复接受新任务。");
|
|
100
|
+
return true;
|
|
101
|
+
}
|
|
102
|
+
async status() {
|
|
103
|
+
if (this.runtime && this.isAdmissionClosed())
|
|
104
|
+
this.snapshot = await this.collectSnapshot();
|
|
105
|
+
return {
|
|
106
|
+
job: this.job ? structuredClone(this.job) : null,
|
|
107
|
+
snapshot: structuredClone(this.snapshot),
|
|
108
|
+
waitingCount: snapshotCount(this.snapshot),
|
|
109
|
+
};
|
|
110
|
+
}
|
|
111
|
+
async tick() {
|
|
112
|
+
if (this.tickRunning || this.job?.phase !== "draining" || !this.runtime)
|
|
113
|
+
return;
|
|
114
|
+
this.tickRunning = true;
|
|
115
|
+
try {
|
|
116
|
+
this.snapshot = await this.collectSnapshot();
|
|
117
|
+
if (snapshotCount(this.snapshot) > 0) {
|
|
118
|
+
this.stableSince = null;
|
|
119
|
+
return;
|
|
120
|
+
}
|
|
121
|
+
const nowMs = this.now().getTime();
|
|
122
|
+
if (this.stableSince === null) {
|
|
123
|
+
this.stableSince = nowMs;
|
|
124
|
+
return;
|
|
125
|
+
}
|
|
126
|
+
if (nowMs - this.stableSince < this.stableIdleMs)
|
|
127
|
+
return;
|
|
128
|
+
await this.executeCurrentJob();
|
|
129
|
+
}
|
|
130
|
+
finally {
|
|
131
|
+
this.tickRunning = false;
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
async recoverAfterStartup(internalRestart) {
|
|
135
|
+
if (!this.job || !this.runtime)
|
|
136
|
+
return;
|
|
137
|
+
if (this.job.phase === "draining") {
|
|
138
|
+
this.startPolling();
|
|
139
|
+
await this.notifyAll(this.job.requesters, "ChatCCC 已恢复未完成的安全维护预约,继续等待现有任务结束。");
|
|
140
|
+
return;
|
|
141
|
+
}
|
|
142
|
+
if (this.job.phase !== "executing")
|
|
143
|
+
return;
|
|
144
|
+
if (internalRestart) {
|
|
145
|
+
const completed = { ...this.job, phase: "completed", updatedAt: this.now().toISOString() };
|
|
146
|
+
writeJob(this.filePath, completed);
|
|
147
|
+
this.job = completed;
|
|
148
|
+
await this.notifyAll(this.job.requesters, this.job.kind === "update" ? "ChatCCC 已安全更新并重新启动。" : "ChatCCC 已安全重新启动。");
|
|
149
|
+
return;
|
|
150
|
+
}
|
|
151
|
+
const message = "安全维护执行期间进程意外退出;为避免重启循环,未自动重试。";
|
|
152
|
+
const failed = {
|
|
153
|
+
...this.job,
|
|
154
|
+
phase: "failed",
|
|
155
|
+
updatedAt: this.now().toISOString(),
|
|
156
|
+
lastError: message,
|
|
157
|
+
};
|
|
158
|
+
writeJob(this.filePath, failed);
|
|
159
|
+
this.job = failed;
|
|
160
|
+
await this.notifyAll(this.job.requesters, message);
|
|
161
|
+
}
|
|
162
|
+
async collectSnapshot() {
|
|
163
|
+
const external = await this.runtime.getSnapshot();
|
|
164
|
+
return {
|
|
165
|
+
activeSessionIds: [...external.activeSessionIds],
|
|
166
|
+
queuedSessionIds: [...external.queuedSessionIds],
|
|
167
|
+
activeEngineIds: [...external.activeEngineIds],
|
|
168
|
+
activeWorkLabels: [...external.activeWorkLabels, ...this.trackedWork.values()],
|
|
169
|
+
};
|
|
170
|
+
}
|
|
171
|
+
async executeCurrentJob() {
|
|
172
|
+
if (!this.job || !this.runtime)
|
|
173
|
+
return;
|
|
174
|
+
const executing = { ...this.job, phase: "executing", updatedAt: this.now().toISOString() };
|
|
175
|
+
writeJob(this.filePath, executing);
|
|
176
|
+
this.job = executing;
|
|
177
|
+
this.stopPolling();
|
|
178
|
+
const label = this.job.kind === "update" ? "更新并重启" : "重启";
|
|
179
|
+
await this.notifyAll(this.job.requesters, `现有任务已全部完成,开始安全${label}。`);
|
|
180
|
+
const started = await this.runtime.execute(this.job.kind).catch(async (error) => {
|
|
181
|
+
await this.markFailed(error instanceof Error ? error.message : String(error));
|
|
182
|
+
return false;
|
|
183
|
+
});
|
|
184
|
+
if (!started && this.job?.phase === "executing") {
|
|
185
|
+
await this.markFailed(`安全${label}未能启动,当前进程将继续提供服务。`);
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
async markFailed(message) {
|
|
189
|
+
if (!this.job)
|
|
190
|
+
return;
|
|
191
|
+
const failed = {
|
|
192
|
+
...this.job,
|
|
193
|
+
phase: "failed",
|
|
194
|
+
updatedAt: this.now().toISOString(),
|
|
195
|
+
lastError: message,
|
|
196
|
+
};
|
|
197
|
+
writeJob(this.filePath, failed);
|
|
198
|
+
this.job = failed;
|
|
199
|
+
await this.notifyAll(this.job.requesters, message);
|
|
200
|
+
}
|
|
201
|
+
startPolling() {
|
|
202
|
+
if (!this.autoPoll || this.timer || this.job?.phase !== "draining")
|
|
203
|
+
return;
|
|
204
|
+
this.timer = setInterval(() => { void this.tick(); }, this.pollMs);
|
|
205
|
+
this.timer.unref?.();
|
|
206
|
+
}
|
|
207
|
+
stopPolling() {
|
|
208
|
+
if (!this.timer)
|
|
209
|
+
return;
|
|
210
|
+
clearInterval(this.timer);
|
|
211
|
+
this.timer = null;
|
|
212
|
+
}
|
|
213
|
+
async notifyAll(requesters, message) {
|
|
214
|
+
if (!this.runtime)
|
|
215
|
+
return;
|
|
216
|
+
await Promise.all(requesters.map((requester) => this.runtime.notify(requester, message).catch(() => { })));
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
function snapshotCount(snapshot) {
|
|
220
|
+
return snapshot.activeSessionIds.length
|
|
221
|
+
+ snapshot.queuedSessionIds.length
|
|
222
|
+
+ snapshot.activeEngineIds.length
|
|
223
|
+
+ snapshot.activeWorkLabels.length;
|
|
224
|
+
}
|
|
225
|
+
function addRequester(requesters, requester) {
|
|
226
|
+
if (requesters.some((item) => item.platform === requester.platform && item.chatId === requester.chatId))
|
|
227
|
+
return requesters;
|
|
228
|
+
return [...requesters, requester];
|
|
229
|
+
}
|
|
230
|
+
function readJob(filePath) {
|
|
231
|
+
if (!existsSync(filePath))
|
|
232
|
+
return null;
|
|
233
|
+
try {
|
|
234
|
+
const value = JSON.parse(readFileSync(filePath, "utf8"));
|
|
235
|
+
if (value.schemaVersion !== 1 || !value.jobId || !Array.isArray(value.requesters))
|
|
236
|
+
return null;
|
|
237
|
+
if (!["restart", "update"].includes(value.kind) || !["draining", "executing", "completed", "failed"].includes(value.phase))
|
|
238
|
+
return null;
|
|
239
|
+
return value;
|
|
240
|
+
}
|
|
241
|
+
catch {
|
|
242
|
+
return null;
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
function writeJob(filePath, job) {
|
|
246
|
+
mkdirSync(dirname(filePath), { recursive: true });
|
|
247
|
+
const temporary = `${filePath}.${process.pid}.${randomUUID()}.tmp`;
|
|
248
|
+
try {
|
|
249
|
+
writeFileSync(temporary, `${JSON.stringify(job, null, 2)}\n`, "utf8");
|
|
250
|
+
renameSync(temporary, filePath);
|
|
251
|
+
}
|
|
252
|
+
finally {
|
|
253
|
+
rmSync(temporary, { force: true });
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
function removeJob(filePath) {
|
|
257
|
+
rmSync(filePath, { force: true });
|
|
258
|
+
}
|
|
259
|
+
export let safeMaintenanceCoordinator = new SafeMaintenanceCoordinator();
|
|
260
|
+
export function _setSafeMaintenanceCoordinatorForTest(coordinator) {
|
|
261
|
+
safeMaintenanceCoordinator = coordinator;
|
|
262
|
+
}
|
|
263
|
+
export function _resetSafeMaintenanceCoordinatorForTest() {
|
|
264
|
+
safeMaintenanceCoordinator = new SafeMaintenanceCoordinator();
|
|
265
|
+
}
|
|
266
|
+
export function isSafeMaintenanceAdmissionClosed() {
|
|
267
|
+
return safeMaintenanceCoordinator.isAdmissionClosed();
|
|
268
|
+
}
|
|
269
|
+
export function beginSafeMaintenanceTrackedWork(label) {
|
|
270
|
+
return safeMaintenanceCoordinator.beginTrackedWork(label);
|
|
271
|
+
}
|
|
@@ -133,6 +133,18 @@ export function setUnifiedDisplayLoopHandle(h) {
|
|
|
133
133
|
unifiedDisplayLoopHandle = h;
|
|
134
134
|
}
|
|
135
135
|
export const queuedMessages = new Map();
|
|
136
|
+
/** Includes prompt execution, finalization, auto-recovery reservations, and accepted queues. */
|
|
137
|
+
export function getSessionDrainSnapshot() {
|
|
138
|
+
const active = new Set([
|
|
139
|
+
...activePrompts.keys(),
|
|
140
|
+
...finalizingSessions,
|
|
141
|
+
...autoRecoveryReservations,
|
|
142
|
+
]);
|
|
143
|
+
return {
|
|
144
|
+
activeSessionIds: [...active].sort(),
|
|
145
|
+
queuedSessionIds: [...queuedMessages.keys()].sort(),
|
|
146
|
+
};
|
|
147
|
+
}
|
|
136
148
|
export function enqueueMessage(sessionId, msg) {
|
|
137
149
|
if (queuedMessages.has(sessionId))
|
|
138
150
|
return false;
|
package/dist/src/session.js
CHANGED
|
@@ -6,6 +6,7 @@ import { progressView } from "./progress/view.js";
|
|
|
6
6
|
import { createAgentActivityTracker, formatAgentActivityTitle, updateAgentActivity, } from "./agent-activity.js";
|
|
7
7
|
import { simplifyToolUse, simplifyToolResult } from "./simplify.js";
|
|
8
8
|
import { logTrace } from "./trace.js";
|
|
9
|
+
import { appendExecutionTranscriptBlock, } from "./execution-transcript.js";
|
|
9
10
|
import { createClaudeAdapter } from "./adapters/claude-adapter.js";
|
|
10
11
|
import { createCursorAdapter } from "./adapters/cursor-adapter.js";
|
|
11
12
|
import { createCodexAdapter } from "./adapters/codex-adapter.js";
|
|
@@ -698,6 +699,11 @@ export function pickFinalReply(state) {
|
|
|
698
699
|
return state.finalCompleteText || state.finalText;
|
|
699
700
|
}
|
|
700
701
|
export function accumulateBlockContent(block, state, toolCallMap) {
|
|
702
|
+
if (state.transcript !== undefined || (block.type !== "agent_progress" && block.type !== "text_reset")) {
|
|
703
|
+
const transcriptState = { transcript: state.transcript ?? [] };
|
|
704
|
+
appendExecutionTranscriptBlock(block, transcriptState);
|
|
705
|
+
state.transcript = transcriptState.transcript;
|
|
706
|
+
}
|
|
701
707
|
switch (block.type) {
|
|
702
708
|
case "thinking":
|
|
703
709
|
state.chunkCount++;
|
|
@@ -1145,6 +1151,7 @@ export async function runAgentSession(sessionId, userText, platform, _chatId, ms
|
|
|
1145
1151
|
refreshBusySessionAvatar(sessionId, tool, platform).catch(() => { });
|
|
1146
1152
|
const state = {
|
|
1147
1153
|
accumulatedContent: "",
|
|
1154
|
+
transcript: [],
|
|
1148
1155
|
finalText: "",
|
|
1149
1156
|
finalCompleteText: "",
|
|
1150
1157
|
chunkCount: 0,
|
|
@@ -1196,6 +1203,7 @@ export async function runAgentSession(sessionId, userText, platform, _chatId, ms
|
|
|
1196
1203
|
status: "auto_ended",
|
|
1197
1204
|
accumulatedContent: state.accumulatedContent,
|
|
1198
1205
|
finalReply: pickFinalReply(state).trim(),
|
|
1206
|
+
transcript: state.transcript,
|
|
1199
1207
|
activity: activityTracker.activity,
|
|
1200
1208
|
chunkCount: state.chunkCount,
|
|
1201
1209
|
turnCount: nextTurnCount,
|
|
@@ -1308,6 +1316,7 @@ export async function runAgentSession(sessionId, userText, platform, _chatId, ms
|
|
|
1308
1316
|
status: "running",
|
|
1309
1317
|
accumulatedContent: state.accumulatedContent,
|
|
1310
1318
|
finalReply: pickFinalReply(state),
|
|
1319
|
+
transcript: state.transcript,
|
|
1311
1320
|
activity: activityTracker.activity,
|
|
1312
1321
|
chunkCount: state.chunkCount,
|
|
1313
1322
|
turnCount: nextTurnCount,
|
|
@@ -1406,6 +1415,7 @@ export async function runAgentSession(sessionId, userText, platform, _chatId, ms
|
|
|
1406
1415
|
status: finalStatus,
|
|
1407
1416
|
accumulatedContent: state.accumulatedContent,
|
|
1408
1417
|
finalReply: finalReplyToWrite,
|
|
1418
|
+
transcript: state.transcript,
|
|
1409
1419
|
activity: activityTracker.activity,
|
|
1410
1420
|
chunkCount: state.chunkCount,
|
|
1411
1421
|
turnCount: nextTurnCount,
|
package/dist/src/stream-state.js
CHANGED
|
@@ -100,6 +100,7 @@ export function createEmptyStreamState(sessionId, cwd, tool, turnCount) {
|
|
|
100
100
|
status: "running",
|
|
101
101
|
accumulatedContent: "",
|
|
102
102
|
finalReply: "",
|
|
103
|
+
transcript: [],
|
|
103
104
|
activity: createAgentActivityTracker(now).activity,
|
|
104
105
|
chunkCount: 0,
|
|
105
106
|
turnCount,
|
package/dist/src/web-ui.js
CHANGED
|
@@ -17,6 +17,7 @@ import { AGENT_TEAM_PAGE_HTML } from "./agent-team/web/agent-team-page.js";
|
|
|
17
17
|
export { AGENT_TEAM_PAGE_HTML } from "./agent-team/web/agent-team-page.js";
|
|
18
18
|
import { buildWebUiUrl, createInternalRestartEnv, openWebUiInDefaultBrowser, } from "./startup-lifecycle.js";
|
|
19
19
|
import { engineManager } from "./engines/engine-specs.js";
|
|
20
|
+
import { isSafeMaintenanceAdmissionClosed } from "./safe-maintenance.js";
|
|
20
21
|
const PROJECT_ROOT = CHATCCC_PACKAGE_ROOT;
|
|
21
22
|
const USER_DATA_DIR = join(homedir(), ".chatccc");
|
|
22
23
|
const CONFIG_FILE = join(USER_DATA_DIR, "config.json");
|
|
@@ -656,6 +657,10 @@ async function handleEngineStatus(engineId, res) {
|
|
|
656
657
|
}
|
|
657
658
|
}
|
|
658
659
|
async function handleEngineInstall(engineId, res) {
|
|
660
|
+
if (isSafeMaintenanceAdmissionClosed()) {
|
|
661
|
+
jsonReply(res, 409, { ok: false, error: "ChatCCC 正在等待安全维护,暂不接受新的依赖安装任务。" });
|
|
662
|
+
return;
|
|
663
|
+
}
|
|
659
664
|
try {
|
|
660
665
|
jsonReply(res, 202, { ok: true, job: await engineManager.startInstall(engineId) });
|
|
661
666
|
}
|