224 lines
7.5 KiB
JavaScript
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 };
|