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 <cursoragent@cursor.com>
This commit is contained in:
oliver 2026-10-10 01:44:24 +08:00
parent ae0353dd12
commit 1d9a8e75c9
4 changed files with 116 additions and 142 deletions

View file

@ -98,8 +98,7 @@ RC2 的 `credentials/reference-updated` 事件会刷新设置卡片中的“已
## DSH 0.1.1-rc.2 适配 ## DSH 0.1.1-rc.2 适配
- DSH host 依赖精确锁定为 `0.1.1-rc.2`,不使用可能落到旧版本线的子包 `latest`。 - DSH host 依赖精确锁定为 `0.1.1-rc.2`,不使用可能落到旧版本线的子包 `latest`。
- Settings 侧栏:`settings.section` + `id: "search-mcp"`(Desktop 0.2 主入口)。 - Settings 侧栏:`settings.section` + `id: "search-mcp"`(Desktop 0.2 / Web 主入口,与 dsh-im-ops 相同)。
- 设置卡片继续使用 keyed slot:`settings.plugin.item` + `key: "search-mcp"`(Web 插件配置页)。
- 新密钥通过 `credentials.set` 单向写入,凭据状态通过 `credentials.describe` 读取。 - 新密钥通过 `credentials.set` 单向写入,凭据状态通过 `credentials.describe` 读取。
- 监听 RC2 的 `credentials/reference-updated`,外部凭据变更后刷新状态 badge。 - 监听 RC2 的 `credentials/reference-updated`,外部凭据变更后刷新状态 badge。
- RC6/RC7 遗留的字面 `apiKey` 仍可由 Host 使用;涉及服务器数组的编辑会阻止不可见旧密钥被意外删除,并要求先迁移。 - RC6/RC7 遗留的字面 `apiKey` 仍可由 Host 使用;涉及服务器数组的编辑会阻止不可见旧密钥被意外删除,并要求先迁移。

View file

@ -1,13 +1,11 @@
/* /*
* dsh-search-mcp — browser half. * dsh-search-mcp — browser half.
* *
* Surfaces (dual-stack), matching netxops / Desktop 0.2: * Surfaces (matching dsh-im-ops / netxops on Desktop 0.2):
* - Always register `settings.section` (Settings sidebar) — never gate the * - Always register `settings.section` (Settings sidebar).
* nav entry on configForms.whileServed (Desktop often never serves our ns). * - Soft-attach real form scopes into a deferred memory scope for Save.
* - Soft-attach real form scopes into a deferred memory scope so Save still * - Optional `plugins.item` when configForms attaches.
* writes the host entry when the transport is ready. * - Do NOT use `settings.plugin.item` — missing on Desktop 0.2 and kills boot.
* - Also keep `settings.plugin.item` / `plugins.item` when forms attach (web
* Plugins tab / Desktop plugins card).
* *
* Form: structured (no raw JSON) — default server, max results, timeout, * Form: structured (no raw JSON) — default server, max results, timeout,
* quick-add presets, collapsible server rows. * quick-add presets, collapsible server rows.
@ -1247,6 +1245,11 @@ window.__ModuleLoader__.load({
} }
function apply(ctx) { function apply(ctx) {
// 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); const t = ctx.locale.bind(NS);
ctx.effect(() => { ctx.effect(() => {
try { try {
@ -1268,7 +1271,6 @@ window.__ModuleLoader__.load({
return; return;
} }
let pluginsItemId; let pluginsItemId;
let pluginCardMounted = false;
try { try {
ctx.effect(() => { ctx.effect(() => {
@ -1280,7 +1282,6 @@ window.__ModuleLoader__.load({
}, "dsh-search-mcp: credential status invalidation"); }, "dsh-search-mcp: credential status invalidation");
} catch { /* optional */ } } catch { /* optional */ }
/** Settings sidebar — unconditional (Desktop 0.2 primary surface). */
try { try {
ctx.slots.inject("settings.section", () => ctx.slots.register({ ctx.slots.inject("settings.section", () => ctx.slots.register({
name: "settings.section", name: "settings.section",
@ -1294,30 +1295,6 @@ window.__ModuleLoader__.load({
ctx.logger?.warn?.("dsh-search-mcp: settings.section unavailable: %s", 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;
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);
});
} catch (error) {
ctx.logger?.warn?.("dsh-search-mcp: settings.plugin.item unavailable: %s", error);
}
};
const registerPluginsItem = (id) => { const registerPluginsItem = (id) => {
if (pluginsItemId === id) return () => {}; if (pluginsItemId === id) return () => {};
try { try {
@ -1354,12 +1331,9 @@ window.__ModuleLoader__.load({
const attachForm = (hit, source) => { const attachForm = (hit, source) => {
ctx.logger?.info?.("dsh-search-mcp: attach form via %s (%s)", source, hit.ns); ctx.logger?.info?.("dsh-search-mcp: attach form via %s (%s)", source, hit.ns);
deferred.attach(hit.scope); deferred.attach(hit.scope);
mountPluginCard(); return registerPluginsItem(hit.ns);
const offPlugins = registerPluginsItem(hit.ns);
return () => { offPlugins(); };
}; };
// DSH ≥0.1.7 / 0.2.0 — configForms (persist + plugins.item).
ctx.inject(["configForms"], (formsCtx) => { ctx.inject(["configForms"], (formsCtx) => {
const forms = formsCtx.configForms || formsCtx.get?.("configForms"); const forms = formsCtx.configForms || formsCtx.get?.("configForms");
if (!forms) { if (!forms) {
@ -1388,7 +1362,6 @@ window.__ModuleLoader__.load({
} }
}); });
// DSH ≤0.1.5 — settingsScope binder.
ctx.inject(["settingsScope"], (scopeCtx) => { ctx.inject(["settingsScope"], (scopeCtx) => {
const binder = scopeCtx.settingsScope || scopeCtx.get?.("settingsScope"); const binder = scopeCtx.settingsScope || scopeCtx.get?.("settingsScope");
if (!binder || typeof binder.bind !== "function") { if (!binder || typeof binder.bind !== "function") {
@ -1398,6 +1371,9 @@ window.__ModuleLoader__.load({
scopeCtx.logger?.info?.("dsh-search-mcp: attach form via settingsScope"); scopeCtx.logger?.info?.("dsh-search-mcp: attach form via settingsScope");
deferred.attach(binder.bind({ namespace: NS })); deferred.attach(binder.bind({ namespace: NS }));
}); });
} catch (error) {
ctx.logger?.error?.("dsh-search-mcp: apply aborted: %s", error);
}
} }
//#endregion //#endregion

View file

@ -1,6 +1,6 @@
{ {
"name": "dsh-search-mcp", "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.", "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", "type": "module",
"main": "lib/index.js", "main": "lib/index.js",

View file

@ -12,7 +12,7 @@ test('package exports resolve and peerDependencies stay open', async () => {
assert.equal(pkg.exports['.'], './lib/index.js'); assert.equal(pkg.exports['.'], './lib/index.js');
assert.equal(pkg.exports['./client'], './lib/client.browser.js'); assert.equal(pkg.exports['./client'], './lib/client.browser.js');
assert.equal(pkg.engines.node, '>=20'); assert.equal(pkg.engines.node, '>=20');
assert.equal(pkg.version, '0.2.4'); assert.equal(pkg.version, '0.2.5');
for (const name of [ for (const name of [
'@deepseek-ai/dsh-api-remotes', '@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, /SearchMcpSection/);
assert.match(client, /createDeferredScope/); assert.match(client, /createDeferredScope/);
assert.match(client, /createMemoryScope/); assert.match(client, /createMemoryScope/);
assert.match(client, /name: "settings\.plugin\.item",\s+key: NS,/); assert.doesNotMatch(client, /settings\.plugin\.item/);
assert.doesNotMatch(client, /name: "settings\.plugin\.item",\s+id:/);
assert.match(client, /name: "plugins\.item"/); assert.match(client, /name: "plugins\.item"/);
assert.match(client, /configForms/); assert.match(client, /configForms/);
assert.match(client, /whileServed/); assert.match(client, /whileServed/);