@taole/dd-playwright 0.1.12 → 0.1.13

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
@@ -74,7 +74,7 @@ dd-pw update
74
74
  dd-pw serve [--port 12309]
75
75
  ```
76
76
 
77
- 1. 启动后在浏览器打开 `http://127.0.0.1:12309`。
77
+ 1. 启动后在浏览器打开 `http://127.0.0.1:12309`。在「全局运行配置」的「软件更新」中可检查 npm 最新版本;当当前服务来自 npm 全局安装且有新版本时,可点击「立即更新」。安装完成后需手动重启 Web 服务和 MCP 客户端。源码或项目依赖运行时请按对应安装方式手动更新。
78
78
  2. 点击右上角 **「+ 添加绑定账号」**,后台会以独立的 Playwright 实例拉起扫码页并渲染二维码。
79
79
  3. 使用微信或点点 App 扫码登录,完成后会自动捕获 Cookie 与 `storageState`,提取点点账号 UID、昵称与头像,并落库至 `$BROWSER_MCP_DATA_DIR/runtime.db`。
80
80
  4. 支持多账号管理、一键设为当前激活账号或解绑账号。
@@ -11,7 +11,7 @@ import { resolveConfig } from "../src/config.mjs";
11
11
  import { PlaywrightBrowserManager } from "../src/browser-manager.mjs";
12
12
  import { pathToFileURL } from "node:url";
13
13
 
14
- export const MCP_VERSION = "0.1.12";
14
+ export const MCP_VERSION = "0.1.13";
15
15
 
16
16
  function textResult(payload) {
17
17
  return {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@taole/dd-playwright",
3
- "version": "0.1.12",
3
+ "version": "0.1.13",
4
4
  "description": "Standalone stdio MCP & CLI: Playwright browser automation + isolated whistle proxy + diandian account storageState injection",
5
5
  "type": "module",
6
6
  "license": "ISC",
@@ -4,6 +4,7 @@ import { fileURLToPath } from "node:url";
4
4
  import { AccountStore } from "../account-store.mjs";
5
5
  import { DiandianAuthManager } from "../auth/diandian-auth.mjs";
6
6
  import { getChromiumMetadata } from "../browser-check.mjs";
7
+ import { createWebUpdater } from "./web-update.mjs";
7
8
 
8
9
  function sendJson(res, statusCode, payload) {
9
10
  res.writeHead(statusCode, {
@@ -478,6 +479,14 @@ browser_navigate({ url: "https://xdd-test.tuwan.com/", device: "desktop", userAg
478
479
  本服务所有数据与账号库已默认固化在用户主目录下的 <code>~/.dd-browser-mcp/runtime.db</code>,无需配置任何复杂环境变量,开箱即用。
479
480
  </div>
480
481
 
482
+ <div style="margin-top: 20px; padding: 16px; background: #0f172a; border-radius: 8px; border: 1px solid var(--border);">
483
+ <div style="font-size: 14px; font-weight: 600; margin-bottom: 8px;">软件更新</div>
484
+ <div id="updateInfo" style="font-size: 12px; color: var(--muted); line-height: 1.8; margin-bottom: 12px;" aria-live="polite">准备检查更新…</div>
485
+ <button id="checkUpdateBtn" class="btn btn-primary" onclick="checkUpdates()">检查更新</button>
486
+ <button id="installUpdateBtn" class="btn btn-primary" onclick="installUpdate()" style="display:none; margin-left: 8px;">立即更新</button>
487
+ <div style="font-size: 12px; color: var(--muted); margin-top: 10px;">更新仅针对全局安装的 npm 包。更新完成后需重启 Web 服务及 MCP 客户端;源码运行请手动更新。</div>
488
+ </div>
489
+
481
490
  <!-- Chromium 浏览器内核状态展示卡片 -->
482
491
  <div style="margin-top: 20px; padding: 16px; background: #0f172a; border-radius: 8px; border: 1px solid var(--border);">
483
492
  <div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 12px;">
@@ -774,15 +783,107 @@ browser_navigate({ url: "https://xdd-test.tuwan.com/", device: "desktop", userAg
774
783
  }
775
784
 
776
785
  // 初次加载账号、配置与内核状态
786
+ async function checkUpdates() {
787
+ const info = document.getElementById('updateInfo');
788
+ const check = document.getElementById('checkUpdateBtn');
789
+ const install = document.getElementById('installUpdateBtn');
790
+ check.disabled = true;
791
+ install.style.display = 'none';
792
+ info.textContent = '正在查询 npm 最新版本…';
793
+ try {
794
+ const response = await fetch('/api/update/check', { cache: 'no-store' });
795
+ const data = await response.json();
796
+ if (!data.ok) throw new Error(data.error || data.message || '检查失败');
797
+ info.textContent = '当前版本:' + data.currentVersion + ';最新版本:' + data.latestVersion + '。' + (data.message || '');
798
+ if (data.updateAvailable && data.canUpdate) install.style.display = 'inline-block';
799
+ } catch (err) {
800
+ info.textContent = '检查更新失败:' + err.message;
801
+ } finally { check.disabled = false; }
802
+ }
803
+
804
+ async function installUpdate() {
805
+ if (!confirm('确认更新全局安装的 dd-playwright?完成后需要重启 Web 服务和 MCP 客户端。')) return;
806
+ const info = document.getElementById('updateInfo');
807
+ const install = document.getElementById('installUpdateBtn');
808
+ const check = document.getElementById('checkUpdateBtn');
809
+ install.disabled = true;
810
+ check.disabled = true;
811
+ try {
812
+ const response = await fetch('/api/update', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: '{}' });
813
+ const data = await response.json();
814
+ if (!data.ok) throw new Error(data.error || data.message || '无法启动更新');
815
+ info.textContent = data.message || '正在更新,请稍候…';
816
+ const poll = async () => {
817
+ try {
818
+ const state = await (await fetch('/api/update/status', { cache: 'no-store' })).json();
819
+ if (!state.ok) throw new Error(state.error || '查询更新状态失败');
820
+ info.textContent = (state.message || '正在更新…') + (state.error ? ':' + state.error : '');
821
+ if (state.status === 'running') { setTimeout(poll, 1500); return; }
822
+ if (state.status === 'success') install.style.display = 'none';
823
+ install.disabled = false;
824
+ check.disabled = false;
825
+ } catch (err) {
826
+ info.textContent = '查询更新状态失败:' + err.message + ';请稍后重新检查。';
827
+ install.disabled = false; check.disabled = false;
828
+ }
829
+ };
830
+ setTimeout(poll, 1000);
831
+ } catch (err) {
832
+ info.textContent = '更新失败:' + err.message;
833
+ install.disabled = false; check.disabled = false;
834
+ }
835
+ }
836
+
777
837
  loadAccounts();
778
838
  loadSettings();
779
839
  loadChromiumMeta();
840
+ checkUpdates();
780
841
  </script>
781
842
  </body>
782
843
  </html>`;
783
844
 
784
- export function createWebServer({ accountStore, authManager }) {
845
+ export function createWebServer({ accountStore, authManager, updater = createWebUpdater() }) {
785
846
  const server = createServer(async (req, res) => {
847
+ // Update endpoints are privileged even when the listener binds 0.0.0.0.
848
+ if (req.url?.split("?")[0].startsWith("/api/update")) {
849
+ const address = req.socket.remoteAddress || "";
850
+ const local = address === "::1" || /^127(?:\.\d{1,3}){3}$/.test(address) || /^::ffff:127(?:\.\d{1,3}){3}$/i.test(address);
851
+ const host = req.headers.host;
852
+ let originOk = false;
853
+ try {
854
+ const origin = new URL(req.headers.origin);
855
+ const target = new URL(`http://${host}`);
856
+ originOk = origin.origin === target.origin && origin.username === "" && origin.password === "" && req.headers.origin === origin.origin &&
857
+ (target.hostname === "localhost" || target.hostname === "[::1]" || /^127(?:\.\d{1,3}){3}$/.test(target.hostname));
858
+ } catch {}
859
+ if (!local || (req.headers.origin && !originOk) || (req.method === "POST" && !originOk)) {
860
+ res.writeHead(403, { "Content-Type": "application/json", "Cache-Control": "no-store" });
861
+ res.end(JSON.stringify({ ok: false, error: "仅允许本机同源请求" }));
862
+ return;
863
+ }
864
+ const sendUpdate = (code, body) => { res.writeHead(code, { "Content-Type": "application/json; charset=utf-8", "Cache-Control": "no-store" }); res.end(JSON.stringify(body)); };
865
+ const path = req.url.split("?")[0];
866
+ if (req.method === "GET" && path === "/api/update/check") {
867
+ try { sendUpdate(200, { ok: true, ...await updater.check() }); }
868
+ catch (err) { sendUpdate(502, { ok: false, error: err.message }); }
869
+ } else if (req.method === "GET" && path === "/api/update/status") {
870
+ sendUpdate(200, { ok: true, ...updater.status() });
871
+ } else if (req.method === "POST" && path === "/api/update") {
872
+ if (!/^application\/json(?:\s*;\s*charset=utf-8)?\s*$/i.test(req.headers["content-type"] || "")) {
873
+ sendUpdate(415, { ok: false, error: "需要 application/json" }); return;
874
+ }
875
+ let body = "";
876
+ for await (const chunk of req) {
877
+ body += chunk;
878
+ if (body.length > 4096) { sendUpdate(413, { ok: false, error: "请求过大" }); return; }
879
+ }
880
+ try { const data = JSON.parse(body); if (!data || typeof data !== "object" || Array.isArray(data)) throw new Error(); }
881
+ catch { sendUpdate(400, { ok: false, error: "需要 JSON 对象" }); return; }
882
+ const { started, ...result } = await updater.start();
883
+ sendUpdate(started ? 202 : 409, { ok: started, ...result });
884
+ } else { sendUpdate(404, { ok: false, error: "Not Found" }); }
885
+ return;
886
+ }
786
887
  const urlObj = new URL(req.url, `http://${req.headers.host || "127.0.0.1"}`);
787
888
  const pathname = urlObj.pathname;
788
889
  // CORS preflight
@@ -0,0 +1,67 @@
1
+ import { spawn, execFile } from "node:child_process";
2
+ import { realpath } from "node:fs/promises";
3
+ import { promisify } from "node:util";
4
+ import { fileURLToPath } from "node:url";
5
+ import { join } from "node:path";
6
+ import { PACKAGE_NAME, compareSemver, getLocalVersion, getRemoteLatestVersion } from "../updater.mjs";
7
+
8
+ const execFileAsync = promisify(execFile);
9
+ const packageDir = fileURLToPath(new URL("../../", import.meta.url));
10
+
11
+ async function isGlobalInstall() {
12
+ try {
13
+ const windows = process.platform === "win32";
14
+ const { stdout } = await execFileAsync(windows ? process.env.ComSpec || "cmd.exe" : "npm", windows ? ["/d", "/s", "/c", "npm.cmd root -g"] : ["root", "-g"], { windowsHide: true, timeout: 10000 });
15
+ const [actual, global] = await Promise.all([realpath(packageDir), realpath(join(stdout.trim(), PACKAGE_NAME))]);
16
+ return windows ? actual.toLowerCase() === global.toLowerCase() : actual === global;
17
+ } catch { return false; }
18
+ }
19
+
20
+ function installGlobal() {
21
+ return new Promise((resolve, reject) => {
22
+ const windows = process.platform === "win32";
23
+ const child = spawn(windows ? process.env.ComSpec || "cmd.exe" : "npm", windows
24
+ ? ["/d", "/s", "/c", `npm.cmd install -g ${PACKAGE_NAME}@latest`]
25
+ : ["install", "-g", `${PACKAGE_NAME}@latest`],
26
+ { windowsHide: true, stdio: ["ignore", "ignore", "pipe"] });
27
+ let stderr = "";
28
+ child.stderr?.on("data", chunk => { stderr = (stderr + chunk.toString()).slice(-4000); });
29
+ child.once("error", reject);
30
+ child.once("close", (code, signal) => code === 0 ? resolve() : reject(new Error(stderr.trim() || `npm 退出: ${signal || code}`)));
31
+ });
32
+ }
33
+
34
+ export function createWebUpdater({ getVersion = getLocalVersion, getLatest = getRemoteLatestVersion, isGlobal = isGlobalInstall, install = installGlobal } = {}) {
35
+ let state = { status: "idle", error: null, message: "尚未开始更新", restartRequired: false };
36
+ return {
37
+ async check() {
38
+ const currentVersion = getVersion();
39
+ const [latestVersion, global] = await Promise.all([getLatest(), isGlobal()]);
40
+ if (!latestVersion) throw new Error("无法查询 npm 最新版本,请检查网络");
41
+ return { currentVersion, latestVersion, updateAvailable: compareSemver(latestVersion, currentVersion) > 0,
42
+ installType: global ? "global" : "local", canUpdate: global,
43
+ message: global ? "更新完成后请手动重启服务" : "当前运行的不是 npm 全局安装,更新全局包不会更新此服务,请按当前安装方式手动更新" };
44
+ },
45
+ status() { return { ...state }; },
46
+ async start() {
47
+ if (state.status === "running") return { started: false, ...state };
48
+ state = { status: "running", error: null, message: "正在确认安装位置", restartRequired: false };
49
+ try {
50
+ if (!await isGlobal()) {
51
+ state = { status: "error", error: "当前服务不是 npm 全局安装,请手动更新当前安装", message: "无法自动更新", restartRequired: false };
52
+ return { started: false, ...state };
53
+ }
54
+ state.message = "正在安装更新,完成后请手动重启服务";
55
+ Promise.resolve().then(() => install()).then(() => {
56
+ state = { status: "success", error: null, message: "安装完成,请手动重启服务以使用新版本", restartRequired: true };
57
+ }, err => {
58
+ state = { status: "error", error: String(err?.message || err), message: "安装失败,服务仍在运行", restartRequired: false };
59
+ });
60
+ return { started: true, ...state };
61
+ } catch (err) {
62
+ state = { status: "error", error: String(err?.message || err), message: "更新启动失败", restartRequired: false };
63
+ return { started: false, ...state };
64
+ }
65
+ },
66
+ };
67
+ }