paper-burner/js/storage/storage-adapter.js

513 lines
18 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.

/**
* @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 */});