修复: 飞书走查六项 + 停靠状态写侧根因解 + agentic/audit 加固

飞书集成走查六项(feishu_channel.rs +420):
1. 审批卡按钮化: on_card_action + handle_card_action(解析 {approval_id,approved}
   → ai_approve) + send_approval_button_card(Card JSON 1.0 双按钮, 失败降级文本卡)
2. 首帧流式提速: AiTextDelta 首个 delta 立即刷占位卡, 不等 3s ticker
3. 断线重连通知: 按 run_loop 时长判据(≥30s 推断开 / <30s 累计 3 次推不稳定),
   新增 notify_monitor()
4. 占位卡带任务原文回显(truncate 60)
5. 白名单配置引导: 后端分步自助引导 + FeishuPanel hintFeishuWhitelist + i18n
6. 审批决议后追加执行结果: spawn_approval_result_watcher 订阅 AiToolCallCompleted
   (匹配 tool_call_id, 60s 超时兜底); 订阅须在 ai_approve 前完成(无订阅者跳过序列化)

停靠(parked)写侧根因解(904):
emit_task_parked(ai/mod.rs) 升级为「写 parked + 发事件 + 通知」三件套——
新增 set_loop_state(conv_id, Some("parked")) fire-and-forget; 8 个业务调用点
(H2 三闸/熔断两路/挂起取消/max_rounds/midstream/fatal + 审批超时) 全经单点。
治「停靠卡继续必报 AI 未在暂停态」(全仓原无写 parked 路径)。

agentic/audit 加固: context_lifecycle/stream_relay/mod 调整 + audit record 完善 +
tool_failure_memory/tool_result_offload/tools/task/settings 同步。

验收: cargo check -p devflow EXIT 0。
This commit is contained in:
lxy
2026-10-09 22:54:19 +08:00
parent b283622c64
commit d7e626a813
18 changed files with 872 additions and 127 deletions
+20 -12
View File
@@ -43,15 +43,20 @@ pub fn register(registry: &mut AiToolRegistry, db: &Arc<Database>) {
schema: object_schema(vec![("project_id", "string", false), ("status", "string", false), ("offset", "integer", false), ("limit", "integer", false)]),
args => {
let repo = df_storage::crud::TaskRepo::new(&db);
let mut tasks = if let Some(pid) = args.get("project_id").and_then(|v| v.as_str()) {
repo.query("project_id", pid).await?
} else {
repo.list_all().await?
// 走 list_by_query(与 IPC/MCP 同源):恒带 `deleted_at IS NULL AND archived = 0`,
// 回收站/归档任务不进结果。
//
// 修复背景:此前用 repo.query("project_id", pid) / repo.list_all(),二者均无
// 软删/归档过滤 → AI 工具能看到回收站任务,但 advance_task 的 CAS SQL 带
// `deleted_at IS NULL AND archived = 0` 必败 → 报「已变更(并发推进或旁路修改)」
// 误导性错误(真实原因是任务在回收站)。改用 list_by_query 后,AI 看到的任务
// 与可推进的任务集合一致,消除「看得见点不动」。
let q = df_storage::crud::TaskQuery {
project_id: args.get("project_id").and_then(|v| v.as_str()).map(String::from),
status: args.get("status").and_then(|v| v.as_str()).map(String::from),
..Default::default()
};
// 按状态过滤(可选):todo/in_progress/in_review/testing/blocked/done/cancelled/deferred
if let Some(status) = args.get("status").and_then(|v| v.as_str()) {
tasks.retain(|t| t.status.as_str() == status);
}
let tasks = repo.list_by_query(&q).await?;
let total = tasks.len();
let offset = args["offset"].as_u64().unwrap_or(0) as usize;
let limit = args["limit"].as_u64().unwrap_or(MAX_LIST_RESULTS as u64).min(MAX_LIST_RESULTS as u64) as usize;
@@ -253,11 +258,14 @@ pub fn register(registry: &mut AiToolRegistry, db: &Arc<Database>) {
schema: object_schema(vec![("project_id", "string", false)]),
args => {
let repo = df_storage::crud::TaskRepo::new(&db);
let tasks = if let Some(pid) = args.get("project_id").and_then(|v| v.as_str()) {
repo.query("project_id", pid).await?
} else {
repo.list_all().await?
// 走 list_by_query(与 list_tasks 同源):恒带 `deleted_at IS NULL AND archived = 0`,
// 计数与可见任务集合一致(此前 list_all/query 无过滤,计数含回收站/归档任务,
// 与 list_tasks 结果数不符,分页策略判断失真)。
let q = df_storage::crud::TaskQuery {
project_id: args.get("project_id").and_then(|v| v.as_str()).map(String::from),
..Default::default()
};
let tasks = repo.list_by_query(&q).await?;
Ok(serde_json::json!({ "total": tasks.len() }))
}
);