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.
- package/dist/conversation-utility-host.bundle.cjs +75276 -26574
- package/dist/core/agent.d.ts +42 -17
- package/dist/core/agent.js +297 -64
- package/dist/core/browserUse.d.ts +7 -0
- package/dist/core/browserUse.js +24 -7
- package/dist/core/displayImages.d.ts +10 -0
- package/dist/core/displayImages.js +31 -8
- package/dist/core/electronBrowserUseHost.d.ts +4 -0
- package/dist/core/electronBrowserUseHost.js +32 -6
- package/dist/core/electronUtilityAgentClient.d.ts +1 -0
- package/dist/core/electronUtilityAgentClient.js +50 -3
- package/dist/core/runtimeDiagnostics.d.ts +18 -0
- package/dist/core/runtimeDiagnostics.js +89 -0
- package/dist/core/runtimeLifecycle.d.ts +7 -1
- package/dist/core/runtimeLifecycle.js +29 -0
- package/dist/core/searchMcpPool.d.ts +80 -0
- package/dist/core/searchMcpPool.js +419 -0
- package/dist/main.js +72 -1
- package/dist/server.js +35 -0
- package/dist/tools/index.d.ts +17 -1
- package/dist/tools/index.js +203 -64
- package/dist/tui/src/data.js +2 -2
- package/dist/ui/index.html +80 -17
- package/dist/wsl-agent-host.bundle.cjs +75283 -26581
- package/package.json +11 -2
|
@@ -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) =>
|
|
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 用持久化记录透出(含被中断的构建,不依赖运行时内存)
|
package/dist/tools/index.d.ts
CHANGED
|
@@ -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;
|