From 5cebfcd864071cdc6f18b87fe8bf04f0ccb50b43 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=BB=9D=E5=B0=98?= <237809796@qq.com> Date: Mon, 19 Oct 2026 00:00:55 +0800 Subject: [PATCH] =?UTF-8?q?=E6=96=B0=E5=A2=9E:=20=E5=8E=8B=E7=BC=A9?= =?UTF-8?q?=E5=A4=B1=E8=B4=A5=E9=99=8D=E7=BA=A7=E5=89=8D=E7=AB=AF=E9=80=8F?= =?UTF-8?q?=E5=87=BA=E4=B8=8Evite=E7=9C=8B=E9=97=A8=E4=BF=AE=E5=A4=8D?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - E2:LLM压缩失败走关键词摘要兜底时新发AiContextDegraded事件, 前端toast「上下文已降级」不再静默失忆(实证76730ce8 seq166 用户被迫原句重发);不发AiError防误判生成结束 - keyword_fallback_commit返回inserted供emit判定,双调用点 (LLM失败/provider重建失败)透出 - vite watch忽略target/:cargo编译写.o触发EBUSY杀掉dev进程 验收:cargo check 0 error;keyword_fallback 3 passed;vue-tsc EXIT 0 --- .../commands/ai/agentic/context_lifecycle.rs | 32 ++++++++++++++++--- src-tauri/src/commands/ai/mod.rs | 5 +++ src/api/types.ts | 3 ++ src/components/AiChat.vue | 4 +++ src/composables/ai/useAiContext.ts | 6 ++++ src/composables/ai/useAiLifecycleEvents.ts | 7 ++++ src/i18n/en/aiChat.ts | 1 + src/i18n/zh-CN/aiChat.ts | 2 ++ vite.config.ts | 4 ++- 9 files changed, 58 insertions(+), 6 deletions(-) diff --git a/src-tauri/src/commands/ai/agentic/context_lifecycle.rs b/src-tauri/src/commands/ai/agentic/context_lifecycle.rs index c0649ba..8335cf9 100644 --- a/src-tauri/src/commands/ai/agentic/context_lifecycle.rs +++ b/src-tauri/src/commands/ai/agentic/context_lifecycle.rs @@ -270,7 +270,7 @@ pub(super) async fn maybe_auto_compress( ); // 复位 is_compressing 收口进 keyword_fallback_commit 单锁(909①-补: // 原先此处单独取锁复位,与提交之间留并发二次压缩窗口)。 - keyword_fallback_commit( + let degraded = keyword_fallback_commit( &session_arc, &conv_id, protect_start, @@ -281,6 +281,16 @@ pub(super) async fn maybe_auto_compress( recall_on.then_some(data_dir.as_path()), ) .await; + // E2:降级不再静默(实证 76730ce8 seq166 用户被迫原句重发) + if degraded { + emit_ai_chat_event( + &app_handle, + AiChatEvent::AiContextDegraded { + conversation_id: Some(conv_id.to_string()), + reason: "压缩 provider 重建失败,已降级为关键词摘要".into(), + }, + ); + } return; } }; @@ -384,7 +394,7 @@ pub(super) async fn maybe_auto_compress( "[ai] 自动压缩失败(后台),降级走关键词摘要兜底(KEYWORD_FALLBACK_ENABLED={})", KEYWORD_FALLBACK_ENABLED, ); - keyword_fallback_commit( + let degraded = keyword_fallback_commit( &session_arc, &conv_id, protect_start, @@ -395,6 +405,17 @@ pub(super) async fn maybe_auto_compress( recall_on.then_some(data_dir.as_path()), ) .await; + // E2:降级不再静默——AiError 语义是生成结束,此处 loop 仍在跑, + // 用专用事件让前端透出降级状态条/提示(不打断流) + if degraded { + emit_ai_chat_event( + &app_handle, + AiChatEvent::AiContextDegraded { + conversation_id: Some(conv_id.to_string()), + reason: "LLM 压缩失败,已降级为关键词摘要".into(), + }, + ); + } } } }) // 闭 catch_unwind 内 async move @@ -444,11 +465,11 @@ async fn keyword_fallback_commit( log_head: &str, head_id: &Option, recall: Option<&Path>, -) { +) -> bool { let inserted = { let mut session = session_arc.lock().await; if !session.per_conv.contains_key(conv_id) { - return; + return false; } let conv = session.conv(conv_id); // 939⑪ 内容版本守卫:上下文已被整体替换 → 丢弃兜底提交(标志亦不复位)。 @@ -458,7 +479,7 @@ async fn keyword_fallback_commit( "[ai] {}兜底提交前上下文版本已变(疑似 clear 重建),丢弃", log_head ); - return; + return false; } // 复位与提交同锁原子(909①-补):消「锁外复位→再取锁提交」之间的并发二次压缩窗口。 conv.messages.set_compressing(false); @@ -495,6 +516,7 @@ async fn keyword_fallback_commit( log_head ); } + inserted } #[cfg(test)] diff --git a/src-tauri/src/commands/ai/mod.rs b/src-tauri/src/commands/ai/mod.rs index 37fb1af..17f5a19 100644 --- a/src-tauri/src/commands/ai/mod.rs +++ b/src-tauri/src/commands/ai/mod.rs @@ -317,6 +317,11 @@ pub enum AiChatEvent { /// 摘要 system 消息已由后端 insert_at,后续 stream 自然带出,无需整会话刷新。 /// miniapp 仍插摘要气泡(对端发生压缩告知用户)。 AiAutoCompressed { conversation_id: Option, summary: String }, + /// E2 压缩失败降级透出(2026-10-18):LLM 压缩失败走关键词摘要兜底时,loop 仍在继续 + /// (不发 AiError——其 case 无条件 setStreaming(false) 会误判生成结束),但用户须知晓 + /// 上下文已降级(实证 76730ce8 seq166:静默降级→AI 失忆→用户被迫原句重发)。 + /// 前端弹一次性状态提示,不中断流。 + AiContextDegraded { conversation_id: Option, reason: String }, /// L1 求助协议(aichat 体验与 agent 能力系统化重构 §2.3,2026-06-21): /// /// agent 断路器(§2.2)熔断 / 自省(识别"我反复失败")时发,区别于 prompt 教 AI diff --git a/src/api/types.ts b/src/api/types.ts index 7331941..b511b03 100644 --- a/src/api/types.ts +++ b/src/api/types.ts @@ -539,6 +539,9 @@ export type AiChatEvent = ({ } | { // 上下文已清空:前端刷新消息列表 + toast 提示 type: 'AiContextCleared' +} | { + // E2 压缩失败降级(关键词摘要兜底,loop 仍在跑):前端 toast 提示不中断流 + type: 'AiContextDegraded'; reason: string }) & { conversation_id?: ConvId } diff --git a/src/components/AiChat.vue b/src/components/AiChat.vue index 70064cb..4d03d10 100644 --- a/src/components/AiChat.vue +++ b/src/components/AiChat.vue @@ -698,6 +698,10 @@ async function initContextEventListeners(): Promise { onError: (_convId, message) => { showToast(t('aiChat.compressError', { msg: message }), 'error') }, + // E2 压缩失败降级:仅提示不刷新不打断流(loop 仍在跑,实证静默降级致用户原句重发) + onContextDegraded: (_convId, reason) => { + showToast(t('aiChat.contextDegraded', { reason }), 'warning') + }, }) } diff --git a/src/composables/ai/useAiContext.ts b/src/composables/ai/useAiContext.ts index 124bc7c..6d38436 100644 --- a/src/composables/ai/useAiContext.ts +++ b/src/composables/ai/useAiContext.ts @@ -46,6 +46,7 @@ async function initContextListener(callbacks: { onCleared?: (conversationId: string) => void onCompressed?: (conversationId: string, summary: string) => void onError?: (conversationId: string, message: string) => void + onContextDegraded?: (conversationId: string, reason: string) => void }): Promise { setContextCallbacks({ onCompressing: () => { isCompressing.value = true }, @@ -62,6 +63,11 @@ async function initContextListener(callbacks: { autoCompressedFlashTimer = setTimeout(() => { autoCompressedFlash.value = false }, 4000) }, onCleared: (convId) => callbacks.onCleared?.(convId), + onContextDegraded: (convId, reason) => { + // E2:压缩失败降级为关键词摘要兜底——复位 loading + 上抛 toast(不中断 LLM 流) + isCompressing.value = false + callbacks.onContextDegraded?.(convId, reason) + }, onError: (convId, message) => { // AiError 也用于流式生成失败(useAiEvents 已处理);此处仅在压缩中时复位 + toast, // 避免双重处理流式错误。 diff --git a/src/composables/ai/useAiLifecycleEvents.ts b/src/composables/ai/useAiLifecycleEvents.ts index aa0f42a..f23892d 100644 --- a/src/composables/ai/useAiLifecycleEvents.ts +++ b/src/composables/ai/useAiLifecycleEvents.ts @@ -30,6 +30,8 @@ export interface ContextCallbacks { onCleared?: (conversationId: string) => void /** 错误(AiError,含压缩失败):仅在 loading 时置 false + toast */ onError?: (conversationId: string, message: string) => void + /** E2 压缩失败降级(AiContextDegraded):loop 仍在跑,仅提示上下文已降级不中断流 */ + onContextDegraded?: (conversationId: string, reason: string) => void } let _contextCallbacks: ContextCallbacks = {} @@ -344,6 +346,11 @@ export async function handleLifecycleEvent(event: AiChatEvent): Promise _contextCallbacks.onCleared?.(event.conversation_id || '') return true + case 'AiContextDegraded': + // E2:压缩失败已走关键词摘要兜底,loop 继续——透出降级提示而非静默失忆 + _contextCallbacks.onContextDegraded?.(event.conversation_id || '', event.reason || '') + return true + default: return false } diff --git a/src/i18n/en/aiChat.ts b/src/i18n/en/aiChat.ts index ce350a6..a0f60a5 100644 --- a/src/i18n/en/aiChat.ts +++ b/src/i18n/en/aiChat.ts @@ -275,6 +275,7 @@ export default { // B4: one-shot auto-compress notice (loop auto-compress no longer fully silent) autoCompressed: 'Context auto-compressed (earlier messages folded into summary)', compressError: 'Compression failed: {msg}', + contextDegraded: 'Context degraded: {reason} — the AI may be missing some earlier details', clearError: 'Clear failed: {msg}', initError: 'AI panel init failed: {msg} (reopen panel to retry)', initDegraded: 'List load failed, continuing in degraded mode: {msg}', diff --git a/src/i18n/zh-CN/aiChat.ts b/src/i18n/zh-CN/aiChat.ts index 9d6e9d1..35042c4 100644 --- a/src/i18n/zh-CN/aiChat.ts +++ b/src/i18n/zh-CN/aiChat.ts @@ -276,6 +276,8 @@ export default { // B4: 自动压缩完成一次性提示(loop 自动压缩不再全程静默) autoCompressed: '上下文已自动压缩(较早消息已折叠为摘要)', compressError: '压缩失败:{msg}', + // E2:压缩失败走关键词摘要兜底,loop 继续——提示降级而非静默失忆 + contextDegraded: '上下文已降级:{reason},AI 可能缺失部分早前细节', clearError: '清空失败:{msg}', initError: 'AI 面板初始化失败:{msg}(重开面板可重试)', initDegraded: '列表加载失败,面板降级继续:{msg}', diff --git a/vite.config.ts b/vite.config.ts index 5b9d305..0d3cbc6 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -23,7 +23,9 @@ export default defineConfig(async () => ({ } : undefined, watch: { - ignored: ["**/src-tauri/**"], + // target/ 同理须忽略:cargo 编译写 .o/.rlib 时 watcher 触发 EBUSY(文件被写锁), + // chokidar 直接抛异常杀掉 dev 进程(2026-10-18 实证:watch sqlite3.o 崩溃) + ignored: ["**/src-tauri/**", "**/target/**"], }, }, }));