feat: 升级至 v0.2.1 — 流式渲染修复、安全增强、工具自动执行
流式渲染修复: - runId 机制防止 abort 后旧流事件污染新 run - run lock 防止并发 run 污染引擎状态 - abort race 提前退出工具执行等待 - TERMINATED 状态通过 stateChange 发射 - tool_call_delta 流式参数拼接 + pending 占位替换 - 首轮卡片创建路径统一,traceStep 按 ID 精确匹配 - compressed 事件转发为 toast 通知 安全增强: - ConfirmationHook 支持持久化自动执行(跨会话) - 设置面板新增自动执行工具管理 UI - SandboxManager 双重安全校验 fail-closed - 审计日志链式哈希防篡改 - PromptInjectionDefender 中文注入标记清理 - scanCode 28 模式 + base64/$() 检测 - validatePath realpathSync 防符号链接逃逸 - code-search 使用 execFile 防命令注入 新增工具: - file_editor、code_search、task_manager、diff_viewer 其他: - Agent Loop 加 PARSING/REFLECTING 状态 + 指数退避重试 - MemoryManager TF-IDF 语义检索 - run_command Windows 中文编码修复(chcp 65001) - 版本号 0.2.0 → 0.2.1
This commit is contained in:
@@ -0,0 +1,221 @@
|
||||
/**
|
||||
* ConfirmationDialog — 工具执行确认对话框(v0.2.0 新增)
|
||||
*
|
||||
* 当 Agent 调用 requiresPermission=true 或 riskLevel>=HIGH 的工具时,
|
||||
* 通过此对话框请求用户确认。
|
||||
*
|
||||
* 监听 IPC 事件 `tool:confirmationRequest`,显示工具详情,
|
||||
* 用户确认/拒绝后通过 `tool:confirmationResponse` IPC 通道发送结果。
|
||||
*/
|
||||
|
||||
import { useState, useEffect, useCallback } from 'react';
|
||||
import {
|
||||
Dialog, DialogTitle, DialogContent, DialogActions,
|
||||
Button, Typography, Box, Chip, Alert, FormControlLabel, Checkbox,
|
||||
Accordion, AccordionSummary, AccordionDetails,
|
||||
} from '@mui/material';
|
||||
import { ShieldAlert, ChevronDown } from 'lucide-react';
|
||||
|
||||
interface ConfirmationRequest {
|
||||
toolCallId: string;
|
||||
toolName: string;
|
||||
args: Record<string, unknown>;
|
||||
riskLevel: string;
|
||||
reason: string;
|
||||
}
|
||||
|
||||
const RISK_COLORS: Record<string, 'default' | 'success' | 'warning' | 'error' | 'info'> = {
|
||||
safe: 'success',
|
||||
low: 'info',
|
||||
medium: 'warning',
|
||||
high: 'error',
|
||||
critical: 'error',
|
||||
};
|
||||
|
||||
export function ConfirmationDialog(): React.JSX.Element | null {
|
||||
const [request, setRequest] = useState<ConfirmationRequest | null>(null);
|
||||
const [remember, setRemember] = useState(false);
|
||||
const [autoExecute, setAutoExecute] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
// 监听来自主进程的确认请求(通过 preload 暴露的 metona.tool API)
|
||||
const cleanup = window.metona?.tool?.onConfirmationRequest((data: unknown) => {
|
||||
setRequest(data as ConfirmationRequest);
|
||||
setRemember(false);
|
||||
setAutoExecute(false);
|
||||
});
|
||||
return cleanup;
|
||||
}, []);
|
||||
|
||||
const handleRespond = useCallback((approved: boolean) => {
|
||||
if (!request) return;
|
||||
// 通过 preload 暴露的 metona.tool API 发送响应
|
||||
// autoExecute 仅在批准时生效(拒绝时无需持久化)
|
||||
window.metona?.tool?.sendConfirmationResponse({
|
||||
toolCallId: request.toolCallId,
|
||||
approved,
|
||||
remember,
|
||||
autoExecute: approved && autoExecute,
|
||||
});
|
||||
setRequest(null);
|
||||
}, [request, remember, autoExecute]);
|
||||
|
||||
if (!request) return null;
|
||||
|
||||
const riskColor = RISK_COLORS[request.riskLevel] ?? 'default';
|
||||
|
||||
// 格式化参数显示
|
||||
const formatArg = (key: string, value: unknown): string => {
|
||||
if (typeof value === 'string') {
|
||||
// 长字符串截断
|
||||
return value.length > 200 ? value.slice(0, 200) + '...' : value;
|
||||
}
|
||||
if (value === null) return 'null';
|
||||
if (value === undefined) return 'undefined';
|
||||
try {
|
||||
return JSON.stringify(value, null, 2);
|
||||
} catch {
|
||||
return String(value);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Dialog
|
||||
open={!!request}
|
||||
onClose={() => handleRespond(false)}
|
||||
maxWidth="sm"
|
||||
fullWidth
|
||||
slotProps={{
|
||||
paper: {
|
||||
sx: { bgcolor: 'background.paper' },
|
||||
},
|
||||
}}
|
||||
>
|
||||
<DialogTitle sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>
|
||||
<ShieldAlert size={20} color="var(--mui-palette-warning-main)" />
|
||||
<Typography variant="h6" component="span">工具执行确认</Typography>
|
||||
</DialogTitle>
|
||||
|
||||
<DialogContent>
|
||||
<Alert severity={riskColor === 'error' ? 'error' : 'warning'} sx={{ mb: 2 }}>
|
||||
{request.reason}
|
||||
</Alert>
|
||||
|
||||
<Box sx={{ mb: 2, display: 'flex', gap: 1, flexWrap: 'wrap' }}>
|
||||
<Chip
|
||||
label={`工具: ${request.toolName}`}
|
||||
color="primary"
|
||||
size="small"
|
||||
/>
|
||||
<Chip
|
||||
label={`风险: ${request.riskLevel}`}
|
||||
color={riskColor}
|
||||
size="small"
|
||||
/>
|
||||
</Box>
|
||||
|
||||
<Typography variant="body2" color="text.secondary" sx={{ mb: 1 }}>
|
||||
Agent 正在请求执行以下工具,请确认参数:
|
||||
</Typography>
|
||||
|
||||
<Accordion defaultExpanded sx={{ bgcolor: 'background.default' }}>
|
||||
<AccordionSummary expandIcon={<ChevronDown size={16} />}>
|
||||
<Typography variant="caption" sx={{ fontWeight: 600 }}>
|
||||
参数详情 ({Object.keys(request.args).length} 个参数)
|
||||
</Typography>
|
||||
</AccordionSummary>
|
||||
<AccordionDetails>
|
||||
<Box
|
||||
component="pre"
|
||||
sx={{
|
||||
fontFamily: 'monospace',
|
||||
fontSize: 11,
|
||||
color: 'text.primary',
|
||||
whiteSpace: 'pre-wrap',
|
||||
wordBreak: 'break-word',
|
||||
margin: 0,
|
||||
maxHeight: 300,
|
||||
overflow: 'auto',
|
||||
}}
|
||||
>
|
||||
{Object.entries(request.args).map(([key, value]) => (
|
||||
<Box key={key} component="div" sx={{ mb: 1 }}>
|
||||
<Typography
|
||||
component="span"
|
||||
variant="caption"
|
||||
sx={{ color: 'info.main', fontWeight: 700 }}
|
||||
>
|
||||
{key}:
|
||||
</Typography>
|
||||
<Typography
|
||||
component="span"
|
||||
variant="caption"
|
||||
sx={{ color: 'text.primary', ml: 1 }}
|
||||
>
|
||||
{formatArg(key, value)}
|
||||
</Typography>
|
||||
</Box>
|
||||
))}
|
||||
</Box>
|
||||
</AccordionDetails>
|
||||
</Accordion>
|
||||
|
||||
<FormControlLabel
|
||||
control={
|
||||
<Checkbox
|
||||
checked={remember}
|
||||
onChange={(e) => setRemember(e.target.checked)}
|
||||
size="small"
|
||||
/>
|
||||
}
|
||||
label={
|
||||
<Typography variant="caption" color="text.secondary">
|
||||
在本次会话中记住此决定(同一工具不再询问)
|
||||
</Typography>
|
||||
}
|
||||
sx={{ mt: 1 }}
|
||||
/>
|
||||
|
||||
<FormControlLabel
|
||||
control={
|
||||
<Checkbox
|
||||
checked={autoExecute}
|
||||
onChange={(e) => {
|
||||
setAutoExecute(e.target.checked);
|
||||
// 勾选永久自动执行时,自动勾选会话内记忆(保持一致)
|
||||
if (e.target.checked) setRemember(true);
|
||||
}}
|
||||
size="small"
|
||||
/>
|
||||
}
|
||||
label={
|
||||
<Typography variant="caption" color="text.secondary">
|
||||
永久自动执行此工具(跨会话不再询问,可在设置中关闭)
|
||||
</Typography>
|
||||
}
|
||||
sx={{ mt: 0.5 }}
|
||||
/>
|
||||
</DialogContent>
|
||||
|
||||
<DialogActions sx={{ px: 3, pb: 2 }}>
|
||||
<Button
|
||||
onClick={() => handleRespond(false)}
|
||||
color="error"
|
||||
variant="outlined"
|
||||
size="small"
|
||||
>
|
||||
拒绝执行
|
||||
</Button>
|
||||
<Button
|
||||
onClick={() => handleRespond(true)}
|
||||
color="success"
|
||||
variant="contained"
|
||||
size="small"
|
||||
autoFocus
|
||||
>
|
||||
确认执行
|
||||
</Button>
|
||||
</DialogActions>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user