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.
Files changed (280) hide show
  1. package/README.md +1 -1
  2. package/bin/cccagent.mjs +12 -3
  3. package/bin/chatccc.mjs +15 -6
  4. package/deepccc-agent/os-prompts/darwin.md +8 -8
  5. package/deepccc-agent/os-prompts/linux.md +8 -8
  6. package/deepccc-agent/os-prompts/win32.md +11 -11
  7. package/deepccc-agent/package.json +65 -65
  8. package/dist/deepccc-agent/src/cli.js +634 -0
  9. package/dist/deepccc-agent/src/config.js +76 -0
  10. package/dist/deepccc-agent/src/context.js +348 -0
  11. package/dist/deepccc-agent/src/file-log.js +34 -0
  12. package/dist/deepccc-agent/src/file-tools.js +1208 -0
  13. package/dist/deepccc-agent/src/index.js +571 -0
  14. package/dist/deepccc-agent/src/permissions.js +170 -0
  15. package/dist/deepccc-agent/src/privacy.js +124 -0
  16. package/dist/deepccc-agent/src/proc-tree-kill.js +60 -0
  17. package/dist/deepccc-agent/src/progress/cards-helpers.js +70 -0
  18. package/dist/deepccc-agent/src/progress/reducer.js +102 -0
  19. package/dist/deepccc-agent/src/progress/terminal-renderer.js +264 -0
  20. package/dist/deepccc-agent/src/progress/view.js +30 -0
  21. package/dist/deepccc-agent/src/raw-stream-log.js +106 -0
  22. package/dist/deepccc-agent/src/session-search.js +276 -0
  23. package/dist/deepccc-agent/src/session-select.js +23 -0
  24. package/dist/deepccc-agent/src/sigint.js +26 -0
  25. package/dist/deepccc-agent/src/skills.js +178 -0
  26. package/dist/deepccc-agent/src/web-tools.js +246 -0
  27. package/dist/src/adapters/adapter-interface.js +19 -0
  28. package/dist/src/adapters/ccc-adapter.js +112 -0
  29. package/dist/src/adapters/claude-adapter.js +497 -0
  30. package/dist/src/adapters/claude-session-meta-store.js +92 -0
  31. package/dist/src/adapters/codex-adapter.js +279 -0
  32. package/dist/src/adapters/codex-session-meta-store.js +94 -0
  33. package/dist/src/adapters/cursor-adapter.js +491 -0
  34. package/dist/src/adapters/cursor-session-meta-store.js +116 -0
  35. package/dist/src/adapters/jsonl-stream.js +104 -0
  36. package/{src/adapters/proc-tree-kill.ts → dist/src/adapters/proc-tree-kill.js} +94 -97
  37. package/dist/src/adapters/raw-stream-log.js +106 -0
  38. package/dist/src/adapters/resource-monitor.js +113 -0
  39. package/dist/src/agent-activity.js +133 -0
  40. package/dist/src/agent-delegate-task-rpc.js +129 -0
  41. package/dist/src/agent-delegate-task.js +48 -0
  42. package/dist/src/agent-file-rpc.js +152 -0
  43. package/dist/src/agent-image-rpc.js +148 -0
  44. package/dist/src/agent-platform-routing.js +13 -0
  45. package/dist/src/agent-reload-config-rpc.js +23 -0
  46. package/dist/src/agent-rpc-body.js +87 -0
  47. package/dist/src/agent-stop-stuck.js +110 -0
  48. package/dist/src/card-action-routing.js +7 -0
  49. package/dist/src/card-plain-text.js +101 -0
  50. package/dist/src/cardkit.js +158 -0
  51. package/dist/src/cards.js +573 -0
  52. package/dist/src/chatgpt-subscription-rpc.js +18 -0
  53. package/dist/src/chatgpt-subscription.js +199 -0
  54. package/dist/src/chrome-devtools-guard.js +238 -0
  55. package/dist/src/claude-sdk-installer.js +249 -0
  56. package/dist/src/codex-reset-actions.js +143 -0
  57. package/dist/src/config-utils.js +149 -0
  58. package/dist/src/config.js +804 -0
  59. package/dist/src/cursor-usage.js +77 -0
  60. package/dist/src/exit-banner.js +28 -0
  61. package/dist/src/feishu-api.js +1404 -0
  62. package/dist/src/feishu-message-ingress.js +137 -0
  63. package/dist/src/feishu-platform.js +97 -0
  64. package/dist/src/format-message.js +252 -0
  65. package/dist/src/git-command.js +155 -0
  66. package/dist/src/im-skills.js +121 -0
  67. package/dist/src/index.js +833 -0
  68. package/dist/src/litellm-proxy.js +300 -0
  69. package/dist/src/orchestrator.js +2078 -0
  70. package/dist/src/package-root.js +26 -0
  71. package/dist/src/platform-adapter.js +7 -0
  72. package/dist/src/platform-startup.js +6 -0
  73. package/dist/src/privacy.js +100 -0
  74. package/dist/src/progress/reducer.js +102 -0
  75. package/dist/src/progress/terminal-renderer.js +264 -0
  76. package/dist/src/progress/view.js +30 -0
  77. package/dist/src/response-stall.js +14 -0
  78. package/dist/src/runtime-entry.js +13 -0
  79. package/dist/src/runtime-reload.js +19 -0
  80. package/dist/src/session-chat-binding.js +183 -0
  81. package/dist/src/session-name.js +7 -0
  82. package/dist/src/session.js +2144 -0
  83. package/dist/src/shared-prefix.js +16 -0
  84. package/dist/src/shared.js +493 -0
  85. package/dist/src/sim-agent.js +105 -0
  86. package/dist/src/sim-platform.js +142 -0
  87. package/dist/src/sim-store.js +231 -0
  88. package/dist/src/simplify.js +99 -0
  89. package/dist/src/startup-lifecycle.js +209 -0
  90. package/dist/src/stream-state.js +141 -0
  91. package/dist/src/terminal-error.js +100 -0
  92. package/dist/src/trace.js +50 -0
  93. package/dist/src/turn-cards.js +92 -0
  94. package/dist/src/update-command-guard.js +114 -0
  95. package/{src/web-ui.ts → dist/src/web-ui.js} +749 -823
  96. package/dist/src/wechat-platform.js +545 -0
  97. package/package.json +76 -74
  98. package/deepccc-agent/LICENSE +0 -201
  99. package/deepccc-agent/bin/deepccc.mjs +0 -26
  100. package/deepccc-agent/docs/cache-hit-rate-1.jpg +0 -0
  101. package/deepccc-agent/docs/cache-hit-rate-2.jpg +0 -0
  102. package/deepccc-agent/package-lock.json +0 -2027
  103. package/deepccc-agent/src/__tests__/chat-session.test.ts +0 -877
  104. package/deepccc-agent/src/__tests__/cli-json.test.ts +0 -49
  105. package/deepccc-agent/src/__tests__/config.test.ts +0 -34
  106. package/deepccc-agent/src/__tests__/context.test.ts +0 -341
  107. package/deepccc-agent/src/__tests__/file-tools.test.ts +0 -240
  108. package/deepccc-agent/src/__tests__/permissions.test.ts +0 -199
  109. package/deepccc-agent/src/__tests__/privacy.test.ts +0 -318
  110. package/deepccc-agent/src/__tests__/progress-reducer.test.ts +0 -121
  111. package/deepccc-agent/src/__tests__/session-search.test.ts +0 -262
  112. package/deepccc-agent/src/__tests__/session-select.test.ts +0 -116
  113. package/deepccc-agent/src/__tests__/sigint.test.ts +0 -56
  114. package/deepccc-agent/src/__tests__/skills.test.ts +0 -284
  115. package/deepccc-agent/src/__tests__/terminal-renderer.test.ts +0 -247
  116. package/deepccc-agent/src/__tests__/web-tools.test.ts +0 -220
  117. package/deepccc-agent/src/cli.ts +0 -682
  118. package/deepccc-agent/src/config.ts +0 -101
  119. package/deepccc-agent/src/context.ts +0 -465
  120. package/deepccc-agent/src/file-log.ts +0 -38
  121. package/deepccc-agent/src/file-tools.ts +0 -1493
  122. package/deepccc-agent/src/index.ts +0 -710
  123. package/deepccc-agent/src/permissions.ts +0 -226
  124. package/deepccc-agent/src/privacy.ts +0 -141
  125. package/deepccc-agent/src/proc-tree-kill.ts +0 -61
  126. package/deepccc-agent/src/progress/cards-helpers.ts +0 -76
  127. package/deepccc-agent/src/progress/reducer.ts +0 -113
  128. package/deepccc-agent/src/progress/terminal-renderer.ts +0 -294
  129. package/deepccc-agent/src/progress/view.ts +0 -77
  130. package/deepccc-agent/src/raw-stream-log.ts +0 -124
  131. package/deepccc-agent/src/session-search.ts +0 -370
  132. package/deepccc-agent/src/session-select.ts +0 -48
  133. package/deepccc-agent/src/sigint.ts +0 -50
  134. package/deepccc-agent/src/skills.ts +0 -205
  135. package/deepccc-agent/src/web-tools.ts +0 -313
  136. package/deepccc-agent/tsconfig.build.json +0 -13
  137. package/deepccc-agent/tsconfig.json +0 -13
  138. package/deepccc-agent/vitest.config.ts +0 -7
  139. package/src/__tests__/adapter-interface.test.ts +0 -152
  140. package/src/__tests__/agent-activity.test.ts +0 -86
  141. package/src/__tests__/agent-delegate-task-rpc.test.ts +0 -165
  142. package/src/__tests__/agent-image-rpc.test.ts +0 -34
  143. package/src/__tests__/agent-platform-routing.test.ts +0 -26
  144. package/src/__tests__/agent-reload-config-rpc.test.ts +0 -99
  145. package/src/__tests__/agent-rpc-body.test.ts +0 -42
  146. package/src/__tests__/builtin-chat-session.test.ts +0 -532
  147. package/src/__tests__/builtin-cli-json.test.ts +0 -39
  148. package/src/__tests__/builtin-config.test.ts +0 -26
  149. package/src/__tests__/builtin-context.test.ts +0 -319
  150. package/src/__tests__/builtin-file-tools.test.ts +0 -240
  151. package/src/__tests__/builtin-permissions.test.ts +0 -219
  152. package/src/__tests__/builtin-session-search.test.ts +0 -262
  153. package/src/__tests__/builtin-session-select.test.ts +0 -116
  154. package/src/__tests__/builtin-sigint.test.ts +0 -56
  155. package/src/__tests__/builtin-skills.test.ts +0 -284
  156. package/src/__tests__/builtin-web-tools.test.ts +0 -220
  157. package/src/__tests__/card-action-routing.test.ts +0 -18
  158. package/src/__tests__/card-plain-text.test.ts +0 -45
  159. package/src/__tests__/cardkit.test.ts +0 -60
  160. package/src/__tests__/cards.test.ts +0 -607
  161. package/src/__tests__/ccc-adapter.test.ts +0 -194
  162. package/src/__tests__/chatgpt-subscription-rpc.test.ts +0 -89
  163. package/src/__tests__/chatgpt-subscription.test.ts +0 -135
  164. package/src/__tests__/chrome-devtools-guard.test.ts +0 -165
  165. package/src/__tests__/claude-adapter.test.ts +0 -614
  166. package/src/__tests__/claude-raw-stream-log.test.ts +0 -96
  167. package/src/__tests__/claude-sdk-installer.test.ts +0 -285
  168. package/src/__tests__/codex-adapter.test.ts +0 -331
  169. package/src/__tests__/codex-raw-stream-log.test.ts +0 -170
  170. package/src/__tests__/codex-reset-actions.test.ts +0 -146
  171. package/src/__tests__/config-reload.test.ts +0 -284
  172. package/src/__tests__/config-sample.test.ts +0 -97
  173. package/src/__tests__/config-utils.test.ts +0 -40
  174. package/src/__tests__/config.test.ts +0 -395
  175. package/src/__tests__/crash-logging.test.ts +0 -360
  176. package/src/__tests__/cursor-adapter.test.ts +0 -890
  177. package/src/__tests__/cursor-session-meta-store.test.ts +0 -212
  178. package/src/__tests__/feishu-api.test.ts +0 -60
  179. package/src/__tests__/feishu-avatar.test.ts +0 -504
  180. package/src/__tests__/feishu-message-ingress.test.ts +0 -138
  181. package/src/__tests__/feishu-platform.test.ts +0 -75
  182. package/src/__tests__/fixtures/codex_simple_text.jsonl +0 -4
  183. package/src/__tests__/fixtures/codex_with_tool.jsonl +0 -6
  184. package/src/__tests__/fixtures/cursor_partial_only.jsonl +0 -5
  185. package/src/__tests__/fixtures/cursor_partial_with_final.jsonl +0 -13
  186. package/src/__tests__/fixtures/cursor_with_tool_call.jsonl +0 -12
  187. package/src/__tests__/format-message.test.ts +0 -316
  188. package/src/__tests__/git-command.test.ts +0 -288
  189. package/src/__tests__/im-skills.test.ts +0 -125
  190. package/src/__tests__/jsonl-stream.test.ts +0 -79
  191. package/src/__tests__/orchestrator.test.ts +0 -1268
  192. package/src/__tests__/package-files.test.ts +0 -24
  193. package/src/__tests__/platform-startup.test.ts +0 -19
  194. package/src/__tests__/privacy.test.ts +0 -198
  195. package/src/__tests__/proc-tree-kill.test.ts +0 -108
  196. package/src/__tests__/progress-reducer.test.ts +0 -121
  197. package/src/__tests__/raw-stream-log.test.ts +0 -106
  198. package/src/__tests__/response-stall.test.ts +0 -49
  199. package/src/__tests__/restart.test.ts +0 -232
  200. package/src/__tests__/session-ccc-config.test.ts +0 -66
  201. package/src/__tests__/session.test.ts +0 -3004
  202. package/src/__tests__/shared-prefix.test.ts +0 -36
  203. package/src/__tests__/sim-agent.test.ts +0 -174
  204. package/src/__tests__/sim-platform.test.ts +0 -93
  205. package/src/__tests__/sim-store.test.ts +0 -214
  206. package/src/__tests__/simplify.test.ts +0 -283
  207. package/src/__tests__/startup-lifecycle.test.ts +0 -231
  208. package/src/__tests__/stop-session.test.ts +0 -162
  209. package/src/__tests__/stream-state.test.ts +0 -164
  210. package/src/__tests__/terminal-error.test.ts +0 -54
  211. package/src/__tests__/terminal-renderer.test.ts +0 -247
  212. package/src/__tests__/update-command-guard.test.ts +0 -144
  213. package/src/__tests__/web-ui.test.ts +0 -438
  214. package/src/__tests__/wechat-platform.test.ts +0 -111
  215. package/src/adapters/adapter-interface.ts +0 -217
  216. package/src/adapters/ccc-adapter.ts +0 -150
  217. package/src/adapters/claude-adapter.ts +0 -673
  218. package/src/adapters/claude-session-meta-store.ts +0 -120
  219. package/src/adapters/codex-adapter.ts +0 -426
  220. package/src/adapters/codex-session-meta-store.ts +0 -131
  221. package/src/adapters/cursor-adapter.ts +0 -681
  222. package/src/adapters/cursor-session-meta-store.ts +0 -154
  223. package/src/adapters/jsonl-stream.ts +0 -157
  224. package/src/adapters/raw-stream-log.ts +0 -124
  225. package/src/adapters/resource-monitor.ts +0 -141
  226. package/src/agent-activity.ts +0 -175
  227. package/src/agent-delegate-task-rpc.ts +0 -153
  228. package/src/agent-delegate-task.ts +0 -91
  229. package/src/agent-file-rpc.ts +0 -172
  230. package/src/agent-image-rpc.ts +0 -168
  231. package/src/agent-platform-routing.ts +0 -28
  232. package/src/agent-reload-config-rpc.ts +0 -34
  233. package/src/agent-rpc-body.ts +0 -92
  234. package/src/agent-stop-stuck.ts +0 -129
  235. package/src/card-action-routing.ts +0 -14
  236. package/src/card-plain-text.ts +0 -108
  237. package/src/cardkit.ts +0 -179
  238. package/src/cards.ts +0 -684
  239. package/src/chatgpt-subscription-rpc.ts +0 -27
  240. package/src/chatgpt-subscription.ts +0 -299
  241. package/src/chrome-devtools-guard.ts +0 -318
  242. package/src/claude-sdk-installer.ts +0 -324
  243. package/src/codex-reset-actions.ts +0 -184
  244. package/src/config-utils.ts +0 -211
  245. package/src/config.ts +0 -1063
  246. package/src/cursor-usage.ts +0 -128
  247. package/src/exit-banner.ts +0 -33
  248. package/src/feishu-api.ts +0 -1616
  249. package/src/feishu-message-ingress.ts +0 -195
  250. package/src/feishu-platform.ts +0 -159
  251. package/src/format-message.ts +0 -293
  252. package/src/git-command.ts +0 -202
  253. package/src/im-skills.ts +0 -149
  254. package/src/index.ts +0 -1089
  255. package/src/litellm-proxy.ts +0 -374
  256. package/src/orchestrator.ts +0 -2543
  257. package/src/platform-adapter.ts +0 -70
  258. package/src/platform-startup.ts +0 -16
  259. package/src/privacy.ts +0 -118
  260. package/src/progress/reducer.ts +0 -113
  261. package/src/progress/terminal-renderer.ts +0 -294
  262. package/src/progress/view.ts +0 -77
  263. package/src/response-stall.ts +0 -28
  264. package/src/runtime-reload.ts +0 -34
  265. package/src/session-chat-binding.ts +0 -292
  266. package/src/session-name.ts +0 -8
  267. package/src/session.ts +0 -2659
  268. package/src/shared-prefix.ts +0 -29
  269. package/src/shared.ts +0 -552
  270. package/src/sim-agent.ts +0 -167
  271. package/src/sim-platform.ts +0 -177
  272. package/src/sim-store.ts +0 -317
  273. package/src/simplify.ts +0 -120
  274. package/src/startup-lifecycle.ts +0 -250
  275. package/src/stream-state.ts +0 -177
  276. package/src/terminal-error.ts +0 -129
  277. package/src/trace.ts +0 -51
  278. package/src/turn-cards.ts +0 -118
  279. package/src/update-command-guard.ts +0 -165
  280. 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
- import { spawn } from "node:child_process";
23
-
24
- /** 异步杀掉以 pid 为根的整棵进程树。
25
- *
26
- * 设计目标:永不抛错、永不阻塞调用者。
27
- * - pid 不存在、参数缺失 静默返回
28
- * - 子进程 spawn 失败 → console.warn 但不 reject
29
- * - Windows taskkill 异步执行,不阻塞 event loop
30
- *
31
- * 调用方约定:返回的 Promise 在 kill 命令发出后立即 resolve。
32
- * 真正的进程退出由 OS 异步完成,调用方如果需要确认"已死透",应自己再轮询
33
- * `process.kill(pid, 0)`。
34
- */
35
- export async function killProcessTree(pid: number | undefined): Promise<void> {
36
- if (pid == null || !Number.isFinite(pid) || pid <= 0) return;
37
- if (process.platform === "win32") {
38
- await killWindowsTree(pid);
39
- return;
40
- }
41
- await killPosixTree(pid);
42
- }
43
-
44
- // ---------------------------------------------------------------------------
45
- // Windows: taskkill /T /F
46
- // ---------------------------------------------------------------------------
47
-
48
- function killWindowsTree(pid: number): Promise<void> {
49
- return new Promise<void>((resolve) => {
50
- let resolved = false;
51
- const done = () => {
52
- if (resolved) return;
53
- resolved = true;
54
- resolve();
55
- };
56
- try {
57
- const proc = spawn("taskkill", ["/pid", String(pid), "/T", "/F"], {
58
- stdio: "ignore",
59
- windowsHide: true,
60
- // taskkill 本身很快(<200ms),不需要 detached
61
- });
62
- proc.once("error", (err) => {
63
- console.warn(`[killProcessTree] taskkill spawn error for pid=${pid}: ${(err as Error).message}`);
64
- done();
65
- });
66
- proc.once("close", () => { done(); });
67
- // 兜底超时:3 秒后强制 resolve,避免极端情况下 hang 住调用方
68
- setTimeout(done, 3000).unref();
69
- } catch (err) {
70
- console.warn(`[killProcessTree] taskkill failed for pid=${pid}: ${(err as Error).message}`);
71
- done();
72
- }
73
- });
74
- }
75
-
76
- // ---------------------------------------------------------------------------
77
- // POSIX: 优先按 process group 杀,回退到按 pid
78
- // ---------------------------------------------------------------------------
79
-
80
- async function killPosixTree(pid: number): Promise<void> {
81
- // 第一次尝试:按 process group 发 SIGTERM。要求 spawn 时 detached:true。
82
- trySignal(-pid, "SIGTERM");
83
- trySignal(pid, "SIGTERM");
84
- // 给进程 1 秒优雅退出机会
85
- await new Promise((r) => setTimeout(r, 1000));
86
- // 兜底:SIGKILL
87
- trySignal(-pid, "SIGKILL");
88
- trySignal(pid, "SIGKILL");
89
- }
90
-
91
- function trySignal(target: number, signal: NodeJS.Signals): void {
92
- try {
93
- process.kill(target, signal);
94
- } catch {
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
+ }