新增: 消息分页懒加载与事件总线空转优化

后端基础设施两项改进:

一、历史消息分页懒加载(治长对话渲染卡顿)
- 消息仓库新增分页查询方法(从尾部取最近 N 条,支持游标)
- 切换对话时后端保留全量消息供模型上下文用,但只返回最近
  50 条给前端渲染,附带是否有更多历史和游标位置
- 新增加载更多消息的接口,前端滚顶时按游标分页拉取

二、事件总线空转优化(消除无消费者时的序列化开销)
- 发布事件前先检查订阅者数量,无订阅者直接跳过序列化
- 当前隧道订阅方未接入,20 余个发射点双写不再空转浪费

同时核验确认对话上下文透明化三项均已落地(目标可见/
项目增强预览/完整上下文面板),关闭对应待办。
This commit is contained in:
2026-06-29 00:44:10 +08:00
parent 8d18918e39
commit 3c18dea45b
5 changed files with 116 additions and 11 deletions

View File

@@ -261,10 +261,10 @@ pub async fn ai_conversation_switch(
// fallback 兜底:ai_messages 表为空(返空 Vec)但旧 messages JSON 列非空 `[]`
// (老库未迁移 / 坏数据 / 批次 B 写路径尚未上线时的新对话)→ 回退读 messages JSON + warn。
// 双向兼容:批次 B 上线后写双轨,读永远先走 ai_messages;迁移未跑的老对话走 fallback。
let records = state.ai_messages.list_by_conversation(&conversation_id).await
let all_records = state.ai_messages.list_by_conversation(&conversation_id).await
.map_err(err_str)?;
let messages: Vec<ChatMessage> = if !records.is_empty() {
records.iter().map(record_to_message).collect()
let messages: Vec<ChatMessage> = if !all_records.is_empty() {
all_records.iter().map(record_to_message).collect()
} else {
// 表空 → fallback 旧 messages JSON 列(若也空则空 Vec,空对话合法)
let has_legacy = record.messages != "[]" && !record.messages.is_empty();
@@ -281,9 +281,25 @@ pub async fn ai_conversation_switch(
}
};
// 由 records/chat_messages 重序列化返回前端(前端契约不变,仍吃 JSON 字符串)。
let messages_json = serde_json::to_string(&messages)
// 后端 per_conv 存全量消息(LLM 上下文用),前端只渲染最近 N 条(分页懒加载,治长对话卡顿)。
// PAGE_SIZE:首屏渲染条数,超出的历史消息由前端滚顶加载更多(ai_conversation_load_more IPC)。
const PAGE_SIZE: usize = 50;
let total_count = messages.len();
let render_messages: Vec<ChatMessage> = if total_count > PAGE_SIZE {
messages[total_count - PAGE_SIZE..].to_vec()
} else {
messages.clone()
};
let messages_json = serde_json::to_string(&render_messages)
.map_err(|e| format!("序列化消息失败: {}", e))?;
let has_more = total_count > PAGE_SIZE;
let earliest_seq = if !all_records.is_empty() {
// 返回首屏最早消息的 seq,前端滚顶加载时传此值作游标
let page_start = total_count.saturating_sub(PAGE_SIZE);
Some(all_records[page_start].seq)
} else {
None
};
let title = record.title.clone();
// B-260617-17 续:历史会话 title 空(显"新对话")→ 切入后触发重新生成(用户诉求)。
// 含 "新对话" 占位(Some 但未生成):title.rs ensure :40 同步排除"新对话"占位不跳过,
@@ -383,6 +399,35 @@ pub async fn ai_conversation_switch(
"id": record.id,
"title": title,
"messages": messages_json,
"has_more": has_more,
"earliest_seq": earliest_seq,
}))
}
/// 加载更多历史消息(分页懒加载,滚顶触发)。
///
/// 前端 switchConversation 首次拿最近 50 条 + has_more=true + earliest_seq。
/// 滚顶时传 earliest_seq 调本命令,返回更早的 50 条 + 新 has_more + 新 earliest_seq。
/// per_conv 内存不受影响(后端始终持全量消息供 LLM 上下文用,分页仅影响前端渲染)。
#[tauri::command]
pub async fn ai_conversation_load_more(
state: State<'_, AppState>,
conversation_id: String,
before_seq: i64,
) -> Result<serde_json::Value, String> {
const PAGE_SIZE: usize = 50;
let records = state.ai_messages.list_recent(&conversation_id, PAGE_SIZE, Some(before_seq))
.await
.map_err(err_str)?;
let has_more = records.len() == PAGE_SIZE;
let new_earliest_seq = records.first().map(|r| r.seq);
let messages: Vec<ChatMessage> = records.iter().map(record_to_message).collect();
let messages_json = serde_json::to_string(&messages)
.map_err(|e| format!("序列化消息失败: {}", e))?;
Ok(serde_json::json!({
"messages": messages_json,
"has_more": has_more,
"earliest_seq": new_earliest_seq,
}))
}

View File

@@ -164,8 +164,16 @@ impl EventBus {
/// 序列化为 Value 后调 [`publish`](Self::publish)。供 emit 点双写(publish + app.emit),
/// 经 EventBus 透传给 tunnel subscriber(阶段2 接入)。
///
/// 序列化失败兜底返 0(丢弃,对齐 publish 静默丢弃语义;AiChatEvent 序列化稳定不触发,防御性)。
/// 性能优化(治空转):无订阅者时跳过序列化直接返 0。当前 tunnel subscriber 尚未接入,
/// 20+ emit 点双写调本方法但无消费者,跳过序列化消除热路径开销。接入消费者后自动生效。
///
/// 序列化失败兑底返 0(丢弃,对齐 publish 静默丢弃语义;AiChatEvent 序列化稳定不触发,防御性)。
pub fn publish_event(&self, event: AiChatEvent) -> usize {
// 无订阅者时跳过序列化(治空转开销)。broadcast::receiver_count 是同步原子读,代价极低。
// 有订阅者后才做 serde_json::to_value 序列化 + send。
if self.sender.receiver_count() == 0 {
return 0;
}
match serde_json::to_value(&event) {
Ok(v) => self.publish(v),
Err(_) => 0,

View File

@@ -365,6 +365,7 @@ pub fn run() {
commands::ai::ai_conversation_create,
commands::ai::ai_conversation_list,
commands::ai::ai_conversation_switch,
commands::ai::ai_conversation_load_more,
commands::ai::ai_conversation_delete,
commands::ai::ai_conversation_rename,
commands::ai::ai_conversation_archive,