Rl-Auto / apps /web-legacy /src /lib /polishClient.ts
Lazywords's picture
Deploy RL Auto Docker Space
c4ae742
Raw
History Blame Contribute Delete
6.1 kB
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));
}