- 会话终态提炼: 删除前+停靠双挂点,快照与清水位同临界区 - 目标快照: 恢复引导按最终有效意图生成,旧目标不复活 - 知识检索项目维度软排序,WorkingContext 持久化迁移 - 脉络卡: published 知识按项目聚合纯读侧,带入上下文经输入框显式注入 - 压缩与缓存能力在主循环/子代理/提示词三面接线 - 两份专项设计文档与看板销账
116 lines
5.2 KiB
Rust
116 lines
5.2 KiB
Rust
//! D2a 工具结果过期外置:治 devflow.db 膨胀(193.7MB,审计大结果为主要头)。
|
|
//!
|
|
//! 设计(对齐 u-claw 成熟实践「大内容外置+摘要留指针」):
|
|
//! - 超阈值(8KB)的审计 result 正文落盘至 {app_data}/tool-results/{yyyy-MM}/{id}.txt,
|
|
//! DB 只留句柄 JSON(offloaded 标记 + 路径 + 头部摘要 + 大小 + 工具名/时间)。
|
|
//! - 句柄可读:句柄含 path,用户/agent 需要细节时按路径找回(工具结果在 messages 侧
|
|
//! 已有 namespace 外置,本模块治审计表侧——两处同构,互不影响)。
|
|
//! - 归档清理已彻底移除(2026-10-11):原 90 天归档设计(2026-10-06 提出,2026-10-09 停用)
|
|
//! 现整体删除,外置文件只增不减、永不清理。read_offloaded 读回入口已接线
|
|
//! (IPC ai_read_offloaded_result),句柄永远可读回。
|
|
//! - 历史数据默认保留禁清理(全局规则),与本次决策一致,外置派生文件同样保留。
|
|
|
|
use std::path::PathBuf;
|
|
use std::sync::OnceLock;
|
|
|
|
/// 外置根目录(进程级一次注入;lib.rs setup 调 init_base)。未注入时 offload_result 直通。
|
|
static BASE: OnceLock<PathBuf> = OnceLock::new();
|
|
|
|
/// 注入外置根目录(app data dir)。幂等,首次生效。
|
|
pub fn init_base(base: PathBuf) {
|
|
let _ = BASE.set(base);
|
|
}
|
|
|
|
/// 无参包装:build_audit_record 等审计写路径统一走此入口。
|
|
/// base 未注入或失败降级时原样返回(不外置不丢数据)。
|
|
pub fn offload_result(id: &str, tool_name: &str, result: &str) -> String {
|
|
match BASE.get() {
|
|
Some(b) => offload_if_large(b, id, tool_name, result),
|
|
None => result.to_string(),
|
|
}
|
|
}
|
|
|
|
/// 外置阈值:审计 result 超 8KB 落盘(DB 只留句柄)。namespace 侧阈值 2048B 更激进
|
|
/// (上下文敏感),审计侧存证要求低,8KB 平衡 IO 频率与瘦身收益。
|
|
pub const OFFLOAD_THRESHOLD_BYTES: usize = 8 * 1024;
|
|
|
|
/// 句柄 JSON 字段名(读取降级判据)。
|
|
const HANDLE_MARK: &str = "__offloaded__";
|
|
|
|
/// 计算外置目录({base}/tool-results/{yyyy-MM}/)。base 由调用方传入(app data dir),
|
|
/// 本模块不做 Tauri 依赖(纯函数可测)。
|
|
fn month_dir(base: &PathBuf, id: &str) -> PathBuf {
|
|
// id 前缀含时间成分则用之,否则统一放 unsorted/;目录按月仅为人读友好
|
|
let month = id.get(0..7).filter(|s| s.len() == 7 && s.as_bytes()[4] == b'-').unwrap_or("unsorted");
|
|
base.join("tool-results").join(month)
|
|
}
|
|
|
|
/// 落盘并生成句柄。失败(磁盘/权限)时返回原内容(降级:不外置也不丢数据)。
|
|
pub fn offload_if_large(base: &PathBuf, id: &str, tool_name: &str, result: &str) -> String {
|
|
if result.len() <= OFFLOAD_THRESHOLD_BYTES {
|
|
return result.to_string();
|
|
}
|
|
let dir = month_dir(base, id);
|
|
if std::fs::create_dir_all(&dir).is_err() {
|
|
return result.to_string();
|
|
}
|
|
let file = dir.join(format!("{id}.txt"));
|
|
if std::fs::write(&file, result).is_err() {
|
|
return result.to_string();
|
|
}
|
|
let head: String = result.chars().take(200).collect();
|
|
// meta 旁路文件:标记本模块自产物(带 offloaded 标记),供溯源识别
|
|
let meta = serde_json::json!({ HANDLE_MARK: true, "id": id, "tool": tool_name });
|
|
let _ = std::fs::write(file.with_extension("meta.json"), meta.to_string());
|
|
let handle = serde_json::json!({
|
|
HANDLE_MARK: true,
|
|
"path": file.to_string_lossy(),
|
|
"size_bytes": result.len(),
|
|
"tool": tool_name,
|
|
"head": head,
|
|
"hint": "结果过大已外置落盘,需要细节时按 path 读取原文"
|
|
});
|
|
handle.to_string()
|
|
}
|
|
|
|
/// 读外置文件(前端/查询侧展开句柄用)。文件缺失时返回带语义的错误(降级不 panic)。
|
|
///
|
|
/// 注:90 天归档清理已停用(2026-10-09 用户决策,外置文件全部保留),故此路径
|
|
/// 正常应恒可读;报错仅当文件被外部误删或路径失效。
|
|
pub fn read_offloaded(path: &str) -> Result<String, String> {
|
|
std::fs::read_to_string(path).map_err(|e| format!("外置结果读取失败(文件可能已被外部删除): {e}"))
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
|
|
#[test]
|
|
fn small_result_not_offloaded() {
|
|
let base = std::env::temp_dir().join(format!("d2a-test-{}", std::process::id()));
|
|
let out = offload_if_large(&base, "2026-10-07-x", "read_file", "short");
|
|
assert_eq!(out, "short");
|
|
let _ = std::fs::remove_dir_all(base.join("tool-results"));
|
|
}
|
|
|
|
#[test]
|
|
fn large_result_offloaded_with_handle() {
|
|
let base = std::env::temp_dir().join(format!("d2a-test-{}", std::process::id()));
|
|
let big = "x".repeat(10 * 1024);
|
|
let out = offload_if_large(&base, "2026-10-07-y", "run_command", &big);
|
|
let v: serde_json::Value = serde_json::from_str(&out).unwrap();
|
|
assert_eq!(v.get(HANDLE_MARK).and_then(|b| b.as_bool()), Some(true));
|
|
let path = v.get("path").and_then(|p| p.as_str()).unwrap();
|
|
// 原文可找回
|
|
assert_eq!(read_offloaded(path).unwrap(), big);
|
|
let _ = std::fs::remove_dir_all(base.join("tool-results"));
|
|
}
|
|
|
|
#[test]
|
|
fn month_dir_parses_id_prefix() {
|
|
let base = PathBuf::from("/tmp");
|
|
assert!(month_dir(&base, "2026-10-07-abcd").ends_with("tool-results/2026-10"));
|
|
assert!(month_dir(&base, "weird").ends_with("tool-results/unsorted"));
|
|
}
|
|
}
|