优化: UX错误气泡区分重试类型+工具卡按轮次自动收起

This commit is contained in:
2026-06-16 23:48:06 +08:00
parent 107b8486ae
commit bebf2ec329
6 changed files with 77 additions and 20 deletions

View File

@@ -401,13 +401,18 @@
</button>
</div>
<!-- UX-03:错误气泡操作入口(重试 / 去设置)
isError 消息显示;去设置 error_type {auth, provider_config} 显示 -->
<!-- UX-03 / UX-260616-01:错误气泡操作入口(重试 / 去设置)
isError 消息显示;重试可重试错误(network/timeout/unknown)显示,
Fatal (auth/provider_config 鉴权/4xx/参数错)重试必败故隐藏;
去设置 error_type {auth, provider_config} 显示;
Fatal 时附 notRetryable 提示,说明为何无重试按钮 -->
<div
v-if="msg.isError && !store.state.streaming"
class="ai-msg-actions ai-msg-actions--error"
>
<span v-if="!canRetry(msg)" class="ai-msg-not-retryable">{{ $t('ai.notRetryable') }}</span>
<button
v-if="canRetry(msg)"
class="ai-msg-act ai-msg-act--primary"
:disabled="store.state.streaming"
@click.stop="handleRetry"
@@ -1632,6 +1637,31 @@ onBeforeUnmount(() => {
// (deep watch 检测仍每帧跑,但 body 跳过)。剔除 len 后流式 delta 不再触发收起重算,
// 只在 toolCall 状态翻转(新卡片/完成/拒绝)时触发,与折叠语义精确对齐。
// snap/MESSAGE_CAP 兜底机制保持不变(deep watch 仍做变化检测,只是 body 短路掉无意义重算)。
//
// UX-260616-03: 触发时机扩展。除原 messages deep watch(toolCall 状态翻转/新消息条数),
// 另加 agentRound watch(AiAgentRound 新轮次 → 收起上一轮工具卡,聚焦当前轮)。
// active Set 推导逻辑复用 buildActiveToolIds,两 watch 共用。
function buildActiveToolIds(msgs: typeof store.state.messages): Set<string> {
const active = new Set<string>()
for (const m of msgs) {
for (const t of (m.toolCalls || [])) {
// 与 ToolCard.shouldKeepOpen 同源:running/pending_approval/rejected/write_file 保持展开
if (t.status === 'running' || t.status === 'pending_approval'
|| t.status === 'rejected' || t.name === 'write_file') {
active.add(t.id)
}
}
}
return active
}
/** 逐实例调 collapseInactive(v-for 内 ref 收集为数组,逐实例调否则报 not a function) */
function collapseAllToolLists(active: Set<string>): void {
const tcl = toolCardListRef.value as unknown
const refs = Array.isArray(tcl) ? tcl : (tcl ? [tcl] : [])
refs.forEach((r: any) => r?.collapseInactive?.(active))
}
let lastMsgSnapshot = ''
watch(
() => store.state.messages,
@@ -1644,25 +1674,21 @@ watch(
const isFirst = lastMsgSnapshot === ''
lastMsgSnapshot = snap
if (isFirst) return // 首次加载/切换对话不触发收起
const active = new Set<string>()
for (const m of msgs) {
for (const t of (m.toolCalls || [])) {
// 与 ToolCard.shouldKeepOpen 同源:running/pending_approval/rejected/write_file 保持展开
if (t.status === 'running' || t.status === 'pending_approval'
|| t.status === 'rejected' || t.name === 'write_file') {
active.add(t.id)
}
}
}
// ref 绑定在 v-for 内 → Vue 3 收集为数组;逐实例调用 collapseInactive
// 否则数组上访问 .collapseInactive 报 not a function中断组件更新致乐观状态不渲染
const tcl = toolCardListRef.value as unknown
const refs = Array.isArray(tcl) ? tcl : (tcl ? [tcl] : [])
refs.forEach((r: any) => r?.collapseInactive?.(active))
collapseAllToolLists(buildActiveToolIds(msgs))
},
{ deep: true },
)
// UX-260616-03: AiAgentRound 新轮次到达时收起上一轮工具卡(round 递增)。
// 首次加载不触发(等真到达更"新"的轮次才收起,避免历史会话加载即折叠)。
watch(
() => store.state.agentRound,
(cur, prev) => {
if (cur <= (prev ?? 0)) return
collapseAllToolLists(buildActiveToolIds(store.state.messages))
},
)
onMounted(async () => {
// 全局快捷键(UX-2025-07):window 级 listener,卸载时移除(对齐 _unlistenToolSlow 生命周期)
window.addEventListener('keydown', onGlobalKeydown)
@@ -1905,6 +1931,19 @@ function canOpenSettings(msg: AiMessage): boolean {
return et === 'auth' || et === 'provider_config'
}
/**
* UX-260616-01: 错误是否可重试(显隐「重试」按钮)。
* Fatal 类错误(鉴权/Provider 配置错——4xx/401/403/invalid_api_key)重试必再败,
* 隐藏重试按钮,引导用户走「去设置」。network/timeout/unknown 为瞬时错误,可重试。
* retryable 推断依据后端 error_type(本批前端推断;后端 AiStreamRetry/AiError 加 retryable 字段下批 df-ai 增强)。
*/
function canRetry(msg: AiMessage): boolean {
const et = (msg as AiMessageWithErrorType).errorType
// auth/provider_config = Fatal(鉴权/配置错),重试无意义
if (et === 'auth' || et === 'provider_config') return false
return true
}
/**
* 错误气泡「重试」:取错误前的末尾 user 消息重发。
* 复用 regenerate 链路(后端弹末尾 AI 回复——错误消息本身——取触发它的 user 消息重跑)。
@@ -2654,6 +2693,14 @@ body.ai-sidebar-resizing * {
.ai-msg-actions--error {
opacity: 1;
}
/* UX-260616-01: Fatal 错误(不可重试)的提示文案——替代隐藏的「重试」按钮,
说明为何无重试入口,引导走「去设置」。 */
.ai-msg-not-retryable {
font-size: 10.5px;
color: var(--df-text-dim);
font-style: italic;
align-self: center;
}
.ai-msg-act {
display: inline-flex;
align-items: center;

View File

@@ -348,7 +348,13 @@ function groupDisplayName(group: ToolCallGroup): string {
transform: rotate(90deg);
}
/* -- 分组隐藏态的卡片 -- */
/* -- 分组隐藏态的卡片
UX-260616-03: 保持 display:none(原行为)。
不改 max-height/opacity 动画的原因:flex 容器 gap:8px 对零高 item 仍贡献 gap,
会留双倍间距致布局回归;display:none 移出流才无 gap。
真正的高度动画需 <Transition> JS 钩子(高度 auto→0 不可纯 CSS transition),
本轮野人 lite 不重构 ToolCard 单卡级折叠(v-show 同因)。
触发时机扩展(新消息/新轮次)已落地,见 AiChat.vue watch。 -- */
.ai-tool-card--group-hidden {
display: none;
}

View File

@@ -24,6 +24,8 @@ export default {
errorNetwork: 'Network connection failed. Please check your network.',
// CR-30-2 / F-260616-07(d) / decision a1: stream pre-failure retry bubble copy
aiStreamRetry: '⚠ Request failed, retrying ({attempt}/{max})…',
// UX-260616-01: Fatal errors (4xx/auth) are not retryable — prompt user to check settings instead
notRetryable: 'This error cannot be resolved by retrying. Please check the configuration.',
streamInterruptedAfterTool: '⚠ The tool finished, but the following reply was interrupted (no data stream for a long time). You can click Continue to retry.',
streamInterrupted: '⚠ The response was interrupted (no data stream for a long time). This may be due to a network disconnection or a backend error. Please try again.',
// UX-2025-04 / CR-30-2 / decision a1: system notice bubble after mid-stream failure preserve (frontend mirrors backend session.messages)

View File

@@ -109,7 +109,7 @@ export default {
editNotLastUser: 'Only the last user message can be edited',
placeholderEditing: 'Edit message then Enter to regenerate (Esc to cancel)',
// ── Error bubble actions (UX-03) ──
// ── Error bubble actions (UX-03 / UX-260616-01) ──
retry: 'Retry',
goToSettingsAction: 'Open settings',

View File

@@ -24,6 +24,8 @@ export default {
errorNetwork: '网络连接失败,请检查网络',
// CR-30-2 / F-260616-07(d) / 决策 a1: 流前失败重试中错误气泡内更新文案
aiStreamRetry: '⚠ 调用失败,正在重试({attempt}/{max})…',
// UX-260616-01: Fatal 类错误(4xx/鉴权)不可重试,提示用户去设置而非重试
notRetryable: '此错误无法通过重试解决,请检查配置',
streamInterruptedAfterTool: '⚠ 工具已执行完成,后续回复中断(长时间无数据流)。可点继续重试。',
streamInterrupted: '⚠ 响应中断(长时间无数据流)。可能是网络断连或后端异常,请重试。',
// UX-2025-04 / CR-30-2 / 决策 a1: 流中途失败保文后的系统提示气泡(前端镜像后端 session.messages)

View File

@@ -109,7 +109,7 @@ export default {
editNotLastUser: '只能编辑最后一条用户消息',
placeholderEditing: '编辑消息后回车重新生成Esc 取消)',
// ── 错误气泡操作入口(UX-03) ──
// ── 错误气泡操作入口(UX-03 / UX-260616-01) ──
retry: '重试',
goToSettingsAction: '去设置',