/* global chrome */ // ---------- 默认配置 ---------- const DEFAULTS = { enabled: true, preset: 'deepseek', apiType: 'openai', // openai | anthropic | gemini baseURL: 'https://api.deepseek.com/v1', apiKey: '', model: 'deepseek-chat', targetLang: '简体中文', sourceLang: '自动检测', triggerMode: 'icon', // icon | auto | shortcut systemPrompt: '', temperature: 0.3, fontSize: 16, maxLength: 5000 }; const PRESETS = { deepseek: { name: 'DeepSeek', apiType: 'openai', baseURL: 'https://api.deepseek.com/v1', model: 'deepseek-chat' }, openai: { name: 'OpenAI', apiType: 'openai', baseURL: 'https://api.openai.com/v1', model: 'gpt-4o-mini' }, kimi: { name: '月之暗面 Kimi', apiType: 'openai', baseURL: 'https://api.moonshot.cn/v1', model: 'moonshot-v1-8k' }, zhipu: { name: '智谱 GLM', apiType: 'openai', baseURL: 'https://open.bigmodel.cn/api/paas/v4', model: 'glm-4-flash' }, qwen: { name: '通义千问', apiType: 'openai', baseURL: 'https://dashscope.aliyuncs.com/compatible-mode/v1', model: 'qwen-plus' }, siliconflow: { name: '硅基流动', apiType: 'openai', baseURL: 'https://api.siliconflow.cn/v1', model: 'Qwen/Qwen2.5-7B-Instruct' }, gemini: { name: 'Google Gemini', apiType: 'gemini', baseURL: 'https://generativelanguage.googleapis.com', model: 'gemini-2.0-flash' }, anthropic: { name: 'Anthropic Claude', apiType: 'anthropic', baseURL: 'https://api.anthropic.com', model: 'claude-3-5-haiku-latest' }, ollama: { name: 'Ollama(本地)', apiType: 'openai', baseURL: 'http://localhost:11434/v1', model: 'qwen2.5:7b' }, custom: { name: '自定义(OpenAI 兼容)', apiType: 'openai', baseURL: '', model: '' } }; async function getSettings() { const stored = await chrome.storage.sync.get(DEFAULTS); return { ...DEFAULTS, ...stored }; } // ---------- Prompt ---------- function buildSystemPrompt(cfg) { if (cfg.systemPrompt && cfg.systemPrompt.trim()) { return cfg.systemPrompt .replaceAll('{text}', '%TEXT%') // 防止用户模板注入原文 .replaceAll('{targetLang}', cfg.targetLang) .replaceAll('{sourceLang}', cfg.sourceLang); } const src = cfg.sourceLang === '自动检测' ? '自动识别源语言' : `源语言为 ${cfg.sourceLang}`; return ( `你是一个专业、精准的翻译引擎。${src},将用户提供的文本翻译成${cfg.targetLang}。\n` + `要求:\n` + `1. 只输出译文本身,不要任何解释、前缀、引号或注释。\n` + `2. 如果文本已经是${cfg.targetLang},原样返回文本。\n` + `3. 保留原文的换行、段落结构与 Markdown 格式。\n` + `4. 专业术语、代码、URL、邮箱保持原样。\n` + `5. 在准确的前提下尽量符合${cfg.targetLang}的表达习惯。` ); } // ---------- 各服务商请求构造 ---------- function buildRequest(cfg, userText) { const sys = buildSystemPrompt(cfg); const type = cfg.apiType; const base = (cfg.baseURL || '').replace(/\/+$/, ''); if (type === 'anthropic') { return { url: `${base}/v1/messages`, headers: { 'content-type': 'application/json', 'x-api-key': cfg.apiKey, 'anthropic-version': '2023-06-01', 'anthropic-dangerous-direct-browser-access': 'true' }, body: JSON.stringify({ model: cfg.model, max_tokens: 4096, temperature: cfg.temperature, system: sys, stream: true, messages: [{ role: 'user', content: userText }] }) }; } if (type === 'gemini') { return { url: `${base}/v1beta/models/${encodeURIComponent(cfg.model)}:streamGenerateContent?alt=sse`, headers: { 'content-type': 'application/json', 'x-goog-api-key': cfg.apiKey }, body: JSON.stringify({ systemInstruction: { parts: [{ text: sys }] }, contents: [{ role: 'user', parts: [{ text: userText }] }], generationConfig: { temperature: cfg.temperature, maxOutputTokens: 4096 } }) }; } // OpenAI 兼容 return { url: `${base}/chat/completions`, headers: { 'content-type': 'application/json', authorization: `Bearer ${cfg.apiKey}` }, body: JSON.stringify({ model: cfg.model, temperature: cfg.temperature, stream: true, messages: [ { role: 'system', content: sys }, { role: 'user', content: userText } ] }) }; } function extractDelta(apiType, json) { try { if (apiType === 'anthropic') { return json.type === 'content_block_delta' ? (json.delta?.text ?? '') : ''; } if (apiType === 'gemini') { return json.candidates?.[0]?.content?.parts?.map(p => p.text || '').join('') || ''; } return json.choices?.[0]?.delta?.content ?? ''; } catch { return ''; } } // ---------- 流式翻译 ---------- async function streamTranslate(port, msg) { const cfg = await getSettings(); let aborted = false; port.onDisconnect.addListener(() => { aborted = true; }); try { const text = String(msg.text || '').slice(0, cfg.maxLength); if (!text.trim()) throw new Error('没有可翻译的文本'); const req = buildRequest(cfg, text); const controller = new AbortController(); port.onDisconnect.addListener(() => controller.abort()); const resp = await fetch(req.url, { method: 'POST', headers: req.headers, body: req.body, signal: controller.signal }); if (!resp.ok) { let detail = ''; try { const j = await resp.json(); detail = j.error?.message || j.message || JSON.stringify(j).slice(0, 300); } catch { /* ignore */ } const hints = { 401: '(API Key 无效或未填写)', 403: '(无权限,请检查 Key 与模型授权)', 404: '(接口地址或模型名可能不正确)', 429: '(请求过于频繁或额度不足)' }; throw new Error(`请求失败 HTTP ${resp.status} ${hints[resp.status] || ''}\n${detail}`); } if (!resp.body) throw new Error('响应不包含数据流'); const reader = resp.body.getReader(); const decoder = new TextDecoder(); let buffer = ''; let full = ''; port.postMessage({ type: 'start' }); while (true) { if (aborted) break; const { done, value } = await reader.read(); if (done) break; buffer += decoder.decode(value, { stream: true }); let nl; while ((nl = buffer.indexOf('\n')) >= 0) { const line = buffer.slice(0, nl).replace(/\r$/, ''); buffer = buffer.slice(nl + 1); if (!line.startsWith('data:')) continue; const data = line.slice(5).trim(); if (!data || data === '[DONE]') continue; try { const json = JSON.parse(data); const delta = extractDelta(cfg.apiType, json); if (delta) { full += delta; port.postMessage({ type: 'delta', text: delta }); } } catch { /* 忽略不完整/非 JSON 行 */ } } } if (!full.trim()) throw new Error('模型未返回内容,请检查模型名称或接口配置'); port.postMessage({ type: 'done', full }); } catch (err) { if (aborted) return; const msgText = err.name === 'AbortError' ? '请求已取消' : (err.message || String(err)); try { port.postMessage({ type: 'error', error: msgText }); } catch { /* port closed */ } } } // ---------- 消息通道 ---------- chrome.runtime.onConnect.addListener(port => { if (port.name === 'translate') { port.onMessage.addListener(msg => { if (msg.type === 'translate') streamTranslate(port, msg); }); } }); // ---------- 快捷键 ---------- chrome.commands.onCommand.addListener(async command => { if (command !== 'translate-selection') return; const [tab] = await chrome.tabs.query({ active: true, currentWindow: true }); if (tab?.id != null) { chrome.tabs.sendMessage(tab.id, { type: 'translate-selection' }).catch(() => { /* 无内容脚本 */ }); } }); // ---------- 右键菜单 ---------- chrome.runtime.onInstalled.addListener(() => { chrome.contextMenus.removeAll(() => { chrome.contextMenus.create({ id: 'ai-translate-selection', title: '用译泡翻译“%s”', contexts: ['selection'] }); }); }); chrome.contextMenus.onClicked.addListener(async (info, tab) => { if (info.menuItemId !== 'ai-translate-selection' || !tab?.id) return; chrome.tabs.sendMessage(tab.id, { type: 'translate-text', text: info.selectionText || '' }).catch(() => { /* 无内容脚本 */ }); }); // 供 popup / options 读取预设 chrome.runtime.onMessage.addListener((msg, _sender, sendResponse) => { if (msg.type === 'get-presets') { sendResponse({ presets: PRESETS, defaults: DEFAULTS }); } return false; });