feat(ume): OClaw forwarder runtime task and i18n status codes

Register oclaw_alarm_forwarder in background tasks with pause/resume, emit rt:/ws:/fwd: codes for last_error, and translate them in the UI for English and Chinese.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
oliver 2026-06-24 10:29:10 +08:00
parent 5a00736f23
commit ba1a40f725
10 changed files with 380 additions and 37 deletions

View file

@ -342,6 +342,7 @@ const en = {
wsConnected: "connected",
wsDisconnected: "disconnected",
wsDisabled: "disabled",
wsPaused: "paused",
publishedOk: "published ok",
publishedFail: "published fail",
queue: "queue",
@ -409,6 +410,48 @@ const en = {
intervalTitle: "Configured sleep between loop iterations (clamped at process start)",
lastRunTitle: "Last finished sync for scheduled tasks; refreshed at each loop tick",
deletedTitle: "Reconcile deletes: inventory = NE rows; alarms_current = current alarm rows",
lastErrorCol: "last_error",
runtimeError: {
startup_alarm_sync_before_ws: "Starting up: syncing current alarms (WSS connects after completion)…",
startup_gate_waiting: "Startup REST snapshot pending; scheduled REST and WSS on standby",
wss_active_skip_rest: "WSS receiving live alarms; REST sync skipped",
pulling_alarms_current: "Pulling UME current alarms…",
alarms_sync_in_progress_skip: "Another current-alarm REST sync in progress; skipped",
pulling_inventory: "Pulling UME inventory…",
ume_ws_disabled_no_base_url: "Disabled or UME_BASE_URL not configured",
oclaw_fwd_disabled: "Disabled or NETX_OCLAW_ALARM_WS / token / url not configured",
resumed_sync_soon: "Resumed: skipping debounce wait, sync soon",
resumed_wss_reconnect: "Resumed: reconnecting WSS soon",
resumed_oclaw_wss_reconnect: "Resumed: reconnecting OClaw WSS soon",
resumed: "Resumed",
keepalive_failed: "Token keepalive failed",
},
wsState: {
init: "Initializing",
connected: "Connected",
connecting: "Connecting",
disconnected: "Disconnected",
no_subscription: "No subscription",
waiting_token: "Waiting for token",
paused: "Paused",
error: "Connection error",
reconnecting: "Reconnecting",
subscription_lost: "UME subscription lost",
disabled_no_base_url: "Disabled or UME_BASE_URL not configured",
},
fwdState: {
paused: "Paused",
disabled: "Disabled",
connecting: "Connecting",
connected: "Connected",
},
runtimeTask: {
token_keepalive: "UME token keepalive",
alarms_current_auto_sync: "Current alarms auto sync",
alarms_current_ws_consumer: "UME alarm WSS",
oclaw_alarm_forwarder: "OClaw key-alert WSS",
inventory_auto_sync: "Inventory auto sync",
},
},
syncStatus: {
title: "Sync history",

View file

@ -340,6 +340,7 @@ const zh = {
wsConnected: "已连接",
wsDisconnected: "未连接",
wsDisabled: "未启用",
wsPaused: "已暂停",
publishedOk: "发布成功",
publishedFail: "发布失败",
queue: "待发队列",
@ -407,6 +408,48 @@ const zh = {
intervalTitle: "Configured sleep between loop iterations (clamped at process start)",
lastRunTitle: "Last finished sync for scheduled tasks; refreshed at each loop tick",
deletedTitle: "全量对账删除条数:inventory 为网元行,alarms_current 为当前告警行,其余域多为 0",
lastErrorCol: "last_error",
runtimeError: {
startup_alarm_sync_before_ws: "启动中:正在同步当前告警(完成后连接 WSS)…",
startup_gate_waiting: "启动 REST 全量同步未完成,定时 REST 与 WSS 均待命",
wss_active_skip_rest: "WSS 实时接收中,已跳过 REST 同步",
pulling_alarms_current: "正在拉取 UME 当前告警…",
alarms_sync_in_progress_skip: "另一条当前告警 REST 同步进行中,已跳过",
pulling_inventory: "正在拉取 UME 网元清单…",
ume_ws_disabled_no_base_url: "未启用或未配置 UME_BASE_URL",
oclaw_fwd_disabled: "未启用或未配置 NETX_OCLAW_ALARM_WS / token / url",
resumed_sync_soon: "已恢复:将跳过本轮周期等待并尽快同步",
resumed_wss_reconnect: "已恢复:将尽快重连 WSS",
resumed_oclaw_wss_reconnect: "已恢复:将尽快重连 OClaw WSS",
resumed: "已恢复",
keepalive_failed: "Token 保活失败",
},
wsState: {
init: "初始化",
connected: "已连接",
connecting: "连接中",
disconnected: "已断开",
no_subscription: "无订阅",
waiting_token: "等待 token",
paused: "已暂停",
error: "连接异常",
reconnecting: "重连等待",
subscription_lost: "UME 订阅已丢失",
disabled_no_base_url: "未启用或未配置 UME_BASE_URL",
},
fwdState: {
paused: "已暂停",
disabled: "未启用",
connecting: "连接中",
connected: "已连接",
},
runtimeTask: {
token_keepalive: "UME Token 保活",
alarms_current_auto_sync: "当前告警定时同步",
alarms_current_ws_consumer: "UME 告警 WSS",
oclaw_alarm_forwarder: "OClaw 关键告警 WSS",
inventory_auto_sync: "Inventory 定时同步",
},
},
syncStatus: {
title: "同步状态",

View file

@ -27,6 +27,7 @@ import { useI18n } from "../i18n";
import { useToast } from "../hooks/useToast";
import type { UmeAlarmSubscriptionStatus, UmeKeyAlertRuleItem } from "../types";
import { pageCount, runtimeIntervalLabel } from "../utils/display";
import { runtimeLastError, wsConnectionLabel } from "../utils/runtimeMessages";
import { formatSystemTime } from "../utils/time";
export function UmePage() {
@ -278,8 +279,12 @@ export function UmePage() {
const wsState = String(wsConn?.state || "");
const wsPaused = Boolean(wsConsumer?.paused);
const wsLabel =
wsConn?.label ||
(wsPaused ? t("ume.subscription.wssPaused") : wsConsumer?.last_error || wsConsumer?.status || t("common.empty"));
(wsPaused
? t("ume.subscription.wssPaused")
: wsConnectionLabel(wsState, wsConn?.label, t)) ||
runtimeLastError(wsConsumer?.last_error, t) ||
wsConsumer?.status ||
t("common.empty");
const subscriptionActive = Boolean(alarmSub.active);
const serverSubLost = Boolean(
subscriptionStatusQuery.data?.server_subscription_lost ?? alarmSub.server_subscription_lost,
@ -303,7 +308,8 @@ export function UmePage() {
? "warn"
: wsState === "no_subscription" || wsState === "disconnected" || wsState === "error" || wsState === "init"
? "down"
: String(wsConsumer?.last_error || "").includes("connected")
: String(wsConsumer?.last_error || "").includes("connected") ||
String(wsConsumer?.last_error || "").includes("ws:connected")
? "up"
: "unknown";
const wsLogs = [...(subscriptionStatusQuery.data?.ws_logs ?? alarmSub.ws_logs ?? [])].reverse();
@ -420,9 +426,17 @@ export function UmePage() {
const oclawWsPill =
!keyAlertForwarder?.enabled
? "unknown"
: keyAlertForwarder.connected
? "up"
: "down";
: keyAlertForwarder.paused
? "warn"
: keyAlertForwarder.connected
? "up"
: "down";
const runtimeTaskLabel = (task: string) => {
const key = `ume.tasks.runtimeTask.${task}`;
const label = t(key);
return label === key ? task : label;
};
const perPage = (n: number) => t("common.perPage", { n: String(n) });
@ -524,7 +538,7 @@ export function UmePage() {
{subscriptionActive || wsConsumer ? (
<span
className={`conn-pill conn-pill--${wsPillLevel}`}
title={wsConn?.detail || wsConsumer?.last_error || alarmSub.wss_uri || ""}
title={wsConn?.detail || runtimeLastError(wsConsumer?.last_error, t) || alarmSub.wss_uri || ""}
>
WSS: {wsLabel}
</span>
@ -665,9 +679,11 @@ export function UmePage() {
{t("ume.keyAlert.ws")}:{" "}
{!keyAlertForwarder?.enabled
? t("ume.keyAlert.wsDisabled")
: keyAlertForwarder.connected
? t("ume.keyAlert.wsConnected")
: t("ume.keyAlert.wsDisconnected")}
: keyAlertForwarder.paused
? t("ume.keyAlert.wsPaused")
: keyAlertForwarder.connected
? t("ume.keyAlert.wsConnected")
: t("ume.keyAlert.wsDisconnected")}
</span>
{keyAlertForwarder?.enabled ? (
<>
@ -1078,20 +1094,20 @@ export function UmePage() {
<th title={t("ume.tasks.intervalTitle")}>interval</th>
<th>status</th>
<th title={t("ume.tasks.lastRunTitle")}>last_run_at</th>
<th>last_error</th>
<th>{t("ume.tasks.lastErrorCol")}</th>
<th>actions</th>
</tr>
</thead>
<tbody>
{runtimeTasks.map((x) => (
<tr key={`runtime-${x.task}`}>
<td>{x.task}</td>
<td title={x.task}>{runtimeTaskLabel(x.task)}</td>
<td title={typeof x.interval_s === "number" ? `${x.interval_s}s` : undefined}>
{runtimeIntervalLabel(x.interval_label)}
</td>
<td>{x.status}</td>
<td>{x.last_run_at ? formatSystemTime(x.last_run_at) : t("common.empty")}</td>
<td>{x.last_error || t("common.empty")}</td>
<td>{runtimeLastError(x.last_error, t) || t("common.empty")}</td>
<td>
{Boolean(x.paused) ? (
<button

View file

@ -39,6 +39,8 @@ export type UmeInventoryNeTypeItem = {
export type UmeKeyAlertForwarderStatus = {
enabled: boolean;
operational?: boolean;
paused?: boolean;
connected: boolean;
queue_size: number;
url: string;

View file

@ -0,0 +1,77 @@
/** Map backend runtime status codes (rt:* / ws:* / fwd:*) and legacy Chinese to i18n keys. */
type TFunc = (key: string) => string;
const LEGACY_ZH_RUNTIME_ERROR: Record<string, string> = {
"启动:正在同步当前告警(完成后连接 WSS)…": "ume.tasks.runtimeError.startup_alarm_sync_before_ws",
"启动 REST 全量同步未完成,定时 REST 与 WSS 均待命": "ume.tasks.runtimeError.startup_gate_waiting",
"WSS 实时接收中,已跳过 REST 同步": "ume.tasks.runtimeError.wss_active_skip_rest",
"正在拉取 UME 当前告警…": "ume.tasks.runtimeError.pulling_alarms_current",
"另一条当前告警 REST 同步进行中,已跳过": "ume.tasks.runtimeError.alarms_sync_in_progress_skip",
"正在拉取 UME 网元清单…": "ume.tasks.runtimeError.pulling_inventory",
"未启用或未配置 UME_BASE_URL": "ume.tasks.runtimeError.ume_ws_disabled_no_base_url",
"未启用或未配置 NETX_OCLAW_ALARM_WS / token / url": "ume.tasks.runtimeError.oclaw_fwd_disabled",
"已恢复:将跳过本轮周期等待并尽快同步": "ume.tasks.runtimeError.resumed_sync_soon",
"已恢复:将尽快重连 WSS": "ume.tasks.runtimeError.resumed_wss_reconnect",
"已恢复:将尽快重连 OClaw WSS": "ume.tasks.runtimeError.resumed_oclaw_wss_reconnect",
已恢复: "ume.tasks.runtimeError.resumed",
初始化: "ume.tasks.wsState.init",
已连接: "ume.tasks.wsState.connected",
连接中: "ume.tasks.wsState.connecting",
已断开: "ume.tasks.wsState.disconnected",
无订阅: "ume.tasks.wsState.no_subscription",
"等待 token": "ume.tasks.wsState.waiting_token",
已暂停: "ume.tasks.wsState.paused",
连接异常: "ume.tasks.wsState.error",
重连等待: "ume.tasks.wsState.reconnecting",
UME订阅已丢失: "ume.tasks.wsState.subscription_lost",
};
function translatePrefixed(code: string, prefix: string, i18nSection: string, t: TFunc): string | null {
if (!code.startsWith(prefix)) return null;
const slug = code.slice(prefix.length);
if (!slug) return null;
const key = `${i18nSection}.${slug}`;
const tr = t(key);
return tr !== key ? tr : null;
}
export function runtimeLastError(raw: string | null | undefined, t: TFunc): string {
const s = String(raw ?? "").trim();
if (!s) return "";
const rt = translatePrefixed(s, "rt:", "ume.tasks.runtimeError", t);
if (rt) return rt;
const ws = translatePrefixed(s, "ws:", "ume.tasks.wsState", t);
if (ws) return ws;
const fwd = translatePrefixed(s, "fwd:", "ume.tasks.fwdState", t);
if (fwd) return fwd;
if (s === "keepalive_failed") {
const tr = t("ume.tasks.runtimeError.keepalive_failed");
if (tr !== "ume.tasks.runtimeError.keepalive_failed") return tr;
}
const legacyKey = LEGACY_ZH_RUNTIME_ERROR[s];
if (legacyKey) {
const tr = t(legacyKey);
if (tr !== legacyKey) return tr;
}
return s;
}
export function wsConnectionLabel(
state: string | null | undefined,
label: string | null | undefined,
t: TFunc,
): string {
const fromLabel = runtimeLastError(label, t);
if (fromLabel && fromLabel !== String(label ?? "").trim()) return fromLabel;
const st = String(state ?? "").trim();
if (st) {
const tr = runtimeLastError(`ws:${st}`, t);
if (tr) return tr;
}
return fromLabel || st || "";
}