chatccc 0.2.229 → 0.2.231
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/.agents/skills/create-chatccc-feishu-app/SKILL.md +85 -85
- package/.claude/skills/create-chatccc-feishu-app/SKILL.md +85 -85
- package/.cursor/skills/create-chatccc-feishu-app/SKILL.md +85 -85
- package/README.md +90 -90
- 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/bin/cccagent.mjs +17 -17
- 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-activity.test.ts +86 -76
- package/src/__tests__/agent-delegate-task-rpc.test.ts +165 -165
- package/src/__tests__/agent-reload-config-rpc.test.ts +99 -99
- package/src/__tests__/builtin-chat-session.test.ts +65 -1
- package/src/__tests__/builtin-cli-json.test.ts +39 -39
- package/src/__tests__/builtin-config.test.ts +26 -26
- package/src/__tests__/builtin-context.test.ts +202 -163
- package/src/__tests__/builtin-permissions.test.ts +211 -211
- package/src/__tests__/builtin-session-select.test.ts +116 -116
- package/src/__tests__/builtin-sigint.test.ts +56 -56
- package/src/__tests__/builtin-web-tools.test.ts +220 -220
- package/src/__tests__/card-action-routing.test.ts +18 -18
- package/src/__tests__/cardkit.test.ts +60 -60
- package/src/__tests__/ccc-adapter.test.ts +170 -136
- 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 -165
- package/src/__tests__/claude-adapter.test.ts +614 -614
- package/src/__tests__/claude-raw-stream-log.test.ts +87 -87
- package/src/__tests__/codex-adapter.test.ts +58 -58
- package/src/__tests__/codex-raw-stream-log.test.ts +170 -170
- package/src/__tests__/codex-reset-actions.test.ts +146 -146
- package/src/__tests__/cursor-adapter.test.ts +268 -268
- package/src/__tests__/feishu-api.test.ts +60 -60
- package/src/__tests__/feishu-avatar.test.ts +164 -164
- package/src/__tests__/feishu-message-ingress.test.ts +138 -138
- package/src/__tests__/feishu-platform.test.ts +22 -22
- package/src/__tests__/format-message.test.ts +47 -47
- package/src/__tests__/jsonl-stream.test.ts +79 -79
- package/src/__tests__/package-files.test.ts +24 -24
- package/src/__tests__/privacy.test.ts +198 -198
- package/src/__tests__/progress-reducer.test.ts +121 -110
- package/src/__tests__/raw-stream-log.test.ts +106 -106
- package/src/__tests__/response-stall.test.ts +49 -49
- package/src/__tests__/session.test.ts +174 -16
- package/src/__tests__/shared-prefix.test.ts +36 -36
- package/src/__tests__/sim-platform.test.ts +16 -16
- package/src/__tests__/startup-lifecycle.test.ts +231 -231
- package/src/__tests__/stop-session.test.ts +34 -34
- package/src/__tests__/stream-state.test.ts +42 -42
- package/src/__tests__/terminal-error.test.ts +54 -0
- package/src/__tests__/terminal-renderer.test.ts +247 -247
- package/src/__tests__/update-command-guard.test.ts +144 -144
- package/src/__tests__/web-ui.test.ts +326 -326
- package/src/adapters/adapter-interface.ts +24 -18
- package/src/adapters/ccc-adapter.ts +141 -131
- package/src/adapters/claude-adapter.ts +620 -620
- package/src/adapters/claude-session-meta-store.ts +120 -120
- package/src/adapters/codex-adapter.ts +426 -426
- package/src/adapters/cursor-adapter.ts +681 -681
- package/src/adapters/jsonl-stream.ts +157 -157
- package/src/adapters/raw-stream-log.ts +124 -124
- package/src/adapters/resource-monitor.ts +140 -140
- package/src/agent-activity.ts +175 -170
- package/src/agent-delegate-task-rpc.ts +153 -153
- package/src/agent-delegate-task.ts +91 -91
- package/src/agent-reload-config-rpc.ts +34 -34
- package/src/agent-stop-stuck.ts +129 -129
- package/src/builtin/cli.ts +9 -2
- package/src/builtin/config.ts +84 -84
- package/src/builtin/context.ts +62 -4
- package/src/builtin/file-log.ts +38 -38
- package/src/builtin/index.ts +83 -19
- package/src/builtin/proc-tree-kill.ts +61 -61
- package/src/builtin/progress/cards-helpers.ts +76 -76
- package/src/builtin/progress/reducer.ts +113 -108
- package/src/builtin/progress/terminal-renderer.ts +294 -294
- package/src/builtin/progress/view.ts +77 -77
- package/src/builtin/raw-stream-log.ts +124 -124
- package/src/builtin/session-select.ts +48 -48
- package/src/builtin/sigint.ts +50 -50
- package/src/builtin/web-tools.ts +313 -313
- package/src/card-action-routing.ts +14 -14
- package/src/chatgpt-subscription-rpc.ts +27 -27
- package/src/chatgpt-subscription.ts +299 -299
- package/src/chrome-devtools-guard.ts +318 -318
- package/src/codex-reset-actions.ts +184 -184
- package/src/feishu-api.ts +193 -193
- package/src/feishu-message-ingress.ts +195 -195
- package/src/feishu-platform.ts +20 -20
- package/src/format-message.ts +293 -293
- package/src/litellm-proxy.ts +374 -374
- package/src/orchestrator.ts +8 -3
- package/src/platform-adapter.ts +6 -6
- package/src/privacy.ts +118 -118
- package/src/progress/reducer.ts +113 -108
- package/src/progress/terminal-renderer.ts +294 -294
- package/src/progress/view.ts +77 -77
- package/src/response-stall.ts +28 -28
- package/src/runtime-reload.ts +34 -34
- package/src/session-chat-binding.ts +82 -82
- package/src/session-name.ts +8 -8
- package/src/session.ts +149 -26
- package/src/shared-prefix.ts +29 -29
- package/src/sim-platform.ts +20 -20
- package/src/startup-lifecycle.ts +250 -250
- package/src/stream-state.ts +21 -18
- package/src/terminal-error.ts +129 -0
- package/src/turn-cards.ts +117 -117
- package/src/update-command-guard.ts +165 -165
|
@@ -0,0 +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
|
+
}
|
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
|
}
|