From fcdd11ac59f49a63148257f51cc5b42ab8a93109 Mon Sep 17 00:00:00 2001 From: oliver Date: Sun, 13 Sep 2026 11:22:58 +0800 Subject: [PATCH] Fix kbRoot browse ignoring Typert Result from directoryPicker.pick. Unwrap { ok, value } so the chosen folder path lands in the field and auto-saves. Co-authored-by: Cursor --- lib/client.js | 79 +++++++++++++++++++++----------- src/client/controller.ts | 87 +++++++++++++++++++++++++----------- src/client/locales.ts | 8 ++-- test/kb-pick-result.test.mjs | 48 ++++++++++++++++++++ 4 files changed, 165 insertions(+), 57 deletions(-) create mode 100644 test/kb-pick-result.test.mjs diff --git a/lib/client.js b/lib/client.js index 5a5cfd2..e1a5922 100644 --- a/lib/client.js +++ b/lib/client.js @@ -2021,19 +2021,30 @@ class NetxopsCardController { this.kbUiError = null; this.store.set(this.projection()); const applyPath = (path) => { - if (typeof path !== "string" || path.trim() === "") { - return; - } - this.form.actions().edit("kbRoot", path.trim()); + this.form.actions().edit("kbRoot", path); this.refreshKbPreview(); + this.form.save().then(() => { + this.refreshKbStatus(); + this.refreshKbPreview(); + }); }; const fail = (error) => { this.kbUiError = error instanceof Error ? error.message : String(error); this.store.set(this.projection()); }; + const acceptPickResult = (result) => { + const path = unwrapDirectoryPickResult(result); + if (path === null) + return; + if (path === undefined) { + fail(`unexpected directoryPicker result: ${safeJson(result)}`); + return; + } + applyPath(path); + }; const picker = this.kbDirectoryPicker; if (picker !== undefined) { - picker.pick().then(applyPath).catch(fail).finally(() => { + picker.pick().then(acceptPickResult).catch(fail).finally(() => { this.kbBrowseInFlight = false; this.store.set(this.projection()); }); @@ -2046,24 +2057,7 @@ class NetxopsCardController { 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(() => { + call("/api", "directoryPicker/pick", { args: {} }).then(acceptPickResult).catch(fail).finally(() => { this.kbBrowseInFlight = false; this.store.set(this.projection()); }); @@ -2215,6 +2209,37 @@ function refOf(snapshot) { const declared = snapshot.value?.tokenCredentialRef; return declared !== undefined && declared.length > 0 ? declared : DEFAULT_TOKEN_REF; } +function unwrapDirectoryPickResult(result) { + if (result === null) + return null; + if (typeof result === "string") { + const trimmed = result.trim(); + return trimmed === "" ? null : trimmed; + } + if (result === null || typeof result !== "object" || Array.isArray(result)) + return; + const row = result; + if (row.ok === false) { + throw new Error(row.error?.message || "directoryPicker/pick failed"); + } + if ("value" in row) { + if (row.value === null) + return null; + if (typeof row.value === "string") { + const trimmed = row.value.trim(); + return trimmed === "" ? null : trimmed; + } + return; + } + return; +} +function safeJson(value) { + try { + return JSON.stringify(value); + } catch { + return String(value); + } +} // src/client/locales.ts var en = { @@ -2237,11 +2262,11 @@ var en = { groupInPreset: "In Netx Ops preset", groupPublic: "Publish to other presets", kbRoot: "Package root", - kbRootHint: "Folder with MANIFEST.json (operator-subset v1.0). Empty = pure netx.", + kbRootHint: "Folder with MANIFEST.json (operator-subset v1.0). Browse auto-saves.", kbBrowse: "Browse…", 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.", + kbSaveHint: "Browse writes and saves automatically. Manual paste still needs Save.", kbStatusConfigured: "Knowledge base: {operator} ({country}) v{version}", kbStatusUnconfigured: "Knowledge base: not configured (pure netx)", kbStatusError: "Knowledge base: error — {detail}", @@ -2309,11 +2334,11 @@ var zh = { groupInPreset: "在 Netx Ops 预设中启用", groupPublic: "对其他预设公开", kbRoot: "知识包根目录", - kbRootHint: "含 MANIFEST.json 的运营商子集包(v1.0)。留空=纯 netx。", + kbRootHint: "含 MANIFEST.json 的运营商子集包(v1.0)。浏览选目录后会自动保存。", kbBrowse: "浏览…", kbBrowseUnavailable: "目录选择器不可用 — 请粘贴绝对目录路径后点保存。", kbBrowseFailed: "浏览失败:{detail}", - kbSaveHint: "选好路径后请点「保存」,知识库才会生效。", + kbSaveHint: "点「浏览」选目录后会自动写入并保存;若是手粘路径仍需点保存。", kbStatusConfigured: "知识库: {operator}({country}) v{version}", kbStatusUnconfigured: "知识库: 未配置(纯 netx)", kbStatusError: "知识库: 错误 — {detail}", diff --git a/src/client/controller.ts b/src/client/controller.ts index 101812d..880f93f 100644 --- a/src/client/controller.ts +++ b/src/client/controller.ts @@ -412,13 +412,14 @@ export class NetxopsCardController { this.kbUiError = null this.store.set(this.projection()) - const applyPath = (path: unknown): void => { - if (typeof path !== 'string' || path.trim() === '') { - // Cancel / empty — no error - return - } - this.form.actions().edit('kbRoot', path.trim()) + 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() + }) } const fail = (error: unknown): void => { @@ -426,10 +427,24 @@ export class NetxopsCardController { this.store.set(this.projection()) } + /** + * `remote.directoryPicker.pick()` returns a Typert Result `{ ok, value }`, + * not a bare string (see dsh ui-workspace Navigation.pickDirectory). + */ + const acceptPickResult = (result: unknown): void => { + const path = unwrapDirectoryPickResult(result) + if (path === null) return // user cancelled + if (path === undefined) { + fail(`unexpected directoryPicker result: ${safeJson(result)}`) + return + } + applyPath(path) + } + const picker = this.kbDirectoryPicker if (picker !== undefined) { void picker.pick() - .then(applyPath) + .then(acceptPickResult) .catch(fail) .finally(() => { this.kbBrowseInFlight = false @@ -447,25 +462,7 @@ export class NetxopsCardController { return } void call('/api', 'directoryPicker/pick', { args: {} }) - .then((result) => { - // Remotes often return the path directly, or { ok, value }. - if (typeof result === 'string' || result === null) { - applyPath(result) - return - } - if (result !== null && typeof result === 'object') { - const row = result as { ok?: boolean; value?: unknown; error?: { message?: string } } - if (row.ok === false) { - fail(row.error?.message || 'directoryPicker/pick failed') - return - } - if ('value' in row) { - applyPath(row.value) - return - } - } - applyPath(result) - }) + .then(acceptPickResult) .catch(fail) .finally(() => { this.kbBrowseInFlight = false @@ -625,3 +622,41 @@ function refOf(snapshot: SettingsScopeSnapshot): string { const declared = snapshot.value?.tokenCredentialRef return declared !== undefined && declared.length > 0 ? declared : DEFAULT_TOKEN_REF } + +/** + * Normalize directoryPicker pick outcomes. + * @returns trimmed path; `null` = cancelled; `undefined` = unparseable. + */ +function unwrapDirectoryPickResult(result: unknown): string | null | undefined { + if (result === null) return null + if (typeof result === 'string') { + const trimmed = result.trim() + return trimmed === '' ? null : trimmed + } + if (result === null || typeof result !== 'object' || Array.isArray(result)) return undefined + const row = result as { + ok?: boolean + value?: unknown + error?: { message?: string } + } + if (row.ok === false) { + throw new Error(row.error?.message || 'directoryPicker/pick failed') + } + if ('value' in row) { + if (row.value === null) return null + if (typeof row.value === 'string') { + const trimmed = row.value.trim() + return trimmed === '' ? null : trimmed + } + return undefined + } + return undefined +} + +function safeJson(value: unknown): string { + try { + return JSON.stringify(value) + } catch { + return String(value) + } +} diff --git a/src/client/locales.ts b/src/client/locales.ts index 51a5464..d548d2c 100644 --- a/src/client/locales.ts +++ b/src/client/locales.ts @@ -92,11 +92,11 @@ export const en: Record = { groupInPreset: 'In Netx Ops preset', groupPublic: 'Publish to other presets', kbRoot: 'Package root', - kbRootHint: 'Folder with MANIFEST.json (operator-subset v1.0). Empty = pure netx.', + kbRootHint: 'Folder with MANIFEST.json (operator-subset v1.0). Browse auto-saves.', kbBrowse: 'Browse…', 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.', + kbSaveHint: 'Browse writes and saves automatically. Manual paste still needs Save.', kbStatusConfigured: 'Knowledge base: {operator} ({country}) v{version}', kbStatusUnconfigured: 'Knowledge base: not configured (pure netx)', kbStatusError: 'Knowledge base: error — {detail}', @@ -165,11 +165,11 @@ export const zh: Record = { groupInPreset: '在 Netx Ops 预设中启用', groupPublic: '对其他预设公开', kbRoot: '知识包根目录', - kbRootHint: '含 MANIFEST.json 的运营商子集包(v1.0)。留空=纯 netx。', + kbRootHint: '含 MANIFEST.json 的运营商子集包(v1.0)。浏览选目录后会自动保存。', kbBrowse: '浏览…', kbBrowseUnavailable: '目录选择器不可用 — 请粘贴绝对目录路径后点保存。', kbBrowseFailed: '浏览失败:{detail}', - kbSaveHint: '选好路径后请点「保存」,知识库才会生效。', + kbSaveHint: '点「浏览」选目录后会自动写入并保存;若是手粘路径仍需点保存。', kbStatusConfigured: '知识库: {operator}({country}) v{version}', kbStatusUnconfigured: '知识库: 未配置(纯 netx)', kbStatusError: '知识库: 错误 — {detail}', diff --git a/test/kb-pick-result.test.mjs b/test/kb-pick-result.test.mjs new file mode 100644 index 0000000..3c9fc57 --- /dev/null +++ b/test/kb-pick-result.test.mjs @@ -0,0 +1,48 @@ +/** + * Directory-picker Result unwrapping (mirrors controller helper). + */ + +import assert from 'node:assert/strict' +import test from 'node:test' + +function unwrapDirectoryPickResult(result) { + if (result === null) return null + if (typeof result === 'string') { + const trimmed = result.trim() + return trimmed === '' ? null : trimmed + } + if (typeof result !== 'object' || Array.isArray(result)) return undefined + const row = result + if (row.ok === false) { + throw new Error(row.error?.message || 'directoryPicker/pick failed') + } + if ('value' in row) { + if (row.value === null) return null + if (typeof row.value === 'string') { + const trimmed = row.value.trim() + return trimmed === '' ? null : trimmed + } + return undefined + } + return undefined +} + +test('unwrap bare string path', () => { + assert.equal(unwrapDirectoryPickResult('D:\\kb'), 'D:\\kb') +}) + +test('unwrap typert Result value', () => { + assert.equal(unwrapDirectoryPickResult({ ok: true, value: 'D:\\kb\\ioh' }), 'D:\\kb\\ioh') +}) + +test('unwrap cancel null', () => { + assert.equal(unwrapDirectoryPickResult(null), null) + assert.equal(unwrapDirectoryPickResult({ ok: true, value: null }), null) +}) + +test('reject Result error', () => { + assert.throws( + () => unwrapDirectoryPickResult({ ok: false, error: { message: 'no native' } }), + /no native/, + ) +})