@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
@@ -1,5 +1,16 @@
1
1
  #!/usr/bin/env python3
2
2
  # -*- coding: utf-8 -*-
3
+ # v3.0 路径自举: 引导到 common/bootstrap, 统一 sys.path 逻辑
4
+ import os as _o, sys as _s
5
+ _f = _o.path.abspath(__file__)
6
+ for _ in range(10):
7
+ _f = _o.path.dirname(_f)
8
+ _cp = _o.path.join(_f, 'foundation')
9
+ if _o.path.isfile(_o.path.join(_cp, 'bootstrap.py')):
10
+ break
11
+ if _cp not in _s.path: _s.path.insert(0, _cp)
12
+ from bootstrap import setup; setup()
13
+
3
14
  """
4
15
  QODER Team Sync - 无感 git 同步引擎
5
16
 
@@ -34,8 +45,8 @@ if sys.platform == 'win32':
34
45
  pass
35
46
 
36
47
  sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
37
- from common.paths import get_repo_root, get_developer
38
- from common.filelock import FileLock, LockTimeoutError
48
+ from foundation.core.paths import get_repo_root, get_developer
49
+ from foundation.io.filelock import FileLock, LockTimeoutError
39
50
  from syncgate import run_gates, SAFE_EXTENSIONS
40
51
 
41
52
  BASE = str(get_repo_root())
@@ -49,16 +60,31 @@ PUSH_LOCK = os.path.join(BASE, '.qoder', '.runtime', 'team-sync.lock')
49
60
  MAX_PUSH_RETRY = 3
50
61
 
51
62
 
52
- def git(*args, check=False):
53
- """git 命令包装。git 未安装时返回 rc=127 的伪结果, 不崩溃。"""
63
+ def git(*args, check=False, timeout=None):
64
+ """git 命令包装。git 未安装时返回 rc=127 的伪结果, 不崩溃。
65
+
66
+ timeout: 网络/远程操作 (fetch/pull/push/clone) 必须传, 防止认证提示或网络
67
+ 挂起导致整个 sync 无限阻塞。本地操作 (add/commit/status/diff) 不需要。
68
+ 用法: git('fetch', 'origin', timeout=60)
69
+ """
54
70
  try:
55
- r = subprocess.run(['git'] + list(args), cwd=BASE, capture_output=True,
56
- text=True, encoding='utf-8', errors='replace')
71
+ kw = dict(cwd=BASE, capture_output=True, text=True, encoding='utf-8', errors='replace')
72
+ if timeout:
73
+ kw['timeout'] = timeout
74
+ r = subprocess.run(['git'] + list(args), **kw)
57
75
  except FileNotFoundError:
58
76
  # git 未安装 — 返回伪 CompletedProcess, 让上层把"无 git"当"无远端/无仓库"处理
59
77
  import types
60
78
  r = types.SimpleNamespace(returncode=127, stdout='', stderr='git not installed',
61
79
  encoding='utf-8')
80
+ except subprocess.TimeoutExpired:
81
+ # 网络操作超时 (常见: 远程要认证但凭证没缓存 / 网络不通)
82
+ # 返回 rc=124 (与 timeout 命令一致), 不让整个 sync 挂死
83
+ import types
84
+ cmd_hint = ' '.join(args[:3])
85
+ r = types.SimpleNamespace(returncode=124, stdout='',
86
+ stderr='git {} timed out after {}s (网络/认证问题)'.format(cmd_hint, timeout),
87
+ encoding='utf-8')
62
88
  if check and r.returncode != 0:
63
89
  print('git {} failed: {}'.format(' '.join(args), r.stderr.strip()[:300]))
64
90
  return r
@@ -93,7 +119,7 @@ def _notify_prd_publications(staged_files):
93
119
  """
94
120
  import re as _re
95
121
  try:
96
- from common.feishu import notify_prd_published, is_enabled
122
+ from foundation.integrations.feishu import notify_prd_published, is_enabled
97
123
  except ImportError as e:
98
124
  print('[sync] WARN: feishu 模块缺失, 跳过 PRD 发布通知: %s' % e, file=sys.stderr)
99
125
  return
@@ -204,7 +230,8 @@ def _auto_resolve_kg_conflict():
204
230
 
205
231
 
206
232
  def do_pull(quiet=False):
207
- """拉取团队最新。安全: autostash 保护未提交改动。"""
233
+ """拉取团队最新。安全: autostash 保护未提交改动 + 文件锁防与 push 竞态。"""
234
+ # 前置检查(不需锁): 远程/rebase 状态快速失败, 避免占锁后才发现问题
208
235
  if rebase_in_progress():
209
236
  print('SYNC_CONFLICT: 仓库处于未完成的 rebase 状态, AI 请先处理 (git rebase --abort 或 --continue)。')
210
237
  return 3
@@ -219,8 +246,31 @@ def do_pull(quiet=False):
219
246
  print('ERROR: detached HEAD or no branch - AI please check.')
220
247
  return 1
221
248
 
222
- r = git('pull', '--rebase', '--autostash', 'origin', branch)
249
+ # === 文件锁: 与 do_push 互斥。pull --rebase --autostash 会动工作区 + index,
250
+ # 若与锁内的 _stage_scopes_safely (git add) 交叉会产生半 staged 状态。 ===
251
+ os.makedirs(os.path.dirname(PUSH_LOCK), exist_ok=True)
252
+ try:
253
+ lock_ctx = FileLock(PUSH_LOCK, timeout=60, stale_seconds=300)
254
+ lock_ctx.acquire()
255
+ except LockTimeoutError as e:
256
+ print('SYNC_BUSY: 另一个同步正在进行, 等待超时。稍后重试。')
257
+ print(' ' + str(e))
258
+ return 2 # lock contention
259
+
260
+ try:
261
+ return _do_pull_locked(quiet, branch)
262
+ finally:
263
+ lock_ctx.release()
264
+
265
+
266
+ def _do_pull_locked(quiet, branch):
267
+ """锁内的实际 pull 逻辑。"""
268
+ r = git('pull', '--rebase', '--autostash', 'origin', branch, timeout=90)
223
269
  if r.returncode != 0:
270
+ if r.returncode == 124:
271
+ print('Pull 超时 (网络/认证问题): ' + (r.stderr or '').strip()[:150])
272
+ print('检查 git 凭证或团队仓库连通性。继续离线工作, 下次同步自动补推。')
273
+ return 1
224
274
  if 'CONFLICT' in (r.stdout + r.stderr) or rebase_in_progress():
225
275
  return report_conflict(r.stderr or r.stdout)
226
276
  print('Pull failed (network/remote?): ' + (r.stderr or r.stdout).strip()[:200])
@@ -282,22 +332,25 @@ def _stage_scopes_safely(skip_secret=False):
282
332
  只 stage SYNC_SCOPES 下、扩展名在 SAFE_EXTENSIONS 里的文件。
283
333
  其他文件 (如误放的 .env/.zip/.docx) 被跳过并打印警告。
284
334
 
335
+ 性能: 收集后批量 git add (每批 ~400 个), 避免 N 个文件 = N 次 git 进程 fork。
336
+ 50 人团队大改动 (数百文件) 时, 逐文件 add 在 Windows CreateProcess 开销下
337
+ 会很慢。批量后 push 阶段从 O(N 次 fork) 降到 O(N/400 次 fork)。
338
+
285
339
  Returns:
286
340
  (staged_count, skipped_files)
287
341
  """
288
- staged = 0
342
+ to_add = [] # 待批量 add 的文件 (已收集, 一次性分批 add)
289
343
  skipped = []
290
344
  for scope in SYNC_SCOPES:
291
345
  scope_path = os.path.join(BASE, scope)
292
346
  if not os.path.isdir(scope_path):
293
347
  continue
294
348
  # 用 git ls-files 拿已跟踪的 + git status 拿未跟踪的, 逐个判断扩展名
295
- # 已跟踪文件: 直接 add (它们已经在版本控制, 不可能是新秘密)
349
+ # 已跟踪文件: 收集 (它们已经在版本控制, 不可能是新秘密)
296
350
  r = git('ls-files', '--', scope)
297
- tracked = [f for f in r.stdout.strip().splitlines() if f.strip()]
298
- for f in tracked:
299
- git('add', '--', f)
300
- staged += 1
351
+ for f in r.stdout.strip().splitlines():
352
+ if f.strip():
353
+ to_add.append(f)
301
354
  # 未跟踪的新文件: 按扩展名过滤
302
355
  r = git('status', '--porcelain', '--', scope)
303
356
  for line in r.stdout.strip().splitlines():
@@ -313,17 +366,24 @@ def _stage_scopes_safely(skip_secret=False):
313
366
  if 'brainstorm-agent' in basename:
314
367
  skipped.append(fpath + ' (brainstorm 中间文件, 不提交)')
315
368
  elif ext in SAFE_EXTENSIONS:
316
- git('add', '--', fpath)
317
- staged += 1
369
+ to_add.append(fpath)
318
370
  else:
319
371
  skipped.append(fpath)
320
372
  elif 'D' not in status:
321
- # 修改/重命名等 (非删除) —— 已跟踪, 直接 add
373
+ # 修改/重命名等 (非删除) —— 已跟踪, 收集
322
374
  # 重命名格式 "R old -> new", 取 new
323
375
  if '->' in fpath:
324
376
  fpath = fpath.split('->')[-1].strip().strip('"')
325
- git('add', '--', fpath)
326
- staged += 1
377
+ to_add.append(fpath)
378
+
379
+ # 批量 add: 每批最多 400 个文件 (防命令行长度溢出, Windows ~8191 字符限制留余量)
380
+ BATCH = 400
381
+ staged = 0
382
+ for i in range(0, len(to_add), BATCH):
383
+ batch = to_add[i:i + BATCH]
384
+ git('add', '--', *batch)
385
+ staged += len(batch)
386
+
327
387
  if skipped:
328
388
  print('[gate] 跳过 {} 个非白名单文件 (可能含二进制/秘密, 手动处理):'.format(len(skipped)))
329
389
  for s in skipped[:10]:
@@ -410,7 +470,7 @@ def _do_push_locked(message, dev, skip_eval, skip_secret):
410
470
  return 1
411
471
 
412
472
  # 5. 检查是否有待推送的提交
413
- git('fetch', 'origin', branch)
473
+ git('fetch', 'origin', branch, timeout=60)
414
474
  r = git('rev-list', '--count', 'origin/{}..HEAD'.format(branch))
415
475
  ahead = int(r.stdout.strip() or 0) if r.returncode == 0 else 1
416
476
  if ahead == 0:
@@ -420,7 +480,7 @@ def _do_push_locked(message, dev, skip_eval, skip_secret):
420
480
 
421
481
  # 6. pull --rebase + push, 失败重试
422
482
  for attempt in range(1, MAX_PUSH_RETRY + 1):
423
- r = git('pull', '--rebase', '--autostash', 'origin', branch)
483
+ r = git('pull', '--rebase', '--autostash', 'origin', branch, timeout=90)
424
484
  if r.returncode != 0:
425
485
  # kg.duckdb 二进制冲突: 自动解决 (用户无感)
426
486
  _auto_resolve_kg_conflict()
@@ -437,11 +497,22 @@ def _do_push_locked(message, dev, skip_eval, skip_secret):
437
497
  return report_conflict(r.stderr or r.stdout)
438
498
  # 非冲突的 pull 失败
439
499
  print('Pull failed (attempt {}): {}'.format(attempt, (r.stderr or r.stdout).strip()[:150]))
500
+ if r.returncode == 124:
501
+ print(' (网络/认证超时 — 检查 git 凭证或团队仓库连通性)')
502
+ break # 网络挂起重试无意义, 直接退出
503
+ # 连接性失败 (unable to connect / Could not resolve) 重试也是浪费时间,
504
+ # 直接退出让用户离线工作, 别让 50 人团队的弱网用户干等 3 倍时间
505
+ err_txt = (r.stderr or r.stdout or '').lower()
506
+ if any(k in err_txt for k in ('unable to connect', 'could not resolve',
507
+ 'connection refused', 'no route to host',
508
+ 'network is unreachable', 'timed out')):
509
+ print(' (网络不通 — 产出已本地提交, 下次同步自动补推)')
510
+ break
440
511
  if rebase_in_progress():
441
512
  git('rebase', '--abort')
442
513
  continue
443
514
 
444
- r = git('push', 'origin', branch)
515
+ r = git('push', 'origin', branch, timeout=90)
445
516
  if r.returncode == 0:
446
517
  touch_pull_marker()
447
518
  print('Synced to team. (attempt {})'.format(attempt))
@@ -465,6 +536,13 @@ def _do_push_locked(message, dev, skip_eval, skip_secret):
465
536
  pass # 埋点失败不阻塞同步
466
537
  return 0
467
538
  if attempt < MAX_PUSH_RETRY:
539
+ # 网络不通重试无意义, 别让弱网用户干等 (push 超时/连接失败直接退出)
540
+ err_txt = (r.stderr or r.stdout or '').lower()
541
+ if r.returncode == 124 or any(k in err_txt for k in (
542
+ 'unable to connect', 'could not resolve', 'connection refused',
543
+ 'no route to host', 'network is unreachable', 'timed out')):
544
+ print('Push failed (网络不通): 产出已本地提交, 下次同步自动补推。')
545
+ return 1
468
546
  print('Push rejected (someone pushed first?), retrying...')
469
547
 
470
548
  print('Push failed after {} attempts: 产出已本地提交不会丢失, 下次同步自动补推。'.format(MAX_PUSH_RETRY))
@@ -0,0 +1,370 @@
1
+ #!/usr/bin/env python3
2
+ # -*- coding: utf-8 -*-
3
+ """
4
+ zentao_sync.py - 本地任务 ↔ 禅道 双向同步(安全优先)
5
+
6
+ 设计目标 (回应团队最担心的「拉错/覆盖队友」):
7
+ - 拉取(禅道→本地): 强制 assignedTo == 我的禅道账号 过滤, 物理上拉不到队友任务。
8
+ - 推送(本地→禅道): 默认 --dry-run, 仅推 creator==我 的任务, 且禅道同名任务
9
+ assignedTo 须与我一致才更新, 不一致拒绝 (防误改队友任务)。
10
+ - 幂等: task.json 的 zentao_id 做主键, 已有则跳过 create。
11
+
12
+ 隔离契约: contracts/isolation.md「禅道同步」红线。
13
+ 归属映射: member.json 的 zentao_account 字段 (本地 dev 名 ↔ 禅道 account)。
14
+
15
+ 用法:
16
+ python .qoder/scripts/domain/task/zentao_sync.py pull <execution_id> [--apply] # 默认 dry-run 预览
17
+ python .qoder/scripts/domain/task/zentao_sync.py push [--execution_id N] [--apply]
18
+ python .qoder/scripts/domain/task/zentao_sync.py status # 看同步状态概览
19
+
20
+ 退出码:
21
+ 0 成功 (含 dry-run 预览); 2 busy; 3 配置/归属错误; 4 权限拒绝; 5 网络不通
22
+ """
23
+ import os
24
+ import sys
25
+
26
+ # ── 路径自举: 让脚本能独立运行 (复用 bootstrap 模式) ──
27
+ # task_utils/identity 用相对导入 (.paths), 必须从 common 包上下文导入,
28
+ # 因此把 scripts/ 加入 path, 用 common.xxx / task.xxx 访问。
29
+ _HERE = os.path.dirname(os.path.abspath(__file__))
30
+ _SCRIPTS = os.path.dirname(_HERE) # .qoder/scripts
31
+ _COMMON = os.path.join(_SCRIPTS, 'foundation')
32
+ for _p in (_SCRIPTS, _COMMON):
33
+ if _p not in sys.path:
34
+ sys.path.insert(0, _p)
35
+ try:
36
+ from bootstrap import setup; setup()
37
+ except Exception:
38
+ pass
39
+
40
+ import json
41
+ import argparse
42
+
43
+ from foundation.core.paths import get_repo_root, get_tasks_dir, get_developer
44
+ from foundation.identity.identity import get_member
45
+ from foundation.data.task_utils import modify_task_json, load_task_json, write_task_json
46
+ from foundation.integrations.zentao_client import ZentaoClient
47
+ from datetime import datetime
48
+ from pathlib import Path
49
+
50
+
51
+ def _create_local_task(repo_root, title, slug_seed, zentao_id, developer):
52
+ """直接创建本地任务目录 + task.json (含 zentao_id), 不走 cmd_create。
53
+
54
+ 自包含, 避免 cmd_create 的 argparse Namespace 签名 + team_sync stage 副作用。
55
+ 返回新建的 task_dir 或 None。"""
56
+ date_prefix = datetime.now().strftime("%m-%d")
57
+ tasks_dir = get_tasks_dir(repo_root)
58
+ task_dir_name = "%s-%s" % (date_prefix, slug_seed)
59
+ task_dir = tasks_dir / task_dir_name
60
+ if task_dir.exists():
61
+ # 撞名加后缀
62
+ task_dir_name = "%s-%s-%s" % (date_prefix, slug_seed, developer[:6])
63
+ task_dir = tasks_dir / task_dir_name
64
+ task_dir.mkdir(parents=True, exist_ok=True)
65
+ now_iso = datetime.now().isoformat()
66
+ task_data = {
67
+ "title": title, "slug": slug_seed, "status": "planning", "priority": "P2",
68
+ "assignee": developer, "creator": developer,
69
+ "created_at": now_iso, "updated_at": now_iso,
70
+ "branch": None, "base_branch": None, "scope": None,
71
+ "parent": None, "children": [], "tags": [],
72
+ "due_date": None, "start_date": None, "estimate_hours": None,
73
+ "blocked_by": [], "blocks": [],
74
+ "stage_ts": {"created": now_iso, "started": None, "completed": None},
75
+ "zentao_id": zentao_id,
76
+ }
77
+ if write_task_json(task_dir, task_data):
78
+ return task_dir
79
+ return None
80
+
81
+
82
+ # ============================================================
83
+ # 配置与归属解析
84
+ # ============================================================
85
+
86
+ def _zentao_creds():
87
+ """构建禅道客户端。凭据不全返回 None。
88
+ 底层 HTTP 逻辑收敛在 common.zentao_client.ZentaoClient (与 mcp_server 共享)。"""
89
+ return ZentaoClient.from_config()
90
+
91
+
92
+ def _resolve_my_account(developer):
93
+ """解析当前本地 developer → 禅道 account。
94
+ 无 member.json 或无 zentao_account → 返回 (None, 原因)。"""
95
+ mem = get_member(developer)
96
+ if not mem:
97
+ return None, '当前开发者 %s 未注册 (无 member.json)。请先 /wl-init。' % developer
98
+ acc = (mem.get('zentao_account') or '').strip()
99
+ if not acc:
100
+ return None, ('%s 的 member.json 未配 zentao_account (本地名↔禅道账号映射)。'
101
+ '\n 手动编辑 workspace/members/%s/member.json 加 "zentao_account": "你的禅道账号"。'
102
+ % (developer, developer))
103
+ return acc, None
104
+
105
+
106
+ # ============================================================
107
+ # 禅道 HTTP 层已收敛到 common.zentao_client.ZentaoClient (与 mcp_server 共享同一份)
108
+ # 下面 cmd_* 通过 creds (ZentaoClient 实例) 的 .api() / .check_intranet() 调用
109
+ # ============================================================
110
+
111
+ # ============================================================
112
+ # 本地任务遍历
113
+ # ============================================================
114
+
115
+ def _iter_local_tasks(repo_root):
116
+ """遍历 workspace/tasks/*/task.json, yield (task_dir, data)。"""
117
+ tdir = get_tasks_dir(repo_root)
118
+ if not tdir.is_dir():
119
+ return
120
+ for child in sorted(tdir.iterdir()):
121
+ if not child.is_dir():
122
+ continue
123
+ data = load_task_json(child)
124
+ if data:
125
+ yield child, data
126
+
127
+
128
+ # ============================================================
129
+ # 拉取: 禅道 → 本地 (归属硬过滤)
130
+ # ============================================================
131
+
132
+ def cmd_pull(args, creds, my_account):
133
+ """拉取某执行下 assignedTo==我 的任务到本地。默认 dry-run。"""
134
+ eid = args.execution_id
135
+ print('═══ 拉取 执行 #%s (仅 assignedTo=%s) ═══' % (eid, my_account))
136
+ if not args.apply:
137
+ print('[DRY-RUN] 仅预览, 不写本地。加 --apply 才真写。\n')
138
+
139
+ r = creds.api('GET', '/executions/%s/tasks' % eid, {'limit': 100})
140
+ if r.status_code != 200:
141
+ print('查询失败: %s %s' % (r.status_code, r.text[:120]))
142
+ return 3
143
+ data = r.json()
144
+ tasks = data.get('tasks') if isinstance(data, dict) else None
145
+ if not isinstance(tasks, list):
146
+ print('响应格式异常 (无 tasks 列表)。')
147
+ return 3
148
+
149
+ # ── 归属硬过滤: 只取 assignedTo == 我 ──
150
+ mine = [t for t in tasks if isinstance(t, dict)
151
+ and str(t.get('assignedTo', '')) == my_account]
152
+ skipped = len(tasks) - len(mine)
153
+ print('执行 #%s 共 %d 个任务, 归属我的 %d 个 (跳过队友 %d 个, 不拉)。\n'
154
+ % (eid, len(tasks), len(mine), skipped))
155
+
156
+ if not mine:
157
+ print('没有归属你的任务, 无需同步。')
158
+ return 0
159
+
160
+ # 建立本地 zentao_id 索引 (防重复 create)
161
+ repo_root = get_repo_root()
162
+ existing_zids = {}
163
+ for td, d in _iter_local_tasks(repo_root):
164
+ zid = d.get('zentao_id')
165
+ if zid:
166
+ existing_zids[int(zid)] = td
167
+
168
+ created = skipped_dup = 0
169
+ for t in mine:
170
+ zid = int(t.get('id', 0))
171
+ name = str(t.get('name', ''))[:50]
172
+ status = t.get('status', '')
173
+ if zid in existing_zids:
174
+ print(' ⊘ 跳过 #%s「%s」(本地已有 zentao_id=%s)' % (zid, name, zid))
175
+ skipped_dup += 1
176
+ continue
177
+ if not args.apply:
178
+ print(' + [将创建] #%s「%s」status=%s' % (zid, name, status))
179
+ created += 1
180
+ continue
181
+ # 真写: 直接创建本地任务 (含 zentao_id), 自包含不走 cmd_create
182
+ slug_seed = 'zentao-%s' % zid
183
+ new_dir = _create_local_task(repo_root, 'zentao#%s %s' % (zid, name),
184
+ slug_seed, zid, developer)
185
+ if new_dir:
186
+ print(' ✅ 创建 #%s「%s」并回填 zentao_id -> %s' % (zid, name, new_dir.name))
187
+ created += 1
188
+ else:
189
+ print(' ⚠️ 创建 #%s 失败' % zid)
190
+
191
+ print('\n小结: 创建 %d, 幂等跳过 %d, 队友任务跳过 %d。%s'
192
+ % (created, skipped_dup, skipped,
193
+ '(dry-run, 未真写)' if not args.apply else ''))
194
+ return 0
195
+
196
+
197
+ # ============================================================
198
+ # 推送: 本地 → 禅道 (默认 dry-run + 双校验)
199
+ # ============================================================
200
+
201
+ def cmd_push(args, creds, my_account, developer):
202
+ """推送本地 creator==我 的任务到禅道。默认 dry-run。"""
203
+ print('═══ 推送 本地 → 禅道 (仅 creator=%s, 强制 assignedTo=%s) ═══'
204
+ % (developer, my_account))
205
+ if not args.apply:
206
+ print('[DRY-RUN] 仅预览, 不写禅道。加 --apply 才真推。\n')
207
+
208
+ repo_root = get_repo_root()
209
+ candidates = []
210
+ for td, d in _iter_local_tasks(repo_root):
211
+ # ── 校验 1: 只推我创建的 (复用本地归属) ──
212
+ if (d.get('creator') or '') != developer:
213
+ continue
214
+ candidates.append((td, d))
215
+
216
+ if not candidates:
217
+ print('没有你创建的本地任务可推送。')
218
+ return 0
219
+
220
+ print('本地归属你的任务 %d 个:\n' % len(candidates))
221
+ pushed = skipped = 0
222
+ for td, d in candidates:
223
+ zid = d.get('zentao_id')
224
+ title = str(d.get('title', ''))[:50]
225
+ if zid:
226
+ # 已有 zentao_id → 更新前校验归属一致性
227
+ chk = creds.api('GET', '/tasks/%s' % zid)
228
+ if chk.status_code == 200:
229
+ remote_assign = str(chk.json().get('assignedTo', ''))
230
+ if remote_assign != my_account:
231
+ print(' 🚫 拒绝更新 #%s「%s」: 禅道 assignedTo=%s ≠ 你(%s)'
232
+ ' (防误改队友任务)' % (zid, title, remote_assign, my_account))
233
+ skipped += 1
234
+ continue
235
+ if not args.apply:
236
+ print(' ⟳ [将更新] #%s「%s」' % (zid, title))
237
+ pushed += 1
238
+ continue
239
+ payload = {'name': d.get('title', ''), 'assignedTo': my_account}
240
+ if d.get('estimate_hours'):
241
+ payload['estimate'] = d['estimate_hours']
242
+ if d.get('due_date'):
243
+ payload['deadline'] = d['due_date']
244
+ upd = creds.api('PUT', '/tasks/%s' % zid, payload)
245
+ print(' %s 更新 #%s「%s」%s'
246
+ % ('✅' if upd.status_code in (200, 201) else '⚠️', zid, title,
247
+ '' if upd.status_code in (200, 201) else '(%s)' % upd.status_code))
248
+ pushed += 1
249
+ else:
250
+ print(' ⚠️ 查询 #%s 失败 (%s), 跳过' % (zid, chk.status_code))
251
+ skipped += 1
252
+ else:
253
+ # 无 zentao_id → 新建 (强制 assignedTo=我)
254
+ if not args.execution_id:
255
+ print(' ⊙ 跳过「%s」(无 zentao_id, 且未给 --execution_id, 无法新建)'
256
+ % title)
257
+ skipped += 1
258
+ continue
259
+ if not args.apply:
260
+ print(' + [将新建]「%s」→ 执行 #%s' % (title, args.execution_id))
261
+ pushed += 1
262
+ continue
263
+ payload = {'execution': args.execution_id, 'name': d.get('title', ''),
264
+ 'assignedTo': my_account, 'type': 'devel', 'pri': 3}
265
+ cre = creds.api('POST', '/tasks', payload)
266
+ if cre.status_code in (200, 201):
267
+ new_zid = cre.json().get('id')
268
+ def _set(d, _z=new_zid):
269
+ d['zentao_id'] = _z
270
+ modify_task_json(td, _set)
271
+ print(' ✅ 新建「%s」→ 禅道 #%s, 回填 zentao_id' % (title, new_zid))
272
+ pushed += 1
273
+ else:
274
+ print(' ⚠️ 新建「%s」失败 (%s)' % (title, cre.status_code))
275
+ skipped += 1
276
+
277
+ print('\n小结: 推送 %d, 跳过 %d。%s'
278
+ % (pushed, skipped, '(dry-run, 未真写)' if not args.apply else ''))
279
+ return 0
280
+
281
+
282
+ # ============================================================
283
+ # status 概览
284
+ # ============================================================
285
+
286
+ def cmd_status(args, creds, my_account, developer):
287
+ """看本地任务与禅道的关联状态概览 (只读)。"""
288
+ repo_root = get_repo_root()
289
+ total = mine = linked = unlinked = 0
290
+ for td, d in _iter_local_tasks(repo_root):
291
+ total += 1
292
+ if (d.get('creator') or '') == developer:
293
+ mine += 1
294
+ if d.get('zentao_id'):
295
+ linked += 1
296
+ else:
297
+ unlinked += 1
298
+ print('═══ 同步状态 (%s / 禅道账号 %s) ═══' % (developer, my_account))
299
+ print('本地任务总数: %d' % total)
300
+ print(' 归属你的: %d' % mine)
301
+ print(' 已关联禅道 (有 zentao_id): %d' % linked)
302
+ print(' 未关联禅道 (推送时需 --execution_id 新建): %d' % unlinked)
303
+ ok, _ = creds.check_intranet()
304
+ print('\n禅道连通: %s (%s)' % ('✅ 可达' if ok else '❌ 不可达', creds.url))
305
+ return 0
306
+
307
+
308
+ # ============================================================
309
+ # 主入口
310
+ # ============================================================
311
+
312
+ def main(argv=None):
313
+ p = argparse.ArgumentParser(description='本地任务 ↔ 禅道安全同步',
314
+ formatter_class=argparse.RawDescriptionHelpFormatter,
315
+ epilog='安全原则: 拉取只取 assignedTo=我; 推送默认 dry-run 且'
316
+ ' 仅推 creator=我, 同名任务归属不一致则拒绝。')
317
+ sub = p.add_subparsers(dest='cmd')
318
+
319
+ pp = sub.add_parser('pull', help='拉取禅道任务到本地 (仅 assignedTo=我)')
320
+ pp.add_argument('execution_id', type=int, help='执行ID')
321
+ pp.add_argument('--apply', action='store_true', help='真写 (默认 dry-run 仅预览)')
322
+
323
+ sp = sub.add_parser('push', help='推送本地任务到禅道 (默认 dry-run)')
324
+ sp.add_argument('--execution_id', type=int, help='新建任务落到哪个执行 (无 zentao_id 的任务需要)')
325
+ sp.add_argument('--apply', action='store_true', help='真推 (默认 dry-run 仅预览)')
326
+
327
+ sub.add_parser('status', help='看同步状态概览 (只读)')
328
+
329
+ args = p.parse_args(argv)
330
+ if not args.cmd:
331
+ p.print_help()
332
+ return 0
333
+
334
+ developer = get_developer()
335
+ if not developer:
336
+ print('ERROR: 未设置当前开发者 (无 .developer)。请先 /wl-init。', file=sys.stderr)
337
+ return 4
338
+
339
+ my_account, err = _resolve_my_account(developer)
340
+ if err:
341
+ print('ERROR: %s' % err, file=sys.stderr)
342
+ return 3
343
+
344
+ creds = _zentao_creds()
345
+ if not creds:
346
+ print('ERROR: 禅道凭据未配置。请在 .qoder/config.yaml 的 zentao: 段填 url/user/password。',
347
+ file=sys.stderr)
348
+ return 3
349
+
350
+ # 连通性预检
351
+ ok, msg = creds.check_intranet()
352
+ if not ok:
353
+ print('ERROR: %s' % msg, file=sys.stderr)
354
+ return 5
355
+
356
+ try:
357
+ if args.cmd == 'pull':
358
+ return cmd_pull(args, creds, my_account)
359
+ elif args.cmd == 'push':
360
+ return cmd_push(args, creds, my_account, developer)
361
+ elif args.cmd == 'status':
362
+ return cmd_status(args, creds, my_account, developer)
363
+ except RuntimeError as e:
364
+ print('ERROR: %s' % e, file=sys.stderr)
365
+ return 3
366
+ return 0
367
+
368
+
369
+ if __name__ == '__main__':
370
+ sys.exit(main())
File without changes