feat(governance): 顯示 Agent 可委派版本治理
All checks were successful
CD Pipeline / tests (push) Successful in 1m25s
Code Review / ai-code-review (push) Successful in 13s
CD Pipeline / build-and-deploy (push) Successful in 5m42s
CD Pipeline / post-deploy-checks (push) Successful in 1m37s

This commit is contained in:
Your Name
2026-06-11 18:03:16 +08:00
parent c9d0eb69df
commit 308fd3d80e
15 changed files with 744 additions and 45 deletions

View File

@@ -36,6 +36,8 @@ import { StatusOrb } from '@/components/ui/status-orb'
import {
apiClient,
type AiAgentDeploymentLayoutSnapshot,
type AiAgentHostStatefulVersionInventorySnapshot,
type AiAgentProactiveOperationsContractSnapshot,
type AiProviderRouteMatrixSnapshot,
type AiAgentAutomationBacklogSnapshot,
type AiAgentAutomationInventorySnapshot,
@@ -313,6 +315,8 @@ export function AutomationInventoryTab() {
const [observabilityMatrix, setObservabilityMatrix] = useState<ObservabilityContractMatrixSnapshot | null>(null)
const [providerRouteMatrix, setProviderRouteMatrix] = useState<AiProviderRouteMatrixSnapshot | null>(null)
const [deploymentLayout, setDeploymentLayout] = useState<AiAgentDeploymentLayoutSnapshot | null>(null)
const [proactiveOperations, setProactiveOperations] = useState<AiAgentProactiveOperationsContractSnapshot | null>(null)
const [hostStatefulInventory, setHostStatefulInventory] = useState<AiAgentHostStatefulVersionInventorySnapshot | null>(null)
const [serviceHealthGapMatrix, setServiceHealthGapMatrix] = useState<ServiceHealthGapMatrixSnapshot | null>(null)
const [serviceHealthNotificationPolicy, setServiceHealthNotificationPolicy] = useState<ServiceHealthFailureNotificationPolicySnapshot | null>(null)
const [loading, setLoading] = useState(true)
@@ -332,6 +336,8 @@ export function AutomationInventoryTab() {
apiClient.getObservabilityContractMatrix(),
apiClient.getAiProviderRouteMatrix(),
apiClient.getAiAgentDeploymentLayout(),
apiClient.getAiAgentProactiveOperationsContract(),
apiClient.getAiAgentHostStatefulVersionInventory(),
apiClient.getServiceHealthGapMatrix(),
apiClient.getServiceHealthFailureNotificationPolicy(),
] as const
@@ -350,6 +356,8 @@ export function AutomationInventoryTab() {
observabilityMatrixResult,
providerRouteMatrixResult,
deploymentLayoutResult,
proactiveOperationsResult,
hostStatefulInventoryResult,
serviceHealthGapMatrixResult,
serviceHealthNotificationPolicyResult,
] = results
@@ -365,6 +373,8 @@ export function AutomationInventoryTab() {
setObservabilityMatrix(observabilityMatrixResult.status === 'fulfilled' ? observabilityMatrixResult.value : null)
setProviderRouteMatrix(providerRouteMatrixResult.status === 'fulfilled' ? providerRouteMatrixResult.value : null)
setDeploymentLayout(deploymentLayoutResult.status === 'fulfilled' ? deploymentLayoutResult.value : null)
setProactiveOperations(proactiveOperationsResult.status === 'fulfilled' ? proactiveOperationsResult.value : null)
setHostStatefulInventory(hostStatefulInventoryResult.status === 'fulfilled' ? hostStatefulInventoryResult.value : null)
setServiceHealthGapMatrix(serviceHealthGapMatrixResult.status === 'fulfilled' ? serviceHealthGapMatrixResult.value : null)
setServiceHealthNotificationPolicy(serviceHealthNotificationPolicyResult.status === 'fulfilled' ? serviceHealthNotificationPolicyResult.value : null)
setError([
@@ -378,6 +388,8 @@ export function AutomationInventoryTab() {
observabilityMatrixResult,
providerRouteMatrixResult,
deploymentLayoutResult,
proactiveOperationsResult,
hostStatefulInventoryResult,
serviceHealthGapMatrixResult,
serviceHealthNotificationPolicyResult,
].some(result => result.status === 'rejected'))
@@ -444,6 +456,56 @@ export function AutomationInventoryTab() {
.slice(0, 12)
}, [deploymentLayout])
const visibleDelegableCapabilities = useMemo(() => {
if (!proactiveOperations) return []
const riskPriority = { critical: 0, high: 1, medium: 2, low: 3 } as Record<string, number>
return [...proactiveOperations.delegable_capabilities]
.sort((a, b) => {
const riskLeft = riskPriority[a.risk_tier] ?? 4
const riskRight = riskPriority[b.risk_tier] ?? 4
if (riskLeft !== riskRight) return riskLeft - riskRight
return a.capability_id.localeCompare(b.capability_id)
})
.slice(0, 8)
}, [proactiveOperations])
const visibleVersionDomains = useMemo(() => {
if (!proactiveOperations) return []
const ownerPriority = { openclaw: 0, hermes: 1, nemotron: 2 } as Record<string, number>
return [...proactiveOperations.version_lifecycle_domains]
.sort((a, b) => {
const ownerLeft = ownerPriority[a.primary_owner] ?? 3
const ownerRight = ownerPriority[b.primary_owner] ?? 3
if (ownerLeft !== ownerRight) return ownerLeft - ownerRight
return a.domain_id.localeCompare(b.domain_id)
})
.slice(0, 8)
}, [proactiveOperations])
const visibleHostInventory = useMemo(() => {
if (!hostStatefulInventory) return []
return [...hostStatefulInventory.host_inventory]
.sort((a, b) => {
if (a.maintenance_window_required !== b.maintenance_window_required) {
return a.maintenance_window_required ? -1 : 1
}
return a.host_id.localeCompare(b.host_id)
})
.slice(0, 5)
}, [hostStatefulInventory])
const visibleStatefulServices = useMemo(() => {
if (!hostStatefulInventory) return []
return [...hostStatefulInventory.stateful_services]
.sort((a, b) => {
if (a.backup_required_before_change !== b.backup_required_before_change) {
return a.backup_required_before_change ? -1 : 1
}
return a.service_id.localeCompare(b.service_id)
})
.slice(0, 8)
}, [hostStatefulInventory])
const visibleReadinessRows = useMemo(() => {
if (!backupReadiness) return []
const priority = { blocked: 0, action_required: 1, deferred: 2, ready: 3 } as Record<string, number>
@@ -587,7 +649,7 @@ export function AutomationInventoryTab() {
)
}
if (error || !snapshot || !backlog || !backupTargets || !backupReadiness || !backupPolicy || !offsiteEscrow || !giteaHealth || !observabilityMatrix || !providerRouteMatrix || !deploymentLayout || !serviceHealthGapMatrix || !serviceHealthNotificationPolicy) {
if (error || !snapshot || !backlog || !backupTargets || !backupReadiness || !backupPolicy || !offsiteEscrow || !giteaHealth || !observabilityMatrix || !providerRouteMatrix || !deploymentLayout || !proactiveOperations || !hostStatefulInventory || !serviceHealthGapMatrix || !serviceHealthNotificationPolicy) {
return (
<div style={{ padding: 20 }}>
<GlassCard variant="subtle" padding="lg">
@@ -660,6 +722,29 @@ export function AutomationInventoryTab() {
const deploymentLayoutNemotronTargets = deploymentLayout.rollups.by_primary_agent.nemotron ?? 0
const deploymentLayoutBlockedTargets = deploymentLayout.rollups.blocked_target_ids.length
const deploymentLayoutApprovalTargets = deploymentLayout.rollups.approval_required_target_ids.length
const proactiveOverall = proactiveOperations.program_status.overall_completion_percent
const proactiveDelegableCapabilities = proactiveOperations.rollups.delegable_capability_count
const proactiveApprovalCapabilities = proactiveOperations.rollups.approval_required_capability_count
const proactiveBlockedVersionDomains = proactiveOperations.rollups.blocked_update_domain_ids.length
const proactiveTelegramActionRequired = proactiveOperations.rollups.telegram_action_required_capability_ids.length
const proactiveAutoExecuteAllowed = proactiveOperations.rollups.auto_execute_allowed_count
const hostReadonlyDeniedCount = (
hostStatefulInventory.rollups.ssh_login_allowed_count
+ hostStatefulInventory.rollups.kubectl_command_execution_allowed_count
+ hostStatefulInventory.rollups.apt_upgrade_allowed_count
+ hostStatefulInventory.rollups.k3s_upgrade_allowed_count
+ hostStatefulInventory.rollups.node_drain_allowed_count
+ hostStatefulInventory.rollups.reboot_allowed_count
+ hostStatefulInventory.rollups.stateful_service_restart_allowed_count
+ hostStatefulInventory.rollups.telegram_direct_send_allowed_count
+ hostStatefulInventory.rollups.conversation_transcript_allowed_count
)
const hostRedactionLocked = (
hostStatefulInventory.display_redaction_contract.conversation_transcript_display_allowed === false
&& hostStatefulInventory.display_redaction_contract.redaction_required === true
)
const hostTelegramDirectSendAllowed = hostStatefulInventory.telegram_policy.direct_send_allowed ? 1 : 0
const hostTelegramGatewayWriteAllowed = hostStatefulInventory.telegram_policy.gateway_queue_write_allowed ? 1 : 0
const serviceHealthActions = serviceHealthGapMatrix.rollups.target_ids_requiring_action.length
const serviceHealthStaleGaps = serviceHealthGapMatrix.rollups.stale_endpoint_ids.length
const serviceHealthOperatorReviews = serviceHealthGapMatrix.rollups.health_gap_ids.length
@@ -820,6 +905,22 @@ export function AutomationInventoryTab() {
}
}
const proactiveValueLabel = (group: string, value: string) => {
try {
return t(`proactiveOperations.${group}.${value}` as never)
} catch {
return value
}
}
const hostStatefulValueLabel = (group: string, value: string) => {
try {
return t(`hostStateful.${group}.${value}` as never)
} catch {
return value
}
}
const serviceHealthValueLabel = (value: string) => {
try {
return t(`serviceHealth.values.${value}` as never)
@@ -1109,6 +1210,171 @@ export function AutomationInventoryTab() {
</div>
</GlassCard>
<GlassCard variant="subtle" padding="md">
<div style={{ display: 'flex', flexDirection: 'column', gap: 13, minWidth: 0 }}>
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', gap: 12, flexWrap: 'wrap' }}>
<div style={{ display: 'flex', alignItems: 'center', gap: 7, minWidth: 0 }}>
<HardDrive size={14} style={{ color: '#d97757' }} />
<span style={{ fontFamily: 'Syne, sans-serif', fontSize: 13, fontWeight: 700, color: '#141413' }}>
{t('proactiveOperations.title')}
</span>
</div>
<Chip
value={t('proactiveOperations.source', {
generated: formatDateTime(proactiveOperations.generated_at),
current: proactiveOperations.program_status.current_task_id,
next: proactiveOperations.program_status.next_task_id,
})}
muted
/>
</div>
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(6, minmax(0, 1fr))', gap: 12 }} className="automation-inventory-proactive-kpi-grid">
<MetricCard label={t('proactiveOperations.metrics.overall')} value={`${proactiveOverall}%`} tone="ok" icon={<Gauge size={16} />} />
<MetricCard label={t('proactiveOperations.metrics.capabilities')} value={proactiveDelegableCapabilities} icon={<Boxes size={16} />} />
<MetricCard label={t('proactiveOperations.metrics.approval')} value={proactiveApprovalCapabilities} tone="warn" icon={<Lock size={16} />} />
<MetricCard label={t('proactiveOperations.metrics.blockedDomains')} value={proactiveBlockedVersionDomains} tone="warn" icon={<AlertTriangle size={16} />} />
<MetricCard label={t('proactiveOperations.metrics.telegram')} value={proactiveTelegramActionRequired} tone="warn" icon={<BellRing size={16} />} />
<MetricCard label={t('proactiveOperations.metrics.autoExecute')} value={proactiveAutoExecuteAllowed} tone="ok" icon={<ShieldCheck size={16} />} />
<MetricCard label={t('hostStateful.metrics.hosts')} value={hostStatefulInventory.rollups.host_count} icon={<Server size={16} />} />
<MetricCard label={t('hostStateful.metrics.k3sNodes')} value={hostStatefulInventory.rollups.k3s_node_count} icon={<Route size={16} />} />
<MetricCard label={t('hostStateful.metrics.stateful')} value={hostStatefulInventory.rollups.stateful_service_count} icon={<Database size={16} />} />
<MetricCard label={t('hostStateful.metrics.probes')} value={hostStatefulInventory.rollups.readonly_probe_step_count} icon={<FileText size={16} />} />
<MetricCard label={t('hostStateful.metrics.maintenance')} value={hostStatefulInventory.rollups.maintenance_required_field_count} tone="warn" icon={<ShieldAlert size={16} />} />
<MetricCard label={t('hostStateful.metrics.denied')} value={hostReadonlyDeniedCount} tone="ok" icon={<BellOff size={16} />} />
</div>
<div style={{ display: 'grid', gridTemplateColumns: 'minmax(0, 1.1fr) minmax(0, 0.9fr)', gap: 12 }} className="automation-inventory-proactive-grid">
<div style={{ display: 'flex', flexDirection: 'column', gap: 12, minWidth: 0 }}>
<div style={{ padding: 12, border: '0.5px solid #e0ddd4', borderRadius: 7, background: '#fff', display: 'flex', flexDirection: 'column', gap: 10, minWidth: 0 }}>
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', gap: 10, flexWrap: 'wrap' }}>
<span style={{ fontFamily: 'Syne, sans-serif', fontSize: 13, fontWeight: 700, color: '#141413' }}>
{t('proactiveOperations.capabilitiesTitle')}
</span>
<Chip value={t('proactiveOperations.capabilitiesShown', { count: visibleDelegableCapabilities.length, total: proactiveDelegableCapabilities })} muted />
</div>
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(2, minmax(0, 1fr))', gap: 10 }} className="automation-inventory-capability-grid">
{visibleDelegableCapabilities.map(capability => (
<div key={capability.capability_id} style={{ padding: 11, border: '0.5px solid #eee9dd', borderRadius: 7, background: '#faf9f3', display: 'flex', flexDirection: 'column', gap: 8, minWidth: 0 }}>
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', gap: 8, minWidth: 0 }}>
<span style={{ fontFamily: "'DM Mono', monospace", fontSize: 11, fontWeight: 700, color: '#141413', overflowWrap: 'anywhere' }}>
{capability.capability_id}
</span>
<Chip value={proactiveValueLabel('risks', capability.risk_tier)} muted={capability.risk_tier === 'low'} />
</div>
<span style={{ fontFamily: 'Syne, sans-serif', fontSize: 12, fontWeight: 700, color: '#141413', lineHeight: 1.35 }}>
{capability.display_name}
</span>
<div style={{ display: 'flex', flexWrap: 'wrap', gap: 6, minWidth: 0 }}>
<Chip value={`${t('proactiveOperations.labels.owner')}: ${proactiveValueLabel('agents', capability.primary_owner)}`} />
<Chip value={`${t('proactiveOperations.labels.level')}: ${proactiveValueLabel('autonomyLevels', capability.automation_level)}`} muted />
<Chip value={`${t('proactiveOperations.labels.gate')}: ${capability.approval_gate}`} muted={capability.approval_gate === 'read_only_allowed'} />
<Chip value={`${t('proactiveOperations.labels.outputs')}: ${capability.outputs.length}`} muted />
</div>
<SmallLabel>{t('proactiveOperations.labels.telegram')}</SmallLabel>
<span style={{ fontFamily: "'DM Mono', monospace", fontSize: 10, color: '#87867f', lineHeight: 1.45, overflowWrap: 'anywhere' }}>
{capability.telegram_policy}
</span>
</div>
))}
</div>
</div>
<div style={{ padding: 12, border: '0.5px solid #e0ddd4', borderRadius: 7, background: '#fff', display: 'flex', flexDirection: 'column', gap: 10, minWidth: 0 }}>
<SmallLabel>{t('proactiveOperations.domainsTitle')}</SmallLabel>
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(2, minmax(0, 1fr))', gap: 10 }} className="automation-inventory-domain-grid">
{visibleVersionDomains.map(domain => (
<div key={domain.domain_id} style={{ padding: 10, border: '0.5px solid #eee9dd', borderRadius: 7, background: '#faf9f3', display: 'flex', flexDirection: 'column', gap: 7, minWidth: 0 }}>
<div style={{ display: 'flex', justifyContent: 'space-between', gap: 8, alignItems: 'center', minWidth: 0 }}>
<span style={{ fontFamily: 'Syne, sans-serif', fontSize: 12, fontWeight: 700, color: '#141413', overflowWrap: 'anywhere' }}>
{domain.display_name}
</span>
<Chip value={proactiveValueLabel('agents', domain.primary_owner)} muted />
</div>
<div style={{ display: 'flex', flexWrap: 'wrap', gap: 6 }}>
<Chip value={`${t('proactiveOperations.labels.cadence')}: ${domain.cadence}`} muted />
<Chip value={`${t('proactiveOperations.labels.level')}: ${proactiveValueLabel('autonomyLevels', domain.current_allowed_autonomy)}`} muted />
<Chip value={`${t('proactiveOperations.labels.examples')}: ${domain.tracked_examples.length}`} muted />
</div>
<span style={{ fontFamily: "'DM Mono', monospace", fontSize: 10, color: '#87867f', lineHeight: 1.45, overflowWrap: 'anywhere' }}>
{domain.approval_gate}
</span>
</div>
))}
</div>
</div>
</div>
<div style={{ display: 'flex', flexDirection: 'column', gap: 12, minWidth: 0 }}>
<div style={{ padding: 12, border: '0.5px solid #e0ddd4', borderRadius: 7, background: '#fff', display: 'flex', flexDirection: 'column', gap: 10, minWidth: 0 }}>
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', gap: 10, flexWrap: 'wrap' }}>
<span style={{ fontFamily: 'Syne, sans-serif', fontSize: 13, fontWeight: 700, color: '#141413' }}>
{t('hostStateful.hostListTitle')}
</span>
<Chip value={t('hostStateful.source', {
generated: formatDateTime(hostStatefulInventory.generated_at),
current: hostStatefulInventory.program_status.current_task_id,
next: hostStatefulInventory.program_status.next_task_id,
})} muted />
</div>
{visibleHostInventory.map(host => (
<div key={host.host_id} style={{ padding: 10, border: '0.5px solid #eee9dd', borderRadius: 7, background: '#faf9f3', display: 'flex', flexDirection: 'column', gap: 7, minWidth: 0 }}>
<div style={{ display: 'flex', justifyContent: 'space-between', gap: 8, alignItems: 'center', minWidth: 0 }}>
<span style={{ fontFamily: "'DM Mono', monospace", fontSize: 11, fontWeight: 700, color: '#141413', overflowWrap: 'anywhere' }}>
{host.host_id}
</span>
<Chip value={hostStatefulValueLabel('values', host.version_observation_status)} muted />
</div>
<span style={{ fontFamily: 'Syne, sans-serif', fontSize: 12, fontWeight: 700, color: '#141413', lineHeight: 1.35 }}>
{host.display_name}
</span>
<div style={{ display: 'flex', flexWrap: 'wrap', gap: 6 }}>
<Chip value={`${t('hostStateful.labels.surfaces')}: ${host.primary_surfaces.length}`} muted />
<Chip value={`${t('hostStateful.labels.evidence')}: ${host.next_evidence_needed.length}`} muted />
<Chip value={`${t('hostStateful.labels.blocked')}: ${host.blocked_actions.length}`} muted={host.blocked_actions.length === 0} />
<Chip value={host.maintenance_window_required ? t('hostStateful.labels.maintenanceRequired') : t('hostStateful.labels.maintenanceNotRequired')} />
</div>
</div>
))}
</div>
<div style={{ padding: 12, border: '0.5px solid #e0ddd4', borderRadius: 7, background: '#fff', display: 'flex', flexDirection: 'column', gap: 10, minWidth: 0 }}>
<SmallLabel>{t('hostStateful.serviceListTitle')}</SmallLabel>
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(2, minmax(0, 1fr))', gap: 8 }} className="automation-inventory-stateful-grid">
{visibleStatefulServices.map(service => (
<div key={service.service_id} style={{ padding: 9, border: '0.5px solid #eee9dd', borderRadius: 7, background: '#faf9f3', display: 'flex', flexDirection: 'column', gap: 6, minWidth: 0 }}>
<span style={{ fontFamily: 'Syne, sans-serif', fontSize: 12, fontWeight: 700, color: '#141413' }}>
{service.display_name}
</span>
<div style={{ display: 'flex', flexWrap: 'wrap', gap: 6 }}>
<Chip value={service.service_id} muted />
<Chip value={service.backup_required_before_change ? t('hostStateful.labels.backupRequired') : t('hostStateful.labels.backupNotRequired')} />
<Chip value={`${t('hostStateful.labels.restart')}: ${String(service.restart_authorized)}`} muted />
</div>
</div>
))}
</div>
</div>
<div style={{ padding: 12, border: '0.5px solid #e0ddd4', borderRadius: 7, background: '#fff', display: 'flex', flexDirection: 'column', gap: 8, minWidth: 0 }}>
<SmallLabel>{t('hostStateful.maintenanceTitle')}</SmallLabel>
<div style={{ display: 'flex', flexWrap: 'wrap', gap: 6 }}>
<Chip value={`${t('hostStateful.labels.requiredFields')}: ${hostStatefulInventory.maintenance_window_approval_package.required_fields.length}`} />
<Chip value={`${t('hostStateful.labels.smoke')}: ${hostStatefulInventory.maintenance_window_approval_package.minimum_smoke_plan.length}`} muted />
<Chip value={`${t('hostStateful.labels.forbiddenFields')}: ${hostStatefulInventory.maintenance_window_approval_package.forbidden_fields.length}`} muted />
<Chip value={`${t('hostStateful.labels.probeSteps')}: ${hostStatefulInventory.readonly_probe_plan.length}`} muted />
</div>
<div style={{ display: 'flex', flexWrap: 'wrap', gap: 6 }}>
<Chip value={`${t('hostStateful.labels.telegramDirect')}: ${hostTelegramDirectSendAllowed}`} muted />
<Chip value={`${t('hostStateful.labels.telegramQueue')}: ${hostTelegramGatewayWriteAllowed}`} muted />
<Chip value={hostRedactionLocked ? t('hostStateful.labels.redactionLocked') : t('hostStateful.labels.redactionReview')} />
</div>
</div>
</div>
</div>
</div>
</GlassCard>
<GlassCard variant="subtle" padding="md">
<div style={{ display: 'flex', flexDirection: 'column', gap: 13, minWidth: 0 }}>
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', gap: 12, flexWrap: 'wrap' }}>