chatccc 0.2.187 → 0.2.189
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/agent-prompts/claude_specific.md +45 -45
- package/agent-prompts/codex_specific.md +2 -2
- package/agent-prompts/cursor_specific.md +13 -13
- package/config.sample.json +5 -0
- package/im-skills/feishu-skill/receive-send-file.md +63 -63
- package/im-skills/feishu-skill/receive-send-image.md +24 -24
- package/im-skills/feishu-skill/skill.md +3 -3
- package/im-skills/wechat-file-skill/receive-send-file.md +38 -38
- package/im-skills/wechat-file-skill/send-file.mjs +83 -83
- package/im-skills/wechat-file-skill/skill.md +10 -10
- package/im-skills/wechat-image-skill/skill.md +10 -10
- package/im-skills/wechat-video-skill/receive-send-video.md +38 -38
- package/im-skills/wechat-video-skill/send-video.mjs +79 -79
- package/im-skills/wechat-video-skill/skill.md +10 -10
- package/package.json +1 -1
- package/scripts/postinstall-sharp-check.mjs +58 -58
- package/src/__tests__/agent-delegate-task-rpc.test.ts +165 -165
- package/src/__tests__/builtin-config.test.ts +33 -0
- package/src/__tests__/card-plain-text.test.ts +5 -5
- package/src/__tests__/cardkit.test.ts +60 -60
- package/src/__tests__/cards.test.ts +77 -77
- package/src/__tests__/chatgpt-subscription-rpc.test.ts +89 -89
- package/src/__tests__/chatgpt-subscription.test.ts +135 -135
- package/src/__tests__/chrome-devtools-guard.test.ts +165 -125
- package/src/__tests__/claude-adapter.test.ts +592 -592
- package/src/__tests__/codex-reset-actions.test.ts +146 -146
- package/src/__tests__/config-reload.test.ts +1 -0
- package/src/__tests__/config-sample.test.ts +11 -0
- package/src/__tests__/feishu-api.test.ts +60 -60
- package/src/__tests__/feishu-avatar.test.ts +180 -115
- package/src/__tests__/feishu-platform.test.ts +22 -22
- package/src/__tests__/format-message.test.ts +47 -47
- package/src/__tests__/orchestrator.test.ts +150 -2
- package/src/__tests__/privacy.test.ts +198 -198
- package/src/__tests__/raw-stream-log.test.ts +106 -106
- package/src/__tests__/session.test.ts +10 -0
- package/src/__tests__/shared-prefix.test.ts +36 -36
- package/src/__tests__/stream-state.test.ts +42 -42
- package/src/__tests__/web-ui.test.ts +209 -130
- package/src/adapters/claude-adapter.ts +566 -566
- package/src/adapters/claude-session-meta-store.ts +120 -120
- package/src/adapters/codex-adapter.ts +10 -6
- package/src/adapters/cursor-adapter.ts +46 -46
- package/src/adapters/raw-stream-log.ts +124 -124
- package/src/adapters/resource-monitor.ts +140 -140
- package/src/agent-delegate-task-rpc.ts +153 -153
- package/src/agent-delegate-task.ts +81 -81
- package/src/agent-stop-stuck.ts +129 -129
- package/src/builtin/cli.ts +189 -197
- package/src/builtin/index.ts +168 -167
- package/src/cards.ts +130 -89
- package/src/chatgpt-subscription-rpc.ts +27 -27
- package/src/chatgpt-subscription.ts +299 -299
- package/src/chrome-devtools-guard.ts +318 -242
- package/src/codex-reset-actions.ts +184 -184
- package/src/config.ts +38 -0
- package/src/feishu-api.ts +219 -190
- package/src/feishu-platform.ts +20 -20
- package/src/format-message.ts +293 -293
- package/src/index.ts +2 -2
- package/src/litellm-proxy.ts +374 -374
- package/src/orchestrator.ts +201 -9
- package/src/platform-adapter.ts +9 -1
- package/src/privacy.ts +118 -118
- package/src/session-chat-binding.ts +6 -6
- package/src/session-name.ts +8 -8
- package/src/session.ts +44 -16
- package/src/shared-prefix.ts +29 -29
- package/src/sim-platform.ts +20 -20
- package/src/turn-cards.ts +117 -117
- package/src/web-ui.ts +142 -24
package/src/session.ts
CHANGED
|
@@ -4,7 +4,6 @@ import { dirname, join } from "node:path";
|
|
|
4
4
|
import {
|
|
5
5
|
CLAUDE_API_KEY,
|
|
6
6
|
CLAUDE_BASE_URL,
|
|
7
|
-
CLAUDE_EFFORT,
|
|
8
7
|
CLAUDE_MAX_TURN,
|
|
9
8
|
CLAUDE_MODEL,
|
|
10
9
|
CLAUDE_SUBAGENT_MODEL,
|
|
@@ -17,6 +16,7 @@ import {
|
|
|
17
16
|
config,
|
|
18
17
|
fileLog,
|
|
19
18
|
getDefaultCwd,
|
|
19
|
+
getDefaultEffortForTool,
|
|
20
20
|
isAnthropicConfigEmpty,
|
|
21
21
|
toolDisplayName,
|
|
22
22
|
ts,
|
|
@@ -335,6 +335,9 @@ export function resetState(): void {
|
|
|
335
335
|
}
|
|
336
336
|
activePrompts.clear();
|
|
337
337
|
displayCards.clear();
|
|
338
|
+
sessionModelOverrides.clear();
|
|
339
|
+
sessionEffortOverrides.clear();
|
|
340
|
+
adapterCache.clear();
|
|
338
341
|
stopUnifiedDisplayLoop();
|
|
339
342
|
console.log(`[${ts()}] [RESET] State cleared (dedup + active sessions + bindings)`);
|
|
340
343
|
}
|
|
@@ -350,6 +353,7 @@ const adapterCache = new Map<string, ToolAdapter>();
|
|
|
350
353
|
|
|
351
354
|
// Per-session 模型覆盖(/model 命令设置,不持久化)
|
|
352
355
|
const sessionModelOverrides = new Map<string, string>();
|
|
356
|
+
const sessionEffortOverrides = new Map<string, string>();
|
|
353
357
|
|
|
354
358
|
/** 返回 session 的生效模型:优先 per-session 覆盖,其次全局配置(Claude) */
|
|
355
359
|
function getModelForSession(sessionId?: string): string {
|
|
@@ -371,6 +375,17 @@ export function getEffectiveModelForTool(tool: string, sessionId?: string): stri
|
|
|
371
375
|
return CLAUDE_MODEL;
|
|
372
376
|
}
|
|
373
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
|
+
|
|
374
389
|
/** 为指定 session 设置模型覆盖(/model <name>) */
|
|
375
390
|
export function setSessionModelOverride(sessionId: string, model: string): void {
|
|
376
391
|
sessionModelOverrides.set(sessionId, model);
|
|
@@ -383,9 +398,20 @@ export function clearSessionModelOverride(sessionId: string): void {
|
|
|
383
398
|
adapterCache.clear();
|
|
384
399
|
}
|
|
385
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
|
+
|
|
386
411
|
export function getAdapterForTool(tool: string, sessionId?: string): ToolAdapter {
|
|
387
412
|
const effectiveModel = getEffectiveModelForTool(tool, sessionId);
|
|
388
|
-
const
|
|
413
|
+
const effectiveEffort = getEffectiveEffortForTool(tool, sessionId);
|
|
414
|
+
const cacheKey = `${tool}:${effectiveModel || ""}:${effectiveEffort || ""}`;
|
|
389
415
|
const cached = adapterCache.get(cacheKey);
|
|
390
416
|
if (cached) return cached;
|
|
391
417
|
|
|
@@ -393,12 +419,12 @@ export function getAdapterForTool(tool: string, sessionId?: string): ToolAdapter
|
|
|
393
419
|
if (tool === "cursor") {
|
|
394
420
|
adapter = createCursorAdapter({ model: effectiveModel || undefined });
|
|
395
421
|
} else if (tool === "codex") {
|
|
396
|
-
adapter = createCodexAdapter({ model: effectiveModel || undefined });
|
|
422
|
+
adapter = createCodexAdapter({ model: effectiveModel || undefined, effort: effectiveEffort || undefined });
|
|
397
423
|
} else {
|
|
398
424
|
adapter = createClaudeAdapter({
|
|
399
425
|
model: effectiveModel,
|
|
400
426
|
subagentModel: CLAUDE_SUBAGENT_MODEL,
|
|
401
|
-
effort:
|
|
427
|
+
effort: effectiveEffort,
|
|
402
428
|
apiKey: CLAUDE_API_KEY,
|
|
403
429
|
baseUrl: CLAUDE_BASE_URL,
|
|
404
430
|
isEmpty: isAnthropicConfigEmpty,
|
|
@@ -831,15 +857,15 @@ function formatToolConfigForLog(tool: string, sessionModel?: string, sessionId?:
|
|
|
831
857
|
return `model=${sessionModel ?? "(由 cursor-agent 决定,init 事件后学习)"}`;
|
|
832
858
|
}
|
|
833
859
|
if (tool === "codex") {
|
|
834
|
-
const m =
|
|
835
|
-
const e =
|
|
860
|
+
const m = getEffectiveModelForTool(tool, sessionId);
|
|
861
|
+
const e = getEffectiveEffortForTool(tool, sessionId);
|
|
836
862
|
const modelStr = m.trim() !== "" ? m : "(由 codex config.toml 决定)";
|
|
837
863
|
const effortStr = e.trim() !== ""
|
|
838
864
|
? `effort=${e}`
|
|
839
865
|
: "effort=(由 codex config.toml 决定)";
|
|
840
866
|
return `model=${modelStr}, ${effortStr}`;
|
|
841
867
|
}
|
|
842
|
-
return `model=${anthropicConfigDisplay(getModelForSession(sessionId))}, subagentModel=${anthropicConfigDisplay(CLAUDE_SUBAGENT_MODEL)}, effort=${anthropicConfigDisplay(
|
|
868
|
+
return `model=${anthropicConfigDisplay(getModelForSession(sessionId))}, subagentModel=${anthropicConfigDisplay(CLAUDE_SUBAGENT_MODEL)}, effort=${anthropicConfigDisplay(getEffectiveEffortForTool(tool, sessionId))}`;
|
|
843
869
|
}
|
|
844
870
|
|
|
845
871
|
export async function initClaudeSession(tool: string, overrideCwd?: string, chatId?: string): Promise<{ sessionId: string; cwd: string }> {
|
|
@@ -955,12 +981,12 @@ export async function runAgentSession(
|
|
|
955
981
|
const imSkillsCacheDir = join(USER_DATA_DIR, "im-skills");
|
|
956
982
|
const skillVariables = {
|
|
957
983
|
cwd,
|
|
958
|
-
session_id: sessionId,
|
|
959
|
-
im_skills_cache_dir: imSkillsCacheDir,
|
|
960
|
-
delegate_task_url: `http://127.0.0.1:${CHATCCC_PORT}/api/agent/delegate-task`,
|
|
961
|
-
send_image_url: `http://127.0.0.1:${CHATCCC_PORT}/api/agent/send-image`,
|
|
962
|
-
send_file_url: `http://127.0.0.1:${CHATCCC_PORT}/api/agent/send-file`,
|
|
963
|
-
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"),
|
|
964
990
|
send_file_script: join(feishuSkillDir, "send-file.mjs"),
|
|
965
991
|
download_video_script: join(feishuSkillDir, "download-video.mjs"),
|
|
966
992
|
wechat_send_image_script: join(wechatImageSkillDir, "send-image.mjs"),
|
|
@@ -1756,8 +1782,8 @@ async function resolveModelEffort(
|
|
|
1756
1782
|
return { model, effort: null };
|
|
1757
1783
|
}
|
|
1758
1784
|
if (tool === "codex") {
|
|
1759
|
-
const m =
|
|
1760
|
-
const e =
|
|
1785
|
+
const m = getEffectiveModelForTool(tool, sessionId);
|
|
1786
|
+
const e = getEffectiveEffortForTool(tool, sessionId);
|
|
1761
1787
|
return {
|
|
1762
1788
|
model: m.trim() !== "" ? m : UNKNOWN_MODEL_PLACEHOLDER,
|
|
1763
1789
|
effort: e.trim() !== "" ? e : UNKNOWN_MODEL_PLACEHOLDER,
|
|
@@ -1765,7 +1791,7 @@ async function resolveModelEffort(
|
|
|
1765
1791
|
}
|
|
1766
1792
|
return {
|
|
1767
1793
|
model: anthropicConfigDisplay(getModelForSession(sessionId)),
|
|
1768
|
-
effort: anthropicConfigDisplay(
|
|
1794
|
+
effort: anthropicConfigDisplay(getEffectiveEffortForTool(tool, sessionId)),
|
|
1769
1795
|
};
|
|
1770
1796
|
}
|
|
1771
1797
|
|
|
@@ -1873,6 +1899,8 @@ export function _setAdapterForToolForTest(tool: string, adapter: ToolAdapter): v
|
|
|
1873
1899
|
adapterCache.set(tool, adapter);
|
|
1874
1900
|
// 同时设置当前配置模型对应的 key(getAdapterForTool 会优先 lookup 含 model 的 key)
|
|
1875
1901
|
const effective = getEffectiveModelForTool(tool);
|
|
1902
|
+
const effort = getEffectiveEffortForTool(tool);
|
|
1903
|
+
adapterCache.set(`${tool}:${effective || ""}:${effort || ""}`, adapter);
|
|
1876
1904
|
if (effective) adapterCache.set(`${tool}:${effective}`, adapter);
|
|
1877
1905
|
}
|
|
1878
1906
|
|
package/src/shared-prefix.ts
CHANGED
|
@@ -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
|
+
}
|
package/src/sim-platform.ts
CHANGED
|
@@ -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
|
}
|