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:
jackwener committed 2026-03-15 07:14:07 +08:00
1 parent 1ee4919a24
commit 93f4753d1c
4 files changed
+52 -19

No files matched your search

+33 -1
View File
@@ -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();
}
}
+5 -9
View File
@@ -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]
+5 -8
View File
@@ -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
View File
@@ -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 : {};