Files
LittleWhiteBox/core/server-storage.js

187 lines
6.1 KiB
JavaScript
Raw Normal View History

2026-01-17 16:34:39 +08:00
// ═══════════════════════════════════════════════════════════════════════════
// 服务器文件存储工具
// ═══════════════════════════════════════════════════════════════════════════
import { getRequestHeaders } from '../../../../../script.js';
import { debounce } from '../../../../utils.js';
const toBase64 = (text) => btoa(unescape(encodeURIComponent(text)));
class StorageFile {
constructor(filename, opts = {}) {
this.filename = filename;
this.cache = null;
this._loading = null;
this._dirtyVersion = 0;
this._savedVersion = 0;
this._saving = false;
this._pendingSave = false;
this._retryCount = 0;
this._retryTimer = null;
this._maxRetries = Number.isFinite(opts.maxRetries) ? opts.maxRetries : 5;
const debounceMs = Number.isFinite(opts.debounceMs) ? opts.debounceMs : 2000;
this._saveDebounced = debounce(() => this.saveNow({ silent: true }), debounceMs);
}
async load() {
if (this.cache !== null) return this.cache;
if (this._loading) return this._loading;
this._loading = (async () => {
try {
const res = await fetch(`/user/files/${this.filename}`, {
headers: getRequestHeaders(),
cache: 'no-cache',
});
if (!res.ok) {
this.cache = {};
return this.cache;
}
const text = await res.text();
this.cache = text ? (JSON.parse(text) || {}) : {};
} catch {
this.cache = {};
} finally {
this._loading = null;
}
return this.cache;
})();
return this._loading;
}
async get(key, defaultValue = null) {
const data = await this.load();
return data[key] ?? defaultValue;
}
async set(key, value) {
const data = await this.load();
data[key] = value;
this._dirtyVersion++;
this._saveDebounced();
}
async delete(key) {
const data = await this.load();
if (key in data) {
delete data[key];
this._dirtyVersion++;
this._saveDebounced();
}
}
/**
* 立即保存
* @param {Object} options
* @param {boolean} options.silent - 静默模式失败时不抛异常返回 false
* @returns {Promise<boolean>} 是否保存成功
*/
async saveNow({ silent = true } = {}) {
// 🔧 核心修复:非静默模式等待当前保存完成
if (this._saving) {
this._pendingSave = true;
if (!silent) {
await this._waitForSaveComplete();
if (this._dirtyVersion > this._savedVersion) {
return this.saveNow({ silent });
}
return this._dirtyVersion === this._savedVersion;
}
return true;
}
if (!this.cache || this._dirtyVersion === this._savedVersion) {
return true;
}
this._saving = true;
this._pendingSave = false;
const versionToSave = this._dirtyVersion;
try {
const json = JSON.stringify(this.cache);
const base64 = toBase64(json);
const res = await fetch('/api/files/upload', {
method: 'POST',
headers: getRequestHeaders(),
body: JSON.stringify({ name: this.filename, data: base64 }),
});
if (!res.ok) {
throw new Error(`服务器返回 ${res.status}`);
}
this._savedVersion = Math.max(this._savedVersion, versionToSave);
this._retryCount = 0;
if (this._retryTimer) {
clearTimeout(this._retryTimer);
this._retryTimer = null;
}
return true;
} catch (err) {
console.error('[ServerStorage] 保存失败:', err);
this._retryCount++;
const delay = Math.min(30000, 2000 * (2 ** Math.max(0, this._retryCount - 1)));
if (!this._retryTimer && this._retryCount <= this._maxRetries) {
this._retryTimer = setTimeout(() => {
this._retryTimer = null;
this.saveNow({ silent: true });
}, delay);
}
if (!silent) {
throw err;
}
return false;
} finally {
this._saving = false;
if (this._pendingSave || this._dirtyVersion > this._savedVersion) {
this._saveDebounced();
}
}
}
/** 等待保存完成 */
_waitForSaveComplete() {
return new Promise(resolve => {
const check = () => {
if (!this._saving) resolve();
else setTimeout(check, 50);
};
check();
});
}
clearCache() {
this.cache = null;
this._loading = null;
}
getCacheSize() {
if (!this.cache) return 0;
return Object.keys(this.cache).length;
}
getCacheBytes() {
if (!this.cache) return 0;
try {
return JSON.stringify(this.cache).length * 2;
} catch {
return 0;
}
}
}
export const TasksStorage = new StorageFile('LittleWhiteBox_Tasks.json');
export const StoryOutlineStorage = new StorageFile('LittleWhiteBox_StoryOutline.json');
export const NovelDrawStorage = new StorageFile('LittleWhiteBox_NovelDraw.json', { debounceMs: 800 });
export const TtsStorage = new StorageFile('LittleWhiteBox_TTS.json', { debounceMs: 800 });
export const CommonSettingStorage = new StorageFile('LittleWhiteBox_CommonSettings.json', { debounceMs: 1000 });
2026-01-26 01:16:35 +08:00
export const VectorStorage = new StorageFile('LittleWhiteBox_Vectors.json', { debounceMs: 3000 });