新增: AI Chat多项增强(审批去重/编辑重发/导出/实体引用/会话置顶搜索)+任务推进链df-nodes落地
This commit is contained in:
@@ -287,6 +287,14 @@ impl ContextManager {
|
||||
fn sanitize_messages(messages: Vec<ChatMessage>) -> Vec<ChatMessage> {
|
||||
use std::collections::HashSet;
|
||||
|
||||
// step 0(UX-09):过滤 truncated 软删消息,不进 LLM 上下文。
|
||||
// 编辑某条 user 消息后其后续消息标 truncated(保留 DB 可追溯),发送视图必须剔除,
|
||||
// 否则被编辑前的旧回复仍进入 LLM 历史,污染重生成语义。落库全量保留不受影响。
|
||||
let messages: Vec<ChatMessage> = messages
|
||||
.into_iter()
|
||||
.filter(|m| m.is_active())
|
||||
.collect();
|
||||
|
||||
// step 1:已闭合的 tool_call_id 集合(role=Tool 消息全部提供过的 id)
|
||||
let resolved_ids: HashSet<&str> = messages
|
||||
.iter()
|
||||
@@ -432,6 +440,77 @@ impl ContextManager {
|
||||
true
|
||||
}
|
||||
|
||||
/// 弹出末尾连续的 assistant 消息(含其 tool_calls 三元组尾随 tool_result)
|
||||
///
|
||||
/// 用于「重新生成」(UX-02):删掉最后一条 AI 回复(可能跨多轮 tool_calls + tool_results
|
||||
/// 紧随其后),保留触发它的 user 消息,以便重跑 agentic loop 再生成。
|
||||
///
|
||||
/// 语义:从末尾向前弹出,直到弹出至少一条 assistant 消息;若弹出 assistant 后紧邻的更早
|
||||
/// 消息仍是 assistant/tool(同一轮多块),继续一并弹出,确保不留半截三元组污染下轮。
|
||||
/// user 消息作为停止边界(不弹出 user),保证重生成时历史末尾是 user 消息。
|
||||
pub fn pop_last_assistant_round(&mut self) -> bool {
|
||||
if self.messages.is_empty() {
|
||||
return false;
|
||||
}
|
||||
let mut popped_any = false;
|
||||
// 从尾向前:先弹掉末尾非 user 的消息(assistant / tool),直到遇到 user 或空
|
||||
while let Some(last) = self.messages.last() {
|
||||
if matches!(last.message.role, MessageRole::User) {
|
||||
break;
|
||||
}
|
||||
let removed = self.messages.pop().expect("just checked non-empty");
|
||||
self.history_tokens = self.history_tokens.saturating_sub(removed.token_count);
|
||||
if matches!(removed.message.role, MessageRole::Assistant) {
|
||||
popped_any = true;
|
||||
}
|
||||
}
|
||||
popped_any
|
||||
}
|
||||
|
||||
/// 编辑某条 user 消息后,将其后所有消息标记为 truncated(UX-09 编辑重生成)。
|
||||
///
|
||||
/// 软删语义:保留在内存真相源 + DB(可追溯),但 sanitize_messages 过滤后不进 LLM 上下文,
|
||||
/// 前端按 is_active 过滤从视图移除。返回被标 truncated 的条数(0 表示该 user 消息已是末尾,无后续)。
|
||||
///
|
||||
/// `target_content` 为该 user 消息的预期内容(用于反向唯一定位:末条 user 消息可能内容相同,
|
||||
/// 故从尾部向前找第一条 role=User 且 content 匹配且仍 active 的消息)。
|
||||
/// 找不到返回 Err(()),调用方据此报错。
|
||||
pub fn truncate_after_user_message(&mut self, target_content: &str) -> Result<usize, ()> {
|
||||
// 反向找末条 active user 消息且 content 匹配
|
||||
let pos = self.messages.iter().rposition(|t| {
|
||||
matches!(t.message.role, MessageRole::User)
|
||||
&& t.message.content == target_content
|
||||
&& t.message.is_active()
|
||||
});
|
||||
let Some(i) = pos else { return Err(()) };
|
||||
// i 之后的全部标 truncated(已 truncated 的跳过,只统计本次新标的)
|
||||
let mut count = 0usize;
|
||||
for t in self.messages[i + 1..].iter_mut() {
|
||||
if t.message.is_active() {
|
||||
t.message.status = Some("truncated".to_string());
|
||||
count += 1;
|
||||
}
|
||||
}
|
||||
Ok(count)
|
||||
}
|
||||
|
||||
/// 替换末条 active user 消息的 content(UX-09 编辑重生成)。
|
||||
///
|
||||
/// 编辑语义:只能编辑最后一条 user 消息(中间编辑语义复杂,拒绝)。返回 Err(()) 表示无 active user 消息。
|
||||
/// 成功后调用方应紧接着 truncate_after_user_message(new_content) 软删其后续消息。
|
||||
pub fn replace_last_active_user_content(&mut self, new_content: &str) -> Result<(), ()> {
|
||||
let pos = self.messages.iter().rposition(|t| {
|
||||
matches!(t.message.role, MessageRole::User) && t.message.is_active()
|
||||
});
|
||||
let Some(i) = pos else { return Err(()) };
|
||||
let old_tokens = self.messages[i].token_count;
|
||||
self.messages[i].message.content = new_content.to_string();
|
||||
let new_tokens = self.estimator.estimate_message(&self.messages[i].message);
|
||||
self.messages[i].token_count = new_tokens;
|
||||
self.history_tokens = self.history_tokens.saturating_sub(old_tokens).saturating_add(new_tokens);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// 只读迭代(兼容 ensure_conversation_title 的 .iter().filter() 等)
|
||||
pub fn iter(&self) -> impl Iterator<Item = &ChatMessage> {
|
||||
self.messages.iter().map(|t| &t.message)
|
||||
|
||||
Reference in New Issue
Block a user