修复: 飞书走查六项 + 停靠状态写侧根因解 + 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
+15 -6
View File
@@ -84,28 +84,28 @@
:disabled="approving"
@click="onAuthorizeOnce">
<span v-if="approving" class="ai-tool-btn-spinner" />
<svg aria-hidden="true" v-else width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><polyline points="20 6 9 17 4 12"/></svg>
<span v-else class="ai-tool-btn-icon" v-html="ICON_CHECK" />
{{ $t('aiChat.dirAuthOnce') }}
</button>
<button class="ai-tool-btn ai-tool-btn--session"
:disabled="approving"
@click="onAuthorizeSession">
<span v-if="approving" class="ai-tool-btn-spinner" />
<svg aria-hidden="true" v-else width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><polyline points="20 6 9 17 4 12"/></svg>
<span v-else class="ai-tool-btn-icon" v-html="ICON_CHECK" />
{{ $t('aiChat.dirAuthSession') }}
</button>
<button class="ai-tool-btn ai-tool-btn--always"
:disabled="approving"
@click="onAuthorizeAlways">
<span v-if="approving" class="ai-tool-btn-spinner" />
<svg aria-hidden="true" v-else width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><polyline points="20 6 9 17 4 12"/></svg>
<span v-else class="ai-tool-btn-icon" v-html="ICON_CHECK" />
{{ $t('aiChat.dirAuthAlways') }}
</button>
<button class="ai-tool-btn ai-tool-btn--reject"
:disabled="approving"
@click="onDeny">
<span v-if="approving" class="ai-tool-btn-spinner" />
<svg aria-hidden="true" v-else width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><line x1="18" y1="6" x2="6" y2="18"/><line x1="6" y1="6" x2="18" y2="18"/></svg>
<span v-else class="ai-tool-btn-icon" v-html="ICON_CROSS" />
{{ $t('aiChat.dirAuthDeny') }}
</button>
</div>
@@ -114,14 +114,14 @@
:disabled="approving"
@click="onApprove">
<span v-if="approving" class="ai-tool-btn-spinner" />
<svg aria-hidden="true" v-else width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><polyline points="20 6 9 17 4 12"/></svg>
<span v-else class="ai-tool-btn-icon" v-html="ICON_CHECK" />
{{ $t('aiTool.approve') }}
</button>
<button class="ai-tool-btn ai-tool-btn--reject"
:disabled="approving"
@click="onReject">
<span v-if="approving" class="ai-tool-btn-spinner" />
<svg aria-hidden="true" v-else width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><line x1="18" y1="6" x2="6" y2="18"/><line x1="6" y1="6" x2="18" y2="18"/></svg>
<span v-else class="ai-tool-btn-icon" v-html="ICON_CROSS" />
{{ $t('aiTool.reject') }}
</button>
</div>
@@ -180,6 +180,10 @@ import type { AiToolCallInfo } from '@/api/types'
import ConfirmDialog from './ConfirmDialog.vue'
import ToolResultBody from './ToolResultBody.vue'
// 审批按钮图标 SVG 常量(消除模板 6 处内联重复;v-html 渲染完整 svg 元素)
const ICON_CHECK = '<svg aria-hidden="true" width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><polyline points="20 6 9 17 4 12"/></svg>'
const ICON_CROSS = '<svg aria-hidden="true" width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><line x1="18" y1="6" x2="6" y2="18"/><line x1="6" y1="6" x2="18" y2="18"/></svg>'
const { t } = useI18n()
const props = defineProps<{
@@ -511,6 +515,11 @@ const visibleArgs = computed(() => {
.ai-tool-btn:disabled { opacity: 0.6; cursor: not-allowed; }
.ai-tool-btn--approve:disabled:hover { background: var(--df-success-bg); color: var(--df-success); border-color: var(--df-border); }
.ai-tool-btn--reject:disabled:hover { background: var(--df-danger-bg); color: var(--df-danger); border-color: var(--df-border); }
.ai-tool-btn-icon {
display: inline-flex; align-items: center; justify-content: center;
width: 12px; height: 12px; flex-shrink: 0;
}
.ai-tool-btn-icon :deep(svg) { display: block; }
.ai-tool-btn-spinner {
width: 12px; height: 12px; border: 1.5px solid currentColor; border-top-color: transparent;
border-radius: 50%; display: inline-block; flex-shrink: 0; animation: aiToolBtnSpin 0.7s linear infinite;