本教程基于一个真实的企业级 AI Agent 平台前端项目提炼而成,所有业务名称、接口路径、内部术语均已脱敏并使用通用化名。教程聚焦于架构设计、核心流程与工程实践,旨在为同类项目提供可复用的设计思路和实现参考。
目录
- [第 1 章 项目概述](#第 1 章 项目概述 "#%E7%AC%AC-1-%E7%AB%A0-%E9%A1%B9%E7%9B%AE%E6%A6%82%E8%BF%B0")
- [第 2 章 整体架构设计](#第 2 章 整体架构设计 "#%E7%AC%AC-2-%E7%AB%A0-%E6%95%B4%E4%BD%93%E6%9E%B6%E6%9E%84%E8%AE%BE%E8%AE%A1")
- [第 3 章 工程化与构建体系](#第 3 章 工程化与构建体系 "#%E7%AC%AC-3-%E7%AB%A0-%E5%B7%A5%E7%A8%8B%E5%8C%96%E4%B8%8E%E6%9E%84%E5%BB%BA%E4%BD%93%E7%B3%BB")
- [第 4 章 状态管理体系:MobX Store 设计](#第 4 章 状态管理体系:MobX Store 设计 "#%E7%AC%AC-4-%E7%AB%A0-%E7%8A%B6%E6%80%81%E7%AE%A1%E7%90%86%E4%BD%93%E7%B3%BBmobx-store-%E8%AE%BE%E8%AE%A1")
- [第 5 章 网关层设计:统一通信抽象](#第 5 章 网关层设计:统一通信抽象 "#%E7%AC%AC-5-%E7%AB%A0-%E7%BD%91%E5%85%B3%E5%B1%82%E8%AE%BE%E8%AE%A1%E7%BB%9F%E4%B8%80%E9%80%9A%E4%BF%A1%E6%8A%BD%E8%B1%A1")
- [第 6 章 实时通信与流式回复](#第 6 章 实时通信与流式回复 "#%E7%AC%AC-6-%E7%AB%A0-%E5%AE%9E%E6%97%B6%E9%80%9A%E4%BF%A1%E4%B8%8E%E6%B5%81%E5%BC%8F%E5%9B%9E%E5%A4%8D")
- [第 7 章 核心业务模块详解](#第 7 章 核心业务模块详解 "#%E7%AC%AC-7-%E7%AB%A0-%E6%A0%B8%E5%BF%83%E4%B8%9A%E5%8A%A1%E6%A8%A1%E5%9D%97%E8%AF%A6%E8%A7%A3")
- [第 8 章 审批与安全机制](#第 8 章 审批与安全机制 "#%E7%AC%AC-8-%E7%AB%A0-%E5%AE%A1%E6%89%B9%E4%B8%8E%E5%AE%89%E5%85%A8%E6%9C%BA%E5%88%B6")
- [第 9 章 设计模式与最佳实践总结](#第 9 章 设计模式与最佳实践总结 "#%E7%AC%AC-9-%E7%AB%A0-%E8%AE%BE%E8%AE%A1%E6%A8%A1%E5%BC%8F%E4%B8%8E%E6%9C%80%E4%BD%B3%E5%AE%9E%E8%B7%B5%E6%80%BB%E7%BB%93")
第 1 章 项目概述
1.1 产品定位
AI Agent 工作台(以下简称「工作台」)是一个面向企业内部用户的统一 Agent 控制前端,服务于办公、运维、开发、审核等多类岗位。用户在工作台中管理名下的 AI Agent、发起对话、查看执行过程、处理审批、管理定时任务。
产品的核心定位可以用一句话概括:
在一处指挥,在一处看清。
- 指挥:用户高效地把任务交给 Agent 完成
- 观测:随时看清每个 Agent 正在做什么、做到了哪一步、有什么等着自己处理
1.2 核心用户与场景
| 用户角色 | 典型场景 |
|---|---|
| 运维工程师 | 让 Agent 执行服务器巡检、日志分析、故障排查 |
| 办公人员 | 让 Agent 处理文档、生成报告、日程协调 |
| 开发人员 | 让 Agent 写代码、查问题、跑测试 |
| 审核人员 | 审批 Agent 的敏感操作、查看执行留痕 |
1.3 设计原则
- 状态永远可读:每个 Agent 的在线/离线/回复中/待审批状态,离开再回来也能一眼找回
- 过程即界面:流式回复、工具调用、审批动作是核心体验,把「AI 正在干活」的过程呈现好
- 一处控制面:Agent、会话、定时任务、日志、审批之间的切换低成本、上下文不丢
- 先进而不炫技:科技感来自精确的层次、克制的动效与实时性,而非装饰
1.4 技术栈概览
| 类别 | 选型 | 说明 |
|---|---|---|
| 框架 | React 19 + TypeScript | 函数式组件 + Hooks |
| 状态管理 | MobX 6 + mobx-react-lite | 细粒度响应式更新 |
| 路由 | react-router-dom v7 | 浏览器路由模式 |
| UI 组件库 | Ant Design v5 | 企业级组件库 |
| 构建工具 | Vite 7 | 极速开发与构建 |
| 样式 | Less | 全局变量 + mixin 注入 |
| 网络请求 | axios | HTTP 客户端 |
| 实时通信 | WebSocket(原生) | 双通道独立连接 |
| 富文本渲染 | react-markdown + remark-gfm | Markdown 消息渲染 |
| 代码编辑器 | CodeMirror 6 | SQL/代码编辑场景 |
| 文件预览 | @file-viewer/react | Office/PDF/图片预览 |
| 代码规范 | ESLint + Prettier + Stylelint | 代码质量保障 |
| 测试框架 | Vitest | 单元测试 |
1.5 项目目录结构
bash
src/
├── app.tsx # 应用入口(路由配置)
├── main.tsx # React 渲染入口
├── constants/ # 全局常量
├── styles/ # 全局样式(变量、mixin、重置)
├── utils/ # 通用工具函数
├── components/ # 跨模块复用的基础组件
│ ├── AnimatedIcons/
│ ├── AttachmentFileIcon/
│ ├── ConfirmDialog/
│ ├── CronInput/
│ ├── PillActionButton/
│ └── ...
├── pages/
│ ├── workbench/ # 工作台主页面(核心业务)
│ │ ├── components/
│ │ │ └── WorkbenchView/ # 工作台视图组件
│ │ ├── store/ # MobX 状态管理
│ │ ├── services/ # 服务层(网关、API、WS)
│ │ ├── utils/ # 工作台工具函数
│ │ ├── constants/ # 工作台常量
│ │ └── routes.ts # 工作台路由定义
│ ├── chat-embed/ # 嵌入模式页面
│ └── whats-new/ # 版本更新说明页
└── workers/
└── shared-worker/ # Shared Worker(跨标签页通信)
第 2 章 整体架构设计
2.1 五层架构模型
工作台采用经典的分层架构,从上到下依次为:
bash
┌─────────────────────────────────────────────────────┐
│ 路由层 (Router) │
│ URL → 页面组件映射,懒加载,参数解析 │
├─────────────────────────────────────────────────────┤
│ 视图层 (View / Components) │
│ React 组件树,消费 MobX store,渲染 UI,处理用户交互 │
├─────────────────────────────────────────────────────┤
│ 状态层 (Store / MobX) │
│ 领域状态管理,业务逻辑编排,跨组件状态共享 │
├─────────────────────────────────────────────────────┤
│ 网关层 (Gateway) │
│ 统一通信抽象,封装 HTTP + WS,屏蔽传输细节 │
│ ┌──────────────┐ ┌──────────────┐ │
│ │ 聊天消息通道 │ │ 静默命令通道 │ │
│ └──────────────┘ └──────────────┘ │
├─────────────────────────────────────────────────────┤
│ 传输层 (Transport) │
│ HTTP (axios) + WebSocket (原生双连接) │
└─────────────────────────────────────────────────────┘
每一层的职责单一,依赖方向严格自顶向下:
- 视图层 只依赖 状态层(通过 MobX observable)
- 状态层 只依赖 网关层(通过 Gateway 接口)
- 网关层 只依赖 传输层
- 下层不知道上层的存在
2.2 为什么这样分层
| 分层收益 | 具体体现 |
|---|---|
| 可测试性 | 每一层都可以独立 mock 和测试 |
| 可替换性 | 网关层有接口抽象,可以切换 mock / 真实后端 / polling 降级 |
| 可维护性 | 业务逻辑集中在 Store,组件只负责渲染和交互 |
| 可扩展性 | 新增传输方式(如 SSE)只需扩展网关层,不影响上层 |
2.3 核心领域模型
项目围绕以下几个核心实体构建:
bash
User ── owns ──> Agent ── has ──> Conversation ── has ──> Message
│ │
│ └─ has ──> UnreadState
│
├─ has ──> Skill(技能)
├─ has ──> Toolset(工具集)
├─ has ──> CronTask(定时任务)
└─ has ──> Memory(记忆)
Message ── triggers ──> Run(执行过程)── has ──> ExecutionEvent
│
└─ may contain ──> Approval(审批请求)
核心实体之间的关系:
- 一个用户可以拥有多个 Agent
- 一个 Agent 可以有多个会话(Conversation)
- 一个会话包含多条消息(Message)
- 一条用户消息可能触发一次 Agent 运行(Run)
- 运行过程中产生一系列执行事件(ExecutionEvent)
- 运行过程中可能产生审批请求(Approval)
2.4 数据流向
整个应用遵循 单向数据流 原则:
bash
用户操作
│
▼
组件调用 store 方法
│
▼
store 调用 gateway 方法
│
▼
gateway 发起 HTTP/WS 请求
│
▼
后端响应 / 实时事件
│
▼
gateway 归一化为领域事件
│
▼
store 更新 observable 状态
│
▼
MobX 响应式更新 → 组件 re-render
2.5 路由设计
工作台采用嵌套路由结构,所有功能都在 Workbench 页面框架内切换:
bash
/agent/ # Agent 列表 + 会话列表(默认视图)
/agent/:agentId/ # 选中某 Agent
/agent/:agentId/:conversationId # 选中某会话(聊天主界面)
/cron/ # 定时任务列表
/cron/:taskId # 定时任务详情
/settings/:tab # 设置面板
/memory/ # 记忆管理
/chat-embed # 嵌入模式(独立页面)
路由参数驱动状态:
agentId变化 →AgentStore.selectedAgentId变化conversationId变化 →ConversationStore选中会话变化 + 加载历史消息
第 3 章 工程化与构建体系
3.1 构建工具与配置
使用 Vite 作为构建工具,核心配置要点:
路径别名:
bash
@ → src/
样式预处理:
- Less 全局注入
variables.less和mixins.less - 排除
node_modules避免性能问题 - 采用全局样式模式(非 CSS Module)
开发服务器代理:
- API 前缀统一代理到后端服务器
- WebSocket 代理单独配置
- WASM 签名文件单独代理
3.2 代码分包策略
生产构建采用精细化的代码分割(manualChunks):
| 分包 | 内容 | 原因 |
|---|---|---|
request |
axios | 请求层独立,减少首屏体积 |
request-sign |
请求签名模块 | 供混淆插件单独处理 |
ant-design |
Ant Design 组件库 | 体积大,独立缓存 |
| 业务模块 | 按目录名分包 | 按需加载,减少首屏体积 |
3.3 代码规范体系
| 工具 | 作用 | 配置文件 |
|---|---|---|
| ESLint + typescript-eslint | TypeScript/TSX 代码质量检查 | eslint.config.ts |
| Prettier | 代码格式化 | .prettierrc.js |
| Stylelint | CSS/Less 规范检查 | stylelint.config.js |
| EditorConfig | 编辑器格式统一 | .editorconfig |
推荐的工作流:
bash
git commit → pre-commit → prettier + eslint --fix → 提交
3.4 Mock 体系
使用 vite-plugin-mock-dev-server 实现全链路 Mock:
- HTTP Mock:REST 接口 mock,匹配 API 前缀
- WS Mock:WebSocket 消息 mock,支持事件回放
- 按模块组织:每个业务模块独立的 mock 文件
- 回放能力:支持从录制的 fixture 回放 WS 事件序列
Mock 模式启动:
bash
vite --mode mock
3.5 环境配置
通过 .env 文件管理不同环境的配置:
| 变量 | 说明 |
|---|---|
VITE_BASE |
应用部署 base path |
VITE_PORT |
开发服务器端口 |
VITE_SERVER_HOST |
后端服务器地址 |
VITE_DIST |
构建输出目录 |
3.6 安全措施
- 代码混淆 :使用
javascript-obfuscator对核心代码进行混淆保护 - 请求签名:使用 WASM 模块对关键请求进行签名验证
- 权限校验:前端启动时校验用户是否有工作台访问权限
- 敏感操作二次确认:删除 Agent、执行危险命令等操作需确认
第 4 章 状态管理体系:MobX Store 设计
4.1 为什么选择 MobX
在 Redux 和 MobX 之间,本项目选择了 MobX,原因如下:
| 考量 | MobX 的优势 |
|---|---|
| 实时流式场景 | 细粒度更新,流式消息逐条到达时只有相关组件重渲染 |
| 开发效率 | 样板代码少,直接修改 observable 属性即可触发更新 |
| 心智模型 | 面向对象的 store 设计,更贴近业务领域建模 |
| 模块化 | 多个 store 独立管理各自领域,通过根 store 协调 |
4.2 Store 组织结构
采用 根 Store + 子 Store 的组织方式:
bash
WorkbenchRootStore(根 Store)
├── AgentStore # Agent 列表与选中态
├── ConversationStore # 会话列表管理
├── SessionStore # 当前会话的消息
├── ExecutionStore # 执行日志与运行面板
├── NotificationStore # 通知与弹窗
├── ConnectionStore # 连接状态
├── CronStore # 定时任务
├── CronReminderStore # 定时任务提醒
├── VersionStore # 版本更新
├── HindsightStore # 事后分析
└── ShortTermMemoryStore # 短期记忆
根 Store 的职责:
- 组装所有子 Store
- 协调跨 Store 的业务流程
- 处理实时事件的分发(通过
RootRealtimeHandler) - 处理用户消息的发送(通过
RootMessageDispatcher)
4.3 Store 设计原则
原则一:单一职责
每个 Store 只管理一个业务领域的状态:
AgentStore只管 Agent 列表,不管消息ConversationStore只管会话列表,不管消息内容SessionStore只管当前会话的消息,不管其他会话
原则二:可观察性最小化
只把需要触发 UI 更新的数据标记为 observable:
typescript
class SessionStore {
messages: ConversationMessage[] = []; // observable
sessionId: string | null = null; // observable
streamingPhase: StreamingPhase | null = null; // observable
private messageIndex = new Map<number, number>(); // 非 observable(纯计算用)
}
原则三:方法命名清晰
- 查询方法用
get/has/find前缀 - 修改方法用
set/add/append/remove/patch前缀 - 异步方法返回 Promise,内部用
runInAction包裹状态更新
4.4 关键 Store 详解
4.4.1 AgentStore
typescript
class AgentStore {
agents: Agent[] = [];
selectedAgentId: number | null = null;
loading = false;
keyword = '';
get selectedAgent(): Agent | null { ... } // 计算属性
get filteredAgents(): Agent[] { ... } // 过滤后的列表
setAgents(agents: Agent[]) { ... }
selectAgent(id: number) { ... }
setKeyword(keyword: string) { ... }
patchAgent(id: number, patch: Partial<Agent>) { ... }
clearUnread(agentId: number) { ... }
}
核心能力:
- 维护 Agent 列表及其元数据(名称、状态、未读数等)
- 管理当前选中的 Agent
- 支持搜索过滤
4.4.2 ConversationStore
typescript
class ConversationStore {
// agentRecordId → 会话分组数据
private agentConversations = new Map<number, AgentConversationGroups>();
private selectedConversationIds = new Map<number, string | null>();
private unreadMap = new Map<string, UnreadState>();
getConversations(agentId: number): ChatConversationView[] { ... }
getSelectedConversationId(agentId: number): string | null { ... }
selectConversation(agentId: number, conversationId: string | null) { ... }
upsertConversation(agentId: number, conversation: ChatConversationView) { ... }
patchConversation(agentId: number, conversationId: string, patch: Partial<...>) { ... }
removeConversation(agentId: number, conversationId: string) { ... }
createDraftConversation(agentId: number, meta: {...}) { ... }
promoteDraftConversation(...) { ... }
hasUnread(conversationId: string): boolean { ... }
}
设计要点:
- 按 Agent 分组存储会话列表(一个用户可以有多个 Agent)
- 支持草稿会话(用户输入但未发送的新会话)
- 独立的未读状态管理(未读数、最后消息时间等)
patchConversation模式:局部更新,避免全量替换
4.4.3 SessionStore
typescript
class SessionStore {
sessionId: string | null = null;
messages: ConversationMessage[] = [];
streamingPhase: StreamingPhase | null = null;
detailLoading = false;
historyLoadingState: 'idle' | 'loading_more' | 'loading_after' = 'idle';
beforeCursor: string | null = null;
afterCursor: string | null = null;
hasMoreBefore = false;
hasMoreAfter = false;
setMessages(messages: ConversationMessage[], cursorInfo: {...}) { ... }
appendMessage(message: ConversationMessage) { ... }
prependMessages(messages: ConversationMessage[], cursor, hasMore) { ... }
appendAssistantDelta(messageId: string, delta: string) { ... }
upsertAssistantToolPart(messageId: string, tool: ToolPart) { ... }
setAssistantFinalContent(messageId: string, content: string) { ... }
finalizeAssistantMessage(messageId: string) { ... }
updateMessageStatus(messageId: string, status: MessageStatus) { ... }
}
设计要点:
- 只维护当前活跃会话的消息(切换会话时清空并重新加载)
- 支持双向无限滚动(
hasMoreBefore/hasMoreAfter) - 流式消息的增量更新(
appendAssistantDelta) - 消息分块结构(text / tool / block / approval 等 Part)
4.4.4 ExecutionStore
typescript
class ExecutionStore {
visible = false;
currentRunId: string | null = null;
events: ExecutionEvent[] = [];
currentConversationId: string | null = null;
private runLogCache = new Map<string, ExecutionEvent[]>();
setRun(runId: string) { ... }
pushEvent(event: ExecutionEvent) { ... }
setEvents(events: ExecutionEvent[]) { ... }
focusRun(runId: string) { ... }
attachConversation(conversationId: string | null) { ... }
reset() { ... }
}
设计要点:
- 运行日志按 run 缓存(切换会话时恢复最后一次的日志)
- 实时事件追加模式
- 面板显隐独立管理
4.5 根 Store 的协调角色
根 Store 是跨 Store 业务流程的编排者。以「发送消息」为例:
bash
sendMessage(content)
→ 检查当前 Agent 和会话
→ messageDispatcher 编排发送
→ 更新 sessionStore(本地回显)
→ 更新 conversationStore(触碰时间)
→ 更新 executionStore(设置新 run)
→ 调用 gateway.sendMessage
以「切换 Agent」为例:
bash
selectAgent(agentId)
→ executionStore.reset() (清空执行面板)
→ agentStore.selectAgent()
→ conversationStore.setLoadingAgent()
→ 预取 Agent 技能列表
→ gateway.getGroupedConversations() (加载会话列表)
→ conversationStore.setAgentConversationGroups()
→ 打开默认会话
4.6 实时事件分发
根 Store 通过 RootRealtimeHandler 接收网关层的 RealtimeEvent,然后分发到各个子 Store:
bash
RealtimeEvent
├─ user_message
│ ├─ sessionStore.appendMessage() (如果正在查看)
│ └─ conversationStore.markUnread() (如果未查看)
├─ assistant_reply
│ ├─ sessionStore.appendAssistantDelta()
│ ├─ sessionStore.upsertAssistantToolPart()
│ └─ executionStore.pushEvent()
├─ done
│ └─ streamingRegistry.stopStreaming()
├─ unread_changed
│ └─ conversationStore.patchConversationUnread()
└─ ...
这种设计的好处是:
- 所有实时事件的处理逻辑集中在一处,便于排查
- 子 Store 不需要知道实时事件的存在,只暴露修改方法
- 新增事件类型只需在 handler 中加一个 case
第 5 章 网关层设计:统一通信抽象
5.1 为什么需要网关层
在一个 AI Agent 工作台中,前端与后端的交互方式非常多样:
| 交互类型 | 传输方式 | 特点 |
|---|---|---|
| 列表/详情查询 | HTTP | 请求-响应模式 |
| 发送消息 | WebSocket | 需要实时性 |
| 接收回复 | WebSocket | 流式、增量 |
| 静默指令 | WebSocket | 不进入对话流 |
| 审批操作 | HTTP 或 WS | 取决于场景 |
如果让 Store 层直接调用 axios 和 WebSocket,会导致:
- 业务逻辑与传输方式耦合
- 难以 mock 测试
- 传输层变化影响面大
网关层的作用就是屏蔽这些差异,向上层提供统一的、与传输无关的接口。
5.2 网关接口设计
定义 WorkbenchGateway 接口,涵盖所有前端需要的后端能力:
typescript
interface WorkbenchGateway {
// ===== Agent 管理 =====
getAgents(): Promise<Agent[]>;
createAgent(input: CreateAgentInput): Promise<CreateAgentResult>;
deleteAgentPreview(agentId: number): Promise<AgentDeletePreviewResult>;
deleteAgentConfirm(agentId: number, token: string): Promise<AgentDeleteConfirmResult>;
updateAgentAlias(agentId: number, alias: string | null): Promise<void>;
// ===== 会话管理 =====
getGroupedConversations(params: {...}): Promise<GroupedConversations>;
getArchivedConversations(params: {...}): Promise<ChatConversationView[]>;
initSession(agentId: number, conversationId: string): Promise<SessionInitResult>;
fetchMoreHistory(conversationId: string, cursor: string): Promise<HistoryResult>;
updateConversation(conversationId: string, patch: {...}): Promise<ChatConversationView>;
deleteConversation(conversationId: string): Promise<void>;
markConversationRead(conversationId: string): Promise<ReadState>;
// ===== 消息发送 =====
sendMessage(input: SendMessageInput): Promise<SendMessageResult>;
sendMentionMessage(input: SendMentionInput): Promise<SendMessageResult>;
// ===== 实时订阅 =====
subscribeEvents(params: {...}, callback: (event: RealtimeEvent) => void): RealtimeConnection;
replaceConversationSubscriptions(ids: string[]): void;
// ===== 静默命令 =====
sendSilentCommand?(command: string, opts: {...}): Promise<string>;
// ===== 执行日志 =====
getRunLogs(runId: string): Promise<ExecutionEvent[]>;
// ===== Token 管理 =====
getBotTokens(): Promise<BotToken[]>;
createBotToken(name?: string): Promise<BotToken>;
deleteBotToken(id: number): Promise<void>;
// ===== 传输元信息 =====
botCommandTransport: 'http' | 'work-order-ws';
}
上层代码(Store)只依赖这个接口,不依赖具体实现。
5.3 网关实现架构
网关的具体实现采用 门面 + 通道 模式:
bash
HttpWorkbenchGateway(门面)
├── ChatWebSocketClient × 2 # 双 WS 连接
│ ├── chatWs # 普通聊天通道
│ └── workOrderWs # 工单聊天通道
├── ConversationContextRouter # 上下文路由(多键索引)
├── ConversationMessageChannel # 聊天消息通道
├── SilentCommandChannel # 静默命令通道
├── AssistantReplyState # 流式回复状态管理
└── HTTP 客户端 (axios) # REST 接口
5.4 双通道设计
为什么需要两条 WebSocket 连接?
| 通道 | 用途 | 特点 |
|---|---|---|
| chatWs | 普通聊天 | 标准对话流,订阅-发布模式 |
| workOrderWs | 工单对话 | 绑定工单上下文,独立鉴权 |
两条连接的技术实现相同(都是 ChatWebSocketClient 类),区别在于:
- 连接路径不同
- 订阅的会话集合不同
- 消息 payload 结构略有差异
通过 getPrimaryWs() 方法根据当前场景动态选择主通道。
5.5 消息通道(ConversationMessageChannel)
聊天消息通道负责处理所有进入对话流的消息事件:
bash
WS 原始事件 → Channel.handleEvent()
├─ message_created → 注册上下文 → emit user_message
├─ dispatched → emit dispatched
├─ assistant_reply → 按 phase 拆分 → emit start/delta/tool/block/actions/end
├─ done → emit done
├─ error → emit error
├─ execution_event → 写入缓存 → emit execution_event
└─ 其他 → 透传
通道的输出是统一格式的 RealtimeEvent,这是网关层向上层暴露的事件契约。
5.6 静默命令通道(SilentCommandChannel)
静默命令是一种 请求-响应模式 的后台指令,特点是:
- 不进入对话消息流(用户看不到)
- 以 Promise 方式调用和等待结果
- 有超时机制
典型使用场景:
- 查询 Agent 技能列表
- 查询安全策略状态
- 查询当前模型
- 停止回复
实现原理:
- 发送时生成唯一
requestId,创建 Promise,存入 pending map - 设置超时定时器
- WS 收到回复事件时,匹配 requestId,resolve Promise,清理定时器
- 超时则 reject 并清理
typescript
class SilentCommandChannel {
private pending = new Map<string, PendingCommand>();
send(command: string, opts: {...}): Promise<string> {
const requestId = generateRequestId();
return new Promise((resolve, reject) => {
const timeout = setTimeout(() => {
this.pending.delete(requestId);
reject(new Error('timeout'));
}, opts.timeout ?? 15000);
this.pending.set(requestId, { resolve, reject, timeout });
this.ws.send({ action: 'send_message', requestId, data: {...} });
});
}
handleEvent(event: WsEvent): boolean {
const pending = this.findPending(event);
if (!pending) return false; // 不是静默命令,交给其他通道处理
// ... 累积内容,遇到 end/done 时 resolve
return true; // 已消费,不再向下传递
}
}
5.7 上下文路由器(ConversationContextRouter)
在流式对话中,一条消息的生命周期跨越多个事件,每个事件可能携带不同的标识:
requestId:发送时生成的请求 IDparentMessageId:用户消息 ID(回复哪条消息)botMessageId:AI 消息 ID(正在生成的消息)conversationId:会话 ID
上下文路由器维护这几个标识之间的多向索引:
bash
activeRequestContexts (requestId → context)
activeParentContexts (parentMessageId → context)
activeAssistantContexts (botMessageId → context)
收到事件时,按优先级匹配:
- 先用
requestId匹配(最精确) - 再用
parentMessageId匹配 - 再用
botMessageId匹配 - 最后用
conversationId + messageId构造历史上下文
这样即使某些阶段的事件缺少某个标识,也能正确关联到发送上下文。
5.8 网关的工厂创建
通过工厂函数创建网关实例,便于未来扩展多种实现:
typescript
function createWorkbenchGateway(options: GatewayOptions): WorkbenchGateway {
return createHttpWorkbenchGateway(options);
}
// 未来可以扩展:
// function createMockGateway(): WorkbenchGateway { ... }
// function createPollingGateway(): WorkbenchGateway { ... }
第 6 章 实时通信与流式回复
6.1 WebSocket 客户端设计
ChatWebSocketClient 是对原生 WebSocket 的封装,提供以下能力:
6.1.1 连接生命周期管理
bash
connect()
→ _doConnect()
→ new WebSocket(url, protocols)
→ onopen: 重置重试 + 启动心跳 + 设置 status='connected'
→ onmessage: 解析 JSON → 分发给 handler
→ onclose: 非主动关闭 → _scheduleReconnect()
→ onerror: (由 onclose 统一处理)
6.1.2 重连策略
| 参数 | 值 | 说明 |
|---|---|---|
| 初始延迟 | 2s | 第一次重连等待时间 |
| 退避因子 | 2 | 每次翻倍 |
| 最大延迟 | 30s | 上限 |
| 最大重试次数 | 10 次 | 达到后触发回调 |
重连是自动的,但提供人工干预接口:
pauseRetryAndDisconnect():暂停重连(如 session_replaced 场景等待用户确认)resumeRetry():恢复重连cancelRetry():取消重连(用户主动断开)
6.1.3 心跳保活
每 30s 发送 { action: 'ping' },收到 pong 重置心跳计时器。心跳超时会触发连接关闭 → 重连。
6.1.4 Pod 路由重定向
在微服务 / K8s 环境中,WS 连接可能需要路由到特定的 Pod。由于浏览器原生 WebSocket 不能设置自定义请求头,项目采用 Sec-WebSocket-Protocol 子协议头 承载目标 Pod IP:
6.2 消息格式设计
6.2.1 发送消息格式
统一使用 envelope 格式:
json
{
"action": "send_message",
"requestId": "req-1710000000000-1",
"data": {
"botId": "bot-xxx",
"agentId": "agent-xxx",
"conversationId": "conv-xxx",
"content": "你好",
"metadata": {}
}
}
支持的 action 类型:
send_message:发送普通消息send_mention_message:发送 @ 消息subscribe:订阅会话unsubscribe:取消订阅ping:心跳
6.2.2 接收消息格式
控制类消息使用 event 字段:
json
{ "event": "user_bound", "data": {...} }
{ "event": "session_replaced", "data": {...} }
{ "event": "redirect", "data": {...} }
{ "event": "agent_list_update", "data": {...} }
聊天回流消息使用 type 字段:
json
{ "type": "message_created", "data": {...} }
{ "type": "assistant_reply", "data": {...} }
{ "type": "done", "data": {...} }
{ "type": "error", "data": {...} }
6.3 用户绑定(user_bound)
WS 连接建立后并不立即可用,需要等待服务端推送 user_bound 事件(通过同源握手上下文鉴权)。
isReady() 的判定条件:
bash
readyState === OPEN && bound === true
绑定完成后执行:
- 批量订阅当前待订阅的会话
- 执行 pendingOnReadyCallbacks 队列(所有在就绪前排队的操作)
6.4 流式回复的 8 个阶段
一条用户消息从发送到完成,经历以下阶段:
每个阶段都会通过 WS 推送一个事件,前端逐阶段更新 UI。
6.5 流式回复的增量计算
后端有两种流式推送方式:
- 全量推送:每次推送截至当前的完整内容
- 增量推送:每次只推送新增的 delta 文本
为了兼容两种模式,AssistantReplyState 模块负责增量计算:
typescript
class AssistantReplyState {
private contentCache = new Map<string, string>();
buildDelta(botMessageId: string, fullContent: string): string {
const cached = this.contentCache.get(botMessageId) ?? '';
if (fullContent.startsWith(cached)) {
const delta = fullContent.slice(cached.length);
this.contentCache.set(botMessageId, fullContent);
return delta;
}
// 内容不一致,全量替换(返回特殊标记)
this.contentCache.set(botMessageId, fullContent);
return fullContent;
}
}
6.6 流式注册器(StreamingRegistry)
需要一个地方追踪「哪些会话正在流式回复」以及「处于什么阶段」,用于:
- 会话列表的「回复中」状态指示
- 侧边栏 Agent 卡片的状态点
- 消息流中的「思考中」动画
typescript
class StreamingRegistry {
// conversationId::parentMessageId → phase
private streaming = new Map<string, StreamingPhase>();
startStreaming(conversationId: string, parentMessageId: string, phase: StreamingPhase) { ... }
updateStreamingPhase(key: string, phase: StreamingPhase) { ... }
stopStreaming(key: string) { ... }
getConversationStreamingPhase(conversationId: string): StreamingPhase | null { ... }
get streamingConversationIds(): Set<string> { ... }
}
它是 MobX observable 的,因此任何组件都可以响应式地获取流式状态。
6.7 草稿会话转正(Draft Promotion)
用户在新会话中输入第一条消息时,会话还没有正式 ID(后端还没创建)。流程是:
bash
1. 用户发送消息 → 前端创建草稿会话(临时 ID)
2. WS 发送消息,携带 createConversation=true
3. 后端创建会话,返回 message_created + draft_conversation_created 事件
4. 前端将草稿会话「提升」为正式会话
├─ ConversationStore: 替换 conversationId
├─ SessionStore: 替换消息的会话 ID
├─ StreamingRegistry: 迁移流式键
└─ Composer: 迁移草稿和推理等级
这个机制保证了用户体验的连续性------发送消息的瞬间就有了对话界面,而不是等后端返回后才显示。
6.8 已查看 / 未查看会话分流
为了性能优化,实时事件的处理区分「正在查看的会话」和「后台会话」:
- 正在查看的会话:实时更新消息列表(流式插入、增量追加)→ 立即渲染
- 后台会话:只更新未读计数,不加载消息内容 → 不触发渲染
判断依据:conversationId === SessionStore.sessionId
这样当用户有很多会话都在接收消息时,只有当前会话会触发渲染,避免性能问题。
第 7 章 核心业务模块详解
7.1 聊天会话模块
聊天会话是工作台的核心交互界面,包含以下子组件:
bash
Conversation
├── ConversationHeader # 会话头部(标题、操作按钮)
├── ThreadMessageList # 消息列表(虚拟滚动 + 流式渲染)
│ ├── MessageFrame # 消息框架(头像、发送者、时间)
│ │ ├── MessageBubble # 消息气泡
│ │ │ └── MessageContent # 消息内容(Markdown + 结构化内容)
│ │ ├── ToolOutputGroup # 工具输出分组
│ │ └── ApprovalCard # 审批卡片
│ ├── TypingBubble # 「正在输入」动画
│ └── QuestionTimeline # 问题时间线
├── ApprovalQueuePanel # 审批队列面板
└── MessageSearch # 消息搜索
消息内容分块结构
一条 AI 消息不是单一文本,而是由多个 Part 组成的有序列表:
bash
AssistantMessage
├── TextPart # 文本内容(Markdown)
├── ToolPart # 工具调用
├── BlockPart # 结构化块(代码块、表格等)
├── ApprovalPart # 审批请求
├── DatabaseResultPart # 数据库查询结果
└── ActionPart # 建议操作(工单场景)
这种结构支持流式渲染------不同类型的内容可以边生成边追加,不需要等全部完成。
消息列表的无限滚动
支持两个方向的滚动加载:
- 向上滚动 :加载更早的历史(
hasMoreBefore+beforeCursor) - 向下滚动 :如果因为网络原因有滞后,加载更新的消息(
hasMoreAfter+afterCursor)
7.2 输入框模块(Composer)
Composer 是用户与 Agent 交互的入口,功能丰富:
bash
Composer
├── ComposerContainer # 主容器
├── ComposerInputPreview # 输入预览区
├── ComposerContextBar # 上下文提示栏
├── ComposerAttachedStrip # 已附件条
├── AttachmentUpload # 附件上传
├── MentionMenu # @Agent 菜单
├── SlashCommandMenu # /命令 菜单
├── ComposerToolbar # 工具栏
│ ├── AttachmentButton # 附件按钮
│ ├── BusyModeSelect # 忙时策略选择
│ ├── ModelSelect # 模型选择
│ └── PermissionSelect # 权限模式选择
├── DatabaseEditor # 数据库操作编辑器
│ ├── DatabaseTargetBar # 目标数据库选择
│ └── DatabaseTemplateMenu # SQL 模板
└── DangerousCommandDialog # 危险命令确认对话框
核心交互特性
- 本地回显:发送后立即在消息列表显示用户消息(pending 状态),不等后端确认
- 草稿保存:未发送的内容自动保存,切换会话再切回来还在
- Mention 支持 :输入
@弹出 Agent 选择菜单 - 斜杠命令 :输入
/弹出命令菜单(技能触发) - 附件上传:支持拖拽、粘贴、点击上传
- SQL 模式:数据库操作场景下的专用编辑器
7.3 执行面板模块(Exec Panel)
执行面板是右侧抽屉式面板,展示 Agent 执行过程的详细信息。
6 个功能 Tab
| Tab | 功能 |
|---|---|
| Output | 终端输出 / 结构化值预览 |
| Run | 运行概览(状态、耗时、调用链) |
| File | 文件操作预览(Markdown / 图片 / CSV / 文本 / Office) |
| Browser | 浏览器操作预览(Agent 浏览网页时) |
| Database | 数据库操作结果预览 |
| Changes | 文件变更对比 |
设计要点
- 执行日志按
runId缓存,切换会话时恢复最后一次的日志 - 实时事件流式追加,支持自动滚动到底部
- 支持折叠/展开工具调用详情
- 文件预览集成
@file-viewer/react,支持多种 Office 格式
7.4 侧边栏模块
侧边栏是工作台的主导航区域,包含:
bash
Sidebar
├── Agent 列表区
│ └── AgentCard # Agent 卡片(名称、状态点、未读数)
├── 会话列表区
│ ├── 置顶会话
│ ├── 普通会话
│ └── 草稿会话
├── 定时任务列表
│ └── CronCard # 定时任务卡片
└── 技能列表
└── SkillItem # 技能项
侧边栏支持:
- Agent 搜索过滤
- 会话搜索过滤
- 定时任务搜索过滤
- 新对话创建
- 设置入口
7.5 定时任务模块(Cron)
Agent 可以按照 Cron 表达式定时执行任务。
功能清单
| 功能 | 说明 |
|---|---|
| 任务列表 | 展示所有定时任务及其状态 |
| 创建任务 | Cron 表达式编辑器 + 自然语言描述 |
| 编辑任务 | 修改 Cron 表达式、Prompt、目标 Agent |
| 启用/停用 | 开关控制任务是否生效 |
| 手动触发 | 立即执行一次任务 |
| 删除任务 | 删除定时任务 |
| 执行历史 | 查看任务的历史执行记录 |
| 任务详情 | 展示任务配置、下一次执行时间、历史记录 |
Cron 编辑器
使用 react-js-cron 组件提供可视化 Cron 编辑,配合 cronstrue 将 Cron 表达式转为人类可读的自然语言描述(如「每天上午 9 点」)。
7.6 记忆管理模块
Agent 的记忆分为短期记忆和长期记忆两类。
短期记忆(Short-term Memory)
- 用户画像:Agent 对用户的了解和偏好
- Agent 观察历史:Agent 在交互中观察到的信息
- 支持编辑、查看历史变更
长期记忆
多种可视化视图:
- 表格视图:列表形式展示所有记忆条目
- 时间线视图:按时间顺序展示记忆的形成过程
- 星座图视图:2D Canvas 可视化,以节点和连线展示记忆之间的关联
记忆来源:
- 用户手动创建
- Agent 自动生成
- 技能安装时带入
- 系统内置
7.7 设置模块
设置模块包含多个 Tab:
| Tab | 功能 |
|---|---|
| 个人资料 | 用户信息、Bot 配置、Soul/Preferences 文件编辑 |
| Agent 设置 | 忙时策略、文件权限、技能管理、工具集管理、创建 Agent |
| Token 管理 | Bot Token 的生成、查看、删除(带遮罩和复制) |
| 显示设置 | 主题切换、字体大小等 |
Agent 设置的核心能力
- 忙时策略:Agent 忙碌时的处理方式(排队 / 拒绝 / 转交)
- 技能管理:从技能市场安装技能、启用/停用技能、卸载技能
- 工具集管理:配置 Agent 可用的工具集
- 文件设置:Agent 可访问的文件路径和权限
- Agent 创建:创建新的 Agent 实例,可选是否公开
7.8 事后分析模块(Hindsight)
Hindsight 是对 Agent 执行过程的事后回顾和分析能力。
功能包括:
- 执行链路的可视化
- 关键节点的回溯
- 效果评估和改进建议
7.9 嵌入模式(Chat Embed)
嵌入模式是一个独立的页面,用于将 Agent 对话能力嵌入到其他系统(如工单系统)中。
特点:
- 独立路由
/chat-embed - 通过 URL 参数或 postMessage 与宿主页面通信
- 使用独立的 WebSocket 通道(工单通道)
- 支持 Action 卡片(建议操作的采纳/拒绝)
- 支持工单上下文注入(工单 ID、标题、描述、优先级等)
与主工作台的区别:
- 只显示单一对话,没有 Agent 列表和侧边栏
- 工单数据通过外部系统注入
- 操作结果通过 postMessage 回传给宿主页面
第 8 章 审批与安全机制
8.1 审批机制概述
在企业级场景中,Agent 的某些操作需要人工审批才能执行。审批机制是工作台的重要安全保障。
审批触发时机
当 Agent 要执行敏感操作时(如执行命令、修改文件、访问生产环境等),会暂停执行并向用户发起审批请求。
审批内容结构
typescript
interface ApprovalContent {
id: string; // 审批请求 ID
type: 'approval'; // 内容类型
approvalKind: string; // 审批类型:'exec' | 'file' | 'database' | ...
allowedActions: string[]; // 允许的操作:['allow-once', 'allow-always', 'deny']
expiresAtMs: number; // 过期时间戳
description?: string; // 操作描述
}
8.2 审批展示
审批请求以内联卡片的形式展示在对话流中,位于相关 AI 消息的下方:
bash
┌─ AI 消息气泡 ─────────────────────────────────┐
│ 我需要执行以下命令来检查 CPU 使用率: │
│ │
│ top -bn1 | grep Cpu │
│ │
│ ┌─ 审批卡片 ──────────────────────────────┐ │
│ │ ⚠️ 执行命令需要审批 │ │
│ │ 命令:top -bn1 | grep Cpu │ │
│ │ │ │
│ │ [单次允许] [始终允许] [拒绝] │ │
│ └─────────────────────────────────────────┘ │
└───────────────────────────────────────────────┘
8.3 审批操作流程
bash
Agent 触发敏感操作
→ 生成审批请求
→ 通过 WS 推送到前端(assistant_reply end 事件的 content 中)
→ 前端展示审批卡片
→ 用户点击操作
├─ allow-once → 仅本次允许
├─ allow-always → 此类操作始终允许(加入白名单)
└─ deny → 拒绝执行
→ 前端发送审批响应
→ Agent 继续执行 / 中止执行
8.4 权限模式(Security Mode)
Agent 有不同的安全运行模式,控制其自主程度:
| 模式 | 说明 |
|---|---|
| 完全自主 | Agent 可以自由执行所有操作,无需审批 |
| 敏感操作审批 | 高风险操作需要审批,普通操作自动执行 |
| 全部审批 | 所有操作都需要人工确认 |
权限模式可以在 Composer 工具栏或设置中切换。
8.5 请求安全
请求签名
关键 API 请求使用 WASM 模块进行签名验证:
- 页面加载时加载
jssignaops.wasm模块 - 发送请求前调用签名函数生成签名字段
- 后端验签通过才处理请求
鉴权机制
- 页面加载时调用
/self接口获取当前用户信息 - 校验用户是否有工作台访问权限(资源列表中包含
tec01) - WebSocket 连接通过同源 cookie +
user_bound事件完成鉴权
8.6 危险命令防护
Composer 内置危险命令检测:
- 检测用户输入的命令是否包含高风险操作(如
rm -rf /、drop table等) - 发送前弹出二次确认对话框
- 要求用户输入确认文本后才允许发送
第 9 章 设计模式与最佳实践总结
9.1 项目中用到的设计模式
| 模式 | 应用场景 | 收益 |
|---|---|---|
| 门面模式 | Gateway 层统一封装 HTTP + WS | 上层只依赖一个接口 |
| 工厂模式 | createGateway 工厂函数 | 便于扩展多种网关实现 |
| 通道模式 | 聊天消息通道 / 静默命令通道分离 | 不同类型的消息流独立演进 |
| 策略模式 | 双 WS 通道(chat / work-order)动态选择 | 根据场景切换通信方式 |
| 观察者模式 | MobX observable + reaction | 细粒度响应式更新 |
| 状态机模式 | 消息生命周期(start→delta→end→done) | 状态转换清晰可追溯 |
| 备忘录模式 | runLogCache 缓存执行日志 | 切换会话时恢复上下文 |
| 代理模式 | Vite dev server 代理 | 本地开发跨域访问后端 |
9.2 MobX 使用最佳实践
1. 细粒度 observable,避免大对象全量替换
typescript
// ✅ 好:局部 patch
patchConversation(agentId, conversationId, patch) {
const conv = this.getConversation(agentId, conversationId);
if (conv) Object.assign(conv, patch);
}
// ❌ 不好:全量替换
setConversation(agentId, conversationId, newConv) {
const list = this.agentConversations.get(agentId);
const index = list.findIndex(c => c.id === conversationId);
list[index] = newConv; // 触发整个列表重新渲染
}
2. 使用计算属性(computed)派生状态
typescript
get selectedAgent() {
return this.agents.find(a => a.id === this.selectedAgentId) ?? null;
}
get filteredAgents() {
const keyword = this.keyword.toLowerCase();
if (!keyword) return this.agents;
return this.agents.filter(a => a.name.toLowerCase().includes(keyword));
}
3. 异步更新统一用 runInAction 包裹
typescript
async loadData() {
this.loading = true;
try {
const data = await api.fetch();
runInAction(() => {
this.items = data;
this.loading = false;
});
} catch (e) {
runInAction(() => {
this.loading = false;
this.error = e;
});
}
}
9.3 实时通信最佳实践
1. 永远考虑重连
- 所有 WS 连接都要有自动重连机制
- 重连后需要重新订阅、同步状态
- 提供用户手动重连的入口
2. 事件驱动,不要轮询
- 能走 WS 推送的就不要走轮询
- 轮询只作为降级方案
3. 本地回显 + 最终一致
- 发送消息后立即本地显示(optimistic UI)
- 后端确认后更新状态
- 失败时回滚并提示
4. 上下文路由优先用 requestId
- 发送时生成唯一 requestId
- 后端在所有相关事件中回传 requestId
- 前端用 requestId 作为主键匹配上下文
9.4 组件设计最佳实践
1. 容器组件 + 展示组件分离
bash
Container(容器组件) → 消费 store、处理业务逻辑
└─ Presentational(展示组件) → 只通过 props 接收数据,纯渲染
2. 组件粒度适中
- 太小:props drilling 严重,文件碎片化
- 太大:难复用,内部逻辑复杂
- 原则:一个组件做一件事,文件长度控制在 200-400 行
3. 优先复用基础组件和 UI 库
- 能用 Ant Design 组件就不手写
- 跨模块复用的组件抽到
components/目录 - 业务专用组件放在模块自己的目录下
9.5 项目治理最佳实践
1. AI 开发治理体系
本项目建立了完整的 AI 开发规范体系,包括:
- 最高指引文件:定义开发原则和硬边界
- 开发规范:编码、样式、状态、协议、缓存、组件复用等规范
- 业务边界:按模块划分 Agent 职责,每个模块有对应的 Owner
- 协作流程:跨模块协作的调度流程
- 代码图谱:CodeGraph 辅助理解代码结构和影响面
2. 模块化边界清晰
| 边界 | 规则 |
|---|---|
| 业务逻辑 | 只放在 Store / Service 层,不写在组件里 |
| 样式 | 模块样式写在模块自己的目录,不跨模块污染 |
| Mock | 只放在 mock/ 目录,不混入业务代码 |
| 全局组件 | 只放跨模块复用且不绑定业务 store 的 UI 能力 |
| 协议 | 前后端协议变更同步更新类型定义 |
3. 测试策略
- 单元测试:Store 层和工具函数,覆盖率优先保障核心逻辑
- 组件测试:关键交互组件的行为测试
- Mock 联调:全链路 Mock 支持,前端可独立于后端开发
9.6 性能优化要点
| 优化点 | 做法 |
|---|---|
| 流式渲染 | 消息逐条增量更新,不做全量重绘 |
| 虚拟列表 | 长消息列表使用虚拟滚动(视项目规模) |
| 后台会话不渲染 | 未查看的会话只更新未读数,不加载消息 |
| 代码分割 | 按模块分包,首屏只加载必要代码 |
| 按需加载 | 重组件(如文件预览、代码编辑器)懒加载 |
| 防抖节流 | 搜索输入、滚动加载等场景 |
| 缓存策略 | 技能列表、Agent 信息等做 session 级缓存 |
结语
本教程从一个真实的企业级 AI Agent 工作台项目提炼而来,覆盖了从架构设计到具体实现的各个层面。核心的设计理念可以总结为三句话:
- 分层解耦------网关层屏蔽传输差异,Store 层集中业务逻辑,组件层只负责渲染
- 实时优先------所有交互围绕 WebSocket 流式推送设计,以事件驱动状态更新
- 观测并重------不仅能指挥 Agent 干活,更能看清过程、审批敏感操作、追溯历史
希望这份教程能为你构建自己的 AI Agent 工作台提供有价值的参考。
本文档基于企业级 AI Agent 工作台前端项目提炼,所有业务名称、接口路径、内部术语均已脱敏。