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 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
- | `/update` | 更新 npm 全局包并重启(仅限 `npm install -g chatccc` 安装的全局进程;同一飞书事件跨重启去重) |
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` 会在执行 npm 更新前把飞书消息或按钮事件 ID 原子写入 `~/.chatccc/state/update-command-guard.json`。同一 ID 跨重启重投时会静默忽略;用户主动发送的新 `/update` 因事件 ID 不同,仍可立即执行。该保护仅作用于 `/update`,普通消息与 `/restart` 的处理不变。
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
- const result = await delegateAgentTask({
87
- platform,
88
- tool,
89
- cwd,
90
- promptText,
91
- openIds,
92
- chatNamePrefix: stringValue(payload.chat_name) || promptNamePrefix.slice(0, 10),
93
- });
94
- jsonReply(res, 200, {
95
- ok: true,
96
- chat_id: result.chatId,
97
- session_id: result.sessionId,
98
- tool: result.tool,
99
- cwd: result.cwd,
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
- return this.options.repository.listByProject(projectId);
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) minmax(170px,230px) 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;gap:8px}.main-agent-controls select{min-width:130px}.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}
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 auto}.main-agent-copy{grid-column:1/-1}.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-bar{grid-template-columns:1fr}.main-agent-copy{grid-column:auto}.main-agent-controls{display:grid;grid-template-columns:1fr auto}.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}}
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>设置</button></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>设置为主 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
- <h2 id="modal-title">添加任务</h2>
58
- <div class="field"><label for="task-title">标题</label><input id="task-title" maxlength="200" required></div>
59
- <div class="field"><label for="task-description">描述</label><textarea id="task-description" maxlength="20000"></textarea></div>
60
- <div class="modal-actions"><button type="button" id="cancel" class="ghost">取消</button><button type="submit" class="primary">保存</button></div>
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.disabled=active;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);
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(!active&&!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);
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='设置';return;}
191
- if(!selected){status.textContent='尚未设置;设置后将创建“主Agent-'+workspaceShortName(state.board.workspacePath)+'”群聊';status.className='agent-status';mainAgentButton.textContent='设置';return;}
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 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('modal').classList.remove('hidden');setTimeout(function(){document.getElementById('task-title').focus();},0);}
205
- 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;document.getElementById('modal').classList.remove('hidden');}
206
- function closeModal(){document.getElementById('modal').classList.add('hidden');state.modalTaskId=null;}
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
+ }