chatccc 0.2.92 → 0.2.93
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/package.json +1 -1
- package/src/__tests__/session.test.ts +20 -3
- package/src/index.ts +3 -1
- package/src/orchestrator.ts +0 -10
- package/src/session-chat-binding.ts +14 -4
- package/src/session.ts +142 -192
package/package.json
CHANGED
|
@@ -41,7 +41,8 @@ import {
|
|
|
41
41
|
setSessionPlatform,
|
|
42
42
|
recordChatPlatform,
|
|
43
43
|
_getPlatformForChatForTest,
|
|
44
|
-
|
|
44
|
+
startUnifiedDisplayLoop,
|
|
45
|
+
stopUnifiedDisplayLoop,
|
|
45
46
|
} from "../session.ts";
|
|
46
47
|
import {
|
|
47
48
|
activePrompts,
|
|
@@ -184,7 +185,7 @@ describe("chat platform routing", () => {
|
|
|
184
185
|
// processedMessages 全部保留——SDK 重连不应当影响后台 prompt 的执行。
|
|
185
186
|
// ---------------------------------------------------------------------------
|
|
186
187
|
|
|
187
|
-
describe("
|
|
188
|
+
describe("unified display loop WeChat delta", () => {
|
|
188
189
|
beforeEach(() => {
|
|
189
190
|
vi.useFakeTimers();
|
|
190
191
|
resetState();
|
|
@@ -193,6 +194,7 @@ describe("ensureDisplayLoop WeChat delta", () => {
|
|
|
193
194
|
});
|
|
194
195
|
|
|
195
196
|
afterEach(() => {
|
|
197
|
+
stopUnifiedDisplayLoop();
|
|
196
198
|
resetBindingState();
|
|
197
199
|
vi.useRealTimers();
|
|
198
200
|
});
|
|
@@ -212,16 +214,31 @@ describe("ensureDisplayLoop WeChat delta", () => {
|
|
|
212
214
|
tool: "claude",
|
|
213
215
|
});
|
|
214
216
|
|
|
217
|
+
// 模拟 runAgentSession 创建的 WeChat display 条目
|
|
218
|
+
displayCards.set("chat-wechat", {
|
|
219
|
+
cardId: "",
|
|
220
|
+
sequence: 0,
|
|
221
|
+
cardBusy: false,
|
|
222
|
+
cardCreatedAt: Date.now(),
|
|
223
|
+
lastSentContent: "",
|
|
224
|
+
streamErrorNotified: false,
|
|
225
|
+
sessionId: "sid-wechat",
|
|
226
|
+
turnCount: 1,
|
|
227
|
+
dotCount: 0,
|
|
228
|
+
});
|
|
229
|
+
|
|
215
230
|
mockStreamStates.set("sid-wechat", {
|
|
216
231
|
accumulatedContent: "",
|
|
217
232
|
finalReply: "partial reply",
|
|
233
|
+
status: "running",
|
|
218
234
|
});
|
|
219
|
-
|
|
235
|
+
startUnifiedDisplayLoop();
|
|
220
236
|
await vi.advanceTimersByTimeAsync(3000);
|
|
221
237
|
|
|
222
238
|
mockStreamStates.set("sid-wechat", {
|
|
223
239
|
accumulatedContent: "tool output\n",
|
|
224
240
|
finalReply: "partial reply",
|
|
241
|
+
status: "running",
|
|
225
242
|
});
|
|
226
243
|
await vi.advanceTimersByTimeAsync(3000);
|
|
227
244
|
|
package/src/index.ts
CHANGED
|
@@ -92,6 +92,7 @@ import {
|
|
|
92
92
|
rebuildBindingsFromRegistry,
|
|
93
93
|
resetState,
|
|
94
94
|
setSessionPlatform,
|
|
95
|
+
startUnifiedDisplayLoop,
|
|
95
96
|
} from "./session.ts";
|
|
96
97
|
import {
|
|
97
98
|
rebuildSessionChatsFromRegistry,
|
|
@@ -595,13 +596,14 @@ async function startBotServiceCore(): Promise<void> {
|
|
|
595
596
|
// 进程首次启动:此时所有 Map 都是空的,resetState 主要是打个 LOG 标识"开始
|
|
596
597
|
// 干净状态"。修正残留的 running stream-state 并重建 session→chat 映射。
|
|
597
598
|
resetState();
|
|
599
|
+
startUnifiedDisplayLoop();
|
|
598
600
|
fixStaleStreamStates().then(async () => {
|
|
599
601
|
const registry = await loadSessionRegistryForBinding();
|
|
600
602
|
rebuildSessionChatsFromRegistry(registry);
|
|
601
603
|
}).catch((err) => console.error(`[${ts()}] Init bindings failed: ${(err as Error).message}`));
|
|
602
604
|
|
|
603
605
|
// ⚠️ 关键设计:onReady / onReconnected 只重建只读映射,**绝不**清空运行时
|
|
604
|
-
// 状态(activePrompts、
|
|
606
|
+
// 状态(activePrompts、sessionInfoMap、processedMessages)。
|
|
605
607
|
// SDK 重连只是底层 WebSocket 抖动,业务层不受影响:
|
|
606
608
|
// - 后台 generator 仍在跑、stream-state 仍在被写
|
|
607
609
|
// - display loop 仍在向群推送
|
package/src/orchestrator.ts
CHANGED
|
@@ -740,11 +740,6 @@ export async function handleCommand(
|
|
|
740
740
|
.setChatAvatar(chatId, descriptionTool, "new")
|
|
741
741
|
.catch(() => {});
|
|
742
742
|
|
|
743
|
-
if (isSessionRunning(newSessionId)) {
|
|
744
|
-
const { ensureDisplayLoop } = await import("./session.ts");
|
|
745
|
-
ensureDisplayLoop(newSessionId);
|
|
746
|
-
}
|
|
747
|
-
|
|
748
743
|
await platform.sendCard(
|
|
749
744
|
chatId,
|
|
750
745
|
`${toolLabel} Session Reset`,
|
|
@@ -901,11 +896,6 @@ export async function handleCommand(
|
|
|
901
896
|
|
|
902
897
|
platform.setChatAvatar(chatId, target.tool, "new").catch(() => {});
|
|
903
898
|
|
|
904
|
-
if (isSessionRunning(target.sessionId)) {
|
|
905
|
-
const { ensureDisplayLoop } = await import("./session.ts");
|
|
906
|
-
ensureDisplayLoop(target.sessionId);
|
|
907
|
-
}
|
|
908
|
-
|
|
909
899
|
const targetToolLabel = toolDisplayName(target.tool);
|
|
910
900
|
const busyNote = isSessionRunning(target.sessionId)
|
|
911
901
|
? "\n\n⚠️ 该会话当前正在生成中,请等待完成后再发送消息。"
|
|
@@ -130,6 +130,8 @@ export interface DisplayCardState {
|
|
|
130
130
|
cardCreatedAt: number;
|
|
131
131
|
lastSentContent: string;
|
|
132
132
|
streamErrorNotified: boolean;
|
|
133
|
+
/** 所属 session */
|
|
134
|
+
sessionId: string;
|
|
133
135
|
/** 所属 turn */
|
|
134
136
|
turnCount: number;
|
|
135
137
|
/** 卡片轮转时记录的基线,轮转后只展示增量 */
|
|
@@ -139,12 +141,18 @@ export interface DisplayCardState {
|
|
|
139
141
|
lastSentAccLen?: number;
|
|
140
142
|
/** WeChat delta: 上次发送时的 finalReply */
|
|
141
143
|
lastSentFinalReply?: string;
|
|
144
|
+
/** 点点点动画计数器(统一 display loop 每个卡片独立计数) */
|
|
145
|
+
dotCount: number;
|
|
142
146
|
}
|
|
143
147
|
|
|
144
148
|
export const displayCards = new Map<string, DisplayCardState>();
|
|
145
149
|
|
|
146
|
-
/**
|
|
147
|
-
export
|
|
150
|
+
/** 统一 display loop 的 interval handle,由 session.ts 管理 */
|
|
151
|
+
export let unifiedDisplayLoopHandle: ReturnType<typeof setInterval> | null = null;
|
|
152
|
+
|
|
153
|
+
export function setUnifiedDisplayLoopHandle(h: ReturnType<typeof setInterval> | null): void {
|
|
154
|
+
unifiedDisplayLoopHandle = h;
|
|
155
|
+
}
|
|
148
156
|
|
|
149
157
|
// ---------------------------------------------------------------------------
|
|
150
158
|
// queuedMessages: sessionId → 缓存消息(生成中排队,队列最大长度 1)
|
|
@@ -201,6 +209,8 @@ export function resetBindingState(): void {
|
|
|
201
209
|
activePrompts.clear();
|
|
202
210
|
queuedMessages.clear();
|
|
203
211
|
displayCards.clear();
|
|
204
|
-
|
|
205
|
-
|
|
212
|
+
if (unifiedDisplayLoopHandle !== null) {
|
|
213
|
+
clearInterval(unifiedDisplayLoopHandle);
|
|
214
|
+
unifiedDisplayLoopHandle = null;
|
|
215
|
+
}
|
|
206
216
|
}
|
package/src/session.ts
CHANGED
|
@@ -46,7 +46,8 @@ import {
|
|
|
46
46
|
isSessionRunning,
|
|
47
47
|
activePrompts,
|
|
48
48
|
displayCards,
|
|
49
|
-
|
|
49
|
+
unifiedDisplayLoopHandle,
|
|
50
|
+
setUnifiedDisplayLoopHandle,
|
|
50
51
|
rebuildSessionChatsFromRegistry,
|
|
51
52
|
recordLastActiveChat,
|
|
52
53
|
getLastActiveChat,
|
|
@@ -171,8 +172,7 @@ export function resetState(): void {
|
|
|
171
172
|
chatPlatformMap.clear();
|
|
172
173
|
activePrompts.clear();
|
|
173
174
|
displayCards.clear();
|
|
174
|
-
|
|
175
|
-
displayLoops.clear();
|
|
175
|
+
stopUnifiedDisplayLoop();
|
|
176
176
|
console.log(`[${ts()}] [RESET] State cleared (dedup + active sessions + bindings)`);
|
|
177
177
|
}
|
|
178
178
|
|
|
@@ -321,7 +321,7 @@ export async function loadSessionRegistryForBinding(): Promise<SessionRegistryDa
|
|
|
321
321
|
* 否则下条用户消息会绕过 isSessionRunning 检查再开一条 prompt,
|
|
322
322
|
* 导致同一 sessionId 双开 generator
|
|
323
323
|
* - **不动** sessionInfoMap:内存里的轮数/contextTokens 比 registry 更新
|
|
324
|
-
* - **不动** displayCards
|
|
324
|
+
* - **不动** displayCards:正在跑的 prompt 还需要它们继续推卡片
|
|
325
325
|
* - **不动** processedMessages / lastMsgTimestamps:SDK 重连若重推已 ack 消息,
|
|
326
326
|
* 去重 set 还在才能避免同一 prompt 跑两遍
|
|
327
327
|
*
|
|
@@ -805,10 +805,9 @@ export async function runAgentSession(
|
|
|
805
805
|
if (displayChatId) {
|
|
806
806
|
const pp = platformForChat(displayChatId);
|
|
807
807
|
const display = displayCards.get(displayChatId);
|
|
808
|
-
const oldLoopExisted = displayLoops.has(sessionId);
|
|
809
808
|
|
|
810
809
|
if (display && pp) {
|
|
811
|
-
//
|
|
810
|
+
// 统一 display loop 被 cardBusy 挡住或尚未 tick → 现在终结卡片
|
|
812
811
|
while (display.cardBusy) await new Promise(r => setTimeout(r, 20));
|
|
813
812
|
const nextSeq = display.sequence + 1;
|
|
814
813
|
const headerTitle = prevState.status === "stopped" ? "已停止" : "完成";
|
|
@@ -826,13 +825,13 @@ export async function runAgentSession(
|
|
|
826
825
|
await pp.sendText(displayChatId, prevState.finalReply).catch(() => {});
|
|
827
826
|
}
|
|
828
827
|
pp.setChatAvatar(displayChatId, prevState.tool, "idle").catch(() => {});
|
|
829
|
-
} else if (
|
|
830
|
-
// display
|
|
831
|
-
|
|
828
|
+
} else if (pp && prevState.finalReply) {
|
|
829
|
+
// 无 display 记录但上一轮有 finalReply(极快轮次),至少发送
|
|
830
|
+
const finalStatus = prevState.status === "stopped" ? "stopped" : "done";
|
|
832
831
|
finalizeTurnCards(sessionId, prevState.turnCount, finalStatus).catch(() => {});
|
|
833
832
|
await pp.sendText(displayChatId, prevState.finalReply).catch(() => {});
|
|
834
833
|
}
|
|
835
|
-
// else:
|
|
834
|
+
// else: displayCards 无记录且无 finalReply → 无需处理
|
|
836
835
|
}
|
|
837
836
|
}
|
|
838
837
|
|
|
@@ -841,7 +840,7 @@ export async function runAgentSession(
|
|
|
841
840
|
await writeStreamState(initialState);
|
|
842
841
|
|
|
843
842
|
// 为新 turn 创建第一张展示卡片,同时注册到 turn-cards 持久化。
|
|
844
|
-
//
|
|
843
|
+
// 统一 display loop 始终运行,卡片创建后下一个 tick 即自动开始更新。
|
|
845
844
|
const displayChatIdForNew = pickDisplayChat(sessionId);
|
|
846
845
|
if (displayChatIdForNew) {
|
|
847
846
|
const ppNew = platformForChat(displayChatIdForNew);
|
|
@@ -858,16 +857,28 @@ export async function runAgentSession(
|
|
|
858
857
|
cardCreatedAt: Date.now(),
|
|
859
858
|
lastSentContent: "",
|
|
860
859
|
streamErrorNotified: false,
|
|
860
|
+
sessionId,
|
|
861
861
|
turnCount: nextTurnCount,
|
|
862
|
+
dotCount: 0,
|
|
862
863
|
});
|
|
863
864
|
addCardToTurn(sessionId, nextTurnCount, cardId).catch(() => {});
|
|
864
865
|
}
|
|
866
|
+
} else if (ppNew && ppNew.kind === "wechat") {
|
|
867
|
+
// WeChat: 无卡片,但需要 display entry 追踪已发送内容
|
|
868
|
+
displayCards.set(displayChatIdForNew, {
|
|
869
|
+
cardId: "",
|
|
870
|
+
sequence: 0,
|
|
871
|
+
cardBusy: false,
|
|
872
|
+
cardCreatedAt: Date.now(),
|
|
873
|
+
lastSentContent: "",
|
|
874
|
+
streamErrorNotified: false,
|
|
875
|
+
sessionId,
|
|
876
|
+
turnCount: nextTurnCount,
|
|
877
|
+
dotCount: 0,
|
|
878
|
+
});
|
|
865
879
|
}
|
|
866
880
|
}
|
|
867
881
|
|
|
868
|
-
// 启动 display loop
|
|
869
|
-
ensureDisplayLoop(sessionId);
|
|
870
|
-
|
|
871
882
|
// 设置最后活跃群头像为 busy
|
|
872
883
|
const activeCid = getLastActiveChat(sessionId) ?? getChatsForSession(sessionId)[0];
|
|
873
884
|
if (activeCid) {
|
|
@@ -1019,90 +1030,74 @@ export async function runAgentSession(
|
|
|
1019
1030
|
}
|
|
1020
1031
|
|
|
1021
1032
|
// ---------------------------------------------------------------------------
|
|
1022
|
-
//
|
|
1033
|
+
// startUnifiedDisplayLoop — 全局统一 display 循环,遍历 displayCards 更新卡片
|
|
1034
|
+
// ---------------------------------------------------------------------------
|
|
1035
|
+
// 替代旧的 per-session ensureDisplayLoop,消除 kill/restart 竞态条件。
|
|
1036
|
+
// 单一定时器遍历所有 displayCards 条目,通过条目内的 sessionId 查找 stream state。
|
|
1023
1037
|
// ---------------------------------------------------------------------------
|
|
1024
1038
|
|
|
1025
1039
|
const CARD_ROTATE_MS = 9 * 60 * 1000;
|
|
1026
1040
|
|
|
1027
|
-
export function
|
|
1028
|
-
|
|
1029
|
-
// (发送完成卡片、清理 display)尚未 delete 自己,新 turn 的
|
|
1030
|
-
// runAgentSession 调用 ensureDisplayLoop 时因 guard 直接返回,
|
|
1031
|
-
// 导致新 turn 没有 display loop → 卡片永久不更新。
|
|
1032
|
-
const oldStop = displayLoops.get(sessionId);
|
|
1033
|
-
if (oldStop) {
|
|
1034
|
-
console.log(`[${ts()}] [DISPLAY] ensureDisplayLoop restarting loop for ${sessionId} (old loop existed)`);
|
|
1035
|
-
oldStop();
|
|
1036
|
-
displayLoops.delete(sessionId);
|
|
1037
|
-
}
|
|
1038
|
-
|
|
1039
|
-
let dotCount = 0;
|
|
1041
|
+
export function startUnifiedDisplayLoop(): void {
|
|
1042
|
+
if (unifiedDisplayLoopHandle !== null) return;
|
|
1040
1043
|
|
|
1041
1044
|
const interval = setInterval(() => {
|
|
1042
1045
|
void (async () => {
|
|
1043
|
-
const
|
|
1044
|
-
|
|
1045
|
-
|
|
1046
|
-
|
|
1047
|
-
|
|
1048
|
-
|
|
1049
|
-
|
|
1050
|
-
|
|
1051
|
-
|
|
1052
|
-
|
|
1053
|
-
|
|
1054
|
-
|
|
1055
|
-
|
|
1056
|
-
|
|
1057
|
-
if (
|
|
1058
|
-
|
|
1046
|
+
for (const [chatId, display] of displayCards) {
|
|
1047
|
+
if (display.cardBusy) continue;
|
|
1048
|
+
|
|
1049
|
+
const sessionId = display.sessionId;
|
|
1050
|
+
const state = await readStreamState(sessionId);
|
|
1051
|
+
if (!state) {
|
|
1052
|
+
displayCards.delete(chatId);
|
|
1053
|
+
continue;
|
|
1054
|
+
}
|
|
1055
|
+
|
|
1056
|
+
// 交叉验证:chat 当前绑定的 session 是否仍是 display 记录的 session。
|
|
1057
|
+
// 若 chat 已被切换到其他 session(如 /newh),旧 display 必须停推。
|
|
1058
|
+
const currentSessionForChat = sessionInfoMap.get(chatId)?.sessionId;
|
|
1059
|
+
if (currentSessionForChat && currentSessionForChat !== sessionId) {
|
|
1060
|
+
if (state.status !== "running") {
|
|
1061
|
+
displayCards.delete(chatId);
|
|
1059
1062
|
}
|
|
1063
|
+
continue;
|
|
1060
1064
|
}
|
|
1061
|
-
return;
|
|
1062
|
-
}
|
|
1063
1065
|
|
|
1064
|
-
|
|
1065
|
-
|
|
1066
|
-
|
|
1067
|
-
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
|
|
1071
|
-
displayLoops.delete(sessionId);
|
|
1066
|
+
// 验证 chat 仍是该 session 的最后活跃群
|
|
1067
|
+
const lastActive = getLastActiveChat(sessionId);
|
|
1068
|
+
if (lastActive !== chatId) {
|
|
1069
|
+
if (state.status !== "running") {
|
|
1070
|
+
displayCards.delete(chatId);
|
|
1071
|
+
}
|
|
1072
|
+
continue;
|
|
1072
1073
|
}
|
|
1073
|
-
return;
|
|
1074
|
-
}
|
|
1075
1074
|
|
|
1076
|
-
|
|
1077
|
-
|
|
1078
|
-
|
|
1079
|
-
|
|
1080
|
-
|
|
1081
|
-
|
|
1082
|
-
|
|
1083
|
-
|
|
1084
|
-
|
|
1085
|
-
|
|
1086
|
-
|
|
1087
|
-
|
|
1088
|
-
|
|
1089
|
-
|
|
1090
|
-
|
|
1091
|
-
|
|
1092
|
-
|
|
1093
|
-
|
|
1094
|
-
|
|
1075
|
+
const isTerminal = state.status !== "running";
|
|
1076
|
+
|
|
1077
|
+
try {
|
|
1078
|
+
const p = platformForChat(chatId);
|
|
1079
|
+
if (!p) continue;
|
|
1080
|
+
|
|
1081
|
+
const isWechat = p.kind === "wechat";
|
|
1082
|
+
|
|
1083
|
+
if (isTerminal) {
|
|
1084
|
+
if (isWechat) {
|
|
1085
|
+
const prevAccLen = display.lastSentAccLen ?? 0;
|
|
1086
|
+
const prevFinalReply = display.lastSentFinalReply ?? "";
|
|
1087
|
+
const accDelta = state.accumulatedContent.slice(prevAccLen);
|
|
1088
|
+
let replyDelta: string;
|
|
1089
|
+
if (prevFinalReply && state.finalReply.startsWith(prevFinalReply)) {
|
|
1090
|
+
replyDelta = state.finalReply.slice(prevFinalReply.length);
|
|
1091
|
+
} else {
|
|
1092
|
+
replyDelta = state.finalReply;
|
|
1093
|
+
}
|
|
1094
|
+
const remaining = (accDelta + replyDelta).trim();
|
|
1095
|
+
const tail = "━━━ 回答结束 ━━━";
|
|
1096
|
+
const finalMsg = remaining ? remaining + "\n" + tail : tail;
|
|
1097
|
+
await p.sendText(chatId, finalMsg).catch(() => {});
|
|
1098
|
+
displayCards.delete(chatId);
|
|
1095
1099
|
} else {
|
|
1096
|
-
|
|
1097
|
-
}
|
|
1098
|
-
const remaining = (accDelta + replyDelta).trim();
|
|
1099
|
-
const tail = "━━━ 回答结束 ━━━";
|
|
1100
|
-
const finalMsg = remaining ? remaining + "\n" + tail : tail;
|
|
1101
|
-
await p.sendText(chatId, finalMsg).catch(() => {});
|
|
1102
|
-
if (display) displayCards.delete(chatId);
|
|
1103
|
-
} else {
|
|
1104
|
-
// 发送最终结果(卡片平台)
|
|
1105
|
-
if (display) {
|
|
1100
|
+
// 发送最终结果(卡片平台)
|
|
1106
1101
|
while (display.cardBusy) await new Promise(r => setTimeout(r, 20));
|
|
1107
1102
|
const nextSeq = display.sequence + 1;
|
|
1108
1103
|
const headerTitle = state.status === "stopped" ? "已停止" : "完成";
|
|
@@ -1113,99 +1108,55 @@ export function ensureDisplayLoop(sessionId: string): void {
|
|
|
1113
1108
|
const finalSt = state.status === "stopped" ? "stopped" : "done";
|
|
1114
1109
|
finalizeTurnCards(sessionId, state.turnCount, finalSt).catch(() => {});
|
|
1115
1110
|
displayCards.delete(chatId);
|
|
1116
|
-
|
|
1117
|
-
|
|
1118
|
-
|
|
1119
|
-
|
|
1120
|
-
|
|
1121
|
-
await p.sendText(chatId, `[生成过程]\n${short}`).catch(() => {});
|
|
1122
|
-
}
|
|
1123
|
-
}
|
|
1124
|
-
p.setChatAvatar(chatId, state.tool, "idle").catch(() => {});
|
|
1125
|
-
} else {
|
|
1126
|
-
// running: 创建或更新展示
|
|
1127
|
-
if (isWechat) {
|
|
1128
|
-
// WeChat: 不使用卡片,基于 agent 真实 delta 推送 raw content
|
|
1129
|
-
if (!display) {
|
|
1130
|
-
displayCards.set(chatId, {
|
|
1131
|
-
cardId: "",
|
|
1132
|
-
sequence: 0,
|
|
1133
|
-
cardBusy: false,
|
|
1134
|
-
cardCreatedAt: Date.now(),
|
|
1135
|
-
lastSentContent: "",
|
|
1136
|
-
streamErrorNotified: false,
|
|
1137
|
-
});
|
|
1138
|
-
}
|
|
1139
|
-
const d = displayCards.get(chatId);
|
|
1140
|
-
if (!d || d.cardBusy) return;
|
|
1141
|
-
|
|
1142
|
-
// 分开追踪 accumulatedContent 和 finalReply 的已发送位置。
|
|
1143
|
-
// 如果只用 rawFull.startsWith(prevRaw),当新的 tool_use/tool_result
|
|
1144
|
-
// 插入到 accumulatedContent 中间时,rawFull 不再以 prevRaw 开头,
|
|
1145
|
-
// 会回退到发送完整内容 → 产生大量重复。
|
|
1146
|
-
const prevAccLen = d.lastSentAccLen ?? 0;
|
|
1147
|
-
const prevFinalReply = d.lastSentFinalReply ?? "";
|
|
1148
|
-
const accDelta = state.accumulatedContent.slice(prevAccLen);
|
|
1149
|
-
let replyDelta: string;
|
|
1150
|
-
if (prevFinalReply && state.finalReply.startsWith(prevFinalReply)) {
|
|
1151
|
-
replyDelta = state.finalReply.slice(prevFinalReply.length);
|
|
1152
|
-
} else {
|
|
1153
|
-
replyDelta = state.finalReply;
|
|
1154
|
-
}
|
|
1155
|
-
const delta = (accDelta + replyDelta).trim();
|
|
1156
|
-
if (!delta) return;
|
|
1157
|
-
|
|
1158
|
-
d.cardBusy = true;
|
|
1159
|
-
try {
|
|
1160
|
-
const ok = await p.sendText(chatId, compressWechatDisplayText(delta));
|
|
1161
|
-
if (ok) {
|
|
1162
|
-
d.lastSentAccLen = state.accumulatedContent.length;
|
|
1163
|
-
d.lastSentFinalReply = state.finalReply;
|
|
1164
|
-
d.lastSentContent = delta;
|
|
1165
|
-
} else {
|
|
1166
|
-
// 发送失败(限流等),不更新光标,下次合并重试
|
|
1167
|
-
return;
|
|
1168
|
-
}
|
|
1169
|
-
} catch (err) {
|
|
1170
|
-
console.error(`[${ts()}] WeChat sendText error: chatId=${chatId} ${(err as Error).message}`);
|
|
1171
|
-
if (!d.streamErrorNotified) {
|
|
1172
|
-
d.streamErrorNotified = true;
|
|
1173
|
-
p.sendText(chatId, "文本发送失败,请稍后查看结果。").catch(() => {});
|
|
1111
|
+
if (state.finalReply) {
|
|
1112
|
+
await p.sendText(chatId, state.finalReply).catch(() => {});
|
|
1113
|
+
} else if (state.accumulatedContent.trim()) {
|
|
1114
|
+
const short = truncateContent(state.accumulatedContent, 30, 4000);
|
|
1115
|
+
await p.sendText(chatId, `[生成过程]\n${short}`).catch(() => {});
|
|
1174
1116
|
}
|
|
1175
|
-
} finally {
|
|
1176
|
-
d.cardBusy = false;
|
|
1177
1117
|
}
|
|
1118
|
+
p.setChatAvatar(chatId, state.tool, "idle").catch(() => {});
|
|
1119
|
+
console.log(`[${ts()}] [DISPLAY] unified loop deleted display for ${chatId} (terminal: ${state.status})`);
|
|
1178
1120
|
} else {
|
|
1179
|
-
//
|
|
1180
|
-
if (
|
|
1181
|
-
//
|
|
1182
|
-
const
|
|
1183
|
-
|
|
1184
|
-
|
|
1185
|
-
|
|
1186
|
-
|
|
1187
|
-
|
|
1188
|
-
|
|
1189
|
-
|
|
1190
|
-
lastSentContent: "",
|
|
1191
|
-
streamErrorNotified: false,
|
|
1192
|
-
turnCount: state.turnCount,
|
|
1193
|
-
});
|
|
1194
|
-
addCardToTurn(sessionId, state.turnCount, cardId).catch(() => {});
|
|
1121
|
+
// running: 创建或更新展示
|
|
1122
|
+
if (isWechat) {
|
|
1123
|
+
// WeChat: 不使用卡片,基于 agent 真实 delta 推送 raw content
|
|
1124
|
+
const prevAccLen = display.lastSentAccLen ?? 0;
|
|
1125
|
+
const prevFinalReply = display.lastSentFinalReply ?? "";
|
|
1126
|
+
const accDelta = state.accumulatedContent.slice(prevAccLen);
|
|
1127
|
+
let replyDelta: string;
|
|
1128
|
+
if (prevFinalReply && state.finalReply.startsWith(prevFinalReply)) {
|
|
1129
|
+
replyDelta = state.finalReply.slice(prevFinalReply.length);
|
|
1130
|
+
} else {
|
|
1131
|
+
replyDelta = state.finalReply;
|
|
1195
1132
|
}
|
|
1196
|
-
|
|
1197
|
-
if (
|
|
1133
|
+
const delta = (accDelta + replyDelta).trim();
|
|
1134
|
+
if (!delta) continue;
|
|
1198
1135
|
|
|
1199
|
-
|
|
1200
|
-
|
|
1201
|
-
|
|
1136
|
+
display.cardBusy = true;
|
|
1137
|
+
try {
|
|
1138
|
+
const ok = await p.sendText(chatId, compressWechatDisplayText(delta));
|
|
1139
|
+
if (ok) {
|
|
1140
|
+
display.lastSentAccLen = state.accumulatedContent.length;
|
|
1141
|
+
display.lastSentFinalReply = state.finalReply;
|
|
1142
|
+
display.lastSentContent = delta;
|
|
1143
|
+
}
|
|
1144
|
+
} catch (err) {
|
|
1145
|
+
console.error(`[${ts()}] WeChat sendText error: chatId=${chatId} ${(err as Error).message}`);
|
|
1146
|
+
if (!display.streamErrorNotified) {
|
|
1147
|
+
display.streamErrorNotified = true;
|
|
1148
|
+
p.sendText(chatId, "文本发送失败,请稍后查看结果。").catch(() => {});
|
|
1149
|
+
}
|
|
1150
|
+
} finally {
|
|
1151
|
+
display.cardBusy = false;
|
|
1152
|
+
}
|
|
1153
|
+
} else {
|
|
1154
|
+
// 非 WeChat: 卡片流程
|
|
1202
1155
|
if (display.turnCount !== state.turnCount) {
|
|
1203
1156
|
console.log(`[${ts()}] [DISPLAY] turn mismatch for ${chatId}: display.turnCount=${display.turnCount} state.turnCount=${state.turnCount}, resetting`);
|
|
1204
|
-
// 兜底:turn mismatch 意味着旧 turn 已结束但卡片未在飞书端终结,
|
|
1205
|
-
// 持久化标记为 done,避免 turn-cards.json 残留 "active"
|
|
1206
1157
|
finalizeTurnCards(sessionId, display.turnCount, "done").catch(() => {});
|
|
1207
1158
|
displayCards.delete(chatId);
|
|
1208
|
-
|
|
1159
|
+
continue;
|
|
1209
1160
|
}
|
|
1210
1161
|
|
|
1211
1162
|
// 卡片轮转
|
|
@@ -1227,8 +1178,6 @@ export function ensureDisplayLoop(sessionId: string): void {
|
|
|
1227
1178
|
display.cardId = newCardId;
|
|
1228
1179
|
display.sequence = 1;
|
|
1229
1180
|
display.cardCreatedAt = Date.now();
|
|
1230
|
-
// 记录基线:分开追踪 accumulatedContent 长度和 finalReply,
|
|
1231
|
-
// 避免 tool 内容中间插入时前缀匹配失败 → 回退发完整内容
|
|
1232
1181
|
display.rotationAccLen = state.accumulatedContent.length;
|
|
1233
1182
|
display.rotationFinalReply = state.finalReply;
|
|
1234
1183
|
display.lastSentContent = "";
|
|
@@ -1239,11 +1188,10 @@ export function ensureDisplayLoop(sessionId: string): void {
|
|
|
1239
1188
|
} finally {
|
|
1240
1189
|
display.cardBusy = false;
|
|
1241
1190
|
}
|
|
1242
|
-
|
|
1191
|
+
continue;
|
|
1243
1192
|
}
|
|
1244
1193
|
|
|
1245
|
-
// 轮转后:分开追踪 accumulatedContent 和 finalReply
|
|
1246
|
-
// 避免 tool 内容插入中间时前缀匹配失败
|
|
1194
|
+
// 轮转后:分开追踪 accumulatedContent 和 finalReply 增量
|
|
1247
1195
|
if (display.rotationAccLen !== undefined) {
|
|
1248
1196
|
const accDelta = state.accumulatedContent.slice(display.rotationAccLen);
|
|
1249
1197
|
const rotReply = display.rotationFinalReply ?? "";
|
|
@@ -1255,10 +1203,9 @@ export function ensureDisplayLoop(sessionId: string): void {
|
|
|
1255
1203
|
}
|
|
1256
1204
|
const delta = (accDelta + replyDelta).trim();
|
|
1257
1205
|
|
|
1258
|
-
|
|
1259
|
-
|
|
1260
|
-
|
|
1261
|
-
if (displayContent === display.lastSentContent) return;
|
|
1206
|
+
display.dotCount = (display.dotCount % 9) + 1;
|
|
1207
|
+
const displayContent = (delta || "") + "\n" + "。" .repeat(display.dotCount);
|
|
1208
|
+
if (displayContent === display.lastSentContent) continue;
|
|
1262
1209
|
|
|
1263
1210
|
display.lastSentContent = displayContent;
|
|
1264
1211
|
const deltaCard = buildProgressCard(truncateContent(displayContent) || "处理中...", { showStop: true, headerTitle: "生成中..." });
|
|
@@ -1276,12 +1223,12 @@ export function ensureDisplayLoop(sessionId: string): void {
|
|
|
1276
1223
|
} finally {
|
|
1277
1224
|
display.cardBusy = false;
|
|
1278
1225
|
}
|
|
1279
|
-
|
|
1226
|
+
continue;
|
|
1280
1227
|
}
|
|
1281
1228
|
|
|
1282
|
-
dotCount = (dotCount % 9) + 1;
|
|
1283
|
-
const fullContent = state.accumulatedContent + state.finalReply + "\n" + "。".repeat(dotCount);
|
|
1284
|
-
if (fullContent === display.lastSentContent)
|
|
1229
|
+
display.dotCount = (display.dotCount % 9) + 1;
|
|
1230
|
+
const fullContent = state.accumulatedContent + state.finalReply + "\n" + "。".repeat(display.dotCount);
|
|
1231
|
+
if (fullContent === display.lastSentContent) continue;
|
|
1285
1232
|
|
|
1286
1233
|
display.lastSentContent = fullContent;
|
|
1287
1234
|
const cardContent = truncateContent(fullContent);
|
|
@@ -1302,23 +1249,26 @@ export function ensureDisplayLoop(sessionId: string): void {
|
|
|
1302
1249
|
}
|
|
1303
1250
|
}
|
|
1304
1251
|
}
|
|
1252
|
+
} catch (err) {
|
|
1253
|
+
console.error(`[${ts()}] Display loop error for ${chatId}: ${(err as Error).message}`);
|
|
1305
1254
|
}
|
|
1306
|
-
} catch (err) {
|
|
1307
|
-
console.error(`[${ts()}] Display loop error for ${chatId}: ${(err as Error).message}`);
|
|
1308
|
-
}
|
|
1309
|
-
|
|
1310
|
-
if (isTerminal) {
|
|
1311
|
-
console.log(`[${ts()}] [DISPLAY] loop stopping for ${sessionId} (terminal state: ${state.status})`);
|
|
1312
|
-
clearInterval(interval);
|
|
1313
|
-
displayLoops.delete(sessionId);
|
|
1314
1255
|
}
|
|
1315
1256
|
})().catch((err: unknown) => {
|
|
1316
1257
|
const e = err instanceof Error ? err : new Error(String(err));
|
|
1317
|
-
console.error(`[${ts()}]
|
|
1258
|
+
console.error(`[${ts()}] Unified display loop uncaught: ${e.message}`);
|
|
1318
1259
|
});
|
|
1319
1260
|
}, 3000);
|
|
1320
1261
|
|
|
1321
|
-
|
|
1262
|
+
setUnifiedDisplayLoopHandle(interval);
|
|
1263
|
+
console.log(`[${ts()}] [DISPLAY] Unified display loop started`);
|
|
1264
|
+
}
|
|
1265
|
+
|
|
1266
|
+
export function stopUnifiedDisplayLoop(): void {
|
|
1267
|
+
if (unifiedDisplayLoopHandle !== null) {
|
|
1268
|
+
clearInterval(unifiedDisplayLoopHandle);
|
|
1269
|
+
setUnifiedDisplayLoopHandle(null);
|
|
1270
|
+
console.log(`[${ts()}] [DISPLAY] Unified display loop stopped`);
|
|
1271
|
+
}
|
|
1322
1272
|
}
|
|
1323
1273
|
|
|
1324
1274
|
// ---------------------------------------------------------------------------
|