fix: critical MCP result parsing + V2EX strategy + extension tab cleanup
- browser.ts: strip '### Ran Playwright code' suffix from MCP evaluate results to correctly parse JSON data (was causing all evaluate-based commands to fail) - browser.ts: auto-close chrome-extension:// tabs on session close - pipeline.ts: add JSON string auto-parse fallback for evaluate step - v2ex: add strategy:public + browser:false (public API, no browser needed) - v2ex: simplify to use fetch step instead of evaluate Tested: bilibili hot/search ✅, zhihu hot ✅, v2ex hot ✅, hackernews top ✅, github search ✅
This commit is contained in:
1 parent
1ee4919a24
commit
93f4753d1c
4 files changed
+52
-19
No files matched your search
+33
-1
@@ -36,7 +36,18 @@ export class Page {
|
||||
if (result?.content) {
|
||||
const textParts = result.content.filter((c: any) => c.type === 'text');
|
||||
if (textParts.length === 1) {
|
||||
const text = textParts[0].text;
|
||||
let text = textParts[0].text;
|
||||
// MCP browser_evaluate returns: "[JSON]\n### Ran Playwright code\n```js\n...\n```"
|
||||
// Strip the "### Ran Playwright code" suffix to get clean JSON
|
||||
const codeMarker = text.indexOf('### Ran Playwright code');
|
||||
if (codeMarker !== -1) {
|
||||
text = text.slice(0, codeMarker).trim();
|
||||
}
|
||||
// Also handle "### Result\n[JSON]" format (some MCP versions)
|
||||
const resultMarker = text.indexOf('### Result\n');
|
||||
if (resultMarker !== -1) {
|
||||
text = text.slice(resultMarker + '### Result\n'.length).trim();
|
||||
}
|
||||
try { return JSON.parse(text); } catch { return text; }
|
||||
}
|
||||
}
|
||||
@@ -115,6 +126,8 @@ export class PlaywrightMCP {
|
||||
private _lockAcquired = false;
|
||||
private _initialTabCount = 0;
|
||||
|
||||
private _page: Page | null = null;
|
||||
|
||||
async connect(opts: { timeout?: number } = {}): Promise<Page> {
|
||||
await this._acquireLock();
|
||||
const timeout = opts.timeout ?? CONNECT_TIMEOUT;
|
||||
@@ -137,6 +150,7 @@ export class PlaywrightMCP {
|
||||
(msg) => { if (this._proc?.stdin?.writable) this._proc.stdin.write(msg); },
|
||||
() => new Promise<any>((res) => { this._waiters.push(res); }),
|
||||
);
|
||||
this._page = page;
|
||||
|
||||
this._proc.stdout?.on('data', (chunk: Buffer) => {
|
||||
this._buffer += chunk.toString();
|
||||
@@ -185,11 +199,29 @@ export class PlaywrightMCP {
|
||||
|
||||
async close(): Promise<void> {
|
||||
try {
|
||||
// Close extension tab(s) opened during this session
|
||||
if (this._page && this._proc && !this._proc.killed) {
|
||||
try {
|
||||
const tabs = await this._page.tabs();
|
||||
const tabStr = typeof tabs === 'string' ? tabs : JSON.stringify(tabs);
|
||||
// Find and close chrome-extension:// tabs (opened by MCP Bridge)
|
||||
const tabMatches = tabStr.match(/Tab (\d+).*?chrome-extension:\/\//g);
|
||||
if (tabMatches) {
|
||||
for (const match of tabMatches) {
|
||||
const idx = match.match(/Tab (\d+)/)?.[1];
|
||||
if (idx) {
|
||||
try { await this._page.closeTab(parseInt(idx)); } catch {}
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch {}
|
||||
}
|
||||
if (this._proc && !this._proc.killed) {
|
||||
this._proc.kill('SIGTERM');
|
||||
await new Promise<void>((res) => { this._proc?.on('exit', () => res()); setTimeout(res, 3000); });
|
||||
}
|
||||
} finally {
|
||||
this._page = null;
|
||||
this._releaseLock();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,6 +2,8 @@ site: v2ex
|
||||
name: hot
|
||||
description: V2EX 热门话题
|
||||
domain: www.v2ex.com
|
||||
strategy: public
|
||||
browser: false
|
||||
|
||||
args:
|
||||
limit:
|
||||
@@ -10,20 +12,14 @@ args:
|
||||
description: Number of topics
|
||||
|
||||
pipeline:
|
||||
- evaluate: |
|
||||
(async () => {
|
||||
const res = await fetch('https://www.v2ex.com/api/topics/hot.json');
|
||||
return await res.json();
|
||||
})()
|
||||
- fetch:
|
||||
url: https://www.v2ex.com/api/topics/hot.json
|
||||
|
||||
- map:
|
||||
rank: ${{ index + 1 }}
|
||||
title: ${{ item.title }}
|
||||
node: ${{ item.node?.title }}
|
||||
author: ${{ item.member?.username }}
|
||||
replies: ${{ item.replies }}
|
||||
url: ${{ item.url }}
|
||||
|
||||
- limit: ${{ args.limit }}
|
||||
|
||||
columns: [rank, title, node, author, replies]
|
||||
columns: [rank, title, replies]
|
||||
@@ -2,6 +2,8 @@ site: v2ex
|
||||
name: latest
|
||||
description: V2EX 最新话题
|
||||
domain: www.v2ex.com
|
||||
strategy: public
|
||||
browser: false
|
||||
|
||||
args:
|
||||
limit:
|
||||
@@ -10,19 +12,14 @@ args:
|
||||
description: Number of topics
|
||||
|
||||
pipeline:
|
||||
- evaluate: |
|
||||
(async () => {
|
||||
const res = await fetch('https://www.v2ex.com/api/topics/latest.json');
|
||||
return await res.json();
|
||||
})()
|
||||
- fetch:
|
||||
url: https://www.v2ex.com/api/topics/latest.json
|
||||
|
||||
- map:
|
||||
rank: ${{ index + 1 }}
|
||||
title: ${{ item.title }}
|
||||
node: ${{ item.node?.title }}
|
||||
author: ${{ item.member?.username }}
|
||||
replies: ${{ item.replies }}
|
||||
|
||||
- limit: ${{ args.limit }}
|
||||
|
||||
columns: [rank, title, node, author, replies]
|
||||
columns: [rank, title, replies]
|
||||
+9
-1
@@ -143,7 +143,15 @@ async function executeStep(page: any, op: string, params: any, data: any, args:
|
||||
}
|
||||
case 'evaluate': {
|
||||
const js = String(render(params, { args, data }));
|
||||
return page.evaluate(normalizeEvaluateSource(js));
|
||||
let result = await page.evaluate(normalizeEvaluateSource(js));
|
||||
// MCP may return JSON as a string — auto-parse it
|
||||
if (typeof result === 'string') {
|
||||
const trimmed = result.trim();
|
||||
if ((trimmed.startsWith('[') && trimmed.endsWith(']')) || (trimmed.startsWith('{') && trimmed.endsWith('}'))) {
|
||||
try { result = JSON.parse(trimmed); } catch {}
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
case 'snapshot': {
|
||||
const opts = (typeof params === 'object' && params) ? params : {};
|
||||
|
||||
Reference in new issue
Block a user