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:
thzxx
2026-07-13 22:36:58 +08:00
parent 4f5f570ac8
commit e4d81d8247
47 changed files with 2247 additions and 475 deletions
+15 -3
View File
@@ -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 }}>
+155
View File
@@ -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>
);
}
+45 -8
View File
@@ -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;
+6 -1
View File
@@ -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 (