后端: - 工作流推进链(D-03):advance_task/状态机/闸门走 df-nodes Node trait,conditions 条件引擎扩展 - 想法评估闭环:启发式评分+对抗评估,df-ideas/scoring + df-storage/idea_eval_repo + idea 前端打通 - 全局事件数据总线:df-ai/context+context_helpers+augmentation 跨模块解耦 - AI planner/plan_hint/intent:aichat B 路线并行多轮基础 - patch_file 加固(TD-03/04):读改写整体锁防 lost update,expected_hash 合约闭环 - 压缩超时兜底(F-15 卡死根治) - F-09 多会话并发:LlmConcurrency per-conv + streamingGuard 前端守护 + verify 脚本 - 知识注入 DRY/skills/audit 扩展 清理: - aichat 技术债(误报 allow/死导入/过时注释 30 项) - URGENT.md 删除(11 项加急全解决/迁 todo) - 文档整理(todo/待决策/待审查/ARCHITECTURE/INDEX + 总线/技术债审查新文档)
161 lines
6.5 KiB
TypeScript
161 lines
6.5 KiB
TypeScript
/**
|
|
* ToolCard 头部显示逻辑(从 ToolCard.vue 抽离)。
|
|
*
|
|
* 职责:工具显示名 + 审批参数值的「语义化回显」(裸 id → 项目名/任务标题),
|
|
* 以及 http url → host 精简。这些依赖 project store(响应式),故以 composable 形式封装,
|
|
* 传入 tc 即得 { toolDisplayName, displayArgValue }。
|
|
*
|
|
* 不含响应式状态(纯函数 + store 读),逻辑与原 ToolCard.vue 内联实现一字等价。
|
|
*/
|
|
import { useProjectStore } from '@/stores/project'
|
|
import { useI18n } from 'vue-i18n'
|
|
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<string, 'id' | 'project_id'> = {
|
|
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<string, 'id' | 'task_id'> = {
|
|
advance_task: 'id',
|
|
run_workflow: 'task_id',
|
|
}
|
|
|
|
/**
|
|
* 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。
|
|
*/
|
|
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 t('aiTool.projectLabel', { name })
|
|
return t('aiTool.projectIdNotFound', { id })
|
|
}
|
|
}
|
|
// UX-260618-14: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 t('aiTool.taskLabel', { name })
|
|
return t('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')
|
|
}
|
|
default: return formatToolName(tc.name)
|
|
}
|
|
}
|
|
|
|
/** 审批参数键值对(头部 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
|
|
}
|
|
}
|