重构: aichat agent 能力系统化(L1元能力+L2/L3后端+list去重)

L1 agent 元能力层(治痛①死循环零交付):
- env_profile 环境姿势注入 + shell 默认 PowerShell(防引号地狱)
- 断路器:同类工具失败≥3熔断 + guard.reset
- detect_environment 主动探测工具(python/node/shell)
- 求助协议 AiHelpRequired 事件 + 前端求助卡

L2 统一状态机后端(治痛②,前端批2):
- ConvState enum 5态 + 合法转换守卫(conv_state.rs)
- GeneratingGuard 接入视图层(guard.rs)

L3 事件总线后端骨架(治痛③④⑤,接入批2):
- EventBus pub-sub + AiBusEvent 8变体(event_bus.rs)

list 工具调用重复治理第一步:
- build_system_prompt_with_excluded 去重被@实体 + 清单注明语
This commit is contained in:
2026-06-22 00:04:14 +08:00
parent bd6a41fe6e
commit d2cada97cd
16 changed files with 1752 additions and 31 deletions

View File

@@ -45,7 +45,8 @@ const appSettings = useAppSettingsStore()
// 模块级 Set 复用,避免 handleEvent 每事件(delta/token 高频)新建数组字面量做 includes。
// F-260616-03: AiMaxRoundsReached 加入——达 max 暂停态等用户决定继续/停止,不计整流超时。
// F-260619-03 Phase B: AiDirAuthRequired 加入——路径授权挂起等用户决定,不计整流超时。
const NO_RESET_WATCHDOG = new Set<AiChatEvent['type']>(['AiApprovalRequired', 'AiCompleted', 'AiError', 'AiMaxRoundsReached', 'AiDirAuthRequired'])
// L1 求助协议(§2.3):AiHelpRequired 加入——断路器熔断已 guard.reset 终止 loop,等用户选 option。
const NO_RESET_WATCHDOG = new Set<AiChatEvent['type']>(['AiApprovalRequired', 'AiCompleted', 'AiError', 'AiMaxRoundsReached', 'AiDirAuthRequired', 'AiHelpRequired'])
// B-260616-12: 工具执行超时提示(纯前端降级,后端无工具级取消 IPC)。
// 每个 running 工具一个独立 setTimeout;到时若仍未收到 Completed/Approval,
@@ -121,6 +122,22 @@ export interface PendingDirAuth {
}
export const pendingDirAuths = ref<PendingDirAuth[]>([])
// L1 求助协议(aichat 体验与 agent 能力系统化重构 §2.3,2026-06-21):
//
// agent 断路器熔断时后端 emit AiHelpRequired(已 guard.reset,generating=false,loop 终止)。
// 前端据此翻 pendingHelp 驱动求助卡(HelpRequiredCard),显 reason + options 按钮供用户选。
// 模块级 ref(不进 store.state,与 pendingMaxRounds/pendingDirAuths 同性质是"待用户操作"信号,
// 独立事件源——求助卡不与达 max 操作卡/审批卡混)。TD-260621-02 per-conv:存挂起 convId,
// 消费方(HelpRequiredCard)守卫按 activeConversationId 精确比对(F-09 多会话并发不串台)。
// 一次只追踪一个挂起求助(后端断路器熔断即 return 终止 loop,用户选 option 前不再发)。
export interface PendingHelp {
reason: string
context: string
options: string[]
conversationId: string | null
}
export const pendingHelp = ref<PendingHelp | null>(null)
/** 通知会话列表发生变化(供 newConversation/deleteConversation/rename/archive 等触发刷新侧栏) */
export function notifyConversationChanged() {
emit('ai-conversation-changed', {})
@@ -468,6 +485,10 @@ function handleLifecycleEvent(event: AiChatEvent): boolean {
if (pendingMaxRounds.value && pendingMaxRounds.value === (event.conversation_id || '')) {
pendingMaxRounds.value = null
}
// L1 求助协议(§2.3):求助卡挂起仅清本 conv(新一轮发送时旧求助卡应消失)。
if (pendingHelp.value && pendingHelp.value.conversationId === (event.conversation_id || '')) {
pendingHelp.value = null
}
// TD-260621-03 per-conv:仅清本 conv 的 path_auth 挂起(其他会话的弹窗不连累清空)。
// 批2-A 刚把 pendingMaxRounds per-conv,pendingDirAuths 漏跟;此处补齐——
// F-09 多会话并发下全清会让 B 会话的 DirAuthDialog 凭空消失(用户报"弹窗没了我没操作")。
@@ -533,6 +554,10 @@ function handleLifecycleEvent(event: AiChatEvent): boolean {
if (pendingMaxRounds.value && pendingMaxRounds.value === (event.conversation_id || '')) {
pendingMaxRounds.value = null
}
// L1 求助协议(§2.3):求助卡挂起仅清本 conv(错误中断后旧求助卡应消失)。
if (pendingHelp.value && pendingHelp.value.conversationId === (event.conversation_id || '')) {
pendingHelp.value = null
}
// TD-260621-03 per-conv:仅清本 conv 的 path_auth 挂起(异常中断只清本会话弹窗,不连累并发会话)。
const errConv2 = event.conversation_id || ''
pendingDirAuths.value = pendingDirAuths.value.filter(p => !p.conversationId || p.conversationId === errConv2)
@@ -558,6 +583,47 @@ function handleLifecycleEvent(event: AiChatEvent): boolean {
return true
}
case 'AiHelpRequired': {
// L1 求助协议(§2.3,2026-06-21):后端断路器熔断已 guard.reset(generating=false,loop 终止)。
// 前端按终止态收尾(对齐 AiError 分支:清看门狗/流式态/快照/队尾文本 flushCurrentText),
// 但不创建错误气泡(求助非错误,是 AI 主动求助)——改为翻 pendingHelp 驱动求助卡(HelpRequiredCard)
// 显 reason + options 按钮供用户选。
clearStreamWatchdog(event.conversation_id || undefined)
clearAllToolSlowTimers()
flushCurrentText()
setStreaming(false, { convId: event.conversation_id || null, reason: 'AiHelpRequired' })
state.generatingConvs.delete(event.conversation_id || '')
state.currentText = ''
state.agentRound = 0
// 求助即终止 loop,清残留待审批项(对齐 AiError 分支语义,防残留审批卡误导)。
state.pendingApprovals = []
// TD-260621-02 per-conv:仅当 pendingMaxRounds===本 conv 才清 null(求助终止同理清达 max 挂起)。
if (pendingMaxRounds.value && pendingMaxRounds.value === (event.conversation_id || '')) {
pendingMaxRounds.value = null
}
// per-conv 清本 conv path_auth 挂起(求助终止只清本会话弹窗,不连累并发会话)。
const helpConv2 = event.conversation_id || ''
pendingDirAuths.value = pendingDirAuths.value.filter(p => !p.conversationId || p.conversationId === helpConv2)
// F-09: 清理分离窗口生成态快照(per-conv key,清本会话快照;兼容旧单 key)
const helpConv = event.conversation_id || ''
if (helpConv) {
localStorage.removeItem(`df-ai-gen-${helpConv}`)
localStorage.removeItem(`df-ai-text-${helpConv}`)
}
localStorage.removeItem('df-ai-gen')
localStorage.removeItem('df-ai-text')
// 翻 pendingHelp 驱动求助卡:用 convId 兜底(后端必带,无时默认当前活跃会话,优于丢卡片)。
pendingHelp.value = {
reason: event.reason,
context: event.context,
options: event.options,
conversationId: event.conversation_id || state.activeConversationId || null,
}
void loadConversations()
notifyConversationChanged()
return true
}
default:
return false
}
@@ -574,18 +640,19 @@ export function handleEvent(event: AiChatEvent) {
state.activeConversationId = convId
void appSettings.set('df-ai-active-conv', convId)
}
// 事件不属于当前展示对话(生成中切走了)→ 不污染当前视图,仅完成/错误时刷新侧边栏
// 事件不属于当前展示对话(生成中切走了)→ 不污染当前视图,仅完成/错误/求助时刷新侧边栏
// L1 求助协议(§2.3):AiHelpRequired 同 AiError 后端已 guard.reset 终止 loop,需移出生成集。
const isCurrent = !convId || convId === state.activeConversationId
if (!isCurrent) {
if (event.type === 'AiCompleted' || event.type === 'AiError') {
if (event.type === 'AiCompleted' || event.type === 'AiError' || event.type === 'AiHelpRequired') {
// F-09: 多会话并发 — 仅从 Set 移除该 conv(其他会话可能仍在生成),不再置 null 全局清空
state.generatingConvs.delete(convId || '')
void loadConversations()
}
return
}
// 标记正在生成的对话(完成/错误事件除外)
if (convId && event.type !== 'AiCompleted' && event.type !== 'AiError') {
// 标记正在生成的对话(完成/错误/求助事件除外——三者后端均已终止 loop)
if (convId && event.type !== 'AiCompleted' && event.type !== 'AiError' && event.type !== 'AiHelpRequired') {
state.generatingConvs.add(convId)
}
// 流式看门狗:活跃事件(delta/工具/新轮/审批结果)重置;审批等待/完成/错误在 case 内 clear