新增: 无人值守P0任务边界全链

- 任务级授权边界判定与审批旁路单点插桩
- boundary_json 随会话落库(V45 可逆迁移)
- 六路非正常终局统一安全停靠与托盘通知
- 每五轮进度事件与会话头部进度条
- 前端边界配置卡侧栏徽标及 UX 校验补齐
This commit is contained in:
lxy
2026-10-04 16:27:30 +08:00
parent 8396849d04
commit b2d44f85e9
29 changed files with 2773 additions and 142 deletions
+59 -6
View File
@@ -51,7 +51,9 @@ pub fn run(conn: &Connection) -> Result<()> {
// 纯数据 UPDATE 不经过 promote 函数,不触发嵌入生成)。
// V44 = 子代理轨迹来源标记(ai_conversations.origin TEXT,spawn_agent 出口整轨迹落库,
// 桌面侧栏/miniapp 列表按 origin 过滤子会话)。
let steps: [(i32, fn(&Connection) -> Result<()>); 44] = [
// V45 = 无人值守任务边界(ai_conversations.boundary_json TEXT,TaskBoundary JSON 快照,
// 发起时一次声明存续期生效,随会话同亡)。
let steps: [(i32, fn(&Connection) -> Result<()>); 45] = [
(1, migrate_v1),
(2, migrate_v2),
(3, migrate_v3),
@@ -96,6 +98,7 @@ pub fn run(conn: &Connection) -> Result<()> {
(42, migrate_v42),
(43, migrate_v43),
(44, migrate_v44),
(45, migrate_v45),
];
for (version, migrate_fn) in steps {
@@ -1334,6 +1337,23 @@ fn migrate_v44(conn: &Connection) -> Result<()> {
Ok(())
}
/// V45: 无人值守任务边界 — ai_conversations 加 boundary_json TEXT 列。
///
/// TaskBoundary JSON 快照(风险上限/目录白名单/禁破坏性/预算),发起时一次声明、
/// 任务存续期生效、随会话同亡(无独立表,设计拍板点 2: 任务与会话 1:1,独立表过度设计)。
/// NULL=未声明(现状逐次审批,渐进增强零倒退)。
/// - **幂等**:column_exists 守卫(同 V42/V44 模式),崩溃重跑/全链两遍不炸;
/// - **可逆**:`ALTER TABLE ai_conversations DROP COLUMN boundary_json`(SQLite 3.35+)。
fn migrate_v45(conn: &Connection) -> Result<()> {
if !column_exists(conn, "ai_conversations", "boundary_json") {
conn.execute("ALTER TABLE ai_conversations ADD COLUMN boundary_json TEXT", [])?;
tracing::info!("v45: ai_conversations 加 boundary_json 列(无人值守任务边界)");
}
conn.execute("INSERT OR IGNORE INTO schema_version (version) VALUES (?)", [45])?;
tracing::info!("迁移 v45 完成: ai_conversations 加 boundary_json 列");
Ok(())
}
/// V21 建表 SQL — 消息拆分存储 ai_messages 表
///
/// 与 V9_SQL 中的 ai_messages 镜像(V9 给新库,此 const 给老库 V21 迁移用 IF NOT EXISTS)。
@@ -2338,7 +2358,7 @@ mod tests {
"tasks.module_id 列缺失(V41 加)"
);
// 3. schema_version 应推进到 43(全量迁移成功落版本号)
// 3. schema_version 应推进到 45(全量迁移成功落版本号)
let max_version: i64 = conn
.query_row(
"SELECT COALESCE(MAX(version), 0) FROM schema_version",
@@ -2347,8 +2367,8 @@ mod tests {
)
.expect("查 schema_version 应成功");
assert_eq!(
max_version, 44,
"全量迁移后 schema_version 应为 44(实际 {}),说明某条 migrate_vN 链路断在中间",
max_version, 45,
"全量迁移后 schema_version 应为 45(实际 {}),说明某条 migrate_vN 链路断在中间",
max_version
);
@@ -2425,7 +2445,7 @@ mod tests {
|r| r.get(0),
)
.unwrap();
assert_eq!(max_v, 44, "首轮应推进到 44");
assert_eq!(max_v, 45, "首轮应推进到 45");
// 清空版本表强制全链第二遍(每步 execute 第二次)
conn.execute("DELETE FROM schema_version", []).unwrap();
@@ -2437,7 +2457,7 @@ mod tests {
|r| r.get(0),
)
.unwrap();
assert_eq!(max_v2, 44, "重跑后应重新推进到 44");
assert_eq!(max_v2, 45, "重跑后应重新推进到 45");
}
// ============================================================
@@ -2510,4 +2530,37 @@ mod tests {
.unwrap();
assert_eq!(v, 41);
}
/// V45 幂等:最小库补建 boundary_json 列;重跑(含手动回退版本号模拟崩溃断点)不炸、
/// 不报 duplicate column、版本号 INSERT OR IGNORE 防 PK 冲突。
#[test]
fn v45_boundary_json_idempotent() {
let conn = setup_legacy_db();
migrate_v45(&conn).expect("v45 首次迁移应成功");
assert!(column_exists(&conn, "ai_conversations", "boundary_json"), "应有 boundary_json 列");
let v: i64 = conn
.query_row("SELECT MAX(version) FROM schema_version", [], |r| r.get(0))
.unwrap();
assert_eq!(v, 45, "应写入版本号 45");
// 模拟崩溃断点:版本号回退(45 未落)但列已建 → 重跑走 column_exists 短路
conn.execute("DELETE FROM schema_version WHERE version = 45", []).unwrap();
migrate_v45(&conn).expect("列已存在时重跑应幂等跳过 ALTER 不报错");
let v2: i64 = conn
.query_row("SELECT MAX(version) FROM schema_version", [], |r| r.get(0))
.unwrap();
assert_eq!(v2, 45, "重跑后版本号恢复 45");
// 列可写可读(NULL 语义=未声明边界)
conn.execute(
"INSERT INTO ai_conversations (id, messages, created_at, updated_at, boundary_json) \
VALUES ('c1', '[]', '0', '0', '{\"risk_ceiling\":\"medium\"}')",
[],
)
.unwrap();
let got: Option<String> = conn
.query_row("SELECT boundary_json FROM ai_conversations WHERE id = 'c1'", [], |r| r.get(0))
.unwrap();
assert!(got.is_some(), "boundary_json 应可写可读");
}
}