@hupan56/wlkj 2.7.12 → 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 (253) hide show
  1. package/bin/cli.js +1435 -1107
  2. package/package.json +29 -29
  3. package/templates/qoder/agents/insight-planning.md +1 -1
  4. package/templates/qoder/agents/insight-research.md +28 -15
  5. package/templates/qoder/commands/optional/wl-insight.md +161 -161
  6. package/templates/qoder/commands/optional/wl-report.md +20 -39
  7. package/templates/qoder/commands/optional/wl-status.md +4 -4
  8. package/templates/qoder/commands/wl-code.md +2 -2
  9. package/templates/qoder/commands/wl-design.md +6 -6
  10. package/templates/qoder/commands/wl-init.md +3 -3
  11. package/templates/qoder/commands/wl-prd.md +26 -13
  12. package/templates/qoder/commands/wl-req.md +43 -0
  13. package/templates/qoder/commands/wl-search.md +78 -47
  14. package/templates/qoder/commands/wl-task.md +343 -40
  15. package/templates/qoder/commands/wl-test.md +43 -17
  16. package/templates/qoder/config.yaml +52 -14
  17. package/templates/qoder/hooks/post-tool-use.py +181 -0
  18. package/templates/qoder/hooks/session-start.py +94 -23
  19. package/templates/qoder/hooks/stop-eval.py +207 -0
  20. package/templates/qoder/hooks/user-prompt-submit.py +139 -0
  21. package/templates/qoder/rules/wl-pipeline.md +85 -49
  22. package/templates/qoder/scripts/README.md +102 -0
  23. package/templates/qoder/scripts/capability/__init__.py +26 -0
  24. package/templates/qoder/scripts/capability/__main__.py +72 -0
  25. package/templates/qoder/scripts/capability/adapters/__init__.py +29 -0
  26. package/templates/qoder/scripts/capability/adapters/cli.py +316 -0
  27. package/templates/qoder/scripts/capability/adapters/mcp.py +334 -0
  28. package/templates/qoder/scripts/capability/adapters/qw.py +271 -0
  29. package/templates/qoder/scripts/capability/caps/__init__.py +27 -0
  30. package/templates/qoder/scripts/capability/caps/context.py +36 -0
  31. package/templates/qoder/scripts/capability/caps/cron.py +50 -0
  32. package/templates/qoder/scripts/capability/caps/identity.py +43 -0
  33. package/templates/qoder/scripts/capability/caps/memory.py +71 -0
  34. package/templates/qoder/scripts/capability/caps/notify.py +41 -0
  35. package/templates/qoder/scripts/capability/caps/present.py +48 -0
  36. package/templates/qoder/scripts/capability/caps/repo.py +31 -0
  37. package/templates/qoder/scripts/capability/caps/sandbox.py +43 -0
  38. package/templates/qoder/scripts/capability/chain.py +92 -0
  39. package/templates/qoder/scripts/capability/memory_chain.py +41 -0
  40. package/templates/qoder/scripts/capability/registry.py +246 -0
  41. package/templates/qoder/scripts/capability/registry_mcp.py +250 -0
  42. package/templates/qoder/scripts/capability/smoke_test.py +211 -0
  43. package/templates/qoder/scripts/capability/smoke_test_report.json +94 -0
  44. package/templates/qoder/scripts/deployment/setup/__init__.py +11 -0
  45. package/templates/qoder/scripts/deployment/setup/carriers.py +669 -0
  46. package/templates/qoder/scripts/deployment/setup/carriers_verify.py +148 -0
  47. package/templates/qoder/scripts/{init_doctor.py → deployment/setup/init_doctor.py} +100 -39
  48. package/templates/qoder/scripts/{install_qoderwork.py → deployment/setup/install_qoderwork.py} +48 -30
  49. package/templates/qoder/scripts/{platform_doctor.py → deployment/setup/platform_doctor.py} +271 -259
  50. package/templates/qoder/scripts/{repo_root.py → deployment/setup/repo_root.py} +9 -2
  51. package/templates/qoder/scripts/{setup.py → deployment/setup/setup.py} +143 -14
  52. package/templates/qoder/scripts/{setup_lanhu.py → deployment/setup/setup_lanhu.py} +979 -963
  53. package/templates/qoder/scripts/domain/design/__init__.py +0 -0
  54. package/templates/qoder/scripts/{fill_prototype.py → domain/design/fill_prototype.py} +17 -5
  55. package/templates/qoder/scripts/{gen_design_doc.py → domain/design/gen_design_doc.py} +406 -394
  56. package/templates/qoder/scripts/domain/kg/__init__.py +11 -0
  57. package/templates/qoder/scripts/domain/kg/build/__init__.py +0 -0
  58. package/templates/qoder/scripts/domain/kg/build/build_entity_registry.py +201 -0
  59. package/templates/qoder/scripts/domain/kg/build/build_relations.py +132 -0
  60. package/templates/qoder/scripts/{build_style_index.py → domain/kg/build/build_style_index.py} +50 -15
  61. package/templates/qoder/scripts/domain/kg/build/build_workflows.py +149 -0
  62. package/templates/qoder/scripts/{kg_build.py → domain/kg/build/kg_build.py} +685 -612
  63. package/templates/qoder/scripts/{kg_build_db.py → domain/kg/build/kg_build_db.py} +338 -327
  64. package/templates/qoder/scripts/{kg_incremental.py → domain/kg/build/kg_incremental.py} +425 -393
  65. package/templates/qoder/scripts/{learn_aggregate.py → domain/kg/build/learn_aggregate.py} +212 -201
  66. package/templates/qoder/scripts/domain/kg/extract/__init__.py +0 -0
  67. package/templates/qoder/scripts/{common → domain/kg/extract}/extract.py +430 -419
  68. package/templates/qoder/scripts/domain/kg/extract/test_extract.py +115 -0
  69. package/templates/qoder/scripts/{common → domain/kg/extract}/ts_extract.py +614 -536
  70. package/templates/qoder/scripts/domain/kg/graph/__init__.py +0 -0
  71. package/templates/qoder/scripts/{kg_link_db.py → domain/kg/graph/kg_link_db.py} +235 -224
  72. package/templates/qoder/scripts/domain/kg/graph/kg_semantic.py +683 -0
  73. package/templates/qoder/scripts/{kg.py → domain/kg/kg.py} +871 -708
  74. package/templates/qoder/scripts/domain/kg/kg_capabilities.py +182 -0
  75. package/templates/qoder/scripts/domain/kg/search/__init__.py +0 -0
  76. package/templates/qoder/scripts/{context_pack.py → domain/kg/search/context_pack.py} +45 -17
  77. package/templates/qoder/scripts/{enrich_prompt.py → domain/kg/search/enrich_prompt.py} +238 -226
  78. package/templates/qoder/scripts/domain/kg/search/prefetch.py +384 -0
  79. package/templates/qoder/scripts/{common → domain/kg/search}/search_engine.py +207 -205
  80. package/templates/qoder/scripts/{search_index.py → domain/kg/search/search_index.py} +210 -88
  81. package/templates/qoder/scripts/domain/kg/server/__init__.py +0 -0
  82. package/templates/qoder/scripts/domain/kg/server/kgd.py +549 -0
  83. package/templates/qoder/scripts/domain/kg/server/perf_bench.py +197 -0
  84. package/templates/qoder/scripts/domain/kg/storage/__init__.py +0 -0
  85. package/templates/qoder/scripts/{kg_duckdb.py → domain/kg/storage/kg_duckdb.py} +70 -45
  86. package/templates/qoder/scripts/domain/learning/__init__.py +0 -0
  87. package/templates/qoder/scripts/{learn.py → domain/learning/learn.py} +157 -146
  88. package/templates/qoder/scripts/domain/report/__init__.py +11 -0
  89. package/templates/qoder/scripts/{add_session.py → domain/report/add_session.py} +256 -244
  90. package/templates/qoder/scripts/{export.py → domain/report/export.py} +72 -4
  91. package/templates/qoder/scripts/{report.py → domain/report/report.py} +292 -281
  92. package/templates/qoder/scripts/domain/report/report_snapshot.py +261 -0
  93. package/templates/qoder/scripts/domain/report/role.py +39 -0
  94. package/templates/qoder/scripts/{status.py → domain/report/status.py} +737 -628
  95. package/templates/qoder/scripts/domain/report/status_snapshot.py +191 -0
  96. package/templates/qoder/scripts/domain/requirement/__init__.py +0 -0
  97. package/templates/qoder/scripts/{archive_prd.py → domain/requirement/archive_prd.py} +389 -377
  98. package/templates/qoder/scripts/domain/requirement/req.py +228 -0
  99. package/templates/qoder/scripts/domain/task/__init__.py +11 -0
  100. package/templates/qoder/scripts/{git_sync.py → domain/task/git_sync.py} +90 -50
  101. package/templates/qoder/scripts/{syncgate.py → domain/task/syncgate.py} +18 -7
  102. package/templates/qoder/scripts/domain/task/task.py +229 -0
  103. package/templates/qoder/scripts/domain/task/task_lifecycle.py +606 -0
  104. package/templates/qoder/scripts/domain/task/task_query.py +162 -0
  105. package/templates/qoder/scripts/domain/task/task_relations.py +425 -0
  106. package/templates/qoder/scripts/{team_sync.py → domain/task/team_sync.py} +101 -23
  107. package/templates/qoder/scripts/domain/task/zentao_sync.py +370 -0
  108. package/templates/qoder/scripts/foundation/__init__.py +0 -0
  109. package/templates/qoder/scripts/foundation/bootstrap.py +145 -0
  110. package/templates/qoder/scripts/foundation/core/__init__.py +0 -0
  111. package/templates/qoder/scripts/foundation/core/cmd_registry.py +112 -0
  112. package/templates/qoder/scripts/foundation/core/config.py +187 -0
  113. package/templates/qoder/scripts/{common → foundation/core}/paths.py +706 -400
  114. package/templates/qoder/scripts/foundation/data/__init__.py +0 -0
  115. package/templates/qoder/scripts/foundation/data/contract.py +317 -0
  116. package/templates/qoder/scripts/{common → foundation/data}/events.py +2 -2
  117. package/templates/qoder/scripts/foundation/data/result.py +223 -0
  118. package/templates/qoder/scripts/{common → foundation/data}/task_utils.py +427 -392
  119. package/templates/qoder/scripts/foundation/identity/__init__.py +0 -0
  120. package/templates/qoder/scripts/foundation/identity/check_publish.py +103 -0
  121. package/templates/qoder/scripts/{common → foundation/identity}/developer.py +239 -238
  122. package/templates/qoder/scripts/foundation/identity/guard.py +159 -0
  123. package/templates/qoder/scripts/{common → foundation/identity}/identity.py +132 -9
  124. package/templates/qoder/scripts/foundation/identity/roles.py +60 -0
  125. package/templates/qoder/scripts/foundation/integrations/__init__.py +0 -0
  126. package/templates/qoder/scripts/{common → foundation/integrations}/active_task.py +30 -15
  127. package/templates/qoder/scripts/{common → foundation/integrations}/feishu.py +11 -10
  128. package/templates/qoder/scripts/{common → foundation/integrations}/terms.py +3 -3
  129. package/templates/qoder/scripts/foundation/integrations/zentao_client.py +220 -0
  130. package/templates/qoder/scripts/foundation/io/__init__.py +0 -0
  131. package/templates/qoder/scripts/{common → foundation/io}/filelock.py +1 -1
  132. package/templates/qoder/scripts/{common → foundation/io}/reqid.py +1 -1
  133. package/templates/qoder/scripts/foundation/protocol/__init__.py +0 -0
  134. package/templates/qoder/scripts/foundation/protocol/duckdb_conn.py +39 -0
  135. package/templates/qoder/scripts/foundation/protocol/mcp_base.py +268 -0
  136. package/templates/qoder/scripts/foundation/utils/__init__.py +0 -0
  137. package/templates/qoder/scripts/{common → foundation/utils}/eval_api.py +12 -1
  138. package/templates/qoder/scripts/{common → foundation/utils}/pip_install.py +23 -6
  139. package/templates/qoder/scripts/{common → foundation/utils}/platform_guard.py +1 -1
  140. package/templates/qoder/scripts/orchestration/__init__.py +0 -0
  141. package/templates/qoder/scripts/orchestration/wlkj.py +185 -0
  142. package/templates/qoder/scripts/protocol/__init__.py +0 -0
  143. package/templates/qoder/scripts/protocol/browser/README.md +207 -0
  144. package/templates/qoder/scripts/protocol/browser/SKILL.md +265 -0
  145. package/templates/qoder/scripts/protocol/browser/config.env +9 -0
  146. package/templates/qoder/scripts/protocol/browser/references/cdp-api.md +110 -0
  147. package/templates/qoder/scripts/protocol/browser/references/migration-2.5.3.md +72 -0
  148. package/templates/qoder/scripts/protocol/browser/references/site-patterns/.gitkeep +0 -0
  149. package/templates/qoder/scripts/protocol/browser/scripts/browser-discovery.mjs +138 -0
  150. package/templates/qoder/scripts/protocol/browser/scripts/cdp-proxy.mjs +672 -0
  151. package/templates/qoder/scripts/protocol/browser/scripts/check-deps.mjs +206 -0
  152. package/templates/qoder/scripts/protocol/browser/scripts/find-url.mjs +253 -0
  153. package/templates/qoder/scripts/protocol/browser/scripts/match-site.mjs +46 -0
  154. package/templates/qoder/scripts/protocol/browser/templates/config.env.template +9 -0
  155. package/templates/qoder/scripts/protocol/mcp/__init__.py +11 -0
  156. package/templates/qoder/scripts/protocol/mcp/kg_mcp_server.py +478 -0
  157. package/templates/qoder/scripts/{lanhu_stdio_wrapper.py → protocol/mcp/lanhu_stdio_wrapper.py} +131 -119
  158. package/templates/qoder/scripts/protocol/mcp/mcp_doctor.py +548 -0
  159. package/templates/qoder/scripts/protocol/mcp/mcp_launcher.py +600 -0
  160. package/templates/qoder/scripts/protocol/mcp/mysql_mcp_server.py +461 -0
  161. package/templates/qoder/scripts/protocol/mcp/zentao_mcp_server.py +1920 -0
  162. package/templates/qoder/scripts/protocol/transports/__init__.py +56 -0
  163. package/templates/qoder/scripts/protocol/transports/base.py +87 -0
  164. package/templates/qoder/scripts/protocol/transports/cli.py +132 -0
  165. package/templates/qoder/scripts/protocol/transports/http.py +141 -0
  166. package/templates/qoder/scripts/protocol/transports/stdio.py +293 -0
  167. package/templates/qoder/scripts/run_weekly_update.bat +27 -18
  168. package/templates/qoder/scripts/run_weekly_update.sh +22 -13
  169. package/templates/qoder/scripts/validation/__init__.py +0 -0
  170. package/templates/qoder/scripts/validation/eval/__init__.py +0 -0
  171. package/templates/qoder/scripts/validation/eval/perf-report-2026-06-24.md +139 -0
  172. package/templates/qoder/scripts/validation/eval/qwork_harness.py +617 -0
  173. package/templates/qoder/scripts/validation/eval/report-commands.md +220 -0
  174. package/templates/qoder/scripts/validation/eval/transcript_timing.py +386 -0
  175. package/templates/qoder/scripts/validation/metrics/__init__.py +0 -0
  176. package/templates/qoder/scripts/{eval_prd.py → validation/metrics/eval_prd.py} +84 -15
  177. package/templates/qoder/scripts/validation/metrics/usability_score.py +750 -0
  178. package/templates/qoder/scripts/validation/test/__init__.py +11 -0
  179. package/templates/qoder/scripts/validation/test/assertion_gen.py +551 -0
  180. package/templates/qoder/scripts/{autotest.py → validation/test/autotest.py} +1234 -1751
  181. package/templates/qoder/scripts/validation/test/autotest_auth.py +109 -0
  182. package/templates/qoder/scripts/{autotest_batch.py → validation/test/autotest_batch.py} +255 -224
  183. package/templates/qoder/scripts/validation/test/autotest_data.py +680 -0
  184. package/templates/qoder/scripts/validation/test/autotest_opencli.py +646 -0
  185. package/templates/qoder/scripts/{autotest_run.py → validation/test/autotest_run.py} +323 -297
  186. package/templates/qoder/scripts/validation/test/autotest_webaccess.py +571 -0
  187. package/templates/qoder/scripts/{benchmark.py → validation/test/benchmark.py} +17 -5
  188. package/templates/qoder/scripts/{kg_auto_login.py → validation/test/kg_auto_login.py} +208 -196
  189. package/templates/qoder/scripts/{kg_test_runner.py → validation/test/kg_test_runner.py} +13 -2
  190. package/templates/qoder/scripts/{page_probe.py → validation/test/page_probe.py} +470 -459
  191. package/templates/qoder/scripts/validation/test/smoke_all_commands.py +170 -0
  192. package/templates/qoder/settings.json +23 -2
  193. package/templates/qoder/skills/design-import/SKILL.md +235 -226
  194. package/templates/qoder/skills/design-review/SKILL.md +91 -82
  195. package/templates/qoder/skills/prd-generator/SKILL.md +38 -24
  196. package/templates/qoder/skills/prd-review/SKILL.md +8 -6
  197. package/templates/qoder/skills/prototype-generator/SKILL.md +296 -247
  198. package/templates/qoder/skills/spec-coder/SKILL.md +24 -15
  199. package/templates/qoder/skills/spec-generator/SKILL.md +24 -12
  200. package/templates/qoder/skills/test-generator/SKILL.md +9 -7
  201. package/templates/qoder/skills/wl-code/SKILL.md +25 -13
  202. package/templates/qoder/skills/wl-commit/SKILL.md +10 -9
  203. package/templates/qoder/skills/wl-design/SKILL.md +7 -5
  204. package/templates/qoder/skills/wl-init/SKILL.md +8 -8
  205. package/templates/qoder/skills/wl-insight/SKILL.md +25 -12
  206. package/templates/qoder/skills/wl-prd-full/SKILL.md +59 -8
  207. package/templates/qoder/skills/wl-prd-quick/SKILL.md +7 -7
  208. package/templates/qoder/skills/wl-prd-review/SKILL.md +18 -6
  209. package/templates/qoder/skills/wl-report/SKILL.md +27 -25
  210. package/templates/qoder/skills/wl-search/SKILL.md +151 -80
  211. package/templates/qoder/skills/wl-spec/SKILL.md +9 -7
  212. package/templates/qoder/skills/wl-status/SKILL.md +50 -18
  213. package/templates/qoder/skills/wl-task/SKILL.md +94 -12
  214. package/templates/qoder/skills/wl-test/SKILL.md +154 -45
  215. package/templates/qoder/templates/prd-full-template.md +7 -0
  216. package/templates/root/AGENTS.md +275 -237
  217. package/templates/root/requirements.txt +3 -0
  218. package/templates/root//344/275/277/347/224/250/350/257/264/346/230/216.md +3 -3
  219. package/templates/root//346/226/260/346/211/213/346/214/207/345/215/227.md +2 -2
  220. package/templates/qoder/hooks/inject-workflow-state.py +0 -169
  221. package/templates/qoder/scripts/__pycache__/check_mcp_launch.cpython-39.pyc +0 -0
  222. package/templates/qoder/scripts/__pycache__/install_qoderwork.cpython-39.pyc +0 -0
  223. package/templates/qoder/scripts/__pycache__/mcp_launcher.cpython-39.pyc +0 -0
  224. package/templates/qoder/scripts/__pycache__/platform_doctor.cpython-39.pyc +0 -0
  225. package/templates/qoder/scripts/check_carriers.py +0 -238
  226. package/templates/qoder/scripts/check_mcp.py +0 -298
  227. package/templates/qoder/scripts/check_mcp_launch.py +0 -183
  228. package/templates/qoder/scripts/check_qoderwork_consistency.py +0 -166
  229. package/templates/qoder/scripts/collect_prds.py +0 -31
  230. package/templates/qoder/scripts/common/mentions.py +0 -134
  231. package/templates/qoder/scripts/common/utf8.py +0 -38
  232. package/templates/qoder/scripts/extract_api_params.py +0 -246
  233. package/templates/qoder/scripts/extract_routes.py +0 -54
  234. package/templates/qoder/scripts/extract_routes_tree.py +0 -78
  235. package/templates/qoder/scripts/handoff.py +0 -22
  236. package/templates/qoder/scripts/init_developer.py +0 -76
  237. package/templates/qoder/scripts/kg_mcp_server.py +0 -801
  238. package/templates/qoder/scripts/kg_semantic.py +0 -150
  239. package/templates/qoder/scripts/mcp_launcher.py +0 -414
  240. package/templates/qoder/scripts/mysql_mcp_server.py +0 -396
  241. package/templates/qoder/scripts/parse_prds.py +0 -33
  242. package/templates/qoder/scripts/role.py +0 -51
  243. package/templates/qoder/scripts/sync_carriers.py +0 -259
  244. package/templates/qoder/scripts/task.py +0 -1261
  245. package/templates/qoder/scripts/workspace_init.py +0 -102
  246. package/templates/qoder/scripts/zentao_mcp_server.py +0 -424
  247. package/templates/qoder/skills/prompt-enrich/SKILL.md +0 -90
  248. /package/templates/qoder/scripts/{common → deployment}/__init__.py +0 -0
  249. /package/templates/qoder/{skills/prototype-generator/SKILL.md.zcode-79180-2af4721f-f9a6-412c-88db-c0af680d211b.tmp → scripts/domain/__init__.py} +0 -0
  250. /package/templates/qoder/scripts/{common → domain/kg/graph}/graph_traverse.py +0 -0
  251. /package/templates/qoder/scripts/{common → domain/kg/server}/repowiki.py +0 -0
  252. /package/templates/qoder/scripts/{common → foundation/io}/atomicio.py +0 -0
  253. /package/templates/qoder/scripts/{secure-ls.js → validation/test/secure-ls.js} +0 -0
@@ -1,297 +1,323 @@
1
- # -*- coding: utf-8 -*-
2
- """autotest_run.py 真功能测试执行器 (ZCode 直接驱动 Playwright).
3
- 用法: python autotest_run.py --task <任务文件>
4
- 任务文件 JSON: {
5
- "page_path": "/veh/vehicle/vehAffair/insurance",
6
- "page_name": "保险管理",
7
- "cases": [ {case_id, title, steps:[{intent,desc,value,anchor}], expected, assert_fn} ]
8
- }
9
- --task 是必须的 (绕过 MSYS 路径转换; /xxx 会被 bash 转成 Windows 路径)
10
-
11
- 真测: 每步真实驱动浏览器 (goto/fill/click/extract/assert), 按断言判定 pass/fail.
12
- 覆盖与 QoderWork wl-test 相同的工作流 (路由查询→登录态→意图式用例→执行→沉淀).
13
- """
14
- import json, sys, time, os, re, argparse, subprocess
15
- from playwright.sync_api import sync_playwright
16
-
17
- # ===== 配置 (路径自检测) =====
18
- SCRIPTS = os.path.dirname(os.path.abspath(__file__))
19
- PROJECT = os.path.normpath(os.path.join(SCRIPTS, '..', '..'))
20
-
21
-
22
- def _read_developer():
23
- """读 .qoder/.developer 拿当前开发者, 身份隔离用 (绝不 fallback, 防串号)."""
24
- dev_file = os.path.join(PROJECT, '.qoder', '.developer')
25
- if os.path.isfile(dev_file):
26
- for ln in open(dev_file, encoding='utf-8'):
27
- if ln.strip().startswith('name='):
28
- return ln.strip().split('=', 1)[1].strip()
29
- raise RuntimeError('未检测到开发者身份(.qoder/.developer)。请先运行 /wl-init。')
30
-
31
-
32
- DEVELOPER = _read_developer()
33
- MEMBER_DIR = os.path.join(PROJECT, 'workspace', 'members', DEVELOPER)
34
- AUTH_FILE = os.path.join(MEMBER_DIR, 'auth-state.json')
35
- SECURELS_JS = os.path.join(SCRIPTS, 'secure-ls.js')
36
- AUTOLEARN = os.path.join(SCRIPTS, 'autotest.py')
37
- BASE = 'https://icsqas.inforecloud.com'
38
- ACCESS_KEY = 'ICS-1.3.8-prod-core-access'
39
- SECRET = 'please-replace-me-with-your-own-key'
40
-
41
- # ===== 登录态: 读 token, 失效则调 kg_auto_login.py =====
42
- def get_token():
43
- if os.path.exists(AUTH_FILE):
44
- tok = json.load(open(AUTH_FILE, encoding='utf-8')).get('access_token')
45
- # 校验
46
- import requests
47
- try:
48
- r = requests.get(BASE+'/test-api/system/user/getInfo',
49
- headers={'Authorization':'Bearer '+tok,'clientid':'e5cd7e4891bf95d1d19206ce24a7b32e'}, timeout=8)
50
- if r.json().get('code')==200: return tok
51
- except Exception: pass
52
- # 重新登录
53
- subprocess.run([sys.executable, os.path.join(SCRIPTS,'kg_auto_login.py')], capture_output=True, timeout=120)
54
- if os.path.exists(AUTH_FILE):
55
- return json.load(open(AUTH_FILE, encoding='utf-8')).get('access_token')
56
- return None
57
-
58
- # ===== SPA 会话: 注入 token + 初始化菜单 =====
59
- def init_session(page, token):
60
- page.goto(BASE+'/auth/login', wait_until='domcontentloaded'); time.sleep(1.2)
61
- page.add_script_tag(path=SECURELS_JS); time.sleep(0.3)
62
- page.evaluate('''(a)=>{const[t,k,s]=a;const S=window.SecureLS.default||window.SecureLS;const ls=new S({encodingType:'aes',isCompression:true,encryptionSecret:s});ls.set(k,{accessToken:t,refreshToken:t,accessCodes:[],originMenus:[],webMenus:[],accessMenus:[],accessRoutes:[],isAccessChecked:false,loginExpired:false});}''',[token,ACCESS_KEY,SECRET])
63
- page.goto(BASE+'/dashboard/workspace', wait_until='domcontentloaded')
64
- for _ in range(15):
65
- if '加载菜单中' not in page.evaluate('()=>document.body.innerText'): return True
66
- time.sleep(1)
67
- return False
68
-
69
- def _wait_table_stable(page, max_wait=8):
70
- """轮询表格行数, 等 SPA 异步加载完成.
71
- 结束条件: 行数>0 (有数据) 或 行数连续3次为0 (确实无表格/无数据) 或 超时."""
72
- deadline = time.time() + max_wait
73
- zero_streak = 0
74
- last = -1
75
- while time.time() < deadline:
76
- try:
77
- n = page.locator('.vxe-body--row').count()
78
- except Exception:
79
- n = 0
80
- if n > 0:
81
- # 有数据了, 再确认一次稳定 (避免loading中间态)
82
- time.sleep(0.6)
83
- try:
84
- if page.locator('.vxe-body--row').count() >= n:
85
- return n
86
- except Exception:
87
- return n
88
- else:
89
- zero_streak += 1
90
- if zero_streak >= 3:
91
- return 0 # 连续3次0, 认为确实无表格数据
92
- time.sleep(0.5)
93
- return last
94
-
95
- # ===== 锚点定位: 优先 recall, 否则按 anchor 字段匹配 =====
96
- def locate(page, anchor, desc=''):
97
- """根据 anchor {role,name,placeholder,field,text} 定位元素, 返回 locatorNone."""
98
- if not anchor: return None
99
- role = anchor.get('role','')
100
- name = anchor.get('name','')
101
- ph = anchor.get('placeholder','')
102
- field = anchor.get('field','')
103
- sels = []
104
- if field: sels.append("input[name='%s']" % field)
105
- if ph: sels.append("input[placeholder*='%s']" % ph)
106
- if role == 'tab' and name: sels.append(".ant-tabs-tab:has-text('%s')" % name)
107
- if role == 'button' and name: sels.append("button:has-text('%s')" % name)
108
- if role == 'textbox' and name: sels.append("input[placeholder*='%s']" % name)
109
- for sel in sels:
110
- loc = page.locator(sel).first
111
- try:
112
- if loc.count() > 0: return loc
113
- except Exception: pass
114
- # 表单字段可能异步渲染, 轮询等待最多 5s
115
- deadline = time.time() + 5
116
- while time.time() < deadline:
117
- for sel in sels:
118
- try:
119
- if page.locator(sel).first.count() > 0:
120
- return page.locator(sel).first
121
- except Exception: pass
122
- time.sleep(0.4)
123
- return None
124
-
125
- # ===== 单步执行 =====
126
- def exec_step(page, step, timeout=8000):
127
- """执行一步, 返回 (ok, detail)."""
128
- intent = step.get('intent','')
129
- desc = step.get('desc','')
130
- anchor = step.get('anchor', {}) or {}
131
- val = step.get('value','')
132
- try:
133
- if intent == 'goto':
134
- url = BASE + val if val.startswith('/') else val
135
- page.goto(url, wait_until='domcontentloaded', timeout=12000); time.sleep(2)
136
- # 等待表格数据加载完成 (列表页核心): 轮询直到行数>0 或稳定或超时
137
- _wait_table_stable(page, max_wait=8)
138
- return True, 'navigated'
139
- if intent == 'fill':
140
- loc = locate(page, anchor, desc)
141
- if not loc: return False, '元素未找到(%s)' % desc
142
- # value 可能是 {{ask:xxx}} 占位, 调用方应已替换; 这里直接填
143
- loc.fill(str(val), timeout=timeout); time.sleep(0.3)
144
- return True, 'filled'
145
- if intent == 'click':
146
- loc = locate(page, anchor, desc)
147
- if not loc: return False, '元素未找到(%s)' % desc
148
- loc.click(timeout=timeout); time.sleep(1.5)
149
- return True, 'clicked'
150
- if intent == 'observe':
151
- # 只读页面, 总是成功, 返回关键信息供后续断言
152
- return True, 'observed'
153
- if intent == 'extract':
154
- # 语义断言: 看 body 文本是否含 expected 关键词, 或满足 step.check
155
- body = page.evaluate('()=>document.body.innerText')
156
- check = step.get('check','') # 期望出现的关键词
157
- if check:
158
- return (check in body), ('命中:%s' % check if check in body else '未命中:%s'%check)
159
- return True, 'extracted(no check)'
160
- if intent == 'assert':
161
- # 确定性断言: step.check JS 表达式或条件
162
- check = step.get('check','')
163
- if check.startswith('rows>'):
164
- n = int(check[5:])
165
- _wait_table_stable(page, max_wait=6)
166
- rows = page.locator('.vxe-body--row').count()
167
- return rows>n, ('行数%d>%d'%(rows,n) if rows>n else '行数%d<=%d'%(rows,n))
168
- if check.startswith('rows=='):
169
- n = int(check[6:])
170
- _wait_table_stable(page, max_wait=6)
171
- rows = page.locator('.vxe-body--row').count()
172
- return rows==n, '行数=%d'%rows
173
- if check.startswith('url_contains'):
174
- cur = page.url
175
- kw = check.split(':',1)[1] if ':' in check else ''
176
- return (kw in cur), ('url含%s'%kw if kw in cur else 'url不含%s'%kw)
177
- if check.startswith('input_empty:'):
178
- field = check.split(':',1)[1]
179
- v = page.locator("input[name='%s']"%field).first.input_value()
180
- return v=='', '框值=%r'%v
181
- return True, 'assert(no check)'
182
- except Exception as e:
183
- return False, str(e)[:60]
184
- return False, '未知intent:%s' % intent
185
-
186
- # ===== 跑一个用例 =====
187
- def run_case(page, case):
188
- cid = case.get('case_id','?')
189
- steps = case.get('steps',[])
190
- details = []
191
- all_ok = True
192
- for step in steps:
193
- ok, detail = exec_step(page, step)
194
- details.append('%s:%s(%s)' % (step.get('intent','?'), '✓' if ok else '✗', detail))
195
- if not ok:
196
- all_ok = False
197
- break # 失败即停 (前置步骤失败后续无意义)
198
- return {'case_id': cid, 'title': case.get('title',''), 'pass': all_ok,
199
- 'expected': case.get('expected',''), 'details': details}
200
-
201
- # ===== 沉淀锚点 =====
202
- def sediment(page_path, anchors):
203
- if not anchors: return 0
204
- try:
205
- pj = json.dumps({'url':page_path,'elements':[
206
- {'role':a.get('role'),'name':a.get('name'),'desc':a.get('name')} for a in anchors
207
- ]}, ensure_ascii=False)
208
- subprocess.run([sys.executable, AUTOLEARN, 'learn', '--url', page_path, '--page-json', pj],
209
- capture_output=True, timeout=20)
210
- return len(anchors)
211
- except Exception:
212
- return 0
213
-
214
- def collect_anchors(page):
215
- try:
216
- els = page.evaluate('''()=>{const out=[];document.querySelectorAll('button,.ant-tabs-tab,input[placeholder],input[name],[role=tab]').forEach(el=>{const name=(el.innerText||el.getAttribute('placeholder')||el.getAttribute('name')||'').trim();const role=el.getAttribute('role')||(el.tagName==='INPUT'?'textbox':el.tagName.toLowerCase());if(name&&name.length<25&&!/^[0-9\s]+$/.test(name))out.push({role,name});});return out.slice(0,30);}''')
217
- seen=set(); uniq=[]
218
- for el in els:
219
- k=(el['role'],el['name'])
220
- if k not in seen: seen.add(k); uniq.append(el)
221
- return uniq
222
- except Exception:
223
- return []
224
-
225
- # ===== =====
226
- def main():
227
- ap = argparse.ArgumentParser()
228
- ap.add_argument('--task', required=True, help='任务文件 JSON')
229
- args = ap.parse_args()
230
- task = json.load(open(args.task, encoding='utf-8'))
231
- page_path = task['page_path']
232
- cases = task.get('cases', [])
233
-
234
- result = {'page': page_path, 'page_name': task.get('page_name',''),
235
- 'case_results': [], 'anchors_count': 0, 'error': ''}
236
-
237
- TOKEN = get_token()
238
- if not TOKEN:
239
- result['error'] = 'LOGIN_FAIL'; print(json.dumps(result, ensure_ascii=False)); sys.exit(1)
240
-
241
- # 录屏目录
242
- video_dir = os.path.join(MEMBER_DIR, 'drafts', 'videos')
243
- os.makedirs(video_dir, exist_ok=True)
244
-
245
- with sync_playwright() as pw:
246
- b = pw.chromium.launch(headless=True)
247
- ctx = b.new_context(locale='zh-CN', viewport={'width':1440,'height':900},
248
- record_video_dir=video_dir)
249
- page = ctx.new_page()
250
- page.set_default_timeout(12000)
251
- try:
252
- if not init_session(page, TOKEN):
253
- result['error']='INIT_FAIL'; print(json.dumps(result,ensure_ascii=False)); b.close(); sys.exit(1)
254
- # goto 目标页
255
- page.goto(BASE+page_path, wait_until='domcontentloaded'); time.sleep(2.5)
256
- # 等表格
257
- for _ in range(10):
258
- if page.locator('.vxe-body--row').count()>0: break
259
- time.sleep(0.5)
260
- # 跑每个用例
261
- for case in cases:
262
- # 每个用例前重新 goto 保证干净状态
263
- page.goto(BASE+page_path, wait_until='domcontentloaded'); time.sleep(2)
264
- cr = run_case(page, case)
265
- result['case_results'].append(cr)
266
- tag = '✓' if cr['pass'] else '✗'
267
- print(' %s %s %s' % (tag, cr['case_id'], cr['title'][:30]))
268
- for d in cr['details']: print(' %s' % d)
269
- # 收集沉淀
270
- anc = collect_anchors(page)
271
- result['anchors_count'] = len(anc)
272
- sediment(page_path, anc)
273
- except Exception as e:
274
- result['error'] = str(e)[:100]
275
- # 关 page 触发视频落盘, 再取路径
276
- try:
277
- video_path = page.video.path() if page.video else None
278
- except Exception:
279
- video_path = None
280
- ctx.close()
281
- b.close()
282
- result['video'] = video_path or ''
283
-
284
- passed = sum(1 for c in result['case_results'] if c['pass'])
285
- total = len(result['case_results'])
286
- result['summary'] = '%d/%d 通过' % (passed, total)
287
- print()
288
- print('汇总: %s 锚点%d %s' % (result['summary'], result['anchors_count'], result['error']))
289
- if result.get('video'):
290
- print('录屏: %s' % result['video'])
291
- # 结果写文件
292
- out = args.task.replace('.json','_run.json')
293
- json.dump(result, open(out,'w',encoding='utf-8'), ensure_ascii=False, indent=2)
294
- print('结果: %s' % out)
295
-
296
- if __name__ == '__main__':
297
- main()
1
+ # -*- coding: utf-8 -*-
2
+ # v3.0 路径自举: 引导到 common/bootstrap, 统一 sys.path 逻辑
3
+ import os as _o, sys as _s
4
+ _f = _o.path.abspath(__file__)
5
+ for _ in range(10):
6
+ _f = _o.path.dirname(_f)
7
+ _cp = _o.path.join(_f, 'foundation')
8
+ if _o.path.isfile(_o.path.join(_cp, 'bootstrap.py')):
9
+ break
10
+ if _cp not in _s.path: _s.path.insert(0, _cp)
11
+ from bootstrap import setup; setup()
12
+
13
+ """autotest_run.py — 真功能测试执行器 (ZCode 直接驱动 Playwright).
14
+ 用法: python autotest_run.py --task <任务文件>
15
+ 任务文件 JSON: {
16
+ "page_path": "/veh/vehicle/vehAffair/insurance",
17
+ "page_name": "保险管理",
18
+ "cases": [ {case_id, title, steps:[{intent,desc,value,anchor}], expected, assert_fn} ]
19
+ }
20
+ --task 是必须的 (绕过 MSYS 路径转换; /xxx 会被 bash 转成 Windows 路径)
21
+
22
+ 真测: 每步真实驱动浏览器 (goto/fill/click/extract/assert), 按断言判定 pass/fail.
23
+ 覆盖与 QoderWork wl-test 相同的工作流 (路由查询→登录态→意图式用例→执行→沉淀).
24
+ """
25
+ import json, sys, time, os, re, argparse, subprocess
26
+ from playwright.sync_api import sync_playwright
27
+
28
+ # ===== 配置 (路径自检测) =====
29
+ SCRIPTS = os.path.dirname(os.path.abspath(__file__))
30
+ # test 目录在 .qoder/scripts/test, 回项目根要上 3 级. 向上找含 .qoder 的目录兜底,
31
+ # 避免 ../.. 层级写错 (test 目录比 scripts 根下别的脚本深一级).
32
+ _d = SCRIPTS
33
+ while _d != os.path.dirname(_d):
34
+ if os.path.isdir(os.path.join(_d, '.qoder')):
35
+ PROJECT = _d
36
+ break
37
+ _d = os.path.dirname(_d)
38
+ else:
39
+ PROJECT = os.path.normpath(os.path.join(SCRIPTS, '..', '..', '..'))
40
+
41
+
42
+ def _read_developer():
43
+ """读 .qoder/.developer 拿当前开发者, 身份隔离用 (绝不 fallback, 防串号)."""
44
+ dev_file = os.path.join(PROJECT, '.qoder', '.developer')
45
+ if os.path.isfile(dev_file):
46
+ for ln in open(dev_file, encoding='utf-8'):
47
+ if ln.strip().startswith('name='):
48
+ return ln.strip().split('=', 1)[1].strip()
49
+ raise RuntimeError('未检测到开发者身份(.qoder/.developer)。请先运行 /wl-init。')
50
+
51
+
52
+ DEVELOPER = _read_developer()
53
+ MEMBER_DIR = os.path.join(PROJECT, 'workspace', 'members', DEVELOPER)
54
+ AUTH_FILE = os.path.join(MEMBER_DIR, 'auth-state.json')
55
+ SECURELS_JS = os.path.join(SCRIPTS, 'secure-ls.js')
56
+ AUTOLEARN = os.path.join(SCRIPTS, 'autotest.py')
57
+ BASE = 'https://icsqas.inforecloud.com'
58
+ ACCESS_KEY = 'ICS-1.3.8-prod-core-access'
59
+ SECRET = 'please-replace-me-with-your-own-key'
60
+
61
+ # ===== 登录态: 读 token, 失效则调 kg_auto_login.py =====
62
+ # v3.0: 从 common.autotest_auth 复用 (单一真源, 消除 3 处重复)
63
+ try:
64
+ sys.path.insert(0, SCRIPTS)
65
+ from test.autotest_auth import get_token as _get_token_shared
66
+ def get_token():
67
+ return _get_token_shared(dev=DEVELOPER, base_url=BASE)
68
+ except Exception:
69
+ # 回退: 内联逻辑 (common 不可用时, 与 common/autotest_auth 保持一致)
70
+ def get_token():
71
+ if os.path.exists(AUTH_FILE):
72
+ tok = json.load(open(AUTH_FILE, encoding='utf-8')).get('access_token')
73
+ import requests
74
+ try:
75
+ r = requests.get(BASE+'/test-api/system/user/getInfo',
76
+ headers={'Authorization':'Bearer '+tok,'clientid':'e5cd7e4891bf95d1d19206ce24a7b32e'}, timeout=8)
77
+ if r.json().get('code')==200: return tok
78
+ except Exception: pass
79
+ subprocess.run([sys.executable, os.path.join(SCRIPTS,'kg_auto_login.py')], capture_output=True, timeout=120)
80
+ if os.path.exists(AUTH_FILE):
81
+ return json.load(open(AUTH_FILE, encoding='utf-8')).get('access_token')
82
+ return None
83
+
84
+ # ===== SPA 会话: 注入 token + 初始化菜单 =====
85
+ def init_session(page, token):
86
+ page.goto(BASE+'/auth/login', wait_until='domcontentloaded'); time.sleep(1.2)
87
+ page.add_script_tag(path=SECURELS_JS); time.sleep(0.3)
88
+ page.evaluate('''(a)=>{const[t,k,s]=a;const S=window.SecureLS.default||window.SecureLS;const ls=new S({encodingType:'aes',isCompression:true,encryptionSecret:s});ls.set(k,{accessToken:t,refreshToken:t,accessCodes:[],originMenus:[],webMenus:[],accessMenus:[],accessRoutes:[],isAccessChecked:false,loginExpired:false});}''',[token,ACCESS_KEY,SECRET])
89
+ page.goto(BASE+'/dashboard/workspace', wait_until='domcontentloaded')
90
+ for _ in range(15):
91
+ if '加载菜单中' not in page.evaluate('()=>document.body.innerText'): return True
92
+ time.sleep(1)
93
+ return False
94
+
95
+ def _wait_table_stable(page, max_wait=8):
96
+ """轮询表格行数, SPA 异步加载完成.
97
+ 结束条件: 行数>0 (有数据) 行数连续3次为0 (确实无表格/无数据)超时."""
98
+ deadline = time.time() + max_wait
99
+ zero_streak = 0
100
+ last = -1
101
+ while time.time() < deadline:
102
+ try:
103
+ n = page.locator('.vxe-body--row').count()
104
+ except Exception:
105
+ n = 0
106
+ if n > 0:
107
+ # 有数据了, 再确认一次稳定 (避免loading中间态)
108
+ time.sleep(0.6)
109
+ try:
110
+ if page.locator('.vxe-body--row').count() >= n:
111
+ return n
112
+ except Exception:
113
+ return n
114
+ else:
115
+ zero_streak += 1
116
+ if zero_streak >= 3:
117
+ return 0 # 连续3次0, 认为确实无表格数据
118
+ time.sleep(0.5)
119
+ return last
120
+
121
+ # ===== 锚点定位: 优先 recall, 否则按 anchor 字段匹配 =====
122
+ def locate(page, anchor, desc=''):
123
+ """根据 anchor {role,name,placeholder,field,text} 定位元素, 返回 locator 或 None."""
124
+ if not anchor: return None
125
+ role = anchor.get('role','')
126
+ name = anchor.get('name','')
127
+ ph = anchor.get('placeholder','')
128
+ field = anchor.get('field','')
129
+ sels = []
130
+ if field: sels.append("input[name='%s']" % field)
131
+ if ph: sels.append("input[placeholder*='%s']" % ph)
132
+ if role == 'tab' and name: sels.append(".ant-tabs-tab:has-text('%s')" % name)
133
+ if role == 'button' and name: sels.append("button:has-text('%s')" % name)
134
+ if role == 'textbox' and name: sels.append("input[placeholder*='%s']" % name)
135
+ for sel in sels:
136
+ loc = page.locator(sel).first
137
+ try:
138
+ if loc.count() > 0: return loc
139
+ except Exception: pass
140
+ # 表单字段可能异步渲染, 轮询等待最多 5s
141
+ deadline = time.time() + 5
142
+ while time.time() < deadline:
143
+ for sel in sels:
144
+ try:
145
+ if page.locator(sel).first.count() > 0:
146
+ return page.locator(sel).first
147
+ except Exception: pass
148
+ time.sleep(0.4)
149
+ return None
150
+
151
+ # ===== 单步执行 =====
152
+ def exec_step(page, step, timeout=8000):
153
+ """执行一步, 返回 (ok, detail)."""
154
+ intent = step.get('intent','')
155
+ desc = step.get('desc','')
156
+ anchor = step.get('anchor', {}) or {}
157
+ val = step.get('value','')
158
+ try:
159
+ if intent == 'goto':
160
+ url = BASE + val if val.startswith('/') else val
161
+ page.goto(url, wait_until='domcontentloaded', timeout=12000); time.sleep(2)
162
+ # 等待表格数据加载完成 (列表页核心): 轮询直到行数>0 或稳定或超时
163
+ _wait_table_stable(page, max_wait=8)
164
+ return True, 'navigated'
165
+ if intent == 'fill':
166
+ loc = locate(page, anchor, desc)
167
+ if not loc: return False, '元素未找到(%s)' % desc
168
+ # value 可能是 {{ask:xxx}} 占位, 调用方应已替换; 这里直接填
169
+ loc.fill(str(val), timeout=timeout); time.sleep(0.3)
170
+ return True, 'filled'
171
+ if intent == 'click':
172
+ loc = locate(page, anchor, desc)
173
+ if not loc: return False, '元素未找到(%s)' % desc
174
+ loc.click(timeout=timeout); time.sleep(1.5)
175
+ return True, 'clicked'
176
+ if intent == 'observe':
177
+ # 只读页面, 总是成功, 返回关键信息供后续断言
178
+ return True, 'observed'
179
+ if intent == 'extract':
180
+ # 语义断言: 看 body 文本是否含 expected 关键词, 或满足 step.check
181
+ body = page.evaluate('()=>document.body.innerText')
182
+ check = step.get('check','') # 期望出现的关键词
183
+ if check:
184
+ return (check in body), ('命中:%s' % check if check in body else '未命中:%s'%check)
185
+ return True, 'extracted(no check)'
186
+ if intent == 'assert':
187
+ # 确定性断言: step.check 是 JS 表达式或条件
188
+ check = step.get('check','')
189
+ if check.startswith('rows>'):
190
+ n = int(check[5:])
191
+ _wait_table_stable(page, max_wait=6)
192
+ rows = page.locator('.vxe-body--row').count()
193
+ return rows>n, ('行数%d>%d'%(rows,n) if rows>n else '行数%d<=%d'%(rows,n))
194
+ if check.startswith('rows=='):
195
+ n = int(check[6:])
196
+ _wait_table_stable(page, max_wait=6)
197
+ rows = page.locator('.vxe-body--row').count()
198
+ return rows==n, '行数=%d'%rows
199
+ if check.startswith('url_contains'):
200
+ cur = page.url
201
+ kw = check.split(':',1)[1] if ':' in check else ''
202
+ return (kw in cur), ('url含%s'%kw if kw in cur else 'url不含%s'%kw)
203
+ if check.startswith('input_empty:'):
204
+ field = check.split(':',1)[1]
205
+ v = page.locator("input[name='%s']"%field).first.input_value()
206
+ return v=='', '框值=%r'%v
207
+ return True, 'assert(no check)'
208
+ except Exception as e:
209
+ return False, str(e)[:60]
210
+ return False, '未知intent:%s' % intent
211
+
212
+ # ===== 跑一个用例 =====
213
+ def run_case(page, case):
214
+ cid = case.get('case_id','?')
215
+ steps = case.get('steps',[])
216
+ details = []
217
+ all_ok = True
218
+ for step in steps:
219
+ ok, detail = exec_step(page, step)
220
+ details.append('%s:%s(%s)' % (step.get('intent','?'), '✓' if ok else '✗', detail))
221
+ if not ok:
222
+ all_ok = False
223
+ break # 失败即停 (前置步骤失败后续无意义)
224
+ return {'case_id': cid, 'title': case.get('title',''), 'pass': all_ok,
225
+ 'expected': case.get('expected',''), 'details': details}
226
+
227
+ # ===== 沉淀锚点 =====
228
+ def sediment(page_path, anchors):
229
+ if not anchors: return 0
230
+ try:
231
+ pj = json.dumps({'url':page_path,'elements':[
232
+ {'role':a.get('role'),'name':a.get('name'),'desc':a.get('name')} for a in anchors
233
+ ]}, ensure_ascii=False)
234
+ subprocess.run([sys.executable, AUTOLEARN, 'learn', '--url', page_path, '--page-json', pj],
235
+ capture_output=True, timeout=20)
236
+ return len(anchors)
237
+ except Exception:
238
+ return 0
239
+
240
+ def collect_anchors(page):
241
+ try:
242
+ els = page.evaluate('''()=>{const out=[];document.querySelectorAll('button,.ant-tabs-tab,input[placeholder],input[name],[role=tab]').forEach(el=>{const name=(el.innerText||el.getAttribute('placeholder')||el.getAttribute('name')||'').trim();const role=el.getAttribute('role')||(el.tagName==='INPUT'?'textbox':el.tagName.toLowerCase());if(name&&name.length<25&&!/^[0-9\s]+$/.test(name))out.push({role,name});});return out.slice(0,30);}''')
243
+ seen=set(); uniq=[]
244
+ for el in els:
245
+ k=(el['role'],el['name'])
246
+ if k not in seen: seen.add(k); uniq.append(el)
247
+ return uniq
248
+ except Exception:
249
+ return []
250
+
251
+ # ===== 主 =====
252
+ def main():
253
+ ap = argparse.ArgumentParser()
254
+ ap.add_argument('--task', required=True, help='任务文件 JSON')
255
+ args = ap.parse_args()
256
+ task = json.load(open(args.task, encoding='utf-8'))
257
+ page_path = task['page_path']
258
+ cases = task.get('cases', [])
259
+
260
+ result = {'page': page_path, 'page_name': task.get('page_name',''),
261
+ 'case_results': [], 'anchors_count': 0, 'error': ''}
262
+
263
+ TOKEN = get_token()
264
+ if not TOKEN:
265
+ result['error'] = 'LOGIN_FAIL'; print(json.dumps(result, ensure_ascii=False)); sys.exit(1)
266
+
267
+ # 录屏目录
268
+ video_dir = os.path.join(MEMBER_DIR, 'drafts', 'videos')
269
+ os.makedirs(video_dir, exist_ok=True)
270
+
271
+ with sync_playwright() as pw:
272
+ b = pw.chromium.launch(headless=True)
273
+ ctx = b.new_context(locale='zh-CN', viewport={'width':1440,'height':900},
274
+ record_video_dir=video_dir)
275
+ page = ctx.new_page()
276
+ page.set_default_timeout(12000)
277
+ try:
278
+ if not init_session(page, TOKEN):
279
+ result['error']='INIT_FAIL'; print(json.dumps(result,ensure_ascii=False)); b.close(); sys.exit(1)
280
+ # 先 goto 目标页
281
+ page.goto(BASE+page_path, wait_until='domcontentloaded'); time.sleep(2.5)
282
+ # 等表格
283
+ for _ in range(10):
284
+ if page.locator('.vxe-body--row').count()>0: break
285
+ time.sleep(0.5)
286
+ # 跑每个用例
287
+ for case in cases:
288
+ # 每个用例前重新 goto 保证干净状态
289
+ page.goto(BASE+page_path, wait_until='domcontentloaded'); time.sleep(2)
290
+ cr = run_case(page, case)
291
+ result['case_results'].append(cr)
292
+ tag = '' if cr['pass'] else '✗'
293
+ print(' %s %s %s' % (tag, cr['case_id'], cr['title'][:30]))
294
+ for d in cr['details']: print(' %s' % d)
295
+ # 收集沉淀
296
+ anc = collect_anchors(page)
297
+ result['anchors_count'] = len(anc)
298
+ sediment(page_path, anc)
299
+ except Exception as e:
300
+ result['error'] = str(e)[:100]
301
+ # 关 page 触发视频落盘, 再取路径
302
+ try:
303
+ video_path = page.video.path() if page.video else None
304
+ except Exception:
305
+ video_path = None
306
+ ctx.close()
307
+ b.close()
308
+ result['video'] = video_path or ''
309
+
310
+ passed = sum(1 for c in result['case_results'] if c['pass'])
311
+ total = len(result['case_results'])
312
+ result['summary'] = '%d/%d 通过' % (passed, total)
313
+ print()
314
+ print('汇总: %s 锚点%d %s' % (result['summary'], result['anchors_count'], result['error']))
315
+ if result.get('video'):
316
+ print('录屏: %s' % result['video'])
317
+ # 结果写文件
318
+ out = args.task.replace('.json','_run.json')
319
+ json.dump(result, open(out,'w',encoding='utf-8'), ensure_ascii=False, indent=2)
320
+ print('结果: %s' % out)
321
+
322
+ if __name__ == '__main__':
323
+ main()