@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
|
@@ -36,8 +36,8 @@ This single command does ALL of:
|
|
|
36
36
|
6. 检查周五自动构建状态, 给出本系统的设置命令
|
|
37
37
|
|
|
38
38
|
After doctor finishes:
|
|
39
|
-
-
|
|
40
|
-
-
|
|
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
|
-
|
|
227
|
-
|
|
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
|
-
|
|
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
|
-
|
|
309
|
-
|
|
310
|
-
|
|
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
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
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 请按以下步骤解决冲突:')
|