新增: UX-04工具卡片友好展示(formatToolResult全工具摘要+i18n)
This commit is contained in:
@@ -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) {
|
||||||
return `${r.path} (${formatBytes(r.bytes_written)})`
|
case 'write_file':
|
||||||
|
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 ''
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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})',
|
||||||
|
|
||||||
|
|||||||
@@ -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})',
|
||||||
|
|||||||
Reference in New Issue
Block a user