优化 DirectProject 历史幂等检查

按文件尾部反向分块查找带 id 的历史 item

避免每次追加都完整读取并扫描历史文件
This commit is contained in:
2026-09-05 10:42:13 +08:00
parent 7791bd7de0
commit b0a1ee0de2
@@ -5,7 +5,7 @@ use crate::project::{
use crate::{LocalConversationMessageRecord, LocalConversationResult};
use serde_json::Value;
use std::fs::File;
use std::io::{BufRead, BufReader};
use std::io::{BufRead, BufReader, Read, Seek, SeekFrom};
use std::path::{Path, PathBuf};
const DIRECT_PROJECT_HISTORY_RECORD_TYPE: &str = "response_item";
@@ -22,6 +22,92 @@ fn record(item: &Value) -> Result<String, String> {
.map_err(|error| format!("序列化 DirectProject 历史失败:{error}"))
}
const DIRECT_PROJECT_HISTORY_REVERSE_SCAN_CHUNK_BYTES: usize = 16 * 1024;
fn find_direct_project_history_item_by_id_at(
path: &Path,
item_id: &str,
) -> Result<Option<Value>, String> {
let mut file = File::open(path)
.map_err(|error| format!("打开 DirectProject 历史失败:{}: {error}", path.display()))?;
let mut position = file
.metadata()
.map_err(|error| {
format!(
"读取 DirectProject 历史元数据失败:{}: {error}",
path.display()
)
})?
.len();
let mut pending = Vec::new();
let mut chunk = vec![0u8; DIRECT_PROJECT_HISTORY_REVERSE_SCAN_CHUNK_BYTES];
loop {
if position == 0 {
break;
}
let read_len = usize::try_from(position)
.unwrap_or(usize::MAX)
.min(chunk.len());
position -= read_len as u64;
file.seek(SeekFrom::Start(position))
.map_err(|error| format!("定位 DirectProject 历史失败:{}: {error}", path.display()))?;
file.read_exact(&mut chunk[..read_len])
.map_err(|error| format!("读取 DirectProject 历史失败:{}: {error}", path.display()))?;
let mut combined = Vec::with_capacity(read_len + pending.len());
combined.extend_from_slice(&chunk[..read_len]);
combined.extend_from_slice(&pending);
let mut line_end = combined.len();
while let Some(newline) = combined[..line_end].iter().rposition(|byte| *byte == b'\n') {
let line = &combined[newline + 1..line_end];
if !line.is_empty() {
if let Some(item) = direct_project_history_item_from_line(path, line)? {
if item.get("id").and_then(Value::as_str) == Some(item_id) {
return Ok(Some(item));
}
}
}
line_end = newline;
}
pending = combined[..line_end].to_vec();
}
if !pending.is_empty() {
if let Some(item) = direct_project_history_item_from_line(path, &pending)? {
if item.get("id").and_then(Value::as_str) == Some(item_id) {
return Ok(Some(item));
}
}
}
Ok(None)
}
fn direct_project_history_item_from_line(
path: &Path,
line: &[u8],
) -> Result<Option<Value>, String> {
if line.iter().all(|byte| byte.is_ascii_whitespace()) {
return Ok(None);
}
let parsed: Value = serde_json::from_slice(line)
.map_err(|error| format!("解析 DirectProject 历史失败:{}: {error}", path.display()))?;
if parsed.get("type").and_then(Value::as_str) != Some(DIRECT_PROJECT_HISTORY_RECORD_TYPE) {
return Err(format!(
"DirectProject 历史记录类型无效:{}",
path.display()
));
}
let item = parsed
.get("payload")
.cloned()
.ok_or_else(|| format!("DirectProject 历史记录缺少 payload:{}", path.display()))?;
if is_direct_project_internal_context_item(&item) {
return Ok(None);
}
Ok(Some(item))
}
pub(crate) fn append_direct_project_history_item_at(
root: &Path,
item: &Value,
@@ -33,10 +119,7 @@ pub(crate) fn append_direct_project_history_item_at(
let lock = project_append_lock_for(&path)?;
let _append_guard = lock.lock("DirectProject 历史追加写")?;
if let Some(item_id) = item.get("id").and_then(Value::as_str) {
if let Some(existing) = read_direct_project_history_items_at(root)?
.into_iter()
.find(|existing| existing.get("id").and_then(Value::as_str) == Some(item_id))
{
if let Some(existing) = find_direct_project_history_item_by_id_at(&path, item_id)? {
if &existing == item {
return Ok(());
}