chatccc 0.2.170 → 0.2.172
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__/feishu-api.test.ts +60 -0
- package/src/__tests__/orchestrator.test.ts +21 -1
- package/src/__tests__/session.test.ts +113 -0
- package/src/__tests__/stop-session.test.ts +16 -2
- package/src/feishu-api.ts +18 -1
- package/src/orchestrator.ts +1 -2
- package/src/session.ts +60 -33
package/package.json
CHANGED
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import { afterEach, describe, expect, it, vi } from "vitest";
|
|
2
|
+
|
|
3
|
+
import { sendTextReply } from "../feishu-api.ts";
|
|
4
|
+
|
|
5
|
+
function abortError(): Error {
|
|
6
|
+
const err = new Error("aborted");
|
|
7
|
+
err.name = "AbortError";
|
|
8
|
+
return err;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
describe("sendTextReply timeout", () => {
|
|
12
|
+
afterEach(() => {
|
|
13
|
+
vi.useRealTimers();
|
|
14
|
+
vi.restoreAllMocks();
|
|
15
|
+
vi.unstubAllGlobals();
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
it("aborts a hung text send request", async () => {
|
|
19
|
+
vi.useFakeTimers();
|
|
20
|
+
vi.spyOn(console, "error").mockImplementation(() => {});
|
|
21
|
+
|
|
22
|
+
const fetchMock = vi.fn((_url: Parameters<typeof fetch>[0], init?: Parameters<typeof fetch>[1]) => (
|
|
23
|
+
new Promise<Response>((_resolve, reject) => {
|
|
24
|
+
init?.signal?.addEventListener("abort", () => reject(abortError()));
|
|
25
|
+
})
|
|
26
|
+
));
|
|
27
|
+
vi.stubGlobal("fetch", fetchMock);
|
|
28
|
+
|
|
29
|
+
const send = expect(sendTextReply("token", "chat-1", "hello"))
|
|
30
|
+
.resolves.toBe(false);
|
|
31
|
+
await vi.advanceTimersByTimeAsync(15_000);
|
|
32
|
+
|
|
33
|
+
await send;
|
|
34
|
+
expect(fetchMock).toHaveBeenCalledWith(
|
|
35
|
+
expect.stringContaining("/im/v1/messages?receive_id_type=chat_id"),
|
|
36
|
+
expect.objectContaining({ signal: expect.any(AbortSignal) }),
|
|
37
|
+
);
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
it("also aborts when response body reading hangs", async () => {
|
|
41
|
+
vi.useFakeTimers();
|
|
42
|
+
vi.spyOn(console, "error").mockImplementation(() => {});
|
|
43
|
+
|
|
44
|
+
const fetchMock = vi.fn((_url: Parameters<typeof fetch>[0], init?: Parameters<typeof fetch>[1]) => (
|
|
45
|
+
Promise.resolve({
|
|
46
|
+
status: 200,
|
|
47
|
+
text: () => new Promise<string>((_resolve, reject) => {
|
|
48
|
+
init?.signal?.addEventListener("abort", () => reject(abortError()));
|
|
49
|
+
}),
|
|
50
|
+
} as Response)
|
|
51
|
+
));
|
|
52
|
+
vi.stubGlobal("fetch", fetchMock);
|
|
53
|
+
|
|
54
|
+
const send = expect(sendTextReply("token", "chat-2", "hello"))
|
|
55
|
+
.resolves.toBe(false);
|
|
56
|
+
await vi.advanceTimersByTimeAsync(15_000);
|
|
57
|
+
|
|
58
|
+
await send;
|
|
59
|
+
});
|
|
60
|
+
});
|
|
@@ -169,6 +169,26 @@ describe("handleCommand WeChat processing ack", () => {
|
|
|
169
169
|
expect(platform.sendText).toHaveBeenCalledWith("wx-chat", "生成中...");
|
|
170
170
|
});
|
|
171
171
|
|
|
172
|
+
it("does not send the stopped success text until the running prompt really exits", async () => {
|
|
173
|
+
const platform = mockPlatform();
|
|
174
|
+
await recordSessionRegistry({
|
|
175
|
+
chatId: "wx-chat",
|
|
176
|
+
sessionId: "sid-wechat",
|
|
177
|
+
tool: "claude",
|
|
178
|
+
chatName: "busy-session",
|
|
179
|
+
running: true,
|
|
180
|
+
});
|
|
181
|
+
activePrompts.set("sid-wechat", {
|
|
182
|
+
controller: new AbortController(),
|
|
183
|
+
stopped: false,
|
|
184
|
+
startTime: Date.now(),
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
await handleCommand(platform, "/stop", "wx-chat", "wx-user", Date.now(), "p2p");
|
|
188
|
+
|
|
189
|
+
expect(platform.sendText).not.toHaveBeenCalledWith("wx-chat", "会话已停止。");
|
|
190
|
+
});
|
|
191
|
+
|
|
172
192
|
it("cleans stale Feishu p2p binding, creates a group, and sends the private message as first prompt", async () => {
|
|
173
193
|
const platform = mockPlatform("feishu");
|
|
174
194
|
const prompt = vi.fn(async function* (_sessionId: string, userText: string) {
|
|
@@ -177,7 +197,7 @@ describe("handleCommand WeChat processing ack", () => {
|
|
|
177
197
|
blocks: [{ type: "text" as const, text: `收到: ${userText}` }],
|
|
178
198
|
};
|
|
179
199
|
});
|
|
180
|
-
_setAdapterForToolForTest("claude", {
|
|
200
|
+
_setAdapterForToolForTest("claude", {
|
|
181
201
|
displayName: "Claude",
|
|
182
202
|
sessionDescPrefix: "Claude Session:",
|
|
183
203
|
createSession: vi.fn(async () => ({ sessionId: "sid-feishu-new" })),
|
|
@@ -88,6 +88,7 @@ import {
|
|
|
88
88
|
recordChatPlatform,
|
|
89
89
|
_getPlatformForChatForTest,
|
|
90
90
|
runAgentSession,
|
|
91
|
+
stopSession,
|
|
91
92
|
startUnifiedDisplayLoop,
|
|
92
93
|
stopUnifiedDisplayLoop,
|
|
93
94
|
_setProcessAliveForTest,
|
|
@@ -396,6 +397,66 @@ describe("runAgentSession process monitor", () => {
|
|
|
396
397
|
expect.stringContaining("进程异常结束"),
|
|
397
398
|
);
|
|
398
399
|
});
|
|
400
|
+
|
|
401
|
+
it("sends the stopped notice only after the prompt generator exits", async () => {
|
|
402
|
+
const platform = mockPlatform("feishu");
|
|
403
|
+
setSessionPlatform(platform);
|
|
404
|
+
bindChatToSession("sid-stop-notice", "chat-stop-notice");
|
|
405
|
+
recordLastActiveChat("sid-stop-notice", "chat-stop-notice");
|
|
406
|
+
|
|
407
|
+
const closeSession = vi.fn();
|
|
408
|
+
let releasePrompt: (() => void) | undefined;
|
|
409
|
+
const adapter: ToolAdapter = {
|
|
410
|
+
displayName: "Claude Code",
|
|
411
|
+
sessionDescPrefix: "Claude Code Session:",
|
|
412
|
+
createSession: async () => ({ sessionId: "sid-stop-notice" }),
|
|
413
|
+
getSessionInfo: async (sid) => ({ sessionId: sid, cwd: "F:\\repo" }),
|
|
414
|
+
closeSession: async () => {},
|
|
415
|
+
prompt: async function* (
|
|
416
|
+
_sid: string,
|
|
417
|
+
_text: string,
|
|
418
|
+
_cwd: string,
|
|
419
|
+
signal?: AbortSignal,
|
|
420
|
+
options?: ToolPromptOptions,
|
|
421
|
+
) {
|
|
422
|
+
const waitForStop = new Promise<void>((resolve) => {
|
|
423
|
+
releasePrompt = resolve;
|
|
424
|
+
signal?.addEventListener("abort", () => resolve(), { once: true });
|
|
425
|
+
});
|
|
426
|
+
options?.onSessionCreated?.(() => {
|
|
427
|
+
closeSession();
|
|
428
|
+
releasePrompt?.();
|
|
429
|
+
});
|
|
430
|
+
yield { type: "assistant", blocks: [{ type: "text", text: "partial answer" }] };
|
|
431
|
+
await waitForStop;
|
|
432
|
+
},
|
|
433
|
+
};
|
|
434
|
+
_setAdapterForToolForTest("claude", adapter);
|
|
435
|
+
|
|
436
|
+
const runPromise = runAgentSession(
|
|
437
|
+
"sid-stop-notice",
|
|
438
|
+
"prompt",
|
|
439
|
+
platform,
|
|
440
|
+
"chat-stop-notice",
|
|
441
|
+
Date.now(),
|
|
442
|
+
"claude",
|
|
443
|
+
);
|
|
444
|
+
|
|
445
|
+
await vi.waitFor(() => {
|
|
446
|
+
expect(activePrompts.get("sid-stop-notice")?.closeSession).toBeTypeOf("function");
|
|
447
|
+
});
|
|
448
|
+
expect(platform.sendText).not.toHaveBeenCalledWith("chat-stop-notice", "会话已停止。");
|
|
449
|
+
|
|
450
|
+
const ok = stopSession("sid-stop-notice");
|
|
451
|
+
expect(ok).toBe(true);
|
|
452
|
+
expect(closeSession).toHaveBeenCalledTimes(1);
|
|
453
|
+
|
|
454
|
+
await runPromise;
|
|
455
|
+
|
|
456
|
+
expect(platform.sendText).toHaveBeenCalledWith("chat-stop-notice", "会话已停止。");
|
|
457
|
+
expect(activePrompts.has("sid-stop-notice")).toBe(false);
|
|
458
|
+
});
|
|
459
|
+
|
|
399
460
|
it("re-injects IM skill capabilities for each resumed Claude prompt", async () => {
|
|
400
461
|
const platform = mockPlatform("feishu");
|
|
401
462
|
setSessionPlatform(platform);
|
|
@@ -569,6 +630,58 @@ describe("unified display loop terminal card update", () => {
|
|
|
569
630
|
);
|
|
570
631
|
expect(displayCards.get("chat-terminal")?.sequence).toBe(110);
|
|
571
632
|
});
|
|
633
|
+
|
|
634
|
+
it("keeps terminal display and retries final text when sending fails", async () => {
|
|
635
|
+
vi.spyOn(console, "error").mockImplementation(() => {});
|
|
636
|
+
const platform = mockPlatform("feishu");
|
|
637
|
+
platform.cardUpdate = vi.fn(async () => {});
|
|
638
|
+
platform.sendText = vi.fn()
|
|
639
|
+
.mockResolvedValueOnce(false)
|
|
640
|
+
.mockResolvedValueOnce(true);
|
|
641
|
+
setSessionPlatform(platform);
|
|
642
|
+
|
|
643
|
+
bindChatToSession("sid-terminal-retry", "chat-terminal-retry");
|
|
644
|
+
recordLastActiveChat("sid-terminal-retry", "chat-terminal-retry");
|
|
645
|
+
sessionInfoMap.set("chat-terminal-retry", {
|
|
646
|
+
sessionId: "sid-terminal-retry",
|
|
647
|
+
turnCount: 1,
|
|
648
|
+
lastContextTokens: 0,
|
|
649
|
+
startTime: 0,
|
|
650
|
+
tool: "claude",
|
|
651
|
+
});
|
|
652
|
+
displayCards.set("chat-terminal-retry", {
|
|
653
|
+
cardId: "card-terminal-retry",
|
|
654
|
+
sequence: 4,
|
|
655
|
+
cardBusy: false,
|
|
656
|
+
cardCreatedAt: Date.now(),
|
|
657
|
+
lastSentContent: "",
|
|
658
|
+
streamErrorNotified: false,
|
|
659
|
+
sessionId: "sid-terminal-retry",
|
|
660
|
+
turnCount: 1,
|
|
661
|
+
dotCount: 0,
|
|
662
|
+
});
|
|
663
|
+
mockStreamStates.set("sid-terminal-retry", {
|
|
664
|
+
accumulatedContent: "work log",
|
|
665
|
+
finalReply: "final answer",
|
|
666
|
+
status: "done",
|
|
667
|
+
turnCount: 1,
|
|
668
|
+
});
|
|
669
|
+
|
|
670
|
+
startUnifiedDisplayLoop();
|
|
671
|
+
await vi.advanceTimersByTimeAsync(3000);
|
|
672
|
+
|
|
673
|
+
expect(platform.cardUpdate).toHaveBeenCalledTimes(1);
|
|
674
|
+
expect(platform.sendText).toHaveBeenCalledTimes(1);
|
|
675
|
+
expect(displayCards.has("chat-terminal-retry")).toBe(true);
|
|
676
|
+
expect(mockStreamStates.get("sid-terminal-retry")?.finalReplySentTurn).toBeUndefined();
|
|
677
|
+
|
|
678
|
+
await vi.advanceTimersByTimeAsync(3000);
|
|
679
|
+
|
|
680
|
+
expect(platform.cardUpdate).toHaveBeenCalledTimes(1);
|
|
681
|
+
expect(platform.sendText).toHaveBeenCalledTimes(2);
|
|
682
|
+
expect(displayCards.has("chat-terminal-retry")).toBe(false);
|
|
683
|
+
expect(mockStreamStates.get("sid-terminal-retry")?.finalReplySentTurn).toBe(1);
|
|
684
|
+
});
|
|
572
685
|
});
|
|
573
686
|
|
|
574
687
|
describe("rebuildBindingsFromRegistry", () => {
|
|
@@ -41,9 +41,13 @@ vi.mock("../stream-state.ts", () => ({
|
|
|
41
41
|
import { stopSession } from "../session.ts";
|
|
42
42
|
import { activePrompts } from "../session-chat-binding.ts";
|
|
43
43
|
|
|
44
|
-
function seedRunningSession(
|
|
44
|
+
function seedRunningSession(
|
|
45
|
+
sid: string,
|
|
46
|
+
accumulated = "partial output",
|
|
47
|
+
closeSession?: () => void,
|
|
48
|
+
): AbortController {
|
|
45
49
|
const controller = new AbortController();
|
|
46
|
-
activePrompts.set(sid, { controller, stopped: false, startTime: Date.now() });
|
|
50
|
+
activePrompts.set(sid, { controller, stopped: false, startTime: Date.now(), closeSession });
|
|
47
51
|
stateStore.set(sid, {
|
|
48
52
|
sessionId: sid,
|
|
49
53
|
status: "running",
|
|
@@ -123,4 +127,14 @@ describe("stopSession 行为护栏", () => {
|
|
|
123
127
|
stopSession("sid-C");
|
|
124
128
|
expect(activePrompts.get("sid-C")?.stopped).toBe(true);
|
|
125
129
|
});
|
|
130
|
+
|
|
131
|
+
it("调用 adapter 提供的 closeSession 以主动关闭底层 SDK session", () => {
|
|
132
|
+
const closeSession = vi.fn();
|
|
133
|
+
seedRunningSession("sid-D", "partial output", closeSession);
|
|
134
|
+
|
|
135
|
+
const ok = stopSession("sid-D");
|
|
136
|
+
|
|
137
|
+
expect(ok).toBe(true);
|
|
138
|
+
expect(closeSession).toHaveBeenCalledTimes(1);
|
|
139
|
+
});
|
|
126
140
|
});
|
package/src/feishu-api.ts
CHANGED
|
@@ -757,6 +757,9 @@ export async function sendTextReply(
|
|
|
757
757
|
config: { wide_screen_mode: true },
|
|
758
758
|
elements: [{ tag: "markdown", content: wrappedText }],
|
|
759
759
|
});
|
|
760
|
+
const timeoutMs = 15_000;
|
|
761
|
+
const controller = new AbortController();
|
|
762
|
+
const timer = setTimeout(() => controller.abort(), timeoutMs);
|
|
760
763
|
try {
|
|
761
764
|
const resp = await fetch(`${BASE_URL}/im/v1/messages?receive_id_type=chat_id`, {
|
|
762
765
|
method: "POST",
|
|
@@ -769,8 +772,16 @@ export async function sendTextReply(
|
|
|
769
772
|
msg_type: "interactive",
|
|
770
773
|
content: card,
|
|
771
774
|
}),
|
|
775
|
+
signal: controller.signal,
|
|
772
776
|
});
|
|
773
|
-
const
|
|
777
|
+
const respText = await resp.text();
|
|
778
|
+
let data: { code: number; msg?: string; data?: { message_id?: string } };
|
|
779
|
+
try {
|
|
780
|
+
data = JSON.parse(respText);
|
|
781
|
+
} catch {
|
|
782
|
+
console.error(`[${ts()}] [SEND] text FAIL: chatId=${chatId} invalid JSON status=${resp.status}`);
|
|
783
|
+
return false;
|
|
784
|
+
}
|
|
774
785
|
if (data.code !== 0) {
|
|
775
786
|
console.error(`[${ts()}] [SEND] text FAIL: chatId=${chatId} code=${data.code} msg="${data.msg ?? ""}"`);
|
|
776
787
|
return false;
|
|
@@ -778,8 +789,14 @@ export async function sendTextReply(
|
|
|
778
789
|
console.log(`[${ts()}] [SEND] text OK: chatId=${chatId} msgId=${data.data?.message_id ?? "N/A"}`);
|
|
779
790
|
return true;
|
|
780
791
|
} catch (err) {
|
|
792
|
+
if ((err as Error).name === "AbortError") {
|
|
793
|
+
console.error(`[${ts()}] [SEND] text TIMEOUT after ${timeoutMs}ms: chatId=${chatId}`);
|
|
794
|
+
return false;
|
|
795
|
+
}
|
|
781
796
|
console.error(`[${ts()}] [SEND] text FAIL: chatId=${chatId} ${(err as Error).message}`);
|
|
782
797
|
return false;
|
|
798
|
+
} finally {
|
|
799
|
+
clearTimeout(timer);
|
|
783
800
|
}
|
|
784
801
|
}
|
|
785
802
|
|
package/src/orchestrator.ts
CHANGED
|
@@ -778,8 +778,7 @@ export async function handleCommand(
|
|
|
778
778
|
logTrace(tid, "BRANCH", { cmd: "/stop" });
|
|
779
779
|
if (stopSession(sessionId)) {
|
|
780
780
|
console.log(`[${ts()}] [STOP] User sent /stop, session=${sessionId}`);
|
|
781
|
-
|
|
782
|
-
logTrace(tid, "DONE", { outcome: "stopped" });
|
|
781
|
+
logTrace(tid, "DONE", { outcome: "stop_requested" });
|
|
783
782
|
} else {
|
|
784
783
|
await platform
|
|
785
784
|
.sendText(chatId, "当前没有正在进行的会话。")
|
package/src/session.ts
CHANGED
|
@@ -1247,7 +1247,10 @@ export async function runAgentSession(
|
|
|
1247
1247
|
});
|
|
1248
1248
|
}
|
|
1249
1249
|
const active1 = getLastActiveChat(sessionId) ?? getChatsForSession(sessionId)[0];
|
|
1250
|
-
if (active1)
|
|
1250
|
+
if (active1) {
|
|
1251
|
+
await platform.sendText(active1, "会话已停止。").catch(() => {});
|
|
1252
|
+
platform.setChatAvatar(active1, tool, "idle").catch(() => {});
|
|
1253
|
+
}
|
|
1251
1254
|
console.log(`[${ts()}] Session ${sessionId} stopped (content chunks: ${state.chunkCount})`);
|
|
1252
1255
|
if (tid) logTrace(tid, "SESSION_END", { sessionId, outcome: "stopped", chunks: state.chunkCount });
|
|
1253
1256
|
} else if (wasAbnormalExit) {
|
|
@@ -1300,17 +1303,21 @@ const CARD_ROTATE_MS = 9 * 60 * 1000;
|
|
|
1300
1303
|
export function startUnifiedDisplayLoop(): void {
|
|
1301
1304
|
if (unifiedDisplayLoopHandle !== null) return;
|
|
1302
1305
|
|
|
1306
|
+
let tickRunning = false;
|
|
1303
1307
|
const interval = setInterval(() => {
|
|
1304
1308
|
void (async () => {
|
|
1305
|
-
|
|
1306
|
-
|
|
1307
|
-
|
|
1308
|
-
const
|
|
1309
|
-
|
|
1310
|
-
|
|
1311
|
-
|
|
1312
|
-
|
|
1313
|
-
|
|
1309
|
+
if (tickRunning) return;
|
|
1310
|
+
tickRunning = true;
|
|
1311
|
+
try {
|
|
1312
|
+
for (const [chatId, display] of displayCards) {
|
|
1313
|
+
if (display.cardBusy) continue;
|
|
1314
|
+
|
|
1315
|
+
const sessionId = display.sessionId;
|
|
1316
|
+
const state = await readStreamState(sessionId);
|
|
1317
|
+
if (!state) {
|
|
1318
|
+
displayCards.delete(chatId);
|
|
1319
|
+
continue;
|
|
1320
|
+
}
|
|
1314
1321
|
|
|
1315
1322
|
// 交叉验证:chat 当前绑定的 session 是否仍是 display 记录的 session。
|
|
1316
1323
|
// 若 chat 已被切换到其他 session(如 /newh),旧 display 必须停推。
|
|
@@ -1375,45 +1382,57 @@ export function startUnifiedDisplayLoop(): void {
|
|
|
1375
1382
|
) {
|
|
1376
1383
|
continue;
|
|
1377
1384
|
}
|
|
1378
|
-
const
|
|
1379
|
-
|
|
1380
|
-
|
|
1381
|
-
|
|
1382
|
-
|
|
1383
|
-
|
|
1384
|
-
|
|
1385
|
-
|
|
1386
|
-
|
|
1387
|
-
|
|
1388
|
-
if (isCardKitSequenceConflict(err)) {
|
|
1385
|
+
const terminalCardAlreadyUpdated =
|
|
1386
|
+
display.lastSentAccLen === state.accumulatedContent.length &&
|
|
1387
|
+
display.lastSentFinalReply === state.finalReply;
|
|
1388
|
+
let terminalCardUpdateAccepted = terminalCardAlreadyUpdated;
|
|
1389
|
+
if (!terminalCardAlreadyUpdated) {
|
|
1390
|
+
const nextSeq = display.sequence + 1;
|
|
1391
|
+
const { title: headerTitle, template: headerTemplate } = formatTerminalHeader(state.status);
|
|
1392
|
+
const cardContent = truncateContent(state.accumulatedContent + state.finalReply) || " ";
|
|
1393
|
+
const doneCard = buildProgressCard(cardContent, { showStop: false, headerTitle, headerTemplate });
|
|
1394
|
+
await p.cardUpdate(display.cardId, doneCard, nextSeq).then(() => {
|
|
1389
1395
|
display.sequence = nextSeq;
|
|
1390
1396
|
terminalCardUpdateAccepted = true;
|
|
1397
|
+
}).catch(err => {
|
|
1398
|
+
console.error(`[${ts()}] [DISPLAY] terminal cardUpdate failed: ${(err as Error).message}`);
|
|
1399
|
+
if (isCardKitSequenceConflict(err)) {
|
|
1400
|
+
display.sequence = nextSeq;
|
|
1401
|
+
terminalCardUpdateAccepted = true;
|
|
1402
|
+
}
|
|
1403
|
+
});
|
|
1404
|
+
if (terminalCardUpdateAccepted) {
|
|
1405
|
+
display.lastSentAccLen = state.accumulatedContent.length;
|
|
1406
|
+
display.lastSentFinalReply = state.finalReply;
|
|
1391
1407
|
}
|
|
1392
|
-
}
|
|
1408
|
+
}
|
|
1393
1409
|
|
|
1394
1410
|
// 若 session 仍在 activePrompts 中,说明 runAgentSession 的 finally
|
|
1395
1411
|
// 还没执行,当前 stream state 可能是 stopSession fire-and-forget
|
|
1396
1412
|
// 写入的,finalReply 滞后于内存态。卡片已更新为终态外观,但不发送
|
|
1397
1413
|
// 文本、不删除 display 条目,留给 finally 落盘后的下一次 tick 处理。
|
|
1398
1414
|
if (promptStillActive) {
|
|
1399
|
-
if (terminalCardUpdateAccepted) {
|
|
1400
|
-
display.lastSentAccLen = state.accumulatedContent.length;
|
|
1401
|
-
display.lastSentFinalReply = state.finalReply;
|
|
1402
|
-
}
|
|
1403
1415
|
continue;
|
|
1404
1416
|
}
|
|
1405
1417
|
|
|
1406
|
-
|
|
1407
|
-
finalizeTurnCards(sessionId, state.turnCount, finalSt).catch(() => {});
|
|
1408
|
-
displayCards.delete(chatId);
|
|
1418
|
+
let terminalTextDelivered = true;
|
|
1409
1419
|
if (state.finalReply) {
|
|
1410
1420
|
if (!isFinalReplySentForTurn(state)) {
|
|
1411
|
-
await sendFinalReplyTextOnce(p, chatId, sessionId, state.turnCount, state.finalReply);
|
|
1421
|
+
terminalTextDelivered = await sendFinalReplyTextOnce(p, chatId, sessionId, state.turnCount, state.finalReply);
|
|
1412
1422
|
}
|
|
1413
1423
|
} else if (state.accumulatedContent.trim()) {
|
|
1414
1424
|
const short = truncateContent(state.accumulatedContent, 30, 4000);
|
|
1415
|
-
await p.sendText(chatId, `[生成过程]\n${short}`).catch(() =>
|
|
1425
|
+
terminalTextDelivered = await p.sendText(chatId, `[生成过程]\n${short}`).then((ok) => ok !== false).catch(() => false);
|
|
1416
1426
|
}
|
|
1427
|
+
|
|
1428
|
+
if (!terminalTextDelivered) {
|
|
1429
|
+
console.error(`[${ts()}] [DISPLAY] terminal text send failed, keep display for retry: chatId=${chatId} session=${sessionId} turn=${state.turnCount}`);
|
|
1430
|
+
continue;
|
|
1431
|
+
}
|
|
1432
|
+
|
|
1433
|
+
const finalSt = turnFinalStatus(state.status);
|
|
1434
|
+
finalizeTurnCards(sessionId, state.turnCount, finalSt).catch(() => {});
|
|
1435
|
+
displayCards.delete(chatId);
|
|
1417
1436
|
}
|
|
1418
1437
|
p.setChatAvatar(chatId, state.tool, "idle").catch(() => {});
|
|
1419
1438
|
console.log(`[${ts()}] [DISPLAY] unified loop deleted display for ${chatId} (terminal: ${state.status})`);
|
|
@@ -1561,9 +1580,12 @@ export function startUnifiedDisplayLoop(): void {
|
|
|
1561
1580
|
}
|
|
1562
1581
|
}
|
|
1563
1582
|
}
|
|
1564
|
-
|
|
1565
|
-
|
|
1583
|
+
} catch (err) {
|
|
1584
|
+
console.error(`[${ts()}] Display loop error for ${chatId}: ${(err as Error).message}`);
|
|
1585
|
+
}
|
|
1566
1586
|
}
|
|
1587
|
+
} finally {
|
|
1588
|
+
tickRunning = false;
|
|
1567
1589
|
}
|
|
1568
1590
|
})().catch((err: unknown) => {
|
|
1569
1591
|
const e = err instanceof Error ? err : new Error(String(err));
|
|
@@ -1604,6 +1626,11 @@ export function stopSession(sessionId: string): boolean {
|
|
|
1604
1626
|
prompt.stopped = true;
|
|
1605
1627
|
clearPromptProcessMonitor(sessionId);
|
|
1606
1628
|
cancelQueuedMessage(sessionId);
|
|
1629
|
+
try {
|
|
1630
|
+
prompt.closeSession?.();
|
|
1631
|
+
} catch (err) {
|
|
1632
|
+
console.warn(`[${ts()}] [STOP] closeSession failed for ${sessionId}: ${(err as Error).message}`);
|
|
1633
|
+
}
|
|
1607
1634
|
prompt.controller.abort();
|
|
1608
1635
|
|
|
1609
1636
|
// 强制杀死 CLI 子进程。controller.abort() 只在 for-await 收到下一条
|