chatccc 0.2.172 → 0.2.173
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 +73 -0
- package/src/session.ts +66 -22
package/package.json
CHANGED
|
@@ -398,6 +398,79 @@ describe("runAgentSession process monitor", () => {
|
|
|
398
398
|
);
|
|
399
399
|
});
|
|
400
400
|
|
|
401
|
+
it("recreates the progress card when the initial CardKit send fails", async () => {
|
|
402
|
+
vi.spyOn(console, "error").mockImplementation(() => {});
|
|
403
|
+
const platform = mockPlatform("feishu");
|
|
404
|
+
platform.cardCreate = vi.fn()
|
|
405
|
+
.mockResolvedValueOnce("bad-card")
|
|
406
|
+
.mockResolvedValueOnce("good-card");
|
|
407
|
+
platform.cardSend = vi.fn()
|
|
408
|
+
.mockRejectedValueOnce(new Error("cardid is invalid"))
|
|
409
|
+
.mockResolvedValueOnce("message-good");
|
|
410
|
+
setSessionPlatform(platform);
|
|
411
|
+
bindChatToSession("sid-card-retry", "chat-card-retry");
|
|
412
|
+
recordLastActiveChat("sid-card-retry", "chat-card-retry");
|
|
413
|
+
|
|
414
|
+
const adapter: ToolAdapter = {
|
|
415
|
+
displayName: "Claude Code",
|
|
416
|
+
sessionDescPrefix: "Claude Code Session:",
|
|
417
|
+
createSession: async () => ({ sessionId: "sid-card-retry" }),
|
|
418
|
+
getSessionInfo: async (sid) => ({ sessionId: sid, cwd: "F:\\repo" }),
|
|
419
|
+
closeSession: async () => {},
|
|
420
|
+
prompt: async function* () {
|
|
421
|
+
yield { type: "assistant", blocks: [{ type: "text", text: "done" }] };
|
|
422
|
+
},
|
|
423
|
+
};
|
|
424
|
+
_setAdapterForToolForTest("claude", adapter);
|
|
425
|
+
|
|
426
|
+
await runAgentSession("sid-card-retry", "prompt", platform, "chat-card-retry", Date.now(), "claude");
|
|
427
|
+
|
|
428
|
+
expect(platform.cardCreate).toHaveBeenCalledTimes(2);
|
|
429
|
+
expect(platform.cardSend).toHaveBeenNthCalledWith(1, "chat-card-retry", "bad-card");
|
|
430
|
+
expect(platform.cardSend).toHaveBeenNthCalledWith(2, "chat-card-retry", "good-card");
|
|
431
|
+
expect(displayCards.get("chat-card-retry")?.cardId).toBe("good-card");
|
|
432
|
+
expect(platform.sendText).not.toHaveBeenCalledWith(
|
|
433
|
+
"chat-card-retry",
|
|
434
|
+
"生成中卡片发送失败,结果将以文本形式发送。",
|
|
435
|
+
);
|
|
436
|
+
});
|
|
437
|
+
|
|
438
|
+
it("does not register an invisible progress card and sends the final text fallback", async () => {
|
|
439
|
+
vi.spyOn(console, "error").mockImplementation(() => {});
|
|
440
|
+
const platform = mockPlatform("feishu");
|
|
441
|
+
platform.cardCreate = vi.fn()
|
|
442
|
+
.mockResolvedValueOnce("bad-card-1")
|
|
443
|
+
.mockResolvedValueOnce("bad-card-2");
|
|
444
|
+
platform.cardSend = vi.fn()
|
|
445
|
+
.mockRejectedValueOnce(new Error("cardid is invalid"))
|
|
446
|
+
.mockRejectedValueOnce(new Error("cardid is invalid again"));
|
|
447
|
+
setSessionPlatform(platform);
|
|
448
|
+
bindChatToSession("sid-card-fallback", "chat-card-fallback");
|
|
449
|
+
recordLastActiveChat("sid-card-fallback", "chat-card-fallback");
|
|
450
|
+
|
|
451
|
+
const adapter: ToolAdapter = {
|
|
452
|
+
displayName: "Claude Code",
|
|
453
|
+
sessionDescPrefix: "Claude Code Session:",
|
|
454
|
+
createSession: async () => ({ sessionId: "sid-card-fallback" }),
|
|
455
|
+
getSessionInfo: async (sid) => ({ sessionId: sid, cwd: "F:\\repo" }),
|
|
456
|
+
closeSession: async () => {},
|
|
457
|
+
prompt: async function* () {
|
|
458
|
+
yield { type: "assistant", blocks: [{ type: "text", text: "final answer" }] };
|
|
459
|
+
},
|
|
460
|
+
};
|
|
461
|
+
_setAdapterForToolForTest("claude", adapter);
|
|
462
|
+
|
|
463
|
+
await runAgentSession("sid-card-fallback", "prompt", platform, "chat-card-fallback", Date.now(), "claude");
|
|
464
|
+
|
|
465
|
+
expect(displayCards.has("chat-card-fallback")).toBe(false);
|
|
466
|
+
expect(platform.sendText).toHaveBeenCalledWith(
|
|
467
|
+
"chat-card-fallback",
|
|
468
|
+
"生成中卡片发送失败,结果将以文本形式发送。",
|
|
469
|
+
);
|
|
470
|
+
expect(platform.sendText).toHaveBeenCalledWith("chat-card-fallback", "final answer");
|
|
471
|
+
expect(mockStreamStates.get("sid-card-fallback")?.finalReplySentTurn).toBe(1);
|
|
472
|
+
});
|
|
473
|
+
|
|
401
474
|
it("sends the stopped notice only after the prompt generator exits", async () => {
|
|
402
475
|
const platform = mockPlatform("feishu");
|
|
403
476
|
setSessionPlatform(platform);
|
package/src/session.ts
CHANGED
|
@@ -81,6 +81,36 @@ async function sendFinalReplyTextOnce(
|
|
|
81
81
|
return sent;
|
|
82
82
|
}
|
|
83
83
|
|
|
84
|
+
async function createVisibleProgressCard(
|
|
85
|
+
platform: PlatformAdapter,
|
|
86
|
+
chatId: string,
|
|
87
|
+
sessionId: string,
|
|
88
|
+
turnCount: number,
|
|
89
|
+
notifyFailureText?: string,
|
|
90
|
+
): Promise<string | null> {
|
|
91
|
+
for (let attempt = 1; attempt <= 2; attempt++) {
|
|
92
|
+
let cardId: string | null = null;
|
|
93
|
+
try {
|
|
94
|
+
cardId = await platform.cardCreate(
|
|
95
|
+
buildProgressCard("", { showStop: true, headerTitle: "生成中..." }),
|
|
96
|
+
);
|
|
97
|
+
if (!cardId) throw new Error("empty card id");
|
|
98
|
+
await platform.cardSend(chatId, cardId);
|
|
99
|
+
await addCardToTurn(sessionId, turnCount, cardId);
|
|
100
|
+
return cardId;
|
|
101
|
+
} catch (err) {
|
|
102
|
+
console.error(
|
|
103
|
+
`[${ts()}] [DISPLAY] progress card send attempt ${attempt} failed: chatId=${chatId} cardId=${cardId || "(none)"} ${(err as Error).message}`,
|
|
104
|
+
);
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
if (notifyFailureText) {
|
|
109
|
+
await platform.sendText(chatId, notifyFailureText).catch(() => {});
|
|
110
|
+
}
|
|
111
|
+
return null;
|
|
112
|
+
}
|
|
113
|
+
|
|
84
114
|
// ---------------------------------------------------------------------------
|
|
85
115
|
// Shared state (imported by index.ts)
|
|
86
116
|
// ---------------------------------------------------------------------------
|
|
@@ -1053,11 +1083,14 @@ export async function runAgentSession(
|
|
|
1053
1083
|
if (displayChatIdForNew) {
|
|
1054
1084
|
const ppNew = platformForChat(displayChatIdForNew);
|
|
1055
1085
|
if (ppNew && ppNew.kind !== "wechat") {
|
|
1056
|
-
const cardId = await
|
|
1057
|
-
|
|
1058
|
-
|
|
1086
|
+
const cardId = await createVisibleProgressCard(
|
|
1087
|
+
ppNew,
|
|
1088
|
+
displayChatIdForNew,
|
|
1089
|
+
sessionId,
|
|
1090
|
+
nextTurnCount,
|
|
1091
|
+
"生成中卡片发送失败,结果将以文本形式发送。",
|
|
1092
|
+
);
|
|
1059
1093
|
if (cardId) {
|
|
1060
|
-
await ppNew.cardSend(displayChatIdForNew, cardId).catch(() => null);
|
|
1061
1094
|
displayCards.set(displayChatIdForNew, {
|
|
1062
1095
|
cardId,
|
|
1063
1096
|
sequence: 1,
|
|
@@ -1069,7 +1102,6 @@ export async function runAgentSession(
|
|
|
1069
1102
|
turnCount: nextTurnCount,
|
|
1070
1103
|
dotCount: 0,
|
|
1071
1104
|
});
|
|
1072
|
-
addCardToTurn(sessionId, nextTurnCount, cardId).catch(() => {});
|
|
1073
1105
|
}
|
|
1074
1106
|
} else if (ppNew && ppNew.kind === "wechat") {
|
|
1075
1107
|
// WeChat: 无卡片,但需要 display entry 追踪已发送内容
|
|
@@ -1284,7 +1316,14 @@ export async function runAgentSession(
|
|
|
1284
1316
|
});
|
|
1285
1317
|
}
|
|
1286
1318
|
const active2 = getLastActiveChat(sessionId) ?? getChatsForSession(sessionId)[0];
|
|
1287
|
-
if (active2)
|
|
1319
|
+
if (active2) {
|
|
1320
|
+
const terminalState = await readStreamState(sessionId);
|
|
1321
|
+
if (finalReply && !displayCards.has(active2) && (!terminalState || !isFinalReplySentForTurn(terminalState))) {
|
|
1322
|
+
const pp = platformForChat(active2) ?? platform;
|
|
1323
|
+
await sendFinalReplyTextOnce(pp, active2, sessionId, nextTurnCount, finalReply);
|
|
1324
|
+
}
|
|
1325
|
+
platform.setChatAvatar(active2, tool, "idle").catch(() => {});
|
|
1326
|
+
}
|
|
1288
1327
|
console.log(`[${ts()}] Session ${sessionId} stream complete (content chunks: ${state.chunkCount})`);
|
|
1289
1328
|
if (tid) logTrace(tid, "SESSION_END", { sessionId, chunks: state.chunkCount, finalTextLen: finalReply.length });
|
|
1290
1329
|
}
|
|
@@ -1482,28 +1521,33 @@ export function startUnifiedDisplayLoop(): void {
|
|
|
1482
1521
|
if (Date.now() - display.cardCreatedAt > CARD_ROTATE_MS) {
|
|
1483
1522
|
display.cardBusy = true;
|
|
1484
1523
|
try {
|
|
1524
|
+
const newCardId = await createVisibleProgressCard(
|
|
1525
|
+
p,
|
|
1526
|
+
chatId,
|
|
1527
|
+
sessionId,
|
|
1528
|
+
display.turnCount,
|
|
1529
|
+
display.streamErrorNotified ? undefined : "生成中卡片发送失败,结果将继续更新在上一张卡片中。",
|
|
1530
|
+
);
|
|
1531
|
+
if (!newCardId) {
|
|
1532
|
+
display.streamErrorNotified = true;
|
|
1533
|
+
continue;
|
|
1534
|
+
}
|
|
1485
1535
|
const oldSeqBase = display.sequence;
|
|
1486
1536
|
const oldContent = state.accumulatedContent + state.finalReply;
|
|
1487
1537
|
const oldCard = buildProgressCard(truncateContent(oldContent) || " ", { showStop: false, headerTitle: "生成中(上轮)" });
|
|
1488
|
-
await p.cardUpdate(display.cardId, oldCard, oldSeqBase + 1).
|
|
1538
|
+
await p.cardUpdate(display.cardId, oldCard, oldSeqBase + 1).then(() => {
|
|
1539
|
+
display.sequence = oldSeqBase + 1;
|
|
1540
|
+
}).catch(err => {
|
|
1489
1541
|
console.error(`[${ts()}] [DISPLAY] rotation old cardUpdate failed: ${(err as Error).message}`);
|
|
1490
1542
|
});
|
|
1491
1543
|
markCardDone(sessionId, display.turnCount, display.cardId).catch(() => {});
|
|
1492
|
-
|
|
1493
|
-
|
|
1494
|
-
|
|
1495
|
-
|
|
1496
|
-
|
|
1497
|
-
|
|
1498
|
-
|
|
1499
|
-
display.cardId = newCardId;
|
|
1500
|
-
display.sequence = 1;
|
|
1501
|
-
display.cardCreatedAt = Date.now();
|
|
1502
|
-
display.rotationAccLen = state.accumulatedContent.length;
|
|
1503
|
-
display.rotationFinalReply = state.finalReply;
|
|
1504
|
-
display.lastSentContent = "";
|
|
1505
|
-
display.streamErrorNotified = false;
|
|
1506
|
-
}
|
|
1544
|
+
display.cardId = newCardId;
|
|
1545
|
+
display.sequence = 1;
|
|
1546
|
+
display.cardCreatedAt = Date.now();
|
|
1547
|
+
display.rotationAccLen = state.accumulatedContent.length;
|
|
1548
|
+
display.rotationFinalReply = state.finalReply;
|
|
1549
|
+
display.lastSentContent = "";
|
|
1550
|
+
display.streamErrorNotified = false;
|
|
1507
1551
|
} catch (err) {
|
|
1508
1552
|
console.error(`[${ts()}] [CARDIKT] rotation FAIL for ${chatId}: ${(err as Error).message}`);
|
|
1509
1553
|
} finally {
|