/** * 后端的错误信息是英文(API 契约就是英文):前端在这里把常见的错误码、字段名和校验信息翻成中文。 * 没有收录的信息原样显示,不会丢。 */ const CODES: Record = { validation_failed: '填写的内容有问题', budget_exceeded: '超出了这台节点本地设定的带宽或包速率上限', unauthorized: '用户名或密码不对,或者登录已经失效', csrf_failed: '登录状态已经过期,请刷新页面重新登录', forbidden: '没有权限做这件事', site_private: '站点未公开:请管理员登录,或在管理端把站点设为公开', not_found: '没有找到', agent_not_found: '没有找到这个 Agent', probe_not_found: '没有找到这个 Probe', probe_revoked: '这个 Probe 已经被吊销,不能再换密钥,请新建一个 Probe', probe_required: '有多个 Probe,请指定具体的线路', task_not_found: '没有找到这个任务', test_not_found: '没有找到这次测试', chunk_not_found: '没有找到这个原始数据块', token_not_found: '没有找到这个令牌', unknown_kind: '不认识这种采集方法', unknown_plan: '不认识这个测试方案', name_conflict: '名称已经被使用,换一个', probe_offline: '这个 Probe 现在没有连着这个 Agent', test_busy: '这条线路上已经有测试在运行', agent_not_active: '这个 Agent 还没有启用', agent_offline: 'Agent 不在线', raw_expired: '这份原始数据已经超过保留期限', join_token_expired: '加入令牌已经过期', precondition_failed: '这条记录刚被别人改过,请刷新后再改', payload_too_large: '提交的内容太大', range_too_large: '选的时间范围太大,请缩短或增大步长', invalid_step: '步长不合法', invalid_range: '时间范围不合法', unknown_metric: '不认识这个指标', unknown_preset: '不认识这个指标分组', too_many_metrics: '一次选的指标太多', too_many_requests: '请求太频繁,请稍后再试', too_many_streams: '实时连接太多', storage_unavailable: '存储暂时不可用,请稍后再试', fleet_unsupported: '这个 Agent 不支持这项功能', fleet_join_token_invalid: '加入令牌无效', fleet_node_disabled: '这个节点已被停用', internal_error: '服务器出错了,请稍后再试', method_not_allowed: '不支持这个操作', unsupported_media_type: '提交的格式不对', } const FIELDS: Record = { name: '名称', username: '用户名', password: '密码', note: '备注', labels: '标签', public_host: '公开地址', public_visible: '展示页可见', max_uses: '可用次数', expires_in_hours: '有效期', scope: '类型', kind: '采集方法', target: '目标', schedule: '调度', agent_id: 'Agent', base_task_id: '要覆盖的模板', plan: '方案', params: '参数', range: '时间范围', from: '开始时间', to: '结束时间', step: '步长', lines: '线路', metric: '指标', metrics: '指标', cursor: '分页游标', anchor: '时间锚点', tz: '时区', } const MESSAGES: [RegExp, (m: RegExpMatchArray) => string][] = [ [/^is required$/, () => '不能为空'], [/^must be 1-64 characters( of letters, digits|: letters \(any language\), digits), '\.', '_' or '-'$/, () => '只能是 1–64 个字、字母、数字、点、下划线或连字符(可以用中文)'], [/^must be 3-32 characters of letters, digits, '\.', '_' or '-'$/, () => '只能是 3–32 个字母、数字、点、下划线或连字符'], [/^must be 1-64 characters$/, () => '长度需要 1–64 个字符'], [/^must be (\d+)-(\d+) characters$/, (m) => `长度需要 ${m[1]}–${m[2]} 个字符`], [/^must be at most (\d+) characters$/, (m) => `最多 ${m[1]} 个字符`], [/^at most (\d+) labels of (\d+)-(\d+) characters$/, (m) => `最多 ${m[1]} 个标签,每个 ${m[2]}–${m[3]} 个字符`], [/^must be a host name or IP address$/, () => '必须是域名或 IP 地址'], [/^must be an IP address with an optional port$/, () => '必须是 IP 地址,端口可选'], [/^must be between (-?[\d.]+) and (-?[\d.]+)$/, (m) => `必须在 ${m[1]} 到 ${m[2]} 之间`], [/^must be an integer$/, () => '必须是整数'], [/^must be a number$/, () => '必须是数字'], [/^must be a boolean$/, () => '必须是 是 / 否'], [/^must be one of: (.*)$/, (m) => `只能是:${m[1]}`], [/^is not a parameter of this kind$/, () => '这种采集方法没有这个参数'], [/^must be line or target$/, () => '只能是线路任务或目标任务'], [/^must be read or admin$/, () => '只能是 read(只读)或 admin(全部权限)'], [/^must be now or latest$/, () => '只能是 now 或 latest'], [/^is not a valid cursor$/, () => '不是有效的分页游标'], [/^is mutually exclusive with from and to$/, () => '不能和开始、结束时间同时使用'], [/^only UTC is supported at the moment$/, () => '目前只支持 UTC'], [/^line tasks have no target$/, () => '线路任务没有目标'], [/^agent_id is required for target tasks$/, () => '目标任务必须指定 Agent'], [/^per-agent line tasks must be overrides \(base_task_id\)$/, () => '针对某个 Agent 的线路任务必须是覆盖(要选一个模板)'], [/^must reference a line template and requires agent_id$/, () => '必须引用一个线路模板,并且要指定 Agent'], [/^an override must use the same kind as its template$/, () => '覆盖必须和它的模板是同一种采集方法'], [/^once tasks are created through the tests API$/, () => '一次性测试请在「按需测试」里发起'], [/^unknown kind (.+)$/, (m) => `不认识采集方法 ${m[1]}`], [/^unknown plan (.+)$/, (m) => `不认识测试方案 ${m[1]}`], [/^a probe named (.+) already exists$/, (m) => `已经有一个叫 ${m[1]} 的 Probe`], [/^probe (\S+) has no session with this agent$/, () => '这个 Probe 现在没有连着这个 Agent'], [/^probe (\S+) is not connected to this agent$/, () => '这个 Probe 现在没有连着这个 Agent'], [/^this probe has no session with this agent$/, () => '这个 Probe 现在没有连着这个 Agent'], [/^the probe has no session with this agent$/, () => '这个 Probe 现在没有连着这个 Agent'], [/^another test is already running on this line$/, () => '这条线路上已经有测试在运行'], [/^agent is offline$/, () => 'Agent 不在线'], [/^agent is not active$/, () => 'Agent 还没有启用'], [/^agent not found$/, () => '没有找到这个 Agent'], [/^probe not found$/, () => '没有找到这个 Probe'], [/^line not found$/, () => '没有找到这条线路'], [/^there is no active probe$/, () => '现在没有可用的 Probe'], [/^several probes exist.*$/, () => '有多个 Probe,请指定具体的线路'], [/^a revoked probe cannot get a new key.*$/, () => '已吊销的 Probe 不能换密钥,请新建一个 Probe'], [/^this agent does not support (\S+), which the (\S+) plan needs$/, (m) => `这个 Agent 不支持 ${m[1]},而 ${m[2]} 方案需要它`], [/^the agent did not accept the test$/, () => 'Agent 没有接受这次测试'], [/^a valid X-CSRF-Token header is required$/, () => '登录状态已经过期,请刷新页面重新登录'], ] export function translateMessage(message: string): string { const text = message.trim() for (const [pattern, render] of MESSAGES) { const match = text.match(pattern) if (match) return render(match) } return text } export const translateField = (field: string): string => FIELDS[field] ?? field export function translateCode(code: string): string | undefined { return CODES[code] } const SETTINGS: Record = { 'site.public': '匿名访客能不能读展示页的数据(关闭时只有登录的管理员能看)', 'fleet.auto_link': '所有 Probe 自动连接所有 Agent(关闭后只连在「Probe → 线路」里手动选中的)', 'site.show_build': '展示页底部和节点卡片上显示后端、前端、Agent、Probe 的版本、tag 和构建时间', 'retention.raw_days': '持续任务的原始数据块保留多少天', 'retention.test_raw_days': '按需测试的原始数据块保留多少天', 'retention.metric_10s_hours': '10 秒精度的指标保留多少小时', 'retention.metric_1m_days': '1 分钟精度的指标保留多少天', 'retention.metric_5m_days': '5 分钟精度的指标保留多少天', 'retention.events_days': '事件保留多少天', 'retention.host_days': 'Agent 主机资源占用(每分钟一个样本)保留多少天', } const PLANS: Record = { hy2_ladder: { title: 'Hysteria2 阶梯测试', description: 'UDP 灌包,依次以 10、25、50、100 Mbit/s 各跑 10 秒,用来判断 Brutal 固定码率拥塞控制能不能跑得起来。' }, reality_session: { title: 'REALITY 会话测试', description: '建连测试、4 秒 TLS 批量传输(量吞吐峰值)和一个长连接保持。' }, } export const translateSetting = (key: string, fallback: string): string => SETTINGS[key] ?? fallback export const translatePlan = (id: string, title: string, description: string): { title: string; description: string } => PLANS[id] ?? { title, description }