/** * ToolCard 头部显示逻辑(从 ToolCard.vue 抽离)。 * * 职责:工具显示名 + 审批参数值的「语义化回显」(裸 id → 项目名/任务标题), * 以及 http url → host 精简。 * * 两层导出: * - `toolDisplayName(tc)` 模块级纯函数(i18n 走 i18n-helpers 全局 t,project store 走单例): * 无 setup 上下文依赖,可被 ToolCard(经 composable 包装)与 ApprovalPopup(独立 WebviewWindow, * 无 composable 上下文)共用。抽离自原 composable 内部函数,消除 ApprovalPopup 的 _placeholder hack。 * - `useToolCardHeader(getTc)` composable:封装 displayArgValue/argsEntries(依赖 getTc 取当前 tc, * 响应式由 store 保证),返回 { toolDisplayName(转发模块级), displayArgValue, argsEntries }。 * * 不含响应式状态(纯函数 + store 读),逻辑与原 ToolCard.vue 内联实现一字等价。 */ import { useProjectStore } from '@/stores/project' import { t as tGlobal } from '@/i18n/i18n-helpers' import { formatToolName, shortCmd, toolArgsEntries, formatArgValue, shortPath, argString, } from '@/composables/ai/useToolCard' import type { AiToolCallInfo } from '@/api/types' /** 工具名 → 该工具中代表项目 id 的参数 key(仅项目类工具特化;delete_task 的 id 是任务 id,不在此列) */ const PROJECT_ID_TOOL_ARG: Record = { delete_project: 'id', restore_project: 'id', purge_project: 'id', update_project: 'id', bind_directory: 'id', create_task: 'project_id', } /** * 工具名 → 该工具中代表任务 id 的参数 key(任务类工具特化)。 * advance_task 的任务 id 在 "id" key,run_workflow 在 "task_id" key。 */ const TASK_ID_TOOL_ARG: Record = { advance_task: 'id', run_workflow: 'task_id', } /** * 按 id 查项目名(覆盖活跃 + 回收站):审批 delete/restore/purge 各阶段都可能引用项目, * 故先查 projects 再查 deletedProjects,找到即返回(提前退出),无则返回 undefined(调用方走 fallback)。 * 复用 project store 已加载列表(useProjectStore 单例缓存,每次调用廉价),不新增网络请求;响应式由 store 数组保证。 * 函数体内调 useProjectStore()(非模块级缓存)以保留原 lazy-init 时机,避免模块 import 时创建 store 改变初始化顺序。 */ function projectNameById(id: string): string | undefined { const projectStore = useProjectStore() return projectStore.projects.find(p => p.id === id)?.name ?? projectStore.deletedProjects.find(p => p.id === id)?.name } /** * 按 id 查任务标题:审批 advance_task/run_workflow 的 task_id 时,把裸 UUID 转标题。 * 复用 project store 已加载的 state.tasks(仅当前项目范围,useProjectStore 单例),不新增网络请求。 */ function taskNameById(id: string): string | undefined { return useProjectStore().tasks.find(tk => tk.id === id)?.title } /** * 工具显示名称(含目标摘要);文件类按 path,项目类按 id 解析项目名,create_* 用固定文案。 * * 模块级纯函数:无 setup 依赖(i18n 走 i18n-helpers 的全局 t,project store 走单例), * 故可被 ToolCard(经 useToolCardHeader 包装)与 ApprovalPopup(独立 WebviewWindow,无 store/emit * 上下文)共用,消除 ApprovalPopup 原 `_placeholder` 占位 hack(构造空 AiToolCallInfo 喂 composable)。 */ export function toolDisplayName(tc: AiToolCallInfo): string { const p = argString(tc.args, 'path') switch (tc.name) { case 'read_file': return p ? `${tGlobal('aiTool.readPrefix')} ${shortPath(p)}` : tGlobal('aiTool.readFallback') case 'list_directory': return p ? `${tGlobal('aiTool.dirPrefix')} ${shortPath(p)}` : tGlobal('aiTool.dirFallback') case 'write_file': return p ? `${tGlobal('aiTool.writePrefix')} ${shortPath(p)}` : tGlobal('aiTool.writeFallback') case 'grep': { // grep 头部:搜索根 + pattern(折叠态可见搜索什么) const pat = argString(tc.args, 'pattern') const head = p ? `${tGlobal('aiTool.grepPrefix')} ${shortPath(p)}` : tGlobal('aiTool.grepFallback') return pat ? `${head} 「${pat}」` : head } case 'delete_project': case 'restore_project': case 'purge_project': case 'update_project': { const idKey = PROJECT_ID_TOOL_ARG[tc.name] const id = idKey ? argString(tc.args, idKey) : '' const name = id ? projectNameById(id) : '' const prefixKey = `${tc.name.replace('_project', '')}Prefix` as 'deletePrefix' | 'restorePrefix' | 'purgePrefix' | 'updatePrefix' const fallbackKey = `${tc.name.replace('_project', '')}Fallback` as 'deleteFallback' | 'restoreFallback' | 'purgeFallback' | 'updateFallback' return name ? `${tGlobal('aiTool.' + prefixKey)}「${name}」` : tGlobal('aiTool.' + fallbackKey) } case 'create_task': return tGlobal('aiTool.createTask') case 'create_project': return tGlobal('aiTool.createProject') case 'run_command': { const cmd = argString(tc.args, 'command') return cmd ? `$ ${shortCmd(cmd)}` : formatToolName(tc.name) } case 'http_request': { const method = argString(tc.args, 'method') || 'GET' const url = argString(tc.args, 'url') const host = httpHost(url) return host ? `${method} ${host}` : tGlobal('aiTool.httpRequestFallback') } case 'fetch_url': { // fetch_url 头部:获取 host(对齐 http_request 精简模式,折叠态一眼知抓哪个站点) const url = argString(tc.args, 'url') const host = httpHost(url) return host ? `${tGlobal('aiTool.fetchUrlPrefix')} ${host}` : tGlobal('aiTool.fetchUrlFallback') } default: return formatToolName(tc.name) } } /** * ToolCard 头部/审批参数显示辅助。 * @param getTc 取当前 tc 的函数(避免 props.tc 在 composable 调用时机脱节,由调用方透传) */ export function useToolCardHeader(getTc: () => AiToolCallInfo) { /** * 审批参数值展示对项目类工具的 id/project_id 特化——优先回显项目名,查不到则显示 * 「项目已不存在」提示 + 原 id。非项目工具或其他 key 走通用 formatArgValue。 */ function displayArgValue(arg: { key: string; val: unknown }): string { const tc = getTc() const projectArgKey = PROJECT_ID_TOOL_ARG[tc.name] if (projectArgKey && arg.key === projectArgKey) { const id = typeof arg.val === 'string' ? arg.val : '' if (id) { const name = projectNameById(id) if (name) return tGlobal('aiTool.projectLabel', { name }) return tGlobal('aiTool.projectIdNotFound', { id }) } } // advance_task/run_workflow 的 task_id → 任务标题 const taskArgKey = TASK_ID_TOOL_ARG[tc.name] if (taskArgKey && arg.key === taskArgKey) { const id = typeof arg.val === 'string' ? arg.val : '' if (id) { const name = taskNameById(id) if (name) return tGlobal('aiTool.taskLabel', { name }) return tGlobal('aiTool.taskIdNotFound', { id }) } } return formatArgValue(arg.val) } /** 审批参数键值对(头部 toolArgsEntries 透传,本 composable 仅暴露便于复用) */ function argsEntries() { return toolArgsEntries(getTc().args) } return { toolDisplayName, displayArgValue, argsEntries } } /** * http_request url → host 显示用(折叠态头部精简,不全显 url)。 * 取 scheme://host[:port],剥 path/query;非 http(s) url 返回空串回退通用名。 * 注:用 URL 构造器容错,非法 url 抛错时回退原 url 截断。 */ export function httpHost(raw: string): string { if (!raw) return '' try { const u = new URL(raw) if (u.protocol !== 'http:' && u.protocol !== 'https:') return '' const portPart = u.port ? `:${u.port}` : '' return `${u.hostname}${portPart}` } catch { return raw.length > 40 ? raw.slice(0, 40) + '…' : raw } }