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:
thzxx
2026-07-12 12:54:52 +08:00
parent 469f53b623
commit 3c5aea8fb7
41 changed files with 4972 additions and 423 deletions
+221
View File
@@ -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>
);
}