chatccc 0.2.88 → 0.2.90
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/package.json +63 -63
- package/src/session-chat-binding.ts +2 -2
- package/src/session.ts +83 -31
- package/src/turn-cards.ts +118 -0
package/package.json
CHANGED
|
@@ -1,63 +1,63 @@
|
|
|
1
|
-
{
|
|
2
|
-
"name": "chatccc",
|
|
3
|
-
"version": "0.2.
|
|
4
|
-
"description": "Feishu bot bridge for Claude Code",
|
|
5
|
-
"license": "Apache-2.0",
|
|
6
|
-
"type": "module",
|
|
7
|
-
"main": "./src/index.ts",
|
|
8
|
-
"bin": {
|
|
9
|
-
"chatccc": "bin/chatccc.mjs"
|
|
10
|
-
},
|
|
11
|
-
"files": [
|
|
12
|
-
"src/",
|
|
13
|
-
"bin/",
|
|
14
|
-
"scripts/postinstall-sharp-check.mjs",
|
|
15
|
-
"demo/ilink_echo_probe.ts",
|
|
16
|
-
"im-skills/",
|
|
17
|
-
"images/img_readme_*.jpg",
|
|
18
|
-
"images/img_readme_*.png",
|
|
19
|
-
"images/avatars/status_*.png",
|
|
20
|
-
"images/avatars/badges/",
|
|
21
|
-
"package.json",
|
|
22
|
-
"README.md",
|
|
23
|
-
"config.sample.json"
|
|
24
|
-
],
|
|
25
|
-
"scripts": {
|
|
26
|
-
"dev": "tsx src/index.ts",
|
|
27
|
-
"chatccc": "tsx src/index.ts",
|
|
28
|
-
"start": "tsx src/index.ts",
|
|
29
|
-
"demo:bot-test": "tsx demo/bot_test.ts",
|
|
30
|
-
"demo:bot-test:local": "tsx demo/bot_test.ts --local",
|
|
31
|
-
"demo:create-group": "tsx src/index.ts",
|
|
32
|
-
"demo:create-group:local": "tsx src/index.ts --local",
|
|
33
|
-
"demo:permission-check": "tsx demo/permission_check.ts",
|
|
34
|
-
"demo:claude-hi": "tsx demo/claude_say_hi.ts",
|
|
35
|
-
"demo:codex-hi": "tsx demo/codex_say_hi.ts",
|
|
36
|
-
"demo:ilink-echo": "tsx demo/ilink_echo_probe.ts",
|
|
37
|
-
"test": "vitest run",
|
|
38
|
-
"test:watch": "vitest",
|
|
39
|
-
"postinstall": "node scripts/postinstall-sharp-check.mjs"
|
|
40
|
-
},
|
|
41
|
-
"dependencies": {
|
|
42
|
-
"@ai-sdk/openai-compatible": "^2.0.47",
|
|
43
|
-
"@anthropic-ai/claude-agent-sdk": "0.2.133",
|
|
44
|
-
"@larksuiteoapi/node-sdk": "^1.59.0",
|
|
45
|
-
"@openilink/openilink-sdk-node": "^0.6.0",
|
|
46
|
-
"ai": "^6.0.184",
|
|
47
|
-
"nodemailer": "^8.0.7",
|
|
48
|
-
"qrcode-terminal": "^0.12.0",
|
|
49
|
-
"sharp": "^0.34.5",
|
|
50
|
-
"tsx": "^4.0.0",
|
|
51
|
-
"ws": "^8.18.0"
|
|
52
|
-
},
|
|
53
|
-
"devDependencies": {
|
|
54
|
-
"@types/node": "^20.0.0",
|
|
55
|
-
"@types/qrcode-terminal": "^0.12.2",
|
|
56
|
-
"@types/ws": "^8.18.1",
|
|
57
|
-
"typescript": "^5.0.0",
|
|
58
|
-
"vitest": "^3.2.4"
|
|
59
|
-
},
|
|
60
|
-
"engines": {
|
|
61
|
-
"node": ">=20"
|
|
62
|
-
}
|
|
63
|
-
}
|
|
1
|
+
{
|
|
2
|
+
"name": "chatccc",
|
|
3
|
+
"version": "0.2.90",
|
|
4
|
+
"description": "Feishu bot bridge for Claude Code",
|
|
5
|
+
"license": "Apache-2.0",
|
|
6
|
+
"type": "module",
|
|
7
|
+
"main": "./src/index.ts",
|
|
8
|
+
"bin": {
|
|
9
|
+
"chatccc": "bin/chatccc.mjs"
|
|
10
|
+
},
|
|
11
|
+
"files": [
|
|
12
|
+
"src/",
|
|
13
|
+
"bin/",
|
|
14
|
+
"scripts/postinstall-sharp-check.mjs",
|
|
15
|
+
"demo/ilink_echo_probe.ts",
|
|
16
|
+
"im-skills/",
|
|
17
|
+
"images/img_readme_*.jpg",
|
|
18
|
+
"images/img_readme_*.png",
|
|
19
|
+
"images/avatars/status_*.png",
|
|
20
|
+
"images/avatars/badges/",
|
|
21
|
+
"package.json",
|
|
22
|
+
"README.md",
|
|
23
|
+
"config.sample.json"
|
|
24
|
+
],
|
|
25
|
+
"scripts": {
|
|
26
|
+
"dev": "tsx src/index.ts",
|
|
27
|
+
"chatccc": "tsx src/index.ts",
|
|
28
|
+
"start": "tsx src/index.ts",
|
|
29
|
+
"demo:bot-test": "tsx demo/bot_test.ts",
|
|
30
|
+
"demo:bot-test:local": "tsx demo/bot_test.ts --local",
|
|
31
|
+
"demo:create-group": "tsx src/index.ts",
|
|
32
|
+
"demo:create-group:local": "tsx src/index.ts --local",
|
|
33
|
+
"demo:permission-check": "tsx demo/permission_check.ts",
|
|
34
|
+
"demo:claude-hi": "tsx demo/claude_say_hi.ts",
|
|
35
|
+
"demo:codex-hi": "tsx demo/codex_say_hi.ts",
|
|
36
|
+
"demo:ilink-echo": "tsx demo/ilink_echo_probe.ts",
|
|
37
|
+
"test": "vitest run",
|
|
38
|
+
"test:watch": "vitest",
|
|
39
|
+
"postinstall": "node scripts/postinstall-sharp-check.mjs"
|
|
40
|
+
},
|
|
41
|
+
"dependencies": {
|
|
42
|
+
"@ai-sdk/openai-compatible": "^2.0.47",
|
|
43
|
+
"@anthropic-ai/claude-agent-sdk": "0.2.133",
|
|
44
|
+
"@larksuiteoapi/node-sdk": "^1.59.0",
|
|
45
|
+
"@openilink/openilink-sdk-node": "^0.6.0",
|
|
46
|
+
"ai": "^6.0.184",
|
|
47
|
+
"nodemailer": "^8.0.7",
|
|
48
|
+
"qrcode-terminal": "^0.12.0",
|
|
49
|
+
"sharp": "^0.34.5",
|
|
50
|
+
"tsx": "^4.0.0",
|
|
51
|
+
"ws": "^8.18.0"
|
|
52
|
+
},
|
|
53
|
+
"devDependencies": {
|
|
54
|
+
"@types/node": "^20.0.0",
|
|
55
|
+
"@types/qrcode-terminal": "^0.12.2",
|
|
56
|
+
"@types/ws": "^8.18.1",
|
|
57
|
+
"typescript": "^5.0.0",
|
|
58
|
+
"vitest": "^3.2.4"
|
|
59
|
+
},
|
|
60
|
+
"engines": {
|
|
61
|
+
"node": ">=20"
|
|
62
|
+
}
|
|
63
|
+
}
|
|
@@ -130,6 +130,8 @@ export interface DisplayCardState {
|
|
|
130
130
|
cardCreatedAt: number;
|
|
131
131
|
lastSentContent: string;
|
|
132
132
|
streamErrorNotified: boolean;
|
|
133
|
+
/** 所属 turn */
|
|
134
|
+
turnCount: number;
|
|
133
135
|
/** 卡片轮转时记录的基线,轮转后只展示增量 */
|
|
134
136
|
rotationAccLen?: number;
|
|
135
137
|
rotationFinalReply?: string;
|
|
@@ -137,8 +139,6 @@ export interface DisplayCardState {
|
|
|
137
139
|
lastSentAccLen?: number;
|
|
138
140
|
/** WeChat delta: 上次发送时的 finalReply */
|
|
139
141
|
lastSentFinalReply?: string;
|
|
140
|
-
/** 上次 loop 读取到的 turnCount,用于检测轮次切换 */
|
|
141
|
-
lastTurnCount?: number;
|
|
142
142
|
}
|
|
143
143
|
|
|
144
144
|
export const displayCards = new Map<string, DisplayCardState>();
|
package/src/session.ts
CHANGED
|
@@ -38,6 +38,7 @@ function compressWechatDisplayText(text: string): string {
|
|
|
38
38
|
return [...lines.slice(0, 5), "...", ...lines.slice(-5)].join("\n");
|
|
39
39
|
}
|
|
40
40
|
import { readStreamState, writeStreamState, createEmptyStreamState, fixStaleStreamStates } from "./stream-state.ts";
|
|
41
|
+
import { addCardToTurn, finalizeTurnCards, markCardDone } from "./turn-cards.ts";
|
|
41
42
|
import {
|
|
42
43
|
bindChatToSession,
|
|
43
44
|
unbindChatFromSession,
|
|
@@ -791,10 +792,77 @@ export async function runAgentSession(
|
|
|
791
792
|
running: true,
|
|
792
793
|
});
|
|
793
794
|
|
|
795
|
+
// 在覆盖 stream state 前,先终结上一轮的展示卡片并发送最终回复。
|
|
796
|
+
// 竞态根因:上一轮 finally 写入 "done" 状态后,200ms setTimeout 即启动
|
|
797
|
+
// 新一轮 runAgentSession,立即覆盖为 "running" 状态并 kill 旧 display
|
|
798
|
+
// loop。旧 loop 的 3s 间隔 tick 只有约 6.7% 概率在 200ms 窗口内命中,
|
|
799
|
+
// 导致 finalReply 丢失、完成卡片空白。此处主动读取上一轮终端状态完成
|
|
800
|
+
// 卡片终结和回复发送,不依赖 display loop 时序,保证"先发完上一个回答
|
|
801
|
+
// 再开始缓存问题对应的任务"。
|
|
802
|
+
const prevState = await readStreamState(sessionId);
|
|
803
|
+
if (prevState && prevState.status !== "running") {
|
|
804
|
+
const displayChatId = pickDisplayChat(sessionId);
|
|
805
|
+
if (displayChatId) {
|
|
806
|
+
const pp = platformForChat(displayChatId);
|
|
807
|
+
const display = displayCards.get(displayChatId);
|
|
808
|
+
const oldLoopExisted = displayLoops.has(sessionId);
|
|
809
|
+
|
|
810
|
+
if (display && pp) {
|
|
811
|
+
// 旧 display loop 被 kill 前来不及终结卡片 → 现在终结
|
|
812
|
+
while (display.cardBusy) await new Promise(r => setTimeout(r, 20));
|
|
813
|
+
const nextSeq = display.sequence + 1;
|
|
814
|
+
const headerTitle = prevState.status === "stopped" ? "已停止" : "完成";
|
|
815
|
+
const headerTemplate = prevState.status === "stopped" ? "red" : undefined;
|
|
816
|
+
const cardContent = prevState.accumulatedContent || " ";
|
|
817
|
+
const doneCard = buildProgressCard(cardContent, { showStop: false, headerTitle, headerTemplate });
|
|
818
|
+
await pp.cardUpdate(display.cardId, doneCard, nextSeq).catch(() => {});
|
|
819
|
+
displayCards.delete(displayChatId);
|
|
820
|
+
|
|
821
|
+
// 持久化:标记上一轮所有卡片为终态
|
|
822
|
+
const finalStatus = prevState.status === "stopped" ? "stopped" : "done";
|
|
823
|
+
finalizeTurnCards(sessionId, prevState.turnCount, finalStatus).catch(() => {});
|
|
824
|
+
|
|
825
|
+
if (prevState.finalReply) {
|
|
826
|
+
await pp.sendText(displayChatId, prevState.finalReply).catch(() => {});
|
|
827
|
+
}
|
|
828
|
+
pp.setChatAvatar(displayChatId, prevState.tool, "idle").catch(() => {});
|
|
829
|
+
} else if (oldLoopExisted && pp && prevState.finalReply) {
|
|
830
|
+
// display loop 存在但尚未创建卡片(极快轮次),至少发送 finalReply
|
|
831
|
+
await pp.sendText(displayChatId, prevState.finalReply).catch(() => {});
|
|
832
|
+
}
|
|
833
|
+
// else: display loop 已自行终结(displayCards 无记录)→ 无需处理
|
|
834
|
+
}
|
|
835
|
+
}
|
|
836
|
+
|
|
794
837
|
// 初始化 stream-state.json
|
|
795
838
|
const initialState = createEmptyStreamState(sessionId, cwd, tool, nextTurnCount);
|
|
796
839
|
await writeStreamState(initialState);
|
|
797
840
|
|
|
841
|
+
// 为新 turn 创建第一张展示卡片,同时注册到 turn-cards 持久化。
|
|
842
|
+
// 放在 ensureDisplayLoop 之前,确保卡片立即可见(不等 3s 首次 tick)。
|
|
843
|
+
const displayChatIdForNew = pickDisplayChat(sessionId);
|
|
844
|
+
if (displayChatIdForNew) {
|
|
845
|
+
const ppNew = platformForChat(displayChatIdForNew);
|
|
846
|
+
if (ppNew && ppNew.kind !== "wechat") {
|
|
847
|
+
const cardId = await ppNew.cardCreate(
|
|
848
|
+
buildProgressCard("", { showStop: true, headerTitle: "生成中..." }),
|
|
849
|
+
).catch(() => null);
|
|
850
|
+
if (cardId) {
|
|
851
|
+
await ppNew.cardSend(displayChatIdForNew, cardId).catch(() => null);
|
|
852
|
+
displayCards.set(displayChatIdForNew, {
|
|
853
|
+
cardId,
|
|
854
|
+
sequence: 1,
|
|
855
|
+
cardBusy: false,
|
|
856
|
+
cardCreatedAt: Date.now(),
|
|
857
|
+
lastSentContent: "",
|
|
858
|
+
streamErrorNotified: false,
|
|
859
|
+
turnCount: nextTurnCount,
|
|
860
|
+
});
|
|
861
|
+
addCardToTurn(sessionId, nextTurnCount, cardId).catch(() => {});
|
|
862
|
+
}
|
|
863
|
+
}
|
|
864
|
+
}
|
|
865
|
+
|
|
798
866
|
// 启动 display loop
|
|
799
867
|
ensureDisplayLoop(sessionId);
|
|
800
868
|
|
|
@@ -1040,6 +1108,8 @@ export function ensureDisplayLoop(sessionId: string): void {
|
|
|
1040
1108
|
const cardContent = state.accumulatedContent || " ";
|
|
1041
1109
|
const doneCard = buildProgressCard(cardContent, { showStop: false, headerTitle, headerTemplate });
|
|
1042
1110
|
await p.cardUpdate(display.cardId, doneCard, nextSeq).catch(() => {});
|
|
1111
|
+
const finalSt = state.status === "stopped" ? "stopped" : "done";
|
|
1112
|
+
finalizeTurnCards(sessionId, state.turnCount, finalSt).catch(() => {});
|
|
1043
1113
|
displayCards.delete(chatId);
|
|
1044
1114
|
}
|
|
1045
1115
|
if (state.finalReply) {
|
|
@@ -1106,6 +1176,7 @@ export function ensureDisplayLoop(sessionId: string): void {
|
|
|
1106
1176
|
} else {
|
|
1107
1177
|
// 非 WeChat: 卡片流程
|
|
1108
1178
|
if (!display) {
|
|
1179
|
+
// 兜底:runAgentSession 应已创建卡片,若没有则补建
|
|
1109
1180
|
const cardId = await p.cardCreate(buildProgressCard("", { showStop: true, headerTitle: "生成中..." })).catch(() => null);
|
|
1110
1181
|
if (cardId) {
|
|
1111
1182
|
await p.cardSend(chatId, cardId).catch(() => null);
|
|
@@ -1116,57 +1187,38 @@ export function ensureDisplayLoop(sessionId: string): void {
|
|
|
1116
1187
|
cardCreatedAt: Date.now(),
|
|
1117
1188
|
lastSentContent: "",
|
|
1118
1189
|
streamErrorNotified: false,
|
|
1119
|
-
|
|
1190
|
+
turnCount: state.turnCount,
|
|
1120
1191
|
});
|
|
1192
|
+
addCardToTurn(sessionId, state.turnCount, cardId).catch(() => {});
|
|
1121
1193
|
}
|
|
1122
1194
|
} else {
|
|
1123
1195
|
if (display.cardBusy) return;
|
|
1124
1196
|
|
|
1125
|
-
//
|
|
1126
|
-
//
|
|
1127
|
-
//
|
|
1128
|
-
if (display.
|
|
1129
|
-
display.
|
|
1130
|
-
|
|
1131
|
-
const doneSeq = display.sequence + 1;
|
|
1132
|
-
const doneCard = buildProgressCard("", { showStop: false, headerTitle: "完成" });
|
|
1133
|
-
await p.cardUpdate(display.cardId, doneCard, doneSeq).catch(() => {});
|
|
1134
|
-
displayCards.delete(chatId);
|
|
1135
|
-
} catch (_) { /* ignore */ }
|
|
1136
|
-
display.cardBusy = false;
|
|
1137
|
-
// 新建卡片
|
|
1138
|
-
const cardId = await p.cardCreate(buildProgressCard("", { showStop: true, headerTitle: "生成中..." })).catch(() => null);
|
|
1139
|
-
if (cardId) {
|
|
1140
|
-
await p.cardSend(chatId, cardId).catch(() => null);
|
|
1141
|
-
displayCards.set(chatId, {
|
|
1142
|
-
cardId,
|
|
1143
|
-
sequence: 1,
|
|
1144
|
-
cardBusy: false,
|
|
1145
|
-
cardCreatedAt: Date.now(),
|
|
1146
|
-
lastSentContent: "",
|
|
1147
|
-
streamErrorNotified: false,
|
|
1148
|
-
lastTurnCount: state.turnCount,
|
|
1149
|
-
});
|
|
1150
|
-
}
|
|
1197
|
+
// 守卫:display 归属的 turn 与当前 stream state 不一致。
|
|
1198
|
+
// 正常流程 runAgentSession 会终结旧 turn 并建新卡,
|
|
1199
|
+
// 此处仅兜底处理异常情况(如进程重启后 display 残留)。
|
|
1200
|
+
if (display.turnCount !== state.turnCount) {
|
|
1201
|
+
console.log(`[${ts()}] [DISPLAY] turn mismatch for ${chatId}: display.turnCount=${display.turnCount} state.turnCount=${state.turnCount}, resetting`);
|
|
1202
|
+
displayCards.delete(chatId);
|
|
1151
1203
|
return;
|
|
1152
1204
|
}
|
|
1153
1205
|
|
|
1154
|
-
// 更新当前 turn 的 lastTurnCount
|
|
1155
|
-
display.lastTurnCount = state.turnCount;
|
|
1156
|
-
|
|
1157
1206
|
// 卡片轮转
|
|
1158
1207
|
if (Date.now() - display.cardCreatedAt > CARD_ROTATE_MS) {
|
|
1159
1208
|
display.cardBusy = true;
|
|
1160
1209
|
try {
|
|
1161
1210
|
const oldSeqBase = display.sequence;
|
|
1162
|
-
const
|
|
1211
|
+
const oldContent = state.accumulatedContent + state.finalReply;
|
|
1212
|
+
const oldCard = buildProgressCard(truncateContent(oldContent) || " ", { showStop: false, headerTitle: "完成" });
|
|
1163
1213
|
await p.cardUpdate(display.cardId, oldCard, oldSeqBase + 1).catch(() => {});
|
|
1214
|
+
markCardDone(sessionId, display.turnCount, display.cardId).catch(() => {});
|
|
1164
1215
|
const newCardId = await p.cardCreate(buildProgressCard(
|
|
1165
1216
|
"",
|
|
1166
1217
|
{ showStop: true, headerTitle: "生成中..." },
|
|
1167
1218
|
));
|
|
1168
1219
|
if (newCardId) {
|
|
1169
1220
|
await p.cardSend(chatId, newCardId);
|
|
1221
|
+
addCardToTurn(sessionId, display.turnCount, newCardId).catch(() => {});
|
|
1170
1222
|
display.cardId = newCardId;
|
|
1171
1223
|
display.sequence = 1;
|
|
1172
1224
|
display.cardCreatedAt = Date.now();
|
|
@@ -0,0 +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
|
+
}
|
|
118
|
+
}
|