Skip to content
源码版本47f9438 (dsh@0.1)

Web UI 与插件通信

Web UI 与插件通过三层机制交互:

  1. CordisDynamicPort RPC seam:inventory/stop/remove 三个方法,刻意抽离 React 层以便测试
  2. HostObservable 反应式数据源:inventory/activeRuns/runErrors/renderFailures/loaded
  3. Slot 系统:tool.view.cordis keyed slot 让动态 Client Package 在 Run 卡片里渲染业务 UI

CordisPanel 是 frame-wide 入口,用 createCordisInventory 单飞读 + 重连 reset 处理 wire 不可靠。

CordisDynamicPort:RPC seam

packages/extensions/ui-cordis/src/client/dynamic-port.ts:14-24interface CordisDynamicPort:
ts
interface CordisDynamicPort {
  stop      // 停止 run
  remove    // 移除 plugin
  inventory // 读 inventory
}

刻意抽离 React 层以便测试——UI 逻辑不直接耦合 RPC 实现。

createCordisInventory:单飞读 + 重连

packages/extensions/ui-cordis/src/client/inventory.ts:54-113createCordisInventory(port, onError):
  • listeners Set + generation 计数做单飞读
  • reset() 让重连丢弃旧 in-flight 结果——避免把旧 host 的 rows 发布到新连接上
packages/extensions/ui-cordis/src/client/inventory.ts:25-46 的类型:
ts
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:
ts
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-72CordisPanelFace 接口:
  • 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-120export function CordisPanel({...}):
tsx
function CordisPanel({...}) {
  // 用 useInventory / useActiveRuns / useRunErrors / useLoaded
  //    / useRenderFailures / useSessions 六个 hook 拉反应式数据
  // selected / pending / actionErrors 局部状态
}

visiblePanelStatus:阻塞型优先

packages/extensions/ui-cordis/src/client/CordisPanel.tsx:61-81visiblePanelStatus(view, selectedPackageId, loaded):
  • awaiting-approval 优先
  • failed 次之
  • 无 activeRun 则 idle
  • 否则委托 cordisVisibleStatus(...)

保证阻塞型(待审批)行排在最前。

UI 状态分两层:静态 inventory(rows from host registry)与动态 activity(activeRuns/runErrors from 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-140reload(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。

接下来读什么