chatccc 0.2.231 → 0.2.233

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 (130) 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__/crash-logging.test.ts +95 -1
  47. package/src/__tests__/cursor-adapter.test.ts +268 -268
  48. package/src/__tests__/feishu-api.test.ts +60 -60
  49. package/src/__tests__/feishu-avatar.test.ts +164 -164
  50. package/src/__tests__/feishu-message-ingress.test.ts +138 -138
  51. package/src/__tests__/feishu-platform.test.ts +22 -22
  52. package/src/__tests__/format-message.test.ts +47 -47
  53. package/src/__tests__/jsonl-stream.test.ts +79 -79
  54. package/src/__tests__/package-files.test.ts +24 -24
  55. package/src/__tests__/privacy.test.ts +198 -198
  56. package/src/__tests__/progress-reducer.test.ts +121 -121
  57. package/src/__tests__/raw-stream-log.test.ts +106 -106
  58. package/src/__tests__/response-stall.test.ts +49 -49
  59. package/src/__tests__/restart.test.ts +55 -10
  60. package/src/__tests__/session.test.ts +199 -199
  61. package/src/__tests__/shared-prefix.test.ts +36 -36
  62. package/src/__tests__/sim-platform.test.ts +16 -16
  63. package/src/__tests__/startup-lifecycle.test.ts +231 -231
  64. package/src/__tests__/stop-session.test.ts +34 -34
  65. package/src/__tests__/stream-state.test.ts +42 -42
  66. package/src/__tests__/terminal-error.test.ts +54 -54
  67. package/src/__tests__/terminal-renderer.test.ts +247 -247
  68. package/src/__tests__/update-command-guard.test.ts +144 -144
  69. package/src/__tests__/web-ui.test.ts +326 -326
  70. package/src/adapters/adapter-interface.ts +33 -33
  71. package/src/adapters/ccc-adapter.ts +141 -141
  72. package/src/adapters/claude-adapter.ts +620 -620
  73. package/src/adapters/claude-session-meta-store.ts +120 -120
  74. package/src/adapters/codex-adapter.ts +426 -426
  75. package/src/adapters/cursor-adapter.ts +681 -681
  76. package/src/adapters/jsonl-stream.ts +157 -157
  77. package/src/adapters/raw-stream-log.ts +124 -124
  78. package/src/adapters/resource-monitor.ts +140 -140
  79. package/src/agent-activity.ts +175 -175
  80. package/src/agent-delegate-task-rpc.ts +153 -153
  81. package/src/agent-delegate-task.ts +91 -91
  82. package/src/agent-reload-config-rpc.ts +34 -34
  83. package/src/agent-stop-stuck.ts +129 -129
  84. package/src/builtin/cli.ts +6 -0
  85. package/src/builtin/config.ts +84 -84
  86. package/src/builtin/context.ts +465 -391
  87. package/src/builtin/file-log.ts +38 -38
  88. package/src/builtin/file-tools.ts +34 -0
  89. package/src/builtin/index.ts +24 -23
  90. package/src/builtin/proc-tree-kill.ts +61 -61
  91. package/src/builtin/progress/cards-helpers.ts +76 -76
  92. package/src/builtin/progress/reducer.ts +113 -113
  93. package/src/builtin/progress/terminal-renderer.ts +294 -294
  94. package/src/builtin/progress/view.ts +77 -77
  95. package/src/builtin/raw-stream-log.ts +124 -124
  96. package/src/builtin/session-search.ts +363 -0
  97. package/src/builtin/session-select.ts +48 -48
  98. package/src/builtin/sigint.ts +50 -50
  99. package/src/builtin/skills.ts +205 -205
  100. package/src/builtin/web-tools.ts +313 -313
  101. package/src/card-action-routing.ts +14 -14
  102. package/src/chatgpt-subscription-rpc.ts +27 -27
  103. package/src/chatgpt-subscription.ts +299 -299
  104. package/src/chrome-devtools-guard.ts +318 -318
  105. package/src/codex-reset-actions.ts +184 -184
  106. package/src/feishu-api.ts +193 -193
  107. package/src/feishu-message-ingress.ts +195 -195
  108. package/src/feishu-platform.ts +20 -20
  109. package/src/format-message.ts +293 -293
  110. package/src/index.ts +6 -1
  111. package/src/litellm-proxy.ts +374 -374
  112. package/src/orchestrator.ts +36 -13
  113. package/src/platform-adapter.ts +6 -6
  114. package/src/privacy.ts +118 -118
  115. package/src/progress/reducer.ts +113 -113
  116. package/src/progress/terminal-renderer.ts +294 -294
  117. package/src/progress/view.ts +77 -77
  118. package/src/response-stall.ts +28 -28
  119. package/src/runtime-reload.ts +34 -34
  120. package/src/session-chat-binding.ts +82 -82
  121. package/src/session-name.ts +8 -8
  122. package/src/session.ts +201 -201
  123. package/src/shared-prefix.ts +29 -29
  124. package/src/shared.ts +44 -0
  125. package/src/sim-platform.ts +20 -20
  126. package/src/startup-lifecycle.ts +250 -250
  127. package/src/stream-state.ts +21 -21
  128. package/src/terminal-error.ts +129 -129
  129. package/src/turn-cards.ts +117 -117
  130. package/src/update-command-guard.ts +165 -165
@@ -1,10 +1,10 @@
1
1
  import { readFile, writeFile, mkdir, rename, unlink } from "node:fs/promises";
2
2
  import { dirname, join } from "node:path";
3
3
 
4
- import { USER_DATA_DIR, ts } from "./config.ts";
5
- import { createAgentActivityTracker } from "./agent-activity.ts";
6
- import type { AgentActivity } from "./agent-activity.ts";
7
- import type { TerminalErrorInfo } from "./terminal-error.ts";
4
+ import { USER_DATA_DIR, ts } from "./config.ts";
5
+ import { createAgentActivityTracker } from "./agent-activity.ts";
6
+ import type { AgentActivity } from "./agent-activity.ts";
7
+ import type { TerminalErrorInfo } from "./terminal-error.ts";
8
8
 
9
9
  // ---------------------------------------------------------------------------
10
10
  // stream-state.json — 每个 session 的流式输出持久化文件
@@ -14,14 +14,14 @@ export const STREAMS_DIR = join(USER_DATA_DIR, "state", "streams");
14
14
 
15
15
  export interface StreamState {
16
16
  sessionId: string;
17
- status: "running" | "done" | "stopped" | "error" | "auto_ended";
17
+ status: "running" | "done" | "stopped" | "error" | "auto_ended";
18
18
  accumulatedContent: string;
19
19
  /** 本轮会话中 LLM 输出的全部文本内容(所有 text block 的累加)。
20
20
  * 命名含 "final" 但实为"全部累积文本",并非仅"最终一段回复"。
21
21
  * 参见 session.ts 的 AccumulatorState 注释。 */
22
- finalReply: string;
23
- /** Current user-visible work phase for running progress cards. */
24
- activity?: AgentActivity;
22
+ finalReply: string;
23
+ /** Current user-visible work phase for running progress cards. */
24
+ activity?: AgentActivity;
25
25
  /** The turn whose terminal text reply has already been delivered to IM. */
26
26
  finalReplySentTurn?: number;
27
27
  finalReplySentAt?: number;
@@ -33,12 +33,12 @@ export interface StreamState {
33
33
  tool: string;
34
34
  /** Set by stop-stuck-loop to prevent the session from being resumed.
35
35
  * The orchestrator checks this before resuming and creates a new session instead. */
36
- stuckAt?: number;
37
- /** Set when the shared response watchdog ends a turn after three minutes without new characters. */
38
- autoEndedAt?: number;
39
- /** 脱敏后的用户可见根因;完整原始异常只保留在运行日志中。 */
40
- terminalError?: TerminalErrorInfo;
41
- }
36
+ stuckAt?: number;
37
+ /** Set when the shared response watchdog ends a turn after three minutes without new characters. */
38
+ autoEndedAt?: number;
39
+ /** 脱敏后的用户可见根因;完整原始异常只保留在运行日志中。 */
40
+ terminalError?: TerminalErrorInfo;
41
+ }
42
42
 
43
43
  function getStreamStatePath(sessionId: string): string {
44
44
  return join(STREAMS_DIR, `${sessionId}.json`);
@@ -131,18 +131,18 @@ export async function markFinalReplySent(sessionId: string, turnCount: number, s
131
131
  await writeStreamState(state);
132
132
  }
133
133
 
134
- export function createEmptyStreamState(sessionId: string, cwd: string, tool: string, turnCount: number): StreamState {
135
- const now = Date.now();
136
- return {
134
+ export function createEmptyStreamState(sessionId: string, cwd: string, tool: string, turnCount: number): StreamState {
135
+ const now = Date.now();
136
+ return {
137
137
  sessionId,
138
138
  status: "running",
139
- accumulatedContent: "",
140
- finalReply: "",
141
- activity: createAgentActivityTracker(now).activity,
139
+ accumulatedContent: "",
140
+ finalReply: "",
141
+ activity: createAgentActivityTracker(now).activity,
142
142
  chunkCount: 0,
143
143
  turnCount,
144
144
  contextTokens: 0,
145
- updatedAt: now,
145
+ updatedAt: now,
146
146
  cwd,
147
147
  tool,
148
148
  };
@@ -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
  }