@hupan56/wlkj 3.3.0 → 3.3.2

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 (32) hide show
  1. package/package.json +29 -29
  2. package/templates/.qoder/.runtime/hook-errors.log +4 -0
  3. package/templates/qoder/commands/optional/wl-status.md +2 -0
  4. package/templates/qoder/commands/wl-code.md +45 -10
  5. package/templates/qoder/commands/wl-init.md +129 -129
  6. package/templates/qoder/commands/wl-prd.md +27 -12
  7. package/templates/qoder/commands/wl-search.md +32 -0
  8. package/templates/qoder/commands/wl-task.md +8 -4
  9. package/templates/qoder/commands/wl-test.md +4 -0
  10. package/templates/qoder/hooks/post-tool-use.py +31 -1
  11. package/templates/qoder/hooks/pre-tool-use.py +136 -0
  12. package/templates/qoder/hooks/session-start.py +365 -365
  13. package/templates/qoder/scripts/capability/__pycache__/present_html.cpython-39.pyc +0 -0
  14. package/templates/qoder/scripts/capability/__pycache__/registry.cpython-39.pyc +0 -0
  15. package/templates/qoder/scripts/capability/caps/__init__.py +4 -4
  16. package/templates/qoder/scripts/capability/caps/__pycache__/__init__.cpython-39.pyc +0 -0
  17. package/templates/qoder/scripts/capability/caps/__pycache__/notify.cpython-39.pyc +0 -0
  18. package/templates/qoder/scripts/capability/caps/__pycache__/sandbox.cpython-39.pyc +0 -0
  19. package/templates/qoder/scripts/capability/caps/notify.py +64 -0
  20. package/templates/qoder/scripts/capability/caps/sandbox.py +38 -0
  21. package/templates/qoder/scripts/capability/present_html.py +68 -0
  22. package/templates/qoder/scripts/capability/registry.py +6 -2
  23. package/templates/qoder/scripts/capability/registry_mcp.py +312 -314
  24. package/templates/qoder/scripts/domain/integration/__pycache__/return_to_platform.cpython-39.pyc +0 -0
  25. package/templates/qoder/scripts/domain/integration/return_to_platform.py +394 -392
  26. package/templates/qoder/scripts/domain/integration/spec_upload.py +208 -209
  27. package/templates/qoder/scripts/domain/kg/switch_project.py +158 -159
  28. package/templates/qoder/scripts/engine/poller.py +219 -219
  29. package/templates/qoder/scripts/validation/metrics/__pycache__/present_board.cpython-39.pyc +0 -0
  30. package/templates/qoder/scripts/validation/metrics/present_board.py +180 -0
  31. package/templates/qoder/settings.json +10 -0
  32. package/templates/qoder/scripts/domain/kg/extract/extract.py.bak +0 -430
@@ -1,365 +1,365 @@
1
- # session-start.py - Inject project context on session start
2
- import os, json, sys
3
-
4
- if sys.platform == 'win32':
5
- try:
6
- sys.stdout.reconfigure(encoding='utf-8')
7
- except Exception:
8
- pass
9
-
10
- NL = chr(10)
11
- BASE = os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
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
-
65
- # v3.0 M4: 删 CRITICAL_RULE (会话开始主动推销工作流铁律 → 非侵入式改造)
66
- # 平台询问逻辑保留在 wl-pipeline.md 规则1 (命令启动后才生效, 不在会话开头强推)
67
-
68
-
69
- def load_json_safe(path):
70
- try:
71
- with open(path, encoding='utf-8') as f:
72
- return json.load(f)
73
- except Exception:
74
- return None
75
-
76
-
77
- def get_developer():
78
- df = os.path.join(BASE, '.qoder', '.developer')
79
- if not os.path.isfile(df):
80
- return None
81
- try:
82
- with open(df, encoding='utf-8') as f:
83
- content = f.read()
84
- except Exception:
85
- return None
86
- info = {}
87
- for line in content.strip().splitlines():
88
- for sep in ('=', ':'):
89
- if sep in line:
90
- k, v = line.split(sep, 1)
91
- info[k.strip()] = v.strip()
92
- break
93
- return info or None
94
-
95
-
96
- def get_active_tasks():
97
- td = os.path.join(BASE, 'workspace', 'tasks')
98
- if not os.path.isdir(td):
99
- return []
100
- tasks = []
101
- for d in sorted(os.listdir(td)):
102
- data = load_json_safe(os.path.join(td, d, 'task.json'))
103
- if data is None:
104
- continue
105
- tasks.append({
106
- 'name': d,
107
- 'title': data.get('title', d),
108
- 'status': data.get('status', 'unknown')
109
- })
110
- return tasks
111
-
112
-
113
- def get_team_size():
114
- md = os.path.join(BASE, 'workspace', 'members')
115
- if not os.path.isdir(md):
116
- return 0
117
- return len([d for d in os.listdir(md) if os.path.isdir(os.path.join(md, d))])
118
-
119
-
120
- def get_index_info():
121
- lines = []
122
- # Hook-3: 检查 hook 错误日志, 超过 3 条新错误时告警
123
- try:
124
- log_path = os.path.join(BASE, '.qoder', '.runtime', 'hook-errors.log')
125
- if os.path.isfile(log_path):
126
- with open(log_path, 'r', encoding='utf-8', errors='replace') as f:
127
- errors = [l for l in f.readlines() if l.strip()]
128
- # 只看最近 24h 的错误
129
- recent = errors[-10:] # 取最后 10 行检查
130
- if len(recent) >= 3:
131
- lines.append('[WARN] hook 最近频繁失败 (%d 条错误), 详情见 .qoder/.runtime/hook-errors.log' % len(recent))
132
- lines.append(' 跑 python .qoder/scripts/setup/init_doctor.py --fix 排查')
133
- except OSError:
134
- pass
135
-
136
- lines.append('## Knowledge Graph')
137
- lines.append('MCP工具 (Qoder/QoderWork自动调用): search_code / rag_search / get_impact / project_brief / graphrag_ask 等43个')
138
- lines.append(' python .qoder/scripts/orchestration/wlkj.py search <keyword> [--platform web|app]')
139
- lines.append(' python .qoder/scripts/orchestration/wlkj.py context <keyword> # 一次打包全部上下文')
140
-
141
- # DuckDB 知识图谱状态检查
142
- # Read role from .developer file
143
- role = 'pm'
144
- try:
145
- dev_file = os.path.join(BASE, '.qoder', '.developer')
146
- if os.path.isfile(dev_file):
147
- for _line in open(dev_file, encoding='utf-8'):
148
- if _line.strip().startswith('role='):
149
- role = _line.split('=', 1)[1].strip()
150
- except Exception:
151
- pass
152
- # ★ 知识层健康检查:全迁 PG 后不再检查本地 kg.duckdb(已删除),改探活 MCP 知识层。
153
- # 复用 wlinkj-workflow/mcp_config.json 的 token+url,调 get_knowledge_health 拿真实健康分。
154
- # best-effort:任何失败都不阻塞会话启动(与其它 hook 铁律一致)。
155
- kg_info = None
156
- try:
157
- import json as _json
158
- import urllib.request as _url
159
- cfg_path = os.path.join(BASE, 'wlinkj-workflow', 'mcp_config.json')
160
- if os.path.isfile(cfg_path):
161
- _cfg = _json.load(open(cfg_path, encoding='utf-8'))
162
- _srv = _cfg.get('mcpServers', {}).get('wlinkj-knowledge', {})
163
- _url_s = _srv.get('url', '')
164
- _tok = _srv.get('token', '')
165
- if _url_s and _tok and _url_s != 'WILL_BE_FILLED_BY_WL_INIT':
166
- _rpc = {"jsonrpc": "2.0", "id": 1, "method": "tools/call",
167
- "params": {"name": "get_knowledge_health", "arguments": {}}}
168
- _req = _url.Request(_url_s, data=_json.dumps(_rpc).encode(),
169
- headers={"Content-Type": "application/json",
170
- "X-MCP-Token": _tok}, method="POST")
171
- _resp = _url.urlopen(_req, timeout=4).read()
172
- kg_info = _json.loads(_json.loads(_resp)["result"]["content"][0]["text"])
173
- except Exception:
174
- kg_info = None
175
-
176
- if kg_info and isinstance(kg_info, dict) and "error" not in kg_info:
177
- lines.append(' ✓ 知识层在线 (PG@5432): 健康分 %s / 覆盖 %s / 新鲜 %s / 完整 %s' % (
178
- kg_info.get("score", "?"), kg_info.get("coverage", "?"),
179
- kg_info.get("freshness", "?"), kg_info.get("completeness", "?")))
180
- issues = kg_info.get("issues", []) or []
181
- if issues and role in ('admin', 'dev'):
182
- lines.append(' [提示] 健康问题: ' + '; '.join(str(i)[:60] for i in issues[:2]))
183
- else:
184
- lines.append(' [WARN] 知识层未响应(MCP 后端未启动?)')
185
- lines.append(' → 启动: cd wlinkj-workspace && start_pg.bat(后端 :10010)')
186
-
187
- return NL.join(lines)
188
-
189
-
190
- def get_style_info():
191
- """Read style metadata for quick reference"""
192
- meta = load_json_safe(os.path.join(BASE, 'data', 'index', 'ui-style-meta.json'))
193
- if not meta:
194
- return None
195
-
196
- lines = []
197
- lines.append('## UI Style Index')
198
- lines.append('Style search: python .qoder/scripts/orchestration/wlkj.py style --style <table|form|modal>')
199
- lines.append('Field search: python .qoder/scripts/orchestration/wlkj.py search --field <name>')
200
-
201
- for proj, d in meta.get('projects', {}).items():
202
- vue = d.get('vue_files', 0)
203
- if vue > 0:
204
- pt = d.get('page_types', {})
205
- types_str = ', '.join([k + ':' + str(v) for k, v in pt.items() if v > 0])
206
- lines.append(' ' + proj + ': ' + str(vue) + ' Vue (' + types_str + ')')
207
-
208
- lines.append('Top components: ' + ', '.join(list(meta.get('common_components', {}).keys())[:8]))
209
- lines.append('Field map: ' + str(meta.get('field_count', 0)) + ' entries')
210
-
211
- if os.path.isdir(os.path.join(BASE, 'data', 'style')):
212
- lines.append('Style PDFs: data/style/ (design specs, code takes priority)')
213
-
214
- return NL.join(lines)
215
-
216
-
217
- def get_usability_summary():
218
- """读工作流可用性指标, 把"不达标"项注入会话上下文。
219
-
220
- 吸收宿主 SessionStart hook 能力: 不用等用户跑 /wl-status, 每次开会话主动
221
- 暴露"工作流用没用起来"的红灯项。只展示红/黄 (绿的不打扰), 无数据则不显示。
222
- (与 usability_score.py 同源逻辑, 但 hook 内联精简版, 避免 import 验证脚本拖慢启动)
223
- """
224
- lines = []
225
- try:
226
- # U2 一次过审率 (从 eval-history 取首条)
227
- eval_path = os.path.join(BASE, 'data', 'learning', 'eval-history.jsonl')
228
- records = []
229
- if os.path.isfile(eval_path):
230
- with open(eval_path, encoding='utf-8', errors='replace') as f:
231
- for ln in f:
232
- ln = ln.strip()
233
- if ln:
234
- try:
235
- records.append(json.loads(ln))
236
- except Exception:
237
- continue
238
- # 取最近 7 天
239
- from datetime import datetime, timedelta
240
- cutoff = datetime.now() - timedelta(days=7)
241
- recent = []
242
- for r in records:
243
- t = r.get('time', '')
244
- try:
245
- if datetime.fromisoformat(str(t).split('.')[0]) >= cutoff:
246
- recent.append(r)
247
- except Exception:
248
- continue
249
-
250
- issues = [] # 只收红/黄
251
- if recent:
252
- passed = sum(1 for r in recent if r.get('passed'))
253
- rate = 100.0 * passed / len(recent)
254
- if rate < 60: # 黄线 30, 红线 60
255
- tag = '🔴' if rate < 30 else '🟡'
256
- issues.append('%s 一次过审率 %.0f%% (<60%% 目标) — PRD 质量需提升' % (tag, rate))
257
-
258
- # U4 规则违反率 (从 feedback)
259
- fb_dir = os.path.join(BASE, 'workspace', 'members')
260
- violations = 0
261
- prds_gen = 0
262
- if os.path.isdir(fb_dir):
263
- for dev in os.listdir(fb_dir):
264
- fb = os.path.join(fb_dir, dev, 'journal', 'feedback.jsonl')
265
- if not os.path.isfile(fb):
266
- continue
267
- try:
268
- with open(fb, encoding='utf-8', errors='replace') as f:
269
- for ln in f:
270
- if '"rule_violation"' in ln:
271
- violations += 1
272
- elif '"prd_accepted"' in ln:
273
- prds_gen += 1
274
- except Exception:
275
- continue
276
- if prds_gen and violations:
277
- vrate = 100.0 * violations / prds_gen
278
- if vrate > 10:
279
- tag = '🔴' if vrate > 25 else '🟡'
280
- issues.append('%s 规则违反率 %.0f%% (>10%% 目标) — 关键规则没拦住' % (tag, vrate))
281
-
282
- if issues:
283
- lines.append('## Workflow Usability (近7天)')
284
- lines.append('工作流"用不用得起来"红灯 (详情: wlkj.py usability):')
285
- for it in issues:
286
- lines.append(' ' + it)
287
- except Exception:
288
- pass # hook 失败绝不阻塞会话
289
- return NL.join(lines) if lines else ''
290
-
291
-
292
- def main():
293
- parts = []
294
- parts.append('<qoder-context>')
295
- # Hook-3: 上次会话以来的 hook 错误告警 (不只落盘, 要让用户/AI 看到)
296
- # hook 坏了会导致上下文注入失败 (任务状态/平台规则缺失), 必须可见
297
- recent_errors = _recent_hook_errors(hours=24, limit=3)
298
- if recent_errors:
299
- parts.append('⚠️ [HOOK-WARN] 过去 24h 有 hook 错误 (上下文注入可能失效):')
300
- for err in recent_errors:
301
- parts.append(' ' + err)
302
- parts.append(' 详情见 .qoder/.runtime/hook-errors.log; 检查 Python 环境或跑 init_doctor --fix')
303
- dev = get_developer()
304
- if dev:
305
- name = dev.get('name', 'unknown')
306
- # role 真源在 member.json (.developer 常无 role= 行 → 显示 unknown)。
307
- # 回退查 member.json, 再回退 'pm' (绝不显示 unknown, 误导 AI)。
308
- role = dev.get('role')
309
- if not role or role == 'unknown':
310
- try:
311
- mj = os.path.join(BASE, 'workspace', 'members', name, 'member.json')
312
- if os.path.isfile(mj):
313
- m = json.load(open(mj, encoding='utf-8'))
314
- role = m.get('role') or 'pm'
315
- except Exception:
316
- pass
317
- role = role or 'pm'
318
- parts.append('Developer: ' + name + ' (' + role + ')')
319
- parts.append('Workspace: workspace/members/' + name + '/')
320
- else:
321
- parts.append('Developer: NOT SET. Run /wl-init to register.')
322
- team = get_team_size()
323
- if team > 0:
324
- parts.append('Team members: ' + str(team))
325
- tasks = get_active_tasks()
326
- if tasks:
327
- parts.append('Active tasks:')
328
- for t in tasks:
329
- parts.append(' [' + t['status'] + '] ' + t['name'] + ' - ' + t['title'])
330
-
331
- usability = get_usability_summary()
332
- if usability:
333
- parts.append('')
334
- parts.append(usability)
335
-
336
- parts.append('')
337
-
338
- idx_info = get_index_info()
339
- if idx_info:
340
- parts.append('')
341
- parts.append(idx_info)
342
-
343
- style_info = get_style_info()
344
- if style_info:
345
- parts.append('')
346
- parts.append(style_info)
347
-
348
- parts.append('</qoder-context>')
349
- # Hook-1: 健康标记 — AI 能感知 hook 是否真的跑了
350
- # QoderWork/Quest 里看不到 [hook-ok] 就知道 hook 没生效, 要自取上下文
351
- parts.append('<!-- [hook-ok] session-start hook ran successfully -->')
352
- print(NL.join(parts))
353
-
354
-
355
- if __name__ == '__main__':
356
- try:
357
- main()
358
- except Exception as e:
359
- # A broken hook must never kill the session - emit minimal context
360
- # 同时落盘到日志 (带轮转), 方便下次会话告警
361
- _append_hook_error(HOOK_ERRORS_LOG, str(e)[:200])
362
- print('<qoder-context>')
363
- print('session-start hook error: ' + str(e)[:200])
364
- print(' (详情见 .qoder/.runtime/hook-errors.log)')
365
- print('</qoder-context>')
1
+ # session-start.py - Inject project context on session start
2
+ import os, json, sys
3
+
4
+ if sys.platform == 'win32':
5
+ try:
6
+ sys.stdout.reconfigure(encoding='utf-8')
7
+ except Exception:
8
+ pass
9
+
10
+ NL = chr(10)
11
+ BASE = os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
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
+
65
+ # v3.0 M4: 删 CRITICAL_RULE (会话开始主动推销工作流铁律 → 非侵入式改造)
66
+ # 平台询问逻辑保留在 wl-pipeline.md 规则1 (命令启动后才生效, 不在会话开头强推)
67
+
68
+
69
+ def load_json_safe(path):
70
+ try:
71
+ with open(path, encoding='utf-8') as f:
72
+ return json.load(f)
73
+ except Exception:
74
+ return None
75
+
76
+
77
+ def get_developer():
78
+ df = os.path.join(BASE, '.qoder', '.developer')
79
+ if not os.path.isfile(df):
80
+ return None
81
+ try:
82
+ with open(df, encoding='utf-8') as f:
83
+ content = f.read()
84
+ except Exception:
85
+ return None
86
+ info = {}
87
+ for line in content.strip().splitlines():
88
+ for sep in ('=', ':'):
89
+ if sep in line:
90
+ k, v = line.split(sep, 1)
91
+ info[k.strip()] = v.strip()
92
+ break
93
+ return info or None
94
+
95
+
96
+ def get_active_tasks():
97
+ td = os.path.join(BASE, 'workspace', 'tasks')
98
+ if not os.path.isdir(td):
99
+ return []
100
+ tasks = []
101
+ for d in sorted(os.listdir(td)):
102
+ data = load_json_safe(os.path.join(td, d, 'task.json'))
103
+ if data is None:
104
+ continue
105
+ tasks.append({
106
+ 'name': d,
107
+ 'title': data.get('title', d),
108
+ 'status': data.get('status', 'unknown')
109
+ })
110
+ return tasks
111
+
112
+
113
+ def get_team_size():
114
+ md = os.path.join(BASE, 'workspace', 'members')
115
+ if not os.path.isdir(md):
116
+ return 0
117
+ return len([d for d in os.listdir(md) if os.path.isdir(os.path.join(md, d))])
118
+
119
+
120
+ def get_index_info():
121
+ lines = []
122
+ # Hook-3: 检查 hook 错误日志, 超过 3 条新错误时告警
123
+ try:
124
+ log_path = os.path.join(BASE, '.qoder', '.runtime', 'hook-errors.log')
125
+ if os.path.isfile(log_path):
126
+ with open(log_path, 'r', encoding='utf-8', errors='replace') as f:
127
+ errors = [l for l in f.readlines() if l.strip()]
128
+ # 只看最近 24h 的错误
129
+ recent = errors[-10:] # 取最后 10 行检查
130
+ if len(recent) >= 3:
131
+ lines.append('[WARN] hook 最近频繁失败 (%d 条错误), 详情见 .qoder/.runtime/hook-errors.log' % len(recent))
132
+ lines.append(' 跑 python .qoder/scripts/setup/init_doctor.py --fix 排查')
133
+ except OSError:
134
+ pass
135
+
136
+ lines.append('## Knowledge Graph')
137
+ lines.append('MCP工具 (Qoder/QoderWork自动调用): search_code / rag_search / get_impact / project_brief / graphrag_ask 等43个')
138
+ lines.append(' python .qoder/scripts/orchestration/wlkj.py search <keyword> [--platform web|app]')
139
+ lines.append(' python .qoder/scripts/orchestration/wlkj.py context <keyword> # 一次打包全部上下文')
140
+
141
+ # DuckDB 知识图谱状态检查
142
+ # Read role from .developer file
143
+ role = 'pm'
144
+ try:
145
+ dev_file = os.path.join(BASE, '.qoder', '.developer')
146
+ if os.path.isfile(dev_file):
147
+ for _line in open(dev_file, encoding='utf-8'):
148
+ if _line.strip().startswith('role='):
149
+ role = _line.split('=', 1)[1].strip()
150
+ except Exception:
151
+ pass
152
+ # ★ 知识层健康检查:全迁 PG 后不再检查本地 kg.duckdb(已删除),改探活 MCP 知识层。
153
+ # 复用 .qoder/mcp_config.json 的 token+url,调 get_knowledge_health 拿真实健康分。
154
+ # best-effort:任何失败都不阻塞会话启动(与其它 hook 铁律一致)。
155
+ kg_info = None
156
+ try:
157
+ import json as _json
158
+ import urllib.request as _url
159
+ cfg_path = os.path.join(BASE, '.qoder', 'mcp_config.json')
160
+ if os.path.isfile(cfg_path):
161
+ _cfg = _json.load(open(cfg_path, encoding='utf-8'))
162
+ _srv = _cfg.get('mcpServers', {}).get('wlinkj-knowledge', {})
163
+ _url_s = _srv.get('url', '')
164
+ _tok = _srv.get('token', '')
165
+ if _url_s and _tok and _url_s != 'WILL_BE_FILLED_BY_WL_INIT':
166
+ _rpc = {"jsonrpc": "2.0", "id": 1, "method": "tools/call",
167
+ "params": {"name": "get_knowledge_health", "arguments": {}}}
168
+ _req = _url.Request(_url_s, data=_json.dumps(_rpc).encode(),
169
+ headers={"Content-Type": "application/json",
170
+ "X-MCP-Token": _tok}, method="POST")
171
+ _resp = _url.urlopen(_req, timeout=4).read()
172
+ kg_info = _json.loads(_json.loads(_resp)["result"]["content"][0]["text"])
173
+ except Exception:
174
+ kg_info = None
175
+
176
+ if kg_info and isinstance(kg_info, dict) and "error" not in kg_info:
177
+ lines.append(' ✓ 知识层在线 (PG@5432): 健康分 %s / 覆盖 %s / 新鲜 %s / 完整 %s' % (
178
+ kg_info.get("score", "?"), kg_info.get("coverage", "?"),
179
+ kg_info.get("freshness", "?"), kg_info.get("completeness", "?")))
180
+ issues = kg_info.get("issues", []) or []
181
+ if issues and role in ('admin', 'dev'):
182
+ lines.append(' [提示] 健康问题: ' + '; '.join(str(i)[:60] for i in issues[:2]))
183
+ else:
184
+ lines.append(' [WARN] 知识层未响应(MCP 后端未启动?)')
185
+ lines.append(' → 启动: cd wlinkj-workspace && start_pg.bat(后端 :10010)')
186
+
187
+ return NL.join(lines)
188
+
189
+
190
+ def get_style_info():
191
+ """Read style metadata for quick reference"""
192
+ meta = load_json_safe(os.path.join(BASE, 'data', 'index', 'ui-style-meta.json'))
193
+ if not meta:
194
+ return None
195
+
196
+ lines = []
197
+ lines.append('## UI Style Index')
198
+ lines.append('Style search: python .qoder/scripts/orchestration/wlkj.py style --style <table|form|modal>')
199
+ lines.append('Field search: python .qoder/scripts/orchestration/wlkj.py search --field <name>')
200
+
201
+ for proj, d in meta.get('projects', {}).items():
202
+ vue = d.get('vue_files', 0)
203
+ if vue > 0:
204
+ pt = d.get('page_types', {})
205
+ types_str = ', '.join([k + ':' + str(v) for k, v in pt.items() if v > 0])
206
+ lines.append(' ' + proj + ': ' + str(vue) + ' Vue (' + types_str + ')')
207
+
208
+ lines.append('Top components: ' + ', '.join(list(meta.get('common_components', {}).keys())[:8]))
209
+ lines.append('Field map: ' + str(meta.get('field_count', 0)) + ' entries')
210
+
211
+ if os.path.isdir(os.path.join(BASE, 'data', 'style')):
212
+ lines.append('Style PDFs: data/style/ (design specs, code takes priority)')
213
+
214
+ return NL.join(lines)
215
+
216
+
217
+ def get_usability_summary():
218
+ """读工作流可用性指标, 把"不达标"项注入会话上下文。
219
+
220
+ 吸收宿主 SessionStart hook 能力: 不用等用户跑 /wl-status, 每次开会话主动
221
+ 暴露"工作流用没用起来"的红灯项。只展示红/黄 (绿的不打扰), 无数据则不显示。
222
+ (与 usability_score.py 同源逻辑, 但 hook 内联精简版, 避免 import 验证脚本拖慢启动)
223
+ """
224
+ lines = []
225
+ try:
226
+ # U2 一次过审率 (从 eval-history 取首条)
227
+ eval_path = os.path.join(BASE, 'data', 'learning', 'eval-history.jsonl')
228
+ records = []
229
+ if os.path.isfile(eval_path):
230
+ with open(eval_path, encoding='utf-8', errors='replace') as f:
231
+ for ln in f:
232
+ ln = ln.strip()
233
+ if ln:
234
+ try:
235
+ records.append(json.loads(ln))
236
+ except Exception:
237
+ continue
238
+ # 取最近 7 天
239
+ from datetime import datetime, timedelta
240
+ cutoff = datetime.now() - timedelta(days=7)
241
+ recent = []
242
+ for r in records:
243
+ t = r.get('time', '')
244
+ try:
245
+ if datetime.fromisoformat(str(t).split('.')[0]) >= cutoff:
246
+ recent.append(r)
247
+ except Exception:
248
+ continue
249
+
250
+ issues = [] # 只收红/黄
251
+ if recent:
252
+ passed = sum(1 for r in recent if r.get('passed'))
253
+ rate = 100.0 * passed / len(recent)
254
+ if rate < 60: # 黄线 30, 红线 60
255
+ tag = '🔴' if rate < 30 else '🟡'
256
+ issues.append('%s 一次过审率 %.0f%% (<60%% 目标) — PRD 质量需提升' % (tag, rate))
257
+
258
+ # U4 规则违反率 (从 feedback)
259
+ fb_dir = os.path.join(BASE, 'workspace', 'members')
260
+ violations = 0
261
+ prds_gen = 0
262
+ if os.path.isdir(fb_dir):
263
+ for dev in os.listdir(fb_dir):
264
+ fb = os.path.join(fb_dir, dev, 'journal', 'feedback.jsonl')
265
+ if not os.path.isfile(fb):
266
+ continue
267
+ try:
268
+ with open(fb, encoding='utf-8', errors='replace') as f:
269
+ for ln in f:
270
+ if '"rule_violation"' in ln:
271
+ violations += 1
272
+ elif '"prd_accepted"' in ln:
273
+ prds_gen += 1
274
+ except Exception:
275
+ continue
276
+ if prds_gen and violations:
277
+ vrate = 100.0 * violations / prds_gen
278
+ if vrate > 10:
279
+ tag = '🔴' if vrate > 25 else '🟡'
280
+ issues.append('%s 规则违反率 %.0f%% (>10%% 目标) — 关键规则没拦住' % (tag, vrate))
281
+
282
+ if issues:
283
+ lines.append('## Workflow Usability (近7天)')
284
+ lines.append('工作流"用不用得起来"红灯 (详情: wlkj.py usability):')
285
+ for it in issues:
286
+ lines.append(' ' + it)
287
+ except Exception:
288
+ pass # hook 失败绝不阻塞会话
289
+ return NL.join(lines) if lines else ''
290
+
291
+
292
+ def main():
293
+ parts = []
294
+ parts.append('<qoder-context>')
295
+ # Hook-3: 上次会话以来的 hook 错误告警 (不只落盘, 要让用户/AI 看到)
296
+ # hook 坏了会导致上下文注入失败 (任务状态/平台规则缺失), 必须可见
297
+ recent_errors = _recent_hook_errors(hours=24, limit=3)
298
+ if recent_errors:
299
+ parts.append('⚠️ [HOOK-WARN] 过去 24h 有 hook 错误 (上下文注入可能失效):')
300
+ for err in recent_errors:
301
+ parts.append(' ' + err)
302
+ parts.append(' 详情见 .qoder/.runtime/hook-errors.log; 检查 Python 环境或跑 init_doctor --fix')
303
+ dev = get_developer()
304
+ if dev:
305
+ name = dev.get('name', 'unknown')
306
+ # role 真源在 member.json (.developer 常无 role= 行 → 显示 unknown)。
307
+ # 回退查 member.json, 再回退 'pm' (绝不显示 unknown, 误导 AI)。
308
+ role = dev.get('role')
309
+ if not role or role == 'unknown':
310
+ try:
311
+ mj = os.path.join(BASE, 'workspace', 'members', name, 'member.json')
312
+ if os.path.isfile(mj):
313
+ m = json.load(open(mj, encoding='utf-8'))
314
+ role = m.get('role') or 'pm'
315
+ except Exception:
316
+ pass
317
+ role = role or 'pm'
318
+ parts.append('Developer: ' + name + ' (' + role + ')')
319
+ parts.append('Workspace: workspace/members/' + name + '/')
320
+ else:
321
+ parts.append('Developer: NOT SET. Run /wl-init to register.')
322
+ team = get_team_size()
323
+ if team > 0:
324
+ parts.append('Team members: ' + str(team))
325
+ tasks = get_active_tasks()
326
+ if tasks:
327
+ parts.append('Active tasks:')
328
+ for t in tasks:
329
+ parts.append(' [' + t['status'] + '] ' + t['name'] + ' - ' + t['title'])
330
+
331
+ usability = get_usability_summary()
332
+ if usability:
333
+ parts.append('')
334
+ parts.append(usability)
335
+
336
+ parts.append('')
337
+
338
+ idx_info = get_index_info()
339
+ if idx_info:
340
+ parts.append('')
341
+ parts.append(idx_info)
342
+
343
+ style_info = get_style_info()
344
+ if style_info:
345
+ parts.append('')
346
+ parts.append(style_info)
347
+
348
+ parts.append('</qoder-context>')
349
+ # Hook-1: 健康标记 — AI 能感知 hook 是否真的跑了
350
+ # QoderWork/Quest 里看不到 [hook-ok] 就知道 hook 没生效, 要自取上下文
351
+ parts.append('<!-- [hook-ok] session-start hook ran successfully -->')
352
+ print(NL.join(parts))
353
+
354
+
355
+ if __name__ == '__main__':
356
+ try:
357
+ main()
358
+ except Exception as e:
359
+ # A broken hook must never kill the session - emit minimal context
360
+ # 同时落盘到日志 (带轮转), 方便下次会话告警
361
+ _append_hook_error(HOOK_ERRORS_LOG, str(e)[:200])
362
+ print('<qoder-context>')
363
+ print('session-start hook error: ' + str(e)[:200])
364
+ print(' (详情见 .qoder/.runtime/hook-errors.log)')
365
+ print('</qoder-context>')