chrome-control-proxy 1.0.1 → 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 +51 -2
- package/index.js +74 -1
- package/lib/playwright-controller.js +287 -153
- package/package.json +6 -1
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
|
-
- **`
|
|
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 序列化后返回。
|
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 || '
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
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
|
|
576
|
-
|
|
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
|
|
579
|
-
|
|
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
|
-
|
|
702
|
+
if (beforePageDom) {
|
|
703
|
+
response.before = await buildPageDomPayloadFromPage(page, beforePageDom);
|
|
704
|
+
response.currentUrl = page.url();
|
|
705
|
+
}
|
|
587
706
|
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
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
|
|
725
|
+
return response;
|
|
593
726
|
} catch (err) {
|
|
594
727
|
err.currentUrl = err.currentUrl || tryPageUrl(page);
|
|
595
|
-
log.error('playwright', `
|
|
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.
|
|
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": [
|