@hupan56/wlkj 2.7.12 → 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.
- package/bin/cli.js +344 -78
- package/package.json +29 -29
- package/templates/.qoder/.runtime/ctx-cache-5660152f1d6dd819.md +23 -0
- package/templates/.qoder/.runtime/ctx-cache-afdce0dac06b25b0.md +23 -0
- package/templates/.qoder/.runtime/search-cache-eae7644e7b122f35.txt +1 -0
- package/templates/.qoder/learning/eval-history.jsonl +28 -0
- package/templates/data/index/wiki-index.json +8 -0
- 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 +159 -161
- package/templates/qoder/commands/optional/wl-report.md +4 -4
- package/templates/qoder/commands/optional/wl-status.md +2 -2
- package/templates/qoder/commands/wl-code.md +2 -2
- package/templates/qoder/commands/wl-design.md +2 -2
- package/templates/qoder/commands/wl-init.md +3 -3
- package/templates/qoder/commands/wl-prd.md +2 -2
- package/templates/qoder/commands/wl-req.md +43 -0
- package/templates/qoder/commands/wl-search.md +8 -8
- package/templates/qoder/commands/wl-task.md +17 -17
- package/templates/qoder/commands/wl-test.md +41 -15
- package/templates/qoder/config.yaml +17 -1
- package/templates/qoder/hooks/session-start.py +12 -22
- package/templates/qoder/nul +4 -0
- package/templates/qoder/rules/wl-pipeline.md +22 -48
- package/templates/qoder/scripts/README.md +139 -0
- package/templates/qoder/scripts/common/autotest_auth.py +109 -0
- package/templates/qoder/scripts/common/bootstrap.py +145 -0
- package/templates/qoder/scripts/common/check_publish.py +98 -0
- package/templates/qoder/scripts/common/cmd_registry.py +112 -0
- package/templates/qoder/scripts/common/config.py +187 -0
- package/templates/qoder/scripts/common/contract.py +317 -0
- package/templates/qoder/scripts/common/developer.py +2 -1
- package/templates/qoder/scripts/common/feishu.py +10 -9
- package/templates/qoder/scripts/common/guard.py +159 -0
- package/templates/qoder/scripts/common/identity.py +121 -2
- package/templates/qoder/scripts/common/kg_capabilities.py +182 -0
- package/templates/qoder/scripts/common/mcp_base.py +268 -0
- package/templates/qoder/scripts/common/paths.py +187 -1
- package/templates/qoder/scripts/common/result.py +223 -0
- package/templates/qoder/scripts/common/roles.py +60 -0
- package/templates/qoder/scripts/common/task_utils.py +21 -9
- package/templates/qoder/scripts/common/test_extract.py +115 -0
- package/templates/qoder/scripts/kg/__init__.py +11 -0
- package/templates/qoder/scripts/kg/build_entity_registry.py +196 -0
- package/templates/qoder/scripts/kg/build_relations.py +127 -0
- package/templates/qoder/scripts/{build_style_index.py → kg/build_style_index.py} +39 -10
- package/templates/qoder/scripts/kg/build_workflows.py +144 -0
- package/templates/qoder/scripts/{context_pack.py → kg/context_pack.py} +18 -11
- package/templates/qoder/scripts/{enrich_prompt.py → kg/enrich_prompt.py} +232 -226
- package/templates/qoder/scripts/{extract_api_params.py → kg/extract.py} +398 -246
- package/templates/qoder/scripts/{kg.py → kg/kg.py} +638 -708
- package/templates/qoder/scripts/{kg_build.py → kg/kg_build.py} +618 -612
- package/templates/qoder/scripts/{kg_build_db.py → kg/kg_build_db.py} +333 -327
- package/templates/qoder/scripts/{kg_duckdb.py → kg/kg_duckdb.py} +38 -37
- package/templates/qoder/scripts/{kg_incremental.py → kg/kg_incremental.py} +420 -393
- package/templates/qoder/scripts/{kg_link_db.py → kg/kg_link_db.py} +230 -224
- package/templates/qoder/scripts/{kg_semantic.py → kg/kg_semantic.py} +156 -150
- package/templates/qoder/scripts/kg/prefetch.py +359 -0
- package/templates/qoder/scripts/{search_index.py → kg/search_index.py} +70 -14
- package/templates/qoder/scripts/mcp/__init__.py +11 -0
- package/templates/qoder/scripts/{kg_mcp_server.py → mcp/kg_mcp_server.py} +77 -272
- package/templates/qoder/scripts/{lanhu_stdio_wrapper.py → mcp/lanhu_stdio_wrapper.py} +125 -119
- package/templates/qoder/scripts/{check_mcp.py → mcp/mcp_doctor.py} +515 -298
- package/templates/qoder/scripts/{mcp_launcher.py → mcp/mcp_launcher.py} +442 -414
- package/templates/qoder/scripts/{mysql_mcp_server.py → mcp/mysql_mcp_server.py} +347 -396
- package/templates/qoder/scripts/{zentao_mcp_server.py → mcp/zentao_mcp_server.py} +384 -424
- package/templates/qoder/scripts/report/__init__.py +11 -0
- package/templates/qoder/scripts/{add_session.py → report/add_session.py} +250 -244
- package/templates/qoder/scripts/{archive_prd.py → report/archive_prd.py} +383 -377
- package/templates/qoder/scripts/{eval_prd.py → report/eval_prd.py} +73 -11
- package/templates/qoder/scripts/{export.py → report/export.py} +63 -0
- package/templates/qoder/scripts/{fill_prototype.py → report/fill_prototype.py} +6 -0
- package/templates/qoder/scripts/{gen_design_doc.py → report/gen_design_doc.py} +400 -394
- package/templates/qoder/scripts/{learn.py → report/learn.py} +152 -146
- package/templates/qoder/scripts/{learn_aggregate.py → report/learn_aggregate.py} +207 -201
- package/templates/qoder/scripts/{report.py → report/report.py} +287 -281
- package/templates/qoder/scripts/report/req.py +222 -0
- package/templates/qoder/scripts/report/role.py +33 -0
- package/templates/qoder/scripts/{status.py → report/status.py} +634 -628
- package/templates/qoder/scripts/setup/__init__.py +11 -0
- package/templates/qoder/scripts/setup/carriers.py +662 -0
- package/templates/qoder/scripts/{init_doctor.py → setup/init_doctor.py} +63 -26
- package/templates/qoder/scripts/{install_qoderwork.py → setup/install_qoderwork.py} +36 -26
- package/templates/qoder/scripts/{platform_doctor.py → setup/platform_doctor.py} +265 -259
- package/templates/qoder/scripts/{repo_root.py → setup/repo_root.py} +112 -106
- package/templates/qoder/scripts/{setup.py → setup/setup.py} +113 -4
- package/templates/qoder/scripts/{setup_lanhu.py → setup/setup_lanhu.py} +973 -963
- package/templates/qoder/scripts/task/__init__.py +11 -0
- package/templates/qoder/scripts/{git_sync.py → task/git_sync.py} +52 -31
- package/templates/qoder/scripts/{syncgate.py → task/syncgate.py} +6 -0
- package/templates/qoder/scripts/task/task.py +221 -0
- package/templates/qoder/scripts/task/task_lifecycle.py +596 -0
- package/templates/qoder/scripts/task/task_query.py +161 -0
- package/templates/qoder/scripts/task/task_relations.py +424 -0
- package/templates/qoder/scripts/{team_sync.py → task/team_sync.py} +93 -20
- package/templates/qoder/scripts/test/__init__.py +11 -0
- package/templates/qoder/scripts/{autotest.py → test/autotest.py} +1174 -1751
- package/templates/qoder/scripts/{autotest_batch.py → test/autotest_batch.py} +242 -224
- package/templates/qoder/scripts/test/autotest_data.py +675 -0
- package/templates/qoder/scripts/{autotest_run.py → test/autotest_run.py} +309 -297
- package/templates/qoder/scripts/{benchmark.py → test/benchmark.py} +6 -0
- package/templates/qoder/scripts/{kg_auto_login.py → test/kg_auto_login.py} +202 -196
- package/templates/qoder/scripts/{kg_test_runner.py → test/kg_test_runner.py} +7 -1
- package/templates/qoder/scripts/{page_probe.py → test/page_probe.py} +465 -459
- package/templates/qoder/scripts/wlkj.py +116 -0
- package/templates/qoder/settings.json +1 -10
- package/templates/qoder/skills/design-import/SKILL.md +226 -226
- package/templates/qoder/skills/design-review/SKILL.md +82 -82
- package/templates/qoder/skills/prd-generator/SKILL.md +26 -16
- package/templates/qoder/skills/prd-review/SKILL.md +5 -5
- package/templates/qoder/skills/prototype-generator/SKILL.md +256 -256
- package/templates/qoder/skills/spec-coder/SKILL.md +4 -4
- package/templates/qoder/skills/spec-generator/SKILL.md +4 -4
- package/templates/qoder/skills/test-generator/SKILL.md +5 -5
- package/templates/qoder/skills/wl-code/SKILL.md +4 -4
- package/templates/qoder/skills/wl-commit/SKILL.md +4 -4
- package/templates/qoder/skills/wl-design/SKILL.md +3 -3
- package/templates/qoder/skills/wl-init/SKILL.md +8 -8
- package/templates/qoder/skills/wl-insight/SKILL.md +5 -5
- package/templates/qoder/skills/wl-prd-full/SKILL.md +6 -6
- package/templates/qoder/skills/wl-prd-quick/SKILL.md +6 -6
- package/templates/qoder/skills/wl-prd-review/SKILL.md +4 -4
- package/templates/qoder/skills/wl-report/SKILL.md +7 -7
- package/templates/qoder/skills/wl-search/SKILL.md +13 -13
- package/templates/qoder/skills/wl-spec/SKILL.md +5 -5
- package/templates/qoder/skills/wl-status/SKILL.md +5 -5
- package/templates/qoder/skills/wl-task/SKILL.md +6 -6
- package/templates/qoder/skills/wl-test/SKILL.md +102 -39
- package/templates/root/AGENTS.md +39 -40
- 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_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_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/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/skills/prompt-enrich/SKILL.md +0 -90
- package/templates/qoder/skills/prototype-generator/SKILL.md.zcode-79180-2af4721f-f9a6-412c-88db-c0af680d211b.tmp +0 -0
- /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 {})
|