feat(flywheel): predict_task_risk 风险画像 MCP 工具——预测层落地
- 新增 commands/risk.rs:scope 级(前缀匹配子 scope)+ 文件级 (commit_files JOIN commit_metrics)历史返工率聚合 - classify_risk 分级:返工率 ≥0.35 high、≥0.15 medium、其余 low; 有效返工率取 scope 与文件(样本 ≥3)的较高者 - 可信度防线:样本 <5 或 conventional 率 <0.5(脏历史)→ confidence=low 并附原因 notes,不让脏数据装可信 - CI 自修 commit(is_ci_auto)不计返工,与 L0 口径一致 - MCP 注册 predict_task_risk:agent 开工前按任务卡 scope/files 查询风险,工具清单测试断言驻军 - 7 个单测(in-memory DB + conn 注入),90 全绿 Rules-Applied: R01,R06,R07,R13
This commit is contained in:
parent
0ac809970f
commit
330ddc24dd
@ -167,8 +167,9 @@
|
|||||||
- files: src-tauri/src/commands/commit_metrics.rs, src-tauri/src/commands/buff.rs, src-tauri/src/db.rs
|
- files: src-tauri/src/commands/commit_metrics.rs, src-tauri/src/commands/buff.rs, src-tauri/src/db.rs
|
||||||
- acceptance: "[cargo test] 新表 commit_files(sha,path 联合主键) migration 到位(R05),git log --name-only 解析纯函数单测覆盖多 commit 多文件;[Tauri command] ingest_full_git_history 对炼境自身重跑 → commit_files 行数 >0 且重跑幂等(行数不变)"
|
- acceptance: "[cargo test] 新表 commit_files(sha,path 联合主键) migration 到位(R05),git log --name-only 解析纯函数单测覆盖多 commit 多文件;[Tauri command] ingest_full_git_history 对炼境自身重跑 → commit_files 行数 >0 且重跑幂等(行数不变)"
|
||||||
|
|
||||||
### 📋 P3-C. risk.rs 风险画像 + predict_task_risk MCP 工具
|
### ✅ P3-C. risk.rs 风险画像 + predict_task_risk MCP 工具
|
||||||
- status: todo
|
- status: done
|
||||||
|
- done_note: 2026-07-04 完成。commands/risk.rs:scope 级(前缀匹配子 scope)+ 文件级(commit_files JOIN)返工率聚合,classify_risk 阈值 ≥0.35 high / ≥0.15 medium;样本 <5 或 conventional 率 <0.5(脏历史)→ confidence=low 并附 notes;CI 自修剔除。MCP 工具 schema+dispatch+清单断言。7 个单测(高返工 scope/小样本/脏历史/热点文件/CI 剔除/入参校验/阈值),90 全绿。MCP 实调待新 build 重启后终验
|
||||||
- complexity: M
|
- complexity: M
|
||||||
- depends: flywheel-intelligence(卡 P3-B)
|
- depends: flywheel-intelligence(卡 P3-B)
|
||||||
- files: src-tauri/src/commands/risk.rs, src-tauri/src/commands/mod.rs, src-tauri/src/mcp_tools.rs
|
- files: src-tauri/src/commands/risk.rs, src-tauri/src/commands/mod.rs, src-tauri/src/mcp_tools.rs
|
||||||
|
|||||||
@ -18,6 +18,7 @@ pub mod cicd;
|
|||||||
pub mod blueprint;
|
pub mod blueprint;
|
||||||
pub mod buff;
|
pub mod buff;
|
||||||
pub mod commit_metrics;
|
pub mod commit_metrics;
|
||||||
|
pub mod risk;
|
||||||
pub mod onboarding;
|
pub mod onboarding;
|
||||||
pub mod canvas;
|
pub mod canvas;
|
||||||
pub mod claude_config;
|
pub mod claude_config;
|
||||||
|
|||||||
297
src-tauri/src/commands/risk.rs
Normal file
297
src-tauri/src/commands/risk.rs
Normal file
@ -0,0 +1,297 @@
|
|||||||
|
//! 飞轮阶段三:执行质量风险画像(预测层)。
|
||||||
|
//!
|
||||||
|
//! 从 commit_metrics(scope 级)+ commit_files(文件级)聚合历史返工率,
|
||||||
|
//! 供 agent 开工前通过 MCP 工具 `predict_task_risk` 评估任务风险。
|
||||||
|
//! 全部查询走 conn 注入(R06),聚合与分级逻辑可用 in-memory DB 独立单测。
|
||||||
|
|
||||||
|
use rusqlite::Connection;
|
||||||
|
use serde::Serialize;
|
||||||
|
|
||||||
|
/// scope 级返工画像。
|
||||||
|
#[derive(Debug, Serialize)]
|
||||||
|
pub struct ScopeRisk {
|
||||||
|
pub scope: String,
|
||||||
|
pub commits: u32,
|
||||||
|
pub rework: u32,
|
||||||
|
pub rework_rate: f64,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 单文件返工画像。
|
||||||
|
#[derive(Debug, Serialize)]
|
||||||
|
pub struct FileRisk {
|
||||||
|
pub path: String,
|
||||||
|
pub commits: u32,
|
||||||
|
pub rework: u32,
|
||||||
|
pub rework_rate: f64,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// predict_task_risk 的完整返回。
|
||||||
|
#[derive(Debug, Serialize)]
|
||||||
|
pub struct TaskRiskPrediction {
|
||||||
|
pub project_id: String,
|
||||||
|
pub scope_stats: Option<ScopeRisk>,
|
||||||
|
pub file_stats: Vec<FileRisk>,
|
||||||
|
/// 结论依据的样本量(scope 命中 commit 数;无 scope 时取文件最大命中数)
|
||||||
|
pub sample_size: u32,
|
||||||
|
/// low = 样本 <5 或历史数据脏(conventional 率 <0.5),结论仅供参考
|
||||||
|
pub confidence: String,
|
||||||
|
/// low / medium / high,取 scope 与文件返工率的较高者分级
|
||||||
|
pub risk_level: String,
|
||||||
|
pub notes: Vec<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 返工率分级阈值:≥0.35 high、≥0.15 medium、其余 low。纯函数。
|
||||||
|
pub fn classify_risk(rate: f64) -> &'static str {
|
||||||
|
if rate >= 0.35 {
|
||||||
|
"high"
|
||||||
|
} else if rate >= 0.15 {
|
||||||
|
"medium"
|
||||||
|
} else {
|
||||||
|
"low"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn rate(rework: u32, commits: u32) -> f64 {
|
||||||
|
if commits == 0 {
|
||||||
|
0.0
|
||||||
|
} else {
|
||||||
|
(rework as f64 / commits as f64 * 1000.0).round() / 1000.0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// scope 级聚合:精确匹配或前缀匹配("auth" 命中 "auth" 与 "auth/login")。
|
||||||
|
fn scope_risk(conn: &Connection, project_id: &str, scope: &str) -> Result<Option<ScopeRisk>, String> {
|
||||||
|
let (commits, rework): (u32, u32) = conn
|
||||||
|
.query_row(
|
||||||
|
"SELECT COUNT(*),
|
||||||
|
COALESCE(SUM(CASE WHEN is_rework = 1 AND is_ci_auto = 0 THEN 1 ELSE 0 END), 0)
|
||||||
|
FROM commit_metrics
|
||||||
|
WHERE project_id = ?1 AND (scope = ?2 OR scope LIKE ?2 || '/%')",
|
||||||
|
rusqlite::params![project_id, scope],
|
||||||
|
|r| Ok((r.get(0)?, r.get(1)?)),
|
||||||
|
)
|
||||||
|
.map_err(|e| e.to_string())?;
|
||||||
|
if commits == 0 {
|
||||||
|
return Ok(None);
|
||||||
|
}
|
||||||
|
Ok(Some(ScopeRisk {
|
||||||
|
scope: scope.to_string(),
|
||||||
|
rework_rate: rate(rework, commits),
|
||||||
|
commits,
|
||||||
|
rework,
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 文件级聚合:每个文件在该项目历史中被多少 commit 触碰、其中多少是返工。
|
||||||
|
fn file_risks(conn: &Connection, project_id: &str, files: &[String]) -> Result<Vec<FileRisk>, String> {
|
||||||
|
let mut result = Vec::new();
|
||||||
|
for path in files {
|
||||||
|
let (commits, rework): (u32, u32) = conn
|
||||||
|
.query_row(
|
||||||
|
"SELECT COUNT(*),
|
||||||
|
COALESCE(SUM(CASE WHEN cm.is_rework = 1 AND cm.is_ci_auto = 0 THEN 1 ELSE 0 END), 0)
|
||||||
|
FROM commit_files cf
|
||||||
|
JOIN commit_metrics cm ON cm.sha = cf.sha
|
||||||
|
WHERE cm.project_id = ?1 AND cf.path = ?2",
|
||||||
|
rusqlite::params![project_id, path],
|
||||||
|
|r| Ok((r.get(0)?, r.get(1)?)),
|
||||||
|
)
|
||||||
|
.map_err(|e| e.to_string())?;
|
||||||
|
if commits > 0 {
|
||||||
|
result.push(FileRisk {
|
||||||
|
path: path.clone(),
|
||||||
|
rework_rate: rate(rework, commits),
|
||||||
|
commits,
|
||||||
|
rework,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// 返工次数降序,agent 一眼看到最烫的文件
|
||||||
|
result.sort_by(|a, b| b.rework.cmp(&a.rework));
|
||||||
|
Ok(result)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 核心入口:按 scope 和/或文件清单预测任务风险。
|
||||||
|
pub fn predict_task_risk(
|
||||||
|
conn: &Connection,
|
||||||
|
project_id: &str,
|
||||||
|
scope: Option<&str>,
|
||||||
|
files: &[String],
|
||||||
|
) -> Result<TaskRiskPrediction, String> {
|
||||||
|
if scope.is_none() && files.is_empty() {
|
||||||
|
return Err("scope 与 files 至少提供一个".to_string());
|
||||||
|
}
|
||||||
|
|
||||||
|
let scope_stats = match scope {
|
||||||
|
Some(s) if !s.trim().is_empty() => scope_risk(conn, project_id, s.trim())?,
|
||||||
|
_ => None,
|
||||||
|
};
|
||||||
|
let file_stats = file_risks(conn, project_id, files)?;
|
||||||
|
|
||||||
|
let mut notes = Vec::new();
|
||||||
|
|
||||||
|
// 脏历史检测:conventional 率 <0.5 时结论不可信(数据可信度提示,与 onboarding 卡 D 同源)
|
||||||
|
let (total, conv): (u32, u32) = conn
|
||||||
|
.query_row(
|
||||||
|
"SELECT COUNT(*),
|
||||||
|
COALESCE(SUM(CASE WHEN commit_type IS NOT NULL THEN 1 ELSE 0 END), 0)
|
||||||
|
FROM commit_metrics WHERE project_id = ?1",
|
||||||
|
[project_id],
|
||||||
|
|r| Ok((r.get(0)?, r.get(1)?)),
|
||||||
|
)
|
||||||
|
.map_err(|e| e.to_string())?;
|
||||||
|
let dirty_history = total > 0 && (conv as f64 / total as f64) < 0.5;
|
||||||
|
if dirty_history {
|
||||||
|
notes.push(format!(
|
||||||
|
"历史数据脏:conventional 率 {:.0}%({}/{}),建议种 lefthook 后以新数据为准",
|
||||||
|
conv as f64 / total as f64 * 100.0,
|
||||||
|
conv,
|
||||||
|
total
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
let sample_size = scope_stats
|
||||||
|
.as_ref()
|
||||||
|
.map(|s| s.commits)
|
||||||
|
.unwrap_or_else(|| file_stats.iter().map(|f| f.commits).max().unwrap_or(0));
|
||||||
|
|
||||||
|
// 有效返工率取 scope 与文件中的较高者(文件样本 ≥3 才参与,避免单次 fix 拉爆)
|
||||||
|
let scope_rate = scope_stats.as_ref().map(|s| s.rework_rate).unwrap_or(0.0);
|
||||||
|
let file_rate = file_stats
|
||||||
|
.iter()
|
||||||
|
.filter(|f| f.commits >= 3)
|
||||||
|
.map(|f| f.rework_rate)
|
||||||
|
.fold(0.0_f64, f64::max);
|
||||||
|
let effective_rate = scope_rate.max(file_rate);
|
||||||
|
|
||||||
|
let confidence = if sample_size < 5 || dirty_history {
|
||||||
|
if sample_size < 5 {
|
||||||
|
notes.push(format!("样本量 {sample_size} <5,结论仅供参考"));
|
||||||
|
}
|
||||||
|
"low"
|
||||||
|
} else {
|
||||||
|
"normal"
|
||||||
|
};
|
||||||
|
|
||||||
|
Ok(TaskRiskPrediction {
|
||||||
|
project_id: project_id.to_string(),
|
||||||
|
scope_stats,
|
||||||
|
file_stats,
|
||||||
|
sample_size,
|
||||||
|
confidence: confidence.to_string(),
|
||||||
|
risk_level: classify_risk(effective_rate).to_string(),
|
||||||
|
notes,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
use crate::commands::commit_metrics::{record_commit_entry, GitLogEntry};
|
||||||
|
|
||||||
|
fn seed(conn: &Connection, pid: &str, sha: &str, subject: &str, files: &[&str]) {
|
||||||
|
let entry = GitLogEntry {
|
||||||
|
sha: sha.into(),
|
||||||
|
subject: subject.into(),
|
||||||
|
committed_at: "2026-07-04T00:00:00+09:00".into(),
|
||||||
|
rules_trailer: String::new(),
|
||||||
|
files: files.iter().map(|s| s.to_string()).collect(),
|
||||||
|
};
|
||||||
|
record_commit_entry(conn, pid, &entry).unwrap();
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn classify_thresholds() {
|
||||||
|
assert_eq!(classify_risk(0.4), "high");
|
||||||
|
assert_eq!(classify_risk(0.35), "high");
|
||||||
|
assert_eq!(classify_risk(0.2), "medium");
|
||||||
|
assert_eq!(classify_risk(0.1), "low");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn high_rework_scope_flagged_high() {
|
||||||
|
let conn = crate::db::conn_with_schema();
|
||||||
|
// onboarding scope:6 commits 里 3 条 fix(含一个子 scope),返工率 0.5
|
||||||
|
for (i, subj) in [
|
||||||
|
"feat(onboarding): a",
|
||||||
|
"fix(onboarding): b",
|
||||||
|
"fix(onboarding/pack): c",
|
||||||
|
"feat(onboarding): d",
|
||||||
|
"fix(onboarding): e",
|
||||||
|
"chore(onboarding): f",
|
||||||
|
]
|
||||||
|
.iter()
|
||||||
|
.enumerate()
|
||||||
|
{
|
||||||
|
seed(&conn, "p1", &format!("s{i}"), subj, &[]);
|
||||||
|
}
|
||||||
|
let r = predict_task_risk(&conn, "p1", Some("onboarding"), &[]).unwrap();
|
||||||
|
let s = r.scope_stats.unwrap();
|
||||||
|
assert_eq!(s.commits, 6, "前缀匹配应包含子 scope");
|
||||||
|
assert_eq!(s.rework, 3);
|
||||||
|
assert_eq!(r.risk_level, "high");
|
||||||
|
assert_eq!(r.confidence, "normal");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn small_sample_low_confidence() {
|
||||||
|
let conn = crate::db::conn_with_schema();
|
||||||
|
seed(&conn, "p1", "s1", "fix(auth): x", &[]);
|
||||||
|
seed(&conn, "p1", "s2", "feat(auth): y", &[]);
|
||||||
|
let r = predict_task_risk(&conn, "p1", Some("auth"), &[]).unwrap();
|
||||||
|
assert_eq!(r.confidence, "low");
|
||||||
|
assert!(r.notes.iter().any(|n| n.contains("样本量")), "应有小样本提示");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn dirty_history_noted_and_low_confidence() {
|
||||||
|
let conn = crate::db::conn_with_schema();
|
||||||
|
// 10 条里仅 3 条 conventional → 脏历史
|
||||||
|
for i in 0..7 {
|
||||||
|
seed(&conn, "p1", &format!("junk{i}"), "update stuff", &[]);
|
||||||
|
}
|
||||||
|
seed(&conn, "p1", "c1", "feat(api): a", &[]);
|
||||||
|
seed(&conn, "p1", "c2", "feat(api): b", &[]);
|
||||||
|
seed(&conn, "p1", "c3", "fix(api): c", &[]);
|
||||||
|
// api scope 样本 5 条以下也行,这里重点断言脏历史标记
|
||||||
|
let r = predict_task_risk(&conn, "p1", Some("api"), &[]).unwrap();
|
||||||
|
assert_eq!(r.confidence, "low");
|
||||||
|
assert!(r.notes.iter().any(|n| n.contains("历史数据脏")));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn file_level_rework_surfaces_hot_file() {
|
||||||
|
let conn = crate::db::conn_with_schema();
|
||||||
|
// hot.rs 被 4 个 commit 触碰,其中 2 个 fix;cold.rs 只有 feat
|
||||||
|
seed(&conn, "p1", "s1", "feat(a): x", &["src/hot.rs", "src/cold.rs"]);
|
||||||
|
seed(&conn, "p1", "s2", "fix(a): y", &["src/hot.rs"]);
|
||||||
|
seed(&conn, "p1", "s3", "fix(a): z", &["src/hot.rs"]);
|
||||||
|
seed(&conn, "p1", "s4", "feat(a): w", &["src/hot.rs"]);
|
||||||
|
let r = predict_task_risk(
|
||||||
|
&conn,
|
||||||
|
"p1",
|
||||||
|
None,
|
||||||
|
&["src/hot.rs".into(), "src/cold.rs".into(), "src/ghost.rs".into()],
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
assert_eq!(r.file_stats.len(), 2, "无历史的文件不进结果");
|
||||||
|
assert_eq!(r.file_stats[0].path, "src/hot.rs", "返工多的文件排前");
|
||||||
|
assert_eq!(r.file_stats[0].rework, 2);
|
||||||
|
assert_eq!(r.risk_level, "high", "hot.rs 返工率 0.5 应分级 high");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn requires_scope_or_files() {
|
||||||
|
let conn = crate::db::conn_with_schema();
|
||||||
|
assert!(predict_task_risk(&conn, "p1", None, &[]).is_err());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn ci_auto_fix_excluded_from_rework() {
|
||||||
|
let conn = crate::db::conn_with_schema();
|
||||||
|
seed(&conn, "p1", "s1", "fix(ci): auto-fix [DeepSeek-V3]", &[]);
|
||||||
|
seed(&conn, "p1", "s2", "feat(ci): x", &[]);
|
||||||
|
let r = predict_task_risk(&conn, "p1", Some("ci"), &[]).unwrap();
|
||||||
|
assert_eq!(r.scope_stats.unwrap().rework, 0, "CI 自修不计返工");
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -245,6 +245,19 @@ pub fn tools_list_result() -> Value {
|
|||||||
"name": "scan_unregistered_projects",
|
"name": "scan_unregistered_projects",
|
||||||
"description": "扫描已登记项目的父目录,找出存在于文件系统但尚未在炼境注册的项目目录。返回每个未注册目录的路径、是否有 .git、技术栈特征文件(package.json / Cargo.toml / go.mod)、是否有 AGENTS.md。用于日常发现漏网项目并决定是否接入炼境。",
|
"description": "扫描已登记项目的父目录,找出存在于文件系统但尚未在炼境注册的项目目录。返回每个未注册目录的路径、是否有 .git、技术栈特征文件(package.json / Cargo.toml / go.mod)、是否有 AGENTS.md。用于日常发现漏网项目并决定是否接入炼境。",
|
||||||
"inputSchema": { "type": "object", "properties": {}, "required": [] }
|
"inputSchema": { "type": "object", "properties": {}, "required": [] }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "predict_task_risk",
|
||||||
|
"description": "任务风险预测(飞轮阶段三):按 scope 和/或文件清单查询该项目历史返工率,返回 risk_level(low/medium/high)、样本量与置信度、热点文件排行。agent 开工前调用——任务卡涉及高返工 scope/文件时提高警惕(多写测试、小步提交)。样本 <5 或 commit 历史不规范时 confidence=low,结论仅供参考。",
|
||||||
|
"inputSchema": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"project_id": { "type": "string", "description": "项目 workspace ID(来自 list_group_projects 的返回结果)" },
|
||||||
|
"scope": { "type": "string", "description": "conventional commit scope(如 onboarding、auth/login),前缀匹配子 scope" },
|
||||||
|
"files": { "type": "array", "items": { "type": "string" }, "description": "任务卡涉及的文件相对路径清单(与 git log --name-only 路径格式一致)" }
|
||||||
|
},
|
||||||
|
"required": ["project_id"]
|
||||||
|
}
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
})
|
})
|
||||||
@ -462,6 +475,22 @@ pub async fn tools_call(group_id: &str, params: Option<&Value>) -> Value {
|
|||||||
}
|
}
|
||||||
"list_all_projects" => list_all_projects(),
|
"list_all_projects" => list_all_projects(),
|
||||||
"scan_unregistered_projects" => scan_unregistered_projects(),
|
"scan_unregistered_projects" => scan_unregistered_projects(),
|
||||||
|
"predict_task_risk" => {
|
||||||
|
let pid = args.get("project_id").and_then(|v| v.as_str()).unwrap_or("");
|
||||||
|
let scope = args.get("scope").and_then(|v| v.as_str());
|
||||||
|
let files: Vec<String> = args
|
||||||
|
.get("files")
|
||||||
|
.and_then(|v| v.as_array())
|
||||||
|
.map(|arr| {
|
||||||
|
arr.iter()
|
||||||
|
.filter_map(|f| f.as_str().map(str::to_string))
|
||||||
|
.collect()
|
||||||
|
})
|
||||||
|
.unwrap_or_default();
|
||||||
|
let conn = db::pool().get().map_err(|e| e.to_string())?;
|
||||||
|
crate::commands::risk::predict_task_risk(&conn, pid, scope, &files)
|
||||||
|
.and_then(|r| serde_json::to_string_pretty(&r).map_err(|e| e.to_string()))
|
||||||
|
}
|
||||||
"apply_onboarding_pack" => {
|
"apply_onboarding_pack" => {
|
||||||
let pid = args.get("project_id").and_then(|v| v.as_str()).unwrap_or("");
|
let pid = args.get("project_id").and_then(|v| v.as_str()).unwrap_or("");
|
||||||
resolve_project_root(&gid, pid).and_then(|root| {
|
resolve_project_root(&gid, pid).and_then(|root| {
|
||||||
@ -1039,6 +1068,7 @@ mod tests {
|
|||||||
assert!(names.contains(&"inject_mcp"), "缺少 inject_mcp");
|
assert!(names.contains(&"inject_mcp"), "缺少 inject_mcp");
|
||||||
assert!(names.contains(&"create_pull_request"), "缺少 create_pull_request");
|
assert!(names.contains(&"create_pull_request"), "缺少 create_pull_request");
|
||||||
assert!(names.contains(&"poll_now"), "缺少 poll_now");
|
assert!(names.contains(&"poll_now"), "缺少 poll_now");
|
||||||
|
assert!(names.contains(&"predict_task_risk"), "缺少 predict_task_risk");
|
||||||
// 每个工具都有 description 和 inputSchema
|
// 每个工具都有 description 和 inputSchema
|
||||||
for tool in tools {
|
for tool in tools {
|
||||||
let name = tool["name"].as_str().unwrap_or("?");
|
let name = tool["name"].as_str().unwrap_or("?");
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user