@zqagent/core 0.3.10 → 0.3.11
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/dist/index.cjs +96 -5
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +46 -2
- package/dist/index.d.ts +46 -2
- package/dist/index.js +94 -5
- package/dist/index.js.map +1 -1
- package/dist/internal/providers/diagnostics.cjs +17 -3
- package/dist/internal/providers/diagnostics.cjs.map +1 -1
- package/dist/internal/providers/diagnostics.d.cts +11 -1
- package/dist/internal/providers/diagnostics.d.ts +11 -1
- package/dist/internal/providers/diagnostics.js +15 -2
- package/dist/internal/providers/diagnostics.js.map +1 -1
- package/dist/internal/providers/models.cjs.map +1 -1
- package/dist/internal/providers/models.js.map +1 -1
- package/dist/internal/update/controller.cjs +2 -2
- package/dist/internal/update/controller.cjs.map +1 -1
- package/dist/internal/update/controller.js +1 -1
- package/dist/internal/update/platform.cjs +2 -2
- package/dist/internal/update/platform.cjs.map +1 -1
- package/dist/internal/update/platform.js +1 -1
- package/dist/tools/explore.cjs +76 -4
- package/dist/tools/explore.cjs.map +1 -1
- package/dist/tools/explore.js +76 -4
- package/dist/tools/explore.js.map +1 -1
- package/package.json +2 -2
|
@@ -33,7 +33,8 @@ __export(diagnostics_exports, {
|
|
|
33
33
|
networkErrorHints: () => networkErrorHints,
|
|
34
34
|
normalizeBaseURL: () => normalizeBaseURL,
|
|
35
35
|
normalizeErrorFetch: () => normalizeErrorFetch,
|
|
36
|
-
paramErrorHints: () => paramErrorHints
|
|
36
|
+
paramErrorHints: () => paramErrorHints,
|
|
37
|
+
toolCallHistoryHints: () => toolCallHistoryHints
|
|
37
38
|
});
|
|
38
39
|
module.exports = __toCommonJS(diagnostics_exports);
|
|
39
40
|
function isNetworkErrorText(text) {
|
|
@@ -184,10 +185,22 @@ function authErrorHints(message, code = "") {
|
|
|
184
185
|
"\u51ED\u636E\u65E0\u6548\uFF1A\u82E5\u7528\u7684\u662F\u300C\u5FD7\u524D\u5927\u6A21\u578B\uFF08\u5185\u7F6E\uFF09\u300D\uFF0C\u5728\u8282\u70B9\u8BBE\u7F6E\u7684\u6A21\u578B\u9875\u70B9\u300C\u5237\u65B0\u5185\u7F6E\u6A21\u578B\u300D\u91CD\u65B0\u9886\u53D6 Key\uFF08\u5728\u63A7\u5236\u53F0\u91CD\u7F6E\u8FC7 Key \u540E\u9700\u8981\u8FD9\u4E00\u6B65\uFF09\uFF1B\u82E5\u662F\u81EA\u586B\u7684\u4F9B\u5E94\u5546\uFF0C\u8BF7\u68C0\u67E5 Key \u662F\u5426\u6B63\u786E\u6216\u5DF2\u8FC7\u671F"
|
|
185
186
|
];
|
|
186
187
|
}
|
|
188
|
+
function toolCallHistoryHints(message, code = "") {
|
|
189
|
+
const text = `${message} ${code}`;
|
|
190
|
+
if (!/tool_calls|tool[_ ]?call[_ ]?id/i.test(text) || !/must be followed by tool messages|insufficient tool messages|no tool output found|tool messages responding/i.test(
|
|
191
|
+
text
|
|
192
|
+
)) {
|
|
193
|
+
return [];
|
|
194
|
+
}
|
|
195
|
+
return [
|
|
196
|
+
"\u8FD9\u4E0D\u662F\u6A21\u578B\u6216\u4F59\u989D\u95EE\u9898\uFF1A\u4F1A\u8BDD\u5386\u53F2\u91CC\u6709\u4E00\u6761\u300C\u5DE5\u5177\u8C03\u7528\u6CA1\u6709\u62FF\u5230\u7ED3\u679C\u300D\u7684\u8BB0\u5F55\uFF08\u901A\u5E38\u662F\u4E0A\u6B21\u5728\u5DE5\u5177\u6267\u884C\u4E2D\u6309\u4E86 Ctrl+C\uFF0C\u6216\u8FDB\u7A0B\u88AB\u4E2D\u65AD\uFF09",
|
|
197
|
+
"\u5F15\u64CE\u5DF2\u81EA\u52A8\u6E05\u7406\u5E76\u91CD\u8BD5\uFF1B\u5982\u679C\u4ECD\u62A5\u540C\u6837\u7684\u9519\u8BEF\uFF0C\u7528 /new \u5F00\u4E00\u4E2A\u65B0\u4F1A\u8BDD\u5373\u53EF\uFF08\u65E7\u4F1A\u8BDD\u7684\u5386\u53F2\u91CC\u8FD8\u7559\u7740\u8FD9\u6761\u8BB0\u5F55\uFF09"
|
|
198
|
+
];
|
|
199
|
+
}
|
|
187
200
|
function paramErrorHints(message, code = "") {
|
|
188
201
|
const text = `${message} ${code}`;
|
|
189
202
|
const hints = [];
|
|
190
|
-
if (/not[_\s]*(activated|enabled|purchased)|arrearage|quota|subscription|insufficient
|
|
203
|
+
if (/not[_\s]*(activated|enabled|purchased)|arrearage|quota|subscription|billing|未开通|欠费|额度|余额不足|insufficient[_\s]*(quota|balance|credit|funds)/i.test(
|
|
191
204
|
text
|
|
192
205
|
)) {
|
|
193
206
|
hints.push(
|
|
@@ -283,6 +296,7 @@ function llmMaxRetries() {
|
|
|
283
296
|
networkErrorHints,
|
|
284
297
|
normalizeBaseURL,
|
|
285
298
|
normalizeErrorFetch,
|
|
286
|
-
paramErrorHints
|
|
299
|
+
paramErrorHints,
|
|
300
|
+
toolCallHistoryHints
|
|
287
301
|
});
|
|
288
302
|
//# sourceMappingURL=diagnostics.cjs.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../src/providers/diagnostics.ts"],"sourcesContent":["/**\n * LLM 连接诊断与配置归一化\n *\n * 背景:CLI 在部分环境(如 openEuler 等国产 Linux、经企业网关/代理的网络)会\n * 只看到 `Connection error.`(OpenAI SDK 的 APIConnectionError),无法判断是\n * DNS、TLS、代理还是配置问题。这里提供:\n * - `cleanLlmValue` / `normalizeBaseURL`:清理 CRLF/引号/BOM、补协议、\n * 去掉 `/chat/completions` 之类的端点后缀(配置从其它系统/编辑器带进来时常见)\n * - `enrichLlmError`:把「底层 errno(沿 cause 链递归,穿透 SDK 包装)\n * + 实际请求端点」补进错误信息\n * - `networkErrorHints`:按错误给出可执行的排查建议(CLI 直接打印)\n */\n\n/** 是否连接/网络类错误(用于自动重试与提示) */\nexport function isNetworkErrorText(text: string): boolean {\n const t = text.toLowerCase();\n return [\n \"connection error\",\n \"fetch failed\",\n \"econnreset\",\n \"econnrefused\",\n \"etimedout\",\n \"ehostunreach\",\n \"enetunreach\",\n \"enotfound\",\n \"eai_again\",\n \"socket hang up\",\n \"und_err\",\n \"network\",\n \"timed out\",\n \"tls\",\n \"certificate\",\n \"unable_to_verify\",\n \"self signed\",\n ].some((m) => t.includes(m));\n}\n\n/**\n * 清理从配置文件/环境变量/粘贴进来的值:\n * BOM、首尾空白、换行与制表符、以及整体包裹的引号。\n * (Windows 记事本/CRLF、PowerShell `setx`、复制粘贴常见)\n */\nexport function cleanLlmValue(v?: string | null): string | undefined {\n if (v == null) return undefined;\n let s = String(v).replace(/^\\uFEFF/, \"\");\n s = s.replace(/[\\r\\n\\t]/g, \"\").trim();\n if (\n s.length >= 2 &&\n ((s.startsWith('\"') && s.endsWith('\"')) || (s.startsWith(\"'\") && s.endsWith(\"'\")))\n ) {\n s = s.slice(1, -1).trim();\n }\n return s || undefined;\n}\n\n/**\n * 规范化 Base URL:\n * - 缺协议时补 `https://`\n * - 去掉末尾 `/`\n * - 去掉误填的端点后缀(`/chat/completions`、`/responses`、`/completions`、`/models`)\n */\nexport function normalizeBaseURL(raw?: string | null): string | undefined {\n let s = cleanLlmValue(raw);\n if (!s) return undefined;\n if (!/^[a-z][a-z0-9+.-]*:\\/\\//i.test(s)) s = `https://${s}`;\n s = s.replace(/\\/+$/, \"\");\n s = s.replace(/\\/(chat\\/completions|completions|responses|models)$/i, \"\");\n return s.replace(/\\/+$/, \"\") || undefined;\n}\n\n/**\n * 是否为推理模型(o 系列 / gpt-5 等)。\n *\n * 这类模型在 Chat Completions 上**只接受** `max_completion_tokens`,传\n * `max_tokens` 会直接 400 `Unsupported parameter: 'max_tokens' is not\n * supported with this model`。Provider 与模型自检(`probeModel`)必须共用\n * 这一判断:一旦两处各写一份,自检就会把可用的推理模型误判成不可用。\n */\nexport function isReasoningModel(model: string): boolean {\n return /^(o\\d|gpt-5)/i.test(model);\n}\n\nfunction asRecord(v: unknown): Record<string, unknown> | null {\n return v && typeof v === \"object\" ? (v as Record<string, unknown>) : null;\n}\n\n/**\n * 展开错误链:`err` 本身 → `cause` → `cause.cause`…\n * 同时展开 `AggregateError.errors`(IPv4/IPv6 双栈都连不上时 undici 会这样抛)。\n * 带已访问集合,防止 cause 循环引用死循环。\n */\nfunction causeChain(err: unknown, maxNodes = 6): Record<string, unknown>[] {\n const out: Record<string, unknown>[] = [];\n const seen = new Set<unknown>();\n const queue: unknown[] = [err];\n while (queue.length > 0 && out.length < maxNodes) {\n const cur = queue.shift();\n if (!cur || typeof cur !== \"object\" || seen.has(cur)) continue;\n seen.add(cur);\n const rec = asRecord(cur);\n if (!rec) continue;\n out.push(rec);\n if (rec[\"cause\"]) queue.push(rec[\"cause\"]);\n const errs = rec[\"errors\"];\n if (Array.isArray(errs)) queue.push(...errs);\n }\n return out;\n}\n\n/**\n * 形如错误码的 name(`ECONNRESET`、`UND_ERR_CONNECT_TIMEOUT`)。\n * 内建/包装类的 name(`TypeError`、`Error`、`APIConnectionError`)不是错误码,\n * 混进 `code=` 只会误导排查方向。\n */\nfunction isErrorCodeLike(v: string): boolean {\n return /^[A-Z][A-Z0-9_]*$/.test(v);\n}\n\n/**\n * 自顶向下穿透包装层,取第一个可用的错误码(如 undici 的 ECONNRESET /\n * UND_ERR_* / ENOTFOUND)。\n *\n * OpenAI SDK 在连接失败时抛出的是\n * `APIConnectionError(\"Connection error.\")` → `cause: TypeError(\"fetch failed\")`\n * → `cause: Error(\"getaddrinfo ENOTFOUND ...\")`,真正的 errno 在最深处。\n * 只看一层只会拿到 `TypeError`,等于没有信息。\n */\nexport function errorCode(err: unknown): string | undefined {\n for (const src of causeChain(err)) {\n for (const k of [\"code\", \"errno\"]) {\n const v = src[k];\n if (typeof v === \"string\" && v) return v;\n }\n const name = src[\"name\"];\n if (typeof name === \"string\" && name !== \"Error\" && isErrorCodeLike(name)) {\n return name;\n }\n }\n return undefined;\n}\n\n/** 从 SDK 错误对象里取供应商原始错误体(OpenAI SDK 挂在 `error` 属性上) */\nfunction errorBody(err: unknown): Record<string, unknown> | null {\n return asRecord(asRecord(err)?.[\"error\"]);\n}\n\n/** 「端点 + 底层原因」后缀(无可用信息时返回空串) */\nexport function llmErrorContext(err: unknown, baseURL?: string): string {\n const parts: string[] = [];\n const code = errorCode(err);\n if (code) parts.push(`code=${code}`);\n const msg = err instanceof Error ? err.message : String(err ?? \"\");\n // 优先取「错误码所在那一层」的 message,保证 code 与文字描述同源;\n // 该层没有 message 时再取链路最深层(如 fetch failed 之下的\n // \"getaddrinfo ENOTFOUND ...\"),而不是停在 TypeError 的 \"fetch failed\"。\n const chain = causeChain(err);\n const codeSrc = chain.find(\n (c) => typeof c[\"code\"] === \"string\" || typeof c[\"errno\"] === \"string\",\n );\n const pickMsg = (c?: Record<string, unknown>): string | undefined => {\n const m = c?.[\"message\"];\n return typeof m === \"string\" && m && m !== msg ? m : undefined;\n };\n const causeMsg =\n pickMsg(codeSrc) ??\n [...chain].reverse().map(pickMsg).find((m): m is string => !!m) ??\n \"\";\n if (causeMsg) parts.push(causeMsg.slice(0, 160));\n // 供应商错误体里的真实原因(经 normalizeErrorFetch 归一化后可读到):\n // 形状非标准的网关 SDK 只会给出一句 \"400 status code (no body)\"\n const body = errorBody(err);\n const bodyMsg = typeof body?.[\"message\"] === \"string\" ? body[\"message\"] : \"\";\n if (bodyMsg && !msg.includes(bodyMsg)) parts.push(`详情: ${bodyMsg.slice(0, 200)}`);\n const bodyCode = typeof body?.[\"code\"] === \"string\" ? body[\"code\"] : \"\";\n if (bodyCode && !parts.some((p) => p.includes(bodyCode))) {\n parts.push(`code=${bodyCode}`);\n }\n const rid =\n (typeof err === \"object\" && err !== null\n ? String((err as { request_id?: unknown }).request_id ?? \"\")\n : \"\") ||\n (typeof body?.[\"request_id\"] === \"string\" ? body[\"request_id\"] : \"\");\n if (rid) parts.push(`request_id=${rid}`);\n if (baseURL) parts.push(`端点 ${baseURL}`);\n return parts.length > 0 ? `(${parts.join(\" · \")})` : \"\";\n}\n\n/**\n * 就地把「端点 + 底层原因」补进错误信息(保留原错误对象类型/status,\n * 便于上层继续按类型分支,如 Responses 不支持时降级)。\n */\nexport function enrichLlmError(err: unknown, baseURL?: string): unknown {\n if (!(err instanceof Error)) return err;\n const ctx = llmErrorContext(err, baseURL);\n if (!ctx) return err;\n if (err.message.includes(\"端点 \") || err.message.includes(ctx)) return err;\n try {\n err.message = `${err.message}${ctx}`;\n } catch {\n // message 只读时忽略\n }\n return err;\n}\n\n/** 连接类错误 → 可执行的排查建议(非连接类返回空数组) */\nexport function networkErrorHints(message: string, errorType = \"\"): string[] {\n const text = `${message} ${errorType}`;\n if (!isNetworkErrorText(text)) return [];\n const hints: string[] = [\n \"排查:① 网络是否可达:curl -sI <BaseURL>/models(或 /connect 重测)\",\n \"② 需要代理时:SOCKS5_PROXY=socks5://127.0.0.1:7890(HTTP_PROXY 不支持)\",\n \"③ 企业网关/证书拦截:NODE_EXTRA_CA_CERTS=/path/to/ca.pem\",\n ];\n if (/enotfound|eai_again|getaddrinfo/i.test(text)) {\n hints.push(\"④ DNS 解析失败:检查 /etc/resolv.conf,或改用 IP/内网域名\");\n } else if (/etimedout|econnrefused|unreachable/i.test(text)) {\n hints.push(\"④ 连接超时/被拒:检查防火墙出网策略与端口(443)\");\n } else if (/certificate|unable_to_verify|self signed|tls/i.test(text)) {\n hints.push(\"④ 证书链问题:为 Node 指定系统 CA(NODE_EXTRA_CA_CERTS)或补齐 ca-certificates\");\n } else if (/econnreset|socket hang up/i.test(text)) {\n hints.push(\"④ 连接被重置:多为链路/网关抖动,已自动重试;可调大 NETWORK_RETRY_MAX\");\n }\n hints.push(\"⑤ IPv6 抖动可试:NODE_OPTIONS=--dns-result-order=ipv4first\");\n return hints;\n}\n\n/**\n * 「凭据无效」类错误的排查建议(非该类错误返回空数组)。\n *\n * 与 `paramErrorHints` 分开:401 不是模型问题,混进去会让「模型类提示」\n * 在鉴权失败时也冒出来,反而误导。这里只解释「Key 为什么失效、去哪儿换」。\n *\n * 内置模型(志前 API)的 Key 由平台托管:用户在控制台重置 Key 后本机这份\n * 立即失效,必须引导去点「刷新内置模型」,否则会一直看到 401 以为节点坏了。\n */\nexport function authErrorHints(message: string, code = \"\"): string[] {\n const text = `${message} ${code}`;\n if (!/401|403|unauthorized|forbidden|invalid[_\\s]*api[_\\s]*key|api[\\s_-]*key\\s*(?:无效|invalid)|管理令牌无效/i.test(text)) {\n return [];\n }\n return [\n \"凭据无效:若用的是「志前大模型(内置)」,在节点设置的模型页点「刷新内置模型」重新领取 Key\" +\n \"(在控制台重置过 Key 后需要这一步);若是自填的供应商,请检查 Key 是否正确或已过期\",\n ];\n}\n\n/**\n * 「模型/参数不被接受」类错误的排查建议(非该类错误返回空数组)。\n *\n * 典型场景(均为阿里兼容模式实测):\n * - 选到非对话/专用通道模型(`cmaas-*`、向量/语音/图像…)\n * → `InvalidParameter: Required parameter(s) missing or invalid`\n * - 模型在该账号下没开通\n * → `invalid_parameter_error: The product is not activated`\n * 这两句原文对排查毫无帮助,必须翻成可执行的下一步。\n */\nexport function paramErrorHints(message: string, code = \"\"): string[] {\n const text = `${message} ${code}`;\n const hints: string[] = [];\n\n // 一、账号/开通类\n if (\n /not[_\\s]*(activated|enabled|purchased)|arrearage|quota|subscription|insufficient|未开通|欠费|额度|余额不足/i.test(\n text,\n )\n ) {\n hints.push(\n \"该模型在此 **API Key/账号** 下未开通(或账号欠费、额度用尽):到网关控制台开通该模型,或换一个已开通的模型\",\n );\n // 内置模型(志前 API)走的是用户自己的租户额度,额度用完是最常见的失败原因,\n // 必须给出「去哪儿充值」而不是让人对着 402 猜\n hints.push(\n \"如果你用的是「志前大模型(内置)」:到 https://hub.cd-zq.com 查看余额并充值;\" +\n \"也可以先切换到你自己配置的其它模型继续(节点设置 → 模型 → 设为默认)\",\n );\n }\n\n // 二、模型名/参数类\n if (\n /invalid[_\\s]*parameter|invalidparameter|required[_\\s]+parameter|missing[_\\s]+parameter|invalid[_\\s]*model|model[_\\s]*not[_\\s]*found|not[_\\s]*support|unsupported/i.test(\n text,\n )\n ) {\n hints.push(\n \"多半是**模型名不可用于对话接口**:网关 /models 列的是全量目录,含向量(embedding)、重排(rerank)、语音(asr/tts)、实时(realtime)、图像(image/wan)、专用通道(cmaas-*) 等,这些模型不能当 Agent 后端\",\n \"换一个对话模型:/connect 重新选择(对话模型已排在列表前面),或 /models 查看\",\n \"手动验证某个模型能不能用:\\n\" +\n \" curl -s <BaseURL>/chat/completions -H 'Authorization: Bearer $KEY' -H 'content-type: application/json' \\\\\\n\" +\n \" -d '{\\\"model\\\":\\\"<模型名>\\\",\\\"messages\\\":[{\\\"role\\\":\\\"user\\\",\\\"content\\\":\\\"hi\\\"}]}'\",\n \"若确认模型无误:检查是否多传了该服务不支持的参数(如推理强度 reasoning/effort),可用 /connect 把推理强度设为「默认」\",\n );\n }\n\n return hints;\n}\n\n/** LLM 请求超时(毫秒):OPENAI_TIMEOUT_MS,默认 10 分钟 */\nexport function llmTimeoutMs(): number {\n const n = Number(process.env.OPENAI_TIMEOUT_MS);\n return Number.isFinite(n) && n > 0 ? n : 600_000;\n}\n\n/**\n * LLM 流空闲超时(毫秒):OPENAI_STREAM_IDLE_MS,默认 2 分钟。\n *\n * 上面的 `llmTimeoutMs()` 走 SDK 的 timeout,而 SDK 在**响应头返回后**就清掉了\n * 计时器 —— 响应体的流式读取阶段没有任何超时保护。网关静默断供(既不推数据\n * 也不关连接)时,引擎会永久挂在 `for await` 上,会话状态卡在 running。\n * 故对两次 chunk 之间的间隔单独设一道看门狗。\n */\nexport function llmStreamIdleMs(): number {\n const n = Number(process.env.OPENAI_STREAM_IDLE_MS);\n return Number.isFinite(n) && n > 0 ? n : 120_000;\n}\n\n// ===================== 错误体归一化(fetch 层) =====================\n\ntype FetchLike = (input: any, init?: any) => Promise<any>;\n\n/** 把 Headers 转成普通对象(不同 fetch 实现的 Headers 都能遍历) */\nfunction headerRecord(h: unknown): Record<string, string> | undefined {\n if (!h) return undefined;\n const out: Record<string, string> = {};\n try {\n const iter =\n (h as any).entries?.() ?? ((h as any)[Symbol.iterator]?.() as any);\n if (iter) for (const [k, v] of iter) out[String(k)] = String(v);\n } catch {\n // 不可遍历的 Headers:放弃(新 Response 用默认头)\n }\n return Object.keys(out).length > 0 ? out : undefined;\n}\n\n/**\n * 包装 fetch:把**非标准形状的错误响应体**归一化成 OpenAI SDK 认识的结构。\n *\n * SDK 的 `APIError.generate` 只读 `body.error`,而阿里云百炼兼容模式的\n * `/chat/completions` 错误体是顶层 `{code, message, request_id}`(没有\n * `error` 包裹),于是 SDK 抛出的 message 变成\n * `400 status code (no body)` —— 真实原因(InvalidParameter / 模型不可用)\n * 被彻底吞掉,降级判定与排查建议全部失效。这里在读到非 2xx 时补一层\n * `{error:{...}}`,让下游(分类、日志、CLI 提示)拿到原始信息。\n */\nexport function normalizeErrorFetch(inner: FetchLike): FetchLike {\n return async (input, init) => {\n const res = await inner(input, init);\n if (res?.ok) return res;\n // 204/205/304 不允许带 body(Response 构造会抛),也没有错误信息可读\n if (res?.status === 204 || res?.status === 205 || res?.status === 304) return res;\n\n let text: string;\n try {\n text = await res.text();\n } catch {\n return res; // 读不动就原样交回(SDK 会按连接错误处理)\n }\n\n let out = text;\n try {\n const body = JSON.parse(text) as Record<string, unknown> | null;\n const std = body && typeof body === \"object\" && !(\"error\" in body) &&\n (body.message != null || body.code != null);\n if (std && body) {\n out = JSON.stringify({\n error: {\n message: String(body.message ?? `HTTP ${res.status}`),\n code: body.code ?? null,\n type: body.type ?? \"invalid_request_error\",\n param: body.param ?? null,\n },\n request_id: body.request_id ?? null,\n });\n }\n } catch {\n // 非 JSON 错误体(网关 HTML 等):保持原文,让 SDK 至少带上状态码\n }\n\n return new Response(out, {\n status: res.status,\n statusText: res.statusText,\n headers: headerRecord(res.headers),\n });\n };\n}\n\n/** LLM SDK 传输层重试次数:OPENAI_MAX_RETRIES,默认 3 */\nexport function llmMaxRetries(): number {\n const n = Number(process.env.OPENAI_MAX_RETRIES);\n return Number.isFinite(n) && n >= 0 ? n : 3;\n}\n"],"mappings":"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","names":[]}
|
|
1
|
+
{"version":3,"sources":["../../../src/providers/diagnostics.ts"],"sourcesContent":["/**\n * LLM 连接诊断与配置归一化\n *\n * 背景:CLI 在部分环境(如 openEuler 等国产 Linux、经企业网关/代理的网络)会\n * 只看到 `Connection error.`(OpenAI SDK 的 APIConnectionError),无法判断是\n * DNS、TLS、代理还是配置问题。这里提供:\n * - `cleanLlmValue` / `normalizeBaseURL`:清理 CRLF/引号/BOM、补协议、\n * 去掉 `/chat/completions` 之类的端点后缀(配置从其它系统/编辑器带进来时常见)\n * - `enrichLlmError`:把「底层 errno(沿 cause 链递归,穿透 SDK 包装)\n * + 实际请求端点」补进错误信息\n * - `networkErrorHints`:按错误给出可执行的排查建议(CLI 直接打印)\n */\n\n/** 是否连接/网络类错误(用于自动重试与提示) */\nexport function isNetworkErrorText(text: string): boolean {\n const t = text.toLowerCase();\n return [\n \"connection error\",\n \"fetch failed\",\n \"econnreset\",\n \"econnrefused\",\n \"etimedout\",\n \"ehostunreach\",\n \"enetunreach\",\n \"enotfound\",\n \"eai_again\",\n \"socket hang up\",\n \"und_err\",\n \"network\",\n \"timed out\",\n \"tls\",\n \"certificate\",\n \"unable_to_verify\",\n \"self signed\",\n ].some((m) => t.includes(m));\n}\n\n/**\n * 清理从配置文件/环境变量/粘贴进来的值:\n * BOM、首尾空白、换行与制表符、以及整体包裹的引号。\n * (Windows 记事本/CRLF、PowerShell `setx`、复制粘贴常见)\n */\nexport function cleanLlmValue(v?: string | null): string | undefined {\n if (v == null) return undefined;\n let s = String(v).replace(/^\\uFEFF/, \"\");\n s = s.replace(/[\\r\\n\\t]/g, \"\").trim();\n if (\n s.length >= 2 &&\n ((s.startsWith('\"') && s.endsWith('\"')) || (s.startsWith(\"'\") && s.endsWith(\"'\")))\n ) {\n s = s.slice(1, -1).trim();\n }\n return s || undefined;\n}\n\n/**\n * 规范化 Base URL:\n * - 缺协议时补 `https://`\n * - 去掉末尾 `/`\n * - 去掉误填的端点后缀(`/chat/completions`、`/responses`、`/completions`、`/models`)\n */\nexport function normalizeBaseURL(raw?: string | null): string | undefined {\n let s = cleanLlmValue(raw);\n if (!s) return undefined;\n if (!/^[a-z][a-z0-9+.-]*:\\/\\//i.test(s)) s = `https://${s}`;\n s = s.replace(/\\/+$/, \"\");\n s = s.replace(/\\/(chat\\/completions|completions|responses|models)$/i, \"\");\n return s.replace(/\\/+$/, \"\") || undefined;\n}\n\n/**\n * 是否为推理模型(o 系列 / gpt-5 等)。\n *\n * 这类模型在 Chat Completions 上**只接受** `max_completion_tokens`,传\n * `max_tokens` 会直接 400 `Unsupported parameter: 'max_tokens' is not\n * supported with this model`。Provider 与模型自检(`probeModel`)必须共用\n * 这一判断:一旦两处各写一份,自检就会把可用的推理模型误判成不可用。\n */\nexport function isReasoningModel(model: string): boolean {\n return /^(o\\d|gpt-5)/i.test(model);\n}\n\nfunction asRecord(v: unknown): Record<string, unknown> | null {\n return v && typeof v === \"object\" ? (v as Record<string, unknown>) : null;\n}\n\n/**\n * 展开错误链:`err` 本身 → `cause` → `cause.cause`…\n * 同时展开 `AggregateError.errors`(IPv4/IPv6 双栈都连不上时 undici 会这样抛)。\n * 带已访问集合,防止 cause 循环引用死循环。\n */\nfunction causeChain(err: unknown, maxNodes = 6): Record<string, unknown>[] {\n const out: Record<string, unknown>[] = [];\n const seen = new Set<unknown>();\n const queue: unknown[] = [err];\n while (queue.length > 0 && out.length < maxNodes) {\n const cur = queue.shift();\n if (!cur || typeof cur !== \"object\" || seen.has(cur)) continue;\n seen.add(cur);\n const rec = asRecord(cur);\n if (!rec) continue;\n out.push(rec);\n if (rec[\"cause\"]) queue.push(rec[\"cause\"]);\n const errs = rec[\"errors\"];\n if (Array.isArray(errs)) queue.push(...errs);\n }\n return out;\n}\n\n/**\n * 形如错误码的 name(`ECONNRESET`、`UND_ERR_CONNECT_TIMEOUT`)。\n * 内建/包装类的 name(`TypeError`、`Error`、`APIConnectionError`)不是错误码,\n * 混进 `code=` 只会误导排查方向。\n */\nfunction isErrorCodeLike(v: string): boolean {\n return /^[A-Z][A-Z0-9_]*$/.test(v);\n}\n\n/**\n * 自顶向下穿透包装层,取第一个可用的错误码(如 undici 的 ECONNRESET /\n * UND_ERR_* / ENOTFOUND)。\n *\n * OpenAI SDK 在连接失败时抛出的是\n * `APIConnectionError(\"Connection error.\")` → `cause: TypeError(\"fetch failed\")`\n * → `cause: Error(\"getaddrinfo ENOTFOUND ...\")`,真正的 errno 在最深处。\n * 只看一层只会拿到 `TypeError`,等于没有信息。\n */\nexport function errorCode(err: unknown): string | undefined {\n for (const src of causeChain(err)) {\n for (const k of [\"code\", \"errno\"]) {\n const v = src[k];\n if (typeof v === \"string\" && v) return v;\n }\n const name = src[\"name\"];\n if (typeof name === \"string\" && name !== \"Error\" && isErrorCodeLike(name)) {\n return name;\n }\n }\n return undefined;\n}\n\n/** 从 SDK 错误对象里取供应商原始错误体(OpenAI SDK 挂在 `error` 属性上) */\nfunction errorBody(err: unknown): Record<string, unknown> | null {\n return asRecord(asRecord(err)?.[\"error\"]);\n}\n\n/** 「端点 + 底层原因」后缀(无可用信息时返回空串) */\nexport function llmErrorContext(err: unknown, baseURL?: string): string {\n const parts: string[] = [];\n const code = errorCode(err);\n if (code) parts.push(`code=${code}`);\n const msg = err instanceof Error ? err.message : String(err ?? \"\");\n // 优先取「错误码所在那一层」的 message,保证 code 与文字描述同源;\n // 该层没有 message 时再取链路最深层(如 fetch failed 之下的\n // \"getaddrinfo ENOTFOUND ...\"),而不是停在 TypeError 的 \"fetch failed\"。\n const chain = causeChain(err);\n const codeSrc = chain.find(\n (c) => typeof c[\"code\"] === \"string\" || typeof c[\"errno\"] === \"string\",\n );\n const pickMsg = (c?: Record<string, unknown>): string | undefined => {\n const m = c?.[\"message\"];\n return typeof m === \"string\" && m && m !== msg ? m : undefined;\n };\n const causeMsg =\n pickMsg(codeSrc) ??\n [...chain].reverse().map(pickMsg).find((m): m is string => !!m) ??\n \"\";\n if (causeMsg) parts.push(causeMsg.slice(0, 160));\n // 供应商错误体里的真实原因(经 normalizeErrorFetch 归一化后可读到):\n // 形状非标准的网关 SDK 只会给出一句 \"400 status code (no body)\"\n const body = errorBody(err);\n const bodyMsg = typeof body?.[\"message\"] === \"string\" ? body[\"message\"] : \"\";\n if (bodyMsg && !msg.includes(bodyMsg)) parts.push(`详情: ${bodyMsg.slice(0, 200)}`);\n const bodyCode = typeof body?.[\"code\"] === \"string\" ? body[\"code\"] : \"\";\n if (bodyCode && !parts.some((p) => p.includes(bodyCode))) {\n parts.push(`code=${bodyCode}`);\n }\n const rid =\n (typeof err === \"object\" && err !== null\n ? String((err as { request_id?: unknown }).request_id ?? \"\")\n : \"\") ||\n (typeof body?.[\"request_id\"] === \"string\" ? body[\"request_id\"] : \"\");\n if (rid) parts.push(`request_id=${rid}`);\n if (baseURL) parts.push(`端点 ${baseURL}`);\n return parts.length > 0 ? `(${parts.join(\" · \")})` : \"\";\n}\n\n/**\n * 就地把「端点 + 底层原因」补进错误信息(保留原错误对象类型/status,\n * 便于上层继续按类型分支,如 Responses 不支持时降级)。\n */\nexport function enrichLlmError(err: unknown, baseURL?: string): unknown {\n if (!(err instanceof Error)) return err;\n const ctx = llmErrorContext(err, baseURL);\n if (!ctx) return err;\n if (err.message.includes(\"端点 \") || err.message.includes(ctx)) return err;\n try {\n err.message = `${err.message}${ctx}`;\n } catch {\n // message 只读时忽略\n }\n return err;\n}\n\n/** 连接类错误 → 可执行的排查建议(非连接类返回空数组) */\nexport function networkErrorHints(message: string, errorType = \"\"): string[] {\n const text = `${message} ${errorType}`;\n if (!isNetworkErrorText(text)) return [];\n const hints: string[] = [\n \"排查:① 网络是否可达:curl -sI <BaseURL>/models(或 /connect 重测)\",\n \"② 需要代理时:SOCKS5_PROXY=socks5://127.0.0.1:7890(HTTP_PROXY 不支持)\",\n \"③ 企业网关/证书拦截:NODE_EXTRA_CA_CERTS=/path/to/ca.pem\",\n ];\n if (/enotfound|eai_again|getaddrinfo/i.test(text)) {\n hints.push(\"④ DNS 解析失败:检查 /etc/resolv.conf,或改用 IP/内网域名\");\n } else if (/etimedout|econnrefused|unreachable/i.test(text)) {\n hints.push(\"④ 连接超时/被拒:检查防火墙出网策略与端口(443)\");\n } else if (/certificate|unable_to_verify|self signed|tls/i.test(text)) {\n hints.push(\"④ 证书链问题:为 Node 指定系统 CA(NODE_EXTRA_CA_CERTS)或补齐 ca-certificates\");\n } else if (/econnreset|socket hang up/i.test(text)) {\n hints.push(\"④ 连接被重置:多为链路/网关抖动,已自动重试;可调大 NETWORK_RETRY_MAX\");\n }\n hints.push(\"⑤ IPv6 抖动可试:NODE_OPTIONS=--dns-result-order=ipv4first\");\n return hints;\n}\n\n/**\n * 「凭据无效」类错误的排查建议(非该类错误返回空数组)。\n *\n * 与 `paramErrorHints` 分开:401 不是模型问题,混进去会让「模型类提示」\n * 在鉴权失败时也冒出来,反而误导。这里只解释「Key 为什么失效、去哪儿换」。\n *\n * 内置模型(志前 API)的 Key 由平台托管:用户在控制台重置 Key 后本机这份\n * 立即失效,必须引导去点「刷新内置模型」,否则会一直看到 401 以为节点坏了。\n */\nexport function authErrorHints(message: string, code = \"\"): string[] {\n const text = `${message} ${code}`;\n if (!/401|403|unauthorized|forbidden|invalid[_\\s]*api[_\\s]*key|api[\\s_-]*key\\s*(?:无效|invalid)|管理令牌无效/i.test(text)) {\n return [];\n }\n return [\n \"凭据无效:若用的是「志前大模型(内置)」,在节点设置的模型页点「刷新内置模型」重新领取 Key\" +\n \"(在控制台重置过 Key 后需要这一步);若是自填的供应商,请检查 Key 是否正确或已过期\",\n ];\n}\n\n/**\n * 「工具调用没有对应结果」这一协议错误的排查建议。\n *\n * 历史里若存在「assistant 带 tool_calls、却没有对应 tool 消息」的记录,整个请求\n * 会被供应商原样拒绝(400 insufficient tool messages following tool_calls\n * message)。这不是额度或模型问题——历史多为上次在工具执行中被中断 / 进程被杀\n * 时留下的。引擎在每轮开始与发送前都会自动修复,这里只负责把原因讲清楚,\n * 避免用户按「未开通/欠费」的错误提示去充值。\n */\nexport function toolCallHistoryHints(message: string, code = \"\"): string[] {\n const text = `${message} ${code}`;\n if (\n !/tool_calls|tool[_ ]?call[_ ]?id/i.test(text) ||\n !/must be followed by tool messages|insufficient tool messages|no tool output found|tool messages responding/i.test(\n text,\n )\n ) {\n return [];\n }\n return [\n \"这不是模型或余额问题:会话历史里有一条「工具调用没有拿到结果」的记录(通常是上次在工具执行中按了 Ctrl+C,或进程被中断)\",\n \"引擎已自动清理并重试;如果仍报同样的错误,用 /new 开一个新会话即可(旧会话的历史里还留着这条记录)\",\n ];\n}\n\n/**\n * 「模型/参数不被接受」类错误的排查建议(非该类错误返回空数组)。\n *\n * 典型场景(均为阿里兼容模式实测):\n * - 选到非对话/专用通道模型(`cmaas-*`、向量/语音/图像…)\n * → `InvalidParameter: Required parameter(s) missing or invalid`\n * - 模型在该账号下没开通\n * → `invalid_parameter_error: The product is not activated`\n * 这两句原文对排查毫无帮助,必须翻成可执行的下一步。\n */\nexport function paramErrorHints(message: string, code = \"\"): string[] {\n const text = `${message} ${code}`;\n const hints: string[] = [];\n\n /*\n * 一、账号/开通类\n *\n * 只认「额度 / 开通 / 计费」类措辞。`insufficient` 单独出现多半是别的语义\n * (如 `insufficient tool messages following tool_calls message` 这条协议错误),\n * 把它当成欠费会给出完全错误的建议——用户会跑去充值,而真正的问题在会话历史。\n */\n if (\n /not[_\\s]*(activated|enabled|purchased)|arrearage|quota|subscription|billing|未开通|欠费|额度|余额不足|insufficient[_\\s]*(quota|balance|credit|funds)/i.test(\n text,\n )\n ) {\n hints.push(\n \"该模型在此 **API Key/账号** 下未开通(或账号欠费、额度用尽):到网关控制台开通该模型,或换一个已开通的模型\",\n );\n // 内置模型(志前 API)走的是用户自己的租户额度,额度用完是最常见的失败原因,\n // 必须给出「去哪儿充值」而不是让人对着 402 猜\n hints.push(\n \"如果你用的是「志前大模型(内置)」:到 https://hub.cd-zq.com 查看余额并充值;\" +\n \"也可以先切换到你自己配置的其它模型继续(节点设置 → 模型 → 设为默认)\",\n );\n }\n\n // 二、模型名/参数类\n if (\n /invalid[_\\s]*parameter|invalidparameter|required[_\\s]+parameter|missing[_\\s]+parameter|invalid[_\\s]*model|model[_\\s]*not[_\\s]*found|not[_\\s]*support|unsupported/i.test(\n text,\n )\n ) {\n hints.push(\n \"多半是**模型名不可用于对话接口**:网关 /models 列的是全量目录,含向量(embedding)、重排(rerank)、语音(asr/tts)、实时(realtime)、图像(image/wan)、专用通道(cmaas-*) 等,这些模型不能当 Agent 后端\",\n \"换一个对话模型:/connect 重新选择(对话模型已排在列表前面),或 /models 查看\",\n \"手动验证某个模型能不能用:\\n\" +\n \" curl -s <BaseURL>/chat/completions -H 'Authorization: Bearer $KEY' -H 'content-type: application/json' \\\\\\n\" +\n \" -d '{\\\"model\\\":\\\"<模型名>\\\",\\\"messages\\\":[{\\\"role\\\":\\\"user\\\",\\\"content\\\":\\\"hi\\\"}]}'\",\n \"若确认模型无误:检查是否多传了该服务不支持的参数(如推理强度 reasoning/effort),可用 /connect 把推理强度设为「默认」\",\n );\n }\n\n return hints;\n}\n\n/** LLM 请求超时(毫秒):OPENAI_TIMEOUT_MS,默认 10 分钟 */\nexport function llmTimeoutMs(): number {\n const n = Number(process.env.OPENAI_TIMEOUT_MS);\n return Number.isFinite(n) && n > 0 ? n : 600_000;\n}\n\n/**\n * LLM 流空闲超时(毫秒):OPENAI_STREAM_IDLE_MS,默认 2 分钟。\n *\n * 上面的 `llmTimeoutMs()` 走 SDK 的 timeout,而 SDK 在**响应头返回后**就清掉了\n * 计时器 —— 响应体的流式读取阶段没有任何超时保护。网关静默断供(既不推数据\n * 也不关连接)时,引擎会永久挂在 `for await` 上,会话状态卡在 running。\n * 故对两次 chunk 之间的间隔单独设一道看门狗。\n */\nexport function llmStreamIdleMs(): number {\n const n = Number(process.env.OPENAI_STREAM_IDLE_MS);\n return Number.isFinite(n) && n > 0 ? n : 120_000;\n}\n\n// ===================== 错误体归一化(fetch 层) =====================\n\ntype FetchLike = (input: any, init?: any) => Promise<any>;\n\n/** 把 Headers 转成普通对象(不同 fetch 实现的 Headers 都能遍历) */\nfunction headerRecord(h: unknown): Record<string, string> | undefined {\n if (!h) return undefined;\n const out: Record<string, string> = {};\n try {\n const iter =\n (h as any).entries?.() ?? ((h as any)[Symbol.iterator]?.() as any);\n if (iter) for (const [k, v] of iter) out[String(k)] = String(v);\n } catch {\n // 不可遍历的 Headers:放弃(新 Response 用默认头)\n }\n return Object.keys(out).length > 0 ? out : undefined;\n}\n\n/**\n * 包装 fetch:把**非标准形状的错误响应体**归一化成 OpenAI SDK 认识的结构。\n *\n * SDK 的 `APIError.generate` 只读 `body.error`,而阿里云百炼兼容模式的\n * `/chat/completions` 错误体是顶层 `{code, message, request_id}`(没有\n * `error` 包裹),于是 SDK 抛出的 message 变成\n * `400 status code (no body)` —— 真实原因(InvalidParameter / 模型不可用)\n * 被彻底吞掉,降级判定与排查建议全部失效。这里在读到非 2xx 时补一层\n * `{error:{...}}`,让下游(分类、日志、CLI 提示)拿到原始信息。\n */\nexport function normalizeErrorFetch(inner: FetchLike): FetchLike {\n return async (input, init) => {\n const res = await inner(input, init);\n if (res?.ok) return res;\n // 204/205/304 不允许带 body(Response 构造会抛),也没有错误信息可读\n if (res?.status === 204 || res?.status === 205 || res?.status === 304) return res;\n\n let text: string;\n try {\n text = await res.text();\n } catch {\n return res; // 读不动就原样交回(SDK 会按连接错误处理)\n }\n\n let out = text;\n try {\n const body = JSON.parse(text) as Record<string, unknown> | null;\n const std = body && typeof body === \"object\" && !(\"error\" in body) &&\n (body.message != null || body.code != null);\n if (std && body) {\n out = JSON.stringify({\n error: {\n message: String(body.message ?? `HTTP ${res.status}`),\n code: body.code ?? null,\n type: body.type ?? \"invalid_request_error\",\n param: body.param ?? null,\n },\n request_id: body.request_id ?? null,\n });\n }\n } catch {\n // 非 JSON 错误体(网关 HTML 等):保持原文,让 SDK 至少带上状态码\n }\n\n return new Response(out, {\n status: res.status,\n statusText: res.statusText,\n headers: headerRecord(res.headers),\n });\n };\n}\n\n/** LLM SDK 传输层重试次数:OPENAI_MAX_RETRIES,默认 3 */\nexport function llmMaxRetries(): number {\n const n = Number(process.env.OPENAI_MAX_RETRIES);\n return Number.isFinite(n) && n >= 0 ? n : 3;\n}\n"],"mappings":"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","names":[]}
|
|
@@ -63,6 +63,16 @@ declare function networkErrorHints(message: string, errorType?: string): string[
|
|
|
63
63
|
* 立即失效,必须引导去点「刷新内置模型」,否则会一直看到 401 以为节点坏了。
|
|
64
64
|
*/
|
|
65
65
|
declare function authErrorHints(message: string, code?: string): string[];
|
|
66
|
+
/**
|
|
67
|
+
* 「工具调用没有对应结果」这一协议错误的排查建议。
|
|
68
|
+
*
|
|
69
|
+
* 历史里若存在「assistant 带 tool_calls、却没有对应 tool 消息」的记录,整个请求
|
|
70
|
+
* 会被供应商原样拒绝(400 insufficient tool messages following tool_calls
|
|
71
|
+
* message)。这不是额度或模型问题——历史多为上次在工具执行中被中断 / 进程被杀
|
|
72
|
+
* 时留下的。引擎在每轮开始与发送前都会自动修复,这里只负责把原因讲清楚,
|
|
73
|
+
* 避免用户按「未开通/欠费」的错误提示去充值。
|
|
74
|
+
*/
|
|
75
|
+
declare function toolCallHistoryHints(message: string, code?: string): string[];
|
|
66
76
|
/**
|
|
67
77
|
* 「模型/参数不被接受」类错误的排查建议(非该类错误返回空数组)。
|
|
68
78
|
*
|
|
@@ -100,4 +110,4 @@ declare function normalizeErrorFetch(inner: FetchLike): FetchLike;
|
|
|
100
110
|
/** LLM SDK 传输层重试次数:OPENAI_MAX_RETRIES,默认 3 */
|
|
101
111
|
declare function llmMaxRetries(): number;
|
|
102
112
|
|
|
103
|
-
export { authErrorHints, cleanLlmValue, enrichLlmError, errorCode, isNetworkErrorText, isReasoningModel, llmErrorContext, llmMaxRetries, llmStreamIdleMs, llmTimeoutMs, networkErrorHints, normalizeBaseURL, normalizeErrorFetch, paramErrorHints };
|
|
113
|
+
export { authErrorHints, cleanLlmValue, enrichLlmError, errorCode, isNetworkErrorText, isReasoningModel, llmErrorContext, llmMaxRetries, llmStreamIdleMs, llmTimeoutMs, networkErrorHints, normalizeBaseURL, normalizeErrorFetch, paramErrorHints, toolCallHistoryHints };
|
|
@@ -63,6 +63,16 @@ declare function networkErrorHints(message: string, errorType?: string): string[
|
|
|
63
63
|
* 立即失效,必须引导去点「刷新内置模型」,否则会一直看到 401 以为节点坏了。
|
|
64
64
|
*/
|
|
65
65
|
declare function authErrorHints(message: string, code?: string): string[];
|
|
66
|
+
/**
|
|
67
|
+
* 「工具调用没有对应结果」这一协议错误的排查建议。
|
|
68
|
+
*
|
|
69
|
+
* 历史里若存在「assistant 带 tool_calls、却没有对应 tool 消息」的记录,整个请求
|
|
70
|
+
* 会被供应商原样拒绝(400 insufficient tool messages following tool_calls
|
|
71
|
+
* message)。这不是额度或模型问题——历史多为上次在工具执行中被中断 / 进程被杀
|
|
72
|
+
* 时留下的。引擎在每轮开始与发送前都会自动修复,这里只负责把原因讲清楚,
|
|
73
|
+
* 避免用户按「未开通/欠费」的错误提示去充值。
|
|
74
|
+
*/
|
|
75
|
+
declare function toolCallHistoryHints(message: string, code?: string): string[];
|
|
66
76
|
/**
|
|
67
77
|
* 「模型/参数不被接受」类错误的排查建议(非该类错误返回空数组)。
|
|
68
78
|
*
|
|
@@ -100,4 +110,4 @@ declare function normalizeErrorFetch(inner: FetchLike): FetchLike;
|
|
|
100
110
|
/** LLM SDK 传输层重试次数:OPENAI_MAX_RETRIES,默认 3 */
|
|
101
111
|
declare function llmMaxRetries(): number;
|
|
102
112
|
|
|
103
|
-
export { authErrorHints, cleanLlmValue, enrichLlmError, errorCode, isNetworkErrorText, isReasoningModel, llmErrorContext, llmMaxRetries, llmStreamIdleMs, llmTimeoutMs, networkErrorHints, normalizeBaseURL, normalizeErrorFetch, paramErrorHints };
|
|
113
|
+
export { authErrorHints, cleanLlmValue, enrichLlmError, errorCode, isNetworkErrorText, isReasoningModel, llmErrorContext, llmMaxRetries, llmStreamIdleMs, llmTimeoutMs, networkErrorHints, normalizeBaseURL, normalizeErrorFetch, paramErrorHints, toolCallHistoryHints };
|
|
@@ -147,10 +147,22 @@ function authErrorHints(message, code = "") {
|
|
|
147
147
|
"\u51ED\u636E\u65E0\u6548\uFF1A\u82E5\u7528\u7684\u662F\u300C\u5FD7\u524D\u5927\u6A21\u578B\uFF08\u5185\u7F6E\uFF09\u300D\uFF0C\u5728\u8282\u70B9\u8BBE\u7F6E\u7684\u6A21\u578B\u9875\u70B9\u300C\u5237\u65B0\u5185\u7F6E\u6A21\u578B\u300D\u91CD\u65B0\u9886\u53D6 Key\uFF08\u5728\u63A7\u5236\u53F0\u91CD\u7F6E\u8FC7 Key \u540E\u9700\u8981\u8FD9\u4E00\u6B65\uFF09\uFF1B\u82E5\u662F\u81EA\u586B\u7684\u4F9B\u5E94\u5546\uFF0C\u8BF7\u68C0\u67E5 Key \u662F\u5426\u6B63\u786E\u6216\u5DF2\u8FC7\u671F"
|
|
148
148
|
];
|
|
149
149
|
}
|
|
150
|
+
function toolCallHistoryHints(message, code = "") {
|
|
151
|
+
const text = `${message} ${code}`;
|
|
152
|
+
if (!/tool_calls|tool[_ ]?call[_ ]?id/i.test(text) || !/must be followed by tool messages|insufficient tool messages|no tool output found|tool messages responding/i.test(
|
|
153
|
+
text
|
|
154
|
+
)) {
|
|
155
|
+
return [];
|
|
156
|
+
}
|
|
157
|
+
return [
|
|
158
|
+
"\u8FD9\u4E0D\u662F\u6A21\u578B\u6216\u4F59\u989D\u95EE\u9898\uFF1A\u4F1A\u8BDD\u5386\u53F2\u91CC\u6709\u4E00\u6761\u300C\u5DE5\u5177\u8C03\u7528\u6CA1\u6709\u62FF\u5230\u7ED3\u679C\u300D\u7684\u8BB0\u5F55\uFF08\u901A\u5E38\u662F\u4E0A\u6B21\u5728\u5DE5\u5177\u6267\u884C\u4E2D\u6309\u4E86 Ctrl+C\uFF0C\u6216\u8FDB\u7A0B\u88AB\u4E2D\u65AD\uFF09",
|
|
159
|
+
"\u5F15\u64CE\u5DF2\u81EA\u52A8\u6E05\u7406\u5E76\u91CD\u8BD5\uFF1B\u5982\u679C\u4ECD\u62A5\u540C\u6837\u7684\u9519\u8BEF\uFF0C\u7528 /new \u5F00\u4E00\u4E2A\u65B0\u4F1A\u8BDD\u5373\u53EF\uFF08\u65E7\u4F1A\u8BDD\u7684\u5386\u53F2\u91CC\u8FD8\u7559\u7740\u8FD9\u6761\u8BB0\u5F55\uFF09"
|
|
160
|
+
];
|
|
161
|
+
}
|
|
150
162
|
function paramErrorHints(message, code = "") {
|
|
151
163
|
const text = `${message} ${code}`;
|
|
152
164
|
const hints = [];
|
|
153
|
-
if (/not[_\s]*(activated|enabled|purchased)|arrearage|quota|subscription|insufficient
|
|
165
|
+
if (/not[_\s]*(activated|enabled|purchased)|arrearage|quota|subscription|billing|未开通|欠费|额度|余额不足|insufficient[_\s]*(quota|balance|credit|funds)/i.test(
|
|
154
166
|
text
|
|
155
167
|
)) {
|
|
156
168
|
hints.push(
|
|
@@ -245,6 +257,7 @@ export {
|
|
|
245
257
|
networkErrorHints,
|
|
246
258
|
normalizeBaseURL,
|
|
247
259
|
normalizeErrorFetch,
|
|
248
|
-
paramErrorHints
|
|
260
|
+
paramErrorHints,
|
|
261
|
+
toolCallHistoryHints
|
|
249
262
|
};
|
|
250
263
|
//# sourceMappingURL=diagnostics.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../src/providers/diagnostics.ts"],"sourcesContent":["/**\n * LLM 连接诊断与配置归一化\n *\n * 背景:CLI 在部分环境(如 openEuler 等国产 Linux、经企业网关/代理的网络)会\n * 只看到 `Connection error.`(OpenAI SDK 的 APIConnectionError),无法判断是\n * DNS、TLS、代理还是配置问题。这里提供:\n * - `cleanLlmValue` / `normalizeBaseURL`:清理 CRLF/引号/BOM、补协议、\n * 去掉 `/chat/completions` 之类的端点后缀(配置从其它系统/编辑器带进来时常见)\n * - `enrichLlmError`:把「底层 errno(沿 cause 链递归,穿透 SDK 包装)\n * + 实际请求端点」补进错误信息\n * - `networkErrorHints`:按错误给出可执行的排查建议(CLI 直接打印)\n */\n\n/** 是否连接/网络类错误(用于自动重试与提示) */\nexport function isNetworkErrorText(text: string): boolean {\n const t = text.toLowerCase();\n return [\n \"connection error\",\n \"fetch failed\",\n \"econnreset\",\n \"econnrefused\",\n \"etimedout\",\n \"ehostunreach\",\n \"enetunreach\",\n \"enotfound\",\n \"eai_again\",\n \"socket hang up\",\n \"und_err\",\n \"network\",\n \"timed out\",\n \"tls\",\n \"certificate\",\n \"unable_to_verify\",\n \"self signed\",\n ].some((m) => t.includes(m));\n}\n\n/**\n * 清理从配置文件/环境变量/粘贴进来的值:\n * BOM、首尾空白、换行与制表符、以及整体包裹的引号。\n * (Windows 记事本/CRLF、PowerShell `setx`、复制粘贴常见)\n */\nexport function cleanLlmValue(v?: string | null): string | undefined {\n if (v == null) return undefined;\n let s = String(v).replace(/^\\uFEFF/, \"\");\n s = s.replace(/[\\r\\n\\t]/g, \"\").trim();\n if (\n s.length >= 2 &&\n ((s.startsWith('\"') && s.endsWith('\"')) || (s.startsWith(\"'\") && s.endsWith(\"'\")))\n ) {\n s = s.slice(1, -1).trim();\n }\n return s || undefined;\n}\n\n/**\n * 规范化 Base URL:\n * - 缺协议时补 `https://`\n * - 去掉末尾 `/`\n * - 去掉误填的端点后缀(`/chat/completions`、`/responses`、`/completions`、`/models`)\n */\nexport function normalizeBaseURL(raw?: string | null): string | undefined {\n let s = cleanLlmValue(raw);\n if (!s) return undefined;\n if (!/^[a-z][a-z0-9+.-]*:\\/\\//i.test(s)) s = `https://${s}`;\n s = s.replace(/\\/+$/, \"\");\n s = s.replace(/\\/(chat\\/completions|completions|responses|models)$/i, \"\");\n return s.replace(/\\/+$/, \"\") || undefined;\n}\n\n/**\n * 是否为推理模型(o 系列 / gpt-5 等)。\n *\n * 这类模型在 Chat Completions 上**只接受** `max_completion_tokens`,传\n * `max_tokens` 会直接 400 `Unsupported parameter: 'max_tokens' is not\n * supported with this model`。Provider 与模型自检(`probeModel`)必须共用\n * 这一判断:一旦两处各写一份,自检就会把可用的推理模型误判成不可用。\n */\nexport function isReasoningModel(model: string): boolean {\n return /^(o\\d|gpt-5)/i.test(model);\n}\n\nfunction asRecord(v: unknown): Record<string, unknown> | null {\n return v && typeof v === \"object\" ? (v as Record<string, unknown>) : null;\n}\n\n/**\n * 展开错误链:`err` 本身 → `cause` → `cause.cause`…\n * 同时展开 `AggregateError.errors`(IPv4/IPv6 双栈都连不上时 undici 会这样抛)。\n * 带已访问集合,防止 cause 循环引用死循环。\n */\nfunction causeChain(err: unknown, maxNodes = 6): Record<string, unknown>[] {\n const out: Record<string, unknown>[] = [];\n const seen = new Set<unknown>();\n const queue: unknown[] = [err];\n while (queue.length > 0 && out.length < maxNodes) {\n const cur = queue.shift();\n if (!cur || typeof cur !== \"object\" || seen.has(cur)) continue;\n seen.add(cur);\n const rec = asRecord(cur);\n if (!rec) continue;\n out.push(rec);\n if (rec[\"cause\"]) queue.push(rec[\"cause\"]);\n const errs = rec[\"errors\"];\n if (Array.isArray(errs)) queue.push(...errs);\n }\n return out;\n}\n\n/**\n * 形如错误码的 name(`ECONNRESET`、`UND_ERR_CONNECT_TIMEOUT`)。\n * 内建/包装类的 name(`TypeError`、`Error`、`APIConnectionError`)不是错误码,\n * 混进 `code=` 只会误导排查方向。\n */\nfunction isErrorCodeLike(v: string): boolean {\n return /^[A-Z][A-Z0-9_]*$/.test(v);\n}\n\n/**\n * 自顶向下穿透包装层,取第一个可用的错误码(如 undici 的 ECONNRESET /\n * UND_ERR_* / ENOTFOUND)。\n *\n * OpenAI SDK 在连接失败时抛出的是\n * `APIConnectionError(\"Connection error.\")` → `cause: TypeError(\"fetch failed\")`\n * → `cause: Error(\"getaddrinfo ENOTFOUND ...\")`,真正的 errno 在最深处。\n * 只看一层只会拿到 `TypeError`,等于没有信息。\n */\nexport function errorCode(err: unknown): string | undefined {\n for (const src of causeChain(err)) {\n for (const k of [\"code\", \"errno\"]) {\n const v = src[k];\n if (typeof v === \"string\" && v) return v;\n }\n const name = src[\"name\"];\n if (typeof name === \"string\" && name !== \"Error\" && isErrorCodeLike(name)) {\n return name;\n }\n }\n return undefined;\n}\n\n/** 从 SDK 错误对象里取供应商原始错误体(OpenAI SDK 挂在 `error` 属性上) */\nfunction errorBody(err: unknown): Record<string, unknown> | null {\n return asRecord(asRecord(err)?.[\"error\"]);\n}\n\n/** 「端点 + 底层原因」后缀(无可用信息时返回空串) */\nexport function llmErrorContext(err: unknown, baseURL?: string): string {\n const parts: string[] = [];\n const code = errorCode(err);\n if (code) parts.push(`code=${code}`);\n const msg = err instanceof Error ? err.message : String(err ?? \"\");\n // 优先取「错误码所在那一层」的 message,保证 code 与文字描述同源;\n // 该层没有 message 时再取链路最深层(如 fetch failed 之下的\n // \"getaddrinfo ENOTFOUND ...\"),而不是停在 TypeError 的 \"fetch failed\"。\n const chain = causeChain(err);\n const codeSrc = chain.find(\n (c) => typeof c[\"code\"] === \"string\" || typeof c[\"errno\"] === \"string\",\n );\n const pickMsg = (c?: Record<string, unknown>): string | undefined => {\n const m = c?.[\"message\"];\n return typeof m === \"string\" && m && m !== msg ? m : undefined;\n };\n const causeMsg =\n pickMsg(codeSrc) ??\n [...chain].reverse().map(pickMsg).find((m): m is string => !!m) ??\n \"\";\n if (causeMsg) parts.push(causeMsg.slice(0, 160));\n // 供应商错误体里的真实原因(经 normalizeErrorFetch 归一化后可读到):\n // 形状非标准的网关 SDK 只会给出一句 \"400 status code (no body)\"\n const body = errorBody(err);\n const bodyMsg = typeof body?.[\"message\"] === \"string\" ? body[\"message\"] : \"\";\n if (bodyMsg && !msg.includes(bodyMsg)) parts.push(`详情: ${bodyMsg.slice(0, 200)}`);\n const bodyCode = typeof body?.[\"code\"] === \"string\" ? body[\"code\"] : \"\";\n if (bodyCode && !parts.some((p) => p.includes(bodyCode))) {\n parts.push(`code=${bodyCode}`);\n }\n const rid =\n (typeof err === \"object\" && err !== null\n ? String((err as { request_id?: unknown }).request_id ?? \"\")\n : \"\") ||\n (typeof body?.[\"request_id\"] === \"string\" ? body[\"request_id\"] : \"\");\n if (rid) parts.push(`request_id=${rid}`);\n if (baseURL) parts.push(`端点 ${baseURL}`);\n return parts.length > 0 ? `(${parts.join(\" · \")})` : \"\";\n}\n\n/**\n * 就地把「端点 + 底层原因」补进错误信息(保留原错误对象类型/status,\n * 便于上层继续按类型分支,如 Responses 不支持时降级)。\n */\nexport function enrichLlmError(err: unknown, baseURL?: string): unknown {\n if (!(err instanceof Error)) return err;\n const ctx = llmErrorContext(err, baseURL);\n if (!ctx) return err;\n if (err.message.includes(\"端点 \") || err.message.includes(ctx)) return err;\n try {\n err.message = `${err.message}${ctx}`;\n } catch {\n // message 只读时忽略\n }\n return err;\n}\n\n/** 连接类错误 → 可执行的排查建议(非连接类返回空数组) */\nexport function networkErrorHints(message: string, errorType = \"\"): string[] {\n const text = `${message} ${errorType}`;\n if (!isNetworkErrorText(text)) return [];\n const hints: string[] = [\n \"排查:① 网络是否可达:curl -sI <BaseURL>/models(或 /connect 重测)\",\n \"② 需要代理时:SOCKS5_PROXY=socks5://127.0.0.1:7890(HTTP_PROXY 不支持)\",\n \"③ 企业网关/证书拦截:NODE_EXTRA_CA_CERTS=/path/to/ca.pem\",\n ];\n if (/enotfound|eai_again|getaddrinfo/i.test(text)) {\n hints.push(\"④ DNS 解析失败:检查 /etc/resolv.conf,或改用 IP/内网域名\");\n } else if (/etimedout|econnrefused|unreachable/i.test(text)) {\n hints.push(\"④ 连接超时/被拒:检查防火墙出网策略与端口(443)\");\n } else if (/certificate|unable_to_verify|self signed|tls/i.test(text)) {\n hints.push(\"④ 证书链问题:为 Node 指定系统 CA(NODE_EXTRA_CA_CERTS)或补齐 ca-certificates\");\n } else if (/econnreset|socket hang up/i.test(text)) {\n hints.push(\"④ 连接被重置:多为链路/网关抖动,已自动重试;可调大 NETWORK_RETRY_MAX\");\n }\n hints.push(\"⑤ IPv6 抖动可试:NODE_OPTIONS=--dns-result-order=ipv4first\");\n return hints;\n}\n\n/**\n * 「凭据无效」类错误的排查建议(非该类错误返回空数组)。\n *\n * 与 `paramErrorHints` 分开:401 不是模型问题,混进去会让「模型类提示」\n * 在鉴权失败时也冒出来,反而误导。这里只解释「Key 为什么失效、去哪儿换」。\n *\n * 内置模型(志前 API)的 Key 由平台托管:用户在控制台重置 Key 后本机这份\n * 立即失效,必须引导去点「刷新内置模型」,否则会一直看到 401 以为节点坏了。\n */\nexport function authErrorHints(message: string, code = \"\"): string[] {\n const text = `${message} ${code}`;\n if (!/401|403|unauthorized|forbidden|invalid[_\\s]*api[_\\s]*key|api[\\s_-]*key\\s*(?:无效|invalid)|管理令牌无效/i.test(text)) {\n return [];\n }\n return [\n \"凭据无效:若用的是「志前大模型(内置)」,在节点设置的模型页点「刷新内置模型」重新领取 Key\" +\n \"(在控制台重置过 Key 后需要这一步);若是自填的供应商,请检查 Key 是否正确或已过期\",\n ];\n}\n\n/**\n * 「模型/参数不被接受」类错误的排查建议(非该类错误返回空数组)。\n *\n * 典型场景(均为阿里兼容模式实测):\n * - 选到非对话/专用通道模型(`cmaas-*`、向量/语音/图像…)\n * → `InvalidParameter: Required parameter(s) missing or invalid`\n * - 模型在该账号下没开通\n * → `invalid_parameter_error: The product is not activated`\n * 这两句原文对排查毫无帮助,必须翻成可执行的下一步。\n */\nexport function paramErrorHints(message: string, code = \"\"): string[] {\n const text = `${message} ${code}`;\n const hints: string[] = [];\n\n // 一、账号/开通类\n if (\n /not[_\\s]*(activated|enabled|purchased)|arrearage|quota|subscription|insufficient|未开通|欠费|额度|余额不足/i.test(\n text,\n )\n ) {\n hints.push(\n \"该模型在此 **API Key/账号** 下未开通(或账号欠费、额度用尽):到网关控制台开通该模型,或换一个已开通的模型\",\n );\n // 内置模型(志前 API)走的是用户自己的租户额度,额度用完是最常见的失败原因,\n // 必须给出「去哪儿充值」而不是让人对着 402 猜\n hints.push(\n \"如果你用的是「志前大模型(内置)」:到 https://hub.cd-zq.com 查看余额并充值;\" +\n \"也可以先切换到你自己配置的其它模型继续(节点设置 → 模型 → 设为默认)\",\n );\n }\n\n // 二、模型名/参数类\n if (\n /invalid[_\\s]*parameter|invalidparameter|required[_\\s]+parameter|missing[_\\s]+parameter|invalid[_\\s]*model|model[_\\s]*not[_\\s]*found|not[_\\s]*support|unsupported/i.test(\n text,\n )\n ) {\n hints.push(\n \"多半是**模型名不可用于对话接口**:网关 /models 列的是全量目录,含向量(embedding)、重排(rerank)、语音(asr/tts)、实时(realtime)、图像(image/wan)、专用通道(cmaas-*) 等,这些模型不能当 Agent 后端\",\n \"换一个对话模型:/connect 重新选择(对话模型已排在列表前面),或 /models 查看\",\n \"手动验证某个模型能不能用:\\n\" +\n \" curl -s <BaseURL>/chat/completions -H 'Authorization: Bearer $KEY' -H 'content-type: application/json' \\\\\\n\" +\n \" -d '{\\\"model\\\":\\\"<模型名>\\\",\\\"messages\\\":[{\\\"role\\\":\\\"user\\\",\\\"content\\\":\\\"hi\\\"}]}'\",\n \"若确认模型无误:检查是否多传了该服务不支持的参数(如推理强度 reasoning/effort),可用 /connect 把推理强度设为「默认」\",\n );\n }\n\n return hints;\n}\n\n/** LLM 请求超时(毫秒):OPENAI_TIMEOUT_MS,默认 10 分钟 */\nexport function llmTimeoutMs(): number {\n const n = Number(process.env.OPENAI_TIMEOUT_MS);\n return Number.isFinite(n) && n > 0 ? n : 600_000;\n}\n\n/**\n * LLM 流空闲超时(毫秒):OPENAI_STREAM_IDLE_MS,默认 2 分钟。\n *\n * 上面的 `llmTimeoutMs()` 走 SDK 的 timeout,而 SDK 在**响应头返回后**就清掉了\n * 计时器 —— 响应体的流式读取阶段没有任何超时保护。网关静默断供(既不推数据\n * 也不关连接)时,引擎会永久挂在 `for await` 上,会话状态卡在 running。\n * 故对两次 chunk 之间的间隔单独设一道看门狗。\n */\nexport function llmStreamIdleMs(): number {\n const n = Number(process.env.OPENAI_STREAM_IDLE_MS);\n return Number.isFinite(n) && n > 0 ? n : 120_000;\n}\n\n// ===================== 错误体归一化(fetch 层) =====================\n\ntype FetchLike = (input: any, init?: any) => Promise<any>;\n\n/** 把 Headers 转成普通对象(不同 fetch 实现的 Headers 都能遍历) */\nfunction headerRecord(h: unknown): Record<string, string> | undefined {\n if (!h) return undefined;\n const out: Record<string, string> = {};\n try {\n const iter =\n (h as any).entries?.() ?? ((h as any)[Symbol.iterator]?.() as any);\n if (iter) for (const [k, v] of iter) out[String(k)] = String(v);\n } catch {\n // 不可遍历的 Headers:放弃(新 Response 用默认头)\n }\n return Object.keys(out).length > 0 ? out : undefined;\n}\n\n/**\n * 包装 fetch:把**非标准形状的错误响应体**归一化成 OpenAI SDK 认识的结构。\n *\n * SDK 的 `APIError.generate` 只读 `body.error`,而阿里云百炼兼容模式的\n * `/chat/completions` 错误体是顶层 `{code, message, request_id}`(没有\n * `error` 包裹),于是 SDK 抛出的 message 变成\n * `400 status code (no body)` —— 真实原因(InvalidParameter / 模型不可用)\n * 被彻底吞掉,降级判定与排查建议全部失效。这里在读到非 2xx 时补一层\n * `{error:{...}}`,让下游(分类、日志、CLI 提示)拿到原始信息。\n */\nexport function normalizeErrorFetch(inner: FetchLike): FetchLike {\n return async (input, init) => {\n const res = await inner(input, init);\n if (res?.ok) return res;\n // 204/205/304 不允许带 body(Response 构造会抛),也没有错误信息可读\n if (res?.status === 204 || res?.status === 205 || res?.status === 304) return res;\n\n let text: string;\n try {\n text = await res.text();\n } catch {\n return res; // 读不动就原样交回(SDK 会按连接错误处理)\n }\n\n let out = text;\n try {\n const body = JSON.parse(text) as Record<string, unknown> | null;\n const std = body && typeof body === \"object\" && !(\"error\" in body) &&\n (body.message != null || body.code != null);\n if (std && body) {\n out = JSON.stringify({\n error: {\n message: String(body.message ?? `HTTP ${res.status}`),\n code: body.code ?? null,\n type: body.type ?? \"invalid_request_error\",\n param: body.param ?? null,\n },\n request_id: body.request_id ?? null,\n });\n }\n } catch {\n // 非 JSON 错误体(网关 HTML 等):保持原文,让 SDK 至少带上状态码\n }\n\n return new Response(out, {\n status: res.status,\n statusText: res.statusText,\n headers: headerRecord(res.headers),\n });\n };\n}\n\n/** LLM SDK 传输层重试次数:OPENAI_MAX_RETRIES,默认 3 */\nexport function llmMaxRetries(): number {\n const n = Number(process.env.OPENAI_MAX_RETRIES);\n return Number.isFinite(n) && n >= 0 ? n : 3;\n}\n"],"mappings":"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","names":[]}
|
|
1
|
+
{"version":3,"sources":["../../../src/providers/diagnostics.ts"],"sourcesContent":["/**\n * LLM 连接诊断与配置归一化\n *\n * 背景:CLI 在部分环境(如 openEuler 等国产 Linux、经企业网关/代理的网络)会\n * 只看到 `Connection error.`(OpenAI SDK 的 APIConnectionError),无法判断是\n * DNS、TLS、代理还是配置问题。这里提供:\n * - `cleanLlmValue` / `normalizeBaseURL`:清理 CRLF/引号/BOM、补协议、\n * 去掉 `/chat/completions` 之类的端点后缀(配置从其它系统/编辑器带进来时常见)\n * - `enrichLlmError`:把「底层 errno(沿 cause 链递归,穿透 SDK 包装)\n * + 实际请求端点」补进错误信息\n * - `networkErrorHints`:按错误给出可执行的排查建议(CLI 直接打印)\n */\n\n/** 是否连接/网络类错误(用于自动重试与提示) */\nexport function isNetworkErrorText(text: string): boolean {\n const t = text.toLowerCase();\n return [\n \"connection error\",\n \"fetch failed\",\n \"econnreset\",\n \"econnrefused\",\n \"etimedout\",\n \"ehostunreach\",\n \"enetunreach\",\n \"enotfound\",\n \"eai_again\",\n \"socket hang up\",\n \"und_err\",\n \"network\",\n \"timed out\",\n \"tls\",\n \"certificate\",\n \"unable_to_verify\",\n \"self signed\",\n ].some((m) => t.includes(m));\n}\n\n/**\n * 清理从配置文件/环境变量/粘贴进来的值:\n * BOM、首尾空白、换行与制表符、以及整体包裹的引号。\n * (Windows 记事本/CRLF、PowerShell `setx`、复制粘贴常见)\n */\nexport function cleanLlmValue(v?: string | null): string | undefined {\n if (v == null) return undefined;\n let s = String(v).replace(/^\\uFEFF/, \"\");\n s = s.replace(/[\\r\\n\\t]/g, \"\").trim();\n if (\n s.length >= 2 &&\n ((s.startsWith('\"') && s.endsWith('\"')) || (s.startsWith(\"'\") && s.endsWith(\"'\")))\n ) {\n s = s.slice(1, -1).trim();\n }\n return s || undefined;\n}\n\n/**\n * 规范化 Base URL:\n * - 缺协议时补 `https://`\n * - 去掉末尾 `/`\n * - 去掉误填的端点后缀(`/chat/completions`、`/responses`、`/completions`、`/models`)\n */\nexport function normalizeBaseURL(raw?: string | null): string | undefined {\n let s = cleanLlmValue(raw);\n if (!s) return undefined;\n if (!/^[a-z][a-z0-9+.-]*:\\/\\//i.test(s)) s = `https://${s}`;\n s = s.replace(/\\/+$/, \"\");\n s = s.replace(/\\/(chat\\/completions|completions|responses|models)$/i, \"\");\n return s.replace(/\\/+$/, \"\") || undefined;\n}\n\n/**\n * 是否为推理模型(o 系列 / gpt-5 等)。\n *\n * 这类模型在 Chat Completions 上**只接受** `max_completion_tokens`,传\n * `max_tokens` 会直接 400 `Unsupported parameter: 'max_tokens' is not\n * supported with this model`。Provider 与模型自检(`probeModel`)必须共用\n * 这一判断:一旦两处各写一份,自检就会把可用的推理模型误判成不可用。\n */\nexport function isReasoningModel(model: string): boolean {\n return /^(o\\d|gpt-5)/i.test(model);\n}\n\nfunction asRecord(v: unknown): Record<string, unknown> | null {\n return v && typeof v === \"object\" ? (v as Record<string, unknown>) : null;\n}\n\n/**\n * 展开错误链:`err` 本身 → `cause` → `cause.cause`…\n * 同时展开 `AggregateError.errors`(IPv4/IPv6 双栈都连不上时 undici 会这样抛)。\n * 带已访问集合,防止 cause 循环引用死循环。\n */\nfunction causeChain(err: unknown, maxNodes = 6): Record<string, unknown>[] {\n const out: Record<string, unknown>[] = [];\n const seen = new Set<unknown>();\n const queue: unknown[] = [err];\n while (queue.length > 0 && out.length < maxNodes) {\n const cur = queue.shift();\n if (!cur || typeof cur !== \"object\" || seen.has(cur)) continue;\n seen.add(cur);\n const rec = asRecord(cur);\n if (!rec) continue;\n out.push(rec);\n if (rec[\"cause\"]) queue.push(rec[\"cause\"]);\n const errs = rec[\"errors\"];\n if (Array.isArray(errs)) queue.push(...errs);\n }\n return out;\n}\n\n/**\n * 形如错误码的 name(`ECONNRESET`、`UND_ERR_CONNECT_TIMEOUT`)。\n * 内建/包装类的 name(`TypeError`、`Error`、`APIConnectionError`)不是错误码,\n * 混进 `code=` 只会误导排查方向。\n */\nfunction isErrorCodeLike(v: string): boolean {\n return /^[A-Z][A-Z0-9_]*$/.test(v);\n}\n\n/**\n * 自顶向下穿透包装层,取第一个可用的错误码(如 undici 的 ECONNRESET /\n * UND_ERR_* / ENOTFOUND)。\n *\n * OpenAI SDK 在连接失败时抛出的是\n * `APIConnectionError(\"Connection error.\")` → `cause: TypeError(\"fetch failed\")`\n * → `cause: Error(\"getaddrinfo ENOTFOUND ...\")`,真正的 errno 在最深处。\n * 只看一层只会拿到 `TypeError`,等于没有信息。\n */\nexport function errorCode(err: unknown): string | undefined {\n for (const src of causeChain(err)) {\n for (const k of [\"code\", \"errno\"]) {\n const v = src[k];\n if (typeof v === \"string\" && v) return v;\n }\n const name = src[\"name\"];\n if (typeof name === \"string\" && name !== \"Error\" && isErrorCodeLike(name)) {\n return name;\n }\n }\n return undefined;\n}\n\n/** 从 SDK 错误对象里取供应商原始错误体(OpenAI SDK 挂在 `error` 属性上) */\nfunction errorBody(err: unknown): Record<string, unknown> | null {\n return asRecord(asRecord(err)?.[\"error\"]);\n}\n\n/** 「端点 + 底层原因」后缀(无可用信息时返回空串) */\nexport function llmErrorContext(err: unknown, baseURL?: string): string {\n const parts: string[] = [];\n const code = errorCode(err);\n if (code) parts.push(`code=${code}`);\n const msg = err instanceof Error ? err.message : String(err ?? \"\");\n // 优先取「错误码所在那一层」的 message,保证 code 与文字描述同源;\n // 该层没有 message 时再取链路最深层(如 fetch failed 之下的\n // \"getaddrinfo ENOTFOUND ...\"),而不是停在 TypeError 的 \"fetch failed\"。\n const chain = causeChain(err);\n const codeSrc = chain.find(\n (c) => typeof c[\"code\"] === \"string\" || typeof c[\"errno\"] === \"string\",\n );\n const pickMsg = (c?: Record<string, unknown>): string | undefined => {\n const m = c?.[\"message\"];\n return typeof m === \"string\" && m && m !== msg ? m : undefined;\n };\n const causeMsg =\n pickMsg(codeSrc) ??\n [...chain].reverse().map(pickMsg).find((m): m is string => !!m) ??\n \"\";\n if (causeMsg) parts.push(causeMsg.slice(0, 160));\n // 供应商错误体里的真实原因(经 normalizeErrorFetch 归一化后可读到):\n // 形状非标准的网关 SDK 只会给出一句 \"400 status code (no body)\"\n const body = errorBody(err);\n const bodyMsg = typeof body?.[\"message\"] === \"string\" ? body[\"message\"] : \"\";\n if (bodyMsg && !msg.includes(bodyMsg)) parts.push(`详情: ${bodyMsg.slice(0, 200)}`);\n const bodyCode = typeof body?.[\"code\"] === \"string\" ? body[\"code\"] : \"\";\n if (bodyCode && !parts.some((p) => p.includes(bodyCode))) {\n parts.push(`code=${bodyCode}`);\n }\n const rid =\n (typeof err === \"object\" && err !== null\n ? String((err as { request_id?: unknown }).request_id ?? \"\")\n : \"\") ||\n (typeof body?.[\"request_id\"] === \"string\" ? body[\"request_id\"] : \"\");\n if (rid) parts.push(`request_id=${rid}`);\n if (baseURL) parts.push(`端点 ${baseURL}`);\n return parts.length > 0 ? `(${parts.join(\" · \")})` : \"\";\n}\n\n/**\n * 就地把「端点 + 底层原因」补进错误信息(保留原错误对象类型/status,\n * 便于上层继续按类型分支,如 Responses 不支持时降级)。\n */\nexport function enrichLlmError(err: unknown, baseURL?: string): unknown {\n if (!(err instanceof Error)) return err;\n const ctx = llmErrorContext(err, baseURL);\n if (!ctx) return err;\n if (err.message.includes(\"端点 \") || err.message.includes(ctx)) return err;\n try {\n err.message = `${err.message}${ctx}`;\n } catch {\n // message 只读时忽略\n }\n return err;\n}\n\n/** 连接类错误 → 可执行的排查建议(非连接类返回空数组) */\nexport function networkErrorHints(message: string, errorType = \"\"): string[] {\n const text = `${message} ${errorType}`;\n if (!isNetworkErrorText(text)) return [];\n const hints: string[] = [\n \"排查:① 网络是否可达:curl -sI <BaseURL>/models(或 /connect 重测)\",\n \"② 需要代理时:SOCKS5_PROXY=socks5://127.0.0.1:7890(HTTP_PROXY 不支持)\",\n \"③ 企业网关/证书拦截:NODE_EXTRA_CA_CERTS=/path/to/ca.pem\",\n ];\n if (/enotfound|eai_again|getaddrinfo/i.test(text)) {\n hints.push(\"④ DNS 解析失败:检查 /etc/resolv.conf,或改用 IP/内网域名\");\n } else if (/etimedout|econnrefused|unreachable/i.test(text)) {\n hints.push(\"④ 连接超时/被拒:检查防火墙出网策略与端口(443)\");\n } else if (/certificate|unable_to_verify|self signed|tls/i.test(text)) {\n hints.push(\"④ 证书链问题:为 Node 指定系统 CA(NODE_EXTRA_CA_CERTS)或补齐 ca-certificates\");\n } else if (/econnreset|socket hang up/i.test(text)) {\n hints.push(\"④ 连接被重置:多为链路/网关抖动,已自动重试;可调大 NETWORK_RETRY_MAX\");\n }\n hints.push(\"⑤ IPv6 抖动可试:NODE_OPTIONS=--dns-result-order=ipv4first\");\n return hints;\n}\n\n/**\n * 「凭据无效」类错误的排查建议(非该类错误返回空数组)。\n *\n * 与 `paramErrorHints` 分开:401 不是模型问题,混进去会让「模型类提示」\n * 在鉴权失败时也冒出来,反而误导。这里只解释「Key 为什么失效、去哪儿换」。\n *\n * 内置模型(志前 API)的 Key 由平台托管:用户在控制台重置 Key 后本机这份\n * 立即失效,必须引导去点「刷新内置模型」,否则会一直看到 401 以为节点坏了。\n */\nexport function authErrorHints(message: string, code = \"\"): string[] {\n const text = `${message} ${code}`;\n if (!/401|403|unauthorized|forbidden|invalid[_\\s]*api[_\\s]*key|api[\\s_-]*key\\s*(?:无效|invalid)|管理令牌无效/i.test(text)) {\n return [];\n }\n return [\n \"凭据无效:若用的是「志前大模型(内置)」,在节点设置的模型页点「刷新内置模型」重新领取 Key\" +\n \"(在控制台重置过 Key 后需要这一步);若是自填的供应商,请检查 Key 是否正确或已过期\",\n ];\n}\n\n/**\n * 「工具调用没有对应结果」这一协议错误的排查建议。\n *\n * 历史里若存在「assistant 带 tool_calls、却没有对应 tool 消息」的记录,整个请求\n * 会被供应商原样拒绝(400 insufficient tool messages following tool_calls\n * message)。这不是额度或模型问题——历史多为上次在工具执行中被中断 / 进程被杀\n * 时留下的。引擎在每轮开始与发送前都会自动修复,这里只负责把原因讲清楚,\n * 避免用户按「未开通/欠费」的错误提示去充值。\n */\nexport function toolCallHistoryHints(message: string, code = \"\"): string[] {\n const text = `${message} ${code}`;\n if (\n !/tool_calls|tool[_ ]?call[_ ]?id/i.test(text) ||\n !/must be followed by tool messages|insufficient tool messages|no tool output found|tool messages responding/i.test(\n text,\n )\n ) {\n return [];\n }\n return [\n \"这不是模型或余额问题:会话历史里有一条「工具调用没有拿到结果」的记录(通常是上次在工具执行中按了 Ctrl+C,或进程被中断)\",\n \"引擎已自动清理并重试;如果仍报同样的错误,用 /new 开一个新会话即可(旧会话的历史里还留着这条记录)\",\n ];\n}\n\n/**\n * 「模型/参数不被接受」类错误的排查建议(非该类错误返回空数组)。\n *\n * 典型场景(均为阿里兼容模式实测):\n * - 选到非对话/专用通道模型(`cmaas-*`、向量/语音/图像…)\n * → `InvalidParameter: Required parameter(s) missing or invalid`\n * - 模型在该账号下没开通\n * → `invalid_parameter_error: The product is not activated`\n * 这两句原文对排查毫无帮助,必须翻成可执行的下一步。\n */\nexport function paramErrorHints(message: string, code = \"\"): string[] {\n const text = `${message} ${code}`;\n const hints: string[] = [];\n\n /*\n * 一、账号/开通类\n *\n * 只认「额度 / 开通 / 计费」类措辞。`insufficient` 单独出现多半是别的语义\n * (如 `insufficient tool messages following tool_calls message` 这条协议错误),\n * 把它当成欠费会给出完全错误的建议——用户会跑去充值,而真正的问题在会话历史。\n */\n if (\n /not[_\\s]*(activated|enabled|purchased)|arrearage|quota|subscription|billing|未开通|欠费|额度|余额不足|insufficient[_\\s]*(quota|balance|credit|funds)/i.test(\n text,\n )\n ) {\n hints.push(\n \"该模型在此 **API Key/账号** 下未开通(或账号欠费、额度用尽):到网关控制台开通该模型,或换一个已开通的模型\",\n );\n // 内置模型(志前 API)走的是用户自己的租户额度,额度用完是最常见的失败原因,\n // 必须给出「去哪儿充值」而不是让人对着 402 猜\n hints.push(\n \"如果你用的是「志前大模型(内置)」:到 https://hub.cd-zq.com 查看余额并充值;\" +\n \"也可以先切换到你自己配置的其它模型继续(节点设置 → 模型 → 设为默认)\",\n );\n }\n\n // 二、模型名/参数类\n if (\n /invalid[_\\s]*parameter|invalidparameter|required[_\\s]+parameter|missing[_\\s]+parameter|invalid[_\\s]*model|model[_\\s]*not[_\\s]*found|not[_\\s]*support|unsupported/i.test(\n text,\n )\n ) {\n hints.push(\n \"多半是**模型名不可用于对话接口**:网关 /models 列的是全量目录,含向量(embedding)、重排(rerank)、语音(asr/tts)、实时(realtime)、图像(image/wan)、专用通道(cmaas-*) 等,这些模型不能当 Agent 后端\",\n \"换一个对话模型:/connect 重新选择(对话模型已排在列表前面),或 /models 查看\",\n \"手动验证某个模型能不能用:\\n\" +\n \" curl -s <BaseURL>/chat/completions -H 'Authorization: Bearer $KEY' -H 'content-type: application/json' \\\\\\n\" +\n \" -d '{\\\"model\\\":\\\"<模型名>\\\",\\\"messages\\\":[{\\\"role\\\":\\\"user\\\",\\\"content\\\":\\\"hi\\\"}]}'\",\n \"若确认模型无误:检查是否多传了该服务不支持的参数(如推理强度 reasoning/effort),可用 /connect 把推理强度设为「默认」\",\n );\n }\n\n return hints;\n}\n\n/** LLM 请求超时(毫秒):OPENAI_TIMEOUT_MS,默认 10 分钟 */\nexport function llmTimeoutMs(): number {\n const n = Number(process.env.OPENAI_TIMEOUT_MS);\n return Number.isFinite(n) && n > 0 ? n : 600_000;\n}\n\n/**\n * LLM 流空闲超时(毫秒):OPENAI_STREAM_IDLE_MS,默认 2 分钟。\n *\n * 上面的 `llmTimeoutMs()` 走 SDK 的 timeout,而 SDK 在**响应头返回后**就清掉了\n * 计时器 —— 响应体的流式读取阶段没有任何超时保护。网关静默断供(既不推数据\n * 也不关连接)时,引擎会永久挂在 `for await` 上,会话状态卡在 running。\n * 故对两次 chunk 之间的间隔单独设一道看门狗。\n */\nexport function llmStreamIdleMs(): number {\n const n = Number(process.env.OPENAI_STREAM_IDLE_MS);\n return Number.isFinite(n) && n > 0 ? n : 120_000;\n}\n\n// ===================== 错误体归一化(fetch 层) =====================\n\ntype FetchLike = (input: any, init?: any) => Promise<any>;\n\n/** 把 Headers 转成普通对象(不同 fetch 实现的 Headers 都能遍历) */\nfunction headerRecord(h: unknown): Record<string, string> | undefined {\n if (!h) return undefined;\n const out: Record<string, string> = {};\n try {\n const iter =\n (h as any).entries?.() ?? ((h as any)[Symbol.iterator]?.() as any);\n if (iter) for (const [k, v] of iter) out[String(k)] = String(v);\n } catch {\n // 不可遍历的 Headers:放弃(新 Response 用默认头)\n }\n return Object.keys(out).length > 0 ? out : undefined;\n}\n\n/**\n * 包装 fetch:把**非标准形状的错误响应体**归一化成 OpenAI SDK 认识的结构。\n *\n * SDK 的 `APIError.generate` 只读 `body.error`,而阿里云百炼兼容模式的\n * `/chat/completions` 错误体是顶层 `{code, message, request_id}`(没有\n * `error` 包裹),于是 SDK 抛出的 message 变成\n * `400 status code (no body)` —— 真实原因(InvalidParameter / 模型不可用)\n * 被彻底吞掉,降级判定与排查建议全部失效。这里在读到非 2xx 时补一层\n * `{error:{...}}`,让下游(分类、日志、CLI 提示)拿到原始信息。\n */\nexport function normalizeErrorFetch(inner: FetchLike): FetchLike {\n return async (input, init) => {\n const res = await inner(input, init);\n if (res?.ok) return res;\n // 204/205/304 不允许带 body(Response 构造会抛),也没有错误信息可读\n if (res?.status === 204 || res?.status === 205 || res?.status === 304) return res;\n\n let text: string;\n try {\n text = await res.text();\n } catch {\n return res; // 读不动就原样交回(SDK 会按连接错误处理)\n }\n\n let out = text;\n try {\n const body = JSON.parse(text) as Record<string, unknown> | null;\n const std = body && typeof body === \"object\" && !(\"error\" in body) &&\n (body.message != null || body.code != null);\n if (std && body) {\n out = JSON.stringify({\n error: {\n message: String(body.message ?? `HTTP ${res.status}`),\n code: body.code ?? null,\n type: body.type ?? \"invalid_request_error\",\n param: body.param ?? null,\n },\n request_id: body.request_id ?? null,\n });\n }\n } catch {\n // 非 JSON 错误体(网关 HTML 等):保持原文,让 SDK 至少带上状态码\n }\n\n return new Response(out, {\n status: res.status,\n statusText: res.statusText,\n headers: headerRecord(res.headers),\n });\n };\n}\n\n/** LLM SDK 传输层重试次数:OPENAI_MAX_RETRIES,默认 3 */\nexport function llmMaxRetries(): number {\n const n = Number(process.env.OPENAI_MAX_RETRIES);\n return Number.isFinite(n) && n >= 0 ? n : 3;\n}\n"],"mappings":"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","names":[]}
|