修复: AiCompleted 携带 pinned_goals 根治目标面板刷新
This commit is contained in:
@@ -823,6 +823,15 @@ pub(crate) async fn run_agentic_loop(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 对话透明化 L1:拍快照供 AiCompleted 事件携带,前端直接读取 pinned_goals 无需等 loadConversations
|
||||||
|
let pinned_goals_snapshot: Vec<String> = {
|
||||||
|
let session = session_arc.lock().await;
|
||||||
|
session
|
||||||
|
.conv_read(&conv_id)
|
||||||
|
.map(|c| c.pinned_goals.clone())
|
||||||
|
.unwrap_or_default()
|
||||||
|
};
|
||||||
|
|
||||||
// F-260616-13: system_prompt 是 run_agentic_loop 的不变参数(整个 loop 期间文本不变),
|
// F-260616-13: system_prompt 是 run_agentic_loop 的不变参数(整个 loop 期间文本不变),
|
||||||
// 其 token 估算在 loop 外算一次缓存复用,避免每轮/每次重试重复 estimate_text(低收益优化,行为不变)。
|
// 其 token 估算在 loop 外算一次缓存复用,避免每轮/每次重试重复 estimate_text(低收益优化,行为不变)。
|
||||||
let sys_tokens = TokenEstimator::default().estimate_text(&system_prompt);
|
let sys_tokens = TokenEstimator::default().estimate_text(&system_prompt);
|
||||||
@@ -854,7 +863,7 @@ pub(crate) async fn run_agentic_loop(
|
|||||||
spawn_ensure_title(&provider_config, &db, &conv_id, &app_handle, &session_arc, &llm_concurrency);
|
spawn_ensure_title(&provider_config, &db, &conv_id, &app_handle, &session_arc, &llm_concurrency);
|
||||||
guard.reset().await;
|
guard.reset().await;
|
||||||
// generating 复位后再 emit Completed:保证前端收事件时后端已可接下一条(发送队列续发不被"正在生成中"拒绝)
|
// generating 复位后再 emit Completed:保证前端收事件时后端已可接下一条(发送队列续发不被"正在生成中"拒绝)
|
||||||
let _ = app_handle.emit("ai-chat-event", AiChatEvent::AiCompleted { total_tokens: usage.total_tokens, prompt_tokens: tokens.prompt(), completion_tokens: tokens.completion(), incomplete: None, conversation_id: Some(conv_id.clone()) });
|
let _ = app_handle.emit("ai-chat-event", AiChatEvent::AiCompleted { total_tokens: usage.total_tokens, prompt_tokens: tokens.prompt(), completion_tokens: tokens.completion(), incomplete: None, conversation_id: Some(conv_id.clone()), pinned_goals: pinned_goals_snapshot.clone() });
|
||||||
// L3 emit 双写:入口 stop 的 AiCompleted publish 到事件总线(EVENT_BUS_ENABLED 门控在 publish 内)。
|
// L3 emit 双写:入口 stop 的 AiCompleted publish 到事件总线(EVENT_BUS_ENABLED 门控在 publish 内)。
|
||||||
let _ = app_handle.state::<AppState>().ai_event_bus.publish_event(AiChatEvent::AiCompleted {
|
let _ = app_handle.state::<AppState>().ai_event_bus.publish_event(AiChatEvent::AiCompleted {
|
||||||
total_tokens: usage.total_tokens,
|
total_tokens: usage.total_tokens,
|
||||||
@@ -862,6 +871,7 @@ pub(crate) async fn run_agentic_loop(
|
|||||||
completion_tokens: tokens.completion(),
|
completion_tokens: tokens.completion(),
|
||||||
incomplete: None,
|
incomplete: None,
|
||||||
conversation_id: Some(conv_id.clone()),
|
conversation_id: Some(conv_id.clone()),
|
||||||
|
pinned_goals: pinned_goals_snapshot.clone(),
|
||||||
});
|
});
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -1432,6 +1442,7 @@ pub(crate) async fn run_agentic_loop(
|
|||||||
completion_tokens: tokens.completion(),
|
completion_tokens: tokens.completion(),
|
||||||
conversation_id: Some(conv_id.clone()),
|
conversation_id: Some(conv_id.clone()),
|
||||||
incomplete: Some(true),
|
incomplete: Some(true),
|
||||||
|
pinned_goals: pinned_goals_snapshot.clone(),
|
||||||
});
|
});
|
||||||
// L3 emit 双写:MidStream 保文 AiCompleted publish 到事件总线(门控在 publish 内)。
|
// L3 emit 双写:MidStream 保文 AiCompleted publish 到事件总线(门控在 publish 内)。
|
||||||
let _ = app_handle.state::<AppState>().ai_event_bus.publish_event(AiChatEvent::AiCompleted {
|
let _ = app_handle.state::<AppState>().ai_event_bus.publish_event(AiChatEvent::AiCompleted {
|
||||||
@@ -1440,6 +1451,7 @@ pub(crate) async fn run_agentic_loop(
|
|||||||
completion_tokens: tokens.completion(),
|
completion_tokens: tokens.completion(),
|
||||||
incomplete: None,
|
incomplete: None,
|
||||||
conversation_id: Some(conv_id.clone()),
|
conversation_id: Some(conv_id.clone()),
|
||||||
|
pinned_goals: pinned_goals_snapshot.clone(),
|
||||||
});
|
});
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -1500,7 +1512,7 @@ pub(crate) async fn run_agentic_loop(
|
|||||||
spawn_ensure_title(&provider_config, &db, &conv_id, &app_handle, &session_arc, &llm_concurrency);
|
spawn_ensure_title(&provider_config, &db, &conv_id, &app_handle, &session_arc, &llm_concurrency);
|
||||||
guard.reset().await;
|
guard.reset().await;
|
||||||
// generating 复位后再 emit Completed:保证前端收事件时后端已可接下一条(发送队列续发不被"正在生成中"拒绝)
|
// generating 复位后再 emit Completed:保证前端收事件时后端已可接下一条(发送队列续发不被"正在生成中"拒绝)
|
||||||
let _ = app_handle.emit("ai-chat-event", AiChatEvent::AiCompleted { total_tokens: usage.total_tokens, prompt_tokens: tokens.prompt(), completion_tokens: tokens.completion(), incomplete: None, conversation_id: Some(conv_id.clone()) });
|
let _ = app_handle.emit("ai-chat-event", AiChatEvent::AiCompleted { total_tokens: usage.total_tokens, prompt_tokens: tokens.prompt(), completion_tokens: tokens.completion(), incomplete: None, conversation_id: Some(conv_id.clone()), pinned_goals: pinned_goals_snapshot.clone() });
|
||||||
// L3 emit 双写:stream 后 stop 的 AiCompleted publish 到事件总线(门控在 publish 内)。
|
// L3 emit 双写:stream 后 stop 的 AiCompleted publish 到事件总线(门控在 publish 内)。
|
||||||
let _ = app_handle.state::<AppState>().ai_event_bus.publish_event(AiChatEvent::AiCompleted {
|
let _ = app_handle.state::<AppState>().ai_event_bus.publish_event(AiChatEvent::AiCompleted {
|
||||||
total_tokens: usage.total_tokens,
|
total_tokens: usage.total_tokens,
|
||||||
@@ -1508,6 +1520,7 @@ pub(crate) async fn run_agentic_loop(
|
|||||||
completion_tokens: tokens.completion(),
|
completion_tokens: tokens.completion(),
|
||||||
incomplete: None,
|
incomplete: None,
|
||||||
conversation_id: Some(conv_id.clone()),
|
conversation_id: Some(conv_id.clone()),
|
||||||
|
pinned_goals: pinned_goals_snapshot.clone(),
|
||||||
});
|
});
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -1813,7 +1826,7 @@ pub(crate) async fn run_agentic_loop(
|
|||||||
|
|
||||||
guard.reset().await;
|
guard.reset().await;
|
||||||
// generating 复位后再 emit Completed:落库/标题/提炼已在后台,前端立即感知完成
|
// generating 复位后再 emit Completed:落库/标题/提炼已在后台,前端立即感知完成
|
||||||
let _ = app_handle.emit("ai-chat-event", AiChatEvent::AiCompleted { total_tokens: usage_total, prompt_tokens: tokens.prompt(), completion_tokens: tokens.completion(), incomplete: None, conversation_id: Some(conv_id.clone()) });
|
let _ = app_handle.emit("ai-chat-event", AiChatEvent::AiCompleted { total_tokens: usage_total, prompt_tokens: tokens.prompt(), completion_tokens: tokens.completion(), incomplete: None, conversation_id: Some(conv_id.clone()), pinned_goals: pinned_goals_snapshot.clone() });
|
||||||
// L3 emit 双写:正常完成 AiCompleted publish 到事件总线(EVENT_BUS_ENABLED 门控在 publish 内,
|
// L3 emit 双写:正常完成 AiCompleted publish 到事件总线(EVENT_BUS_ENABLED 门控在 publish 内,
|
||||||
// 无消费者空转留批3 真实消费者接入)。AiTextDelta/AiToolCall* 高频事件不双写(无消费者空转)。
|
// 无消费者空转留批3 真实消费者接入)。AiTextDelta/AiToolCall* 高频事件不双写(无消费者空转)。
|
||||||
let _ = app_handle.state::<AppState>().ai_event_bus.publish_event(AiChatEvent::AiCompleted {
|
let _ = app_handle.state::<AppState>().ai_event_bus.publish_event(AiChatEvent::AiCompleted {
|
||||||
@@ -1822,6 +1835,7 @@ pub(crate) async fn run_agentic_loop(
|
|||||||
completion_tokens: tokens.completion(),
|
completion_tokens: tokens.completion(),
|
||||||
incomplete: None,
|
incomplete: None,
|
||||||
conversation_id: Some(conv_id.clone()),
|
conversation_id: Some(conv_id.clone()),
|
||||||
|
pinned_goals: pinned_goals_snapshot.clone(),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1939,12 +1953,14 @@ pub(crate) async fn try_continue_agent_loop(
|
|||||||
let is_generating = conv.map(|c| c.conv_state.is_active()).unwrap_or(false);
|
let is_generating = conv.map(|c| c.conv_state.is_active()).unwrap_or(false);
|
||||||
let agent_language = conv.and_then(|c| c.agent_language.clone());
|
let agent_language = conv.and_then(|c| c.agent_language.clone());
|
||||||
let model_override = conv.and_then(|c| c.model_override.clone());
|
let model_override = conv.and_then(|c| c.model_override.clone());
|
||||||
|
let pinned_goals_snapshot = conv.map(|c| c.pinned_goals.clone()).unwrap_or_default();
|
||||||
ContinueSnapshot {
|
ContinueSnapshot {
|
||||||
is_generating,
|
is_generating,
|
||||||
has_pending,
|
has_pending,
|
||||||
pending_conv_id,
|
pending_conv_id,
|
||||||
agent_language,
|
agent_language,
|
||||||
model_override,
|
model_override,
|
||||||
|
pinned_goals_snapshot,
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
let should_continue = snap.is_generating && !snap.has_pending;
|
let should_continue = snap.is_generating && !snap.has_pending;
|
||||||
@@ -1971,6 +1987,7 @@ pub(crate) async fn try_continue_agent_loop(
|
|||||||
completion_tokens: 0,
|
completion_tokens: 0,
|
||||||
incomplete: None,
|
incomplete: None,
|
||||||
conversation_id: Some(emit_conv_id),
|
conversation_id: Some(emit_conv_id),
|
||||||
|
pinned_goals: snap.pinned_goals_snapshot.clone(),
|
||||||
};
|
};
|
||||||
let _ = app.emit("ai-chat-event", ev.clone());
|
let _ = app.emit("ai-chat-event", ev.clone());
|
||||||
// L3 emit 双写:tunnel subscriber(阶段2)透传 miniapp
|
// L3 emit 双写:tunnel subscriber(阶段2)透传 miniapp
|
||||||
@@ -2062,6 +2079,7 @@ pub(crate) async fn try_continue_agent_loop(
|
|||||||
completion_tokens: 0,
|
completion_tokens: 0,
|
||||||
incomplete: None,
|
incomplete: None,
|
||||||
conversation_id: Some(conv_id_owned.clone()),
|
conversation_id: Some(conv_id_owned.clone()),
|
||||||
|
pinned_goals: snap.pinned_goals_snapshot.clone(),
|
||||||
};
|
};
|
||||||
let _ = app.emit("ai-chat-event", ev.clone());
|
let _ = app.emit("ai-chat-event", ev.clone());
|
||||||
// L3 emit 双写:tunnel subscriber(阶段2)透传 miniapp
|
// L3 emit 双写:tunnel subscriber(阶段2)透传 miniapp
|
||||||
@@ -2097,6 +2115,8 @@ struct ContinueSnapshot {
|
|||||||
pending_conv_id: Option<String>,
|
pending_conv_id: Option<String>,
|
||||||
agent_language: Option<String>,
|
agent_language: Option<String>,
|
||||||
model_override: Option<String>,
|
model_override: Option<String>,
|
||||||
|
/// 对话透明化 L1:快照 pinned_goals 供 AiCompleted emit 携带
|
||||||
|
pinned_goals_snapshot: Vec<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
|
|||||||
@@ -1459,7 +1459,7 @@ pub async fn ai_chat_force_send(
|
|||||||
// F-260616-09 B 批4(决策 e):force_send 仅复位**目标 conv 自己**的 generating(用户当前面板),
|
// F-260616-09 B 批4(决策 e):force_send 仅复位**目标 conv 自己**的 generating(用户当前面板),
|
||||||
// 不再跨 conv 杀(旧实现清全局 generating + 全 clear pending_approvals,在真并发下会误杀其他
|
// 不再跨 conv 杀(旧实现清全局 generating + 全 clear pending_approvals,在真并发下会误杀其他
|
||||||
// 后台 conv 的 loop/审批)。pending_approvals 仅清目标 conv 的(retain),保留其他 conv 的。
|
// 后台 conv 的 loop/审批)。pending_approvals 仅清目标 conv 的(retain),保留其他 conv 的。
|
||||||
let (old_conv_id, conv_id, _user_message_id) = {
|
let (old_conv_id, conv_id, _user_message_id, old_pinned_goals) = {
|
||||||
let mut session = state.ai_session.lock().await;
|
let mut session = state.ai_session.lock().await;
|
||||||
// 目标 conv:入参优先 → active → 懒创建。
|
// 目标 conv:入参优先 → active → 懒创建。
|
||||||
let target = conversation_id.clone().filter(|s| !s.is_empty())
|
let target = conversation_id.clone().filter(|s| !s.is_empty())
|
||||||
@@ -1540,7 +1540,7 @@ pub async fn ai_chat_force_send(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
(was_gen.then_some(target.clone()), target, user_msg_id)
|
(was_gen.then_some(target.clone()), target, user_msg_id, conv.pinned_goals.clone())
|
||||||
};
|
};
|
||||||
|
|
||||||
// 通知前端目标 conv 旧生成已结束(若先前在生成;emit 在锁外,避免持锁调 runtime emit)
|
// 通知前端目标 conv 旧生成已结束(若先前在生成;emit 在锁外,避免持锁调 runtime emit)
|
||||||
@@ -1551,6 +1551,7 @@ pub async fn ai_chat_force_send(
|
|||||||
completion_tokens: 0,
|
completion_tokens: 0,
|
||||||
incomplete: None,
|
incomplete: None,
|
||||||
conversation_id: Some(cid.clone()),
|
conversation_id: Some(cid.clone()),
|
||||||
|
pinned_goals: old_pinned_goals.clone(),
|
||||||
};
|
};
|
||||||
let _ = app.emit("ai-chat-event", ev.clone());
|
let _ = app.emit("ai-chat-event", ev.clone());
|
||||||
// L3 emit 双写:tunnel subscriber(阶段2 后续)透传 miniapp。
|
// L3 emit 双写:tunnel subscriber(阶段2 后续)透传 miniapp。
|
||||||
@@ -1649,10 +1650,12 @@ pub async fn ai_chat_stop(
|
|||||||
),
|
),
|
||||||
}
|
}
|
||||||
conv.stop_flag.store(true, Ordering::SeqCst); // 双保险:防 try_continue 误判重启
|
conv.stop_flag.store(true, Ordering::SeqCst); // 双保险:防 try_continue 误判重启
|
||||||
|
let pinned_goals = conv.pinned_goals.clone();
|
||||||
drop(session);
|
drop(session);
|
||||||
let ev = AiChatEvent::AiCompleted {
|
let ev = AiChatEvent::AiCompleted {
|
||||||
total_tokens: 0, prompt_tokens: 0, completion_tokens: 0, incomplete: None,
|
total_tokens: 0, prompt_tokens: 0, completion_tokens: 0, incomplete: None,
|
||||||
conversation_id: Some(target),
|
conversation_id: Some(target),
|
||||||
|
pinned_goals,
|
||||||
};
|
};
|
||||||
let _ = app.emit("ai-chat-event", ev.clone());
|
let _ = app.emit("ai-chat-event", ev.clone());
|
||||||
// L3 emit 双写:tunnel subscriber(阶段2 后续)透传 miniapp。
|
// L3 emit 双写:tunnel subscriber(阶段2 后续)透传 miniapp。
|
||||||
@@ -1701,6 +1704,7 @@ pub async fn ai_chat_stop(
|
|||||||
"[ai] ai_chat_stop 3秒超时 ConvState→Idle 非法(不阻断 emit AiCompleted)"
|
"[ai] ai_chat_stop 3秒超时 ConvState→Idle 非法(不阻断 emit AiCompleted)"
|
||||||
),
|
),
|
||||||
}
|
}
|
||||||
|
let pinned_goals = conv.pinned_goals.clone();
|
||||||
drop(session); // 释放锁后再 emit,避免持锁调 runtime emit
|
drop(session); // 释放锁后再 emit,避免持锁调 runtime emit
|
||||||
let ev = AiChatEvent::AiCompleted {
|
let ev = AiChatEvent::AiCompleted {
|
||||||
total_tokens: 0,
|
total_tokens: 0,
|
||||||
@@ -1708,6 +1712,7 @@ pub async fn ai_chat_stop(
|
|||||||
completion_tokens: 0,
|
completion_tokens: 0,
|
||||||
incomplete: None,
|
incomplete: None,
|
||||||
conversation_id: conv_id,
|
conversation_id: conv_id,
|
||||||
|
pinned_goals,
|
||||||
};
|
};
|
||||||
let _ = app_handle.emit("ai-chat-event", ev.clone());
|
let _ = app_handle.emit("ai-chat-event", ev.clone());
|
||||||
// L3 emit 双写:tunnel subscriber(阶段2 后续)透传 miniapp。闭包内 app_handle 仍可访问 AppState。
|
// L3 emit 双写:tunnel subscriber(阶段2 后续)透传 miniapp。闭包内 app_handle 仍可访问 AppState。
|
||||||
@@ -1772,6 +1777,7 @@ pub async fn ai_stop_loop(
|
|||||||
state: State<'_, AppState>,
|
state: State<'_, AppState>,
|
||||||
conversation_id: String,
|
conversation_id: String,
|
||||||
) -> Result<String, String> {
|
) -> Result<String, String> {
|
||||||
|
let pinned_goals: Vec<String>;
|
||||||
{
|
{
|
||||||
let mut session = state.ai_session.lock().await;
|
let mut session = state.ai_session.lock().await;
|
||||||
// F-260616-09 B 批4(决策 e):conv_id 来源 IPC 参数 conversation_id,移除 active 一致性校验
|
// F-260616-09 B 批4(决策 e):conv_id 来源 IPC 参数 conversation_id,移除 active 一致性校验
|
||||||
@@ -1794,6 +1800,7 @@ pub async fn ai_stop_loop(
|
|||||||
"[ai] ai_stop_loop ConvState→Idle 非法(不阻断 emit AiCompleted)"
|
"[ai] ai_stop_loop ConvState→Idle 非法(不阻断 emit AiCompleted)"
|
||||||
),
|
),
|
||||||
}
|
}
|
||||||
|
pinned_goals = conv.pinned_goals.clone();
|
||||||
}
|
}
|
||||||
// 暂停态进入前已 save_conversation,此处零 token 上报仅作收敛信号(与 try_continue 补发 AiCompleted 一致)
|
// 暂停态进入前已 save_conversation,此处零 token 上报仅作收敛信号(与 try_continue 补发 AiCompleted 一致)
|
||||||
let ev = AiChatEvent::AiCompleted {
|
let ev = AiChatEvent::AiCompleted {
|
||||||
@@ -1802,6 +1809,7 @@ pub async fn ai_stop_loop(
|
|||||||
completion_tokens: 0,
|
completion_tokens: 0,
|
||||||
incomplete: None,
|
incomplete: None,
|
||||||
conversation_id: Some(conversation_id),
|
conversation_id: Some(conversation_id),
|
||||||
|
pinned_goals,
|
||||||
};
|
};
|
||||||
let _ = app.emit("ai-chat-event", ev.clone());
|
let _ = app.emit("ai-chat-event", ev.clone());
|
||||||
// L3 emit 双写:tunnel subscriber(阶段2 后续)透传 miniapp。
|
// L3 emit 双写:tunnel subscriber(阶段2 后续)透传 miniapp。
|
||||||
|
|||||||
@@ -138,6 +138,8 @@ pub enum AiChatEvent {
|
|||||||
/// 不完整标记(可选):Some(true)=网络中断保文,None 或 Some(false)=完整回复
|
/// 不完整标记(可选):Some(true)=网络中断保文,None 或 Some(false)=完整回复
|
||||||
incomplete: Option<bool>,
|
incomplete: Option<bool>,
|
||||||
conversation_id: Option<String>,
|
conversation_id: Option<String>,
|
||||||
|
/// 当前会话 pinned_goals(G1 目标钉扎持久化,前端直接读取刷新)
|
||||||
|
pinned_goals: Vec<String>,
|
||||||
},
|
},
|
||||||
/// 错误
|
/// 错误
|
||||||
///
|
///
|
||||||
|
|||||||
@@ -89,6 +89,13 @@ pub fn run() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
// 对话透明化 L1:收集每个 dirty conv 的 pinned_goals 快照(供 emit AiCompleted 携带)
|
||||||
|
let pinned_goals_map: std::collections::HashMap<String, Vec<String>> = dirty_convs
|
||||||
|
.iter()
|
||||||
|
.filter_map(|cid| {
|
||||||
|
session.per_conv.get(cid).map(|c| (cid.clone(), c.pinned_goals.clone()))
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
// BUG-260619-06 修复: clear 致冷启动 restore 重建审批丢失(restore 填充后 clear 无条件清空,
|
// BUG-260619-06 修复: clear 致冷启动 restore 重建审批丢失(restore 填充后 clear 无条件清空,
|
||||||
// 重启后待审批工具全丢)。改 retain 仅清非 recovered(本次会话/HMR 死 pending),
|
// 重启后待审批工具全丢)。改 retain 仅清非 recovered(本次会话/HMR 死 pending),
|
||||||
// 保留 restore 重建(recovered=true,audit.rs:331),对齐 switchConversation retain 保护意图。
|
// 保留 restore 重建(recovered=true,audit.rs:331),对齐 switchConversation retain 保护意图。
|
||||||
@@ -107,6 +114,7 @@ pub fn run() {
|
|||||||
completion_tokens: 0,
|
completion_tokens: 0,
|
||||||
incomplete: None,
|
incomplete: None,
|
||||||
conversation_id: Some(cid.clone()),
|
conversation_id: Some(cid.clone()),
|
||||||
|
pinned_goals: pinned_goals_map.get(cid).cloned().unwrap_or_default(),
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -372,7 +372,7 @@ export type AiChatEvent = ({
|
|||||||
} | {
|
} | {
|
||||||
// UX-2025-04 / CR-30-2 / 决策 F-260616-07 a1: incomplete 标记流中途失败保文(网络中断),
|
// UX-2025-04 / CR-30-2 / 决策 F-260616-07 a1: incomplete 标记流中途失败保文(网络中断),
|
||||||
// 前端据此差异化展示(如系统提示「⚠ 响应因网络中断不完整」)。正常完成/停止均为 undefined。
|
// 前端据此差异化展示(如系统提示「⚠ 响应因网络中断不完整」)。正常完成/停止均为 undefined。
|
||||||
type: 'AiCompleted'; total_tokens: number; prompt_tokens: number; completion_tokens: number; incomplete?: boolean
|
type: 'AiCompleted'; total_tokens: number; prompt_tokens: number; completion_tokens: number; incomplete?: boolean; pinned_goals?: string[]
|
||||||
} | {
|
} | {
|
||||||
type: 'AiError'; error: string; error_type?: AiErrorType
|
type: 'AiError'; error: string; error_type?: AiErrorType
|
||||||
} | {
|
} | {
|
||||||
|
|||||||
@@ -580,7 +580,11 @@ function handleLifecycleEvent(event: AiChatEvent): boolean {
|
|||||||
timestamp: Date.now(),
|
timestamp: Date.now(),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
// 对话透明化 L1:刷新会话列表(更新 pinned_goals)
|
// 对话透明化 L1:直接从事件更新 pinned_goals,不等 loadConversations 异步刷新
|
||||||
|
if (event.pinned_goals && state.activeConversationId) {
|
||||||
|
const conv = state.conversations.find(c => c.id === state.activeConversationId)
|
||||||
|
if (conv) conv.pinned_goals = event.pinned_goals
|
||||||
|
}
|
||||||
void loadConversations()
|
void loadConversations()
|
||||||
// token 用量记录(开关开时):lastTokenUsage 供当前回复展示,convTokenTotal 累加对话总量
|
// token 用量记录(开关开时):lastTokenUsage 供当前回复展示,convTokenTotal 累加对话总量
|
||||||
if (isShowTokenUsage()) {
|
if (isShowTokenUsage()) {
|
||||||
|
|||||||
Reference in New Issue
Block a user