fix(web): 整理 Code Review 候選分類
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 4m3s
CD Pipeline / post-deploy-checks (push) Successful in 2m21s

This commit is contained in:
Your Name
2026-06-05 01:56:53 +08:00
parent 3f6592e6fa
commit 292cfec96d
4 changed files with 480 additions and 141 deletions

View File

@@ -13,97 +13,83 @@ import {
FileCheck2,
GitBranch,
Gauge,
ListChecks,
LockKeyhole,
SearchCheck,
ShieldCheck,
Workflow,
} from 'lucide-react'
import { useTranslations } from 'next-intl'
const agents = [
{ name: 'Hermes', role: '變更摘要與規則脈絡', state: 'wired' },
{ name: 'OpenClaw', role: 'AI 程式碼審查編排', state: 'wired' },
{ name: 'Elephant Alpha', role: '風險分級與修復決策', state: 'wired' },
{ name: 'NemoTron', role: '高風險推理席位', state: 'standby' },
{ name: 'Hermes', roleKey: 'hermes', state: 'wired' },
{ name: 'OpenClaw', roleKey: 'openclaw', state: 'wired' },
{ name: 'Elephant Alpha', roleKey: 'elephantAlpha', state: 'wired' },
{ name: 'NemoTron', roleKey: 'nemotron', state: 'standby' },
]
const stages = [
{ label: '推送', state: 'Gitea main' },
{ label: '啟動', state: 'Telegram 卡片' },
{ label: '掃描', state: '機密 / 毀滅性操作 / 差異檢查' },
{ label: '分級', state: '危急 / 高 / 中 / 低' },
{ label: '回報', state: 'Telegram 報告' },
{ key: 'push' },
{ key: 'start' },
{ key: 'scan' },
{ key: 'classify' },
{ key: 'report' },
]
const agentStateLabel: Record<string, string> = {
wired: '已接線',
standby: '待命',
}
const handoffFlags = [
'codex_handoff_mode=read_only_candidate',
'codex_auto_coding_enabled=false',
'runtime_execution_authorized=false',
'production_deploy_authorized=false',
]
const codexHandoffLanes = [
{
key: 'draftable',
icon: Code2,
title: '可交給 Codex 起草',
metric: '4 類',
detail: '前端體驗、測試補洞、文件同步、低風險重構。',
guard: '只建立草稿修正與驗證清單。',
className: 'border-emerald-500/30 bg-emerald-500/10 text-emerald-100',
iconClassName: 'text-emerald-300',
},
{
key: 'approvalRequired',
icon: FileCheck2,
title: '需人工批准後接手',
metric: '3 類',
detail: '高風險程式路徑、部署流程、跨專案設定。',
guard: '先有人控範圍、回滾條件與證據。',
className: 'border-amber-500/30 bg-amber-500/10 text-amber-100',
iconClassName: 'text-amber-300',
},
{
key: 'blocked',
icon: LockKeyhole,
title: '禁止自動轉工作',
metric: '5 條',
detail: 'Kali 主機變更、掃描、正式推版、主要來源切換、執行期閘門。',
guard: '維持 Gate 0 與獨立人工批准。',
className: 'border-rose-500/30 bg-rose-500/10 text-rose-100',
iconClassName: 'text-rose-300',
},
]
const codexCandidateWork = [
{
id: 'CR-CX-01',
track: '前端體驗',
finding: '版面密度、手機寬度、文字層級',
handoff: 'Codex 起草 UI 修正與截圖證據',
gate: '人工確認 scope',
state: '候選',
},
{
id: 'CR-CX-02',
track: '守門測試',
finding: 'IwoooS 邊界、禁用動作、進度口徑',
handoff: '補 guard / smoke test',
gate: '不得碰 runtime',
state: '候選',
},
{
id: 'CR-CX-03',
track: '繁中紀錄',
finding: '部署證據、LOGBOOK、平行工作同步',
handoff: '整理已驗證事實',
gate: '不寫未驗證結論',
state: '候選',
},
{
id: 'CR-CX-04',
track: '禁止轉派',
finding: 'Kali 更新、掃描、GitHub primary、正式部署',
handoff: '維持人工閘門',
gate: '獨立批准',
state: '封鎖',
},
{ id: 'CR-CX-01', key: 'frontendExperience', state: 'candidate' },
{ id: 'CR-CX-02', key: 'testCoverage', state: 'candidate' },
{ id: 'CR-CX-03', key: 'documentationSync', state: 'candidate' },
{ id: 'CR-CX-04', key: 'lowRiskRefactor', state: 'candidate' },
{ id: 'CR-BLOCK-01', key: 'runtimeBlocked', state: 'blocked' },
]
const approvalFlowSteps = [
'candidate',
'manualApproval',
'codexTask',
'patch',
'guards',
'deploy',
]
const summaryCards = [
{ key: 'source', icon: GitBranch, iconClassName: 'text-emerald-300' },
{ key: 'trigger', icon: Activity, iconClassName: 'text-sky-300' },
{ key: 'gate', icon: ShieldCheck, iconClassName: 'text-amber-300' },
{ key: 'report', icon: Gauge, iconClassName: 'text-lime-300' },
]
export default function CodeReviewPage({ params }: { params: { locale: string } }) {
const t = useTranslations('codeReview')
const evidenceHref = `/${params.locale}/awooop/runs`
return (
@@ -114,19 +100,17 @@ export default function CodeReviewPage({ params }: { params: { locale: string }
<div>
<div className="flex items-center gap-2 text-xs font-mono uppercase text-emerald-300">
<SearchCheck className="h-4 w-4" />
AWOOOI
{t('header.eyebrow')}
</div>
<h1 className="mt-2 text-2xl font-semibold text-white">AI </h1>
<p className="mt-1 text-sm text-gray-400">
Hermes OpenClaw Elephant Alpha NemoTron
</p>
<h1 className="mt-2 text-2xl font-semibold text-white">{t('header.title')}</h1>
<p className="mt-1 text-sm text-gray-400">{t('header.subtitle')}</p>
</div>
<a
href={evidenceHref}
className="inline-flex h-10 items-center justify-center gap-2 rounded border border-emerald-500/40 px-3 text-sm font-medium text-emerald-200 transition-colors hover:border-emerald-300 hover:bg-emerald-500/10"
className="inline-flex min-h-10 items-center justify-center gap-2 rounded border border-emerald-500/40 px-3 py-2 text-sm font-medium text-emerald-200 transition-colors hover:border-emerald-300 hover:bg-emerald-500/10"
>
<ExternalLink className="h-4 w-4" />
AwoooP
{t('header.evidenceLink')}
</a>
</header>
@@ -141,26 +125,17 @@ export default function CodeReviewPage({ params }: { params: { locale: string }
<div className="min-w-0">
<div className="flex items-center gap-2 text-xs font-mono uppercase text-sky-300">
<ClipboardList className="h-4 w-4" />
Code Review Codex
{t('handoff.eyebrow')}
</div>
<h2 className="mt-2 text-xl font-semibold text-white"> Coding </h2>
<p className="mt-2 max-w-xl text-sm leading-6 text-gray-400">
Codex 稿
</p>
<h2 className="mt-2 text-xl font-semibold text-white">{t('handoff.title')}</h2>
<p className="mt-2 max-w-xl text-sm leading-6 text-gray-400">{t('handoff.subtitle')}</p>
</div>
<div className="grid gap-2 text-xs font-mono text-gray-400 sm:grid-cols-2">
<div className="min-w-0 break-all rounded border border-gray-800 bg-black/25 px-3 py-2">
codex_handoff_mode=read_only_candidate
</div>
<div className="min-w-0 break-all rounded border border-gray-800 bg-black/25 px-3 py-2">
codex_auto_coding_enabled=false
</div>
<div className="min-w-0 break-all rounded border border-gray-800 bg-black/25 px-3 py-2">
runtime_execution_authorized=false
</div>
<div className="min-w-0 break-all rounded border border-gray-800 bg-black/25 px-3 py-2">
production_deploy_authorized=false
</div>
{handoffFlags.map((flag) => (
<div key={flag} className="min-w-0 break-all rounded border border-gray-800 bg-black/25 px-3 py-2">
{flag}
</div>
))}
</div>
</div>
@@ -169,16 +144,16 @@ export default function CodeReviewPage({ params }: { params: { locale: string }
const Icon = lane.icon
return (
<div
key={lane.title}
key={lane.key}
className={`flex min-h-[12rem] min-w-0 flex-col rounded border p-3 ${lane.className}`}
>
<div className="flex items-center justify-between gap-3">
<Icon className={`h-5 w-5 ${lane.iconClassName}`} />
<span className="font-mono text-xl font-semibold">{lane.metric}</span>
<span className="font-mono text-xl font-semibold">{t(`handoff.lanes.${lane.key}.metric` as never)}</span>
</div>
<div className="mt-4 text-sm font-semibold text-white">{lane.title}</div>
<p className="mt-2 text-sm leading-5 text-gray-300">{lane.detail}</p>
<div className="mt-auto pt-4 text-xs leading-5 text-gray-400">{lane.guard}</div>
<div className="mt-4 text-sm font-semibold text-white">{t(`handoff.lanes.${lane.key}.title` as never)}</div>
<p className="mt-2 text-sm leading-5 text-gray-300">{t(`handoff.lanes.${lane.key}.detail` as never)}</p>
<div className="mt-auto pt-4 text-xs leading-5 text-gray-400">{t(`handoff.lanes.${lane.key}.guard` as never)}</div>
{index < codexHandoffLanes.length - 1 ? (
<ArrowRight className="mt-3 hidden h-4 w-4 self-end text-gray-600 md:block" />
) : null}
@@ -194,34 +169,51 @@ export default function CodeReviewPage({ params }: { params: { locale: string }
>
<div className="grid min-w-0 gap-2 border-b border-gray-800 px-4 py-3 md:grid-cols-[minmax(0,1fr)_auto] md:items-center">
<div className="min-w-0">
<div className="break-words text-sm font-semibold text-white">Codex </div>
<div className="mt-1 break-words text-xs text-gray-500">
稿
<div className="flex items-center gap-2 break-words text-sm font-semibold text-white">
<ListChecks className="h-4 w-4 text-emerald-300" />
{t('candidateQueue.title')}
</div>
<div className="mt-1 break-words text-xs text-gray-500">{t('candidateQueue.subtitle')}</div>
</div>
<div className="min-w-0 max-w-full break-all rounded border border-emerald-500/30 px-3 py-1 text-xs font-mono text-emerald-200 md:justify-self-end">
action_buttons_allowed=false
</div>
</div>
<div className="hidden grid-cols-[7rem_8rem_1.1fr_1.15fr_8rem_6rem] gap-3 border-b border-gray-800 px-4 py-2 text-xs font-mono uppercase text-gray-500 md:grid">
<span>{t('candidateQueue.columns.id')}</span>
<span>{t('candidateQueue.columns.track')}</span>
<span>{t('candidateQueue.columns.finding')}</span>
<span>{t('candidateQueue.columns.handoff')}</span>
<span>{t('candidateQueue.columns.gate')}</span>
<span>{t('candidateQueue.columns.state')}</span>
</div>
<div className="divide-y divide-gray-800">
{codexCandidateWork.map((item) => (
<div
key={item.id}
className="grid gap-3 px-4 py-3 text-sm md:grid-cols-[7rem_8rem_1.2fr_1.1fr_8rem_5rem] md:items-center"
className="grid gap-3 px-4 py-3 text-sm md:grid-cols-[7rem_8rem_1.1fr_1.15fr_8rem_6rem] md:items-center"
>
<span className="min-w-0 break-words font-mono text-gray-500">{item.id}</span>
<span className="min-w-0 break-words font-semibold text-white">{item.track}</span>
<span className="min-w-0 break-words text-gray-400">{item.finding}</span>
<span className="min-w-0 break-words text-gray-300">{item.handoff}</span>
<span className="min-w-0 break-words text-gray-500">{item.gate}</span>
<span className="min-w-0 break-words font-semibold text-white">
{t(`candidateQueue.items.${item.key}.track` as never)}
</span>
<span className="min-w-0 break-words text-gray-400">
{t(`candidateQueue.items.${item.key}.finding` as never)}
</span>
<span className="min-w-0 break-words text-gray-300">
{t(`candidateQueue.items.${item.key}.handoff` as never)}
</span>
<span className="min-w-0 break-words text-gray-500">
{t(`candidateQueue.items.${item.key}.gate` as never)}
</span>
<span
className={
item.state === '封鎖'
item.state === 'blocked'
? 'justify-self-start rounded bg-rose-500/10 px-2 py-1 text-xs font-mono text-rose-200'
: 'justify-self-start rounded bg-emerald-500/10 px-2 py-1 text-xs font-mono text-emerald-200'
}
>
{item.state}
{t(`states.${item.state}` as never)}
</span>
</div>
))}
@@ -229,37 +221,34 @@ export default function CodeReviewPage({ params }: { params: { locale: string }
</section>
<section className="grid gap-3 md:grid-cols-4">
<div className="rounded border border-gray-800 bg-gray-950 p-4">
<div className="flex items-center gap-2 text-xs text-gray-400">
<GitBranch className="h-4 w-4 text-emerald-300" />
</div>
<div className="mt-3 text-lg font-semibold text-white">gitea main</div>
<div className="mt-1 text-xs text-gray-500"></div>
{summaryCards.map((card) => {
const Icon = card.icon
return (
<div key={card.key} className="rounded border border-gray-800 bg-gray-950 p-4">
<div className="flex items-center gap-2 text-xs text-gray-400">
<Icon className={`h-4 w-4 ${card.iconClassName}`} />
{t(`summaryCards.${card.key}.label` as never)}
</div>
<div className="mt-3 text-lg font-semibold text-white">{t(`summaryCards.${card.key}.value` as never)}</div>
<div className="mt-1 text-xs text-gray-500">{t(`summaryCards.${card.key}.detail` as never)}</div>
</div>
)
})}
</section>
<section className="rounded border border-gray-800 bg-gray-950 p-4">
<div className="flex items-center gap-2 text-sm font-semibold text-white">
<Workflow className="h-4 w-4 text-sky-300" />
{t('approvalFlow.title')}
</div>
<div className="rounded border border-gray-800 bg-gray-950 p-4">
<div className="flex items-center gap-2 text-xs text-gray-400">
<Activity className="h-4 w-4 text-sky-300" />
</div>
<div className="mt-3 text-lg font-semibold text-white"> / </div>
<div className="mt-1 text-xs text-gray-500"></div>
</div>
<div className="rounded border border-gray-800 bg-gray-950 p-4">
<div className="flex items-center gap-2 text-xs text-gray-400">
<ShieldCheck className="h-4 w-4 text-amber-300" />
</div>
<div className="mt-3 text-lg font-semibold text-white"> v1</div>
<div className="mt-1 text-xs text-gray-500"> / </div>
</div>
<div className="rounded border border-gray-800 bg-gray-950 p-4">
<div className="flex items-center gap-2 text-xs text-gray-400">
<Gauge className="h-4 w-4 text-lime-300" />
</div>
<div className="mt-3 text-lg font-semibold text-white">Telegram + Actions</div>
<div className="mt-1 text-xs text-gray-500"></div>
<div className="mt-3 grid gap-2 md:grid-cols-6">
{approvalFlowSteps.map((step, index) => (
<div key={step} className="min-w-0 rounded border border-gray-800 bg-black/20 px-3 py-3">
<div className="font-mono text-xs text-gray-500">{String(index + 1).padStart(2, '0')}</div>
<div className="mt-2 text-sm font-semibold text-white">{t(`approvalFlow.steps.${step}.title` as never)}</div>
<div className="mt-1 text-xs leading-5 text-gray-400">{t(`approvalFlow.steps.${step}.detail` as never)}</div>
</div>
))}
</div>
</section>
@@ -267,17 +256,17 @@ export default function CodeReviewPage({ params }: { params: { locale: string }
<div className="space-y-3">
<div className="flex items-center gap-2 text-sm font-semibold text-white">
<CheckCircle2 className="h-4 w-4 text-emerald-300" />
{t('reviewFlow.title')}
</div>
<div className="grid gap-2">
{stages.map((stage, index) => (
<div
key={stage.label}
key={stage.key}
className="grid grid-cols-[2rem_8rem_1fr] items-center rounded border border-gray-800 bg-gray-950 px-3 py-2 text-sm"
>
<span className="font-mono text-gray-500">{String(index + 1).padStart(2, '0')}</span>
<span className="font-mono text-emerald-200">{stage.label}</span>
<span className="truncate text-gray-300">{stage.state}</span>
<span className="font-mono text-emerald-200">{t(`reviewFlow.stages.${stage.key}.label` as never)}</span>
<span className="truncate text-gray-300">{t(`reviewFlow.stages.${stage.key}.state` as never)}</span>
</div>
))}
</div>
@@ -286,7 +275,7 @@ export default function CodeReviewPage({ params }: { params: { locale: string }
<div className="space-y-3">
<div className="flex items-center gap-2 text-sm font-semibold text-white">
<Bot className="h-4 w-4 text-sky-300" />
Agent
{t('agents.title')}
</div>
<div className="grid gap-2">
{agents.map((agent) => (
@@ -295,9 +284,9 @@ export default function CodeReviewPage({ params }: { params: { locale: string }
className="grid grid-cols-[8rem_1fr_4.5rem] items-center rounded border border-gray-800 bg-gray-950 px-3 py-2 text-sm"
>
<span className="font-semibold text-white">{agent.name}</span>
<span className="truncate text-gray-400">{agent.role}</span>
<span className="truncate text-gray-400">{t(`agents.roles.${agent.roleKey}` as never)}</span>
<span className="justify-self-end rounded bg-emerald-500/10 px-2 py-1 text-xs font-mono text-emerald-200">
{agentStateLabel[agent.state] ?? agent.state}
{t(`agentStates.${agent.state}` as never)}
</span>
</div>
))}