chatccc 0.2.266 → 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 +34 -20
- 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
|
};
|
|
@@ -10,13 +10,13 @@ export const AGENT_TEAM_PAGE_HTML = `<!DOCTYPE html>
|
|
|
10
10
|
.sidebar{position:sticky;top:0;height:100vh;display:flex;flex-direction:column;gap:24px;padding:24px 18px;background:var(--panel);border-right:1px solid var(--line)}.brand{display:flex;align-items:center;gap:11px;color:var(--text);text-decoration:none;font-size:17px;font-weight:800}.brand-icon{width:34px;height:34px;display:grid;place-items:center;border-radius:11px;background:var(--accent);color:#fff;box-shadow:0 8px 18px rgba(103,88,232,.25)}.nav{display:grid;gap:7px}.nav-item{width:100%;display:flex;align-items:center;gap:11px;padding:10px 12px;border:0;border-radius:10px;background:transparent;color:var(--muted);font-size:13px;font-weight:650;text-align:left}.nav-item:hover{border:0;background:var(--soft)}.nav-item.active{background:var(--accent-soft);color:var(--accent)}.nav-icon{width:18px;text-align:center}.side-note{margin-top:auto;padding:13px;border:1px solid var(--line);background:var(--soft);border-radius:12px;font-size:11px;line-height:1.55;color:var(--muted)}.side-note strong{color:var(--success)}
|
|
11
11
|
.shell{min-width:320px;padding:24px}.top{display:flex;align-items:flex-start;justify-content:space-between;gap:20px;margin-bottom:18px}.eyebrow{font-size:11px;font-weight:800;letter-spacing:.12em;color:var(--accent);margin-bottom:6px}.top h1{font-size:27px;line-height:1.2;letter-spacing:-.025em;margin:0}.top p{font-size:12px;color:var(--muted);margin:6px 0 0}.top a{height:38px;display:inline-flex;align-items:center;padding:0 13px;border:1px solid var(--line);border-radius:10px;background:var(--panel);color:var(--text);text-decoration:none;font-size:12px;font-weight:700}
|
|
12
12
|
.workspace-bar{display:grid;grid-template-columns:minmax(180px,220px) minmax(280px,1fr) auto auto;gap:9px;padding:11px;background:var(--panel);border:1px solid var(--line);border-radius:15px;margin-bottom:12px;box-shadow:var(--shadow)}input,textarea,select{border:1px solid var(--line);background:var(--soft);color:var(--text);border-radius:9px;padding:10px 12px;outline:none}input:focus,textarea:focus,select:focus{border-color:var(--accent);box-shadow:0 0 0 3px rgba(103,88,232,.13)}button{cursor:pointer;border:1px solid var(--line);border-radius:9px;padding:10px 14px;background:var(--panel);color:var(--text);font-weight:700}button:hover{border-color:#c8cde0;background:#fbfbfe}button.primary{background:var(--accent);border-color:var(--accent);color:#fff;box-shadow:0 7px 17px rgba(103,88,232,.22)}button.primary:hover{background:#5949d6;border-color:#5949d6}button.ghost{background:var(--soft)}button.danger{background:#fff1f3;border-color:#f3c4ce;color:var(--danger)}.hidden{display:none!important}
|
|
13
|
-
.main-agent-bar{display:grid;grid-template-columns:minmax(180px,1fr)
|
|
13
|
+
.main-agent-bar{display:grid;grid-template-columns:minmax(180px,1fr) auto;align-items:center;gap:12px;padding:13px 15px;background:var(--panel);border:1px solid var(--line);border-radius:15px;margin-bottom:12px;box-shadow:var(--shadow)}.main-agent-copy{min-width:0}.main-agent-copy strong{display:block;font-size:13px;margin-bottom:3px}.main-agent-copy span{display:block;color:var(--muted);font-size:11px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.main-agent-controls{display:flex;align-items:center;gap:8px}.main-agent-controls select{min-width:130px}.main-agent-controls button{min-width:120px;white-space:nowrap}.agent-status{display:inline-flex;align-items:center;gap:6px;color:var(--muted)}.agent-status::before{content:"●";font-size:9px;color:#a3aabd}.agent-status.ready::before{color:var(--success)}.agent-status.error::before{color:var(--danger)}.agent-status.provisioning::before{color:#b7791f}
|
|
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}
|
|
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
|
|
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-
|
|
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
|
+
@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}.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>
|
|
@@ -40,7 +40,7 @@ export const AGENT_TEAM_PAGE_HTML = `<!DOCTYPE html>
|
|
|
40
40
|
</section>
|
|
41
41
|
<section class="main-agent-bar" aria-label="项目主 Agent">
|
|
42
42
|
<div class="main-agent-copy"><strong>项目主 Agent</strong><span id="main-agent-status" class="agent-status">请先打开一个项目</span></div>
|
|
43
|
-
<div class="main-agent-controls"><select id="main-agent" disabled aria-label="选择主 Agent"><option value="">选择主 Agent</option></select><button id="save-main-agent" class="primary" disabled
|
|
43
|
+
<div class="main-agent-controls"><select id="main-agent" disabled aria-label="选择主 Agent"><option value="">选择主 Agent</option></select><button id="save-main-agent" class="primary" disabled>设置为主 Agent</button></div>
|
|
44
44
|
</section>
|
|
45
45
|
<div class="status-row"><span id="workspace-name" class="workspace-name">尚未打开工作目录</span><span id="save-state" class="save-state">本地 JSON</span></div>
|
|
46
46
|
<div class="board-scroll"><section class="board" id="board">
|
|
@@ -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
|
}
|
|
@@ -187,12 +194,12 @@ export const AGENT_TEAM_PAGE_HTML = `<!DOCTYPE html>
|
|
|
187
194
|
state.agentOptions.forEach(function(agent){var option=document.createElement('option');option.value=agent.id;option.textContent=agent.label;mainAgentSelect.appendChild(option);});
|
|
188
195
|
mainAgentSelect.value=selected;mainAgentSelect.disabled=!state.board;mainAgentButton.disabled=!state.board||!mainAgentSelect.value;
|
|
189
196
|
var status=document.getElementById('main-agent-status');var binding=state.binding;
|
|
190
|
-
if(!state.board){status.textContent='请先打开一个项目';status.className='agent-status';mainAgentButton.textContent='
|
|
191
|
-
if(!selected){status.textContent='尚未设置;设置后将创建“主Agent-'+workspaceShortName(state.board.workspacePath)+'”群聊';status.className='agent-status';mainAgentButton.textContent='
|
|
197
|
+
if(!state.board){status.textContent='请先打开一个项目';status.className='agent-status';mainAgentButton.textContent='设置为主 Agent';return;}
|
|
198
|
+
if(!selected){status.textContent='尚未设置;设置后将创建“主Agent-'+workspaceShortName(state.board.workspacePath)+'”群聊';status.className='agent-status';mainAgentButton.textContent='设置为主 Agent';return;}
|
|
192
199
|
if(binding&&binding.status==='ready'){status.textContent='主 Agent 群已就绪';status.className='agent-status ready';}
|
|
193
200
|
else if(binding&&binding.status==='error'){status.textContent='创建失败,可重新设置后重试';status.className='agent-status error';}
|
|
194
201
|
else{status.textContent='正在准备主 Agent 群';status.className='agent-status provisioning';}
|
|
195
|
-
mainAgentButton.textContent='
|
|
202
|
+
mainAgentButton.textContent='设置为主 Agent';
|
|
196
203
|
}
|
|
197
204
|
function closeDmModal(){document.getElementById('dm-modal').classList.add('hidden');if(state.contactTimer){clearInterval(state.contactTimer);state.contactTimer=null;}state.contactChecking=false;}
|
|
198
205
|
function showDmModal(){document.getElementById('dm-modal').classList.remove('hidden');document.getElementById('dm-detect-status').textContent='正在等待私聊消息…';if(state.contactTimer)clearInterval(state.contactTimer);state.contactTimer=setInterval(checkFeishuContact,1500);}
|
|
@@ -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
|
+
}
|