fix(desktop): 状态镜像到宿主侧,修复随机端口导致重启丢数据

This commit is contained in:
2026-11-02 18:20:16 +08:00
parent f98246ef0c
commit a870f74875
9 changed files with 523 additions and 113 deletions
+92
View File
@@ -0,0 +1,92 @@
/**
* 宿主侧状态镜像(本地补丁)。
*
* 背景:桌面端宿主每次启动都带 `--port 0`(随机端口),页面来源随之变化
* (如 127.0.0.1:2080 → 127.0.0.1:8494)。浏览器按来源隔离 localStorage /
* IndexedDB,换端口就等于换一个空库,项目/布局/外观重启后全丢。
*
* 做法:把「项目 + 视图」这份 JSON 镜像到宿主侧文件(/api/worktable/state),
* 启动时若宿主侧比本地新,就用宿主侧恢复。localStorage 仍是第一落点(同步、快),
* 宿主文件是跨重启的兜底。
*/
const ENDPOINT = '/api/worktable/state'
export type HostStateParts = { projects?: unknown; view?: unknown }
/** 宿主镜像里可能带的写入时间戳(毫秒) */
export type HostState = HostStateParts & { updatedAt?: number }
/** 拉取宿主侧镜像;没有/不可用都返回 null(不抛错,不影响工作台本身) */
export async function loadHostState(): Promise<HostState | null> {
try {
const r = await fetch(ENDPOINT, { cache: 'no-store', headers: { accept: 'application/json' } })
if (!r.ok) return null
const j: any = await r.json()
return j && typeof j === 'object' && j.state && typeof j.state === 'object' ? (j.state as HostState) : null
} catch {
return null
}
}
/** 累积中的镜像内容:POST 只带最新值,避免每次编辑都发整包 */
let parts: HostStateParts = {}
let timer: any = null
let inflight = false
let dirty = false
/** 恢复完成前不写宿主,否则首屏的默认值会把宿主里的旧数据覆盖掉 */
let hydrated = false
function schedule() {
if (timer) return
timer = setTimeout(() => {
timer = null
void flush()
}, 400)
}
async function flush(): Promise<void> {
if (!hydrated || inflight) {
if (inflight) dirty = true
return
}
if (!parts.projects && !parts.view) return
inflight = true
try {
await fetch(ENDPOINT, {
method: 'POST',
headers: { 'content-type': 'application/json' },
body: JSON.stringify({ state: { ...parts, updatedAt: Date.now() } }),
keepalive: true,
})
} catch {
// 宿主不可用时静默:localStorage 仍是本次会话的真值
} finally {
inflight = false
if (dirty) {
dirty = false
schedule()
}
}
}
/** 记下要镜像的部分;400ms 内多次调用合并成一次 POST */
export function queueHostSave(patch: HostStateParts): void {
parts = { ...parts, ...patch }
if (!hydrated) return
schedule()
}
/** 首次恢复判定结束:之后再放行宿主写入 */
export function markHydrated(): void {
hydrated = true
}
/** 立即冲刷(页面隐藏/关闭前调用,避免丢掉最后一个 400ms 内的改动) */
export function flushHostSave(): void {
if (timer) {
clearTimeout(timer)
timer = null
}
void flush()
}