资讯动态

第 9 章 审批与安全机制

发布时间:2026/10/20 2:09:53 来源:尧图企业网站定制
本章目标实现审批流程和安全机制让 Agent 在安全约束下工作⭐⭐ 难度基础 | ⏱️ 预计时间1 小时9.1 为什么需要审批先想一个问题如果你的 AI 助手可以直接执行命令、修改文件、访问数据库你放心让它随便干吗用户帮我清理一下磁盘空间 AI好的我执行rm-rf/* 来清理 这可不行在企业场景下AI Agent 的每一个敏感操作都必须经过人工审批。这不仅是用户体验问题更是安全问题。审批机制的作用作用说明安全保障防止 AI 执行危险操作删库、执行高危命令等用户掌控让用户知道 AI 在干嘛对操作有决定权合规要求企业级场景下重要操作需要留痕和审批逐步授权可以选择「单次允许」或「始终允许」灵活控制9.2 审批流程是怎样的完整的审批流程1. AI 想要执行一个敏感操作 │ ▼2. AI 暂停发起审批请求 │ ▼3. 前端展示审批卡片显示要做什么、有什么风险 │ ├─ 用户点「允许一次」→ AI 执行这次操作 ├─ 用户点「始终允许」→ 这类操作以后自动通过 └─ 用户点「拒绝」 → AI 取消这次操作换个方式或解释原因 │ ▼4. 审批结果通过 WebSocket 发给后端AI 继续执行或中止审批内容长什么样审批请求包含这些信息interfaceApprovalRequest{id:string;// 审批请求 IDkind:string;// 审批类型exec | file_write | database | ...title:string;// 标题简短描述要做什么description:string;// 详细描述command?:string;// 要执行的命令如果是命令执行filePath?:string;// 要操作的文件路径如果是文件操作allowedActions:string[];// 允许的操作[allow-once, allow-always, deny]expiresAt?:number;// 过期时间}审批类型常见的审批类型类型触发场景敏感程度exec执行终端命令高file_write修改/删除文件中高file_read读取敏感文件中database操作数据库高web_browse访问外部网站低api_call调用第三方 API中9.3 审批卡片组件审批卡片是审批流程的核心 UI通常展示在 AI 消息的下方。组件实现创建src/pages/workbench/components/Conversation/ApprovalCard.tsximport React, { useState, useEffect } from react; import { Button, Space, Tag, Alert } from antd; import { CheckCircleOutlined, CloseCircleOutlined, ClockCircleOutlined, WarningOutlined, } from ant-design/icons; import { observer } from mobx-react-lite; import { useStore } from /store/StoreContext; import styles from ./ApprovalCard.less; interface Props { approval: ApprovalRequest; messageId: string; } // 审批类型对应的标签颜色 const kindColors: Recordstring, string { exec: red, file_write: orange, file_read: blue, database: purple, web_browse: green, api_call: cyan, }; // 审批类型的中文名称 const kindNames: Recordstring, string { exec: 命令执行, file_write: 文件写入, file_read: 文件读取, database: 数据库操作, web_browse: 网页浏览, api_call: API 调用, }; const ApprovalCard observer(({ approval, messageId }: Props) { const { rootStore } useStore(); const [status, setStatus] useStatepending | approved | denied(pending); const [loadingAction, setLoadingAction] useStatestring | null(null); // 计算剩余时间 const [timeLeft, setTimeLeft] useStatestring(); useEffect(() { if (!approval.expiresAt) return; const updateTimeLeft () { const diff approval.expiresAt! - Date.now(); if (diff 0) { setTimeLeft(已过期); return; } const minutes Math.floor(diff / 60000); const seconds Math.floor((diff % 60000) / 1000); setTimeLeft(${minutes}分${seconds}秒); }; updateTimeLeft(); const timer setInterval(updateTimeLeft, 1000); return () clearInterval(timer); }, [approval.expiresAt]); // 处理审批操作 const handleAction async (action: string) { setLoadingAction(action); try { await rootStore.submitApproval(approval.id, action, messageId); setStatus(action deny ? denied : approved); } catch (error) { console.error(审批操作失败:, error); } finally { setLoadingAction(null); } }; // 已处理完成的状态 if (status ! pending) { return ( div className{${styles.approvalCard} ${styles[status]}} div className{styles.resultRow} {status approved ? ( CheckCircleOutlined className{styles.resultIcon} / ) : ( CloseCircleOutlined className{styles.resultIcon} / )} span className{styles.resultText} {status approved ? 已允许执行 : 已拒绝} /span /div /div ); } return ( div className{styles.approvalCard} {/* 头部 */} div className{styles.header} div className{styles.headerLeft} WarningOutlined className{styles.warningIcon} / span className{styles.title}需要你的审批/span Tag color{kindColors[approval.kind] || default} {kindNames[approval.kind] || approval.kind} /Tag /div {approval.expiresAt ( div className{styles.expiry} ClockCircleOutlined / span剩余 {timeLeft}/span /div )} /div {/* 内容描述 */} div className{styles.content} p className{styles.description}{approval.description}/p {/* 如果有命令展示命令 */} {approval.command ( Alert typewarning showIcon message将执行以下命令 description{code className{styles.command}{approval.command}/code} className{styles.commandAlert} / )} {/* 如果有文件路径 */} {approval.filePath ( div className{styles.filePath} span className{styles.fileLabel}文件路径/span code{approval.filePath}/code /div )} /div {/* 操作按钮 */} div className{styles.actions} Space {approval.allowedActions.includes(allow-once) ( Button typeprimary loading{loadingAction allow-once} onClick{() handleAction(allow-once)} 允许一次 /Button )} {approval.allowedActions.includes(allow-always) ( Button loading{loadingAction allow-always} onClick{() handleAction(allow-always)} 始终允许 /Button )} {approval.allowedActions.includes(deny) ( Button danger loading{loadingAction deny} onClick{() handleAction(deny)} 拒绝 /Button )} /Space /div /div ); }); export default ApprovalCard; // 类型定义实际项目中应该放在 types 文件里 interface ApprovalRequest { id: string; kind: string; title: string; description: string; command?: string; filePath?: string; allowedActions: string[]; expiresAt?: number; }样式src/pages/workbench/components/Conversation/ApprovalCard.less.approvalCard { background: #fffbe6; border: 1px solid #ffe58f; border-radius: radius-md; padding: spacing-md; margin: spacing-sm 0; max-width: 600px; .approved { background: #f6ffed; border-color: #b7eb8f; } .denied { background: #fff2f0; border-color: #ffccc7; } } .header { display: flex; align-items: center; justify-content: space-between; margin-bottom: spacing-sm; } .headerLeft { display: flex; align-items: center; gap: spacing-sm; } .warningIcon { color: warning-color; font-size: 18px; } .title { font-weight: 500; font-size: 15px; color: text-primary; } .expiry { font-size: 12px; color: text-secondary; display: flex; align-items: center; gap: 4px; } .content { margin-bottom: spacing-md; } .description { margin: 0 0 spacing-sm; color: text-primary; line-height: 1.6; } .commandAlert { margin-top: spacing-sm !important; :global(.ant-alert-description) { margin-top: 8px; } } .command { display: block; background: rgba(0, 0, 0, 0.06); padding: spacing-sm; border-radius: radius-sm; font-family: Courier New, monospace; word-break: break-all; margin: 0; } .filePath { font-size: 13px; color: text-secondary; margin-top: spacing-sm; .fileLabel { color: text-primary; } code { background: rgba(0, 0, 0, 0.06); padding: 2px 6px; border-radius: 4px; } } .actions { display: flex; justify-content: flex-end; } .resultRow { display: flex; align-items: center; gap: spacing-sm; } .resultIcon { font-size: 18px; .approved { color: success-color; } .denied { color: error-color; } } .resultText { font-weight: 500; .approved { color: success-color; } .denied { color: error-color; } }9.4 审批数据的存储审批请求是消息的一部分所以我们把它存在Message类型里作为消息的一个 content part。更新 Message 类型更新src/store/SessionStore.ts// 消息内容块的类型exporttypeMessagePart|{type:text;content:string}|{type:tool;toolName:string;phase:string;input?:string;output?:string}|{type:approval;approval:ApprovalRequest};// 更新 MessageexportinterfaceMessage{id:string;role:MessageRole;content:string;// 纯文本内容用于搜索等parts:MessagePart[];// 结构化内容块createdAt:string;status?:MessageStatus;}// 审批请求类型exportinterfaceApprovalRequest{id:string;kind:string;title:string;description:string;command?:string;filePath?:string;allowedActions:string[];expiresAt?:number;}设计思路消息不是单一文本而是由多个 Part 组成的。文本是一个 Part工具调用是一个 Part审批也是一个 Part。这样流式渲染的时候不同类型的 Part 可以边生成边追加非常灵活。在 SessionStore 中添加审批相关方法classSessionStore{// ... 其他属性和方法// 给消息追加审批请求addApprovalPart(messageId:string,approval:ApprovalRequest){constmsgthis.messages.find(mm.idmessageId);if(msg){msg.parts.push({type:approval,approval});}}// 更新审批状态用户操作后updateApprovalStatus(messageId:string,approvalId:string,status:approved|denied){constmsgthis.messages.find(mm.idmessageId);if(!msg)return;constapprovalPartmsg.parts.find(pp.typeapprovalp.approval.idapprovalId)asExtractMessagePart,{type:approval}|undefined;if(approvalPart){// 可以在 approval 对象上加 status 字段(approvalPart.approvalasany).statusstatus;}}}9.5 审批的实时事件审批请求是通过 WebSocket 推送过来的和流式回复一样。在 RealtimeHandler 中处理更新src/store/RealtimeHandler.ts// 在 RealtimeEvent 类型中新增exporttypeRealtimeEvent// ... 其他事件|{type:approval_request;data:{messageId:string;conversationId:string;approval:ApprovalRequest;};};// 在 handleEvent 中新增 casecaseapproval_request:this._handleApprovalRequest(event.data);break;// 新增处理方法private_handleApprovalRequest(data:any){if(this.sessionStore.sessionId!data.conversationId)return;this.sessionStore.addApprovalPart(data.messageId,data.approval);}提交审批的方法在RootStore中添加提交审批的方法asyncsubmitApproval(approvalId:string,action:string,messageId:string){try{awaitthis.gateway.submitApproval({approvalId,action,});// 本地更新状态乐观更新this.sessionStore.updateApprovalStatus(messageId,approvalId,actiondeny?denied:approved);}catch(error){console.error(审批失败:,error);throwerror;}}在网关层添加对应方法// WorkbenchGateway 接口中新增submitApproval(params:{approvalId:string;action:string}):Promisevoid;// HttpGateway 中实现asyncsubmitApproval(params:{approvalId:string;action:string}):Promisevoid{awaithttp.post(/approvals/submit,params);}9.6 安全模式权限等级除了单次审批Agent 还有不同的安全运行模式控制它的自主程度。三种安全模式模式说明适用场景完全自主所有操作都自动执行不需要审批完全信任的环境、低风险操作敏感操作审批高风险操作需要审批普通操作自动执行大多数日常场景全部审批所有操作都需要人工确认高安全要求环境、重要操作安全模式的 Store可以在 AgentStore 或单独的 SettingsStore 里管理exporttypeSecurityModefull_auto|sensitive_approval|all_approval;classSettingsStore{securityMode:SecurityModesensitive_approval;constructor(){makeAutoObservable(this);}setSecurityMode(mode:SecurityMode){this.securityModemode;// 同步到后端// gateway.setSecurityMode(mode);}}在 Composer 中切换在输入框工具栏加一个安全模式切换按钮让用户可以快速切换// 在 Composer 组件中 const handleModeChange (mode: SecurityMode) { settingsStore.setSecurityMode(mode); }; // 渲染模式选择下拉 Select value{securityMode} onChange{handleModeChange} Option valuefull_auto完全自主/Option Option valuesensitive_approval敏感审批/Option Option valueall_approval全部审批/Option /Select9.7 危险命令检测除了后端控制前端也可以加一层防护——检测用户输入的危险命令二次确认。危险命令检测函数创建src/utils/dangerousCommand.ts// 危险命令模式列表简化版实际项目会更完善constDANGEROUS_PATTERNS[{pattern:/\brm\s-rf\s(\/|\*|~)/,severity:high,description:递归删除根目录},{pattern:/\bdd\sif/,severity:high,description:dd 命令可能破坏磁盘},{pattern:/\bmkfs\b/,severity:high,description:格式化文件系统},{pattern:/\bDROP\sTABLE\b/i,severity:high,description:删除数据库表},{pattern:/\bDELETE\sFROM\s\w\s*(;|$)/i,severity:medium,description:删除表数据无 WHERE},{pattern:/\bUPDATE\s\w\sSET\s\w\s*/i,severity:medium,description:更新数据请确认 WHERE 条件},];exportinterfaceDangerCheckResult{dangerous:boolean;severity:none|low|medium|high;warnings:string[];}exportfunctioncheckDangerousCommand(input:string):DangerCheckResult{constwarnings:string[][];letmaxSeverity:none|low|medium|highnone;for(const{pattern,severity,description}ofDANGEROUS_PATTERNS){if(pattern.test(input)){warnings.push(description);if((severityhigh)||(severitymediummaxSeverity!high)||(severitylowmaxSeveritynone)){maxSeverityseverityasany;}}}return{dangerous:warnings.length0,severity:maxSeverity,warnings,};}在发送前检测在 Composer 的发送逻辑中加检测const handleSend () { // 危险命令检测 const check checkDangerousCommand(content); if (check.dangerous check.severity high) { Modal.confirm({ title: ⚠️ 检测到危险操作, icon: WarningOutlined /, content: ( div p你输入的内容包含以下危险操作/p ul {check.warnings.map((w, i) li key{i}{w}/li)} /ul p style{{ color: #ff4d4f, marginTop: 12 }} 请确认你知道自己在做什么继续执行吗 /p /div ), okText: 确认执行, okType: danger, cancelText: 取消, onOk: doSend, }); return; } doSend(); };⚠️注意前端检测只是辅助防护真正的安全控制一定要在后端做。前端检测的目的是「提醒用户」和「提升体验」不能替代后端校验。9.8 本章小结你学到了什么✅ 理解了审批机制的必要性和作用✅ 掌握了审批流程的完整链路✅ 实现了审批卡片组件✅ 学会了在消息中存储结构化内容Message Part✅ 了解了三种安全模式✅ 学会了前端危险命令检测安全机制总览安全防线 ├── 第一道前端危险命令检测提醒用户 ├── 第二道审批机制敏感操作人工确认 │ ├── 允许一次 │ ├── 始终允许加白名单 │ └── 拒绝 └── 第三道安全模式整体权限等级 ├── 完全自主 ├── 敏感操作审批 └── 全部审批✏️ 动手练习跟着本章实现审批卡片组件用 Mock 数据模拟一个审批流程看看效果增加「始终允许」功能点击后把该类型加入白名单下次自动通过实现危险命令检测功能高风险命令弹出二次确认思考一下审批过期了怎么办前端和后端分别应该怎么处理下一章我们来看更多的业务模块——定时任务、记忆管理、设置面板… 学完你就知道一个完整的工作台还需要哪些功能了。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑