chatccc 0.2.231 → 0.2.232

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 (127) hide show
  1. package/.agents/skills/create-chatccc-feishu-app/SKILL.md +85 -85
  2. package/.claude/skills/create-chatccc-feishu-app/SKILL.md +85 -85
  3. package/.cursor/skills/create-chatccc-feishu-app/SKILL.md +85 -85
  4. package/README.md +90 -90
  5. package/agent-prompts/claude_specific.md +45 -45
  6. package/agent-prompts/codex_specific.md +2 -2
  7. package/agent-prompts/cursor_specific.md +13 -13
  8. package/bin/cccagent.mjs +17 -17
  9. package/im-skills/feishu-skill/receive-send-file.md +63 -63
  10. package/im-skills/feishu-skill/receive-send-image.md +24 -24
  11. package/im-skills/feishu-skill/skill.md +3 -3
  12. package/im-skills/wechat-file-skill/receive-send-file.md +38 -38
  13. package/im-skills/wechat-file-skill/send-file.mjs +83 -83
  14. package/im-skills/wechat-file-skill/skill.md +10 -10
  15. package/im-skills/wechat-image-skill/skill.md +10 -10
  16. package/im-skills/wechat-video-skill/receive-send-video.md +38 -38
  17. package/im-skills/wechat-video-skill/send-video.mjs +79 -79
  18. package/im-skills/wechat-video-skill/skill.md +10 -10
  19. package/package.json +1 -1
  20. package/scripts/postinstall-sharp-check.mjs +58 -58
  21. package/src/__tests__/agent-activity.test.ts +86 -86
  22. package/src/__tests__/agent-delegate-task-rpc.test.ts +165 -165
  23. package/src/__tests__/agent-reload-config-rpc.test.ts +99 -99
  24. package/src/__tests__/builtin-chat-session.test.ts +449 -393
  25. package/src/__tests__/builtin-cli-json.test.ts +39 -39
  26. package/src/__tests__/builtin-config.test.ts +26 -26
  27. package/src/__tests__/builtin-context.test.ts +319 -202
  28. package/src/__tests__/builtin-file-tools.test.ts +240 -240
  29. package/src/__tests__/builtin-permissions.test.ts +211 -211
  30. package/src/__tests__/builtin-session-search.test.ts +228 -0
  31. package/src/__tests__/builtin-session-select.test.ts +116 -116
  32. package/src/__tests__/builtin-sigint.test.ts +56 -56
  33. package/src/__tests__/builtin-skills.test.ts +284 -284
  34. package/src/__tests__/builtin-web-tools.test.ts +220 -220
  35. package/src/__tests__/card-action-routing.test.ts +18 -18
  36. package/src/__tests__/cardkit.test.ts +60 -60
  37. package/src/__tests__/ccc-adapter.test.ts +170 -170
  38. package/src/__tests__/chatgpt-subscription-rpc.test.ts +89 -89
  39. package/src/__tests__/chatgpt-subscription.test.ts +135 -135
  40. package/src/__tests__/chrome-devtools-guard.test.ts +165 -165
  41. package/src/__tests__/claude-adapter.test.ts +614 -614
  42. package/src/__tests__/claude-raw-stream-log.test.ts +87 -87
  43. package/src/__tests__/codex-adapter.test.ts +58 -58
  44. package/src/__tests__/codex-raw-stream-log.test.ts +170 -170
  45. package/src/__tests__/codex-reset-actions.test.ts +146 -146
  46. package/src/__tests__/cursor-adapter.test.ts +268 -268
  47. package/src/__tests__/feishu-api.test.ts +60 -60
  48. package/src/__tests__/feishu-avatar.test.ts +164 -164
  49. package/src/__tests__/feishu-message-ingress.test.ts +138 -138
  50. package/src/__tests__/feishu-platform.test.ts +22 -22
  51. package/src/__tests__/format-message.test.ts +47 -47
  52. package/src/__tests__/jsonl-stream.test.ts +79 -79
  53. package/src/__tests__/package-files.test.ts +24 -24
  54. package/src/__tests__/privacy.test.ts +198 -198
  55. package/src/__tests__/progress-reducer.test.ts +121 -121
  56. package/src/__tests__/raw-stream-log.test.ts +106 -106
  57. package/src/__tests__/response-stall.test.ts +49 -49
  58. package/src/__tests__/restart.test.ts +132 -132
  59. package/src/__tests__/session.test.ts +199 -199
  60. package/src/__tests__/shared-prefix.test.ts +36 -36
  61. package/src/__tests__/sim-platform.test.ts +16 -16
  62. package/src/__tests__/startup-lifecycle.test.ts +231 -231
  63. package/src/__tests__/stop-session.test.ts +34 -34
  64. package/src/__tests__/stream-state.test.ts +42 -42
  65. package/src/__tests__/terminal-error.test.ts +54 -54
  66. package/src/__tests__/terminal-renderer.test.ts +247 -247
  67. package/src/__tests__/update-command-guard.test.ts +144 -144
  68. package/src/__tests__/web-ui.test.ts +326 -326
  69. package/src/adapters/adapter-interface.ts +33 -33
  70. package/src/adapters/ccc-adapter.ts +141 -141
  71. package/src/adapters/claude-adapter.ts +620 -620
  72. package/src/adapters/claude-session-meta-store.ts +120 -120
  73. package/src/adapters/codex-adapter.ts +426 -426
  74. package/src/adapters/cursor-adapter.ts +681 -681
  75. package/src/adapters/jsonl-stream.ts +157 -157
  76. package/src/adapters/raw-stream-log.ts +124 -124
  77. package/src/adapters/resource-monitor.ts +140 -140
  78. package/src/agent-activity.ts +175 -175
  79. package/src/agent-delegate-task-rpc.ts +153 -153
  80. package/src/agent-delegate-task.ts +91 -91
  81. package/src/agent-reload-config-rpc.ts +34 -34
  82. package/src/agent-stop-stuck.ts +129 -129
  83. package/src/builtin/cli.ts +6 -0
  84. package/src/builtin/config.ts +84 -84
  85. package/src/builtin/context.ts +465 -391
  86. package/src/builtin/file-log.ts +38 -38
  87. package/src/builtin/file-tools.ts +34 -0
  88. package/src/builtin/index.ts +24 -23
  89. package/src/builtin/proc-tree-kill.ts +61 -61
  90. package/src/builtin/progress/cards-helpers.ts +76 -76
  91. package/src/builtin/progress/reducer.ts +113 -113
  92. package/src/builtin/progress/terminal-renderer.ts +294 -294
  93. package/src/builtin/progress/view.ts +77 -77
  94. package/src/builtin/raw-stream-log.ts +124 -124
  95. package/src/builtin/session-search.ts +363 -0
  96. package/src/builtin/session-select.ts +48 -48
  97. package/src/builtin/sigint.ts +50 -50
  98. package/src/builtin/skills.ts +205 -205
  99. package/src/builtin/web-tools.ts +313 -313
  100. package/src/card-action-routing.ts +14 -14
  101. package/src/chatgpt-subscription-rpc.ts +27 -27
  102. package/src/chatgpt-subscription.ts +299 -299
  103. package/src/chrome-devtools-guard.ts +318 -318
  104. package/src/codex-reset-actions.ts +184 -184
  105. package/src/feishu-api.ts +193 -193
  106. package/src/feishu-message-ingress.ts +195 -195
  107. package/src/feishu-platform.ts +20 -20
  108. package/src/format-message.ts +293 -293
  109. package/src/litellm-proxy.ts +374 -374
  110. package/src/orchestrator.ts +2494 -2494
  111. package/src/platform-adapter.ts +6 -6
  112. package/src/privacy.ts +118 -118
  113. package/src/progress/reducer.ts +113 -113
  114. package/src/progress/terminal-renderer.ts +294 -294
  115. package/src/progress/view.ts +77 -77
  116. package/src/response-stall.ts +28 -28
  117. package/src/runtime-reload.ts +34 -34
  118. package/src/session-chat-binding.ts +82 -82
  119. package/src/session-name.ts +8 -8
  120. package/src/session.ts +201 -201
  121. package/src/shared-prefix.ts +29 -29
  122. package/src/sim-platform.ts +20 -20
  123. package/src/startup-lifecycle.ts +250 -250
  124. package/src/stream-state.ts +21 -21
  125. package/src/terminal-error.ts +129 -129
  126. package/src/turn-cards.ts +117 -117
  127. package/src/update-command-guard.ts +165 -165
@@ -1,129 +1,129 @@
1
- /** 用户可见的终态错误。这里只保存脱敏摘要;完整原始异常继续只写运行日志。 */
2
- export type TerminalErrorKind =
3
- | "network_timeout"
4
- | "network"
5
- | "authentication"
6
- | "rate_limit"
7
- | "provider"
8
- | "process"
9
- | "resource"
10
- | "unknown";
11
-
12
- export interface TerminalErrorInfo {
13
- kind: TerminalErrorKind;
14
- title: string;
15
- message: string;
16
- occurredAt: number;
17
- }
18
-
19
- const MAX_UNKNOWN_ERROR_LENGTH = 300;
20
-
21
- function errorMessage(error: unknown): string {
22
- if (error instanceof Error) return error.message;
23
- if (typeof error === "string") return error;
24
- try {
25
- return JSON.stringify(error);
26
- } catch {
27
- return String(error);
28
- }
29
- }
30
-
31
- /**
32
- * 未分类错误仍应给出可诊断线索,但不能把常见凭据带进群聊或持久化状态。
33
- * 已分类错误使用固定文案,不复述地址、请求体或 provider 原始响应。
34
- */
35
- export function sanitizeTerminalErrorDetail(raw: string): string {
36
- return raw
37
- .replace(/\bBearer\s+[^\s,;]+/gi, "Bearer [已脱敏]")
38
- .replace(/\b(api[_-]?key|access[_-]?token|token|secret)\s*[:=]\s*[^\s,;]+/gi, "$1=[已脱敏]")
39
- .replace(/\bsk-[A-Za-z0-9._-]{6,}\b/g, "sk-[已脱敏]")
40
- .replace(/([?&](?:api[_-]?key|access[_-]?token|token|secret)=)[^&\s]+/gi, "$1[已脱敏]")
41
- .slice(0, MAX_UNKNOWN_ERROR_LENGTH);
42
- }
43
-
44
- function parsePositiveInt(message: string, pattern: RegExp): number | undefined {
45
- const value = Number.parseInt(message.match(pattern)?.[1] ?? "", 10);
46
- return Number.isFinite(value) && value > 0 ? value : undefined;
47
- }
48
-
49
- function formatSeconds(milliseconds: number): string {
50
- if (milliseconds % 1000 === 0) return `${milliseconds / 1000} 秒`;
51
- return `${milliseconds} 毫秒`;
52
- }
53
-
54
- export function classifyTerminalError(error: unknown, occurredAt = Date.now()): TerminalErrorInfo {
55
- const raw = errorMessage(error);
56
- const lower = raw.toLowerCase();
57
- const attempts = parsePositiveInt(raw, /\bafter\s+(\d+)\s+attempts?\b/i);
58
- const timeoutMs = parsePositiveInt(raw, /\btimeout\s*:\s*(\d+)\s*ms\b/i);
59
-
60
- if (/\b429\b|rate[ _-]?limit|too many requests/.test(lower)) {
61
- return {
62
- kind: "rate_limit",
63
- title: "请求受到限流",
64
- message: "模型服务请求受到限流,请稍后重试。",
65
- occurredAt,
66
- };
67
- }
68
-
69
- if (/\b401\b|\b403\b|unauthori[sz]ed|forbidden|invalid api key|authentication/.test(lower)) {
70
- return {
71
- kind: "authentication",
72
- title: "模型服务鉴权失败",
73
- message: "模型服务拒绝了当前凭据,请检查 API Key、账号权限或凭据是否过期。",
74
- occurredAt,
75
- };
76
- }
77
-
78
- if (
79
- /connect timeout|connection timed out|etimedout|und_err_connect_timeout/.test(lower)
80
- || (lower.includes("cannot connect") && lower.includes("timeout"))
81
- ) {
82
- const retryText = attempts ? `,已重试 ${attempts} 次` : "";
83
- const timeoutText = timeoutMs ? `,单次连接等待 ${formatSeconds(timeoutMs)}` : "";
84
- return {
85
- kind: "network_timeout",
86
- title: "网络连接超时",
87
- message: `连接模型服务失败${retryText}${timeoutText}。请检查网络、VPN或模型服务状态后重试。`,
88
- occurredAt,
89
- };
90
- }
91
-
92
- if (/econnrefused|econnreset|enotfound|eai_again|socket hang up|network error|cannot connect/.test(lower)) {
93
- return {
94
- kind: "network",
95
- title: "无法连接模型服务",
96
- message: "与模型服务的网络连接失败,请检查网络、VPN、DNS或服务状态后重试。",
97
- occurredAt,
98
- };
99
- }
100
-
101
- const httpStatus = raw.match(/\b(?:HTTP\s*)?(5\d\d)\b/i)?.[1];
102
- if (httpStatus || /service unavailable|bad gateway|gateway timeout|provider error/.test(lower)) {
103
- return {
104
- kind: "provider",
105
- title: "模型服务暂时不可用",
106
- message: `模型服务返回异常${httpStatus ? `(HTTP ${httpStatus})` : ""},请稍后重试。`,
107
- occurredAt,
108
- };
109
- }
110
-
111
- const safeDetail = sanitizeTerminalErrorDetail(raw).trim() || "未提供错误详情";
112
- return {
113
- kind: "unknown",
114
- title: "Agent 执行错误",
115
- message: `发生未分类错误:${safeDetail}`,
116
- occurredAt,
117
- };
118
- }
119
-
120
- export function formatTerminalErrorReason(error: TerminalErrorInfo): string {
121
- return `⚠️ 异常结束:${error.title}\n${error.message}`;
122
- }
123
-
124
- export function formatTerminalErrorNotice(error: TerminalErrorInfo, finalReply = ""): string {
125
- const reason = formatTerminalErrorReason(error);
126
- return finalReply.trim()
127
- ? `${reason}\n\n以下回复可能不完整:\n\n${finalReply.trim()}`
128
- : reason;
129
- }
1
+ /** 用户可见的终态错误。这里只保存脱敏摘要;完整原始异常继续只写运行日志。 */
2
+ export type TerminalErrorKind =
3
+ | "network_timeout"
4
+ | "network"
5
+ | "authentication"
6
+ | "rate_limit"
7
+ | "provider"
8
+ | "process"
9
+ | "resource"
10
+ | "unknown";
11
+
12
+ export interface TerminalErrorInfo {
13
+ kind: TerminalErrorKind;
14
+ title: string;
15
+ message: string;
16
+ occurredAt: number;
17
+ }
18
+
19
+ const MAX_UNKNOWN_ERROR_LENGTH = 300;
20
+
21
+ function errorMessage(error: unknown): string {
22
+ if (error instanceof Error) return error.message;
23
+ if (typeof error === "string") return error;
24
+ try {
25
+ return JSON.stringify(error);
26
+ } catch {
27
+ return String(error);
28
+ }
29
+ }
30
+
31
+ /**
32
+ * 未分类错误仍应给出可诊断线索,但不能把常见凭据带进群聊或持久化状态。
33
+ * 已分类错误使用固定文案,不复述地址、请求体或 provider 原始响应。
34
+ */
35
+ export function sanitizeTerminalErrorDetail(raw: string): string {
36
+ return raw
37
+ .replace(/\bBearer\s+[^\s,;]+/gi, "Bearer [已脱敏]")
38
+ .replace(/\b(api[_-]?key|access[_-]?token|token|secret)\s*[:=]\s*[^\s,;]+/gi, "$1=[已脱敏]")
39
+ .replace(/\bsk-[A-Za-z0-9._-]{6,}\b/g, "sk-[已脱敏]")
40
+ .replace(/([?&](?:api[_-]?key|access[_-]?token|token|secret)=)[^&\s]+/gi, "$1[已脱敏]")
41
+ .slice(0, MAX_UNKNOWN_ERROR_LENGTH);
42
+ }
43
+
44
+ function parsePositiveInt(message: string, pattern: RegExp): number | undefined {
45
+ const value = Number.parseInt(message.match(pattern)?.[1] ?? "", 10);
46
+ return Number.isFinite(value) && value > 0 ? value : undefined;
47
+ }
48
+
49
+ function formatSeconds(milliseconds: number): string {
50
+ if (milliseconds % 1000 === 0) return `${milliseconds / 1000} 秒`;
51
+ return `${milliseconds} 毫秒`;
52
+ }
53
+
54
+ export function classifyTerminalError(error: unknown, occurredAt = Date.now()): TerminalErrorInfo {
55
+ const raw = errorMessage(error);
56
+ const lower = raw.toLowerCase();
57
+ const attempts = parsePositiveInt(raw, /\bafter\s+(\d+)\s+attempts?\b/i);
58
+ const timeoutMs = parsePositiveInt(raw, /\btimeout\s*:\s*(\d+)\s*ms\b/i);
59
+
60
+ if (/\b429\b|rate[ _-]?limit|too many requests/.test(lower)) {
61
+ return {
62
+ kind: "rate_limit",
63
+ title: "请求受到限流",
64
+ message: "模型服务请求受到限流,请稍后重试。",
65
+ occurredAt,
66
+ };
67
+ }
68
+
69
+ if (/\b401\b|\b403\b|unauthori[sz]ed|forbidden|invalid api key|authentication/.test(lower)) {
70
+ return {
71
+ kind: "authentication",
72
+ title: "模型服务鉴权失败",
73
+ message: "模型服务拒绝了当前凭据,请检查 API Key、账号权限或凭据是否过期。",
74
+ occurredAt,
75
+ };
76
+ }
77
+
78
+ if (
79
+ /connect timeout|connection timed out|etimedout|und_err_connect_timeout/.test(lower)
80
+ || (lower.includes("cannot connect") && lower.includes("timeout"))
81
+ ) {
82
+ const retryText = attempts ? `,已重试 ${attempts} 次` : "";
83
+ const timeoutText = timeoutMs ? `,单次连接等待 ${formatSeconds(timeoutMs)}` : "";
84
+ return {
85
+ kind: "network_timeout",
86
+ title: "网络连接超时",
87
+ message: `连接模型服务失败${retryText}${timeoutText}。请检查网络、VPN或模型服务状态后重试。`,
88
+ occurredAt,
89
+ };
90
+ }
91
+
92
+ if (/econnrefused|econnreset|enotfound|eai_again|socket hang up|network error|cannot connect/.test(lower)) {
93
+ return {
94
+ kind: "network",
95
+ title: "无法连接模型服务",
96
+ message: "与模型服务的网络连接失败,请检查网络、VPN、DNS或服务状态后重试。",
97
+ occurredAt,
98
+ };
99
+ }
100
+
101
+ const httpStatus = raw.match(/\b(?:HTTP\s*)?(5\d\d)\b/i)?.[1];
102
+ if (httpStatus || /service unavailable|bad gateway|gateway timeout|provider error/.test(lower)) {
103
+ return {
104
+ kind: "provider",
105
+ title: "模型服务暂时不可用",
106
+ message: `模型服务返回异常${httpStatus ? `(HTTP ${httpStatus})` : ""},请稍后重试。`,
107
+ occurredAt,
108
+ };
109
+ }
110
+
111
+ const safeDetail = sanitizeTerminalErrorDetail(raw).trim() || "未提供错误详情";
112
+ return {
113
+ kind: "unknown",
114
+ title: "Agent 执行错误",
115
+ message: `发生未分类错误:${safeDetail}`,
116
+ occurredAt,
117
+ };
118
+ }
119
+
120
+ export function formatTerminalErrorReason(error: TerminalErrorInfo): string {
121
+ return `⚠️ 异常结束:${error.title}\n${error.message}`;
122
+ }
123
+
124
+ export function formatTerminalErrorNotice(error: TerminalErrorInfo, finalReply = ""): string {
125
+ const reason = formatTerminalErrorReason(error);
126
+ return finalReply.trim()
127
+ ? `${reason}\n\n以下回复可能不完整:\n\n${finalReply.trim()}`
128
+ : reason;
129
+ }
package/src/turn-cards.ts CHANGED
@@ -1,118 +1,118 @@
1
- import { readFile, writeFile, mkdir } from "node:fs/promises";
2
- import { dirname, join } from "node:path";
3
-
4
- import { USER_DATA_DIR, ts } from "./config.ts";
5
-
6
- // ---------------------------------------------------------------------------
7
- // turn-cards.json — 每个 session 的卡片归属持久化
8
- // 记录每个 turn 有哪些卡片(轮转会产生多张),以及每张卡片的状态
9
- // ---------------------------------------------------------------------------
10
-
11
- export const TURNS_DIR = join(USER_DATA_DIR, "state", "turns");
12
-
13
- export interface TurnCardEntry {
14
- cardId: string;
15
- status: "active" | "done" | "stopped";
16
- createdAt: number;
17
- }
18
-
19
- export interface TurnCardsFile {
20
- sessionId: string;
21
- turns: { turnCount: number; cards: TurnCardEntry[] }[];
22
- }
23
-
24
- function getTurnCardsPath(sessionId: string): string {
25
- return join(TURNS_DIR, `${sessionId}.json`);
26
- }
27
-
28
- export function createEmptyTurnCards(sessionId: string): TurnCardsFile {
29
- return { sessionId, turns: [] };
30
- }
31
-
32
- export async function readTurnCards(sessionId: string): Promise<TurnCardsFile | null> {
33
- try {
34
- const raw = await readFile(getTurnCardsPath(sessionId), "utf-8");
35
- const parsed = JSON.parse(raw) as TurnCardsFile;
36
- if (parsed && typeof parsed.sessionId === "string" && Array.isArray(parsed.turns)) {
37
- return parsed;
38
- }
39
- return null;
40
- } catch {
41
- return null;
42
- }
43
- }
44
-
45
- export async function writeTurnCards(sessionId: string, data: TurnCardsFile): Promise<void> {
46
- try {
47
- const filePath = getTurnCardsPath(sessionId);
48
- await mkdir(dirname(filePath), { recursive: true });
49
- await writeFile(filePath, JSON.stringify(data, null, 2), "utf-8");
50
- } catch (err) {
51
- console.error(`[${ts()}] Failed to write turn-cards for ${sessionId}: ${(err as Error).message}`);
52
- }
53
- }
54
-
55
- /** 向指定 turn 追加一张卡片并持久化。若 turn 不存在则创建。 */
56
- export async function addCardToTurn(
57
- sessionId: string,
58
- turnCount: number,
59
- cardId: string,
60
- ): Promise<void> {
61
- const data = (await readTurnCards(sessionId)) ?? createEmptyTurnCards(sessionId);
62
- let turn = data.turns.find(t => t.turnCount === turnCount);
63
- if (!turn) {
64
- turn = { turnCount, cards: [] };
65
- data.turns.push(turn);
66
- }
67
- turn.cards.push({ cardId, status: "active", createdAt: Date.now() });
68
- await writeTurnCards(sessionId, data);
69
- }
70
-
71
- /** 将指定 turn 的所有 active 卡片标记为终态并持久化。返回被标记的 cardId 列表。 */
72
- export async function finalizeTurnCards(
73
- sessionId: string,
74
- turnCount: number,
75
- finalStatus: "done" | "stopped",
76
- ): Promise<string[]> {
77
- const data = await readTurnCards(sessionId);
78
- if (!data) return [];
79
- const turn = data.turns.find(t => t.turnCount === turnCount);
80
- if (!turn) return [];
81
- const finalized: string[] = [];
82
- for (const card of turn.cards) {
83
- if (card.status === "active") {
84
- card.status = finalStatus;
85
- finalized.push(card.cardId);
86
- }
87
- }
88
- if (finalized.length > 0) {
89
- await writeTurnCards(sessionId, data);
90
- }
91
- return finalized;
92
- }
93
-
94
- /** 将指定 turn 中的一张 active 卡片标记为 done(轮转时用),并持久化。 */
95
- export async function markCardDone(
96
- sessionId: string,
97
- turnCount: number,
98
- cardId: string,
99
- ): Promise<void> {
100
- const data = await readTurnCards(sessionId);
101
- if (!data) return;
102
- const turn = data.turns.find(t => t.turnCount === turnCount);
103
- if (!turn) return;
104
- const card = turn.cards.find(c => c.cardId === cardId && c.status === "active");
105
- if (!card) return;
106
- card.status = "done";
107
- await writeTurnCards(sessionId, data);
108
- }
109
-
110
- /** 清理 session 的 turn-cards 文件 */
111
- export async function removeTurnCards(sessionId: string): Promise<void> {
112
- try {
113
- const { unlink } = await import("node:fs/promises");
114
- await unlink(getTurnCardsPath(sessionId));
115
- } catch {
116
- // 文件不存在,忽略
117
- }
1
+ import { readFile, writeFile, mkdir } from "node:fs/promises";
2
+ import { dirname, join } from "node:path";
3
+
4
+ import { USER_DATA_DIR, ts } from "./config.ts";
5
+
6
+ // ---------------------------------------------------------------------------
7
+ // turn-cards.json — 每个 session 的卡片归属持久化
8
+ // 记录每个 turn 有哪些卡片(轮转会产生多张),以及每张卡片的状态
9
+ // ---------------------------------------------------------------------------
10
+
11
+ export const TURNS_DIR = join(USER_DATA_DIR, "state", "turns");
12
+
13
+ export interface TurnCardEntry {
14
+ cardId: string;
15
+ status: "active" | "done" | "stopped";
16
+ createdAt: number;
17
+ }
18
+
19
+ export interface TurnCardsFile {
20
+ sessionId: string;
21
+ turns: { turnCount: number; cards: TurnCardEntry[] }[];
22
+ }
23
+
24
+ function getTurnCardsPath(sessionId: string): string {
25
+ return join(TURNS_DIR, `${sessionId}.json`);
26
+ }
27
+
28
+ export function createEmptyTurnCards(sessionId: string): TurnCardsFile {
29
+ return { sessionId, turns: [] };
30
+ }
31
+
32
+ export async function readTurnCards(sessionId: string): Promise<TurnCardsFile | null> {
33
+ try {
34
+ const raw = await readFile(getTurnCardsPath(sessionId), "utf-8");
35
+ const parsed = JSON.parse(raw) as TurnCardsFile;
36
+ if (parsed && typeof parsed.sessionId === "string" && Array.isArray(parsed.turns)) {
37
+ return parsed;
38
+ }
39
+ return null;
40
+ } catch {
41
+ return null;
42
+ }
43
+ }
44
+
45
+ export async function writeTurnCards(sessionId: string, data: TurnCardsFile): Promise<void> {
46
+ try {
47
+ const filePath = getTurnCardsPath(sessionId);
48
+ await mkdir(dirname(filePath), { recursive: true });
49
+ await writeFile(filePath, JSON.stringify(data, null, 2), "utf-8");
50
+ } catch (err) {
51
+ console.error(`[${ts()}] Failed to write turn-cards for ${sessionId}: ${(err as Error).message}`);
52
+ }
53
+ }
54
+
55
+ /** 向指定 turn 追加一张卡片并持久化。若 turn 不存在则创建。 */
56
+ export async function addCardToTurn(
57
+ sessionId: string,
58
+ turnCount: number,
59
+ cardId: string,
60
+ ): Promise<void> {
61
+ const data = (await readTurnCards(sessionId)) ?? createEmptyTurnCards(sessionId);
62
+ let turn = data.turns.find(t => t.turnCount === turnCount);
63
+ if (!turn) {
64
+ turn = { turnCount, cards: [] };
65
+ data.turns.push(turn);
66
+ }
67
+ turn.cards.push({ cardId, status: "active", createdAt: Date.now() });
68
+ await writeTurnCards(sessionId, data);
69
+ }
70
+
71
+ /** 将指定 turn 的所有 active 卡片标记为终态并持久化。返回被标记的 cardId 列表。 */
72
+ export async function finalizeTurnCards(
73
+ sessionId: string,
74
+ turnCount: number,
75
+ finalStatus: "done" | "stopped",
76
+ ): Promise<string[]> {
77
+ const data = await readTurnCards(sessionId);
78
+ if (!data) return [];
79
+ const turn = data.turns.find(t => t.turnCount === turnCount);
80
+ if (!turn) return [];
81
+ const finalized: string[] = [];
82
+ for (const card of turn.cards) {
83
+ if (card.status === "active") {
84
+ card.status = finalStatus;
85
+ finalized.push(card.cardId);
86
+ }
87
+ }
88
+ if (finalized.length > 0) {
89
+ await writeTurnCards(sessionId, data);
90
+ }
91
+ return finalized;
92
+ }
93
+
94
+ /** 将指定 turn 中的一张 active 卡片标记为 done(轮转时用),并持久化。 */
95
+ export async function markCardDone(
96
+ sessionId: string,
97
+ turnCount: number,
98
+ cardId: string,
99
+ ): Promise<void> {
100
+ const data = await readTurnCards(sessionId);
101
+ if (!data) return;
102
+ const turn = data.turns.find(t => t.turnCount === turnCount);
103
+ if (!turn) return;
104
+ const card = turn.cards.find(c => c.cardId === cardId && c.status === "active");
105
+ if (!card) return;
106
+ card.status = "done";
107
+ await writeTurnCards(sessionId, data);
108
+ }
109
+
110
+ /** 清理 session 的 turn-cards 文件 */
111
+ export async function removeTurnCards(sessionId: string): Promise<void> {
112
+ try {
113
+ const { unlink } = await import("node:fs/promises");
114
+ await unlink(getTurnCardsPath(sessionId));
115
+ } catch {
116
+ // 文件不存在,忽略
117
+ }
118
118
  }