diff --git a/lib/client.js b/lib/client.js index 0b6bba5..5a5cfd2 100644 --- a/lib/client.js +++ b/lib/client.js @@ -621,7 +621,10 @@ async function fetchKbStatus(call, signal) { return unconfiguredKbSnapshot(); } async function resolveKbPath(call, path, signal) { - const result = await call(NETXOPS_RPC_CHANNEL2, KB_RESOLVE_ENDPOINT, { path }, signal); + let result = await call(NETXOPS_RPC_CHANNEL2, KB_RESOLVE_ENDPOINT, { path }, signal); + if (!(result !== null && typeof result === "object" && result.ok === true)) { + result = await call(NETXOPS_RPC_CHANNEL2, KB_RESOLVE_ENDPOINT, { args: { path } }, signal); + } if (result !== null && typeof result === "object" && result.ok === true) { return asKbSnapshot(result.value); } @@ -1376,7 +1379,7 @@ function NetxopsCard(props) { /* @__PURE__ */ jsx_runtime.jsx("button", { type: "button", className: "dsh-nx-btn", - disabled: disabled || !state.kbDirectoryPickerReady, + disabled, onClick: () => { props.browseKbRoot(); }, @@ -1388,6 +1391,15 @@ function NetxopsCard(props) { className: state.kbRoot.invalid ? "dsh-nx-invalid" : "dsh-nx-hint", children: state.kbRoot.invalid ? t("invalid") : state.kbDirectoryPickerReady ? t("kbRootHint") : t("kbBrowseUnavailable") }), + /* @__PURE__ */ jsx_runtime.jsx("p", { + className: "dsh-nx-hint", + children: t("kbSaveHint") + }), + state.kbUiError ? /* @__PURE__ */ jsx_runtime.jsx("p", { + className: "dsh-nx-invalid", + role: "status", + children: fillTemplate(t("kbBrowseFailed"), { detail: state.kbUiError }) + }) : null, state.kbStatus?.status === "error" && state.kbStatus.errorMessage ? /* @__PURE__ */ jsx_runtime.jsx("p", { className: "dsh-nx-invalid", role: "status", @@ -1806,6 +1818,7 @@ class NetxopsCardController { kbDirectoryPicker; kbBrowseInFlight = false; kbStatusInFlight = false; + kbUiError = null; imDeliveryCatalog = { ...EMPTY_IM_DELIVERY_CATALOG }; sessionsExportStatus = null; sessionsExportBusy = false; @@ -2002,17 +2015,57 @@ class NetxopsCardController { } catch {} } browseKbRoot() { - const picker = this.kbDirectoryPicker; - if (picker === undefined || this.kbBrowseInFlight) + if (this.kbBrowseInFlight) return; this.kbBrowseInFlight = true; - picker.pick().then((path) => { - if (typeof path === "string" && path.trim() !== "") { - this.form.actions().edit("kbRoot", path); - this.refreshKbPreview(); + this.kbUiError = null; + this.store.set(this.projection()); + const applyPath = (path) => { + if (typeof path !== "string" || path.trim() === "") { + return; } - }).catch(() => {}).finally(() => { + this.form.actions().edit("kbRoot", path.trim()); + this.refreshKbPreview(); + }; + const fail = (error) => { + this.kbUiError = error instanceof Error ? error.message : String(error); + this.store.set(this.projection()); + }; + const picker = this.kbDirectoryPicker; + if (picker !== undefined) { + picker.pick().then(applyPath).catch(fail).finally(() => { + this.kbBrowseInFlight = false; + this.store.set(this.projection()); + }); + return; + } + const call = this.rpcCall; + if (call === undefined) { this.kbBrowseInFlight = false; + this.kbUiError = "directory picker unavailable — paste an absolute folder path and Save"; + this.store.set(this.projection()); + return; + } + call("/api", "directoryPicker/pick", { args: {} }).then((result) => { + if (typeof result === "string" || result === null) { + applyPath(result); + return; + } + if (result !== null && typeof result === "object") { + const row = result; + if (row.ok === false) { + fail(row.error?.message || "directoryPicker/pick failed"); + return; + } + if ("value" in row) { + applyPath(row.value); + return; + } + } + applyPath(result); + }).catch(fail).finally(() => { + this.kbBrowseInFlight = false; + this.store.set(this.projection()); }); } exportAllSessions() { @@ -2071,7 +2124,8 @@ class NetxopsCardController { apiTokenRemoteReady: this.credential.remoteReady, alarmPushStatus: this.alarmPushStatus, kbStatus: this.kbPreview ?? this.kbStatus, - kbDirectoryPickerReady: this.kbDirectoryPicker !== undefined, + kbDirectoryPickerReady: this.kbDirectoryPicker !== undefined || this.rpcCall !== undefined, + kbUiError: this.kbUiError, imDeliveryCatalog: this.imDeliveryCatalog, sessionsExportStatus: this.sessionsExportStatus, sessionsExportBusy: this.sessionsExportBusy, @@ -2185,7 +2239,9 @@ var en = { kbRoot: "Package root", kbRootHint: "Folder with MANIFEST.json (operator-subset v1.0). Empty = pure netx.", kbBrowse: "Browse…", - kbBrowseUnavailable: "Directory picker unavailable — paste an absolute path.", + kbBrowseUnavailable: "Directory picker unavailable — paste an absolute folder path, then Save.", + kbBrowseFailed: "Browse failed: {detail}", + kbSaveHint: "Click Save after choosing a path to apply the knowledge base.", kbStatusConfigured: "Knowledge base: {operator} ({country}) v{version}", kbStatusUnconfigured: "Knowledge base: not configured (pure netx)", kbStatusError: "Knowledge base: error — {detail}", @@ -2255,7 +2311,9 @@ var zh = { kbRoot: "知识包根目录", kbRootHint: "含 MANIFEST.json 的运营商子集包(v1.0)。留空=纯 netx。", kbBrowse: "浏览…", - kbBrowseUnavailable: "目录选择器不可用 — 请粘贴绝对路径。", + kbBrowseUnavailable: "目录选择器不可用 — 请粘贴绝对目录路径后点保存。", + kbBrowseFailed: "浏览失败:{detail}", + kbSaveHint: "选好路径后请点「保存」,知识库才会生效。", kbStatusConfigured: "知识库: {operator}({country}) v{version}", kbStatusUnconfigured: "知识库: 未配置(纯 netx)", kbStatusError: "知识库: 错误 — {detail}", @@ -2350,13 +2408,16 @@ function apply(ctx) { card.setAlarmPushRpc(undefined); }, "netxops: clear alarm-push rpc"); }); - ctx.inject(["remote.directoryPicker"], (dpCtx) => { - const picker = dpCtx.remote?.directoryPicker; - if (!picker || typeof picker.pick !== "function") { - dpCtx.logger.warn("netxops: remote.directoryPicker.pick unavailable — browse button disabled"); + 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"); diff --git a/lib/index.js b/lib/index.js index 3ea7653..dec5043 100644 --- a/lib/index.js +++ b/lib/index.js @@ -844,7 +844,7 @@ function registerKbContextSkill(ctx, snapshot) { // src/netx/kb-manifest.ts import { existsSync as existsSync2, readdirSync, readFileSync, statSync } from "node:fs"; -import { dirname as dirname2, join as join3, resolve } from "node:path"; +import { basename, dirname as dirname2, join as join3, resolve } from "node:path"; var EMPTY_CONTENT = { regions: false, theory: false, @@ -1000,11 +1000,26 @@ function resolveKbRoot(kbRoot, maxDepth = 3) { const trimmed = kbRoot.trim(); if (!trimmed) return unconfiguredKbSnapshot(); - const located = findManifest(trimmed, maxDepth); + let root = resolve(trimmed); + try { + const st = statSync(root); + if (st.isFile()) { + if (basename(root).toLowerCase() === "manifest.json") { + root = dirname2(root); + } else { + return errorSnapshot(`kbRoot is not a directory: ${root}`); + } + } else if (!st.isDirectory()) { + return errorSnapshot(`kbRoot is not a directory: ${root}`); + } + } catch { + return errorSnapshot(`kbRoot not found: ${root}`); + } + const located = findManifest(root, maxDepth); if (located.error) return errorSnapshot(located.error); if (located.paths.length === 0) { - return errorSnapshot(`no MANIFEST.json under ${resolve(trimmed)} (maxDepth=${maxDepth})`); + return errorSnapshot(`no MANIFEST.json under ${root} (maxDepth=${maxDepth})`); } if (located.paths.length > 1) { return errorSnapshot(`ambiguous MANIFEST.json (${located.paths.length} hits); pick a unique package root`); diff --git a/src/client/NetxopsCard.tsx b/src/client/NetxopsCard.tsx index de4e630..734bb44 100644 --- a/src/client/NetxopsCard.tsx +++ b/src/client/NetxopsCard.tsx @@ -521,7 +521,7 @@ export function NetxopsCard(props: NetxopsCardProps) {