mirror of
https://github.com/hansjone/dsh-search-mcp.git
synced 2026-10-17 21:43:26 +08:00
Soft-fail Desktop 0.2 settings primitives for search-mcp.
Avoid hard settingsScope/store requires and inject ui-slots so the plugin loads on Desktop without breaking boot. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
parent
d5c4d2006c
commit
743e894598
3 changed files with 213 additions and 22 deletions
|
|
@ -24,10 +24,80 @@ window.__ModuleLoader__.load({
|
||||||
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
||||||
let react_jsx_runtime = require("react/jsx-runtime");
|
let react_jsx_runtime = require("react/jsx-runtime");
|
||||||
let react = require("react");
|
let react = require("react");
|
||||||
let _deepseek_ai_dsh_client_ui_primitives = require("@deepseek-ai/dsh-client-ui-primitives");
|
// Desktop 0.2 may not seed ui-primitives / dsh-client-store in the ModuleLoader
|
||||||
// 0.1.2+: createSnapshotStore lives on the platform seed @deepseek-ai/dsh-client-store
|
// table. Soft-require + local fallbacks so import never fails web boot.
|
||||||
// (old @deepseek-ai/dsh-client-runtime/client is gone from the module table).
|
let _deepseek_ai_dsh_client_ui_primitives = {};
|
||||||
let _deepseek_ai_dsh_client_runtime_client = require("@deepseek-ai/dsh-client-store");
|
try {
|
||||||
|
_deepseek_ai_dsh_client_ui_primitives = require("@deepseek-ai/dsh-client-ui-primitives");
|
||||||
|
} catch {
|
||||||
|
_deepseek_ai_dsh_client_ui_primitives = {};
|
||||||
|
}
|
||||||
|
if (
|
||||||
|
_deepseek_ai_dsh_client_ui_primitives
|
||||||
|
&& _deepseek_ai_dsh_client_ui_primitives.default
|
||||||
|
&& typeof _deepseek_ai_dsh_client_ui_primitives.default === "object"
|
||||||
|
) {
|
||||||
|
_deepseek_ai_dsh_client_ui_primitives = {
|
||||||
|
..._deepseek_ai_dsh_client_ui_primitives.default,
|
||||||
|
..._deepseek_ai_dsh_client_ui_primitives,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
if (typeof _deepseek_ai_dsh_client_ui_primitives.IconChevronDownOutline14 !== "function") {
|
||||||
|
_deepseek_ai_dsh_client_ui_primitives.IconChevronDownOutline14 = function IconChevronDownOutline14(props) {
|
||||||
|
return (0, react_jsx_runtime.jsx)("svg", {
|
||||||
|
viewBox: "0 0 14 14",
|
||||||
|
width: "14",
|
||||||
|
height: "14",
|
||||||
|
"aria-hidden": "true",
|
||||||
|
className: props && props.className,
|
||||||
|
children: (0, react_jsx_runtime.jsx)("path", {
|
||||||
|
d: "M3.5 5.25L7 8.75L10.5 5.25",
|
||||||
|
fill: "none",
|
||||||
|
stroke: "currentColor",
|
||||||
|
strokeWidth: "1.5",
|
||||||
|
strokeLinecap: "round",
|
||||||
|
strokeLinejoin: "round",
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
};
|
||||||
|
}
|
||||||
|
function createSnapshotStoreFallback(initial) {
|
||||||
|
let state = initial;
|
||||||
|
const listeners = new Set();
|
||||||
|
return {
|
||||||
|
getSnapshot() {
|
||||||
|
return state;
|
||||||
|
},
|
||||||
|
subscribe(listener) {
|
||||||
|
listeners.add(listener);
|
||||||
|
return () => listeners.delete(listener);
|
||||||
|
},
|
||||||
|
set(next) {
|
||||||
|
state = next;
|
||||||
|
for (const listener of listeners) {
|
||||||
|
try { listener(); } catch { /* ignore */ }
|
||||||
|
}
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
let createSnapshotStore = createSnapshotStoreFallback;
|
||||||
|
try {
|
||||||
|
const storeMod = require("@deepseek-ai/dsh-client-store");
|
||||||
|
if (typeof storeMod?.createSnapshotStore === "function") {
|
||||||
|
createSnapshotStore = storeMod.createSnapshotStore;
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
try {
|
||||||
|
const runtimeMod = require("@deepseek-ai/dsh-client-runtime");
|
||||||
|
const nested = runtimeMod?.client || runtimeMod;
|
||||||
|
if (typeof nested?.createSnapshotStore === "function") {
|
||||||
|
createSnapshotStore = nested.createSnapshotStore;
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
// keep fallback
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const _deepseek_ai_dsh_client_runtime_client = { createSnapshotStore };
|
||||||
|
|
||||||
//#region styles
|
//#region styles
|
||||||
const css = [
|
const css = [
|
||||||
|
|
@ -1043,30 +1113,113 @@ window.__ModuleLoader__.load({
|
||||||
//#endregion
|
//#endregion
|
||||||
|
|
||||||
//#region apply
|
//#region apply
|
||||||
/** Required client services (resolved by the client kernel). */
|
/**
|
||||||
const inject = ["slots", "locale", "settingsScope", "remote", "remote.credentials", "remote.settings"];
|
* Hard-inject only services present on DSH ≤0.1.5 and ≥0.1.7 / 0.2.0.
|
||||||
|
* Do NOT hard-inject settingsScope — Desktop 0.2 removed it (configForms).
|
||||||
|
*/
|
||||||
|
const inject = ["slots", "locale"];
|
||||||
|
|
||||||
|
function noopDescribeFace() {
|
||||||
|
return {
|
||||||
|
load: async () => {},
|
||||||
|
ensure: async () => {},
|
||||||
|
acceptView: () => {},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
function apply(ctx) {
|
function resolveRemote(ctx) {
|
||||||
const t = ctx.locale.bind(NS);
|
return ctx.remote || ctx.get?.("remote") || {
|
||||||
ctx.effect(() => ctx.locale.register(NS, { zh, en }), "dsh-search-mcp: section dictionaries");
|
credentials: { set: async () => ({ ok: false }), describe: async () => ({ ok: false }) },
|
||||||
const describeFace = ctx.settingsScope.describe();
|
settings: { mutate: async () => ({ ok: false }), describe: async () => ({ ok: false }) },
|
||||||
const controller = new SearchMcpCardController(ctx.settingsScope.bind({ namespace: NS }), ctx.remote, describeFace);
|
$on: () => () => {},
|
||||||
ctx.effect(() => ctx.remote.$on("credentials/reference-updated", (ref) => {
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function mountSearchCard(ctx, scope, describeFace) {
|
||||||
|
const remote = resolveRemote(ctx);
|
||||||
|
const controller = new SearchMcpCardController(scope, remote, describeFace || noopDescribeFace());
|
||||||
|
try {
|
||||||
|
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);
|
const used = controller.baseDraft().servers.some((row) => row.apiKeyEnv.trim() === ref);
|
||||||
if (used) controller.readSecretState();
|
if (used) controller.readSecretState();
|
||||||
}), "dsh-search-mcp: credential status invalidation");
|
});
|
||||||
|
}, "dsh-search-mcp: credential status invalidation");
|
||||||
|
} catch { /* optional */ }
|
||||||
|
try {
|
||||||
ctx.slots.inject("settings.plugin.item", function* () {
|
ctx.slots.inject("settings.plugin.item", function* () {
|
||||||
yield ctx.slots.register({
|
yield ctx.slots.register({
|
||||||
name: "settings.plugin.item",
|
name: "settings.plugin.item",
|
||||||
key: NS,
|
key: NS,
|
||||||
order: 30,
|
order: 30,
|
||||||
|
locale: NS,
|
||||||
|
inject: () => controller.inject(),
|
||||||
|
}, SearchMcpCard);
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
ctx.logger?.warn?.("dsh-search-mcp: settings.plugin.item unavailable: %s", error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
locale: NS,
|
function apply(ctx) {
|
||||||
inject: () => controller.inject()
|
const t = ctx.locale.bind(NS);
|
||||||
}, SearchMcpCard);
|
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");
|
||||||
|
|
||||||
|
let mounted = false;
|
||||||
|
const tryMount = (scope, describeFace, source) => {
|
||||||
|
if (mounted || !scope || typeof scope.getSnapshot !== "function") return;
|
||||||
|
mounted = true;
|
||||||
|
ctx.logger?.info?.("dsh-search-mcp: settings card via %s", source);
|
||||||
|
mountSearchCard(ctx, scope, describeFace);
|
||||||
|
};
|
||||||
|
|
||||||
|
// DSH ≥0.1.7 / 0.2.0 — configForms (settingsScope removed).
|
||||||
|
ctx.inject(["configForms"], (formsCtx) => {
|
||||||
|
const forms = formsCtx.configForms || formsCtx.get?.("configForms");
|
||||||
|
if (!forms || typeof forms.get !== "function") {
|
||||||
|
formsCtx.logger?.warn?.("dsh-search-mcp: configForms present but .get missing");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const candidates = [NS, "dsh-search-mcp", "search-mcp"];
|
||||||
|
const hit = candidates.map((id) => {
|
||||||
|
try { return { id, scope: forms.get(id) }; } catch { return null; }
|
||||||
|
}).find((row) => row?.scope && typeof row.scope.getSnapshot === "function");
|
||||||
|
if (hit) tryMount(hit.scope, noopDescribeFace(), `configForms:${hit.id}`);
|
||||||
|
if (typeof forms.whileServed === "function") {
|
||||||
|
formsCtx.effect(() => forms.whileServed(candidates, (served) => {
|
||||||
|
const id = candidates.find((c) => served.has(c));
|
||||||
|
if (!id) return () => {};
|
||||||
|
try {
|
||||||
|
const scope = forms.get(id);
|
||||||
|
tryMount(scope, noopDescribeFace(), `whileServed:${id}`);
|
||||||
|
} catch { /* ignore */ }
|
||||||
|
return () => {};
|
||||||
|
}), "dsh-search-mcp: configForms whileServed");
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// DSH ≤0.1.5 — settingsScope binder.
|
||||||
|
ctx.inject(["settingsScope"], (scopeCtx) => {
|
||||||
|
if (mounted) return;
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
const describeFace = typeof binder.describe === "function" ? binder.describe() : noopDescribeFace();
|
||||||
|
tryMount(binder.bind({ namespace: NS }), describeFace, "settingsScope");
|
||||||
|
});
|
||||||
|
|
||||||
|
void t;
|
||||||
}
|
}
|
||||||
//#endregion
|
//#endregion
|
||||||
|
|
||||||
|
|
|
||||||
44
lib/index.js
44
lib/index.js
|
|
@ -57,19 +57,57 @@ export const Config = z.object({
|
||||||
/** Settings namespace owning this plugin's section (Settings → Plugins card). */
|
/** Settings namespace owning this plugin's section (Settings → Plugins card). */
|
||||||
export const SEARCH_MCP_SETTINGS_NAMESPACE = 'search-mcp';
|
export const SEARCH_MCP_SETTINGS_NAMESPACE = 'search-mcp';
|
||||||
|
|
||||||
|
/** Normalize settings.describe() across DSH generations. */
|
||||||
|
function describeRows(describe) {
|
||||||
|
if (typeof describe !== 'function') return [];
|
||||||
|
try {
|
||||||
|
const raw = describe();
|
||||||
|
if (Array.isArray(raw)) return raw;
|
||||||
|
if (raw && typeof raw === 'object' && Array.isArray(raw.namespaces)) return raw.namespaces;
|
||||||
|
} catch {
|
||||||
|
// describe can throw while the provider is settling
|
||||||
|
}
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
/** Register the search provider and the live settings section. */
|
/** Register the search provider and the live settings section. */
|
||||||
export function apply(ctx, config) {
|
export function apply(ctx, config) {
|
||||||
let current = () => config;
|
let current = () => config;
|
||||||
// Optional settings seam: fall back to the composition entry when settings
|
// Optional settings seam: fall back to the composition entry when settings
|
||||||
// is absent (same pattern as @deepseek-ai/dsh-web-search-deepseek).
|
// is absent (same pattern as @deepseek-ai/dsh-web-search-deepseek).
|
||||||
ctx.inject(['settings'], (settingsCtx) => {
|
ctx.inject(['settings'], (settingsCtx) => {
|
||||||
settingsCtx.settings.installSection(ctx, SEARCH_MCP_SETTINGS_NAMESPACE, Config, config, {
|
const settings = settingsCtx.settings;
|
||||||
|
const hooks = {
|
||||||
setSource: (source) => {
|
setSource: (source) => {
|
||||||
current = source;
|
current = source;
|
||||||
},
|
},
|
||||||
// Provider projects the section per search; no re-registration needed.
|
|
||||||
onChange: () => {},
|
onChange: () => {},
|
||||||
});
|
};
|
||||||
|
if (typeof settings?.installSection === 'function') {
|
||||||
|
settings.installSection(ctx, SEARCH_MCP_SETTINGS_NAMESPACE, Config, config, hooks);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// DSH ≥0.1.7 / 0.2.0 — Config projection via describe() (no installSection).
|
||||||
|
const readLive = () => {
|
||||||
|
const row = describeRows(settings?.describe).find((item) => item.ns === SEARCH_MCP_SETTINGS_NAMESPACE);
|
||||||
|
if (row?.value !== null && typeof row?.value === 'object' && !Array.isArray(row.value)) {
|
||||||
|
return { ...config, ...row.value };
|
||||||
|
}
|
||||||
|
return config;
|
||||||
|
};
|
||||||
|
hooks.setSource(readLive);
|
||||||
|
let last = JSON.stringify(readLive());
|
||||||
|
const timer = setInterval(() => {
|
||||||
|
const next = readLive();
|
||||||
|
const fingerprint = JSON.stringify(next);
|
||||||
|
if (fingerprint === last) return;
|
||||||
|
last = fingerprint;
|
||||||
|
hooks.onChange();
|
||||||
|
}, 2_000);
|
||||||
|
settingsCtx.effect(() => () => clearInterval(timer), 'dsh-search-mcp: settings describe poll');
|
||||||
|
settingsCtx.logger?.info?.(
|
||||||
|
'dsh-search-mcp: following settings via describe() (no installSection — DSH ≥0.1.7 path)',
|
||||||
|
);
|
||||||
});
|
});
|
||||||
// `registerSearchProvider` owns its cleanup via ctx.effect (HMR/dispose safe).
|
// `registerSearchProvider` owns its cleanup via ctx.effect (HMR/dispose safe).
|
||||||
ctx.web.registerSearchProvider(new SearchMCPProvider(() => resolveOptions(ctx, current())));
|
ctx.web.registerSearchProvider(new SearchMCPProvider(() => resolveOptions(ctx, current())));
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"name": "dsh-search-mcp",
|
"name": "dsh-search-mcp",
|
||||||
"version": "0.2.0",
|
"version": "0.2.2",
|
||||||
"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",
|
||||||
|
|
@ -42,7 +42,7 @@
|
||||||
"platform": "web",
|
"platform": "web",
|
||||||
"immediately": true,
|
"immediately": true,
|
||||||
"inject": [
|
"inject": [
|
||||||
"@deepseek-ai/dsh-client-ui-settings-plugins",
|
"@deepseek-ai/dsh-client-ui-slots",
|
||||||
"@deepseek-ai/dsh-client-locale"
|
"@deepseek-ai/dsh-client-locale"
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue