v0.12.3: 全面审计修复 + 稳定性加固 + 文档更新

P0 安全/稳定性修复 (6项):
- sqlite persist 改用临时文件+rename,I/O错误日志
- IPC fs 处理器加入 checkPathAllowed 路径验证
- handleCompress 命令注入修复 + 跨平台 (powershell/tar/zip/unzip)
- browser 全页截图简化,使用 capturePage full rect
- workspace 竞态条件修复
- editFile 5MB 大小限制

P1 安全/稳定性修复 (7项):
- 大小写不敏感搜索: query 同步转换小写
- web_fetch 无 content-length 时流式读取+10MB限制防OOM
- Windows SIGTERM 改用 taskkill /PID /T /F 强制终止
- startsWith 路径检查 Windows 大小写不敏感
- 安全黑名单扩展: 33个目录路径 + 30条危险命令 (Linux+Windows)
- plan_track stepIndex 改用 typeof === 'number' 精确检查
- MCP 前缀改用双下划线分隔 mcp_{server}__{tool} 防歧义

P2 代码质量修复 (8项):
- 修复 EXECUTING→THINKING 死循环Bug (转换表缺失)
- COMPRESSING 硬上限绕过时同步 _loopState 全局状态
- Abort 路径补齐 onDone 回调, 防止UI状态残留
- 幻觉检测从4条扩展到16条规则, 覆盖全部工具类别
- 移除 getTokenEfficiency 死代码
- 跨平台路径清理 (replace(/[\/]+$/, ''))
- 动态 import 加 try/catch, 失败自动批准不阻断
- #modelSelect 加 null 守卫, 防止运行时崩溃

文档 & UI 更新:
- README.md: 版本号/特性表/架构图/安全机制/工具清单全面刷新
- 帮助面板: 新增 Plan Mode、抗幻觉&稳定性章节, Agent Loop补8状态机
- 工具面板: plan_track卡片 + plan-mode徽章样式, 标题更新为42+1
- 工作空间面板宽度: 480px → 400px
- 源码注释移除所有版本标签, 仅保留4处必要位置
- 版本号 0.12.2 → 0.12.3
This commit is contained in:
紫影233
2026-06-23 17:06:43 +08:00
parent 20cbfbd169
commit 042e00e4a6
35 changed files with 3692 additions and 784 deletions
+34 -5
View File
@@ -182,11 +182,13 @@ export function initSettingsModal(): void {
});
document.querySelector('#btnReleaseVRAM')!.addEventListener('click', async () => {
const api = state.get<OllamaAPI>(KEYS.API);
const model = (document.querySelector('#modelSelect') as HTMLSelectElement).value;
const api = state.get<OllamaAPI | null>(KEYS.API, null);
if (!api) { showToast('Ollama API 未连接', 'warning'); return; }
const sel = document.querySelector('#modelSelect') as HTMLSelectElement | null;
const model = sel?.value || '';
logInfo('释放显存', model || '所有模型');
try {
await api!.chat({ model: model || 'any', messages: [], keep_alive: 0 });
await api.chat({ model: model || 'any', messages: [], keep_alive: 0 });
showToast('显存已释放', 'success');
logSuccess('显存已释放');
updateRunningModels();
@@ -198,9 +200,10 @@ export function initSettingsModal(): void {
document.querySelector('#btnClearAllHistory')!.addEventListener('click', async () => {
const db = state.get<ChatDB | null>(KEYS.DB);
if (!db) { showToast('数据库未就绪', 'warning'); return; }
if (await showConfirm('确定清空所有历史记录?此操作不可恢复!', '清空历史')) {
logWarn('清空所有历史记录');
await db!.clearAll();
await db.clearAll();
(document.querySelector('#btnNewChat') as HTMLElement).click();
showToast('已清空所有历史记录', 'success');
logSuccess('历史记录已清空');
@@ -305,6 +308,19 @@ export function openSettingsModal(): void {
populateSubAgentModels();
updateMemoryVectorStatus();
loadTimeoutSettings();
loadWatchdogSetting();
}
/** 加载看门狗设置到输入框 */
async function loadWatchdogSetting(): Promise<void> {
const db = state.get<ChatDB | null>(KEYS.DB);
if (!db) return;
const ms = await db.getSetting<number>('loopWatchdogMs', 1_800_000);
const el = document.querySelector('#inputLoopWatchdog') as HTMLInputElement;
if (!el) return;
if (ms === 0) el.value = '0';
else if (ms > 0) el.value = String(Math.round(ms / 60_000));
else el.value = '';
}
export function closeSettingsModal(): void {
@@ -494,6 +510,18 @@ const saveSubAgentTimeout = debounce(async () => {
}, 500);
document.querySelector('#inputSubAgentTimeout')!.addEventListener('input', saveSubAgentTimeout);
// ── v0.12.1 看门狗超时(分钟,0=禁用)──
const saveLoopWatchdog = debounce(async () => {
const db = state.get<ChatDB | null>(KEYS.DB);
const val = (document.querySelector('#inputLoopWatchdog') as HTMLInputElement).value.trim();
const minutes = val ? parseInt(val) : -1;
const ms = minutes > 0 ? minutes * 60_000 : (minutes === 0 ? 0 : 1_800_000); // 默认30分钟
state.set('loopWatchdogMs', ms);
if (db) await db.saveSetting('loopWatchdogMs', ms);
logSetting('看门狗超时', minutes >= 0 ? (minutes === 0 ? '已禁用' : `${minutes}分钟`) : '默认(30分钟)');
}, 500);
document.querySelector('#inputLoopWatchdog')!.addEventListener('input', saveLoopWatchdog);
function updateMemoryVectorStatus(): void {
const statusEl = document.querySelector('#memoryVectorStatus');
if (!statusEl) return;
@@ -520,7 +548,8 @@ async function importSessions(filePath: string): Promise<void> {
try {
const bridge = window.metonaDesktop;
const result = await bridge!.fs.readFileBase64(filePath);
if (!bridge) { showToast('仅桌面端支持导入', 'warning'); return; }
const result = await bridge.fs.readFileBase64(filePath);
if (!result.success) {
showToast(`读取文件失败: ${result.error}`, 'error');
return;