优化: 前端体验批次(Knowledge持久化+流式缓存失效 + dfnodes节点注册 + delta 50ms合批 + MCP schema必填校验+bind_directory对齐)
This commit is contained in:
@@ -176,7 +176,18 @@ async fn resolve_and_inject(
|
||||
return String::new();
|
||||
}
|
||||
let loc = sanitize::locality_of(provider);
|
||||
let (augs, _errors) = state.resolvers.resolve_all(&refs, loc).await;
|
||||
let (augs, errors) = state.resolvers.resolve_all(&refs, loc).await;
|
||||
// 技能/提及注入失败透出:单条 resolve 失败不阻断整批注入(既有语义保留),
|
||||
// 但失败不能静默——warn 日志让技能注入失败可见(不改前端行为)。
|
||||
if !errors.is_empty() {
|
||||
tracing::warn!(
|
||||
skill = ?skill,
|
||||
fail_count = errors.len(),
|
||||
"[ai] 技能/提及注入失败 {} 条(已跳过,不阻断注入): {:?}",
|
||||
errors.len(),
|
||||
errors
|
||||
);
|
||||
}
|
||||
build_augmentation_segment(&augs, lang)
|
||||
}
|
||||
|
||||
|
||||
@@ -148,6 +148,27 @@ fn accumulate_tool_calls(
|
||||
}
|
||||
}
|
||||
|
||||
/// 双写 emit: Tauri 事件 + ai_event_bus 总线(跨端透传)。消费 ev。
|
||||
fn emit_ai_chat_event(app_handle: &AppHandle, ev: AiChatEvent) {
|
||||
let _ = app_handle.emit("ai-chat-event", ev.clone());
|
||||
let _ = app_handle.state::<crate::state::AppState>().ai_event_bus.publish_event(ev);
|
||||
}
|
||||
|
||||
/// AR-8: flush 合批的 AiTextDelta(双写 emit + event_bus)。空 buffer 不 emit。
|
||||
fn flush_delta(app_handle: &AppHandle, conv_id: &str, pending_delta: &mut String) {
|
||||
if pending_delta.is_empty() {
|
||||
return;
|
||||
}
|
||||
let delta = std::mem::take(pending_delta);
|
||||
emit_ai_chat_event(
|
||||
app_handle,
|
||||
AiChatEvent::AiTextDelta {
|
||||
delta,
|
||||
conversation_id: Some(conv_id.to_string()),
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
/// 流式接收 LLM 响应。
|
||||
///
|
||||
/// 三类异常处理(返回 StreamResult 显式区分出口):
|
||||
@@ -184,6 +205,10 @@ pub(crate) async fn stream_llm(
|
||||
const FIRST_CHUNK_TIMEOUT: Duration = Duration::from_secs(10);
|
||||
/// 心跳间隔:静默期向前端报「LLM 仍在跑」,reset watchdog
|
||||
const HEARTBEAT_INTERVAL: Duration = Duration::from_secs(30);
|
||||
/// AR-8: 后端 delta 合批窗口。50ms 内的多次 AiTextDelta 合并为一条再 emit,
|
||||
/// 降低 IPC 事件风暴(前端 rAF 只节流渲染不节流 IPC);合并后前端 currentText
|
||||
/// 逐条累加,最终一致。
|
||||
const DELTA_FLUSH_INTERVAL: Duration = Duration::from_millis(50);
|
||||
|
||||
// ================================================================
|
||||
// BUG-2026-07-17 根治: 整个 LLM 流式请求运行在独立 OS 线程的
|
||||
@@ -264,6 +289,10 @@ pub(crate) async fn stream_llm(
|
||||
let mut final_usage: Option<df_ai::provider::TokenUsage> = None;
|
||||
// BUG-260617-12: DeepSeek thinking 模式推理内容累积(多轮需回传)
|
||||
let mut reasoning_content_acc: Option<String> = None;
|
||||
// AR-8: per-conv delta 合批累加器。pending_delta 累积未 flush 的文本,
|
||||
// next_flush_at 记录下次 flush 的绝对时刻(首个入 buffer 时置 now+50ms)。
|
||||
let mut pending_delta = String::new();
|
||||
let mut next_flush_at: Option<tokio::time::Instant> = None;
|
||||
|
||||
// B-260615-15:heartbeat interval 提至 loop 外复用,避免每轮重建计时器
|
||||
// (每轮重建会丢已积累的节拍,且 interval 首次 tick 立即返回的特性会被误用)。
|
||||
@@ -290,6 +319,7 @@ pub(crate) async fn stream_llm(
|
||||
// BUG-2026-07-14 根治: wall-clock deadline 检查(不依赖 select! timeout 语义)。
|
||||
if tokio::time::Instant::now() >= idle_deadline {
|
||||
if !first_chunk_done {
|
||||
flush_delta(app_handle, conv_id, &mut pending_delta);
|
||||
return StreamResult::InitFailed {
|
||||
retryable: true,
|
||||
error: format!(
|
||||
@@ -304,6 +334,7 @@ pub(crate) async fn stream_llm(
|
||||
text_len = full_text.len(),
|
||||
"[ai] 流中途 idle timeout(deadline),保文不重试(incomplete)",
|
||||
);
|
||||
flush_delta(app_handle, conv_id, &mut pending_delta);
|
||||
return StreamResult::Partial {
|
||||
text: full_text,
|
||||
tool_calls: tool_calls_acc,
|
||||
@@ -312,10 +343,24 @@ pub(crate) async fn stream_llm(
|
||||
};
|
||||
}
|
||||
|
||||
// 三分支 select!:
|
||||
// AR-8: 到点 flush 合批的 delta(单条 AiTextDelta 双写 emit + event_bus)。
|
||||
if let Some(deadline) = next_flush_at {
|
||||
if tokio::time::Instant::now() >= deadline {
|
||||
next_flush_at = None;
|
||||
flush_delta(app_handle, conv_id, &mut pending_delta);
|
||||
}
|
||||
}
|
||||
|
||||
// 多分支 select!:
|
||||
// 1) chunk_rx.recv():来自专用线程的 LLM chunk (15s 保底 timeout,防 select! 死等)
|
||||
// 2) heartbeat.tick():静默期发 AiHeartbeat reset 前端 watchdog
|
||||
// 3) notify.notified():用户停止即时打断
|
||||
// 4) flush 定时器(sleep_until next_flush_at):尾部 delta 不因流静默而延迟(AR-8)。
|
||||
let flush_deadline = next_flush_at
|
||||
.unwrap_or_else(|| tokio::time::Instant::now() + Duration::from_secs(3600));
|
||||
let flush_fut = tokio::time::sleep_until(flush_deadline);
|
||||
tokio::pin!(flush_fut);
|
||||
|
||||
tokio::select! {
|
||||
chunk_result = tokio::time::timeout(
|
||||
Duration::from_secs(15),
|
||||
@@ -325,6 +370,7 @@ pub(crate) async fn stream_llm(
|
||||
Err(_elapsed) => {
|
||||
// 15s 保底 timeout 触发。正常路径不会到这里:心跳 30s 会先触发 select! 返回。
|
||||
if !first_chunk_done {
|
||||
flush_delta(app_handle, conv_id, &mut pending_delta);
|
||||
return StreamResult::InitFailed {
|
||||
retryable: true,
|
||||
error: format!(
|
||||
@@ -339,6 +385,7 @@ pub(crate) async fn stream_llm(
|
||||
text_len = full_text.len(),
|
||||
"[ai] 流中途 idle timeout,保文不重试(incomplete)",
|
||||
);
|
||||
flush_delta(app_handle, conv_id, &mut pending_delta);
|
||||
return StreamResult::Partial {
|
||||
text: full_text,
|
||||
tool_calls: tool_calls_acc,
|
||||
@@ -355,13 +402,11 @@ pub(crate) async fn stream_llm(
|
||||
idle_deadline = tokio::time::Instant::now() + STREAM_IDLE_TIMEOUT;
|
||||
if !chunk.delta.is_empty() {
|
||||
full_text.push_str(&chunk.delta);
|
||||
// L3 emit 双写
|
||||
let ev = AiChatEvent::AiTextDelta {
|
||||
delta: chunk.delta,
|
||||
conversation_id: Some(conv_id.to_string()),
|
||||
};
|
||||
let _ = app_handle.emit("ai-chat-event", ev.clone());
|
||||
let _ = app_handle.state::<crate::state::AppState>().ai_event_bus.publish_event(ev);
|
||||
// AR-8: 合批入 buffer,首个入 buffer 时置 flush 时刻(到点统一 emit 单条)。
|
||||
pending_delta.push_str(&chunk.delta);
|
||||
if next_flush_at.is_none() {
|
||||
next_flush_at = Some(tokio::time::Instant::now() + DELTA_FLUSH_INTERVAL);
|
||||
}
|
||||
}
|
||||
if let Some(tc_deltas) = &chunk.tool_calls {
|
||||
accumulate_tool_calls(tc_deltas, &mut tool_calls_acc);
|
||||
@@ -395,6 +440,7 @@ pub(crate) async fn stream_llm(
|
||||
"[ai] provider 流式错误事件",
|
||||
);
|
||||
if full_text.is_empty() && tool_calls_acc.is_empty() {
|
||||
flush_delta(app_handle, conv_id, &mut pending_delta);
|
||||
return StreamResult::InitFailed {
|
||||
retryable,
|
||||
error: fmt_diag(
|
||||
@@ -405,6 +451,7 @@ pub(crate) async fn stream_llm(
|
||||
),
|
||||
};
|
||||
}
|
||||
flush_delta(app_handle, conv_id, &mut pending_delta);
|
||||
return StreamResult::Partial {
|
||||
text: full_text,
|
||||
tool_calls: tool_calls_acc,
|
||||
@@ -429,6 +476,7 @@ pub(crate) async fn stream_llm(
|
||||
"[ai] 流式接收中途错误(from 专用线程)",
|
||||
);
|
||||
if full_text.is_empty() && tool_calls_acc.is_empty() {
|
||||
flush_delta(app_handle, conv_id, &mut pending_delta);
|
||||
return StreamResult::InitFailed {
|
||||
retryable: classify_status_or_class(&status_or_class),
|
||||
error: fmt_diag(
|
||||
@@ -439,6 +487,7 @@ pub(crate) async fn stream_llm(
|
||||
),
|
||||
};
|
||||
}
|
||||
flush_delta(app_handle, conv_id, &mut pending_delta);
|
||||
return StreamResult::Partial {
|
||||
text: full_text,
|
||||
tool_calls: tool_calls_acc,
|
||||
@@ -450,11 +499,9 @@ pub(crate) async fn stream_llm(
|
||||
}
|
||||
// 2) 心跳:静默期 30s 发 AiHeartbeat,前端 watchdog reset(B-260615-02)
|
||||
_ = heartbeat.tick() => {
|
||||
let ev = AiChatEvent::AiHeartbeat {
|
||||
emit_ai_chat_event(app_handle, AiChatEvent::AiHeartbeat {
|
||||
conversation_id: Some(conv_id.to_string()),
|
||||
};
|
||||
let _ = app_handle.emit("ai-chat-event", ev.clone());
|
||||
let _ = app_handle.state::<crate::state::AppState>().ai_event_bus.publish_event(ev);
|
||||
});
|
||||
}
|
||||
// 3) 即时停止唤醒(B-260615-14)
|
||||
_ = notify.notified() => {
|
||||
@@ -463,11 +510,19 @@ pub(crate) async fn stream_llm(
|
||||
break;
|
||||
}
|
||||
}
|
||||
// 4) AR-8: delta 合批到点 flush(流静默/工具执行等待期也能按时发出尾部文本)
|
||||
_ = &mut flush_fut => {
|
||||
if next_flush_at.is_some() {
|
||||
next_flush_at = None;
|
||||
flush_delta(app_handle, conv_id, &mut pending_delta);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 用户停止
|
||||
if stopped {
|
||||
flush_delta(app_handle, conv_id, &mut pending_delta);
|
||||
return StreamResult::Complete {
|
||||
text: full_text,
|
||||
tool_calls: tool_calls_acc,
|
||||
@@ -490,6 +545,7 @@ pub(crate) async fn stream_llm(
|
||||
text_len = full_text.len(),
|
||||
"[ai] 流尽未收到 finished 但有 partial_text,保文不重试(incomplete)",
|
||||
);
|
||||
flush_delta(app_handle, conv_id, &mut pending_delta);
|
||||
return StreamResult::Partial {
|
||||
text: full_text,
|
||||
tool_calls: tool_calls_acc,
|
||||
@@ -499,6 +555,7 @@ pub(crate) async fn stream_llm(
|
||||
}
|
||||
|
||||
// 正常完成
|
||||
flush_delta(app_handle, conv_id, &mut pending_delta);
|
||||
StreamResult::Complete {
|
||||
text: full_text,
|
||||
tool_calls: tool_calls_acc,
|
||||
|
||||
Reference in New Issue
Block a user