优化: 清理无意义注释(走查编号前缀/过时历史标注/死代码注释)
This commit is contained in:
@@ -80,7 +80,7 @@ impl AnthropicCompatProvider {
|
||||
/// - `api_key`: API 密钥(Anthropic 用 x-api-key 头,非 Bearer)
|
||||
/// - `default_model`: 默认模型名称
|
||||
pub fn new(base_url: impl Into<String>, api_key: impl Into<String>, default_model: impl Into<String>) -> Self {
|
||||
// SW-260618-10: reqwest Client 构建抽 crate::build_provider_client(与 OpenAI 共用 DRY)。
|
||||
// reqwest Client 构建抽 crate::build_provider_client(与 OpenAI 共用 DRY)。
|
||||
let client = crate::build_provider_client();
|
||||
Self {
|
||||
client,
|
||||
@@ -890,7 +890,7 @@ mod tests {
|
||||
assert!(!c.finished);
|
||||
}
|
||||
|
||||
/// error 事件 → error=Some + finished=false(R-P1-1:避免残缺响应被当正常完成入库,
|
||||
/// error 事件 → error=Some + finished=false避免残缺响应被当正常完成入库,
|
||||
/// 由 stream_llm 识别 error 非空发 AiError + 丢弃残缺,与 OpenAI 路径 Err 一致)
|
||||
#[test]
|
||||
fn anthropic_error_event_yields_error_not_finished() {
|
||||
|
||||
@@ -61,12 +61,12 @@ pub(crate) struct AnthropicToolDef {
|
||||
/// Anthropic 同步响应
|
||||
#[derive(Debug, Deserialize)]
|
||||
pub(crate) struct AnthropicResponse {
|
||||
// SW-260618-25: Anthropic 响应反序列化字段,保留以对齐响应结构(响应 id),标注意图消除 dead_code warning
|
||||
// Anthropic 响应反序列化字段,保留以对齐响应结构(响应 id),标注意图消除 dead_code warning
|
||||
#[allow(dead_code)]
|
||||
id: String,
|
||||
pub model: String,
|
||||
pub content: Vec<AnthropicContentBlock>,
|
||||
// SW-260618-25: Anthropic 响应反序列化字段,保留以备调试/未来消费(如日志记录调用终止原因),标注意图消除 dead_code warning
|
||||
// Anthropic 响应反序列化字段,保留以备调试/未来消费(如日志记录调用终止原因),标注意图消除 dead_code warning
|
||||
#[allow(dead_code)]
|
||||
stop_reason: Option<String>,
|
||||
pub usage: AnthropicUsage,
|
||||
|
||||
@@ -275,7 +275,7 @@ impl ContextManager {
|
||||
self.messages.iter().map(|t| t.message.clone()).collect()
|
||||
}
|
||||
|
||||
/// 只读最近 N 条消息(尾部切片 clone)。BE2(AC-EFF-R2-1):断路器/探索熔断检查等只关心
|
||||
/// 只读最近 N 条消息(尾部切片 clone)。BE2 断路器/探索熔断检查等只关心
|
||||
/// 最近消息的读路径,用本方法避免 all_messages_clone 每轮全量 clone(长对话每轮 O(n) 深克隆浪费)。
|
||||
/// 尾部顺序保持时间正序(与 all_messages_clone 一致,调用方从尾部反向扫即可)。
|
||||
pub fn recent_messages(&self, n: usize) -> Vec<ChatMessage> {
|
||||
@@ -326,7 +326,7 @@ impl ContextManager {
|
||||
|
||||
/// 弹出末尾连续的 assistant 消息(含其 tool_calls 三元组尾随 tool_result)
|
||||
///
|
||||
/// 用于「重新生成」(UX-02):删掉最后一条 AI 回复(可能跨多轮 tool_calls + tool_results
|
||||
/// 用于「重新生成」删掉最后一条 AI 回复(可能跨多轮 tool_calls + tool_results
|
||||
/// 紧随其后),保留触发它的 user 消息,以便重跑 agentic loop 再生成。
|
||||
///
|
||||
/// 语义:从末尾向前弹出,直到弹出至少一条 assistant 消息;若弹出 assistant 后紧邻的更早
|
||||
|
||||
@@ -34,7 +34,7 @@ use crate::provider::{ChatMessage, MessageRole, ToolCall};
|
||||
pub fn sanitize_messages(messages: Vec<ChatMessage>) -> Vec<ChatMessage> {
|
||||
use std::collections::HashSet;
|
||||
|
||||
// step 0(UX-09):过滤 truncated 软删消息,不进 LLM 上下文。
|
||||
// step 0过滤 truncated 软删消息,不进 LLM 上下文。
|
||||
// 编辑某条 user 消息后其后续消息标 truncated(保留 DB 可追溯),发送视图必须剔除,
|
||||
// 否则被编辑前的旧回复仍进入 LLM 历史,污染重生成语义。落库全量保留不受影响。
|
||||
let messages: Vec<ChatMessage> = messages
|
||||
|
||||
@@ -1135,7 +1135,7 @@ mod tests {
|
||||
#[test]
|
||||
fn extract_key_info_single_huge_line_no_newline_compressed() {
|
||||
// 单行超大内容(50KB)原本逃逸压缩,现按字符数截断保留头尾。
|
||||
// 注:read_file 已豁免压缩(BUG-260801),此处用 run_command 验证通用压缩路径。
|
||||
// 注:read_file 已豁免压缩,此处用 run_command 验证通用压缩路径。
|
||||
let content = "x".repeat(50_000);
|
||||
let result = extract_key_info(&content, "run_command");
|
||||
assert!(result.len() < content.len(), "单行超长应压缩: {} >= {}", result.len(), content.len());
|
||||
@@ -1149,7 +1149,7 @@ mod tests {
|
||||
fn extract_key_info_json_huge_string_field_truncated() {
|
||||
// JSON 对象中大字符串字段(单行少行)逃逸压缩。
|
||||
// 如 `{"path":"src/main.rs","content":"单行超大文本..."}`。
|
||||
// 注:read_file 已豁免压缩(BUG-260801),此处用 run_command 验证通用压缩路径。
|
||||
// 注:read_file 已豁免压缩,此处用 run_command 验证通用压缩路径。
|
||||
let large = "z".repeat(10_000);
|
||||
let content = format!("{{\"path\":\"src/main.rs\",\"content\":\"{}\"}}", large);
|
||||
let result = extract_key_info(&content, "run_command");
|
||||
@@ -1171,7 +1171,7 @@ mod tests {
|
||||
#[test]
|
||||
fn extract_key_info_eleven_lines_triggers_compression() {
|
||||
// 边界:行数 == 11(刚超 kept_boundary=10)→ 触发压缩,含标记
|
||||
// 注:read_file 已豁免压缩(BUG-260801),此处用 run_command 验证通用压缩路径。
|
||||
// 注:read_file 已豁免压缩,此处用 run_command 验证通用压缩路径。
|
||||
let lines: Vec<String> = (1..=11).map(|i| format!("line {}", i)).collect();
|
||||
let content = lines.join("\n");
|
||||
let result = extract_key_info(&content, "run_command");
|
||||
@@ -1185,7 +1185,7 @@ mod tests {
|
||||
// 边界:错误行恰在头部区间内(idx < head_end)→ 不重复插入(头部已含)
|
||||
// 错误行在尾部区间内(idx >= tail_start)→ 不重复插入(尾部已含)
|
||||
// 错误行在中间区间 → 标注 [行 N] 插入
|
||||
// 注:read_file 已豁免压缩(BUG-260801),此处用 run_command 验证通用压缩路径。
|
||||
// 注:read_file 已豁免压缩,此处用 run_command 验证通用压缩路径。
|
||||
let mut lines: Vec<String> = (1..=20).map(|i| format!("norm {}", i)).collect();
|
||||
// idx=2(头部区间 [0,5))错误行 → 头部已含,不在 error_lines(扫描跳过 head/tail)
|
||||
lines[2] = "error in head zone".to_string();
|
||||
@@ -1347,7 +1347,7 @@ mod tests {
|
||||
assert_eq!(extract_pending_tc_id(""), None);
|
||||
}
|
||||
|
||||
// ── read_file 豁免压缩(BUG-260801: AI 定向读代码不应被折叠) ──
|
||||
// ── read_file 豁免压缩AI 定向读代码不应被折叠) ──
|
||||
//
|
||||
// 根因:extract_key_info 的 JSON 分支对 content 字段(文件内容)行数 > 10 即折叠中间为
|
||||
// "(压缩中间内容)"。read_file limit=100 读 28KB(100 行)→ 触发 → AI 只拿到首尾各 5 行,
|
||||
|
||||
@@ -56,7 +56,7 @@ use reqwest::Client;
|
||||
// df-ai-core 直接暴露,供需要直接引用 trait crate 的下游(可选)。
|
||||
pub use df_ai_core;
|
||||
|
||||
/// SW-260618-10: 构建 Provider 共用的 reqwest Client(OpenAI/Anthropic Provider::new DRY)。
|
||||
/// 构建 Provider 共用的 reqwest Client(OpenAI/Anthropic Provider::new DRY)。
|
||||
///
|
||||
/// connect_timeout 30s 防连接阶段静默 hang(网络静默断);不设总 timeout——reqwest 的
|
||||
/// `.timeout()` 会限制整个响应 body 时长,流式长生成任务会被误砍。读取阶段中途静默由上层
|
||||
|
||||
@@ -239,7 +239,7 @@ mod tests {
|
||||
// SenseNova U1 系列是图像生成模型(走 /v1/images/generations,非 chat completions),
|
||||
// /v1/models 会返回但不应留为 chat 模型,否则用户选它对话会失败。
|
||||
assert!(is_non_chat_model("sensenova-u1-fast"));
|
||||
assert!(is_non_chat_model("SenseNova-U1-Fast")); // 大小写无关
|
||||
assert!(is_non_chat_model("SenseNova-ast")); // 大小写无关
|
||||
assert!(is_non_chat_model("sensenova-u1-pro"));
|
||||
// 通用图像生成命名也剔
|
||||
assert!(is_non_chat_model("infographic-v1"));
|
||||
|
||||
@@ -47,7 +47,7 @@ impl OpenAICompatProvider {
|
||||
/// - `api_key`: API 密钥
|
||||
/// - `default_model`: 默认模型名称
|
||||
pub fn new(base_url: impl Into<String>, api_key: impl Into<String>, default_model: impl Into<String>) -> Self {
|
||||
// SW-260618-10: reqwest Client 构建抽 crate::build_provider_client(与 Anthropic 共用 DRY)。
|
||||
// reqwest Client 构建抽 crate::build_provider_client(与 Anthropic 共用 DRY)。
|
||||
// connect_timeout/回退策略集中此处,未来改一处即可(见 lib.rs::build_provider_client)。
|
||||
let client = crate::build_provider_client();
|
||||
Self {
|
||||
|
||||
@@ -67,7 +67,7 @@ pub(crate) struct OpenAiResponse {
|
||||
#[derive(Debug, Deserialize)]
|
||||
pub(crate) struct OpenAiChoice {
|
||||
pub message: OpenAiMessageResp,
|
||||
// SW-260618-24: OpenAI 响应反序列化字段,保留以备调试/未来消费(如日志记录调用终止原因),标注意图消除 dead_code warning
|
||||
// OpenAI 响应反序列化字段,保留以备调试/未来消费(如日志记录调用终止原因),标注意图消除 dead_code warning
|
||||
#[allow(dead_code)]
|
||||
pub finish_reason: Option<String>,
|
||||
}
|
||||
@@ -84,7 +84,7 @@ pub(crate) struct OpenAiMessageResp {
|
||||
#[derive(Debug, Deserialize)]
|
||||
pub(crate) struct OpenAiToolCallResp {
|
||||
pub id: String,
|
||||
// SW-260618-24: 反序列化 #[serde(rename="type")] 字段,保留以对齐 OpenAI 响应结构,标注意图消除 dead_code warning
|
||||
// 反序列化 #[serde(rename="type")] 字段,保留以对齐 OpenAI 响应结构,标注意图消除 dead_code warning
|
||||
#[allow(dead_code)]
|
||||
#[serde(rename = "type")]
|
||||
pub call_type: String,
|
||||
|
||||
@@ -40,7 +40,7 @@ const JITTER_RATIO: f64 = 1.0;
|
||||
|
||||
/// 一次尝试的分类结果 —— 在 anyhow 不透明化前决定是否值得重试。
|
||||
///
|
||||
/// CR-30-1: 暴露给 agentic.rs 流前重试复用(Init 失败分类 Fatal/Retryable 决定是否重试)。
|
||||
/// 暴露给 agentic.rs 流前重试复用(Init 失败分类 Fatal/Retryable 决定是否重试)。
|
||||
#[derive(Debug)]
|
||||
pub enum AttemptOutcome<T> {
|
||||
/// 成功,携带结果。
|
||||
@@ -60,7 +60,7 @@ pub fn is_reqwest_error_retryable(e: &reqwest::Error) -> bool {
|
||||
|
||||
/// 判定 HTTP 状态码是否可重试: 5xx 与 429 重试,其余(含 4xx)不重试。
|
||||
///
|
||||
/// CR-30-1: 暴露给 agentic.rs 流前重试复用(4xx Fatal 立即放弃,5xx/429 重试)。
|
||||
/// 暴露给 agentic.rs 流前重试复用(4xx Fatal 立即放弃,5xx/429 重试)。
|
||||
pub fn is_status_retryable(status: u16) -> bool {
|
||||
status == 429 || (500..600).contains(&status)
|
||||
}
|
||||
@@ -91,7 +91,7 @@ pub fn backoff_delay(attempt: u32) -> Duration {
|
||||
/// 调用方约定: `attempt_fn` 内部应保留完整请求重发能力(每次重建 RequestBuilder),
|
||||
/// 因为 reqwest::RequestBuilder 一次 `.send()` 后不可复用。
|
||||
///
|
||||
/// CR-30-1: 仍 pub(crate)(complete() 内部用),流前重试 stream_recv/agentic 不走此函数
|
||||
/// 仍 pub(crate)(complete() 内部用),流前重试 stream_recv/agentic 不走此函数
|
||||
/// (流式 request 不可整体 retry_with_backoff 包裹,需在 agentic loop 内手写循环复用
|
||||
/// backoff_delay + is_status_retryable)。
|
||||
pub(crate) async fn retry_with_backoff<T, F, Fut>(
|
||||
|
||||
@@ -258,7 +258,7 @@ fn build_command(request: ShellRequest) -> tokio::process::Command {
|
||||
}
|
||||
};
|
||||
|
||||
// CR-15-1: kill_on_drop(true) 让 Command 被 drop 时主动 kill 子进程。
|
||||
// kill_on_drop(true) 让 Command 被 drop 时主动 kill 子进程。
|
||||
// 配合 tokio::time::timeout 超时场景:超时 drop future → Command 析构 → kill 子进程,
|
||||
// 不再让超时后的命令变孤儿继续后台跑(长 hang 命令/死循环仍占资源)。
|
||||
// 对齐 tool_registry.rs:514「进程已终止」文案名副其实。tokio 1.52.3 支持。
|
||||
|
||||
@@ -296,7 +296,7 @@ pub(crate) fn extract_json(text: &str) -> String {
|
||||
// (原实现在此处 return body.trim() 会把语言标记/尾随文字一起喂 serde,
|
||||
// 必然失败降级启发式,即便正则本可救回。)
|
||||
}
|
||||
// 兜底:围栏不在开头或混杂前后文字 → 正则提取首个 JSON 对象(CR-40-2)。
|
||||
// 兜底:围栏不在开头或混杂前后文字 → 正则提取首个 JSON 对象。
|
||||
// (?s) 让 . 匹配换行,贪婪 {*} 取首 { 到末 },覆盖嵌套对象。
|
||||
// 提取失败(无 { })则返回 trimmed 走原文 serde 报错降级,语义不变。
|
||||
static JSON_RE: std::sync::OnceLock<regex::Regex> = std::sync::OnceLock::new();
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
//! 想法晋升 — PromotionResult(promote_idea IPC 返回类型)。
|
||||
//! 实际晋升路径:commands/idea.rs::promote_idea → df-project::ProjectManager::create_from_idea。
|
||||
//! 历史空壳 IdeaPromoter/PromotionPolicy 已删(R-PD-14)。
|
||||
|
||||
use serde::Serialize;
|
||||
|
||||
|
||||
@@ -152,7 +152,7 @@ where
|
||||
let resp = dispatch(ctx, config.read_only, req.id.clone(), method).await;
|
||||
write_response(&mut writer, &resp).await?;
|
||||
if let Some(name) = tool_name {
|
||||
// MC-5(MCP-6):仅业务成功才触发写回调——防业务失败(如 update_project 非法 status、
|
||||
// MC-5仅业务成功才触发写回调——防业务失败(如 update_project 非法 status、
|
||||
// create_project 空名等 result.isError=true)仍假触发 df-data-changed。
|
||||
if is_success_tool_call(&resp) {
|
||||
fire_write_hook(config, &name);
|
||||
@@ -169,7 +169,7 @@ where
|
||||
/// - `result` 内 `isError=true`(MCP 业务错,handler 返 `CallToolResult::error`)→ 失败
|
||||
/// - 其余(正常 result / initialize/ping 等非 tools/call)→ 成功
|
||||
///
|
||||
/// MC-5(MCP-6):stdio(main_loop)与 HTTP(server_http on_tool_call)共用此判定,
|
||||
/// stdio(main_loop)与 HTTP(server_http on_tool_call)共用此判定,
|
||||
/// 保证两 transport 对「业务失败不触发写回调」语义一致。
|
||||
pub(crate) fn is_success_tool_call(resp: &Response) -> bool {
|
||||
if resp.error.is_some() {
|
||||
@@ -651,7 +651,7 @@ mod tests {
|
||||
|
||||
#[tokio::test]
|
||||
async fn main_loop_no_write_callback_on_business_failure() {
|
||||
// MC-5(MCP-6):业务失败(handler 返 isError=true)不得触发写回调(防假 df-data-changed)。
|
||||
// MC-5业务失败(handler 返 isError=true)不得触发写回调(防假 df-data-changed)。
|
||||
// create_project 空名(name 为纯空白)→ MC-6 拒空 → isError=true。
|
||||
let ctx = test_ctx().await;
|
||||
let calls: Arc<Mutex<Vec<String>>> = Arc::new(Mutex::new(Vec::new()));
|
||||
|
||||
@@ -163,7 +163,7 @@ async fn handle_body(state: &McpHttpState, body: &[u8]) -> Outcome {
|
||||
let resp = dispatch(&state.ctx, state.read_only, req.id.clone(), method).await;
|
||||
|
||||
// ⑥ 成功 tools/call(JSON-RPC 无 error 且 result.isError≠true)→ on_tool_call 回调
|
||||
// (桌面端据此刷新 GUI)。MC-5/MCP-6:业务失败(handler 返 isError=true)不触发,
|
||||
// (桌面端据此刷新 GUI)。MC-5/业务失败(handler 返 isError=true)不触发,
|
||||
// 防假 df-data-changed;与 stdio fire_write_hook 共用 is_success_tool_call,两 transport 统一。
|
||||
if crate::server::is_success_tool_call(&resp) {
|
||||
if let (Some(name), Some(cb)) = (tool_name, &state.on_tool_call) {
|
||||
|
||||
+10
-10
@@ -247,7 +247,7 @@ fn check_expected_updated_at(args: &Value, db_updated_at: &str) -> Result<(), Ca
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// 跨实体校验(防 B-260801-01:跨实体误操作)
|
||||
// 跨实体校验(防 跨实体误操作)
|
||||
// ============================================================
|
||||
//
|
||||
// 各实体表(projects / tasks / ideas)独立存储,Repo::get_by_id 只查本表。
|
||||
@@ -352,7 +352,7 @@ fn create_project(ctx: &Ctx, args: Value) -> BoxFuture<'static, CallToolResult>
|
||||
Ok(v) => v,
|
||||
Err(r) => return Box::pin(std::future::ready(r)),
|
||||
};
|
||||
// MC-6(MCP-4):name trim + 拒空(对齐 GUI create_with_binding / ProjectManager::create,
|
||||
// name trim + 拒空(对齐 GUI create_with_binding / ProjectManager::create,
|
||||
// 防空白项目名进库——required 只保证传参,不保证非空白)。
|
||||
let name = name.trim().to_string();
|
||||
if name.is_empty() {
|
||||
@@ -406,7 +406,7 @@ fn update_project(ctx: &Ctx, args: Value) -> BoxFuture<'static, CallToolResult>
|
||||
let existing = match repo.get_by_id(&id).await {
|
||||
Ok(Some(p)) => p,
|
||||
Ok(None) => {
|
||||
// 跨实体校验(B-260801-01):id 可能属于 task/idea,给精确错误防误操作
|
||||
// 跨实体校验id 可能属于 task/idea,给精确错误防误操作
|
||||
if let Some(msg) = cross_entity_err(&db, &id, "project").await {
|
||||
return CallToolResult::error(msg);
|
||||
}
|
||||
@@ -421,7 +421,7 @@ fn update_project(ctx: &Ctx, args: Value) -> BoxFuture<'static, CallToolResult>
|
||||
// 部分更新:name/description 缺省回退 existing,避免空默认清空数据
|
||||
let name = arg_str(&args, "name").unwrap_or_else(|_| existing.name.clone());
|
||||
let description = arg_str(&args, "description").unwrap_or_else(|_| existing.description.clone());
|
||||
// MC-1(MCP-1):status 不再写入——移除 status 参数对 DB 的影响,保留原值(对齐 update_task
|
||||
// status 不再写入——移除 status 参数对 DB 的影响,保留原值(对齐 update_task
|
||||
// 收口思路:status 是生命周期状态,不经 update_project 旁路改写,防制造 GUI 不可能的非法跳态)。
|
||||
// 客户端传 status 参数被静默忽略(保留原值);项目状态变更走专用流转路径。
|
||||
let status = existing.status;
|
||||
@@ -517,7 +517,7 @@ fn bind_directory(ctx: &Ctx, args: Value) -> BoxFuture<'static, CallToolResult>
|
||||
Ok(false) => return CallToolResult::error(format!("项目不存在: {id}")),
|
||||
Err(e) => return err_str(e),
|
||||
}
|
||||
// MC-6(MCP-2):两步写降级——stack 写失败不中断绑定(path 已落库,核心目标达成)。
|
||||
// MC-6两步写降级——stack 写失败不中断绑定(path 已落库,核心目标达成)。
|
||||
// tracing::warn 记录,仍返回已绑定结果(path 已更新);GUI 侧可后续经 relocate 重探测。
|
||||
if let Err(e) = repo.update_field(&id, "stack", &stack_json).await {
|
||||
tracing::warn!(
|
||||
@@ -578,7 +578,7 @@ fn create_task(ctx: &Ctx, args: Value) -> BoxFuture<'static, CallToolResult> {
|
||||
Ok(v) => v,
|
||||
Err(r) => return Box::pin(std::future::ready(r)),
|
||||
};
|
||||
// MC-6(MCP-4):title trim + 拒空(对齐 GUI create_task BE-CMD-1,防空白标题进库)。
|
||||
// title trim + 拒空(对齐 GUI create_task BE-CMD-1,防空白标题进库)。
|
||||
let title = title.trim().to_string();
|
||||
if title.is_empty() {
|
||||
return Box::pin(std::future::ready(CallToolResult::error(
|
||||
@@ -669,7 +669,7 @@ fn update_task(ctx: &Ctx, args: Value) -> BoxFuture<'static, CallToolResult> {
|
||||
let existing = match repo.get_by_id(&id).await {
|
||||
Ok(Some(t)) => t,
|
||||
Ok(None) => {
|
||||
// 跨实体校验(B-260801-01):id 可能属于 project/idea,给精确错误防误操作
|
||||
// 跨实体校验id 可能属于 project/idea,给精确错误防误操作
|
||||
if let Some(msg) = cross_entity_err(&db, &id, "task").await {
|
||||
return CallToolResult::error(msg);
|
||||
}
|
||||
@@ -795,7 +795,7 @@ fn create_idea(ctx: &Ctx, args: Value) -> BoxFuture<'static, CallToolResult> {
|
||||
Err(r) => return Box::pin(std::future::ready(r)),
|
||||
};
|
||||
let description = arg_str_or(&args, "description", "");
|
||||
// MC-6(MCP-3):priority 值域校验(复用 df-storage normalize_priority 与 GUI/AI 工具同源,
|
||||
// priority 值域校验(复用 df-storage normalize_priority 与 GUI/AI 工具同源,
|
||||
// 拦截 99 等越界值——原 arg_int_or 直落 99 被静默吞为前端 Critical,IPC 拒/MCP 放行漂移)。
|
||||
let priority = match df_storage::crud::normalize_priority(
|
||||
args.get("priority").and_then(|v| v.as_i64()),
|
||||
@@ -845,7 +845,7 @@ fn update_idea(ctx: &Ctx, args: Value) -> BoxFuture<'static, CallToolResult> {
|
||||
let existing = match repo.get_by_id(&id).await {
|
||||
Ok(Some(i)) => i,
|
||||
Ok(None) => {
|
||||
// 跨实体校验(B-260801-01):id 可能属于 project/task,给精确错误防误操作
|
||||
// 跨实体校验id 可能属于 project/task,给精确错误防误操作
|
||||
if let Some(msg) = cross_entity_err(&db, &id, "idea").await {
|
||||
return CallToolResult::error(msg);
|
||||
}
|
||||
@@ -1451,7 +1451,7 @@ mod tests {
|
||||
}
|
||||
}
|
||||
|
||||
// ── 跨实体校验(B-260801-01):update_* 检测 id 属于其他实体时报精确错误 ──
|
||||
// ── 跨实体校验update_* 检测 id 属于其他实体时报精确错误 ──
|
||||
//
|
||||
// 各实体表独立,Repo::get_by_id 只查本表。当 id 实属另一实体时,
|
||||
// 旧实现只报「任务/项目/想法不存在」(误导),改后报「id 属于 X,不能用 update_Y 修改」。
|
||||
|
||||
@@ -124,7 +124,7 @@ impl Node for AiNode {
|
||||
"base_url": { "type": "string", "description": "(已废弃过渡)明文 API 地址,改用 provider_id" },
|
||||
"api_key": { "type": "string", "description": "(已废弃过渡)明文 API 密钥,改用 provider_id;密钥经 secret 解析" }
|
||||
},
|
||||
// SW-260802-01: schema 与 handler 行为对齐 — config 层 required=[] 正确,但 prompt 运行时必填。
|
||||
// schema 与 handler 行为对齐 — config 层 required=[] 正确,但 prompt 运行时必填。
|
||||
// prompt: execute → parse_params(ai_node_helpers.rs:189-200) 取 inputs["prompt"] > config.prompt,
|
||||
// 两者皆无则 Err("缺少必填参数: prompt")。即 prompt 真必填,但可由上游节点注入,
|
||||
// JSON Schema 只校验 config 属性无法表达"二选一",故 required 留空 + description 标注兜底来源,
|
||||
@@ -427,7 +427,7 @@ mod tests {
|
||||
// ai_self_review_node.rs(与被测代码同位,纯搬运)。
|
||||
|
||||
// ============================================================
|
||||
// SW-260802-01: schema required 与 handler 行为对齐测试
|
||||
// schema required 与 handler 行为对齐测试
|
||||
// ============================================================
|
||||
//
|
||||
// 真实 bug(误判修正):原注释称"prompt/provider_id 均留空走兜底",实则 prompt 运行时必填 ——
|
||||
|
||||
@@ -64,7 +64,7 @@ pub(crate) struct ResolvedProvider {
|
||||
/// 无任何 provider → 友好错误「未配置 AI Provider」。
|
||||
///
|
||||
/// model:config["model"] 非空用之,否则 record.default_model,再否则 "gpt-4o-mini" 占位。
|
||||
/// SW-260618-09: provider 三件套 DRY —— resolve_provider + parse_params 合并
|
||||
/// provider 三件套 DRY —— resolve_provider + parse_params 合并
|
||||
/// (AiNode/AiSelfReviewNode execute 逐字重复)。
|
||||
pub(crate) async fn resolve_and_parse(
|
||||
db: &Arc<Database>,
|
||||
@@ -72,7 +72,7 @@ pub(crate) async fn resolve_and_parse(
|
||||
inputs: &HashMap<String, NodeOutput>,
|
||||
) -> anyhow::Result<AiNodeParams> {
|
||||
let provider_cfg = resolve_provider(db, config).await?;
|
||||
// P0-B: parse_params 缺 prompt 报错时兜底 —— 工作流推进链模板 AiNode config 无 prompt
|
||||
// parse_params 缺 prompt 报错时兜底 —— 工作流推进链模板 AiNode config 无 prompt
|
||||
// (in_progress/testing/done 三链首节点 AiNode 仅带 provider_id, prompt 留空),
|
||||
// 原行为直接 Err 致整链必失败。兜底:从 config.task_id 读 TaskRecord 生成基于
|
||||
// title/description 的执行 prompt;无 task_id 用通用默认。不改 LLM 调用结构,
|
||||
@@ -134,7 +134,7 @@ fn default_fallback_prompt() -> String {
|
||||
"请根据上下文完成当前任务,产出可执行结果并自检。若需更多信息请说明。".to_string()
|
||||
}
|
||||
|
||||
/// SW-260618-09: build_provider 5 行封装 DRY(AiNode/AiSelfReviewNode execute 逐字重复)。
|
||||
/// build_provider 5 行封装 DRY(AiNode/AiSelfReviewNode execute 逐字重复)。
|
||||
pub(crate) fn provider_from_params(p: &AiNodeParams) -> Box<dyn LlmProvider> {
|
||||
df_ai::build_provider(
|
||||
&p.provider.protocol,
|
||||
|
||||
@@ -225,7 +225,7 @@ impl Node for AiSelfReviewNode {
|
||||
//
|
||||
// 设计选型(三方案对比,见 commit/设计文档):
|
||||
// A) AiNode 内部门控(本方案):1 处 return Err,复用 executor first_err + ②-4 回调,
|
||||
// 零 executor 核心改动,零依赖暂缓的条件引擎(T-260614-11)。最简。
|
||||
// 零 executor 核心改动,零依赖暂缓的条件引擎。最简。
|
||||
// B) DAG edges 条件 + ConditionEngine 求值:依赖 T-260614-11(暂缓),executor 当前
|
||||
// 完全不评估 edge.condition(topological_layers 无条件收录所有边),须先做条件
|
||||
// 引擎 Phase1+2 才能用 → 拆波。
|
||||
@@ -264,7 +264,7 @@ impl Node for AiSelfReviewNode {
|
||||
"max_tokens": { "type": "integer" },
|
||||
"gate": { "type": "boolean", "description": "闸门开关:false(默认)=自审辅助,verdict 仅透传展示;true=自审结果作 DAG 闸门,verdict=fail 返回 Err 阻断下游(工作流 failed → ②-4 退回),verdict=unknown/pass 放行" }
|
||||
},
|
||||
// SW-260802-01: schema 与 handler 行为对齐 — required 仅列 handler 真正强制必填的字段。
|
||||
// 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。
|
||||
@@ -703,7 +703,7 @@ mod tests {
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// SW-260802-01: schema required 与 handler 行为对齐测试
|
||||
// schema required 与 handler 行为对齐测试
|
||||
// ============================================================
|
||||
//
|
||||
// 真实 bug:schema `required=["task_id","provider_id"]` 与 handler 不一致 ——
|
||||
|
||||
@@ -56,7 +56,7 @@ impl Node for HumanNode {
|
||||
let mut rx = ctx.event_bus.subscribe();
|
||||
|
||||
// 2. 发送人工审批请求到事件总线
|
||||
// send 是 async fn(broadcast 同步发但 async 包装),必须 await 否则 Future 不 poll、Request 不进 channel(B-03b-R6)
|
||||
// send 是 async fn(broadcast 同步发但 async 包装),必须 await 否则 Future 不 poll、Request 不进 channel(b-R6)
|
||||
let _ = ctx.event_bus
|
||||
.send(WorkflowEvent::HumanApprovalRequest {
|
||||
execution_id: ctx.execution_id.clone(),
|
||||
@@ -292,7 +292,7 @@ mod tests {
|
||||
|
||||
#[tokio::test]
|
||||
async fn request_is_emitted_to_bus() {
|
||||
// B-03b-R8: 验证 HumanNode 真发出 HumanApprovalRequest 到事件总线
|
||||
// b-R8: 验证 HumanNode 真发出 HumanApprovalRequest 到事件总线
|
||||
// (R6 修复前 execute 内 event_bus.send().await 缺 await → Request 未进 channel,此测会超时失败)
|
||||
let bus = EventBus::new();
|
||||
let mut rx = bus.subscribe(); // subscribe 先于 execute send(broadcast 不回放)
|
||||
@@ -375,7 +375,7 @@ mod tests {
|
||||
|
||||
#[tokio::test]
|
||||
async fn invalid_decision_ignored_then_timeout() {
|
||||
// R-P1-6: options 非空且 decision ∉ options → 不立即 Err(一次手误不应杀死节点),
|
||||
// options 非空且 decision ∉ options → 不立即 Err(一次手误不应杀死节点),
|
||||
// warn 记录 + continue 续等下一条合法 Response;此处仅发一条非法的,短超时验证 → Err 超时。
|
||||
let bus = EventBus::new();
|
||||
let ctx = make_ctx(
|
||||
@@ -421,7 +421,7 @@ mod tests {
|
||||
|
||||
#[tokio::test]
|
||||
async fn empty_decision_ignored_then_timeout_even_with_empty_options() {
|
||||
// R-P1-6: 空 decision 始终非法,但同样不立即 Err——warn + continue 续等,短超时验证 → Err 超时。
|
||||
// 空 decision 始终非法,但同样不立即 Err——warn + continue 续等,短超时验证 → Err 超时。
|
||||
let bus = EventBus::new();
|
||||
let ctx = make_ctx(&bus, "exec-1", "node-h", json!({ "options": [], "timeout_secs": 1 }));
|
||||
let bus_clone = bus.clone();
|
||||
@@ -441,7 +441,7 @@ mod tests {
|
||||
// 在 execute 运行期间通道无法 Closed(至少该 sender 存活)。
|
||||
// 该分支仅在所有 sender drop 后可达,需独立集成测试,此处不强造。
|
||||
|
||||
// ===== B-03b-R8: executor 级端到端集成测试 =====
|
||||
// ===== b-R8: executor 级端到端集成测试 =====
|
||||
// 上述单测均为 HumanNode.execute 直接调用级,不覆盖 executor 驱动含 human 的 DAG。
|
||||
// 端到端覆盖 NodeStarted/Completed 事件流转、共享 state_machine、Request 经 executor 驱动真发出、
|
||||
// outputs 收集,是 R6(send 缺 await)/R7(契约失配)的存活土壤。
|
||||
@@ -470,7 +470,7 @@ mod tests {
|
||||
}
|
||||
}
|
||||
|
||||
/// B-03b-R8: executor 驱动 a(SleepNode) → b(HumanNode) 两层 DAG 端到端。
|
||||
/// b-R8: executor 驱动 a(SleepNode) → b(HumanNode) 两层 DAG 端到端。
|
||||
/// a 完成 → b 阻塞等审批 → 外部发 Response(模拟 approve_human_approval IPC) → b Ok → 工作流完成。
|
||||
/// 验证:Request 经 executor 驱动真发出(R6)、outputs 收集两层、共享 state_machine 两节点皆 Completed。
|
||||
#[tokio::test]
|
||||
@@ -532,7 +532,7 @@ mod tests {
|
||||
assert_eq!(sm.get(&"b".to_string()), NodeStatus::Completed);
|
||||
}
|
||||
|
||||
/// B-03b-R8: executor 驱动 human DAG,外部 set_cancelled(模拟 cancel_workflow_node IPC)
|
||||
/// b-R8: executor 驱动 human DAG,外部 set_cancelled(模拟 cancel_workflow_node IPC)
|
||||
/// → human cancel_tick 命中 is_cancelled → Err → executor 跳过 set_failed(Cancelled 已终态,R1)
|
||||
/// → run 返回取消 Err + 状态保持 Cancelled。覆盖取消在真实 HumanNode 上的端到端(R2)。
|
||||
#[tokio::test]
|
||||
|
||||
@@ -35,7 +35,7 @@ use crate::task_state_machine::{can_transition, is_regression, is_valid_state, A
|
||||
/// 3. 状态机(三类拒绝,错误区分供前端分辨):
|
||||
/// - 同态拒绝(from==to):Validation「相同状态,无需推进」(非状态机违例,是空操作)
|
||||
/// - 非法转换(跳态/终态后继等):InvalidState「非法状态转换 X→Y」(含 from/to 上下文)
|
||||
/// - 两类拒绝均附 legal_targets(from) 合法目标列表,供 LLM 下次选对目标态(AC-5)
|
||||
/// - 两类拒绝均附 legal_targets(from) 合法目标列表,供 LLM 下次选对目标态
|
||||
/// - can_transition 闸门矩阵判否即此分支
|
||||
/// 4. 原子写:advance_status_atomic CAS,to 是退回转换时 bump_rounds=true
|
||||
/// 5. CAS 失败(affected==0):状态已被并发改动 → InvalidState 错误(防 TOCTOU 静默成功)
|
||||
@@ -468,7 +468,7 @@ mod tests {
|
||||
|
||||
#[tokio::test]
|
||||
async fn illegal_skip_rejected() {
|
||||
// CR-01-D: 非法转换(跳态)归 InvalidState,且消息含 from→to 上下文与「非法状态转换」。
|
||||
// 非法转换(跳态)归 InvalidState,且消息含 from→to 上下文与「非法状态转换」。
|
||||
let repo = setup().await;
|
||||
repo.insert(rec("t1", TaskStatus::Todo)).await.unwrap();
|
||||
let err = advance_task_atomic(&repo, "t1", "done").await.unwrap_err();
|
||||
@@ -484,7 +484,7 @@ mod tests {
|
||||
|
||||
#[tokio::test]
|
||||
async fn terminal_done_no_successor() {
|
||||
// CR-01-D: 终态无后继也是非法转换路径,归 InvalidState(同 illegal_skip_rejected)。
|
||||
// 终态无后继也是非法转换路径,归 InvalidState(同 illegal_skip_rejected)。
|
||||
let repo = setup().await;
|
||||
repo.insert(rec("t1", TaskStatus::Done)).await.unwrap();
|
||||
let err = advance_task_atomic(&repo, "t1", "todo").await.unwrap_err();
|
||||
@@ -498,7 +498,7 @@ mod tests {
|
||||
|
||||
#[tokio::test]
|
||||
async fn same_status_rejected() {
|
||||
// CR-01-D: 同态拒绝(from==to)与非法转换(跳态)错误类型区分。
|
||||
// 同态拒绝(from==to)与非法转换(跳态)错误类型区分。
|
||||
// 同态属空操作,归 Validation「相同状态,无需推进」;
|
||||
// 非法转换归 InvalidState「非法状态转换 X→Y」(见 illegal_skip_rejected)。
|
||||
let repo = setup().await;
|
||||
|
||||
@@ -13,7 +13,7 @@ use df_relay::{DefaultRelayServer, RelayError, RelayServer};
|
||||
#[tokio::main]
|
||||
async fn main() -> df_relay::Result<()> {
|
||||
tracing_subscriber::fmt::init();
|
||||
// RLY-5:token 未设置 → 启动即返回错误(exit code 1 + 明确消息),而非运行期 panic。
|
||||
// token 未设置 → 启动即返回错误(exit code 1 + 明确消息),而非运行期 panic。
|
||||
// relay.rs expected_token() 已改为返回 Option 兜底,但 fail-fast 比慢速拒连更早暴露配置错误。
|
||||
if std::env::var("DF_RELAY_TOKEN").is_err() {
|
||||
return Err(RelayError::Start(
|
||||
|
||||
@@ -15,7 +15,7 @@
|
||||
//! `device_id` 并冒充该设备收发指令**。生产级 per-device token(配对时按 device 颁发独立 token
|
||||
//! 并存储)涉及配对流程改造(需 df-miniapp/df-tunnel 配合),当前批次不实施,仅做最小加固:
|
||||
//! - 连接建立时校验 `device_id` 格式(非空 + 长度上限,见 `validate_device_id`)。
|
||||
//! - token 比较用常量时间比较(RLY-4),防时序侧信道。
|
||||
//! - token 比较用常量时间比较,防时序侧信道。
|
||||
//! 完整 per-device 鉴权留设计文档 Phase3。
|
||||
//!
|
||||
//! AiChatEvent JSON 透传:relay 不解析 payload,只按 device_id + 方向转发。
|
||||
@@ -41,13 +41,13 @@ use crate::conn::{next_conn_id, ConnHandle, ConnId, RelayState};
|
||||
use crate::error::{RelayError, Result};
|
||||
|
||||
/// 读取期望 token(必需:env `DF_RELAY_TOKEN` 必须设置)。
|
||||
/// RLY-5:不再 panic,改为返回 Option——缺失时由连接层显式拒绝握手(main 启动时也已校验)。
|
||||
/// 不再 panic,改为返回 Option——缺失时由连接层显式拒绝握手(main 启动时也已校验)。
|
||||
/// 生产级鉴权(每 device 独立 token + 过期刷新)留 Phase3。
|
||||
fn expected_token() -> Option<String> {
|
||||
std::env::var("DF_RELAY_TOKEN").ok()
|
||||
}
|
||||
|
||||
/// RLY-3:入站文本帧大小上限(1 MiB)。
|
||||
/// 入站文本帧大小上限(1 MiB)。
|
||||
/// 防恶意/异常客户端发超大帧耗尽内存与转发带宽;超限直接断开连接。
|
||||
const MAX_FRAME_BYTES: usize = 1 * 1024 * 1024;
|
||||
|
||||
@@ -272,7 +272,7 @@ async fn handle_connection(socket: WebSocket, state: RelayState, expected: Clien
|
||||
let _ = socket_tx.close().await;
|
||||
return;
|
||||
}
|
||||
// RLY-5:token 未配置(服务启动时应已由 main 校验)→ 拒绝握手而非 panic
|
||||
// token 未配置(服务启动时应已由 main 校验)→ 拒绝握手而非 panic
|
||||
let Some(expected) = expected_token() else {
|
||||
tracing::error!("DF_RELAY_TOKEN 未设置,拒绝握手");
|
||||
let _ = send_text(
|
||||
@@ -283,7 +283,7 @@ async fn handle_connection(socket: WebSocket, state: RelayState, expected: Clien
|
||||
let _ = socket_tx.close().await;
|
||||
return;
|
||||
};
|
||||
// RLY-4:常量时间比较防时序侧信道
|
||||
// 常量时间比较防时序侧信道
|
||||
if !constant_time_eq(&hello.token, &expected) {
|
||||
tracing::warn!(
|
||||
device_id = %hello.device_id,
|
||||
@@ -332,7 +332,7 @@ async fn handle_connection(socket: WebSocket, state: RelayState, expected: Clien
|
||||
maybe_msg = socket_rx.next() => {
|
||||
match maybe_msg {
|
||||
Some(Ok(Message::Text(text))) => {
|
||||
// RLY-3:入站帧超限(>1MiB)直接断开,防大帧耗尽内存/带宽
|
||||
// 入站帧超限(>1MiB)直接断开,防大帧耗尽内存/带宽
|
||||
if text.len() > MAX_FRAME_BYTES {
|
||||
tracing::warn!(
|
||||
conn_id = conn_id.0,
|
||||
@@ -402,7 +402,7 @@ async fn recv_hello(rx: &mut futures_util::stream::SplitStream<WebSocket>) -> Re
|
||||
}
|
||||
_ => return Err(RelayError::Client("握手首帧类型非法".into())),
|
||||
};
|
||||
// RLY-3:Hello 帧同样限长(Hello 结构很小,超限视为异常/恶意)
|
||||
// Hello 帧同样限长(Hello 结构很小,超限视为异常/恶意)
|
||||
if text.len() > MAX_FRAME_BYTES {
|
||||
return Err(RelayError::Client(format!(
|
||||
"Hello 帧超限({} B > {MAX_FRAME_BYTES} B)",
|
||||
|
||||
@@ -404,7 +404,7 @@ impl IdeaRepo {
|
||||
|
||||
/// 更新单字段,**仅作用于未软删记录**(`WHERE id AND deleted_at IS NULL`)。
|
||||
///
|
||||
/// LW-6(BE-CMD-5):通用 [`update_field`] 不过滤软删(回收站灵感仍可改字段),
|
||||
/// LW-6通用 [`update_field`] 不过滤软删(回收站灵感仍可改字段),
|
||||
/// 本方法收口软删防护,供命令层 `update_idea` 使用——软删灵感(回收站)返回 `false`,
|
||||
/// 调用方据此报「已删除」。字段名走同款 [`validate_column_name`] 白名单防注入。
|
||||
pub async fn update_field_active(&self, id: &str, field: &str, value: &str) -> Result<bool> {
|
||||
@@ -430,7 +430,7 @@ impl IdeaRepo {
|
||||
|
||||
/// 原子「立项认领」:CAS 写回 status=promoted + promoted_to(`WHERE id AND promoted_to IS NULL`)。
|
||||
///
|
||||
/// LW-8(BE-CMD-7):promote_idea 读-改-写竞态的原子关闭。promote_idea 先建项目再回写灵感,
|
||||
/// promote_idea 读-改-写竞态的原子关闭。promote_idea 先建项目再回写灵感,
|
||||
/// 双击/并发两次 promote 都读到 promoted_to=None → 各自建项目;回写时本方法用
|
||||
/// `promoted_to IS NULL` 做 CAS——仅首个认领成功(affected=1),第二个 affected=0,
|
||||
/// 调用方据此判定「灵感已立项」并回滚自己刚建的项目(补偿删除),杜绝重复立项。
|
||||
|
||||
@@ -45,7 +45,7 @@ pub struct ProjectQuery {
|
||||
|
||||
/// order_by 白名单(独立于 update_field 白名单,对齐 ideas 的 validate_idea_order_by 模式)。
|
||||
///
|
||||
/// BE-CMD-3:projects update_field 白名单已剔除 id/created_at(主键与创建时间不可经通用
|
||||
/// projects update_field 白名单已剔除 id/created_at(主键与创建时间不可经通用
|
||||
/// update_field 改写),但 `created_at` 作为**排序字段**仍合法——故排序白名单单独定义,
|
||||
/// 不依赖 update_field 白名单(否则 order_by=created_at 会被误拒,破坏 list_by_query 默认排序)。
|
||||
const PROJECT_ORDER_BY_ALLOWED: &[&str] = &["created_at", "updated_at", "name", "status"];
|
||||
@@ -387,7 +387,7 @@ impl ProjectRepo {
|
||||
|
||||
/// 查找已绑定该规范化路径的项目(排除 exclude_id 自身)。无冲突返回 None。
|
||||
///
|
||||
/// DRY(R-PD-11):统一 project.rs::find_binding_conflict 与 tool_registry.rs::bind_dir_to_project
|
||||
/// DRY统一 project.rs::find_binding_conflict 与 tool_registry.rs::bind_dir_to_project
|
||||
/// 的「列项目→排除自身→按规范化路径比较」逻辑。本 crate 不依赖 df-project,故 norm_path 须由
|
||||
/// 调用方先经 `df_project::scan::normalize_path`(内含 canonicalize,防 `C:\a\b` vs `C:/a/b/` 绕过)。
|
||||
pub async fn find_path_conflict(
|
||||
@@ -464,7 +464,7 @@ impl ProjectRepo {
|
||||
|
||||
/// 更新单字段,**仅作用于未软删记录**(`WHERE id AND deleted_at IS NULL`)。
|
||||
///
|
||||
/// LW-6(BE-CMD-5):通用 [`update_field`] 不过滤软删(回收站项目仍可改字段),
|
||||
/// LW-6通用 [`update_field`] 不过滤软删(回收站项目仍可改字段),
|
||||
/// 本方法收口软删防护,供命令层 `update_project` 使用——软删项目(回收站)返回 `false`,
|
||||
/// 调用方据此报「已删除」。字段名走同款 [`validate_column_name`] 白名单防注入。
|
||||
pub async fn update_field_active(&self, id: &str, field: &str, value: &str) -> Result<bool> {
|
||||
|
||||
@@ -461,7 +461,7 @@ impl TaskRepo {
|
||||
/// 复用 list_by_query 的 WHERE 构造逻辑(仅 WHERE,无 ORDER BY/LIMIT),
|
||||
/// 返回满足条件的总行数(忽略分页裁剪)。
|
||||
///
|
||||
/// LW-5(BE-CMD-2):补齐 assignee/queue/parent_id/module_id 维度,与 list_by_query
|
||||
/// LW-5补齐 assignee/queue/parent_id/module_id 维度,与 list_by_query
|
||||
/// 全维度对齐——此前 count 缺四维导致「count 超算、list 空页」翻页不一致
|
||||
/// (前端分页 total 与页数据对不上)。
|
||||
pub async fn count_by_query(&self, query: &TaskQuery) -> Result<i64> {
|
||||
@@ -493,7 +493,7 @@ impl TaskRepo {
|
||||
where_clauses.push(format!("priority = ?{}", params_vec.len() + 1));
|
||||
params_vec.push(Box::new(p));
|
||||
}
|
||||
// LW-5: assignee 维度(与 list_by_query 同 WHERE 构造,防 count/list 漂移)
|
||||
// assignee 维度(与 list_by_query 同 WHERE 构造,防 count/list 漂移)
|
||||
if let Some(ref a) = assignee {
|
||||
where_clauses.push(format!("assignee = ?{}", params_vec.len() + 1));
|
||||
params_vec.push(Box::new(a.clone()));
|
||||
@@ -507,7 +507,7 @@ impl TaskRepo {
|
||||
params_vec.push(Box::new(pat.clone()));
|
||||
params_vec.push(Box::new(pat));
|
||||
}
|
||||
// LW-5: queue / parent_id / module_id 维度(知识图谱 V29 + 工程 V41)
|
||||
// queue / parent_id / module_id 维度(知识图谱 V29 + 工程 V41)
|
||||
if let Some(ref q) = queue {
|
||||
where_clauses.push(format!("queue = ?{}", params_vec.len() + 1));
|
||||
params_vec.push(Box::new(q.clone()));
|
||||
@@ -815,7 +815,7 @@ impl TaskRepo {
|
||||
|
||||
/// 更新单字段,**仅作用于未软删记录**(`WHERE id AND deleted_at IS NULL`)。
|
||||
///
|
||||
/// LW-6(BE-CMD-5):通用 [`update_field`] 不过滤软删(回收站任务仍可改字段),
|
||||
/// LW-6通用 [`update_field`] 不过滤软删(回收站任务仍可改字段),
|
||||
/// 本方法收口软删防护,供命令层 `update_task` 使用——软删任务(回收站)返回 `false`,
|
||||
/// 调用方据此报「已删除」,杜绝回收站任务被字段更新复活/改动。
|
||||
/// 字段名走同款 [`validate_column_name`] 白名单(防注入 + 按表隔离)。
|
||||
|
||||
@@ -704,7 +704,7 @@ fn migrate_v26(conn: &Connection) -> Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// V27: 审批状态统一 executed→completed(TD-260621-05)
|
||||
/// V27: 审批状态统一 executed→completed
|
||||
///
|
||||
/// chat.rs ai_approve/ai_authorize_dir 审批通过后工具执行成功历史写 "executed",audit 内联执行
|
||||
/// (低风险无审批)写 "completed",双轨并存致:① DTO 文档(audit/mod.rs:53)只列 completed 契约失配;
|
||||
@@ -1880,7 +1880,7 @@ mod tests {
|
||||
assert_eq!(v, 20);
|
||||
}
|
||||
|
||||
/// V27:审批状态统一 executed→completed(TD-260621-05)
|
||||
/// V27:审批状态统一 executed→completed
|
||||
/// 存量 executed 记录转 completed,rejected/completed/failed 不变(只动 executed)
|
||||
#[test]
|
||||
fn v27_unifies_executed_to_completed() {
|
||||
|
||||
@@ -74,7 +74,7 @@ pub enum WorkflowEvent {
|
||||
},
|
||||
/// 工作流执行完成
|
||||
///
|
||||
/// B-03b-R10 ③(波17 治本): execution_id 字段标识本次终态事件归属的工作流执行。
|
||||
/// b-R10 ③(波17 治本): execution_id 字段标识本次终态事件归属的工作流执行。
|
||||
/// 此前该变体不带 execution_id,AppState.event_bus 全局单例下并发工作流的终态事件
|
||||
/// 会被 forward 循环的 matches! 完成判定(workflow.rs:159-163 只看变体不看 exec_id)误判,
|
||||
/// 致任一工作流终态触发所有 forward 循环 break(过早断他人链)。
|
||||
@@ -86,7 +86,7 @@ pub enum WorkflowEvent {
|
||||
},
|
||||
/// 工作流执行失败
|
||||
///
|
||||
/// B-03b-R10 ③(波17 治本): execution_id 字段标识本次终态事件归属的工作流执行(同 WorkflowCompleted)。
|
||||
/// b-R10 ③(波17 治本): execution_id 字段标识本次终态事件归属的工作流执行(同 WorkflowCompleted)。
|
||||
WorkflowFailed {
|
||||
#[serde(default)]
|
||||
execution_id: ExecutionId,
|
||||
|
||||
@@ -207,7 +207,7 @@ impl DagExecutor {
|
||||
Err(e) => {
|
||||
let error_msg = e.to_string();
|
||||
// 已取消的节点(如 HumanNode 审批取消)跳过 set_failed:
|
||||
// Cancelled 已是终态,transition(Cancelled→Failed) 非法会 bail 致工作流崩溃(B-03b-R1)
|
||||
// Cancelled 已是终态,transition(Cancelled→Failed) 非法会 bail 致工作流崩溃(b-R1)
|
||||
if self.state_machine.is_cancelled(&node_id) {
|
||||
// emit NodeCancelled(非 NodeFailed):取消语义有别于失败,前端按 type 归「取消」非「失败」
|
||||
self.event_bus
|
||||
@@ -239,7 +239,7 @@ impl DagExecutor {
|
||||
}
|
||||
|
||||
let total = start.elapsed().as_millis() as u64;
|
||||
// B-03b-R10 ③: WorkflowCompleted 携带 execution_id,供 forward 循环按 exec_id 匹配
|
||||
// b-R10 ③: WorkflowCompleted 携带 execution_id,供 forward 循环按 exec_id 匹配
|
||||
// (全局 event_bus 单例下并发工作流的终态事件不再误触发他人 forward 的完成判定)。
|
||||
self.event_bus
|
||||
.send(WorkflowEvent::WorkflowCompleted {
|
||||
|
||||
@@ -198,7 +198,7 @@ impl Node for CancelSelfNode {
|
||||
}
|
||||
}
|
||||
|
||||
/// B-03b-R1:取消的节点返回 Err 时,executor 跳过 set_failed(Cancelled→Failed transition 非法会 bail),
|
||||
/// b-R1:取消的节点返回 Err 时,executor 跳过 set_failed(Cancelled→Failed transition 非法会 bail),
|
||||
/// 状态保持 Cancelled,run 返回取消相关 Err 而非状态转换错误。
|
||||
#[tokio::test]
|
||||
async fn test_cancelled_node_skips_set_failed() {
|
||||
@@ -248,7 +248,7 @@ impl Node for CancelSelfThenOkNode {
|
||||
}
|
||||
}
|
||||
|
||||
/// R-P1-3:Ok 后取消的 TOCTOU 防护。节点返回 Ok 但执行期间已被 set_cancelled,
|
||||
/// Ok 后取消的 TOCTOU 防护。节点返回 Ok 但执行期间已被 set_cancelled,
|
||||
/// executor Ok 分支必须跳过 set_completed(transition Cancelled→Completed 非法会 bail),
|
||||
/// 状态保持 Cancelled,run 返回 Ok(已批准审批不应误报失败)。
|
||||
#[tokio::test]
|
||||
|
||||
Reference in New Issue
Block a user