新增: 无人值守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
+43 -3
View File
@@ -296,15 +296,23 @@ fn match_task_audit_row(tool_name: &str, result: Option<&str>, task_id: &str) ->
/// 故走专用 SELECT(与 state.rs 清理孤儿 pending 消息同款 app 层直查先例);SQL 里的工具名单
/// 是 TASK_CONV_TOOLS 的硬拷贝,单测覆盖三个名字防漂移。
///
/// P2-3 task_id 预筛下沉 SQL:`result LIKE '%"id":"<task_id>"%'` 只作预筛(缩小扫描窗口,
/// 参数化绑定防注入),精确判等仍在 Rust 侧 match_task_audit_row(解析 JSON 取顶层 id 字段
/// 判等,「禁裸 LIKE 判等」不破)。预筛只会多放进相关行(嵌套 id/巧合同形文本),由 Rust
/// 侧剔除;不会漏放——audit result 均由 serde_json 紧凑序列化落库(无空格),顶层 id 字段
/// 必然以 `"id":"<id>"` 形态出现。若无预筛,LIMIT 窗口取的是全局最近 1000 行,高流量下
/// 目标任务的关联行会被无关行挤出窗外(关联静默消失);预筛后窗口=含该任务 id 形文本的
/// 最近 1000 行,早期关联仍可命中(有单测锁定)。
///
/// 过滤:① 命中行按会话去重(行按 requested_at 倒序,首见=最近一次);② 子代理会话
/// (origin='sub_agent',V44)剔除,v1 只列主会话;③ 会话已不在库的 id 一并剔除
/// (switchConversation 对不存在 id 会走"新建兜底",跳过去是空会话,不如不列)。
/// LIMIT 1000 扫描上界:抽屉每次打开都查,封顶防超大审计表全表拖慢;取最近 1000 行,
/// 更早的关联对"跳回最近上下文"场景无意义。
/// LIMIT 1000 扫描上界:抽屉每次打开都查,封顶防超大审计表全表拖慢。
pub(crate) async fn find_task_conversations(
db: &Database,
task_id: &str,
) -> Result<Vec<TaskConversationRef>, String> {
let pattern = format!("%\"id\":\"{}\"%", task_id);
let conn = db.conn();
let rows: Vec<(String, String, Option<String>, String)> =
tokio::task::spawn_blocking(move || -> Result<Vec<(String, String, Option<String>, String)>, String> {
@@ -315,11 +323,12 @@ pub(crate) async fn find_task_conversations(
"SELECT conversation_id, tool_name, result, requested_at FROM ai_tool_executions \
WHERE tool_name IN ('create_task','update_task','advance_task') \
AND conversation_id IS NOT NULL \
AND result LIKE ?1 \
ORDER BY CAST(requested_at AS INTEGER) DESC LIMIT 1000",
)
.map_err(|e| e.to_string())?;
let mapped = stmt
.query_map([], |row| {
.query_map([&pattern], |row| {
Ok((
row.get::<_, String>(0)?,
row.get::<_, String>(1)?,
@@ -590,6 +599,7 @@ mod tests {
created_at: "1".to_string(),
updated_at: "1".to_string(),
origin: origin.map(|s| s.to_string()),
boundary_json: None,
})
.await
.expect("测试会话插入应成功");
@@ -637,6 +647,36 @@ mod tests {
assert_eq!(out[0].requested_at, (t - 10).to_string());
}
/// P2-3 窗口外早期关联仍可命中:目标任务行之后灌 1000+ 条更新的无关任务工具行,
/// 旧的「全局最近 1000 条」窗口会把早期关联挤出(静默消失);task_id 预筛下沉后,
/// 窗口按预筛结果截取,早期关联仍须命中(精确判等仍在 Rust 侧,误命中不发生)。
#[tokio::test]
async fn find_task_conversations_early_hit_survives_window_flood() {
let db = Database::open_in_memory().await.expect("in-memory db 初始化失败");
let repo = AiToolExecutionRepo::new(&db);
let t_early = 1_000_000_000_000i64;
insert_conversation(&db, "conv-early", Some("早期会话"), None).await;
insert_conv_audit(&repo, "conv-early", "create_task",
Some(r#"{"id":"t-early","title":"E"}"#.into()), t_early).await;
// 灌 1100 条更新的无关任务工具行(其他任务 id),把全表推过 1000 行窗口
let t_flood = t_early + 1_000_000;
for i in 0..1100i64 {
insert_conv_audit(&repo, "conv-noise", "update_task",
Some(format!(r#"{{"id":"noise-{}"}}"#, i)), t_flood + i).await;
}
let out = find_task_conversations(&db, "t-early").await.unwrap();
assert_eq!(out.len(), 1, "早期关联不得被 1000 行窗口挤出");
assert_eq!(out[0].conversation_id, "conv-early");
assert_eq!(out[0].title.as_deref(), Some("早期会话"));
// 预筛不放无关行进来:noise 任务查不到任何关联
assert!(
find_task_conversations(&db, "noise-1").await.unwrap().is_empty(),
"noise 会话不在库,即使 id 前缀巧合也不得列出"
);
}
/// S4 纯函数判定表:名单外工具 / 非 JSON / 缺 id / id 不等 均不命中,精确判等才命中。
#[test]
fn match_task_audit_row_truth_table() {