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:
@@ -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
|
||||||
|
|
||||||
|
|||||||
Generated
+2
-2
@@ -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
@@ -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
@@ -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()
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -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"/>
|
||||||
|
|||||||
@@ -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') {
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
Reference in New Issue
Block a user