@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.
- package/bin/cli.js +1435 -1107
- package/package.json +29 -29
- package/templates/qoder/agents/insight-planning.md +1 -1
- package/templates/qoder/agents/insight-research.md +28 -15
- package/templates/qoder/commands/optional/wl-insight.md +161 -161
- package/templates/qoder/commands/optional/wl-report.md +20 -39
- package/templates/qoder/commands/optional/wl-status.md +4 -4
- package/templates/qoder/commands/wl-code.md +2 -2
- package/templates/qoder/commands/wl-design.md +6 -6
- package/templates/qoder/commands/wl-init.md +3 -3
- package/templates/qoder/commands/wl-prd.md +26 -13
- package/templates/qoder/commands/wl-req.md +43 -0
- package/templates/qoder/commands/wl-search.md +78 -47
- package/templates/qoder/commands/wl-task.md +343 -40
- package/templates/qoder/commands/wl-test.md +43 -17
- package/templates/qoder/config.yaml +52 -14
- package/templates/qoder/hooks/post-tool-use.py +181 -0
- package/templates/qoder/hooks/session-start.py +94 -23
- package/templates/qoder/hooks/stop-eval.py +207 -0
- package/templates/qoder/hooks/user-prompt-submit.py +139 -0
- package/templates/qoder/rules/wl-pipeline.md +85 -49
- package/templates/qoder/scripts/README.md +102 -0
- package/templates/qoder/scripts/capability/__init__.py +26 -0
- package/templates/qoder/scripts/capability/__main__.py +72 -0
- package/templates/qoder/scripts/capability/adapters/__init__.py +29 -0
- package/templates/qoder/scripts/capability/adapters/cli.py +316 -0
- package/templates/qoder/scripts/capability/adapters/mcp.py +334 -0
- package/templates/qoder/scripts/capability/adapters/qw.py +271 -0
- package/templates/qoder/scripts/capability/caps/__init__.py +27 -0
- package/templates/qoder/scripts/capability/caps/context.py +36 -0
- package/templates/qoder/scripts/capability/caps/cron.py +50 -0
- package/templates/qoder/scripts/capability/caps/identity.py +43 -0
- package/templates/qoder/scripts/capability/caps/memory.py +71 -0
- package/templates/qoder/scripts/capability/caps/notify.py +41 -0
- package/templates/qoder/scripts/capability/caps/present.py +48 -0
- package/templates/qoder/scripts/capability/caps/repo.py +31 -0
- package/templates/qoder/scripts/capability/caps/sandbox.py +43 -0
- package/templates/qoder/scripts/capability/chain.py +92 -0
- package/templates/qoder/scripts/capability/memory_chain.py +41 -0
- package/templates/qoder/scripts/capability/registry.py +246 -0
- package/templates/qoder/scripts/capability/registry_mcp.py +250 -0
- package/templates/qoder/scripts/capability/smoke_test.py +211 -0
- package/templates/qoder/scripts/capability/smoke_test_report.json +94 -0
- package/templates/qoder/scripts/deployment/setup/__init__.py +11 -0
- package/templates/qoder/scripts/deployment/setup/carriers.py +669 -0
- package/templates/qoder/scripts/deployment/setup/carriers_verify.py +148 -0
- package/templates/qoder/scripts/{init_doctor.py → deployment/setup/init_doctor.py} +100 -39
- package/templates/qoder/scripts/{install_qoderwork.py → deployment/setup/install_qoderwork.py} +48 -30
- package/templates/qoder/scripts/{platform_doctor.py → deployment/setup/platform_doctor.py} +271 -259
- package/templates/qoder/scripts/{repo_root.py → deployment/setup/repo_root.py} +9 -2
- package/templates/qoder/scripts/{setup.py → deployment/setup/setup.py} +143 -14
- package/templates/qoder/scripts/{setup_lanhu.py → deployment/setup/setup_lanhu.py} +979 -963
- package/templates/qoder/scripts/domain/design/__init__.py +0 -0
- package/templates/qoder/scripts/{fill_prototype.py → domain/design/fill_prototype.py} +17 -5
- package/templates/qoder/scripts/{gen_design_doc.py → domain/design/gen_design_doc.py} +406 -394
- package/templates/qoder/scripts/domain/kg/__init__.py +11 -0
- package/templates/qoder/scripts/domain/kg/build/__init__.py +0 -0
- package/templates/qoder/scripts/domain/kg/build/build_entity_registry.py +201 -0
- package/templates/qoder/scripts/domain/kg/build/build_relations.py +132 -0
- package/templates/qoder/scripts/{build_style_index.py → domain/kg/build/build_style_index.py} +50 -15
- package/templates/qoder/scripts/domain/kg/build/build_workflows.py +149 -0
- package/templates/qoder/scripts/{kg_build.py → domain/kg/build/kg_build.py} +685 -612
- package/templates/qoder/scripts/{kg_build_db.py → domain/kg/build/kg_build_db.py} +338 -327
- package/templates/qoder/scripts/{kg_incremental.py → domain/kg/build/kg_incremental.py} +425 -393
- package/templates/qoder/scripts/{learn_aggregate.py → domain/kg/build/learn_aggregate.py} +212 -201
- package/templates/qoder/scripts/domain/kg/extract/__init__.py +0 -0
- package/templates/qoder/scripts/{common → domain/kg/extract}/extract.py +430 -419
- package/templates/qoder/scripts/domain/kg/extract/test_extract.py +115 -0
- package/templates/qoder/scripts/{common → domain/kg/extract}/ts_extract.py +614 -536
- package/templates/qoder/scripts/domain/kg/graph/__init__.py +0 -0
- package/templates/qoder/scripts/{kg_link_db.py → domain/kg/graph/kg_link_db.py} +235 -224
- package/templates/qoder/scripts/domain/kg/graph/kg_semantic.py +683 -0
- package/templates/qoder/scripts/{kg.py → domain/kg/kg.py} +871 -708
- package/templates/qoder/scripts/domain/kg/kg_capabilities.py +182 -0
- package/templates/qoder/scripts/domain/kg/search/__init__.py +0 -0
- package/templates/qoder/scripts/{context_pack.py → domain/kg/search/context_pack.py} +45 -17
- package/templates/qoder/scripts/{enrich_prompt.py → domain/kg/search/enrich_prompt.py} +238 -226
- package/templates/qoder/scripts/domain/kg/search/prefetch.py +384 -0
- package/templates/qoder/scripts/{common → domain/kg/search}/search_engine.py +207 -205
- package/templates/qoder/scripts/{search_index.py → domain/kg/search/search_index.py} +210 -88
- package/templates/qoder/scripts/domain/kg/server/__init__.py +0 -0
- package/templates/qoder/scripts/domain/kg/server/kgd.py +549 -0
- package/templates/qoder/scripts/domain/kg/server/perf_bench.py +197 -0
- package/templates/qoder/scripts/domain/kg/storage/__init__.py +0 -0
- package/templates/qoder/scripts/{kg_duckdb.py → domain/kg/storage/kg_duckdb.py} +70 -45
- package/templates/qoder/scripts/domain/learning/__init__.py +0 -0
- package/templates/qoder/scripts/{learn.py → domain/learning/learn.py} +157 -146
- package/templates/qoder/scripts/domain/report/__init__.py +11 -0
- package/templates/qoder/scripts/{add_session.py → domain/report/add_session.py} +256 -244
- package/templates/qoder/scripts/{export.py → domain/report/export.py} +72 -4
- package/templates/qoder/scripts/{report.py → domain/report/report.py} +292 -281
- package/templates/qoder/scripts/domain/report/report_snapshot.py +261 -0
- package/templates/qoder/scripts/domain/report/role.py +39 -0
- package/templates/qoder/scripts/{status.py → domain/report/status.py} +737 -628
- package/templates/qoder/scripts/domain/report/status_snapshot.py +191 -0
- package/templates/qoder/scripts/domain/requirement/__init__.py +0 -0
- package/templates/qoder/scripts/{archive_prd.py → domain/requirement/archive_prd.py} +389 -377
- package/templates/qoder/scripts/domain/requirement/req.py +228 -0
- package/templates/qoder/scripts/domain/task/__init__.py +11 -0
- package/templates/qoder/scripts/{git_sync.py → domain/task/git_sync.py} +90 -50
- package/templates/qoder/scripts/{syncgate.py → domain/task/syncgate.py} +18 -7
- package/templates/qoder/scripts/domain/task/task.py +229 -0
- package/templates/qoder/scripts/domain/task/task_lifecycle.py +606 -0
- package/templates/qoder/scripts/domain/task/task_query.py +162 -0
- package/templates/qoder/scripts/domain/task/task_relations.py +425 -0
- package/templates/qoder/scripts/{team_sync.py → domain/task/team_sync.py} +101 -23
- package/templates/qoder/scripts/domain/task/zentao_sync.py +370 -0
- package/templates/qoder/scripts/foundation/__init__.py +0 -0
- package/templates/qoder/scripts/foundation/bootstrap.py +145 -0
- package/templates/qoder/scripts/foundation/core/__init__.py +0 -0
- package/templates/qoder/scripts/foundation/core/cmd_registry.py +112 -0
- package/templates/qoder/scripts/foundation/core/config.py +187 -0
- package/templates/qoder/scripts/{common → foundation/core}/paths.py +706 -400
- package/templates/qoder/scripts/foundation/data/__init__.py +0 -0
- package/templates/qoder/scripts/foundation/data/contract.py +317 -0
- package/templates/qoder/scripts/{common → foundation/data}/events.py +2 -2
- package/templates/qoder/scripts/foundation/data/result.py +223 -0
- package/templates/qoder/scripts/{common → foundation/data}/task_utils.py +427 -392
- package/templates/qoder/scripts/foundation/identity/__init__.py +0 -0
- package/templates/qoder/scripts/foundation/identity/check_publish.py +103 -0
- package/templates/qoder/scripts/{common → foundation/identity}/developer.py +239 -238
- package/templates/qoder/scripts/foundation/identity/guard.py +159 -0
- package/templates/qoder/scripts/{common → foundation/identity}/identity.py +132 -9
- package/templates/qoder/scripts/foundation/identity/roles.py +60 -0
- package/templates/qoder/scripts/foundation/integrations/__init__.py +0 -0
- package/templates/qoder/scripts/{common → foundation/integrations}/active_task.py +30 -15
- package/templates/qoder/scripts/{common → foundation/integrations}/feishu.py +11 -10
- package/templates/qoder/scripts/{common → foundation/integrations}/terms.py +3 -3
- package/templates/qoder/scripts/foundation/integrations/zentao_client.py +220 -0
- package/templates/qoder/scripts/foundation/io/__init__.py +0 -0
- package/templates/qoder/scripts/{common → foundation/io}/filelock.py +1 -1
- package/templates/qoder/scripts/{common → foundation/io}/reqid.py +1 -1
- package/templates/qoder/scripts/foundation/protocol/__init__.py +0 -0
- package/templates/qoder/scripts/foundation/protocol/duckdb_conn.py +39 -0
- package/templates/qoder/scripts/foundation/protocol/mcp_base.py +268 -0
- package/templates/qoder/scripts/foundation/utils/__init__.py +0 -0
- package/templates/qoder/scripts/{common → foundation/utils}/eval_api.py +12 -1
- package/templates/qoder/scripts/{common → foundation/utils}/pip_install.py +23 -6
- package/templates/qoder/scripts/{common → foundation/utils}/platform_guard.py +1 -1
- package/templates/qoder/scripts/orchestration/__init__.py +0 -0
- package/templates/qoder/scripts/orchestration/wlkj.py +185 -0
- package/templates/qoder/scripts/protocol/__init__.py +0 -0
- package/templates/qoder/scripts/protocol/browser/README.md +207 -0
- package/templates/qoder/scripts/protocol/browser/SKILL.md +265 -0
- package/templates/qoder/scripts/protocol/browser/config.env +9 -0
- package/templates/qoder/scripts/protocol/browser/references/cdp-api.md +110 -0
- package/templates/qoder/scripts/protocol/browser/references/migration-2.5.3.md +72 -0
- package/templates/qoder/scripts/protocol/browser/references/site-patterns/.gitkeep +0 -0
- package/templates/qoder/scripts/protocol/browser/scripts/browser-discovery.mjs +138 -0
- package/templates/qoder/scripts/protocol/browser/scripts/cdp-proxy.mjs +672 -0
- package/templates/qoder/scripts/protocol/browser/scripts/check-deps.mjs +206 -0
- package/templates/qoder/scripts/protocol/browser/scripts/find-url.mjs +253 -0
- package/templates/qoder/scripts/protocol/browser/scripts/match-site.mjs +46 -0
- package/templates/qoder/scripts/protocol/browser/templates/config.env.template +9 -0
- package/templates/qoder/scripts/protocol/mcp/__init__.py +11 -0
- package/templates/qoder/scripts/protocol/mcp/kg_mcp_server.py +478 -0
- package/templates/qoder/scripts/{lanhu_stdio_wrapper.py → protocol/mcp/lanhu_stdio_wrapper.py} +131 -119
- package/templates/qoder/scripts/protocol/mcp/mcp_doctor.py +548 -0
- package/templates/qoder/scripts/protocol/mcp/mcp_launcher.py +600 -0
- package/templates/qoder/scripts/protocol/mcp/mysql_mcp_server.py +461 -0
- package/templates/qoder/scripts/protocol/mcp/zentao_mcp_server.py +1920 -0
- package/templates/qoder/scripts/protocol/transports/__init__.py +56 -0
- package/templates/qoder/scripts/protocol/transports/base.py +87 -0
- package/templates/qoder/scripts/protocol/transports/cli.py +132 -0
- package/templates/qoder/scripts/protocol/transports/http.py +141 -0
- package/templates/qoder/scripts/protocol/transports/stdio.py +293 -0
- package/templates/qoder/scripts/run_weekly_update.bat +27 -18
- package/templates/qoder/scripts/run_weekly_update.sh +22 -13
- package/templates/qoder/scripts/validation/__init__.py +0 -0
- package/templates/qoder/scripts/validation/eval/__init__.py +0 -0
- package/templates/qoder/scripts/validation/eval/perf-report-2026-06-24.md +139 -0
- package/templates/qoder/scripts/validation/eval/qwork_harness.py +617 -0
- package/templates/qoder/scripts/validation/eval/report-commands.md +220 -0
- package/templates/qoder/scripts/validation/eval/transcript_timing.py +386 -0
- package/templates/qoder/scripts/validation/metrics/__init__.py +0 -0
- package/templates/qoder/scripts/{eval_prd.py → validation/metrics/eval_prd.py} +84 -15
- package/templates/qoder/scripts/validation/metrics/usability_score.py +750 -0
- package/templates/qoder/scripts/validation/test/__init__.py +11 -0
- package/templates/qoder/scripts/validation/test/assertion_gen.py +551 -0
- package/templates/qoder/scripts/{autotest.py → validation/test/autotest.py} +1234 -1751
- package/templates/qoder/scripts/validation/test/autotest_auth.py +109 -0
- package/templates/qoder/scripts/{autotest_batch.py → validation/test/autotest_batch.py} +255 -224
- package/templates/qoder/scripts/validation/test/autotest_data.py +680 -0
- package/templates/qoder/scripts/validation/test/autotest_opencli.py +646 -0
- package/templates/qoder/scripts/{autotest_run.py → validation/test/autotest_run.py} +323 -297
- package/templates/qoder/scripts/validation/test/autotest_webaccess.py +571 -0
- package/templates/qoder/scripts/{benchmark.py → validation/test/benchmark.py} +17 -5
- package/templates/qoder/scripts/{kg_auto_login.py → validation/test/kg_auto_login.py} +208 -196
- package/templates/qoder/scripts/{kg_test_runner.py → validation/test/kg_test_runner.py} +13 -2
- package/templates/qoder/scripts/{page_probe.py → validation/test/page_probe.py} +470 -459
- package/templates/qoder/scripts/validation/test/smoke_all_commands.py +170 -0
- package/templates/qoder/settings.json +23 -2
- package/templates/qoder/skills/design-import/SKILL.md +235 -226
- package/templates/qoder/skills/design-review/SKILL.md +91 -82
- package/templates/qoder/skills/prd-generator/SKILL.md +38 -24
- package/templates/qoder/skills/prd-review/SKILL.md +8 -6
- package/templates/qoder/skills/prototype-generator/SKILL.md +296 -247
- package/templates/qoder/skills/spec-coder/SKILL.md +24 -15
- package/templates/qoder/skills/spec-generator/SKILL.md +24 -12
- package/templates/qoder/skills/test-generator/SKILL.md +9 -7
- package/templates/qoder/skills/wl-code/SKILL.md +25 -13
- package/templates/qoder/skills/wl-commit/SKILL.md +10 -9
- package/templates/qoder/skills/wl-design/SKILL.md +7 -5
- package/templates/qoder/skills/wl-init/SKILL.md +8 -8
- package/templates/qoder/skills/wl-insight/SKILL.md +25 -12
- package/templates/qoder/skills/wl-prd-full/SKILL.md +59 -8
- package/templates/qoder/skills/wl-prd-quick/SKILL.md +7 -7
- package/templates/qoder/skills/wl-prd-review/SKILL.md +18 -6
- package/templates/qoder/skills/wl-report/SKILL.md +27 -25
- package/templates/qoder/skills/wl-search/SKILL.md +151 -80
- package/templates/qoder/skills/wl-spec/SKILL.md +9 -7
- package/templates/qoder/skills/wl-status/SKILL.md +50 -18
- package/templates/qoder/skills/wl-task/SKILL.md +94 -12
- package/templates/qoder/skills/wl-test/SKILL.md +154 -45
- package/templates/qoder/templates/prd-full-template.md +7 -0
- package/templates/root/AGENTS.md +275 -237
- package/templates/root/requirements.txt +3 -0
- package/templates/root//344/275/277/347/224/250/350/257/264/346/230/216.md +3 -3
- package/templates/root//346/226/260/346/211/213/346/214/207/345/215/227.md +2 -2
- package/templates/qoder/hooks/inject-workflow-state.py +0 -169
- package/templates/qoder/scripts/__pycache__/check_mcp_launch.cpython-39.pyc +0 -0
- package/templates/qoder/scripts/__pycache__/install_qoderwork.cpython-39.pyc +0 -0
- package/templates/qoder/scripts/__pycache__/mcp_launcher.cpython-39.pyc +0 -0
- package/templates/qoder/scripts/__pycache__/platform_doctor.cpython-39.pyc +0 -0
- package/templates/qoder/scripts/check_carriers.py +0 -238
- package/templates/qoder/scripts/check_mcp.py +0 -298
- package/templates/qoder/scripts/check_mcp_launch.py +0 -183
- package/templates/qoder/scripts/check_qoderwork_consistency.py +0 -166
- package/templates/qoder/scripts/collect_prds.py +0 -31
- package/templates/qoder/scripts/common/mentions.py +0 -134
- package/templates/qoder/scripts/common/utf8.py +0 -38
- package/templates/qoder/scripts/extract_api_params.py +0 -246
- package/templates/qoder/scripts/extract_routes.py +0 -54
- package/templates/qoder/scripts/extract_routes_tree.py +0 -78
- package/templates/qoder/scripts/handoff.py +0 -22
- package/templates/qoder/scripts/init_developer.py +0 -76
- package/templates/qoder/scripts/kg_mcp_server.py +0 -801
- package/templates/qoder/scripts/kg_semantic.py +0 -150
- package/templates/qoder/scripts/mcp_launcher.py +0 -414
- package/templates/qoder/scripts/mysql_mcp_server.py +0 -396
- package/templates/qoder/scripts/parse_prds.py +0 -33
- package/templates/qoder/scripts/role.py +0 -51
- package/templates/qoder/scripts/sync_carriers.py +0 -259
- package/templates/qoder/scripts/task.py +0 -1261
- package/templates/qoder/scripts/workspace_init.py +0 -102
- package/templates/qoder/scripts/zentao_mcp_server.py +0 -424
- package/templates/qoder/skills/prompt-enrich/SKILL.md +0 -90
- /package/templates/qoder/scripts/{common → deployment}/__init__.py +0 -0
- /package/templates/qoder/{skills/prototype-generator/SKILL.md.zcode-79180-2af4721f-f9a6-412c-88db-c0af680d211b.tmp → scripts/domain/__init__.py} +0 -0
- /package/templates/qoder/scripts/{common → domain/kg/graph}/graph_traverse.py +0 -0
- /package/templates/qoder/scripts/{common → domain/kg/server}/repowiki.py +0 -0
- /package/templates/qoder/scripts/{common → foundation/io}/atomicio.py +0 -0
- /package/templates/qoder/scripts/{secure-ls.js → validation/test/secure-ls.js} +0 -0
|
@@ -0,0 +1,571 @@
|
|
|
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
|
+
# 强制 stdout 无缓冲 — 被 timeout 杀掉/管道消费时也要看到输出 (实测踩坑)
|
|
14
|
+
try:
|
|
15
|
+
sys.stdout.reconfigure(line_buffering=True)
|
|
16
|
+
except Exception:
|
|
17
|
+
pass
|
|
18
|
+
|
|
19
|
+
"""autotest_webaccess.py — 真功能测试执行器 (web-access CDP 直连已登录 Chrome).
|
|
20
|
+
|
|
21
|
+
与 autotest_run.py 平级, 镜像同一个 task.json 契约, 引擎换成 web-access.
|
|
22
|
+
吃同样的 --task 文件, 吐同样结构的 result JSON.
|
|
23
|
+
|
|
24
|
+
为什么用 web-access 而非 opencli:
|
|
25
|
+
- opencli 走 chrome.debugger.attach (扩展代理), 在 Windows 后台 session 静默失败.
|
|
26
|
+
- web-access 走 CDP 端口直连 (chrome://inspect#remote-debugging), 不装扩展,
|
|
27
|
+
和 QoderWork chrome 连通器同源机制, 稳.
|
|
28
|
+
- 登录态天然复用 (连的是日常 Chrome, cookie/session 全在), 验证码零接触.
|
|
29
|
+
|
|
30
|
+
用法: python autotest_webaccess.py --task <任务文件> [--browser chrome]
|
|
31
|
+
任务文件 JSON: {
|
|
32
|
+
"page_path": "/veh/vehicle/vehAffair/insurance",
|
|
33
|
+
"page_name": "保险管理",
|
|
34
|
+
"cases": [ {case_id, title, steps:[{intent,desc,value,anchor,check,expect}], expected} ]
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
前置: 用户 Chrome 开 chrome://inspect/#remote-debugging 勾选允许调试.
|
|
38
|
+
没开 → 友好提示 + 自动回退 playwright (50 人零报错).
|
|
39
|
+
"""
|
|
40
|
+
import json, sys, time, os, re, argparse, subprocess
|
|
41
|
+
import urllib.request, urllib.error
|
|
42
|
+
|
|
43
|
+
# ===== 配置 (路径自检测, 与 autotest_run.py 一致) =====
|
|
44
|
+
SCRIPTS = os.path.dirname(os.path.abspath(__file__))
|
|
45
|
+
# test 目录在 .qoder/scripts/test, 回项目根要上 3 级. 向上找含 .qoder 的目录兜底.
|
|
46
|
+
_d = SCRIPTS
|
|
47
|
+
while _d != os.path.dirname(_d):
|
|
48
|
+
if os.path.isdir(os.path.join(_d, '.qoder')):
|
|
49
|
+
PROJECT = _d
|
|
50
|
+
break
|
|
51
|
+
_d = os.path.dirname(_d)
|
|
52
|
+
else:
|
|
53
|
+
PROJECT = os.path.normpath(os.path.join(SCRIPTS, '..', '..', '..'))
|
|
54
|
+
|
|
55
|
+
WEBACCESS_DIR = os.path.join(os.path.dirname(SCRIPTS), 'web-access') # .qoder/scripts/web-access (test 同级)
|
|
56
|
+
CDP_PROXY_PORT = 3456
|
|
57
|
+
PROXY_URL = 'http://localhost:%d' % CDP_PROXY_PORT
|
|
58
|
+
AUTOLEARN = os.path.join(SCRIPTS, 'autotest.py')
|
|
59
|
+
BASE = 'https://icsqas.inforecloud.com'
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
# ===== web-access HTTP API 封装 (零依赖, 用 urllib) =====
|
|
63
|
+
def _http(method, path, body=None, timeout=15):
|
|
64
|
+
"""调 cdp-proxy 的 HTTP API. 返回解析后的 JSON dict."""
|
|
65
|
+
url = PROXY_URL + path
|
|
66
|
+
data = body.encode('utf-8') if body is not None else None
|
|
67
|
+
req = urllib.request.Request(url, data=data, method=method)
|
|
68
|
+
if body is not None:
|
|
69
|
+
req.add_header('Content-Type', 'text/plain')
|
|
70
|
+
try:
|
|
71
|
+
with urllib.request.urlopen(req, timeout=timeout) as r:
|
|
72
|
+
return json.loads(r.read().decode('utf-8', 'ignore'))
|
|
73
|
+
except urllib.error.URLError as e:
|
|
74
|
+
return {'error': '连接失败: %s (proxy 没起? 跑 node web-access/scripts/check-deps.mjs --browser chrome)' % str(e)[:50]}
|
|
75
|
+
except Exception as e:
|
|
76
|
+
return {'error': str(e)[:60]}
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def proxy_ready():
|
|
80
|
+
"""检查 cdp-proxy 是否在跑 + 连着浏览器."""
|
|
81
|
+
h = _http('GET', '/health', timeout=5)
|
|
82
|
+
return h.get('connected') is True, h
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def start_proxy(browser='chrome'):
|
|
86
|
+
"""启动 cdp-proxy (自动发现 Chrome 调试端口). 返回 (ok, msg).
|
|
87
|
+
proxy 是长驻进程 — 已在跑就直接复用, 不重复 spawn (避免冲突/卡死)."""
|
|
88
|
+
# 快路径: proxy 已在跑且连着浏览器 → 直接用
|
|
89
|
+
ok, h = proxy_ready()
|
|
90
|
+
if ok:
|
|
91
|
+
return True, h.get('browser', {}).get('label', '')
|
|
92
|
+
check = os.path.join(WEBACCESS_DIR, 'scripts', 'check-deps.mjs')
|
|
93
|
+
if not os.path.isfile(check):
|
|
94
|
+
return False, 'web-access 引擎未安装 (%s 不存在)' % check
|
|
95
|
+
# check-deps 会自动起 proxy 并连浏览器. stdout 含 proxy: ready 即成功.
|
|
96
|
+
try:
|
|
97
|
+
r = subprocess.run(['node', check, '--browser', browser],
|
|
98
|
+
capture_output=True, timeout=40, cwd=WEBACCESS_DIR)
|
|
99
|
+
out = (r.stdout + r.stderr).decode('utf-8', 'ignore')
|
|
100
|
+
except subprocess.TimeoutExpired:
|
|
101
|
+
return False, 'check-deps 超时 (Chrome 调试开关没开?)'
|
|
102
|
+
except FileNotFoundError:
|
|
103
|
+
return False, 'node 未安装或不在 PATH (web-access 要 Node 22+)'
|
|
104
|
+
if 'proxy: ready' in out or 'proxy: connecting' in out:
|
|
105
|
+
time.sleep(2) # 等 proxy 完全就绪
|
|
106
|
+
ok, h = proxy_ready()
|
|
107
|
+
return ok, h.get('browser', {}).get('label', '') if ok else 'proxy 起了但没连上'
|
|
108
|
+
# 没成功 → 解析失败原因给用户
|
|
109
|
+
if 'needs decision' in out or '请询问用户' in out:
|
|
110
|
+
return False, '需指定浏览器: 加 --browser chrome'
|
|
111
|
+
if 'remote debugging' in out.lower() or '调试' in out:
|
|
112
|
+
return False, 'Chrome 远程调试未开启. 地址栏开 chrome://inspect/#remote-debugging 勾选 "Allow remote debugging"'
|
|
113
|
+
return False, out.strip().split('\n')[-1][:80]
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
# ===== tab 管理 =====
|
|
117
|
+
def new_tab(url, wait=4):
|
|
118
|
+
"""开新后台 tab, 返回 targetId 或 None."""
|
|
119
|
+
r = _http('POST', '/new', body=url, timeout=30)
|
|
120
|
+
tid = r.get('targetId')
|
|
121
|
+
if tid and wait:
|
|
122
|
+
time.sleep(wait)
|
|
123
|
+
return tid
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
def navigate(tid, url, wait=3):
|
|
127
|
+
"""在已有 tab 导航."""
|
|
128
|
+
r = _http('POST', '/navigate?target=%s' % tid, body=url, timeout=30)
|
|
129
|
+
if wait:
|
|
130
|
+
time.sleep(wait)
|
|
131
|
+
return 'error' not in r
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
def close_tab(tid):
|
|
135
|
+
"""关闭 tab."""
|
|
136
|
+
try:
|
|
137
|
+
_http('GET', '/close?target=%s' % tid, timeout=10)
|
|
138
|
+
except Exception:
|
|
139
|
+
pass
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
def page_info(tid):
|
|
143
|
+
"""读页面 title/url/readyState."""
|
|
144
|
+
return _http('GET', '/info?target=%s' % tid, timeout=10)
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
def eval_js(tid, code, timeout=15):
|
|
148
|
+
"""执行 JS, 返回 (value, error). value 是任意类型."""
|
|
149
|
+
r = _http('POST', '/eval?target=%s' % tid, body=code, timeout=timeout)
|
|
150
|
+
if 'error' in r:
|
|
151
|
+
return None, r['error']
|
|
152
|
+
return r.get('value'), None
|
|
153
|
+
|
|
154
|
+
|
|
155
|
+
def click_css(tid, selector):
|
|
156
|
+
"""JS 点击 (CSS 选择器). 返回 (ok, msg)."""
|
|
157
|
+
r = _http('POST', '/click?target=%s' % tid, body=selector, timeout=10)
|
|
158
|
+
return 'error' not in r, r.get('error', 'clicked')
|
|
159
|
+
|
|
160
|
+
|
|
161
|
+
# ===== 登录态: 无需伪造, CDP 连的是已登录 Chrome. 但需验证登录态有效 =====
|
|
162
|
+
def check_logged_in(tid):
|
|
163
|
+
"""检查当前页是否在登录页 (在登录页 = 未登录/失效)."""
|
|
164
|
+
info = page_info(tid)
|
|
165
|
+
url = info.get('url', '')
|
|
166
|
+
# SPA 登录页通常 url 含 /auth/login 或 title 是登录相关
|
|
167
|
+
return '/auth/login' not in url, url
|
|
168
|
+
|
|
169
|
+
|
|
170
|
+
# ===== anchor → CSS 选择器 (web-access click 用 CSS) =====
|
|
171
|
+
def anchor_to_css(anchor, desc=''):
|
|
172
|
+
"""把 anchor {role,name,placeholder,field,text} 转 CSS 选择器.
|
|
173
|
+
web-access 的 click 接受 CSS, 比 opencli 的语义 flag 弱一点, 但 eval 能兜底."""
|
|
174
|
+
if not anchor:
|
|
175
|
+
return ''
|
|
176
|
+
field = anchor.get('field', '')
|
|
177
|
+
if field:
|
|
178
|
+
return "input[name='%s']" % field
|
|
179
|
+
name = anchor.get('name', '')
|
|
180
|
+
ph = anchor.get('placeholder', '')
|
|
181
|
+
role = anchor.get('role', '')
|
|
182
|
+
text = anchor.get('text', '')
|
|
183
|
+
# 优先按 placeholder/name 文本匹配 input/button
|
|
184
|
+
if role == 'textbox':
|
|
185
|
+
if ph:
|
|
186
|
+
return "input[placeholder*='%s']" % ph
|
|
187
|
+
if name:
|
|
188
|
+
return "input[placeholder*='%s']" % name
|
|
189
|
+
if role == 'button':
|
|
190
|
+
# button 文本匹配用 eval 兜底更准, 这里先给个基础
|
|
191
|
+
if name:
|
|
192
|
+
return "button"
|
|
193
|
+
if text:
|
|
194
|
+
return "*"
|
|
195
|
+
return ''
|
|
196
|
+
|
|
197
|
+
|
|
198
|
+
def click_by_anchor(tid, anchor, desc=''):
|
|
199
|
+
"""按 anchor 点击: 先试 CSS, 失败用 eval 文本匹配兜底."""
|
|
200
|
+
css = anchor_to_css(anchor, desc)
|
|
201
|
+
name = anchor.get('name', '')
|
|
202
|
+
role = anchor.get('role', '')
|
|
203
|
+
text = anchor.get('text', '')
|
|
204
|
+
# button/link/tab 用 eval 按 innerText 精确匹配 (比 CSS :has-text 稳)
|
|
205
|
+
if role in ('button', 'link', 'tab') and (name or text):
|
|
206
|
+
kw = name or text
|
|
207
|
+
code = ("(function(){const els=[...document.querySelectorAll('%s')];"
|
|
208
|
+
"const el=els.find(e=>(e.innerText||'').trim()==='%s'||(e.innerText||'').includes('%s'));"
|
|
209
|
+
"if(el){el.scrollIntoView();el.click();return 'ok'}return 'notfound'})()" % (
|
|
210
|
+
'button,a,[role=tab],.ant-tabs-tab' if role == 'tab' else 'button,a,[role=button]',
|
|
211
|
+
kw, kw))
|
|
212
|
+
v, e = eval_js(tid, code)
|
|
213
|
+
return (v == 'ok'), ('clicked:%s' % kw if v == 'ok' else '未找到%s' % kw)
|
|
214
|
+
# textbox 用 CSS
|
|
215
|
+
if css:
|
|
216
|
+
ok, msg = click_css(tid, css)
|
|
217
|
+
if ok:
|
|
218
|
+
return True, 'clicked'
|
|
219
|
+
return False, '元素未找到(%s)' % desc
|
|
220
|
+
|
|
221
|
+
|
|
222
|
+
def fill_by_anchor(tid, anchor, val, desc=''):
|
|
223
|
+
"""填值: 用 eval 直接设 value + dispatch input event (兼容 Vue)."""
|
|
224
|
+
css = anchor_to_css(anchor, desc)
|
|
225
|
+
if not css:
|
|
226
|
+
return False, '元素未找到(anchor空:%s)' % desc
|
|
227
|
+
# Vue 需要触发 input 事件框架才认. 用原生 setter + dispatchEvent.
|
|
228
|
+
safe_val = str(val).replace("\\", "\\\\").replace("'", "\\'")
|
|
229
|
+
code = ("(function(){const el=document.querySelector(\"%s\");"
|
|
230
|
+
"if(!el)return 'notfound';"
|
|
231
|
+
"const setter=Object.getOwnPropertyDescriptor(window.HTMLInputElement.prototype,'value').set;"
|
|
232
|
+
"setter.call(el,'%s');"
|
|
233
|
+
"el.dispatchEvent(new Event('input',{bubbles:true}));"
|
|
234
|
+
"el.dispatchEvent(new Event('change',{bubbles:true}));"
|
|
235
|
+
"return el.value})()" % (css.replace('"', '\\"'), safe_val))
|
|
236
|
+
v, e = eval_js(tid, code)
|
|
237
|
+
if v == 'notfound':
|
|
238
|
+
return False, '元素未找到(%s)' % desc
|
|
239
|
+
return (v == str(val)), 'filled(actual=%s)' % _short(v)
|
|
240
|
+
|
|
241
|
+
|
|
242
|
+
# ===== 表格等待 (与 autotest_run.py 等价语义) =====
|
|
243
|
+
def wait_table_stable(tid, max_wait=8):
|
|
244
|
+
"""轮询 .vxe-body--row 行数, 等表格加载稳定."""
|
|
245
|
+
deadline = time.time() + max_wait
|
|
246
|
+
zero_streak = 0
|
|
247
|
+
last = -1
|
|
248
|
+
while time.time() < deadline:
|
|
249
|
+
v, _ = eval_js(tid, "document.querySelectorAll('.vxe-body--row').length", timeout=5)
|
|
250
|
+
try:
|
|
251
|
+
n = int(v)
|
|
252
|
+
except (TypeError, ValueError):
|
|
253
|
+
n = 0
|
|
254
|
+
if n > 0:
|
|
255
|
+
time.sleep(0.6)
|
|
256
|
+
v2, _ = eval_js(tid, "document.querySelectorAll('.vxe-body--row').length", timeout=5)
|
|
257
|
+
try:
|
|
258
|
+
n2 = int(v2)
|
|
259
|
+
except (TypeError, ValueError):
|
|
260
|
+
n2 = 0
|
|
261
|
+
if n2 >= n:
|
|
262
|
+
return n2
|
|
263
|
+
else:
|
|
264
|
+
zero_streak += 1
|
|
265
|
+
if zero_streak >= 3:
|
|
266
|
+
return 0
|
|
267
|
+
time.sleep(0.5)
|
|
268
|
+
return last
|
|
269
|
+
|
|
270
|
+
|
|
271
|
+
# ===== 单步执行 (镜像 autotest_run.py exec_step 结构) =====
|
|
272
|
+
def exec_step(tid, step, timeout=8):
|
|
273
|
+
"""执行一步, 返回 (ok, detail)."""
|
|
274
|
+
intent = step.get('intent', '')
|
|
275
|
+
desc = step.get('desc', '')
|
|
276
|
+
anchor = step.get('anchor', {}) or {}
|
|
277
|
+
val = step.get('value', '')
|
|
278
|
+
|
|
279
|
+
try:
|
|
280
|
+
if intent == 'goto':
|
|
281
|
+
url = BASE + val if val.startswith('/') else val
|
|
282
|
+
navigate(tid, url)
|
|
283
|
+
wait_table_stable(tid, max_wait=8)
|
|
284
|
+
return True, 'navigated'
|
|
285
|
+
|
|
286
|
+
if intent == 'fill':
|
|
287
|
+
return fill_by_anchor(tid, anchor, val, desc)
|
|
288
|
+
|
|
289
|
+
if intent == 'click':
|
|
290
|
+
ok, msg = click_by_anchor(tid, anchor, desc)
|
|
291
|
+
time.sleep(1.5)
|
|
292
|
+
return ok, msg
|
|
293
|
+
|
|
294
|
+
if intent == 'observe':
|
|
295
|
+
return True, 'observed'
|
|
296
|
+
|
|
297
|
+
if intent == 'extract':
|
|
298
|
+
# 优先 expect (数据断言, 可能是 DOM 值 或 fetch API)
|
|
299
|
+
if step.get('expect'):
|
|
300
|
+
return _check_expect(tid, step['expect'])
|
|
301
|
+
# 否则走旧语义: body 文本含 check 关键词
|
|
302
|
+
check = step.get('check', '')
|
|
303
|
+
v, _ = eval_js(tid, "document.body.innerText", timeout=10)
|
|
304
|
+
body = str(v) if v else ''
|
|
305
|
+
if check:
|
|
306
|
+
hit = check in body
|
|
307
|
+
return (hit, '命中:%s' % check if hit else '未命中:%s' % check)
|
|
308
|
+
return True, 'extracted(no check)'
|
|
309
|
+
|
|
310
|
+
if intent == 'assert':
|
|
311
|
+
return _check_assert(tid, step.get('check', ''))
|
|
312
|
+
|
|
313
|
+
except Exception as e:
|
|
314
|
+
return False, str(e)[:60]
|
|
315
|
+
return False, '未知intent:%s' % intent
|
|
316
|
+
|
|
317
|
+
|
|
318
|
+
# ===== 数据断言 (web-access 核心增量: eval DOM 值 或 eval fetch API) =====
|
|
319
|
+
def _check_expect(tid, expect):
|
|
320
|
+
"""数据断言. expect 支持两种模式:
|
|
321
|
+
1. {dom: "css", jsonpath: "text", equals: "湖南石门"} — 读 DOM 元素值
|
|
322
|
+
2. {fetch: "/api/...", jsonpath: "code", equals: 200} — 调后端接口验字段
|
|
323
|
+
jsonpath 用点路径 (data.total / rows[0].name), 轻量实现不引依赖.
|
|
324
|
+
"""
|
|
325
|
+
# 模式 1: DOM 值断言
|
|
326
|
+
if 'dom' in expect:
|
|
327
|
+
css = expect['dom']
|
|
328
|
+
jp = expect.get('jsonpath', 'text').lstrip('$.').lstrip('.')
|
|
329
|
+
want = expect.get('equals')
|
|
330
|
+
code = "(function(){const el=document.querySelector(\"%s\");if(!el)return null;" \
|
|
331
|
+
"const v=el.innerText||el.value||el.textContent;return v.trim()})()" % css.replace('"', '\\"')
|
|
332
|
+
v, e = eval_js(tid, code)
|
|
333
|
+
actual = str(v).strip() if v else ''
|
|
334
|
+
ok = (actual == str(want)) if want is not None else bool(actual)
|
|
335
|
+
return ok, '%s=%s(期望%s)' % (jp, _short(actual), _short(want))
|
|
336
|
+
|
|
337
|
+
# 模式 2: API 断言 (从页面上下文 fetch, 带登录态)
|
|
338
|
+
if 'fetch' in expect:
|
|
339
|
+
endpoint = expect['fetch']
|
|
340
|
+
jp = expect.get('jsonpath', '').lstrip('$.').lstrip('.')
|
|
341
|
+
want = expect.get('equals')
|
|
342
|
+
method = expect.get('method', 'POST')
|
|
343
|
+
body = expect.get('body', {})
|
|
344
|
+
body_js = json.dumps(body, ensure_ascii=False)
|
|
345
|
+
# 注意: 裸 fetch 可能 401 (没带 token). 优先建议用 dom 断言.
|
|
346
|
+
code = ("(async()=>{try{const r=await fetch('%s',{method:'%s',headers:{'Content-Type':'application/json'}," \
|
|
347
|
+
"body:%s});const d=await r.json();return JSON.stringify(d)}catch(e){return 'ERR:'+e.message}})()" % (
|
|
348
|
+
endpoint, method, body_js))
|
|
349
|
+
v, e = eval_js(tid, code, timeout=20)
|
|
350
|
+
if e:
|
|
351
|
+
return False, 'fetch失败:%s' % e
|
|
352
|
+
try:
|
|
353
|
+
data = json.loads(v) if isinstance(v, str) else v
|
|
354
|
+
except Exception:
|
|
355
|
+
return False, '响应非JSON:%s' % _short(v)
|
|
356
|
+
actual = _dig(data, jp)
|
|
357
|
+
ok = (actual == want)
|
|
358
|
+
return ok, '%s=%s(期望%s)' % (jp, _short(actual), _short(want))
|
|
359
|
+
|
|
360
|
+
return True, 'expect(无dom/fetch字段)'
|
|
361
|
+
|
|
362
|
+
|
|
363
|
+
def _check_assert(tid, check):
|
|
364
|
+
"""镜像 autotest_run.py 的 assert: rows>/rows==/url_contains/input_empty."""
|
|
365
|
+
if not check:
|
|
366
|
+
return True, 'assert(no check)'
|
|
367
|
+
if check.startswith('rows>'):
|
|
368
|
+
n = int(check[5:])
|
|
369
|
+
wait_table_stable(tid, max_wait=6)
|
|
370
|
+
v, _ = eval_js(tid, "document.querySelectorAll('.vxe-body--row').length", timeout=5)
|
|
371
|
+
try:
|
|
372
|
+
rows = int(v)
|
|
373
|
+
except (TypeError, ValueError):
|
|
374
|
+
rows = 0
|
|
375
|
+
return (rows > n, '行数%d>%d' % (rows, n) if rows > n else '行数%d<=%d' % (rows, n))
|
|
376
|
+
if check.startswith('rows=='):
|
|
377
|
+
n = int(check[6:])
|
|
378
|
+
wait_table_stable(tid, max_wait=6)
|
|
379
|
+
v, _ = eval_js(tid, "document.querySelectorAll('.vxe-body--row').length", timeout=5)
|
|
380
|
+
try:
|
|
381
|
+
rows = int(v)
|
|
382
|
+
except (TypeError, ValueError):
|
|
383
|
+
rows = 0
|
|
384
|
+
return (rows == n, '行数=%d' % rows)
|
|
385
|
+
if check.startswith('url_contains'):
|
|
386
|
+
kw = check.split(':', 1)[1] if ':' in check else ''
|
|
387
|
+
info = page_info(tid)
|
|
388
|
+
cur = info.get('url', '')
|
|
389
|
+
return (kw in cur, 'url含%s' % kw if kw in cur else 'url不含%s' % kw)
|
|
390
|
+
if check.startswith('input_empty:'):
|
|
391
|
+
field = check.split(':', 1)[1]
|
|
392
|
+
v, _ = eval_js(tid, "(document.querySelector(\"input[name='%s']\")||{}).value||''" % field, timeout=5)
|
|
393
|
+
val = str(v) if v else ''
|
|
394
|
+
return (val == '', '框值=%r' % val)
|
|
395
|
+
return True, 'assert(未知check)'
|
|
396
|
+
|
|
397
|
+
|
|
398
|
+
def _dig(obj, path):
|
|
399
|
+
"""简单点路径取值: data.total / rows[0].name."""
|
|
400
|
+
if not path:
|
|
401
|
+
return obj
|
|
402
|
+
cur = obj
|
|
403
|
+
for part in re.split(r'\.', path):
|
|
404
|
+
m = re.match(r'([^\[]+)(\[(\d+)\])?', part)
|
|
405
|
+
if not m:
|
|
406
|
+
return None
|
|
407
|
+
key = m.group(1)
|
|
408
|
+
idx = m.group(3)
|
|
409
|
+
if isinstance(cur, dict) and key in cur:
|
|
410
|
+
cur = cur[key]
|
|
411
|
+
elif isinstance(cur, list) and key.isdigit() and int(key) < len(cur):
|
|
412
|
+
cur = cur[int(key)]
|
|
413
|
+
else:
|
|
414
|
+
return None
|
|
415
|
+
if idx is not None and isinstance(cur, list) and int(idx) < len(cur):
|
|
416
|
+
cur = cur[int(idx)]
|
|
417
|
+
return cur
|
|
418
|
+
|
|
419
|
+
|
|
420
|
+
def _short(v, n=30):
|
|
421
|
+
s = str(v)
|
|
422
|
+
return s if len(s) <= n else s[:n] + '...'
|
|
423
|
+
|
|
424
|
+
|
|
425
|
+
# ===== 跑一个用例 (镜像 autotest_run.py run_case) =====
|
|
426
|
+
def run_case(tid, case):
|
|
427
|
+
cid = case.get('case_id', '?')
|
|
428
|
+
steps = case.get('steps', [])
|
|
429
|
+
details = []
|
|
430
|
+
all_ok = True
|
|
431
|
+
for step in steps:
|
|
432
|
+
ok, detail = exec_step(tid, step)
|
|
433
|
+
details.append('%s:%s(%s)' % (step.get('intent', '?'), '✓' if ok else '✗', detail))
|
|
434
|
+
if not ok:
|
|
435
|
+
all_ok = False
|
|
436
|
+
break
|
|
437
|
+
return {'case_id': cid, 'title': case.get('title', ''), 'pass': all_ok,
|
|
438
|
+
'expected': case.get('expected', ''), 'details': details}
|
|
439
|
+
|
|
440
|
+
|
|
441
|
+
# ===== 沉淀锚点 (与 autotest_run.py 一致) =====
|
|
442
|
+
def collect_anchors(tid):
|
|
443
|
+
js = '''()=>{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);}'''
|
|
444
|
+
v, _ = eval_js(tid, js, timeout=10)
|
|
445
|
+
els = v if isinstance(v, list) else []
|
|
446
|
+
seen = set(); uniq = []
|
|
447
|
+
for el in els:
|
|
448
|
+
k = (el.get('role'), el.get('name'))
|
|
449
|
+
if k not in seen:
|
|
450
|
+
seen.add(k); uniq.append(el)
|
|
451
|
+
return uniq
|
|
452
|
+
|
|
453
|
+
|
|
454
|
+
def sediment(page_path, anchors):
|
|
455
|
+
if not anchors:
|
|
456
|
+
return 0
|
|
457
|
+
try:
|
|
458
|
+
pj = json.dumps({'url': page_path, 'elements': [
|
|
459
|
+
{'role': a.get('role'), 'name': a.get('name'), 'desc': a.get('name')} for a in anchors
|
|
460
|
+
]}, ensure_ascii=False)
|
|
461
|
+
subprocess.run([sys.executable, AUTOLEARN, 'learn', '--url', page_path, '--page-json', pj],
|
|
462
|
+
capture_output=True, timeout=20)
|
|
463
|
+
return len(anchors)
|
|
464
|
+
except Exception:
|
|
465
|
+
return 0
|
|
466
|
+
|
|
467
|
+
|
|
468
|
+
# ===== 友好提示 + 自动回退 =====
|
|
469
|
+
def _print_not_ready(reason, explicit=False):
|
|
470
|
+
print('')
|
|
471
|
+
print('╔══ web-access 引擎未就绪 ═════════════════════════════════════')
|
|
472
|
+
print('║ 原因: %s' % str(reason)[:56])
|
|
473
|
+
print('║ ────────────────────────────────────────────────────────────')
|
|
474
|
+
print('║ 怎么修:')
|
|
475
|
+
print('║ 1. 打开 Chrome, 地址栏输入 chrome://inspect/#remote-debugging')
|
|
476
|
+
print('║ 2. 勾选 "Allow remote debugging for this browser instance"')
|
|
477
|
+
print('║ 3. 确保 Chrome 开着 (不用切前台, 后台即可)')
|
|
478
|
+
if not explicit:
|
|
479
|
+
print('║')
|
|
480
|
+
print('║ ✅ 已自动回退: 改用 playwright 引擎 (autotest_run.py)')
|
|
481
|
+
print('║ playwright 开箱即用, 不需要开调试开关.')
|
|
482
|
+
print('║ web-access 是可选增强 —— 复用已登录 Chrome, 验证码零接触.')
|
|
483
|
+
print('╚══════════════════════════════════════════════════════════════')
|
|
484
|
+
print('')
|
|
485
|
+
|
|
486
|
+
|
|
487
|
+
# ===== 主 =====
|
|
488
|
+
def main():
|
|
489
|
+
ap = argparse.ArgumentParser()
|
|
490
|
+
ap.add_argument('--task', required=True, help='任务文件 JSON')
|
|
491
|
+
ap.add_argument('--browser', default='chrome', help='浏览器 (chrome/edge, 默认 chrome)')
|
|
492
|
+
ap.add_argument('--url', default='', help='临时覆盖 base url')
|
|
493
|
+
ap.add_argument('--force', action='store_true', help='强制 web-access, 没装不回退')
|
|
494
|
+
args = ap.parse_args()
|
|
495
|
+
|
|
496
|
+
global BASE
|
|
497
|
+
if args.url:
|
|
498
|
+
BASE = args.url.rstrip('/')
|
|
499
|
+
|
|
500
|
+
task = json.load(open(args.task, encoding='utf-8'))
|
|
501
|
+
page_path = task['page_path']
|
|
502
|
+
cases = task.get('cases', [])
|
|
503
|
+
|
|
504
|
+
# 1. 启动 proxy + 连浏览器 (没开调试开关 → 友好提示 + 回退)
|
|
505
|
+
ok, msg = start_proxy(args.browser)
|
|
506
|
+
if not ok:
|
|
507
|
+
_print_not_ready(msg, explicit=args.force)
|
|
508
|
+
if args.force:
|
|
509
|
+
r = {'engine': 'web-access', 'error': 'WEBAccess_NOT_READY', 'case_results': [], 'page': page_path}
|
|
510
|
+
print(json.dumps(r, ensure_ascii=False)); sys.exit(1)
|
|
511
|
+
print('>>> 自动转用 playwright 引擎 ...')
|
|
512
|
+
runner = os.path.join(SCRIPTS, 'autotest_run.py')
|
|
513
|
+
rc = subprocess.run([sys.executable, runner, '--task', args.task], cwd=PROJECT).returncode
|
|
514
|
+
sys.exit(rc)
|
|
515
|
+
|
|
516
|
+
result = {'page': page_path, 'page_name': task.get('page_name', ''),
|
|
517
|
+
'engine': 'web-access', 'case_results': [], 'anchors_count': 0, 'error': '', 'video': ''}
|
|
518
|
+
t0 = time.time()
|
|
519
|
+
tid = None
|
|
520
|
+
try:
|
|
521
|
+
# 2. 开目标页 (新 tab, 复用已登录 Chrome 的登录态)
|
|
522
|
+
tid = new_tab(BASE + page_path, wait=4)
|
|
523
|
+
if not tid:
|
|
524
|
+
result['error'] = 'OPEN_FAIL'; print(json.dumps(result, ensure_ascii=False)); sys.exit(1)
|
|
525
|
+
|
|
526
|
+
logged_in, cur_url = check_logged_in(tid)
|
|
527
|
+
if not logged_in:
|
|
528
|
+
result['error'] = 'NOT_LOGGED_IN: 请先在 Chrome 手动登录 %s' % BASE
|
|
529
|
+
print(json.dumps(result, ensure_ascii=False)); sys.exit(1)
|
|
530
|
+
print(' 已连接: %s' % cur_url)
|
|
531
|
+
|
|
532
|
+
# 3. 跑每个用例
|
|
533
|
+
# 第一个用例已在目标页 (new_tab 已导航), 无需重导; 后续用例才重导保证干净状态.
|
|
534
|
+
# wait_table_stable 首次等久点 (表格可能没加载), 后续缩到 4s (页面已热).
|
|
535
|
+
first = True
|
|
536
|
+
for case in cases:
|
|
537
|
+
if not first:
|
|
538
|
+
navigate(tid, BASE + page_path, wait=2)
|
|
539
|
+
first = False
|
|
540
|
+
wait_table_stable(tid, max_wait=8 if result['case_results'] == [] else 4)
|
|
541
|
+
cr = run_case(tid, case)
|
|
542
|
+
result['case_results'].append(cr)
|
|
543
|
+
tag = '✓' if cr['pass'] else '✗'
|
|
544
|
+
print(' %s %s %s' % (tag, cr['case_id'], cr['title'][:30]))
|
|
545
|
+
for d in cr['details']:
|
|
546
|
+
print(' %s' % d)
|
|
547
|
+
|
|
548
|
+
# 4. 沉淀
|
|
549
|
+
anc = collect_anchors(tid)
|
|
550
|
+
result['anchors_count'] = len(anc)
|
|
551
|
+
sediment(page_path, anc)
|
|
552
|
+
|
|
553
|
+
except Exception as e:
|
|
554
|
+
result['error'] = str(e)[:100]
|
|
555
|
+
finally:
|
|
556
|
+
if tid:
|
|
557
|
+
close_tab(tid)
|
|
558
|
+
|
|
559
|
+
result['duration'] = round(time.time() - t0, 1)
|
|
560
|
+
passed = sum(1 for c in result['case_results'] if c['pass'])
|
|
561
|
+
total = len(result['case_results'])
|
|
562
|
+
result['summary'] = '%d/%d 通过' % (passed, total)
|
|
563
|
+
print()
|
|
564
|
+
print('汇总: %s 锚点%d 耗时%ss %s' % (result['summary'], result['anchors_count'], result['duration'], result['error']))
|
|
565
|
+
out = args.task.replace('.json', '_webaccess.json')
|
|
566
|
+
json.dump(result, open(out, 'w', encoding='utf-8'), ensure_ascii=False, indent=2)
|
|
567
|
+
print('结果: %s' % out)
|
|
568
|
+
|
|
569
|
+
|
|
570
|
+
if __name__ == '__main__':
|
|
571
|
+
main()
|
|
@@ -1,5 +1,17 @@
|
|
|
1
1
|
#!/usr/bin/env python3
|
|
2
2
|
# -*- coding: utf-8 -*-
|
|
3
|
+
# v3.0 路径自举: 引导到 common/bootstrap, 统一 sys.path 逻辑
|
|
4
|
+
import os as _o, sys as _s
|
|
5
|
+
_f = _o.path.abspath(__file__)
|
|
6
|
+
for _ in range(10):
|
|
7
|
+
_f = _o.path.dirname(_f)
|
|
8
|
+
_cp = _o.path.join(_f, 'foundation')
|
|
9
|
+
if _o.path.isfile(_o.path.join(_cp, 'bootstrap.py')):
|
|
10
|
+
break
|
|
11
|
+
if _cp not in _s.path: _s.path.insert(0, _cp)
|
|
12
|
+
from bootstrap import setup; setup()
|
|
13
|
+
from foundation.core.paths import get_repo_root
|
|
14
|
+
|
|
3
15
|
"""
|
|
4
16
|
benchmark.py - QODER 流水线性能基准 (性能优化 A7)
|
|
5
17
|
|
|
@@ -21,7 +33,7 @@ Usage:
|
|
|
21
33
|
"""
|
|
22
34
|
|
|
23
35
|
import argparse
|
|
24
|
-
from
|
|
36
|
+
from foundation.core.paths import BUILD_CACHE_DIR
|
|
25
37
|
import json
|
|
26
38
|
import os
|
|
27
39
|
import sys
|
|
@@ -36,7 +48,7 @@ except (AttributeError, TypeError, OSError):
|
|
|
36
48
|
THIS_DIR = os.path.dirname(os.path.abspath(__file__)) # .qoder/scripts
|
|
37
49
|
sys.path.insert(0, THIS_DIR)
|
|
38
50
|
# BASE = repo root = scripts 的上两级 (.qoder/scripts -> .qoder -> repo)
|
|
39
|
-
BASE =
|
|
51
|
+
BASE = get_repo_root()
|
|
40
52
|
|
|
41
53
|
INDEX_DIR = os.path.join(BASE, 'data', 'index')
|
|
42
54
|
|
|
@@ -65,7 +77,7 @@ def bench_search():
|
|
|
65
77
|
results['keyword_count'] = len(ki)
|
|
66
78
|
|
|
67
79
|
# 2. 全扫描 (旧逻辑)
|
|
68
|
-
from search_index import _match_keyword
|
|
80
|
+
from domain.kg.search.search_index import _match_keyword
|
|
69
81
|
queries = ['insurance', 'attendance', 'salary', 'vehicle', 'user']
|
|
70
82
|
def full_scan():
|
|
71
83
|
for q in queries:
|
|
@@ -75,7 +87,7 @@ def bench_search():
|
|
|
75
87
|
results['full_scan_5q_ms'] = measure(full_scan, repeat=5)[0] / 5
|
|
76
88
|
|
|
77
89
|
# 3. 倒排索引构建 (首次)
|
|
78
|
-
from
|
|
90
|
+
from search_engine import get_inverted_index, search_keywords_fast, build_inverted_index
|
|
79
91
|
# 清缓存测构建
|
|
80
92
|
cache_path = os.path.join(INDEX_DIR, '.inverted-cache.json')
|
|
81
93
|
if os.path.exists(cache_path):
|
|
@@ -103,7 +115,7 @@ def bench_search():
|
|
|
103
115
|
|
|
104
116
|
def bench_cnmap():
|
|
105
117
|
"""CN_MAP 查询基准。"""
|
|
106
|
-
from
|
|
118
|
+
from foundation.integrations.terms import CN_MAP, BUSINESS_PATH_MAP, get_cn_map_with_auto
|
|
107
119
|
results = {}
|
|
108
120
|
results['cn_map_static_count'] = len(CN_MAP)
|
|
109
121
|
results['business_path_map_count'] = len(BUSINESS_PATH_MAP)
|