@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 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(` 可手动重跑: python .qoder/scripts/setup.py`);
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hupan56/wlkj",
3
- "version": "2.5.0",
3
+ "version": "2.6.0",
4
4
  "description": "AI Product R&D Workflow - PRD/Prototype/Search/Task/Report",
5
5
  "bin": {
6
6
  "wlkj": "bin/cli.js"
@@ -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 靠此识别模式).
@@ -36,6 +36,7 @@ python .qoder/scripts/context_pack.py <关键词> --platform <web|app>
36
36
 
37
37
  ## Mini-PRD 极简格式
38
38
  ```markdown
39
+ <!-- mode: quick -->
39
40
  # Mini-PRD: {标题}
40
41
 
41
42
  REQ-ID: {分配的ID}
@@ -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
- ### Quick 模式的特殊处理
84
+ ### 全模式标记规则(生成 PRD 时必须遵守)
85
85
 
86
- Quick 模式生成的 PRD **头部必须标注 `mode: quick`**:
86
+ **每种模式生成的 PRD,第一行必须是 `<!-- mode: <模式名> -->` 标记**:
87
87
  ```markdown
88
- <!-- mode: quick -->
89
- # Mini-PRD: {标题}
88
+ <!-- mode: reference --> (或 brainstorm / planning / quick)
89
+ # 标题...
90
90
  ```
91
- syncgate 的 EVA 门禁检查到 `mode: quick` 时**跳过 A3(字段引用检查)**,
92
- 因为 Mini-PRD 格式精简,A3 的表格字段检查不适用。
93
- 其他检查(平台/REQ-ID/秘密)仍然生效。
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
- - 若用户意图跟 ≥2 个模式都沾边(如既像"分析"又像"规划")→ **先问用户**:
112
- "这个需求我理解为 [X 模式],对吗?还是你想要 [Y/Z]?"
113
- - 默认走 Reference 仅当用户明确给出需求描述且无探讨/创新/规划信号时。
114
- - **宁可多问一句,不要错模式**(错模式的 PRD 是废纸,EVA 门禁只管格式不管方向)。
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
- return # 状态没变, 不重复注入 (省 token)
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
- from datetime import datetime
133
- log_path = Path(__file__).resolve().parents[1] / '.runtime' / 'hook-errors.log'
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
- # 同时落盘到日志, 方便后续排查 (stdout 一次性, AI 看完就没了)
220
- try:
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)')
@@ -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
- """确保 git user.name/email 已配置。未配则用 --local 设为 init 的名字。
76
+ """确认 git user.name/email 已配置。未配则提示 (绝不自动造假账号)。
77
77
 
78
- 检测顺序: 全局 > local。都没有就提示用户。
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
- print(f" [OK] git 作者: {author}")
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 未配 user.name/email: 用 init 名字设 local config (不影响全局)
94
- print(f" [设置] git 未配置 user.name/email, 用你的名字设为 local:")
95
- fake_email = f"{name}@local"
96
- try:
97
- subprocess.run(["git", "config", "--local", "user.name", name],
98
- cwd=str(repo_root), capture_output=True, timeout=5)
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 配置 (检测 + 自动设 local)
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
- "git_author": git_author or existing.get("git_author") or "%s <unknown@local>" % name,
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
- is_quick = ('Mini-PRD' in prd_text or '零星需求' in prd_text
166
- or len(prd_text) < 3000)
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(' -> 建议: python .qoder/scripts/setup.py <你的名字>')
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 后重跑 setup.py)')
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(' 填完后重跑 setup.py 即可自动克隆+建索引。')
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
- if 'mode: quick' in head.lower():
284
- continue # Quick 模式, 跳过 EVA
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
 
@@ -7,6 +7,8 @@
7
7
 
8
8
  ---
9
9
 
10
+ <!-- mode: reference -->
11
+ <!-- 生成时按实际模式替换上面的值: reference / brainstorm / planning -->
10
12
  # REQ-{YYYY}-{NNN} {需求标题}
11
13
 
12
14
  ## 前言
@@ -6,6 +6,7 @@
6
6
 
7
7
  ---
8
8
 
9
+ <!-- mode: quick -->
9
10
  # {功能模块} - {改动摘要}
10
11
 
11
12
  > 标题让读者一眼知道改了什么,避免"XX功能修改"这类模糊表述。