chatccc 0.2.51 → 0.2.53
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 +103 -217
- package/bin/chatccc.mjs +23 -23
- package/config.sample.json +34 -30
- package/demo/ilink_echo_probe.ts +222 -222
- package/im-skills/feishu-skill/download-video.mjs +162 -162
- package/im-skills/feishu-skill/receive-send-file.md +66 -66
- package/im-skills/feishu-skill/receive-send-image.md +27 -27
- package/im-skills/feishu-skill/send-file.mjs +50 -50
- package/im-skills/feishu-skill/send-image.mjs +50 -50
- package/im-skills/feishu-skill/skill.md +10 -10
- package/package.json +59 -59
- package/src/__tests__/agent-image-rpc.test.ts +33 -33
- package/src/__tests__/agent-rpc-body.test.ts +41 -41
- package/src/__tests__/card-plain-text.test.ts +42 -0
- package/src/__tests__/claude-adapter.test.ts +54 -1
- package/src/__tests__/codex-adapter.test.ts +304 -304
- package/src/__tests__/config-reload.test.ts +58 -41
- package/src/__tests__/config-sample.test.ts +19 -0
- package/src/__tests__/crash-logging.test.ts +62 -62
- package/src/__tests__/fixtures/codex_simple_text.jsonl +4 -4
- package/src/__tests__/fixtures/codex_with_tool.jsonl +6 -6
- package/src/__tests__/im-skills.test.ts +46 -46
- package/src/__tests__/session.test.ts +95 -2
- package/src/__tests__/sim-agent.test.ts +173 -173
- package/src/__tests__/sim-platform.test.ts +76 -76
- package/src/__tests__/sim-store.test.ts +213 -213
- package/src/__tests__/stream-state.test.ts +134 -134
- package/src/__tests__/wechat-platform.test.ts +57 -0
- package/src/adapters/claude-adapter.ts +23 -2
- package/src/adapters/codex-adapter.ts +294 -294
- package/src/adapters/codex-session-meta-store.ts +130 -130
- package/src/agent-file-rpc.ts +156 -156
- package/src/agent-image-rpc.ts +152 -152
- package/src/agent-rpc-body.ts +48 -48
- package/src/card-plain-text.ts +108 -0
- package/src/cards.ts +4 -4
- package/src/config.ts +775 -742
- package/src/feishu-api.ts +6 -0
- package/src/im-skills.ts +109 -109
- package/src/index.ts +155 -788
- package/src/orchestrator.ts +1032 -0
- package/src/platform-adapter.ts +61 -0
- package/src/session-chat-binding.ts +7 -0
- package/src/session.ts +278 -113
- package/src/sim-agent.ts +167 -156
- package/src/trace.ts +50 -50
- package/src/web-ui.ts +1891 -1718
- package/src/wechat-platform.ts +517 -0
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* platform-adapter.ts — IM 平台适配器接口
|
|
3
|
+
*
|
|
4
|
+
* 独立于 orchestrator.ts,避免 orchestrator ↔ session 循环依赖。
|
|
5
|
+
* 每个方法内部自行管理认证,消费者不感知 token 等认证细节。
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
export interface PlatformAdapter {
|
|
9
|
+
/** 平台标识,用于区分不同平台的行为(如 wechat、feishu 等) */
|
|
10
|
+
kind?: string;
|
|
11
|
+
|
|
12
|
+
/** 发送纯文本回复 */
|
|
13
|
+
sendText(chatId: string, text: string): Promise<boolean>;
|
|
14
|
+
|
|
15
|
+
/** 发送卡片回复(标题 + 内容 + 颜色模板) */
|
|
16
|
+
sendCard(
|
|
17
|
+
chatId: string,
|
|
18
|
+
title: string,
|
|
19
|
+
content: string,
|
|
20
|
+
template: string,
|
|
21
|
+
): Promise<boolean>;
|
|
22
|
+
|
|
23
|
+
/** 发送原始卡片 JSON */
|
|
24
|
+
sendRawCard(chatId: string, cardJson: string): Promise<boolean>;
|
|
25
|
+
|
|
26
|
+
/** 创建群聊,返回新群 ID */
|
|
27
|
+
createGroup(name: string, userIds: string[]): Promise<string>;
|
|
28
|
+
|
|
29
|
+
/** 更新群名称和描述 */
|
|
30
|
+
updateChatInfo(
|
|
31
|
+
chatId: string,
|
|
32
|
+
name: string,
|
|
33
|
+
description: string,
|
|
34
|
+
): Promise<void>;
|
|
35
|
+
|
|
36
|
+
/** 获取群信息 */
|
|
37
|
+
getChatInfo(chatId: string): Promise<{ name: string; description: string }>;
|
|
38
|
+
|
|
39
|
+
/** 解散群聊 */
|
|
40
|
+
disbandChat(chatId: string): Promise<void>;
|
|
41
|
+
|
|
42
|
+
/** 设置群头像 */
|
|
43
|
+
setChatAvatar(chatId: string, tool: string, status: string): Promise<void>;
|
|
44
|
+
|
|
45
|
+
/** 从群描述中提取 session 信息 */
|
|
46
|
+
extractSessionInfo(
|
|
47
|
+
description: string,
|
|
48
|
+
): { sessionId: string; tool: string } | null;
|
|
49
|
+
|
|
50
|
+
// ---- 进度展示(display loop 使用) ----
|
|
51
|
+
// 不同平台能力不同:飞书用 CardKit 实时卡片,微信降级为纯文本。
|
|
52
|
+
|
|
53
|
+
/** 创建进度展示实体,返回唯一标识;不支持进度展示的平台返回 null */
|
|
54
|
+
cardCreate(cardJson: string): Promise<string | null>;
|
|
55
|
+
|
|
56
|
+
/** 将进度展示发送到指定会话,返回 message_id */
|
|
57
|
+
cardSend(chatId: string, cardId: string): Promise<string>;
|
|
58
|
+
|
|
59
|
+
/** 更新已发送的进度展示,sequence 保证有序 */
|
|
60
|
+
cardUpdate(cardId: string, cardJson: string, sequence: number): Promise<void>;
|
|
61
|
+
}
|
|
@@ -109,6 +109,13 @@ export interface DisplayCardState {
|
|
|
109
109
|
cardCreatedAt: number;
|
|
110
110
|
lastSentContent: string;
|
|
111
111
|
streamErrorNotified: boolean;
|
|
112
|
+
/** 卡片轮转时记录的基线,轮转后只展示增量 */
|
|
113
|
+
rotationAccLen?: number;
|
|
114
|
+
rotationFinalReply?: string;
|
|
115
|
+
/** WeChat delta: 上次发送时 accumulatedContent 的长度 */
|
|
116
|
+
lastSentAccLen?: number;
|
|
117
|
+
/** WeChat delta: 上次发送时的 finalReply */
|
|
118
|
+
lastSentFinalReply?: string;
|
|
112
119
|
}
|
|
113
120
|
|
|
114
121
|
export const displayCards = new Map<string, DisplayCardState>();
|
package/src/session.ts
CHANGED
|
@@ -20,12 +20,6 @@ import {
|
|
|
20
20
|
ts,
|
|
21
21
|
} from "./config.ts";
|
|
22
22
|
import { buildProgressCard, getToolEmoji, truncateContent } from "./cards.ts";
|
|
23
|
-
import {
|
|
24
|
-
createCardKitCard,
|
|
25
|
-
sendCardKitMessage,
|
|
26
|
-
updateCardKitCard,
|
|
27
|
-
} from "./cardkit.ts";
|
|
28
|
-
import { sendTextReply, setChatAvatar } from "./feishu-platform.ts";
|
|
29
23
|
import { logTrace } from "./trace.ts";
|
|
30
24
|
import type { UnifiedBlock } from "./adapters/adapter-interface.ts";
|
|
31
25
|
import type { ToolAdapter } from "./adapters/adapter-interface.ts";
|
|
@@ -33,6 +27,7 @@ import { createClaudeAdapter } from "./adapters/claude-adapter.ts";
|
|
|
33
27
|
import { createCursorAdapter } from "./adapters/cursor-adapter.ts";
|
|
34
28
|
import { createCodexAdapter } from "./adapters/codex-adapter.ts";
|
|
35
29
|
import { buildImSkillsPrompt, exportSkillSubDocs } from "./im-skills.ts";
|
|
30
|
+
import type { PlatformAdapter } from "./platform-adapter.ts";
|
|
36
31
|
import { readStreamState, writeStreamState, createEmptyStreamState, fixStaleStreamStates } from "./stream-state.ts";
|
|
37
32
|
import {
|
|
38
33
|
bindChatToSession,
|
|
@@ -58,6 +53,36 @@ export const MAX_PROCESSED = 5000;
|
|
|
58
53
|
/** 每个 chatId 上一次已处理消息的时间戳,用于拦截延迟送达的旧消息 */
|
|
59
54
|
export const lastMsgTimestamps = new Map<string, number>();
|
|
60
55
|
|
|
56
|
+
// ---------------------------------------------------------------------------
|
|
57
|
+
// 平台引用 —— session 模块通过此引用访问 IM 平台操作,
|
|
58
|
+
// 避免 import feishu-platform.ts 造成的耦合。
|
|
59
|
+
// 由 index.ts 在启动时调用 setSessionPlatform 注入。
|
|
60
|
+
// ---------------------------------------------------------------------------
|
|
61
|
+
|
|
62
|
+
let platformRef: PlatformAdapter | null = null;
|
|
63
|
+
const chatPlatformMap = new Map<string, PlatformAdapter>();
|
|
64
|
+
|
|
65
|
+
/** 注入当前 IM 平台适配器,供 session 模块使用 */
|
|
66
|
+
export function setSessionPlatform(platform: PlatformAdapter): void {
|
|
67
|
+
platformRef = platform;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
export function recordChatPlatform(chatId: string, platform: PlatformAdapter): void {
|
|
71
|
+
chatPlatformMap.set(chatId, platform);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export function forgetChatPlatform(chatId: string): void {
|
|
75
|
+
chatPlatformMap.delete(chatId);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function platformForChat(chatId: string): PlatformAdapter | null {
|
|
79
|
+
return chatPlatformMap.get(chatId) ?? platformRef;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
export function _getPlatformForChatForTest(chatId: string): PlatformAdapter | null {
|
|
83
|
+
return platformForChat(chatId);
|
|
84
|
+
}
|
|
85
|
+
|
|
61
86
|
export let sessionGen = 0;
|
|
62
87
|
/** @deprecated 使用 activePrompts (session-chat-binding.ts) + displayCards 替代 */
|
|
63
88
|
export const chatSessionMap = new Map<string, {
|
|
@@ -117,6 +142,7 @@ export function resetState(): void {
|
|
|
117
142
|
sessionInfoMap.clear();
|
|
118
143
|
processedMessages.clear();
|
|
119
144
|
lastMsgTimestamps.clear();
|
|
145
|
+
chatPlatformMap.clear();
|
|
120
146
|
activePrompts.clear();
|
|
121
147
|
displayCards.clear();
|
|
122
148
|
for (const stop of displayLoops.values()) stop();
|
|
@@ -582,13 +608,13 @@ export async function initClaudeSession(tool: string, overrideCwd?: string, chat
|
|
|
582
608
|
export async function resumeAndPrompt(
|
|
583
609
|
sessionId: string,
|
|
584
610
|
userText: string,
|
|
585
|
-
|
|
611
|
+
platform: PlatformAdapter,
|
|
586
612
|
chatId: string,
|
|
587
613
|
msgTimestamp: number,
|
|
588
614
|
tool: string,
|
|
589
615
|
traceId?: string,
|
|
590
616
|
): Promise<void> {
|
|
591
|
-
return runAgentSession(sessionId, userText,
|
|
617
|
+
return runAgentSession(sessionId, userText, platform, chatId, msgTimestamp, tool, traceId);
|
|
592
618
|
}
|
|
593
619
|
|
|
594
620
|
// ---------------------------------------------------------------------------
|
|
@@ -598,7 +624,7 @@ export async function resumeAndPrompt(
|
|
|
598
624
|
export async function runAgentSession(
|
|
599
625
|
sessionId: string,
|
|
600
626
|
userText: string,
|
|
601
|
-
|
|
627
|
+
platform: PlatformAdapter,
|
|
602
628
|
_chatId: string,
|
|
603
629
|
msgTimestamp: number,
|
|
604
630
|
tool: string,
|
|
@@ -607,47 +633,23 @@ export async function runAgentSession(
|
|
|
607
633
|
const tid = traceId ?? "";
|
|
608
634
|
|
|
609
635
|
// 记录用户最后发送消息的群(display loop 只推送到该群)
|
|
636
|
+
recordChatPlatform(_chatId, platform);
|
|
610
637
|
recordLastActiveChat(sessionId, _chatId);
|
|
611
638
|
|
|
612
639
|
// 并发检查:同一 session 只能有一个活跃 prompt
|
|
613
640
|
if (activePrompts.has(sessionId)) {
|
|
614
641
|
if (tid) logTrace(tid, "BLOCKED", { outcome: "session_busy", sessionId });
|
|
615
642
|
console.log(`[${ts()}] [BLOCKED] Session ${sessionId} is already generating`);
|
|
616
|
-
|
|
643
|
+
const isWechatBusy = platform.kind === "wechat";
|
|
644
|
+
const busyMsg = isWechatBusy
|
|
645
|
+
? "当前正在生成回复中,请等待完成后再发送消息。如需中断生成,请发送 /stop 指令。"
|
|
646
|
+
: "该会话正在生成回复中,请等待完成后再发送消息。";
|
|
647
|
+
await platform.sendText(_chatId, busyMsg).catch(() => {});
|
|
617
648
|
return;
|
|
618
649
|
}
|
|
619
650
|
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
const cwd = info?.cwd ?? (await getDefaultCwd(_chatId));
|
|
623
|
-
if (tid) logTrace(tid, "SESSION_START", { sessionId, tool, cwd, turn: (sessionInfoMap.get(_chatId)?.turnCount ?? 0) + 1 });
|
|
624
|
-
console.log(
|
|
625
|
-
`[${ts()}] Running ${adapter.displayName} session: ${sessionId} (${formatToolConfigForLog(tool, info?.model)}, cwd=${cwd})`
|
|
626
|
-
);
|
|
627
|
-
|
|
628
|
-
// 构建 IM skills prompt(sessionId 方式,无 token)
|
|
629
|
-
const feishuSkillDir = join(PROJECT_ROOT, "im-skills", "feishu-skill");
|
|
630
|
-
const imSkillsCacheDir = join(USER_DATA_DIR, "im-skills");
|
|
631
|
-
const skillVariables = {
|
|
632
|
-
cwd,
|
|
633
|
-
session_id: sessionId,
|
|
634
|
-
im_skills_cache_dir: imSkillsCacheDir,
|
|
635
|
-
send_image_url: `http://127.0.0.1:${CHATCCC_PORT}/api/agent/send-image`,
|
|
636
|
-
send_file_url: `http://127.0.0.1:${CHATCCC_PORT}/api/agent/send-file`,
|
|
637
|
-
send_image_script: join(feishuSkillDir, "send-image.mjs"),
|
|
638
|
-
send_file_script: join(feishuSkillDir, "send-file.mjs"),
|
|
639
|
-
download_video_script: join(feishuSkillDir, "download-video.mjs"),
|
|
640
|
-
};
|
|
641
|
-
const imSkillsPrompt = await buildImSkillsPrompt({ variables: skillVariables });
|
|
642
|
-
await exportSkillSubDocs({ variables: skillVariables }, imSkillsCacheDir);
|
|
643
|
-
const userTextWithCapabilities = [
|
|
644
|
-
...(imSkillsPrompt ? [imSkillsPrompt, ""] : []),
|
|
645
|
-
"[User message]",
|
|
646
|
-
userText,
|
|
647
|
-
"[/User message]",
|
|
648
|
-
].join("\n");
|
|
649
|
-
|
|
650
|
-
// 设置活跃 prompt
|
|
651
|
+
// 立即标记活跃,确保 /sessions、isSessionRunning 等查询在异步准备阶段就能看到运行状态。
|
|
652
|
+
// 注意:下面的 try/catch 在准备失败时会清理 activePrompts。
|
|
651
653
|
const controller = new AbortController();
|
|
652
654
|
const now = Date.now();
|
|
653
655
|
activePrompts.set(sessionId, {
|
|
@@ -656,6 +658,46 @@ export async function runAgentSession(
|
|
|
656
658
|
startTime: now,
|
|
657
659
|
});
|
|
658
660
|
|
|
661
|
+
// 异步准备工作(session info、IM skills prompt 等)
|
|
662
|
+
let adapter: ToolAdapter;
|
|
663
|
+
let info: Awaited<ReturnType<ToolAdapter["getSessionInfo"]>>;
|
|
664
|
+
let cwd: string;
|
|
665
|
+
try {
|
|
666
|
+
adapter = getAdapterForTool(tool);
|
|
667
|
+
info = await adapter.getSessionInfo(sessionId);
|
|
668
|
+
cwd = info?.cwd ?? (await getDefaultCwd(_chatId));
|
|
669
|
+
if (tid) logTrace(tid, "SESSION_START", { sessionId, tool, cwd, turn: (sessionInfoMap.get(_chatId)?.turnCount ?? 0) + 1 });
|
|
670
|
+
console.log(
|
|
671
|
+
`[${ts()}] Running ${adapter.displayName} session: ${sessionId} (${formatToolConfigForLog(tool, info?.model)}, cwd=${cwd})`
|
|
672
|
+
);
|
|
673
|
+
|
|
674
|
+
// 构建 IM skills prompt(sessionId 方式,无 token)
|
|
675
|
+
const feishuSkillDir = join(PROJECT_ROOT, "im-skills", "feishu-skill");
|
|
676
|
+
const imSkillsCacheDir = join(USER_DATA_DIR, "im-skills");
|
|
677
|
+
const skillVariables = {
|
|
678
|
+
cwd,
|
|
679
|
+
session_id: sessionId,
|
|
680
|
+
im_skills_cache_dir: imSkillsCacheDir,
|
|
681
|
+
send_image_url: `http://127.0.0.1:${CHATCCC_PORT}/api/agent/send-image`,
|
|
682
|
+
send_file_url: `http://127.0.0.1:${CHATCCC_PORT}/api/agent/send-file`,
|
|
683
|
+
send_image_script: join(feishuSkillDir, "send-image.mjs"),
|
|
684
|
+
send_file_script: join(feishuSkillDir, "send-file.mjs"),
|
|
685
|
+
download_video_script: join(feishuSkillDir, "download-video.mjs"),
|
|
686
|
+
};
|
|
687
|
+
var imSkillsPrompt = await buildImSkillsPrompt({ variables: skillVariables });
|
|
688
|
+
await exportSkillSubDocs({ variables: skillVariables }, imSkillsCacheDir);
|
|
689
|
+
var userTextWithCapabilities = [
|
|
690
|
+
...(imSkillsPrompt ? [imSkillsPrompt, ""] : []),
|
|
691
|
+
"[User message]",
|
|
692
|
+
userText,
|
|
693
|
+
"[/User message]",
|
|
694
|
+
].join("\n");
|
|
695
|
+
} catch (preambleErr) {
|
|
696
|
+
// 准备工作失败,清理活跃标记,避免"僵尸"活跃状态阻塞后续消息
|
|
697
|
+
activePrompts.delete(sessionId);
|
|
698
|
+
throw preambleErr;
|
|
699
|
+
}
|
|
700
|
+
|
|
659
701
|
// 更新 sessionInfoMap(所有绑定群共用)
|
|
660
702
|
const existingInfo = sessionInfoMap.get(_chatId);
|
|
661
703
|
const nextTurnCount = (existingInfo?.turnCount ?? 0) + 1;
|
|
@@ -702,7 +744,7 @@ export async function runAgentSession(
|
|
|
702
744
|
// 设置最后活跃群头像为 busy
|
|
703
745
|
const activeCid = getLastActiveChat(sessionId) ?? getChatsForSession(sessionId)[0];
|
|
704
746
|
if (activeCid) {
|
|
705
|
-
setChatAvatar(
|
|
747
|
+
platform.setChatAvatar(activeCid, tool, "busy").catch(() => {});
|
|
706
748
|
}
|
|
707
749
|
|
|
708
750
|
const state: AccumulatorState = {
|
|
@@ -793,7 +835,7 @@ export async function runAgentSession(
|
|
|
793
835
|
});
|
|
794
836
|
}
|
|
795
837
|
const active1 = getLastActiveChat(sessionId) ?? getChatsForSession(sessionId)[0];
|
|
796
|
-
if (active1) setChatAvatar(
|
|
838
|
+
if (active1) platform.setChatAvatar(active1, tool, "idle").catch(() => {});
|
|
797
839
|
console.log(`[${ts()}] Session ${sessionId} stopped (content chunks: ${state.chunkCount})`);
|
|
798
840
|
if (tid) logTrace(tid, "SESSION_END", { sessionId, outcome: "stopped", chunks: state.chunkCount });
|
|
799
841
|
} else {
|
|
@@ -810,7 +852,7 @@ export async function runAgentSession(
|
|
|
810
852
|
});
|
|
811
853
|
}
|
|
812
854
|
const active2 = getLastActiveChat(sessionId) ?? getChatsForSession(sessionId)[0];
|
|
813
|
-
if (active2) setChatAvatar(
|
|
855
|
+
if (active2) platform.setChatAvatar(active2, tool, "idle").catch(() => {});
|
|
814
856
|
console.log(`[${ts()}] Session ${sessionId} stream complete (content chunks: ${state.chunkCount})`);
|
|
815
857
|
if (tid) logTrace(tid, "SESSION_END", { sessionId, chunks: state.chunkCount, finalTextLen: finalReply.length });
|
|
816
858
|
}
|
|
@@ -843,103 +885,226 @@ export function ensureDisplayLoop(sessionId: string): void {
|
|
|
843
885
|
displayLoops.delete(sessionId);
|
|
844
886
|
// 兜底:lastActiveChatMap 可能因进程重启丢失,从 registry 映射恢复头像
|
|
845
887
|
const fallbackChat = getChatsForSession(sessionId)[0];
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
setChatAvatar(
|
|
888
|
+
const fallbackPlatform = fallbackChat ? platformForChat(fallbackChat) : null;
|
|
889
|
+
if (fallbackChat && fallbackPlatform) {
|
|
890
|
+
fallbackPlatform.setChatAvatar(fallbackChat, state.tool, "idle").catch(() => {});
|
|
849
891
|
}
|
|
850
892
|
}
|
|
851
893
|
return;
|
|
852
894
|
}
|
|
853
895
|
|
|
896
|
+
// 交叉验证:chat 当前绑定的 session 是否仍是本 display loop 的 session。
|
|
897
|
+
// 若 chat 已被切换到其他 session(如 /new p2p 未解绑旧 session 的历史遗留
|
|
898
|
+
// 或任何未来新增的切换路径),旧 loop 必须停推,避免向已离开的 chat 推送内容。
|
|
899
|
+
const currentSessionForChat = sessionInfoMap.get(chatId)?.sessionId;
|
|
900
|
+
if (currentSessionForChat && currentSessionForChat !== sessionId) {
|
|
901
|
+
if (state.status !== "running") {
|
|
902
|
+
clearInterval(interval);
|
|
903
|
+
displayLoops.delete(sessionId);
|
|
904
|
+
}
|
|
905
|
+
return;
|
|
906
|
+
}
|
|
907
|
+
|
|
854
908
|
const isTerminal = state.status !== "running";
|
|
855
909
|
|
|
856
910
|
try {
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
const token = await tokenModule.getTenantAccessToken();
|
|
911
|
+
const p = platformForChat(chatId);
|
|
912
|
+
if (!p) return;
|
|
860
913
|
const display = displayCards.get(chatId);
|
|
861
914
|
|
|
915
|
+
const isWechat = p.kind === "wechat";
|
|
916
|
+
|
|
862
917
|
if (isTerminal) {
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
const
|
|
867
|
-
const
|
|
868
|
-
const
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
const
|
|
878
|
-
await
|
|
918
|
+
if (isWechat) {
|
|
919
|
+
// WeChat: 没有卡片需要终结,用 delta 逻辑发剩余内容,避免重发已推送的部分
|
|
920
|
+
// 分开追踪 accumulatedContent 和 finalReply,而非拼接后对比
|
|
921
|
+
const prevAccLen = display?.lastSentAccLen ?? 0;
|
|
922
|
+
const prevFinalReply = display?.lastSentFinalReply ?? "";
|
|
923
|
+
const accDelta = state.accumulatedContent.slice(prevAccLen);
|
|
924
|
+
let replyDelta: string;
|
|
925
|
+
if (prevFinalReply && state.finalReply.startsWith(prevFinalReply)) {
|
|
926
|
+
replyDelta = state.finalReply.slice(prevFinalReply.length);
|
|
927
|
+
} else {
|
|
928
|
+
replyDelta = state.finalReply;
|
|
929
|
+
}
|
|
930
|
+
const remaining = (accDelta + replyDelta).trim();
|
|
931
|
+
const tail = "━━━ 回答结束 ━━━";
|
|
932
|
+
const finalMsg = remaining ? remaining + "\n" + tail : tail;
|
|
933
|
+
await p.sendText(chatId, finalMsg).catch(() => {});
|
|
934
|
+
if (display) displayCards.delete(chatId);
|
|
935
|
+
} else {
|
|
936
|
+
// 发送最终结果(卡片平台)
|
|
937
|
+
if (display) {
|
|
938
|
+
while (display.cardBusy) await new Promise(r => setTimeout(r, 20));
|
|
939
|
+
const nextSeq = display.sequence + 1;
|
|
940
|
+
const headerTitle = state.status === "stopped" ? "已停止" : "完成";
|
|
941
|
+
const headerTemplate = state.status === "stopped" ? "red" : undefined;
|
|
942
|
+
const cardContent = state.accumulatedContent || " ";
|
|
943
|
+
const doneCard = buildProgressCard(cardContent, { showStop: false, headerTitle, headerTemplate });
|
|
944
|
+
await p.cardUpdate(display.cardId, doneCard, nextSeq).catch(() => {});
|
|
945
|
+
displayCards.delete(chatId);
|
|
946
|
+
}
|
|
947
|
+
if (state.finalReply) {
|
|
948
|
+
await p.sendText(chatId, state.finalReply).catch(() => {});
|
|
949
|
+
} else if (!display && state.accumulatedContent.trim()) {
|
|
950
|
+
const short = truncateContent(state.accumulatedContent, 30, 4000);
|
|
951
|
+
await p.sendText(chatId, `[生成过程]\n${short}`).catch(() => {});
|
|
952
|
+
}
|
|
879
953
|
}
|
|
880
|
-
setChatAvatar(
|
|
954
|
+
p.setChatAvatar(chatId, state.tool, "idle").catch(() => {});
|
|
881
955
|
} else {
|
|
882
|
-
// running:
|
|
883
|
-
if (
|
|
884
|
-
|
|
885
|
-
if (
|
|
886
|
-
await sendCardKitMessage(token, chatId, cardId).catch(() => null);
|
|
956
|
+
// running: 创建或更新展示
|
|
957
|
+
if (isWechat) {
|
|
958
|
+
// WeChat: 不使用卡片,基于 agent 真实 delta 推送 raw content
|
|
959
|
+
if (!display) {
|
|
887
960
|
displayCards.set(chatId, {
|
|
888
|
-
cardId,
|
|
889
|
-
sequence:
|
|
961
|
+
cardId: "",
|
|
962
|
+
sequence: 0,
|
|
890
963
|
cardBusy: false,
|
|
891
964
|
cardCreatedAt: Date.now(),
|
|
892
965
|
lastSentContent: "",
|
|
893
966
|
streamErrorNotified: false,
|
|
894
967
|
});
|
|
895
968
|
}
|
|
969
|
+
const d = displayCards.get(chatId);
|
|
970
|
+
if (!d || d.cardBusy) return;
|
|
971
|
+
|
|
972
|
+
// 分开追踪 accumulatedContent 和 finalReply 的已发送位置。
|
|
973
|
+
// 如果只用 rawFull.startsWith(prevRaw),当新的 tool_use/tool_result
|
|
974
|
+
// 插入到 accumulatedContent 中间时,rawFull 不再以 prevRaw 开头,
|
|
975
|
+
// 会回退到发送完整内容 → 产生大量重复。
|
|
976
|
+
const prevAccLen = d.lastSentAccLen ?? 0;
|
|
977
|
+
const prevFinalReply = d.lastSentFinalReply ?? "";
|
|
978
|
+
const accDelta = state.accumulatedContent.slice(prevAccLen);
|
|
979
|
+
let replyDelta: string;
|
|
980
|
+
if (prevFinalReply && state.finalReply.startsWith(prevFinalReply)) {
|
|
981
|
+
replyDelta = state.finalReply.slice(prevFinalReply.length);
|
|
982
|
+
} else {
|
|
983
|
+
replyDelta = state.finalReply;
|
|
984
|
+
}
|
|
985
|
+
const delta = (accDelta + replyDelta).trim();
|
|
986
|
+
if (!delta) return;
|
|
987
|
+
|
|
988
|
+
d.cardBusy = true;
|
|
989
|
+
try {
|
|
990
|
+
const ok = await p.sendText(chatId, delta);
|
|
991
|
+
if (ok) {
|
|
992
|
+
d.lastSentAccLen = state.accumulatedContent.length;
|
|
993
|
+
d.lastSentFinalReply = state.finalReply;
|
|
994
|
+
d.lastSentContent = delta;
|
|
995
|
+
} else {
|
|
996
|
+
// 发送失败(限流等),不更新光标,下次合并重试
|
|
997
|
+
return;
|
|
998
|
+
}
|
|
999
|
+
} catch (err) {
|
|
1000
|
+
console.error(`[${ts()}] WeChat sendText error: chatId=${chatId} ${(err as Error).message}`);
|
|
1001
|
+
if (!d.streamErrorNotified) {
|
|
1002
|
+
d.streamErrorNotified = true;
|
|
1003
|
+
p.sendText(chatId, "文本发送失败,请稍后查看结果。").catch(() => {});
|
|
1004
|
+
}
|
|
1005
|
+
} finally {
|
|
1006
|
+
d.cardBusy = false;
|
|
1007
|
+
}
|
|
896
1008
|
} else {
|
|
897
|
-
|
|
1009
|
+
// 非 WeChat: 卡片流程
|
|
1010
|
+
if (!display) {
|
|
1011
|
+
const cardId = await p.cardCreate(buildProgressCard("", { showStop: true, headerTitle: "生成中..." })).catch(() => null);
|
|
1012
|
+
if (cardId) {
|
|
1013
|
+
await p.cardSend(chatId, cardId).catch(() => null);
|
|
1014
|
+
displayCards.set(chatId, {
|
|
1015
|
+
cardId,
|
|
1016
|
+
sequence: 1,
|
|
1017
|
+
cardBusy: false,
|
|
1018
|
+
cardCreatedAt: Date.now(),
|
|
1019
|
+
lastSentContent: "",
|
|
1020
|
+
streamErrorNotified: false,
|
|
1021
|
+
});
|
|
1022
|
+
}
|
|
1023
|
+
} else {
|
|
1024
|
+
if (display.cardBusy) return;
|
|
1025
|
+
|
|
1026
|
+
// 卡片轮转
|
|
1027
|
+
if (Date.now() - display.cardCreatedAt > CARD_ROTATE_MS) {
|
|
1028
|
+
display.cardBusy = true;
|
|
1029
|
+
try {
|
|
1030
|
+
const oldSeqBase = display.sequence;
|
|
1031
|
+
const oldDisplayContent = truncateContent(state.accumulatedContent + state.finalReply) || "处理中...";
|
|
1032
|
+
const oldCard = buildProgressCard(oldDisplayContent, { showStop: false, headerTitle: "生成中...(上轮)" });
|
|
1033
|
+
await p.cardUpdate(display.cardId, oldCard, oldSeqBase + 1).catch(() => {});
|
|
1034
|
+
const newCardId = await p.cardCreate(buildProgressCard("", { showStop: true, headerTitle: "生成中..." }));
|
|
1035
|
+
if (newCardId) {
|
|
1036
|
+
await p.cardSend(chatId, newCardId);
|
|
1037
|
+
display.cardId = newCardId;
|
|
1038
|
+
display.sequence = 1;
|
|
1039
|
+
display.cardCreatedAt = Date.now();
|
|
1040
|
+
// 记录基线:分开追踪 accumulatedContent 长度和 finalReply,
|
|
1041
|
+
// 避免 tool 内容中间插入时前缀匹配失败 → 回退发完整内容
|
|
1042
|
+
display.rotationAccLen = state.accumulatedContent.length;
|
|
1043
|
+
display.rotationFinalReply = state.finalReply;
|
|
1044
|
+
display.lastSentContent = "";
|
|
1045
|
+
display.streamErrorNotified = false;
|
|
1046
|
+
}
|
|
1047
|
+
} catch (err) {
|
|
1048
|
+
console.error(`[${ts()}] [CARDIKT] rotation FAIL for ${chatId}: ${(err as Error).message}`);
|
|
1049
|
+
} finally {
|
|
1050
|
+
display.cardBusy = false;
|
|
1051
|
+
}
|
|
1052
|
+
return;
|
|
1053
|
+
}
|
|
1054
|
+
|
|
1055
|
+
// 轮转后:分开追踪 accumulatedContent 和 finalReply 增量,
|
|
1056
|
+
// 避免 tool 内容插入中间时前缀匹配失败
|
|
1057
|
+
if (display.rotationAccLen !== undefined) {
|
|
1058
|
+
const accDelta = state.accumulatedContent.slice(display.rotationAccLen);
|
|
1059
|
+
const rotReply = display.rotationFinalReply ?? "";
|
|
1060
|
+
let replyDelta: string;
|
|
1061
|
+
if (rotReply && state.finalReply.startsWith(rotReply)) {
|
|
1062
|
+
replyDelta = state.finalReply.slice(rotReply.length);
|
|
1063
|
+
} else {
|
|
1064
|
+
replyDelta = state.finalReply;
|
|
1065
|
+
}
|
|
1066
|
+
const delta = (accDelta + replyDelta).trim();
|
|
1067
|
+
if (!delta || delta === display.lastSentContent) return;
|
|
1068
|
+
display.lastSentContent = delta;
|
|
1069
|
+
const deltaCard = buildProgressCard(truncateContent(delta) || "处理中...", { showStop: true, headerTitle: "生成中..." });
|
|
1070
|
+
display.cardBusy = true;
|
|
1071
|
+
const mySeq = display.sequence + 1;
|
|
1072
|
+
try {
|
|
1073
|
+
await p.cardUpdate(display.cardId, deltaCard, mySeq);
|
|
1074
|
+
display.sequence = mySeq;
|
|
1075
|
+
} catch (err) {
|
|
1076
|
+
console.error(`[${ts()}] CardKit update error: chatId=${chatId} ${(err as Error).message}`);
|
|
1077
|
+
if (!display.streamErrorNotified) {
|
|
1078
|
+
display.streamErrorNotified = true;
|
|
1079
|
+
p.sendText(chatId, "卡片更新失败,结果将以文本形式发送。").catch(() => {});
|
|
1080
|
+
}
|
|
1081
|
+
} finally {
|
|
1082
|
+
display.cardBusy = false;
|
|
1083
|
+
}
|
|
1084
|
+
return;
|
|
1085
|
+
}
|
|
898
1086
|
|
|
899
|
-
|
|
900
|
-
|
|
1087
|
+
dotCount = (dotCount % 9) + 1;
|
|
1088
|
+
const fullContent = state.accumulatedContent + state.finalReply + "\n" + "。".repeat(dotCount);
|
|
1089
|
+
if (fullContent === display.lastSentContent) return;
|
|
1090
|
+
|
|
1091
|
+
display.lastSentContent = fullContent;
|
|
1092
|
+
const cardContent = truncateContent(fullContent);
|
|
901
1093
|
display.cardBusy = true;
|
|
1094
|
+
const mySeq = display.sequence + 1;
|
|
902
1095
|
try {
|
|
903
|
-
const
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
await updateCardKitCard(token, display.cardId, oldCard, oldSeqBase + 1).catch(() => {});
|
|
907
|
-
const newCardId = await createCardKitCard(token, buildProgressCard("", { showStop: true, headerTitle: "生成中..." }));
|
|
908
|
-
if (newCardId) {
|
|
909
|
-
await sendCardKitMessage(token, chatId, newCardId);
|
|
910
|
-
display.cardId = newCardId;
|
|
911
|
-
display.sequence = 1;
|
|
912
|
-
display.cardCreatedAt = Date.now();
|
|
913
|
-
display.lastSentContent = "";
|
|
914
|
-
display.streamErrorNotified = false;
|
|
915
|
-
}
|
|
1096
|
+
const card = buildProgressCard(cardContent, { showStop: true, headerTitle: "生成中..." });
|
|
1097
|
+
await p.cardUpdate(display.cardId, card, mySeq);
|
|
1098
|
+
display.sequence = mySeq;
|
|
916
1099
|
} catch (err) {
|
|
917
|
-
console.error(`[${ts()}]
|
|
1100
|
+
console.error(`[${ts()}] CardKit update error: chatId=${chatId} ${(err as Error).message}`);
|
|
1101
|
+
if (!display.streamErrorNotified) {
|
|
1102
|
+
display.streamErrorNotified = true;
|
|
1103
|
+
p.sendText(chatId, "卡片更新失败,结果将以文本形式发送。").catch(() => {});
|
|
1104
|
+
}
|
|
918
1105
|
} finally {
|
|
919
1106
|
display.cardBusy = false;
|
|
920
1107
|
}
|
|
921
|
-
return;
|
|
922
|
-
}
|
|
923
|
-
|
|
924
|
-
dotCount = (dotCount % 9) + 1;
|
|
925
|
-
const content = truncateContent(state.accumulatedContent + state.finalReply + "\n" + "。".repeat(dotCount));
|
|
926
|
-
if (content === display.lastSentContent) return;
|
|
927
|
-
|
|
928
|
-
display.lastSentContent = content;
|
|
929
|
-
display.cardBusy = true;
|
|
930
|
-
const mySeq = display.sequence + 1;
|
|
931
|
-
try {
|
|
932
|
-
const card = buildProgressCard(content, { showStop: true, headerTitle: "生成中..." });
|
|
933
|
-
await updateCardKitCard(token, display.cardId, card, mySeq);
|
|
934
|
-
display.sequence = mySeq;
|
|
935
|
-
} catch (err) {
|
|
936
|
-
console.error(`[${ts()}] CardKit update error: chatId=${chatId} ${(err as Error).message}`);
|
|
937
|
-
if (!display.streamErrorNotified) {
|
|
938
|
-
display.streamErrorNotified = true;
|
|
939
|
-
sendTextReply(token, chatId, "卡片更新失败,结果将以文本形式发送。").catch(() => {});
|
|
940
|
-
}
|
|
941
|
-
} finally {
|
|
942
|
-
display.cardBusy = false;
|
|
943
1108
|
}
|
|
944
1109
|
}
|
|
945
1110
|
}
|