/** * @file js/storage.js * @description * 此文件负责管理应用程序中所有与浏览器本地存储相关的功能, * 包括 localStorage 和 IndexedDB。它提供了统一的接口来保存和加载用户设置、 * API 密钥、已处理文件记录、模型配置以及其他需要持久化的数据。 * * 主要功能包括: * - **常量定义**: 定义用于 localStorage 和 IndexedDB 存储键的常量。 * - **已处理文件记录**: 管理已上传并处理过的文件记录,避免重复处理。 * - **通用设置**: 保存和加载应用的全局设置,如默认处理选项等。 * - **IndexedDB 数据库操作**: * - 打开和初始化名为 `ResultDB` 的 IndexedDB 数据库,其中包含 `results` 对象存储区。 * - 将PDF处理结果(包括元数据、提取的文本、翻译、摘要等)保存到 IndexedDB。 * - 从 IndexedDB 检索、删除或清空处理结果。 * - **UUID 生成**: 提供生成唯一标识符的功能,主要用于 IndexedDB 中的记录ID。 * - **模型配置**: (部分可能为旧版) 保存和加载与特定翻译模型相关的配置,例如自定义模型的 Base URL。 * - **API 密钥管理**: 安全地保存和加载用户为不同翻译服务或自定义模型配置的 API 密钥。 * 支持多模型/多源站点的密钥管理,并包含对旧版单一密钥格式的兼容和迁移逻辑。 * - **旧配置迁移**: 实现将旧版本存储的自定义模型配置迁移到新版多源站点结构的功能。 * - **自定义源站点配置**: 管理用户添加的自定义 API 源站点及其相关配置(如名称、Base URL、API Key、默认模型等)。 */ // ===================== // 常量定义 // ===================== /** * @const {string} SETTINGS_KEY * @description 用于在 localStorage 中存储通用设置的键名。 */ const SETTINGS_KEY = 'userSettings'; /** * @const {string} PROCESSED_FILES_KEY * @description 用于在 localStorage 中存储已处理文件记录的键名。 */ const PROCESSED_FILES_KEY = 'processedFilesRecord'; /** * @const {string} API_KEYS_STORAGE_KEY * @description 用于在 localStorage 中存储(新版)多模型/多源站 API 密钥列表的键名。 * @deprecated 请使用 `MODEL_KEYS_STORAGE_KEY`。此常量可能指向旧的密钥存储方式或已被取代。 */ const API_KEYS_STORAGE_KEY = 'apiKeys'; // 旧的或特定用途的, 新的统一用 modelKeys /** * @const {string} CUSTOM_MODELS_KEY * @description 用于在 localStorage 中存储自定义模型列表的键名 (可能指旧版可用模型列表)。 */ const CUSTOM_MODELS_KEY = 'customModels'; // 存储自定义模型列表 /** * @const {string} LEGACY_CUSTOM_CONFIG_KEY * @description 用于在 localStorage 中存储旧版单一自定义模型配置的键名。 * 这个配置通常包含一个自定义模型的 Base URL 和 API Key。 */ const LEGACY_CUSTOM_CONFIG_KEY = 'custom_model_config'; // 旧的自定义配置key /** * @const {string} MODEL_KEYS_STORAGE_KEY * @description 用于在 localStorage 中存储(新版)与多个模型或源站点关联的 API 密钥及配置列表的键名。 * 这个键名代表了当前推荐的存储 API Keys 和相关源站信息的方式。 */ const MODEL_KEYS_STORAGE_KEY = 'modelKeys'; // 新的存储key,用于多站点 /** * @const {string} GLOSSARY_KEY * @description 翻译备择库(术语库)存储键名。 */ const GLOSSARY_KEY = 'translationGlossary'; /** * @const {string} GLOSSARY_SETS_KEY * @description 多术语库集合键名:{ [id]: { id, name, enabled, entries: [...] } } */ const GLOSSARY_SETS_KEY = 'translationGlossarySets'; /** * @const {string} DB_NAME * @description IndexedDB 数据库的名称。 */ const DB_NAME = 'ResultDB'; /** * @const {string} DB_STORE_NAME * @description IndexedDB 中用于存储处理结果的对象存储区的名称。 */ const DB_STORE_NAME = 'results'; /** * @const {string} ANNOTATIONS_STORE_NAME * @description IndexedDB 中用于存储高亮和批注的对象存储区的名称。 */ const ANNOTATIONS_STORE_NAME = 'annotations'; /** * @const {number} DB_VERSION * @description IndexedDB 数据库的版本号。更改此版本号会触发 `onupgradeneeded` 事件。 */ const DB_VERSION = 3; /** * @const {string} SEMANTIC_GROUPS_STORE_NAME * @description IndexedDB 中用于存储意群数据的对象存储区名称。 */ const SEMANTIC_GROUPS_STORE_NAME = 'semantic_groups'; // ===================== // 本地存储相关工具函数 // ===================== // 导入依赖 (如果需要,例如 showNotification) // import { showNotification } from './ui.js'; const MODEL_CONFIGS_KEY = 'translationModelConfigs'; const MODEL_KEYS_KEY = 'translationModelKeys'; const CUSTOM_SOURCE_SITES_KEY = 'paperBurnerCustomSourceSites'; // 新增:自定义源站列表的 Key const LAST_SUCCESSFUL_KEYS_LS_KEY_STORAGE_REF = 'paperBurnerLastSuccessfulKeys'; // 新增:用于迁移和删除时引用 // --------------------- // API Key 存储与管理 // --------------------- /** * 更新 localStorage 中的 API Key * @param {string} keyName - 存储键名(如 'mistralApiKeys') * @param {string} value - 密钥内容 * @param {boolean} shouldRemember - 是否记住 */ function updateApiKeyStorage(keyName, value, shouldRemember) { // keyName 应该是 'mistralApiKeys' 或 'translationApiKeys' if (shouldRemember) { localStorage.setItem(keyName, value); } else { localStorage.removeItem(keyName); } } // --------------------- // 已处理文件记录 // --------------------- /** * 加载已处理文件记录(防止重复处理) * @returns {Object} 文件标识到 true 的映射 */ function loadProcessedFilesRecord() { let record = {}; try { const storedRecord = localStorage.getItem(PROCESSED_FILES_KEY); if (storedRecord) { record = JSON.parse(storedRecord); console.log("Loaded processed files record:", record); } } catch (e) { console.error("Failed to load processed files record from localStorage:", e); record = {}; // 重置为空对象 } return record; // 返回加载的记录 } /** * 保存已处理文件记录到 localStorage * @param {Object} processedFilesRecord - 文件标识到 true 的映射 */ function saveProcessedFilesRecord(processedFilesRecord) { try { localStorage.setItem(PROCESSED_FILES_KEY, JSON.stringify(processedFilesRecord)); console.log("Saved processed files record."); } catch (e) { console.error("Failed to save processed files record to localStorage:", e); // showNotification("无法保存已处理文件记录到浏览器缓存", "error"); // 避免循环依赖 } } /** * 判断文件是否已处理 * @param {string} fileIdentifier - 文件唯一标识 * @param {Object} processedFilesRecord - 已处理记录 * @returns {boolean} */ function isAlreadyProcessed(fileIdentifier, processedFilesRecord) { return processedFilesRecord.hasOwnProperty(fileIdentifier) && processedFilesRecord[fileIdentifier] === true; } /** * 标记文件为已处理 * @param {string} fileIdentifier - 文件唯一标识 * @param {Object} processedFilesRecord - 已处理记录 */ function markFileAsProcessed(fileIdentifier, processedFilesRecord) { processedFilesRecord[fileIdentifier] = true; // 注意:保存操作通常在批处理结束时进行,而不是每次标记时 } // --------------------- // 通用设置项存储 // --------------------- /** * 保存设置项到 localStorage * @param {Object} settingsData - 设置对象 */ function saveSettings(settingsData) { // settingsData 应该是一个包含所有要保存设置的对象 // 例如: { maxTokensPerChunk: ..., skipProcessedFiles: ..., ... } try { localStorage.setItem(SETTINGS_KEY, JSON.stringify(settingsData)); //console.log("Settings saved:", settingsData); } catch (e) { console.error('保存设置失败:', e); // showNotification('无法保存设置到浏览器缓存', 'error'); // 避免循环依赖 } } /** * 加载设置项(带默认值) * @returns {Object} 设置对象 */ function loadSettings() { let settings = { // 提供默认值 maxTokensPerChunk: '2000', skipProcessedFiles: false, selectedTranslationModel: 'none', concurrencyLevel: '1', translationConcurrencyLevel: '15', targetLanguage: 'chinese', customTargetLanguageName: '', customModelSettings: { apiEndpoint: '', modelId: '', requestFormat: 'openai', temperature: 0.5, max_tokens: 8000 }, defaultSystemPrompt: '', defaultUserPromptTemplate: '', useCustomPrompts: false, enableGlossary: false, batchModeEnabled: false, batchModeTemplate: '{original_name}_{output_language}_{processing_time:YYYYMMDD-HHmmss}.{original_type}', batchModeFormats: ['original', 'markdown'], batchModeZipEnabled: false }; try { const storedSettings = localStorage.getItem(SETTINGS_KEY); if (storedSettings) { const loaded = JSON.parse(storedSettings); // 合并加载的设置与默认值,确保所有键都存在 settings = { ...settings, ...loaded }; // 确保 customModelSettings 也是合并的 if (loaded.customModelSettings) { settings.customModelSettings = { ...settings.customModelSettings, ...loaded.customModelSettings }; } //console.log("Settings loaded:", settings); // 如果启用了自定义模型检测器,尝试加载可用模型 if (typeof initModelDetectorUI === 'function') { setTimeout(() => { loadAvailableModels(); }, 0); } } else { console.log("No settings found in localStorage, using defaults."); } } catch (e) { console.error('加载设置失败,使用默认值:', e); // settings 保持为默认值 } return settings; // 返回加载或默认的设置对象 } /** * 加载可用模型列表 */ function loadAvailableModels() { try { // 这里我们只是触发检查和UI更新,不实际加载模型 // 实际加载和UI更新由modelDetector模块负责 if (typeof window.modelDetector !== 'undefined') { const customModelId = document.getElementById('customModelId'); const customModelIdInput = document.getElementById('customModelIdInput'); // 尝试加载保存的模型列表,如果有 const savedModels = localStorage.getItem('availableCustomModels'); if (savedModels) { const lastSelectedModel = localStorage.getItem('lastSelectedCustomModel'); // 如果有lastSelectedModel,设置输入框的值 if (lastSelectedModel && customModelIdInput) { customModelIdInput.value = lastSelectedModel; } } } } catch (e) { console.error('加载可用模型列表失败:', e); } } // --- IndexedDB 历史记录存储 --- function openDB() { return new Promise(function(resolve, reject) { const req = indexedDB.open(DB_NAME, DB_VERSION); req.onupgradeneeded = function(e) { const db = e.target.result; if (!db.objectStoreNames.contains(DB_STORE_NAME)) { db.createObjectStore(DB_STORE_NAME, { keyPath: 'id' }); } // 新增:创建 annotations 对象存储区 if (!db.objectStoreNames.contains(ANNOTATIONS_STORE_NAME)) { const annotationsStore = db.createObjectStore(ANNOTATIONS_STORE_NAME, { keyPath: 'id' }); annotationsStore.createIndex('docId', 'docId', { unique: false }); } // 新增:创建 semantic_groups 对象存储区(按 docId 存取) if (!db.objectStoreNames.contains(SEMANTIC_GROUPS_STORE_NAME)) { db.createObjectStore(SEMANTIC_GROUPS_STORE_NAME, { keyPath: 'docId' }); } }; req.onsuccess = function() { resolve(req.result); }; req.onerror = function() { reject(req.error); }; }); } async function saveResultToDB(resultObj) { const db = await openDB(); return new Promise(function(resolve, reject) { const tx = db.transaction(DB_STORE_NAME, 'readwrite'); tx.objectStore(DB_STORE_NAME).put(resultObj); tx.oncomplete = function() { resolve(); }; tx.onerror = function() { reject(tx.error); }; }); } async function getAllResultsFromDB() { const db = await openDB(); return new Promise(function(resolve, reject) { const tx = db.transaction(DB_STORE_NAME, 'readonly'); const store = tx.objectStore(DB_STORE_NAME); const req = store.getAll(); req.onsuccess = function() { resolve(req.result); }; req.onerror = function() { reject(req.error); }; }); } async function getResultFromDB(id) { const db = await openDB(); return new Promise(function(resolve, reject) { const tx = db.transaction(DB_STORE_NAME, 'readonly'); const store = tx.objectStore(DB_STORE_NAME); const req = store.get(id); req.onsuccess = function() { resolve(req.result); }; req.onerror = function() { reject(req.error); }; }); } async function deleteResultFromDB(id) { const db = await openDB(); return new Promise(function(resolve, reject) { const tx = db.transaction(DB_STORE_NAME, 'readwrite'); tx.objectStore(DB_STORE_NAME).delete(id); tx.oncomplete = function() { resolve(); }; tx.onerror = function() { reject(tx.error); }; }); } async function clearAllResultsFromDB() { const db = await openDB(); return new Promise(function(resolve, reject) { const tx = db.transaction(DB_STORE_NAME, 'readwrite'); tx.objectStore(DB_STORE_NAME).clear(); tx.oncomplete = function() { resolve(); }; tx.onerror = function() { reject(tx.error); }; }); } // ========== 新增:多模型配置与Key存取 ========== // --------- 新增:意群数据持久化(IndexedDB) --------- /** * 将意群数据保存到 IndexedDB。 * @param {string} docId 文档唯一ID。 * @param {Array} groups 意群数组。 * @param {Object} [extra] 额外信息,例如版本、来源等。 */ async function saveSemanticGroupsToDB(docId, groups, extra = {}) { const db = await openDB(); return new Promise(function(resolve, reject) { try { const tx = db.transaction(SEMANTIC_GROUPS_STORE_NAME, 'readwrite'); const store = tx.objectStore(SEMANTIC_GROUPS_STORE_NAME); store.put({ docId, groups, updatedAt: Date.now(), ...extra }); tx.oncomplete = function() { resolve(); }; tx.onerror = function() { reject(tx.error); }; } catch (e) { reject(e); } }); } /** * 从 IndexedDB 加载意群数据。 * @param {string} docId 文档唯一ID。 * @returns {Promise<{docId: string, groups: Array, updatedAt: number} | undefined>} */ async function loadSemanticGroupsFromDB(docId) { const db = await openDB(); return new Promise(function(resolve, reject) { try { const tx = db.transaction(SEMANTIC_GROUPS_STORE_NAME, 'readonly'); const store = tx.objectStore(SEMANTIC_GROUPS_STORE_NAME); const req = store.get(docId); req.onsuccess = function() { resolve(req.result); }; req.onerror = function() { reject(req.error); }; } catch (e) { reject(e); } }); } /** * 删除指定文档的意群数据。 * @param {string} docId 文档唯一ID。 */ async function deleteSemanticGroupsFromDB(docId) { const db = await openDB(); return new Promise(function(resolve, reject) { try { const tx = db.transaction(SEMANTIC_GROUPS_STORE_NAME, 'readwrite'); const store = tx.objectStore(SEMANTIC_GROUPS_STORE_NAME); store.delete(docId); tx.oncomplete = function() { resolve(); }; tx.onerror = function() { reject(tx.error); }; } catch (e) { reject(e); } }); } // 暴露到全局(页面通过