@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.
Files changed (127) hide show
  1. package/bin/cli.js +289 -12
  2. package/package.json +1 -1
  3. package/templates/qoder/agents/insight-planning.md +67 -0
  4. package/templates/qoder/agents/insight-research.md +61 -0
  5. package/templates/qoder/agents/prd-quick.md +1 -0
  6. package/templates/qoder/agents/prd-reference.md +10 -2
  7. package/templates/qoder/commands/optional/wl-insight.md +275 -0
  8. package/templates/qoder/commands/{wl-report.md → optional/wl-report.md} +13 -5
  9. package/templates/qoder/commands/{wl-spec.md → optional/wl-spec.md} +1 -1
  10. package/templates/qoder/commands/{wl-status.md → optional/wl-status.md} +28 -2
  11. package/templates/qoder/commands/wl-code.md +10 -2
  12. package/templates/qoder/commands/wl-commit.md +1 -1
  13. package/templates/qoder/commands/wl-design-draw.md +78 -0
  14. package/templates/qoder/commands/wl-design-scan.md +108 -0
  15. package/templates/qoder/commands/wl-design-spec.md +154 -0
  16. package/templates/qoder/commands/wl-design.md +32 -0
  17. package/templates/qoder/commands/wl-init.md +24 -3
  18. package/templates/qoder/commands/wl-prd-full.md +226 -0
  19. package/templates/qoder/commands/wl-prd-quick.md +134 -0
  20. package/templates/qoder/commands/wl-prd-review.md +104 -0
  21. package/templates/qoder/commands/wl-prd.md +17 -288
  22. package/templates/qoder/commands/wl-search.md +66 -30
  23. package/templates/qoder/commands/wl-task.md +290 -59
  24. package/templates/qoder/commands/wl-test.md +92 -24
  25. package/templates/qoder/config.yaml +59 -15
  26. package/templates/qoder/hooks/inject-workflow-state.py +35 -9
  27. package/templates/qoder/hooks/session-start.py +144 -62
  28. package/templates/qoder/rules/wl-pipeline.md +216 -105
  29. package/templates/qoder/scripts/__pycache__/search_index.cpython-39.pyc +0 -0
  30. package/templates/qoder/scripts/archive_prd.py +377 -0
  31. package/templates/qoder/scripts/autotest.py +1715 -0
  32. package/templates/qoder/scripts/autotest_batch.py +224 -0
  33. package/templates/qoder/scripts/autotest_run.py +297 -0
  34. package/templates/qoder/scripts/benchmark.py +210 -209
  35. package/templates/qoder/scripts/build_style_index.py +444 -4
  36. package/templates/qoder/scripts/check_carriers.py +238 -0
  37. package/templates/qoder/scripts/check_mcp.py +298 -0
  38. package/templates/qoder/scripts/check_qoderwork_consistency.py +166 -0
  39. package/templates/qoder/scripts/common/developer.py +26 -19
  40. package/templates/qoder/scripts/common/events.py +46 -0
  41. package/templates/qoder/scripts/common/extract.py +419 -0
  42. package/templates/qoder/scripts/common/graph_traverse.py +533 -0
  43. package/templates/qoder/scripts/common/identity.py +6 -1
  44. package/templates/qoder/scripts/common/paths.py +89 -0
  45. package/templates/qoder/scripts/common/pip_install.py +144 -0
  46. package/templates/qoder/scripts/common/platform_guard.py +61 -0
  47. package/templates/qoder/scripts/common/search_engine.py +205 -205
  48. package/templates/qoder/scripts/common/terms.py +57 -0
  49. package/templates/qoder/scripts/common/ts_extract.py +536 -0
  50. package/templates/qoder/scripts/context_pack.py +73 -13
  51. package/templates/qoder/scripts/enrich_prompt.py +226 -0
  52. package/templates/qoder/scripts/eval_prd.py +318 -225
  53. package/templates/qoder/scripts/export.py +487 -487
  54. package/templates/qoder/scripts/extract_api_params.py +246 -0
  55. package/templates/qoder/scripts/extract_routes.py +54 -0
  56. package/templates/qoder/scripts/extract_routes_tree.py +78 -0
  57. package/templates/qoder/scripts/fill_prototype.py +707 -0
  58. package/templates/qoder/scripts/gen_design_doc.py +394 -0
  59. package/templates/qoder/scripts/git_sync.py +27 -15
  60. package/templates/qoder/scripts/init_doctor.py +306 -41
  61. package/templates/qoder/scripts/install_qoderwork.py +366 -9
  62. package/templates/qoder/scripts/kg.py +708 -0
  63. package/templates/qoder/scripts/kg_auto_login.py +196 -0
  64. package/templates/qoder/scripts/kg_build.py +612 -0
  65. package/templates/qoder/scripts/kg_build_db.py +327 -0
  66. package/templates/qoder/scripts/kg_duckdb.py +549 -0
  67. package/templates/qoder/scripts/kg_incremental.py +393 -0
  68. package/templates/qoder/scripts/kg_link_db.py +224 -0
  69. package/templates/qoder/scripts/kg_mcp_server.py +801 -0
  70. package/templates/qoder/scripts/kg_semantic.py +150 -0
  71. package/templates/qoder/scripts/kg_test_runner.py +241 -0
  72. package/templates/qoder/scripts/lanhu_stdio_wrapper.py +119 -0
  73. package/templates/qoder/scripts/learn.py +118 -39
  74. package/templates/qoder/scripts/learn_aggregate.py +201 -0
  75. package/templates/qoder/scripts/mcp_launcher.py +359 -0
  76. package/templates/qoder/scripts/mysql_mcp_server.py +396 -0
  77. package/templates/qoder/scripts/repo_root.py +106 -0
  78. package/templates/qoder/scripts/role.py +12 -0
  79. package/templates/qoder/scripts/run_weekly_update.bat +5 -0
  80. package/templates/qoder/scripts/run_weekly_update.sh +5 -0
  81. package/templates/qoder/scripts/search_index.py +307 -60
  82. package/templates/qoder/scripts/secure-ls.js +5640 -0
  83. package/templates/qoder/scripts/setup.py +706 -453
  84. package/templates/qoder/scripts/setup_lanhu.py +963 -0
  85. package/templates/qoder/scripts/status.py +250 -11
  86. package/templates/qoder/scripts/sync_carriers.py +259 -0
  87. package/templates/qoder/scripts/syncgate.py +5 -4
  88. package/templates/qoder/scripts/task.py +75 -0
  89. package/templates/qoder/scripts/team_sync.py +60 -4
  90. package/templates/qoder/scripts/workspace_init.py +1 -1
  91. package/templates/qoder/skills/design-import/SKILL.md +226 -0
  92. package/templates/qoder/skills/design-import/figma-workflow.md +81 -0
  93. package/templates/qoder/skills/design-review/SKILL.md +82 -25
  94. package/templates/qoder/skills/prd-generator/SKILL.md +185 -58
  95. package/templates/qoder/skills/prd-review/SKILL.md +18 -1
  96. package/templates/qoder/skills/prompt-enrich/SKILL.md +90 -0
  97. package/templates/qoder/skills/prototype-generator/SKILL.md +256 -141
  98. package/templates/qoder/skills/prototype-generator/SKILL.md.zcode-79180-2af4721f-f9a6-412c-88db-c0af680d211b.tmp +0 -0
  99. package/templates/qoder/skills/spec-coder/SKILL.md +18 -1
  100. package/templates/qoder/skills/spec-generator/SKILL.md +18 -1
  101. package/templates/qoder/skills/test-generator/SKILL.md +15 -2
  102. package/templates/qoder/skills/wl-code/SKILL.md +55 -36
  103. package/templates/qoder/skills/wl-commit/SKILL.md +89 -76
  104. package/templates/qoder/skills/wl-design/SKILL.md +55 -0
  105. package/templates/qoder/skills/wl-init/SKILL.md +76 -67
  106. package/templates/qoder/skills/wl-insight/SKILL.md +201 -81
  107. package/templates/qoder/skills/wl-prd-full/SKILL.md +69 -0
  108. package/templates/qoder/skills/wl-prd-quick/SKILL.md +49 -0
  109. package/templates/qoder/skills/wl-prd-review/SKILL.md +34 -0
  110. package/templates/qoder/skills/wl-report/SKILL.md +131 -107
  111. package/templates/qoder/skills/wl-search/SKILL.md +141 -75
  112. package/templates/qoder/skills/wl-spec/SKILL.md +49 -39
  113. package/templates/qoder/skills/wl-status/SKILL.md +83 -61
  114. package/templates/qoder/skills/wl-task/SKILL.md +132 -58
  115. package/templates/qoder/skills/wl-test/SKILL.md +406 -40
  116. package/templates/qoder/templates/prd-full-template.md +2 -0
  117. package/templates/qoder/templates/prd-quick-template.md +1 -0
  118. package/templates/qoder/templates/prototype-app.html +13 -8
  119. package/templates/qoder/templates/prototype-web.html +376 -93
  120. package/templates/root/AGENTS.md +89 -34
  121. package/templates/root/requirements.txt +21 -0
  122. package/templates/root//344/275/277/347/224/250/350/257/264/346/230/216.md +259 -259
  123. package/templates/root//346/226/260/346/211/213/346/214/207/345/215/227.md +186 -186
  124. package/templates/qoder/agents/prd-planning.md +0 -56
  125. package/templates/qoder/agents/prd-research.md +0 -33
  126. package/templates/qoder/commands/wl-insight.md +0 -51
  127. package/templates/qoder/skills/wl-prd/SKILL.md +0 -89
@@ -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')
@@ -88,8 +113,8 @@ def get_status():
88
113
 
89
114
  # Statuses written by task.py: planning -> in_progress -> completed
90
115
  GUIDE = {
91
- None: 'No active task. Use /wl-prd to start, or /wl-task create.',
92
- 'planning': 'Task in planning. Use /wl-prd to write the PRD, then /wl-task start.',
116
+ None: 'No active task. Use /wl-prd-full to start, or /wl-task create.',
117
+ 'planning': 'Task in planning. Use /wl-prd-full to write the PRD, then /wl-task start.',
93
118
  'in_progress': 'Dev in progress. /wl-spec to generate spec, /wl-code to implement.',
94
119
  'completed': 'Task completed. /wl-task archive to archive, or start the next one.',
95
120
  }
@@ -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,10 +10,62 @@ 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',
16
- 'When user asks for /wl-prd, ALWAYS ask platform FIRST:',
68
+ 'When user asks for /wl-prd-full, ALWAYS ask platform FIRST:',
17
69
  '"这个需求是针对哪个平台?',
18
70
  ' 1. Web 管理端 (fywl-ui) - Ant Design Vue + VxeGrid 风格',
19
71
  ' 2. APP 移动端 (Carmg-H5) - Vant 风格',
@@ -90,63 +142,94 @@ def get_index_info():
90
142
  except OSError:
91
143
  pass
92
144
 
93
- lines.append('## Knowledge Index')
94
- lines.append('Use search script (DO NOT read large JSON files directly):')
145
+ lines.append('## Knowledge Graph')
146
+ lines.append('MCP工具 (QoderWork自动调用): search_code / get_impact / coverage_matrix / get_workflow 等11个')
95
147
  lines.append(' python .qoder/scripts/search_index.py <keyword> [--platform web|app]')
96
- lines.append(' python .qoder/scripts/search_index.py --prd <keyword> # search PRDs')
97
- lines.append(' python .qoder/scripts/search_index.py --style <table|form> [--platform p]')
98
- lines.append(' python .qoder/scripts/search_index.py --field <name> # field usage')
99
- lines.append(' python .qoder/scripts/search_index.py --api <keyword> # API endpoints')
100
- lines.append(' python .qoder/scripts/search_index.py --components # UI components')
101
- lines.append(' python .qoder/scripts/search_index.py --modules # project overview')
102
- lines.append(' python .qoder/scripts/search_index.py --list # top keywords')
103
-
104
- meta = load_json_safe(os.path.join(BASE, 'data', 'index', '.index-meta.json'))
105
- if meta:
106
- projects = meta.get('projects', {})
107
- if isinstance(projects, dict):
108
- for proj, info in projects.items():
109
- if isinstance(info, dict):
110
- files = info.get('files', 0)
111
- else:
112
- files = info # new meta format: {project: file_count}
113
- lines.append(' ' + str(proj) + ': ' + str(files) + ' files')
114
- elif isinstance(projects, list):
115
- for proj in projects:
116
- lines.append(' ' + str(proj))
117
- lines.append('Last sync: ' + str(meta.get('last_sync', 'unknown')))
118
- # Staleness warning (审计发现: cron 从未注册, 索引可能静默过期)
119
- last_sync = meta.get('last_sync')
120
- if last_sync:
121
- try:
122
- from datetime import datetime
123
- # 兼容多种格式: "2026-06-12 16:49" / ISO
124
- ts = str(last_sync).replace('T', ' ').split('.')[0].strip()
125
- for fmt in ('%Y-%m-%d %H:%M', '%Y-%m-%d %H:%M:%S', '%Y-%m-%dT%H:%M:%S'):
126
- try:
127
- sync_dt = datetime.strptime(ts, fmt)
128
- break
129
- except ValueError:
130
- continue
131
- else:
132
- sync_dt = None
133
- if sync_dt:
134
- age_days = (datetime.now() - sync_dt).days
135
- if age_days > 30:
136
- lines.append('[CRITICAL] 索引已 {} 天未更新! 搜索结果严重过期. 跑: python .qoder/scripts/git_sync.py'.format(age_days))
137
- elif age_days > 7:
138
- lines.append('[WARN] 索引已 {} 天未更新, 建议刷新: python .qoder/scripts/git_sync.py'.format(age_days))
139
- except Exception:
140
- pass # 日期解析失败不阻塞
141
- if meta.get('failures'):
142
- lines.append('WARNING: last sync had errors - see data/index/.index-meta.json')
148
+ lines.append(' python .qoder/scripts/context_pack.py <keyword> # 一次打包全部上下文')
149
+
150
+ # DuckDB 知识图谱状态检查
151
+ # Read role from .developer file
152
+ role = 'pm'
153
+ try:
154
+ dev_file = os.path.join(BASE, '.qoder', '.developer')
155
+ if os.path.isfile(dev_file):
156
+ for _line in open(dev_file, encoding='utf-8'):
157
+ if _line.strip().startswith('role='):
158
+ role = _line.split('=', 1)[1].strip()
159
+ except Exception:
160
+ pass
161
+ db_path = os.path.join(BASE, 'data', 'index', 'kg.duckdb')
162
+
163
+ if os.path.isfile(db_path):
164
+ import time as _time
165
+ db_mtime = os.path.getmtime(db_path)
166
+ db_age_hours = (_time.time() - db_mtime) / 3600
167
+
168
+ # 检查代码是否比 DuckDB 新 (采样几个文件)
169
+ code_dir = os.path.join(BASE, 'data', 'code')
170
+ code_newer = False
171
+ if os.path.isdir(code_dir):
172
+ for root, dirs, files in os.walk(code_dir):
173
+ dirs[:] = [d for d in dirs if d not in ('node_modules', '.git', 'dist', '__pycache__')]
174
+ depth = root[len(code_dir):].count(os.sep)
175
+ if depth >= 3:
176
+ dirs[:] = []
177
+ continue
178
+ for fn in files:
179
+ if fn.endswith(('.vue', '.java', '.ts')):
180
+ try:
181
+ if os.path.getmtime(os.path.join(root, fn)) > db_mtime:
182
+ code_newer = True
183
+ break
184
+ except OSError:
185
+ pass
186
+ if code_newer:
187
+ break
188
+
189
+ lines.append(' kg.duckdb: %.0f小时前构建' % db_age_hours)
190
+
191
+ if role in ('admin', 'dev'):
192
+ # 管理员/开发: 能构建
193
+ if code_newer or db_age_hours > 168: # 7天 = 168小时
194
+ lines.append(' [WARN] 知识图谱过期! 代码有新变更.')
195
+ lines.append(' → 管理员请运行: python .qoder/scripts/kg_build.py')
196
+ lines.append(' → 完成后: python .qoder/scripts/team_sync.py push (共享给团队)')
197
+ else:
198
+ lines.append(' ✓ 知识图谱最新')
199
+ else:
200
+ # 非管理员: 只拉取
201
+ if code_newer or db_age_hours > 168:
202
+ lines.append(' [WARN] 知识图谱可能过期.')
203
+ lines.append(' → 运行: python .qoder/scripts/team_sync.py pull (获取管理员最新构建)')
204
+ else:
205
+ lines.append(' ✓ 知识图谱最新')
206
+
207
+ # 检查是否有未标注的模块 (提示AI做语义标注)
208
+ try:
209
+ import subprocess as _sp
210
+ r = _sp.run([sys.executable, os.path.join(BASE, '.qoder', 'scripts', 'kg_semantic.py'), '--check'],
211
+ capture_output=True, text=True, encoding='utf-8', timeout=5)
212
+ if r.stdout and 'semantic_labeling_needed' in r.stdout:
213
+ import json as _json
214
+ data = _json.loads(r.stdout)
215
+ unlabeled = len(data.get('modules', []))
216
+ if unlabeled > 0:
217
+ lines.append(' [提示] %d个模块未标注中文名, 运行 kg_semantic.py --check 查看' % unlabeled)
218
+ except Exception:
219
+ pass
220
+ else:
221
+ lines.append(' [MISSING] kg.duckdb 不存在!')
222
+ if role in ('admin', 'dev'):
223
+ lines.append(' → 运行: python .qoder/scripts/kg_build.py')
224
+ else:
225
+ lines.append(' → 运行: python .qoder/scripts/team_sync.py pull')
143
226
 
144
227
  return NL.join(lines)
145
228
 
146
229
 
147
230
  def get_style_info():
148
231
  """Read style metadata for quick reference"""
149
- meta = load_json_safe(os.path.join(BASE, 'data', 'index', 'style-meta.json'))
232
+ meta = load_json_safe(os.path.join(BASE, 'data', 'index', 'ui-style-meta.json'))
150
233
  if not meta:
151
234
  return None
152
235
 
@@ -174,6 +257,14 @@ def get_style_info():
174
257
  def main():
175
258
  parts = []
176
259
  parts.append('<qoder-context>')
260
+ # Hook-3: 上次会话以来的 hook 错误告警 (不只落盘, 要让用户/AI 看到)
261
+ # hook 坏了会导致上下文注入失败 (任务状态/平台规则缺失), 必须可见
262
+ recent_errors = _recent_hook_errors(hours=24, limit=3)
263
+ if recent_errors:
264
+ parts.append('⚠️ [HOOK-WARN] 过去 24h 有 hook 错误 (上下文注入可能失效):')
265
+ for err in recent_errors:
266
+ parts.append(' ' + err)
267
+ parts.append(' 详情见 .qoder/.runtime/hook-errors.log; 检查 Python 环境或跑 init_doctor --fix')
177
268
  dev = get_developer()
178
269
  if dev:
179
270
  name = dev.get('name', 'unknown')
@@ -216,17 +307,8 @@ if __name__ == '__main__':
216
307
  main()
217
308
  except Exception as e:
218
309
  # 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 # 日志写失败不影响降级
310
+ # 同时落盘到日志 (带轮转), 方便下次会话告警
311
+ _append_hook_error(HOOK_ERRORS_LOG, str(e)[:200])
230
312
  print('<qoder-context>')
231
313
  print('session-start hook error: ' + str(e)[:200])
232
314
  print(' (详情见 .qoder/.runtime/hook-errors.log)')