mirror of
https://github.com/hansjone/dsh-search-mcp.git
synced 2026-10-11 01:43:18 +08:00
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:
parent
ae0353dd12
commit
1d9a8e75c9
4 changed files with 116 additions and 142 deletions
|
|
@ -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 使用;涉及服务器数组的编辑会阻止不可见旧密钥被意外删除,并要求先迁移。
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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/);
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue