新增: 压缩失败降级前端透出与vite看门修复
- 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
This commit is contained in:
@@ -270,7 +270,7 @@ pub(super) async fn maybe_auto_compress(
|
|||||||
);
|
);
|
||||||
// 复位 is_compressing 收口进 keyword_fallback_commit 单锁(909①-补:
|
// 复位 is_compressing 收口进 keyword_fallback_commit 单锁(909①-补:
|
||||||
// 原先此处单独取锁复位,与提交之间留并发二次压缩窗口)。
|
// 原先此处单独取锁复位,与提交之间留并发二次压缩窗口)。
|
||||||
keyword_fallback_commit(
|
let degraded = keyword_fallback_commit(
|
||||||
&session_arc,
|
&session_arc,
|
||||||
&conv_id,
|
&conv_id,
|
||||||
protect_start,
|
protect_start,
|
||||||
@@ -281,6 +281,16 @@ pub(super) async fn maybe_auto_compress(
|
|||||||
recall_on.then_some(data_dir.as_path()),
|
recall_on.then_some(data_dir.as_path()),
|
||||||
)
|
)
|
||||||
.await;
|
.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;
|
return;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -384,7 +394,7 @@ pub(super) async fn maybe_auto_compress(
|
|||||||
"[ai] 自动压缩失败(后台),降级走关键词摘要兜底(KEYWORD_FALLBACK_ENABLED={})",
|
"[ai] 自动压缩失败(后台),降级走关键词摘要兜底(KEYWORD_FALLBACK_ENABLED={})",
|
||||||
KEYWORD_FALLBACK_ENABLED,
|
KEYWORD_FALLBACK_ENABLED,
|
||||||
);
|
);
|
||||||
keyword_fallback_commit(
|
let degraded = keyword_fallback_commit(
|
||||||
&session_arc,
|
&session_arc,
|
||||||
&conv_id,
|
&conv_id,
|
||||||
protect_start,
|
protect_start,
|
||||||
@@ -395,6 +405,17 @@ pub(super) async fn maybe_auto_compress(
|
|||||||
recall_on.then_some(data_dir.as_path()),
|
recall_on.then_some(data_dir.as_path()),
|
||||||
)
|
)
|
||||||
.await;
|
.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
|
}) // 闭 catch_unwind 内 async move
|
||||||
@@ -444,11 +465,11 @@ async fn keyword_fallback_commit(
|
|||||||
log_head: &str,
|
log_head: &str,
|
||||||
head_id: &Option<String>,
|
head_id: &Option<String>,
|
||||||
recall: Option<&Path>,
|
recall: Option<&Path>,
|
||||||
) {
|
) -> bool {
|
||||||
let inserted = {
|
let inserted = {
|
||||||
let mut session = session_arc.lock().await;
|
let mut session = session_arc.lock().await;
|
||||||
if !session.per_conv.contains_key(conv_id) {
|
if !session.per_conv.contains_key(conv_id) {
|
||||||
return;
|
return false;
|
||||||
}
|
}
|
||||||
let conv = session.conv(conv_id);
|
let conv = session.conv(conv_id);
|
||||||
// 939⑪ 内容版本守卫:上下文已被整体替换 → 丢弃兜底提交(标志亦不复位)。
|
// 939⑪ 内容版本守卫:上下文已被整体替换 → 丢弃兜底提交(标志亦不复位)。
|
||||||
@@ -458,7 +479,7 @@ async fn keyword_fallback_commit(
|
|||||||
"[ai] {}兜底提交前上下文版本已变(疑似 clear 重建),丢弃",
|
"[ai] {}兜底提交前上下文版本已变(疑似 clear 重建),丢弃",
|
||||||
log_head
|
log_head
|
||||||
);
|
);
|
||||||
return;
|
return false;
|
||||||
}
|
}
|
||||||
// 复位与提交同锁原子(909①-补):消「锁外复位→再取锁提交」之间的并发二次压缩窗口。
|
// 复位与提交同锁原子(909①-补):消「锁外复位→再取锁提交」之间的并发二次压缩窗口。
|
||||||
conv.messages.set_compressing(false);
|
conv.messages.set_compressing(false);
|
||||||
@@ -495,6 +516,7 @@ async fn keyword_fallback_commit(
|
|||||||
log_head
|
log_head
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
inserted
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
|
|||||||
@@ -317,6 +317,11 @@ pub enum AiChatEvent {
|
|||||||
/// 摘要 system 消息已由后端 insert_at,后续 stream 自然带出,无需整会话刷新。
|
/// 摘要 system 消息已由后端 insert_at,后续 stream 自然带出,无需整会话刷新。
|
||||||
/// miniapp 仍插摘要气泡(对端发生压缩告知用户)。
|
/// miniapp 仍插摘要气泡(对端发生压缩告知用户)。
|
||||||
AiAutoCompressed { conversation_id: Option<String>, summary: String },
|
AiAutoCompressed { conversation_id: Option<String>, summary: String },
|
||||||
|
/// E2 压缩失败降级透出(2026-10-18):LLM 压缩失败走关键词摘要兜底时,loop 仍在继续
|
||||||
|
/// (不发 AiError——其 case 无条件 setStreaming(false) 会误判生成结束),但用户须知晓
|
||||||
|
/// 上下文已降级(实证 76730ce8 seq166:静默降级→AI 失忆→用户被迫原句重发)。
|
||||||
|
/// 前端弹一次性状态提示,不中断流。
|
||||||
|
AiContextDegraded { conversation_id: Option<String>, reason: String },
|
||||||
/// L1 求助协议(aichat 体验与 agent 能力系统化重构 §2.3,2026-06-21):
|
/// L1 求助协议(aichat 体验与 agent 能力系统化重构 §2.3,2026-06-21):
|
||||||
///
|
///
|
||||||
/// agent 断路器(§2.2)熔断 / 自省(识别"我反复失败")时发,区别于 prompt 教 AI
|
/// agent 断路器(§2.2)熔断 / 自省(识别"我反复失败")时发,区别于 prompt 教 AI
|
||||||
|
|||||||
@@ -539,6 +539,9 @@ export type AiChatEvent = ({
|
|||||||
} | {
|
} | {
|
||||||
// 上下文已清空:前端刷新消息列表 + toast 提示
|
// 上下文已清空:前端刷新消息列表 + toast 提示
|
||||||
type: 'AiContextCleared'
|
type: 'AiContextCleared'
|
||||||
|
} | {
|
||||||
|
// E2 压缩失败降级(关键词摘要兜底,loop 仍在跑):前端 toast 提示不中断流
|
||||||
|
type: 'AiContextDegraded'; reason: string
|
||||||
}) & {
|
}) & {
|
||||||
conversation_id?: ConvId
|
conversation_id?: ConvId
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -698,6 +698,10 @@ async function initContextEventListeners(): Promise<void> {
|
|||||||
onError: (_convId, message) => {
|
onError: (_convId, message) => {
|
||||||
showToast(t('aiChat.compressError', { msg: message }), 'error')
|
showToast(t('aiChat.compressError', { msg: message }), 'error')
|
||||||
},
|
},
|
||||||
|
// E2 压缩失败降级:仅提示不刷新不打断流(loop 仍在跑,实证静默降级致用户原句重发)
|
||||||
|
onContextDegraded: (_convId, reason) => {
|
||||||
|
showToast(t('aiChat.contextDegraded', { reason }), 'warning')
|
||||||
|
},
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -46,6 +46,7 @@ async function initContextListener(callbacks: {
|
|||||||
onCleared?: (conversationId: string) => void
|
onCleared?: (conversationId: string) => void
|
||||||
onCompressed?: (conversationId: string, summary: string) => void
|
onCompressed?: (conversationId: string, summary: string) => void
|
||||||
onError?: (conversationId: string, message: string) => void
|
onError?: (conversationId: string, message: string) => void
|
||||||
|
onContextDegraded?: (conversationId: string, reason: string) => void
|
||||||
}): Promise<void> {
|
}): Promise<void> {
|
||||||
setContextCallbacks({
|
setContextCallbacks({
|
||||||
onCompressing: () => { isCompressing.value = true },
|
onCompressing: () => { isCompressing.value = true },
|
||||||
@@ -62,6 +63,11 @@ async function initContextListener(callbacks: {
|
|||||||
autoCompressedFlashTimer = setTimeout(() => { autoCompressedFlash.value = false }, 4000)
|
autoCompressedFlashTimer = setTimeout(() => { autoCompressedFlash.value = false }, 4000)
|
||||||
},
|
},
|
||||||
onCleared: (convId) => callbacks.onCleared?.(convId),
|
onCleared: (convId) => callbacks.onCleared?.(convId),
|
||||||
|
onContextDegraded: (convId, reason) => {
|
||||||
|
// E2:压缩失败降级为关键词摘要兜底——复位 loading + 上抛 toast(不中断 LLM 流)
|
||||||
|
isCompressing.value = false
|
||||||
|
callbacks.onContextDegraded?.(convId, reason)
|
||||||
|
},
|
||||||
onError: (convId, message) => {
|
onError: (convId, message) => {
|
||||||
// AiError 也用于流式生成失败(useAiEvents 已处理);此处仅在压缩中时复位 + toast,
|
// AiError 也用于流式生成失败(useAiEvents 已处理);此处仅在压缩中时复位 + toast,
|
||||||
// 避免双重处理流式错误。
|
// 避免双重处理流式错误。
|
||||||
|
|||||||
@@ -30,6 +30,8 @@ export interface ContextCallbacks {
|
|||||||
onCleared?: (conversationId: string) => void
|
onCleared?: (conversationId: string) => void
|
||||||
/** 错误(AiError,含压缩失败):仅在 loading 时置 false + toast */
|
/** 错误(AiError,含压缩失败):仅在 loading 时置 false + toast */
|
||||||
onError?: (conversationId: string, message: string) => void
|
onError?: (conversationId: string, message: string) => void
|
||||||
|
/** E2 压缩失败降级(AiContextDegraded):loop 仍在跑,仅提示上下文已降级不中断流 */
|
||||||
|
onContextDegraded?: (conversationId: string, reason: string) => void
|
||||||
}
|
}
|
||||||
let _contextCallbacks: ContextCallbacks = {}
|
let _contextCallbacks: ContextCallbacks = {}
|
||||||
|
|
||||||
@@ -344,6 +346,11 @@ export async function handleLifecycleEvent(event: AiChatEvent): Promise<boolean>
|
|||||||
_contextCallbacks.onCleared?.(event.conversation_id || '')
|
_contextCallbacks.onCleared?.(event.conversation_id || '')
|
||||||
return true
|
return true
|
||||||
|
|
||||||
|
case 'AiContextDegraded':
|
||||||
|
// E2:压缩失败已走关键词摘要兜底,loop 继续——透出降级提示而非静默失忆
|
||||||
|
_contextCallbacks.onContextDegraded?.(event.conversation_id || '', event.reason || '')
|
||||||
|
return true
|
||||||
|
|
||||||
default:
|
default:
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -275,6 +275,7 @@ export default {
|
|||||||
// B4: one-shot auto-compress notice (loop auto-compress no longer fully silent)
|
// B4: one-shot auto-compress notice (loop auto-compress no longer fully silent)
|
||||||
autoCompressed: 'Context auto-compressed (earlier messages folded into summary)',
|
autoCompressed: 'Context auto-compressed (earlier messages folded into summary)',
|
||||||
compressError: 'Compression failed: {msg}',
|
compressError: 'Compression failed: {msg}',
|
||||||
|
contextDegraded: 'Context degraded: {reason} — the AI may be missing some earlier details',
|
||||||
clearError: 'Clear failed: {msg}',
|
clearError: 'Clear failed: {msg}',
|
||||||
initError: 'AI panel init failed: {msg} (reopen panel to retry)',
|
initError: 'AI panel init failed: {msg} (reopen panel to retry)',
|
||||||
initDegraded: 'List load failed, continuing in degraded mode: {msg}',
|
initDegraded: 'List load failed, continuing in degraded mode: {msg}',
|
||||||
|
|||||||
@@ -276,6 +276,8 @@ export default {
|
|||||||
// B4: 自动压缩完成一次性提示(loop 自动压缩不再全程静默)
|
// B4: 自动压缩完成一次性提示(loop 自动压缩不再全程静默)
|
||||||
autoCompressed: '上下文已自动压缩(较早消息已折叠为摘要)',
|
autoCompressed: '上下文已自动压缩(较早消息已折叠为摘要)',
|
||||||
compressError: '压缩失败:{msg}',
|
compressError: '压缩失败:{msg}',
|
||||||
|
// E2:压缩失败走关键词摘要兜底,loop 继续——提示降级而非静默失忆
|
||||||
|
contextDegraded: '上下文已降级:{reason},AI 可能缺失部分早前细节',
|
||||||
clearError: '清空失败:{msg}',
|
clearError: '清空失败:{msg}',
|
||||||
initError: 'AI 面板初始化失败:{msg}(重开面板可重试)',
|
initError: 'AI 面板初始化失败:{msg}(重开面板可重试)',
|
||||||
initDegraded: '列表加载失败,面板降级继续:{msg}',
|
initDegraded: '列表加载失败,面板降级继续:{msg}',
|
||||||
|
|||||||
+3
-1
@@ -23,7 +23,9 @@ export default defineConfig(async () => ({
|
|||||||
}
|
}
|
||||||
: undefined,
|
: undefined,
|
||||||
watch: {
|
watch: {
|
||||||
ignored: ["**/src-tauri/**"],
|
// target/ 同理须忽略:cargo 编译写 .o/.rlib 时 watcher 触发 EBUSY(文件被写锁),
|
||||||
|
// chokidar 直接抛异常杀掉 dev 进程(2026-10-18 实证:watch sqlite3.o 崩溃)
|
||||||
|
ignored: ["**/src-tauri/**", "**/target/**"],
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
}));
|
}));
|
||||||
|
|||||||
Reference in New Issue
Block a user