mirror of
https://github.com/hansjone/netxops.git
synced 2026-10-09 04:00:46 +08:00
Fix KB settings badge stuck on unconfigured after bind (0.1.38).
Reload Host kbRoot after browse/save so the status badge tracks the live package instead of a stale snapshot. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
parent
0f31a6648a
commit
1b4c402723
9 changed files with 261 additions and 50 deletions
|
|
@ -42,7 +42,7 @@ Authoritative packaging prose lives in the workspace contract notes (`插件` /
|
|||
| Skills from pack | Registered when `configured` **and** `hasSkills` **and** the inPreset/public toggle for that plane |
|
||||
| Local FS tools | When `configured` + `paths.local` + same KB toggles: `netx__kbWriteRef` / `WriteMemory` / `WriteDraft` / `WriteSuggestion` / `UpdateLocal` / `DeleteLocal` / `ListLocal` — jail under `refs\|memories\|drafts\|suggestions`. HQ pack is read-only; evolve site product knowledge with `kbWriteRef` (per-`host_name` PROFILE) and capture experience with `kbWriteMemory` as it appears. |
|
||||
|
||||
RPC (channel `/netxops`): `kb.status` (saved snapshot), `kb.resolve` with `{ path }` (preview unsaved paths).
|
||||
RPC (channel `/netxops`): `kb.status` (published snapshot), `kb.reload` (re-resolve live `kbRoot` + republish), `kb.resolve` with `{ path }` (preview unsaved paths).
|
||||
|
||||
## Pack skill registration
|
||||
|
||||
|
|
|
|||
111
lib/client.js
111
lib/client.js
|
|
@ -591,6 +591,7 @@ function unconfiguredKbSnapshot() {
|
|||
// src/client/kb-status-view.ts
|
||||
var NETXOPS_RPC_CHANNEL2 = "/netxops";
|
||||
var KB_STATUS_ENDPOINT = "kb.status";
|
||||
var KB_RELOAD_ENDPOINT = "kb.reload";
|
||||
var KB_RESOLVE_ENDPOINT = "kb.resolve";
|
||||
function asKbSnapshot(value) {
|
||||
const empty = unconfiguredKbSnapshot();
|
||||
|
|
@ -624,22 +625,44 @@ function asKbSnapshot(value) {
|
|||
errorMessage: typeof row.errorMessage === "string" ? row.errorMessage : ""
|
||||
};
|
||||
}
|
||||
function asRpcOk(result) {
|
||||
return result !== null && typeof result === "object" && result.ok === true;
|
||||
}
|
||||
async function fetchKbStatus(call, signal) {
|
||||
const result = await call(NETXOPS_RPC_CHANNEL2, KB_STATUS_ENDPOINT, {}, signal);
|
||||
if (result !== null && typeof result === "object" && result.ok === true) {
|
||||
if (asRpcOk(result)) {
|
||||
return asKbSnapshot(result.value);
|
||||
}
|
||||
return unconfiguredKbSnapshot();
|
||||
if (result !== null && typeof result === "object" && result.ok === false) {
|
||||
const message = String(result.error?.message ?? "kb.status rpc failed");
|
||||
return { ...unconfiguredKbSnapshot(), status: "error", errorMessage: message };
|
||||
}
|
||||
return {
|
||||
...unconfiguredKbSnapshot(),
|
||||
status: "error",
|
||||
errorMessage: "kb.status unavailable"
|
||||
};
|
||||
}
|
||||
async function reloadKbStatus(call, signal) {
|
||||
const result = await call(NETXOPS_RPC_CHANNEL2, KB_RELOAD_ENDPOINT, {}, signal);
|
||||
if (asRpcOk(result)) {
|
||||
return asKbSnapshot(result.value);
|
||||
}
|
||||
return fetchKbStatus(call, signal);
|
||||
}
|
||||
async function resolveKbPath(call, path, signal) {
|
||||
let result = await call(NETXOPS_RPC_CHANNEL2, KB_RESOLVE_ENDPOINT, { path }, signal);
|
||||
if (!(result !== null && typeof result === "object" && result.ok === true)) {
|
||||
if (!asRpcOk(result)) {
|
||||
result = await call(NETXOPS_RPC_CHANNEL2, KB_RESOLVE_ENDPOINT, { args: { path } }, signal);
|
||||
}
|
||||
if (result !== null && typeof result === "object" && result.ok === true) {
|
||||
if (asRpcOk(result)) {
|
||||
return asKbSnapshot(result.value);
|
||||
}
|
||||
return unconfiguredKbSnapshot();
|
||||
return {
|
||||
...unconfiguredKbSnapshot(),
|
||||
status: "error",
|
||||
errorMessage: "kb.resolve unavailable"
|
||||
};
|
||||
}
|
||||
function kbStatusTone(status) {
|
||||
if (status === "configured")
|
||||
|
|
@ -1886,7 +1909,7 @@ class CardForm {
|
|||
}
|
||||
async store(field, value) {
|
||||
await this.scope.set(field, value);
|
||||
return this.userLayer()?.[field] === value;
|
||||
return this.sectionValue(field) === value || this.userLayer()?.[field] === value;
|
||||
}
|
||||
stage(field, edit) {
|
||||
this.staged.set(field, edit);
|
||||
|
|
@ -1945,7 +1968,9 @@ class NetxopsCardController {
|
|||
kbDirectoryPicker;
|
||||
kbBrowseInFlight = false;
|
||||
kbStatusInFlight = false;
|
||||
kbReloadInFlight = false;
|
||||
kbUiError = null;
|
||||
lastSyncedKbRoot = undefined;
|
||||
imDeliveryCatalog = { ...EMPTY_IM_DELIVERY_CATALOG };
|
||||
sessionsExportStatus = null;
|
||||
sessionsExportBusy = false;
|
||||
|
|
@ -1983,6 +2008,7 @@ class NetxopsCardController {
|
|||
scope.subscribe(() => {
|
||||
this.readCredential();
|
||||
this.refreshKbPreview();
|
||||
this.syncKbRootFromSettings();
|
||||
});
|
||||
this.readCredential();
|
||||
}
|
||||
|
|
@ -2026,7 +2052,7 @@ class NetxopsCardController {
|
|||
}
|
||||
this.startStatusPoll();
|
||||
this.refreshAlarmPushStatus();
|
||||
this.refreshKbStatus();
|
||||
this.reloadKbFromHost();
|
||||
this.refreshKbPreview();
|
||||
this.refreshImDeliveryCatalog();
|
||||
this.refreshSessionsExportStatus();
|
||||
|
|
@ -2114,15 +2140,42 @@ class NetxopsCardController {
|
|||
this.kbStatusInFlight = true;
|
||||
try {
|
||||
const next = await fetchKbStatus(call);
|
||||
const prev = this.kbStatus;
|
||||
if (prev && prev.status === next.status && prev.realRoot === next.realRoot && prev.operatorName === next.operatorName && prev.country === next.country && prev.version === next.version && prev.errorMessage === next.errorMessage)
|
||||
return;
|
||||
this.kbStatus = next;
|
||||
this.store.set(this.projection());
|
||||
this.applyKbSnapshot(next);
|
||||
} catch {} finally {
|
||||
this.kbStatusInFlight = false;
|
||||
}
|
||||
}
|
||||
async reloadKbFromHost() {
|
||||
const call = this.rpcCall;
|
||||
if (call === undefined || this.kbReloadInFlight)
|
||||
return;
|
||||
this.kbReloadInFlight = true;
|
||||
try {
|
||||
const next = await reloadKbStatus(call);
|
||||
this.applyKbSnapshot(next);
|
||||
this.lastSyncedKbRoot = (this.scope.getSnapshot().value?.kbRoot ?? "").trim();
|
||||
} catch {
|
||||
this.refreshKbStatus();
|
||||
} finally {
|
||||
this.kbReloadInFlight = false;
|
||||
}
|
||||
}
|
||||
applyKbSnapshot(next) {
|
||||
const prev = this.kbStatus;
|
||||
if (prev && prev.status === next.status && prev.realRoot === next.realRoot && prev.operatorName === next.operatorName && prev.country === next.country && prev.version === next.version && prev.errorMessage === next.errorMessage)
|
||||
return;
|
||||
this.kbStatus = next;
|
||||
this.store.set(this.projection());
|
||||
}
|
||||
async syncKbRootFromSettings() {
|
||||
if (this.rpcCall === undefined)
|
||||
return;
|
||||
const saved = (this.scope.getSnapshot().value?.kbRoot ?? "").trim();
|
||||
if (saved === this.lastSyncedKbRoot)
|
||||
return;
|
||||
this.lastSyncedKbRoot = saved;
|
||||
await this.reloadKbFromHost();
|
||||
}
|
||||
async refreshKbPreview() {
|
||||
const call = this.rpcCall;
|
||||
if (call === undefined)
|
||||
|
|
@ -2145,6 +2198,30 @@ class NetxopsCardController {
|
|||
this.store.set(this.projection());
|
||||
} catch {}
|
||||
}
|
||||
async commitKbRoot(path) {
|
||||
const trimmed = path.trim();
|
||||
this.kbUiError = null;
|
||||
this.store.set(this.projection());
|
||||
await this.form.save();
|
||||
const shell = this.form.shell();
|
||||
const saved = (this.scope.getSnapshot().value?.kbRoot ?? "").trim();
|
||||
if (shell.failed || saved !== trimmed) {
|
||||
this.kbUiError = saved === trimmed ? "kbRoot save failed" : `kbRoot save did not land (saved="${saved || "(empty)"}")`;
|
||||
this.store.set(this.projection());
|
||||
const call = this.rpcCall;
|
||||
if (call !== undefined && trimmed !== "") {
|
||||
try {
|
||||
this.kbPreview = await resolveKbPath(call, trimmed);
|
||||
this.store.set(this.projection());
|
||||
} catch {}
|
||||
}
|
||||
return;
|
||||
}
|
||||
this.kbPreview = null;
|
||||
this.lastSyncedKbRoot = undefined;
|
||||
await this.reloadKbFromHost();
|
||||
this.refreshKbPreview();
|
||||
}
|
||||
browseKbRoot() {
|
||||
if (this.kbBrowseInFlight)
|
||||
return;
|
||||
|
|
@ -2153,11 +2230,7 @@ class NetxopsCardController {
|
|||
this.store.set(this.projection());
|
||||
const applyPath = (path) => {
|
||||
this.form.actions().edit("kbRoot", path);
|
||||
this.refreshKbPreview();
|
||||
this.form.save().then(() => {
|
||||
this.refreshKbStatus();
|
||||
this.refreshKbPreview();
|
||||
});
|
||||
this.commitKbRoot(path);
|
||||
};
|
||||
const fail = (error) => {
|
||||
this.kbUiError = error instanceof Error ? error.message : String(error);
|
||||
|
|
@ -2406,7 +2479,7 @@ var en = {
|
|||
kbBrowse: "Browse…",
|
||||
kbBrowseUnavailable: "Directory picker unavailable — paste an absolute folder path, then Save.",
|
||||
kbBrowseFailed: "Browse failed: {detail}",
|
||||
kbSaveHint: "Browse writes and saves automatically. Manual paste still needs Save.",
|
||||
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}",
|
||||
|
|
@ -2483,7 +2556,7 @@ var zh = {
|
|||
kbBrowse: "浏览…",
|
||||
kbBrowseUnavailable: "目录选择器不可用 — 请粘贴绝对目录路径后点保存。",
|
||||
kbBrowseFailed: "浏览失败:{detail}",
|
||||
kbSaveHint: "点「浏览」选目录后会自动写入并保存;若是手粘路径仍需点保存。",
|
||||
kbSaveHint: "点「浏览」选目录后会自动写入、保存,并刷新宿主知识库状态;若是手粘路径仍需点保存。",
|
||||
kbStatusConfigured: "知识库: {operator}({country}) v{version}",
|
||||
kbStatusUnconfigured: "知识库: 未配置(纯 netx)",
|
||||
kbStatusError: "知识库: 错误 — {detail}",
|
||||
|
|
|
|||
14
lib/index.js
14
lib/index.js
|
|
@ -4878,6 +4878,20 @@ function apply(ctx, config = Config({})) {
|
|||
if (endpoint === "kb.status") {
|
||||
return { ok: true, value: getKbContext() };
|
||||
}
|
||||
if (endpoint === "kb.reload") {
|
||||
const current = source();
|
||||
const kb = resolveKbRoot(current.kbRoot ?? "");
|
||||
publishKbContext(kb);
|
||||
applyKbEnv(kb);
|
||||
if (kb.status === "configured") {
|
||||
connCtx.logger.info("netxops: kb.reload → %s (%s / %s v%s)", kb.realRoot, kb.operatorName, kb.country, kb.version);
|
||||
} else if (kb.status === "error") {
|
||||
connCtx.logger.warn("netxops: kb.reload error — %s", kb.errorMessage);
|
||||
} else {
|
||||
connCtx.logger.info("netxops: kb.reload → unconfigured (kbRoot empty)");
|
||||
}
|
||||
return { ok: true, value: kb };
|
||||
}
|
||||
if (endpoint === "kb.resolve") {
|
||||
const path = extractKbResolvePath(payload);
|
||||
return { ok: true, value: resolveKbRoot(path) };
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "dsh-netxops",
|
||||
"version": "0.1.37",
|
||||
"version": "0.1.38",
|
||||
"description": "DeepSeek Harness Netx Ops: ops/topology + IM delivery + session export + knowledge-base MANIFEST/_skills + localSkills",
|
||||
"license": "MIT",
|
||||
"type": "module",
|
||||
|
|
|
|||
|
|
@ -218,7 +218,9 @@ export class CardForm<T> {
|
|||
|
||||
private async store(field: string, value: unknown): Promise<boolean> {
|
||||
await this.scope.set(field, value)
|
||||
return this.userLayer()?.[field] === value
|
||||
// `settings.mutate` can settle without throwing on a refused write; confirm
|
||||
// via the resolved section value (user layer alone can lag or omit keys).
|
||||
return this.sectionValue(field) === value || this.userLayer()?.[field] === value
|
||||
}
|
||||
|
||||
private stage(field: string, edit: StagedEdit): void {
|
||||
|
|
|
|||
|
|
@ -17,6 +17,7 @@ import {
|
|||
} from './alarm-push-status-view.ts'
|
||||
import {
|
||||
fetchKbStatus,
|
||||
reloadKbStatus,
|
||||
resolveKbPath,
|
||||
type KbSnapshot,
|
||||
} from './kb-status-view.ts'
|
||||
|
|
@ -155,7 +156,9 @@ export class NetxopsCardController {
|
|||
private kbDirectoryPicker: DirectoryPickerRemote | undefined
|
||||
private kbBrowseInFlight = false
|
||||
private kbStatusInFlight = false
|
||||
private kbReloadInFlight = false
|
||||
private kbUiError: string | null = null
|
||||
private lastSyncedKbRoot: string | undefined = undefined
|
||||
private imDeliveryCatalog: ImDeliveryCatalog = { ...EMPTY_IM_DELIVERY_CATALOG }
|
||||
private sessionsExportStatus: SessionsExportStatus | null = null
|
||||
private sessionsExportBusy = false
|
||||
|
|
@ -199,6 +202,7 @@ export class NetxopsCardController {
|
|||
scope.subscribe(() => {
|
||||
void this.readCredential()
|
||||
void this.refreshKbPreview()
|
||||
void this.syncKbRootFromSettings()
|
||||
})
|
||||
void this.readCredential()
|
||||
}
|
||||
|
|
@ -246,7 +250,7 @@ export class NetxopsCardController {
|
|||
}
|
||||
this.startStatusPoll()
|
||||
void this.refreshAlarmPushStatus()
|
||||
void this.refreshKbStatus()
|
||||
void this.reloadKbFromHost()
|
||||
void this.refreshKbPreview()
|
||||
void this.refreshImDeliveryCatalog()
|
||||
void this.refreshSessionsExportStatus()
|
||||
|
|
@ -364,18 +368,7 @@ export class NetxopsCardController {
|
|||
this.kbStatusInFlight = true
|
||||
try {
|
||||
const next = await fetchKbStatus(call)
|
||||
const prev = this.kbStatus
|
||||
if (
|
||||
prev
|
||||
&& prev.status === next.status
|
||||
&& prev.realRoot === next.realRoot
|
||||
&& prev.operatorName === next.operatorName
|
||||
&& prev.country === next.country
|
||||
&& prev.version === next.version
|
||||
&& prev.errorMessage === next.errorMessage
|
||||
) return
|
||||
this.kbStatus = next
|
||||
this.store.set(this.projection())
|
||||
this.applyKbSnapshot(next)
|
||||
} catch {
|
||||
// Keep last good snapshot; next poll retries.
|
||||
} finally {
|
||||
|
|
@ -383,6 +376,50 @@ export class NetxopsCardController {
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Ask Host to re-resolve kbRoot from live settings and republish.
|
||||
* Prefer this after browse/save so the badge cannot stick on a stale snapshot.
|
||||
*/
|
||||
private async reloadKbFromHost(): Promise<void> {
|
||||
const call = this.rpcCall
|
||||
if (call === undefined || this.kbReloadInFlight) return
|
||||
this.kbReloadInFlight = true
|
||||
try {
|
||||
const next = await reloadKbStatus(call)
|
||||
this.applyKbSnapshot(next)
|
||||
this.lastSyncedKbRoot = (this.scope.getSnapshot().value?.kbRoot ?? '').trim()
|
||||
} catch {
|
||||
// Fall back to published status poll.
|
||||
void this.refreshKbStatus()
|
||||
} finally {
|
||||
this.kbReloadInFlight = false
|
||||
}
|
||||
}
|
||||
|
||||
private applyKbSnapshot(next: KbSnapshot): void {
|
||||
const prev = this.kbStatus
|
||||
if (
|
||||
prev
|
||||
&& prev.status === next.status
|
||||
&& prev.realRoot === next.realRoot
|
||||
&& prev.operatorName === next.operatorName
|
||||
&& prev.country === next.country
|
||||
&& prev.version === next.version
|
||||
&& prev.errorMessage === next.errorMessage
|
||||
) return
|
||||
this.kbStatus = next
|
||||
this.store.set(this.projection())
|
||||
}
|
||||
|
||||
/** When settings document kbRoot changes, force Host reload (covers footer Save / Reset). */
|
||||
private async syncKbRootFromSettings(): Promise<void> {
|
||||
if (this.rpcCall === undefined) return
|
||||
const saved = (this.scope.getSnapshot().value?.kbRoot ?? '').trim()
|
||||
if (saved === this.lastSyncedKbRoot) return
|
||||
this.lastSyncedKbRoot = saved
|
||||
await this.reloadKbFromHost()
|
||||
}
|
||||
|
||||
private async refreshKbPreview(): Promise<void> {
|
||||
const call = this.rpcCall
|
||||
if (call === undefined) return
|
||||
|
|
@ -416,6 +453,38 @@ export class NetxopsCardController {
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Persist kbRoot then force Host reload so the badge matches the bound package.
|
||||
*/
|
||||
private async commitKbRoot(path: string): Promise<void> {
|
||||
const trimmed = path.trim()
|
||||
this.kbUiError = null
|
||||
this.store.set(this.projection())
|
||||
await this.form.save()
|
||||
const shell = this.form.shell()
|
||||
const saved = (this.scope.getSnapshot().value?.kbRoot ?? '').trim()
|
||||
if (shell.failed || saved !== trimmed) {
|
||||
this.kbUiError = saved === trimmed
|
||||
? 'kbRoot save failed'
|
||||
: `kbRoot save did not land (saved="${saved || '(empty)'}")`
|
||||
this.store.set(this.projection())
|
||||
const call = this.rpcCall
|
||||
if (call !== undefined && trimmed !== '') {
|
||||
try {
|
||||
this.kbPreview = await resolveKbPath(call, trimmed)
|
||||
this.store.set(this.projection())
|
||||
} catch {
|
||||
// keep error banner
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
this.kbPreview = null
|
||||
this.lastSyncedKbRoot = undefined
|
||||
await this.reloadKbFromHost()
|
||||
void this.refreshKbPreview()
|
||||
}
|
||||
|
||||
/**
|
||||
* Open the Host OS directory chooser (or /api directoryPicker/pick fallback)
|
||||
* and write the path into kbRoot.
|
||||
|
|
@ -428,12 +497,7 @@ export class NetxopsCardController {
|
|||
|
||||
const applyPath = (path: string): void => {
|
||||
this.form.actions().edit('kbRoot', path)
|
||||
void this.refreshKbPreview()
|
||||
// Selecting a folder should land immediately — don't require a separate Save.
|
||||
void this.form.save().then(() => {
|
||||
void this.refreshKbStatus()
|
||||
void this.refreshKbPreview()
|
||||
})
|
||||
void this.commitKbRoot(path)
|
||||
}
|
||||
|
||||
const fail = (error: unknown): void => {
|
||||
|
|
|
|||
|
|
@ -10,6 +10,7 @@ import type { AlarmPushRpcCall } from './alarm-push-status-view.ts'
|
|||
export const NETXOPS_RPC_CHANNEL = '/netxops'
|
||||
|
||||
export const KB_STATUS_ENDPOINT = 'kb.status'
|
||||
export const KB_RELOAD_ENDPOINT = 'kb.reload'
|
||||
export const KB_RESOLVE_ENDPOINT = 'kb.resolve'
|
||||
|
||||
export type { KbSnapshot, KbStatus }
|
||||
|
|
@ -50,6 +51,10 @@ export function asKbSnapshot(value: unknown): KbSnapshot {
|
|||
}
|
||||
}
|
||||
|
||||
function asRpcOk(result: unknown): result is { ok: true; value?: unknown } {
|
||||
return result !== null && typeof result === 'object' && (result as { ok?: boolean }).ok === true
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch the published KB snapshot from the Host.
|
||||
*/
|
||||
|
|
@ -58,10 +63,37 @@ export async function fetchKbStatus(
|
|||
signal?: AbortSignal,
|
||||
): Promise<KbSnapshot> {
|
||||
const result = await call(NETXOPS_RPC_CHANNEL, KB_STATUS_ENDPOINT, {}, signal)
|
||||
if (result !== null && typeof result === 'object' && (result as { ok?: boolean }).ok === true) {
|
||||
return asKbSnapshot((result as { value?: unknown }).value)
|
||||
if (asRpcOk(result)) {
|
||||
return asKbSnapshot(result.value)
|
||||
}
|
||||
return unconfiguredKbSnapshot()
|
||||
// Do not pretend "unconfigured" on transport/shape miss — keep empty identity
|
||||
// but mark error so the badge is not stuck on the soft "pure netx" copy.
|
||||
if (result !== null && typeof result === 'object' && (result as { ok?: boolean }).ok === false) {
|
||||
const message = String(
|
||||
(result as { error?: { message?: string } }).error?.message ?? 'kb.status rpc failed',
|
||||
)
|
||||
return { ...unconfiguredKbSnapshot(), status: 'error', errorMessage: message }
|
||||
}
|
||||
return {
|
||||
...unconfiguredKbSnapshot(),
|
||||
status: 'error',
|
||||
errorMessage: 'kb.status unavailable',
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Force Host to re-resolve `kbRoot` from live settings and republish.
|
||||
*/
|
||||
export async function reloadKbStatus(
|
||||
call: AlarmPushRpcCall,
|
||||
signal?: AbortSignal,
|
||||
): Promise<KbSnapshot> {
|
||||
const result = await call(NETXOPS_RPC_CHANNEL, KB_RELOAD_ENDPOINT, {}, signal)
|
||||
if (asRpcOk(result)) {
|
||||
return asKbSnapshot(result.value)
|
||||
}
|
||||
// Older hosts without kb.reload — fall back to status / resolve.
|
||||
return fetchKbStatus(call, signal)
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -74,13 +106,17 @@ export async function resolveKbPath(
|
|||
): Promise<KbSnapshot> {
|
||||
// Prefer flat `{ path }` (netxops channel); also try `{ args: { path } }`.
|
||||
let result = await call(NETXOPS_RPC_CHANNEL, KB_RESOLVE_ENDPOINT, { path }, signal)
|
||||
if (!(result !== null && typeof result === 'object' && (result as { ok?: boolean }).ok === true)) {
|
||||
if (!asRpcOk(result)) {
|
||||
result = await call(NETXOPS_RPC_CHANNEL, KB_RESOLVE_ENDPOINT, { args: { path } }, signal)
|
||||
}
|
||||
if (result !== null && typeof result === 'object' && (result as { ok?: boolean }).ok === true) {
|
||||
return asKbSnapshot((result as { value?: unknown }).value)
|
||||
if (asRpcOk(result)) {
|
||||
return asKbSnapshot(result.value)
|
||||
}
|
||||
return {
|
||||
...unconfiguredKbSnapshot(),
|
||||
status: 'error',
|
||||
errorMessage: 'kb.resolve unavailable',
|
||||
}
|
||||
return unconfiguredKbSnapshot()
|
||||
}
|
||||
|
||||
/** Badge tone for the settings card. */
|
||||
|
|
|
|||
|
|
@ -106,7 +106,7 @@ export const en: Record<NetxopsLocaleKey, string> = {
|
|||
kbBrowse: 'Browse…',
|
||||
kbBrowseUnavailable: 'Directory picker unavailable — paste an absolute folder path, then Save.',
|
||||
kbBrowseFailed: 'Browse failed: {detail}',
|
||||
kbSaveHint: 'Browse writes and saves automatically. Manual paste still needs Save.',
|
||||
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}',
|
||||
|
|
@ -184,7 +184,7 @@ export const zh: Record<NetxopsLocaleKey, string> = {
|
|||
kbBrowse: '浏览…',
|
||||
kbBrowseUnavailable: '目录选择器不可用 — 请粘贴绝对目录路径后点保存。',
|
||||
kbBrowseFailed: '浏览失败:{detail}',
|
||||
kbSaveHint: '点「浏览」选目录后会自动写入并保存;若是手粘路径仍需点保存。',
|
||||
kbSaveHint: '点「浏览」选目录后会自动写入、保存,并刷新宿主知识库状态;若是手粘路径仍需点保存。',
|
||||
kbStatusConfigured: '知识库: {operator}({country}) v{version}',
|
||||
kbStatusUnconfigured: '知识库: 未配置(纯 netx)',
|
||||
kbStatusError: '知识库: 错误 — {detail}',
|
||||
|
|
|
|||
22
src/index.ts
22
src/index.ts
|
|
@ -624,6 +624,28 @@ export function apply(ctx: Context, config: Config = Config({})): void {
|
|||
if (endpoint === 'kb.status') {
|
||||
return { ok: true, value: getKbContext() }
|
||||
}
|
||||
if (endpoint === 'kb.reload') {
|
||||
// Re-read live settings and publish — used by the settings card after
|
||||
// browse/save so the badge does not stick on a stale unconfigured snapshot.
|
||||
const current = source()
|
||||
const kb = resolveKbRoot(current.kbRoot ?? '')
|
||||
publishKbContext(kb)
|
||||
applyKbEnv(kb)
|
||||
if (kb.status === 'configured') {
|
||||
connCtx.logger.info(
|
||||
'netxops: kb.reload → %s (%s / %s v%s)',
|
||||
kb.realRoot,
|
||||
kb.operatorName,
|
||||
kb.country,
|
||||
kb.version,
|
||||
)
|
||||
} else if (kb.status === 'error') {
|
||||
connCtx.logger.warn('netxops: kb.reload error — %s', kb.errorMessage)
|
||||
} else {
|
||||
connCtx.logger.info('netxops: kb.reload → unconfigured (kbRoot empty)')
|
||||
}
|
||||
return { ok: true, value: kb }
|
||||
}
|
||||
if (endpoint === 'kb.resolve') {
|
||||
const path = extractKbResolvePath(payload)
|
||||
return { ok: true, value: resolveKbRoot(path) }
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue