chatccc 0.2.228 → 0.2.230
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__/builtin-chat-session.test.ts +329 -350
- package/src/__tests__/builtin-file-tools.test.ts +240 -275
- package/src/__tests__/builtin-skills.test.ts +284 -252
- package/src/__tests__/builtin-web-tools.test.ts +220 -220
- package/src/__tests__/restart.test.ts +132 -0
- package/src/__tests__/session.test.ts +163 -0
- package/src/__tests__/terminal-error.test.ts +54 -0
- package/src/builtin/context.ts +333 -323
- package/src/builtin/file-tools.ts +17 -2
- package/src/builtin/index.ts +12 -5
- package/src/builtin/skills.ts +205 -190
- package/src/builtin/web-tools.ts +313 -313
- package/src/index.ts +315 -310
- package/src/orchestrator.ts +2494 -2388
- package/src/session.ts +137 -15
- package/src/stream-state.ts +21 -18
- package/src/terminal-error.ts +129 -0
package/src/session.ts
CHANGED
|
@@ -43,6 +43,12 @@ import { resourceMonitor, registerProcess, unregisterProcess } from "./adapters/
|
|
|
43
43
|
import { buildImSkillsPromptCached, exportSkillSubDocs, clearImSkillsPromptCache } from "./im-skills.ts";
|
|
44
44
|
import type { PlatformAdapter } from "./platform-adapter.ts";
|
|
45
45
|
import { hasResponseStalled, observeResponseProgress } from "./response-stall.ts";
|
|
46
|
+
import {
|
|
47
|
+
classifyTerminalError,
|
|
48
|
+
formatTerminalErrorNotice,
|
|
49
|
+
formatTerminalErrorReason,
|
|
50
|
+
type TerminalErrorInfo,
|
|
51
|
+
} from "./terminal-error.ts";
|
|
46
52
|
import {
|
|
47
53
|
MAX_PROCESSED,
|
|
48
54
|
clearFeishuMessageLedgerMemory,
|
|
@@ -305,13 +311,18 @@ function scheduleFinalResponseCloseGuard(
|
|
|
305
311
|
runningPrompt.finalResponseCloseTimer = handle;
|
|
306
312
|
}
|
|
307
313
|
|
|
308
|
-
function formatTerminalHeader(
|
|
314
|
+
function formatTerminalHeader(
|
|
315
|
+
status: "running" | "done" | "stopped" | "error" | "auto_ended",
|
|
316
|
+
terminalError?: TerminalErrorInfo,
|
|
317
|
+
): {
|
|
309
318
|
title: string;
|
|
310
319
|
template?: string;
|
|
311
320
|
} {
|
|
312
321
|
if (status === "auto_ended") return { title: "已自动结束 · 3分钟无新内容", template: "orange" };
|
|
313
322
|
if (status === "stopped") return { title: "已停止", template: "red" };
|
|
314
|
-
if (status === "error")
|
|
323
|
+
if (status === "error") {
|
|
324
|
+
return { title: terminalError ? `异常结束 · ${terminalError.title}` : "异常结束", template: "red" };
|
|
325
|
+
}
|
|
315
326
|
return { title: "完成" };
|
|
316
327
|
}
|
|
317
328
|
|
|
@@ -337,11 +348,42 @@ function monitorsOutputProgress(kind: AgentActivityKind): boolean {
|
|
|
337
348
|
function formatTerminalReply(
|
|
338
349
|
status: "running" | "done" | "stopped" | "error" | "auto_ended",
|
|
339
350
|
finalReply: string,
|
|
351
|
+
terminalError?: TerminalErrorInfo,
|
|
340
352
|
): string | null {
|
|
341
353
|
if (status === "auto_ended") return formatAutoEndedReply(finalReply);
|
|
354
|
+
if (status === "error") {
|
|
355
|
+
const error = terminalError ?? {
|
|
356
|
+
kind: "unknown" as const,
|
|
357
|
+
title: "原因未记录",
|
|
358
|
+
message: "当前状态中没有可用的错误详情,请查看运行日志。",
|
|
359
|
+
occurredAt: Date.now(),
|
|
360
|
+
};
|
|
361
|
+
return formatTerminalErrorNotice(error, finalReply);
|
|
362
|
+
}
|
|
342
363
|
return finalReply || null;
|
|
343
364
|
}
|
|
344
365
|
|
|
366
|
+
function formatTerminalCardContent(state: {
|
|
367
|
+
status: "running" | "done" | "stopped" | "error" | "auto_ended";
|
|
368
|
+
accumulatedContent: string;
|
|
369
|
+
finalReply: string;
|
|
370
|
+
terminalError?: TerminalErrorInfo;
|
|
371
|
+
}): string {
|
|
372
|
+
const content = state.accumulatedContent + state.finalReply;
|
|
373
|
+
if (state.status !== "error") return content;
|
|
374
|
+
|
|
375
|
+
const error = state.terminalError ?? {
|
|
376
|
+
kind: "unknown" as const,
|
|
377
|
+
title: "原因未记录",
|
|
378
|
+
message: "当前状态中没有可用的错误详情,请查看运行日志。",
|
|
379
|
+
occurredAt: Date.now(),
|
|
380
|
+
};
|
|
381
|
+
const reason = formatTerminalErrorReason(error);
|
|
382
|
+
return content.trim()
|
|
383
|
+
? `${content}\n\n${reason}\n以上内容可能不完整。`
|
|
384
|
+
: reason;
|
|
385
|
+
}
|
|
386
|
+
|
|
345
387
|
function isCardKitSequenceConflict(err: unknown): boolean {
|
|
346
388
|
return err instanceof Error && err.message.includes("300317");
|
|
347
389
|
}
|
|
@@ -1110,7 +1152,7 @@ export async function resumeAndPrompt(
|
|
|
1110
1152
|
msgTimestamp: number,
|
|
1111
1153
|
tool: string,
|
|
1112
1154
|
traceId?: string,
|
|
1113
|
-
): Promise<
|
|
1155
|
+
): Promise<SessionRunOutcome> {
|
|
1114
1156
|
return runAgentSession(sessionId, userText, platform, chatId, msgTimestamp, tool, traceId);
|
|
1115
1157
|
}
|
|
1116
1158
|
|
|
@@ -1126,6 +1168,8 @@ interface RunAgentSessionOptions {
|
|
|
1126
1168
|
autoRecovery?: boolean;
|
|
1127
1169
|
}
|
|
1128
1170
|
|
|
1171
|
+
export type SessionRunOutcome = "busy" | "done" | "stopped" | "error" | "auto_ended";
|
|
1172
|
+
|
|
1129
1173
|
export async function runAgentSession(
|
|
1130
1174
|
sessionId: string,
|
|
1131
1175
|
userText: string,
|
|
@@ -1135,7 +1179,7 @@ export async function runAgentSession(
|
|
|
1135
1179
|
tool: string,
|
|
1136
1180
|
traceId?: string,
|
|
1137
1181
|
options: RunAgentSessionOptions = {},
|
|
1138
|
-
): Promise<
|
|
1182
|
+
): Promise<SessionRunOutcome> {
|
|
1139
1183
|
const tid = traceId ?? "";
|
|
1140
1184
|
|
|
1141
1185
|
// runAgentSession 是飞书用户消息、队列消息与内部自动恢复共同使用的唯一
|
|
@@ -1162,7 +1206,7 @@ export async function runAgentSession(
|
|
|
1162
1206
|
? "当前正在生成回复中,请等待完成后再发送消息。也可以发送 /stop 结束,已完成的步骤不会丢失。"
|
|
1163
1207
|
: "该会话正在生成回复中,请等待完成后再发送消息。也可以发送 /stop 结束,已完成的步骤不会丢失。";
|
|
1164
1208
|
await platform.sendText(_chatId, busyMsg).catch(() => {});
|
|
1165
|
-
return;
|
|
1209
|
+
return "busy";
|
|
1166
1210
|
}
|
|
1167
1211
|
|
|
1168
1212
|
// 立即标记活跃,确保 /sessions、isSessionRunning 等查询在异步准备阶段就能看到运行状态。
|
|
@@ -1290,7 +1334,7 @@ export async function runAgentSession(
|
|
|
1290
1334
|
// 再开始缓存问题对应的任务"。
|
|
1291
1335
|
const prevState = await readStreamState(sessionId);
|
|
1292
1336
|
if (prevState && prevState.status !== "running") {
|
|
1293
|
-
const prevTerminalReply = formatTerminalReply(prevState.status, prevState.finalReply);
|
|
1337
|
+
const prevTerminalReply = formatTerminalReply(prevState.status, prevState.finalReply, prevState.terminalError);
|
|
1294
1338
|
const displayChatId = pickDisplayChat(sessionId);
|
|
1295
1339
|
if (displayChatId) {
|
|
1296
1340
|
const pp = platformForChat(displayChatId);
|
|
@@ -1309,8 +1353,8 @@ export async function runAgentSession(
|
|
|
1309
1353
|
setSessionChatAvatar(pp, displayChatId, prevState.tool, "idle", sessionId).catch(() => {});
|
|
1310
1354
|
} else {
|
|
1311
1355
|
const nextSeq = display.sequence + 1;
|
|
1312
|
-
const { title: headerTitle, template: headerTemplate } = formatTerminalHeader(prevState.status);
|
|
1313
|
-
const cardContent = truncateContent(prevState
|
|
1356
|
+
const { title: headerTitle, template: headerTemplate } = formatTerminalHeader(prevState.status, prevState.terminalError);
|
|
1357
|
+
const cardContent = truncateContent(formatTerminalCardContent(prevState)) || " ";
|
|
1314
1358
|
const doneCard = buildProgressCard(progressView({ text: cardContent, status: "done", showStop: false, headerTitle, headerTemplate }));
|
|
1315
1359
|
await pp.cardUpdate(display.cardId, doneCard, nextSeq).catch(err => {
|
|
1316
1360
|
console.error(`[${ts()}] [DISPLAY] prevState final cardUpdate failed: ${(err as Error).message}`);
|
|
@@ -1405,6 +1449,8 @@ export async function runAgentSession(
|
|
|
1405
1449
|
const FILE_WRITE_INTERVAL_MS = 2000;
|
|
1406
1450
|
const toolCallMap = new Map<string, { name: string; input: unknown }>();
|
|
1407
1451
|
let streamErrored = false;
|
|
1452
|
+
let streamTerminalError: TerminalErrorInfo | undefined;
|
|
1453
|
+
let runOutcome: SessionRunOutcome = "error";
|
|
1408
1454
|
|
|
1409
1455
|
const runningPrompt = activePrompts.get(sessionId);
|
|
1410
1456
|
if (runningPrompt) {
|
|
@@ -1580,6 +1626,7 @@ export async function runAgentSession(
|
|
|
1580
1626
|
}
|
|
1581
1627
|
} catch (streamErr) {
|
|
1582
1628
|
streamErrored = true;
|
|
1629
|
+
streamTerminalError = classifyTerminalError(streamErr);
|
|
1583
1630
|
console.error(`[${ts()}] [STREAM] Error in stream loop for ${sessionId}: ${(streamErr as Error).message}`);
|
|
1584
1631
|
} finally {
|
|
1585
1632
|
// 标记 prompt 结束
|
|
@@ -1629,6 +1676,23 @@ export async function runAgentSession(
|
|
|
1629
1676
|
? "stopped"
|
|
1630
1677
|
: "done";
|
|
1631
1678
|
const finalReply = pickFinalReply(state).trim();
|
|
1679
|
+
const terminalError = streamTerminalError
|
|
1680
|
+
?? (wasAbnormalExit
|
|
1681
|
+
? {
|
|
1682
|
+
kind: "process" as const,
|
|
1683
|
+
title: "Agent 进程意外退出",
|
|
1684
|
+
message: "Agent CLI 进程已退出。若回复不完整,请重新发送上一条指令。",
|
|
1685
|
+
occurredAt: Date.now(),
|
|
1686
|
+
}
|
|
1687
|
+
: wasResourceStuck
|
|
1688
|
+
? {
|
|
1689
|
+
kind: "resource" as const,
|
|
1690
|
+
title: "Agent 进程失去响应",
|
|
1691
|
+
message: "Agent 进程长时间没有 CPU 或内存变化,已被强制停止。若回复不完整,请重新发送上一条指令。",
|
|
1692
|
+
occurredAt: Date.now(),
|
|
1693
|
+
}
|
|
1694
|
+
: undefined);
|
|
1695
|
+
runOutcome = finalStatus;
|
|
1632
1696
|
|
|
1633
1697
|
// stop-stuck-loop 接口可能在 fire-and-forget 中已写入带 final_reply 的
|
|
1634
1698
|
// stream state,finally 不应覆盖它。同时保留 stuckAt 标记,防止
|
|
@@ -1659,6 +1723,7 @@ export async function runAgentSession(
|
|
|
1659
1723
|
tool,
|
|
1660
1724
|
...(preserveStuckAt ? { stuckAt: preserveStuckAt } : {}),
|
|
1661
1725
|
...(autoEndedAt !== undefined ? { autoEndedAt } : {}),
|
|
1726
|
+
...(terminalError ? { terminalError } : {}),
|
|
1662
1727
|
});
|
|
1663
1728
|
|
|
1664
1729
|
// display loop 下一轮会读到最终状态并发送消息
|
|
@@ -1749,6 +1814,52 @@ export async function runAgentSession(
|
|
|
1749
1814
|
if (activeErr) setSessionChatAvatar(platform, activeErr, tool, "idle", sessionId).catch(() => {});
|
|
1750
1815
|
console.log(`[${ts()}] Session ${sessionId} process exited unexpectedly (content chunks: ${state.chunkCount})`);
|
|
1751
1816
|
if (tid) logTrace(tid, "SESSION_END", { sessionId, outcome: "process_missing", chunks: state.chunkCount });
|
|
1817
|
+
} else if (streamErrored || wasResourceStuck) {
|
|
1818
|
+
for (const cid of finalizationChatIds) {
|
|
1819
|
+
const finfo = sessionInfoMap.get(cid);
|
|
1820
|
+
await recordSessionRegistry({
|
|
1821
|
+
chatId: cid,
|
|
1822
|
+
sessionId,
|
|
1823
|
+
tool,
|
|
1824
|
+
turnCount: finfo?.turnCount ?? nextTurnCount,
|
|
1825
|
+
lastContextTokens: finfo?.lastContextTokens ?? nextContextTokens,
|
|
1826
|
+
startTime: finfo?.startTime ?? now,
|
|
1827
|
+
running: false,
|
|
1828
|
+
});
|
|
1829
|
+
}
|
|
1830
|
+
const activeErr = getLastActiveChat(sessionId) ?? finalizationChatIds[0];
|
|
1831
|
+
if (activeErr) {
|
|
1832
|
+
const pp = platformForChat(activeErr) ?? platform;
|
|
1833
|
+
const terminalState = await readStreamState(sessionId);
|
|
1834
|
+
if (
|
|
1835
|
+
terminalError
|
|
1836
|
+
&& !displayCards.has(activeErr)
|
|
1837
|
+
&& (!terminalState || !isFinalReplySentForTurn(terminalState))
|
|
1838
|
+
) {
|
|
1839
|
+
await sendFinalReplyTextOnce(
|
|
1840
|
+
pp,
|
|
1841
|
+
activeErr,
|
|
1842
|
+
sessionId,
|
|
1843
|
+
nextTurnCount,
|
|
1844
|
+
formatTerminalErrorNotice(terminalError, finalReplyToWrite),
|
|
1845
|
+
);
|
|
1846
|
+
}
|
|
1847
|
+
setSessionChatAvatar(pp, activeErr, tool, "idle", sessionId).catch(() => {});
|
|
1848
|
+
}
|
|
1849
|
+
const errorOutcome = wasResourceStuck ? "resource_stuck" : "stream_error";
|
|
1850
|
+
console.error(
|
|
1851
|
+
`[${ts()}] Session ${sessionId} ended with ${errorOutcome}` +
|
|
1852
|
+
`${terminalError ? ` (${terminalError.kind}: ${terminalError.title})` : ""} (content chunks: ${state.chunkCount})`,
|
|
1853
|
+
);
|
|
1854
|
+
if (tid) {
|
|
1855
|
+
logTrace(tid, "SESSION_END", {
|
|
1856
|
+
sessionId,
|
|
1857
|
+
outcome: errorOutcome,
|
|
1858
|
+
errorKind: terminalError?.kind,
|
|
1859
|
+
errorTitle: terminalError?.title,
|
|
1860
|
+
chunks: state.chunkCount,
|
|
1861
|
+
});
|
|
1862
|
+
}
|
|
1752
1863
|
} else {
|
|
1753
1864
|
for (const cid of finalizationChatIds) {
|
|
1754
1865
|
const finfo = sessionInfoMap.get(cid);
|
|
@@ -1862,6 +1973,7 @@ export async function runAgentSession(
|
|
|
1862
1973
|
clearSessionFinalizing(sessionId);
|
|
1863
1974
|
}
|
|
1864
1975
|
}
|
|
1976
|
+
return runOutcome;
|
|
1865
1977
|
}
|
|
1866
1978
|
|
|
1867
1979
|
// ---------------------------------------------------------------------------
|
|
@@ -1941,9 +2053,11 @@ export function startUnifiedDisplayLoop(): void {
|
|
|
1941
2053
|
const tail = "━━━ 回答结束 ━━━";
|
|
1942
2054
|
const finalMsg = state.status === "auto_ended"
|
|
1943
2055
|
? formatAutoEndedReply(remaining)
|
|
1944
|
-
:
|
|
1945
|
-
? remaining
|
|
1946
|
-
:
|
|
2056
|
+
: state.status === "error"
|
|
2057
|
+
? formatTerminalReply(state.status, remaining, state.terminalError) ?? tail
|
|
2058
|
+
: remaining
|
|
2059
|
+
? remaining + "\n" + tail
|
|
2060
|
+
: tail;
|
|
1947
2061
|
if (!isFinalReplySentForTurn(state)) {
|
|
1948
2062
|
await sendFinalReplyTextOnce(p, chatId, sessionId, state.turnCount, finalMsg);
|
|
1949
2063
|
}
|
|
@@ -1965,8 +2079,8 @@ export function startUnifiedDisplayLoop(): void {
|
|
|
1965
2079
|
let terminalCardUpdateAccepted = terminalCardAlreadyUpdated;
|
|
1966
2080
|
if (!terminalCardAlreadyUpdated) {
|
|
1967
2081
|
const nextSeq = display.sequence + 1;
|
|
1968
|
-
const { title: headerTitle, template: headerTemplate } = formatTerminalHeader(state.status);
|
|
1969
|
-
const cardContent = truncateContent(state
|
|
2082
|
+
const { title: headerTitle, template: headerTemplate } = formatTerminalHeader(state.status, state.terminalError);
|
|
2083
|
+
const cardContent = truncateContent(formatTerminalCardContent(state)) || " ";
|
|
1970
2084
|
const doneCard = buildProgressCard(progressView({ text: cardContent, status: "done", showStop: false, headerTitle, headerTemplate }));
|
|
1971
2085
|
await p.cardUpdate(display.cardId, doneCard, nextSeq).then(() => {
|
|
1972
2086
|
display.sequence = nextSeq;
|
|
@@ -1993,8 +2107,16 @@ export function startUnifiedDisplayLoop(): void {
|
|
|
1993
2107
|
}
|
|
1994
2108
|
|
|
1995
2109
|
let terminalTextDelivered = true;
|
|
1996
|
-
const terminalReply = formatTerminalReply(state.status, state.finalReply);
|
|
1997
|
-
|
|
2110
|
+
const terminalReply = formatTerminalReply(state.status, state.finalReply, state.terminalError);
|
|
2111
|
+
const errorWasDeliveredByCard =
|
|
2112
|
+
state.status === "error"
|
|
2113
|
+
&& !state.finalReply.trim()
|
|
2114
|
+
&& terminalCardUpdateAccepted;
|
|
2115
|
+
if (errorWasDeliveredByCard) {
|
|
2116
|
+
if (!isFinalReplySentForTurn(state)) {
|
|
2117
|
+
await markFinalReplySent(sessionId, state.turnCount);
|
|
2118
|
+
}
|
|
2119
|
+
} else if (terminalReply) {
|
|
1998
2120
|
if (!isFinalReplySentForTurn(state)) {
|
|
1999
2121
|
terminalTextDelivered = await sendFinalReplyTextOnce(p, chatId, sessionId, state.turnCount, terminalReply);
|
|
2000
2122
|
}
|
package/src/stream-state.ts
CHANGED
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
import { readFile, writeFile, mkdir, rename, unlink } from "node:fs/promises";
|
|
2
2
|
import { dirname, join } from "node:path";
|
|
3
3
|
|
|
4
|
-
import { USER_DATA_DIR, ts } from "./config.ts";
|
|
5
|
-
import { createAgentActivityTracker } from "./agent-activity.ts";
|
|
6
|
-
import type { AgentActivity } from "./agent-activity.ts";
|
|
4
|
+
import { USER_DATA_DIR, ts } from "./config.ts";
|
|
5
|
+
import { createAgentActivityTracker } from "./agent-activity.ts";
|
|
6
|
+
import type { AgentActivity } from "./agent-activity.ts";
|
|
7
|
+
import type { TerminalErrorInfo } from "./terminal-error.ts";
|
|
7
8
|
|
|
8
9
|
// ---------------------------------------------------------------------------
|
|
9
10
|
// stream-state.json — 每个 session 的流式输出持久化文件
|
|
@@ -13,14 +14,14 @@ export const STREAMS_DIR = join(USER_DATA_DIR, "state", "streams");
|
|
|
13
14
|
|
|
14
15
|
export interface StreamState {
|
|
15
16
|
sessionId: string;
|
|
16
|
-
status: "running" | "done" | "stopped" | "error" | "auto_ended";
|
|
17
|
+
status: "running" | "done" | "stopped" | "error" | "auto_ended";
|
|
17
18
|
accumulatedContent: string;
|
|
18
19
|
/** 本轮会话中 LLM 输出的全部文本内容(所有 text block 的累加)。
|
|
19
20
|
* 命名含 "final" 但实为"全部累积文本",并非仅"最终一段回复"。
|
|
20
21
|
* 参见 session.ts 的 AccumulatorState 注释。 */
|
|
21
|
-
finalReply: string;
|
|
22
|
-
/** Current user-visible work phase for running progress cards. */
|
|
23
|
-
activity?: AgentActivity;
|
|
22
|
+
finalReply: string;
|
|
23
|
+
/** Current user-visible work phase for running progress cards. */
|
|
24
|
+
activity?: AgentActivity;
|
|
24
25
|
/** The turn whose terminal text reply has already been delivered to IM. */
|
|
25
26
|
finalReplySentTurn?: number;
|
|
26
27
|
finalReplySentAt?: number;
|
|
@@ -32,10 +33,12 @@ export interface StreamState {
|
|
|
32
33
|
tool: string;
|
|
33
34
|
/** Set by stop-stuck-loop to prevent the session from being resumed.
|
|
34
35
|
* The orchestrator checks this before resuming and creates a new session instead. */
|
|
35
|
-
stuckAt?: number;
|
|
36
|
-
/** Set when the shared response watchdog ends a turn after three minutes without new characters. */
|
|
37
|
-
autoEndedAt?: number;
|
|
38
|
-
|
|
36
|
+
stuckAt?: number;
|
|
37
|
+
/** Set when the shared response watchdog ends a turn after three minutes without new characters. */
|
|
38
|
+
autoEndedAt?: number;
|
|
39
|
+
/** 脱敏后的用户可见根因;完整原始异常只保留在运行日志中。 */
|
|
40
|
+
terminalError?: TerminalErrorInfo;
|
|
41
|
+
}
|
|
39
42
|
|
|
40
43
|
function getStreamStatePath(sessionId: string): string {
|
|
41
44
|
return join(STREAMS_DIR, `${sessionId}.json`);
|
|
@@ -128,18 +131,18 @@ export async function markFinalReplySent(sessionId: string, turnCount: number, s
|
|
|
128
131
|
await writeStreamState(state);
|
|
129
132
|
}
|
|
130
133
|
|
|
131
|
-
export function createEmptyStreamState(sessionId: string, cwd: string, tool: string, turnCount: number): StreamState {
|
|
132
|
-
const now = Date.now();
|
|
133
|
-
return {
|
|
134
|
+
export function createEmptyStreamState(sessionId: string, cwd: string, tool: string, turnCount: number): StreamState {
|
|
135
|
+
const now = Date.now();
|
|
136
|
+
return {
|
|
134
137
|
sessionId,
|
|
135
138
|
status: "running",
|
|
136
|
-
accumulatedContent: "",
|
|
137
|
-
finalReply: "",
|
|
138
|
-
activity: createAgentActivityTracker(now).activity,
|
|
139
|
+
accumulatedContent: "",
|
|
140
|
+
finalReply: "",
|
|
141
|
+
activity: createAgentActivityTracker(now).activity,
|
|
139
142
|
chunkCount: 0,
|
|
140
143
|
turnCount,
|
|
141
144
|
contextTokens: 0,
|
|
142
|
-
updatedAt: now,
|
|
145
|
+
updatedAt: now,
|
|
143
146
|
cwd,
|
|
144
147
|
tool,
|
|
145
148
|
};
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
/** 用户可见的终态错误。这里只保存脱敏摘要;完整原始异常继续只写运行日志。 */
|
|
2
|
+
export type TerminalErrorKind =
|
|
3
|
+
| "network_timeout"
|
|
4
|
+
| "network"
|
|
5
|
+
| "authentication"
|
|
6
|
+
| "rate_limit"
|
|
7
|
+
| "provider"
|
|
8
|
+
| "process"
|
|
9
|
+
| "resource"
|
|
10
|
+
| "unknown";
|
|
11
|
+
|
|
12
|
+
export interface TerminalErrorInfo {
|
|
13
|
+
kind: TerminalErrorKind;
|
|
14
|
+
title: string;
|
|
15
|
+
message: string;
|
|
16
|
+
occurredAt: number;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
const MAX_UNKNOWN_ERROR_LENGTH = 300;
|
|
20
|
+
|
|
21
|
+
function errorMessage(error: unknown): string {
|
|
22
|
+
if (error instanceof Error) return error.message;
|
|
23
|
+
if (typeof error === "string") return error;
|
|
24
|
+
try {
|
|
25
|
+
return JSON.stringify(error);
|
|
26
|
+
} catch {
|
|
27
|
+
return String(error);
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* 未分类错误仍应给出可诊断线索,但不能把常见凭据带进群聊或持久化状态。
|
|
33
|
+
* 已分类错误使用固定文案,不复述地址、请求体或 provider 原始响应。
|
|
34
|
+
*/
|
|
35
|
+
export function sanitizeTerminalErrorDetail(raw: string): string {
|
|
36
|
+
return raw
|
|
37
|
+
.replace(/\bBearer\s+[^\s,;]+/gi, "Bearer [已脱敏]")
|
|
38
|
+
.replace(/\b(api[_-]?key|access[_-]?token|token|secret)\s*[:=]\s*[^\s,;]+/gi, "$1=[已脱敏]")
|
|
39
|
+
.replace(/\bsk-[A-Za-z0-9._-]{6,}\b/g, "sk-[已脱敏]")
|
|
40
|
+
.replace(/([?&](?:api[_-]?key|access[_-]?token|token|secret)=)[^&\s]+/gi, "$1[已脱敏]")
|
|
41
|
+
.slice(0, MAX_UNKNOWN_ERROR_LENGTH);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function parsePositiveInt(message: string, pattern: RegExp): number | undefined {
|
|
45
|
+
const value = Number.parseInt(message.match(pattern)?.[1] ?? "", 10);
|
|
46
|
+
return Number.isFinite(value) && value > 0 ? value : undefined;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function formatSeconds(milliseconds: number): string {
|
|
50
|
+
if (milliseconds % 1000 === 0) return `${milliseconds / 1000} 秒`;
|
|
51
|
+
return `${milliseconds} 毫秒`;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export function classifyTerminalError(error: unknown, occurredAt = Date.now()): TerminalErrorInfo {
|
|
55
|
+
const raw = errorMessage(error);
|
|
56
|
+
const lower = raw.toLowerCase();
|
|
57
|
+
const attempts = parsePositiveInt(raw, /\bafter\s+(\d+)\s+attempts?\b/i);
|
|
58
|
+
const timeoutMs = parsePositiveInt(raw, /\btimeout\s*:\s*(\d+)\s*ms\b/i);
|
|
59
|
+
|
|
60
|
+
if (/\b429\b|rate[ _-]?limit|too many requests/.test(lower)) {
|
|
61
|
+
return {
|
|
62
|
+
kind: "rate_limit",
|
|
63
|
+
title: "请求受到限流",
|
|
64
|
+
message: "模型服务请求受到限流,请稍后重试。",
|
|
65
|
+
occurredAt,
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
if (/\b401\b|\b403\b|unauthori[sz]ed|forbidden|invalid api key|authentication/.test(lower)) {
|
|
70
|
+
return {
|
|
71
|
+
kind: "authentication",
|
|
72
|
+
title: "模型服务鉴权失败",
|
|
73
|
+
message: "模型服务拒绝了当前凭据,请检查 API Key、账号权限或凭据是否过期。",
|
|
74
|
+
occurredAt,
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
if (
|
|
79
|
+
/connect timeout|connection timed out|etimedout|und_err_connect_timeout/.test(lower)
|
|
80
|
+
|| (lower.includes("cannot connect") && lower.includes("timeout"))
|
|
81
|
+
) {
|
|
82
|
+
const retryText = attempts ? `,已重试 ${attempts} 次` : "";
|
|
83
|
+
const timeoutText = timeoutMs ? `,单次连接等待 ${formatSeconds(timeoutMs)}` : "";
|
|
84
|
+
return {
|
|
85
|
+
kind: "network_timeout",
|
|
86
|
+
title: "网络连接超时",
|
|
87
|
+
message: `连接模型服务失败${retryText}${timeoutText}。请检查网络、VPN或模型服务状态后重试。`,
|
|
88
|
+
occurredAt,
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
if (/econnrefused|econnreset|enotfound|eai_again|socket hang up|network error|cannot connect/.test(lower)) {
|
|
93
|
+
return {
|
|
94
|
+
kind: "network",
|
|
95
|
+
title: "无法连接模型服务",
|
|
96
|
+
message: "与模型服务的网络连接失败,请检查网络、VPN、DNS或服务状态后重试。",
|
|
97
|
+
occurredAt,
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
const httpStatus = raw.match(/\b(?:HTTP\s*)?(5\d\d)\b/i)?.[1];
|
|
102
|
+
if (httpStatus || /service unavailable|bad gateway|gateway timeout|provider error/.test(lower)) {
|
|
103
|
+
return {
|
|
104
|
+
kind: "provider",
|
|
105
|
+
title: "模型服务暂时不可用",
|
|
106
|
+
message: `模型服务返回异常${httpStatus ? `(HTTP ${httpStatus})` : ""},请稍后重试。`,
|
|
107
|
+
occurredAt,
|
|
108
|
+
};
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
const safeDetail = sanitizeTerminalErrorDetail(raw).trim() || "未提供错误详情";
|
|
112
|
+
return {
|
|
113
|
+
kind: "unknown",
|
|
114
|
+
title: "Agent 执行错误",
|
|
115
|
+
message: `发生未分类错误:${safeDetail}`,
|
|
116
|
+
occurredAt,
|
|
117
|
+
};
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
export function formatTerminalErrorReason(error: TerminalErrorInfo): string {
|
|
121
|
+
return `⚠️ 异常结束:${error.title}\n${error.message}`;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
export function formatTerminalErrorNotice(error: TerminalErrorInfo, finalReply = ""): string {
|
|
125
|
+
const reason = formatTerminalErrorReason(error);
|
|
126
|
+
return finalReply.trim()
|
|
127
|
+
? `${reason}\n\n以下回复可能不完整:\n\n${finalReply.trim()}`
|
|
128
|
+
: reason;
|
|
129
|
+
}
|