@hupan56/wlkj 3.0.0 → 3.1.2

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.
Files changed (237) hide show
  1. package/bin/cli.js +1435 -1373
  2. package/package.json +1 -1
  3. package/templates/qoder/commands/optional/wl-insight.md +6 -4
  4. package/templates/qoder/commands/optional/wl-report.md +20 -39
  5. package/templates/qoder/commands/optional/wl-status.md +4 -4
  6. package/templates/qoder/commands/wl-code.md +2 -2
  7. package/templates/qoder/commands/wl-design.md +5 -5
  8. package/templates/qoder/commands/wl-init.md +3 -3
  9. package/templates/qoder/commands/wl-prd.md +26 -13
  10. package/templates/qoder/commands/wl-req.md +3 -3
  11. package/templates/qoder/commands/wl-search.md +78 -47
  12. package/templates/qoder/commands/wl-task.md +343 -40
  13. package/templates/qoder/commands/wl-test.md +10 -10
  14. package/templates/qoder/config.yaml +35 -13
  15. package/templates/qoder/hooks/post-tool-use.py +181 -0
  16. package/templates/qoder/hooks/session-start.py +82 -1
  17. package/templates/qoder/hooks/stop-eval.py +207 -0
  18. package/templates/qoder/hooks/user-prompt-submit.py +139 -0
  19. package/templates/qoder/rules/wl-pipeline.md +70 -8
  20. package/templates/qoder/scripts/README.md +78 -115
  21. package/templates/qoder/scripts/capability/__init__.py +26 -0
  22. package/templates/qoder/scripts/capability/__main__.py +72 -0
  23. package/templates/qoder/scripts/capability/adapters/__init__.py +29 -0
  24. package/templates/qoder/scripts/capability/adapters/cli.py +316 -0
  25. package/templates/qoder/scripts/capability/adapters/mcp.py +334 -0
  26. package/templates/qoder/scripts/capability/adapters/qw.py +271 -0
  27. package/templates/qoder/scripts/capability/caps/__init__.py +27 -0
  28. package/templates/qoder/scripts/capability/caps/context.py +36 -0
  29. package/templates/qoder/scripts/capability/caps/cron.py +50 -0
  30. package/templates/qoder/scripts/capability/caps/identity.py +43 -0
  31. package/templates/qoder/scripts/capability/caps/memory.py +71 -0
  32. package/templates/qoder/scripts/capability/caps/notify.py +41 -0
  33. package/templates/qoder/scripts/capability/caps/present.py +48 -0
  34. package/templates/qoder/scripts/capability/caps/repo.py +31 -0
  35. package/templates/qoder/scripts/capability/caps/sandbox.py +43 -0
  36. package/templates/qoder/scripts/capability/chain.py +92 -0
  37. package/templates/qoder/scripts/capability/memory_chain.py +41 -0
  38. package/templates/qoder/scripts/capability/registry.py +246 -0
  39. package/templates/qoder/scripts/capability/registry_mcp.py +250 -0
  40. package/templates/qoder/scripts/capability/smoke_test.py +211 -0
  41. package/templates/qoder/scripts/capability/smoke_test_report.json +94 -0
  42. package/templates/qoder/scripts/{report → deployment/setup}/__init__.py +11 -11
  43. package/templates/qoder/scripts/{setup → deployment/setup}/carriers.py +33 -26
  44. package/templates/qoder/scripts/deployment/setup/carriers_verify.py +148 -0
  45. package/templates/qoder/scripts/{setup → deployment/setup}/init_doctor.py +40 -16
  46. package/templates/qoder/scripts/{setup → deployment/setup}/install_qoderwork.py +20 -12
  47. package/templates/qoder/scripts/{setup → deployment/setup}/platform_doctor.py +8 -2
  48. package/templates/qoder/scripts/{setup → deployment/setup}/repo_root.py +113 -112
  49. package/templates/qoder/scripts/{setup → deployment/setup}/setup.py +31 -11
  50. package/templates/qoder/scripts/{setup → deployment/setup}/setup_lanhu.py +13 -7
  51. package/templates/qoder/scripts/domain/__init__.py +0 -0
  52. package/templates/qoder/scripts/domain/design/__init__.py +0 -0
  53. package/templates/qoder/scripts/{report → domain/design}/fill_prototype.py +12 -6
  54. package/templates/qoder/scripts/{report → domain/design}/gen_design_doc.py +8 -2
  55. package/templates/qoder/scripts/{setup → domain/kg}/__init__.py +11 -11
  56. package/templates/qoder/scripts/domain/kg/build/__init__.py +0 -0
  57. package/templates/qoder/scripts/{kg → domain/kg/build}/build_entity_registry.py +7 -2
  58. package/templates/qoder/scripts/{kg → domain/kg/build}/build_relations.py +7 -2
  59. package/templates/qoder/scripts/{kg → domain/kg/build}/build_style_index.py +13 -7
  60. package/templates/qoder/scripts/{kg → domain/kg/build}/build_workflows.py +7 -2
  61. package/templates/qoder/scripts/{kg → domain/kg/build}/kg_build.py +76 -9
  62. package/templates/qoder/scripts/{kg → domain/kg/build}/kg_build_db.py +7 -2
  63. package/templates/qoder/scripts/{kg → domain/kg/build}/kg_incremental.py +11 -6
  64. package/templates/qoder/scripts/{report → domain/kg/build}/learn_aggregate.py +9 -4
  65. package/templates/qoder/scripts/domain/kg/extract/__init__.py +0 -0
  66. package/templates/qoder/scripts/{common → domain/kg/extract}/extract.py +430 -419
  67. package/templates/qoder/scripts/{common → domain/kg/extract}/test_extract.py +115 -115
  68. package/templates/qoder/scripts/{common → domain/kg/extract}/ts_extract.py +614 -536
  69. package/templates/qoder/scripts/domain/kg/graph/__init__.py +0 -0
  70. package/templates/qoder/scripts/{kg → domain/kg/graph}/kg_link_db.py +7 -2
  71. package/templates/qoder/scripts/domain/kg/graph/kg_semantic.py +683 -0
  72. package/templates/qoder/scripts/{kg → domain/kg}/kg.py +263 -30
  73. package/templates/qoder/scripts/{common → domain/kg}/kg_capabilities.py +182 -182
  74. package/templates/qoder/scripts/domain/kg/search/__init__.py +0 -0
  75. package/templates/qoder/scripts/{kg → domain/kg/search}/context_pack.py +30 -9
  76. package/templates/qoder/scripts/{kg → domain/kg/search}/enrich_prompt.py +10 -4
  77. package/templates/qoder/scripts/{kg → domain/kg/search}/prefetch.py +33 -8
  78. package/templates/qoder/scripts/{common → domain/kg/search}/search_engine.py +207 -205
  79. package/templates/qoder/scripts/{kg → domain/kg/search}/search_index.py +143 -77
  80. package/templates/qoder/scripts/domain/kg/server/__init__.py +0 -0
  81. package/templates/qoder/scripts/domain/kg/server/kgd.py +549 -0
  82. package/templates/qoder/scripts/domain/kg/server/perf_bench.py +197 -0
  83. package/templates/qoder/scripts/domain/kg/storage/__init__.py +0 -0
  84. package/templates/qoder/scripts/{kg → domain/kg/storage}/kg_duckdb.py +38 -14
  85. package/templates/qoder/scripts/domain/learning/__init__.py +0 -0
  86. package/templates/qoder/scripts/{report → domain/learning}/learn.py +8 -3
  87. package/templates/qoder/scripts/{mcp → domain/report}/__init__.py +11 -11
  88. package/templates/qoder/scripts/{report → domain/report}/add_session.py +9 -3
  89. package/templates/qoder/scripts/{report → domain/report}/export.py +10 -5
  90. package/templates/qoder/scripts/{report → domain/report}/report.py +9 -4
  91. package/templates/qoder/scripts/domain/report/report_snapshot.py +261 -0
  92. package/templates/qoder/scripts/{report → domain/report}/role.py +10 -4
  93. package/templates/qoder/scripts/{report → domain/report}/status.py +112 -9
  94. package/templates/qoder/scripts/domain/report/status_snapshot.py +191 -0
  95. package/templates/qoder/scripts/domain/requirement/__init__.py +0 -0
  96. package/templates/qoder/scripts/{report → domain/requirement}/archive_prd.py +8 -2
  97. package/templates/qoder/scripts/{report → domain/requirement}/req.py +228 -222
  98. package/templates/qoder/scripts/{kg → domain/task}/__init__.py +11 -11
  99. package/templates/qoder/scripts/{task → domain/task}/git_sync.py +40 -21
  100. package/templates/qoder/scripts/{task → domain/task}/syncgate.py +13 -8
  101. package/templates/qoder/scripts/{task → domain/task}/task.py +13 -5
  102. package/templates/qoder/scripts/{task → domain/task}/task_lifecycle.py +606 -596
  103. package/templates/qoder/scripts/{task → domain/task}/task_query.py +162 -161
  104. package/templates/qoder/scripts/{task → domain/task}/task_relations.py +425 -424
  105. package/templates/qoder/scripts/{task → domain/task}/team_sync.py +9 -4
  106. package/templates/qoder/scripts/domain/task/zentao_sync.py +370 -0
  107. package/templates/qoder/scripts/foundation/__init__.py +0 -0
  108. package/templates/qoder/scripts/{common → foundation}/bootstrap.py +10 -10
  109. package/templates/qoder/scripts/foundation/core/__init__.py +0 -0
  110. package/templates/qoder/scripts/{common → foundation/core}/cmd_registry.py +1 -1
  111. package/templates/qoder/scripts/{common → foundation/core}/config.py +1 -1
  112. package/templates/qoder/scripts/{common → foundation/core}/paths.py +706 -586
  113. package/templates/qoder/scripts/foundation/data/__init__.py +0 -0
  114. package/templates/qoder/scripts/{common → foundation/data}/contract.py +2 -2
  115. package/templates/qoder/scripts/{common → foundation/data}/events.py +2 -2
  116. package/templates/qoder/scripts/{common → foundation/data}/result.py +1 -1
  117. package/templates/qoder/scripts/{common → foundation/data}/task_utils.py +427 -404
  118. package/templates/qoder/scripts/foundation/identity/__init__.py +0 -0
  119. package/templates/qoder/scripts/{common → foundation/identity}/check_publish.py +103 -98
  120. package/templates/qoder/scripts/{common → foundation/identity}/developer.py +239 -239
  121. package/templates/qoder/scripts/{common → foundation/identity}/guard.py +2 -2
  122. package/templates/qoder/scripts/{common → foundation/identity}/identity.py +14 -10
  123. package/templates/qoder/scripts/{common → foundation/identity}/roles.py +1 -1
  124. package/templates/qoder/scripts/foundation/integrations/__init__.py +0 -0
  125. package/templates/qoder/scripts/{common → foundation/integrations}/active_task.py +30 -15
  126. package/templates/qoder/scripts/{common → foundation/integrations}/feishu.py +2 -2
  127. package/templates/qoder/scripts/{common → foundation/integrations}/terms.py +3 -3
  128. package/templates/qoder/scripts/foundation/integrations/zentao_client.py +220 -0
  129. package/templates/qoder/scripts/foundation/io/__init__.py +0 -0
  130. package/templates/qoder/scripts/{common → foundation/io}/filelock.py +1 -1
  131. package/templates/qoder/scripts/{common → foundation/io}/reqid.py +1 -1
  132. package/templates/qoder/scripts/foundation/protocol/__init__.py +0 -0
  133. package/templates/qoder/scripts/foundation/protocol/duckdb_conn.py +39 -0
  134. package/templates/qoder/scripts/{common → foundation/protocol}/mcp_base.py +1 -1
  135. package/templates/qoder/scripts/foundation/utils/__init__.py +0 -0
  136. package/templates/qoder/scripts/{common → foundation/utils}/eval_api.py +12 -1
  137. package/templates/qoder/scripts/{common → foundation/utils}/pip_install.py +23 -6
  138. package/templates/qoder/scripts/{common → foundation/utils}/platform_guard.py +1 -1
  139. package/templates/qoder/scripts/orchestration/__init__.py +0 -0
  140. package/templates/qoder/scripts/orchestration/wlkj.py +185 -0
  141. package/templates/qoder/scripts/protocol/__init__.py +0 -0
  142. package/templates/qoder/scripts/protocol/browser/README.md +207 -0
  143. package/templates/qoder/scripts/protocol/browser/SKILL.md +265 -0
  144. package/templates/qoder/scripts/protocol/browser/config.env +9 -0
  145. package/templates/qoder/scripts/protocol/browser/references/cdp-api.md +110 -0
  146. package/templates/qoder/scripts/protocol/browser/references/migration-2.5.3.md +72 -0
  147. package/templates/qoder/scripts/protocol/browser/references/site-patterns/.gitkeep +0 -0
  148. package/templates/qoder/scripts/protocol/browser/scripts/browser-discovery.mjs +138 -0
  149. package/templates/qoder/scripts/protocol/browser/scripts/cdp-proxy.mjs +672 -0
  150. package/templates/qoder/scripts/protocol/browser/scripts/check-deps.mjs +206 -0
  151. package/templates/qoder/scripts/protocol/browser/scripts/find-url.mjs +253 -0
  152. package/templates/qoder/scripts/protocol/browser/scripts/match-site.mjs +46 -0
  153. package/templates/qoder/scripts/protocol/browser/templates/config.env.template +9 -0
  154. package/templates/qoder/scripts/protocol/mcp/__init__.py +11 -0
  155. package/templates/qoder/scripts/protocol/mcp/kg_mcp_server.py +478 -0
  156. package/templates/qoder/scripts/{mcp → protocol/mcp}/lanhu_stdio_wrapper.py +11 -5
  157. package/templates/qoder/scripts/{mcp → protocol/mcp}/mcp_doctor.py +47 -14
  158. package/templates/qoder/scripts/protocol/mcp/mcp_launcher.py +600 -0
  159. package/templates/qoder/scripts/{mcp → protocol/mcp}/mysql_mcp_server.py +181 -67
  160. package/templates/qoder/scripts/protocol/mcp/zentao_mcp_server.py +1920 -0
  161. package/templates/qoder/scripts/protocol/transports/__init__.py +56 -0
  162. package/templates/qoder/scripts/protocol/transports/base.py +87 -0
  163. package/templates/qoder/scripts/protocol/transports/cli.py +132 -0
  164. package/templates/qoder/scripts/protocol/transports/http.py +141 -0
  165. package/templates/qoder/scripts/protocol/transports/stdio.py +293 -0
  166. package/templates/qoder/scripts/run_weekly_update.bat +27 -18
  167. package/templates/qoder/scripts/run_weekly_update.sh +22 -13
  168. package/templates/qoder/scripts/validation/__init__.py +0 -0
  169. package/templates/qoder/scripts/validation/eval/__init__.py +0 -0
  170. package/templates/qoder/scripts/validation/eval/perf-report-2026-06-24.md +139 -0
  171. package/templates/qoder/scripts/validation/eval/qwork_harness.py +617 -0
  172. package/templates/qoder/scripts/validation/eval/report-commands.md +220 -0
  173. package/templates/qoder/scripts/validation/eval/transcript_timing.py +386 -0
  174. package/templates/qoder/scripts/validation/metrics/__init__.py +0 -0
  175. package/templates/qoder/scripts/{report → validation/metrics}/eval_prd.py +17 -10
  176. package/templates/qoder/scripts/validation/metrics/usability_score.py +750 -0
  177. package/templates/qoder/scripts/validation/test/__init__.py +11 -0
  178. package/templates/qoder/scripts/validation/test/assertion_gen.py +551 -0
  179. package/templates/qoder/scripts/{test → validation/test}/autotest.py +64 -4
  180. package/templates/qoder/scripts/{common → validation/test}/autotest_auth.py +1 -1
  181. package/templates/qoder/scripts/{test → validation/test}/autotest_batch.py +16 -3
  182. package/templates/qoder/scripts/{test → validation/test}/autotest_data.py +17 -12
  183. package/templates/qoder/scripts/validation/test/autotest_opencli.py +646 -0
  184. package/templates/qoder/scripts/{test → validation/test}/autotest_run.py +17 -3
  185. package/templates/qoder/scripts/validation/test/autotest_webaccess.py +571 -0
  186. package/templates/qoder/scripts/{test → validation/test}/benchmark.py +12 -6
  187. package/templates/qoder/scripts/{test → validation/test}/kg_auto_login.py +8 -2
  188. package/templates/qoder/scripts/{test → validation/test}/kg_test_runner.py +7 -2
  189. package/templates/qoder/scripts/{test → validation/test}/page_probe.py +8 -3
  190. package/templates/qoder/scripts/validation/test/smoke_all_commands.py +170 -0
  191. package/templates/qoder/settings.json +30 -0
  192. package/templates/qoder/skills/design-import/SKILL.md +22 -13
  193. package/templates/qoder/skills/design-review/SKILL.md +19 -10
  194. package/templates/qoder/skills/prd-generator/SKILL.md +22 -18
  195. package/templates/qoder/skills/prd-review/SKILL.md +6 -4
  196. package/templates/qoder/skills/prototype-generator/SKILL.md +63 -14
  197. package/templates/qoder/skills/spec-coder/SKILL.md +22 -13
  198. package/templates/qoder/skills/spec-generator/SKILL.md +22 -10
  199. package/templates/qoder/skills/test-generator/SKILL.md +7 -5
  200. package/templates/qoder/skills/wl-code/SKILL.md +23 -11
  201. package/templates/qoder/skills/wl-commit/SKILL.md +9 -8
  202. package/templates/qoder/skills/wl-design/SKILL.md +5 -3
  203. package/templates/qoder/skills/wl-init/SKILL.md +6 -6
  204. package/templates/qoder/skills/wl-insight/SKILL.md +23 -10
  205. package/templates/qoder/skills/wl-prd-full/SKILL.md +57 -6
  206. package/templates/qoder/skills/wl-prd-quick/SKILL.md +3 -3
  207. package/templates/qoder/skills/wl-prd-review/SKILL.md +16 -4
  208. package/templates/qoder/skills/wl-report/SKILL.md +25 -23
  209. package/templates/qoder/skills/wl-search/SKILL.md +149 -78
  210. package/templates/qoder/skills/wl-spec/SKILL.md +7 -5
  211. package/templates/qoder/skills/wl-status/SKILL.md +48 -16
  212. package/templates/qoder/skills/wl-task/SKILL.md +92 -10
  213. package/templates/qoder/skills/wl-test/SKILL.md +75 -29
  214. package/templates/qoder/templates/prd-full-template.md +7 -0
  215. package/templates/root/AGENTS.md +275 -236
  216. package/templates/root/requirements.txt +3 -0
  217. package/templates/root//344/275/277/347/224/250/350/257/264/346/230/216.md +3 -3
  218. package/templates/root//346/226/260/346/211/213/346/214/207/345/215/227.md +2 -2
  219. package/templates/.qoder/.runtime/ctx-cache-5660152f1d6dd819.md +0 -23
  220. package/templates/.qoder/.runtime/ctx-cache-afdce0dac06b25b0.md +0 -23
  221. package/templates/.qoder/.runtime/search-cache-eae7644e7b122f35.txt +0 -1
  222. package/templates/.qoder/learning/eval-history.jsonl +0 -28
  223. package/templates/data/index/wiki-index.json +0 -8
  224. package/templates/qoder/nul +0 -4
  225. package/templates/qoder/scripts/kg/extract.py +0 -398
  226. package/templates/qoder/scripts/kg/kg_semantic.py +0 -156
  227. package/templates/qoder/scripts/mcp/kg_mcp_server.py +0 -606
  228. package/templates/qoder/scripts/mcp/mcp_launcher.py +0 -442
  229. package/templates/qoder/scripts/mcp/zentao_mcp_server.py +0 -384
  230. package/templates/qoder/scripts/task/__init__.py +0 -11
  231. package/templates/qoder/scripts/test/__init__.py +0 -11
  232. package/templates/qoder/scripts/wlkj.py +0 -116
  233. /package/templates/qoder/scripts/{common → deployment}/__init__.py +0 -0
  234. /package/templates/qoder/scripts/{common → domain/kg/graph}/graph_traverse.py +0 -0
  235. /package/templates/qoder/scripts/{common → domain/kg/server}/repowiki.py +0 -0
  236. /package/templates/qoder/scripts/{common → foundation/io}/atomicio.py +0 -0
  237. /package/templates/qoder/scripts/{test → validation/test}/secure-ls.js +0 -0
@@ -0,0 +1,672 @@
1
+ #!/usr/bin/env node
2
+ // CDP Proxy - 通过 HTTP API 操控用户日常浏览器(Chrome / Edge / Chromium 等)
3
+ // 要求:浏览器已开启 remote debugging(chrome://inspect#remote-debugging toggle)
4
+ // Node.js 22+(使用原生 WebSocket)
5
+
6
+ import http from 'node:http';
7
+ import { URL } from 'node:url';
8
+ import fs from 'node:fs';
9
+ import path from 'node:path';
10
+ import os from 'node:os';
11
+ import net from 'node:net';
12
+ import { selectBrowser, findFallbackPort } from './browser-discovery.mjs';
13
+
14
+ // --- 解析命令行 --browser 参数(本次启动用哪个浏览器)---
15
+ function parseBrowserArg() {
16
+ const argv = process.argv.slice(2);
17
+ for (let i = 0; i < argv.length; i++) {
18
+ if (argv[i] === '--browser' && argv[i + 1]) return argv[i + 1];
19
+ if (argv[i].startsWith('--browser=')) return argv[i].slice('--browser='.length);
20
+ }
21
+ return null;
22
+ }
23
+ const BROWSER_OVERRIDE = parseBrowserArg();
24
+
25
+ const PORT = parseInt(process.env.CDP_PROXY_PORT || '3456');
26
+ let ws = null;
27
+ let cmdId = 0;
28
+ const pending = new Map(); // id -> {resolve, timer}
29
+ const sessions = new Map(); // targetId -> sessionId
30
+ const managedTabs = new Map(); // targetId -> { lastAccessed: number }
31
+ const TAB_IDLE_TIMEOUT = parseInt(process.env.CDP_TAB_IDLE_TIMEOUT || '900000'); // 15 min default
32
+ const CLEANUP_INTERVAL = 60000; // sweep every 60s
33
+
34
+ // --- WebSocket 兼容层 ---
35
+ let WS;
36
+ if (typeof globalThis.WebSocket !== 'undefined') {
37
+ // Node 22+ 原生 WebSocket(浏览器兼容 API)
38
+ WS = globalThis.WebSocket;
39
+ } else {
40
+ // 回退到 ws 模块
41
+ try {
42
+ WS = (await import('ws')).default;
43
+ } catch {
44
+ console.error('[CDP Proxy] 错误:Node.js 版本 < 22 且未安装 ws 模块');
45
+ console.error(' 解决方案:升级到 Node.js 22+ 或执行 npm install -g ws');
46
+ process.exit(1);
47
+ }
48
+ }
49
+
50
+ // proxy 启动时连接到的浏览器(用于 /health 暴露给 check-deps 比较)
51
+ let connectedBrowser = null; // { id, label, source }
52
+
53
+ // pin 首次成功连接的浏览器 id。重连时只接受同一 id,避免悄悄降级到别的浏览器。
54
+ let pinnedBrowserId = null;
55
+
56
+ // --- 自动发现浏览器调试端口 ---
57
+ // 决策完全委派给 browser-discovery.selectBrowser;此处只做日志和返回结构包装。
58
+ async function discoverChromePort() {
59
+ const result = await selectBrowser(BROWSER_OVERRIDE);
60
+ if (result.kind === 'ok') {
61
+ if (pinnedBrowserId && pinnedBrowserId !== result.browser.id) {
62
+ throw new Error(
63
+ `本次连接的浏览器已经是 ${pinnedBrowserId},不会自动切到 ${result.browser.id}。` +
64
+ `如果想换成 ${result.browser.id},请先在终端运行 pkill -f cdp-proxy.mjs 重置。`
65
+ );
66
+ }
67
+ pinnedBrowserId = result.browser.id;
68
+ connectedBrowser = { id: result.browser.id, label: result.browser.label, source: result.source };
69
+ const tag = result.source === 'override' ? '[--browser 指定]' : '[config.env 偏好]';
70
+ console.log(`[CDP Proxy] 选用 ${result.browser.label} (端口 ${result.browser.port}${result.browser.wsPath ? ',带 wsPath' : ''}) ${tag}`);
71
+ return { port: result.browser.port, wsPath: result.browser.wsPath };
72
+ }
73
+ // mismatch:有显式偏好但未检测到 —— 硬错,绝不降级
74
+ if (result.kind === 'mismatch') {
75
+ const expected = result.override || result.configured;
76
+ const src = result.source === 'override' ? '本次指定' : '默认偏好';
77
+ throw new Error(
78
+ `${src}的浏览器是 "${expected}",但没连上。Agent 处理顺序:` +
79
+ `(1) 先用系统命令打开 ${expected}(按平台选择,如 macOS 的 open -a),稍等后重试请求;` +
80
+ `(2) 若仍失败,说明远程调试开关没启用 —— 告知用户在地址栏访问 ${expected}://inspect/#remote-debugging 勾选 "Allow remote debugging for this browser instance"。`
81
+ );
82
+ }
83
+ // 已 pin 过浏览器(如首次连上 edge 后 edge 退出):拒绝任何 fallback
84
+ if (pinnedBrowserId) {
85
+ throw new Error(
86
+ `本次连接的浏览器是 ${pinnedBrowserId},但现在没连上。Agent 处理顺序:` +
87
+ `(1) 先用系统命令打开 ${pinnedBrowserId}(按平台选择),稍等后重试请求;` +
88
+ `(2) 若仍失败,告知用户在地址栏访问 ${pinnedBrowserId}://inspect/#remote-debugging 重新勾选允许。` +
89
+ `若想换成其他浏览器,请先在终端运行 pkill -f cdp-proxy.mjs 重置。`
90
+ );
91
+ }
92
+ // 仅在「从未成功连接 + 无偏好/override」时允许固定端口兜底(手动 --remote-debugging-port 启动场景)
93
+ const fallbackPort = await findFallbackPort();
94
+ if (fallbackPort !== null) {
95
+ connectedBrowser = { id: 'unknown', label: '未知(通过手动调试端口连接)', source: 'fallback' };
96
+ console.log(`[CDP Proxy] 通过手动调试端口连接: ${fallbackPort}`);
97
+ return { port: fallbackPort, wsPath: null };
98
+ }
99
+ return null;
100
+ }
101
+
102
+ function getWebSocketUrl(port, wsPath) {
103
+ if (wsPath) return `ws://127.0.0.1:${port}${wsPath}`;
104
+ return `ws://127.0.0.1:${port}/devtools/browser`;
105
+ }
106
+
107
+ // --- WebSocket 连接管理 ---
108
+ let chromePort = null;
109
+ let chromeWsPath = null;
110
+
111
+ let connectingPromise = null;
112
+ async function connect() {
113
+ if (ws && (ws.readyState === WS.OPEN || ws.readyState === 1)) return;
114
+ if (connectingPromise) return connectingPromise; // 复用进行中的连接
115
+
116
+ if (!chromePort) {
117
+ const discovered = await discoverChromePort();
118
+ if (!discovered) {
119
+ throw new Error(
120
+ 'Chrome 未开启远程调试端口。请用以下方式启动 Chrome:\n' +
121
+ ' macOS: /Applications/Google\\ Chrome.app/Contents/MacOS/Google\\ Chrome --remote-debugging-port=9222\n' +
122
+ ' Linux: google-chrome --remote-debugging-port=9222\n' +
123
+ ' 或在 chrome://flags 中搜索 "remote debugging" 并启用'
124
+ );
125
+ }
126
+ chromePort = discovered.port;
127
+ chromeWsPath = discovered.wsPath;
128
+ }
129
+
130
+ const wsUrl = getWebSocketUrl(chromePort, chromeWsPath);
131
+ if (!wsUrl) throw new Error('无法获取 Chrome WebSocket URL');
132
+
133
+ return connectingPromise = new Promise((resolve, reject) => {
134
+ ws = new WS(wsUrl);
135
+
136
+ const onOpen = () => {
137
+ cleanup();
138
+ connectingPromise = null;
139
+ console.log(`[CDP Proxy] 已连接浏览器 (端口 ${chromePort})`);
140
+ resolve();
141
+ };
142
+ const onError = (e) => {
143
+ cleanup();
144
+ connectingPromise = null;
145
+ ws = null;
146
+ chromePort = null;
147
+ chromeWsPath = null;
148
+ const msg = e.message || e.error?.message || '连接失败';
149
+ console.error('[CDP Proxy] 连接错误:', msg, '(端口缓存已清除,下次将重新发现)');
150
+ reject(new Error(msg));
151
+ };
152
+ const onClose = () => {
153
+ console.log('[CDP Proxy] 连接断开');
154
+ ws = null;
155
+ chromePort = null; // 重置端口缓存,下次连接重新发现
156
+ chromeWsPath = null;
157
+ sessions.clear();
158
+ managedTabs.clear();
159
+ };
160
+ const onMessage = (evt) => {
161
+ const data = typeof evt === 'string' ? evt : (evt.data || evt);
162
+ const msg = JSON.parse(typeof data === 'string' ? data : data.toString());
163
+
164
+ if (msg.method === 'Target.attachedToTarget') {
165
+ const { sessionId, targetInfo } = msg.params;
166
+ sessions.set(targetInfo.targetId, sessionId);
167
+ }
168
+ // 拦截页面对 Chrome 调试端口的探测请求(反风控)
169
+ if (msg.method === 'Fetch.requestPaused') {
170
+ const { requestId, sessionId: sid } = msg.params;
171
+ sendCDP('Fetch.failRequest', { requestId, errorReason: 'ConnectionRefused' }, sid).catch(() => {});
172
+ }
173
+ if (msg.id && pending.has(msg.id)) {
174
+ const { resolve, timer } = pending.get(msg.id);
175
+ clearTimeout(timer);
176
+ pending.delete(msg.id);
177
+ resolve(msg);
178
+ }
179
+ };
180
+
181
+ function cleanup() {
182
+ ws.removeEventListener?.('open', onOpen);
183
+ ws.removeEventListener?.('error', onError);
184
+ }
185
+
186
+ // 兼容 Node 原生 WebSocket 和 ws 模块的事件 API
187
+ if (ws.on) {
188
+ ws.on('open', onOpen);
189
+ ws.on('error', onError);
190
+ ws.on('close', onClose);
191
+ ws.on('message', onMessage);
192
+ } else {
193
+ ws.addEventListener('open', onOpen);
194
+ ws.addEventListener('error', onError);
195
+ ws.addEventListener('close', onClose);
196
+ ws.addEventListener('message', onMessage);
197
+ }
198
+ });
199
+ }
200
+
201
+ function sendCDP(method, params = {}, sessionId = null) {
202
+ return new Promise((resolve, reject) => {
203
+ if (!ws || (ws.readyState !== WS.OPEN && ws.readyState !== 1)) {
204
+ return reject(new Error('WebSocket 未连接'));
205
+ }
206
+ const id = ++cmdId;
207
+ const msg = { id, method, params };
208
+ if (sessionId) msg.sessionId = sessionId;
209
+ const timer = setTimeout(() => {
210
+ pending.delete(id);
211
+ reject(new Error('CDP 命令超时: ' + method));
212
+ }, 30000);
213
+ pending.set(id, { resolve, timer });
214
+ ws.send(JSON.stringify(msg));
215
+ });
216
+ }
217
+
218
+ // 已启用端口拦截的 session 集合(避免重复启用)
219
+ const portGuardedSessions = new Set();
220
+
221
+ async function ensureSession(targetId) {
222
+ if (sessions.has(targetId)) return sessions.get(targetId);
223
+ const resp = await sendCDP('Target.attachToTarget', { targetId, flatten: true });
224
+ if (resp.result?.sessionId) {
225
+ const sid = resp.result.sessionId;
226
+ sessions.set(targetId, sid);
227
+ // 启用调试端口探测拦截
228
+ await enablePortGuard(sid);
229
+ return sid;
230
+ }
231
+ throw new Error('attach 失败: ' + JSON.stringify(resp.error));
232
+ }
233
+
234
+ // 拦截页面对 Chrome 调试端口的探测(反风控)
235
+ // 只拦截 127.0.0.1:{chromePort} 的请求,不影响其他任何本地服务
236
+ async function enablePortGuard(sessionId) {
237
+ if (!chromePort || portGuardedSessions.has(sessionId)) return;
238
+ try {
239
+ await sendCDP('Fetch.enable', {
240
+ patterns: [
241
+ { urlPattern: `http://127.0.0.1:${chromePort}/*`, requestStage: 'Request' },
242
+ { urlPattern: `http://localhost:${chromePort}/*`, requestStage: 'Request' },
243
+ ]
244
+ }, sessionId);
245
+ portGuardedSessions.add(sessionId);
246
+ } catch { /* Fetch 域启用失败不影响主流程 */ }
247
+ }
248
+
249
+ // --- 闲置 Tab 自动清理 ---
250
+ function touchTab(targetId) {
251
+ const entry = managedTabs.get(targetId);
252
+ if (entry) entry.lastAccessed = Date.now();
253
+ }
254
+
255
+ async function cleanupIdleTabs() {
256
+ if (!ws || (ws.readyState !== WS.OPEN && ws.readyState !== 1)) return;
257
+ const now = Date.now();
258
+ for (const [targetId, info] of managedTabs) {
259
+ if (now - info.lastAccessed < TAB_IDLE_TIMEOUT) continue;
260
+ try { await sendCDP('Target.closeTarget', { targetId }); } catch { /* tab may already be closed */ }
261
+ sessions.delete(targetId);
262
+ managedTabs.delete(targetId);
263
+ console.log(`[CDP Proxy] Auto-closed idle tab: ${targetId}`);
264
+ }
265
+ }
266
+
267
+ async function closeAllManagedTabs() {
268
+ if (!ws || (ws.readyState !== WS.OPEN && ws.readyState !== 1)) return;
269
+ const targets = [...managedTabs.keys()];
270
+ for (const targetId of targets) {
271
+ try { await sendCDP('Target.closeTarget', { targetId }); } catch { /* ignore */ }
272
+ sessions.delete(targetId);
273
+ managedTabs.delete(targetId);
274
+ }
275
+ if (targets.length) console.log(`[CDP Proxy] Shutdown: closed ${targets.length} managed tab(s)`);
276
+ }
277
+
278
+ // --- 等待页面加载 ---
279
+ async function waitForLoad(sessionId, timeoutMs = 15000) {
280
+ // 启用 Page 域
281
+ await sendCDP('Page.enable', {}, sessionId);
282
+
283
+ return new Promise((resolve) => {
284
+ let resolved = false;
285
+ const done = (result) => {
286
+ if (resolved) return;
287
+ resolved = true;
288
+ clearTimeout(timer);
289
+ clearInterval(checkInterval);
290
+ resolve(result);
291
+ };
292
+
293
+ const timer = setTimeout(() => done('timeout'), timeoutMs);
294
+ const checkInterval = setInterval(async () => {
295
+ try {
296
+ const resp = await sendCDP('Runtime.evaluate', {
297
+ expression: 'document.readyState',
298
+ returnByValue: true,
299
+ }, sessionId);
300
+ if (resp.result?.result?.value === 'complete') {
301
+ done('complete');
302
+ }
303
+ } catch { /* 忽略 */ }
304
+ }, 500);
305
+ });
306
+ }
307
+
308
+ // --- 读取 POST body ---
309
+ async function readBody(req) {
310
+ let body = '';
311
+ for await (const chunk of req) body += chunk;
312
+ return body;
313
+ }
314
+
315
+ // --- HTTP API ---
316
+ const server = http.createServer(async (req, res) => {
317
+ const parsed = new URL(req.url, `http://localhost:${PORT}`);
318
+ const pathname = parsed.pathname;
319
+ const q = Object.fromEntries(parsed.searchParams);
320
+ if (q.target) touchTab(q.target);
321
+
322
+ res.setHeader('Content-Type', 'application/json; charset=utf-8');
323
+
324
+ try {
325
+ // /health 不需要连接浏览器
326
+ if (pathname === '/health') {
327
+ const connected = ws && (ws.readyState === WS.OPEN || ws.readyState === 1);
328
+ res.end(JSON.stringify({
329
+ status: 'ok',
330
+ connected,
331
+ browser: connectedBrowser,
332
+ sessions: sessions.size,
333
+ managedTabs: managedTabs.size,
334
+ chromePort,
335
+ }));
336
+ return;
337
+ }
338
+
339
+ await connect();
340
+
341
+ // GET /targets - 列出所有页面
342
+ if (pathname === '/targets') {
343
+ const resp = await sendCDP('Target.getTargets');
344
+ const pages = resp.result.targetInfos.filter(t => t.type === 'page');
345
+ res.end(JSON.stringify(pages, null, 2));
346
+ }
347
+
348
+ // POST /new (body=URL) - 创建新后台 tab
349
+ else if (pathname === '/new') {
350
+ if (req.method !== 'POST') {
351
+ res.statusCode = 400;
352
+ res.end(JSON.stringify({
353
+ error: 'v2.5.3 起 /new 改为 POST 传 URL(避免目标 URL 含 query 时被错误切分)',
354
+ migration: 'references/migration-2.5.3.md',
355
+ example: "curl -X POST --data-raw 'https://example.com' http://localhost:3456/new",
356
+ }));
357
+ return;
358
+ }
359
+ const body = (await readBody(req)).trim();
360
+ const targetUrl = body || 'about:blank';
361
+ const resp = await sendCDP('Target.createTarget', { url: targetUrl, background: true });
362
+ const targetId = resp.result.targetId;
363
+ managedTabs.set(targetId, { lastAccessed: Date.now() });
364
+
365
+ // 等待页面加载
366
+ if (targetUrl !== 'about:blank') {
367
+ try {
368
+ const sid = await ensureSession(targetId);
369
+ await waitForLoad(sid);
370
+ } catch { /* 非致命,继续 */ }
371
+ }
372
+
373
+ res.end(JSON.stringify({ targetId }));
374
+ }
375
+
376
+ // GET /close?target=xxx - 关闭 tab
377
+ else if (pathname === '/close') {
378
+ const resp = await sendCDP('Target.closeTarget', { targetId: q.target });
379
+ sessions.delete(q.target);
380
+ managedTabs.delete(q.target);
381
+ res.end(JSON.stringify(resp.result));
382
+ }
383
+
384
+ // POST /navigate?target=xxx (body=URL) - 导航(自动等待加载)
385
+ else if (pathname === '/navigate') {
386
+ if (req.method !== 'POST') {
387
+ res.statusCode = 400;
388
+ res.end(JSON.stringify({
389
+ error: 'v2.5.3 起 /navigate 改为 POST 传 URL(避免目标 URL 含 query 时被错误切分)',
390
+ migration: 'references/migration-2.5.3.md',
391
+ example: "curl -X POST --data-raw 'https://example.com' 'http://localhost:3456/navigate?target=ID'",
392
+ }));
393
+ return;
394
+ }
395
+ const targetUrl = (await readBody(req)).trim();
396
+ const sid = await ensureSession(q.target);
397
+ const resp = await sendCDP('Page.navigate', { url: targetUrl }, sid);
398
+
399
+ // 等待页面加载完成
400
+ await waitForLoad(sid);
401
+
402
+ res.end(JSON.stringify(resp.result));
403
+ }
404
+
405
+ // GET /back?target=xxx - 后退
406
+ else if (pathname === '/back') {
407
+ const sid = await ensureSession(q.target);
408
+ await sendCDP('Runtime.evaluate', { expression: 'history.back()' }, sid);
409
+ await waitForLoad(sid);
410
+ res.end(JSON.stringify({ ok: true }));
411
+ }
412
+
413
+ // POST /eval?target=xxx - 执行 JS
414
+ else if (pathname === '/eval') {
415
+ const sid = await ensureSession(q.target);
416
+ const body = await readBody(req);
417
+ const expr = body || q.expr || 'document.title';
418
+ const resp = await sendCDP('Runtime.evaluate', {
419
+ expression: expr,
420
+ returnByValue: true,
421
+ awaitPromise: true,
422
+ }, sid);
423
+ if (resp.result?.result?.value !== undefined) {
424
+ res.end(JSON.stringify({ value: resp.result.result.value }));
425
+ } else if (resp.result?.exceptionDetails) {
426
+ res.statusCode = 400;
427
+ res.end(JSON.stringify({ error: resp.result.exceptionDetails.text }));
428
+ } else {
429
+ res.end(JSON.stringify(resp.result));
430
+ }
431
+ }
432
+
433
+ // POST /click?target=xxx - 点击(body 为 CSS 选择器)
434
+ // POST /click?target=xxx — JS 层面点击(简单快速,覆盖大多数场景)
435
+ else if (pathname === '/click') {
436
+ const sid = await ensureSession(q.target);
437
+ const selector = await readBody(req);
438
+ if (!selector) {
439
+ res.statusCode = 400;
440
+ res.end(JSON.stringify({ error: 'POST body 需要 CSS 选择器' }));
441
+ return;
442
+ }
443
+ const selectorJson = JSON.stringify(selector);
444
+ const js = `(() => {
445
+ const el = document.querySelector(${selectorJson});
446
+ if (!el) return { error: '未找到元素: ' + ${selectorJson} };
447
+ el.scrollIntoView({ block: 'center' });
448
+ el.click();
449
+ return { clicked: true, tag: el.tagName, text: (el.textContent || '').slice(0, 100) };
450
+ })()`;
451
+ const resp = await sendCDP('Runtime.evaluate', {
452
+ expression: js,
453
+ returnByValue: true,
454
+ awaitPromise: true,
455
+ }, sid);
456
+ if (resp.result?.result?.value) {
457
+ const val = resp.result.result.value;
458
+ if (val.error) {
459
+ res.statusCode = 400;
460
+ res.end(JSON.stringify(val));
461
+ } else {
462
+ res.end(JSON.stringify(val));
463
+ }
464
+ } else {
465
+ res.end(JSON.stringify(resp.result));
466
+ }
467
+ }
468
+
469
+ // POST /clickAt?target=xxx — CDP 浏览器级真实鼠标点击(算用户手势,能触发文件对话框、绕过反自动化检测)
470
+ else if (pathname === '/clickAt') {
471
+ const sid = await ensureSession(q.target);
472
+ const selector = await readBody(req);
473
+ if (!selector) {
474
+ res.statusCode = 400;
475
+ res.end(JSON.stringify({ error: 'POST body 需要 CSS 选择器' }));
476
+ return;
477
+ }
478
+ const selectorJson = JSON.stringify(selector);
479
+ const js = `(() => {
480
+ const el = document.querySelector(${selectorJson});
481
+ if (!el) return { error: '未找到元素: ' + ${selectorJson} };
482
+ el.scrollIntoView({ block: 'center' });
483
+ const rect = el.getBoundingClientRect();
484
+ return { x: rect.x + rect.width / 2, y: rect.y + rect.height / 2, tag: el.tagName, text: (el.textContent || '').slice(0, 100) };
485
+ })()`;
486
+ const coordResp = await sendCDP('Runtime.evaluate', {
487
+ expression: js,
488
+ returnByValue: true,
489
+ awaitPromise: true,
490
+ }, sid);
491
+ const coord = coordResp.result?.result?.value;
492
+ if (!coord || coord.error) {
493
+ res.statusCode = 400;
494
+ res.end(JSON.stringify(coord || coordResp.result));
495
+ return;
496
+ }
497
+ await sendCDP('Input.dispatchMouseEvent', {
498
+ type: 'mousePressed', x: coord.x, y: coord.y, button: 'left', clickCount: 1
499
+ }, sid);
500
+ await sendCDP('Input.dispatchMouseEvent', {
501
+ type: 'mouseReleased', x: coord.x, y: coord.y, button: 'left', clickCount: 1
502
+ }, sid);
503
+ res.end(JSON.stringify({ clicked: true, x: coord.x, y: coord.y, tag: coord.tag, text: coord.text }));
504
+ }
505
+
506
+ // POST /setFiles?target=xxx — 给 file input 设置本地文件(绕过文件对话框)
507
+ // body: JSON { "selector": "input[type=file]", "files": ["/path/to/file1.png", "/path/to/file2.png"] }
508
+ else if (pathname === '/setFiles') {
509
+ const sid = await ensureSession(q.target);
510
+ const body = JSON.parse(await readBody(req));
511
+ if (!body.selector || !body.files) {
512
+ res.statusCode = 400;
513
+ res.end(JSON.stringify({ error: '需要 selector 和 files 字段' }));
514
+ return;
515
+ }
516
+ // 获取 DOM 节点
517
+ await sendCDP('DOM.enable', {}, sid);
518
+ const doc = await sendCDP('DOM.getDocument', {}, sid);
519
+ const node = await sendCDP('DOM.querySelector', {
520
+ nodeId: doc.result.root.nodeId,
521
+ selector: body.selector
522
+ }, sid);
523
+ if (!node.result?.nodeId) {
524
+ res.statusCode = 400;
525
+ res.end(JSON.stringify({ error: '未找到元素: ' + body.selector }));
526
+ return;
527
+ }
528
+ // 设置文件
529
+ await sendCDP('DOM.setFileInputFiles', {
530
+ nodeId: node.result.nodeId,
531
+ files: body.files
532
+ }, sid);
533
+ res.end(JSON.stringify({ success: true, files: body.files.length }));
534
+ }
535
+
536
+ // GET /scroll?target=xxx&y=3000 - 滚动
537
+ else if (pathname === '/scroll') {
538
+ const sid = await ensureSession(q.target);
539
+ const y = parseInt(q.y || '3000');
540
+ const direction = q.direction || 'down'; // down | up | top | bottom
541
+ let js;
542
+ if (direction === 'top') {
543
+ js = 'window.scrollTo(0, 0); "scrolled to top"';
544
+ } else if (direction === 'bottom') {
545
+ js = 'window.scrollTo(0, document.body.scrollHeight); "scrolled to bottom"';
546
+ } else if (direction === 'up') {
547
+ js = `window.scrollBy(0, -${Math.abs(y)}); "scrolled up ${Math.abs(y)}px"`;
548
+ } else {
549
+ js = `window.scrollBy(0, ${Math.abs(y)}); "scrolled down ${Math.abs(y)}px"`;
550
+ }
551
+ const resp = await sendCDP('Runtime.evaluate', {
552
+ expression: js,
553
+ returnByValue: true,
554
+ }, sid);
555
+ // 等待懒加载触发
556
+ await new Promise(r => setTimeout(r, 800));
557
+ res.end(JSON.stringify({ value: resp.result?.result?.value }));
558
+ }
559
+
560
+ // GET /screenshot?target=xxx&file=/tmp/x.png - 截图
561
+ else if (pathname === '/screenshot') {
562
+ const sid = await ensureSession(q.target);
563
+ const format = q.format || 'png';
564
+ const resp = await sendCDP('Page.captureScreenshot', {
565
+ format,
566
+ quality: format === 'jpeg' ? 80 : undefined,
567
+ }, sid);
568
+ if (q.file) {
569
+ fs.writeFileSync(q.file, Buffer.from(resp.result.data, 'base64'));
570
+ res.end(JSON.stringify({ saved: q.file }));
571
+ } else {
572
+ res.setHeader('Content-Type', 'image/' + format);
573
+ res.end(Buffer.from(resp.result.data, 'base64'));
574
+ }
575
+ }
576
+
577
+ // GET /info?target=xxx - 获取页面信息
578
+ else if (pathname === '/info') {
579
+ const sid = await ensureSession(q.target);
580
+ const resp = await sendCDP('Runtime.evaluate', {
581
+ expression: 'JSON.stringify({title: document.title, url: location.href, ready: document.readyState})',
582
+ returnByValue: true,
583
+ }, sid);
584
+ res.end(resp.result?.result?.value || '{}');
585
+ }
586
+
587
+ else {
588
+ res.statusCode = 404;
589
+ res.end(JSON.stringify({
590
+ error: '未知端点',
591
+ endpoints: {
592
+ '/health': 'GET - 健康检查',
593
+ '/targets': 'GET - 列出所有页面 tab',
594
+ '/new': 'POST body=URL - 创建新后台 tab(自动等待加载)',
595
+ '/close?target=': 'GET - 关闭 tab',
596
+ '/navigate?target=': 'POST body=URL - 导航(自动等待加载)',
597
+ '/back?target=': 'GET - 后退',
598
+ '/info?target=': 'GET - 页面标题/URL/状态',
599
+ '/eval?target=': 'POST body=JS表达式 - 执行 JS',
600
+ '/click?target=': 'POST body=CSS选择器 - 点击元素',
601
+ '/scroll?target=&y=&direction=': 'GET - 滚动页面',
602
+ '/screenshot?target=&file=': 'GET - 截图',
603
+ },
604
+ }));
605
+ }
606
+ } catch (e) {
607
+ res.statusCode = 500;
608
+ res.end(JSON.stringify({ error: e.message }));
609
+ }
610
+ });
611
+
612
+ // 检查端口是否被占用
613
+ function checkPortAvailable(port) {
614
+ return new Promise((resolve) => {
615
+ const s = net.createServer();
616
+ s.once('error', () => resolve(false));
617
+ s.once('listening', () => { s.close(); resolve(true); });
618
+ s.listen(port, '127.0.0.1');
619
+ });
620
+ }
621
+
622
+ async function main() {
623
+ // 检查是否已有 proxy 在运行
624
+ const available = await checkPortAvailable(PORT);
625
+ if (!available) {
626
+ // 验证已有实例是否健康
627
+ try {
628
+ const ok = await new Promise((resolve) => {
629
+ http.get(`http://127.0.0.1:${PORT}/health`, { timeout: 2000 }, (res) => {
630
+ let d = '';
631
+ res.on('data', c => d += c);
632
+ res.on('end', () => resolve(d.includes('"ok"')));
633
+ }).on('error', () => resolve(false));
634
+ });
635
+ if (ok) {
636
+ console.log(`[CDP Proxy] 已有实例运行在端口 ${PORT},退出`);
637
+ process.exit(0);
638
+ }
639
+ } catch { /* 端口占用但非 proxy,继续报错 */ }
640
+ console.error(`[CDP Proxy] 端口 ${PORT} 已被占用`);
641
+ process.exit(1);
642
+ }
643
+
644
+ server.listen(PORT, '127.0.0.1', () => {
645
+ console.log(`[CDP Proxy] 运行在 http://localhost:${PORT}`);
646
+ // 启动时尝试连接 Chrome(非阻塞)
647
+ connect().catch(e => console.error('[CDP Proxy] 初始连接失败:', e.message, '(将在首次请求时重试)'));
648
+ });
649
+
650
+ // 定时清理闲置 tab
651
+ const cleanupTimer = setInterval(cleanupIdleTabs, CLEANUP_INTERVAL);
652
+ cleanupTimer.unref();
653
+
654
+ const shutdown = async (sig) => {
655
+ console.log(`[CDP Proxy] ${sig}, cleaning up...`);
656
+ clearInterval(cleanupTimer);
657
+ await closeAllManagedTabs();
658
+ process.exit(0);
659
+ };
660
+ process.on('SIGINT', () => shutdown('SIGINT'));
661
+ process.on('SIGTERM', () => shutdown('SIGTERM'));
662
+ }
663
+
664
+ // 防止未捕获异常导致进程崩溃
665
+ process.on('uncaughtException', (e) => {
666
+ console.error('[CDP Proxy] 未捕获异常:', e.message);
667
+ });
668
+ process.on('unhandledRejection', (e) => {
669
+ console.error('[CDP Proxy] 未处理拒绝:', e?.message || e);
670
+ });
671
+
672
+ main();