@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
|
@@ -11,6 +11,9 @@ NotifyCap —— IM 通知能力契约。
|
|
|
11
11
|
"""
|
|
12
12
|
from __future__ import annotations
|
|
13
13
|
|
|
14
|
+
import json
|
|
15
|
+
import os
|
|
16
|
+
import urllib.request
|
|
14
17
|
from typing import Protocol, runtime_checkable
|
|
15
18
|
|
|
16
19
|
|
|
@@ -39,3 +42,64 @@ class NoOpNotifyCap:
|
|
|
39
42
|
|
|
40
43
|
def list_channels(self) -> list[dict]:
|
|
41
44
|
return []
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
class WebhookNotifyCap:
|
|
48
|
+
"""真实通知 adapter(#17):POST 消息到配置的 IM webhook(飞书/企业微信/钉钉)。
|
|
49
|
+
|
|
50
|
+
webhook 从 env WLKJ_NOTIFY_WEBHOOKS 解析:
|
|
51
|
+
"default=https://open.feishu.cn/...,review=https://..." 或 JSON '{"default":"..."}'
|
|
52
|
+
用 stdlib urllib(宿主脚本可能无 httpx/requests),POST 通用 text payload。
|
|
53
|
+
available 仅当至少配了一个 webhook,否则 False(优雅降级 NoOp,不谎报)。
|
|
54
|
+
"""
|
|
55
|
+
|
|
56
|
+
def __init__(self) -> None:
|
|
57
|
+
self._channels: dict[str, str] = self._load()
|
|
58
|
+
|
|
59
|
+
def _load(self) -> dict[str, str]:
|
|
60
|
+
raw = os.environ.get("WLKJ_NOTIFY_WEBHOOKS", "").strip()
|
|
61
|
+
if not raw:
|
|
62
|
+
return {}
|
|
63
|
+
if raw.startswith("{"): # JSON 形式
|
|
64
|
+
try:
|
|
65
|
+
d = json.loads(raw)
|
|
66
|
+
return {str(k): str(v) for k, v in d.items() if v}
|
|
67
|
+
except Exception:
|
|
68
|
+
return {}
|
|
69
|
+
out: dict[str, str] = {}
|
|
70
|
+
for pair in raw.split(","):
|
|
71
|
+
if "=" in pair:
|
|
72
|
+
k, v = pair.split("=", 1)
|
|
73
|
+
k, v = k.strip(), v.strip()
|
|
74
|
+
if v:
|
|
75
|
+
out[k] = v
|
|
76
|
+
return out
|
|
77
|
+
|
|
78
|
+
@property
|
|
79
|
+
def available(self) -> bool:
|
|
80
|
+
return len(self._channels) > 0
|
|
81
|
+
|
|
82
|
+
def send(self, channel: str, message: str) -> bool:
|
|
83
|
+
url = self._channels.get(channel) or self._channels.get("default")
|
|
84
|
+
if not url:
|
|
85
|
+
return False
|
|
86
|
+
try:
|
|
87
|
+
# 通用 text payload(飞书 msg_type/content;钉钉/企微也兼容 text 字段)
|
|
88
|
+
payload = json.dumps({"msg_type": "text", "content": {"text": message}}).encode("utf-8")
|
|
89
|
+
req = urllib.request.Request(url, data=payload,
|
|
90
|
+
headers={"Content-Type": "application/json"})
|
|
91
|
+
with urllib.request.urlopen(req, timeout=5) as r:
|
|
92
|
+
return getattr(r, "status", 200) == 200
|
|
93
|
+
except Exception:
|
|
94
|
+
return False # IM 不可达不阻塞工作流
|
|
95
|
+
|
|
96
|
+
def list_channels(self) -> list[dict]:
|
|
97
|
+
return [{"channel": k, "configured": bool(v)} for k, v in self._channels.items()]
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
def default_notify() -> NotifyCap:
|
|
101
|
+
"""工厂:配了 webhook→WebhookNotifyCap;否则 NoOpNotifyCap。"""
|
|
102
|
+
cap = WebhookNotifyCap()
|
|
103
|
+
if cap.available:
|
|
104
|
+
return cap # type: ignore[return-value]
|
|
105
|
+
return NoOpNotifyCap() # type: ignore[return-value]
|
|
@@ -13,6 +13,8 @@ SandboxCap —— 安全执行能力契约。
|
|
|
13
13
|
"""
|
|
14
14
|
from __future__ import annotations
|
|
15
15
|
|
|
16
|
+
import os
|
|
17
|
+
import subprocess
|
|
16
18
|
from typing import Protocol, runtime_checkable
|
|
17
19
|
|
|
18
20
|
|
|
@@ -41,3 +43,39 @@ class NoOpSandboxCap:
|
|
|
41
43
|
|
|
42
44
|
def approval_mode(self) -> str:
|
|
43
45
|
return "suggest"
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
class SubprocessSandboxCap:
|
|
49
|
+
"""真实沙箱 adapter(#17):子进程执行 + 超时强杀 + stdout/stderr/returncode 捕获。
|
|
50
|
+
|
|
51
|
+
无 docker 时的进程级隔离(超时强杀 + 输出捕获 + cwd 隔离),全平台可用。
|
|
52
|
+
docker 容器级隔离(FS/网络限制)后续可加 DockerSandboxCap,工厂按 env 切换。
|
|
53
|
+
available=True(subprocess 全平台可用)。
|
|
54
|
+
"""
|
|
55
|
+
|
|
56
|
+
available = True
|
|
57
|
+
|
|
58
|
+
def run(self, command: str, cwd: str | None = None, timeout: int = 60) -> dict:
|
|
59
|
+
try:
|
|
60
|
+
# errors='replace' 健壮化:Windows 命令常输出 GBK 中文,UTF-8 解码会崩;
|
|
61
|
+
# replace 不崩(个别字符显示?),比 exec_error 丢全部输出好。
|
|
62
|
+
p = subprocess.run(command, shell=True, cwd=cwd, timeout=timeout,
|
|
63
|
+
capture_output=True, text=True, errors="replace")
|
|
64
|
+
return {"ok": p.returncode == 0, "stdout": p.stdout,
|
|
65
|
+
"stderr": p.stderr, "returncode": p.returncode}
|
|
66
|
+
except subprocess.TimeoutExpired:
|
|
67
|
+
return {"ok": False, "reason": "timeout(%ds)" % timeout, "returncode": -1}
|
|
68
|
+
except Exception as e: # 命令不存在 / 权限 等
|
|
69
|
+
return {"ok": False, "reason": "exec_error: %s" % str(e)[:200], "returncode": -1}
|
|
70
|
+
|
|
71
|
+
def approval_mode(self) -> str:
|
|
72
|
+
return os.environ.get("WLKJ_APPROVAL_MODE", "suggest")
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def default_sandbox() -> SandboxCap:
|
|
76
|
+
"""工厂:env WLKJ_SANDBOX=off→NoOp;docker(占位)→Subprocess 兜底;默认 Subprocess。"""
|
|
77
|
+
mode = os.environ.get("WLKJ_SANDBOX", "subprocess").lower()
|
|
78
|
+
if mode == "off":
|
|
79
|
+
return NoOpSandboxCap() # type: ignore[return-value]
|
|
80
|
+
# docker 模式暂未实现容器隔离,先兜底 Subprocess(避免谎报容器级安全)
|
|
81
|
+
return SubprocessSandboxCap() # type: ignore[return-value]
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
"""present_html.py — 富展示 HTML 统一入口(cap.present 双轨)。
|
|
3
|
+
|
|
4
|
+
模式(抄 usability_score.py --html):
|
|
5
|
+
1. QoderWork(cap.present.available)→ cap.present.widget(html) 嵌入对话流
|
|
6
|
+
2. 降级 → 落盘 workspace/members/{dev}/journal/{name}-{ts}.html
|
|
7
|
+
3. 最终兜底 → stdout
|
|
8
|
+
|
|
9
|
+
所有命令的富展示(impact/coverage/feature/report 看板)统一调 present(html, name),
|
|
10
|
+
不在各自脚本重复 widget+落盘逻辑。复用点:caps/present.py PresentCap + adapters/qw.py QwPresentCap。
|
|
11
|
+
"""
|
|
12
|
+
import os
|
|
13
|
+
import sys
|
|
14
|
+
from pathlib import Path
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def _resolve_repo(repo_root=None):
|
|
18
|
+
if repo_root:
|
|
19
|
+
return Path(repo_root)
|
|
20
|
+
env = os.environ.get("QODER_REPO") or os.environ.get("WLKJ_REPO")
|
|
21
|
+
if env:
|
|
22
|
+
return Path(env)
|
|
23
|
+
p = Path(__file__).resolve()
|
|
24
|
+
for parent in p.parents:
|
|
25
|
+
if (parent / ".qoder").is_dir() or (parent / "packages").is_dir():
|
|
26
|
+
return parent
|
|
27
|
+
return p.parent
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def _read_dev(repo):
|
|
31
|
+
f = repo / ".qoder" / ".developer"
|
|
32
|
+
if f.is_file():
|
|
33
|
+
for line in f.read_text(encoding="utf-8").splitlines():
|
|
34
|
+
if "name=" in line:
|
|
35
|
+
return line.split("name=", 1)[1].strip().split()[0]
|
|
36
|
+
return os.environ.get("QODER_DEV", "") or "shared"
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def present(html, name="dashboard", repo_root=None, dev=None):
|
|
40
|
+
"""展示 HTML:QoderWork 嵌入对话;降级落盘 journal;兜底 stdout。返回 (sinked, path)。"""
|
|
41
|
+
repo = _resolve_repo(repo_root)
|
|
42
|
+
# 1. QoderWork widget(富宿主嵌入对话流)
|
|
43
|
+
try:
|
|
44
|
+
scripts_dir = str(repo / ".qoder" / "scripts")
|
|
45
|
+
if scripts_dir not in sys.path:
|
|
46
|
+
sys.path.insert(0, scripts_dir)
|
|
47
|
+
from capability.registry import resolve
|
|
48
|
+
cap = resolve()
|
|
49
|
+
if getattr(cap.present, "available", False):
|
|
50
|
+
if cap.present.widget(html):
|
|
51
|
+
return True, None
|
|
52
|
+
except Exception:
|
|
53
|
+
pass
|
|
54
|
+
# 2. 降级落盘
|
|
55
|
+
try:
|
|
56
|
+
out_dev = dev or _read_dev(repo)
|
|
57
|
+
out = repo / "workspace" / "members" / out_dev / "journal"
|
|
58
|
+
out.mkdir(parents=True, exist_ok=True)
|
|
59
|
+
from datetime import datetime
|
|
60
|
+
import re
|
|
61
|
+
safe_name = re.sub(r'[\\/:*?"<>|]', "_", name) # Windows 非法字符清洗
|
|
62
|
+
fp = out / ("%s-%s.html" % (safe_name, datetime.now().strftime("%Y%m%d-%H%M%S")))
|
|
63
|
+
fp.write_text(html, encoding="utf-8")
|
|
64
|
+
print("[HTML] 已落盘: %s" % fp)
|
|
65
|
+
return False, str(fp)
|
|
66
|
+
except Exception:
|
|
67
|
+
print(html)
|
|
68
|
+
return False, None
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# -*- coding: utf-8 -*-
|
|
2
2
|
"""
|
|
3
|
-
|
|
3
|
+
capability.registry —— 宿主发现 + Nexus Facade。
|
|
4
4
|
|
|
5
5
|
设计模式:
|
|
6
6
|
- Registry(HOST_TABLE 单一事实源,泛化 common.active_task._ENV_SESSION_KEYS)
|
|
@@ -8,7 +8,7 @@ nexus.registry —— 宿主发现 + Nexus Facade。
|
|
|
8
8
|
- Bridge(Cap 抽象 × 宿主实现 正交)
|
|
9
9
|
|
|
10
10
|
主轴: 工作流面向能力编程,不面向宿主。
|
|
11
|
-
cap =
|
|
11
|
+
cap = resolve()
|
|
12
12
|
cap.memory.recall(...) # 不管在 QoderWork 还是 CLI,同一行代码
|
|
13
13
|
"""
|
|
14
14
|
from __future__ import annotations
|
|
@@ -36,26 +36,24 @@ from .caps import (
|
|
|
36
36
|
# =============================================================================
|
|
37
37
|
|
|
38
38
|
HOST_TABLE: dict[str, dict] = {
|
|
39
|
-
# ── Qoder
|
|
39
|
+
# ── Qoder 系列(第一宿主系列,2026-07 对齐最新产品线)─────────────
|
|
40
|
+
# 主开发宿主 + 办公产品。(Qoder CLI / Cloud Agents 后续按需再加;QoderWake 架构性不接。)
|
|
41
|
+
# features = 引擎已接(非 NoOp adapter);native = 产品原生能力(仅建模,不算实现)。
|
|
42
|
+
"qoder-desktop": {
|
|
43
|
+
# Qoder 1.0 "Autonomous Development Desktop"(原 qoder-ide;Quest Window/Agent Mode 是其模式,故原 qoder-quest 合并于此)
|
|
44
|
+
"detect": ("QODER_DESKTOP", "QODER_IDE"),
|
|
45
|
+
"features": ("mcp", "memory", "cron", "present"),
|
|
46
|
+
"native": ("agent-mode", "context-engine", "vm-sandbox", "hooks", "skills", "mcp-sse"),
|
|
47
|
+
"mcp_servers": ["qoder-knowledge-graph", "qoder-zentao", "qoder-mysql", "lanhu"],
|
|
48
|
+
"note": "Qoder Desktop/IDE 1.0(主开发宿主;Quest 是其模式,非独立宿主)",
|
|
49
|
+
},
|
|
40
50
|
"qoder-work": {
|
|
51
|
+
# QoderWork:办公产品(非开发者,文档/报告/PPT/M365)。~/.qoderwork/ 投影 + qoder_show_widget 是其接入点。
|
|
41
52
|
"detect": ("QODER_SESSION_ID",),
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
"features": ("mcp", "memory", "cron"),
|
|
53
|
+
"features": ("mcp", "memory", "cron", "present"),
|
|
54
|
+
"native": ("office-agent", "m365", "human-in-loop", "hooks", "show-widget"),
|
|
45
55
|
"mcp_servers": ["qoder-knowledge-graph", "qoder-zentao", "qoder-mysql", "lanhu"],
|
|
46
|
-
"note": "QoderWork
|
|
47
|
-
},
|
|
48
|
-
"qoder-ide": {
|
|
49
|
-
"detect": ("QODER_IDE",),
|
|
50
|
-
"features": ("mcp", "memory"),
|
|
51
|
-
"mcp_servers": ["qoder-knowledge-graph", "qoder-zentao", "qoder-mysql"],
|
|
52
|
-
"note": "Qoder IDE (VSCode 扩展)",
|
|
53
|
-
},
|
|
54
|
-
"qoder-quest": {
|
|
55
|
-
"detect": ("QODER_QUEST",),
|
|
56
|
-
"features": ("mcp", "memory"),
|
|
57
|
-
"mcp_servers": ["qoder-knowledge-graph", "qoder-zentao"],
|
|
58
|
-
"note": "Qoder Quest,自然语言入口",
|
|
56
|
+
"note": "QoderWork 桌面办公 Agent(非开发者;文档/PPT/M365);~/.qoderwork/ 投影入口",
|
|
59
57
|
},
|
|
60
58
|
# ── 其他宿主(通用 AI 工具)────────────────────────────────
|
|
61
59
|
"codex": {
|
|
@@ -124,7 +122,7 @@ def detect_host() -> str:
|
|
|
124
122
|
class Nexus:
|
|
125
123
|
"""宿主能力 Facade —— 聚合所有能力,对外统一入口。
|
|
126
124
|
|
|
127
|
-
工作流永远只调
|
|
125
|
+
工作流永远只调 resolve() 拿到本对象,再访问 .memory/.mcp/...,
|
|
128
126
|
不直接 import 任何 adapter(Facade 模式)。
|
|
129
127
|
"""
|
|
130
128
|
|
|
@@ -198,12 +196,14 @@ def _resolve_cron(host: str):
|
|
|
198
196
|
|
|
199
197
|
|
|
200
198
|
def _resolve_notify(host: str):
|
|
201
|
-
|
|
199
|
+
"""L2 通知 —— 配了 WLKJ_NOTIFY_WEBHOOKS→WebhookNotifyCap(飞书/企微/钉钉);否则 NoOp 降级。"""
|
|
200
|
+
from .caps.notify import default_notify
|
|
201
|
+
return default_notify()
|
|
202
202
|
|
|
203
203
|
|
|
204
204
|
def _resolve_present(host: str):
|
|
205
|
-
"""L2 富展示 ——
|
|
206
|
-
if host in ("qoder-
|
|
205
|
+
"""L2 富展示 —— Qoder Desktop/Work 调 qoder_show_widget 嵌入对话; 其余 NoOp 降级表格。"""
|
|
206
|
+
if host in ("qoder-desktop", "qoder-work"):
|
|
207
207
|
try:
|
|
208
208
|
from .adapters.qw import QwPresentCap
|
|
209
209
|
return QwPresentCap()
|
|
@@ -213,7 +213,9 @@ def _resolve_present(host: str):
|
|
|
213
213
|
|
|
214
214
|
|
|
215
215
|
def _resolve_sandbox(host: str):
|
|
216
|
-
|
|
216
|
+
"""L2 安全执行 —— env WLKJ_SANDBOX≠off→SubprocessSandboxCap(进程级隔离+超时);off→NoOp。"""
|
|
217
|
+
from .caps.sandbox import default_sandbox
|
|
218
|
+
return default_sandbox()
|
|
217
219
|
|
|
218
220
|
|
|
219
221
|
def _resolve_mcp(host: str):
|
|
@@ -227,7 +229,7 @@ def _resolve_mcp(host: str):
|
|
|
227
229
|
def resolve() -> Nexus:
|
|
228
230
|
"""解析当前环境,返回 Nexus Facade。
|
|
229
231
|
|
|
230
|
-
一行入口: cap =
|
|
232
|
+
一行入口: cap = resolve()
|
|
231
233
|
内部: detect_host → 按宿主装配各 Cap → 注入 NoOp 兜底。
|
|
232
234
|
工作流代码零宿主感知。
|
|
233
235
|
"""
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# -*- coding: utf-8 -*-
|
|
2
2
|
"""
|
|
3
|
-
|
|
3
|
+
capability.registry_mcp —— MCP 工具路由(tool → server → transport)。
|
|
4
4
|
|
|
5
5
|
设计模式: Registry + Strategy(配合 transports 层)
|
|
6
6
|
|
|
@@ -34,11 +34,25 @@ from protocol.transports import get_transport, Transport, NoOpTransport, ToolRes
|
|
|
34
34
|
# 未来改这里即可切换部署形态,工作流零改动。
|
|
35
35
|
# =============================================================================
|
|
36
36
|
|
|
37
|
+
# =============================================================================
|
|
38
|
+
# mcp_config.json 消费 —— 让配置文件真正生效(单一事实源)
|
|
39
|
+
# -----------------------------------------------------------------------------
|
|
40
|
+
# 历史 bug: mcp_config.json / registry_mcp_config.json 是"死配置"(全仓 0 读取),
|
|
41
|
+
# registry 用硬编码 SERVERS。改配置文件不生效。
|
|
42
|
+
# 现在: 优先读 .qoder/mcp_config.json 的 mcpServers,读不到才 fallback。
|
|
43
|
+
# =============================================================================
|
|
44
|
+
# 配置文件中知识层 server 的别名映射(统一命名,两边都支持):
|
|
45
|
+
# mcp_config.json 里叫 "wlinkj-knowledge",本地硬编码叫 "qoder-knowledge-graph"。
|
|
46
|
+
# 让 _load_servers 把 config 的 server 也注册成 "qoder-knowledge-graph" 别名,
|
|
47
|
+
# 这样反向索引 / 显式 server 调用两边名字都能命中。
|
|
48
|
+
_KNOWLEDGE_SERVER_ALIASES = ["wlinkj-knowledge", "qoder-knowledge-graph"]
|
|
49
|
+
|
|
50
|
+
# 硬编码 fallback(配置文件读不到时兜底,保证引擎不崩)
|
|
37
51
|
SERVERS: dict[str, dict] = {
|
|
38
52
|
"qoder-knowledge-graph": {
|
|
39
|
-
"kind": "
|
|
40
|
-
"
|
|
41
|
-
"note": "
|
|
53
|
+
"kind": "http",
|
|
54
|
+
"url": os.environ.get("WLKJ_BASE_URL", "http://10.89.7.120") + "/mcp",
|
|
55
|
+
"note": "知识图谱。已改为 http 模式,指向资产平台 MCP 服务(POST /mcp JSON-RPC)",
|
|
42
56
|
},
|
|
43
57
|
"qoder-zentao": {
|
|
44
58
|
"kind": "stdio",
|
|
@@ -58,6 +72,53 @@ SERVERS: dict[str, dict] = {
|
|
|
58
72
|
}
|
|
59
73
|
|
|
60
74
|
|
|
75
|
+
def _mcp_config_path() -> Path:
|
|
76
|
+
"""定位 mcp_config.json(在仓库根/宿主下)。"""
|
|
77
|
+
repo = _get_repo_root()
|
|
78
|
+
# registry_mcp.py 在 .../scripts/capability/, mcp_config.json 在宿主根(.qoder)。
|
|
79
|
+
for cand in [
|
|
80
|
+
repo / "mcp_config.json",
|
|
81
|
+
repo / "registry_mcp_config.json",
|
|
82
|
+
]:
|
|
83
|
+
if cand.is_file():
|
|
84
|
+
return cand
|
|
85
|
+
return repo / "mcp_config.json"
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
def _load_servers() -> dict[str, dict]:
|
|
89
|
+
"""从 mcp_config.json 加载 mcpServers,合并到硬编码 SERVERS 上。
|
|
90
|
+
|
|
91
|
+
配置文件优先(改配置真生效),硬编码兜底(配置缺失也不崩)。
|
|
92
|
+
同时把知识层 server 注册多个别名(wlinkj-knowledge / qoder-knowledge-graph),
|
|
93
|
+
让引擎用任一名字都能调到。
|
|
94
|
+
"""
|
|
95
|
+
import json as _json
|
|
96
|
+
|
|
97
|
+
merged: dict[str, dict] = {k: dict(v) for k, v in SERVERS.items()}
|
|
98
|
+
try:
|
|
99
|
+
cfg_path = _mcp_config_path()
|
|
100
|
+
if not cfg_path.is_file():
|
|
101
|
+
return merged
|
|
102
|
+
with open(cfg_path, encoding="utf-8") as f:
|
|
103
|
+
cfg = _json.load(f)
|
|
104
|
+
mcp_servers = cfg.get("mcpServers") or {}
|
|
105
|
+
for name, spec in mcp_servers.items():
|
|
106
|
+
if not isinstance(spec, dict):
|
|
107
|
+
continue
|
|
108
|
+
# 剥离 _comment / _usage 等说明字段,只留 kind/url/token/env/launcher_arg/note
|
|
109
|
+
clean = {k: v for k, v in spec.items() if not k.startswith("_")}
|
|
110
|
+
# 知识层 server: 注册全部别名,都指向同一份配置
|
|
111
|
+
if name in _KNOWLEDGE_SERVER_ALIASES:
|
|
112
|
+
for alias in _KNOWLEDGE_SERVER_ALIASES:
|
|
113
|
+
merged[alias] = clean
|
|
114
|
+
else:
|
|
115
|
+
merged[name] = clean
|
|
116
|
+
except Exception:
|
|
117
|
+
# 读配置失败绝不能让引擎崩 —— 退回硬编码
|
|
118
|
+
pass
|
|
119
|
+
return merged
|
|
120
|
+
|
|
121
|
+
|
|
61
122
|
def _get_repo_root() -> Path:
|
|
62
123
|
"""从 common.paths 获取仓库根(复用既有工具,不重复造轮子)。"""
|
|
63
124
|
try:
|
|
@@ -76,7 +137,8 @@ class McpRegistry:
|
|
|
76
137
|
|
|
77
138
|
def __init__(self, repo_root: Optional[Path] = None, servers: Optional[dict] = None):
|
|
78
139
|
self._repo = repo_root or _get_repo_root()
|
|
79
|
-
|
|
140
|
+
# 默认从 mcp_config.json 加载(改配置真生效),显式传 servers 时覆盖
|
|
141
|
+
self._servers = servers if servers is not None else _load_servers()
|
|
80
142
|
self._pool: dict[str, Transport] = {}
|
|
81
143
|
self._tool_to_server: Optional[dict[str, str]] = None # 懒构建
|
|
82
144
|
self._index_lock_built = False
|
|
@@ -1,24 +1,24 @@
|
|
|
1
1
|
{
|
|
2
|
-
"timestamp": "2026-
|
|
2
|
+
"timestamp": "2026-07-02 11:13:44",
|
|
3
3
|
"servers": {
|
|
4
4
|
"qoder-knowledge-graph": {
|
|
5
5
|
"available": true,
|
|
6
|
-
"tools_indexed":
|
|
7
|
-
"latency_ms":
|
|
6
|
+
"tools_indexed": 35,
|
|
7
|
+
"latency_ms": 16.0,
|
|
8
8
|
"sample_tools": [
|
|
9
9
|
"search_code",
|
|
10
10
|
"search_api",
|
|
11
|
-
"
|
|
12
|
-
"
|
|
11
|
+
"search_field",
|
|
12
|
+
"search_style",
|
|
13
13
|
"context_360"
|
|
14
14
|
],
|
|
15
|
-
"spawn_pid":
|
|
15
|
+
"spawn_pid": null,
|
|
16
16
|
"status": "PASS"
|
|
17
17
|
},
|
|
18
18
|
"qoder-zentao": {
|
|
19
19
|
"available": true,
|
|
20
20
|
"tools_indexed": 68,
|
|
21
|
-
"latency_ms":
|
|
21
|
+
"latency_ms": 1625.0,
|
|
22
22
|
"sample_tools": [
|
|
23
23
|
"check_zentao_status",
|
|
24
24
|
"get_zentao_url",
|
|
@@ -26,13 +26,13 @@
|
|
|
26
26
|
"get_product",
|
|
27
27
|
"create_product"
|
|
28
28
|
],
|
|
29
|
-
"spawn_pid":
|
|
29
|
+
"spawn_pid": 76064,
|
|
30
30
|
"status": "PASS"
|
|
31
31
|
},
|
|
32
32
|
"qoder-mysql": {
|
|
33
33
|
"available": true,
|
|
34
34
|
"tools_indexed": 6,
|
|
35
|
-
"latency_ms":
|
|
35
|
+
"latency_ms": 500.0,
|
|
36
36
|
"sample_tools": [
|
|
37
37
|
"list_envs",
|
|
38
38
|
"query_schema",
|
|
@@ -40,13 +40,13 @@
|
|
|
40
40
|
"query_distinct",
|
|
41
41
|
"list_databases"
|
|
42
42
|
],
|
|
43
|
-
"spawn_pid":
|
|
43
|
+
"spawn_pid": 26784,
|
|
44
44
|
"status": "PASS"
|
|
45
45
|
},
|
|
46
46
|
"lanhu": {
|
|
47
47
|
"available": true,
|
|
48
48
|
"tools_indexed": 13,
|
|
49
|
-
"latency_ms":
|
|
49
|
+
"latency_ms": 4906.0,
|
|
50
50
|
"sample_tools": [
|
|
51
51
|
"lanhu_resolve_invite_link",
|
|
52
52
|
"lanhu_list_product_documents",
|
|
@@ -54,14 +54,28 @@
|
|
|
54
54
|
"lanhu_get_ai_analyze_page_result",
|
|
55
55
|
"lanhu_get_designs"
|
|
56
56
|
],
|
|
57
|
-
"spawn_pid":
|
|
57
|
+
"spawn_pid": 14148,
|
|
58
|
+
"status": "PASS"
|
|
59
|
+
},
|
|
60
|
+
"wlinkj-knowledge": {
|
|
61
|
+
"available": true,
|
|
62
|
+
"tools_indexed": 35,
|
|
63
|
+
"latency_ms": 32.0,
|
|
64
|
+
"sample_tools": [
|
|
65
|
+
"search_code",
|
|
66
|
+
"search_api",
|
|
67
|
+
"search_field",
|
|
68
|
+
"search_style",
|
|
69
|
+
"context_360"
|
|
70
|
+
],
|
|
71
|
+
"spawn_pid": null,
|
|
58
72
|
"status": "PASS"
|
|
59
73
|
}
|
|
60
74
|
},
|
|
61
75
|
"call_tool": {
|
|
62
76
|
"status": "PASS",
|
|
63
|
-
"latency_ms":
|
|
64
|
-
"text_preview": "
|
|
77
|
+
"latency_ms": 7281.0,
|
|
78
|
+
"text_preview": "{\"query\": \"保险\", \"platform\": null, \"module\": null, \"total\": 1, \"items\": [{\"type\": \"BUTTON\", \"name\": \"history\", \"label\": \"保险记录\"}]}",
|
|
65
79
|
"is_error": false
|
|
66
80
|
},
|
|
67
81
|
"memory": {
|
|
@@ -69,13 +83,13 @@
|
|
|
69
83
|
"providers": [
|
|
70
84
|
{
|
|
71
85
|
"provider": "McpMemoryProvider",
|
|
72
|
-
"available":
|
|
73
|
-
"hit":
|
|
86
|
+
"available": false,
|
|
87
|
+
"hit": false
|
|
74
88
|
},
|
|
75
89
|
{
|
|
76
90
|
"provider": "FileMemoryCap",
|
|
77
91
|
"available": true,
|
|
78
|
-
"hit":
|
|
92
|
+
"hit": true
|
|
79
93
|
},
|
|
80
94
|
{
|
|
81
95
|
"provider": "NoOpMemoryCap",
|
|
@@ -87,8 +101,8 @@
|
|
|
87
101
|
},
|
|
88
102
|
"summary": {
|
|
89
103
|
"all_pass": true,
|
|
90
|
-
"active_servers":
|
|
91
|
-
"total_servers":
|
|
92
|
-
"total_tools":
|
|
104
|
+
"active_servers": 5,
|
|
105
|
+
"total_servers": 5,
|
|
106
|
+
"total_tools": 157
|
|
93
107
|
}
|
|
94
108
|
}
|
|
@@ -3,7 +3,9 @@
|
|
|
3
3
|
"""carriers.py - 四载体一致性 (Facade 外观模式)
|
|
4
4
|
|
|
5
5
|
v3.0 合并自: check_carriers + check_qoderwork_consistency + sync_carriers。
|
|
6
|
-
|
|
6
|
+
四载体 = 命令/技能的 4 个部署面: Qoder Desktop(含 Quest 模式) / CLI / QoderWork / npm 镜像。
|
|
7
|
+
注意: 这是"部署面"轴,与 capability/registry.py:HOST_TABLE 的"宿主产品"轴正交
|
|
8
|
+
(HOST_TABLE 现列 desktop/work;cli/cloud 后续按需)。
|
|
7
9
|
对外应是 1 个 carriers 命令的子操作 (Facade: 多个细粒度检查统一入口)。
|
|
8
10
|
|
|
9
11
|
用法:
|
|
@@ -485,7 +485,11 @@ def check_index(fix):
|
|
|
485
485
|
print('\n--- 5. 知识图谱 (data/index/) ---')
|
|
486
486
|
age = index_age_days()
|
|
487
487
|
if age is None:
|
|
488
|
-
#
|
|
488
|
+
# 全迁 PG 后 kg.duckdb 废弃:环境配了 PG → 知识层在平台,不报缺失
|
|
489
|
+
if os.environ.get('KNOWLEDGE_BACKEND', '').lower() == 'pg' or 'postgresql' in os.environ.get('DATABASE_URL', ''):
|
|
490
|
+
say(OK, '知识层在平台 PostgreSQL(KNOWLEDGE_BACKEND=pg / DATABASE_URL=pg,不依赖本地 kg.duckdb)')
|
|
491
|
+
return
|
|
492
|
+
# 完全缺失(非 PG 环境)→ 先尝试 team_sync pull 拉管理员建好的 kg.duckdb
|
|
489
493
|
if fix:
|
|
490
494
|
print(' 索引缺失, 先从团队仓库拉取 (team_sync pull)...')
|
|
491
495
|
if run_script('team_sync.py', 'pull', timeout=120):
|
|
@@ -549,7 +553,8 @@ def check_style(fix):
|
|
|
549
553
|
else:
|
|
550
554
|
problem(fname + ' 缺失', '该文件提交在 git 中, 先解决团队同步 (team_sync pull)')
|
|
551
555
|
|
|
552
|
-
# kg.duckdb 知识图谱 (统一存储)
|
|
556
|
+
# kg.duckdb 知识图谱 (统一存储) — 全迁 PG 后知识层在平台,本地 duckdb 废弃
|
|
557
|
+
_pg_mode = os.environ.get('KNOWLEDGE_BACKEND', '').lower() == 'pg' or 'postgresql' in os.environ.get('DATABASE_URL', '')
|
|
553
558
|
kg_path = os.path.join(INDEX_DIR, 'kg.duckdb')
|
|
554
559
|
role = ''
|
|
555
560
|
try:
|
|
@@ -561,7 +566,9 @@ def check_style(fix):
|
|
|
561
566
|
except Exception:
|
|
562
567
|
pass
|
|
563
568
|
|
|
564
|
-
if
|
|
569
|
+
if _pg_mode:
|
|
570
|
+
say(OK, '知识层在平台 PostgreSQL(不依赖本地 kg.duckdb)')
|
|
571
|
+
elif os.path.isfile(kg_path):
|
|
565
572
|
say(OK, 'kg.duckdb 知识图谱 (%d KB)' % (os.path.getsize(kg_path) // 1024))
|
|
566
573
|
# 过期检测
|
|
567
574
|
import time as _t
|
|
File without changes
|
|
Binary file
|