152 lines
7.0 KiB
JavaScript
152 lines
7.0 KiB
JavaScript
/* global chrome */
|
||
|
||
const LANGS = ['简体中文', '繁體中文', 'English', '日本語', '한국어', 'Français', 'Deutsch', 'Español', 'Português', 'Italiano', 'Русский', 'العربية', 'हिन्दी', 'ไทย', 'Việt Nam', 'Indonesia'].filter((v, i, a) => a.indexOf(v) === i);
|
||
|
||
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(普通 API·按量计费)', apiType: 'openai', baseURL: 'https://open.bigmodel.cn/api/paas/v4', model: 'glm-4-flash' },
|
||
'zhipu-coding': { name: '智谱 GLM Coding Plan(订阅套餐专用接入点)', apiType: 'openai', baseURL: 'https://open.bigmodel.cn/api/coding/paas/v4', model: 'glm-5.3' },
|
||
qwen: { name: '通义千问(阿里云百炼)', apiType: 'openai', baseURL: 'https://dashscope.aliyuncs.com/compatible-mode/v1', model: 'qwen-plus' },
|
||
siliconflow: { name: '硅基流动 SiliconFlow', 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(本地,无需 Key)', apiType: 'openai', baseURL: 'http://localhost:11434/v1', model: 'qwen2.5:7b' },
|
||
custom: { name: '自定义(OpenAI 兼容)', apiType: 'openai', baseURL: '', model: '' }
|
||
};
|
||
|
||
const FIELDS = ['preset', 'apiType', 'baseURL', 'apiKey', 'model', 'targetLang', 'sourceLang', 'triggerMode', 'systemPrompt', 'temperature', 'fontSize'];
|
||
const $ = id => document.getElementById(id);
|
||
let dirty = false;
|
||
|
||
async function init() {
|
||
const cfg = await chrome.storage.sync.get({
|
||
preset: 'deepseek', apiType: 'openai', baseURL: '', apiKey: '', model: '',
|
||
targetLang: '简体中文', sourceLang: '自动检测', triggerMode: 'icon',
|
||
systemPrompt: '', temperature: 0.3, fontSize: 16
|
||
});
|
||
|
||
// 服务商下拉
|
||
const presetSel = $('preset');
|
||
Object.entries(PRESETS).forEach(([k, v]) => {
|
||
const o = document.createElement('option');
|
||
o.value = k; o.textContent = v.name;
|
||
presetSel.appendChild(o);
|
||
});
|
||
presetSel.value = cfg.preset in PRESETS ? cfg.preset : 'custom';
|
||
|
||
// 语言下拉
|
||
['targetLang', 'sourceLang'].forEach(id => {
|
||
const sel = $(id);
|
||
(id === 'sourceLang' ? ['自动检测', ...LANGS] : LANGS).forEach(l => {
|
||
const o = document.createElement('option');
|
||
o.value = l; o.textContent = l;
|
||
sel.appendChild(o);
|
||
});
|
||
});
|
||
|
||
FIELDS.forEach(id => { $(id).value = cfg[id]; });
|
||
|
||
presetSel.addEventListener('change', () => {
|
||
const p = PRESETS[presetSel.value];
|
||
if (!p) return;
|
||
$('apiType').value = p.apiType;
|
||
$('baseURL').value = p.baseURL;
|
||
$('model').value = p.model;
|
||
markDirty();
|
||
});
|
||
|
||
$('eyeBtn').addEventListener('click', () => {
|
||
const inp = $('apiKey');
|
||
inp.type = inp.type === 'password' ? 'text' : 'password';
|
||
});
|
||
|
||
FIELDS.forEach(id => $(id).addEventListener('input', markDirty));
|
||
|
||
$('saveBtn').addEventListener('click', save);
|
||
$('testBtn').addEventListener('click', testConnection);
|
||
|
||
// 侧边栏高亮
|
||
const links = document.querySelectorAll('.side nav a');
|
||
const observer = new IntersectionObserver(entries => {
|
||
entries.forEach(e => {
|
||
if (e.isIntersecting) {
|
||
links.forEach(l => l.classList.toggle('active', l.hash === '#' + e.target.id));
|
||
}
|
||
});
|
||
}, { rootMargin: '-30% 0px -60% 0px' });
|
||
document.querySelectorAll('main section').forEach(s => observer.observe(s));
|
||
}
|
||
|
||
function markDirty() {
|
||
dirty = true;
|
||
$('saveHint').textContent = '有未保存的修改';
|
||
$('savebar').classList.add('show');
|
||
}
|
||
|
||
async function save() {
|
||
const data = {};
|
||
FIELDS.forEach(id => {
|
||
data[id] = $(id).type === 'number' ? parseFloat($(id).value) || 0 : $(id).value.trim ? $(id).value.trim() : $(id).value;
|
||
});
|
||
data.temperature = Math.min(1, Math.max(0, data.temperature));
|
||
data.fontSize = Math.min(22, Math.max(12, Math.round(data.fontSize)));
|
||
await chrome.storage.sync.set(data);
|
||
dirty = false;
|
||
$('saveHint').textContent = '✓ 已保存';
|
||
$('savebar').classList.add('show');
|
||
setTimeout(() => { if (!dirty) $('savebar').classList.remove('show'); }, 2000);
|
||
}
|
||
|
||
async function testConnection() {
|
||
const btn = $('testBtn'), out = $('testResult');
|
||
btn.disabled = true;
|
||
out.className = 'test-result';
|
||
out.textContent = '连接中…';
|
||
|
||
const cfg = {};
|
||
FIELDS.forEach(id => cfg[id] = $(id).value.trim ? $(id).value.trim() : $(id).value);
|
||
const base = cfg.baseURL.replace(/\/+$/, '');
|
||
const prompt = `请把这句话翻译成${cfg.targetLang}:Hello world`;
|
||
|
||
try {
|
||
let url, headers, body;
|
||
if (cfg.apiType === 'anthropic') {
|
||
url = `${base}/v1/messages`;
|
||
headers = { 'content-type': 'application/json', 'x-api-key': cfg.apiKey, 'anthropic-version': '2023-06-01' };
|
||
body = JSON.stringify({ model: cfg.model, max_tokens: 100, messages: [{ role: 'user', content: prompt }] });
|
||
} else if (cfg.apiType === 'gemini') {
|
||
url = `${base}/v1beta/models/${encodeURIComponent(cfg.model)}:generateContent`;
|
||
headers = { 'content-type': 'application/json', 'x-goog-api-key': cfg.apiKey };
|
||
body = JSON.stringify({ contents: [{ role: 'user', parts: [{ text: prompt }] }] });
|
||
} else {
|
||
url = `${base}/chat/completions`;
|
||
headers = { 'content-type': 'application/json', authorization: `Bearer ${cfg.apiKey}` };
|
||
body = JSON.stringify({ model: cfg.model, max_tokens: 100, messages: [{ role: 'user', content: prompt }] });
|
||
}
|
||
|
||
const resp = await fetch(url, { method: 'POST', headers, body });
|
||
const json = await resp.json().catch(() => ({}));
|
||
|
||
if (!resp.ok) {
|
||
throw new Error(json.error?.message || json.message || `HTTP ${resp.status}`);
|
||
}
|
||
const reply = cfg.apiType === 'anthropic' ? json.content?.[0]?.text
|
||
: cfg.apiType === 'gemini' ? json.candidates?.[0]?.content?.parts?.[0]?.text
|
||
: json.choices?.[0]?.message?.content;
|
||
|
||
if (!reply) throw new Error('未解析到回复,请检查模型名');
|
||
if (dirty) await save();
|
||
out.className = 'test-result ok';
|
||
out.textContent = `✓ 连接成功:${String(reply).trim().slice(0, 50)}`;
|
||
} catch (err) {
|
||
out.className = 'test-result err';
|
||
out.textContent = '✗ ' + (err.message || err);
|
||
} finally {
|
||
btn.disabled = false;
|
||
}
|
||
}
|
||
|
||
init();
|