@hupan56/wlkj 2.7.12 → 3.1.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bin/cli.js +1435 -1107
- package/package.json +29 -29
- package/templates/qoder/agents/insight-planning.md +1 -1
- package/templates/qoder/agents/insight-research.md +28 -15
- package/templates/qoder/commands/optional/wl-insight.md +161 -161
- package/templates/qoder/commands/optional/wl-report.md +20 -39
- package/templates/qoder/commands/optional/wl-status.md +4 -4
- package/templates/qoder/commands/wl-code.md +2 -2
- package/templates/qoder/commands/wl-design.md +6 -6
- package/templates/qoder/commands/wl-init.md +3 -3
- package/templates/qoder/commands/wl-prd.md +26 -13
- package/templates/qoder/commands/wl-req.md +43 -0
- package/templates/qoder/commands/wl-search.md +78 -47
- package/templates/qoder/commands/wl-task.md +343 -40
- package/templates/qoder/commands/wl-test.md +43 -17
- package/templates/qoder/config.yaml +52 -14
- package/templates/qoder/hooks/post-tool-use.py +181 -0
- package/templates/qoder/hooks/session-start.py +94 -23
- package/templates/qoder/hooks/stop-eval.py +207 -0
- package/templates/qoder/hooks/user-prompt-submit.py +139 -0
- package/templates/qoder/rules/wl-pipeline.md +85 -49
- package/templates/qoder/scripts/README.md +102 -0
- package/templates/qoder/scripts/capability/__init__.py +26 -0
- package/templates/qoder/scripts/capability/__main__.py +72 -0
- package/templates/qoder/scripts/capability/adapters/__init__.py +29 -0
- package/templates/qoder/scripts/capability/adapters/cli.py +316 -0
- package/templates/qoder/scripts/capability/adapters/mcp.py +334 -0
- package/templates/qoder/scripts/capability/adapters/qw.py +271 -0
- package/templates/qoder/scripts/capability/caps/__init__.py +27 -0
- package/templates/qoder/scripts/capability/caps/context.py +36 -0
- package/templates/qoder/scripts/capability/caps/cron.py +50 -0
- package/templates/qoder/scripts/capability/caps/identity.py +43 -0
- package/templates/qoder/scripts/capability/caps/memory.py +71 -0
- package/templates/qoder/scripts/capability/caps/notify.py +41 -0
- package/templates/qoder/scripts/capability/caps/present.py +48 -0
- package/templates/qoder/scripts/capability/caps/repo.py +31 -0
- package/templates/qoder/scripts/capability/caps/sandbox.py +43 -0
- package/templates/qoder/scripts/capability/chain.py +92 -0
- package/templates/qoder/scripts/capability/memory_chain.py +41 -0
- package/templates/qoder/scripts/capability/registry.py +246 -0
- package/templates/qoder/scripts/capability/registry_mcp.py +250 -0
- package/templates/qoder/scripts/capability/smoke_test.py +211 -0
- package/templates/qoder/scripts/capability/smoke_test_report.json +94 -0
- package/templates/qoder/scripts/deployment/setup/__init__.py +11 -0
- package/templates/qoder/scripts/deployment/setup/carriers.py +669 -0
- package/templates/qoder/scripts/deployment/setup/carriers_verify.py +148 -0
- package/templates/qoder/scripts/{init_doctor.py → deployment/setup/init_doctor.py} +100 -39
- package/templates/qoder/scripts/{install_qoderwork.py → deployment/setup/install_qoderwork.py} +48 -30
- package/templates/qoder/scripts/{platform_doctor.py → deployment/setup/platform_doctor.py} +271 -259
- package/templates/qoder/scripts/{repo_root.py → deployment/setup/repo_root.py} +9 -2
- package/templates/qoder/scripts/{setup.py → deployment/setup/setup.py} +143 -14
- package/templates/qoder/scripts/{setup_lanhu.py → deployment/setup/setup_lanhu.py} +979 -963
- package/templates/qoder/scripts/domain/design/__init__.py +0 -0
- package/templates/qoder/scripts/{fill_prototype.py → domain/design/fill_prototype.py} +17 -5
- package/templates/qoder/scripts/{gen_design_doc.py → domain/design/gen_design_doc.py} +406 -394
- package/templates/qoder/scripts/domain/kg/__init__.py +11 -0
- package/templates/qoder/scripts/domain/kg/build/__init__.py +0 -0
- package/templates/qoder/scripts/domain/kg/build/build_entity_registry.py +201 -0
- package/templates/qoder/scripts/domain/kg/build/build_relations.py +132 -0
- package/templates/qoder/scripts/{build_style_index.py → domain/kg/build/build_style_index.py} +50 -15
- package/templates/qoder/scripts/domain/kg/build/build_workflows.py +149 -0
- package/templates/qoder/scripts/{kg_build.py → domain/kg/build/kg_build.py} +685 -612
- package/templates/qoder/scripts/{kg_build_db.py → domain/kg/build/kg_build_db.py} +338 -327
- package/templates/qoder/scripts/{kg_incremental.py → domain/kg/build/kg_incremental.py} +425 -393
- package/templates/qoder/scripts/{learn_aggregate.py → domain/kg/build/learn_aggregate.py} +212 -201
- package/templates/qoder/scripts/domain/kg/extract/__init__.py +0 -0
- package/templates/qoder/scripts/{common → domain/kg/extract}/extract.py +430 -419
- package/templates/qoder/scripts/domain/kg/extract/test_extract.py +115 -0
- package/templates/qoder/scripts/{common → domain/kg/extract}/ts_extract.py +614 -536
- package/templates/qoder/scripts/domain/kg/graph/__init__.py +0 -0
- package/templates/qoder/scripts/{kg_link_db.py → domain/kg/graph/kg_link_db.py} +235 -224
- package/templates/qoder/scripts/domain/kg/graph/kg_semantic.py +683 -0
- package/templates/qoder/scripts/{kg.py → domain/kg/kg.py} +871 -708
- package/templates/qoder/scripts/domain/kg/kg_capabilities.py +182 -0
- package/templates/qoder/scripts/domain/kg/search/__init__.py +0 -0
- package/templates/qoder/scripts/{context_pack.py → domain/kg/search/context_pack.py} +45 -17
- package/templates/qoder/scripts/{enrich_prompt.py → domain/kg/search/enrich_prompt.py} +238 -226
- package/templates/qoder/scripts/domain/kg/search/prefetch.py +384 -0
- package/templates/qoder/scripts/{common → domain/kg/search}/search_engine.py +207 -205
- package/templates/qoder/scripts/{search_index.py → domain/kg/search/search_index.py} +210 -88
- package/templates/qoder/scripts/domain/kg/server/__init__.py +0 -0
- package/templates/qoder/scripts/domain/kg/server/kgd.py +549 -0
- package/templates/qoder/scripts/domain/kg/server/perf_bench.py +197 -0
- package/templates/qoder/scripts/domain/kg/storage/__init__.py +0 -0
- package/templates/qoder/scripts/{kg_duckdb.py → domain/kg/storage/kg_duckdb.py} +70 -45
- package/templates/qoder/scripts/domain/learning/__init__.py +0 -0
- package/templates/qoder/scripts/{learn.py → domain/learning/learn.py} +157 -146
- package/templates/qoder/scripts/domain/report/__init__.py +11 -0
- package/templates/qoder/scripts/{add_session.py → domain/report/add_session.py} +256 -244
- package/templates/qoder/scripts/{export.py → domain/report/export.py} +72 -4
- package/templates/qoder/scripts/{report.py → domain/report/report.py} +292 -281
- package/templates/qoder/scripts/domain/report/report_snapshot.py +261 -0
- package/templates/qoder/scripts/domain/report/role.py +39 -0
- package/templates/qoder/scripts/{status.py → domain/report/status.py} +737 -628
- package/templates/qoder/scripts/domain/report/status_snapshot.py +191 -0
- package/templates/qoder/scripts/domain/requirement/__init__.py +0 -0
- package/templates/qoder/scripts/{archive_prd.py → domain/requirement/archive_prd.py} +389 -377
- package/templates/qoder/scripts/domain/requirement/req.py +228 -0
- package/templates/qoder/scripts/domain/task/__init__.py +11 -0
- package/templates/qoder/scripts/{git_sync.py → domain/task/git_sync.py} +90 -50
- package/templates/qoder/scripts/{syncgate.py → domain/task/syncgate.py} +18 -7
- package/templates/qoder/scripts/domain/task/task.py +229 -0
- package/templates/qoder/scripts/domain/task/task_lifecycle.py +606 -0
- package/templates/qoder/scripts/domain/task/task_query.py +162 -0
- package/templates/qoder/scripts/domain/task/task_relations.py +425 -0
- package/templates/qoder/scripts/{team_sync.py → domain/task/team_sync.py} +101 -23
- package/templates/qoder/scripts/domain/task/zentao_sync.py +370 -0
- package/templates/qoder/scripts/foundation/__init__.py +0 -0
- package/templates/qoder/scripts/foundation/bootstrap.py +145 -0
- package/templates/qoder/scripts/foundation/core/__init__.py +0 -0
- package/templates/qoder/scripts/foundation/core/cmd_registry.py +112 -0
- package/templates/qoder/scripts/foundation/core/config.py +187 -0
- package/templates/qoder/scripts/{common → foundation/core}/paths.py +706 -400
- package/templates/qoder/scripts/foundation/data/__init__.py +0 -0
- package/templates/qoder/scripts/foundation/data/contract.py +317 -0
- package/templates/qoder/scripts/{common → foundation/data}/events.py +2 -2
- package/templates/qoder/scripts/foundation/data/result.py +223 -0
- package/templates/qoder/scripts/{common → foundation/data}/task_utils.py +427 -392
- package/templates/qoder/scripts/foundation/identity/__init__.py +0 -0
- package/templates/qoder/scripts/foundation/identity/check_publish.py +103 -0
- package/templates/qoder/scripts/{common → foundation/identity}/developer.py +239 -238
- package/templates/qoder/scripts/foundation/identity/guard.py +159 -0
- package/templates/qoder/scripts/{common → foundation/identity}/identity.py +132 -9
- package/templates/qoder/scripts/foundation/identity/roles.py +60 -0
- package/templates/qoder/scripts/foundation/integrations/__init__.py +0 -0
- package/templates/qoder/scripts/{common → foundation/integrations}/active_task.py +30 -15
- package/templates/qoder/scripts/{common → foundation/integrations}/feishu.py +11 -10
- package/templates/qoder/scripts/{common → foundation/integrations}/terms.py +3 -3
- package/templates/qoder/scripts/foundation/integrations/zentao_client.py +220 -0
- package/templates/qoder/scripts/foundation/io/__init__.py +0 -0
- package/templates/qoder/scripts/{common → foundation/io}/filelock.py +1 -1
- package/templates/qoder/scripts/{common → foundation/io}/reqid.py +1 -1
- package/templates/qoder/scripts/foundation/protocol/__init__.py +0 -0
- package/templates/qoder/scripts/foundation/protocol/duckdb_conn.py +39 -0
- package/templates/qoder/scripts/foundation/protocol/mcp_base.py +268 -0
- package/templates/qoder/scripts/foundation/utils/__init__.py +0 -0
- package/templates/qoder/scripts/{common → foundation/utils}/eval_api.py +12 -1
- package/templates/qoder/scripts/{common → foundation/utils}/pip_install.py +23 -6
- package/templates/qoder/scripts/{common → foundation/utils}/platform_guard.py +1 -1
- package/templates/qoder/scripts/orchestration/__init__.py +0 -0
- package/templates/qoder/scripts/orchestration/wlkj.py +185 -0
- package/templates/qoder/scripts/protocol/__init__.py +0 -0
- package/templates/qoder/scripts/protocol/browser/README.md +207 -0
- package/templates/qoder/scripts/protocol/browser/SKILL.md +265 -0
- package/templates/qoder/scripts/protocol/browser/config.env +9 -0
- package/templates/qoder/scripts/protocol/browser/references/cdp-api.md +110 -0
- package/templates/qoder/scripts/protocol/browser/references/migration-2.5.3.md +72 -0
- package/templates/qoder/scripts/protocol/browser/references/site-patterns/.gitkeep +0 -0
- package/templates/qoder/scripts/protocol/browser/scripts/browser-discovery.mjs +138 -0
- package/templates/qoder/scripts/protocol/browser/scripts/cdp-proxy.mjs +672 -0
- package/templates/qoder/scripts/protocol/browser/scripts/check-deps.mjs +206 -0
- package/templates/qoder/scripts/protocol/browser/scripts/find-url.mjs +253 -0
- package/templates/qoder/scripts/protocol/browser/scripts/match-site.mjs +46 -0
- package/templates/qoder/scripts/protocol/browser/templates/config.env.template +9 -0
- package/templates/qoder/scripts/protocol/mcp/__init__.py +11 -0
- package/templates/qoder/scripts/protocol/mcp/kg_mcp_server.py +478 -0
- package/templates/qoder/scripts/{lanhu_stdio_wrapper.py → protocol/mcp/lanhu_stdio_wrapper.py} +131 -119
- package/templates/qoder/scripts/protocol/mcp/mcp_doctor.py +548 -0
- package/templates/qoder/scripts/protocol/mcp/mcp_launcher.py +600 -0
- package/templates/qoder/scripts/protocol/mcp/mysql_mcp_server.py +461 -0
- package/templates/qoder/scripts/protocol/mcp/zentao_mcp_server.py +1920 -0
- package/templates/qoder/scripts/protocol/transports/__init__.py +56 -0
- package/templates/qoder/scripts/protocol/transports/base.py +87 -0
- package/templates/qoder/scripts/protocol/transports/cli.py +132 -0
- package/templates/qoder/scripts/protocol/transports/http.py +141 -0
- package/templates/qoder/scripts/protocol/transports/stdio.py +293 -0
- package/templates/qoder/scripts/run_weekly_update.bat +27 -18
- package/templates/qoder/scripts/run_weekly_update.sh +22 -13
- package/templates/qoder/scripts/validation/__init__.py +0 -0
- package/templates/qoder/scripts/validation/eval/__init__.py +0 -0
- package/templates/qoder/scripts/validation/eval/perf-report-2026-06-24.md +139 -0
- package/templates/qoder/scripts/validation/eval/qwork_harness.py +617 -0
- package/templates/qoder/scripts/validation/eval/report-commands.md +220 -0
- package/templates/qoder/scripts/validation/eval/transcript_timing.py +386 -0
- package/templates/qoder/scripts/validation/metrics/__init__.py +0 -0
- package/templates/qoder/scripts/{eval_prd.py → validation/metrics/eval_prd.py} +84 -15
- package/templates/qoder/scripts/validation/metrics/usability_score.py +750 -0
- package/templates/qoder/scripts/validation/test/__init__.py +11 -0
- package/templates/qoder/scripts/validation/test/assertion_gen.py +551 -0
- package/templates/qoder/scripts/{autotest.py → validation/test/autotest.py} +1234 -1751
- package/templates/qoder/scripts/validation/test/autotest_auth.py +109 -0
- package/templates/qoder/scripts/{autotest_batch.py → validation/test/autotest_batch.py} +255 -224
- package/templates/qoder/scripts/validation/test/autotest_data.py +680 -0
- package/templates/qoder/scripts/validation/test/autotest_opencli.py +646 -0
- package/templates/qoder/scripts/{autotest_run.py → validation/test/autotest_run.py} +323 -297
- package/templates/qoder/scripts/validation/test/autotest_webaccess.py +571 -0
- package/templates/qoder/scripts/{benchmark.py → validation/test/benchmark.py} +17 -5
- package/templates/qoder/scripts/{kg_auto_login.py → validation/test/kg_auto_login.py} +208 -196
- package/templates/qoder/scripts/{kg_test_runner.py → validation/test/kg_test_runner.py} +13 -2
- package/templates/qoder/scripts/{page_probe.py → validation/test/page_probe.py} +470 -459
- package/templates/qoder/scripts/validation/test/smoke_all_commands.py +170 -0
- package/templates/qoder/settings.json +23 -2
- package/templates/qoder/skills/design-import/SKILL.md +235 -226
- package/templates/qoder/skills/design-review/SKILL.md +91 -82
- package/templates/qoder/skills/prd-generator/SKILL.md +38 -24
- package/templates/qoder/skills/prd-review/SKILL.md +8 -6
- package/templates/qoder/skills/prototype-generator/SKILL.md +296 -247
- package/templates/qoder/skills/spec-coder/SKILL.md +24 -15
- package/templates/qoder/skills/spec-generator/SKILL.md +24 -12
- package/templates/qoder/skills/test-generator/SKILL.md +9 -7
- package/templates/qoder/skills/wl-code/SKILL.md +25 -13
- package/templates/qoder/skills/wl-commit/SKILL.md +10 -9
- package/templates/qoder/skills/wl-design/SKILL.md +7 -5
- package/templates/qoder/skills/wl-init/SKILL.md +8 -8
- package/templates/qoder/skills/wl-insight/SKILL.md +25 -12
- package/templates/qoder/skills/wl-prd-full/SKILL.md +59 -8
- package/templates/qoder/skills/wl-prd-quick/SKILL.md +7 -7
- package/templates/qoder/skills/wl-prd-review/SKILL.md +18 -6
- package/templates/qoder/skills/wl-report/SKILL.md +27 -25
- package/templates/qoder/skills/wl-search/SKILL.md +151 -80
- package/templates/qoder/skills/wl-spec/SKILL.md +9 -7
- package/templates/qoder/skills/wl-status/SKILL.md +50 -18
- package/templates/qoder/skills/wl-task/SKILL.md +94 -12
- package/templates/qoder/skills/wl-test/SKILL.md +154 -45
- package/templates/qoder/templates/prd-full-template.md +7 -0
- package/templates/root/AGENTS.md +275 -237
- package/templates/root/requirements.txt +3 -0
- package/templates/root//344/275/277/347/224/250/350/257/264/346/230/216.md +3 -3
- package/templates/root//346/226/260/346/211/213/346/214/207/345/215/227.md +2 -2
- package/templates/qoder/hooks/inject-workflow-state.py +0 -169
- package/templates/qoder/scripts/__pycache__/check_mcp_launch.cpython-39.pyc +0 -0
- package/templates/qoder/scripts/__pycache__/install_qoderwork.cpython-39.pyc +0 -0
- package/templates/qoder/scripts/__pycache__/mcp_launcher.cpython-39.pyc +0 -0
- package/templates/qoder/scripts/__pycache__/platform_doctor.cpython-39.pyc +0 -0
- package/templates/qoder/scripts/check_carriers.py +0 -238
- package/templates/qoder/scripts/check_mcp.py +0 -298
- package/templates/qoder/scripts/check_mcp_launch.py +0 -183
- package/templates/qoder/scripts/check_qoderwork_consistency.py +0 -166
- package/templates/qoder/scripts/collect_prds.py +0 -31
- package/templates/qoder/scripts/common/mentions.py +0 -134
- package/templates/qoder/scripts/common/utf8.py +0 -38
- package/templates/qoder/scripts/extract_api_params.py +0 -246
- package/templates/qoder/scripts/extract_routes.py +0 -54
- package/templates/qoder/scripts/extract_routes_tree.py +0 -78
- package/templates/qoder/scripts/handoff.py +0 -22
- package/templates/qoder/scripts/init_developer.py +0 -76
- package/templates/qoder/scripts/kg_mcp_server.py +0 -801
- package/templates/qoder/scripts/kg_semantic.py +0 -150
- package/templates/qoder/scripts/mcp_launcher.py +0 -414
- package/templates/qoder/scripts/mysql_mcp_server.py +0 -396
- package/templates/qoder/scripts/parse_prds.py +0 -33
- package/templates/qoder/scripts/role.py +0 -51
- package/templates/qoder/scripts/sync_carriers.py +0 -259
- package/templates/qoder/scripts/task.py +0 -1261
- package/templates/qoder/scripts/workspace_init.py +0 -102
- package/templates/qoder/scripts/zentao_mcp_server.py +0 -424
- package/templates/qoder/skills/prompt-enrich/SKILL.md +0 -90
- /package/templates/qoder/scripts/{common → deployment}/__init__.py +0 -0
- /package/templates/qoder/{skills/prototype-generator/SKILL.md.zcode-79180-2af4721f-f9a6-412c-88db-c0af680d211b.tmp → scripts/domain/__init__.py} +0 -0
- /package/templates/qoder/scripts/{common → domain/kg/graph}/graph_traverse.py +0 -0
- /package/templates/qoder/scripts/{common → domain/kg/server}/repowiki.py +0 -0
- /package/templates/qoder/scripts/{common → foundation/io}/atomicio.py +0 -0
- /package/templates/qoder/scripts/{secure-ls.js → validation/test/secure-ls.js} +0 -0
|
@@ -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
|
|
38
|
-
from
|
|
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
|
-
|
|
56
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
# 已跟踪文件:
|
|
349
|
+
# 已跟踪文件: 收集 (它们已经在版本控制, 不可能是新秘密)
|
|
296
350
|
r = git('ls-files', '--', scope)
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
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
|
-
|
|
317
|
-
staged += 1
|
|
369
|
+
to_add.append(fpath)
|
|
318
370
|
else:
|
|
319
371
|
skipped.append(fpath)
|
|
320
372
|
elif 'D' not in status:
|
|
321
|
-
# 修改/重命名等 (非删除) —— 已跟踪,
|
|
373
|
+
# 修改/重命名等 (非删除) —— 已跟踪, 收集
|
|
322
374
|
# 重命名格式 "R old -> new", 取 new
|
|
323
375
|
if '->' in fpath:
|
|
324
376
|
fpath = fpath.split('->')[-1].strip().strip('"')
|
|
325
|
-
|
|
326
|
-
|
|
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
|