@hupan56/wlkj 3.0.0 → 3.1.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bin/cli.js +1435 -1373
- package/package.json +1 -1
- package/templates/qoder/commands/optional/wl-insight.md +6 -4
- 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 +5 -5
- 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 +3 -3
- 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 +10 -10
- package/templates/qoder/config.yaml +35 -13
- package/templates/qoder/hooks/post-tool-use.py +181 -0
- package/templates/qoder/hooks/session-start.py +82 -1
- 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 +70 -8
- package/templates/qoder/scripts/README.md +78 -115
- 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/{report → deployment/setup}/__init__.py +11 -11
- package/templates/qoder/scripts/{setup → deployment/setup}/carriers.py +33 -26
- package/templates/qoder/scripts/deployment/setup/carriers_verify.py +148 -0
- package/templates/qoder/scripts/{setup → deployment/setup}/init_doctor.py +40 -16
- package/templates/qoder/scripts/{setup → deployment/setup}/install_qoderwork.py +20 -12
- package/templates/qoder/scripts/{setup → deployment/setup}/platform_doctor.py +8 -2
- package/templates/qoder/scripts/{setup → deployment/setup}/repo_root.py +113 -112
- package/templates/qoder/scripts/{setup → deployment/setup}/setup.py +31 -11
- package/templates/qoder/scripts/{setup → deployment/setup}/setup_lanhu.py +13 -7
- package/templates/qoder/scripts/domain/__init__.py +0 -0
- package/templates/qoder/scripts/domain/design/__init__.py +0 -0
- package/templates/qoder/scripts/{report → domain/design}/fill_prototype.py +12 -6
- package/templates/qoder/scripts/{report → domain/design}/gen_design_doc.py +8 -2
- package/templates/qoder/scripts/{setup → domain/kg}/__init__.py +11 -11
- package/templates/qoder/scripts/domain/kg/build/__init__.py +0 -0
- package/templates/qoder/scripts/{kg → domain/kg/build}/build_entity_registry.py +7 -2
- package/templates/qoder/scripts/{kg → domain/kg/build}/build_relations.py +7 -2
- package/templates/qoder/scripts/{kg → domain/kg/build}/build_style_index.py +13 -7
- package/templates/qoder/scripts/{kg → domain/kg/build}/build_workflows.py +7 -2
- package/templates/qoder/scripts/{kg → domain/kg/build}/kg_build.py +76 -9
- package/templates/qoder/scripts/{kg → domain/kg/build}/kg_build_db.py +7 -2
- package/templates/qoder/scripts/{kg → domain/kg/build}/kg_incremental.py +11 -6
- package/templates/qoder/scripts/{report → domain/kg/build}/learn_aggregate.py +9 -4
- 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/{common → domain/kg/extract}/test_extract.py +115 -115
- 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 → domain/kg/graph}/kg_link_db.py +7 -2
- package/templates/qoder/scripts/domain/kg/graph/kg_semantic.py +683 -0
- package/templates/qoder/scripts/{kg → domain/kg}/kg.py +263 -30
- package/templates/qoder/scripts/{common → domain/kg}/kg_capabilities.py +182 -182
- package/templates/qoder/scripts/domain/kg/search/__init__.py +0 -0
- package/templates/qoder/scripts/{kg → domain/kg/search}/context_pack.py +30 -9
- package/templates/qoder/scripts/{kg → domain/kg/search}/enrich_prompt.py +10 -4
- package/templates/qoder/scripts/{kg → domain/kg/search}/prefetch.py +33 -8
- package/templates/qoder/scripts/{common → domain/kg/search}/search_engine.py +207 -205
- package/templates/qoder/scripts/{kg → domain/kg/search}/search_index.py +143 -77
- 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 → domain/kg/storage}/kg_duckdb.py +38 -14
- package/templates/qoder/scripts/domain/learning/__init__.py +0 -0
- package/templates/qoder/scripts/{report → domain/learning}/learn.py +8 -3
- package/templates/qoder/scripts/{mcp → domain/report}/__init__.py +11 -11
- package/templates/qoder/scripts/{report → domain/report}/add_session.py +9 -3
- package/templates/qoder/scripts/{report → domain/report}/export.py +10 -5
- package/templates/qoder/scripts/{report → domain/report}/report.py +9 -4
- package/templates/qoder/scripts/domain/report/report_snapshot.py +261 -0
- package/templates/qoder/scripts/{report → domain/report}/role.py +10 -4
- package/templates/qoder/scripts/{report → domain/report}/status.py +112 -9
- 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/{report → domain/requirement}/archive_prd.py +8 -2
- package/templates/qoder/scripts/{report → domain/requirement}/req.py +228 -222
- package/templates/qoder/scripts/{kg → domain/task}/__init__.py +11 -11
- package/templates/qoder/scripts/{task → domain/task}/git_sync.py +40 -21
- package/templates/qoder/scripts/{task → domain/task}/syncgate.py +13 -8
- package/templates/qoder/scripts/{task → domain/task}/task.py +13 -5
- package/templates/qoder/scripts/{task → domain/task}/task_lifecycle.py +606 -596
- package/templates/qoder/scripts/{task → domain/task}/task_query.py +162 -161
- package/templates/qoder/scripts/{task → domain/task}/task_relations.py +425 -424
- package/templates/qoder/scripts/{task → domain/task}/team_sync.py +9 -4
- package/templates/qoder/scripts/domain/task/zentao_sync.py +370 -0
- package/templates/qoder/scripts/foundation/__init__.py +0 -0
- package/templates/qoder/scripts/{common → foundation}/bootstrap.py +10 -10
- package/templates/qoder/scripts/foundation/core/__init__.py +0 -0
- package/templates/qoder/scripts/{common → foundation/core}/cmd_registry.py +1 -1
- package/templates/qoder/scripts/{common → foundation/core}/config.py +1 -1
- package/templates/qoder/scripts/{common → foundation/core}/paths.py +706 -586
- package/templates/qoder/scripts/foundation/data/__init__.py +0 -0
- package/templates/qoder/scripts/{common → foundation/data}/contract.py +2 -2
- package/templates/qoder/scripts/{common → foundation/data}/events.py +2 -2
- package/templates/qoder/scripts/{common → foundation/data}/result.py +1 -1
- package/templates/qoder/scripts/{common → foundation/data}/task_utils.py +427 -404
- package/templates/qoder/scripts/foundation/identity/__init__.py +0 -0
- package/templates/qoder/scripts/{common → foundation/identity}/check_publish.py +103 -98
- package/templates/qoder/scripts/{common → foundation/identity}/developer.py +239 -239
- package/templates/qoder/scripts/{common → foundation/identity}/guard.py +2 -2
- package/templates/qoder/scripts/{common → foundation/identity}/identity.py +14 -10
- package/templates/qoder/scripts/{common → foundation/identity}/roles.py +1 -1
- 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 +2 -2
- 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/{common → foundation/protocol}/mcp_base.py +1 -1
- 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/{mcp → protocol/mcp}/lanhu_stdio_wrapper.py +11 -5
- package/templates/qoder/scripts/{mcp → protocol/mcp}/mcp_doctor.py +47 -14
- package/templates/qoder/scripts/protocol/mcp/mcp_launcher.py +600 -0
- package/templates/qoder/scripts/{mcp → protocol/mcp}/mysql_mcp_server.py +181 -67
- 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/{report → validation/metrics}/eval_prd.py +17 -10
- 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/{test → validation/test}/autotest.py +64 -4
- package/templates/qoder/scripts/{common → validation/test}/autotest_auth.py +1 -1
- package/templates/qoder/scripts/{test → validation/test}/autotest_batch.py +16 -3
- package/templates/qoder/scripts/{test → validation/test}/autotest_data.py +17 -12
- package/templates/qoder/scripts/validation/test/autotest_opencli.py +646 -0
- package/templates/qoder/scripts/{test → validation/test}/autotest_run.py +17 -3
- package/templates/qoder/scripts/validation/test/autotest_webaccess.py +571 -0
- package/templates/qoder/scripts/{test → validation/test}/benchmark.py +12 -6
- package/templates/qoder/scripts/{test → validation/test}/kg_auto_login.py +8 -2
- package/templates/qoder/scripts/{test → validation/test}/kg_test_runner.py +7 -2
- package/templates/qoder/scripts/{test → validation/test}/page_probe.py +8 -3
- package/templates/qoder/scripts/validation/test/smoke_all_commands.py +170 -0
- package/templates/qoder/settings.json +30 -0
- package/templates/qoder/skills/design-import/SKILL.md +22 -13
- package/templates/qoder/skills/design-review/SKILL.md +19 -10
- package/templates/qoder/skills/prd-generator/SKILL.md +22 -18
- package/templates/qoder/skills/prd-review/SKILL.md +6 -4
- package/templates/qoder/skills/prototype-generator/SKILL.md +63 -14
- package/templates/qoder/skills/spec-coder/SKILL.md +22 -13
- package/templates/qoder/skills/spec-generator/SKILL.md +22 -10
- package/templates/qoder/skills/test-generator/SKILL.md +7 -5
- package/templates/qoder/skills/wl-code/SKILL.md +23 -11
- package/templates/qoder/skills/wl-commit/SKILL.md +9 -8
- package/templates/qoder/skills/wl-design/SKILL.md +5 -3
- package/templates/qoder/skills/wl-init/SKILL.md +6 -6
- package/templates/qoder/skills/wl-insight/SKILL.md +23 -10
- package/templates/qoder/skills/wl-prd-full/SKILL.md +57 -6
- package/templates/qoder/skills/wl-prd-quick/SKILL.md +3 -3
- package/templates/qoder/skills/wl-prd-review/SKILL.md +16 -4
- package/templates/qoder/skills/wl-report/SKILL.md +25 -23
- package/templates/qoder/skills/wl-search/SKILL.md +149 -78
- package/templates/qoder/skills/wl-spec/SKILL.md +7 -5
- package/templates/qoder/skills/wl-status/SKILL.md +48 -16
- package/templates/qoder/skills/wl-task/SKILL.md +92 -10
- package/templates/qoder/skills/wl-test/SKILL.md +75 -29
- package/templates/qoder/templates/prd-full-template.md +7 -0
- package/templates/root/AGENTS.md +275 -236
- 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/.runtime/ctx-cache-5660152f1d6dd819.md +0 -23
- package/templates/.qoder/.runtime/ctx-cache-afdce0dac06b25b0.md +0 -23
- package/templates/.qoder/.runtime/search-cache-eae7644e7b122f35.txt +0 -1
- package/templates/.qoder/learning/eval-history.jsonl +0 -28
- package/templates/data/index/wiki-index.json +0 -8
- package/templates/qoder/nul +0 -4
- package/templates/qoder/scripts/kg/extract.py +0 -398
- package/templates/qoder/scripts/kg/kg_semantic.py +0 -156
- package/templates/qoder/scripts/mcp/kg_mcp_server.py +0 -606
- package/templates/qoder/scripts/mcp/mcp_launcher.py +0 -442
- package/templates/qoder/scripts/mcp/zentao_mcp_server.py +0 -384
- package/templates/qoder/scripts/task/__init__.py +0 -11
- package/templates/qoder/scripts/test/__init__.py +0 -11
- package/templates/qoder/scripts/wlkj.py +0 -116
- /package/templates/qoder/scripts/{common → deployment}/__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/{test → validation/test}/secure-ls.js +0 -0
|
@@ -0,0 +1,220 @@
|
|
|
1
|
+
# QoderWork 命令自动化测试框架 — 成果报告
|
|
2
|
+
|
|
3
|
+
> 把 QoderWork 当被测系统:通过 CDP 自动注入命令、自动代答 AskUserQuestion、
|
|
4
|
+
> 等 turn.finished、用 transcript 遥测出耗时构成报告。
|
|
5
|
+
> 让 `/wl-*` 命令的"首回延迟 / 工具耗时 / 模型推理占比"可量化、可回归。
|
|
6
|
+
|
|
7
|
+
## 组件
|
|
8
|
+
|
|
9
|
+
| 文件 | 作用 | 状态 |
|
|
10
|
+
|------|------|------|
|
|
11
|
+
| `transcript_timing.py` | 解析 QoderWork transcript jsonl,算每轮命令耗时构成 | ✅ 真实 jsonl 验证通过 |
|
|
12
|
+
| `qwork_harness.py` | CDP 注入命令 → 轮询提问自动代答 → 出报告 | ✅ 端到端实测通过 |
|
|
13
|
+
|
|
14
|
+
两个文件都在 `.qoder/scripts/eval/` 下,纯 Python 3 + websocket-client,零其它依赖。
|
|
15
|
+
|
|
16
|
+
---
|
|
17
|
+
|
|
18
|
+
## transcript_timing.py — 计时引擎
|
|
19
|
+
|
|
20
|
+
解析 `~/.qoderwork/logs/sessions/**/segments/*.jsonl`(QoderWork 遥测事件流),
|
|
21
|
+
按 turn 聚合,产出每条命令的耗时构成:
|
|
22
|
+
|
|
23
|
+
```
|
|
24
|
+
[/wl-search ] 61.4s (首回12.41s, 工具3.28s/5.3%, 模型59.23s, 10次调用, 4轮迭代)
|
|
25
|
+
└ /wl-search 考勤
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
### 指标定义(对齐 QoderWork 遥测事件)
|
|
29
|
+
|
|
30
|
+
| 指标 | 来源事件 | 含义 |
|
|
31
|
+
|------|---------|------|
|
|
32
|
+
| `duration_ms` | `turn.finished.data.duration_ms` | 权威总耗时(框架自报) |
|
|
33
|
+
| `first_reply_s` | prompt → 首个 `model.response.completed` | 首次回复延迟 |
|
|
34
|
+
| `tool_total_s` | Σ `tool.requested` ↔ `tool.execution.finished`(按工具名配对) | 工具执行总耗时 |
|
|
35
|
+
| `model_total_s` | Σ `model.request.started` ↔ `.completed`(按 request_index) | 模型推理窗口 |
|
|
36
|
+
| `by_tool` | 同上,按 tool_name 分桶 | 哪个工具最慢 |
|
|
37
|
+
| `other_s` | duration - tool - model | 排队/框架开销 |
|
|
38
|
+
|
|
39
|
+
### 关键事件类型(实测确认)
|
|
40
|
+
|
|
41
|
+
```
|
|
42
|
+
input.prompt.submitted 用户发命令 (data.text_preview 含命令文本)
|
|
43
|
+
turn.started / .finished 一轮对话 (finished.duration_ms = 权威耗时)
|
|
44
|
+
model.request.started 开始请求模型 (data.request_index)
|
|
45
|
+
model.response.completed 模型返回 (data.request_index, data.stop_reason)
|
|
46
|
+
tool.requested 模型请求调工具 (data.tool_name, data.args)
|
|
47
|
+
tool.execution.finished 工具执行完 (data.tool_name, data.status)
|
|
48
|
+
permission.requested 权限确认 (AskUserQuestion 在此渲染选项)
|
|
49
|
+
permission.resolved 权限已决 (updated_args.answers 含用户答案)
|
|
50
|
+
loop.iteration.started/finished agent 内部迭代
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
### 用法
|
|
54
|
+
|
|
55
|
+
```bash
|
|
56
|
+
# 分析单个 jsonl
|
|
57
|
+
python transcript_timing.py <jsonl文件>
|
|
58
|
+
python transcript_timing.py <jsonl文件> --json # 机器可读
|
|
59
|
+
|
|
60
|
+
# 扫目录下所有 jsonl 汇总
|
|
61
|
+
python transcript_timing.py <dir>
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
也被 harness 实时调用:`from transcript_timing import analyze`。
|
|
65
|
+
|
|
66
|
+
---
|
|
67
|
+
|
|
68
|
+
## qwork_harness.py — 自动化驱动
|
|
69
|
+
|
|
70
|
+
### 流程
|
|
71
|
+
|
|
72
|
+
```
|
|
73
|
+
连 CDP(6529) → 清洗命令(修MSYS污染) → focus输入框 → Input.insertText注入
|
|
74
|
+
→ Enter提交 → 等落盘 → 锁定session jsonl → 轮询:
|
|
75
|
+
├─ find_unanswered_questions: 检测待答 AskUserQuestion
|
|
76
|
+
│ └─ pick_answer_option: 关键词打分选最全/最安全项
|
|
77
|
+
│ └─ answer_via_dom(点选项) / answer_via_text(兜底注入)
|
|
78
|
+
└─ turn.finished 出现 → analyze() 出报告
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
### AskUserQuestion 代答机制(实测 seq 验证)
|
|
82
|
+
|
|
83
|
+
AskUserQuestion 在 transcript 里的真实生命周期:
|
|
84
|
+
|
|
85
|
+
| seq | type | 含义 | harness 动作 |
|
|
86
|
+
|-----|------|------|-------------|
|
|
87
|
+
| N | `tool.requested` | 模型发起,args.questions=完整问题+选项 | — |
|
|
88
|
+
| N+1 | `permission.requested` | UI 渲染选项卡 | **在此代答** |
|
|
89
|
+
| N+2 | `permission.resolved` | answers 已填 | 表示已答,跳过 |
|
|
90
|
+
| N+3 | `tool.execution.finished` | 工具结束 | — |
|
|
91
|
+
|
|
92
|
+
- **tool_name 精确等于 `"AskUserQuestion"`**(大写驼峰),不是含 "ask"
|
|
93
|
+
- 一个 AskUserQuestion 可含**多个 question**,每个都要答
|
|
94
|
+
- `multiSelect: true` 的可多选
|
|
95
|
+
- 答案在 `permission.resolved.data.updated_args.answers`(以 question 文本为 key)
|
|
96
|
+
|
|
97
|
+
### 代答选择策略
|
|
98
|
+
|
|
99
|
+
`pick_answer_option`:对每个 option 的 label+description 关键词打分
|
|
100
|
+
|
|
101
|
+
- **PREFER**(加分):整体/全部/概览/web/管理端/默认/推荐/确认/继续
|
|
102
|
+
- **AVOID**(减分):取消/否/cancel/停止
|
|
103
|
+
- 兜底:偏第一项(通常是推荐项)
|
|
104
|
+
- 可 `--auto-answer=N` 强制指定
|
|
105
|
+
|
|
106
|
+
### 已修复的实战 bug
|
|
107
|
+
|
|
108
|
+
1. **MSYS 路径污染** ⚠️ 关键
|
|
109
|
+
Git bash 会把 `/wl-search 考勤` 转成 `D:/.../Git/wl-search 考勤`(斜杠被吃成路径分隔)。
|
|
110
|
+
harness 内置清洗:检测到命令不以 `/wl-` 开头但含 `wl-` → 截断还原成 `/wl-xxx`。
|
|
111
|
+
**根因规避**:调用时不要让命令经过 bash argv,用文件/环境变量传。
|
|
112
|
+
|
|
113
|
+
2. **session 切换失锁**
|
|
114
|
+
注入命令可能创建新 jsonl,harness 原先固定用注入前抓的 baseline jsonl → 永远等不到 turn.finished。
|
|
115
|
+
修复:提交后等落盘,重新锁定当前最新 session 作为监控目标;
|
|
116
|
+
轮询中检测到更新的 session 文件自动切换跟踪。
|
|
117
|
+
|
|
118
|
+
3. **代答事件误判**
|
|
119
|
+
原先用 `tool_name 含 "ask"` + tool.execution.finished 判定,与真实事件名不符。
|
|
120
|
+
修复:改用 `permission.requested`(tool_name=="AskUserQuestion")检测待答,
|
|
121
|
+
`permission.resolved.updated_args.answers` 非空判定已答。
|
|
122
|
+
|
|
123
|
+
4. **后台 review turn 干扰** ⚠️ P3 实测发现
|
|
124
|
+
QoderWork 每条用户命令跑完后,自动追加 `[SYSTEM: automated background review]`
|
|
125
|
+
后台审核 turn(也产生 turn.finished)。旧的 `baseline+1` 计数法把它误判成"命令完成",
|
|
126
|
+
导致命令还没真跑完就注入下一条 → 队列堆积、串台。
|
|
127
|
+
修复:turn 完成判定改为"匹配我注入命令的 prompt + 跳过系统后台 turn"
|
|
128
|
+
(`_is_system_prompt` 识别 SYSTEM/automated background/must be fulfilled)。
|
|
129
|
+
|
|
130
|
+
5. **队列堆积保护** ⚠️ P3 实测发现
|
|
131
|
+
连续注入时若 QoderWork 正忙,命令进队列而非立即执行。
|
|
132
|
+
修复:注入前 `_ui_is_busy` 双重检测(UI 文字"处理中/队列" + transcript turn 配对),
|
|
133
|
+
忙则等待最多 120s,空闲才注入。
|
|
134
|
+
|
|
135
|
+
6. **完成后空转不退出** ⚠️ P3 实测发现
|
|
136
|
+
检测到 turn.finished 后只 break 内层 for,没跳出外层 while → 每 2s 重复打印直到超时。
|
|
137
|
+
修复:完成判定后 `if new_turn_finished: break` 跳出 while。
|
|
138
|
+
|
|
139
|
+
### 用法
|
|
140
|
+
|
|
141
|
+
```bash
|
|
142
|
+
# 基本(注意:命令含 / 在 bash 下会被 MSYS 污染, 见上)
|
|
143
|
+
python qwork_harness.py "/wl-search 考勤"
|
|
144
|
+
|
|
145
|
+
# 强制选项序号(0-based)
|
|
146
|
+
python qwork_harness.py "/wl-prd-full 用户管理" --auto-answer=0
|
|
147
|
+
|
|
148
|
+
# 只跑不代答(遇到提问就停)
|
|
149
|
+
python qwork_harness.py "/wl-search 考勤" --no-answer
|
|
150
|
+
|
|
151
|
+
# JSON 输出
|
|
152
|
+
python qwork_harness.py "/wl-search 考勤" --json
|
|
153
|
+
```
|
|
154
|
+
|
|
155
|
+
---
|
|
156
|
+
|
|
157
|
+
## 实测结果(2026-06-24)
|
|
158
|
+
|
|
159
|
+
### 干净 session 单条验证 ✓
|
|
160
|
+
|
|
161
|
+
```
|
|
162
|
+
[harness] 注入命令: /wl-search 用电导入
|
|
163
|
+
[harness][7s] 锁定我的命令 prompt: ...17:56:43...
|
|
164
|
+
[harness][62s] ✓ 我的命令 turn.finished! (dur=55226ms)
|
|
165
|
+
[result] wall=65.5s status=completed
|
|
166
|
+
命令耗时=55.23s 工具=1.3s 模型=54.0s
|
|
167
|
+
```
|
|
168
|
+
命令完成即退出,不再空转。核心链路全部验证通过。
|
|
169
|
+
|
|
170
|
+
### 批量测试发现的问题(P3)
|
|
171
|
+
|
|
172
|
+
批量跑只读命令时暴露了 QoderWork 的两类行为,harness 已针对修复:
|
|
173
|
+
|
|
174
|
+
| 现象 | 根因 | 修复 |
|
|
175
|
+
|------|------|------|
|
|
176
|
+
| 命令进队列堆积 | 后台 review turn 的 finished 被误判为命令完成,提前注入下一条 | turn 判定跳过后台 turn(bug #4) |
|
|
177
|
+
| harness 永久超时 | 完成后没跳出 while 循环 | 完成即 break(bug #6) |
|
|
178
|
+
| 注入被挡 | QoderWork 正忙时新命令进队列 | 注入前等空闲(bug #5) |
|
|
179
|
+
|
|
180
|
+
### 性能画像(来自真实 transcript)
|
|
181
|
+
|
|
182
|
+
`/wl-search` 系列典型耗时构成:
|
|
183
|
+
- 总耗时 12~118s(视关键词),其中**模型推理占 ~95%+**,工具 <5%
|
|
184
|
+
- 首回延迟 ~12s,4 轮 agent 迭代,10 次 MCP 工具调用
|
|
185
|
+
- 瓶颈在模型推理(连读多个文件 + 拼结果),不在工具执行
|
|
186
|
+
|
|
187
|
+
---
|
|
188
|
+
|
|
189
|
+
## 已知限制(P3 遗留)
|
|
190
|
+
|
|
191
|
+
1. **污染 session 的 turn 归属判定**
|
|
192
|
+
单个 session 累积过多 turn(测试塞入 20+ 个)后,用户命令 turn 与后台 review turn
|
|
193
|
+
时序交织,`_is_system_prompt` 启发式可能误判归属,导致等不到 turn.finished 超时。
|
|
194
|
+
**规避**:测试用干净 session(新建对话);生产用单条注入。
|
|
195
|
+
**后续**:改用 turn.started↔finished 的 seq 严格配对替代"最近 prompt"启发式。
|
|
196
|
+
|
|
197
|
+
2. **AskUserQuestion 选项卡 DOM 点击不生效**
|
|
198
|
+
QoderWork 选项卡是 React 组件,监听 pointer/mousedown 而非 click。
|
|
199
|
+
JS `.click()` 和 CDP `mousePressed` 都无法触发选择。
|
|
200
|
+
**影响**:代答的 DOM 策略(answer_via_dom)失效,会回退到文本注入(也未必生效)。
|
|
201
|
+
**后续**:研究 QoderWork 选项卡事件绑定,或用 CDP `Input.dispatchMouseEvent`
|
|
202
|
+
的完整 pointer 事件序列(mouseMoved+pointerDown+pointerUp)。
|
|
203
|
+
|
|
204
|
+
3. **命令经 bash argv 会被 MSYS 污染**
|
|
205
|
+
`/wl-search` 在 Git bash 下被转成路径。harness 内置清洗能还原,但最稳是绕过 argv
|
|
206
|
+
(命令写在调用脚本里,或用环境变量)。
|
|
207
|
+
|
|
208
|
+
---
|
|
209
|
+
|
|
210
|
+
## 用法
|
|
211
|
+
|
|
212
|
+
```bash
|
|
213
|
+
# 推荐: 命令写在脚本里调用(避开 bash argv 污染)
|
|
214
|
+
# 或直接 python qwork_harness.py "/wl-search 考勤" (注意 / 在 bash 下的转义)
|
|
215
|
+
|
|
216
|
+
python qwork_harness.py "/wl-search 考勤"
|
|
217
|
+
python qwork_harness.py "/wl-prd-full 用户管理" --auto-answer=0 # 强制选项序号
|
|
218
|
+
python qwork_harness.py "/wl-search 考勤" --no-answer # 不代答
|
|
219
|
+
python qwork_harness.py "/wl-search 考勤" --json # JSON 输出
|
|
220
|
+
```
|
|
@@ -0,0 +1,386 @@
|
|
|
1
|
+
"""
|
|
2
|
+
transcript_timing.py — QoderWork transcript jsonl 计时引擎 (P1)
|
|
3
|
+
|
|
4
|
+
解析 ~/.qoderwork/logs/sessions/**/segments/*.jsonl, 算出每条命令(turn)的耗时构成:
|
|
5
|
+
- 首次回复延迟 (prompt.submitted → 首个 model.response.completed)
|
|
6
|
+
- 工具执行总耗时 (Σ tool.requested ↔ tool.execution.finished, 按 turn 内序号配对)
|
|
7
|
+
- AI 推理窗口 (model.request.started ↔ model.response.completed, 按 request_index)
|
|
8
|
+
- 按工具名分桶 (哪个工具最慢)
|
|
9
|
+
- 直接读 turn.finished.data.duration_ms 作为权威总耗时
|
|
10
|
+
|
|
11
|
+
QoderWork transcript 是遥测事件流, 每行: {ts, seq, level, type, data}
|
|
12
|
+
关键事件类型:
|
|
13
|
+
input.prompt.submitted 用户真正发出一条命令 (data.text_preview 含命令文本)
|
|
14
|
+
turn.started / .finished 一轮 agent 对话 (finished.data.duration_ms = 权威耗时)
|
|
15
|
+
tool.requested 模型请求调用工具 (data.tool_name, data.args)
|
|
16
|
+
tool.execution.finished 工具执行完 (data.tool_name, data.status)
|
|
17
|
+
model.request.started 开始请求模型 (data.request_index)
|
|
18
|
+
model.response.completed 模型返回 (data.request_index, data.stop_reason)
|
|
19
|
+
|
|
20
|
+
纯函数, 零外部依赖。既能分析历史 transcript, 也被 harness 实时调用。
|
|
21
|
+
|
|
22
|
+
用法:
|
|
23
|
+
python transcript_timing.py <jsonl文件路径>
|
|
24
|
+
python transcript_timing.py <jsonl文件路径> --json # 机器可读
|
|
25
|
+
python transcript_timing.py <dir> # 扫目录下所有 jsonl 汇总
|
|
26
|
+
from transcript_timing import analyze; analyze(path)
|
|
27
|
+
"""
|
|
28
|
+
import json
|
|
29
|
+
import re
|
|
30
|
+
import sys
|
|
31
|
+
import os
|
|
32
|
+
import glob
|
|
33
|
+
from datetime import datetime
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
# ── 时间戳解析: QoderWork ts 为 ISO-8601 字符串 (带时区) ─────────────
|
|
37
|
+
def _parse_ts(ts):
|
|
38
|
+
if ts is None:
|
|
39
|
+
return None
|
|
40
|
+
if isinstance(ts, (int, float)):
|
|
41
|
+
# epoch-ms?
|
|
42
|
+
if ts > 1e12:
|
|
43
|
+
return float(ts) / 1000.0
|
|
44
|
+
return float(ts)
|
|
45
|
+
if isinstance(ts, str):
|
|
46
|
+
s = ts.strip()
|
|
47
|
+
if not s:
|
|
48
|
+
return None
|
|
49
|
+
try:
|
|
50
|
+
return datetime.fromisoformat(s.replace("Z", "+00:00")).timestamp()
|
|
51
|
+
except ValueError:
|
|
52
|
+
try:
|
|
53
|
+
return datetime.fromisoformat(s).timestamp()
|
|
54
|
+
except ValueError:
|
|
55
|
+
return None
|
|
56
|
+
return None
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def _dur(a, b):
|
|
60
|
+
"""安全算时差(秒), 任一为空返回 None; b<a 时返回 0。"""
|
|
61
|
+
if a is None or b is None:
|
|
62
|
+
return None
|
|
63
|
+
d = b - a
|
|
64
|
+
return d if d >= 0 else 0.0
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
# ── 命令名提取 ────────────────────────────────────────────────────────
|
|
68
|
+
# QoderWork 的命令有两种出现形式:
|
|
69
|
+
# 1) 用户直接输入: /wl-search 考勤 → /wl-search
|
|
70
|
+
# 2) skill 包装文本: "wl-search" skill ... → /wl-search (补斜杠归一化)
|
|
71
|
+
_CMD_SLASH_RE = re.compile(r"/wl-[a-z][a-z-]*")
|
|
72
|
+
_CMD_QUOTED_RE = re.compile(r"[\"']wl-([a-z][a-z-]*)[\"']")
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def _extract_command(text):
|
|
76
|
+
if not text:
|
|
77
|
+
return None
|
|
78
|
+
# 优先斜杠形式
|
|
79
|
+
m = _CMD_SLASH_RE.search(text)
|
|
80
|
+
if m:
|
|
81
|
+
return m.group(0)
|
|
82
|
+
# 再认引号包裹的 "wl-xxx"
|
|
83
|
+
m = _CMD_QUOTED_RE.search(text)
|
|
84
|
+
if m:
|
|
85
|
+
return "/wl-" + m.group(1)
|
|
86
|
+
return None
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
def _short_text(text, n=60):
|
|
90
|
+
if not text:
|
|
91
|
+
return ""
|
|
92
|
+
text = text.replace("\n", " ").strip()
|
|
93
|
+
return text[:n] + ("…" if len(text) > n else "")
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
# ── 核心分析 ────────────────────────────────────────────────────────
|
|
97
|
+
def analyze(path, command_hint=None):
|
|
98
|
+
"""分析单个 transcript jsonl, 返回耗时报告 dict。
|
|
99
|
+
|
|
100
|
+
一个 jsonl 可能含多个 turn(多条命令), 按 turn 聚合返回 per_turn 列表 + 汇总。
|
|
101
|
+
|
|
102
|
+
Returns:
|
|
103
|
+
{
|
|
104
|
+
"file": "...",
|
|
105
|
+
"turns": [
|
|
106
|
+
{
|
|
107
|
+
"command": "/wl-search",
|
|
108
|
+
"prompt_preview": "用电导入...",
|
|
109
|
+
"submitted_ts": "...",
|
|
110
|
+
"duration_ms": 247859, # turn.finished 权威值
|
|
111
|
+
"duration_s": 247.86,
|
|
112
|
+
"first_reply_s": 8.18, # prompt→首个模型返回
|
|
113
|
+
"tool_total_s": 12.3,
|
|
114
|
+
"model_total_s": 100.5, # Σ 推理窗口
|
|
115
|
+
"tool_pct": 4.9,
|
|
116
|
+
"model_pct": 40.5,
|
|
117
|
+
"tool_calls": 33,
|
|
118
|
+
"by_tool": {"Bash": {"count":20,"total_s":8.1,"max_s":2.3}},
|
|
119
|
+
"num_turns": 33, # agent 内部迭代次数
|
|
120
|
+
"input_tokens": ..., "output_tokens": ...,
|
|
121
|
+
},
|
|
122
|
+
...
|
|
123
|
+
],
|
|
124
|
+
"summary": { ... 跨 turn 汇总 ... }
|
|
125
|
+
}
|
|
126
|
+
"""
|
|
127
|
+
events = []
|
|
128
|
+
with open(path, "r", encoding="utf-8") as f:
|
|
129
|
+
for line in f:
|
|
130
|
+
line = line.strip()
|
|
131
|
+
if not line:
|
|
132
|
+
continue
|
|
133
|
+
try:
|
|
134
|
+
events.append(json.loads(line))
|
|
135
|
+
except json.JSONDecodeError:
|
|
136
|
+
continue
|
|
137
|
+
|
|
138
|
+
if not events:
|
|
139
|
+
return {"file": path, "turns": [], "summary": {"error": "空文件", "events": 0}}
|
|
140
|
+
|
|
141
|
+
# ── 1. 找所有 prompt.submitted 作为每轮起点 ─────────────────────
|
|
142
|
+
prompts = [e for e in events if e.get("type") == "input.prompt.submitted"]
|
|
143
|
+
|
|
144
|
+
# ── 2. 找所有 turn.finished 作为每轮权威终点 ────────────────────
|
|
145
|
+
turn_finished = [e for e in events if e.get("type") == "turn.finished"]
|
|
146
|
+
turn_started = [e for e in events if e.get("type") == "turn.started"]
|
|
147
|
+
|
|
148
|
+
turns = []
|
|
149
|
+
# 配对: 第 i 个 prompt 对应第 i 个 turn.started/finished (时序一致)
|
|
150
|
+
n_turns = min(len(prompts), len(turn_finished))
|
|
151
|
+
|
|
152
|
+
# 预收集工具 & 模型事件, 按时间窗口归到对应 turn
|
|
153
|
+
tool_req = [e for e in events if e.get("type") == "tool.requested"]
|
|
154
|
+
tool_fin = [e for e in events if e.get("type") == "tool.execution.finished"]
|
|
155
|
+
model_req = [e for e in events if e.get("type") == "model.request.started"]
|
|
156
|
+
model_fin = [e for e in events if e.get("type") == "model.response.completed"]
|
|
157
|
+
|
|
158
|
+
for i in range(n_turns):
|
|
159
|
+
p = prompts[i]
|
|
160
|
+
p_data = p.get("data", {}) or {}
|
|
161
|
+
p_ts = _parse_ts(p.get("ts"))
|
|
162
|
+
|
|
163
|
+
tf = turn_finished[i]
|
|
164
|
+
tf_data = tf.get("data", {}) or {}
|
|
165
|
+
tf_ts = _parse_ts(tf.get("ts"))
|
|
166
|
+
|
|
167
|
+
ts_start = turn_started[i].get("ts") if i < len(turn_started) else None
|
|
168
|
+
start_ts = _parse_ts(ts_start)
|
|
169
|
+
|
|
170
|
+
# 命令名
|
|
171
|
+
cmd = command_hint or _extract_command(p_data.get("text_preview", ""))
|
|
172
|
+
|
|
173
|
+
# 本 turn 时间窗口 [p_ts, tf_ts]
|
|
174
|
+
win_lo = p_ts
|
|
175
|
+
win_hi = tf_ts
|
|
176
|
+
|
|
177
|
+
# ── 工具配对: 窗口内的 tool.requested ↔ 同 turn 的 execution.finished ──
|
|
178
|
+
# 同名连续: 用栈式配对(先进先出)
|
|
179
|
+
req_queue = {} # {tool_name: [ts,...]}
|
|
180
|
+
by_tool = {}
|
|
181
|
+
tool_calls = 0
|
|
182
|
+
tool_total = 0.0
|
|
183
|
+
|
|
184
|
+
for e in tool_req:
|
|
185
|
+
t = _parse_ts(e.get("ts"))
|
|
186
|
+
if win_lo is not None and (t is None or t < win_lo):
|
|
187
|
+
continue
|
|
188
|
+
if win_hi is not None and t is not None and t > win_hi:
|
|
189
|
+
break
|
|
190
|
+
d = e.get("data", {}) or {}
|
|
191
|
+
name = d.get("tool_name", "?")
|
|
192
|
+
req_queue.setdefault(name, []).append(t)
|
|
193
|
+
|
|
194
|
+
for e in tool_fin:
|
|
195
|
+
t = _parse_ts(e.get("ts"))
|
|
196
|
+
if win_lo is not None and (t is None or t < win_lo):
|
|
197
|
+
continue
|
|
198
|
+
if win_hi is not None and t is not None and t > win_hi:
|
|
199
|
+
break
|
|
200
|
+
d = e.get("data", {}) or {}
|
|
201
|
+
name = d.get("tool_name", "?")
|
|
202
|
+
q = req_queue.get(name)
|
|
203
|
+
if not q:
|
|
204
|
+
continue
|
|
205
|
+
req_ts = q.pop(0)
|
|
206
|
+
dur = _dur(req_ts, t)
|
|
207
|
+
if dur is None:
|
|
208
|
+
continue
|
|
209
|
+
b = by_tool.setdefault(name, {"count": 0, "total_s": 0.0, "max_s": 0.0})
|
|
210
|
+
b["count"] += 1
|
|
211
|
+
b["total_s"] += dur
|
|
212
|
+
b["max_s"] = max(b["max_s"], dur)
|
|
213
|
+
tool_calls += 1
|
|
214
|
+
tool_total += dur
|
|
215
|
+
|
|
216
|
+
# ── 模型推理窗口: request.started ↔ response.completed (按 request_index) ──
|
|
217
|
+
model_total = 0.0
|
|
218
|
+
first_reply = None
|
|
219
|
+
# 窗口内按 request_index 配对
|
|
220
|
+
req_idx_ts = {}
|
|
221
|
+
for e in model_req:
|
|
222
|
+
t = _parse_ts(e.get("ts"))
|
|
223
|
+
if win_lo is not None and (t is None or t < win_lo):
|
|
224
|
+
continue
|
|
225
|
+
if win_hi is not None and t is not None and t > win_hi:
|
|
226
|
+
break
|
|
227
|
+
d = e.get("data", {}) or {}
|
|
228
|
+
ri = d.get("request_index")
|
|
229
|
+
if ri is not None:
|
|
230
|
+
req_idx_ts[ri] = t
|
|
231
|
+
for e in model_fin:
|
|
232
|
+
t = _parse_ts(e.get("ts"))
|
|
233
|
+
if win_lo is not None and (t is None or t < win_lo):
|
|
234
|
+
continue
|
|
235
|
+
if win_hi is not None and t is not None and t > win_hi:
|
|
236
|
+
break
|
|
237
|
+
d = e.get("data", {}) or {}
|
|
238
|
+
ri = d.get("request_index")
|
|
239
|
+
if ri in req_idx_ts:
|
|
240
|
+
dur = _dur(req_idx_ts[ri], t)
|
|
241
|
+
if dur is not None:
|
|
242
|
+
model_total += dur
|
|
243
|
+
if first_reply is None:
|
|
244
|
+
first_reply = dur # 首个模型返回 = 首次回复
|
|
245
|
+
|
|
246
|
+
# 若没有 model 窗口数据, 回退: prompt→首个 tool.requested
|
|
247
|
+
if first_reply is None and tool_req:
|
|
248
|
+
for e in tool_req:
|
|
249
|
+
t = _parse_ts(e.get("ts"))
|
|
250
|
+
if win_lo is not None and (t is None or t < win_lo):
|
|
251
|
+
continue
|
|
252
|
+
if win_hi is not None and t is not None and t > win_hi:
|
|
253
|
+
break
|
|
254
|
+
first_reply = _dur(p_ts, t)
|
|
255
|
+
break
|
|
256
|
+
|
|
257
|
+
# 权威总耗时
|
|
258
|
+
dur_ms = tf_data.get("duration_ms")
|
|
259
|
+
dur_s = dur_ms / 1000.0 if dur_ms is not None else _dur(p_ts, tf_ts)
|
|
260
|
+
|
|
261
|
+
# AI 排队/其它 = 总 - 工具 - 模型推理
|
|
262
|
+
other_s = None
|
|
263
|
+
if dur_s is not None:
|
|
264
|
+
other_s = dur_s - tool_total - model_total
|
|
265
|
+
if other_s < 0:
|
|
266
|
+
other_s = 0.0
|
|
267
|
+
|
|
268
|
+
by_tool_clean = {
|
|
269
|
+
name: {"count": b["count"], "total_s": round(b["total_s"], 2), "max_s": round(b["max_s"], 2)}
|
|
270
|
+
for name, b in sorted(by_tool.items(), key=lambda x: -x[1]["total_s"])
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
turns.append({
|
|
274
|
+
"command": cmd or "(未知)",
|
|
275
|
+
"prompt_preview": _short_text(p_data.get("text_preview", "")),
|
|
276
|
+
"submitted_ts": p.get("ts"),
|
|
277
|
+
"finished_ts": tf.get("ts"),
|
|
278
|
+
"duration_ms": dur_ms,
|
|
279
|
+
"duration_s": round(dur_s, 2) if dur_s is not None else None,
|
|
280
|
+
"first_reply_s": round(first_reply, 2) if first_reply is not None else None,
|
|
281
|
+
"tool_total_s": round(tool_total, 2),
|
|
282
|
+
"model_total_s": round(model_total, 2),
|
|
283
|
+
"other_s": round(other_s, 2) if other_s is not None else None,
|
|
284
|
+
"tool_pct": round(tool_total / dur_s * 100, 1) if dur_s else None,
|
|
285
|
+
"model_pct": round(model_total / dur_s * 100, 1) if dur_s else None,
|
|
286
|
+
"tool_calls": tool_calls,
|
|
287
|
+
"by_tool": by_tool_clean,
|
|
288
|
+
"num_turns": tf_data.get("num_turns"),
|
|
289
|
+
"reason": tf_data.get("reason"),
|
|
290
|
+
"input_tokens": tf_data.get("input_tokens"),
|
|
291
|
+
"output_tokens": tf_data.get("output_tokens"),
|
|
292
|
+
"model": (turn_started[i].get("data", {}) or {}).get("model") if i < len(turn_started) else None,
|
|
293
|
+
})
|
|
294
|
+
|
|
295
|
+
# ── 汇总 ────────────────────────────────────────────────────────
|
|
296
|
+
summary = {
|
|
297
|
+
"events": len(events),
|
|
298
|
+
"turns": len(turns),
|
|
299
|
+
"total_duration_s": round(sum(t["duration_s"] for t in turns if t["duration_s"]), 2),
|
|
300
|
+
"total_tool_s": round(sum(t["tool_total_s"] for t in turns), 2),
|
|
301
|
+
"total_model_s": round(sum(t["model_total_s"] for t in turns), 2),
|
|
302
|
+
"avg_turn_s": None,
|
|
303
|
+
"slowest_turn": None,
|
|
304
|
+
}
|
|
305
|
+
if turns:
|
|
306
|
+
ds = [t["duration_s"] for t in turns if t["duration_s"]]
|
|
307
|
+
if ds:
|
|
308
|
+
summary["avg_turn_s"] = round(sum(ds) / len(ds), 2)
|
|
309
|
+
slow = max(turns, key=lambda t: t["duration_s"] or 0)
|
|
310
|
+
summary["slowest_turn"] = {
|
|
311
|
+
"command": slow["command"],
|
|
312
|
+
"duration_s": slow["duration_s"],
|
|
313
|
+
"prompt_preview": slow["prompt_preview"],
|
|
314
|
+
}
|
|
315
|
+
# 跨 turn 工具汇总
|
|
316
|
+
agg_tool = {}
|
|
317
|
+
for t in turns:
|
|
318
|
+
for name, b in t["by_tool"].items():
|
|
319
|
+
a = agg_tool.setdefault(name, {"count": 0, "total_s": 0.0, "max_s": 0.0})
|
|
320
|
+
a["count"] += b["count"]
|
|
321
|
+
a["total_s"] += b["total_s"]
|
|
322
|
+
a["max_s"] = max(a["max_s"], b["max_s"])
|
|
323
|
+
summary["by_tool"] = {
|
|
324
|
+
name: {**b, "total_s": round(b["total_s"], 2), "max_s": round(b["max_s"], 2)}
|
|
325
|
+
for name, b in sorted(agg_tool.items(), key=lambda x: -x[1]["total_s"])
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
return {"file": os.path.basename(path), "turns": turns, "summary": summary}
|
|
329
|
+
|
|
330
|
+
|
|
331
|
+
# ── 人类可读报告 ──────────────────────────────────────────────────────
|
|
332
|
+
def _format_report(r):
|
|
333
|
+
lines = []
|
|
334
|
+
s = r["summary"]
|
|
335
|
+
if "error" in s:
|
|
336
|
+
return f"[错误] {s['error']}"
|
|
337
|
+
lines.append(f"文件: {r['file']}")
|
|
338
|
+
lines.append(f"事件数: {s['events']} | turn数: {s['turns']}")
|
|
339
|
+
lines.append(f"总耗时: {s['total_duration_s']}s | 平均/turn: {s.get('avg_turn_s')}s")
|
|
340
|
+
if s.get("slowest_turn"):
|
|
341
|
+
st = s["slowest_turn"]
|
|
342
|
+
lines.append(f"最慢turn: {st['command']} {st['duration_s']}s — {st['prompt_preview']}")
|
|
343
|
+
lines.append(f"工具耗时合计: {s['total_tool_s']}s | 模型推理合计: {s['total_model_s']}s")
|
|
344
|
+
if s.get("by_tool"):
|
|
345
|
+
lines.append("")
|
|
346
|
+
lines.append("工具分桶(全部turn):")
|
|
347
|
+
for name, b in list(s["by_tool"].items())[:12]:
|
|
348
|
+
lines.append(f" {name:25s} ×{b['count']:3d} 共{b['total_s']:7.1f}s 最慢{b['max_s']:6.1f}s")
|
|
349
|
+
lines.append("")
|
|
350
|
+
lines.append("各 turn 明细:")
|
|
351
|
+
for t in r["turns"]:
|
|
352
|
+
lines.append(f" [{t['command']:14s}] {t['duration_s']:7.1f}s "
|
|
353
|
+
f"(首回{t['first_reply_s']}s, 工具{t['tool_total_s']}s/{t.get('tool_pct')}%, "
|
|
354
|
+
f"模型{t['model_total_s']}s, {t['tool_calls']}次调用, {t['num_turns']}轮迭代)")
|
|
355
|
+
lines.append(f" └ {t['prompt_preview']}")
|
|
356
|
+
return "\n".join(lines)
|
|
357
|
+
|
|
358
|
+
|
|
359
|
+
def main():
|
|
360
|
+
if len(sys.argv) < 2:
|
|
361
|
+
print("用法: python transcript_timing.py <jsonl|dir> [--json]")
|
|
362
|
+
sys.exit(1)
|
|
363
|
+
target = sys.argv[1]
|
|
364
|
+
as_json = "--json" in sys.argv
|
|
365
|
+
|
|
366
|
+
if os.path.isdir(target):
|
|
367
|
+
files = sorted(glob.glob(os.path.join(target, "**", "*.jsonl"), recursive=True))
|
|
368
|
+
results = [analyze(f) for f in files]
|
|
369
|
+
if as_json:
|
|
370
|
+
print(json.dumps(results, ensure_ascii=False, indent=2))
|
|
371
|
+
else:
|
|
372
|
+
for r in results:
|
|
373
|
+
if r["summary"].get("turns"):
|
|
374
|
+
print(_format_report(r))
|
|
375
|
+
print("\n" + "=" * 70 + "\n")
|
|
376
|
+
return
|
|
377
|
+
|
|
378
|
+
r = analyze(target)
|
|
379
|
+
if as_json:
|
|
380
|
+
print(json.dumps(r, ensure_ascii=False, indent=2))
|
|
381
|
+
else:
|
|
382
|
+
print(_format_report(r))
|
|
383
|
+
|
|
384
|
+
|
|
385
|
+
if __name__ == "__main__":
|
|
386
|
+
main()
|
|
File without changes
|