/** * 把 `claude -p --output-format stream-json --verbose` 吐出的每行 NDJSON * 压缩成人能看懂的进度短语,供长任务(如提交代码)实时展示。 * * 只关心 assistant 消息里的 `tool_use` 和 `text` 块,其它行(system / user / * result / thinking 块 / 坏 JSON)一律返回空数组,调用方无需再做判断。 */ import * as path from 'node:path' const LABEL_MAX_CHARS = 60 interface ContentBlock { type?: unknown text?: unknown name?: unknown input?: unknown } function truncate(text: string): string { if (text.length <= LABEL_MAX_CHARS) return text return `${text.slice(0, LABEL_MAX_CHARS)}…` } function asString(value: unknown): string { return typeof value === 'string' ? value : '' } /** 一次工具调用写成人看得懂的短语:「读取 routes.py」「搜索 get_user」。 */ export function describeToolUse(name: string, input: Record): string { switch (name) { case 'Bash': return `执行命令:${truncate(asString(input.description) || asString(input.command))}` case 'Read': return `读取 ${path.basename(asString(input.file_path))}` case 'Edit': case 'Write': case 'MultiEdit': return `修改 ${path.basename(asString(input.file_path))}` case 'Grep': case 'Glob': return `搜索 ${truncate(asString(input.pattern))}` default: return `调用 ${name}` } } function describeBlock(block: ContentBlock): string | null { if (block.type === 'tool_use' && typeof block.name === 'string') { const input = block.input && typeof block.input === 'object' ? block.input as Record : {} return describeToolUse(block.name, input) } if (block.type === 'text' && typeof block.text === 'string') { const firstLine = block.text.split('\n').map(l => l.trim()).find(l => l.length > 0) return firstLine ? truncate(firstLine) : null } return null } export function summarizeClaudeStreamLine(line: string): string[] { let parsed: unknown try { parsed = JSON.parse(line) } catch { return [] } if (!parsed || typeof parsed !== 'object') return [] const obj = parsed as { type?: unknown, message?: { content?: unknown } } if (obj.type !== 'assistant' || !Array.isArray(obj.message?.content)) return [] const labels: string[] = [] for (const block of obj.message.content as ContentBlock[]) { if (!block || typeof block !== 'object') continue const label = describeBlock(block) if (label) labels.push(label) } return labels }