@hupan56/wlkj 2.7.12 → 3.1.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (253) hide show
  1. package/bin/cli.js +1435 -1107
  2. package/package.json +29 -29
  3. package/templates/qoder/agents/insight-planning.md +1 -1
  4. package/templates/qoder/agents/insight-research.md +28 -15
  5. package/templates/qoder/commands/optional/wl-insight.md +161 -161
  6. package/templates/qoder/commands/optional/wl-report.md +20 -39
  7. package/templates/qoder/commands/optional/wl-status.md +4 -4
  8. package/templates/qoder/commands/wl-code.md +2 -2
  9. package/templates/qoder/commands/wl-design.md +6 -6
  10. package/templates/qoder/commands/wl-init.md +3 -3
  11. package/templates/qoder/commands/wl-prd.md +26 -13
  12. package/templates/qoder/commands/wl-req.md +43 -0
  13. package/templates/qoder/commands/wl-search.md +78 -47
  14. package/templates/qoder/commands/wl-task.md +343 -40
  15. package/templates/qoder/commands/wl-test.md +43 -17
  16. package/templates/qoder/config.yaml +52 -14
  17. package/templates/qoder/hooks/post-tool-use.py +181 -0
  18. package/templates/qoder/hooks/session-start.py +94 -23
  19. package/templates/qoder/hooks/stop-eval.py +207 -0
  20. package/templates/qoder/hooks/user-prompt-submit.py +139 -0
  21. package/templates/qoder/rules/wl-pipeline.md +85 -49
  22. package/templates/qoder/scripts/README.md +102 -0
  23. package/templates/qoder/scripts/capability/__init__.py +26 -0
  24. package/templates/qoder/scripts/capability/__main__.py +72 -0
  25. package/templates/qoder/scripts/capability/adapters/__init__.py +29 -0
  26. package/templates/qoder/scripts/capability/adapters/cli.py +316 -0
  27. package/templates/qoder/scripts/capability/adapters/mcp.py +334 -0
  28. package/templates/qoder/scripts/capability/adapters/qw.py +271 -0
  29. package/templates/qoder/scripts/capability/caps/__init__.py +27 -0
  30. package/templates/qoder/scripts/capability/caps/context.py +36 -0
  31. package/templates/qoder/scripts/capability/caps/cron.py +50 -0
  32. package/templates/qoder/scripts/capability/caps/identity.py +43 -0
  33. package/templates/qoder/scripts/capability/caps/memory.py +71 -0
  34. package/templates/qoder/scripts/capability/caps/notify.py +41 -0
  35. package/templates/qoder/scripts/capability/caps/present.py +48 -0
  36. package/templates/qoder/scripts/capability/caps/repo.py +31 -0
  37. package/templates/qoder/scripts/capability/caps/sandbox.py +43 -0
  38. package/templates/qoder/scripts/capability/chain.py +92 -0
  39. package/templates/qoder/scripts/capability/memory_chain.py +41 -0
  40. package/templates/qoder/scripts/capability/registry.py +246 -0
  41. package/templates/qoder/scripts/capability/registry_mcp.py +250 -0
  42. package/templates/qoder/scripts/capability/smoke_test.py +211 -0
  43. package/templates/qoder/scripts/capability/smoke_test_report.json +94 -0
  44. package/templates/qoder/scripts/deployment/setup/__init__.py +11 -0
  45. package/templates/qoder/scripts/deployment/setup/carriers.py +669 -0
  46. package/templates/qoder/scripts/deployment/setup/carriers_verify.py +148 -0
  47. package/templates/qoder/scripts/{init_doctor.py → deployment/setup/init_doctor.py} +100 -39
  48. package/templates/qoder/scripts/{install_qoderwork.py → deployment/setup/install_qoderwork.py} +48 -30
  49. package/templates/qoder/scripts/{platform_doctor.py → deployment/setup/platform_doctor.py} +271 -259
  50. package/templates/qoder/scripts/{repo_root.py → deployment/setup/repo_root.py} +9 -2
  51. package/templates/qoder/scripts/{setup.py → deployment/setup/setup.py} +143 -14
  52. package/templates/qoder/scripts/{setup_lanhu.py → deployment/setup/setup_lanhu.py} +979 -963
  53. package/templates/qoder/scripts/domain/design/__init__.py +0 -0
  54. package/templates/qoder/scripts/{fill_prototype.py → domain/design/fill_prototype.py} +17 -5
  55. package/templates/qoder/scripts/{gen_design_doc.py → domain/design/gen_design_doc.py} +406 -394
  56. package/templates/qoder/scripts/domain/kg/__init__.py +11 -0
  57. package/templates/qoder/scripts/domain/kg/build/__init__.py +0 -0
  58. package/templates/qoder/scripts/domain/kg/build/build_entity_registry.py +201 -0
  59. package/templates/qoder/scripts/domain/kg/build/build_relations.py +132 -0
  60. package/templates/qoder/scripts/{build_style_index.py → domain/kg/build/build_style_index.py} +50 -15
  61. package/templates/qoder/scripts/domain/kg/build/build_workflows.py +149 -0
  62. package/templates/qoder/scripts/{kg_build.py → domain/kg/build/kg_build.py} +685 -612
  63. package/templates/qoder/scripts/{kg_build_db.py → domain/kg/build/kg_build_db.py} +338 -327
  64. package/templates/qoder/scripts/{kg_incremental.py → domain/kg/build/kg_incremental.py} +425 -393
  65. package/templates/qoder/scripts/{learn_aggregate.py → domain/kg/build/learn_aggregate.py} +212 -201
  66. package/templates/qoder/scripts/domain/kg/extract/__init__.py +0 -0
  67. package/templates/qoder/scripts/{common → domain/kg/extract}/extract.py +430 -419
  68. package/templates/qoder/scripts/domain/kg/extract/test_extract.py +115 -0
  69. package/templates/qoder/scripts/{common → domain/kg/extract}/ts_extract.py +614 -536
  70. package/templates/qoder/scripts/domain/kg/graph/__init__.py +0 -0
  71. package/templates/qoder/scripts/{kg_link_db.py → domain/kg/graph/kg_link_db.py} +235 -224
  72. package/templates/qoder/scripts/domain/kg/graph/kg_semantic.py +683 -0
  73. package/templates/qoder/scripts/{kg.py → domain/kg/kg.py} +871 -708
  74. package/templates/qoder/scripts/domain/kg/kg_capabilities.py +182 -0
  75. package/templates/qoder/scripts/domain/kg/search/__init__.py +0 -0
  76. package/templates/qoder/scripts/{context_pack.py → domain/kg/search/context_pack.py} +45 -17
  77. package/templates/qoder/scripts/{enrich_prompt.py → domain/kg/search/enrich_prompt.py} +238 -226
  78. package/templates/qoder/scripts/domain/kg/search/prefetch.py +384 -0
  79. package/templates/qoder/scripts/{common → domain/kg/search}/search_engine.py +207 -205
  80. package/templates/qoder/scripts/{search_index.py → domain/kg/search/search_index.py} +210 -88
  81. package/templates/qoder/scripts/domain/kg/server/__init__.py +0 -0
  82. package/templates/qoder/scripts/domain/kg/server/kgd.py +549 -0
  83. package/templates/qoder/scripts/domain/kg/server/perf_bench.py +197 -0
  84. package/templates/qoder/scripts/domain/kg/storage/__init__.py +0 -0
  85. package/templates/qoder/scripts/{kg_duckdb.py → domain/kg/storage/kg_duckdb.py} +70 -45
  86. package/templates/qoder/scripts/domain/learning/__init__.py +0 -0
  87. package/templates/qoder/scripts/{learn.py → domain/learning/learn.py} +157 -146
  88. package/templates/qoder/scripts/domain/report/__init__.py +11 -0
  89. package/templates/qoder/scripts/{add_session.py → domain/report/add_session.py} +256 -244
  90. package/templates/qoder/scripts/{export.py → domain/report/export.py} +72 -4
  91. package/templates/qoder/scripts/{report.py → domain/report/report.py} +292 -281
  92. package/templates/qoder/scripts/domain/report/report_snapshot.py +261 -0
  93. package/templates/qoder/scripts/domain/report/role.py +39 -0
  94. package/templates/qoder/scripts/{status.py → domain/report/status.py} +737 -628
  95. package/templates/qoder/scripts/domain/report/status_snapshot.py +191 -0
  96. package/templates/qoder/scripts/domain/requirement/__init__.py +0 -0
  97. package/templates/qoder/scripts/{archive_prd.py → domain/requirement/archive_prd.py} +389 -377
  98. package/templates/qoder/scripts/domain/requirement/req.py +228 -0
  99. package/templates/qoder/scripts/domain/task/__init__.py +11 -0
  100. package/templates/qoder/scripts/{git_sync.py → domain/task/git_sync.py} +90 -50
  101. package/templates/qoder/scripts/{syncgate.py → domain/task/syncgate.py} +18 -7
  102. package/templates/qoder/scripts/domain/task/task.py +229 -0
  103. package/templates/qoder/scripts/domain/task/task_lifecycle.py +606 -0
  104. package/templates/qoder/scripts/domain/task/task_query.py +162 -0
  105. package/templates/qoder/scripts/domain/task/task_relations.py +425 -0
  106. package/templates/qoder/scripts/{team_sync.py → domain/task/team_sync.py} +101 -23
  107. package/templates/qoder/scripts/domain/task/zentao_sync.py +370 -0
  108. package/templates/qoder/scripts/foundation/__init__.py +0 -0
  109. package/templates/qoder/scripts/foundation/bootstrap.py +145 -0
  110. package/templates/qoder/scripts/foundation/core/__init__.py +0 -0
  111. package/templates/qoder/scripts/foundation/core/cmd_registry.py +112 -0
  112. package/templates/qoder/scripts/foundation/core/config.py +187 -0
  113. package/templates/qoder/scripts/{common → foundation/core}/paths.py +706 -400
  114. package/templates/qoder/scripts/foundation/data/__init__.py +0 -0
  115. package/templates/qoder/scripts/foundation/data/contract.py +317 -0
  116. package/templates/qoder/scripts/{common → foundation/data}/events.py +2 -2
  117. package/templates/qoder/scripts/foundation/data/result.py +223 -0
  118. package/templates/qoder/scripts/{common → foundation/data}/task_utils.py +427 -392
  119. package/templates/qoder/scripts/foundation/identity/__init__.py +0 -0
  120. package/templates/qoder/scripts/foundation/identity/check_publish.py +103 -0
  121. package/templates/qoder/scripts/{common → foundation/identity}/developer.py +239 -238
  122. package/templates/qoder/scripts/foundation/identity/guard.py +159 -0
  123. package/templates/qoder/scripts/{common → foundation/identity}/identity.py +132 -9
  124. package/templates/qoder/scripts/foundation/identity/roles.py +60 -0
  125. package/templates/qoder/scripts/foundation/integrations/__init__.py +0 -0
  126. package/templates/qoder/scripts/{common → foundation/integrations}/active_task.py +30 -15
  127. package/templates/qoder/scripts/{common → foundation/integrations}/feishu.py +11 -10
  128. package/templates/qoder/scripts/{common → foundation/integrations}/terms.py +3 -3
  129. package/templates/qoder/scripts/foundation/integrations/zentao_client.py +220 -0
  130. package/templates/qoder/scripts/foundation/io/__init__.py +0 -0
  131. package/templates/qoder/scripts/{common → foundation/io}/filelock.py +1 -1
  132. package/templates/qoder/scripts/{common → foundation/io}/reqid.py +1 -1
  133. package/templates/qoder/scripts/foundation/protocol/__init__.py +0 -0
  134. package/templates/qoder/scripts/foundation/protocol/duckdb_conn.py +39 -0
  135. package/templates/qoder/scripts/foundation/protocol/mcp_base.py +268 -0
  136. package/templates/qoder/scripts/foundation/utils/__init__.py +0 -0
  137. package/templates/qoder/scripts/{common → foundation/utils}/eval_api.py +12 -1
  138. package/templates/qoder/scripts/{common → foundation/utils}/pip_install.py +23 -6
  139. package/templates/qoder/scripts/{common → foundation/utils}/platform_guard.py +1 -1
  140. package/templates/qoder/scripts/orchestration/__init__.py +0 -0
  141. package/templates/qoder/scripts/orchestration/wlkj.py +185 -0
  142. package/templates/qoder/scripts/protocol/__init__.py +0 -0
  143. package/templates/qoder/scripts/protocol/browser/README.md +207 -0
  144. package/templates/qoder/scripts/protocol/browser/SKILL.md +265 -0
  145. package/templates/qoder/scripts/protocol/browser/config.env +9 -0
  146. package/templates/qoder/scripts/protocol/browser/references/cdp-api.md +110 -0
  147. package/templates/qoder/scripts/protocol/browser/references/migration-2.5.3.md +72 -0
  148. package/templates/qoder/scripts/protocol/browser/references/site-patterns/.gitkeep +0 -0
  149. package/templates/qoder/scripts/protocol/browser/scripts/browser-discovery.mjs +138 -0
  150. package/templates/qoder/scripts/protocol/browser/scripts/cdp-proxy.mjs +672 -0
  151. package/templates/qoder/scripts/protocol/browser/scripts/check-deps.mjs +206 -0
  152. package/templates/qoder/scripts/protocol/browser/scripts/find-url.mjs +253 -0
  153. package/templates/qoder/scripts/protocol/browser/scripts/match-site.mjs +46 -0
  154. package/templates/qoder/scripts/protocol/browser/templates/config.env.template +9 -0
  155. package/templates/qoder/scripts/protocol/mcp/__init__.py +11 -0
  156. package/templates/qoder/scripts/protocol/mcp/kg_mcp_server.py +478 -0
  157. package/templates/qoder/scripts/{lanhu_stdio_wrapper.py → protocol/mcp/lanhu_stdio_wrapper.py} +131 -119
  158. package/templates/qoder/scripts/protocol/mcp/mcp_doctor.py +548 -0
  159. package/templates/qoder/scripts/protocol/mcp/mcp_launcher.py +600 -0
  160. package/templates/qoder/scripts/protocol/mcp/mysql_mcp_server.py +461 -0
  161. package/templates/qoder/scripts/protocol/mcp/zentao_mcp_server.py +1920 -0
  162. package/templates/qoder/scripts/protocol/transports/__init__.py +56 -0
  163. package/templates/qoder/scripts/protocol/transports/base.py +87 -0
  164. package/templates/qoder/scripts/protocol/transports/cli.py +132 -0
  165. package/templates/qoder/scripts/protocol/transports/http.py +141 -0
  166. package/templates/qoder/scripts/protocol/transports/stdio.py +293 -0
  167. package/templates/qoder/scripts/run_weekly_update.bat +27 -18
  168. package/templates/qoder/scripts/run_weekly_update.sh +22 -13
  169. package/templates/qoder/scripts/validation/__init__.py +0 -0
  170. package/templates/qoder/scripts/validation/eval/__init__.py +0 -0
  171. package/templates/qoder/scripts/validation/eval/perf-report-2026-06-24.md +139 -0
  172. package/templates/qoder/scripts/validation/eval/qwork_harness.py +617 -0
  173. package/templates/qoder/scripts/validation/eval/report-commands.md +220 -0
  174. package/templates/qoder/scripts/validation/eval/transcript_timing.py +386 -0
  175. package/templates/qoder/scripts/validation/metrics/__init__.py +0 -0
  176. package/templates/qoder/scripts/{eval_prd.py → validation/metrics/eval_prd.py} +84 -15
  177. package/templates/qoder/scripts/validation/metrics/usability_score.py +750 -0
  178. package/templates/qoder/scripts/validation/test/__init__.py +11 -0
  179. package/templates/qoder/scripts/validation/test/assertion_gen.py +551 -0
  180. package/templates/qoder/scripts/{autotest.py → validation/test/autotest.py} +1234 -1751
  181. package/templates/qoder/scripts/validation/test/autotest_auth.py +109 -0
  182. package/templates/qoder/scripts/{autotest_batch.py → validation/test/autotest_batch.py} +255 -224
  183. package/templates/qoder/scripts/validation/test/autotest_data.py +680 -0
  184. package/templates/qoder/scripts/validation/test/autotest_opencli.py +646 -0
  185. package/templates/qoder/scripts/{autotest_run.py → validation/test/autotest_run.py} +323 -297
  186. package/templates/qoder/scripts/validation/test/autotest_webaccess.py +571 -0
  187. package/templates/qoder/scripts/{benchmark.py → validation/test/benchmark.py} +17 -5
  188. package/templates/qoder/scripts/{kg_auto_login.py → validation/test/kg_auto_login.py} +208 -196
  189. package/templates/qoder/scripts/{kg_test_runner.py → validation/test/kg_test_runner.py} +13 -2
  190. package/templates/qoder/scripts/{page_probe.py → validation/test/page_probe.py} +470 -459
  191. package/templates/qoder/scripts/validation/test/smoke_all_commands.py +170 -0
  192. package/templates/qoder/settings.json +23 -2
  193. package/templates/qoder/skills/design-import/SKILL.md +235 -226
  194. package/templates/qoder/skills/design-review/SKILL.md +91 -82
  195. package/templates/qoder/skills/prd-generator/SKILL.md +38 -24
  196. package/templates/qoder/skills/prd-review/SKILL.md +8 -6
  197. package/templates/qoder/skills/prototype-generator/SKILL.md +296 -247
  198. package/templates/qoder/skills/spec-coder/SKILL.md +24 -15
  199. package/templates/qoder/skills/spec-generator/SKILL.md +24 -12
  200. package/templates/qoder/skills/test-generator/SKILL.md +9 -7
  201. package/templates/qoder/skills/wl-code/SKILL.md +25 -13
  202. package/templates/qoder/skills/wl-commit/SKILL.md +10 -9
  203. package/templates/qoder/skills/wl-design/SKILL.md +7 -5
  204. package/templates/qoder/skills/wl-init/SKILL.md +8 -8
  205. package/templates/qoder/skills/wl-insight/SKILL.md +25 -12
  206. package/templates/qoder/skills/wl-prd-full/SKILL.md +59 -8
  207. package/templates/qoder/skills/wl-prd-quick/SKILL.md +7 -7
  208. package/templates/qoder/skills/wl-prd-review/SKILL.md +18 -6
  209. package/templates/qoder/skills/wl-report/SKILL.md +27 -25
  210. package/templates/qoder/skills/wl-search/SKILL.md +151 -80
  211. package/templates/qoder/skills/wl-spec/SKILL.md +9 -7
  212. package/templates/qoder/skills/wl-status/SKILL.md +50 -18
  213. package/templates/qoder/skills/wl-task/SKILL.md +94 -12
  214. package/templates/qoder/skills/wl-test/SKILL.md +154 -45
  215. package/templates/qoder/templates/prd-full-template.md +7 -0
  216. package/templates/root/AGENTS.md +275 -237
  217. package/templates/root/requirements.txt +3 -0
  218. package/templates/root//344/275/277/347/224/250/350/257/264/346/230/216.md +3 -3
  219. package/templates/root//346/226/260/346/211/213/346/214/207/345/215/227.md +2 -2
  220. package/templates/qoder/hooks/inject-workflow-state.py +0 -169
  221. package/templates/qoder/scripts/__pycache__/check_mcp_launch.cpython-39.pyc +0 -0
  222. package/templates/qoder/scripts/__pycache__/install_qoderwork.cpython-39.pyc +0 -0
  223. package/templates/qoder/scripts/__pycache__/mcp_launcher.cpython-39.pyc +0 -0
  224. package/templates/qoder/scripts/__pycache__/platform_doctor.cpython-39.pyc +0 -0
  225. package/templates/qoder/scripts/check_carriers.py +0 -238
  226. package/templates/qoder/scripts/check_mcp.py +0 -298
  227. package/templates/qoder/scripts/check_mcp_launch.py +0 -183
  228. package/templates/qoder/scripts/check_qoderwork_consistency.py +0 -166
  229. package/templates/qoder/scripts/collect_prds.py +0 -31
  230. package/templates/qoder/scripts/common/mentions.py +0 -134
  231. package/templates/qoder/scripts/common/utf8.py +0 -38
  232. package/templates/qoder/scripts/extract_api_params.py +0 -246
  233. package/templates/qoder/scripts/extract_routes.py +0 -54
  234. package/templates/qoder/scripts/extract_routes_tree.py +0 -78
  235. package/templates/qoder/scripts/handoff.py +0 -22
  236. package/templates/qoder/scripts/init_developer.py +0 -76
  237. package/templates/qoder/scripts/kg_mcp_server.py +0 -801
  238. package/templates/qoder/scripts/kg_semantic.py +0 -150
  239. package/templates/qoder/scripts/mcp_launcher.py +0 -414
  240. package/templates/qoder/scripts/mysql_mcp_server.py +0 -396
  241. package/templates/qoder/scripts/parse_prds.py +0 -33
  242. package/templates/qoder/scripts/role.py +0 -51
  243. package/templates/qoder/scripts/sync_carriers.py +0 -259
  244. package/templates/qoder/scripts/task.py +0 -1261
  245. package/templates/qoder/scripts/workspace_init.py +0 -102
  246. package/templates/qoder/scripts/zentao_mcp_server.py +0 -424
  247. package/templates/qoder/skills/prompt-enrich/SKILL.md +0 -90
  248. /package/templates/qoder/scripts/{common → deployment}/__init__.py +0 -0
  249. /package/templates/qoder/{skills/prototype-generator/SKILL.md.zcode-79180-2af4721f-f9a6-412c-88db-c0af680d211b.tmp → scripts/domain/__init__.py} +0 -0
  250. /package/templates/qoder/scripts/{common → domain/kg/graph}/graph_traverse.py +0 -0
  251. /package/templates/qoder/scripts/{common → domain/kg/server}/repowiki.py +0 -0
  252. /package/templates/qoder/scripts/{common → foundation/io}/atomicio.py +0 -0
  253. /package/templates/qoder/scripts/{secure-ls.js → validation/test/secure-ls.js} +0 -0
@@ -0,0 +1,220 @@
1
+ # -*- coding: utf-8 -*-
2
+ """
3
+ zentao_client.py —— 禅道 HTTP 客户端(唯一真相源)。
4
+
5
+ 消除重复: zentao_sync.py (CLI 批量同步) 和 zentao_mcp_server.py (MCP server)
6
+ 原本各自实现了一份禅道 HTTP 层(内网探测/登录/token缓存/API/401重登),
7
+ 现统一收敛到本类。
8
+
9
+ 设计:
10
+ - ZentaoClient(url, user, password) —— 显式构造
11
+ - ZentaoClient.from_env() —— 从 ZENTAO_URL/USER/PASSWORD 环境变量 (mcp_launcher 注入)
12
+ - ZentaoClient.from_config() —— 从 .qoder/config.yaml 的 zentao 段 (CLI 用)
13
+ - 凭据不全时工厂返回 None(调用方自行降级/报错)
14
+
15
+ client.check_intranet() -> (ok, msg) 带缓存,避免连续 ping
16
+ client.api(method, path, data) -> Response 内含 401 自动重登
17
+
18
+ 与 nexus 的关系:
19
+ 禅道 MCP server (zentao_mcp_server.py) 是 nexus L1 层的 MCP 资产。
20
+ 本 client 是其 HTTP 底座;未来 McpCap 实现后,工作流统一走
21
+ nexus.resolve().mcp.call("list_execution_tasks"),本 client 仍是底层实现。
22
+ """
23
+ from __future__ import annotations
24
+
25
+ import os
26
+ import socket
27
+ from typing import Optional
28
+
29
+
30
+ class ZentaoClient:
31
+ """禅道 REST API 客户端(登录/token/401重登/内网探测)。
32
+
33
+ Attributes:
34
+ url: 禅道根地址(构造时已去尾斜杠)。
35
+ user/password: 账号密码。
36
+ """
37
+
38
+ def __init__(self, url: str, user: str, password: str, intranet_ttl: int = 60):
39
+ self.url = (url or '').rstrip('/')
40
+ self.user = (user or '').strip()
41
+ self.password = password or ''
42
+ self._token: Optional[str] = None
43
+ # 内网可达性缓存 [timestamp, ok, msg],list 原地写避免 global 声明
44
+ self._intranet_cache: list = []
45
+ self._intranet_ttl = intranet_ttl
46
+
47
+ # ------------------------------------------------------------------
48
+ # 工厂
49
+ # ------------------------------------------------------------------
50
+
51
+ @classmethod
52
+ def from_env(cls) -> Optional['ZentaoClient']:
53
+ """从环境变量构造(mcp_launcher 注入)。凭据不全返回 None。"""
54
+ url = os.environ.get('ZENTAO_URL', '').strip()
55
+ user = os.environ.get('ZENTAO_USER', '').strip()
56
+ pwd = os.environ.get('ZENTAO_PASSWORD', '')
57
+ if not url or not user or not pwd:
58
+ return None
59
+ return cls(url, user, pwd)
60
+
61
+ @classmethod
62
+ def from_config(cls) -> Optional['ZentaoClient']:
63
+ """从 .qoder/config.yaml 的 zentao 段构造(CLI 用)。凭据不全返回 None。"""
64
+ try:
65
+ from foundation.core.config import load_config_section, SECTIONS
66
+ cfg = load_config_section(SECTIONS.ZENTAO)
67
+ except Exception:
68
+ return None
69
+ url = (cfg.get('url') or '').strip()
70
+ user = (cfg.get('user') or '').strip()
71
+ pwd = (cfg.get('password') or '')
72
+ if not url or not user or not pwd:
73
+ return None
74
+ return cls(url, user, pwd)
75
+
76
+ # ------------------------------------------------------------------
77
+ # 校验
78
+ # ------------------------------------------------------------------
79
+
80
+ def credentials_ok(self) -> tuple[bool, str]:
81
+ """凭据完整性校验。返回 (ok, msg)。"""
82
+ missing = []
83
+ if not self.url:
84
+ missing.append('url')
85
+ if not self.user:
86
+ missing.append('user')
87
+ if not self.password:
88
+ missing.append('password')
89
+ if missing:
90
+ return False, '禅道凭据未配置 (缺 %s)。请在 .qoder/config.yaml 的 zentao: 段填写。' % '/'.join(missing)
91
+ return True, ''
92
+
93
+ # ------------------------------------------------------------------
94
+ # 内网探测
95
+ # ------------------------------------------------------------------
96
+
97
+ def _ping_target(self) -> tuple[str, int]:
98
+ """从 url 解析 (host, port)。"""
99
+ host = self.url.replace('http://', '').replace('https://', '').split('/')[0]
100
+ try:
101
+ port = 80
102
+ if self.url.count(':') >= 2:
103
+ port = int(self.url.split(':')[2].split('/')[0])
104
+ except (ValueError, IndexError):
105
+ port = 80
106
+ return host, port
107
+
108
+ def check_intranet(self) -> tuple[bool, str]:
109
+ """内网连通性(带 TTL 缓存,失败不缓存)。
110
+
111
+ 多工具连续调用时只 ping 一次。失败不缓存,便于网络恢复后立即重试。
112
+ """
113
+ import time
114
+ now = time.time()
115
+ cached = self._intranet_cache
116
+ if cached and now - cached[0] < self._intranet_ttl and cached[1]:
117
+ return cached[1], cached[2]
118
+ host, port = self._ping_target()
119
+ try:
120
+ with socket.create_connection((host, port), timeout=3):
121
+ self._intranet_cache[:] = [now, True, '']
122
+ return True, ''
123
+ except Exception as e:
124
+ return False, '无法连接禅道 (%s): %s。请确认已连内网。' % (self.url, str(e)[:60])
125
+
126
+ # ------------------------------------------------------------------
127
+ # HTTP
128
+ # ------------------------------------------------------------------
129
+
130
+ @staticmethod
131
+ def _ensure_requests():
132
+ """延迟导入 requests(未装时报清晰错误,而非启动即崩)。"""
133
+ try:
134
+ import requests
135
+ return requests
136
+ except ImportError:
137
+ raise RuntimeError('缺少 requests 库。请运行: pip install requests')
138
+
139
+ def login(self) -> str:
140
+ """登录拿 token(进程内缓存)。"""
141
+ if self._token:
142
+ return self._token
143
+ requests = self._ensure_requests()
144
+ r = requests.post(self.url + '/api.php/v1/tokens',
145
+ json={'account': self.user, 'password': self.password},
146
+ timeout=10)
147
+ if r.status_code in (200, 201):
148
+ self._token = r.json().get('token')
149
+ return self._token
150
+ raise RuntimeError('禅道登录失败: %s %s' % (r.status_code, r.text[:100]))
151
+
152
+ def api(self, method: str, path: str, data: dict | None = None):
153
+ """调禅道 REST API。401 自动重登一次。
154
+
155
+ Args:
156
+ method: GET/POST/PUT/DELETE
157
+ path: /api.php/v1 之后的路径(如 /executions/12/tasks)
158
+ data: GET 时作 query params,POST/PUT 时作 json body
159
+ Returns:
160
+ requests.Response
161
+ """
162
+ requests = self._ensure_requests()
163
+ token = self.login()
164
+ full = self.url + '/api.php/v1' + path
165
+ kwargs = dict(
166
+ headers={'Token': token},
167
+ json=data if method in ('POST', 'PUT') else None,
168
+ params=data if method == 'GET' else None,
169
+ timeout=15,
170
+ )
171
+ r = requests.request(method, full, **kwargs)
172
+ if r.status_code == 401:
173
+ self._token = None
174
+ token = self.login()
175
+ kwargs['headers'] = {'Token': token}
176
+ r = requests.request(method, full, **kwargs)
177
+ return r
178
+
179
+ def upload_file(self, file_path: str, title: str = '', extra_fields: dict | None = None):
180
+ """上传文件到禅道 (multipart/form-data, POST /api.php/v1/files)。
181
+
182
+ 禅道文件接口需 multipart 而非 JSON, api() 不支持, 故单独实现。
183
+ 用于: 上传原型图 HTML / PRD 附件到需求。
184
+
185
+ Args:
186
+ file_path: 本地文件绝对路径
187
+ title: 文件标题 (可选, 默认取文件名)
188
+ extra_fields: 额外表单字段 (如 {'objectType':'story','objectID':812} 关联到对象)
189
+ Returns:
190
+ requests.Response
191
+ """
192
+ import os
193
+ requests = self._ensure_requests()
194
+ token = self.login()
195
+ full = self.url + '/api.php/v1/files'
196
+ fname = os.path.basename(file_path)
197
+ title = title or fname
198
+ fields = {'title': title}
199
+ if extra_fields:
200
+ fields.update(extra_fields)
201
+ with open(file_path, 'rb') as f:
202
+ files = {'file': (fname, f, 'application/octet-stream')}
203
+ r = requests.post(full, headers={'Token': token},
204
+ data=fields, files=files, timeout=30)
205
+ if r.status_code == 401:
206
+ self._token = None
207
+ token = self.login()
208
+ with open(file_path, 'rb') as f:
209
+ files = {'file': (fname, f, 'application/octet-stream')}
210
+ r = requests.post(full, headers={'Token': token},
211
+ data=fields, files=files, timeout=30)
212
+ return r
213
+
214
+ @staticmethod
215
+ def err_text(r) -> str:
216
+ """从 response 提取错误文本(容错)。"""
217
+ try:
218
+ return r.json().get('error', r.text[:120])
219
+ except Exception:
220
+ return r.text[:120]
@@ -6,7 +6,7 @@ filelock.py - 跨平台原子文件锁
6
6
  Windows / macOS / Linux 均无需管理员权限。
7
7
 
8
8
  用法:
9
- from common.filelock import FileLock
9
+ from foundation.io.filelock import FileLock
10
10
  with FileLock('.qoder/.runtime/team-sync.lock', timeout=30):
11
11
  # 临界区:同一时刻只有一个进程能进入
12
12
  do_critical_work()
@@ -36,7 +36,7 @@ if _THIS_DIR not in sys.path:
36
36
  try:
37
37
  from .atomicio import atomic_write_json, safe_read_json
38
38
  from .filelock import FileLock, LockTimeoutError
39
- from .paths import get_repo_root
39
+ from foundation.core.paths import get_repo_root
40
40
  except ImportError:
41
41
  # 被当作脚本直接运行时
42
42
  from atomicio import atomic_write_json, safe_read_json
@@ -0,0 +1,39 @@
1
+ # -*- coding: utf-8 -*-
2
+ """
3
+ duckdb_conn —— DuckDB 连接基础设施(L0 基础设施层)。
4
+
5
+ 设计模式: Utility(纯连接管理,无领域语义)
6
+
7
+ 职责:打开/关闭 kg.duckdb 连接。不知道表结构,不知道业务逻辑。
8
+ 被 L2(nexus)和 L3(kg/learning)共用,避免 L2 反向依赖 L3。
9
+
10
+ init_schema 等领域 DDL 留在各域(kg_duckdb.py 定义图谱表,
11
+ nexus/adapters/mcp.py 定义自己的 learning_events 表)。
12
+ """
13
+ import os
14
+
15
+ from foundation.core.paths import DATA_INDEX_DIR
16
+
17
+ DB_PATH = str(DATA_INDEX_DIR / 'kg.duckdb')
18
+
19
+
20
+ def get_db():
21
+ """Open DuckDB read-write connection (creates file if not exists)."""
22
+ os.makedirs(os.path.dirname(DB_PATH), exist_ok=True)
23
+ import duckdb
24
+ return duckdb.connect(DB_PATH)
25
+
26
+
27
+ def get_db_readonly():
28
+ """Open read-only DuckDB connection. Returns None if file missing.
29
+
30
+ DuckDB 不允许对同一文件混用 read_only 和 read_write 连接,
31
+ 所以只读路径必须全程用 read_only。
32
+ """
33
+ import duckdb
34
+ if not os.path.isfile(DB_PATH):
35
+ return None
36
+ try:
37
+ return duckdb.connect(DB_PATH, read_only=True)
38
+ except Exception:
39
+ return None
@@ -0,0 +1,268 @@
1
+ # -*- coding: utf-8 -*-
2
+ """
3
+ mcp_base.py - MCP Server 基类 (Template Method 模式)
4
+
5
+ 治"3个MCP server协议层逐行复制~255行"。三者(kg/mysql/zentao)的:
6
+ - _send (JSON-RPC写)
7
+ - main (readline循环 + 每请求新event_loop)
8
+ - _handle 五分支骨架 (initialize/notifications/initialized/tools·list/tools·call/ping/error)
9
+ - stdout重定向 (stdout→stderr, 避免污染stdio协议)
10
+ 全部逐字一致, 仅差异点:
11
+ - serverInfo.name/version
12
+ - initialize 是否调 check_intranet 塞 warning
13
+ - tools/call 的异常分类 (mysql 多 ValueError→只读拒绝)
14
+ - error 分支文案
15
+
16
+ 本基类把骨架固化为 final 方法, 子类填钩子:
17
+ - SERVER_NAME / SERVER_VERSION: 类属性
18
+ - TOOLS: 工具元数据列表
19
+ - handle_tool(name, args) -> str: 工具执行 (Strategy 钩子)
20
+ - on_initialize_hook(info) -> None: 塞 warning 等 (默认空)
21
+ - on_tool_error(e) -> (text, is_error): 自定义异常分类 (默认通用)
22
+
23
+ Usage (子类示例):
24
+ from foundation.protocol.mcp_base import BaseMCPServer
25
+
26
+ class ZentaoMCPServer(BaseMCPServer):
27
+ SERVER_NAME = "qoder-zentao"
28
+ SERVER_VERSION = "1.0.0"
29
+ TOOLS = [...] # 工具元数据
30
+ def handle_tool(self, name, args):
31
+ if name == 'list_stories': return self._list_stories(args)
32
+ return '未知工具: %s' % name
33
+ def on_initialize_hook(self, info):
34
+ ok, msg = check_intranet()
35
+ if not ok: info['warning'] = msg
36
+
37
+ if __name__ == '__main__':
38
+ ZentaoMCPServer().run()
39
+
40
+ 收益: 加第4个MCP server从复制~255行降到写~30行(只填tools+handle_tool)。
41
+
42
+ v3.0+ 增强: ToolRegistry + @tool 装饰器 (Command Registry 模式)。
43
+ 消除子类 _execute_tool 的长 if-elif 链 + TOOLS 元数据两处维护的漂移。
44
+ 用法见底部示例。
45
+ """
46
+
47
+ import asyncio
48
+ import json
49
+ import os
50
+ import sys
51
+ from typing import Any, Dict, List, Optional, Tuple
52
+
53
+ __all__ = ["BaseMCPServer", "tool", "ToolRegistry"]
54
+
55
+
56
+ # ============================================================
57
+ # ToolRegistry: @tool 装饰器收集工具定义 (Command Registry 模式)
58
+ # ============================================================
59
+
60
+ def tool(name: str, description: str = "", input_schema: Optional[Dict] = None):
61
+ """装饰器: 把一个方法注册为 MCP 工具。
62
+
63
+ 替代旧式的 "TOOLS 列表 + _execute_tool if 链 两处维护"。
64
+ 装饰后, BaseMCPServer.TOOLS 自动收集, handle_tool 自动分发。
65
+
66
+ Usage (子类内):
67
+ @tool("search_code", "搜代码", {"type":"object","properties":{...}})
68
+ def search_code(self, args):
69
+ return "结果文本"
70
+ """
71
+ def decorator(method):
72
+ method._tool_meta = {
73
+ "name": name,
74
+ "description": description or method.__doc__ or "",
75
+ "inputSchema": input_schema or {"type": "object", "properties": {}},
76
+ }
77
+ return method
78
+ return decorator
79
+
80
+
81
+ class _ToolRegistryMeta(type):
82
+ """元类: 子类定义完成后, 扫描 @tool 装饰的方法, 自动填充 TOOLS + _tool_map。
83
+
84
+ 这样子类只需用 @tool 装饰, 不用再手写 TOOLS 列表和 handle_tool if 链。
85
+ """
86
+ def __new__(mcs, name, bases, namespace):
87
+ cls = super().__new__(mcs, name, bases, namespace)
88
+ # 收集本类 + 父类的 @tool 方法
89
+ tool_map = {}
90
+ # 先继承父类的
91
+ for base in bases:
92
+ if hasattr(base, "_tool_map"):
93
+ tool_map.update(base._tool_map)
94
+ # 再收集本类的
95
+ for attr_name, attr_val in namespace.items():
96
+ meta = getattr(attr_val, "_tool_meta", None)
97
+ if meta:
98
+ tool_map[meta["name"]] = {
99
+ "meta": meta,
100
+ "handler": attr_name, # 方法名, 运行时 getattr 取
101
+ }
102
+ cls._tool_map = tool_map
103
+ # 如果子类没显式定义 TOOLS (用装饰器模式), 自动生成
104
+ if tool_map and not namespace.get("TOOLS"):
105
+ cls.TOOLS = [v["meta"] for v in tool_map.values()]
106
+ return cls
107
+
108
+
109
+ class BaseMCPServer(metaclass=_ToolRegistryMeta):
110
+ """MCP stdio server 基类 (Template Method + ToolRegistry)。
111
+
112
+ 两种用法 (二选一):
113
+ A) 装饰器模式 (推荐, 消除 if 链):
114
+ class MyServer(BaseMCPServer):
115
+ SERVER_NAME = "my"
116
+ @tool("foo", "做foo", {...})
117
+ def foo(self, args): return "结果"
118
+ # TOOLS 自动收集, handle_tool 自动分发, 无需覆写
119
+ B) 传统模式 (兼容旧代码):
120
+ class MyServer(BaseMCPServer):
121
+ SERVER_NAME = "my"
122
+ TOOLS = [...] # 手写
123
+ def handle_tool(self, name, args): ... # 手写 if 链
124
+ """
125
+
126
+ # ---- 子类必填类属性 ----
127
+ SERVER_NAME: str = "qoder-mcp"
128
+ SERVER_VERSION: str = "0.1.0"
129
+ TOOLS: List[Dict[str, Any]] = []
130
+ _tool_map: Dict[str, Dict] = {} # 由元类自动填充
131
+
132
+ # ---- 协议常量 (可覆写) ----
133
+ PROTOCOL_VERSION: str = "2024-11-05"
134
+
135
+ # ============================================================
136
+ # 内部: stdout 重定向 (MCP 用 stdout 传 JSON-RPC, 不能被 print 污染)
137
+ # ============================================================
138
+ _real_stdout = None
139
+
140
+ @classmethod
141
+ def _setup_streams(cls):
142
+ """把 sys.stdout 存起来, 重定向 sys.stdout→stderr (子进程的 print 进 stderr)。"""
143
+ if cls._real_stdout is None:
144
+ cls._real_stdout = sys.stdout
145
+ sys.stdout = sys.stderr
146
+
147
+ # ============================================================
148
+ # final: 协议发送
149
+ # ============================================================
150
+ @classmethod
151
+ def _send(cls, msg: Dict[str, Any]) -> None:
152
+ """发送 JSON-RPC 消息到真实 stdout (跨3个server逐字一致)。"""
153
+ cls._real_stdout.write(json.dumps(msg, ensure_ascii=False) + "\n")
154
+ cls._real_stdout.flush()
155
+
156
+ # ============================================================
157
+ # 默认 handle_tool: 从 _tool_map 自动分发 (装饰器模式)
158
+ # ============================================================
159
+ def handle_tool(self, name: str, args: Dict[str, Any]) -> str:
160
+ """[默认实现] 从 ToolRegistry 取 handler 执行。
161
+
162
+ - 若用了 @tool 装饰器: 自动分发, 子类不覆写
163
+ - 若传统模式 (子类覆写了 handle_tool): 走子类的 if 链
164
+ """
165
+ entry = self._tool_map.get(name)
166
+ if entry:
167
+ handler = getattr(self, entry["handler"])
168
+ return handler(args)
169
+ return "未知工具: %s" % name
170
+
171
+ def on_initialize_hook(self, info: Dict[str, Any]) -> None:
172
+ """[可选覆写] initialize 时往 serverInfo 塞额外信息 (如 warning)。
173
+
174
+ 默认空。mysql/zentao 在此调 check_intranet 塞 warning。
175
+ """
176
+ pass
177
+
178
+ def on_tool_error(self, e: Exception) -> Tuple[str, bool]:
179
+ """[可选覆写] tools/call 抛异常时的分类。
180
+
181
+ Returns:
182
+ (text, is_error): 返回给客户端的文本 + 是否标记为错误
183
+ 默认: 通用异常 → "[error] xxx" + is_error=True。
184
+ mysql 覆写此方法把 ValueError(只读拒绝)单独分类。
185
+ """
186
+ return ("[error] %s" % str(e)[:200], True)
187
+
188
+ # ============================================================
189
+ # final: 请求处理骨架 (Template Method)
190
+ # ============================================================
191
+ async def _handle(self, req: Dict[str, Any]) -> None:
192
+ """处理一个 JSON-RPC 请求 (五分支骨架, 子类不覆写)。"""
193
+ method = req.get("method", "")
194
+ req_id = req.get("id")
195
+ params = req.get("params", {}) or {}
196
+
197
+ if method == "initialize":
198
+ info = {"name": self.SERVER_NAME, "version": self.SERVER_VERSION}
199
+ self.on_initialize_hook(info) # 钩子: 子类塞 warning
200
+ self._send({
201
+ "jsonrpc": "2.0", "id": req_id,
202
+ "result": {
203
+ "protocolVersion": self.PROTOCOL_VERSION,
204
+ "capabilities": {"tools": {}},
205
+ "serverInfo": info,
206
+ },
207
+ })
208
+ elif method == "notifications/initialized":
209
+ pass
210
+ elif method == "tools/list":
211
+ self._send({"jsonrpc": "2.0", "id": req_id, "result": {"tools": self.TOOLS}})
212
+ elif method == "tools/call":
213
+ tool_name = params.get("name", "")
214
+ arguments = params.get("arguments", {}) or {}
215
+ try:
216
+ result_text = await asyncio.get_event_loop().run_in_executor(
217
+ None, self.handle_tool, tool_name, arguments
218
+ )
219
+ self._send({
220
+ "jsonrpc": "2.0", "id": req_id,
221
+ "result": {
222
+ "content": [{"type": "text", "text": result_text}],
223
+ "isError": False,
224
+ },
225
+ })
226
+ except Exception as e:
227
+ text, is_error = self.on_tool_error(e)
228
+ self._send({
229
+ "jsonrpc": "2.0", "id": req_id,
230
+ "result": {
231
+ "content": [{"type": "text", "text": text}],
232
+ "isError": is_error,
233
+ },
234
+ })
235
+ elif method == "ping":
236
+ self._send({"jsonrpc": "2.0", "id": req_id, "result": {}})
237
+ elif req_id is not None:
238
+ self._send({
239
+ "jsonrpc": "2.0", "id": req_id,
240
+ "error": {"code": -32601, "message": "Method not found: %s" % method},
241
+ })
242
+
243
+ # ============================================================
244
+ # final: 主循环
245
+ # ============================================================
246
+ def run(self) -> None:
247
+ """启动 stdio 主循环 (readline → 解析 → _handle)。子类不覆写。"""
248
+ self._setup_streams()
249
+ tag = "[%s]" % self.SERVER_NAME
250
+ while True:
251
+ try:
252
+ line = sys.stdin.buffer.readline()
253
+ except (EOFError, OSError):
254
+ break
255
+ if not line:
256
+ break
257
+ line = line.strip()
258
+ if not line:
259
+ continue
260
+ try:
261
+ req = json.loads(line.decode("utf-8"))
262
+ loop = asyncio.new_event_loop()
263
+ loop.run_until_complete(self._handle(req))
264
+ loop.close()
265
+ except json.JSONDecodeError:
266
+ pass
267
+ except Exception as e:
268
+ print("%s error: %s" % (tag, str(e)[:80]), file=sys.stderr)
@@ -26,6 +26,7 @@ eval_prd.py 的 main() 也改为调用本模块。
26
26
 
27
27
  import os
28
28
  import sys
29
+ import re
29
30
 
30
31
  _THIS_DIR = os.path.dirname(os.path.abspath(__file__))
31
32
  _SCRIPTS_DIR = os.path.dirname(_THIS_DIR)
@@ -33,7 +34,7 @@ if _SCRIPTS_DIR not in sys.path:
33
34
  sys.path.insert(0, _SCRIPTS_DIR)
34
35
 
35
36
  # 复用 eval_prd.py 的评分函数 (不重复实现)
36
- from eval_prd import (
37
+ from validation.metrics.eval_prd import (
37
38
  score_reality, score_style, score_template, read_text,
38
39
  FULL_SECTIONS, QUICK_SECTIONS,
39
40
  )
@@ -75,6 +76,16 @@ def evaluate(prd_path, html_path=None):
75
76
  try:
76
77
  r2, msg2, miss2 = score_style(read_text(html_path))
77
78
  a2 = round(r2 * 30)
79
+ # v3.1 来源锚定检查: HTML 必须含 @prototype-source 或 x-source-pages 注释
80
+ # 没有 = AI 没做 Step 2 真实页面锚定, 直接从模板手抄 → 扣 5 分
81
+ html_text = read_text(html_path)
82
+ has_source = bool(re.search(r'@prototype-source:|x-source-pages', html_text))
83
+ if not has_source:
84
+ a2 = max(0, a2 - 5)
85
+ msg2 = (msg2 or '') + '; 缺来源锚定(@prototype-source 注释), 扣 5 分'
86
+ if miss2 is None:
87
+ miss2 = []
88
+ miss2 = list(miss2) + ['缺 @prototype-source 注释']
78
89
  except Exception as e:
79
90
  r2, msg2, miss2 = 0.0, "评估异常: %s" % e, []
80
91
  a2 = 0
@@ -35,17 +35,34 @@ def _run_pip(args, index_url=None, timeout=MIRROR_TIMEOUT):
35
35
  """跑一次 pip, 返回 (returncode, stdout, stderr)。
36
36
  index_url 非 None 时加 -i 指定镜像源。
37
37
  用 trusted-host 避免某些镜像的 SSL 证书警告阻断。
38
+
39
+ ⚠ 不用 subprocess.run / communicate:
40
+ Anaconda Python 3.9 + Windows 下, subprocess 的 communicate() 内部 join 读子进程
41
+ 输出的线程时, 会触发 "PyEval_SaveThread: GIL released" 致命崩溃
42
+ (pip 跑 tree-sitter/playwright/ddddocr 这类大包时尤其稳定复现)。
43
+ 50 人不同环境都要跑这步, 必须跨版本稳。
44
+
45
+ 解法: Popen + wait(), stdout/stderr 全丢 DEVNULL。
46
+ pip 的啰嗦输出对我们无用 (成功与否以 _verify_packages 的 import 验证为准),
47
+ 丢了反而避免大输出撑爆管道 + 触发 GIL 崩溃。超时用独立的计时线程判, 不走 communicate。
38
48
  """
39
- cmd = [sys.executable, '-m', 'pip', 'install'] + args
49
+ cmd = [sys.executable, '-m', 'pip', 'install', '--no-input',
50
+ '--progress-bar', 'off'] + args
40
51
  if index_url:
41
52
  cmd += ['-i', index_url,
42
53
  '--trusted-host', _host_of(index_url)]
43
54
  try:
44
- r = subprocess.run(cmd, capture_output=True, text=True,
45
- encoding='utf-8', errors='replace', timeout=timeout)
46
- return r.returncode, r.stdout or '', r.stderr or ''
47
- except subprocess.TimeoutExpired:
48
- return 1, '', '安装超时 (%ds)' % timeout
55
+ proc = subprocess.Popen(cmd, stdout=subprocess.DEVNULL,
56
+ stderr=subprocess.DEVNULL)
57
+ try:
58
+ rc = proc.wait(timeout=timeout)
59
+ return rc, '', ''
60
+ except subprocess.TimeoutExpired:
61
+ try:
62
+ proc.kill()
63
+ except Exception:
64
+ pass
65
+ return 1, '', '安装超时 (%ds)' % timeout
49
66
  except Exception as e:
50
67
  return 1, '', str(e)
51
68
 
@@ -26,7 +26,7 @@ def _platform_explicitly_passed(argv=None):
26
26
  return '--platform' in argv
27
27
 
28
28
 
29
- def assert_platform_selected(skill='wl-prd-full', argv=None):
29
+ def assert_platform_selected(skill='wl-prd', argv=None):
30
30
  """硬墙: 没显式传 --platform 就调用 PRD 链路脚本 = 违规。
31
31
 
32
32
  1. best-effort 上报 rule_violation 事件 (不阻塞, 失败静默)