fix: localize agnet console copy

This commit is contained in:
gongzhiyong
2026-05-28 18:08:18 +08:00
parent d2f7ab7333
commit efaac446ab
4 changed files with 196 additions and 36 deletions
+1 -1
View File
@@ -1 +1 @@
1.4.10
1.4.11
+8 -2
View File
@@ -37,7 +37,13 @@ export function CommandMenu() {
)
return (
<CommandDialog modal open={open} onOpenChange={setOpen}>
<CommandDialog
modal
open={open}
onOpenChange={setOpen}
title={t('Command Palette')}
description={t('Search for a command to run...')}
>
<CommandInput placeholder={t('Type a command or search...')} />
<CommandList>
<ScrollArea type='hover' className='h-72 pe-1'>
@@ -79,7 +85,7 @@ export function CommandMenu() {
</CommandGroup>
))}
<CommandSeparator />
<CommandGroup heading='Theme'>
<CommandGroup heading={t('Theme')}>
<CommandItem onSelect={() => runCommand(() => setTheme('light'))}>
<Sun /> <span>{t('Light')}</span>
</CommandItem>
+121 -33
View File
@@ -90,6 +90,7 @@ const STATUS_TO_KEY: Record<string, StatusKey> = {
running: 'running',
active: 'running',
in_progress: 'running',
stopped: 'success',
succeeded: 'success',
success: 'success',
completed: 'success',
@@ -105,7 +106,44 @@ function classifyStatus(status: string): StatusKey {
return STATUS_TO_KEY[(status || '').toLowerCase()] ?? 'pending'
}
function formatStatusLabel(
status: string | undefined,
t: (key: string) => string
): string {
const value = (status || '').trim()
if (!value) return t('Unknown')
const normalized = value.toLowerCase()
if (normalized.startsWith('handoff:')) {
const tail = normalized.replace('handoff:', '')
return `${t('Handoff')}: ${formatStatusLabel(tail, t)}`
}
const labels: Record<string, string> = {
running: t('Running'),
active: t('Running'),
in_progress: t('In progress'),
stopped: t('Stopped'),
succeeded: t('Success'),
success: t('Success'),
completed: t('Completed'),
failed: t('Failed'),
error: t('Failed'),
rejected: t('Rejected'),
pending: t('Pending'),
queued: t('Queued'),
awaiting: t('Awaiting'),
observed: t('Observed'),
blocked: t('Blocked'),
requested: t('Requested'),
high: t('High'),
medium: t('Medium'),
mid: t('Medium'),
low: t('Low'),
}
return labels[normalized] || value
}
function StatusBadge({ phase }: { phase: string }) {
const { t } = useTranslation()
const k = classifyStatus(phase)
const map: Record<StatusKey, { cls: string; icon: ReactNode }> = {
running: {
@@ -134,7 +172,7 @@ function StatusBadge({ phase }: { phase: string }) {
)}
>
{m.icon}
{phase || 'unknown'}
{formatStatusLabel(phase, t)}
</span>
)
}
@@ -238,22 +276,37 @@ function describeRiskLevel(dep: AgnetDeployment): {
return { label: 'low', tone: 'low' }
}
function formatRiskLabel(label: string, t: (key: string) => string): string {
const value = label.toLowerCase()
if (value === 'high') return t('High')
if (value === 'medium' || value === 'mid') return t('Medium')
if (value === 'low') return t('Low')
return label
}
function describeSubMode(dep: AgnetDeployment): string {
return dep.sub_mode || dep.orchestration_plan?.sub_mode || 'agile'
}
function formatSubModeLabel(mode: string, t: (key: string) => string): string {
const value = mode.toLowerCase()
if (value === 'agile') return t('Agile')
if (value === 'waterfall') return t('Waterfall')
return mode
}
function describeBudget(dep: AgnetDeployment): string {
const budget = dep.orchestration_plan?.budget
if (!budget) {
const agents = dep.orchestration_plan?.agents?.length ?? 0
return `${agents} agents`
return `${agents} 个 Agent`
}
const parts = []
if (budget.max_cost_usd > 0) parts.push(`$${budget.max_cost_usd}`)
if (budget.max_tokens > 0) parts.push(`${budget.max_tokens} tokens`)
if (budget.max_tokens > 0) parts.push(`${budget.max_tokens} 个 token`)
if (budget.max_duration_sec > 0)
parts.push(`${Math.round(budget.max_duration_sec / 60)}m`)
parts.push(`${Math.round(budget.max_duration_sec / 60)} 分钟`)
return parts.length > 0 ? parts.join(' / ') : '—'
}
@@ -277,9 +330,11 @@ function compactRuntimeRef(value: string | undefined): string {
function formatRecordSource(value: unknown): string {
const source = String(value || '').trim()
if (!source) return 'manager'
if (source.includes('simulate')) return 'simulated'
if (source.includes('runtime') || source.includes('swarm')) return 'runtime'
const normalized = source.toLowerCase()
if (!source || normalized.includes('manager')) return 'Manager'
if (normalized.includes('simulate')) return '模拟'
if (normalized.includes('runtime') || normalized.includes('swarm'))
return 'Runtime'
return source
}
@@ -324,11 +379,11 @@ function taskFlowDetail(item: Record<string, unknown>): string {
const attempt = String(recordPayloadValue(item, 'attempt') || '')
const agentRole = String(recordPayloadValue(item, 'agent_role') || '')
const parts = [
taskId && `task ${taskId}`,
agentRole && `role ${agentRole}`,
taskId && `任务 ${taskId}`,
agentRole && `角色 ${agentRole}`,
fromRole && toRole && `${fromRole} -> ${toRole}`,
reason && `reason ${reason}`,
attempt && `attempt ${attempt}`,
reason && `原因 ${reason}`,
attempt && `第 ${attempt} 次`,
].filter(Boolean)
return parts.join(' / ') || '—'
}
@@ -415,13 +470,13 @@ function formatRelativeTime(value: string | undefined): string {
if (Number.isNaN(date.getTime())) return value
const diff = Date.now() - date.getTime()
const sec = Math.round(diff / 1000)
if (sec < 60) return `${sec}s ago`
if (sec < 60) return `${sec} 秒前`
const min = Math.round(sec / 60)
if (min < 60) return `${min}m ago`
if (min < 60) return `${min} 分钟前`
const hr = Math.round(min / 60)
if (hr < 24) return `${hr}h ago`
if (hr < 24) return `${hr} 小时前`
const day = Math.round(hr / 24)
return `${day}d ago`
return `${day} 天前`
}
// maskSecretRef shows enough of a secret_ref to identify which vault
@@ -461,6 +516,18 @@ function grantStatusToneClass(status: string | undefined): string {
}
}
function formatGrantStatus(
status: string | undefined,
t: (key: string) => string
): string {
const value = (status || 'active').toLowerCase()
if (value === 'active') return t('Active')
if (value === 'pending') return t('Pending')
if (value === 'disabled') return t('Disabled')
if (value === 'revoked') return t('Revoked')
return status || t('Active')
}
function RunDetailPanel({ dep }: { dep: AgnetDeployment }) {
const { t } = useTranslation()
const queryClient = useQueryClient()
@@ -527,13 +594,21 @@ function RunDetailPanel({ dep }: { dep: AgnetDeployment }) {
</div>
<div className='mt-4 grid gap-2 sm:grid-cols-2 xl:grid-cols-6'>
<MetaPill icon={Activity} label={t('status')} value={phase || '—'} />
<MetaPill
icon={Activity}
label={t('status')}
value={formatStatusLabel(phase, t)}
/>
<MetaPill
icon={Rocket}
label={t('mode')}
value={describeSubMode(dep)}
value={formatSubModeLabel(describeSubMode(dep), t)}
/>
<MetaPill
icon={Tag}
label={t('risk')}
value={formatRiskLabel(risk.label, t)}
/>
<MetaPill icon={Tag} label={t('risk')} value={risk.label} />
<MetaPill
icon={Coins}
label={t('budget')}
@@ -546,7 +621,7 @@ function RunDetailPanel({ dep }: { dep: AgnetDeployment }) {
/>
<MetaPill
icon={ShieldCheck}
label='secret_ref'
label={t('secret_ref')}
value={describeSecretRefs(dep)}
/>
</div>
@@ -555,16 +630,16 @@ function RunDetailPanel({ dep }: { dep: AgnetDeployment }) {
<MetaPill
icon={Cpu}
label={t('runtime')}
value={dep.runtime_state || 'control-plane'}
value={dep.runtime_state || t('Control plane')}
/>
<MetaPill
icon={Rocket}
label='runtime_id'
label={t('runtime_id')}
value={compactRuntimeRef(dep.runtime_deployment_id)}
/>
<MetaPill
icon={GitCommit}
label='swarm_id'
label={t('swarm_id')}
value={compactRuntimeRef(dep.runtime_swarm_id)}
/>
</div>
@@ -694,7 +769,7 @@ function RunDetailPanel({ dep }: { dep: AgnetDeployment }) {
grantStatusToneClass(status)
)}
>
{status}
{formatGrantStatus(status, t)}
</span>
</td>
</tr>
@@ -1112,7 +1187,7 @@ function RunRelatedRecordsPanel({ deploymentId }: { deploymentId: string }) {
) : (
<ol className='mt-2 max-h-48 space-y-2 overflow-y-auto pe-1'>
{taskFlowRecords.slice(0, 8).map((item, idx) => {
const event = String(item.event_type || item.event || 'event')
const event = String(item.event_type || item.event || t('Event'))
const source = formatRecordSource(item.source)
const detail = taskFlowDetail(item)
return (
@@ -1222,7 +1297,7 @@ function RunRelatedRecordsPanel({ deploymentId }: { deploymentId: string }) {
artifactTypeToneClass(item.artifact_type)
)}
>
{String(item.artifact_type || 'artifact')}
{String(item.artifact_type || t('Artifact'))}
</span>
</div>
<p className='text-muted-foreground mt-1 line-clamp-2 text-[11px]'>
@@ -1254,10 +1329,10 @@ function RunRelatedRecordsPanel({ deploymentId }: { deploymentId: string }) {
className='bg-muted/25 rounded-lg p-2'
>
<p className='truncate font-mono text-[11px]'>
{String(item.snapshot_id || 'snapshot')}
{String(item.snapshot_id || t('Snapshot'))}
</p>
<p className='text-muted-foreground mt-1 truncate text-[11px]'>
{String(item.source_type || 'source')} ·{' '}
{String(item.source_type || t('Source'))} ·{' '}
{String(item.source_ref || '—')}
</p>
</li>
@@ -1278,7 +1353,7 @@ function RunRelatedRecordsPanel({ deploymentId }: { deploymentId: string }) {
) : (
<ol className='mt-2 max-h-52 space-y-2 overflow-y-auto pe-1'>
{timeline.slice(0, 8).map((item, idx) => {
const event = String(item.event_type || item.event || 'event')
const event = String(item.event_type || item.event || t('Event'))
const source = formatRecordSource(item.source)
return (
<li
@@ -1314,6 +1389,17 @@ const EVENT_LEVELS = ['all', 'info', 'warn', 'error'] as const
type EventLevel = (typeof EVENT_LEVELS)[number]
function formatEventLevelLabel(
level: EventLevel,
t: (key: string) => string
): string {
if (level === 'all') return t('All levels')
if (level === 'info') return t('Info')
if (level === 'warn') return t('Warning')
if (level === 'error') return t('Error')
return level
}
function classifyEventLevel(entry: Record<string, unknown>): EventLevel {
const candidate = String(
entry.level || entry.severity || entry.status || ''
@@ -1391,7 +1477,7 @@ export function AgnetEventsPage() {
<SelectContent>
{EVENT_LEVELS.map((lv) => (
<SelectItem key={lv} value={lv}>
{lv}
{formatEventLevelLabel(lv, t)}
</SelectItem>
))}
</SelectContent>
@@ -1655,6 +1741,7 @@ function AgnetApprovalCard({
onApprove: () => void
onReject: () => void
}) {
const { t } = useTranslation()
return (
<article className='border-border/70 bg-background/45 rounded-xl border p-3'>
<div className='flex flex-wrap items-start justify-between gap-3'>
@@ -1675,7 +1762,7 @@ function AgnetApprovalCard({
<RedactedField k='expires_at' v={formatUnixMs(approval.expires_at)} />
<RedactedField
k='credential'
v={approval.requires_credential ? 'required' : 'not required'}
v={approval.requires_credential ? t('Required') : t('Not required')}
/>
</dl>
<div className='mt-3 flex flex-wrap justify-end gap-2'>
@@ -1685,14 +1772,14 @@ function AgnetApprovalCard({
disabled={rejectBusy || approveBusy}
onClick={onReject}
>
Reject
{t('Reject')}
</Button>
<Button
size='sm'
disabled={approveBusy || rejectBusy}
onClick={onApprove}
>
Approve
{t('Approve')}
</Button>
</div>
</article>
@@ -1708,6 +1795,7 @@ function AgnetLeaseCard({
busy: boolean
onRevoke: () => void
}) {
const { t } = useTranslation()
return (
<article className='border-border/70 bg-background/45 rounded-xl border p-3'>
<div className='flex flex-wrap items-start justify-between gap-3'>
@@ -1727,7 +1815,7 @@ function AgnetLeaseCard({
</dl>
<div className='mt-3 flex justify-end'>
<Button size='sm' variant='outline' disabled={busy} onClick={onRevoke}>
Revoke
{t('Revoke')}
</Button>
</div>
</article>
+66
View File
@@ -4081,9 +4081,75 @@
"Resource binding": "资源绑定",
"Show manifest": "展开权限清单",
"Hide manifest": "收起权限清单",
"Resource": "资源",
"Allowed actions": "允许动作",
"Constraints": "约束",
"secret_ref": "密钥引用",
"runtime_id": "运行 ID",
"swarm_id": "蜂群 ID",
"mode": "模式",
"runtime": "运行时",
"Control plane": "控制面",
"Stopped": "已停止",
"In progress": "进行中",
"Awaiting": "等待中",
"Observed": "已观测",
"Blocked": "已阻塞",
"Requested": "已请求",
"Rejected": "已拒绝",
"Active": "启用中",
"Disabled": "已禁用",
"Revoked": "已撤销",
"High": "高",
"Medium": "中",
"Low": "低",
"Agile": "敏捷",
"Waterfall": "瀑布",
"Handoff": "交接",
"Event": "事件",
"Snapshot": "快照",
"Artifact": "产物",
"Command Palette": "命令面板",
"Search for a command to run...": "搜索可执行命令...",
"Related records": "关联记录",
"Callbacks, artifacts, SK snapshots and merged timeline": "回调、产物、SK 快照和合并时间线",
"callbacks": "回调",
"artifacts": "产物",
"timeline": "时间线",
"Sub task flow": "子任务流",
"No task, retry or handoff callback records yet": "暂无任务、重试或交接回调记录",
"Agent task map": "Agent 任务图",
"Waiting for Runtime task graph callbacks. Manager will show task, Agent role, handoff and source here when callbacks arrive.": "等待 Runtime 回传任务图。收到回调后,这里会展示任务、Agent 角色、交接关系和来源。",
"Agent role": "Agent 角色",
"Artifacts": "产物",
"No artifacts yet": "暂无产物",
"SK snapshots": "SK 快照",
"No SK snapshots yet": "暂无 SK 快照",
"Merged timeline": "合并时间线",
"No timeline records yet": "暂无时间线记录",
"Simulate": "模拟事件",
"Simulated events recorded": "模拟事件已记录",
"Failed to simulate events": "模拟事件失败",
"All levels": "全部级别",
"Info": "信息",
"Active credential leases": "有效凭证租约",
"Approve or reject high-risk Agnet operations before credentials are leased.": "在下发凭证租约前,审批或拒绝高危 Agnet 操作。",
"No pending approvals": "暂无待审批项",
"High-risk operations will appear here.": "高危操作会显示在这里。",
"No active credential leases": "暂无有效凭证租约",
"Only short-lived lease references are shown. Revoke after the task ends.": "这里只展示短期租约引用。任务结束后请及时撤销。",
"Approved credential leases will appear here.": "已审批的凭证租约会显示在这里。",
"Approve": "批准",
"Reject": "拒绝",
"Revoke": "撤销",
"Not required": "不需要",
"Approval accepted": "审批已通过",
"Approval rejected": "审批已拒绝",
"Credential lease revoked": "凭证租约已撤销",
"Approved from Manager audit page": "从 Manager 审计页批准",
"Rejected from Manager audit page": "从 Manager 审计页拒绝",
"Revoked from Manager audit page": "从 Manager 审计页撤销",
"Connect code, docs and cloud resources for the current task, then confirm the recommendation before launching Agnet.": "为当前任务绑定代码、文档和云资源,然后确认推荐方案再启动 Agnet。",
"Plaintext credentials are never shown. The secret_ref column is a vault pointer, not the secret itself.": "永远不展示明文密钥。表中的 secret_ref 只是密钥保管器里的引用指针,不是密钥本体。",
"Confirm before launch": "确认后启动",
"Heicode summarises what this run will do, what resources it can use, what stays off-limits, and the expected cost. Confirm to launch.": "Heicode 已汇总本次会做什么、能用哪些资源、哪些事情不会做、以及预计消耗。请确认后启动。",