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/README.md +1 -1
- package/im-skills/feishu-skill/receive-send-file.md +3 -11
- package/im-skills/feishu-skill/receive-send-image.md +3 -11
- package/im-skills/feishu-skill/send-file.mjs +5 -6
- package/im-skills/feishu-skill/send-image.mjs +5 -6
- package/im-skills/feishu-skill/skill.md +2 -3
- package/package.json +1 -1
- package/src/__tests__/agent-image-rpc.test.ts +17 -52
- package/src/__tests__/session.test.ts +31 -1
- package/src/agent-file-rpc.ts +108 -106
- package/src/agent-image-rpc.ts +78 -113
- package/src/cards.ts +2 -2
- package/src/index.ts +165 -116
- package/src/session-chat-binding.ts +87 -0
- package/src/session.ts +328 -263
- package/src/stream-state.ts +94 -0
- package/src/agent-grants-rpc.ts +0 -71
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,
|
|
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",
|
|
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
|
-
//
|
|
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
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
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 === "/
|
|
629
|
-
logTrace(tid, "BRANCH", { cmd: "/
|
|
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
|
-
|
|
640
|
-
|
|
641
|
-
|
|
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: "
|
|
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()}] [
|
|
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()}] [
|
|
696
|
-
logTrace(tid, "DONE", { outcome: "
|
|
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
|
-
|
|
724
|
-
if (
|
|
725
|
-
|
|
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
|
-
|
|
836
|
-
if (
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
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
|
-
|
|
899
|
-
|
|
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: () => {
|
|
1159
|
-
|
|
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
|
|
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
|
|
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
|
+
}
|