mirror of
https://github.com/hansjone/dsh-search-mcp.git
synced 2026-10-11 04:30:45 +08:00
Fix Desktop search-mcp takeover so web_search uses Bailian.
Drop nested Config.volatile that left the fiber inactive, unwrap the cordis web proxy to pin/wrap search at runtime, and restore deepseek on disable. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
parent
d729b03f70
commit
23685cd545
8 changed files with 343 additions and 72 deletions
13
README.md
13
README.md
|
|
@ -122,12 +122,10 @@ RC2 的 `credentials/reference-updated` 事件会刷新设置卡片中的“已
|
|||
|
||||
## 组合覆盖
|
||||
|
||||
插件通过 `cordis.patch.yml`:
|
||||
插件通过 `cordis.patch.yml` 插入 `search-mcp` 行并提高 `tool-web` 上限;**不**在 patch 里写 `web.searchProvider`(Desktop 0.2 会死锁)。路由切换由 host `apply` 在注册后 **live-pin** `ctx.web.searchProviderId = search-mcp` 完成:
|
||||
|
||||
- 注册 `search-mcp` provider。
|
||||
- 设置 `web.searchProvider: search-mcp`。
|
||||
- 禁用 `web-search-deepseek`。
|
||||
- 保持 `web_fetch` 关闭。
|
||||
- 注册 `search-mcp` provider,并热切换活跃搜索提供商。
|
||||
- 保持 `web_fetch` 关闭(`tool-web.fetch: false`)。
|
||||
- 请求 `tool-web.searchMaxResults: 50` 和 `searchMaxQueries: 4`。
|
||||
|
||||
RC2 的 standard、code、cordis agent preset 各自包含 `tool-web` 行,并且都省略了 `searchMaxResults` 和 `searchMaxQueries`,因此实际采用 `dsh-tool-web` 默认值 8 和 4。agent-scoped 工具会遮蔽根层同名工具,所以根层 patch 中的 50 条请求不会提高这些 shipped preset 的实际上限。验证结果上限时必须检查 session 使用的 preset,不能只依据根层 `--dump-config`。
|
||||
|
|
@ -153,11 +151,12 @@ dsh --profile web --dump-config |
|
|||
|
||||
预期至少包括:
|
||||
|
||||
- `web.searchProvider: search-mcp`
|
||||
- `web-search-deepseek.disabled: true`
|
||||
- 组合中出现 `search-mcp` 行(bailian / `DASHSCOPE_API_KEY`)
|
||||
- `tool-web.disabled: false`
|
||||
- `fetch: false`
|
||||
|
||||
说明:Desktop 0.2 下 `--dump-config` **不会**出现 `web.searchProvider: search-mcp`(避免启动死锁);运行时由 host live-pin。验收应以错误密钥时出现 search-mcp/百炼错误、而非 DeepSeek HTTP 401 为准。
|
||||
|
||||
实际 agent preset 的结果数和多查询上限应在隔离 profile/session 中单独验证。
|
||||
|
||||
## 故障排查
|
||||
|
|
|
|||
|
|
@ -5,9 +5,9 @@
|
|||
# load-order deadlock (web waits for search-mcp, search-mcp waits for web)
|
||||
# and freezes the UI on "Loading plugins…".
|
||||
#
|
||||
# The host `apply` registers the provider and then soft-switches the web
|
||||
# row via settings when available. Removing this package restores the
|
||||
# previous composition (no searchProvider pin left behind by this file).
|
||||
# The host `apply` registers the provider and then live-pins
|
||||
# `ctx.web.searchProviderId = search-mcp` (no settings mutate / no patch pin).
|
||||
# Removing this package restores the previous composition.
|
||||
|
||||
- insert:
|
||||
- id: search-mcp
|
||||
|
|
|
|||
218
lib/index.js
218
lib/index.js
|
|
@ -1,59 +1,75 @@
|
|||
/**
|
||||
* dsh-search-mcp — replace dsh's built-in web search with search MCP servers.
|
||||
*
|
||||
* Adapted for DeepSeek Harness 0.1.2+: settings use
|
||||
* `ctx.settings.installSection` (the old free-function
|
||||
* `installSettingsSection` from 0.1.1-rc.2 no longer exists).
|
||||
* Registers a `ctx.web` search provider under id `search-mcp`, then **live-pins**
|
||||
* the underlying WebRuntime's `searchProviderId` so `web_search` stops using
|
||||
* `deepseek-official`.
|
||||
*
|
||||
* A Cordis plugin that
|
||||
* - registers a `ctx.web` search provider under the id `search-mcp`, and
|
||||
* - installs a Settings section (`search-mcp`) where the user manages the
|
||||
* search MCP server list (kind, endpoint/command, API key or key env
|
||||
* reference, tool name) plus `defaultServer` / `maxResults` /
|
||||
* `searchTimeoutMs` from the web Settings → Plugins page.
|
||||
* Why not cordis.patch.yml?
|
||||
* Pinning `web.searchProvider: search-mcp` (or disabling deepseek) in the
|
||||
* bundle patch deadlocks Desktop 0.2 boot: `web` waits for `search-mcp` while
|
||||
* this plugin `inject: ['web']`. Live-pin after `registerSearchProvider` avoids
|
||||
* that cycle. Do not persist the pin into settings.yaml either — a restart
|
||||
* would reintroduce the same deadlock.
|
||||
*
|
||||
* The package's `cordis.patch.yml` (bundle layer) switches
|
||||
* `web.searchProvider` to `search-mcp` and disables the built-in
|
||||
* `web-search-deepseek` provider, so while this plugin is enabled the
|
||||
* built-in search is unavailable and every `web_search` call runs through
|
||||
* the configured MCP server(s). Removing the package restores the built-in.
|
||||
* Why unwrap cordis.original?
|
||||
* `ctx.web` is a traceable Proxy. Assigning `ctx.web.searchProviderId = …`
|
||||
* does not update the field `search()` reads; pin the unwrapped instance.
|
||||
*/
|
||||
import { readFileSync, writeFileSync } from 'node:fs';
|
||||
import { dirname, join } from 'node:path';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
import z from '@deepseek-ai/schemastery';
|
||||
import { credentialRef } from '@deepseek-ai/dsh-credentials';
|
||||
import { launchEnvironmentOf } from '@deepseek-ai/dsh-launch-environment';
|
||||
import { SearchMCPProvider } from './provider.js';
|
||||
import { SEARCH_MCP_PROVIDER_ID, SearchMCPProvider } from './provider.js';
|
||||
|
||||
/** Cordis plugin name used by loader diagnostics. */
|
||||
export const name = 'search-mcp';
|
||||
|
||||
/** Module-load breadcrumb — proves Desktop resolved this build (even if apply never runs). */
|
||||
try {
|
||||
const pkgVersion = JSON.parse(
|
||||
readFileSync(join(dirname(fileURLToPath(import.meta.url)), '..', 'package.json'), 'utf8'),
|
||||
).version;
|
||||
writeFileSync(
|
||||
join(process.env.USERPROFILE || process.env.HOME || '', '.dsh', 'search-mcp-loaded.json'),
|
||||
`${JSON.stringify({ t: new Date().toISOString(), version: pkgVersion, name }, null, 2)}\n`,
|
||||
);
|
||||
} catch {
|
||||
/* ignore */
|
||||
}
|
||||
|
||||
/** The web seam this provider registers into. */
|
||||
export const inject = ['web'];
|
||||
|
||||
/**
|
||||
* DSH ≥0.1.7 / 0.2.0 only projects Config fields marked `.volatile()` into the
|
||||
* settings UI. Zero volatile fields → the whole entry is silently filtered.
|
||||
* Older schemastery without `volatile` keeps the bare schema (≤0.1.5 path).
|
||||
*/
|
||||
function vol(schema) {
|
||||
return typeof schema?.volatile === 'function' ? schema.volatile() : schema;
|
||||
}
|
||||
/** Unwrap cordis service proxy → concrete Service instance. */
|
||||
const CORDIS_ORIGINAL = Symbol.for('cordis.original');
|
||||
|
||||
/**
|
||||
* Config schema — no `.volatile()` anywhere.
|
||||
*
|
||||
* Desktop 0.2 cordis rejects nested volatiles (`servers` + `servers.*.id`) with
|
||||
* ValidationError and leaves the fiber inactive. The settings UI is a custom
|
||||
* `settings.section` (client.browser.js), so Config does not need volatile
|
||||
* projection for the form to appear.
|
||||
*/
|
||||
const serverSchema = z.object({
|
||||
id: vol(z.string()),
|
||||
kind: vol(z.string().default('custom')),
|
||||
transport: vol(z.string().default('http')),
|
||||
url: vol(z.string().default('')),
|
||||
command: vol(z.string().default('')),
|
||||
args: vol(z.array(z.string()).default([])),
|
||||
apiKey: vol(z.string().role('secret')),
|
||||
apiKeyEnv: vol(z.string().role('credential-ref').default('')),
|
||||
authStyle: vol(z.string().default('')),
|
||||
authParam: vol(z.string().default('')),
|
||||
authPrefix: vol(z.string().default('')),
|
||||
toolName: vol(z.string().default('')),
|
||||
id: z.string(),
|
||||
kind: z.string().default('custom'),
|
||||
transport: z.string().default('http'),
|
||||
url: z.string().default(''),
|
||||
command: z.string().default(''),
|
||||
args: z.array(z.string()).default([]),
|
||||
apiKey: z.string().role('secret'),
|
||||
apiKeyEnv: z.string().role('credential-ref').default(''),
|
||||
authStyle: z.string().default(''),
|
||||
authParam: z.string().default(''),
|
||||
authPrefix: z.string().default(''),
|
||||
toolName: z.string().default(''),
|
||||
// Note: this schemastery fork has no `.optional()`; object fields are
|
||||
// optional unless `.required()` is applied, so absence is already allowed.
|
||||
maxResults: vol(z.number().step(1).min(1).max(50)),
|
||||
maxResults: z.number().step(1).min(1).max(50),
|
||||
});
|
||||
|
||||
const DEFAULT_BAILIAN_SERVER = {
|
||||
|
|
@ -63,34 +79,128 @@ const DEFAULT_BAILIAN_SERVER = {
|
|||
};
|
||||
|
||||
export const Config = z.object({
|
||||
defaultServer: vol(z.string().default('bailian')),
|
||||
maxResults: vol(z.number().step(1).min(1).max(50).default(8)),
|
||||
searchTimeoutMs: vol(z.number().step(1).min(1000).default(30000)),
|
||||
servers: vol(z.array(serverSchema).default([DEFAULT_BAILIAN_SERVER])),
|
||||
defaultServer: z.string().default('bailian'),
|
||||
maxResults: z.number().step(1).min(1).max(50).default(8),
|
||||
searchTimeoutMs: z.number().step(1).min(1000).default(30000),
|
||||
servers: z.array(serverSchema).default([DEFAULT_BAILIAN_SERVER]),
|
||||
});
|
||||
|
||||
/** 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 [];
|
||||
/** Concrete WebRuntime behind `ctx.web` (cordis traceable proxy). */
|
||||
function unwrapWeb(web) {
|
||||
if (!web || typeof web !== 'object') return null;
|
||||
const raw = web[CORDIS_ORIGINAL];
|
||||
return raw && typeof raw === 'object' ? raw : web;
|
||||
}
|
||||
|
||||
/** Register the search provider. Settings UI is owned by the web client. */
|
||||
/** Match WebRuntime.capSources so direct dispatch keeps the same contract. */
|
||||
function capSources(result, maxResults) {
|
||||
if (maxResults === undefined || !Array.isArray(result?.sources) || result.sources.length <= maxResults) {
|
||||
return result;
|
||||
}
|
||||
return { ...result, sources: result.sources.slice(0, maxResults), truncated: true };
|
||||
}
|
||||
|
||||
/**
|
||||
* Hot-takeover for the life of this fiber:
|
||||
* - pin + wrap on apply (enable / boot)
|
||||
* - restore previous provider + unwrap on dispose (disable)
|
||||
*
|
||||
* No Desktop restart needed for enable/disable — only for loading a new
|
||||
* package build into an already-running process.
|
||||
*/
|
||||
function takeOverWebSearch(ctx, provider) {
|
||||
const web = unwrapWeb(ctx.web);
|
||||
if (!web || typeof web.search !== 'function' || !provider) return false;
|
||||
|
||||
const previousId = web.searchProviderId;
|
||||
// Leftover pin from a crashed unload → fall back to Desktop default.
|
||||
const restoreId =
|
||||
previousId === SEARCH_MCP_PROVIDER_ID || previousId === undefined
|
||||
? 'deepseek-official'
|
||||
: previousId;
|
||||
|
||||
const previousSearch = web.search.__searchMcpWrapped ? web.search.__searchMcpOriginal : web.search;
|
||||
|
||||
try {
|
||||
web.searchProviderId = SEARCH_MCP_PROVIDER_ID;
|
||||
} catch (error) {
|
||||
ctx.logger?.warn?.('search-mcp: failed to assign web.searchProviderId: %s', error);
|
||||
return false;
|
||||
}
|
||||
|
||||
async function searchMcpPinned(request, signal) {
|
||||
web.searchProviderId = SEARCH_MCP_PROVIDER_ID;
|
||||
if (typeof provider.available === 'function' && provider.available()) {
|
||||
const result = await provider.search(request, signal);
|
||||
return capSources(result, request?.maxResults);
|
||||
}
|
||||
return previousSearch.call(web, request, signal);
|
||||
}
|
||||
searchMcpPinned.__searchMcpWrapped = true;
|
||||
searchMcpPinned.__searchMcpOriginal = previousSearch;
|
||||
web.search = searchMcpPinned;
|
||||
|
||||
// Fiber unload (plugin disable) → hand search back to the built-in path.
|
||||
ctx.effect(() => () => {
|
||||
if (web.search === searchMcpPinned) {
|
||||
web.search = previousSearch;
|
||||
}
|
||||
if (web.searchProviderId === SEARCH_MCP_PROVIDER_ID) {
|
||||
web.searchProviderId = restoreId;
|
||||
}
|
||||
ctx.logger?.info?.(
|
||||
'search-mcp: released web search (restored searchProviderId → %s)',
|
||||
restoreId === undefined ? '(unset)' : restoreId,
|
||||
);
|
||||
});
|
||||
|
||||
ctx.logger?.info?.(
|
||||
'search-mcp: took over web.search (%s → %s); disable plugin to release',
|
||||
previousId === undefined ? '(unset)' : previousId,
|
||||
SEARCH_MCP_PROVIDER_ID,
|
||||
);
|
||||
return true;
|
||||
}
|
||||
|
||||
/** Best-effort runtime breadcrumb for Desktop diagnosis (~/.dsh/search-mcp-runtime.json). */
|
||||
async function writeRuntimeBreadcrumb(payload) {
|
||||
try {
|
||||
const { writeFileSync } = await import('node:fs');
|
||||
const { join } = await import('node:path');
|
||||
const home = process.env.USERPROFILE || process.env.HOME || '';
|
||||
if (!home) return;
|
||||
writeFileSync(
|
||||
join(home, '.dsh', 'search-mcp-runtime.json'),
|
||||
`${JSON.stringify({ t: new Date().toISOString(), ...payload }, null, 2)}\n`,
|
||||
);
|
||||
} catch {
|
||||
/* ignore */
|
||||
}
|
||||
}
|
||||
|
||||
/** Register the search provider and take over web_search selection. */
|
||||
export function apply(ctx, config) {
|
||||
const current = () => config;
|
||||
// Desktop 0.2: avoid soft-inject(['settings']) + describe poll during bring-up.
|
||||
// Desktop 0.2: avoid soft-inject(['settings']) during bring-up.
|
||||
// Loader already projects Config from the cordis entry / patch insert.
|
||||
ctx.web.registerSearchProvider(new SearchMCPProvider(() => resolveOptions(ctx, current())));
|
||||
const provider = new SearchMCPProvider(() => resolveOptions(ctx, current()));
|
||||
ctx.web.registerSearchProvider(provider);
|
||||
// Hot path: enable = take over now; disable = effect disposer restores.
|
||||
const tookOver = takeOverWebSearch(ctx, provider);
|
||||
if (!tookOver) {
|
||||
ctx.logger?.warn?.(
|
||||
'search-mcp: could not take over search provider; web_search may still use deepseek-official',
|
||||
);
|
||||
}
|
||||
void writeRuntimeBreadcrumb({
|
||||
event: 'apply',
|
||||
tookOver,
|
||||
searchProviderId: unwrapWeb(ctx.web)?.searchProviderId,
|
||||
servers: (config.servers ?? []).map((s) => ({ id: s.id, kind: s.kind })),
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -1,10 +1,10 @@
|
|||
/**
|
||||
* The `search-mcp` web search provider.
|
||||
*
|
||||
* Registers into `ctx.web` under the stable id `search-mcp`; the profile
|
||||
* patch switches `web.searchProvider` to this id and disables the built-in
|
||||
* DeepSeek provider, so the model-facing `web_search` tool executes entirely
|
||||
* through the configured search MCP server(s).
|
||||
* Registers into `ctx.web` under the stable id `search-mcp`. The host
|
||||
* `apply` then live-pins `ctx.web.searchProviderId` to this id so
|
||||
* model-facing `web_search` runs through the configured search MCP
|
||||
* server(s) (Desktop 0.2 cannot safely pin via cordis.patch.yml).
|
||||
*/
|
||||
import { WebError } from '@deepseek-ai/dsh-web';
|
||||
import { resolveServer } from './catalog.js';
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "dsh-search-mcp",
|
||||
"version": "0.2.32",
|
||||
"version": "0.2.38",
|
||||
"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",
|
||||
|
|
|
|||
73
scripts/_validate-config.mjs
Normal file
73
scripts/_validate-config.mjs
Normal file
|
|
@ -0,0 +1,73 @@
|
|||
/**
|
||||
* Reproduce Desktop Config validation for search-mcp.
|
||||
* Usage: node scripts/_validate-config.mjs
|
||||
*/
|
||||
import { createRequire } from 'node:module'
|
||||
import { pathToFileURL } from 'node:url'
|
||||
import { join, dirname } from 'node:path'
|
||||
import { fileURLToPath } from 'node:url'
|
||||
|
||||
const pkgRoot = join(dirname(fileURLToPath(import.meta.url)), '..')
|
||||
const nm = join(process.env.USERPROFILE || '', '.dsh', 'profiles', 'node_modules')
|
||||
const req = createRequire(join(nm, 'package.json'))
|
||||
const cordisUrl = pathToFileURL(req.resolve('@deepseek-ai/cordis')).href
|
||||
const { Context, Service } = await import(cordisUrl)
|
||||
const mod = await import(pathToFileURL(join(pkgRoot, 'lib', 'index.js')).href)
|
||||
|
||||
class WebRuntime extends Service {
|
||||
searchProviders = new Map()
|
||||
searchProviderId = 'deepseek-official'
|
||||
constructor(ctx) {
|
||||
super(ctx, 'web')
|
||||
}
|
||||
registerSearchProvider(p) {
|
||||
this.searchProviders.set(p.id, p)
|
||||
return () => this.searchProviders.delete(p.id)
|
||||
}
|
||||
async search() {
|
||||
return { used: this.searchProviderId }
|
||||
}
|
||||
}
|
||||
|
||||
function plugin(ctx, config) {
|
||||
mod.apply(ctx, config)
|
||||
}
|
||||
plugin.inject = ['web']
|
||||
plugin.Config = mod.Config
|
||||
|
||||
const root = new Context()
|
||||
try {
|
||||
await root.plugin(WebRuntime)
|
||||
await root.plugin(plugin, {
|
||||
defaultServer: 'bailian',
|
||||
maxResults: 8,
|
||||
searchTimeoutMs: 30000,
|
||||
servers: [{ id: 'bailian', kind: 'bailian', apiKeyEnv: 'DASHSCOPE_API_KEY' }],
|
||||
})
|
||||
const raw = root.web[Symbol.for('cordis.original')]
|
||||
console.log(
|
||||
JSON.stringify(
|
||||
{
|
||||
ok: true,
|
||||
faceId: root.web.searchProviderId,
|
||||
rawId: raw?.searchProviderId,
|
||||
wrapped: !!raw?.search?.__searchMcpWrapped,
|
||||
},
|
||||
null,
|
||||
2,
|
||||
),
|
||||
)
|
||||
} catch (e) {
|
||||
console.log(
|
||||
JSON.stringify(
|
||||
{
|
||||
ok: false,
|
||||
message: String(e?.message || e),
|
||||
stack: String(e?.stack || '').slice(0, 1200),
|
||||
},
|
||||
null,
|
||||
2,
|
||||
),
|
||||
)
|
||||
process.exit(1)
|
||||
}
|
||||
78
scripts/_verify-takeover.mjs
Normal file
78
scripts/_verify-takeover.mjs
Normal file
|
|
@ -0,0 +1,78 @@
|
|||
/**
|
||||
* Prove unwrap + direct web.search wrap routes off deepseek-official.
|
||||
* Run: node scripts/_verify-takeover.mjs
|
||||
*/
|
||||
import { createRequire } from 'node:module'
|
||||
import { dirname, join } from 'node:path'
|
||||
import { fileURLToPath, pathToFileURL } from 'node:url'
|
||||
|
||||
const pkgRoot = join(dirname(fileURLToPath(import.meta.url)), '..')
|
||||
const profilesNm = join(process.env.USERPROFILE || '', '.dsh', 'profiles', 'node_modules')
|
||||
const require = createRequire(join(profilesNm, 'package.json'))
|
||||
const { Context, Service } = require('@deepseek-ai/cordis')
|
||||
const { apply } = await import(pathToFileURL(join(pkgRoot, 'lib', 'index.js')).href)
|
||||
const ORIGINAL = Symbol.for('cordis.original')
|
||||
|
||||
class WebRuntime extends Service {
|
||||
searchProviders = new Map()
|
||||
searchProviderId
|
||||
constructor(ctx, config = {}) {
|
||||
super(ctx, 'web')
|
||||
this.searchProviderId = config.searchProvider ?? 'deepseek-official'
|
||||
}
|
||||
registerSearchProvider(p) {
|
||||
this.searchProviders.set(p.id, p)
|
||||
return () => this.searchProviders.delete(p.id)
|
||||
}
|
||||
async search() {
|
||||
if (this.searchProviderId === 'deepseek-official') {
|
||||
return { used: 'deepseek-official', error: 'DeepSeek API error (HTTP 401)' }
|
||||
}
|
||||
const p = this.searchProviders.get(this.searchProviderId)
|
||||
return { used: this.searchProviderId, via: p?.id }
|
||||
}
|
||||
}
|
||||
|
||||
const smcp = Object.assign(function (ctx) {
|
||||
apply(ctx, {
|
||||
defaultServer: 'bailian',
|
||||
maxResults: 8,
|
||||
searchTimeoutMs: 30000,
|
||||
servers: [{ id: 'bailian', kind: 'bailian', apiKeyEnv: 'DASHSCOPE_API_KEY' }],
|
||||
})
|
||||
}, { inject: ['web'] })
|
||||
|
||||
const app = new Context()
|
||||
await app.plugin(WebRuntime, { searchProvider: 'deepseek-official' })
|
||||
const fiber = await app.plugin(smcp)
|
||||
|
||||
const raw = app.web[ORIGINAL]
|
||||
let thrown
|
||||
try {
|
||||
await app.web.search({ query: 'venezuela', maxResults: 5 })
|
||||
} catch (error) {
|
||||
thrown = String(error?.message || error)
|
||||
}
|
||||
|
||||
const enabledOk =
|
||||
raw.searchProviderId === 'search-mcp'
|
||||
&& raw.search.__searchMcpWrapped === true
|
||||
&& /search-mcp/.test(thrown || '')
|
||||
|
||||
await fiber.dispose()
|
||||
const afterDisable = await app.web.search({ query: 'venezuela', maxResults: 5 })
|
||||
const disabledOk =
|
||||
raw.searchProviderId === 'deepseek-official'
|
||||
&& !raw.search.__searchMcpWrapped
|
||||
&& afterDisable?.used === 'deepseek-official'
|
||||
|
||||
const ok = enabledOk && disabledOk
|
||||
console.log(JSON.stringify({
|
||||
ok,
|
||||
enabledOk,
|
||||
disabledOk,
|
||||
thrown: thrown?.slice(0, 160),
|
||||
afterDisable,
|
||||
rawId: raw.searchProviderId,
|
||||
}, null, 2))
|
||||
process.exit(ok ? 0 : 1)
|
||||
|
|
@ -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.32')
|
||||
assert.equal(pkg.version, '0.2.38')
|
||||
assert.equal(pkg.dsh.client.immediately, false)
|
||||
|
||||
for (const name of [
|
||||
|
|
@ -51,13 +51,24 @@ test('browser half registers Settings sidebar + deferred form attach', async ()
|
|||
assert.match(client, /legacyKeyBlocked/)
|
||||
})
|
||||
|
||||
test('host registers search provider without settings soft-inject poll', async () => {
|
||||
test('host registers search provider and hot-takeover without settings mutate', async () => {
|
||||
const host = await read('lib/index.js')
|
||||
assert.match(host, /function vol\(/)
|
||||
assert.match(host, /registerSearchProvider/)
|
||||
assert.match(host, /function takeOverWebSearch/)
|
||||
assert.match(host, /Symbol\.for\(['"]cordis\.original['"]\)/)
|
||||
assert.match(host, /function unwrapWeb/)
|
||||
assert.match(host, /provider\.search\(request, signal\)/)
|
||||
assert.match(host, /web\.searchProviderId\s*=\s*SEARCH_MCP_PROVIDER_ID/)
|
||||
assert.match(host, /released web search/)
|
||||
assert.match(host, /takeOverWebSearch\(ctx, provider\)/)
|
||||
assert.match(host, /ctx\.effect\(\(\) => \(\) => \{/)
|
||||
// Desktop 0.2: Config must not call .volatile() (array inner is always blocked).
|
||||
assert.doesNotMatch(host, /[.\w]\.volatile\s*\(/)
|
||||
assert.doesNotMatch(host, /function vol\(/)
|
||||
const applyBody = host.slice(host.indexOf('export function apply'), host.indexOf('function resolveOptions'))
|
||||
assert.doesNotMatch(applyBody, /ctx\.inject\(\s*\[['"]settings['"]\]/)
|
||||
assert.doesNotMatch(applyBody, /setInterval\(/)
|
||||
assert.doesNotMatch(applyBody, /settings\.mutate/)
|
||||
})
|
||||
|
||||
test('known providers are CDKey-only while custom keeps advanced fields', async () => {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue