@hupan56/wlkj 2.4.0 → 2.4.1

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hupan56/wlkj",
3
- "version": "2.4.0",
3
+ "version": "2.4.1",
4
4
  "description": "AI Product R&D Workflow - PRD/Prototype/Search/Task/Report",
5
5
  "bin": {
6
6
  "wlkj": "bin/cli.js"
@@ -36,8 +36,8 @@ This single command does ALL of:
36
36
  6. 检查周五自动构建状态, 给出本系统的设置命令
37
37
 
38
38
  After doctor finishes:
39
- - Create member.json with role if not exists (workspace/members/{name}/member.json)
40
- - Run team.py add if it's a new member
39
+ - init_developer auto-registers identity (member.json + signing key)
40
+ - No separate "team.py add" needed anymore
41
41
 
42
42
  ## Incremental Guarantee (为什么不会重头再来)
43
43
 
@@ -115,6 +115,12 @@ def _save_cache(key, content):
115
115
  with open(tmp, 'w', encoding='utf-8') as f:
116
116
  f.write(content)
117
117
  os.replace(tmp, cache_path)
118
+ # LRU 清理: 保留最新 30 个, 删更老的
119
+ try:
120
+ from search_index import _cleanup_cache_dir
121
+ _cleanup_cache_dir(cache_dir, 'ctx-cache-', max_files=30)
122
+ except Exception:
123
+ pass
118
124
  except OSError:
119
125
  pass
120
126
 
@@ -185,6 +185,32 @@ def search_keywords(query, platform=None):
185
185
  with open(tmp, 'w', encoding='utf-8') as f:
186
186
  f.write(output)
187
187
  os.replace(tmp, cache_path)
188
+ # LRU 清理: 保留最新 50 个缓存文件, 删更老的
189
+ _cleanup_cache_dir(cache_dir, 'search-cache-', max_files=50)
190
+ except OSError:
191
+ pass
192
+
193
+
194
+ def _cleanup_cache_dir(cache_dir, prefix, max_files=50):
195
+ """清理缓存目录, 只保留最新 max_files 个匹配 prefix 的文件。"""
196
+ try:
197
+ files = [f for f in os.listdir(cache_dir) if f.startswith(prefix)]
198
+ if len(files) <= max_files:
199
+ return
200
+ # 按 mtime 降序排, 删掉超出部分的
201
+ files_with_mtime = []
202
+ for f in files:
203
+ p = os.path.join(cache_dir, f)
204
+ try:
205
+ files_with_mtime.append((os.path.getmtime(p), p))
206
+ except OSError:
207
+ continue
208
+ files_with_mtime.sort(reverse=True)
209
+ for _, p in files_with_mtime[max_files:]:
210
+ try:
211
+ os.remove(p)
212
+ except OSError:
213
+ pass
188
214
  except OSError:
189
215
  pass
190
216
 
@@ -208,6 +208,50 @@ def scan_secrets(file_paths: List[str], repo_root: str) -> None:
208
208
  raise SecretFound("\n".join(lines))
209
209
 
210
210
 
211
+ # ============================================================
212
+ # 3.6 平台标注门禁 (PRD 必须标注目标平台, 防 prompt 失效)
213
+ # ============================================================
214
+
215
+ # 合法平台关键词 (出现在 PRD 内容里即视为已标注)
216
+ _PLATFORM_KEYWORDS = [
217
+ 'web', 'pc', '管理端', 'fywl-ui', 'fywl_ui', 'vben', 'ant design',
218
+ 'app', 'h5', '移动端', 'carmg', 'vant',
219
+ '两端', '双端', 'both',
220
+ '后端', 'backend', '无前端',
221
+ ]
222
+
223
+ def check_platform_gate(prd_paths: List[str], repo_root: str) -> None:
224
+ """检查 PRD 文件是否标注了目标平台。未标注抛 GateFailure。
225
+
226
+ 这是 /wl-prd "先问平台" 的程序级兜底:
227
+ 如果 AI 没问平台就生成了 PRD, push 时这里会拦住。
228
+ """
229
+ missing = []
230
+ for prd in prd_paths:
231
+ try:
232
+ # 只读前 4KB (头部元数据区通常在这里)
233
+ with open(prd, 'r', encoding='utf-8', errors='replace') as f:
234
+ head = f.read(4096)
235
+ except OSError:
236
+ continue # 文件读不了, 跳过 (不因 IO 错误阻塞)
237
+ head_lower = head.lower()
238
+ # 检查是否包含任一平台关键词
239
+ found = any(kw in head_lower for kw in _PLATFORM_KEYWORDS)
240
+ if not found:
241
+ missing.append(os.path.basename(prd))
242
+
243
+ if missing:
244
+ raise GateFailure(
245
+ "[gate] 拒绝: %d 个 PRD 未标注目标平台 (违反 /wl-prd 先问平台规则):\n"
246
+ " %s\n"
247
+ "修复: 在 PRD 头部加一行:\n"
248
+ " - **平台**: Web管理端 (fywl-ui) # 或 APP移动端 / 两端 / 后端" % (
249
+ len(missing),
250
+ '\n '.join(missing[:5]),
251
+ )
252
+ )
253
+
254
+
211
255
  # ============================================================
212
256
  # 4. EVA 门禁 (PRD 质量校验)
213
257
  # ============================================================
@@ -223,11 +267,8 @@ def check_eval_gate(
223
267
  """
224
268
  if skip_eval or not prd_paths:
225
269
  return
226
- try:
227
- from common.eval_api import evaluate
228
- except ImportError:
229
- # eval_api 还没建 (阶段 1.2 建), 跳过不阻塞
230
- return
270
+ # 硬 import: eval_api 是必需模块,缺失说明引擎损坏,应报错而非静默跳过
271
+ from common.eval_api import evaluate
231
272
 
232
273
  failures = []
233
274
  for prd in prd_paths:
@@ -260,7 +301,7 @@ def check_eval_gate(
260
301
  lines.append(" %s %s" % (prd, reason))
261
302
  issues_all.append("%s: %s" % (prd, reason))
262
303
  lines.append("修复 PRD 后重试, 或 admin 用 --skip-eval 跳过 (会记录审计)")
263
- # D2: 飞书通知 EVA 拒绝
304
+ # D2: 飞书通知 EVA 拒绝 (best-effort, 失败不阻塞门禁)
264
305
  try:
265
306
  from common.feishu import notify_eval_rejected
266
307
  notify_eval_rejected(
@@ -268,8 +309,8 @@ def check_eval_gate(
268
309
  title="批量 EVA 拒绝",
269
310
  issues=issues_all,
270
311
  )
271
- except Exception:
272
- pass
312
+ except Exception as e:
313
+ print('[gate] WARN: 飞书通知失败 (不阻塞): %s' % str(e)[:80], file=sys.stderr)
273
314
  raise GateFailure("\n".join(lines))
274
315
 
275
316
 
@@ -304,20 +345,21 @@ def run_gates(
304
345
  and f.lower().endswith(".md")
305
346
  ]
306
347
  if prd_files:
348
+ # 硬 import: reqid 是必需模块,缺失说明引擎损坏
349
+ from common.reqid import validate_req_ids, migrate_from_existing, ReqIdError
350
+ # 计数器缺失则先迁移 (幂等)
307
351
  try:
308
- from common.reqid import validate_req_ids, migrate_from_existing, ReqIdError
309
- # 计数器缺失则先迁移 (幂等)
310
- try:
352
+ validate_req_ids(prd_files, repo_root)
353
+ except ReqIdError as e:
354
+ if '超过计数器最大值 0' in str(e) or 'req-counter' in str(e).lower():
355
+ # 计数器未初始化, 迁移后重试
356
+ migrate_from_existing(repo_root)
311
357
  validate_req_ids(prd_files, repo_root)
312
- except ReqIdError as e:
313
- if '超过计数器最大值 0' in str(e) or 'req-counter' in str(e).lower():
314
- # 计数器未初始化, 迁移后重试
315
- migrate_from_existing(repo_root)
316
- validate_req_ids(prd_files, repo_root)
317
- else:
318
- raise
319
- except ImportError:
320
- pass # reqid 模块不可用, 跳过 (向后兼容)
358
+ else:
359
+ raise
360
+ # 4.5 平台标注门禁 (PRD 必须标注目标平台, 防 prompt 失效)
361
+ if prd_files:
362
+ check_platform_gate(prd_files, repo_root)
321
363
  # 5. EVA 门禁 (只对 PRD 文件)
322
364
  check_eval_gate(prd_files, repo_root, skip_eval=skip_eval)
323
365
  return True, ""
@@ -94,7 +94,8 @@ def _notify_prd_publications(staged_files):
94
94
  import re as _re
95
95
  try:
96
96
  from common.feishu import notify_prd_published, is_enabled
97
- except ImportError:
97
+ except ImportError as e:
98
+ print('[sync] WARN: feishu 模块缺失, 跳过 PRD 发布通知: %s' % e, file=sys.stderr)
98
99
  return
99
100
  if not is_enabled('prd_published'):
100
101
  return
@@ -141,6 +142,13 @@ def report_conflict(stderr):
141
142
  if r.returncode != 0:
142
143
  print('WARNING: rebase --abort 失败 (rc={}): {}'.format(r.returncode, (r.stderr or '').strip()[:120]))
143
144
  print('仓库可能处于半 rebase 状态。AI 请手动跑: git rebase --abort')
145
+ # autostash 风险检查: --autostash 可能在 abort 后留下未弹回的 stash
146
+ stash_r = git('stash', 'list')
147
+ if stash_r.returncode == 0 and stash_r.stdout.strip():
148
+ stash_count = len([l for l in stash_r.stdout.strip().split('\n') if l.strip()])
149
+ print('WARNING: 检测到 {} 个 stash (可能是 autostash 未弹回)!'.format(stash_count))
150
+ print(' 查看 stash: git stash list')
151
+ print(' 恢复改动: git stash pop (如果包含你的草稿)')
144
152
  print('SYNC_CONFLICT: 自动同步遇到冲突, 已安全回退 (本地产出未丢失)。')
145
153
  branch = current_branch()
146
154
  print('AI 请按以下步骤解决冲突:')