refactor(web): unify form controls, one notification rule, dedupe i18n copy (#54)

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Yaowei Zheng
2026-07-24 18:43:18 +08:00
committed by GitHub
parent c7465625a4
commit 3f868ac385
50 changed files with 2391 additions and 1190 deletions
+64 -74
View File
@@ -5,7 +5,9 @@
* state/locale.tsx, which calls `setActiveStrings` to switch and remounts the whole
* tree keyed by locale, so `S.x` reads in components always reflect the current
* language (module-level constants do not update on switch — keep reads inside components).
* Keep domain terms capitalized in English — Agent, Workspace, Token, Task, etc.
* Keep domain terms capitalized in English — Workspace, Token, Task, Session, Project, Trace.
* "agent" is a common noun: lowercase mid-sentence, capitalized only at the start of a
* label/sentence or in a proper name (Agent State, AgentHub). zh keeps "Agent" as-is.
*/
export const zh = {
appName: "PenguinHarness",
@@ -63,20 +65,28 @@ export const zh = {
confirm: "确认",
close: "关闭",
loading: "加载中…",
loadMore: "加载更多",
saved: "已保存",
saving: "保存中…",
/** Clicking save with nothing changed: an info toast instead of a silent no-op. */
noChangesToSave: "当前没有需要保存的修改",
/** Confirm-before-save dialog shared by the settings forms (writes go to server-side config files). */
confirmSaveTitle: "保存修改",
confirmSaveBody: "确定保存这些修改吗?修改将写入服务器上的配置文件。",
none: "(无)",
retry: "重试",
unknownError: "请求失败,请稍后重试",
requiredField: "此项必填",
copied: "已复制",
ownerOnly: "仅 owner 可修改",
name: "名称",
username: "用户名",
role: "角色",
actions: "操作",
created: "创建时间",
cost: "成本",
time: "时间",
},
auth: {
loginTitle: "登录 PenguinHarness",
username: "用户名",
usernameHint: "2~32 位:小写字母开头,仅小写字母、数字与下划线",
password: "密码",
passwordHint: "至少 8 个字符",
@@ -101,12 +111,8 @@ export const zh = {
admin: {
users: "用户管理",
userId: "用户名",
role: "角色",
roleAdmin: "管理员",
roleUser: "用户",
createdAt: "创建时间",
actions: "操作",
createUser: "新增用户",
initialPassword: "初始密码",
initialPasswordFlag: "初始密码",
@@ -114,7 +120,6 @@ export const zh = {
resetPassword: "重置密码",
resetPasswordTitle: (u: string): string => `重置 ${u} 的密码`,
resetPasswordNote: "重置后该用户的登录会话全部失效,需用新密码重新登录",
deleteUser: "删除",
deleteUserTitle: (u: string): string => `删除用户 ${u}`,
deleteUserConfirm: (u: string): string =>
`将删除用户 ${u} 及其名下全部 Project(含数据目录),不可恢复。`,
@@ -132,12 +137,7 @@ export const zh = {
settingsTitle: "Project 设置",
members: "成员",
addMember: "添加成员",
memberUsername: "用户名",
memberRole: "角色",
memberActions: "操作",
removeMember: "移除",
owner: "owner",
member: "member",
deleteProject: "删除 Project",
deleteConfirm: "确认删除该 Project?项目目录将被递归删除,不可恢复。",
deleteDefaultForbidden: "default_project 与 CLI 共用,不允许在 Web 端删除",
@@ -155,7 +155,6 @@ export const zh = {
createTitle: "创建 Agent",
id: "Agent id",
idHint: "2~64 位:小写字母开头,仅小写字母、数字与下划线;创建后不可修改",
name: "名称",
nameHint: "留空则使用 Agent id 作为名称",
description: "描述",
activeSessions: "活跃 Session",
@@ -220,8 +219,6 @@ export const zh = {
maxTurnsInvalid: "max_turns 必须 > 0 或为 -1",
timeoutInvalid: "timeoutMs 必须 > 0 或为 -1",
toolFieldInvalid: (name: string, field: string) => `${name}: ${field} 必须是 > 0 的整数或 -1`,
builtinTools: "内置工具",
toolName: "名称",
toolPermission: "permission",
permissionReadLabel: "Read-only",
permissionReadDescription: "仅读取。审批模式为 read-only 时自动放行,无需确认。",
@@ -232,7 +229,6 @@ export const zh = {
mcpServers: "MCP Server(只读)",
defaultValue: "(缺省)",
/** Reset link next to the runtime dropdowns: rewinds the local pick back to "not overridden" (the menus offer no inherit row). */
resetToDefault: "恢复缺省",
deleteAgent: "删除 Agent",
builtinUndeletable: "内置 Agent 不可被删除",
deleteConfirm: (name: string): string =>
@@ -254,11 +250,14 @@ export const zh = {
addToGroup: "新增模型",
editTitle: "模型配置",
addTitle: "新增模型(OpenAI 协议)",
addTitleVendor: "新增模型(按 id 自动路由)",
addTitleVendor: "新增模型",
addProtocolHint:
"新增模型固定走 OpenAI Chat Completions 兼容协议(不按模型 id 自动路由),base URL 填其兼容端点",
addAutoRouteHint:
"该分组的新模型按上游 id 由 AgentHub 自动路由到厂商官方客户端:base URL 留空即官方端点,API key 留空按解析出的客户端读取环境变量",
/** Caution beside the base URL when the entry uses a vendor's official protocol (everything except the OpenAI-protocol path). */
baseUrlOfficialNote:
"注意:该模型走厂商官方协议——自定义 base URL 必须是兼容官方协议的端点,不会因此切换为 OpenAI 协议",
autoRouteNone:
"该 id 无法被 AgentHub 自动路由:请核对 id,或改在 Custom / 自建分组下以 OpenAI 协议接入",
addGroup: "新增分组",
@@ -317,21 +316,15 @@ export const zh = {
testing: "测试中…",
testOk: (ms: number): string => `连通正常(${ms} ms)`,
testFailed: (msg: string): string => `连通失败:${msg}`,
modelIdRenameHint: "改 id 后原配置与 API key 一并迁移",
priceCacheRead: "缓存读取价格",
priceCacheWrite: "缓存写入价格",
priceOutput: "输出价格",
priceUnit: "每百万 Token",
currency: "币种",
currencyUsd: "美元 $",
currencyCny: "人民币 ¥",
currencyHint: "价格按 1 美元 ≈ 7 人民币折算;存储始终为美元",
firstModelDefault: "已自动设为默认模型",
credential: "凭据",
apiKey: "API key",
apiKeyKeepHint: "留空保留现有 key",
apiKeyEnvHint: (envKey: string): string => `留空则使用环境变量 ${envKey}`,
useEnvVar: (envKey: string): string => `环境变量 ${envKey}`,
keyConfigured: "已配置 key",
clearApiKey: "清除已存 API key",
baseUrl: "自定义 base URL",
@@ -364,17 +357,14 @@ export const zh = {
confirmDefaultTitle: "设为默认模型",
confirmDefault: (name: string): string =>
`确定把「${name}」设为默认模型?新建的 Session 将默认使用它。`,
createdAt: "创建时间",
default: "默认",
setDefault: "设为默认模型",
remove: "删除模型",
saveAll: "保存全部",
readOnlyHint: "member 只读;模型与 credential 修改仅 owner 可执行",
empty: "尚未配置任何模型",
noKey: "未配置 key",
/** Chat model dropdown's bottom expander row: reveals the models hidden by the configured-key filter. */
showModelsWithoutKey: (n: number): string => `显示未配置 key 的模型(${n} 个)`,
pendingSave: "(待保存)",
modelIdExists: "该模型 id 已存在",
pricingAllOrNone: "三项价格需一并填写",
pricingInvalid: "必须为数字",
@@ -391,6 +381,8 @@ export const zh = {
remove: "删除",
deleteTitle: "删除环境变量",
deleteConfirm: (key: string): string => `确认删除环境变量「${key}」?值不可恢复。`,
overwriteTitle: "覆盖已有环境变量",
overwriteConfirm: (key: string): string => `「${key}」已存在,保存将覆盖原值且不可恢复。`,
empty: "尚未配置任何环境变量",
readOnlyHint: "member 只读;Vault 修改仅 owner 可执行",
keyHint: "字母、数字与下划线,不能以数字开头",
@@ -401,7 +393,6 @@ export const zh = {
schedule: {
desc: "定时任务(agent_state/schedule/*.toml):到点自动向目标 Session 发送 prompt;文件亦可手工编辑,Web 端修改后即时生效。",
readOnlyHint: "member 只读;定时任务修改仅 owner 可执行",
colName: "名称",
colStatus: "状态",
colPeriod: "周期",
colTarget: "目标",
@@ -424,7 +415,6 @@ export const zh = {
disable: "停用",
addTitle: "新建定时任务",
editTitle: (name: string): string => `编辑定时任务「${name}」`,
name: "名称",
nameHint: "即文件名(不含 .toml),创建后不可改",
prompt: "Prompt",
enabled: "启用",
@@ -462,9 +452,16 @@ export const zh = {
installedToast: (skill: string, agent: string): string => `已将 ${skill} 安装到 ${agent}`,
updateOutdated: (n: number): string => `有新版本:更新 ${n} 个 Agent 的安装`,
updateAction: "更新",
updateConfirmTitle: (name: string): string => `更新 ${name}`,
updateConfirmWarning: (name: string): string =>
`更新 ${name} 会把库内当前副本重装到各 Agent,覆盖其已安装的文件——对已装技能的本地改动会丢失,如有需要请先导出备份。`,
updatedToast: (skill: string, n: number): string =>
`已将 ${skill} 更新到最新版(${n} 个 Agent)`,
uninstalledToast: (skill: string, agent: string): string => `已从 ${agent} 卸载 ${skill}`,
/** Uninstall confirmation: removing the installed copy deletes its files (local edits included). */
uninstallConfirmTitle: (name: string): string => `卸载 ${name}`,
uninstallConfirmBody: (skill: string, agent: string): string =>
`确定从 ${agent} 卸载 ${skill} 吗?已安装的技能文件(含本地改动)将被删除。`,
},
chat: {
@@ -494,7 +491,7 @@ export const zh = {
draftSubtitle: "最擅长 AI 开发任务的自进化 Agent",
/**
* Example task cards on the draft screen: one click auto-submits the canned prompt (game
* card first, RAG card below/after it). These are the FULL working prompts — the README and
* card, then the LoL-player card, then the RAG card). These are the FULL working prompts — the README and
* landing page show a condensed one-sentence version of the RAG example for reading, and
* the cards' own desc lines stay short, but what actually gets submitted stays detailed:
* build quality depends on it.
@@ -571,7 +568,6 @@ Penguin 视觉风格(见 web-design 技能),深色/浅色主题(<html da
"read-only": "放行只读(read-only)",
"always-ask": "总是询问(always-ask)",
} as Record<string, string>,
statusIdle: "空闲",
statusRunning: "运行中",
statusCompacting: "压缩中",
pendingApprovals: (n: number) => `${n} 个待审批`,
@@ -580,7 +576,6 @@ Penguin 视觉风格(见 web-design 技能),深色/浅色主题(<html da
inputPlaceholderShort: "输入消息…",
send: "发送",
stop: "停止",
compactingHint: "上下文压缩中,暂不接受新输入",
compact: "压缩上下文",
approve: "允许",
deny: "拒绝",
@@ -613,7 +608,6 @@ Penguin 视觉风格(见 web-design 技能),深色/浅色主题(<html da
infoPanel: "Session 信息",
sessionStats: "统计",
statTokens: "Token 累计",
statCost: "成本",
statElapsed: "用时",
statInput: "输入 tokens",
statCached: "已缓存",
@@ -625,7 +619,6 @@ Penguin 视觉风格(见 web-design 技能),深色/浅色主题(<html da
noSessions: "还没有 Session",
emptyStream: "发送一条消息开始对话",
historyLoadFailed: "历史消息加载失败",
sessionHealed: "Session 已自愈为新 id",
statsLabel: "统计信息",
removeImage: "移除图片",
openWorkspace: "打开工作区",
@@ -650,12 +643,6 @@ Penguin 视觉风格(见 web-design 技能),深色/浅色主题(<html da
handoffFrom: (agent: string) => `由 ${agent} 的对话交接而来`,
handoffBack: (title?: string) => (title ? `回到原对话:${title}` : "回到原对话"),
scheduledFrom: (name: string) => `由定时任务「${name}」触发`,
/** Source badge on session list rows (user-created sessions have no source). */
sourceNames: {
schedule: "定时",
subagent: "子",
} as Record<string, string>,
commandCompact: "/compact",
emptyGreeting: "开始一段新对话",
compactionRunning: (mode: string) => `压缩进行中(${mode})…`,
compactionDone: (mode: string): string =>
@@ -663,17 +650,13 @@ Penguin 视觉风格(见 web-design 技能),深色/浅色主题(<html da
compactionFailed: (status: string) =>
`[压缩] ${status === "aborted" ? "已中断" : "失败"},保留当前上下文`,
unknownTool: "(未知工具)",
toolCall: "工具调用",
workGroupTitle: "推理与工具",
workRunning: "运行中",
workDone: "运行完毕",
workGroupSteps: (n: number) => `${n} 步`,
approvalWaiting: "待审批",
copyCode: "复制代码",
copyStats: "复制统计",
copyReply: "复制回复",
copyMessage: "复制消息",
copied: "已复制",
deleteSession: "删除对话",
renameSession: "重命名对话",
renameSessionLabel: "标题",
@@ -681,13 +664,13 @@ Penguin 视觉风格(见 web-design 技能),深色/浅色主题(<html da
`确定删除「${title}」?该对话的消息与 Trace 将被移除,且不可恢复。`,
archiveSession: "归档",
unarchiveSession: "取消归档",
archivedGroup: (n: number) => `已归档(${n})`,
/** Sidebar group "reveal/load next page" row (display cap + server paging). */
loadMore: "更多",
/** Sidebar folders for automation-created sessions (one per origin), parallel to 已归档; wording matches the sourceNames badges. */
sourceGroups: {
/** Collapsed sidebar folders inside a group (lazy-loaded); the count is the group's exact server share. */
folderGroups: {
subagent: (n: number) => `子智能体(${n})`,
schedule: (n: number) => `定时任务(${n})`,
archived: (n: number) => `已归档(${n})`,
},
skillsBanner: (names: string[]): string => `使用技能:${names.join("、")}`,
},
@@ -702,11 +685,11 @@ Penguin 视觉风格(见 web-design 技能),深色/浅色主题(<html da
refresh: "刷新",
root: "根目录",
empty: "空目录",
name: "名称",
size: "大小",
modified: "修改时间",
previewUnsupported: "该类型不支持预览,请下载查看",
uploaded: "已上传",
/** Upload-overwrite confirmation: same-name files in the current directory will be replaced. */
overwriteTitle: "覆盖同名文件",
overwriteConfirm: (n: number): string => `当前目录已存在以下 ${n} 个同名文件,上传将覆盖:`,
loadFailed: "加载失败",
previewTruncated: "内容过大,预览已截断,请下载查看完整文件",
details: "详情",
@@ -723,16 +706,13 @@ Penguin 视觉风格(见 web-design 技能),深色/浅色主题(<html da
last7d: "近 7 天",
total: "累计",
tokens: "Token",
cost: "成本",
requests: "Requests",
from: "起始日期",
to: "结束日期",
colCacheRead: "cache_read",
colCacheWrite: "cache_write",
colOutput: "output",
colCost: "成本",
trendTitle: "近 30 天趋势",
uncostedNote: "* 含未配置 pricing 的记录,成本为不完全统计",
uncostedNote: "* 只计入配置了价格的模型成本",
filterAllAgents: "全部 Agent",
filterAllModels: "全部模型",
chartAgentCalls: "各 Agent 调用次数",
@@ -746,7 +726,6 @@ Penguin 视觉风格(见 web-design 技能),深色/浅色主题(<html da
errorsUnexpected: "未预期",
errorsExpected: "预期内",
errorsTopCode: "最常见",
errorsColTime: "时间",
errorsColCode: "来源 · 错误码",
errorsColKind: "类型",
errorsColMessage: "消息",
@@ -755,15 +734,8 @@ Penguin 视觉风格(见 web-design 技能),深色/浅色主题(<html da
traces: {
title: "轨迹观测",
filterAll: "全部",
filterModel: "model_msg",
filterEvent: "event_msg",
analysis: "轨迹观测",
timeline: "执行时间线",
laneLLM: "模型",
laneTools: "工具",
phaseApproval: "审批",
phaseExec: "执行",
kindThinking: "思考",
kindModelReply: "模型回复",
kindToolGen: "工具调用生成",
@@ -787,16 +759,9 @@ Penguin 视觉风格(见 web-design 技能),深色/浅色主题(<html da
taskOutput: "本轮输出 tokens",
cacheHit: "命中缓存",
hitRate: "命中率",
toolFailRate: "失败率",
compactions: "压缩次数",
compactionRound: "压缩",
usageTrend: "Token 用量趋势",
selectFile: "在左侧选择一个 Trace 文件",
empty: "该 Agent 暂无 Trace",
colTime: "时间",
colType: "类型",
colSummary: "摘要",
lines: (n: number) => `${n} 行`,
inProgress: "进行中",
systemPrompt: "系统提示词",
toolDefs: (n: number) => `工具定义(${n})`,
@@ -815,11 +780,9 @@ Penguin 视觉风格(见 web-design 技能),深色/浅色主题(<html da
summaryLabel: "评估说明",
/** Chart legend: older evaluation records with no model label (gray series). */
legendUnlabeled: "未标注模型",
colTime: "时间",
colVersion: "版本",
colModel: "模型",
colScore: "总分",
colCost: "成本",
colDuration: "耗时",
colCase: "题目",
colRun: "运行",
@@ -828,11 +791,38 @@ Penguin 视觉风格(见 web-design 技能),深色/浅色主题(<html da
// Server error code → localized copy (the server's message is hardcoded Chinese; this is only a fallback for unknown codes).
errors: {
unauthorized: "登录已过期,请重新登录",
networkError: "网络错误,请检查连接",
modelCredentialMissing: (modelId: string) =>
`模型 ${modelId} 还没有可用的 API key,请先在「模型」页为它配置`,
noDefaultModel: "该 Project 还没有默认模型,请先在「模型」页添加模型并设为默认",
/** Localized text for the common server error codes (server error messages are English-only); looked up by ApiError.code in apiErrorText, falling back to the raw message for unmapped codes. */
byCode: {
invalid_credentials: "用户名或密码错误。",
password_mismatch: "当前密码不正确。",
invalid_password: "密码至少 8 位。",
admin_required: "仅管理员可执行此操作。",
not_found: "资源不存在,或你没有访问权限。",
agent_not_found: "该 Agent 已不存在。",
agent_exists: "该 Agent id 已被占用。",
project_exists: "该 Project id 已被占用。",
user_exists: "该用户名已被占用。",
user_not_found: "该用户已不存在。",
cannot_delete_admin: "内置 admin 不可删除。",
member_not_found: "该用户不是本 Project 的成员。",
schedule_exists: "已存在同名定时任务。",
schedule_not_found: "该定时任务已不存在。",
unknown_skill: "该技能不在技能库中。",
file_not_found: "该文件已不存在。",
file_too_large: "文件过大。",
payload_too_large: "请求体过大。",
dir_not_absolute: "目录必须是绝对路径。",
not_a_dir: "该路径不是目录。",
path_not_found: "该路径不存在。",
workspace_missing: "该 Session 的 Workspace 已不存在。",
task_in_progress: "该 Session 已有任务在运行。",
version_conflict: "快照版本不高于当前版本。",
invalid_title: "标题无效。",
},
},
};