新增: 批次工作落地(推进链/评估闭环/事件总线/并发/加固) + 技术债清理 + 文档整理
后端: - 工作流推进链(D-03):advance_task/状态机/闸门走 df-nodes Node trait,conditions 条件引擎扩展 - 想法评估闭环:启发式评分+对抗评估,df-ideas/scoring + df-storage/idea_eval_repo + idea 前端打通 - 全局事件数据总线:df-ai/context+context_helpers+augmentation 跨模块解耦 - AI planner/plan_hint/intent:aichat B 路线并行多轮基础 - patch_file 加固(TD-03/04):读改写整体锁防 lost update,expected_hash 合约闭环 - 压缩超时兜底(F-15 卡死根治) - F-09 多会话并发:LlmConcurrency per-conv + streamingGuard 前端守护 + verify 脚本 - 知识注入 DRY/skills/audit 扩展 清理: - aichat 技术债(误报 allow/死导入/过时注释 30 项) - URGENT.md 删除(11 项加急全解决/迁 todo) - 文档整理(todo/待决策/待审查/ARCHITECTURE/INDEX + 总线/技术债审查新文档)
This commit is contained in:
@@ -474,6 +474,69 @@ pub const PROTECT_COUNT: usize = 6;
|
||||
/// 此类 id 必然无匹配 tool_result,是历史中毒的标志,sanitize 时据此剔除畸形三元组。
|
||||
pub const TOOL_MISSING_PREFIX: &str = "tool_missing_";
|
||||
|
||||
/// 阶段2(path_auth 审批链重构):占位配对完整性(解 400 orphan)常量开关。
|
||||
///
|
||||
/// 根因:审批挂起占位 tool_result(内容为 audit/cache.rs:PENDING_APPROVAL_PLACEHOLDER)
|
||||
/// 与其 tool_call 头经 sanitize/compress 裁剪后丢配对头 → orphan tool_result(无头)→
|
||||
/// deepseek-v4-pro 等端点 400。本开关控制 sanitize step3.5(反向 orphan 丢弃) +
|
||||
/// 发送视图出口断言占位配对完整(失败降级 TOOL_MISSING_PREFIX 自愈)。
|
||||
///
|
||||
/// true(默认):启用反向 orphan 检测 + 出口占位配对断言自愈(根治 400)。
|
||||
/// false(回退):sanitize 仅做原正向 orphan(头无 result)处理,出口不断言(旧行为)。
|
||||
/// 兜底:sanitize view-only 不改持久化,失败只影响单请求,flag 关→行为完全等价改动前。
|
||||
pub const PLACEHOLDER_INTEGRITY_ENABLED: bool = true;
|
||||
|
||||
/// 占位 tool_result 内容中嵌入的唯一标记前缀(供 sanitize 识别"审批挂起占位,不可裁")。
|
||||
///
|
||||
/// 写入处(audit/cache.rs:PENDING_APPROVAL_PLACEHOLDER)格式:`{占位文本}{__PENDING__}{tc_id}`,
|
||||
/// 如 "需要用户审批,等待确认__PENDING__:call_abc123"。读取处(sanitize/出口断言)按此前缀
|
||||
/// 切出 tc_id,据此把占位 tool_result 与其 tool_call 头强绑定:若头被裁/丢了,降级自愈
|
||||
/// (头补 TOOL_MISSING_PREFIX 或 tool_result 丢弃),防 orphan 触发 provider 400。
|
||||
///
|
||||
/// 设计权衡:不用独立字段(改 ChatMessage schema 跨 crate + DB 迁移成本高),而是 content
|
||||
/// 内嵌标记——占位文本固定且唯一(非用户内容),内嵌不污染语义(LLM 看到也理解"等待审批")。
|
||||
pub const PENDING_MARKER_PREFIX: &str = "__PENDING__:";
|
||||
|
||||
/// 判定 tool_result content 是否为审批挂起占位(含 PENDING_MARKER_PREFIX 标记)。
|
||||
///
|
||||
/// 兼容老占位(无标记,纯 PENDING_APPROVAL_PLACEHOLDER 文本)与新占位(带 __PENDING__:tc_id):
|
||||
/// - 新占位:content 含 PENDING_MARKER_PREFIX(`__PENDING__:` 是独占信号,权威判定)
|
||||
/// - 老占位:content == PENDING_APPROVAL_PLACEHOLDER(向前兼容,迁移期共存)
|
||||
///
|
||||
/// **收紧(误判防护)**:`__PENDING__` 标记是 audit/cache.rs 占位模板独占字符串,绝不会出现在
|
||||
/// 用户正文里 → 标记存在即权威。老占位分支原用 `starts_with("需要用户审批")` 过松:若用户真实
|
||||
/// tool_result(如读到的文档片段)恰好以"需要用户审批..."开头,会被误判占位 → sanitize 豁免保留
|
||||
/// 或 compress 跳过压缩,污染 LLM 上下文。收紧为**精确等于**老占位全文(与 audit/cache.rs
|
||||
/// `PENDING_APPROVAL_PLACEHOLDER = "需要用户审批,等待确认"` 字面量同步),杜绝前缀误伤。
|
||||
///
|
||||
/// 注:老占位字面量在 src-tauri audit/cache.rs 定义(pub(crate) 不可跨 crate 引用),
|
||||
/// 此处内联同步字面量并加测试锁定;改字面量须同步 audit/cache.rs 与下方单测。
|
||||
pub fn is_pending_placeholder(content: &str) -> bool {
|
||||
// 权威信号:`__PENDING__` 标记独占,存在即占位。
|
||||
if content.contains(PENDING_MARKER_PREFIX) {
|
||||
return true;
|
||||
}
|
||||
// 收紧:老占位精确全文匹配(非 starts_with 前缀),杜绝用户内容前缀误伤。
|
||||
content == LEGACY_PENDING_PLACEHOLDER_TEXT
|
||||
}
|
||||
|
||||
/// 老占位(无 __PENDING__ 标记)的精确全文,与 audit/cache.rs `PENDING_APPROVAL_PLACEHOLDER` 同步。
|
||||
///
|
||||
/// 单独常量便于:① 字面量漂移自检(改字面量搜此常量即定位所有引用);② 测试锁定。
|
||||
/// 独立于 PENDING_MARKER_PREFIX(新占位用),老占位是迁移期共存数据。
|
||||
pub const LEGACY_PENDING_PLACEHOLDER_TEXT: &str = "需要用户审批,等待确认";
|
||||
|
||||
/// 从占位 tool_result content 切出嵌入的 tc_id(供 sanitize 反向定位其 tool_call 头)。
|
||||
///
|
||||
/// content 形如 "...__PENDING__:call_abc123",切出 "call_abc123"。无标记 → None(老占位
|
||||
/// 或非占位,调用方按无 tc_id 处理:走常规反向 orphan 检测,占位无标记时不享受强绑定保护)。
|
||||
pub fn extract_pending_tc_id(content: &str) -> Option<&str> {
|
||||
content
|
||||
.split_once(PENDING_MARKER_PREFIX)
|
||||
.map(|(_, id)| id.trim())
|
||||
.filter(|id| !id.is_empty())
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
@@ -929,4 +992,88 @@ mod tests {
|
||||
assert!(toks.contains(&"call".to_string()));
|
||||
assert!(toks.contains(&"now".to_string()));
|
||||
}
|
||||
|
||||
// ── 阶段2 占位配对完整性:is_pending_placeholder / extract_pending_tc_id ──
|
||||
|
||||
#[test]
|
||||
fn is_pending_placeholder_new_with_marker() {
|
||||
// 新占位:占位文本 + __PENDING__:tc_id 标记
|
||||
let content = "需要用户审批,等待确认__PENDING__:call_abc123";
|
||||
assert!(is_pending_placeholder(content), "带标记的新占位应识别");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn is_pending_placeholder_legacy_without_marker() {
|
||||
// 老占位:纯文本无标记(向前兼容,迁移期共存)
|
||||
assert!(is_pending_placeholder("需要用户审批,等待确认"), "老占位(纯文本)应识别");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn is_pending_placeholder_rejects_normal_content() {
|
||||
// 非占位的正常 tool_result 内容不应误判
|
||||
assert!(!is_pending_placeholder("文件内容: hello world"));
|
||||
assert!(!is_pending_placeholder("{\"ok\":true}"));
|
||||
assert!(!is_pending_placeholder(""));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn is_pending_placeholder_rejects_user_content_with_prefix() {
|
||||
// 收紧(防误伤):用户真实 tool_result 内容恰好以"需要用户审批"开头(如读到的文档片段),
|
||||
// 但不是完整老占位全文 → 不应误判为占位(否则 sanitize 豁免保留/compress 跳过压缩污染 LLM)。
|
||||
assert!(
|
||||
!is_pending_placeholder("需要用户审批的文件清单如下:\n1. 文档A\n2. 文档B"),
|
||||
"以占位前缀开头但非完整老占位的用户内容,不应误判占位"
|
||||
);
|
||||
assert!(
|
||||
!is_pending_placeholder("需要用户审批一下这个改动,我看不太懂"),
|
||||
"前缀 + 后续不同文本不应误判"
|
||||
);
|
||||
// 仅完整老占位全文才匹配(精确等值,非前缀)
|
||||
assert!(
|
||||
is_pending_placeholder(LEGACY_PENDING_PLACEHOLDER_TEXT),
|
||||
"完整老占位全文应识别"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn is_pending_placeholder_marker_is_authoritative_signal() {
|
||||
// 对抗:__PENDING__: 标记是 audit/cache.rs 占位模板独占字符串,绝不会出现在用户正文里。
|
||||
// 故标记存在即权威信号(即使非开头也认占位)——独占语义保证不误判。
|
||||
assert!(
|
||||
is_pending_placeholder("任意内容__PENDING__:call_x"),
|
||||
"__PENDING__ 标记是独占信号,存在即识别为占位"
|
||||
);
|
||||
// 反向:无标记 + 非占位文本开头 → 不识别
|
||||
assert!(
|
||||
!is_pending_placeholder("用户问:这个需要审批吗?"),
|
||||
"无标记 + 非占位模板开头不应识别"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn extract_pending_tc_id_new_marker() {
|
||||
// 新占位:切出 __PENDING__: 之后的 tc_id
|
||||
let content = "需要用户审批,等待确认__PENDING__:call_xyz789";
|
||||
assert_eq!(extract_pending_tc_id(content), Some("call_xyz789"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn extract_pending_tc_id_legacy_no_marker() {
|
||||
// 老占位:无标记 → None
|
||||
assert_eq!(extract_pending_tc_id("需要用户审批,等待确认"), None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn extract_pending_tc_id_empty_id() {
|
||||
// 标记后无内容(异常)→ None(防空 tc_id 误判)
|
||||
assert_eq!(extract_pending_tc_id("需要用户审批,等待确认__PENDING__:"), None);
|
||||
assert_eq!(extract_pending_tc_id("需要用户审批,等待确认__PENDING__: "), None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn extract_pending_tc_id_non_placeholder() {
|
||||
// 非占位内容 → None
|
||||
assert_eq!(extract_pending_tc_id("文件内容"), None);
|
||||
assert_eq!(extract_pending_tc_id(""), None);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user