结论
shared UI 的机器门禁目前只禁止 hubClient 的 runtime value import,允许 import type { HubClient }。但 useWorkbenchProjectsRoute 仍直接接受具体 HubClient,并在 shared hook 内执行:
list/create/update workspace project API;
pagination cursor 管理;
loading/error/saving 状态;
parent-managed props 与 internal Hub state 双模式切换;
mock data fallback。
这虽然没有 runtime import,却仍然把 presentation/workbench 层绑定到一个具体 transport client 的完整方法集合。类型层耦合会迫使 shared route 随 HubClient 扩张,也让 Web/Desktop 的数据 ownership 不明确。
此外 loadMore 对异常使用空 catch,错误完全不可见,滚动 sentinel 会在下次触发时继续重试,用户和监控均不知道分页失败。
实证位置
app/shared/src/workbench/useWorkbenchProjectsRoute.ts
hubClient?: HubClient;
shared hook 直接调用 listWorkspaceProjects/createWorkspaceProject/updateWorkspaceProject;
projects 存在时由 parent 管理,否则内部管理;
load-more error 被静默吞掉。
scripts/verify/verify-shared-ui-hubclient.ps1
当前 policy 只禁止 value import,并明确允许 type-only concrete HubClient。
[P2] Workbench 合同切片:用领域 assembler 收口项目/Agent/市场 prop-bag #1528 正在处理 Workbench domain assembler,本问题应与其设计对齐。
风险
shared UI 表面 transport-neutral,实际对具体 HubClient API 形状耦合;
同一 route 同时支持 controlled data 与 internal fetching,ownership 难以推断;
Web/Desktop 测试可能走不同分支,造成跨端行为漂移;
concrete client 继续向 Workbench props 扩散;
silent pagination error 形成不可观测的半加载状态;
mock/real/controlled 三套数据路径增加组合爆炸。
目标设计
shared Workbench 只消费窄领域 port/controller,不认识具体 HubClient:
interface ProjectsController {
state : {
items : ProjectInfo [ ] ;
activeId : string | null ;
loading : boolean ;
loadingMore : boolean ;
saving : boolean ;
error ?: DomainError ;
hasMore : boolean ;
} ;
select ( id : string ) : void ;
create ( draft : ProjectDraft ) : Promise < ProjectInfo > ;
update ( id : string , draft : ProjectDraft ) : Promise < ProjectInfo > ;
loadMore ( ) : Promise < void > ;
retry ( ) : Promise < void > ;
}
Web/Desktop composition root 使用 HubClient 实现该 controller;
shared hook 只处理视图选择、filter/tab/preview 等纯 UI 状态;
data fetching、pagination、retry、cache ownership 位于平台/domain adapter;
一个 consumer 只能选择一个 ownership mode,不再运行时猜测 projects ?? hubClient ?? mock;
demo/mock 通过显式 DemoProjectsController 注入。
#1528 的 projects assembler 不应只是把 hubClient 塞进 projectsController 后原逻辑不变。
推荐顺序:
[P2] Workbench 合同切片:用领域 assembler 收口项目/Agent/市场 prop-bag #1528 先建立 typed projects model/controller 边界;
本 Issue 删除 concrete HubClient fallback 和双 ownership;
更新 shared boundary verifier,禁止 shared UI 类型签名引用 concrete HubClient,仅允许领域 port。
若 #1528 尚未开工,可将本问题作为其 projects slice 的强制验收项,但 Issue 保持独立用于防止被“props 数下降”掩盖。
必须测试
Web 与 Desktop 使用相同 projects controller contract suite;
initial load / create / update / load more / retry / error;
loadMore 失败在 UI 状态和 telemetry 中可见;
不重复追加相同 page;
cursor 变化与 stale response 处理;
unmount/cancel 后不写 stale state;
demo mode 显式注入,不会在 real mode 泄漏 mock;
shared Workbench source 无 concrete HubClient type/value reference。
禁止
建一个 type ProjectsController = Pick<HubClient, ...> 伪装解耦;
把 HubClient 放进 React Context 后继续由 shared 直接调用;
长期保留旧 hubClient prop 与新 controller 双入口;
继续静默吞掉 pagination error;
用 Record<string, unknown> 代替领域合同。
完成条件
shared Workbench 不引用具体 HubClient;
Projects 数据只有一个明确 owner;
Web/Desktop adapter 通过同一 contract tests;
pagination failure 可见、可重试、可观测;
verifier 能阻止 concrete transport type 再次进入 shared presentation。
结论
shared UI 的机器门禁目前只禁止
hubClient的 runtime value import,允许import type { HubClient }。但useWorkbenchProjectsRoute仍直接接受具体HubClient,并在 shared hook 内执行:这虽然没有 runtime import,却仍然把 presentation/workbench 层绑定到一个具体 transport client 的完整方法集合。类型层耦合会迫使 shared route 随 HubClient 扩张,也让 Web/Desktop 的数据 ownership 不明确。
此外
loadMore对异常使用空catch,错误完全不可见,滚动 sentinel 会在下次触发时继续重试,用户和监控均不知道分页失败。实证位置
app/shared/src/workbench/useWorkbenchProjectsRoute.tshubClient?: HubClient;listWorkspaceProjects/createWorkspaceProject/updateWorkspaceProject;projects存在时由 parent 管理,否则内部管理;scripts/verify/verify-shared-ui-hubclient.ps1风险
目标设计
shared Workbench 只消费窄领域 port/controller,不认识具体 HubClient:
projects ?? hubClient ?? mock;与 #1528 的关系
#1528 的 projects assembler 不应只是把
hubClient塞进projectsController后原逻辑不变。推荐顺序:
HubClient,仅允许领域 port。若 #1528 尚未开工,可将本问题作为其 projects slice 的强制验收项,但 Issue 保持独立用于防止被“props 数下降”掩盖。
必须测试
禁止
type ProjectsController = Pick<HubClient, ...>伪装解耦;hubClientprop 与新 controller 双入口;Record<string, unknown>代替领域合同。完成条件