6.4 KiB
Metapi 前端架构约定
这份约定把管理端的技术入口收敛成一条可检查的路径,作为新增页面和重构时 的维护者参考。它不改变现有页面能力、路由、协议或主题。
Canonical stack
pages/** → pages/<domain>/** → components/ui/** → Radix behavior primitives
└──────────→ index.css design tokens/classes
- 生产页面使用 React 18 + TypeScript + Vite,并通过
src/web/api.ts发起 认证 HTTP/SSE 请求。 src/web/components/ui/**是项目拥有的 UI 边界。只有这一层可以直接 import@radix-ui/*(以及其他交互原语库);页面和领域组件只接收 Metapi 的语义 props。CenteredModal、MobileDrawer、ModernSelect是兼容 facade。新代码优先 使用components/ui的 wrapper;旧导出在迁移完成前继续有效。- 浏览器 renderer 的 Radix modal overlay 已负责
RemoveScroll;components/ui只在 SSR/test/不完整 DOM fallback 使用引用计数 body lock,避免多个 owner 在卸载顺序不同步时留下页面滚动锁。 - 视觉来源是
src/web/index.css的 CSS variables 和语义 class。不要为单个 页面引入 CSS module、styled-components 或另一套 UI framework;复用布局时 先扩展已有 class/primitive。 @import "tailwindcss"只允许出现在src/web/index.css。当前它作为 Vite 的单一 utility/preflight 入口保留;页面不得再创建第二个样式 runtime。
异步与状态边界
src/web/api.ts负责认证、超时、错误提取、取消和 SSE 解析;页面只拥有 自己的 server state、URL intent 和领域 mutation。- 请求 effect 必须在 target 改变和卸载时 abort 或 stale-guard,并清理 timer、 listener、animation frame、portal 和 body-scroll lock。
- loading、empty、error、cancel 状态使用共享窄职责 primitive 或既有语义
class;用户可见文本通过
t()/tr()翻译。
维护检查
提交前运行:
npm run typecheck:web
npm run typecheck:web:test
npm run repo:drift-check
npm run build:web
git diff --check
repo:drift-check 会报告以下架构漂移:
- 页面/领域组件直接 import 第三方 UI 原语;
- 顶层 page 互相 import;以及
proxy-core、transformer 和整包响应读取等既有边界债务。
新的 wrapper 只能放入 src/web/components/ui/**,不能通过新增 allowlist
隐藏违规。若确有浏览器能力例外,应在拥有该能力的 helper 中记录原因、测试
和后续迁移边界。
迁移策略
优先迁移低耦合的 modal/drawer、异步按钮和反馈状态,再处理大型领域页面。
每个切片都应保留旧 props/class、桌面与移动断点、键盘行为和翻译,并以聚焦
测试证明 focus return、Escape/backdrop、body-scroll 多实例清理和 stale 请求
语义没有回归。不要为了“统一”机械替换所有历史 <button> 或业务卡片。
当前交付证据(2026-08-31)
本轮简化已经把以下高频路径接入同一套生命周期 owner:
Dashboard、ProxyLogs、Models和SearchModal使用src/web/components/useAsyncResource.ts;该 helper 负责AbortController、 stale identity、single-flight、silent refresh、timeout/abort 分类和卸载清理。Dashboard的跨域测速集中在pages/helpers/siteSpeedProbe.ts,固定 worker pool(目标并发不超过 4)、逐站 timeout 和done/timeout/error/aborted结果。 这是有意保留的 browser-onlyfetch(..., { mode: 'no-cors' })能力,不属于 认证管理 API。ModelTester的 SSE delta/raw event 通过 24ms batcher 提交;停止、完成、 abort 和卸载都会先 flush,再取消 reader/controller。批处理只改变 React commit 频率,不改变 cumulative delta 去重或 raw event 顺序。TokensPanel位于pages/tokens/,pages/Tokens.tsx只保留 legacy route redirect 和兼容导出;页面之间不再通过顶层 page 互引。
验证记录(本地 Node v22.23.1,包声明 Node >=25.0.0):
npm run build:web:2614 modules transformed;CSS136.83 kB(gzip22.01 kB),构建通过;npm test -- --run src/web:157 个文件、539 个测试通过,2 个跳过;npm test -- --run src/server:301 个文件、2,155 个测试通过,6 个跳过;npm test:480 个文件、2,777 个测试通过,1 个文件/8 个测试跳过;npm run typecheck、npm run build、npm run repo:drift-check和git diff --check通过;drift 结果为 0 violations / 0 tracked debt;- build 仍显示
vchart-vendor约 2.15 MB(gzip 约 587 kB)的 large-chunk warning。当前没有真实浏览器 below-fold/network trace 来证明拆分收益,故把 它记录为后续独立性能任务,不宣称本轮已经解决。
服务端对应边界
管理 route 只做输入解析、endpoint 选择和 HTTP 响应映射;账号验证诊断由
src/server/services/accountVerificationDiagnostics.ts 单一拥有,
downstreamPolicy 与 multipart helper 由 src/server/services/ 拥有,旧
routes/proxy/* 文件只保留兼容 re-export。proxy-core surface 不得重新引入
routes/proxy,transformer 继续保持协议纯净。
OneApi/Veloera 的整套继承统一、以及 completions/embeddings/images/search/ Gemini 的所有 proxy-log 写入迁移暂缓:真实差异包括 header、quota divisor、 check-in 文案、model error propagation、usage/billing 默认值和 await 时序。 只有补齐参数化协议回归后,才允许另建任务继续合并;“减少文件数”不是跳过这些 契约的理由。
维护者工作流
新增 UI 交互时按以下顺序检查:
- 先搜索
components/ui、现有 responsive primitive 和api.tsowner; - 在 wrapper/helper 层补行为测试,再迁移一个页面;
- 运行
npm run repo:drift-check,确认没有第三方 UI/style runtime 直引; - 记录请求并发、stale、timer/reader cleanup 和 chunk 证据;没有实测的收益 写成“待验证瓶颈”,不要用构建成功替代性能结论。
ModernSelect 是兼容 facade,当前仍使用项目自己的 listbox 实现;它不是
“所有交互都已迁移到 Radix Select”的信号。未来若替换内部行为,必须保持现有
options/value/onChange/placeholder 契约并新增键盘/controlled 回归测试。