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,141 @@
1
+ import { readFile, writeFile, mkdir, rename, unlink } from "node:fs/promises";
2
+ import { dirname, join } from "node:path";
3
+ import { USER_DATA_DIR, ts } from "./config.js";
4
+ import { createAgentActivityTracker } from "./agent-activity.js";
5
+ // ---------------------------------------------------------------------------
6
+ // stream-state.json — 每个 session 的流式输出持久化文件
7
+ // ---------------------------------------------------------------------------
8
+ export const STREAMS_DIR = join(USER_DATA_DIR, "state", "streams");
9
+ function getStreamStatePath(sessionId) {
10
+ return join(STREAMS_DIR, `${sessionId}.json`);
11
+ }
12
+ export async function readStreamState(sessionId) {
13
+ try {
14
+ const raw = await readFile(getStreamStatePath(sessionId), "utf-8");
15
+ const parsed = JSON.parse(raw);
16
+ if (parsed && typeof parsed.sessionId === "string")
17
+ return parsed;
18
+ return null;
19
+ }
20
+ catch {
21
+ return null;
22
+ }
23
+ }
24
+ // rename 的可注入实现——仅供测试模拟"Windows EPERM 降级"路径。
25
+ // 生产代码使用 node:fs/promises.rename。
26
+ let renameImpl = rename;
27
+ /**
28
+ * 仅供单测:注入自定义 rename 实现以验证降级分支。
29
+ * 调用方负责测试结束后用 _resetRenameForTest 还原。
30
+ */
31
+ export function _setRenameForTest(impl) {
32
+ renameImpl = impl;
33
+ }
34
+ export function _resetRenameForTest() {
35
+ renameImpl = rename;
36
+ }
37
+ /**
38
+ * 把 StreamState 持久化到磁盘——尽量原子地写。
39
+ *
40
+ * 为什么需要原子写:display loop 每 3 秒读一次 stream-state.json,而
41
+ * runAgentSession 每 2 秒写一次。如果用 `writeFile(filePath, ...)` 直接
42
+ * 覆盖,reader 在写入过程中读会拿到半截 JSON,JSON.parse 报错——虽然
43
+ * readStreamState 包了 try/catch 返回 null 不会崩,但相当于"丢一帧"。
44
+ *
45
+ * 实现:
46
+ * 1) 先把内容写到 `<filePath>.tmp`(reader 不会读 .tmp)
47
+ * 2) 再用 `rename` 把 .tmp 替换成正式文件——`rename` 在 POSIX 上是真原子
48
+ * 操作,reader 任何时刻读到的都是某个完整的旧/新版本
49
+ *
50
+ * Windows 兼容性:Windows 上 `rename` 偶尔会因为目标文件正被 reader 打开
51
+ * 抛 EPERM/EBUSY(实测罕见,但非零概率)。降级方案:直接 writeFile 覆盖
52
+ * 真文件,这一帧可能被 reader 读到半截,但 readStreamState 的 try/catch 兜底
53
+ * 会让 loop 跳过这一帧,2 秒后下次 write 大概率成功——比写盘失败丢数据好。
54
+ */
55
+ export async function writeStreamState(state) {
56
+ const filePath = getStreamStatePath(state.sessionId);
57
+ const payload = JSON.stringify(state, null, 2);
58
+ try {
59
+ await mkdir(dirname(filePath), { recursive: true });
60
+ const tmpPath = filePath + ".tmp";
61
+ await writeFile(tmpPath, payload, "utf-8");
62
+ try {
63
+ await renameImpl(tmpPath, filePath);
64
+ }
65
+ catch (renameErr) {
66
+ // Windows 偶发 EPERM/EBUSY:rename 失败时降级为直接覆盖写。
67
+ // 此次写入失去原子性(reader 可能读到半截 JSON,但 readStreamState
68
+ // 的 try/catch 会兜底返回 null,只是丢一帧 display 更新)。
69
+ console.warn(`[${ts()}] [STREAM-STATE] rename failed for ${state.sessionId}, ` +
70
+ `fallback to overwrite: ${renameErr.message}`);
71
+ await writeFile(filePath, payload, "utf-8");
72
+ // 尽力清理 .tmp,失败也无所谓——下次 write 会覆盖它
73
+ await unlink(tmpPath).catch(() => { });
74
+ }
75
+ }
76
+ catch (err) {
77
+ console.error(`[${ts()}] Failed to write stream-state for ${state.sessionId}: ${err.message}`);
78
+ }
79
+ }
80
+ export function isFinalReplySentForTurn(state) {
81
+ return state.finalReplySentTurn === state.turnCount;
82
+ }
83
+ export async function markFinalReplySent(sessionId, turnCount, sentAt = Date.now()) {
84
+ const state = await readStreamState(sessionId);
85
+ if (!state)
86
+ return;
87
+ if (state.turnCount !== turnCount)
88
+ return;
89
+ if (state.status === "running")
90
+ return;
91
+ state.finalReplySentTurn = turnCount;
92
+ state.finalReplySentAt = sentAt;
93
+ state.updatedAt = Date.now();
94
+ await writeStreamState(state);
95
+ }
96
+ export function createEmptyStreamState(sessionId, cwd, tool, turnCount) {
97
+ const now = Date.now();
98
+ return {
99
+ sessionId,
100
+ status: "running",
101
+ accumulatedContent: "",
102
+ finalReply: "",
103
+ activity: createAgentActivityTracker(now).activity,
104
+ chunkCount: 0,
105
+ turnCount,
106
+ contextTokens: 0,
107
+ updatedAt: now,
108
+ cwd,
109
+ tool,
110
+ };
111
+ }
112
+ /** 进程重启时修正虚假的 "running" 状态 */
113
+ export async function fixStaleStreamStates() {
114
+ // 启动时将残留的 running 标记为 error
115
+ // 实际 agent 进程已不存在,下次 prompt 时会自然覆盖
116
+ try {
117
+ const { readdir } = await import("node:fs/promises");
118
+ let entries;
119
+ try {
120
+ entries = await readdir(STREAMS_DIR);
121
+ }
122
+ catch {
123
+ return;
124
+ }
125
+ for (const entry of entries) {
126
+ if (!entry.endsWith(".json"))
127
+ continue;
128
+ const state = await readStreamState(entry.replace(".json", ""));
129
+ if (state && state.status === "running") {
130
+ state.status = "error";
131
+ state.accumulatedContent += "\n\n⚠️ 进程重启,流式输出中断。";
132
+ state.updatedAt = Date.now();
133
+ await writeStreamState(state);
134
+ console.log(`[${ts()}] [STREAM-STATE] marked stale running as error: ${state.sessionId}`);
135
+ }
136
+ }
137
+ }
138
+ catch (err) {
139
+ console.error(`[${ts()}] [STREAM-STATE] fixStaleStreamStates failed: ${err.message}`);
140
+ }
141
+ }
@@ -0,0 +1,100 @@
1
+ const MAX_UNKNOWN_ERROR_LENGTH = 300;
2
+ function errorMessage(error) {
3
+ if (error instanceof Error)
4
+ return error.message;
5
+ if (typeof error === "string")
6
+ return error;
7
+ try {
8
+ return JSON.stringify(error);
9
+ }
10
+ catch {
11
+ return String(error);
12
+ }
13
+ }
14
+ /**
15
+ * 未分类错误仍应给出可诊断线索,但不能把常见凭据带进群聊或持久化状态。
16
+ * 已分类错误使用固定文案,不复述地址、请求体或 provider 原始响应。
17
+ */
18
+ export function sanitizeTerminalErrorDetail(raw) {
19
+ return raw
20
+ .replace(/\bBearer\s+[^\s,;]+/gi, "Bearer [已脱敏]")
21
+ .replace(/\b(api[_-]?key|access[_-]?token|token|secret)\s*[:=]\s*[^\s,;]+/gi, "$1=[已脱敏]")
22
+ .replace(/\bsk-[A-Za-z0-9._-]{6,}\b/g, "sk-[已脱敏]")
23
+ .replace(/([?&](?:api[_-]?key|access[_-]?token|token|secret)=)[^&\s]+/gi, "$1[已脱敏]")
24
+ .slice(0, MAX_UNKNOWN_ERROR_LENGTH);
25
+ }
26
+ function parsePositiveInt(message, pattern) {
27
+ const value = Number.parseInt(message.match(pattern)?.[1] ?? "", 10);
28
+ return Number.isFinite(value) && value > 0 ? value : undefined;
29
+ }
30
+ function formatSeconds(milliseconds) {
31
+ if (milliseconds % 1000 === 0)
32
+ return `${milliseconds / 1000} 秒`;
33
+ return `${milliseconds} 毫秒`;
34
+ }
35
+ export function classifyTerminalError(error, occurredAt = Date.now()) {
36
+ const raw = errorMessage(error);
37
+ const lower = raw.toLowerCase();
38
+ const attempts = parsePositiveInt(raw, /\bafter\s+(\d+)\s+attempts?\b/i);
39
+ const timeoutMs = parsePositiveInt(raw, /\btimeout\s*:\s*(\d+)\s*ms\b/i);
40
+ if (/\b429\b|rate[ _-]?limit|too many requests/.test(lower)) {
41
+ return {
42
+ kind: "rate_limit",
43
+ title: "请求受到限流",
44
+ message: "模型服务请求受到限流,请稍后重试。",
45
+ occurredAt,
46
+ };
47
+ }
48
+ if (/\b401\b|\b403\b|unauthori[sz]ed|forbidden|invalid api key|authentication/.test(lower)) {
49
+ return {
50
+ kind: "authentication",
51
+ title: "模型服务鉴权失败",
52
+ message: "模型服务拒绝了当前凭据,请检查 API Key、账号权限或凭据是否过期。",
53
+ occurredAt,
54
+ };
55
+ }
56
+ if (/connect timeout|connection timed out|etimedout|und_err_connect_timeout/.test(lower)
57
+ || (lower.includes("cannot connect") && lower.includes("timeout"))) {
58
+ const retryText = attempts ? `,已重试 ${attempts} 次` : "";
59
+ const timeoutText = timeoutMs ? `,单次连接等待 ${formatSeconds(timeoutMs)}` : "";
60
+ return {
61
+ kind: "network_timeout",
62
+ title: "网络连接超时",
63
+ message: `连接模型服务失败${retryText}${timeoutText}。请检查网络、VPN或模型服务状态后重试。`,
64
+ occurredAt,
65
+ };
66
+ }
67
+ if (/econnrefused|econnreset|enotfound|eai_again|socket hang up|network error|cannot connect/.test(lower)) {
68
+ return {
69
+ kind: "network",
70
+ title: "无法连接模型服务",
71
+ message: "与模型服务的网络连接失败,请检查网络、VPN、DNS或服务状态后重试。",
72
+ occurredAt,
73
+ };
74
+ }
75
+ const httpStatus = raw.match(/\b(?:HTTP\s*)?(5\d\d)\b/i)?.[1];
76
+ if (httpStatus || /service unavailable|bad gateway|gateway timeout|provider error/.test(lower)) {
77
+ return {
78
+ kind: "provider",
79
+ title: "模型服务暂时不可用",
80
+ message: `模型服务返回异常${httpStatus ? `(HTTP ${httpStatus})` : ""},请稍后重试。`,
81
+ occurredAt,
82
+ };
83
+ }
84
+ const safeDetail = sanitizeTerminalErrorDetail(raw).trim() || "未提供错误详情";
85
+ return {
86
+ kind: "unknown",
87
+ title: "Agent 执行错误",
88
+ message: `发生未分类错误:${safeDetail}`,
89
+ occurredAt,
90
+ };
91
+ }
92
+ export function formatTerminalErrorReason(error) {
93
+ return `⚠️ 异常结束:${error.title}\n${error.message}`;
94
+ }
95
+ export function formatTerminalErrorNotice(error, finalReply = "") {
96
+ const reason = formatTerminalErrorReason(error);
97
+ return finalReply.trim()
98
+ ? `${reason}\n\n以下回复可能不完整:\n\n${finalReply.trim()}`
99
+ : reason;
100
+ }
@@ -0,0 +1,50 @@
1
+ /**
2
+ * 轻量级消息全链路 trace 工具。
3
+ *
4
+ * 每条消息生成唯一 traceId,在关键决策点、API 调用、消息发送时输出结构化日志。
5
+ * grep traceId 即可看到该消息的完整 RECV → BRANCH → SEND → DONE 链路。
6
+ *
7
+ * 设计原则:trace 自身绝不能抛异常导致主流程中断——所有函数内都有 try/catch 兜底。
8
+ */
9
+ let _traceSeq = 0;
10
+ /** 基于时间戳 + 递增序号生成短 trace ID(约 10 字符) */
11
+ export function makeTraceId() {
12
+ try {
13
+ _traceSeq++;
14
+ const ts = Date.now().toString(36).slice(-6);
15
+ const seq = _traceSeq.toString(36).padStart(3, "0");
16
+ return `${ts}${seq}`;
17
+ }
18
+ catch {
19
+ // 极端情况(计数器溢出等)降级为纯时间戳
20
+ return Date.now().toString(36);
21
+ }
22
+ }
23
+ /**
24
+ * 输出一条 trace 日志。
25
+ * detail 中的值做浅层截断(字符串 > 200 字符会截断),避免日志膨胀。
26
+ */
27
+ export function logTrace(traceId, step, detail) {
28
+ try {
29
+ const safe = {};
30
+ if (detail) {
31
+ for (const [k, v] of Object.entries(detail)) {
32
+ if (typeof v === "string" && v.length > 200) {
33
+ safe[k] = v.slice(0, 200) + "...(truncated)";
34
+ }
35
+ else {
36
+ safe[k] = v;
37
+ }
38
+ }
39
+ }
40
+ const extra = Object.keys(safe).length > 0 ? " " + JSON.stringify(safe) : "";
41
+ console.log(`[TRACE ${traceId}] ${step}${extra}`);
42
+ }
43
+ catch {
44
+ // trace 自身绝不能抛异常
45
+ }
46
+ }
47
+ /** 重置序号(仅单测使用,确保 trace ID 可预测) */
48
+ export function _resetTraceSeqForTest() {
49
+ _traceSeq = 0;
50
+ }
@@ -0,0 +1,92 @@
1
+ import { readFile, writeFile, mkdir } from "node:fs/promises";
2
+ import { dirname, join } from "node:path";
3
+ import { USER_DATA_DIR, ts } from "./config.js";
4
+ // ---------------------------------------------------------------------------
5
+ // turn-cards.json — 每个 session 的卡片归属持久化
6
+ // 记录每个 turn 有哪些卡片(轮转会产生多张),以及每张卡片的状态
7
+ // ---------------------------------------------------------------------------
8
+ export const TURNS_DIR = join(USER_DATA_DIR, "state", "turns");
9
+ function getTurnCardsPath(sessionId) {
10
+ return join(TURNS_DIR, `${sessionId}.json`);
11
+ }
12
+ export function createEmptyTurnCards(sessionId) {
13
+ return { sessionId, turns: [] };
14
+ }
15
+ export async function readTurnCards(sessionId) {
16
+ try {
17
+ const raw = await readFile(getTurnCardsPath(sessionId), "utf-8");
18
+ const parsed = JSON.parse(raw);
19
+ if (parsed && typeof parsed.sessionId === "string" && Array.isArray(parsed.turns)) {
20
+ return parsed;
21
+ }
22
+ return null;
23
+ }
24
+ catch {
25
+ return null;
26
+ }
27
+ }
28
+ export async function writeTurnCards(sessionId, data) {
29
+ try {
30
+ const filePath = getTurnCardsPath(sessionId);
31
+ await mkdir(dirname(filePath), { recursive: true });
32
+ await writeFile(filePath, JSON.stringify(data, null, 2), "utf-8");
33
+ }
34
+ catch (err) {
35
+ console.error(`[${ts()}] Failed to write turn-cards for ${sessionId}: ${err.message}`);
36
+ }
37
+ }
38
+ /** 向指定 turn 追加一张卡片并持久化。若 turn 不存在则创建。 */
39
+ export async function addCardToTurn(sessionId, turnCount, cardId) {
40
+ const data = (await readTurnCards(sessionId)) ?? createEmptyTurnCards(sessionId);
41
+ let turn = data.turns.find(t => t.turnCount === turnCount);
42
+ if (!turn) {
43
+ turn = { turnCount, cards: [] };
44
+ data.turns.push(turn);
45
+ }
46
+ turn.cards.push({ cardId, status: "active", createdAt: Date.now() });
47
+ await writeTurnCards(sessionId, data);
48
+ }
49
+ /** 将指定 turn 的所有 active 卡片标记为终态并持久化。返回被标记的 cardId 列表。 */
50
+ export async function finalizeTurnCards(sessionId, turnCount, finalStatus) {
51
+ const data = await readTurnCards(sessionId);
52
+ if (!data)
53
+ return [];
54
+ const turn = data.turns.find(t => t.turnCount === turnCount);
55
+ if (!turn)
56
+ return [];
57
+ const finalized = [];
58
+ for (const card of turn.cards) {
59
+ if (card.status === "active") {
60
+ card.status = finalStatus;
61
+ finalized.push(card.cardId);
62
+ }
63
+ }
64
+ if (finalized.length > 0) {
65
+ await writeTurnCards(sessionId, data);
66
+ }
67
+ return finalized;
68
+ }
69
+ /** 将指定 turn 中的一张 active 卡片标记为 done(轮转时用),并持久化。 */
70
+ export async function markCardDone(sessionId, turnCount, cardId) {
71
+ const data = await readTurnCards(sessionId);
72
+ if (!data)
73
+ return;
74
+ const turn = data.turns.find(t => t.turnCount === turnCount);
75
+ if (!turn)
76
+ return;
77
+ const card = turn.cards.find(c => c.cardId === cardId && c.status === "active");
78
+ if (!card)
79
+ return;
80
+ card.status = "done";
81
+ await writeTurnCards(sessionId, data);
82
+ }
83
+ /** 清理 session 的 turn-cards 文件 */
84
+ export async function removeTurnCards(sessionId) {
85
+ try {
86
+ const { unlink } = await import("node:fs/promises");
87
+ await unlink(getTurnCardsPath(sessionId));
88
+ }
89
+ catch {
90
+ // 文件不存在,忽略
91
+ }
92
+ }
@@ -0,0 +1,114 @@
1
+ import { existsSync, mkdirSync, readFileSync, renameSync, rmSync, writeFileSync, } from "node:fs";
2
+ import { homedir } from "node:os";
3
+ import { dirname, join } from "node:path";
4
+ export const UPDATE_COMMAND_GUARD_FILE = join(homedir(), ".chatccc", "state", "update-command-guard.json");
5
+ const UPDATE_COMMAND_GUARD_VERSION = 1;
6
+ const DEFAULT_MAX_PROCESSED_IDS = 100;
7
+ function asRecord(value) {
8
+ return typeof value === "object" && value !== null
9
+ ? value
10
+ : undefined;
11
+ }
12
+ /** 从飞书事件信封中读取重投时保持不变的 event_id。 */
13
+ export function extractFeishuEventId(data) {
14
+ const envelope = asRecord(data);
15
+ const event = asRecord(envelope?.event);
16
+ const header = asRecord(envelope?.header) ?? asRecord(event?.header);
17
+ const context = asRecord(event?.context);
18
+ const candidates = [header?.event_id, envelope?.event_id, event?.event_id, context?.event_id];
19
+ for (const candidate of candidates) {
20
+ if (typeof candidate === "string" && candidate.trim())
21
+ return candidate.trim();
22
+ }
23
+ return undefined;
24
+ }
25
+ /** 分隔文字消息和卡片回调 ID 的命名空间。 */
26
+ export function buildUpdateCommandId(source, id) {
27
+ const normalized = id?.trim();
28
+ return normalized ? `${source}:${normalized}` : undefined;
29
+ }
30
+ function emptyState() {
31
+ return { version: UPDATE_COMMAND_GUARD_VERSION, processed: [] };
32
+ }
33
+ function parseState(raw, maxEntries) {
34
+ const parsed = JSON.parse(raw);
35
+ if (parsed.version !== UPDATE_COMMAND_GUARD_VERSION || !Array.isArray(parsed.processed)) {
36
+ throw new Error("invalid update command guard schema");
37
+ }
38
+ const processed = parsed.processed.map((entry) => {
39
+ if (typeof entry !== "object"
40
+ || entry === null
41
+ || typeof entry.id !== "string"
42
+ || entry.id.length === 0
43
+ || typeof entry.recordedAt !== "number"
44
+ || !Number.isFinite(entry.recordedAt)
45
+ || entry.recordedAt < 0) {
46
+ throw new Error("invalid processed update command entry");
47
+ }
48
+ return { id: entry.id, recordedAt: entry.recordedAt };
49
+ });
50
+ return {
51
+ version: UPDATE_COMMAND_GUARD_VERSION,
52
+ processed: processed.slice(-maxEntries),
53
+ };
54
+ }
55
+ function loadState(filePath, maxEntries, warn) {
56
+ if (!existsSync(filePath))
57
+ return emptyState();
58
+ try {
59
+ return parseState(readFileSync(filePath, "utf8"), maxEntries);
60
+ }
61
+ catch (err) {
62
+ warn(`[UPDATE-GUARD] 状态文件损坏,将重建 ${filePath}: ${err.message}`);
63
+ return emptyState();
64
+ }
65
+ }
66
+ /**
67
+ * 原子写入更新命令 ID。写失败时调用方必须拒绝更新:只有先落盘,
68
+ * 新进程才能识别飞书在旧进程退出后重投的同一条 `/update`。
69
+ */
70
+ function persistState(filePath, state) {
71
+ mkdirSync(dirname(filePath), { recursive: true });
72
+ const tempPath = `${filePath}.${process.pid}.${Date.now()}.tmp`;
73
+ try {
74
+ writeFileSync(tempPath, `${JSON.stringify(state, null, 2)}\n`, "utf8");
75
+ renameSync(tempPath, filePath);
76
+ }
77
+ catch (err) {
78
+ try {
79
+ rmSync(tempPath, { force: true });
80
+ }
81
+ catch { }
82
+ throw err;
83
+ }
84
+ }
85
+ /**
86
+ * 获取 `/update` 执行资格。只比较稳定消息/事件 ID,因此用户主动发送的
87
+ * 不同 `/update` 消息仍可立即执行。
88
+ */
89
+ export function acquireUpdateCommandGuard(options = {}) {
90
+ const filePath = options.filePath ?? UPDATE_COMMAND_GUARD_FILE;
91
+ const commandId = options.commandId?.trim() || undefined;
92
+ const now = options.now ?? Date.now();
93
+ const maxEntries = Number.isInteger(options.maxEntries) && (options.maxEntries ?? 0) > 0
94
+ ? options.maxEntries
95
+ : DEFAULT_MAX_PROCESSED_IDS;
96
+ const warn = options.warn ?? ((message) => console.warn(message));
97
+ // 模拟注入等没有稳定事件 ID 的入口无法做跨重启判断,保持原有行为。
98
+ if (!commandId)
99
+ return { allowed: true, reason: "missing_id" };
100
+ const state = loadState(filePath, maxEntries, warn);
101
+ if (state.processed.some((entry) => entry.id === commandId)) {
102
+ return { allowed: false, reason: "duplicate_id" };
103
+ }
104
+ state.processed.push({ id: commandId, recordedAt: now });
105
+ state.processed = state.processed.slice(-maxEntries);
106
+ try {
107
+ persistState(filePath, state);
108
+ }
109
+ catch (err) {
110
+ warn(`[UPDATE-GUARD] 无法写入状态文件 ${filePath}: ${err.message}`);
111
+ return { allowed: false, reason: "state_write_failed" };
112
+ }
113
+ return { allowed: true, reason: "accepted" };
114
+ }