@hupan56/wlkj 2.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 (87) hide show
  1. package/bin/cli.js +213 -0
  2. package/package.json +11 -0
  3. package/templates/cli.js +198 -0
  4. package/templates/qoder/commands/wl-code.md +43 -0
  5. package/templates/qoder/commands/wl-commit.md +30 -0
  6. package/templates/qoder/commands/wl-init.md +80 -0
  7. package/templates/qoder/commands/wl-insight.md +51 -0
  8. package/templates/qoder/commands/wl-prd.md +199 -0
  9. package/templates/qoder/commands/wl-report.md +166 -0
  10. package/templates/qoder/commands/wl-search.md +52 -0
  11. package/templates/qoder/commands/wl-spec.md +18 -0
  12. package/templates/qoder/commands/wl-status.md +51 -0
  13. package/templates/qoder/commands/wl-task.md +71 -0
  14. package/templates/qoder/commands/wl-test.md +42 -0
  15. package/templates/qoder/config.toml +5 -0
  16. package/templates/qoder/config.yaml +141 -0
  17. package/templates/qoder/hooks/inject-workflow-state.py +117 -0
  18. package/templates/qoder/hooks/session-start.py +204 -0
  19. package/templates/qoder/rules/wl-pipeline.md +105 -0
  20. package/templates/qoder/scripts/add_session.py +245 -0
  21. package/templates/qoder/scripts/benchmark.py +209 -0
  22. package/templates/qoder/scripts/build_style_index.py +268 -0
  23. package/templates/qoder/scripts/code_index.py +41 -0
  24. package/templates/qoder/scripts/collect_prds.py +31 -0
  25. package/templates/qoder/scripts/common/__init__.py +0 -0
  26. package/templates/qoder/scripts/common/active_task.py +230 -0
  27. package/templates/qoder/scripts/common/atomicio.py +172 -0
  28. package/templates/qoder/scripts/common/developer.py +161 -0
  29. package/templates/qoder/scripts/common/eval_api.py +144 -0
  30. package/templates/qoder/scripts/common/feishu.py +278 -0
  31. package/templates/qoder/scripts/common/filelock.py +211 -0
  32. package/templates/qoder/scripts/common/identity.py +285 -0
  33. package/templates/qoder/scripts/common/mentions.py +134 -0
  34. package/templates/qoder/scripts/common/paths.py +311 -0
  35. package/templates/qoder/scripts/common/reqid.py +218 -0
  36. package/templates/qoder/scripts/common/search_engine.py +205 -0
  37. package/templates/qoder/scripts/common/task_utils.py +342 -0
  38. package/templates/qoder/scripts/common/terms.py +234 -0
  39. package/templates/qoder/scripts/common/utf8.py +38 -0
  40. package/templates/qoder/scripts/context_pack.py +196 -0
  41. package/templates/qoder/scripts/eval_prd.py +225 -0
  42. package/templates/qoder/scripts/export.py +487 -0
  43. package/templates/qoder/scripts/git_sync.py +1087 -0
  44. package/templates/qoder/scripts/handoff.py +22 -0
  45. package/templates/qoder/scripts/init_developer.py +76 -0
  46. package/templates/qoder/scripts/init_doctor.py +527 -0
  47. package/templates/qoder/scripts/install_qoderwork.py +339 -0
  48. package/templates/qoder/scripts/learn.py +67 -0
  49. package/templates/qoder/scripts/notify.py +5 -0
  50. package/templates/qoder/scripts/parse_prds.py +33 -0
  51. package/templates/qoder/scripts/report.py +281 -0
  52. package/templates/qoder/scripts/role.py +39 -0
  53. package/templates/qoder/scripts/run_weekly_update.bat +17 -0
  54. package/templates/qoder/scripts/run_weekly_update.sh +20 -0
  55. package/templates/qoder/scripts/search_index.py +352 -0
  56. package/templates/qoder/scripts/setup.py +453 -0
  57. package/templates/qoder/scripts/setup_weekly_cron.bat +22 -0
  58. package/templates/qoder/scripts/setup_weekly_cron.sh +19 -0
  59. package/templates/qoder/scripts/status.py +389 -0
  60. package/templates/qoder/scripts/syncgate.py +330 -0
  61. package/templates/qoder/scripts/task.py +954 -0
  62. package/templates/qoder/scripts/team.py +29 -0
  63. package/templates/qoder/scripts/team_sync.py +419 -0
  64. package/templates/qoder/scripts/workspace_init.py +102 -0
  65. package/templates/qoder/settings.json +53 -0
  66. package/templates/qoder/skills/design-review/SKILL.md +25 -0
  67. package/templates/qoder/skills/prd-generator/SKILL.md +180 -0
  68. package/templates/qoder/skills/prd-review/SKILL.md +36 -0
  69. package/templates/qoder/skills/prototype-generator/SKILL.md +141 -0
  70. package/templates/qoder/skills/spec-coder/SKILL.md +69 -0
  71. package/templates/qoder/skills/spec-generator/SKILL.md +67 -0
  72. package/templates/qoder/skills/test-generator/SKILL.md +72 -0
  73. package/templates/qoder/skills/wl-commit/SKILL.md +76 -0
  74. package/templates/qoder/skills/wl-init/SKILL.md +67 -0
  75. package/templates/qoder/skills/wl-insight/SKILL.md +81 -0
  76. package/templates/qoder/skills/wl-report/SKILL.md +87 -0
  77. package/templates/qoder/skills/wl-search/SKILL.md +75 -0
  78. package/templates/qoder/skills/wl-status/SKILL.md +61 -0
  79. package/templates/qoder/skills/wl-task/SKILL.md +58 -0
  80. package/templates/qoder/templates/prd-full-template.md +103 -0
  81. package/templates/qoder/templates/prd-quick-template.md +69 -0
  82. package/templates/qoder/templates/prototype-app.html +344 -0
  83. package/templates/qoder/templates/prototype-web.html +310 -0
  84. package/templates/root/AGENTS.md +182 -0
  85. package/templates/root/README-pipeline.md +56 -0
  86. package/templates/root/ROLES.md +85 -0
  87. package/templates/root//346/226/260/346/211/213/346/214/207/345/215/227.md +186 -0
@@ -0,0 +1,22 @@
1
+ # handoff.py - Handoff management
2
+ import os, json
3
+ from datetime import datetime
4
+ BASE = os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
5
+
6
+ def create_handoff(task_id, from_role, to_role, artifacts, checklist):
7
+ td = os.path.join(BASE, 'workspace', 'tasks', task_id, 'handoff')
8
+ os.makedirs(td, exist_ok=True)
9
+ fname = from_role + '-to-' + to_role + '.json'
10
+ data = {
11
+ 'task_id': task_id,
12
+ 'from_role': from_role,
13
+ 'to_role': to_role,
14
+ 'artifacts': artifacts,
15
+ 'checklist': [{'item': c, 'status': 'pending'} for c in checklist],
16
+ 'status': 'pending',
17
+ 'created_at': datetime.now().isoformat()
18
+ }
19
+ with open(os.path.join(td, fname), 'w') as f:
20
+ json.dump(data, f, indent=2, ensure_ascii=False)
21
+ print('OK: handoff ' + from_role + ' -> ' + to_role)
22
+ return data
@@ -0,0 +1,76 @@
1
+ #!/usr/bin/env python3
2
+ # -*- coding: utf-8 -*-
3
+ """
4
+ QODER Pipeline - 开发者初始化
5
+
6
+ Usage:
7
+ python init_developer.py <developer-name>
8
+
9
+ 功能:
10
+ - 创建 .qoder/.developer 文件 (gitignored, 包含开发者身份信息)
11
+ - 创建 .qoder/workspace/<name>/ 目录结构
12
+ - 创建初始日志文件 journal-1.md
13
+ - 创建工作空间索引 index.md
14
+
15
+ 参考: Trellis 的 init_developer.py 设计
16
+
17
+ 示例:
18
+ python .qoder/scripts/init_developer.py zhangsan
19
+ python .qoder/scripts/init_developer.py john
20
+ """
21
+
22
+ from __future__ import annotations
23
+
24
+ import sys
25
+ import os
26
+
27
+ # 将 scripts 目录加入路径
28
+ sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
29
+
30
+ from common.paths import (
31
+ DIR_WORKFLOW,
32
+ FILE_DEVELOPER,
33
+ get_developer,
34
+ get_repo_root,
35
+ )
36
+ from common.developer import init_developer, show_developer_info
37
+
38
+
39
+ def main() -> None:
40
+ """CLI 入口。"""
41
+ if len(sys.argv) < 2:
42
+ print(f"Usage: {sys.argv[0]} <developer-name> [role]")
43
+ print()
44
+ print("Example:")
45
+ print(f" {sys.argv[0]} zhangsan")
46
+ print(f" {sys.argv[0]} zhangsan pm")
47
+ sys.exit(1)
48
+
49
+ name = sys.argv[1]
50
+ role = sys.argv[2] if len(sys.argv) > 2 else None
51
+
52
+ # 检查是否已初始化
53
+ existing = get_developer()
54
+ if existing == name:
55
+ print(f"Developer already initialized: {existing}")
56
+ print()
57
+ print("Current developer info:")
58
+ show_developer_info()
59
+ sys.exit(0)
60
+ elif existing:
61
+ print(f"Switching developer: {existing} -> {name}")
62
+
63
+ # 初始化 (幂等, 也用于切换开发者)
64
+ if init_developer(name, role):
65
+ print()
66
+ print("Next steps:")
67
+ print(f" 1. Your workspace is at: workspace/members/{name}/")
68
+ print(f" 2. Start creating tasks: python .qoder/scripts/task.py create \"My Task\"")
69
+ print(f" 3. Check status: python .qoder/scripts/task.py list")
70
+ sys.exit(0)
71
+ else:
72
+ sys.exit(1)
73
+
74
+
75
+ if __name__ == "__main__":
76
+ main()
@@ -0,0 +1,527 @@
1
+ #!/usr/bin/env python3
2
+ # -*- coding: utf-8 -*-
3
+ """
4
+ QODER Init Doctor - 幂等的一站式环境体检 + 按需修复
5
+
6
+ /wl-init 的引擎。设计原则:
7
+ - 幂等: 跑一百次和跑一次结果一样, 健康项直接跳过
8
+ - 增量: 永远不重头再来 —— 索引新鲜(7天内)则跳过, 过期则增量更新,
9
+ 只有索引完全缺失才全量构建
10
+ - 跨平台: Windows / macOS / Linux 纯 Python 实现
11
+
12
+ Usage:
13
+ python init_doctor.py # 只体检, 报告问题 (不改任何东西)
14
+ python init_doctor.py --fix # 体检 + 自动修复 (拉代码/建索引等)
15
+ python init_doctor.py --fix 小王 pm # 同时注册/切换开发者
16
+
17
+ 体检项:
18
+ 1. 基础环境 (python / git)
19
+ 2. 开发者身份 (.qoder/.developer)
20
+ 3. 团队仓库同步 (team_sync pull)
21
+ 4. 源码仓库 (data/code/* 按 config.yaml 克隆)
22
+ 5. 知识图谱新鲜度 (7 天内=健康 / 过期=增量 / 缺失=全量)
23
+ 6. 风格约束 (style-index / vben / chart reference)
24
+ 7. PRD 模板 (docx 源 + 蒸馏 md, docx 更新会提示重新蒸馏)
25
+ 8. 周五自动构建 (检测 + 给出当前系统的设置命令)
26
+ 9. QoderWork 桌面端技能 (可选, 检查 ~/.qoderwork/skills/ 的 junction)
27
+
28
+ Exit: 0 = 全部健康, 1 = 有问题未修复 (--fix 可修的会自动修)
29
+ """
30
+
31
+ import os
32
+ import sys
33
+ import subprocess
34
+ from datetime import datetime, timedelta
35
+
36
+ # UTF-8 stdio (防御性: stdout 被捕获时不崩溃)
37
+ try:
38
+ sys.stdout.reconfigure(encoding='utf-8', errors='replace')
39
+ except (AttributeError, TypeError, OSError, IOError):
40
+ try:
41
+ sys.stdout.reconfigure(encoding='utf-8')
42
+ except Exception:
43
+ pass
44
+
45
+ SCRIPTS_DIR = os.path.dirname(os.path.abspath(__file__))
46
+ sys.path.insert(0, SCRIPTS_DIR)
47
+
48
+ from common.paths import get_repo_root, get_developer, get_developer_info
49
+
50
+ BASE = str(get_repo_root())
51
+ CODE_DIR = os.path.join(BASE, 'data', 'code')
52
+ INDEX_DIR = os.path.join(BASE, 'data', 'index')
53
+ TPL_DIR = os.path.join(BASE, '.qoder', 'templates')
54
+ PRDTPL_DIR = os.path.join(BASE, 'data', 'docs', 'constitution', 'prdtemplate')
55
+
56
+ FRESH_DAYS = 7 # 索引多少天内算新鲜
57
+ PULL_STALE_HOURS = 24 # 距上次 pull 超过多少小时建议重新拉
58
+
59
+ OK, WARN, FIX, BAD = '[OK] ', '[WARN]', '[FIX] ', '[MISS]'
60
+
61
+ issues = [] # 未解决的问题
62
+ actions = [] # 本次执行的修复动作
63
+
64
+
65
+ def say(tag, msg):
66
+ print('{} {}'.format(tag, msg))
67
+
68
+
69
+ def problem(msg, hint=None):
70
+ issues.append(msg)
71
+ say(BAD, msg)
72
+ if hint:
73
+ print(' -> ' + hint)
74
+
75
+
76
+ def run_script(script, *args, timeout=1800):
77
+ """跑同目录下的另一个脚本, 实时透传输出"""
78
+ cmd = [sys.executable, os.path.join(SCRIPTS_DIR, script)] + list(args)
79
+ r = subprocess.run(cmd, cwd=BASE, timeout=timeout)
80
+ return r.returncode == 0
81
+
82
+
83
+ def load_config():
84
+ cfg = os.path.join(BASE, '.qoder', 'config.yaml')
85
+ if not os.path.isfile(cfg):
86
+ return {}
87
+ try:
88
+ import yaml
89
+ with open(cfg, encoding='utf-8') as f:
90
+ return yaml.safe_load(f) or {}
91
+ except Exception:
92
+ return {}
93
+
94
+
95
+ # ============================================================
96
+ # 1. 基础环境
97
+ # ============================================================
98
+
99
+ def check_basics():
100
+ print('\n--- 1. 基础环境 ---')
101
+ say(OK, 'Python {}.{} ({})'.format(sys.version_info[0], sys.version_info[1], sys.platform))
102
+ # git 可选 (无 git 时核心功能仍可用, 仅团队同步禁用)
103
+ try:
104
+ r = subprocess.run(['git', '--version'], capture_output=True, text=True,
105
+ encoding='utf-8', errors='replace')
106
+ if r.returncode == 0:
107
+ say(OK, r.stdout.strip())
108
+ else:
109
+ say(WARN, 'git 不可用 — 团队同步/源码克隆将禁用, 本地功能仍可用')
110
+ except FileNotFoundError:
111
+ say(WARN, 'git 未安装 — 团队同步/源码克隆将禁用, 本地 PRD/搜索/任务/报告仍可用')
112
+
113
+
114
+ # ============================================================
115
+ # 2. 开发者身份
116
+ # ============================================================
117
+
118
+ def check_developer(fix, name=None, role=None):
119
+ print('\n--- 2. 开发者身份 ---')
120
+
121
+ # 名字自动探测: 显式参数 > git config user.name > 已注册的 .developer
122
+ if not name:
123
+ try:
124
+ r = subprocess.run(['git', 'config', 'user.name'],
125
+ capture_output=True, text=True, encoding='utf-8', errors='replace')
126
+ git_name = r.stdout.strip() if r.returncode == 0 else ''
127
+ if git_name:
128
+ name = git_name
129
+ print(' (从 git config 探测到名字: {})'.format(name))
130
+ except Exception:
131
+ pass
132
+
133
+ current = get_developer()
134
+ if not current and not name:
135
+ say(WARN, '未设置开发者身份, 也探测不到 git user.name')
136
+ print(' -> 建议: python .qoder/scripts/setup.py <你的名字>')
137
+ return None
138
+ if name and (not current or current != name):
139
+ if fix:
140
+ from common.developer import init_developer
141
+ if init_developer(name, role):
142
+ actions.append('注册/切换开发者: ' + name)
143
+ return name
144
+ problem('开发者初始化失败')
145
+ return None
146
+ problem('开发者待注册: ' + name, '加 --fix 自动注册')
147
+ return None
148
+ if current:
149
+ info = get_developer_info() or {}
150
+ say(OK, '开发者: {} ({})'.format(current, info.get('role', '?')))
151
+ # 个人空间目录
152
+ member = os.path.join(BASE, 'workspace', 'members', current)
153
+ missing = [s for s in ('drafts', 'inbox', 'journal')
154
+ if not os.path.isdir(os.path.join(member, s))]
155
+ if missing:
156
+ if fix:
157
+ for s in missing:
158
+ os.makedirs(os.path.join(member, s), exist_ok=True)
159
+ actions.append('补建个人空间子目录: ' + ', '.join(missing))
160
+ else:
161
+ problem('个人空间缺少子目录: ' + ', '.join(missing), '加 --fix 自动补建')
162
+ # git author 一致性 (影响 /wl-report 统计 + 零信任 push 门禁)
163
+ # git 未安装时跳过此项
164
+ git_name = ''
165
+ try:
166
+ r = subprocess.run(['git', 'config', 'user.name'], cwd=BASE,
167
+ capture_output=True, text=True, encoding='utf-8', errors='replace')
168
+ if r.returncode == 0:
169
+ git_name = r.stdout.strip()
170
+ except FileNotFoundError:
171
+ pass # 无 git, 跳过 author 一致性
172
+ if git_name and git_name != current:
173
+ say(WARN, 'git user.name="{}" 与开发者 "{}" 不一致, /wl-report 统计会查不到, 且 push 门禁会拒绝'.format(git_name, current))
174
+ if fix:
175
+ try:
176
+ subprocess.run(['git', 'config', '--local', 'user.name', current],
177
+ cwd=BASE, capture_output=True)
178
+ import json as _json
179
+ mj = os.path.join(BASE, 'workspace', 'members', current, 'member.json')
180
+ email_set = None
181
+ if os.path.isfile(mj):
182
+ with open(mj, encoding='utf-8') as f:
183
+ mdata = _json.load(f)
184
+ ga = mdata.get('git_author', '')
185
+ if '<' in ga and '>' in ga:
186
+ email_set = ga[ga.find('<')+1:ga.find('>')].strip()
187
+ if email_set:
188
+ subprocess.run(['git', 'config', '--local', 'user.email', email_set],
189
+ cwd=BASE, capture_output=True)
190
+ say(FIX, 'git config user.name="{}"{} (本仓库)'.format(
191
+ current, ' user.email="{}"'.format(email_set) if email_set else ''))
192
+ actions.append('修正 git user.name -> {} (本仓库)'.format(current))
193
+ except Exception as e:
194
+ problem('自动修正 git user.name 失败: {}'.format(e),
195
+ '手动: git config --local user.name {}'.format(current))
196
+ else:
197
+ print(' -> 建议: git config --local user.name {} (或加 --fix 自动修)'.format(current))
198
+ return current
199
+ problem('未注册开发者', '运行 /wl-init <名字> <角色>, 或 init_doctor.py --fix <名字> <角色>')
200
+ return None
201
+
202
+
203
+ # ============================================================
204
+ # 3. 团队仓库同步
205
+ # ============================================================
206
+
207
+ def check_team_sync(fix):
208
+ print('\n--- 3. 团队仓库同步 ---')
209
+ marker = os.path.join(BASE, '.qoder', '.runtime', 'last-pull')
210
+ stale = True
211
+ if os.path.isfile(marker):
212
+ age = datetime.now() - datetime.fromtimestamp(os.path.getmtime(marker))
213
+ stale = age > timedelta(hours=PULL_STALE_HOURS)
214
+ if not stale:
215
+ say(OK, '团队仓库 {:.0f} 小时前已同步'.format(age.total_seconds() / 3600))
216
+ if stale:
217
+ if fix:
218
+ if run_script('team_sync.py', 'pull', timeout=120):
219
+ actions.append('拉取团队最新产出')
220
+ else:
221
+ say(WARN, '团队仓库拉取未成功 (离线? 冲突?), 不影响本地工作')
222
+ else:
223
+ say(WARN, '超过 {} 小时未同步团队仓库'.format(PULL_STALE_HOURS) + ', 加 --fix 自动拉取')
224
+
225
+
226
+ # ============================================================
227
+ # 4. 源码仓库
228
+ # ============================================================
229
+
230
+ def check_source_repos(fix, config):
231
+ print('\n--- 4. 源码仓库 (data/code/) ---')
232
+ projects = (config.get('git_sync', {}) or {}).get('projects', {}) or {}
233
+ if not projects:
234
+ say(WARN, 'config.yaml 未配置 git_sync.projects, 跳过')
235
+ return
236
+ os.makedirs(CODE_DIR, exist_ok=True)
237
+ for name, proj in projects.items():
238
+ pdir = os.path.join(CODE_DIR, name)
239
+ if os.path.isdir(os.path.join(pdir, '.git')):
240
+ say(OK, name)
241
+ continue
242
+ url = (proj or {}).get('url')
243
+ branch = (proj or {}).get('branch')
244
+ if not url:
245
+ problem('{} 缺失且无克隆地址'.format(name))
246
+ continue
247
+ if fix:
248
+ print(' 克隆 {} ({}) ... 大仓库可能需要几分钟'.format(name, branch or 'default'))
249
+ cmd = ['git', 'clone', '--single-branch']
250
+ if branch:
251
+ cmd += ['-b', branch]
252
+ cmd += [url, pdir]
253
+ r = subprocess.run(cmd, cwd=BASE)
254
+ if r.returncode == 0:
255
+ actions.append('克隆源码仓库: ' + name)
256
+ else:
257
+ problem('{} 克隆失败 (网络/权限?)'.format(name))
258
+ else:
259
+ problem('{} 源码缺失'.format(name), '加 --fix 自动克隆 ' + url)
260
+
261
+
262
+ # ============================================================
263
+ # 5. 知识图谱新鲜度 (增量的核心逻辑)
264
+ # ============================================================
265
+
266
+ def index_age_days():
267
+ """索引年龄: 取 .index-meta.json 的 last_sync, 退化用文件 mtime"""
268
+ meta_path = os.path.join(INDEX_DIR, '.index-meta.json')
269
+ ki_path = os.path.join(INDEX_DIR, 'keyword-index.json')
270
+ if not os.path.isfile(ki_path):
271
+ return None # 索引不存在
272
+ ts = None
273
+ if os.path.isfile(meta_path):
274
+ try:
275
+ import json
276
+ with open(meta_path, encoding='utf-8') as f:
277
+ meta = json.load(f)
278
+ ts = datetime.strptime(meta.get('last_sync', ''), '%Y-%m-%d %H:%M')
279
+ except Exception:
280
+ ts = None
281
+ if ts is None:
282
+ ts = datetime.fromtimestamp(os.path.getmtime(ki_path))
283
+ return (datetime.now() - ts).total_seconds() / 86400
284
+
285
+
286
+ def check_index(fix):
287
+ print('\n--- 5. 知识图谱 (data/index/) ---')
288
+ age = index_age_days()
289
+ if age is None:
290
+ # 完全缺失 -> 需要全量 (唯一允许"重头再来"的情形)
291
+ if fix:
292
+ if os.path.isdir(CODE_DIR) and os.listdir(CODE_DIR):
293
+ print(' 索引缺失, 全量构建 (一次性, 之后都是增量) ...')
294
+ if run_script('git_sync.py', '--index-only'):
295
+ actions.append('全量构建知识图谱')
296
+ else:
297
+ problem('索引构建失败, 查看上方报错')
298
+ else:
299
+ problem('索引缺失且无源码', '先解决第 4 项再重跑')
300
+ else:
301
+ problem('知识图谱不存在', '加 --fix 全量构建 (仅首次)')
302
+ return
303
+ if age <= FRESH_DAYS:
304
+ say(OK, '知识图谱 {:.1f} 天前更新过, 新鲜 (阈值 {} 天), 跳过'.format(age, FRESH_DAYS))
305
+ return
306
+ # 过期 -> 增量更新 (git pull 变更文件 diff, 不重建)
307
+ if fix:
308
+ print(' 索引 {:.1f} 天未更新, 增量同步 (只处理变更文件) ...'.format(age))
309
+ if run_script('git_sync.py'):
310
+ actions.append('增量更新知识图谱')
311
+ else:
312
+ say(WARN, '增量同步有报错, 详见上方 (索引仍可用, 只是可能略旧)')
313
+ else:
314
+ say(WARN, '知识图谱 {:.1f} 天未更新'.format(age) + ', 加 --fix 增量同步')
315
+
316
+
317
+ # ============================================================
318
+ # 6. 风格约束
319
+ # ============================================================
320
+
321
+ def check_style(fix):
322
+ print('\n--- 6. 风格约束 ---')
323
+ required = {
324
+ 'vben-style-reference.json': 'Web 设计 Token (Vben HSL)',
325
+ 'chart-style-reference.json': '看板/大屏风格先例',
326
+ 'icon-reference.json': '真实图标库 (antd SVG + Vant, 原型禁 emoji)',
327
+ 'style-index.json': 'UI 模式索引 (--style/--field 数据源)',
328
+ 'style-meta.json': '风格摘要 (会话注入)',
329
+ }
330
+ missing_buildable = False
331
+ for fname, desc in required.items():
332
+ if os.path.isfile(os.path.join(INDEX_DIR, fname)):
333
+ say(OK, '{} - {}'.format(fname, desc))
334
+ elif fname in ('style-index.json', 'style-meta.json'):
335
+ missing_buildable = True
336
+ if not fix:
337
+ problem(fname + ' 缺失', '加 --fix 自动构建')
338
+ else:
339
+ problem(fname + ' 缺失', '该文件提交在 git 中, 先解决第 3 项团队同步')
340
+ tpls = ['prototype-web.html', 'prototype-app.html']
341
+ for t in tpls:
342
+ if os.path.isfile(os.path.join(TPL_DIR, t)):
343
+ say(OK, '原型模板 ' + t)
344
+ else:
345
+ problem('原型模板缺失: ' + t)
346
+ if missing_buildable and fix:
347
+ if os.path.isdir(CODE_DIR) and os.listdir(CODE_DIR):
348
+ print(' 构建风格索引 ...')
349
+ if run_script('build_style_index.py'):
350
+ actions.append('构建风格索引')
351
+ else:
352
+ problem('风格索引缺源码无法构建', '先解决第 4 项')
353
+
354
+
355
+ # ============================================================
356
+ # 7. PRD 模板
357
+ # ============================================================
358
+
359
+ def check_prd_templates():
360
+ print('\n--- 7. PRD 模板 + Qoder 载体 ---')
361
+ pairs = [
362
+ ('PRD通用模板-纯框架版.docx', 'prd-full-template.md'),
363
+ ('零星需求PRD简易模板.docx', 'prd-quick-template.md'),
364
+ ]
365
+ for docx, md in pairs:
366
+ docx_path = os.path.join(PRDTPL_DIR, docx)
367
+ md_path = os.path.join(TPL_DIR, md)
368
+ has_docx = os.path.isfile(docx_path)
369
+ has_md = os.path.isfile(md_path)
370
+ if has_md and has_docx:
371
+ if os.path.getmtime(docx_path) > os.path.getmtime(md_path):
372
+ say(WARN, '{} 比蒸馏版 {} 新, 让 AI 重新蒸馏一次'.format(docx, md))
373
+ else:
374
+ say(OK, '{} (源: {})'.format(md, docx))
375
+ elif has_md:
376
+ say(OK, md + ' (docx 源缺失, 蒸馏版仍可用)')
377
+ else:
378
+ problem('PRD 模板缺失: ' + md, '需要 AI 从团队章程 docx 重新蒸馏')
379
+
380
+ # Qoder 全系列载体: rules 在 IDE/Quest 生效, AGENTS.md 在 CLI 生效
381
+ rules_file = os.path.join(BASE, '.qoder', 'rules', 'wl-pipeline.md')
382
+ if os.path.isfile(rules_file):
383
+ say(OK, '.qoder/rules/wl-pipeline.md (Qoder IDE/Quest 规则载体)')
384
+ else:
385
+ problem('.qoder/rules/wl-pipeline.md 缺失', 'Quest 模式将不知道工作流规则, 从 git 同步取回')
386
+ if os.path.isfile(os.path.join(BASE, 'AGENTS.md')):
387
+ say(OK, 'AGENTS.md (Qoder CLI / 其他 Agent 工具载体)')
388
+ else:
389
+ problem('AGENTS.md 缺失')
390
+ wiki = os.path.join(BASE, '.qoder', 'repowiki')
391
+ if os.path.isdir(wiki):
392
+ say(OK, '.qoder/repowiki 存在 (Repo Wiki 知识源可用)')
393
+ else:
394
+ say(WARN, '未生成 Repo Wiki (可选): Qoder IDE 中触发生成后提交, 团队共享模块级文档')
395
+
396
+
397
+ # ============================================================
398
+ # 8. 周五自动构建
399
+ # ============================================================
400
+
401
+ def check_weekly(fix):
402
+ print('\n--- 8. 周五自动构建 ---')
403
+ log = os.path.join(BASE, '.qoder', 'logs', 'weekly-update.log')
404
+ if os.path.isfile(log):
405
+ age = (datetime.now() - datetime.fromtimestamp(os.path.getmtime(log))).days
406
+ if age <= FRESH_DAYS:
407
+ say(OK, '自动构建 {} 天前运行过'.format(age))
408
+ return
409
+ say(WARN, '自动构建日志已 {} 天未更新, 计划任务可能失效'.format(age))
410
+ else:
411
+ say(WARN, '未发现自动构建日志 (本机可能未设置周五任务)')
412
+ print(' 本机不是更新机也没关系: 团队中只需一台机器跑周五任务,')
413
+ print(' 其他人 /wl-init 时会自动 git pull 拿到最新图谱。')
414
+ if sys.platform == 'win32':
415
+ print(' 本机设置(Windows): .qoder\\scripts\\setup_weekly_cron.bat')
416
+ else:
417
+ print(' 本机设置(macOS/Linux): bash .qoder/scripts/setup_weekly_cron.sh')
418
+
419
+
420
+ # ============================================================
421
+ # 9. QoderWork 桌面端技能安装(可选,跨产品族打通)
422
+ # ============================================================
423
+
424
+ def check_qoderwork():
425
+ """检查 QoderWork 桌面端是否已安装本项目的技能(junction 到 ~/.qoderwork/skills/)。"""
426
+ print('\n--- 9. QoderWork 桌面端技能 (可选) ---')
427
+ src_skills = os.path.join(BASE, '.qoder', 'skills')
428
+ if not os.path.isdir(src_skills):
429
+ say(WARN, '.qoder/skills/ 不存在, 跳过 QoderWork 检查')
430
+ return
431
+
432
+ # 数源 skill 个数
433
+ src_count = sum(
434
+ 1 for d in os.listdir(src_skills)
435
+ if os.path.isfile(os.path.join(src_skills, d, 'SKILL.md'))
436
+ )
437
+ if src_count == 0:
438
+ say(WARN, '.qoder/skills/ 下无有效 SKILL.md')
439
+ return
440
+
441
+ # QoderWork 目标目录
442
+ home = os.environ.get('USERPROFILE') or os.environ.get('HOME') or os.path.expanduser('~')
443
+ qw_dir = os.path.join(home, '.qoderwork', 'skills')
444
+ if not os.path.isdir(qw_dir):
445
+ say(WARN, 'QoderWork 未安装技能 ({})'.format(qw_dir))
446
+ print(' 若你用 QoderWork 桌面端, 跑: python .qoder/scripts/install_qoderwork.py')
447
+ print(' (不用 QoderWork 可忽略此项)')
448
+ return
449
+
450
+ # 数目标里属于本项目的 junction
451
+ installed = 0
452
+ not_junction = []
453
+ for d in os.listdir(qw_dir):
454
+ src_test = os.path.join(src_skills, d, 'SKILL.md')
455
+ if not os.path.isfile(src_test):
456
+ continue # 不是本项目的 skill
457
+ link = os.path.join(qw_dir, d)
458
+ # 检测 reparse point(junction 的标志)
459
+ try:
460
+ import stat as _stat
461
+ st = os.lstat(link)
462
+ is_reparse = bool(getattr(st, 'st_file_attributes', 0) & 0x400)
463
+ except (OSError, AttributeError):
464
+ is_reparse = os.path.islink(link)
465
+ if is_reparse:
466
+ installed += 1
467
+ else:
468
+ not_junction.append(d)
469
+
470
+ if installed == src_count and not not_junction:
471
+ say(OK, 'QoderWork 技能已安装 ({}/{} junction)'.format(installed, src_count))
472
+ elif installed > 0:
473
+ say(WARN, 'QoderWork 技能部分安装 ({}/{})'.format(installed, src_count))
474
+ print(' 重跑同步: python .qoder/scripts/install_qoderwork.py')
475
+ if not_junction:
476
+ print(' 非 junction 的项目 (可能需人工处理): ' + ', '.join(not_junction))
477
+ issues.append('QoderWork 技能部分安装')
478
+ else:
479
+ say(WARN, 'QoderWork 目录存在但无本项目技能')
480
+ print(' 安装: python .qoder/scripts/install_qoderwork.py')
481
+
482
+
483
+ # ============================================================
484
+ # Main
485
+ # ============================================================
486
+
487
+ def main():
488
+ args = [a for a in sys.argv[1:]]
489
+ fix = '--fix' in args
490
+ pos = [a for a in args if not a.startswith('--')]
491
+ name = pos[0] if pos else None
492
+ role = pos[1] if len(pos) > 1 else None
493
+
494
+ print('=' * 56)
495
+ print('QODER Init Doctor ({} 模式)'.format('体检+修复' if fix else '仅体检'))
496
+ print('项目: ' + BASE)
497
+ print('=' * 56)
498
+
499
+ config = load_config()
500
+ check_basics()
501
+ check_developer(fix, name, role)
502
+ check_team_sync(fix)
503
+ check_source_repos(fix, config)
504
+ check_index(fix)
505
+ check_style(fix)
506
+ check_prd_templates()
507
+ check_weekly(fix)
508
+ check_qoderwork()
509
+
510
+ print('\n' + '=' * 56)
511
+ if actions:
512
+ print('本次修复: ')
513
+ for a in actions:
514
+ print(' + ' + a)
515
+ if issues:
516
+ print('未解决问题 ({}):'.format(len(issues)))
517
+ for i in issues:
518
+ print(' - ' + i)
519
+ print('=' * 56)
520
+ return 1
521
+ print('环境健康, 可以开始工作: /wl-prd <需求描述>')
522
+ print('=' * 56)
523
+ return 0
524
+
525
+
526
+ if __name__ == '__main__':
527
+ sys.exit(main())