修复: AI工具OOM/路径遍历+续跑锁收敛+UX交互批
This commit is contained in:
@@ -128,6 +128,11 @@
|
||||
:danger-label="t('common.confirm')"
|
||||
@result="answerConfirm"
|
||||
/>
|
||||
|
||||
<!-- UX-260617-14:审批 loading 超时兜底 toast(零依赖 reactive,复用 AiChat 同款样式) -->
|
||||
<Transition name="ai-tool-toast">
|
||||
<div v-if="approveToast.visible" class="ai-tool-toast ai-tool-toast--warning">{{ approveToast.msg }}</div>
|
||||
</Transition>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -202,7 +207,8 @@ function shouldKeepOpen(tc: AiToolCallInfo): boolean {
|
||||
* - run_command exit_code≠0:结果 JSON 合法,parseResult 命中,看 exit_code 判失败
|
||||
* - 通用工具失败:AR-6 路径后端塞 serde_json::Value::String("工具 X 执行失败: ..."),
|
||||
* 前端 parseResult 返回 null(非 JSON 对象)→ 走 raw 文本兜底,无结构信号
|
||||
* 此时按字符串前缀 "执行失败"/"failed" 启发式判定(对齐后端 err_msg 格式)。
|
||||
* 此时按行首前缀 "执行失败"/"Error:"/"Failed:" 启发式判定(对齐后端 err_msg 格式,
|
||||
* 行首锚定避免误伤含这些子串的正常产出,见 UX-260617-18)。
|
||||
*
|
||||
* 注意:status==='rejected'(用户拒绝)走独立分支,不算工具失败,不进此函数。
|
||||
* running/pending_approval 自然 false(无 result 或 result 未稳定)。
|
||||
@@ -220,7 +226,10 @@ function isToolFailure(tc: AiToolCallInfo): boolean {
|
||||
if (!parseResult(tc.result)) {
|
||||
const raw = typeof tc.result === 'string' ? tc.result : ''
|
||||
if (!raw) return false
|
||||
return /执行失败|failed|error[:\s]/i.test(raw)
|
||||
// UX-260617-18:收窄为行首锚定(原 /执行失败|failed|error[:\s]/i 匹配任意位置的子串,
|
||||
// 误伤 search_files 命中 error.log / run_command stderr 含 "warning" 等正常产出)。
|
||||
// 仅当某行以「执行失败」(中文 err_msg 前缀)/「Error:」/「Failed:」开头才判失败。
|
||||
return /^(执行失败|Error:|Failed:)/m.test(raw)
|
||||
}
|
||||
return false
|
||||
}
|
||||
@@ -464,10 +473,11 @@ function formatFileInfoResult(r: ToolResult): string {
|
||||
</script>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed, ref, watch, onBeforeUnmount } from 'vue'
|
||||
import { computed, ref, reactive, watch, onBeforeUnmount } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { useProjectStore } from '@/stores/project'
|
||||
import { useConfirm } from '@/composables/useConfirm'
|
||||
import { STREAM_TIMEOUT_MS } from '@/composables/ai/useAiStream'
|
||||
import ConfirmDialog from './ConfirmDialog.vue'
|
||||
|
||||
const { t } = useI18n()
|
||||
@@ -504,10 +514,23 @@ const emit = defineEmits<{
|
||||
* AiApprovalResult/approved 后 hang 在工具执行前),全局看门狗只回滚 running 不会触及本卡。
|
||||
* 故此处加本地兜底计时(对齐全局看门狗 STREAM_TIMEOUT_MS),到点复位 loading 让用户能重试/改判。
|
||||
*/
|
||||
const APPROVE_LOADING_TIMEOUT_MS = 130000 // 对齐 useAiStream.STREAM_TIMEOUT_MS
|
||||
// UX-260617-14:复用 useAiStream 共享常量(原硬编码 130000,单一来源防漂移)。
|
||||
const APPROVE_LOADING_TIMEOUT_MS = STREAM_TIMEOUT_MS
|
||||
const approving = ref(false)
|
||||
let approvingTimer: ReturnType<typeof setTimeout> | null = null
|
||||
|
||||
// ── 审批超时兜底 toast(零依赖,复用 AiChat/Settings 同款 reactive toast 模式) ──
|
||||
// ToolCard 是子组件无根 toast 挂载点,且可能以分离窗口运行(App.vue 根 toast 不在 DOM),
|
||||
// 故本卡自管一个最小 toast;仅在审批 loading 超时兜底时短暂提示。
|
||||
const approveToast = reactive({ visible: false, msg: '' })
|
||||
let _approveToastTimer: ReturnType<typeof setTimeout> | null = null
|
||||
function showApproveTimeoutToast(): void {
|
||||
approveToast.msg = t('aiTool.approveLoadingTimeout')
|
||||
approveToast.visible = true
|
||||
if (_approveToastTimer) clearTimeout(_approveToastTimer)
|
||||
_approveToastTimer = setTimeout(() => { approveToast.visible = false }, 3000)
|
||||
}
|
||||
|
||||
/**
|
||||
* AE-2025-05:High 风险工具白名单(后端 tool_registry.rs RiskLevel::High 对齐)。
|
||||
* AiToolCallInfo 无 risk 字段(风险仅存后端),故前端按工具名判定。新增 High 工具时同步加这里。
|
||||
@@ -538,9 +561,12 @@ async function onApprove(approved: boolean) {
|
||||
}
|
||||
approving.value = true
|
||||
// 本地兜底:到点强制复位 loading(后端无回执、卡片仍 pending_approval 时给用户重试入口)
|
||||
// UX-260617-14:超时复位同时给 warning toast + 控制台日志,原仅静默 approving=false 用户无感知。
|
||||
approvingTimer = setTimeout(() => {
|
||||
approving.value = false
|
||||
approvingTimer = null
|
||||
console.warn('[AI] 审批 loading 超时,后端可能未回执,已复位允许重试:', props.tc.id)
|
||||
showApproveTimeoutToast()
|
||||
}, APPROVE_LOADING_TIMEOUT_MS)
|
||||
emit('approve', { id: props.tc.id, approved })
|
||||
}
|
||||
@@ -554,6 +580,7 @@ watch(() => props.tc.status, (s) => {
|
||||
// 组件卸载清计时器防泄漏
|
||||
onBeforeUnmount(() => {
|
||||
if (approvingTimer) clearTimeout(approvingTimer)
|
||||
if (_approveToastTimer) clearTimeout(_approveToastTimer)
|
||||
})
|
||||
|
||||
/** 一次解析结果供模板多次读(模板引用 6 次,computed 避免每次 patch 重 parse) */
|
||||
@@ -707,6 +734,8 @@ function toolResultSummary(tc: AiToolCallInfo): string {
|
||||
overflow: hidden;
|
||||
max-width: var(--df-msg-max-width);
|
||||
animation: fadeInUp 0.2s var(--df-ease);
|
||||
/* UX-260617-14:相对定位锚定本卡 toast(absolute 子元素) */
|
||||
position: relative;
|
||||
/* UX-260616-03: border/shadow/opacity 过渡让折叠/失败态切换不突兀。
|
||||
不做 body 高度动画(v-show display 切换 + flex gap 会留双倍间距致布局跳变,
|
||||
见 ToolCardList 注释);opacity 过渡覆盖自动收起时的视觉跳变(约 200ms)。 */
|
||||
@@ -1174,4 +1203,27 @@ function toolResultSummary(tc: AiToolCallInfo): string {
|
||||
white-space: pre-wrap;
|
||||
word-break: break-all;
|
||||
}
|
||||
|
||||
/* -- UX-260617-14: 审批 loading 超时兜底 toast(复用 AiChat 同款视觉 token) -- */
|
||||
.ai-tool-toast {
|
||||
position: absolute;
|
||||
top: 6px;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
padding: 6px 12px;
|
||||
border-radius: var(--df-radius-sm);
|
||||
font-size: 11px;
|
||||
z-index: 100;
|
||||
box-shadow: 0 4px 16px rgba(0,0,0,0.3);
|
||||
max-width: 90%;
|
||||
overflow-wrap: anywhere;
|
||||
pointer-events: none;
|
||||
}
|
||||
.ai-tool-toast--warning {
|
||||
background: var(--df-warning-bg);
|
||||
color: var(--df-warning);
|
||||
border: 0.5px solid var(--df-warning);
|
||||
}
|
||||
.ai-tool-toast-enter-active, .ai-tool-toast-leave-active { transition: opacity 0.2s, transform 0.2s; }
|
||||
.ai-tool-toast-enter-from, .ai-tool-toast-leave-to { opacity: 0; transform: translate(-50%, -6px); }
|
||||
</style>
|
||||
|
||||
@@ -45,8 +45,8 @@ import { useI18n } from 'vue-i18n'
|
||||
import ToolCard from './ToolCard.vue'
|
||||
import type { AiToolCallInfo } from '@/api/types'
|
||||
|
||||
// useI18n needed for $t() in template (collapseAll / expandAll)
|
||||
useI18n()
|
||||
// useI18n needed for $t() in template (collapseAll / expandAll) + groupDisplayName(i18n key)
|
||||
const { t } = useI18n()
|
||||
|
||||
const props = defineProps<{
|
||||
/** 当前消息的工具调用列表 */
|
||||
@@ -166,6 +166,34 @@ function toggleAllGroups(): void {
|
||||
collapsedGroups.value = next
|
||||
}
|
||||
|
||||
/**
|
||||
* 分组显示名:工具名 → i18n 友好标签(英文化,原硬编码中文)。
|
||||
* UX-260617-17:用 aiTool.toolGroup.* 子键查表;未命中的工具回退 _ → 空格风格。
|
||||
*/
|
||||
// tool.name → aiTool.toolGroup.* 子键(与后端 tool_registry 工具名一一对应)
|
||||
const TOOL_GROUP_I18N_KEY: Record<string, string> = {
|
||||
read_file: 'readFile',
|
||||
write_file: 'writeFile',
|
||||
list_directory: 'listDirectory',
|
||||
create_task: 'createTask',
|
||||
create_project: 'createProject',
|
||||
create_idea: 'createIdea',
|
||||
list_tasks: 'listTasks',
|
||||
list_projects: 'listProjects',
|
||||
list_ideas: 'listIdeas',
|
||||
delete_project: 'deleteProject',
|
||||
restore_project: 'restoreProject',
|
||||
purge_project: 'purgeProject',
|
||||
update_project: 'updateProject',
|
||||
run_workflow: 'runWorkflow',
|
||||
}
|
||||
|
||||
function groupDisplayName(group: ToolCallGroup): string {
|
||||
const subKey = TOOL_GROUP_I18N_KEY[group.name]
|
||||
if (subKey) return t(`aiTool.toolGroup.${subKey}`)
|
||||
return group.name.replace(/_/g, ' ')
|
||||
}
|
||||
|
||||
/**
|
||||
* 单卡是否展开:
|
||||
* - 分组未折叠 → 用户手动 expandedCards 或首卡默认展开
|
||||
@@ -253,28 +281,6 @@ function groupIcon(name: string): string {
|
||||
if (name.includes('delete')) return '<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polyline points="3 6 5 6 21 6"/><path d="M19 6v14a2 2 0 01-2 2H7a2 2 0 01-2-2V6m3 0V4a2 2 0 012-2h4a2 2 0 012 2v2"/></svg>'
|
||||
return '<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="11" cy="11" r="8"/><line x1="21" y1="21" x2="16.65" y2="16.65"/></svg>'
|
||||
}
|
||||
|
||||
/** 分组显示名:工具名 + 数量提示 */
|
||||
function groupDisplayName(group: ToolCallGroup): string {
|
||||
const nameMap: Record<string, string> = {
|
||||
read_file: '读取文件',
|
||||
write_file: '写入文件',
|
||||
list_directory: '列出目录',
|
||||
create_task: '创建任务',
|
||||
create_project: '创建项目',
|
||||
create_idea: '创建灵感',
|
||||
list_tasks: '列出任务',
|
||||
list_projects: '列出项目',
|
||||
list_ideas: '列出灵感',
|
||||
delete_project: '删除项目',
|
||||
restore_project: '恢复项目',
|
||||
purge_project: '彻底删除项目',
|
||||
update_project: '更新项目',
|
||||
run_workflow: '运行工作流',
|
||||
}
|
||||
const label = nameMap[group.name] || group.name.replace(/_/g, ' ')
|
||||
return label
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
|
||||
@@ -9,10 +9,15 @@ import { useAppSettingsStore } from '@/stores/appSettings'
|
||||
import { state } from '@/stores/ai'
|
||||
import { notifyConversationChanged } from './useAiEvents'
|
||||
import { persistUiState } from './useAiPanel'
|
||||
import type { AiToolCallInfo } from '@/api/types'
|
||||
import { nextMsgId } from './aiShared'
|
||||
import i18n from '@/i18n'
|
||||
import type { AiMessage, AiToolCallInfo } from '@/api/types'
|
||||
|
||||
const appSettings = useAppSettingsStore()
|
||||
|
||||
// composable 无组件上下文,借全局 i18n 实例翻译错误文案(同 useAiSend/aiShared 模式)
|
||||
const t = ((i18n as any).global.t as (k: string, named?: Record<string, unknown>) => string).bind((i18n as any).global)
|
||||
|
||||
/** 拉取会话列表;首次加载时若 appSettings 仍有有效 active-conv 且当前无活跃对话则恢复 */
|
||||
async function loadConversations() {
|
||||
try {
|
||||
@@ -22,8 +27,21 @@ async function loadConversations() {
|
||||
if (pending && !state.activeConversationId && state.conversations.some(c => c.id === pending)) {
|
||||
await switchConversation(pending)
|
||||
}
|
||||
} catch {
|
||||
// 静默失败,不影响主流程
|
||||
} catch (e) {
|
||||
// UX-260617-08:网络抖动/IPC 断开时原静默失败,列表可能突空用户不知原因。
|
||||
// 此处保留旧列表(赋值语句抛出,旧 state.conversations 不被覆盖)+ 推错误气泡反馈。
|
||||
// 仅首次加载(state.conversations 为空)时推气泡,避免拉取/侧栏刷新重试时反复弹错。
|
||||
console.error('[AI] 加载会话列表失败:', e)
|
||||
if (state.conversations.length === 0) {
|
||||
state.messages.push({
|
||||
id: `load-conv-fail-${nextMsgId()}`,
|
||||
role: 'assistant',
|
||||
content: t('ai.loadConvFail'),
|
||||
isError: true,
|
||||
timestamp: Date.now(),
|
||||
} as AiMessage)
|
||||
notifyConversationChanged()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -114,8 +132,17 @@ export async function switchConversation(id: string) {
|
||||
}
|
||||
}),
|
||||
}))
|
||||
} catch {
|
||||
state.messages = []
|
||||
} catch (e) {
|
||||
// UX-260617-08:历史消息解析/映射失败原仅 state.messages=[] → 切换后空白用户不知原因。
|
||||
// 改为推错误气泡提示 + 控制台日志(保留 state.messages 不再清空,避免空白无反馈)。
|
||||
console.error('[AI] 切换对话历史消息解析失败:', e)
|
||||
state.messages.push({
|
||||
id: `switch-conv-fail-${nextMsgId()}`,
|
||||
role: 'assistant',
|
||||
content: t('ai.switchConvFail'),
|
||||
isError: true,
|
||||
timestamp: Date.now(),
|
||||
} as AiMessage)
|
||||
}
|
||||
|
||||
// 加载历史对话 token 总量(来自 DB summary);切换对话清空实时值
|
||||
|
||||
@@ -326,11 +326,15 @@ export function handleEvent(event: AiChatEvent) {
|
||||
case 'AiError': {
|
||||
clearStreamWatchdog()
|
||||
clearAllToolSlowTimers() // B-260616-12: 错误收尾清全部工具慢执行计时器与已提示集合
|
||||
clearAllApprovalTimers() // UX-260617-10: 错误中断释放所有审批超时计时器,防回调改 state 触发已卸载/已错流程
|
||||
state.streaming = false
|
||||
state.generatingConvId = null
|
||||
state.currentText = ''
|
||||
state.agentRound = 0 // AE-2025-07: agentic 异常中断,复位轮次
|
||||
state.queue = [] // B-32:错误收尾清队列,防生成中入队的消息被静默丢失(drainQueue 仅 AiCompleted 触发)
|
||||
// UX-260617-10: 错误收尾清残留待审批项——错误发生时若有工具停在 pending_approval,
|
||||
// 残留可点击审批按钮会让用户误以为还能批(实际后端已终止),残留审批卡误导操作。
|
||||
state.pendingApprovals = []
|
||||
pendingMaxRounds.value = false // F-260616-03: 异常中断,清操作卡
|
||||
localStorage.removeItem('df-ai-gen')
|
||||
localStorage.removeItem('df-ai-text')
|
||||
@@ -356,14 +360,23 @@ export async function startListener() {
|
||||
if (_unlistenAiEvent && _unlistenConvChanged && _unlistenApprovalClear) return
|
||||
// 并发去重:防 onMounted 与 sendMessage 首发竞态重复注册 listener → 文字双倍
|
||||
if (_startPromise) return _startPromise
|
||||
// UX-260617-09: 内部任一 await reject 时,onMounted/sendMessage 的 await 在 Vue async
|
||||
// 生命周期/void 调用点处成为未捕获 rejection(无 console 输出)→ listener 未注册且无任何提示,
|
||||
// 表现为「发消息永远不回」。这里 catch + console.error 保留可观测性,再 throw 让外层
|
||||
// finally 清 _startPromise(并发去重复位),调用方仍可 try/catch 兜底。
|
||||
_startPromise = (async () => {
|
||||
_unlistenAiEvent = await aiApi.onEvent(handleEvent)
|
||||
_unlistenConvChanged = await listen('ai-conversation-changed', () => { void loadConversations() })
|
||||
// AE-2025-06: onStreamTimeout(流式整流超时收尾)广播的清理事件 → 清全部审批超时计时器。
|
||||
// useAiStream 不 import useAiSend(避循环依赖),经事件总线中转。
|
||||
_unlistenApprovalClear = await listen('ai-approval-clear-timers', () => { clearAllApprovalTimers() })
|
||||
// B-260616-01: L0 握手 — 通知后端前端已就绪,后端据此清除 HMR/刷新导致的残留 generating 状态
|
||||
emit('ai-client-ready', { lastConvId: state.activeConversationId })
|
||||
try {
|
||||
_unlistenAiEvent = await aiApi.onEvent(handleEvent)
|
||||
_unlistenConvChanged = await listen('ai-conversation-changed', () => { void loadConversations() })
|
||||
// AE-2025-06: onStreamTimeout(流式整流超时收尾)广播的清理事件 → 清全部审批超时计时器。
|
||||
// useAiStream 不 import useAiSend(避循环依赖),经事件总线中转。
|
||||
_unlistenApprovalClear = await listen('ai-approval-clear-timers', () => { clearAllApprovalTimers() })
|
||||
// B-260616-01: L0 握手 — 通知后端前端已就绪,后端据此清除 HMR/刷新导致的残留 generating 状态
|
||||
emit('ai-client-ready', { lastConvId: state.activeConversationId })
|
||||
} catch (err) {
|
||||
console.error('[useAiEvents] startListener 注册失败,事件监听未生效:', err)
|
||||
throw err
|
||||
}
|
||||
})()
|
||||
try {
|
||||
await _startPromise
|
||||
@@ -382,6 +395,10 @@ function stopListener() {
|
||||
_unlistenAiEvent = null
|
||||
_unlistenConvChanged = null
|
||||
_unlistenApprovalClear = null
|
||||
// UX-260617-26: stop 清零去重 promise——极速 mount/unmount/mount(HMR)时,首 mount 的
|
||||
// _startPromise 可能仍在 pending(未 await 完即 unmount),再 mount 若命中并发去重分支会
|
||||
// 返回这个已代表「旧注册」的过期 promise,新 listener 实际未注册。
|
||||
_startPromise = null
|
||||
clearStreamWatchdog()
|
||||
clearAllToolSlowTimers() // B-260616-12: 卸载时释放所有工具慢执行计时器,防回调改 state 触发已卸载组件
|
||||
clearAllApprovalTimers() // AE-2025-06: 卸载时释放所有审批超时计时器,防回调 push 消息触发已卸载组件
|
||||
|
||||
@@ -480,6 +480,13 @@ async function sendQueuedNow(index: number) {
|
||||
|
||||
/** 停止当前生成:本地先复位 streaming,再发停止信号。
|
||||
* 不依赖后端 AiCompleted 收尾——审批态看门狗已 clear(useAiEvents),若 AiCompleted 竞态丢失则 streaming 永久 true 卡死,故本地兜底。
|
||||
*
|
||||
* UX-260617-19: stop IPC 失败时的兜底。本地已先置 streaming=false,但若 stop 信号未送达后端,
|
||||
* 后端会继续生成并发回 delta/Completed 事件,与用户已感知的「已停止」语义冲突;更坏情况是
|
||||
* 后端 hang 既不收尾也不发事件(stop_flag 未置位、无 watchdog 兜底则无恢复路径)。
|
||||
* 故 stop 失败时不 clearStreamWatchdog,反而重启看门狗:后端若继续生成,活跃 delta 会不断重置它
|
||||
* 不误触发;后端若真 hang,130s 后 onStreamTimeout 兜底复位 streaming+清队列+推错误气泡。
|
||||
* 同时推一条 stopLoopFailed 错误消息让用户知晓停止未生效(可再点或等待收尾)。
|
||||
*/
|
||||
async function stopChat() {
|
||||
state.streaming = false // 本地先复位,不依赖后端 AiCompleted(审批态看门狗已 clear,竞态丢失则卡死)
|
||||
@@ -487,7 +494,22 @@ async function stopChat() {
|
||||
// UX-260616-05 决策 a(逐条续发):不再清队列,保留供 AiCompleted 链式触发 drainQueue 续发。
|
||||
// 后端 agentic.rs:190 emit AiCompleted 注释明确「保证前端收事件时后端已可接下一条,队列续发不被拒」;
|
||||
// 当前未完成回复已入库(agentic.rs:185 save_conversation)保留为截断回复。
|
||||
await aiApi.stopChat()
|
||||
try {
|
||||
await aiApi.stopChat()
|
||||
} catch (e) {
|
||||
// stop 信号未送达后端:重启看门狗(后端继续生成时活跃事件会重置它,真 hang 则 130s 后兜底收尾),
|
||||
// 避免后端仍在跑却无任何恢复路径(原 clearStreamWatchdog 已把兜底关掉)。
|
||||
resetStreamWatchdog()
|
||||
const errMsg = e instanceof Error ? e.message : String(e)
|
||||
state.messages.push({
|
||||
id: `stop-err-${nextMsgId()}`,
|
||||
role: 'assistant',
|
||||
content: t('aiChat.stopLoopFailed', { msg: errMsg }),
|
||||
isError: true,
|
||||
timestamp: Date.now(),
|
||||
} as AiMessage)
|
||||
console.error('[AI] stopChat IPC 失败,已重启看门狗兜底:', e)
|
||||
}
|
||||
}
|
||||
|
||||
export function useAiSend() {
|
||||
|
||||
@@ -38,6 +38,9 @@ export default {
|
||||
approvalNotDelivered: 'Approval operation failed to reach the backend: {error}',
|
||||
queuedSendFailed: 'Failed to send queued message: {error}',
|
||||
approvalTimeout: '⚰ Approval timed out, automatically rejected: {toolName}',
|
||||
// UX-260617-08: conversation load/switch failure feedback (network jitter/IPC down → empty list/blank, was silent)
|
||||
loadConvFail: 'Failed to load conversations. Please check your network or backend connection.',
|
||||
switchConvFail: 'Failed to switch conversation. Please retry.',
|
||||
// AE-2025-07: Agentic loop progress bar copy
|
||||
// Note: max round / completed tool count are not surfaced by backend this round;
|
||||
// template conditionally omits them. Add *WithMax keys once backend exposes them.
|
||||
|
||||
@@ -82,6 +82,30 @@ export default {
|
||||
// Tool result detail format (plain-text result + approval placeholder appends arg summary)
|
||||
resultDetail: '{raw} ({detail})',
|
||||
|
||||
// UX-260617-17: ToolCardList group header (shown when 2+ same-name tools; was hardcoded zh)
|
||||
toolGroup: {
|
||||
readFile: 'Read File',
|
||||
writeFile: 'Write File',
|
||||
listDirectory: 'List Directory',
|
||||
createTask: 'Create Task',
|
||||
createProject: 'Create Project',
|
||||
createIdea: 'Create Idea',
|
||||
listTasks: 'List Tasks',
|
||||
listProjects: 'List Projects',
|
||||
listIdeas: 'List Ideas',
|
||||
deleteProject: 'Delete Project',
|
||||
restoreProject: 'Restore Project',
|
||||
purgeProject: 'Purge Project',
|
||||
updateProject: 'Update Project',
|
||||
runWorkflow: 'Run Workflow',
|
||||
},
|
||||
|
||||
// UX-260617-14: approval loading fallback timeout notice (no backend ack, card still pending_approval → reset + prompt)
|
||||
approveLoadingTimeout: 'Approval response timed out, please retry or check the backend',
|
||||
|
||||
// UX-260617-18: tool failure detection copy (regex narrowed to line-anchored err_msg prefix)
|
||||
// (reuse executionFailed/executionFailedHint, no new key)
|
||||
|
||||
// Approval argument key labels
|
||||
argLabel: {
|
||||
id: 'ID',
|
||||
|
||||
@@ -38,6 +38,9 @@ export default {
|
||||
approvalNotDelivered: '审批操作未送达后端:{error}',
|
||||
queuedSendFailed: '队列消息发送失败:{error}',
|
||||
approvalTimeout: '⚰ 审批超时,已自动拒绝:{toolName}',
|
||||
// UX-260617-08:对话加载/切换失败反馈(网络抖动/IPC 断开 → 列表空/切换空白,原静默无反馈)
|
||||
loadConvFail: '对话列表加载失败,请检查网络或后端连接',
|
||||
switchConvFail: '对话切换失败,请重试',
|
||||
// AE-2025-07: Agentic 循环进度条文案
|
||||
// 注:max 轮次/已完成工具数本轮后端暂不透传,模板条件渲染省略;后端补全后新增 *WithMax key
|
||||
agenticProgress: '🔄 循环 {round} · ⏳{pending}待审批',
|
||||
|
||||
@@ -85,6 +85,30 @@ export default {
|
||||
// 工具结果详情格式(非 JSON 纯文本 + 审批占位时补参数摘要)
|
||||
resultDetail: '{raw}({detail})',
|
||||
|
||||
// UX-260617-17:ToolCardList 分组标题(同名工具 >=2 时显示;原硬编码中文,英文化)
|
||||
toolGroup: {
|
||||
readFile: '读取文件',
|
||||
writeFile: '写入文件',
|
||||
listDirectory: '列出目录',
|
||||
createTask: '创建任务',
|
||||
createProject: '创建项目',
|
||||
createIdea: '创建灵感',
|
||||
listTasks: '列出任务',
|
||||
listProjects: '列出项目',
|
||||
listIdeas: '列出灵感',
|
||||
deleteProject: '删除项目',
|
||||
restoreProject: '恢复项目',
|
||||
purgeProject: '彻底删除项目',
|
||||
updateProject: '更新项目',
|
||||
runWorkflow: '运行工作流',
|
||||
},
|
||||
|
||||
// UX-260617-14:审批 loading 超时兜底提示(后端无回执,卡片仍 pending_approval 时复位 + 提示用户)
|
||||
approveLoadingTimeout: '审批响应超时,请重试或检查后端',
|
||||
|
||||
// UX-260617-18:工具失败检测文案(收窄正则后,行首锚定 err_msg 前缀)
|
||||
// (复用 executionFailed/executionFailedHint,不新增)
|
||||
|
||||
// 审批参数键标签
|
||||
argLabel: {
|
||||
id: 'ID',
|
||||
|
||||
Reference in New Issue
Block a user