企业级 AI Agent 工作台前端实战教程

本教程基于一个真实的企业级 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 设计原则

  1. 状态永远可读:每个 Agent 的在线/离线/回复中/待审批状态,离开再回来也能一眼找回
  2. 过程即界面:流式回复、工具调用、审批动作是核心体验,把「AI 正在干活」的过程呈现好
  3. 一处控制面:Agent、会话、定时任务、日志、审批之间的切换低成本、上下文不丢
  4. 先进而不炫技:科技感来自精确的层次、克制的动效与实时性,而非装饰

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 安全措施

  1. 代码混淆 :使用 javascript-obfuscator 对核心代码进行混淆保护
  2. 请求签名:使用 WASM 模块对关键请求进行签名验证
  3. 权限校验:前端启动时校验用户是否有工作台访问权限
  4. 敏感操作二次确认:删除 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 的职责:

  1. 组装所有子 Store
  2. 协调跨 Store 的业务流程
  3. 处理实时事件的分发(通过 RootRealtimeHandler)
  4. 处理用户消息的发送(通过 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 技能列表
  • 查询安全策略状态
  • 查询当前模型
  • 停止回复

实现原理:

  1. 发送时生成唯一 requestId,创建 Promise,存入 pending map
  2. 设置超时定时器
  3. WS 收到回复事件时,匹配 requestId,resolve Promise,清理定时器
  4. 超时则 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:发送时生成的请求 ID
  • parentMessageId:用户消息 ID(回复哪条消息)
  • botMessageId:AI 消息 ID(正在生成的消息)
  • conversationId:会话 ID

上下文路由器维护这几个标识之间的多向索引:

bash 复制代码
activeRequestContexts    (requestId → context)
activeParentContexts     (parentMessageId → context)
activeAssistantContexts  (botMessageId → context)

收到事件时,按优先级匹配:

  1. 先用 requestId 匹配(最精确)
  2. 再用 parentMessageId 匹配
  3. 再用 botMessageId 匹配
  4. 最后用 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   # 危险命令确认对话框

核心交互特性

  1. 本地回显:发送后立即在消息列表显示用户消息(pending 状态),不等后端确认
  2. 草稿保存:未发送的内容自动保存,切换会话再切回来还在
  3. Mention 支持 :输入 @ 弹出 Agent 选择菜单
  4. 斜杠命令 :输入 / 弹出命令菜单(技能触发)
  5. 附件上传:支持拖拽、粘贴、点击上传
  6. 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 模块进行签名验证:

  1. 页面加载时加载 jssignaops.wasm 模块
  2. 发送请求前调用签名函数生成签名字段
  3. 后端验签通过才处理请求

鉴权机制

  • 页面加载时调用 /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 工作台项目提炼而来,覆盖了从架构设计到具体实现的各个层面。核心的设计理念可以总结为三句话:

  1. 分层解耦------网关层屏蔽传输差异,Store 层集中业务逻辑,组件层只负责渲染
  2. 实时优先------所有交互围绕 WebSocket 流式推送设计,以事件驱动状态更新
  3. 观测并重------不仅能指挥 Agent 干活,更能看清过程、审批敏感操作、追溯历史

希望这份教程能为你构建自己的 AI Agent 工作台提供有价值的参考。


本文档基于企业级 AI Agent 工作台前端项目提炼,所有业务名称、接口路径、内部术语均已脱敏。

相关推荐
deli00744 分钟前
换个 DNS 网速真的变快吗?解析 6 步逐跳实测,快慢差 45 倍
前端
葡萄城技术团队1 小时前
接一个前端库:以前要啃几天文档,现在三步——活字格 V12.1 AI辅助开发上手
前端
愿得一猪蹄1 小时前
浏览器开发者工具隐藏技巧,你可能只用了一成
前端
高晶1 小时前
ARM 嵌入式 Linux 内核编译与结构解析实战
前端·架构
今年下半年2 小时前
【前端】Vue2 项目 ECharts 统计图表组件封装与使用实践(附组件源码)
前端·javascript·echarts
西索斯coding2 小时前
在 中接入 教程:p配置、m填写、扩展思考参数一篇就够(2)
java·前端·javascript·ai
代码猎人2 小时前
前端怎么优化项目基础架构
前端
__sjfzllv___2 小时前
在职前端Leader学习/转行 AI Agent -DAY78
前端
代码猎人2 小时前
前端怎么封装工具类
前端