新增: 工具结果落盘瘦身与空轮自愈
- 超阈值工具结果正文落盘+句柄可读回(治 read_file 29.6% 体量裸奔) - read_file 改小文件豁免(8KB), 超阈值压缩+落盘句柄, 无损可找回 - 空轮判据自愈: 连续空轮先 nudge 续轮, 达阈值停靠止损(开关可回退) - 零 token 收敛信号透传(非截断语义分离) - 上下文清单描述按字符截断, 防清单体量无限膨胀
This commit is contained in:
@@ -555,6 +555,24 @@ pub(crate) async fn build_system_prompt_with_excluded(
|
||||
prompt
|
||||
}
|
||||
|
||||
/// 项目/任务清单单条描述截断上限(字符数,中文一字一符)。
|
||||
///
|
||||
/// 防清单体量随数据无限增长:描述可粘贴长文档,20 项目 + 20 任务 × 长描述可达数万字符,
|
||||
/// 截断后接「…」。技能清单同类截断见 agentic/mod.rs `SKILL_DESC_MAX_CHARS`(80)。
|
||||
const CONTEXT_DESC_MAX_CHARS: usize = 120;
|
||||
|
||||
/// 清单描述截断(纯函数):超 `max` 字符按 Unicode 标量截断并追加「…」,否则原样返回。
|
||||
///
|
||||
/// 按字符而非字节计数(中文描述一字一符,按字节会截断 UTF-8 序列产生乱码)。
|
||||
fn truncate_desc(s: &str, max: usize) -> String {
|
||||
if s.chars().count() <= max {
|
||||
return s.to_string();
|
||||
}
|
||||
let mut out: String = s.chars().take(max).collect();
|
||||
out.push('…');
|
||||
out
|
||||
}
|
||||
|
||||
/// 项目/任务全局清单段(DRY:主构建 [`build_system_prompt_with_excluded`] 与 caching 易变段
|
||||
/// [`build_system_prompt_parts`] 共用)。
|
||||
///
|
||||
@@ -578,7 +596,13 @@ async fn build_context_lists(
|
||||
if excluded_project_ids.iter().any(|id| id == &p.id) {
|
||||
continue;
|
||||
}
|
||||
out.push_str(&format!("- {} (id: {}) ({}): {}\n", p.name, p.id, p.status.as_str(), p.description));
|
||||
out.push_str(&format!(
|
||||
"- {} (id: {}) ({}): {}\n",
|
||||
p.name,
|
||||
p.id,
|
||||
p.status.as_str(),
|
||||
truncate_desc(&p.description, CONTEXT_DESC_MAX_CHARS)
|
||||
));
|
||||
if let Some(ref dir) = p.path {
|
||||
out.push_str(&format!(" 目录: {}\n", dir));
|
||||
}
|
||||
@@ -597,7 +621,13 @@ async fn build_context_lists(
|
||||
if excluded_task_ids.iter().any(|id| id == &tk.id) {
|
||||
continue;
|
||||
}
|
||||
out.push_str(&format!("- {} (id: {}) ({}): {}\n", tk.title, tk.id, tk.status.as_str(), tk.description));
|
||||
out.push_str(&format!(
|
||||
"- {} (id: {}) ({}): {}\n",
|
||||
tk.title,
|
||||
tk.id,
|
||||
tk.status.as_str(),
|
||||
truncate_desc(&tk.description, CONTEXT_DESC_MAX_CHARS)
|
||||
));
|
||||
}
|
||||
// 机制层注明语(中/英):仅最近 20 条,全量/按项目查询走 list_tasks
|
||||
out.push_str(&tasks_listed_note(lang));
|
||||
@@ -631,10 +661,23 @@ pub(crate) fn cache_split_provider_type(provider_type: &str) -> bool {
|
||||
)
|
||||
}
|
||||
|
||||
/// 读取 prompt caching 拆分开关(AppState settings KV `df-ai-anthropic-cache`,默认关)
|
||||
/// 解析 prompt caching 拆分开关原始值为布尔(纯函数,默认开)。
|
||||
///
|
||||
/// 值语义(见 [`cache_enabled`]):缺省/空(从未设置过)→ 默认开;
|
||||
/// 显式 "false"/"0"(设置页开关关)→ 关 = 用户回退;
|
||||
/// "true"/"1" 沿用旧识别;其余杂值保守按关(与历史默认关行为一致)。
|
||||
/// 前端落库为 JSON 布尔字符串("true"/"false",无引号),直接等值比较即可。
|
||||
fn parse_cache_flag(v: &str) -> bool {
|
||||
v.is_empty() || v == "true" || v == "1"
|
||||
}
|
||||
|
||||
/// 读取 prompt caching 拆分开关(AppState settings KV `df-ai-anthropic-cache`,默认开)
|
||||
/// 并同步 df-ai 进程级显式缓存开关。
|
||||
///
|
||||
/// 值语义:非 "true"/"1"(含缺省/空)一律视为关——默认关、用户对支持的端点显式开启。
|
||||
/// 值语义见 [`parse_cache_flag`]:默认开——拆分后易变段(日期/清单)移出稳定前缀,
|
||||
/// anthropic 系显式缓存与 openai_compat 自动前缀缓存命中率提升;调用方另按
|
||||
/// provider_type gate(`cache_split_provider_type`),不支持的端点仍走逐字等价旧路径,
|
||||
/// 默认开对它们零影响。回退:设置页「AI Prompt 缓存」开关关(落库 "false")即整链回旧行为。
|
||||
/// 同步:anthropic_compat::convert_request 读取同一进程级开关决定 system 形态
|
||||
/// (数组+cache_control / 纯字符串),保证开关关时非官方网关零风险。
|
||||
/// 键名保持历史名(向后兼容,已有键不换名),语义已扩展覆盖 openai_compat 前缀稳定拆分。
|
||||
@@ -646,7 +689,7 @@ pub(crate) async fn cache_enabled(state: &AppState) -> bool {
|
||||
.ok()
|
||||
.flatten()
|
||||
.unwrap_or_default();
|
||||
let enabled = v == "true" || v == "1";
|
||||
let enabled = parse_cache_flag(&v);
|
||||
df_ai::anthropic_compat::set_anthropic_cache_enabled(enabled);
|
||||
enabled
|
||||
}
|
||||
@@ -1038,4 +1081,40 @@ mod tests {
|
||||
assert!(en_task.contains("20 most recent tasks"));
|
||||
assert!(en_task.contains("list_tasks"));
|
||||
}
|
||||
|
||||
// 缓存拆分开关默认值断言(看板 939 ⑧:默认关→默认开)
|
||||
#[test]
|
||||
fn parse_cache_flag_defaults_on_and_honors_explicit_off() {
|
||||
// 缺省/空(从未设置过)→ 默认开
|
||||
assert!(parse_cache_flag(""), "缺省(未设置)应默认开");
|
||||
assert!(parse_cache_flag("true"));
|
||||
assert!(parse_cache_flag("1"));
|
||||
// 显式回退:设置页开关关落库 JSON 布尔 "false"
|
||||
assert!(!parse_cache_flag("false"), "显式 false 应为关(用户回退通道)");
|
||||
assert!(!parse_cache_flag("0"));
|
||||
// 杂值保守按关(与历史默认关行为一致,不因脏数据意外开启)
|
||||
assert!(!parse_cache_flag("garbage"));
|
||||
}
|
||||
|
||||
// 清单描述截断(纯函数):短原样 / 恰好上限不截 / 超长截断加 … / 中文按字符非字节
|
||||
#[test]
|
||||
fn truncate_desc_short_exact_and_cjk() {
|
||||
// 短描述与空串原样
|
||||
assert_eq!(truncate_desc("hello", 120), "hello");
|
||||
assert_eq!(truncate_desc("", 120), "");
|
||||
// 恰好等于上限不截断(无省略号)
|
||||
let exact = "字".repeat(120);
|
||||
assert_eq!(truncate_desc(&exact, 120), exact);
|
||||
// 超长:截到 max 字符 + …
|
||||
assert_eq!(truncate_desc("abcdef", 5), "abcde…");
|
||||
// 中文一字一符(按 Unicode 标量计数,非 UTF-8 字节,不产生乱码截断)
|
||||
let long = "汉".repeat(130);
|
||||
let got = truncate_desc(&long, 5);
|
||||
assert_eq!(got, "汉汉汉汉汉…");
|
||||
// 常量绑定:实际清单截断上限 120(防止常量被误改无人知)
|
||||
assert_eq!(CONTEXT_DESC_MAX_CHARS, 120);
|
||||
let got120 = truncate_desc(&long, CONTEXT_DESC_MAX_CHARS);
|
||||
assert_eq!(got120.chars().count(), 121);
|
||||
assert!(got120.ends_with('…'));
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user