chatccc 0.1.5 → 0.2.0
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/README.md +10 -12
- package/bin/chatccc.mjs +26 -9
- package/package.json +2 -1
- package/src/cardkit.ts +157 -157
- package/src/cards.ts +214 -169
- package/src/config.ts +223 -80
- package/src/exit-banner.ts +23 -0
- package/src/feishu-api.ts +248 -243
- package/src/index.ts +708 -599
- package/src/session.ts +455 -409
- package/src/shared.ts +285 -185
package/src/session.ts
CHANGED
|
@@ -1,410 +1,456 @@
|
|
|
1
|
-
import { getSessionInfo, unstable_v2_createSession, unstable_v2_resumeSession } from "@anthropic-ai/claude-agent-sdk";
|
|
2
|
-
|
|
3
|
-
import {
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
});
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
}
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
let
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
const
|
|
279
|
-
const
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
const
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
if (
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
1
|
+
import { getSessionInfo, unstable_v2_createSession, unstable_v2_resumeSession } from "@anthropic-ai/claude-agent-sdk";
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
CLAUDE_EFFORT,
|
|
5
|
+
CLAUDE_MODEL,
|
|
6
|
+
anthropicConfigDisplay,
|
|
7
|
+
fileLog,
|
|
8
|
+
getDefaultCwd,
|
|
9
|
+
isSdkAnthropicDefault,
|
|
10
|
+
ts,
|
|
11
|
+
} from "./config.ts";
|
|
12
|
+
import { buildThinkingCardV2, getToolEmoji, truncateContent } from "./cards.ts";
|
|
13
|
+
import {
|
|
14
|
+
createCardKitCard,
|
|
15
|
+
sendCardKitMessage,
|
|
16
|
+
updateCardKitCard,
|
|
17
|
+
} from "./cardkit.ts";
|
|
18
|
+
import { sendTextReply } from "./feishu-api.ts";
|
|
19
|
+
|
|
20
|
+
// ---------------------------------------------------------------------------
|
|
21
|
+
// Shared state (imported by index.ts)
|
|
22
|
+
// ---------------------------------------------------------------------------
|
|
23
|
+
|
|
24
|
+
export const processedMessages = new Set<string>();
|
|
25
|
+
export const MAX_PROCESSED = 5000;
|
|
26
|
+
|
|
27
|
+
export let sessionGen = 0;
|
|
28
|
+
export const chatSessionMap = new Map<string, {
|
|
29
|
+
gen: number;
|
|
30
|
+
close: () => void;
|
|
31
|
+
cardId: string | null;
|
|
32
|
+
stopped: boolean;
|
|
33
|
+
accumulatedThinking: string;
|
|
34
|
+
finalText: string;
|
|
35
|
+
spinnerTimer: ReturnType<typeof setInterval> | null;
|
|
36
|
+
msgTimestamp: number;
|
|
37
|
+
sequence: number;
|
|
38
|
+
cardBusy: boolean;
|
|
39
|
+
}>();
|
|
40
|
+
|
|
41
|
+
// 持久化会话信息,流式结束后不清除,供 /status 查询
|
|
42
|
+
export const sessionInfoMap = new Map<string, {
|
|
43
|
+
sessionId: string;
|
|
44
|
+
turnCount: number;
|
|
45
|
+
lastContextTokens: number;
|
|
46
|
+
startTime: number;
|
|
47
|
+
model: string;
|
|
48
|
+
effort: string;
|
|
49
|
+
}>();
|
|
50
|
+
|
|
51
|
+
export function resetState(): void {
|
|
52
|
+
for (const entry of chatSessionMap.values()) {
|
|
53
|
+
if (entry.spinnerTimer) clearInterval(entry.spinnerTimer);
|
|
54
|
+
try { entry.close(); } catch { /* ignore */ }
|
|
55
|
+
}
|
|
56
|
+
chatSessionMap.clear();
|
|
57
|
+
sessionInfoMap.clear();
|
|
58
|
+
processedMessages.clear();
|
|
59
|
+
console.log(`[${ts()}] [RESET] State cleared (dedup + active sessions)`);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// ---------------------------------------------------------------------------
|
|
63
|
+
// Claude session management
|
|
64
|
+
// ---------------------------------------------------------------------------
|
|
65
|
+
|
|
66
|
+
function claudeSdkSessionOptions(cwd: string): Record<string, unknown> {
|
|
67
|
+
const o: Record<string, unknown> = {
|
|
68
|
+
cwd,
|
|
69
|
+
permissionMode: "bypassPermissions",
|
|
70
|
+
allowDangerouslySkipPermissions: true,
|
|
71
|
+
autoCompactEnabled: true,
|
|
72
|
+
};
|
|
73
|
+
if (!isSdkAnthropicDefault(CLAUDE_MODEL)) o.model = CLAUDE_MODEL;
|
|
74
|
+
if (!isSdkAnthropicDefault(CLAUDE_EFFORT)) o.effort = CLAUDE_EFFORT;
|
|
75
|
+
return o;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
export async function initClaudeSession(): Promise<string> {
|
|
79
|
+
const cwd = await getDefaultCwd();
|
|
80
|
+
console.log(
|
|
81
|
+
`[${ts()}] [STEP 1/5] Creating Claude session via SDK (model=${anthropicConfigDisplay(CLAUDE_MODEL)}, effort=${anthropicConfigDisplay(CLAUDE_EFFORT)}, cwd=${cwd})`
|
|
82
|
+
);
|
|
83
|
+
|
|
84
|
+
const session = unstable_v2_createSession(claudeSdkSessionOptions(cwd) as any);
|
|
85
|
+
|
|
86
|
+
await session.send("ok");
|
|
87
|
+
|
|
88
|
+
const stream = session.stream();
|
|
89
|
+
|
|
90
|
+
const first = await stream.next();
|
|
91
|
+
if (first.done || !(first.value as { session_id?: string }).session_id) {
|
|
92
|
+
session.close();
|
|
93
|
+
throw new Error("No session ID in Claude init event");
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
const initMsg = first.value as { session_id: string };
|
|
97
|
+
const sessionId = initMsg.session_id;
|
|
98
|
+
console.log(`[${ts()}] → sessionId: ${sessionId}`);
|
|
99
|
+
|
|
100
|
+
(async () => {
|
|
101
|
+
try {
|
|
102
|
+
for await (const _msg of stream) {
|
|
103
|
+
// 静默消费,不做额外处理
|
|
104
|
+
}
|
|
105
|
+
} catch {
|
|
106
|
+
// stream 异常不阻塞主流程
|
|
107
|
+
} finally {
|
|
108
|
+
session.close();
|
|
109
|
+
}
|
|
110
|
+
})();
|
|
111
|
+
|
|
112
|
+
return sessionId;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
export async function resumeAndPrompt(
|
|
116
|
+
sessionId: string,
|
|
117
|
+
userText: string,
|
|
118
|
+
token: string,
|
|
119
|
+
chatId: string,
|
|
120
|
+
msgTimestamp: number
|
|
121
|
+
): Promise<void> {
|
|
122
|
+
const cwd = (await getSessionInfo(sessionId))?.cwd ?? (await getDefaultCwd());
|
|
123
|
+
console.log(
|
|
124
|
+
`[${ts()}] Resuming Claude session: ${sessionId} (model=${anthropicConfigDisplay(CLAUDE_MODEL)}, effort=${anthropicConfigDisplay(CLAUDE_EFFORT)}, cwd=${cwd})`
|
|
125
|
+
);
|
|
126
|
+
|
|
127
|
+
const session = unstable_v2_resumeSession(sessionId, claudeSdkSessionOptions(cwd) as any);
|
|
128
|
+
|
|
129
|
+
let cardId: string | null = null;
|
|
130
|
+
chatSessionMap.set(chatId, {
|
|
131
|
+
gen: ++sessionGen,
|
|
132
|
+
close: () => session.close(),
|
|
133
|
+
cardId: null,
|
|
134
|
+
stopped: false,
|
|
135
|
+
accumulatedThinking: "",
|
|
136
|
+
finalText: "",
|
|
137
|
+
spinnerTimer: null,
|
|
138
|
+
msgTimestamp,
|
|
139
|
+
sequence: 0,
|
|
140
|
+
cardBusy: false,
|
|
141
|
+
});
|
|
142
|
+
const myGen = sessionGen;
|
|
143
|
+
|
|
144
|
+
// 更新持久化会话信息
|
|
145
|
+
const now = Date.now();
|
|
146
|
+
const existingInfo = sessionInfoMap.get(chatId);
|
|
147
|
+
sessionInfoMap.set(chatId, {
|
|
148
|
+
sessionId,
|
|
149
|
+
turnCount: (existingInfo?.turnCount ?? 0) + 1,
|
|
150
|
+
lastContextTokens: existingInfo?.lastContextTokens ?? 0,
|
|
151
|
+
startTime: now,
|
|
152
|
+
model: anthropicConfigDisplay(CLAUDE_MODEL),
|
|
153
|
+
effort: anthropicConfigDisplay(CLAUDE_EFFORT),
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
await session.send(userText);
|
|
157
|
+
|
|
158
|
+
cardId = await createCardKitCard(token, buildThinkingCardV2("", { showStop: true, headerTitle: "生成中..." })).catch((err) => {
|
|
159
|
+
console.error(`[${ts()}] [CARDIKT] createCard FAIL: chatId=${chatId} ${(err as Error).message}`);
|
|
160
|
+
fileLog.flush();
|
|
161
|
+
sendTextReply(token, chatId, "⚠️ 流式卡片创建失败(可能因限流),将使用文本回复。").catch(() => {});
|
|
162
|
+
return null;
|
|
163
|
+
});
|
|
164
|
+
if (cardId) {
|
|
165
|
+
const cEntry = chatSessionMap.get(chatId);
|
|
166
|
+
if (cEntry) { cEntry.cardId = cardId; cEntry.sequence = 1; }
|
|
167
|
+
const sendOk = await sendCardKitMessage(token, chatId, cardId).catch((err) => {
|
|
168
|
+
console.error(`[${ts()}] [CARDIKT] sendMessage FAIL: chatId=${chatId} cardId=${cardId} ${(err as Error).message}`);
|
|
169
|
+
fileLog.flush();
|
|
170
|
+
return false;
|
|
171
|
+
});
|
|
172
|
+
if (!sendOk) {
|
|
173
|
+
sendTextReply(token, chatId, "⚠️ 卡片发送失败,将使用文本回复。").catch(() => {});
|
|
174
|
+
cardId = null;
|
|
175
|
+
if (cEntry) { cEntry.cardId = null; cEntry.sequence = 0; }
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
const stream = session.stream();
|
|
180
|
+
|
|
181
|
+
let thinkingCount = 0;
|
|
182
|
+
let accumulatedThinking = "";
|
|
183
|
+
let finalText = "";
|
|
184
|
+
|
|
185
|
+
let cardCreatedAt = Date.now();
|
|
186
|
+
const CARD_ROTATE_MS = 9 * 60 * 1000; // 飞书 CardKit 流式超时 10 分钟,提前 1 分钟切换
|
|
187
|
+
|
|
188
|
+
let dotCount = 0;
|
|
189
|
+
let lastSentContent = "";
|
|
190
|
+
let streamErrorNotified = false;
|
|
191
|
+
let healthLogTicks = 0;
|
|
192
|
+
const sendInterval = cardId ? setInterval(async () => {
|
|
193
|
+
const cEntry = chatSessionMap.get(chatId);
|
|
194
|
+
if (!cEntry || cEntry.stopped || cEntry.cardBusy) return;
|
|
195
|
+
if (cEntry.cardId !== cardId) return;
|
|
196
|
+
|
|
197
|
+
// 9 分钟超时:主动结束当前卡片流,创建新卡片继续
|
|
198
|
+
if (Date.now() - cardCreatedAt > CARD_ROTATE_MS) {
|
|
199
|
+
cEntry.cardBusy = true;
|
|
200
|
+
const oldCardId = cardId;
|
|
201
|
+
try {
|
|
202
|
+
// 1. 用当前累积内容更新旧卡片为静态版本
|
|
203
|
+
const oldSeqBase = cEntry.sequence;
|
|
204
|
+
const oldDisplay = truncateContent(accumulatedThinking + finalText) || "处理中...";
|
|
205
|
+
const oldCard = buildThinkingCardV2(oldDisplay, { showStop: false, headerTitle: "生成中...(上轮)" });
|
|
206
|
+
await updateCardKitCard(token, oldCardId!, oldCard, oldSeqBase + 1).catch(() => {});
|
|
207
|
+
// 2. 创建新卡片并发送
|
|
208
|
+
const newCardId = await createCardKitCard(token, buildThinkingCardV2("", { showStop: true, headerTitle: "生成中..." }));
|
|
209
|
+
if (!newCardId) throw new Error("createCardKitCard returned empty");
|
|
210
|
+
await sendCardKitMessage(token, chatId, newCardId);
|
|
211
|
+
// 3. 切换到新卡片
|
|
212
|
+
cardId = newCardId;
|
|
213
|
+
cEntry.cardId = newCardId;
|
|
214
|
+
cEntry.sequence = 1;
|
|
215
|
+
cardCreatedAt = Date.now();
|
|
216
|
+
lastSentContent = "";
|
|
217
|
+
streamErrorNotified = false;
|
|
218
|
+
console.log(`[${ts()}] [CARDIKT] rotated: old=${oldCardId} new=${newCardId} (9min timeout)`);
|
|
219
|
+
} catch (err) {
|
|
220
|
+
console.error(`[${ts()}] [CARDIKT] rotation FAIL: ${(err as Error).message}`);
|
|
221
|
+
} finally {
|
|
222
|
+
cEntry.cardBusy = false;
|
|
223
|
+
}
|
|
224
|
+
return;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
dotCount = (dotCount % 9) + 1;
|
|
228
|
+
const content = truncateContent(accumulatedThinking + finalText + "\n" + "。".repeat(dotCount));
|
|
229
|
+
if (content === lastSentContent) return;
|
|
230
|
+
|
|
231
|
+
lastSentContent = content;
|
|
232
|
+
cEntry.cardBusy = true;
|
|
233
|
+
const mySeq = cEntry.sequence + 1;
|
|
234
|
+
try {
|
|
235
|
+
const card = buildThinkingCardV2(content, { showStop: true, headerTitle: "生成中..." });
|
|
236
|
+
await updateCardKitCard(token, cardId!, card, mySeq);
|
|
237
|
+
cEntry.sequence = mySeq;
|
|
238
|
+
cEntry.accumulatedThinking = accumulatedThinking;
|
|
239
|
+
streamErrorNotified = false;
|
|
240
|
+
healthLogTicks++;
|
|
241
|
+
if (healthLogTicks % 10 === 0) {
|
|
242
|
+
console.log(`[${ts()}] [CARDIKT] update health: seq=${mySeq} thinking=${accumulatedThinking.length}chars text=${finalText.length}chars cardAge=${Math.round((Date.now() - cardCreatedAt) / 1000)}s`);
|
|
243
|
+
}
|
|
244
|
+
} catch (err) {
|
|
245
|
+
console.error(`[${ts()}] CardKit update error: chatId=${chatId} cardId=${cardId} seq=${mySeq} ${(err as Error).message}`);
|
|
246
|
+
if (!streamErrorNotified) {
|
|
247
|
+
streamErrorNotified = true;
|
|
248
|
+
sendTextReply(token, chatId, "⚠️ 卡片更新失败,结果将以文本形式发送。").catch(() => {});
|
|
249
|
+
}
|
|
250
|
+
} finally {
|
|
251
|
+
cEntry.cardBusy = false;
|
|
252
|
+
}
|
|
253
|
+
}, 3000) : null;
|
|
254
|
+
if (sendInterval) {
|
|
255
|
+
const entry = chatSessionMap.get(chatId);
|
|
256
|
+
if (entry) entry.spinnerTimer = sendInterval;
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
try {
|
|
260
|
+
for await (const msg of stream) {
|
|
261
|
+
const sdkMsg = msg as {
|
|
262
|
+
type?: string;
|
|
263
|
+
message?: { content?: Array<{ type: string; thinking?: string; text?: string; name?: string; input?: unknown; tool_use_id?: string; content?: unknown; is_error?: boolean }> };
|
|
264
|
+
};
|
|
265
|
+
if ((sdkMsg.type === "assistant" || sdkMsg.type === "user") && sdkMsg.message?.content) {
|
|
266
|
+
for (const block of sdkMsg.message.content) {
|
|
267
|
+
|
|
268
|
+
if (block.type === "thinking" && block.thinking) {
|
|
269
|
+
thinkingCount++;
|
|
270
|
+
accumulatedThinking += block.thinking;
|
|
271
|
+
} else if (block.type === "tool_use") {
|
|
272
|
+
const toolName = (block as { name?: string }).name ?? "unknown";
|
|
273
|
+
const toolInput = (block as { input?: unknown }).input;
|
|
274
|
+
const inputStr = typeof toolInput === "object" ? JSON.stringify(toolInput) : String(toolInput ?? "");
|
|
275
|
+
const shortInput = inputStr.length > 300 ? inputStr.slice(0, 300) + "..." : inputStr;
|
|
276
|
+
accumulatedThinking += `\n\n${getToolEmoji(toolName)} **${toolName}**\n\`${shortInput}\`\n`;
|
|
277
|
+
} else if (block.type === "tool_result") {
|
|
278
|
+
const toolUseId = (block as { tool_use_id?: string }).tool_use_id ?? "";
|
|
279
|
+
const resultContent = (block as { content?: unknown }).content;
|
|
280
|
+
let resultStr = "";
|
|
281
|
+
if (typeof resultContent === "string") {
|
|
282
|
+
resultStr = resultContent;
|
|
283
|
+
} else if (Array.isArray(resultContent)) {
|
|
284
|
+
resultStr = resultContent.map((c: { type?: string; text?: string }) => c.text ?? "").join("");
|
|
285
|
+
} else if (resultContent) {
|
|
286
|
+
resultStr = JSON.stringify(resultContent);
|
|
287
|
+
}
|
|
288
|
+
const shortResult = resultStr.length > 200 ? resultStr.slice(0, 200) + "..." : resultStr;
|
|
289
|
+
const isError = (block as { is_error?: boolean }).is_error;
|
|
290
|
+
const icon = isError ? "❌" : "✅";
|
|
291
|
+
accumulatedThinking += `${icon} *${toolUseId.slice(-6)}*: ${shortResult}\n`;
|
|
292
|
+
} else if (block.type === "redacted_thinking") {
|
|
293
|
+
accumulatedThinking += "\n\n⚠️ 思考内容被安全过滤\n";
|
|
294
|
+
} else if (block.type === "search_result") {
|
|
295
|
+
const searchQuery = (block as { query?: string }).query ?? "";
|
|
296
|
+
accumulatedThinking += `\n\n🔍 联网搜索: **${searchQuery}**\n`;
|
|
297
|
+
} else if (block.type === "text" && block.text) {
|
|
298
|
+
finalText += block.text;
|
|
299
|
+
const entry = chatSessionMap.get(chatId);
|
|
300
|
+
if (entry) entry.finalText = finalText;
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
} else if (sdkMsg.type === "system" && (sdkMsg as { subtype?: string }).subtype === "compact_boundary") {
|
|
304
|
+
const compactMeta = (sdkMsg as { compact_metadata?: { trigger?: string; pre_tokens?: number; post_tokens?: number } }).compact_metadata;
|
|
305
|
+
if (compactMeta) {
|
|
306
|
+
const triggerLabel = compactMeta.trigger === "manual" ? "手动" : "自动";
|
|
307
|
+
accumulatedThinking += `\n\n🔄 上下文压缩(${triggerLabel}): **${compactMeta.pre_tokens}** → **${compactMeta.post_tokens}** tokens\n`;
|
|
308
|
+
// 更新持久化上下文 token 数
|
|
309
|
+
if (compactMeta.post_tokens) {
|
|
310
|
+
const info = sessionInfoMap.get(chatId);
|
|
311
|
+
if (info) { info.lastContextTokens = compactMeta.post_tokens; }
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
} catch (streamErr) {
|
|
317
|
+
console.error(`[${ts()}] [STREAM] Error in stream loop: ${(streamErr as Error).message}`);
|
|
318
|
+
} finally {
|
|
319
|
+
if (sendInterval) clearInterval(sendInterval);
|
|
320
|
+
session.close();
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
const cEntry = chatSessionMap.get(chatId);
|
|
324
|
+
if (!cEntry || cEntry.gen !== myGen) return;
|
|
325
|
+
const wasStopped = cEntry.stopped;
|
|
326
|
+
chatSessionMap.delete(chatId);
|
|
327
|
+
|
|
328
|
+
if (cardId && accumulatedThinking) {
|
|
329
|
+
while (cEntry.cardBusy) {
|
|
330
|
+
await new Promise(r => setTimeout(r, 20));
|
|
331
|
+
}
|
|
332
|
+
const nextSeq = cEntry.sequence + 1;
|
|
333
|
+
if (wasStopped) {
|
|
334
|
+
const stopCard = buildThinkingCardV2(accumulatedThinking || "已停止", { showStop: false, headerTitle: "已停止", headerTemplate: "red" });
|
|
335
|
+
await updateCardKitCard(token, cardId, stopCard, nextSeq).catch((err) => {
|
|
336
|
+
console.error(`[${ts()}] CardKit finalize: chatId=${chatId} cardId=${cardId} ${(err as Error).message}`);
|
|
337
|
+
fileLog.flush();
|
|
338
|
+
});
|
|
339
|
+
} else {
|
|
340
|
+
const doneCard = buildThinkingCardV2(accumulatedThinking, { showStop: false, headerTitle: "完成" });
|
|
341
|
+
await updateCardKitCard(token, cardId, doneCard, nextSeq).catch((err) => {
|
|
342
|
+
console.error(`[${ts()}] CardKit finalize: chatId=${chatId} cardId=${cardId} ${(err as Error).message}`);
|
|
343
|
+
fileLog.flush();
|
|
344
|
+
sendTextReply(token, chatId, "⚠️ 卡片最终更新失败。").catch(() => {});
|
|
345
|
+
});
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
// Text fallback: if CardKit streaming broke, always send full result as text
|
|
350
|
+
if (wasStopped) {
|
|
351
|
+
if (finalText.trim()) {
|
|
352
|
+
await sendTextReply(token, chatId, finalText.trim()).catch((err) =>
|
|
353
|
+
console.error(`[${ts()}] Failed to send partial text: ${(err as Error).message}`)
|
|
354
|
+
);
|
|
355
|
+
}
|
|
356
|
+
console.log(`[${ts()}] Session ${sessionId} stopped by user (thinking chunks: ${thinkingCount})`);
|
|
357
|
+
return;
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
if (streamErrorNotified) {
|
|
361
|
+
// CardKit streaming failed — send everything as text to ensure user sees the result
|
|
362
|
+
if (accumulatedThinking.trim()) {
|
|
363
|
+
const shortThinking = truncateContent(accumulatedThinking, 30, 4000);
|
|
364
|
+
await sendTextReply(token, chatId, `[思考过程]\n${shortThinking}`).catch((err) =>
|
|
365
|
+
console.error(`[${ts()}] Failed to send thinking fallback: ${(err as Error).message}`)
|
|
366
|
+
);
|
|
367
|
+
}
|
|
368
|
+
if (finalText.trim()) {
|
|
369
|
+
await sendTextReply(token, chatId, finalText.trim()).catch((err) =>
|
|
370
|
+
console.error(`[${ts()}] Failed to send text fallback: ${(err as Error).message}`)
|
|
371
|
+
);
|
|
372
|
+
}
|
|
373
|
+
} else {
|
|
374
|
+
// Normal path: card streaming worked fine
|
|
375
|
+
if (finalText.trim()) {
|
|
376
|
+
await sendTextReply(token, chatId, finalText.trim()).catch((err) =>
|
|
377
|
+
console.error(`[${ts()}] Failed to send final text: ${(err as Error).message}`)
|
|
378
|
+
);
|
|
379
|
+
} else if (!cardId && accumulatedThinking.trim()) {
|
|
380
|
+
const shortThinking = truncateContent(accumulatedThinking, 30, 4000);
|
|
381
|
+
await sendTextReply(token, chatId, `[思考过程]\n${shortThinking}`).catch((err) =>
|
|
382
|
+
console.error(`[${ts()}] Failed to send thinking text: ${(err as Error).message}`)
|
|
383
|
+
);
|
|
384
|
+
}
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
console.log(`[${ts()}] Session ${sessionId} stream complete (thinking chunks: ${thinkingCount})`);
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
// ---------------------------------------------------------------------------
|
|
391
|
+
// Session status query (供 /status 命令使用)
|
|
392
|
+
// ---------------------------------------------------------------------------
|
|
393
|
+
|
|
394
|
+
export interface SessionStatus {
|
|
395
|
+
sessionId: string;
|
|
396
|
+
running: boolean;
|
|
397
|
+
turnCount: number;
|
|
398
|
+
lastContextTokens: number;
|
|
399
|
+
startTime: number;
|
|
400
|
+
model: string;
|
|
401
|
+
effort: string;
|
|
402
|
+
accumulatedLength: number;
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
export function getSessionStatus(chatId: string): SessionStatus | null {
|
|
406
|
+
const info = sessionInfoMap.get(chatId);
|
|
407
|
+
if (!info) return null;
|
|
408
|
+
|
|
409
|
+
const active = chatSessionMap.get(chatId);
|
|
410
|
+
return {
|
|
411
|
+
sessionId: info.sessionId,
|
|
412
|
+
running: active !== undefined && !active.stopped,
|
|
413
|
+
turnCount: info.turnCount,
|
|
414
|
+
lastContextTokens: info.lastContextTokens,
|
|
415
|
+
startTime: info.startTime,
|
|
416
|
+
model: anthropicConfigDisplay(info.model),
|
|
417
|
+
effort: anthropicConfigDisplay(info.effort),
|
|
418
|
+
accumulatedLength: active ? active.accumulatedThinking.length + active.finalText.length : 0,
|
|
419
|
+
};
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
/**
|
|
423
|
+
* 获取所有已记录的会话状态列表(供 /sessions 命令使用)
|
|
424
|
+
*/
|
|
425
|
+
export function getAllSessionsStatus(): Array<{
|
|
426
|
+
chatId: string;
|
|
427
|
+
sessionId: string;
|
|
428
|
+
active: boolean;
|
|
429
|
+
turnCount: number;
|
|
430
|
+
startTime: number;
|
|
431
|
+
model: string;
|
|
432
|
+
effort: string;
|
|
433
|
+
}> {
|
|
434
|
+
const result: Array<{
|
|
435
|
+
chatId: string;
|
|
436
|
+
sessionId: string;
|
|
437
|
+
active: boolean;
|
|
438
|
+
turnCount: number;
|
|
439
|
+
startTime: number;
|
|
440
|
+
model: string;
|
|
441
|
+
effort: string;
|
|
442
|
+
}> = [];
|
|
443
|
+
for (const [chatId, info] of sessionInfoMap) {
|
|
444
|
+
const active = chatSessionMap.get(chatId);
|
|
445
|
+
result.push({
|
|
446
|
+
chatId,
|
|
447
|
+
sessionId: info.sessionId,
|
|
448
|
+
active: active !== undefined && !active.stopped,
|
|
449
|
+
turnCount: info.turnCount,
|
|
450
|
+
startTime: info.startTime,
|
|
451
|
+
model: info.model,
|
|
452
|
+
effort: info.effort,
|
|
453
|
+
});
|
|
454
|
+
}
|
|
455
|
+
return result;
|
|
410
456
|
}
|