@zqagent/core 0.3.11 → 0.3.13

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.
Files changed (33) hide show
  1. package/dist/{TurnEngine-DtGEuVLT.d.ts → TurnEngine-BZjUGic_.d.ts} +34 -4
  2. package/dist/{TurnEngine-BPKhWuSI.d.cts → TurnEngine-CMGdcSdp.d.cts} +34 -4
  3. package/dist/index.cjs +69 -16
  4. package/dist/index.cjs.map +1 -1
  5. package/dist/index.d.cts +7 -2
  6. package/dist/index.d.ts +7 -2
  7. package/dist/index.js +69 -16
  8. package/dist/index.js.map +1 -1
  9. package/dist/internal/engine/FileLogHook.d.cts +2 -1
  10. package/dist/internal/engine/FileLogHook.d.ts +2 -1
  11. package/dist/internal/llmConfig.cjs +30 -5
  12. package/dist/internal/llmConfig.cjs.map +1 -1
  13. package/dist/internal/llmConfig.d.cts +32 -1
  14. package/dist/internal/llmConfig.d.ts +32 -1
  15. package/dist/internal/llmConfig.js +30 -5
  16. package/dist/internal/llmConfig.js.map +1 -1
  17. package/dist/internal/providers/diagnostics.cjs +22 -2
  18. package/dist/internal/providers/diagnostics.cjs.map +1 -1
  19. package/dist/internal/providers/diagnostics.d.cts +25 -3
  20. package/dist/internal/providers/diagnostics.d.ts +25 -3
  21. package/dist/internal/providers/diagnostics.js +19 -2
  22. package/dist/internal/providers/diagnostics.js.map +1 -1
  23. package/dist/internal/providers/models.cjs.map +1 -1
  24. package/dist/internal/providers/models.js.map +1 -1
  25. package/dist/internal/update/controller.cjs +1 -1
  26. package/dist/internal/update/controller.js +1 -1
  27. package/dist/internal/update/platform.cjs +1 -1
  28. package/dist/internal/update/platform.js +1 -1
  29. package/dist/tools/explore.cjs.map +1 -1
  30. package/dist/tools/explore.d.cts +2 -1
  31. package/dist/tools/explore.d.ts +2 -1
  32. package/dist/tools/explore.js.map +1 -1
  33. package/package.json +4 -3
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/llmConfig.ts","../../src/crypto/secrets.ts","../../src/providers/proxy.ts","../../../../node_modules/.pnpm/socks-proxy-agent@9.0.0_supports-color@7.2.0/node_modules/socks-proxy-agent/src/index.ts","../../../../node_modules/.pnpm/agent-base@8.0.0/node_modules/agent-base/src/index.ts","../../src/providers/models.ts","../../src/paths.ts"],"sourcesContent":["/**\n * LLM 配置统一存储(CLI `/connect` 与 Server「节点配置」共用同一份文件)\n *\n * 位置:`~/.config/zqagent/connections.json`(可用 `XDG_CONFIG_HOME` 覆盖;\n * 或用 `ZQAGENT_LLM_CONFIG_FILE` 直接指定文件,便于测试与自定义部署)。\n * 同一台机器上,CLI、以本机用户身份运行的 server、桌面端内嵌 sidecar 读写的都是这一份,\n * 因此任一端的增删改都会立即表现在其它端。\n *\n * 格式 v3:\n * { version: 3, activeId, providers: [{ id, name, type, baseURL, model,\n * reasoningEffort, apiKeyEnc, models, nonChat, modelMeta }], migratedFrom }\n *\n * - `apiKeyEnc`:AES-256-GCM 密文(主密钥与配置同目录,0600)。\n * - `type` 为预设 id(合并了 CLI 的 `provider` 与 Web 的 `type` 两个历史字段名)。\n * - 兼容读取旧格式并自动迁移:CLI 的 v2 / 更早的单连接格式,以及\n * Server 的 `{DATA_DIR}/node-llm.json`(明文 key → 加密)。\n */\nimport * as fs from \"fs\";\nimport * as path from \"path\";\nimport * as crypto from \"crypto\";\nimport type { ReasoningEffort } from \"./engine/types.js\";\nimport { loadOrCreateMasterKey, encryptWithKey, decryptWithKey } from \"./crypto/secrets.js\";\nimport { normalizeModelMeta, type ModelMeta } from \"./providers/models.js\";\nimport { configDir, stateDir } from \"./paths.js\";\nimport { llmPreset } from \"@zqagent/shared/llm-presets\";\n\n/**\n * 统一配置文件路径(惰性解析:读取时才计算,便于测试注入环境变量)。\n * 优先级:`ZQAGENT_LLM_CONFIG_FILE` → `configDir()/connections.json`。\n */\nexport function llmConfigFile(): string {\n const override = process.env.ZQAGENT_LLM_CONFIG_FILE?.trim();\n if (override) return override;\n return path.join(configDir(), \"connections.json\");\n}\n\n/** 单个供应商条目(含加密后的 key) */\nexport interface LlmProviderEntry {\n id: string;\n /** 展示名 */\n name: string;\n /** 预设 id(`llmPresets` 的 id;历史 provider/type 合并到此) */\n type: string;\n baseURL: string;\n model: string;\n /** 推理强度;不支持或未设置时为 null */\n reasoningEffort: ReasoningEffort | null;\n /** API Key 密文(AES-256-GCM,`v1:iv:tag:ciphertext`) */\n apiKeyEnc: string;\n /** 上次刷新得到的候选模型(可选) */\n models?: string[];\n /** 候选中非对话模型的子集(可选,仅用于提示) */\n nonChat?: string[];\n /**\n * 刷新模型列表时从供应商 `/models` 拿到的元数据(含真实上下文长度)。\n * 落盘后供引擎按实际模型精确计算压缩阈值(见 `providers/models.ts`)。\n */\n modelMeta?: ModelMeta[];\n}\n\nexport interface LlmConfigFile {\n version: 3;\n activeId: string | null;\n providers: LlmProviderEntry[];\n /** 已导入过的旧配置来源(绝对路径),避免重复合并 */\n migratedFrom?: string[];\n}\n\n// ===================== 加解密 =====================\n\n/** 主密钥按「配置目录」缓存(不同配置目录用不同密钥;测试隔离后互不影响) */\nconst keyCache = new Map<string, Buffer>();\n\nfunction masterKeyFileFor(file: string): string {\n return path.join(path.dirname(file), \"secret.key\");\n}\n\nfunction getMasterKey(file: string): Buffer {\n const keyFile = masterKeyFileFor(file);\n let key = keyCache.get(keyFile);\n if (!key) {\n key = loadOrCreateMasterKey(keyFile);\n keyCache.set(keyFile, key);\n }\n return key;\n}\n\n/** 加密 API Key;`file` 为配置文件路径(缺省用统一路径,决定主密钥位置) */\nexport function encryptKey(plain: string, file = llmConfigFile()): string {\n return encryptWithKey(getMasterKey(file), plain);\n}\n\n/** 解密 API Key;格式非法 / 主密钥变化返回 null */\nexport function decryptKey(payload: string, file = llmConfigFile()): string | null {\n return decryptWithKey(getMasterKey(file), payload);\n}\n\n/** 生成新的供应商 id */\nexport function newProviderId(): string {\n return crypto.randomBytes(8).toString(\"hex\");\n}\n\n// ===================== 规范化 =====================\n\nfunction normEffort(v: unknown): ReasoningEffort | null {\n return v === \"low\" || v === \"medium\" || v === \"high\" ? v : null;\n}\n\n/** 规范化模型列表(去重、限长) */\nfunction normModelList(v: unknown): string[] | undefined {\n if (!Array.isArray(v)) return undefined;\n const out: string[] = [];\n const seen = new Set<string>();\n for (const x of v) {\n const s = String(x ?? \"\").trim();\n if (!s || seen.has(s)) continue;\n seen.add(s);\n out.push(s);\n if (out.length >= 500) break;\n }\n return out.length ? out : undefined;\n}\n\n/** 由端点反查预设 id(迁移无 type 的旧数据) */\nfunction guessTypeByBaseURL(baseURL: string): string {\n const u = baseURL.toLowerCase().replace(/\\/+$/, \"\");\n if (!u) return \"custom\";\n for (const p of KNOWN_PRESETS) {\n const b = p.baseURL.toLowerCase().replace(/\\/+$/, \"\");\n if (b && (u === b || u.startsWith(b))) return p.id;\n }\n return \"custom\";\n}\n\nconst KNOWN_PRESETS = [\"openai\", \"deepseek\", \"qwen\", \"zhipu\", \"moonshot\", \"xiaomi\"].map(\n (id) => ({ id, baseURL: llmPreset(id)?.baseURL ?? \"\" }),\n);\n\n/** 解析单个条目(兼容 v3 / v2 / server 明文 key 三种形态) */\nfunction parseEntry(o: unknown, file: string): LlmProviderEntry | null {\n if (!o || typeof o !== \"object\") return null;\n const r = o as Record<string, unknown>;\n const baseURL = typeof r.baseURL === \"string\" ? r.baseURL : \"\";\n const model = typeof r.model === \"string\" ? r.model : \"\";\n\n // type:v3 的 type → v2 的 provider → 由 baseURL 反查预设 → custom\n let type = String(r.type ?? r.provider ?? \"\").trim();\n if (!type) type = guessTypeByBaseURL(baseURL);\n\n // key:v3/v2 用密文;server 旧格式是明文 apiKey,迁移时加密\n let apiKeyEnc = typeof r.apiKeyEnc === \"string\" ? r.apiKeyEnc : \"\";\n const plain = typeof r.apiKey === \"string\" ? r.apiKey.trim() : \"\";\n if (!apiKeyEnc && plain) apiKeyEnc = encryptKey(plain, file);\n\n // 迁移时可能既无 key 也无模型(如未填完的条目):仍保留,交由 UI 补全\n if (!baseURL && !model && !apiKeyEnc && !type) return null;\n\n const models = normModelList(r.models);\n const nonChat = normModelList(r.nonChat);\n const modelMeta = normalizeModelMeta(r.modelMeta);\n const name = String(r.name ?? \"\").trim() || autoProviderName(type, model);\n return {\n id: typeof r.id === \"string\" && r.id ? r.id : newProviderId(),\n name,\n type,\n baseURL,\n model,\n reasoningEffort: normEffort(r.reasoningEffort),\n apiKeyEnc,\n ...(models ? { models } : {}),\n ...(nonChat ? { nonChat } : {}),\n ...(modelMeta ? { modelMeta } : {}),\n };\n}\n\n/** 自动展示名:预设名 · 模型 */\nexport function autoProviderName(type: string, model: string): string {\n const name = llmPreset(type)?.name ?? type ?? \"\";\n return model ? `${name} · ${model}` : name;\n}\n\nconst PRESET_NAMES = new Set(\n [\"openai\", \"deepseek\", \"qwen\", \"zhipu\", \"moonshot\", \"xiaomi\", \"custom\"].map(\n (id) => llmPreset(id)?.name ?? \"\",\n ),\n);\n\n/**\n * 修正历史遗留的陈旧自动名(供应商标签变了、或模型换了但名字没跟上)。\n * 只动「看起来是自动名 / 顶着别家预设标签」的名字,用户自起的名字保留。\n * @returns 是否发生修改\n */\nexport function fixStaleName(p: LlmProviderEntry): boolean {\n const label = llmPreset(p.type)?.name ?? p.type;\n const fresh = autoProviderName(p.type, p.model);\n if (p.name === fresh) return false;\n const looksAuto =\n p.name === label || p.name.startsWith(`${label} · `) || PRESET_NAMES.has(p.name);\n if (!looksAuto) return false;\n p.name = fresh;\n return true;\n}\n\n// ===================== 读 / 写 =====================\n\nfunction emptyConfig(): LlmConfigFile {\n return { version: 3, activeId: null, providers: [] };\n}\n\n/** 规范化列表:按 id 去重(后者覆盖前者)、activeId 回退到首项 */\nfunction normalize(f: LlmConfigFile): LlmConfigFile {\n const byId = new Map<string, LlmProviderEntry>();\n for (const p of f.providers) byId.set(p.id, p);\n const providers = [...byId.values()];\n const activeId = providers.some((p) => p.id === f.activeId)\n ? f.activeId\n : (providers[0]?.id ?? null);\n return { ...f, providers, activeId };\n}\n\n/**\n * 读取并规范化一个配置文件。\n * @param file 要读取的文件\n * @param keyFile 主密钥所在配置(迁移时应传**目标**配置:导入的明文 key 要用目标密钥加密)\n */\nfunction readRaw(file: string, keyFile = file): LlmConfigFile | null {\n if (!fs.existsSync(file)) return null;\n let raw: unknown;\n try {\n raw = JSON.parse(fs.readFileSync(file, \"utf8\"));\n } catch {\n return null;\n }\n if (!raw || typeof raw !== \"object\") return null;\n const o = raw as Record<string, unknown>;\n\n // v3 / v2:providers 列表\n if (Array.isArray(o.providers)) {\n const providers = o.providers\n .map((x) => parseEntry(x, keyFile))\n .filter((p): p is LlmProviderEntry => p !== null);\n const activeId =\n typeof o.activeId === \"string\"\n ? o.activeId\n : typeof o.defaultProviderId === \"string\"\n ? o.defaultProviderId\n : null;\n const migratedFrom = Array.isArray(o.migratedFrom)\n ? o.migratedFrom.filter((x): x is string => typeof x === \"string\")\n : undefined;\n return normalize({\n version: 3,\n activeId,\n providers,\n ...(migratedFrom ? { migratedFrom } : {}),\n });\n }\n\n // 更早的「单连接对象」格式\n const single = parseEntry(o, keyFile);\n if (single) return { version: 3, activeId: single.id, providers: [single] };\n return null;\n}\n\n/** 把外部配置(如 server 的 node-llm.json)合并进主配置 */\nfunction mergeExternal(\n base: LlmConfigFile,\n file: string,\n targetFile: string,\n): { config: LlmConfigFile; changed: boolean } {\n const migrated = base.migratedFrom ?? [];\n if (migrated.includes(file)) return { config: base, changed: false };\n // 用目标配置的主密钥加密导入的明文 key,保证之后能被同一密钥解开\n const ext = readRaw(file, targetFile);\n if (!ext || ext.providers.length === 0) {\n return { config: base, changed: false };\n }\n // 同名 id 保留主配置中的那份;其余追加\n const have = new Set(base.providers.map((p) => p.id));\n const added = ext.providers.filter((p) => !have.has(p.id));\n const config = normalize({\n ...base,\n providers: [...base.providers, ...added],\n activeId: base.activeId ?? ext.activeId,\n migratedFrom: [...migrated, file],\n });\n return { config, changed: true };\n}\n\n/**\n * 读取统一配置(含自动迁移)。\n *\n * @param opts.file 配置文件路径(缺省 `llmConfigFile()`)\n * @param opts.legacyFiles 需要合并的旧配置文件(如 `{DATA_DIR}/node-llm.json`),\n * 仅在主配置尚未记录该来源时导入一次\n */\nexport function readLlmConfig(opts: { file?: string; legacyFiles?: string[] } = {}): LlmConfigFile {\n const file = opts.file ?? llmConfigFile();\n let f = readRaw(file) ?? emptyConfig();\n let changed = false;\n\n // 顺手修正陈旧自动名\n // 必须遍历完整个列表:fixStaleName 带副作用,some 命中首个 true 会短路\n if (f.providers.map(fixStaleName).some(Boolean)) changed = true;\n\n for (const legacy of opts.legacyFiles ?? []) {\n const r = mergeExternal(f, legacy, file);\n f = r.config;\n changed = changed || r.changed;\n }\n\n // v2 / 单连接 / 合并过旧文件:规范化后落盘固化\n if (changed || !fs.existsSync(file)) writeLlmConfig(f, file);\n return f;\n}\n\n/** 写入统一配置(0600) */\nexport function writeLlmConfig(f: LlmConfigFile, file = llmConfigFile()): void {\n const next = normalize(f);\n fs.mkdirSync(path.dirname(file), { recursive: true, mode: 0o700 });\n fs.writeFileSync(file, JSON.stringify(next, null, 2) + \"\\n\", {\n mode: 0o600,\n encoding: \"utf8\",\n });\n}\n\n/**\n * 常见的历史配置来源(供迁移合并):\n * 旧版桌面端内嵌 server 的默认数据目录 `{stateDir}/data/node-llm.json`\n * (现默认目录为 `{stateDir}/.zqagent_data`,此处保留旧路径以便老用户配置被自动接管)。\n * 桌面端与 CLI 同属一个登录用户,这样先配桌面端也能被 CLI 立刻看到。\n */\nexport function commonLegacyFiles(): string[] {\n return [path.join(stateDir(), \"data\", \"node-llm.json\")];\n}\n","// 已迁移至 @zqagent/shared(core 与 hub 共用);此文件保留为兼容再导出。\nexport * from \"@zqagent/shared/secrets\";\n","/**\n * 代理出口(LLM 相关 HTTP 请求统一走这里)\n *\n * 支持 `SOCKS5_PROXY`(`socks5://` / `socks5h://`),供 node-fetch 使用:\n * - `fetchModels()`(/connect 刷新模型)→ node-fetch + agent;未配置时代理为 null。\n * - OpenAI SDK 的请求默认走 Node 内置 fetch(undici),undici **不读**\n * HTTP_PROXY/HTTPS_PROXY/ALL_PROXY 环境变量;因此配置了 SOCKS5 代理时,\n * 这里同时提供 `makeSocksFetch()`,把它注入 SDK 的 `fetch` 选项,\n * 保证「对话」与「刷新模型」走同一条网络路径(避免一边通一边不通)。\n */\nimport nodeFetch from \"node-fetch\";\nimport { SocksProxyAgent } from \"socks-proxy-agent\";\n\n/** SOCKS5 代理地址(环境变量 SOCKS5_PROXY),未设置时为 null */\nexport function socksProxyUrl(): string | null {\n const raw = (process.env.SOCKS5_PROXY ?? \"\").trim();\n return raw || null;\n}\n\n/** 是否为 socks 代理地址 */\nfunction isSocks(raw: string): boolean {\n return /^socks/i.test(raw);\n}\n\n/** 全局共享的 SOCKS5 Agent;未配置或构造失败时为 null(直连) */\nlet cached: { url: string; agent: SocksProxyAgent | null } | null = null;\n\nexport function socksAgent(): SocksProxyAgent | null {\n const url = socksProxyUrl();\n if (!url) return null;\n if (cached?.url === url) return cached.agent;\n let agent: SocksProxyAgent | null = null;\n if (isSocks(url)) {\n try {\n agent = new SocksProxyAgent(url);\n } catch {\n try {\n agent = new SocksProxyAgent(url.replace(/^socks5h:\\/\\//, \"socks5://\"));\n } catch {\n agent = null;\n }\n }\n }\n cached = { url, agent };\n return agent;\n}\n\n/**\n * 检测到「不支持的环境变量代理」时给出可执行提示(否则返回 null)。\n * undici/内置 fetch 不读这些变量,本项目也不引入 http(s) 代理 agent。\n */\nexport function unsupportedProxyHint(): string | null {\n const keys = [\"HTTPS_PROXY\", \"https_proxy\", \"HTTP_PROXY\", \"http_proxy\", \"ALL_PROXY\", \"all_proxy\"];\n const hit = keys.filter((k) => {\n const v = (process.env[k] ?? \"\").trim();\n return v && !isSocks(v);\n });\n if (hit.length === 0) return null;\n return `检测到 ${hit[0]}=${process.env[hit[0]]}(HTTP 代理暂不支持):请改为 SOCKS5_PROXY=socks5://<host>:<port>(或让本地代理开放 socks5 端口)`;\n}\n\n/** 用 node-fetch + socks agent 实现 fetch(保持与全局 fetch 相同的调用签名) */\nexport function makeSocksFetch(agent: SocksProxyAgent): typeof fetch {\n return ((url: unknown, init?: unknown) =>\n nodeFetch(String(url), { ...(init as object), agent } as never)) as unknown as typeof fetch;\n}\n",null,null,"/**\n * LLM 可用模型列表(OpenAI 兼容 /models 接口)\n *\n * 供 CLI(/connect 刷新模型列表)与 Server(GET /api/v1/models)共用。\n * 支持 SOCKS5_PROXY 走代理出口;单个端点 10s 超时。\n */\nimport { makeSocksFetch, socksAgent } from \"./proxy.js\";\nimport {\n cleanLlmValue,\n isReasoningModel,\n normalizeBaseURL,\n} from \"./diagnostics.js\";\nimport {\n capabilitiesOf,\n normalizeCapabilities,\n type ModelCapability,\n} from \"@zqagent/shared/model-capabilities\";\n\n/** 统一 HTTP 请求封装:配置了 SOCKS5 代理时用 node-fetch + agent,否则用全局 fetch。 */\nasync function doFetch(url: string, init?: Record<string, unknown>): Promise<unknown> {\n const agent = socksAgent();\n if (agent) {\n const f = makeSocksFetch(agent);\n return f(url, init as RequestInit);\n }\n return (globalThis as unknown as { fetch: typeof fetch }).fetch(url, init as RequestInit);\n}\n\n/** 单个模型的元数据(从 /models 响应中提取,部分供应商提供) */\nexport interface ModelMeta {\n /** 模型 id */\n id: string;\n /** 上下文窗口大小(token 数);仅当供应商在 /models 响应中返回时才有值 */\n contextWindow?: number;\n /**\n * 该模型可接收的输入类型(用户在「节点配置 → LLM」里勾选)。\n * 未配置时按纯文本处理,见 `modelCapabilities`。\n */\n capabilities?: ModelCapability[];\n}\n\n/**\n * 规范化模型元数据列表(按 id 去重;仅保留合法的上下文长度)。\n * 供配置持久化(`llmConfig.ts`)与 Server 视图(`server/nodeLlm.ts`)共用。\n */\nexport function normalizeModelMeta(v: unknown): ModelMeta[] | undefined {\n if (!Array.isArray(v)) return undefined;\n const out: ModelMeta[] = [];\n const seen = new Set<string>();\n for (const x of v) {\n if (!x || typeof x !== \"object\") continue;\n const rec = x as Record<string, unknown>;\n const id = String(rec.id ?? \"\").trim();\n if (!id || seen.has(id)) continue;\n seen.add(id);\n const ctx = Number(rec.contextWindow);\n const capabilities = normalizeCapabilities(rec.capabilities);\n out.push({\n id,\n ...(Number.isFinite(ctx) && ctx > 0 ? { contextWindow: ctx } : {}),\n ...(capabilities ? { capabilities } : {}),\n });\n if (out.length >= 500) break;\n }\n return out;\n}\n\n/** 从模型元数据中查输入能力(精确匹配优先,其次前缀匹配;缺省仅文本) */\nexport function modelCapabilities(\n meta: ModelMeta[] | undefined,\n model: string,\n): ModelCapability[] {\n return capabilitiesOf(meta, model);\n}\n\n/**\n * 合并两份模型元数据(按 id)。\n *\n * 用途:刷新 `/models` 拿到新的上下文长度、重新拉取内置模型凭据时,\n * 不能把用户手工勾选的 `capabilities` 覆盖掉——供应商并不返回能力信息。\n * 因此 **base 的 capabilities 始终优先**,patch 只补充其余字段(上下文长度等)。\n *\n * @param base 旧元数据(提供 capabilities)\n * @param patch 新元数据(提供 contextWindow 等)\n */\nexport function mergeModelMeta(\n base: ModelMeta[] | undefined,\n patch: ModelMeta[] | undefined,\n): ModelMeta[] | undefined {\n if (!base?.length) return patch;\n if (!patch?.length) return base;\n const byId = new Map<string, ModelMeta>();\n for (const m of base) byId.set(m.id, { ...m });\n for (const m of patch) {\n const prev = byId.get(m.id);\n byId.set(m.id, {\n ...prev,\n ...m,\n ...(prev?.capabilities?.length ? { capabilities: prev.capabilities } : {}),\n });\n }\n return [...byId.values()];\n}\n\n/** 从模型元数据中查上下文长度(精确匹配优先,其次前缀匹配),未命中返回 undefined */\nexport function modelContextWindow(\n meta: ModelMeta[] | undefined,\n model: string,\n): number | undefined {\n if (!model || !meta?.length) return undefined;\n const exact = meta.find((m) => m.id === model && m.contextWindow);\n if (exact?.contextWindow) return exact.contextWindow;\n const prefix = meta.find((m) => m.contextWindow && model.startsWith(m.id));\n return prefix?.contextWindow;\n}\n\n/** 模型列表查询结果 */\nexport interface ModelListResult {\n /** 模型 id 列表(**对话模型优先**,非对话模型排在末尾) */\n models: string[];\n /** 实际命中的端点 */\n endpoint: string;\n /** 疑似非对话模型(向量/语音/图像/专用通道等);这些模型不能用作 Agent 后端 */\n nonChat: string[];\n /** 模型元数据(上下文长度等);仅当供应商 /models 响应包含这些信息时才有值 */\n modelMeta?: ModelMeta[];\n}\n\n/**\n * 明显**不提供** OpenAI 兼容对话接口的模型特征。\n *\n * 网关的 /models 往往是\"全量目录\"而非\"可用对话模型\":阿里云百炼会把\n * 向量、重排、语音识别/合成、实时音视频、图像生成、以及专用通道\n * (cmaas-* 机密推理、内部运维模型)一并列出。把它们排在前面会让\n * 用户在 /connect 里一眼选到不可用的模型(实测 `cmaas-qwen3.8-flash`\n * 在 /chat/completions 与 /responses 上都回 400 InvalidParameter)。\n */\nconst NON_CHAT_PATTERNS: RegExp[] = [\n /embedding/i,\n /rerank/i,\n /(^|[-_/.])(asr|tts)([-_/.]|$)/i,\n /speech|voice|unisound|audio-?coding/i,\n /realtime|livetranslate|(^|[-_])s2s([-_]|$)/i,\n /image|wan\\d|z-image|paint|draw|flux|kolors|cogview/i,\n /deep-research|search-planning/i,\n /(^|[-_])cmaas[-_]|confidential/i,\n /(^|[-_])test[-_]|auto-handle|gpu-auto/i,\n];\n\n/** 是否可作为 Agent 的对话后端(启发式判断,不确定时按\"可用\"处理) */\nexport function isChatModel(modelId: string): boolean {\n return !NON_CHAT_PATTERNS.some((re) => re.test(modelId));\n}\n\n/** 对话模型在前、非对话模型在后,两组内保持服务端原顺序 */\nfunction rankModels(models: string[]): { ordered: string[]; nonChat: string[] } {\n const chat: string[] = [];\n const nonChat: string[] = [];\n for (const m of models) (isChatModel(m) ? chat : nonChat).push(m);\n return { ordered: [...chat, ...nonChat], nonChat };\n}\n\n/** 解析 OpenAI 兼容 /models 响应({ data: [{ id, ... }] }),提取模型 id 及可选的上下文长度 */\nfunction parseModelList(body: unknown): { models: string[]; meta: ModelMeta[] } {\n if (!body || typeof body !== \"object\") return { models: [], meta: [] };\n const data = (body as Record<string, unknown>).data;\n if (!Array.isArray(data)) return { models: [], meta: [] };\n const models: string[] = [];\n const meta: ModelMeta[] = [];\n for (const item of data) {\n if (!item || typeof item !== \"object\") continue;\n const rec = item as Record<string, unknown>;\n const id = rec.id;\n if (typeof id !== \"string\" || !id.trim()) continue;\n models.push(id);\n // 各供应商的字段名不一:OpenRouter=context_length、部分网关=context_window /\n // max_context_length / max_input_tokens、vLLM=max_model_len。取首个合法值。\n const ctx =\n rec.context_length ??\n rec.context_window ??\n rec.max_context_length ??\n rec.max_input_tokens ??\n rec.max_model_len;\n if (typeof ctx === \"number\" && ctx > 0) {\n meta.push({ id, contextWindow: ctx });\n }\n }\n return { models, meta };\n}\n\n/**\n * `/models` 的候选 URL 组合。\n *\n * 各家的「模型列表」路径并不统一,而 Base URL 的写法更是五花八门\n * (`https://api.deepseek.com`、`…/v1`、`…/compatible-mode/v1`,甚至把\n * `/models`、`/chat/completions` 整段粘进来)。只试 `${base}/models`\n * 与 `${base}/v1/models` 两种时,填了 `/v1` 的 DeepSeek、填了网关前缀的\n * 自建服务都会「刷新出 0 个模型」而不报错,用户无从判断哪里错了。\n *\n * 这里按「最可能命中的顺序」生成候选:先原样,再去掉版本段(`/v1`、\n * `/compatible-mode/v1`…)试根路径,最后补 `/api/v1`、`/api` 前缀。\n * 调用方按顺序尝试,命中第一个返回非空列表的端点。\n */\nexport function modelListCandidates(baseURL: string): string[] {\n const base = baseURL.replace(/\\/+$/, \"\");\n if (!base) return [];\n const out: string[] = [];\n const add = (u: string) => {\n if (u && !out.includes(u)) out.push(u);\n };\n\n // 1. 用户填的 base 原样(…/v1、…/compatible-mode/v1 都能直接命中)\n add(`${base}/models`);\n // 2. 补标准 /v1 前缀(base 只填到域名时最常见)\n if (!/\\/v\\d+[a-z0-9]*$/i.test(base)) add(`${base}/v1/models`);\n\n // 3. 去掉版本段后的根:DeepSeek 官方是 GET /models,不挂在 /v1 下\n const root = stripVersionSuffix(base);\n if (root !== base) {\n add(`${root}/models`);\n add(`${root}/v1/models`);\n }\n // 4. 部分网关把控制面 API 挂在 /api 下\n add(`${root}/api/v1/models`);\n add(`${root}/api/models`);\n return out.slice(0, 6);\n}\n\n/** 去掉结尾的版本/兼容模式段(`/v1`、`/v1beta`、`/compatible-mode/v1`、`/openai/v1`…) */\nexport function stripVersionSuffix(base: string): string {\n const s = base.replace(/\\/+$/, \"\");\n const suffixes = [\n \"/compatible-mode/v1\",\n \"/compatible-mode\",\n \"/openai/v1\",\n \"/v1beta\",\n \"/v1\",\n // DeepSeek 的 /beta 只用于前缀补全(FIM),模型列表仍在根路径\n \"/beta\",\n ];\n for (const suf of suffixes) {\n if (s.toLowerCase().endsWith(suf)) return s.slice(0, -suf.length).replace(/\\/+$/, \"\");\n }\n return s;\n}\n\n/** 一次刷新尝试的结果(用于把失败原因回传给界面) */\nexport interface ModelFetchAttempt {\n /** 尝试的完整 URL */\n url: string;\n /** HTTP 状态码;网络层失败(超时/DNS)时无 */\n status?: number;\n /** 失败原因摘要(供应商错误体 / 网络错误) */\n error?: string;\n}\n\n/** 刷新模型列表的诊断结果 */\nexport interface ModelFetchDiagnostics {\n /** 成功时的结果;全部候选都失败为 null */\n result: ModelListResult | null;\n /** 依次尝试过的端点与原因 */\n attempts: ModelFetchAttempt[];\n /** 是否因鉴权失败提前终止(此时换路径也没有意义) */\n authFailed: boolean;\n}\n\n/** 读一小段错误响应体(供提示用),失败不抛 */\nasync function errorSnippet(resp: unknown): Promise<string | undefined> {\n const r = resp as { text?: () => Promise<string> };\n if (typeof r.text !== \"function\") return undefined;\n try {\n const text = (await r.text()).trim();\n if (!text) return undefined;\n // 优先取结构化错误里的 message:原样截断会把 JSON 截成半句(很难看)\n try {\n const body = JSON.parse(text) as Record<string, unknown>;\n const err = body[\"error\"];\n const msg =\n (err && typeof err === \"object\" ? String((err as Record<string, unknown>)[\"message\"] ?? \"\") : \"\") ||\n String(body[\"message\"] ?? \"\");\n if (msg) {\n // 去掉 request_id 之类的噪声,同一原因在多个候选上重复出现时更易读\n return msg.replace(/\\s*\\(request_id:[^)]*\\)/gi, \"\").slice(0, 160);\n }\n } catch {\n // 非 JSON 错误体(网关 HTML 等):按原文截断\n }\n return text.replace(/\\s+/g, \" \").slice(0, 200);\n } catch {\n return undefined;\n }\n}\n\n/**\n * 刷新模型列表(带诊断)。\n *\n * 与 `fetchModels` 的区别:全部失败时仍返回「试过哪些 URL、各自为什么失败」,\n * 让上层能把原因写进错误提示——静默返回 null 只会让界面显示「✓ 0」。\n *\n * @param opts.budgetMs 总预算;超出后剩余候选直接标记为未尝试(避免一次点击\n * 把节点事件循环占住几十秒,FleetCommands 的 6s 上限依赖它)\n * @param opts.timeoutMs 单个端点超时(默认 8s)\n */\nexport async function fetchModelsWithDiagnostics(\n baseURL: string,\n apiKey: string,\n preferredModel?: string,\n opts: { budgetMs?: number; timeoutMs?: number } = {},\n): Promise<ModelFetchDiagnostics> {\n // 归一化:补协议、去端点后缀;Key 去掉 CRLF/引号(Windows/openEuler 粘贴常见)\n const base = normalizeBaseURL(baseURL) ?? baseURL.replace(/\\/+$/, \"\");\n const key = cleanLlmValue(apiKey) ?? \"\";\n const budgetMs = opts.budgetMs ?? 25_000;\n const timeoutMs = opts.timeoutMs ?? 8_000;\n const started = Date.now();\n const attempts: ModelFetchAttempt[] = [];\n const candidates = modelListCandidates(base);\n /** 是否见过 401/403:全部候选都失败后据此给出「Key 无效」的提示 */\n let authFailed = false;\n\n for (const url of candidates) {\n const left = budgetMs - (Date.now() - started);\n if (left < 500) {\n // 预算用尽:不再发请求,但要如实告诉用户哪些端点没试过\n attempts.push({ url, error: \"超出重试总预算,未尝试\" });\n continue;\n }\n try {\n const resp = (await doFetch(url, {\n method: \"GET\",\n headers: {\n Authorization: `Bearer ${key}`,\n Accept: \"application/json\",\n },\n signal: AbortSignal.timeout(Math.min(timeoutMs, left)),\n })) as { ok: boolean; status?: number; json: () => Promise<unknown>; text?: () => Promise<string> };\n\n if (!resp.ok) {\n const status = typeof resp.status === \"number\" ? resp.status : undefined;\n const detail = await errorSnippet(resp);\n attempts.push({ url, ...(status !== undefined ? { status } : {}), ...(detail ? { error: detail } : {}) });\n // 401/403 也继续试其它组合:少数网关对「路径不存在」也回 401,\n // 早停会漏掉真正的模型列表端点。全部失败后再按此提示 Key 问题。\n if (status === 401 || status === 403) authFailed = true;\n continue;\n }\n\n const body = (await resp.json()) as unknown;\n const { models, meta } = parseModelList(body);\n if (models.length === 0) {\n // 部分网关对不存在的路径也回 200 + 空目录:继续试下一个组合\n attempts.push({ url, error: \"返回的模型列表为空\" });\n continue;\n }\n const { ordered, nonChat } = rankModels(models);\n const filled = await fillContextWindow(base, key, ordered, meta, preferredModel, {\n budgetMs: Math.max(1_000, budgetMs - (Date.now() - started)),\n });\n return {\n result: {\n models: ordered,\n endpoint: url,\n nonChat,\n ...(filled.length > 0 ? { modelMeta: filled } : {}),\n },\n attempts,\n authFailed: false,\n };\n } catch (e) {\n // 单端点失败(超时/网络/解析),尝试下一个候选\n attempts.push({ url, error: e instanceof Error ? e.message : String(e) });\n }\n }\n return { result: null, attempts, authFailed };\n}\n\n/**\n * 把刷新失败翻译成可操作的提示。\n *\n * 典型:Base URL 填成 `https://api.deepseek.com/v1` 时,官方 `GET /models`\n * 不在 /v1 下,旧实现只会显示「刷新出 0 个」,用户完全不知道该怎么办。\n */\nexport function describeModelFetchFailure(\n baseURL: string,\n diag: ModelFetchDiagnostics,\n): string {\n const lines = diag.attempts.map((a) => {\n const why = a.status !== undefined ? `HTTP ${a.status}` : (a.error ?? \"请求失败\");\n return ` · ${a.url} → ${why}${a.status !== undefined && a.error ? `(${a.error})` : \"\"}`;\n });\n if (diag.authFailed) {\n return (\n `模型列表刷新失败:API Key 无效或无权限(Base URL:${baseURL})。` +\n `\\n已尝试:\\n${lines.join(\"\\n\")}` +\n `\\n请检查 Key 是否填写正确、是否已过期,或该 Key 是否有权访问此端点。`\n );\n }\n return (\n `模型列表刷新失败:所有候选端点都没有返回可用模型(Base URL:${baseURL})。` +\n `\\n已尝试:\\n${lines.join(\"\\n\")}` +\n `\\n请确认 Base URL 是否正确(一般填到 /v1 为止,如 https://api.deepseek.com),` +\n `以及该服务是否提供 OpenAI 兼容的 GET /models 接口。`\n );\n}\n\n/** `/models` 未给出上下文长度时,对当前(或首个)模型单体探测一次兜底 */\nasync function fillContextWindow(\n base: string,\n key: string,\n models: string[],\n meta: ModelMeta[],\n preferred?: string,\n opts: { budgetMs?: number } = {},\n): Promise<ModelMeta[]> {\n if (meta.length > 0) return meta;\n const target = preferred?.trim() || models[0];\n if (!target) return meta;\n const ctx = await fetchModelContext(base, key, target, opts.budgetMs);\n return ctx ? [{ id: target, contextWindow: ctx }] : meta;\n}\n\n/** 刷新模型列表(简单形态):只要结果,失败返回 null。需要失败原因时用 `fetchModelsWithDiagnostics`。 */\nexport async function fetchModels(\n baseURL: string,\n apiKey: string,\n /** 当前已选模型:/models 未返回上下文长度时对它单体探测一次兜底 */\n preferredModel?: string,\n): Promise<ModelListResult | null> {\n const diag = await fetchModelsWithDiagnostics(baseURL, apiKey, preferredModel);\n return diag.result;\n}\n\n/**\n * 兜底:从 `/models/{id}` 端点读取单个模型的上下文长度。\n *\n * 多数 OpenAI 兼容网关并不实现该端点(OpenAI 官方与阿里百炼都不提供),\n * 因此失败时静默返回 undefined,由引擎回退到内置的模型表。\n */\nexport async function fetchModelContext(\n baseURL: string,\n apiKey: string,\n modelId: string,\n timeoutMs = 10_000,\n): Promise<number | undefined> {\n if (!modelId) return undefined;\n const base = normalizeBaseURL(baseURL) ?? baseURL.replace(/\\/+$/, \"\");\n const key = cleanLlmValue(apiKey) ?? \"\";\n try {\n const resp = (await doFetch(`${base}/models/${encodeURIComponent(modelId)}`, {\n method: \"GET\",\n headers: { Authorization: `Bearer ${key}`, Accept: \"application/json\" },\n signal: AbortSignal.timeout(Math.max(500, timeoutMs)),\n })) as { ok: boolean; json: () => Promise<unknown> };\n if (!resp.ok) return undefined;\n const body = (await resp.json()) as unknown;\n // 兼容两种形态:`{ id, context_length }` 与 `{ data: { … } }`\n const rec = (body ?? {}) as Record<string, unknown>;\n const inner = rec.data && typeof rec.data === \"object\" && !Array.isArray(rec.data) ? rec.data : rec;\n return parseModelList({ data: [inner] }).meta[0]?.contextWindow;\n } catch {\n return undefined;\n }\n}\n\n// ===================== 模型可用性自检 =====================\n\n/** 模型自检结果 */\nexport interface ModelProbeResult {\n /** 该模型能否正常对话 */\n ok: boolean;\n /** 失败原因(provider 的 message + code / 网络错误) */\n error?: string;\n /** provider 返回的错误码(如 InvalidParameter、model_not_found) */\n code?: string;\n /** 自检请求耗时(毫秒) */\n latencyMs: number;\n}\n\n/** 从错误响应体里尽量提取可读信息(两种兼容模式风格都支持) */\nfunction extractError(body: unknown): { message: string; code?: string } {\n const b = (body ?? {}) as Record<string, unknown>;\n const e = (b.error ?? b) as Record<string, unknown>;\n const message = String(e.message ?? b.message ?? \"\").trim();\n const code = typeof e.code === \"string\" ? e.code : typeof b.code === \"string\" ? b.code : undefined;\n const requestId = typeof b.request_id === \"string\" ? b.request_id : undefined;\n return {\n message: message || \"未知错误\",\n code: code ? `${code}${requestId ? ` (request_id=${requestId})` : \"\"}` : undefined,\n };\n}\n\n/**\n * 自检:向 `/chat/completions` 发一条最小请求,确认所选模型真的可用。\n *\n * 背景:网关的 /models 是\"全量目录\",列出的模型未必能在对话接口上用\n * (阿里兼容模式对专用通道/非对话模型直接回 400 InvalidParameter)。\n * 在 /connect 里选完就跑一次,比让用户在第一次对话时才撞墙要好得多。\n *\n * 用 Chat Completions 而非 Responses:前者是全供应商的公共底线。\n */\nexport async function probeModel(\n baseURL: string,\n apiKey: string,\n model: string,\n): Promise<ModelProbeResult> {\n const base = normalizeBaseURL(baseURL) ?? baseURL.replace(/\\/+$/, \"\");\n const key = cleanLlmValue(apiKey) ?? \"\";\n const started = Date.now();\n try {\n const resp = (await doFetch(`${base}/chat/completions`, {\n method: \"POST\",\n headers: {\n \"Content-Type\": \"application/json\",\n Authorization: `Bearer ${key}`,\n },\n body: JSON.stringify({\n model,\n messages: [{ role: \"user\", content: \"ping\" }],\n // 与 Provider 共用 isReasoningModel:o 系列 / gpt-5 只接受\n // max_completion_tokens,传 max_tokens 会 400,把可用模型误判成不可用\n ...(isReasoningModel(model)\n ? { max_completion_tokens: 8 }\n : { max_tokens: 8 }),\n stream: false,\n }),\n signal: AbortSignal.timeout(20_000),\n })) as { status: number; ok: boolean; json: () => Promise<unknown> };\n\n const latencyMs = Date.now() - started;\n if (resp.ok) return { ok: true, latencyMs };\n\n let body: unknown = null;\n try {\n body = await resp.json();\n } catch {\n // 非 JSON 错误体(网关 HTML 等)\n }\n const { message, code } = extractError(body);\n return {\n ok: false,\n error: `HTTP ${resp.status} ${message}`,\n ...(code ? { code } : {}),\n latencyMs,\n };\n } catch (err) {\n return {\n ok: false,\n error: err instanceof Error ? err.message : String(err),\n latencyMs: Date.now() - started,\n };\n }\n}\n","/**\n * 本机配置 / 状态目录的跨平台解析\n *\n * POSIX 沿用 XDG 约定;Windows 上 `HOME` 在 cmd / PowerShell 中通常不存在,\n * 若直接回退 `/tmp` 会把配置写到 `\\tmp\\...`(当前盘符根),且与\n * `%APPDATA%` 的常规位置不一致。统一顺序:显式 XDG 变量(用户显式指定,\n * 优先级最高)→ 平台原生目录 → XDG 默认位置(家目录)→ 临时目录。\n */\nimport * as os from \"os\";\nimport * as path from \"path\";\n\n/** 应用目录名(`~/.config/zqagent` 的最后一段) */\nconst APP_DIR_NAME = \"zqagent\";\n\n/**\n * 节点默认数据目录名(相对启动目录,即 `./.zqagent_data`)。\n * server / CLI 在既无 `--data-dir` 也无 `DATA_DIR` 时用它兜底,\n * 集中在此避免各处字面量走散。\n */\nexport const DEFAULT_DATA_DIR = \".zqagent_data\";\n\n/**\n * 官方 Hub 基址(不带结尾斜杠):CLI 在既无 `--hub` 也无 `HUB_URL` 时用它兜底\n * (首个交互式接入提示的默认值)。自建 Hub 请用 `--hub` / `HUB_URL` 覆盖。\n */\nexport const DEFAULT_HUB_URL = \"https://hub.cd-zq.com\";\n\n/** 是否为 Windows(仅此处需要区分平台;APPDATA 在别的系统上可能是无关变量) */\nconst IS_WINDOWS = process.platform === \"win32\";\n\n/** 用户主目录:`HOME` → Windows `USERPROFILE` → `os.homedir()` → 临时目录 */\nfunction homeDir(): string {\n return (\n process.env.HOME || process.env.USERPROFILE || os.homedir() || os.tmpdir()\n );\n}\n\n/** 配置目录:`$XDG_CONFIG_HOME/zqagent`、Windows `%APPDATA%\\zqagent`、其余 `~/.config/zqagent` */\nexport function configDir(): string {\n const base =\n process.env.XDG_CONFIG_HOME ||\n (IS_WINDOWS ? process.env.APPDATA : undefined) ||\n path.join(homeDir(), \".config\");\n return path.join(base, APP_DIR_NAME);\n}\n\n/** 状态目录:`$XDG_STATE_HOME/zqagent`、Windows `%LOCALAPPDATA%\\zqagent`、其余 `~/.local/state/zqagent` */\nexport function stateDir(): string {\n const base =\n process.env.XDG_STATE_HOME ||\n (IS_WINDOWS ? process.env.LOCALAPPDATA : undefined) ||\n path.join(homeDir(), \".local\", \"state\");\n return path.join(base, APP_DIR_NAME);\n}\n"],"mappings":"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","names":["path","Agent","net","lookup","Agent"]}
1
+ {"version":3,"sources":["../../src/llmConfig.ts","../../src/crypto/secrets.ts","../../src/providers/proxy.ts","../../../../node_modules/.pnpm/socks-proxy-agent@9.0.0_supports-color@7.2.0/node_modules/socks-proxy-agent/src/index.ts","../../../../node_modules/.pnpm/agent-base@8.0.0/node_modules/agent-base/src/index.ts","../../src/providers/diagnostics.ts","../../src/providers/models.ts","../../src/paths.ts"],"sourcesContent":["/**\n * LLM 配置统一存储(CLI `/connect` 与 Server「节点配置」共用同一份文件)\n *\n * 位置:`~/.config/zqagent/connections.json`(可用 `XDG_CONFIG_HOME` 覆盖;\n * 或用 `ZQAGENT_LLM_CONFIG_FILE` 直接指定文件,便于测试与自定义部署)。\n * 同一台机器上,CLI、以本机用户身份运行的 server、桌面端内嵌 sidecar 读写的都是这一份,\n * 因此任一端的增删改都会立即表现在其它端。\n *\n * 格式 v3:\n * { version: 3, activeId, providers: [{ id, name, type, baseURL, model,\n * reasoningEffort, apiKeyEnc, models, nonChat, modelMeta }], migratedFrom }\n *\n * - `apiKeyEnc`:AES-256-GCM 密文(主密钥与配置同目录,0600)。\n * - `type` 为预设 id(合并了 CLI 的 `provider` 与 Web 的 `type` 两个历史字段名)。\n * - 兼容读取旧格式并自动迁移:CLI 的 v2 / 更早的单连接格式,以及\n * Server 的 `{DATA_DIR}/node-llm.json`(明文 key → 加密)。\n */\nimport * as fs from \"fs\";\nimport * as path from \"path\";\nimport * as crypto from \"crypto\";\nimport type { ReasoningEffort } from \"./engine/types.js\";\nimport { loadOrCreateMasterKey, encryptWithKey, decryptWithKey } from \"./crypto/secrets.js\";\nimport { normalizeModelMeta, type ModelMeta } from \"./providers/models.js\";\nimport { configDir, stateDir } from \"./paths.js\";\nimport { llmPreset } from \"@zqagent/shared/llm-presets\";\nimport { normApiMode, normLlmTimeoutMs, type ApiMode } from \"./providers/diagnostics.js\";\n\n/**\n * 统一配置文件路径(惰性解析:读取时才计算,便于测试注入环境变量)。\n * 优先级:`ZQAGENT_LLM_CONFIG_FILE` → `configDir()/connections.json`。\n */\nexport function llmConfigFile(): string {\n const override = process.env.ZQAGENT_LLM_CONFIG_FILE?.trim();\n if (override) return override;\n return path.join(configDir(), \"connections.json\");\n}\n\n/** 单个供应商条目(含加密后的 key) */\nexport interface LlmProviderEntry {\n id: string;\n /** 展示名 */\n name: string;\n /** 预设 id(`llmPresets` 的 id;历史 provider/type 合并到此) */\n type: string;\n baseURL: string;\n model: string;\n /** 推理强度;不支持或未设置时为 null */\n reasoningEffort: ReasoningEffort | null;\n /**\n * 单次请求超时(毫秒);null 表示跟随全局 `OPENAI_TIMEOUT_MS` / 默认值。\n *\n * 必须按供应商配置:推理模型与普通模型的合理预算差一个数量级,用一个全局值\n * 只能二选一(要么慢模型被掐断,要么快模型失败时干等十分钟)。\n */\n timeoutMs?: number | null;\n /**\n * API 模式;null/auto 表示「先 Responses,失败降级 Chat」。\n *\n * 与 `reasoningEffort` 同理:一个节点上可能同时挂着只支持 Responses 与只支持\n * Chat 的两家供应商,全局 `OPENAI_FORCE_API_MODE` 无法同时满足。\n */\n apiMode?: ApiMode | null;\n /** API Key 密文(AES-256-GCM,`v1:iv:tag:ciphertext`) */\n apiKeyEnc: string;\n /**\n * 该供应商**不需要** API Key(本地 Ollama / llama.cpp、内网网关)。\n *\n * 必须是显式开关,不能靠「Key 为空」推断:Key 为空也可能只是还没填,\n * 那种情况下要回落到环境变量 `OPENAI_API_KEY`,否则原本正常工作的配置会\n * 突然发出一个占位 Key 而收到 401。\n */\n noApiKey?: boolean;\n /** 上次刷新得到的候选模型(可选) */\n models?: string[];\n /** 候选中非对话模型的子集(可选,仅用于提示) */\n nonChat?: string[];\n /**\n * 刷新模型列表时从供应商 `/models` 拿到的元数据(含真实上下文长度)。\n * 落盘后供引擎按实际模型精确计算压缩阈值(见 `providers/models.ts`)。\n */\n modelMeta?: ModelMeta[];\n}\n\nexport interface LlmConfigFile {\n version: 3;\n activeId: string | null;\n providers: LlmProviderEntry[];\n /** 已导入过的旧配置来源(绝对路径),避免重复合并 */\n migratedFrom?: string[];\n}\n\n// ===================== 加解密 =====================\n\n/** 主密钥按「配置目录」缓存(不同配置目录用不同密钥;测试隔离后互不影响) */\nconst keyCache = new Map<string, Buffer>();\n\nfunction masterKeyFileFor(file: string): string {\n return path.join(path.dirname(file), \"secret.key\");\n}\n\nfunction getMasterKey(file: string): Buffer {\n const keyFile = masterKeyFileFor(file);\n let key = keyCache.get(keyFile);\n if (!key) {\n key = loadOrCreateMasterKey(keyFile);\n keyCache.set(keyFile, key);\n }\n return key;\n}\n\n/** 加密 API Key;`file` 为配置文件路径(缺省用统一路径,决定主密钥位置) */\nexport function encryptKey(plain: string, file = llmConfigFile()): string {\n return encryptWithKey(getMasterKey(file), plain);\n}\n\n/** 解密 API Key;格式非法 / 主密钥变化返回 null */\nexport function decryptKey(payload: string, file = llmConfigFile()): string | null {\n return decryptWithKey(getMasterKey(file), payload);\n}\n\n/** 生成新的供应商 id */\nexport function newProviderId(): string {\n return crypto.randomBytes(8).toString(\"hex\");\n}\n\n// ===================== 规范化 =====================\n\nfunction normEffort(v: unknown): ReasoningEffort | null {\n return v === \"low\" || v === \"medium\" || v === \"high\" ? v : null;\n}\n\n/** 规范化模型列表(去重、限长) */\nfunction normModelList(v: unknown): string[] | undefined {\n if (!Array.isArray(v)) return undefined;\n const out: string[] = [];\n const seen = new Set<string>();\n for (const x of v) {\n const s = String(x ?? \"\").trim();\n if (!s || seen.has(s)) continue;\n seen.add(s);\n out.push(s);\n if (out.length >= 500) break;\n }\n return out.length ? out : undefined;\n}\n\n/** 由端点反查预设 id(迁移无 type 的旧数据) */\nfunction guessTypeByBaseURL(baseURL: string): string {\n const u = baseURL.toLowerCase().replace(/\\/+$/, \"\");\n if (!u) return \"custom\";\n for (const p of KNOWN_PRESETS) {\n const b = p.baseURL.toLowerCase().replace(/\\/+$/, \"\");\n if (b && (u === b || u.startsWith(b))) return p.id;\n }\n return \"custom\";\n}\n\nconst KNOWN_PRESETS = [\"openai\", \"deepseek\", \"qwen\", \"zhipu\", \"moonshot\", \"xiaomi\"].map(\n (id) => ({ id, baseURL: llmPreset(id)?.baseURL ?? \"\" }),\n);\n\n/** 解析单个条目(兼容 v3 / v2 / server 明文 key 三种形态) */\nfunction parseEntry(o: unknown, file: string): LlmProviderEntry | null {\n if (!o || typeof o !== \"object\") return null;\n const r = o as Record<string, unknown>;\n const baseURL = typeof r.baseURL === \"string\" ? r.baseURL : \"\";\n const model = typeof r.model === \"string\" ? r.model : \"\";\n\n // type:v3 的 type → v2 的 provider → 由 baseURL 反查预设 → custom\n let type = String(r.type ?? r.provider ?? \"\").trim();\n if (!type) type = guessTypeByBaseURL(baseURL);\n\n // key:v3/v2 用密文;server 旧格式是明文 apiKey,迁移时加密\n let apiKeyEnc = typeof r.apiKeyEnc === \"string\" ? r.apiKeyEnc : \"\";\n const plain = typeof r.apiKey === \"string\" ? r.apiKey.trim() : \"\";\n if (!apiKeyEnc && plain) apiKeyEnc = encryptKey(plain, file);\n\n // 迁移时可能既无 key 也无模型(如未填完的条目):仍保留,交由 UI 补全\n if (!baseURL && !model && !apiKeyEnc && !type) return null;\n\n const models = normModelList(r.models);\n const nonChat = normModelList(r.nonChat);\n const modelMeta = normalizeModelMeta(r.modelMeta);\n const name = String(r.name ?? \"\").trim() || autoProviderName(type, model);\n return {\n id: typeof r.id === \"string\" && r.id ? r.id : newProviderId(),\n name,\n type,\n baseURL,\n model,\n reasoningEffort: normEffort(r.reasoningEffort),\n timeoutMs: normLlmTimeoutMs(r.timeoutMs),\n apiMode: normApiMode(r.apiMode),\n apiKeyEnc,\n ...(r.noApiKey === true ? { noApiKey: true } : {}),\n ...(models ? { models } : {}),\n ...(nonChat ? { nonChat } : {}),\n ...(modelMeta ? { modelMeta } : {}),\n };\n}\n\n/** 自动展示名:预设名 · 模型 */\nexport function autoProviderName(type: string, model: string): string {\n const name = llmPreset(type)?.name ?? type ?? \"\";\n return model ? `${name} · ${model}` : name;\n}\n\nconst PRESET_NAMES = new Set(\n [\"openai\", \"deepseek\", \"qwen\", \"zhipu\", \"moonshot\", \"xiaomi\", \"custom\"].map(\n (id) => llmPreset(id)?.name ?? \"\",\n ),\n);\n\n/**\n * 修正历史遗留的陈旧自动名(供应商标签变了、或模型换了但名字没跟上)。\n * 只动「看起来是自动名 / 顶着别家预设标签」的名字,用户自起的名字保留。\n * @returns 是否发生修改\n */\nexport function fixStaleName(p: LlmProviderEntry): boolean {\n const label = llmPreset(p.type)?.name ?? p.type;\n const fresh = autoProviderName(p.type, p.model);\n if (p.name === fresh) return false;\n const looksAuto =\n p.name === label || p.name.startsWith(`${label} · `) || PRESET_NAMES.has(p.name);\n if (!looksAuto) return false;\n p.name = fresh;\n return true;\n}\n\n// ===================== 读 / 写 =====================\n\nfunction emptyConfig(): LlmConfigFile {\n return { version: 3, activeId: null, providers: [] };\n}\n\n/** 规范化列表:按 id 去重(后者覆盖前者)、activeId 回退到首项 */\nfunction normalize(f: LlmConfigFile): LlmConfigFile {\n const byId = new Map<string, LlmProviderEntry>();\n for (const p of f.providers) byId.set(p.id, p);\n const providers = [...byId.values()];\n const activeId = providers.some((p) => p.id === f.activeId)\n ? f.activeId\n : (providers[0]?.id ?? null);\n return { ...f, providers, activeId };\n}\n\n/**\n * 读取并规范化一个配置文件。\n * @param file 要读取的文件\n * @param keyFile 主密钥所在配置(迁移时应传**目标**配置:导入的明文 key 要用目标密钥加密)\n */\nfunction readRaw(file: string, keyFile = file): LlmConfigFile | null {\n if (!fs.existsSync(file)) return null;\n let raw: unknown;\n try {\n raw = JSON.parse(fs.readFileSync(file, \"utf8\"));\n } catch {\n return null;\n }\n if (!raw || typeof raw !== \"object\") return null;\n const o = raw as Record<string, unknown>;\n\n // v3 / v2:providers 列表\n if (Array.isArray(o.providers)) {\n const providers = o.providers\n .map((x) => parseEntry(x, keyFile))\n .filter((p): p is LlmProviderEntry => p !== null);\n const activeId =\n typeof o.activeId === \"string\"\n ? o.activeId\n : typeof o.defaultProviderId === \"string\"\n ? o.defaultProviderId\n : null;\n const migratedFrom = Array.isArray(o.migratedFrom)\n ? o.migratedFrom.filter((x): x is string => typeof x === \"string\")\n : undefined;\n return normalize({\n version: 3,\n activeId,\n providers,\n ...(migratedFrom ? { migratedFrom } : {}),\n });\n }\n\n // 更早的「单连接对象」格式\n const single = parseEntry(o, keyFile);\n if (single) return { version: 3, activeId: single.id, providers: [single] };\n return null;\n}\n\n/** 把外部配置(如 server 的 node-llm.json)合并进主配置 */\nfunction mergeExternal(\n base: LlmConfigFile,\n file: string,\n targetFile: string,\n): { config: LlmConfigFile; changed: boolean } {\n const migrated = base.migratedFrom ?? [];\n if (migrated.includes(file)) return { config: base, changed: false };\n // 用目标配置的主密钥加密导入的明文 key,保证之后能被同一密钥解开\n const ext = readRaw(file, targetFile);\n if (!ext || ext.providers.length === 0) {\n return { config: base, changed: false };\n }\n // 同名 id 保留主配置中的那份;其余追加\n const have = new Set(base.providers.map((p) => p.id));\n const added = ext.providers.filter((p) => !have.has(p.id));\n const config = normalize({\n ...base,\n providers: [...base.providers, ...added],\n activeId: base.activeId ?? ext.activeId,\n migratedFrom: [...migrated, file],\n });\n return { config, changed: true };\n}\n\n/**\n * 读取统一配置(含自动迁移)。\n *\n * @param opts.file 配置文件路径(缺省 `llmConfigFile()`)\n * @param opts.legacyFiles 需要合并的旧配置文件(如 `{DATA_DIR}/node-llm.json`),\n * 仅在主配置尚未记录该来源时导入一次\n */\nexport function readLlmConfig(opts: { file?: string; legacyFiles?: string[] } = {}): LlmConfigFile {\n const file = opts.file ?? llmConfigFile();\n let f = readRaw(file) ?? emptyConfig();\n let changed = false;\n\n // 顺手修正陈旧自动名\n // 必须遍历完整个列表:fixStaleName 带副作用,some 命中首个 true 会短路\n if (f.providers.map(fixStaleName).some(Boolean)) changed = true;\n\n for (const legacy of opts.legacyFiles ?? []) {\n const r = mergeExternal(f, legacy, file);\n f = r.config;\n changed = changed || r.changed;\n }\n\n // v2 / 单连接 / 合并过旧文件:规范化后落盘固化\n if (changed || !fs.existsSync(file)) writeLlmConfig(f, file);\n return f;\n}\n\n/**\n * 写入统一配置(0600)。\n *\n * 先写同目录临时文件再 rename:CLI、桌面内嵌 sidecar、节点服务**共享同一份**\n * 配置文件,直接 `writeFileSync` 会让并发的读者读到写了一半的 JSON\n * (`readRaw` 解析失败会退化成空配置,界面上表现为「供应商全没了」,用户\n * 此时一保存就把真配置覆盖掉)。rename 在同一文件系统内是原子的,读者要么\n * 看到旧内容、要么看到新内容。\n */\nexport function writeLlmConfig(f: LlmConfigFile, file = llmConfigFile()): void {\n const next = normalize(f);\n const dir = path.dirname(file);\n fs.mkdirSync(dir, { recursive: true, mode: 0o700 });\n const body = JSON.stringify(next, null, 2) + \"\\n\";\n const tmp = path.join(dir, `.${path.basename(file)}.${process.pid}.${Date.now()}.tmp`);\n try {\n fs.writeFileSync(tmp, body, { mode: 0o600, encoding: \"utf8\" });\n fs.renameSync(tmp, file);\n } catch (e) {\n // 清理残留临时文件后原样抛出:写失败必须让调用方知道,不能静默丢配置\n try {\n fs.unlinkSync(tmp);\n } catch {\n /* 已经不在了 */\n }\n throw e;\n }\n}\n\n/**\n * 常见的历史配置来源(供迁移合并):\n * 旧版桌面端内嵌 server 的默认数据目录 `{stateDir}/data/node-llm.json`\n * (现默认目录为 `{stateDir}/.zqagent_data`,此处保留旧路径以便老用户配置被自动接管)。\n * 桌面端与 CLI 同属一个登录用户,这样先配桌面端也能被 CLI 立刻看到。\n */\nexport function commonLegacyFiles(): string[] {\n return [path.join(stateDir(), \"data\", \"node-llm.json\")];\n}\n","// 已迁移至 @zqagent/shared(core 与 hub 共用);此文件保留为兼容再导出。\nexport * from \"@zqagent/shared/secrets\";\n","/**\n * 代理出口(LLM 相关 HTTP 请求统一走这里)\n *\n * 支持 `SOCKS5_PROXY`(`socks5://` / `socks5h://`),供 node-fetch 使用:\n * - `fetchModels()`(/connect 刷新模型)→ node-fetch + agent;未配置时代理为 null。\n * - OpenAI SDK 的请求默认走 Node 内置 fetch(undici),undici **不读**\n * HTTP_PROXY/HTTPS_PROXY/ALL_PROXY 环境变量;因此配置了 SOCKS5 代理时,\n * 这里同时提供 `makeSocksFetch()`,把它注入 SDK 的 `fetch` 选项,\n * 保证「对话」与「刷新模型」走同一条网络路径(避免一边通一边不通)。\n */\nimport nodeFetch from \"node-fetch\";\nimport { SocksProxyAgent } from \"socks-proxy-agent\";\n\n/** SOCKS5 代理地址(环境变量 SOCKS5_PROXY),未设置时为 null */\nexport function socksProxyUrl(): string | null {\n const raw = (process.env.SOCKS5_PROXY ?? \"\").trim();\n return raw || null;\n}\n\n/** 是否为 socks 代理地址 */\nfunction isSocks(raw: string): boolean {\n return /^socks/i.test(raw);\n}\n\n/** 全局共享的 SOCKS5 Agent;未配置或构造失败时为 null(直连) */\nlet cached: { url: string; agent: SocksProxyAgent | null } | null = null;\n\nexport function socksAgent(): SocksProxyAgent | null {\n const url = socksProxyUrl();\n if (!url) return null;\n if (cached?.url === url) return cached.agent;\n let agent: SocksProxyAgent | null = null;\n if (isSocks(url)) {\n try {\n agent = new SocksProxyAgent(url);\n } catch {\n try {\n agent = new SocksProxyAgent(url.replace(/^socks5h:\\/\\//, \"socks5://\"));\n } catch {\n agent = null;\n }\n }\n }\n cached = { url, agent };\n return agent;\n}\n\n/**\n * 检测到「不支持的环境变量代理」时给出可执行提示(否则返回 null)。\n * undici/内置 fetch 不读这些变量,本项目也不引入 http(s) 代理 agent。\n */\nexport function unsupportedProxyHint(): string | null {\n const keys = [\"HTTPS_PROXY\", \"https_proxy\", \"HTTP_PROXY\", \"http_proxy\", \"ALL_PROXY\", \"all_proxy\"];\n const hit = keys.filter((k) => {\n const v = (process.env[k] ?? \"\").trim();\n return v && !isSocks(v);\n });\n if (hit.length === 0) return null;\n return `检测到 ${hit[0]}=${process.env[hit[0]]}(HTTP 代理暂不支持):请改为 SOCKS5_PROXY=socks5://<host>:<port>(或让本地代理开放 socks5 端口)`;\n}\n\n/** 用 node-fetch + socks agent 实现 fetch(保持与全局 fetch 相同的调用签名) */\nexport function makeSocksFetch(agent: SocksProxyAgent): typeof fetch {\n return ((url: unknown, init?: unknown) =>\n nodeFetch(String(url), { ...(init as object), agent } as never)) as unknown as typeof fetch;\n}\n",null,null,"/**\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 请求超时(毫秒) */\nexport const DEFAULT_LLM_TIMEOUT_MS = 600_000;\n\n/**\n * 规范化单次请求超时(毫秒)。\n *\n * 允许**每个供应商**各自设置:慢模型(长思考/大上下文)需要更长的预算,\n * 而本地小模型希望快速失败。取值非法(≤0、非数字)时返回 null,由调用方\n * 回落到 `llmTimeoutMs()` 的全局默认。\n *\n * 上限 30 分钟:再长就属于「环境有问题」,让 SDK 一直挂着只会让人以为卡死。\n */\nexport function normLlmTimeoutMs(v: unknown): number | null {\n if (v === null || v === undefined || v === \"\") return null;\n const n = Number(v);\n if (!Number.isFinite(n) || n <= 0) return null;\n return Math.min(Math.round(n), 30 * 60_000);\n}\n\n/** LLM 请求超时(毫秒):显式值 > OPENAI_TIMEOUT_MS > 默认 10 分钟 */\nexport function llmTimeoutMs(explicit?: number | null): number {\n const fromArg = normLlmTimeoutMs(explicit);\n if (fromArg) return fromArg;\n const n = Number(process.env.OPENAI_TIMEOUT_MS);\n return Number.isFinite(n) && n > 0 ? n : DEFAULT_LLM_TIMEOUT_MS;\n}\n\n/** API 模式:auto = 先试 Responses,失败再降级 Chat */\nexport type ApiMode = \"auto\" | \"responses\" | \"chat\";\n\n/**\n * 规范化 API 模式。\n *\n * 历史上只能用全局环境变量 `OPENAI_FORCE_API_MODE` 强制,导致「一个节点上\n * 不同供应商需要不同模式」时无法表达(如供应商 A 只支持 Responses,供应商 B\n * 只支持 Chat)。现在允许按供应商配置,环境变量仍作为全局兜底。\n */\nexport function normApiMode(v: unknown): ApiMode | null {\n const s = typeof v === \"string\" ? v.trim().toLowerCase() : \"\";\n if (s === \"auto\" || s === \"responses\" || s === \"chat\") return s;\n return null;\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","/**\n * LLM 可用模型列表(OpenAI 兼容 /models 接口)\n *\n * 供 CLI(/connect 刷新模型列表)与 Server(GET /api/v1/models)共用。\n * 支持 SOCKS5_PROXY 走代理出口;单个端点 10s 超时。\n */\nimport { makeSocksFetch, socksAgent } from \"./proxy.js\";\nimport {\n cleanLlmValue,\n isReasoningModel,\n normalizeBaseURL,\n} from \"./diagnostics.js\";\nimport {\n capabilitiesOf,\n normalizeCapabilities,\n type ModelCapability,\n} from \"@zqagent/shared/model-capabilities\";\n\n/** 统一 HTTP 请求封装:配置了 SOCKS5 代理时用 node-fetch + agent,否则用全局 fetch。 */\nasync function doFetch(url: string, init?: Record<string, unknown>): Promise<unknown> {\n const agent = socksAgent();\n if (agent) {\n const f = makeSocksFetch(agent);\n return f(url, init as RequestInit);\n }\n return (globalThis as unknown as { fetch: typeof fetch }).fetch(url, init as RequestInit);\n}\n\n/** 单个模型的元数据(从 /models 响应中提取,部分供应商提供) */\nexport interface ModelMeta {\n /** 模型 id */\n id: string;\n /** 上下文窗口大小(token 数);仅当供应商在 /models 响应中返回时才有值 */\n contextWindow?: number;\n /**\n * 该模型可接收的输入类型(用户在「节点配置 → LLM」里勾选)。\n * 未配置时按纯文本处理,见 `modelCapabilities`。\n */\n capabilities?: ModelCapability[];\n}\n\n/**\n * 规范化模型元数据列表(按 id 去重;仅保留合法的上下文长度)。\n * 供配置持久化(`llmConfig.ts`)与 Server 视图(`server/nodeLlm.ts`)共用。\n */\nexport function normalizeModelMeta(v: unknown): ModelMeta[] | undefined {\n if (!Array.isArray(v)) return undefined;\n const out: ModelMeta[] = [];\n const seen = new Set<string>();\n for (const x of v) {\n if (!x || typeof x !== \"object\") continue;\n const rec = x as Record<string, unknown>;\n const id = String(rec.id ?? \"\").trim();\n if (!id || seen.has(id)) continue;\n seen.add(id);\n const ctx = Number(rec.contextWindow);\n const capabilities = normalizeCapabilities(rec.capabilities);\n out.push({\n id,\n ...(Number.isFinite(ctx) && ctx > 0 ? { contextWindow: ctx } : {}),\n ...(capabilities ? { capabilities } : {}),\n });\n if (out.length >= 500) break;\n }\n return out;\n}\n\n/** 从模型元数据中查输入能力(精确匹配优先,其次前缀匹配;缺省仅文本) */\nexport function modelCapabilities(\n meta: ModelMeta[] | undefined,\n model: string,\n): ModelCapability[] {\n return capabilitiesOf(meta, model);\n}\n\n/**\n * 合并两份模型元数据(按 id)。\n *\n * 用途:刷新 `/models` 拿到新的上下文长度、重新拉取内置模型凭据时,\n * 不能把用户手工勾选的 `capabilities` 覆盖掉——供应商并不返回能力信息。\n * 因此 **base 的 capabilities 始终优先**,patch 只补充其余字段(上下文长度等)。\n *\n * @param base 旧元数据(提供 capabilities)\n * @param patch 新元数据(提供 contextWindow 等)\n */\nexport function mergeModelMeta(\n base: ModelMeta[] | undefined,\n patch: ModelMeta[] | undefined,\n): ModelMeta[] | undefined {\n if (!base?.length) return patch;\n if (!patch?.length) return base;\n const byId = new Map<string, ModelMeta>();\n for (const m of base) byId.set(m.id, { ...m });\n for (const m of patch) {\n const prev = byId.get(m.id);\n byId.set(m.id, {\n ...prev,\n ...m,\n ...(prev?.capabilities?.length ? { capabilities: prev.capabilities } : {}),\n });\n }\n return [...byId.values()];\n}\n\n/** 从模型元数据中查上下文长度(精确匹配优先,其次前缀匹配),未命中返回 undefined */\nexport function modelContextWindow(\n meta: ModelMeta[] | undefined,\n model: string,\n): number | undefined {\n if (!model || !meta?.length) return undefined;\n const exact = meta.find((m) => m.id === model && m.contextWindow);\n if (exact?.contextWindow) return exact.contextWindow;\n const prefix = meta.find((m) => m.contextWindow && model.startsWith(m.id));\n return prefix?.contextWindow;\n}\n\n/** 模型列表查询结果 */\nexport interface ModelListResult {\n /** 模型 id 列表(**对话模型优先**,非对话模型排在末尾) */\n models: string[];\n /** 实际命中的端点 */\n endpoint: string;\n /** 疑似非对话模型(向量/语音/图像/专用通道等);这些模型不能用作 Agent 后端 */\n nonChat: string[];\n /** 模型元数据(上下文长度等);仅当供应商 /models 响应包含这些信息时才有值 */\n modelMeta?: ModelMeta[];\n}\n\n/**\n * 明显**不提供** OpenAI 兼容对话接口的模型特征。\n *\n * 网关的 /models 往往是\"全量目录\"而非\"可用对话模型\":阿里云百炼会把\n * 向量、重排、语音识别/合成、实时音视频、图像生成、以及专用通道\n * (cmaas-* 机密推理、内部运维模型)一并列出。把它们排在前面会让\n * 用户在 /connect 里一眼选到不可用的模型(实测 `cmaas-qwen3.8-flash`\n * 在 /chat/completions 与 /responses 上都回 400 InvalidParameter)。\n */\nconst NON_CHAT_PATTERNS: RegExp[] = [\n /embedding/i,\n /rerank/i,\n /(^|[-_/.])(asr|tts)([-_/.]|$)/i,\n /speech|voice|unisound|audio-?coding/i,\n /realtime|livetranslate|(^|[-_])s2s([-_]|$)/i,\n /image|wan\\d|z-image|paint|draw|flux|kolors|cogview/i,\n /deep-research|search-planning/i,\n /(^|[-_])cmaas[-_]|confidential/i,\n /(^|[-_])test[-_]|auto-handle|gpu-auto/i,\n];\n\n/** 是否可作为 Agent 的对话后端(启发式判断,不确定时按\"可用\"处理) */\nexport function isChatModel(modelId: string): boolean {\n return !NON_CHAT_PATTERNS.some((re) => re.test(modelId));\n}\n\n/** 对话模型在前、非对话模型在后,两组内保持服务端原顺序 */\nfunction rankModels(models: string[]): { ordered: string[]; nonChat: string[] } {\n const chat: string[] = [];\n const nonChat: string[] = [];\n for (const m of models) (isChatModel(m) ? chat : nonChat).push(m);\n return { ordered: [...chat, ...nonChat], nonChat };\n}\n\n/** 解析 OpenAI 兼容 /models 响应({ data: [{ id, ... }] }),提取模型 id 及可选的上下文长度 */\nfunction parseModelList(body: unknown): { models: string[]; meta: ModelMeta[] } {\n if (!body || typeof body !== \"object\") return { models: [], meta: [] };\n const data = (body as Record<string, unknown>).data;\n if (!Array.isArray(data)) return { models: [], meta: [] };\n const models: string[] = [];\n const meta: ModelMeta[] = [];\n for (const item of data) {\n if (!item || typeof item !== \"object\") continue;\n const rec = item as Record<string, unknown>;\n const id = rec.id;\n if (typeof id !== \"string\" || !id.trim()) continue;\n models.push(id);\n // 各供应商的字段名不一:OpenRouter=context_length、部分网关=context_window /\n // max_context_length / max_input_tokens、vLLM=max_model_len。取首个合法值。\n const ctx =\n rec.context_length ??\n rec.context_window ??\n rec.max_context_length ??\n rec.max_input_tokens ??\n rec.max_model_len;\n if (typeof ctx === \"number\" && ctx > 0) {\n meta.push({ id, contextWindow: ctx });\n }\n }\n return { models, meta };\n}\n\n/**\n * `/models` 的候选 URL 组合。\n *\n * 各家的「模型列表」路径并不统一,而 Base URL 的写法更是五花八门\n * (`https://api.deepseek.com`、`…/v1`、`…/compatible-mode/v1`,甚至把\n * `/models`、`/chat/completions` 整段粘进来)。只试 `${base}/models`\n * 与 `${base}/v1/models` 两种时,填了 `/v1` 的 DeepSeek、填了网关前缀的\n * 自建服务都会「刷新出 0 个模型」而不报错,用户无从判断哪里错了。\n *\n * 这里按「最可能命中的顺序」生成候选:先原样,再去掉版本段(`/v1`、\n * `/compatible-mode/v1`…)试根路径,最后补 `/api/v1`、`/api` 前缀。\n * 调用方按顺序尝试,命中第一个返回非空列表的端点。\n */\nexport function modelListCandidates(baseURL: string): string[] {\n const base = baseURL.replace(/\\/+$/, \"\");\n if (!base) return [];\n const out: string[] = [];\n const add = (u: string) => {\n if (u && !out.includes(u)) out.push(u);\n };\n\n // 1. 用户填的 base 原样(…/v1、…/compatible-mode/v1 都能直接命中)\n add(`${base}/models`);\n // 2. 补标准 /v1 前缀(base 只填到域名时最常见)\n if (!/\\/v\\d+[a-z0-9]*$/i.test(base)) add(`${base}/v1/models`);\n\n // 3. 去掉版本段后的根:DeepSeek 官方是 GET /models,不挂在 /v1 下\n const root = stripVersionSuffix(base);\n if (root !== base) {\n add(`${root}/models`);\n add(`${root}/v1/models`);\n }\n // 4. 部分网关把控制面 API 挂在 /api 下\n add(`${root}/api/v1/models`);\n add(`${root}/api/models`);\n return out.slice(0, 6);\n}\n\n/** 去掉结尾的版本/兼容模式段(`/v1`、`/v1beta`、`/compatible-mode/v1`、`/openai/v1`…) */\nexport function stripVersionSuffix(base: string): string {\n const s = base.replace(/\\/+$/, \"\");\n const suffixes = [\n \"/compatible-mode/v1\",\n \"/compatible-mode\",\n \"/openai/v1\",\n \"/v1beta\",\n \"/v1\",\n // DeepSeek 的 /beta 只用于前缀补全(FIM),模型列表仍在根路径\n \"/beta\",\n ];\n for (const suf of suffixes) {\n if (s.toLowerCase().endsWith(suf)) return s.slice(0, -suf.length).replace(/\\/+$/, \"\");\n }\n return s;\n}\n\n/** 一次刷新尝试的结果(用于把失败原因回传给界面) */\nexport interface ModelFetchAttempt {\n /** 尝试的完整 URL */\n url: string;\n /** HTTP 状态码;网络层失败(超时/DNS)时无 */\n status?: number;\n /** 失败原因摘要(供应商错误体 / 网络错误) */\n error?: string;\n}\n\n/** 刷新模型列表的诊断结果 */\nexport interface ModelFetchDiagnostics {\n /** 成功时的结果;全部候选都失败为 null */\n result: ModelListResult | null;\n /** 依次尝试过的端点与原因 */\n attempts: ModelFetchAttempt[];\n /** 是否因鉴权失败提前终止(此时换路径也没有意义) */\n authFailed: boolean;\n}\n\n/** 读一小段错误响应体(供提示用),失败不抛 */\nasync function errorSnippet(resp: unknown): Promise<string | undefined> {\n const r = resp as { text?: () => Promise<string> };\n if (typeof r.text !== \"function\") return undefined;\n try {\n const text = (await r.text()).trim();\n if (!text) return undefined;\n // 优先取结构化错误里的 message:原样截断会把 JSON 截成半句(很难看)\n try {\n const body = JSON.parse(text) as Record<string, unknown>;\n const err = body[\"error\"];\n const msg =\n (err && typeof err === \"object\" ? String((err as Record<string, unknown>)[\"message\"] ?? \"\") : \"\") ||\n String(body[\"message\"] ?? \"\");\n if (msg) {\n // 去掉 request_id 之类的噪声,同一原因在多个候选上重复出现时更易读\n return msg.replace(/\\s*\\(request_id:[^)]*\\)/gi, \"\").slice(0, 160);\n }\n } catch {\n // 非 JSON 错误体(网关 HTML 等):按原文截断\n }\n return text.replace(/\\s+/g, \" \").slice(0, 200);\n } catch {\n return undefined;\n }\n}\n\n/**\n * 刷新模型列表(带诊断)。\n *\n * 与 `fetchModels` 的区别:全部失败时仍返回「试过哪些 URL、各自为什么失败」,\n * 让上层能把原因写进错误提示——静默返回 null 只会让界面显示「✓ 0」。\n *\n * @param opts.budgetMs 总预算;超出后剩余候选直接标记为未尝试(避免一次点击\n * 把节点事件循环占住几十秒,FleetCommands 的 6s 上限依赖它)\n * @param opts.timeoutMs 单个端点超时(默认 8s)\n */\nexport async function fetchModelsWithDiagnostics(\n baseURL: string,\n apiKey: string,\n preferredModel?: string,\n opts: { budgetMs?: number; timeoutMs?: number } = {},\n): Promise<ModelFetchDiagnostics> {\n // 归一化:补协议、去端点后缀;Key 去掉 CRLF/引号(Windows/openEuler 粘贴常见)\n const base = normalizeBaseURL(baseURL) ?? baseURL.replace(/\\/+$/, \"\");\n const key = cleanLlmValue(apiKey) ?? \"\";\n const budgetMs = opts.budgetMs ?? 25_000;\n const timeoutMs = opts.timeoutMs ?? 8_000;\n const started = Date.now();\n const attempts: ModelFetchAttempt[] = [];\n const candidates = modelListCandidates(base);\n /** 是否见过 401/403:全部候选都失败后据此给出「Key 无效」的提示 */\n let authFailed = false;\n\n for (const url of candidates) {\n const left = budgetMs - (Date.now() - started);\n if (left < 500) {\n // 预算用尽:不再发请求,但要如实告诉用户哪些端点没试过\n attempts.push({ url, error: \"超出重试总预算,未尝试\" });\n continue;\n }\n try {\n const resp = (await doFetch(url, {\n method: \"GET\",\n headers: {\n Authorization: `Bearer ${key}`,\n Accept: \"application/json\",\n },\n signal: AbortSignal.timeout(Math.min(timeoutMs, left)),\n })) as { ok: boolean; status?: number; json: () => Promise<unknown>; text?: () => Promise<string> };\n\n if (!resp.ok) {\n const status = typeof resp.status === \"number\" ? resp.status : undefined;\n const detail = await errorSnippet(resp);\n attempts.push({ url, ...(status !== undefined ? { status } : {}), ...(detail ? { error: detail } : {}) });\n // 401/403 也继续试其它组合:少数网关对「路径不存在」也回 401,\n // 早停会漏掉真正的模型列表端点。全部失败后再按此提示 Key 问题。\n if (status === 401 || status === 403) authFailed = true;\n continue;\n }\n\n const body = (await resp.json()) as unknown;\n const { models, meta } = parseModelList(body);\n if (models.length === 0) {\n // 部分网关对不存在的路径也回 200 + 空目录:继续试下一个组合\n attempts.push({ url, error: \"返回的模型列表为空\" });\n continue;\n }\n const { ordered, nonChat } = rankModels(models);\n const filled = await fillContextWindow(base, key, ordered, meta, preferredModel, {\n budgetMs: Math.max(1_000, budgetMs - (Date.now() - started)),\n });\n return {\n result: {\n models: ordered,\n endpoint: url,\n nonChat,\n ...(filled.length > 0 ? { modelMeta: filled } : {}),\n },\n attempts,\n authFailed: false,\n };\n } catch (e) {\n // 单端点失败(超时/网络/解析),尝试下一个候选\n attempts.push({ url, error: e instanceof Error ? e.message : String(e) });\n }\n }\n return { result: null, attempts, authFailed };\n}\n\n/**\n * 把刷新失败翻译成可操作的提示。\n *\n * 典型:Base URL 填成 `https://api.deepseek.com/v1` 时,官方 `GET /models`\n * 不在 /v1 下,旧实现只会显示「刷新出 0 个」,用户完全不知道该怎么办。\n */\nexport function describeModelFetchFailure(\n baseURL: string,\n diag: ModelFetchDiagnostics,\n): string {\n const lines = diag.attempts.map((a) => {\n const why = a.status !== undefined ? `HTTP ${a.status}` : (a.error ?? \"请求失败\");\n return ` · ${a.url} → ${why}${a.status !== undefined && a.error ? `(${a.error})` : \"\"}`;\n });\n if (diag.authFailed) {\n return (\n `模型列表刷新失败:API Key 无效或无权限(Base URL:${baseURL})。` +\n `\\n已尝试:\\n${lines.join(\"\\n\")}` +\n `\\n请检查 Key 是否填写正确、是否已过期,或该 Key 是否有权访问此端点。`\n );\n }\n return (\n `模型列表刷新失败:所有候选端点都没有返回可用模型(Base URL:${baseURL})。` +\n `\\n已尝试:\\n${lines.join(\"\\n\")}` +\n `\\n请确认 Base URL 是否正确(一般填到 /v1 为止,如 https://api.deepseek.com),` +\n `以及该服务是否提供 OpenAI 兼容的 GET /models 接口。`\n );\n}\n\n/** `/models` 未给出上下文长度时,对当前(或首个)模型单体探测一次兜底 */\nasync function fillContextWindow(\n base: string,\n key: string,\n models: string[],\n meta: ModelMeta[],\n preferred?: string,\n opts: { budgetMs?: number } = {},\n): Promise<ModelMeta[]> {\n if (meta.length > 0) return meta;\n const target = preferred?.trim() || models[0];\n if (!target) return meta;\n const ctx = await fetchModelContext(base, key, target, opts.budgetMs);\n return ctx ? [{ id: target, contextWindow: ctx }] : meta;\n}\n\n/** 刷新模型列表(简单形态):只要结果,失败返回 null。需要失败原因时用 `fetchModelsWithDiagnostics`。 */\nexport async function fetchModels(\n baseURL: string,\n apiKey: string,\n /** 当前已选模型:/models 未返回上下文长度时对它单体探测一次兜底 */\n preferredModel?: string,\n): Promise<ModelListResult | null> {\n const diag = await fetchModelsWithDiagnostics(baseURL, apiKey, preferredModel);\n return diag.result;\n}\n\n/**\n * 兜底:从 `/models/{id}` 端点读取单个模型的上下文长度。\n *\n * 多数 OpenAI 兼容网关并不实现该端点(OpenAI 官方与阿里百炼都不提供),\n * 因此失败时静默返回 undefined,由引擎回退到内置的模型表。\n */\nexport async function fetchModelContext(\n baseURL: string,\n apiKey: string,\n modelId: string,\n timeoutMs = 10_000,\n): Promise<number | undefined> {\n if (!modelId) return undefined;\n const base = normalizeBaseURL(baseURL) ?? baseURL.replace(/\\/+$/, \"\");\n const key = cleanLlmValue(apiKey) ?? \"\";\n try {\n const resp = (await doFetch(`${base}/models/${encodeURIComponent(modelId)}`, {\n method: \"GET\",\n headers: { Authorization: `Bearer ${key}`, Accept: \"application/json\" },\n signal: AbortSignal.timeout(Math.max(500, timeoutMs)),\n })) as { ok: boolean; json: () => Promise<unknown> };\n if (!resp.ok) return undefined;\n const body = (await resp.json()) as unknown;\n // 兼容两种形态:`{ id, context_length }` 与 `{ data: { … } }`\n const rec = (body ?? {}) as Record<string, unknown>;\n const inner = rec.data && typeof rec.data === \"object\" && !Array.isArray(rec.data) ? rec.data : rec;\n return parseModelList({ data: [inner] }).meta[0]?.contextWindow;\n } catch {\n return undefined;\n }\n}\n\n// ===================== 模型可用性自检 =====================\n\n/** 模型自检结果 */\nexport interface ModelProbeResult {\n /** 该模型能否正常对话 */\n ok: boolean;\n /** 失败原因(provider 的 message + code / 网络错误) */\n error?: string;\n /** provider 返回的错误码(如 InvalidParameter、model_not_found) */\n code?: string;\n /** 自检请求耗时(毫秒) */\n latencyMs: number;\n}\n\n/** 从错误响应体里尽量提取可读信息(两种兼容模式风格都支持) */\nfunction extractError(body: unknown): { message: string; code?: string } {\n const b = (body ?? {}) as Record<string, unknown>;\n const e = (b.error ?? b) as Record<string, unknown>;\n const message = String(e.message ?? b.message ?? \"\").trim();\n const code = typeof e.code === \"string\" ? e.code : typeof b.code === \"string\" ? b.code : undefined;\n const requestId = typeof b.request_id === \"string\" ? b.request_id : undefined;\n return {\n message: message || \"未知错误\",\n code: code ? `${code}${requestId ? ` (request_id=${requestId})` : \"\"}` : undefined,\n };\n}\n\n/**\n * 自检:向 `/chat/completions` 发一条最小请求,确认所选模型真的可用。\n *\n * 背景:网关的 /models 是\"全量目录\",列出的模型未必能在对话接口上用\n * (阿里兼容模式对专用通道/非对话模型直接回 400 InvalidParameter)。\n * 在 /connect 里选完就跑一次,比让用户在第一次对话时才撞墙要好得多。\n *\n * 用 Chat Completions 而非 Responses:前者是全供应商的公共底线。\n */\nexport async function probeModel(\n baseURL: string,\n apiKey: string,\n model: string,\n): Promise<ModelProbeResult> {\n const base = normalizeBaseURL(baseURL) ?? baseURL.replace(/\\/+$/, \"\");\n const key = cleanLlmValue(apiKey) ?? \"\";\n const started = Date.now();\n try {\n const resp = (await doFetch(`${base}/chat/completions`, {\n method: \"POST\",\n headers: {\n \"Content-Type\": \"application/json\",\n Authorization: `Bearer ${key}`,\n },\n body: JSON.stringify({\n model,\n messages: [{ role: \"user\", content: \"ping\" }],\n // 与 Provider 共用 isReasoningModel:o 系列 / gpt-5 只接受\n // max_completion_tokens,传 max_tokens 会 400,把可用模型误判成不可用\n ...(isReasoningModel(model)\n ? { max_completion_tokens: 8 }\n : { max_tokens: 8 }),\n stream: false,\n }),\n signal: AbortSignal.timeout(20_000),\n })) as { status: number; ok: boolean; json: () => Promise<unknown> };\n\n const latencyMs = Date.now() - started;\n if (resp.ok) return { ok: true, latencyMs };\n\n let body: unknown = null;\n try {\n body = await resp.json();\n } catch {\n // 非 JSON 错误体(网关 HTML 等)\n }\n const { message, code } = extractError(body);\n return {\n ok: false,\n error: `HTTP ${resp.status} ${message}`,\n ...(code ? { code } : {}),\n latencyMs,\n };\n } catch (err) {\n return {\n ok: false,\n error: err instanceof Error ? err.message : String(err),\n latencyMs: Date.now() - started,\n };\n }\n}\n","/**\n * 本机配置 / 状态目录的跨平台解析\n *\n * POSIX 沿用 XDG 约定;Windows 上 `HOME` 在 cmd / PowerShell 中通常不存在,\n * 若直接回退 `/tmp` 会把配置写到 `\\tmp\\...`(当前盘符根),且与\n * `%APPDATA%` 的常规位置不一致。统一顺序:显式 XDG 变量(用户显式指定,\n * 优先级最高)→ 平台原生目录 → XDG 默认位置(家目录)→ 临时目录。\n */\nimport * as os from \"os\";\nimport * as path from \"path\";\n\n/** 应用目录名(`~/.config/zqagent` 的最后一段) */\nconst APP_DIR_NAME = \"zqagent\";\n\n/**\n * 节点默认数据目录名(相对启动目录,即 `./.zqagent_data`)。\n * server / CLI 在既无 `--data-dir` 也无 `DATA_DIR` 时用它兜底,\n * 集中在此避免各处字面量走散。\n */\nexport const DEFAULT_DATA_DIR = \".zqagent_data\";\n\n/**\n * 官方 Hub 基址(不带结尾斜杠):CLI 在既无 `--hub` 也无 `HUB_URL` 时用它兜底\n * (首个交互式接入提示的默认值)。自建 Hub 请用 `--hub` / `HUB_URL` 覆盖。\n */\nexport const DEFAULT_HUB_URL = \"https://hub.cd-zq.com\";\n\n/** 是否为 Windows(仅此处需要区分平台;APPDATA 在别的系统上可能是无关变量) */\nconst IS_WINDOWS = process.platform === \"win32\";\n\n/** 用户主目录:`HOME` → Windows `USERPROFILE` → `os.homedir()` → 临时目录 */\nfunction homeDir(): string {\n return (\n process.env.HOME || process.env.USERPROFILE || os.homedir() || os.tmpdir()\n );\n}\n\n/** 配置目录:`$XDG_CONFIG_HOME/zqagent`、Windows `%APPDATA%\\zqagent`、其余 `~/.config/zqagent` */\nexport function configDir(): string {\n const base =\n process.env.XDG_CONFIG_HOME ||\n (IS_WINDOWS ? process.env.APPDATA : undefined) ||\n path.join(homeDir(), \".config\");\n return path.join(base, APP_DIR_NAME);\n}\n\n/** 状态目录:`$XDG_STATE_HOME/zqagent`、Windows `%LOCALAPPDATA%\\zqagent`、其余 `~/.local/state/zqagent` */\nexport function stateDir(): string {\n const base =\n process.env.XDG_STATE_HOME ||\n (IS_WINDOWS ? process.env.LOCALAPPDATA : undefined) ||\n path.join(homeDir(), \".local\", \"state\");\n return path.join(base, APP_DIR_NAME);\n}\n"],"mappings":"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","names":["path","Agent","net","lookup","Agent"]}
@@ -20,6 +20,7 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
20
20
  // src/providers/diagnostics.ts
21
21
  var diagnostics_exports = {};
22
22
  __export(diagnostics_exports, {
23
+ DEFAULT_LLM_TIMEOUT_MS: () => DEFAULT_LLM_TIMEOUT_MS,
23
24
  authErrorHints: () => authErrorHints,
24
25
  cleanLlmValue: () => cleanLlmValue,
25
26
  enrichLlmError: () => enrichLlmError,
@@ -31,6 +32,8 @@ __export(diagnostics_exports, {
31
32
  llmStreamIdleMs: () => llmStreamIdleMs,
32
33
  llmTimeoutMs: () => llmTimeoutMs,
33
34
  networkErrorHints: () => networkErrorHints,
35
+ normApiMode: () => normApiMode,
36
+ normLlmTimeoutMs: () => normLlmTimeoutMs,
34
37
  normalizeBaseURL: () => normalizeBaseURL,
35
38
  normalizeErrorFetch: () => normalizeErrorFetch,
36
39
  paramErrorHints: () => paramErrorHints,
@@ -224,9 +227,23 @@ function paramErrorHints(message, code = "") {
224
227
  }
225
228
  return hints;
226
229
  }
227
- function llmTimeoutMs() {
230
+ var DEFAULT_LLM_TIMEOUT_MS = 6e5;
231
+ function normLlmTimeoutMs(v) {
232
+ if (v === null || v === void 0 || v === "") return null;
233
+ const n = Number(v);
234
+ if (!Number.isFinite(n) || n <= 0) return null;
235
+ return Math.min(Math.round(n), 30 * 6e4);
236
+ }
237
+ function llmTimeoutMs(explicit) {
238
+ const fromArg = normLlmTimeoutMs(explicit);
239
+ if (fromArg) return fromArg;
228
240
  const n = Number(process.env.OPENAI_TIMEOUT_MS);
229
- return Number.isFinite(n) && n > 0 ? n : 6e5;
241
+ return Number.isFinite(n) && n > 0 ? n : DEFAULT_LLM_TIMEOUT_MS;
242
+ }
243
+ function normApiMode(v) {
244
+ const s = typeof v === "string" ? v.trim().toLowerCase() : "";
245
+ if (s === "auto" || s === "responses" || s === "chat") return s;
246
+ return null;
230
247
  }
231
248
  function llmStreamIdleMs() {
232
249
  const n = Number(process.env.OPENAI_STREAM_IDLE_MS);
@@ -283,6 +300,7 @@ function llmMaxRetries() {
283
300
  }
284
301
  // Annotate the CommonJS export names for ESM import in node:
285
302
  0 && (module.exports = {
303
+ DEFAULT_LLM_TIMEOUT_MS,
286
304
  authErrorHints,
287
305
  cleanLlmValue,
288
306
  enrichLlmError,
@@ -294,6 +312,8 @@ function llmMaxRetries() {
294
312
  llmStreamIdleMs,
295
313
  llmTimeoutMs,
296
314
  networkErrorHints,
315
+ normApiMode,
316
+ normLlmTimeoutMs,
297
317
  normalizeBaseURL,
298
318
  normalizeErrorFetch,
299
319
  paramErrorHints,
@@ -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 * 历史里若存在「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":[]}
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 请求超时(毫秒) */\nexport const DEFAULT_LLM_TIMEOUT_MS = 600_000;\n\n/**\n * 规范化单次请求超时(毫秒)。\n *\n * 允许**每个供应商**各自设置:慢模型(长思考/大上下文)需要更长的预算,\n * 而本地小模型希望快速失败。取值非法(≤0、非数字)时返回 null,由调用方\n * 回落到 `llmTimeoutMs()` 的全局默认。\n *\n * 上限 30 分钟:再长就属于「环境有问题」,让 SDK 一直挂着只会让人以为卡死。\n */\nexport function normLlmTimeoutMs(v: unknown): number | null {\n if (v === null || v === undefined || v === \"\") return null;\n const n = Number(v);\n if (!Number.isFinite(n) || n <= 0) return null;\n return Math.min(Math.round(n), 30 * 60_000);\n}\n\n/** LLM 请求超时(毫秒):显式值 > OPENAI_TIMEOUT_MS > 默认 10 分钟 */\nexport function llmTimeoutMs(explicit?: number | null): number {\n const fromArg = normLlmTimeoutMs(explicit);\n if (fromArg) return fromArg;\n const n = Number(process.env.OPENAI_TIMEOUT_MS);\n return Number.isFinite(n) && n > 0 ? n : DEFAULT_LLM_TIMEOUT_MS;\n}\n\n/** API 模式:auto = 先试 Responses,失败再降级 Chat */\nexport type ApiMode = \"auto\" | \"responses\" | \"chat\";\n\n/**\n * 规范化 API 模式。\n *\n * 历史上只能用全局环境变量 `OPENAI_FORCE_API_MODE` 强制,导致「一个节点上\n * 不同供应商需要不同模式」时无法表达(如供应商 A 只支持 Responses,供应商 B\n * 只支持 Chat)。现在允许按供应商配置,环境变量仍作为全局兜底。\n */\nexport function normApiMode(v: unknown): ApiMode | null {\n const s = typeof v === \"string\" ? v.trim().toLowerCase() : \"\";\n if (s === \"auto\" || s === \"responses\" || s === \"chat\") return s;\n return null;\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":[]}
@@ -84,8 +84,30 @@ declare function toolCallHistoryHints(message: string, code?: string): string[];
84
84
  * 这两句原文对排查毫无帮助,必须翻成可执行的下一步。
85
85
  */
86
86
  declare function paramErrorHints(message: string, code?: string): string[];
87
- /** LLM 请求超时(毫秒):OPENAI_TIMEOUT_MS,默认 10 分钟 */
88
- declare function llmTimeoutMs(): number;
87
+ /** 默认 LLM 请求超时(毫秒) */
88
+ declare const DEFAULT_LLM_TIMEOUT_MS = 600000;
89
+ /**
90
+ * 规范化单次请求超时(毫秒)。
91
+ *
92
+ * 允许**每个供应商**各自设置:慢模型(长思考/大上下文)需要更长的预算,
93
+ * 而本地小模型希望快速失败。取值非法(≤0、非数字)时返回 null,由调用方
94
+ * 回落到 `llmTimeoutMs()` 的全局默认。
95
+ *
96
+ * 上限 30 分钟:再长就属于「环境有问题」,让 SDK 一直挂着只会让人以为卡死。
97
+ */
98
+ declare function normLlmTimeoutMs(v: unknown): number | null;
99
+ /** LLM 请求超时(毫秒):显式值 > OPENAI_TIMEOUT_MS > 默认 10 分钟 */
100
+ declare function llmTimeoutMs(explicit?: number | null): number;
101
+ /** API 模式:auto = 先试 Responses,失败再降级 Chat */
102
+ type ApiMode = "auto" | "responses" | "chat";
103
+ /**
104
+ * 规范化 API 模式。
105
+ *
106
+ * 历史上只能用全局环境变量 `OPENAI_FORCE_API_MODE` 强制,导致「一个节点上
107
+ * 不同供应商需要不同模式」时无法表达(如供应商 A 只支持 Responses,供应商 B
108
+ * 只支持 Chat)。现在允许按供应商配置,环境变量仍作为全局兜底。
109
+ */
110
+ declare function normApiMode(v: unknown): ApiMode | null;
89
111
  /**
90
112
  * LLM 流空闲超时(毫秒):OPENAI_STREAM_IDLE_MS,默认 2 分钟。
91
113
  *
@@ -110,4 +132,4 @@ declare function normalizeErrorFetch(inner: FetchLike): FetchLike;
110
132
  /** LLM SDK 传输层重试次数:OPENAI_MAX_RETRIES,默认 3 */
111
133
  declare function llmMaxRetries(): number;
112
134
 
113
- export { authErrorHints, cleanLlmValue, enrichLlmError, errorCode, isNetworkErrorText, isReasoningModel, llmErrorContext, llmMaxRetries, llmStreamIdleMs, llmTimeoutMs, networkErrorHints, normalizeBaseURL, normalizeErrorFetch, paramErrorHints, toolCallHistoryHints };
135
+ export { type ApiMode, DEFAULT_LLM_TIMEOUT_MS, authErrorHints, cleanLlmValue, enrichLlmError, errorCode, isNetworkErrorText, isReasoningModel, llmErrorContext, llmMaxRetries, llmStreamIdleMs, llmTimeoutMs, networkErrorHints, normApiMode, normLlmTimeoutMs, normalizeBaseURL, normalizeErrorFetch, paramErrorHints, toolCallHistoryHints };
@@ -84,8 +84,30 @@ declare function toolCallHistoryHints(message: string, code?: string): string[];
84
84
  * 这两句原文对排查毫无帮助,必须翻成可执行的下一步。
85
85
  */
86
86
  declare function paramErrorHints(message: string, code?: string): string[];
87
- /** LLM 请求超时(毫秒):OPENAI_TIMEOUT_MS,默认 10 分钟 */
88
- declare function llmTimeoutMs(): number;
87
+ /** 默认 LLM 请求超时(毫秒) */
88
+ declare const DEFAULT_LLM_TIMEOUT_MS = 600000;
89
+ /**
90
+ * 规范化单次请求超时(毫秒)。
91
+ *
92
+ * 允许**每个供应商**各自设置:慢模型(长思考/大上下文)需要更长的预算,
93
+ * 而本地小模型希望快速失败。取值非法(≤0、非数字)时返回 null,由调用方
94
+ * 回落到 `llmTimeoutMs()` 的全局默认。
95
+ *
96
+ * 上限 30 分钟:再长就属于「环境有问题」,让 SDK 一直挂着只会让人以为卡死。
97
+ */
98
+ declare function normLlmTimeoutMs(v: unknown): number | null;
99
+ /** LLM 请求超时(毫秒):显式值 > OPENAI_TIMEOUT_MS > 默认 10 分钟 */
100
+ declare function llmTimeoutMs(explicit?: number | null): number;
101
+ /** API 模式:auto = 先试 Responses,失败再降级 Chat */
102
+ type ApiMode = "auto" | "responses" | "chat";
103
+ /**
104
+ * 规范化 API 模式。
105
+ *
106
+ * 历史上只能用全局环境变量 `OPENAI_FORCE_API_MODE` 强制,导致「一个节点上
107
+ * 不同供应商需要不同模式」时无法表达(如供应商 A 只支持 Responses,供应商 B
108
+ * 只支持 Chat)。现在允许按供应商配置,环境变量仍作为全局兜底。
109
+ */
110
+ declare function normApiMode(v: unknown): ApiMode | null;
89
111
  /**
90
112
  * LLM 流空闲超时(毫秒):OPENAI_STREAM_IDLE_MS,默认 2 分钟。
91
113
  *
@@ -110,4 +132,4 @@ declare function normalizeErrorFetch(inner: FetchLike): FetchLike;
110
132
  /** LLM SDK 传输层重试次数:OPENAI_MAX_RETRIES,默认 3 */
111
133
  declare function llmMaxRetries(): number;
112
134
 
113
- export { authErrorHints, cleanLlmValue, enrichLlmError, errorCode, isNetworkErrorText, isReasoningModel, llmErrorContext, llmMaxRetries, llmStreamIdleMs, llmTimeoutMs, networkErrorHints, normalizeBaseURL, normalizeErrorFetch, paramErrorHints, toolCallHistoryHints };
135
+ export { type ApiMode, DEFAULT_LLM_TIMEOUT_MS, authErrorHints, cleanLlmValue, enrichLlmError, errorCode, isNetworkErrorText, isReasoningModel, llmErrorContext, llmMaxRetries, llmStreamIdleMs, llmTimeoutMs, networkErrorHints, normApiMode, normLlmTimeoutMs, normalizeBaseURL, normalizeErrorFetch, paramErrorHints, toolCallHistoryHints };