@hupan56/wlkj 2.7.11 → 3.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (152) hide show
  1. package/bin/cli.js +375 -78
  2. package/package.json +29 -29
  3. package/templates/.qoder/.runtime/ctx-cache-5660152f1d6dd819.md +23 -0
  4. package/templates/.qoder/.runtime/ctx-cache-afdce0dac06b25b0.md +23 -0
  5. package/templates/.qoder/.runtime/search-cache-eae7644e7b122f35.txt +1 -0
  6. package/templates/.qoder/learning/eval-history.jsonl +28 -0
  7. package/templates/data/index/wiki-index.json +8 -0
  8. package/templates/qoder/agents/insight-planning.md +1 -1
  9. package/templates/qoder/agents/insight-research.md +28 -15
  10. package/templates/qoder/commands/optional/wl-insight.md +159 -161
  11. package/templates/qoder/commands/optional/wl-report.md +4 -4
  12. package/templates/qoder/commands/optional/wl-status.md +2 -2
  13. package/templates/qoder/commands/wl-code.md +2 -2
  14. package/templates/qoder/commands/wl-design.md +2 -2
  15. package/templates/qoder/commands/wl-init.md +3 -3
  16. package/templates/qoder/commands/wl-prd.md +2 -2
  17. package/templates/qoder/commands/wl-req.md +43 -0
  18. package/templates/qoder/commands/wl-search.md +8 -8
  19. package/templates/qoder/commands/wl-task.md +17 -17
  20. package/templates/qoder/commands/wl-test.md +41 -15
  21. package/templates/qoder/config.yaml +17 -1
  22. package/templates/qoder/hooks/session-start.py +12 -22
  23. package/templates/qoder/nul +4 -0
  24. package/templates/qoder/rules/wl-pipeline.md +22 -48
  25. package/templates/qoder/scripts/README.md +139 -0
  26. package/templates/qoder/scripts/common/autotest_auth.py +109 -0
  27. package/templates/qoder/scripts/common/bootstrap.py +145 -0
  28. package/templates/qoder/scripts/common/check_publish.py +98 -0
  29. package/templates/qoder/scripts/common/cmd_registry.py +112 -0
  30. package/templates/qoder/scripts/common/config.py +187 -0
  31. package/templates/qoder/scripts/common/contract.py +317 -0
  32. package/templates/qoder/scripts/common/developer.py +2 -1
  33. package/templates/qoder/scripts/common/feishu.py +10 -9
  34. package/templates/qoder/scripts/common/guard.py +159 -0
  35. package/templates/qoder/scripts/common/identity.py +121 -2
  36. package/templates/qoder/scripts/common/kg_capabilities.py +182 -0
  37. package/templates/qoder/scripts/common/mcp_base.py +268 -0
  38. package/templates/qoder/scripts/common/paths.py +187 -1
  39. package/templates/qoder/scripts/common/result.py +223 -0
  40. package/templates/qoder/scripts/common/roles.py +60 -0
  41. package/templates/qoder/scripts/common/task_utils.py +21 -9
  42. package/templates/qoder/scripts/common/test_extract.py +115 -0
  43. package/templates/qoder/scripts/kg/__init__.py +11 -0
  44. package/templates/qoder/scripts/kg/build_entity_registry.py +196 -0
  45. package/templates/qoder/scripts/kg/build_relations.py +127 -0
  46. package/templates/qoder/scripts/{build_style_index.py → kg/build_style_index.py} +39 -10
  47. package/templates/qoder/scripts/kg/build_workflows.py +144 -0
  48. package/templates/qoder/scripts/{context_pack.py → kg/context_pack.py} +18 -11
  49. package/templates/qoder/scripts/{enrich_prompt.py → kg/enrich_prompt.py} +232 -226
  50. package/templates/qoder/scripts/{extract_api_params.py → kg/extract.py} +398 -246
  51. package/templates/qoder/scripts/{kg.py → kg/kg.py} +638 -708
  52. package/templates/qoder/scripts/{kg_build.py → kg/kg_build.py} +618 -612
  53. package/templates/qoder/scripts/{kg_build_db.py → kg/kg_build_db.py} +333 -327
  54. package/templates/qoder/scripts/{kg_duckdb.py → kg/kg_duckdb.py} +38 -37
  55. package/templates/qoder/scripts/{kg_incremental.py → kg/kg_incremental.py} +420 -393
  56. package/templates/qoder/scripts/{kg_link_db.py → kg/kg_link_db.py} +230 -224
  57. package/templates/qoder/scripts/{kg_semantic.py → kg/kg_semantic.py} +156 -150
  58. package/templates/qoder/scripts/kg/prefetch.py +359 -0
  59. package/templates/qoder/scripts/{search_index.py → kg/search_index.py} +70 -14
  60. package/templates/qoder/scripts/mcp/__init__.py +11 -0
  61. package/templates/qoder/scripts/{kg_mcp_server.py → mcp/kg_mcp_server.py} +77 -272
  62. package/templates/qoder/scripts/{lanhu_stdio_wrapper.py → mcp/lanhu_stdio_wrapper.py} +125 -119
  63. package/templates/qoder/scripts/{check_mcp.py → mcp/mcp_doctor.py} +515 -298
  64. package/templates/qoder/scripts/{mcp_launcher.py → mcp/mcp_launcher.py} +442 -414
  65. package/templates/qoder/scripts/{mysql_mcp_server.py → mcp/mysql_mcp_server.py} +347 -396
  66. package/templates/qoder/scripts/{zentao_mcp_server.py → mcp/zentao_mcp_server.py} +384 -424
  67. package/templates/qoder/scripts/report/__init__.py +11 -0
  68. package/templates/qoder/scripts/{add_session.py → report/add_session.py} +250 -244
  69. package/templates/qoder/scripts/{archive_prd.py → report/archive_prd.py} +383 -377
  70. package/templates/qoder/scripts/{eval_prd.py → report/eval_prd.py} +73 -11
  71. package/templates/qoder/scripts/{export.py → report/export.py} +63 -0
  72. package/templates/qoder/scripts/{fill_prototype.py → report/fill_prototype.py} +6 -0
  73. package/templates/qoder/scripts/{gen_design_doc.py → report/gen_design_doc.py} +400 -394
  74. package/templates/qoder/scripts/{learn.py → report/learn.py} +152 -146
  75. package/templates/qoder/scripts/{learn_aggregate.py → report/learn_aggregate.py} +207 -201
  76. package/templates/qoder/scripts/{report.py → report/report.py} +287 -281
  77. package/templates/qoder/scripts/report/req.py +222 -0
  78. package/templates/qoder/scripts/report/role.py +33 -0
  79. package/templates/qoder/scripts/{status.py → report/status.py} +634 -628
  80. package/templates/qoder/scripts/setup/__init__.py +11 -0
  81. package/templates/qoder/scripts/setup/carriers.py +662 -0
  82. package/templates/qoder/scripts/{init_doctor.py → setup/init_doctor.py} +63 -26
  83. package/templates/qoder/scripts/{install_qoderwork.py → setup/install_qoderwork.py} +36 -26
  84. package/templates/qoder/scripts/{platform_doctor.py → setup/platform_doctor.py} +265 -259
  85. package/templates/qoder/scripts/{repo_root.py → setup/repo_root.py} +112 -106
  86. package/templates/qoder/scripts/{setup.py → setup/setup.py} +147 -0
  87. package/templates/qoder/scripts/{setup_lanhu.py → setup/setup_lanhu.py} +973 -963
  88. package/templates/qoder/scripts/task/__init__.py +11 -0
  89. package/templates/qoder/scripts/{git_sync.py → task/git_sync.py} +52 -31
  90. package/templates/qoder/scripts/{syncgate.py → task/syncgate.py} +6 -0
  91. package/templates/qoder/scripts/task/task.py +221 -0
  92. package/templates/qoder/scripts/task/task_lifecycle.py +596 -0
  93. package/templates/qoder/scripts/task/task_query.py +161 -0
  94. package/templates/qoder/scripts/task/task_relations.py +424 -0
  95. package/templates/qoder/scripts/{team_sync.py → task/team_sync.py} +93 -20
  96. package/templates/qoder/scripts/test/__init__.py +11 -0
  97. package/templates/qoder/scripts/{autotest.py → test/autotest.py} +1174 -1751
  98. package/templates/qoder/scripts/{autotest_batch.py → test/autotest_batch.py} +242 -224
  99. package/templates/qoder/scripts/test/autotest_data.py +675 -0
  100. package/templates/qoder/scripts/{autotest_run.py → test/autotest_run.py} +309 -297
  101. package/templates/qoder/scripts/{benchmark.py → test/benchmark.py} +6 -0
  102. package/templates/qoder/scripts/{kg_auto_login.py → test/kg_auto_login.py} +202 -196
  103. package/templates/qoder/scripts/{kg_test_runner.py → test/kg_test_runner.py} +7 -1
  104. package/templates/qoder/scripts/{page_probe.py → test/page_probe.py} +465 -459
  105. package/templates/qoder/scripts/wlkj.py +116 -0
  106. package/templates/qoder/settings.json +1 -10
  107. package/templates/qoder/skills/design-import/SKILL.md +226 -226
  108. package/templates/qoder/skills/design-review/SKILL.md +82 -82
  109. package/templates/qoder/skills/prd-generator/SKILL.md +26 -16
  110. package/templates/qoder/skills/prd-review/SKILL.md +5 -5
  111. package/templates/qoder/skills/prototype-generator/SKILL.md +256 -256
  112. package/templates/qoder/skills/spec-coder/SKILL.md +4 -4
  113. package/templates/qoder/skills/spec-generator/SKILL.md +4 -4
  114. package/templates/qoder/skills/test-generator/SKILL.md +5 -5
  115. package/templates/qoder/skills/wl-code/SKILL.md +4 -4
  116. package/templates/qoder/skills/wl-commit/SKILL.md +4 -4
  117. package/templates/qoder/skills/wl-design/SKILL.md +3 -3
  118. package/templates/qoder/skills/wl-init/SKILL.md +8 -8
  119. package/templates/qoder/skills/wl-insight/SKILL.md +5 -5
  120. package/templates/qoder/skills/wl-prd-full/SKILL.md +6 -6
  121. package/templates/qoder/skills/wl-prd-quick/SKILL.md +6 -6
  122. package/templates/qoder/skills/wl-prd-review/SKILL.md +4 -4
  123. package/templates/qoder/skills/wl-report/SKILL.md +7 -7
  124. package/templates/qoder/skills/wl-search/SKILL.md +13 -13
  125. package/templates/qoder/skills/wl-spec/SKILL.md +5 -5
  126. package/templates/qoder/skills/wl-status/SKILL.md +5 -5
  127. package/templates/qoder/skills/wl-task/SKILL.md +6 -6
  128. package/templates/qoder/skills/wl-test/SKILL.md +102 -39
  129. package/templates/root/AGENTS.md +39 -40
  130. package/templates/qoder/hooks/inject-workflow-state.py +0 -169
  131. package/templates/qoder/scripts/__pycache__/check_mcp_launch.cpython-39.pyc +0 -0
  132. package/templates/qoder/scripts/__pycache__/install_qoderwork.cpython-39.pyc +0 -0
  133. package/templates/qoder/scripts/__pycache__/mcp_launcher.cpython-39.pyc +0 -0
  134. package/templates/qoder/scripts/__pycache__/platform_doctor.cpython-39.pyc +0 -0
  135. package/templates/qoder/scripts/check_carriers.py +0 -238
  136. package/templates/qoder/scripts/check_mcp_launch.py +0 -183
  137. package/templates/qoder/scripts/check_qoderwork_consistency.py +0 -166
  138. package/templates/qoder/scripts/collect_prds.py +0 -31
  139. package/templates/qoder/scripts/common/mentions.py +0 -134
  140. package/templates/qoder/scripts/common/utf8.py +0 -38
  141. package/templates/qoder/scripts/extract_routes.py +0 -54
  142. package/templates/qoder/scripts/extract_routes_tree.py +0 -78
  143. package/templates/qoder/scripts/handoff.py +0 -22
  144. package/templates/qoder/scripts/init_developer.py +0 -76
  145. package/templates/qoder/scripts/parse_prds.py +0 -33
  146. package/templates/qoder/scripts/role.py +0 -51
  147. package/templates/qoder/scripts/sync_carriers.py +0 -259
  148. package/templates/qoder/scripts/task.py +0 -1261
  149. package/templates/qoder/scripts/workspace_init.py +0 -102
  150. package/templates/qoder/skills/prompt-enrich/SKILL.md +0 -90
  151. package/templates/qoder/skills/prototype-generator/SKILL.md.zcode-79180-2af4721f-f9a6-412c-88db-c0af680d211b.tmp +0 -0
  152. /package/templates/qoder/scripts/{secure-ls.js → test/secure-ls.js} +0 -0
@@ -0,0 +1,109 @@
1
+ # -*- coding: utf-8 -*-
2
+ """
3
+ autotest_auth.py - 自动化测试登录态 (单一真源, 消除 3 处重复)
4
+
5
+ 现状: get_token / read_developer / AUTH_FILE 在三个文件各自重复:
6
+ - autotest.py (用 common.config 读 dev)
7
+ - autotest_run.py:42-56 (读 token + 校验 + 失效重登)
8
+ - autotest_batch.py:22-45 (read_developer + login_api + get_token)
9
+ 三份逻辑基本一致 (读 auth-state.json 的 access_token, 失效调 kg_auto_login),
10
+ 但细节差异 (校验 URL/timeout/headers) 容易分叉。
11
+
12
+ 本模块作为单一真源, 三个文件共用。
13
+
14
+ 核心 API:
15
+ get_developer() -> str 读 .qoder/.developer
16
+ auth_file(dev=None) -> str 成员的 auth-state.json 路径
17
+ get_token(dev=None, base_url=None) -> str|None 读 token, 失效重登
18
+
19
+ Usage:
20
+ from common.autotest_auth import get_token, get_developer
21
+ tok = get_token() # 自动读当前开发者, token 失效自动调 kg_auto_login
22
+ """
23
+
24
+ import json
25
+ import os
26
+ import subprocess
27
+ import sys
28
+
29
+ __all__ = ["get_developer", "auth_file", "get_token", "MEMBER_DIR"]
30
+
31
+ # 相对导入
32
+ try:
33
+ from .paths import get_repo_root
34
+ except ImportError:
35
+ _COMMON_DIR = os.path.dirname(os.path.abspath(__file__))
36
+ if _COMMON_DIR not in sys.path:
37
+ sys.path.insert(0, _COMMON_DIR)
38
+ from paths import get_repo_root # type: ignore
39
+
40
+
41
+ def get_developer() -> str:
42
+ """读当前开发者名 (.qoder/.developer)。未初始化抛 RuntimeError。"""
43
+ dev_file = os.path.join(str(get_repo_root()), '.qoder', '.developer')
44
+ if os.path.isfile(dev_file):
45
+ for ln in open(dev_file, encoding='utf-8'):
46
+ if ln.strip().startswith('name='):
47
+ return ln.strip().split('=', 1)[1].strip()
48
+ raise RuntimeError('未检测到开发者身份(.qoder/.developer)。请先运行 /wl-init。')
49
+
50
+
51
+ def _member_dir(dev: str = None) -> str:
52
+ dev = dev or get_developer()
53
+ return os.path.join(str(get_repo_root()), 'workspace', 'members', dev)
54
+
55
+
56
+ MEMBER_DIR = _member_dir # 模块级便捷别名 (首次 import 时解析)
57
+
58
+
59
+ def auth_file(dev: str = None) -> str:
60
+ """成员的 auth-state.json 路径 (v3.0: 收进 .private/, 向后兼容)。"""
61
+ try:
62
+ from .paths import get_private_file
63
+ return str(get_private_file('auth-state.json', dev))
64
+ except Exception:
65
+ return os.path.join(_member_dir(dev), 'auth-state.json')
66
+
67
+
68
+ def get_token(dev: str = None, base_url: str = None) -> str:
69
+ """读 access_token, 失效则调 kg_auto_login.py 重登。
70
+
71
+ Args:
72
+ dev: 开发者名 (默认当前)
73
+ base_url: 校验 token 用的 base_url (默认用 QAS 测试环境)
74
+ Returns:
75
+ token 字符串, 或 None (重登后仍无)
76
+ """
77
+ af = auth_file(dev)
78
+ base = base_url or 'https://icsqas.inforecloud.com'
79
+
80
+ def _read_tok():
81
+ if os.path.exists(af):
82
+ try:
83
+ return json.load(open(af, encoding='utf-8')).get('access_token')
84
+ except Exception:
85
+ return None
86
+ return None
87
+
88
+ tok = _read_tok()
89
+ if tok:
90
+ # 校验 token 是否有效
91
+ try:
92
+ import requests
93
+ r = requests.get(
94
+ base + '/test-api/system/user/getInfo',
95
+ headers={'Authorization': 'Bearer ' + tok, 'clientid': 'e5cd7e4891bf95d1d19206ce24a7b32e'},
96
+ timeout=8,
97
+ )
98
+ if r.json().get('code') == 200:
99
+ return tok
100
+ except Exception:
101
+ pass # 校验失败 → 重登
102
+
103
+ # 重新登录
104
+ scripts_dir = os.path.join(str(get_repo_root()), '.qoder', 'scripts')
105
+ subprocess.run(
106
+ [sys.executable, os.path.join(scripts_dir, 'kg_auto_login.py')],
107
+ capture_output=True, timeout=120,
108
+ )
109
+ return _read_tok()
@@ -0,0 +1,145 @@
1
+ # -*- coding: utf-8 -*-
2
+ """
3
+ bootstrap.py - 统一 CLI 启动样板 (DRY) + 子目录化路径自举 (v3.0)
4
+
5
+ 治"70处 sys.path + 50处 reconfigure" + 子目录化后的路径定位。
6
+
7
+ 子目录化后, 脚本在 scripts/kg/kg.py, 它的 dirname(__file__)=scripts/kg/,
8
+ 直接 sys.path.insert(scripts/kg/) 后 `from common.x` 会断 (common 在 scripts/common/)。
9
+
10
+ 本模块的 setup() 解决: 无论脚本在 scripts/ 根还是 scripts/某子包/,
11
+ 都能正确定位 scripts/ 根并注入, 让 `from common.x` 和跨包 import 都能用。
12
+
13
+ 核心: _find_scripts_root() 向上找含 'common/' 子目录的那个目录 = scripts/ 根。
14
+
15
+ Usage (两种姿势):
16
+ # 姿势1: 子包脚本开头 (推荐)
17
+ import os, sys
18
+ _S = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) # 子包→scripts
19
+ if _S not in sys.path: sys.path.insert(0, _S)
20
+ from common.bootstrap import setup; setup()
21
+
22
+ # 姿势2: scripts/ 根脚本 (老姿势仍可用)
23
+ from common.bootstrap import setup; setup()
24
+ """
25
+
26
+ import os
27
+ import sys
28
+
29
+ __all__ = ["setup", "SCRIPTS_DIR", "REPO_ROOT", "find_scripts_root", "ensure_scripts_path"]
30
+
31
+
32
+ def find_scripts_root(start=None):
33
+ """定位 .qoder/scripts/ 根目录。
34
+
35
+ 策略: 从 start (默认 __file__ 的目录) 向上找, 直到某目录含 'common/' 子目录
36
+ 且该 common/ 下有 bootstrap.py (即本文件) → 那就是 scripts/ 根。
37
+ 兜底: 找含 '.qoder' 的目录 + '/scripts'。
38
+ """
39
+ start = start or os.path.dirname(os.path.abspath(__file__))
40
+ cur = start
41
+ for _ in range(10):
42
+ # scripts/ 根的标志: 含 common/bootstrap.py
43
+ if os.path.isfile(os.path.join(cur, "common", "bootstrap.py")):
44
+ return cur
45
+ parent = os.path.dirname(cur)
46
+ if parent == cur:
47
+ break
48
+ cur = parent
49
+ # 兜底: cwd 向上找 .qoder/scripts
50
+ cwd = os.getcwd()
51
+ for _ in range(10):
52
+ cand = os.path.join(cwd, ".qoder", "scripts")
53
+ if os.path.isdir(cand):
54
+ return cand
55
+ parent = os.path.dirname(cwd)
56
+ if parent == cwd:
57
+ break
58
+ cwd = parent
59
+ return start
60
+
61
+
62
+ def ensure_scripts_path(start=None):
63
+ """确保 scripts/ 根在 sys.path 最前。幂等。
64
+
65
+ 子包脚本调用此函数后, `from common.x` 即可用。
66
+ """
67
+ root = find_scripts_root(start)
68
+ if root not in sys.path:
69
+ sys.path.insert(0, root)
70
+ return root
71
+
72
+
73
+ def _find_scripts_dir() -> str:
74
+ """[内部] 定位 scripts/ 根 (兼容旧名)。"""
75
+ return find_scripts_root()
76
+
77
+
78
+ def ensure_utf8_stdio():
79
+ """UTF-8 stdio 配置 (v3.0: 从 utf8.py 并入)。
80
+
81
+ 把 stdout/stderr 切到 UTF-8, 失败则忽略 (不崩溃)。
82
+ 幂等: 多次调用无害。
83
+ """
84
+ for stream_name in ("stdout", "stderr"):
85
+ stream = getattr(sys, stream_name, None)
86
+ if stream is None:
87
+ continue
88
+ reconfigure = getattr(stream, "reconfigure", None)
89
+ if reconfigure is None:
90
+ continue
91
+ try:
92
+ reconfigure(encoding="utf-8", errors="replace")
93
+ except (TypeError, ValueError, OSError, IOError):
94
+ try:
95
+ reconfigure(encoding="utf-8")
96
+ except Exception:
97
+ pass
98
+
99
+
100
+ def setup():
101
+ """统一 CLI 启动初始化。幂等。
102
+
103
+ 做: ① ensure_utf8_stdio ② scripts/ 根注入 sys.path ③ scripts/common/ 注入
104
+ Returns: (SCRIPTS_DIR, REPO_ROOT)
105
+ """
106
+ # 先确保 scripts/ 根在 path (本文件可能在 common/ 或被引用)
107
+ scripts_dir = ensure_scripts_root = find_scripts_root()
108
+ if scripts_dir not in sys.path:
109
+ sys.path.insert(0, scripts_dir)
110
+ common_dir = os.path.join(scripts_dir, "common")
111
+ if os.path.isdir(common_dir) and common_dir not in sys.path:
112
+ sys.path.insert(0, common_dir)
113
+ # 注入所有子包目录 (kg/build/mcp/test/setup/carriers/task/search/report)
114
+ # 让跨包 import (旧式不带前缀, 如 import search_index) 仍可用
115
+ try:
116
+ for _name in os.listdir(scripts_dir):
117
+ _subdir = os.path.join(scripts_dir, _name)
118
+ if os.path.isdir(_subdir) and _subdir not in sys.path:
119
+ sys.path.insert(0, _subdir)
120
+ except OSError:
121
+ pass
122
+
123
+ # 1. UTF-8 stdio (v3.0: utf8.py 已并入 bootstrap, 内联实现)
124
+ try:
125
+ ensure_utf8_stdio()
126
+ except Exception:
127
+ for sname in ("stdout", "stderr"):
128
+ s = getattr(sys, sname, None)
129
+ rc = getattr(s, "reconfigure", None) if s else None
130
+ if rc:
131
+ try:
132
+ rc(encoding="utf-8", errors="replace")
133
+ except Exception:
134
+ try:
135
+ rc(encoding="utf-8")
136
+ except Exception:
137
+ pass
138
+
139
+ repo_root = os.path.dirname(os.path.dirname(scripts_dir))
140
+ return scripts_dir, repo_root
141
+
142
+
143
+ # 模块级常量
144
+ SCRIPTS_DIR = find_scripts_root()
145
+ REPO_ROOT = os.path.dirname(os.path.dirname(SCRIPTS_DIR))
@@ -0,0 +1,98 @@
1
+ # -*- coding: utf-8 -*-
2
+ # v3.0 路径自举
3
+ import os as _o, sys as _s
4
+ _cp = _o.path.join(_o.path.dirname(_o.path.dirname(_o.path.abspath(__file__))), 'common')
5
+ if _cp not in _s.path: _s.path.insert(0, _cp)
6
+ from bootstrap import setup; setup()
7
+
8
+ """
9
+ check_publish.py - 统一发布前置检查 (guard + contract 双门)
10
+
11
+ 把"造了没接"的两个基建真正接入执行链路:
12
+ - guard.py (角色边界): 当前角色能否产出该契约? 越权 → 软提示
13
+ - contract.py (契约校验): 产出文件带不带 @contract 头? 带了是否合规?
14
+
15
+ 设计原则 (与 guard/contract 一致):
16
+ - 降级不阻塞: 越权/无契约头/校验警告 → 打印提示但 exit 0 (让用户决定)
17
+ - 只有"硬错误"(契约校验 FAIL) → exit 1, 阻止发布
18
+ - 统一入口: 所有产出型 skill 发布前调这一个脚本, 不用各自接 guard/contract
19
+
20
+ 用法:
21
+ python .qoder/scripts/common/check_publish.py <契约类型> <产出文件路径>
22
+ python .qoder/scripts/common/check_publish.py prd workspace/.../REQ-2026-001-xxx.md
23
+ python .qoder/scripts/common/check_publish.py design-spec workspace/.../xxx-design-spec.json
24
+ python .qoder/scripts/common/check_publish.py test-cases workspace/.../xxx-cases.json
25
+
26
+ 退出码:
27
+ 0 = 通过 (含软警告, 可发布)
28
+ 1 = 硬失败 (契约校验 FAIL, 建议不发布)
29
+ 2 = 参数错误 / 内部异常
30
+
31
+ 输出: 人类可读的检查报告 (角色 + 契约两段)
32
+ """
33
+
34
+ import sys
35
+
36
+
37
+ def run(contract_type, artifact_path):
38
+ """跑 guard + contract 双门检查。返回 exit code。"""
39
+ # ============ 门 1: 角色边界 (guard) ============
40
+ # 越权只软提示, 不阻塞 (ensure=False)
41
+ print('--- 角色边界检查 ---')
42
+ try:
43
+ from common.guard import check_produce
44
+ gr = check_produce(contract_type)
45
+ if gr.ok and not gr.is_warn:
46
+ print('✓', gr.msg)
47
+ else:
48
+ # 越权 (WARN): 提示但仍可继续
49
+ print(gr.msg)
50
+ if gr.hint:
51
+ print(' →', gr.hint)
52
+ print(' (软提示, 不阻塞发布。若需切换角色: /wl-init)')
53
+ except Exception as e:
54
+ print('⚠️ 角色检查不可用 ({}), 跳过'.format(str(e)[:60]))
55
+
56
+ # ============ 门 2: 契约校验 (contract) ============
57
+ print()
58
+ print('--- 契约校验 ---')
59
+ import os
60
+ if not os.path.isfile(artifact_path):
61
+ print('⚠️ 产出文件不存在: {}'.format(artifact_path))
62
+ print(' (跳过契约校验)')
63
+ return 0
64
+ try:
65
+ from common.contract import validate_contract
66
+ cr = validate_contract(artifact_path)
67
+ if cr.ok and not cr.is_warn:
68
+ print('✓', cr.msg)
69
+ return 0
70
+ elif cr.is_warn:
71
+ # 警告 (无契约头/字段警告/无schema): 软门禁, 可继续
72
+ print(cr.msg)
73
+ if cr.hint:
74
+ print(' →', cr.hint)
75
+ return 0
76
+ else:
77
+ # FAIL (硬错误): 缺必填/类型错/枚举越界 → 阻止
78
+ print('[FAIL]', cr.msg)
79
+ if cr.hint:
80
+ print(' →', cr.hint)
81
+ print(' (硬失败, 建议修正后再发布)')
82
+ return 1
83
+ except Exception as e:
84
+ print('⚠️ 契约校验异常 ({}), 跳过'.format(str(e)[:60]))
85
+ return 0
86
+
87
+
88
+ def main():
89
+ if len(sys.argv) < 3:
90
+ print(__doc__)
91
+ return 2
92
+ contract_type = sys.argv[1]
93
+ artifact_path = sys.argv[2]
94
+ return run(contract_type, artifact_path)
95
+
96
+
97
+ if __name__ == '__main__':
98
+ sys.exit(main())
@@ -0,0 +1,112 @@
1
+ # -*- coding: utf-8 -*-
2
+ """
3
+ cmd_registry.py - 命令注册器 (Command Pattern, 治 argparse 样板重复)
4
+
5
+ 现状: task.py 14 子命令(81行样板) + autotest.py 12 子命令(105行样板),
6
+ 每个子命令都要 add_parser + add_argument×N + set_defaults(func=) 三行,
7
+ 新增命令要改 3 处 (函数定义 + add_parser + set_defaults), 容易漏。
8
+
9
+ 本模块引入声明式注册: 用 (name, help, args_spec, func) 元组表 + build_parser
10
+ 一次性构建 subparser, 消除样板。
11
+
12
+ args_spec 格式 (简化 argparse.add_argument):
13
+ [
14
+ ('title', {...}), # 位置参数
15
+ ('--slug', {'help': 'slug'}), # 可选参数
16
+ ('--pri', {'choices':[1,2,3,4], 'type':int, 'default':3}),
17
+ ]
18
+
19
+ Usage:
20
+ from common.cmd_registry import build_parser
21
+
22
+ COMMANDS = [
23
+ ('create', '建任务', [
24
+ ('title', {'help': '任务标题'}),
25
+ ('--slug', {'help': 'URL slug'}),
26
+ ], cmd_create),
27
+ ('list', '列任务', [], cmd_list),
28
+ ]
29
+ parser = build_parser('task', '任务系统', COMMANDS)
30
+ args = parser.parse_args()
31
+ args.func(args) # set_defaults 已自动绑定
32
+
33
+ 收益: task/autotest 各省 ~60-80 行样板, 新增命令从改 3 处降到改 1 处元组行。
34
+ """
35
+
36
+ import argparse
37
+ import sys
38
+ from typing import Any, Callable, Dict, List, Sequence, Tuple
39
+
40
+ __all__ = ["build_parser", "run_command"]
41
+
42
+ # 类型别名: (name, help, args_spec, handler)
43
+ CommandSpec = Tuple[str, str, List[Tuple[str, Dict[str, Any]]], Callable]
44
+
45
+
46
+ def _add_args(subparser: argparse.ArgumentParser, args_spec: List[Tuple[str, Dict[str, Any]]]) -> None:
47
+ """把 args_spec 转成 subparser.add_argument 调用。"""
48
+ for name, opts in args_spec:
49
+ # 拆出 positional vs optional 的特殊处理
50
+ kwargs = dict(opts) if opts else {}
51
+ try:
52
+ subparser.add_argument(name, **kwargs)
53
+ except (TypeError, ValueError):
54
+ # 容错: 个别参数键不兼容时降级
55
+ subparser.add_argument(name)
56
+
57
+
58
+ def build_parser(
59
+ prog: str,
60
+ description: str,
61
+ commands: Sequence[CommandSpec],
62
+ *,
63
+ common_args: List[Tuple[str, Dict[str, Any]]] = None,
64
+ ) -> argparse.ArgumentParser:
65
+ """从命令元组表构建 argparse parser。
66
+
67
+ Args:
68
+ prog: 程序名 (如 'task')
69
+ description: 顶部描述 (如 '任务系统')
70
+ commands: [(name, help, args_spec, handler), ...] 命令元组表
71
+ common_args: 所有子命令共享的可选参数 (如 --platform)
72
+
73
+ Returns:
74
+ 配置好的 ArgumentParser, parse_args 后 args.func 即处理函数。
75
+ """
76
+ parser = argparse.ArgumentParser(prog=prog, description=description)
77
+ sub = parser.add_subparsers(dest="command")
78
+
79
+ for name, help_text, args_spec, handler in commands:
80
+ sp = sub.add_parser(name, help=help_text, description=help_text)
81
+ _add_args(sp, args_spec)
82
+ # 共享参数
83
+ if common_args:
84
+ _add_args(sp, common_args)
85
+ sp.set_defaults(func=handler)
86
+
87
+ return parser
88
+
89
+
90
+ def run_command(
91
+ commands: Sequence[CommandSpec],
92
+ argv: List[str] = None,
93
+ *,
94
+ prog: str = "",
95
+ description: str = "",
96
+ common_args: List[Tuple[str, Dict[str, Any]]] = None,
97
+ ) -> int:
98
+ """便捷运行: 构建 parser + parse + 调用 func。
99
+
100
+ Returns:
101
+ 处理函数的返回值 (通常是 int exit code), 无命令时返回 0。
102
+ """
103
+ parser = build_parser(prog or "cmd", description, commands, common_args=common_args)
104
+ args = parser.parse_args(argv if argv is not None else sys.argv[1:])
105
+ if not getattr(args, "command", None):
106
+ parser.print_help()
107
+ return 0
108
+ func = getattr(args, "func", None)
109
+ if func is None:
110
+ return 1
111
+ ret = func(args)
112
+ return int(ret) if isinstance(ret, int) else 0
@@ -0,0 +1,187 @@
1
+ # -*- coding: utf-8 -*-
2
+ """
3
+ config.py - Config 单例 (Singleton 模式, 治"4份 load_config + 明文凭据")
4
+
5
+ 现状: config.yaml 被至少 4 处各自重读 (autotest/git_sync/init_doctor/feishu),
6
+ 每次都 open + yaml.safe_load, 无缓存, 且段名用裸字符串散落各处 (拼写风险)。
7
+
8
+ 本模块引入单例 Config:
9
+ - 单次 yaml.safe_load + mtime 缓存 (config 改了自动重读)
10
+ - 14 段名抽常量 (Config.SECTIONS), 消拼写风险
11
+ - 类型化访问: Config.load().feishu / .autotest / .mysql ...
12
+ - 凭据从代码移除: mysql/zentao 密码只在 config.yaml, 不在源码默认值
13
+
14
+ Usage:
15
+ from common.config import Config, SECTIONS
16
+ cfg = Config.load() # 单例, 全进程共享
17
+ feishu = cfg.section('feishu') # {'enabled': false, ...}
18
+ mysql = cfg.mysql # 类型化便捷访问
19
+ if not feishu.get('enabled'): ...
20
+
21
+ 向后兼容: load_config() / load_config_section() 旧函数保留, 转发到 Config。
22
+ """
23
+
24
+ import os
25
+ import sys
26
+ import threading
27
+ from typing import Any, Dict, Optional
28
+
29
+ __all__ = ["Config", "SECTIONS", "load_config", "load_config_section"]
30
+
31
+ # 相对导入
32
+ try:
33
+ from .paths import get_repo_root
34
+ except ImportError:
35
+ _COMMON_DIR = os.path.dirname(os.path.abspath(__file__))
36
+ if _COMMON_DIR not in sys.path:
37
+ sys.path.insert(0, _COMMON_DIR)
38
+ from paths import get_repo_root # type: ignore
39
+
40
+
41
+ # ============================================================
42
+ # 14 个顶层段名常量 (单一真源, 消拼写风险)
43
+ # ============================================================
44
+
45
+ class SECTIONS:
46
+ """config.yaml 顶层段名。调用方用 SECTIONS.FEISHU 而非裸字符串 'feishu'。"""
47
+ ROLES = "roles"
48
+ PIPELINE = "pipeline"
49
+ MYSQL = "mysql"
50
+ ZENTAO = "zentao"
51
+ PLATFORMS = "platforms"
52
+ AUTOTEST = "autotest"
53
+ TEAM_REMOTE = "team_remote"
54
+ GIT_SYNC = "git_sync"
55
+ PRD_MODES = "prd_modes"
56
+ TASK = "task"
57
+ STATUS = "status"
58
+ INSIGHT = "insight"
59
+ FEISHU = "feishu"
60
+ NOTIFICATION = "notification"
61
+
62
+
63
+ # ============================================================
64
+ # Config 单例
65
+ # ============================================================
66
+
67
+ class _ConfigData:
68
+ """config.yaml 的类型化包装。由 Config.load() 返回实例。"""
69
+
70
+ def __init__(self, raw: Dict[str, Any]):
71
+ self._raw = raw or {}
72
+
73
+ def section(self, name: str, default: Optional[dict] = None) -> dict:
74
+ """取某段 (顶层 key)。不存在返回 default (默认 {})。"""
75
+ val = self._raw.get(name, {} if default is None else default)
76
+ return val if isinstance(val, dict) else ({} if default is None else default)
77
+
78
+ def get(self, name: str, default: Any = None) -> Any:
79
+ """取任意顶层 key (非段, 如标量)。"""
80
+ return self._raw.get(name, default)
81
+
82
+ @property
83
+ def raw(self) -> Dict[str, Any]:
84
+ """原始整份 dict (兼容旧 load_config() 返回)。"""
85
+ return self._raw
86
+
87
+ # ---- 类型化便捷访问 (常用段) ----
88
+ @property
89
+ def roles(self) -> dict:
90
+ return self.section(SECTIONS.ROLES)
91
+
92
+ @property
93
+ def mysql(self) -> dict:
94
+ return self.section(SECTIONS.MYSQL)
95
+
96
+ @property
97
+ def zentao(self) -> dict:
98
+ return self.section(SECTIONS.ZENTAO)
99
+
100
+ @property
101
+ def autotest(self) -> dict:
102
+ return self.section(SECTIONS.AUTOTEST)
103
+
104
+ @property
105
+ def platforms(self) -> dict:
106
+ return self.section(SECTIONS.PLATFORMS)
107
+
108
+ @property
109
+ def git_sync(self) -> dict:
110
+ return self.section(SECTIONS.GIT_SYNC)
111
+
112
+ @property
113
+ def feishu(self) -> dict:
114
+ return self.section(SECTIONS.FEISHU)
115
+
116
+ @property
117
+ def team_remote(self) -> dict:
118
+ return self.section(SECTIONS.TEAM_REMOTE)
119
+
120
+
121
+ class Config:
122
+ """config.yaml 单例加载器 (mtime 缓存)。
123
+
124
+ - 首次 load() 读文件 + yaml.safe_load, 缓存结果 + 文件 mtime
125
+ - 后续 load() 检查 mtime, 变了才重读 (热重载, 无需重启)
126
+ - config.yaml 不存在 → 返回空 ConfigData (软降级, 不崩)
127
+ - 线程安全 (Lock 保护首次加载)
128
+ """
129
+
130
+ _instance: Optional[_ConfigData] = None
131
+ _mtime: Optional[float] = None
132
+ _path: Optional[str] = None
133
+ _lock = threading.Lock()
134
+
135
+ @classmethod
136
+ def _config_path(cls) -> str:
137
+ if cls._path is None:
138
+ cls._path = str(get_repo_root() / ".qoder" / "config.yaml")
139
+ return cls._path
140
+
141
+ @classmethod
142
+ def load(cls) -> _ConfigData:
143
+ """加载/返回缓存的 config。mtime 变化时自动重读。"""
144
+ path = cls._config_path()
145
+ try:
146
+ cur_mtime = os.path.getmtime(path)
147
+ except OSError:
148
+ # 文件不存在 → 空配置 (软降级)
149
+ return _ConfigData({})
150
+ with cls._lock:
151
+ if cls._instance is not None and cls._mtime == cur_mtime:
152
+ return cls._instance
153
+ # (重)读
154
+ try:
155
+ import yaml
156
+ with open(path, encoding="utf-8") as f:
157
+ raw = yaml.safe_load(f) or {}
158
+ except Exception:
159
+ raw = {}
160
+ cls._instance = _ConfigData(raw)
161
+ cls._mtime = cur_mtime
162
+ return cls._instance
163
+
164
+ @classmethod
165
+ def reload(cls) -> _ConfigData:
166
+ """强制重读 (忽略缓存)。测试/配置热更新用。"""
167
+ cls._mtime = None
168
+ return cls.load()
169
+
170
+
171
+ # ============================================================
172
+ # 向后兼容: 旧函数签名转发到 Config 单例
173
+ # ============================================================
174
+
175
+ def load_config() -> dict:
176
+ """[兼容] 返回整份 config dict (替代 git_sync/init_doctor 各自的 load_config)。"""
177
+ return Config.load().raw
178
+
179
+
180
+ def load_config_section(section: str = SECTIONS.AUTOTEST, default: Optional[dict] = None) -> dict:
181
+ """[兼容] 返回某段 dict (替代 autotest/feishu 各自的 load_config_section/_load_config)。
182
+
183
+ Args:
184
+ section: 段名 (用 SECTIONS.XXX 常量, 不要裸字符串)
185
+ default: 段不存在时的返回值 (默认 {})
186
+ """
187
+ return Config.load().section(section, default if default is not None else {})