Files
DevFlow/src/components/ai/HelpRequiredCard.vue
T
lxy 25ec9cf69a 优化: 前端u-agent令牌对齐与组件拆分续批
- 全局样式类(df-sec-h/df-toast族)替换散装实现,主题token去硬编码兜底
- 拆分子组件:ai-chat/queue-panel(队列时钟与编辑随迁)、
  conversation-sidebar/ConversationList、settings/provider-panel
- 父组件死副本清理(队列编辑/时钟/键位表,TS6133清零)并补
  fill-draft父侧转发(重构半成品断链修复)
- 验收:vue-tsc EXIT 0
2026-10-18 23:15:57 +08:00

124 lines
5.5 KiB
Vue

<template>
<!-- L1 求助协议(aichat 体验与 agent 能力系统化重构 §2.3,2026-06-21):断路器熔断后展示的操作卡。
触发:后端断路器连续同类失败达阈值 → emit AiHelpRequired(已 guard.reset,loop 终止)→
useAiEvents handleLifecycleEvent 翻 pendingHelp(模块级 ref)→ 本卡渲染。
复用 MaxRoundsCard 模式:per-conv 守卫(pendingHelp.conversationId === active)+ 行为已记入本地 ref。
显 reason(原因)+ context(最近错误 sample)+ options 按钮供用户选(换思路/授权路径/人工接管)。
选 option → 仅关闭求助卡(后端 loop 已终止,用户重新发消息即"换思路/人工接管"入口,
路径授权走 DirAuthDialog 既有链路)。机制优先 prompt 说教:代码强制熔断 + 结构化求助,
非"教 AI 失败就问用户"(LLM 不可靠)。 -->
<div v-if="helpActive" ref="helpRef" class="ai-help-required">
<span class="ai-help-required-text">{{ helpActive.reason }}</span>
<span class="ai-help-required-hint">{{ helpActive.context }}</span>
<div class="ai-help-required-options">
<button
v-for="opt in helpActive.options"
:key="opt"
type="button"
class="ai-help-required-btn"
@click="handleSelectOption(opt)"
>{{ opt }}</button>
</div>
</div>
</template>
<script setup lang="ts">
import { computed, ref, watch, nextTick } from 'vue'
import { useI18n } from 'vue-i18n'
import { useAiStore } from '../../stores/ai'
import { pendingHelp } from '../../composables/ai/useAiPendingState'
// toast 经 emit 转父(保持单一 toast 源,与 MaxRoundsCard 一致)
const emit = defineEmits<{
(e: 'toast', payload: { msg: string; type: 'error' | 'warning' | 'info' }): void
}>()
const store = useAiStore()
const { t } = useI18n()
// 当前挂起的求助(有值即后端发过 AiHelpRequired)。pendingHelp 由 useAiEvents 翻:
// AiHelpRequired 设值;AiCompleted/AiError 清值(重新发送或错误中断后求助卡消失)。
//
// helpActive 既做守卫(挂起会话===当前展示会话)又做模板取值源:返回 PendingHelp 非空或 null,
// 模板 v-if="helpActive" 取真值后 Vue 类型收窄为非 null,可直接访问 .reason/.context/.options。
// 显示守卫:仅当挂起求助属当前展示会话时显(F-09 多会话并发不串台)。
// 注:不依赖 streaming——求助事件后端已 guard.reset(generating=false),若依赖 isGenerating
// 会导致卡片不显(对齐 MaxRoundsCard F-260620 去 streaming 依赖的根治理)。
const helpActive = computed(() => {
const p = pendingHelp.value
if (p && p.conversationId === store.state.activeConversationId) return p
return null
})
// M8:卡片根 ref。断路器熔断常发生在多轮迭代后,消息流已长,求助卡被挤出视口用户看不到 → 求助无声
// 卡死。卡片显示时 scrollIntoView 让 option 按钮组立即进入视口。
const helpRef = ref<HTMLElement | null>(null)
/** M8:helpActive 从 null→非 null(求助挂起出现)时 scrollIntoView 让操作入口立即可见。
* 仅监 truthy 翻转,不监 reason/context 变化避免已可见时反复跳。 */
watch(helpActive, async (p) => {
if (!p) return
await nextTick()
helpRef.value?.scrollIntoView({ block: 'center', behavior: 'smooth' })
})
/** 选 option:关求助卡即可。后端 loop 已在 AiHelpRequired 时终止(guard.reset + return),
* 用户选 option 后的"换思路/授权路径/人工接管"由用户重新发消息/操作触发(无独立 IPC)。
* 点"授权路径"提示用户路径授权链路(DirAuthDialog 自动触发,无需此卡代发)。
* 选后清 pendingHelp,卡片隐藏(对齐 MaxRoundsCard 点继续/停止后由事件清的语义,此处无续跑事件故立即清)。 */
function handleSelectOption(opt: string): void {
void opt // option 仅区分按钮文案,后端 loop 已终止无独立 IPC 续跑,选即清卡片
pendingHelp.value = null
emit('toast', { msg: t('aiChat.helpRequiredAck'), type: 'info' })
}
</script>
<style scoped>
/* wave2(2026-10-18):统一透明彩底卡范式,danger 系(断路器熔断=最高警示级)——
边框 color-mix 30% + 底 8% + 标题同色 12px/600 + 正文 secondary 12px + 透明底描边钮 */
.ai-help-required {
display: flex;
flex-direction: column;
gap: 3px;
margin-bottom: 6px;
padding: 6px 8px;
background: color-mix(in srgb, var(--df-danger) 8%, transparent);
border: 0.5px solid color-mix(in srgb, var(--df-danger) 30%, transparent);
border-radius: var(--df-radius);
}
.ai-help-required-text {
font-size: 12px;
font-weight: 600;
color: var(--df-danger);
}
.ai-help-required-hint {
font-size: 12px;
color: var(--df-text-secondary);
word-break: break-all;
/* M7:context 含错误堆栈,长栈撑高卡片。等宽字体利堆栈行对齐;max-height + overflow-y:auto
短内容自动不显滚动条,长堆栈滚动扫看(比折叠实用,无需展开按钮)。 */
font-family: var(--df-font-mono);
max-height: 200px;
overflow-y: auto;
}
.ai-help-required-options {
display: flex;
align-items: center;
flex-wrap: wrap;
gap: 6px;
margin-top: 2px;
}
.ai-help-required-btn {
border: 0.5px solid color-mix(in srgb, var(--df-danger) 40%, transparent);
background: color-mix(in srgb, var(--df-danger) 10%, transparent);
color: var(--df-danger);
cursor: pointer;
font-size: 11px;
font-weight: 600;
padding: 3px 10px;
border-radius: calc(var(--df-radius) - 2px);
transition: background 0.15s var(--df-ease);
}
.ai-help-required-btn:hover { background: color-mix(in srgb, var(--df-danger) 16%, transparent); }
</style>