fix(appearance): 浏览器里同步从来没跑起来(三处叠加)+ 部署链加"前端不得比源码旧"闸门
用户说「webui 你也没改呢」。查证:**部署是活的**(本地产物 = 线上产物、CSS 里壁纸 修复的规则都在、入口 `Cache-Control: no-cache`、资源哈希+immutable)——是我新加的 "外观存服务端"那套在**浏览器**里根本没生效。沿途挖出三处叠加缺陷 + 一处部署链真空子: ## ① 路径写成绝对 `/api/v1/...`(双前缀 ⇒ 404) `resolveBase()` 解析出来的 base 已经含 `/api/v1`(默认就是它),既有调用者传的都是 `/me/mail/inbox` 这种**相对基地址**的形状。我写成 `/api/v1/me/appearance` ⇒ 实际请求 `/api/v1/api/v1/me/appearance` ⇒ 404。 **单测全绿却没抓住**:我只断言了方法、报文,没断言 URL。现在补了 URL 判据 (含"不得出现 /api/v1/api/v1"这条)。 ## ② 浏览器密码登录只有 cookie、没有 Bearer ⇒ `currentAuth()` 直接短路 `currentAuth()` 原先要求 token 非空,而密码登录只建 cookie 会话(桌面端粘贴用户密钥 才设 Bearer)⇒ WebUI 里 `pull/push` 从来没跑过。已放宽为"只要有 base",并补了两条 判据(cookie 会话也要能拉、能推)。 ("完全没有网关地址 ⇒ local-only"这条判据删掉了:`resolveBase()` 总有默认值, 那个状态到不了 —— 判据不量够不着的对象。) ## ③ 服务端"无记录"时拿默认值覆盖本地 首次启用同步时每个老用户都会中招:服务端回默认值(theme=system / bg=none), 客户端照着应用 ⇒ **用户已有的主题与本地壁纸被静默重置**。现在改为"以本地为准、 推上去认领",并补判据(含"有记录时以服务端为准"的反向对照)。 ## ④ 部署链真空子:dist 比源码旧也能"同步成功" 改完源码忘了 `vite build`,`redeploy-gateway.sh` 照样把旧 dist 打进二进制 —— 这正是 ①在线上一直没被发现的直接原因。现在部署脚本会比对 `src/**` 与 `dist/index.html` 的 mtime,旧了就 **FAIL** 并提示先 build。 ## 顺带:我自己在真实账号上留的测试数据 线上 E2E 时我把 `theme=dark/bg=preset(dusk)/dim=35` PUT 到了 **jianf** 这个真实账号 (应该用测试账号)。已删掉那条记录(接口现在回 `saved:false`),配合 ③ 的修复, 用户本地那份外观会被认领上去而不会被覆盖。 ## 验证 - 浏览器实测(自带无头 Chromium + 真实功能,非注入 CSS): `200 GET /api/v1/me/appearance` → `data-bg=on`、`dark=true`、本地缓存写入 ✓ - 三张对比图(自定义图片档 / 关背景 / 预设渐变)已随邮件发给用户 - 前端 253 条(含新增 URL 判据与 cookie 会话判据)、server 10 包、打包一致性全绿
This commit is contained in:
@ -39,8 +39,16 @@ export interface AppearanceSnapshot {
|
||||
bgBlur: number;
|
||||
}
|
||||
|
||||
export const APPEARANCE_PATH = '/api/v1/me/appearance';
|
||||
export const APPEARANCE_IMAGE_PATH = '/api/v1/me/appearance/image';
|
||||
/**
|
||||
* ⚠️ 路径是**相对基地址**的:`resolveBase()` 解析出来的 base 已经含 `/api/v1`
|
||||
* (默认就是它,见 api/config.ts),既有调用者传的都是 `/me/mail/inbox` 这种形状。
|
||||
*
|
||||
* 我第一版写成 `/api/v1/me/appearance` ⇒ 实际请求成了 `/api/v1/api/v1/me/appearance`
|
||||
* ⇒ 浏览器里这套同步**从来没生效过**(而单测全绿,因为只断言了方法与报文、没断言 URL)。
|
||||
* 现在测试里专门有一条断言请求路径。
|
||||
*/
|
||||
export const APPEARANCE_PATH = '/me/appearance';
|
||||
export const APPEARANCE_IMAGE_PATH = '/me/appearance/image';
|
||||
|
||||
const THEMES = new Set(['light', 'dark', 'system']);
|
||||
const KINDS = new Set(['none', 'preset', 'image']);
|
||||
@ -102,8 +110,10 @@ export interface AppearanceAuth {
|
||||
export async function pullAppearance(
|
||||
auth: AppearanceAuth,
|
||||
fetchImpl: typeof fetchWithAuth = fetchWithAuth
|
||||
): Promise<{ snapshot: AppearanceSnapshot; imageDataUrl: string } | null> {
|
||||
if (!auth?.base || !auth?.token) return null;
|
||||
): Promise<{ snapshot: AppearanceSnapshot; imageDataUrl: string; saved: boolean } | null> {
|
||||
// 只要求 base:**浏览器密码登录只建 cookie 会话、没有 Bearer 令牌**,
|
||||
// 拿 token 当硬门槛会让这条路直接短路(实测:登录成功但外观不生效、且控制台 401)。
|
||||
if (!auth?.base) return null;
|
||||
try {
|
||||
const res = await fetchImpl(auth, APPEARANCE_PATH);
|
||||
if (!res.ok) return null;
|
||||
@ -119,7 +129,10 @@ export async function pullAppearance(
|
||||
imageDataUrl = await blobToDataUrl(blob);
|
||||
}
|
||||
}
|
||||
return { snapshot, imageDataUrl };
|
||||
// `saved` 必须回传:服务端"没有记录"时给的是默认值,而拿默认值去覆盖本地
|
||||
// 等于把用户已有的外观(尤其是本地缓存的壁纸)抹掉 —— 首次启用这套同步时
|
||||
// 每个老用户都会中招。调用方据此改走"以本地为准、推上去"。
|
||||
return { snapshot, imageDataUrl, saved: body?.saved === true };
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
@ -131,7 +144,7 @@ export async function pushAppearance(
|
||||
payload: Record<string, unknown>,
|
||||
fetchImpl: typeof fetchWithAuth = fetchWithAuth
|
||||
): Promise<boolean> {
|
||||
if (!auth?.base || !auth?.token) return false;
|
||||
if (!auth?.base) return false;
|
||||
try {
|
||||
const res = await fetchImpl(auth, APPEARANCE_PATH, {
|
||||
method: 'PUT',
|
||||
@ -150,7 +163,7 @@ export async function uploadAppearanceImage(
|
||||
imageDataUrl: string,
|
||||
fetchImpl: typeof fetchWithAuth = fetchWithAuth
|
||||
): Promise<boolean> {
|
||||
if (!auth?.base || !auth?.token || !imageDataUrl) return false;
|
||||
if (!auth?.base || !imageDataUrl) return false;
|
||||
try {
|
||||
const blob = dataUrlToBlob(imageDataUrl);
|
||||
const form = new FormData();
|
||||
|
||||
Reference in New Issue
Block a user