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
+378
View File
@@ -0,0 +1,378 @@
/**
* TaskList — 任务列表
*
* 显示当前会话的任务,支持新增、完成切换、删除、展开查看描述。
*/
import { useState, useEffect, useCallback } from 'react';
import {
Box, Typography, Stack, List, ListItem, ListItemIcon,
Checkbox, Chip, IconButton, TextField, Button, Select, MenuItem, Collapse,
} from '@mui/material';
import { ListChecks, Plus, Trash2, ChevronRight, Circle } from 'lucide-react';
import { useAgentStore } from '@renderer/stores/agent-store';
import { formatTime } from '@renderer/lib/formatters';
// ===== 类型 =====
type TaskStatus = MetonaTask['status'];
type TaskPriority = MetonaTask['priority'];
// ===== 常量 =====
const STATUS_LABELS: Record<TaskStatus, string> = {
pending: '待处理',
in_progress: '进行中',
completed: '已完成',
blocked: '阻塞',
cancelled: '已取消',
};
const STATUS_COLORS: Record<TaskStatus, string> = {
pending: '#f59e0b',
in_progress: '#a855f7',
completed: '#22c55e',
blocked: '#ef4444',
cancelled: '#64748b',
};
const PRIORITY_LABELS: Record<TaskPriority, string> = {
low: '低',
medium: '中',
high: '高',
critical: '紧急',
};
const PRIORITY_COLORS: Record<TaskPriority, string> = {
low: '#64748b',
medium: '#3b82f6',
high: '#f97316',
critical: '#ef4444',
};
const PRIORITY_OPTIONS: TaskPriority[] = ['low', 'medium', 'high', 'critical'];
// ===== 主组件 =====
export function TaskList(): React.JSX.Element {
const sessionId = useAgentStore((s) => s.currentSessionId);
const [tasks, setTasks] = useState<MetonaTask[]>([]);
const [error, setError] = useState<string | null>(null);
const [showAddForm, setShowAddForm] = useState(false);
const [newTitle, setNewTitle] = useState('');
const [newPriority, setNewPriority] = useState<TaskPriority>('medium');
const [expandedId, setExpandedId] = useState<string | null>(null);
const [creating, setCreating] = useState(false);
const loadTasks = useCallback(async (sid?: string) => {
if (!window.metona?.tasks?.list) return;
try {
setError(null);
const res = await window.metona.tasks.list(sid);
if (!res.success) {
setError('加载任务失败');
return;
}
const list = (res.data ?? []).slice().sort((a, b) => {
if (a.order_idx !== b.order_idx) return a.order_idx - b.order_idx;
return a.created_at - b.created_at;
});
setTasks(list);
} catch (err) {
setError((err as Error).message ?? '加载任务失败');
}
}, []);
useEffect(() => {
loadTasks(sessionId ?? undefined);
}, [sessionId, loadTasks]);
const handleCreate = async () => {
if (!sessionId) {
setError('请先选择或创建会话');
return;
}
if (!newTitle.trim()) {
setError('任务标题不能为空');
return;
}
if (!window.metona?.tasks?.create) return;
setCreating(true);
setError(null);
try {
const res = await window.metona.tasks.create({
sessionId,
title: newTitle.trim(),
priority: newPriority,
});
if (!res.success) {
setError(res.error ?? '创建任务失败');
return;
}
setNewTitle('');
setNewPriority('medium');
setShowAddForm(false);
await loadTasks(sessionId);
} catch (err) {
setError((err as Error).message ?? '创建任务失败');
} finally {
setCreating(false);
}
};
const handleToggleComplete = async (task: MetonaTask) => {
if (!window.metona?.tasks?.update) return;
const nextStatus: TaskStatus = task.status === 'completed' ? 'pending' : 'completed';
setError(null);
try {
const res = await window.metona.tasks.update(task.id, { status: nextStatus });
if (!res.success) {
setError(res.error ?? '更新任务失败');
return;
}
await loadTasks(sessionId ?? undefined);
} catch (err) {
setError((err as Error).message ?? '更新任务失败');
}
};
const handleDelete = async (id: string) => {
if (!window.metona?.tasks?.delete) return;
setError(null);
try {
const res = await window.metona.tasks.delete(id);
if (!res.success) {
setError(res.error ?? '删除任务失败');
return;
}
if (expandedId === id) setExpandedId(null);
await loadTasks(sessionId ?? undefined);
} catch (err) {
setError((err as Error).message ?? '删除任务失败');
}
};
const toggleExpand = (id: string) => {
setExpandedId(expandedId === id ? null : id);
};
return (
<Box sx={{ flex: 1, display: 'flex', flexDirection: 'column', overflow: 'hidden', minHeight: 0 }}>
<Stack direction="row" spacing={1} sx={{ mb: 1.5, flexShrink: 0, alignItems: 'center' }}>
<ListChecks size={14} style={{ color: '#22d3ee' }} />
<Typography variant="caption" sx={{ fontWeight: 600, textTransform: 'uppercase', letterSpacing: 1, color: 'text.secondary' }}>
</Typography>
<Typography variant="caption" sx={{ ml: 'auto', color: 'text.disabled', fontSize: 10 }}>
{tasks.length}
</Typography>
<IconButton
size="small"
onClick={() => setShowAddForm(!showAddForm)}
sx={{ p: 0.25, color: 'primary.main', '&:hover': { bgcolor: 'action.hover' } }}
title="新增任务"
>
<Plus size={14} />
</IconButton>
</Stack>
{error && (
<Typography
variant="caption"
sx={{
mb: 1, px: 1, py: 0.5, fontSize: 10,
color: 'error.main', bgcolor: 'error.main' + '14',
borderRadius: 1, flexShrink: 0,
}}
>
{error}
</Typography>
)}
<Collapse in={showAddForm} sx={{ flexShrink: 0 }}>
<Box sx={{ mb: 1.5, p: 1, borderRadius: 1, bgcolor: 'background.default', border: '1px solid', borderColor: 'divider' }}>
<TextField
size="small"
fullWidth
value={newTitle}
onChange={(e) => setNewTitle(e.target.value)}
onKeyDown={(e) => { if (e.key === 'Enter') handleCreate(); }}
placeholder="任务标题..."
sx={{ mb: 1, '& .MuiOutlinedInput-root': { fontSize: 11, height: 30, borderRadius: 1 } }}
/>
<Stack direction="row" spacing={1} sx={{ alignItems: 'center' }}>
<Select
size="small"
value={newPriority}
onChange={(e) => setNewPriority(e.target.value as TaskPriority)}
sx={{ flex: 1, height: 28, fontSize: 11, '& .MuiSelect-select': { py: 0.5, px: 1 } }}
>
{PRIORITY_OPTIONS.map((p) => (
<MenuItem key={p} value={p} sx={{ fontSize: 11, py: 0.5 }}>
<Stack direction="row" spacing={1} sx={{ alignItems: 'center' }}>
<Circle size={8} style={{ color: PRIORITY_COLORS[p] }} />
<Typography variant="caption" sx={{ fontSize: 11 }}>{PRIORITY_LABELS[p]}</Typography>
</Stack>
</MenuItem>
))}
</Select>
<Button
variant="contained"
size="small"
onClick={handleCreate}
disabled={creating || !newTitle.trim()}
sx={{ height: 28, fontSize: 11, minWidth: 60, textTransform: 'none' }}
>
{creating ? '...' : '创建'}
</Button>
<IconButton
size="small"
onClick={() => { setShowAddForm(false); setNewTitle(''); setNewPriority('medium'); }}
sx={{ p: 0.5, color: 'text.secondary' }}
>
<Trash2 size={12} />
</IconButton>
</Stack>
</Box>
</Collapse>
<Box sx={{ flex: 1, overflowY: 'auto', minHeight: 0 }}>
{!sessionId ? (
<Typography variant="caption" sx={{ textAlign: 'center', py: 4, display: 'block', color: 'text.disabled' }}>
</Typography>
) : tasks.length === 0 ? (
<Typography variant="caption" sx={{ textAlign: 'center', py: 4, display: 'block', color: 'text.disabled' }}>
+
</Typography>
) : (
<List disablePadding dense>
{tasks.map((task) => (
<TaskItemRow
key={task.id}
task={task}
expanded={expandedId === task.id}
onToggleComplete={() => handleToggleComplete(task)}
onToggleExpand={() => toggleExpand(task.id)}
onDelete={() => handleDelete(task.id)}
/>
))}
</List>
)}
</Box>
</Box>
);
}
// ===== 子组件 =====
function TaskItemRow({
task,
expanded,
onToggleComplete,
onToggleExpand,
onDelete,
}: {
task: MetonaTask;
expanded: boolean;
onToggleComplete: () => void;
onToggleExpand: () => void;
onDelete: () => void;
}): React.JSX.Element {
const isCompleted = task.status === 'completed';
const statusColor = STATUS_COLORS[task.status];
const priorityColor = PRIORITY_COLORS[task.priority];
return (
<ListItem
disableGutters
disablePadding
sx={{
display: 'block',
px: 0.5, py: 0.25,
borderRadius: 1,
'&:hover': { bgcolor: 'action.hover' },
'&:hover .delete-btn': { opacity: 1 },
}}
>
<Stack direction="row" spacing={0.5} sx={{ alignItems: 'flex-start' }}>
<ListItemIcon sx={{ minWidth: 20, mt: 0.25 }}>
<Checkbox
size="small"
checked={isCompleted}
onChange={onToggleComplete}
sx={{ p: 0.25, '& .MuiSvgIcon-root': { fontSize: 14 } }}
/>
</ListItemIcon>
<Box sx={{ flex: 1, minWidth: 0 }}>
<Box
onClick={onToggleExpand}
sx={{
fontSize: 11, lineHeight: 1.3, cursor: 'pointer',
color: isCompleted ? 'text.disabled' : 'text.primary',
textDecoration: isCompleted ? 'line-through' : 'none',
overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap',
}}
>
{task.title}
</Box>
<Stack direction="row" spacing={0.5} sx={{ alignItems: 'center', mt: 0.25, flexWrap: 'wrap' }}>
<Chip
label={STATUS_LABELS[task.status]}
size="small"
sx={{
height: 14, fontSize: 9,
bgcolor: statusColor + '22', color: statusColor,
'& .MuiChip-label': { px: 0.5 },
}}
/>
<Chip
label={PRIORITY_LABELS[task.priority]}
size="small"
sx={{
height: 14, fontSize: 9,
bgcolor: priorityColor + '22', color: priorityColor,
'& .MuiChip-label': { px: 0.5 },
}}
/>
<Typography
variant="caption"
component="span"
sx={{ fontSize: 9, color: 'text.disabled', fontFamily: 'monospace' }}
>
{formatTime(task.created_at)}
</Typography>
</Stack>
</Box>
<ChevronRight
size={12}
style={{
color: 'text.disabled',
transition: 'transform 150ms',
transform: expanded ? 'rotate(90deg)' : 'rotate(0deg)',
cursor: 'pointer',
marginTop: 4,
}}
onClick={onToggleExpand}
/>
<IconButton
className="delete-btn"
size="small"
onClick={onDelete}
sx={{ opacity: 0, transition: 'opacity 150ms', p: 0.25, mt: 0.25, '&:hover': { color: 'error.main' } }}
>
<Trash2 size={11} />
</IconButton>
</Stack>
<Collapse in={expanded} timeout="auto" unmountOnExit>
<Box sx={{ pl: 3.5, pr: 1, py: 0.5, fontSize: 11, color: 'text.secondary', lineHeight: 1.4, wordBreak: 'break-word' }}>
{task.description ? task.description : (
<Typography variant="caption" sx={{ fontSize: 10, color: 'text.disabled', fontStyle: 'italic' }}>
</Typography>
)}
</Box>
</Collapse>
</ListItem>
);
}