feat(governance): 新增 AI Agent 活動動畫
All checks were successful
Code Review / ai-code-review (push) Successful in 14s
CD Pipeline / tests (push) Successful in 1m30s
CD Pipeline / build-and-deploy (push) Successful in 4m3s
CD Pipeline / post-deploy-checks (push) Successful in 1m59s

This commit is contained in:
Your Name
2026-06-14 09:42:37 +08:00
parent f2fa845464
commit a3de0ffb82
6 changed files with 570 additions and 0 deletions

View File

@@ -3024,11 +3024,27 @@
"error": "無法載入待辦佇列",
"retry": "重試"
},
"agentActivity": {
"automationTitle": "AI Agent 協作脈衝",
"automationSubtitle": "OpenClaw 仲裁Hermes 記憶NemoTron 執行;目前只讀讀回,正式寫入維持 0。",
"marketTitle": "Agent 市場觀測脈衝",
"marketSubtitle": "外部候選先進入觀測與評分Agent 只產生證據與批准包,不自動替換。",
"status": "狀態",
"footnote": "原創動態參考官方 Agent 視覺語言,不使用官方商標或素材。"
},
"agentMarket": {
"title": "Agent 市場治理",
"generatedAt": "產生時間",
"error": "無法載入 Agent 市場治理快照",
"retry": "重試",
"agentActivity": {
"metrics": {
"candidates": "候選數",
"prescreenReady": "可預篩",
"blocked": "已阻擋",
"approvals": "Runtime 批准"
}
},
"metrics": {
"candidates": "候選數",
"sources": "來源數",
@@ -3141,6 +3157,14 @@
"readOnly": "只讀模式",
"error": "無法載入自動化盤點快照",
"retry": "重試",
"agentActivity": {
"metrics": {
"backlog": "待辦進度",
"readback": "讀回關卡",
"gates": "人工 gate",
"liveWrites": "正式寫入"
}
},
"metrics": {
"progress": "整體進度",
"assets": "資產數",

View File

@@ -3024,11 +3024,27 @@
"error": "無法載入待辦佇列",
"retry": "重試"
},
"agentActivity": {
"automationTitle": "AI Agent 協作脈衝",
"automationSubtitle": "OpenClaw 仲裁Hermes 記憶NemoTron 執行;目前只讀讀回,正式寫入維持 0。",
"marketTitle": "Agent 市場觀測脈衝",
"marketSubtitle": "外部候選先進入觀測與評分Agent 只產生證據與批准包,不自動替換。",
"status": "狀態",
"footnote": "原創動態參考官方 Agent 視覺語言,不使用官方商標或素材。"
},
"agentMarket": {
"title": "Agent 市場治理",
"generatedAt": "產生時間",
"error": "無法載入 Agent 市場治理快照",
"retry": "重試",
"agentActivity": {
"metrics": {
"candidates": "候選數",
"prescreenReady": "可預篩",
"blocked": "已阻擋",
"approvals": "Runtime 批准"
}
},
"metrics": {
"candidates": "候選數",
"sources": "來源數",
@@ -3141,6 +3157,14 @@
"readOnly": "只讀模式",
"error": "無法載入自動化盤點快照",
"retry": "重試",
"agentActivity": {
"metrics": {
"backlog": "待辦進度",
"readback": "讀回關卡",
"gates": "人工 gate",
"liveWrites": "正式寫入"
}
},
"metrics": {
"progress": "整體進度",
"assets": "資產數",

View File

@@ -13,6 +13,7 @@ import { AlertTriangle, Ban, CalendarClock, CheckCircle2, ListChecks, Lock, Refr
import { useTranslations } from 'next-intl'
import { GlassCard } from '@/components/ui/glass-card'
import { StatusOrb } from '@/components/ui/status-orb'
import { AgentActivityConstellation } from '@/components/governance/agent-activity-constellation'
import { apiClient, type AgentMarketGovernanceSnapshot } from '@/lib/api-client'
// =============================================================================
@@ -306,6 +307,17 @@ export function AgentMarketTab() {
</div>
</GlassCard>
<AgentActivityConstellation
mode="market"
statusValue={snapshot.current_decision}
metrics={[
{ label: t('agentActivity.metrics.candidates'), value: summary.candidate_count, tone: 'neutral' },
{ label: t('agentActivity.metrics.prescreenReady'), value: summary.eligible_for_market_scorecard_prescreen, tone: 'ok' },
{ label: t('agentActivity.metrics.blocked'), value: summary.blocked_from_integration, tone: summary.blocked_from_integration > 0 ? 'warn' : 'ok' },
{ label: t('agentActivity.metrics.approvals'), value: allApprovals, tone: allApprovals === 0 ? 'ok' : 'warn' },
]}
/>
<div style={{
display: 'grid',
gridTemplateColumns: 'repeat(4, minmax(0, 1fr))',

View File

@@ -34,6 +34,7 @@ import {
import { useTranslations } from 'next-intl'
import { GlassCard } from '@/components/ui/glass-card'
import { StatusOrb } from '@/components/ui/status-orb'
import { AgentActivityConstellation } from '@/components/governance/agent-activity-constellation'
import {
apiClient,
type AiAgentCandidateOperationDryRunEvidenceSnapshot,
@@ -3576,6 +3577,17 @@ export function AutomationInventoryTab() {
</div>
</GlassCard>
<AgentActivityConstellation
mode="automation"
statusValue={`${snapshot.program_status.current_task_id}${snapshot.program_status.next_task_id}`}
metrics={[
{ label: t('agentActivity.metrics.backlog'), value: `${backlogProgressPercent}%`, tone: 'ok' },
{ label: t('agentActivity.metrics.readback'), value: `${resultCaptureReleaseReadbackOverall}%`, tone: 'ok' },
{ label: t('agentActivity.metrics.gates'), value: explicitApprovalTaskCount, tone: explicitApprovalTaskCount > 0 ? 'warn' : 'ok' },
{ label: t('agentActivity.metrics.liveWrites'), value: resultCaptureReleaseReadbackLiveWrites, tone: resultCaptureReleaseReadbackLiveWrites === 0 ? 'ok' : 'danger' },
]}
/>
<div style={{ display: 'grid', gridTemplateColumns: 'minmax(0, 1.05fr) minmax(0, 0.95fr)', gap: 12 }} className="automation-inventory-command-grid">
<GlassCard variant="subtle" padding="md">
<div style={{ display: 'flex', flexDirection: 'column', gap: 13, minWidth: 0 }}>

View File

@@ -0,0 +1,472 @@
'use client'
import { BrainCircuit, Cpu, RadioTower, Route, ShieldCheck } from 'lucide-react'
import type { CSSProperties } from 'react'
import { useTranslations } from 'next-intl'
import { GlassCard } from '@/components/ui/glass-card'
type AgentActivityMode = 'automation' | 'market'
type AgentActivityTone = 'ok' | 'warn' | 'danger' | 'neutral'
export type AgentActivityMetric = {
label: string
value: string | number
tone?: AgentActivityTone
}
export type AgentActivityConstellationProps = {
mode: AgentActivityMode
statusValue: string
metrics: AgentActivityMetric[]
}
function toneColor(tone: AgentActivityTone = 'neutral') {
if (tone === 'ok') return '#22C55E'
if (tone === 'warn') return '#F59E0B'
if (tone === 'danger') return '#EF4444'
return '#4A90D9'
}
export function AgentActivityConstellation({
mode,
statusValue,
metrics,
}: AgentActivityConstellationProps) {
const t = useTranslations('governance.agentActivity')
const isMarket = mode === 'market'
const title = isMarket ? t('marketTitle') : t('automationTitle')
const subtitle = isMarket ? t('marketSubtitle') : t('automationSubtitle')
return (
<GlassCard variant="subtle" padding="md" className="agent-activity-constellation min-w-0">
<div className="agent-activity-layout">
<div
className={`agent-activity-stage ${isMarket ? 'is-market' : 'is-automation'}`}
aria-label={title}
role="img"
>
<div className="agent-activity-orbit agent-activity-orbit-outer" />
<div className="agent-activity-orbit agent-activity-orbit-inner" />
<div className="agent-activity-bus">
<RadioTower size={18} strokeWidth={1.8} />
</div>
<div className="agent-activity-node agent-activity-node-openclaw">
<ShieldCheck size={17} strokeWidth={1.9} />
<span>OpenClaw</span>
</div>
<div className="agent-activity-node agent-activity-node-hermes">
<BrainCircuit size={17} strokeWidth={1.9} />
<span>Hermes</span>
</div>
<div className="agent-activity-node agent-activity-node-nemotron">
<Cpu size={17} strokeWidth={1.9} />
<span>NemoTron</span>
</div>
<div className="agent-activity-packet agent-activity-packet-a" />
<div className="agent-activity-packet agent-activity-packet-b" />
<div className="agent-activity-packet agent-activity-packet-c" />
</div>
<div className="agent-activity-copy">
<div className="agent-activity-heading">
<Route size={14} strokeWidth={1.8} />
<span>{title}</span>
</div>
<p>{subtitle}</p>
<div className="agent-activity-status">
<span>{t('status')}</span>
<strong>{statusValue}</strong>
</div>
<div className="agent-activity-metrics">
{metrics.map((metric) => (
<div
key={`${metric.label}-${metric.value}`}
className="agent-activity-metric"
style={{ '--metric-color': toneColor(metric.tone) } as CSSProperties}
>
<span>{metric.label}</span>
<strong>{metric.value}</strong>
</div>
))}
</div>
<div className="agent-activity-footnote">{t('footnote')}</div>
</div>
</div>
<style jsx>{`
.agent-activity-layout {
display: grid;
grid-template-columns: minmax(220px, 0.82fr) minmax(0, 1.18fr);
gap: 16px;
align-items: center;
min-width: 0;
}
.agent-activity-stage {
position: relative;
min-height: 190px;
border-radius: 8px;
border: 0.5px solid rgba(20, 20, 19, 0.06);
overflow: hidden;
background:
linear-gradient(90deg, rgba(20, 20, 19, 0.035) 1px, transparent 1px),
linear-gradient(0deg, rgba(20, 20, 19, 0.035) 1px, transparent 1px),
radial-gradient(circle at 50% 50%, rgba(74, 144, 217, 0.13), transparent 56%),
#fbfaf5;
background-size: 28px 28px, 28px 28px, auto, auto;
isolation: isolate;
}
.agent-activity-stage::before {
content: '';
position: absolute;
inset: -40%;
background: conic-gradient(
from 0deg,
transparent 0deg,
rgba(74, 144, 217, 0.2) 62deg,
transparent 126deg,
rgba(34, 197, 94, 0.18) 206deg,
transparent 300deg
);
animation: agent-activity-sweep 9s linear infinite;
z-index: 0;
}
.agent-activity-stage.is-market::before {
background: conic-gradient(
from 0deg,
transparent 0deg,
rgba(245, 158, 11, 0.2) 62deg,
transparent 142deg,
rgba(74, 144, 217, 0.2) 230deg,
transparent 300deg
);
}
.agent-activity-orbit {
position: absolute;
left: 50%;
top: 50%;
border-radius: 999px;
border: 1px solid rgba(20, 20, 19, 0.08);
transform: translate(-50%, -50%);
z-index: 1;
}
.agent-activity-orbit-outer {
width: 172px;
height: 124px;
border-style: dashed;
animation: agent-activity-drift 12s linear infinite;
}
.agent-activity-orbit-inner {
width: 104px;
height: 72px;
border-color: rgba(74, 144, 217, 0.24);
animation: agent-activity-drift-reverse 10s linear infinite;
}
.agent-activity-bus {
position: absolute;
left: 50%;
top: 50%;
width: 46px;
height: 46px;
display: flex;
align-items: center;
justify-content: center;
border-radius: 999px;
color: #141413;
background: rgba(255, 255, 255, 0.86);
border: 0.5px solid rgba(20, 20, 19, 0.08);
box-shadow: 0 12px 26px rgba(20, 20, 19, 0.08);
transform: translate(-50%, -50%);
z-index: 3;
}
.agent-activity-node {
position: absolute;
z-index: 4;
display: inline-flex;
align-items: center;
gap: 6px;
max-width: calc(100% - 24px);
min-height: 30px;
padding: 5px 8px;
border-radius: 7px;
border: 0.5px solid rgba(20, 20, 19, 0.08);
background: rgba(255, 255, 255, 0.9);
box-shadow: 0 10px 22px rgba(20, 20, 19, 0.06);
color: #141413;
font-family: 'DM Mono', monospace;
font-size: 10px;
line-height: 1.15;
white-space: nowrap;
animation: agent-activity-node-pulse 3.6s ease-in-out infinite;
}
.agent-activity-node-openclaw {
left: 16px;
top: 20px;
color: #4a90d9;
animation-delay: 0s;
}
.agent-activity-node-hermes {
right: 14px;
top: 56px;
color: #d97757;
animation-delay: 0.5s;
}
.agent-activity-node-nemotron {
left: 50%;
bottom: 18px;
color: #22c55e;
transform: translateX(-50%);
animation-delay: 1s;
}
.agent-activity-packet {
position: absolute;
width: 7px;
height: 7px;
border-radius: 999px;
background: #4a90d9;
box-shadow: 0 0 18px rgba(74, 144, 217, 0.55);
z-index: 2;
}
.agent-activity-packet-a {
animation: agent-activity-packet-a 4.8s cubic-bezier(0.4, 0, 0.2, 1) infinite;
}
.agent-activity-packet-b {
background: #d97757;
box-shadow: 0 0 18px rgba(217, 119, 87, 0.5);
animation: agent-activity-packet-b 5.4s cubic-bezier(0.4, 0, 0.2, 1) infinite;
}
.agent-activity-packet-c {
background: #22c55e;
box-shadow: 0 0 18px rgba(34, 197, 94, 0.5);
animation: agent-activity-packet-c 6s cubic-bezier(0.4, 0, 0.2, 1) infinite;
}
.agent-activity-copy {
display: flex;
flex-direction: column;
gap: 10px;
min-width: 0;
}
.agent-activity-heading {
display: flex;
align-items: center;
gap: 7px;
min-width: 0;
color: #d97757;
}
.agent-activity-heading span {
color: #141413;
font-family: Syne, sans-serif;
font-size: 14px;
font-weight: 700;
line-height: 1.25;
min-width: 0;
overflow-wrap: anywhere;
}
.agent-activity-copy p {
margin: 0;
color: #625f58;
font-family: 'DM Mono', monospace;
font-size: 11px;
line-height: 1.55;
overflow-wrap: anywhere;
}
.agent-activity-status {
display: inline-flex;
align-items: center;
gap: 7px;
max-width: 100%;
min-width: 0;
color: #87867f;
font-family: 'DM Mono', monospace;
font-size: 10px;
overflow-wrap: anywhere;
}
.agent-activity-status strong {
color: #141413;
font-size: 11px;
font-weight: 700;
min-width: 0;
overflow-wrap: anywhere;
}
.agent-activity-metrics {
display: grid;
grid-template-columns: repeat(4, minmax(0, 1fr));
gap: 8px;
min-width: 0;
}
.agent-activity-metric {
min-width: 0;
border-left: 2px solid var(--metric-color);
padding: 7px 8px;
border-radius: 6px;
background: rgba(250, 249, 243, 0.74);
}
.agent-activity-metric span,
.agent-activity-footnote {
display: block;
color: #87867f;
font-family: 'DM Mono', monospace;
font-size: 9px;
line-height: 1.3;
overflow-wrap: anywhere;
}
.agent-activity-metric strong {
display: block;
margin-top: 3px;
color: #141413;
font-family: 'DM Mono', monospace;
font-size: 13px;
line-height: 1.2;
overflow-wrap: anywhere;
}
.agent-activity-footnote {
padding-top: 2px;
}
@keyframes agent-activity-sweep {
to {
transform: rotate(360deg);
}
}
@keyframes agent-activity-drift {
to {
transform: translate(-50%, -50%) rotate(360deg);
}
}
@keyframes agent-activity-drift-reverse {
to {
transform: translate(-50%, -50%) rotate(-360deg);
}
}
@keyframes agent-activity-node-pulse {
0%,
100% {
box-shadow: 0 10px 22px rgba(20, 20, 19, 0.06);
}
50% {
box-shadow: 0 12px 30px rgba(74, 144, 217, 0.18);
}
}
@keyframes agent-activity-packet-a {
0%,
100% {
left: 52px;
top: 46px;
opacity: 0.2;
}
42% {
left: 50%;
top: 50%;
opacity: 1;
}
72% {
left: calc(100% - 58px);
top: 76px;
opacity: 0.75;
}
}
@keyframes agent-activity-packet-b {
0%,
100% {
right: 58px;
top: 82px;
opacity: 0.18;
}
42% {
right: 50%;
top: 50%;
opacity: 1;
}
72% {
right: calc(50% - 6px);
top: calc(100% - 45px);
opacity: 0.78;
}
}
@keyframes agent-activity-packet-c {
0%,
100% {
left: 50%;
bottom: 38px;
opacity: 0.18;
}
42% {
left: calc(50% - 2px);
bottom: 50%;
opacity: 1;
}
72% {
left: 54px;
bottom: calc(100% - 54px);
opacity: 0.72;
}
}
@media (max-width: 900px) {
.agent-activity-layout {
grid-template-columns: 1fr;
}
.agent-activity-stage {
min-height: 176px;
}
}
@media (max-width: 640px) {
.agent-activity-metrics {
grid-template-columns: repeat(2, minmax(0, 1fr));
}
.agent-activity-node {
font-size: 9px;
padding: 5px 7px;
}
}
@media (prefers-reduced-motion: reduce) {
.agent-activity-stage::before,
.agent-activity-orbit,
.agent-activity-node,
.agent-activity-packet {
animation: none;
}
.agent-activity-packet {
opacity: 0.65;
}
}
`}</style>
</GlassCard>
)
}

View File

@@ -1,3 +1,29 @@
## 2026-06-14AI Agent 活動動畫本地完成
**背景**:統帥要求在相關治理頁加入 AI Agent 動畫,讓使用者能直覺看見 OpenClaw、Hermes、NemoTron 正在分工、溝通與產生治理證據;同時不得把工作視窗對話內容顯示到前端頁面。
**完成內容**
- 新增 `AgentActivityConstellation` client component使用原創抽象視覺呈現 `OpenClaw = 仲裁``Hermes = 記憶 / 學習``NemoTron = 執行 / 推理` 與中樞訊號匯流。
- Governance `automation-inventory` 頁新增「AI Agent 協作脈衝」,顯示待辦進度、讀回關卡、人工 gate、正式寫入等既有 snapshot 指標。
- Governance `agent-market` 頁新增「Agent 市場觀測脈衝」,顯示候選數、可預篩、已阻擋與 Runtime 批准等既有 market snapshot 指標。
- 視覺參考 NVIDIA Nemotron 官方公開定位的 agentic / throughput / multi-agent execution以及 Nous Hermes Agent 官方公開定位的 persistent memory / learning loop / skill growth未使用官方 Logo、商標、圖片或素材。
- 動畫支援 `prefers-reduced-motion: reduce`,小螢幕改為單欄與兩欄指標,避免文字溢出。
- 前端只讀取既有 API snapshot 與翻譯 key不新增後端寫入、不新增 Telegram send、不新增 Bot API、不新增 secret、不新增依賴、不新增 runtime 權限。
**本地驗證**
- JSON parse`apps/web/messages/zh-TW.json``apps/web/messages/en.json` 通過。
- Web typecheck`pnpm --filter @awoooi/web typecheck` 通過。
- Web production build`NEXT_PUBLIC_API_URL=https://awoooi.wooo.work pnpm --filter @awoooi/web build` 通過92 個 static pages 生成完成。
- Guard`git diff --check``doc-secrets-sanity-check.py docs .gitea``source-control-owner-response-guard.py --root .``security-mirror-progress-guard.py --root .` 全部通過。
- 本機 browser 預覽 `http://127.0.0.1:3011/zh-TW/governance?tab=automation-inventory` 因本機來源無法載入正式自動化 snapshot只顯示既有「無法載入自動化盤點快照」狀態不將此視為 production UI 真相。
- 本機頁面讀回未出現 `批准!繼續``My request for Codex``In app browser``work_window_transcript``raw prompt``private reasoning``chain-of-thought` 等工作視窗內容。
**安全邊界**
- 本輪只是可視化層與翻譯層整合,不改 OpenClaw / Hermes / NemoTron runtime 分工、不改批准政策、不開啟正式寫入、不送 Telegram、不呼叫 Bot API、不讀 secret、不做 destructive action。
**下一步**
- 推送 Gitea main 後等待 CD取得 deploy marker 後再做 production browser smoke確認 `automation-inventory``agent-market` 皆可見 Agent 活動動畫、水平溢位 `0`、正式寫入仍為 `0`、禁用內部協作片語命中 `0`
## 2026-06-14P2-136 釋出驗證器預檢關卡本地完成
**背景**P2-135 已把 release authorization readback gate 正式驗證完成;但授權讀回仍不得被誤讀成 post-release verifier ready、release authorization granted / passed、rollback release passed 或 live apply release passed。P2-136 因此只建立 release verifier preflight gate把 release authorization readback、rollback release readback、maintenance window readback hold、live-apply release readback hold 與 blocked release readback transition 轉成釋出驗證器預檢視圖,供 operator / owner 後續審核。