news 2026/10/20 2:09:52

第 9 章 审批与安全机制

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
第 9 章 审批与安全机制

📌 本章目标:实现审批流程和安全机制,让 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.tsx:

import 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: Record<string, string> = { exec: 'red', file_write: 'orange', file_read: 'blue', database: 'purple', web_browse: 'green', api_call: 'cyan', }; // 审批类型的中文名称 const kindNames: Record<string, string> = { exec: '命令执行', file_write: '文件写入', file_read: '文件读取', database: '数据库操作', web_browse: '网页浏览', api_call: 'API 调用', }; const ApprovalCard = observer(({ approval, messageId }: Props) => { const { rootStore } = useStore(); const [status, setStatus] = useState<'pending' | 'approved' | 'denied'>('pending'); const [loadingAction, setLoadingAction] = useState<string | null>(null); // 计算剩余时间 const [timeLeft, setTimeLeft] = useState<string>(''); 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 type="warning" 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 type="primary" 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){constmsg=this.messages.find(m=>m.id===messageId);if(msg){msg.parts.push({type:'approval',approval});}}// 更新审批状态(用户操作后)updateApprovalStatus(messageId:string,approvalId:string,status:'approved'|'denied'){constmsg=this.messages.find(m=>m.id===messageId);if(!msg)return;constapprovalPart=msg.parts.find(p=>p.type==='approval'&&p.approval.id===approvalId)asExtract<MessagePart,{type:'approval'}>|undefined;if(approvalPart){// 可以在 approval 对象上加 status 字段(approvalPart.approvalasany).status=status;}}}

9.5 审批的实时事件

审批请求是通过 WebSocket 推送过来的,和流式回复一样。

在 RealtimeHandler 中处理

更新src/store/RealtimeHandler.ts:

// 在 RealtimeEvent 类型中新增exporttypeRealtimeEvent=// ... 其他事件|{type:'approval_request';data:{messageId:string;conversationId:string;approval:ApprovalRequest;};};// 在 handleEvent 中新增 casecase'approval_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,action==='deny'?'denied':'approved');}catch(error){console.error('审批失败:',error);throwerror;}}

在网关层添加对应方法:

// WorkbenchGateway 接口中新增submitApproval(params:{approvalId:string;action:string}):Promise<void>;// HttpGateway 中实现asyncsubmitApproval(params:{approvalId:string;action:string}):Promise<void>{awaithttp.post('/approvals/submit',params);}

9.6 安全模式(权限等级)

除了单次审批,Agent 还有不同的安全运行模式,控制它的自主程度。

三种安全模式

模式说明适用场景
完全自主所有操作都自动执行,不需要审批完全信任的环境、低风险操作
敏感操作审批高风险操作需要审批,普通操作自动执行大多数日常场景
全部审批所有操作都需要人工确认高安全要求环境、重要操作

安全模式的 Store

可以在 AgentStore 或单独的 SettingsStore 里管理:

exporttypeSecurityMode='full_auto'|'sensitive_approval'|'all_approval';classSettingsStore{securityMode:SecurityMode='sensitive_approval';constructor(){makeAutoObservable(this);}setSecurityMode(mode:SecurityMode){this.securityMode=mode;// 同步到后端// gateway.setSecurityMode(mode);}}

在 Composer 中切换

在输入框工具栏加一个安全模式切换按钮,让用户可以快速切换:

// 在 Composer 组件中 const handleModeChange = (mode: SecurityMode) => { settingsStore.setSecurityMode(mode); }; // 渲染模式选择下拉 <Select value={securityMode} onChange={handleModeChange}> <Option value="full_auto">完全自主</Option> <Option value="sensitive_approval">敏感审批</Option> <Option value="all_approval">全部审批</Option> </Select>

9.7 危险命令检测

除了后端控制,前端也可以加一层防护——检测用户输入的危险命令,二次确认。

危险命令检测函数

创建src/utils/dangerousCommand.ts:

// 危险命令模式列表(简化版,实际项目会更完善)constDANGEROUS_PATTERNS=[{pattern:/\brm\s+-rf\s+(\/|\*|~)/,severity:'high',description:'递归删除根目录'},{pattern:/\bdd\s+if=/,severity:'high',description:'dd 命令可能破坏磁盘'},{pattern:/\bmkfs\b/,severity:'high',description:'格式化文件系统'},{pattern:/\bDROP\s+TABLE\b/i,severity:'high',description:'删除数据库表'},{pattern:/\bDELETE\s+FROM\s+\w+\s*(;|$)/i,severity:'medium',description:'删除表数据(无 WHERE)'},{pattern:/\bUPDATE\s+\w+\s+SET\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'|'high'='none';for(const{pattern,severity,description}ofDANGEROUS_PATTERNS){if(pattern.test(input)){warnings.push(description);if((severity==='high')||(severity==='medium'&&maxSeverity!=='high')||(severity==='low'&&maxSeverity==='none')){maxSeverity=severityasany;}}}return{dangerous:warnings.length>0,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 本章小结

你学到了什么?

  1. ✅ 理解了审批机制的必要性和作用
  2. ✅ 掌握了审批流程的完整链路
  3. ✅ 实现了审批卡片组件
  4. ✅ 学会了在消息中存储结构化内容(Message Part)
  5. ✅ 了解了三种安全模式
  6. ✅ 学会了前端危险命令检测

安全机制总览

安全防线 ├── 第一道:前端危险命令检测(提醒用户) ├── 第二道:审批机制(敏感操作人工确认) │ ├── 允许一次 │ ├── 始终允许(加白名单) │ └── 拒绝 └── 第三道:安全模式(整体权限等级) ├── 完全自主 ├── 敏感操作审批 └── 全部审批

✏️ 动手练习

  1. 跟着本章实现审批卡片组件
  2. 用 Mock 数据模拟一个审批流程,看看效果
  3. 增加「始终允许」功能:点击后把该类型加入白名单,下次自动通过
  4. 实现危险命令检测功能,高风险命令弹出二次确认
  5. 思考一下:审批过期了怎么办?前端和后端分别应该怎么处理?

下一章我们来看更多的业务模块——定时任务、记忆管理、设置面板… 学完你就知道一个完整的工作台还需要哪些功能了。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/20 2:09:06

2027计算机专业毕设怎么选?教你用AI分析20个计算机视觉项目方向(附完整Prompt)

摘要 AI编程时代,计算机专业学生完成毕业设计,最大的困难已经不只是“不会写代码”。 很多同学真正遇到的问题是: 不知道选择什么毕业设计方向; 不知道自己的基础适合什么项目; 不知道如何规划技术路线; 不知道如何把一个深度学习模型变成完整的软件系统; 遇到代码错误…

作者头像 李华
网站建设 2026/10/20 2:08:51

Ionic Toggle 开关组件完全指南:从基础用法到实战案例

1. 引言Ionic 框架作为移动端跨平台开发的利器&#xff0c;提供了丰富且易用的 UI 组件库。其中&#xff0c;Toggle&#xff08;开关&#xff09;组件是移动端表单交互中不可或缺的元素&#xff0c;常用于设置项、权限控制、功能开关等场景。本文将深入讲解 Ionic Toggle 组件的…

作者头像 李华
网站建设 2026/10/20 2:07:55

STM32+华为云IoT+Qt实战:从零打通智能家居数据链路

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/10/20 2:07:31

主客观观测和层级感知嵌套序言| 赵杰 | 量子感知论

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/10/20 2:06:24

告别单会话限制:RDPWrap 五分钟实现免费多用户并发远程桌面

告别单会话限制&#xff1a;RDPWrap 五分钟实现免费多用户并发远程桌面 【免费下载链接】rdpwrap.ini RDPWrap.ini for RDP Wrapper Library by StasM 项目地址: https://gitcode.com/GitHub_Trending/rd/rdpwrap.ini 家里的电脑跑着重要任务&#xff0c;想远程接手时却…

作者头像 李华
网站建设 2026/10/20 2:06:18

大模型API实战:从Token计费到上下文管理,看懂DeepSeek与GLM之争

最近 AI 圈流行一句话&#xff1a;“DeepSeek 又端大号碗筷还无限续杯&#xff0c;最后还把路过的 GLM 给撑死了。”我一开始看到也愣了一下&#xff0c;以为是某个段子手在玩抽象&#xff0c;后来把这句话拆开&#xff0c;和实际的大模型 API 调用经验放在一起对照&#xff0c…

作者头像 李华