@hupan56/wlkj 2.5.0 → 2.6.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 +28 -1
- package/package.json +1 -1
- package/templates/qoder/agents/prd-planning.md +2 -1
- package/templates/qoder/agents/prd-quick.md +1 -0
- package/templates/qoder/agents/prd-reference.md +2 -0
- package/templates/qoder/agents/prd-research.md +1 -0
- package/templates/qoder/commands/wl-prd.md +34 -11
- package/templates/qoder/config.yaml +8 -0
- package/templates/qoder/hooks/inject-workflow-state.py +33 -7
- package/templates/qoder/hooks/session-start.py +62 -11
- package/templates/qoder/scripts/__pycache__/init_doctor.cpython-39.pyc +0 -0
- package/templates/qoder/scripts/__pycache__/setup.cpython-39.pyc +0 -0
- package/templates/qoder/scripts/common/__pycache__/developer.cpython-39.pyc +0 -0
- package/templates/qoder/scripts/common/developer.py +26 -19
- package/templates/qoder/scripts/common/identity.py +6 -1
- package/templates/qoder/scripts/eval_prd.py +8 -2
- package/templates/qoder/scripts/init_doctor.py +14 -1
- package/templates/qoder/scripts/setup.py +190 -2
- package/templates/qoder/scripts/syncgate.py +3 -2
- package/templates/qoder/skills/prd-generator/SKILL.md +7 -4
- package/templates/qoder/skills/wl-prd/SKILL.md +3 -0
- package/templates/qoder/templates/prd-full-template.md +2 -0
- package/templates/qoder/templates/prd-quick-template.md +1 -0
package/bin/cli.js
CHANGED
|
@@ -131,7 +131,7 @@ function doInit(name) {
|
|
|
131
131
|
execSync(cmd, { cwd, stdio: "inherit", timeout: 300000 });
|
|
132
132
|
} catch (e) {
|
|
133
133
|
console.log(` setup 部分失败 (不阻塞): ${(e.message || "").slice(0, 100)}`);
|
|
134
|
-
console.log(`
|
|
134
|
+
console.log(` 可重新运行: npx @hupan56/wlkj init ${name || "<你的名字>"}`);
|
|
135
135
|
}
|
|
136
136
|
} else {
|
|
137
137
|
console.log(` setup.py 未找到, 跳过自动初始化`);
|
|
@@ -386,6 +386,33 @@ function doUpdate() {
|
|
|
386
386
|
console.log(`\n 生效方式:`);
|
|
387
387
|
console.log(` Qoder IDE / Quest: 新建对话即可`);
|
|
388
388
|
console.log(` QoderWork: 重启应用或新建对话`);
|
|
389
|
+
|
|
390
|
+
// 检查 git 身份是否需要配置 (只检测不引导, update 保持轻量)
|
|
391
|
+
// 老用户如果之前 init 时塞了假账号, 这里提示重跑 init 修正
|
|
392
|
+
let gitWarn = false;
|
|
393
|
+
try {
|
|
394
|
+
const devFile = path.join(cwd, ".qoder", ".developer");
|
|
395
|
+
if (fs.existsSync(devFile)) {
|
|
396
|
+
const devName = fs.readFileSync(devFile, "utf-8").trim();
|
|
397
|
+
const mj = path.join(cwd, "workspace", "members", devName, "member.json");
|
|
398
|
+
if (fs.existsSync(mj)) {
|
|
399
|
+
const m = JSON.parse(fs.readFileSync(mj, "utf-8"));
|
|
400
|
+
const ga = m.git_author || "";
|
|
401
|
+
if (!ga || ga.includes("@local") || ga.includes("@unknown")) {
|
|
402
|
+
gitWarn = true;
|
|
403
|
+
}
|
|
404
|
+
}
|
|
405
|
+
}
|
|
406
|
+
} catch { /* 检测失败不阻塞 */ }
|
|
407
|
+
|
|
408
|
+
if (gitWarn) {
|
|
409
|
+
console.log(`\n ⚠ git 账号未配置或为假账号 (push/同步会失败)`);
|
|
410
|
+
console.log(` 重跑初始化可修复: npx @hupan56/wlkj init <你的名字>`);
|
|
411
|
+
console.log(` (init 会交互式引导你配置团队 git 账号和团队仓库)`);
|
|
412
|
+
} else {
|
|
413
|
+
console.log(`\n 如果还没配过 git 账号或团队仓库, 重跑: npx @hupan56/wlkj init <你的名字>`);
|
|
414
|
+
}
|
|
415
|
+
|
|
389
416
|
if (protectedN > 0) {
|
|
390
417
|
console.log(`\n ⚠ 有配置文件新版有变化, 生成 .new 文件:`);
|
|
391
418
|
console.log(` 确认无需合并后可直接删除, 或手动合并后删除`);
|
package/package.json
CHANGED
|
@@ -53,4 +53,5 @@ with the user's latest answers (do not loop indefinitely).
|
|
|
53
53
|
## Output
|
|
54
54
|
1. Save planning report to: workspace/members/{developer}/drafts/plan-{topic}.md
|
|
55
55
|
2. If the user wants to proceed with the MVP, also generate
|
|
56
|
-
PRD + prototype (per /wl-prd Step 6, platform template applies)
|
|
56
|
+
PRD + prototype (per /wl-prd Step 6, platform template applies)
|
|
57
|
+
**PRD 第一行必须是 `<!-- mode: planning -->`** (syncgate/eval_prd 靠此识别模式).
|
|
@@ -28,6 +28,8 @@ Generate PRD + prototype by referencing existing project data.
|
|
|
28
28
|
genuinely ambiguous points.
|
|
29
29
|
6. After confirmation, generate:
|
|
30
30
|
a. PRD (product-type template + 10-point quality check)
|
|
31
|
+
**PRD 第一行必须是 `<!-- mode: reference -->`** (从模板继承, 别删/改).
|
|
32
|
+
syncgate/eval_prd 靠这个标记识别模式; 缺失会按 full 模板检查 (此处正确).
|
|
31
33
|
b. Interactive HTML prototype from the platform template
|
|
32
34
|
(.qoder/templates/prototype-web.html or prototype-app.html;
|
|
33
35
|
Both -> two files with -web/-app suffix)
|
|
@@ -24,6 +24,7 @@ Your specific perspective is defined in the dispatch prompt.
|
|
|
24
24
|
|
|
25
25
|
Note: your report is an intermediate artifact - the caller merges all
|
|
26
26
|
agent reports into the PRD and then deletes brainstorm-agent*.md files.
|
|
27
|
+
合并生成的最终 PRD 第一行必须是 `<!-- mode: brainstorm -->` (syncgate/eval_prd 靠此识别模式).
|
|
27
28
|
|
|
28
29
|
## Output Format
|
|
29
30
|
# Brainstorm Report: {perspective}
|
|
@@ -81,16 +81,27 @@ Discuss 结束、用户说"出PRD"时,AI 必须输出:
|
|
|
81
81
|
```
|
|
82
82
|
然后按 Reference 模式流程生成。
|
|
83
83
|
|
|
84
|
-
###
|
|
84
|
+
### 全模式标记规则(生成 PRD 时必须遵守)
|
|
85
85
|
|
|
86
|
-
|
|
86
|
+
**每种模式生成的 PRD,第一行必须是 `<!-- mode: <模式名> -->` 标记**:
|
|
87
87
|
```markdown
|
|
88
|
-
<!-- mode:
|
|
89
|
-
#
|
|
88
|
+
<!-- mode: reference --> (或 brainstorm / planning / quick)
|
|
89
|
+
# 标题...
|
|
90
90
|
```
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
91
|
+
|
|
92
|
+
各模式标记值与 EVA 门禁行为:
|
|
93
|
+
|
|
94
|
+
| 模式 | mode 标记 | EVA 处理 | 说明 |
|
|
95
|
+
|------|----------|---------|------|
|
|
96
|
+
| Reference | `<!-- mode: reference -->` | 全跑 (A1+A3) | 默认完整 PRD |
|
|
97
|
+
| Brainstorm | `<!-- mode: brainstorm -->` | 全跑 | 创新方案 PRD |
|
|
98
|
+
| Planning | `<!-- mode: planning -->` | 全跑 | 规划路线图 PRD |
|
|
99
|
+
| Quick | `<!-- mode: quick -->` | **跳过 EVA** | Mini-PRD 精简, A3 字段检查不适用 |
|
|
100
|
+
| Discuss | (不出文件, 无标记) | — | 纯讨论 |
|
|
101
|
+
|
|
102
|
+
> 标记从模板继承(模板已有正确默认值)。生成时**只改 mode 值,别删标记**。
|
|
103
|
+
> syncgate 和 eval_prd 都靠这个标记识别模式;缺失标记会按 full 模板检查(对 Quick 会误杀)。
|
|
104
|
+
> 向后兼容:旧 PRD 无标记时,eval_prd 回退到内容识别(看 Mini-PRD/零星需求字样)。
|
|
94
105
|
|
|
95
106
|
### Brainstorm 中间文件清理
|
|
96
107
|
|
|
@@ -108,10 +119,22 @@ For simple requirements you may run the mode workflow inline instead of
|
|
|
108
119
|
dispatching — the steps are the same.
|
|
109
120
|
|
|
110
121
|
### 模式不确定时的兜底(必须遵守)
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
122
|
+
|
|
123
|
+
**推断流程(每次必走)**:
|
|
124
|
+
1. AI 根据用户关键词推断出一个候选模式
|
|
125
|
+
2. **置信度自检**:问自己"这个需求是否可能属于另一个模式?"
|
|
126
|
+
- 若可能(≥2 模式沾边,如既像"分析"又像"规划")→ **必须问用户**
|
|
127
|
+
- 若无明显歧义 → 直接用候选模式
|
|
128
|
+
3. 默认走 Reference 仅当用户明确给出需求描述且无探讨/创新/规划信号时
|
|
129
|
+
|
|
130
|
+
**歧义确认话术(统一问法,降低不一致)**:
|
|
131
|
+
```
|
|
132
|
+
这个需求我理解为 [X 模式](会:[X 的产出])。
|
|
133
|
+
也可以走 [Y 模式](会:[Y 的产出])。
|
|
134
|
+
你要哪种?
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
**铁律**:宁可多问一句,不要错模式。错模式的 PRD 是废纸——EVA 门禁只管格式不管方向,格式过了的废 PRD 照样能发布。
|
|
115
138
|
|
|
116
139
|
## Storage
|
|
117
140
|
|
|
@@ -35,6 +35,14 @@ platforms:
|
|
|
35
35
|
ui_stack: "Vant 3"
|
|
36
36
|
prototype_template: .qoder/templates/prototype-app.html
|
|
37
37
|
|
|
38
|
+
# ── 团队协作仓库 (workspace/ 产出 push 到这里) ──
|
|
39
|
+
# 这是团队共享 PRD/任务/原型的仓库, 跟 data/code/ 的源码仓库不同。
|
|
40
|
+
# setup.py init 时会交互式配置 git remote add origin <url>。
|
|
41
|
+
# 不配也能本地工作, 但 push/同步功能不可用。
|
|
42
|
+
team_remote:
|
|
43
|
+
url: "" # 留空则 setup.py 会问; 填了团队所有人 init 时自动用这个地址
|
|
44
|
+
branch: master # 团队主分支
|
|
45
|
+
|
|
38
46
|
# ── Git Sync (Weekly) ──
|
|
39
47
|
# 每个项目在 data/code/ 下有自己的 git 仓库
|
|
40
48
|
# 运行: python .qoder/scripts/git_sync.py
|
|
@@ -15,6 +15,31 @@ BASE = os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)
|
|
|
15
15
|
_CACHE = {}
|
|
16
16
|
|
|
17
17
|
|
|
18
|
+
def _append_hook_error(log_path, line):
|
|
19
|
+
"""写一行错误到 hook-errors.log, 超 100KB 截断保留最后 50KB (防无限膨胀)。
|
|
20
|
+
|
|
21
|
+
best-effort: 任何 IO 失败都静默 (不能让日志写入失败导致 hook 二次崩溃)。
|
|
22
|
+
"""
|
|
23
|
+
from datetime import datetime
|
|
24
|
+
try:
|
|
25
|
+
os.makedirs(os.path.dirname(log_path), exist_ok=True)
|
|
26
|
+
# 轮转: 文件过大则截断
|
|
27
|
+
if os.path.isfile(log_path):
|
|
28
|
+
try:
|
|
29
|
+
if os.path.getsize(log_path) > 100 * 1024:
|
|
30
|
+
with open(log_path, 'r', encoding='utf-8', errors='replace') as f:
|
|
31
|
+
tail = f.read()[-50 * 1024:]
|
|
32
|
+
with open(log_path, 'w', encoding='utf-8') as f:
|
|
33
|
+
f.write(tail)
|
|
34
|
+
except OSError:
|
|
35
|
+
pass
|
|
36
|
+
with open(log_path, 'a', encoding='utf-8') as f:
|
|
37
|
+
f.write('[{}] inject-workflow-state: {}\n'.format(
|
|
38
|
+
datetime.now().strftime('%Y-%m-%d %H:%M:%S'), str(line)[:200]))
|
|
39
|
+
except Exception:
|
|
40
|
+
pass
|
|
41
|
+
|
|
42
|
+
|
|
18
43
|
def _read_developer():
|
|
19
44
|
"""读当前开发者名 (容错: UTF-8/GBK)."""
|
|
20
45
|
df = os.path.join(BASE, '.qoder', '.developer')
|
|
@@ -106,7 +131,10 @@ def main():
|
|
|
106
131
|
if os.path.isfile(state_file):
|
|
107
132
|
last_sig = open(state_file, 'r', encoding='utf-8').read().strip()
|
|
108
133
|
if last_sig == current_sig:
|
|
109
|
-
|
|
134
|
+
# 状态没变, 不重复注入 (省 token), 但仍输出健康标记
|
|
135
|
+
# QoderWork 看不到任何输出时分不清"缓存跳过"还是"hook 没跑"
|
|
136
|
+
print('<!-- [hook-ok] inject-workflow-state: state unchanged (cached) -->')
|
|
137
|
+
return
|
|
110
138
|
open(state_file, 'w', encoding='utf-8').write(current_sig)
|
|
111
139
|
except OSError:
|
|
112
140
|
pass
|
|
@@ -119,6 +147,8 @@ def main():
|
|
|
119
147
|
parts.append('Status: ' + str(st))
|
|
120
148
|
parts.append('Do: ' + g)
|
|
121
149
|
parts.append('</qoder-workflow>')
|
|
150
|
+
# Hook-1: 健康标记 — 与 session-start 一致, QoderWork 看不到说明 hook 没生效
|
|
151
|
+
parts.append('<!-- [hook-ok] inject-workflow-state hook ran -->')
|
|
122
152
|
print(NL.join(parts))
|
|
123
153
|
|
|
124
154
|
|
|
@@ -129,12 +159,8 @@ if __name__ == '__main__':
|
|
|
129
159
|
# 落盘到日志 (stdout 一次性, 追溯用)
|
|
130
160
|
try:
|
|
131
161
|
from pathlib import Path
|
|
132
|
-
|
|
133
|
-
log_path
|
|
134
|
-
log_path.parent.mkdir(parents=True, exist_ok=True)
|
|
135
|
-
with open(log_path, 'a', encoding='utf-8') as f:
|
|
136
|
-
f.write('[{}] inject-workflow-state: {}\n'.format(
|
|
137
|
-
datetime.now().strftime('%Y-%m-%d %H:%M:%S'), str(e)[:200]))
|
|
162
|
+
log_path = str(Path(__file__).resolve().parents[1] / '.runtime' / 'hook-errors.log')
|
|
163
|
+
_append_hook_error(log_path, str(e)[:200])
|
|
138
164
|
except Exception:
|
|
139
165
|
pass
|
|
140
166
|
print('<qoder-workflow>')
|
|
@@ -10,6 +10,58 @@ if sys.platform == 'win32':
|
|
|
10
10
|
NL = chr(10)
|
|
11
11
|
BASE = os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
|
12
12
|
|
|
13
|
+
HOOK_ERRORS_LOG = os.path.join(BASE, '.qoder', '.runtime', 'hook-errors.log')
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def _append_hook_error(log_path, line):
|
|
17
|
+
"""写一行错误到 hook-errors.log, 超 100KB 截断保留最后 50KB (防无限膨胀)。best-effort。"""
|
|
18
|
+
from datetime import datetime
|
|
19
|
+
try:
|
|
20
|
+
os.makedirs(os.path.dirname(log_path), exist_ok=True)
|
|
21
|
+
if os.path.isfile(log_path):
|
|
22
|
+
try:
|
|
23
|
+
if os.path.getsize(log_path) > 100 * 1024:
|
|
24
|
+
with open(log_path, 'r', encoding='utf-8', errors='replace') as f:
|
|
25
|
+
tail = f.read()[-50 * 1024:]
|
|
26
|
+
with open(log_path, 'w', encoding='utf-8') as f:
|
|
27
|
+
f.write(tail)
|
|
28
|
+
except OSError:
|
|
29
|
+
pass
|
|
30
|
+
with open(log_path, 'a', encoding='utf-8') as f:
|
|
31
|
+
f.write('[{}] session-start: {}\n'.format(
|
|
32
|
+
datetime.now().strftime('%Y-%m-%d %H:%M:%S'), str(line)[:200]))
|
|
33
|
+
except Exception:
|
|
34
|
+
pass
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def _recent_hook_errors(hours=24, limit=3):
|
|
38
|
+
"""读最近 N 小时的 hook 错误, 返回最多 limit 条 (供会话注入告警)。
|
|
39
|
+
|
|
40
|
+
错误之前只落盘到 .runtime/hook-errors.log, PM 永远看不到 → hook 坏了无感知。
|
|
41
|
+
改后每次开会话把最近错误摘要注入上下文。
|
|
42
|
+
"""
|
|
43
|
+
try:
|
|
44
|
+
from datetime import datetime, timedelta
|
|
45
|
+
if not os.path.isfile(HOOK_ERRORS_LOG):
|
|
46
|
+
return []
|
|
47
|
+
cutoff = datetime.now() - timedelta(hours=hours)
|
|
48
|
+
recent = []
|
|
49
|
+
with open(HOOK_ERRORS_LOG, encoding='utf-8', errors='replace') as f:
|
|
50
|
+
for line in f:
|
|
51
|
+
line = line.strip()
|
|
52
|
+
if not line or '[' not in line or ']' not in line:
|
|
53
|
+
continue
|
|
54
|
+
try:
|
|
55
|
+
ts = line.split(']')[0].lstrip('[')
|
|
56
|
+
dt = datetime.strptime(ts, '%Y-%m-%d %H:%M:%S')
|
|
57
|
+
if dt >= cutoff:
|
|
58
|
+
recent.append(line)
|
|
59
|
+
except (ValueError, IndexError):
|
|
60
|
+
continue
|
|
61
|
+
return recent[-limit:] if recent else []
|
|
62
|
+
except Exception:
|
|
63
|
+
return []
|
|
64
|
+
|
|
13
65
|
# Injected as REAL OUTPUT below (a comment here would never reach the AI)
|
|
14
66
|
CRITICAL_RULE = NL.join([
|
|
15
67
|
'## CRITICAL RULE',
|
|
@@ -174,6 +226,14 @@ def get_style_info():
|
|
|
174
226
|
def main():
|
|
175
227
|
parts = []
|
|
176
228
|
parts.append('<qoder-context>')
|
|
229
|
+
# Hook-3: 上次会话以来的 hook 错误告警 (不只落盘, 要让用户/AI 看到)
|
|
230
|
+
# hook 坏了会导致上下文注入失败 (任务状态/平台规则缺失), 必须可见
|
|
231
|
+
recent_errors = _recent_hook_errors(hours=24, limit=3)
|
|
232
|
+
if recent_errors:
|
|
233
|
+
parts.append('⚠️ [HOOK-WARN] 过去 24h 有 hook 错误 (上下文注入可能失效):')
|
|
234
|
+
for err in recent_errors:
|
|
235
|
+
parts.append(' ' + err)
|
|
236
|
+
parts.append(' 详情见 .qoder/.runtime/hook-errors.log; 检查 Python 环境或跑 init_doctor --fix')
|
|
177
237
|
dev = get_developer()
|
|
178
238
|
if dev:
|
|
179
239
|
name = dev.get('name', 'unknown')
|
|
@@ -216,17 +276,8 @@ if __name__ == '__main__':
|
|
|
216
276
|
main()
|
|
217
277
|
except Exception as e:
|
|
218
278
|
# A broken hook must never kill the session - emit minimal context
|
|
219
|
-
#
|
|
220
|
-
|
|
221
|
-
from pathlib import Path
|
|
222
|
-
log_path = Path(__file__).resolve().parents[1] / '.runtime' / 'hook-errors.log'
|
|
223
|
-
log_path.parent.mkdir(parents=True, exist_ok=True)
|
|
224
|
-
from datetime import datetime
|
|
225
|
-
with open(log_path, 'a', encoding='utf-8') as f:
|
|
226
|
-
f.write('[{}] session-start: {}\n'.format(
|
|
227
|
-
datetime.now().strftime('%Y-%m-%d %H:%M:%S'), str(e)[:200]))
|
|
228
|
-
except Exception:
|
|
229
|
-
pass # 日志写失败不影响降级
|
|
279
|
+
# 同时落盘到日志 (带轮转), 方便下次会话告警
|
|
280
|
+
_append_hook_error(HOOK_ERRORS_LOG, str(e)[:200])
|
|
230
281
|
print('<qoder-context>')
|
|
231
282
|
print('session-start hook error: ' + str(e)[:200])
|
|
232
283
|
print(' (详情见 .qoder/.runtime/hook-errors.log)')
|
|
Binary file
|
|
Binary file
|
|
@@ -73,9 +73,11 @@ def _detect_git_author() -> str | None:
|
|
|
73
73
|
|
|
74
74
|
|
|
75
75
|
def _ensure_git_config(name: str, repo_root: Path) -> None:
|
|
76
|
-
"""
|
|
76
|
+
"""确认 git user.name/email 已配置。未配则提示 (绝不自动造假账号)。
|
|
77
77
|
|
|
78
|
-
|
|
78
|
+
旧逻辑会塞 name@local 假账号, 与零信任 push 门禁冲突, 已废弃。
|
|
79
|
+
真正的 git 配置由 setup.py configure_git_identity() 在 init 前完成。
|
|
80
|
+
这里只做兜底检测: 如果走到这还没配, 提示用户去配。
|
|
79
81
|
"""
|
|
80
82
|
import subprocess
|
|
81
83
|
# 先看 git 是否可用
|
|
@@ -87,23 +89,20 @@ def _ensure_git_config(name: str, repo_root: Path) -> None:
|
|
|
87
89
|
|
|
88
90
|
author = _detect_git_author()
|
|
89
91
|
if author:
|
|
90
|
-
|
|
92
|
+
# 检测到疑似假账号也要提示 (虽然配了, 但 push 会被门禁拒)
|
|
93
|
+
if "@local" in author:
|
|
94
|
+
print(f" [WARN] git 作者疑似假账号: {author} (push 门禁会拒绝)")
|
|
95
|
+
print(f" 请重跑 'wlkj init' 配置真实团队 git 账号")
|
|
96
|
+
else:
|
|
97
|
+
print(f" [OK] git 作者: {author}")
|
|
91
98
|
return
|
|
92
99
|
|
|
93
|
-
# git
|
|
94
|
-
print(f" [
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
subprocess.run(["git", "config", "--local", "user.email", fake_email],
|
|
100
|
-
cwd=str(repo_root), capture_output=True, timeout=5)
|
|
101
|
-
print(f" user.name = {name}")
|
|
102
|
-
print(f" user.email = {fake_email}")
|
|
103
|
-
print(f" (仅本仓库生效。如有团队 git 账号, 自行改: git config --local user.email 你的邮箱)")
|
|
104
|
-
except (FileNotFoundError, OSError, subprocess.SubprocessError) as e:
|
|
105
|
-
print(f" [WARN] 设置 git config 失败 (不阻塞): {e}")
|
|
106
|
-
print(f" 请手动跑: git config user.name {name} && git config user.email 你的邮箱")
|
|
100
|
+
# git 未配: 不造假, 只提示
|
|
101
|
+
print(f" [WARN] git 未配置 user.name/email")
|
|
102
|
+
print(f" push/commit 会失败。请配置团队 git 账号:")
|
|
103
|
+
print(f" git config --global user.name '你的名字'")
|
|
104
|
+
print(f" git config --global user.email '你的团队邮箱'")
|
|
105
|
+
print(f" 或重跑: wlkj init <你的名字>")
|
|
107
106
|
|
|
108
107
|
|
|
109
108
|
def init_developer(name: str, role: str | None = None,
|
|
@@ -173,7 +172,15 @@ def init_developer(name: str, role: str | None = None,
|
|
|
173
172
|
# 这是 push 门禁的前提: 没 member.json → syncgate 拒绝 push
|
|
174
173
|
git_author = _detect_git_author()
|
|
175
174
|
try:
|
|
176
|
-
from common.identity import register_member, IdentityError
|
|
175
|
+
from common.identity import register_member, IdentityError, get_member
|
|
176
|
+
# 修正: 旧 member.json 若是假账号(@local), 且现在 git 配了真账号 → 用真账号覆盖
|
|
177
|
+
# register_member 已有"已注册则只更新 git_author, 密钥不动"的逻辑, 这里只加提示
|
|
178
|
+
existing_member = get_member(name, repo_root)
|
|
179
|
+
if existing_member:
|
|
180
|
+
old_author = existing_member.get("git_author", "")
|
|
181
|
+
if (old_author and "@local" in old_author
|
|
182
|
+
and git_author and "@local" not in git_author):
|
|
183
|
+
print(f" [修正] member.json git_author 从假账号更新为真账号: {git_author}")
|
|
177
184
|
try:
|
|
178
185
|
register_member(name, role or "pm", git_author, repo_root)
|
|
179
186
|
print(f" [OK] 身份注册: member.json + 签名密钥已生成")
|
|
@@ -182,7 +189,7 @@ def init_developer(name: str, role: str | None = None,
|
|
|
182
189
|
except ImportError:
|
|
183
190
|
pass # identity 模块不可用, 跳过 (向后兼容)
|
|
184
191
|
|
|
185
|
-
# 5. 确保 git 配置 (
|
|
192
|
+
# 5. 确保 git 配置 (只检测+提示, 不造假; 真账号由 setup.py 在 init 前配好)
|
|
186
193
|
_ensure_git_config(name, repo_root)
|
|
187
194
|
|
|
188
195
|
print(f"Developer initialized: {name}" + (f" ({role})" if role else ""))
|
|
@@ -185,7 +185,12 @@ def register_member(
|
|
|
185
185
|
"name": name,
|
|
186
186
|
"member_id": member_id,
|
|
187
187
|
"role": role,
|
|
188
|
-
|
|
188
|
+
# existing 可能为 None (全新安装, member.json 尚不存在), 判空防 NPE
|
|
189
|
+
"git_author": (
|
|
190
|
+
git_author
|
|
191
|
+
or (existing.get("git_author") if existing else None)
|
|
192
|
+
or "%s <unknown@local>" % name
|
|
193
|
+
),
|
|
189
194
|
"joined_at": joined_at,
|
|
190
195
|
}
|
|
191
196
|
atomic_write_json(mj, record)
|
|
@@ -162,8 +162,14 @@ def score_style(html_text):
|
|
|
162
162
|
# ============================================================
|
|
163
163
|
|
|
164
164
|
def score_template(prd_text):
|
|
165
|
-
|
|
166
|
-
|
|
165
|
+
# 优先看 mode 标记 (HTML 注释, 头部 2KB 内); 无标记回退到内容自识别 (旧 PRD 兼容)
|
|
166
|
+
# 注意: 去掉了旧的 len<3000 启发式 —— 它会把短的 full PRD 误判为 quick
|
|
167
|
+
_m = re.search(r'<!--\s*mode:\s*(\w+)\s*-->', prd_text[:2048], re.IGNORECASE)
|
|
168
|
+
if _m:
|
|
169
|
+
is_quick = _m.group(1).lower() == 'quick'
|
|
170
|
+
else:
|
|
171
|
+
_low = prd_text.lower()
|
|
172
|
+
is_quick = ('mini-prd' in _low or '零星需求' in prd_text)
|
|
167
173
|
sections = QUICK_SECTIONS if is_quick else FULL_SECTIONS
|
|
168
174
|
kind = '零星模板' if is_quick else '完整模板'
|
|
169
175
|
missing = [s for s in sections if s not in prd_text]
|
|
@@ -133,7 +133,7 @@ def check_developer(fix, name=None, role=None):
|
|
|
133
133
|
current = get_developer()
|
|
134
134
|
if not current and not name:
|
|
135
135
|
say(WARN, '未设置开发者身份, 也探测不到 git user.name')
|
|
136
|
-
print(' -> 建议:
|
|
136
|
+
print(' -> 建议: wlkj init <你的名字>')
|
|
137
137
|
return None
|
|
138
138
|
if name and (not current or current != name):
|
|
139
139
|
if fix:
|
|
@@ -159,6 +159,19 @@ def check_developer(fix, name=None, role=None):
|
|
|
159
159
|
actions.append('补建个人空间子目录: ' + ', '.join(missing))
|
|
160
160
|
else:
|
|
161
161
|
problem('个人空间缺少子目录: ' + ', '.join(missing), '加 --fix 自动补建')
|
|
162
|
+
# member.json 假账号检测 (旧版 init 可能塞了 @local 假账号, push 门禁会拒绝)
|
|
163
|
+
import json as _json_mj
|
|
164
|
+
mj_path = os.path.join(BASE, 'workspace', 'members', current, 'member.json')
|
|
165
|
+
if os.path.isfile(mj_path):
|
|
166
|
+
try:
|
|
167
|
+
with open(mj_path, encoding='utf-8') as _f:
|
|
168
|
+
_mdata = _json_mj.load(_f)
|
|
169
|
+
_ga = _mdata.get('git_author', '')
|
|
170
|
+
if _ga and '@local' in _ga:
|
|
171
|
+
say(WARN, 'member.json 的 git_author 是假账号: {} (push 门禁会拒绝)'.format(_ga))
|
|
172
|
+
print(' -> 建议重跑 \'wlkj init\' 配置真实团队 git 账号, 会自动修正 member.json')
|
|
173
|
+
except Exception:
|
|
174
|
+
pass
|
|
162
175
|
# git author 一致性 (影响 /wl-report 统计 + 零信任 push 门禁)
|
|
163
176
|
# git 未安装时跳过此项
|
|
164
177
|
git_name = ''
|
|
@@ -96,7 +96,7 @@ def check_env():
|
|
|
96
96
|
ok('git 已自动安装')
|
|
97
97
|
else:
|
|
98
98
|
warn('git 未安装 — 团队同步/源码克隆将禁用, 本地 PRD/搜索/任务/报告仍可用')
|
|
99
|
-
print(' (想启用团队协作: 安装 git 后重跑
|
|
99
|
+
print(' (想启用团队协作: 安装 git 后重跑 \'wlkj init\')')
|
|
100
100
|
return True # 不阻塞, 继续
|
|
101
101
|
ok('git 可用')
|
|
102
102
|
return True
|
|
@@ -275,7 +275,189 @@ def guide_config():
|
|
|
275
275
|
warn('config.yaml 的 git_sync.projects 未填真实 URL')
|
|
276
276
|
print(' 打开 .qoder/config.yaml, 找到 git_sync.projects 段,')
|
|
277
277
|
print(' 把 url 改成你团队的 git 地址。或手动把代码放到 data/code/。')
|
|
278
|
-
print(' 填完后重跑
|
|
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])
|
|
279
461
|
|
|
280
462
|
|
|
281
463
|
# ============================================================
|
|
@@ -430,6 +612,9 @@ def main():
|
|
|
430
612
|
fail('未能确定开发者名字')
|
|
431
613
|
return 1
|
|
432
614
|
|
|
615
|
+
# Step 1.5: Git 身份配置 (在 init_doctor 之前, 确保下游探测到真账号而非假账号)
|
|
616
|
+
configure_git_identity(name)
|
|
617
|
+
|
|
433
618
|
# Step 2
|
|
434
619
|
if not run_doctor(name, args.role):
|
|
435
620
|
warn('init_doctor 未完全成功, 继续 setup 后续步骤...')
|
|
@@ -437,6 +622,9 @@ def main():
|
|
|
437
622
|
# Step 3
|
|
438
623
|
guide_config()
|
|
439
624
|
|
|
625
|
+
# Step 3.5: 团队协作仓库 remote (workspace/ 产出 push 到这里)
|
|
626
|
+
configure_team_remote()
|
|
627
|
+
|
|
440
628
|
# Step 4
|
|
441
629
|
offer_cron(skip=args.skip_cron)
|
|
442
630
|
|
|
@@ -280,8 +280,9 @@ def check_eval_gate(
|
|
|
280
280
|
try:
|
|
281
281
|
with open(full, 'r', encoding='utf-8', errors='replace') as f:
|
|
282
282
|
head = f.read(2048)
|
|
283
|
-
|
|
284
|
-
|
|
283
|
+
head_lower = head.lower()
|
|
284
|
+
if 'mode: quick' in head_lower or 'mini-prd' in head_lower or '零星需求' in head:
|
|
285
|
+
continue # Quick 模式, 跳过 EVA (mode 标记优先, 兼容旧 Mini-PRD 字样)
|
|
285
286
|
except OSError:
|
|
286
287
|
pass
|
|
287
288
|
# 找同名原型 (可选)
|
|
@@ -88,10 +88,13 @@ python .qoder/scripts/context_pack.py <关键词> --platform <web|app>
|
|
|
88
88
|
|
|
89
89
|
## PRD 模板
|
|
90
90
|
|
|
91
|
-
| 模式 | 模板 |
|
|
92
|
-
|
|
93
|
-
| Reference/Brainstorm/Planning | `.qoder/templates/prd-full-template.md` |
|
|
94
|
-
| Quick | `.qoder/templates/prd-quick-template.md` |
|
|
91
|
+
| 模式 | 模板 | mode 标记 |
|
|
92
|
+
|------|------|----------|
|
|
93
|
+
| Reference/Brainstorm/Planning | `.qoder/templates/prd-full-template.md` | `<!-- mode: reference/brainstorm/planning -->` |
|
|
94
|
+
| Quick | `.qoder/templates/prd-quick-template.md` | `<!-- mode: quick -->` |
|
|
95
|
+
|
|
96
|
+
> 模板头部已含 mode 标记。生成 PRD 时**只改 mode 值,别删标记**——
|
|
97
|
+
> syncgate/eval_prd 靠它识别模式(Quick 跳过 EVA,其他全跑)。
|
|
95
98
|
|
|
96
99
|
## 质量自检(10 项,Quick 跳过)
|
|
97
100
|
1. 背景回答"为什么" 2. 目标可量化 3. 用户画像具体
|
|
@@ -49,6 +49,9 @@ trigger: "用户描述需求、要 PRD、要原型、要 mockup,或直接 /wl-
|
|
|
49
49
|
|
|
50
50
|
> **深度模式** = 先做现状+Gap+多角度分析,再出 PRD。适合大需求/新领域/想创新。
|
|
51
51
|
> **探讨模式** = 聊完说"出PRD"→ 自动转默认生成。
|
|
52
|
+
>
|
|
53
|
+
> **mode 标记**:生成的 PRD 第一行写 `<!-- mode: quick/brainstorm/planning/reference -->`,
|
|
54
|
+
> syncgate/eval_prd 靠它识别模式(Quick 跳过 EVA)。模板已带默认值,只改值别删。
|
|
52
55
|
|
|
53
56
|
## 完整流程(默认/深度/快速 模式)
|
|
54
57
|
|