修复: MCP list_tasks 默认排除终态 + read_skill 隔离围栏
- list_tasks 无 status 过滤时默认排除 done/cancelled, 终态存量(260+条)挤占分页窗口致活跃任务被挤出视图; 显式传 status 不干预 - df-storage TaskQuery 新增 status_not_in 字段 + SQL NOT IN 下推(占位符先拼后绑, 防序号/值错位) - read_skill 返回内容外包 skill_documentation XML 围栏+隔离声明, 防技能文档内容与系统指令混淆 - df-mcp 64 passed(含新增终态排除测试)
This commit is contained in:
@@ -547,9 +547,18 @@ fn list_tasks(ctx: &Ctx, args: Value) -> BoxFuture<'static, CallToolResult> {
|
||||
Box::pin(async move {
|
||||
let repo = TaskRepo::new(&db);
|
||||
// 分页:取 limit+1 条探测是否有下一页(has_more),再截断到 limit。
|
||||
// 缺陷修复(1eff911c,登记于 2026-08-08):无 status 过滤时默认排除 done/cancelled——
|
||||
// 终态任务是存量大头(本库 260+ 条),不过滤会挤占分页窗口,活跃任务被
|
||||
// created_at DESC + limit 截断挤出视图(表象=「过滤失效/他项目任务混入」)。
|
||||
// 显式传 status(含 done)不干预,保留查终态能力。
|
||||
let query = TaskQuery {
|
||||
project_id: project_id_filter,
|
||||
status: status_filter,
|
||||
status: status_filter.clone(),
|
||||
status_not_in: if status_filter.is_some() {
|
||||
None
|
||||
} else {
|
||||
Some(vec!["done".to_string(), "cancelled".to_string()])
|
||||
},
|
||||
limit: Some(limit + 1),
|
||||
offset: Some(offset),
|
||||
..Default::default()
|
||||
@@ -1740,6 +1749,43 @@ mod tests {
|
||||
assert_eq!(v["has_more"], false, "3 条 < 100,无下一页");
|
||||
}
|
||||
|
||||
/// 1eff911c 缺陷修复:无 status 过滤时默认排除 done/cancelled 终态任务;
|
||||
/// 显式传 status=done 仍可查终态。
|
||||
#[tokio::test]
|
||||
async fn list_tasks_excludes_terminal_status_by_default() {
|
||||
let ctx = test_ctx().await;
|
||||
let pid = seed_project(&ctx, "终态过滤").await;
|
||||
let done_id = seed_task(&ctx, &pid, "已完成任务").await;
|
||||
seed_task(&ctx, &pid, "进行中任务").await;
|
||||
// 把第一条推进到 done
|
||||
let repo = TaskRepo::new(&ctx.db);
|
||||
repo.advance_status_atomic(&done_id, "todo", "done", false).await.unwrap();
|
||||
|
||||
// 无 status 过滤:done 应被排除,只返回活跃任务
|
||||
let r = list_tasks(&ctx, json!({ "project_id": pid })).await;
|
||||
assert!(r.is_error.is_none(), "{:?}", text_of(&r));
|
||||
let v = json_of(&r);
|
||||
let titles: Vec<&str> = v["tasks"]
|
||||
.as_array()
|
||||
.unwrap()
|
||||
.iter()
|
||||
.map(|t| t["title"].as_str().unwrap())
|
||||
.collect();
|
||||
assert!(
|
||||
!titles.contains(&"已完成任务"),
|
||||
"done 任务不应出现在默认结果: {titles:?}"
|
||||
);
|
||||
assert!(titles.contains(&"进行中任务"), "活跃任务应保留: {titles:?}");
|
||||
|
||||
// 显式 status=done:仍可查终态(能力不回退)
|
||||
let r = list_tasks(&ctx, json!({ "project_id": pid, "status": "done" })).await;
|
||||
assert!(r.is_error.is_none(), "{:?}", text_of(&r));
|
||||
let v = json_of(&r);
|
||||
let arr = v["tasks"].as_array().unwrap();
|
||||
assert_eq!(arr.len(), 1, "显式查 done 应命中 1 条");
|
||||
assert_eq!(arr[0]["title"], "已完成任务");
|
||||
}
|
||||
|
||||
/// list_projects 分页:offset 生效 + has_more 标记。
|
||||
#[tokio::test]
|
||||
async fn list_projects_pagination_offset_and_has_more() {
|
||||
|
||||
@@ -100,6 +100,10 @@ pub struct TaskQuery {
|
||||
pub project_id: Option<String>,
|
||||
/// 状态过滤(P1 下沉,命中 idx_tasks_status)。值集由上层 TaskStatus::is_valid 兜底校验。
|
||||
pub status: Option<String>,
|
||||
/// 状态排除集(1eff911c 缺陷修复,MCP list_tasks 无 status 过滤时默认排除
|
||||
/// done/cancelled 终态,防存量终态任务挤占分页窗口)。与 status 互斥使用:
|
||||
/// 同传时 status 等值优先,status_not_in 忽略(SQL 拼接顺序 status 先判)。
|
||||
pub status_not_in: Option<Vec<String>>,
|
||||
/// 优先级过滤(0=critical..3=low)。P3 基建就绪,视图暂不用。
|
||||
pub priority: Option<i32>,
|
||||
/// 负责人过滤。P3 基建就绪,视图暂不用。
|
||||
@@ -390,6 +394,7 @@ impl TaskRepo {
|
||||
// 拷贝 query 字段进闭包('static 生命周期,spawn_blocking 要求)
|
||||
let project_id = query.project_id.clone();
|
||||
let status = query.status.clone();
|
||||
let status_not_in = query.status_not_in.clone();
|
||||
let priority = query.priority;
|
||||
let assignee = query.assignee.clone();
|
||||
let keyword = query.keyword.clone();
|
||||
@@ -417,6 +422,22 @@ impl TaskRepo {
|
||||
where_clauses.push(format!("status = ?{}", params_vec.len() + 1));
|
||||
params_vec.push(Box::new(s.clone()));
|
||||
}
|
||||
// status_not_in(1eff911c 修复):状态排除集,先拼占位符再统一绑值(两步分离,
|
||||
// 避免边拼边绑的序号/值错位)。与 status 同传时 status 等值优先(互斥由调用方保证)。
|
||||
if status.is_none() {
|
||||
if let Some(excl) = &status_not_in {
|
||||
if !excl.is_empty() {
|
||||
let base = params_vec.len();
|
||||
let placeholders: Vec<String> = (0..excl.len())
|
||||
.map(|i| format!("?{}", base + i + 1))
|
||||
.collect();
|
||||
for v in excl {
|
||||
params_vec.push(Box::new(v.clone()));
|
||||
}
|
||||
where_clauses.push(format!("status NOT IN ({})", placeholders.join(", ")));
|
||||
}
|
||||
}
|
||||
}
|
||||
if let Some(p) = priority {
|
||||
where_clauses.push(format!("priority = ?{}", params_vec.len() + 1));
|
||||
params_vec.push(Box::new(p));
|
||||
|
||||
Reference in New Issue
Block a user