返回知识库
0

第十三章:Web Client 架构

本章目标:帮助你理解 dsh 的 Web Client 架构——React 组件体系(ui-chat、ui-approval、ui-settings 等)、Slot 机制与 UI 插件化、会话投影在前端的渲染、以及 WebSocket 实时通信。阅读本章后,你应该能回答"Web Client 如何渲染会话"以及"如何添加新的 UI 插件"。


13.1 架构概览

13.1.1 前后端分离

预览
源码

通信

Server (Host)

Browser (Client)

React App

Slot Registry

UI Plugins

API Gateway

Cordis Runtime

Session Log

WebSocket

JSON-RPC

graph TB
    subgraph "Browser (Client)"
        A[React App]
        B[Slot Registry]
        C[UI Plugins]
    end

    subgraph "Server (Host)"
        D[API Gateway]
        E[Cordis Runtime]
        F[Session Log]
    end

    subgraph "通信"
        G[WebSocket]
        H[JSON-RPC]
    end

    A --> G
    G --> D
    D --> H
    H --> E
    E --> F

13.1.2 包组织

预览
源码

依赖

client/ 客户端包

ui-layout/ (布局框架)

ui-chat/ (聊天界面)

ui-approval/ (审批界面)

ui-settings/ (设置界面)

ui-conversation/ (会话组件)

ui-renderer/ (渲染器)

connection/ (WebSocket 连接)

graph TB
    subgraph "client/ 客户端包"
        A["ui-layout/ (布局框架)"]
        B["ui-chat/ (聊天界面)"]
        C["ui-approval/ (审批界面)"]
        D["ui-settings/ (设置界面)"]
        E["ui-conversation/ (会话组件)"]
        F["ui-renderer/ (渲染器)"]
    end

    subgraph "依赖"
        G["connection/ (WebSocket 连接)"]
    end

    A --> B
    A --> C
    A --> D
    A --> E
    F --> A
    A --> G

13.2 Slot 机制

13.2.1 概念

Slot 是 dsh Web Client 的插件化 UI 机制。每个 UI 组件通过 Slot 注册,可以被其他插件覆盖或扩展。

预览
源码

扩展实现

默认实现

Slot Registry

覆盖

扩展

chat-slot

approval-slot

settings-slot

ChatCard

ApprovalCard

SettingsCard

CustomChatCard

CustomApprovalCard

graph TB
    subgraph "Slot Registry"
        A[chat-slot]
        B[approval-slot]
        C[settings-slot]
    end

    subgraph "默认实现"
        D[ChatCard]
        E[ApprovalCard]
        F[SettingsCard]
    end

    subgraph "扩展实现"
        G[CustomChatCard]
        H[CustomApprovalCard]
    end

    A --> D
    B --> E
    C --> F
    A -.->|"覆盖"| G
    B -.->|"扩展"| H

13.2.2 SlotRegistry

// 来自 packages/client/ui-renderer/src/client/registry.ts
class SlotRegistry {
  /** 注册 Slot */
  register<K extends string>(key: K, component: ComponentType): Disposable {
    this.slots.set(key, component)
    
    // 通知更新
    this.notifyUpdate(key)
    
    return () => {
      this.slots.delete(key)
      this.notifyUpdate(key)
    }
  }
  
  /** 获取 Slot */
  get<K extends string>(key: K): ComponentType | undefined {
    return this.slots.get(key)
  }
  
  /** 渲染 Slot */
  render<K extends string>(key: K, props: Record<string, unknown>): ReactNode {
    const component = this.get(key)
    if (!component) return null
    
    return React.createElement(component, props)
  }
}

13.2.3 使用 Slot

// 在组件中使用 Slot
function ChatView() {
  const slotRegistry = useSlotRegistry()
  
  return (
    <div className="chat-view">
      {slotRegistry.render('chat-slot', {
        messages: messages,
        onSend: handleSend
      })}
    </div>
  )
}

// 注册自定义 Slot
function registerCustomChat Slot(ctx: Context) {
  ctx.clientSlots.register('chat-slot', CustomChatCard)
}

13.3 UI 组件

13.3.1 ui-chat

聊天界面组件,处理消息显示和输入。

// 来自 packages/client/ui-chat/src/client
function ChatCard({ messages, onSend }: ChatCardProps) {
  return (
    <div className="chat-card">
      <MessageList messages={messages} />
      <InputBox onSend={onSend} />
    </div>
  )
}

13.3.2 ui-approval

审批界面组件,显示审批请求和用户选择。

// 来自 packages/client/ui-approval/src/client
function ApprovalCard({ request, onApprove, onDeny }: ApprovalCardProps) {
  return (
    <div className="approval-card">
      <h3>审批请求</h3>
      <p>{request.description}</p>
      <div className="actions">
        <button onClick={() => onApprove('once')}>批准一次</button>
        <button onClick={() => onApprove('always')}>始终批准</button>
        <button onClick={() => onDeny()}>拒绝</button>
      </div>
    </div>
  )
}

13.3.3 ui-settings

设置界面组件,管理用户配置。

// 来自 packages/client/ui-settings/src/client
function SettingsCard({ settings, onUpdate }: SettingsCardProps) {
  return (
    <div className="settings-card">
      <h3>设置</h3>
      <SettingsForm
        settings={settings}
        onChange={onUpdate}
      />
    </div>
  )
}

13.4 会话投影渲染

13.4.1 概念

前端使用投影来渲染会话状态,而不是直接读取日志。

预览
源码

Session Log

投影运行时

投影状态

React 组件

UI 渲染

graph TB
    A[Session Log] --> B[投影运行时]
    B --> C[投影状态]
    C --> D[React 组件]
    D --> E[UI 渲染]

13.4.2 投影订阅

// 来自 packages/client/connection/src/client
function useProjection<K extends string>(key: K): ProjectionState<K> {
  const [state, setState] = useState<ProjectionState<K>>()
  
  useEffect(() => {
    // 订阅投影更新
    const unsubscribe = connection.onProjectionUpdate(key, (newState) => {
      setState(newState)
    })
    
    // 获取初始状态
    connection.getProjection(key).then(setState)
    
    return unsubscribe
  }, [key])
  
  return state
}

13.4.3 在组件中使用

function ConversationView() {
  const turnBoundary = useProjection('turnBoundary')
  const toolUsage = useProjection('toolUsage')
  
  return (
    <div className="conversation">
      <TurnIndicator turn={turnBoundary?.lastTurn} />
      <ToolUsagePanel usage={toolUsage} />
      <MessageList />
    </div>
  )
}

13.5 WebSocket 通信

13.5.1 连接管理

// 来自 packages/client/connection/src/client
class WebSocketConnection {
  private ws: WebSocket
  
  constructor(private url: string) {
    this.ws = new WebSocket(url)
    this.setupEventHandlers()
  }
  
  /** 设置事件处理 */
  private setupEventHandlers(): void {
    this.ws.onopen = () => {
      console.log('WebSocket connected')
    }
    
    this.ws.onmessage = (event) => {
      const message = JSON.parse(event.data)
      this.handleMessage(message)
    }
    
    this.ws.onclose = () => {
      console.log('WebSocket disconnected')
      this.reconnect()
    }
  }
  
  /** 发送 JSON-RPC 请求 */
  async request(method: string, params: unknown): Promise<unknown> {
    return new Promise((resolve, reject) => {
      const id = generateRequestId()
      
      this.ws.send(JSON.stringify({
        jsonrpc: '2.0',
        id,
        method,
        params
      }))
      
      this.pendingRequests.set(id, { resolve, reject })
    })
  }
}

13.5.2 事件流

// 概念性描述
class EventStream {
  /** 订阅会话事件 */
  subscribe(sessionId: string, callback: (event: SessionEvent) => void): Disposable {
    // 发送订阅请求
    this.connection.request('event.subscribe', { sessionId })
    
    // 监听事件
    const handler = (message: StreamMessage) => {
      if (message.sessionId === sessionId) {
        callback(message.event)
      }
    }
    
    this.connection.on('event', handler)
    
    return () => {
      this.connection.off('event', handler)
      this.connection.request('event.unsubscribe', { sessionId })
    }
  }
}

13.6 ConversationNode 机制

13.6.1 概念

ConversationNode 是会话中可渲染的节点类型。每个节点类型有对应的渲染器。

interface ConversationNodeDefinition {
  /** 节点类型 */
  type: string
  /** 渲染器 */
  renderer: ComponentType<ConversationNodeProps>
  /** 匹配函数 */
  match: (event: SessionEvent) => boolean
}

13.6.2 注册 ConversationNode

// 注册自定义节点类型
ctx.clientConversation.registerNode({
  type: 'code-block',
  match: (event) => event.type === 'assistant/message' && event.data.content.includes('```'),
  renderer: CodeBlockNode
})

13.7 配置

13.7.1 Client 配置

# cordis.patch.yml
- id: client
  config:
    # WebSocket 端点
    wsEndpoint: 'ws://localhost:3000'
    
    # 主题
    theme: 'dark'  # 'light' | 'dark' | 'auto'
    
    # 启用的插件
    plugins:
      - 'ui-chat'
      - 'ui-approval'
      - 'ui-settings'

13.8 小结

概念一句话解释
Slot RegistryUI 插件化机制
ConversationNode会话中可渲染的节点
投影渲染前端使用投影渲染状态
WebSocket实时双向通信
JSON-RPC前后端通信协议

下一步第十四章:API Gateway 与 SDK——深入理解 BFF 架构、JSON-RPC 协议和 SDK 实现。

DeepSeek-Harness / 13-Web Client 架构 0 0 iliuqi
2026-09-04T07:48:53.413794215Z 2026-09-04T07:58:12.958347928Z