Add Netx Ops i18n and promote settings to a dedicated section.

Localize alarm prompts and UI reason codes for zh/en, and restyle the settings page like uds-auth as its own settings.section.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
oliver 2026-09-09 22:09:47 +08:00
parent 909b5d61a7
commit a4993b2407
18 changed files with 1329 additions and 840 deletions

View file

@ -7,6 +7,7 @@ import {
publishAlarmPushStatus,
type AlarmPushPhase,
} from './alarm-push-status.ts'
import { alarmActionLabel, tHost } from './i18n.ts'
export function alarmSubscribeUrl(apiUrl: string): string {
const trimmed = apiUrl.trim().replace(/\/$/, '')
@ -42,16 +43,6 @@ export interface KeyAlarmPayload {
/** Human-readable prompt for a sticky Netx Ops DSH session. */
export function formatAlarmPrompt(payload: KeyAlarmPayload, lang = 'zh'): string {
const action = String(payload.action ?? '').trim().toLowerCase()
const actionZh: Record<string, string> = {
inserted: '告警产生',
updated: '告警更新',
deleted: '告警清除',
}
const actionEn: Record<string, string> = {
inserted: 'Alarm Raised',
updated: 'Alarm Updated',
deleted: 'Alarm Cleared',
}
const ne = payload.ne && typeof payload.ne === 'object' ? payload.ne : {}
const host = String(ne.host_name ?? '').trim()
const ip = String(ne.ip_address ?? '').trim()
@ -59,32 +50,22 @@ export function formatAlarmPrompt(payload: KeyAlarmPayload, lang = 'zh'): string
let device = host || neName || String(payload.ne_id ?? '').trim() || '-'
if (ip) device = device === '-' ? ip : `${device} (${ip})`
const label = String(payload.rule_label ?? payload.native_probable_cause ?? '关键告警').trim()
if (lang.startsWith('en')) {
return [
`[NMS ${actionEn[action] ?? (action || 'Alarm')}] ${label}`,
`Device: ${device}`,
`Object: ${String(payload.object_name ?? '-').trim()}`,
`Severity: ${String(payload.perceived_severity ?? '-').trim()}`,
`Cause: ${String(payload.native_probable_cause ?? '-').trim()}`,
`Time: ${String(payload.time_created ?? '-').trim()}`,
`notificationId: ${String(payload.notification_id ?? '-').trim()}`,
`alarm_key: ${String(payload.alarm_key ?? '-').trim()}`,
'',
'Please analyze this key alarm and suggest next ops steps.',
].join('\n')
}
const label = String(
payload.rule_label
?? payload.native_probable_cause
?? tHost('alarm.defaultLabel', lang),
).trim()
return [
`[NMS ${actionZh[action] ?? (action || '告警')}] ${label}`,
`设备: ${device}`,
`对象: ${String(payload.object_name ?? '-').trim()}`,
`级别: ${String(payload.perceived_severity ?? '-').trim()}`,
`原因: ${String(payload.native_probable_cause ?? '-').trim()}`,
`时间: ${String(payload.time_created ?? '-').trim()}`,
`[NMS ${alarmActionLabel(action, lang)}] ${label}`,
`${tHost('alarm.field.device', lang)}: ${device}`,
`${tHost('alarm.field.object', lang)}: ${String(payload.object_name ?? '-').trim()}`,
`${tHost('alarm.field.severity', lang)}: ${String(payload.perceived_severity ?? '-').trim()}`,
`${tHost('alarm.field.cause', lang)}: ${String(payload.native_probable_cause ?? '-').trim()}`,
`${tHost('alarm.field.time', lang)}: ${String(payload.time_created ?? '-').trim()}`,
`notificationId: ${String(payload.notification_id ?? '-').trim()}`,
`alarm_key: ${String(payload.alarm_key ?? '-').trim()}`,
'',
'请分析这条关键告警并给出下一步运维建议。',
tHost('alarm.analyzeHint', lang),
].join('\n')
}

View file

@ -10,10 +10,10 @@ import { homedir } from 'node:os'
import { join } from 'node:path'
import type { Context } from '@deepseek-ai/cordis'
import { formatAlarmPrompt, type KeyAlarmPayload } from './alarm-push.ts'
import { tHost } from './i18n.ts'
const PRESET_ID = 'netxops'
const PERMISSION_PRESET = 'default'
const TITLE = 'Netx 关键告警'
interface StickyHandle {
sessionId: string
@ -75,7 +75,7 @@ export async function deliverAlarmToSession(
}
}
await createStickySession(ctx, prompt)
await createStickySession(ctx, prompt, lang)
}
const next = deliveryChain.then(run, run)
@ -121,7 +121,7 @@ async function followup(ctx: Context, agent: { followup: (msg: unknown) => unkno
}
}
async function createStickySession(ctx: Context, prompt: string): Promise<void> {
async function createStickySession(ctx: Context, prompt: string, lang = 'zh'): Promise<void> {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const c = ctx as any
const agents = c.agents
@ -179,7 +179,7 @@ async function createStickySession(ctx: Context, prompt: string): Promise<void>
permissionPresets.set(handle.agent.session, PERMISSION_PRESET)
}
if (sessionTitle && typeof sessionTitle.rename === 'function') {
sessionTitle.rename(handle.agent.session, TITLE)
sessionTitle.rename(handle.agent.session, tHost('alarm.sessionTitle', lang))
}
await followup(ctx, handle.agent, prompt)
sticky = {

90
src/netx/i18n.ts Normal file
View file

@ -0,0 +1,90 @@
/**
* Host-plane Netx Ops i18n (alarm prompts / session titles).
* UI copy lives in `src/client/locales.ts` via dsh-client-locale.
*/
export type NetxLocale = 'zh' | 'en'
export type NetxHostMessageKey =
| 'alarm.sessionTitle'
| 'alarm.defaultLabel'
| 'alarm.action.inserted'
| 'alarm.action.updated'
| 'alarm.action.deleted'
| 'alarm.action.fallback'
| 'alarm.field.device'
| 'alarm.field.object'
| 'alarm.field.severity'
| 'alarm.field.cause'
| 'alarm.field.time'
| 'alarm.analyzeHint'
const ZH: Record<NetxHostMessageKey, string> = {
'alarm.sessionTitle': 'Netx 关键告警',
'alarm.defaultLabel': '关键告警',
'alarm.action.inserted': '告警产生',
'alarm.action.updated': '告警更新',
'alarm.action.deleted': '告警清除',
'alarm.action.fallback': '告警',
'alarm.field.device': '设备',
'alarm.field.object': '对象',
'alarm.field.severity': '级别',
'alarm.field.cause': '原因',
'alarm.field.time': '时间',
'alarm.analyzeHint': '请分析这条关键告警并给出下一步运维建议。',
}
const EN: Record<NetxHostMessageKey, string> = {
'alarm.sessionTitle': 'Netx key alarm',
'alarm.defaultLabel': 'Key alarm',
'alarm.action.inserted': 'Alarm Raised',
'alarm.action.updated': 'Alarm Updated',
'alarm.action.deleted': 'Alarm Cleared',
'alarm.action.fallback': 'Alarm',
'alarm.field.device': 'Device',
'alarm.field.object': 'Object',
'alarm.field.severity': 'Severity',
'alarm.field.cause': 'Cause',
'alarm.field.time': 'Time',
'alarm.analyzeHint': 'Please analyze this key alarm and suggest next ops steps.',
}
const TABLES: Record<NetxLocale, Record<NetxHostMessageKey, string>> = {
zh: ZH,
en: EN,
}
/** Map plugin `lang` / Accept-Language-ish values to zh|en (default zh). */
export function normalizeNetxLocale(lang: string | undefined | null): NetxLocale {
const normalized = String(lang ?? '').trim().toLowerCase()
if (normalized === 'english' || /^en(?:[-_].*)?$/u.test(normalized)) return 'en'
return 'zh'
}
/**
* Translate a host-plane message key.
* @param key - stable message id.
* @param lang - plugin lang or locale tag.
* @param vars - `{name}` replacements.
*/
export function tHost(
key: NetxHostMessageKey,
lang: string | undefined | null = 'zh',
vars?: Record<string, string | number>,
): string {
const locale = normalizeNetxLocale(lang)
const template = TABLES[locale][key] ?? TABLES.zh[key] ?? key
if (vars === undefined) return template
return template.replace(/\{(\w+)\}/g, (match, name: string) => (
Object.prototype.hasOwnProperty.call(vars, name) ? String(vars[name]) : match
))
}
/** Alarm action verb for prompt headers. */
export function alarmActionLabel(action: string, lang: string | undefined | null): string {
const normalized = String(action ?? '').trim().toLowerCase()
if (normalized === 'inserted') return tHost('alarm.action.inserted', lang)
if (normalized === 'updated') return tHost('alarm.action.updated', lang)
if (normalized === 'deleted') return tHost('alarm.action.deleted', lang)
return normalized || tHost('alarm.action.fallback', lang)
}

View file

@ -132,6 +132,7 @@ export async function getSessionsExportStatus(
available: false,
sessionCount: 0,
supportsRawArtifacts: false,
reasonCode: 'no_persistence',
reason: 'sessionPersistence unavailable — mount a JSONL session backend (web profile default)',
}
}
@ -140,6 +141,7 @@ export async function getSessionsExportStatus(
available: false,
sessionCount: 0,
supportsRawArtifacts: false,
reasonCode: 'no_raw_artifacts',
reason: 'persistence backend does not expose per-session raw artifacts (SQLite export unsupported)',
}
}
@ -156,6 +158,7 @@ export async function getSessionsExportStatus(
available: false,
sessionCount: 0,
supportsRawArtifacts: true,
reasonCode: 'list_failed',
reason: error instanceof Error ? error.message : String(error),
}
}