chatccc 0.2.4 → 0.2.5
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 +1 -1
- package/src/__tests__/adapter-interface.test.ts +152 -0
- package/src/__tests__/claude-adapter.test.ts +529 -0
- package/src/__tests__/session.test.ts +145 -0
- package/src/adapters/adapter-interface.ts +127 -0
- package/src/adapters/claude-adapter.ts +257 -0
- package/src/config.ts +4 -1
- package/src/index.ts +7 -4
- package/src/session.ts +450 -455
package/src/session.ts
CHANGED
|
@@ -1,456 +1,451 @@
|
|
|
1
|
-
import {
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
} from "./
|
|
18
|
-
import {
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
//
|
|
22
|
-
//
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
export const
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
export
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
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
|
-
//
|
|
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
|
-
|
|
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
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
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 (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 (content chunks: ${chunkCount})`);
|
|
357
|
-
return;
|
|
358
|
-
}
|
|
359
|
-
|
|
360
|
-
if (streamErrorNotified) {
|
|
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
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
model: info.model,
|
|
452
|
-
effort: info.effort,
|
|
453
|
-
});
|
|
454
|
-
}
|
|
455
|
-
return result;
|
|
1
|
+
import {
|
|
2
|
+
CLAUDE_EFFORT,
|
|
3
|
+
CLAUDE_MODEL,
|
|
4
|
+
anthropicConfigDisplay,
|
|
5
|
+
fileLog,
|
|
6
|
+
getDefaultCwd,
|
|
7
|
+
isSdkAnthropicDefault,
|
|
8
|
+
ts,
|
|
9
|
+
} from "./config.ts";
|
|
10
|
+
import { buildProgressCard, getToolEmoji, truncateContent } from "./cards.ts";
|
|
11
|
+
import {
|
|
12
|
+
createCardKitCard,
|
|
13
|
+
sendCardKitMessage,
|
|
14
|
+
updateCardKitCard,
|
|
15
|
+
} from "./cardkit.ts";
|
|
16
|
+
import { sendTextReply } from "./feishu-api.ts";
|
|
17
|
+
import type { UnifiedBlock } from "./adapters/adapter-interface.ts";
|
|
18
|
+
import type { ToolAdapter } from "./adapters/adapter-interface.ts";
|
|
19
|
+
import { createClaudeAdapter } from "./adapters/claude-adapter.ts";
|
|
20
|
+
|
|
21
|
+
// ---------------------------------------------------------------------------
|
|
22
|
+
// Shared state (imported by index.ts)
|
|
23
|
+
// ---------------------------------------------------------------------------
|
|
24
|
+
|
|
25
|
+
export const processedMessages = new Set<string>();
|
|
26
|
+
export const MAX_PROCESSED = 5000;
|
|
27
|
+
|
|
28
|
+
export let sessionGen = 0;
|
|
29
|
+
export const chatSessionMap = new Map<string, {
|
|
30
|
+
gen: number;
|
|
31
|
+
close: () => void;
|
|
32
|
+
cardId: string | null;
|
|
33
|
+
stopped: boolean;
|
|
34
|
+
accumulatedContent: string;
|
|
35
|
+
finalText: string;
|
|
36
|
+
spinnerTimer: ReturnType<typeof setInterval> | null;
|
|
37
|
+
msgTimestamp: number;
|
|
38
|
+
sequence: number;
|
|
39
|
+
cardBusy: boolean;
|
|
40
|
+
}>();
|
|
41
|
+
|
|
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
|
+
// Adapter singleton
|
|
64
|
+
// ---------------------------------------------------------------------------
|
|
65
|
+
|
|
66
|
+
let adapter: ToolAdapter | null = null;
|
|
67
|
+
|
|
68
|
+
export function initAdapter(): ToolAdapter {
|
|
69
|
+
adapter = createClaudeAdapter({
|
|
70
|
+
model: CLAUDE_MODEL,
|
|
71
|
+
effort: CLAUDE_EFFORT,
|
|
72
|
+
isDefault: isSdkAnthropicDefault,
|
|
73
|
+
});
|
|
74
|
+
return adapter;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
export function getAdapter(): ToolAdapter {
|
|
78
|
+
if (!adapter) throw new Error("Adapter not initialized. Call initAdapter() first.");
|
|
79
|
+
return adapter;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// ---------------------------------------------------------------------------
|
|
83
|
+
// accumulateBlockContent — 将 UnifiedBlock 累积到渲染状态(纯函数,可测试)
|
|
84
|
+
// ---------------------------------------------------------------------------
|
|
85
|
+
|
|
86
|
+
export interface AccumulatorState {
|
|
87
|
+
accumulatedContent: string;
|
|
88
|
+
finalText: string;
|
|
89
|
+
chunkCount: number;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
export function accumulateBlockContent(
|
|
93
|
+
block: UnifiedBlock,
|
|
94
|
+
state: AccumulatorState,
|
|
95
|
+
): void {
|
|
96
|
+
switch (block.type) {
|
|
97
|
+
case "thinking":
|
|
98
|
+
state.chunkCount++;
|
|
99
|
+
state.accumulatedContent += block.thinking;
|
|
100
|
+
break;
|
|
101
|
+
case "tool_use": {
|
|
102
|
+
const inputStr =
|
|
103
|
+
typeof block.input === "object"
|
|
104
|
+
? JSON.stringify(block.input)
|
|
105
|
+
: String(block.input ?? "");
|
|
106
|
+
const shortInput =
|
|
107
|
+
inputStr.length > 300 ? inputStr.slice(0, 300) + "..." : inputStr;
|
|
108
|
+
state.accumulatedContent +=
|
|
109
|
+
`\n\n${getToolEmoji(block.name)} **${block.name}**\n\`${shortInput}\`\n`;
|
|
110
|
+
break;
|
|
111
|
+
}
|
|
112
|
+
case "tool_result": {
|
|
113
|
+
const toolUseId = block.tool_use_id;
|
|
114
|
+
const resultContent = block.content;
|
|
115
|
+
let resultStr = "";
|
|
116
|
+
if (typeof resultContent === "string") {
|
|
117
|
+
resultStr = resultContent;
|
|
118
|
+
} else if (Array.isArray(resultContent)) {
|
|
119
|
+
resultStr = resultContent
|
|
120
|
+
.map((c: { type?: string; text?: string }) => c.text ?? "")
|
|
121
|
+
.join("");
|
|
122
|
+
} else if (resultContent) {
|
|
123
|
+
resultStr = JSON.stringify(resultContent);
|
|
124
|
+
}
|
|
125
|
+
const shortResult =
|
|
126
|
+
resultStr.length > 200 ? resultStr.slice(0, 200) + "..." : resultStr;
|
|
127
|
+
const isError = block.is_error;
|
|
128
|
+
const icon = isError ? "❌" : "✅"; // ❌ : ✅
|
|
129
|
+
state.accumulatedContent +=
|
|
130
|
+
`${icon} *${toolUseId.slice(-6)}*: ${shortResult}\n`;
|
|
131
|
+
break;
|
|
132
|
+
}
|
|
133
|
+
case "redacted_thinking":
|
|
134
|
+
state.accumulatedContent += "\n\n⚠️ 内容被安全过滤\n"; // ⚠️
|
|
135
|
+
break;
|
|
136
|
+
case "search_result":
|
|
137
|
+
state.accumulatedContent +=
|
|
138
|
+
`\n\n🔍 联网搜索: **${block.query}**\n`; // 🔍
|
|
139
|
+
break;
|
|
140
|
+
case "text":
|
|
141
|
+
state.finalText += block.text;
|
|
142
|
+
break;
|
|
143
|
+
case "compact_boundary": {
|
|
144
|
+
const triggerLabel = block.trigger === "manual" ? "手动" : "自动"; // 手动 / 自动
|
|
145
|
+
state.accumulatedContent +=
|
|
146
|
+
`\n\n🔄 上下文压缩(${triggerLabel}): **${block.pre_tokens}** → **${block.post_tokens}** tokens\n`; // 🔄 / →
|
|
147
|
+
break;
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
// ---------------------------------------------------------------------------
|
|
153
|
+
// Claude session management
|
|
154
|
+
// ---------------------------------------------------------------------------
|
|
155
|
+
|
|
156
|
+
export async function initClaudeSession(): Promise<string> {
|
|
157
|
+
const cwd = await getDefaultCwd();
|
|
158
|
+
console.log(
|
|
159
|
+
`[${ts()}] [STEP 1/5] Creating ${getAdapter().displayName} session (model=${anthropicConfigDisplay(CLAUDE_MODEL)}, effort=${anthropicConfigDisplay(CLAUDE_EFFORT)}, cwd=${cwd})`
|
|
160
|
+
);
|
|
161
|
+
|
|
162
|
+
const result = await getAdapter().createSession(cwd);
|
|
163
|
+
const sessionId = result.sessionId;
|
|
164
|
+
console.log(`[${ts()}] → sessionId: ${sessionId}`);
|
|
165
|
+
|
|
166
|
+
return sessionId;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
export async function resumeAndPrompt(
|
|
170
|
+
sessionId: string,
|
|
171
|
+
userText: string,
|
|
172
|
+
token: string,
|
|
173
|
+
chatId: string,
|
|
174
|
+
msgTimestamp: number
|
|
175
|
+
): Promise<void> {
|
|
176
|
+
const info = await getAdapter().getSessionInfo(sessionId);
|
|
177
|
+
const cwd = info?.cwd ?? (await getDefaultCwd());
|
|
178
|
+
console.log(
|
|
179
|
+
`[${ts()}] Resuming ${getAdapter().displayName} session: ${sessionId} (model=${anthropicConfigDisplay(CLAUDE_MODEL)}, effort=${anthropicConfigDisplay(CLAUDE_EFFORT)}, cwd=${cwd})`
|
|
180
|
+
);
|
|
181
|
+
|
|
182
|
+
const controller = new AbortController();
|
|
183
|
+
|
|
184
|
+
chatSessionMap.set(chatId, {
|
|
185
|
+
gen: ++sessionGen,
|
|
186
|
+
close: () => controller.abort(),
|
|
187
|
+
cardId: null,
|
|
188
|
+
stopped: false,
|
|
189
|
+
accumulatedContent: "",
|
|
190
|
+
finalText: "",
|
|
191
|
+
spinnerTimer: null,
|
|
192
|
+
msgTimestamp,
|
|
193
|
+
sequence: 0,
|
|
194
|
+
cardBusy: false,
|
|
195
|
+
});
|
|
196
|
+
const myGen = sessionGen;
|
|
197
|
+
|
|
198
|
+
const now = Date.now();
|
|
199
|
+
const existingInfo = sessionInfoMap.get(chatId);
|
|
200
|
+
sessionInfoMap.set(chatId, {
|
|
201
|
+
sessionId,
|
|
202
|
+
turnCount: (existingInfo?.turnCount ?? 0) + 1,
|
|
203
|
+
lastContextTokens: existingInfo?.lastContextTokens ?? 0,
|
|
204
|
+
startTime: now,
|
|
205
|
+
model: anthropicConfigDisplay(CLAUDE_MODEL),
|
|
206
|
+
effort: anthropicConfigDisplay(CLAUDE_EFFORT),
|
|
207
|
+
});
|
|
208
|
+
|
|
209
|
+
let cardId: string | null = null;
|
|
210
|
+
cardId = await createCardKitCard(token, buildProgressCard("", { showStop: true, headerTitle: "生成中..." })).catch((err) => {
|
|
211
|
+
console.error(`[${ts()}] [CARDIKT] createCard FAIL: chatId=${chatId} ${(err as Error).message}`);
|
|
212
|
+
fileLog.flush();
|
|
213
|
+
sendTextReply(token, chatId, "⚠️ 流式卡片创建失败(可能因限流),将使用文本回复。").catch(() => {});
|
|
214
|
+
return null;
|
|
215
|
+
});
|
|
216
|
+
if (cardId) {
|
|
217
|
+
const cEntry = chatSessionMap.get(chatId);
|
|
218
|
+
if (cEntry) { cEntry.cardId = cardId; cEntry.sequence = 1; }
|
|
219
|
+
const sendOk = await sendCardKitMessage(token, chatId, cardId).catch((err) => {
|
|
220
|
+
console.error(`[${ts()}] [CARDIKT] sendMessage FAIL: chatId=${chatId} cardId=${cardId} ${(err as Error).message}`);
|
|
221
|
+
fileLog.flush();
|
|
222
|
+
return false;
|
|
223
|
+
});
|
|
224
|
+
if (!sendOk) {
|
|
225
|
+
sendTextReply(token, chatId, "⚠️ 卡片发送失败,将使用文本回复。").catch(() => {});
|
|
226
|
+
cardId = null;
|
|
227
|
+
if (cEntry) { cEntry.cardId = null; cEntry.sequence = 0; }
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
const state: AccumulatorState = {
|
|
232
|
+
accumulatedContent: "",
|
|
233
|
+
finalText: "",
|
|
234
|
+
chunkCount: 0,
|
|
235
|
+
};
|
|
236
|
+
|
|
237
|
+
let cardCreatedAt = Date.now();
|
|
238
|
+
const CARD_ROTATE_MS = 9 * 60 * 1000;
|
|
239
|
+
|
|
240
|
+
let dotCount = 0;
|
|
241
|
+
let lastSentContent = "";
|
|
242
|
+
let streamErrorNotified = false;
|
|
243
|
+
let healthLogTicks = 0;
|
|
244
|
+
const sendInterval = cardId ? setInterval(async () => {
|
|
245
|
+
const cEntry = chatSessionMap.get(chatId);
|
|
246
|
+
if (!cEntry || cEntry.stopped || cEntry.cardBusy) return;
|
|
247
|
+
if (cEntry.cardId !== cardId) return;
|
|
248
|
+
|
|
249
|
+
if (Date.now() - cardCreatedAt > CARD_ROTATE_MS) {
|
|
250
|
+
cEntry.cardBusy = true;
|
|
251
|
+
try {
|
|
252
|
+
const oldSeqBase = cEntry.sequence;
|
|
253
|
+
const oldDisplay = truncateContent(state.accumulatedContent + state.finalText) || "处理中...";
|
|
254
|
+
const oldCard = buildProgressCard(oldDisplay, { showStop: false, headerTitle: "生成中...(上轮)" });
|
|
255
|
+
await updateCardKitCard(token, cardId!, oldCard, oldSeqBase + 1).catch(() => {});
|
|
256
|
+
const newCardId = await createCardKitCard(token, buildProgressCard("", { showStop: true, headerTitle: "生成中..." }));
|
|
257
|
+
if (!newCardId) throw new Error("createCardKitCard returned empty");
|
|
258
|
+
await sendCardKitMessage(token, chatId, newCardId);
|
|
259
|
+
cardId = newCardId;
|
|
260
|
+
cEntry.cardId = newCardId;
|
|
261
|
+
cEntry.sequence = 1;
|
|
262
|
+
cardCreatedAt = Date.now();
|
|
263
|
+
lastSentContent = "";
|
|
264
|
+
streamErrorNotified = false;
|
|
265
|
+
console.log(`[${ts()}] [CARDIKT] rotated: old=${oldSeqBase} new=${newCardId} (9min timeout)`);
|
|
266
|
+
} catch (err) {
|
|
267
|
+
console.error(`[${ts()}] [CARDIKT] rotation FAIL: ${(err as Error).message}`);
|
|
268
|
+
} finally {
|
|
269
|
+
cEntry.cardBusy = false;
|
|
270
|
+
}
|
|
271
|
+
return;
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
dotCount = (dotCount % 9) + 1;
|
|
275
|
+
const content = truncateContent(state.accumulatedContent + state.finalText + "\n" + "。".repeat(dotCount));
|
|
276
|
+
if (content === lastSentContent) return;
|
|
277
|
+
|
|
278
|
+
lastSentContent = content;
|
|
279
|
+
cEntry.cardBusy = true;
|
|
280
|
+
const mySeq = cEntry.sequence + 1;
|
|
281
|
+
try {
|
|
282
|
+
const card = buildProgressCard(content, { showStop: true, headerTitle: "生成中..." });
|
|
283
|
+
await updateCardKitCard(token, cardId!, card, mySeq);
|
|
284
|
+
cEntry.sequence = mySeq;
|
|
285
|
+
cEntry.accumulatedContent = state.accumulatedContent;
|
|
286
|
+
streamErrorNotified = false;
|
|
287
|
+
healthLogTicks++;
|
|
288
|
+
if (healthLogTicks % 10 === 0) {
|
|
289
|
+
console.log(`[${ts()}] [CARDIKT] update health: seq=${mySeq} content=${state.accumulatedContent.length}chars text=${state.finalText.length}chars cardAge=${Math.round((Date.now() - cardCreatedAt) / 1000)}s`);
|
|
290
|
+
}
|
|
291
|
+
} catch (err) {
|
|
292
|
+
console.error(`[${ts()}] CardKit update error: chatId=${chatId} cardId=${cardId} seq=${mySeq} ${(err as Error).message}`);
|
|
293
|
+
if (!streamErrorNotified) {
|
|
294
|
+
streamErrorNotified = true;
|
|
295
|
+
sendTextReply(token, chatId, "⚠️ 卡片更新失败,结果将以文本形式发送。").catch(() => {});
|
|
296
|
+
}
|
|
297
|
+
} finally {
|
|
298
|
+
cEntry.cardBusy = false;
|
|
299
|
+
}
|
|
300
|
+
}, 3000) : null;
|
|
301
|
+
if (sendInterval) {
|
|
302
|
+
const entry = chatSessionMap.get(chatId);
|
|
303
|
+
if (entry) entry.spinnerTimer = sendInterval;
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
try {
|
|
307
|
+
for await (const unifiedMsg of getAdapter().prompt(sessionId, userText, cwd, controller.signal)) {
|
|
308
|
+
for (const block of unifiedMsg.blocks) {
|
|
309
|
+
accumulateBlockContent(block, state);
|
|
310
|
+
|
|
311
|
+
// 更新持久化上下文 token 数(compact_boundary 事件)
|
|
312
|
+
if (block.type === "compact_boundary" && block.post_tokens) {
|
|
313
|
+
const info = sessionInfoMap.get(chatId);
|
|
314
|
+
if (info) { info.lastContextTokens = block.post_tokens; }
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
} catch (streamErr) {
|
|
319
|
+
console.error(`[${ts()}] [STREAM] Error in stream loop: ${(streamErr as Error).message}`);
|
|
320
|
+
} finally {
|
|
321
|
+
if (sendInterval) clearInterval(sendInterval);
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
const cEntry = chatSessionMap.get(chatId);
|
|
325
|
+
if (!cEntry || cEntry.gen !== myGen) return;
|
|
326
|
+
const wasStopped = cEntry.stopped;
|
|
327
|
+
chatSessionMap.delete(chatId);
|
|
328
|
+
|
|
329
|
+
if (cardId && state.accumulatedContent) {
|
|
330
|
+
while (cEntry.cardBusy) {
|
|
331
|
+
await new Promise(r => setTimeout(r, 20));
|
|
332
|
+
}
|
|
333
|
+
const nextSeq = cEntry.sequence + 1;
|
|
334
|
+
if (wasStopped) {
|
|
335
|
+
const stopCard = buildProgressCard(state.accumulatedContent || "已停止", { showStop: false, headerTitle: "已停止", headerTemplate: "red" });
|
|
336
|
+
await updateCardKitCard(token, cardId, stopCard, nextSeq).catch((err) => {
|
|
337
|
+
console.error(`[${ts()}] CardKit finalize: chatId=${chatId} cardId=${cardId} ${(err as Error).message}`);
|
|
338
|
+
fileLog.flush();
|
|
339
|
+
});
|
|
340
|
+
} else {
|
|
341
|
+
const doneCard = buildProgressCard(state.accumulatedContent, { showStop: false, headerTitle: "完成" });
|
|
342
|
+
await updateCardKitCard(token, cardId, doneCard, nextSeq).catch((err) => {
|
|
343
|
+
console.error(`[${ts()}] CardKit finalize: chatId=${chatId} cardId=${cardId} ${(err as Error).message}`);
|
|
344
|
+
fileLog.flush();
|
|
345
|
+
sendTextReply(token, chatId, "⚠️ 卡片最终更新失败。").catch(() => {});
|
|
346
|
+
});
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
if (wasStopped) {
|
|
351
|
+
if (state.finalText.trim()) {
|
|
352
|
+
await sendTextReply(token, chatId, state.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 (content chunks: ${state.chunkCount})`);
|
|
357
|
+
return;
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
if (streamErrorNotified) {
|
|
361
|
+
if (state.accumulatedContent.trim()) {
|
|
362
|
+
const shortContent = truncateContent(state.accumulatedContent, 30, 4000);
|
|
363
|
+
await sendTextReply(token, chatId, `[生成过程]\n${shortContent}`).catch((err) =>
|
|
364
|
+
console.error(`[${ts()}] Failed to send content fallback: ${(err as Error).message}`)
|
|
365
|
+
);
|
|
366
|
+
}
|
|
367
|
+
if (state.finalText.trim()) {
|
|
368
|
+
await sendTextReply(token, chatId, state.finalText.trim()).catch((err) =>
|
|
369
|
+
console.error(`[${ts()}] Failed to send text fallback: ${(err as Error).message}`)
|
|
370
|
+
);
|
|
371
|
+
}
|
|
372
|
+
} else {
|
|
373
|
+
if (state.finalText.trim()) {
|
|
374
|
+
await sendTextReply(token, chatId, state.finalText.trim()).catch((err) =>
|
|
375
|
+
console.error(`[${ts()}] Failed to send final text: ${(err as Error).message}`)
|
|
376
|
+
);
|
|
377
|
+
} else if (!cardId && state.accumulatedContent.trim()) {
|
|
378
|
+
const shortContent = truncateContent(state.accumulatedContent, 30, 4000);
|
|
379
|
+
await sendTextReply(token, chatId, `[生成过程]\n${shortContent}`).catch((err) =>
|
|
380
|
+
console.error(`[${ts()}] Failed to send content text: ${(err as Error).message}`)
|
|
381
|
+
);
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
console.log(`[${ts()}] Session ${sessionId} stream complete (content chunks: ${state.chunkCount})`);
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
// ---------------------------------------------------------------------------
|
|
389
|
+
// Session status query (供 /status 命令使用)
|
|
390
|
+
// ---------------------------------------------------------------------------
|
|
391
|
+
|
|
392
|
+
export interface SessionStatus {
|
|
393
|
+
sessionId: string;
|
|
394
|
+
running: boolean;
|
|
395
|
+
turnCount: number;
|
|
396
|
+
lastContextTokens: number;
|
|
397
|
+
startTime: number;
|
|
398
|
+
model: string;
|
|
399
|
+
effort: string;
|
|
400
|
+
accumulatedLength: number;
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
export function getSessionStatus(chatId: string): SessionStatus | null {
|
|
404
|
+
const info = sessionInfoMap.get(chatId);
|
|
405
|
+
if (!info) return null;
|
|
406
|
+
|
|
407
|
+
const active = chatSessionMap.get(chatId);
|
|
408
|
+
return {
|
|
409
|
+
sessionId: info.sessionId,
|
|
410
|
+
running: active !== undefined && !active.stopped,
|
|
411
|
+
turnCount: info.turnCount,
|
|
412
|
+
lastContextTokens: info.lastContextTokens,
|
|
413
|
+
startTime: info.startTime,
|
|
414
|
+
model: anthropicConfigDisplay(info.model),
|
|
415
|
+
effort: anthropicConfigDisplay(info.effort),
|
|
416
|
+
accumulatedLength: active ? active.accumulatedContent.length + active.finalText.length : 0,
|
|
417
|
+
};
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
export function getAllSessionsStatus(): Array<{
|
|
421
|
+
chatId: string;
|
|
422
|
+
sessionId: string;
|
|
423
|
+
active: boolean;
|
|
424
|
+
turnCount: number;
|
|
425
|
+
startTime: number;
|
|
426
|
+
model: string;
|
|
427
|
+
effort: string;
|
|
428
|
+
}> {
|
|
429
|
+
const result: Array<{
|
|
430
|
+
chatId: string;
|
|
431
|
+
sessionId: string;
|
|
432
|
+
active: boolean;
|
|
433
|
+
turnCount: number;
|
|
434
|
+
startTime: number;
|
|
435
|
+
model: string;
|
|
436
|
+
effort: string;
|
|
437
|
+
}> = [];
|
|
438
|
+
for (const [chatId, info] of sessionInfoMap) {
|
|
439
|
+
const active = chatSessionMap.get(chatId);
|
|
440
|
+
result.push({
|
|
441
|
+
chatId,
|
|
442
|
+
sessionId: info.sessionId,
|
|
443
|
+
active: active !== undefined && !active.stopped,
|
|
444
|
+
turnCount: info.turnCount,
|
|
445
|
+
startTime: info.startTime,
|
|
446
|
+
model: info.model,
|
|
447
|
+
effort: info.effort,
|
|
448
|
+
});
|
|
449
|
+
}
|
|
450
|
+
return result;
|
|
456
451
|
}
|