Files
PopLingo/options/options.js
T
zzzjy d660c204c2
CI / check (push) Successful in 10s
Release / release (push) Successful in 7s
feat: 区分智谱 GLM 两个接入点(普通 API / GLM Coding Plan 订阅专属接入点)
2026-09-17 23:34:28 +08:00

152 lines
7.0 KiB
JavaScript
Raw 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.
/* 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();