优化: 批A前端(timer所有权+会话族+审批conv-scoped+FilePreview+失败反馈)
A1-B1 timer所有权:新建 useTimerOwnership composable(每实例独立ref+onUnmounted清理)+ useToast _timer 下沉 + Ideas debounce 补清理(治跨实例串扰) A1-B2 FilePreview:reqSeq 双计数器守卫(loadFile/loadDiff 最新seq才写,防乱序覆盖)+ mermaid securityLevel strict + filePath 比对(防跨文件SVG注入) A1-B3 会话族:load_more 滚顶加载接线(switch透传has_more/earliest_seq+prepend去重+scrollTop恢复)+ switch失败保留视图+报错(仅对话不存在才create-new)+ new/delete失败反馈(withConvOp helper收敛)+ delete清ai_messages孤儿 A1-B5前端契约:isToolFailure 三处加 success===false 判定(useToolCard/ToolResultBody/ToolCard,git只读失败不再绿框) A2-B10 审批conv-scoped:pendingApprovals 补conversationId + cleanup 按convId filter + 移除全局清(AiError不再误清其他conv)+ :676 dir auth filter方向修 usage打标前端:is_estimated 字段+事件透传+MessageList『估算』角标(详情面板说明) A2-B9前端:clearChat try/catch 错误气泡
This commit is contained in:
@@ -208,7 +208,14 @@ export function friendlyError(raw: string): string {
|
||||
|
||||
/** 把流式累积的 currentText 回填到最后一条 assistant 消息(AiAgentRound/AiCompleted/AiError 收尾共用) */
|
||||
export function flushCurrentText() {
|
||||
if (!state.currentText) return
|
||||
// 根因修复(空气泡):只 guard 空串会漏 whitespace —— LLM 工具调用前常推 `\n`/空格,
|
||||
// 累积成 whitespace-only currentText 后写进占位 content,前端 !content 真值判断漏过
|
||||
// → 渲染带边框空气泡。trim 兜底空白,空白流式文本不回填(无实质内容)。
|
||||
if (!state.currentText || !state.currentText.trim()) {
|
||||
state.currentText = ''
|
||||
_lastDelta = '' // 同步复位 delta 跟踪(防下一轮首个 delta 误判重复)
|
||||
return
|
||||
}
|
||||
// 从末尾向前找最后一个非 isError assistant 气泡写入。跳过 AiStreamRetry 错误气泡
|
||||
// (isError),写入其前的占位 assistant,保留部分回复(UX-260619-06 MED-1)。
|
||||
for (let i = state.messages.length - 1; i >= 0; i--) {
|
||||
@@ -416,6 +423,8 @@ function handleStreamingEvent(event: AiChatEvent): boolean {
|
||||
kind: 'path',
|
||||
dir: event.dir,
|
||||
path: event.path,
|
||||
// A2-B10 conv-scoped:挂起项归属会话 id,cleanup 按此仅清本会话审批(不连累并发会话)。
|
||||
conversationId: event.conversation_id ?? state.activeConversationId ?? undefined,
|
||||
// path 类审批提示复用 aiChat.dirAuthHint(已存在 i18n,tool+path 文案);
|
||||
// 不新增 key 避免 i18n 缺失 prod runtime 报错(memory: i18n-message-compile-blindspot)。
|
||||
reason: t('aiChat.dirAuthHint', { tool: event.tool, path: event.path }),
|
||||
@@ -525,6 +534,9 @@ function handleToolEvent(event: AiChatEvent): boolean {
|
||||
// tc.reason 与 pendingApprovals[].reason 是两条独立赋值路径:tc 走 findToolCall,
|
||||
// 浮窗走主窗口推送的 pendingApprovals 快照,后者此前漏写 reason。
|
||||
reason: event.reason,
|
||||
// A2-B10 conv-scoped:挂起项归属会话 id,cleanup 按此仅清本会话审批(不连累并发会话)。
|
||||
// 事件必带 conversation_id(缺省兜底当前活跃会话——AiApprovalRequired 仅当 isCurrent 才到达此处)。
|
||||
conversationId: event.conversation_id ?? state.activeConversationId ?? undefined,
|
||||
}
|
||||
state.pendingApprovals.push(info)
|
||||
const tc = findToolCall(event.id)
|
||||
@@ -605,6 +617,16 @@ function handleConvStateEvent(event: AiChatEvent): boolean {
|
||||
* 返回值:true=命中 AiUserMessage;false=其他事件。 */
|
||||
function handleUserMessageEvent(event: AiChatEvent): boolean {
|
||||
if (event.type !== 'AiUserMessage') return false
|
||||
// 多会话并行隔离(2026-08-05 BUG-260805-02):AiUserMessage 必带 conversation_id(后端已 resolve)。
|
||||
// - 当前会话(或 null)→ push user 气泡(微信端当前会话消息,桌面若同会话直接展示)
|
||||
// - 非当前会话 → 不污染当前视图(桌面在并行看别的会话),仅刷新会话列表
|
||||
// (侧栏显示该会话有新消息;用户切过去 switchConversation 从 DB 加载完整历史)。
|
||||
// 旧实现无条件 push 到 state.messages → 非当前会话的 user 消息混入当前视图(历史内容被"污染")。
|
||||
const uc = event.conversation_id ?? null
|
||||
if (uc && state.activeConversationId && uc !== state.activeConversationId) {
|
||||
void loadConversations()
|
||||
return true
|
||||
}
|
||||
// 去重:末条已是 user 且 content 相同则跳过(防桌面本地乐观 push + 事件回灌双气泡)。
|
||||
const last = state.messages[state.messages.length - 1]
|
||||
if (last && last.role === 'user' && last.content === event.message) {
|
||||
@@ -622,11 +644,12 @@ function handleUserMessageEvent(event: AiChatEvent): boolean {
|
||||
/**
|
||||
* 公共会话终止收尾(任务 #6 DRY 抽离):AiCompleted / AiError / AiHelpRequired
|
||||
* 三分支共用同一套清场逻辑——看门狗/计时器/流式态/currentText/agentRound/per-conv
|
||||
* 挂起(localStorage 快照 + pendingMaxRounds + pendingDirAuths + convStates)。
|
||||
* 挂起(localStorage 快照 + pendingMaxRounds + pendingDirAuths + convStates +
|
||||
* A2-B10 conv-scoped 审批收尾:pendingApprovals + 对应审批超时计时器按 convId 收敛)。
|
||||
*
|
||||
* 语义差异(调用方自行处理):
|
||||
* - AiCompleted: 调用后追加 incomplete 气泡 / token 用量 / 队列 drain
|
||||
* - AiError: 调用后追加错误气泡 + clearAllApprovalTimers + 清 queue/pendingApprovals
|
||||
* - AiError: 调用后追加错误气泡 + 清 queue(审批清理已在 cleanup 内 conv-scoped 完成)
|
||||
* - AiHelpRequired: 调用后翻 pendingHelp 驱动求助卡
|
||||
*/
|
||||
function cleanupTerminatedConversation(convId: string, reason: 'AiCompleted' | 'AiError' | 'AiHelpRequired') {
|
||||
@@ -635,6 +658,15 @@ function cleanupTerminatedConversation(convId: string, reason: 'AiCompleted' | '
|
||||
clearTextIdleTimer() // 清文本空闲定时器 + 置 textIdle=true(整轮结束不该有活跃信号残留)
|
||||
flushCurrentText()
|
||||
state.currentText = ''
|
||||
// 清「重试中」过渡气泡(成功后残留根治,与 miniapp clearRetryBubbles 对齐)。
|
||||
// AiStreamRetry 推的 retry 气泡(isError,content 含「正在重试(」)是过渡态,终态不清会
|
||||
// 永久残留列表(flushCurrentText 只写非 error 气泡)。这里按 i18n 文案前缀清除。
|
||||
for (let i = state.messages.length - 1; i >= 0; i--) {
|
||||
const m = state.messages[i]
|
||||
if (m.isError && typeof m.content === 'string' && m.content.includes('正在重试(')) {
|
||||
state.messages.splice(i, 1)
|
||||
}
|
||||
}
|
||||
setStreaming(false, { convId: convId || null, reason })
|
||||
state.agentRound = 0 // AE-2025-07: agentic 结束/中断/求助,复位轮次(隐藏进度条)
|
||||
|
||||
@@ -657,7 +689,18 @@ function cleanupTerminatedConversation(convId: string, reason: 'AiCompleted' | '
|
||||
}
|
||||
// TD-260621-03 per-conv:仅清本 conv 的 path_auth 挂起(终止只清本会话弹窗,不连累并发会话)。
|
||||
// F-09 多会话并发下全清会让 B 会话的 DirAuthDialog 凭空消失(用户报"弹窗没了我没操作")。
|
||||
pendingDirAuths.value = pendingDirAuths.value.filter(p => !p.conversationId || p.conversationId === convId)
|
||||
// A2-B10 修正:终止语义=清本 conv(该会话已结束),保其他 conv;无归属旧项保守保留。
|
||||
pendingDirAuths.value = pendingDirAuths.value.filter(p => !p.conversationId || p.conversationId !== convId)
|
||||
|
||||
// A2-B10 conv-scoped 审批收尾:仅清目标 conv 的待审批项 + 对称清其审批超时计时器。
|
||||
// 对齐上方 pendingDirAuths per-conv filter 范例。conversationId 缺失的旧项(无法归属)
|
||||
// 保守保留——误清其他 conv 正在审批的卡 = 回归;仅清 conversationId===convId 的项。
|
||||
// 与旧全局清(AiError/AiHelpRequired 分支 state.pendingApprovals = [] + clearAllApprovalTimers)
|
||||
// 的差异:此处按 convId 收敛,其他会话的审批卡/计时器不受影响。
|
||||
const removedApprovals = state.pendingApprovals.filter(p => p.conversationId === convId)
|
||||
state.pendingApprovals = state.pendingApprovals.filter(p => !p.conversationId || p.conversationId !== convId)
|
||||
// 对称清本 conv 被移除项的审批超时计时器(id 集合),防到点回调改已终止会话 state。
|
||||
for (const p of removedApprovals) clearApprovalTimer(p.id)
|
||||
|
||||
// F-09: 清理分离窗口生成态快照(per-conv key,清本会话快照;兼容旧单 key)
|
||||
if (convId) {
|
||||
@@ -721,6 +764,7 @@ function handleLifecycleEvent(event: AiChatEvent): boolean {
|
||||
cache_hit: event.prompt_cache_hit_tokens,
|
||||
cache_miss: event.prompt_cache_miss_tokens,
|
||||
reasoning: event.reasoning_tokens,
|
||||
is_estimated: event.is_estimated,
|
||||
}
|
||||
m.prompt_cache_hit_tokens = event.prompt_cache_hit_tokens
|
||||
m.prompt_cache_miss_tokens = event.prompt_cache_miss_tokens
|
||||
@@ -737,16 +781,15 @@ function handleLifecycleEvent(event: AiChatEvent): boolean {
|
||||
|
||||
case 'AiError': {
|
||||
cleanupTerminatedConversation(event.conversation_id || '', 'AiError')
|
||||
clearAllApprovalTimers() // UX-260617-10: 错误中断释放所有审批超时计时器,防回调改 state 触发已卸载/已错流程
|
||||
// A2-B10 conv-scoped 审批收尾:本 conv 的待审批项 + 审批超时计时器已由
|
||||
// cleanupTerminatedConversation 按 convId 收敛(不再全局 clearAllApprovalTimers /
|
||||
// state.pendingApprovals = [],避免连累并发会话正在审批的卡)。
|
||||
// 只清除出错会话的队列项,不误伤其他会话的排队消息
|
||||
if (event.conversation_id) {
|
||||
state.queue = state.queue.filter(q => q.conversationId !== event.conversation_id)
|
||||
} else {
|
||||
state.queue = []
|
||||
}
|
||||
// UX-260617-10: 错误收尾清残留待审批项——错误发生时若有工具停在 pending_approval,
|
||||
// 残留可点击审批按钮会让用户误以为还能批(实际后端已终止),残留审批卡误导操作。
|
||||
state.pendingApprovals = []
|
||||
// UX-03: 错误消息携带 error_type(供错误气泡差异化显隐「去设置」按钮)。
|
||||
// AiMessage 类型未含 errorType 字段(不在本批白名单),用对象字面量 + cast 扩展;
|
||||
// 消费方(AiChat.vue canOpenSettings)经同 cast 读取,类型闭环在两端,不污染 types.ts。
|
||||
@@ -770,8 +813,9 @@ function handleLifecycleEvent(event: AiChatEvent): boolean {
|
||||
// 但不创建错误气泡(求助非错误,是 AI 主动求助)——改为翻 pendingHelp 驱动求助卡(HelpRequiredCard)
|
||||
// 显 reason + options 按钮供用户选。
|
||||
cleanupTerminatedConversation(event.conversation_id || '', 'AiHelpRequired')
|
||||
// 求助即终止 loop,清残留待审批项(对齐 AiError 分支语义,防残留审批卡误导)。
|
||||
state.pendingApprovals = []
|
||||
// A2-B10 conv-scoped 审批收尾:本 conv 的待审批项 + 审批超时计时器已由
|
||||
// cleanupTerminatedConversation 按 convId 收敛(不再全局 state.pendingApprovals = [],
|
||||
// 避免连累并发会话正在审批的卡)。
|
||||
// 翻 pendingHelp 驱动求助卡:用 convId 兜底(后端必带,无时默认当前活跃会话,优于丢卡片)。
|
||||
pendingHelp.value = {
|
||||
reason: event.reason,
|
||||
|
||||
Reference in New Issue
Block a user