Web UI 与插件通信
Web UI 与插件通过三层机制交互:
CordisDynamicPortRPC seam:inventory/stop/remove 三个方法,刻意抽离 React 层以便测试HostObservable反应式数据源:inventory/activeRuns/runErrors/renderFailures/loaded- Slot 系统:
tool.view.cordiskeyed slot 让动态 Client Package 在 Run 卡片里渲染业务 UI
CordisPanel 是 frame-wide 入口,用 createCordisInventory 单飞读 + 重连 reset 处理 wire 不可靠。
CordisDynamicPort:RPC seam
packages/extensions/ui-cordis/src/client/dynamic-port.ts:14-24 的 interface CordisDynamicPort:interface CordisDynamicPort {
stop // 停止 run
remove // 移除 plugin
inventory // 读 inventory
}刻意抽离 React 层以便测试——UI 逻辑不直接耦合 RPC 实现。
createCordisInventory:单飞读 + 重连
packages/extensions/ui-cordis/src/client/inventory.ts:54-113 的 createCordisInventory(port, onError):- 用
listenersSet +generation计数做单飞读 reset()让重连丢弃旧 in-flight 结果——避免把旧 host 的 rows 发布到新连接上
packages/extensions/ui-cordis/src/client/inventory.ts:25-46 的类型:CordisInventorySnapshot { rows, removed, read, error }
CordisInventory { refresh, retire, reset }read: false 时面板显示 loading 而非 empty state。
单飞读 + generation 是重连关键:旧连接的 in-flight promise 即使 resolve 也因
issued !== generation被丢弃,新连接 reset 时generation += 1让旧读作废。
removed: Set<CordisDynamicPluginId>保留显式删除的 ID 用于历史卡片——避免面板把刚删的 plugin 立刻从 UI 抹掉造成闪烁。
Slot 系统:UI 的核心扩展点
tool.view.cordis keyed slot
packages/extensions/ui-cordis/src/client/slots.ts:23-37:declare module '@deepseek-ai/dsh-client-ui-slots' {
interface SlotMap {
'tool.view.cordis': {
scope: 'session'
owner: CordisToolViewOwnerProps // pluginId/packageId/pluginRunId
}
}
}Dynamic Client 代码用 key: 'self' 注册,Guard 绑定到当前 Plugin/Package。
Slot 是 DSH UI 的核心扩展点:不是让 plugin 直接
return <Element>,而是要求先Slots.listSubTree查出契约再注册——保证 owner currency(pluginId/packageId/pluginRunId)传递,卸载时 SlotCore 单 owner unregister 清理。
CordisPanelFace
packages/extensions/ui-cordis/src/client/slots.ts:57-72 的 CordisPanelFace 接口:- hooks:inventory/activeRuns/runErrors/renderFailures/loaded
- 六个回调:
onApprove/onDecline/onRun/onStop/onRemove/onRefresh - 由 sidebar footer slot 注入
CordisPanel:frame-wide 入口
packages/extensions/ui-cordis/src/client/CordisPanel.tsx:106-120 的 export function CordisPanel({...}):function CordisPanel({...}) {
// 用 useInventory / useActiveRuns / useRunErrors / useLoaded
// / useRenderFailures / useSessions 六个 hook 拉反应式数据
// selected / pending / actionErrors 局部状态
}visiblePanelStatus:阻塞型优先
packages/extensions/ui-cordis/src/client/CordisPanel.tsx:61-81 的 visiblePanelStatus(view, selectedPackageId, loaded):awaiting-approval优先failed次之- 无 activeRun 则
idle - 否则委托
cordisVisibleStatus(...)
保证阻塞型(待审批)行排在最前。
UI 状态分两层:静态 inventory(
rowsfrom host registry)与动态 activity(activeRuns/runErrorsfrom Client runner),面板用RowView合并两者;selectedPackageIdOf优先取用户选择,退到 next/current/最后/活动版本。
事件类型
packages/extensions/ui-cordis/src/client/events.ts:6-18:类型 re-export(CordisDynamicPluginId/DynamicCordisInventoryRow/DynamicCordisRunRequest 等),通过 @deepseek-ai/dsh-api-remotes/client 把 Remote 装配的类型合并进 client 程序。浏览器侧热替换
packages/client/hmr/src/client/index.ts:104-140 的 reload(id):invalidate(id) → prefetch(id) → 拿 entry.fiber
→ registry.delete(runtime.callback) // registry-first
→ drain oldFiber.inertia
→ delete entry.fiber
→ removeOwnedStyles(id)
→ entry.refresh()
→ entry.fiber?.await()详细的 registry-first teardown 顺序与原因见 卸载与热重载。
动态端口(SSE)与静态端口(
/plugins/events):host 半 stat-poll bundle mtime → SSE 推rebuilt→ browser 半 EventSource 接 → 串行队列防交错 dispose/execute 损坏单 slot handoff。