@hupan56/wlkj 3.1.31 → 3.2.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/package.json +1 -1
- package/templates/qoder/agents/insight-planning.md +67 -67
- package/templates/qoder/agents/prd-reference.md +47 -47
- package/templates/qoder/commands/optional/wl-insight.md +276 -276
- package/templates/qoder/commands/optional/wl-spec.md +10 -2
- package/templates/qoder/commands/wl-code.md +63 -4
- package/templates/qoder/commands/wl-design.md +1 -1
- package/templates/qoder/commands/wl-prd.md +87 -13
- package/templates/qoder/commands/wl-req.md +10 -3
- package/templates/qoder/commands/wl-task.md +613 -613
- package/templates/qoder/commands/wl-test.md +2 -2
- package/templates/qoder/contracts/CHANGELOG.md +418 -0
- package/templates/qoder/contracts/README.md +180 -0
- package/templates/qoder/contracts/code.md +82 -0
- package/templates/qoder/contracts/commit.md +86 -0
- package/templates/qoder/contracts/contract-header.md +76 -0
- package/templates/qoder/contracts/design.md +106 -0
- package/templates/qoder/contracts/fallback.md +126 -0
- package/templates/qoder/contracts/isolation.md +119 -0
- package/templates/qoder/contracts/prd.md +118 -0
- package/templates/qoder/contracts/schemas/design-spec.schema.json +46 -0
- package/templates/qoder/contracts/schemas/prd.schema.json +36 -0
- package/templates/qoder/contracts/schemas/test-cases.schema.json +40 -0
- package/templates/qoder/contracts/spec.md +112 -0
- package/templates/qoder/contracts/task.md +125 -0
- package/templates/qoder/contracts/test.md +112 -0
- package/templates/qoder/hooks/post-tool-use.py +20 -0
- package/templates/qoder/hooks/stop-eval.py +47 -0
- package/templates/qoder/rules/wl-pipeline.md +37 -0
- package/templates/qoder/scripts/deployment/setup/install_qoderwork.py +11 -0
- package/templates/qoder/scripts/deployment/setup/setup.py +70 -0
- package/templates/qoder/scripts/deployment/setup/wlkj_shim.py +104 -0
- package/templates/qoder/scripts/domain/deployment/deploy_to_test.py +298 -0
- package/templates/qoder/scripts/domain/kg/build/kg_build.py +241 -22
- package/templates/qoder/scripts/domain/kg/build/kg_incremental.py +108 -3
- package/templates/qoder/scripts/domain/kg/build/kg_signatures.py +169 -0
- package/templates/qoder/scripts/domain/kg/extract/ts_extract.py +111 -0
- package/templates/qoder/scripts/domain/kg/storage/kg_duckdb.py +43 -0
- package/templates/qoder/scripts/domain/requirement/req.py +134 -28
- package/templates/qoder/scripts/domain/task/zentao_panel.py +688 -53
- package/templates/qoder/scripts/foundation/core/paths.py +102 -0
- package/templates/qoder/scripts/protocol/mcp/zentao_mcp_server.py +23 -10
- package/templates/qoder/scripts/validation/eval/qwork_harness.py +1 -1
- package/templates/qoder/scripts/validation/eval/report-commands.md +2 -2
- package/templates/qoder/settings.json +27 -9
- package/templates/qoder/skills/design-import/SKILL.md +3 -3
- package/templates/qoder/skills/design-review/SKILL.md +1 -1
- package/templates/qoder/skills/prd-generator/SKILL.md +4 -4
- package/templates/qoder/skills/prd-review/SKILL.md +1 -1
- package/templates/qoder/skills/prototype-generator/SKILL.md +3 -3
- package/templates/qoder/skills/spec-coder/SKILL.md +1 -1
- package/templates/qoder/skills/spec-generator/SKILL.md +80 -23
- package/templates/qoder/skills/test-generator/SKILL.md +1 -1
- package/templates/qoder/skills/wl-code/SKILL.md +13 -1
- package/templates/qoder/skills/wl-commit/SKILL.md +1 -1
- package/templates/qoder/skills/wl-design/SKILL.md +6 -6
- package/templates/qoder/skills/wl-init/SKILL.md +2 -2
- package/templates/qoder/skills/wl-insight/SKILL.md +5 -5
- package/templates/qoder/skills/wl-prd/SKILL.md +60 -0
- package/templates/qoder/skills/wl-report/SKILL.md +2 -2
- package/templates/qoder/skills/wl-search/SKILL.md +1 -1
- package/templates/qoder/skills/wl-spec/SKILL.md +2 -2
- package/templates/qoder/skills/wl-status/SKILL.md +2 -2
- package/templates/qoder/skills/wl-task/SKILL.md +3 -3
- package/templates/qoder/skills/wl-test/SKILL.md +2 -2
- package/templates/qoder/templates/spec-template.md +124 -0
- package/templates/root/AGENTS.md +32 -4
- package/templates/qoder/skills/wl-prd-full/SKILL.md +0 -121
- package/templates/qoder/skills/wl-prd-quick/SKILL.md +0 -50
- package/templates/qoder/skills/wl-prd-review/SKILL.md +0 -47
|
@@ -105,6 +105,35 @@ def _recent_prd_files():
|
|
|
105
105
|
return candidates
|
|
106
106
|
|
|
107
107
|
|
|
108
|
+
# 快速模式 6 章必需标题 (与 prd-quick-template.md 对齐)
|
|
109
|
+
QUICK_REQUIRED_SECTIONS = [
|
|
110
|
+
'功能入口', '需求背景', '需求说明', '影响范围', '验收标准', '不在本次范围',
|
|
111
|
+
]
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
def _check_quick_prd_complete(prd_path):
|
|
115
|
+
"""检测 quick 模式 PRD 是否缺章。返回缺失章节列表 (空=完整)。
|
|
116
|
+
|
|
117
|
+
判定 quick 模式: @contract 头含 mode: quick。
|
|
118
|
+
必需 6 章: 功能入口/需求背景/需求说明/影响范围/验收标准/不在本次范围。
|
|
119
|
+
治"残废品" (真实案例: 海外考勤 PRD 只写"功能入口"一段, 6 章缺 5 章)。
|
|
120
|
+
"""
|
|
121
|
+
try:
|
|
122
|
+
text = open(prd_path, encoding='utf-8', errors='replace').read()
|
|
123
|
+
except Exception:
|
|
124
|
+
return [] # 读不到不拦 (误拦比放过更糟)
|
|
125
|
+
# 只检 quick 模式 (完整档 13 章另算, 不在此 hook 范围)
|
|
126
|
+
if 'mode: quick' not in text[:500]:
|
|
127
|
+
return []
|
|
128
|
+
missing = []
|
|
129
|
+
for sec in QUICK_REQUIRED_SECTIONS:
|
|
130
|
+
# 章节标题: ## 功能入口 / ## 需求背景 等 (允许前后空格)
|
|
131
|
+
import re
|
|
132
|
+
if not re.search(r'^#{1,3}\s*' + sec, text, re.MULTILINE):
|
|
133
|
+
missing.append(sec)
|
|
134
|
+
return missing
|
|
135
|
+
|
|
136
|
+
|
|
108
137
|
def _evald_prds():
|
|
109
138
|
"""读 eval-history.jsonl, 返回【已通过门禁】的 prd 文件名集合。
|
|
110
139
|
|
|
@@ -161,6 +190,24 @@ def main():
|
|
|
161
190
|
recent = _recent_prd_files()
|
|
162
191
|
if not recent:
|
|
163
192
|
sys.exit(0) # 本轮没产 PRD, 放行
|
|
193
|
+
|
|
194
|
+
# ── 先查 quick 模式 PRD 缺章 (残废品门禁, 比 eval 更优先) ──
|
|
195
|
+
incomplete = []
|
|
196
|
+
for p in recent:
|
|
197
|
+
miss = _check_quick_prd_complete(p)
|
|
198
|
+
if miss:
|
|
199
|
+
incomplete.append((p, miss))
|
|
200
|
+
if incomplete:
|
|
201
|
+
lines = []
|
|
202
|
+
for p, miss in incomplete[:3]:
|
|
203
|
+
lines.append(' %s 缺: %s' % (os.path.basename(p), '、'.join(miss)))
|
|
204
|
+
sys.stderr.write(
|
|
205
|
+
'【PRD 残废品门禁】检测到快速模式 PRD 缺章 (6 章缺一不可):\n'
|
|
206
|
+
+ '\n'.join(lines) + '\n'
|
|
207
|
+
'修复: 补齐缺失章节的标题和内容 (不涉及的写一句话说明, 标题必须保留),\n'
|
|
208
|
+
' 章节顺序: 功能入口/需求背景/需求说明/影响范围/验收标准/不在本次范围。\n'
|
|
209
|
+
' 模板: .qoder/templates/prd-quick-template.md\n')
|
|
210
|
+
sys.exit(2)
|
|
164
211
|
evald = _evald_prds()
|
|
165
212
|
last_records = _last_record_by_prd()
|
|
166
213
|
# 区分两种未过门禁: 从没跑过 eval / 跑了但没过线
|
|
@@ -53,6 +53,19 @@ test -f .qoder/.developer && echo "已初始化" || echo "未初始化"
|
|
|
53
53
|
注意:用户说"同步/保存产出"通常指 team_sync(push),不是 git commit;
|
|
54
54
|
用户说"提交/commit 代码"才是 wl-commit。
|
|
55
55
|
|
|
56
|
+
## 规则 0.3:命令路由是最终决定(最高优先级,记忆/偏好不可覆盖)
|
|
57
|
+
|
|
58
|
+
**用户明确打了 `/wl-X` → 就走 X,绝不改道。** 这一条优先级高于一切(包括"问平台"),
|
|
59
|
+
任何"用户偏好""历史记忆""上下文暗示"都不可覆盖当前明确的命令。命令名 = 最终路由。
|
|
60
|
+
|
|
61
|
+
- 用户打 `/wl-prd`(哪怕描述像 bug:"无法提交,时间报错")→ **必须走 PRD**(为修复写需求),**不准**跑去 `/wl-test` / `/wl-code` / 排查
|
|
62
|
+
- 用户打 `/wl-code` → 走代码;`/wl-test` → 走测试;`/wl-task` → 走任务;其它命令同理
|
|
63
|
+
- 判断不了路由 → **问用户一句**,不要凭"偏好/记忆"自己猜
|
|
64
|
+
|
|
65
|
+
> 🚫 反面教材(禁止,真实案例):用户打 `/wl-prd 快速,园林绿化养护计划无法提交,时间有问题报错`,
|
|
66
|
+
> AI 因"记忆里用户偏好报bug走test"擅自改道 `/wl-test` → **违规**。命令是 `/wl-prd`,描述像 bug 也走 PRD
|
|
67
|
+
> (`wl-prd.md` 命令优先铁律连这个例子都举了)。"偏好/记忆"绝不可覆盖用户当前明确的命令。
|
|
68
|
+
|
|
56
69
|
## 规则 0.5:数据库查询必须先问环境(安全红线,问了就停)
|
|
57
70
|
|
|
58
71
|
AI 检测到需要查询**真实数据库**(调用 `mcp__qoder-mysql` 的 query_schema / query_data /
|
|
@@ -132,6 +145,30 @@ kg.py context(= context_pack)一条命令返回:相关代码文件 + 同
|
|
|
132
145
|
> 这是工作流比原生慢的根因——原生 AI 本能地并发,工作流却被"逐步执行"的文档节奏带成了串行。
|
|
133
146
|
> 把串行变并发,同样工作量耗时可砍到 1/3。MCP server(kg/mysql/zentao)都支持 JSON-RPC 并发请求。
|
|
134
147
|
|
|
148
|
+
## 规则 2.6:思考链收敛——先取证再推理(治"超长思考链")
|
|
149
|
+
|
|
150
|
+
**痛点:AI 取证了(读了代码)却没抓住矛盾,在假设上连续空转,15+ 步才定位 3 步能找到的根因。**
|
|
151
|
+
> ⚠️ 本规则**只约束排查/修码/取证类推理**;**探索/规划/写PRD** 类(用 feature/workflow/coverage/context 撒网理解业务)不受约束,反而要多用 kg。
|
|
152
|
+
|
|
153
|
+
### 2.6.1 先取证再推理(铁律)
|
|
154
|
+
每个推断必须立刻验证,**不许"假设A → 假设B → 假设C"连环空转**;验证不了就换方向。
|
|
155
|
+
**一轮工具调用 = 一轮推理**,不许"无工具调用的纯思考"超过 1 轮。
|
|
156
|
+
|
|
157
|
+
### 2.6.2 取证优先用知识层(kg 比 grep 快 10 倍,与规则 2 一脉相承)
|
|
158
|
+
- 相关代码文件 → `context_pack` 列文件(**别** grep 全库找)
|
|
159
|
+
- 字段真实类型/命名 → `query_schema` / `field_map` / `db_columns`(**别** Read 整个 Entity 翻)
|
|
160
|
+
- 改动波及面 → `table_impact` / `get_impact`(**别** 手动追踪调用链)
|
|
161
|
+
- API 路由/参数 → `code-api` 索引(**别** grep Controller)
|
|
162
|
+
→ **kg 给了文件才 Read 那几个;kg 给了类型就不读全文。kg 用得更准,不是闲置。**
|
|
163
|
+
|
|
164
|
+
### 2.6.3 排查/修码类先对齐链路(1-2 步对齐,再动手;kg 一次对齐多环)
|
|
165
|
+
- **后端报错/类型异常**:`query_schema`(DB列类型) + `context_pack`(BO/Entity文件) → Read 对齐 → 找不一致点
|
|
166
|
+
- **接口不通/400**:`code-api`(路由/参数) + `table_impact`(影响面)
|
|
167
|
+
- **前端白屏/NaN**:`context_pack`(组件/API) + `field_map`(字段映射)
|
|
168
|
+
|
|
169
|
+
> 正例:`query_schema` 得 DB列=Date + `context_pack` 得 BO=String → "类型不一致,改BO"(2步定位)
|
|
170
|
+
> 反例:假设null → 假设Invalid Date → 假设时区 → 假设disabledDate → …(15步空转,养护计划实录)
|
|
171
|
+
|
|
135
172
|
## 规则 3:原型风格必须来自真源(含图标)
|
|
136
173
|
|
|
137
174
|
- 起点永远是平台模板:.qoder/templates/prototype-web.html / prototype-app.html
|
|
@@ -537,6 +537,17 @@ def main():
|
|
|
537
537
|
print(f" [SKIP] .repo-root 未更新 ({PROJECT_ROOT} 非真仓库, 缺 workspace/data/.git)")
|
|
538
538
|
except OSError:
|
|
539
539
|
pass
|
|
540
|
+
# 5. 复制 wlkj 薄壳 (QoderWork 桌面端任意 cwd 直跑 wlkj, 不用每次先算 R)
|
|
541
|
+
# 治"相对路径在会话临时 cwd 失效"踩坑 (真实: 连续 2 条命令报错浪费一轮)
|
|
542
|
+
shim_src = SOURCE_COMMANDS_DIR.parent / "scripts" / "deployment" / "setup" / "wlkj_shim.py"
|
|
543
|
+
shim_dst = _HOME / ".qoderwork" / "wlkj.py"
|
|
544
|
+
try:
|
|
545
|
+
if shim_src.is_file():
|
|
546
|
+
import shutil as _sh2
|
|
547
|
+
_sh2.copy2(str(shim_src), str(shim_dst))
|
|
548
|
+
print(f" [OK] wlkj 薄壳: {shim_dst} (任意 cwd 可跑: python ~/.qoderwork/wlkj.py ...)")
|
|
549
|
+
except OSError as e:
|
|
550
|
+
print(f" [ERR] wlkj 薄壳复制失败: {e}")
|
|
540
551
|
return
|
|
541
552
|
|
|
542
553
|
print("=" * 56)
|
|
@@ -664,6 +664,73 @@ def offer_cron(skip=False):
|
|
|
664
664
|
print(' 跳过。以后可手动跑: bash .qoder/scripts/setup_weekly_cron.sh')
|
|
665
665
|
|
|
666
666
|
|
|
667
|
+
# ============================================================
|
|
668
|
+
# Step 3.6: 禅道凭据配置 (每个开发者配自己的禅道账号)
|
|
669
|
+
# ============================================================
|
|
670
|
+
|
|
671
|
+
def offer_zentao(name):
|
|
672
|
+
"""引导禅道凭据配置。
|
|
673
|
+
|
|
674
|
+
禅道 URL 团队共享(配在 config.yaml), 但用户名/密码是个人凭据,
|
|
675
|
+
存 workspace/members/{dev}/.private/secrets/zentao.env (gitignored)。
|
|
676
|
+
不配也能用 (kg/search/PRD 不依赖禅道), 但 /wl-task 禅道同步和
|
|
677
|
+
/wl-report 禅道查询需要。
|
|
678
|
+
"""
|
|
679
|
+
print('\n--- 禅道凭据 (可选, 不配也能用核心功能) ---')
|
|
680
|
+
|
|
681
|
+
# 检查 config.yaml 有没有禅道 URL
|
|
682
|
+
zentao_url = ''
|
|
683
|
+
try:
|
|
684
|
+
from foundation.core.config import load_config_section, SECTIONS
|
|
685
|
+
cfg = load_config_section(SECTIONS.ZENTAO)
|
|
686
|
+
zentao_url = (cfg.get('url') or '').strip()
|
|
687
|
+
except Exception:
|
|
688
|
+
pass
|
|
689
|
+
|
|
690
|
+
if not zentao_url:
|
|
691
|
+
print(' 团队未配置禅道 URL (config.yaml 无 zentao.url)。')
|
|
692
|
+
print(' 管理员配: 在 config.yaml 加 zentao.url: http://你的禅道地址')
|
|
693
|
+
print(' 跳过禅道配置。配好 URL 后重跑 init 即可。')
|
|
694
|
+
return
|
|
695
|
+
|
|
696
|
+
print(f' 禅道地址: {zentao_url}')
|
|
697
|
+
|
|
698
|
+
# 检查是否已配
|
|
699
|
+
try:
|
|
700
|
+
from foundation.core.paths import get_secrets_dir
|
|
701
|
+
secrets_dir = get_secrets_dir(name)
|
|
702
|
+
env_file = os.path.join(secrets_dir, 'zentao.env')
|
|
703
|
+
if os.path.isfile(env_file):
|
|
704
|
+
ok('禅道凭据已配置 (zentao.env 存在)')
|
|
705
|
+
return
|
|
706
|
+
except Exception:
|
|
707
|
+
pass
|
|
708
|
+
|
|
709
|
+
if not ask('配置禅道账号? (用于同步任务/需求/Bug)', 'n'):
|
|
710
|
+
print(' 跳过。以后需要: npx @hupan56/wlkj init <名字> <角色>')
|
|
711
|
+
return
|
|
712
|
+
|
|
713
|
+
# 交互式收集禅道凭据
|
|
714
|
+
try:
|
|
715
|
+
zentao_user = input(' 禅道用户名: ').strip()
|
|
716
|
+
zentao_pwd = input(' 禅道密码: ').strip()
|
|
717
|
+
if not zentao_user or not zentao_pwd:
|
|
718
|
+
warn('用户名/密码为空, 跳过')
|
|
719
|
+
return
|
|
720
|
+
|
|
721
|
+
# 写入 zentao.env
|
|
722
|
+
secrets_dir = os.path.join(os.getcwd(), 'workspace', 'members', name, '.private', 'secrets')
|
|
723
|
+
os.makedirs(secrets_dir, exist_ok=True)
|
|
724
|
+
env_file = os.path.join(secrets_dir, 'zentao.env')
|
|
725
|
+
with open(env_file, 'w', encoding='utf-8') as f:
|
|
726
|
+
f.write(f'ZENTAO_USER={zentao_user}\n')
|
|
727
|
+
f.write(f'ZENTAO_PASSWORD={zentao_pwd}\n')
|
|
728
|
+
ok(f'禅道凭据已保存: {env_file}')
|
|
729
|
+
print(' (个人凭据, 已 gitignored, 不会 push 给团队)')
|
|
730
|
+
except (EOFError, KeyboardInterrupt):
|
|
731
|
+
print(' 跳过')
|
|
732
|
+
|
|
733
|
+
|
|
667
734
|
# ============================================================
|
|
668
735
|
# Step 4.5: 蓝湖 MCP (设计师直读蓝湖设计稿)
|
|
669
736
|
# ============================================================
|
|
@@ -873,6 +940,9 @@ def main():
|
|
|
873
940
|
# Step 3.5: 团队协作仓库 remote (workspace/ 产出 push 到这里)
|
|
874
941
|
configure_team_remote()
|
|
875
942
|
|
|
943
|
+
# Step 3.6: 禅道凭据配置 (每个开发者都要配自己的禅道账号)
|
|
944
|
+
offer_zentao(name)
|
|
945
|
+
|
|
876
946
|
# Step 4
|
|
877
947
|
offer_cron(skip=args.skip_cron)
|
|
878
948
|
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
# -*- coding: utf-8 -*-
|
|
3
|
+
# wlkj_shim.py — QoderWork 桌面端 wlkj 薄壳 (零 import 依赖, 任意 cwd 直接跑)
|
|
4
|
+
#
|
|
5
|
+
# 为什么需要它:
|
|
6
|
+
# QoderWork 桌面端工作目录是临时会话目录 (workspace/xxx), 不是仓库根。
|
|
7
|
+
# 命令文件里写的相对路径 .qoder/scripts/orchestration/wlkj.py 全部失效
|
|
8
|
+
# (真实踩坑: AI 连续 2 条命令报错 "can't open file", 浪费一轮, 拖慢)。
|
|
9
|
+
# 本薄壳放在 ~/.qoderwork/wlkj.py, AI 只需记住一个绝对路径:
|
|
10
|
+
# python ~/.qoderwork/wlkj.py <命令> [参数...]
|
|
11
|
+
# 即可在任意 cwd 跑通工作流, 不用每次先算 R=repo_root。
|
|
12
|
+
#
|
|
13
|
+
# 机制 (与 repo_root.py 一致): 从 ~/.qoderwork/mcp.json 的 kg_mcp_server.py
|
|
14
|
+
# 绝对路径反推仓库根 (换机器重装后 mcp.json 会更新, 无需硬编码)。然后 subprocess
|
|
15
|
+
# 调真正的 wlkj.py, 透传全部 argv 和 exit code。
|
|
16
|
+
#
|
|
17
|
+
# 零依赖: 不能 import 仓库内任何模块 (定位器不能依赖被定位的东西)。
|
|
18
|
+
#
|
|
19
|
+
# 安装: install_qoderwork.py 会把本文件复制到 ~/.qoderwork/wlkj.py。
|
|
20
|
+
"""
|
|
21
|
+
wlkj_shim.py — QoderWork 桌面端 wlkj 薄壳。
|
|
22
|
+
|
|
23
|
+
用法:
|
|
24
|
+
python ~/.qoderwork/wlkj.py <命令> [参数...] # 任意 cwd, 直接转发到真 wlkj.py
|
|
25
|
+
python ~/.qoderwork/wlkj.py --where-repo # 只打印仓库根 (调试用)
|
|
26
|
+
|
|
27
|
+
等价于在仓库根跑:
|
|
28
|
+
python <repo_root>/.qoder/scripts/orchestration/wlkj.py <命令> [参数...]
|
|
29
|
+
"""
|
|
30
|
+
import json
|
|
31
|
+
import os
|
|
32
|
+
import subprocess
|
|
33
|
+
import sys
|
|
34
|
+
|
|
35
|
+
HOME = os.path.expanduser('~')
|
|
36
|
+
REPO_ROOT_SH = os.path.join(HOME, '.qoderwork', 'repo_root.py') # 已验证可用的定位器
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def _resolve_repo_root():
|
|
40
|
+
"""定位仓库根。优先调已验证的 ~/.qoderwork/repo_root.py (它走 .repo-root 锚点,
|
|
41
|
+
最可靠); 失败则自己读锚点兜底。返回绝对路径或 None。
|
|
42
|
+
|
|
43
|
+
与 repo_root.py 同走 ~/.qoderwork/.repo-root 锚点 (install 时写, 主路径)。
|
|
44
|
+
不重复造定位逻辑 —— repo_root.py 已经被验证可用。
|
|
45
|
+
"""
|
|
46
|
+
# 主路径: 调 repo_root.py (它输出仓库根绝对路径, 经 transcript 验证可用)
|
|
47
|
+
if os.path.isfile(REPO_ROOT_SH):
|
|
48
|
+
try:
|
|
49
|
+
out = subprocess.run([sys.executable, REPO_ROOT_SH],
|
|
50
|
+
capture_output=True, text=True, timeout=10)
|
|
51
|
+
line = out.stdout.strip().splitlines()
|
|
52
|
+
# repo_root.py 成功时 stdout 第一行是仓库根; 失败时 stderr 报错 stdout 空
|
|
53
|
+
if line and os.path.isdir(os.path.join(line[0], '.qoder', 'scripts')):
|
|
54
|
+
return line[0]
|
|
55
|
+
except Exception:
|
|
56
|
+
pass
|
|
57
|
+
# 兜底: 直接读锚点文件 (与 repo_root.py 方法1 一致)
|
|
58
|
+
anchor = os.path.join(HOME, '.qoderwork', '.repo-root')
|
|
59
|
+
try:
|
|
60
|
+
if os.path.isfile(anchor):
|
|
61
|
+
r = open(anchor, encoding='utf-8').read().strip()
|
|
62
|
+
if r and os.path.isdir(os.path.join(r, '.qoder', 'scripts')):
|
|
63
|
+
return r
|
|
64
|
+
except OSError:
|
|
65
|
+
pass
|
|
66
|
+
return None
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def main():
|
|
70
|
+
argv = sys.argv[1:]
|
|
71
|
+
if not argv or argv[0] in ('-h', '--help', 'help'):
|
|
72
|
+
sys.stderr.write(
|
|
73
|
+
'wlkj 薄壳 (QoderWork 桌面端专用, 任意 cwd 可跑)\n'
|
|
74
|
+
'用法: python ~/.qoderwork/wlkj.py <命令> [参数...]\n'
|
|
75
|
+
'调试: python ~/.qoderwork/wlkj.py --where-repo\n'
|
|
76
|
+
'命令同仓库内 wlkj.py (kg/search/context/prefetch/task/req/eval/...)\n')
|
|
77
|
+
return 0 if not argv else 0
|
|
78
|
+
|
|
79
|
+
if argv[0] == '--where-repo':
|
|
80
|
+
root = _resolve_repo_root()
|
|
81
|
+
print(root if root else '(未定位到仓库根, 检查 ~/.qoderwork/.repo-root 锚点)')
|
|
82
|
+
return 0
|
|
83
|
+
|
|
84
|
+
repo = _resolve_repo_root()
|
|
85
|
+
if not repo:
|
|
86
|
+
sys.stderr.write(
|
|
87
|
+
'【wlkj 薄壳】未定位到仓库根。\n'
|
|
88
|
+
' 修复: 在仓库内跑 python .qoder/scripts/deployment/setup/install_qoderwork.py\n'
|
|
89
|
+
' (会写 ~/.qoderwork/.repo-root 锚点)\n')
|
|
90
|
+
return 1
|
|
91
|
+
|
|
92
|
+
wlkj = os.path.join(repo, '.qoder', 'scripts', 'orchestration', 'wlkj.py')
|
|
93
|
+
if not os.path.isfile(wlkj):
|
|
94
|
+
sys.stderr.write('【wlkj 薄壳】真 wlkj.py 不存在: %s\n' % wlkj)
|
|
95
|
+
return 1
|
|
96
|
+
|
|
97
|
+
# 透传: 切到仓库根跑 (避免被调脚本依赖 cwd), 传全部 argv 和 exit code
|
|
98
|
+
r = subprocess.run([sys.executable, wlkj] + argv, cwd=repo)
|
|
99
|
+
return r.returncode
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
if __name__ == '__main__':
|
|
103
|
+
sys.exit(main())
|
|
104
|
+
|
|
@@ -0,0 +1,298 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
# -*- coding: utf-8 -*-
|
|
3
|
+
"""
|
|
4
|
+
deploy_to_test.py - 提交部署: 拉取 → cherry-pick到test分支 → 触发Jenkins构建
|
|
5
|
+
|
|
6
|
+
面板「提交」按钮调用。在 fywl-ics 代码仓库执行 git 操作。
|
|
7
|
+
|
|
8
|
+
三步:
|
|
9
|
+
1. 拉取当前分支最新代码
|
|
10
|
+
2. cherry-pick 当前提交到 test 分支 (冲突→abort→提示手动)
|
|
11
|
+
3. 从代码改动推断该构建哪个 Jenkins job → 触发构建
|
|
12
|
+
|
|
13
|
+
用法:
|
|
14
|
+
python deploy_to_test.py --json # 执行全部三步
|
|
15
|
+
python deploy_to_test.py --json --no-deploy # 只拉取+cherry-pick, 不触发Jenkins
|
|
16
|
+
python deploy_to_test.py --jobs # 列出可选的Jenkins job (供面板展示)
|
|
17
|
+
python deploy_to_test.py --json --job qas_ics_quality # 指定构建哪个job
|
|
18
|
+
|
|
19
|
+
配置 (从 .qoder/config.yaml 读, 没有就用默认):
|
|
20
|
+
deploy:
|
|
21
|
+
code_repo: data/code/fywl-ics # 代码仓库相对路径
|
|
22
|
+
test_branch: test # cherry-pick目标分支
|
|
23
|
+
jenkins_url: http://10.54.6.35:8866
|
|
24
|
+
jenkins_user: public_ics
|
|
25
|
+
jenkins_pass: a123456
|
|
26
|
+
jenkins_view: ICS-QAS # Jenkins视图名
|
|
27
|
+
|
|
28
|
+
退出码: 0成功; 3配置错误; 4冲突; 5网络/构建失败
|
|
29
|
+
"""
|
|
30
|
+
import os
|
|
31
|
+
import sys
|
|
32
|
+
import json
|
|
33
|
+
import argparse
|
|
34
|
+
import subprocess
|
|
35
|
+
|
|
36
|
+
# 路径自举
|
|
37
|
+
_HERE = os.path.dirname(os.path.abspath(__file__))
|
|
38
|
+
_SCRIPTS = os.path.dirname(os.path.dirname(os.path.dirname(_HERE))) # .qoder/scripts
|
|
39
|
+
_COMMON = os.path.join(_SCRIPTS, 'foundation')
|
|
40
|
+
for _p in (_SCRIPTS, _COMMON):
|
|
41
|
+
if _p not in sys.path:
|
|
42
|
+
sys.path.insert(0, _p)
|
|
43
|
+
try:
|
|
44
|
+
from bootstrap import setup; setup()
|
|
45
|
+
except Exception:
|
|
46
|
+
pass
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def _load_config():
|
|
50
|
+
"""从 config.yaml 读 deploy 段, 没有用默认。"""
|
|
51
|
+
import yaml
|
|
52
|
+
cfg_path = os.path.join(os.path.dirname(_SCRIPTS), 'config.yaml')
|
|
53
|
+
defaults = {
|
|
54
|
+
'code_repo': 'data/code/fywl-ics',
|
|
55
|
+
'test_branch': 'test',
|
|
56
|
+
'jenkins_url': 'http://10.54.6.35:8866',
|
|
57
|
+
'jenkins_user': 'public_ics',
|
|
58
|
+
'jenkins_pass': 'a123456',
|
|
59
|
+
'jenkins_view': 'ICS-QAS',
|
|
60
|
+
}
|
|
61
|
+
try:
|
|
62
|
+
with open(cfg_path, encoding='utf-8') as f:
|
|
63
|
+
full = yaml.safe_load(f) or {}
|
|
64
|
+
deploy = full.get('deploy', {})
|
|
65
|
+
defaults.update({k: v for k, v in deploy.items() if v})
|
|
66
|
+
except Exception:
|
|
67
|
+
pass
|
|
68
|
+
return defaults
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def _git(args, repo_dir, check=True):
|
|
72
|
+
"""在指定仓库目录执行 git 命令。返回 (returncode, stdout, stderr)。"""
|
|
73
|
+
r = subprocess.run(['git'] + args, capture_output=True, text=True,
|
|
74
|
+
cwd=repo_dir, timeout=30)
|
|
75
|
+
if check and r.returncode != 0:
|
|
76
|
+
raise RuntimeError('git %s 失败: %s' % (' '.join(args), r.stderr.strip()))
|
|
77
|
+
return r.returncode, r.stdout.strip(), r.stderr.strip()
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def _repo_root(cfg):
|
|
81
|
+
"""定位代码仓库根目录。"""
|
|
82
|
+
from foundation.core.paths import get_repo_root
|
|
83
|
+
base = get_repo_root()
|
|
84
|
+
repo = os.path.join(str(base), cfg['code_repo'])
|
|
85
|
+
if not os.path.isdir(os.path.join(repo, '.git')):
|
|
86
|
+
# 兜底: 往上找
|
|
87
|
+
repo = os.path.join(str(base), 'data', 'code', 'fywl-ics')
|
|
88
|
+
return repo
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
def step_pull(repo_dir):
|
|
92
|
+
"""第1步: 拉取当前分支最新代码。"""
|
|
93
|
+
_, branch, _ = _git(['branch', '--show-current'], repo_dir)
|
|
94
|
+
_git(['fetch', 'origin'], repo_dir, check=False)
|
|
95
|
+
rc, out, err = _git(['pull', 'origin', branch], repo_dir, check=False)
|
|
96
|
+
if rc != 0:
|
|
97
|
+
return {'ok': False, 'error': '拉取失败: %s' % err, 'branch': branch}
|
|
98
|
+
return {'ok': True, 'branch': branch, 'message': '已拉取 %s 分支' % branch}
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
def step_cherry_pick(repo_dir, test_branch):
|
|
102
|
+
"""第2步: cherry-pick 当前提交到 test 分支。冲突→abort→提示手动。"""
|
|
103
|
+
# 拿当前分支最新 commit
|
|
104
|
+
_, commit, _ = _git(['rev-parse', 'HEAD'], repo_dir)
|
|
105
|
+
_, cur_branch, _ = _git(['branch', '--show-current'], repo_dir)
|
|
106
|
+
|
|
107
|
+
# 切到 test 分支
|
|
108
|
+
_git(['fetch', 'origin', test_branch], repo_dir, check=False)
|
|
109
|
+
rc, out, err = _git(['checkout', test_branch], repo_dir, check=False)
|
|
110
|
+
if rc != 0:
|
|
111
|
+
# test 分支不存在, 从 origin 拉远程
|
|
112
|
+
rc2, out2, err2 = _git(['checkout', '-b', test_branch, 'origin/%s' % test_branch], repo_dir, check=False)
|
|
113
|
+
if rc2 != 0:
|
|
114
|
+
return {'ok': False, 'error': '切换test分支失败: %s' % err2}
|
|
115
|
+
|
|
116
|
+
_git(['pull', 'origin', test_branch], repo_dir, check=False)
|
|
117
|
+
|
|
118
|
+
# cherry-pick
|
|
119
|
+
rc, out, err = _git(['cherry-pick', commit], repo_dir, check=False)
|
|
120
|
+
if rc != 0:
|
|
121
|
+
# 冲突 → abort → 切回原分支 → 提示手动
|
|
122
|
+
_git(['cherry-pick', '--abort'], repo_dir, check=False)
|
|
123
|
+
_git(['checkout', cur_branch], repo_dir, check=False)
|
|
124
|
+
return {'ok': False, 'error': 'cherry-pick 到 %s 分支冲突, 已撤销。请手动解决:\n'
|
|
125
|
+
' git checkout %s && git cherry-pick %s' % (test_branch, test_branch, commit[:8]),
|
|
126
|
+
'conflict': True}
|
|
127
|
+
|
|
128
|
+
# 推送
|
|
129
|
+
rc, out, err = _git(['push', 'origin', test_branch], repo_dir, check=False)
|
|
130
|
+
if rc != 0:
|
|
131
|
+
return {'ok': False, 'error': '推送到 %s 失败: %s' % (test_branch, err)}
|
|
132
|
+
|
|
133
|
+
# 切回原分支
|
|
134
|
+
_git(['checkout', cur_branch], repo_dir, check=False)
|
|
135
|
+
|
|
136
|
+
return {'ok': True, 'commit': commit[:8], 'test_branch': test_branch,
|
|
137
|
+
'message': '已 cherry-pick %s 到 %s 并推送' % (commit[:8], test_branch)}
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
def _get_changed_modules(repo_dir):
|
|
141
|
+
"""从 git diff 提取改动的模块名 (用于推断 Jenkins job)。"""
|
|
142
|
+
_, out, _ = _git(['diff', '--name-only', 'HEAD~1'], repo_dir, check=False)
|
|
143
|
+
if not out:
|
|
144
|
+
_, out, _ = _git(['diff', '--name-only', '--cached'], repo_dir, check=False)
|
|
145
|
+
if not out:
|
|
146
|
+
_, out, _ = _git(['status', '--porcelain'], repo_dir, check=False)
|
|
147
|
+
modules = set()
|
|
148
|
+
for line in out.split('\n'):
|
|
149
|
+
line = line.strip().split()[-1] if line.strip() else ''
|
|
150
|
+
if not line:
|
|
151
|
+
continue
|
|
152
|
+
# 从路径提取模块: ics-modules/ics-quality/xxx → ics-quality
|
|
153
|
+
parts = line.split('/')
|
|
154
|
+
for i, p in enumerate(parts):
|
|
155
|
+
if p.startswith('ics-') and i + 1 < len(parts):
|
|
156
|
+
modules.add(p)
|
|
157
|
+
break
|
|
158
|
+
# fywl-ui 前端
|
|
159
|
+
if 'fywl-ui' in line or line.startswith('apps/'):
|
|
160
|
+
modules.add('ui')
|
|
161
|
+
return modules
|
|
162
|
+
|
|
163
|
+
|
|
164
|
+
def _list_jenkins_jobs(cfg):
|
|
165
|
+
"""拉 Jenkins 视图下的 job 列表。"""
|
|
166
|
+
import urllib.request, base64
|
|
167
|
+
url = '%s/view/%s/api/json?tree=jobs[name]' % (cfg['jenkins_url'], cfg['jenkins_view'])
|
|
168
|
+
req = urllib.request.Request(url)
|
|
169
|
+
cred = base64.b64encode(('%s:%s' % (cfg['jenkins_user'], cfg['jenkins_pass'])).encode()).decode()
|
|
170
|
+
req.add_header('Authorization', 'Basic ' + cred)
|
|
171
|
+
r = urllib.request.urlopen(req, timeout=10)
|
|
172
|
+
data = json.loads(r.read())
|
|
173
|
+
return [j['name'] for j in data.get('jobs', [])]
|
|
174
|
+
|
|
175
|
+
|
|
176
|
+
def _guess_jobs(modules, all_jobs):
|
|
177
|
+
"""从改动模块推断该构建哪些 Jenkins job。"""
|
|
178
|
+
matched = []
|
|
179
|
+
for mod in modules:
|
|
180
|
+
# 模块名 ics-quality → job名 qas_ics_quality 或 qas-ics-quality
|
|
181
|
+
mod_norm = mod.replace('-', '_').replace('_', '')
|
|
182
|
+
for job in all_jobs:
|
|
183
|
+
job_norm = job.replace('-', '_').replace('_', '').lower()
|
|
184
|
+
if mod_norm in job_norm:
|
|
185
|
+
matched.append(job)
|
|
186
|
+
return list(set(matched))
|
|
187
|
+
|
|
188
|
+
|
|
189
|
+
def step_jenkins_build(cfg, job_name):
|
|
190
|
+
"""第3步: 触发 Jenkins 构建。"""
|
|
191
|
+
import urllib.request, base64
|
|
192
|
+
# 先拿 crumb (Jenkins CSRF)
|
|
193
|
+
crumb_url = '%s/crumbIssuer/api/json' % cfg['jenkins_url']
|
|
194
|
+
cred = base64.b64encode(('%s:%s' % (cfg['jenkins_user'], cfg['jenkins_pass'])).encode()).decode()
|
|
195
|
+
headers = {'Authorization': 'Basic ' + cred}
|
|
196
|
+
try:
|
|
197
|
+
req = urllib.request.Request(crumb_url, headers=headers)
|
|
198
|
+
crumb_data = json.loads(urllib.request.urlopen(req, timeout=10).read())
|
|
199
|
+
crumb_field = crumb_data.get('crumbRequestField', '')
|
|
200
|
+
crumb_value = crumb_data.get('crumb', '')
|
|
201
|
+
headers[crumb_field] = crumb_value
|
|
202
|
+
except Exception:
|
|
203
|
+
pass # 没 crumb 也试
|
|
204
|
+
|
|
205
|
+
build_url = '%s/job/%s/build' % (cfg['jenkins_url'], job_name)
|
|
206
|
+
req = urllib.request.Request(build_url, data=b'', headers=headers, method='POST')
|
|
207
|
+
try:
|
|
208
|
+
r = urllib.request.urlopen(req, timeout=15)
|
|
209
|
+
return {'ok': True, 'job': job_name, 'message': '已触发 %s 构建 (HTTP %d)' % (job_name, r.status)}
|
|
210
|
+
except urllib.error.HTTPError as e:
|
|
211
|
+
if e.code == 201:
|
|
212
|
+
return {'ok': True, 'job': job_name, 'message': '已触发 %s 构建' % job_name}
|
|
213
|
+
return {'ok': False, 'error': '触发构建失败 (HTTP %d): %s' % (e.code, str(e)[:60])}
|
|
214
|
+
except Exception as e:
|
|
215
|
+
return {'ok': False, 'error': '触发构建异常: %s' % str(e)[:80]}
|
|
216
|
+
|
|
217
|
+
|
|
218
|
+
def deploy(job_name=None, skip_deploy=False):
|
|
219
|
+
"""执行部署全流程。返回 (result_dict, exit_code)。"""
|
|
220
|
+
cfg = _load_config()
|
|
221
|
+
repo_dir = _repo_root(cfg)
|
|
222
|
+
if not os.path.isdir(os.path.join(repo_dir, '.git')):
|
|
223
|
+
return {'ok': False, 'error': '代码仓库不存在: %s' % repo_dir}, 3
|
|
224
|
+
|
|
225
|
+
results = {'steps': []}
|
|
226
|
+
|
|
227
|
+
# ① 拉取
|
|
228
|
+
r1 = step_pull(repo_dir)
|
|
229
|
+
results['steps'].append({'step': 'pull', **r1})
|
|
230
|
+
if not r1['ok']:
|
|
231
|
+
return {'ok': False, 'error': r1['error'], 'results': results}, 5
|
|
232
|
+
|
|
233
|
+
# ② cherry-pick
|
|
234
|
+
r2 = step_cherry_pick(repo_dir, cfg['test_branch'])
|
|
235
|
+
results['steps'].append({'step': 'cherry-pick', **r2})
|
|
236
|
+
if not r2['ok']:
|
|
237
|
+
# 冲突不阻塞 Jenkins (开发可能只想先构建), 但标记冲突
|
|
238
|
+
results['conflict'] = r2.get('conflict', False)
|
|
239
|
+
|
|
240
|
+
# ③ Jenkins 构建
|
|
241
|
+
if not skip_deploy:
|
|
242
|
+
all_jobs = _list_jenkins_jobs(cfg)
|
|
243
|
+
if job_name:
|
|
244
|
+
# 指定了 job
|
|
245
|
+
r3 = step_jenkins_build(cfg, job_name)
|
|
246
|
+
else:
|
|
247
|
+
# 自动推断
|
|
248
|
+
modules = _get_changed_modules(repo_dir)
|
|
249
|
+
guessed = _guess_jobs(modules, all_jobs)
|
|
250
|
+
results['guessed_modules'] = list(modules)
|
|
251
|
+
results['guessed_jobs'] = guessed
|
|
252
|
+
if len(guessed) == 1:
|
|
253
|
+
r3 = step_jenkins_build(cfg, guessed[0])
|
|
254
|
+
elif len(guessed) > 1:
|
|
255
|
+
# 多个匹配 → 不自动构建, 让面板让用户选
|
|
256
|
+
r3 = {'ok': False, 'need_select': True, 'jobs': guessed,
|
|
257
|
+
'error': '改动涉及多个模块, 请选择要构建的job: %s' % ', '.join(guessed)}
|
|
258
|
+
else:
|
|
259
|
+
# 匹配不到 → 返回全部 job 让用户选
|
|
260
|
+
r3 = {'ok': False, 'need_select': True, 'jobs': all_jobs,
|
|
261
|
+
'error': '无法从改动推断构建模板, 请选择'}
|
|
262
|
+
results['steps'].append({'step': 'jenkins', **r3})
|
|
263
|
+
|
|
264
|
+
return {'ok': not results.get('conflict', False), 'results': results}, 0
|
|
265
|
+
|
|
266
|
+
|
|
267
|
+
def main():
|
|
268
|
+
parser = argparse.ArgumentParser(description='提交部署: 拉取→cherry-pick→Jenkins构建')
|
|
269
|
+
parser.add_argument('--json', action='store_true', help='JSON输出')
|
|
270
|
+
parser.add_argument('--no-deploy', action='store_true', help='只拉取+cherry-pick, 不触发Jenkins')
|
|
271
|
+
parser.add_argument('--job', help='指定构建的Jenkins job名')
|
|
272
|
+
parser.add_argument('--jobs', action='store_true', help='列出可选的Jenkins job')
|
|
273
|
+
args = parser.parse_args()
|
|
274
|
+
|
|
275
|
+
cfg = _load_config()
|
|
276
|
+
|
|
277
|
+
if args.jobs:
|
|
278
|
+
try:
|
|
279
|
+
jobs = _list_jenkins_jobs(cfg)
|
|
280
|
+
result = {'ok': True, 'jobs': jobs}
|
|
281
|
+
print(json.dumps(result, ensure_ascii=False))
|
|
282
|
+
except Exception as e:
|
|
283
|
+
print(json.dumps({'ok': False, 'error': str(e)}))
|
|
284
|
+
sys.exit(0)
|
|
285
|
+
|
|
286
|
+
result, code = deploy(job_name=args.job, skip_deploy=args.no_deploy)
|
|
287
|
+
|
|
288
|
+
if args.json:
|
|
289
|
+
print(json.dumps(result, ensure_ascii=False, default=str))
|
|
290
|
+
else:
|
|
291
|
+
for step in result.get('results', {}).get('steps', []):
|
|
292
|
+
status = 'OK' if step.get('ok') else 'FAIL'
|
|
293
|
+
print('[%s] %s: %s' % (status, step.get('step'), step.get('message', step.get('error', ''))))
|
|
294
|
+
sys.exit(code)
|
|
295
|
+
|
|
296
|
+
|
|
297
|
+
if __name__ == '__main__':
|
|
298
|
+
main()
|