mirror of
https://github.com/hansjone/dsh-search-mcp.git
synced 2026-10-11 05:40:46 +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" });
|
||||
let react_jsx_runtime = require("react/jsx-runtime");
|
||||
let react = require("react");
|
||||
let _deepseek_ai_dsh_client_ui_primitives = require("@deepseek-ai/dsh-client-ui-primitives");
|
||||
// 0.1.2+: createSnapshotStore lives on the platform seed @deepseek-ai/dsh-client-store
|
||||
// (old @deepseek-ai/dsh-client-runtime/client is gone from the module table).
|
||||
let _deepseek_ai_dsh_client_runtime_client = require("@deepseek-ai/dsh-client-store");
|
||||
// Desktop 0.2 may not seed ui-primitives / dsh-client-store in the ModuleLoader
|
||||
// table. Soft-require + local fallbacks so import never fails web boot.
|
||||
let _deepseek_ai_dsh_client_ui_primitives = {};
|
||||
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
|
||||
const css = [
|
||||
|
|
@ -1043,30 +1113,113 @@ window.__ModuleLoader__.load({
|
|||
//#endregion
|
||||
|
||||
//#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) {
|
||||
const t = ctx.locale.bind(NS);
|
||||
ctx.effect(() => ctx.locale.register(NS, { zh, en }), "dsh-search-mcp: section dictionaries");
|
||||
const describeFace = ctx.settingsScope.describe();
|
||||
const controller = new SearchMcpCardController(ctx.settingsScope.bind({ namespace: NS }), ctx.remote, describeFace);
|
||||
ctx.effect(() => ctx.remote.$on("credentials/reference-updated", (ref) => {
|
||||
function resolveRemote(ctx) {
|
||||
return ctx.remote || ctx.get?.("remote") || {
|
||||
credentials: { set: async () => ({ ok: false }), describe: async () => ({ ok: false }) },
|
||||
settings: { mutate: async () => ({ ok: false }), describe: async () => ({ ok: false }) },
|
||||
$on: () => () => {},
|
||||
};
|
||||
}
|
||||
|
||||
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);
|
||||
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* () {
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
locale: NS,
|
||||
inject: () => controller.inject()
|
||||
}, SearchMcpCard);
|
||||
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");
|
||||
|
||||
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
|
||||
|
||||
|
|
|
|||
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). */
|
||||
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. */
|
||||
export function apply(ctx, config) {
|
||||
let current = () => config;
|
||||
// Optional settings seam: fall back to the composition entry when settings
|
||||
// is absent (same pattern as @deepseek-ai/dsh-web-search-deepseek).
|
||||
ctx.inject(['settings'], (settingsCtx) => {
|
||||
settingsCtx.settings.installSection(ctx, SEARCH_MCP_SETTINGS_NAMESPACE, Config, config, {
|
||||
const settings = settingsCtx.settings;
|
||||
const hooks = {
|
||||
setSource: (source) => {
|
||||
current = source;
|
||||
},
|
||||
// Provider projects the section per search; no re-registration needed.
|
||||
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).
|
||||
ctx.web.registerSearchProvider(new SearchMCPProvider(() => resolveOptions(ctx, current())));
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"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.",
|
||||
"type": "module",
|
||||
"main": "lib/index.js",
|
||||
|
|
@ -42,7 +42,7 @@
|
|||
"platform": "web",
|
||||
"immediately": true,
|
||||
"inject": [
|
||||
"@deepseek-ai/dsh-client-ui-settings-plugins",
|
||||
"@deepseek-ai/dsh-client-ui-slots",
|
||||
"@deepseek-ai/dsh-client-locale"
|
||||
]
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue