@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.
- package/bin/cli.js +213 -0
- package/package.json +11 -0
- package/templates/cli.js +198 -0
- package/templates/qoder/commands/wl-code.md +43 -0
- package/templates/qoder/commands/wl-commit.md +30 -0
- package/templates/qoder/commands/wl-init.md +80 -0
- package/templates/qoder/commands/wl-insight.md +51 -0
- package/templates/qoder/commands/wl-prd.md +199 -0
- package/templates/qoder/commands/wl-report.md +166 -0
- package/templates/qoder/commands/wl-search.md +52 -0
- package/templates/qoder/commands/wl-spec.md +18 -0
- package/templates/qoder/commands/wl-status.md +51 -0
- package/templates/qoder/commands/wl-task.md +71 -0
- package/templates/qoder/commands/wl-test.md +42 -0
- package/templates/qoder/config.toml +5 -0
- package/templates/qoder/config.yaml +141 -0
- package/templates/qoder/hooks/inject-workflow-state.py +117 -0
- package/templates/qoder/hooks/session-start.py +204 -0
- package/templates/qoder/rules/wl-pipeline.md +105 -0
- package/templates/qoder/scripts/add_session.py +245 -0
- package/templates/qoder/scripts/benchmark.py +209 -0
- package/templates/qoder/scripts/build_style_index.py +268 -0
- package/templates/qoder/scripts/code_index.py +41 -0
- package/templates/qoder/scripts/collect_prds.py +31 -0
- package/templates/qoder/scripts/common/__init__.py +0 -0
- package/templates/qoder/scripts/common/active_task.py +230 -0
- package/templates/qoder/scripts/common/atomicio.py +172 -0
- package/templates/qoder/scripts/common/developer.py +161 -0
- package/templates/qoder/scripts/common/eval_api.py +144 -0
- package/templates/qoder/scripts/common/feishu.py +278 -0
- package/templates/qoder/scripts/common/filelock.py +211 -0
- package/templates/qoder/scripts/common/identity.py +285 -0
- package/templates/qoder/scripts/common/mentions.py +134 -0
- package/templates/qoder/scripts/common/paths.py +311 -0
- package/templates/qoder/scripts/common/reqid.py +218 -0
- package/templates/qoder/scripts/common/search_engine.py +205 -0
- package/templates/qoder/scripts/common/task_utils.py +342 -0
- package/templates/qoder/scripts/common/terms.py +234 -0
- package/templates/qoder/scripts/common/utf8.py +38 -0
- package/templates/qoder/scripts/context_pack.py +196 -0
- package/templates/qoder/scripts/eval_prd.py +225 -0
- package/templates/qoder/scripts/export.py +487 -0
- package/templates/qoder/scripts/git_sync.py +1087 -0
- package/templates/qoder/scripts/handoff.py +22 -0
- package/templates/qoder/scripts/init_developer.py +76 -0
- package/templates/qoder/scripts/init_doctor.py +527 -0
- package/templates/qoder/scripts/install_qoderwork.py +339 -0
- package/templates/qoder/scripts/learn.py +67 -0
- package/templates/qoder/scripts/notify.py +5 -0
- package/templates/qoder/scripts/parse_prds.py +33 -0
- package/templates/qoder/scripts/report.py +281 -0
- package/templates/qoder/scripts/role.py +39 -0
- package/templates/qoder/scripts/run_weekly_update.bat +17 -0
- package/templates/qoder/scripts/run_weekly_update.sh +20 -0
- package/templates/qoder/scripts/search_index.py +352 -0
- package/templates/qoder/scripts/setup.py +453 -0
- package/templates/qoder/scripts/setup_weekly_cron.bat +22 -0
- package/templates/qoder/scripts/setup_weekly_cron.sh +19 -0
- package/templates/qoder/scripts/status.py +389 -0
- package/templates/qoder/scripts/syncgate.py +330 -0
- package/templates/qoder/scripts/task.py +954 -0
- package/templates/qoder/scripts/team.py +29 -0
- package/templates/qoder/scripts/team_sync.py +419 -0
- package/templates/qoder/scripts/workspace_init.py +102 -0
- package/templates/qoder/settings.json +53 -0
- package/templates/qoder/skills/design-review/SKILL.md +25 -0
- package/templates/qoder/skills/prd-generator/SKILL.md +180 -0
- package/templates/qoder/skills/prd-review/SKILL.md +36 -0
- package/templates/qoder/skills/prototype-generator/SKILL.md +141 -0
- package/templates/qoder/skills/spec-coder/SKILL.md +69 -0
- package/templates/qoder/skills/spec-generator/SKILL.md +67 -0
- package/templates/qoder/skills/test-generator/SKILL.md +72 -0
- package/templates/qoder/skills/wl-commit/SKILL.md +76 -0
- package/templates/qoder/skills/wl-init/SKILL.md +67 -0
- package/templates/qoder/skills/wl-insight/SKILL.md +81 -0
- package/templates/qoder/skills/wl-report/SKILL.md +87 -0
- package/templates/qoder/skills/wl-search/SKILL.md +75 -0
- package/templates/qoder/skills/wl-status/SKILL.md +61 -0
- package/templates/qoder/skills/wl-task/SKILL.md +58 -0
- package/templates/qoder/templates/prd-full-template.md +103 -0
- package/templates/qoder/templates/prd-quick-template.md +69 -0
- package/templates/qoder/templates/prototype-app.html +344 -0
- package/templates/qoder/templates/prototype-web.html +310 -0
- package/templates/root/AGENTS.md +182 -0
- package/templates/root/README-pipeline.md +56 -0
- package/templates/root/ROLES.md +85 -0
- 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())
|