87 lines
2.5 KiB
TypeScript
87 lines
2.5 KiB
TypeScript
/**
|
|
* 把 `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, unknown>): 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<string, unknown>
|
|
: {}
|
|
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
|
|
}
|