优化: 前端走查 P2+UX(status union + ApprovalPopup hack消除/拖动JS API/Esc)
- status union: AiMessage 加 status 字面量联合,删 AiChat/MessageList AiMessageWithStatus 重复+cast - ApprovalPopup: 删 _placeholder hack(toolDisplayName 提纯函数) + dispatchApprovalIPC 共享分派消除三套重复 + always 按钮语义对齐 + closeWithFallback 兜底 + 拖动 JS API startDragging(避子元素覆盖) + Esc 快捷键
This commit is contained in:
@@ -2,13 +2,19 @@
|
||||
* ToolCard 头部显示逻辑(从 ToolCard.vue 抽离)。
|
||||
*
|
||||
* 职责:工具显示名 + 审批参数值的「语义化回显」(裸 id → 项目名/任务标题),
|
||||
* 以及 http url → host 精简。这些依赖 project store(响应式),故以 composable 形式封装,
|
||||
* 传入 tc 即得 { toolDisplayName, displayArgValue }。
|
||||
* 以及 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 { useI18n } from 'vue-i18n'
|
||||
import { t as tGlobal } from '@/i18n/i18n-helpers'
|
||||
import {
|
||||
formatToolName,
|
||||
shortCmd,
|
||||
@@ -38,32 +44,83 @@ const TASK_ID_TOOL_ARG: Record<string, 'id' | '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) {
|
||||
const t = useI18n().t
|
||||
const projectStore = useProjectStore()
|
||||
|
||||
/**
|
||||
* 按 id 查项目名(覆盖活跃 + 回收站):审批 delete/restore/purge 各阶段都可能引用项目,
|
||||
* 故先查 projects 再查 deletedProjects,找到即返回(提前退出),无则返回 undefined(调用方走 fallback)。
|
||||
* 复用 project store 已加载列表,不新增网络请求;响应式由 store 数组保证。
|
||||
*/
|
||||
function projectNameById(id: string): string | undefined {
|
||||
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(仅当前项目范围),不新增网络请求。
|
||||
*/
|
||||
function taskNameById(id: string): string | undefined {
|
||||
return projectStore.tasks.find(tk => tk.id === id)?.title
|
||||
}
|
||||
|
||||
/**
|
||||
* 审批参数值展示(AR-3):对项目类工具的 id/project_id 特化——优先回显项目名,查不到则显示
|
||||
* 「项目已不存在」提示 + 原 id。非项目工具或其他 key 走通用 formatArgValue。
|
||||
@@ -75,8 +132,8 @@ export function useToolCardHeader(getTc: () => AiToolCallInfo) {
|
||||
const id = typeof arg.val === 'string' ? arg.val : ''
|
||||
if (id) {
|
||||
const name = projectNameById(id)
|
||||
if (name) return t('aiTool.projectLabel', { name })
|
||||
return t('aiTool.projectIdNotFound', { id })
|
||||
if (name) return tGlobal('aiTool.projectLabel', { name })
|
||||
return tGlobal('aiTool.projectIdNotFound', { id })
|
||||
}
|
||||
}
|
||||
// UX-260618-14:advance_task/run_workflow 的 task_id → 任务标题
|
||||
@@ -85,61 +142,13 @@ export function useToolCardHeader(getTc: () => AiToolCallInfo) {
|
||||
const id = typeof arg.val === 'string' ? arg.val : ''
|
||||
if (id) {
|
||||
const name = taskNameById(id)
|
||||
if (name) return t('aiTool.taskLabel', { name })
|
||||
return t('aiTool.taskIdNotFound', { id })
|
||||
if (name) return tGlobal('aiTool.taskLabel', { name })
|
||||
return tGlobal('aiTool.taskIdNotFound', { id })
|
||||
}
|
||||
}
|
||||
return formatArgValue(arg.val)
|
||||
}
|
||||
|
||||
/**
|
||||
* 工具显示名称(含目标摘要);文件类按 path,项目类按 id 解析项目名,create_* 用固定文案。
|
||||
*/
|
||||
function toolDisplayName(tc: AiToolCallInfo): string {
|
||||
const p = argString(tc.args, 'path')
|
||||
switch (tc.name) {
|
||||
case 'read_file': return p ? `${t('aiTool.readPrefix')} ${shortPath(p)}` : t('aiTool.readFallback')
|
||||
case 'list_directory': return p ? `${t('aiTool.dirPrefix')} ${shortPath(p)}` : t('aiTool.dirFallback')
|
||||
case 'write_file': return p ? `${t('aiTool.writePrefix')} ${shortPath(p)}` : t('aiTool.writeFallback')
|
||||
case 'grep': {
|
||||
// grep 头部:搜索根 + pattern(折叠态可见搜索什么)
|
||||
const pat = argString(tc.args, 'pattern')
|
||||
const head = p ? `${t('aiTool.grepPrefix')} ${shortPath(p)}` : t('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 ? `${t('aiTool.' + prefixKey)}「${name}」` : t('aiTool.' + fallbackKey)
|
||||
}
|
||||
case 'create_task': return t('aiTool.createTask')
|
||||
case 'create_project': return t('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}` : t('aiTool.httpRequestFallback')
|
||||
}
|
||||
case 'fetch_url': {
|
||||
// fetch_url 头部:获取 host(对齐 http_request 精简模式,折叠态一眼知抓哪个站点)
|
||||
const url = argString(tc.args, 'url')
|
||||
const host = httpHost(url)
|
||||
return host ? `${t('aiTool.fetchUrlPrefix')} ${host}` : t('aiTool.fetchUrlFallback')
|
||||
}
|
||||
default: return formatToolName(tc.name)
|
||||
}
|
||||
}
|
||||
|
||||
/** 审批参数键值对(头部 toolArgsEntries 透传,本 composable 仅暴露便于复用) */
|
||||
function argsEntries() {
|
||||
return toolArgsEntries(getTc().args)
|
||||
|
||||
Reference in New Issue
Block a user