新增: 消息级溯源 P1 + 修复流式失败重试(UX-260619-06)

消息级溯源 P1(溯源字段升级):
- ContextManager 加 last_assistant/last_user_message_id 取消息 id
- audit 6 处 message_id 接真值(替换 P0 None 占位)
- 知识提炼 source_ref 升级 conv_msg:{id}(知识生命线 extracted/referenced 双事件)
- 4 处 build_knowledge_context 调用方传 user message id
- 老数据 id=None 降级 conv:{id} 兼容

流式失败重试修复(UX-260619-06):
- 症状1(回答一半消失):AiError 收尾前 flushCurrentText 保留部分回复到占位气泡
  (原仅 AiAgentRound/AiCompleted 回填,AiError 清 currentText 致部分回复丢失)
- 症状2(重试报"没有可重新生成的回复"):ai_regenerate pop_last_assistant_round 返 false 时
  末尾是 user(流式中途失败这轮 assistant 未持久化)直接从该 user 重跑(等价重发)
  1214 连续 user 已由 merge_consecutive_users(7c98134)修,本批补失败重试链路

自验: workspace EXIT 0 + cargo test 全绿 + vue-tsc EXIT 0
This commit is contained in:
2026-06-19 19:54:16 +08:00
parent cea0c71546
commit d3e7640b8d
7 changed files with 219 additions and 25 deletions

View File

@@ -68,12 +68,17 @@ impl KnowledgeTimeline {
}
/// AI 对话提炼产生(context: conv_id / conv_title / reasoning)
///
/// F-260619-04 P1 消息级溯源:`message_id` 为末条 assistant 消息 id(本轮 AI 产出知识的载体)。
/// - Some(id) → source_ref = `conv_msg:{id}`(消息级)
/// - None(老数据无 id) → source_ref = `conv:{conv_id}`(对话级,向前兼容)
pub async fn record_extracted(
&self,
knowledge_id: &str,
conv_id: &str,
conv_title: &str,
reasoning: &str,
message_id: Option<&str>,
) {
let ctx = serde_json::json!({
"conv_id": conv_id,
@@ -81,22 +86,40 @@ impl KnowledgeTimeline {
"reasoning": reasoning,
})
.to_string();
let source_ref = match message_id {
Some(mid) => format!("conv_msg:{}", mid),
None => format!("conv:{}", conv_id),
};
self.fire(
knowledge_id,
EVENT_EXTRACTED,
Some(format!("conv:{}", conv_id)),
Some(source_ref),
Some(ctx),
)
.await;
}
/// 被检索命中/注入(context: conv_id / query)
pub async fn record_referenced(&self, knowledge_id: &str, conv_id: &str, query: &str) {
///
/// F-260619-04 P1 消息级溯源:`message_id` 为触发检索的 user 消息 id(用户问题命中知识)。
/// - Some(id) → source_ref = `conv_msg:{id}`(消息级)
/// - None(老数据无 id) → source_ref = `conv:{conv_id}`(对话级,向前兼容)
pub async fn record_referenced(
&self,
knowledge_id: &str,
conv_id: &str,
query: &str,
message_id: Option<&str>,
) {
let ctx = serde_json::json!({ "conv_id": conv_id, "query": query }).to_string();
let source_ref = match message_id {
Some(mid) => format!("conv_msg:{}", mid),
None => format!("conv:{}", conv_id),
};
self.fire(
knowledge_id,
EVENT_REFERENCED,
Some(format!("conv:{}", conv_id)),
Some(source_ref),
Some(ctx),
)
.await;