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 ( 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...')} /> <CommandInput placeholder={t('Type a command or search...')} />
<CommandList> <CommandList>
<ScrollArea type='hover' className='h-72 pe-1'> <ScrollArea type='hover' className='h-72 pe-1'>
@@ -79,7 +85,7 @@ export function CommandMenu() {
</CommandGroup> </CommandGroup>
))} ))}
<CommandSeparator /> <CommandSeparator />
<CommandGroup heading='Theme'> <CommandGroup heading={t('Theme')}>
<CommandItem onSelect={() => runCommand(() => setTheme('light'))}> <CommandItem onSelect={() => runCommand(() => setTheme('light'))}>
<Sun /> <span>{t('Light')}</span> <Sun /> <span>{t('Light')}</span>
</CommandItem> </CommandItem>
+121 -33
View File
@@ -90,6 +90,7 @@ const STATUS_TO_KEY: Record<string, StatusKey> = {
running: 'running', running: 'running',
active: 'running', active: 'running',
in_progress: 'running', in_progress: 'running',
stopped: 'success',
succeeded: 'success', succeeded: 'success',
success: 'success', success: 'success',
completed: 'success', completed: 'success',
@@ -105,7 +106,44 @@ function classifyStatus(status: string): StatusKey {
return STATUS_TO_KEY[(status || '').toLowerCase()] ?? 'pending' 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 }) { function StatusBadge({ phase }: { phase: string }) {
const { t } = useTranslation()
const k = classifyStatus(phase) const k = classifyStatus(phase)
const map: Record<StatusKey, { cls: string; icon: ReactNode }> = { const map: Record<StatusKey, { cls: string; icon: ReactNode }> = {
running: { running: {
@@ -134,7 +172,7 @@ function StatusBadge({ phase }: { phase: string }) {
)} )}
> >
{m.icon} {m.icon}
{phase || 'unknown'} {formatStatusLabel(phase, t)}
</span> </span>
) )
} }
@@ -238,22 +276,37 @@ function describeRiskLevel(dep: AgnetDeployment): {
return { label: 'low', tone: 'low' } 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 { function describeSubMode(dep: AgnetDeployment): string {
return dep.sub_mode || dep.orchestration_plan?.sub_mode || 'agile' 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 { function describeBudget(dep: AgnetDeployment): string {
const budget = dep.orchestration_plan?.budget const budget = dep.orchestration_plan?.budget
if (!budget) { if (!budget) {
const agents = dep.orchestration_plan?.agents?.length ?? 0 const agents = dep.orchestration_plan?.agents?.length ?? 0
return `${agents} agents` return `${agents} 个 Agent`
} }
const parts = [] const parts = []
if (budget.max_cost_usd > 0) parts.push(`$${budget.max_cost_usd}`) 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) 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(' / ') : '—' return parts.length > 0 ? parts.join(' / ') : '—'
} }
@@ -277,9 +330,11 @@ function compactRuntimeRef(value: string | undefined): string {
function formatRecordSource(value: unknown): string { function formatRecordSource(value: unknown): string {
const source = String(value || '').trim() const source = String(value || '').trim()
if (!source) return 'manager' const normalized = source.toLowerCase()
if (source.includes('simulate')) return 'simulated' if (!source || normalized.includes('manager')) return 'Manager'
if (source.includes('runtime') || source.includes('swarm')) return 'runtime' if (normalized.includes('simulate')) return '模拟'
if (normalized.includes('runtime') || normalized.includes('swarm'))
return 'Runtime'
return source return source
} }
@@ -324,11 +379,11 @@ function taskFlowDetail(item: Record<string, unknown>): string {
const attempt = String(recordPayloadValue(item, 'attempt') || '') const attempt = String(recordPayloadValue(item, 'attempt') || '')
const agentRole = String(recordPayloadValue(item, 'agent_role') || '') const agentRole = String(recordPayloadValue(item, 'agent_role') || '')
const parts = [ const parts = [
taskId && `task ${taskId}`, taskId && `任务 ${taskId}`,
agentRole && `role ${agentRole}`, agentRole && `角色 ${agentRole}`,
fromRole && toRole && `${fromRole} -> ${toRole}`, fromRole && toRole && `${fromRole} -> ${toRole}`,
reason && `reason ${reason}`, reason && `原因 ${reason}`,
attempt && `attempt ${attempt}`, attempt && `第 ${attempt} 次`,
].filter(Boolean) ].filter(Boolean)
return parts.join(' / ') || '—' return parts.join(' / ') || '—'
} }
@@ -415,13 +470,13 @@ function formatRelativeTime(value: string | undefined): string {
if (Number.isNaN(date.getTime())) return value if (Number.isNaN(date.getTime())) return value
const diff = Date.now() - date.getTime() const diff = Date.now() - date.getTime()
const sec = Math.round(diff / 1000) const sec = Math.round(diff / 1000)
if (sec < 60) return `${sec}s ago` if (sec < 60) return `${sec} 秒前`
const min = Math.round(sec / 60) const min = Math.round(sec / 60)
if (min < 60) return `${min}m ago` if (min < 60) return `${min} 分钟前`
const hr = Math.round(min / 60) const hr = Math.round(min / 60)
if (hr < 24) return `${hr}h ago` if (hr < 24) return `${hr} 小时前`
const day = Math.round(hr / 24) const day = Math.round(hr / 24)
return `${day}d ago` return `${day} 天前`
} }
// maskSecretRef shows enough of a secret_ref to identify which vault // 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 }) { function RunDetailPanel({ dep }: { dep: AgnetDeployment }) {
const { t } = useTranslation() const { t } = useTranslation()
const queryClient = useQueryClient() const queryClient = useQueryClient()
@@ -527,13 +594,21 @@ function RunDetailPanel({ dep }: { dep: AgnetDeployment }) {
</div> </div>
<div className='mt-4 grid gap-2 sm:grid-cols-2 xl:grid-cols-6'> <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 <MetaPill
icon={Rocket} icon={Rocket}
label={t('mode')} 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 <MetaPill
icon={Coins} icon={Coins}
label={t('budget')} label={t('budget')}
@@ -546,7 +621,7 @@ function RunDetailPanel({ dep }: { dep: AgnetDeployment }) {
/> />
<MetaPill <MetaPill
icon={ShieldCheck} icon={ShieldCheck}
label='secret_ref' label={t('secret_ref')}
value={describeSecretRefs(dep)} value={describeSecretRefs(dep)}
/> />
</div> </div>
@@ -555,16 +630,16 @@ function RunDetailPanel({ dep }: { dep: AgnetDeployment }) {
<MetaPill <MetaPill
icon={Cpu} icon={Cpu}
label={t('runtime')} label={t('runtime')}
value={dep.runtime_state || 'control-plane'} value={dep.runtime_state || t('Control plane')}
/> />
<MetaPill <MetaPill
icon={Rocket} icon={Rocket}
label='runtime_id' label={t('runtime_id')}
value={compactRuntimeRef(dep.runtime_deployment_id)} value={compactRuntimeRef(dep.runtime_deployment_id)}
/> />
<MetaPill <MetaPill
icon={GitCommit} icon={GitCommit}
label='swarm_id' label={t('swarm_id')}
value={compactRuntimeRef(dep.runtime_swarm_id)} value={compactRuntimeRef(dep.runtime_swarm_id)}
/> />
</div> </div>
@@ -694,7 +769,7 @@ function RunDetailPanel({ dep }: { dep: AgnetDeployment }) {
grantStatusToneClass(status) grantStatusToneClass(status)
)} )}
> >
{status} {formatGrantStatus(status, t)}
</span> </span>
</td> </td>
</tr> </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'> <ol className='mt-2 max-h-48 space-y-2 overflow-y-auto pe-1'>
{taskFlowRecords.slice(0, 8).map((item, idx) => { {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 source = formatRecordSource(item.source)
const detail = taskFlowDetail(item) const detail = taskFlowDetail(item)
return ( return (
@@ -1222,7 +1297,7 @@ function RunRelatedRecordsPanel({ deploymentId }: { deploymentId: string }) {
artifactTypeToneClass(item.artifact_type) artifactTypeToneClass(item.artifact_type)
)} )}
> >
{String(item.artifact_type || 'artifact')} {String(item.artifact_type || t('Artifact'))}
</span> </span>
</div> </div>
<p className='text-muted-foreground mt-1 line-clamp-2 text-[11px]'> <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' className='bg-muted/25 rounded-lg p-2'
> >
<p className='truncate font-mono text-[11px]'> <p className='truncate font-mono text-[11px]'>
{String(item.snapshot_id || 'snapshot')} {String(item.snapshot_id || t('Snapshot'))}
</p> </p>
<p className='text-muted-foreground mt-1 truncate text-[11px]'> <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 || '—')} {String(item.source_ref || '—')}
</p> </p>
</li> </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'> <ol className='mt-2 max-h-52 space-y-2 overflow-y-auto pe-1'>
{timeline.slice(0, 8).map((item, idx) => { {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) const source = formatRecordSource(item.source)
return ( return (
<li <li
@@ -1314,6 +1389,17 @@ const EVENT_LEVELS = ['all', 'info', 'warn', 'error'] as const
type EventLevel = (typeof EVENT_LEVELS)[number] 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 { function classifyEventLevel(entry: Record<string, unknown>): EventLevel {
const candidate = String( const candidate = String(
entry.level || entry.severity || entry.status || '' entry.level || entry.severity || entry.status || ''
@@ -1391,7 +1477,7 @@ export function AgnetEventsPage() {
<SelectContent> <SelectContent>
{EVENT_LEVELS.map((lv) => ( {EVENT_LEVELS.map((lv) => (
<SelectItem key={lv} value={lv}> <SelectItem key={lv} value={lv}>
{lv} {formatEventLevelLabel(lv, t)}
</SelectItem> </SelectItem>
))} ))}
</SelectContent> </SelectContent>
@@ -1655,6 +1741,7 @@ function AgnetApprovalCard({
onApprove: () => void onApprove: () => void
onReject: () => void onReject: () => void
}) { }) {
const { t } = useTranslation()
return ( return (
<article className='border-border/70 bg-background/45 rounded-xl border p-3'> <article className='border-border/70 bg-background/45 rounded-xl border p-3'>
<div className='flex flex-wrap items-start justify-between gap-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='expires_at' v={formatUnixMs(approval.expires_at)} />
<RedactedField <RedactedField
k='credential' k='credential'
v={approval.requires_credential ? 'required' : 'not required'} v={approval.requires_credential ? t('Required') : t('Not required')}
/> />
</dl> </dl>
<div className='mt-3 flex flex-wrap justify-end gap-2'> <div className='mt-3 flex flex-wrap justify-end gap-2'>
@@ -1685,14 +1772,14 @@ function AgnetApprovalCard({
disabled={rejectBusy || approveBusy} disabled={rejectBusy || approveBusy}
onClick={onReject} onClick={onReject}
> >
Reject {t('Reject')}
</Button> </Button>
<Button <Button
size='sm' size='sm'
disabled={approveBusy || rejectBusy} disabled={approveBusy || rejectBusy}
onClick={onApprove} onClick={onApprove}
> >
Approve {t('Approve')}
</Button> </Button>
</div> </div>
</article> </article>
@@ -1708,6 +1795,7 @@ function AgnetLeaseCard({
busy: boolean busy: boolean
onRevoke: () => void onRevoke: () => void
}) { }) {
const { t } = useTranslation()
return ( return (
<article className='border-border/70 bg-background/45 rounded-xl border p-3'> <article className='border-border/70 bg-background/45 rounded-xl border p-3'>
<div className='flex flex-wrap items-start justify-between gap-3'> <div className='flex flex-wrap items-start justify-between gap-3'>
@@ -1727,7 +1815,7 @@ function AgnetLeaseCard({
</dl> </dl>
<div className='mt-3 flex justify-end'> <div className='mt-3 flex justify-end'>
<Button size='sm' variant='outline' disabled={busy} onClick={onRevoke}> <Button size='sm' variant='outline' disabled={busy} onClick={onRevoke}>
Revoke {t('Revoke')}
</Button> </Button>
</div> </div>
</article> </article>
+66
View File
@@ -4081,9 +4081,75 @@
"Resource binding": "资源绑定", "Resource binding": "资源绑定",
"Show manifest": "展开权限清单", "Show manifest": "展开权限清单",
"Hide manifest": "收起权限清单", "Hide manifest": "收起权限清单",
"Resource": "资源",
"Allowed actions": "允许动作", "Allowed actions": "允许动作",
"Constraints": "约束", "Constraints": "约束",
"secret_ref": "密钥引用", "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 只是密钥保管器里的引用指针,不是密钥本体。", "Plaintext credentials are never shown. The secret_ref column is a vault pointer, not the secret itself.": "永远不展示明文密钥。表中的 secret_ref 只是密钥保管器里的引用指针,不是密钥本体。",
"Confirm before launch": "确认后启动", "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 已汇总本次会做什么、能用哪些资源、哪些事情不会做、以及预计消耗。请确认后启动。", "Heicode summarises what this run will do, what resources it can use, what stays off-limits, and the expected cost. Confirm to launch.": "Heicode 已汇总本次会做什么、能用哪些资源、哪些事情不会做、以及预计消耗。请确认后启动。",