feat: Phase 2 - Orchestrator workflow node + tool-level human approval

2.1 Orchestrator in workflow:
- New run_orchestrator_node() in workflow_integration.py loads agents from DB,
  supports route/sequential/debate/pipeline modes
- New 'orchestrator' node type in workflow_engine.py execute_node dispatch

2.2 Tool-level human approval:
- AgentToolConfig extended with require_approval, approval_timeout_ms,
  approval_default fields
- New ApprovalManager (approval_manager.py) with asyncio.Event-based
  create/wait_for_decision/resolve pattern
- AgentRuntime run() and run_stream() intercept tool execution,
  wait for approval decision before executing
- New POST /api/v1/approval/{id}/resolve REST endpoint
- Frontend: approval_required SSE event handling, approval dialog UI
  with approve/deny/skip buttons

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
renjianbo
2026-05-04 23:17:59 +08:00
parent d895922438
commit f3cb35c460
8 changed files with 437 additions and 1 deletions

View File

@@ -204,6 +204,22 @@
<el-button @click="showOrchestrateEditor = false">关闭</el-button>
</template>
</el-dialog>
<!-- 工具审批对话框 -->
<el-dialog v-model="showApprovalDialog" title="工具执行审批" width="480px" :close-on-click-modal="false">
<div style="margin-bottom: 12px">
<el-tag type="warning" size="large">{{ approvalToolName }}</el-tag>
<span style="margin-left: 8px; color: #666">需要人工审批才能执行</span>
</div>
<div v-if="Object.keys(approvalArgs).length > 0" style="background: #f5f7fa; padding: 12px; border-radius: 6px; max-height: 200px; overflow-y: auto">
<pre style="margin: 0; font-size: 13px; white-space: pre-wrap; word-break: break-all">{{ JSON.stringify(approvalArgs, null, 2) }}</pre>
</div>
<template #footer>
<el-button @click="resolveApproval('denied')">拒绝</el-button>
<el-button @click="resolveApproval('skip')">跳过</el-button>
<el-button type="primary" @click="resolveApproval('approved')">批准执行</el-button>
</template>
</el-dialog>
</MainLayout>
</template>
@@ -249,6 +265,18 @@ interface ChatState {
orchestrateAgents: OrchestrateAgentForm[]
}
async function resolveApproval(decision: string) {
try {
await api.post(`/api/v1/approval/${approvalId.value}/resolve`, { decision })
} catch {
// 审批已超时或已处理,忽略
}
showApprovalDialog.value = false
approvalId.value = ''
approvalToolName.value = ''
approvalArgs.value = {}
}
function saveState() {
try {
const state: ChatState = {
@@ -285,6 +313,11 @@ const messages = ref<Record<string, ChatMessage[]>>({})
const inputMessage = ref('')
const loading = ref(false)
const streamingActive = ref(false)
// 工具审批状态
const showApprovalDialog = ref(false)
const approvalId = ref('')
const approvalToolName = ref('')
const approvalArgs = ref<Record<string, any>>({})
const messagesRef = ref<HTMLElement | null>(null)
const sessionId = ref<Record<string, string>>({})
const agent = ref<Agent | null>(null)
@@ -486,6 +519,11 @@ async function sendMessage() {
tool_name: data.name,
tool_result: data.result,
})
} else if (eventType === 'approval_required') {
approvalId.value = data.approval_id || ''
approvalToolName.value = data.tool_name || ''
approvalArgs.value = data.args || {}
showApprovalDialog.value = true
} else if (eventType === 'final') {
currentMsg.content = data.content || ''
currentMsg.iterations = data.iterations_used || 0