@hupan56/wlkj 2.7.11 → 3.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (152) hide show
  1. package/bin/cli.js +375 -78
  2. package/package.json +29 -29
  3. package/templates/.qoder/.runtime/ctx-cache-5660152f1d6dd819.md +23 -0
  4. package/templates/.qoder/.runtime/ctx-cache-afdce0dac06b25b0.md +23 -0
  5. package/templates/.qoder/.runtime/search-cache-eae7644e7b122f35.txt +1 -0
  6. package/templates/.qoder/learning/eval-history.jsonl +28 -0
  7. package/templates/data/index/wiki-index.json +8 -0
  8. package/templates/qoder/agents/insight-planning.md +1 -1
  9. package/templates/qoder/agents/insight-research.md +28 -15
  10. package/templates/qoder/commands/optional/wl-insight.md +159 -161
  11. package/templates/qoder/commands/optional/wl-report.md +4 -4
  12. package/templates/qoder/commands/optional/wl-status.md +2 -2
  13. package/templates/qoder/commands/wl-code.md +2 -2
  14. package/templates/qoder/commands/wl-design.md +2 -2
  15. package/templates/qoder/commands/wl-init.md +3 -3
  16. package/templates/qoder/commands/wl-prd.md +2 -2
  17. package/templates/qoder/commands/wl-req.md +43 -0
  18. package/templates/qoder/commands/wl-search.md +8 -8
  19. package/templates/qoder/commands/wl-task.md +17 -17
  20. package/templates/qoder/commands/wl-test.md +41 -15
  21. package/templates/qoder/config.yaml +17 -1
  22. package/templates/qoder/hooks/session-start.py +12 -22
  23. package/templates/qoder/nul +4 -0
  24. package/templates/qoder/rules/wl-pipeline.md +22 -48
  25. package/templates/qoder/scripts/README.md +139 -0
  26. package/templates/qoder/scripts/common/autotest_auth.py +109 -0
  27. package/templates/qoder/scripts/common/bootstrap.py +145 -0
  28. package/templates/qoder/scripts/common/check_publish.py +98 -0
  29. package/templates/qoder/scripts/common/cmd_registry.py +112 -0
  30. package/templates/qoder/scripts/common/config.py +187 -0
  31. package/templates/qoder/scripts/common/contract.py +317 -0
  32. package/templates/qoder/scripts/common/developer.py +2 -1
  33. package/templates/qoder/scripts/common/feishu.py +10 -9
  34. package/templates/qoder/scripts/common/guard.py +159 -0
  35. package/templates/qoder/scripts/common/identity.py +121 -2
  36. package/templates/qoder/scripts/common/kg_capabilities.py +182 -0
  37. package/templates/qoder/scripts/common/mcp_base.py +268 -0
  38. package/templates/qoder/scripts/common/paths.py +187 -1
  39. package/templates/qoder/scripts/common/result.py +223 -0
  40. package/templates/qoder/scripts/common/roles.py +60 -0
  41. package/templates/qoder/scripts/common/task_utils.py +21 -9
  42. package/templates/qoder/scripts/common/test_extract.py +115 -0
  43. package/templates/qoder/scripts/kg/__init__.py +11 -0
  44. package/templates/qoder/scripts/kg/build_entity_registry.py +196 -0
  45. package/templates/qoder/scripts/kg/build_relations.py +127 -0
  46. package/templates/qoder/scripts/{build_style_index.py → kg/build_style_index.py} +39 -10
  47. package/templates/qoder/scripts/kg/build_workflows.py +144 -0
  48. package/templates/qoder/scripts/{context_pack.py → kg/context_pack.py} +18 -11
  49. package/templates/qoder/scripts/{enrich_prompt.py → kg/enrich_prompt.py} +232 -226
  50. package/templates/qoder/scripts/{extract_api_params.py → kg/extract.py} +398 -246
  51. package/templates/qoder/scripts/{kg.py → kg/kg.py} +638 -708
  52. package/templates/qoder/scripts/{kg_build.py → kg/kg_build.py} +618 -612
  53. package/templates/qoder/scripts/{kg_build_db.py → kg/kg_build_db.py} +333 -327
  54. package/templates/qoder/scripts/{kg_duckdb.py → kg/kg_duckdb.py} +38 -37
  55. package/templates/qoder/scripts/{kg_incremental.py → kg/kg_incremental.py} +420 -393
  56. package/templates/qoder/scripts/{kg_link_db.py → kg/kg_link_db.py} +230 -224
  57. package/templates/qoder/scripts/{kg_semantic.py → kg/kg_semantic.py} +156 -150
  58. package/templates/qoder/scripts/kg/prefetch.py +359 -0
  59. package/templates/qoder/scripts/{search_index.py → kg/search_index.py} +70 -14
  60. package/templates/qoder/scripts/mcp/__init__.py +11 -0
  61. package/templates/qoder/scripts/{kg_mcp_server.py → mcp/kg_mcp_server.py} +77 -272
  62. package/templates/qoder/scripts/{lanhu_stdio_wrapper.py → mcp/lanhu_stdio_wrapper.py} +125 -119
  63. package/templates/qoder/scripts/{check_mcp.py → mcp/mcp_doctor.py} +515 -298
  64. package/templates/qoder/scripts/{mcp_launcher.py → mcp/mcp_launcher.py} +442 -414
  65. package/templates/qoder/scripts/{mysql_mcp_server.py → mcp/mysql_mcp_server.py} +347 -396
  66. package/templates/qoder/scripts/{zentao_mcp_server.py → mcp/zentao_mcp_server.py} +384 -424
  67. package/templates/qoder/scripts/report/__init__.py +11 -0
  68. package/templates/qoder/scripts/{add_session.py → report/add_session.py} +250 -244
  69. package/templates/qoder/scripts/{archive_prd.py → report/archive_prd.py} +383 -377
  70. package/templates/qoder/scripts/{eval_prd.py → report/eval_prd.py} +73 -11
  71. package/templates/qoder/scripts/{export.py → report/export.py} +63 -0
  72. package/templates/qoder/scripts/{fill_prototype.py → report/fill_prototype.py} +6 -0
  73. package/templates/qoder/scripts/{gen_design_doc.py → report/gen_design_doc.py} +400 -394
  74. package/templates/qoder/scripts/{learn.py → report/learn.py} +152 -146
  75. package/templates/qoder/scripts/{learn_aggregate.py → report/learn_aggregate.py} +207 -201
  76. package/templates/qoder/scripts/{report.py → report/report.py} +287 -281
  77. package/templates/qoder/scripts/report/req.py +222 -0
  78. package/templates/qoder/scripts/report/role.py +33 -0
  79. package/templates/qoder/scripts/{status.py → report/status.py} +634 -628
  80. package/templates/qoder/scripts/setup/__init__.py +11 -0
  81. package/templates/qoder/scripts/setup/carriers.py +662 -0
  82. package/templates/qoder/scripts/{init_doctor.py → setup/init_doctor.py} +63 -26
  83. package/templates/qoder/scripts/{install_qoderwork.py → setup/install_qoderwork.py} +36 -26
  84. package/templates/qoder/scripts/{platform_doctor.py → setup/platform_doctor.py} +265 -259
  85. package/templates/qoder/scripts/{repo_root.py → setup/repo_root.py} +112 -106
  86. package/templates/qoder/scripts/{setup.py → setup/setup.py} +147 -0
  87. package/templates/qoder/scripts/{setup_lanhu.py → setup/setup_lanhu.py} +973 -963
  88. package/templates/qoder/scripts/task/__init__.py +11 -0
  89. package/templates/qoder/scripts/{git_sync.py → task/git_sync.py} +52 -31
  90. package/templates/qoder/scripts/{syncgate.py → task/syncgate.py} +6 -0
  91. package/templates/qoder/scripts/task/task.py +221 -0
  92. package/templates/qoder/scripts/task/task_lifecycle.py +596 -0
  93. package/templates/qoder/scripts/task/task_query.py +161 -0
  94. package/templates/qoder/scripts/task/task_relations.py +424 -0
  95. package/templates/qoder/scripts/{team_sync.py → task/team_sync.py} +93 -20
  96. package/templates/qoder/scripts/test/__init__.py +11 -0
  97. package/templates/qoder/scripts/{autotest.py → test/autotest.py} +1174 -1751
  98. package/templates/qoder/scripts/{autotest_batch.py → test/autotest_batch.py} +242 -224
  99. package/templates/qoder/scripts/test/autotest_data.py +675 -0
  100. package/templates/qoder/scripts/{autotest_run.py → test/autotest_run.py} +309 -297
  101. package/templates/qoder/scripts/{benchmark.py → test/benchmark.py} +6 -0
  102. package/templates/qoder/scripts/{kg_auto_login.py → test/kg_auto_login.py} +202 -196
  103. package/templates/qoder/scripts/{kg_test_runner.py → test/kg_test_runner.py} +7 -1
  104. package/templates/qoder/scripts/{page_probe.py → test/page_probe.py} +465 -459
  105. package/templates/qoder/scripts/wlkj.py +116 -0
  106. package/templates/qoder/settings.json +1 -10
  107. package/templates/qoder/skills/design-import/SKILL.md +226 -226
  108. package/templates/qoder/skills/design-review/SKILL.md +82 -82
  109. package/templates/qoder/skills/prd-generator/SKILL.md +26 -16
  110. package/templates/qoder/skills/prd-review/SKILL.md +5 -5
  111. package/templates/qoder/skills/prototype-generator/SKILL.md +256 -256
  112. package/templates/qoder/skills/spec-coder/SKILL.md +4 -4
  113. package/templates/qoder/skills/spec-generator/SKILL.md +4 -4
  114. package/templates/qoder/skills/test-generator/SKILL.md +5 -5
  115. package/templates/qoder/skills/wl-code/SKILL.md +4 -4
  116. package/templates/qoder/skills/wl-commit/SKILL.md +4 -4
  117. package/templates/qoder/skills/wl-design/SKILL.md +3 -3
  118. package/templates/qoder/skills/wl-init/SKILL.md +8 -8
  119. package/templates/qoder/skills/wl-insight/SKILL.md +5 -5
  120. package/templates/qoder/skills/wl-prd-full/SKILL.md +6 -6
  121. package/templates/qoder/skills/wl-prd-quick/SKILL.md +6 -6
  122. package/templates/qoder/skills/wl-prd-review/SKILL.md +4 -4
  123. package/templates/qoder/skills/wl-report/SKILL.md +7 -7
  124. package/templates/qoder/skills/wl-search/SKILL.md +13 -13
  125. package/templates/qoder/skills/wl-spec/SKILL.md +5 -5
  126. package/templates/qoder/skills/wl-status/SKILL.md +5 -5
  127. package/templates/qoder/skills/wl-task/SKILL.md +6 -6
  128. package/templates/qoder/skills/wl-test/SKILL.md +102 -39
  129. package/templates/root/AGENTS.md +39 -40
  130. package/templates/qoder/hooks/inject-workflow-state.py +0 -169
  131. package/templates/qoder/scripts/__pycache__/check_mcp_launch.cpython-39.pyc +0 -0
  132. package/templates/qoder/scripts/__pycache__/install_qoderwork.cpython-39.pyc +0 -0
  133. package/templates/qoder/scripts/__pycache__/mcp_launcher.cpython-39.pyc +0 -0
  134. package/templates/qoder/scripts/__pycache__/platform_doctor.cpython-39.pyc +0 -0
  135. package/templates/qoder/scripts/check_carriers.py +0 -238
  136. package/templates/qoder/scripts/check_mcp_launch.py +0 -183
  137. package/templates/qoder/scripts/check_qoderwork_consistency.py +0 -166
  138. package/templates/qoder/scripts/collect_prds.py +0 -31
  139. package/templates/qoder/scripts/common/mentions.py +0 -134
  140. package/templates/qoder/scripts/common/utf8.py +0 -38
  141. package/templates/qoder/scripts/extract_routes.py +0 -54
  142. package/templates/qoder/scripts/extract_routes_tree.py +0 -78
  143. package/templates/qoder/scripts/handoff.py +0 -22
  144. package/templates/qoder/scripts/init_developer.py +0 -76
  145. package/templates/qoder/scripts/parse_prds.py +0 -33
  146. package/templates/qoder/scripts/role.py +0 -51
  147. package/templates/qoder/scripts/sync_carriers.py +0 -259
  148. package/templates/qoder/scripts/task.py +0 -1261
  149. package/templates/qoder/scripts/workspace_init.py +0 -102
  150. package/templates/qoder/skills/prompt-enrich/SKILL.md +0 -90
  151. package/templates/qoder/skills/prototype-generator/SKILL.md.zcode-79180-2af4721f-f9a6-412c-88db-c0af680d211b.tmp +0 -0
  152. /package/templates/qoder/scripts/{secure-ls.js → test/secure-ls.js} +0 -0
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  name: wl-test
3
3
  description: "测试工作流: 意图式用例 + Playwright MCP 执行 + 锚点缓存。触发: /wl-test 命令; 或自然语言里带'功能验证点'(如'测一下XX支不支持YY''验一下XX能不能ZZ''XX对不对')。纯随手看('打开XX页''看看登录')→不走本工作流。需确认(DANGEROUS)。"
4
- trigger: "/wl-test 命令; 或自然语言带具体功能验证点: '测一下XX支不支持YY''验一下XX能不能''XX功能对不对''回归XX'。仅'打开/看看/瞄一眼'等无验证点的不触发。"
4
+ trigger: "user invokes /wl-* command explicitly"
5
5
  ---
6
6
 
7
7
  # wl-test — 测试工作流(/wl-test 命令 或 带功能验证点的自然语言触发)
@@ -32,9 +32,9 @@ trigger: "/wl-test 命令; 或自然语言带具体功能验证点: '测一下XX
32
32
 
33
33
  **① 必须用意图式格式(intent+anchor),禁止旧 action+selector 格式。**
34
34
  ```json
35
- // ✅ 对: 意图式, anchor 来自 recall
35
+ // ✅ 对: 意图式, anchor 来自 recall 或 snapshot
36
36
  {"intent":"fill","desc":"账号输入框","value":"{{ask:登录账号}}","anchor":{"role":"textbox","name":"尊敬的管理员"}}
37
- // ❌ 错: 旧 action+CSS selector (脆弱, recall 锚点白沉淀)
37
+ // ❌ 错: 旧 action+CSS selector (脆弱, 脆弱, recall 锚点白沉淀)
38
38
  {"action":"form_input","target":"input[placeholder*='账号']","value":"{{ask:测试账号}}"}
39
39
  ```
40
40
 
@@ -46,20 +46,48 @@ trigger: "/wl-test 命令; 或自然语言带具体功能验证点: '测一下XX
46
46
  **③ 用例 JSON 必须用 `--cases-file` 传文件,禁止内联 `--cases`。**
47
47
  ```bash
48
48
  # ✅ 对: 写文件再传路径(不截断)
49
- python "$R/.qoder/scripts/autotest.py" quick --cases-file <文件路径> --env test --platform web
49
+ python "$R/.qoder/scripts/test/autotest.py" quick --cases-file <文件路径> --env test --platform web
50
50
  # ❌ 错: 内联(长JSON截断, 多条用例必丢)
51
- python "$R/.qoder/scripts/autotest.py" quick --cases '[{"case_id"...(超长)...}]'
51
+ python "$R/.qoder/scripts/test/autotest.py" quick --cases '[{"case_id"...(超长)...}]'
52
52
  ```
53
53
 
54
- **④ 执行前必须 recall 查锚点。有锚点 直接填进 anchor,不调 LLM 匹配。**
54
+ **④ 无锚点时必须先 snapshot 拿真实 anchor 再填,禁止预猜 CSS selector。**
55
55
  ```bash
56
- python "$R/.qoder/scripts/autotest.py" recall --url "/auth/login"
56
+ # recall 空时 → 不预猜! 先 navigate + snapshot 拿到真实 a11y 树
57
+ # 再把 snapshot 里看到的 role+name 填进 step.anchor
57
58
  ```
58
- recall 返回的每个锚点,直接用它的 role+name 填进 step.anchor。这步是**省模型调用**的根本。
59
+ - recall 命中 直接用 recall 的 anchor(确定性命中,零 LLM)
60
+ - recall 空(首次)→ **navigate + browser_snapshot → 从 a11y 树提取 anchor → 填进用例**
61
+ - **绝不**在没看页面的情况下凭"登录页一般有账号框"去猜 selector
59
62
 
60
- **⑤ 验证码必须用 Playwright MCP 的 `browser_take_screenshot` 截图 → QoderWork 自带视觉识别 → `browser_type` 填入 → 删图。禁止用 Computer 截图(实测反复失败超时)、禁止瞎蒙默认值。**
61
- - `{{ask:}}` 占位符里**绝不出现验证码**——验证码不存个人文档,执行时实时识别。
62
- - 截图工具不可用时 用 JS 提取 base64 → 存图 → Read 看 → 算答案 → 填入 → **删图**。
63
+ **⑤ URL 必须写成 `{{base_url}}/auth/login`(带斜杠)。**
64
+ - base_url 已去尾斜杠(脚本行为),写 `{{base_url}}auth/login` 会拼成 `https://xxxauth/login` 坏地址。
65
+ - 脚本会检测并告警,但 AI 应一次写对。
66
+
67
+ **⑥ recall 的 `--url` 必须用相对路径(无前导斜杠),避开 Git-bash/MSYS 路径转换坑。**
68
+ ```bash
69
+ # ✅ 对: 相对路径 (MSYS 不转换)
70
+ python "$R/.qoder/scripts/test/autotest.py" recall --url auth/login
71
+ # ❌ 错: 前导斜杠会被 Windows Git-bash 转成 /D:/.../Git/auth/login
72
+ python "$R/.qoder/scripts/test/autotest.py" recall --url /auth/login
73
+ ```
74
+ (脚本已加 normalize 兜底能修这个,但别依赖兜底——直接写相对路径最干净。)
75
+
76
+ **⑦ Playwright 工具一次性批量加载,snapshot 结果直接用内存 a11y 树,禁止写文件再 Read。**
77
+ ```bash
78
+ # ✅ 对: 一个回合里并发加载所有要用的 browser_* 工具描述
79
+ # ✅ 对: browser_snapshot 返回的 a11y 树直接在对话里读, 不落地文件
80
+ # ❌ 错: 用一个加载一个(调用数翻倍); snapshot 写 yml 文件 → 再 Read 读回来(每步2次调用)
81
+ ```
82
+
83
+ **⑧ 验证码识别一气呵成(截图→Read→type→删图),不绕路、不预先纠结。**
84
+ - 登录用例里**不写**验证码 step——点登录后若弹验证码,执行循环自动走 5 步。
85
+ - 固定 5 步连发:`browser_snapshot` 找验证码图 → `browser_take_screenshot` 存图 → Read 看图算 → `browser_type` 填 → 删图。
86
+ - 验证码有时效(1-2分钟),所以**填完立刻点登录**,别在中间插别的操作。
87
+ - 自识别连失 2 次才交人。
88
+
89
+ **⑨ 用例必须一次生成全部(正向+异常),禁止一条一条生成。**
90
+ - 每生成一条 = 一次模型调用。一次写完 Q-1(正向) + Q-2(异常) + ... 全部。
63
91
 
64
92
  ## ⚙️ 自取上下文(Quest / QoderWork 无 hook 注入,必须自读)
65
93
 
@@ -70,8 +98,8 @@ recall 返回的每个锚点,直接用它的 role+name 填进 step.anchor。
70
98
  > R=$(python ~/.qoderwork/repo_root.py 2>/dev/null) || R=.
71
99
  > ```
72
100
  > `repo_root.py` 从 `~/.qoderwork/mcp.json` 反推仓库根;失败回退 `.`(IDE/CLI 工作目录即仓库根)。
73
- > **读到 1 次记住,后续所有脚本统一用 `python "$R/.qoder/scripts/xxx.py"`,不要再搜!**
74
- > 如果 `repo_root.py` 报错找不到,先在仓库里跑 `python "$R/.qoder/scripts/install_qoderwork.py"`。
101
+ > **读到 1 次记住,后续所有脚本统一用 `python "$R/.qoder/scripts/<子包>/<脚本>.py"`(子包: kg/build/test/setup/io),不要再搜!**
102
+ > 如果 `repo_root.py` 报错找不到,先在仓库里跑 `python "$R/.qoder/scripts/setup/install_qoderwork.py"`。
75
103
 
76
104
  - `<R>/.qoder/.developer` — 当前开发者
77
105
  - `<R>/.qoder/config.yaml` 的 `autotest:` 段 — 环境/域名
@@ -84,7 +112,7 @@ recall 返回的每个锚点,直接用它的 role+name 填进 step.anchor。
84
112
 
85
113
  **唯一正确做法**——1 条命令查 MySQL sys_menu 真实路由:
86
114
  ```bash
87
- python "$R/.qoder/scripts/search_index.py" --routes 保险
115
+ python "$R/.qoder/scripts/kg/search_index.py" --routes 保险
88
116
  # → 保险管理 -> /veh/vehicle/vehAffair/insurance
89
117
  ```
90
118
  - 数据来自 MySQL `sys_menu` 表(后台动态菜单,**唯一事实源**)
@@ -131,12 +159,12 @@ coverage_matrix()
131
159
 
132
160
  **不要任务/PRD。零搜索。** 用意图+锚点格式(A方案),observe-act-extract 执行。
133
161
 
134
- ### Step 0:先 recall 查有没有现成锚点(跳过 LLM)
162
+ ### Step 0recall 查锚点(用相对 URL,避开 MSYS 坑)
135
163
  ```bash
136
- python "$R/.qoder/scripts/autotest.py" recall --url "/auth/login"
164
+ python "$R/.qoder/scripts/test/autotest.py" recall --url auth/login
137
165
  ```
138
166
  - **有锚点** → 生成用例时直接把 anchor 填进 step(确定性命中,快、零 LLM)
139
- - **无锚点**用意图式生成(首次会调 LLM 匹配,执行后 learn 沉淀,下次就快)
167
+ - **无锚点(首次)****先 navigate + snapshot 拿真实锚点,再生成用例**(见 Step 0.6)
140
168
 
141
169
  ### Step 0.5:用知识图谱 MCP 增强用例(1 次调用取全)
142
170
 
@@ -146,15 +174,42 @@ python "$R/.qoder/scripts/autotest.py" recall --url "/auth/login"
146
174
 
147
175
  > 图谱无数据 → 跳过,按通用模式生成。**不要再单独调 feature_overview**(信息重叠,浪费一轮)。
148
176
 
177
+ ### Step 0.6:recall 空时,先 snapshot 拿真实锚点(🔴 首次跑关键,禁止预猜 selector)
178
+
179
+ **recall 返回空 = 第一次测这页。此时绝不能凭"登录页一般有账号框"去猜 CSS selector。**
180
+ 正确做法:先用 Playwright 打开页面拿一次 a11y 树,从里面提取真实 anchor:
181
+
182
+ ```bash
183
+ # 1. 批量加载要用的 browser_* 工具描述 (一次并发, 别一个一个加载)
184
+ # 最少: browser_navigate, browser_snapshot, browser_click, browser_type,
185
+ # browser_take_screenshot, browser_evaluate
186
+
187
+ # 2. browser_navigate 到 {{base_url}}/auth/login (base_url 从 config 取, 已去尾斜杠)
188
+ # 3. browser_snapshot 一次 → 拿到 a11y 树 (直接读对话里的内容, 不写文件)
189
+ ```
190
+ snapshot 返回形如:
191
+ ```
192
+ - textbox "尊敬的管理员,请输入您的账号" [ref=e36]
193
+ - textbox "请输入您的密码" [ref=e42]
194
+ - textbox "验证码" [ref=e56]
195
+ - button "login" [ref=e64]: 登录
196
+ ```
197
+ **把这里的 role+name 直接填进用例的 anchor**:
198
+ ```json
199
+ {"intent":"fill","desc":"账号输入框","value":"{{ask:登录账号}}","anchor":{"role":"textbox","name":"尊敬的管理员,请输入您的账号"}}
200
+ ```
201
+ > 这一步把"首次跑要靠 LLM 兜底匹配"变成"确定性命中",是省调用的根本。
202
+ > 拿到锚点后继续 Step 1。
203
+
149
204
  **🎯 知道页面名/URL 时,AI 先查按钮+接口画像(最精准,1 条命令拿到所有交互点):**
150
205
  ```bash
151
- python "$R/.qoder/scripts/autotest.py" page --kw 保险 # 按业务名查
152
- python "$R/.qoder/scripts/autotest.py" page --url /veh/insurance # 按URL查
206
+ python "$R/.qoder/scripts/test/autotest.py" page --kw 保险 # 按业务名查
207
+ python "$R/.qoder/scripts/test/autotest.py" page --url /veh/insurance # 按URL查
153
208
  ```
154
209
  (这是 AI 在 `/wl-test` 流程内部自动调用的工具,**用户只用 `/wl-test`,不碰此命令**。)
155
210
  输出每个按钮的 handler + 调用接口 + HTTP 方法 + 置信度,**直接对应到用例的 click→assert 断言点**。开发 `finish` 任务时也会自动生成 `test-handoff.md`,内容同此画像——交接场景直接读那份即可,不必重查。
156
211
 
157
- ### Step 1:AI 据用户描述 + 图谱信息生成意图式用例 JSON(**一次生成全部用例**)
212
+ ### Step 1:AI 据用户描述 + 锚点生成意图式用例 JSON(**一次生成全部用例**)
158
213
  **意图+锚点格式**(A方案核心):用 `intent` 表达"做什么",用 `anchor` 精确定位(有锚点则确定性命中,无则 LLM 兜底)。
159
214
  ```json
160
215
  [{"case_id":"Q-1","title":"正确账号密码登录","platform":"web","env":"test",
@@ -181,7 +236,7 @@ python "$R/.qoder/scripts/autotest.py" page --url /veh/insurance # 按URL查
181
236
  # AI 先写文件:
182
237
  # 写 workspace/members/{dev}/drafts/_autotest-cases.json
183
238
  # 再调:
184
- python "$R/.qoder/scripts/autotest.py" quick \
239
+ python "$R/.qoder/scripts/test/autotest.py" quick \
185
240
  --cases-file workspace/members/{dev}/drafts/_autotest-cases.json \
186
241
  --desc "<用户描述>" --platform <web|app>
187
242
  ```
@@ -220,32 +275,40 @@ python "$R/.qoder/scripts/autotest.py" quick \
220
275
  > ⚠️ **导航是 `browser_navigate`(不是 BrowserNavigate / navigate)**。
221
276
  > 调用前对照上面清单,名字写错 = 工具不存在 = 整个测试失败。
222
277
 
278
+ **🔴 调用效率(实测:一次登录测试不该超过 12 步调用):**
279
+ 1. **工具批量加载**:进入执行阶段时,在一个回合里并发加载所有要用的 `browser_*` 工具描述(navigate/snapshot/click/type/take_screenshot/evaluate),不要用一个加载一个。
280
+ 2. **snapshot 不落地**:`browser_snapshot` 返回的 a11y 树**直接在对话里读**,禁止写 yml 文件再 Read 回来(每步省一次调用)。
281
+ 3. **有 anchor 连发不思考**:锚点已确定,`browser_type`/`browser_click` 直接按 role+name 操作,零模型调用。
282
+
223
283
  **对每个 step(用 Playwright MCP 工具,连着发,别每步深度思考):**
224
284
  1. **intent=goto** → `browser_navigate`(1 次)
225
- 2. **首次到新页面** → `browser_snapshot` **1 次**拿 a11y
285
+ 2. **首次到新页面** → `browser_snapshot` **1 次**拿 a11y 树(直接读,不写文件)
226
286
  3. **intent=click/fill**(锚点优先,**确定性匹配不思考**):
227
287
  - **有 anchor** → snapshot 结果里按 `role+name`/`placeholder` 直接匹配 → 拿到元素 → `browser_type`/`browser_click`(**零模型调用,连着发**)
228
288
  - **无 anchor** → 用 `desc` 语义匹配(1 次模型调用)
229
289
  - 匹配失败 → self-heal:重新 `browser_snapshot` + 重试,最多 2 次
230
- 4. **intent=extract** → 看 URL / read_page 判断(1 次模型调用)
290
+ 4. **intent=extract** → 看 URL / 当前页面判断(1 次模型调用)
231
291
  5. **intent=ask_human** → 暂停等人
232
292
  6. **intent=assert** → 确定性校验(零模型调用)
233
293
 
234
- ### Step 3.5:验证码识别一条龙(数学题)
294
+ ### Step 3.5:验证码识别一条龙(数学题)— 一气呵成,别绕路
235
295
 
236
296
  > **🔴 验证码铁律(实测反复踩坑):**
237
297
  > - **禁止用 Computer 截图 / zoom 截验证码**——实测反复失败、超时、浪费 5-6 步。
238
298
  > - **禁止填默认值/瞎蒙**(如 000000)。
239
299
  > - **唯一正确路径**:JS 提取 base64 → 存图 → Read 看 → 算答案 → 填 → 删图。固定 5 步,不偏离。
300
+ > - **验证码有时效(1-2 分钟),所以填完立刻点登录,中间别插别的操作。**
240
301
 
241
- 按这个**固定顺序**一次走完(用 Playwright MCP 工具,约 30 秒):
242
- 1. **`browser_snapshot`** 看页面元素,找到验证码图片元素
302
+ 按这个**固定顺序**一次走完(用 Playwright MCP 工具,约 30 秒,**连发不思考**):
303
+ 1. **`browser_snapshot`**(如果刚才已经 snapshot 过且页面没变,可跳过)看页面元素,找到验证码图片元素
243
304
  2. **`browser_take_screenshot`** 截验证码图片 → 存临时文件(稳定,不像 Computer 截图会失败)
244
305
  3. **Read 工具看图 → 算出答案**(QoderWork 自带视觉,1 次模型调用,数学题识别率高)
245
306
  4. **`browser_type`** 填答案到验证码输入框
246
- 5. **删掉临时图片**(bash `del _captcha.png`)
247
- > 验证码有时效(1-2 分钟),所以要快。过期了刷新重走一遍。
307
+ 5. **立刻 `browser_click` 点登录**(验证码刚填、还没过期,抓住窗口)
308
+ 6. **删掉临时图片**(bash `del _captcha.png`)
309
+ > 验证码过期了(提示"验证码已失效")→ 别纠结,直接刷新重走 1-5 步。
248
310
  > 识别完**必须删图**,不留垃圾。
311
+ > 自识别连续失败 2 次或明显是短信/扫码类 → 才交人(见 Step A 铁律⑤)。
249
312
 
250
313
  ### Step 4:回收结果 + 自学习沉淀(🔴 必须,不做完不出报告)
251
314
 
@@ -254,7 +317,7 @@ python "$R/.qoder/scripts/autotest.py" quick \
254
317
 
255
318
  **执行方式**:把测试结果 + 测试中 `browser_snapshot` 看到的页面元素一起回传:
256
319
  ```bash
257
- python "$R/.qoder/scripts/autotest.py" quick \
320
+ python "$R/.qoder/scripts/test/autotest.py" quick \
258
321
  --cases-file <之前的cases文件> \
259
322
  --record '{"Q-1":"pass","Q-2":"pass","Q-3":"pass"}' \
260
323
  --page-json '{"url":"/veh/vehicle/vehAffair/insurance","elements":[{"role":"textbox","name":"车牌号"},{"role":"button","name":"搜索"},{"role":"button","name":"新增"}]}'
@@ -275,11 +338,11 @@ python "$R/.qoder/scripts/autotest.py" quick \
275
338
  ## browser 基于任务的浏览器测试(用户明确给了任务名时)
276
339
 
277
340
  ```bash
278
- python "$R/.qoder/scripts/task.py" list # 确认任务存在
279
- python "$R/.qoder/scripts/autotest.py" generate <task> # 生成骨架到 autotest-cases.jsonl
341
+ python "$R/.qoder/scripts/task/task.py" list # 确认任务存在
342
+ python "$R/.qoder/scripts/test/autotest.py" generate <task> # 生成骨架到 autotest-cases.jsonl
280
343
  # AI 读任务 PRD「验收标准」补全 steps/expected (真实数据用 {{ask:}}, 不搜源码)
281
- python "$R/.qoder/scripts/autotest.py" run <task> # 注入+出执行计划
282
- python "$R/.qoder/scripts/autotest.py" run <task> --record '{"..":"pass"}' # 回收结果
344
+ python "$R/.qoder/scripts/test/autotest.py" run <task> # 注入+出执行计划
345
+ python "$R/.qoder/scripts/test/autotest.py" run <task> --record '{"..":"pass"}' # 回收结果
283
346
  ```
284
347
 
285
348
  **回归测试增强(图谱 MCP)**:测接口改动时,用 `get_impact` 查影响范围,自动覆盖受影响页面:
@@ -307,9 +370,9 @@ JUnit 测试(Mock + MockMvc,Given-When-Then,`@DisplayName`,AssertJ)→
307
370
  域名才放 config(全团队一致)。账号密码每人不同 → 个人文档。
308
371
  - **个人文档**:`workspace/members/{你的名字}/autotest-data.yaml`(gitignored,永不 push)
309
372
  ```bash
310
- python "$R/.qoder/scripts/autotest.py" init-data # 生成模板
311
- python "$R/.qoder/scripts/autotest.py" set-data 登录账号 test --env test # 填(按环境/平台分块)
312
- python "$R/.qoder/scripts/autotest.py" list-data --env test # 查看(密码脱敏)
373
+ python "$R/.qoder/scripts/test/autotest.py" init-data # 生成模板
374
+ python "$R/.qoder/scripts/test/autotest.py" set-data 登录账号 test --env test # 填(按环境/平台分块)
375
+ python "$R/.qoder/scripts/test/autotest.py" list-data --env test # 查看(密码脱敏)
313
376
  ```
314
377
  run/quick 时按 env/platform 自动取最精确的值;`--data` 可临时覆盖。
315
378
 
@@ -319,7 +382,7 @@ run/quick 时按 env/platform 自动取最精确的值;`--data` 可临时覆
319
382
 
320
383
  **Step A:查登录态**
321
384
  ```bash
322
- python "$R/.qoder/scripts/autotest.py" login-state show
385
+ python "$R/.qoder/scripts/test/autotest.py" login-state show
323
386
  ```
324
387
 
325
388
  **Step B:按登录态决定行为**
@@ -340,8 +403,8 @@ python "$R/.qoder/scripts/autotest.py" login-state show
340
403
 
341
404
  ```bash
342
405
  # 手动操作(一般不用,AI 会自动):
343
- python "$R/.qoder/scripts/autotest.py" login-state mark --env pre --platform web # 标记已登录
344
- python "$R/.qoder/scripts/autotest.py" login-state clear --env pre # 清除(强制重登录)
406
+ python "$R/.qoder/scripts/test/autotest.py" login-state mark --env pre --platform web # 标记已登录
407
+ python "$R/.qoder/scripts/test/autotest.py" login-state clear --env pre # 清除(强制重登录)
345
408
  ```
346
409
 
347
410
  ## 🧩 QoderWork 增强:浏览器自动执行(可选 · 无连接器则自动回退)
@@ -1,4 +1,19 @@
1
- # QODER Pipeline
1
+ # QODER Pipeline
2
+
3
+ ## 🏗️ 架构: 引擎与数据分离 (Trellis 化)
4
+
5
+ `.qoder/` 一分为二 (v2.8.0+):
6
+
7
+ | 类别 | 内容 | 来源 | 进团队 git? |
8
+ |------|------|------|------------|
9
+ | **引擎代码** | `.qoder/scripts/` `skills/` `commands/` `agents/` `contracts/` `hooks/` `templates/` `rules/` | `npx @hupan56/wlkj init/update` 生成 | ❌ gitignore |
10
+ | **团队数据** | `.qoder/config.yaml` `config.toml` `settings.json` `learning/` + `data/` (kg.duckdb/索引) + `workspace/` (PRD/任务) | git 跟踪, 团队共享 | ✅ |
11
+
12
+ - 引擎代码**只由 npm 包生成**, 不再进团队 git → 彻底消除"引擎更新污染团队 git"和"新机 init 产生 unrelated histories"。
13
+ - Qoder 全系列仍读 `.qoder/` (路径不变), 只是引擎来源从 git 变成 npm 生成。
14
+ - 老团队升级 (管理员跑一次): `npx @hupan56/wlkj migrate`, 然后团队成员各自 `npx @hupan56/wlkj update`。
15
+
16
+ ---
2
17
 
3
18
  ## ⚠️ CRITICAL RULE #1: ALWAYS ASK PLATFORM FIRST
4
19
 
@@ -33,41 +48,25 @@ Examples:
33
48
  - "保险定时任务" -> /wl-search insurance
34
49
  - "薪资API" -> /wl-search --api salary
35
50
 
36
- ## CRITICAL RULE #2.5: 自然语言 技能 路由(Quest / QoderWork 入口)
37
-
38
- Quest 与 QoderWork 没有斜杠命令,AI 必须按下表把用户自然语言映射到
39
- `.qoder/skills/` 下的技能(Qoder IDE 编辑器与 CLI 用 `/wl-*` 命令,同样适用):
40
-
41
- | 用户说什么 | 触发 skill |
42
- |------------|-----------|
43
- | "写个需求""生成PRD""做个原型""新模块" | /wl-prd-full(完整档,正经需求)|
44
- | "加个字段""加个导出按钮""改下文案""小改动" | /wl-prd-quick(快档,零星需求)|
45
- | "评审PRD""检查需求文档""PRD质量怎么样""这个PRD合格吗" | /wl-prd-review(评审 PRD 质量)|
46
- | "录入设计稿""Figma稿录入""把这个设计录入" | wl-design import (design-import) |
47
- | "按设计出原型""按设计师规范出原型" | wl-design generate (prototype-generator) |
48
- | "评审原型""设计稿看一下""检查交互稿" | wl-design review (design-review) |
49
- | "查一下""代码在哪""搜保险""考勤API" | wl-search |
50
- | "建任务""排优先级""任务做完了" | wl-task |
51
- | "项目怎么样""进度""健康度" | wl-status |
52
- | "写日报""周报""我干了啥" | wl-report |
53
- | "调研""竞品""对标""规划""路线图""接下来做什么""发现个机会""探讨需求" | wl-insight(探索站)|
54
- | "生成规格""写spec" | spec-generator |
55
- | "开始写代码""按规格实现" | spec-coder |
56
- | "测一下XX支不支持YY""验一下XX能不能""XX功能对不对""跑测试用例""回归测试" | /wl-test(测试工作流, 命令或带验证点的自然语言触发) |
57
- | "写单元测试""补单测" | /wl-test unit(test-generator) |
58
- | "评审PRD" | prd-review |
59
- | "提交代码""commit""推上去" | wl-commit |
60
- | "初始化""换开发者""环境检查" | wl-init |
61
-
62
- 找不到明确匹配 → 先问用户意图,不要乱跑。
51
+ ## CRITICAL RULE #2.5: 触发方式(非侵入式 v3.0)
52
+
53
+ 工作流**只在显式调用时启动**,不主动干预普通对话。
54
+
55
+ **两种触发方式(且仅此两种):**
56
+ 1. **斜杠命令**:`/wl-prd-full` `/wl-search` `/wl-req` → 启动工作流
57
+ 2. **npx**:`npx @hupan56/wlkj init/update` → 安装/同步
58
+
59
+ **自然语言不触发工作流。** 用户说"写个需求""查下代码"等,AI 正常回答,不自动拉入工作流。
60
+ (工作流启动后的过程内交互 如选平台、确认需求 仍可用自然语言。)
61
+
63
62
  注意区分:"同步/保存产出" = team_sync(push);"提交/commit 代码" = wl-commit。
64
63
 
65
64
  ## CRITICAL RULE #3: User NEVER touches git
66
65
 
67
66
  PMs on this team do not know git. ALL git operations are invisible:
68
67
  - After any publish/save action (/wl-prd-full publish, /wl-task create/finish,
69
- /wl-report save), run: `python .qoder/scripts/team_sync.py push`
70
- - To get teammates' latest work: `python .qoder/scripts/team_sync.py pull`
68
+ /wl-report save), run: `python .qoder/scripts/task/team_sync.py push`
69
+ - To get teammates' latest work: `python .qoder/scripts/task/team_sync.py pull`
71
70
  - If team_sync prints SYNC_CONFLICT, YOU (the AI) resolve the conflict
72
71
  yourself (it tells you the commands). NEVER ask the user to run git.
73
72
  - When the user says 同步/保存/提交 in natural language -> team_sync, not raw git.
@@ -126,7 +125,7 @@ app/mobile/h5/移动端 -> Carmg-H5
126
125
 
127
126
  3 projects: Carmg-H5(1378 files), fywl-ics(8973 files, 3903 APIs), fywl-ui(3270 files)
128
127
  Search: /wl-search <keyword> returns compact results (~200 bytes)
129
- Weekly sync: python .qoder/scripts/git_sync.py
128
+ Weekly sync: python .qoder/scripts/task/git_sync.py
130
129
 
131
130
  ### MCP Servers (QoderWork / Qoder IDE)
132
131
  注册在 `~/.qoderwork/mcp.json`,setup.py 一键配齐:
@@ -135,7 +134,7 @@ Weekly sync: python .qoder/scripts/git_sync.py
135
134
  - `qoder-mysql` (stdio) — 只读查真实数据库结构/数据。自动拉起。
136
135
  - `lanhu` (STDIO) — 蓝湖设计稿直读,设计师发链接 AI 提取颜色/尺寸/字体/切图。
137
136
  **STDIO 模式:开 QoderWork 自动起、关自动停**(QoderWork 原生进程管理,无需手动 start)。
138
- cookie 按角色隔离(UI 能改稿/PM 只读):wrapper 读当前 `.developer` → 该角色的 `workspace/members/{dev}/.secrets/lanhu.env`(gitignored)。换角色改 `.developer` + 配该角色 cookie → 重启 QoderWork。首次配置跑 `python .qoder/scripts/setup_lanhu.py`。
137
+ cookie 按角色隔离(UI 能改稿/PM 只读):wrapper 读当前 `.developer` → 该角色的 `workspace/members/{dev}/.secrets/lanhu.env`(gitignored)。换角色改 `.developer` + 配该角色 cookie → 重启 QoderWork。首次配置跑 `python .qoder/scripts/setup/setup_lanhu.py`。
139
138
 
140
139
  ### Style Index (Prototype Reference)
141
140
  UI patterns from Vue files: Tables(718), Forms(301), Detail(129), Dashboard(90)
@@ -144,10 +143,10 @@ Form components: Input(2262), Select(1049), ApiSelect(725), DatePicker(265)
144
143
  Field map: 2383 entries
145
144
 
146
145
  Style search:
147
- python .qoder/scripts/search_index.py --style table/form/modal/dashboard
148
- python .qoder/scripts/search_index.py --field <field_name>
149
- python .qoder/scripts/search_index.py --components
150
- python .qoder/scripts/search_index.py --vben # Vben Admin CSS variables
146
+ python .qoder/scripts/kg/search_index.py --style table/form/modal/dashboard
147
+ python .qoder/scripts/kg/search_index.py --field <field_name>
148
+ python .qoder/scripts/kg/search_index.py --components
149
+ python .qoder/scripts/kg/search_index.py --vben # Vben Admin CSS variables
151
150
 
152
151
  Prototype templates:
153
152
  .qoder/templates/prototype-web.html # Web管理端 (Vben Admin + Ant Design)
@@ -182,7 +181,7 @@ Each product loads rules from a different carrier — keep them in sync:
182
181
  | QoderWork 桌面 Agent | 安装 .qoder/skills/ 下的技能 | 自然语言 -> skills |
183
182
 
184
183
  > 本流水线聚焦 Qoder 全系列,4 种载体功能等价。知识图谱 13 个能力(影响分析/
185
- > 覆盖矩阵/功能画像/业务流程/多跳遍历…)通过统一 CLI `python .qoder/scripts/kg.py`
184
+ > 覆盖矩阵/功能画像/业务流程/多跳遍历…)通过统一 CLI `python .qoder/scripts/kg/kg.py`
186
185
  > 全系列触达;QoderWork 环境也可直接调 MCP 工具。
187
186
 
188
187
  ## QoderWork Integration: Optional Enhancement Only (Progressive Enhancement)
@@ -211,11 +210,11 @@ Each product loads rules from a different carrier — keep them in sync:
211
210
  macOS/Linux -> scripts/setup_weekly_cron.sh. Only ONE machine per team
212
211
  needs this; everyone else gets the graph via /wl-init's auto pull.
213
212
  - Environment problems of any kind -> run:
214
- `python .qoder/scripts/init_doctor.py` (add --fix to auto-repair).
213
+ `python .qoder/scripts/setup/init_doctor.py` (add --fix to auto-repair).
215
214
 
216
215
  ## Fast Path: one call to get all context
217
216
 
218
- `python .qoder/scripts/context_pack.py <业务词> --platform <web|app>` returns
217
+ `python .qoder/scripts/kg/context_pack.py <业务词> --platform <web|app>` returns
219
218
  code files + page examples + field specs + PRDs + APIs + style/icon pointers
220
219
  in ONE call. Use it FIRST; fall back to search_index.py for drill-down.
221
220
 
@@ -226,7 +225,7 @@ Web = Ant Design inline SVG, APP = Vant font icons. Emoji in a prototype = FAIL.
226
225
 
227
226
  ## Quality Gate (EVA)
228
227
 
229
- Before publishing any PRD: `python .qoder/scripts/eval_prd.py <prd> [prototype]`
228
+ Before publishing any PRD: `python .qoder/scripts/report/eval_prd.py <prd> [prototype]`
230
229
  must PASS (>=80%). Metrics definition: data/docs/constitution/eva-metrics.md
231
230
 
232
231
  ## Permission Model
@@ -1,169 +0,0 @@
1
- # inject-workflow-state.py - Inject workflow state on every message
2
- import os, json, sys
3
-
4
- if sys.platform == 'win32':
5
- try:
6
- sys.stdout.reconfigure(encoding='utf-8')
7
- except Exception:
8
- pass
9
-
10
- NL = chr(10)
11
- BASE = os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
12
-
13
- # mtime 缓存 (审计 H5: 每条用户消息都跑这个 hook, 缓存避免重复读盘)
14
- # 格式: {filepath: (mtime, parsed_value)}
15
- _CACHE = {}
16
-
17
-
18
- def _append_hook_error(log_path, line):
19
- """写一行错误到 hook-errors.log, 超 100KB 截断保留最后 50KB (防无限膨胀)。
20
-
21
- best-effort: 任何 IO 失败都静默 (不能让日志写入失败导致 hook 二次崩溃)。
22
- """
23
- from datetime import datetime
24
- try:
25
- os.makedirs(os.path.dirname(log_path), exist_ok=True)
26
- # 轮转: 文件过大则截断
27
- if os.path.isfile(log_path):
28
- try:
29
- if os.path.getsize(log_path) > 100 * 1024:
30
- with open(log_path, 'r', encoding='utf-8', errors='replace') as f:
31
- tail = f.read()[-50 * 1024:]
32
- with open(log_path, 'w', encoding='utf-8') as f:
33
- f.write(tail)
34
- except OSError:
35
- pass
36
- with open(log_path, 'a', encoding='utf-8') as f:
37
- f.write('[{}] inject-workflow-state: {}\n'.format(
38
- datetime.now().strftime('%Y-%m-%d %H:%M:%S'), str(line)[:200]))
39
- except Exception:
40
- pass
41
-
42
-
43
- def _read_developer():
44
- """读当前开发者名 (容错: UTF-8/GBK)."""
45
- df = os.path.join(BASE, '.qoder', '.developer')
46
- if not os.path.isfile(df):
47
- return None
48
- for enc in ('utf-8', 'gbk', 'utf-8-sig'):
49
- try:
50
- with open(df, encoding=enc) as f:
51
- for line in f:
52
- line = line.strip()
53
- if line.startswith('name='):
54
- return line.split('=', 1)[1].strip()
55
- if line.startswith('name:') or ':' in line:
56
- # 容错 name: x 格式
57
- for sep in ('=', ':'):
58
- if sep in line:
59
- k, v = line.split(sep, 1)
60
- if k.strip() == 'name':
61
- return v.strip()
62
- except (OSError, UnicodeDecodeError):
63
- continue
64
- return None
65
-
66
-
67
- def _cached_read(path, parser):
68
- """带 mtime 缓存的文件读。parser: callable(content_str) -> value."""
69
- try:
70
- mtime = os.path.getmtime(path)
71
- except OSError:
72
- return None
73
- cached = _CACHE.get(path)
74
- if cached and cached[0] == mtime:
75
- return cached[1]
76
- # 重读
77
- for enc in ('utf-8', 'gbk', 'utf-8-sig'):
78
- try:
79
- with open(path, encoding=enc) as f:
80
- content = f.read()
81
- value = parser(content)
82
- _CACHE[path] = (mtime, value)
83
- return value
84
- except (OSError, UnicodeDecodeError):
85
- continue
86
- return None
87
-
88
-
89
- def get_status():
90
- # 优先读按开发者隔离的 current-task 文件
91
- dev = _read_developer()
92
- ct_candidates = []
93
- if dev:
94
- safe = ''.join(c for c in dev if c.isalnum() or c in '_-') or 'anon'
95
- ct_candidates.append(os.path.join(BASE, '.qoder', '.runtime', 'current-task.' + safe))
96
- ct_candidates.append(os.path.join(BASE, '.qoder', '.current-task')) # 旧格式回退
97
-
98
- rel = None
99
- for ct in ct_candidates:
100
- rel = _cached_read(ct, lambda c: c.strip() or None)
101
- if rel:
102
- break
103
- if not rel:
104
- return None, None
105
-
106
- tj = os.path.join(BASE, rel, 'task.json')
107
- if os.path.isfile(tj):
108
- d = _cached_read(tj, lambda c: json.loads(c) if c.strip() else None)
109
- if d:
110
- return rel, d.get('status', 'unknown')
111
- return rel, None
112
-
113
-
114
- # Statuses written by task.py: planning -> in_progress -> completed
115
- GUIDE = {
116
- None: 'No active task. Use /wl-prd-full to start, or /wl-task create.',
117
- 'planning': 'Task in planning. Use /wl-prd-full to write the PRD, then /wl-task start.',
118
- 'in_progress': 'Dev in progress. /wl-spec to generate spec, /wl-code to implement.',
119
- 'completed': 'Task completed. /wl-task archive to archive, or start the next one.',
120
- }
121
-
122
-
123
- def main():
124
- tp, st = get_status()
125
- # Hook-2: 如果任务/状态没变, 输出空 (减少 token 消耗)
126
- # 用 .runtime/.last-workflow-state 存上次输出, mtime 变了才重新注入
127
- state_file = os.path.join(BASE, '.qoder', '.runtime', '.last-wf-state.txt')
128
- current_sig = '{}|{}'.format(tp or '', st or '')
129
- try:
130
- os.makedirs(os.path.dirname(state_file), exist_ok=True)
131
- if os.path.isfile(state_file):
132
- last_sig = open(state_file, 'r', encoding='utf-8').read().strip()
133
- if last_sig == current_sig:
134
- # 状态没变, 不重复注入 (省 token), 但仍输出健康标记
135
- # QoderWork 看不到任何输出时分不清"缓存跳过"还是"hook 没跑"
136
- print('<!-- [hook-ok] inject-workflow-state: state unchanged (cached) -->')
137
- return
138
- open(state_file, 'w', encoding='utf-8').write(current_sig)
139
- except OSError:
140
- pass
141
-
142
- g = GUIDE.get(st, 'Status "{}" - check task.json, expected planning/in_progress/completed.'.format(st))
143
- parts = []
144
- parts.append('<qoder-workflow>')
145
- if tp:
146
- parts.append('Task: ' + tp)
147
- parts.append('Status: ' + str(st))
148
- parts.append('Do: ' + g)
149
- parts.append('</qoder-workflow>')
150
- # Hook-1: 健康标记 — 与 session-start 一致, QoderWork 看不到说明 hook 没生效
151
- parts.append('<!-- [hook-ok] inject-workflow-state hook ran -->')
152
- print(NL.join(parts))
153
-
154
-
155
- if __name__ == '__main__':
156
- try:
157
- main()
158
- except Exception as e:
159
- # 落盘到日志 (stdout 一次性, 追溯用)
160
- try:
161
- from pathlib import Path
162
- log_path = str(Path(__file__).resolve().parents[1] / '.runtime' / 'hook-errors.log')
163
- _append_hook_error(log_path, str(e)[:200])
164
- except Exception:
165
- pass
166
- print('<qoder-workflow>')
167
- print('hook error: ' + str(e)[:200])
168
- print(' (详情见 .qoder/.runtime/hook-errors.log)')
169
- print('</qoder-workflow>')