修复: 飞书走查六项 + 停靠状态写侧根因解 + 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 -1
View File
@@ -150,18 +150,37 @@ async function cleanupTerminatedConversation(convId: string, reason: 'AiComplete
/**
* 登记无人值守停靠卡(幂等,convId 键控覆盖)。lifecycle 主路径与 useAiEvents 后台会话
* 分支共用唯一写入口,防止两处字段口径漂移。convId 缺失兜底当前活跃会话(优于丢卡)。
*
* #906 打扰治理:同会话短时内重复停靠节流。网络抖动期「重发→中断→再弹」会连续触发停靠,
* 卡片反复弹/消造成打扰观感。策略:同一 conv 距上次停靠 < PARKED_REPARK_THROTTLE_MS 时,
* 若当前已有停靠卡(未被用户操作摘除)→ 保留原卡不刷新(避免重渲染/时间戳跳动);若原卡已被
* 用户「继续/停止」摘除(用户已主动处置)→ 仍放行新卡(不能吞掉用户需决策的新终局)。
* 节流仅压「未处置的重复弹」,不吞「用户已处置后的真实新停靠」,安全边界清晰。
*/
const PARKED_REPARK_THROTTLE_MS = 20000
// 上次停靠时刻(per-conv,跨摘卡保留):用于判定「用户已处置后短时再停靠」是否属抖动回环。
const _lastParkedAt = new Map<string, number>()
export function recordTaskParked(convId: string | null | undefined, reason: string, stopKind?: string): void {
const id = convId || state.activeConversationId
if (!id) return
const now = Date.now()
const existing = pendingTaskParked.get(id)
const lastAt = _lastParkedAt.get(id) ?? 0
// 节流:卡仍在(未处置)+ 距上次停靠极近 → 保留原卡,不刷新(压抖动期重复弹)。
if (existing && now - lastAt < PARKED_REPARK_THROTTLE_MS) {
return
}
_lastParkedAt.set(id, now)
pendingTaskParked.set(id, {
reason,
stopKind: stopKind || undefined,
conversationId: id,
parkedAt: Date.now(),
parkedAt: now,
})
}
/** lifecycle 域:整轮收尾(AiCompleted)与异常中断(AiError)。返回 true=已处理。
* 异步说明:cleanup 内含仲裁 IPC(ai_is_generating),故本函数 async;handleEvent 以 void 调用,
* 每个终态 case 必须先 await cleanup 拿到仲裁结果再决定下游执行。 */