PushGo Auto Commit 2026-03-29T12:17:45.673Z

This commit is contained in:
PushGo User
2026-03-29 20:17:45 +08:00
commit 8398984cc5
12 changed files with 1276 additions and 0 deletions

230
background.js Normal file
View File

@@ -0,0 +1,230 @@
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);
}
});