跨端: 接手 pi 的两个 WebUI 开项——默认值统一到服务端契约 12/4;缓存键按账号(含一次性迁移)
pi 问"这两个开项谁执行",我接了(他那边无 shell,我这边改过 WebUI)。两件都是他读源码读出来的实缺陷。 ## 1 默认值:不是审美,是**服务端契约**(pi 更正了自己上一封) `server/internal/models/models.go` 的 `DefaultAppearance()` 明写 `BgDim: 12, BgBlur: 4`, 且注释宣称"与客户端 backgroundStore / themeStore 的默认值一致"——而 WebUI 的 `backgroundStore.ts` 是 `dim: 24, blur: 8`,**那句注释是假的**;`lib/appearance.ts` 的 `clamp(..., 12, 4)` 又是另一套。**同一份代码里两个"默认值"**,走哪条路就落哪个数。 后果不是"两处代码不一样"这么轻:服务端"没有记录"时客户端以本地为准推上去, 于是**新账号的初始外观由第一个同步它的客户端决定**(先 WebUI 登录存 24/8, 先鸿蒙登录存 12/4)——同一个账号,压暗强度取决于谁先到。 改法:新增 `src/lib/appearanceDefaults.ts` 作为**唯一来源**(DEFAULT_DIM/DEFAULT_BLUR/ 上限),`backgroundStore` 与 `lib/appearance` 都引用它,字面量全部消失。 ## 2 缓存键按账号(含旧全局键的一次性迁移) `STORAGE_KEY = 'agentmail.background'` → `storageKey(accountId)` = 前缀 + 账号; 写盘只走 `storageKey()`;旧全局键**只作为迁移源**:当前账号首次读到它时接管并存进自己的键, 然后**立刻删除**(否则下一个账号继续从它"继承",等于把刚修的缺陷留在原地); 未登录时不迁移(旧值不能送给一个还不知道是谁的账号)。 配套顺序:`appearanceSync` 在账号切换时**先 `reloadForAccount()` 再 `pull()`** —— 服务端"没有记录"时 `pull()` 会"以本地为准推上去",那时"本地"必须已经是本账号的值。 ## 3 判据(新增第 13 个判据文件 appearance-defaults) `test/appearance-defaults.test.mjs`:**去 Go 源码里读** `DefaultAppearance()` 的四个字段, 再比对三处(WebUI 常量、store 的 DEFAULT_BACKGROUND 不许有字面量、鸿蒙 Appearance 的字段默认值); 另两条钉"键按账号、不许退回全局键、旧键必须被删除"与"重读在 pull 之前"。 这样服务端那句注释是**可核对**的,不是承诺。 顺带更正:`Wallpaper.ts` 里"WebUI 默认 24"的注释已过时 → 改 12 并写明缘由; `harmony-appearance` 里"WebUI 是全局键"的前提失效 → 改为断言两端都按账号分键。 ## 验证 `npm test` 退出码 0(13 个判据文件全绿 + vitest 263 passed,原 258 + 新增 5 条行为测试: 键隔离、迁移一次并删除、未登录不迁移、默认值=12/4)。
This commit is contained in:
@ -1,13 +1,14 @@
|
||||
import { beforeEach, describe, expect, it } from 'vitest';
|
||||
import {
|
||||
DEFAULT_BACKGROUND,
|
||||
LEGACY_STORAGE_KEY,
|
||||
MAX_DATA_URL_BYTES,
|
||||
STORAGE_KEY,
|
||||
readStored,
|
||||
storageKey,
|
||||
applyBackground,
|
||||
clampBlur,
|
||||
clampDim,
|
||||
normalizeBackground,
|
||||
readStored,
|
||||
useBackgroundStore
|
||||
} from '../../src/stores/backgroundStore';
|
||||
|
||||
@ -77,7 +78,7 @@ describe('脏数据归一化', () => {
|
||||
});
|
||||
|
||||
it('★ 瞬态里的空图片状态被持久化后,重读必须退回 none(静止态不放松)', () => {
|
||||
localStorage.setItem(STORAGE_KEY, JSON.stringify({ kind: 'image', imageDataUrl: '', dim: 30, blur: 10 }));
|
||||
localStorage.setItem(storageKey(''), JSON.stringify({ kind: 'image', imageDataUrl: '', dim: 30, blur: 10 }));
|
||||
const back = readStored();
|
||||
expect(back.kind).toBe('none');
|
||||
// 用户接着选图片仍能成功(不是"坏状态卡死")
|
||||
@ -155,13 +156,13 @@ describe('store 动作', () => {
|
||||
const s = useBackgroundStore.getState();
|
||||
expect(s.kind).toBe('preset');
|
||||
expect(s.presetId).toBe('sand');
|
||||
expect(JSON.parse(localStorage.getItem(STORAGE_KEY)!).presetId).toBe('sand');
|
||||
expect(JSON.parse(localStorage.getItem(storageKey(''))!).presetId).toBe('sand');
|
||||
});
|
||||
|
||||
it('setImage 记录图片并落盘', () => {
|
||||
useBackgroundStore.getState().setImage('data:image/jpeg;base64,DDDD');
|
||||
expect(useBackgroundStore.getState().kind).toBe('image');
|
||||
expect(JSON.parse(localStorage.getItem(STORAGE_KEY)!).imageDataUrl).toContain('DDDD');
|
||||
expect(JSON.parse(localStorage.getItem(storageKey(''))!).imageDataUrl).toContain('DDDD');
|
||||
});
|
||||
|
||||
it('reset 回到默认并落盘', () => {
|
||||
@ -170,7 +171,7 @@ describe('store 动作', () => {
|
||||
const s = useBackgroundStore.getState();
|
||||
expect(s.kind).toBe('none');
|
||||
expect(s.presetId).toBe(DEFAULT_BACKGROUND.presetId);
|
||||
expect(JSON.parse(localStorage.getItem(STORAGE_KEY)!).kind).toBe('none');
|
||||
expect(JSON.parse(localStorage.getItem(storageKey(''))!).kind).toBe('none');
|
||||
});
|
||||
|
||||
it('localStorage 抛异常时不打断操作', () => {
|
||||
@ -191,3 +192,53 @@ describe('上限常量', () => {
|
||||
expect(MAX_DATA_URL_BYTES).toBeLessThan(5_000_000 / 2);
|
||||
});
|
||||
});
|
||||
|
||||
describe('按账号分键(★ 这条修的是"新账号继承上一个账号的外观")', () => {
|
||||
it('键按账号区分,互不串味', () => {
|
||||
// pi 2026-09-14 复核时点名:全局键会让"切到服务端没有记录的账号"时,
|
||||
// saved=false 分支把**上一个账号的外观**推上去(而且写进了服务端)。
|
||||
expect(storageKey('acct-a')).not.toBe(storageKey('acct-b'));
|
||||
expect(storageKey('acct-a')).toBe('agentmail.background.acct-a');
|
||||
// 未登录/首屏(取不到账号)时用匿名键 —— 但**不能退回全局键**,那正是要修的东西
|
||||
expect(storageKey('')).toBe('agentmail.background.anonymous');
|
||||
expect(storageKey('')).not.toBe('agentmail.background');
|
||||
});
|
||||
|
||||
it('两个账号的缓存互相看不见', () => {
|
||||
localStorage.setItem(storageKey('acct-a'), JSON.stringify({ ...DEFAULT_BACKGROUND, dim: 30, presetId: 'sand' }));
|
||||
localStorage.setItem(storageKey('acct-b'), JSON.stringify({ ...DEFAULT_BACKGROUND, dim: 5, presetId: 'ink' }));
|
||||
expect(readStored('acct-a').presetId).toBe('sand');
|
||||
expect(readStored('acct-a').dim).toBe(30);
|
||||
expect(readStored('acct-b').presetId).toBe('ink');
|
||||
expect(readStored('acct-b').dim).toBe(5);
|
||||
});
|
||||
|
||||
it('没有本账号缓存时,旧全局键被迁移一次并立刻删除(不留"继承源")', () => {
|
||||
// 迁移的意义:老用户升级后观感不凭空消失;删除的意义:下一个账号不会再从它继承
|
||||
localStorage.clear();
|
||||
localStorage.setItem(LEGACY_STORAGE_KEY, JSON.stringify({ ...DEFAULT_BACKGROUND, dim: 42, presetId: 'dusk' }));
|
||||
const first = readStored('acct-a');
|
||||
expect(first.dim).toBe(42);
|
||||
expect(first.presetId).toBe('dusk');
|
||||
expect(localStorage.getItem(LEGACY_STORAGE_KEY)).toBeNull();
|
||||
expect(JSON.parse(localStorage.getItem(storageKey('acct-a'))!).dim).toBe(42);
|
||||
// 第二个账号拿不到那份旧值,得到的是默认外观(不是继承来的)
|
||||
expect(readStored('acct-b')).toEqual(DEFAULT_BACKGROUND);
|
||||
});
|
||||
|
||||
it('未登录时不迁移(旧值不能送给一个还不知道是谁的账号)', () => {
|
||||
localStorage.clear();
|
||||
localStorage.setItem(LEGACY_STORAGE_KEY, JSON.stringify({ ...DEFAULT_BACKGROUND, dim: 42 }));
|
||||
expect(readStored('')).toEqual(DEFAULT_BACKGROUND);
|
||||
expect(localStorage.getItem(LEGACY_STORAGE_KEY)).not.toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
describe('默认值 = 服务端契约', () => {
|
||||
it('默认压暗/模糊与服务端 DefaultAppearance 的 12/4 一致', () => {
|
||||
// 权威值在服务端(server/internal/models/models.go 的 DefaultAppearance);
|
||||
// 跨端那条判据会直接去 Go 源码里读这两个数比对,这里守住"本文件不再自写一套"
|
||||
expect(DEFAULT_BACKGROUND.dim).toBe(12);
|
||||
expect(DEFAULT_BACKGROUND.blur).toBe(4);
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user