优化: df-ai/df-storage/df-nodes 底层加固: 上下文/模型/节点/仓储

df-ai:
- context/mod.rs + manager_tests.rs: 上下文管理调整 + 测试同步(48 行)
- openai_compat.rs: OpenAI 兼容层增强(+51)
- presets/models.json: 模型预设 +18

df-storage:
- crud/conversation_repo.rs: 会话仓储(loop_state 相关)
- crud/task_repo.rs: 任务仓储
- crud/mod.rs: 导出调整

df-nodes:
- task_advance_node.rs: 任务推进节点

验收: cargo check -p devflow EXIT 0。
This commit is contained in:
lxy
2026-10-09 22:54:25 +08:00
parent d7e626a813
commit f4ca01f2f2
8 changed files with 296 additions and 36 deletions
@@ -1094,4 +1094,43 @@ mod tests {
.expect("snapshot");
assert_eq!(snapshot, r#"[{"seq":0}]"#, "冲突时应保留首次快照");
}
/// #904 契约:set_loop_state 写 "parked" 后 get_by_id 可读回(停靠出口写侧单点)。
/// 锁死「停靠状态写入收敛进 emit_task_parked」依赖的 repo 能力——若 set_loop_state
/// 或 from_row 的 loop_state 映射回归,停靠卡「继续」将再次撞「AI 未在暂停态」。
#[tokio::test]
async fn set_loop_state_parked_roundtrip() {
let db = Database::open_in_memory().await.expect("open_in_memory");
let repo = AiConversationRepo::new(&db);
repo.insert(AiConversationRecord {
id: "c1".into(),
title: None,
messages: "[]".into(),
provider_id: None,
model: None,
models: None,
archived: false,
pinned: false,
prompt_tokens: Some(0),
completion_tokens: Some(0),
pinned_goals: None,
pending_approvals: None,
created_at: "0".into(),
updated_at: "0".into(),
origin: None,
boundary_json: None,
loop_state: None,
})
.await
.expect("insert");
assert!(repo.set_loop_state("c1", Some("parked")).await.expect("set"));
let got = repo.get_by_id("c1").await.expect("get").expect("row");
assert_eq!(got.loop_state.as_deref(), Some("parked"), "写 parked 应可读回");
// 清态(running 收敛出口)可读回 None
assert!(repo.set_loop_state("c1", None).await.expect("clear"));
let cleared = repo.get_by_id("c1").await.expect("get").expect("row");
assert_eq!(cleared.loop_state, None, "清 loop_state 应可读回 None");
}
}
+1
View File
@@ -72,6 +72,7 @@ macro_rules! impl_repo {
update => |$u_conn:ident, $u_rec:ident| $update_body:expr
) => {
$(#[$meta])*
#[derive(Clone)]
pub struct $name {
conn: Arc<Mutex<Connection>>,
}
+28
View File
@@ -249,6 +249,34 @@ impl TaskRepo {
.map_err(storage_err)?
}
/// 判定任务是否在回收站(soft_delete 已设 deleted_at)。
///
/// 专供推进链 CAS 失败后的**原因区分**:`advance_status_atomic` 的 CAS SQL 带
/// `deleted_at IS NULL` 条件,软删任务必然 affected==0 返回 None;调用方据此
/// 回查本方法,把「已变更(并发推进或旁路修改)」这个误导性错误,精确改写为
/// 「任务在回收站」,让 LLM/用户知道该先恢复而非重试。
///
/// 返回 true = 任务存在且 deleted_at 非 NULL(在回收站);false = 不存在或未删。
/// 单列轻查询(SELECT 1),不拉全记录,失败回查开销可忽略。
pub async fn is_deleted(&self, id: &str) -> Result<bool> {
let conn = self.conn.clone();
let id = id.to_owned();
tokio::task::spawn_blocking(move || {
let guard = conn.blocking_lock();
let mut stmt = guard
.prepare("SELECT 1 FROM tasks WHERE id = ?1 AND deleted_at IS NOT NULL")
.map_err(storage_err)?;
let found = stmt
.query_row(params![id], |_| Ok(()))
.optional()
.map_err(storage_err)?
.is_some();
Ok(found)
})
.await
.map_err(storage_err)?
}
/// 按 id 查未删除任务(`WHERE id AND deleted_at IS NULL`)。
///
/// 专供 [`validate_parent_id`](super::task_validation) 的父任务存在性检查: