@taole/dd-playwright 0.3.0 → 0.4.0
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 +5 -0
- package/SKILL.md +6 -0
- package/bin/cli.mjs +7 -0
- package/bin/mcp-server.mjs +16 -0
- package/package.json +1 -1
- package/src/skill-manager.mjs +80 -0
package/README.md
CHANGED
|
@@ -81,10 +81,15 @@ dd-pw doctor
|
|
|
81
81
|
# 诊断并自动修复安全项(当前为缺失的 Chromium 内核):
|
|
82
82
|
dd-pw doctor --fix
|
|
83
83
|
|
|
84
|
+
# 获取 npm 最新发布的 Skill 内容和来源版本(JSON,不写本地文件):
|
|
85
|
+
dd-pw skill get
|
|
86
|
+
|
|
84
87
|
# (可选)前台阻塞运行(Ctrl+C 退出):
|
|
85
88
|
dd-pw serve [--port 12309]
|
|
86
89
|
```
|
|
87
90
|
|
|
91
|
+
MCP 的 `skill_get_latest` 和 CLI 的 `dd-pw skill get` 获取 npm 最新发布包中的 `SKILL.md`,校验完整性后返回内容与来源版本,不安装 Skill、不修改本地文件或 MCP 配置。Agent 可按当前工具支持的路径安装;覆盖可能包含本地修改的文件前应取得用户授权。升级 dd-pw 后需重新连接 MCP 才能发现新工具;更新 Skill 后可能需要重载 Agent 或开启新会话。
|
|
92
|
+
|
|
88
93
|
1. 启动后在浏览器打开 `http://127.0.0.1:12309`。在「全局运行配置」的「软件更新」中可检查 npm 最新版本;发现新版本时,可在终端执行 `dd-pw update`,更新后在 Agent 或 IDE 中重载 MCP。源码或项目依赖运行时请按对应安装方式手动更新。
|
|
89
94
|
2. 点击右上角 **「+ 添加绑定账号」**,后台会以独立的 Playwright 实例拉起扫码页并渲染二维码。
|
|
90
95
|
3. 使用微信或点点 App 扫码登录,完成后会自动捕获 Cookie 与 `storageState`,提取点点账号 UID、昵称与头像,并落库至 `$BROWSER_MCP_DATA_DIR/runtime.db`。
|
package/SKILL.md
CHANGED
|
@@ -9,6 +9,12 @@ description: >-
|
|
|
9
9
|
|
|
10
10
|
本技能必须通过 **`dd-playwright` MCP 服务**提供的 `browser_*` 系列工具执行。
|
|
11
11
|
|
|
12
|
+
## Skill 更新
|
|
13
|
+
- 用户要求更新 dd-playwright Skill 时,调用 MCP `skill_get_latest` 获取 npm 最新发布的完整内容及版本;该工具只读取内容,不安装或修改本地文件。
|
|
14
|
+
- 根据当前工具实际支持的 Skill 路径安装;不确定当前工具、路径或作用域时询问用户,不猜测。尊重用户对 `.agents` 等目录的选择。
|
|
15
|
+
- 写入前检查现有文件并比较内容;若不一致且无法确认没有本地修改,先告知路径和覆盖风险,取得用户明确授权后再覆盖。内容一致则无需写入。
|
|
16
|
+
- 报告来源版本与实际路径;可能需要重载 Agent 或开启新会话。不修改 MCP 配置,也不升级 dd-pw。旧 MCP 缺少该工具时先升级 dd-pw 并重新连接 MCP。
|
|
17
|
+
|
|
12
18
|
## 1. 登录态与身份切换
|
|
13
19
|
- **每次导航必须显式选择身份**:默认账号使用 `identity: { mode: "default" }`;游客使用 `identity: { mode: "anonymous" }`;指定账号使用 `identity: { mode: "uid", uid: "28689781" }`。缺少身份或参数冲突会报错。
|
|
14
20
|
- **身份隔离**:相同身份导航复用上下文;切换身份会关闭当前页面并重建上下文。游客模式不注入登录态,默认账号模式使用当前激活账号或配置的凭据。
|
package/bin/cli.mjs
CHANGED
|
@@ -10,6 +10,7 @@ import {
|
|
|
10
10
|
} from "../src/server/daemon.mjs";
|
|
11
11
|
import { getLocalVersion, handleUpdateCommand } from "../src/updater.mjs";
|
|
12
12
|
import { runDoctor, formatDoctorReport } from "../src/doctor.mjs";
|
|
13
|
+
import { getLatestSkill } from "../src/skill-manager.mjs";
|
|
13
14
|
|
|
14
15
|
const args = process.argv.slice(2);
|
|
15
16
|
const command = args[0];
|
|
@@ -25,6 +26,7 @@ dd-playwright (别名: dd-pw) CLI
|
|
|
25
26
|
dd-pw serve [--port 12309] 前台阻塞启动 Web 控制台(按 Ctrl+C 退出)
|
|
26
27
|
dd-pw version, --version, -v 查看当前版本
|
|
27
28
|
dd-pw doctor [--fix] 诊断环境(Node/Chromium/数据库/Web 服务/版本)
|
|
29
|
+
dd-pw skill get 输出 npm 最新发布的 Skill 内容及版本(JSON)
|
|
28
30
|
dd-pw update 检查远端最新版本并自动升级
|
|
29
31
|
dd-pw mcp 以 stdio 模式启动 MCP 浏览器服务
|
|
30
32
|
dd-pw --help, -h 查看帮助信息
|
|
@@ -178,6 +180,11 @@ async function main() {
|
|
|
178
180
|
// 用 exitCode 而非 process.exit():Windows 下显式退出与在途 fetch 的
|
|
179
181
|
// handle 关闭竞争会触发 uv async.c 断言崩溃
|
|
180
182
|
process.exitCode = report.ok ? 0 : 1;
|
|
183
|
+
} else if (command === "skill") {
|
|
184
|
+
if (args.length !== 2 || args[1] !== "get") {
|
|
185
|
+
throw new Error("用法:dd-pw skill get");
|
|
186
|
+
}
|
|
187
|
+
console.log(JSON.stringify(await getLatestSkill(), null, 2));
|
|
181
188
|
} else if (command === "update" || command === "upgrade") {
|
|
182
189
|
await handleUpdateCommand();
|
|
183
190
|
} else if (command === "mcp") {
|
package/bin/mcp-server.mjs
CHANGED
|
@@ -9,6 +9,7 @@ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"
|
|
|
9
9
|
import * as z from "zod/v4";
|
|
10
10
|
import { resolveConfig } from "../src/config.mjs";
|
|
11
11
|
import { PlaywrightBrowserManager } from "../src/browser-manager.mjs";
|
|
12
|
+
import { getLatestSkill } from "../src/skill-manager.mjs";
|
|
12
13
|
import { pathToFileURL } from "node:url";
|
|
13
14
|
|
|
14
15
|
export const MCP_VERSION = "0.1.14";
|
|
@@ -445,6 +446,21 @@ export function createBrowserMcpServer({ manager, config }) {
|
|
|
445
446
|
});
|
|
446
447
|
},
|
|
447
448
|
);
|
|
449
|
+
server.registerTool(
|
|
450
|
+
"skill_get_latest",
|
|
451
|
+
{
|
|
452
|
+
description: "从 npm 最新发布包获取并校验 dd-playwright 的 SKILL.md,返回包名、版本及完整内容;不安装、不修改本地文件或 MCP 配置。",
|
|
453
|
+
inputSchema: {},
|
|
454
|
+
},
|
|
455
|
+
async () => {
|
|
456
|
+
try {
|
|
457
|
+
return textResult(await getLatestSkill());
|
|
458
|
+
} catch (err) {
|
|
459
|
+
return errorResult("获取 Skill 失败", err);
|
|
460
|
+
}
|
|
461
|
+
},
|
|
462
|
+
);
|
|
463
|
+
|
|
448
464
|
return server;
|
|
449
465
|
}
|
|
450
466
|
|
package/package.json
CHANGED
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import { createHash, timingSafeEqual } from 'node:crypto';
|
|
2
|
+
import { gunzipSync } from 'node:zlib';
|
|
3
|
+
|
|
4
|
+
const PACKAGE = '@taole/dd-playwright';
|
|
5
|
+
const MAX_ARCHIVE = 20 * 1024 * 1024;
|
|
6
|
+
const MAX_UNPACKED = 50 * 1024 * 1024;
|
|
7
|
+
|
|
8
|
+
async function fetchBytes(fetchImpl, url, limit) {
|
|
9
|
+
const response = await fetchImpl(url);
|
|
10
|
+
if (!response.ok) throw new Error(`Request failed (${response.status}): ${url}`);
|
|
11
|
+
const length = Number(response.headers?.get('content-length'));
|
|
12
|
+
if (length > limit) throw new Error('Download exceeds size limit');
|
|
13
|
+
// Stream when available to avoid buffering an unbounded response.
|
|
14
|
+
if (response.body?.getReader) {
|
|
15
|
+
const reader = response.body.getReader();
|
|
16
|
+
const chunks = []; let size = 0;
|
|
17
|
+
try {
|
|
18
|
+
for (;;) {
|
|
19
|
+
const { done, value } = await reader.read();
|
|
20
|
+
if (done) break;
|
|
21
|
+
size += value.length;
|
|
22
|
+
if (size > limit) { await reader.cancel(); throw new Error('Download exceeds size limit'); }
|
|
23
|
+
chunks.push(Buffer.from(value));
|
|
24
|
+
}
|
|
25
|
+
} finally { reader.releaseLock(); }
|
|
26
|
+
return Buffer.concat(chunks, size);
|
|
27
|
+
}
|
|
28
|
+
const data = Buffer.from(await response.arrayBuffer());
|
|
29
|
+
if (data.length > limit) throw new Error('Download exceeds size limit');
|
|
30
|
+
return data;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function verifyIntegrity(data, integrity) {
|
|
34
|
+
if (typeof integrity !== 'string') throw new Error('Missing npm dist.integrity');
|
|
35
|
+
const tokens = integrity.trim().split(/\s+/).map(token => /^(sha512|sha384|sha256)-([A-Za-z0-9+/]+={0,2})(?:\?.*)?$/.exec(token)).filter(Boolean);
|
|
36
|
+
const algorithm = ['sha512', 'sha384', 'sha256'].find(name => tokens.some(token => token[1] === name));
|
|
37
|
+
if (!algorithm) throw new Error('Unsupported npm dist.integrity');
|
|
38
|
+
const actual = createHash(algorithm).update(data).digest();
|
|
39
|
+
if (!tokens.some(token => {
|
|
40
|
+
if (token[1] !== algorithm) return false;
|
|
41
|
+
const expected = Buffer.from(token[2], 'base64');
|
|
42
|
+
return actual.length === expected.length && timingSafeEqual(actual, expected);
|
|
43
|
+
})) throw new Error('npm dist.integrity mismatch');
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function extractSkill(archive) {
|
|
47
|
+
const tar = gunzipSync(archive, { maxOutputLength: MAX_UNPACKED });
|
|
48
|
+
let found = null;
|
|
49
|
+
for (let offset = 0; offset + 512 <= tar.length;) {
|
|
50
|
+
const header = tar.subarray(offset, offset + 512);
|
|
51
|
+
if (header.every(byte => byte === 0)) break;
|
|
52
|
+
const text = (start, end) => header.subarray(start, end).toString('utf8').replace(/\0.*$/, '');
|
|
53
|
+
const name = text(0, 100);
|
|
54
|
+
const prefix = text(345, 500);
|
|
55
|
+
const sizeText = text(124, 136).trim();
|
|
56
|
+
if (!/^[0-7]+$/.test(sizeText)) throw new Error('Invalid tar entry size');
|
|
57
|
+
const size = parseInt(sizeText, 8);
|
|
58
|
+
const start = offset + 512;
|
|
59
|
+
const next = start + Math.ceil(size / 512) * 512;
|
|
60
|
+
if (!Number.isSafeInteger(next) || next > tar.length) throw new Error('Truncated tar archive');
|
|
61
|
+
if ((prefix ? `${prefix}/${name}` : name) === 'package/SKILL.md') {
|
|
62
|
+
if (found || !['0', '\0'].includes(String.fromCharCode(header[156]))) throw new Error('Invalid SKILL.md tar entry');
|
|
63
|
+
found = Buffer.from(tar.subarray(start, start + size));
|
|
64
|
+
}
|
|
65
|
+
offset = next;
|
|
66
|
+
}
|
|
67
|
+
if (!found || !found.length) throw new Error('Package does not contain SKILL.md');
|
|
68
|
+
return found;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export async function getLatestSkill({ fetchImpl = globalThis.fetch, registry = 'https://registry.npmjs.org' } = {}) {
|
|
72
|
+
const metadata = JSON.parse((await fetchBytes(fetchImpl, `${registry.replace(/\/$/, '')}/${encodeURIComponent(PACKAGE)}/latest`, 2 * 1024 * 1024)).toString('utf8'));
|
|
73
|
+
if (metadata.name !== PACKAGE || typeof metadata.version !== 'string' || !metadata.version || typeof metadata.dist?.tarball !== 'string') throw new Error('Invalid npm package metadata');
|
|
74
|
+
const url = new URL(metadata.dist.tarball);
|
|
75
|
+
if (url.protocol !== 'https:' && url.hostname !== 'localhost' && url.hostname !== '127.0.0.1') throw new Error('Insecure npm tarball URL');
|
|
76
|
+
const archive = await fetchBytes(fetchImpl, url.href, MAX_ARCHIVE);
|
|
77
|
+
verifyIntegrity(archive, metadata.dist.integrity);
|
|
78
|
+
return { package: PACKAGE, version: metadata.version, content: extractSkill(archive).toString('utf8') };
|
|
79
|
+
}
|
|
80
|
+
|