feat: v4.0.0 大版本升级 - SQLite3 + ReAct Agent
- P0: 存储层 IndexedDB → SQLite3 (better-sqlite3),7张表 + FTS5全文搜索 - P1: Agent Engine 升级为 ReAct 模式(思考→行动→观察→反思) - P2: 新增 4 个工具(memory_search, memory_add, session_list, session_read) - P3: 上下文管理器(滑动窗口 + 摘要压缩 + 记忆注入) - P4: UI 改版(ReAct 执行面板样式 + 思考过程卡片) - P5: IndexedDB → SQLite 数据迁移支持 - MAX_LOOPS 10→15, MAX_LOOP_TIME 5min→10min, 错误自动重试 2 次
This commit is contained in:
@@ -1,6 +1,7 @@
|
||||
/**
|
||||
* Agent Engine - Agent Loop 核心引擎
|
||||
* 管理带工具调用的流式对话循环
|
||||
* Agent Engine - ReAct Agent Loop 核心引擎 (v4.0)
|
||||
* ReAct 模式: Thought → Action → Observation → Reflection
|
||||
* 支持任务复杂度评估、自动规划、错误重试、执行轨迹记录
|
||||
*/
|
||||
|
||||
import { OllamaAPI } from '../api/ollama.js';
|
||||
@@ -12,35 +13,79 @@ import {
|
||||
} from './tool-registry.js';
|
||||
import { searchMemories, buildMemoryContext, markMemoryUsed, isMemoryEnabled } from './memory-manager.js';
|
||||
import { showToast } from '../components/toast.js';
|
||||
import { logInfo, logWarn, logError, logToolStart, logToolResult, logThink, logStream, logAgentLoop, logModelResponse } from './log-service.js';
|
||||
import { logInfo, logWarn, logError, logToolStart, logToolResult, logThink, logAgentLoop, logModelResponse } from './log-service.js';
|
||||
import { getWorkspaceDirPath } from '../components/workspace-panel.js';
|
||||
import { generateId } from '../utils/utils.js';
|
||||
import type {
|
||||
OllamaMessage,
|
||||
OllamaStreamChunk,
|
||||
ToolCall,
|
||||
ToolResult,
|
||||
ToolCallRecord
|
||||
ToolCallRecord,
|
||||
TraceEntry
|
||||
} from '../types.js';
|
||||
|
||||
const MAX_LOOPS = 10;
|
||||
const MAX_LOOP_TIME = 300000; // 5 分钟总超时
|
||||
const TOOL_EXEC_TIMEOUT = 30000; // 工具执行超时 30 秒
|
||||
const STREAM_TIMEOUT = 120000; // 单次流式调用超时 2 分钟
|
||||
const MAX_LOOPS = 15;
|
||||
const MAX_LOOP_TIME = 600000; // 10 分钟总超时
|
||||
const TOOL_EXEC_TIMEOUT = 30000; // 工具执行超时 30 秒
|
||||
const STREAM_TIMEOUT = 120000; // 单次流式调用超时 2 分钟
|
||||
const MAX_RETRIES = 2; // 工具错误自动重试次数
|
||||
|
||||
const REACT_SYSTEM_PROMPT = `你是一个具备 ReAct(Reasoning + Acting)能力的 AI 助手。你必须严格按照以下格式与用户交互:
|
||||
|
||||
## ReAct 输出格式
|
||||
|
||||
每次响应时,你必须按照以下结构思考和行动:
|
||||
|
||||
**Thought:** [你对当前任务的分析和推理。思考用户需要什么、当前有哪些信息、下一步应该做什么。]
|
||||
|
||||
**Action:** [如果需要使用工具,选择一个工具并说明原因。如果不需要工具,直接给出最终答案。]
|
||||
|
||||
**Action Input:** [工具的参数,必须是合法 JSON 格式]
|
||||
|
||||
然后等待 Observation(工具返回的结果)。
|
||||
|
||||
收到 Observation 后:
|
||||
|
||||
**Thought:** [基于工具结果的进一步推理。结果是否满足需求?是否需要更多步骤?]
|
||||
|
||||
**Action:** [下一步行动,或者 "Final Answer" 如果任务已完成]
|
||||
|
||||
**Action Input:** [下一步工具参数,或者最终答案]
|
||||
|
||||
## 任务复杂度评估
|
||||
|
||||
在开始时评估任务:
|
||||
- **简单**:直接回答(1 步),不需要工具或仅需 1 个工具调用
|
||||
- **中等**:需要 2-4 步,可能需要多个工具协作
|
||||
- **复杂**:需要 5+ 步,需要规划和分解
|
||||
|
||||
对于复杂任务,先输出一个简短的执行计划,再开始 ReAct 循环。
|
||||
|
||||
## 错误处理
|
||||
|
||||
- 如果工具返回错误,分析原因并尝试不同方法
|
||||
- 最多重试 2 次,如果仍然失败,向用户说明情况
|
||||
- 不要反复调用相同参数的相同工具
|
||||
|
||||
## 最终答案
|
||||
|
||||
当任务完成时,输出 **Final Answer:** 后面跟你的最终回答。回答应该:
|
||||
- 汇总所有执行步骤和结果
|
||||
- 直接回答用户的问题
|
||||
- 如果有重要发现,明确指出`;
|
||||
|
||||
const TOOL_USAGE_GUIDE = `【工具使用规则】
|
||||
1. 路径上下文:当用户说"修改这个文件"、"删除它"、"查看里面"等未指定路径时,必须从本次对话中最近的工具调用结果中提取路径。例如用户之前让你读取了 /home/user/project/config.json,后续说"修改配置"就应继续使用该路径,而不是推测新路径。
|
||||
1. 路径上下文:当用户说"修改这个文件"、"删除它"、"查看里面"等未指定路径时,必须从本次对话中最近的工具调用结果中提取路径。
|
||||
2. 工具调用结果中的 path 字段是权威的文件路径,优先使用它。
|
||||
3. 如果对话中从未出现过相关路径,应先用 search_files 或 list_directory 定位,不要凭空猜测路径。
|
||||
4. 对同一文件的连续操作(读取→修改、读取→删除),路径必须一致。
|
||||
5. 当用户要求执行命令时,使用 run_command 工具执行。命令通过工作空间终端实时执行并显示,执行完成后将 stdout/stderr 结果告知用户。
|
||||
6. 联网搜索联动:当用户询问需要最新信息的问题时(如版本号、新闻、价格等),先用 web_search 搜索,然后必须从搜索结果中选择最相关的 1-3 个 URL,用 web_fetch 抓取页面详细内容获取准确数据。不要仅凭搜索摘要回答——摘要往往不完整,必须抓取原文才能给出准确答案。
|
||||
7. 主动使用工具:当你不确定答案、信息可能已过时、或需要查阅实时数据时,主动调用工具而不是凭记忆猜测。以下场景必须使用工具:
|
||||
- 用户问"最新版本"、"现在多少钱"、"最近有什么新闻" → web_search + web_fetch
|
||||
- 用户让你查看/修改/分析文件 → read_file / list_directory / search_files
|
||||
- 用户问项目状态、代码内容 → git status / read_file
|
||||
- 用户让你执行操作 → run_command / write_file 等
|
||||
宁可多调一次工具,也不要给出过时或编造的信息。
|
||||
8. 严禁重复调用:如果本次对话中已经成功调用过某个工具并获得了结果,不要再次调用相同工具和相同参数。直接使用已有结果继续工作。例如:已经 web_fetch 抓取了页面内容,下一步应直接处理该内容(如 write_file 保存),而不是重新抓取。`;
|
||||
5. 当用户要求执行命令时,使用 run_command 工具执行。
|
||||
6. 联网搜索联动:当用户询问需要最新信息的问题时,先用 web_search 搜索,然后必须从搜索结果中选择最相关的 1-3 个 URL,用 web_fetch 抓取页面详细内容。
|
||||
7. 主动使用工具:当你不确定答案、信息可能已过时、或需要查阅实时数据时,主动调用工具而不是凭记忆猜测。
|
||||
8. 严禁重复调用:如果本次对话中已经成功调用过某个工具并获得了结果,不要再次调用相同工具和相同参数。
|
||||
9. 记忆工具:你可以使用 memory_search 搜索过去的记忆,使用 memory_add 添加新的重要记忆。
|
||||
10. 会话工具:你可以使用 session_list 列出历史会话,使用 session_read 读取历史会话内容。`;
|
||||
|
||||
/** 工具调用缓存:key = toolName + JSON(args),用于避免重复调用 */
|
||||
const toolResultCache = new Map<string, ToolResult>();
|
||||
@@ -48,7 +93,6 @@ const toolResultCache = new Map<string, ToolResult>();
|
||||
/** 生成工具调用缓存 key */
|
||||
function getToolCacheKey(name: string, args: Record<string, unknown>): string {
|
||||
try {
|
||||
// 对 arguments 排序 key 以保证一致性
|
||||
return name + '::' + JSON.stringify(args, Object.keys(args).sort());
|
||||
} catch {
|
||||
return name + '::' + String(args);
|
||||
@@ -56,10 +100,10 @@ function getToolCacheKey(name: string, args: Record<string, unknown>): string {
|
||||
}
|
||||
|
||||
/** 检测当前轮次是否存在重复工具调用 */
|
||||
function isDuplicateCall(call: ToolCall, loopCalls: ToolCall[]): boolean {
|
||||
function isDuplicateCall(call: ToolCall, allCalls: ToolCall[]): boolean {
|
||||
const callKey = getToolCacheKey(call.function.name, call.function.arguments);
|
||||
return loopCalls.some((prev, idx) => {
|
||||
if (idx === loopCalls.length - 1) return false; // 跳过自身
|
||||
return allCalls.some((prev, idx) => {
|
||||
if (idx === allCalls.length - 1) return false;
|
||||
return getToolCacheKey(prev.function.name, prev.function.arguments) === callKey;
|
||||
});
|
||||
}
|
||||
@@ -74,6 +118,26 @@ export interface AgentCallbacks {
|
||||
onConfirmTool: (call: ToolCall) => Promise<boolean>;
|
||||
}
|
||||
|
||||
/** 保存执行轨迹到 SQLite */
|
||||
async function saveTrace(trace: Omit<TraceEntry, 'id' | 'createdAt'>): Promise<void> {
|
||||
try {
|
||||
const bridge = (window as any).metonaDesktop;
|
||||
if (!bridge?.db) return;
|
||||
const entry = {
|
||||
id: `trace_${generateId()}`,
|
||||
session_id: trace.sessionId,
|
||||
step_index: trace.stepIndex,
|
||||
thought: trace.thought,
|
||||
action: trace.action,
|
||||
action_input: trace.actionInput,
|
||||
observation: trace.observation,
|
||||
loop_count: trace.loopCount,
|
||||
created_at: trace.createdAt
|
||||
};
|
||||
await bridge.db.saveTrace(entry);
|
||||
} catch { /* 不阻塞主流程 */ }
|
||||
}
|
||||
|
||||
export async function runAgentLoop(
|
||||
userContent: string,
|
||||
images: string[],
|
||||
@@ -82,6 +146,8 @@ export async function runAgentLoop(
|
||||
): Promise<void> {
|
||||
const api = state.get<OllamaAPI>(KEYS.API);
|
||||
const model = state.get<string>('_defaultModel', '');
|
||||
const currentSession = state.get(KEYS.CURRENT_SESSION);
|
||||
const sessionId = currentSession?.id || 'unknown';
|
||||
|
||||
if (!api || !model) {
|
||||
showToast('请先选择模型', 'error');
|
||||
@@ -93,12 +159,8 @@ export async function runAgentLoop(
|
||||
|
||||
// 检查模型是否支持 Tool Calling
|
||||
const modelSupportsTools = state.get<boolean>('modelSupportsTools', false);
|
||||
if (!modelSupportsTools) {
|
||||
logWarn(`模型 ${model} 不支持 Tool Calling`, 'Agent Loop 可能不稳定');
|
||||
}
|
||||
|
||||
const messages: OllamaMessage[] = [];
|
||||
|
||||
let systemPromptParts: string[] = [];
|
||||
|
||||
// 注入记忆上下文
|
||||
@@ -121,12 +183,14 @@ export async function runAgentLoop(
|
||||
文件操作工具(read_file、write_file 等)的相对路径基于此目录解析。`);
|
||||
}
|
||||
|
||||
if (systemPromptParts.length > 0) {
|
||||
messages.push({ role: 'system', content: systemPromptParts.join('\n\n') + '\n\n' + TOOL_USAGE_GUIDE });
|
||||
} else {
|
||||
messages.push({ role: 'system', content: TOOL_USAGE_GUIDE });
|
||||
}
|
||||
// 组合 system prompt
|
||||
const fullSystemPrompt = systemPromptParts.length > 0
|
||||
? systemPromptParts.join('\n\n') + '\n\n' + REACT_SYSTEM_PROMPT + '\n\n' + TOOL_USAGE_GUIDE
|
||||
: REACT_SYSTEM_PROMPT + '\n\n' + TOOL_USAGE_GUIDE;
|
||||
|
||||
messages.push({ role: 'system', content: fullSystemPrompt });
|
||||
|
||||
// 添加历史消息(使用 context-manager 构建)
|
||||
for (const msg of historyMessages) {
|
||||
messages.push({
|
||||
role: msg.role as 'user' | 'assistant',
|
||||
@@ -135,6 +199,7 @@ export async function runAgentLoop(
|
||||
});
|
||||
}
|
||||
|
||||
// 用户消息
|
||||
const userMsg: OllamaMessage = {
|
||||
role: 'user',
|
||||
content: userContent || (images?.length ? (images.length > 1 ? `请分析这 ${images.length} 张图片` : '请分析这张图片') : ''),
|
||||
@@ -145,18 +210,19 @@ export async function runAgentLoop(
|
||||
const tools = getEnabledToolDefinitions();
|
||||
const useTools = tools.length > 0;
|
||||
|
||||
logInfo(`Agent Loop 启动: ${model}`, `工具: ${useTools ? '开启' : '关闭'}, 记忆: ${isMemoryEnabled() ? '开启' : '关闭'}`);
|
||||
logInfo(`ReAct Agent Loop 启动: ${model}`, `工具: ${useTools ? '开启' : '关闭'}, 记忆: ${isMemoryEnabled() ? '开启' : '关闭'}`);
|
||||
|
||||
let loopCount = 0;
|
||||
const allToolRecords: ToolCallRecord[] = [];
|
||||
const loopStartTime = Date.now();
|
||||
let content = '';
|
||||
let totalEvalCount = 0;
|
||||
/** 记录前一轮的工具调用 key,用于跨轮去重 */
|
||||
let prevLoopToolKeys: string[] = [];
|
||||
/** 每轮工具调用计数(用于去重) */
|
||||
let allCallsThisLoop: ToolCall[] = [];
|
||||
|
||||
const makeStats = () => {
|
||||
const totalDuration = (Date.now() - loopStartTime) * 1e6; // 转为纳秒
|
||||
const totalDuration = (Date.now() - loopStartTime) * 1e6;
|
||||
return { eval_count: totalEvalCount || undefined, total_duration: totalDuration };
|
||||
};
|
||||
|
||||
@@ -165,14 +231,14 @@ export async function runAgentLoop(
|
||||
|
||||
// 全局超时检查
|
||||
if (Date.now() - loopStartTime > MAX_LOOP_TIME) {
|
||||
logWarn('Agent Loop 达到最大运行时间(5分钟),自动停止');
|
||||
logWarn('ReAct Agent Loop 达到最大运行时间(10分钟),自动停止');
|
||||
callbacks.onDone(content || '(达到最大运行时间限制)', allToolRecords, makeStats());
|
||||
return;
|
||||
}
|
||||
|
||||
// 检查是否已中止
|
||||
if (state.get<AbortController | null>(KEYS.ABORT_CONTROLLER)?.signal.aborted) {
|
||||
logInfo('Agent Loop 已中止');
|
||||
logInfo('ReAct Agent Loop 已中止');
|
||||
callbacks.onDone(content, allToolRecords.length > 0 ? allToolRecords : undefined, makeStats());
|
||||
return;
|
||||
}
|
||||
@@ -182,6 +248,7 @@ export async function runAgentLoop(
|
||||
let thinking = '';
|
||||
content = '';
|
||||
const toolCalls: ToolCall[] = [];
|
||||
allCallsThisLoop = [];
|
||||
|
||||
const abortController = new AbortController();
|
||||
state.set(KEYS.ABORT_CONTROLLER, abortController);
|
||||
@@ -205,10 +272,6 @@ export async function runAgentLoop(
|
||||
if (chunk.message?.thinking) {
|
||||
thinking += chunk.message.thinking;
|
||||
callbacks.onThinking(thinking);
|
||||
// 每收到 200 字符记录一次思考日志
|
||||
if (thinking.length % 200 < 10) {
|
||||
logThink(thinking);
|
||||
}
|
||||
}
|
||||
if (chunk.message?.content) {
|
||||
content += chunk.message.content;
|
||||
@@ -218,7 +281,6 @@ export async function runAgentLoop(
|
||||
if (chunk.message?.tool_calls?.length) {
|
||||
for (const tc of chunk.message.tool_calls) {
|
||||
if (tc.function?.name) {
|
||||
// 新的工具调用
|
||||
toolCalls.push({
|
||||
type: 'function',
|
||||
function: {
|
||||
@@ -227,7 +289,6 @@ export async function runAgentLoop(
|
||||
}
|
||||
});
|
||||
} else if (toolCalls.length > 0) {
|
||||
// 增量 arguments 追加到当前最后一个 tool_call
|
||||
const last = toolCalls[toolCalls.length - 1];
|
||||
if (tc.function?.arguments && typeof tc.function.arguments === 'object') {
|
||||
Object.assign(last.function.arguments, tc.function.arguments);
|
||||
@@ -246,24 +307,15 @@ export async function runAgentLoop(
|
||||
if (abortController.signal.aborted) {
|
||||
logInfo('流式调用已中止');
|
||||
if (content || thinking) {
|
||||
messages.push({
|
||||
role: 'assistant',
|
||||
content,
|
||||
...(thinking && { thinking })
|
||||
});
|
||||
messages.push({ role: 'assistant', content, ...(thinking && { thinking }) });
|
||||
}
|
||||
callbacks.onDone(content, allToolRecords.length > 0 ? allToolRecords : undefined, makeStats());
|
||||
return;
|
||||
}
|
||||
// 超时错误 — 保留已生成的内容
|
||||
if ((err as Error).message.includes('超时')) {
|
||||
logError('流式调用超时(2分钟无数据)', (err as Error).message);
|
||||
logError('流式调用超时', (err as Error).message);
|
||||
if (content || thinking) {
|
||||
messages.push({
|
||||
role: 'assistant',
|
||||
content: content || '(模型响应超时)',
|
||||
...(thinking && { thinking })
|
||||
});
|
||||
messages.push({ role: 'assistant', content: content || '(模型响应超时)', ...(thinking && { thinking }) });
|
||||
}
|
||||
callbacks.onDone(content || '(模型响应超时,已自动中断)', allToolRecords.length > 0 ? allToolRecords : undefined, makeStats());
|
||||
return;
|
||||
@@ -271,6 +323,11 @@ export async function runAgentLoop(
|
||||
throw err;
|
||||
}
|
||||
|
||||
// 提取 ReAct 思考过程
|
||||
const thoughtMatch = content.match(/\*\*Thought:\*\*\s*([\s\S]*?)(?=\*\*Action:\*\*|\*\*Final Answer:\*\*|$)/i);
|
||||
const thought = thoughtMatch ? thoughtMatch[1].trim() : '';
|
||||
|
||||
// 保存 assistant 消息
|
||||
const assistantMsg: OllamaMessage = {
|
||||
role: 'assistant',
|
||||
content,
|
||||
@@ -278,30 +335,54 @@ export async function runAgentLoop(
|
||||
};
|
||||
if (toolCalls.length > 0) {
|
||||
assistantMsg.tool_calls = toolCalls;
|
||||
// 官方示例要求:tool_calls 时同时保留 content 和 thinking
|
||||
// 确保 content 不为空(模型可能在调用工具前说了话)
|
||||
}
|
||||
messages.push(assistantMsg);
|
||||
|
||||
logModelResponse(content.length, toolCalls.length);
|
||||
|
||||
// 检查是否是 Final Answer(没有工具调用且包含 Final Answer)
|
||||
const isFinalAnswer = content.includes('Final Answer:') || content.includes('最终答案:');
|
||||
if (toolCalls.length === 0) {
|
||||
logInfo('无工具调用,Agent Loop 结束');
|
||||
logInfo('无工具调用,ReAct Agent Loop 结束');
|
||||
|
||||
// 自动提取记忆
|
||||
if (isMemoryEnabled() && messages.length >= 4) {
|
||||
try {
|
||||
const { extractMemoriesFromConversation } = await import('./memory-manager.js');
|
||||
await extractMemoriesFromConversation(
|
||||
messages.filter(m => m.role === 'user' || m.role === 'assistant').map(m => ({ role: m.role, content: m.content })),
|
||||
currentSession?.title
|
||||
);
|
||||
} catch { /* 不阻塞 */ }
|
||||
}
|
||||
|
||||
callbacks.onDone(content, allToolRecords.length > 0 ? allToolRecords : undefined, makeStats());
|
||||
return;
|
||||
}
|
||||
|
||||
// 跨轮次完整重复检测:如果本轮所有工具调用与上一轮完全相同,终止循环
|
||||
// 跨轮次完整重复检测
|
||||
const currentLoopKeys = toolCalls.map(c => getToolCacheKey(c.function.name, c.function.arguments)).sort();
|
||||
const currentKeysStr = JSON.stringify(currentLoopKeys);
|
||||
const prevKeysStr = JSON.stringify([...prevLoopToolKeys].sort());
|
||||
if (currentKeysStr === prevKeysStr && currentLoopKeys.length > 0) {
|
||||
logWarn('检测到连续两轮工具调用完全相同,终止 Agent Loop', currentKeysStr.slice(0, 200));
|
||||
logWarn('检测到连续两轮工具调用完全相同,终止 ReAct Loop', currentKeysStr.slice(0, 200));
|
||||
callbacks.onDone(content || '(检测到重复工具调用,已自动停止)', allToolRecords, makeStats());
|
||||
return;
|
||||
}
|
||||
prevLoopToolKeys = currentLoopKeys;
|
||||
|
||||
// 记录 ReAct 轨迹
|
||||
const traceStep = {
|
||||
sessionId,
|
||||
stepIndex: loopCount,
|
||||
thought: thought || content.slice(0, 200),
|
||||
action: toolCalls.map(t => t.function.name).join(', '),
|
||||
actionInput: JSON.stringify(toolCalls.map(t => t.function.arguments)),
|
||||
observation: '',
|
||||
loopCount,
|
||||
createdAt: Date.now()
|
||||
};
|
||||
|
||||
for (const call of toolCalls) {
|
||||
// 检查是否已中止
|
||||
if (abortController.signal.aborted) {
|
||||
@@ -311,30 +392,22 @@ export async function runAgentLoop(
|
||||
|
||||
// 同一轮内重复调用检测
|
||||
if (isDuplicateCall(call, toolCalls)) {
|
||||
logWarn(`跳过重复工具调用: ${call.function.name}`, JSON.stringify(call.function.arguments).slice(0, 200));
|
||||
logWarn(`跳过重复工具调用: ${call.function.name}`);
|
||||
const cachedKey = getToolCacheKey(call.function.name, call.function.arguments);
|
||||
const cached = toolResultCache.get(cachedKey);
|
||||
if (cached) {
|
||||
messages.push({
|
||||
role: 'tool',
|
||||
tool_name: call.function.name,
|
||||
content: JSON.stringify(cached)
|
||||
});
|
||||
messages.push({ role: 'tool', tool_name: call.function.name, content: JSON.stringify(cached) });
|
||||
callbacks.onToolCallResult(call.function.name, cached, call);
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
// 跨轮次缓存命中检测
|
||||
// 跨轮次缓存命中
|
||||
const cacheKey = getToolCacheKey(call.function.name, call.function.arguments);
|
||||
const cachedResult = toolResultCache.get(cacheKey);
|
||||
if (cachedResult) {
|
||||
logInfo(`工具缓存命中: ${call.function.name}`, JSON.stringify(call.function.arguments).slice(0, 200));
|
||||
messages.push({
|
||||
role: 'tool',
|
||||
tool_name: call.function.name,
|
||||
content: JSON.stringify(cachedResult)
|
||||
});
|
||||
logInfo(`工具缓存命中: ${call.function.name}`);
|
||||
messages.push({ role: 'tool', tool_name: call.function.name, content: JSON.stringify(cachedResult) });
|
||||
callbacks.onToolCallResult(call.function.name, cachedResult, call);
|
||||
continue;
|
||||
}
|
||||
@@ -344,13 +417,12 @@ export async function runAgentLoop(
|
||||
|
||||
if (needsConfirmation(call.function.name)) {
|
||||
const confirmed = await callbacks.onConfirmTool(call);
|
||||
// 确认后再次检查是否已中止
|
||||
if (abortController.signal.aborted) {
|
||||
callbacks.onDone(content, allToolRecords.length > 0 ? allToolRecords : undefined, makeStats());
|
||||
return;
|
||||
}
|
||||
if (!confirmed) {
|
||||
logWarn(`工具取消: ${call.function.name}`, '用户取消了操作');
|
||||
logWarn(`工具取消: ${call.function.name}`);
|
||||
const record: ToolCallRecord = {
|
||||
name: call.function.name,
|
||||
arguments: call.function.arguments,
|
||||
@@ -359,77 +431,76 @@ export async function runAgentLoop(
|
||||
timestamp: Date.now()
|
||||
};
|
||||
allToolRecords.push(record);
|
||||
|
||||
messages.push({
|
||||
role: 'tool',
|
||||
tool_name: call.function.name,
|
||||
content: JSON.stringify({ success: false, error: '用户取消了操作' })
|
||||
});
|
||||
messages.push({ role: 'tool', tool_name: call.function.name, content: JSON.stringify({ success: false, error: '用户取消了操作' }) });
|
||||
callbacks.onToolCallError(call.function.name, '用户取消', call);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
// 工具执行(run_command 无超时,其他工具 30 秒超时)
|
||||
logInfo(`执行工具: ${call.function.name}`, JSON.stringify(call.function.arguments));
|
||||
let result: ToolResult;
|
||||
if (call.function.name === 'run_command') {
|
||||
// run_command 通过 workspace IPC 执行,无超时限制
|
||||
result = await executeTool(call.function.name, call.function.arguments);
|
||||
} else {
|
||||
// 其他工具保持 30 秒超时保护
|
||||
result = await Promise.race([
|
||||
executeTool(call.function.name, call.function.arguments).catch(err =>
|
||||
({ success: false, error: err?.message || String(err) }) as ToolResult
|
||||
),
|
||||
new Promise<ToolResult>((_, reject) =>
|
||||
setTimeout(() => reject(new Error('工具执行超时(30秒)')), TOOL_EXEC_TIMEOUT)
|
||||
)
|
||||
]);
|
||||
}
|
||||
const record: ToolCallRecord = {
|
||||
name: call.function.name,
|
||||
arguments: call.function.arguments,
|
||||
result,
|
||||
status: result.success ? 'success' : 'error',
|
||||
timestamp: Date.now()
|
||||
};
|
||||
allToolRecords.push(record);
|
||||
// 工具执行 + 自动重试
|
||||
let lastError = '';
|
||||
for (let retry = 0; retry <= MAX_RETRIES; retry++) {
|
||||
try {
|
||||
let result: ToolResult;
|
||||
if (call.function.name === 'run_command') {
|
||||
result = await executeTool(call.function.name, call.function.arguments);
|
||||
} else {
|
||||
result = await Promise.race([
|
||||
executeTool(call.function.name, call.function.arguments).catch(err =>
|
||||
({ success: false, error: err?.message || String(err) }) as ToolResult
|
||||
),
|
||||
new Promise<ToolResult>((_, reject) =>
|
||||
setTimeout(() => reject(new Error('工具执行超时(30秒)')), TOOL_EXEC_TIMEOUT)
|
||||
)
|
||||
]);
|
||||
}
|
||||
|
||||
messages.push({
|
||||
role: 'tool',
|
||||
tool_name: call.function.name,
|
||||
content: JSON.stringify(result)
|
||||
});
|
||||
// 缓存成功结果(非 run_command,因为命令执行可能有副作用)
|
||||
if (result.success && call.function.name !== 'run_command') {
|
||||
toolResultCache.set(cacheKey, result);
|
||||
// 成功
|
||||
const record: ToolCallRecord = {
|
||||
name: call.function.name,
|
||||
arguments: call.function.arguments,
|
||||
result,
|
||||
status: result.success ? 'success' : 'error',
|
||||
timestamp: Date.now()
|
||||
};
|
||||
allToolRecords.push(record);
|
||||
messages.push({ role: 'tool', tool_name: call.function.name, content: JSON.stringify(result) });
|
||||
if (result.success && call.function.name !== 'run_command') {
|
||||
toolResultCache.set(cacheKey, result);
|
||||
}
|
||||
callbacks.onToolCallResult(call.function.name, result, call);
|
||||
logToolResult(call.function.name, result.success, result.success ? undefined : result.error);
|
||||
break; // 成功,退出重试循环
|
||||
} catch (err) {
|
||||
lastError = (err as Error).message;
|
||||
if (retry < MAX_RETRIES) {
|
||||
logWarn(`工具重试 ${retry + 1}/${MAX_RETRIES}: ${call.function.name}`, lastError);
|
||||
await new Promise(r => setTimeout(r, 500)); // 短暂延迟后重试
|
||||
continue;
|
||||
}
|
||||
// 最终失败
|
||||
logError(`工具执行失败: ${call.function.name}`, lastError);
|
||||
const record: ToolCallRecord = {
|
||||
name: call.function.name,
|
||||
arguments: call.function.arguments,
|
||||
result: { success: false, error: lastError },
|
||||
status: 'error',
|
||||
timestamp: Date.now()
|
||||
};
|
||||
allToolRecords.push(record);
|
||||
messages.push({ role: 'tool', tool_name: call.function.name, content: JSON.stringify({ success: false, error: lastError }) });
|
||||
callbacks.onToolCallError(call.function.name, lastError, call);
|
||||
}
|
||||
callbacks.onToolCallResult(call.function.name, result, call);
|
||||
logToolResult(call.function.name, result.success, result.success ? undefined : result.error);
|
||||
} catch (err) {
|
||||
const errorMsg = (err as Error).message;
|
||||
logError(`工具执行失败: ${call.function.name}`, errorMsg);
|
||||
const record: ToolCallRecord = {
|
||||
name: call.function.name,
|
||||
arguments: call.function.arguments,
|
||||
result: { success: false, error: errorMsg },
|
||||
status: 'error',
|
||||
timestamp: Date.now()
|
||||
};
|
||||
allToolRecords.push(record);
|
||||
|
||||
messages.push({
|
||||
role: 'tool',
|
||||
tool_name: call.function.name,
|
||||
content: JSON.stringify({ success: false, error: errorMsg })
|
||||
});
|
||||
callbacks.onToolCallError(call.function.name, errorMsg, call);
|
||||
}
|
||||
}
|
||||
|
||||
// 保存本轮轨迹
|
||||
traceStep.observation = allToolRecords.slice(-toolCalls.length).map(r =>
|
||||
`${r.name}: ${r.result?.success ? 'success' : 'error'}`
|
||||
).join('; ');
|
||||
saveTrace(traceStep);
|
||||
}
|
||||
|
||||
logWarn('Agent Loop 达到最大工具调用次数限制');
|
||||
logWarn('ReAct Agent Loop 达到最大工具调用次数限制');
|
||||
callbacks.onDone(content || '(达到最大工具调用次数限制)', allToolRecords, makeStats());
|
||||
}
|
||||
|
||||
@@ -0,0 +1,175 @@
|
||||
/**
|
||||
* Context Manager - 智能上下文管理 (v4.0)
|
||||
* 三层策略:滑动窗口 + 消息摘要压缩 + 记忆注入
|
||||
*/
|
||||
|
||||
import type { OllamaMessage } from '../types.js';
|
||||
import { logInfo, logWarn } from './log-service.js';
|
||||
|
||||
/** 粗略估算 token 数 */
|
||||
export function estimateTokens(text: string): number {
|
||||
if (!text) return 0;
|
||||
// 中文按 1.5 字/token,英文按 4 字符/token
|
||||
let chineseChars = 0;
|
||||
let otherChars = 0;
|
||||
for (const ch of text) {
|
||||
if (/[\u4e00-\u9fff]/.test(ch)) chineseChars++;
|
||||
else otherChars++;
|
||||
}
|
||||
return Math.ceil(chineseChars / 1.5 + otherChars / 4);
|
||||
}
|
||||
|
||||
export interface ContextBuildOptions {
|
||||
/** 滑动窗口大小(最近 N 条消息完整保留) */
|
||||
windowSize?: number;
|
||||
/** 每 N 条更早的消息压缩为一段摘要 */
|
||||
summaryBatchSize?: number;
|
||||
/** 最大 token 数限制 */
|
||||
maxTokens?: number;
|
||||
/** 系统 prompt 注入的记忆上下文 */
|
||||
memoryContext?: string;
|
||||
/** 工作空间上下文 */
|
||||
workspaceContext?: string;
|
||||
}
|
||||
|
||||
const DEFAULT_OPTIONS: Required<ContextBuildOptions> = {
|
||||
windowSize: 20,
|
||||
summaryBatchSize: 20,
|
||||
maxTokens: 24576,
|
||||
memoryContext: '',
|
||||
workspaceContext: ''
|
||||
};
|
||||
|
||||
/**
|
||||
* 构建发送给模型的 messages
|
||||
* 三层策略:
|
||||
* a. 滑动窗口:最近 N 条消息完整保留
|
||||
* b. 更早的消息:每 N 条压缩为一段摘要
|
||||
* c. 系统 prompt 注入记忆上下文
|
||||
*/
|
||||
export function buildContext(
|
||||
allMessages: OllamaMessage[],
|
||||
options: ContextBuildOptions = {}
|
||||
): OllamaMessage[] {
|
||||
const opts = { ...DEFAULT_OPTIONS, ...options };
|
||||
const result: OllamaMessage[] = [];
|
||||
|
||||
// 系统 prompt
|
||||
let systemContent = '';
|
||||
if (opts.memoryContext) {
|
||||
systemContent += opts.memoryContext + '\n\n';
|
||||
}
|
||||
if (opts.workspaceContext) {
|
||||
systemContent += opts.workspaceContext + '\n\n';
|
||||
}
|
||||
|
||||
// 提取已有的 system 消息
|
||||
const existingSystem = allMessages.find(m => m.role === 'system');
|
||||
if (existingSystem) {
|
||||
systemContent += existingSystem.content;
|
||||
}
|
||||
|
||||
if (systemContent.trim()) {
|
||||
result.push({ role: 'system', content: systemContent.trim() });
|
||||
}
|
||||
|
||||
// 非 system 消息
|
||||
const nonSystemMessages = allMessages.filter(m => m.role !== 'system');
|
||||
|
||||
if (nonSystemMessages.length <= opts.windowSize) {
|
||||
// 消息不多,全部保留
|
||||
result.push(...nonSystemMessages);
|
||||
return result;
|
||||
}
|
||||
|
||||
// 滑动窗口:最近 N 条
|
||||
const recentMessages = nonSystemMessages.slice(-opts.windowSize);
|
||||
|
||||
// 更早的消息:压缩为摘要
|
||||
const olderMessages = nonSystemMessages.slice(0, -opts.windowSize);
|
||||
const summaries = summarizeOlderMessages(olderMessages, opts.summaryBatchSize);
|
||||
|
||||
result.push(...summaries, ...recentMessages);
|
||||
|
||||
// Token 估算和裁剪
|
||||
const trimmed = trimByTokenLimit(result, opts.maxTokens);
|
||||
|
||||
logInfo(`上下文构建: ${nonSystemMessages.length} 条消息 → ${trimmed.length} 条 (窗口: ${opts.windowSize})`,
|
||||
`估算 tokens: ${estimateTokens(trimmed.map(m => m.content).join(''))}`);
|
||||
|
||||
return trimmed;
|
||||
}
|
||||
|
||||
/**
|
||||
* 将较早的消息每 batchSize 条压缩为一段摘要
|
||||
*/
|
||||
function summarizeOlderMessages(messages: OllamaMessage[], batchSize: number): OllamaMessage[] {
|
||||
const summaries: OllamaMessage[] = [];
|
||||
|
||||
for (let i = 0; i < messages.length; i += batchSize) {
|
||||
const batch = messages.slice(i, i + batchSize);
|
||||
const summary = createQuickSummary(batch);
|
||||
summaries.push({
|
||||
role: 'system',
|
||||
content: `【更早的对话摘要(第 ${Math.floor(i / batchSize) + 1} 部分)】\n${summary}`
|
||||
});
|
||||
}
|
||||
|
||||
return summaries;
|
||||
}
|
||||
|
||||
/**
|
||||
* 快速摘要(不调用模型,直接提取关键信息)
|
||||
*/
|
||||
function createQuickSummary(messages: OllamaMessage[]): string {
|
||||
const parts: string[] = [];
|
||||
|
||||
for (const msg of messages) {
|
||||
const role = msg.role === 'user' ? '用户' : 'AI';
|
||||
const content = msg.content || '';
|
||||
// 取前 100 字符作为摘要
|
||||
const preview = content.length > 100 ? content.slice(0, 100) + '...' : content;
|
||||
if (preview.trim()) {
|
||||
parts.push(`${role}: ${preview}`);
|
||||
}
|
||||
// 如果有工具调用,记录工具名
|
||||
if (msg.tool_calls?.length) {
|
||||
const toolNames = msg.tool_calls.map(t => t.function.name).join(', ');
|
||||
parts.push(` [工具: ${toolNames}]`);
|
||||
}
|
||||
}
|
||||
|
||||
return parts.join('\n');
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据 token 限制裁剪消息
|
||||
*/
|
||||
function trimByTokenLimit(messages: OllamaMessage[], maxTokens: number): OllamaMessage[] {
|
||||
let totalTokens = 0;
|
||||
const result: OllamaMessage[] = [];
|
||||
|
||||
// 从后往前添加(优先保留最近的消息)
|
||||
for (let i = messages.length - 1; i >= 0; i--) {
|
||||
const msg = messages[i];
|
||||
const msgTokens = estimateTokens(msg.content || '') +
|
||||
(msg.images ? msg.images.length * 100 : 0) +
|
||||
(msg.tool_calls ? msg.tool_calls.length * 50 : 0);
|
||||
|
||||
// system 消息始终保留
|
||||
if (msg.role === 'system') {
|
||||
totalTokens += msgTokens;
|
||||
result.unshift(msg);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (totalTokens + msgTokens > maxTokens && result.length > 2) {
|
||||
break; // 已到限制,保留至少 system + 1 条消息
|
||||
}
|
||||
|
||||
totalTokens += msgTokens;
|
||||
result.unshift(msg);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
@@ -350,6 +350,71 @@ export const TOOL_DEFINITIONS: ToolDefinition[] = [
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
// ══════════════════════════════════════════════
|
||||
// v4.0 新增工具:记忆和会话管理
|
||||
// ══════════════════════════════════════════════
|
||||
{
|
||||
type: 'function',
|
||||
function: {
|
||||
name: 'memory_search',
|
||||
description: 'Search agent memories by semantic similarity or keywords. Returns relevant memories about the user, preferences, rules, and past conversations.',
|
||||
parameters: {
|
||||
type: 'object',
|
||||
required: ['query'],
|
||||
properties: {
|
||||
query: { type: 'string', description: 'Search query for memories.' },
|
||||
limit: { type: 'integer', description: 'Max results to return. Default: 8.' },
|
||||
type: { type: 'string', enum: ['fact', 'preference', 'rule'], description: 'Filter by memory type.' }
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
type: 'function',
|
||||
function: {
|
||||
name: 'memory_add',
|
||||
description: 'Add a new memory entry for future reference. Use when you learn something important about the user, their preferences, or rules they want you to follow.',
|
||||
parameters: {
|
||||
type: 'object',
|
||||
required: ['type', 'content'],
|
||||
properties: {
|
||||
type: { type: 'string', enum: ['fact', 'preference', 'rule'], description: 'Memory type: fact (about user), preference (user preference), rule (must follow).' },
|
||||
content: { type: 'string', description: 'The memory content. Be concise and specific.' },
|
||||
importance: { type: 'integer', description: 'Importance 1-10. Higher = more important. Default: 5.' },
|
||||
tags: { type: 'array', items: { type: 'string' }, description: 'Tags for search matching. 2-5 keywords.' }
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
type: 'function',
|
||||
function: {
|
||||
name: 'session_list',
|
||||
description: 'List previous chat sessions with their titles and timestamps. Useful for referencing past conversations.',
|
||||
parameters: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
limit: { type: 'integer', description: 'Max sessions to return. Default: 20.' },
|
||||
search: { type: 'string', description: 'Filter sessions by title keyword.' }
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
type: 'function',
|
||||
function: {
|
||||
name: 'session_read',
|
||||
description: 'Read the messages from a previous chat session. Use when the user references something from a past conversation.',
|
||||
parameters: {
|
||||
type: 'object',
|
||||
required: ['session_id'],
|
||||
properties: {
|
||||
session_id: { type: 'string', description: 'The session ID to read.' },
|
||||
max_messages: { type: 'integer', description: 'Max messages to return. Default: 50.' }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
];
|
||||
|
||||
@@ -380,7 +445,8 @@ let enabledTools: Set<string> = new Set([
|
||||
'run_command',
|
||||
'move_file', 'copy_file', 'web_fetch', 'web_search', 'append_file', 'edit_file',
|
||||
'get_file_info', 'tree', 'download_file', 'diff_files', 'replace_in_files',
|
||||
'read_multiple_files', 'git', 'compress'
|
||||
'read_multiple_files', 'git', 'compress',
|
||||
'memory_search', 'memory_add', 'session_list', 'session_read'
|
||||
]);
|
||||
|
||||
export function setToolEnabled(toolName: string, enabled: boolean): void {
|
||||
@@ -409,6 +475,70 @@ export async function executeTool(toolName: string, args: Record<string, unknown
|
||||
}
|
||||
|
||||
try {
|
||||
// 内存工具(不走 IPC,直接处理)
|
||||
if (toolName === 'memory_search') {
|
||||
const { searchMemories } = await import('./memory-manager.js');
|
||||
const query = args.query as string;
|
||||
const limit = (args.limit as number) || 8;
|
||||
const type = args.type as string | undefined;
|
||||
let results = searchMemories(query, limit);
|
||||
if (type) results = results.filter(r => r.type === type);
|
||||
logToolResult('memory_search', true, `${results.length} 条结果`);
|
||||
return { success: true, results, total: results.length };
|
||||
}
|
||||
if (toolName === 'memory_add') {
|
||||
const { addMemory } = await import('./memory-manager.js');
|
||||
const type = args.type as 'fact' | 'preference' | 'rule';
|
||||
const content = args.content as string;
|
||||
const importance = (args.importance as number) || 5;
|
||||
const tags = (args.tags as string[]) || [];
|
||||
if (!content || content.length < 2) {
|
||||
return { success: false, error: 'content 不能为空且至少 2 个字符' };
|
||||
}
|
||||
const entry = await addMemory({ type, content, importance, tags });
|
||||
logToolResult('memory_add', true, `${type}: ${content.slice(0, 50)}`);
|
||||
return { success: true, id: entry.id, type: entry.type, content: entry.content };
|
||||
}
|
||||
if (toolName === 'session_list') {
|
||||
const bridge = window.metonaDesktop;
|
||||
if (!bridge?.db) return { success: false, error: '桌面 API 不可用' };
|
||||
const limit = (args.limit as number) || 20;
|
||||
const search = (args.search as string) || '';
|
||||
const sessions = await bridge.db.getAllSessions();
|
||||
let filtered = sessions.map((s: any) => ({
|
||||
id: s.id,
|
||||
title: s.title,
|
||||
model: s.model,
|
||||
messageCount: 0, // 从 SQLite 获取的原始行不含 messages
|
||||
createdAt: s.created_at,
|
||||
updatedAt: s.updated_at
|
||||
}));
|
||||
if (search) {
|
||||
filtered = filtered.filter((s: any) => s.title.toLowerCase().includes(search.toLowerCase()));
|
||||
}
|
||||
filtered.sort((a: any, b: any) => b.updatedAt - a.updatedAt);
|
||||
filtered = filtered.slice(0, limit);
|
||||
logToolResult('session_list', true, `${filtered.length} 个会话`);
|
||||
return { success: true, sessions: filtered, total: filtered.length };
|
||||
}
|
||||
if (toolName === 'session_read') {
|
||||
const bridge = window.metonaDesktop;
|
||||
if (!bridge?.db) return { success: false, error: '桌面 API 不可用' };
|
||||
const sessionId = args.session_id as string;
|
||||
const maxMessages = (args.max_messages as number) || 50;
|
||||
if (!sessionId) return { success: false, error: '缺少 session_id 参数' };
|
||||
const session = await bridge.db.getSession(sessionId);
|
||||
if (!session) return { success: false, error: `会话 ${sessionId} 不存在` };
|
||||
const messages = await bridge.db.getMessages(sessionId);
|
||||
const msgs = messages.slice(0, maxMessages).map((m: any) => ({
|
||||
role: m.role,
|
||||
content: m.content || '',
|
||||
timestamp: m.created_at
|
||||
}));
|
||||
logToolResult('session_read', true, `${msgs.length} 条消息`);
|
||||
return { success: true, session: { id: session.id, title: session.title, model: session.model }, messages: msgs, total: messages.length };
|
||||
}
|
||||
|
||||
// run_command 走 workspace IPC
|
||||
if (toolName === 'run_command') {
|
||||
const command = args.command as string;
|
||||
@@ -445,54 +575,27 @@ export async function executeTool(toolName: string, args: Record<string, unknown
|
||||
|
||||
export function getToolIcon(name: string): string {
|
||||
const icons: Record<string, string> = {
|
||||
read_file: '📄',
|
||||
write_file: '✏️',
|
||||
list_directory: '📁',
|
||||
search_files: '🔍',
|
||||
create_directory: '📂',
|
||||
delete_file: '🗑️',
|
||||
run_command: '💻',
|
||||
move_file: '📦',
|
||||
copy_file: '📋',
|
||||
web_fetch: '🌐',
|
||||
append_file: '➕',
|
||||
edit_file: '✂️',
|
||||
get_file_info: 'ℹ️',
|
||||
tree: '🌳',
|
||||
download_file: '⬇️',
|
||||
diff_files: '🔀',
|
||||
replace_in_files: '🔄',
|
||||
read_multiple_files: '📚',
|
||||
git: '🔖',
|
||||
compress: '🗜️',
|
||||
web_search: '🔍'
|
||||
read_file: '📄', write_file: '✏️', list_directory: '📁',
|
||||
search_files: '🔍', create_directory: '📂', delete_file: '🗑️', run_command: '💻',
|
||||
move_file: '📦', copy_file: '📋', web_fetch: '🌐', append_file: '➕',
|
||||
edit_file: '✂️', get_file_info: 'ℹ️', tree: '🌳', download_file: '⬇️',
|
||||
diff_files: '🔀', replace_in_files: '🔄', read_multiple_files: '📚',
|
||||
git: '🔖', compress: '🗜️', web_search: '🔍',
|
||||
memory_search: '🧠', memory_add: '💾', session_list: '📋', session_read: '📖'
|
||||
};
|
||||
return icons[name] || '🔧';
|
||||
}
|
||||
|
||||
export function formatToolName(name: string): string {
|
||||
const names: Record<string, string> = {
|
||||
read_file: '读取文件',
|
||||
write_file: '写入文件',
|
||||
list_directory: '列出目录',
|
||||
search_files: '搜索文件',
|
||||
create_directory: '创建目录',
|
||||
delete_file: '删除文件',
|
||||
run_command: '执行命令',
|
||||
move_file: '移动文件',
|
||||
copy_file: '复制文件',
|
||||
web_fetch: '网页抓取',
|
||||
append_file: '追加文件',
|
||||
edit_file: '编辑文件',
|
||||
get_file_info: '文件信息',
|
||||
tree: '目录树',
|
||||
download_file: '下载文件',
|
||||
diff_files: '文件对比',
|
||||
replace_in_files: '批量替换',
|
||||
read_multiple_files: '批量读取',
|
||||
git: 'Git 操作',
|
||||
compress: '压缩/解压',
|
||||
web_search: '联网搜索'
|
||||
read_file: '读取文件', write_file: '写入文件', list_directory: '列出目录',
|
||||
search_files: '搜索文件', create_directory: '创建目录', delete_file: '删除文件',
|
||||
run_command: '执行命令', move_file: '移动文件', copy_file: '复制文件',
|
||||
web_fetch: '网页抓取', append_file: '追加文件', edit_file: '编辑文件',
|
||||
get_file_info: '文件信息', tree: '目录树', download_file: '下载文件',
|
||||
diff_files: '文件对比', replace_in_files: '批量替换', read_multiple_files: '批量读取',
|
||||
git: 'Git 操作', compress: '压缩/解压', web_search: '联网搜索',
|
||||
memory_search: '搜索记忆', memory_add: '添加记忆', session_list: '会话列表', session_read: '读取会话'
|
||||
};
|
||||
return names[name] || name;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user