import { resolveModel } from "../settings"; import type { AppSettings, PolishResult, PolishTask } from "../types"; const RESULT_SCHEMA = { type: "object", additionalProperties: false, required: ["zh", "en", "notes"], properties: { zh: { type: "string" }, en: { type: "string" }, notes: { type: "string" }, }, }; export async function polishTask(task: PolishTask, settings: AppSettings): Promise { return polishBrowser(task, settings); } export async function polishBrowser(task: PolishTask, settings: AppSettings): Promise { if (!settings.apiKey.trim()) { throw new Error("请填写浏览器 API Key,或在个人设置里选择已保存的连接。"); } const apiMode = resolveApiMode(settings.baseURL, settings.apiMode); const endpoint = resolveEndpoint(settings.baseURL, apiMode); const response = await fetch(endpoint, { method: "POST", headers: buildHeaders(settings, endpoint), body: JSON.stringify(buildModelBody(task.raw, settings, apiMode, endpoint)), }); const payload = await response.json().catch(() => null); if (!response.ok) { throw new Error(extractError(payload) || `HTTP ${response.status}`); } const text = apiMode === "chat" ? String(payload?.choices?.[0]?.message?.content || "") : extractResponsesText(payload); return normalizeResult(parseJsonText(text)); } export function createSystemPrompt(): string { return [ "你是一位专业的任务描述润色专家。", "将原始任务改写成真实、自然、目标导向的用户请求。", "不要泄露具体操作路径、API 名称、按钮位置或实现策略。", "保留用户自然需要的业务上下文、人名、文件名、对象名和期望结果。", "输出 JSON:{\"zh\":\"简体中文润色结果\",\"en\":\"自然英文请求\",\"notes\":\"中文修改说明\"}。", ].join("\n"); } function buildHeaders(settings: AppSettings, endpoint: string): HeadersInit { const headers: Record = { "Content-Type": "application/json" }; if (/azure\.com/i.test(endpoint)) headers["api-key"] = settings.apiKey; else headers.Authorization = `Bearer ${settings.apiKey}`; return headers; } function buildModelBody(raw: string, settings: AppSettings, apiMode: "chat" | "responses", endpoint: string) { const model = resolveModel(settings); if (!model) throw new Error("请在设置里选择模型。"); if (apiMode === "chat") { const body: Record = { model, messages: [ { role: "system", content: createSystemPrompt() }, { role: "user", content: raw }, ], temperature: clamp(settings.temperature, 0, 2), top_p: clamp(settings.topP, 0, 1), max_tokens: clamp(settings.maxTokens, 1, 131072), response_format: { type: "json_object" }, stream: false, }; if (/bigmodel\.cn|zhipuai/i.test(endpoint)) { body.thinking = { type: settings.enableThinking ? "enabled" : "disabled" }; } else { body.chat_template_kwargs = { enable_thinking: settings.enableThinking, clear_thinking: settings.clearThinking, }; } return body; } return { model, store: false, temperature: clamp(settings.temperature, 0, 2), input: [ { role: "system", content: [{ type: "input_text", text: createSystemPrompt() }] }, { role: "user", content: [{ type: "input_text", text: raw }] }, ], text: { format: { type: "json_schema", name: "task_polisher_result", strict: true, schema: RESULT_SCHEMA, }, }, }; } function resolveApiMode(baseURL: string, fallback: "chat" | "responses"): "chat" | "responses" { if (/\/chat\/completions(\/?|$)/i.test(baseURL)) return "chat"; if (/\/responses(\/?|$)/i.test(baseURL)) return "responses"; return fallback === "responses" ? "responses" : "chat"; } function resolveEndpoint(baseURL: string, apiMode: "chat" | "responses"): string { const suffix = apiMode === "chat" ? "/chat/completions" : "/responses"; const value = String(baseURL || "").trim(); if (!value) return "https://api.openai.com/v1" + suffix; let trimmed = value.replace(/\/+$/g, ""); trimmed = trimmed.replace(/\/(responses|chat\/completions)$/i, ""); if (/\/v\d+$/i.test(trimmed) || /\/openai\/v\d+$/i.test(trimmed)) { return trimmed + suffix; } return trimmed + "/v1" + suffix; } function extractResponsesText(payload: unknown): string { const value = payload as { output_text?: string; output?: Array<{ content?: Array<{ text?: string; type?: string }> }>; }; if (value?.output_text) return value.output_text; return (value?.output || []) .flatMap((item) => item.content || []) .map((content) => content.text || "") .join(""); } function normalizeResult(value: unknown): PolishResult { const raw = value as Partial & { notes?: string | string[] }; return { zh: String(raw?.zh || "").trim(), en: String(raw?.en || "").trim(), notes: Array.isArray(raw?.notes) ? raw.notes.join("\n") : String(raw?.notes || "").trim(), }; } function parseJsonText(text: string): unknown { const cleaned = String(text || "") .trim() .replace(/^```json\s*/i, "") .replace(/^```\s*/i, "") .replace(/```\s*$/i, "") .trim(); try { return JSON.parse(cleaned); } catch { const start = cleaned.indexOf("{"); const end = cleaned.lastIndexOf("}"); if (start >= 0 && end > start) return JSON.parse(cleaned.slice(start, end + 1)); throw new Error("模型返回非 JSON,无法解析润色结果。"); } } function extractError(payload: unknown): string { const value = payload as { error?: { message?: string } | string; message?: string }; if (typeof value?.error === "string") return value.error; return value?.error?.message || value?.message || ""; } function clamp(value: number, min: number, max: number): number { const parsed = Number(value); if (!Number.isFinite(parsed)) return min; return Math.min(max, Math.max(min, parsed)); }