Skip to content

Commit 58b1bd4

Browse files
committed
fix(error): API 错误信息友好化,不再向用户暴露完整堆栈
- 新增 extractApiErrorMessage:从 RetryError/APICallError 嵌套结构中提取根因 - 优先解析 responseBody 中的原始错误消息(如"号池见底,无法注册") - Agent 中 logger.error 改为 logger.debug 避免堆栈打到终端 - useCommandHandler 和 headless 模式同步优化
1 parent 82937a8 commit 58b1bd4

3 files changed

Lines changed: 120 additions & 9 deletions

File tree

packages/cli/src/agent/Agent.ts

Lines changed: 45 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -86,6 +86,46 @@ import type {
8686
// 创建 Agent 专用 Logger
8787
const logger = createLogger(LogCategory.AGENT);
8888

89+
/**
90+
* 从 API 错误中提取用户友好的错误信息
91+
* 处理 Vercel AI SDK 的 RetryError 和 APICallError 嵌套结构
92+
*/
93+
function extractApiErrorMessage(error: unknown): string {
94+
if (!(error instanceof Error)) return '未知错误';
95+
96+
// Vercel AI SDK RetryError: 从嵌套的 lastError/cause 中提取根因
97+
const retryError = error as Error & { lastError?: Error; reason?: string };
98+
const rootError = retryError.lastError ?? error;
99+
100+
// APICallError: 尝试从 responseBody 解析原始错误消息
101+
const apiError = rootError as Error & {
102+
responseBody?: string;
103+
statusCode?: number;
104+
};
105+
106+
if (apiError.responseBody) {
107+
try {
108+
const body = JSON.parse(apiError.responseBody);
109+
const msg = body?.error?.message;
110+
if (msg) {
111+
const statusHint = apiError.statusCode ? ` (HTTP ${apiError.statusCode})` : '';
112+
return `${msg}${statusHint}`;
113+
}
114+
} catch {
115+
// JSON 解析失败,fallback
116+
}
117+
}
118+
119+
// 清理 RetryError 的冗长前缀
120+
const message = error.message;
121+
const lastErrorMatch = message.match(/Last error:\s*(.+)$/);
122+
if (lastErrorMatch) {
123+
return lastErrorMatch[1];
124+
}
125+
126+
return message;
127+
}
128+
89129
/**
90130
* Skill 执行上下文
91131
* 用于跟踪当前活动的 Skill 及其工具限制
@@ -1584,12 +1624,15 @@ IMPORTANT: Execute according to the approved plan above. Follow the steps exactl
15841624
};
15851625
}
15861626

1587-
logger.error('Enhanced chat processing error:', error);
1627+
// 只在 debug 模式下打印完整堆栈,避免用户看到巨大的错误信息
1628+
logger.debug('Enhanced chat processing error (full):', error);
1629+
const friendlyMessage = extractApiErrorMessage(error);
1630+
logger.error(`API 调用失败: ${friendlyMessage}`);
15881631
return {
15891632
success: false,
15901633
error: {
15911634
type: 'api_error',
1592-
message: `处理消息时发生错误: ${error instanceof Error ? error.message : '未知错误'}`,
1635+
message: friendlyMessage,
15931636
details: error,
15941637
},
15951638
metadata: {

packages/cli/src/commands/headless.ts

Lines changed: 29 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -213,6 +213,34 @@ function createConfirmationHandler() {
213213
};
214214
}
215215

216+
/**
217+
* 从 API 错误中提取用户友好的错误信息
218+
*/
219+
function extractHeadlessErrorMessage(error: unknown): string {
220+
if (!(error instanceof Error)) return 'Unknown error';
221+
222+
const retryError = error as Error & { lastError?: Error };
223+
const rootError = retryError.lastError ?? error;
224+
const apiError = rootError as Error & { responseBody?: string; statusCode?: number };
225+
226+
if (apiError.responseBody) {
227+
try {
228+
const body = JSON.parse(apiError.responseBody);
229+
const msg = body?.error?.message;
230+
if (msg) {
231+
return apiError.statusCode ? `${msg} (HTTP ${apiError.statusCode})` : msg;
232+
}
233+
} catch {
234+
// fallback
235+
}
236+
}
237+
238+
const lastErrorMatch = error.message.match(/Last error:\s*(.+)$/);
239+
if (lastErrorMatch) return lastErrorMatch[1];
240+
241+
return error.message;
242+
}
243+
216244
function resolveOutputFormat(outputFormat?: string): HeadlessOutputFormat {
217245
return outputFormat === 'jsonl' ? 'jsonl' : 'text';
218246
}
@@ -492,7 +520,7 @@ export async function runHeadless(
492520
if (streamState.hasOpenThinking() && outputFormat === 'text') {
493521
io.stderr.write('\n');
494522
}
495-
eventWriter.error(`Error: ${error instanceof Error ? error.message : 'Unknown error'}`);
523+
eventWriter.error(`Error: ${extractHeadlessErrorMessage(error)}`);
496524
return 1;
497525
}
498526
}

packages/cli/src/ui/hooks/useCommandHandler.ts

Lines changed: 46 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,45 @@ import type { ResolvedInput } from './useInputBuffer.js';
4040
// 创建 UI Hook 专用 Logger
4141
const logger = createLogger(LogCategory.UI);
4242

43+
/**
44+
* 从 API 错误中提取用户友好的错误信息
45+
* 处理 Vercel AI SDK 的 RetryError/APICallError 嵌套结构
46+
*/
47+
function extractFriendlyErrorMessage(error: unknown): string {
48+
if (!(error instanceof Error)) return '未知错误';
49+
50+
// Vercel AI SDK RetryError: 从嵌套的 lastError 中提取根因
51+
const retryError = error as Error & { lastError?: Error };
52+
const rootError = retryError.lastError ?? error;
53+
54+
// APICallError: 尝试从 responseBody 解析原始错误消息
55+
const apiError = rootError as Error & {
56+
responseBody?: string;
57+
statusCode?: number;
58+
};
59+
60+
if (apiError.responseBody) {
61+
try {
62+
const body = JSON.parse(apiError.responseBody);
63+
const msg = body?.error?.message;
64+
if (msg) {
65+
const statusHint = apiError.statusCode ? ` (HTTP ${apiError.statusCode})` : '';
66+
return `${msg}${statusHint}`;
67+
}
68+
} catch {
69+
// JSON 解析失败,fallback
70+
}
71+
}
72+
73+
// 清理 RetryError 的冗长前缀
74+
const lastErrorMatch = error.message.match(/Last error:\s*(.+)$/);
75+
if (lastErrorMatch) {
76+
return lastErrorMatch[1];
77+
}
78+
79+
return error.message;
80+
}
81+
4382
/**
4483
* invoke_skill action 的数据类型
4584
*/
@@ -891,15 +930,16 @@ Remember: Follow the above instructions carefully to complete the user's request
891930
return { success: false, error: 'aborted' };
892931
}
893932

894-
const errorMessage = error instanceof Error ? error.message : '未知错误';
933+
const errorMessage = extractFriendlyErrorMessage(error);
895934

896935
// 检测是否是图片/多模态不支持的错误
936+
const rawMessage = error instanceof Error ? error.message : '';
897937
const isVisionNotSupportedError =
898-
errorMessage.includes('can only concatenate str') ||
899-
errorMessage.includes('image_url') ||
900-
errorMessage.includes('multimodal') ||
901-
errorMessage.includes('vision') ||
902-
errorMessage.includes('does not support images');
938+
rawMessage.includes('can only concatenate str') ||
939+
rawMessage.includes('image_url') ||
940+
rawMessage.includes('multimodal') ||
941+
rawMessage.includes('vision') ||
942+
rawMessage.includes('does not support images');
903943

904944
let displayMessage = errorMessage;
905945
if (isVisionNotSupportedError) {

0 commit comments

Comments
 (0)