/** * @file js/storage/storage-adapter.js * @description * 存储适配器 - 支持 localStorage(前端模式)和 Backend API(后端模式)双模式 * * 使用方式: * 1. 前端模式(Vercel/静态/直接打开 index.html): 使用 localStorage + IndexedDB * 2. 后端模式(Docker/自建后端): 使用 Backend API + 数据库 */ // ---------------- 部署模式与后端探测 ---------------- // 优先级(高→低):URL 查询参数 ?mode=backend|frontend → window.ENV_DEPLOYMENT_MODE → 自动探测 /api/health → 默认 frontend function getQueryModeOverride() { try { const p = new URLSearchParams(window.location.search); const m = (p.get('mode') || '').toLowerCase(); if (m === 'backend' || m === 'frontend') return m; } catch {} return null; } let DEPLOYMENT_MODE = (getQueryModeOverride() || (window.ENV_DEPLOYMENT_MODE && window.ENV_DEPLOYMENT_MODE !== 'auto' ? window.ENV_DEPLOYMENT_MODE : 'frontend')); const API_BASE_URL = window.ENV_API_BASE_URL || '/api'; async function autoDetectBackendAvailability(timeoutMs = 900) { // file:// 明确无后端 if (window.location.protocol === 'file:') return false; // 显式覆盖不探测 if (getQueryModeOverride() || (window.ENV_DEPLOYMENT_MODE && window.ENV_DEPLOYMENT_MODE !== 'auto')) { return DEPLOYMENT_MODE === 'backend'; } try { const controller = new AbortController(); const id = setTimeout(() => controller.abort(), timeoutMs); const res = await fetch(`${API_BASE_URL}/health`, { signal: controller.signal, cache: 'no-store' }); clearTimeout(id); return res.ok; } catch { return false; } } // ---------------- 认证 Token 管理 ---------------- class AuthManager { static getToken() { return localStorage.getItem('auth_token'); } static setToken(token) { localStorage.setItem('auth_token', token); } static removeToken() { localStorage.removeItem('auth_token'); } static isAuthenticated() { return !!this.getToken(); } static getHeaders() { const token = this.getToken(); return token ? { 'Authorization': `Bearer ${token}`, 'Content-Type': 'application/json' } : { 'Content-Type': 'application/json' }; } } // ---------------- 后端存储实现 ---------------- class BackendStorage { // 回退到本地存储的方法 _fallbackTo(method, ...args) { const localMethod = window[method]; if (typeof localMethod === 'function') { return localMethod.apply(null, args); } console.warn(`[BackendStorage] No local fallback for ${method}`); return Promise.resolve(); } async fetchAPI(endpoint, options = {}) { const response = await fetch(`${API_BASE_URL}${endpoint}`, { ...options, headers: { ...AuthManager.getHeaders(), ...options.headers } }); if (!response.ok) { if (response.status === 401) { // Token 过期,清除token AuthManager.removeToken(); } throw new Error(`API Error: ${response.status}`); } return response.json(); } // 用户设置 async loadSettings() { try { if (!AuthManager.isAuthenticated()) { return this._fallbackTo('loadSettings'); } const data = await this.fetchAPI('/user/settings'); return data; } catch (error) { console.error('Failed to load settings from backend:', error); return this._fallbackTo('loadSettings'); } } async saveSettings(settings) { try { if (!AuthManager.isAuthenticated()) { return this._fallbackTo('saveSettings', settings); } await this.fetchAPI('/user/settings', { method: 'PUT', body: JSON.stringify(settings) }); } catch (error) { console.error('Failed to save settings to backend:', error); return this._fallbackTo('saveSettings', settings); } } // API Keys async loadModelKeys(provider) { try { if (!AuthManager.isAuthenticated()) { return this._fallbackTo('loadModelKeys', provider); } const keys = await this.fetchAPI(`/user/api-keys?provider=${provider}`); return keys; } catch (error) { console.error('Failed to load API keys:', error); return this._fallbackTo('loadModelKeys', provider); } } async saveModelKeys(provider, keys) { try { if (!AuthManager.isAuthenticated()) { return this._fallbackTo('saveModelKeys', provider, keys); } await this.fetchAPI('/user/api-keys', { method: 'POST', body: JSON.stringify({ provider, keys }) }); } catch (error) { console.error('Failed to save API keys:', error); return this._fallbackTo('saveModelKeys', provider, keys); } } // 文档历史 async saveResultToDB(document) { try { if (!AuthManager.isAuthenticated()) { return this._fallbackTo('saveResultToDB', document); } await this.fetchAPI('/documents', { method: 'POST', body: JSON.stringify(document) }); } catch (error) { console.error('Failed to save document to backend, falling back to local:', error); return this._fallbackTo('saveResultToDB', document); } } async getAllResultsFromDB() { try { if (!AuthManager.isAuthenticated()) { return this._fallbackTo('getAllResultsFromDB'); } const data = await this.fetchAPI('/documents'); return data.documents || []; } catch (error) { console.error('Failed to load documents from backend, falling back to local:', error); return this._fallbackTo('getAllResultsFromDB'); } } async getResultFromDB(id) { try { if (!AuthManager.isAuthenticated()) { return this._fallbackTo('getResultFromDB', id); } return await this.fetchAPI(`/documents/${id}`); } catch (error) { console.error('Failed to load document from backend, falling back to local:', error); return this._fallbackTo('getResultFromDB', id); } } async deleteResultFromDB(id) { try { if (!AuthManager.isAuthenticated()) { return this._fallbackTo('deleteResultFromDB', id); } await this.fetchAPI(`/documents/${id}`, { method: 'DELETE' }); } catch (error) { console.error('Failed to delete document from backend, falling back to local:', error); return this._fallbackTo('deleteResultFromDB', id); } } async clearAllResultsFromDB() { try { if (!AuthManager.isAuthenticated()) { return this._fallbackTo('clearAllResultsFromDB'); } // 后端没有批量删除接口,逐个删除 const docs = await this.getAllResultsFromDB(); for (const doc of docs) { await this.fetchAPI(`/documents/${doc.id}`, { method: 'DELETE' }); } } catch (error) { console.error('Failed to clear all documents from backend, falling back to local:', error); return this._fallbackTo('clearAllResultsFromDB'); } } // 术语库 async loadGlossarySets() { try { if (!AuthManager.isAuthenticated()) { return this._fallbackTo('loadGlossarySets'); } const glossaries = await this.fetchAPI('/user/glossaries'); const sets = {}; glossaries.forEach(g => { sets[g.id] = g; }); return sets; } catch (error) { console.error('Failed to load glossaries:', error); return this._fallbackTo('loadGlossarySets'); } } async saveGlossarySets(sets) { try { if (!AuthManager.isAuthenticated()) { return this._fallbackTo('saveGlossarySets', sets); } // 批量保存(简化实现) for (const [id, set] of Object.entries(sets)) { if (set._isNew) { await this.fetchAPI('/user/glossaries', { method: 'POST', body: JSON.stringify(set) }); } else { await this.fetchAPI(`/user/glossaries/${id}`, { method: 'PUT', body: JSON.stringify(set) }); } } } catch (error) { console.error('Failed to save glossaries:', error); return this._fallbackTo('saveGlossarySets', sets); } } // 标注 async saveAnnotationToDB(annotation) { try { if (!AuthManager.isAuthenticated()) { return this._fallbackTo('saveAnnotationToDB', annotation); } await this.fetchAPI(`/documents/${annotation.documentId}/annotations`, { method: 'POST', body: JSON.stringify(annotation) }); } catch (error) { console.error('Failed to save annotation:', error); return this._fallbackTo('saveAnnotationToDB', annotation); } } async getAnnotationsForDocFromDB(docId) { try { if (!AuthManager.isAuthenticated()) { return this._fallbackTo('getAnnotationsForDocFromDB', docId); } return await this.fetchAPI(`/documents/${docId}/annotations`); } catch (error) { console.error('Failed to load annotations:', error); return this._fallbackTo('getAnnotationsForDocFromDB', docId); } } // 聊天历史 async loadChatHistory(docId) { try { if (!AuthManager.isAuthenticated()) { // 聊天历史在本地存储中没有对应方法,返回空数组 return []; } const data = await this.fetchAPI(`/chat/${docId}/history`); return data.messages || []; } catch (error) { console.error('Failed to load chat history:', error); return []; } } async saveChatMessage(docId, message) { try { if (!AuthManager.isAuthenticated()) { // 聊天历史在本地存储中没有对应方法 return; } await this.fetchAPI(`/chat/${docId}/history`, { method: 'POST', body: JSON.stringify(message) }); } catch (error) { console.error('Failed to save chat message:', error); } } async clearChatHistory(docId) { try { if (!AuthManager.isAuthenticated()) { return; } await this.fetchAPI(`/chat/${docId}/history`, { method: 'DELETE' }); } catch (error) { console.error('Failed to clear chat history:', error); } } // 文献引用 async loadReferences(docId) { try { if (!AuthManager.isAuthenticated()) { return this._fallbackTo('loadReferences', docId) || []; } return await this.fetchAPI(`/references/${docId}/references`); } catch (error) { console.error('Failed to load references:', error); return this._fallbackTo('loadReferences', docId) || []; } } async saveReference(docId, reference) { try { if (!AuthManager.isAuthenticated()) { return this._fallbackTo('saveReference', docId, reference); } await this.fetchAPI(`/references/${docId}/references`, { method: 'POST', body: JSON.stringify(reference) }); } catch (error) { console.error('Failed to save reference:', error); return this._fallbackTo('saveReference', docId, reference); } } async deleteReference(docId, refId) { try { if (!AuthManager.isAuthenticated()) { return this._fallbackTo('deleteReference', docId, refId); } await this.fetchAPI(`/references/${docId}/references/${refId}`, { method: 'DELETE' }); } catch (error) { console.error('Failed to delete reference:', error); return this._fallbackTo('deleteReference', docId, refId); } } // Prompt Pool async loadPromptPool() { try { if (!AuthManager.isAuthenticated()) { return this._fallbackTo('loadPromptPool'); } return await this.fetchAPI('/prompt-pool'); } catch (error) { console.error('Failed to load prompt pool:', error); return this._fallbackTo('loadPromptPool'); } } async savePromptPool(data) { try { if (!AuthManager.isAuthenticated()) { return this._fallbackTo('savePromptPool', data); } await this.fetchAPI('/prompt-pool', { method: 'PUT', body: JSON.stringify(data) }); } catch (error) { console.error('Failed to save prompt pool:', error); return this._fallbackTo('savePromptPool', data); } } _getDefaultSettings() { return { maxTokensPerChunk: 2000, skipProcessedFiles: false, selectedTranslationModel: 'none', concurrencyLevel: 1, translationConcurrencyLevel: 15, targetLanguage: 'chinese', customTargetLanguageName: '', enableGlossary: false, batchModeEnabled: false, batchModeTemplate: '{original_name}_{output_language}_{processing_time:YYYYMMDD-HHmmss}.{original_type}', batchModeFormats: ['original', 'markdown'], batchModeZipEnabled: false }; } } // ---------------- 存储适配器工厂 ---------------- class StorageAdapterFactory { static create(mode) { if (mode === 'backend') { console.log('[Storage] Using Backend Storage Mode'); const instance = new BackendStorage(); instance.isFrontendMode = false; // 供其他模块探测 return instance; } console.log('[Storage] Using Local Storage Mode'); // 返回 storage.js 中的函数包装(保持现有调用不变) const adapter = { loadSettings: window.loadSettings, saveSettings: window.saveSettings, loadModelKeys: window.loadModelKeys, saveModelKeys: window.saveModelKeys, saveResultToDB: window.saveResultToDB, getAllResultsFromDB: window.getAllResultsFromDB, getResultFromDB: window.getResultFromDB, deleteResultFromDB: window.deleteResultFromDB, clearAllResultsFromDB: window.clearAllResultsFromDB, loadGlossarySets: window.loadGlossarySets, saveGlossarySets: window.saveGlossarySets, saveAnnotationToDB: window.saveAnnotationToDB, getAnnotationsForDocFromDB: window.getAnnotationsForDocFromDB, updateAnnotationInDB: window.updateAnnotationInDB, deleteAnnotationFromDB: window.deleteAnnotationFromDB, loadProcessedFilesRecord: window.loadProcessedFilesRecord, saveProcessedFilesRecord: window.saveProcessedFilesRecord, // Prompt Pool(前端模式:落地到 localStorage,键与 process/prompt-pool.js 一致) loadPromptPool: async function () { try { const prompts = JSON.parse(localStorage.getItem('paperBurnerPromptPool') || '[]'); const healthConfig = JSON.parse(localStorage.getItem('paperBurnerPromptHealthConfig') || 'null'); return { prompts, healthConfig }; } catch (e) { console.warn('[StorageAdapter] loadPromptPool(local) 失败,返回空集合:', e); return { prompts: [], healthConfig: null }; } }, savePromptPool: async function (data) { try { if (!data || typeof data !== 'object') return; if (Array.isArray(data.prompts)) { localStorage.setItem('paperBurnerPromptPool', JSON.stringify(data.prompts)); } if (data.healthConfig) { localStorage.setItem('paperBurnerPromptHealthConfig', JSON.stringify(data.healthConfig)); } } catch (e) { console.warn('[StorageAdapter] savePromptPool(local) 失败:', e); } } }; adapter.isFrontendMode = true; // 供其他模块探测 return adapter; } } // ---------------- 初始化与自动切换 ---------------- function printBanner() { const infoStyle = 'font-size: 14px; color: #10b981;'; const modeStyle = 'font-size: 14px; font-weight: bold; color: #f59e0b;'; const borderStyle = 'color: #6366f1;'; const linkStyle = 'font-size: 13px; color: #06b6d4; text-decoration: underline;'; const mode = DEPLOYMENT_MODE === 'backend' ? '后端模式 (Backend Mode)' : '前端模式 (Frontend Mode)'; const storage = DEPLOYMENT_MODE === 'backend' ? 'Backend API + PostgreSQL' : 'localStorage + IndexedDB'; const auth = DEPLOYMENT_MODE === 'backend' ? 'JWT Authentication' : 'No Authentication'; console.log('%c╔════════════════════════════════════════════════════════════╗', borderStyle); console.log('%c║ 系统信息 / System Info ║', borderStyle); console.log('%c╠════════════════════════════════════════════════════════════╣', borderStyle); console.log('%c║ %c运行模式: ' + mode + ' %c║', borderStyle, modeStyle, borderStyle); console.log('%c║ %c存储方式: ' + storage + ' %c║', borderStyle, infoStyle, borderStyle); console.log('%c║ %c认证方式: ' + auth + ' %c║', borderStyle, infoStyle, borderStyle); console.log('%c╚════════════════════════════════════════════════════════════╝', borderStyle); console.log('%c\n🚀 Paper Burner X 已就绪!Ready to burn papers!\n', 'font-size: 14px; color: #8b5cf6; font-weight: bold;'); console.log('%c→ GitHub: %chttps://github.com/Feather-2/paper-burner-x', 'font-size: 13px; color: #64748b;', linkStyle); } // 初始实例(默认前端/显式覆盖),随后可能自动切换为 backend window.storageAdapter = StorageAdapterFactory.create(DEPLOYMENT_MODE); window.AuthManager = AuthManager; window.DEPLOYMENT_MODE = DEPLOYMENT_MODE; printBanner(); // 自动探测,有后端则无缝切换(不阻塞静态模式渲染) autoDetectBackendAvailability().then((hasBackend) => { if (hasBackend && DEPLOYMENT_MODE !== 'backend') { DEPLOYMENT_MODE = 'backend'; window.DEPLOYMENT_MODE = DEPLOYMENT_MODE; window.storageAdapter = StorageAdapterFactory.create('backend'); try { window.dispatchEvent(new CustomEvent('pb:storage-mode-changed', { detail: { mode: 'backend' } })); } catch {} console.log('[Storage] Auto-switched to Backend mode (health check passed)'); } }).catch(() => {/* ignore */});