chatccc 0.2.252 → 0.2.254
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 +10 -1
- package/bin/cccagent.mjs +12 -3
- package/bin/chatccc.mjs +15 -6
- package/config.sample.json +2 -1
- package/deepccc-agent/README.md +40 -35
- 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 +81 -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 +601 -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/agent-team/application/board-service.js +142 -0
- package/dist/src/agent-team/application/main-agent-service.js +151 -0
- package/dist/src/agent-team/domain/board.js +69 -0
- package/dist/src/agent-team/http/board-routes.js +213 -0
- package/dist/src/agent-team/infrastructure/filesystem-browser.js +112 -0
- package/dist/src/agent-team/infrastructure/main-agent-session-runtime.js +30 -0
- package/dist/src/agent-team/main-agent-bootstrap.js +15 -0
- package/dist/src/agent-team/repositories/board-repository.js +10 -0
- package/dist/src/agent-team/repositories/feishu-p2p-contact-store.js +64 -0
- package/dist/src/agent-team/repositories/json-board-repository.js +209 -0
- package/dist/src/agent-team/repositories/main-agent-binding-repository.js +84 -0
- package/dist/src/agent-team/sync/board-sync-adapter.js +1 -0
- package/dist/src/agent-team/web/agent-team-page.js +219 -0
- package/dist/src/agent-tool.js +10 -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 +808 -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 +835 -0
- package/dist/src/litellm-proxy.js +300 -0
- package/dist/src/orchestrator.js +2096 -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 +2147 -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} +753 -839
- 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,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
|
+
}
|