Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
137 lines
4.9 KiB
TypeScript
137 lines
4.9 KiB
TypeScript
/**
|
|
* Execute a function with automatic retry on failure.
|
|
* Used to wrap external API calls in tool executors.
|
|
*/
|
|
export async function executeWithRetry<T>(
|
|
fn: () => Promise<T>,
|
|
retries = 1,
|
|
options?: { backoffMs?: number; exponential?: boolean; onAttempt?: (attempt: number) => void; abortSignal?: AbortSignal },
|
|
): Promise<T> {
|
|
const backoffMs = options?.backoffMs ?? 1000;
|
|
const exponential = options?.exponential ?? false;
|
|
let attempt = 0;
|
|
const maxAttempts = retries + 1;
|
|
|
|
while (true) {
|
|
try {
|
|
return await fn();
|
|
} catch (e) {
|
|
attempt++;
|
|
options?.onAttempt?.(attempt);
|
|
if (attempt >= maxAttempts) throw e;
|
|
const delay = exponential
|
|
? backoffMs * Math.pow(2, attempt - 1)
|
|
: backoffMs;
|
|
console.warn(
|
|
`[retry] attempt ${attempt}/${maxAttempts} failed, retrying in ${delay}ms`,
|
|
);
|
|
await new Promise<void>((resolve, reject) => {
|
|
const t = setTimeout(resolve, delay);
|
|
options?.abortSignal?.addEventListener("abort", () => {
|
|
clearTimeout(t);
|
|
reject(new Error("RunCancelled"));
|
|
}, { once: true });
|
|
});
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Classify error type from raw error for context-aware messaging.
|
|
*/
|
|
function classifyError(error: unknown): "timeout" | "not_found" | "bad_request" | "auth" | "server" | "generic" {
|
|
const msg = error instanceof Error ? error.message : String(error);
|
|
if (msg.includes("TimeoutError") || msg.includes("abort") || msg.includes("timeout")) {
|
|
return "timeout";
|
|
}
|
|
if (msg.includes("401") || msg.includes("403")) return "auth";
|
|
if (msg.includes("404")) return "not_found";
|
|
if (msg.includes("400")) return "bad_request";
|
|
if (msg.includes("500") || msg.includes("502") || msg.includes("503")) return "server";
|
|
return "generic";
|
|
}
|
|
|
|
/**
|
|
* Map tool names + error type to user-friendly Chinese error messages.
|
|
* Hides raw HTTP errors, stack traces, and technical details from the LLM/user.
|
|
*/
|
|
/**
|
|
* Fallback hints appended to error messages for specific tools.
|
|
* These are only visible to the LLM (not rendered to user) so
|
|
* the LLM can autonomously decide whether to call a fallback tool.
|
|
*/
|
|
const TOOL_FALLBACK_HINTS: Record<string, string> = {
|
|
kb_search: "建议:可尝试使用搜索引擎查找相关信息。",
|
|
};
|
|
|
|
const TOOL_ERROR_MAP: Record<string, Partial<Record<ReturnType<typeof classifyError>, string>> & { generic: string }> = {
|
|
kb_search: {
|
|
timeout: "知识库检索服务暂时响应较慢,请稍后再试",
|
|
auth: "知识库权限验证失败,请联系管理员",
|
|
server: "知识库服务暂时不可用",
|
|
generic: "知识库检索服务暂时不可用,请稍后再试",
|
|
},
|
|
ticket_list: {
|
|
timeout: "工单系统响应较慢,请稍后再试",
|
|
auth: "工单系统权限验证失败,请联系管理员",
|
|
server: "工单系统服务暂时不可用",
|
|
generic: "工单列表查询失败,请稍后再试",
|
|
},
|
|
ticket_detail: {
|
|
timeout: "工单系统响应较慢,请稍后再试",
|
|
not_found: "未找到该工单,请确认工单编号后重试",
|
|
auth: "工单系统权限验证失败,请联系管理员",
|
|
server: "工单系统服务暂时不可用",
|
|
generic: "工单详情查询失败,请检查工单编号后重试",
|
|
},
|
|
google_search: {
|
|
auth: "搜索服务权限验证失败,请联系管理员",
|
|
server: "搜索服务暂时不可用",
|
|
generic: "搜索服务暂时不可用,请稍后再试",
|
|
},
|
|
web_search_deep: {
|
|
server: "深度搜索服务暂时不可用",
|
|
generic: "深度搜索暂时不可用,已尝试自动重试",
|
|
},
|
|
web_read: {
|
|
not_found: "该网页不存在或已被删除",
|
|
generic: "网页读取失败,该页面可能无法访问或已被删除",
|
|
},
|
|
code_execute: {
|
|
bad_request: "代码执行环境暂不可用,请稍后再试",
|
|
server: "代码执行服务暂时不可用",
|
|
generic: "代码执行环境暂不可用,请稍后再试",
|
|
},
|
|
code_install: {
|
|
generic: "依赖安装失败,请检查包名后重试",
|
|
},
|
|
sandbox_run: {
|
|
bad_request: "沙盒执行环境暂不可用,请稍后再试",
|
|
server: "沙盒服务暂时不可用",
|
|
generic: "沙盒执行环境暂不可用,请稍后再试",
|
|
},
|
|
doc_create: {
|
|
generic: "文档创建失败,请稍后再试",
|
|
},
|
|
doc_edit: {
|
|
generic: "文档编辑失败,请稍后再试",
|
|
},
|
|
doc_translate: {
|
|
generic: "文档翻译失败,请稍后再试",
|
|
},
|
|
};
|
|
|
|
export function formatToolError(toolName: string, error: unknown): string {
|
|
const kind = classifyError(error);
|
|
const map = TOOL_ERROR_MAP[toolName];
|
|
let friendly = map?.[kind] ?? map?.generic ?? "处理请求时遇到问题,请重试";
|
|
// Append fallback hint for LLM (not shown to user directly)
|
|
const hint = TOOL_FALLBACK_HINTS[toolName];
|
|
if (hint) {
|
|
friendly = `${friendly}\n${hint}`;
|
|
}
|
|
// Log raw error for debugging but return friendly message to LLM
|
|
console.error(`[tool-error] ${toolName} (${kind}):`, error);
|
|
return friendly;
|
|
}
|