feat: 升级至 v0.3.1 — 全量代码审计修复 + 安全增强
本次升级基于完整代码审查,修复 Critical/High/Medium/Low 四级共 96 项问题, 并通过返工审计修复 10 项遗留问题,tsc 双端类型检查零错误。 Critical (10/10 完成): - C-4: command.ts 接入 shell-quote 进行 token-level 注入检测,替代原有正则匹配 可防御 r"m" -rf /、$'rm'、$(echo rm) 等字符串拼接绕过 High (11/11 完成): - 竞态保护、Promise.allSettled、AbortController 资源泄漏、IPC 参数校验等 Medium (55/55 完成): - 事务保护、敏感数据脱敏、枚举校验、MUI v9 Stack prop 迁移、 React 组件 cancelled 标志、类型收窄等 Low (20/20 完成): - 辅助方法提取(flushToolCallBuffer/scoreAndPushMemory/tryAddColumn 等) - nanoid 统一替代 Date.now()+Math.random() - confirm() 替换为 MUI Dialog、useMemo 缓存、魔法数字命名化等 返工审计修复 (10/10 完成): - L-11: LogsSettings 残留的原生 confirm()/alert() 全部替换为 MUI Dialog/Alert - M-53: MemoryViewer handleSearch 独立 ref,修复 searching 状态卡死 - M-42: 脱敏短值(length <= 4)泄露修复 - M-47: tasks:update 补全 title/description 类型校验 - L-9: ollama.adapter 非流式路径 nanoid 统一 - M-45: audit:query limit 策略与 memory:listAll 一致化 - SettingsModal handleConfirmRemove 补全 try/catch + loadServers cleanup - L-15: CommandPalette useMemo 补全 sessions 响应式依赖 - useAgentStream 事件类型补全 seq/timestamp 字段 新增依赖: shell-quote + @types/shell-quote 版本号: 0.3.0 -> 0.3.1
This commit is contained in:
@@ -6,7 +6,7 @@
|
||||
|
||||
import { Box, Typography, Stack, Table, TableBody, TableRow, TableCell, Chip } from '@mui/material';
|
||||
import { Activity, Cpu } from 'lucide-react';
|
||||
import { useEffect } from 'react';
|
||||
import { useEffect, useMemo } from 'react';
|
||||
import { useAgentStore, type AgentStatus } from '@renderer/stores/agent-store';
|
||||
import { AGENT_STATUS_COLORS, AGENT_STATUS_LABELS, PROVIDER_LABELS } from '@renderer/lib/constants';
|
||||
import { formatDuration } from '@renderer/lib/formatters';
|
||||
@@ -23,17 +23,29 @@ export function AgentMonitor(): React.JSX.Element {
|
||||
const setMaxIterations = useAgentStore((s) => s.setMaxIterations);
|
||||
|
||||
useEffect(() => {
|
||||
// M-11/M-28 修复: 添加 cancelled 标志 + 错误日志记录
|
||||
let cancelled = false;
|
||||
if (window.metona?.config?.get) {
|
||||
window.metona.config.get('agent.maxIterations').then((v) => {
|
||||
if (cancelled) return;
|
||||
if (typeof v === 'number' && v > 0) setMaxIterations(v);
|
||||
}).catch(() => {});
|
||||
}).catch((err) => {
|
||||
// M-11 修复: 记录错误而非静默吞掉,便于诊断配置加载异常
|
||||
console.error('[AgentMonitor] Failed to load maxIterations:', err);
|
||||
});
|
||||
}
|
||||
return () => { cancelled = true; };
|
||||
}, [setMaxIterations]);
|
||||
|
||||
// L-14 修复: 使用 useMemo 缓存 totalDuration,避免每次渲染都遍历 traceSteps 数组
|
||||
const totalDuration = useMemo(
|
||||
() => traceSteps.reduce((sum, s) => sum + (s.completedAt ? s.completedAt - s.startedAt : 0), 0),
|
||||
[traceSteps],
|
||||
);
|
||||
|
||||
const StatusIcon = STATUS_ICONS[agentStatus];
|
||||
const statusColor = AGENT_STATUS_COLORS[agentStatus];
|
||||
const statusLabel = AGENT_STATUS_LABELS[agentStatus];
|
||||
const totalDuration = traceSteps.reduce((sum, s) => sum + (s.completedAt ? s.completedAt - s.startedAt : 0), 0);
|
||||
|
||||
return (
|
||||
<Box sx={{ mt: 2, pt: 2, borderTop: 1, borderColor: 'divider', flexShrink: 0 }}>
|
||||
|
||||
@@ -0,0 +1,155 @@
|
||||
/**
|
||||
* Header Bar — 应用顶部栏
|
||||
*
|
||||
* H-7 修复: 规范要求布局包含 Header Bar(全宽),之前 App.tsx 缺失此组件。
|
||||
* @see docs/MetonaAI-Desktop UI UX 设计集成方案.html — 整体布局
|
||||
*
|
||||
* 布局: Header Bar (全宽) + [Sidebar | ChatPanel | DetailPanel] + StatusBar
|
||||
*
|
||||
* 功能:
|
||||
* - 应用标题/Logo
|
||||
* - 当前 Provider/Model 显示
|
||||
* - 面板切换(侧边栏、详情面板、专注模式)
|
||||
* - 主题切换
|
||||
* - 设置入口
|
||||
*/
|
||||
|
||||
import { AppBar, Toolbar, Typography, IconButton, Box, Chip, Tooltip, Divider } from '@mui/material';
|
||||
import {
|
||||
PanelLeft,
|
||||
PanelRight,
|
||||
Focus,
|
||||
Settings,
|
||||
Sun,
|
||||
Moon,
|
||||
Monitor,
|
||||
} from 'lucide-react';
|
||||
import { useUIStore, type ThemeMode } from '@renderer/stores/ui-store';
|
||||
import { useAgentStore } from '@renderer/stores/agent-store';
|
||||
|
||||
const PROVIDER_LABELS: Record<string, string> = {
|
||||
deepseek: 'DeepSeek',
|
||||
agnes: 'Agnes',
|
||||
ollama: 'Ollama',
|
||||
};
|
||||
|
||||
export function Header(): React.JSX.Element {
|
||||
const sidebarVisible = useUIStore((s) => s.sidebarVisible);
|
||||
const detailVisible = useUIStore((s) => s.detailVisible);
|
||||
const focusMode = useUIStore((s) => s.focusMode);
|
||||
const theme = useUIStore((s) => s.theme);
|
||||
const toggleSidebar = useUIStore((s) => s.toggleSidebar);
|
||||
const toggleDetail = useUIStore((s) => s.toggleDetail);
|
||||
const toggleFocusMode = useUIStore((s) => s.toggleFocusMode);
|
||||
const setTheme = useUIStore((s) => s.setTheme);
|
||||
const openSettings = useUIStore((s) => s.openSettings);
|
||||
|
||||
const provider = useAgentStore((s) => s.provider);
|
||||
const model = useAgentStore((s) => s.model);
|
||||
|
||||
const cycleTheme = () => {
|
||||
const order: ThemeMode[] = ['light', 'dark', 'auto'];
|
||||
const idx = order.indexOf(theme);
|
||||
setTheme(order[(idx + 1) % order.length]);
|
||||
};
|
||||
|
||||
const ThemeIcon = theme === 'light' ? Sun : theme === 'dark' ? Moon : Monitor;
|
||||
const themeLabel = theme === 'light' ? '浅色主题' : theme === 'dark' ? '深色主题' : '跟随系统';
|
||||
|
||||
return (
|
||||
<AppBar
|
||||
position="static"
|
||||
elevation={0}
|
||||
sx={{
|
||||
flexShrink: 0,
|
||||
borderBottom: 1,
|
||||
borderColor: 'divider',
|
||||
bgcolor: 'background.paper',
|
||||
}}
|
||||
>
|
||||
<Toolbar variant="dense" sx={{ minHeight: 48, gap: 1 }}>
|
||||
{/* 左侧:应用标题 */}
|
||||
<Typography
|
||||
variant="h6"
|
||||
component="span"
|
||||
sx={{
|
||||
fontWeight: 700,
|
||||
background: 'linear-gradient(135deg, #6366f1, #a855f7)',
|
||||
WebkitBackgroundClip: 'text',
|
||||
WebkitTextFillColor: 'transparent',
|
||||
mr: 1,
|
||||
}}
|
||||
>
|
||||
MetonaAI
|
||||
</Typography>
|
||||
|
||||
{/* Provider/Model 显示 */}
|
||||
{provider && (
|
||||
<>
|
||||
<Divider orientation="vertical" flexItem sx={{ mx: 0.5 }} />
|
||||
<Chip
|
||||
size="small"
|
||||
label={PROVIDER_LABELS[provider] ?? provider}
|
||||
color="primary"
|
||||
variant="outlined"
|
||||
/>
|
||||
{model && (
|
||||
<Typography variant="caption" color="text.secondary" sx={{ ml: 0.5 }}>
|
||||
{model}
|
||||
</Typography>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
|
||||
{/* 右侧:操作按钮 */}
|
||||
<Box sx={{ flexGrow: 1 }} />
|
||||
|
||||
<Tooltip title={sidebarVisible ? '隐藏侧边栏' : '显示侧边栏'}>
|
||||
<IconButton
|
||||
size="small"
|
||||
onClick={toggleSidebar}
|
||||
color={sidebarVisible ? 'primary' : 'default'}
|
||||
disabled={focusMode}
|
||||
>
|
||||
<PanelLeft size={18} />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
|
||||
<Tooltip title={detailVisible ? '隐藏详情面板' : '显示详情面板'}>
|
||||
<IconButton
|
||||
size="small"
|
||||
onClick={toggleDetail}
|
||||
color={detailVisible ? 'primary' : 'default'}
|
||||
disabled={focusMode}
|
||||
>
|
||||
<PanelRight size={18} />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
|
||||
<Tooltip title={focusMode ? '退出专注模式' : '进入专注模式'}>
|
||||
<IconButton
|
||||
size="small"
|
||||
onClick={toggleFocusMode}
|
||||
color={focusMode ? 'primary' : 'default'}
|
||||
>
|
||||
<Focus size={18} />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
|
||||
<Divider orientation="vertical" flexItem sx={{ mx: 0.5 }} />
|
||||
|
||||
<Tooltip title={themeLabel}>
|
||||
<IconButton size="small" onClick={cycleTheme}>
|
||||
<ThemeIcon size={18} />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
|
||||
<Tooltip title="设置">
|
||||
<IconButton size="small" onClick={openSettings}>
|
||||
<Settings size={18} />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
</Toolbar>
|
||||
</AppBar>
|
||||
);
|
||||
}
|
||||
@@ -4,7 +4,7 @@
|
||||
* 始终显示,包含会话列表、新建会话按钮、搜索。
|
||||
*/
|
||||
|
||||
import { useState, useEffect } from 'react';
|
||||
import { useState, useEffect, useMemo } 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, ChevronDown, ChevronRight, Trash2 } from 'lucide-react';
|
||||
@@ -23,14 +23,26 @@ export function Sidebar(): React.JSX.Element {
|
||||
const agentStatus = useAgentStore((s) => s.agentStatus);
|
||||
|
||||
useEffect(() => {
|
||||
if (window.metona?.sessions?.list) window.metona.sessions.list().then((list) => useSessionStore.getState().setSessions((list as any[]).map((s) => ({ id: s.id, title: s.title, createdAt: s.createdAt, updatedAt: s.updatedAt, messageCount: s.messageCount, pinned: s.pinned, archived: s.archived })))).catch(() => {});
|
||||
// M-27 修复: 添加 cancelled 标志防止组件卸载后 setState
|
||||
let cancelled = false;
|
||||
if (window.metona?.sessions?.list) {
|
||||
window.metona.sessions.list().then((list) => {
|
||||
if (cancelled) return;
|
||||
useSessionStore.getState().setSessions((list as Array<{ id: string; title: string; createdAt: number; updatedAt: number; messageCount: number; pinned: boolean; archived: boolean }>).map((s) => ({ id: s.id, title: s.title, createdAt: s.createdAt, updatedAt: s.updatedAt, messageCount: s.messageCount, pinned: s.pinned, archived: s.archived })));
|
||||
}).catch((err) => {
|
||||
// M-11 修复: 记录错误而非静默吞掉,便于诊断
|
||||
console.error('[Sidebar] Failed to load sessions:', err);
|
||||
});
|
||||
}
|
||||
return () => { cancelled = true; };
|
||||
}, []);
|
||||
|
||||
const filteredSessions = (() => {
|
||||
// L-13 修复: 使用 useMemo 缓存 filteredSessions,避免每次渲染都重建 Fuse 索引
|
||||
const filteredSessions = useMemo(() => {
|
||||
let list = sessions.filter((s) => !s.archived);
|
||||
if (searchQuery) { const fuse = new Fuse(list, { keys: ['title'], threshold: 0.4, ignoreLocation: true }); list = fuse.search(searchQuery).map((r) => r.item); }
|
||||
return list.sort((a, b) => a.pinned === b.pinned ? b.updatedAt - a.updatedAt : a.pinned ? -1 : 1);
|
||||
})();
|
||||
}, [sessions, searchQuery]);
|
||||
|
||||
const handleNewSession = async () => {
|
||||
if (window.metona?.sessions?.create) {
|
||||
@@ -40,7 +52,15 @@ export function Sidebar(): React.JSX.Element {
|
||||
setCurrentSession(r.id);
|
||||
loadSessionMessages(r.id);
|
||||
return;
|
||||
} catch {}
|
||||
} catch (err) {
|
||||
// M-9 修复: 显示错误提示而非静默吞错后创建本地假会话
|
||||
// 之前的行为:catch 后继续创建本地 s_${Date.now()} 会话,但该会话在主进程不存在,下次刷新消失
|
||||
console.error('[Sidebar] Failed to create session:', err);
|
||||
import('metona-toast').then((mod) => {
|
||||
mod.default.error('创建会话失败,请检查数据库状态');
|
||||
}).catch(() => {});
|
||||
return; // 不创建本地假会话
|
||||
}
|
||||
}
|
||||
const newSession: Session = { id: `s_${Date.now()}`, title: '新会话', createdAt: Date.now(), updatedAt: Date.now(), messageCount: 0, pinned: false, archived: false };
|
||||
useSessionStore.getState().addSession(newSession);
|
||||
@@ -91,8 +111,22 @@ function SessionItem({ session, isActive, isAgentActive, onClick }: { session: S
|
||||
setShowDeleteDialog(true);
|
||||
};
|
||||
|
||||
const confirmDelete = () => {
|
||||
window.metona?.sessions.delete(session.id).catch(() => {});
|
||||
const confirmDelete = async () => {
|
||||
// M-10 修复: 乐观更新失败时回滚,避免 UI 与数据库状态不一致
|
||||
// 之前行为:catch 静默吞错,但下一行已 removeSession,导致用户以为已删除实际未删
|
||||
if (window.metona?.sessions?.delete) {
|
||||
try {
|
||||
await window.metona.sessions.delete(session.id);
|
||||
} catch (err) {
|
||||
console.error('[Sidebar] Failed to delete session:', err);
|
||||
import('metona-toast').then((mod) => {
|
||||
mod.default.error('删除会话失败,请重试');
|
||||
}).catch(() => {});
|
||||
// 不调用 removeSession,保留会话在 UI 中(与数据库状态一致)
|
||||
setShowDeleteDialog(false);
|
||||
return;
|
||||
}
|
||||
}
|
||||
useSessionStore.getState().removeSession(session.id);
|
||||
setShowDeleteDialog(false);
|
||||
};
|
||||
@@ -137,11 +171,14 @@ function ToolManagerPanel() {
|
||||
const [tools, setTools] = useState<Array<{ name: string; description: string; category: string; riskLevel: string; requiresPermission: boolean; enabled: boolean }>>([]);
|
||||
|
||||
useEffect(() => {
|
||||
// M-54 修复: 添加 cancelled 标志,防止组件卸载后 setState
|
||||
let cancelled = false;
|
||||
if (window.metona?.tools?.list) {
|
||||
window.metona.tools.list().then((list) => {
|
||||
setTools(list as MetonaToolInfo[]);
|
||||
if (!cancelled) setTools(list as MetonaToolInfo[]);
|
||||
}).catch((err) => { console.error('[Sidebar]', err); });
|
||||
}
|
||||
return () => { cancelled = true; };
|
||||
}, []);
|
||||
|
||||
const readyCount = tools.filter((t) => t.enabled).length;
|
||||
|
||||
@@ -21,9 +21,14 @@ export function StatusBar(): React.JSX.Element {
|
||||
const [version, setVersion] = useState('v0.1.1');
|
||||
|
||||
useEffect(() => {
|
||||
// M-28 修复: 添加 cancelled 标志防止组件卸载后 setState
|
||||
let cancelled = false;
|
||||
if (window.metona?.app?.getVersion) {
|
||||
window.metona.app.getVersion().then((v) => setVersion(`v${v}`)).catch((err) => { console.error('[StatusBar]', err); });
|
||||
window.metona.app.getVersion().then((v) => {
|
||||
if (!cancelled) setVersion(`v${v}`);
|
||||
}).catch((err) => { console.error('[StatusBar]', err); });
|
||||
}
|
||||
return () => { cancelled = true; };
|
||||
}, []);
|
||||
|
||||
return (
|
||||
|
||||
Reference in New Issue
Block a user