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
+46 -5
View File
@@ -1,16 +1,25 @@
/**
* DetailPanel — 右侧详情面板
*
* 布局策略:TraceViewer flex:1 撑满,TokenUsage/AgentMonitor flexShrink:0 固定
* 通过 Tab 切换显示 Trace / Memory / Tasks
*/
import { Box } from '@mui/material';
import { useState } from 'react';
import { Box, Tabs, Tab } from '@mui/material';
import { Activity, Brain, ListChecks } from 'lucide-react';
import { TraceViewer } from '@renderer/components/trace/TraceViewer';
import { TokenUsage } from '@renderer/components/trace/TokenUsage';
import { AgentMonitor } from '@renderer/components/layout/AgentMonitor';
import { MemoryViewer } from '@renderer/components/memory/MemoryViewer';
import { TaskList } from '@renderer/components/tasks/TaskList';
import { ErrorBoundary } from '@renderer/components/common/ErrorBoundary';
import { LAYOUT } from '@renderer/lib/constants';
type DetailTab = 'trace' | 'memory' | 'tasks';
export function DetailPanel(): React.JSX.Element {
const [tab, setTab] = useState<DetailTab>('trace');
return (
<Box
component="aside"
@@ -19,10 +28,42 @@ export function DetailPanel(): React.JSX.Element {
bgcolor: 'background.paper', borderLeft: 1, borderColor: 'divider', width: LAYOUT.DETAIL_WIDTH,
}}
>
<Tabs
value={tab}
onChange={(_, v) => setTab(v as DetailTab)}
variant="fullWidth"
sx={{
minHeight: 32, height: 32, flexShrink: 0,
borderBottom: 1, borderColor: 'divider',
'& .MuiTab-root': { minHeight: 32, height: 32, fontSize: 11, textTransform: 'none', py: 0, px: 1 },
'& .MuiTabs-indicator': { height: 2 },
}}
>
<Tab icon={<Activity size={12} />} iconPosition="start" label="Trace" value="trace" />
<Tab icon={<Brain size={12} />} iconPosition="start" label="Memory" value="memory" />
<Tab icon={<ListChecks size={12} />} iconPosition="start" label="Tasks" value="tasks" />
</Tabs>
<Box sx={{ p: 1.5, flex: 1, display: 'flex', flexDirection: 'column', overflow: 'hidden', minHeight: 0 }}>
<TraceViewer />
<TokenUsage />
<AgentMonitor />
{tab === 'trace' && (
<ErrorBoundary fallbackTitle="Trace 渲染失败">
<>
<TraceViewer />
<TokenUsage />
<AgentMonitor />
</>
</ErrorBoundary>
)}
{tab === 'memory' && (
<ErrorBoundary fallbackTitle="Memory 渲染失败">
<MemoryViewer />
</ErrorBoundary>
)}
{tab === 'tasks' && (
<ErrorBoundary fallbackTitle="Tasks 渲染失败">
<TaskList />
</ErrorBoundary>
)}
</Box>
</Box>
);
+1 -39
View File
@@ -7,7 +7,7 @@
import { useState, useEffect } from 'react';
import { Box, Typography, Button, IconButton, TextField, Stack, Collapse, List, ListItemButton, ListItemIcon, ListItemText, Badge, Divider, Dialog, DialogTitle, DialogContent, DialogActions } from '@mui/material';
import Fuse from 'fuse.js';
import { Plus, Search, MessageSquare, Pin, Wrench, Database, ChevronDown, ChevronRight, Trash2 } from 'lucide-react';
import { Plus, Search, MessageSquare, Pin, Wrench, ChevronDown, ChevronRight, Trash2 } from 'lucide-react';
import { useSessionStore, type Session } from '@renderer/stores/session-store';
import { useAgentStore } from '@renderer/stores/agent-store';
import { formatRelativeTime } from '@renderer/lib/formatters';
@@ -78,7 +78,6 @@ export function Sidebar(): React.JSX.Element {
<Divider sx={{ mt: 'auto', mb: 1 }} />
<ToolManagerPanel />
<MemorySearchPanel />
</Box>
</Box>
);
@@ -169,40 +168,3 @@ function ToolManagerPanel() {
</Box>
);
}
function MemorySearchPanel() {
const [expanded, setExpanded] = useState(false);
const [query, setQuery] = useState('');
const [results, setResults] = useState<Array<{ id: string; type: string; content: string; importance: number }>>([]);
const [searching, setSearching] = useState(false);
const handleSearch = async () => { if (!query.trim() || !window.metona?.memory?.search) return; setSearching(true); try { setResults((await window.metona.memory.search(query, { topK: 5 })) as any); } catch { setResults([]); } setSearching(false); };
return (
<Box>
<ListItemButton onClick={() => setExpanded(!expanded)} dense sx={{ borderRadius: 1, px: 1.5, py: 0.75 }}>
<ListItemIcon sx={{ minWidth: 24 }}>{expanded ? <ChevronDown size={12} /> : <ChevronRight size={12} />}<Database size={12} style={{ marginLeft: 4 }} /></ListItemIcon>
<ListItemText primary={<Typography variant="body2" sx={{ fontSize: 12 }}></Typography>} />
</ListItemButton>
<Collapse in={expanded}>
<Stack spacing={1} sx={{ pl: 3, pr: 1, pb: 1 }}>
<Stack direction="row" spacing={0.5}>
<TextField size="small" value={query} onChange={(e) => setQuery(e.target.value)} onKeyDown={(e) => { if (e.key === 'Enter') handleSearch(); }} placeholder="搜索记忆..." sx={{ flex: 1, '& .MuiInputBase-root': { fontSize: 11, height: 28 } }} />
<Button variant="outlined" size="small" onClick={handleSearch} disabled={searching} sx={{ minWidth: 40, height: 28, fontSize: 10 }}>{searching ? '...' : '搜索'}</Button>
</Stack>
{results.length > 0 && (
<Box sx={{ maxHeight: 200, overflowY: 'auto', display: 'flex', flexDirection: 'column', gap: 0.5 }}>
{results.map((r) => (
<Box key={r.id} sx={{ px: 1, py: 0.75, borderRadius: 1, bgcolor: 'background.default', fontSize: 10, color: 'text.secondary' }}>
<Stack direction="row" spacing={0.5} sx={{ mb: 0.25, alignItems: 'center' }}>
<Box sx={{ px: 0.5, borderRadius: 0.5, bgcolor: 'action.hover', color: 'primary.main', fontSize: 9 }}>{r.type}</Box>
<Typography variant="caption" sx={{ fontSize: 9 }}>: {r.importance.toFixed(1)}</Typography>
</Stack>
<Typography variant="caption" sx={{ fontSize: 10, display: 'block', overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}>{r.content.slice(0, 100)}</Typography>
</Box>
))}
</Box>
)}
</Stack>
</Collapse>
</Box>
);
}