agent-Specialization/easyagent/src/ui/tool_display.js

224 lines
7.5 KiB
JavaScript

'use strict';
const readline = require('readline');
const { blue, gray, red, green } = require('../utils/colors');
const DOT_ON = '•';
const DOT_OFF = '◦';
const { stripAnsi, visibleWidth, truncatePlain, truncateVisible } = require('../utils/text_width');
function toolNameMap(name) {
const map = {
read_file: '读取文件',
edit_file: '修改文件',
run_command: '运行指令',
web_search: '网络搜索',
extract_webpage: '提取网页',
search_workspace: '文件搜索',
read_mediafile: '读取媒体文件',
};
return map[name] || name;
}
function normalizePreviewLine(line) {
return String(line ?? '').replace(/^\s+/, '');
}
function splitCommandLines(command) {
const text = String(command ?? '');
const parts = text.split(/\r?\n/);
if (parts.length > 1 && parts[parts.length - 1] === '') parts.pop();
return parts.length ? parts : [''];
}
function buildRunCommandLines(label, command, suffix = '') {
const width = Number(process.stdout.columns) || 80;
const parts = splitCommandLines(command);
const firstLine = parts[0] ?? '';
const labelLen = visibleWidth(label);
const suffixLen = visibleWidth(suffix);
const available = width - labelLen - 1 - suffixLen;
const commandLine = available > 0 ? truncatePlain(firstLine, available) : '';
const startLine = truncateVisible(`${label} ${commandLine}${suffix}`, width);
if (parts.length <= 1) {
return { startLine, finalLine: startLine };
}
const preview = parts.slice(1, 3).map(normalizePreviewLine);
const lines = [startLine, ...preview.map((line) => ` ${truncatePlain(line, Math.max(0, width - 2))}`), ` 总指令${parts.length}`];
return { startLine, finalLine: lines.join('\n') };
}
function buildStartLine(name, args) {
const label = blue(toolNameMap(name));
if (name === 'run_command') {
const command = args && Object.prototype.hasOwnProperty.call(args, 'command') ? args.command : '';
const { startLine } = buildRunCommandLines(label, command, ` (timeout=${args.timeout}s)`);
return startLine;
}
if (name === 'web_search') {
return `${label} ${args.query}`;
}
if (name === 'search_workspace') {
const root = args.root || '.';
const mode = args.mode === 'content' ? '内容搜索' : '文件搜索';
return `${blue(mode)}${root} 搜索 ${args.query}`;
}
if (name === 'read_file') {
return `${label} ${args.path}`;
}
if (name === 'edit_file') {
return `${label} ${args.file_path}`;
}
if (name === 'extract_webpage') {
return `${label} ${args.url}`;
}
if (name === 'read_mediafile') {
return `${label} ${args.path}`;
}
return `${label}`;
}
function buildFinalLine(name, args) {
if (name === 'run_command') {
const label = blue(toolNameMap(name));
const command = args && Object.prototype.hasOwnProperty.call(args, 'command') ? args.command : '';
const { finalLine } = buildRunCommandLines(label, command, ` (timeout=${args.timeout}s)`);
return finalLine;
}
return buildStartLine(name, args);
}
function startToolDisplay(line) {
let on = false;
const rawLine = String(line ?? '').replace(/\r?\n/g, ' ');
let lastLines = 1;
function clearPrevious() {
if (lastLines > 1) readline.moveCursor(process.stdout, 0, -(lastLines - 1));
for (let i = 0; i < lastLines; i += 1) {
readline.clearLine(process.stdout, 0);
if (i < lastLines - 1) readline.moveCursor(process.stdout, 0, 1);
}
if (lastLines > 1) readline.moveCursor(process.stdout, 0, -(lastLines - 1));
readline.cursorTo(process.stdout, 0);
}
function render() {
clearPrevious();
const dot = on ? DOT_ON : DOT_OFF;
const width = Number(process.stdout.columns) || 80;
const maxLine = Math.max(0, width - 3);
const displayLine = truncateVisible(rawLine, maxLine);
const lineText = `${dot} ${displayLine}`;
process.stdout.write(lineText);
const lineLen = visibleWidth(lineText);
lastLines = Math.max(1, Math.ceil(lineLen / Math.max(1, width)));
}
render();
const timer = setInterval(() => {
on = !on;
render();
}, 120);
return {
stop(finalLine) {
clearInterval(timer);
clearPrevious();
process.stdout.write(`${DOT_ON} ${finalLine}\n`);
},
};
}
function formatResultLines(name, args, raw) {
if (!raw || raw.success === false) {
const msg = raw && raw.error ? raw.error : '执行失败';
if (msg === '任务被用户取消') return [red(msg)];
return [red(`失败: ${msg}`)];
}
if (name === 'run_command') {
const output = raw.output || '';
const lines = output.split(/\r?\n/).filter((l) => l !== '');
const tail = lines.slice(-5);
const summary = '运行结果';
return [summary, ...tail];
}
if (name === 'web_search') {
const results = raw.results || [];
const title = results[0]?.title || '';
return [`浏览 ${results.length} 个网站 | ${title}`];
}
if (name === 'search_workspace') {
if (raw.mode === 'file') {
const total = raw.matches.length;
const top = raw.matches.slice(0, 3);
const lines = [`搜索到 ${total} 个结果`, ...top];
if (total > 3) lines.push(gray(`更多 ${total - 3} 个结果`));
return lines;
}
if (raw.mode === 'content') {
const total = raw.results.length;
const top = raw.results.slice(0, 3).map((item) => {
const first = item.matches && item.matches[0];
return first ? `${item.file}: L${first.line} ${first.snippet}` : item.file;
});
const lines = [`搜索到 ${total} 个文件`, ...top];
if (total > 3) lines.push(gray(`更多 ${total - 3} 个结果`));
return lines;
}
}
if (name === 'read_file') {
if (raw.type === 'search') {
const count = (raw.matches || []).length;
return [`搜索到 ${count} 个结果`];
}
if (raw.type === 'extract') {
const count = (raw.segments || []).length;
return [`抽取了 ${count} 个片段`];
}
const lines = raw.line_end && raw.line_start ? raw.line_end - raw.line_start + 1 : 0;
const chars = raw.content ? raw.content.length : 0;
return [`读取了 ${lines} 行 / ${chars} 字符`];
}
if (name === 'extract_webpage') {
if (args.mode === 'save') {
return [`已保存 ${args.target_path}`];
}
const preview = (raw.content || '').slice(0, 50);
return [`${preview}${preview.length ? '...' : ''}`];
}
if (name === 'edit_file') {
const diff = raw.diff || { added: 0, removed: 0, hunks: [] };
const lines = [`减少了 ${diff.removed} 行 增加了 ${diff.added}`];
diff.hunks.forEach((hunk, idx) => {
if (idx > 0) lines.push(' ⋮');
hunk.forEach((ln) => {
if (ln.lineNum == null) return;
const num = String(ln.lineNum).padStart(4, ' ');
const mark = ln.prefix === ' ' ? ' ' : ln.prefix;
let text = `${num} ${mark} ${ln.content}`;
if (ln.prefix === '+') text = green(text);
if (ln.prefix === '-') text = red(text);
lines.push(text);
});
});
return lines;
}
if (name === 'read_mediafile') {
if (raw.type === 'image') return ['已附加图片'];
if (raw.type === 'video') return ['已附加视频'];
}
return ['完成'];
}
function printResultLines(lines) {
if (!lines || !lines.length) return;
lines.forEach((line, idx) => {
if (idx === 0) {
process.stdout.write(`${line}\n`);
} else {
process.stdout.write(` ${line}\n`);
}
});
process.stdout.write('\n');
}
module.exports = { buildStartLine, buildFinalLine, startToolDisplay, formatResultLines, printResultLines };