v0.12.5: Plan 自动追踪 + 待办完成确认 + 搜索/上下文质量加固

Fix:
- Plan Mode 自动追踪 — 工具执行成功后自动匹配步骤标记完成,不再依赖模型调用 plan_track
- 待办全部完成时注入正面确认信号,防止模型遗忘已完成的写入操作
- plan_track 提示词简化 — 移除冗余催促文本,进度自动更新
- 搜索自动抓取硬上限 MAX_AUTO_FETCH=8 + 相关性过滤
- 工作空间路径安全豁免 — 修复 AppData 黑名单自相矛盾
- 浏览器回退 LRU 缓存 — 同 URL 10min 内不重复渲染
- Plan Mode 任务描述固化到系统提示词 — 压缩后不丢失
- ephemeral 清理高负载时跳过 — Plan 进度在压缩时仍可见
- SearXNG 抓取条数输入框 min=1 max=8
This commit is contained in:
thzxx
2026-06-23 21:15:17 +08:00
parent d3c3bacb8d
commit 44cb5037ec
7 changed files with 88 additions and 12 deletions
+3 -3
View File
@@ -14,7 +14,7 @@
</p> </p>
<p align="center"> <p align="center">
<img src="https://img.shields.io/badge/version-v0.12.4-E8734A?style=flat-square" alt="version"> <img src="https://img.shields.io/badge/version-v0.12.5-E8734A?style=flat-square" alt="version">
<img src="https://img.shields.io/badge/electron-33+-47848F?style=flat-square&logo=electron" alt="electron"> <img src="https://img.shields.io/badge/electron-33+-47848F?style=flat-square&logo=electron" alt="electron">
<img src="https://img.shields.io/badge/typescript-5.7+-3178C6?style=flat-square&logo=typescript" alt="typescript"> <img src="https://img.shields.io/badge/typescript-5.7+-3178C6?style=flat-square&logo=typescript" alt="typescript">
<img src="https://img.shields.io/badge/license-MIT-green?style=flat-square" alt="license"> <img src="https://img.shields.io/badge/license-MIT-green?style=flat-square" alt="license">
@@ -256,7 +256,7 @@ npm start
ELECTRON_MIRROR=https://npmmirror.com/mirrors/electron/ npm run dist ELECTRON_MIRROR=https://npmmirror.com/mirrors/electron/ npm run dist
``` ```
产出:`release/Metona Ollama Setup v0.12.4.exe` 产出:`release/Metona Ollama Setup v0.12.5.exe`
## 🛠️ 常用命令 ## 🛠️ 常用命令
@@ -507,7 +507,7 @@ npm start
ELECTRON_MIRROR=https://npmmirror.com/mirrors/electron/ npm run dist ELECTRON_MIRROR=https://npmmirror.com/mirrors/electron/ npm run dist
``` ```
Output: `release/Metona Ollama Setup v0.12.4.exe` Output: `release/Metona Ollama Setup v0.12.5.exe`
## 🛠️ Common Commands ## 🛠️ Common Commands
+2 -2
View File
@@ -1,12 +1,12 @@
{ {
"name": "metona-ollama-desktop", "name": "metona-ollama-desktop",
"version": "0.12.4", "version": "0.12.5",
"lockfileVersion": 3, "lockfileVersion": 3,
"requires": true, "requires": true,
"packages": { "packages": {
"": { "": {
"name": "metona-ollama-desktop", "name": "metona-ollama-desktop",
"version": "0.12.4", "version": "0.12.5",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"ffmpeg-static": "^5.2.0", "ffmpeg-static": "^5.2.0",
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "metona-ollama-desktop", "name": "metona-ollama-desktop",
"version": "0.12.4", "version": "0.12.5",
"description": "Metona Ollama - TypeScript + Electron \u684c\u9762 AI \u804a\u5929\u5ba2\u6237\u7aef", "description": "Metona Ollama - TypeScript + Electron \u684c\u9762 AI \u804a\u5929\u5ba2\u6237\u7aef",
"main": "dist/main/main.js", "main": "dist/main/main.js",
"author": "thzxx", "author": "thzxx",
+1 -1
View File
@@ -101,7 +101,7 @@ export function createMenu(): void {
dialog.showMessageBox(mainWindow!, { dialog.showMessageBox(mainWindow!, {
type: 'info', type: 'info',
title: '关于 Metona Ollama', title: '关于 Metona Ollama',
message: 'Metona Ollama Desktop v0.12.4', message: 'Metona Ollama Desktop v0.12.5',
detail: 'TypeScript + Electron Ollama AI 聊天客户端\n\nhttps://gitee.com/thzxx/metona-ollama', detail: 'TypeScript + Electron Ollama AI 聊天客户端\n\nhttps://gitee.com/thzxx/metona-ollama',
icon: getIconPath() icon: getIconPath()
}); });
+1 -1
View File
@@ -28,7 +28,7 @@
<div class="header-left"> <div class="header-left">
<span class="logo">🦙</span> <span class="logo">🦙</span>
<span class="app-title">Metona Ollama</span> <span class="app-title">Metona Ollama</span>
<span class="app-version">v0.12.4</span> <span class="app-version">v0.12.5</span>
<button class="icon-btn help-btn" id="btnHelp" title="使用帮助"> <button class="icon-btn help-btn" id="btnHelp" title="使用帮助">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"> <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<circle cx="12" cy="12" r="10"/><path d="M9.09 9a3 3 0 0 1 5.83 1c0 2-3 3-3 3"/> <circle cx="12" cy="12" r="10"/><path d="M9.09 9a3 3 0 0 1 5.83 1c0 2-3 3-3 3"/>
+78 -3
View File
@@ -12,6 +12,7 @@ import {
needsConfirmation, needsConfirmation,
initPlanTracker, initPlanTracker,
getPlanTracker, getPlanTracker,
savePlanTracker,
formatPlanStatus, formatPlanStatus,
clearPlanTracker, clearPlanTracker,
} from './tool-registry.js'; } from './tool-registry.js';
@@ -538,9 +539,9 @@ async function handleInit(
systemPromptParts.push(`[Plan Mode 执行规则] systemPromptParts.push(`[Plan Mode 执行规则]
你当前处于 Plan Mode(先规划后执行)。重要规则: 你当前处于 Plan Mode(先规划后执行)。重要规则:
1. 第一轮回复必须是执行计划,不要直接调用工具。列出步骤、涉及工具和预期结果后,等待用户批准。 1. 第一轮回复必须是执行计划,不要直接调用工具。列出步骤、涉及工具和预期结果后,等待用户批准。
2. 计划批准后,每一次完成一个步骤,必须调用 plan_track(action='mark_done', step_index=N) 标记该步骤完成。 2. 计划批准后,系统会自动追踪工具执行进度——你每完成一个步骤对应的工具调用,系统会自动标记该步骤完成。
3. 每轮思考前系统会自动注入当前执行进度。请对照进度确保所有步骤最终都被标记为完成 3. 你也可以手动调用 plan_track(action='mark_done', step_index=N) 来更新进度
4. 所有步骤完成后调用 plan_track(action='mark_all_done') 或直接给出最终回答。`); 4. 所有步骤完成后直接给出最终回答。不需要手动调用 plan_track 来标记全部完成——系统会自动检测`);
// 将用户原始任务描述固化到系统提示词中(不会被压缩或清理) // 将用户原始任务描述固化到系统提示词中(不会被压缩或清理)
const taskDesc = userContent?.slice(0, 200) || ''; const taskDesc = userContent?.slice(0, 200) || '';
@@ -688,6 +689,42 @@ function extractPlanSteps(content: string): string[] {
return steps.length > 0 ? steps.slice(0, 10) : ['执行任务计划(详见上方描述)']; return steps.length > 0 ? steps.slice(0, 10) : ['执行任务计划(详见上方描述)'];
} }
/** Plan Mode 自动追踪:工具执行成功后,匹配 Plan 步骤并自动标记完成 */
function autoTrackPlanStep(toolName: string): void {
const tracker = getPlanTracker();
if (!tracker.active || tracker.steps.length === 0) return;
// 工具名 → 计划步骤关键词映射(用于自动匹配)
const TOOL_STEP_KEYWORDS: Record<string, string[]> = {
web_search: ['搜索', '查找', '检索', 'search', '查询'],
web_fetch: ['抓取', 'fetch', '获取', '浏览', '提取内容'],
read_file: ['读取', 'read', '查看', '检查'],
write_file: ['写入', 'write', '生成', '创建.*文件', '保存', '输出'],
edit_file: ['编辑', '修改', 'edit'],
list_directory: ['列出', 'list', '浏览.*目录', '查看.*结构'],
tree: ['目录树', 'tree', '结构'],
run_command: ['运行', '执行', 'run', '命令', '编译', '测试'],
git: ['git', '提交', '推送', '暂存', 'commit'],
browser_open: ['打开.*网页', '浏览器', 'browser'],
};
const keywords = TOOL_STEP_KEYWORDS[toolName] || [];
if (keywords.length === 0) return;
for (const step of tracker.steps) {
if (step.done) continue;
for (const kw of keywords) {
if (new RegExp(kw, 'i').test(step.label)) {
step.done = true;
tracker.done = tracker.steps.filter(s => s.done).length;
savePlanTracker(tracker);
logInfo(`Plan 自动追踪: ${toolName} → 步骤${step.index} "${step.label}" 已自动标记完成 (${tracker.done}/${tracker.total})`);
return; // 一个工具只标记一个步骤
}
}
}
}
/** /**
* 中途幻觉检测 — 轻量级检查 * 中途幻觉检测 — 轻量级检查
* 仅检查最关键的几类幻觉(文件写入、搜索、命令执行) * 仅检查最关键的几类幻觉(文件写入、搜索、命令执行)
@@ -772,6 +809,30 @@ function detectPendingActions(
return pending; return pending;
} }
/** 获取用户请求中提到的全部动作标签(不论是否完成),用于全部完成时生成确认信号 */
function detectAllMentionedActions(userText: string): string[] {
const txt = userText.toLowerCase();
const mentioned: string[] = [];
const ACTION_MAP: Array<{ keywords: string[]; label: string }> = [
{ keywords: ['搜索', '查找', '查一下', '搜一下', 'search', '检索', '查询'], label: '搜索/查找' },
{ keywords: ['生成', '写入', '创建文件', '保存', '写.*文件', '生成.*文件', '存入', '输出'], label: '写入文件' },
{ keywords: ['运行', '执行', 'run', '启动', '编译'], label: '执行命令' },
{ keywords: ['下载', 'download'], label: '下载文件' },
{ keywords: ['提交', 'commit', '推送', 'push'], label: 'Git 操作' },
{ keywords: ['删除', '移除', 'delete', 'remove'], label: '删除文件' },
{ keywords: ['移动', 'move', '重命名', 'rename'], label: '移动/重命名' },
{ keywords: ['复制', 'copy'], label: '复制文件' },
{ keywords: ['压缩', '解压', 'compress', 'zip', 'tar'], label: '压缩/解压' },
{ keywords: ['打开网页', '浏览器', 'browser', '截图'], label: '浏览器操作' },
];
for (const action of ACTION_MAP) {
if (action.keywords.some(kw => new RegExp(kw, 'i').test(txt))) {
mentioned.push(action.label);
}
}
return mentioned;
}
/** /**
* 通用工具结果核验 — 对所有写类工具执行后验证 * 通用工具结果核验 — 对所有写类工具执行后验证
* 读取/搜索类工具已有返回结果作为验证,此处只核验会产生副作用的操作 * 读取/搜索类工具已有返回结果作为验证,此处只核验会产生副作用的操作
@@ -934,6 +995,8 @@ async function handleThinking(
const firstUserMsg = ctx.messages.find(m => m.role === 'user'); const firstUserMsg = ctx.messages.find(m => m.role === 'user');
const userText = firstUserMsg?.content || ''; const userText = firstUserMsg?.content || '';
const pending = detectPendingActions(userText, ctx.allToolRecords); const pending = detectPendingActions(userText, ctx.allToolRecords);
// 同时获取用户提及的全部动作(用于判断是否全部完成)
const allMentioned = detectAllMentionedActions(userText);
if (pending.length > 0) { if (pending.length > 0) {
ctx.messages.push({ ctx.messages.push({
role: 'user', role: 'user',
@@ -941,6 +1004,14 @@ async function handleThinking(
ephemeral: true, ephemeral: true,
}); });
logInfo('待办追踪: 注入未完成任务提醒', pending.join(', ')); logInfo('待办追踪: 注入未完成任务提醒', pending.join(', '));
} else if (allMentioned.length > 0) {
// 全部待办已完成 → 注入正面确认信号,防止模型"遗忘"已完成的写入操作
ctx.messages.push({
role: 'user',
content: `✅ 待办检测:用户请求中的全部操作已实际执行完成(${allMentioned.join('、')})。如果检索到的信息已经足够,请直接给出最终回答,不要再重复调用已执行过的工具。`,
ephemeral: true,
});
logInfo('待办追踪: 全部完成,注入完成确认', allMentioned.join(', '));
} }
} }
@@ -1297,6 +1368,10 @@ async function handleExecuting(
} }
// ── post_tool Hook ── // ── post_tool Hook ──
executeHooks('post_tool', ctx, { toolName: record.name, toolArgs: record.arguments, toolResult: record.result! }); executeHooks('post_tool', ctx, { toolName: record.name, toolArgs: record.arguments, toolResult: record.result! });
// ── Plan Mode 自动追踪 — 工具执行成功后自动匹配 Plan 步骤标记完成 ──
if (record.status === 'success' && ctx.mode === 'plan') {
autoTrackPlanStep(record.name);
}
if (record.status === 'success') { if (record.status === 'success') {
callbacks.onToolCallResult(record.name, record.result!, batch.find(c => c.function.name === record.name)!); callbacks.onToolCallResult(record.name, record.result!, batch.find(c => c.function.name === record.name)!);
} else if (record.status === 'cancelled') { } else if (record.status === 'cancelled') {
+2 -1
View File
@@ -1041,7 +1041,8 @@ export function formatPlanStatus(): string {
return `${icon} 步骤${s.index}: ${s.label}`; return `${icon} 步骤${s.index}: ${s.label}`;
}); });
const pct = t.total > 0 ? Math.round(t.done / t.total * 100) : 0; const pct = t.total > 0 ? Math.round(t.done / t.total * 100) : 0;
return `[Plan Mode 执行进度: ${t.done}/${t.total} (${pct}%)]\n${lines.join('\n')}\n\n完成每一步后请调用 plan_track(action='mark_done', step_index=N) 标记已完成。`; const hint = t.done >= t.total ? '✅ 全部步骤已完成,请给出最终回答。' : '';
return `[Plan Mode 执行进度: ${t.done}/${t.total} (${pct}%)]\n${lines.join('\n')}${hint ? '\n\n' + hint : ''}`;
} }
export function formatToolName(name: string): string { export function formatToolName(name: string): string {