chatccc 0.2.47 → 0.2.49

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.
@@ -1,87 +1,103 @@
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();
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
+ // lastActiveChat: sessionId用户最后发送消息的 chatId
65
+ // 用于确保 display loop 只推送到用户最近活跃的群
66
+ // ---------------------------------------------------------------------------
67
+
68
+ const lastActiveChatMap = new Map<string, { chatId: string; timestamp: number }>();
69
+
70
+ export function recordLastActiveChat(sessionId: string, chatId: string): void {
71
+ lastActiveChatMap.set(sessionId, { chatId, timestamp: Date.now() });
72
+ }
73
+
74
+ export function getLastActiveChat(sessionId: string): string | undefined {
75
+ return lastActiveChatMap.get(sessionId)?.chatId;
76
+ }
77
+
78
+ // ---------------------------------------------------------------------------
79
+ // displayCards: chatId 展示卡片状态(display loop 用)
80
+ // ---------------------------------------------------------------------------
81
+
82
+ export interface DisplayCardState {
83
+ cardId: string;
84
+ sequence: number;
85
+ cardBusy: boolean;
86
+ cardCreatedAt: number;
87
+ lastSentContent: string;
88
+ streamErrorNotified: boolean;
89
+ }
90
+
91
+ export const displayCards = new Map<string, DisplayCardState>();
92
+
93
+ /** displayLoops: sessionId → 展示循环的 stop 函数 */
94
+ export const displayLoops = new Map<string, () => void>();
95
+
96
+ export function resetBindingState(): void {
97
+ sessionChatsMap.clear();
98
+ lastActiveChatMap.clear();
99
+ activePrompts.clear();
100
+ displayCards.clear();
101
+ for (const stop of displayLoops.values()) stop();
102
+ displayLoops.clear();
87
103
  }
package/src/session.ts CHANGED
@@ -43,6 +43,8 @@ import {
43
43
  displayCards,
44
44
  displayLoops,
45
45
  rebuildSessionChatsFromRegistry,
46
+ recordLastActiveChat,
47
+ getLastActiveChat,
46
48
  } from "./session-chat-binding.ts";
47
49
 
48
50
  // ---------------------------------------------------------------------------
@@ -245,6 +247,12 @@ export async function recordSessionRegistry(update: SessionRegistryUpdate): Prom
245
247
  await saveSessionRegistry(data);
246
248
  }
247
249
 
250
+ export async function removeSessionRegistryRecord(chatId: string): Promise<void> {
251
+ const data = await loadSessionRegistry();
252
+ delete data[chatId];
253
+ await saveSessionRegistry(data);
254
+ }
255
+
248
256
  export function _setSessionRegistryFileForTest(filePath: string): void {
249
257
  sessionRegistryFile = filePath;
250
258
  }
@@ -423,6 +431,9 @@ export async function runAgentSession(
423
431
  ): Promise<void> {
424
432
  const tid = traceId ?? "";
425
433
 
434
+ // 记录用户最后发送消息的群(display loop 只推送到该群)
435
+ recordLastActiveChat(sessionId, _chatId);
436
+
426
437
  // 并发检查:同一 session 只能有一个活跃 prompt
427
438
  if (activePrompts.has(sessionId)) {
428
439
  if (tid) logTrace(tid, "BLOCKED", { outcome: "session_busy", sessionId });
@@ -513,9 +524,10 @@ export async function runAgentSession(
513
524
  // 启动 display loop
514
525
  ensureDisplayLoop(sessionId);
515
526
 
516
- // 设置所有绑定群头像为 busy
517
- for (const cid of getChatsForSession(sessionId)) {
518
- setChatAvatar(token, cid, tool, "busy").catch(() => {});
527
+ // 设置最后活跃群头像为 busy
528
+ const activeCid = getLastActiveChat(sessionId) ?? getChatsForSession(sessionId)[0];
529
+ if (activeCid) {
530
+ setChatAvatar(token, activeCid, tool, "busy").catch(() => {});
519
531
  }
520
532
 
521
533
  const state: AccumulatorState = {
@@ -605,6 +617,8 @@ export async function runAgentSession(
605
617
  running: false,
606
618
  });
607
619
  }
620
+ const active1 = getLastActiveChat(sessionId) ?? getChatsForSession(sessionId)[0];
621
+ if (active1) setChatAvatar(token, active1, tool, "idle").catch(() => {});
608
622
  console.log(`[${ts()}] Session ${sessionId} stopped (content chunks: ${state.chunkCount})`);
609
623
  if (tid) logTrace(tid, "SESSION_END", { sessionId, outcome: "stopped", chunks: state.chunkCount });
610
624
  } else {
@@ -619,8 +633,9 @@ export async function runAgentSession(
619
633
  startTime: finfo?.startTime ?? now,
620
634
  running: false,
621
635
  });
622
- setChatAvatar(token, cid, tool, "idle").catch(() => {});
623
636
  }
637
+ const active2 = getLastActiveChat(sessionId) ?? getChatsForSession(sessionId)[0];
638
+ if (active2) setChatAvatar(token, active2, tool, "idle").catch(() => {});
624
639
  console.log(`[${ts()}] Session ${sessionId} stream complete (content chunks: ${state.chunkCount})`);
625
640
  if (tid) logTrace(tid, "SESSION_END", { sessionId, chunks: state.chunkCount, finalTextLen: finalReply.length });
626
641
  }
@@ -628,7 +643,7 @@ export async function runAgentSession(
628
643
  }
629
644
 
630
645
  // ---------------------------------------------------------------------------
631
- // ensureDisplayLoop — 每个 session 一个 display 循环,读文件更新所有绑定群的卡片
646
+ // ensureDisplayLoop — 每个 session 一个 display 循环,读文件更新最后活跃群的卡片
632
647
  // ---------------------------------------------------------------------------
633
648
 
634
649
  const CARD_ROTATE_MS = 9 * 60 * 1000;
@@ -643,112 +658,116 @@ export function ensureDisplayLoop(sessionId: string): void {
643
658
  const state = await readStreamState(sessionId);
644
659
  if (!state) return;
645
660
 
646
- const chats = getChatsForSession(sessionId);
647
- if (chats.length === 0) {
648
- // 无绑定群,若 session 已结束则停止 loop
661
+ const chatId = getLastActiveChat(sessionId);
662
+ if (!chatId) {
663
+ // 无活跃群,若 session 已结束则停止 loop
649
664
  if (state.status !== "running") {
650
665
  clearInterval(interval);
651
666
  displayLoops.delete(sessionId);
667
+ // 兜底:lastActiveChatMap 可能因进程重启丢失,从 registry 映射恢复头像
668
+ const fallbackChat = getChatsForSession(sessionId)[0];
669
+ if (fallbackChat) {
670
+ const t = await import("./feishu-platform.ts").then(m => m.getTenantAccessToken());
671
+ setChatAvatar(t, fallbackChat, state.tool, "idle").catch(() => {});
672
+ }
652
673
  }
653
674
  return;
654
675
  }
655
676
 
656
677
  const isTerminal = state.status !== "running";
657
678
 
658
- for (const chatId of chats) {
659
- try {
660
- // getTenantAccessToken 有缓存,多次调用开销低
661
- const tokenModule = await import("./feishu-platform.ts");
662
- const token = await tokenModule.getTenantAccessToken();
663
- const display = displayCards.get(chatId);
664
-
665
- if (isTerminal) {
666
- // 发送最终结果
667
- if (display) {
668
- while (display.cardBusy) await new Promise(r => setTimeout(r, 20));
669
- const nextSeq = display.sequence + 1;
670
- const headerTitle = state.status === "stopped" ? "已停止" : "完成";
671
- const headerTemplate = state.status === "stopped" ? "red" : undefined;
672
- const cardContent = state.accumulatedContent || " ";
673
- const doneCard = buildProgressCard(cardContent, { showStop: false, headerTitle, headerTemplate });
674
- await updateCardKitCard(token, display.cardId, doneCard, nextSeq).catch(() => {});
675
- displayCards.delete(chatId);
676
- }
677
- if (state.finalReply) {
678
- await sendTextReply(token, chatId, state.finalReply).catch(() => {});
679
- } else if (!display && state.accumulatedContent.trim()) {
680
- const short = truncateContent(state.accumulatedContent, 30, 4000);
681
- await sendTextReply(token, chatId, `[生成过程]\n${short}`).catch(() => {});
679
+ try {
680
+ // getTenantAccessToken 有缓存,多次调用开销低
681
+ const tokenModule = await import("./feishu-platform.ts");
682
+ const token = await tokenModule.getTenantAccessToken();
683
+ const display = displayCards.get(chatId);
684
+
685
+ if (isTerminal) {
686
+ // 发送最终结果
687
+ if (display) {
688
+ while (display.cardBusy) await new Promise(r => setTimeout(r, 20));
689
+ const nextSeq = display.sequence + 1;
690
+ const headerTitle = state.status === "stopped" ? "已停止" : "完成";
691
+ const headerTemplate = state.status === "stopped" ? "red" : undefined;
692
+ const cardContent = state.accumulatedContent || " ";
693
+ const doneCard = buildProgressCard(cardContent, { showStop: false, headerTitle, headerTemplate });
694
+ await updateCardKitCard(token, display.cardId, doneCard, nextSeq).catch(() => {});
695
+ displayCards.delete(chatId);
696
+ }
697
+ if (state.finalReply) {
698
+ await sendTextReply(token, chatId, state.finalReply).catch(() => {});
699
+ } else if (!display && state.accumulatedContent.trim()) {
700
+ const short = truncateContent(state.accumulatedContent, 30, 4000);
701
+ await sendTextReply(token, chatId, `[生成过程]\n${short}`).catch(() => {});
702
+ }
703
+ setChatAvatar(token, chatId, state.tool, "idle").catch(() => {});
704
+ } else {
705
+ // running: 创建或更新卡片
706
+ if (!display) {
707
+ const cardId = await createCardKitCard(token, buildProgressCard("", { showStop: true, headerTitle: "生成中..." })).catch(() => null);
708
+ if (cardId) {
709
+ await sendCardKitMessage(token, chatId, cardId).catch(() => null);
710
+ displayCards.set(chatId, {
711
+ cardId,
712
+ sequence: 1,
713
+ cardBusy: false,
714
+ cardCreatedAt: Date.now(),
715
+ lastSentContent: "",
716
+ streamErrorNotified: false,
717
+ });
682
718
  }
683
- setChatAvatar(token, chatId, state.tool, "idle").catch(() => {});
684
719
  } else {
685
- // running: 创建或更新卡片
686
- if (!display) {
687
- const cardId = await createCardKitCard(token, buildProgressCard("", { showStop: true, headerTitle: "生成中..." })).catch(() => null);
688
- if (cardId) {
689
- await sendCardKitMessage(token, chatId, cardId).catch(() => null);
690
- displayCards.set(chatId, {
691
- cardId,
692
- sequence: 1,
693
- cardBusy: false,
694
- cardCreatedAt: Date.now(),
695
- lastSentContent: "",
696
- streamErrorNotified: false,
697
- });
698
- }
699
- } else {
700
- if (display.cardBusy) continue;
701
-
702
- // 卡片轮转
703
- if (Date.now() - display.cardCreatedAt > CARD_ROTATE_MS) {
704
- display.cardBusy = true;
705
- try {
706
- const oldSeqBase = display.sequence;
707
- const oldDisplayContent = truncateContent(state.accumulatedContent + state.finalReply) || "处理中...";
708
- const oldCard = buildProgressCard(oldDisplayContent, { showStop: false, headerTitle: "生成中...(上轮)" });
709
- await updateCardKitCard(token, display.cardId, oldCard, oldSeqBase + 1).catch(() => {});
710
- const newCardId = await createCardKitCard(token, buildProgressCard("", { showStop: true, headerTitle: "生成中..." }));
711
- if (newCardId) {
712
- await sendCardKitMessage(token, chatId, newCardId);
713
- display.cardId = newCardId;
714
- display.sequence = 1;
715
- display.cardCreatedAt = Date.now();
716
- display.lastSentContent = "";
717
- display.streamErrorNotified = false;
718
- }
719
- } catch (err) {
720
- console.error(`[${ts()}] [CARDIKT] rotation FAIL for ${chatId}: ${(err as Error).message}`);
721
- } finally {
722
- display.cardBusy = false;
723
- }
724
- continue;
725
- }
720
+ if (display.cardBusy) return;
726
721
 
727
- dotCount = (dotCount % 9) + 1;
728
- const content = truncateContent(state.accumulatedContent + state.finalReply + "\n" + "。".repeat(dotCount));
729
- if (content === display.lastSentContent) continue;
730
-
731
- display.lastSentContent = content;
722
+ // 卡片轮转
723
+ if (Date.now() - display.cardCreatedAt > CARD_ROTATE_MS) {
732
724
  display.cardBusy = true;
733
- const mySeq = display.sequence + 1;
734
725
  try {
735
- const card = buildProgressCard(content, { showStop: true, headerTitle: "生成中..." });
736
- await updateCardKitCard(token, display.cardId, card, mySeq);
737
- display.sequence = mySeq;
738
- } catch (err) {
739
- console.error(`[${ts()}] CardKit update error: chatId=${chatId} ${(err as Error).message}`);
740
- if (!display.streamErrorNotified) {
741
- display.streamErrorNotified = true;
742
- sendTextReply(token, chatId, "卡片更新失败,结果将以文本形式发送。").catch(() => {});
726
+ const oldSeqBase = display.sequence;
727
+ const oldDisplayContent = truncateContent(state.accumulatedContent + state.finalReply) || "处理中...";
728
+ const oldCard = buildProgressCard(oldDisplayContent, { showStop: false, headerTitle: "生成中...(上轮)" });
729
+ await updateCardKitCard(token, display.cardId, oldCard, oldSeqBase + 1).catch(() => {});
730
+ const newCardId = await createCardKitCard(token, buildProgressCard("", { showStop: true, headerTitle: "生成中..." }));
731
+ if (newCardId) {
732
+ await sendCardKitMessage(token, chatId, newCardId);
733
+ display.cardId = newCardId;
734
+ display.sequence = 1;
735
+ display.cardCreatedAt = Date.now();
736
+ display.lastSentContent = "";
737
+ display.streamErrorNotified = false;
743
738
  }
739
+ } catch (err) {
740
+ console.error(`[${ts()}] [CARDIKT] rotation FAIL for ${chatId}: ${(err as Error).message}`);
744
741
  } finally {
745
742
  display.cardBusy = false;
746
743
  }
744
+ return;
745
+ }
746
+
747
+ dotCount = (dotCount % 9) + 1;
748
+ const content = truncateContent(state.accumulatedContent + state.finalReply + "\n" + "。".repeat(dotCount));
749
+ if (content === display.lastSentContent) return;
750
+
751
+ display.lastSentContent = content;
752
+ display.cardBusy = true;
753
+ const mySeq = display.sequence + 1;
754
+ try {
755
+ const card = buildProgressCard(content, { showStop: true, headerTitle: "生成中..." });
756
+ await updateCardKitCard(token, display.cardId, card, mySeq);
757
+ display.sequence = mySeq;
758
+ } catch (err) {
759
+ console.error(`[${ts()}] CardKit update error: chatId=${chatId} ${(err as Error).message}`);
760
+ if (!display.streamErrorNotified) {
761
+ display.streamErrorNotified = true;
762
+ sendTextReply(token, chatId, "卡片更新失败,结果将以文本形式发送。").catch(() => {});
763
+ }
764
+ } finally {
765
+ display.cardBusy = false;
747
766
  }
748
767
  }
749
- } catch (err) {
750
- console.error(`[${ts()}] Display loop error for ${chatId}: ${(err as Error).message}`);
751
768
  }
769
+ } catch (err) {
770
+ console.error(`[${ts()}] Display loop error for ${chatId}: ${(err as Error).message}`);
752
771
  }
753
772
 
754
773
  if (isTerminal) {