const SYSTEM_PROMPT = "你是翻译引擎,只输出译文,不解释不分析不扩展"; function setupContextMenu() { chrome.contextMenus.removeAll(() => { chrome.contextMenus.create({ id: "transly-translate-selection", title: "Transly: 翻译选中文本", contexts: ["selection"] }); }); } chrome.runtime.onInstalled.addListener((details) => { setupContextMenu(); if (details.reason === "install") { chrome.runtime.openOptionsPage(); } }); chrome.runtime.onStartup.addListener(setupContextMenu); async function getConfig() { const syncData = await chrome.storage.sync.get({ baseUrl: "https://api.openai.com", model: "gpt-4o-mini", targetLang: "zh-CN" }); const localData = await chrome.storage.local.get({ apiKey: "" }); return normalizeConfig({ ...syncData, ...localData }); } function normalizeConfig(config) { return { baseUrl: String(config?.baseUrl || "").trim().replace(/\/+$/, ""), apiKey: String(config?.apiKey || "").trim(), model: String(config?.model || "").trim(), targetLang: String(config?.targetLang || "zh-CN").trim() || "zh-CN" }; } function parseContent(content) { if (Array.isArray(content)) { return content.map((item) => item?.text || "").join("").trim(); } return String(content || "").trim(); } async function requestTranslate(text, useReasoningEffort, configOverride) { const cfg = configOverride ? normalizeConfig(configOverride) : await getConfig(); const { baseUrl, apiKey, model, targetLang } = cfg; if (!baseUrl || !apiKey || !model) { throw new Error("请先在 options 填写 Base URL / API Key / Model"); } const body = { model, temperature: 0, messages: [ { role: "system", content: SYSTEM_PROMPT }, { role: "user", content: `请翻译为 ${targetLang},只输出译文:\n${text}` } ] }; if (useReasoningEffort) { body.reasoning_effort = "low"; } const res = await fetch(`${baseUrl}/v1/chat/completions`, { method: "POST", headers: { "Content-Type": "application/json", Authorization: `Bearer ${apiKey}` }, body: JSON.stringify(body) }); if (!res.ok) { const errText = (await res.text()).slice(0, 200); throw new Error(`请求失败(${res.status}) ${errText}`); } const data = await res.json(); const translated = parseContent(data?.choices?.[0]?.message?.content); if (!translated) { throw new Error("未收到有效译文"); } return translated; } async function translate(text, configOverride) { if (!text || !text.trim()) { throw new Error("没有可翻译文本"); } try { return await requestTranslate(text.trim(), true, configOverride); } catch (err) { const message = String(err?.message || ""); if (message.includes("400") || message.includes("reasoning")) { return requestTranslate(text.trim(), false, configOverride); } throw err; } } async function testConfig(configInput) { const cfg = normalizeConfig(configInput); if (!cfg.baseUrl || !cfg.apiKey || !cfg.model) { throw new Error("请先填写 Base URL / API Key / Model"); } await translate("hello world", cfg); return "连接成功,API Key 与模型可用"; } async function getSelectedTextFromTab(tabId) { const results = await chrome.scripting.executeScript({ target: { tabId }, func: () => { const selected = String(window.getSelection() || "").trim(); if (selected) { return selected; } const el = document.activeElement; if (!el) { return ""; } const isInput = el.tagName === "INPUT" || el.tagName === "TEXTAREA"; if (!isInput || typeof el.value !== "string") { return ""; } const start = el.selectionStart ?? 0; const end = el.selectionEnd ?? 0; return el.value.slice(Math.min(start, end), Math.max(start, end)).trim(); } }); return String(results?.[0]?.result || "").trim(); } async function showTextInTab(tabId, text, isError) { const message = String(text || "").trim(); if (!message) { return false; } try { await chrome.scripting.executeScript({ target: { tabId }, func: (msg, error) => { alert(error ? `Transly: ${msg}` : msg); }, args: [message, !!isError] }); return true; } catch (_err) { // 某些页面禁止注入,降级尝试 content script 消息。 } return new Promise((resolve) => { chrome.tabs.sendMessage( tabId, isError ? { action: "showError", error: message } : { action: "showTranslation", text: message }, (response) => { if (chrome.runtime.lastError || !response?.ok) { resolve(false); return; } resolve(true); } ); }); } async function translateSelectionInTab(tabId) { const selectedText = await getSelectedTextFromTab(tabId); if (!selectedText) { throw new Error("请先选中文本"); } const text = await translate(selectedText); const shown = await showTextInTab(tabId, text, false); return { text, shown }; } chrome.runtime.onMessage.addListener((message, _sender, sendResponse) => { if (message?.action === "translate") { translate(message.text) .then((text) => sendResponse({ ok: true, text })) .catch((err) => sendResponse({ ok: false, error: err.message || "翻译失败" })); return true; } if (message?.action === "translateSelectionInTab") { translateSelectionInTab(message.tabId) .then((result) => sendResponse({ ok: true, text: result.text, shown: result.shown })) .catch((err) => sendResponse({ ok: false, error: err.message || "翻译失败" })); return true; } if (message?.action === "testConfig") { testConfig(message.config) .then((text) => sendResponse({ ok: true, text })) .catch((err) => sendResponse({ ok: false, error: err.message || "测试失败" })); return true; } }); chrome.contextMenus.onClicked.addListener(async (info, tab) => { if (info.menuItemId !== "transly-translate-selection" || !info.selectionText || !tab?.id) { return; } try { const text = await translate(info.selectionText); await showTextInTab(tab.id, text, false); } catch (err) { await showTextInTab(tab.id, err.message || "翻译失败", true); } });