chatccc 0.2.41 → 0.2.43
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/config.sample.json +30 -29
- package/demo/ilink_echo_probe.ts +222 -222
- package/package.json +59 -59
- package/src/__tests__/cards.test.ts +22 -6
- package/src/__tests__/crash-logging.test.ts +62 -62
- package/src/__tests__/session.test.ts +124 -11
- package/src/cards.ts +5 -3
- package/src/config.ts +742 -736
- package/src/index.ts +126 -13
- package/src/session.ts +136 -7
- package/src/shared.ts +63 -63
package/src/index.ts
CHANGED
|
@@ -41,6 +41,7 @@ import {
|
|
|
41
41
|
CLAUDE_EFFORT,
|
|
42
42
|
CLAUDE_MODEL,
|
|
43
43
|
GIT_TIMEOUT_MS,
|
|
44
|
+
ALLOW_INTERRUPT,
|
|
44
45
|
reloadConfigFromDisk,
|
|
45
46
|
anthropicConfigDisplay,
|
|
46
47
|
LOCAL_RELAY_URL,
|
|
@@ -102,6 +103,7 @@ import {
|
|
|
102
103
|
resetState,
|
|
103
104
|
resumeAndPrompt,
|
|
104
105
|
sessionInfoMap,
|
|
106
|
+
recordSessionRegistry,
|
|
105
107
|
getAdapterForTool,
|
|
106
108
|
} from "./session.ts";
|
|
107
109
|
|
|
@@ -309,7 +311,8 @@ async function handleSimInjectMessage(req: IncomingMessage, res: ServerResponse)
|
|
|
309
311
|
|
|
310
312
|
async function handleCommand(text: string, chatId: string, openId: string, msgTimestamp: number, chatType = "group", traceId?: string): Promise<void> {
|
|
311
313
|
const tid = traceId ?? makeTraceId();
|
|
312
|
-
|
|
314
|
+
const textLower = text.toLowerCase();
|
|
315
|
+
if (textLower === "/restart") {
|
|
313
316
|
logTrace(tid, "BRANCH", { cmd: "/restart" });
|
|
314
317
|
const restartToken = await getTenantAccessToken();
|
|
315
318
|
await sendTextReply(restartToken, chatId, "正在重启...").catch(() => {});
|
|
@@ -326,7 +329,7 @@ async function handleCommand(text: string, chatId: string, openId: string, msgTi
|
|
|
326
329
|
return;
|
|
327
330
|
}
|
|
328
331
|
|
|
329
|
-
if (
|
|
332
|
+
if (textLower === "/cd" || textLower.startsWith("/cd ")) {
|
|
330
333
|
logTrace(tid, "BRANCH", { cmd: "/cd", arg: text.slice(3).trim() || "(none)" });
|
|
331
334
|
const cdToken = await getTenantAccessToken();
|
|
332
335
|
const currentDir = await getDefaultCwd(chatId);
|
|
@@ -415,8 +418,8 @@ async function handleCommand(text: string, chatId: string, openId: string, msgTi
|
|
|
415
418
|
return;
|
|
416
419
|
}
|
|
417
420
|
|
|
418
|
-
if (
|
|
419
|
-
const toolArg = text.slice(5).trim();
|
|
421
|
+
if (textLower === "/new" || textLower.startsWith("/new ")) {
|
|
422
|
+
const toolArg = text.slice(5).trim().toLowerCase();
|
|
420
423
|
const tool = toolArg || "claude";
|
|
421
424
|
logTrace(tid, "BRANCH", { cmd: "/new", tool });
|
|
422
425
|
const validTools = ["claude", "cursor", "codex"];
|
|
@@ -483,6 +486,15 @@ async function handleCommand(text: string, chatId: string, openId: string, msgTi
|
|
|
483
486
|
|
|
484
487
|
// 让新群的默认工作目录继承当前会话的 cwd
|
|
485
488
|
await setDefaultCwd(cwd, newChatId);
|
|
489
|
+
await recordSessionRegistry({
|
|
490
|
+
chatId: newChatId,
|
|
491
|
+
sessionId,
|
|
492
|
+
tool,
|
|
493
|
+
chatName: initialName,
|
|
494
|
+
turnCount: 0,
|
|
495
|
+
startTime: Date.now(),
|
|
496
|
+
running: false,
|
|
497
|
+
});
|
|
486
498
|
|
|
487
499
|
const adapter = getAdapterForTool(tool);
|
|
488
500
|
await sendCardReply(
|
|
@@ -530,12 +542,13 @@ async function handleCommand(text: string, chatId: string, openId: string, msgTi
|
|
|
530
542
|
try {
|
|
531
543
|
await updateChatInfo(freshToken, chatId, newName, description);
|
|
532
544
|
console.log(`[${ts()}] [RENAME] First message → group renamed to "${newName}"`);
|
|
545
|
+
await recordSessionRegistry({ chatId, chatName: newName }).catch(() => {});
|
|
533
546
|
} catch (err) {
|
|
534
547
|
console.error(`[${ts()}] [RENAME] Failed: ${(err as Error).message}`);
|
|
535
548
|
}
|
|
536
549
|
}
|
|
537
550
|
|
|
538
|
-
if (
|
|
551
|
+
if (textLower === "/stop") {
|
|
539
552
|
logTrace(tid, "BRANCH", { cmd: "/stop" });
|
|
540
553
|
const cEntry = chatSessionMap.get(chatId);
|
|
541
554
|
if (cEntry) {
|
|
@@ -556,12 +569,13 @@ async function handleCommand(text: string, chatId: string, openId: string, msgTi
|
|
|
556
569
|
return;
|
|
557
570
|
}
|
|
558
571
|
|
|
559
|
-
if (
|
|
572
|
+
if (textLower === "/status") {
|
|
560
573
|
logTrace(tid, "BRANCH", { cmd: "/status" });
|
|
561
574
|
const status = await getSessionStatus(chatId);
|
|
562
575
|
const running = chatSessionMap.get(chatId);
|
|
563
576
|
const isActive = running && !running.stopped;
|
|
564
577
|
const statusText = [
|
|
578
|
+
`**群名:** ${status?.chatName || "—"}`,
|
|
565
579
|
`**Session ID:** \`${status?.sessionId ?? sessionId}\``,
|
|
566
580
|
`**工具:** ${toolLabel}`,
|
|
567
581
|
`**状态:** ${isActive ? "🟢 运行中" : "⚪ 空闲"}`,
|
|
@@ -590,12 +604,13 @@ async function handleCommand(text: string, chatId: string, openId: string, msgTi
|
|
|
590
604
|
return;
|
|
591
605
|
}
|
|
592
606
|
|
|
593
|
-
if (
|
|
607
|
+
if (textLower === "/sessions") {
|
|
594
608
|
logTrace(tid, "BRANCH", { cmd: "/sessions" });
|
|
595
609
|
const allSessions = await getAllSessionsStatus();
|
|
596
610
|
const now = Date.now();
|
|
597
611
|
const cardData = allSessions.map(s => ({
|
|
598
612
|
sessionId: s.sessionId,
|
|
613
|
+
chatName: s.chatName,
|
|
599
614
|
active: s.active,
|
|
600
615
|
turnCount: s.turnCount,
|
|
601
616
|
elapsedSeconds: s.active ? Math.floor((now - s.startTime) / 1000) : null,
|
|
@@ -609,7 +624,7 @@ async function handleCommand(text: string, chatId: string, openId: string, msgTi
|
|
|
609
624
|
return;
|
|
610
625
|
}
|
|
611
626
|
|
|
612
|
-
if (
|
|
627
|
+
if (textLower === "/forget") {
|
|
613
628
|
logTrace(tid, "BRANCH", { cmd: "/forget" });
|
|
614
629
|
const adapter = getAdapterForTool(descriptionTool);
|
|
615
630
|
let cwd: string;
|
|
@@ -643,9 +658,9 @@ async function handleCommand(text: string, chatId: string, openId: string, msgTi
|
|
|
643
658
|
}
|
|
644
659
|
|
|
645
660
|
const descPrefix = sessionPrefixForTool(descriptionTool);
|
|
646
|
-
const
|
|
647
|
-
await updateChatInfo(freshToken, chatId,
|
|
648
|
-
console.log(`[${ts()}] [FORGET] Group updated: name="${
|
|
661
|
+
const newName = sessionChatName("新会话", cwd);
|
|
662
|
+
await updateChatInfo(freshToken, chatId, newName, `${descPrefix} ${newSessionId}`);
|
|
663
|
+
console.log(`[${ts()}] [FORGET] Group updated: name="${newName}" desc="${descPrefix} ${newSessionId}"`);
|
|
649
664
|
|
|
650
665
|
sessionInfoMap.set(chatId, {
|
|
651
666
|
sessionId: newSessionId,
|
|
@@ -654,6 +669,16 @@ async function handleCommand(text: string, chatId: string, openId: string, msgTi
|
|
|
654
669
|
startTime: Date.now(),
|
|
655
670
|
tool: descriptionTool,
|
|
656
671
|
});
|
|
672
|
+
await recordSessionRegistry({
|
|
673
|
+
chatId,
|
|
674
|
+
sessionId: newSessionId,
|
|
675
|
+
tool: descriptionTool,
|
|
676
|
+
chatName: newName,
|
|
677
|
+
turnCount: 0,
|
|
678
|
+
lastContextTokens: 0,
|
|
679
|
+
startTime: Date.now(),
|
|
680
|
+
running: false,
|
|
681
|
+
});
|
|
657
682
|
|
|
658
683
|
setChatAvatar(freshToken, chatId, descriptionTool, "new").catch(() => {});
|
|
659
684
|
|
|
@@ -671,10 +696,86 @@ async function handleCommand(text: string, chatId: string, openId: string, msgTi
|
|
|
671
696
|
return;
|
|
672
697
|
}
|
|
673
698
|
|
|
699
|
+
// /session <number>:切换到 /sessions 列表中的指定会话
|
|
700
|
+
const sessionMatch = textLower.match(/^\/session\s+(\d+)$/);
|
|
701
|
+
if (sessionMatch) {
|
|
702
|
+
const index = parseInt(sessionMatch[1], 10) - 1;
|
|
703
|
+
logTrace(tid, "BRANCH", { cmd: "/session", index: index + 1 });
|
|
704
|
+
const allSessions = await getAllSessionsStatus();
|
|
705
|
+
if (allSessions.length === 0) {
|
|
706
|
+
await sendCardReply(freshToken, chatId, "/session", "暂无历史会话。", "yellow");
|
|
707
|
+
logTrace(tid, "DONE", { outcome: "session_no_sessions" });
|
|
708
|
+
return;
|
|
709
|
+
}
|
|
710
|
+
if (index < 0 || index >= allSessions.length) {
|
|
711
|
+
await sendCardReply(freshToken, chatId, "/session", `序号超出范围,当前共 ${allSessions.length} 个会话。`, "yellow");
|
|
712
|
+
logTrace(tid, "DONE", { outcome: "session_out_of_range", index: index + 1, total: allSessions.length });
|
|
713
|
+
return;
|
|
714
|
+
}
|
|
715
|
+
const target = allSessions[index];
|
|
716
|
+
|
|
717
|
+
const existing2 = chatSessionMap.get(chatId);
|
|
718
|
+
if (existing2) {
|
|
719
|
+
existing2.stopped = true;
|
|
720
|
+
if (existing2.spinnerTimer) { clearInterval(existing2.spinnerTimer); existing2.spinnerTimer = null; }
|
|
721
|
+
existing2.close();
|
|
722
|
+
const prevTs2 = lastMsgTimestamps.get(chatId);
|
|
723
|
+
if (prevTs2 === undefined || existing2.msgTimestamp > prevTs2) {
|
|
724
|
+
lastMsgTimestamps.set(chatId, existing2.msgTimestamp);
|
|
725
|
+
}
|
|
726
|
+
chatSessionMap.delete(chatId);
|
|
727
|
+
}
|
|
728
|
+
|
|
729
|
+
const targetAdapter = getAdapterForTool(target.tool);
|
|
730
|
+
let cwd2: string;
|
|
731
|
+
try {
|
|
732
|
+
const targetInfo = await targetAdapter.getSessionInfo(target.sessionId);
|
|
733
|
+
cwd2 = targetInfo?.cwd ?? (await getDefaultCwd(chatId));
|
|
734
|
+
} catch {
|
|
735
|
+
cwd2 = await getDefaultCwd(chatId);
|
|
736
|
+
}
|
|
737
|
+
|
|
738
|
+
const descPrefix2 = sessionPrefixForTool(target.tool);
|
|
739
|
+
const newName2 = sessionChatName("新会话", cwd2);
|
|
740
|
+
await updateChatInfo(freshToken, chatId, newName2, `${descPrefix2} ${target.sessionId}`);
|
|
741
|
+
|
|
742
|
+
sessionInfoMap.set(chatId, {
|
|
743
|
+
sessionId: target.sessionId,
|
|
744
|
+
turnCount: target.turnCount,
|
|
745
|
+
lastContextTokens: 0,
|
|
746
|
+
startTime: Date.now(),
|
|
747
|
+
tool: target.tool,
|
|
748
|
+
});
|
|
749
|
+
await recordSessionRegistry({
|
|
750
|
+
chatId,
|
|
751
|
+
sessionId: target.sessionId,
|
|
752
|
+
tool: target.tool,
|
|
753
|
+
chatName: newName2,
|
|
754
|
+
running: false,
|
|
755
|
+
});
|
|
756
|
+
|
|
757
|
+
setChatAvatar(freshToken, chatId, target.tool, "new").catch(() => {});
|
|
758
|
+
|
|
759
|
+
const targetToolLabel = toolDisplayName(target.tool);
|
|
760
|
+
await sendCardReply(
|
|
761
|
+
freshToken, chatId, `${targetToolLabel} Session Switched`,
|
|
762
|
+
`已切换到 **${targetToolLabel}** 会话。\n\n` +
|
|
763
|
+
`**序号:** ${index + 1}\n` +
|
|
764
|
+
`**Session ID:** ${target.sessionId}\n` +
|
|
765
|
+
`**工作目录:** \`${cwd2}\`\n\n` +
|
|
766
|
+
`直接在这里发消息即可继续对话。`,
|
|
767
|
+
"green"
|
|
768
|
+
);
|
|
769
|
+
|
|
770
|
+
console.log(`[${ts()}] [SESSION] Switched to session ${target.sessionId} (#${index + 1})`);
|
|
771
|
+
logTrace(tid, "DONE", { outcome: "session_switch", sessionId: target.sessionId, index: index + 1, cwd: cwd2 });
|
|
772
|
+
return;
|
|
773
|
+
}
|
|
774
|
+
|
|
674
775
|
// /git <args>:在「当前会话工作目录」执行 git 命令,把输出回发到群里。
|
|
675
776
|
// 注意 cwd 必须取自 adapter.getSessionInfo(会话真实 cwd),而非
|
|
676
777
|
// getDefaultCwd(下一次 /new 才会使用的默认路径)。
|
|
677
|
-
if (
|
|
778
|
+
if (textLower.startsWith("/git ") || textLower === "/git") {
|
|
678
779
|
const args = text === "/git" ? "" : text.slice(5).trim();
|
|
679
780
|
logTrace(tid, "BRANCH", { cmd: "/git", args: args || "(none)" });
|
|
680
781
|
if (!args) {
|
|
@@ -732,6 +833,18 @@ async function handleCommand(text: string, chatId: string, openId: string, msgTi
|
|
|
732
833
|
console.log(`[${ts()}] [SKIP] Older message (${msgTimestamp} <= ${existing.msgTimestamp}), ignoring`);
|
|
733
834
|
return;
|
|
734
835
|
}
|
|
836
|
+
|
|
837
|
+
if (!ALLOW_INTERRUPT) {
|
|
838
|
+
logTrace(tid, "BLOCKED", { outcome: "interrupt_disabled", sessionId });
|
|
839
|
+
console.log(`[${ts()}] [BLOCKED] allowInterrupt=false, ignoring message during generation. Hint sent to user.`);
|
|
840
|
+
await sendCardReply(
|
|
841
|
+
freshToken, chatId, "生成中",
|
|
842
|
+
"AI 正在生成回复中,请先点击卡片上的「停止」按钮停止当前生成后,再发送新消息。",
|
|
843
|
+
"yellow"
|
|
844
|
+
);
|
|
845
|
+
return;
|
|
846
|
+
}
|
|
847
|
+
|
|
735
848
|
existing.stopped = true;
|
|
736
849
|
if (existing.spinnerTimer) { clearInterval(existing.spinnerTimer); existing.spinnerTimer = null; }
|
|
737
850
|
existing.close();
|
|
@@ -1011,7 +1124,7 @@ async function startBotServiceCore(): Promise<void> {
|
|
|
1011
1124
|
const openId = event.sender?.sender_id?.open_id ?? "";
|
|
1012
1125
|
const chatId = message.chat_id ?? "";
|
|
1013
1126
|
appendChatLog(chatId, openId, text);
|
|
1014
|
-
if (text === "/new" && openId) {
|
|
1127
|
+
if (text.toLowerCase() === "/new" && openId) {
|
|
1015
1128
|
console.log(`[MSG] /new from ${openId}, but local relay does not handle /new yet. Use SDK mode.`);
|
|
1016
1129
|
}
|
|
1017
1130
|
} catch { /* ignore */ }
|
package/src/session.ts
CHANGED
|
@@ -168,6 +168,87 @@ export async function getSessionTool(sessionId: string): Promise<string | null>
|
|
|
168
168
|
return record?.tool ?? null;
|
|
169
169
|
}
|
|
170
170
|
|
|
171
|
+
// ---------------------------------------------------------------------------
|
|
172
|
+
// Conversation session registry for /sessions
|
|
173
|
+
// ---------------------------------------------------------------------------
|
|
174
|
+
|
|
175
|
+
export const SESSION_REGISTRY_FILE = join(USER_DATA_DIR, "state", "session-registry.json");
|
|
176
|
+
let sessionRegistryFile = SESSION_REGISTRY_FILE;
|
|
177
|
+
|
|
178
|
+
export interface SessionRegistryUpdate {
|
|
179
|
+
chatId: string;
|
|
180
|
+
sessionId: string;
|
|
181
|
+
tool: string;
|
|
182
|
+
chatName?: string;
|
|
183
|
+
turnCount?: number;
|
|
184
|
+
lastContextTokens?: number;
|
|
185
|
+
startTime?: number;
|
|
186
|
+
updatedAt?: number;
|
|
187
|
+
running?: boolean;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
interface SessionRegistryRecord {
|
|
191
|
+
chatId: string;
|
|
192
|
+
sessionId: string;
|
|
193
|
+
tool: string;
|
|
194
|
+
chatName: string;
|
|
195
|
+
turnCount: number;
|
|
196
|
+
lastContextTokens: number;
|
|
197
|
+
startTime: number;
|
|
198
|
+
updatedAt: number;
|
|
199
|
+
running: boolean;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
type SessionRegistryData = Record<string, SessionRegistryRecord>;
|
|
203
|
+
|
|
204
|
+
async function loadSessionRegistry(): Promise<SessionRegistryData> {
|
|
205
|
+
try {
|
|
206
|
+
const raw = await readFile(sessionRegistryFile, "utf-8");
|
|
207
|
+
const parsed = JSON.parse(raw) as SessionRegistryData;
|
|
208
|
+
return parsed && typeof parsed === "object" ? parsed : {};
|
|
209
|
+
} catch {
|
|
210
|
+
return {};
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
async function saveSessionRegistry(data: SessionRegistryData): Promise<void> {
|
|
215
|
+
try {
|
|
216
|
+
await mkdir(dirname(sessionRegistryFile), { recursive: true });
|
|
217
|
+
await writeFile(sessionRegistryFile, JSON.stringify(data, null, 2), "utf-8");
|
|
218
|
+
} catch (err) {
|
|
219
|
+
console.error(`[${ts()}] Failed to save session-registry.json: ${(err as Error).message}`);
|
|
220
|
+
fileLog.flush();
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
export async function recordSessionRegistry(update: SessionRegistryUpdate): Promise<void> {
|
|
225
|
+
const data = await loadSessionRegistry();
|
|
226
|
+
const existing = data[update.chatId];
|
|
227
|
+
const now = update.updatedAt ?? Date.now();
|
|
228
|
+
|
|
229
|
+
data[update.chatId] = {
|
|
230
|
+
chatId: update.chatId,
|
|
231
|
+
sessionId: update.sessionId,
|
|
232
|
+
tool: update.tool,
|
|
233
|
+
chatName: update.chatName ?? existing?.chatName ?? "",
|
|
234
|
+
turnCount: update.turnCount ?? existing?.turnCount ?? 0,
|
|
235
|
+
lastContextTokens: update.lastContextTokens ?? existing?.lastContextTokens ?? 0,
|
|
236
|
+
startTime: update.startTime ?? existing?.startTime ?? now,
|
|
237
|
+
updatedAt: now,
|
|
238
|
+
running: update.running ?? existing?.running ?? false,
|
|
239
|
+
};
|
|
240
|
+
|
|
241
|
+
await saveSessionRegistry(data);
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
export function _setSessionRegistryFileForTest(filePath: string): void {
|
|
245
|
+
sessionRegistryFile = filePath;
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
export function _resetSessionRegistryFileForTest(): void {
|
|
249
|
+
sessionRegistryFile = SESSION_REGISTRY_FILE;
|
|
250
|
+
}
|
|
251
|
+
|
|
171
252
|
// ---------------------------------------------------------------------------
|
|
172
253
|
// accumulateBlockContent — 将 UnifiedBlock 累积到渲染状态(纯函数,可测试)
|
|
173
254
|
// ---------------------------------------------------------------------------
|
|
@@ -385,13 +466,24 @@ export async function resumeAndPrompt(
|
|
|
385
466
|
|
|
386
467
|
const now = Date.now();
|
|
387
468
|
const existingInfo = sessionInfoMap.get(chatId);
|
|
469
|
+
const nextTurnCount = (existingInfo?.turnCount ?? 0) + 1;
|
|
470
|
+
const nextContextTokens = existingInfo?.lastContextTokens ?? 0;
|
|
388
471
|
sessionInfoMap.set(chatId, {
|
|
389
472
|
sessionId,
|
|
390
|
-
turnCount:
|
|
391
|
-
lastContextTokens:
|
|
473
|
+
turnCount: nextTurnCount,
|
|
474
|
+
lastContextTokens: nextContextTokens,
|
|
392
475
|
startTime: now,
|
|
393
476
|
tool,
|
|
394
477
|
});
|
|
478
|
+
await recordSessionRegistry({
|
|
479
|
+
chatId,
|
|
480
|
+
sessionId,
|
|
481
|
+
tool,
|
|
482
|
+
turnCount: nextTurnCount,
|
|
483
|
+
lastContextTokens: nextContextTokens,
|
|
484
|
+
startTime: now,
|
|
485
|
+
running: true,
|
|
486
|
+
});
|
|
395
487
|
|
|
396
488
|
let cardId: string | null = null;
|
|
397
489
|
cardId = await createCardKitCard(token, buildProgressCard("", { showStop: true, headerTitle: "生成中..." })).catch((err) => {
|
|
@@ -512,6 +604,13 @@ export async function resumeAndPrompt(
|
|
|
512
604
|
if (block.type === "compact_boundary" && block.post_tokens) {
|
|
513
605
|
const info = sessionInfoMap.get(chatId);
|
|
514
606
|
if (info) { info.lastContextTokens = block.post_tokens; }
|
|
607
|
+
await recordSessionRegistry({
|
|
608
|
+
chatId,
|
|
609
|
+
sessionId,
|
|
610
|
+
tool,
|
|
611
|
+
lastContextTokens: block.post_tokens,
|
|
612
|
+
running: true,
|
|
613
|
+
});
|
|
515
614
|
}
|
|
516
615
|
}
|
|
517
616
|
}
|
|
@@ -561,6 +660,16 @@ export async function resumeAndPrompt(
|
|
|
561
660
|
const finalReply = pickFinalReply(state).trim();
|
|
562
661
|
|
|
563
662
|
if (wasStopped) {
|
|
663
|
+
const finalInfo = sessionInfoMap.get(chatId);
|
|
664
|
+
await recordSessionRegistry({
|
|
665
|
+
chatId,
|
|
666
|
+
sessionId,
|
|
667
|
+
tool,
|
|
668
|
+
turnCount: finalInfo?.turnCount ?? nextTurnCount,
|
|
669
|
+
lastContextTokens: finalInfo?.lastContextTokens ?? nextContextTokens,
|
|
670
|
+
startTime: finalInfo?.startTime ?? now,
|
|
671
|
+
running: false,
|
|
672
|
+
});
|
|
564
673
|
if (finalReply) {
|
|
565
674
|
await sendTextReply(token, chatId, finalReply).catch((err) =>
|
|
566
675
|
console.error(`[${ts()}] Failed to send partial text: ${(err as Error).message}`)
|
|
@@ -597,6 +706,16 @@ export async function resumeAndPrompt(
|
|
|
597
706
|
}
|
|
598
707
|
|
|
599
708
|
console.log(`[${ts()}] Session ${sessionId} stream complete (content chunks: ${state.chunkCount})`);
|
|
709
|
+
const finalInfo = sessionInfoMap.get(chatId);
|
|
710
|
+
await recordSessionRegistry({
|
|
711
|
+
chatId,
|
|
712
|
+
sessionId,
|
|
713
|
+
tool,
|
|
714
|
+
turnCount: finalInfo?.turnCount ?? nextTurnCount,
|
|
715
|
+
lastContextTokens: finalInfo?.lastContextTokens ?? nextContextTokens,
|
|
716
|
+
startTime: finalInfo?.startTime ?? now,
|
|
717
|
+
running: false,
|
|
718
|
+
});
|
|
600
719
|
if (tid) logTrace(tid, "SESSION_END", { sessionId, chunks: state.chunkCount, finalTextLen: finalReply.length });
|
|
601
720
|
}
|
|
602
721
|
|
|
@@ -620,6 +739,7 @@ export const UNKNOWN_MODEL_PLACEHOLDER = "—";
|
|
|
620
739
|
|
|
621
740
|
export interface SessionStatus {
|
|
622
741
|
sessionId: string;
|
|
742
|
+
chatName: string;
|
|
623
743
|
running: boolean;
|
|
624
744
|
turnCount: number;
|
|
625
745
|
lastContextTokens: number;
|
|
@@ -666,8 +786,12 @@ export async function getSessionStatus(chatId: string): Promise<SessionStatus |
|
|
|
666
786
|
const active = chatSessionMap.get(chatId);
|
|
667
787
|
const { model, effort } = await resolveModelEffort(info.tool, info.sessionId);
|
|
668
788
|
|
|
789
|
+
const registry = await loadSessionRegistry();
|
|
790
|
+
const chatName = registry[chatId]?.chatName ?? "";
|
|
791
|
+
|
|
669
792
|
return {
|
|
670
793
|
sessionId: info.sessionId,
|
|
794
|
+
chatName,
|
|
671
795
|
running: active !== undefined && !active.stopped,
|
|
672
796
|
turnCount: info.turnCount,
|
|
673
797
|
lastContextTokens: info.lastContextTokens,
|
|
@@ -681,6 +805,7 @@ export async function getSessionStatus(chatId: string): Promise<SessionStatus |
|
|
|
681
805
|
export interface SessionsListEntry {
|
|
682
806
|
chatId: string;
|
|
683
807
|
sessionId: string;
|
|
808
|
+
chatName: string;
|
|
684
809
|
active: boolean;
|
|
685
810
|
turnCount: number;
|
|
686
811
|
startTime: number;
|
|
@@ -691,16 +816,20 @@ export interface SessionsListEntry {
|
|
|
691
816
|
}
|
|
692
817
|
|
|
693
818
|
export async function getAllSessionsStatus(): Promise<SessionsListEntry[]> {
|
|
694
|
-
const
|
|
819
|
+
const registry = await loadSessionRegistry();
|
|
820
|
+
const entries = Object.values(registry)
|
|
821
|
+
.filter((record) => record.chatId && record.sessionId && record.tool)
|
|
822
|
+
.sort((a, b) => b.updatedAt - a.updatedAt)
|
|
823
|
+
.slice(0, 20);
|
|
695
824
|
// 并行解析每个 session 的 model/effort(cursor 涉及异步 store IO)
|
|
696
825
|
return Promise.all(
|
|
697
|
-
entries.map(async (
|
|
698
|
-
const active = chatSessionMap.get(chatId);
|
|
826
|
+
entries.map(async (info) => {
|
|
699
827
|
const { model, effort } = await resolveModelEffort(info.tool, info.sessionId);
|
|
700
828
|
return {
|
|
701
|
-
chatId,
|
|
829
|
+
chatId: info.chatId,
|
|
702
830
|
sessionId: info.sessionId,
|
|
703
|
-
|
|
831
|
+
chatName: info.chatName || "",
|
|
832
|
+
active: info.running === true,
|
|
704
833
|
turnCount: info.turnCount,
|
|
705
834
|
startTime: info.startTime,
|
|
706
835
|
model,
|
package/src/shared.ts
CHANGED
|
@@ -1,17 +1,17 @@
|
|
|
1
1
|
import { execSync } from "node:child_process";
|
|
2
|
-
import {
|
|
3
|
-
appendFileSync,
|
|
4
|
-
existsSync,
|
|
5
|
-
mkdirSync,
|
|
6
|
-
readFileSync,
|
|
7
|
-
unlinkSync,
|
|
8
|
-
writeFileSync,
|
|
9
|
-
} from "node:fs";
|
|
10
|
-
import { createServer } from "node:http";
|
|
11
|
-
import { homedir } from "node:os";
|
|
12
|
-
import { join } from "node:path";
|
|
13
|
-
import { inspect } from "node:util";
|
|
14
|
-
import { WebSocketServer, WebSocket } from "ws";
|
|
2
|
+
import {
|
|
3
|
+
appendFileSync,
|
|
4
|
+
existsSync,
|
|
5
|
+
mkdirSync,
|
|
6
|
+
readFileSync,
|
|
7
|
+
unlinkSync,
|
|
8
|
+
writeFileSync,
|
|
9
|
+
} from "node:fs";
|
|
10
|
+
import { createServer } from "node:http";
|
|
11
|
+
import { homedir } from "node:os";
|
|
12
|
+
import { join } from "node:path";
|
|
13
|
+
import { inspect } from "node:util";
|
|
14
|
+
import { WebSocketServer, WebSocket } from "ws";
|
|
15
15
|
|
|
16
16
|
import { printServiceDidNotStart } from "./exit-banner.ts";
|
|
17
17
|
|
|
@@ -355,56 +355,56 @@ export function installCrashLogging(opts: CrashHandlersOptions = {}): InstallCra
|
|
|
355
355
|
// 文件日志:同时输出到控制台和日志文件
|
|
356
356
|
// ---------------------------------------------------------------------------
|
|
357
357
|
|
|
358
|
-
export function setupFileLogging(logDir: string, prefix: string): { logPath: string; flush: () => void } {
|
|
359
|
-
mkdirSync(logDir, { recursive: true });
|
|
360
|
-
const ts = new Date().toISOString().replace(/[:.]/g, "-").slice(0, 19);
|
|
361
|
-
const logPath = join(logDir, `${prefix}-${ts}.log`);
|
|
362
|
-
writeFileSync(logPath, "", { flag: "a", encoding: "utf8" });
|
|
363
|
-
const origConsoleLog = console.log.bind(console);
|
|
364
|
-
const origConsoleError = console.error.bind(console);
|
|
365
|
-
const formatArg = (arg: unknown): string => {
|
|
366
|
-
if (typeof arg === "string") return arg;
|
|
367
|
-
if (arg instanceof Error) return arg.stack ?? arg.message;
|
|
368
|
-
return inspect(arg, { depth: 6, breakLength: Infinity, maxArrayLength: 200 });
|
|
369
|
-
};
|
|
370
|
-
const writeLine = (level: string, args: unknown[]) => {
|
|
371
|
-
try {
|
|
372
|
-
const line = args.map(formatArg).join(" ");
|
|
373
|
-
appendFileSync(logPath, `[${new Date().toISOString()}] [${level}] ${line}\n`, "utf8");
|
|
374
|
-
} catch (err) {
|
|
375
|
-
try {
|
|
376
|
-
appendStartupTrace("fileLog write failed", { level, error: err instanceof Error ? err.message : String(err) });
|
|
377
|
-
} catch {
|
|
378
|
-
// 日志系统自身不能影响主流程
|
|
379
|
-
}
|
|
380
|
-
}
|
|
381
|
-
};
|
|
382
|
-
console.log = (...args: unknown[]) => {
|
|
383
|
-
writeLine("LOG", args);
|
|
384
|
-
try {
|
|
385
|
-
origConsoleLog(...args);
|
|
386
|
-
} catch {
|
|
387
|
-
// 控制台输出失败也不能拖垮服务
|
|
388
|
-
}
|
|
389
|
-
};
|
|
390
|
-
console.error = (...args: unknown[]) => {
|
|
391
|
-
writeLine("ERR", args);
|
|
392
|
-
try {
|
|
393
|
-
origConsoleError(...args);
|
|
394
|
-
} catch {
|
|
395
|
-
// 控制台输出失败也不能拖垮服务
|
|
396
|
-
}
|
|
397
|
-
};
|
|
398
|
-
const flush = () => {
|
|
399
|
-
try {
|
|
400
|
-
appendFileSync(logPath, "", "utf8");
|
|
401
|
-
} catch (err) {
|
|
402
|
-
appendStartupTrace("fileLog flush failed", { error: err instanceof Error ? err.message : String(err) });
|
|
403
|
-
}
|
|
404
|
-
};
|
|
405
|
-
origConsoleLog(`Log file: ${logPath}`);
|
|
406
|
-
return { logPath, flush };
|
|
407
|
-
}
|
|
358
|
+
export function setupFileLogging(logDir: string, prefix: string): { logPath: string; flush: () => void } {
|
|
359
|
+
mkdirSync(logDir, { recursive: true });
|
|
360
|
+
const ts = new Date().toISOString().replace(/[:.]/g, "-").slice(0, 19);
|
|
361
|
+
const logPath = join(logDir, `${prefix}-${ts}.log`);
|
|
362
|
+
writeFileSync(logPath, "", { flag: "a", encoding: "utf8" });
|
|
363
|
+
const origConsoleLog = console.log.bind(console);
|
|
364
|
+
const origConsoleError = console.error.bind(console);
|
|
365
|
+
const formatArg = (arg: unknown): string => {
|
|
366
|
+
if (typeof arg === "string") return arg;
|
|
367
|
+
if (arg instanceof Error) return arg.stack ?? arg.message;
|
|
368
|
+
return inspect(arg, { depth: 6, breakLength: Infinity, maxArrayLength: 200 });
|
|
369
|
+
};
|
|
370
|
+
const writeLine = (level: string, args: unknown[]) => {
|
|
371
|
+
try {
|
|
372
|
+
const line = args.map(formatArg).join(" ");
|
|
373
|
+
appendFileSync(logPath, `[${new Date().toISOString()}] [${level}] ${line}\n`, "utf8");
|
|
374
|
+
} catch (err) {
|
|
375
|
+
try {
|
|
376
|
+
appendStartupTrace("fileLog write failed", { level, error: err instanceof Error ? err.message : String(err) });
|
|
377
|
+
} catch {
|
|
378
|
+
// 日志系统自身不能影响主流程
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
};
|
|
382
|
+
console.log = (...args: unknown[]) => {
|
|
383
|
+
writeLine("LOG", args);
|
|
384
|
+
try {
|
|
385
|
+
origConsoleLog(...args);
|
|
386
|
+
} catch {
|
|
387
|
+
// 控制台输出失败也不能拖垮服务
|
|
388
|
+
}
|
|
389
|
+
};
|
|
390
|
+
console.error = (...args: unknown[]) => {
|
|
391
|
+
writeLine("ERR", args);
|
|
392
|
+
try {
|
|
393
|
+
origConsoleError(...args);
|
|
394
|
+
} catch {
|
|
395
|
+
// 控制台输出失败也不能拖垮服务
|
|
396
|
+
}
|
|
397
|
+
};
|
|
398
|
+
const flush = () => {
|
|
399
|
+
try {
|
|
400
|
+
appendFileSync(logPath, "", "utf8");
|
|
401
|
+
} catch (err) {
|
|
402
|
+
appendStartupTrace("fileLog flush failed", { error: err instanceof Error ? err.message : String(err) });
|
|
403
|
+
}
|
|
404
|
+
};
|
|
405
|
+
origConsoleLog(`Log file: ${logPath}`);
|
|
406
|
+
return { logPath, flush };
|
|
407
|
+
}
|
|
408
408
|
|
|
409
409
|
// ---------------------------------------------------------------------------
|
|
410
410
|
// 本地 WebSocket 中继服务器(同一端口、多客户端广播)
|