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
+125 -7
View File
@@ -580,11 +580,51 @@ function MCPSettings() {
const [newName, setNewName] = useState('');
const [newCommand, setNewCommand] = useState('');
const [newArgs, setNewArgs] = useState('');
const loadServers = () => { if (window.metona?.mcp?.listServers) window.metona.mcp.listServers().then((l) => setServers(l as MetonaMCPServerStatus[])).catch((err) => { console.error('[SettingsModal]', err); }); };
useEffect(() => { loadServers(); }, []);
// L-11 修复: 用 MUI Dialog 替换浏览器原生 confirm(),保持 UI 一致性
const [confirmRemove, setConfirmRemove] = useState<string | null>(null);
// L-20 修复: loadServers 改为多行 async/await 写法,提升可读性
const loadServers = useCallback(async () => {
if (!window.metona?.mcp?.listServers) return;
try {
const list = await window.metona.mcp.listServers();
setServers(list as MetonaMCPServerStatus[]);
} catch (err) {
console.error('[SettingsModal]', err);
}
}, []);
// 审计补充修复: useEffect 添加 cancelled 标志,防止卸载后 setState
useEffect(() => {
let cancelled = false;
(async () => {
if (!window.metona?.mcp?.listServers) return;
try {
const list = await window.metona.mcp.listServers();
if (!cancelled) setServers(list as MetonaMCPServerStatus[]);
} catch (err) {
if (!cancelled) console.error('[SettingsModal]', err);
}
})();
return () => { cancelled = true; };
}, []);
const handleAdd = async () => { if (!newName.trim() || !newCommand.trim()) return; await window.metona?.mcp?.addServer({ name: newName.trim(), transport: 'stdio', command: newCommand.trim(), args: newArgs.trim() ? newArgs.trim().split(/\s+/) : [], enabled: true }); setNewName(''); setNewCommand(''); setNewArgs(''); setShowAdd(false); loadServers(); };
const statusColors: Record<string, string> = { connected: 'success.main', connecting: 'warning.main', disconnected: 'text.secondary', error: 'error.main' };
// L-11 修复: 确认移除 MCP 服务
// 审计补充修复: 添加 try/catch,避免 removeServer reject 时 Dialog 卡死无法关闭
const [removeError, setRemoveError] = useState<string | null>(null);
const handleConfirmRemove = async () => {
if (!confirmRemove) return;
try {
setRemoveError(null);
await window.metona?.mcp?.removeServer(confirmRemove);
setConfirmRemove(null);
loadServers();
} catch (err) {
setRemoveError((err as Error).message ?? '移除失败');
}
};
return (
<Stack spacing={2}>
<Typography variant="subtitle2" sx={{ fontWeight: 600 }}>MCP </Typography>
@@ -598,10 +638,33 @@ function MCPSettings() {
</Stack>
<Stack direction="row" spacing={0.5}>
<Button size="small" sx={{ fontSize: 10, minWidth: 40 }} onClick={async () => { await window.metona?.mcp?.toggleServer(s.name, s.status !== 'connected'); loadServers(); }}>{s.status === 'connected' ? '断开' : '连接'}</Button>
<Button size="small" color="error" sx={{ fontSize: 10, minWidth: 40 }} onClick={async () => { if (confirm(`确定移除 "${s.name}"`)) { await window.metona?.mcp?.removeServer(s.name); loadServers(); } }}></Button>
{/* L-11 修复: 点击移除打开 MUI Dialog 二次确认,而非原生 confirm() */}
<Button size="small" color="error" sx={{ fontSize: 10, minWidth: 40 }} onClick={() => setConfirmRemove(s.name)}></Button>
</Stack>
</Stack>
))}
{/* L-11 修复: MUI Dialog 替代原生 confirm() */}
<Dialog
open={confirmRemove !== null}
onClose={() => { setConfirmRemove(null); setRemoveError(null); }}
maxWidth="xs"
fullWidth
>
<DialogTitle></DialogTitle>
<DialogContent>
<Typography variant="body2">
MCP "{confirmRemove}"
</Typography>
{removeError && (
<Alert severity="error" sx={{ mt: 1, fontSize: 12 }}>{removeError}</Alert>
)}
</DialogContent>
<DialogActions>
<Button onClick={() => { setConfirmRemove(null); setRemoveError(null); }} color="inherit"></Button>
<Button onClick={handleConfirmRemove} color="error" variant="contained"></Button>
</DialogActions>
</Dialog>
{showAdd ? (
<Stack spacing={1} sx={{ p: 1.5, borderRadius: 1.5, bgcolor: 'secondary.main', border: '1px solid', borderColor: 'divider' }}>
<TextField size="small" value={newName} onChange={(e) => setNewName(e.target.value)} placeholder="服务名称" />
@@ -820,8 +883,33 @@ function AppearanceSettings({ theme, setTheme }: { theme: ThemeMode; setTheme: (
function LogsSettings() {
const [logLevel, setLogLevel] = useConfig('logging.level', 'info');
const [clearing, setClearing] = useState<string | null>(null);
// L-11 修复(审计补充): 用 MUI Dialog 替换原生 confirm() 和 alert(),保持 UI 一致性
const [confirmClear, setConfirmClear] = useState<'sessions' | 'memories' | 'auditLogs' | null>(null);
const [resultAlert, setResultAlert] = useState<{ type: 'success' | 'error'; message: string } | null>(null);
const handleExport = async () => { if (!window.metona?.data?.exportData) return; const r = await window.metona.data.exportData(); if (r.success && r.data) { const b = new Blob([JSON.stringify(r.data, null, 2)], { type: 'application/json' }); const a = document.createElement('a'); a.href = URL.createObjectURL(b); a.download = `metona-export-${Date.now()}.json`; a.click(); } };
const handleClear = async (type: 'sessions' | 'memories' | 'auditLogs') => { const labels: Record<string, string> = { sessions: '所有会话', memories: '所有记忆', auditLogs: '审计日志' }; if (!confirm(`确定清理${labels[type]}`)) return; setClearing(type); try { let r; if (type === 'sessions') r = await window.metona?.data?.clearSessions(); else if (type === 'memories') r = await window.metona?.data?.clearMemories(); else r = await window.metona?.data?.clearAuditLogs(); if (r?.success) alert(`${labels[type]}已清理`); else alert(`失败: ${r?.error}`); } catch (e) { alert(`失败: ${(e as Error).message}`); } setClearing(null); };
const labels: Record<string, string> = { sessions: '所有会话', memories: '所有记忆', auditLogs: '审计日志' };
// L-11 修复(审计补充): 清理数据改用 Dialog 确认 + Alert 反馈,不再用原生 confirm/alert
const handleClearConfirm = async () => {
if (!confirmClear) return;
const type = confirmClear;
setClearing(type);
setConfirmClear(null);
try {
let r;
if (type === 'sessions') r = await window.metona?.data?.clearSessions();
else if (type === 'memories') r = await window.metona?.data?.clearMemories();
else r = await window.metona?.data?.clearAuditLogs();
if (r?.success) {
setResultAlert({ type: 'success', message: `${labels[type]}已清理` });
} else {
setResultAlert({ type: 'error', message: `失败: ${r?.error ?? '未知错误'}` });
}
} catch (e) {
setResultAlert({ type: 'error', message: `失败: ${(e as Error).message}` });
}
setClearing(null);
};
return (
<Stack spacing={2}>
@@ -834,9 +922,39 @@ function LogsSettings() {
<Divider />
<Typography variant="caption" sx={{ fontWeight: 600, color: 'text.secondary' }}></Typography>
<Button variant="outlined" fullWidth size="small" onClick={handleExport}>📦 JSON</Button>
<Button variant="outlined" fullWidth size="small" color="error" onClick={() => handleClear('sessions')} disabled={clearing !== null}>{clearing === 'sessions' ? '清理中...' : '🗑️ 清理所有会话'}</Button>
<Button variant="outlined" fullWidth size="small" color="error" onClick={() => handleClear('memories')} disabled={clearing !== null}>{clearing === 'memories' ? '清理中...' : '🗑️ 清理所有记忆'}</Button>
<Button variant="outlined" fullWidth size="small" color="error" onClick={() => handleClear('auditLogs')} disabled={clearing !== null}>{clearing === 'auditLogs' ? '清理中...' : '🗑️ 清理审计日志'}</Button>
<Button variant="outlined" fullWidth size="small" color="error" onClick={() => { setResultAlert(null); setConfirmClear('sessions'); }} disabled={clearing !== null}>{clearing === 'sessions' ? '清理中...' : '🗑️ 清理所有会话'}</Button>
<Button variant="outlined" fullWidth size="small" color="error" onClick={() => { setResultAlert(null); setConfirmClear('memories'); }} disabled={clearing !== null}>{clearing === 'memories' ? '清理中...' : '🗑️ 清理所有记忆'}</Button>
<Button variant="outlined" fullWidth size="small" color="error" onClick={() => { setResultAlert(null); setConfirmClear('auditLogs'); }} disabled={clearing !== null}>{clearing === 'auditLogs' ? '清理中...' : '🗑️ 清理审计日志'}</Button>
{/* L-11 修复(审计补充): 清理数据确认 Dialog(替代原生 confirm() */}
<Dialog
open={confirmClear !== null}
onClose={() => setConfirmClear(null)}
maxWidth="xs"
fullWidth
>
<DialogTitle></DialogTitle>
<DialogContent>
<Typography variant="body2">
{confirmClear ? labels[confirmClear] : ''}
</Typography>
</DialogContent>
<DialogActions>
<Button onClick={() => setConfirmClear(null)} color="inherit"></Button>
<Button onClick={handleClearConfirm} color="error" variant="contained"></Button>
</DialogActions>
</Dialog>
{/* L-11 修复(审计补充): 清理结果反馈 Alert(替代原生 alert()),3 秒后自动消失 */}
{resultAlert && (
<Alert
severity={resultAlert.type}
onClose={() => setResultAlert(null)}
sx={{ fontSize: 12 }}
>
{resultAlert.message}
</Alert>
)}
</Stack>
);
}