@hupan56/wlkj 3.3.15 → 3.3.16
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/package.json +1 -1
- package/templates/qoder/scripts/capability/registry_mcp.py +4 -10
- package/templates/qoder/scripts/capability/smoke_test_report.json.new +94 -0
- package/templates/qoder/scripts/deployment/setup/install_qoderwork.py +1 -1
- package/templates/qoder/scripts/domain/integration/return_to_platform.py +2 -4
- package/templates/qoder/scripts/domain/integration/spec_upload.py +4 -4
- 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/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/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/switch_project.py +2 -2
- package/templates/qoder/scripts/domain/kg/sync_repowiki.py +109 -0
- package/templates/qoder/scripts/domain/task/wlkj_panel.py +14 -169
- package/templates/qoder/scripts/engine/poller.py +219 -0
- package/templates/qoder/scripts/orchestration/wlkj.py +0 -106
- package/templates/qoder/settings.json +0 -8
|
@@ -51,118 +51,9 @@ from foundation.integrations.zentao_client import ZentaoClient
|
|
|
51
51
|
# ============================================================
|
|
52
52
|
|
|
53
53
|
def _zentao_creds():
|
|
54
|
-
"""
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
① 云平台 per-user 凭据(Option A:get_zentao_creds)—— account 已知(preset)
|
|
58
|
-
② 本地 .qoder/config.yaml + secrets/zentao.env —— account 走 member.json
|
|
59
|
-
云凭据优先:demo 机器在内网可达禅道,平台凭据比本地文件更可能是最新配的。
|
|
60
|
-
凭据全无 → (None, '')。
|
|
61
|
-
"""
|
|
62
|
-
c = _cloud_zentao_creds()
|
|
63
|
-
if c and c.get('ok') and c.get('url') and c.get('account') and c.get('password'):
|
|
64
|
-
try:
|
|
65
|
-
return ZentaoClient(c['url'], c['account'], c['password']), c['account']
|
|
66
|
-
except Exception:
|
|
67
|
-
pass
|
|
68
|
-
return ZentaoClient.from_config(), ''
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
def _cloud_my_panel():
|
|
72
|
-
"""云优先:调平台 get_my_zentao_panel(凭据走平台 per-user 配置,不出平台)。
|
|
73
|
-
|
|
74
|
-
直连云 /mcp JSON-RPC(读 .qoder/mcp_config.json 的 token+url)。
|
|
75
|
-
Returns: {ok:True, tasks, bugs, stories, account} 或 None(云不可达/工具失败)。
|
|
76
|
-
云返 {ok:False,error}(未配置/登录失败)→ 返该 dict(调用方据此提示+回退本地)。
|
|
77
|
-
"""
|
|
78
|
-
import json as _json
|
|
79
|
-
import urllib.request as _u
|
|
80
|
-
import urllib.error as _ue
|
|
81
|
-
# 定位 mcp_config.json(switch_project 写在 .qoder/)
|
|
82
|
-
cfg_candidates = []
|
|
83
|
-
try:
|
|
84
|
-
from foundation.core.paths import get_repo_root
|
|
85
|
-
cfg_candidates.append(os.path.join(str(get_repo_root()), '.qoder', 'mcp_config.json'))
|
|
86
|
-
except Exception:
|
|
87
|
-
pass
|
|
88
|
-
cfg_candidates.append(os.path.join(_SCRIPTS, '..', 'mcp_config.json'))
|
|
89
|
-
token, url = '', ''
|
|
90
|
-
for cp in cfg_candidates:
|
|
91
|
-
try:
|
|
92
|
-
with open(cp, encoding='utf-8') as f:
|
|
93
|
-
cfg = _json.load(f)
|
|
94
|
-
srv = (cfg.get('mcpServers') or {}).get('wlinkj-knowledge') or {}
|
|
95
|
-
token = srv.get('token', '')
|
|
96
|
-
url = (srv.get('url') or '').rstrip('/')
|
|
97
|
-
if token and url:
|
|
98
|
-
break
|
|
99
|
-
except Exception:
|
|
100
|
-
continue
|
|
101
|
-
if not token or not url:
|
|
102
|
-
return None # 云未绑定(switch_project 没跑)→ 回退本地
|
|
103
|
-
endpoint = url if url.endswith('/mcp') else url + '/mcp'
|
|
104
|
-
body = _json.dumps({"jsonrpc": "2.0", "id": 1, "method": "tools/call",
|
|
105
|
-
"params": {"name": "get_my_zentao_panel", "arguments": {}}}).encode('utf-8')
|
|
106
|
-
req = _u.Request(endpoint, data=body, method='POST')
|
|
107
|
-
req.add_header('Content-Type', 'application/json')
|
|
108
|
-
req.add_header('X-MCP-Token', token)
|
|
109
|
-
try:
|
|
110
|
-
with _u.urlopen(req, timeout=30) as resp:
|
|
111
|
-
r = _json.loads(resp.read().decode('utf-8', errors='replace'))
|
|
112
|
-
except Exception:
|
|
113
|
-
return None # 网络不通 → 回退本地
|
|
114
|
-
try:
|
|
115
|
-
text = r.get('result', {}).get('content', [{}])[0].get('text', '')
|
|
116
|
-
data = _json.loads(text) if text else {}
|
|
117
|
-
except Exception:
|
|
118
|
-
return None
|
|
119
|
-
return data # {ok:True/False, ...}
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
def _cloud_zentao_creds():
|
|
123
|
-
"""Option A:从平台取禅道凭据(url/account/password 解密)。
|
|
124
|
-
|
|
125
|
-
用途:云到禅道被防火墙挡(my-panel 登录失败)时,本机(demo 机器,内网可达)
|
|
126
|
-
用平台凭据直连禅道拉真实数据。凭据随绑定 token 取本人配置(同信任级)。
|
|
127
|
-
Returns: {ok:True, url, account, password} 或 None(云不可达/未配置)。
|
|
128
|
-
"""
|
|
129
|
-
import json as _json
|
|
130
|
-
import urllib.request as _u
|
|
131
|
-
cfg_candidates = []
|
|
132
|
-
try:
|
|
133
|
-
from foundation.core.paths import get_repo_root
|
|
134
|
-
cfg_candidates.append(os.path.join(str(get_repo_root()), '.qoder', 'mcp_config.json'))
|
|
135
|
-
except Exception:
|
|
136
|
-
pass
|
|
137
|
-
cfg_candidates.append(os.path.join(_SCRIPTS, '..', 'mcp_config.json'))
|
|
138
|
-
token, url = '', ''
|
|
139
|
-
for cp in cfg_candidates:
|
|
140
|
-
try:
|
|
141
|
-
with open(cp, encoding='utf-8') as f:
|
|
142
|
-
cfg = _json.load(f)
|
|
143
|
-
srv = (cfg.get('mcpServers') or {}).get('wlinkj-knowledge') or {}
|
|
144
|
-
token = srv.get('token', '')
|
|
145
|
-
url = (srv.get('url') or '').rstrip('/')
|
|
146
|
-
if token and url:
|
|
147
|
-
break
|
|
148
|
-
except Exception:
|
|
149
|
-
continue
|
|
150
|
-
if not token or not url:
|
|
151
|
-
return None
|
|
152
|
-
endpoint = url if url.endswith('/mcp') else url + '/mcp'
|
|
153
|
-
body = _json.dumps({"jsonrpc": "2.0", "id": 1, "method": "tools/call",
|
|
154
|
-
"params": {"name": "get_zentao_creds", "arguments": {}}}).encode('utf-8')
|
|
155
|
-
req = _u.Request(endpoint, data=body, method='POST')
|
|
156
|
-
req.add_header('Content-Type', 'application/json')
|
|
157
|
-
req.add_header('X-MCP-Token', token)
|
|
158
|
-
try:
|
|
159
|
-
with _u.urlopen(req, timeout=15) as resp:
|
|
160
|
-
r = _json.loads(resp.read().decode('utf-8', errors='replace'))
|
|
161
|
-
text = r.get('result', {}).get('content', [{}])[0].get('text', '')
|
|
162
|
-
data = _json.loads(text) if text else {}
|
|
163
|
-
return data if data.get('ok') else None
|
|
164
|
-
except Exception:
|
|
165
|
-
return None
|
|
54
|
+
"""构建禅道客户端。凭据不全返回 None。
|
|
55
|
+
url 来自 .qoder/config.yaml (团队共享); user/password 来自 secrets/zentao.env (个人)。"""
|
|
56
|
+
return ZentaoClient.from_config()
|
|
166
57
|
|
|
167
58
|
|
|
168
59
|
def _resolve_my_account(developer):
|
|
@@ -468,72 +359,26 @@ def fetch_workbench(include_closed=False, kind=None):
|
|
|
468
359
|
"""
|
|
469
360
|
developer = get_developer()
|
|
470
361
|
|
|
471
|
-
#
|
|
472
|
-
|
|
473
|
-
# 未配置/登录失败 → 返回 {ok:False} 提示用户去平台配置,并回退本地(本地配了仍可用)。
|
|
474
|
-
# 云不可达(返回 None) → 静默回退本地全流程。
|
|
475
|
-
cloud = _cloud_my_panel()
|
|
476
|
-
if isinstance(cloud, dict):
|
|
477
|
-
# 云可达 → 云的判断权威(平台是凭据源)。
|
|
478
|
-
if cloud.get('ok'):
|
|
479
|
-
tasks = cloud.get('tasks') or []
|
|
480
|
-
bugs = cloud.get('bugs') or []
|
|
481
|
-
stories = cloud.get('stories') or []
|
|
482
|
-
# 按 kind 裁剪(与本地一致)
|
|
483
|
-
if kind == 'tasks':
|
|
484
|
-
bugs, stories = [], []
|
|
485
|
-
elif kind == 'bugs':
|
|
486
|
-
tasks, stories = [], []
|
|
487
|
-
elif kind == 'stories':
|
|
488
|
-
tasks, bugs = [], []
|
|
489
|
-
tasks, bugs, stories = _filter_live(tasks, bugs, stories, include_closed)
|
|
490
|
-
# 云路径:数据已平台聚合,归一化即可(进度检测需本地 client,云模式跳过给默认值)
|
|
491
|
-
norm_tasks = [_norm_task(t) for t in tasks]
|
|
492
|
-
norm_bugs = [_norm_bug(b) for b in bugs]
|
|
493
|
-
norm_stories = [_norm_story(s) for s in stories]
|
|
494
|
-
for it in norm_tasks + norm_bugs + norm_stories:
|
|
495
|
-
it['progress'] = {'prd': False, 'spec': 'none', 'code': False}
|
|
496
|
-
data = {'account': cloud.get('account', ''), 'developer': developer,
|
|
497
|
-
'current_task': None, 'tasks': norm_tasks,
|
|
498
|
-
'bugs': norm_bugs, 'stories': norm_stories}
|
|
499
|
-
meta = {'fetched_at': datetime.now().isoformat(timespec='seconds'),
|
|
500
|
-
'source': 'cloud',
|
|
501
|
-
'counts': {'tasks': len(norm_tasks), 'bugs': len(norm_bugs),
|
|
502
|
-
'stories': len(norm_stories)},
|
|
503
|
-
'include_closed': include_closed}
|
|
504
|
-
return {'ok': True, 'data': data, 'meta': meta}, 0
|
|
505
|
-
# 云可达但失败(未配置/登录失败/防火墙)→ 记下云诊断,回退本地。
|
|
506
|
-
# 回退价值:云到禅道常被防火墙挡,但本机(demo 机器,内网)可能直连可达。
|
|
507
|
-
# 本地也失败时,把云诊断一起带出(平台有凭据但云连不上 + 本地无凭据)。
|
|
508
|
-
cloud_error = cloud.get('error', '禅道不可用')
|
|
509
|
-
else:
|
|
510
|
-
cloud_error = None # 云不可达(switch_project 没跑/平台宕机)→ 静默回退本地
|
|
511
|
-
|
|
512
|
-
# ① 凭据(云平台优先 Option A,回退本地文件)
|
|
513
|
-
client, preset_account = _zentao_creds()
|
|
362
|
+
# ① 凭据
|
|
363
|
+
client = _zentao_creds()
|
|
514
364
|
if client is None:
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
base_msg += '(云 my-panel 失败:%s)' % cloud_error
|
|
519
|
-
return {'ok': False, 'error': base_msg}, 3
|
|
365
|
+
return {'ok': False,
|
|
366
|
+
'error': '禅道凭据未配置。请检查 .qoder/config.yaml 的 zentao.url, '
|
|
367
|
+
'及 workspace/members/<我>/.private/secrets/zentao.env 的账号密码。'}, 3
|
|
520
368
|
|
|
521
369
|
ok, msg = client.credentials_ok()
|
|
522
370
|
if not ok:
|
|
523
371
|
return {'ok': False, 'error': '禅道凭据不完整: %s' % msg}, 3
|
|
524
372
|
|
|
525
|
-
# ②
|
|
373
|
+
# ② 内网连通
|
|
526
374
|
ok, msg = client.check_intranet()
|
|
527
375
|
if not ok:
|
|
528
|
-
return {'ok': False, 'error': '
|
|
376
|
+
return {'ok': False, 'error': '连不上禅道内网: %s' % msg}, 5
|
|
529
377
|
|
|
530
|
-
# ③
|
|
531
|
-
if
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
my_account, err = _resolve_my_account(developer) if developer else (None, '未识别当前开发者身份。')
|
|
535
|
-
if not my_account:
|
|
536
|
-
return {'ok': False, 'error': err}, 3
|
|
378
|
+
# ③ 归属
|
|
379
|
+
my_account, err = _resolve_my_account(developer) if developer else (None, '未识别当前开发者身份。')
|
|
380
|
+
if not my_account:
|
|
381
|
+
return {'ok': False, 'error': err}, 3
|
|
537
382
|
|
|
538
383
|
# ④ 收集 (三域并行, 按 kind 决定取哪几域)
|
|
539
384
|
from concurrent.futures import ThreadPoolExecutor
|
|
@@ -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 "http://127.0.0.1:10010"
|
|
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()
|
|
@@ -47,17 +47,6 @@ DISPATCH = {
|
|
|
47
47
|
'kg-incremental': 'domain/kg/build/kg_incremental.py',
|
|
48
48
|
'kg-semantic': 'domain/kg/graph/kg_semantic.py',
|
|
49
49
|
'semantic': 'domain/kg/graph/kg_semantic.py',
|
|
50
|
-
# ★ MCP 名别名(AI 常把 MCP 工具名当 wlkj.py 命令调 → 未知命令。归一化后两边名都认)
|
|
51
|
-
'search_code': 'domain/kg/search/search_index.py',
|
|
52
|
-
'search_api': 'domain/kg/search/search_index.py',
|
|
53
|
-
'search_prds': 'domain/kg/search/search_index.py',
|
|
54
|
-
'search_field': 'domain/kg/search/search_index.py',
|
|
55
|
-
'search_style': 'domain/kg/search/search_index.py',
|
|
56
|
-
'rag_search': 'domain/kg/graph/kg_semantic.py',
|
|
57
|
-
'ask_corpus': 'domain/kg/graph/kg_semantic.py',
|
|
58
|
-
'context_pack': 'domain/kg/search/context_pack.py',
|
|
59
|
-
'context_360': 'domain/kg/search/context_pack.py',
|
|
60
|
-
'get_impact': 'domain/kg/search/search_index.py',
|
|
61
50
|
'style': 'domain/kg/build/build_style_index.py',
|
|
62
51
|
# ── 任务 / 同步 ──
|
|
63
52
|
'task': 'domain/task/task.py',
|
|
@@ -71,7 +60,6 @@ DISPATCH = {
|
|
|
71
60
|
# ── 回流平台 (引擎产出 → 平台 AI回流Tab) ──
|
|
72
61
|
# 用法: wlkj.py return prd <路径> <标题> | return prototype <路径> <功能名> [--platform web]
|
|
73
62
|
'return': 'domain/integration/return_to_platform.py',
|
|
74
|
-
'spec-upload': 'domain/integration/spec_upload.py',
|
|
75
63
|
# ── 设计 ──
|
|
76
64
|
'fill-prototype': 'domain/design/fill_prototype.py',
|
|
77
65
|
'design-doc': 'domain/design/gen_design_doc.py',
|
|
@@ -96,7 +84,6 @@ DISPATCH = {
|
|
|
96
84
|
'autotest-batch': 'validation/test/autotest_batch.py',
|
|
97
85
|
'benchmark': 'validation/test/benchmark.py',
|
|
98
86
|
# ── 环境 / 初始化 ──
|
|
99
|
-
'switch-project': 'domain/kg/switch_project.py',
|
|
100
87
|
'doctor': 'deployment/setup/init_doctor.py',
|
|
101
88
|
'init': 'deployment/setup/setup.py',
|
|
102
89
|
'install-qw': 'deployment/setup/install_qoderwork.py',
|
|
@@ -160,95 +147,6 @@ def _usage():
|
|
|
160
147
|
return '\n'.join(lines)
|
|
161
148
|
|
|
162
149
|
|
|
163
|
-
def _ctx_cache_cmd(rest, scripts_root):
|
|
164
|
-
"""wlkj.py ctx-cache <REQ-ID> <query> [--platform web|app] — 按 REQ 缓存 context_pack 结果(T1.3)。
|
|
165
|
-
HIT 回放缓存(省一次 KG/MCP 往返);MISS 跑 context_pack 捕获 stdout 写缓存再回放。
|
|
166
|
-
wlkj.py ctx-cache <REQ-ID> --clear — 清该 REQ 缓存(PRD/spec 改动后失效)。
|
|
167
|
-
任何异常 fail-open 降级直拉(缓存坏了不影响主流程)。
|
|
168
|
-
"""
|
|
169
|
-
do_clear = "--clear" in rest
|
|
170
|
-
rest = [a for a in rest if a != "--clear"]
|
|
171
|
-
|
|
172
|
-
req_id = None
|
|
173
|
-
query_parts, passthrough = [], []
|
|
174
|
-
i = 0
|
|
175
|
-
while i < len(rest):
|
|
176
|
-
a = rest[i]
|
|
177
|
-
if a == "--platform" and i + 1 < len(rest):
|
|
178
|
-
passthrough.extend([a, rest[i + 1]]); i += 2; continue
|
|
179
|
-
if a.startswith("-"):
|
|
180
|
-
passthrough.append(a); i += 1; continue
|
|
181
|
-
if req_id is None:
|
|
182
|
-
req_id = a; i += 1; continue
|
|
183
|
-
query_parts.append(a); i += 1
|
|
184
|
-
query = " ".join(query_parts).strip()
|
|
185
|
-
if not req_id or (not query and not do_clear):
|
|
186
|
-
sys.stderr.write("用法: wlkj.py ctx-cache <REQ-ID> <query> [--platform web|app] | ctx-cache <REQ-ID> --clear\n")
|
|
187
|
-
return 1
|
|
188
|
-
|
|
189
|
-
cc = None
|
|
190
|
-
try:
|
|
191
|
-
if scripts_root not in sys.path:
|
|
192
|
-
sys.path.insert(0, scripts_root)
|
|
193
|
-
from foundation.io import context_cache as _cc # noqa
|
|
194
|
-
cc = _cc
|
|
195
|
-
except Exception as e:
|
|
196
|
-
sys.stderr.write("[cache] 模块导入失败, 降级直拉: %s\n" % str(e)[:80])
|
|
197
|
-
|
|
198
|
-
# --clear: 清该 REQ 缓存
|
|
199
|
-
if do_clear:
|
|
200
|
-
if cc is None:
|
|
201
|
-
sys.stderr.write("[cache] 模块不可用, 无法 clear\n"); return 1
|
|
202
|
-
cc.invalidate(req_id)
|
|
203
|
-
sys.stderr.write("[cache] CLEARED req=%s\n" % req_id)
|
|
204
|
-
return 0
|
|
205
|
-
|
|
206
|
-
if cc is not None:
|
|
207
|
-
try:
|
|
208
|
-
cached = cc.get(req_id, query)
|
|
209
|
-
if cached is not None:
|
|
210
|
-
sys.stdout.write(cached if cached.endswith("\n") else cached + "\n")
|
|
211
|
-
sys.stderr.write("[cache] HIT req=%s q=%s (省一次 context_pack 往返)\n" % (req_id, query[:30]))
|
|
212
|
-
return 0
|
|
213
|
-
except Exception as e:
|
|
214
|
-
sys.stderr.write("[cache] get 失败, 降级直拉: %s\n" % str(e)[:80])
|
|
215
|
-
|
|
216
|
-
# MISS / 降级:跑 context_pack,捕获 stdout,写缓存,回放
|
|
217
|
-
script_path = os.path.join(scripts_root, DISPATCH["context"])
|
|
218
|
-
r = subprocess.run([sys.executable, script_path, query] + passthrough, capture_output=True)
|
|
219
|
-
sys.stderr.write(r.stderr.decode(errors="replace"))
|
|
220
|
-
out = r.stdout.decode(errors="replace")
|
|
221
|
-
sys.stdout.write(out)
|
|
222
|
-
if out.strip() and cc is not None:
|
|
223
|
-
try:
|
|
224
|
-
cc.put(req_id, query, out)
|
|
225
|
-
sys.stderr.write("[cache] MISS+PUT req=%s q=%s\n" % (req_id, query[:30]))
|
|
226
|
-
except Exception as e:
|
|
227
|
-
sys.stderr.write("[cache] put 失败(不影响本次输出): %s\n" % str(e)[:80])
|
|
228
|
-
return r.returncode
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
def _bf_score_cmd(rest, scripts_root):
|
|
232
|
-
"""wlkj.py bf-score <prd|proto|code|search> <产物文件> [--feature 主题] — 业务保真打分(T1/FW12)。
|
|
233
|
-
转发 bf_score.py 对真命令产出文件打 BF 分(PRD→BF1/2, 原型→BF3/4, code/search→BF5/6)。
|
|
234
|
-
bf_score.py 跑在云容器(接真 db_tables/design_system/真PRD);本地无业务数据时降级提示。"""
|
|
235
|
-
script = os.path.join(scripts_root, "validation", "eval", "bf_score.py")
|
|
236
|
-
if not os.path.isfile(script):
|
|
237
|
-
sys.stderr.write("[bf-score] bf_score.py 不在,跳过\n")
|
|
238
|
-
return 1
|
|
239
|
-
r = subprocess.run([sys.executable, script] + rest)
|
|
240
|
-
return r.returncode
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
# 带 custom 逻辑的命令(在 DISPATCH 透明转发前拦截)
|
|
244
|
-
SPECIAL = {
|
|
245
|
-
'ctx-cache': _ctx_cache_cmd,
|
|
246
|
-
'context-cache': _ctx_cache_cmd,
|
|
247
|
-
'bf-score': _bf_score_cmd,
|
|
248
|
-
'bfscore': _bf_score_cmd,
|
|
249
|
-
}
|
|
250
|
-
|
|
251
|
-
|
|
252
150
|
def main():
|
|
253
151
|
argv = sys.argv[1:]
|
|
254
152
|
if not argv or argv[0] in ('-h', '--help', 'help'):
|
|
@@ -261,10 +159,6 @@ def main():
|
|
|
261
159
|
# scripts/ 根 = __file__ 的上两级 (orchestration/ -> scripts/)
|
|
262
160
|
scripts_root = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
|
263
161
|
|
|
264
|
-
# T1.3 带 custom 逻辑的命令(如 ctx-cache 缓存)在透明转发前拦截
|
|
265
|
-
if cmd in SPECIAL:
|
|
266
|
-
return SPECIAL[cmd](rest, scripts_root)
|
|
267
|
-
|
|
268
162
|
script = DISPATCH.get(cmd)
|
|
269
163
|
if not script:
|
|
270
164
|
# Fallback: 当作脚本名在 scripts/ 下递归找 (支撑 'wlkj.py run <脚本>')
|
|
@@ -76,14 +76,6 @@
|
|
|
76
76
|
"command": "python .qoder/hooks/pre-tool-use.py || python3 .qoder/hooks/pre-tool-use.py"
|
|
77
77
|
}
|
|
78
78
|
]
|
|
79
|
-
},
|
|
80
|
-
{
|
|
81
|
-
"matcher": "Bash",
|
|
82
|
-
"hooks": [
|
|
83
|
-
{
|
|
84
|
-
"command": "python .qoder/hooks/pre-tool-use-commit.py || python3 .qoder/hooks/pre-tool-use-commit.py"
|
|
85
|
-
}
|
|
86
|
-
]
|
|
87
79
|
}
|
|
88
80
|
],
|
|
89
81
|
"PostToolUse": [
|