From d292c34579152c467f177a70ae99a7dbdfabe6db Mon Sep 17 00:00:00 2001 From: baiyanyun Date: Thu, 11 Jun 2026 17:44:16 +0800 Subject: [PATCH] =?UTF-8?q?=E5=90=B8=E6=94=B6=E6=95=B0=E5=AD=97=E5=91=98?= =?UTF-8?q?=E5=B7=A5=E5=85=A5=E5=8F=A3=E8=B7=AF=E7=94=B1=E7=AD=96=E7=95=A5?= =?UTF-8?q?=E4=B8=8B=E5=8F=91?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../embedded/sgrobot-digital/src/lib/api.ts | 7 + .../src/lib/qimingclawAdapter.ts | 222 ++++++++++++- .../src/pages/digital/CommandDeck.tsx | 41 ++- .../embedded/sgrobot-digital/src/types/api.ts | 34 ++ .../sgrobot-digital/assets/index-BUX1PxSf.js | 184 +++++++++++ .../sgrobot-digital/assets/index-DxdWKD94.js | 184 ----------- .../public/sgrobot-digital/index.html | 2 +- .../scripts/check-digital-employee.js | 50 +++ .../main/ipc/digitalEmployeeHandlers.test.ts | 24 ++ .../src/main/ipc/digitalEmployeeHandlers.ts | 10 + .../digitalEmployee/stateService.test.ts | 90 ++++++ .../services/digitalEmployee/stateService.ts | 298 ++++++++++++++++++ .../digitalEmployee/syncService.test.ts | 78 +++++ .../services/digitalEmployee/syncService.ts | 150 ++++++++- .../src/preload/webviewPerfBridge.ts | 6 + ...ital-employee-frontend-integration-plan.md | 5 +- 16 files changed, 1195 insertions(+), 190 deletions(-) create mode 100644 qimingclaw/crates/agent-electron-client/public/sgrobot-digital/assets/index-BUX1PxSf.js delete mode 100644 qimingclaw/crates/agent-electron-client/public/sgrobot-digital/assets/index-DxdWKD94.js diff --git a/qimingclaw/crates/agent-electron-client/embedded/sgrobot-digital/src/lib/api.ts b/qimingclaw/crates/agent-electron-client/embedded/sgrobot-digital/src/lib/api.ts index 0667367d..fa0755a4 100644 --- a/qimingclaw/crates/agent-electron-client/embedded/sgrobot-digital/src/lib/api.ts +++ b/qimingclaw/crates/agent-electron-client/embedded/sgrobot-digital/src/lib/api.ts @@ -29,6 +29,7 @@ import type { SkillSpec, CreatePlanRequest, RouteDecisionTimelineResponse, + RouteDecisionPolicyPullResult, IngressRouteRequest, IngressRouteResponse, GovernanceCommandRequest, @@ -631,6 +632,12 @@ export function pullRiskApprovalPolicy(): Promise<{ ok: boolean; skipped?: boole }); } +export function pullRouteDecisionPolicy(): Promise { + return apiFetch('/api/route-decision/policy/pull', { + method: 'POST', + }); +} + export function getPlanSteps(params: Record = {}): Promise> { const query = new URLSearchParams(); Object.entries(params).forEach(([key, value]) => { diff --git a/qimingclaw/crates/agent-electron-client/embedded/sgrobot-digital/src/lib/qimingclawAdapter.ts b/qimingclaw/crates/agent-electron-client/embedded/sgrobot-digital/src/lib/qimingclawAdapter.ts index 2f141f68..419fb014 100644 --- a/qimingclaw/crates/agent-electron-client/embedded/sgrobot-digital/src/lib/qimingclawAdapter.ts +++ b/qimingclaw/crates/agent-electron-client/embedded/sgrobot-digital/src/lib/qimingclawAdapter.ts @@ -38,6 +38,8 @@ import type { PlanStepDispatchPolicy, PlanStepDispatchPolicyPullResult, PlanStepSummary, + RouteDecisionPolicy, + RouteDecisionPolicyPullResult, RouteDecisionRecord, RouteDecisionTimelineResponse, SkillSpec, @@ -59,6 +61,8 @@ declare global { pullApprovalUpdates?: () => Promise<{ ok?: boolean; applied?: number; ignored?: number; skipped?: boolean; error?: string | null }>; submitApprovalDecision?: (input: Record) => Promise<{ ok?: boolean; error?: string | null }>; evaluateRiskPolicy?: (input: QimingclawRiskPolicyInput) => Promise; + pullRouteDecisionPolicy?: () => Promise; + evaluateRouteDecisionPolicy?: (input: QimingclawRouteDecisionPolicyInput) => Promise; getPlanTemplates?: () => Promise; listMemories?: (options?: { query?: string; category?: string; limit?: number }) => Promise; addMemory?: (input: QimingclawMemoryUpsertInput) => Promise; @@ -100,6 +104,7 @@ interface AdapterState { notificationPreferences?: NotificationPreferences; approvalSubscriptionPreferences?: ApprovalSubscriptionPreferences; planStepDispatchPolicy?: PlanStepDispatchPolicy; + routeDecisionPolicy?: RouteDecisionPolicy; consoleRole?: string; rolePreferences?: Record; profile: { @@ -360,6 +365,10 @@ interface QimingclawRouteDecisionInput { createsTaskRun?: boolean; approvalMode?: string | null; policyResult?: string | null; + policySource?: string | null; + blockedReason?: string | null; + approvalId?: string | null; + matchedIntent?: string | null; entrypoint?: string | null; actor?: string | null; message?: string | null; @@ -367,6 +376,38 @@ interface QimingclawRouteDecisionInput { recordedAt?: string | null; } +interface QimingclawRouteDecisionPolicyInput { + routeKind?: string | null; + intentKind?: string | null; + reasonCodes?: string[] | null; + candidateSkills?: string[] | null; + contextRefs?: Record | null; + correlationId?: string | null; + idempotencyKey?: string | null; + entrypoint?: string | null; + actor?: string | null; + source?: string | null; + payload?: Record | null; + occurredAt?: string | null; +} + +interface QimingclawRouteDecisionPolicyResult { + ok?: boolean; + decision?: 'allowed' | 'approval_required' | 'blocked' | string; + status?: 'allowed' | 'approval_required' | 'blocked' | string; + policy_result?: 'accepted' | 'approval_required' | 'blocked' | string; + route_kind?: string; + intent_kind?: string; + reason_codes?: string[]; + matched_intent?: string | null; + blocked_reason?: string | null; + approval_id?: string | null; + event_id?: string | null; + policy_source?: string | null; + policy?: RouteDecisionPolicy; + error?: string | null; +} + interface QimingclawRiskPolicyInput { actionKind?: string | null; actionLabel?: string | null; @@ -1022,6 +1063,9 @@ export async function handleQimingclawDigitalApi( if (method === 'POST' && pathname === '/api/risk-approval/policy/pull') { return await pullBridgeRiskApprovalPolicy() as T; } + if (method === 'POST' && pathname === '/api/route-decision/policy/pull') { + return await pullBridgeRouteDecisionPolicy() as T; + } if (method === 'POST' && pathname === '/api/approval/updates/pull') { return await pullBridgeApprovalUpdates() as T; } @@ -1045,6 +1089,7 @@ function defaultState(): AdapterState { notificationPreferences: defaultNotificationPreferences(), approvalSubscriptionPreferences: defaultApprovalSubscriptionPreferences(), planStepDispatchPolicy: defaultPlanStepDispatchPolicy(), + routeDecisionPolicy: defaultRouteDecisionPolicy(), consoleRole: 'sales', rolePreferences: {}, profile: { @@ -1072,6 +1117,7 @@ function normalizeAdapterState(value: Partial): AdapterState { notificationPreferences: notificationPreferences(value), approvalSubscriptionPreferences: approvalSubscriptionPreferences(value), planStepDispatchPolicy: planStepDispatchPolicy(value), + routeDecisionPolicy: routeDecisionPolicy(value), }; } @@ -1177,6 +1223,27 @@ async function pullBridgeRiskApprovalPolicy(): Promise<{ ok: boolean; skipped?: } } +async function pullBridgeRouteDecisionPolicy(): Promise { + const fallbackPolicy = routeDecisionPolicy((await readBridgeUiState()) ?? readState()); + const bridge = window.QimingClawBridge?.digital; + if (!bridge?.pullRouteDecisionPolicy) { + return { ok: false, policy: fallbackPolicy, error: 'qimingclaw_bridge_unavailable' }; + } + try { + const result = await bridge.pullRouteDecisionPolicy(); + return { + ok: result?.ok !== false, + skipped: result?.skipped, + endpoint: result?.endpoint ?? null, + pulled_at: result?.pulled_at ?? null, + policy: routeDecisionPolicy({ routeDecisionPolicy: result?.policy ?? fallbackPolicy }), + error: result?.error ?? null, + }; + } catch (error) { + return { ok: false, policy: fallbackPolicy, error: error instanceof Error ? error.message : String(error) }; + } +} + async function pullBridgeApprovalUpdates(): Promise<{ ok: boolean; applied?: number; ignored?: number; skipped?: boolean; error?: string | null }> { const bridge = window.QimingClawBridge?.digital; if (!bridge?.pullApprovalUpdates) { @@ -1223,6 +1290,39 @@ async function evaluateBridgeRiskPolicy(input: QimingclawRiskPolicyInput): Promi } } +async function evaluateBridgeRouteDecisionPolicy(input: QimingclawRouteDecisionPolicyInput): Promise { + const bridge = window.QimingClawBridge?.digital; + if (!bridge?.evaluateRouteDecisionPolicy) { + return { + ok: true, + decision: 'allowed', + status: 'allowed', + policy_result: 'accepted', + route_kind: input.routeKind || 'ChatOnly', + intent_kind: normalizeRouteIntentKind(input.intentKind), + reason_codes: ['qimingclaw_bridge_unavailable'], + policy_source: 'local', + policy: defaultRouteDecisionPolicy(), + }; + } + try { + return await bridge.evaluateRouteDecisionPolicy(input); + } catch (error) { + return { + ok: true, + decision: 'allowed', + status: 'allowed', + policy_result: 'accepted', + route_kind: input.routeKind || 'ChatOnly', + intent_kind: normalizeRouteIntentKind(input.intentKind), + reason_codes: ['route_policy_evaluation_failed'], + policy_source: 'local', + policy: defaultRouteDecisionPolicy(), + error: error instanceof Error ? error.message : String(error), + }; + } +} + function riskPolicyBlockedResponse(result: QimingclawRiskPolicyResult): Record | null { if (result.decision === 'allowed') return null; return { @@ -3097,6 +3197,10 @@ function routeDecisionFromRuntimePayload(payload: Record, event creates_task_run: Boolean(decision.creates_task_run ?? decision.createsTaskRun), approval_mode: stringValue(decision.approval_mode ?? decision.approvalMode) || null, policy_result: stringValue(decision.policy_result ?? decision.policyResult) || 'accepted', + policy_source: stringValue(decision.policy_source ?? decision.policySource) || null, + blocked_reason: stringValue(decision.blocked_reason ?? decision.blockedReason) || null, + approval_id: stringValue(decision.approval_id ?? decision.approvalId) || null, + matched_intent: stringValue(decision.matched_intent ?? decision.matchedIntent) || null, recorded_at: stringValue(decision.recorded_at ?? decision.recordedAt) || event.occurredAt, entrypoint: stringValue(decision.entrypoint) || null, actor: stringValue(decision.actor) || null, @@ -3104,6 +3208,7 @@ function routeDecisionFromRuntimePayload(payload: Record, event } function routeDecisionAttentionLevel(decision: RouteDecisionRecord): 'info' | 'action' | 'warn' | 'error' { + if (decision.policy_result === 'approval_required') return 'action'; if (decision.policy_result && decision.policy_result !== 'accepted') return 'error'; if (decision.reason_codes.includes('candidate_skills_disabled') || decision.reason_codes.includes('route_action_missing_context')) return 'warn'; if (decision.created_command_id) return 'action'; @@ -3113,6 +3218,7 @@ function routeDecisionAttentionLevel(decision: RouteDecisionRecord): 'info' | 'a function routeDecisionStatusLabel(decision: RouteDecisionRecord): string { const attentionLevel = routeDecisionAttentionLevel(decision); if (attentionLevel === 'error') return '策略拒绝'; + if (decision.policy_result === 'approval_required') return '待审批'; if (attentionLevel === 'warn') return '需要关注'; if (decision.created_command_id) return '已映射命令'; return '已记录'; @@ -3121,10 +3227,16 @@ function routeDecisionStatusLabel(decision: RouteDecisionRecord): string { function buildRouteDecisionSummary(snapshot: QimingclawSnapshot | null): DigitalEmployeeRouteSummary { const rows = routeDecisionRows(snapshot); const latest = rows[0] ?? null; + const policy = routeDecisionPolicy(snapshot?.uiState ?? readState()); return { total: rows.length, mapped_count: rows.filter((row) => stringValue(row.created_command_id)).length, attention_count: rows.filter((row) => ['warn', 'error'].includes(stringValue(row.attention_level))).length, + blocked_count: rows.filter((row) => stringValue(row.policy_result) === 'blocked').length, + approval_required_count: rows.filter((row) => stringValue(row.policy_result) === 'approval_required').length, + policy_source: policy.source ?? null, + policy_last_pulled_at: policy.last_pulled_at ?? null, + policy_last_pull_error: policy.last_pull_error ?? null, latest: latest ? { id: stringValue(latest.route_decision_id), @@ -3133,6 +3245,9 @@ function buildRouteDecisionSummary(snapshot: QimingclawSnapshot | null): Digital status_label: stringValue(latest.status_label), attention_level: stringValue(latest.attention_level), created_command_id: stringValue(latest.created_command_id) || null, + policy_source: stringValue(latest.policy_source) || null, + blocked_reason: stringValue(latest.blocked_reason) || null, + approval_id: stringValue(latest.approval_id) || null, recorded_at: stringValue(latest.recorded_at), } : null, @@ -4152,6 +4267,21 @@ function defaultPlanStepDispatchPolicy(): PlanStepDispatchPolicy { }; } +function defaultRouteDecisionPolicy(): RouteDecisionPolicy { + return { + enabled: true, + auto_create_governance_commands: true, + approval_required_intents: [], + blocked_intents: [], + preferred_route_kinds: [], + updated_at: null, + source: 'local', + remote_updated_at: null, + last_pulled_at: null, + last_pull_error: null, + }; +} + function approvalSubscriptionPreferences(state: Partial | null | undefined): ApprovalSubscriptionPreferences { const stored = state?.approvalSubscriptionPreferences; const fallback = defaultApprovalSubscriptionPreferences(); @@ -4188,6 +4318,28 @@ function planStepDispatchPolicy(state: Partial | null | undefined) }; } +function routeDecisionPolicy(state: Partial | null | undefined): RouteDecisionPolicy { + const stored = state?.routeDecisionPolicy; + const fallback = defaultRouteDecisionPolicy(); + if (!stored || typeof stored !== 'object') return fallback; + return { + enabled: stored.enabled !== false, + auto_create_governance_commands: stored.auto_create_governance_commands !== false, + approval_required_intents: uniqueStrings(arrayValue(stored.approval_required_intents).map(normalizeRouteIntentKind)), + blocked_intents: uniqueStrings(arrayValue(stored.blocked_intents).map(normalizeRouteIntentKind)), + preferred_route_kinds: uniqueStrings(arrayValue(stored.preferred_route_kinds)), + updated_at: typeof stored.updated_at === 'string' ? stored.updated_at : fallback.updated_at, + source: stored.source === 'remote' ? 'remote' : 'local', + remote_updated_at: typeof stored.remote_updated_at === 'string' ? stored.remote_updated_at : null, + last_pulled_at: typeof stored.last_pulled_at === 'string' ? stored.last_pulled_at : null, + last_pull_error: typeof stored.last_pull_error === 'string' ? stored.last_pull_error : null, + }; +} + +function normalizeRouteIntentKind(value: unknown): string { + return stringValue(value).trim().toLowerCase().replace(/[^a-z0-9_.:-]+/g, '_') || 'chat'; +} + function notificationPreferences(state: Partial | null | undefined): NotificationPreferences { const stored = state?.notificationPreferences; const fallback = defaultNotificationPreferences(); @@ -5720,11 +5872,50 @@ async function handleIngressRoute( creates_task_run: classification.createsTaskRun, approval_mode: null, policy_result: 'accepted', + policy_source: null, + blocked_reason: null, + approval_id: null, + matched_intent: null, recorded_at: recordedAt, entrypoint: body.entrypoint ?? null, actor: body.actor ?? null, }; - const routeCommand = buildGovernanceCommandForRouteDecision(decision, body, snapshot); + const routePolicy = await evaluateBridgeRouteDecisionPolicy({ + routeKind: decision.route_kind, + intentKind: decision.intent_kind, + reasonCodes: decision.reason_codes, + candidateSkills: decision.candidate_skills, + contextRefs: decision.context_refs, + correlationId, + idempotencyKey: body.idempotency_key, + entrypoint: body.entrypoint ?? null, + actor: body.actor ?? null, + source: 'qimingclaw-ingress-route', + payload: { + text: body.text ?? null, + payload: body.payload ?? null, + attachments: body.attachments ?? [], + }, + occurredAt: recordedAt, + }); + const routePolicyDecision = stringValue(routePolicy.decision) || 'allowed'; + decision.policy_result = routePolicy.policy_result || (routePolicyDecision === 'allowed' ? 'accepted' : routePolicyDecision) || 'accepted'; + decision.policy_source = routePolicy.policy_source ?? routePolicy.policy?.source ?? null; + decision.blocked_reason = routePolicy.blocked_reason ?? null; + decision.approval_id = routePolicy.approval_id ?? null; + decision.matched_intent = routePolicy.matched_intent ?? null; + decision.reason_codes = uniqueStrings([...decision.reason_codes, ...(routePolicy.reason_codes ?? [])]); + const preferredKinds = routePolicy.policy?.preferred_route_kinds ?? []; + if (preferredKinds.length > 0 && !preferredKinds.includes(decision.route_kind)) { + decision.reason_codes = uniqueStrings([...decision.reason_codes, 'route_kind_not_preferred']); + } + const shouldExecuteCommand = routePolicyDecision === 'allowed' && routePolicy.policy?.auto_create_governance_commands !== false; + if (routePolicyDecision === 'allowed' && routePolicy.policy?.auto_create_governance_commands === false) { + decision.reason_codes = uniqueStrings([...decision.reason_codes, 'route_command_auto_create_disabled']); + } + const routeCommand = shouldExecuteCommand + ? buildGovernanceCommandForRouteDecision(decision, body, snapshot) + : { command: null, reasonCode: routePolicyDecision === 'allowed' ? 'route_command_auto_create_disabled' : `route_policy_${decision.policy_result}` }; const command = routeCommand.command ? await handleGovernanceCommand(routeCommand.command, snapshot) : null; @@ -5750,6 +5941,10 @@ async function handleIngressRoute( createsTaskRun: decision.creates_task_run, approvalMode: decision.approval_mode, policyResult: decision.policy_result, + policySource: decision.policy_source, + blockedReason: decision.blocked_reason, + approvalId: decision.approval_id, + matchedIntent: decision.matched_intent, entrypoint: decision.entrypoint, actor: decision.actor, message: `入口路由决策:${decision.route_kind} / ${decision.intent_kind}`, @@ -5763,6 +5958,31 @@ async function handleIngressRoute( : null; const routeDecision = recorded ?? decision; const resultIds = routeCommandResultIds(command); + if (routePolicyDecision !== 'allowed') { + return { + accepted: false, + envelope: { + entrypoint: body.entrypoint ?? 'chat', + text: body.text ?? null, + payload: body.payload ?? null, + attachments: body.attachments ?? [], + context_refs: contextRefs, + }, + response_kind: 'route_decision', + route_decision: routeDecision, + command: null, + created_plan_id: null, + created_schedule_id: null, + created_task_ids: [], + created_run_ids: [], + projection_name: null, + projection_payload: null, + error: 'policy_blocked', + message: routePolicyDecision === 'approval_required' + ? `入口路由需要审批:${routeDecision.route_kind} / ${routeDecision.intent_kind}` + : `入口路由已被策略阻断:${routeDecision.route_kind} / ${routeDecision.intent_kind}`, + }; + } return { accepted: true, envelope: { diff --git a/qimingclaw/crates/agent-electron-client/embedded/sgrobot-digital/src/pages/digital/CommandDeck.tsx b/qimingclaw/crates/agent-electron-client/embedded/sgrobot-digital/src/pages/digital/CommandDeck.tsx index 003c72bc..9c711331 100644 --- a/qimingclaw/crates/agent-electron-client/embedded/sgrobot-digital/src/pages/digital/CommandDeck.tsx +++ b/qimingclaw/crates/agent-electron-client/embedded/sgrobot-digital/src/pages/digital/CommandDeck.tsx @@ -14,6 +14,7 @@ import { pullApprovalUpdates, pullPlanStepDispatchPolicy, pullRiskApprovalPolicy, + pullRouteDecisionPolicy, recordApprovalAction, restartManagedService, } from '@/lib/api'; @@ -1414,6 +1415,28 @@ export default function CommandDeck({ onNavigate }: { onNavigate: (target: Digit } }, [fetchProjection]); + const refreshRouteDecisionPolicy = useCallback(async () => { + setActionBusy('pull_route_decision_policy'); + setActionError(null); + setActionNotice(null); + try { + const result = await pullRouteDecisionPolicy(); + if (!mountedRef.current) return; + await fetchProjection(); + if (!mountedRef.current) return; + const policy = result.policy; + setActionNotice(result.ok + ? `入口路由策略已拉取:阻断 ${policy.blocked_intents.length} 类,审批 ${policy.approval_required_intents.length} 类。` + : `入口路由策略拉取失败,已沿用本地策略:${result.error || '未知错误'}。`); + } catch (error) { + if (mountedRef.current) { + setActionError(error instanceof Error ? error.message : '拉取入口路由策略失败'); + } + } finally { + if (mountedRef.current) setActionBusy(null); + } + }, [fetchProjection]); + const refreshApprovalUpdates = useCallback(async () => { setActionBusy('pull_approval_updates'); setActionError(null); @@ -2103,6 +2126,16 @@ export default function CommandDeck({ onNavigate }: { onNavigate: (target: Digit