第 13 章:Frontend Core、状态与缓存
13.1 共享的是应用内核,不是所有页面
前端分层不是简单的 Component Library:
- packages/ui 提供无业务含义的视觉原语;
- packages/core 提供 API、Schema、Query、Mutation、Store、Realtime 与平台抽象;
- packages/views 组装业务页面;
- Web/Desktop Host 提供路由、存储、认证和系统能力。
依赖方向是 views → core + ui;UI 不依赖 Core,Views 也不应直接绑死 Next Router 或 Electron IPC。
13.2 CoreProvider 是组合根
platform/core-provider.tsx 在第一次渲染时创建:
- ApiClient;
- Auth Store;
- Chat Store;
- QueryClient;
- WebSocket Provider;
- I18n Provider;
- Storage 与 Platform Adapter;
- Client Usage Reporter;
- Freeze Watchdog。
Api、Auth 和 Chat 使用模块级 Singleton。注释明确说明它们只在首次渲染初始化,HMR 也保留同一个实例。
因此 Host 不能在运行中随意换 apiBaseUrl 或 Storage Adapter;这类参数是启动配置,不是普通 React Prop。
13.3 Provider 嵌套顺序
简化后的树是:
顺序有语义:
- Query 必须先于 Realtime Cache Updater;
- Auth 初始化后 WS 才知道凭据;
- I18n 在最外层避免服务端与客户端 Hydration 文案不一致;
- Desktop 自己报告 Runtime 状态,因此共享 Reporter 会避开 Desktop,防两个写者。
13.4 ApiClient 的职责
api/client.ts 不只是 Fetch 包装:
- 保存 Base URL 和可选 Token;
- 选择 Cookie Auth 或 Bearer Token;
- 自动加 Workspace Slug;
- 加 Client Platform、OS、Version、Install ID 等身份头;
- 生成 Request ID;
- 统一解析错误;
- 在 401 时清理本地 Token;
- 对关键响应执行 Zod 校验。
它是 Client 与 Go API 的契约边界。页面组件不应手写一套 Headers 或自行解释非 2xx Body。
13.5 Workspace 是 URL 驱动的 Singleton
CoreProvider 注释明确:当前 Workspace 不从 Storage 在 Boot 时恢复,而由 [workspaceSlug] 路由解析后调用 setCurrentWorkspace(slug, wsId)。
ApiClient 再从 workspace-storage.ts 读取当前 Slug,写入 X-Workspace-Slug。
这个设计让:
- 深链接决定当前工作区;
- 浏览器刷新保持语义;
- Desktop Tab 也能按 Route 还原;
- Storage 不会与 URL 争夺事实源。
但 Singleton 意味着同一个 Renderer 中必须有唯一“当前 Workspace”。Desktop 若同时显示多 Workspace,需要 Tab/Window 边界协调,不能让多个页面互相抢全局值。
13.6 API Response Compatibility
api/schema.ts 的 parseWithFallback 使用 Zod safeParse:
- 成功就返回解析结果;
- 失败记录 Endpoint、Issues 与收到的数据;
- 不抛异常;
- 返回调用方提供的安全 Fallback。
目标是把 Server/Client 版本漂移从白屏事故降为“页面可渲染但部分数据缺失”。
13.7 Schema 为什么故意宽松
客户端 TypeScript 类型可以很严格,Wire Schema 却常对枚举使用字符串而非封闭 Union。原因是滚动升级时新 Server 可能先发一个新状态。
正确处理是:
- Schema 接受未知字符串;
- UI Switch 有 Default;
- 可选字段用 Optional Chaining;
- 关键对象校验失败时返回显式 Empty/Fallback;
- 日志保留可定位的 Endpoint。
若 Zod 和 TS 都把枚举封死,新状态会让旧客户端整页降级。
13.8 Fallback 不是吞错
每个 Endpoint 都应有语义合理的 Fallback:
- 列表 → 空列表;
- 分页 → 空 Items + 合法分页元数据;
- 详情 → 受控 Empty Object 或 Unreadable 状态;
- Mutation Response → 不能伪造成功对象。
日志必须包含 Endpoint,否则大量 Schema Warning 无法定位。安全降级也不能把权限失败假装成“没有数据”;HTTP 状态仍由 ApiClient 处理。
13.9 React Query 管 Server State
query-client.ts 默认:
staleTime: Infinity;gcTime: 10 分钟;- Window Focus 不自动 Refetch;
- Reconnect 自动 Refetch;
- Query Retry 一次;
- Mutation 不自动 Retry。
这个配置与第 12 章的实时模型配套:在线变化靠 WebSocket Patch/Invalidate,断线恢复靠 Reconnect Refetch。
Mutation 默认不重试,是因为 Create Issue、Post Comment 等写操作未必具备客户端透明重放的幂等条件。
13.10 Query Key 是缓存 Schema
每个领域模块定义 Key Factory,例如:
issueKeys;chatKeys;agentTasksKeys;workspaceKeys;inboxKeys。
Query Key 必须包含影响响应的全部维度:
- Workspace ID;
- 对象 ID;
- Filter / Sort / Group;
- 当前用户相关 Scope;
- 分页参数。
漏掉一个维度会把两个逻辑查询错误地合并;随意改变 Key 形状则会让 Realtime Invalidation 静默失效。
13.11 Mutation 的三阶段
典型乐观 Mutation:
onMutate取消相关 Query 并保存 Snapshot;- 乐观 Patch 局部 Cache;
- 失败时 Rollback;
- 成功时用 Server Entity 对齐临时对象;
onSettledInvalidate 受影响的列表与详情。
不是每个动作都适合 Optimistic Update。权限、跨对象排序、Server 生成复杂副作用时,保守 Invalidate 更可靠。
13.12 Realtime Updater 是第二个写入口
realtime/use-realtime-sync.ts 同时处理:
- Issue、Label、Property;
- Comment、Reaction、Subscriber;
- Agent Task 与 Activity;
- Chat Message、Session、Pending Task;
- Inbox、Notification Preference;
- Workspace Membership;
- Runtime、Integration、Autopilot 等。
所以任何 Cache 设计都要同时考虑 Mutation 和 WS 两个写入口;只测试按钮点击后的 Cache 不够。
13.13 Patch 还是 Invalidate
选择原则:
| 条件 | 策略 |
|---|---|
| Payload 是完整实体,且有 ID/版本 | Patch |
| Payload 只表示“发生变化” | Invalidate |
| 聚合结果受权限或复杂过滤影响 | Invalidate |
| 高频文本流且已有序列号 | Merge/Patch 后最终 Invalidate |
| 删除对象 | 从已知 Cache 移除并失效相关聚合 |
Chat Done 会先把 Assistant Message 幂等加入缓存,再 Invalidate 消息查询,用低延迟和最终校验兼得。
13.14 权限敏感聚合不能乐观伪造
refetchPendingChatAggregate 的源码注释给出了典型安全案例:
- Workspace 的
task:*事件会发给所有成员; - Payload 不包含足够的 Creator/Agent Visibility;
- 如果直接把
has_pending=true写入本地聚合,会绕过 Server 的权限过滤。
因此只 Invalidate,重新调用按当前用户过滤的 Endpoint。缓存策略在这里是授权模型的一部分。
13.15 Zustand 管 Client State
Zustand Stores 保存不属于 Server 事实的状态:
- 列表视图、排序与展开;
- Draft、Composer、Quick Create;
- Selection、Modal;
- Chat 面板尺寸与最近上下文;
- Desktop Tab 与 Window Overlay;
- 快捷键偏好。
把这些塞进 React Query 会让每次 UI 操作看起来像 Server Data;反过来,把 Issue Entity 长期复制进 Zustand 会形成两个事实源。
13.16 持久化 Store 要 Workspace-aware
workspace-storage.ts 和 storage-cleanup.ts 为 Workspace 命名空间、退出和删除提供清理。
适合跨会话保存的有:
- 某工作区的 View Preference;
- 未提交 Draft;
- 最近访问;
- 面板布局。
不应长期保存:
- Access Control 结果;
- Runtime 在线状态;
- Task 终态;
- 完整 Server Entity。
这些会漂移,应该由 Query/Realtime 重建。
13.17 Auth Store 与 Token 模式
Web 常使用 HttpOnly Cookie;CLI/Desktop 场景可能使用 Token。CoreProvider 在非 Cookie 模式从 Storage 恢复 multica_token,并交给 ApiClient。
401 时:
- ApiClient 清理 Token;
- Auth Store 转为未认证;
- WS 断开;
- Host 决定跳 Login 或发起重认证。
不要在每个 Query Hook 单独处理 Login Redirect,否则并发 401 会造成导航风暴。
13.18 日志、分析与隐私
Core 使用命名 Logger,例如 api、api-schema、chat.ws。Analytics 还有:
- Benign Exception 过滤;
- Exception 去重;
- 敏感字段 Redaction。
API Schema 日志当前会携带 Received Data,扩展新敏感 Endpoint 时应检查脱敏;不能因为“只在 Warning”就打印 Token 或 Secret。
13.19 页面组件的正确边界
Views 应该:
- 调用 Core 导出的 Query/Mutation Hook;
- 读取 Store 的 View State;
- 用 UI 原语渲染;
- 用 Navigation Adapter 跳转。
Views 不应该:
- 直接 Fetch;
- 直接读 LocalStorage;
- 导入 Next Router 或 Electron IPC;
- 自建与 Core 重复的 Issue 类型;
- 手动监听 WebSocket;
- 把权限判断只写在按钮隐藏逻辑。
Server 仍是最终授权者,前端规则只是交互优化。
13.20 增加一个前端领域模块
推荐顺序:
- 在
types定义领域与 Wire 类型; - 在
api/schemas增加宽容 Schema 与 Fallback; - 在 ApiClient 增加 Endpoint;
- 建 Query Key Factory;
- 写 Query/Mutation Hooks;
- 判断哪些 UI 状态属于 Zustand;
- 接入 Realtime Event;
- 覆盖 Optimistic + WS 回声 + Rollback;
- 在 Views 组装;
- 由 Web/Desktop Host 接路由。
13.21 常见故障
| 症状 | 高概率原因 |
|---|---|
| 刷新后正常,实时不更新 | Event 没有 Invalidate 正确 Key |
| 切工作区闪现旧数据 | Key 或 Store 未做 Workspace Namespace |
| 新 Server 字段导致白屏 | Wire Schema 过严或直接 Parse Throw |
| 评论出现两次 | Optimistic 与 WS 回声没有按 ID 去重 |
| Desktop 正常、Web 异常 | Host Adapter 或 Cookie Auth 差异 |
| 一个成员看到别人的 Pending | 权限敏感聚合被乐观 Patch |
| HMR 后请求旧 Server | Singleton 初始化参数在运行时被错误假定可变 |
13.22 本章结论
Frontend Core 的核心价值不是复用代码量,而是集中维护四个跨端不变量:
- API 契约可降级;
- Server State 只有一个 Query 事实源;
- Client State 与 Workspace 隔离;
- Realtime 与 Mutation 最终收敛。
这些边界稳定后,Web 和 Desktop 才能共享业务界面而不共享所有宿主细节。