Simplify settings copy and polish responsive form layout

This commit is contained in:
oliver 2026-10-11 13:02:39 +08:00
parent 0b9a3a82bc
commit 9271494b37
8 changed files with 590 additions and 690 deletions

View file

@ -78,6 +78,10 @@
## 验证与维护
设置页已精简重复的“自动保存”“即时挂载/卸载”和“已覆盖”提示。发布范围使用简短标签,挂载解释与明细集中在折叠的诊断信息中;模型语言独立分组,使用本地化选项。连接字段在宽面板并排、窄面板单列,保存/丢弃栏仅在待处理修改、保存中或保存失败时出现。实际错误仍显示在对应区域。
本次界面验证覆盖运行中的桌面深色设置页、390px 窄屏布局、中英文文案、预设列表展开与选择,以及手工草稿保存后收起操作栏。预览凭据服务为内存模拟,不读取或写入真实 token。预览截图已更新为精简后的界面。
验证命令:
```powershell

Binary file not shown.

Before

Width:  |  Height:  |  Size: 39 KiB

After

Width:  |  Height:  |  Size: 18 KiB

Before After
Before After

Binary file not shown.

Before

Width:  |  Height:  |  Size: 121 KiB

After

Width:  |  Height:  |  Size: 73 KiB

Before After
Before After

View file

@ -23,7 +23,15 @@ const scope = {
async set(field: string, value: unknown) { user[field] = value; for (const fn of listeners) fn() },
async unset(field: string) { delete user[field]; for (const fn of listeners) fn() },
}
const controller = new NetxopsCardController(scope, { get: () => undefined } as never)
const previewCredentials = {
async describe(refs: string[]) {
return { ok: true, value: Object.fromEntries(refs.map(ref => [ref, { configured: true, writable: true }])) }
},
async set() { /* Preview only; no credentials are stored. */ },
}
const controller = new NetxopsCardController(scope, {
get: key => key === 'remote.credentials' ? previewCredentials : undefined,
} as never)
controller.setAlarmPushRpc(async (_channel, endpoint) => {
const value = scope.getSnapshot().value
if (endpoint === 'presets.catalog') return { ok: true, value: { presets: [

File diff suppressed because it is too large Load diff

View file

@ -1,8 +1,6 @@
/**
* Netx Ops settings section — uds-auth visual language (page header + cards).
*/
/** Netx Ops settings: connection, publication, knowledge and delivery. */
import { useEffect, useRef, useState, type ReactNode } from 'react'
import type { ReactNode } from 'react'
import type { InjectFace, PropsLocale, PropsRuntime } from '@deepseek-ai/dsh-client-ui-slots'
import type { NetxopsCardFace, NetxopsCardState } from './controller.ts'
import type { NetxopsLocaleKey } from './locales.ts'
@ -66,7 +64,7 @@ function kbBadgeLabel(
return t('kbStatusUnconfigured')
}
if (snapshot.status === 'error') {
return fillTemplate(t('kbStatusError'), { detail: snapshot.errorMessage || 'error' })
return t('kbStatusError')
}
return fillTemplate(t('kbStatusConfigured'), {
operator: snapshot.operatorName,
@ -88,12 +86,11 @@ function toolsBadgeLabel(
if (!view) return t('toolsStatusUnknown')
if (!view.toolsEnabled) return t('toolsDisabled')
if (view.error) {
return fillTemplate(t('toolsStatusError'), { detail: view.error })
return t('toolsStatusError')
}
if (view.count > 0) {
return fillTemplate(t('toolsStatusMounted'), {
count: String(view.count),
version: view.version || '?',
})
}
return t('toolsStatusEmpty')
@ -136,9 +133,8 @@ export type NetxopsCardProps =
function ValueField(props: {
id: string
label: string
hint: string
hint?: string
field: CardFieldState
overriddenLabel: string
resetLabel: string
invalidLabel: string
disabled: boolean
@ -152,7 +148,6 @@ function ValueField(props: {
{props.field.overridden
? (
<span className="dsh-nx-badges">
<span className="dsh-nx-badge">{props.overriddenLabel}</span>
<button type="button" className="dsh-nx-reset" disabled={props.disabled} onClick={props.onReset}>
{props.resetLabel}
</button>
@ -164,14 +159,13 @@ function ValueField(props: {
id={props.id}
className={props.field.invalid ? 'dsh-nx-inputInvalid' : undefined}
type="text"
placeholder={props.hint}
value={props.field.text}
disabled={props.disabled}
aria-invalid={props.field.invalid || undefined}
onChange={(event) => { props.onEdit(event.target.value) }}
/>
<p className={props.field.invalid ? 'dsh-nx-invalid' : 'dsh-nx-hint'}>
{props.field.invalid ? props.invalidLabel : props.hint}
</p>
{props.field.invalid ? <p className="dsh-nx-invalid">{props.invalidLabel}</p> : null}
</div>
)
}
@ -179,10 +173,8 @@ function ValueField(props: {
function SelectField(props: {
id: string
label: string
hint: string
field: CardFieldState
options: ReadonlyArray<{ value: string, label: string }>
overriddenLabel: string
resetLabel: string
invalidLabel: string
disabled: boolean
@ -197,7 +189,6 @@ function SelectField(props: {
{props.field.overridden
? (
<span className="dsh-nx-badges">
<span className="dsh-nx-badge">{props.overriddenLabel}</span>
<button type="button" className="dsh-nx-reset" disabled={props.disabled} onClick={props.onReset}>
{props.resetLabel}
</button>
@ -220,9 +211,7 @@ function SelectField(props: {
<option key={row.value} value={row.value}>{row.label}</option>
))}
</select>
<p className={props.field.invalid ? 'dsh-nx-invalid' : 'dsh-nx-hint'}>
{props.field.invalid ? props.invalidLabel : props.hint}
</p>
{props.field.invalid ? <p className="dsh-nx-invalid">{props.invalidLabel}</p> : null}
</div>
)
}
@ -343,7 +332,6 @@ function PresetTargets(props: {
return (
<details className="dsh-nx-targets">
<summary>{props.t('selectedPresets')} · {selected.length}</summary>
<p className="dsh-nx-hint">{props.t('publicationHint')}</p>
{props.state.presetCatalogError ? <p className="dsh-nx-invalid">{props.t('presetCatalogUnavailable')}</p> : null}
{[...rows, ...unknown.map(id => ({ id, name: undefined, broken: undefined }))].map(row => (
<label key={row.id} className="dsh-nx-checkRow">
@ -361,6 +349,7 @@ function PresetTargets(props: {
function CapabilityGroupBlock(props: {
title: string
toolCount?: number
inPresetLabel: string
publicLabel: string
inPreset: CardFieldState
@ -371,8 +360,11 @@ function CapabilityGroupBlock(props: {
targets?: ReactNode
}) {
return (
<div className="dsh-nx-groupBlock">
<div className="dsh-nx-groupTitle">{props.title}</div>
<div className="dsh-nx-groupBlock" role="group" aria-label={props.title}>
<div className="dsh-nx-groupTitle">
<span>{props.title}</span>
{props.toolCount !== undefined ? <span className="dsh-nx-badgeMuted">{props.toolCount}</span> : null}
</div>
<div className="dsh-nx-groupChecks">
<label className="dsh-nx-checkRow">
<input
@ -404,33 +396,26 @@ function ToggleField(props: {
checked: boolean
disabled: boolean
overridden: boolean
overriddenLabel: string
resetLabel: string
onToggle: (checked: boolean) => void
onReset: () => void
trailing?: ReactNode
hint?: ReactNode
error?: ReactNode
}) {
return (
<div className="dsh-nx-settings-field">
<div className="dsh-nx-settings-field dsh-nx-toggleField">
<div className="dsh-nx-field-head">
<label htmlFor={props.id}>{props.label}</label>
<span className="dsh-nx-badges">
{props.trailing}
{props.overridden
? (
<>
<span className="dsh-nx-badge">{props.overriddenLabel}</span>
<button type="button" className="dsh-nx-reset" disabled={props.disabled} onClick={props.onReset}>
{props.resetLabel}
</button>
</>
<button type="button" className="dsh-nx-reset" disabled={props.disabled} onClick={props.onReset}>
{props.resetLabel}
</button>
)
: null}
</span>
</div>
<label className="dsh-nx-checkRow" htmlFor={props.id}>
<input
id={props.id}
type="checkbox"
@ -439,8 +424,7 @@ function ToggleField(props: {
aria-label={props.label}
onChange={(event) => { props.onToggle(event.target.checked) }}
/>
</label>
{props.hint}
</div>
{props.error}
</div>
)
@ -450,23 +434,6 @@ export function NetxopsCard(props: NetxopsCardProps) {
ensureStyles()
const { t } = props
const state: NetxopsCardState = props.useNetxopsCard(snapshot => snapshot)
const [flash, setFlash] = useState<'ok' | 'err' | null>(null)
const saveStarted = useRef(false)
useEffect(() => {
if (state.saving) {
saveStarted.current = true
setFlash(null)
return
}
if (!saveStarted.current) return
saveStarted.current = false
if (state.failed) {
setFlash('err')
return
}
if (!state.dirty) setFlash('ok')
}, [state.dirty, state.failed, state.saving])
if (!state.available) {
return (
@ -490,7 +457,6 @@ export function NetxopsCard(props: NetxopsCardProps) {
<section className="dsh-nx-settings" aria-label={t('title')}>
<header>
<h2>{t('title')}</h2>
<p className="dsh-nx-settings-intro">{t('description')}</p>
</header>
{!state.writable
@ -499,25 +465,6 @@ export function NetxopsCard(props: NetxopsCardProps) {
<div className="dsh-nx-settings-card">
<h3>{t('sectionConnection')}</h3>
<div className="dsh-nx-settings-field">
<div className="dsh-nx-field-head">
<span>{t('toolsStatus')}</span>
<StatusBadge
tone={toolsMountTone(state.toolsStatus)}
label={toolsBadgeLabel(t, state.toolsStatus)}
/>
</div>
<p className="dsh-nx-hint">{t('mountStatusHint')}</p>
{state.toolsStatus && state.toolsStatus.mounts.length > 0 ? (
<details><summary>{t('mountDetails')}</summary>
{state.toolsStatus.mounts.map((mount, index) => (
<p key={`${mount.source}:${index}`} className={mount.error ? 'dsh-nx-invalid' : 'dsh-nx-hint'}>
{mount.source} · {mount.count} · {mount.groups.join(', ') || '—'}{mount.error ? ` · ${mount.error}` : ''}
</p>
))}
</details>
) : null}
</div>
<div className="dsh-nx-settings-field">
<div className="dsh-nx-field-head">
<label htmlFor="netxops-api-token">{t('apiToken')}</label>
@ -531,96 +478,106 @@ export function NetxopsCard(props: NetxopsCardProps) {
id="netxops-api-token"
type="password"
autoComplete="off"
placeholder={t('apiTokenHint')}
value={state.apiToken.text}
disabled={!state.apiTokenWritable}
onChange={(event) => { props.edit('apiToken', event.target.value) }}
/>
<p className="dsh-nx-hint">
{state.apiTokenRemoteReady ? t('apiTokenHint') : t('apiTokenUnavailable')}
</p>
{!state.apiTokenRemoteReady ? <p className="dsh-nx-invalid">{t('apiTokenUnavailable')}</p> : null}
</div>
<ValueField
id="netxops-api-url"
label={t('apiUrl')}
hint={t('apiUrlHint')}
field={state.apiUrl}
overriddenLabel={t('overridden')}
resetLabel={t('reset')}
invalidLabel={t('apiUrlInvalid')}
disabled={disabled}
onEdit={(text) => { props.edit('apiUrl', text) }}
onReset={() => { props.resetField('apiUrl') }}
/>
<ValueField
id="netxops-lang"
label={t('lang')}
hint={t('langHint')}
field={state.lang}
overriddenLabel={t('overridden')}
resetLabel={t('reset')}
invalidLabel={t('invalid')}
disabled={disabled}
onEdit={(text) => { props.edit('lang', text) }}
onReset={() => { props.resetField('lang') }}
/>
<SelectField
id="netxops-thinking-language"
label={t('thinkingLanguage')}
hint={t('thinkingLanguageHint')}
field={state.thinkingLanguage}
options={[
{ value: 'auto', label: 'auto' },
{ value: 'zh-CN', label: 'zh-CN' },
{ value: 'en', label: 'en' },
]}
overriddenLabel={t('overridden')}
resetLabel={t('reset')}
invalidLabel={t('invalid')}
disabled={disabled}
onEdit={(text) => { props.edit('thinkingLanguage', text) }}
onReset={() => { props.resetField('thinkingLanguage') }}
/>
<SelectField
id="netxops-reply-language"
label={t('replyLanguage')}
hint={t('replyLanguageHint')}
field={state.replyLanguage}
options={[
{ value: 'follow-user', label: 'follow-user' },
{ value: 'zh', label: 'zh' },
{ value: 'en', label: 'en' },
]}
overriddenLabel={t('overridden')}
resetLabel={t('reset')}
invalidLabel={t('invalid')}
disabled={disabled}
onEdit={(text) => { props.edit('replyLanguage', text) }}
onReset={() => { props.resetField('replyLanguage') }}
/>
<ValueField
id="netxops-nms-provider"
label={t('nmsProvider')}
hint={t('nmsProviderHint')}
field={state.nmsProvider}
overriddenLabel={t('overridden')}
resetLabel={t('reset')}
invalidLabel={t('invalid')}
disabled={disabled}
onEdit={(text) => { props.edit('nmsProvider', text) }}
onReset={() => { props.resetField('nmsProvider') }}
/>
<div className="dsh-nx-fieldGrid">
<SelectField
id="netxops-lang"
label={t('lang')}
field={state.lang}
options={[
{ value: 'zh', label: t('languageChinese') },
{ value: 'en', label: t('languageEnglish') },
]}
resetLabel={t('reset')}
invalidLabel={t('invalid')}
disabled={disabled}
onEdit={(text) => { props.edit('lang', text) }}
onReset={() => { props.resetField('lang') }}
/>
<SelectField
id="netxops-nms-provider"
label={t('nmsProvider')}
field={state.nmsProvider}
options={[{ value: 'zte-ume', label: 'ZTE UME' }]}
resetLabel={t('reset')}
invalidLabel={t('invalid')}
disabled={disabled}
onEdit={(text) => { props.edit('nmsProvider', text) }}
onReset={() => { props.resetField('nmsProvider') }}
/>
</div>
</div>
<div className="dsh-nx-settings-card">
<h3>{t('sectionCapabilities')}</h3>
<div className="dsh-nx-card-head">
<h3>{t('sectionLanguage')}</h3>
<span className="dsh-nx-hint">{t('languageScope')}</span>
</div>
<div className="dsh-nx-fieldGrid">
<SelectField
id="netxops-thinking-language"
label={t('thinkingLanguage')}
field={state.thinkingLanguage}
options={[
{ value: 'auto', label: t('languageAuto') },
{ value: 'zh-CN', label: t('languageChinese') },
{ value: 'en', label: t('languageEnglish') },
]}
resetLabel={t('reset')}
invalidLabel={t('invalid')}
disabled={disabled}
onEdit={(text) => { props.edit('thinkingLanguage', text) }}
onReset={() => { props.resetField('thinkingLanguage') }}
/>
<SelectField
id="netxops-reply-language"
label={t('replyLanguage')}
field={state.replyLanguage}
options={[
{ value: 'follow-user', label: t('languageFollowUser') },
{ value: 'zh', label: t('languageChinese') },
{ value: 'en', label: t('languageEnglish') },
]}
resetLabel={t('reset')}
invalidLabel={t('invalid')}
disabled={disabled}
onEdit={(text) => { props.edit('replyLanguage', text) }}
onReset={() => { props.resetField('replyLanguage') }}
/>
</div>
</div>
<div className="dsh-nx-settings-card">
<div className="dsh-nx-card-head">
<h3>{t('sectionCapabilities')}</h3>
<StatusBadge tone={toolsMountTone(state.toolsStatus)} label={toolsBadgeLabel(t, state.toolsStatus)} />
</div>
<ToggleField id="netxops-tools-enabled" label={t('toolsEnabled')}
checked={state.toolsEnabled.text === 'true'} disabled={disabled}
overridden={state.toolsEnabled.overridden} overriddenLabel={t('overridden')} resetLabel={t('reset')}
overridden={state.toolsEnabled.overridden} resetLabel={t('reset')}
onToggle={checked => { props.edit('toolsEnabled', String(checked)) }}
onReset={() => { props.resetField('toolsEnabled') }}
hint={<p className="dsh-nx-hint">{t('toolsEnabledHint')}</p>} />
error={state.toolsStatus?.error ? <p className="dsh-nx-invalid" role="status">{state.toolsStatus.error}</p> : null} />
<CapabilityGroupBlock
title={`${t('groupOps')} · ${TOOLS_BY_GROUP.ops.length}`}
title={t('groupOps')}
toolCount={TOOLS_BY_GROUP.ops.length}
targets={targets('groupOpsPresets', state.groupOpsPresets)}
inPresetLabel={t('groupInPreset')}
publicLabel={t('groupPublic')}
@ -635,7 +592,8 @@ export function NetxopsCard(props: NetxopsCardProps) {
}}
/>
<CapabilityGroupBlock
title={`${t('groupTopology')} · ${TOOLS_BY_GROUP.topology.length}`}
title={t('groupTopology')}
toolCount={TOOLS_BY_GROUP.topology.length}
targets={targets('groupTopologyPresets', state.groupTopologyPresets)}
inPresetLabel={t('groupInPreset')}
publicLabel={t('groupPublic')}
@ -650,7 +608,8 @@ export function NetxopsCard(props: NetxopsCardProps) {
}}
/>
<CapabilityGroupBlock
title={`${t('groupBizMonitor')} · ${TOOLS_BY_GROUP.bizMonitor.length}`}
title={t('groupBizMonitor')}
toolCount={TOOLS_BY_GROUP.bizMonitor.length}
targets={targets('groupBizMonitorPresets', state.groupBizMonitorPresets)}
inPresetLabel={t('groupInPreset')}
publicLabel={t('groupPublic')}
@ -664,22 +623,30 @@ export function NetxopsCard(props: NetxopsCardProps) {
props.edit('groupBizMonitorPublic', checked ? 'true' : 'false')
}}
/>
{state.toolsStatus && state.toolsStatus.mounts.length > 0 ? (
<details className="dsh-nx-diagnostics"><summary>{t('mountDetails')}</summary>
<p className="dsh-nx-hint">{t('mountStatusHint')}</p>
{state.toolsStatus.mounts.map((mount, index) => (
<p key={`${mount.source}:${index}`} className={mount.error ? 'dsh-nx-invalid' : 'dsh-nx-hint'}>
{mount.source} · {mount.count} · {mount.groups.join(', ') || '—'}{mount.error ? ` · ${mount.error}` : ''}
</p>
))}
</details>
) : null}
</div>
<div className="dsh-nx-settings-card">
<h3>{t('sectionKnowledge')}</h3>
<div className="dsh-nx-card-head">
<h3>{t('sectionKnowledge')}</h3>
<StatusBadge tone={kbStatusTone(state.kbStatus?.status ?? 'unconfigured')} label={kbBadgeLabel(t, state.kbStatus)} />
</div>
<div className="dsh-nx-settings-field">
<div className="dsh-nx-field-head">
<label htmlFor="netxops-kb-root">{t('kbRoot')}</label>
<span className="dsh-nx-badges">
<StatusBadge
tone={kbStatusTone(state.kbStatus?.status ?? 'unconfigured')}
label={kbBadgeLabel(t, state.kbStatus)}
/>
{state.kbRoot.overridden
? (
<>
<span className="dsh-nx-badge">{t('overridden')}</span>
<button
type="button"
className="dsh-nx-reset"
@ -698,6 +665,7 @@ export function NetxopsCard(props: NetxopsCardProps) {
id="netxops-kb-root"
className={state.kbRoot.invalid ? 'dsh-nx-inputInvalid' : undefined}
type="text"
placeholder={t('kbRootHint')}
value={state.kbRoot.text}
disabled={disabled}
aria-invalid={state.kbRoot.invalid || undefined}
@ -706,20 +674,14 @@ export function NetxopsCard(props: NetxopsCardProps) {
<button
type="button"
className="dsh-nx-btn"
disabled={disabled}
disabled={disabled || !state.kbDirectoryPickerReady}
title={!state.kbDirectoryPickerReady ? t('kbBrowseUnavailable') : undefined}
onClick={() => { props.browseKbRoot() }}
>
{t('kbBrowse')}
</button>
</div>
<p className={state.kbRoot.invalid ? 'dsh-nx-invalid' : 'dsh-nx-hint'}>
{state.kbRoot.invalid
? t('invalid')
: state.kbDirectoryPickerReady
? t('kbRootHint')
: t('kbBrowseUnavailable')}
</p>
<p className="dsh-nx-hint">{t('kbSaveHint')}</p>
{state.kbRoot.invalid ? <p className="dsh-nx-invalid">{t('invalid')}</p> : null}
{state.kbUiError
? (
<p className="dsh-nx-invalid" role="status">
@ -730,7 +692,7 @@ export function NetxopsCard(props: NetxopsCardProps) {
{state.kbStatus?.status === 'error' && state.kbStatus.errorMessage
? <p className="dsh-nx-invalid" role="status">{state.kbStatus.errorMessage}</p>
: null}
{state.kbStatus?.status === 'configured' && state.kbStatus.realRoot
{state.kbStatus?.status === 'configured' && state.kbStatus.realRoot && state.kbStatus.realRoot !== state.kbRoot.text
? <p className="dsh-nx-hint">{state.kbStatus.realRoot}</p>
: null}
</div>
@ -759,7 +721,6 @@ export function NetxopsCard(props: NetxopsCardProps) {
checked={state.alarmPushEnabled.text === 'true'}
disabled={disabled}
overridden={state.alarmPushEnabled.overridden}
overriddenLabel={t('overridden')}
resetLabel={t('reset')}
onToggle={(checked) => {
props.edit('alarmPushEnabled', checked ? 'true' : 'false')
@ -769,11 +730,10 @@ export function NetxopsCard(props: NetxopsCardProps) {
? (
<StatusBadge
phase={pushStatus.phase}
label={`${t('alarmPushStatus')}: ${t(phaseLocaleKey(pushStatus.phase))}`}
label={t(phaseLocaleKey(pushStatus.phase))}
/>
)
: null}
hint={pushStatus?.wsUrl ? <p className="dsh-nx-hint">{pushStatus.wsUrl}</p> : null}
error={pushStatus?.lastError
? <p className="dsh-nx-invalid" role="status">{pushStatus.lastError}</p>
: null}
@ -784,7 +744,6 @@ export function NetxopsCard(props: NetxopsCardProps) {
checked={state.alarmDeliverDsh.text === 'true'}
disabled={disabled}
overridden={state.alarmDeliverDsh.overridden}
overriddenLabel={t('overridden')}
resetLabel={t('reset')}
onToggle={(checked) => {
props.edit('alarmDeliverDsh', checked ? 'true' : 'false')
@ -816,7 +775,6 @@ export function NetxopsCard(props: NetxopsCardProps) {
<div className="dsh-nx-settings-card">
<h3>{t('sectionExport')}</h3>
<div className="dsh-nx-settings-field">
<span className="dsh-nx-field-label">{t('sessionsExport')}</span>
{state.sessionsExportStatus === null
? <p className="dsh-nx-hint">{t('sessionsExportUnavailable')}</p>
: state.sessionsExportStatus.available
@ -863,7 +821,7 @@ export function NetxopsCard(props: NetxopsCardProps) {
</div>
</div>
<div className="dsh-nx-settings-actions">
{state.dirty || state.saving || state.failed ? <div className="dsh-nx-settings-actions">
<button
type="button"
className="dsh-nx-btn dsh-nx-btn-primary"
@ -876,23 +834,17 @@ export function NetxopsCard(props: NetxopsCardProps) {
type="button"
className="dsh-nx-btn dsh-nx-btn-ghost"
disabled={!state.dirty || state.saving}
onClick={() => {
props.discard()
setFlash(null)
}}
onClick={props.discard}
>
{t('discard')}
</button>
{state.dirty && !state.saving
? <span className="dsh-nx-settings-msg">{t('unsaved')}</span>
: null}
{flash === 'ok' && !state.dirty
? <span className="dsh-nx-settings-msg ok">{t('configSaved')}</span>
: null}
{(flash === 'err' || state.failed)
{state.failed
? <span className="dsh-nx-settings-msg err" role="status">{t('saveFailed')}</span>
: null}
</div>
</div> : null}
</section>
)
}

View file

@ -3,18 +3,21 @@
export type NetxopsLocaleKey =
| 'settingsUnavailable'
| 'toolsEnabled'
| 'toolsEnabledHint'
| 'toolsDisabled'
| 'selectedPresets'
| 'publicationHint'
| 'presetCatalogUnavailable'
| 'presetUnavailable'
| 'noOtherPresets'
| 'mountStatusHint'
| 'mountDetails'
| 'title'
| 'description'
| 'sectionConnection'
| 'sectionLanguage'
| 'languageScope'
| 'languageAuto'
| 'languageFollowUser'
| 'languageChinese'
| 'languageEnglish'
| 'sectionCapabilities'
| 'sectionKnowledge'
| 'sectionAlarms'
@ -23,14 +26,9 @@ export type NetxopsLocaleKey =
| 'apiUrlHint'
| 'apiUrlInvalid'
| 'lang'
| 'langHint'
| 'thinkingLanguage'
| 'thinkingLanguageHint'
| 'replyLanguage'
| 'replyLanguageHint'
| 'capabilityGroups'
| 'nmsProvider'
| 'nmsProviderHint'
| 'groupOps'
| 'groupTopology'
| 'groupBizMonitor'
@ -42,12 +40,10 @@ export type NetxopsLocaleKey =
| 'kbBrowse'
| 'kbBrowseUnavailable'
| 'kbBrowseFailed'
| 'kbSaveHint'
| 'kbStatusConfigured'
| 'kbStatusUnconfigured'
| 'kbStatusError'
| 'alarmPushEnabled'
| 'alarmPushStatus'
| 'alarmPushPhaseDisabled'
| 'alarmPushPhaseIdle'
| 'alarmPushPhaseConnecting'
@ -66,12 +62,10 @@ export type NetxopsLocaleKey =
| 'apiTokenUnavailable'
| 'apiTokenSet'
| 'apiTokenUnset'
| 'toolsStatus'
| 'toolsStatusMounted'
| 'toolsStatusEmpty'
| 'toolsStatusError'
| 'toolsStatusUnknown'
| 'sessionsExport'
| 'sessionsExportCount'
| 'sessionsExportUnavailable'
| 'sessionsExportNoPersistence'
@ -84,7 +78,6 @@ export type NetxopsLocaleKey =
| 'sessionsExportButton'
| 'sessionsExportBusy'
| 'sessionsExportDone'
| 'overridden'
| 'reset'
| 'invalid'
| 'unsaved'
@ -93,56 +86,51 @@ export type NetxopsLocaleKey =
| 'discard'
| 'save'
| 'saving'
| 'configSaved'
export const en: Record<NetxopsLocaleKey, string> = {
settingsUnavailable: 'Waiting for Host settings. Saving is disabled until connected. If this persists, check that the Host plugin is enabled.',
toolsEnabled: 'Publish Netx capabilities',
toolsEnabledHint: 'Auto-saves. Turn off to withdraw all Netx tools and skills; your group selections are retained.',
toolsDisabled: 'All Netx tools and skills are off',
selectedPresets: 'Selected other presets',
publicationHint: 'Tools and their skills are published together. Changes auto-save and apply to the next model request in current and new sessions. Public capabilities are inherited by every preset, including Netx Ops.',
presetCatalogUnavailable: 'Preset roster unavailable. Existing selections are retained and can be removed.',
settingsUnavailable: 'Connecting to Host settings…',
toolsEnabled: 'Enable tools and skills',
toolsDisabled: 'Disabled',
selectedPresets: 'Other presets',
presetCatalogUnavailable: 'Cannot load presets. Existing selections are retained.',
presetUnavailable: 'Preset unavailable',
noOtherPresets: 'No other presets available on this Host.',
mountStatusHint: 'Actual mounts on this Host. Selected other presets mount when a session is active. This is registration status; API access is checked during tool calls.',
mountDetails: 'Mount details',
noOtherPresets: 'No other presets available.',
mountStatusHint: 'Registered tools in active sessions; API access is verified when a tool runs.',
mountDetails: 'Diagnostics',
title: 'Netx Ops',
description: 'API, token, capabilities, knowledge base, and alarm delivery.',
sectionConnection: 'Connection',
sectionCapabilities: 'Capability groups',
sectionLanguage: 'Model language',
languageScope: 'Applies to all presets',
languageAuto: 'Automatic',
languageFollowUser: 'Follow user',
languageChinese: 'Chinese',
languageEnglish: 'English',
sectionCapabilities: 'Tools and skills',
sectionKnowledge: 'Knowledge base',
sectionAlarms: 'Key-alarm delivery',
sectionExport: 'Session export',
apiUrl: 'API URL',
apiUrlHint: 'e.g. http://127.0.0.1:8890',
apiUrlInvalid: 'Must be http(s) without username/password (no file:/javascript:)',
apiUrlHint: 'http://127.0.0.1:8890',
apiUrlInvalid: 'Enter an HTTP or HTTPS URL without credentials.',
lang: 'API / alarm language',
langHint: 'zh / en — netx API query + alarm prompt copy only (not model reply)',
thinkingLanguage: 'Thinking language',
thinkingLanguageHint: 'auto / zh-CN / en — model chain-of-thought for all presets',
replyLanguage: 'Reply language',
replyLanguageHint: 'follow-user / zh / en — final answers for all presets',
capabilityGroups: 'Capability groups',
nmsProvider: 'NMS provider',
nmsProviderHint: 'zte-ume',
groupOps: 'Alarms, inventory and CLI',
groupTopology: 'Topology and layout',
groupBizMonitor: 'Cutover monitoring',
groupKb: 'Knowledge skills',
groupInPreset: 'In Netx Ops preset (auto-saves; mount/unmount immediately)',
groupPublic: 'Publish to every preset (auto-saves)',
groupInPreset: 'Netx Ops',
groupPublic: 'All presets',
kbRoot: 'Package root',
kbRootHint: 'Folder with MANIFEST.json (operator-subset v1.0). Browse auto-saves. Pack skills load from paths.skills/_skills and paths.localSkills when hasSkills.',
kbRootHint: 'Absolute path to a folder containing MANIFEST.json',
kbBrowse: 'Browse…',
kbBrowseUnavailable: 'Directory picker unavailable — paste an absolute folder path, then Save.',
kbBrowseUnavailable: 'Enter the folder path manually.',
kbBrowseFailed: 'Browse failed: {detail}',
kbSaveHint: 'Browse writes and saves automatically, then reloads Host KB status. Manual paste still needs Save.',
kbStatusConfigured: 'Knowledge base: {operator} ({country}) v{version}',
kbStatusUnconfigured: 'Knowledge base: not configured (pure netx)',
kbStatusError: 'Knowledge base: error — {detail}',
alarmPushEnabled: 'Key-alarm push',
alarmPushStatus: 'Status',
kbStatusConfigured: '{operator} ({country}) · v{version}',
kbStatusUnconfigured: 'Not configured',
kbStatusError: 'Load failed',
alarmPushEnabled: 'Enable push',
alarmPushPhaseDisabled: 'Off',
alarmPushPhaseIdle: 'Idle',
alarmPushPhaseConnecting: 'Connecting…',
@ -153,33 +141,30 @@ export const en: Record<NetxopsLocaleKey, string> = {
alarmPushPhaseError: 'Error',
alarmDeliverDsh: 'Deliver to DSH session',
alarmDeliverIm: 'Deliver to WhatsApp / IM',
imTargetNone: 'No delivery targets yet — create one in IM delivery settings.',
imTargetNone: 'No targets. Add one in IM settings.',
imTargetSelectedCount: '{count} selected',
imCatalogUnavailable: 'Cannot load IM targets (install/update dsh-im-ops).',
imCatalogUnavailable: 'IM delivery unavailable',
apiToken: 'API token',
apiTokenHint: 'Stored as NETX_API_TOKEN. Leave blank to keep current.',
apiTokenUnavailable: 'remote.credentials unavailable — set NETX_API_TOKEN via script.',
apiTokenHint: 'Leave blank to keep the current token',
apiTokenUnavailable: 'Credential service unavailable',
apiTokenSet: 'Configured',
apiTokenUnset: 'Not set',
toolsStatus: 'Agent tools',
toolsStatusMounted: '{count} netx__* tools (v{version})',
toolsStatusEmpty: 'No active Netx tool mounts',
toolsStatusError: 'Tool mount failed: {detail}',
toolsStatusUnknown: 'Host status unavailable',
sessionsExport: 'Export all sessions',
toolsStatusMounted: '{count} active tools',
toolsStatusEmpty: 'No active tools',
toolsStatusError: 'Publication failed',
toolsStatusUnknown: 'Status unavailable',
sessionsExportCount: '{count} sessions',
sessionsExportUnavailable: 'Export unavailable',
sessionsExportNoPersistence: 'Session store unavailable (JSONL backend required).',
sessionsExportNoRawArtifacts: 'This persistence backend cannot export raw session files.',
sessionsExportNoPersistence: 'Session store unavailable.',
sessionsExportNoRawArtifacts: 'This session store does not support export.',
sessionsExportListFailed: 'Failed to list sessions: {detail}',
sessionsExportRpcUnavailable: 'Cannot reach Host RPC for export status.',
sessionsExportRpcFailed: 'Export status RPC failed.',
sessionsExportRpcUnavailable: 'Export service unavailable.',
sessionsExportRpcFailed: 'Cannot load export status.',
sessionsExportHttpFailed: 'Export failed: HTTP {status}{detail}',
sessionsExportEmpty: 'Export failed: empty ZIP body.',
sessionsExportButton: 'Download ZIP',
sessionsExportBusy: 'Preparing…',
sessionsExportDone: 'Downloaded: {file}',
overridden: 'Overridden',
reset: 'Reset',
invalid: 'Invalid',
unsaved: 'Unsaved changes',
@ -188,57 +173,52 @@ export const en: Record<NetxopsLocaleKey, string> = {
discard: 'Discard',
save: 'Save',
saving: 'Saving…',
configSaved: 'Saved',
}
export const zh: Record<NetxopsLocaleKey, string> = {
settingsUnavailable: '正在等待 Host 设置,连接就绪前不能保存。若持续等待,请检查 Host 插件是否启用。',
toolsEnabled: '启用 Netx 能力发布',
toolsEnabledHint: '自动保存。关闭会撤销全部 Netx 工具和 skill,保留各组选择,便于随时恢复。',
toolsDisabled: '全部 Netx 工具和 skill 已关闭',
selectedPresets: '指定其他预设',
publicationHint: '工具与对应 skill 一起发布。选择自动保存,对当前及新会话的下一次模型请求生效。公开能力由所有预设继承,也包括 Netx Ops。',
presetCatalogUnavailable: '暂时无法读取预设列表,已有选择会保留,也可以取消。',
settingsUnavailable: '正在连接 Host 设置…',
toolsEnabled: '启用工具与技能',
toolsDisabled: '已关闭',
selectedPresets: '其他预设',
presetCatalogUnavailable: '无法加载预设列表,已有选择已保留。',
presetUnavailable: '预设不可用',
noOtherPresets: '此 Host 暂无其他预设。',
mountStatusHint: '显示此 Host 的实际挂载。指定其他预设在会话活跃时挂载;此状态表示注册结果,API 权限在调用工具时验证。',
mountDetails: '查看实际挂载明细',
noOtherPresets: '暂无其他预设。',
mountStatusHint: '当前活跃会话中的工具注册情况;API 权限在调用时验证。',
mountDetails: '诊断信息',
title: 'Netx Ops',
description: 'API、Token、能力组、知识库与告警投递。',
sectionConnection: '连接',
sectionCapabilities: '能力组',
sectionLanguage: '模型语言',
languageScope: '适用于所有预设',
languageAuto: '自动',
languageFollowUser: '跟随用户',
languageChinese: '中文',
languageEnglish: 'English',
sectionCapabilities: '工具与技能',
sectionKnowledge: '知识库',
sectionAlarms: '关键告警投递',
sectionExport: '会话导出',
apiUrl: 'API 地址',
apiUrlHint: '例如 http://127.0.0.1:8890',
apiUrlInvalid: '须为无账号密码的 http(s) 地址(不支持 file:/javascript:)',
apiUrlHint: 'http://127.0.0.1:8890',
apiUrlInvalid: '请输入不含账号密码的 HTTP 或 HTTPS 地址。',
lang: 'API / 告警语言',
langHint: 'zh / en — 仅影响 netx API 与告警文案,不控制模型回复',
thinkingLanguage: '思考语言',
thinkingLanguageHint: 'auto / zh-CN / en — 全预设模型思考过程语言',
replyLanguage: '回复语言',
replyLanguageHint: 'follow-user / zh / en — 全预设最终回答语言',
capabilityGroups: '能力组',
nmsProvider: 'NMS 提供方',
nmsProviderHint: 'zte-ume',
groupOps: '告警、资产与 CLI 运维',
groupTopology: '拓扑与布局',
groupBizMonitor: '割接业务监控',
groupKb: '知识库技能',
groupInPreset: '在 Netx Ops 预设中启用(自动保存,即时挂载/卸载)',
groupPublic: '对所有预设公开(自动保存)',
groupInPreset: 'Netx Ops',
groupPublic: '所有预设',
kbRoot: '知识包根目录',
kbRootHint: '含 MANIFEST.json 的运营商子集包(v1.0)。浏览选目录后会自动保存;hasSkills 时加载 paths.skills/_skills 与 paths.localSkills。',
kbRootHint: '含 MANIFEST.json 的知识包绝对路径',
kbBrowse: '浏览…',
kbBrowseUnavailable: '目录选择器不可用 — 请粘贴绝对目录路径后点保存。',
kbBrowseUnavailable: '请手动输入目录路径。',
kbBrowseFailed: '浏览失败:{detail}',
kbSaveHint: '点「浏览」选目录后会自动写入、保存,并刷新宿主知识库状态;若是手粘路径仍需点保存。',
kbStatusConfigured: '知识库: {operator}({country}) v{version}',
kbStatusUnconfigured: '知识库: 未配置(纯 netx)',
kbStatusError: '知识库: 错误 — {detail}',
alarmPushEnabled: '关键告警推送',
alarmPushStatus: '状态',
kbStatusConfigured: '{operator}({country})· v{version}',
kbStatusUnconfigured: '未配置',
kbStatusError: '加载失败',
alarmPushEnabled: '启用推送',
alarmPushPhaseDisabled: '未开启',
alarmPushPhaseIdle: '空闲',
alarmPushPhaseConnecting: '连接中…',
@ -249,33 +229,30 @@ export const zh: Record<NetxopsLocaleKey, string> = {
alarmPushPhaseError: '异常',
alarmDeliverDsh: '投递到 DSH 会话',
alarmDeliverIm: '投递到 WhatsApp / IM',
imTargetNone: '暂无投递目标 — 请先在 IM「投递设置」新建。',
imTargetNone: '暂无目标,请在 IM 设置中添加。',
imTargetSelectedCount: '已选 {count} 个',
imCatalogUnavailable: '无法加载投递目标(请安装/更新 dsh-im-ops)。',
imCatalogUnavailable: 'IM 投递不可用',
apiToken: 'API Token',
apiTokenHint: '写入凭据 NETX_API_TOKEN;留空表示保留已有。',
apiTokenUnavailable: '未提供 remote.credentials — 请用脚本写入 NETX_API_TOKEN。',
apiTokenHint: '留空保留当前凭据',
apiTokenUnavailable: '凭据服务不可用',
apiTokenSet: '已配置',
apiTokenUnset: '未设置',
toolsStatus: 'Agent 工具',
toolsStatusMounted: '已挂载 {count} 个 netx__*(v{version})',
toolsStatusEmpty: '当前没有活跃的 Netx 工具挂载',
toolsStatusError: '工具挂载失败:{detail}',
toolsStatusUnknown: '无法读取 Host 状态',
sessionsExport: '导出全部会话',
toolsStatusMounted: '{count} 个工具',
toolsStatusEmpty: '暂无活跃工具',
toolsStatusError: '发布异常',
toolsStatusUnknown: '状态不可用',
sessionsExportCount: '{count} 个会话',
sessionsExportUnavailable: '无法导出',
sessionsExportNoPersistence: '会话持久化不可用(需要 JSONL 后端)。',
sessionsExportNoRawArtifacts: '当前持久化后端不支持导出原始会话文件。',
sessionsExportNoPersistence: '会话存储不可用。',
sessionsExportNoRawArtifacts: '当前会话存储不支持导出。',
sessionsExportListFailed: '列出会话失败:{detail}',
sessionsExportRpcUnavailable: '无法通过 Host RPC 查询导出状态。',
sessionsExportRpcFailed: '导出状态 RPC 失败。',
sessionsExportRpcUnavailable: '导出服务不可用。',
sessionsExportRpcFailed: '无法获取导出状态。',
sessionsExportHttpFailed: '导出失败:HTTP {status}{detail}',
sessionsExportEmpty: '导出失败:ZIP 为空。',
sessionsExportButton: '下载 ZIP',
sessionsExportBusy: '准备中…',
sessionsExportDone: '已下载:{file}',
overridden: '已覆盖',
reset: '重置',
invalid: '无效',
unsaved: '有未保存更改',
@ -284,5 +261,4 @@ export const zh: Record<NetxopsLocaleKey, string> = {
discard: '丢弃',
save: '保存',
saving: '保存中…',
configSaved: '已保存',
}

View file

@ -1,19 +1,23 @@
/** Inject shared Netx Ops settings-section styles once (uds-auth visual language). */
/** Shared settings styles, using the Host's theme tokens. */
const STYLE_ID = 'dsh-netxops-settings-css'
const CSS = `
.dsh-nx-settings{box-sizing:border-box;display:flex;flex-direction:column;gap:20px;min-height:0;padding:4px 4px 24px;color:var(--dsw-alias-label-primary,#1f2329)}
.dsh-nx-settings{box-sizing:border-box;display:flex;flex-direction:column;gap:16px;min-height:0;padding:4px 4px 24px;color:var(--dsw-alias-label-primary,#1f2329)}
.dsh-nx-settings>header{display:flex;flex-direction:column;gap:4px}
.dsh-nx-settings h2{margin:0;font-size:20px;font-weight:600;line-height:28px}
.dsh-nx-settings-intro{margin:0;font-size:13px;line-height:1.5;color:var(--dsw-alias-label-secondary,#646a73)}
.dsh-nx-settings-card{border:1px solid var(--dsw-alias-border-l2,#dee0e3);border-radius:12px;padding:16px;background:var(--dsw-alias-bg-layer-1,transparent)}
.dsh-nx-settings-card h3{margin:0 0 12px;font-size:15px;font-weight:600;line-height:22px}
.dsh-nx-card-head{display:flex;align-items:center;justify-content:space-between;flex-wrap:wrap;gap:8px;margin-bottom:14px}
.dsh-nx-card-head h3{margin:0}
.dsh-nx-settings-field{display:flex;flex-direction:column;gap:4px;margin-bottom:12px}
.dsh-nx-settings-field:last-child{margin-bottom:0}
.dsh-nx-settings-field>label,.dsh-nx-field-label{font-size:12px;line-height:18px;color:var(--dsw-alias-label-secondary,#646a73)}
.dsh-nx-settings-field>label,.dsh-nx-field-label,.dsh-nx-field-head>label{font-size:13px;line-height:20px;color:var(--dsw-alias-label-secondary,#646a73)}
.dsh-nx-field-head{display:flex;align-items:center;justify-content:space-between;gap:8px}
.dsh-nx-field-head>label,.dsh-nx-field-head>.dsh-nx-field-label{flex:1;min-width:0}
.dsh-nx-fieldGrid{display:grid;grid-template-columns:repeat(auto-fit,minmax(min(230px,100%),1fr));gap:12px 20px}
.dsh-nx-fieldGrid>.dsh-nx-settings-field{min-width:0;margin:0}
.dsh-nx-toggleField>.dsh-nx-field-head{min-height:28px}
.dsh-nx-settings-field input[type=text],
.dsh-nx-settings-field input[type=password],
.dsh-nx-settings-field input:not([type]),
@ -25,12 +29,12 @@ const CSS = `
.dsh-nx-settings-field input:disabled,
.dsh-nx-settings-field select:disabled{opacity:.55;cursor:default}
.dsh-nx-inputInvalid{border-color:var(--dsw-alias-state-error-primary,#d54941)!important}
.dsh-nx-hint{margin:0;font-size:12px;line-height:1.5;color:var(--dsw-alias-label-tertiary,#8f959e)}
.dsh-nx-invalid{margin:0;font-size:12px;line-height:1.5;color:var(--dsw-alias-state-error-primary,#d54941)}
.dsh-nx-hint{margin:0;font-size:12px;line-height:1.5;overflow-wrap:anywhere;color:var(--dsw-alias-label-tertiary,#8f959e)}
.dsh-nx-invalid{margin:0;font-size:12px;line-height:1.5;overflow-wrap:anywhere;color:var(--dsw-alias-state-error-primary,#d54941)}
.dsh-nx-badges{display:inline-flex;align-items:center;gap:8px;flex:none}
.dsh-nx-badge{border-radius:999px;padding:1px 8px;font-size:11px;line-height:17px;white-space:nowrap;font-weight:500;background:var(--dsw-alias-bg-module-platform,#f4f5f7);color:var(--dsw-alias-label-secondary,#646a73)}
.dsh-nx-badgeMuted{border-radius:999px;padding:1px 8px;font-size:11px;line-height:17px;white-space:nowrap;color:var(--dsw-alias-label-tertiary,#8f959e)}
.dsh-nx-status{border-radius:999px;padding:1px 8px;font-size:11px;line-height:17px;font-weight:500;white-space:nowrap;background:var(--dsw-alias-bg-module-platform,#f4f5f7);color:var(--dsw-alias-label-tertiary,#8f959e)}
.dsh-nx-status{border-radius:999px;padding:1px 8px;font-size:11px;line-height:17px;font-weight:500;max-width:100%;overflow-wrap:anywhere;background:var(--dsw-alias-bg-module-platform,#f4f5f7);color:var(--dsw-alias-label-tertiary,#8f959e)}
.dsh-nx-statusOk{color:var(--dsw-alias-state-success-primary,#20a162);background:rgba(32,161,98,.12)}
.dsh-nx-statusWarn{color:var(--dsw-alias-state-warn-primary,#d97706);background:rgba(217,119,6,.12)}
.dsh-nx-statusErr{color:var(--dsw-alias-state-error-primary,#d54941);background:rgba(213,73,65,.12)}
@ -38,22 +42,26 @@ const CSS = `
.dsh-nx-reset:hover:not(:disabled){opacity:.85}
.dsh-nx-reset:disabled{opacity:.4;cursor:default}
.dsh-nx-checkRow{display:flex;align-items:flex-start;gap:10px;margin:0;font-size:13px;line-height:1.5;color:var(--dsw-alias-label-primary,#1f2329);cursor:pointer}
.dsh-nx-checkRow input{margin-top:3px;flex:none}
.dsh-nx-groupBlock{display:flex;flex-direction:column;gap:8px;padding:4px 0 8px}
.dsh-nx-groupBlock+.dsh-nx-groupBlock{border-top:1px solid var(--dsw-alias-border-l1,#eef0f3);padding-top:12px}
.dsh-nx-groupTitle{font-size:13px;font-weight:600;line-height:1.5;color:var(--dsw-alias-label-primary,#1f2329)}
.dsh-nx-groupChecks{display:flex;flex-direction:column;gap:8px;padding-left:2px}
.dsh-nx-targets{margin-top:10px;padding-top:10px;border-top:1px solid var(--dsw-alias-border-l2,#dfe1e5)}
.dsh-nx-checkRow input{margin:3px 0 0;flex:none}
.dsh-nx-settings input[type=checkbox]{accent-color:var(--dsw-alias-state-business-primary,#3370ff)}
.dsh-nx-groupBlock{display:flex;flex-direction:column;gap:10px;padding:12px 0}
.dsh-nx-groupBlock+.dsh-nx-groupBlock{border-top:1px solid var(--dsw-alias-border-l1,#eef0f3)}
.dsh-nx-groupTitle{display:flex;align-items:center;gap:8px;font-size:13px;font-weight:600;line-height:1.5;color:var(--dsw-alias-label-primary,#1f2329)}
.dsh-nx-groupChecks{display:flex;flex-wrap:wrap;gap:8px 24px}
.dsh-nx-targets{margin-top:0}
.dsh-nx-diagnostics{padding-top:12px;border-top:1px solid var(--dsw-alias-border-l1,#eef0f3)}
.dsh-nx-diagnostics[open] summary{margin-bottom:8px}
.dsh-nx-diagnostics p+p{margin-top:4px}
.dsh-nx-settings summary{cursor:pointer;font-size:12px;font-weight:500;line-height:1.6}
.dsh-nx-targets[open] .dsh-nx-checkRow{padding:5px 0;flex-wrap:wrap}
.dsh-nx-settings input:focus-visible,.dsh-nx-settings button:focus-visible,.dsh-nx-settings summary:focus-visible{outline:2px solid var(--dsw-alias-state-business-primary,#3370ff);outline-offset:2px}
.dsh-nx-settings>.dsh-nx-settings-actions{position:sticky;bottom:0;padding:12px;background:var(--dsw-alias-bg-base,#fff);border-top:1px solid var(--dsw-alias-border-l2,#dfe1e5);flex-wrap:wrap}
.dsh-nx-settings-actions{display:flex;align-items:center;gap:8px}
.dsh-nx-imTargetList{display:flex;flex-direction:column;gap:8px;padding:4px 0 2px}
.dsh-nx-pathRow{display:flex;gap:8px;align-items:stretch}
.dsh-nx-pathRow>input{flex:1;min-width:0}
.dsh-nx-pathRow>.dsh-nx-btn{flex:none;align-self:stretch}
.dsh-nx-settings-msg{font-size:12px;color:var(--dsw-alias-label-secondary,#646a73)}
.dsh-nx-settings-msg.ok{color:var(--dsw-alias-state-success-primary,#20a162)}
.dsh-nx-settings-msg.err{color:var(--dsw-alias-state-error-primary,#d54941)}
.dsh-nx-settings-empty{padding:24px;text-align:center;color:var(--dsw-alias-label-tertiary,#8f959e);font-size:13px}
.dsh-nx-btn{appearance:none;display:inline-flex;align-items:center;justify-content:center;border:1px solid var(--dsw-alias-border-l2,#dfe1e5);border-radius:4px;padding:8px 12px;font:inherit;font-size:13px;line-height:20px;cursor:pointer;background:var(--dsw-alias-bg-module-platform,#f4f5f7);color:var(--dsw-alias-label-primary,#1f2329)}
@ -66,7 +74,11 @@ const CSS = `
export function ensureStyles(): void {
if (typeof document === 'undefined') return
if (document.getElementById(STYLE_ID)) return
const existing = document.getElementById(STYLE_ID)
if (existing) {
if (existing.textContent !== CSS) existing.textContent = CSS
return
}
const tag = document.createElement('style')
tag.id = STYLE_ID
tag.textContent = CSS