@taole/dd-playwright 0.2.0 → 0.3.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 +6 -0
- package/bin/cli.mjs +11 -0
- package/package.json +1 -1
- package/src/doctor.mjs +160 -0
- package/src/updater.mjs +9 -0
package/README.md
CHANGED
|
@@ -75,6 +75,12 @@ dd-pw version
|
|
|
75
75
|
# 一键检查并升级到最新版本:
|
|
76
76
|
dd-pw update
|
|
77
77
|
|
|
78
|
+
# 环境诊断(Node 版本 / Chromium 内核 / 账号库 / Web 服务 / 版本检查):
|
|
79
|
+
dd-pw doctor
|
|
80
|
+
|
|
81
|
+
# 诊断并自动修复安全项(当前为缺失的 Chromium 内核):
|
|
82
|
+
dd-pw doctor --fix
|
|
83
|
+
|
|
78
84
|
# (可选)前台阻塞运行(Ctrl+C 退出):
|
|
79
85
|
dd-pw serve [--port 12309]
|
|
80
86
|
```
|
package/bin/cli.mjs
CHANGED
|
@@ -9,6 +9,7 @@ import {
|
|
|
9
9
|
getStoredWebPort,
|
|
10
10
|
} from "../src/server/daemon.mjs";
|
|
11
11
|
import { getLocalVersion, handleUpdateCommand } from "../src/updater.mjs";
|
|
12
|
+
import { runDoctor, formatDoctorReport } from "../src/doctor.mjs";
|
|
12
13
|
|
|
13
14
|
const args = process.argv.slice(2);
|
|
14
15
|
const command = args[0];
|
|
@@ -23,6 +24,7 @@ dd-playwright (别名: dd-pw) CLI
|
|
|
23
24
|
dd-pw statusweb 查看后台 Web 控制台运行状态与端口
|
|
24
25
|
dd-pw serve [--port 12309] 前台阻塞启动 Web 控制台(按 Ctrl+C 退出)
|
|
25
26
|
dd-pw version, --version, -v 查看当前版本
|
|
27
|
+
dd-pw doctor [--fix] 诊断环境(Node/Chromium/数据库/Web 服务/版本)
|
|
26
28
|
dd-pw update 检查远端最新版本并自动升级
|
|
27
29
|
dd-pw mcp 以 stdio 模式启动 MCP 浏览器服务
|
|
28
30
|
dd-pw --help, -h 查看帮助信息
|
|
@@ -167,6 +169,15 @@ async function main() {
|
|
|
167
169
|
console.error(`[dd-playwright] 启动失败:`, err);
|
|
168
170
|
process.exit(1);
|
|
169
171
|
}
|
|
172
|
+
} else if (command === "doctor") {
|
|
173
|
+
const report = await runDoctor({
|
|
174
|
+
config: resolveConfig(),
|
|
175
|
+
fix: args.includes("--fix"),
|
|
176
|
+
});
|
|
177
|
+
console.log(formatDoctorReport(report));
|
|
178
|
+
// 用 exitCode 而非 process.exit():Windows 下显式退出与在途 fetch 的
|
|
179
|
+
// handle 关闭竞争会触发 uv async.c 断言崩溃
|
|
180
|
+
process.exitCode = report.ok ? 0 : 1;
|
|
170
181
|
} else if (command === "update" || command === "upgrade") {
|
|
171
182
|
await handleUpdateCommand();
|
|
172
183
|
} else if (command === "mcp") {
|
package/package.json
CHANGED
package/src/doctor.mjs
ADDED
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* dd-pw doctor:环境健康度诊断。只读检查,不启动浏览器/代理,不做修复。
|
|
3
|
+
* 每项检查返回 { name, ok, detail, hint };exit code 由汇总结果决定。
|
|
4
|
+
*/
|
|
5
|
+
import { existsSync, accessSync, constants, readFileSync } from "node:fs";
|
|
6
|
+
import { DatabaseSync } from "node:sqlite";
|
|
7
|
+
import { join } from "node:path";
|
|
8
|
+
import { getChromiumMetadata } from "./browser-check.mjs";
|
|
9
|
+
import { getLocalVersion, getRemoteLatestVersion, compareSemver } from "./updater.mjs";
|
|
10
|
+
import { getStoredWebPort, getStoredWebPid } from "./server/daemon.mjs";
|
|
11
|
+
|
|
12
|
+
const MIN_NODE = [22, 13, 0];
|
|
13
|
+
|
|
14
|
+
function check(name, fn) {
|
|
15
|
+
return Promise.resolve()
|
|
16
|
+
.then(fn)
|
|
17
|
+
.then((r) => ({ name, ok: r.ok !== false, detail: r.detail ?? "", hint: r.hint ?? "" }))
|
|
18
|
+
.catch((err) => ({ name, ok: false, detail: String(err.message || err), hint: "" }));
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function checkNodeVersion() {
|
|
22
|
+
const [maj, min, pat] = process.versions.node.split(".").map((n) => parseInt(n, 10));
|
|
23
|
+
const ok =
|
|
24
|
+
maj > MIN_NODE[0] ||
|
|
25
|
+
(maj === MIN_NODE[0] && min > MIN_NODE[1]) ||
|
|
26
|
+
(maj === MIN_NODE[0] && min === MIN_NODE[1] && pat >= MIN_NODE[2]);
|
|
27
|
+
return {
|
|
28
|
+
detail: `当前 ${process.version},要求 >= ${MIN_NODE.join(".")}`,
|
|
29
|
+
ok,
|
|
30
|
+
hint: ok ? "" : "升级 Node.js 后重试(依赖内置 node:sqlite)",
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
async function checkChromium(fix) {
|
|
35
|
+
const meta = await getChromiumMetadata();
|
|
36
|
+
if (meta.installed && meta.version) {
|
|
37
|
+
return { detail: `${meta.version} (${meta.executablePath})` };
|
|
38
|
+
}
|
|
39
|
+
const hint = "执行 npx playwright install chromium 补齐内核";
|
|
40
|
+
if (fix) {
|
|
41
|
+
const { ensureChromiumInstalled } = await import("./browser-check.mjs");
|
|
42
|
+
const done = await ensureChromiumInstalled({ silent: true });
|
|
43
|
+
if (done) {
|
|
44
|
+
const after = await getChromiumMetadata();
|
|
45
|
+
return { detail: `已自动修复:${after.version} (${after.executablePath})` };
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
return { ok: false, detail: meta.error || `内核缺失:${meta.executablePath || "未知路径"}`, hint };
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function checkDataDir(config) {
|
|
52
|
+
const dir = config.dataDir;
|
|
53
|
+
if (!existsSync(dir)) {
|
|
54
|
+
return { ok: true, detail: `${dir} 尚不存在(首次使用时自动创建)` };
|
|
55
|
+
}
|
|
56
|
+
accessSync(dir, constants.R_OK | constants.W_OK);
|
|
57
|
+
return { detail: `${dir} 可读写` };
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function checkDatabase(config) {
|
|
61
|
+
const dbPath = config.dbPath;
|
|
62
|
+
if (!existsSync(dbPath)) {
|
|
63
|
+
return { ok: true, detail: `${dbPath} 不存在(未绑定账号属正常)` };
|
|
64
|
+
}
|
|
65
|
+
const db = new DatabaseSync(dbPath, { readOnly: true });
|
|
66
|
+
try {
|
|
67
|
+
const tables = db
|
|
68
|
+
.prepare("SELECT name FROM sqlite_master WHERE type='table'")
|
|
69
|
+
.all()
|
|
70
|
+
.map((r) => r.name);
|
|
71
|
+
const missing = ["diandian_accounts", "app_settings"].filter((t) => !tables.includes(t));
|
|
72
|
+
if (missing.length) {
|
|
73
|
+
return {
|
|
74
|
+
ok: false,
|
|
75
|
+
detail: `数据库缺少表:${missing.join(", ")}`,
|
|
76
|
+
hint: "数据库可能来自旧版本或已损坏;可备份后删除 runtime.db 重新扫码绑定",
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
const row = db
|
|
80
|
+
.prepare("SELECT account_id, nickname, storage_state FROM diandian_accounts WHERE is_active = 1 LIMIT 1")
|
|
81
|
+
.get();
|
|
82
|
+
if (!row) {
|
|
83
|
+
return { ok: true, detail: "数据库正常,未设置激活账号" };
|
|
84
|
+
}
|
|
85
|
+
try {
|
|
86
|
+
JSON.parse(row.storage_state);
|
|
87
|
+
return { detail: `激活账号 ${row.account_id}(${row.nickname || "未知昵称"}),storageState 可解析` };
|
|
88
|
+
} catch {
|
|
89
|
+
return {
|
|
90
|
+
ok: false,
|
|
91
|
+
detail: `激活账号 ${row.account_id} 的 storageState 无法解析`,
|
|
92
|
+
hint: "在 Web 控制台重新扫码绑定该账号",
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
} finally {
|
|
96
|
+
db.close();
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
async function checkWebDaemon(config) {
|
|
101
|
+
const pid = getStoredWebPid(config.webPidFile);
|
|
102
|
+
if (!pid) {
|
|
103
|
+
return { ok: true, detail: "后台 Web 控制台未运行(dd-pw startweb 启动)" };
|
|
104
|
+
}
|
|
105
|
+
const storedPort = getStoredWebPort(config.dataDir) || 12309;
|
|
106
|
+
try {
|
|
107
|
+
const res = await fetch(`http://127.0.0.1:${storedPort}/api/accounts`, {
|
|
108
|
+
signal: AbortSignal.timeout(2000),
|
|
109
|
+
});
|
|
110
|
+
if (res.ok) {
|
|
111
|
+
return { detail: `运行中 PID=${pid},http://127.0.0.1:${storedPort} 可访问` };
|
|
112
|
+
}
|
|
113
|
+
return { ok: false, detail: `PID=${pid} 存在但端口 ${storedPort} 响应 ${res.status}`, hint: "dd-pw stopweb 后重新 startweb" };
|
|
114
|
+
} catch {
|
|
115
|
+
return { ok: false, detail: `PID=${pid} 存在但端口 ${storedPort} 不可访问`, hint: "PID 文件可能残留,执行 dd-pw stopweb 清理后重新 startweb" };
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
async function checkVersion() {
|
|
120
|
+
const local = getLocalVersion();
|
|
121
|
+
const remote = await getRemoteLatestVersion();
|
|
122
|
+
if (!remote) {
|
|
123
|
+
return { ok: true, detail: `本地 ${local},远端版本查询失败(网络问题,不影响使用)` };
|
|
124
|
+
}
|
|
125
|
+
const cmp = compareSemver(local, remote);
|
|
126
|
+
if (cmp < 0) {
|
|
127
|
+
return { ok: false, detail: `本地 ${local} 落后于远端 ${remote}`, hint: "执行 dd-pw update 升级" };
|
|
128
|
+
}
|
|
129
|
+
return { detail: `本地 ${local} 已是最新` };
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* 执行全部诊断
|
|
134
|
+
* @param {object} options
|
|
135
|
+
* @param {object} options.config resolveConfig() 结果
|
|
136
|
+
* @param {boolean} [options.fix] 允许自动修复安全项(当前仅 Chromium 内核缺失)
|
|
137
|
+
* @returns {Promise<{ok: boolean, results: Array}>}
|
|
138
|
+
*/
|
|
139
|
+
export async function runDoctor({ config, fix = false }) {
|
|
140
|
+
const results = await Promise.all([
|
|
141
|
+
check("Node 版本", checkNodeVersion),
|
|
142
|
+
check("Chromium 内核", () => checkChromium(fix)),
|
|
143
|
+
check("数据目录", () => checkDataDir(config)),
|
|
144
|
+
check("账号数据库", () => checkDatabase(config)),
|
|
145
|
+
check("Web 控制台", () => checkWebDaemon(config)),
|
|
146
|
+
check("包版本", checkVersion),
|
|
147
|
+
]);
|
|
148
|
+
return { ok: results.every((r) => r.ok), results };
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
export function formatDoctorReport({ ok, results }) {
|
|
152
|
+
const lines = results.map((r) => {
|
|
153
|
+
const mark = r.ok ? "✔" : "✘";
|
|
154
|
+
let line = `${mark} ${r.name}: ${r.detail}`;
|
|
155
|
+
if (!r.ok && r.hint) line += `\n 建议: ${r.hint}`;
|
|
156
|
+
return line;
|
|
157
|
+
});
|
|
158
|
+
lines.push(ok ? "\n全部检查通过。" : `\n发现 ${results.filter((r) => !r.ok).length} 项问题。`);
|
|
159
|
+
return lines.join("\n");
|
|
160
|
+
}
|
package/src/updater.mjs
CHANGED
|
@@ -122,6 +122,15 @@ export async function handleUpdateCommand(overrideCurrentVersion = null) {
|
|
|
122
122
|
});
|
|
123
123
|
console.log(`\n🎉 自动升级成功!已成功升级至最新版本 v${latestVersion}。\n`);
|
|
124
124
|
|
|
125
|
+
// 升级后 Playwright 期望的 Chromium revision 可能变化,自动补齐内核,
|
|
126
|
+
// 避免下次启动 MCP 时报“浏览器二进制缺失”
|
|
127
|
+
try {
|
|
128
|
+
const { ensureChromiumInstalled } = await import("./browser-check.mjs");
|
|
129
|
+
await ensureChromiumInstalled();
|
|
130
|
+
} catch (err) {
|
|
131
|
+
console.warn(`[dd-playwright] Chromium 内核自动补齐失败: ${err.message}`);
|
|
132
|
+
console.log(`可稍后执行 dd-pw doctor --fix 重试。\n`);
|
|
133
|
+
}
|
|
125
134
|
if (wasWebRunning) {
|
|
126
135
|
console.log(`[dd-playwright] 正在自动为您重启后台 Web 服务...`);
|
|
127
136
|
const restartRes = await startWebDaemon({
|