OpenHuman React Reuse Foundation 设计解析从重复实现到共享 UI 与状态管理接缝【免费下载链接】openhumanOpenHuman is an open source personal AI for Mac, Windows and Linux — local-first memory, agent orchestration, and deep research.项目地址: https://gitcode.com/GitHub_Trending/op/openhuman本设计文档docs/specs/2026-07-24-react-reuse-foundation-design.md状态Proposed定义了一套贯穿 OpenHuman 桌面应用前端app/src的复用治理方案在不做整体重设计、不改 RPC 契约的前提下沉淀一小批可复用的 UI 与状态管理接缝并按照「迁移而非重构」的方式把已被重复多次的实现收敛到共享所有者。读完本文你将掌握该方案的治理规则、四类计划切片、组件与异步 Hook 的契约边界、逐片迁移流程以及当前仓库中已经落地的ModalShell、ConfirmDialog与一系列共享 Hook 的真实实现细节。背景重复实现的代价与「复用治理规则」React 应用在功能迭代中很容易出现一类问题共享基础组件shared primitives确实存在但各功能模块并不一致地把重复行为提升到它们之上导致大量「外观相似、语义各写一份」的实现散落在页面与 feature 组件中。该方案为此确立了唯一的治理规则governing rule当第二个实现重复了同样的语义时要么复用或抽取出共享所有者要么书面记录两个实现必须分离的理由。仅靠样式相似不足以构成抽象的理由。换句话说抽象的判据是语义重复semantics而非样式相似styling。这与仓库中现有原语的设计取向一致——例如 Input.tsx 通过cnclsxtailwind-merge组合类名让调用方可以覆盖布局但无法破坏其核心视觉契约。目标与非目标目标移除至少重复两次的完全一致或近似一致的 React 实现优先采纳既有原语而非新增原语为共享行为明确所有权层级全局 UI、设置、Agent World、flows、或 feature 内部 helper在不改变用户可见行为的前提下提升可访问性与一致性在迁移消费方之前先在共享边界上补充回归测试每个独立验证的切片以**原子提交atomic commit**落地。非目标对应用做整体重设计或重排版引入 schema 驱动的表单框架建立横跨无关领域的统一状态模型把 drawer、阻塞 gate、sheet 与普通 dialog 合并成一个组件仅为降低行数而重写每个大组件改变 RPC 契约、授权语义或持久化格式。非目标中的最后一条尤其重要Rust 领域、控制器 schema、JSON-RPC 方法与传输层保持不变因此该方案是纯前端、纯 React 的改动不需要新增 Rust/JSON-RPC 实现或新的 E2E 场景。设计原则原则一提升到「最窄的稳定所有者」所有权ownership是这套方案的核心概念全局展示原语归属app/src/components/ui领域词汇与状态映射留在领域内。例如全局Badge拥有形状与色调tone而JobStatusBadge拥有从任务状态到色调的映射——前者是通用表现后者是领域语义仅 Agent World 使用的展示组件归属app/src/agentworld/componentsAgent World 的数据 Hook 归属app/src/agentworld/hooks按设计文档规划该目录在当前仓库中尚属新增目标尚未存在属于计划中的切片落点flow-run 查询协调归属app/src/hooks因为它同时被页面与 feature 组件消费。这种「分层放置」确保了抽象不会过早跨领域一个组件只能因为被多个消费方需要而上移不能因为「看起来通用」而上移。原则二保留领域语义抽取应该整合机制mechanics而不是抹除含义meaning审批卡片approval cards可以共享布局与请求生命周期但 tool、flow、gate 的授权动作仍然彼此区分加载与错误原语通过变体variants表达差异而不是把警告、校验消息与致命错误都塞进同一个状态模型。仓库中已落地的 ApprovalDecisionCard.tsx 正是这一原则的体现它通过actions: ApprovalDecisionAction[]让调用方传入领域专属的动作描述符id、label、busyLabel、variant、tone、title卡片本身只负责审批展示与动作渲染的共享机制动作语义完全由调用方决定。原则三组合优于配置引擎共享组件只接收普通的 React 内容与小型语义 props。校验、RPC 调用与领域专属渲染仍然由 feature 代码负责——不引入 schema 驱动、不建立可配置化引擎。这保证了抽象是「薄」的它只封装机制不吞掉领域逻辑。计划切片一采纳并强化既有 UI 原语第一个切片不是从零造新组件而是在既有原语上按需扩展只对「当前消费方需要且稳定」的能力进行增强原语计划增强点ModalShell可选 footer、关闭策略close policy、内容/布局插槽ErrorBanneralert 语义、React 内容、尺寸、可选 actionInputinvalid 与 monospace 表现供原始 input 重复处收敛ChipTabs逐项可访问性关系per-item accessibility与紧凑表现Card仅在语义化列表/区块消费方需要时支持多态输出polymorphic output迁移顺序是「先精确匹配、后弹性变体」drawer、全屏 gate 与 consent overlay 保持独立不合并。ConfirmDialog从 Agent World 提升为全局原语计划把 Agent World 的确认对话框提升为 app/src/components/ui/ConfirmDialog.tsx。该组件在当前仓库中已经落地其契约与设计文档完全对应组合ModalShell支持自定义 body 内容body: ReactNode支持破坏性destructive确认按钮切到tonedanger与中性确认支持 busy 状态与 busy 标签busybusyLabelbusy 期间按钮禁用支持关闭禁用busy 时closePolicy{{ escape: false, backdrop: false, button: false }}真正不可关闭提供confirmTestId/cancelTestId供测试定位——其中confirmTestId保留了历史硬编码默认值confirm-dialog-confirm而cancelTestId刻意不设默认避免给全应用每个对话框凭空打上多余属性。值得一提的细节ConfirmDialog的三个标签确认/取消/进行中默认通过useT()走 i18n而非英文硬编码。其注释说明了缘由——此前默认参数里的英文常量会绕过i18n:react:check审计该审计只扫描 JSX 中的aria-label/placeholder/title/alt/label导致未显式传标签的对话框在非英文环境下仍渲染英文。ModalShell 的底层实现ModalShell.tsx 现由 RadixDialog驱动相较此前手写的 portal 实现带来了真实焦点陷阱focus trap、滚动锁定、aria-hidden以及可存活的焦点恢复closePolicyescape/backdrop/button三个布尔位通过preventDefault()映射到 Radix 的 escape/outside 事件而不是丢弃 handler因此{ escape: false, backdrop: false, button: false }是真正不可关闭的焦点恢复由组件自身负责而非依赖 Radix 的onCloseAutoFocus因为所有调用方都通过卸载组件而非翻转open来关闭对话框Radix 根本观察不到 open→false 转换若依赖它每次关闭焦点都会掉到body——源码注释明确标注这一点「verified against a bare Dialog.Root, not assumed」其余能力icon/subtitle头部插槽、footer、maxWidthClassName、contentClassName、panelClassName、labelledBy/describedBy、可指定 portal 目标的container以及刻意不设默认的testId。计划切片二提取 Agent World 复用接缝该切片为 Agent World 新增一组领域内共享构件StatusBlock语义色调、标题、可选 body、加载状态、可选 action用于替换当时的八个重复副本useMyAgentId解析 Solana 钱包身份显式区分 loading / disconnected / ready / error 四种状态小型表单原语FormField与FormActions负责 label、描述、错误关联error association与动作布局但不管理表单状态ExpandableResourceRow为 job 与 bounty 行提供 disclosure 机制与可访问性。Skeleton 原子组件与 live-stream 指示器仅作为 follow-up——只有初始迁移证明其几何与语义稳定后才推进。计划切片三收敛共享 Hook 与查询协调设计文档规划了若干窄范围narrowly scoped的 Hook。这些 Hook 在当前仓库的 app/src/hooks 中大部分已经落地并配套了测试可以作为切片的实现证据Hook职责useFlowRunsQuery初始加载、最新请求保护、静默刷新、flow-run 实时刷新共享 pending-approvals 数据源 selector hooks列表与详情视图不再各自独立轮询同一端点useLatestAsync最新请求与卸载保护不规定调用方如何存储/合并返回数据useDebouncedValue搜索输入的定时器清理useClipboardFeedback复制状态、失败状态、定时器替换、卸载清理useDismissLayer组合既有useEscapeKey显式控制外部指针行为useFlowRunsQuery一次竞态安全的查询生命周期useFlowRunsQuery.ts 展示了「race-safe query lifecycle」的实现手法scope区分{ kind: flow; flowId }与{ kind: all }enabled控制是否可发请求通过多个 generation 计数器requestGenerationRef、latestForegroundGenerationRef、latestSilentGenerationRef、publishedGenerationRef实现最新请求保护响应返回时若generation ! latestForegroundGenerationRef.current或已被更新的已发布代数覆盖则直接丢弃防止旧响应覆盖新状态refresh()走前台加载态refreshSilently()静默更新——静默刷新结果不会覆盖更新的前台请求反之亦然卸载清理unmount时递增所有 generation杜绝卸载后 setState错误仅做归一化字符串不打印原始 payload 或用户内容对应文档「must not log raw payloads」的契约要求。useLatestAsync最小化的最新请求守卫useLatestAsync.ts 提供{ begin, isLatest, invalidate }三元组begin()递增代数并返回当前代数isLatest(gen)判断「已挂载且该代数仍是最新」invalidate()主动作废卸载时自动递增代数。它刻意不规定调用方如何存储或合并返回数据——把存储策略留给调用方保持了 Hook 的窄范围。useDebouncedValue 与 useClipboardFeedbackuseDebouncedValue.ts对delayMs做Math.max(0, …)归一化每次值变化重置setTimeout卸载时clearTimeout保证无泄漏useClipboardFeedback.ts状态机为idle | copied | errorresetAfterMs默认 2000ms用operationIdRef保证只有最新一次复制操作能更新状态用resetTimerRef保证定时器替换与卸载清理writeText可注入测试与敏感场景敏感剪贴板流程可另行 opt-in。useDismissLayer组合而非复制 Escape 逻辑useDismissLayer.ts 组合了既有的 useEscapeKey.ts并显式暴露dismissOnEscape与dismissOnOutsidePointer两个开关外部指针判定基于layerRef的contains(target)通过onPointerDownCapture捕获阶段处理避免与内部交互冲突。文档同时明确socket 订阅收敛要推迟——只有先确定 canonical-versus-legacy 事件别名行为并补上测试才推进该部分。计划切片四收敛重复的领域展示与解析ApprovalDecisionCard共享审批展示与动作渲染调用方提供领域专属动作描述符。当前仓库中的实现见 app/src/components/approvals/ApprovalDecisionCard.tsx支持density: default | compact与busyActionIdflow-run 状态展示共享色调与圆点原语但不合并彼此不同的状态联合类型status unionsworkflow-proposal 解析器收敛为 API 映射层使用的唯一规范实现。此外HTTP JSON-RPC 传输层收敛被刻意列为后续的非 React 切片在 React 侧改动干净收尾后再单独规划。组件契约语义 Props 与可访问性基线共享 UI 组件的两个硬性契约使用语义 props而非任意 Tailwind 颜色类转发可访问性属性accessibility attributes并允许调用方通过className做布局级覆盖但不能替换核心视觉契约cn的tailwind-merge语义即为此服务Dialog/drawer 原语必须提供可访问名称accessible name、由显式策略控制的 Escape 与 backdrop 行为、焦点放置与恢复、允许关闭时的真实关闭控件、需要时的 portal 渲染、卸载后不再执行任何动作。异步 Hook 的契约在状态差异影响渲染时使用可辨识联合discriminated state必须防止过期请求覆盖新状态不得记录原始 payload 或用户编写的错误内容。迁移策略五步原子切片每个切片严格遵循固定顺序添加或扩展共享边界并配套聚焦测试迁移两个代表性消费方运行相关测试与 TypeScript 校验pnpm typecheck迁移剩余精确匹配项再次运行相关测试并原子提交该切片。两个硬性约束行为实质不同的消费方保持不动并登记为有意例外任何迁移不得把行为变更与复用重构混在一起一个提交只做一件事。测试与质量门槛共享组件按适用面覆盖渲染、交互、键盘、ARIA、busy 状态、焦点恢复测试Hook 按适用面覆盖成功、错误、卸载、过期响应、定时器清理、并发消费者测试既有消费者测试保持绿色被迁移行为此前无覆盖时补充定向测试每个切片运行相关 Vitest 文件 pnpm typecheck完成后的分支运行pnpm lint、pnpm test、pnpm buildRust 领域、控制器 schema、JSON-RPC 方法、传输行为均不变因此无需新增 Rust/JSON-RPC 实现或新 E2E 场景既有消费者流程的 E2E 覆盖继续适用。当前仓库中的落地证据与这些门槛一一对应app/src/hooks/__tests__下已有useFlowRunsQuery.test.ts、useLatestAsync.test.ts、useDebouncedValue.test.ts、useClipboardFeedback.test.ts、useDismissLayer.test.tsapp/src/components/ui下已有ModalShell.test.tsx、ConfirmDialog.test.tsx、Input.test.tsx、Card.test.tsx、Badge.test.tsx、Dialog.centering.test.tsx等app/src/components/layout/ChipTabs.test.tsx与app/src/components/approvals/ApprovalDecisionCard.test.tsx则覆盖了领域侧迁移产物。交付顺序与成功标准交付按八步推进每项独立可评审并以atomic-commit提交全局ConfirmDialog与简单ModalShell迁移既有 loading/error 原语采纳Agent WorldStatusBlockAgent WorlduseMyAgentIdAgent World 表单与可展开行原语flow-run 查询与 pending-approval 协调clipboard、debounce、latest-async、dismiss-layer Hook审批展示、flow 状态展示与 workflow 解析器。最终成功标准Agent World 的八个状态块共享一个实现重复的普通确认使用全局对话框原语Agent World 钱包身份解析收敛为一个 Hook、一个状态契约flow-run 表面共享一个竞态安全的查询生命周期并发 flow-run 表面共享 pending-approval 轮询既有的精确 loading、error、input、chip 重复使用既定原语被迁移的 feature 不丢失行为、可访问性或测试覆盖前端 lint、单元测试、类型检查与生产构建全部通过。结语一次可度量、可回滚的复用治理这份设计文档的价值在于它的克制不为复用而造框架、不合并语义不同的状态、不改任何后端契约而是用「语义重复 最小稳定所有者 组合优于配置 原子切片迁移」四根支柱把前端重复实现收敛为一批薄而有契约的共享接缝。对于任何正在膨胀的 React 代码库这套「先定规则、再定所有权、再逐片迁移、每片带测试提交」的路线本身就是一份可直接借鉴的工程模板。【免费下载链接】openhumanOpenHuman is an open source personal AI for Mac, Windows and Linux — local-first memory, agent orchestration, and deep research.项目地址: https://gitcode.com/GitHub_Trending/op/openhuman创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考