mirror of
https://github.com/hansjone/netxops.git
synced 2026-10-18 12:33:23 +08:00
Adapt settings client for DSH 0.2.0 configForms without breaking boot.
Soft-inject configForms/settingsScope and mark Config fields volatile so desktop 0.2.0 no longer waits forever on the removed settingsScope service. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
parent
30d4df23fa
commit
9b8e873c18
8 changed files with 358 additions and 166 deletions
|
|
@ -2629,9 +2629,7 @@ var zh = {
|
||||||
var LOCALE_NS = "settings.netxops";
|
var LOCALE_NS = "settings.netxops";
|
||||||
var inject = [
|
var inject = [
|
||||||
"slots",
|
"slots",
|
||||||
"locale",
|
"locale"
|
||||||
"remote",
|
|
||||||
"settingsScope"
|
|
||||||
];
|
];
|
||||||
function apply(ctx) {
|
function apply(ctx) {
|
||||||
ctx.effect(() => {
|
ctx.effect(() => {
|
||||||
|
|
@ -2646,8 +2644,13 @@ function apply(ctx) {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}, "netxops: locales");
|
}, "netxops: locales");
|
||||||
const card = new NetxopsCardController(ctx.settingsScope.bind({ namespace: NETXOPS_NS }), ctx);
|
|
||||||
const t = ctx.locale.bind(LOCALE_NS);
|
const t = ctx.locale.bind(LOCALE_NS);
|
||||||
|
let mounted = false;
|
||||||
|
const mountCard = (scope) => {
|
||||||
|
if (mounted)
|
||||||
|
return;
|
||||||
|
mounted = true;
|
||||||
|
const card = new NetxopsCardController(scope, ctx);
|
||||||
ctx.inject(["remote.credentials"], (credCtx) => {
|
ctx.inject(["remote.credentials"], (credCtx) => {
|
||||||
card.setCredentialsAvailable(true);
|
card.setCredentialsAvailable(true);
|
||||||
credCtx.effect(() => {
|
credCtx.effect(() => {
|
||||||
|
|
@ -2693,6 +2696,27 @@ function apply(ctx) {
|
||||||
locale: LOCALE_NS,
|
locale: LOCALE_NS,
|
||||||
inject: () => card.inject()
|
inject: () => card.inject()
|
||||||
}, NetxopsCard));
|
}, NetxopsCard));
|
||||||
|
};
|
||||||
|
ctx.inject(["configForms"], (formsCtx) => {
|
||||||
|
const forms = formsCtx.configForms;
|
||||||
|
if (!forms || typeof forms.get !== "function") {
|
||||||
|
formsCtx.logger?.warn?.("netxops: configForms present but .get missing");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
formsCtx.logger?.info?.("netxops: settings card via configForms");
|
||||||
|
mountCard(forms.get(NETXOPS_NS));
|
||||||
|
});
|
||||||
|
ctx.inject(["settingsScope"], (scopeCtx) => {
|
||||||
|
const binder = scopeCtx.settingsScope;
|
||||||
|
if (!binder || typeof binder.bind !== "function") {
|
||||||
|
scopeCtx.logger?.warn?.("netxops: settingsScope present but .bind missing");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (mounted)
|
||||||
|
return;
|
||||||
|
scopeCtx.logger?.info?.("netxops: settings card via settingsScope");
|
||||||
|
mountCard(binder.bind({ namespace: NETXOPS_NS }));
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
return module.exports;
|
return module.exports;
|
||||||
|
|
|
||||||
91
lib/index.js
91
lib/index.js
|
|
@ -5236,30 +5236,34 @@ var NETXOPS_RPC_CHANNEL = "/netxops";
|
||||||
var NETXOPS_SETTINGS_NAMESPACE = "netxops";
|
var NETXOPS_SETTINGS_NAMESPACE = "netxops";
|
||||||
var NETXOPS_PRESET_ID = "netxops";
|
var NETXOPS_PRESET_ID = "netxops";
|
||||||
var DEFAULT_TOKEN_REF = "NETX_API_TOKEN";
|
var DEFAULT_TOKEN_REF = "NETX_API_TOKEN";
|
||||||
|
function vol(schema) {
|
||||||
|
const candidate = schema;
|
||||||
|
return typeof candidate.volatile === "function" ? candidate.volatile() : schema;
|
||||||
|
}
|
||||||
var Config = z.object({
|
var Config = z.object({
|
||||||
apiUrl: z.string().default("http://127.0.0.1:8890"),
|
apiUrl: vol(z.string().default("http://127.0.0.1:8890")),
|
||||||
lang: z.string().default("zh"),
|
lang: vol(z.string().default("zh")),
|
||||||
thinkingLanguage: z.string().default("auto"),
|
thinkingLanguage: vol(z.string().default("auto")),
|
||||||
replyLanguage: z.string().default("follow-user"),
|
replyLanguage: vol(z.string().default("follow-user")),
|
||||||
tokenCredentialRef: z.string().role("credential-ref").default(DEFAULT_TOKEN_REF),
|
tokenCredentialRef: vol(z.string().role("credential-ref").default(DEFAULT_TOKEN_REF)),
|
||||||
toolCallTimeoutMs: z.number().step(1).min(1000).default(120000),
|
toolCallTimeoutMs: vol(z.number().step(1).min(1000).default(120000)),
|
||||||
installAgentPreset: z.boolean().default(true),
|
installAgentPreset: vol(z.boolean().default(true)),
|
||||||
alarmPushEnabled: z.boolean().default(false),
|
alarmPushEnabled: vol(z.boolean().default(false)),
|
||||||
alarmDeliverDsh: z.boolean().default(true),
|
alarmDeliverDsh: vol(z.boolean().default(true)),
|
||||||
alarmDeliverIm: z.boolean().default(false),
|
alarmDeliverIm: vol(z.boolean().default(false)),
|
||||||
imTargets: z.string().default(""),
|
imTargets: vol(z.string().default("")),
|
||||||
imBotId: z.string().default(""),
|
imBotId: vol(z.string().default("")),
|
||||||
imTargetId: z.string().default(""),
|
imTargetId: vol(z.string().default("")),
|
||||||
nmsProvider: z.string().default("zte-ume"),
|
nmsProvider: vol(z.string().default("zte-ume")),
|
||||||
groupOpsInPreset: z.boolean().default(true),
|
groupOpsInPreset: vol(z.boolean().default(true)),
|
||||||
groupOpsPublic: z.boolean().default(false),
|
groupOpsPublic: vol(z.boolean().default(false)),
|
||||||
groupTopologyInPreset: z.boolean().default(false),
|
groupTopologyInPreset: vol(z.boolean().default(false)),
|
||||||
groupTopologyPublic: z.boolean().default(false),
|
groupTopologyPublic: vol(z.boolean().default(false)),
|
||||||
groupBizMonitorInPreset: z.boolean().default(true),
|
groupBizMonitorInPreset: vol(z.boolean().default(true)),
|
||||||
groupBizMonitorPublic: z.boolean().default(false),
|
groupBizMonitorPublic: vol(z.boolean().default(false)),
|
||||||
kbRoot: z.string().default(""),
|
kbRoot: vol(z.string().default("")),
|
||||||
groupKbInPreset: z.boolean().default(true),
|
groupKbInPreset: vol(z.boolean().default(true)),
|
||||||
groupKbPublic: z.boolean().default(false)
|
groupKbPublic: vol(z.boolean().default(false))
|
||||||
});
|
});
|
||||||
function packageRoot() {
|
function packageRoot() {
|
||||||
return join7(dirname4(fileURLToPath2(import.meta.url)), "..");
|
return join7(dirname4(fileURLToPath2(import.meta.url)), "..");
|
||||||
|
|
@ -5296,6 +5300,19 @@ async function resolveToken(ctx, refName) {
|
||||||
const hit = await ctx.credentials.resolve(credentialRef(refName));
|
const hit = await ctx.credentials.resolve(credentialRef(refName));
|
||||||
return hit?.value ?? "";
|
return hit?.value ?? "";
|
||||||
}
|
}
|
||||||
|
function describeRows(describe) {
|
||||||
|
if (typeof describe !== "function")
|
||||||
|
return [];
|
||||||
|
try {
|
||||||
|
const raw = describe();
|
||||||
|
if (Array.isArray(raw))
|
||||||
|
return raw;
|
||||||
|
if (raw && typeof raw === "object" && Array.isArray(raw.namespaces)) {
|
||||||
|
return raw.namespaces;
|
||||||
|
}
|
||||||
|
} catch {}
|
||||||
|
return [];
|
||||||
|
}
|
||||||
function installNetxopsSettings(ctx, entry, hooks) {
|
function installNetxopsSettings(ctx, entry, hooks) {
|
||||||
const legacy = DshSettings.installSettingsSection;
|
const legacy = DshSettings.installSettingsSection;
|
||||||
if (typeof legacy === "function") {
|
if (typeof legacy === "function") {
|
||||||
|
|
@ -5303,7 +5320,33 @@ function installNetxopsSettings(ctx, entry, hooks) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
ctx.inject(["settings"], (settingsCtx) => {
|
ctx.inject(["settings"], (settingsCtx) => {
|
||||||
settingsCtx.settings.installSection(ctx, NETXOPS_SETTINGS_NAMESPACE, Config, entry, hooks);
|
const settings = settingsCtx.settings;
|
||||||
|
if (typeof settings.installSection === "function") {
|
||||||
|
settings.installSection(ctx, NETXOPS_SETTINGS_NAMESPACE, Config, entry, hooks);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const readLive = () => {
|
||||||
|
const row = describeRows(settings.describe).find((item) => item.ns === NETXOPS_SETTINGS_NAMESPACE);
|
||||||
|
if (row?.value !== null && typeof row?.value === "object" && !Array.isArray(row.value)) {
|
||||||
|
return { ...entry, ...row.value };
|
||||||
|
}
|
||||||
|
return entry;
|
||||||
|
};
|
||||||
|
hooks.setSource(readLive);
|
||||||
|
hooks.onChange();
|
||||||
|
let lastFingerprint = JSON.stringify(readLive());
|
||||||
|
const timer = setInterval(() => {
|
||||||
|
const next = readLive();
|
||||||
|
const fingerprint = JSON.stringify(next);
|
||||||
|
if (fingerprint === lastFingerprint)
|
||||||
|
return;
|
||||||
|
lastFingerprint = fingerprint;
|
||||||
|
hooks.onChange();
|
||||||
|
}, 2000);
|
||||||
|
settingsCtx.effect(() => () => {
|
||||||
|
clearInterval(timer);
|
||||||
|
}, "netxops: settings describe poll");
|
||||||
|
settingsCtx.logger.info("netxops: following settings via describe() (no installSection — DSH ≥0.1.7 path)");
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
function apply(ctx, config = Config({})) {
|
function apply(ctx, config = Config({})) {
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"name": "dsh-netxops",
|
"name": "dsh-netxops",
|
||||||
"version": "0.1.41",
|
"version": "0.1.42",
|
||||||
"description": "DeepSeek Harness Netx Ops: ops/topology + IM delivery + session export + knowledge-base MANIFEST/_skills + localSkills",
|
"description": "DeepSeek Harness Netx Ops: ops/topology + IM delivery + session export + knowledge-base MANIFEST/_skills + localSkills",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
|
|
@ -63,10 +63,10 @@
|
||||||
},
|
},
|
||||||
"client": {
|
"client": {
|
||||||
"platform": "web",
|
"platform": "web",
|
||||||
|
"immediately": true,
|
||||||
"inject": [
|
"inject": [
|
||||||
"@deepseek-ai/dsh-client-locale",
|
"@deepseek-ai/dsh-client-ui-slots",
|
||||||
"@deepseek-ai/dsh-client-ui-settings",
|
"@deepseek-ai/dsh-client-locale"
|
||||||
"@deepseek-ai/dsh-api-remotes"
|
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,7 @@
|
||||||
* Self-contained (cannot value-import ui-settings-plugins — client bundle purity).
|
* Self-contained (cannot value-import ui-settings-plugins — client bundle purity).
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import type { SettingsScope, SettingsScopeSnapshot } from '@deepseek-ai/dsh-client-ui-settings/client'
|
import type { SettingsFormScope, SettingsFormSnapshot } from './settings-form.ts'
|
||||||
import { createSnapshotStore, type SnapshotStore } from './snapshot-store.ts'
|
import { createSnapshotStore, type SnapshotStore } from './snapshot-store.ts'
|
||||||
|
|
||||||
export type FieldWrite = { kind: 'set'; value: unknown } | { kind: 'clear' }
|
export type FieldWrite = { kind: 'set'; value: unknown } | { kind: 'clear' }
|
||||||
|
|
@ -110,7 +110,7 @@ export class CardForm<T> {
|
||||||
private failed = false
|
private failed = false
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
private readonly scope: SettingsScope<T>,
|
private readonly scope: SettingsFormScope<T>,
|
||||||
specs: CardFieldSpec[],
|
specs: CardFieldSpec[],
|
||||||
secrets: CardSecretSpec[] = [],
|
secrets: CardSecretSpec[] = [],
|
||||||
) {
|
) {
|
||||||
|
|
@ -235,7 +235,7 @@ export class CardForm<T> {
|
||||||
return spec
|
return spec
|
||||||
}
|
}
|
||||||
|
|
||||||
private snapshotOf(): SettingsScopeSnapshot<T> {
|
private snapshotOf(): SettingsFormSnapshot<T> {
|
||||||
return this.scope.getSnapshot()
|
return this.scope.getSnapshot()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,7 @@
|
||||||
|
|
||||||
import type { Context as ClientContext } from '@deepseek-ai/cordis'
|
import type { Context as ClientContext } from '@deepseek-ai/cordis'
|
||||||
import type {} from '@deepseek-ai/dsh-api-remotes/client'
|
import type {} from '@deepseek-ai/dsh-api-remotes/client'
|
||||||
import type { SettingsScope, SettingsScopeSnapshot } from '@deepseek-ai/dsh-client-ui-settings/client'
|
import type { SettingsFormScope, SettingsFormSnapshot } from './settings-form.ts'
|
||||||
import type { SnapshotStore } from './snapshot-store.ts'
|
import type { SnapshotStore } from './snapshot-store.ts'
|
||||||
import {
|
import {
|
||||||
CardForm, textField, booleanField, booleanFieldPersistFalse,
|
CardForm, textField, booleanField, booleanFieldPersistFalse,
|
||||||
|
|
@ -175,7 +175,7 @@ export class NetxopsCardController {
|
||||||
private exportInFlight: Promise<void> | undefined
|
private exportInFlight: Promise<void> | undefined
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
private readonly scope: SettingsScope<NetxopsSettings>,
|
private readonly scope: SettingsFormScope<NetxopsSettings>,
|
||||||
private readonly ctx: ClientContext,
|
private readonly ctx: ClientContext,
|
||||||
) {
|
) {
|
||||||
this.form = new CardForm(
|
this.form = new CardForm(
|
||||||
|
|
@ -708,7 +708,7 @@ export class NetxopsCardController {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function refOf(snapshot: SettingsScopeSnapshot<NetxopsSettings>): string {
|
function refOf(snapshot: SettingsFormSnapshot<NetxopsSettings>): string {
|
||||||
const declared = snapshot.value?.tokenCredentialRef
|
const declared = snapshot.value?.tokenCredentialRef
|
||||||
return declared !== undefined && declared.length > 0 ? declared : DEFAULT_TOKEN_REF
|
return declared !== undefined && declared.length > 0 ? declared : DEFAULT_TOKEN_REF
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,19 +1,23 @@
|
||||||
/**
|
/**
|
||||||
* Browser half — Settings → Netx Ops section (uds-auth-style page).
|
* Browser half — Settings → Netx Ops section (uds-auth-style page).
|
||||||
*
|
*
|
||||||
* Do not hard-inject `remote.credentials`: shipped `@deepseek-ai/dsh` 0.1.1-rc.2
|
* Hard-inject only services present on both DSH ≤0.1.5 and ≥0.1.7.
|
||||||
* remotes assembly does not mount that namespace (Models/Plugins cards only
|
* Do NOT hard-inject `settingsScope`: DSH ≥0.1.7 / 0.2.0 removed it
|
||||||
* inject `remote`). Soft-inject when a newer Host provides it.
|
* (`configForms` replaced it) and a hard wait kills web boot.
|
||||||
|
*
|
||||||
|
* Soft-inject `configForms` first (0.2.0 desktop), then `settingsScope`
|
||||||
|
* (≤0.1.5). Soft-inject credentials / connection / directoryPicker when present.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import type { Context as ClientContext } from '@deepseek-ai/cordis'
|
import type { Context as ClientContext } from '@deepseek-ai/cordis'
|
||||||
import type {} from '@deepseek-ai/dsh-client-locale/client'
|
import type {} from '@deepseek-ai/dsh-client-locale/client'
|
||||||
import type {} from '@deepseek-ai/dsh-client-ui-settings/client'
|
|
||||||
import type {} from '@deepseek-ai/dsh-client-ui-renderer/client'
|
import type {} from '@deepseek-ai/dsh-client-ui-renderer/client'
|
||||||
import type {} from '@deepseek-ai/dsh-api-remotes/client'
|
import type {} from '@deepseek-ai/dsh-api-remotes/client'
|
||||||
import { NetxopsCard } from './NetxopsCard.tsx'
|
import { NetxopsCard } from './NetxopsCard.tsx'
|
||||||
import { NETXOPS_NS, NetxopsCardController } from './controller.ts'
|
import { NETXOPS_NS, NetxopsCardController } from './controller.ts'
|
||||||
import { en, zh, type NetxopsLocaleKey } from './locales.ts'
|
import { en, zh, type NetxopsLocaleKey } from './locales.ts'
|
||||||
|
import type { SettingsFormScope } from './settings-form.ts'
|
||||||
|
import type { NetxopsSettings } from './controller.ts'
|
||||||
|
|
||||||
declare module '@deepseek-ai/dsh-client-ui-slots' {
|
declare module '@deepseek-ai/dsh-client-ui-slots' {
|
||||||
interface LocaleNamespaceMap {
|
interface LocaleNamespaceMap {
|
||||||
|
|
@ -23,12 +27,13 @@ declare module '@deepseek-ai/dsh-client-ui-slots' {
|
||||||
|
|
||||||
const LOCALE_NS = 'settings.netxops'
|
const LOCALE_NS = 'settings.netxops'
|
||||||
|
|
||||||
/** Match shipped settings shell inject (no remote.credentials). */
|
/**
|
||||||
|
* Services that exist on every supported DSH client roster.
|
||||||
|
* Settings transport (`configForms` / `settingsScope`) is soft-injected below.
|
||||||
|
*/
|
||||||
export const inject = [
|
export const inject = [
|
||||||
'slots',
|
'slots',
|
||||||
'locale',
|
'locale',
|
||||||
'remote',
|
|
||||||
'settingsScope',
|
|
||||||
]
|
]
|
||||||
|
|
||||||
export function apply(ctx: ClientContext): void {
|
export function apply(ctx: ClientContext): void {
|
||||||
|
|
@ -45,11 +50,14 @@ export function apply(ctx: ClientContext): void {
|
||||||
}
|
}
|
||||||
}, 'netxops: locales')
|
}, 'netxops: locales')
|
||||||
|
|
||||||
const card = new NetxopsCardController(
|
|
||||||
ctx.settingsScope.bind({ namespace: NETXOPS_NS }),
|
|
||||||
ctx,
|
|
||||||
)
|
|
||||||
const t = ctx.locale.bind(LOCALE_NS) as (key: NetxopsLocaleKey) => string
|
const t = ctx.locale.bind(LOCALE_NS) as (key: NetxopsLocaleKey) => string
|
||||||
|
let mounted = false
|
||||||
|
|
||||||
|
const mountCard = (scope: SettingsFormScope<NetxopsSettings>): void => {
|
||||||
|
if (mounted) return
|
||||||
|
mounted = true
|
||||||
|
|
||||||
|
const card = new NetxopsCardController(scope, ctx)
|
||||||
|
|
||||||
// Optional: newer remotes that mount credentials unlock the token field.
|
// Optional: newer remotes that mount credentials unlock the token field.
|
||||||
ctx.inject(['remote.credentials'], (credCtx) => {
|
ctx.inject(['remote.credentials'], (credCtx) => {
|
||||||
|
|
@ -79,7 +87,6 @@ export function apply(ctx: ClientContext): void {
|
||||||
})
|
})
|
||||||
|
|
||||||
// Soft-inject Host directory picker for knowledge-base browse.
|
// Soft-inject Host directory picker for knowledge-base browse.
|
||||||
// Prefer the dedicated remote namespace; also accept ctx.remote.directoryPicker.
|
|
||||||
const bindDirectoryPicker = (picker: { pick?: (signal?: AbortSignal) => Promise<string | null> } | undefined): void => {
|
const bindDirectoryPicker = (picker: { pick?: (signal?: AbortSignal) => Promise<string | null> } | undefined): void => {
|
||||||
if (!picker || typeof picker.pick !== 'function') return
|
if (!picker || typeof picker.pick !== 'function') return
|
||||||
card.setDirectoryPicker(picker as { pick: (signal?: AbortSignal) => Promise<string | null> })
|
card.setDirectoryPicker(picker as { pick: (signal?: AbortSignal) => Promise<string | null> })
|
||||||
|
|
@ -110,4 +117,31 @@ export function apply(ctx: ClientContext): void {
|
||||||
locale: LOCALE_NS,
|
locale: LOCALE_NS,
|
||||||
inject: () => card.inject(),
|
inject: () => card.inject(),
|
||||||
}, NetxopsCard))
|
}, NetxopsCard))
|
||||||
|
}
|
||||||
|
|
||||||
|
// DSH ≥0.1.7 / 0.2.0 — profile entry id matches cordis.patch.yml `id: netxops`.
|
||||||
|
ctx.inject(['configForms'], (formsCtx) => {
|
||||||
|
const forms = (formsCtx as { configForms?: { get: (id: string) => SettingsFormScope<NetxopsSettings> } })
|
||||||
|
.configForms
|
||||||
|
if (!forms || typeof forms.get !== 'function') {
|
||||||
|
formsCtx.logger?.warn?.('netxops: configForms present but .get missing')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
formsCtx.logger?.info?.('netxops: settings card via configForms')
|
||||||
|
mountCard(forms.get(NETXOPS_NS))
|
||||||
|
})
|
||||||
|
|
||||||
|
// DSH ≤0.1.5 — settingsScope binder.
|
||||||
|
ctx.inject(['settingsScope'], (scopeCtx) => {
|
||||||
|
const binder = (scopeCtx as {
|
||||||
|
settingsScope?: { bind: (spec: { namespace: string }) => SettingsFormScope<NetxopsSettings> }
|
||||||
|
}).settingsScope
|
||||||
|
if (!binder || typeof binder.bind !== 'function') {
|
||||||
|
scopeCtx.logger?.warn?.('netxops: settingsScope present but .bind missing')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (mounted) return
|
||||||
|
scopeCtx.logger?.info?.('netxops: settings card via settingsScope')
|
||||||
|
mountCard(binder.bind({ namespace: NETXOPS_NS }))
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
|
||||||
24
src/client/settings-form.ts
Normal file
24
src/client/settings-form.ts
Normal file
|
|
@ -0,0 +1,24 @@
|
||||||
|
/**
|
||||||
|
* Shared settings-form face used by the Plugins card.
|
||||||
|
*
|
||||||
|
* DSH ≤0.1.5 exposes `ctx.settingsScope.bind({ namespace })`.
|
||||||
|
* DSH ≥0.1.7 / 0.2.0 exposes `ctx.configForms.get(entryId)`.
|
||||||
|
* Snapshot + set/unset/subscribe shapes match, so the card stays agnostic.
|
||||||
|
*/
|
||||||
|
|
||||||
|
export interface SettingsFormSnapshot<T> {
|
||||||
|
status: string
|
||||||
|
value?: T
|
||||||
|
base?: unknown
|
||||||
|
user?: unknown
|
||||||
|
revision?: number
|
||||||
|
writable: boolean
|
||||||
|
mode?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface SettingsFormScope<T> {
|
||||||
|
getSnapshot(): SettingsFormSnapshot<T>
|
||||||
|
subscribe(listener: () => void): () => void
|
||||||
|
set(field: string, value: unknown): Promise<boolean | void>
|
||||||
|
unset(field: string): Promise<boolean | void>
|
||||||
|
}
|
||||||
117
src/index.ts
117
src/index.ts
|
|
@ -168,30 +168,40 @@ export interface Config {
|
||||||
groupKbPublic: boolean
|
groupKbPublic: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* DSH ≥0.1.7 / 0.2.0 only projects Config fields marked `.volatile()` into the
|
||||||
|
* settings UI. Zero volatile fields → the whole entry is silently filtered.
|
||||||
|
* Older schemastery without `volatile` keeps the bare schema (≤0.1.5 path).
|
||||||
|
*/
|
||||||
|
function vol<S>(schema: S): S {
|
||||||
|
const candidate = schema as { volatile?: () => S }
|
||||||
|
return typeof candidate.volatile === 'function' ? candidate.volatile() : schema
|
||||||
|
}
|
||||||
|
|
||||||
export const Config: z<Config> = z.object({
|
export const Config: z<Config> = z.object({
|
||||||
apiUrl: z.string().default('http://127.0.0.1:8890'),
|
apiUrl: vol(z.string().default('http://127.0.0.1:8890')),
|
||||||
lang: z.string().default('zh'),
|
lang: vol(z.string().default('zh')),
|
||||||
thinkingLanguage: z.string().default('auto'),
|
thinkingLanguage: vol(z.string().default('auto')),
|
||||||
replyLanguage: z.string().default('follow-user'),
|
replyLanguage: vol(z.string().default('follow-user')),
|
||||||
tokenCredentialRef: z.string().role('credential-ref').default(DEFAULT_TOKEN_REF),
|
tokenCredentialRef: vol(z.string().role('credential-ref').default(DEFAULT_TOKEN_REF)),
|
||||||
toolCallTimeoutMs: z.number().step(1).min(1000).default(120_000),
|
toolCallTimeoutMs: vol(z.number().step(1).min(1000).default(120_000)),
|
||||||
installAgentPreset: z.boolean().default(true),
|
installAgentPreset: vol(z.boolean().default(true)),
|
||||||
alarmPushEnabled: z.boolean().default(false),
|
alarmPushEnabled: vol(z.boolean().default(false)),
|
||||||
alarmDeliverDsh: z.boolean().default(true),
|
alarmDeliverDsh: vol(z.boolean().default(true)),
|
||||||
alarmDeliverIm: z.boolean().default(false),
|
alarmDeliverIm: vol(z.boolean().default(false)),
|
||||||
imTargets: z.string().default(''),
|
imTargets: vol(z.string().default('')),
|
||||||
imBotId: z.string().default(''),
|
imBotId: vol(z.string().default('')),
|
||||||
imTargetId: z.string().default(''),
|
imTargetId: vol(z.string().default('')),
|
||||||
nmsProvider: z.string().default('zte-ume'),
|
nmsProvider: vol(z.string().default('zte-ume')),
|
||||||
groupOpsInPreset: z.boolean().default(true),
|
groupOpsInPreset: vol(z.boolean().default(true)),
|
||||||
groupOpsPublic: z.boolean().default(false),
|
groupOpsPublic: vol(z.boolean().default(false)),
|
||||||
groupTopologyInPreset: z.boolean().default(false),
|
groupTopologyInPreset: vol(z.boolean().default(false)),
|
||||||
groupTopologyPublic: z.boolean().default(false),
|
groupTopologyPublic: vol(z.boolean().default(false)),
|
||||||
groupBizMonitorInPreset: z.boolean().default(true),
|
groupBizMonitorInPreset: vol(z.boolean().default(true)),
|
||||||
groupBizMonitorPublic: z.boolean().default(false),
|
groupBizMonitorPublic: vol(z.boolean().default(false)),
|
||||||
kbRoot: z.string().default(''),
|
kbRoot: vol(z.string().default('')),
|
||||||
groupKbInPreset: z.boolean().default(true),
|
groupKbInPreset: vol(z.boolean().default(true)),
|
||||||
groupKbPublic: z.boolean().default(false),
|
groupKbPublic: vol(z.boolean().default(false)),
|
||||||
})
|
})
|
||||||
|
|
||||||
/** Package root (parent of `lib/` or `src/` depending on launch). */
|
/** Package root (parent of `lib/` or `src/` depending on launch). */
|
||||||
|
|
@ -243,10 +253,27 @@ async function resolveToken(ctx: Context, refName: string): Promise<string> {
|
||||||
return hit?.value ?? ''
|
return hit?.value ?? ''
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Normalize settings.describe() across DSH generations (array or { namespaces }). */
|
||||||
|
function describeRows(describe: unknown): Array<{ ns?: string, value?: unknown }> {
|
||||||
|
if (typeof describe !== 'function') return []
|
||||||
|
try {
|
||||||
|
const raw = (describe as () => unknown)()
|
||||||
|
if (Array.isArray(raw)) return raw as Array<{ ns?: string, value?: unknown }>
|
||||||
|
if (raw && typeof raw === 'object' && Array.isArray((raw as { namespaces?: unknown }).namespaces)) {
|
||||||
|
return (raw as { namespaces: Array<{ ns?: string, value?: unknown }> }).namespaces
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
// describe can throw while the provider is settling
|
||||||
|
}
|
||||||
|
return []
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Register the settings namespace on both dsh generations:
|
* Register / follow the settings namespace across DSH generations:
|
||||||
* - 0.1.1-rc.2: standalone `installSettingsSection`
|
* - 0.1.1-rc.2: standalone `installSettingsSection`
|
||||||
* - ≥0.1.2-rc.1: `settings.installSection` on the provider
|
* - ≥0.1.2-rc.1: `settings.installSection` on the provider
|
||||||
|
* - ≥0.1.7 / 0.2.0: Config is projected from the Loader entry; no owner scope —
|
||||||
|
* re-read via `settings.describe()` and poll for UI writes (no scope.watch).
|
||||||
*/
|
*/
|
||||||
function installNetxopsSettings(
|
function installNetxopsSettings(
|
||||||
ctx: Context,
|
ctx: Context,
|
||||||
|
|
@ -271,7 +298,47 @@ function installNetxopsSettings(
|
||||||
}
|
}
|
||||||
|
|
||||||
ctx.inject(['settings'], (settingsCtx) => {
|
ctx.inject(['settings'], (settingsCtx) => {
|
||||||
settingsCtx.settings.installSection(ctx, NETXOPS_SETTINGS_NAMESPACE, Config, entry, hooks)
|
const settings = settingsCtx.settings as {
|
||||||
|
installSection?: (
|
||||||
|
context: Context,
|
||||||
|
ns: string,
|
||||||
|
schema: z<Config>,
|
||||||
|
base: Config,
|
||||||
|
sectionHooks: typeof hooks,
|
||||||
|
) => void
|
||||||
|
describe?: () => unknown
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeof settings.installSection === 'function') {
|
||||||
|
settings.installSection(ctx, NETXOPS_SETTINGS_NAMESPACE, Config, entry, hooks)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// DSH ≥0.1.7: profile Config projection — follow describe() for live values.
|
||||||
|
const readLive = (): Config => {
|
||||||
|
const row = describeRows(settings.describe).find(item => item.ns === NETXOPS_SETTINGS_NAMESPACE)
|
||||||
|
if (row?.value !== null && typeof row?.value === 'object' && !Array.isArray(row.value)) {
|
||||||
|
return { ...entry, ...(row.value as Partial<Config>) }
|
||||||
|
}
|
||||||
|
return entry
|
||||||
|
}
|
||||||
|
hooks.setSource(readLive)
|
||||||
|
hooks.onChange()
|
||||||
|
|
||||||
|
let lastFingerprint = JSON.stringify(readLive())
|
||||||
|
const timer = setInterval(() => {
|
||||||
|
const next = readLive()
|
||||||
|
const fingerprint = JSON.stringify(next)
|
||||||
|
if (fingerprint === lastFingerprint) return
|
||||||
|
lastFingerprint = fingerprint
|
||||||
|
hooks.onChange()
|
||||||
|
}, 2_000)
|
||||||
|
settingsCtx.effect(() => () => {
|
||||||
|
clearInterval(timer)
|
||||||
|
}, 'netxops: settings describe poll')
|
||||||
|
settingsCtx.logger.info(
|
||||||
|
'netxops: following settings via describe() (no installSection — DSH ≥0.1.7 path)',
|
||||||
|
)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue