chatccc 0.2.190 → 0.2.191

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 (67) hide show
  1. package/agent-prompts/claude_specific.md +45 -45
  2. package/agent-prompts/codex_specific.md +2 -2
  3. package/agent-prompts/cursor_specific.md +13 -13
  4. package/config.sample.json +14 -14
  5. package/im-skills/feishu-skill/receive-send-file.md +63 -63
  6. package/im-skills/feishu-skill/receive-send-image.md +24 -24
  7. package/im-skills/feishu-skill/skill.md +3 -3
  8. package/im-skills/wechat-file-skill/receive-send-file.md +38 -38
  9. package/im-skills/wechat-file-skill/send-file.mjs +83 -83
  10. package/im-skills/wechat-file-skill/skill.md +10 -10
  11. package/im-skills/wechat-image-skill/skill.md +10 -10
  12. package/im-skills/wechat-video-skill/receive-send-video.md +38 -38
  13. package/im-skills/wechat-video-skill/send-video.mjs +79 -79
  14. package/im-skills/wechat-video-skill/skill.md +10 -10
  15. package/package.json +1 -1
  16. package/scripts/postinstall-sharp-check.mjs +58 -58
  17. package/src/__tests__/agent-delegate-task-rpc.test.ts +165 -165
  18. package/src/__tests__/builtin-config.test.ts +33 -33
  19. package/src/__tests__/card-plain-text.test.ts +5 -5
  20. package/src/__tests__/cardkit.test.ts +60 -60
  21. package/src/__tests__/cards.test.ts +77 -77
  22. package/src/__tests__/chatgpt-subscription-rpc.test.ts +89 -89
  23. package/src/__tests__/chatgpt-subscription.test.ts +135 -135
  24. package/src/__tests__/chrome-devtools-guard.test.ts +165 -165
  25. package/src/__tests__/claude-adapter.test.ts +592 -592
  26. package/src/__tests__/codex-reset-actions.test.ts +146 -146
  27. package/src/__tests__/config-reload.test.ts +4 -4
  28. package/src/__tests__/config-sample.test.ts +17 -17
  29. package/src/__tests__/feishu-api.test.ts +60 -60
  30. package/src/__tests__/feishu-platform.test.ts +22 -22
  31. package/src/__tests__/format-message.test.ts +47 -47
  32. package/src/__tests__/orchestrator.test.ts +120 -120
  33. package/src/__tests__/privacy.test.ts +198 -198
  34. package/src/__tests__/raw-stream-log.test.ts +106 -106
  35. package/src/__tests__/session.test.ts +17 -17
  36. package/src/__tests__/shared-prefix.test.ts +36 -36
  37. package/src/__tests__/stream-state.test.ts +42 -42
  38. package/src/__tests__/web-ui.test.ts +209 -209
  39. package/src/adapters/claude-adapter.ts +566 -566
  40. package/src/adapters/claude-session-meta-store.ts +120 -120
  41. package/src/adapters/codex-adapter.ts +30 -30
  42. package/src/adapters/cursor-adapter.ts +46 -46
  43. package/src/adapters/raw-stream-log.ts +124 -124
  44. package/src/adapters/resource-monitor.ts +140 -140
  45. package/src/agent-delegate-task-rpc.ts +153 -153
  46. package/src/agent-delegate-task.ts +81 -81
  47. package/src/agent-stop-stuck.ts +129 -129
  48. package/src/builtin/cli.ts +204 -189
  49. package/src/builtin/index.ts +170 -170
  50. package/src/cards.ts +137 -137
  51. package/src/chatgpt-subscription-rpc.ts +27 -27
  52. package/src/chatgpt-subscription.ts +299 -299
  53. package/src/chrome-devtools-guard.ts +318 -318
  54. package/src/codex-reset-actions.ts +184 -184
  55. package/src/config.ts +86 -86
  56. package/src/feishu-platform.ts +20 -20
  57. package/src/format-message.ts +293 -293
  58. package/src/litellm-proxy.ts +374 -374
  59. package/src/orchestrator.ts +143 -143
  60. package/src/privacy.ts +118 -118
  61. package/src/session-chat-binding.ts +6 -6
  62. package/src/session-name.ts +8 -8
  63. package/src/session.ts +98 -98
  64. package/src/shared-prefix.ts +29 -29
  65. package/src/sim-platform.ts +20 -20
  66. package/src/turn-cards.ts +117 -117
  67. package/src/web-ui.ts +205 -205
package/src/session.ts CHANGED
@@ -2,9 +2,9 @@ import { readFile, writeFile, mkdir } from "node:fs/promises";
2
2
  import { dirname, join } from "node:path";
3
3
 
4
4
  import {
5
- CLAUDE_API_KEY,
6
- CLAUDE_BASE_URL,
7
- CLAUDE_MAX_TURN,
5
+ CLAUDE_API_KEY,
6
+ CLAUDE_BASE_URL,
7
+ CLAUDE_MAX_TURN,
8
8
  CLAUDE_MODEL,
9
9
  CLAUDE_SUBAGENT_MODEL,
10
10
  CHATCCC_PORT,
@@ -14,13 +14,13 @@ import {
14
14
  addRecentDir,
15
15
  anthropicConfigDisplay,
16
16
  config,
17
- fileLog,
18
- getDefaultCwd,
19
- getDefaultEffortForTool,
20
- isAnthropicConfigEmpty,
21
- toolDisplayName,
22
- ts,
23
- } from "./config.ts";
17
+ fileLog,
18
+ getDefaultCwd,
19
+ getDefaultEffortForTool,
20
+ isAnthropicConfigEmpty,
21
+ toolDisplayName,
22
+ ts,
23
+ } from "./config.ts";
24
24
  import { buildProgressCard, getToolEmoji, isCodeBlockOpen, truncateContent } from "./cards.ts";
25
25
  import { simplifyToolUse, simplifyToolResult } from "./simplify.ts";
26
26
  import { logTrace } from "./trace.ts";
@@ -333,14 +333,14 @@ export function resetState(): void {
333
333
  for (const prompt of activePrompts.values()) {
334
334
  if (prompt.processMonitor) clearInterval(prompt.processMonitor);
335
335
  }
336
- activePrompts.clear();
337
- displayCards.clear();
338
- sessionModelOverrides.clear();
339
- sessionEffortOverrides.clear();
340
- adapterCache.clear();
341
- stopUnifiedDisplayLoop();
342
- console.log(`[${ts()}] [RESET] State cleared (dedup + active sessions + bindings)`);
343
- }
336
+ activePrompts.clear();
337
+ displayCards.clear();
338
+ sessionModelOverrides.clear();
339
+ sessionEffortOverrides.clear();
340
+ adapterCache.clear();
341
+ stopUnifiedDisplayLoop();
342
+ console.log(`[${ts()}] [RESET] State cleared (dedup + active sessions + bindings)`);
343
+ }
344
344
 
345
345
  // 注:`rebuildBindingsFromRegistry` 定义在下方与 loadSessionRegistry 同区域,
346
346
  // 是 onReady/onReconnected 取代 resetState 的正确入口。
@@ -352,8 +352,8 @@ export function resetState(): void {
352
352
  const adapterCache = new Map<string, ToolAdapter>();
353
353
 
354
354
  // Per-session 模型覆盖(/model 命令设置,不持久化)
355
- const sessionModelOverrides = new Map<string, string>();
356
- const sessionEffortOverrides = new Map<string, string>();
355
+ const sessionModelOverrides = new Map<string, string>();
356
+ const sessionEffortOverrides = new Map<string, string>();
357
357
 
358
358
  /** 返回 session 的生效模型:优先 per-session 覆盖,其次全局配置(Claude) */
359
359
  function getModelForSession(sessionId?: string): string {
@@ -365,26 +365,26 @@ function getModelForSession(sessionId?: string): string {
365
365
  }
366
366
 
367
367
  /** 返回指定 tool 的生效模型:优先 per-session 覆盖,其次 tool 默认配置 */
368
- export function getEffectiveModelForTool(tool: string, sessionId?: string): string {
369
- if (sessionId) {
370
- const override = sessionModelOverrides.get(sessionId);
371
- if (override) return override;
372
- }
368
+ export function getEffectiveModelForTool(tool: string, sessionId?: string): string {
369
+ if (sessionId) {
370
+ const override = sessionModelOverrides.get(sessionId);
371
+ if (override) return override;
372
+ }
373
373
  if (tool === "cursor") return config.cursor.model;
374
374
  if (tool === "codex") return config.codex.model;
375
- return CLAUDE_MODEL;
376
- }
377
-
378
- export function getEffectiveEffortForTool(tool: string, sessionId?: string): string {
379
- if (sessionId) {
380
- const override = sessionEffortOverrides.get(sessionId);
381
- if (override) return override;
382
- }
383
- if (tool === "claude" || tool === "codex") {
384
- return getDefaultEffortForTool(tool);
385
- }
386
- return "";
387
- }
375
+ return CLAUDE_MODEL;
376
+ }
377
+
378
+ export function getEffectiveEffortForTool(tool: string, sessionId?: string): string {
379
+ if (sessionId) {
380
+ const override = sessionEffortOverrides.get(sessionId);
381
+ if (override) return override;
382
+ }
383
+ if (tool === "claude" || tool === "codex") {
384
+ return getDefaultEffortForTool(tool);
385
+ }
386
+ return "";
387
+ }
388
388
 
389
389
  /** 为指定 session 设置模型覆盖(/model <name>) */
390
390
  export function setSessionModelOverride(sessionId: string, model: string): void {
@@ -393,41 +393,41 @@ export function setSessionModelOverride(sessionId: string, model: string): void
393
393
  }
394
394
 
395
395
  /** 清除指定 session 的模型覆盖(/model clear) */
396
- export function clearSessionModelOverride(sessionId: string): void {
397
- sessionModelOverrides.delete(sessionId);
398
- adapterCache.clear();
399
- }
400
-
401
- export function setSessionEffortOverride(sessionId: string, effort: string): void {
402
- sessionEffortOverrides.set(sessionId, effort);
403
- adapterCache.clear();
404
- }
405
-
406
- export function clearSessionEffortOverride(sessionId: string): void {
407
- sessionEffortOverrides.delete(sessionId);
408
- adapterCache.clear();
409
- }
410
-
411
- export function getAdapterForTool(tool: string, sessionId?: string): ToolAdapter {
412
- const effectiveModel = getEffectiveModelForTool(tool, sessionId);
413
- const effectiveEffort = getEffectiveEffortForTool(tool, sessionId);
414
- const cacheKey = `${tool}:${effectiveModel || ""}:${effectiveEffort || ""}`;
415
- const cached = adapterCache.get(cacheKey);
416
- if (cached) return cached;
417
-
418
- let adapter: ToolAdapter;
419
- if (tool === "cursor") {
420
- adapter = createCursorAdapter({ model: effectiveModel || undefined });
421
- } else if (tool === "codex") {
422
- adapter = createCodexAdapter({ model: effectiveModel || undefined, effort: effectiveEffort || undefined });
423
- } else {
424
- adapter = createClaudeAdapter({
425
- model: effectiveModel,
426
- subagentModel: CLAUDE_SUBAGENT_MODEL,
427
- effort: effectiveEffort,
428
- apiKey: CLAUDE_API_KEY,
429
- baseUrl: CLAUDE_BASE_URL,
430
- isEmpty: isAnthropicConfigEmpty,
396
+ export function clearSessionModelOverride(sessionId: string): void {
397
+ sessionModelOverrides.delete(sessionId);
398
+ adapterCache.clear();
399
+ }
400
+
401
+ export function setSessionEffortOverride(sessionId: string, effort: string): void {
402
+ sessionEffortOverrides.set(sessionId, effort);
403
+ adapterCache.clear();
404
+ }
405
+
406
+ export function clearSessionEffortOverride(sessionId: string): void {
407
+ sessionEffortOverrides.delete(sessionId);
408
+ adapterCache.clear();
409
+ }
410
+
411
+ export function getAdapterForTool(tool: string, sessionId?: string): ToolAdapter {
412
+ const effectiveModel = getEffectiveModelForTool(tool, sessionId);
413
+ const effectiveEffort = getEffectiveEffortForTool(tool, sessionId);
414
+ const cacheKey = `${tool}:${effectiveModel || ""}:${effectiveEffort || ""}`;
415
+ const cached = adapterCache.get(cacheKey);
416
+ if (cached) return cached;
417
+
418
+ let adapter: ToolAdapter;
419
+ if (tool === "cursor") {
420
+ adapter = createCursorAdapter({ model: effectiveModel || undefined });
421
+ } else if (tool === "codex") {
422
+ adapter = createCodexAdapter({ model: effectiveModel || undefined, effort: effectiveEffort || undefined });
423
+ } else {
424
+ adapter = createClaudeAdapter({
425
+ model: effectiveModel,
426
+ subagentModel: CLAUDE_SUBAGENT_MODEL,
427
+ effort: effectiveEffort,
428
+ apiKey: CLAUDE_API_KEY,
429
+ baseUrl: CLAUDE_BASE_URL,
430
+ isEmpty: isAnthropicConfigEmpty,
431
431
  maxTurn: CLAUDE_MAX_TURN,
432
432
  });
433
433
  }
@@ -855,18 +855,18 @@ export async function switchChatBinding(args: SwitchChatBindingArgs): Promise<Sw
855
855
  function formatToolConfigForLog(tool: string, sessionModel?: string, sessionId?: string): string {
856
856
  if (tool === "cursor") {
857
857
  return `model=${sessionModel ?? "(由 cursor-agent 决定,init 事件后学习)"}`;
858
- }
859
- if (tool === "codex") {
860
- const m = getEffectiveModelForTool(tool, sessionId);
861
- const e = getEffectiveEffortForTool(tool, sessionId);
858
+ }
859
+ if (tool === "codex") {
860
+ const m = getEffectiveModelForTool(tool, sessionId);
861
+ const e = getEffectiveEffortForTool(tool, sessionId);
862
862
  const modelStr = m.trim() !== "" ? m : "(由 codex config.toml 决定)";
863
863
  const effortStr = e.trim() !== ""
864
864
  ? `effort=${e}`
865
865
  : "effort=(由 codex config.toml 决定)";
866
866
  return `model=${modelStr}, ${effortStr}`;
867
867
  }
868
- return `model=${anthropicConfigDisplay(getModelForSession(sessionId))}, subagentModel=${anthropicConfigDisplay(CLAUDE_SUBAGENT_MODEL)}, effort=${anthropicConfigDisplay(getEffectiveEffortForTool(tool, sessionId))}`;
869
- }
868
+ return `model=${anthropicConfigDisplay(getModelForSession(sessionId))}, subagentModel=${anthropicConfigDisplay(CLAUDE_SUBAGENT_MODEL)}, effort=${anthropicConfigDisplay(getEffectiveEffortForTool(tool, sessionId))}`;
869
+ }
870
870
 
871
871
  export async function initClaudeSession(tool: string, overrideCwd?: string, chatId?: string): Promise<{ sessionId: string; cwd: string }> {
872
872
  const cwd = overrideCwd ?? (await getDefaultCwd(chatId));
@@ -981,12 +981,12 @@ export async function runAgentSession(
981
981
  const imSkillsCacheDir = join(USER_DATA_DIR, "im-skills");
982
982
  const skillVariables = {
983
983
  cwd,
984
- session_id: sessionId,
985
- im_skills_cache_dir: imSkillsCacheDir,
986
- delegate_task_url: `http://127.0.0.1:${CHATCCC_PORT}/api/agent/delegate-task`,
987
- send_image_url: `http://127.0.0.1:${CHATCCC_PORT}/api/agent/send-image`,
988
- send_file_url: `http://127.0.0.1:${CHATCCC_PORT}/api/agent/send-file`,
989
- send_image_script: join(feishuSkillDir, "send-image.mjs"),
984
+ session_id: sessionId,
985
+ im_skills_cache_dir: imSkillsCacheDir,
986
+ delegate_task_url: `http://127.0.0.1:${CHATCCC_PORT}/api/agent/delegate-task`,
987
+ send_image_url: `http://127.0.0.1:${CHATCCC_PORT}/api/agent/send-image`,
988
+ send_file_url: `http://127.0.0.1:${CHATCCC_PORT}/api/agent/send-file`,
989
+ send_image_script: join(feishuSkillDir, "send-image.mjs"),
990
990
  send_file_script: join(feishuSkillDir, "send-file.mjs"),
991
991
  download_video_script: join(feishuSkillDir, "download-video.mjs"),
992
992
  wechat_send_image_script: join(wechatImageSkillDir, "send-image.mjs"),
@@ -1780,20 +1780,20 @@ async function resolveModelEffort(
1780
1780
  // adapter 异常时降级为占位符(不阻塞 /state 卡片)
1781
1781
  }
1782
1782
  return { model, effort: null };
1783
- }
1784
- if (tool === "codex") {
1785
- const m = getEffectiveModelForTool(tool, sessionId);
1786
- const e = getEffectiveEffortForTool(tool, sessionId);
1783
+ }
1784
+ if (tool === "codex") {
1785
+ const m = getEffectiveModelForTool(tool, sessionId);
1786
+ const e = getEffectiveEffortForTool(tool, sessionId);
1787
1787
  return {
1788
1788
  model: m.trim() !== "" ? m : UNKNOWN_MODEL_PLACEHOLDER,
1789
1789
  effort: e.trim() !== "" ? e : UNKNOWN_MODEL_PLACEHOLDER,
1790
1790
  };
1791
- }
1792
- return {
1793
- model: anthropicConfigDisplay(getModelForSession(sessionId)),
1794
- effort: anthropicConfigDisplay(getEffectiveEffortForTool(tool, sessionId)),
1795
- };
1796
- }
1791
+ }
1792
+ return {
1793
+ model: anthropicConfigDisplay(getModelForSession(sessionId)),
1794
+ effort: anthropicConfigDisplay(getEffectiveEffortForTool(tool, sessionId)),
1795
+ };
1796
+ }
1797
1797
 
1798
1798
  export async function getSessionStatus(chatId: string): Promise<SessionStatus | null> {
1799
1799
  const info = sessionInfoMap.get(chatId);
@@ -1899,9 +1899,9 @@ export function _setAdapterForToolForTest(tool: string, adapter: ToolAdapter): v
1899
1899
  adapterCache.set(tool, adapter);
1900
1900
  // 同时设置当前配置模型对应的 key(getAdapterForTool 会优先 lookup 含 model 的 key)
1901
1901
  const effective = getEffectiveModelForTool(tool);
1902
- const effort = getEffectiveEffortForTool(tool);
1903
- adapterCache.set(`${tool}:${effective || ""}:${effort || ""}`, adapter);
1904
- if (effective) adapterCache.set(`${tool}:${effective}`, adapter);
1902
+ const effort = getEffectiveEffortForTool(tool);
1903
+ adapterCache.set(`${tool}:${effective || ""}:${effort || ""}`, adapter);
1904
+ if (effective) adapterCache.set(`${tool}:${effective}`, adapter);
1905
1905
  }
1906
1906
 
1907
1907
  export function clearAdapterCache(): void {
@@ -1,29 +1,29 @@
1
- export const ABD_PREFIX = "/abd";
2
-
3
- export const ABD_APPEND_PROMPT =
4
- "请从第一性原理出发挖掘我的真实需求。你觉得我的需求合理吗?开始实现之前有什么问题要问我的?";
5
-
6
- export const ABD_HELP_LINE =
7
- `发送 **${ABD_PREFIX}** 前缀消息,将以第一性原理追问真实需求、合理性和实现前问题`;
8
-
9
- export interface SharedPrefixResult {
10
- matched: boolean;
11
- text: string;
12
- body: string;
13
- prefix?: typeof ABD_PREFIX;
14
- }
15
-
16
- export function applySharedPrefix(text: string): SharedPrefixResult {
17
- if (!text.toLowerCase().startsWith(ABD_PREFIX)) {
18
- return { matched: false, text, body: text };
19
- }
20
-
21
- const body = text.slice(ABD_PREFIX.length).replace(/^\s+/, "");
22
- const appendix = ["---", ABD_APPEND_PROMPT].join("\n");
23
- return {
24
- matched: true,
25
- prefix: ABD_PREFIX,
26
- body,
27
- text: body ? [body, appendix].join("\n\n") : appendix,
28
- };
29
- }
1
+ export const ABD_PREFIX = "/abd";
2
+
3
+ export const ABD_APPEND_PROMPT =
4
+ "请从第一性原理出发挖掘我的真实需求。你觉得我的需求合理吗?开始实现之前有什么问题要问我的?";
5
+
6
+ export const ABD_HELP_LINE =
7
+ `发送 **${ABD_PREFIX}** 前缀消息,将以第一性原理追问真实需求、合理性和实现前问题`;
8
+
9
+ export interface SharedPrefixResult {
10
+ matched: boolean;
11
+ text: string;
12
+ body: string;
13
+ prefix?: typeof ABD_PREFIX;
14
+ }
15
+
16
+ export function applySharedPrefix(text: string): SharedPrefixResult {
17
+ if (!text.toLowerCase().startsWith(ABD_PREFIX)) {
18
+ return { matched: false, text, body: text };
19
+ }
20
+
21
+ const body = text.slice(ABD_PREFIX.length).replace(/^\s+/, "");
22
+ const appendix = ["---", ABD_APPEND_PROMPT].join("\n");
23
+ return {
24
+ matched: true,
25
+ prefix: ABD_PREFIX,
26
+ body,
27
+ text: body ? [body, appendix].join("\n\n") : appendix,
28
+ };
29
+ }
@@ -123,25 +123,25 @@ export const SimulatedPlatform: FeishuPlatform = {
123
123
  },
124
124
 
125
125
  // ---- 头像 ----
126
- async setChatAvatar(_token, _chatId, _tool, _status) {
127
- // 模拟模式不需要头像
128
- },
129
-
130
- async getCodexUsageSummary() {
131
- return {
132
- fiveHour: { usedPercent: 0, remainingPercent: 100, resetAtEpochSeconds: null, resetAfterSeconds: null },
133
- weekly: { usedPercent: 0, remainingPercent: 100, resetAtEpochSeconds: null, resetAfterSeconds: null },
134
- rateLimitResetCreditsAvailable: null,
135
- rateLimitResetCredits: null,
136
- };
137
- },
138
-
139
- async consumeCodexRateLimitResetCredit(_redeemRequestId) {
140
- return { code: "no_credit" as const, windowsReset: 0 };
141
- },
142
-
143
- // ---- 图片下载 ----
144
- async getOrDownloadImage(_token, _messageId, fileKey) {
126
+ async setChatAvatar(_token, _chatId, _tool, _status) {
127
+ // 模拟模式不需要头像
128
+ },
129
+
130
+ async getCodexUsageSummary() {
131
+ return {
132
+ fiveHour: { usedPercent: 0, remainingPercent: 100, resetAtEpochSeconds: null, resetAfterSeconds: null },
133
+ weekly: { usedPercent: 0, remainingPercent: 100, resetAtEpochSeconds: null, resetAfterSeconds: null },
134
+ rateLimitResetCreditsAvailable: null,
135
+ rateLimitResetCredits: null,
136
+ };
137
+ },
138
+
139
+ async consumeCodexRateLimitResetCredit(_redeemRequestId) {
140
+ return { code: "no_credit" as const, windowsReset: 0 };
141
+ },
142
+
143
+ // ---- 图片下载 ----
144
+ async getOrDownloadImage(_token, _messageId, fileKey) {
145
145
  return join(SIM_DIR, "images", fileKey);
146
146
  },
147
147
 
@@ -174,4 +174,4 @@ export const SimulatedPlatform: FeishuPlatform = {
174
174
  };
175
175
 
176
176
  /** 模拟模式下的默认 chat_id(重新导出以保持向后兼容) */
177
- export { SIM_DEFAULT_CHAT_ID } from "./sim-store.ts";
177
+ export { SIM_DEFAULT_CHAT_ID } from "./sim-store.ts";
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
  }