chatccc 0.2.6 → 0.2.8
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 +50 -11
- package/package.json +1 -1
- package/src/__tests__/adapter-interface.test.ts +151 -151
- package/src/__tests__/cards.test.ts +417 -413
- package/src/__tests__/claude-adapter.test.ts +528 -528
- package/src/__tests__/config.test.ts +123 -0
- package/src/__tests__/cursor-adapter.test.ts +662 -249
- package/src/__tests__/cursor-session-meta-store.test.ts +212 -0
- package/src/__tests__/fixtures/cursor_partial_only.jsonl +5 -0
- package/src/__tests__/fixtures/cursor_partial_with_final.jsonl +13 -0
- package/src/__tests__/fixtures/cursor_with_tool_call.jsonl +12 -0
- package/src/__tests__/git-command.test.ts +288 -0
- package/src/__tests__/session.test.ts +475 -296
- package/src/adapters/adapter-interface.ts +151 -126
- package/src/adapters/claude-adapter.ts +257 -257
- package/src/adapters/cursor-adapter.ts +401 -228
- package/src/adapters/cursor-session-meta-store.ts +154 -0
- package/src/cards.ts +33 -21
- package/src/config.ts +92 -2
- package/src/feishu-api.ts +1 -1
- package/src/git-command.ts +202 -0
- package/src/index.ts +58 -4
- package/src/session.ts +620 -513
package/src/session.ts
CHANGED
|
@@ -1,514 +1,621 @@
|
|
|
1
|
-
import { readFile, writeFile, mkdir } from "node:fs/promises";
|
|
2
|
-
import { dirname } from "node:path";
|
|
3
|
-
|
|
4
|
-
import {
|
|
5
|
-
CLAUDE_EFFORT,
|
|
6
|
-
CLAUDE_MODEL,
|
|
7
|
-
SESSIONS_FILE,
|
|
8
|
-
addRecentDir,
|
|
9
|
-
anthropicConfigDisplay,
|
|
10
|
-
fileLog,
|
|
11
|
-
getDefaultCwd,
|
|
12
|
-
isSdkAnthropicDefault,
|
|
13
|
-
toolDisplayName,
|
|
14
|
-
ts,
|
|
15
|
-
} from "./config.ts";
|
|
16
|
-
import { buildProgressCard, getToolEmoji, truncateContent } from "./cards.ts";
|
|
17
|
-
import {
|
|
18
|
-
createCardKitCard,
|
|
19
|
-
sendCardKitMessage,
|
|
20
|
-
updateCardKitCard,
|
|
21
|
-
} from "./cardkit.ts";
|
|
22
|
-
import { sendTextReply } from "./feishu-api.ts";
|
|
23
|
-
import type { UnifiedBlock } from "./adapters/adapter-interface.ts";
|
|
24
|
-
import type { ToolAdapter } from "./adapters/adapter-interface.ts";
|
|
25
|
-
import { createClaudeAdapter } from "./adapters/claude-adapter.ts";
|
|
26
|
-
import { createCursorAdapter } from "./adapters/cursor-adapter.ts";
|
|
27
|
-
|
|
28
|
-
// ---------------------------------------------------------------------------
|
|
29
|
-
// Shared state (imported by index.ts)
|
|
30
|
-
// ---------------------------------------------------------------------------
|
|
31
|
-
|
|
32
|
-
export const processedMessages = new Set<string>();
|
|
33
|
-
export const MAX_PROCESSED = 5000;
|
|
34
|
-
|
|
35
|
-
export let sessionGen = 0;
|
|
36
|
-
export const chatSessionMap = new Map<string, {
|
|
37
|
-
gen: number;
|
|
38
|
-
close: () => void;
|
|
39
|
-
cardId: string | null;
|
|
40
|
-
stopped: boolean;
|
|
41
|
-
accumulatedContent: string;
|
|
42
|
-
finalText: string;
|
|
43
|
-
spinnerTimer: ReturnType<typeof setInterval> | null;
|
|
44
|
-
msgTimestamp: number;
|
|
45
|
-
sequence: number;
|
|
46
|
-
cardBusy: boolean;
|
|
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
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
state.
|
|
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
|
-
|
|
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
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
}
|
|
432
|
-
}
|
|
433
|
-
if (
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
}
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
}
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
1
|
+
import { readFile, writeFile, mkdir } from "node:fs/promises";
|
|
2
|
+
import { dirname } from "node:path";
|
|
3
|
+
|
|
4
|
+
import {
|
|
5
|
+
CLAUDE_EFFORT,
|
|
6
|
+
CLAUDE_MODEL,
|
|
7
|
+
SESSIONS_FILE,
|
|
8
|
+
addRecentDir,
|
|
9
|
+
anthropicConfigDisplay,
|
|
10
|
+
fileLog,
|
|
11
|
+
getDefaultCwd,
|
|
12
|
+
isSdkAnthropicDefault,
|
|
13
|
+
toolDisplayName,
|
|
14
|
+
ts,
|
|
15
|
+
} from "./config.ts";
|
|
16
|
+
import { buildProgressCard, getToolEmoji, truncateContent } from "./cards.ts";
|
|
17
|
+
import {
|
|
18
|
+
createCardKitCard,
|
|
19
|
+
sendCardKitMessage,
|
|
20
|
+
updateCardKitCard,
|
|
21
|
+
} from "./cardkit.ts";
|
|
22
|
+
import { sendTextReply } from "./feishu-api.ts";
|
|
23
|
+
import type { UnifiedBlock } from "./adapters/adapter-interface.ts";
|
|
24
|
+
import type { ToolAdapter } from "./adapters/adapter-interface.ts";
|
|
25
|
+
import { createClaudeAdapter } from "./adapters/claude-adapter.ts";
|
|
26
|
+
import { createCursorAdapter } from "./adapters/cursor-adapter.ts";
|
|
27
|
+
|
|
28
|
+
// ---------------------------------------------------------------------------
|
|
29
|
+
// Shared state (imported by index.ts)
|
|
30
|
+
// ---------------------------------------------------------------------------
|
|
31
|
+
|
|
32
|
+
export const processedMessages = new Set<string>();
|
|
33
|
+
export const MAX_PROCESSED = 5000;
|
|
34
|
+
|
|
35
|
+
export let sessionGen = 0;
|
|
36
|
+
export const chatSessionMap = new Map<string, {
|
|
37
|
+
gen: number;
|
|
38
|
+
close: () => void;
|
|
39
|
+
cardId: string | null;
|
|
40
|
+
stopped: boolean;
|
|
41
|
+
accumulatedContent: string;
|
|
42
|
+
finalText: string;
|
|
43
|
+
spinnerTimer: ReturnType<typeof setInterval> | null;
|
|
44
|
+
msgTimestamp: number;
|
|
45
|
+
sequence: number;
|
|
46
|
+
cardBusy: boolean;
|
|
47
|
+
}>();
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* sessionInfoMap 记录每个 chatId 当前会话的"轻量元数据":
|
|
51
|
+
*
|
|
52
|
+
* 注意此处**不**保存 model / effort:
|
|
53
|
+
* - Claude 会话:model/effort 由 ChatCCC 启动时的环境变量决定(CLAUDE_MODEL/EFFORT),
|
|
54
|
+
* getSessionStatus 直接读全局配置即可。
|
|
55
|
+
* - Cursor 会话:model 是 cursor-agent 自报的运行时值(如 Composer 2 Fast),
|
|
56
|
+
* 由 cursor-adapter 持久化到 cursor-session-meta.json,
|
|
57
|
+
* getSessionStatus 通过 adapter.getSessionInfo 实时获取;effort 概念不适用。
|
|
58
|
+
*
|
|
59
|
+
* 把 model/effort 从 sessionInfoMap 移除是为了消除"硬塞 CLAUDE_* 给 Cursor"
|
|
60
|
+
* 的不一致 bug——/status、/sessions 必须显示真实工具的真实信息。
|
|
61
|
+
*/
|
|
62
|
+
export const sessionInfoMap = new Map<string, {
|
|
63
|
+
sessionId: string;
|
|
64
|
+
turnCount: number;
|
|
65
|
+
lastContextTokens: number;
|
|
66
|
+
startTime: number;
|
|
67
|
+
tool: string;
|
|
68
|
+
}>();
|
|
69
|
+
|
|
70
|
+
export function resetState(): void {
|
|
71
|
+
for (const entry of chatSessionMap.values()) {
|
|
72
|
+
if (entry.spinnerTimer) clearInterval(entry.spinnerTimer);
|
|
73
|
+
try { entry.close(); } catch { /* ignore */ }
|
|
74
|
+
}
|
|
75
|
+
chatSessionMap.clear();
|
|
76
|
+
sessionInfoMap.clear();
|
|
77
|
+
processedMessages.clear();
|
|
78
|
+
console.log(`[${ts()}] [RESET] State cleared (dedup + active sessions)`);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// ---------------------------------------------------------------------------
|
|
82
|
+
// Adapter: 按 tool 创建并缓存
|
|
83
|
+
// ---------------------------------------------------------------------------
|
|
84
|
+
|
|
85
|
+
const adapterCache = new Map<string, ToolAdapter>();
|
|
86
|
+
|
|
87
|
+
export function getAdapterForTool(tool: string): ToolAdapter {
|
|
88
|
+
const cached = adapterCache.get(tool);
|
|
89
|
+
if (cached) return cached;
|
|
90
|
+
|
|
91
|
+
let adapter: ToolAdapter;
|
|
92
|
+
if (tool === "cursor") {
|
|
93
|
+
adapter = createCursorAdapter();
|
|
94
|
+
} else {
|
|
95
|
+
adapter = createClaudeAdapter({
|
|
96
|
+
model: CLAUDE_MODEL,
|
|
97
|
+
effort: CLAUDE_EFFORT,
|
|
98
|
+
isDefault: isSdkAnthropicDefault,
|
|
99
|
+
});
|
|
100
|
+
}
|
|
101
|
+
adapterCache.set(tool, adapter);
|
|
102
|
+
return adapter;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// ---------------------------------------------------------------------------
|
|
106
|
+
// Session tool persistence (.claude/sessions.json)
|
|
107
|
+
// ---------------------------------------------------------------------------
|
|
108
|
+
|
|
109
|
+
interface SessionToolRecord {
|
|
110
|
+
tool: string;
|
|
111
|
+
createdAt: number;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
async function loadSessionTools(): Promise<Record<string, SessionToolRecord>> {
|
|
115
|
+
try {
|
|
116
|
+
const raw = await readFile(SESSIONS_FILE, "utf-8");
|
|
117
|
+
return JSON.parse(raw);
|
|
118
|
+
} catch {
|
|
119
|
+
return {};
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
async function saveSessionTools(data: Record<string, SessionToolRecord>): Promise<void> {
|
|
124
|
+
try {
|
|
125
|
+
await mkdir(dirname(SESSIONS_FILE), { recursive: true });
|
|
126
|
+
await writeFile(SESSIONS_FILE, JSON.stringify(data, null, 2), "utf-8");
|
|
127
|
+
} catch (err) {
|
|
128
|
+
console.error(`[${ts()}] Failed to save sessions.json: ${(err as Error).message}`);
|
|
129
|
+
fileLog.flush();
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
export async function saveSessionTool(sessionId: string, tool: string): Promise<void> {
|
|
134
|
+
const data = await loadSessionTools();
|
|
135
|
+
data[sessionId] = { tool, createdAt: Date.now() };
|
|
136
|
+
await saveSessionTools(data);
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
export async function getSessionTool(sessionId: string): Promise<string | null> {
|
|
140
|
+
const data = await loadSessionTools();
|
|
141
|
+
const record = data[sessionId];
|
|
142
|
+
return record?.tool ?? null;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
// ---------------------------------------------------------------------------
|
|
146
|
+
// accumulateBlockContent — 将 UnifiedBlock 累积到渲染状态(纯函数,可测试)
|
|
147
|
+
// ---------------------------------------------------------------------------
|
|
148
|
+
|
|
149
|
+
export interface AccumulatorState {
|
|
150
|
+
accumulatedContent: string;
|
|
151
|
+
/** partial text 块按追加语义累积;适用于 Cursor 的流式增量与 Claude SDK 的 delta */
|
|
152
|
+
finalText: string;
|
|
153
|
+
/**
|
|
154
|
+
* 适配器明确给出的"完整最终文本"(覆盖语义)。
|
|
155
|
+
* 仅 Cursor `--stream-partial-output` 模式末尾的 final assistant 消息会写入;
|
|
156
|
+
* 用于配合 pickFinalReply 在 partial 累加 vs final 完整文本之间挑选最终回复,
|
|
157
|
+
* 避免最终消息出现两段重复内容。
|
|
158
|
+
*/
|
|
159
|
+
finalCompleteText: string;
|
|
160
|
+
chunkCount: number;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
/**
|
|
164
|
+
* 在 partial 累加(finalText)与适配器给出的"完整最终文本"(finalCompleteText)
|
|
165
|
+
* 之间挑选最终回复:
|
|
166
|
+
* - finalCompleteText 非空时永远优先(来自 cursor result.result 等权威源)
|
|
167
|
+
* - 否则回退到 finalText(partial 累加)
|
|
168
|
+
*
|
|
169
|
+
* 不做长度比较:cursor 在工具调用前会发 buffered flush(重复快照),
|
|
170
|
+
* 若按当前 adapter 误把 buffered flush 当 delta 累加,partial 累加可能"虚高",
|
|
171
|
+
* 此时取更长会选错;权威源(result.result)才是正解。
|
|
172
|
+
*/
|
|
173
|
+
export function pickFinalReply(state: AccumulatorState): string {
|
|
174
|
+
return state.finalCompleteText || state.finalText;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
export function accumulateBlockContent(
|
|
178
|
+
block: UnifiedBlock,
|
|
179
|
+
state: AccumulatorState,
|
|
180
|
+
): void {
|
|
181
|
+
switch (block.type) {
|
|
182
|
+
case "thinking":
|
|
183
|
+
state.chunkCount++;
|
|
184
|
+
state.accumulatedContent += block.thinking;
|
|
185
|
+
break;
|
|
186
|
+
case "tool_use": {
|
|
187
|
+
const inputStr =
|
|
188
|
+
typeof block.input === "object"
|
|
189
|
+
? JSON.stringify(block.input)
|
|
190
|
+
: String(block.input ?? "");
|
|
191
|
+
const shortInput =
|
|
192
|
+
inputStr.length > 300 ? inputStr.slice(0, 300) + "..." : inputStr;
|
|
193
|
+
state.accumulatedContent +=
|
|
194
|
+
`\n\n${getToolEmoji(block.name)} **${block.name}**\n\`${shortInput}\`\n`;
|
|
195
|
+
break;
|
|
196
|
+
}
|
|
197
|
+
case "tool_result": {
|
|
198
|
+
const toolUseId = block.tool_use_id;
|
|
199
|
+
const resultContent = block.content;
|
|
200
|
+
let resultStr = "";
|
|
201
|
+
if (typeof resultContent === "string") {
|
|
202
|
+
resultStr = resultContent;
|
|
203
|
+
} else if (Array.isArray(resultContent)) {
|
|
204
|
+
resultStr = resultContent
|
|
205
|
+
.map((c: { type?: string; text?: string }) => c.text ?? "")
|
|
206
|
+
.join("");
|
|
207
|
+
} else if (resultContent) {
|
|
208
|
+
resultStr = JSON.stringify(resultContent);
|
|
209
|
+
}
|
|
210
|
+
const shortResult =
|
|
211
|
+
resultStr.length > 200 ? resultStr.slice(0, 200) + "..." : resultStr;
|
|
212
|
+
const isError = block.is_error;
|
|
213
|
+
const icon = isError ? "❌" : "✅"; // ❌ : ✅
|
|
214
|
+
state.accumulatedContent +=
|
|
215
|
+
`${icon} *${toolUseId.slice(-6)}*: ${shortResult}\n`;
|
|
216
|
+
break;
|
|
217
|
+
}
|
|
218
|
+
case "redacted_thinking":
|
|
219
|
+
state.accumulatedContent += "\n\n⚠️ 内容被安全过滤\n"; // ⚠️
|
|
220
|
+
break;
|
|
221
|
+
case "search_result":
|
|
222
|
+
state.accumulatedContent +=
|
|
223
|
+
`\n\n🔍 联网搜索: **${block.query}**\n`; // 🔍
|
|
224
|
+
break;
|
|
225
|
+
case "text":
|
|
226
|
+
state.finalText += block.text;
|
|
227
|
+
// 新的增量文本到达时清空 finalCompleteText,确保 pickFinalReply 回退到
|
|
228
|
+
// finalText(累积文本)。否则 Cursor buffered flush 设置的旧
|
|
229
|
+
// finalCompleteText 会"吞掉"工具调用后新到达的增量文本。
|
|
230
|
+
state.finalCompleteText = "";
|
|
231
|
+
break;
|
|
232
|
+
case "text_final":
|
|
233
|
+
// 覆盖而非追加:适配器已保证这是一段完整最终文本(如 Cursor 流末快照)
|
|
234
|
+
state.finalCompleteText = block.text;
|
|
235
|
+
break;
|
|
236
|
+
case "compact_boundary": {
|
|
237
|
+
const triggerLabel = block.trigger === "manual" ? "手动" : "自动"; // 手动 / 自动
|
|
238
|
+
state.accumulatedContent +=
|
|
239
|
+
`\n\n🔄 上下文压缩(${triggerLabel}): **${block.pre_tokens}** → **${block.post_tokens}** tokens\n`; // 🔄 / →
|
|
240
|
+
break;
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
// ---------------------------------------------------------------------------
|
|
246
|
+
// Claude session management
|
|
247
|
+
// ---------------------------------------------------------------------------
|
|
248
|
+
|
|
249
|
+
/**
|
|
250
|
+
* 日志用:把 tool 对应的"配置摘要"格式化为单行字符串。
|
|
251
|
+
* Claude 显示 model/effort(来自环境变量);Cursor 显示 model(运行时由
|
|
252
|
+
* cursor-agent 决定,初次创建时尚未学习到,故显示占位)。
|
|
253
|
+
*/
|
|
254
|
+
function formatToolConfigForLog(tool: string, sessionModel?: string): string {
|
|
255
|
+
if (tool === "cursor") {
|
|
256
|
+
return `model=${sessionModel ?? "(由 cursor-agent 决定,init 事件后学习)"}`;
|
|
257
|
+
}
|
|
258
|
+
return `model=${anthropicConfigDisplay(CLAUDE_MODEL)}, effort=${anthropicConfigDisplay(CLAUDE_EFFORT)}`;
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
export async function initClaudeSession(tool: string): Promise<string> {
|
|
262
|
+
const cwd = await getDefaultCwd();
|
|
263
|
+
const adapter = getAdapterForTool(tool);
|
|
264
|
+
console.log(
|
|
265
|
+
`[${ts()}] [STEP 1/5] Creating ${adapter.displayName} session (${formatToolConfigForLog(tool)}, cwd=${cwd})`
|
|
266
|
+
);
|
|
267
|
+
|
|
268
|
+
const result = await adapter.createSession(cwd);
|
|
269
|
+
const sessionId = result.sessionId;
|
|
270
|
+
console.log(`[${ts()}] → sessionId: ${sessionId}`);
|
|
271
|
+
|
|
272
|
+
await saveSessionTool(sessionId, tool);
|
|
273
|
+
|
|
274
|
+
await addRecentDir(cwd);
|
|
275
|
+
|
|
276
|
+
return sessionId;
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
export async function resumeAndPrompt(
|
|
280
|
+
sessionId: string,
|
|
281
|
+
userText: string,
|
|
282
|
+
token: string,
|
|
283
|
+
chatId: string,
|
|
284
|
+
msgTimestamp: number,
|
|
285
|
+
tool: string,
|
|
286
|
+
): Promise<void> {
|
|
287
|
+
const adapter = getAdapterForTool(tool);
|
|
288
|
+
const info = await adapter.getSessionInfo(sessionId);
|
|
289
|
+
const cwd = info?.cwd ?? (await getDefaultCwd());
|
|
290
|
+
console.log(
|
|
291
|
+
`[${ts()}] Resuming ${adapter.displayName} session: ${sessionId} (${formatToolConfigForLog(tool, info?.model)}, cwd=${cwd})`
|
|
292
|
+
);
|
|
293
|
+
|
|
294
|
+
const controller = new AbortController();
|
|
295
|
+
|
|
296
|
+
chatSessionMap.set(chatId, {
|
|
297
|
+
gen: ++sessionGen,
|
|
298
|
+
close: () => controller.abort(),
|
|
299
|
+
cardId: null,
|
|
300
|
+
stopped: false,
|
|
301
|
+
accumulatedContent: "",
|
|
302
|
+
finalText: "",
|
|
303
|
+
spinnerTimer: null,
|
|
304
|
+
msgTimestamp,
|
|
305
|
+
sequence: 0,
|
|
306
|
+
cardBusy: false,
|
|
307
|
+
});
|
|
308
|
+
const myGen = sessionGen;
|
|
309
|
+
|
|
310
|
+
const now = Date.now();
|
|
311
|
+
const existingInfo = sessionInfoMap.get(chatId);
|
|
312
|
+
sessionInfoMap.set(chatId, {
|
|
313
|
+
sessionId,
|
|
314
|
+
turnCount: (existingInfo?.turnCount ?? 0) + 1,
|
|
315
|
+
lastContextTokens: existingInfo?.lastContextTokens ?? 0,
|
|
316
|
+
startTime: now,
|
|
317
|
+
tool,
|
|
318
|
+
});
|
|
319
|
+
|
|
320
|
+
let cardId: string | null = null;
|
|
321
|
+
cardId = await createCardKitCard(token, buildProgressCard("", { showStop: true, headerTitle: "生成中..." })).catch((err) => {
|
|
322
|
+
console.error(`[${ts()}] [CARDIKT] createCard FAIL: chatId=${chatId} ${(err as Error).message}`);
|
|
323
|
+
fileLog.flush();
|
|
324
|
+
sendTextReply(token, chatId, "⚠️ 流式卡片创建失败(可能因限流),将使用文本回复。").catch(() => {});
|
|
325
|
+
return null;
|
|
326
|
+
});
|
|
327
|
+
if (cardId) {
|
|
328
|
+
const cEntry = chatSessionMap.get(chatId);
|
|
329
|
+
if (cEntry) { cEntry.cardId = cardId; cEntry.sequence = 1; }
|
|
330
|
+
const sendOk = await sendCardKitMessage(token, chatId, cardId).catch((err) => {
|
|
331
|
+
console.error(`[${ts()}] [CARDIKT] sendMessage FAIL: chatId=${chatId} cardId=${cardId} ${(err as Error).message}`);
|
|
332
|
+
fileLog.flush();
|
|
333
|
+
return false;
|
|
334
|
+
});
|
|
335
|
+
if (!sendOk) {
|
|
336
|
+
sendTextReply(token, chatId, "⚠️ 卡片发送失败,将使用文本回复。").catch(() => {});
|
|
337
|
+
cardId = null;
|
|
338
|
+
if (cEntry) { cEntry.cardId = null; cEntry.sequence = 0; }
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
const state: AccumulatorState = {
|
|
343
|
+
accumulatedContent: "",
|
|
344
|
+
finalText: "",
|
|
345
|
+
finalCompleteText: "",
|
|
346
|
+
chunkCount: 0,
|
|
347
|
+
};
|
|
348
|
+
|
|
349
|
+
let cardCreatedAt = Date.now();
|
|
350
|
+
const CARD_ROTATE_MS = 9 * 60 * 1000;
|
|
351
|
+
|
|
352
|
+
let dotCount = 0;
|
|
353
|
+
let lastSentContent = "";
|
|
354
|
+
let streamErrorNotified = false;
|
|
355
|
+
let healthLogTicks = 0;
|
|
356
|
+
const sendInterval = cardId ? setInterval(async () => {
|
|
357
|
+
const cEntry = chatSessionMap.get(chatId);
|
|
358
|
+
if (!cEntry || cEntry.stopped || cEntry.cardBusy) return;
|
|
359
|
+
if (cEntry.cardId !== cardId) return;
|
|
360
|
+
|
|
361
|
+
if (Date.now() - cardCreatedAt > CARD_ROTATE_MS) {
|
|
362
|
+
cEntry.cardBusy = true;
|
|
363
|
+
try {
|
|
364
|
+
const oldSeqBase = cEntry.sequence;
|
|
365
|
+
const oldDisplay = truncateContent(state.accumulatedContent + pickFinalReply(state)) || "处理中...";
|
|
366
|
+
const oldCard = buildProgressCard(oldDisplay, { showStop: false, headerTitle: "生成中...(上轮)" });
|
|
367
|
+
await updateCardKitCard(token, cardId!, oldCard, oldSeqBase + 1).catch(() => {});
|
|
368
|
+
const newCardId = await createCardKitCard(token, buildProgressCard("", { showStop: true, headerTitle: "生成中..." }));
|
|
369
|
+
if (!newCardId) throw new Error("createCardKitCard returned empty");
|
|
370
|
+
await sendCardKitMessage(token, chatId, newCardId);
|
|
371
|
+
cardId = newCardId;
|
|
372
|
+
cEntry.cardId = newCardId;
|
|
373
|
+
cEntry.sequence = 1;
|
|
374
|
+
cardCreatedAt = Date.now();
|
|
375
|
+
lastSentContent = "";
|
|
376
|
+
streamErrorNotified = false;
|
|
377
|
+
console.log(`[${ts()}] [CARDIKT] rotated: old=${oldSeqBase} new=${newCardId} (9min timeout)`);
|
|
378
|
+
} catch (err) {
|
|
379
|
+
console.error(`[${ts()}] [CARDIKT] rotation FAIL: ${(err as Error).message}`);
|
|
380
|
+
} finally {
|
|
381
|
+
cEntry.cardBusy = false;
|
|
382
|
+
}
|
|
383
|
+
return;
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
dotCount = (dotCount % 9) + 1;
|
|
387
|
+
const content = truncateContent(state.accumulatedContent + pickFinalReply(state) + "\n" + "。".repeat(dotCount));
|
|
388
|
+
if (content === lastSentContent) return;
|
|
389
|
+
|
|
390
|
+
lastSentContent = content;
|
|
391
|
+
cEntry.cardBusy = true;
|
|
392
|
+
const mySeq = cEntry.sequence + 1;
|
|
393
|
+
try {
|
|
394
|
+
const card = buildProgressCard(content, { showStop: true, headerTitle: "生成中..." });
|
|
395
|
+
await updateCardKitCard(token, cardId!, card, mySeq);
|
|
396
|
+
cEntry.sequence = mySeq;
|
|
397
|
+
cEntry.accumulatedContent = state.accumulatedContent;
|
|
398
|
+
streamErrorNotified = false;
|
|
399
|
+
healthLogTicks++;
|
|
400
|
+
if (healthLogTicks % 10 === 0) {
|
|
401
|
+
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`);
|
|
402
|
+
}
|
|
403
|
+
} catch (err) {
|
|
404
|
+
console.error(`[${ts()}] CardKit update error: chatId=${chatId} cardId=${cardId} seq=${mySeq} ${(err as Error).message}`);
|
|
405
|
+
if (!streamErrorNotified) {
|
|
406
|
+
streamErrorNotified = true;
|
|
407
|
+
sendTextReply(token, chatId, "⚠️ 卡片更新失败,结果将以文本形式发送。").catch(() => {});
|
|
408
|
+
}
|
|
409
|
+
} finally {
|
|
410
|
+
cEntry.cardBusy = false;
|
|
411
|
+
}
|
|
412
|
+
}, 3000) : null;
|
|
413
|
+
if (sendInterval) {
|
|
414
|
+
const entry = chatSessionMap.get(chatId);
|
|
415
|
+
if (entry) entry.spinnerTimer = sendInterval;
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
try {
|
|
419
|
+
for await (const unifiedMsg of adapter.prompt(sessionId, userText, cwd, controller.signal)) {
|
|
420
|
+
for (const block of unifiedMsg.blocks) {
|
|
421
|
+
accumulateBlockContent(block, state);
|
|
422
|
+
|
|
423
|
+
// 更新持久化上下文 token 数(compact_boundary 事件)
|
|
424
|
+
if (block.type === "compact_boundary" && block.post_tokens) {
|
|
425
|
+
const info = sessionInfoMap.get(chatId);
|
|
426
|
+
if (info) { info.lastContextTokens = block.post_tokens; }
|
|
427
|
+
}
|
|
428
|
+
}
|
|
429
|
+
}
|
|
430
|
+
} catch (streamErr) {
|
|
431
|
+
console.error(`[${ts()}] [STREAM] Error in stream loop: ${(streamErr as Error).message}`);
|
|
432
|
+
} finally {
|
|
433
|
+
if (sendInterval) clearInterval(sendInterval);
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
const cEntry = chatSessionMap.get(chatId);
|
|
437
|
+
if (!cEntry || cEntry.gen !== myGen) return;
|
|
438
|
+
const wasStopped = cEntry.stopped;
|
|
439
|
+
chatSessionMap.delete(chatId);
|
|
440
|
+
|
|
441
|
+
const finalCardContent = state.accumulatedContent || " ";
|
|
442
|
+
if (cardId) {
|
|
443
|
+
while (cEntry.cardBusy) {
|
|
444
|
+
await new Promise(r => setTimeout(r, 20));
|
|
445
|
+
}
|
|
446
|
+
const nextSeq = cEntry.sequence + 1;
|
|
447
|
+
if (wasStopped) {
|
|
448
|
+
const stopCard = buildProgressCard(finalCardContent, { showStop: false, headerTitle: "已停止", headerTemplate: "red" });
|
|
449
|
+
await updateCardKitCard(token, cardId, stopCard, nextSeq).catch((err) => {
|
|
450
|
+
console.error(`[${ts()}] CardKit finalize: chatId=${chatId} cardId=${cardId} ${(err as Error).message}`);
|
|
451
|
+
fileLog.flush();
|
|
452
|
+
});
|
|
453
|
+
} else {
|
|
454
|
+
const doneCard = buildProgressCard(finalCardContent, { showStop: false, headerTitle: "完成" });
|
|
455
|
+
await updateCardKitCard(token, cardId, doneCard, nextSeq).catch((err) => {
|
|
456
|
+
console.error(`[${ts()}] CardKit finalize: chatId=${chatId} cardId=${cardId} ${(err as Error).message}`);
|
|
457
|
+
fileLog.flush();
|
|
458
|
+
sendTextReply(token, chatId, "⚠️ 卡片最终更新失败。").catch(() => {});
|
|
459
|
+
});
|
|
460
|
+
}
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
// 在 partial 累加 vs 适配器给出的 final 完整文本之间挑选;
|
|
464
|
+
// Cursor 流末会发 final 完整快照,若与 partial 累加都直接发会出现两段重复。
|
|
465
|
+
const finalReply = pickFinalReply(state).trim();
|
|
466
|
+
|
|
467
|
+
if (wasStopped) {
|
|
468
|
+
if (finalReply) {
|
|
469
|
+
await sendTextReply(token, chatId, finalReply).catch((err) =>
|
|
470
|
+
console.error(`[${ts()}] Failed to send partial text: ${(err as Error).message}`)
|
|
471
|
+
);
|
|
472
|
+
}
|
|
473
|
+
console.log(`[${ts()}] Session ${sessionId} stopped by user (content chunks: ${state.chunkCount})`);
|
|
474
|
+
return;
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
if (streamErrorNotified) {
|
|
478
|
+
if (state.accumulatedContent.trim()) {
|
|
479
|
+
const shortContent = truncateContent(state.accumulatedContent, 30, 4000);
|
|
480
|
+
await sendTextReply(token, chatId, `[生成过程]\n${shortContent}`).catch((err) =>
|
|
481
|
+
console.error(`[${ts()}] Failed to send content fallback: ${(err as Error).message}`)
|
|
482
|
+
);
|
|
483
|
+
}
|
|
484
|
+
if (finalReply) {
|
|
485
|
+
await sendTextReply(token, chatId, finalReply).catch((err) =>
|
|
486
|
+
console.error(`[${ts()}] Failed to send text fallback: ${(err as Error).message}`)
|
|
487
|
+
);
|
|
488
|
+
}
|
|
489
|
+
} else {
|
|
490
|
+
if (finalReply) {
|
|
491
|
+
await sendTextReply(token, chatId, finalReply).catch((err) =>
|
|
492
|
+
console.error(`[${ts()}] Failed to send final text: ${(err as Error).message}`)
|
|
493
|
+
);
|
|
494
|
+
} else if (!cardId && state.accumulatedContent.trim()) {
|
|
495
|
+
const shortContent = truncateContent(state.accumulatedContent, 30, 4000);
|
|
496
|
+
await sendTextReply(token, chatId, `[生成过程]\n${shortContent}`).catch((err) =>
|
|
497
|
+
console.error(`[${ts()}] Failed to send content text: ${(err as Error).message}`)
|
|
498
|
+
);
|
|
499
|
+
}
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
console.log(`[${ts()}] Session ${sessionId} stream complete (content chunks: ${state.chunkCount})`);
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
// ---------------------------------------------------------------------------
|
|
506
|
+
// Session status query (供 /status、/sessions 命令使用)
|
|
507
|
+
// ---------------------------------------------------------------------------
|
|
508
|
+
//
|
|
509
|
+
// model / effort 的来源策略(按 tool 区分,避免硬塞 ChatCCC 全局配置导致显示
|
|
510
|
+
// 与实际不符):
|
|
511
|
+
// - tool === "cursor"
|
|
512
|
+
// model:调用 cursor-adapter.getSessionInfo 取持久化的真实模型,
|
|
513
|
+
// 未学习到时显示占位符 "—"
|
|
514
|
+
// effort:cursor-agent 没有 effort 概念,恒为 null(卡片渲染时隐藏该行)
|
|
515
|
+
// - tool === "claude"(默认)
|
|
516
|
+
// model:anthropicConfigDisplay(CLAUDE_MODEL)
|
|
517
|
+
// effort:anthropicConfigDisplay(CLAUDE_EFFORT)
|
|
518
|
+
// ---------------------------------------------------------------------------
|
|
519
|
+
|
|
520
|
+
/** 未知/未学习到时的 model 占位符(卡片可视提示,区别于"显示成 default"的旧 bug) */
|
|
521
|
+
export const UNKNOWN_MODEL_PLACEHOLDER = "—";
|
|
522
|
+
|
|
523
|
+
export interface SessionStatus {
|
|
524
|
+
sessionId: string;
|
|
525
|
+
running: boolean;
|
|
526
|
+
turnCount: number;
|
|
527
|
+
lastContextTokens: number;
|
|
528
|
+
startTime: number;
|
|
529
|
+
model: string;
|
|
530
|
+
/** null 表示该工具没有 effort 概念(如 Cursor),调用方应隐藏该行 */
|
|
531
|
+
effort: string | null;
|
|
532
|
+
accumulatedLength: number;
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
async function resolveModelEffort(
|
|
536
|
+
tool: string,
|
|
537
|
+
sessionId: string,
|
|
538
|
+
): Promise<{ model: string; effort: string | null }> {
|
|
539
|
+
if (tool === "cursor") {
|
|
540
|
+
let model = UNKNOWN_MODEL_PLACEHOLDER;
|
|
541
|
+
try {
|
|
542
|
+
const adapter = getAdapterForTool(tool);
|
|
543
|
+
const info = await adapter.getSessionInfo(sessionId);
|
|
544
|
+
if (info?.model) model = info.model;
|
|
545
|
+
} catch {
|
|
546
|
+
// adapter 异常时降级为占位符(不阻塞 /status 卡片)
|
|
547
|
+
}
|
|
548
|
+
return { model, effort: null };
|
|
549
|
+
}
|
|
550
|
+
return {
|
|
551
|
+
model: anthropicConfigDisplay(CLAUDE_MODEL),
|
|
552
|
+
effort: anthropicConfigDisplay(CLAUDE_EFFORT),
|
|
553
|
+
};
|
|
554
|
+
}
|
|
555
|
+
|
|
556
|
+
export async function getSessionStatus(chatId: string): Promise<SessionStatus | null> {
|
|
557
|
+
const info = sessionInfoMap.get(chatId);
|
|
558
|
+
if (!info) return null;
|
|
559
|
+
|
|
560
|
+
const active = chatSessionMap.get(chatId);
|
|
561
|
+
const { model, effort } = await resolveModelEffort(info.tool, info.sessionId);
|
|
562
|
+
|
|
563
|
+
return {
|
|
564
|
+
sessionId: info.sessionId,
|
|
565
|
+
running: active !== undefined && !active.stopped,
|
|
566
|
+
turnCount: info.turnCount,
|
|
567
|
+
lastContextTokens: info.lastContextTokens,
|
|
568
|
+
startTime: info.startTime,
|
|
569
|
+
model,
|
|
570
|
+
effort,
|
|
571
|
+
accumulatedLength: active ? active.accumulatedContent.length + active.finalText.length : 0,
|
|
572
|
+
};
|
|
573
|
+
}
|
|
574
|
+
|
|
575
|
+
export interface SessionsListEntry {
|
|
576
|
+
chatId: string;
|
|
577
|
+
sessionId: string;
|
|
578
|
+
active: boolean;
|
|
579
|
+
turnCount: number;
|
|
580
|
+
startTime: number;
|
|
581
|
+
model: string;
|
|
582
|
+
/** null 表示该工具没有 effort 概念(如 Cursor) */
|
|
583
|
+
effort: string | null;
|
|
584
|
+
tool: string;
|
|
585
|
+
}
|
|
586
|
+
|
|
587
|
+
export async function getAllSessionsStatus(): Promise<SessionsListEntry[]> {
|
|
588
|
+
const entries = Array.from(sessionInfoMap.entries());
|
|
589
|
+
// 并行解析每个 session 的 model/effort(cursor 涉及异步 store IO)
|
|
590
|
+
return Promise.all(
|
|
591
|
+
entries.map(async ([chatId, info]) => {
|
|
592
|
+
const active = chatSessionMap.get(chatId);
|
|
593
|
+
const { model, effort } = await resolveModelEffort(info.tool, info.sessionId);
|
|
594
|
+
return {
|
|
595
|
+
chatId,
|
|
596
|
+
sessionId: info.sessionId,
|
|
597
|
+
active: active !== undefined && !active.stopped,
|
|
598
|
+
turnCount: info.turnCount,
|
|
599
|
+
startTime: info.startTime,
|
|
600
|
+
model,
|
|
601
|
+
effort,
|
|
602
|
+
tool: info.tool,
|
|
603
|
+
};
|
|
604
|
+
}),
|
|
605
|
+
);
|
|
606
|
+
}
|
|
607
|
+
|
|
608
|
+
// ---------------------------------------------------------------------------
|
|
609
|
+
// 测试辅助:注入自定义 adapter 到 adapterCache
|
|
610
|
+
// ---------------------------------------------------------------------------
|
|
611
|
+
// 仅供单测使用——下划线前缀表明非生产 API。让 session-status 的测试可以
|
|
612
|
+
// 注入一个内存 store + adapter,以验证 cursor 分支按 tool 取真实 model。
|
|
613
|
+
// ---------------------------------------------------------------------------
|
|
614
|
+
|
|
615
|
+
export function _setAdapterForToolForTest(tool: string, adapter: ToolAdapter): void {
|
|
616
|
+
adapterCache.set(tool, adapter);
|
|
617
|
+
}
|
|
618
|
+
|
|
619
|
+
export function _clearAdapterCacheForTest(): void {
|
|
620
|
+
adapterCache.clear();
|
|
514
621
|
}
|