diff --git a/src/pkg/config/config.test.ts b/src/pkg/config/config.test.ts new file mode 100644 index 000000000..b4e18c390 --- /dev/null +++ b/src/pkg/config/config.test.ts @@ -0,0 +1,159 @@ +import { describe, expect, it, beforeEach } from "vitest"; +import { SystemConfig } from "./config"; +import { MessageQueue } from "@Packages/message/message_queue"; + +describe("SystemConfig 双 storage 与懒迁移", () => { + let mq: MessageQueue; + let config: SystemConfig; + + beforeEach(() => { + // 清空 storage 数据 + chrome.storage.sync.clear(); + chrome.storage.local.clear(); + mq = new MessageQueue(); + config = new SystemConfig(mq); + }); + + describe("local key 读写", () => { + it("cloud_sync 应写入 local storage 而非 sync", async () => { + const cloudSync = { + enable: true, + syncDelete: false, + syncStatus: true, + filesystem: "onedrive" as const, + params: { token: "test" }, + }; + config.setCloudSync(cloudSync); + + const result = await config.getCloudSync(); + expect(result).toEqual(cloudSync); + + // 验证值在 local 中 + const localData = await chrome.storage.local.get("system_cloud_sync"); + expect(localData["system_cloud_sync"]).toEqual(cloudSync); + + // 验证值不在 sync 中 + const syncData = await chrome.storage.sync.get("system_cloud_sync"); + expect(syncData["system_cloud_sync"]).toBeUndefined(); + }); + + it("language 应写入 local storage", async () => { + config.setLanguage("zh-CN"); + + // 通过 storage 验证写入位置 + const localData = await chrome.storage.local.get("system_language"); + expect(localData["system_language"]).toBe("zh-CN"); + + const syncData = await chrome.storage.sync.get("system_language"); + expect(syncData["system_language"]).toBeUndefined(); + }); + + it("vscode_url 应写入 local storage", async () => { + config.setVscodeUrl("ws://localhost:9999"); + + const localData = await chrome.storage.local.get("system_vscode_url"); + expect(localData["system_vscode_url"]).toBe("ws://localhost:9999"); + + const syncData = await chrome.storage.sync.get("system_vscode_url"); + expect(syncData["system_vscode_url"]).toBeUndefined(); + }); + + it("enable_script 应写入 local storage", async () => { + config.setEnableScript(false); + + const localData = await chrome.storage.local.get("system_enable_script"); + expect(localData["system_enable_script"]).toBe(false); + + const syncData = await chrome.storage.sync.get("system_enable_script"); + expect(syncData["system_enable_script"]).toBeUndefined(); + }); + }); + + describe("sync key 读写", () => { + it("check_script_update_cycle 应写入 sync storage", async () => { + config.setCheckScriptUpdateCycle(3600); + + const syncData = await chrome.storage.sync.get("system_check_script_update_cycle"); + expect(syncData["system_check_script_update_cycle"]).toBe(3600); + + const localData = await chrome.storage.local.get("system_check_script_update_cycle"); + expect(localData["system_check_script_update_cycle"]).toBeUndefined(); + }); + + it("enable_eslint 应写入 sync storage", async () => { + config.setEnableEslint(false); + + const syncData = await chrome.storage.sync.get("system_enable_eslint"); + expect(syncData["system_enable_eslint"]).toBe(false); + + const localData = await chrome.storage.local.get("system_enable_eslint"); + expect(localData["system_enable_eslint"]).toBeUndefined(); + }); + }); + + describe("懒迁移:sync → local", () => { + it("local key 的旧数据应从 sync 迁移到 local", async () => { + // 模拟旧版本数据在 sync 中 + const oldCloudSync = { + enable: true, + syncDelete: false, + syncStatus: true, + filesystem: "webdav" as const, + params: { url: "https://example.com" }, + }; + await chrome.storage.sync.set({ system_cloud_sync: oldCloudSync }); + + // 读取时应自动迁移 + const result = await config.getCloudSync(); + expect(result).toEqual(oldCloudSync); + + // 验证已迁移到 local + const localData = await chrome.storage.local.get("system_cloud_sync"); + expect(localData["system_cloud_sync"]).toEqual(oldCloudSync); + + // 验证已从 sync 中删除 + const syncData = await chrome.storage.sync.get("system_cloud_sync"); + expect(syncData["system_cloud_sync"]).toBeUndefined(); + }); + + it("local 有值时不应回退到 sync", async () => { + const localValue = "zh-CN"; + const syncValue = "en-US"; + await chrome.storage.local.set({ system_language: localValue }); + await chrome.storage.sync.set({ system_language: syncValue }); + + const result = await config.getLanguage(); + expect(result).toBe(localValue); + + // sync 中的值不应被删除(因为 local 有值,不触发迁移) + const syncData = await chrome.storage.sync.get("system_language"); + expect(syncData["system_language"]).toBe(syncValue); + }); + + it("sync 和 local 都没有值时返回默认值", async () => { + const result = await config.getCloudSync(); + expect(result).toEqual({ + enable: false, + syncDelete: false, + syncStatus: true, + filesystem: "webdav", + params: {}, + }); + }); + + it("迁移后再次读取应走缓存", async () => { + await chrome.storage.sync.set({ system_vscode_url: "ws://old:8642" }); + + // 第一次读取触发迁移 + const first = await config.getVscodeUrl(); + expect(first).toBe("ws://old:8642"); + + // 修改 local storage(模拟外部写入),验证缓存生效 + await chrome.storage.local.set({ system_vscode_url: "ws://new:9999" }); + + // 第二次读取应返回缓存值 + const second = await config.getVscodeUrl(); + expect(second).toBe("ws://old:8642"); + }); + }); +}); diff --git a/src/pkg/config/config.ts b/src/pkg/config/config.ts index 4215fb516..73e075b94 100644 --- a/src/pkg/config/config.ts +++ b/src/pkg/config/config.ts @@ -8,6 +8,7 @@ import { ExtVersion } from "@App/app/const"; import defaultTypeDefinition from "@App/template/scriptcat.d.tpl"; import { toCamelCase } from "../utils/utils"; import EventEmitter from "eventemitter3"; +import { STORAGE_LOCAL_KEYS } from "./consts"; export const SystemConfigChange = "systemConfigChange"; @@ -73,7 +74,19 @@ export type SystemConfigValueType = export class SystemConfig { private readonly cache = new Map(); - private readonly storage = new ChromeStorage("system", true); + // 跨设备同步的配置项,使用 chrome.storage.sync + private readonly syncStorage = new ChromeStorage("system", true); + // 设备相关的配置项,使用 chrome.storage.local(不跨设备同步) + private readonly localStorage = new ChromeStorage("system", false); + + private isLocalKey(key: string): boolean { + return STORAGE_LOCAL_KEYS.has(key); + } + + // 获取 key 对应的主 storage + private getStorage(key: string): ChromeStorage { + return this.isLocalKey(key) ? this.localStorage : this.syncStorage; + } private EE: EventEmitter = new EventEmitter(); @@ -108,21 +121,47 @@ export class SystemConfig { return this.addListener(key, callback); } + private resolveDefault(defaultValue: WithAsyncValue>): T | Promise { + //@ts-ignore + return (defaultValue?.asyncValue?.() || defaultValue) as T | Promise; + } + + private async transferSyncToLocal( + key: SystemConfigKey, + defaultValue: WithAsyncValue> + ): Promise { + const syncVal = await this.syncStorage.get(key); + if (syncVal === undefined) { + this.cache.set(key, undefined); + return this.resolveDefault(defaultValue); + } + // 迁移到 local storage 并从 sync 中删除 + await this.syncStorage.remove(key); // 先删除 + await this.localStorage.set(key, syncVal); // 删除成功后储回本地 + this.cache.set(key, syncVal); + return syncVal as T; + } + private _get( key: SystemConfigKey, defaultValue: WithAsyncValue> ): Promise { if (this.cache.has(key)) { - let val = this.cache.get(key); - //@ts-ignore - val = (val === undefined ? defaultValue?.asyncValue?.() || defaultValue : val) as T | Promise; - return Promise.resolve(val); + const val = this.cache.get(key); + return Promise.resolve(val === undefined ? this.resolveDefault(defaultValue) : (val as T)); } - return this.storage.get(key).then((val) => { + const storage = this.getStorage(key); + return storage.get(key).then((val) => { + if (val !== undefined) { + this.cache.set(key, val); + return val as T; + } + // 对 local key,回退读取 sync storage(兼容旧版本数据迁移) + if (this.isLocalKey(key)) { + return this.transferSyncToLocal(key, defaultValue); + } this.cache.set(key, val); - //@ts-ignore - val = (val === undefined ? defaultValue?.asyncValue?.() || defaultValue : val) as T | Promise; - return val; + return this.resolveDefault(defaultValue); }); } @@ -150,29 +189,25 @@ export class SystemConfig { private _set(key: T, value: SystemConfigValueType | undefined) { const prev = this.cache.get(key); + const storage = this.getStorage(key); + let asyncOp; if (value === undefined) { this.cache.delete(key); - this.storage.remove(key); + asyncOp = storage.remove(key); } else { this.cache.set(key, value); - this.storage.set(key, value); + asyncOp = storage.set(key, value); } - // 发送消息通知更新 - this.mq.publish>(SystemConfigChange, { - key, - value, - prev, + asyncOp.then(() => { + // 发送消息通知更新 + this.mq.publish>(SystemConfigChange, { + key, + value, + prev, + }); }); } - public getChangetime() { - return this._get("changetime", 0); - } - - public setChangetime(n: number) { - this._set("changetime", n); - } - defaultCheckScriptUpdateCycle() { return 86400; } diff --git a/src/pkg/config/consts.ts b/src/pkg/config/consts.ts new file mode 100644 index 000000000..6fa6bf5db --- /dev/null +++ b/src/pkg/config/consts.ts @@ -0,0 +1,14 @@ +// 设备相关的配置项,存储在 chrome.storage.local 而非 sync +// 这些配置不应跨设备同步(如云同步认证、VSCode 连接、UI 布局等) +export const STORAGE_LOCAL_KEYS: Set = new Set([ + "cloud_sync", // 云同步配置(token 存在本地,不应跨设备同步) + "backup", // 备份配置(含设备相关 filesystem params) + "cat_file_storage", // CAT 文件存储配置 + "vscode_url", // VSCode 连接地址(设备相关) + "vscode_reconnect", // VSCode 自动重连 + "language", // 语言偏好(可能因设备不同) + "script_list_column_width", // UI 列宽(取决于屏幕尺寸) + "check_update", // 扩展更新通知及已读状态(各设备已读状态独立) + "enable_script", // 全局脚本开关(设备独立) + "enable_script_incognito", // 隐身模式开关(浏览器级别) +]);