@hupan56/wlkj 3.2.0 → 3.3.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/bin/cli.js +117 -0
- package/package.json +2 -2
- package/templates/.qoder/.runtime/hook-errors.log +4 -0
- package/templates/qoder/commands/optional/wl-insight.md +276 -276
- package/templates/qoder/commands/optional/wl-report.md +1 -1
- package/templates/qoder/commands/optional/wl-spec.md +13 -1
- package/templates/qoder/commands/optional/wl-status.md +14 -1
- package/templates/qoder/commands/wl-code.md +111 -4
- package/templates/qoder/commands/wl-commit.md +12 -1
- package/templates/qoder/commands/wl-design.md +69 -5
- package/templates/qoder/commands/wl-init.md +129 -102
- package/templates/qoder/commands/wl-prd.md +162 -6
- package/templates/qoder/commands/wl-search.md +106 -20
- package/templates/qoder/commands/wl-task.md +617 -613
- package/templates/qoder/commands/wl-test.md +20 -1
- package/templates/qoder/contracts/spec.md +4 -0
- package/templates/qoder/hooks/post-tool-use.py +71 -0
- package/templates/qoder/hooks/pre-tool-use.py +136 -0
- package/templates/qoder/hooks/session-start.py +365 -397
- package/templates/qoder/scripts/capability/__pycache__/__init__.cpython-39.pyc +0 -0
- package/templates/qoder/scripts/capability/__pycache__/present_html.cpython-39.pyc +0 -0
- package/templates/qoder/scripts/capability/__pycache__/registry.cpython-39.pyc +0 -0
- package/templates/qoder/scripts/capability/__pycache__/registry_mcp.cpython-39.pyc +0 -0
- package/templates/qoder/scripts/capability/adapters/__init__.py +1 -1
- package/templates/qoder/scripts/capability/adapters/__pycache__/__init__.cpython-39.pyc +0 -0
- package/templates/qoder/scripts/capability/adapters/__pycache__/cli.cpython-39.pyc +0 -0
- package/templates/qoder/scripts/capability/adapters/__pycache__/mcp.cpython-39.pyc +0 -0
- package/templates/qoder/scripts/capability/adapters/__pycache__/qw.cpython-39.pyc +0 -0
- package/templates/qoder/scripts/capability/adapters/mcp.py +76 -100
- package/templates/qoder/scripts/capability/adapters/qw.py +295 -295
- package/templates/qoder/scripts/capability/caps/__init__.py +5 -5
- package/templates/qoder/scripts/capability/caps/__pycache__/__init__.cpython-39.pyc +0 -0
- package/templates/qoder/scripts/capability/caps/__pycache__/context.cpython-39.pyc +0 -0
- package/templates/qoder/scripts/capability/caps/__pycache__/cron.cpython-39.pyc +0 -0
- package/templates/qoder/scripts/capability/caps/__pycache__/identity.cpython-39.pyc +0 -0
- package/templates/qoder/scripts/capability/caps/__pycache__/memory.cpython-39.pyc +0 -0
- package/templates/qoder/scripts/capability/caps/__pycache__/notify.cpython-39.pyc +0 -0
- package/templates/qoder/scripts/capability/caps/__pycache__/present.cpython-39.pyc +0 -0
- package/templates/qoder/scripts/capability/caps/__pycache__/repo.cpython-39.pyc +0 -0
- package/templates/qoder/scripts/capability/caps/__pycache__/sandbox.cpython-39.pyc +0 -0
- package/templates/qoder/scripts/capability/caps/memory.py +1 -1
- package/templates/qoder/scripts/capability/caps/notify.py +64 -0
- package/templates/qoder/scripts/capability/caps/sandbox.py +38 -0
- package/templates/qoder/scripts/capability/present_html.py +68 -0
- package/templates/qoder/scripts/capability/registry.py +27 -25
- package/templates/qoder/scripts/capability/registry_mcp.py +67 -5
- package/templates/qoder/scripts/capability/smoke_test_report.json +34 -20
- package/templates/qoder/scripts/deployment/setup/carriers.py +3 -1
- package/templates/qoder/scripts/deployment/setup/init_doctor.py +10 -3
- package/templates/qoder/scripts/domain/__pycache__/__init__.cpython-39.pyc +0 -0
- package/templates/qoder/scripts/domain/integration/__init__.py +0 -0
- package/templates/qoder/scripts/domain/integration/__pycache__/__init__.cpython-39.pyc +0 -0
- package/templates/qoder/scripts/domain/integration/__pycache__/return_to_platform.cpython-39.pyc +0 -0
- package/templates/qoder/scripts/domain/integration/return_to_platform.py +395 -0
- package/templates/qoder/scripts/domain/integration/spec_upload.py +208 -0
- package/templates/qoder/scripts/domain/kg/build/kg_build.py +27 -3
- package/templates/qoder/scripts/domain/kg/extract/asset/__init__.py +10 -0
- package/templates/qoder/scripts/domain/kg/extract/asset/asset_tree.py +57 -0
- package/templates/qoder/scripts/domain/kg/extract/asset/discussion_importer.py +62 -0
- package/templates/qoder/scripts/domain/kg/extract/asset/prd_importer.py +146 -0
- package/templates/qoder/scripts/domain/kg/extract/asset/prototype_importer.py +64 -0
- package/templates/qoder/scripts/domain/kg/extract/asset/returns_importer.py +52 -0
- package/templates/qoder/scripts/domain/kg/extract/build_goal3.py +104 -0
- package/templates/qoder/scripts/domain/kg/extract/build_goal4.py +55 -0
- package/templates/qoder/scripts/domain/kg/extract/build_goal5.py +95 -0
- package/templates/qoder/scripts/domain/kg/extract/db/__init__.py +8 -0
- package/templates/qoder/scripts/domain/kg/extract/db/data_profile.py +22 -0
- package/templates/qoder/scripts/domain/kg/extract/db/fk_extractor.py +55 -0
- package/templates/qoder/scripts/domain/kg/extract/db/schema_extractor.py +90 -0
- package/templates/qoder/scripts/domain/kg/extract/extract.py +84 -0
- package/templates/qoder/scripts/domain/kg/extract/inference/__init__.py +9 -0
- package/templates/qoder/scripts/domain/kg/extract/inference/community_summarizer.py +206 -0
- package/templates/qoder/scripts/domain/kg/extract/inference/embed_builder.py +132 -0
- package/templates/qoder/scripts/domain/kg/extract/inference/naming_matcher.py +80 -0
- package/templates/qoder/scripts/domain/kg/extract/inference/promote.py +59 -0
- package/templates/qoder/scripts/domain/kg/extract/inference/recompute.py +93 -0
- package/templates/qoder/scripts/domain/kg/extract/inference/weak_link.py +421 -0
- package/templates/qoder/scripts/domain/kg/extract/java/__init__.py +15 -0
- package/templates/qoder/scripts/domain/kg/extract/java/_parser.py +271 -0
- package/templates/qoder/scripts/domain/kg/extract/java/all.py +145 -0
- package/templates/qoder/scripts/domain/kg/extract/java/build_java_to_pg.py +102 -0
- package/templates/qoder/scripts/domain/kg/extract/java/call_chain.py +49 -0
- package/templates/qoder/scripts/domain/kg/extract/java/class_extractor.py +141 -0
- package/templates/qoder/scripts/domain/kg/extract/java/domain_extractor.py +148 -0
- package/templates/qoder/scripts/domain/kg/extract/java/dubbo_extractor.py +33 -0
- package/templates/qoder/scripts/domain/kg/extract/java/endpoint_extractor.py +36 -0
- package/templates/qoder/scripts/domain/kg/extract/java/javadoc_extractor.py +110 -0
- package/templates/qoder/scripts/domain/kg/extract/java/llm_cn_filler.py +150 -0
- package/templates/qoder/scripts/domain/kg/extract/java/member_extractor.py +157 -0
- package/templates/qoder/scripts/domain/kg/extract/java/mybatisplus_extractor.py +34 -0
- package/templates/qoder/scripts/domain/kg/extract/java/pg_upsert.py +165 -0
- package/templates/qoder/scripts/domain/kg/extract/java/satoken_extractor.py +30 -0
- package/templates/qoder/scripts/domain/kg/extract/java/spring_extractor.py +39 -0
- package/templates/qoder/scripts/domain/kg/extract/java/validation_extractor.py +33 -0
- package/templates/qoder/scripts/domain/kg/extract/mybatis/__init__.py +9 -0
- package/templates/qoder/scripts/domain/kg/extract/mybatis/all.py +79 -0
- package/templates/qoder/scripts/domain/kg/extract/mybatis/mapper_parser.py +99 -0
- package/templates/qoder/scripts/domain/kg/extract/mybatis/relation_builder.py +69 -0
- package/templates/qoder/scripts/domain/kg/extract/mybatis/sql_extractor.py +78 -0
- package/templates/qoder/scripts/domain/kg/extract/prd/__init__.py +8 -0
- package/templates/qoder/scripts/domain/kg/extract/prd/prd_chunk_embed.py +105 -0
- package/templates/qoder/scripts/domain/kg/extract/prd/prd_llm_extract.py +153 -0
- package/templates/qoder/scripts/domain/kg/extract/prd/req_anchor.py +120 -0
- package/templates/qoder/scripts/domain/kg/graph/kg_semantic.py +4 -2
- package/templates/qoder/scripts/domain/kg/kg.py +42 -5
- package/templates/qoder/scripts/domain/kg/search/_remote.py +187 -0
- package/templates/qoder/scripts/domain/kg/search/context_pack.py +32 -2
- package/templates/qoder/scripts/domain/kg/search/search_index.py +74 -20
- package/templates/qoder/scripts/domain/kg/switch_project.py +158 -0
- package/templates/qoder/scripts/domain/kg/sync_repowiki.py +109 -0
- package/templates/qoder/scripts/domain/task/__pycache__/wlkj_panel.cpython-39.pyc +0 -0
- package/templates/qoder/scripts/domain/task/{zentao_panel.py → wlkj_panel.py} +315 -53
- package/templates/qoder/scripts/engine/poller.py +219 -0
- package/templates/qoder/scripts/foundation/__pycache__/__init__.cpython-39.pyc +0 -0
- package/templates/qoder/scripts/foundation/core/__pycache__/__init__.cpython-39.pyc +0 -0
- package/templates/qoder/scripts/foundation/core/__pycache__/paths.cpython-39.pyc +0 -0
- package/templates/qoder/scripts/foundation/integrations/active_task.py +2 -1
- package/templates/qoder/scripts/orchestration/wlkj.py +4 -0
- package/templates/qoder/scripts/protocol/__pycache__/__init__.cpython-39.pyc +0 -0
- package/templates/qoder/scripts/protocol/mcp/zentao_mcp_server.py +1 -1
- package/templates/qoder/scripts/protocol/transports/__pycache__/__init__.cpython-39.pyc +0 -0
- package/templates/qoder/scripts/protocol/transports/__pycache__/base.cpython-39.pyc +0 -0
- package/templates/qoder/scripts/protocol/transports/__pycache__/cli.cpython-39.pyc +0 -0
- package/templates/qoder/scripts/protocol/transports/__pycache__/http.cpython-39.pyc +0 -0
- package/templates/qoder/scripts/protocol/transports/__pycache__/stdio.cpython-39.pyc +0 -0
- package/templates/qoder/scripts/protocol/transports/http.py +7 -1
- package/templates/qoder/scripts/validation/metrics/__pycache__/present_board.cpython-39.pyc +0 -0
- package/templates/qoder/scripts/validation/metrics/present_board.py +180 -0
- package/templates/qoder/settings.json +10 -0
- package/templates/root/AGENTS.md +9 -10
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: wl-test
|
|
3
|
-
description: "测试: quick(意图式用例,默认) / browser(Playwright浏览器) / unit(单元) / coverage(
|
|
3
|
+
description: "测试: quick(意图式用例,默认) / browser(Playwright浏览器) / unit(单元) / coverage(测试覆盖矩阵)。"
|
|
4
4
|
argument-hint: "[quick|browser|unit|coverage] <描述或任务>"
|
|
5
5
|
auto-approve: false
|
|
6
6
|
allowed-tools: [Read, Glob, Grep, Bash, Write, Edit]
|
|
@@ -91,6 +91,10 @@ $PY "$R/.qoder/scripts/orchestration/wlkj.py" test quick \
|
|
|
91
91
|
$PY "$R/.qoder/scripts/orchestration/wlkj.py" test quick --record '{"Q-1":"pass","Q-2":"fail"}'
|
|
92
92
|
```
|
|
93
93
|
|
|
94
|
+
### 下一步
|
|
95
|
+
- 用例全过 → `/wl-commit <[#REQ-xxx] message>` 提交回流(双通飞轮:quick 测 → commit 回流 → `/wl-task finish <REQ-ID>` 收尾)
|
|
96
|
+
- 有失败 → 列原因,修复后重跑 quick,再 commit
|
|
97
|
+
|
|
94
98
|
---
|
|
95
99
|
|
|
96
100
|
## browser 基于任务的浏览器测试(需先有任务)
|
|
@@ -124,10 +128,25 @@ python -c "import json; d=json.load(open('$R/data/index/test-pages.json')); prin
|
|
|
124
128
|
输出:哪些功能/页面有测试用例(test-pages.json + test-assertions.json),哪些是空白。
|
|
125
129
|
AI 据此建议"优先补 XX 功能的测试"。
|
|
126
130
|
|
|
131
|
+
🎨 **富展示**(覆盖热力图 HTML):`$PY "$R/.qoder/scripts/validation/metrics/present_board.py" coverage --project-id <UUID>`(功能×测试 有/无 热力图,QoderWork 嵌入对话 / CLI 落盘 journal)
|
|
132
|
+
|
|
133
|
+
### 改动驱动的必跑测试(提交前门禁 · S4 覆盖推理)
|
|
134
|
+
|
|
135
|
+
改完代码要确定"必跑哪些测试"时,用覆盖推理四件套(已注册 MCP):
|
|
136
|
+
```python
|
|
137
|
+
cap.mcp.call("impact_coverage", {"entity_ids": ["<改动实体id1>", "<id2>"]}) # 改动集→必跑测试
|
|
138
|
+
cap.mcp.call("find_tests_of", {"entity_id": "<某实体id>"}) # 查某实体的测试
|
|
139
|
+
cap.mcp.call("find_coverage_of", {"req_id": "<REQ-ID>"}) # 查某需求的覆盖
|
|
140
|
+
```
|
|
141
|
+
- `impact_coverage` 拿"这次改动必须跑哪些测试"——提交前用它圈定回归范围,不漏不改崩。
|
|
142
|
+
- 改动实体 id 从 `rag_search`/`search_code` 命中里取。
|
|
143
|
+
|
|
127
144
|
---
|
|
128
145
|
|
|
129
146
|
## unit 单元测试
|
|
130
147
|
|
|
148
|
+
> **宿主 sub-agent 激活**:QoderWork 支持 sub-agent 时,dispatch 到 **`qoder-test-gen`** agent(`.qoder/agents/qoder-test-gen.toml`)——自动定位实现代码 + 读 Spec → 生成 JUnit(Service 层 Mock 全依赖 Given-When-Then / Controller 层 MockMvc / @DisplayName / AssertJ / Builder 测试数据),输出 `tests/{包路径}/{类名}Test.java`。dispatch prompt 首行传 `Active Code: <实现文件路径>`。不支持则按下文 skill 手动生成。
|
|
149
|
+
|
|
131
150
|
读 `.qoder/skills/test-generator/SKILL.md`:定位实现代码 → 读 spec → 生成
|
|
132
151
|
JUnit 测试(Mock + MockMvc,Given-When-Then)→ 输出 `tests/{package}/{Class}Test.java`。
|
|
133
152
|
(只有这条线才需要 search_index 找代码。)
|
|
@@ -50,12 +50,16 @@ module: policy
|
|
|
50
50
|
endpoints: [POST /api/policy/amend, GET /api/policy/amend/{id}]
|
|
51
51
|
status: draft
|
|
52
52
|
zentao: task=1453 story=761
|
|
53
|
+
zentao_id: 761
|
|
54
|
+
platform_spec_id:
|
|
53
55
|
-->
|
|
54
56
|
```
|
|
55
57
|
|
|
56
58
|
**字段说明**:
|
|
57
59
|
- `status`:状态机见下表,/wl-code spec gate 的判据。
|
|
58
60
|
- `zentao`:禅道 task 来的必须填(task+story);本地来的留空,待回填时补。回填 task 后 spec 摘要写入该 task 的 `desc`。
|
|
61
|
+
- `zentao_id`:禅道 story 号(关联键,= zentao 的 story)。spec_upload 据此让平台反查 Requirement 挂载;缺失则上传暂存待产品推禅道后回填。
|
|
62
|
+
- `platform_spec_id`:平台 spec id(幂等锚)。spec_upload 首次 create 后自动回写,**勿手填**;有值则下次上传走 update_spec(新增版本不重复建)。
|
|
59
63
|
|
|
60
64
|
**`status` 状态机**(/wl-code spec gate 的判据):
|
|
61
65
|
|
|
@@ -107,6 +107,68 @@ def _dev():
|
|
|
107
107
|
return None
|
|
108
108
|
|
|
109
109
|
|
|
110
|
+
def _auto_learn_from_prd(prd_path):
|
|
111
|
+
"""PRD 写完后自动调 MCP learn 沉淀业务规则(宿主能力接入)。
|
|
112
|
+
|
|
113
|
+
读 PRD 标题 → 调工作台 MCP learn → 知识层自动积累。
|
|
114
|
+
失败静默(铁律:hook 崩溃不阻塞会话)。
|
|
115
|
+
"""
|
|
116
|
+
try:
|
|
117
|
+
title = os.path.basename(prd_path).replace('.md', '')
|
|
118
|
+
# 读 mcp_config 拿 token
|
|
119
|
+
cfg_path = os.path.join(BASE, '.qoder', 'mcp_config.json')
|
|
120
|
+
if not os.path.isfile(cfg_path):
|
|
121
|
+
return
|
|
122
|
+
import json as _j
|
|
123
|
+
cfg = _j.load(open(cfg_path, encoding='utf-8'))
|
|
124
|
+
srv = cfg.get('mcpServers', {}).get('wlinkj-knowledge', {})
|
|
125
|
+
token = srv.get('token', '')
|
|
126
|
+
url = srv.get('url', '').replace('/mcp', '/mcp') # 保持 /mcp
|
|
127
|
+
pid = srv.get('env', {}).get('WLKJ_PROJECT_ID', '')
|
|
128
|
+
if not token or not url or not pid:
|
|
129
|
+
return
|
|
130
|
+
# 调 MCP learn(POST /mcp JSON-RPC)
|
|
131
|
+
import urllib.request
|
|
132
|
+
rpc = {"jsonrpc": "2.0", "id": 1, "method": "tools/call",
|
|
133
|
+
"params": {"name": "learn",
|
|
134
|
+
"arguments": {"project_id": pid, "category": "decision",
|
|
135
|
+
"pattern": "PRD更新: " + title,
|
|
136
|
+
"source": "qoder-hook-auto"}}}
|
|
137
|
+
req = urllib.request.Request(
|
|
138
|
+
url, data=_j.dumps(rpc).encode(),
|
|
139
|
+
headers={"Content-Type": "application/json", "X-MCP-Token": token},
|
|
140
|
+
method="POST")
|
|
141
|
+
urllib.request.urlopen(req, timeout=5).read() # 5s 超时,不阻塞
|
|
142
|
+
except Exception:
|
|
143
|
+
pass # 铁律:静默
|
|
144
|
+
|
|
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
|
+
|
|
110
172
|
def _append_event(event, data):
|
|
111
173
|
"""best-effort append 一条事件到当前 dev 的 feedback.jsonl。避开 learn import 断链, 直接写文件。"""
|
|
112
174
|
try:
|
|
@@ -160,10 +222,19 @@ def main():
|
|
|
160
222
|
plow = p.lower().replace('\\', '/')
|
|
161
223
|
if 'prd' in os.path.basename(plow).lower() and plow.endswith('.md'):
|
|
162
224
|
_append_event('prd_written', {'prd': os.path.basename(p), 'tool': tool_name})
|
|
225
|
+
# ★ 宿主能力接入:PRD 写完 → 自动调 MCP learn 沉淀业务规则
|
|
226
|
+
_auto_learn_from_prd(p)
|
|
227
|
+
# ★ 用户硬要求:PRD 回流平台前必须先问(不问就传=违规)
|
|
228
|
+
_emit_ask_before_return('prd', p)
|
|
163
229
|
if plow.endswith('task.json'):
|
|
164
230
|
_append_event('task_touched', {'task_json': os.path.basename(os.path.dirname(p)), 'tool': tool_name})
|
|
231
|
+
# ★ 宿主能力接入:代码文件改完 → 自动记 code_changed(知识层下次检测过时用)
|
|
232
|
+
if any(plow.endswith(ext) for ext in ('.java', '.vue', '.ts', '.js', '.py')):
|
|
233
|
+
_append_event('code_changed', {'file': os.path.basename(p), 'path': plow[:200]})
|
|
165
234
|
# v3.1 软提示: prototype HTML 写完但缺来源锚定 → stderr 提醒(不阻断)
|
|
166
235
|
if 'prototype' in os.path.basename(plow) and plow.endswith('.html'):
|
|
236
|
+
# ★ 用户硬要求:原型回流平台前必须先问(不问就传=违规)
|
|
237
|
+
_emit_ask_before_return('prototype', p)
|
|
167
238
|
try:
|
|
168
239
|
if os.path.isfile(p):
|
|
169
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) # 永不阻塞
|