宿主交互设计

设计 UI 组件与 downcity 主包之间的标准交互边界,而不是直接导入主包内部模块

宿主交互设计

这一页回答一个更关键的问题:

@downcity/ui 里的组件,应该怎样和 downcity 主包交互?

答案不是“组件里直接 import 主包内部模块”,而是把交互拆成三层:

  1. downcity 主包负责暴露稳定的网关能力
  2. 宿主应用负责把网关包装成适配器或 hook
  3. @downcity/ui 只负责展示和触发交互意图

这样做的原因很直接:

  • 不依赖 cli/city/* 内部路径,生产打包更稳定
  • 组件可以在 console-uihomepage、其他宿主里复用
  • 业务逻辑、运行时控制、视觉组件不会混成一层

不推荐的方式

下面这种方式不应该继续扩散:

import { someInternalRuntimeMethod } from "../../../cli/city/src/console/ui/xxx";

这会带来几个问题:

  • UI 包直接依赖主包源码路径
  • 打包产物无法保证对外可用
  • 主包一重构,组件就失效
  • 组件无法脱离当前 monorepo 复用

推荐的三层结构

cli/city
  └─ 提供 /api/ui/* 与 /api/dashboard/* 网关能力

console-ui
  └─ 封装 request / query / mutation / hook

@downcity/ui
  └─ 提供 Button / Card / Dialog / Sheet / Tabs 等纯展示原语

如果未来需要把“交互适配器”也对外复用,可以单独增加一个包:

@downcity/ui-bridge

但它不应该塞进 @downcity/ui

现有边界已经具备的基础

当前仓库里,其实已经有一条很好的边界:

  • cli/city/src/console/ui/ConsoleUIGateway.ts
  • cli/city/src/types/ConsoleUI.ts
  • products/console/src/lib/dashboard-api.ts
  • products/console/src/hooks/useConsoleDashboard.ts

这说明正确方向已经不是“组件直连主包源码”,而是:

  • 主包提供稳定 API
  • 前端通过适配层访问 API
  • 组件接收数据和回调

推荐组件原型

如果参考 teamprofile 那种“地图 + 详情 + 操作”的组织方式,在 Downcity 里不应该直接复制它的业务模拟逻辑,而应该抽象成一个适合控制台场景的组件:

AgentWorkbenchCard

这是一个“可选中、可查看状态、可触发操作”的工作区卡片。

它负责展示什么

  • agent 名称
  • 当前运行状态
  • 主模型
  • 渠道状态摘要
  • 最近更新时间
  • 主要操作入口

它不负责什么

  • 自己发请求
  • 自己决定 API 地址
  • 自己维护复杂业务状态机
  • 直接访问 downcity 主包源码

推荐的数据契约

export interface AgentWorkbenchSnapshot {
  id: string;
  name: string;
  projectRoot?: string;
  running: boolean;
  primaryModelId?: string;
  updatedAt?: string;
  baseUrl?: string;
  statusText?: string;
  channels: Array<{
    channel: string;
    linkState?: string;
    statusText?: string;
  }>;
}
export interface AgentWorkbenchActions {
  onSelect: (agentId: string) => void;
  onStart?: (agentId: string) => Promise<void> | void;
  onStop?: (agentId: string) => Promise<void> | void;
  onRestart?: (agentId: string) => Promise<void> | void;
  onOpenDetails?: (agentId: string) => void;
}
export interface AgentWorkbenchCardProps
  extends AgentWorkbenchActions {
  snapshot: AgentWorkbenchSnapshot;
  selected?: boolean;
  busy?: boolean;
}

关键点是:

  • 组件拿到的是 snapshot
  • 组件发出的是 intent
  • 真正的请求行为由宿主层实现

推荐的事件流

主包网关返回 agent 列表
  -> console-ui adapter 转成 snapshot
  -> AgentWorkbenchCard 渲染
  -> 用户点击 restart
  -> adapter 调用 /api/ui/agents/restart
  -> hook 刷新 agents / overview / services
  -> 组件收到新 snapshot 并更新

这和 teamprofile 最值得借鉴的地方一致:

  • 组件负责“可视化当前状态”
  • 容器负责“编排交互”
  • 领域层负责“真正执行命令”

推荐的宿主层实现

console-ui 中,应该继续把交互收敛在 query、mutation、hook 里,而不是下沉到 UI 原语。

function AgentWorkbenchCardContainer(props: { agentId: string }) {
  const dashboard = useConsoleDashboard();
  const snapshot = dashboard.agents.find((item) => item.id === props.agentId);

  if (!snapshot) return null;

  return (
    <AgentWorkbenchCard
      snapshot={{
        id: snapshot.id,
        name: snapshot.name,
        projectRoot: snapshot.projectRoot,
        running: Boolean(snapshot.running),
        primaryModelId: snapshot.primaryModelId,
        updatedAt: snapshot.updatedAt,
        baseUrl: snapshot.baseUrl,
        channels:
          snapshot.chatProfiles?.map((item) => ({
            channel: item.channel || "unknown",
            linkState: item.linkState,
            statusText: item.statusText,
          })) || [],
      }}
      selected={dashboard.selectedAgentId === snapshot.id}
      onSelect={dashboard.selectAgent}
      onRestart={dashboard.restartAgent}
      onStop={dashboard.stopAgent}
      onOpenDetails={() => {
        // 由宿主决定是打开 Sheet、Dialog 还是右侧 Inspector
      }}
    />
  );
}

推荐的页面组合

这个组件适合和以下原语组合:

  • Card:承载 agent 摘要
  • Badge:展示运行态、连接态
  • Button:主操作,例如启动、停止、重启
  • DropdownMenu:收纳次级动作
  • Sheet:打开详情面板
  • Tabs:在详情面板里切换概览、服务、日志、配置

组合之后,就是 Downcity 版的“状态地图 + 检查器”模式,只是从 teamprofile 的空间地图,变成了控制台里的运行时工作区。

teamprofile 应该借鉴什么

应该借鉴:

  • 主视图 + Inspector 的双栏关系
  • 选中态驱动详情面板
  • 操作入口靠近当前对象
  • 视觉层只消费结构化状态

不应该借鉴:

  • 业务模拟逻辑直接进 UI 包
  • pathfinding、动画调度、主题状态全部塞进一个组件
  • UI 组件自己持有领域真相

对包边界的建议

@downcity/ui

只放这些内容:

  • 基础组件
  • 通用布局原语
  • 纯展示型复合组件
  • 公共样式与公开类型

console-ui

放这些内容:

  • dashboard-api
  • dashboard-queries
  • dashboard-mutations
  • 页面级 hook
  • 容器组件

cli/city

放这些内容:

  • 网关路由
  • runtime 控制能力
  • agent 状态聚合
  • 类型定义

第一批最值得做的复合组件

如果你要把这套设计真正落到 downcity-ui,我建议先做下面这几类,而不是直接做“业务地图”:

  1. AgentWorkbenchCard
  2. AgentChannelStatusList
  3. AgentInspectorSheet
  4. RuntimeMetricCard
  5. CommandActionBar

它们都可以复用 @downcity/ui 原语,但和主包交互时统一通过宿主适配层完成。

结论

对 Downcity 来说,正确方案不是“把某个业务组件搬进 UI 包”,而是:

  • @downcity/ui 中沉淀可复用视觉组件
  • console-ui 中承接 downcity 主包交互
  • 通过稳定的 snapshot + intent 契约连接两者

如果后面你要继续推进,我建议下一步直接做 AgentWorkbenchCard + AgentInspectorSheet 这一组,这是最贴近当前 downcity 运行时场景的一套组件。