chrome-control-proxy 1.0.0 → 1.0.2

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/README.md CHANGED
@@ -26,6 +26,43 @@ ccp status # 需先启动服务,否则显示 unreachable
26
26
 
27
27
  ---
28
28
 
29
+ ## 发布
30
+
31
+ 升级版本号:
32
+
33
+ ```bash
34
+ npm run release:patch
35
+ # 或
36
+ npm run release:minor
37
+ # 或
38
+ npm run release:major
39
+ ```
40
+
41
+ 说明:
42
+
43
+ - 会同步更新 `package.json` 与 `chrome-control-proxy/SKILL.md` 的版本号
44
+ - 不会自动修改 `CHANGELOG.md`,升级后按实际改动手动补充
45
+
46
+ 发布到 npm:
47
+
48
+ ```bash
49
+ npm run publish:npm
50
+ ```
51
+
52
+ 发布到 ClawHub:
53
+
54
+ ```bash
55
+ npm run publish:clawhub
56
+ ```
57
+
58
+ 说明:
59
+
60
+ - `publish:npm` 等价于执行 `npm publish`
61
+ - `publish:clawhub` 会打开 [ClawHub 导入页](https://clawhub.ai/import),并在终端输出仓库地址与 Skill 目录:`chrome-control-proxy/`
62
+ - 发布 ClawHub Skill 时,仓库地址使用:`https://github.com/zhengxiangqi/chrome-control-proxy`
63
+
64
+ ---
65
+
29
66
  ## CLI:`ccp`
30
67
 
31
68
  管理 **HTTP 服务进程**(`node index.js`),不是替代 `curl` 调接口。
@@ -87,9 +124,10 @@ node index.js
87
124
  |------|------|------|
88
125
  | `/playwright/status` | GET | CDP 是否可连 |
89
126
  | `/playwright/page-dom` | POST | 页面快照:HTML / innerText / a11y / **Playwright 专用可交互列表** |
127
+ | `/playwright/pipeline` | POST | 单次请求串联导航、前后快照与脚本执行 |
90
128
  | `/playwright/run` | POST | 在 VM 中执行用户脚本(注入 `page`、`context`、`browser`) |
91
129
 
92
- `POST /playwright/page-dom` 与 `POST /playwright/run` 在服务端 **串行排队**,避免多请求抢同一浏览器。
130
+ `POST /playwright/page-dom`、`POST /playwright/pipeline` 与 `POST /playwright/run` 在服务端 **串行排队**,避免多请求抢同一浏览器。
93
131
 
94
132
  ---
95
133
 
@@ -113,13 +151,24 @@ curl -s http://host.docker.internal:3333/health
113
151
 
114
152
  ## `POST /playwright/page-dom` 要点
115
153
 
154
+ - 默认 `waitUntil` 为 **`domcontentloaded`**,更适合页面分析;若必须等待完整资源再改成 `load` / `networkidle`。
116
155
  - **`includeHtml: false`**:不返回整页 HTML,利于降 token。
117
156
  - **`includePlaywrightSnapshot: true`**:返回 `playwright.targets[]`(含 **`suggestedLocator`**),便于生成脚本。
118
- - **`selector`**:只截取子树。
157
+ - **`playwrightSnapshotMode: "compact"`**:只返回更精简的可交互信息,适合先给 OpenClaw 做定位分析。
158
+ - **`selector`**:只截取子树,优先限制到主内容区或弹窗根节点。
119
159
  - **`includeInnerText` / `includeAccessibility`**:按需打开。
120
160
 
121
161
  ---
122
162
 
163
+ ## `POST /playwright/pipeline` 要点
164
+
165
+ - 用于一次请求内完成:**导航 -> 分析前快照 -> 执行脚本 -> 分析后快照**。
166
+ - 适合“操作后还要再拿页面结构”的场景,减少额外 HTTP 往返。
167
+ - 外层支持 `url` / `waitUntil` / `timeout` / `target`;快照部分用 `beforePageDom`、`afterPageDom` 传入,结构与 `page-dom` 参数基本一致。
168
+ - 若只需要执行脚本,不必改成 `pipeline`,继续用 `/playwright/run` 即可。
169
+
170
+ ---
171
+
123
172
  ## `POST /playwright/run` 要点
124
173
 
125
174
  - 脚本为 **async 函数体**,可 `await`、`return`;返回值会尽量 JSON 序列化后返回。
@@ -138,4 +187,8 @@ curl -s http://host.docker.internal:3333/health
138
187
 
139
188
  ## OpenClaw 集成说明
140
189
 
141
- 更完整的调用顺序、踩坑说明见仓库内 **`host-chrome-control/SKILL.md`**(随代码维护;**不会**打进 `npm pack` 的默认包文件,clone 仓库即可见)。
190
+ 更完整的调用顺序、踩坑说明见仓库内 **`chrome-control-proxy/SKILL.md`**(随代码维护;**不会**打进 `npm pack` 的默认包文件,clone 仓库即可见)。
191
+
192
+ ### 通过 ClawHub 安装 Skill
193
+
194
+ 在 [ClawHub](https://clawhub.ai) 搜索 **`chrome-control-proxy`** 并一键安装,OpenClaw 即可加载该 Skill,获得完整的调用指南与最佳实践提示。
package/index.js CHANGED
@@ -13,6 +13,7 @@ const {
13
13
  enqueuePlaywright,
14
14
  getPageDomPayload,
15
15
  runPlaywrightUserScript,
16
+ runPlaywrightPipeline,
16
17
  packScriptReturnValue,
17
18
  } = require('./lib/playwright-controller');
18
19
 
@@ -131,7 +132,7 @@ app.post('/playwright/page-dom', async (req, res) => {
131
132
  const payload = await enqueuePlaywright(() =>
132
133
  getPageDomPayload({
133
134
  url: b.url,
134
- waitUntil: b.waitUntil || 'load',
135
+ waitUntil: b.waitUntil || 'domcontentloaded',
135
136
  timeout: b.timeout ?? 30000,
136
137
  target: b.target || 'first',
137
138
  maxHtmlChars: b.maxHtmlChars,
@@ -144,6 +145,7 @@ app.post('/playwright/page-dom', async (req, res) => {
144
145
  includeAccessibility: Boolean(b.includeAccessibility),
145
146
  includePlaywrightSnapshot: Boolean(b.includePlaywrightSnapshot),
146
147
  maxPlaywrightTargets: b.maxPlaywrightTargets,
148
+ playwrightSnapshotMode: b.playwrightSnapshotMode,
147
149
  }),
148
150
  );
149
151
  res.json({
@@ -156,6 +158,77 @@ app.post('/playwright/page-dom', async (req, res) => {
156
158
  }
157
159
  });
158
160
 
161
+ app.post('/playwright/pipeline', async (req, res) => {
162
+ try {
163
+ const b = req.body || {};
164
+ const hasScript = typeof b.script === 'string' && b.script !== '';
165
+ const hasBefore = Boolean(b.beforePageDom);
166
+ const hasAfter = Boolean(b.afterPageDom);
167
+ if (!hasScript && !hasBefore && !hasAfter) {
168
+ log.warn('http', 'POST /playwright/pipeline rejected: empty pipeline');
169
+ return res.status(400).json({
170
+ ok: false,
171
+ error: 'pipeline requires at least one of beforePageDom, script, afterPageDom',
172
+ });
173
+ }
174
+ log.info('http', 'POST /playwright/pipeline', {
175
+ hasScript,
176
+ hasBefore,
177
+ hasAfter,
178
+ });
179
+ const payload = await enqueuePlaywright(() =>
180
+ runPlaywrightPipeline({
181
+ url: b.url,
182
+ waitUntil: b.waitUntil || 'domcontentloaded',
183
+ timeout: b.timeout ?? 30000,
184
+ target: b.target || 'first',
185
+ script: hasScript ? String(b.script) : undefined,
186
+ scriptTimeout: b.scriptTimeout ?? PLAYWRIGHT_RUN_DEFAULT_MS,
187
+ beforePageDom: hasBefore
188
+ ? {
189
+ ...b.beforePageDom,
190
+ waitUntil: undefined,
191
+ url: undefined,
192
+ timeout: b.beforePageDom.timeout ?? b.timeout ?? 30000,
193
+ includeHtml: b.beforePageDom.includeHtml !== false,
194
+ includeInnerText: Boolean(b.beforePageDom.includeInnerText),
195
+ includeAccessibility: Boolean(b.beforePageDom.includeAccessibility),
196
+ includePlaywrightSnapshot: Boolean(b.beforePageDom.includePlaywrightSnapshot),
197
+ }
198
+ : null,
199
+ afterPageDom: hasAfter
200
+ ? {
201
+ ...b.afterPageDom,
202
+ waitUntil: undefined,
203
+ url: undefined,
204
+ timeout: b.afterPageDom.timeout ?? b.timeout ?? 30000,
205
+ includeHtml: b.afterPageDom.includeHtml !== false,
206
+ includeInnerText: Boolean(b.afterPageDom.includeInnerText),
207
+ includeAccessibility: Boolean(b.afterPageDom.includeAccessibility),
208
+ includePlaywrightSnapshot: Boolean(b.afterPageDom.includePlaywrightSnapshot),
209
+ }
210
+ : null,
211
+ }),
212
+ );
213
+ res.json({
214
+ ok: true,
215
+ step: 'pipeline',
216
+ ...payload,
217
+ });
218
+ } catch (err) {
219
+ if (err.code === 'BAD_SCRIPT' || err.code === 'SCRIPT_TOO_LARGE') {
220
+ log.warn('http', `playwright/pipeline client error ${err.code}`, err.message);
221
+ return res.status(400).json({
222
+ ok: false,
223
+ error: err.message,
224
+ code: err.code,
225
+ ...(err.currentUrl && { currentUrl: err.currentUrl }),
226
+ });
227
+ }
228
+ sendPlaywrightError(res, err);
229
+ }
230
+ });
231
+
159
232
  app.post('/playwright/run', async (req, res) => {
160
233
  try {
161
234
  const { script, url, waitUntil, timeout, target, scriptTimeout } = req.body || {};
@@ -99,15 +99,20 @@ function truncateJsonValue(obj, maxChars) {
99
99
  };
100
100
  }
101
101
 
102
+ function getSnapshotMode(mode) {
103
+ return mode === 'compact' ? 'compact' : 'full';
104
+ }
105
+
102
106
  async function collectPlaywrightInteractiveSnapshot(page, options) {
103
- const { selector, maxItems } = options;
107
+ const { selector, maxItems, mode } = options;
104
108
  const maxN = Math.min(
105
109
  Math.max(1, Number(maxItems) || PLAYWRIGHT_SNAPSHOT_MAX_ITEMS),
106
110
  2000,
107
111
  );
112
+ const snapshotMode = getSnapshotMode(mode);
108
113
 
109
114
  return page.evaluate(
110
- ({ rootSelector, maxCount }) => {
115
+ ({ rootSelector, maxCount, snapshotMode }) => {
111
116
  const root = rootSelector ? document.querySelector(rootSelector) : document.body;
112
117
  if (!root) {
113
118
  return {
@@ -115,6 +120,7 @@ async function collectPlaywrightInteractiveSnapshot(page, options) {
115
120
  targets: [],
116
121
  visibleTotal: 0,
117
122
  listTruncated: false,
123
+ mode: snapshotMode,
118
124
  };
119
125
  }
120
126
 
@@ -210,8 +216,21 @@ async function collectPlaywrightInteractiveSnapshot(page, options) {
210
216
  const targets = slice.map((el) => {
211
217
  const tag = el.tagName.toLowerCase();
212
218
  const typeAttr = el.getAttribute('type');
213
- return {
219
+ const base = {
214
220
  tag,
221
+ text: (el.innerText || el.textContent || '')
222
+ .trim()
223
+ .replace(/\s+/g, ' ')
224
+ .slice(0, 160),
225
+ placeholder: el.getAttribute('placeholder'),
226
+ role: el.getAttribute('role'),
227
+ suggestedLocator: suggestLocator(el),
228
+ };
229
+ if (snapshotMode === 'compact') {
230
+ return base;
231
+ }
232
+ return {
233
+ ...base,
215
234
  type: typeAttr ? typeAttr.toLowerCase() : null,
216
235
  name: el.getAttribute('name'),
217
236
  id: el.id || null,
@@ -220,17 +239,10 @@ async function collectPlaywrightInteractiveSnapshot(page, options) {
220
239
  el.getAttribute('data-test-id') ||
221
240
  el.getAttribute('data-cy') ||
222
241
  null,
223
- placeholder: el.getAttribute('placeholder'),
224
242
  ariaLabel: el.getAttribute('aria-label'),
225
- role: el.getAttribute('role'),
226
243
  href: el.getAttribute('href') ? el.getAttribute('href').slice(0, 400) : null,
227
244
  disabled:
228
245
  el.disabled === true || String(el.getAttribute('aria-disabled')).toLowerCase() === 'true',
229
- text: (el.innerText || el.textContent || '')
230
- .trim()
231
- .replace(/\s+/g, ' ')
232
- .slice(0, 160),
233
- suggestedLocator: suggestLocator(el),
234
246
  };
235
247
  });
236
248
 
@@ -239,9 +251,10 @@ async function collectPlaywrightInteractiveSnapshot(page, options) {
239
251
  visibleTotal: visibleEls.length,
240
252
  listTruncated,
241
253
  maxCount,
254
+ mode: snapshotMode,
242
255
  };
243
256
  },
244
- { rootSelector: selector || null, maxCount: maxN },
257
+ { rootSelector: selector || null, maxCount: maxN, snapshotMode },
245
258
  );
246
259
  }
247
260
 
@@ -367,6 +380,173 @@ function packScriptReturnValue(value) {
367
380
  }
368
381
  }
369
382
 
383
+ function assertValidUserScript(userScript) {
384
+ if (typeof userScript !== 'string') {
385
+ const err = new Error('script must be a string');
386
+ err.code = 'BAD_SCRIPT';
387
+ throw err;
388
+ }
389
+ if (userScript.length > PLAYWRIGHT_RUN_MAX_SCRIPT_CHARS) {
390
+ const err = new Error(`script exceeds ${PLAYWRIGHT_RUN_MAX_SCRIPT_CHARS} characters`);
391
+ err.code = 'SCRIPT_TOO_LARGE';
392
+ throw err;
393
+ }
394
+ }
395
+
396
+ async function collectHtmlPayload(page, locator, maxHtmlChars, includeHtml) {
397
+ if (!includeHtml) {
398
+ return {
399
+ html: null,
400
+ htmlLength: null,
401
+ truncated: null,
402
+ };
403
+ }
404
+
405
+ const htmlFull = locator
406
+ ? await locator.evaluate((el) => el.outerHTML)
407
+ : await page.content();
408
+ const maxH = Math.min(
409
+ Number(maxHtmlChars) || PLAYWRIGHT_PAGE_DOM_MAX_CHARS,
410
+ PLAYWRIGHT_PAGE_DOM_MAX_CHARS,
411
+ );
412
+ const truncatedHtml = truncateStr(htmlFull, maxH);
413
+ return {
414
+ html: truncatedHtml.text,
415
+ htmlLength: htmlFull.length,
416
+ truncated: truncatedHtml.truncated,
417
+ };
418
+ }
419
+
420
+ async function collectInnerTextPayload(page, locator, maxTextChars, includeInnerText) {
421
+ if (!includeInnerText) {
422
+ return {};
423
+ }
424
+
425
+ const textFull = locator ? await locator.innerText() : await page.innerText('body');
426
+ const maxT = Math.min(
427
+ Number(maxTextChars) || PLAYWRIGHT_PAGE_DOM_MAX_CHARS,
428
+ PLAYWRIGHT_PAGE_DOM_MAX_CHARS,
429
+ );
430
+ const truncatedText = truncateStr(textFull, maxT);
431
+ return {
432
+ innerText: truncatedText.text,
433
+ innerTextLength: textFull.length,
434
+ innerTextTruncated: truncatedText.truncated,
435
+ };
436
+ }
437
+
438
+ async function collectAccessibilityPayload(page, locator, maxA11yChars, includeAccessibility) {
439
+ if (!includeAccessibility) {
440
+ return {};
441
+ }
442
+
443
+ const rootHandle = locator ? await locator.elementHandle() : null;
444
+ const snap = await page.accessibility.snapshot({ root: rootHandle || undefined });
445
+ const maxA = Math.min(
446
+ Number(maxA11yChars) || PLAYWRIGHT_PAGE_DOM_MAX_CHARS,
447
+ PLAYWRIGHT_PAGE_DOM_MAX_CHARS,
448
+ );
449
+ const packed = truncateJsonValue(snap, maxA);
450
+ if (packed.truncated) {
451
+ return {
452
+ accessibility: null,
453
+ accessibilityTruncated: true,
454
+ accessibilityJsonLength: packed.jsonLength,
455
+ accessibilityPreview: packed.preview,
456
+ };
457
+ }
458
+ return {
459
+ accessibility: packed.value,
460
+ accessibilityTruncated: false,
461
+ accessibilityJsonLength: packed.jsonLength,
462
+ };
463
+ }
464
+
465
+ async function collectPlaywrightSnapshotPayload(page, options) {
466
+ const {
467
+ selector,
468
+ maxPlaywrightTargets,
469
+ maxPlaywrightJsonChars,
470
+ includePlaywrightSnapshot,
471
+ playwrightSnapshotMode,
472
+ } = options;
473
+
474
+ if (!includePlaywrightSnapshot) {
475
+ return {};
476
+ }
477
+
478
+ const snapshot = await collectPlaywrightInteractiveSnapshot(page, {
479
+ selector,
480
+ maxItems: maxPlaywrightTargets,
481
+ mode: playwrightSnapshotMode,
482
+ });
483
+ const maxP = Math.min(
484
+ Number(maxPlaywrightJsonChars) || PLAYWRIGHT_PAGE_DOM_MAX_CHARS,
485
+ PLAYWRIGHT_PAGE_DOM_MAX_CHARS,
486
+ );
487
+ const packed = truncateJsonValue(snapshot, maxP);
488
+ if (packed.truncated) {
489
+ return {
490
+ playwright: null,
491
+ playwrightTruncated: true,
492
+ playwrightJsonLength: packed.jsonLength,
493
+ playwrightPreview: packed.preview,
494
+ };
495
+ }
496
+ return {
497
+ playwright: packed.value,
498
+ playwrightTruncated: false,
499
+ playwrightJsonLength: packed.jsonLength,
500
+ };
501
+ }
502
+
503
+ async function buildPageDomPayloadFromPage(page, options) {
504
+ const {
505
+ timeout = 30000,
506
+ maxHtmlChars = PLAYWRIGHT_PAGE_DOM_MAX_CHARS,
507
+ maxTextChars = PLAYWRIGHT_PAGE_DOM_MAX_CHARS,
508
+ maxA11yChars = PLAYWRIGHT_PAGE_DOM_MAX_CHARS,
509
+ maxPlaywrightJsonChars = PLAYWRIGHT_PAGE_DOM_MAX_CHARS,
510
+ selector,
511
+ includeHtml = true,
512
+ includeInnerText = false,
513
+ includeAccessibility = false,
514
+ includePlaywrightSnapshot = false,
515
+ maxPlaywrightTargets,
516
+ playwrightSnapshotMode = 'full',
517
+ } = options;
518
+
519
+ const locator = selector ? page.locator(selector).first() : null;
520
+ if (locator) {
521
+ await locator.waitFor({ state: 'attached', timeout });
522
+ }
523
+
524
+ const currentUrl = page.url();
525
+ const [title, htmlPayload, innerTextPayload, accessibilityPayload, snapshotPayload] = await Promise.all([
526
+ page.title(),
527
+ collectHtmlPayload(page, locator, maxHtmlChars, includeHtml),
528
+ collectInnerTextPayload(page, locator, maxTextChars, includeInnerText),
529
+ collectAccessibilityPayload(page, locator, maxA11yChars, includeAccessibility),
530
+ collectPlaywrightSnapshotPayload(page, {
531
+ selector,
532
+ maxPlaywrightTargets,
533
+ maxPlaywrightJsonChars,
534
+ includePlaywrightSnapshot,
535
+ playwrightSnapshotMode,
536
+ }),
537
+ ]);
538
+
539
+ return {
540
+ currentUrl,
541
+ title,
542
+ selector: selector || null,
543
+ ...htmlPayload,
544
+ ...innerTextPayload,
545
+ ...accessibilityPayload,
546
+ ...snapshotPayload,
547
+ };
548
+ }
549
+
370
550
  async function getPageDomPayload(options) {
371
551
  let page;
372
552
  const tStart = Date.now();
@@ -376,16 +556,12 @@ async function getPageDomPayload(options) {
376
556
  waitUntil = 'load',
377
557
  timeout = 30000,
378
558
  target = 'first',
379
- maxHtmlChars = PLAYWRIGHT_PAGE_DOM_MAX_CHARS,
380
- maxTextChars = PLAYWRIGHT_PAGE_DOM_MAX_CHARS,
381
- maxA11yChars = PLAYWRIGHT_PAGE_DOM_MAX_CHARS,
382
- maxPlaywrightJsonChars = PLAYWRIGHT_PAGE_DOM_MAX_CHARS,
383
559
  selector,
384
560
  includeHtml = true,
385
561
  includeInnerText = false,
386
562
  includeAccessibility = false,
387
563
  includePlaywrightSnapshot = false,
388
- maxPlaywrightTargets,
564
+ playwrightSnapshotMode = 'full',
389
565
  } = options;
390
566
 
391
567
  log.info('playwright', 'page-dom start', {
@@ -396,6 +572,7 @@ async function getPageDomPayload(options) {
396
572
  includeInnerText,
397
573
  includeAccessibility,
398
574
  includePlaywrightSnapshot,
575
+ playwrightSnapshotMode: getSnapshotMode(playwrightSnapshotMode),
399
576
  selector: selector || null,
400
577
  });
401
578
 
@@ -406,110 +583,7 @@ async function getPageDomPayload(options) {
406
583
  target,
407
584
  });
408
585
 
409
- const title = await page.title();
410
- const currentUrl = page.url();
411
-
412
- if (selector) {
413
- await page.locator(selector).first().waitFor({ state: 'attached', timeout });
414
- }
415
-
416
- const payload = {
417
- currentUrl,
418
- title,
419
- selector: selector || null,
420
- };
421
-
422
- if (includeHtml) {
423
- let htmlFull;
424
- if (selector) {
425
- htmlFull = await page.locator(selector).first().evaluate((el) => el.outerHTML);
426
- } else {
427
- htmlFull = await page.content();
428
- }
429
-
430
- const htmlLength = htmlFull.length;
431
- const maxH = Math.min(
432
- Number(maxHtmlChars) || PLAYWRIGHT_PAGE_DOM_MAX_CHARS,
433
- PLAYWRIGHT_PAGE_DOM_MAX_CHARS,
434
- );
435
- let html = htmlFull;
436
- let truncated = false;
437
- if (html.length > maxH) {
438
- html = html.slice(0, maxH);
439
- truncated = true;
440
- }
441
-
442
- payload.html = html;
443
- payload.htmlLength = htmlLength;
444
- payload.truncated = truncated;
445
- } else {
446
- payload.html = null;
447
- payload.htmlLength = null;
448
- payload.truncated = null;
449
- }
450
-
451
- if (includeInnerText) {
452
- let textFull;
453
- if (selector) {
454
- textFull = await page.locator(selector).first().innerText();
455
- } else {
456
- textFull = await page.innerText('body');
457
- }
458
- const maxT = Math.min(
459
- Number(maxTextChars) || PLAYWRIGHT_PAGE_DOM_MAX_CHARS,
460
- PLAYWRIGHT_PAGE_DOM_MAX_CHARS,
461
- );
462
- const tt = truncateStr(textFull, maxT);
463
- payload.innerText = tt.text;
464
- payload.innerTextLength = textFull.length;
465
- payload.innerTextTruncated = tt.truncated;
466
- }
467
-
468
- if (includeAccessibility) {
469
- let rootHandle = null;
470
- if (selector) {
471
- rootHandle = await page.locator(selector).first().elementHandle();
472
- }
473
- const snap = await page.accessibility.snapshot({ root: rootHandle || undefined });
474
- const maxA = Math.min(
475
- Number(maxA11yChars) || PLAYWRIGHT_PAGE_DOM_MAX_CHARS,
476
- PLAYWRIGHT_PAGE_DOM_MAX_CHARS,
477
- );
478
- const packed = truncateJsonValue(snap, maxA);
479
- if (packed.truncated) {
480
- payload.accessibility = null;
481
- payload.accessibilityTruncated = true;
482
- payload.accessibilityJsonLength = packed.jsonLength;
483
- payload.accessibilityPreview = packed.preview;
484
- } else {
485
- payload.accessibility = packed.value;
486
- payload.accessibilityTruncated = false;
487
- payload.accessibilityJsonLength = packed.jsonLength;
488
- }
489
- }
490
-
491
- if (includePlaywrightSnapshot) {
492
- const pw = await collectPlaywrightInteractiveSnapshot(page, {
493
- selector,
494
- maxItems: maxPlaywrightTargets,
495
- });
496
- const maxP = Math.min(
497
- Number(maxPlaywrightJsonChars) || PLAYWRIGHT_PAGE_DOM_MAX_CHARS,
498
- PLAYWRIGHT_PAGE_DOM_MAX_CHARS,
499
- );
500
- const packed = truncateJsonValue(pw, maxP);
501
- if (packed.truncated) {
502
- payload.playwright = null;
503
- payload.playwrightTruncated = true;
504
- payload.playwrightJsonLength = packed.jsonLength;
505
- payload.playwrightPreview = packed.preview;
506
- } else {
507
- payload.playwright = packed.value;
508
- payload.playwrightTruncated = false;
509
- payload.playwrightJsonLength = packed.jsonLength;
510
- }
511
- }
512
-
586
+ const payload = await buildPageDomPayloadFromPage(page, options);
513
587
  log.info('playwright', `page-dom done ${Date.now() - tStart}ms`, {
514
588
  title: payload.title,
515
589
  currentUrl: payload.currentUrl,
@@ -522,6 +596,30 @@ async function getPageDomPayload(options) {
522
596
  }
523
597
  }
524
598
 
599
+ async function executePlaywrightUserScript(browser, context, page, userScript, scriptTimeout) {
600
+ const sandbox = vm.createContext({
601
+ browser,
602
+ context,
603
+ page,
604
+ console,
605
+ setTimeout,
606
+ clearTimeout,
607
+ setInterval,
608
+ clearInterval,
609
+ });
610
+
611
+ const wrapped = `(async () => {\n${userScript}\n})()`;
612
+ const scriptVm = new vm.Script(wrapped, { filename: 'playwright-run-user.js' });
613
+ const completion = scriptVm.runInContext(sandbox, { displayErrors: true });
614
+ const run = async () => {
615
+ if (completion && typeof completion.then === 'function') {
616
+ return completion;
617
+ }
618
+ return completion;
619
+ };
620
+ return sleepRace(run(), scriptTimeout, 'playwright script');
621
+ }
622
+
525
623
  async function runPlaywrightUserScript(userScript, options) {
526
624
  let page;
527
625
  const tStart = Date.now();
@@ -534,16 +632,7 @@ async function runPlaywrightUserScript(userScript, options) {
534
632
  scriptTimeout = PLAYWRIGHT_RUN_DEFAULT_MS,
535
633
  } = options;
536
634
 
537
- if (typeof userScript !== 'string') {
538
- const err = new Error('script must be a string');
539
- err.code = 'BAD_SCRIPT';
540
- throw err;
541
- }
542
- if (userScript.length > PLAYWRIGHT_RUN_MAX_SCRIPT_CHARS) {
543
- const err = new Error(`script exceeds ${PLAYWRIGHT_RUN_MAX_SCRIPT_CHARS} characters`);
544
- err.code = 'SCRIPT_TOO_LARGE';
545
- throw err;
546
- }
635
+ assertValidUserScript(userScript);
547
636
 
548
637
  log.info('playwright', 'run script start', {
549
638
  scriptChars: userScript.length,
@@ -560,39 +649,83 @@ async function runPlaywrightUserScript(userScript, options) {
560
649
  target: target || 'first',
561
650
  });
562
651
  page = resolvedPage;
652
+ const scriptReturn = await executePlaywrightUserScript(browser, context, page, userScript, scriptTimeout);
653
+
654
+ log.info('playwright', `run script done ${Date.now() - tStart}ms`, {
655
+ currentUrl: page.url(),
656
+ hasReturn: scriptReturn !== undefined,
657
+ });
658
+ return { browser, context, page, result: scriptReturn };
659
+ } catch (err) {
660
+ err.currentUrl = err.currentUrl || tryPageUrl(page);
661
+ log.error('playwright', `run script failed ${Date.now() - tStart}ms`, err);
662
+ throw err;
663
+ }
664
+ }
665
+
666
+ async function runPlaywrightPipeline(options) {
667
+ let page;
668
+ const tStart = Date.now();
669
+ try {
670
+ const {
671
+ url,
672
+ waitUntil,
673
+ timeout,
674
+ target,
675
+ beforePageDom,
676
+ afterPageDom,
677
+ script,
678
+ scriptTimeout = PLAYWRIGHT_RUN_DEFAULT_MS,
679
+ } = options;
563
680
 
564
- const sandbox = vm.createContext({
565
- browser,
566
- context,
567
- page,
568
- console,
569
- setTimeout,
570
- clearTimeout,
571
- setInterval,
572
- clearInterval,
681
+ log.info('playwright', 'pipeline start', {
682
+ outerUrl: url || null,
683
+ waitUntil: waitUntil || 'load',
684
+ target: target || 'first',
685
+ hasBeforePageDom: Boolean(beforePageDom),
686
+ hasScript: typeof script === 'string' && script !== '',
687
+ hasAfterPageDom: Boolean(afterPageDom),
573
688
  });
574
689
 
575
- const wrapped = `(async () => {\n${userScript}\n})()`;
576
- const scriptVm = new vm.Script(wrapped, { filename: 'playwright-run-user.js' });
690
+ const { browser, context, page: resolvedPage } = await resolveBrowserContextPage({
691
+ url,
692
+ waitUntil: waitUntil || 'load',
693
+ timeout: timeout ?? 30000,
694
+ target: target || 'first',
695
+ });
696
+ page = resolvedPage;
577
697
 
578
- const completion = scriptVm.runInContext(sandbox, { displayErrors: true });
579
- const run = async () => {
580
- if (completion && typeof completion.then === 'function') {
581
- return completion;
582
- }
583
- return completion;
698
+ const response = {
699
+ currentUrl: page.url(),
584
700
  };
585
701
 
586
- const scriptReturn = await sleepRace(run(), scriptTimeout, 'playwright script');
702
+ if (beforePageDom) {
703
+ response.before = await buildPageDomPayloadFromPage(page, beforePageDom);
704
+ response.currentUrl = page.url();
705
+ }
587
706
 
588
- log.info('playwright', `run script done ${Date.now() - tStart}ms`, {
589
- currentUrl: page.url(),
590
- hasReturn: scriptReturn !== undefined,
707
+ if (typeof script === 'string' && script !== '') {
708
+ assertValidUserScript(script);
709
+ const scriptResult = await executePlaywrightUserScript(browser, context, page, script, scriptTimeout);
710
+ Object.assign(response, packScriptReturnValue(scriptResult));
711
+ response.currentUrl = page.url();
712
+ }
713
+
714
+ if (afterPageDom) {
715
+ response.after = await buildPageDomPayloadFromPage(page, afterPageDom);
716
+ response.currentUrl = page.url();
717
+ }
718
+
719
+ log.info('playwright', `pipeline done ${Date.now() - tStart}ms`, {
720
+ currentUrl: response.currentUrl,
721
+ hasBefore: Boolean(response.before),
722
+ hasAfter: Boolean(response.after),
723
+ hasReturnValue: Boolean(response.hasReturnValue),
591
724
  });
592
- return { browser, context, page, result: scriptReturn };
725
+ return response;
593
726
  } catch (err) {
594
727
  err.currentUrl = err.currentUrl || tryPageUrl(page);
595
- log.error('playwright', `run script failed ${Date.now() - tStart}ms`, err);
728
+ log.error('playwright', `pipeline failed ${Date.now() - tStart}ms`, err);
596
729
  throw err;
597
730
  }
598
731
  }
@@ -605,5 +738,6 @@ module.exports = {
605
738
  enqueuePlaywright,
606
739
  getPageDomPayload,
607
740
  runPlaywrightUserScript,
741
+ runPlaywrightPipeline,
608
742
  packScriptReturnValue,
609
743
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "chrome-control-proxy",
3
- "version": "1.0.0",
3
+ "version": "1.0.2",
4
4
  "description": "Chrome CDP control proxy: HTTP API for browser lifecycle and Playwright automation",
5
5
  "main": "index.js",
6
6
  "bin": {
@@ -17,6 +17,11 @@
17
17
  "scripts": {
18
18
  "start": "node index.js",
19
19
  "ccp": "node bin/ccp.js",
20
+ "release:patch": "node scripts/release-version.js patch",
21
+ "release:minor": "node scripts/release-version.js minor",
22
+ "release:major": "node scripts/release-version.js major",
23
+ "publish:npm": "npm publish",
24
+ "publish:clawhub": "node -e \"const url='https://clawhub.ai/import'; const repo='https://github.com/zhengxiangqi/chrome-control-proxy'; const dir='chrome-control-proxy/'; console.log('Open ClawHub import page: ' + url); console.log('Repository: ' + repo); console.log('Skill directory: ' + dir); const { spawn } = require('child_process'); const platform = process.platform; const cmd = platform === 'darwin' ? 'open' : platform === 'win32' ? 'cmd' : 'xdg-open'; const args = platform === 'win32' ? ['/c', 'start', '', url] : [url]; const child = spawn(cmd, args, { stdio: 'ignore', detached: true }); child.on('error', () => {}); child.unref();\"",
20
25
  "test": "echo \"Error: no test specified\" && exit 1"
21
26
  },
22
27
  "keywords": [