跳到主要内容

第 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.tsparseWithFallback 使用 Zod safeParse

  1. 成功就返回解析结果;
  2. 失败记录 Endpoint、Issues 与收到的数据;
  3. 不抛异常;
  4. 返回调用方提供的安全 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:

  1. onMutate 取消相关 Query 并保存 Snapshot;
  2. 乐观 Patch 局部 Cache;
  3. 失败时 Rollback;
  4. 成功时用 Server Entity 对齐临时对象;
  5. onSettled Invalidate 受影响的列表与详情。

不是每个动作都适合 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.tsstorage-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,例如 apiapi-schemachat.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 增加一个前端领域模块

推荐顺序:

  1. types 定义领域与 Wire 类型;
  2. api/schemas 增加宽容 Schema 与 Fallback;
  3. 在 ApiClient 增加 Endpoint;
  4. 建 Query Key Factory;
  5. 写 Query/Mutation Hooks;
  6. 判断哪些 UI 状态属于 Zustand;
  7. 接入 Realtime Event;
  8. 覆盖 Optimistic + WS 回声 + Rollback;
  9. 在 Views 组装;
  10. 由 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 后请求旧 ServerSingleton 初始化参数在运行时被错误假定可变

13.22 本章结论

Frontend Core 的核心价值不是复用代码量,而是集中维护四个跨端不变量:

  • API 契约可降级;
  • Server State 只有一个 Query 事实源;
  • Client State 与 Workspace 隔离;
  • Realtime 与 Mutation 最终收敛。

这些边界稳定后,Web 和 Desktop 才能共享业务界面而不共享所有宿主细节。