@taole/dd-playwright 0.0.0-stage → 0.1.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 +98 -2
- package/SKILL.md +109 -0
- package/bin/cli.mjs +173 -0
- package/bin/mcp-server.mjs +416 -0
- package/package.json +30 -4
- package/src/account-store.mjs +234 -0
- package/src/auth/diandian-auth.mjs +279 -0
- package/src/browser-manager.mjs +516 -0
- package/src/config.mjs +61 -0
- package/src/server/daemon.mjs +183 -0
- package/src/server/web-server.mjs +927 -0
- package/src/whistle-proxy.mjs +232 -0
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
import { spawn } from "node:child_process";
|
|
2
|
+
import { existsSync, readFileSync, writeFileSync, unlinkSync, openSync } from "node:fs";
|
|
3
|
+
import { join } from "node:path";
|
|
4
|
+
import { fileURLToPath } from "node:url";
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* 检查指定 PID 的进程是否在运行
|
|
8
|
+
* @param {number} pid
|
|
9
|
+
* @returns {boolean}
|
|
10
|
+
*/
|
|
11
|
+
export function isProcessRunning(pid) {
|
|
12
|
+
if (!pid || typeof pid !== "number") return false;
|
|
13
|
+
try {
|
|
14
|
+
// 0 信号用于检测进程是否存在且当前用户有权限发送信号
|
|
15
|
+
process.kill(pid, 0);
|
|
16
|
+
return true;
|
|
17
|
+
} catch (err) {
|
|
18
|
+
return err.code === "EPERM"; // EPERM 表示进程存在但权限不足,也说明活着
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* 杀死指定 PID 进程(跨平台兼容)
|
|
24
|
+
* @param {number} pid
|
|
25
|
+
*/
|
|
26
|
+
export async function killProcess(pid) {
|
|
27
|
+
if (!isProcessRunning(pid)) return;
|
|
28
|
+
try {
|
|
29
|
+
if (process.platform === "win32") {
|
|
30
|
+
// Windows 下通过 taskkill 递归杀掉子进程
|
|
31
|
+
const { execSync } = await import("node:child_process");
|
|
32
|
+
execSync(`taskkill /pid ${pid} /T /F`, { stdio: "ignore" });
|
|
33
|
+
} else {
|
|
34
|
+
process.kill(pid, "SIGTERM");
|
|
35
|
+
// 等待 1 秒若未退出则强制 SIGKILL
|
|
36
|
+
let count = 0;
|
|
37
|
+
while (isProcessRunning(pid) && count < 10) {
|
|
38
|
+
await new Promise((r) => setTimeout(r, 100));
|
|
39
|
+
count++;
|
|
40
|
+
}
|
|
41
|
+
if (isProcessRunning(pid)) {
|
|
42
|
+
process.kill(pid, "SIGKILL");
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
} catch {}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* 读取已保存的 Web 后台进程 PID
|
|
50
|
+
* @param {string} pidFile
|
|
51
|
+
* @returns {number|null}
|
|
52
|
+
*/
|
|
53
|
+
export function getStoredWebPid(pidFile) {
|
|
54
|
+
if (!existsSync(pidFile)) return null;
|
|
55
|
+
try {
|
|
56
|
+
const raw = readFileSync(pidFile, "utf8").trim();
|
|
57
|
+
const pid = parseInt(raw, 10);
|
|
58
|
+
if (Number.isInteger(pid) && isProcessRunning(pid)) {
|
|
59
|
+
return pid;
|
|
60
|
+
}
|
|
61
|
+
// 已失效的 PID 文件直接清理
|
|
62
|
+
try {
|
|
63
|
+
unlinkSync(pidFile);
|
|
64
|
+
} catch {}
|
|
65
|
+
return null;
|
|
66
|
+
} catch {
|
|
67
|
+
return null;
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* 启动后台常驻 Web 服务
|
|
73
|
+
* @param {object} options
|
|
74
|
+
* @param {number} options.port
|
|
75
|
+
* @param {string} options.dataDir
|
|
76
|
+
* @param {string} options.pidFile
|
|
77
|
+
* @param {string} options.logFile
|
|
78
|
+
*/
|
|
79
|
+
export async function startWebDaemon({ port, dataDir, pidFile, logFile }) {
|
|
80
|
+
const existingPid = getStoredWebPid(pidFile);
|
|
81
|
+
if (existingPid) {
|
|
82
|
+
return {
|
|
83
|
+
started: false,
|
|
84
|
+
alreadyRunning: true,
|
|
85
|
+
pid: existingPid,
|
|
86
|
+
url: `http://127.0.0.1:${port}`,
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// CLI 脚本路径
|
|
91
|
+
const cliPath = fileURLToPath(new URL("../../bin/cli.mjs", import.meta.url));
|
|
92
|
+
|
|
93
|
+
// 打开日志输出文件
|
|
94
|
+
const outLog = openSync(logFile, "a");
|
|
95
|
+
const errLog = openSync(logFile, "a");
|
|
96
|
+
|
|
97
|
+
// 以 detached 模式 spawn node 运行 serve-daemon 命令
|
|
98
|
+
const child = spawn(
|
|
99
|
+
process.execPath,
|
|
100
|
+
[cliPath, "serve-daemon", "--port", String(port)],
|
|
101
|
+
{
|
|
102
|
+
detached: true,
|
|
103
|
+
stdio: ["ignore", outLog, errLog],
|
|
104
|
+
windowsHide: true,
|
|
105
|
+
env: {
|
|
106
|
+
...process.env,
|
|
107
|
+
},
|
|
108
|
+
},
|
|
109
|
+
);
|
|
110
|
+
|
|
111
|
+
const pid = child.pid;
|
|
112
|
+
writeFileSync(pidFile, String(pid), "utf8");
|
|
113
|
+
child.unref();
|
|
114
|
+
|
|
115
|
+
// 等待最多 5 秒检测端口服务是否就绪
|
|
116
|
+
let ready = false;
|
|
117
|
+
const deadline = Date.now() + 5000;
|
|
118
|
+
while (Date.now() < deadline) {
|
|
119
|
+
try {
|
|
120
|
+
const res = await fetch(`http://127.0.0.1:${port}/api/accounts`);
|
|
121
|
+
if (res.ok) {
|
|
122
|
+
ready = true;
|
|
123
|
+
break;
|
|
124
|
+
}
|
|
125
|
+
} catch {}
|
|
126
|
+
await new Promise((r) => setTimeout(r, 200));
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
return {
|
|
130
|
+
started: ready,
|
|
131
|
+
pid,
|
|
132
|
+
port,
|
|
133
|
+
url: `http://127.0.0.1:${port}`,
|
|
134
|
+
logFile,
|
|
135
|
+
};
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* 停止后台常驻 Web 服务
|
|
140
|
+
* @param {string} pidFile
|
|
141
|
+
*/
|
|
142
|
+
export async function stopWebDaemon(pidFile) {
|
|
143
|
+
const pid = getStoredWebPid(pidFile);
|
|
144
|
+
if (!pid) {
|
|
145
|
+
return { stopped: false, message: "当前未发现运行中的后台 Web 服务" };
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
await killProcess(pid);
|
|
149
|
+
|
|
150
|
+
try {
|
|
151
|
+
if (existsSync(pidFile)) {
|
|
152
|
+
unlinkSync(pidFile);
|
|
153
|
+
}
|
|
154
|
+
} catch {}
|
|
155
|
+
|
|
156
|
+
return { stopped: true, pid };
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/**
|
|
160
|
+
* 查询后台常驻 Web 服务运行状态
|
|
161
|
+
* @param {string} pidFile
|
|
162
|
+
* @param {number} [port]
|
|
163
|
+
*/
|
|
164
|
+
export async function getWebDaemonStatus(pidFile, port = 12309) {
|
|
165
|
+
const pid = getStoredWebPid(pidFile);
|
|
166
|
+
if (!pid) {
|
|
167
|
+
return { running: false };
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
let responding = false;
|
|
171
|
+
try {
|
|
172
|
+
const res = await fetch(`http://127.0.0.1:${port}/api/accounts`);
|
|
173
|
+
responding = res.ok;
|
|
174
|
+
} catch {}
|
|
175
|
+
|
|
176
|
+
return {
|
|
177
|
+
running: true,
|
|
178
|
+
pid,
|
|
179
|
+
port,
|
|
180
|
+
responding,
|
|
181
|
+
url: `http://127.0.0.1:${port}`,
|
|
182
|
+
};
|
|
183
|
+
}
|