Add key-alarm push: reuse URL/token, dial netx hub, sticky DSH session (0.1.15).

Plugins card gains an alarm-push toggle; when enabled, netxops subscribes outbound and followups a Netx Ops session without requiring im/WhatsApp.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
oliver 2026-09-05 20:50:51 +08:00
parent 027f83dd00
commit 12f7018103
16 changed files with 966 additions and 31 deletions

View file

@ -147,6 +147,38 @@ export function NetxopsCard(props: NetxopsCardProps) {
onEdit={(text) => { props.edit('lang', text) }}
onReset={() => { props.resetField('lang') }}
/>
<div className="dsh-nx-field">
<div className="dsh-nx-fieldHead">
<label className="dsh-nx-label" htmlFor="netxops-alarm-push">{t('alarmPushEnabled')}</label>
{state.alarmPushEnabled.overridden
? (
<span className="dsh-nx-badges">
<span className="dsh-nx-badge">{t('overridden')}</span>
<button
type="button"
className="dsh-nx-reset"
disabled={disabled}
onClick={() => { props.resetField('alarmPushEnabled') }}
>
{t('reset')}
</button>
</span>
)
: null}
</div>
<label className="dsh-nx-checkRow" htmlFor="netxops-alarm-push">
<input
id="netxops-alarm-push"
type="checkbox"
checked={state.alarmPushEnabled.text === 'true'}
disabled={disabled}
onChange={(event) => {
props.edit('alarmPushEnabled', event.target.checked ? 'true' : 'false')
}}
/>
<span>{t('alarmPushEnabledHint')}</span>
</label>
</div>
<div className="dsh-nx-footer">
{state.failed ? <p className="dsh-nx-failed" role="status">{t('saveFailed')}</p> : null}
<button

View file

@ -62,6 +62,24 @@ export function textField(field: string): CardFieldSpec {
}
}
/** Boolean settings field rendered as a checkbox (stores true / clears when false). */
export function booleanField(field: string): CardFieldSpec {
return {
field,
format: value => (value === true ? 'true' : 'false'),
parse: (text) => {
const normalized = text.trim().toLowerCase()
if (normalized === 'true' || normalized === '1' || normalized === 'yes') {
return { kind: 'set', value: true }
}
if (normalized === 'false' || normalized === '0' || normalized === 'no' || normalized === '') {
return { kind: 'clear' }
}
return undefined
},
}
}
export class CardForm<T> {
private readonly specs: Map<string, CardFieldSpec>
private readonly secretSpecs: Map<string, CardSecretSpec>

View file

@ -7,7 +7,7 @@ import type {} from '@deepseek-ai/dsh-api-remotes/client'
import type { SettingsScope, SettingsScopeSnapshot } from '@deepseek-ai/dsh-client-ui-settings/client'
import type { SnapshotStore } from './snapshot-store.ts'
import {
CardForm, textField,
CardForm, textField, booleanField,
type CardActions, type CardFieldState, type CardShell,
} from './card-form.ts'
@ -19,6 +19,7 @@ export interface NetxopsSettings {
apiUrl?: string
lang?: string
tokenCredentialRef?: string
alarmPushEnabled?: boolean
}
interface CredentialState {
@ -31,6 +32,7 @@ interface CredentialState {
export interface NetxopsCardState extends CardShell {
apiUrl: CardFieldState
lang: CardFieldState
alarmPushEnabled: CardFieldState
apiToken: CardFieldState
apiTokenConfigured: boolean
apiTokenWritable: boolean
@ -64,7 +66,7 @@ export class NetxopsCardController {
) {
this.form = new CardForm(
scope,
[textField('apiUrl'), textField('lang')],
[textField('apiUrl'), textField('lang'), booleanField('alarmPushEnabled')],
[{ field: API_TOKEN_FIELD, write: text => this.writeToken(text) }],
)
this.store = this.form.bind(() => this.projection())
@ -89,6 +91,7 @@ export class NetxopsCardController {
...this.form.shell(),
apiUrl: this.form.field('apiUrl'),
lang: this.form.field('lang'),
alarmPushEnabled: this.form.field('alarmPushEnabled'),
apiToken: this.form.field(API_TOKEN_FIELD),
apiTokenConfigured: this.credential.configured,
apiTokenWritable: this.credential.remoteReady && this.credential.writable,

View file

@ -7,6 +7,8 @@ export type NetxopsLocaleKey =
| 'apiUrlHint'
| 'lang'
| 'langHint'
| 'alarmPushEnabled'
| 'alarmPushEnabledHint'
| 'apiToken'
| 'apiTokenHint'
| 'apiTokenUnavailable'
@ -28,9 +30,11 @@ export const en: Record<NetxopsLocaleKey, string> = {
title: 'Netx Ops',
description: 'UME API endpoint and bearer token for netx__* tools.',
apiUrl: 'API URL',
apiUrlHint: 'netx REST root, e.g. http://127.0.0.1:8890',
apiUrlHint: 'netx REST root, e.g. http://127.0.0.1:8890 (also used for alarm subscribe)',
lang: 'Language',
langHint: 'Response language hint (zh / en).',
alarmPushEnabled: 'Key-alarm push',
alarmPushEnabledHint: 'Dial out to netx and open/follow a DSH session when a matched key alarm arrives. WhatsApp/im is optional and not required.',
apiToken: 'API token',
apiTokenHint: 'Stored as credential NETX_API_TOKEN (never written into settings). Leave blank to keep the current token.',
apiTokenUnavailable: 'This DSH build does not expose remote.credentials. Set the token with scripts/set-netx-token.ps1 (or .sh), then restart is not required if credentials are watched.',
@ -53,9 +57,11 @@ export const zh: Record<NetxopsLocaleKey, string> = {
title: 'Netx Ops',
description: 'UME API 地址与 Bearer Token,供 netx__* 工具使用。',
apiUrl: 'API 地址',
apiUrlHint: 'netx REST 根地址,例如 http://127.0.0.1:8890',
apiUrlHint: 'netx REST 根地址,例如 http://127.0.0.1:8890(告警订阅复用同一地址)',
lang: '语言',
langHint: '响应语言提示(zh / en)。',
alarmPushEnabled: '关键告警推送',
alarmPushEnabledHint: '主动连接 netx;匹配到关键告警后在本机打开/续写一条 DSH 会话。不必安装 WhatsApp / im。',
apiToken: 'API Token',
apiTokenHint: '写入凭据 NETX_API_TOKEN(不会进 settings)。留空表示保留已有 token。',
apiTokenUnavailable: '当前 DSH 未提供 remote.credentials。请用 scripts/set-netx-token.ps1(或 .sh)写入 token;若 harness 在监视凭据文件则无需重启。',

View file

@ -29,6 +29,8 @@ const CSS = `
.dsh-nx-input:focus-visible{outline:none;border-color:var(--dsw-alias-brand-primary)}
.dsh-nx-inputInvalid{border-color:var(--dsw-alias-label-error)}
.dsh-nx-hint{margin:0;font-size:12px;line-height:1.5;color:var(--dsw-alias-label-tertiary)}
.dsh-nx-checkRow{display:flex;align-items:flex-start;gap:10px;font-size:12px;line-height:1.5;color:var(--dsw-alias-label-secondary);cursor:pointer}
.dsh-nx-checkRow input{margin-top:2px;flex:none}
.dsh-nx-invalid{margin:0;font-size:12px;line-height:1.5;color:var(--dsw-alias-label-error)}
.dsh-nx-footer{display:flex;align-items:center;justify-content:flex-end;gap:8px;padding:12px 0 4px;border-top:.5px solid var(--dsw-alias-border-l2)}
.dsh-nx-failed{flex:1;min-width:0;margin:0;font-size:12px;line-height:1.5;color:var(--dsw-alias-label-error)}