chatccc 0.2.252 → 0.2.253
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 +1 -1
- package/bin/cccagent.mjs +12 -3
- package/bin/chatccc.mjs +15 -6
- package/deepccc-agent/os-prompts/darwin.md +8 -8
- package/deepccc-agent/os-prompts/linux.md +8 -8
- package/deepccc-agent/os-prompts/win32.md +11 -11
- package/deepccc-agent/package.json +65 -65
- package/dist/deepccc-agent/src/cli.js +634 -0
- package/dist/deepccc-agent/src/config.js +76 -0
- package/dist/deepccc-agent/src/context.js +348 -0
- package/dist/deepccc-agent/src/file-log.js +34 -0
- package/dist/deepccc-agent/src/file-tools.js +1208 -0
- package/dist/deepccc-agent/src/index.js +571 -0
- package/dist/deepccc-agent/src/permissions.js +170 -0
- package/dist/deepccc-agent/src/privacy.js +124 -0
- package/dist/deepccc-agent/src/proc-tree-kill.js +60 -0
- package/dist/deepccc-agent/src/progress/cards-helpers.js +70 -0
- package/dist/deepccc-agent/src/progress/reducer.js +102 -0
- package/dist/deepccc-agent/src/progress/terminal-renderer.js +264 -0
- package/dist/deepccc-agent/src/progress/view.js +30 -0
- package/dist/deepccc-agent/src/raw-stream-log.js +106 -0
- package/dist/deepccc-agent/src/session-search.js +276 -0
- package/dist/deepccc-agent/src/session-select.js +23 -0
- package/dist/deepccc-agent/src/sigint.js +26 -0
- package/dist/deepccc-agent/src/skills.js +178 -0
- package/dist/deepccc-agent/src/web-tools.js +246 -0
- package/dist/src/adapters/adapter-interface.js +19 -0
- package/dist/src/adapters/ccc-adapter.js +112 -0
- package/dist/src/adapters/claude-adapter.js +497 -0
- package/dist/src/adapters/claude-session-meta-store.js +92 -0
- package/dist/src/adapters/codex-adapter.js +279 -0
- package/dist/src/adapters/codex-session-meta-store.js +94 -0
- package/dist/src/adapters/cursor-adapter.js +491 -0
- package/dist/src/adapters/cursor-session-meta-store.js +116 -0
- package/dist/src/adapters/jsonl-stream.js +104 -0
- package/{src/adapters/proc-tree-kill.ts → dist/src/adapters/proc-tree-kill.js} +94 -97
- package/dist/src/adapters/raw-stream-log.js +106 -0
- package/dist/src/adapters/resource-monitor.js +113 -0
- package/dist/src/agent-activity.js +133 -0
- package/dist/src/agent-delegate-task-rpc.js +129 -0
- package/dist/src/agent-delegate-task.js +48 -0
- package/dist/src/agent-file-rpc.js +152 -0
- package/dist/src/agent-image-rpc.js +148 -0
- package/dist/src/agent-platform-routing.js +13 -0
- package/dist/src/agent-reload-config-rpc.js +23 -0
- package/dist/src/agent-rpc-body.js +87 -0
- package/dist/src/agent-stop-stuck.js +110 -0
- package/dist/src/card-action-routing.js +7 -0
- package/dist/src/card-plain-text.js +101 -0
- package/dist/src/cardkit.js +158 -0
- package/dist/src/cards.js +573 -0
- package/dist/src/chatgpt-subscription-rpc.js +18 -0
- package/dist/src/chatgpt-subscription.js +199 -0
- package/dist/src/chrome-devtools-guard.js +238 -0
- package/dist/src/claude-sdk-installer.js +249 -0
- package/dist/src/codex-reset-actions.js +143 -0
- package/dist/src/config-utils.js +149 -0
- package/dist/src/config.js +804 -0
- package/dist/src/cursor-usage.js +77 -0
- package/dist/src/exit-banner.js +28 -0
- package/dist/src/feishu-api.js +1404 -0
- package/dist/src/feishu-message-ingress.js +137 -0
- package/dist/src/feishu-platform.js +97 -0
- package/dist/src/format-message.js +252 -0
- package/dist/src/git-command.js +155 -0
- package/dist/src/im-skills.js +121 -0
- package/dist/src/index.js +833 -0
- package/dist/src/litellm-proxy.js +300 -0
- package/dist/src/orchestrator.js +2078 -0
- package/dist/src/package-root.js +26 -0
- package/dist/src/platform-adapter.js +7 -0
- package/dist/src/platform-startup.js +6 -0
- package/dist/src/privacy.js +100 -0
- package/dist/src/progress/reducer.js +102 -0
- package/dist/src/progress/terminal-renderer.js +264 -0
- package/dist/src/progress/view.js +30 -0
- package/dist/src/response-stall.js +14 -0
- package/dist/src/runtime-entry.js +13 -0
- package/dist/src/runtime-reload.js +19 -0
- package/dist/src/session-chat-binding.js +183 -0
- package/dist/src/session-name.js +7 -0
- package/dist/src/session.js +2144 -0
- package/dist/src/shared-prefix.js +16 -0
- package/dist/src/shared.js +493 -0
- package/dist/src/sim-agent.js +105 -0
- package/dist/src/sim-platform.js +142 -0
- package/dist/src/sim-store.js +231 -0
- package/dist/src/simplify.js +99 -0
- package/dist/src/startup-lifecycle.js +209 -0
- package/dist/src/stream-state.js +141 -0
- package/dist/src/terminal-error.js +100 -0
- package/dist/src/trace.js +50 -0
- package/dist/src/turn-cards.js +92 -0
- package/dist/src/update-command-guard.js +114 -0
- package/{src/web-ui.ts → dist/src/web-ui.js} +749 -823
- package/dist/src/wechat-platform.js +545 -0
- package/package.json +76 -74
- package/deepccc-agent/LICENSE +0 -201
- package/deepccc-agent/bin/deepccc.mjs +0 -26
- package/deepccc-agent/docs/cache-hit-rate-1.jpg +0 -0
- package/deepccc-agent/docs/cache-hit-rate-2.jpg +0 -0
- package/deepccc-agent/package-lock.json +0 -2027
- package/deepccc-agent/src/__tests__/chat-session.test.ts +0 -877
- package/deepccc-agent/src/__tests__/cli-json.test.ts +0 -49
- package/deepccc-agent/src/__tests__/config.test.ts +0 -34
- package/deepccc-agent/src/__tests__/context.test.ts +0 -341
- package/deepccc-agent/src/__tests__/file-tools.test.ts +0 -240
- package/deepccc-agent/src/__tests__/permissions.test.ts +0 -199
- package/deepccc-agent/src/__tests__/privacy.test.ts +0 -318
- package/deepccc-agent/src/__tests__/progress-reducer.test.ts +0 -121
- package/deepccc-agent/src/__tests__/session-search.test.ts +0 -262
- package/deepccc-agent/src/__tests__/session-select.test.ts +0 -116
- package/deepccc-agent/src/__tests__/sigint.test.ts +0 -56
- package/deepccc-agent/src/__tests__/skills.test.ts +0 -284
- package/deepccc-agent/src/__tests__/terminal-renderer.test.ts +0 -247
- package/deepccc-agent/src/__tests__/web-tools.test.ts +0 -220
- package/deepccc-agent/src/cli.ts +0 -682
- package/deepccc-agent/src/config.ts +0 -101
- package/deepccc-agent/src/context.ts +0 -465
- package/deepccc-agent/src/file-log.ts +0 -38
- package/deepccc-agent/src/file-tools.ts +0 -1493
- package/deepccc-agent/src/index.ts +0 -710
- package/deepccc-agent/src/permissions.ts +0 -226
- package/deepccc-agent/src/privacy.ts +0 -141
- package/deepccc-agent/src/proc-tree-kill.ts +0 -61
- package/deepccc-agent/src/progress/cards-helpers.ts +0 -76
- package/deepccc-agent/src/progress/reducer.ts +0 -113
- package/deepccc-agent/src/progress/terminal-renderer.ts +0 -294
- package/deepccc-agent/src/progress/view.ts +0 -77
- package/deepccc-agent/src/raw-stream-log.ts +0 -124
- package/deepccc-agent/src/session-search.ts +0 -370
- package/deepccc-agent/src/session-select.ts +0 -48
- package/deepccc-agent/src/sigint.ts +0 -50
- package/deepccc-agent/src/skills.ts +0 -205
- package/deepccc-agent/src/web-tools.ts +0 -313
- package/deepccc-agent/tsconfig.build.json +0 -13
- package/deepccc-agent/tsconfig.json +0 -13
- package/deepccc-agent/vitest.config.ts +0 -7
- package/src/__tests__/adapter-interface.test.ts +0 -152
- package/src/__tests__/agent-activity.test.ts +0 -86
- package/src/__tests__/agent-delegate-task-rpc.test.ts +0 -165
- package/src/__tests__/agent-image-rpc.test.ts +0 -34
- package/src/__tests__/agent-platform-routing.test.ts +0 -26
- package/src/__tests__/agent-reload-config-rpc.test.ts +0 -99
- package/src/__tests__/agent-rpc-body.test.ts +0 -42
- package/src/__tests__/builtin-chat-session.test.ts +0 -532
- package/src/__tests__/builtin-cli-json.test.ts +0 -39
- package/src/__tests__/builtin-config.test.ts +0 -26
- package/src/__tests__/builtin-context.test.ts +0 -319
- package/src/__tests__/builtin-file-tools.test.ts +0 -240
- package/src/__tests__/builtin-permissions.test.ts +0 -219
- package/src/__tests__/builtin-session-search.test.ts +0 -262
- package/src/__tests__/builtin-session-select.test.ts +0 -116
- package/src/__tests__/builtin-sigint.test.ts +0 -56
- package/src/__tests__/builtin-skills.test.ts +0 -284
- package/src/__tests__/builtin-web-tools.test.ts +0 -220
- package/src/__tests__/card-action-routing.test.ts +0 -18
- package/src/__tests__/card-plain-text.test.ts +0 -45
- package/src/__tests__/cardkit.test.ts +0 -60
- package/src/__tests__/cards.test.ts +0 -607
- package/src/__tests__/ccc-adapter.test.ts +0 -194
- package/src/__tests__/chatgpt-subscription-rpc.test.ts +0 -89
- package/src/__tests__/chatgpt-subscription.test.ts +0 -135
- package/src/__tests__/chrome-devtools-guard.test.ts +0 -165
- package/src/__tests__/claude-adapter.test.ts +0 -614
- package/src/__tests__/claude-raw-stream-log.test.ts +0 -96
- package/src/__tests__/claude-sdk-installer.test.ts +0 -285
- package/src/__tests__/codex-adapter.test.ts +0 -331
- package/src/__tests__/codex-raw-stream-log.test.ts +0 -170
- package/src/__tests__/codex-reset-actions.test.ts +0 -146
- package/src/__tests__/config-reload.test.ts +0 -284
- package/src/__tests__/config-sample.test.ts +0 -97
- package/src/__tests__/config-utils.test.ts +0 -40
- package/src/__tests__/config.test.ts +0 -395
- package/src/__tests__/crash-logging.test.ts +0 -360
- package/src/__tests__/cursor-adapter.test.ts +0 -890
- package/src/__tests__/cursor-session-meta-store.test.ts +0 -212
- package/src/__tests__/feishu-api.test.ts +0 -60
- package/src/__tests__/feishu-avatar.test.ts +0 -504
- package/src/__tests__/feishu-message-ingress.test.ts +0 -138
- package/src/__tests__/feishu-platform.test.ts +0 -75
- package/src/__tests__/fixtures/codex_simple_text.jsonl +0 -4
- package/src/__tests__/fixtures/codex_with_tool.jsonl +0 -6
- package/src/__tests__/fixtures/cursor_partial_only.jsonl +0 -5
- package/src/__tests__/fixtures/cursor_partial_with_final.jsonl +0 -13
- package/src/__tests__/fixtures/cursor_with_tool_call.jsonl +0 -12
- package/src/__tests__/format-message.test.ts +0 -316
- package/src/__tests__/git-command.test.ts +0 -288
- package/src/__tests__/im-skills.test.ts +0 -125
- package/src/__tests__/jsonl-stream.test.ts +0 -79
- package/src/__tests__/orchestrator.test.ts +0 -1268
- package/src/__tests__/package-files.test.ts +0 -24
- package/src/__tests__/platform-startup.test.ts +0 -19
- package/src/__tests__/privacy.test.ts +0 -198
- package/src/__tests__/proc-tree-kill.test.ts +0 -108
- package/src/__tests__/progress-reducer.test.ts +0 -121
- package/src/__tests__/raw-stream-log.test.ts +0 -106
- package/src/__tests__/response-stall.test.ts +0 -49
- package/src/__tests__/restart.test.ts +0 -232
- package/src/__tests__/session-ccc-config.test.ts +0 -66
- package/src/__tests__/session.test.ts +0 -3004
- package/src/__tests__/shared-prefix.test.ts +0 -36
- package/src/__tests__/sim-agent.test.ts +0 -174
- package/src/__tests__/sim-platform.test.ts +0 -93
- package/src/__tests__/sim-store.test.ts +0 -214
- package/src/__tests__/simplify.test.ts +0 -283
- package/src/__tests__/startup-lifecycle.test.ts +0 -231
- package/src/__tests__/stop-session.test.ts +0 -162
- package/src/__tests__/stream-state.test.ts +0 -164
- package/src/__tests__/terminal-error.test.ts +0 -54
- package/src/__tests__/terminal-renderer.test.ts +0 -247
- package/src/__tests__/update-command-guard.test.ts +0 -144
- package/src/__tests__/web-ui.test.ts +0 -438
- package/src/__tests__/wechat-platform.test.ts +0 -111
- package/src/adapters/adapter-interface.ts +0 -217
- package/src/adapters/ccc-adapter.ts +0 -150
- package/src/adapters/claude-adapter.ts +0 -673
- package/src/adapters/claude-session-meta-store.ts +0 -120
- package/src/adapters/codex-adapter.ts +0 -426
- package/src/adapters/codex-session-meta-store.ts +0 -131
- package/src/adapters/cursor-adapter.ts +0 -681
- package/src/adapters/cursor-session-meta-store.ts +0 -154
- package/src/adapters/jsonl-stream.ts +0 -157
- package/src/adapters/raw-stream-log.ts +0 -124
- package/src/adapters/resource-monitor.ts +0 -141
- package/src/agent-activity.ts +0 -175
- package/src/agent-delegate-task-rpc.ts +0 -153
- package/src/agent-delegate-task.ts +0 -91
- package/src/agent-file-rpc.ts +0 -172
- package/src/agent-image-rpc.ts +0 -168
- package/src/agent-platform-routing.ts +0 -28
- package/src/agent-reload-config-rpc.ts +0 -34
- package/src/agent-rpc-body.ts +0 -92
- package/src/agent-stop-stuck.ts +0 -129
- package/src/card-action-routing.ts +0 -14
- package/src/card-plain-text.ts +0 -108
- package/src/cardkit.ts +0 -179
- package/src/cards.ts +0 -684
- package/src/chatgpt-subscription-rpc.ts +0 -27
- package/src/chatgpt-subscription.ts +0 -299
- package/src/chrome-devtools-guard.ts +0 -318
- package/src/claude-sdk-installer.ts +0 -324
- package/src/codex-reset-actions.ts +0 -184
- package/src/config-utils.ts +0 -211
- package/src/config.ts +0 -1063
- package/src/cursor-usage.ts +0 -128
- package/src/exit-banner.ts +0 -33
- package/src/feishu-api.ts +0 -1616
- package/src/feishu-message-ingress.ts +0 -195
- package/src/feishu-platform.ts +0 -159
- package/src/format-message.ts +0 -293
- package/src/git-command.ts +0 -202
- package/src/im-skills.ts +0 -149
- package/src/index.ts +0 -1089
- package/src/litellm-proxy.ts +0 -374
- package/src/orchestrator.ts +0 -2543
- package/src/platform-adapter.ts +0 -70
- package/src/platform-startup.ts +0 -16
- package/src/privacy.ts +0 -118
- package/src/progress/reducer.ts +0 -113
- package/src/progress/terminal-renderer.ts +0 -294
- package/src/progress/view.ts +0 -77
- package/src/response-stall.ts +0 -28
- package/src/runtime-reload.ts +0 -34
- package/src/session-chat-binding.ts +0 -292
- package/src/session-name.ts +0 -8
- package/src/session.ts +0 -2659
- package/src/shared-prefix.ts +0 -29
- package/src/shared.ts +0 -552
- package/src/sim-agent.ts +0 -167
- package/src/sim-platform.ts +0 -177
- package/src/sim-store.ts +0 -317
- package/src/simplify.ts +0 -120
- package/src/startup-lifecycle.ts +0 -250
- package/src/stream-state.ts +0 -177
- package/src/terminal-error.ts +0 -129
- package/src/trace.ts +0 -51
- package/src/turn-cards.ts +0 -118
- package/src/update-command-guard.ts +0 -165
- package/src/wechat-platform.ts +0 -680
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
import { createInterface } from "node:readline";
|
|
2
|
+
export const DEFAULT_BAD_JSON_IDLE_TIMEOUT_MS = 120_000;
|
|
3
|
+
export const BAD_JSON_IDLE_TIMEOUT_CODE = "BAD_JSON_IDLE_TIMEOUT";
|
|
4
|
+
export class BadJsonIdleTimeoutError extends Error {
|
|
5
|
+
code = BAD_JSON_IDLE_TIMEOUT_CODE;
|
|
6
|
+
tool;
|
|
7
|
+
tag;
|
|
8
|
+
timeoutMs;
|
|
9
|
+
lineNumber;
|
|
10
|
+
lineExcerpt;
|
|
11
|
+
parseError;
|
|
12
|
+
constructor(args) {
|
|
13
|
+
super(`${args.tool} stream stopped after invalid JSON for ${args.timeoutMs}ms ` +
|
|
14
|
+
`(tag=${args.tag}, line=${args.lineNumber}): ${args.lineExcerpt}`);
|
|
15
|
+
this.name = "BadJsonIdleTimeoutError";
|
|
16
|
+
this.tool = args.tool;
|
|
17
|
+
this.tag = args.tag;
|
|
18
|
+
this.timeoutMs = args.timeoutMs;
|
|
19
|
+
this.lineNumber = args.lineNumber;
|
|
20
|
+
this.lineExcerpt = args.lineExcerpt;
|
|
21
|
+
this.parseError = args.parseError;
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
function isJsonLike(line) {
|
|
25
|
+
return line.startsWith("{") || line.startsWith("[");
|
|
26
|
+
}
|
|
27
|
+
function lineExcerpt(line) {
|
|
28
|
+
const max = 500;
|
|
29
|
+
return line.length <= max ? line : `${line.slice(0, max)}...`;
|
|
30
|
+
}
|
|
31
|
+
function parseErrorMessage(error) {
|
|
32
|
+
return error instanceof Error ? error.message : String(error);
|
|
33
|
+
}
|
|
34
|
+
export async function* readJsonLinesWithBadJsonIdleWatchdog({ input, tool, tag = tool, signal, rawLog, idleTimeoutMs = DEFAULT_BAD_JSON_IDLE_TIMEOUT_MS, parse = (line) => JSON.parse(line), onRawLine, onParsedLine, onDone, }) {
|
|
35
|
+
const rl = createInterface({ input, crlfDelay: Infinity });
|
|
36
|
+
let lineCount = 0;
|
|
37
|
+
let protocolError = null;
|
|
38
|
+
let badJsonIdleTimer = null;
|
|
39
|
+
const clearBadJsonIdleTimer = () => {
|
|
40
|
+
if (!badJsonIdleTimer)
|
|
41
|
+
return;
|
|
42
|
+
clearTimeout(badJsonIdleTimer);
|
|
43
|
+
badJsonIdleTimer = null;
|
|
44
|
+
};
|
|
45
|
+
const armBadJsonIdleTimer = (line, lineNumber, error) => {
|
|
46
|
+
if (idleTimeoutMs <= 0)
|
|
47
|
+
return;
|
|
48
|
+
clearBadJsonIdleTimer();
|
|
49
|
+
badJsonIdleTimer = setTimeout(() => {
|
|
50
|
+
protocolError = new BadJsonIdleTimeoutError({
|
|
51
|
+
tool,
|
|
52
|
+
tag,
|
|
53
|
+
timeoutMs: idleTimeoutMs,
|
|
54
|
+
lineNumber,
|
|
55
|
+
lineExcerpt: lineExcerpt(line),
|
|
56
|
+
parseError: parseErrorMessage(error),
|
|
57
|
+
});
|
|
58
|
+
rl.close();
|
|
59
|
+
}, idleTimeoutMs);
|
|
60
|
+
const timerWithUnref = badJsonIdleTimer;
|
|
61
|
+
timerWithUnref.unref?.();
|
|
62
|
+
};
|
|
63
|
+
const onAbort = () => {
|
|
64
|
+
clearBadJsonIdleTimer();
|
|
65
|
+
rl.close();
|
|
66
|
+
};
|
|
67
|
+
signal?.addEventListener("abort", onAbort, { once: true });
|
|
68
|
+
try {
|
|
69
|
+
for await (const line of rl) {
|
|
70
|
+
if (signal?.aborted)
|
|
71
|
+
break;
|
|
72
|
+
lineCount++;
|
|
73
|
+
onRawLine?.(line, lineCount);
|
|
74
|
+
const trimmed = line.trim();
|
|
75
|
+
if (!trimmed)
|
|
76
|
+
continue;
|
|
77
|
+
clearBadJsonIdleTimer();
|
|
78
|
+
rawLog?.writeLine(trimmed);
|
|
79
|
+
try {
|
|
80
|
+
const parsed = parse(trimmed);
|
|
81
|
+
onParsedLine?.(parsed, trimmed, lineCount);
|
|
82
|
+
yield parsed;
|
|
83
|
+
}
|
|
84
|
+
catch (error) {
|
|
85
|
+
if (isJsonLike(trimmed)) {
|
|
86
|
+
armBadJsonIdleTimer(trimmed, lineCount, error);
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
if (protocolError && !signal?.aborted) {
|
|
91
|
+
throw protocolError;
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
finally {
|
|
95
|
+
signal?.removeEventListener("abort", onAbort);
|
|
96
|
+
clearBadJsonIdleTimer();
|
|
97
|
+
rl.close();
|
|
98
|
+
onDone?.({
|
|
99
|
+
lineCount,
|
|
100
|
+
signalAborted: signal?.aborted ?? false,
|
|
101
|
+
protocolError,
|
|
102
|
+
});
|
|
103
|
+
}
|
|
104
|
+
}
|
|
@@ -1,97 +1,94 @@
|
|
|
1
|
-
// =============================================================================
|
|
2
|
-
// proc-tree-kill.ts — 跨平台进程树强杀工具
|
|
3
|
-
// =============================================================================
|
|
4
|
-
// 背景:codex / cursor adapter 通过 `spawn(cmd, args, { shell: true })` 启动 CLI
|
|
5
|
-
// 时,Node 拿到的 proc.pid 是最外层 cmd.exe(Windows)或 /bin/sh(其它)的
|
|
6
|
-
// PID。真正干活的是它再 spawn 出来的:
|
|
7
|
-
//
|
|
8
|
-
// cmd.exe ← proc.kill() 只能杀到这一层
|
|
9
|
-
// └─ node codex.js ← Codex CLI 入口
|
|
10
|
-
// └─ codex.exe ← 实际 Rust 二进制(继续烧 token)
|
|
11
|
-
//
|
|
12
|
-
// 单纯 proc.kill() 在 Windows 上等价于 TerminateProcess 顶层壳,孙子进程不会
|
|
13
|
-
// 收到任何信号、继续运行,导致用户 /stop 看似生效(adapter 标记 stopped)但
|
|
14
|
-
// 实际 codex 仍在后台跑、stream-state 一直停在 "running"。
|
|
15
|
-
//
|
|
16
|
-
// 解决方案:abort 时不要走 proc.kill(),而是用本工具按 pid 杀掉整棵进程树。
|
|
17
|
-
// - Windows: `taskkill /pid <pid> /T /F`(/T = 递归子进程, /F = 强制)
|
|
18
|
-
// - 其它:`process.kill(-pgid, "SIGTERM")` + 兜底 SIGKILL(adapter spawn 时
|
|
19
|
-
// 需配合 detached:true 让子进程拥有独立 process group)
|
|
20
|
-
// =============================================================================
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
*
|
|
26
|
-
*
|
|
27
|
-
* -
|
|
28
|
-
*
|
|
29
|
-
*
|
|
30
|
-
*
|
|
31
|
-
*
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
// ---------------------------------------------------------------------------
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
// ---------------------------------------------------------------------------
|
|
77
|
-
|
|
78
|
-
//
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
// 进程已不存在或权限不足,忽略
|
|
96
|
-
}
|
|
97
|
-
}
|
|
1
|
+
// =============================================================================
|
|
2
|
+
// proc-tree-kill.ts — 跨平台进程树强杀工具
|
|
3
|
+
// =============================================================================
|
|
4
|
+
// 背景:codex / cursor adapter 通过 `spawn(cmd, args, { shell: true })` 启动 CLI
|
|
5
|
+
// 时,Node 拿到的 proc.pid 是最外层 cmd.exe(Windows)或 /bin/sh(其它)的
|
|
6
|
+
// PID。真正干活的是它再 spawn 出来的:
|
|
7
|
+
//
|
|
8
|
+
// cmd.exe ← proc.kill() 只能杀到这一层
|
|
9
|
+
// └─ node codex.js ← Codex CLI 入口
|
|
10
|
+
// └─ codex.exe ← 实际 Rust 二进制(继续烧 token)
|
|
11
|
+
//
|
|
12
|
+
// 单纯 proc.kill() 在 Windows 上等价于 TerminateProcess 顶层壳,孙子进程不会
|
|
13
|
+
// 收到任何信号、继续运行,导致用户 /stop 看似生效(adapter 标记 stopped)但
|
|
14
|
+
// 实际 codex 仍在后台跑、stream-state 一直停在 "running"。
|
|
15
|
+
//
|
|
16
|
+
// 解决方案:abort 时不要走 proc.kill(),而是用本工具按 pid 杀掉整棵进程树。
|
|
17
|
+
// - Windows: `taskkill /pid <pid> /T /F`(/T = 递归子进程, /F = 强制)
|
|
18
|
+
// - 其它:`process.kill(-pgid, "SIGTERM")` + 兜底 SIGKILL(adapter spawn 时
|
|
19
|
+
// 需配合 detached:true 让子进程拥有独立 process group)
|
|
20
|
+
// =============================================================================
|
|
21
|
+
import { spawn } from "node:child_process";
|
|
22
|
+
/** 异步杀掉以 pid 为根的整棵进程树。
|
|
23
|
+
*
|
|
24
|
+
* 设计目标:永不抛错、永不阻塞调用者。
|
|
25
|
+
* - pid 不存在、参数缺失 → 静默返回
|
|
26
|
+
* - 子进程 spawn 失败 → console.warn 但不 reject
|
|
27
|
+
* - Windows 上 taskkill 异步执行,不阻塞 event loop
|
|
28
|
+
*
|
|
29
|
+
* 调用方约定:返回的 Promise 在 kill 命令发出后立即 resolve。
|
|
30
|
+
* 真正的进程退出由 OS 异步完成,调用方如果需要确认"已死透",应自己再轮询
|
|
31
|
+
* `process.kill(pid, 0)`。
|
|
32
|
+
*/
|
|
33
|
+
export async function killProcessTree(pid) {
|
|
34
|
+
if (pid == null || !Number.isFinite(pid) || pid <= 0)
|
|
35
|
+
return;
|
|
36
|
+
if (process.platform === "win32") {
|
|
37
|
+
await killWindowsTree(pid);
|
|
38
|
+
return;
|
|
39
|
+
}
|
|
40
|
+
await killPosixTree(pid);
|
|
41
|
+
}
|
|
42
|
+
// ---------------------------------------------------------------------------
|
|
43
|
+
// Windows: taskkill /T /F
|
|
44
|
+
// ---------------------------------------------------------------------------
|
|
45
|
+
function killWindowsTree(pid) {
|
|
46
|
+
return new Promise((resolve) => {
|
|
47
|
+
let resolved = false;
|
|
48
|
+
const done = () => {
|
|
49
|
+
if (resolved)
|
|
50
|
+
return;
|
|
51
|
+
resolved = true;
|
|
52
|
+
resolve();
|
|
53
|
+
};
|
|
54
|
+
try {
|
|
55
|
+
const proc = spawn("taskkill", ["/pid", String(pid), "/T", "/F"], {
|
|
56
|
+
stdio: "ignore",
|
|
57
|
+
windowsHide: true,
|
|
58
|
+
// taskkill 本身很快(<200ms),不需要 detached
|
|
59
|
+
});
|
|
60
|
+
proc.once("error", (err) => {
|
|
61
|
+
console.warn(`[killProcessTree] taskkill spawn error for pid=${pid}: ${err.message}`);
|
|
62
|
+
done();
|
|
63
|
+
});
|
|
64
|
+
proc.once("close", () => { done(); });
|
|
65
|
+
// 兜底超时:3 秒后强制 resolve,避免极端情况下 hang 住调用方
|
|
66
|
+
setTimeout(done, 3000).unref();
|
|
67
|
+
}
|
|
68
|
+
catch (err) {
|
|
69
|
+
console.warn(`[killProcessTree] taskkill failed for pid=${pid}: ${err.message}`);
|
|
70
|
+
done();
|
|
71
|
+
}
|
|
72
|
+
});
|
|
73
|
+
}
|
|
74
|
+
// ---------------------------------------------------------------------------
|
|
75
|
+
// POSIX: 优先按 process group 杀,回退到按 pid 杀
|
|
76
|
+
// ---------------------------------------------------------------------------
|
|
77
|
+
async function killPosixTree(pid) {
|
|
78
|
+
// 第一次尝试:按 process group 发 SIGTERM。要求 spawn 时 detached:true。
|
|
79
|
+
trySignal(-pid, "SIGTERM");
|
|
80
|
+
trySignal(pid, "SIGTERM");
|
|
81
|
+
// 给进程 1 秒优雅退出机会
|
|
82
|
+
await new Promise((r) => setTimeout(r, 1000));
|
|
83
|
+
// 兜底:SIGKILL
|
|
84
|
+
trySignal(-pid, "SIGKILL");
|
|
85
|
+
trySignal(pid, "SIGKILL");
|
|
86
|
+
}
|
|
87
|
+
function trySignal(target, signal) {
|
|
88
|
+
try {
|
|
89
|
+
process.kill(target, signal);
|
|
90
|
+
}
|
|
91
|
+
catch {
|
|
92
|
+
// 进程已不存在或权限不足,忽略
|
|
93
|
+
}
|
|
94
|
+
}
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
import { createWriteStream } from "node:fs";
|
|
2
|
+
import { mkdir, readdir, rm, stat, unlink } from "node:fs/promises";
|
|
3
|
+
import { once } from "node:events";
|
|
4
|
+
import { join } from "node:path";
|
|
5
|
+
import { createGzip } from "node:zlib";
|
|
6
|
+
export function sanitizeLogPathSegment(value) {
|
|
7
|
+
const safe = value.replace(/[^a-zA-Z0-9._-]+/g, "_").replace(/^[._]+|_+$/g, "");
|
|
8
|
+
return safe || "unknown";
|
|
9
|
+
}
|
|
10
|
+
async function cleanupOldRawStreamLogs(rootDir, retentionDays) {
|
|
11
|
+
if (!Number.isFinite(retentionDays) || retentionDays <= 0)
|
|
12
|
+
return;
|
|
13
|
+
const cutoff = Date.now() - retentionDays * 24 * 60 * 60 * 1000;
|
|
14
|
+
const visit = async (dir) => {
|
|
15
|
+
let entries;
|
|
16
|
+
try {
|
|
17
|
+
entries = await readdir(dir, { withFileTypes: true });
|
|
18
|
+
}
|
|
19
|
+
catch {
|
|
20
|
+
return;
|
|
21
|
+
}
|
|
22
|
+
await Promise.all(entries.map(async (entry) => {
|
|
23
|
+
const path = join(dir, entry.name);
|
|
24
|
+
if (entry.isDirectory()) {
|
|
25
|
+
await visit(path);
|
|
26
|
+
try {
|
|
27
|
+
await rm(path, { recursive: false });
|
|
28
|
+
}
|
|
29
|
+
catch {
|
|
30
|
+
// Directory is not empty or already gone.
|
|
31
|
+
}
|
|
32
|
+
return;
|
|
33
|
+
}
|
|
34
|
+
if (!entry.isFile())
|
|
35
|
+
return;
|
|
36
|
+
try {
|
|
37
|
+
const info = await stat(path);
|
|
38
|
+
if (info.mtimeMs < cutoff)
|
|
39
|
+
await rm(path, { force: true });
|
|
40
|
+
}
|
|
41
|
+
catch {
|
|
42
|
+
// Best-effort cleanup only.
|
|
43
|
+
}
|
|
44
|
+
}));
|
|
45
|
+
};
|
|
46
|
+
await visit(rootDir);
|
|
47
|
+
}
|
|
48
|
+
export async function createRawStreamLog(options) {
|
|
49
|
+
if (!options.enabled)
|
|
50
|
+
return null;
|
|
51
|
+
const maxBytes = Math.max(0, Math.floor(options.maxBytesPerTurn));
|
|
52
|
+
const tool = sanitizeLogPathSegment(options.tool);
|
|
53
|
+
const session = sanitizeLogPathSegment(options.sessionId);
|
|
54
|
+
const label = sanitizeLogPathSegment(options.label);
|
|
55
|
+
const dir = join(options.rootDir, tool, session);
|
|
56
|
+
await mkdir(dir, { recursive: true });
|
|
57
|
+
void cleanupOldRawStreamLogs(join(options.rootDir, tool), options.retentionDays);
|
|
58
|
+
const timestamp = new Date().toISOString().replace(/[:.]/g, "-");
|
|
59
|
+
const filePath = join(dir, `${timestamp}-${label}.jsonl.gz`);
|
|
60
|
+
const output = createWriteStream(filePath);
|
|
61
|
+
const gzip = createGzip();
|
|
62
|
+
gzip.pipe(output);
|
|
63
|
+
let bytes = 0;
|
|
64
|
+
let truncated = false;
|
|
65
|
+
let ended = false;
|
|
66
|
+
const writeLine = (line) => {
|
|
67
|
+
if (ended || truncated)
|
|
68
|
+
return;
|
|
69
|
+
const payload = `${line}\n`;
|
|
70
|
+
const payloadBytes = Buffer.byteLength(payload, "utf-8");
|
|
71
|
+
if (maxBytes > 0 && bytes + payloadBytes > maxBytes) {
|
|
72
|
+
const marker = JSON.stringify({
|
|
73
|
+
type: "chatccc_raw_stream_log_truncated",
|
|
74
|
+
reason: "max_bytes_per_turn_exceeded",
|
|
75
|
+
maxBytesPerTurn: maxBytes,
|
|
76
|
+
writtenBytes: bytes,
|
|
77
|
+
});
|
|
78
|
+
gzip.write(`${marker}\n`);
|
|
79
|
+
truncated = true;
|
|
80
|
+
return;
|
|
81
|
+
}
|
|
82
|
+
bytes += payloadBytes;
|
|
83
|
+
gzip.write(payload);
|
|
84
|
+
};
|
|
85
|
+
const close = async ({ keep }) => {
|
|
86
|
+
if (ended)
|
|
87
|
+
return;
|
|
88
|
+
ended = true;
|
|
89
|
+
gzip.end();
|
|
90
|
+
try {
|
|
91
|
+
await once(output, "finish");
|
|
92
|
+
}
|
|
93
|
+
catch {
|
|
94
|
+
// Ignore close errors; caller cannot recover from debug log failures.
|
|
95
|
+
}
|
|
96
|
+
if (!keep) {
|
|
97
|
+
try {
|
|
98
|
+
await unlink(filePath);
|
|
99
|
+
}
|
|
100
|
+
catch {
|
|
101
|
+
// Best-effort cleanup only.
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
};
|
|
105
|
+
return { filePath, writeLine, close };
|
|
106
|
+
}
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
// =============================================================================
|
|
2
|
+
// resource-monitor.ts — CLI 进程资源监控(CPU + 内存)
|
|
3
|
+
// =============================================================================
|
|
4
|
+
// 对所有 chatccc 启动的 CLI 进程持续监控 CPU 和内存占用。
|
|
5
|
+
// 若连续 3 分钟两项指标均无变化,判定为僵死,发出 "stuck" 事件。
|
|
6
|
+
// =============================================================================
|
|
7
|
+
import { exec } from "node:child_process";
|
|
8
|
+
import { EventEmitter } from "node:events";
|
|
9
|
+
const CHECK_INTERVAL_MS = 30_000; // 30 秒检查一次
|
|
10
|
+
const STUCK_THRESHOLD = 6; // 连续 6 次无变化 = 3 分钟
|
|
11
|
+
export const resourceMonitor = new EventEmitter();
|
|
12
|
+
const tracked = new Map();
|
|
13
|
+
let timer = null;
|
|
14
|
+
function startIfNeeded() {
|
|
15
|
+
if (timer)
|
|
16
|
+
return;
|
|
17
|
+
timer = setInterval(checkAll, CHECK_INTERVAL_MS);
|
|
18
|
+
timer.unref?.();
|
|
19
|
+
}
|
|
20
|
+
function stopIfIdle() {
|
|
21
|
+
if (tracked.size > 0)
|
|
22
|
+
return;
|
|
23
|
+
if (timer) {
|
|
24
|
+
clearInterval(timer);
|
|
25
|
+
timer = null;
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
export function registerProcess(pid, sessionId) {
|
|
29
|
+
tracked.set(pid, { pid, sessionId, snapshot: { cpu: -1, memory: -1, unchangedCount: 0 } });
|
|
30
|
+
startIfNeeded();
|
|
31
|
+
}
|
|
32
|
+
export function unregisterProcess(pid) {
|
|
33
|
+
tracked.delete(pid);
|
|
34
|
+
stopIfIdle();
|
|
35
|
+
}
|
|
36
|
+
// ---------------------------------------------------------------------------
|
|
37
|
+
// 批量查询进程指标(Windows PowerShell)
|
|
38
|
+
// ---------------------------------------------------------------------------
|
|
39
|
+
function execPowerShell(script, timeoutMs) {
|
|
40
|
+
return new Promise((resolve, reject) => {
|
|
41
|
+
exec(`powershell -NoProfile -Command "${script}"`, { timeout: timeoutMs, windowsHide: true }, (err, stdout) => {
|
|
42
|
+
if (err)
|
|
43
|
+
reject(err);
|
|
44
|
+
else
|
|
45
|
+
resolve(stdout);
|
|
46
|
+
});
|
|
47
|
+
});
|
|
48
|
+
}
|
|
49
|
+
async function getProcessMetrics(pids) {
|
|
50
|
+
const result = new Map();
|
|
51
|
+
if (pids.length === 0)
|
|
52
|
+
return result;
|
|
53
|
+
const psScript = `Get-Process -Id ${pids.join(",")} -ErrorAction SilentlyContinue | ForEach-Object { "$($_.Id)|$($_.CPU)|$($_.WorkingSet64)" }`;
|
|
54
|
+
try {
|
|
55
|
+
const stdout = await execPowerShell(psScript, 10_000);
|
|
56
|
+
for (const line of stdout.trim().split(/\r?\n/)) {
|
|
57
|
+
const trimmed = line.trim();
|
|
58
|
+
if (!trimmed)
|
|
59
|
+
continue;
|
|
60
|
+
const [idStr, cpuStr, memStr] = trimmed.split("|");
|
|
61
|
+
const id = parseInt(idStr, 10);
|
|
62
|
+
const cpu = parseFloat(cpuStr);
|
|
63
|
+
const memory = parseInt(memStr, 10);
|
|
64
|
+
if (!isNaN(id) && !isNaN(cpu) && !isNaN(memory)) {
|
|
65
|
+
result.set(id, { cpu, memory });
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
catch {
|
|
70
|
+
// PowerShell 查询失败时跳过本轮,下次重试
|
|
71
|
+
}
|
|
72
|
+
return result;
|
|
73
|
+
}
|
|
74
|
+
// ---------------------------------------------------------------------------
|
|
75
|
+
// 定时检查
|
|
76
|
+
// ---------------------------------------------------------------------------
|
|
77
|
+
async function checkAll() {
|
|
78
|
+
if (tracked.size === 0)
|
|
79
|
+
return;
|
|
80
|
+
const pids = [...tracked.keys()];
|
|
81
|
+
const metrics = await getProcessMetrics(pids);
|
|
82
|
+
for (const [pid, tp] of tracked) {
|
|
83
|
+
const m = metrics.get(pid);
|
|
84
|
+
if (!m) {
|
|
85
|
+
// 进程已不存在,停止追踪
|
|
86
|
+
tracked.delete(pid);
|
|
87
|
+
continue;
|
|
88
|
+
}
|
|
89
|
+
const prev = tp.snapshot;
|
|
90
|
+
const cpuChanged = m.cpu !== prev.cpu;
|
|
91
|
+
// 内存允许 ±1% 波动,避免正常抖动触发误判
|
|
92
|
+
const memTolerance = prev.memory > 0 ? Math.max(prev.memory * 0.01, 1024 * 1024) : 1024 * 1024;
|
|
93
|
+
const memChanged = Math.abs(m.memory - prev.memory) > memTolerance;
|
|
94
|
+
if (!cpuChanged && !memChanged) {
|
|
95
|
+
tp.snapshot.unchangedCount++;
|
|
96
|
+
if (tp.snapshot.unchangedCount >= STUCK_THRESHOLD) {
|
|
97
|
+
const idleMinutes = Math.round((tp.snapshot.unchangedCount * CHECK_INTERVAL_MS) / 60_000);
|
|
98
|
+
resourceMonitor.emit("stuck", {
|
|
99
|
+
pid: tp.pid,
|
|
100
|
+
sessionId: tp.sessionId,
|
|
101
|
+
idleMinutes,
|
|
102
|
+
});
|
|
103
|
+
tracked.delete(pid);
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
else {
|
|
107
|
+
tp.snapshot.unchangedCount = 0;
|
|
108
|
+
}
|
|
109
|
+
tp.snapshot.cpu = m.cpu;
|
|
110
|
+
tp.snapshot.memory = m.memory;
|
|
111
|
+
}
|
|
112
|
+
stopIfIdle();
|
|
113
|
+
}
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
export function createAgentActivityTracker(now = Date.now()) {
|
|
2
|
+
return {
|
|
3
|
+
activity: { kind: "starting", startedAt: now },
|
|
4
|
+
activeTools: new Map(),
|
|
5
|
+
nextAnonymousToolId: 1,
|
|
6
|
+
};
|
|
7
|
+
}
|
|
8
|
+
function sameVisibleActivity(left, right) {
|
|
9
|
+
return left.kind === right.kind
|
|
10
|
+
&& left.toolName === right.toolName
|
|
11
|
+
&& left.toolCount === right.toolCount;
|
|
12
|
+
}
|
|
13
|
+
function setActivity(tracker, next) {
|
|
14
|
+
if (sameVisibleActivity(tracker.activity, next))
|
|
15
|
+
return false;
|
|
16
|
+
tracker.activity = next;
|
|
17
|
+
return true;
|
|
18
|
+
}
|
|
19
|
+
function refreshToolActivity(tracker) {
|
|
20
|
+
const tools = [...tracker.activeTools.values()];
|
|
21
|
+
const first = tools[0];
|
|
22
|
+
if (!first)
|
|
23
|
+
return false;
|
|
24
|
+
return setActivity(tracker, {
|
|
25
|
+
kind: "tool",
|
|
26
|
+
startedAt: first.startedAt,
|
|
27
|
+
toolName: first.name,
|
|
28
|
+
toolCount: tools.length,
|
|
29
|
+
});
|
|
30
|
+
}
|
|
31
|
+
function removeCompletedTool(tracker, toolUseId) {
|
|
32
|
+
if (toolUseId && tracker.activeTools.delete(toolUseId))
|
|
33
|
+
return;
|
|
34
|
+
// Older adapters did not always include a tool ID. Prefer an anonymous entry;
|
|
35
|
+
// if there is only one active call, it is still safe to match that result.
|
|
36
|
+
const anonymousId = [...tracker.activeTools.keys()].find((id) => id.startsWith("anonymous:"));
|
|
37
|
+
if (anonymousId) {
|
|
38
|
+
tracker.activeTools.delete(anonymousId);
|
|
39
|
+
}
|
|
40
|
+
else if (tracker.activeTools.size === 1) {
|
|
41
|
+
const onlyId = tracker.activeTools.keys().next().value;
|
|
42
|
+
if (onlyId)
|
|
43
|
+
tracker.activeTools.delete(onlyId);
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* Applies one normalized Agent event and returns whether the persisted activity
|
|
48
|
+
* changed. Tool activity takes precedence while a tool call is still active.
|
|
49
|
+
*/
|
|
50
|
+
export function updateAgentActivity(tracker, block, now = Date.now()) {
|
|
51
|
+
if (block.type === "tool_use") {
|
|
52
|
+
const id = block.id || `anonymous:${tracker.nextAnonymousToolId++}`;
|
|
53
|
+
const existing = tracker.activeTools.get(id);
|
|
54
|
+
tracker.activeTools.set(id, {
|
|
55
|
+
id,
|
|
56
|
+
name: block.name || "未知工具",
|
|
57
|
+
startedAt: existing?.startedAt ?? now,
|
|
58
|
+
});
|
|
59
|
+
return refreshToolActivity(tracker);
|
|
60
|
+
}
|
|
61
|
+
if (block.type === "tool_result") {
|
|
62
|
+
removeCompletedTool(tracker, block.tool_use_id);
|
|
63
|
+
if (tracker.activeTools.size > 0)
|
|
64
|
+
return refreshToolActivity(tracker);
|
|
65
|
+
return setActivity(tracker, { kind: "processing", startedAt: now });
|
|
66
|
+
}
|
|
67
|
+
if (tracker.activeTools.size > 0)
|
|
68
|
+
return false;
|
|
69
|
+
switch (block.type) {
|
|
70
|
+
case "agent_status":
|
|
71
|
+
return setActivity(tracker, {
|
|
72
|
+
kind: block.status === "compacting" ? "compacting" : "responding",
|
|
73
|
+
startedAt: now,
|
|
74
|
+
});
|
|
75
|
+
case "thinking":
|
|
76
|
+
case "redacted_thinking":
|
|
77
|
+
return setActivity(tracker, { kind: "thinking", startedAt: now });
|
|
78
|
+
case "text":
|
|
79
|
+
case "text_final":
|
|
80
|
+
return setActivity(tracker, { kind: "responding", startedAt: now });
|
|
81
|
+
case "search_result":
|
|
82
|
+
return setActivity(tracker, { kind: "searching", startedAt: now });
|
|
83
|
+
case "compact_boundary":
|
|
84
|
+
return setActivity(tracker, { kind: "compacting", startedAt: now });
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
function formatElapsed(startedAt, now) {
|
|
88
|
+
const totalSeconds = Math.max(0, Math.floor((now - startedAt) / 1000));
|
|
89
|
+
if (totalSeconds < 60)
|
|
90
|
+
return `${totalSeconds}秒`;
|
|
91
|
+
const totalMinutes = Math.floor(totalSeconds / 60);
|
|
92
|
+
const seconds = totalSeconds % 60;
|
|
93
|
+
if (totalMinutes < 60)
|
|
94
|
+
return `${totalMinutes}分${seconds}秒`;
|
|
95
|
+
const hours = Math.floor(totalMinutes / 60);
|
|
96
|
+
const minutes = totalMinutes % 60;
|
|
97
|
+
return `${hours}小时${minutes}分`;
|
|
98
|
+
}
|
|
99
|
+
function displayToolName(name) {
|
|
100
|
+
const normalized = (name || "未知工具").replace(/\s+/g, " ").trim();
|
|
101
|
+
return normalized.length > 24 ? `${normalized.slice(0, 23)}…` : normalized;
|
|
102
|
+
}
|
|
103
|
+
export function formatAgentActivityTitle(activity, now = Date.now()) {
|
|
104
|
+
if (!activity)
|
|
105
|
+
return "正在处理";
|
|
106
|
+
let label;
|
|
107
|
+
switch (activity.kind) {
|
|
108
|
+
case "starting":
|
|
109
|
+
label = "正在启动 Agent";
|
|
110
|
+
break;
|
|
111
|
+
case "thinking":
|
|
112
|
+
label = "思考中";
|
|
113
|
+
break;
|
|
114
|
+
case "tool": {
|
|
115
|
+
const count = Math.max(1, activity.toolCount ?? 1);
|
|
116
|
+
label = `正在执行 ${displayToolName(activity.toolName)}${count > 1 ? ` 等 ${count} 项` : ""}`;
|
|
117
|
+
break;
|
|
118
|
+
}
|
|
119
|
+
case "processing":
|
|
120
|
+
label = "正在处理工具结果";
|
|
121
|
+
break;
|
|
122
|
+
case "responding":
|
|
123
|
+
label = "正在生成回复";
|
|
124
|
+
break;
|
|
125
|
+
case "searching":
|
|
126
|
+
label = "正在处理搜索结果";
|
|
127
|
+
break;
|
|
128
|
+
case "compacting":
|
|
129
|
+
label = "正在整理上下文";
|
|
130
|
+
break;
|
|
131
|
+
}
|
|
132
|
+
return `${label} · ${formatElapsed(activity.startedAt, now)}`;
|
|
133
|
+
}
|