优化 DirectProject 历史幂等检查
按文件尾部反向分块查找带 id 的历史 item 避免每次追加都完整读取并扫描历史文件
This commit is contained in:
@@ -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(());
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user