-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathoptions.js
More file actions
73 lines (73 loc) · 10.6 KB
/
Copy pathoptions.js
File metadata and controls
73 lines (73 loc) · 10.6 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
const fields = ['language', 'provider', 'endpoint', 'model', 'apiKey', 'font'];
const providers = {
openai: { endpoint: 'https://api.openai.com/v1/chat/completions', model: 'gpt-4.1-mini', help: 'OpenAI Chat Completions API。', models: ['gpt-4.1-mini', 'gpt-4.1', 'gpt-4o-mini', 'gpt-4o'] },
deepseek: { endpoint: 'https://api.deepseek.com/chat/completions', model: 'deepseek-v4-flash', help: 'DeepSeek API;提供 Flash(更快)和 Pro(更强)模型。', models: ['deepseek-v4-flash', 'deepseek-v4-pro'] },
openrouter: { endpoint: 'https://openrouter.ai/api/v1/chat/completions', model: 'openai/gpt-4.1-mini', help: 'OpenRouter OpenAI-compatible API;模型使用 provider/model 格式。', models: ['openai/gpt-4.1-mini', 'anthropic/claude-3.5-sonnet', 'google/gemini-2.5-flash'] },
qwen: { endpoint: 'https://dashscope.aliyuncs.com/compatible-mode/v1/chat/completions', model: 'qwen-plus', help: '阿里云百炼的 OpenAI 兼容模式。', models: ['qwen-plus', 'qwen-turbo', 'qwen-max'] },
groq: { endpoint: 'https://api.groq.com/openai/v1/chat/completions', model: 'llama-3.3-70b-versatile', help: 'Groq OpenAI-compatible API。', models: ['llama-3.3-70b-versatile', 'llama-3.1-8b-instant'] },
siliconflow: { endpoint: 'https://api.siliconflow.cn/v1/chat/completions', model: 'Qwen/Qwen2.5-7B-Instruct', help: 'SiliconFlow OpenAI-compatible API。', models: ['Qwen/Qwen2.5-7B-Instruct', 'deepseek-ai/DeepSeek-V3'] },
anthropic: { endpoint: 'https://api.anthropic.com/v1/messages', model: 'claude-3-5-haiku-latest', help: 'Anthropic Messages API。', models: ['claude-3-5-haiku-latest', 'claude-sonnet-4-20250514'] },
gemini: { endpoint: 'https://generativelanguage.googleapis.com/v1beta', model: 'gemini-2.5-flash', help: 'Gemini API;扩展会根据模型自动构造 generateContent 请求。', models: ['gemini-2.5-flash', 'gemini-2.5-pro', 'gemini-2.0-flash'] },
ollama: { endpoint: 'http://localhost:11434/api/chat', model: 'llama3.2', help: '本机 Ollama。无需 API key;请确保 Ollama 正在运行且模型已下载。', models: ['llama3.2', 'qwen2.5', 'deepseek-r1'] },
azure: { endpoint: '', model: '', help: '填写完整 Azure deployment URL,例如 https://RESOURCE.openai.azure.com/openai/deployments/DEPLOYMENT/chat/completions?api-version=2024-10-21。模型填 deployment 名称。', models: [] },
custom: { endpoint: '', model: '', help: '任何返回 OpenAI Chat Completions 格式的服务。填写完整 endpoint,或填写以 /v1 结尾的基础地址。', models: [] }
};
const $ = (id) => document.querySelector(`#${id}`);
const currentSettings = () => Object.fromEntries(fields.map((id) => [id, $(id).value.trim()]));
const showStatus = (message, error = false) => { const status = $('status'); status.textContent = message; status.classList.toggle('error', error); };
function openLibraryDatabase() {
return new Promise((resolve, reject) => { const request = indexedDB.open('leafreaderchrome', 1); request.onupgradeneeded = () => { if (!request.result.objectStoreNames.contains('documents')) request.result.createObjectStore('documents', { keyPath:'id' }); }; request.onsuccess = () => resolve(request.result); request.onerror = () => reject(request.error); });
}
async function readDocuments() {
const db = await openLibraryDatabase(); return new Promise((resolve, reject) => { const request = db.transaction('documents').objectStore('documents').getAll(); request.onsuccess = () => { db.close(); resolve(request.result); }; request.onerror = () => { db.close(); reject(request.error); }; });
}
async function replaceDocuments(documents) {
const db = await openLibraryDatabase(); await new Promise((resolve, reject) => { const transaction = db.transaction('documents', 'readwrite'); const store = transaction.objectStore('documents'); store.clear(); for (const document of documents) store.put(document); transaction.oncomplete = resolve; transaction.onerror = () => reject(transaction.error); transaction.onabort = () => reject(transaction.error); }); db.close();
}
function downloadBackup(backup) {
const url = URL.createObjectURL(new Blob([JSON.stringify(backup, null, 2)], { type:'application/json;charset=utf-8' })); const link = document.createElement('a'); link.href = url; link.download = `leafreader-backup-${new Date().toISOString().slice(0, 10)}.json`; link.click(); setTimeout(() => URL.revokeObjectURL(url), 1000);
}
function applyProvider(preserveValues = false) {
const config = providers[$('provider').value] || providers.custom;
if (!preserveValues || !$('endpoint').value) $('endpoint').value = config.endpoint;
if (!preserveValues || !$('model').value) $('model').value = config.model;
$('endpointHelp').textContent = config.help;
$('keyRow').querySelector('input').placeholder = $('provider').value === 'ollama' ? 'Not needed for Ollama' : 'API key';
$('keyRow').classList.toggle('optional', $('provider').value === 'ollama');
$('modelSuggestions').replaceChildren(...config.models.map((model) => { const option = document.createElement('option'); option.value = model; return option; }));
}
(async () => { const { settings = {} } = await chrome.storage.local.get('settings'); if (settings.provider === 'deepseek' && settings.endpoint === 'https://api.deepseek.com/v1/chat/completions') { settings.endpoint = providers.deepseek.endpoint; await chrome.storage.local.set({ settings }); } for (const id of fields) $(id).value = settings[id] || ({ language: 'auto', font: 'serif', provider: 'openai' }[id] || ''); applyProvider(true); })();
$('provider').onchange = () => { const config = providers[$('provider').value]; $('endpoint').value = config.endpoint; $('model').value = config.model; applyProvider(true); };
$('save').onclick = async () => { await chrome.storage.local.set({ settings: currentSettings() }); showStatus('Saved'); setTimeout(() => showStatus(''), 1800); };
$('test').onclick = async () => { const button = $('test'); await chrome.storage.local.set({ settings: currentSettings() }); button.disabled = true; showStatus('Testing AI connection…'); try { const result = await chrome.runtime.sendMessage({ type: 'AI_TEST' }); showStatus(result?.ok ? `Connected: ${result.content}` : (result?.error || 'No response from extension background.'), !result?.ok); } catch (error) { showStatus(`Test failed: ${error.message}`, true); } finally { button.disabled = false; } };
$('exportData').onclick = async () => {
const button = $('exportData'); button.disabled = true;
try {
const { settings = {}, annotations = [], vocabulary = [], aiConversations = {}, ttsPreferences = {} } = await chrome.storage.local.get(['settings', 'annotations', 'vocabulary', 'aiConversations', 'ttsPreferences']);
const exportedSettings = { ...settings }; if (!$('includeApiKey').checked) delete exportedSettings.apiKey;
const documents = await readDocuments();
downloadBackup({ format:'leafreaderchrome-backup', version:1, exportedAt:new Date().toISOString(), includesApiKey:Boolean($('includeApiKey').checked), settings:exportedSettings, ttsPreferences, annotations, vocabulary, aiConversations, documents });
showStatus(`Exported ${documents.length} webpages, ${annotations.length} annotations, and ${vocabulary.length} words.`);
} catch (error) { showStatus(`Could not export backup: ${error.message}`, true); } finally { button.disabled = false; }
};
$('importData').onchange = async (event) => {
const file = event.target.files?.[0]; event.target.value = ''; if (!file) return;
try {
const backup = JSON.parse(await file.text());
if (backup.format !== 'leafreaderchrome-backup' || !Array.isArray(backup.documents) || !Array.isArray(backup.annotations) || !Array.isArray(backup.vocabulary)) throw new Error('This is not a valid LeafReader Chrome backup.');
if (!confirm(`Restore ${backup.documents.length} webpages, ${backup.annotations.length} annotations, and ${backup.vocabulary.length} words? Current LeafReader data will be replaced.`)) return;
const currentData = await chrome.storage.local.get(['settings', 'ttsPreferences']); const current = currentData.settings || {}; const restoredSettings = { ...(backup.settings || {}) };
let clearedApiKey = false;
if (!backup.includesApiKey) {
const sameTarget = LeafSettings.canReuseApiKey(current, restoredSettings);
restoredSettings.apiKey = sameTarget ? (current.apiKey || '') : '';
clearedApiKey = Boolean(current.apiKey && !sameTarget);
}
await chrome.storage.local.set({ settings:restoredSettings, ttsPreferences:backup.ttsPreferences || currentData.ttsPreferences || {} });
const replaced = await chrome.runtime.sendMessage({ type:'STORAGE_MUTATION', mutation:{ operation:'replaceCollections', values:{ annotations:backup.annotations, vocabulary:backup.vocabulary, aiConversations:backup.aiConversations || {} } } });
if (!replaced?.ok) throw new Error(replaced?.error || 'Could not replace saved reading records.');
await replaceDocuments(backup.documents);
for (const id of fields) $(id).value = restoredSettings[id] || ({ language:'auto', font:'serif', provider:'openai' }[id] || ''); applyProvider(true); showStatus(clearedApiKey ? 'Backup restored. The API key was cleared because the provider or endpoint changed; enter the correct key before using AI.' : 'Backup restored. Reopen the library to see restored webpages.');
} catch (error) { showStatus(`Could not restore backup: ${error.message}`, true); }
};
$('runDiagnostics').onclick = async () => { const output = $('diagnostics'); const button = $('runDiagnostics'); button.disabled = true; output.textContent = 'Checking…'; output.classList.remove('error'); try { const { annotations = [], vocabulary = [], aiConversations = {}, ttsPreferences = {} } = await chrome.storage.local.get(['annotations', 'vocabulary', 'aiConversations', 'ttsPreferences']); const documents = await readDocuments(); const voices = speechSynthesis.getVoices(); const localVoices = voices.filter((voice) => voice.localService).length; const bytes = await chrome.storage.local.getBytesInUse(); const configured = Boolean(currentSettings().endpoint && currentSettings().model && (currentSettings().apiKey || currentSettings().provider === 'ollama')); output.textContent = `Extension ${chrome.runtime.getManifest().version} · ${documents.length} webpages · ${annotations.length} annotations · ${vocabulary.length} words · ${Object.keys(aiConversations).length} AI conversations · ${voices.length} browser voices (${localVoices} local, ${voices.length - localVoices} online; local-only ${ttsPreferences.localOnly !== false ? 'on' : 'off'}) · ${(bytes / 1024).toFixed(1)} KB local storage · AI ${configured ? 'configured' : 'not configured'}.`; } catch (error) { output.textContent = `Diagnostics failed: ${error.message}`; output.classList.add('error'); } finally { button.disabled = false; } };