@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,617 @@
|
|
|
1
|
+
"""
|
|
2
|
+
qwork_harness.py — QoderWork 命令自动化测试框架 (P2)
|
|
3
|
+
|
|
4
|
+
走完整个流程: 注入命令 → 轮询 transcript → 遇到 AskUserQuestion 自动代答
|
|
5
|
+
→ 等 turn.finished → 调 transcript_timing 出耗时报告
|
|
6
|
+
|
|
7
|
+
设计要点:
|
|
8
|
+
- 真相源是 transcript jsonl (实时追加), 不靠猜 DOM
|
|
9
|
+
- AskUserQuestion 的 question+options 全在 tool.requested.data.args 里
|
|
10
|
+
- 代答策略: 关键词匹配 option.label/description, 选最"全"的那个
|
|
11
|
+
(如 "整体/context/全貌/平台/默认" 等), 也可 --auto-answer=1 指定
|
|
12
|
+
- 代答回传: 优先点击渲染的选项 DOM; 兜底注入 label 文本 + Enter
|
|
13
|
+
|
|
14
|
+
用法:
|
|
15
|
+
python qwork_harness.py "/wl-search 考勤"
|
|
16
|
+
python qwork_harness.py "/wl-search 考勤" --auto-answer=0 # 选第1个
|
|
17
|
+
python qwork_harness.py "/wl-search 考勤" --no-answer # 只跑不等答
|
|
18
|
+
python qwork_harness.py "/wl-prd-full 用户管理" --auto-answer=1
|
|
19
|
+
|
|
20
|
+
依赖: websocket-client (pip install websocket-client)
|
|
21
|
+
"""
|
|
22
|
+
import json
|
|
23
|
+
import os
|
|
24
|
+
import re
|
|
25
|
+
import sys
|
|
26
|
+
import time
|
|
27
|
+
import glob
|
|
28
|
+
import urllib.request
|
|
29
|
+
|
|
30
|
+
try:
|
|
31
|
+
import websocket
|
|
32
|
+
except ImportError:
|
|
33
|
+
print("[FATAL] 缺 websocket-client, 请: pip install websocket-client")
|
|
34
|
+
sys.exit(2)
|
|
35
|
+
|
|
36
|
+
# 复用 P1 计时引擎
|
|
37
|
+
_HERE = os.path.dirname(os.path.abspath(__file__))
|
|
38
|
+
sys.path.insert(0, _HERE)
|
|
39
|
+
import transcript_timing as tt # noqa: E402
|
|
40
|
+
|
|
41
|
+
# ── CDP 连接 ────────────────────────────────────────────────────────
|
|
42
|
+
CDP_PORT = 6529
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def _get_qwork_ws(port=CDP_PORT):
|
|
46
|
+
d = json.loads(urllib.request.urlopen(f"http://127.0.0.1:{port}/json", timeout=3).read())
|
|
47
|
+
for t in d:
|
|
48
|
+
if t.get("type") == "page" and "QoderWork" in t.get("title", ""):
|
|
49
|
+
return t["webSocketDebuggerUrl"]
|
|
50
|
+
raise RuntimeError("没找到 QoderWork 页面 (CDP 6529). QoderWork 开着吗?")
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
class CDP:
|
|
54
|
+
def __init__(self, ws_url):
|
|
55
|
+
self.ws = websocket.create_connection(ws_url, timeout=15)
|
|
56
|
+
self.mid = 0
|
|
57
|
+
|
|
58
|
+
def call(self, method, params=None, timeout=15):
|
|
59
|
+
self.mid += 1
|
|
60
|
+
self.ws.send(json.dumps({"id": self.mid, "method": method, "params": params or {}}))
|
|
61
|
+
end = time.time() + timeout
|
|
62
|
+
while time.time() < end:
|
|
63
|
+
r = json.loads(self.ws.recv())
|
|
64
|
+
if r.get("id") == self.mid:
|
|
65
|
+
return r
|
|
66
|
+
return {}
|
|
67
|
+
|
|
68
|
+
def eval(self, expr, timeout=15):
|
|
69
|
+
r = self.call("Runtime.evaluate", {"expression": expr, "returnByValue": True}, timeout)
|
|
70
|
+
body = r.get("result", {})
|
|
71
|
+
if "exceptionDetails" in body:
|
|
72
|
+
return {"ERR": str(body["exceptionDetails"])[:200]}
|
|
73
|
+
res = body.get("result", {})
|
|
74
|
+
return res.get("value", res)
|
|
75
|
+
|
|
76
|
+
def close(self):
|
|
77
|
+
try:
|
|
78
|
+
self.ws.close()
|
|
79
|
+
except Exception:
|
|
80
|
+
pass
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
# ── 输入框操作 ──────────────────────────────────────────────────────
|
|
84
|
+
def focus_input(cdp):
|
|
85
|
+
return cdp.eval(r"""(() => {
|
|
86
|
+
const e = document.querySelector('[contenteditable="true"]');
|
|
87
|
+
if(!e) return 'NO_INPUT';
|
|
88
|
+
e.focus();
|
|
89
|
+
const sel=window.getSelection(); const range=document.createRange();
|
|
90
|
+
range.selectNodeContents(e); range.collapse(false);
|
|
91
|
+
sel.removeAllRanges(); sel.addRange(range);
|
|
92
|
+
return 'ok';
|
|
93
|
+
})()""")
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
def inject_text(cdp, text):
|
|
97
|
+
"""CDP Input.insertText — 模拟真实键盘输入, 框架必识别。"""
|
|
98
|
+
cdp.call("Input.insertText", {"text": text}, timeout=8)
|
|
99
|
+
time.sleep(0.5)
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
def submit_enter(cdp):
|
|
103
|
+
"""CDP 派发 Enter 键提交。"""
|
|
104
|
+
for et in ("rawKeyDown", "keyDown", "keyUp"):
|
|
105
|
+
cdp.call("Input.dispatchKeyEvent", {
|
|
106
|
+
"type": et, "key": "Enter", "code": "Enter", "windowsVirtualKeyCode": 13,
|
|
107
|
+
}, timeout=5)
|
|
108
|
+
time.sleep(0.05)
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
def get_input_text(cdp):
|
|
112
|
+
return cdp.eval(r"""document.querySelector('[contenteditable="true"]')?.textContent || ''""")
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
# ── transcript 实时读取 ─────────────────────────────────────────────
|
|
116
|
+
def _find_session_jsonl():
|
|
117
|
+
"""找当前活跃 session 最新的 jsonl (取 mtime 最新且较大的)。"""
|
|
118
|
+
base = os.path.expanduser(r"~/.qoderwork/logs/sessions")
|
|
119
|
+
if not os.path.isdir(base):
|
|
120
|
+
return None
|
|
121
|
+
allf = []
|
|
122
|
+
for p in glob.glob(os.path.join(base, "**", "*.jsonl"), recursive=True):
|
|
123
|
+
try:
|
|
124
|
+
allf.append((os.path.getmtime(p), os.path.getsize(p), p))
|
|
125
|
+
except OSError:
|
|
126
|
+
continue
|
|
127
|
+
if not allf:
|
|
128
|
+
return None
|
|
129
|
+
allf.sort(reverse=True)
|
|
130
|
+
# 取最新且 >5KB 的(活跃会话)
|
|
131
|
+
for mt, sz, p in allf:
|
|
132
|
+
if sz > 5000:
|
|
133
|
+
return p
|
|
134
|
+
return allf[0][2]
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
# ── 系统/后台 turn 识别 ─────────────────────────────────────────────
|
|
138
|
+
# QoderWork 每条用户命令跑完后, 会自动追加一条 [SYSTEM: automated background
|
|
139
|
+
# review] 后台审核 turn。它的 prompt 文本含这些标记。harness 必须跳过它们,
|
|
140
|
+
# 否则 baseline+1 计数会被后台 turn 干扰, 提前误判"命令完成"。
|
|
141
|
+
_SYSTEM_MARKERS = (
|
|
142
|
+
"automated background review",
|
|
143
|
+
"must be fulfilled using",
|
|
144
|
+
"[system:",
|
|
145
|
+
"this request must be fulfilled",
|
|
146
|
+
)
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
def _is_system_prompt(text):
|
|
150
|
+
"""判断 prompt 文本是否为系统/后台任务(非用户命令)。"""
|
|
151
|
+
if not text:
|
|
152
|
+
return False
|
|
153
|
+
low = text.lower()
|
|
154
|
+
return any(m in low for m in _SYSTEM_MARKERS)
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
def _ui_is_busy(cdp):
|
|
158
|
+
"""检测 QoderWork 是否正忙(有任务在跑或有队列)。
|
|
159
|
+
双重判定: ① UI 状态文字 ② transcript 的 turn 配对(started>finished)。
|
|
160
|
+
返回 (busy:bool, detail:str)。
|
|
161
|
+
"""
|
|
162
|
+
# ① UI 状态文字
|
|
163
|
+
res = cdp.eval(r"""(() => {
|
|
164
|
+
const txts = [...document.querySelectorAll('div,span')]
|
|
165
|
+
.filter(e => e.offsetParent !== null && e.children.length === 0)
|
|
166
|
+
.map(e => (e.textContent || '').trim())
|
|
167
|
+
.filter(t => t && t.length < 30);
|
|
168
|
+
const joined = txts.join('|');
|
|
169
|
+
const busy = /处理中|执行中|running/i.test(joined);
|
|
170
|
+
const queue = (txts.find(t => /\d+\s*个队列任务/.test(t))) || '';
|
|
171
|
+
return {busy, queue};
|
|
172
|
+
})()""")
|
|
173
|
+
ui_busy = False
|
|
174
|
+
detail = ""
|
|
175
|
+
if isinstance(res, dict):
|
|
176
|
+
ui_busy = bool(res.get("busy") or res.get("queue"))
|
|
177
|
+
detail = res.get("queue", "") or ("处理中" if res.get("busy") else "")
|
|
178
|
+
# ② transcript turn 配对(更可靠的兜底)
|
|
179
|
+
jsonl = _find_session_jsonl()
|
|
180
|
+
if jsonl:
|
|
181
|
+
ev = read_events(jsonl)
|
|
182
|
+
ns = sum(1 for e in ev if e.get("type") == "turn.started")
|
|
183
|
+
nf = sum(1 for e in ev if e.get("type") == "turn.finished")
|
|
184
|
+
if ns > nf:
|
|
185
|
+
ui_busy = True
|
|
186
|
+
detail = detail or f"turn未配对(started={ns}>finished={nf})"
|
|
187
|
+
return ui_busy, detail
|
|
188
|
+
|
|
189
|
+
|
|
190
|
+
def read_events(path):
|
|
191
|
+
"""读 jsonl 全部事件。"""
|
|
192
|
+
if not path or not os.path.exists(path):
|
|
193
|
+
return []
|
|
194
|
+
events = []
|
|
195
|
+
with open(path, "r", encoding="utf-8") as f:
|
|
196
|
+
for line in f:
|
|
197
|
+
line = line.strip()
|
|
198
|
+
if not line:
|
|
199
|
+
continue
|
|
200
|
+
try:
|
|
201
|
+
events.append(json.loads(line))
|
|
202
|
+
except json.JSONDecodeError:
|
|
203
|
+
continue
|
|
204
|
+
return events
|
|
205
|
+
|
|
206
|
+
|
|
207
|
+
def _ts_of(event):
|
|
208
|
+
return tt._parse_ts(event.get("ts"))
|
|
209
|
+
|
|
210
|
+
|
|
211
|
+
# ── AskUserQuestion 代答逻辑 ────────────────────────────────────────
|
|
212
|
+
# AskUserQuestion 在 transcript 里的真实生命周期 (经实测 seq 验证):
|
|
213
|
+
# tool.requested (tool_name=="AskUserQuestion", args.questions=完整问题)
|
|
214
|
+
# permission.requested(同上, UI 此时渲染选项卡) ← 在此代答(点 DOM)
|
|
215
|
+
# permission.resolved (updated_args.answers 已填) ← 表示用户/AI 已答
|
|
216
|
+
# tool.execution.finished
|
|
217
|
+
# 一个 AskUserQuestion 可含多个 question, 每个都要答; multiSelect 可多选。
|
|
218
|
+
ANSWER_TOOL = "AskUserQuestion"
|
|
219
|
+
|
|
220
|
+
# 关键词优先级: 出现这些词的 option 优先选 (信息最全/最安全)
|
|
221
|
+
PREFER_KEYWORDS = [
|
|
222
|
+
"整体", "context", "全貌", "全部", "概览", # 搜索类:选最全的
|
|
223
|
+
"web", "管理端", "pc", 1, # 平台: 默认 web
|
|
224
|
+
"默认", "推荐", "recommended",
|
|
225
|
+
"是", "确认", "继续", "ok", "yes",
|
|
226
|
+
]
|
|
227
|
+
AVOID_KEYWORDS = ["取消", "否", "不", "cancel", "no", "停止", "abort"]
|
|
228
|
+
|
|
229
|
+
|
|
230
|
+
def pick_answer_option(question_obj, force_index=None):
|
|
231
|
+
"""从 AskUserQuestion 的单个 question 里选一个 option。
|
|
232
|
+
question_obj = {question, options:[{label,description}]}
|
|
233
|
+
返回 (index, label)。
|
|
234
|
+
"""
|
|
235
|
+
opts = question_obj.get("options", [])
|
|
236
|
+
if not opts:
|
|
237
|
+
return None, None
|
|
238
|
+
if force_index is not None and 0 <= force_index < len(opts):
|
|
239
|
+
return force_index, opts[force_index].get("label", "")
|
|
240
|
+
# 关键词打分
|
|
241
|
+
best_i, best_score = 0, -1
|
|
242
|
+
for i, o in enumerate(opts):
|
|
243
|
+
text = (o.get("label", "") + " " + o.get("description", "")).lower()
|
|
244
|
+
score = 0
|
|
245
|
+
for kw in PREFER_KEYWORDS:
|
|
246
|
+
if str(kw).lower() in text:
|
|
247
|
+
score += 2
|
|
248
|
+
for kw in AVOID_KEYWORDS:
|
|
249
|
+
if str(kw).lower() in text:
|
|
250
|
+
score -= 3
|
|
251
|
+
# 默认偏向第一个(通常是推荐项)
|
|
252
|
+
if i == 0:
|
|
253
|
+
score += 1
|
|
254
|
+
if score > best_score:
|
|
255
|
+
best_score = score
|
|
256
|
+
best_i = i
|
|
257
|
+
return best_i, opts[best_i].get("label", "")
|
|
258
|
+
|
|
259
|
+
|
|
260
|
+
def find_unanswered_questions(events, baseline_turns, already_seqs):
|
|
261
|
+
"""从 transcript 事件里找出"已渲染但未答"的 AskUserQuestion。
|
|
262
|
+
|
|
263
|
+
判定规则(实测 seq 验证):
|
|
264
|
+
- 定位 permission.requested (tool_name==AskUserQuestion): UI 此时已渲染选项
|
|
265
|
+
- 同 turn 窗口内若已有 permission.resolved(updated_args.answers 非空) → 已答, 跳过
|
|
266
|
+
- 用 seq 去重; 已在 already_seqs 里的不再返回
|
|
267
|
+
|
|
268
|
+
Returns: [{seq, questions:[...]}] 按 seq 升序
|
|
269
|
+
"""
|
|
270
|
+
# 1. 收集 AskUserQuestion 的 permission.requested (seq, questions)
|
|
271
|
+
asks = []
|
|
272
|
+
for e in events:
|
|
273
|
+
d = e.get("data", {}) or {}
|
|
274
|
+
if e.get("type") != "permission.requested":
|
|
275
|
+
continue
|
|
276
|
+
if d.get("tool_name") != ANSWER_TOOL:
|
|
277
|
+
continue
|
|
278
|
+
seq = e.get("seq")
|
|
279
|
+
if seq is None or seq in already_seqs:
|
|
280
|
+
continue
|
|
281
|
+
questions = (d.get("args", {}) or {}).get("questions", [])
|
|
282
|
+
if questions:
|
|
283
|
+
asks.append((seq, e.get("ts"), questions))
|
|
284
|
+
|
|
285
|
+
if not asks:
|
|
286
|
+
return []
|
|
287
|
+
|
|
288
|
+
# 2. 收集已 resolved 的 (用 updated_args.answers 非空作为"已答"信号)
|
|
289
|
+
resolved_seqs = set()
|
|
290
|
+
for e in events:
|
|
291
|
+
d = e.get("data", {}) or {}
|
|
292
|
+
if e.get("type") != "permission.resolved":
|
|
293
|
+
continue
|
|
294
|
+
if d.get("tool_name") != ANSWER_TOOL:
|
|
295
|
+
continue
|
|
296
|
+
upd = d.get("updated_args", {}) or {}
|
|
297
|
+
if upd.get("answers"): # 有答案 = 真正答了
|
|
298
|
+
resolved_seqs.add(e.get("seq"))
|
|
299
|
+
|
|
300
|
+
# permission.resolved 的 seq 比 permission.requested 大 1~2;
|
|
301
|
+
# 用 questions 文本指纹匹配更稳: 已 resolved 的 question 文本集合
|
|
302
|
+
answered_qtexts = set()
|
|
303
|
+
for e in events:
|
|
304
|
+
if e.get("type") != "permission.resolved":
|
|
305
|
+
continue
|
|
306
|
+
d = e.get("data", {}) or {}
|
|
307
|
+
if d.get("tool_name") != ANSWER_TOOL:
|
|
308
|
+
continue
|
|
309
|
+
upd = d.get("updated_args", {}) or {}
|
|
310
|
+
for k in (upd.get("answers") or {}).keys():
|
|
311
|
+
answered_qtexts.add(k.strip())
|
|
312
|
+
|
|
313
|
+
out = []
|
|
314
|
+
for seq, ts, questions in asks:
|
|
315
|
+
# 本 AskUserQuestion 的所有 question 是否都已被答(resolved)
|
|
316
|
+
qtexts = {(q.get("question") or "").strip() for q in questions}
|
|
317
|
+
if qtexts and qtexts <= answered_qtexts:
|
|
318
|
+
continue
|
|
319
|
+
out.append({"seq": seq, "ts": ts, "questions": questions})
|
|
320
|
+
return out
|
|
321
|
+
|
|
322
|
+
|
|
323
|
+
def answer_via_dom(cdp, question_obj, chosen_label):
|
|
324
|
+
"""策略A: 点击 DOM 里渲染的选项卡 (按 label 文字匹配)。
|
|
325
|
+
QoderWork 选项卡结构: <BUTTON class="w-full flex..."> 内含序号+label+description。
|
|
326
|
+
必须点 BUTTON 本身(点内层 span/div 不触发 onClick), 且优先精确匹配。
|
|
327
|
+
返回 True 如果成功点到。"""
|
|
328
|
+
js = r"""((label) => {
|
|
329
|
+
const target = label.replace(/\s+/g,' ').trim().toLowerCase();
|
|
330
|
+
// 第1轮: 优先 <button>, 文本含 label 即可(文本带序号前缀如"2新系统...")
|
|
331
|
+
const buttons = [...document.querySelectorAll('button')].filter(b => b.offsetParent !== null);
|
|
332
|
+
for(const el of buttons){
|
|
333
|
+
const txt = (el.textContent||'').replace(/\s+/g,' ').trim();
|
|
334
|
+
if(txt.length > 200) continue; // 排除消息体大按钮
|
|
335
|
+
if(txt.toLowerCase().includes(target)){
|
|
336
|
+
el.scrollIntoView({block:'center'});
|
|
337
|
+
el.click();
|
|
338
|
+
return {clicked:true, via:'button', txt:txt.slice(0,50)};
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
// 第2轮: 退而求其次, 任何 pointer 元素 (div/span) 文本精确等于 label
|
|
342
|
+
const cands = [...document.querySelectorAll('div,span,[role="button"],[role="option"]')]
|
|
343
|
+
.filter(e => e.offsetParent !== null);
|
|
344
|
+
for(const el of cands){
|
|
345
|
+
const cs = getComputedStyle(el);
|
|
346
|
+
if(cs.cursor !== 'pointer') continue;
|
|
347
|
+
const txt = (el.textContent||'').replace(/\s+/g,' ').trim();
|
|
348
|
+
if(!txt || txt.length > 80) continue;
|
|
349
|
+
if(txt.toLowerCase() === target){
|
|
350
|
+
el.scrollIntoView({block:'center'});
|
|
351
|
+
el.click();
|
|
352
|
+
return {clicked:true, via:'pointer', txt:txt.slice(0,50)};
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
return {clicked:false};
|
|
356
|
+
})(""" + json.dumps(chosen_label) + r""")"""
|
|
357
|
+
res = cdp.eval(js)
|
|
358
|
+
return isinstance(res, dict) and res.get("clicked")
|
|
359
|
+
|
|
360
|
+
|
|
361
|
+
def answer_via_text(cdp, chosen_label, question_obj):
|
|
362
|
+
"""策略B(兜底): 把 option.label 当回复文本注入 + Enter。"""
|
|
363
|
+
focus_input(cdp)
|
|
364
|
+
time.sleep(0.3)
|
|
365
|
+
# 清空已有内容
|
|
366
|
+
cdp.eval(r"""(() => {
|
|
367
|
+
const e=document.querySelector('[contenteditable="true"]');
|
|
368
|
+
if(e){ e.focus(); e.textContent=''; }
|
|
369
|
+
return true;
|
|
370
|
+
})()""")
|
|
371
|
+
time.sleep(0.2)
|
|
372
|
+
inject_text(cdp, chosen_label)
|
|
373
|
+
time.sleep(0.3)
|
|
374
|
+
submit_enter(cdp)
|
|
375
|
+
return True
|
|
376
|
+
|
|
377
|
+
|
|
378
|
+
# ── 主流程 ──────────────────────────────────────────────────────────
|
|
379
|
+
def run(command, force_answer=None, timeout=600, no_answer=False, verbose=True):
|
|
380
|
+
"""跑一条命令到完成。
|
|
381
|
+
Args:
|
|
382
|
+
command: "/wl-search 考勤"
|
|
383
|
+
force_answer: int|None, 强制选项序号(0-based)
|
|
384
|
+
timeout: 最大等待秒数
|
|
385
|
+
no_answer: True 则不自动代答(遇到提问就停, 返回待答信息)
|
|
386
|
+
verbose: 打印过程
|
|
387
|
+
Returns dict: {status, report, turns}
|
|
388
|
+
"""
|
|
389
|
+
def log(*a):
|
|
390
|
+
if verbose:
|
|
391
|
+
print(*a)
|
|
392
|
+
|
|
393
|
+
log(f"[harness] 连接 CDP...")
|
|
394
|
+
cdp = CDP(_get_qwork_ws())
|
|
395
|
+
log("[harness] 已连上 QoderWork")
|
|
396
|
+
|
|
397
|
+
# 命令清洗: Git bash(MSYS)会把 /wl-xxx 误转成 Windows 路径
|
|
398
|
+
# 如 "/wl-search 考勤" → "D:/.../Git/wl-search 考勤" (斜杠被吃, Git/wl-search)
|
|
399
|
+
# 检测: 命令不以 /wl- 开头, 但内部有 wl- 词 → 截到 wl- 前补斜杠。
|
|
400
|
+
stripped = command.lstrip()
|
|
401
|
+
if not stripped.startswith("/wl-") and "wl-" in stripped:
|
|
402
|
+
m = re.search(r"(^|[/\\])(wl-[a-z][a-z-]*)", stripped)
|
|
403
|
+
if m:
|
|
404
|
+
# 保留 wl-xxx 及之后的所有参数(参数在第一个空格后, 未被污染)
|
|
405
|
+
head = "/" + m.group(2)
|
|
406
|
+
# 找原始命令第一个空格后的参数部分(参数含中文, 不会被 MSYS 转)
|
|
407
|
+
sp = stripped.find(" ")
|
|
408
|
+
tail = stripped[sp:] if sp > 0 else ""
|
|
409
|
+
clean = head + tail
|
|
410
|
+
log(f"[harness] 命令清洗(MSYS修复): {command!r} → {clean!r}")
|
|
411
|
+
command = clean
|
|
412
|
+
|
|
413
|
+
# 0. 注入前等待 QoderWork 空闲(防队列堆积)
|
|
414
|
+
wait_start = time.time()
|
|
415
|
+
while time.time() - wait_start < 120:
|
|
416
|
+
busy, detail = _ui_is_busy(cdp)
|
|
417
|
+
if not busy:
|
|
418
|
+
break
|
|
419
|
+
if int(time.time() - wait_start) % 10 < 2:
|
|
420
|
+
log(f"[harness] 等待 QoderWork 空闲... ({detail})")
|
|
421
|
+
time.sleep(3)
|
|
422
|
+
else:
|
|
423
|
+
log("[harness] ⚠ QoderWork 120s 仍忙碌, 强行注入(可能进队列)")
|
|
424
|
+
log("[harness] QoderWork 空闲, 开始注入")
|
|
425
|
+
|
|
426
|
+
# 1. 注入命令
|
|
427
|
+
log(f"[harness] 注入命令: {command}")
|
|
428
|
+
focus_input(cdp)
|
|
429
|
+
time.sleep(0.3)
|
|
430
|
+
inject_text(cdp, command)
|
|
431
|
+
entered = get_input_text(cdp)
|
|
432
|
+
if command.strip().split()[0] not in str(entered):
|
|
433
|
+
log(f"[harness][ERR] 注入失败, 输入框={entered!r}")
|
|
434
|
+
cdp.close()
|
|
435
|
+
return {"status": "inject_failed", "input": entered}
|
|
436
|
+
log("[harness] 注入成功, 提交...")
|
|
437
|
+
submit_enter(cdp)
|
|
438
|
+
|
|
439
|
+
start_time = time.time()
|
|
440
|
+
|
|
441
|
+
# 注入后重新锁定 session: 注入可能创建新 jsonl, 必须跟那个文件走。
|
|
442
|
+
# 等 2s 让 QoderWork 落盘, 再取当前最新 session 作为本轮监控目标。
|
|
443
|
+
time.sleep(2)
|
|
444
|
+
monitor_jsonl = _find_session_jsonl()
|
|
445
|
+
baseline_events = read_events(monitor_jsonl) if monitor_jsonl else []
|
|
446
|
+
# 记录注入前的 prompt 总数, 用于识别"我这条命令的 prompt"
|
|
447
|
+
baseline_user_prompts = sum(
|
|
448
|
+
1 for e in baseline_events
|
|
449
|
+
if e.get("type") == "input.prompt.submitted"
|
|
450
|
+
and not _is_system_prompt((e.get("data", {}) or {}).get("text_preview", ""))
|
|
451
|
+
)
|
|
452
|
+
log(f"[harness] 锁定 session: {os.path.basename(monitor_jsonl or '')} | 基线user-prompt={baseline_user_prompts}")
|
|
453
|
+
|
|
454
|
+
answered_questions = []
|
|
455
|
+
new_turn_finished = False
|
|
456
|
+
my_prompt_ts = None # 我这条命令的 prompt.submitted 时间戳
|
|
457
|
+
answered_req_seqs = set() # 已代答/已答的 tool.requested seq
|
|
458
|
+
|
|
459
|
+
# 2. 轮询 transcript
|
|
460
|
+
while time.time() - start_time < timeout:
|
|
461
|
+
time.sleep(2)
|
|
462
|
+
elapsed = int(time.time() - start_time)
|
|
463
|
+
|
|
464
|
+
# ── session 切换保护 ─────────────────────────────
|
|
465
|
+
# QoderWork 后台记忆任务可能开新 session jsonl, 导致锁定文件不再更新。
|
|
466
|
+
# 检测到更新的 session 文件就切过去跟踪。
|
|
467
|
+
cur_latest = _find_session_jsonl()
|
|
468
|
+
if cur_latest and cur_latest != monitor_jsonl:
|
|
469
|
+
try:
|
|
470
|
+
mt_locked = os.path.getmtime(monitor_jsonl) if monitor_jsonl else 0
|
|
471
|
+
mt_latest = os.path.getmtime(cur_latest)
|
|
472
|
+
if mt_latest > mt_locked + 5: # 新文件明显更新
|
|
473
|
+
log(f"[harness][{elapsed}s] 检测到 session 切换: "
|
|
474
|
+
f"{os.path.basename(monitor_jsonl or '')[:16]} → {os.path.basename(cur_latest)[:16]}")
|
|
475
|
+
monitor_jsonl = cur_latest
|
|
476
|
+
base_ev = read_events(monitor_jsonl)
|
|
477
|
+
baseline_user_prompts = sum(
|
|
478
|
+
1 for e in base_ev
|
|
479
|
+
if e.get("type") == "input.prompt.submitted"
|
|
480
|
+
and not _is_system_prompt((e.get("data", {}) or {}).get("text_preview", "")))
|
|
481
|
+
my_prompt_ts = None # 重新锁定我的命令
|
|
482
|
+
except OSError:
|
|
483
|
+
pass
|
|
484
|
+
|
|
485
|
+
jsonl = monitor_jsonl
|
|
486
|
+
if not jsonl or not os.path.exists(jsonl):
|
|
487
|
+
jsonl = _find_session_jsonl()
|
|
488
|
+
if not jsonl:
|
|
489
|
+
continue
|
|
490
|
+
events = read_events(jsonl)
|
|
491
|
+
|
|
492
|
+
# ── 检测 AskUserQuestion (未答的) ─────────────────
|
|
493
|
+
if not no_answer:
|
|
494
|
+
pending = find_unanswered_questions(events, baseline_user_prompts, answered_req_seqs)
|
|
495
|
+
for info in pending:
|
|
496
|
+
seq = info["seq"]
|
|
497
|
+
if seq in answered_req_seqs:
|
|
498
|
+
continue
|
|
499
|
+
q0 = info["questions"][0]
|
|
500
|
+
qtxt = q0.get("question", "")
|
|
501
|
+
# 同问题文本不重复代答
|
|
502
|
+
if any(aq.get("question") == qtxt for aq in answered_questions):
|
|
503
|
+
answered_req_seqs.add(seq)
|
|
504
|
+
continue
|
|
505
|
+
|
|
506
|
+
idx, label = pick_answer_option(q0, force_answer)
|
|
507
|
+
log(f"[harness][{elapsed}s] AI提问(seq={seq}): {qtxt}")
|
|
508
|
+
log(f"[harness][{elapsed}s] 选项: {[o.get('label','') for o in q0.get('options',[])]}")
|
|
509
|
+
log(f"[harness][{elapsed}s] 代答: #{idx} {label!r}")
|
|
510
|
+
|
|
511
|
+
# 策略A: 点 DOM 选项
|
|
512
|
+
ok = answer_via_dom(cdp, q0, label)
|
|
513
|
+
if not ok:
|
|
514
|
+
log(f"[harness][{elapsed}s] DOM点击失败, 兜底注入文本...")
|
|
515
|
+
answer_via_text(cdp, label, q0)
|
|
516
|
+
answered_questions.append({"question": qtxt, "answer_index": idx,
|
|
517
|
+
"answer_label": label})
|
|
518
|
+
answered_req_seqs.add(seq)
|
|
519
|
+
time.sleep(2)
|
|
520
|
+
|
|
521
|
+
# ── 定位"我这条命令"的 prompt.submitted ──────────
|
|
522
|
+
if my_prompt_ts is None:
|
|
523
|
+
user_prompts = [(e.get("ts"), (e.get("data", {}) or {}).get("text_preview", ""))
|
|
524
|
+
for e in events
|
|
525
|
+
if e.get("type") == "input.prompt.submitted"
|
|
526
|
+
and not _is_system_prompt((e.get("data", {}) or {}).get("text_preview", ""))]
|
|
527
|
+
if len(user_prompts) > baseline_user_prompts:
|
|
528
|
+
# 取第 (baseline+1) 条用户 prompt = 我注入的
|
|
529
|
+
my_prompt_ts = user_prompts[baseline_user_prompts][0]
|
|
530
|
+
log(f"[harness][{elapsed}s] 锁定我的命令 prompt: {my_prompt_ts}")
|
|
531
|
+
|
|
532
|
+
# ── 检测 turn.finished (只认我的命令对应的那个) ──
|
|
533
|
+
if my_prompt_ts is not None:
|
|
534
|
+
# 找 my_prompt_ts 之后的 turn.finished (跳过系统后台 turn)
|
|
535
|
+
# 系统 turn 的特征: 它前面紧跟的 prompt 是系统类
|
|
536
|
+
for e in events:
|
|
537
|
+
if e.get("type") != "turn.finished":
|
|
538
|
+
continue
|
|
539
|
+
if my_prompt_ts and e.get("ts", "") < my_prompt_ts:
|
|
540
|
+
continue # 早于我的命令, 忽略
|
|
541
|
+
# 判断这个 turn.finished 是否归属"用户命令"(非系统后台):
|
|
542
|
+
# 取本 turn 窗口内最近的 prompt.submitted, 若是系统类则跳过
|
|
543
|
+
prompt_before = None
|
|
544
|
+
for pe in events:
|
|
545
|
+
if pe.get("type") != "input.prompt.submitted":
|
|
546
|
+
continue
|
|
547
|
+
if pe.get("ts", "") > e.get("ts", ""):
|
|
548
|
+
break
|
|
549
|
+
prompt_before = pe
|
|
550
|
+
if prompt_before and _is_system_prompt(
|
|
551
|
+
(prompt_before.get("data", {}) or {}).get("text_preview", "")):
|
|
552
|
+
continue # 这是后台 review turn, 跳过
|
|
553
|
+
# 归属我的命令 → 完成
|
|
554
|
+
new_turn_finished = True
|
|
555
|
+
dur = (e.get("data", {}) or {}).get("duration_ms")
|
|
556
|
+
log(f"[harness][{elapsed}s] ✓ 我的命令 turn.finished! (dur={dur}ms)")
|
|
557
|
+
break
|
|
558
|
+
|
|
559
|
+
if new_turn_finished:
|
|
560
|
+
break # 命令已完成, 退出轮询
|
|
561
|
+
|
|
562
|
+
# 状态提示 (每 ~6s 一次, 不刷屏)
|
|
563
|
+
cur_turns = sum(1 for e in events if e.get("type") == "turn.finished")
|
|
564
|
+
if elapsed % 6 < 2:
|
|
565
|
+
log(f"[harness][{elapsed}s] 运行中... (events={len(events)}, "
|
|
566
|
+
f"total_turns={cur_turns}, 已答提问={len(answered_questions)})")
|
|
567
|
+
|
|
568
|
+
cdp.close()
|
|
569
|
+
|
|
570
|
+
if not new_turn_finished:
|
|
571
|
+
log(f"[harness] ⚠ 超时 {timeout}s, 未等到 turn.finished")
|
|
572
|
+
return {"status": "timeout", "timeout_s": timeout,
|
|
573
|
+
"answered": answered_questions, "jsonl": monitor_jsonl}
|
|
574
|
+
|
|
575
|
+
# 3. 算耗时报告
|
|
576
|
+
jsonl = monitor_jsonl or _find_session_jsonl()
|
|
577
|
+
log(f"[harness] 出报告: {os.path.basename(jsonl)}")
|
|
578
|
+
report = tt.analyze(jsonl)
|
|
579
|
+
return {"status": "completed", "report": report, "answered": answered_questions,
|
|
580
|
+
"jsonl": jsonl, "elapsed_s": round(time.time() - start_time, 1)}
|
|
581
|
+
|
|
582
|
+
|
|
583
|
+
def main():
|
|
584
|
+
import argparse
|
|
585
|
+
ap = argparse.ArgumentParser()
|
|
586
|
+
ap.add_argument("command", help='要跑的命令, 如 "/wl-search 考勤"')
|
|
587
|
+
ap.add_argument("--auto-answer", type=int, default=None,
|
|
588
|
+
help="强制选项序号(0-based), 如 --auto-answer=0")
|
|
589
|
+
ap.add_argument("--no-answer", action="store_true", help="不自动代答")
|
|
590
|
+
ap.add_argument("--timeout", type=int, default=600)
|
|
591
|
+
ap.add_argument("--json", action="store_true")
|
|
592
|
+
args = ap.parse_args()
|
|
593
|
+
|
|
594
|
+
result = run(args.command, force_answer=args.auto_answer,
|
|
595
|
+
timeout=args.timeout, no_answer=args.no_answer)
|
|
596
|
+
|
|
597
|
+
if args.json:
|
|
598
|
+
print(json.dumps(result, ensure_ascii=False, indent=2, default=str))
|
|
599
|
+
else:
|
|
600
|
+
print("\n" + "=" * 60)
|
|
601
|
+
if result.get("status") == "completed":
|
|
602
|
+
print("✅ 完成! 耗时报告:")
|
|
603
|
+
print("=" * 60)
|
|
604
|
+
print(tt._format_report(result["report"]))
|
|
605
|
+
print("\n自动代答:")
|
|
606
|
+
for a in result.get("answered", []):
|
|
607
|
+
print(f" Q: {a['question']}")
|
|
608
|
+
print(f" A: #{a['answer_index']} {a['answer_label']}")
|
|
609
|
+
print(f"\n实际墙钟: {result.get('elapsed_s')}s")
|
|
610
|
+
elif result.get("status") == "timeout":
|
|
611
|
+
print(f"⏰ 超时 {result.get('timeout_s')}s")
|
|
612
|
+
else:
|
|
613
|
+
print(f"❌ {result.get('status')}: {result}")
|
|
614
|
+
|
|
615
|
+
|
|
616
|
+
if __name__ == "__main__":
|
|
617
|
+
main()
|