chatccc 0.2.44 → 0.2.46

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/src/index.ts CHANGED
@@ -41,7 +41,6 @@ import {
41
41
  CLAUDE_EFFORT,
42
42
  CLAUDE_MODEL,
43
43
  GIT_TIMEOUT_MS,
44
- ALLOW_INTERRUPT,
45
44
  reloadConfigFromDisk,
46
45
  anthropicConfigDisplay,
47
46
  LOCAL_RELAY_URL,
@@ -84,17 +83,14 @@ import {
84
83
  reportPermissionResults,
85
84
  setPlatform,
86
85
  } from "./feishu-platform.ts";
87
- import { buildHelpCard, buildStatusCard, buildProgressCard, buildCdContent, buildCdCard, buildSessionsCard } from "./cards.ts";
88
- import { updateCardKitCard } from "./cardkit.ts";
86
+ import { buildHelpCard, buildStatusCard, buildCdContent, buildCdCard, buildSessionsCard } from "./cards.ts";
89
87
  import { handleAgentImageRequest } from "./agent-image-rpc.ts";
90
88
  import { handleAgentFileRequest } from "./agent-file-rpc.ts";
91
- import { handleAgentGrantsRequest } from "./agent-grants-rpc.ts";
92
89
  import { SimulatedPlatform, SIM_DEFAULT_CHAT_ID } from "./sim-platform.ts";
93
90
  import { setMessageHandler } from "./sim-store.ts";
94
91
  import { formatGitResult, gitResultHeaderTemplate, runGitCommand } from "./git-command.ts";
95
92
  import {
96
93
  MAX_PROCESSED,
97
- chatSessionMap,
98
94
  getSessionStatus,
99
95
  getAllSessionsStatus,
100
96
  initClaudeSession,
@@ -105,7 +101,18 @@ import {
105
101
  sessionInfoMap,
106
102
  recordSessionRegistry,
107
103
  getAdapterForTool,
104
+ stopSession,
105
+ loadSessionRegistryForBinding,
108
106
  } from "./session.ts";
107
+ import {
108
+ bindChatToSession,
109
+ unbindChatFromSession,
110
+ getChatsForSession,
111
+ isSessionRunning,
112
+ activePrompts,
113
+ rebuildSessionChatsFromRegistry,
114
+ } from "./session-chat-binding.ts";
115
+ import { fixStaleStreamStates } from "./stream-state.ts";
109
116
 
110
117
  export function cwdDisplayName(cwd: string): string {
111
118
  const trimmed = cwd.trim().replace(/[\\/]+$/, "");
@@ -232,7 +239,7 @@ function parseCardAction(data: unknown): CardActionResult | null {
232
239
  }
233
240
  if (!cmd) return null;
234
241
 
235
- const CMD_MAP: Record<string, string> = { stop: "/stop", new: "/new", "new claude": "/new claude", "new cursor": "/new cursor", "new codex": "/new codex", restart: "/restart", status: "/status", cd: "/cd", sessions: "/sessions", forget: "/forget" };
242
+ const CMD_MAP: Record<string, string> = { stop: "/stop", new: "/new", "new claude": "/new claude", "new cursor": "/new cursor", "new codex": "/new codex", restart: "/restart", status: "/status", cd: "/cd", sessions: "/sessions", newh: "/newh" };
236
243
  let text = CMD_MAP[cmd] ?? "";
237
244
  if (cmd === "cd" && typeof action.value === "object" && action.value !== null) {
238
245
  const path = (action.value as Record<string, string>).path;
@@ -462,6 +469,41 @@ async function handleCommand(text: string, chatId: string, openId: string, msgTi
462
469
  const cwd = sessionCwd;
463
470
  const initialName = sessionChatName("新会话", cwd);
464
471
 
472
+ // 私聊:不创建群,直接绑定 session 到当前私聊
473
+ if (chatType === "p2p") {
474
+ bindChatToSession(sessionId, chatId);
475
+ sessionInfoMap.set(chatId, {
476
+ sessionId,
477
+ turnCount: 0,
478
+ lastContextTokens: 0,
479
+ startTime: Date.now(),
480
+ tool,
481
+ });
482
+ await setDefaultCwd(cwd, chatId);
483
+ await recordSessionRegistry({
484
+ chatId,
485
+ sessionId,
486
+ tool,
487
+ chatName: initialName,
488
+ turnCount: 0,
489
+ startTime: Date.now(),
490
+ running: false,
491
+ });
492
+ await sendCardReply(
493
+ freshToken, chatId, `${toolLabel} Session Ready`,
494
+ `这是你的 **${toolLabel}** 私聊会话。\n\n` +
495
+ `**Session ID:** ${sessionId}\n` +
496
+ `**工作目录:** \`${cwd}\`\n\n` +
497
+ `直接在这里发消息即可与 ${toolLabel} 对话。\n\n` +
498
+ `发送 **/sessions** 查看所有会话状态。\n` +
499
+ `发送 \`/git <子命令>\` 在本会话工作目录执行 git,例如 \`/git status\`、\`/git log --oneline -n 5\`。`,
500
+ "green"
501
+ );
502
+ console.log(`[${ts()}] [NEW] P2P session created: ${sessionId} (${toolLabel})`);
503
+ logTrace(tid, "DONE", { outcome: "session_ready_p2p", chatId, sessionId, tool });
504
+ return;
505
+ }
506
+
465
507
  let newChatId: string;
466
508
  try {
467
509
  newChatId = await createGroupChat(freshToken, initialName, [openId]);
@@ -515,24 +557,62 @@ async function handleCommand(text: string, chatId: string, openId: string, msgTi
515
557
  return;
516
558
  }
517
559
 
518
- // 私聊没有群 description,无法存储/获取 session ID,跳过群聊检测直接发 help card
560
+ // 检测会话上下文:群聊从 description 获取,私聊从 session-registry 获取
561
+ let sessionId: string | null = null;
562
+ let descriptionTool: string | null = null;
563
+ let toolLabel: string | null = null;
564
+ let chatInfo: Awaited<ReturnType<typeof getChatInfo>> | undefined;
565
+ let description: string | undefined;
566
+
519
567
  if (chatType !== "p2p") {
520
- try {
521
- const token = await getTenantAccessToken();
522
- const chatInfo = await getChatInfo(token, chatId);
523
- const description = chatInfo.description;
524
- const sessionInfo = extractSessionInfo(description);
525
-
526
- if (sessionInfo) {
527
- const sessionId = sessionInfo.sessionId;
528
- const descriptionTool = sessionInfo.tool;
529
- const toolLabel = toolDisplayName(descriptionTool);
530
- logTrace(tid, "BRANCH", { sessionId, tool: descriptionTool });
531
- console.log(`[${ts()}] [RESUME] ${toolLabel} session group detected, session=${sessionId} tool=${descriptionTool}`);
568
+ try {
569
+ const token = await getTenantAccessToken();
570
+ chatInfo = await getChatInfo(token, chatId);
571
+ description = chatInfo.description;
572
+ const sessionInfo = extractSessionInfo(description);
573
+ if (sessionInfo) {
574
+ sessionId = sessionInfo.sessionId;
575
+ descriptionTool = sessionInfo.tool;
576
+ toolLabel = toolDisplayName(descriptionTool);
577
+ }
578
+ } catch (err) {
579
+ logTrace(tid, "BRANCH", { reason: "get_chat_info_failed", error: (err as Error).message });
580
+ console.log(`[${ts()}] [INFO] Cannot get chat info for ${chatId}: ${(err as Error).message}`);
581
+ }
582
+ } else {
583
+ // 私聊:从 session-registry.json 获取绑定的 session
584
+ try {
585
+ const registry = await loadSessionRegistryForBinding();
586
+ const record = registry[chatId];
587
+ if (record && record.sessionId && record.tool) {
588
+ sessionId = record.sessionId;
589
+ descriptionTool = record.tool;
590
+ toolLabel = toolDisplayName(descriptionTool);
591
+ // 确保 sessionInfoMap 中有该私聊的信息
592
+ if (!sessionInfoMap.has(chatId)) {
593
+ sessionInfoMap.set(chatId, {
594
+ sessionId,
595
+ turnCount: record.turnCount ?? 0,
596
+ lastContextTokens: record.lastContextTokens ?? 0,
597
+ startTime: record.startTime ?? Date.now(),
598
+ tool: descriptionTool,
599
+ });
600
+ }
601
+ bindChatToSession(sessionId, chatId);
602
+ }
603
+ } catch (err) {
604
+ console.log(`[${ts()}] [INFO] Cannot load registry for p2p ${chatId}: ${(err as Error).message}`);
605
+ }
606
+ }
607
+
608
+ if (sessionId && descriptionTool && toolLabel) {
609
+ // 有会话上下文 — 路由到命令处理或 prompt
610
+ logTrace(tid, "BRANCH", { sessionId, tool: descriptionTool });
611
+ console.log(`[${ts()}] [RESUME] ${toolLabel} session group detected, session=${sessionId} tool=${descriptionTool}`);
532
612
 
533
613
  const freshToken = await getTenantAccessToken();
534
614
 
535
- if (isUntitledSessionChatName(chatInfo.name)) {
615
+ if (chatType !== "p2p" && isUntitledSessionChatName(chatInfo!.name) && !textLower.startsWith("/")) {
536
616
  const MAX_PREFIX = 10;
537
617
  const prefix = text.slice(0, MAX_PREFIX);
538
618
  const adapter = getAdapterForTool(descriptionTool);
@@ -540,9 +620,9 @@ async function handleCommand(text: string, chatId: string, openId: string, msgTi
540
620
  const sessionCwd = info?.cwd ?? (await getDefaultCwd(chatId));
541
621
  const newName = sessionChatName(prefix, sessionCwd);
542
622
  try {
543
- await updateChatInfo(freshToken, chatId, newName, description);
623
+ await updateChatInfo(freshToken, chatId, newName, description!);
544
624
  console.log(`[${ts()}] [RENAME] First message → group renamed to "${newName}"`);
545
- await recordSessionRegistry({ chatId, chatName: newName }).catch(() => {});
625
+ await recordSessionRegistry({ chatId, sessionId, tool: descriptionTool, chatName: newName }).catch(() => {});
546
626
  } catch (err) {
547
627
  console.error(`[${ts()}] [RENAME] Failed: ${(err as Error).message}`);
548
628
  }
@@ -550,15 +630,7 @@ async function handleCommand(text: string, chatId: string, openId: string, msgTi
550
630
 
551
631
  if (textLower === "/stop") {
552
632
  logTrace(tid, "BRANCH", { cmd: "/stop" });
553
- const cEntry = chatSessionMap.get(chatId);
554
- if (cEntry) {
555
- cEntry.stopped = true;
556
- if (cEntry.spinnerTimer) { clearInterval(cEntry.spinnerTimer); cEntry.spinnerTimer = null; }
557
- cEntry.close();
558
- const prevTs = lastMsgTimestamps.get(chatId);
559
- if (prevTs === undefined || cEntry.msgTimestamp > prevTs) {
560
- lastMsgTimestamps.set(chatId, cEntry.msgTimestamp);
561
- }
633
+ if (stopSession(sessionId)) {
562
634
  console.log(`[${ts()}] [STOP] User sent /stop, session=${sessionId}`);
563
635
  await sendTextReply(freshToken, chatId, "会话已停止。").catch(() => {});
564
636
  logTrace(tid, "DONE", { outcome: "stopped" });
@@ -572,8 +644,7 @@ async function handleCommand(text: string, chatId: string, openId: string, msgTi
572
644
  if (textLower === "/status") {
573
645
  logTrace(tid, "BRANCH", { cmd: "/status" });
574
646
  const status = await getSessionStatus(chatId);
575
- const running = chatSessionMap.get(chatId);
576
- const isActive = running && !running.stopped;
647
+ const isActive = isSessionRunning(sessionId);
577
648
  const statusText = [
578
649
  `**群名:** ${status?.chatName || "—"}`,
579
650
  `**Session ID:** \`${status?.sessionId ?? sessionId}\``,
@@ -625,8 +696,8 @@ async function handleCommand(text: string, chatId: string, openId: string, msgTi
625
696
  return;
626
697
  }
627
698
 
628
- if (textLower === "/forget") {
629
- logTrace(tid, "BRANCH", { cmd: "/forget" });
699
+ if (textLower === "/newh") {
700
+ logTrace(tid, "BRANCH", { cmd: "/newh" });
630
701
  const adapter = getAdapterForTool(descriptionTool);
631
702
  let cwd: string;
632
703
  try {
@@ -636,32 +707,27 @@ async function handleCommand(text: string, chatId: string, openId: string, msgTi
636
707
  cwd = await getDefaultCwd(chatId);
637
708
  }
638
709
 
639
- const existing = chatSessionMap.get(chatId);
640
- if (existing) {
641
- existing.stopped = true;
642
- if (existing.spinnerTimer) { clearInterval(existing.spinnerTimer); existing.spinnerTimer = null; }
643
- existing.close();
644
- const prevTs = lastMsgTimestamps.get(chatId);
645
- if (prevTs === undefined || existing.msgTimestamp > prevTs) {
646
- lastMsgTimestamps.set(chatId, existing.msgTimestamp);
647
- }
648
- chatSessionMap.delete(chatId);
649
- }
710
+ // abort 旧 session,只解绑当前 chat
711
+ // session 如果正在跑,display loop 继续服务其他群
712
+ unbindChatFromSession(sessionId, chatId);
650
713
 
651
714
  let newSessionId: string;
652
715
  try {
653
716
  const init = await initClaudeSession(descriptionTool, cwd);
654
717
  newSessionId = init.sessionId;
655
718
  } catch (err) {
656
- logTrace(tid, "DONE", { outcome: "forget_session_fail", error: (err as Error).message });
719
+ logTrace(tid, "DONE", { outcome: "newh_session_fail", error: (err as Error).message });
657
720
  await sendCardReply(freshToken, chatId, "Error", `Failed to create new session:\n${(err as Error).message}`, "red");
658
721
  return;
659
722
  }
660
723
 
724
+ // 绑定新 session
725
+ bindChatToSession(newSessionId, chatId);
726
+
661
727
  const descPrefix = sessionPrefixForTool(descriptionTool);
662
728
  const newName = sessionChatName("新会话", cwd);
663
729
  await updateChatInfo(freshToken, chatId, newName, `${descPrefix} ${newSessionId}`);
664
- console.log(`[${ts()}] [FORGET] Group updated: name="${newName}" desc="${descPrefix} ${newSessionId}"`);
730
+ console.log(`[${ts()}] [NEWH] Group updated: name="${newName}" desc="${descPrefix} ${newSessionId}"`);
665
731
 
666
732
  sessionInfoMap.set(chatId, {
667
733
  sessionId: newSessionId,
@@ -683,6 +749,12 @@ async function handleCommand(text: string, chatId: string, openId: string, msgTi
683
749
 
684
750
  setChatAvatar(freshToken, chatId, descriptionTool, "new").catch(() => {});
685
751
 
752
+ // 如果新 session 有活跃 prompt,启动 display loop 让本群也能看到
753
+ if (isSessionRunning(newSessionId)) {
754
+ const { ensureDisplayLoop } = await import("./session.ts");
755
+ ensureDisplayLoop(newSessionId);
756
+ }
757
+
686
758
  await sendCardReply(
687
759
  freshToken, chatId, `${toolLabel} Session Reset`,
688
760
  `会话已重置为新的 **${toolLabel}** 会话。\n\n` +
@@ -692,8 +764,8 @@ async function handleCommand(text: string, chatId: string, openId: string, msgTi
692
764
  "green"
693
765
  );
694
766
 
695
- console.log(`[${ts()}] [FORGET] Session ${sessionId} → ${newSessionId} (same cwd=${cwd})`);
696
- logTrace(tid, "DONE", { outcome: "forget", newSessionId, cwd });
767
+ console.log(`[${ts()}] [NEWH] Session ${sessionId} → ${newSessionId} (same cwd=${cwd})`);
768
+ logTrace(tid, "DONE", { outcome: "newh", newSessionId, cwd });
697
769
  return;
698
770
  }
699
771
 
@@ -720,16 +792,9 @@ async function handleCommand(text: string, chatId: string, openId: string, msgTi
720
792
  }
721
793
  const target = ordered[index];
722
794
 
723
- const existing2 = chatSessionMap.get(chatId);
724
- if (existing2) {
725
- existing2.stopped = true;
726
- if (existing2.spinnerTimer) { clearInterval(existing2.spinnerTimer); existing2.spinnerTimer = null; }
727
- existing2.close();
728
- const prevTs2 = lastMsgTimestamps.get(chatId);
729
- if (prevTs2 === undefined || existing2.msgTimestamp > prevTs2) {
730
- lastMsgTimestamps.set(chatId, existing2.msgTimestamp);
731
- }
732
- chatSessionMap.delete(chatId);
795
+ // abort 当前 chat 的旧 session,只解绑再重新绑定
796
+ if (sessionId) {
797
+ unbindChatFromSession(sessionId, chatId);
733
798
  }
734
799
 
735
800
  const targetAdapter = getAdapterForTool(target.tool);
@@ -741,6 +806,9 @@ async function handleCommand(text: string, chatId: string, openId: string, msgTi
741
806
  cwd2 = await getDefaultCwd(chatId);
742
807
  }
743
808
 
809
+ // 绑定到新 session
810
+ bindChatToSession(target.sessionId, chatId);
811
+
744
812
  const descPrefix2 = sessionPrefixForTool(target.tool);
745
813
  const newName2 = target.chatName || sessionChatName("新会话", cwd2);
746
814
  await updateChatInfo(freshToken, chatId, newName2, `${descPrefix2} ${target.sessionId}`);
@@ -763,14 +831,21 @@ async function handleCommand(text: string, chatId: string, openId: string, msgTi
763
831
 
764
832
  setChatAvatar(freshToken, chatId, target.tool, "new").catch(() => {});
765
833
 
834
+ // 如果新 session 有活跃 prompt,加上 display loop
835
+ if (isSessionRunning(target.sessionId)) {
836
+ const { ensureDisplayLoop } = await import("./session.ts");
837
+ ensureDisplayLoop(target.sessionId);
838
+ }
839
+
766
840
  const targetToolLabel = toolDisplayName(target.tool);
841
+ const busyNote = isSessionRunning(target.sessionId) ? "\n\n⚠️ 该会话当前正在生成中,请等待完成后再发送消息。" : "";
767
842
  await sendCardReply(
768
843
  freshToken, chatId, `${targetToolLabel} Session Switched`,
769
844
  `已切换到 **${targetToolLabel}** 会话。\n\n` +
770
845
  `**序号:** ${index + 1}\n` +
771
846
  `**Session ID:** ${target.sessionId}\n` +
772
847
  `**工作目录:** \`${cwd2}\`\n\n` +
773
- `直接在这里发消息即可继续对话。`,
848
+ `直接在这里发消息即可继续对话。${busyNote}`,
774
849
  "green"
775
850
  );
776
851
 
@@ -832,50 +907,16 @@ async function handleCommand(text: string, chatId: string, openId: string, msgTi
832
907
  return;
833
908
  }
834
909
 
835
- const existing = chatSessionMap.get(chatId);
836
- if (existing && !existing.stopped) {
837
- if (msgTimestamp <= existing.msgTimestamp) {
838
- logTrace(tid, "DONE", { outcome: "skip_old_message", msgTimestamp, existingTimestamp: existing.msgTimestamp });
839
- console.log(`[${ts()}] [SKIP] Older message (${msgTimestamp} <= ${existing.msgTimestamp}), ignoring`);
840
- return;
841
- }
842
-
843
- if (!ALLOW_INTERRUPT) {
844
- logTrace(tid, "BLOCKED", { outcome: "interrupt_disabled", sessionId });
845
- console.log(`[${ts()}] [BLOCKED] allowInterrupt=false, ignoring message during generation. Hint sent to user.`);
846
- await sendCardReply(
847
- freshToken, chatId, "生成中",
848
- "AI 正在生成回复中,Agent 不会遗忘当前进度,停止后仍可从断点继续。\n请先点击卡片上的「停止」按钮,再发送新消息。",
849
- "yellow"
850
- );
851
- return;
852
- }
853
-
854
- existing.stopped = true;
855
- if (existing.spinnerTimer) { clearInterval(existing.spinnerTimer); existing.spinnerTimer = null; }
856
- existing.close();
857
- const prevTs = lastMsgTimestamps.get(chatId);
858
- if (prevTs === undefined || existing.msgTimestamp > prevTs) {
859
- lastMsgTimestamps.set(chatId, existing.msgTimestamp);
860
- }
861
- chatSessionMap.delete(chatId);
862
- console.log(`[${ts()}] [INTERRUPT] New message arrived, cancelled previous session ${sessionId}`);
863
- logTrace(tid, "INTERRUPT", { oldSessionId: sessionId });
864
- if (existing.cardId) {
865
- while (existing.cardBusy) {
866
- await new Promise(r => setTimeout(r, 20));
867
- }
868
- const cardId = existing.cardId;
869
- const currentContent = existing.accumulatedContent;
870
- const interruptedCard = buildProgressCard(
871
- currentContent || "新问题已提交,当前回复已中断。",
872
- { showStop: false, headerTitle: "已中断", headerTemplate: "yellow" }
873
- );
874
- let nextSeq = existing.sequence + 1;
875
- await updateCardKitCard(freshToken, cardId, interruptedCard, nextSeq).catch((err) => {
876
- console.error(`[${ts()}] [INTERRUPT] CardKit update failed: ${(err as Error).message}`);
877
- });
878
- }
910
+ // 并发检查:同一 session 只能有一个活跃 prompt
911
+ if (isSessionRunning(sessionId)) {
912
+ logTrace(tid, "BLOCKED", { outcome: "session_busy", sessionId });
913
+ console.log(`[${ts()}] [BLOCKED] Session ${sessionId} is already generating, rejecting message from chat ${chatId}`);
914
+ await sendCardReply(
915
+ freshToken, chatId, "生成中",
916
+ "该会话正在生成回复中,请等待完成后再发送新消息。",
917
+ "yellow"
918
+ );
919
+ return;
879
920
  }
880
921
 
881
922
  try {
@@ -895,13 +936,8 @@ async function handleCommand(text: string, chatId: string, openId: string, msgTi
895
936
  }
896
937
  return;
897
938
  }
898
- // 群聊但 description 中没有 session info → 也走 help card
899
- logTrace(tid, "BRANCH", { reason: "no_session_info_in_group" });
900
- } catch (err) {
901
- logTrace(tid, "BRANCH", { reason: "get_chat_info_failed", error: (err as Error).message });
902
- console.log(`[${ts()}] [INFO] Cannot get chat info for ${chatId}: ${(err as Error).message}`);
903
- }
904
- } // end if (chatType !== "p2p")
939
+
940
+ // 无会话上下文 help card
905
941
 
906
942
  // 私聊或群聊无 session info → 发送 help card
907
943
  logTrace(tid, "SEND", { method: "help_card", chatId });
@@ -1151,12 +1187,25 @@ async function startBotServiceCore(): Promise<void> {
1151
1187
  });
1152
1188
  } else {
1153
1189
  resetState();
1190
+ // 启动时修正残留的 running 状态并重建 session→chat 映射
1191
+ fixStaleStreamStates().then(async () => {
1192
+ const registry = await loadSessionRegistryForBinding();
1193
+ rebuildSessionChatsFromRegistry(registry);
1194
+ }).catch((err) => console.error(`[${ts()}] Init bindings failed: ${(err as Error).message}`));
1154
1195
 
1155
1196
  const wsClient = new WSClient({
1156
1197
  appId: APP_ID,
1157
1198
  appSecret: APP_SECRET,
1158
- onReady: () => { resetState(); },
1159
- onReconnected: () => { resetState(); },
1199
+ onReady: async () => {
1200
+ resetState();
1201
+ const registry = await loadSessionRegistryForBinding();
1202
+ rebuildSessionChatsFromRegistry(registry);
1203
+ },
1204
+ onReconnected: async () => {
1205
+ resetState();
1206
+ const registry = await loadSessionRegistryForBinding();
1207
+ rebuildSessionChatsFromRegistry(registry);
1208
+ },
1160
1209
  });
1161
1210
 
1162
1211
  console.log(`\n[启动 6/7] 飞书长连接:正在通过 SDK 建立 WebSocket …`);
@@ -1212,7 +1261,7 @@ async function main(): Promise<void> {
1212
1261
  setExtraApiHandler(async (req, res) => {
1213
1262
  const injected = await handleSimInjectMessage(req, res);
1214
1263
  if (injected) return true;
1215
- return (await handleAgentGrantsRequest(req, res)) || (await handleAgentImageRequest(req, res)) || (await handleAgentFileRequest(req, res));
1264
+ return (await handleAgentImageRequest(req, res)) || (await handleAgentFileRequest(req, res));
1216
1265
  });
1217
1266
 
1218
1267
  const simServer = createServer(createUiRouter());
@@ -1270,7 +1319,7 @@ async function main(): Promise<void> {
1270
1319
  });
1271
1320
  });
1272
1321
  setExtraApiHandler(async (req, res) => {
1273
- return (await handleAgentGrantsRequest(req, res)) || (await handleAgentImageRequest(req, res)) || (await handleAgentFileRequest(req, res));
1322
+ return (await handleAgentImageRequest(req, res)) || (await handleAgentFileRequest(req, res));
1274
1323
  });
1275
1324
 
1276
1325
  console.log(`[启动 2/7] 环境与凭证检查`);
@@ -0,0 +1,87 @@
1
+ // ---------------------------------------------------------------------------
2
+ // session ↔ chats 双向映射
3
+ // ---------------------------------------------------------------------------
4
+ // sessionChatsMap: sessionId → Set<chatId>
5
+ // 由 session.ts 在初始化时调用 rebuildSessionChatsFromRegistry 重建
6
+ // ---------------------------------------------------------------------------
7
+
8
+ const sessionChatsMap = new Map<string, Set<string>>();
9
+
10
+ /** 从 registry 数据重建映射(由 session.ts 调用,避免循环依赖) */
11
+ export function rebuildSessionChatsFromRegistry(registry: Record<string, { chatId: string; sessionId: string }>): void {
12
+ sessionChatsMap.clear();
13
+ for (const record of Object.values(registry)) {
14
+ if (!record.sessionId || !record.chatId) continue;
15
+ bindChatToSession(record.sessionId, record.chatId);
16
+ }
17
+ }
18
+
19
+ export function bindChatToSession(sessionId: string, chatId: string): void {
20
+ let chats = sessionChatsMap.get(sessionId);
21
+ if (!chats) {
22
+ chats = new Set();
23
+ sessionChatsMap.set(sessionId, chats);
24
+ }
25
+ chats.add(chatId);
26
+ }
27
+
28
+ export function unbindChatFromSession(sessionId: string, chatId: string): void {
29
+ const chats = sessionChatsMap.get(sessionId);
30
+ if (chats) {
31
+ chats.delete(chatId);
32
+ if (chats.size === 0) sessionChatsMap.delete(sessionId);
33
+ }
34
+ }
35
+
36
+ export function getChatsForSession(sessionId: string): string[] {
37
+ const chats = sessionChatsMap.get(sessionId);
38
+ return chats ? Array.from(chats) : [];
39
+ }
40
+
41
+ /** 检查 session 是否还有任何群绑定 */
42
+ export function hasChatsForSession(sessionId: string): boolean {
43
+ return (sessionChatsMap.get(sessionId)?.size ?? 0) > 0;
44
+ }
45
+
46
+ /** 检查 sessionId 是否正被其他 chatId 使用(有活跃 prompt) */
47
+ export function isSessionRunning(sessionId: string): boolean {
48
+ return activePrompts.has(sessionId);
49
+ }
50
+
51
+ // ---------------------------------------------------------------------------
52
+ // activePrompts: sessionId → 活跃 prompt 控制
53
+ // ---------------------------------------------------------------------------
54
+
55
+ export interface ActivePrompt {
56
+ controller: AbortController;
57
+ stopped: boolean;
58
+ startTime: number;
59
+ }
60
+
61
+ export const activePrompts = new Map<string, ActivePrompt>();
62
+
63
+ // ---------------------------------------------------------------------------
64
+ // displayCards: chatId → 展示卡片状态(display loop 用)
65
+ // ---------------------------------------------------------------------------
66
+
67
+ export interface DisplayCardState {
68
+ cardId: string;
69
+ sequence: number;
70
+ cardBusy: boolean;
71
+ cardCreatedAt: number;
72
+ lastSentContent: string;
73
+ streamErrorNotified: boolean;
74
+ }
75
+
76
+ export const displayCards = new Map<string, DisplayCardState>();
77
+
78
+ /** displayLoops: sessionId → 展示循环的 stop 函数 */
79
+ export const displayLoops = new Map<string, () => void>();
80
+
81
+ export function resetBindingState(): void {
82
+ sessionChatsMap.clear();
83
+ activePrompts.clear();
84
+ displayCards.clear();
85
+ for (const stop of displayLoops.values()) stop();
86
+ displayLoops.clear();
87
+ }