newmark-agent 0.5.12 → 0.5.13

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,419 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
35
+ Object.defineProperty(exports, "__esModule", { value: true });
36
+ exports.SearchMcpPool = exports.DEFAULT_SEARCH_MCP_MANIFEST = exports.MAX_SEARCH_MCP_RESULT_CHARS = void 0;
37
+ exports.loadSearchMcpManifest = loadSearchMcpManifest;
38
+ exports.publicSearchMcpManifest = publicSearchMcpManifest;
39
+ exports.redactSearchMcpLocalPaths = redactSearchMcpLocalPaths;
40
+ exports.chooseSearchTool = chooseSearchTool;
41
+ const fs = __importStar(require("fs"));
42
+ const path = __importStar(require("path"));
43
+ const crypto_1 = require("crypto");
44
+ const index_js_1 = require("@modelcontextprotocol/sdk/client/index.js");
45
+ const sse_js_1 = require("@modelcontextprotocol/sdk/client/sse.js");
46
+ const stdio_js_1 = require("@modelcontextprotocol/sdk/client/stdio.js");
47
+ const streamableHttp_js_1 = require("@modelcontextprotocol/sdk/client/streamableHttp.js");
48
+ const DEFAULT_TIMEOUT_MS = 8_000;
49
+ const MAX_TIMEOUT_MS = 30_000;
50
+ exports.MAX_SEARCH_MCP_RESULT_CHARS = 60_000;
51
+ const SEARCH_MCP_HEALTH_FILE = 'search-mcp-health.json';
52
+ const SEARCH_TOOL_NAME = /^(?:web[_-]?search|search(?:_web)?|internet[_-]?search|duckduckgo(?:[_-](?:web[_-]?)?search)?)$/i;
53
+ const SEARCH_TOOL_DESCRIPTION = /\b(?:web|internet|duckduckgo|searxng)\b[\s\S]{0,80}\bsearch\b|\bsearch\b[\s\S]{0,80}\b(?:web|internet)\b/i;
54
+ const SEARCH_QUERY_ARGUMENTS = ['query', 'q', 'search_query', 'searchQuery', 'keywords', 'keyword', 'text'];
55
+ const SAFE_SEARCH_ARGUMENTS = new Set([
56
+ ...SEARCH_QUERY_ARGUMENTS,
57
+ 'count', 'limit', 'max_results', 'maxResults', 'page', 'offset', 'rankingMode',
58
+ 'category', 'categories', 'language', 'locale', 'region', 'country', 'freshness',
59
+ 'time_range', 'timeRange', 'safe_search', 'safesearch',
60
+ ]);
61
+ const DANGEROUS_SEARCH_ARGUMENT = /(?:^|_)(?:command|cmd|shell|path|file|write|delete|execute|script|code|cwd|env|headers|body|method|tool|action)(?:$|_)/i;
62
+ const EMPTY_SEARCH_TEXT = /^(?:\s*(?:\[(?:web[_-]?)?search\]\s*)?)(?:no\s+(?:search\s+)?results?(?:\s+(?:were\s+)?found)?(?=\s*(?:$|[.!。::,;]|(?:for|matching|returned|available)\b))|0\s+(?:search\s+)?results?(?=\s*(?:$|[.!。::,;]|(?:for|found|returned|matching)\b))|nothing\s+(?:was\s+)?found(?=\s*(?:$|[.!。::,;]|(?:for|matching)\b))|(?:未找到(?:任何)?(?:搜索)?结果|无(?:可用)?搜索结果)(?=\s*(?:$|[。!!,,::;;])))/i;
63
+ /**
64
+ * Public protocol catalog. Only endpoints with evidence-backed launch metadata are runnable by default.
65
+ * The remaining requested implementations are exposed as disabled templates until a distributor/user
66
+ * supplies a concrete stdio command or Streamable HTTP/SSE URL in search-mcp.json.
67
+ */
68
+ exports.DEFAULT_SEARCH_MCP_MANIFEST = [
69
+ {
70
+ id: 'wuxing-search-mcp', name: 'Wuxing Search MCP', enabled: false, priority: 100,
71
+ transport: 'stdio', command: process.execPath,
72
+ args: [path.join(__dirname, '../../node_modules/@iflow-mcp/maeshughes-wuxing-search-mcp/src/index.js')],
73
+ env: { ELECTRON_RUN_AS_NODE: '1', SEARXNG_URL: 'http://127.0.0.1:18080', MAX_RESULTS: '12', TIMEOUT: '7000' },
74
+ tool: 'web_search', argument: 'query', timeoutMs: 8_000,
75
+ notes: 'Bundled MCP adapter; requires a reachable SearXNG backend. Priority is configurable and is not privileged.',
76
+ },
77
+ { id: 'web-search-api', name: 'web-search-api', enabled: false, priority: 200, transport: 'template', notes: 'Set a verified stdio command or HTTP/SSE URL before enabling.' },
78
+ { id: 'miyami-websearch-mcp', name: 'miyami-websearch-mcp', enabled: false, priority: 300, transport: 'template', notes: 'Protocol template; no runtime is downloaded implicitly.' },
79
+ { id: 'searxng-mcp', name: 'searxng-mcp', enabled: false, priority: 400, transport: 'template', notes: 'Protocol template; configure a trusted SearXNG MCP deployment.' },
80
+ { id: 'mcp-server-freesearch', name: 'MCP Server FreeSearch', enabled: false, priority: 500, transport: 'template', notes: 'Protocol template; concrete implementation is distributor-selected.' },
81
+ {
82
+ id: 'ignidor-web-search-mcp', name: '@ignidor/web-search-mcp', enabled: true, priority: 600,
83
+ transport: 'stdio', command: process.execPath,
84
+ args: [path.join(__dirname, '../../node_modules/@ignidor/web-search-mcp/dist/index.js')],
85
+ env: { ELECTRON_RUN_AS_NODE: '1' },
86
+ tool: 'search', argument: 'query', timeoutMs: 12_000,
87
+ notes: 'Bundled after initialize/tools-list/tools-call verification; search-only invocation does not expose crawler or browser tools.',
88
+ },
89
+ { id: 'free-search-mcp', name: 'free-search-mcp', enabled: false, priority: 700, transport: 'template', notes: 'Protocol template; configure the intended package or remote service explicitly.' },
90
+ { id: 'duckduckgo-mcp', name: 'DuckDuckGo MCP', enabled: false, priority: 800, transport: 'template', notes: 'MCP implementation is configurable; HTTP DuckDuckGo remains the absolute final fallback.' },
91
+ { id: 'free-mcp-web-search-server', name: 'Free MCP Web Search Server', enabled: false, priority: 900, transport: 'template', notes: 'Protocol template; configure a verified endpoint before enabling.' },
92
+ ];
93
+ function stringRecord(value) {
94
+ if (!value || typeof value !== 'object' || Array.isArray(value))
95
+ return undefined;
96
+ const output = {};
97
+ for (const [key, item] of Object.entries(value)) {
98
+ const clean = String(key || '').trim();
99
+ if (!clean || /[\r\n\0]/.test(clean) || ['__proto__', 'prototype', 'constructor'].includes(clean))
100
+ continue;
101
+ const text = String(item ?? '');
102
+ if (/\0/.test(text) || /[\r\n]/.test(text) && /authorization|header/i.test(clean))
103
+ continue;
104
+ output[clean] = text;
105
+ }
106
+ return output;
107
+ }
108
+ function stringArray(value) {
109
+ if (!Array.isArray(value) || value.length > 100 || value.some(item => typeof item !== 'string'))
110
+ return undefined;
111
+ return value.map(item => String(item).slice(0, 4_000));
112
+ }
113
+ function safeEndpoint(value) {
114
+ if (!value || typeof value !== 'object' || Array.isArray(value))
115
+ return undefined;
116
+ const item = value;
117
+ const id = String(item.id || '').trim().slice(0, 160);
118
+ const name = String(item.name || '').trim().slice(0, 160);
119
+ const transport = ['stdio', 'streamable_http', 'sse', 'template'].includes(String(item.transport))
120
+ ? String(item.transport) : 'template';
121
+ if (!id || !name)
122
+ return undefined;
123
+ const priority = Number.isFinite(Number(item.priority)) ? Math.trunc(Number(item.priority)) : 1_000;
124
+ const timeoutMs = Math.max(1_000, Math.min(MAX_TIMEOUT_MS, Number(item.timeoutMs) || DEFAULT_TIMEOUT_MS));
125
+ const endpoint = {
126
+ id, name, transport, enabled: item.enabled === true, priority, timeoutMs,
127
+ command: String(item.command || '').trim().slice(0, 2_000) || undefined,
128
+ args: stringArray(item.args), cwd: String(item.cwd || '').trim().slice(0, 4_000) || undefined,
129
+ env: stringRecord(item.env), url: String(item.url || '').trim().slice(0, 4_000) || undefined,
130
+ headers: stringRecord(item.headers), tool: String(item.tool || '').trim().slice(0, 160) || undefined,
131
+ argument: String(item.argument || '').trim().slice(0, 160) || undefined,
132
+ notes: String(item.notes || '').trim().slice(0, 1_000) || undefined,
133
+ };
134
+ if (transport === 'stdio' && !endpoint.command)
135
+ endpoint.transport = 'template';
136
+ if (transport === 'streamable_http' || transport === 'sse') {
137
+ try {
138
+ const parsed = new URL(endpoint.url || '');
139
+ if (!['http:', 'https:'].includes(parsed.protocol) || parsed.username || parsed.password)
140
+ endpoint.transport = 'template';
141
+ }
142
+ catch {
143
+ endpoint.transport = 'template';
144
+ }
145
+ }
146
+ return endpoint;
147
+ }
148
+ function mergeManifest(configured, useDefaults = true) {
149
+ const overrides = Array.isArray(configured) ? configured.map(safeEndpoint).filter((item) => !!item) : [];
150
+ const byId = new Map((useDefaults ? exports.DEFAULT_SEARCH_MCP_MANIFEST : []).map(item => [item.id, { ...item }]));
151
+ for (const override of overrides)
152
+ byId.set(override.id, { ...(byId.get(override.id) || {}), ...override });
153
+ return [...byId.values()].sort((a, b) => a.priority - b.priority || a.id.localeCompare(b.id));
154
+ }
155
+ function loadSearchMcpManifest(root) {
156
+ try {
157
+ const parsed = JSON.parse(fs.readFileSync(path.join(root, 'search-mcp.json'), 'utf8'));
158
+ return mergeManifest(parsed.endpoints, false);
159
+ }
160
+ catch {
161
+ return mergeManifest([], true);
162
+ }
163
+ }
164
+ function publicSearchMcpManifest(root) {
165
+ return loadSearchMcpManifest(root).map(endpoint => {
166
+ const { env, headers, command: _command, args: _args, cwd: _cwd, ...rest } = endpoint;
167
+ let publicUrl = endpoint.url;
168
+ try {
169
+ const parsed = new URL(endpoint.url || '');
170
+ for (const key of Array.from(parsed.searchParams.keys())) {
171
+ if (/(?:api.?key|authorization|access.?token|secret|password|credential)/i.test(key))
172
+ parsed.searchParams.set(key, '<redacted>');
173
+ }
174
+ if (parsed.searchParams.size > 0 && parsed.searchParams.toString().length > 2_000)
175
+ parsed.search = '';
176
+ publicUrl = parsed.toString();
177
+ }
178
+ catch { /* template or stdio endpoint */ }
179
+ return { ...rest, url: publicUrl, envKeys: Object.keys(env || {}).sort(), headerKeys: Object.keys(headers || {}).sort() };
180
+ });
181
+ }
182
+ function replaceLiteral(value, target, replacement) {
183
+ if (!target)
184
+ return value;
185
+ return value.replace(new RegExp(target.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'), 'gi'), replacement);
186
+ }
187
+ function redactSearchMcpLocalPaths(value, additionalRoots = []) {
188
+ let output = String(value || '');
189
+ const roots = [
190
+ ...additionalRoots,
191
+ process.cwd(),
192
+ process.env.USERPROFILE,
193
+ process.env.HOME,
194
+ process.env.LOCALAPPDATA,
195
+ process.env.APPDATA,
196
+ process.env.TEMP,
197
+ process.env.TMP,
198
+ process.env.npm_config_cache,
199
+ ].filter((item) => !!item).sort((a, b) => b.length - a.length);
200
+ for (const root of roots) {
201
+ const variants = new Set([
202
+ root,
203
+ root.replace(/\\/g, '/'),
204
+ root.replace(/\//g, '\\'),
205
+ encodeURI(root),
206
+ encodeURI(root.replace(/\\/g, '/')),
207
+ ]);
208
+ for (const variant of variants)
209
+ output = replaceLiteral(output, variant, '<local-path>');
210
+ }
211
+ return output
212
+ .replace(/file:\/\/\/[A-Za-z]:\/[^\s\"'<>|,;\r\n]+/gi, '<local-path>')
213
+ .replace(/(?<![A-Za-z0-9+.-])[A-Za-z]:[\\/][^\s\"'<>|,;\r\n]+/g, '<local-path>')
214
+ .replace(/(?:file:\/\/)?\/(?:home|Users|tmp|var\/tmp)\/[^\s\"'<>|,;\r\n]+/g, '<local-path>');
215
+ }
216
+ function cleanError(error, roots = []) {
217
+ return redactSearchMcpLocalPaths(error instanceof Error ? error.message : String(error || 'unknown error'), roots)
218
+ .replace(/(Authorization\s*:\s*Bearer\s+)[^\s,;]+/ig, '$1[redacted]')
219
+ .replace(/([?&](?:api[_-]?key|access_token|token|key)=)[^&\s]+/ig, '$1[redacted]')
220
+ .slice(0, 300);
221
+ }
222
+ function resultText(result) {
223
+ const content = Array.isArray(result.content) ? result.content : [];
224
+ const chunks = [];
225
+ let length = 0;
226
+ for (const item of content) {
227
+ if (!item || typeof item !== 'object' || item.type !== 'text')
228
+ continue;
229
+ const value = String(item.text || '').trim();
230
+ if (!value)
231
+ continue;
232
+ const separator = chunks.length ? 2 : 0;
233
+ const remaining = exports.MAX_SEARCH_MCP_RESULT_CHARS - length - separator;
234
+ if (remaining <= 0)
235
+ break;
236
+ chunks.push(value.slice(0, remaining));
237
+ length += separator + Math.min(value.length, remaining);
238
+ }
239
+ const text = chunks.join('\n\n').slice(0, exports.MAX_SEARCH_MCP_RESULT_CHARS);
240
+ if (text)
241
+ return text;
242
+ const structured = result.structuredContent;
243
+ return structured && typeof structured === 'object'
244
+ ? JSON.stringify(structured, null, 2).slice(0, exports.MAX_SEARCH_MCP_RESULT_CHARS)
245
+ : '';
246
+ }
247
+ function looksLikeEmptyOrError(text) {
248
+ const clean = String(text || '').trim();
249
+ if (!clean)
250
+ return true;
251
+ try {
252
+ const parsed = JSON.parse(clean);
253
+ if (parsed.success === false || parsed.ok === false || Boolean(parsed.error))
254
+ return true;
255
+ if (Array.isArray(parsed.results) && parsed.results.length === 0 && !parsed.answer)
256
+ return true;
257
+ }
258
+ catch { /* plain text results are valid */ }
259
+ return EMPTY_SEARCH_TEXT.test(clean);
260
+ }
261
+ function dangerousSearchArgument(key) {
262
+ const normalized = String(key || '')
263
+ .replace(/([a-z0-9])([A-Z])/g, '$1_$2')
264
+ .replace(/[^A-Za-z0-9]+/g, '_')
265
+ .toLowerCase();
266
+ return DANGEROUS_SEARCH_ARGUMENT.test(normalized);
267
+ }
268
+ function chooseSearchTool(tools, preferred) {
269
+ const safeTool = (tool) => {
270
+ const properties = tool.inputSchema?.properties;
271
+ if (!properties || tool.inputSchema?.type && tool.inputSchema.type !== 'object')
272
+ return undefined;
273
+ const propertyNames = Object.keys(properties);
274
+ if (propertyNames.some(key => dangerousSearchArgument(key) || !SAFE_SEARCH_ARGUMENTS.has(key)))
275
+ return undefined;
276
+ const argument = SEARCH_QUERY_ARGUMENTS.find(key => properties[key]?.type === 'string');
277
+ if (!argument)
278
+ return undefined;
279
+ const required = Array.isArray(tool.inputSchema?.required) ? tool.inputSchema.required : [];
280
+ if (required.some(key => key !== argument && !SAFE_SEARCH_ARGUMENTS.has(key)))
281
+ return undefined;
282
+ return { name: tool.name, argument };
283
+ };
284
+ if (preferred) {
285
+ const exact = tools.find(tool => tool.name === preferred);
286
+ if (exact && (SEARCH_TOOL_NAME.test(exact.name) || SEARCH_TOOL_DESCRIPTION.test(String(exact.description || '')))) {
287
+ return safeTool(exact);
288
+ }
289
+ }
290
+ for (const tool of tools) {
291
+ if (!SEARCH_TOOL_NAME.test(tool.name) && !SEARCH_TOOL_DESCRIPTION.test(String(tool.description || '')))
292
+ continue;
293
+ const selected = safeTool(tool);
294
+ if (selected)
295
+ return selected;
296
+ }
297
+ return undefined;
298
+ }
299
+ function writeSearchMcpHealthSnapshot(root, result, query) {
300
+ const target = path.join(root, SEARCH_MCP_HEALTH_FILE);
301
+ const temporary = `${target}.${process.pid}.${Math.random().toString(16).slice(2)}.tmp`;
302
+ const attempts = result.attempts.map(attempt => ({
303
+ ...attempt,
304
+ error: attempt.error ? cleanError(attempt.error, [root]) : undefined,
305
+ }));
306
+ const payload = JSON.stringify({
307
+ version: 1,
308
+ invocationId: result.invocationId,
309
+ checkedAt: result.checkedAt,
310
+ querySha256: (0, crypto_1.createHash)('sha256').update(query, 'utf8').digest('hex'),
311
+ ok: result.ok,
312
+ provider: result.provider || '',
313
+ attempts,
314
+ }, null, 2);
315
+ try {
316
+ fs.mkdirSync(root, { recursive: true });
317
+ fs.writeFileSync(temporary, payload, { encoding: 'utf8', mode: 0o600 });
318
+ fs.renameSync(temporary, target);
319
+ }
320
+ catch {
321
+ try {
322
+ fs.rmSync(temporary, { force: true });
323
+ }
324
+ catch { }
325
+ }
326
+ }
327
+ async function callRealEndpoint(endpoint, query, signal) {
328
+ if (endpoint.transport === 'template')
329
+ throw new Error('endpoint has no configured transport');
330
+ const timeout = Math.max(1_000, Math.min(MAX_TIMEOUT_MS, endpoint.timeoutMs || DEFAULT_TIMEOUT_MS));
331
+ const requestInit = endpoint.headers ? { headers: endpoint.headers } : undefined;
332
+ const transport = endpoint.transport === 'stdio'
333
+ ? new stdio_js_1.StdioClientTransport({
334
+ command: endpoint.command || '', args: endpoint.args || [], cwd: endpoint.cwd,
335
+ env: { ...process.env, ...(endpoint.env || {}) }, stderr: 'pipe',
336
+ })
337
+ : endpoint.transport === 'sse'
338
+ ? new sse_js_1.SSEClientTransport(new URL(endpoint.url || ''), { requestInit })
339
+ : new streamableHttp_js_1.StreamableHTTPClientTransport(new URL(endpoint.url || ''), {
340
+ requestInit,
341
+ reconnectionOptions: { initialReconnectionDelay: 250, maxReconnectionDelay: 500, reconnectionDelayGrowFactor: 1, maxRetries: 0 },
342
+ });
343
+ const client = new index_js_1.Client({ name: 'newmark-search-only', version: '0.5.13' }, { capabilities: {} });
344
+ try {
345
+ // Initialization is part of the endpoint probe and must obey the same
346
+ // bounded timeout as tools/list and tools/call. Without request options a
347
+ // launched-but-unresponsive MCP can hold the complete web_search fallback
348
+ // chain at Client.connect() until the SDK's much longer default timeout.
349
+ await client.connect(transport, { signal, timeout, maxTotalTimeout: timeout });
350
+ const listed = await client.listTools(undefined, { signal, timeout, maxTotalTimeout: timeout });
351
+ const selected = chooseSearchTool(listed.tools, endpoint.tool);
352
+ if (!selected)
353
+ throw new Error('server exposes no recognized search-only tool');
354
+ const argument = endpoint.argument || selected.argument;
355
+ if (!SEARCH_QUERY_ARGUMENTS.includes(argument))
356
+ throw new Error('configured MCP query argument is outside the search-only boundary');
357
+ const selectedTool = listed.tools.find(tool => tool.name === selected.name);
358
+ if (!selectedTool || chooseSearchTool([selectedTool], selected.name)?.argument !== argument) {
359
+ throw new Error('configured MCP query argument does not match a declared string search field');
360
+ }
361
+ const result = await client.callTool({ name: selected.name, arguments: { [argument]: query } }, undefined, {
362
+ signal, timeout, maxTotalTimeout: timeout,
363
+ });
364
+ if (result.isError === true)
365
+ throw new Error(resultText(result) || 'MCP search returned isError');
366
+ return resultText(result);
367
+ }
368
+ finally {
369
+ await client.close().catch(() => undefined);
370
+ }
371
+ }
372
+ class SearchMcpPool {
373
+ root;
374
+ callEndpoint;
375
+ now;
376
+ constructor(root, dependencies = {}) {
377
+ this.root = root;
378
+ this.callEndpoint = dependencies.callEndpoint || callRealEndpoint;
379
+ this.now = dependencies.now || Date.now;
380
+ }
381
+ manifest() {
382
+ return loadSearchMcpManifest(this.root).map(endpoint => ({ ...endpoint }));
383
+ }
384
+ async search(query, signal) {
385
+ const invocationId = `search-${process.pid}-${this.now()}-${Math.random().toString(16).slice(2, 10)}`;
386
+ const checkedAt = new Date(this.now()).toISOString();
387
+ const attempts = [];
388
+ const successes = [];
389
+ for (const endpoint of this.manifest().filter(item => item.enabled)) {
390
+ if (signal?.aborted)
391
+ throw signal.reason instanceof Error ? signal.reason : new Error('Agent run aborted');
392
+ const started = this.now();
393
+ if (endpoint.transport === 'template') {
394
+ attempts.push({ id: endpoint.id, name: endpoint.name, status: 'unconfigured', durationMs: Math.max(0, this.now() - started) });
395
+ continue;
396
+ }
397
+ try {
398
+ const text = String(await this.callEndpoint(endpoint, query, signal) || '').trim().slice(0, exports.MAX_SEARCH_MCP_RESULT_CHARS);
399
+ const empty = looksLikeEmptyOrError(text);
400
+ attempts.push({ id: endpoint.id, name: endpoint.name, status: empty ? 'empty' : 'success', durationMs: Math.max(0, this.now() - started) });
401
+ if (!empty)
402
+ successes.push({ endpoint, text });
403
+ }
404
+ catch (error) {
405
+ if (signal?.aborted)
406
+ throw signal.reason instanceof Error ? signal.reason : error;
407
+ attempts.push({ id: endpoint.id, name: endpoint.name, status: 'error', durationMs: Math.max(0, this.now() - started), error: cleanError(error, [this.root, endpoint.cwd || '']) });
408
+ }
409
+ }
410
+ const selected = successes.sort((a, b) => a.endpoint.priority - b.endpoint.priority || a.endpoint.id.localeCompare(b.endpoint.id))[0];
411
+ const result = selected
412
+ ? { invocationId, checkedAt, ok: true, provider: selected.endpoint.name, text: selected.text, attempts }
413
+ : { invocationId, checkedAt, ok: false, attempts };
414
+ writeSearchMcpHealthSnapshot(this.root, result, query);
415
+ return result;
416
+ }
417
+ }
418
+ exports.SearchMcpPool = SearchMcpPool;
419
+ //# sourceMappingURL=searchMcpPool.js.map
package/dist/main.js CHANGED
@@ -129,6 +129,30 @@ const mobileServerWorkEventSubscribers = new Set();
129
129
  const browserGuestContentsByHost = new Map();
130
130
  const browserGuestBindingsByRuntime = new Map();
131
131
  const browserGuestKeyboardBridgeIds = new Set();
132
+ // visible=false Browser-Use sessions are never attached to a BrowserWindow or
133
+ // renderer DOM. Each runtime gets one main-process-owned WebContentsView whose
134
+ // WebContents preserves observe/action continuity until the run is cleared.
135
+ const backgroundBrowserViewsByRuntime = new Map();
136
+ function backgroundBrowserPartition(runtimeKey) {
137
+ const digest = (0, crypto_1.createHash)('sha256').update(String(runtimeKey || 'default')).digest('hex').slice(0, 24);
138
+ return `newmark-browser-use-background-${digest}`;
139
+ }
140
+ function releaseBackgroundBrowserWebContents(runtimeKey, expectedContentsId) {
141
+ const trustedRuntimeKey = String(runtimeKey || '').trim();
142
+ if (!trustedRuntimeKey)
143
+ return;
144
+ const view = backgroundBrowserViewsByRuntime.get(trustedRuntimeKey);
145
+ if (!view || (expectedContentsId && view.webContents.id !== expectedContentsId))
146
+ return;
147
+ backgroundBrowserViewsByRuntime.delete(trustedRuntimeKey);
148
+ const contents = view.webContents;
149
+ if (!contents.isDestroyed())
150
+ contents.close({ waitForBeforeUnload: false });
151
+ }
152
+ function releaseAllBackgroundBrowserWebContents() {
153
+ for (const runtimeKey of [...backgroundBrowserViewsByRuntime.keys()])
154
+ releaseBackgroundBrowserWebContents(runtimeKey);
155
+ }
132
156
  function browserGuestRuntimeKey(target) {
133
157
  return (0, conversationTarget_1.normalizeConversationTarget)(target).runtimeKey;
134
158
  }
@@ -296,6 +320,7 @@ function dispatchAgentWorkEvent(event, mirrorToMobile = true) {
296
320
  if ((workEvent.type === 'done' || workEvent.type === 'error') && workEvent.runtimeKey) {
297
321
  browserUseEngine?.clearRuntime(workEvent.runtimeKey);
298
322
  electronBrowserUseHost?.clear({ runtimeKey: workEvent.runtimeKey, owner: '' });
323
+ releaseBackgroundBrowserWebContents(workEvent.runtimeKey);
299
324
  }
300
325
  for (const win of electron_1.BrowserWindow.getAllWindows()) {
301
326
  if (win.isDestroyed())
@@ -952,10 +977,54 @@ async function ensureBrowserWebContents(boundContentsId, runtimeKey) {
952
977
  host.send('browser:ensureGuest', runtimeKey ? { runtimeKey } : undefined);
953
978
  return await waitForRegisteredBrowserGuest(host, runtimeKey, 12_000);
954
979
  }
980
+ async function ensureBackgroundBrowserWebContents(boundContentsId, runtimeKey) {
981
+ const trustedRuntimeKey = String(runtimeKey || '').trim();
982
+ if (!trustedRuntimeKey)
983
+ throw new Error('Background Browser-Use requires a runtimeKey');
984
+ if (boundContentsId) {
985
+ const bound = electron_1.webContents.fromId(boundContentsId);
986
+ const registered = backgroundBrowserViewsByRuntime.get(trustedRuntimeKey)?.webContents;
987
+ if (bound && registered && !bound.isDestroyed() && registered.id === bound.id)
988
+ return bound;
989
+ }
990
+ const existing = backgroundBrowserViewsByRuntime.get(trustedRuntimeKey);
991
+ if (existing && !existing.webContents.isDestroyed())
992
+ return existing.webContents;
993
+ if (existing)
994
+ backgroundBrowserViewsByRuntime.delete(trustedRuntimeKey);
995
+ const view = new electron_1.WebContentsView({
996
+ webPreferences: {
997
+ partition: backgroundBrowserPartition(trustedRuntimeKey),
998
+ contextIsolation: true,
999
+ nodeIntegration: false,
1000
+ sandbox: true,
1001
+ javascript: true,
1002
+ allowRunningInsecureContent: false,
1003
+ backgroundThrottling: false,
1004
+ },
1005
+ });
1006
+ const contents = view.webContents;
1007
+ backgroundBrowserViewsByRuntime.set(trustedRuntimeKey, view);
1008
+ contents.on('will-prevent-unload', event => event.preventDefault());
1009
+ contents.once('destroyed', () => {
1010
+ if (backgroundBrowserViewsByRuntime.get(trustedRuntimeKey)?.webContents.id === contents.id) {
1011
+ backgroundBrowserViewsByRuntime.delete(trustedRuntimeKey);
1012
+ }
1013
+ });
1014
+ ensureElectronBrowserUseHost().attach(contents);
1015
+ await contents.loadURL('about:blank');
1016
+ return contents;
1017
+ }
955
1018
  function ensureElectronBrowserUseHost() {
956
1019
  if (!electronBrowserUseHost) {
957
1020
  electronBrowserUseHost = new electronBrowserUseHost_1.ElectronBrowserUseHost({
958
- resolveContents: async (scope, boundContentsId) => await ensureBrowserWebContents(boundContentsId, scope.runtimeKey),
1021
+ resolveContents: async (scope, boundContentsId) => scope.visible === false
1022
+ ? await ensureBackgroundBrowserWebContents(boundContentsId, scope.runtimeKey)
1023
+ : await ensureBrowserWebContents(boundContentsId, scope.runtimeKey),
1024
+ releaseContents: (scope, contents) => {
1025
+ if (scope.visible === false)
1026
+ releaseBackgroundBrowserWebContents(scope.runtimeKey, contents.id);
1027
+ },
959
1028
  openExternal: async (url) => { await electron_1.shell.openExternal(url); },
960
1029
  });
961
1030
  }
@@ -2660,6 +2729,7 @@ else {
2660
2729
  browserUseEngine = null;
2661
2730
  electronBrowserUseHost?.dispose();
2662
2731
  electronBrowserUseHost = null;
2732
+ releaseAllBackgroundBrowserWebContents();
2663
2733
  browserControl_1.BrowserControl.setBackend(null);
2664
2734
  browserGuestContentsByHost.clear();
2665
2735
  browserGuestBindingsByRuntime.clear();
@@ -2787,6 +2857,7 @@ else {
2787
2857
  cancelBrowserUseTarget: runtimeKey => {
2788
2858
  browserUseEngine?.clearRuntime(runtimeKey);
2789
2859
  electronBrowserUseHost?.clear({ runtimeKey, owner: '' });
2860
+ releaseBackgroundBrowserWebContents(runtimeKey);
2790
2861
  },
2791
2862
  runAutomation: async (tool, payload, signal) => {
2792
2863
  if (!agent)
package/dist/server.js CHANGED
@@ -56,6 +56,7 @@ const nativeBash_1 = require("./core/nativeBash");
56
56
  const installUpdate_1 = require("./core/installUpdate");
57
57
  const mobilePairing_1 = require("./core/mobilePairing");
58
58
  const workEventCoalescer_1 = require("./core/workEventCoalescer");
59
+ const searchMcpPool_1 = require("./core/searchMcpPool");
59
60
  const PORT = 47890;
60
61
  let agent = null;
61
62
  let automation = null;
@@ -995,6 +996,40 @@ async function handleApi(req, res, body) {
995
996
  });
996
997
  return;
997
998
  }
999
+ case '/api/mobile/web-search': {
1000
+ const input = JSON.parse(body || '{}');
1001
+ const query = String(input.query || '').trim();
1002
+ if (!query) {
1003
+ mobileJson(res, { ok: false, error: 'query is required' }, 400);
1004
+ return;
1005
+ }
1006
+ if (query.length > 2_000) {
1007
+ mobileJson(res, { ok: false, error: 'query is too long' }, 400);
1008
+ return;
1009
+ }
1010
+ const result = await agent.tools.webSearchDetailed(query);
1011
+ mobileJson(res, result);
1012
+ return;
1013
+ }
1014
+ case '/api/mobile/web-search-mcp': {
1015
+ const input = JSON.parse(body || '{}');
1016
+ const query = String(input.query || '').trim();
1017
+ if (!query) {
1018
+ mobileJson(res, { ok: false, error: 'query is required' }, 400);
1019
+ return;
1020
+ }
1021
+ if (query.length > 2_000) {
1022
+ mobileJson(res, { ok: false, error: 'query is too long' }, 400);
1023
+ return;
1024
+ }
1025
+ const result = await agent.tools.webSearchMcpOnly(query);
1026
+ mobileJson(res, result);
1027
+ return;
1028
+ }
1029
+ case '/api/mobile/search-mcp-manifest': {
1030
+ mobileJson(res, { version: 1, endpoints: (0, searchMcpPool_1.publicSearchMcpManifest)(appRoot) });
1031
+ return;
1032
+ }
998
1033
  case '/api/mobile/state': {
999
1034
  const active = agent.getConversationSnapshot(agent.activeConversationId, { window: 200 });
1000
1035
  // 完整对话信息:workRuns 用持久化记录透出(含被中断的构建,不依赖运行时内存)
@@ -3,6 +3,7 @@ import { NewmarkToolDefinition, NewmarkToolResult } from '../core/compat';
3
3
  import { SshManager } from '../core/ssh';
4
4
  import { WorkspaceManager } from '../core/workspace';
5
5
  import { LocalOcrResult } from '../core/localOcr';
6
+ import { SearchMcpAttempt } from '../core/searchMcpPool';
6
7
  export interface ToolExecutionContext {
7
8
  mode?: string;
8
9
  workspacePath?: string;
@@ -21,6 +22,14 @@ export interface ToolHostProfile {
21
22
  electronBrowser: boolean;
22
23
  windowsComputerUse: boolean;
23
24
  }
25
+ export interface WebSearchResult {
26
+ invocationId: string;
27
+ checkedAt: string;
28
+ ok: boolean;
29
+ provider: string;
30
+ text: string;
31
+ attempts: SearchMcpAttempt[];
32
+ }
24
33
  export declare class ToolExecutor {
25
34
  private config;
26
35
  private ssh?;
@@ -28,11 +37,17 @@ export declare class ToolExecutor {
28
37
  private root;
29
38
  private readonly localOcr;
30
39
  private readonly argumentValidators;
40
+ private readonly searchMcpPool;
31
41
  private hostProfile;
32
42
  constructor(root: string, config: ConfigManager, ssh?: SshManager | undefined, workspace?: WorkspaceManager | undefined);
33
- webSearch(query: string): Promise<string>;
43
+ webSearch(query: string, signal?: AbortSignal): Promise<string>;
44
+ webSearchDetailed(query: string, signal?: AbortSignal): Promise<WebSearchResult>;
45
+ /** Search the configured MCP pool without entering any HTTP fallback. */
46
+ webSearchMcpOnly(query: string, signal?: AbortSignal): Promise<WebSearchResult>;
34
47
  /** OCR entry point for the runtime's final visual fallback. */
35
48
  finalVisualFallbackOcr(dataUrl: string, signal?: AbortSignal): Promise<LocalOcrResult>;
49
+ private readPdfFile;
50
+ private extractPdfText;
36
51
  setHostProfile(profile: ToolHostProfile): void;
37
52
  definitions(mode?: string): unknown[];
38
53
  canonicalDefinitions(mode?: string): NewmarkToolDefinition[];
@@ -70,6 +85,7 @@ export declare class ToolExecutor {
70
85
  private proxyEnvironment;
71
86
  private proxyExec;
72
87
  private wsearch;
88
+ private wsearchDetailed;
73
89
  private wfetch;
74
90
  private browserRun;
75
91
  private formatBrowserResult;