218 lines
7.5 KiB
JavaScript
218 lines
7.5 KiB
JavaScript
'use strict';
|
||
|
||
const { readFileTool } = require('./read_file');
|
||
const { editFileTool } = require('./edit_file');
|
||
const { runCommandTool } = require('./run_command');
|
||
const { webSearchTool, extractWebpageTool } = require('./web_search');
|
||
const { searchWorkspaceTool } = require('./search_workspace');
|
||
const { readMediafileTool } = require('./read_mediafile');
|
||
const path = require('path');
|
||
|
||
const MAX_RUN_COMMAND_CHARS = 10000;
|
||
const MAX_EXTRACT_WEBPAGE_CHARS = 80000;
|
||
|
||
function formatFailure(err) {
|
||
return `失败: ${err}`;
|
||
}
|
||
|
||
function formatReadFile(result) {
|
||
if (!result.success) return formatFailure(result.error || '读取失败');
|
||
if (result.type === 'read') {
|
||
return result.content || '';
|
||
}
|
||
if (result.type === 'search') {
|
||
const parts = [];
|
||
for (const match of result.matches || []) {
|
||
const hits = (match.hits || []).join(',') || '';
|
||
parts.push(`[${match.id}] L${match.line_start}-${match.line_end} hits:${hits}`);
|
||
parts.push(match.snippet || '');
|
||
parts.push('');
|
||
}
|
||
return parts.join('\n').trim();
|
||
}
|
||
if (result.type === 'extract') {
|
||
const parts = [];
|
||
for (const seg of result.segments || []) {
|
||
const label = seg.label || 'segment';
|
||
parts.push(`[${label}] L${seg.line_start}-${seg.line_end}`);
|
||
parts.push(seg.content || '');
|
||
parts.push('');
|
||
}
|
||
return parts.join('\n').trim();
|
||
}
|
||
return '';
|
||
}
|
||
|
||
function formatEditFile(result) {
|
||
if (!result.success) return formatFailure(result.error || '修改失败');
|
||
const count = typeof result.replacements === 'number' ? result.replacements : 0;
|
||
return `已替换 ${count} 处: ${result.path}`;
|
||
}
|
||
|
||
function formatWebSearch(result) {
|
||
if (!result.success) return formatFailure(result.error || '搜索失败');
|
||
const time = result.searched_at || new Date().toISOString();
|
||
const summary = result.answer || '';
|
||
const lines = [];
|
||
lines.push(`🔍 搜索查询: ${result.query}`);
|
||
lines.push(`📅 搜索时间: ${time}`);
|
||
lines.push('');
|
||
lines.push('📝 AI摘要:');
|
||
lines.push(summary || '');
|
||
lines.push('');
|
||
lines.push('---');
|
||
lines.push('');
|
||
lines.push('📊 搜索结果:');
|
||
const results = result.results || [];
|
||
results.forEach((item, idx) => {
|
||
lines.push(`${idx + 1}. ${item.title || ''}`);
|
||
lines.push(` 🔗 ${item.url || ''}`);
|
||
lines.push(` 📄 ${item.content || ''}`);
|
||
});
|
||
return lines.join('\n').trim();
|
||
}
|
||
|
||
function formatExtractWebpage(result, mode, url, targetPath) {
|
||
if (!result.success) return formatFailure(result.error || '提取失败');
|
||
if (mode === 'save') {
|
||
return `已保存: ${targetPath} (chars=${result.chars}, bytes=${result.bytes})`;
|
||
}
|
||
let content = result.content || '';
|
||
if (content.length > MAX_EXTRACT_WEBPAGE_CHARS) content = content.slice(0, MAX_EXTRACT_WEBPAGE_CHARS);
|
||
return `URL: ${url}\n${content}`;
|
||
}
|
||
|
||
function formatRunCommand(result) {
|
||
if (!result.success) {
|
||
if (result.status === 'timeout') {
|
||
const output = result.output ? result.output.slice(-MAX_RUN_COMMAND_CHARS) : '';
|
||
return `[timeout after ${result.timeout}s]\n${output}`.trim();
|
||
}
|
||
return `[error rc=${result.return_code ?? '1'}] ${result.error || ''}\n${result.output || ''}`.trim();
|
||
}
|
||
let output = result.output || '';
|
||
if (output.length > MAX_RUN_COMMAND_CHARS) output = output.slice(-MAX_RUN_COMMAND_CHARS);
|
||
return output || '[no_output]';
|
||
}
|
||
|
||
function formatSearchWorkspace(result) {
|
||
if (!result.success) return formatFailure(result.error || '搜索失败');
|
||
if (result.mode === 'file') {
|
||
const lines = [`命中文件(${result.matches.length}):`];
|
||
result.matches.forEach((p, idx) => lines.push(`${idx + 1}) ${p}`));
|
||
return lines.join('\n');
|
||
}
|
||
if (result.mode === 'content') {
|
||
const parts = [];
|
||
for (const item of result.results || []) {
|
||
parts.push(item.file);
|
||
for (const m of item.matches || []) {
|
||
parts.push(`- L${m.line}: ${m.snippet}`);
|
||
}
|
||
parts.push('');
|
||
}
|
||
return parts.join('\n').trim();
|
||
}
|
||
return '';
|
||
}
|
||
|
||
function formatReadMediafile(result) {
|
||
if (!result.success) return formatFailure(result.error || '读取失败');
|
||
if (result.type === 'image') return `已附加图片: ${result.path}`;
|
||
return `已附加视频: ${result.path}`;
|
||
}
|
||
|
||
function buildToolContent(result) {
|
||
if (result.type === 'image' || result.type === 'video') {
|
||
const payload = {
|
||
type: result.type === 'image' ? 'image_url' : 'video_url',
|
||
[result.type === 'image' ? 'image_url' : 'video_url']: {
|
||
url: `data:${result.mime};base64,${result.b64}`,
|
||
},
|
||
};
|
||
return [
|
||
{ type: 'text', text: formatReadMediafile(result) },
|
||
payload,
|
||
];
|
||
}
|
||
return null;
|
||
}
|
||
|
||
async function executeTool({ workspace, config, allowMode, toolCall, abortSignal }) {
|
||
const name = toolCall.function.name;
|
||
let args = {};
|
||
try {
|
||
args = JSON.parse(toolCall.function.arguments || '{}');
|
||
} catch (err) {
|
||
return {
|
||
success: false,
|
||
tool: name,
|
||
error: `JSON解析失败: ${err.message || err}`,
|
||
formatted: formatFailure('参数解析失败'),
|
||
};
|
||
}
|
||
|
||
if (allowMode === 'read_only' && (name === 'edit_file' || name === 'run_command')) {
|
||
const note = '当前为只读模式,已禁用 edit_file、run_command。若需使用,请告知当前无权限需要用户输入 /allow 切换权限。';
|
||
return {
|
||
success: false,
|
||
tool: name,
|
||
error: note,
|
||
formatted: note,
|
||
};
|
||
}
|
||
|
||
if (abortSignal && abortSignal.aborted) {
|
||
return {
|
||
success: false,
|
||
tool: name,
|
||
error: '任务被用户取消',
|
||
formatted: '任务被用户取消',
|
||
raw: { success: false, error: '任务被用户取消', cancelled: true },
|
||
};
|
||
}
|
||
|
||
let raw;
|
||
if (name === 'read_file') raw = readFileTool(workspace, args);
|
||
else if (name === 'edit_file') raw = editFileTool(workspace, args);
|
||
else if (name === 'run_command') raw = await runCommandTool(workspace, args, abortSignal);
|
||
else if (name === 'web_search') raw = await webSearchTool(config, args, abortSignal);
|
||
else if (name === 'extract_webpage') {
|
||
if (!args.mode) args.mode = 'read';
|
||
const targetPath = args.target_path ? (path.isAbsolute(args.target_path) ? args.target_path : path.join(workspace, args.target_path)) : null;
|
||
if (args.mode === 'save') {
|
||
if (!targetPath || !targetPath.toLowerCase().endsWith('.md')) {
|
||
raw = { success: false, error: 'target_path 必须是 .md 文件' };
|
||
} else {
|
||
raw = await extractWebpageTool(config, args, targetPath, abortSignal);
|
||
}
|
||
} else {
|
||
raw = await extractWebpageTool(config, args, targetPath, abortSignal);
|
||
}
|
||
} else if (name === 'search_workspace') raw = await searchWorkspaceTool(workspace, args);
|
||
else if (name === 'read_mediafile') raw = readMediafileTool(workspace, args);
|
||
else raw = { success: false, error: '未知工具' };
|
||
|
||
let formatted = '';
|
||
if (name === 'read_file') formatted = formatReadFile(raw);
|
||
else if (name === 'edit_file') formatted = formatEditFile(raw);
|
||
else if (name === 'run_command') formatted = formatRunCommand(raw);
|
||
else if (name === 'web_search') formatted = formatWebSearch(raw);
|
||
else if (name === 'extract_webpage') formatted = formatExtractWebpage(raw, args.mode, args.url, args.target_path);
|
||
else if (name === 'search_workspace') formatted = formatSearchWorkspace(raw);
|
||
else if (name === 'read_mediafile') formatted = formatReadMediafile(raw);
|
||
else formatted = raw.success ? '' : formatFailure(raw.error || '失败');
|
||
|
||
const toolContent = buildToolContent(raw);
|
||
|
||
return {
|
||
success: raw.success,
|
||
tool: name,
|
||
raw,
|
||
formatted,
|
||
tool_content: toolContent,
|
||
};
|
||
}
|
||
|
||
module.exports = { executeTool };
|