吸收数字员工入口路由干预回写

This commit is contained in:
baiyanyun
2026-06-11 18:31:41 +08:00
parent d292c34579
commit 4c862ef4b7
16 changed files with 1151 additions and 191 deletions

View File

@@ -8,6 +8,7 @@ import {
dispatchPlanReadySteps,
getDigitalEmployeeWorkday,
getPlanStepGraph,
getRouteInterventions,
getSchedulerJobs,
governanceCommand,
postDigitalEmployeeWorkdayAction,
@@ -16,6 +17,7 @@ import {
pullRiskApprovalPolicy,
pullRouteDecisionPolicy,
recordApprovalAction,
decideRouteIntervention,
restartManagedService,
} from '@/lib/api';
import { isTauri } from '@/lib/tauri';
@@ -33,6 +35,7 @@ import type {
GovernanceCommandKind,
PlanStepBusinessRow,
PlanStepDependencyDetail,
RouteInterventionRecord,
} from '@/types/api';
import type { DigitalNavigationTarget } from './navigation';
@@ -990,6 +993,8 @@ export default function CommandDeck({ onNavigate }: { onNavigate: (target: Digit
const [blockedPlanSteps, setBlockedPlanSteps] = useState<PlanStepBusinessRow[]>([]);
const [planStepGraphRows, setPlanStepGraphRows] = useState<PlanStepBusinessRow[]>([]);
const [blockedPlanStepError, setBlockedPlanStepError] = useState<string | null>(null);
const [routeInterventions, setRouteInterventions] = useState<RouteInterventionRecord[]>([]);
const [routeInterventionError, setRouteInterventionError] = useState<string | null>(null);
const [blockedInputTarget, setBlockedInputTarget] = useState<BlockedPlanStepInputTarget | null>(null);
const [blockedInputDraft, setBlockedInputDraft] = useState('');
const [profileForm, setProfileForm] = useState({
@@ -1076,7 +1081,19 @@ export default function CommandDeck({ onNavigate }: { onNavigate: (target: Digit
setBlockedPlanStepError(error instanceof Error ? error.message : '阻塞步骤加载失败');
});
await Promise.allSettled([projectionPromise, jobsPromise, syncStatusPromise, planStepGraphPromise]);
const routeInterventionPromise = getRouteInterventions()
.then((response) => {
if (!mountedRef.current) return;
setRouteInterventions(response.interventions.slice(0, 6));
setRouteInterventionError(null);
})
.catch((error: unknown) => {
if (!mountedRef.current) return;
setRouteInterventions([]);
setRouteInterventionError(error instanceof Error ? error.message : '路由干预加载失败');
});
await Promise.allSettled([projectionPromise, jobsPromise, syncStatusPromise, planStepGraphPromise, routeInterventionPromise]);
if (mountedRef.current) {
setProjectionLoading(false);
}
@@ -1458,6 +1475,30 @@ export default function CommandDeck({ onNavigate }: { onNavigate: (target: Digit
}
}, [fetchProjection]);
const resolveRouteIntervention = useCallback(async (intervention: RouteInterventionRecord, decision: 'approved' | 'rejected') => {
const actionKey = `route-intervention:${intervention.route_decision_id}:${decision}`;
setActionBusy(actionKey);
setActionError(null);
setActionNotice(null);
try {
const result = await decideRouteIntervention(intervention.route_decision_id, {
approval_id: intervention.approval_id,
decision,
actor: 'digital_employee_operator',
});
if (!mountedRef.current) return;
await fetchProjection();
if (!mountedRef.current) return;
setActionNotice(result.message || (decision === 'approved' ? '入口路由干预已通过。' : '入口路由干预已拒绝。'));
} catch (error) {
if (mountedRef.current) {
setActionError(error instanceof Error ? error.message : '处理入口路由干预失败');
}
} finally {
if (mountedRef.current) setActionBusy(null);
}
}, [fetchProjection]);
const copySyncFailureDiagnostic = useCallback(async (failure: ManagementSyncFailure) => {
const diagnostic = buildSyncFailureDiagnostic(failure, managementSyncStatus);
try {
@@ -2177,6 +2218,45 @@ export default function CommandDeck({ onNavigate }: { onNavigate: (target: Digit
{actionError || actionNotice || planDataError || workdayProjection?.demo_reason}
</div>
)}
{(routeInterventions.length > 0 || routeInterventionError) && (
<div className="de-management-sync-failures" aria-label="入口路由干预" style={{ marginBottom: 10 }}>
{routeInterventionError && <p className="de-empty-text">{routeInterventionError}</p>}
{routeInterventions.slice(0, 4).map((intervention) => {
const status = intervention.intervention_status;
const pending = !intervention.resolved_at && ['pending', 'blocked', 'approved'].includes(status);
const approveKey = `route-intervention:${intervention.route_decision_id}:approved`;
const rejectKey = `route-intervention:${intervention.route_decision_id}:rejected`;
return (
<div key={intervention.route_decision_id} className="de-management-sync-failure" title={intervention.blocked_reason || intervention.route_decision_id}>
<span>{intervention.entrypoint || 'route'}</span>
<strong>{intervention.route_kind} / {intervention.intent_kind}</strong>
<em>{status}</em>
<small>{intervention.blocked_reason || intervention.matched_intent || intervention.approval_id || intervention.route_decision_id}</small>
{pending && (
<div className="de-task-agent-actions">
<button
type="button"
className="de-btn-secondary de-btn-sm"
disabled={actionBusy === approveKey}
onClick={() => { void resolveRouteIntervention(intervention, 'approved'); }}
>
{status === 'approved' ? '恢复' : '通过'}
</button>
<button
type="button"
className="de-btn-secondary de-btn-sm"
disabled={actionBusy === rejectKey}
onClick={() => { void resolveRouteIntervention(intervention, 'rejected'); }}
>
</button>
</div>
)}
</div>
);
})}
</div>
)}
{managedServiceIssues.length > 0 && (
<div className="de-management-sync-failures" aria-label="服务状态处理" style={{ marginBottom: 10 }}>
{managedServiceIssues.map((service) => {
@@ -2360,9 +2440,12 @@ export default function CommandDeck({ onNavigate }: { onNavigate: (target: Digit
<span className={routeSummary.attention_count > 0 ? 'is-warning' : ''}><em></em><strong>{routeSummary.attention_count}</strong></span>
{(routeSummary.blocked_count ?? 0) > 0 && <span className="is-warning"><em></em><strong>{routeSummary.blocked_count}</strong></span>}
{(routeSummary.approval_required_count ?? 0) > 0 && <span className="is-warning"><em></em><strong>{routeSummary.approval_required_count}</strong></span>}
{(routeSummary.pending_intervention_count ?? 0) > 0 && <span className="is-warning"><em></em><strong>{routeSummary.pending_intervention_count}</strong></span>}
{(routeSummary.restored_count ?? 0) > 0 && <span><em></em><strong>{routeSummary.restored_count}</strong></span>}
<small>
{latestRoute ? `${latestRoute.route_kind} / ${latestRoute.intent_kind} · ${latestRoute.status_label}` : '暂无路由决策'}
{routeSummary.policy_source ? ` · 策略${routeSummary.policy_source === 'remote' ? '远端' : '本地'}` : ''}
{routeSummary.latest_intervention_status ? ` · 干预${routeSummary.latest_intervention_status}` : ''}
{routeSummary.policy_last_pull_error ? ` · 拉取失败` : ''}
</small>
</div>