重构: 后端 df-ai/commands 拆分+df-nodes/workflow 改造+P0 bug 修复
- df-ai: context 历史中毒三档自愈 sanitize_messages(AC3)+anthropic_compat tool_use_id None 跳过(AC1/AC2)+删 router/stream 死码
- df-core: events 加 select_type+decisions 多选审批契约(F-260615-01)
- df-execute: shell run_command 工具复用(F-260615-05)
- df-nodes: human_node 多选校验+2 端到端测(F-01)+取消跳 set_failed(B-03b-R1/R2/R8)
- df-workflow: executor/dag/state cancel 闭环(B-06/07/03a/b)+provider approve options(R-PD-5)
- df-storage: find_path_conflict 抽公共(R-PD-11)+COLS 常量断言
- df-ideas: 删 IdeaPromoter/PromotionPolicy 死码(R-PD-14)
- src-tauri/commands/ai: secret keyring 迁移(FR-S1/R-PD-4)+GeneratingGuard RAII+disarm(B-09/26)+newConversation 软复位(B-10)+stream 心跳/stop select/空回复判错(B-02/04/05/15)+run_command(F-05)+mask audit(AR-3)
- src-tauri/commands/{project,task,workflow,mod,lib,state}: task detail IPC(F-02)+approve decisions+task list 联动(B-29)
- Cargo.lock+Cargo.toml 依赖同步
This commit is contained in:
@@ -4,12 +4,31 @@ use serde::{Deserialize, Serialize};
|
||||
|
||||
use crate::types::NodeId;
|
||||
|
||||
/// 人工审批选择类型(F-260615-01)
|
||||
/// - Single: 单选(decision 单值),缺省值,向后兼容现有调用方
|
||||
/// - Multiple: 多选(decisions 数组)
|
||||
///
|
||||
/// serde rename_all="snake_case" → "single"/"multiple"
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum SelectType {
|
||||
#[default]
|
||||
Single,
|
||||
Multiple,
|
||||
}
|
||||
|
||||
/// 人工审批响应
|
||||
///
|
||||
/// 兼容字段(decision 单值):仅 select_type=Single 时使用;
|
||||
/// 新字段 decisions 数组:Single 时长度=1,Multiple 时长度≥1。
|
||||
/// 旧调用方仍可只填 decision,HumanNode 下游兼容两者。
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct HumanApprovalResponse {
|
||||
pub execution_id: String,
|
||||
pub node_id: NodeId,
|
||||
pub decision: String,
|
||||
#[serde(default)]
|
||||
pub decisions: Vec<String>,
|
||||
pub comment: Option<String>,
|
||||
}
|
||||
|
||||
@@ -63,12 +82,18 @@ pub enum WorkflowEvent {
|
||||
title: String,
|
||||
description: String,
|
||||
options: Vec<String>,
|
||||
/// F-260615-01: 选择类型,缺省 Single(向后兼容)
|
||||
#[serde(default)]
|
||||
select_type: SelectType,
|
||||
},
|
||||
/// 人工审批响应
|
||||
HumanApprovalResponse {
|
||||
execution_id: String,
|
||||
node_id: NodeId,
|
||||
decision: String,
|
||||
/// F-260615-01: 多选结果(Single 模式长度=1,Multiple 模式长度≥1)
|
||||
#[serde(default)]
|
||||
decisions: Vec<String>,
|
||||
comment: Option<String>,
|
||||
},
|
||||
}
|
||||
|
||||
@@ -3,3 +3,6 @@
|
||||
pub mod error;
|
||||
pub mod events;
|
||||
pub mod types;
|
||||
|
||||
// 顶层 re-export:常用时间工具(跨 crate 调用方用 df_core::now_millis,避免 types:: 前缀)
|
||||
pub use types::now_millis;
|
||||
|
||||
@@ -27,6 +27,21 @@ pub type ExecutionId = String;
|
||||
/// 决策 ID
|
||||
pub type DecisionId = String;
|
||||
|
||||
// ============================================================
|
||||
// 时间工具
|
||||
// ============================================================
|
||||
|
||||
/// 当前 Unix 时间戳(毫秒)
|
||||
///
|
||||
/// 统一时间获取入口,避免 commands 层与 df-storage 层各写一份 SystemTime 调用(DRY)。
|
||||
/// 需要字符串形式的调用方自行 `.to_string()`。
|
||||
pub fn now_millis() -> i64 {
|
||||
std::time::SystemTime::now()
|
||||
.duration_since(std::time::UNIX_EPOCH)
|
||||
.unwrap_or_default()
|
||||
.as_millis() as i64
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// 状态枚举
|
||||
// ============================================================
|
||||
@@ -143,6 +158,29 @@ impl TaskStatus {
|
||||
TaskStatus::Cancelled => "cancelled",
|
||||
}
|
||||
}
|
||||
|
||||
/// 校验字符串是否为合法的 TaskStatus 存储值(小写 snake_case)。
|
||||
/// 用于落库前拦截拼写错误(如 "in-progess"、"in progress"、大小写错等),
|
||||
/// 防止任意字符串静默落库导致按枚举查询全漏、任务"消失"。
|
||||
pub fn is_valid(s: &str) -> bool {
|
||||
matches!(
|
||||
s,
|
||||
"todo" | "in_progress" | "in_review" | "testing" | "done" | "blocked" | "cancelled"
|
||||
)
|
||||
}
|
||||
|
||||
/// 合法 status 值列表,用于错误提示。
|
||||
pub fn valid_values() -> &'static [&'static str] {
|
||||
&[
|
||||
"todo",
|
||||
"in_progress",
|
||||
"in_review",
|
||||
"testing",
|
||||
"done",
|
||||
"blocked",
|
||||
"cancelled",
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for TaskStatus {
|
||||
@@ -299,3 +337,96 @@ impl Default for Priority {
|
||||
pub fn new_id() -> String {
|
||||
uuid::Uuid::new_v4().to_string()
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::TaskStatus;
|
||||
|
||||
// 合法值:覆盖全部枚举变体(与 as_str 一致)
|
||||
#[test]
|
||||
fn is_valid_accepts_all_variants() {
|
||||
assert!(TaskStatus::is_valid("todo"));
|
||||
assert!(TaskStatus::is_valid("in_progress"));
|
||||
assert!(TaskStatus::is_valid("in_review"));
|
||||
assert!(TaskStatus::is_valid("testing"));
|
||||
assert!(TaskStatus::is_valid("done"));
|
||||
assert!(TaskStatus::is_valid("blocked"));
|
||||
assert!(TaskStatus::is_valid("cancelled"));
|
||||
}
|
||||
|
||||
// as_str 与 is_valid 必须自洽:每个变体的存储值都应被 is_valid 接受
|
||||
#[test]
|
||||
fn is_valid_consistent_with_as_str() {
|
||||
let all = [
|
||||
TaskStatus::Todo,
|
||||
TaskStatus::InProgress,
|
||||
TaskStatus::InReview,
|
||||
TaskStatus::Testing,
|
||||
TaskStatus::Done,
|
||||
TaskStatus::Blocked,
|
||||
TaskStatus::Cancelled,
|
||||
];
|
||||
for v in all {
|
||||
assert!(
|
||||
TaskStatus::is_valid(v.as_str()),
|
||||
"as_str({:?}) = {:?} 未被 is_valid 接受",
|
||||
v,
|
||||
v.as_str()
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// 拼写错误(典型回归场景)
|
||||
#[test]
|
||||
fn is_valid_rejects_typos() {
|
||||
assert!(!TaskStatus::is_valid("in-progess")); // 漏 r + 连字符
|
||||
assert!(!TaskStatus::is_valid("in-progress")); // 连字符非下划线
|
||||
assert!(!TaskStatus::is_valid("in progress")); // 空格
|
||||
assert!(!TaskStatus::is_valid("inprogres")); // 漏 s
|
||||
assert!(!TaskStatus::is_valid("tod")); // 截断
|
||||
assert!(!TaskStatus::is_valid("don")); // 截断
|
||||
assert!(!TaskStatus::is_valid("canceled")); // 美式拼写(单 l)
|
||||
assert!(!TaskStatus::is_valid("complete")); // 近义词
|
||||
assert!(!TaskStatus::is_valid("pending")); // 近义词
|
||||
}
|
||||
|
||||
// 大小写敏感
|
||||
#[test]
|
||||
fn is_valid_case_sensitive() {
|
||||
assert!(!TaskStatus::is_valid("Todo"));
|
||||
assert!(!TaskStatus::is_valid("TODO"));
|
||||
assert!(!TaskStatus::is_valid("InProgress"));
|
||||
assert!(!TaskStatus::is_valid("IN_PROGRESS"));
|
||||
assert!(!TaskStatus::is_valid("Done"));
|
||||
}
|
||||
|
||||
// 边界:空串、空白、前后空格、带前后缀
|
||||
#[test]
|
||||
fn is_valid_rejects_empty_and_whitespace() {
|
||||
assert!(!TaskStatus::is_valid(""));
|
||||
assert!(!TaskStatus::is_valid(" "));
|
||||
assert!(!TaskStatus::is_valid(" todo")); // 前导空格
|
||||
assert!(!TaskStatus::is_valid("todo ")); // 尾随空格
|
||||
assert!(!TaskStatus::is_valid(" todo ")); // 两端空格
|
||||
assert!(!TaskStatus::is_valid("todo\n")); // 换行
|
||||
}
|
||||
|
||||
// 边界:非 ASCII / 注入尝试
|
||||
#[test]
|
||||
fn is_valid_rejects_garbage() {
|
||||
assert!(!TaskStatus::is_valid("'; DROP TABLE tasks;--"));
|
||||
assert!(!TaskStatus::is_valid("待开始")); // 中文近义
|
||||
assert!(!TaskStatus::is_valid("todo;done"));
|
||||
assert!(!TaskStatus::is_valid("todo,done"));
|
||||
}
|
||||
|
||||
// valid_values 必须与 is_valid 自洽(防清单漏项)
|
||||
#[test]
|
||||
fn valid_values_all_pass_is_valid() {
|
||||
for &v in TaskStatus::valid_values() {
|
||||
assert!(TaskStatus::is_valid(v), "valid_values 含 {:?} 但 is_valid 拒绝", v);
|
||||
}
|
||||
// 数量应等于枚举变体数
|
||||
assert_eq!(TaskStatus::valid_values().len(), 7);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user