@opentiny/webmcp-cli 0.0.6 → 0.0.8
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 +17 -0
- package/dist/bin.cjs +139 -219
- package/dist/bin.cjs.map +1 -1
- package/dist/bin.js +139 -219
- package/dist/bin.js.map +1 -1
- package/dist/index.cjs +101 -197
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +4 -1
- package/dist/index.d.ts +4 -1
- package/dist/index.js +101 -197
- package/dist/index.js.map +1 -1
- package/dist/inject-bundle.js +1477 -936
- package/dist/webmcp-tools/editor.csdn.net.js +199 -9
- package/dist/webmcp-tools/juejin.cn.js +24454 -8
- package/dist/webmcp-tools/my.oschina.net.js +175 -33
- package/dist/webmcp-tools/segmentfault.com.js +157 -1
- package/dist/webmcp-tools/zhuanlan.zhihu.com.js +2261 -6
- package/package.json +8 -4
package/README.md
CHANGED
|
@@ -17,6 +17,23 @@ npm install -g .
|
|
|
17
17
|
|
|
18
18
|
> **注意:** 在本地联调时,建议使用 `npm install -g .`,这会确保在你的 PATH 中生成有效的可执行文件(`webmcp-cli`)。
|
|
19
19
|
|
|
20
|
+
## 测试
|
|
21
|
+
|
|
22
|
+
统一使用 [Vitest](https://vitest.dev/)(与 `@opentiny/next-sdk` 一致):
|
|
23
|
+
|
|
24
|
+
```bash
|
|
25
|
+
# 单元测试(无浏览器依赖,已接入根目录 pnpm test)
|
|
26
|
+
pnpm test
|
|
27
|
+
|
|
28
|
+
# 浏览器 E2E(需本机 Chrome/Edge,或设置 CHROME_PATH;默认 headless)
|
|
29
|
+
pnpm build:webmcp-cli
|
|
30
|
+
pnpm test:browser
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
CI 中浏览器任务使用 [browser-actions/setup-chrome](https://github.com/browser-actions/setup-chrome) 安装 Chrome,并通过 `CHROME_PATH` + `WEBMCP_HEADLESS=1` 启动。
|
|
34
|
+
|
|
35
|
+
---
|
|
36
|
+
|
|
20
37
|
## 核心架构特性
|
|
21
38
|
|
|
22
39
|
- **后台浏览器驻留**:如果当前没有开启带有调试端口 (`9222`) 的 Chrome,CLI 会自动在后台拉起一个基于你本地 Profile 的独立 Chrome 实例。
|
package/dist/bin.cjs
CHANGED
|
@@ -26,7 +26,7 @@ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__ge
|
|
|
26
26
|
// src/bin.ts
|
|
27
27
|
var import_fs3 = __toESM(require("fs"), 1);
|
|
28
28
|
var import_path3 = __toESM(require("path"), 1);
|
|
29
|
-
var
|
|
29
|
+
var import_os2 = __toESM(require("os"), 1);
|
|
30
30
|
var import_commander = require("commander");
|
|
31
31
|
var import_picocolors2 = __toESM(require("picocolors"), 1);
|
|
32
32
|
var import_fs4 = require("fs");
|
|
@@ -223,11 +223,37 @@ var import_url2 = require("url");
|
|
|
223
223
|
var import_meta = {};
|
|
224
224
|
var __filename = (0, import_url2.fileURLToPath)(import_meta.url);
|
|
225
225
|
var __dirname = import_path2.default.dirname(__filename);
|
|
226
|
-
|
|
227
|
-
|
|
226
|
+
function getCdpPort() {
|
|
227
|
+
const raw = process.env.WEBMCP_CDP_PORT;
|
|
228
|
+
if (raw) {
|
|
229
|
+
const port = Number.parseInt(raw, 10);
|
|
230
|
+
if (Number.isFinite(port) && port > 0) return port;
|
|
231
|
+
}
|
|
232
|
+
return 9222;
|
|
233
|
+
}
|
|
228
234
|
function getWorkspaceDir() {
|
|
229
235
|
return process.env.WEBMCP_WORKSPACE || import_path2.default.join(import_os.default.homedir(), ".webmcp_chrome_profile");
|
|
230
236
|
}
|
|
237
|
+
function shouldLaunchHeadless() {
|
|
238
|
+
if (process.env.WEBMCP_HEADLESS === "0") return false;
|
|
239
|
+
return process.env.WEBMCP_HEADLESS === "1" || process.env.CI === "true" || process.env.CI === "1";
|
|
240
|
+
}
|
|
241
|
+
function readActivePortFromProfile(userDataDir) {
|
|
242
|
+
try {
|
|
243
|
+
const content = import_fs2.default.readFileSync(import_path2.default.join(userDataDir, "DevToolsActivePort"), "utf-8");
|
|
244
|
+
const port = parseInt(content.split("\n")[0].trim(), 10);
|
|
245
|
+
return Number.isFinite(port) && port > 0 ? port : null;
|
|
246
|
+
} catch {
|
|
247
|
+
return null;
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
function candidateCdpBaseUrls(port) {
|
|
251
|
+
return [
|
|
252
|
+
`http://127.0.0.1:${port}`,
|
|
253
|
+
`http://[::1]:${port}`,
|
|
254
|
+
`http://localhost:${port}`
|
|
255
|
+
];
|
|
256
|
+
}
|
|
231
257
|
function getLastTabIdFilePath() {
|
|
232
258
|
return import_path2.default.join(getWorkspaceDir(), ".last-tab-id");
|
|
233
259
|
}
|
|
@@ -321,6 +347,13 @@ async function connectPuppeteer(browserBaseUrl, attempt = 1, maxAttempts = 3) {
|
|
|
321
347
|
}
|
|
322
348
|
}
|
|
323
349
|
function getDefaultBrowserPath() {
|
|
350
|
+
const envPath = process.env.CHROME_PATH || process.env.WEBMCP_CHROME_PATH || process.env.PUPPETEER_EXECUTABLE_PATH;
|
|
351
|
+
if (envPath && import_fs2.default.existsSync(envPath)) {
|
|
352
|
+
return { path: envPath, name: "Chrome" };
|
|
353
|
+
}
|
|
354
|
+
if (envPath) {
|
|
355
|
+
console.log(import_picocolors.default.yellow(`\u6307\u5B9A\u7684\u6D4F\u89C8\u5668\u8DEF\u5F84\u4E0D\u5B58\u5728\uFF0C\u5C06\u56DE\u9000\u5230\u81EA\u52A8\u68C0\u6D4B: ${envPath}`));
|
|
356
|
+
}
|
|
324
357
|
const platform = import_os.default.platform();
|
|
325
358
|
if (platform === "darwin") {
|
|
326
359
|
const chromePath = "/Applications/Google Chrome.app/Contents/MacOS/Google Chrome";
|
|
@@ -364,16 +397,30 @@ async function startBrowserInBackground() {
|
|
|
364
397
|
if (!browserInfo || !import_fs2.default.existsSync(browserInfo.path)) {
|
|
365
398
|
throw new Error("\u65E0\u6CD5\u5728\u7CFB\u7EDF\u4E2D\u627E\u5230 Chrome \u6216 Edge \u6D4F\u89C8\u5668\u7684\u9ED8\u8BA4\u5B89\u88C5\u8DEF\u5F84\u3002");
|
|
366
399
|
}
|
|
367
|
-
|
|
368
|
-
|
|
400
|
+
const cdpPort = getCdpPort();
|
|
401
|
+
console.log(import_picocolors.default.yellow(`\u6B63\u5728\u542F\u52A8\u540E\u53F0 ${browserInfo.name} \u5B9E\u4F8B (\u7AEF\u53E3: ${cdpPort})...`));
|
|
402
|
+
const userDataDir = getWorkspaceDir();
|
|
403
|
+
if (!import_fs2.default.existsSync(userDataDir)) {
|
|
404
|
+
import_fs2.default.mkdirSync(userDataDir, { recursive: true });
|
|
405
|
+
}
|
|
406
|
+
try {
|
|
407
|
+
import_fs2.default.unlinkSync(import_path2.default.join(userDataDir, "DevToolsActivePort"));
|
|
408
|
+
} catch {
|
|
409
|
+
}
|
|
369
410
|
const launchArgs = [
|
|
370
|
-
`--remote-debugging-port=${
|
|
411
|
+
`--remote-debugging-port=${cdpPort}`,
|
|
371
412
|
`--user-data-dir=${userDataDir}`,
|
|
372
413
|
"--no-first-run",
|
|
373
414
|
"--no-default-browser-check"
|
|
374
415
|
];
|
|
375
|
-
if (
|
|
376
|
-
launchArgs.push(
|
|
416
|
+
if (shouldLaunchHeadless()) {
|
|
417
|
+
launchArgs.push(
|
|
418
|
+
"--headless=new",
|
|
419
|
+
"--no-sandbox",
|
|
420
|
+
"--disable-setuid-sandbox",
|
|
421
|
+
"--disable-gpu",
|
|
422
|
+
"--disable-dev-shm-usage"
|
|
423
|
+
);
|
|
377
424
|
}
|
|
378
425
|
const child = (0, import_child_process.spawn)(
|
|
379
426
|
browserInfo.path,
|
|
@@ -384,15 +431,22 @@ async function startBrowserInBackground() {
|
|
|
384
431
|
}
|
|
385
432
|
);
|
|
386
433
|
child.unref();
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
434
|
+
for (let i = 0; i < 40; i++) {
|
|
435
|
+
const ports = /* @__PURE__ */ new Set([cdpPort]);
|
|
436
|
+
const activePort = readActivePortFromProfile(userDataDir);
|
|
437
|
+
if (activePort) ports.add(activePort);
|
|
438
|
+
for (const port of ports) {
|
|
439
|
+
for (const baseUrl of candidateCdpBaseUrls(port)) {
|
|
440
|
+
try {
|
|
441
|
+
await httpGet(`${baseUrl}/json/version`, 1e3);
|
|
442
|
+
if (port !== cdpPort) {
|
|
443
|
+
console.log(import_picocolors.default.yellow(`\u68C0\u6D4B\u5230\u7AEF\u53E3 ${cdpPort} \u5DF2\u88AB\u5176\u4ED6\u7A0B\u5E8F\u5360\u7528\uFF0C${browserInfo.name} \u5DF2\u81EA\u52A8\u5207\u6362\u5230\u7AEF\u53E3 ${port}\u3002`));
|
|
444
|
+
}
|
|
445
|
+
console.log(import_picocolors.default.green(`${browserInfo.name} \u542F\u52A8\u5E76\u5C31\u7EEA\uFF08${baseUrl}\uFF09\u3002`));
|
|
446
|
+
await new Promise((resolve3) => setTimeout(resolve3, 500));
|
|
447
|
+
return baseUrl;
|
|
448
|
+
} catch {
|
|
449
|
+
}
|
|
396
450
|
}
|
|
397
451
|
}
|
|
398
452
|
await new Promise((resolve3) => setTimeout(resolve3, 500));
|
|
@@ -400,13 +454,16 @@ async function startBrowserInBackground() {
|
|
|
400
454
|
throw new Error(`${browserInfo.name} \u542F\u52A8\u8D85\u65F6\uFF0C\u65E0\u6CD5\u8FDE\u63A5\u5230 CDP \u7AEF\u53E3\u3002`);
|
|
401
455
|
}
|
|
402
456
|
async function connectBrowser() {
|
|
457
|
+
const userDataDir = getWorkspaceDir();
|
|
458
|
+
const cdpPort = getCdpPort();
|
|
459
|
+
const recordedPort = readActivePortFromProfile(userDataDir);
|
|
403
460
|
const versionAddresses = [
|
|
404
|
-
|
|
405
|
-
|
|
461
|
+
...recordedPort && recordedPort !== cdpPort ? candidateCdpBaseUrls(recordedPort).map((u) => `${u}/json/version`) : [],
|
|
462
|
+
...candidateCdpBaseUrls(cdpPort).map((u) => `${u}/json/version`)
|
|
406
463
|
];
|
|
407
464
|
const existingUrl = await findAvailableCdpUrl(versionAddresses, 2);
|
|
408
465
|
if (existingUrl) {
|
|
409
|
-
console.log(import_picocolors.default.yellow(`connectBrowser: \u68C0\u6D4B\u5230\u7AEF\u53E3 ${
|
|
466
|
+
console.log(import_picocolors.default.yellow(`connectBrowser: \u68C0\u6D4B\u5230\u7AEF\u53E3 ${cdpPort} \u5DF2\u5C31\u7EEA\uFF0C\u6B63\u5728\u8FDE\u63A5 ${existingUrl}...`));
|
|
410
467
|
try {
|
|
411
468
|
const browser = await connectPuppeteer(existingUrl);
|
|
412
469
|
console.log(import_picocolors.default.green(`connectBrowser: \u6210\u529F\u8FDE\u63A5 ${existingUrl}`));
|
|
@@ -418,25 +475,22 @@ async function connectBrowser() {
|
|
|
418
475
|
);
|
|
419
476
|
}
|
|
420
477
|
}
|
|
421
|
-
console.log(import_picocolors.default.yellow(`connectBrowser: \u7AEF\u53E3 ${
|
|
478
|
+
console.log(import_picocolors.default.yellow(`connectBrowser: \u7AEF\u53E3 ${cdpPort} \u65E0\u54CD\u5E94\uFF0C\u6B63\u5728\u542F\u52A8\u65B0\u7684\u6D4F\u89C8\u5668\u5B9E\u4F8B...`));
|
|
479
|
+
let launchedUrl;
|
|
422
480
|
try {
|
|
423
|
-
await startBrowserInBackground();
|
|
481
|
+
launchedUrl = await startBrowserInBackground();
|
|
424
482
|
} catch (launchError) {
|
|
425
483
|
const msg = launchError instanceof Error ? launchError.message : String(launchError);
|
|
426
484
|
console.error(import_picocolors.default.red(`\u65E0\u6CD5\u542F\u52A8\u6D4F\u89C8\u5668: ${msg}`));
|
|
427
485
|
throw new Error("Browser launch failed.");
|
|
428
486
|
}
|
|
429
|
-
const launchedUrl = await findAvailableCdpUrl(versionAddresses, 5);
|
|
430
|
-
if (!launchedUrl) {
|
|
431
|
-
throw new Error(`\u65E0\u6CD5\u8FDE\u63A5\u5230\u6D4F\u89C8\u5668\uFF08\u7AEF\u53E3 ${CDP_PORT}\uFF09\uFF0C\u8BF7\u68C0\u67E5 Chrome/Edge \u662F\u5426\u5DF2\u5B89\u88C5\u3002`);
|
|
432
|
-
}
|
|
433
487
|
try {
|
|
434
488
|
console.log(import_picocolors.default.yellow(`connectBrowser: \u6D4F\u89C8\u5668\u5DF2\u542F\u52A8\uFF0C\u6B63\u5728\u8FDE\u63A5 ${launchedUrl}...`));
|
|
435
489
|
const browser = await connectPuppeteer(launchedUrl);
|
|
436
490
|
console.log(import_picocolors.default.green(`connectBrowser: \u6210\u529F\u8FDE\u63A5 ${launchedUrl}`));
|
|
437
491
|
return browser;
|
|
438
492
|
} catch {
|
|
439
|
-
throw new Error(`\u65E0\u6CD5\u8FDE\u63A5\u5230\u6D4F\u89C8\u5668\uFF08
|
|
493
|
+
throw new Error(`\u65E0\u6CD5\u8FDE\u63A5\u5230\u6D4F\u89C8\u5668\uFF08${launchedUrl}\uFF09\uFF0C\u8BF7\u68C0\u67E5 Chrome/Edge \u662F\u5426\u5DF2\u5B89\u88C5\u3002`);
|
|
440
494
|
}
|
|
441
495
|
}
|
|
442
496
|
async function getPageTargetId(page) {
|
|
@@ -516,10 +570,11 @@ async function getTargetPage(browser, tabid) {
|
|
|
516
570
|
}
|
|
517
571
|
if (!targetPage) {
|
|
518
572
|
try {
|
|
519
|
-
const
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
];
|
|
573
|
+
const workspaceDir = getWorkspaceDir();
|
|
574
|
+
const cdpPort = getCdpPort();
|
|
575
|
+
const recordedPort = readActivePortFromProfile(workspaceDir);
|
|
576
|
+
const ports = recordedPort && recordedPort !== cdpPort ? [recordedPort, cdpPort] : [cdpPort];
|
|
577
|
+
const urls = ports.flatMap((port) => candidateCdpBaseUrls(port).map((u) => `${u}/json/list`));
|
|
523
578
|
let activeTargetId = null;
|
|
524
579
|
for (const url of urls) {
|
|
525
580
|
try {
|
|
@@ -556,10 +611,10 @@ async function getTargetPage(browser, tabid) {
|
|
|
556
611
|
return targetPage;
|
|
557
612
|
}
|
|
558
613
|
async function injectIntoPage(page) {
|
|
559
|
-
await injectWebMCPPolyfillAndTools(page
|
|
614
|
+
await injectWebMCPPolyfillAndTools(page);
|
|
560
615
|
}
|
|
561
|
-
async function injectWebMCPPolyfillAndTools(page
|
|
562
|
-
const polyfillReady =
|
|
616
|
+
async function injectWebMCPPolyfillAndTools(page) {
|
|
617
|
+
const polyfillReady = await page.evaluate(() => {
|
|
563
618
|
return !!window.__webmcpcli_init;
|
|
564
619
|
}).catch(() => false);
|
|
565
620
|
if (!polyfillReady) {
|
|
@@ -649,11 +704,19 @@ async function stateCommand({ tabid }) {
|
|
|
649
704
|
const state = await page.evaluate(async () => {
|
|
650
705
|
const url = document.URL;
|
|
651
706
|
const title = document.title;
|
|
652
|
-
const mcp = document.modelContext;
|
|
707
|
+
const mcp = document.modelContext || navigator.modelContext;
|
|
653
708
|
let webmcpTools = [];
|
|
654
|
-
if (mcp && typeof mcp.
|
|
655
|
-
|
|
656
|
-
|
|
709
|
+
if (mcp && typeof mcp.getTools === "function") {
|
|
710
|
+
try {
|
|
711
|
+
const toolsResult = await mcp.getTools();
|
|
712
|
+
const list = toolsResult?.tools || toolsResult || [];
|
|
713
|
+
webmcpTools = list.filter((t) => t && typeof t.name === "string").map((t) => ({
|
|
714
|
+
name: t.name,
|
|
715
|
+
description: typeof t.description === "string" ? t.description.slice(0, 200) : void 0
|
|
716
|
+
}));
|
|
717
|
+
} catch {
|
|
718
|
+
webmcpTools = [];
|
|
719
|
+
}
|
|
657
720
|
}
|
|
658
721
|
return { url, title, webmcpTools };
|
|
659
722
|
});
|
|
@@ -685,155 +748,6 @@ async function stateCommand({ tabid }) {
|
|
|
685
748
|
}
|
|
686
749
|
}
|
|
687
750
|
|
|
688
|
-
// src/zhihu/create-article.ts
|
|
689
|
-
var import_os2 = __toESM(require("os"), 1);
|
|
690
|
-
|
|
691
|
-
// src/zhihu/markdown.ts
|
|
692
|
-
var import_marked = require("marked");
|
|
693
|
-
function stripLeadingH1(markdown) {
|
|
694
|
-
return markdown.replace(/^#\s+[^\n]*\n+/, "");
|
|
695
|
-
}
|
|
696
|
-
function markdownToHtml(markdown) {
|
|
697
|
-
const marked = new import_marked.Marked({
|
|
698
|
-
gfm: true,
|
|
699
|
-
breaks: false,
|
|
700
|
-
renderer: {
|
|
701
|
-
code({ text, lang }) {
|
|
702
|
-
const langClass = lang ? ` class="language-${lang}"` : "";
|
|
703
|
-
const escaped = text.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">");
|
|
704
|
-
return `<pre><code${langClass}>${escaped}</code></pre>`;
|
|
705
|
-
},
|
|
706
|
-
codespan({ text }) {
|
|
707
|
-
const escaped = text.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">");
|
|
708
|
-
return `<code>${escaped}</code>`;
|
|
709
|
-
}
|
|
710
|
-
}
|
|
711
|
-
});
|
|
712
|
-
return marked.parse(markdown);
|
|
713
|
-
}
|
|
714
|
-
function decodeBase64Content(content) {
|
|
715
|
-
return Buffer.from(content, "base64").toString("utf-8");
|
|
716
|
-
}
|
|
717
|
-
var ZHIHU_WRITE_PAGE_RE = /^https:\/\/zhuanlan\.zhihu\.com\/(write|p\/\d+\/edit)/;
|
|
718
|
-
function isZhihuWriteUrl(url) {
|
|
719
|
-
return ZHIHU_WRITE_PAGE_RE.test(url);
|
|
720
|
-
}
|
|
721
|
-
|
|
722
|
-
// src/zhihu/create-article.ts
|
|
723
|
-
function delay(ms) {
|
|
724
|
-
return new Promise((resolve3) => setTimeout(resolve3, ms));
|
|
725
|
-
}
|
|
726
|
-
function getPasteModifier() {
|
|
727
|
-
return import_os2.default.platform() === "darwin" ? "Meta" : "Control";
|
|
728
|
-
}
|
|
729
|
-
async function fillTitle(page, title) {
|
|
730
|
-
await page.evaluate((titleText) => {
|
|
731
|
-
const input = document.querySelector('textarea[placeholder*="\u6807\u9898"]') || document.querySelector(".WriteIndex-titleInput textarea") || document.querySelector(".WriteIndex-titleInput input");
|
|
732
|
-
if (!input) throw new Error("\u672A\u627E\u5230\u6807\u9898\u8F93\u5165\u6846\uFF0C\u8BF7\u786E\u8BA4\u7F16\u8F91\u5668\u9875\u9762\u5DF2\u5B8C\u5168\u52A0\u8F7D");
|
|
733
|
-
const setter = Object.getOwnPropertyDescriptor(
|
|
734
|
-
input instanceof HTMLTextAreaElement ? HTMLTextAreaElement.prototype : HTMLInputElement.prototype,
|
|
735
|
-
"value"
|
|
736
|
-
)?.set;
|
|
737
|
-
if (setter) {
|
|
738
|
-
setter.call(input, titleText);
|
|
739
|
-
} else {
|
|
740
|
-
input.value = titleText;
|
|
741
|
-
}
|
|
742
|
-
input.dispatchEvent(new InputEvent("input", { bubbles: true, cancelable: true, data: titleText }));
|
|
743
|
-
input.dispatchEvent(new Event("change", { bubbles: true }));
|
|
744
|
-
if (input.value.trim() !== titleText.trim()) {
|
|
745
|
-
throw new Error("\u6807\u9898\u586B\u5199\u5931\u8D25\uFF0C\u8BF7\u5237\u65B0\u9875\u9762\u540E\u91CD\u8BD5");
|
|
746
|
-
}
|
|
747
|
-
}, title);
|
|
748
|
-
}
|
|
749
|
-
async function pasteHtmlToEditor(page, html) {
|
|
750
|
-
const browser = page.browser();
|
|
751
|
-
const tempPage = await browser.newPage();
|
|
752
|
-
const modifier = getPasteModifier();
|
|
753
|
-
try {
|
|
754
|
-
const fullHtml = `<!DOCTYPE html><html><head><meta charset="utf-8"></head><body>${html}</body></html>`;
|
|
755
|
-
await tempPage.setContent(fullHtml, { waitUntil: "domcontentloaded" });
|
|
756
|
-
await delay(300);
|
|
757
|
-
await tempPage.click("body");
|
|
758
|
-
await tempPage.keyboard.down(modifier);
|
|
759
|
-
await tempPage.keyboard.press("KeyA");
|
|
760
|
-
await tempPage.keyboard.up(modifier);
|
|
761
|
-
await delay(100);
|
|
762
|
-
await tempPage.keyboard.down(modifier);
|
|
763
|
-
await tempPage.keyboard.press("KeyC");
|
|
764
|
-
await tempPage.keyboard.up(modifier);
|
|
765
|
-
await delay(300);
|
|
766
|
-
} finally {
|
|
767
|
-
await tempPage.close();
|
|
768
|
-
}
|
|
769
|
-
await page.bringToFront();
|
|
770
|
-
await page.waitForSelector(".public-DraftEditor-content", { timeout: 1e4 });
|
|
771
|
-
await page.click(".public-DraftEditor-content");
|
|
772
|
-
await delay(200);
|
|
773
|
-
await page.keyboard.down(modifier);
|
|
774
|
-
await page.keyboard.press("KeyA");
|
|
775
|
-
await page.keyboard.up(modifier);
|
|
776
|
-
await delay(100);
|
|
777
|
-
await page.keyboard.press("Backspace");
|
|
778
|
-
await delay(200);
|
|
779
|
-
await page.keyboard.down(modifier);
|
|
780
|
-
await page.keyboard.press("KeyV");
|
|
781
|
-
await page.keyboard.up(modifier);
|
|
782
|
-
await delay(1500);
|
|
783
|
-
const probe = html.replace(/<[^>]+>/g, "").trim().slice(0, 15);
|
|
784
|
-
const verifyResult = await page.evaluate((probeText) => {
|
|
785
|
-
const editor = document.querySelector(".public-DraftEditor-content");
|
|
786
|
-
if (!editor) return { ok: false, reason: "no editor" };
|
|
787
|
-
const text = (editor.innerText || "").trim();
|
|
788
|
-
if (!text) return { ok: false, reason: "empty" };
|
|
789
|
-
const inner = editor.innerHTML;
|
|
790
|
-
const hasRichFormat = inner.includes("data-text") || /font-weight:\s*bold|font-style:\s*italic|code-block|blockquote/i.test(inner);
|
|
791
|
-
const normalizedText = text.replace(/\s+/g, "");
|
|
792
|
-
const normalizedProbe = probeText.replace(/\s+/g, "");
|
|
793
|
-
const hasProbe = normalizedText.includes(normalizedProbe);
|
|
794
|
-
return {
|
|
795
|
-
ok: text.length > 20 && hasProbe && (hasRichFormat || text.length > 50),
|
|
796
|
-
reason: !hasProbe ? "probe mismatch" : !hasRichFormat && text.length <= 50 ? "no rich format" : void 0
|
|
797
|
-
};
|
|
798
|
-
}, probe);
|
|
799
|
-
if (!verifyResult.ok) {
|
|
800
|
-
throw new Error(
|
|
801
|
-
`\u6B63\u6587\u586B\u5199\u5931\u8D25\uFF08Markdown \u8F6C HTML \u7C98\u8D34\u672A\u751F\u6548\uFF09${verifyResult.reason ? `: ${verifyResult.reason}` : ""}`
|
|
802
|
-
);
|
|
803
|
-
}
|
|
804
|
-
}
|
|
805
|
-
async function zhihuCreateArticle(page, args) {
|
|
806
|
-
const { title, content } = args;
|
|
807
|
-
if (!title?.trim()) throw new Error("\u53C2\u6570 title \u4E0D\u80FD\u4E3A\u7A7A");
|
|
808
|
-
if (!content?.trim()) throw new Error("\u53C2\u6570 content \u4E0D\u80FD\u4E3A\u7A7A");
|
|
809
|
-
const url = page.url();
|
|
810
|
-
if (!isZhihuWriteUrl(url)) {
|
|
811
|
-
throw new Error("\u5F53\u524D\u9875\u9762\u4E0D\u662F\u77E5\u4E4E\u4E13\u680F\u7F16\u8F91\u5668\uFF0C\u8BF7\u5148\u6253\u5F00 https://zhuanlan.zhihu.com/write");
|
|
812
|
-
}
|
|
813
|
-
let markdown;
|
|
814
|
-
try {
|
|
815
|
-
markdown = stripLeadingH1(decodeBase64Content(content));
|
|
816
|
-
} catch {
|
|
817
|
-
throw new Error("content \u4E0D\u662F\u6709\u6548\u7684 Base64 \u7F16\u7801\uFF0C\u8BF7\u68C0\u67E5\u53C2\u6570\u6216\u4F7F\u7528 @base64file: \u5F15\u7528\u6587\u4EF6");
|
|
818
|
-
}
|
|
819
|
-
if (!markdown.trim()) {
|
|
820
|
-
throw new Error("\u6B63\u6587\u5185\u5BB9\u4E3A\u7A7A\uFF0C\u8BF7\u68C0\u67E5 Markdown \u6587\u4EF6");
|
|
821
|
-
}
|
|
822
|
-
const html = markdownToHtml(markdown);
|
|
823
|
-
await fillTitle(page, title.trim());
|
|
824
|
-
await delay(500);
|
|
825
|
-
await pasteHtmlToEditor(page, html);
|
|
826
|
-
const result = {
|
|
827
|
-
success: true,
|
|
828
|
-
message: "\u6587\u7AE0\u6807\u9898\u548C\u6B63\u6587\u5DF2\u6210\u529F\u586B\u5199\u5230\u77E5\u4E4E\u4E13\u680F\u7F16\u8F91\u5668\uFF08Markdown \u5DF2\u8F6C\u4E3A\u5BCC\u6587\u672C\uFF09\uFF0C\u8349\u7A3F\u5C06\u81EA\u52A8\u4FDD\u5B58",
|
|
829
|
-
title: title.trim(),
|
|
830
|
-
contentLength: markdown.length
|
|
831
|
-
};
|
|
832
|
-
return {
|
|
833
|
-
content: [{ type: "text", text: JSON.stringify(result) }]
|
|
834
|
-
};
|
|
835
|
-
}
|
|
836
|
-
|
|
837
751
|
// src/commands/run.ts
|
|
838
752
|
async function runCommand({
|
|
839
753
|
toolName,
|
|
@@ -844,18 +758,8 @@ async function runCommand({
|
|
|
844
758
|
try {
|
|
845
759
|
const page = await getTargetPage(browser, tabid);
|
|
846
760
|
const urlBefore = page.url();
|
|
761
|
+
await injectIntoPage(page);
|
|
847
762
|
const cleanedArgs = argsJson.trim();
|
|
848
|
-
if (toolName === "create_article") {
|
|
849
|
-
let parsedArgs;
|
|
850
|
-
try {
|
|
851
|
-
parsedArgs = JSON.parse(cleanedArgs);
|
|
852
|
-
} catch {
|
|
853
|
-
throw new Error("\u53C2\u6570\u4E0D\u662F\u6709\u6548\u7684 JSON");
|
|
854
|
-
}
|
|
855
|
-
if (isZhihuWriteUrl(page.url())) {
|
|
856
|
-
return await zhihuCreateArticle(page, parsedArgs);
|
|
857
|
-
}
|
|
858
|
-
}
|
|
859
763
|
let result;
|
|
860
764
|
try {
|
|
861
765
|
result = await page.evaluate(async (name, inputString) => {
|
|
@@ -1043,7 +947,7 @@ async function tabsForwardCommand(tabid) {
|
|
|
1043
947
|
// package.json
|
|
1044
948
|
var package_default = {
|
|
1045
949
|
name: "@opentiny/webmcp-cli",
|
|
1046
|
-
version: "0.0.
|
|
950
|
+
version: "0.0.8",
|
|
1047
951
|
type: "module",
|
|
1048
952
|
description: "WebMCP CLI for AI Agents to interact with browser via Puppeteer",
|
|
1049
953
|
main: "dist/index.js",
|
|
@@ -1061,7 +965,10 @@ var package_default = {
|
|
|
1061
965
|
build: "tsup && pnpm run build:inject",
|
|
1062
966
|
prepublishOnly: "pnpm run build",
|
|
1063
967
|
"link:global": "npm install -g .",
|
|
1064
|
-
start: "node dist/bin.js"
|
|
968
|
+
start: "node dist/bin.js",
|
|
969
|
+
test: "vitest run",
|
|
970
|
+
"test:watch": "vitest",
|
|
971
|
+
"test:browser": "vitest run --config vitest.e2e.config.ts"
|
|
1065
972
|
},
|
|
1066
973
|
keywords: [
|
|
1067
974
|
"chrome",
|
|
@@ -1092,7 +999,8 @@ var package_default = {
|
|
|
1092
999
|
esbuild: "^0.25.0",
|
|
1093
1000
|
tsup: "^8.0.2",
|
|
1094
1001
|
tsx: "^4.0.0",
|
|
1095
|
-
typescript: "^5.3.3"
|
|
1002
|
+
typescript: "^5.3.3",
|
|
1003
|
+
vitest: "^4.1.10"
|
|
1096
1004
|
}
|
|
1097
1005
|
};
|
|
1098
1006
|
|
|
@@ -1102,6 +1010,30 @@ function parseTabId(id) {
|
|
|
1102
1010
|
if (!id) return void 0;
|
|
1103
1011
|
return id;
|
|
1104
1012
|
}
|
|
1013
|
+
function tryParseBrowserStateText(text) {
|
|
1014
|
+
const tryParse = (jsonStr) => {
|
|
1015
|
+
try {
|
|
1016
|
+
const parsed = JSON.parse(jsonStr);
|
|
1017
|
+
if (parsed && typeof parsed.content === "string") {
|
|
1018
|
+
return parsed;
|
|
1019
|
+
}
|
|
1020
|
+
} catch {
|
|
1021
|
+
}
|
|
1022
|
+
return null;
|
|
1023
|
+
};
|
|
1024
|
+
const direct = tryParse(text);
|
|
1025
|
+
if (direct) return direct;
|
|
1026
|
+
if (text.startsWith("\u6D4F\u89C8\u5668\u72B6\u6001: ")) {
|
|
1027
|
+
let jsonStr = text.substring("\u6D4F\u89C8\u5668\u72B6\u6001: ".length);
|
|
1028
|
+
const firstNewline = jsonStr.indexOf("\n");
|
|
1029
|
+
if (firstNewline !== -1) {
|
|
1030
|
+
const maybeJson = jsonStr.substring(0, firstNewline);
|
|
1031
|
+
if (tryParse(maybeJson)) jsonStr = maybeJson;
|
|
1032
|
+
}
|
|
1033
|
+
return tryParse(jsonStr);
|
|
1034
|
+
}
|
|
1035
|
+
return null;
|
|
1036
|
+
}
|
|
1105
1037
|
function cleanOldLogs(baseDir, logDir) {
|
|
1106
1038
|
try {
|
|
1107
1039
|
const oldLogFile = import_path3.default.join(baseDir, "webmcp-cli.log");
|
|
@@ -1157,23 +1089,11 @@ function formatLogResult(result) {
|
|
|
1157
1089
|
let searchResultStr = "";
|
|
1158
1090
|
if (clone && Array.isArray(clone.content) && clone.content.length > 0 && clone.content[0] && typeof clone.content[0].text === "string") {
|
|
1159
1091
|
const text = clone.content[0].text;
|
|
1160
|
-
|
|
1161
|
-
|
|
1162
|
-
|
|
1163
|
-
|
|
1164
|
-
|
|
1165
|
-
extraAlerts = jsonStr.substring(firstNewline);
|
|
1166
|
-
jsonStr = jsonStr.substring(0, firstNewline);
|
|
1167
|
-
}
|
|
1168
|
-
try {
|
|
1169
|
-
const parsed = JSON.parse(jsonStr);
|
|
1170
|
-
if (parsed && typeof parsed.content === "string") {
|
|
1171
|
-
a11yTreeStr = parsed.content;
|
|
1172
|
-
parsed.content = "[Formatted A11y Tree - See details below]";
|
|
1173
|
-
clone.content[0].text = `\u6D4F\u89C8\u5668\u72B6\u6001: ${JSON.stringify(parsed, null, 2)}${extraAlerts}`;
|
|
1174
|
-
}
|
|
1175
|
-
} catch {
|
|
1176
|
-
}
|
|
1092
|
+
const browserState = tryParseBrowserStateText(text);
|
|
1093
|
+
if (browserState) {
|
|
1094
|
+
a11yTreeStr = browserState.content;
|
|
1095
|
+
browserState.content = "[Formatted A11y Tree - See details below]";
|
|
1096
|
+
clone.content[0].text = JSON.stringify(browserState, null, 2);
|
|
1177
1097
|
} else if (text.includes("A11y Tree \u641C\u7D22\u7ED3\u679C") || text.includes("\u65E0\u969C\u788D\u6811\u641C\u7D22\u7ED3\u679C") || text.includes("\u65E0\u969C\u788D\u6811\u641C\u7D22") || text.includes("\u547D\u4E2D\u884C") || text.includes("context:")) {
|
|
1178
1098
|
searchResultStr = text;
|
|
1179
1099
|
clone.content[0].text = "[Search A11y Tree Result - See details below]";
|
|
@@ -1203,7 +1123,7 @@ ${searchResultStr}`;
|
|
|
1203
1123
|
}
|
|
1204
1124
|
function writeLog(commandName, args, result, error) {
|
|
1205
1125
|
try {
|
|
1206
|
-
const baseDir = process.env.WEBMCP_WORKSPACE || import_path3.default.join(
|
|
1126
|
+
const baseDir = process.env.WEBMCP_WORKSPACE || import_path3.default.join(import_os2.default.homedir(), ".webmcp_chrome_profile");
|
|
1207
1127
|
const logDir = import_path3.default.join(baseDir, "logs");
|
|
1208
1128
|
if (!import_fs3.default.existsSync(logDir)) {
|
|
1209
1129
|
import_fs3.default.mkdirSync(logDir, { recursive: true });
|