fix: 全面修复内置工具问题 + 架构缺陷修复 (v0.14.8)
P0: replace_in_files glob重写, search_files正则修复, list_directory递归分页修复, git stash/tag参数修复, buildSearchResponse query字段修复; P1: compress命令注入修复, run_command输出限制, download_file UA+重试; P2: web_fetch extract_mode/mobile_ua生效, read_multiple_files默认值对齐, CONFIRM_TOOLS扩展, 工具图标/名称映射补全; P3: random死代码清理, IPC类型补全; 架构: 系统提示词重复渲染修复, 版本号动态注入, 上下文余量字段修复, 工具记录丢失修复
This commit is contained in:
@@ -323,13 +323,17 @@ function getToolCacheKey(name: string, args: Record<string, unknown>): string {
|
||||
}
|
||||
}
|
||||
|
||||
/** 检测当前轮次是否存在重复工具调用 */
|
||||
/** 检测当前轮次是否存在重复工具调用(同一参数的工具被调用超过 1 次) */
|
||||
function isDuplicateCall(call: ToolCall, allCalls: ToolCall[]): boolean {
|
||||
const callKey = getToolCacheKey(call.function.name, call.function.arguments);
|
||||
return allCalls.some((prev, idx) => {
|
||||
if (idx === allCalls.length - 1) return false;
|
||||
return getToolCacheKey(prev.function.name, prev.function.arguments) === callKey;
|
||||
});
|
||||
let count = 0;
|
||||
for (const prev of allCalls) {
|
||||
if (getToolCacheKey(prev.function.name, prev.function.arguments) === callKey) {
|
||||
count++;
|
||||
if (count > 1) return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/** 格式化工具结果的通用默认路径 */
|
||||
@@ -453,10 +457,10 @@ function formatToolResultForModel(toolName: string, result: ToolResult): string
|
||||
if ((result as any).duplicate) {
|
||||
return `⚠️ 重复添加: ${(result as any).message || '相同内容已存在'}\n✅ 记忆操作已全部完成,不要再调用 memory 工具,直接输出最终回答。`;
|
||||
}
|
||||
// read_all / search 结果用可读文本格式,确保模型不会"看漏"
|
||||
// read_all / search 结果:包裹在 JSON 中以保持与其他工具一致的格式
|
||||
if ((result as any).action === 'read_all') {
|
||||
const entries = ((result as any).entries || []) as Array<{ id: string; type: string; content: string; importance: number; tags: string[] }>;
|
||||
if (entries.length === 0) return '记忆为空,没有任何已保存的记忆条目。';
|
||||
if (entries.length === 0) return JSON.stringify({ success: true, action: 'read_all', message: '记忆为空,没有任何已保存的记忆条目。', total: 0 });
|
||||
const grouped: Record<string, typeof entries> = {};
|
||||
for (const e of entries) {
|
||||
const t = e.type || 'fact';
|
||||
@@ -470,16 +474,16 @@ function formatToolResultForModel(toolName: string, result: ToolResult): string
|
||||
lines.push(` • [${e.type}] ${e.content}(重要性:${e.importance}, 标签: ${(e.tags || []).join(', ') || '无'})`);
|
||||
}
|
||||
}
|
||||
return lines.join('\n');
|
||||
return JSON.stringify({ success: true, action: 'read_all', formatted: lines.join('\n'), total: entries.length });
|
||||
}
|
||||
if ((result as any).action === 'search') {
|
||||
const results = ((result as any).results || []) as Array<{ id: string; type: string; content: string; importance: number; score: number }>;
|
||||
if (results.length === 0) return '未找到匹配的记忆。';
|
||||
if (results.length === 0) return JSON.stringify({ success: true, action: 'search', message: '未找到匹配的记忆。', total: 0 });
|
||||
const lines = [`[记忆搜索结果] 共 ${results.length} 条:`];
|
||||
for (const r of results) {
|
||||
lines.push(` • [${r.type || 'fact'}] ${r.content}(重要性:${r.importance}, 匹配度:${(r.score || 0).toFixed(0)})`);
|
||||
}
|
||||
return lines.join('\n');
|
||||
return JSON.stringify({ success: true, action: 'search', formatted: lines.join('\n'), total: results.length });
|
||||
}
|
||||
// 其他 action(add/replace/remove)→ 保留完整 JSON,走 default 逻辑
|
||||
return formatDefaultToolResult(toolName, result);
|
||||
@@ -1625,11 +1629,14 @@ async function handleObserving(
|
||||
}
|
||||
}
|
||||
|
||||
// ── 通用重复调用检测:工具结果含去重信号 或 同工具连续成功 2+ 次 ──
|
||||
// ── 通用重复调用检测:工具结果含去重信号 或 同工具同参数连续成功 2+ 次 ──
|
||||
const lastToolContents = ctx.messages.filter(m => m.role === 'tool').slice(-3).map(m => m.content || '');
|
||||
const hasDedupSignal = lastToolContents.some(c => c.includes('⚠️ 重复添加') || c.includes('已跳过') || c.includes('duplicate'));
|
||||
const recentSuccess = ctx.allToolRecords.filter(r => r.status === 'success').slice(-2);
|
||||
const allSameTool = recentSuccess.length >= 2 && recentSuccess.every(r => r.name === recentSuccess[0].name);
|
||||
// P0 修复:仅当工具名 AND 参数完全相同时才判定为重复(避免读两个不同文件被误杀)
|
||||
const allSameTool = recentSuccess.length >= 2
|
||||
&& recentSuccess.every(r => r.name === recentSuccess[0].name)
|
||||
&& getToolCacheKey(recentSuccess[0].name, recentSuccess[0].arguments) === getToolCacheKey(recentSuccess[1].name, recentSuccess[1].arguments);
|
||||
if (hasDedupSignal || allSameTool) {
|
||||
ctx.messages.push({
|
||||
role: 'user',
|
||||
@@ -1700,9 +1707,19 @@ async function handleReflecting(
|
||||
}
|
||||
|
||||
// ── Plan Mode: 第一轮回复 → 检测是否为计划 → 等待用户确认 ──
|
||||
if (ctx.mode === 'plan' && ctx.loopCount === 1 && ctx.toolCalls.length === 0 && ctx.content.length > 50) {
|
||||
// P1 修复:移除 ctx.toolCalls.length === 0 条件 — 模型可能忽略 Plan Mode 指令直接调用工具,
|
||||
// 此时应取消工具执行,转为等待用户确认计划
|
||||
if (ctx.mode === 'plan' && ctx.loopCount === 1 && ctx.content.length > 50) {
|
||||
const isPlanLike = /计划|步骤|step|plan|思路|方案|流程/.test(ctx.content.slice(0, 200));
|
||||
if (isPlanLike && callbacks.onPlanReady) {
|
||||
// 如果模型在第一轮就调用了工具,取消这些工具调用(Plan Mode 要求先规划后执行)
|
||||
if (ctx.toolCalls.length > 0) {
|
||||
logWarn(`Plan Mode: 模型在首轮调用了 ${ctx.toolCalls.length} 个工具,取消并转为计划确认`);
|
||||
for (const call of ctx.toolCalls) {
|
||||
callbacks.onToolCallError(call.function.name, 'Plan Mode: 首轮不应调用工具,请先输出计划', call);
|
||||
}
|
||||
ctx.toolCalls.length = 0;
|
||||
}
|
||||
const steps = extractPlanSteps(ctx.content);
|
||||
const approved = await callbacks.onPlanReady(ctx.content, steps);
|
||||
if (!approved) {
|
||||
|
||||
@@ -12,6 +12,17 @@
|
||||
import { logInfo, logDebug } from './log-service.js';
|
||||
import type { AgentMetrics, LoopContext } from '../types.js';
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════
|
||||
// 应用版本号(由 main.ts 初始化时注入,避免硬编码)
|
||||
// ═══════════════════════════════════════════════════════════════
|
||||
|
||||
let _appVersion = 'unknown';
|
||||
|
||||
/** 初始化应用版本号(应在 app 启动时调用) */
|
||||
export function setAppVersion(version: string): void {
|
||||
_appVersion = version;
|
||||
}
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════
|
||||
// 度量采集
|
||||
// ═══════════════════════════════════════════════════════════════
|
||||
@@ -268,7 +279,7 @@ export function exportMetricsJSON(): string {
|
||||
return JSON.stringify({
|
||||
timestamp: new Date().toISOString(),
|
||||
application: 'metona-ollama-desktop',
|
||||
version: '0.13.8',
|
||||
version: _appVersion,
|
||||
metrics: {
|
||||
sessions_total: metrics.totalSessions,
|
||||
avg_iterations_per_task: metrics.avgIterationsPerTask,
|
||||
|
||||
@@ -283,7 +283,7 @@ export const TOOL_DEFINITIONS: ToolDefinition[] = [
|
||||
required: ['paths'],
|
||||
properties: {
|
||||
paths: { type: 'array', items: { type: 'string' }, description: 'Array of file paths to read.' },
|
||||
max_chars_per_file: { type: 'integer', description: 'Max chars per file. Default: 2000.' }
|
||||
max_chars_per_file: { type: 'integer', description: 'Max chars per file. Default: 10000.' }
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -302,6 +302,8 @@ export const TOOL_DEFINITIONS: ToolDefinition[] = [
|
||||
files: { type: 'array', items: { type: 'string' }, description: 'Files for add/diff/checkout.' },
|
||||
message: { type: 'string', description: 'Commit message for commit action.' },
|
||||
branch: { type: 'string', description: 'Branch name for branch/checkout/merge.' },
|
||||
tag_name: { type: 'string', description: 'Tag name for tag action (creates an annotated tag if message is provided).' },
|
||||
stash_sub: { type: 'string', enum: ['push', 'pop', 'apply', 'list', 'drop'], description: 'Stash subcommand. Default: push.' },
|
||||
remote: { type: 'string', description: 'Remote name for push/pull/remote.' },
|
||||
remote_url: { type: 'string', description: 'Remote URL for remote add.' },
|
||||
count: { type: 'integer', description: 'Number of log entries. Default: 20.' },
|
||||
@@ -673,7 +675,14 @@ CRITICAL: For add action, you MUST include both "type" (fact/preference/rule) an
|
||||
}
|
||||
];
|
||||
|
||||
const CONFIRM_TOOLS = ['run_command'];
|
||||
// 需要用户确认的工具:写操作、删除、命令执行、压缩、浏览器操作
|
||||
const CONFIRM_TOOLS = [
|
||||
'run_command',
|
||||
'write_file', 'create_directory', 'delete_file',
|
||||
'edit_file', 'replace_in_files', 'move_file', 'copy_file',
|
||||
'download_file', 'compress',
|
||||
'browser_open', 'browser_click', 'browser_type', 'browser_evaluate',
|
||||
];
|
||||
|
||||
export function needsConfirmation(toolName: string): boolean {
|
||||
if (toolName === 'run_command') {
|
||||
@@ -963,7 +972,9 @@ export function getToolIcon(name: string): string {
|
||||
session_list: '📋', session_read: '📖', spawn_task: '🤖',
|
||||
plan_track: '📋',
|
||||
browser_open: '🌐', browser_screenshot: '📸', browser_evaluate: '⚡', browser_extract: '📰',
|
||||
browser_click: '👆', browser_type: '⌨️', browser_scroll: '↕️', browser_close: '❌'
|
||||
browser_click: '👆', browser_type: '⌨️', browser_scroll: '↕️', browser_close: '❌',
|
||||
browser_wait: '⏳',
|
||||
datetime: '🕐', calculator: '🔢', random: '🎲', uuid: '🔑', json_format: '📝', hash: '#️⃣'
|
||||
};
|
||||
return icons[name] || '🔧';
|
||||
}
|
||||
@@ -1039,7 +1050,9 @@ export function formatToolName(name: string): string {
|
||||
session_list: '会话列表', session_read: '读取会话', spawn_task: '子代理委派',
|
||||
plan_track: '计划追踪',
|
||||
browser_open: '打开网页', browser_screenshot: '浏览器截图', browser_evaluate: '执行JS', browser_extract: '提取内容',
|
||||
browser_click: '点击元素', browser_type: '输入文本', browser_scroll: '滚动页面', browser_close: '关闭浏览器'
|
||||
browser_click: '点击元素', browser_type: '输入文本', browser_scroll: '滚动页面', browser_close: '关闭浏览器',
|
||||
browser_wait: '等待',
|
||||
datetime: '日期时间', calculator: '计算器', random: '随机数', uuid: '生成UUID', json_format: 'JSON格式化', hash: '哈希计算'
|
||||
};
|
||||
return names[name] || name;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user