@hupan56/wlkj 2.5.0 → 2.7.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 +289 -12
- package/package.json +1 -1
- package/templates/qoder/agents/insight-planning.md +67 -0
- package/templates/qoder/agents/insight-research.md +61 -0
- package/templates/qoder/agents/prd-quick.md +1 -0
- package/templates/qoder/agents/prd-reference.md +10 -2
- package/templates/qoder/commands/optional/wl-insight.md +275 -0
- package/templates/qoder/commands/{wl-report.md → optional/wl-report.md} +13 -5
- package/templates/qoder/commands/{wl-spec.md → optional/wl-spec.md} +1 -1
- package/templates/qoder/commands/{wl-status.md → optional/wl-status.md} +28 -2
- package/templates/qoder/commands/wl-code.md +10 -2
- package/templates/qoder/commands/wl-commit.md +1 -1
- package/templates/qoder/commands/wl-design-draw.md +78 -0
- package/templates/qoder/commands/wl-design-scan.md +108 -0
- package/templates/qoder/commands/wl-design-spec.md +154 -0
- package/templates/qoder/commands/wl-design.md +32 -0
- package/templates/qoder/commands/wl-init.md +24 -3
- package/templates/qoder/commands/wl-prd-full.md +226 -0
- package/templates/qoder/commands/wl-prd-quick.md +134 -0
- package/templates/qoder/commands/wl-prd-review.md +104 -0
- package/templates/qoder/commands/wl-prd.md +17 -288
- package/templates/qoder/commands/wl-search.md +66 -30
- package/templates/qoder/commands/wl-task.md +290 -59
- package/templates/qoder/commands/wl-test.md +92 -24
- package/templates/qoder/config.yaml +59 -15
- package/templates/qoder/hooks/inject-workflow-state.py +35 -9
- package/templates/qoder/hooks/session-start.py +144 -62
- package/templates/qoder/rules/wl-pipeline.md +216 -105
- package/templates/qoder/scripts/__pycache__/search_index.cpython-39.pyc +0 -0
- package/templates/qoder/scripts/archive_prd.py +377 -0
- package/templates/qoder/scripts/autotest.py +1715 -0
- package/templates/qoder/scripts/autotest_batch.py +224 -0
- package/templates/qoder/scripts/autotest_run.py +297 -0
- package/templates/qoder/scripts/benchmark.py +210 -209
- package/templates/qoder/scripts/build_style_index.py +444 -4
- package/templates/qoder/scripts/check_carriers.py +238 -0
- package/templates/qoder/scripts/check_mcp.py +298 -0
- package/templates/qoder/scripts/check_qoderwork_consistency.py +166 -0
- package/templates/qoder/scripts/common/developer.py +26 -19
- package/templates/qoder/scripts/common/events.py +46 -0
- package/templates/qoder/scripts/common/extract.py +419 -0
- package/templates/qoder/scripts/common/graph_traverse.py +533 -0
- package/templates/qoder/scripts/common/identity.py +6 -1
- package/templates/qoder/scripts/common/paths.py +89 -0
- package/templates/qoder/scripts/common/pip_install.py +144 -0
- package/templates/qoder/scripts/common/platform_guard.py +61 -0
- package/templates/qoder/scripts/common/search_engine.py +205 -205
- package/templates/qoder/scripts/common/terms.py +57 -0
- package/templates/qoder/scripts/common/ts_extract.py +536 -0
- package/templates/qoder/scripts/context_pack.py +73 -13
- package/templates/qoder/scripts/enrich_prompt.py +226 -0
- package/templates/qoder/scripts/eval_prd.py +318 -225
- package/templates/qoder/scripts/export.py +487 -487
- package/templates/qoder/scripts/extract_api_params.py +246 -0
- package/templates/qoder/scripts/extract_routes.py +54 -0
- package/templates/qoder/scripts/extract_routes_tree.py +78 -0
- package/templates/qoder/scripts/fill_prototype.py +707 -0
- package/templates/qoder/scripts/gen_design_doc.py +394 -0
- package/templates/qoder/scripts/git_sync.py +27 -15
- package/templates/qoder/scripts/init_doctor.py +306 -41
- package/templates/qoder/scripts/install_qoderwork.py +366 -9
- package/templates/qoder/scripts/kg.py +708 -0
- package/templates/qoder/scripts/kg_auto_login.py +196 -0
- package/templates/qoder/scripts/kg_build.py +612 -0
- package/templates/qoder/scripts/kg_build_db.py +327 -0
- package/templates/qoder/scripts/kg_duckdb.py +549 -0
- package/templates/qoder/scripts/kg_incremental.py +393 -0
- package/templates/qoder/scripts/kg_link_db.py +224 -0
- package/templates/qoder/scripts/kg_mcp_server.py +801 -0
- package/templates/qoder/scripts/kg_semantic.py +150 -0
- package/templates/qoder/scripts/kg_test_runner.py +241 -0
- package/templates/qoder/scripts/lanhu_stdio_wrapper.py +119 -0
- package/templates/qoder/scripts/learn.py +118 -39
- package/templates/qoder/scripts/learn_aggregate.py +201 -0
- package/templates/qoder/scripts/mcp_launcher.py +359 -0
- package/templates/qoder/scripts/mysql_mcp_server.py +396 -0
- package/templates/qoder/scripts/repo_root.py +106 -0
- package/templates/qoder/scripts/role.py +12 -0
- package/templates/qoder/scripts/run_weekly_update.bat +5 -0
- package/templates/qoder/scripts/run_weekly_update.sh +5 -0
- package/templates/qoder/scripts/search_index.py +307 -60
- package/templates/qoder/scripts/secure-ls.js +5640 -0
- package/templates/qoder/scripts/setup.py +706 -453
- package/templates/qoder/scripts/setup_lanhu.py +963 -0
- package/templates/qoder/scripts/status.py +250 -11
- package/templates/qoder/scripts/sync_carriers.py +259 -0
- package/templates/qoder/scripts/syncgate.py +5 -4
- package/templates/qoder/scripts/task.py +75 -0
- package/templates/qoder/scripts/team_sync.py +60 -4
- package/templates/qoder/scripts/workspace_init.py +1 -1
- package/templates/qoder/skills/design-import/SKILL.md +226 -0
- package/templates/qoder/skills/design-import/figma-workflow.md +81 -0
- package/templates/qoder/skills/design-review/SKILL.md +82 -25
- package/templates/qoder/skills/prd-generator/SKILL.md +185 -58
- package/templates/qoder/skills/prd-review/SKILL.md +18 -1
- package/templates/qoder/skills/prompt-enrich/SKILL.md +90 -0
- package/templates/qoder/skills/prototype-generator/SKILL.md +256 -141
- package/templates/qoder/skills/prototype-generator/SKILL.md.zcode-79180-2af4721f-f9a6-412c-88db-c0af680d211b.tmp +0 -0
- package/templates/qoder/skills/spec-coder/SKILL.md +18 -1
- package/templates/qoder/skills/spec-generator/SKILL.md +18 -1
- package/templates/qoder/skills/test-generator/SKILL.md +15 -2
- package/templates/qoder/skills/wl-code/SKILL.md +55 -36
- package/templates/qoder/skills/wl-commit/SKILL.md +89 -76
- package/templates/qoder/skills/wl-design/SKILL.md +55 -0
- package/templates/qoder/skills/wl-init/SKILL.md +76 -67
- package/templates/qoder/skills/wl-insight/SKILL.md +201 -81
- package/templates/qoder/skills/wl-prd-full/SKILL.md +69 -0
- package/templates/qoder/skills/wl-prd-quick/SKILL.md +49 -0
- package/templates/qoder/skills/wl-prd-review/SKILL.md +34 -0
- package/templates/qoder/skills/wl-report/SKILL.md +131 -107
- package/templates/qoder/skills/wl-search/SKILL.md +141 -75
- package/templates/qoder/skills/wl-spec/SKILL.md +49 -39
- package/templates/qoder/skills/wl-status/SKILL.md +83 -61
- package/templates/qoder/skills/wl-task/SKILL.md +132 -58
- package/templates/qoder/skills/wl-test/SKILL.md +406 -40
- package/templates/qoder/templates/prd-full-template.md +2 -0
- package/templates/qoder/templates/prd-quick-template.md +1 -0
- package/templates/qoder/templates/prototype-app.html +13 -8
- package/templates/qoder/templates/prototype-web.html +376 -93
- package/templates/root/AGENTS.md +89 -34
- package/templates/root/requirements.txt +21 -0
- package/templates/root//344/275/277/347/224/250/350/257/264/346/230/216.md +259 -259
- package/templates/root//346/226/260/346/211/213/346/214/207/345/215/227.md +186 -186
- package/templates/qoder/agents/prd-planning.md +0 -56
- package/templates/qoder/agents/prd-research.md +0 -33
- package/templates/qoder/commands/wl-insight.md +0 -51
- package/templates/qoder/skills/wl-prd/SKILL.md +0 -89
|
@@ -1,453 +1,706 @@
|
|
|
1
|
-
#!/usr/bin/env python3
|
|
2
|
-
# -*- coding: utf-8 -*-
|
|
3
|
-
"""
|
|
4
|
-
setup.py - 一键就绪 (小而美)
|
|
5
|
-
|
|
6
|
-
跑这一条命令, 把整套工作流配好可用:
|
|
7
|
-
python .qoder/scripts/setup.py
|
|
8
|
-
python .qoder/scripts/setup.py 小王 # 指定名字
|
|
9
|
-
python .qoder/scripts/setup.py 小王 dev # 指定名字+角色
|
|
10
|
-
python .qoder/scripts/setup.py --skip-cron # 跳过 cron 询问
|
|
11
|
-
|
|
12
|
-
自动完成:
|
|
13
|
-
1. 环境自检 (Python / git)
|
|
14
|
-
2. 身份探测 (从 git config user.name, 或交互输入)
|
|
15
|
-
3. 调 init_doctor --fix (写 .developer / git 对齐 / 拉源码 / 建索引 / QoderWork 检查)
|
|
16
|
-
4. config.yaml 引导 (源码仓库未配时提示)
|
|
17
|
-
5. cron 一键注册 (询问式 opt-in)
|
|
18
|
-
6. QoderWork 技能安装 (检测到桌面端时询问)
|
|
19
|
-
7. 打印就绪报告
|
|
20
|
-
|
|
21
|
-
幂等: 可重复跑, 不会重复安装。
|
|
22
|
-
"""
|
|
23
|
-
|
|
24
|
-
import argparse
|
|
25
|
-
import os
|
|
26
|
-
import subprocess
|
|
27
|
-
import sys
|
|
28
|
-
from pathlib import Path
|
|
29
|
-
|
|
30
|
-
try:
|
|
31
|
-
sys.stdout.reconfigure(encoding='utf-8', errors='replace')
|
|
32
|
-
sys.stderr.reconfigure(encoding='utf-8', errors='replace')
|
|
33
|
-
except (AttributeError, TypeError, OSError):
|
|
34
|
-
pass
|
|
35
|
-
|
|
36
|
-
THIS_DIR = Path(__file__).resolve().parent
|
|
37
|
-
BASE = THIS_DIR.parent.parent # repo root
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
# ============================================================
|
|
41
|
-
# 工具函数
|
|
42
|
-
# ============================================================
|
|
43
|
-
|
|
44
|
-
def ok(msg):
|
|
45
|
-
print(f' [\u2713] {msg}') # ✓
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
def warn(msg):
|
|
49
|
-
print(f' [!] {msg}')
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
def fail(msg):
|
|
53
|
-
print(f' [\u2717] {msg}', file=sys.stderr)
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
def ask(prompt, default='y'):
|
|
57
|
-
"""交互式 yes/no, 默认 default。非交互环境 (无 stdin) 返回 default。"""
|
|
58
|
-
if not sys.stdin.isatty():
|
|
59
|
-
return default.lower() in ('y', 'yes')
|
|
60
|
-
try:
|
|
61
|
-
ans = input(f'{prompt} [{default}] ').strip().lower() or default
|
|
62
|
-
return ans in ('y', 'yes')
|
|
63
|
-
except (EOFError, KeyboardInterrupt):
|
|
64
|
-
return default.lower() in ('y', 'yes')
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
def run(cmd, **kwargs):
|
|
68
|
-
"""跑子进程, 返回 CompletedProcess。encoding 容错。"""
|
|
69
|
-
kwargs.setdefault('capture_output', True)
|
|
70
|
-
kwargs.setdefault('text', True)
|
|
71
|
-
kwargs.setdefault('encoding', 'utf-8')
|
|
72
|
-
kwargs.setdefault('errors', 'replace')
|
|
73
|
-
return subprocess.run(cmd, **kwargs)
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
# ============================================================
|
|
77
|
-
# Step 0: 环境自检
|
|
78
|
-
# ============================================================
|
|
79
|
-
|
|
80
|
-
def check_env():
|
|
81
|
-
print('\n--- 环境自检 ---')
|
|
82
|
-
# Python 版本
|
|
83
|
-
if sys.version_info < (3, 9):
|
|
84
|
-
fail(f'Python {sys.version.split()[0]} 版本过低, 需要 3.9+')
|
|
85
|
-
return False
|
|
86
|
-
ok(f'Python {sys.version.split()[0]}')
|
|
87
|
-
# git (可选: 无 git 时尝试自动装, 装不上就降级)
|
|
88
|
-
r = None
|
|
89
|
-
try:
|
|
90
|
-
r = run(['git', '--version'])
|
|
91
|
-
except FileNotFoundError:
|
|
92
|
-
pass
|
|
93
|
-
if not r or r.returncode != 0:
|
|
94
|
-
# 尝试自动安装 git
|
|
95
|
-
if try_install_git():
|
|
96
|
-
ok('git 已自动安装')
|
|
97
|
-
else:
|
|
98
|
-
warn('git 未安装 — 团队同步/源码克隆将禁用, 本地 PRD/搜索/任务/报告仍可用')
|
|
99
|
-
print(' (想启用团队协作: 安装 git 后重跑
|
|
100
|
-
return True # 不阻塞, 继续
|
|
101
|
-
ok('git 可用')
|
|
102
|
-
return True
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
def try_install_git():
|
|
106
|
-
"""尝试自动安装 git (Windows: winget; macOS: brew; Linux: apt)。
|
|
107
|
-
成功返回 True, 失败/无法自动装返回 False。"""
|
|
108
|
-
print('\n 检测到 git 未安装, 尝试自动安装...')
|
|
109
|
-
if not sys.stdin.isatty():
|
|
110
|
-
# 非交互环境, 不自动装 (避免意外操作)
|
|
111
|
-
warn('非交互环境, 跳过自动安装')
|
|
112
|
-
return False
|
|
113
|
-
|
|
114
|
-
platform = sys.platform
|
|
115
|
-
if platform == 'win32':
|
|
116
|
-
# Windows: 优先 winget, 回退 choco
|
|
117
|
-
if ask('用 winget 安装 Git for Windows? (需要网络, 可能要管理员权限)', 'y'):
|
|
118
|
-
print(' 运行: winget install --id Git.Git -e --source winget')
|
|
119
|
-
r = run(['winget', 'install', '--id', 'Git.Git', '-e', '--source', 'winget',
|
|
120
|
-
'--accept-source-agreements', '--accept-package-agreements'])
|
|
121
|
-
if r.returncode == 0:
|
|
122
|
-
# 刷新 PATH (winget 装的 git 需要新终端才生效, 这里尝试常见路径)
|
|
123
|
-
_refresh_git_path()
|
|
124
|
-
return _git_now_available()
|
|
125
|
-
else:
|
|
126
|
-
warn('winget 安装失败: ' + (r.stderr or '').strip()[:150])
|
|
127
|
-
print(' 手动下载: https://git-scm.com/download/win')
|
|
128
|
-
return False
|
|
129
|
-
elif platform == 'darwin':
|
|
130
|
-
# macOS: brew
|
|
131
|
-
if ask('用 Homebrew 安装 git? (brew install git)', 'y'):
|
|
132
|
-
r = run(['brew', 'install', 'git'])
|
|
133
|
-
if r.returncode == 0:
|
|
134
|
-
return _git_now_available()
|
|
135
|
-
else:
|
|
136
|
-
warn('brew 安装失败: ' + (r.stderr or '').strip()[:150])
|
|
137
|
-
print(' 或手动: https://git-scm.com/download/mac')
|
|
138
|
-
return False
|
|
139
|
-
else:
|
|
140
|
-
# Linux: apt (Debian/Ubuntu) / yum (CentOS) / dnf
|
|
141
|
-
for mgr, cmd in [('apt', ['sudo', 'apt-get', 'install', '-y', 'git']),
|
|
142
|
-
('dnf', ['sudo', 'dnf', 'install', '-y', 'git']),
|
|
143
|
-
('yum', ['sudo', 'yum', 'install', '-y', 'git'])]:
|
|
144
|
-
# 检测包管理器是否存在
|
|
145
|
-
check = run(['which', mgr])
|
|
146
|
-
if check.returncode == 0:
|
|
147
|
-
if ask(f'用 {mgr} 安装 git?', 'y'):
|
|
148
|
-
r = run(cmd)
|
|
149
|
-
if r.returncode == 0:
|
|
150
|
-
return _git_now_available()
|
|
151
|
-
else:
|
|
152
|
-
warn(f'{mgr} 安装失败: ' + (r.stderr or '').strip()[:150])
|
|
153
|
-
return False
|
|
154
|
-
warn('未检测到 apt/dnf/yum, 请手动安装 git')
|
|
155
|
-
return False
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
def _refresh_git_path():
|
|
159
|
-
"""winget 装完 git 后, 尝试把常见路径加到 PATH (当前进程)。"""
|
|
160
|
-
candidates = [
|
|
161
|
-
r'C:\Program Files\Git\cmd',
|
|
162
|
-
r'C:\Program Files\Git\bin',
|
|
163
|
-
r'C:\Program Files (x86)\Git\cmd',
|
|
164
|
-
]
|
|
165
|
-
for c in candidates:
|
|
166
|
-
if os.path.isdir(c):
|
|
167
|
-
os.environ['PATH'] = c + os.pathsep + os.environ.get('PATH', '')
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
def _git_now_available():
|
|
171
|
-
"""检查 git 现在是否可用 (装完后立即验证)。"""
|
|
172
|
-
try:
|
|
173
|
-
r = run(['git', '--version'])
|
|
174
|
-
return r.returncode == 0
|
|
175
|
-
except FileNotFoundError:
|
|
176
|
-
return False
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
# ============================================================
|
|
180
|
-
# Step 1: 身份探测
|
|
181
|
-
# ============================================================
|
|
182
|
-
|
|
183
|
-
def detect_name(explicit=None):
|
|
184
|
-
"""身份探测: 显式参数 > git config user.name > 交互输入。"""
|
|
185
|
-
print('\n--- 身份探测 ---')
|
|
186
|
-
if explicit:
|
|
187
|
-
ok(f'使用指定名字: {explicit}')
|
|
188
|
-
return explicit
|
|
189
|
-
# 从 git config 探测 (git 可能未安装)
|
|
190
|
-
git_name = ''
|
|
191
|
-
try:
|
|
192
|
-
r = run(['git', 'config', 'user.name'])
|
|
193
|
-
if r.returncode == 0:
|
|
194
|
-
git_name = r.stdout.strip()
|
|
195
|
-
except FileNotFoundError:
|
|
196
|
-
pass # git 未装, 跳过
|
|
197
|
-
if git_name:
|
|
198
|
-
if ask(f'检测到 git user.name = "{git_name}", 用这个名字?', 'y'):
|
|
199
|
-
ok(f'使用 git 名字: {git_name}')
|
|
200
|
-
return git_name
|
|
201
|
-
# 交互输入
|
|
202
|
-
if not sys.stdin.isatty():
|
|
203
|
-
# 非交互环境 (如 AI 调用), 默认用 git 名字或 'me'
|
|
204
|
-
name = git_name or 'me'
|
|
205
|
-
warn(f'非交互环境, 默认用: {name}')
|
|
206
|
-
return name
|
|
207
|
-
while True:
|
|
208
|
-
try:
|
|
209
|
-
name = input('你叫什么名字? ').strip()
|
|
210
|
-
if name and len(name) <= 32:
|
|
211
|
-
ok(f'名字: {name}')
|
|
212
|
-
return name
|
|
213
|
-
print(' 名字需 1-32 字符, 重试。')
|
|
214
|
-
except (EOFError, KeyboardInterrupt):
|
|
215
|
-
warn('未输入, 用 git 名字或 "me"')
|
|
216
|
-
return git_name or 'me'
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
# ============================================================
|
|
220
|
-
# Step 2: 调 init_doctor --fix (主体)
|
|
221
|
-
# ============================================================
|
|
222
|
-
|
|
223
|
-
def run_doctor(name, role):
|
|
224
|
-
print('\n--- 初始化 (init_doctor) ---')
|
|
225
|
-
cmd = [sys.executable, str(THIS_DIR / 'init_doctor.py'), '--fix', name, role]
|
|
226
|
-
r = subprocess.run(cmd, cwd=str(BASE))
|
|
227
|
-
# init_doctor 返回 0=健康, 1=有未解决问题
|
|
228
|
-
if r.returncode == 0:
|
|
229
|
-
ok('init_doctor 完成, 环境健康')
|
|
230
|
-
return True
|
|
231
|
-
elif r.returncode == 1:
|
|
232
|
-
warn('init_doctor 完成, 但有未解决问题 (见上方 [WARN])')
|
|
233
|
-
return True # 不阻塞, 用户可继续
|
|
234
|
-
else:
|
|
235
|
-
fail(f'init_doctor 失败 (exit {r.returncode})')
|
|
236
|
-
return False
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
# ============================================================
|
|
240
|
-
# Step 3: config.yaml 引导
|
|
241
|
-
# ============================================================
|
|
242
|
-
|
|
243
|
-
def guide_config():
|
|
244
|
-
"""检查 config.yaml 的 git_sync.projects 是否有效配置。"""
|
|
245
|
-
print('\n--- 源码仓库配置 ---')
|
|
246
|
-
cfg = BASE / '.qoder' / 'config.yaml'
|
|
247
|
-
if not cfg.is_file():
|
|
248
|
-
warn('config.yaml 不存在 (init_doctor 会创建默认)')
|
|
249
|
-
return
|
|
250
|
-
|
|
251
|
-
try:
|
|
252
|
-
content = cfg.read_text(encoding='utf-8')
|
|
253
|
-
except Exception:
|
|
254
|
-
return
|
|
255
|
-
|
|
256
|
-
# 检测 git_sync.projects 是否有真实 URL (非占位/注释)
|
|
257
|
-
has_real_url = False
|
|
258
|
-
in_projects = False
|
|
259
|
-
for line in content.splitlines():
|
|
260
|
-
s = line.strip()
|
|
261
|
-
if s.startswith('projects:'):
|
|
262
|
-
in_projects = True
|
|
263
|
-
continue
|
|
264
|
-
if in_projects:
|
|
265
|
-
if s and not s.startswith('#') and not line[0].isspace():
|
|
266
|
-
in_projects = False # 离开 projects 段
|
|
267
|
-
continue
|
|
268
|
-
if 'url:' in s and 'your-' not in s and 'example.com' not in s and '占位' not in s and not s.startswith('#'):
|
|
269
|
-
has_real_url = True
|
|
270
|
-
break
|
|
271
|
-
|
|
272
|
-
if has_real_url:
|
|
273
|
-
ok('源码仓库已配置')
|
|
274
|
-
else:
|
|
275
|
-
warn('config.yaml 的 git_sync.projects 未填真实 URL')
|
|
276
|
-
print(' 打开 .qoder/config.yaml, 找到 git_sync.projects 段,')
|
|
277
|
-
print(' 把 url 改成你团队的 git 地址。或手动把代码放到 data/code/。')
|
|
278
|
-
print(' 填完后重跑
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
# ============================================================
|
|
282
|
-
# Step
|
|
283
|
-
# ============================================================
|
|
284
|
-
|
|
285
|
-
def
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
#
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
print('
|
|
326
|
-
return
|
|
327
|
-
|
|
328
|
-
#
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
#
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
if
|
|
453
|
-
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
# -*- coding: utf-8 -*-
|
|
3
|
+
"""
|
|
4
|
+
setup.py - 一键就绪 (小而美)
|
|
5
|
+
|
|
6
|
+
跑这一条命令, 把整套工作流配好可用:
|
|
7
|
+
python .qoder/scripts/setup.py
|
|
8
|
+
python .qoder/scripts/setup.py 小王 # 指定名字
|
|
9
|
+
python .qoder/scripts/setup.py 小王 dev # 指定名字+角色
|
|
10
|
+
python .qoder/scripts/setup.py --skip-cron # 跳过 cron 询问
|
|
11
|
+
|
|
12
|
+
自动完成:
|
|
13
|
+
1. 环境自检 (Python / git)
|
|
14
|
+
2. 身份探测 (从 git config user.name, 或交互输入)
|
|
15
|
+
3. 调 init_doctor --fix (写 .developer / git 对齐 / 拉源码 / 建索引 / QoderWork 检查)
|
|
16
|
+
4. config.yaml 引导 (源码仓库未配时提示)
|
|
17
|
+
5. cron 一键注册 (询问式 opt-in)
|
|
18
|
+
6. QoderWork 技能安装 (检测到桌面端时询问)
|
|
19
|
+
7. 打印就绪报告
|
|
20
|
+
|
|
21
|
+
幂等: 可重复跑, 不会重复安装。
|
|
22
|
+
"""
|
|
23
|
+
|
|
24
|
+
import argparse
|
|
25
|
+
import os
|
|
26
|
+
import subprocess
|
|
27
|
+
import sys
|
|
28
|
+
from pathlib import Path
|
|
29
|
+
|
|
30
|
+
try:
|
|
31
|
+
sys.stdout.reconfigure(encoding='utf-8', errors='replace')
|
|
32
|
+
sys.stderr.reconfigure(encoding='utf-8', errors='replace')
|
|
33
|
+
except (AttributeError, TypeError, OSError):
|
|
34
|
+
pass
|
|
35
|
+
|
|
36
|
+
THIS_DIR = Path(__file__).resolve().parent
|
|
37
|
+
BASE = THIS_DIR.parent.parent # repo root
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
# ============================================================
|
|
41
|
+
# 工具函数
|
|
42
|
+
# ============================================================
|
|
43
|
+
|
|
44
|
+
def ok(msg):
|
|
45
|
+
print(f' [\u2713] {msg}') # ✓
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def warn(msg):
|
|
49
|
+
print(f' [!] {msg}')
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def fail(msg):
|
|
53
|
+
print(f' [\u2717] {msg}', file=sys.stderr)
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def ask(prompt, default='y'):
|
|
57
|
+
"""交互式 yes/no, 默认 default。非交互环境 (无 stdin) 返回 default。"""
|
|
58
|
+
if not sys.stdin.isatty():
|
|
59
|
+
return default.lower() in ('y', 'yes')
|
|
60
|
+
try:
|
|
61
|
+
ans = input(f'{prompt} [{default}] ').strip().lower() or default
|
|
62
|
+
return ans in ('y', 'yes')
|
|
63
|
+
except (EOFError, KeyboardInterrupt):
|
|
64
|
+
return default.lower() in ('y', 'yes')
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def run(cmd, **kwargs):
|
|
68
|
+
"""跑子进程, 返回 CompletedProcess。encoding 容错。"""
|
|
69
|
+
kwargs.setdefault('capture_output', True)
|
|
70
|
+
kwargs.setdefault('text', True)
|
|
71
|
+
kwargs.setdefault('encoding', 'utf-8')
|
|
72
|
+
kwargs.setdefault('errors', 'replace')
|
|
73
|
+
return subprocess.run(cmd, **kwargs)
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
# ============================================================
|
|
77
|
+
# Step 0: 环境自检
|
|
78
|
+
# ============================================================
|
|
79
|
+
|
|
80
|
+
def check_env():
|
|
81
|
+
print('\n--- 环境自检 ---')
|
|
82
|
+
# Python 版本
|
|
83
|
+
if sys.version_info < (3, 9):
|
|
84
|
+
fail(f'Python {sys.version.split()[0]} 版本过低, 需要 3.9+')
|
|
85
|
+
return False
|
|
86
|
+
ok(f'Python {sys.version.split()[0]}')
|
|
87
|
+
# git (可选: 无 git 时尝试自动装, 装不上就降级)
|
|
88
|
+
r = None
|
|
89
|
+
try:
|
|
90
|
+
r = run(['git', '--version'])
|
|
91
|
+
except FileNotFoundError:
|
|
92
|
+
pass
|
|
93
|
+
if not r or r.returncode != 0:
|
|
94
|
+
# 尝试自动安装 git
|
|
95
|
+
if try_install_git():
|
|
96
|
+
ok('git 已自动安装')
|
|
97
|
+
else:
|
|
98
|
+
warn('git 未安装 — 团队同步/源码克隆将禁用, 本地 PRD/搜索/任务/报告仍可用')
|
|
99
|
+
print(' (想启用团队协作: 安装 git 后重跑 \'wlkj init\')')
|
|
100
|
+
return True # 不阻塞, 继续
|
|
101
|
+
ok('git 可用')
|
|
102
|
+
return True
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
def try_install_git():
|
|
106
|
+
"""尝试自动安装 git (Windows: winget; macOS: brew; Linux: apt)。
|
|
107
|
+
成功返回 True, 失败/无法自动装返回 False。"""
|
|
108
|
+
print('\n 检测到 git 未安装, 尝试自动安装...')
|
|
109
|
+
if not sys.stdin.isatty():
|
|
110
|
+
# 非交互环境, 不自动装 (避免意外操作)
|
|
111
|
+
warn('非交互环境, 跳过自动安装')
|
|
112
|
+
return False
|
|
113
|
+
|
|
114
|
+
platform = sys.platform
|
|
115
|
+
if platform == 'win32':
|
|
116
|
+
# Windows: 优先 winget, 回退 choco
|
|
117
|
+
if ask('用 winget 安装 Git for Windows? (需要网络, 可能要管理员权限)', 'y'):
|
|
118
|
+
print(' 运行: winget install --id Git.Git -e --source winget')
|
|
119
|
+
r = run(['winget', 'install', '--id', 'Git.Git', '-e', '--source', 'winget',
|
|
120
|
+
'--accept-source-agreements', '--accept-package-agreements'])
|
|
121
|
+
if r.returncode == 0:
|
|
122
|
+
# 刷新 PATH (winget 装的 git 需要新终端才生效, 这里尝试常见路径)
|
|
123
|
+
_refresh_git_path()
|
|
124
|
+
return _git_now_available()
|
|
125
|
+
else:
|
|
126
|
+
warn('winget 安装失败: ' + (r.stderr or '').strip()[:150])
|
|
127
|
+
print(' 手动下载: https://git-scm.com/download/win')
|
|
128
|
+
return False
|
|
129
|
+
elif platform == 'darwin':
|
|
130
|
+
# macOS: brew
|
|
131
|
+
if ask('用 Homebrew 安装 git? (brew install git)', 'y'):
|
|
132
|
+
r = run(['brew', 'install', 'git'])
|
|
133
|
+
if r.returncode == 0:
|
|
134
|
+
return _git_now_available()
|
|
135
|
+
else:
|
|
136
|
+
warn('brew 安装失败: ' + (r.stderr or '').strip()[:150])
|
|
137
|
+
print(' 或手动: https://git-scm.com/download/mac')
|
|
138
|
+
return False
|
|
139
|
+
else:
|
|
140
|
+
# Linux: apt (Debian/Ubuntu) / yum (CentOS) / dnf
|
|
141
|
+
for mgr, cmd in [('apt', ['sudo', 'apt-get', 'install', '-y', 'git']),
|
|
142
|
+
('dnf', ['sudo', 'dnf', 'install', '-y', 'git']),
|
|
143
|
+
('yum', ['sudo', 'yum', 'install', '-y', 'git'])]:
|
|
144
|
+
# 检测包管理器是否存在
|
|
145
|
+
check = run(['which', mgr])
|
|
146
|
+
if check.returncode == 0:
|
|
147
|
+
if ask(f'用 {mgr} 安装 git?', 'y'):
|
|
148
|
+
r = run(cmd)
|
|
149
|
+
if r.returncode == 0:
|
|
150
|
+
return _git_now_available()
|
|
151
|
+
else:
|
|
152
|
+
warn(f'{mgr} 安装失败: ' + (r.stderr or '').strip()[:150])
|
|
153
|
+
return False
|
|
154
|
+
warn('未检测到 apt/dnf/yum, 请手动安装 git')
|
|
155
|
+
return False
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
def _refresh_git_path():
|
|
159
|
+
"""winget 装完 git 后, 尝试把常见路径加到 PATH (当前进程)。"""
|
|
160
|
+
candidates = [
|
|
161
|
+
r'C:\Program Files\Git\cmd',
|
|
162
|
+
r'C:\Program Files\Git\bin',
|
|
163
|
+
r'C:\Program Files (x86)\Git\cmd',
|
|
164
|
+
]
|
|
165
|
+
for c in candidates:
|
|
166
|
+
if os.path.isdir(c):
|
|
167
|
+
os.environ['PATH'] = c + os.pathsep + os.environ.get('PATH', '')
|
|
168
|
+
|
|
169
|
+
|
|
170
|
+
def _git_now_available():
|
|
171
|
+
"""检查 git 现在是否可用 (装完后立即验证)。"""
|
|
172
|
+
try:
|
|
173
|
+
r = run(['git', '--version'])
|
|
174
|
+
return r.returncode == 0
|
|
175
|
+
except FileNotFoundError:
|
|
176
|
+
return False
|
|
177
|
+
|
|
178
|
+
|
|
179
|
+
# ============================================================
|
|
180
|
+
# Step 1: 身份探测
|
|
181
|
+
# ============================================================
|
|
182
|
+
|
|
183
|
+
def detect_name(explicit=None):
|
|
184
|
+
"""身份探测: 显式参数 > git config user.name > 交互输入。"""
|
|
185
|
+
print('\n--- 身份探测 ---')
|
|
186
|
+
if explicit:
|
|
187
|
+
ok(f'使用指定名字: {explicit}')
|
|
188
|
+
return explicit
|
|
189
|
+
# 从 git config 探测 (git 可能未安装)
|
|
190
|
+
git_name = ''
|
|
191
|
+
try:
|
|
192
|
+
r = run(['git', 'config', 'user.name'])
|
|
193
|
+
if r.returncode == 0:
|
|
194
|
+
git_name = r.stdout.strip()
|
|
195
|
+
except FileNotFoundError:
|
|
196
|
+
pass # git 未装, 跳过
|
|
197
|
+
if git_name:
|
|
198
|
+
if ask(f'检测到 git user.name = "{git_name}", 用这个名字?', 'y'):
|
|
199
|
+
ok(f'使用 git 名字: {git_name}')
|
|
200
|
+
return git_name
|
|
201
|
+
# 交互输入
|
|
202
|
+
if not sys.stdin.isatty():
|
|
203
|
+
# 非交互环境 (如 AI 调用), 默认用 git 名字或 'me'
|
|
204
|
+
name = git_name or 'me'
|
|
205
|
+
warn(f'非交互环境, 默认用: {name}')
|
|
206
|
+
return name
|
|
207
|
+
while True:
|
|
208
|
+
try:
|
|
209
|
+
name = input('你叫什么名字? ').strip()
|
|
210
|
+
if name and len(name) <= 32:
|
|
211
|
+
ok(f'名字: {name}')
|
|
212
|
+
return name
|
|
213
|
+
print(' 名字需 1-32 字符, 重试。')
|
|
214
|
+
except (EOFError, KeyboardInterrupt):
|
|
215
|
+
warn('未输入, 用 git 名字或 "me"')
|
|
216
|
+
return git_name or 'me'
|
|
217
|
+
|
|
218
|
+
|
|
219
|
+
# ============================================================
|
|
220
|
+
# Step 2: 调 init_doctor --fix (主体)
|
|
221
|
+
# ============================================================
|
|
222
|
+
|
|
223
|
+
def run_doctor(name, role):
|
|
224
|
+
print('\n--- 初始化 (init_doctor) ---')
|
|
225
|
+
cmd = [sys.executable, str(THIS_DIR / 'init_doctor.py'), '--fix', name, role]
|
|
226
|
+
r = subprocess.run(cmd, cwd=str(BASE))
|
|
227
|
+
# init_doctor 返回 0=健康, 1=有未解决问题
|
|
228
|
+
if r.returncode == 0:
|
|
229
|
+
ok('init_doctor 完成, 环境健康')
|
|
230
|
+
return True
|
|
231
|
+
elif r.returncode == 1:
|
|
232
|
+
warn('init_doctor 完成, 但有未解决问题 (见上方 [WARN])')
|
|
233
|
+
return True # 不阻塞, 用户可继续
|
|
234
|
+
else:
|
|
235
|
+
fail(f'init_doctor 失败 (exit {r.returncode})')
|
|
236
|
+
return False
|
|
237
|
+
|
|
238
|
+
|
|
239
|
+
# ============================================================
|
|
240
|
+
# Step 3: config.yaml 引导
|
|
241
|
+
# ============================================================
|
|
242
|
+
|
|
243
|
+
def guide_config():
|
|
244
|
+
"""检查 config.yaml 的 git_sync.projects 是否有效配置。"""
|
|
245
|
+
print('\n--- 源码仓库配置 ---')
|
|
246
|
+
cfg = BASE / '.qoder' / 'config.yaml'
|
|
247
|
+
if not cfg.is_file():
|
|
248
|
+
warn('config.yaml 不存在 (init_doctor 会创建默认)')
|
|
249
|
+
return
|
|
250
|
+
|
|
251
|
+
try:
|
|
252
|
+
content = cfg.read_text(encoding='utf-8')
|
|
253
|
+
except Exception:
|
|
254
|
+
return
|
|
255
|
+
|
|
256
|
+
# 检测 git_sync.projects 是否有真实 URL (非占位/注释)
|
|
257
|
+
has_real_url = False
|
|
258
|
+
in_projects = False
|
|
259
|
+
for line in content.splitlines():
|
|
260
|
+
s = line.strip()
|
|
261
|
+
if s.startswith('projects:'):
|
|
262
|
+
in_projects = True
|
|
263
|
+
continue
|
|
264
|
+
if in_projects:
|
|
265
|
+
if s and not s.startswith('#') and not line[0].isspace():
|
|
266
|
+
in_projects = False # 离开 projects 段
|
|
267
|
+
continue
|
|
268
|
+
if 'url:' in s and 'your-' not in s and 'example.com' not in s and '占位' not in s and not s.startswith('#'):
|
|
269
|
+
has_real_url = True
|
|
270
|
+
break
|
|
271
|
+
|
|
272
|
+
if has_real_url:
|
|
273
|
+
ok('源码仓库已配置')
|
|
274
|
+
else:
|
|
275
|
+
warn('config.yaml 的 git_sync.projects 未填真实 URL')
|
|
276
|
+
print(' 打开 .qoder/config.yaml, 找到 git_sync.projects 段,')
|
|
277
|
+
print(' 把 url 改成你团队的 git 地址。或手动把代码放到 data/code/。')
|
|
278
|
+
print(' 填完后重跑 \'wlkj init\' 即可自动克隆+建索引。')
|
|
279
|
+
|
|
280
|
+
|
|
281
|
+
# ============================================================
|
|
282
|
+
# Step 2.5: Git 身份配置 (在 init_doctor 之前, 确保下游探测到真账号)
|
|
283
|
+
# ============================================================
|
|
284
|
+
|
|
285
|
+
def configure_git_identity(name):
|
|
286
|
+
"""配置 git user.name/email。未配或疑似假账号时交互式问用户。
|
|
287
|
+
|
|
288
|
+
设计: 绝不自动造假账号。git 没配 → 问用户要真账号; 配了但像假的(@local) → 确认。
|
|
289
|
+
这样下游 init_developer 探测到的就是真账号, member.json 不会写假 git_author。
|
|
290
|
+
"""
|
|
291
|
+
print('\n--- Git 身份配置 ---')
|
|
292
|
+
|
|
293
|
+
# 1. git 是否可用
|
|
294
|
+
try:
|
|
295
|
+
subprocess.run(['git', '--version'], capture_output=True, timeout=5)
|
|
296
|
+
except (FileNotFoundError, OSError):
|
|
297
|
+
warn('git 未安装, 跳过 (push/commit 不可用, 但能本地工作)')
|
|
298
|
+
return False
|
|
299
|
+
|
|
300
|
+
# 2. 探测现有 git config (全局 + local 合并视图)
|
|
301
|
+
existing_name = run(['git', 'config', 'user.name']).stdout.strip()
|
|
302
|
+
existing_email = run(['git', 'config', 'user.email']).stdout.strip()
|
|
303
|
+
|
|
304
|
+
# 3. 判断是否需要配置
|
|
305
|
+
needs_config = False
|
|
306
|
+
if not existing_name or not existing_email:
|
|
307
|
+
needs_config = True
|
|
308
|
+
warn('git 未配置 user.name/user.email (团队协作必须)')
|
|
309
|
+
elif '@local' in existing_email:
|
|
310
|
+
needs_config = True
|
|
311
|
+
warn(f'检测到疑似假账号: {existing_name} <{existing_email}>')
|
|
312
|
+
print(' (可能是旧版 init 自动填的, 需要换成你的团队 git 账号)')
|
|
313
|
+
|
|
314
|
+
if not needs_config:
|
|
315
|
+
# 已有真配置, 确认是否用这个
|
|
316
|
+
if ask(f'用这个 git 账号? {existing_name} <{existing_email}>', 'y'):
|
|
317
|
+
ok(f'git 账号: {existing_name} <{existing_email}>')
|
|
318
|
+
return True
|
|
319
|
+
needs_config = True # 用户不想用, 重新配
|
|
320
|
+
|
|
321
|
+
# 4. 非交互环境: 不能问, 给明确指引后返回 (绝不造假)
|
|
322
|
+
if not sys.stdin.isatty():
|
|
323
|
+
warn('非交互环境, 无法询问 git 账号。请手动配置后重跑 \'wlkj init\':')
|
|
324
|
+
print(' git config --global user.name "你的名字"')
|
|
325
|
+
print(' git config --global user.email "你的团队邮箱"')
|
|
326
|
+
return False
|
|
327
|
+
|
|
328
|
+
# 5. 交互式收集 git 账号
|
|
329
|
+
print('\n 配置你的团队 git 账号 (用于提交代码和团队协作):')
|
|
330
|
+
while True:
|
|
331
|
+
try:
|
|
332
|
+
git_name = input(f' git 用户名 [{existing_name or name}]: ').strip() or existing_name or name
|
|
333
|
+
git_email = input(' git 邮箱 (团队邮箱): ').strip()
|
|
334
|
+
if not git_email or '@' not in git_email:
|
|
335
|
+
print(' 邮箱格式不对 (需含 @), 重试。')
|
|
336
|
+
continue
|
|
337
|
+
if '@local' in git_email:
|
|
338
|
+
if not ask('邮箱含 @local, 这通常不是团队邮箱, 确定用这个?', 'n'):
|
|
339
|
+
continue
|
|
340
|
+
break
|
|
341
|
+
except (EOFError, KeyboardInterrupt):
|
|
342
|
+
warn('未输入, 跳过 git 配置 (push 会失败, 手动配: git config user.name/user.email)')
|
|
343
|
+
return False
|
|
344
|
+
|
|
345
|
+
# 6. 写入 git config (默认 --global, 用户通常只有一个 git 身份)
|
|
346
|
+
use_global = ask('设为全局配置 (所有项目通用)? [Y] 还是只本仓库? [n]', 'y')
|
|
347
|
+
scope = '--global' if use_global else '--local'
|
|
348
|
+
|
|
349
|
+
r1 = run(['git', 'config', scope, 'user.name', git_name], cwd=str(BASE))
|
|
350
|
+
r2 = run(['git', 'config', scope, 'user.email', git_email], cwd=str(BASE))
|
|
351
|
+
if r1.returncode == 0 and r2.returncode == 0:
|
|
352
|
+
ok(f'git {scope.replace("--", "")} 配置: {git_name} <{git_email}>')
|
|
353
|
+
return True
|
|
354
|
+
warn('git config 设置失败: ' + (r1.stderr or r2.stderr or '').strip()[:100])
|
|
355
|
+
return False
|
|
356
|
+
|
|
357
|
+
|
|
358
|
+
def _read_team_remote_from_config():
|
|
359
|
+
"""从 .qoder/config.yaml 读 team_remote.url (没有返回 None)。"""
|
|
360
|
+
cfg = BASE / '.qoder' / 'config.yaml'
|
|
361
|
+
if not cfg.is_file():
|
|
362
|
+
return None
|
|
363
|
+
try:
|
|
364
|
+
content = cfg.read_text(encoding='utf-8')
|
|
365
|
+
except Exception:
|
|
366
|
+
return None
|
|
367
|
+
in_team = False
|
|
368
|
+
for line in content.splitlines():
|
|
369
|
+
s = line.strip()
|
|
370
|
+
if s.startswith('team_remote:'):
|
|
371
|
+
in_team = True
|
|
372
|
+
continue
|
|
373
|
+
if in_team:
|
|
374
|
+
if s and not s.startswith('#') and not line[0].isspace():
|
|
375
|
+
break # 离开 team_remote 段
|
|
376
|
+
if s.startswith('url:'):
|
|
377
|
+
url = s.split(':', 1)[1].strip().strip('\'"')
|
|
378
|
+
if url:
|
|
379
|
+
return url
|
|
380
|
+
return None
|
|
381
|
+
|
|
382
|
+
|
|
383
|
+
def _write_team_remote_to_config(url):
|
|
384
|
+
"""把 team_remote.url 写进 config.yaml (best-effort, 不阻塞)。"""
|
|
385
|
+
cfg = BASE / '.qoder' / 'config.yaml'
|
|
386
|
+
if not cfg.is_file():
|
|
387
|
+
return
|
|
388
|
+
try:
|
|
389
|
+
content = cfg.read_text(encoding='utf-8')
|
|
390
|
+
if 'team_remote:' in content:
|
|
391
|
+
return # 已有段, 不重复写
|
|
392
|
+
# 在 git_sync 段之前插入
|
|
393
|
+
marker = '# ── Git Sync'
|
|
394
|
+
block = (
|
|
395
|
+
'# ── 团队协作仓库 (workspace/ 产出 push 到这里) ──\n'
|
|
396
|
+
'# 团队共享 PRD/任务/原型的仓库, 跟 data/code/ 源码仓库不同。\n'
|
|
397
|
+
'team_remote:\n'
|
|
398
|
+
' url: "%s" # 团队所有人 init 时自动用这个地址\n'
|
|
399
|
+
' branch: master\n\n' % url
|
|
400
|
+
)
|
|
401
|
+
if marker in content:
|
|
402
|
+
content = content.replace(marker, block + marker, 1)
|
|
403
|
+
else:
|
|
404
|
+
content = block + content
|
|
405
|
+
cfg.write_text(content, encoding='utf-8')
|
|
406
|
+
except Exception:
|
|
407
|
+
pass
|
|
408
|
+
|
|
409
|
+
|
|
410
|
+
def configure_team_remote():
|
|
411
|
+
"""配置团队协作仓库的 git remote (workspace/ 产出往这里 push)。
|
|
412
|
+
|
|
413
|
+
当前问题: 完全没人配 remote, 用户不知道往哪 push。
|
|
414
|
+
检测 origin 是否存在, 不存在则问用户要团队仓库地址。
|
|
415
|
+
"""
|
|
416
|
+
print('\n--- 团队协作仓库 ---')
|
|
417
|
+
|
|
418
|
+
# 1. 是否已是 git 仓库
|
|
419
|
+
if not (BASE / '.git').is_dir():
|
|
420
|
+
return # cli.js doInit 已做 git init, 这里兜底
|
|
421
|
+
|
|
422
|
+
# 2. 检测是否已有 origin
|
|
423
|
+
r = run(['git', 'remote'], cwd=str(BASE))
|
|
424
|
+
remotes = r.stdout.split() if r.returncode == 0 else []
|
|
425
|
+
if 'origin' in remotes:
|
|
426
|
+
url_r = run(['git', 'remote', 'get-url', 'origin'], cwd=str(BASE))
|
|
427
|
+
url = url_r.stdout.strip() if url_r.returncode == 0 else '?'
|
|
428
|
+
ok(f'团队仓库已配置: origin -> {url}')
|
|
429
|
+
return
|
|
430
|
+
|
|
431
|
+
# 3. 没有 origin — 从 config.yaml 读 team_remote (如果有)
|
|
432
|
+
team_url = _read_team_remote_from_config()
|
|
433
|
+
if team_url:
|
|
434
|
+
if ask(f'用 config.yaml 里的团队仓库地址? {team_url}', 'y'):
|
|
435
|
+
r = run(['git', 'remote', 'add', 'origin', team_url], cwd=str(BASE))
|
|
436
|
+
if r.returncode == 0:
|
|
437
|
+
ok(f'团队仓库已配置: origin -> {team_url}')
|
|
438
|
+
else:
|
|
439
|
+
warn('remote add 失败: ' + (r.stderr or '').strip()[:100])
|
|
440
|
+
return
|
|
441
|
+
|
|
442
|
+
# 4. 交互式问用户要团队仓库地址
|
|
443
|
+
if not sys.stdin.isatty():
|
|
444
|
+
warn('未配置团队仓库 origin, push 会失败。手动配: git remote add origin <团队仓库URL>')
|
|
445
|
+
return
|
|
446
|
+
|
|
447
|
+
print('\n 配置团队协作仓库 (你的 PRD/任务/产出 push 到这里, 团队共享):')
|
|
448
|
+
try:
|
|
449
|
+
url = input(' 团队仓库地址 (留空跳过, 之后可配): ').strip()
|
|
450
|
+
except (EOFError, KeyboardInterrupt):
|
|
451
|
+
url = ''
|
|
452
|
+
if not url:
|
|
453
|
+
warn('跳过团队仓库配置。之后可手动配: git remote add origin <URL>')
|
|
454
|
+
return
|
|
455
|
+
r = run(['git', 'remote', 'add', 'origin', url], cwd=str(BASE))
|
|
456
|
+
if r.returncode == 0:
|
|
457
|
+
ok(f'团队仓库已配置: origin -> {url}')
|
|
458
|
+
_write_team_remote_to_config(url) # 顺手写 config 供团队复用
|
|
459
|
+
else:
|
|
460
|
+
warn('remote add 失败: ' + (r.stderr or '').strip()[:100])
|
|
461
|
+
|
|
462
|
+
|
|
463
|
+
# ============================================================
|
|
464
|
+
# Step 4: cron 一键注册
|
|
465
|
+
# ============================================================
|
|
466
|
+
|
|
467
|
+
def offer_cron(skip=False):
|
|
468
|
+
if skip:
|
|
469
|
+
return
|
|
470
|
+
print('\n--- 周五自动更新 ---')
|
|
471
|
+
# 检测是否已注册
|
|
472
|
+
if sys.platform == 'win32':
|
|
473
|
+
r = run(['schtasks', '/query', '/tn', 'QODER-WeeklyUpdate'])
|
|
474
|
+
registered = r.returncode == 0
|
|
475
|
+
else:
|
|
476
|
+
r = run(['crontab', '-l'])
|
|
477
|
+
registered = 'QODER-WeeklyUpdate' in (r.stdout or '') or 'run_weekly_update' in (r.stdout or '')
|
|
478
|
+
|
|
479
|
+
if registered:
|
|
480
|
+
ok('周五 cron 已注册')
|
|
481
|
+
return
|
|
482
|
+
|
|
483
|
+
if ask('注册周五 18:00 自动更新知识图谱? (团队只需一台机器注册)', 'y'):
|
|
484
|
+
if sys.platform == 'win32':
|
|
485
|
+
r = run(['cmd', '/c', str(THIS_DIR / 'setup_weekly_cron.bat')], cwd=str(BASE))
|
|
486
|
+
else:
|
|
487
|
+
r = run(['bash', str(THIS_DIR / 'setup_weekly_cron.sh')], cwd=str(BASE))
|
|
488
|
+
if r.returncode == 0:
|
|
489
|
+
ok('cron 已注册 (周五 18:00)')
|
|
490
|
+
else:
|
|
491
|
+
warn(f'cron 注册失败: {(r.stderr or "").strip()[:100]}')
|
|
492
|
+
else:
|
|
493
|
+
print(' 跳过。以后可手动跑: python .qoder/scripts/setup_weekly_cron.bat')
|
|
494
|
+
|
|
495
|
+
|
|
496
|
+
# ============================================================
|
|
497
|
+
# Step 4.5: 蓝湖 MCP (设计师直读蓝湖设计稿)
|
|
498
|
+
# ============================================================
|
|
499
|
+
|
|
500
|
+
def offer_lanhu(name, skip=False):
|
|
501
|
+
"""引导蓝湖 MCP: clone + venv 依赖 + cookie + mcp.json 注册。
|
|
502
|
+
|
|
503
|
+
蓝湖 MCP 是第三方 HTTP server (dsphper/lanhu-mcp), 让设计师发蓝湖链接,
|
|
504
|
+
AI 直读设计稿(颜色/尺寸/字体/切图), 比截图口述精确。opt-in, 不用蓝湖可跳过。
|
|
505
|
+
"""
|
|
506
|
+
if skip:
|
|
507
|
+
return
|
|
508
|
+
print('\n--- 蓝湖 MCP (设计稿直读增强) ---')
|
|
509
|
+
# 已配好就不问, 直接确认
|
|
510
|
+
try:
|
|
511
|
+
sys.path.insert(0, str(THIS_DIR))
|
|
512
|
+
from setup_lanhu import (_read_env_cookie, register_lanhu_mcp,
|
|
513
|
+
QODERWORK_MCP_FILE, LANHU_DIR, is_lanhu_running)
|
|
514
|
+
import json as _json
|
|
515
|
+
already = False
|
|
516
|
+
if QODERWORK_MCP_FILE.is_file():
|
|
517
|
+
try:
|
|
518
|
+
cfg = _json.loads(QODERWORK_MCP_FILE.read_text(encoding='utf-8'))
|
|
519
|
+
already = 'lanhu' in cfg.get('mcpServers', {})
|
|
520
|
+
except (ValueError, OSError):
|
|
521
|
+
pass
|
|
522
|
+
if already:
|
|
523
|
+
ok('蓝湖 MCP 已配置')
|
|
524
|
+
if not is_lanhu_running():
|
|
525
|
+
print(' (蓝湖服务端未启动, 用时再起)')
|
|
526
|
+
return
|
|
527
|
+
except ImportError:
|
|
528
|
+
warn('setup_lanhu.py 不可用, 跳过蓝湖引导')
|
|
529
|
+
return
|
|
530
|
+
|
|
531
|
+
# 问是否配 (默认 y, 因为团队多数用蓝湖)
|
|
532
|
+
if not ask('团队用蓝湖做设计协作吗? 配蓝湖 MCP (设计师发链接 AI 直读设计稿)?', 'y'):
|
|
533
|
+
print(' 跳过。之后可手动跑: python .qoder/scripts/setup_lanhu.py')
|
|
534
|
+
return
|
|
535
|
+
|
|
536
|
+
# 调引导主流程 (clone + 依赖 + cookie + mcp.json 注册)
|
|
537
|
+
r = subprocess.run(
|
|
538
|
+
[sys.executable, str(THIS_DIR / 'setup_lanhu.py'), '--name', name or 'Developer'],
|
|
539
|
+
cwd=str(BASE))
|
|
540
|
+
if r.returncode == 0:
|
|
541
|
+
ok('蓝湖 MCP 引导完成')
|
|
542
|
+
else:
|
|
543
|
+
warn('蓝湖 MCP 引导未完全成功 (见上方输出), 不阻塞主线')
|
|
544
|
+
|
|
545
|
+
|
|
546
|
+
# ============================================================
|
|
547
|
+
# Step 5: QoderWork 技能
|
|
548
|
+
# ============================================================
|
|
549
|
+
|
|
550
|
+
def offer_qoderwork(skip=False):
|
|
551
|
+
if skip:
|
|
552
|
+
return
|
|
553
|
+
print('\n--- QoderWork 桌面端 ---')
|
|
554
|
+
home = Path(os.environ.get('USERPROFILE') or os.environ.get('HOME') or '~').expanduser()
|
|
555
|
+
qw_dir = home / '.qoderwork'
|
|
556
|
+
if not qw_dir.exists():
|
|
557
|
+
print(' 未检测到 QoderWork 桌面端, 跳过。')
|
|
558
|
+
return
|
|
559
|
+
|
|
560
|
+
# 检查是否已装
|
|
561
|
+
skills_dir = qw_dir / 'skills'
|
|
562
|
+
installed = skills_dir.is_dir() and any(skills_dir.iterdir()) if skills_dir.exists() else False
|
|
563
|
+
if installed:
|
|
564
|
+
ok('QoderWork 技能已安装')
|
|
565
|
+
return
|
|
566
|
+
|
|
567
|
+
if ask('检测到 QoderWork, 安装技能 (软链到 ~/.qoderwork/skills/)?', 'y'):
|
|
568
|
+
r = subprocess.run([sys.executable, str(THIS_DIR / 'install_qoderwork.py')], cwd=str(BASE))
|
|
569
|
+
if r.returncode == 0:
|
|
570
|
+
ok('QoderWork 技能已安装')
|
|
571
|
+
else:
|
|
572
|
+
warn('安装失败, 见上方输出')
|
|
573
|
+
|
|
574
|
+
|
|
575
|
+
# ============================================================
|
|
576
|
+
# Step 6: 就绪报告
|
|
577
|
+
# ============================================================
|
|
578
|
+
|
|
579
|
+
def print_ready(name, role):
|
|
580
|
+
print('\n' + '=' * 56)
|
|
581
|
+
print(' 一键就绪完成')
|
|
582
|
+
print('=' * 56)
|
|
583
|
+
|
|
584
|
+
# 读关键状态
|
|
585
|
+
dev_file = BASE / '.qoder' / '.developer'
|
|
586
|
+
if dev_file.is_file():
|
|
587
|
+
ok(f'开发者: {name} ({role})')
|
|
588
|
+
|
|
589
|
+
# git config (git 可能未装)
|
|
590
|
+
try:
|
|
591
|
+
r = run(['git', 'config', 'user.name'], cwd=str(BASE))
|
|
592
|
+
if r.returncode == 0 and r.stdout.strip():
|
|
593
|
+
ok(f'Git: user.name={r.stdout.strip()}')
|
|
594
|
+
except FileNotFoundError:
|
|
595
|
+
pass # 无 git, 跳过此项
|
|
596
|
+
|
|
597
|
+
# 源码仓库
|
|
598
|
+
code_dir = BASE / 'data' / 'code'
|
|
599
|
+
if code_dir.is_dir():
|
|
600
|
+
projects = [d.name for d in code_dir.iterdir() if d.is_dir() and not d.name.startswith('.')]
|
|
601
|
+
if projects:
|
|
602
|
+
ok(f'源码仓库: {len(projects)} 个项目 ({", ".join(projects[:3])})')
|
|
603
|
+
|
|
604
|
+
# 索引
|
|
605
|
+
ki = BASE / 'data' / 'index' / 'code-keyword.json'
|
|
606
|
+
if ki.is_file():
|
|
607
|
+
try:
|
|
608
|
+
import json
|
|
609
|
+
data = json.loads(ki.read_text(encoding='utf-8'))
|
|
610
|
+
ok(f'知识图谱: {len(data)} keywords')
|
|
611
|
+
except Exception:
|
|
612
|
+
pass
|
|
613
|
+
|
|
614
|
+
# cron
|
|
615
|
+
if sys.platform == 'win32':
|
|
616
|
+
r = run(['schtasks', '/query', '/tn', 'QODER-WeeklyUpdate'])
|
|
617
|
+
if r.returncode == 0:
|
|
618
|
+
ok('周五 cron: 已注册')
|
|
619
|
+
|
|
620
|
+
# 蓝湖 MCP (可选增强)
|
|
621
|
+
try:
|
|
622
|
+
import json as _json
|
|
623
|
+
mcp_f = Path(os.environ.get('USERPROFILE') or os.environ.get('HOME') or '~').expanduser() / '.qoderwork' / 'mcp.json'
|
|
624
|
+
if mcp_f.is_file():
|
|
625
|
+
cfg = _json.loads(mcp_f.read_text(encoding='utf-8'))
|
|
626
|
+
if 'lanhu' in cfg.get('mcpServers', {}):
|
|
627
|
+
ok('蓝湖 MCP: 已注册 (设计师可发蓝湖链接)')
|
|
628
|
+
except (ValueError, OSError, ImportError):
|
|
629
|
+
pass
|
|
630
|
+
|
|
631
|
+
print()
|
|
632
|
+
print(' 现在可以开始了:')
|
|
633
|
+
print(' /wl-prd-full 写需求 /wl-search 查代码 /wl-task 建任务')
|
|
634
|
+
print(' /wl-status 看进度 /wl-report 出日报 /wl-commit 提交')
|
|
635
|
+
print()
|
|
636
|
+
print(' 或直接用自然语言:')
|
|
637
|
+
print(' "写个保险异常筛选的 PRD"')
|
|
638
|
+
print(' "查一下考勤代码在哪"')
|
|
639
|
+
print('=' * 56)
|
|
640
|
+
|
|
641
|
+
|
|
642
|
+
# ============================================================
|
|
643
|
+
# Main
|
|
644
|
+
# ============================================================
|
|
645
|
+
|
|
646
|
+
def main():
|
|
647
|
+
parser = argparse.ArgumentParser(
|
|
648
|
+
description='一键就绪: 配置开发者/git/源码/索引/cron/QoderWork',
|
|
649
|
+
formatter_class=argparse.RawDescriptionHelpFormatter,
|
|
650
|
+
epilog='''示例:
|
|
651
|
+
python .qoder/scripts/setup.py # 自动探测名字
|
|
652
|
+
python .qoder/scripts/setup.py 小王 # 指定名字
|
|
653
|
+
python .qoder/scripts/setup.py 小王 dev # 名字+角色
|
|
654
|
+
python .qoder/scripts/setup.py --skip-cron # 跳过 cron 询问
|
|
655
|
+
''')
|
|
656
|
+
parser.add_argument('name', nargs='?', default=None, help='开发者名字 (不传则从 git 探测)')
|
|
657
|
+
parser.add_argument('role', nargs='?', default='pm', help='角色 (pm/dev/design/admin)')
|
|
658
|
+
parser.add_argument('--skip-cron', action='store_true', help='跳过 cron 注册')
|
|
659
|
+
parser.add_argument('--skip-qoderwork', action='store_true', help='跳过 QoderWork 安装')
|
|
660
|
+
parser.add_argument('--skip-lanhu', action='store_true', help='跳过蓝湖 MCP 引导')
|
|
661
|
+
args = parser.parse_args()
|
|
662
|
+
|
|
663
|
+
print('=' * 56)
|
|
664
|
+
print(' QODER 一键就绪')
|
|
665
|
+
print('=' * 56)
|
|
666
|
+
|
|
667
|
+
# Step 0
|
|
668
|
+
if not check_env():
|
|
669
|
+
return 1
|
|
670
|
+
|
|
671
|
+
# Step 1
|
|
672
|
+
name = detect_name(args.name)
|
|
673
|
+
if not name or not name.strip():
|
|
674
|
+
fail('未能确定开发者名字')
|
|
675
|
+
return 1
|
|
676
|
+
|
|
677
|
+
# Step 1.5: Git 身份配置 (在 init_doctor 之前, 确保下游探测到真账号而非假账号)
|
|
678
|
+
configure_git_identity(name)
|
|
679
|
+
|
|
680
|
+
# Step 2
|
|
681
|
+
if not run_doctor(name, args.role):
|
|
682
|
+
warn('init_doctor 未完全成功, 继续 setup 后续步骤...')
|
|
683
|
+
|
|
684
|
+
# Step 3
|
|
685
|
+
guide_config()
|
|
686
|
+
|
|
687
|
+
# Step 3.5: 团队协作仓库 remote (workspace/ 产出 push 到这里)
|
|
688
|
+
configure_team_remote()
|
|
689
|
+
|
|
690
|
+
# Step 4
|
|
691
|
+
offer_cron(skip=args.skip_cron)
|
|
692
|
+
|
|
693
|
+
# Step 4.5: 蓝湖 MCP (设计师直读蓝湖设计稿)
|
|
694
|
+
offer_lanhu(name, skip=args.skip_lanhu)
|
|
695
|
+
|
|
696
|
+
# Step 5
|
|
697
|
+
offer_qoderwork(skip=args.skip_qoderwork)
|
|
698
|
+
|
|
699
|
+
# Step 6
|
|
700
|
+
print_ready(name, args.role)
|
|
701
|
+
|
|
702
|
+
return 0
|
|
703
|
+
|
|
704
|
+
|
|
705
|
+
if __name__ == '__main__':
|
|
706
|
+
sys.exit(main())
|