新增: 消息级溯源 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:
@@ -463,6 +463,36 @@ impl ContextManager {
|
||||
self.messages.iter().map(|t| &t.message)
|
||||
}
|
||||
|
||||
/// F-260619-04 P1 消息级溯源:取末条指定 role 消息的 id(ULID)。
|
||||
///
|
||||
/// 从尾部反向扫描(末条消息命中即停,避免全量 O(n) 正扫累积),返回最近一条
|
||||
/// `role` 匹配且 `id` 非空消息的 id。无匹配或老消息无 id → None(向前兼容:
|
||||
/// 老反序列化消息 id=None,溯源写入降级为 None,展示侧兼容 `conv:` 旧格式)。
|
||||
///
|
||||
/// 用途:
|
||||
/// - `MessageRole::Assistant`:audit/知识提炼写入时取当前 assistant 消息 id
|
||||
/// (LLM 返回带 tool_calls 的 assistant 已 push,process_tool_calls 入口取)
|
||||
/// - `MessageRole::User`:知识注入 referenced 事件溯源取触发检索的 user 消息 id
|
||||
fn last_message_id_by_role(&self, role: MessageRole) -> Option<String> {
|
||||
self.messages
|
||||
.iter()
|
||||
.rev()
|
||||
.find(|t| {
|
||||
std::mem::discriminant(&t.message.role) == std::mem::discriminant(&role)
|
||||
})
|
||||
.and_then(|t| t.message.id.clone())
|
||||
}
|
||||
|
||||
/// 末条 assistant 消息的 id(消息级溯源用)。
|
||||
pub fn last_assistant_message_id(&self) -> Option<String> {
|
||||
self.last_message_id_by_role(MessageRole::Assistant)
|
||||
}
|
||||
|
||||
/// 末条 user 消息的 id(消息级溯源用)。
|
||||
pub fn last_user_message_id(&self) -> Option<String> {
|
||||
self.last_message_id_by_role(MessageRole::User)
|
||||
}
|
||||
|
||||
// ── F-15 上下文管理增强辅助方法(阶段1 基础,零行为变化)──
|
||||
//
|
||||
// 这些方法供阶段2 IPC(ai_chat_compress_context)/ 阶段3 agentic loop
|
||||
@@ -1175,4 +1205,77 @@ mod tests {
|
||||
unit2.end
|
||||
);
|
||||
}
|
||||
|
||||
// ── F-260619-04 P1 消息级溯源:last_assistant/last_user message_id ──
|
||||
|
||||
#[test]
|
||||
fn last_assistant_message_id_returns_latest() {
|
||||
// 多条 assistant,反向扫描取末条 id(本轮 AI 产出的载体)
|
||||
let mut mgr = ContextManager::new(cfg(100_000));
|
||||
mgr.push(ChatMessage::user("问1"));
|
||||
let first = push_and_get_id(&mut mgr, ChatMessage::assistant("答1"));
|
||||
mgr.push(ChatMessage::user("问2"));
|
||||
let last = push_and_get_id(&mut mgr, ChatMessage::assistant("答2"));
|
||||
// 末条 assistant id 应是 last(非 first)
|
||||
assert_eq!(
|
||||
mgr.last_assistant_message_id().as_deref(),
|
||||
Some(last.as_str()),
|
||||
"应取末条 assistant id, 而非首条"
|
||||
);
|
||||
assert_ne!(
|
||||
mgr.last_assistant_message_id().as_deref(),
|
||||
Some(first.as_str())
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn last_user_message_id_returns_latest() {
|
||||
// 多条 user,反向扫描取末条 id(触发本轮检索的 user)
|
||||
let mut mgr = ContextManager::new(cfg(100_000));
|
||||
mgr.push(ChatMessage::user("问1"));
|
||||
mgr.push(ChatMessage::assistant("答1"));
|
||||
let last_user = push_and_get_id(&mut mgr, ChatMessage::user("问2"));
|
||||
assert_eq!(
|
||||
mgr.last_user_message_id().as_deref(),
|
||||
Some(last_user.as_str())
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn last_message_id_none_when_no_such_role() {
|
||||
// 无 assistant → None;无 user → None
|
||||
let mut mgr = ContextManager::new(cfg(100_000));
|
||||
mgr.push(ChatMessage::user("只有 user"));
|
||||
assert!(
|
||||
mgr.last_assistant_message_id().is_none(),
|
||||
"无 assistant 消息应返 None"
|
||||
);
|
||||
|
||||
let mut mgr2 = ContextManager::new(cfg(100_000));
|
||||
mgr2.push(ChatMessage::assistant("只有 assistant"));
|
||||
assert!(
|
||||
mgr2.last_user_message_id().is_none(),
|
||||
"无 user 消息应返 None"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn last_message_id_none_for_legacy_no_id() {
|
||||
// 老数据反序列化消息 id=None → 返 None(向前兼容,溯源降级 conv: 旧格式)
|
||||
let mut mgr = ContextManager::new(cfg(100_000));
|
||||
let mut legacy = ChatMessage::assistant("老消息无 id");
|
||||
legacy.id = None;
|
||||
mgr.push(legacy);
|
||||
assert!(
|
||||
mgr.last_assistant_message_id().is_none(),
|
||||
"老消息无 id 应返 None(向前兼容)"
|
||||
);
|
||||
}
|
||||
|
||||
/// helper:push 一条消息并返回其 id(测试用,确认取到的是该消息自身 id)
|
||||
fn push_and_get_id(mgr: &mut ContextManager, message: ChatMessage) -> String {
|
||||
let id = message.id.clone();
|
||||
mgr.push(message);
|
||||
id.expect("新构造消息必有 id")
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user