export type OpenAiCompatibleChatMessage = { role: "system" | "user" | "assistant"; content: string; }; export type OpenAiCompatibleChatOptions = { baseUrl: string; // 形如 https://host/v1 apiKey: string; model: string; timeoutMs?: number; maxTokens?: number; // 某些 OpenAI 兼容网关使用 max_completion_tokens 字段;如需可传入该值 maxCompletionTokens?: number; responseFormat?: "json_object"; }; export const tryExtractJsonObject = (value: string): Record | null => { const s = value ?? ""; const start = s.indexOf("{"); const end = s.lastIndexOf("}"); if (start === -1 || end === -1 || end <= start) return null; try { const parsed = JSON.parse(s.slice(start, end + 1)); if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) { return parsed as Record; } return null; } catch { return null; } }; export const openAiCompatibleChat = async ( messages: OpenAiCompatibleChatMessage[], opts: OpenAiCompatibleChatOptions, ): Promise<{ text: string; raw: unknown }> => { const timeoutMs = Math.max(500, Math.min(120_000, opts.timeoutMs ?? 20_000)); const controller = new AbortController(); const t = setTimeout(() => controller.abort(), timeoutMs); try { const url = `${opts.baseUrl.replace(/\/+$/, "")}/chat/completions`; const maxCompletionTokens = typeof opts.maxCompletionTokens === "number" && Number.isFinite(opts.maxCompletionTokens) ? Math.max(64, Math.min(16_000, Math.floor(opts.maxCompletionTokens))) : undefined; const maxTokens = typeof opts.maxTokens === "number" && Number.isFinite(opts.maxTokens) ? Math.max(64, Math.min(16_000, Math.floor(opts.maxTokens))) : undefined; const res = await fetch(url, { method: "POST", signal: controller.signal, headers: { "Content-Type": "application/json", Authorization: `Bearer ${opts.apiKey}`, }, body: JSON.stringify({ model: opts.model, stream: false, temperature: 0.2, ...(maxTokens ? { max_tokens: maxTokens } : {}), ...(maxCompletionTokens ? { max_completion_tokens: maxCompletionTokens } : {}), ...(opts.responseFormat === "json_object" ? { response_format: { type: "json_object" } } : {}), messages, }), }); const raw = (await res.json().catch(() => null)) as unknown; if (!res.ok) { const errText = typeof raw === "object" && raw && "error" in (raw as any) ? String((raw as any).error?.message ?? (raw as any).error) : `HTTP ${res.status}`; throw new Error(`在线 AI 调用失败:${errText}`); } const choiceText = (raw as any)?.choices?.[0]?.message?.content ?? (raw as any)?.choices?.[0]?.text ?? ""; const text = String(choiceText ?? ""); return { text, raw }; } finally { clearTimeout(t); } }; export const openAiCompatibleChatJson = async ( messages: OpenAiCompatibleChatMessage[], opts: OpenAiCompatibleChatOptions, ): Promise<{ json: Record; text: string; raw: unknown }> => { const { text, raw } = await openAiCompatibleChat(messages, opts); const json = tryExtractJsonObject(text); if (!json) { const preview = String(text || "").slice(0, 220).replace(/\s+/g, " ").trim(); throw new Error(`在线 AI 未返回可解析的 JSON 对象:${preview || "(empty)"}`); } return { json, text, raw }; };