Files
chrome_plus_Transly/background.js
2026-03-29 20:17:45 +08:00

231 lines
6.2 KiB
JavaScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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);
}
});