'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 };