修复+重构: 全库走查真bug+架构+P1/P2 后端 crate

- df-nodes: schema required 对齐 + docker POSIX 注入防御 + HumanNode timeout 1800 + parse_review_json(verdict规范/score clamp/正则兜底)
- df-mcp: update 实体校验(防跨实体 B-260801-01)
- df-storage: keyring 迁移失败达阈值清除明文
- df-ai: router estimated_context+tier tiebreak+DataReadOnly 兜底 + sanitize step4 显式不制造 orphan
- df-ideas: adversarial tier:None 对齐
This commit is contained in:
lxy
2026-08-02 10:44:09 +08:00
parent f1773dde4f
commit ea309c607b
11 changed files with 1512 additions and 96 deletions
+142 -2
View File
@@ -259,12 +259,16 @@ impl Node for AiSelfReviewNode {
"type": "object",
"properties": {
"task_id": { "type": "string", "description": "自审目标任务 ID(必填)" },
"provider_id": { "type": "string", "description": "AI Provider ID(密钥经 secret 解析不进 config;留空走默认 provider)" },
"provider_id": { "type": "string", "description": "AI Provider ID(可选,留空走默认 provider;密钥经 secret 解析不进 config)" },
"model": { "type": "string", "description": "模型名(可选,留空用 record.default_model)" },
"max_tokens": { "type": "integer" },
"gate": { "type": "boolean", "description": "闸门开关:false(默认)=自审辅助,verdict 仅透传展示;true=自审结果作 DAG 闸门,verdict=fail 返回 Err 阻断下游(工作流 failed → ②-4 退回),verdict=unknown/pass 放行" }
},
"required": ["task_id", "provider_id"]
// SW-260802-01: schema 与 handler 行为对齐 — required 仅列 handler 真正强制必填的字段。
// task_id: execute 第 100-104 行缺 task_id 直接 Err("缺少必填参数: task_id"),真必填 → 保留。
// provider_id: execute 调 resolve_and_parse → resolve_provider(ai_node_helpers.rs:100-108),
// 空串走路径 2(老明文)/路径 3(默认 provider)兜底,运行时非必填 → 移出 required。
"required": ["task_id"]
}),
output: serde_json::json!({
"type": "object",
@@ -408,6 +412,95 @@ mod tests {
assert_eq!(v["summary"], json!("缺边界处理"));
}
// ============================================================
// P2:parse_review_json 三道加固(verdict 规范 / score clamp / 正则兜底)
// ============================================================
//
// 真实 bug 场景:
// - LLM 输出 verdict="Pass"/"PASS" → 原 == "fail" 精确匹配 → 漏阻断(放行 fail 项)
// - LLM 输出 score=99/-1 → 越界值污染闸门阈值(维度 score<6 视 fail)与前端展示
// - LLM 前置解释文字 "审查结果:\n{...}" → 整段 serde 失败 → 兜底 unknown(本可救回)
// 加固后:verdict 规范化 / score clamp [0,10] / 首个 { 到末 } 正则兜底提取。
/// P2-加固1:verdict 大小写/空白变体规范化为 pass/fail/unknown 三态。
/// 防 gate_should_block 精确匹配 "fail" 对 "Fail"/"FAIL" 漏阻断。
#[test]
fn parse_review_json_normalizes_verdict_case_variants() {
// "Pass" → "pass"(防 LLM 首字母大写)
let v = parse_review_json(r#"{"verdict":"Pass","summary":"ok"}"#);
assert_eq!(v["verdict"], json!("pass"), "Pass 应规范化为 pass");
// "FAIL" → "fail"(防漏阻断:原 == "fail" 对 FAIL 放行)
let v = parse_review_json(r#"{"verdict":"FAIL","summary":"缺单测"}"#);
assert_eq!(v["verdict"], json!("fail"), "FAIL 应规范化为 fail");
// " fail "(含空白)→ "fail"
let v = parse_review_json(r#"{"verdict":" fail ","summary":"x"}"#);
assert_eq!(v["verdict"], json!("fail"), "含空白 verdict 应 trim 后规范化");
// "PASS" → "pass"
let v = parse_review_json(r#"{"verdict":"PASS"}"#);
assert_eq!(v["verdict"], json!("pass"), "PASS 应规范化为 pass");
// 拼写错/非标准值 → "unknown"(保守不阻断,保人定权)
let v = parse_review_json(r#"{"verdict":"passed"}"#);
assert_eq!(v["verdict"], json!("unknown"), "非 pass/fail 的值应归 unknown");
}
/// P2-加固1 联动:规范化后 verdict 经 gate_should_block 正确阻断 fail。
/// 验证 "FAIL"/"Fail" 经 parse_review_json 规范化 → gate_should_block 阻断(原会漏)。
#[test]
fn parse_review_json_fail_variants_trigger_gate_block() {
for raw_verdict in ["fail", "Fail", "FAIL", " fail ", "FaIl"] {
let v = parse_review_json(&format!(r#"{{"verdict":"{raw_verdict}"}}"#));
let normalized = v["verdict"].as_str().unwrap();
assert_eq!(
normalized, "fail",
"verdict={raw_verdict:?} 应规范化为 fail"
);
assert!(
gate_should_block(true, normalized),
"gate 开 + 规范化后 fail 应阻断(raw={raw_verdict:?})"
);
}
}
/// P2-加固2:dimensions.*.score 越界值 clamp 到 [0,10]。
/// 防 LLM 输出 99/-1/NaN 类越界值污染闸门阈值(score<6 视 fail)与前端展示。
#[test]
fn parse_review_json_clamps_out_of_range_scores() {
let raw = r#"{"verdict":"pass","dimensions":{
"requirement_fit":{"score":99,"issues":[]},
"completeness":{"score":-5,"issues":[]},
"correctness":{"score":7.5,"issues":[]},
"boundary":{"score":10,"issues":[]}
},"summary":"ok"}"#;
let v = parse_review_json(raw);
// 99 → 10(上界)
assert_eq!(v["dimensions"]["requirement_fit"]["score"], json!(10.0), "score 99 应 clamp 到 10");
// -5 → 0(下界)
assert_eq!(v["dimensions"]["completeness"]["score"], json!(0.0), "score -5 应 clamp 到 0");
// 区间内值不变
assert_eq!(v["dimensions"]["correctness"]["score"], json!(7.5), "score 7.5 区间内不变");
assert_eq!(v["dimensions"]["boundary"]["score"], json!(10.0), "score 10 边界值不变");
}
/// P2-加固3:LLM 前置解释文字 + JSON,正则兜底提取首个 { 到末 } 解析成功。
/// 场景:LLM 无视「只输出 JSON」输出 "审查结果:\n{...}\n以上。" → 原整段失败兜底 unknown。
#[test]
fn parse_review_json_extracts_json_from_leading_text() {
let raw = "好的,以下是审查结果:\n{\"verdict\":\"fail\",\"summary\":\"缺边界处理\"}\n以上为审查结论。";
let v = parse_review_json(raw);
assert_eq!(v["verdict"], json!("fail"), "前置文字应被正则兜底剥离,verdict 正确解析");
assert_eq!(v["summary"], json!("缺边界处理"));
// 前置文字 + 代码块围栏混杂(更极端:LLM 既加解释又加 ```json)
let raw = "审查如下:\n```json\n{\"verdict\":\"pass\",\"summary\":\"ok\"}\n```\n完毕。";
let v = parse_review_json(raw);
// 围栏不在开头 → strip_prefix 不命中 → 正则兜底提取 {...}
assert_eq!(v["verdict"], json!("pass"), "前置文字+围栏混杂应正则兜底解析");
}
/// 步骤③:truncate_for_summary 长文截断。
#[test]
fn truncate_for_summary_long_text() {
@@ -605,4 +698,51 @@ mod tests {
);
assert!(!gate_should_block(true, ""), "空 verdict 不应阻断");
}
// ============================================================
// SW-260802-01: schema required 与 handler 行为对齐测试
// ============================================================
//
// 真实 bug:schema `required=["task_id","provider_id"]` 与 handler 不一致 ——
// task_id 缺失 → execute Err(真必填,对齐 schema)
// provider_id 缺失 → execute 走 resolve_provider 路径 3 默认 provider(非必填,schema 误导)
// 修复后 schema `required=["task_id"]`。此处直接断言 schema,防回归。
//
// 不真调 execute(需真 LLM + 默认 provider 完整链),改为断言 schema 这份"契约"本身 +
// resolve_provider 路径 3 行为(已由 ai_node.rs resolve_provider_fallback_default_provider 覆盖),
// 即足以守 schema↔handler 对齐不被无意改回。
/// schema.required 应仅含 task_id(handler 真必填),不含 provider_id(运行时可空走默认)。
#[tokio::test]
async fn schema_required_matches_handler() {
let db = Database::open_in_memory().await.expect("open_in_memory");
let node = AiSelfReviewNode::new(Arc::new(db));
let schema = node.schema();
let params = schema
.params
.as_object()
.expect("schema.params 应是 object");
let required = params
.get("required")
.and_then(|v| v.as_array())
.expect("schema 应有 required 数组");
// task_id 真必填(execute 第 100-104 行缺 task_id → Err)
assert!(
required.iter().any(|v| v == "task_id"),
"task_id 应在 required(handler 真必填)"
);
// provider_id 非必填(resolve_provider 路径 3 空串走默认 provider)
assert!(
!required.iter().any(|v| v == "provider_id"),
"provider_id 不应在 required(运行时留空走默认 provider,schema 不得误导)"
);
// 必填字段集恰好为 {"task_id"}(防后续误加回 provider_id 或漏列 task_id)
assert_eq!(
required.len(),
1,
"required 应仅 1 项(task_id), 实际: {:?}",
required
);
}
}