v0.12.9: Completion Gate 架构修复 + 工作空间文件夹选择 + 搜索/Plan 全链路加固

Fix:
- Completion Gate read_file 误报消除 — 缩窄匹配为仅明确工具声明 + 写后读豁免
- Gate 阻断重试消息重写 — 待办中与 Gate 拦截匹配时明确要求调工具而非模糊修正
- FileWriteDedupHook (pre_tool) + PlanAutoTrackHook (post_tool) — Harness 插件化
- 工作空间浏览按钮修复 — openFile→openDirectory,原生文件夹选择对话框
- pre_tool Hook 执行结果可阻断 — HookResult.passed=false 时拦截工具执行
- Plan 步骤上限 6 + 动词过滤 + 阶段标题过滤 — 适配不同模型格式
- Plan Mode 任务描述固化到系统提示词 — 压缩后不丢失任务目标
- 全部完成信号去重 — system 消息不重复注入
- handleReflecting 空响应 pop() 精确定位最后 assistant 消息

Refactor:
- agent-engine.ts 移除全部硬编码模块变量和函数 — 迁移到 Hook 系统
- extractPlanSteps 增加 ACTION_VERBS 动词过滤
- Plan rejection 不再重置 loopCount
This commit is contained in:
thzxx
2026-06-23 22:46:00 +08:00
parent 44cb5037ec
commit 2d3ea85255
12 changed files with 194 additions and 76 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.5-E8734A?style=flat-square" alt="version"> <img src="https://img.shields.io/badge/version-v0.12.9-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.5.exe` 产出:`release/Metona Ollama Setup v0.12.9.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.5.exe` Output: `release/Metona Ollama Setup v0.12.9.exe`
## 🛠️ Common Commands ## 🛠️ Common Commands
+2 -2
View File
@@ -1,12 +1,12 @@
{ {
"name": "metona-ollama-desktop", "name": "metona-ollama-desktop",
"version": "0.12.5", "version": "0.12.9",
"lockfileVersion": 3, "lockfileVersion": 3,
"requires": true, "requires": true,
"packages": { "packages": {
"": { "": {
"name": "metona-ollama-desktop", "name": "metona-ollama-desktop",
"version": "0.12.5", "version": "0.12.9",
"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.5", "version": "0.12.9",
"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",
+8
View File
@@ -130,6 +130,14 @@ export async function setupIPC(): Promise<void> {
return result.filePath; return result.filePath;
}); });
ipcMain.handle('dialog:openDirectory', async () => {
const result = await dialog.showOpenDialog(mainWindow!, {
properties: ['openDirectory']
});
if (result.canceled) return null;
return result.filePaths;
});
ipcMain.handle('fs:readFile', async (_, filePath: string) => { ipcMain.handle('fs:readFile', async (_, filePath: string) => {
try { try {
const check = checkPathAllowed(path.resolve(filePath), 'read'); const check = checkPathAllowed(path.resolve(filePath), 'read');
+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.5', message: 'Metona Ollama Desktop v0.12.9',
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()
}); });
+2 -1
View File
@@ -19,7 +19,8 @@ contextBridge.exposeInMainWorld('metonaDesktop', {
}, },
dialog: { dialog: {
openFile: (options?: unknown) => ipcRenderer.invoke('dialog:openFile', options), openFile: (options?: unknown) => ipcRenderer.invoke('dialog:openFile', options),
saveFile: (options?: unknown) => ipcRenderer.invoke('dialog:saveFile', options) saveFile: (options?: unknown) => ipcRenderer.invoke('dialog:saveFile', options),
openDirectory: () => ipcRenderer.invoke('dialog:openDirectory')
}, },
fs: { fs: {
readFile: (filePath: string) => ipcRenderer.invoke('fs:readFile', filePath), readFile: (filePath: string) => ipcRenderer.invoke('fs:readFile', filePath),
+5 -6
View File
@@ -267,12 +267,11 @@ export function initSettingsModal(): void {
btnBrowseWorkspace.addEventListener('click', async () => { btnBrowseWorkspace.addEventListener('click', async () => {
const bridge = window.metonaDesktop; const bridge = window.metonaDesktop;
if (!bridge?.isDesktop) return; if (!bridge?.isDesktop) return;
const paths = await bridge.dialog.openFile({ const paths = await bridge.dialog.openDirectory();
filters: [{ name: '文件夹', extensions: ['*'] }] if (paths && paths.length > 0) {
}); inputWorkspaceDir.value = paths[0];
// Electron openFile 对话框不能直接选文件夹,这里用 saveFile 代替 inputWorkspaceDir.dispatchEvent(new Event('blur'));
// 或者让用户手动输入路径 }
showToast('请在输入框中直接修改路径', 'info');
}); });
// 双击输入框解锁编辑 // 双击输入框解锁编辑
inputWorkspaceDir.addEventListener('dblclick', () => { inputWorkspaceDir.addEventListener('dblclick', () => {
+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.5</span> <span class="app-version">v0.12.9</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"/>
+55 -58
View File
@@ -12,7 +12,6 @@ import {
needsConfirmation, needsConfirmation,
initPlanTracker, initPlanTracker,
getPlanTracker, getPlanTracker,
savePlanTracker,
formatPlanStatus, formatPlanStatus,
clearPlanTracker, clearPlanTracker,
} from './tool-registry.js'; } from './tool-registry.js';
@@ -679,50 +678,19 @@ function truncateByTokenBudget(text: string, maxTokens: number): string {
function extractPlanSteps(content: string): string[] { function extractPlanSteps(content: string): string[] {
const steps: string[] = []; const steps: string[] = [];
const stepRegex = /(?:^|\n)\s*(?:\d+[\.\)、]\s*|[-*]\s+)(.+)/g; const stepRegex = /(?:^|\n)\s*(?:\d+[\.\)、]\s*|[-*]\s+)(.+)/g;
// 过滤项:阶段标题、HTML 标记、纯格式描述
const filterPatterns = [/^\*\*.*阶段.*\*\*/, /\*\*第[一二三]|\*\*Phase/, /^```/, /^<\/?/, /^Stage/i];
// 动作动词:步骤必须包含至少一个表示"要做什么"的词
const ACTION_VERBS = /搜索|查找|检索|抓取|fetch|写入|write|创建|生成|运行|执行|打开|浏览|读取|下载|提交|推送|编译|压缩|解压|截图|提取|设计|构建|编码|HTML|CSS|组织|整理|归类|搜集|汇总/;
let match; let match;
while ((match = stepRegex.exec(content)) !== null) { while ((match = stepRegex.exec(content)) !== null) {
const step = match[1].trim(); const step = match[1].trim();
if (step.length > 5 && step.length < 200) { if (step.length < 5 || step.length > 200) continue;
steps.push(step); if (filterPatterns.some(p => p.test(step))) continue;
} if (!ACTION_VERBS.test(step)) continue; // 纯描述无动词,跳过
} steps.push(step);
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; // 一个工具只标记一个步骤
}
}
} }
return steps.length > 0 ? steps.slice(0, 6) : ['执行任务计划(详见上方描述)'];
} }
/** /**
@@ -1005,13 +973,17 @@ async function handleThinking(
}); });
logInfo('待办追踪: 注入未完成任务提醒', pending.join(', ')); logInfo('待办追踪: 注入未完成任务提醒', pending.join(', '));
} else if (allMentioned.length > 0) { } else if (allMentioned.length > 0) {
// 全部待办已完成 → 注入正面确认信号,防止模型"遗忘"已完成的写入操作 // 全部待办已完成 → 注入 system 级别确认信号(去重:不重复注入)
ctx.messages.push({ const alreadySignaled = ctx.messages.slice(-3).some(
role: 'user', m => m.role === 'system' && m.content?.includes('待办检测:用户请求中的全部操作已实际执行完成')
content: `✅ 待办检测:用户请求中的全部操作已实际执行完成(${allMentioned.join('、')})。如果检索到的信息已经足够,请直接给出最终回答,不要再重复调用已执行过的工具。`, );
ephemeral: true, if (!alreadySignaled) {
}); ctx.messages.push({
logInfo('待办追踪: 全部完成,注入完成确认', allMentioned.join(', ')); role: 'system',
content: `✅ 待办检测:用户请求中的全部操作已实际执行完成(${allMentioned.join('、')})。不要再调用工具,直接输出最终回答。`,
});
logInfo('待办追踪: 全部完成,注入完成确认', allMentioned.join(', '));
}
} }
} }
@@ -1287,8 +1259,17 @@ async function handleExecuting(
callbacks.onToolCallStart(call); callbacks.onToolCallStart(call);
logToolStart(call.function.name, JSON.stringify(call.function.arguments)); logToolStart(call.function.name, JSON.stringify(call.function.arguments));
// ── pre_tool Hook ── // ── pre_tool Hook(可阻断)──
await executeHooks('pre_tool', ctx, { toolName: call.function.name, toolArgs: call.function.arguments }); const hookResult = await executeHooks('pre_tool', ctx, { toolName: call.function.name, toolArgs: call.function.arguments });
if (!hookResult.allPassed) {
const reasons = hookResult.results.filter(r => !r.passed).map(r => r.message).join('; ');
logWarn(`pre_tool Hook 阻断: ${call.function.name}`, reasons);
return [{
name: call.function.name, arguments: call.function.arguments,
result: { success: true, error: '', message: reasons },
status: 'success' as const, timestamp: Date.now()
}, null];
}
await new Promise(r => requestAnimationFrame(r)); await new Promise(r => requestAnimationFrame(r));
@@ -1368,10 +1349,6 @@ 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') {
@@ -1513,7 +1490,13 @@ async function handleReflecting(
// ── 异常1: 空响应 + 有工具历史 → 模型可能困惑 → 注入提示 ── // ── 异常1: 空响应 + 有工具历史 → 模型可能困惑 → 注入提示 ──
if (ctx.toolCalls.length === 0 && !ctx.content.trim() && ctx.loopCount > 1 && ctx.allToolRecords.length > 0) { if (ctx.toolCalls.length === 0 && !ctx.content.trim() && ctx.loopCount > 1 && ctx.allToolRecords.length > 0) {
logWarn('模型返回空内容(有未处理的工具结果),注入继续提示'); logWarn('模型返回空内容(有未处理的工具结果),注入继续提示');
ctx.messages.pop(); // 精准移除最后一条 assistant 消息(而非盲目 pop
for (let i = ctx.messages.length - 1; i >= 0; i--) {
if (ctx.messages[i].role === 'assistant') {
ctx.messages.splice(i, 1);
break;
}
}
ctx.messages.push({ ctx.messages.push({
role: 'user', role: 'user',
content: '请根据上面的工具调用结果继续回答。如果需要更多信息,可以继续调用工具。如果已有足够信息,请给出最终回答。' content: '请根据上面的工具调用结果继续回答。如果需要更多信息,可以继续调用工具。如果已有足够信息,请给出最终回答。'
@@ -1537,7 +1520,6 @@ async function handleReflecting(
content: '已达到最大计划重试次数。请基于当前计划直接开始执行任务。', content: '已达到最大计划重试次数。请基于当前计划直接开始执行任务。',
ephemeral: true, ephemeral: true,
}); });
ctx.loopCount = 0;
transition(ctx, S.THINKING); transition(ctx, S.THINKING);
return; return;
} }
@@ -1547,7 +1529,6 @@ async function handleReflecting(
content: '请基于反馈重新规划。调整你的方案后再次输出计划。', content: '请基于反馈重新规划。调整你的方案后再次输出计划。',
ephemeral: true, ephemeral: true,
}); });
ctx.loopCount = 0;
transition(ctx, S.THINKING); transition(ctx, S.THINKING);
return; return;
} }
@@ -1603,9 +1584,25 @@ async function handleReflecting(
if (gateResult.critical) { if (gateResult.critical) {
// 阻断级失败:强制模型重新回答 // 阻断级失败:强制模型重新回答
logWarn(`Completion Gate 🔴阻断: ${gateResult.reason}`); logWarn(`Completion Gate 🔴阻断: ${gateResult.reason}`);
// 检测是否所有待办任务已完成 — 若是,则只要求修改措辞;若待办中仍有被 Gate 拦截的操作,则明确要求调用工具
const firstUserMsg = ctx.messages.find(m => m.role === 'user');
const userText = firstUserMsg?.content || '';
const stillPending = detectPendingActions(userText, ctx.allToolRecords);
const gateReason = gateResult.reason || '';
let retryMsg: string;
if (stillPending.length === 0) {
retryMsg = `⚠️ 完成检查发现问题: ${gateReason}\n注意:你的所有工具调用已经实际完成。请只修改文字措辞消除上述问题,**绝对不要**调用任何新工具。直接重新输出修正后的文字即可。`;
} else if (gateReason.includes('写入') && stillPending.some(p => p.includes('写入'))) {
// 关键:模型声称写了文件但实际没调 write_file → 明确要求调用工具
retryMsg = `⚠️ 你还没有实际调用 write_file 工具写入文件,但回复中声称已写入。\n请**立即调用 write_file** 工具将内容写入工作空间,不要只在文字中描述。\n待完成任务: ${stillPending.join('、')}`;
} else if (gateReason.includes('搜索') && stillPending.some(p => p.includes('搜索'))) {
retryMsg = `⚠️ 你还没有实际调用 web_search 工具,但回复中声称已搜索。\n请**立即调用 web_search** 工具执行搜索。\n待完成任务: ${stillPending.join('、')}`;
} else {
retryMsg = `⚠️ 完成检查发现问题: ${gateReason}\n请修正后重新给出回答。待完成任务: ${stillPending.join('、')}`;
}
ctx.messages.push({ ctx.messages.push({
role: 'user', role: 'user',
content: `⚠️ 完成检查发现严重问题: ${gateResult.reason}\n请修正后重新给出回答。`, content: retryMsg,
ephemeral: true, ephemeral: true,
}); });
transition(ctx, S.THINKING); transition(ctx, S.THINKING);
+7 -3
View File
@@ -144,9 +144,9 @@ const toolHallucinationCheck: CompletionCheck = {
{ patterns: [/已写入文件/, /已创建文件/, /文件已生成/, /已保存到/, /已成功创建/, /成功写入/, { patterns: [/已写入文件/, /已创建文件/, /文件已生成/, /已保存到/, /已成功创建/, /成功写入/,
/文件.*已.*创建/, /文件.*已.*写入/, /生成.*文件/, /写入.*工作空间/], /文件.*已.*创建/, /文件.*已.*写入/, /生成.*文件/, /写入.*工作空间/],
requiredTools: ['write_file'], label: '写入/创建文件' }, requiredTools: ['write_file'], label: '写入/创建文件' },
// 文件读取 // 文件读取(仅匹配明确声称调用了 read_file 工具的场景)
{ patterns: [/已读取文件/, /文件内容显示/, /读取了.*文件/, /文件.*内容.*如下/, { patterns: [/用read_file读取了/, /调用read_file.*读取/, /通过read_file工具/,
/查看.*文件.*内容/, /打开文件.*看到/], /使用read_file.*查看了/, /read_file.*工具.*读取了/],
requiredTools: ['read_file', 'read_multiple_files'], label: '读取文件' }, requiredTools: ['read_file', 'read_multiple_files'], label: '读取文件' },
// 文件编辑 // 文件编辑
{ patterns: [/已修改文件/, /已编辑文件/, /文件已更新/, /已替换.*内容/, { patterns: [/已修改文件/, /已编辑文件/, /文件已更新/, /已替换.*内容/,
@@ -208,6 +208,10 @@ const toolHallucinationCheck: CompletionCheck = {
if (pattern.test(content)) { if (pattern.test(content)) {
const anyCalled = rule.requiredTools.some(t => calledTools.has(t)); const anyCalled = rule.requiredTools.some(t => calledTools.has(t));
if (!anyCalled) { if (!anyCalled) {
// 写后读豁免:write_file 已调用时,read_file 的声称通常是读取写入结果的自然表述,不阻断
if (rule.label === '读取文件' && calledTools.has('write_file')) {
return { passed: true, reason: '' };
}
const toolList = rule.requiredTools.join(' / '); const toolList = rule.requiredTools.join(' / ');
return { return {
passed: false, passed: false,
+108
View File
@@ -218,6 +218,112 @@ export const iterationMetricsHook: HarnessHook = {
}, },
}; };
// ═══════════════════════════════════════════════════════════════
// Harness Hook: 文件写入去重
// ═══════════════════════════════════════════════════════════════
/** 会话级已写入文件路径集合 — 由 FileWriteDedupHook 和 PlanAutoTrackHook 共同维护 */
const _writtenFilesThisSession = new Set<string>();
/**
* FileWriteDedupHook — write_file 同路径去重(pre_tool 阻断级)
* 同一文件路径在本会话中已成功写入后,后续写入被拦截,
* 防止模型在设计迭代中反复覆盖同一文件。
*/
export const fileWriteDedupHook: HarnessHook = {
name: 'FileWriteDedup',
phase: 'pre_tool',
priority: 95,
enabled: true,
handler: async (_ctx: LoopContext, data: HookData) => {
if (data.toolName !== 'write_file') return { passed: true, message: '' };
const filePath = String(data.toolArgs?.path || '');
if (!filePath) return { passed: true, message: '' };
if (_writtenFilesThisSession.has(filePath)) {
return { passed: false, message: `文件 "${filePath}" 已在本会话中写入,无需重复覆盖。请直接给出最终回答。` };
}
return { passed: true, message: '' };
},
};
/**
* PlanAutoTrackHook — Plan Mode 步骤自动追踪(post_tool 非阻断级)
* 工具执行成功后自动匹配 Plan 步骤标记完成,不再依赖模型手动调用 plan_track。
* write_file 成功后还会自动标记所有"设计/构建/编码"类步骤。
*/
export const planAutoTrackHook: HarnessHook = {
name: 'PlanAutoTrack',
phase: 'post_tool',
priority: 80,
enabled: true,
handler: async (ctx: LoopContext, data: HookData) => {
if (ctx.mode !== 'plan') return { passed: true, message: '' };
const toolName = data.toolName || '';
const toolResult = data.toolResult as Record<string, unknown> | undefined;
if (!toolResult?.success) return { passed: true, message: '' };
// 延迟导入避免循环依赖
const { getPlanTracker, savePlanTracker } = await import('./tool-registry.js');
const tracker = getPlanTracker();
if (!tracker.active || tracker.steps.length === 0) return { passed: true, message: '' };
// 工具名 → 计划步骤关键词映射
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] || [];
let matched = false;
if (keywords.length > 0) {
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.slice(0, 40)}" (${tracker.done}/${tracker.total})`);
matched = true;
break;
}
}
if (matched) break;
}
}
// write_file 成功后,自动标记所有"设计/构建/编码"类步骤
if (toolName === 'write_file') {
const DESIGN_KEYWORDS = /HTML|CSS|构建|编码|设计|代码|前端|布局|排版|样式|视觉|coding|design|style/i;
for (const step of tracker.steps) {
if (step.done) continue;
if (DESIGN_KEYWORDS.test(step.label)) {
step.done = true;
tracker.done = tracker.steps.filter(s => s.done).length;
savePlanTracker(tracker);
logInfo(`Plan 自动追踪: write_file → 设计步骤${step.index} "${step.label.slice(0, 40)}" (${tracker.done}/${tracker.total})`);
}
}
}
// write_file 成功后记录文件路径,供 FileWriteDedupHook 拦截
if (toolName === 'write_file' && data.toolArgs?.path) {
_writtenFilesThisSession.add(String(data.toolArgs.path));
}
return { passed: true, message: '' };
},
};
// ═══════════════════════════════════════════════════════════════ // ═══════════════════════════════════════════════════════════════
// 初始化:注册所有内置 Hook // 初始化:注册所有内置 Hook
// ═══════════════════════════════════════════════════════════════ // ═══════════════════════════════════════════════════════════════
@@ -229,6 +335,8 @@ export function initHarnessHooks(): void {
registerHook(securityCheckHook); registerHook(securityCheckHook);
registerHook(resultValidationHook); registerHook(resultValidationHook);
registerHook(iterationMetricsHook); registerHook(iterationMetricsHook);
registerHook(fileWriteDedupHook);
registerHook(planAutoTrackHook);
hooksInitialized = true; hooksInitialized = true;
logInfo('Harness Hook 系统已初始化', `${hookRegistry.size} 个 Hook 已注册`); logInfo('Harness Hook 系统已初始化', `${hookRegistry.size} 个 Hook 已注册`);
} }
+1
View File
@@ -224,6 +224,7 @@ export interface MetonaDesktopAPI {
dialog: { dialog: {
openFile: (options?: FileFilterOptions) => Promise<string[] | null>; openFile: (options?: FileFilterOptions) => Promise<string[] | null>;
saveFile: (options?: SaveFileOptions) => Promise<string | null>; saveFile: (options?: SaveFileOptions) => Promise<string | null>;
openDirectory: () => Promise<string[] | null>;
}; };
fs: { fs: {
readFile: (filePath: string) => Promise<{ success: boolean; content?: string; name?: string; error?: string }>; readFile: (filePath: string) => Promise<{ success: boolean; content?: string; name?: string; error?: string }>;