mirror of
https://github.com/hansjone/netxops.git
synced 2026-10-10 04:33:17 +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
112
lib/client.js
112
lib/client.js
|
|
@ -2629,9 +2629,7 @@ var zh = {
|
|||
var LOCALE_NS = "settings.netxops";
|
||||
var inject = [
|
||||
"slots",
|
||||
"locale",
|
||||
"remote",
|
||||
"settingsScope"
|
||||
"locale"
|
||||
];
|
||||
function apply(ctx) {
|
||||
ctx.effect(() => {
|
||||
|
|
@ -2646,53 +2644,79 @@ function apply(ctx) {
|
|||
};
|
||||
}
|
||||
}, "netxops: locales");
|
||||
const card = new NetxopsCardController(ctx.settingsScope.bind({ namespace: NETXOPS_NS }), ctx);
|
||||
const t = ctx.locale.bind(LOCALE_NS);
|
||||
ctx.inject(["remote.credentials"], (credCtx) => {
|
||||
card.setCredentialsAvailable(true);
|
||||
credCtx.effect(() => {
|
||||
const off = credCtx.remote.$on("credentials/reference-updated", (ref) => {
|
||||
card.refreshCredential(String(ref));
|
||||
});
|
||||
return () => {
|
||||
off();
|
||||
card.setCredentialsAvailable(false);
|
||||
};
|
||||
}, "netxops: credential invalidations");
|
||||
});
|
||||
ctx.inject(["connection"], (connCtx) => {
|
||||
const call = connCtx.connection?.rpc?.call?.bind(connCtx.connection.rpc);
|
||||
if (typeof call !== "function") {
|
||||
connCtx.logger.warn("netxops: connection.rpc.call unavailable — alarm status UI disabled");
|
||||
let mounted = false;
|
||||
const mountCard = (scope) => {
|
||||
if (mounted)
|
||||
return;
|
||||
mounted = true;
|
||||
const card = new NetxopsCardController(scope, ctx);
|
||||
ctx.inject(["remote.credentials"], (credCtx) => {
|
||||
card.setCredentialsAvailable(true);
|
||||
credCtx.effect(() => {
|
||||
const off = credCtx.remote.$on("credentials/reference-updated", (ref) => {
|
||||
card.refreshCredential(String(ref));
|
||||
});
|
||||
return () => {
|
||||
off();
|
||||
card.setCredentialsAvailable(false);
|
||||
};
|
||||
}, "netxops: credential invalidations");
|
||||
});
|
||||
ctx.inject(["connection"], (connCtx) => {
|
||||
const call = connCtx.connection?.rpc?.call?.bind(connCtx.connection.rpc);
|
||||
if (typeof call !== "function") {
|
||||
connCtx.logger.warn("netxops: connection.rpc.call unavailable — alarm status UI disabled");
|
||||
return;
|
||||
}
|
||||
card.setAlarmPushRpc(call);
|
||||
connCtx.effect(() => () => {
|
||||
card.setAlarmPushRpc(undefined);
|
||||
}, "netxops: clear alarm-push rpc");
|
||||
});
|
||||
const bindDirectoryPicker = (picker) => {
|
||||
if (!picker || typeof picker.pick !== "function")
|
||||
return;
|
||||
card.setDirectoryPicker(picker);
|
||||
};
|
||||
bindDirectoryPicker(ctx.remote?.directoryPicker);
|
||||
ctx.inject(["remote.directoryPicker"], (dpCtx) => {
|
||||
const viaGet = typeof dpCtx.get === "function" ? dpCtx.get("remote.directoryPicker") : undefined;
|
||||
const viaNested = dpCtx.remote?.directoryPicker;
|
||||
bindDirectoryPicker(viaGet ?? viaNested);
|
||||
dpCtx.effect(() => () => {
|
||||
card.setDirectoryPicker(undefined);
|
||||
}, "netxops: clear directory picker");
|
||||
});
|
||||
ctx.slots.inject("settings.section", () => ctx.slots.register({
|
||||
name: "settings.section",
|
||||
id: NETXOPS_NS,
|
||||
order: 24,
|
||||
label: () => t("title"),
|
||||
locale: LOCALE_NS,
|
||||
inject: () => card.inject()
|
||||
}, 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;
|
||||
}
|
||||
card.setAlarmPushRpc(call);
|
||||
connCtx.effect(() => () => {
|
||||
card.setAlarmPushRpc(undefined);
|
||||
}, "netxops: clear alarm-push rpc");
|
||||
formsCtx.logger?.info?.("netxops: settings card via configForms");
|
||||
mountCard(forms.get(NETXOPS_NS));
|
||||
});
|
||||
const bindDirectoryPicker = (picker) => {
|
||||
if (!picker || typeof picker.pick !== "function")
|
||||
ctx.inject(["settingsScope"], (scopeCtx) => {
|
||||
const binder = scopeCtx.settingsScope;
|
||||
if (!binder || typeof binder.bind !== "function") {
|
||||
scopeCtx.logger?.warn?.("netxops: settingsScope present but .bind missing");
|
||||
return;
|
||||
card.setDirectoryPicker(picker);
|
||||
};
|
||||
bindDirectoryPicker(ctx.remote?.directoryPicker);
|
||||
ctx.inject(["remote.directoryPicker"], (dpCtx) => {
|
||||
const viaGet = typeof dpCtx.get === "function" ? dpCtx.get("remote.directoryPicker") : undefined;
|
||||
const viaNested = dpCtx.remote?.directoryPicker;
|
||||
bindDirectoryPicker(viaGet ?? viaNested);
|
||||
dpCtx.effect(() => () => {
|
||||
card.setDirectoryPicker(undefined);
|
||||
}, "netxops: clear directory picker");
|
||||
}
|
||||
if (mounted)
|
||||
return;
|
||||
scopeCtx.logger?.info?.("netxops: settings card via settingsScope");
|
||||
mountCard(binder.bind({ namespace: NETXOPS_NS }));
|
||||
});
|
||||
ctx.slots.inject("settings.section", () => ctx.slots.register({
|
||||
name: "settings.section",
|
||||
id: NETXOPS_NS,
|
||||
order: 24,
|
||||
label: () => t("title"),
|
||||
locale: LOCALE_NS,
|
||||
inject: () => card.inject()
|
||||
}, NetxopsCard));
|
||||
}
|
||||
|
||||
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_PRESET_ID = "netxops";
|
||||
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({
|
||||
apiUrl: z.string().default("http://127.0.0.1:8890"),
|
||||
lang: z.string().default("zh"),
|
||||
thinkingLanguage: z.string().default("auto"),
|
||||
replyLanguage: z.string().default("follow-user"),
|
||||
tokenCredentialRef: z.string().role("credential-ref").default(DEFAULT_TOKEN_REF),
|
||||
toolCallTimeoutMs: z.number().step(1).min(1000).default(120000),
|
||||
installAgentPreset: z.boolean().default(true),
|
||||
alarmPushEnabled: z.boolean().default(false),
|
||||
alarmDeliverDsh: z.boolean().default(true),
|
||||
alarmDeliverIm: z.boolean().default(false),
|
||||
imTargets: z.string().default(""),
|
||||
imBotId: z.string().default(""),
|
||||
imTargetId: z.string().default(""),
|
||||
nmsProvider: z.string().default("zte-ume"),
|
||||
groupOpsInPreset: z.boolean().default(true),
|
||||
groupOpsPublic: z.boolean().default(false),
|
||||
groupTopologyInPreset: z.boolean().default(false),
|
||||
groupTopologyPublic: z.boolean().default(false),
|
||||
groupBizMonitorInPreset: z.boolean().default(true),
|
||||
groupBizMonitorPublic: z.boolean().default(false),
|
||||
kbRoot: z.string().default(""),
|
||||
groupKbInPreset: z.boolean().default(true),
|
||||
groupKbPublic: z.boolean().default(false)
|
||||
apiUrl: vol(z.string().default("http://127.0.0.1:8890")),
|
||||
lang: vol(z.string().default("zh")),
|
||||
thinkingLanguage: vol(z.string().default("auto")),
|
||||
replyLanguage: vol(z.string().default("follow-user")),
|
||||
tokenCredentialRef: vol(z.string().role("credential-ref").default(DEFAULT_TOKEN_REF)),
|
||||
toolCallTimeoutMs: vol(z.number().step(1).min(1000).default(120000)),
|
||||
installAgentPreset: vol(z.boolean().default(true)),
|
||||
alarmPushEnabled: vol(z.boolean().default(false)),
|
||||
alarmDeliverDsh: vol(z.boolean().default(true)),
|
||||
alarmDeliverIm: vol(z.boolean().default(false)),
|
||||
imTargets: vol(z.string().default("")),
|
||||
imBotId: vol(z.string().default("")),
|
||||
imTargetId: vol(z.string().default("")),
|
||||
nmsProvider: vol(z.string().default("zte-ume")),
|
||||
groupOpsInPreset: vol(z.boolean().default(true)),
|
||||
groupOpsPublic: vol(z.boolean().default(false)),
|
||||
groupTopologyInPreset: vol(z.boolean().default(false)),
|
||||
groupTopologyPublic: vol(z.boolean().default(false)),
|
||||
groupBizMonitorInPreset: vol(z.boolean().default(true)),
|
||||
groupBizMonitorPublic: vol(z.boolean().default(false)),
|
||||
kbRoot: vol(z.string().default("")),
|
||||
groupKbInPreset: vol(z.boolean().default(true)),
|
||||
groupKbPublic: vol(z.boolean().default(false))
|
||||
});
|
||||
function packageRoot() {
|
||||
return join7(dirname4(fileURLToPath2(import.meta.url)), "..");
|
||||
|
|
@ -5296,6 +5300,19 @@ async function resolveToken(ctx, refName) {
|
|||
const hit = await ctx.credentials.resolve(credentialRef(refName));
|
||||
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) {
|
||||
const legacy = DshSettings.installSettingsSection;
|
||||
if (typeof legacy === "function") {
|
||||
|
|
@ -5303,7 +5320,33 @@ function installNetxopsSettings(ctx, entry, hooks) {
|
|||
return;
|
||||
}
|
||||
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({})) {
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"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",
|
||||
"license": "MIT",
|
||||
"type": "module",
|
||||
|
|
@ -63,10 +63,10 @@
|
|||
},
|
||||
"client": {
|
||||
"platform": "web",
|
||||
"immediately": true,
|
||||
"inject": [
|
||||
"@deepseek-ai/dsh-client-locale",
|
||||
"@deepseek-ai/dsh-client-ui-settings",
|
||||
"@deepseek-ai/dsh-api-remotes"
|
||||
"@deepseek-ai/dsh-client-ui-slots",
|
||||
"@deepseek-ai/dsh-client-locale"
|
||||
]
|
||||
}
|
||||
},
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@
|
|||
* 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'
|
||||
|
||||
export type FieldWrite = { kind: 'set'; value: unknown } | { kind: 'clear' }
|
||||
|
|
@ -110,7 +110,7 @@ export class CardForm<T> {
|
|||
private failed = false
|
||||
|
||||
constructor(
|
||||
private readonly scope: SettingsScope<T>,
|
||||
private readonly scope: SettingsFormScope<T>,
|
||||
specs: CardFieldSpec[],
|
||||
secrets: CardSecretSpec[] = [],
|
||||
) {
|
||||
|
|
@ -235,7 +235,7 @@ export class CardForm<T> {
|
|||
return spec
|
||||
}
|
||||
|
||||
private snapshotOf(): SettingsScopeSnapshot<T> {
|
||||
private snapshotOf(): SettingsFormSnapshot<T> {
|
||||
return this.scope.getSnapshot()
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@
|
|||
|
||||
import type { Context as ClientContext } from '@deepseek-ai/cordis'
|
||||
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 {
|
||||
CardForm, textField, booleanField, booleanFieldPersistFalse,
|
||||
|
|
@ -175,7 +175,7 @@ export class NetxopsCardController {
|
|||
private exportInFlight: Promise<void> | undefined
|
||||
|
||||
constructor(
|
||||
private readonly scope: SettingsScope<NetxopsSettings>,
|
||||
private readonly scope: SettingsFormScope<NetxopsSettings>,
|
||||
private readonly ctx: ClientContext,
|
||||
) {
|
||||
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
|
||||
return declared !== undefined && declared.length > 0 ? declared : DEFAULT_TOKEN_REF
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,19 +1,23 @@
|
|||
/**
|
||||
* 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
|
||||
* remotes assembly does not mount that namespace (Models/Plugins cards only
|
||||
* inject `remote`). Soft-inject when a newer Host provides it.
|
||||
* Hard-inject only services present on both DSH ≤0.1.5 and ≥0.1.7.
|
||||
* Do NOT hard-inject `settingsScope`: DSH ≥0.1.7 / 0.2.0 removed 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 {} 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-api-remotes/client'
|
||||
import { NetxopsCard } from './NetxopsCard.tsx'
|
||||
import { NETXOPS_NS, NetxopsCardController } from './controller.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' {
|
||||
interface LocaleNamespaceMap {
|
||||
|
|
@ -23,12 +27,13 @@ declare module '@deepseek-ai/dsh-client-ui-slots' {
|
|||
|
||||
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 = [
|
||||
'slots',
|
||||
'locale',
|
||||
'remote',
|
||||
'settingsScope',
|
||||
]
|
||||
|
||||
export function apply(ctx: ClientContext): void {
|
||||
|
|
@ -45,69 +50,98 @@ export function apply(ctx: ClientContext): void {
|
|||
}
|
||||
}, 'netxops: locales')
|
||||
|
||||
const card = new NetxopsCardController(
|
||||
ctx.settingsScope.bind({ namespace: NETXOPS_NS }),
|
||||
ctx,
|
||||
)
|
||||
const t = ctx.locale.bind(LOCALE_NS) as (key: NetxopsLocaleKey) => string
|
||||
let mounted = false
|
||||
|
||||
// Optional: newer remotes that mount credentials unlock the token field.
|
||||
ctx.inject(['remote.credentials'], (credCtx) => {
|
||||
card.setCredentialsAvailable(true)
|
||||
credCtx.effect(() => {
|
||||
const off = credCtx.remote.$on('credentials/reference-updated', (ref) => {
|
||||
card.refreshCredential(String(ref))
|
||||
})
|
||||
return () => {
|
||||
off()
|
||||
card.setCredentialsAvailable(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.
|
||||
ctx.inject(['remote.credentials'], (credCtx) => {
|
||||
card.setCredentialsAvailable(true)
|
||||
credCtx.effect(() => {
|
||||
const off = credCtx.remote.$on('credentials/reference-updated', (ref) => {
|
||||
card.refreshCredential(String(ref))
|
||||
})
|
||||
return () => {
|
||||
off()
|
||||
card.setCredentialsAvailable(false)
|
||||
}
|
||||
}, 'netxops: credential invalidations')
|
||||
})
|
||||
|
||||
// Soft-inject Connection so the card can poll host WSS status + KB status.
|
||||
ctx.inject(['connection'], (connCtx) => {
|
||||
const call = connCtx.connection?.rpc?.call?.bind(connCtx.connection.rpc)
|
||||
if (typeof call !== 'function') {
|
||||
connCtx.logger.warn('netxops: connection.rpc.call unavailable — alarm status UI disabled')
|
||||
return
|
||||
}
|
||||
}, 'netxops: credential invalidations')
|
||||
})
|
||||
card.setAlarmPushRpc(call)
|
||||
connCtx.effect(() => () => {
|
||||
card.setAlarmPushRpc(undefined)
|
||||
}, 'netxops: clear alarm-push rpc')
|
||||
})
|
||||
|
||||
// Soft-inject Connection so the card can poll host WSS status + KB status.
|
||||
ctx.inject(['connection'], (connCtx) => {
|
||||
const call = connCtx.connection?.rpc?.call?.bind(connCtx.connection.rpc)
|
||||
if (typeof call !== 'function') {
|
||||
connCtx.logger.warn('netxops: connection.rpc.call unavailable — alarm status UI disabled')
|
||||
// Soft-inject Host directory picker for knowledge-base browse.
|
||||
const bindDirectoryPicker = (picker: { pick?: (signal?: AbortSignal) => Promise<string | null> } | undefined): void => {
|
||||
if (!picker || typeof picker.pick !== 'function') return
|
||||
card.setDirectoryPicker(picker as { pick: (signal?: AbortSignal) => Promise<string | null> })
|
||||
}
|
||||
bindDirectoryPicker(
|
||||
(ctx as { remote?: { directoryPicker?: { pick?: (signal?: AbortSignal) => Promise<string | null> } } })
|
||||
.remote?.directoryPicker,
|
||||
)
|
||||
ctx.inject(['remote.directoryPicker'], (dpCtx) => {
|
||||
const viaGet = typeof (dpCtx as { get?: (name: string) => unknown }).get === 'function'
|
||||
? (dpCtx as { get: (name: string) => unknown }).get('remote.directoryPicker') as
|
||||
| { pick?: (signal?: AbortSignal) => Promise<string | null> }
|
||||
| undefined
|
||||
: undefined
|
||||
const viaNested = (dpCtx as { remote?: { directoryPicker?: { pick?: (signal?: AbortSignal) => Promise<string | null> } } })
|
||||
.remote?.directoryPicker
|
||||
bindDirectoryPicker(viaGet ?? viaNested)
|
||||
dpCtx.effect(() => () => {
|
||||
card.setDirectoryPicker(undefined)
|
||||
}, 'netxops: clear directory picker')
|
||||
})
|
||||
|
||||
ctx.slots.inject('settings.section', () => ctx.slots.register({
|
||||
name: 'settings.section',
|
||||
id: NETXOPS_NS,
|
||||
order: 24,
|
||||
label: () => t('title'),
|
||||
locale: LOCALE_NS,
|
||||
inject: () => card.inject(),
|
||||
}, 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
|
||||
}
|
||||
card.setAlarmPushRpc(call)
|
||||
connCtx.effect(() => () => {
|
||||
card.setAlarmPushRpc(undefined)
|
||||
}, 'netxops: clear alarm-push rpc')
|
||||
formsCtx.logger?.info?.('netxops: settings card via configForms')
|
||||
mountCard(forms.get(NETXOPS_NS))
|
||||
})
|
||||
|
||||
// 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 => {
|
||||
if (!picker || typeof picker.pick !== 'function') return
|
||||
card.setDirectoryPicker(picker as { pick: (signal?: AbortSignal) => Promise<string | null> })
|
||||
}
|
||||
bindDirectoryPicker(
|
||||
(ctx as { remote?: { directoryPicker?: { pick?: (signal?: AbortSignal) => Promise<string | null> } } })
|
||||
.remote?.directoryPicker,
|
||||
)
|
||||
ctx.inject(['remote.directoryPicker'], (dpCtx) => {
|
||||
const viaGet = typeof (dpCtx as { get?: (name: string) => unknown }).get === 'function'
|
||||
? (dpCtx as { get: (name: string) => unknown }).get('remote.directoryPicker') as
|
||||
| { pick?: (signal?: AbortSignal) => Promise<string | null> }
|
||||
| undefined
|
||||
: undefined
|
||||
const viaNested = (dpCtx as { remote?: { directoryPicker?: { pick?: (signal?: AbortSignal) => Promise<string | null> } } })
|
||||
.remote?.directoryPicker
|
||||
bindDirectoryPicker(viaGet ?? viaNested)
|
||||
dpCtx.effect(() => () => {
|
||||
card.setDirectoryPicker(undefined)
|
||||
}, 'netxops: clear directory picker')
|
||||
// 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 }))
|
||||
})
|
||||
|
||||
ctx.slots.inject('settings.section', () => ctx.slots.register({
|
||||
name: 'settings.section',
|
||||
id: NETXOPS_NS,
|
||||
order: 24,
|
||||
label: () => t('title'),
|
||||
locale: LOCALE_NS,
|
||||
inject: () => card.inject(),
|
||||
}, NetxopsCard))
|
||||
}
|
||||
|
|
|
|||
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
|
||||
}
|
||||
|
||||
/**
|
||||
* 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({
|
||||
apiUrl: z.string().default('http://127.0.0.1:8890'),
|
||||
lang: z.string().default('zh'),
|
||||
thinkingLanguage: z.string().default('auto'),
|
||||
replyLanguage: z.string().default('follow-user'),
|
||||
tokenCredentialRef: z.string().role('credential-ref').default(DEFAULT_TOKEN_REF),
|
||||
toolCallTimeoutMs: z.number().step(1).min(1000).default(120_000),
|
||||
installAgentPreset: z.boolean().default(true),
|
||||
alarmPushEnabled: z.boolean().default(false),
|
||||
alarmDeliverDsh: z.boolean().default(true),
|
||||
alarmDeliverIm: z.boolean().default(false),
|
||||
imTargets: z.string().default(''),
|
||||
imBotId: z.string().default(''),
|
||||
imTargetId: z.string().default(''),
|
||||
nmsProvider: z.string().default('zte-ume'),
|
||||
groupOpsInPreset: z.boolean().default(true),
|
||||
groupOpsPublic: z.boolean().default(false),
|
||||
groupTopologyInPreset: z.boolean().default(false),
|
||||
groupTopologyPublic: z.boolean().default(false),
|
||||
groupBizMonitorInPreset: z.boolean().default(true),
|
||||
groupBizMonitorPublic: z.boolean().default(false),
|
||||
kbRoot: z.string().default(''),
|
||||
groupKbInPreset: z.boolean().default(true),
|
||||
groupKbPublic: z.boolean().default(false),
|
||||
apiUrl: vol(z.string().default('http://127.0.0.1:8890')),
|
||||
lang: vol(z.string().default('zh')),
|
||||
thinkingLanguage: vol(z.string().default('auto')),
|
||||
replyLanguage: vol(z.string().default('follow-user')),
|
||||
tokenCredentialRef: vol(z.string().role('credential-ref').default(DEFAULT_TOKEN_REF)),
|
||||
toolCallTimeoutMs: vol(z.number().step(1).min(1000).default(120_000)),
|
||||
installAgentPreset: vol(z.boolean().default(true)),
|
||||
alarmPushEnabled: vol(z.boolean().default(false)),
|
||||
alarmDeliverDsh: vol(z.boolean().default(true)),
|
||||
alarmDeliverIm: vol(z.boolean().default(false)),
|
||||
imTargets: vol(z.string().default('')),
|
||||
imBotId: vol(z.string().default('')),
|
||||
imTargetId: vol(z.string().default('')),
|
||||
nmsProvider: vol(z.string().default('zte-ume')),
|
||||
groupOpsInPreset: vol(z.boolean().default(true)),
|
||||
groupOpsPublic: vol(z.boolean().default(false)),
|
||||
groupTopologyInPreset: vol(z.boolean().default(false)),
|
||||
groupTopologyPublic: vol(z.boolean().default(false)),
|
||||
groupBizMonitorInPreset: vol(z.boolean().default(true)),
|
||||
groupBizMonitorPublic: vol(z.boolean().default(false)),
|
||||
kbRoot: vol(z.string().default('')),
|
||||
groupKbInPreset: vol(z.boolean().default(true)),
|
||||
groupKbPublic: vol(z.boolean().default(false)),
|
||||
})
|
||||
|
||||
/** 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 ?? ''
|
||||
}
|
||||
|
||||
/** 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.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(
|
||||
ctx: Context,
|
||||
|
|
@ -271,7 +298,47 @@ function installNetxopsSettings(
|
|||
}
|
||||
|
||||
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