优化: 边界加固(AI loop竞态根治+数据/审批/反馈/并发/错误分类)
AI loop 竞态(P0):per-conv epoch/owner token + 存活心跳治 force_send 双loop + stop 3s兜底误判;旧loop stale 全跳过(guard/emit/save)
agentic 收尾(A2-B8):Fatal 退出落库user消息(镜像Exhausted)+ 入口早退补save + usage is_estimated 打标 + emit_ai_completed_once 单点收敛清审批残留
聊天清理(A2-B9):clearChat 先停loop→DB单事务→内存清(clear_conversation_atomic)+ 前端错误气泡
循环并发(A2-B11):三态 ProviderAcquire(NotConfigured/Acquired/Exhausted)+ 候选循环非阻塞+防抖3次饱和降级+单测
错误分类(A2-B12):stream error帧接入 classify_status_or_class + 关键词保守降级 + 7单测
数据(G1.2/G1.4):purge_with_descendants 级联补全(11表单事务+存在性守卫)+ move_task_queue 单事务收口(两调用方共用)
git只读(G3.1):run_git_status/diff/log success判定(exit_code差异语义,失败结构化{success:false,error})
安全(G5.2/G5.6):create_project 目录Err+name校验 + module.rs 路径遍历DRY(分段匹配修a..b.rs误伤)
幂等(V2/V32):裸ALTER全守卫化 + v1..v40全链重跑幂等测试(16过)
附:remote_bridge await 临时引用修(E0716)+ agentic emit 收敛 E0716 app_state 绑定修
This commit is contained in:
@@ -45,7 +45,7 @@ pub fn run(conn: &Connection) -> Result<()> {
|
||||
// 什么数据库、Redis 在哪、有没有 MQ"的基础设施上下文。
|
||||
// V33 = 审批重启恢复:ai_conversations 加 pending_approvals TEXT 列,持久化挂起审批快照,
|
||||
// 重启后从 DB 恢复 pending_approvals 内存态,使待审批不丢。
|
||||
let steps: [(i32, fn(&Connection) -> Result<()>); 39] = [
|
||||
let steps: [(i32, fn(&Connection) -> Result<()>); 40] = [
|
||||
(1, migrate_v1),
|
||||
(2, migrate_v2),
|
||||
(3, migrate_v3),
|
||||
@@ -85,6 +85,7 @@ pub fn run(conn: &Connection) -> Result<()> {
|
||||
(37, migrate_v37),
|
||||
(38, migrate_v38),
|
||||
(39, migrate_v39),
|
||||
(40, migrate_v40),
|
||||
];
|
||||
|
||||
for (version, migrate_fn) in steps {
|
||||
@@ -105,8 +106,21 @@ fn migrate_v1(conn: &Connection) -> Result<()> {
|
||||
}
|
||||
|
||||
/// V2: 补齐关联字段 + branches 表
|
||||
///
|
||||
/// 幂等化(2026-08-05):V2_SQL 原含 7 个裸 ALTER,全链重跑 duplicate column。
|
||||
/// 拆分:CREATE TABLE branches(IF NOT EXISTS 幂等,execute_batch 跑);
|
||||
/// 7 个 ALTER 改逐列 column_exists 守卫(对齐 v4+ 幂等模式)。
|
||||
fn migrate_v2(conn: &Connection) -> Result<()> {
|
||||
// branches 等 CREATE TABLE IF NOT EXISTS 幂等,execute_batch 跑
|
||||
conn.execute_batch(V2_SQL)?;
|
||||
// ideas: 晋升关联 + AI 分析 + 多维评分
|
||||
for (table, col) in [("ideas", "promoted_to"), ("ideas", "ai_analysis"), ("ideas", "scores"),
|
||||
("tasks", "workflow_def_id"), ("tasks", "base_branch"),
|
||||
("workflow_executions", "project_id"), ("workflow_executions", "task_id")] {
|
||||
if !column_exists(conn, table, col) {
|
||||
conn.execute(&format!("ALTER TABLE {table} ADD COLUMN {col} TEXT"), [])?;
|
||||
}
|
||||
}
|
||||
conn.execute("INSERT INTO schema_version (version) VALUES (?)", [2])?;
|
||||
tracing::info!("迁移 v2 完成");
|
||||
Ok(())
|
||||
@@ -917,10 +931,16 @@ fn migrate_v31(conn: &Connection) -> Result<()> {
|
||||
/// 对话目标由 PerConvState.pinned_goals(Vec<GoalEntry>)管理,原先仅在内存态存在,
|
||||
/// 此迁移为其提供持久化列,默认空 JSON 数组'[]'。
|
||||
fn migrate_v32(conn: &Connection) -> Result<()> {
|
||||
conn.execute_batch(
|
||||
"ALTER TABLE ai_conversations ADD COLUMN pinned_goals TEXT DEFAULT '[]';"
|
||||
)?;
|
||||
tracing::info!("v32: ai_conversations 加 pinned_goals 列");
|
||||
// G5.5: 幂等守卫(column_exists 探测,同 v4/v20/v33 模式)——列已存在跳过 ALTER。
|
||||
// 防存量库崩溃重跑/版本号回退后重跑 migrate_v32 报 duplicate column name。
|
||||
if !column_exists(conn, "ai_conversations", "pinned_goals") {
|
||||
conn.execute_batch(
|
||||
"ALTER TABLE ai_conversations ADD COLUMN pinned_goals TEXT DEFAULT '[]';"
|
||||
)?;
|
||||
tracing::info!("v32: ai_conversations 加 pinned_goals 列");
|
||||
} else {
|
||||
tracing::info!("v32: pinned_goals 列已存在,跳过");
|
||||
}
|
||||
conn.execute("INSERT INTO schema_version (version) VALUES (?)", [32])?;
|
||||
tracing::info!("迁移 v32 完成");
|
||||
Ok(())
|
||||
@@ -974,6 +994,9 @@ fn migrate_v33(conn: &Connection) -> Result<()> {
|
||||
/// 单仓库项目退化:项目下只有一个工程(path = 绑定目录)。
|
||||
///
|
||||
/// Git 状态(分支/改动/提交)是实时派生的(查 git 命令),不存表。
|
||||
///
|
||||
/// 注:description / status 列由 V40 追加(老库 V34 时无),新库由 V40 ALTER
|
||||
/// 补建(因 V34 首次建表已注册版本号,新库只跑 V34 一次)。两边列定义须一致。
|
||||
fn migrate_v34(conn: &Connection) -> Result<()> {
|
||||
conn.execute(
|
||||
"CREATE TABLE IF NOT EXISTS project_modules (
|
||||
@@ -1184,6 +1207,29 @@ fn migrate_v39(conn: &Connection) -> Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// V40: project_modules 表加 description / status 列(工程实体化,补全描述身份)
|
||||
///
|
||||
/// 解「工程仅有目录地址、无描述身份」:Monorepo 下前端工程/后端服务/基础设施
|
||||
/// 各有职责与状态,原表只存 name/path/git_url/stack,失去真实场景表达。
|
||||
/// - description:工程职责描述(如"前端 web 工程""后端 API 服务"),nullable 老工程兼容。
|
||||
/// - status:工程状态(active/archived),nullable,默认 active(老工程回读时 None 由
|
||||
/// 应用层归一为 active——见 ProjectModuleRecord 注释)。
|
||||
///
|
||||
/// 列可空,向前兼容:V34 前无此列的老库 ALTER 后旧行 NULL,前端/应用层视为未填写。
|
||||
fn migrate_v40(conn: &Connection) -> Result<()> {
|
||||
if !column_exists(conn, "project_modules", "description") {
|
||||
conn.execute("ALTER TABLE project_modules ADD COLUMN description TEXT", [])?;
|
||||
tracing::info!("v40: project_modules 加 description 列");
|
||||
}
|
||||
if !column_exists(conn, "project_modules", "status") {
|
||||
conn.execute("ALTER TABLE project_modules ADD COLUMN status TEXT", [])?;
|
||||
tracing::info!("v40: project_modules 加 status 列");
|
||||
}
|
||||
conn.execute("INSERT INTO schema_version (version) VALUES (?)", [40])?;
|
||||
tracing::info!("迁移 v40 完成: project_modules 加 description/status 列(工程实体化)");
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// V21 建表 SQL — 消息拆分存储 ai_messages 表
|
||||
///
|
||||
/// 与 V9_SQL 中的 ai_messages 镜像(V9 给新库,此 const 给老库 V21 迁移用 IF NOT EXISTS)。
|
||||
@@ -1329,21 +1375,9 @@ CREATE INDEX IF NOT EXISTS idx_node_executions_workflow_id ON node_executions(wo
|
||||
|
||||
/// V2 迁移 SQL — 补齐数据层断裂字段
|
||||
///
|
||||
/// 注意: SQLite 的 ALTER TABLE ADD COLUMN 一条语句只能加一列。
|
||||
/// 幂等化(2026-08-05):原含 7 个裸 ALTER(ideas/tasks/workflow 加列),全链重跑 duplicate column。
|
||||
/// ALTER 已移至 migrate_v2 逐列 column_exists 守卫,此处仅保留 branches CREATE + 索引(IF NOT EXISTS 幂等)。
|
||||
const V2_SQL: &str = "
|
||||
-- 想法表: 晋升关联 + AI 分析 + 多维评分
|
||||
ALTER TABLE ideas ADD COLUMN promoted_to TEXT;
|
||||
ALTER TABLE ideas ADD COLUMN ai_analysis TEXT;
|
||||
ALTER TABLE ideas ADD COLUMN scores TEXT;
|
||||
|
||||
-- 任务表: 工作流定义关联 + 基础分支
|
||||
ALTER TABLE tasks ADD COLUMN workflow_def_id TEXT;
|
||||
ALTER TABLE tasks ADD COLUMN base_branch TEXT;
|
||||
|
||||
-- 工作流执行表: 项目 / 任务关联
|
||||
ALTER TABLE workflow_executions ADD COLUMN project_id TEXT;
|
||||
ALTER TABLE workflow_executions ADD COLUMN task_id TEXT;
|
||||
|
||||
-- 分支表 — 任务与 Git 分支绑定(核心功能)
|
||||
CREATE TABLE IF NOT EXISTS branches (
|
||||
id TEXT PRIMARY KEY,
|
||||
@@ -1881,7 +1915,7 @@ mod tests {
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// 全量迁移测试 — 新库从零跑完整 V1-V37 路径
|
||||
// 全量迁移测试 — 新库从零跑完整 V1-V40 路径
|
||||
// ------------------------------------------------------------
|
||||
// 目的:某 migrate_vN 的 SQL 手滑写错(列名/类型/缺索引/缺表)只能等运行时暴露,
|
||||
// 此测试一次性覆盖全部迁移路径。任何一条迁移 SQL 写错、列名拼错、缺建表
|
||||
@@ -1905,12 +1939,12 @@ mod tests {
|
||||
);
|
||||
}
|
||||
|
||||
/// 全量迁移:新库从零跑完 V1-V37,验证关键表齐全 + 列数 > 0 + 关键列存在。
|
||||
/// 全量迁移:新库从零跑完 V1-V40,验证关键表齐全 + 列数 > 0 + 关键列存在。
|
||||
///
|
||||
/// 覆盖至少:task / ai_conversations / ai_messages / ai_tool_executions /
|
||||
/// conversation_checkpoints / ai_providers / projects / ideas。
|
||||
/// 抽查关键列:ai_providers.enabled/weight、conversation_checkpoints.snapshot、
|
||||
/// tasks.idea_id(这些列由不同 vN 加,任一漏加此处失败)。
|
||||
/// tasks.idea_id、project_modules.description/status(这些列由不同 vN 加,任一漏加此处失败)。
|
||||
#[tokio::test]
|
||||
async fn test_full_migration_on_fresh_db() {
|
||||
// 用 Database::open_in_memory 打开新库,内部自动跑 migrations::run() 全量迁移
|
||||
@@ -1956,7 +1990,7 @@ mod tests {
|
||||
"tasks.idea_id 列缺失(V1 建表已带)"
|
||||
);
|
||||
|
||||
// 3. schema_version 应推进到 37(全量迁移成功落版本号)
|
||||
// 3. schema_version 应推进到 40(全量迁移成功落版本号)
|
||||
let max_version: i64 = conn
|
||||
.query_row(
|
||||
"SELECT COALESCE(MAX(version), 0) FROM schema_version",
|
||||
@@ -1965,9 +1999,91 @@ mod tests {
|
||||
)
|
||||
.expect("查 schema_version 应成功");
|
||||
assert_eq!(
|
||||
max_version, 37,
|
||||
"全量迁移后 schema_version 应为 37(实际 {}),说明某条 migrate_vN 链路断在中间",
|
||||
max_version, 40,
|
||||
"全量迁移后 schema_version 应为 40(实际 {}),说明某条 migrate_vN 链路断在中间",
|
||||
max_version
|
||||
);
|
||||
|
||||
// 4. V40 抽查:project_modules 表 description / status 列存在(老库 ALTER 补,新库 V40 也跑)
|
||||
assert!(
|
||||
column_exists(&conn, "project_modules", "description"),
|
||||
"project_modules.description 列缺失(V40 加)"
|
||||
);
|
||||
assert!(
|
||||
column_exists(&conn, "project_modules", "status"),
|
||||
"project_modules.status 列缺失(V40 加)"
|
||||
);
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// G5.5: v32 幂等守卫 + 全链幂等不变量
|
||||
// ------------------------------------------------------------
|
||||
// 背景:migrate_v32 原为裸 ALTER(唯一漏网),崩溃重跑/版本号回退后重跑会报
|
||||
// duplicate column name。修后应幂等;再加全链重跑不变量防止未来新 vN 引入裸 ALTER。
|
||||
// ============================================================
|
||||
|
||||
/// G5.5: v32 幂等守卫——pinned_goals 列已存在时重跑不报 duplicate column。
|
||||
/// 构造最小 ai_conversations 表(无 pinned_goals 列)模拟老库,验证补列 + 重跑短路。
|
||||
#[test]
|
||||
fn v32_idempotent_column_guard() {
|
||||
let conn = Connection::open_in_memory().expect("open in-memory db");
|
||||
conn.execute_batch(
|
||||
"CREATE TABLE schema_version (version INTEGER PRIMARY KEY);
|
||||
CREATE TABLE ai_conversations (id TEXT PRIMARY KEY);",
|
||||
)
|
||||
.expect("create tables");
|
||||
assert!(
|
||||
!column_exists(&conn, "ai_conversations", "pinned_goals"),
|
||||
"迁移前应无 pinned_goals 列"
|
||||
);
|
||||
|
||||
migrate_v32(&conn).expect("首次迁移应补 pinned_goals 列");
|
||||
assert!(column_exists(&conn, "ai_conversations", "pinned_goals"));
|
||||
|
||||
// 回退版本号模拟「列已存在但版本号缺失」崩溃重跑场景 → ALTER 应被守卫短路,
|
||||
// 仅补版本号,不报 duplicate column name。
|
||||
conn.execute("DELETE FROM schema_version WHERE version = 32", [])
|
||||
.unwrap();
|
||||
migrate_v32(&conn).expect("列已存在时重跑应幂等不报错");
|
||||
|
||||
let v_count: i64 = conn
|
||||
.query_row(
|
||||
"SELECT COUNT(*) FROM schema_version WHERE version = 32",
|
||||
[],
|
||||
|r| r.get(0),
|
||||
)
|
||||
.unwrap();
|
||||
assert_eq!(v_count, 1, "版本号 32 应只写一次");
|
||||
}
|
||||
|
||||
/// G5.5: 全链幂等不变量——V1-V40 每步执行两遍不抛错。
|
||||
///
|
||||
/// 首轮 run() 建全 schema;清空 schema_version 强制下一轮从 V1 重跑每步
|
||||
/// (模拟存量库 + 崩溃重跑/版本号回退)。任何 migrate_vN 的裸 ALTER(无 column_exists
|
||||
/// 守卫,如 v32 修前形态)都会在第二遍报 duplicate column 被此测试捕获。
|
||||
#[test]
|
||||
fn v1_to_v40_full_chain_rerun_idempotent() {
|
||||
let conn = Connection::open_in_memory().expect("open in-memory db");
|
||||
run(&conn).expect("首轮全量迁移应成功");
|
||||
let max_v: i64 = conn
|
||||
.query_row(
|
||||
"SELECT COALESCE(MAX(version), 0) FROM schema_version",
|
||||
[],
|
||||
|r| r.get(0),
|
||||
)
|
||||
.unwrap();
|
||||
assert_eq!(max_v, 40, "首轮应推进到 40");
|
||||
|
||||
// 清空版本表强制全链第二遍(每步 execute 第二次)
|
||||
conn.execute("DELETE FROM schema_version", []).unwrap();
|
||||
run(&conn).expect("全链第二遍不抛错(幂等不变量)");
|
||||
let max_v2: i64 = conn
|
||||
.query_row(
|
||||
"SELECT COALESCE(MAX(version), 0) FROM schema_version",
|
||||
[],
|
||||
|r| r.get(0),
|
||||
)
|
||||
.unwrap();
|
||||
assert_eq!(max_v2, 40, "重跑后应重新推进到 40");
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user