mirror of
https://github.com/hansjone/netxops.git
synced 2026-10-12 21:20:52 +08:00
Simplify settings copy and polish responsive form layout
This commit is contained in:
parent
0b9a3a82bc
commit
9271494b37
8 changed files with 590 additions and 690 deletions
|
|
@ -78,6 +78,10 @@
|
|||
|
||||
## 验证与维护
|
||||
|
||||
设置页已精简重复的“自动保存”“即时挂载/卸载”和“已覆盖”提示。发布范围使用简短标签,挂载解释与明细集中在折叠的诊断信息中;模型语言独立分组,使用本地化选项。连接字段在宽面板并排、窄面板单列,保存/丢弃栏仅在待处理修改、保存中或保存失败时出现。实际错误仍显示在对应区域。
|
||||
|
||||
本次界面验证覆盖运行中的桌面深色设置页、390px 窄屏布局、中英文文案、预设列表展开与选择,以及手工草稿保存后收起操作栏。预览凭据服务为内存模拟,不读取或写入真实 token。预览截图已更新为精简后的界面。
|
||||
|
||||
验证命令:
|
||||
|
||||
```powershell
|
||||
|
|
|
|||
Binary file not shown.
|
Before Width: | Height: | Size: 39 KiB After Width: | Height: | Size: 18 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 121 KiB After Width: | Height: | Size: 73 KiB |
|
|
@ -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: [
|
||||
|
|
|
|||
746
lib/client.js
746
lib/client.js
File diff suppressed because it is too large
Load diff
|
|
@ -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>
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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: '已保存',
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue