@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
@@ -116,7 +116,7 @@ def _auto_learn_from_prd(prd_path):
116
116
  try:
117
117
  title = os.path.basename(prd_path).replace('.md', '')
118
118
  # 读 mcp_config 拿 token
119
- cfg_path = os.path.join(BASE, 'wlinkj-workflow', 'mcp_config.json')
119
+ cfg_path = os.path.join(BASE, '.qoder', 'mcp_config.json')
120
120
  if not os.path.isfile(cfg_path):
121
121
  return
122
122
  import json as _j
@@ -143,6 +143,32 @@ def _auto_learn_from_prd(prd_path):
143
143
  pass # 铁律:静默
144
144
 
145
145
 
146
+ def _emit_ask_before_return(kind, path):
147
+ """PRD/原型写完 → 注入"先问用户再回流"指令(用户硬要求:上传前必须先问)。
148
+
149
+ hook 无法弹窗等 Y/n(工具执行后跑、输出即退),但能**确定性注入指令**让 AI
150
+ 每次必问、默认不上传。stdout 注入会话上下文,AI 看到后必须先问用户。
151
+ """
152
+ name = os.path.basename(path)
153
+ if kind == 'prd':
154
+ cmd = '$PY "$R/.qoder/scripts/orchestration/wlkj.py" return prd "%s" "<标题>"' % path
155
+ what = 'PRD'
156
+ else:
157
+ cmd = '$PY "$R/.qoder/scripts/orchestration/wlkj.py" return prototype "%s" "<功能>"' % path
158
+ what = '原型(HTML)'
159
+ msg = (
160
+ "\n⚠️ 【回流前必须先问用户】刚写了 %s:%s\n"
161
+ " → 必须先问用户:「要把这份 %s 回流到平台吗?」\n"
162
+ " → 用户【明确同意】(说「回流 / 上传 / 确认」)才执行:%s\n"
163
+ " → 用户拒绝 / 未明确同意 → 只留本地,绝不默认上传。\n"
164
+ " (宿主确定性要求:不问就传 = 违规;这是用户明确要的确认环节)\n"
165
+ ) % (what, name, what, cmd)
166
+ try:
167
+ sys.stdout.write(msg)
168
+ except Exception:
169
+ pass
170
+
171
+
146
172
  def _append_event(event, data):
147
173
  """best-effort append 一条事件到当前 dev 的 feedback.jsonl。避开 learn import 断链, 直接写文件。"""
148
174
  try:
@@ -198,6 +224,8 @@ def main():
198
224
  _append_event('prd_written', {'prd': os.path.basename(p), 'tool': tool_name})
199
225
  # ★ 宿主能力接入:PRD 写完 → 自动调 MCP learn 沉淀业务规则
200
226
  _auto_learn_from_prd(p)
227
+ # ★ 用户硬要求:PRD 回流平台前必须先问(不问就传=违规)
228
+ _emit_ask_before_return('prd', p)
201
229
  if plow.endswith('task.json'):
202
230
  _append_event('task_touched', {'task_json': os.path.basename(os.path.dirname(p)), 'tool': tool_name})
203
231
  # ★ 宿主能力接入:代码文件改完 → 自动记 code_changed(知识层下次检测过时用)
@@ -205,6 +233,8 @@ def main():
205
233
  _append_event('code_changed', {'file': os.path.basename(p), 'path': plow[:200]})
206
234
  # v3.1 软提示: prototype HTML 写完但缺来源锚定 → stderr 提醒(不阻断)
207
235
  if 'prototype' in os.path.basename(plow) and plow.endswith('.html'):
236
+ # ★ 用户硬要求:原型回流平台前必须先问(不问就传=违规)
237
+ _emit_ask_before_return('prototype', p)
208
238
  try:
209
239
  if os.path.isfile(p):
210
240
  with open(p, encoding='utf-8', errors='ignore') as _f:
@@ -0,0 +1,136 @@
1
+ #!/usr/bin/env python3
2
+ # -*- coding: utf-8 -*-
3
+ """pre-tool-use.py - PreToolUse hook: 改码前 get_impact 影响面 priming(#16 / CLAUDE.md 铁律#1 宿主级强制)
4
+
5
+ 吸收宿主确定性 hook 能力:把"改代码前必调 get_impact"从"靠 AI 记得(老忘→裸改崩一片)"
6
+ 升级为"宿主在 Write/Edit/MultiEdit 落到代码文件前,确定性提取符号 + 强提醒"。
7
+
8
+ 做法(确定性、零外部依赖、不依赖后端在线):
9
+ 1. 解析 stdin {tool_name, tool_input:{file_path}}
10
+ 2. 只对代码文件(*.py/java/vue/js/ts/go/kt 等,非 .md/.json/.txt)触发
11
+ 3. 正则提取文件里的 class/def/function 名(要改的符号)
12
+ 4. 输出影响面 advisory 到会话:列符号 + 强制提醒改前调 get_impact(entity=<主符号>)
13
+ 实际 get_impact 由工作流 cap.mcp.call 跑(robust,处理 token/SSE)。
14
+
15
+ 铁律:
16
+ - 永远 exit 0(advisory 不阻断;阻断会卡死工作流)
17
+ - 失败 try/except 静默(hook 崩绝不阻塞会话)
18
+ - 非 .py/.java 等代码文件 / 非编辑类工具 → 静默放行(不滥提醒)
19
+
20
+ stdin 字段名官方未公开,兼容 tool_name/tool_input 等多种写法(同 post-tool-use.py)。
21
+ """
22
+ import json
23
+ import os
24
+ import re
25
+ import sys
26
+
27
+ if sys.platform == 'win32':
28
+ try:
29
+ sys.stdout.reconfigure(encoding='utf-8')
30
+ sys.stderr.reconfigure(encoding='utf-8')
31
+ except Exception:
32
+ pass
33
+
34
+ NL = chr(10)
35
+ BASE = os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
36
+ HOOK_ERRORS_LOG = os.path.join(BASE, '.qoder', '.runtime', 'hook-errors.log')
37
+
38
+ # 触发的工具 + 代码文件后缀
39
+ _EDIT_TOOLS = {"Write", "Edit", "MultiEdit", "write", "edit", "multi_edit"}
40
+ _CODE_EXT = {".py", ".java", ".vue", ".js", ".ts", ".tsx", ".jsx", ".go", ".kt", ".scala", ".cs", ".c", ".cpp", ".h", ".rs"}
41
+
42
+ # 符号提取正则(class/def/function/interface)
43
+ _SYM_RE = re.compile(
44
+ r"^\s*(?:export\s+)?(?:public|private|protected|static|final|abstract|async|\s)*"
45
+ r"(?:class|def|function|interface|struct|enum|object)\s+([A-Za-z_][A-Za-z0-9_]*)",
46
+ re.MULTILINE,
47
+ )
48
+
49
+
50
+ def _log_err(line):
51
+ from datetime import datetime
52
+ try:
53
+ os.makedirs(os.path.dirname(HOOK_ERRORS_LOG), exist_ok=True)
54
+ with open(HOOK_ERRORS_LOG, 'a', encoding='utf-8') as f:
55
+ f.write('[{}] pre-tool-use: {}{}'.format(
56
+ datetime.now().strftime('%Y-%m-%d %H:%M:%S'), str(line)[:200], NL))
57
+ except Exception:
58
+ pass
59
+
60
+
61
+ def _read_stdin():
62
+ try:
63
+ raw = sys.stdin.read()
64
+ if raw and raw.strip():
65
+ return json.loads(raw)
66
+ except Exception as e:
67
+ _log_err("stdin parse: %s" % str(e)[:120])
68
+ return {}
69
+
70
+
71
+ def _is_code(path):
72
+ _, ext = os.path.splitext(path.lower())
73
+ return ext in _CODE_EXT
74
+
75
+
76
+ def _extract_symbols(path, limit=8):
77
+ """从文件提取 class/def/function 名(要改的符号)。best-effort,读不到返空。"""
78
+ try:
79
+ if not os.path.isfile(path):
80
+ # 相对路径 → 试 BASE 下定位
81
+ cand = path
82
+ if not os.path.isabs(path):
83
+ for root in (BASE, os.getcwd()):
84
+ c = os.path.join(root, path)
85
+ if os.path.isfile(c):
86
+ cand = c
87
+ break
88
+ if not os.path.isfile(cand):
89
+ return []
90
+ path = cand
91
+ with open(path, 'r', encoding='utf-8', errors='replace') as f:
92
+ text = f.read()[:200000] # 限 200K 防超大文件慢
93
+ return _SYM_RE.findall(text)[:limit]
94
+ except Exception as e:
95
+ _log_err("extract %s: %s" % (path, str(e)[:80]))
96
+ return []
97
+
98
+
99
+ def main():
100
+ data = _read_stdin()
101
+ tool = data.get("tool_name") or data.get("tool") or ""
102
+ tinput = data.get("tool_input") or data.get("input") or {}
103
+ path = tinput.get("file_path") or tinput.get("path") or ""
104
+
105
+ # 非编辑类工具 / 非代码文件 → 静默放行
106
+ if tool not in _EDIT_TOOLS or not path or not _is_code(path):
107
+ sys.exit(0)
108
+
109
+ syms = _extract_symbols(path)
110
+ rel = path
111
+ try:
112
+ rel = os.path.relpath(path, BASE)
113
+ except Exception:
114
+ pass
115
+
116
+ lines = []
117
+ lines.append("⚠️ **改码前影响面 priming**(CLAUDE.md 铁律#1:改代码前必调 get_impact)")
118
+ lines.append("即将编辑代码文件:`%s`" % rel)
119
+ if syms:
120
+ lines.append("含符号 %d 个:%s" % (len(syms), ", ".join("`%s`" % s for s in syms)))
121
+ lines.append("→ **改前必调** `cap.mcp.call('get_impact', {entity: '%s', depth: 2})` 看 upstream(谁依赖我)/downstream(我依赖谁)。" % syms[0])
122
+ lines.append(" 影响面大 / risk level≥high → 先提示风险再动手;高风险裸改须拦停问用户。")
123
+ else:
124
+ lines.append("(未提取到已知符号,可能是新文件)→ 仍建议改前 rag_search 找相关实现 + get_impact 看上下游。")
125
+
126
+ # advisory 输出到 stdout(注入会话上下文),exit 0 不阻断
127
+ sys.stdout.write(NL.join(lines) + NL)
128
+ sys.exit(0)
129
+
130
+
131
+ if __name__ == "__main__":
132
+ try:
133
+ main()
134
+ except Exception as e:
135
+ _log_err("fatal: %s" % str(e)[:150])
136
+ sys.exit(0) # 永不阻塞