第十三章:Web Client 架构 本章目标 :帮助你理解 dsh 的 Web Client 架构——React 组件体系(ui-chat、ui-approval、ui-settings 等)、Slot 机制与 UI 插件化、会话投影在前端的渲染、以及 WebSocket 实时通信。阅读本章后,你应该能回答"Web Client 如何渲染会话"以及"如何添加新的 UI 插件"。
13.1 架构概览 13.1.1 前后端分离 13.1.2 包组织 13.2 Slot 机制 13.2.1 概念Slot 是 dsh Web Client 的插件化 UI 机制 。每个 UI 组件通过 Slot 注册,可以被其他插件覆盖或扩展。
13.2.2 SlotRegistry
class SlotRegistry {
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)
}
}
get < K extends string > ( key: K ) : ComponentType | undefined {
return this . slots. get ( key)
}
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 概念前端使用投影来渲染会话状态,而不是直接读取日志。
13.4.2 投影订阅
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 连接管理
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 ( )
}
}
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 配置
- id : client
config :
wsEndpoint : 'ws://localhost:3000'
theme : 'dark'
plugins :
- 'ui-chat'
- 'ui-approval'
- 'ui-settings'
13.8 小结
概念 一句话解释 Slot Registry UI 插件化机制 ConversationNode 会话中可渲染的节点 投影渲染 前端使用投影渲染状态 WebSocket 实时双向通信 JSON-RPC 前后端通信协议
下一步 :第十四章:API Gateway 与 SDK ——深入理解 BFF 架构、JSON-RPC 协议和 SDK 实现。