@taole/dd-playwright 0.1.13 → 0.1.14

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`。在「全局运行配置」的「软件更新」中可检查 npm 最新版本;当当前服务来自 npm 全局安装且有新版本时,可点击「立即更新」。安装完成后需手动重启 Web 服务和 MCP 客户端。源码或项目依赖运行时请按对应安装方式手动更新。
77
+ 1. 启动后在浏览器打开 `http://127.0.0.1:12309`。在「全局运行配置」的「软件更新」中可检查 npm 最新版本;发现新版本时,可在终端执行 `dd-pw update`,更新后在 Agent 或 IDE 中重载 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.13";
14
+ export const MCP_VERSION = "0.1.14";
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.13",
3
+ "version": "0.1.14",
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",
@@ -337,18 +337,7 @@ const DASHBOARD_HTML = `<!DOCTYPE html>
337
337
  </button>
338
338
  </div>
339
339
 
340
- <!-- 动态运行环境提示卡片 -->
341
- <div id="envBanner" style="padding: 12px 16px; border-radius: 6px; margin-bottom: 20px; font-size: 13px; line-height: 1.5; {{BANNER_STYLE}}">
342
- {{BANNER_CONTENT}}
343
- </div>
344
- <!-- 配置建议区块 -->
345
340
  {{CONFIG_SECTIONS}}
346
-
347
- <div style="font-size: 12px; color: var(--muted); line-height: 1.6; background: #0f172a; padding: 12px; border-radius: 6px; border: 1px solid var(--border); margin-top: 20px;">
348
- 💡 <b>配置生效小提示:</b><br>
349
- 1. 挂载后启动 AI 客户端,AI 会自动识别 <code>browser_navigate</code>、<code>browser_screenshot</code> 等 13 个完整 Playwright 工具。<br>
350
- 2. 在本页面扫码绑定的账号,MCP 会自动读取同一个 SQLite 账号库免登运行。
351
- </div>
352
341
  </div>
353
342
  </div>
354
343
 
@@ -481,10 +470,8 @@ browser_navigate({ url: "https://xdd-test.tuwan.com/", device: "desktop", userAg
481
470
 
482
471
  <div style="margin-top: 20px; padding: 16px; background: #0f172a; border-radius: 8px; border: 1px solid var(--border);">
483
472
  <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>
473
+ <div id="updateInfo" style="font-size: 12px; color: var(--muted); line-height: 1.8; margin-bottom: 12px;" aria-live="polite">点击检查更新,查看是否有新版本。</div>
485
474
  <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
475
  </div>
489
476
 
490
477
  <!-- Chromium 浏览器内核状态展示卡片 -->
@@ -786,63 +773,31 @@ browser_navigate({ url: "https://xdd-test.tuwan.com/", device: "desktop", userAg
786
773
  async function checkUpdates() {
787
774
  const info = document.getElementById('updateInfo');
788
775
  const check = document.getElementById('checkUpdateBtn');
789
- const install = document.getElementById('installUpdateBtn');
790
776
  check.disabled = true;
791
- install.style.display = 'none';
792
777
  info.textContent = '正在查询 npm 最新版本…';
793
778
  try {
794
779
  const response = await fetch('/api/update/check', { cache: 'no-store' });
795
780
  const data = await response.json();
796
781
  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';
782
+ if (!data.updateAvailable) {
783
+ info.textContent = '当前已是最新版本(v' + data.currentVersion + ')。';
784
+ } else {
785
+ info.textContent = '发现新版本:v' + data.currentVersion + ' → v' + data.latestVersion + '。请在终端执行 dd-pw update,更新后在 Agent 或 IDE 中重载 MCP。';
786
+ }
799
787
  } catch (err) {
800
788
  info.textContent = '检查更新失败:' + err.message;
801
789
  } finally { check.disabled = false; }
802
790
  }
803
791
 
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
-
837
792
  loadAccounts();
838
793
  loadSettings();
839
794
  loadChromiumMeta();
840
- checkUpdates();
841
795
  </script>
842
796
  </body>
843
797
  </html>`;
844
798
 
845
- export function createWebServer({ accountStore, authManager, updater = createWebUpdater() }) {
799
+ export function createWebServer({ accountStore, authManager, updater, port = 12309 }) {
800
+ updater ??= createWebUpdater({ port });
846
801
  const server = createServer(async (req, res) => {
847
802
  // Update endpoints are privileged even when the listener binds 0.0.0.0.
848
803
  if (req.url?.split("?")[0].startsWith("/api/update")) {
@@ -866,21 +821,6 @@ export function createWebServer({ accountStore, authManager, updater = createWeb
866
821
  if (req.method === "GET" && path === "/api/update/check") {
867
822
  try { sendUpdate(200, { ok: true, ...await updater.check() }); }
868
823
  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
824
  } else { sendUpdate(404, { ok: false, error: "Not Found" }); }
885
825
  return;
886
826
  }
@@ -899,8 +839,6 @@ export function createWebServer({ accountStore, authManager, updater = createWeb
899
839
 
900
840
  // 1. 首页控制台
901
841
  if (pathname === "/" || pathname === "/index.html") {
902
- const currentFilePath = fileURLToPath(import.meta.url).replaceAll("\\", "/");
903
- const isInstalledViaNpm = /\/node_modules\/(@taole\/)?dd-playwright\//i.test(currentFilePath);
904
842
  const serverScriptPath = fileURLToPath(
905
843
  new URL("../../bin/mcp-server.mjs", import.meta.url),
906
844
  ).replaceAll("\\", "/");
@@ -918,12 +856,9 @@ export function createWebServer({ accountStore, authManager, updater = createWeb
918
856
  2,
919
857
  );
920
858
 
921
- let bannerStyle = "background: rgba(59, 130, 246, 0.12); border: 1px solid rgba(59, 130, 246, 0.3); color: #60a5fa;";
922
- let bannerContent = `💻 <b>当前代码位置:</b><code style="background: rgba(0,0,0,0.3); padding: 2px 6px; border-radius: 4px; color: #93c5fd;">${serverScriptPath}</code><br><span style="color:#cbd5e1; font-size:12px; margin-top: 4px; display: inline-block;">使用 Node 绝对路径方式挂载 MCP,不受系统 PATH 或环境切换影响,100% 稳定运行。</span>`;
923
-
924
859
  let configSections = `
925
860
  <div class="code-title">
926
- <span>本地绝对路径配置(推荐,开箱即用)</span>
861
+ <span>MCP 配置</span>
927
862
  <button class="copy-btn" onclick="copyCode('code-mcp-local')">复制代码</button>
928
863
  </div>
929
864
  <div id="code-mcp-local" class="code-block">${localConfigStr}</div>
@@ -938,8 +873,6 @@ export function createWebServer({ accountStore, authManager, updater = createWeb
938
873
 
939
874
  const renderedHtml = DASHBOARD_HTML
940
875
  .replace("{{VERSION}}", pkgVersion)
941
- .replace("{{BANNER_STYLE}}", bannerStyle)
942
- .replace("{{BANNER_CONTENT}}", bannerContent)
943
876
  .replace("{{CONFIG_SECTIONS}}", configSections);
944
877
  sendHtml(res, renderedHtml);
945
878
  return;
@@ -1122,7 +1055,7 @@ export async function startWebServer({
1122
1055
  } = {}) {
1123
1056
  const store = accountStore || new AccountStore(dbPath);
1124
1057
  const authManager = new DiandianAuthManager({ accountStore: store });
1125
- const server = createWebServer({ accountStore: store, authManager });
1058
+ const server = createWebServer({ accountStore: store, authManager, port });
1126
1059
 
1127
1060
  return new Promise((resolve, reject) => {
1128
1061
  server.listen(port, "0.0.0.0", () => {
@@ -1,67 +1,13 @@
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";
1
+ import { compareSemver, getLocalVersion, getRemoteLatestVersion } from "../updater.mjs";
7
2
 
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 };
3
+ /** 仅检查版本,不执行安装或重启。 */
4
+ export function createWebUpdater({ getVersion = getLocalVersion, getLatest = getRemoteLatestVersion } = {}) {
36
5
  return {
37
6
  async check() {
38
7
  const currentVersion = getVersion();
39
- const [latestVersion, global] = await Promise.all([getLatest(), isGlobal()]);
8
+ const latestVersion = await getLatest();
40
9
  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
- }
10
+ return { currentVersion, latestVersion, updateAvailable: compareSemver(latestVersion, currentVersion) > 0 };
65
11
  },
66
12
  };
67
13
  }