From 1d9a8e75c9943e8e9e9e9f006f099cec7ed58cce Mon Sep 17 00:00:00 2001 From: oliver Date: Sat, 10 Oct 2026 01:44:24 +0800 Subject: [PATCH] Remove settings.plugin.item inject that crashes Desktop 0.2 boot. Align with dsh-im-ops: settings.section only, plus optional plugins.item after configForms (0.2.5). Co-authored-by: Cursor --- README.md | 3 +- lib/client.browser.js | 248 +++++++++++++++++-------------------- package.json | 2 +- test/compatibility.test.js | 5 +- 4 files changed, 116 insertions(+), 142 deletions(-) diff --git a/README.md b/README.md index baca395..aeeccee 100644 --- a/README.md +++ b/README.md @@ -98,8 +98,7 @@ RC2 的 `credentials/reference-updated` 事件会刷新设置卡片中的“已 ## DSH 0.1.1-rc.2 适配 - DSH host 依赖精确锁定为 `0.1.1-rc.2`,不使用可能落到旧版本线的子包 `latest`。 -- Settings 侧栏:`settings.section` + `id: "search-mcp"`(Desktop 0.2 主入口)。 -- 设置卡片继续使用 keyed slot:`settings.plugin.item` + `key: "search-mcp"`(Web 插件配置页)。 +- Settings 侧栏:`settings.section` + `id: "search-mcp"`(Desktop 0.2 / Web 主入口,与 dsh-im-ops 相同)。 - 新密钥通过 `credentials.set` 单向写入,凭据状态通过 `credentials.describe` 读取。 - 监听 RC2 的 `credentials/reference-updated`,外部凭据变更后刷新状态 badge。 - RC6/RC7 遗留的字面 `apiKey` 仍可由 Host 使用;涉及服务器数组的编辑会阻止不可见旧密钥被意外删除,并要求先迁移。 diff --git a/lib/client.browser.js b/lib/client.browser.js index 8859368..ffd70cd 100644 --- a/lib/client.browser.js +++ b/lib/client.browser.js @@ -1,13 +1,11 @@ /* * dsh-search-mcp — browser half. * - * Surfaces (dual-stack), matching netxops / Desktop 0.2: - * - Always register `settings.section` (Settings sidebar) — never gate the - * nav entry on configForms.whileServed (Desktop often never serves our ns). - * - Soft-attach real form scopes into a deferred memory scope so Save still - * writes the host entry when the transport is ready. - * - Also keep `settings.plugin.item` / `plugins.item` when forms attach (web - * Plugins tab / Desktop plugins card). + * Surfaces (matching dsh-im-ops / netxops on Desktop 0.2): + * - Always register `settings.section` (Settings sidebar). + * - Soft-attach real form scopes into a deferred memory scope for Save. + * - Optional `plugins.item` when configForms attaches. + * - Do NOT use `settings.plugin.item` — missing on Desktop 0.2 and kills boot. * * Form: structured (no raw JSON) — default server, max results, timeout, * quick-add presets, collapsible server rows. @@ -1247,157 +1245,135 @@ window.__ModuleLoader__.load({ } function apply(ctx) { - const t = ctx.locale.bind(NS); - ctx.effect(() => { - try { - return ctx.locale.register(NS, { zh, en }); - } catch { - const offZh = ctx.locale.register(NS, "zh", zh); - const offEn = ctx.locale.register(NS, "en", en); - return () => { offZh?.(); offEn?.(); }; - } - }, "dsh-search-mcp: section dictionaries"); - - const deferred = createDeferredScope(createMemoryScope(MEMORY_DEFAULTS)); - const remote = resolveRemote(ctx); - let controller; - try { - controller = new SearchMcpCardController(deferred, remote, noopDescribeFace()); - } catch (error) { - ctx.logger?.error?.("dsh-search-mcp: controller init failed: %s", error); - return; - } - let pluginsItemId; - let pluginCardMounted = false; - + // Match dsh-im-ops / netxops: only `settings.section` (+ optional plugins.item). + // Never inject `settings.plugin.item` — Desktop 0.2 lacks that slot and + // injecting it fails the client fiber (web boot: dsh-search-mcp: failed), + // including when configForms attaches synchronously during apply. try { + const t = ctx.locale.bind(NS); ctx.effect(() => { - if (typeof remote.$on !== "function") return () => {}; - return remote.$on("credentials/reference-updated", (ref) => { - const used = controller.baseDraft().servers.some((row) => row.apiKeyEnv.trim() === ref); - if (used) controller.readSecretState(); - }); - }, "dsh-search-mcp: credential status invalidation"); - } catch { /* optional */ } + try { + return ctx.locale.register(NS, { zh, en }); + } catch { + const offZh = ctx.locale.register(NS, "zh", zh); + const offEn = ctx.locale.register(NS, "en", en); + return () => { offZh?.(); offEn?.(); }; + } + }, "dsh-search-mcp: section dictionaries"); - /** Settings sidebar — unconditional (Desktop 0.2 primary surface). */ - try { - ctx.slots.inject("settings.section", () => ctx.slots.register({ - name: "settings.section", - id: NS, - order: 30, - label: () => t("title"), - locale: NS, - inject: () => controller.inject(), - }, SearchMcpSection)); - } catch (error) { - ctx.logger?.warn?.("dsh-search-mcp: settings.section unavailable: %s", error); - } - - /** - * Web Plugins → 插件配置 card. Do NOT register this unconditionally: - * Desktop 0.2 may lack `settings.plugin.item`, and injecting a missing - * slot fails the whole client fiber (web boot: dsh-search-mcp: failed). - * Mount only after configForms / settingsScope attaches (web path). - */ - const mountPluginCard = () => { - if (pluginCardMounted) return; - pluginCardMounted = true; + const deferred = createDeferredScope(createMemoryScope(MEMORY_DEFAULTS)); + const remote = resolveRemote(ctx); + let controller; try { - ctx.slots.inject("settings.plugin.item", function* () { - yield ctx.slots.register({ - name: "settings.plugin.item", - key: NS, - order: 30, - locale: NS, - inject: () => controller.inject(), - }, SearchMcpCard); - }); + controller = new SearchMcpCardController(deferred, remote, noopDescribeFace()); } catch (error) { - ctx.logger?.warn?.("dsh-search-mcp: settings.plugin.item unavailable: %s", error); + ctx.logger?.error?.("dsh-search-mcp: controller init failed: %s", error); + return; } - }; + let pluginsItemId; - const registerPluginsItem = (id) => { - if (pluginsItemId === id) return () => {}; try { - const off = ctx.slots.inject("plugins.item", () => ctx.slots.register({ - name: "plugins.item", - id, + ctx.effect(() => { + if (typeof remote.$on !== "function") return () => {}; + return remote.$on("credentials/reference-updated", (ref) => { + const used = controller.baseDraft().servers.some((row) => row.apiKeyEnv.trim() === ref); + if (used) controller.readSecretState(); + }); + }, "dsh-search-mcp: credential status invalidation"); + } catch { /* optional */ } + + try { + ctx.slots.inject("settings.section", () => ctx.slots.register({ + name: "settings.section", + id: NS, order: 30, label: () => t("title"), locale: NS, inject: () => controller.inject(), }, SearchMcpSection)); - pluginsItemId = id; - return () => { - if (pluginsItemId === id) pluginsItemId = undefined; - off(); - }; } catch (error) { - ctx.logger?.warn?.("dsh-search-mcp: plugins.item unavailable: %s", error); - return () => {}; + ctx.logger?.warn?.("dsh-search-mcp: settings.section unavailable: %s", error); } - }; - const tryGetForm = (forms, candidates) => { - if (typeof forms.get !== "function") return undefined; - for (const ns of candidates) { + const registerPluginsItem = (id) => { + if (pluginsItemId === id) return () => {}; try { - const scope = forms.get(ns); - if (scope && typeof scope.getSnapshot === "function") return { ns, scope }; - } catch { /* try next */ } - } - return undefined; - }; + const off = ctx.slots.inject("plugins.item", () => ctx.slots.register({ + name: "plugins.item", + id, + order: 30, + label: () => t("title"), + locale: NS, + inject: () => controller.inject(), + }, SearchMcpSection)); + pluginsItemId = id; + return () => { + if (pluginsItemId === id) pluginsItemId = undefined; + off(); + }; + } catch (error) { + ctx.logger?.warn?.("dsh-search-mcp: plugins.item unavailable: %s", error); + return () => {}; + } + }; - const attachForm = (hit, source) => { - ctx.logger?.info?.("dsh-search-mcp: attach form via %s (%s)", source, hit.ns); - deferred.attach(hit.scope); - mountPluginCard(); - const offPlugins = registerPluginsItem(hit.ns); - return () => { offPlugins(); }; - }; + const tryGetForm = (forms, candidates) => { + if (typeof forms.get !== "function") return undefined; + for (const ns of candidates) { + try { + const scope = forms.get(ns); + if (scope && typeof scope.getSnapshot === "function") return { ns, scope }; + } catch { /* try next */ } + } + return undefined; + }; - // DSH ≥0.1.7 / 0.2.0 — configForms (persist + plugins.item). - ctx.inject(["configForms"], (formsCtx) => { - const forms = formsCtx.configForms || formsCtx.get?.("configForms"); - if (!forms) { - formsCtx.logger?.warn?.("dsh-search-mcp: configForms inject fired but service missing"); - return; - } - const candidates = namespaceCandidates(formsCtx); - formsCtx.logger?.info?.("dsh-search-mcp: configForms candidates=%s", candidates.join(",")); + const attachForm = (hit, source) => { + ctx.logger?.info?.("dsh-search-mcp: attach form via %s (%s)", source, hit.ns); + deferred.attach(hit.scope); + return registerPluginsItem(hit.ns); + }; - if (typeof forms.whileServed === "function") { - formsCtx.effect(() => forms.whileServed(candidates, (served) => { - const ns = candidates.find((id) => served.has(id)); - if (!ns) return () => {}; - const hit = tryGetForm(forms, [ns, ...candidates]); - if (!hit) return () => {}; - return attachForm(hit, "whileServed"); - }), "dsh-search-mcp: configForms whileServed"); - } + ctx.inject(["configForms"], (formsCtx) => { + const forms = formsCtx.configForms || formsCtx.get?.("configForms"); + if (!forms) { + formsCtx.logger?.warn?.("dsh-search-mcp: configForms inject fired but service missing"); + return; + } + const candidates = namespaceCandidates(formsCtx); + formsCtx.logger?.info?.("dsh-search-mcp: configForms candidates=%s", candidates.join(",")); - const immediate = tryGetForm(forms, candidates); - if (immediate) { - formsCtx.effect( - () => attachForm(immediate, "eager"), - "dsh-search-mcp: configForms eager attach", - ); - } - }); + if (typeof forms.whileServed === "function") { + formsCtx.effect(() => forms.whileServed(candidates, (served) => { + const ns = candidates.find((id) => served.has(id)); + if (!ns) return () => {}; + const hit = tryGetForm(forms, [ns, ...candidates]); + if (!hit) return () => {}; + return attachForm(hit, "whileServed"); + }), "dsh-search-mcp: configForms whileServed"); + } - // DSH ≤0.1.5 — settingsScope binder. - ctx.inject(["settingsScope"], (scopeCtx) => { - const binder = scopeCtx.settingsScope || scopeCtx.get?.("settingsScope"); - if (!binder || typeof binder.bind !== "function") { - scopeCtx.logger?.warn?.("dsh-search-mcp: settingsScope present but .bind missing"); - return; - } - scopeCtx.logger?.info?.("dsh-search-mcp: attach form via settingsScope"); - deferred.attach(binder.bind({ namespace: NS })); - }); + const immediate = tryGetForm(forms, candidates); + if (immediate) { + formsCtx.effect( + () => attachForm(immediate, "eager"), + "dsh-search-mcp: configForms eager attach", + ); + } + }); + + ctx.inject(["settingsScope"], (scopeCtx) => { + const binder = scopeCtx.settingsScope || scopeCtx.get?.("settingsScope"); + if (!binder || typeof binder.bind !== "function") { + scopeCtx.logger?.warn?.("dsh-search-mcp: settingsScope present but .bind missing"); + return; + } + scopeCtx.logger?.info?.("dsh-search-mcp: attach form via settingsScope"); + deferred.attach(binder.bind({ namespace: NS })); + }); + } catch (error) { + ctx.logger?.error?.("dsh-search-mcp: apply aborted: %s", error); + } } //#endregion diff --git a/package.json b/package.json index 8f6379b..aacbea0 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "dsh-search-mcp", - "version": "0.2.4", + "version": "0.2.5", "description": "Replace dsh's built-in web search with search MCP servers (Tavily / Brave / Exa / Perplexity / DuckDuckGo / custom), configured from the web Settings page. When this plugin is enabled the built-in DeepSeek search provider is disabled.", "type": "module", "main": "lib/index.js", diff --git a/test/compatibility.test.js b/test/compatibility.test.js index 62d3b51..fe450a4 100644 --- a/test/compatibility.test.js +++ b/test/compatibility.test.js @@ -12,7 +12,7 @@ test('package exports resolve and peerDependencies stay open', async () => { assert.equal(pkg.exports['.'], './lib/index.js'); assert.equal(pkg.exports['./client'], './lib/client.browser.js'); assert.equal(pkg.engines.node, '>=20'); - assert.equal(pkg.version, '0.2.4'); + assert.equal(pkg.version, '0.2.5'); for (const name of [ '@deepseek-ai/dsh-api-remotes', @@ -35,8 +35,7 @@ test('browser half registers Settings sidebar + deferred form attach', async () assert.match(client, /SearchMcpSection/); assert.match(client, /createDeferredScope/); assert.match(client, /createMemoryScope/); - assert.match(client, /name: "settings\.plugin\.item",\s+key: NS,/); - assert.doesNotMatch(client, /name: "settings\.plugin\.item",\s+id:/); + assert.doesNotMatch(client, /settings\.plugin\.item/); assert.match(client, /name: "plugins\.item"/); assert.match(client, /configForms/); assert.match(client, /whileServed/);