新增: UX-04工具卡片友好展示(formatToolResult全工具摘要+i18n)

This commit is contained in:
2026-06-16 23:33:45 +08:00
parent b3e78e6061
commit 22d866d4de
3 changed files with 214 additions and 8 deletions

View File

@@ -132,17 +132,44 @@ const t = ((i18n as any).global.t as (k: string, p?: Record<string, unknown>) =>
/** 工具结果 JSON 已知字段(全可选;list_* 运行时返回数组,靠 Array.isArray 区分) */ /** 工具结果 JSON 已知字段(全可选;list_* 运行时返回数组,靠 Array.isArray 区分) */
interface ToolResult { interface ToolResult {
path?: string path?: string
from?: string
to?: string
content?: string content?: string
lines?: number lines?: number | null
size?: number size?: number | null
bytes_written?: number bytes_written?: number
new_size?: number
bytes_moved?: number
entries?: Array<{ name: string; type: string; size?: number; depth?: number }> entries?: Array<{ name: string; type: string; size?: number; depth?: number }>
name?: string name?: string
title?: string title?: string
field?: string field?: string
id?: string
status?: string
stack?: string
bound?: boolean
deleted?: boolean deleted?: boolean
restored?: boolean restored?: boolean
purged?: boolean purged?: boolean
updated?: boolean
changed?: boolean
renamed?: boolean
exists?: boolean
is_binary?: boolean
is_dir?: boolean
permanent?: boolean
backed_up?: boolean
size_diff?: number
exit_code?: number
duration_ms?: number
stdout?: string
stderr?: string
truncated?: boolean
results?: Array<{ path?: string; size?: number }>
total?: number
has_more?: boolean
warning?: string
review_rounds?: number
} }
/** 内联 SVG 图标字符串(经 v-html 注入,常量安全;图标库改造见决策记录) */ /** 内联 SVG 图标字符串(经 v-html 注入,常量安全;图标库改造见决策记录) */
@@ -267,7 +294,11 @@ function argString(args: unknown, key: string): string {
return '' return ''
} }
/** 通用工具结果格式化(非文件类工具) */ /**
* 通用工具结果格式化(非文件类工具)。
* UX-260616-04:统一 switch 覆盖全部工具产出人类可读摘要,不再走裸 JSON 兜底。
* read_file/list_directory/write_file 有模板特化分支,不进此函数。
*/
function formatToolResult(tc: AiToolCallInfo): string { function formatToolResult(tc: AiToolCallInfo): string {
const r = parseResult(tc.result) const r = parseResult(tc.result)
if (!r) { if (!r) {
@@ -283,10 +314,111 @@ function formatToolResult(tc: AiToolCallInfo): string {
} }
return raw return raw
} }
if (tc.name === 'write_file') { switch (tc.name) {
case 'write_file':
return `${r.path} (${formatBytes(r.bytes_written)})` return `${r.path} (${formatBytes(r.bytes_written)})`
case 'update_task':
return r.updated !== false
? t('aiTool.updatedTaskField', { id: r.id || '', field: r.field || '' })
: t('aiTool.updated')
case 'advance_task':
return t('aiTool.advancedTask', { id: r.id || '', status: r.status || '' })
case 'delete_task':
return r.deleted ? t('aiTool.deleted') : t('aiTool.deleteIneffective')
case 'delete_file':
return r.permanent
? t('aiTool.deleteFilePermanent', { path: r.path || '' })
: t('aiTool.deleteFileSoft', { path: r.path || '' })
case 'run_command':
return formatCommandResult(r)
case 'patch_file':
return r.changed === false
? t('aiTool.patchedNoChange', { path: r.path || '' })
: t('aiTool.patchedFile', { path: r.path || '', diff: formatSizeDiff(r.size_diff) })
case 'append_file':
return t('aiTool.appendedTo', { path: r.path || '', bytes: formatBytes(r.bytes_written) })
case 'rename_file':
return r.renamed
? t('aiTool.renamedFile', { from: r.from || '', to: r.to || '' })
: formatJson(r)
case 'search_files': {
const n = typeof r.total === 'number' ? r.total : (Array.isArray(r.results) ? r.results.length : 0)
return t('aiTool.foundN', { n })
} }
case 'file_info':
return formatFileInfoResult(r)
case 'bind_directory':
return r.bound
? t('aiTool.boundDir', { path: r.path || '', stack: r.stack || '' })
: formatJson(r)
default:
return formatJson(r) return formatJson(r)
}
}
/** size_diff 数字 → "+N"/"-N"/"+0" 字符串(负数补减号,正数补加号) */
function formatSizeDiff(diff: number | undefined | null): string {
const n = typeof diff === 'number' ? diff : 0
return n >= 0 ? `+${n}` : `${n}`
}
/**
* run_command 结果摘要:exit_code 标题 + stdout/stderr 前 N 行 + 总行数提示(截断,不全量显示)。
* 输出可能很大(编译/find/cat 大文件),仅显示前若干行 + 总行数提示,防撑爆卡片。
*/
function formatCommandResult(r: ToolResult): string {
const code = typeof r.exit_code === 'number' ? r.exit_code : -1
const ms = typeof r.duration_ms === 'number' ? r.duration_ms : 0
const head = code === 0
? t('aiTool.commandOk', { ms })
: t('aiTool.commandFailed', { code })
// 合并 stdout + stderr(实际 stderr 通常含报错),各自取前若干行
const MAX_LINES = 20
const out = combineAndTruncateLines(r.stdout, r.stderr, MAX_LINES)
const parts = [head]
if (out.text) {
parts.push(out.text)
if (out.truncated) {
parts.push(t('aiTool.commandOutputLines', { n: out.total }))
}
}
return parts.join('\n')
}
/** 合并 stdout/stderr 取前 N 行,返回截断后文本 + 是否截断 + 总行数 */
function combineAndTruncateLines(stdout: string | undefined, stderr: string | undefined, maxLines: number): { text: string; truncated: boolean; total: number } {
const parts: string[] = []
if (stdout) parts.push(stdout)
if (stderr) parts.push(stderr)
if (!parts.length) return { text: '', truncated: false, total: 0 }
const combined = parts.join('\n')
const allLines = combined.split('\n')
const total = allLines.length
if (total <= maxLines) {
return { text: combined, truncated: false, total }
}
// 取前 N 行 + 省略提示
return { text: allLines.slice(0, maxLines).join('\n'), truncated: true, total }
}
/** file_info 结果摘要:存在性 + 大小 + 行数(文本)/二进制/目录 标注 */
function formatFileInfoResult(r: ToolResult): string {
if (r.exists === false) {
return t('aiTool.fileInfoMissing', { path: r.path || '' })
}
let base = t('aiTool.fileInfoSize', { path: r.path || '', size: formatBytes(typeof r.size === 'number' ? r.size : 0) })
const extras: string[] = []
if (r.is_dir) {
extras.push(t('aiTool.fileInfoDir'))
} else {
if (typeof r.lines === 'number') {
extras.push(t('aiTool.fileInfoLines', { n: r.lines }))
}
if (r.is_binary) {
extras.push(t('aiTool.fileInfoBinary'))
}
}
return base + extras.join('')
} }
</script> </script>
@@ -469,7 +601,11 @@ function toolDisplayName(tc: AiToolCallInfo): string {
} }
} }
/** completed 工具结果的一句话摘要(折叠态 header 也能看到,避免光秃秃) */ /**
* completed 工具结果的一句话摘要(折叠态 header 也能看到,避免光秃秃)。
* UX-260616-04:覆盖范围与 formatToolResult 对齐(header+body 一致);
* read_file/list_directory/write_file 有模板特化分支,header 摘要仍显示关键计数。
*/
function toolResultSummary(tc: AiToolCallInfo): string { function toolResultSummary(tc: AiToolCallInfo): string {
if (tc.status !== 'completed') return '' if (tc.status !== 'completed') return ''
const r = parseResult(tc.result) const r = parseResult(tc.result)
@@ -482,9 +618,39 @@ function toolResultSummary(tc: AiToolCallInfo): string {
case 'create_idea': case 'create_idea':
case 'create_task': return r.title ? t('aiTool.createdWithName', { name: r.title }) : t('aiTool.created') case 'create_task': return r.title ? t('aiTool.createdWithName', { name: r.title }) : t('aiTool.created')
case 'update_project': return r.field ? t('aiTool.updatedField', { field: r.field }) : t('aiTool.updated') case 'update_project': return r.field ? t('aiTool.updatedField', { field: r.field }) : t('aiTool.updated')
case 'delete_project': return r.deleted ? t('aiTool.deleted') : t('aiTool.deleteIneffective') case 'update_task': return r.updated !== false
? t('aiTool.updatedTaskField', { id: r.id || '', field: r.field || '' })
: t('aiTool.updated')
case 'advance_task': return t('aiTool.advancedTask', { id: r.id || '', status: r.status || '' })
case 'delete_project':
case 'delete_task': return r.deleted ? t('aiTool.deleted') : t('aiTool.deleteIneffective')
case 'delete_file': return r.permanent
? t('aiTool.deleteFilePermanent', { path: r.path || '' })
: t('aiTool.deleteFileSoft', { path: r.path || '' })
case 'restore_project': return r.restored ? t('aiTool.restored') : t('aiTool.restoreIneffective') case 'restore_project': return r.restored ? t('aiTool.restored') : t('aiTool.restoreIneffective')
case 'purge_project': return r.purged ? t('aiTool.purged') : t('aiTool.purgeIneffective') case 'purge_project': return r.purged ? t('aiTool.purged') : t('aiTool.purgeIneffective')
case 'run_command': {
const code = typeof r.exit_code === 'number' ? r.exit_code : -1
const ms = typeof r.duration_ms === 'number' ? r.duration_ms : 0
return code === 0 ? t('aiTool.commandOk', { ms }) : t('aiTool.commandFailed', { code })
}
case 'patch_file': return r.changed === false
? t('aiTool.patchedNoChange', { path: r.path || '' })
: t('aiTool.patchedFile', { path: r.path || '', diff: formatSizeDiff(r.size_diff) })
case 'append_file': return t('aiTool.appendedTo', { path: r.path || '', bytes: formatBytes(r.bytes_written) })
case 'rename_file': return r.renamed
? t('aiTool.renamedFile', { from: r.from || '', to: r.to || '' })
: ''
case 'search_files': {
const n = typeof r.total === 'number' ? r.total : (Array.isArray(r.results) ? r.results.length : 0)
return t('aiTool.foundN', { n })
}
case 'file_info': return r.exists === false
? t('aiTool.fileInfoMissing', { path: r.path || '' })
: t('aiTool.fileInfoSize', { path: r.path || '', size: formatBytes(typeof r.size === 'number' ? r.size : 0) })
case 'bind_directory': return r.bound
? t('aiTool.boundDir', { path: r.path || '', stack: r.stack || '' })
: ''
case 'run_workflow': return t('aiTool.workflowHint') case 'run_workflow': return t('aiTool.workflowHint')
default: return '' default: return ''
} }

View File

@@ -48,6 +48,26 @@ export default {
purgeIneffective: 'Purge ineffective', purgeIneffective: 'Purge ineffective',
workflowHint: 'Run it on the workflow page', workflowHint: 'Run it on the workflow page',
// UX-260616-04: formatToolResult covers all tools with human-readable summaries (missing keys)
updatedTaskField: 'Task {id} updated field {field}',
advancedTask: 'Task {id} advanced to {status}',
commandOk: 'Command succeeded ({ms} ms)',
commandFailed: 'Command failed (exit code {code})',
commandOutputLines: '{n} lines of output',
patchedFile: 'Patched {path} ({diff} bytes)',
patchedNoChange: '{path} no change',
appendedTo: 'Appended {bytes} to {path}',
renamedFile: 'Renamed {from} → {to}',
foundN: 'Found {n}',
boundDir: 'Bound directory "{path}" stack: {stack}',
deleteFilePermanent: 'Permanently deleted {path}',
deleteFileSoft: 'Soft-deleted {path} (backed up)',
fileInfoSize: '{path} {size}',
fileInfoLines: ' · {n} lines',
fileInfoBinary: ' · binary',
fileInfoDir: ' · directory',
fileInfoMissing: '{path} does not exist',
projectLabel: '"{name}"', projectLabel: '"{name}"',
projectIdNotFound: '(project not found, id={id})', projectIdNotFound: '(project not found, id={id})',

View File

@@ -50,6 +50,26 @@ export default {
purgeIneffective: '清除未生效', purgeIneffective: '清除未生效',
workflowHint: '请到工作流页面运行', workflowHint: '请到工作流页面运行',
// UX-260616-04:formatToolResult 覆盖全部工具产出人类可读摘要(补缺失 key)
updatedTaskField: '任务 {id} 已更新字段 {field}',
advancedTask: '任务 {id} 已推进至 {status}',
commandOk: '命令成功({ms} 毫秒)',
commandFailed: '命令失败(退出码 {code}',
commandOutputLines: '{n} 行输出',
patchedFile: '已修改 {path}(增减 {diff} 字节)',
patchedNoChange: '{path} 无更改',
appendedTo: '已向 {path} 追加 {bytes}',
renamedFile: '已重命名 {from} → {to}',
foundN: '找到 {n} 项',
boundDir: '已绑定目录「{path}」技术栈:{stack}',
deleteFilePermanent: '已硬删除 {path}',
deleteFileSoft: '已软删除 {path}(已备份)',
fileInfoSize: '{path} {size}',
fileInfoLines: ' · {n} 行',
fileInfoBinary: ' · 二进制',
fileInfoDir: ' · 目录',
fileInfoMissing: '{path} 不存在',
// 审批参数特化(id/project_id 回显项目名,查不到标注原 id) // 审批参数特化(id/project_id 回显项目名,查不到标注原 id)
projectLabel: '「{name}」', projectLabel: '「{name}」',
projectIdNotFound: '(项目已不存在, id={id})', projectIdNotFound: '(项目已不存在, id={id})',