@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
|
@@ -0,0 +1,219 @@
|
|
|
1
|
+
"""引擎轮询器:后台轮询平台 pending 触发 → 认领 → 执行 → 回流 → 回填结果。
|
|
2
|
+
|
|
3
|
+
这是"平台→引擎→平台"真闭环的引擎侧执行器:
|
|
4
|
+
1. 轮询 GET /trigger/pending(带项目令牌)
|
|
5
|
+
2. 认领 POST /trigger/{tid}/claim(原子锁)
|
|
6
|
+
3. 执行:根据 command 调 MCP 取知识 + 生成产出(简化版:rag_search + 模板拼接)
|
|
7
|
+
4. 回流:create_prd/submit_return
|
|
8
|
+
5. 回填:PATCH /trigger/{tid}(mcp_tools_called/knowledge_used)
|
|
9
|
+
|
|
10
|
+
用法:
|
|
11
|
+
python poller.py --config ../mcp_config.json --interval 10
|
|
12
|
+
|
|
13
|
+
注:完整版需要接 LLM(qwen-plus)生成 PRD/原型。当前是简化版(rag_search + 模板),
|
|
14
|
+
验证闭环链路。接 LLM 后把 _execute 里的模板拼接换成 cap.mcp.call("ask_corpus",...) 生成。
|
|
15
|
+
"""
|
|
16
|
+
import json
|
|
17
|
+
import time
|
|
18
|
+
import urllib.request
|
|
19
|
+
import urllib.error
|
|
20
|
+
import argparse
|
|
21
|
+
import os
|
|
22
|
+
import sys
|
|
23
|
+
from pathlib import Path
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def _load_config(config_path: str) -> dict:
|
|
27
|
+
with open(config_path, encoding="utf-8") as f:
|
|
28
|
+
return json.load(f)
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def _get_token(cfg: dict) -> str:
|
|
32
|
+
for srv in (cfg.get("mcpServers") or {}).values():
|
|
33
|
+
return srv.get("token", "")
|
|
34
|
+
return ""
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def _get_pid(cfg: dict) -> str:
|
|
38
|
+
for srv in (cfg.get("mcpServers") or {}).values():
|
|
39
|
+
env = srv.get("env") or {}
|
|
40
|
+
return env.get("WLKJ_PROJECT_ID", "")
|
|
41
|
+
return ""
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def _get_base(cfg: dict) -> str:
|
|
45
|
+
ep = cfg.get("return_endpoint") or {}
|
|
46
|
+
url = ep.get("url", "")
|
|
47
|
+
# 从 return_endpoint.url 提取 base(/api/projects 前面那段)
|
|
48
|
+
if "/api/" in url:
|
|
49
|
+
return url.split("/api/")[0]
|
|
50
|
+
for srv in (cfg.get("mcpServers") or {}).values():
|
|
51
|
+
u = srv.get("url", "")
|
|
52
|
+
if "/mcp" in u:
|
|
53
|
+
return u.split("/mcp")[0]
|
|
54
|
+
return os.environ.get("WLKJ_BASE_URL", "http://10.89.7.120")
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def _api(base, method, path, token, body=None):
|
|
58
|
+
url = base + path
|
|
59
|
+
data = json.dumps(body).encode() if body else None
|
|
60
|
+
req = urllib.request.Request(url, data=data, method=method)
|
|
61
|
+
req.add_header("Content-Type", "application/json")
|
|
62
|
+
if token:
|
|
63
|
+
req.add_header("X-Engine-Token", token)
|
|
64
|
+
try:
|
|
65
|
+
with urllib.request.urlopen(req, timeout=30) as resp:
|
|
66
|
+
return resp.status, json.loads(resp.read().decode())
|
|
67
|
+
except urllib.error.HTTPError as e:
|
|
68
|
+
return e.code, json.loads(e.read().decode() or "{}")
|
|
69
|
+
except Exception as e:
|
|
70
|
+
return 0, {"error": str(e)}
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def _mcp_call(base, token, tool, args):
|
|
74
|
+
"""通过 MCP JSON-RPC 调工具。"""
|
|
75
|
+
body = {"jsonrpc": "2.0", "id": 1, "method": "tools/call",
|
|
76
|
+
"params": {"name": tool, "arguments": args}}
|
|
77
|
+
data = json.dumps(body).encode()
|
|
78
|
+
req = urllib.request.Request(base + "/mcp", data=data, method="POST")
|
|
79
|
+
req.add_header("Content-Type", "application/json")
|
|
80
|
+
req.add_header("X-MCP-Token", token)
|
|
81
|
+
try:
|
|
82
|
+
with urllib.request.urlopen(req, timeout=60) as resp:
|
|
83
|
+
r = json.loads(resp.read().decode())
|
|
84
|
+
text = r.get("result", {}).get("content", [{}])[0].get("text", "")
|
|
85
|
+
return json.loads(text) if text else {}
|
|
86
|
+
except Exception as e:
|
|
87
|
+
return {"error": str(e)}
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
def _execute(base, token, pid, command, args):
|
|
91
|
+
"""执行触发的工作流(简化版:rag_search 取知识 + 模板拼 PRD + learn 写回)。
|
|
92
|
+
完整版应接 LLM(ask_corpus)生成自然语言 PRD。"""
|
|
93
|
+
query = (args or {}).get("query", "")
|
|
94
|
+
tools_called = []
|
|
95
|
+
knowledge_used = []
|
|
96
|
+
|
|
97
|
+
# 1) rag_search 取知识
|
|
98
|
+
if query:
|
|
99
|
+
r = _mcp_call(base, token, "rag_search", {"query": query, "top_k": 8})
|
|
100
|
+
tools_called.append("rag_search")
|
|
101
|
+
knowledge_used = [it.get("entity_id", "") for it in r.get("items", [])[:5]]
|
|
102
|
+
knowledge_text = "\n".join(
|
|
103
|
+
"- %s (%s)" % (it.get("text", "")[:60], it.get("entity_id", "")[:30])
|
|
104
|
+
for it in r.get("items", [])[:8]
|
|
105
|
+
)
|
|
106
|
+
# 1b) get_learnings 查之前学到的(反哺)
|
|
107
|
+
lr = _mcp_call(base, token, "get_learnings", {"project_id": pid, "limit": 5})
|
|
108
|
+
tools_called.append("get_learnings")
|
|
109
|
+
learnings_text = "\n".join(
|
|
110
|
+
"- [%s] %s" % (l.get("category",""), (l.get("pattern","")[:60]))
|
|
111
|
+
for l in (lr.get("items") or [])[:5]
|
|
112
|
+
) or "(暂无学习记录)"
|
|
113
|
+
else:
|
|
114
|
+
knowledge_text = "(无关键词)"
|
|
115
|
+
learnings_text = ""
|
|
116
|
+
|
|
117
|
+
# 2) 根据 command 生成产出
|
|
118
|
+
if "prd" in command.lower():
|
|
119
|
+
prd_md = f"# {query} PRD(引擎自动生成)\n\n## 概述\n基于知识层自动生成的「{query}」需求文档。\n\n## 相关知识\n{knowledge_text}\n\n## 历史学习\n{learnings_text}\n\n## 功能\n(根据上述知识补充)"
|
|
120
|
+
# 回流
|
|
121
|
+
r = _mcp_call(base, token, "create_prd", {
|
|
122
|
+
"project_id": pid, "title": f"{query}PRD(引擎触发)", "content_md": prd_md, "status": "planning"
|
|
123
|
+
})
|
|
124
|
+
tools_called.append("create_prd")
|
|
125
|
+
# 学习写回:把这次的关键认知沉淀
|
|
126
|
+
_mcp_call(base, token, "learn", {"project_id": pid, "category": "rule",
|
|
127
|
+
"pattern": f"{query}: PRD已生成,涉及知识{len(knowledge_used)}条", "source": "wlkj-poller"})
|
|
128
|
+
tools_called.append("learn")
|
|
129
|
+
output = f"生成PRD: {query}PRD(引擎触发) + 学习沉淀"
|
|
130
|
+
elif "design" in command.lower():
|
|
131
|
+
html = f"<html><body><h1>{query}</h1><p>引擎自动生成原型</p></body></html>"
|
|
132
|
+
r = _mcp_call(base, token, "create_prototype", {
|
|
133
|
+
"project_id": pid, "feature": f"{query}(引擎触发)", "platform": "web", "html": html
|
|
134
|
+
})
|
|
135
|
+
tools_called.append("create_prototype")
|
|
136
|
+
output = f"生成原型: {query}(引擎触发)"
|
|
137
|
+
else:
|
|
138
|
+
# learn 写回
|
|
139
|
+
_mcp_call(base, token, "learn", {
|
|
140
|
+
"project_id": pid, "category": "rule", "pattern": f"{query}相关规则(引擎触发)",
|
|
141
|
+
"source": "wlkj-poller"
|
|
142
|
+
})
|
|
143
|
+
tools_called.append("learn")
|
|
144
|
+
output = f"执行命令: {command}"
|
|
145
|
+
|
|
146
|
+
return {"output": output, "tools": tools_called, "knowledge": knowledge_used}
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
def poll_once(cfg):
|
|
150
|
+
"""轮询一次:取 pending → 认领 → 执行 → 回填。"""
|
|
151
|
+
token = _get_token(cfg)
|
|
152
|
+
pid = _get_pid(cfg)
|
|
153
|
+
base = _get_base(cfg)
|
|
154
|
+
if not token or not pid:
|
|
155
|
+
print("[poller] 配置缺 token/project_id,跳过")
|
|
156
|
+
return
|
|
157
|
+
|
|
158
|
+
# 1) 查 pending
|
|
159
|
+
_, pending = _api(base, "GET", f"/api/projects/{pid}/trigger/pending", token)
|
|
160
|
+
items = pending.get("pending", [])
|
|
161
|
+
if not items:
|
|
162
|
+
return # 无待执行
|
|
163
|
+
|
|
164
|
+
for item in items[:1]: # 一次只处理 1 条
|
|
165
|
+
tid = item.get("triggerId")
|
|
166
|
+
cmd = item.get("command", "")
|
|
167
|
+
args = item.get("args", {})
|
|
168
|
+
print(f"[poller] 认领: {tid} {cmd} {args}")
|
|
169
|
+
|
|
170
|
+
# 2) claim
|
|
171
|
+
code, resp = _api(base, "POST", f"/api/projects/{pid}/trigger/workflow/{tid}/claim",
|
|
172
|
+
token, {"engine_id": "poller-001"})
|
|
173
|
+
if code != 200:
|
|
174
|
+
print(f"[poller] 认领失败({code}): {resp}")
|
|
175
|
+
continue
|
|
176
|
+
|
|
177
|
+
# 3) 执行
|
|
178
|
+
t0 = time.time()
|
|
179
|
+
try:
|
|
180
|
+
result = _execute(base, token, pid, cmd, args)
|
|
181
|
+
status = "success"
|
|
182
|
+
except Exception as e:
|
|
183
|
+
result = {"output": str(e), "tools": [], "knowledge": []}
|
|
184
|
+
status = "failed"
|
|
185
|
+
|
|
186
|
+
# 4) patch 回填
|
|
187
|
+
_api(base, "PATCH", f"/api/projects/{pid}/trigger/workflow/{tid}", token, {
|
|
188
|
+
"status": status,
|
|
189
|
+
"output_summary": result["output"],
|
|
190
|
+
"knowledge_used": result["knowledge"],
|
|
191
|
+
"mcp_tools_called": result["tools"],
|
|
192
|
+
"duration_sec": int(time.time() - t0),
|
|
193
|
+
})
|
|
194
|
+
print(f"[poller] 完成: {status} → {result['output']} (tools={result['tools']})")
|
|
195
|
+
|
|
196
|
+
|
|
197
|
+
def main():
|
|
198
|
+
parser = argparse.ArgumentParser(description="wlkj 引擎轮询器")
|
|
199
|
+
parser.add_argument("--config", default=str(Path(__file__).parent.parent.parent / "mcp_config.json"))
|
|
200
|
+
parser.add_argument("--interval", type=int, default=10, help="轮询间隔(秒)")
|
|
201
|
+
parser.add_argument("--once", action="store_true", help="只跑一次")
|
|
202
|
+
args = parser.parse_args()
|
|
203
|
+
|
|
204
|
+
cfg = _load_config(args.config)
|
|
205
|
+
print(f"[poller] 启动: project={_get_pid(cfg)} interval={args.interval}s")
|
|
206
|
+
print(f"[poller] 按 Ctrl+C 停止")
|
|
207
|
+
|
|
208
|
+
while True:
|
|
209
|
+
try:
|
|
210
|
+
poll_once(cfg)
|
|
211
|
+
except Exception as e:
|
|
212
|
+
print(f"[poller] 异常: {e}")
|
|
213
|
+
if args.once:
|
|
214
|
+
break
|
|
215
|
+
time.sleep(args.interval)
|
|
216
|
+
|
|
217
|
+
|
|
218
|
+
if __name__ == "__main__":
|
|
219
|
+
main()
|
|
Binary file
|
|
@@ -44,7 +44,8 @@ try:
|
|
|
44
44
|
)
|
|
45
45
|
except ImportError:
|
|
46
46
|
_ENV_SESSION_KEYS = (
|
|
47
|
-
("
|
|
47
|
+
("qoder-desktop", ("QODER_DESKTOP", "QODER_IDE")),
|
|
48
|
+
("qoder-work", ("QODER_SESSION_ID",)),
|
|
48
49
|
("codex", ("CODEX_SESSION_ID", "CODEX_THREAD_ID")),
|
|
49
50
|
("claude", ("CLAUDE_SESSION_ID", "CLAUDE_CODE_SESSION_ID")),
|
|
50
51
|
("cursor", ("CURSOR_SESSION_ID",)),
|
|
@@ -57,6 +57,9 @@ DISPATCH = {
|
|
|
57
57
|
# ── 需求 / PRD ──
|
|
58
58
|
'req': 'domain/requirement/req.py',
|
|
59
59
|
'archive-prd': 'domain/requirement/archive_prd.py',
|
|
60
|
+
# ── 回流平台 (引擎产出 → 平台 AI回流Tab) ──
|
|
61
|
+
# 用法: wlkj.py return prd <路径> <标题> | return prototype <路径> <功能名> [--platform web]
|
|
62
|
+
'return': 'domain/integration/return_to_platform.py',
|
|
60
63
|
# ── 设计 ──
|
|
61
64
|
'fill-prototype': 'domain/design/fill_prototype.py',
|
|
62
65
|
'design-doc': 'domain/design/gen_design_doc.py',
|
|
@@ -118,6 +121,7 @@ def _usage():
|
|
|
118
121
|
' 同步: sync pull|push (team_sync.py)',
|
|
119
122
|
' 需求: req <子命令> (req.py, 需求生命周期)',
|
|
120
123
|
' archive-prd (PRD 归档)',
|
|
124
|
+
' 回流平台: return prd|prototype <文件> <标题> [--platform web] (PRD/原型回流到平台 AI回流Tab)',
|
|
121
125
|
' 设计: fill-prototype <tpl> (真实数据预填原型)',
|
|
122
126
|
' design-doc (生成 DESIGN.md)',
|
|
123
127
|
' 评估/学习: eval <prd.md> (eval_prd.py, EVA 评分)',
|
|
@@ -190,7 +190,7 @@ class ZentaoMCPServer(BaseMCPServer):
|
|
|
190
190
|
# fail-closed: 共享账号下无法判定归属 → 拒绝写, 防改错队友
|
|
191
191
|
return ('🚫 未识别禅道身份, 无法校验归属, 已拒绝写操作(防改错队友)。\n'
|
|
192
192
|
' 修复: /wl-init 配置禅道账号, 或 member.json 加 "zentao_account"。')
|
|
193
|
-
r = _api('GET', '/%
|
|
193
|
+
r = _api('GET', '/%s/%s' % ({'task': 'tasks', 'bug': 'bugs', 'story': 'stories'}.get(kind, kind + 's'), item_id))
|
|
194
194
|
if r.status_code not in (200, 201):
|
|
195
195
|
return None # 查询失败不阻塞 (禅道权限兜底)
|
|
196
196
|
data = r.json()
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
@@ -72,7 +72,11 @@ class RemoteHttpTransport:
|
|
|
72
72
|
body["params"] = params
|
|
73
73
|
|
|
74
74
|
data = json.dumps(body, ensure_ascii=False).encode("utf-8")
|
|
75
|
-
|
|
75
|
+
# 修复双重 /mcp:若 url 已含 /mcp 后缀则直接用,否则拼接。
|
|
76
|
+
# 历史问题: registry 配的 url 已是 http://host/mcp, 这里又拼 /mcp → /mcp/mcp → 404。
|
|
77
|
+
# 现在两端都安全: url 配 base 或带 /mcp 都能落到正确的 endpoint。
|
|
78
|
+
low = self._url.lower()
|
|
79
|
+
endpoint = self._url if low.endswith("/mcp") else (self._url + "/mcp")
|
|
76
80
|
|
|
77
81
|
req = urllib.request.Request(
|
|
78
82
|
endpoint,
|
|
@@ -81,6 +85,8 @@ class RemoteHttpTransport:
|
|
|
81
85
|
method="POST",
|
|
82
86
|
)
|
|
83
87
|
if self._token:
|
|
88
|
+
# 平台 MCP 校验 X-MCP-Token(项目级令牌);同时带 Authorization Bearer 兼容标准 MCP 客户端
|
|
89
|
+
req.add_header("X-MCP-Token", self._token)
|
|
84
90
|
req.add_header("Authorization", "Bearer %s" % self._token)
|
|
85
91
|
|
|
86
92
|
try:
|
|
Binary file
|
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
"""present_board.py — 知识层富展示看板(cap.present.widget / 落盘降级)。
|
|
3
|
+
|
|
4
|
+
子命令(数据走 MCP,HTML 自包含,复用 capability.present_html.present):
|
|
5
|
+
impact — get_impact 影响传播树(分层 upstream/downstream 可折叠)
|
|
6
|
+
coverage — coverage_matrix 覆盖热力图(功能×测试 有/无)
|
|
7
|
+
feature — feature_overview 功能画像卡(端点+按钮+用例+页面+PRD)
|
|
8
|
+
|
|
9
|
+
用法:
|
|
10
|
+
python present_board.py impact --entity handleExport --project-id <UUID>
|
|
11
|
+
python present_board.py coverage --project-id <UUID>
|
|
12
|
+
python present_board.py feature --feature 资产管理 --project-id <UUID>
|
|
13
|
+
|
|
14
|
+
QoderWork 桌面端 → HTML 嵌入对话流;CLI/IDE → 落盘 workspace/members/{dev}/journal/。
|
|
15
|
+
"""
|
|
16
|
+
import argparse
|
|
17
|
+
import json
|
|
18
|
+
import os
|
|
19
|
+
import sys
|
|
20
|
+
from pathlib import Path
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def _resolve_repo():
|
|
24
|
+
env = os.environ.get("QODER_REPO") or os.environ.get("WLKJ_REPO")
|
|
25
|
+
if env:
|
|
26
|
+
return Path(env)
|
|
27
|
+
p = Path(__file__).resolve()
|
|
28
|
+
for parent in p.parents:
|
|
29
|
+
if (parent / ".qoder").is_dir() or (parent / "packages").is_dir():
|
|
30
|
+
return parent
|
|
31
|
+
return p.parent
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def _cap():
|
|
35
|
+
repo = _resolve_repo()
|
|
36
|
+
sd = str(repo / ".qoder" / "scripts")
|
|
37
|
+
if sd not in sys.path:
|
|
38
|
+
sys.path.insert(0, sd)
|
|
39
|
+
from capability.registry import resolve
|
|
40
|
+
return resolve(), repo
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def _unpack(r):
|
|
44
|
+
"""cap.mcp.call 返回 ToolResult(.text 是 JSON 字符串),解包成 dict。"""
|
|
45
|
+
if isinstance(r, dict):
|
|
46
|
+
return r
|
|
47
|
+
if getattr(r, "is_error", False):
|
|
48
|
+
return {"error": getattr(r, "text", str(r))}
|
|
49
|
+
txt = getattr(r, "text", None)
|
|
50
|
+
if txt is None:
|
|
51
|
+
return r
|
|
52
|
+
try:
|
|
53
|
+
return json.loads(txt)
|
|
54
|
+
except Exception:
|
|
55
|
+
return {"raw": txt}
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
_CSS = """
|
|
59
|
+
body{font-family:-apple-system,'Segoe UI',sans-serif;margin:0;padding:20px;background:#f7f8fa;color:#1f2329}
|
|
60
|
+
h1{font-size:20px;margin:0 0 8px} h3{font-size:14px;margin:12px 0 6px;color:#4e5969}
|
|
61
|
+
p{color:#86909c;font-size:13px;margin:4px 0}
|
|
62
|
+
details{background:#fff;border:1px solid #e5e6eb;border-radius:6px;padding:8px 12px;margin:6px 0}
|
|
63
|
+
summary{cursor:pointer;font-weight:600;font-size:13px}
|
|
64
|
+
ul{margin:6px 0;padding-left:20px} li{font-size:13px;line-height:1.8} small{color:#86909c}
|
|
65
|
+
.grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(160px,1fr));gap:8px}
|
|
66
|
+
.cell{padding:10px;border-radius:6px;font-size:13px;text-align:center}
|
|
67
|
+
.covered{background:#e8ffea;color:#00b42a;border:1px solid #b9f0c6}
|
|
68
|
+
.gap{background:#fff2f0;color:#f53f3f;border:1px solid #ffd4d4}
|
|
69
|
+
.cards{display:grid;grid-template-columns:repeat(auto-fill,minmax(240px,1fr));gap:12px}
|
|
70
|
+
.card{background:#fff;border:1px solid #e5e6eb;border-radius:8px;padding:12px}
|
|
71
|
+
.card h3{margin-top:0}
|
|
72
|
+
"""
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def _wrap(title, body):
|
|
76
|
+
return ("<!DOCTYPE html><html><head><meta charset='utf-8'>"
|
|
77
|
+
"<meta name='viewport' content='width=device-width,initial-scale=1'>"
|
|
78
|
+
"<title>%s</title><style>%s</style></head><body>%s</body></html>"
|
|
79
|
+
% (title, _CSS, body))
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def cmd_impact(args):
|
|
83
|
+
cap, repo = _cap()
|
|
84
|
+
impact = _unpack(cap.mcp.call("get_impact", {"project_id": args.project_id or "",
|
|
85
|
+
"entity": args.entity, "depth": args.depth}))
|
|
86
|
+
from capability.present_html import present
|
|
87
|
+
if "error" in impact:
|
|
88
|
+
body = "<h1>影响分析 · %s</h1><p class='gap'>取数失败: %s</p>" % (args.entity, impact["error"][:200])
|
|
89
|
+
present(_wrap("影响分析-" + args.entity, body), name="impact-" + args.entity[:20], repo_root=repo)
|
|
90
|
+
return
|
|
91
|
+
sections = []
|
|
92
|
+
for direction, label in [("upstream", "↑ 谁依赖我(改动影响范围)"),
|
|
93
|
+
("downstream", "↓ 我依赖谁")]:
|
|
94
|
+
layers = impact.get(direction) or []
|
|
95
|
+
items_html = ""
|
|
96
|
+
for i, lay in enumerate(layers):
|
|
97
|
+
its = lay.get("items", []) if isinstance(lay, dict) else []
|
|
98
|
+
lis = "".join("<li>%s <small>%s</small></li>"
|
|
99
|
+
% (it.get("id", "?"), it.get("type", "")) for it in its)
|
|
100
|
+
items_html += "<details open><summary>第%d层 (%d)</summary><ul>%s</ul></details>" % (i + 1, len(its), lis)
|
|
101
|
+
sections.append("<h3>%s</h3>%s" % (label, items_html))
|
|
102
|
+
totals = impact.get("totals", {}) or {}
|
|
103
|
+
bytype = impact.get("byType", {}) or {}
|
|
104
|
+
body = ("<h1>影响分析 · %s</h1><p>节点 %s · 类型分布 %s · depth %d</p>"
|
|
105
|
+
% (args.entity, totals.get("nodes", 0),
|
|
106
|
+
json.dumps(bytype, ensure_ascii=False), args.depth)) + "".join(sections)
|
|
107
|
+
present(_wrap("影响分析-" + args.entity, body),
|
|
108
|
+
name="impact-" + args.entity[:20], repo_root=repo)
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
def cmd_coverage(args):
|
|
112
|
+
cap, repo = _cap()
|
|
113
|
+
cov = _unpack(cap.mcp.call("coverage_matrix", {"project_id": args.project_id or ""}))
|
|
114
|
+
from capability.present_html import present
|
|
115
|
+
if "error" in cov:
|
|
116
|
+
present(_wrap("覆盖矩阵", "<h1>测试覆盖矩阵</h1><p class='gap'>取数失败: %s</p>" % cov["error"][:200]),
|
|
117
|
+
name="coverage", repo_root=repo)
|
|
118
|
+
return
|
|
119
|
+
items = cov.get("items") or cov.get("matrix") or cov.get("features") or []
|
|
120
|
+
cells = "".join(_cov_cell(it) for it in items) or "<p>无覆盖数据</p>"
|
|
121
|
+
covered = sum(1 for it in items if it.get("has_test") or it.get("covered"))
|
|
122
|
+
body = ("<h1>测试覆盖矩阵</h1><p>共 %d 项 · 已覆盖 %d · 缺口 %d</p>"
|
|
123
|
+
% (len(items), covered, len(items) - covered)) + '<div class="grid">%s</div>' % cells
|
|
124
|
+
present(_wrap("覆盖矩阵", body), name="coverage", repo_root=repo)
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
def _cov_cell(it):
|
|
128
|
+
name = it.get("feature") or it.get("name") or it.get("canonical") or "?"
|
|
129
|
+
has = it.get("has_test") or it.get("covered")
|
|
130
|
+
cls = "covered" if has else "gap"
|
|
131
|
+
return '<div class="cell %s">%s</div>' % (cls, name)
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
def cmd_feature(args):
|
|
135
|
+
cap, repo = _cap()
|
|
136
|
+
feat = _unpack(cap.mcp.call("feature_overview", {"project_id": args.project_id or "",
|
|
137
|
+
"feature": args.feature}))
|
|
138
|
+
from capability.present_html import present
|
|
139
|
+
if "error" in feat:
|
|
140
|
+
present(_wrap("功能画像-" + args.feature, "<h1>功能画像 · %s</h1><p class='gap'>取数失败: %s</p>" % (args.feature, feat["error"][:200])),
|
|
141
|
+
name="feature-" + args.feature[:20], repo_root=repo)
|
|
142
|
+
return
|
|
143
|
+
cards = []
|
|
144
|
+
for key, label in [("endpoints", "端点"), ("buttons", "按钮"),
|
|
145
|
+
("tests", "测试用例"), ("pages", "页面"), ("prds", "PRD")]:
|
|
146
|
+
lst = feat.get(key) or []
|
|
147
|
+
if not lst:
|
|
148
|
+
continue
|
|
149
|
+
lis = "".join("<li>%s</li>" % x for x in (lst if isinstance(lst, list) else [str(lst)])[:20])
|
|
150
|
+
cards.append('<div class="card"><h3>%s (%d)</h3><ul>%s</ul></div>' % (label, len(lst), lis))
|
|
151
|
+
complete = "完整" if len(cards) >= 4 else "部分"
|
|
152
|
+
body = ("<h1>功能画像 · %s</h1><p>覆盖 %d 维 · %s画像</p>"
|
|
153
|
+
% (args.feature, len(cards), complete)) + '<div class="cards">%s</div>' % "".join(cards)
|
|
154
|
+
present(_wrap("功能画像-" + args.feature, body),
|
|
155
|
+
name="feature-" + args.feature[:20], repo_root=repo)
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
def main():
|
|
159
|
+
ap = argparse.ArgumentParser(description="知识层富展示看板")
|
|
160
|
+
sub = ap.add_subparsers(dest="cmd", required=True)
|
|
161
|
+
p_imp = sub.add_parser("impact")
|
|
162
|
+
p_imp.add_argument("--entity", required=True)
|
|
163
|
+
p_imp.add_argument("--project-id")
|
|
164
|
+
p_imp.add_argument("--depth", type=int, default=2)
|
|
165
|
+
p_cov = sub.add_parser("coverage")
|
|
166
|
+
p_cov.add_argument("--project-id")
|
|
167
|
+
p_fea = sub.add_parser("feature")
|
|
168
|
+
p_fea.add_argument("--feature", required=True)
|
|
169
|
+
p_fea.add_argument("--project-id")
|
|
170
|
+
args = ap.parse_args()
|
|
171
|
+
if args.cmd == "impact":
|
|
172
|
+
cmd_impact(args)
|
|
173
|
+
elif args.cmd == "coverage":
|
|
174
|
+
cmd_coverage(args)
|
|
175
|
+
elif args.cmd == "feature":
|
|
176
|
+
cmd_feature(args)
|
|
177
|
+
|
|
178
|
+
|
|
179
|
+
if __name__ == "__main__":
|
|
180
|
+
main()
|
|
@@ -68,6 +68,16 @@
|
|
|
68
68
|
]
|
|
69
69
|
}
|
|
70
70
|
],
|
|
71
|
+
"PreToolUse": [
|
|
72
|
+
{
|
|
73
|
+
"matcher": "Write|Edit|MultiEdit",
|
|
74
|
+
"hooks": [
|
|
75
|
+
{
|
|
76
|
+
"command": "python .qoder/hooks/pre-tool-use.py || python3 .qoder/hooks/pre-tool-use.py"
|
|
77
|
+
}
|
|
78
|
+
]
|
|
79
|
+
}
|
|
80
|
+
],
|
|
71
81
|
"PostToolUse": [
|
|
72
82
|
{
|
|
73
83
|
"matcher": "*",
|
package/templates/root/AGENTS.md
CHANGED
|
@@ -190,21 +190,20 @@ Style priority: Code > PDF > Default
|
|
|
190
190
|
**独立性**:每个模块认文件不认流程。缺上游 → 手动喂个契约文件,或用自然语言 +
|
|
191
191
|
`context_pack.py` 让 AI 自检索补全(见 `.qoder/contracts/fallback.md`)。
|
|
192
192
|
|
|
193
|
-
## Qoder
|
|
193
|
+
## Qoder 系列兼容(Desktop / QoderWork)
|
|
194
194
|
|
|
195
195
|
This pipeline works on Windows + macOS, across the **whole Qoder family**.
|
|
196
196
|
Each product loads rules from a different carrier — keep them in sync:
|
|
197
197
|
|
|
198
|
-
|
|
|
199
|
-
|
|
200
|
-
| Qoder IDE
|
|
201
|
-
|
|
|
202
|
-
| Qoder CLI (qodercli) | 本文件 AGENTS.md | 自然语言 -> scripts |
|
|
203
|
-
| QoderWork 桌面 Agent | 安装 .qoder/skills/ 下的技能 | 自然语言 -> skills |
|
|
198
|
+
| 产品 | 定位 | 规则载体 | 命令载体 |
|
|
199
|
+
|-----------|------|---------|---------|
|
|
200
|
+
| Qoder Desktop/IDE 1.0(含 Quest 模式)| 主开发宿主 | .qoder/rules/wl-pipeline.md + .qoder/skills/ + .qoder/agents/ | .qoder/commands/wl-*.md;Quest 走自然语言→skills |
|
|
201
|
+
| QoderWork 桌面 Agent | 办公(非开发者)| 安装 .qoder/skills/ 下的技能 | 自然语言→skills;~/.qoderwork/ 投影 |
|
|
204
202
|
|
|
205
|
-
> 本流水线聚焦 Qoder
|
|
206
|
-
>
|
|
207
|
-
> 全系列触达;QoderWork 环境也可直接调 MCP 工具。
|
|
203
|
+
> 本流水线聚焦 Qoder 系列(主开发 Desktop + 办公 QoderWork),载体功能等价。知识图谱 17 个能力
|
|
204
|
+
> (影响分析/覆盖矩阵/功能画像/业务流程/多跳遍历…)通过统一 CLI
|
|
205
|
+
> `python .qoder/scripts/domain/kg/kg.py` 全系列触达;Desktop/QoderWork 环境也可直接调 MCP 工具。
|
|
206
|
+
> (Qoder CLI / Cloud Agents 后续按需对接。)
|
|
208
207
|
|
|
209
208
|
## QoderWork Integration: Optional Enhancement Only (Progressive Enhancement)
|
|
210
209
|
|