| 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<PolishResult> { |
| return polishBrowser(task, settings); |
| } |
|
|
| export async function polishBrowser(task: PolishTask, settings: AppSettings): Promise<PolishResult> { |
| 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<string, string> = { "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<string, unknown> = { |
| 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<PolishResult> & { 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)); |
| } |
|
|