@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.
Files changed (130) hide show
  1. package/bin/cli.js +117 -0
  2. package/package.json +2 -2
  3. package/templates/.qoder/.runtime/hook-errors.log +4 -0
  4. package/templates/qoder/commands/optional/wl-insight.md +276 -276
  5. package/templates/qoder/commands/optional/wl-report.md +1 -1
  6. package/templates/qoder/commands/optional/wl-spec.md +13 -1
  7. package/templates/qoder/commands/optional/wl-status.md +14 -1
  8. package/templates/qoder/commands/wl-code.md +111 -4
  9. package/templates/qoder/commands/wl-commit.md +12 -1
  10. package/templates/qoder/commands/wl-design.md +69 -5
  11. package/templates/qoder/commands/wl-init.md +129 -102
  12. package/templates/qoder/commands/wl-prd.md +162 -6
  13. package/templates/qoder/commands/wl-search.md +106 -20
  14. package/templates/qoder/commands/wl-task.md +617 -613
  15. package/templates/qoder/commands/wl-test.md +20 -1
  16. package/templates/qoder/contracts/spec.md +4 -0
  17. package/templates/qoder/hooks/post-tool-use.py +71 -0
  18. package/templates/qoder/hooks/pre-tool-use.py +136 -0
  19. package/templates/qoder/hooks/session-start.py +365 -397
  20. package/templates/qoder/scripts/capability/__pycache__/__init__.cpython-39.pyc +0 -0
  21. package/templates/qoder/scripts/capability/__pycache__/present_html.cpython-39.pyc +0 -0
  22. package/templates/qoder/scripts/capability/__pycache__/registry.cpython-39.pyc +0 -0
  23. package/templates/qoder/scripts/capability/__pycache__/registry_mcp.cpython-39.pyc +0 -0
  24. package/templates/qoder/scripts/capability/adapters/__init__.py +1 -1
  25. package/templates/qoder/scripts/capability/adapters/__pycache__/__init__.cpython-39.pyc +0 -0
  26. package/templates/qoder/scripts/capability/adapters/__pycache__/cli.cpython-39.pyc +0 -0
  27. package/templates/qoder/scripts/capability/adapters/__pycache__/mcp.cpython-39.pyc +0 -0
  28. package/templates/qoder/scripts/capability/adapters/__pycache__/qw.cpython-39.pyc +0 -0
  29. package/templates/qoder/scripts/capability/adapters/mcp.py +76 -100
  30. package/templates/qoder/scripts/capability/adapters/qw.py +295 -295
  31. package/templates/qoder/scripts/capability/caps/__init__.py +5 -5
  32. package/templates/qoder/scripts/capability/caps/__pycache__/__init__.cpython-39.pyc +0 -0
  33. package/templates/qoder/scripts/capability/caps/__pycache__/context.cpython-39.pyc +0 -0
  34. package/templates/qoder/scripts/capability/caps/__pycache__/cron.cpython-39.pyc +0 -0
  35. package/templates/qoder/scripts/capability/caps/__pycache__/identity.cpython-39.pyc +0 -0
  36. package/templates/qoder/scripts/capability/caps/__pycache__/memory.cpython-39.pyc +0 -0
  37. package/templates/qoder/scripts/capability/caps/__pycache__/notify.cpython-39.pyc +0 -0
  38. package/templates/qoder/scripts/capability/caps/__pycache__/present.cpython-39.pyc +0 -0
  39. package/templates/qoder/scripts/capability/caps/__pycache__/repo.cpython-39.pyc +0 -0
  40. package/templates/qoder/scripts/capability/caps/__pycache__/sandbox.cpython-39.pyc +0 -0
  41. package/templates/qoder/scripts/capability/caps/memory.py +1 -1
  42. package/templates/qoder/scripts/capability/caps/notify.py +64 -0
  43. package/templates/qoder/scripts/capability/caps/sandbox.py +38 -0
  44. package/templates/qoder/scripts/capability/present_html.py +68 -0
  45. package/templates/qoder/scripts/capability/registry.py +27 -25
  46. package/templates/qoder/scripts/capability/registry_mcp.py +67 -5
  47. package/templates/qoder/scripts/capability/smoke_test_report.json +34 -20
  48. package/templates/qoder/scripts/deployment/setup/carriers.py +3 -1
  49. package/templates/qoder/scripts/deployment/setup/init_doctor.py +10 -3
  50. package/templates/qoder/scripts/domain/__pycache__/__init__.cpython-39.pyc +0 -0
  51. package/templates/qoder/scripts/domain/integration/__init__.py +0 -0
  52. package/templates/qoder/scripts/domain/integration/__pycache__/__init__.cpython-39.pyc +0 -0
  53. package/templates/qoder/scripts/domain/integration/__pycache__/return_to_platform.cpython-39.pyc +0 -0
  54. package/templates/qoder/scripts/domain/integration/return_to_platform.py +395 -0
  55. package/templates/qoder/scripts/domain/integration/spec_upload.py +208 -0
  56. package/templates/qoder/scripts/domain/kg/build/kg_build.py +27 -3
  57. package/templates/qoder/scripts/domain/kg/extract/asset/__init__.py +10 -0
  58. package/templates/qoder/scripts/domain/kg/extract/asset/asset_tree.py +57 -0
  59. package/templates/qoder/scripts/domain/kg/extract/asset/discussion_importer.py +62 -0
  60. package/templates/qoder/scripts/domain/kg/extract/asset/prd_importer.py +146 -0
  61. package/templates/qoder/scripts/domain/kg/extract/asset/prototype_importer.py +64 -0
  62. package/templates/qoder/scripts/domain/kg/extract/asset/returns_importer.py +52 -0
  63. package/templates/qoder/scripts/domain/kg/extract/build_goal3.py +104 -0
  64. package/templates/qoder/scripts/domain/kg/extract/build_goal4.py +55 -0
  65. package/templates/qoder/scripts/domain/kg/extract/build_goal5.py +95 -0
  66. package/templates/qoder/scripts/domain/kg/extract/db/__init__.py +8 -0
  67. package/templates/qoder/scripts/domain/kg/extract/db/data_profile.py +22 -0
  68. package/templates/qoder/scripts/domain/kg/extract/db/fk_extractor.py +55 -0
  69. package/templates/qoder/scripts/domain/kg/extract/db/schema_extractor.py +90 -0
  70. package/templates/qoder/scripts/domain/kg/extract/extract.py +84 -0
  71. package/templates/qoder/scripts/domain/kg/extract/inference/__init__.py +9 -0
  72. package/templates/qoder/scripts/domain/kg/extract/inference/community_summarizer.py +206 -0
  73. package/templates/qoder/scripts/domain/kg/extract/inference/embed_builder.py +132 -0
  74. package/templates/qoder/scripts/domain/kg/extract/inference/naming_matcher.py +80 -0
  75. package/templates/qoder/scripts/domain/kg/extract/inference/promote.py +59 -0
  76. package/templates/qoder/scripts/domain/kg/extract/inference/recompute.py +93 -0
  77. package/templates/qoder/scripts/domain/kg/extract/inference/weak_link.py +421 -0
  78. package/templates/qoder/scripts/domain/kg/extract/java/__init__.py +15 -0
  79. package/templates/qoder/scripts/domain/kg/extract/java/_parser.py +271 -0
  80. package/templates/qoder/scripts/domain/kg/extract/java/all.py +145 -0
  81. package/templates/qoder/scripts/domain/kg/extract/java/build_java_to_pg.py +102 -0
  82. package/templates/qoder/scripts/domain/kg/extract/java/call_chain.py +49 -0
  83. package/templates/qoder/scripts/domain/kg/extract/java/class_extractor.py +141 -0
  84. package/templates/qoder/scripts/domain/kg/extract/java/domain_extractor.py +148 -0
  85. package/templates/qoder/scripts/domain/kg/extract/java/dubbo_extractor.py +33 -0
  86. package/templates/qoder/scripts/domain/kg/extract/java/endpoint_extractor.py +36 -0
  87. package/templates/qoder/scripts/domain/kg/extract/java/javadoc_extractor.py +110 -0
  88. package/templates/qoder/scripts/domain/kg/extract/java/llm_cn_filler.py +150 -0
  89. package/templates/qoder/scripts/domain/kg/extract/java/member_extractor.py +157 -0
  90. package/templates/qoder/scripts/domain/kg/extract/java/mybatisplus_extractor.py +34 -0
  91. package/templates/qoder/scripts/domain/kg/extract/java/pg_upsert.py +165 -0
  92. package/templates/qoder/scripts/domain/kg/extract/java/satoken_extractor.py +30 -0
  93. package/templates/qoder/scripts/domain/kg/extract/java/spring_extractor.py +39 -0
  94. package/templates/qoder/scripts/domain/kg/extract/java/validation_extractor.py +33 -0
  95. package/templates/qoder/scripts/domain/kg/extract/mybatis/__init__.py +9 -0
  96. package/templates/qoder/scripts/domain/kg/extract/mybatis/all.py +79 -0
  97. package/templates/qoder/scripts/domain/kg/extract/mybatis/mapper_parser.py +99 -0
  98. package/templates/qoder/scripts/domain/kg/extract/mybatis/relation_builder.py +69 -0
  99. package/templates/qoder/scripts/domain/kg/extract/mybatis/sql_extractor.py +78 -0
  100. package/templates/qoder/scripts/domain/kg/extract/prd/__init__.py +8 -0
  101. package/templates/qoder/scripts/domain/kg/extract/prd/prd_chunk_embed.py +105 -0
  102. package/templates/qoder/scripts/domain/kg/extract/prd/prd_llm_extract.py +153 -0
  103. package/templates/qoder/scripts/domain/kg/extract/prd/req_anchor.py +120 -0
  104. package/templates/qoder/scripts/domain/kg/graph/kg_semantic.py +4 -2
  105. package/templates/qoder/scripts/domain/kg/kg.py +42 -5
  106. package/templates/qoder/scripts/domain/kg/search/_remote.py +187 -0
  107. package/templates/qoder/scripts/domain/kg/search/context_pack.py +32 -2
  108. package/templates/qoder/scripts/domain/kg/search/search_index.py +74 -20
  109. package/templates/qoder/scripts/domain/kg/switch_project.py +158 -0
  110. package/templates/qoder/scripts/domain/kg/sync_repowiki.py +109 -0
  111. package/templates/qoder/scripts/domain/task/__pycache__/wlkj_panel.cpython-39.pyc +0 -0
  112. package/templates/qoder/scripts/domain/task/{zentao_panel.py → wlkj_panel.py} +315 -53
  113. package/templates/qoder/scripts/engine/poller.py +219 -0
  114. package/templates/qoder/scripts/foundation/__pycache__/__init__.cpython-39.pyc +0 -0
  115. package/templates/qoder/scripts/foundation/core/__pycache__/__init__.cpython-39.pyc +0 -0
  116. package/templates/qoder/scripts/foundation/core/__pycache__/paths.cpython-39.pyc +0 -0
  117. package/templates/qoder/scripts/foundation/integrations/active_task.py +2 -1
  118. package/templates/qoder/scripts/orchestration/wlkj.py +4 -0
  119. package/templates/qoder/scripts/protocol/__pycache__/__init__.cpython-39.pyc +0 -0
  120. package/templates/qoder/scripts/protocol/mcp/zentao_mcp_server.py +1 -1
  121. package/templates/qoder/scripts/protocol/transports/__pycache__/__init__.cpython-39.pyc +0 -0
  122. package/templates/qoder/scripts/protocol/transports/__pycache__/base.cpython-39.pyc +0 -0
  123. package/templates/qoder/scripts/protocol/transports/__pycache__/cli.cpython-39.pyc +0 -0
  124. package/templates/qoder/scripts/protocol/transports/__pycache__/http.cpython-39.pyc +0 -0
  125. package/templates/qoder/scripts/protocol/transports/__pycache__/stdio.cpython-39.pyc +0 -0
  126. package/templates/qoder/scripts/protocol/transports/http.py +7 -1
  127. package/templates/qoder/scripts/validation/metrics/__pycache__/present_board.cpython-39.pyc +0 -0
  128. package/templates/qoder/scripts/validation/metrics/present_board.py +180 -0
  129. package/templates/qoder/settings.json +10 -0
  130. 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
- nexus.registry —— 宿主发现 + Nexus Facade。
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 = nexus.resolve()
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
- # features = 当前已实现 (非 NoOp) 的能力。notify/present/sandbox 暂无
43
- # 宿主原生 Adapter,声明即误报可用,故不列。后续补齐 Adapter 再加。
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 桌面 Agent,全能力",
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
- 工作流永远只调 nexus.resolve() 拿到本对象,再访问 .memory/.mcp/...,
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
- return NoOpNotifyCap()
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 富展示 —— QoderWork 调 qoder_show_widget 嵌入对话; 其余 NoOp 降级表格。"""
206
- if host in ("qoder-work", "qoder-ide"):
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
- return NoOpSandboxCap()
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 = nexus.resolve()
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
- nexus.registry_mcp —— MCP 工具路由(tool → server → transport)。
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": "stdio",
40
- "launcher_arg": "kg",
41
- "note": "知识图谱。本地分发;未来可上服务器 kind=http + url",
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
- self._servers = servers if servers is not None else SERVERS
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-06-29 11:20:28",
2
+ "timestamp": "2026-07-02 11:13:44",
3
3
  "servers": {
4
4
  "qoder-knowledge-graph": {
5
5
  "available": true,
6
- "tools_indexed": 17,
7
- "latency_ms": 484.0,
6
+ "tools_indexed": 35,
7
+ "latency_ms": 16.0,
8
8
  "sample_tools": [
9
9
  "search_code",
10
10
  "search_api",
11
- "search_prd",
12
- "get_impact",
11
+ "search_field",
12
+ "search_style",
13
13
  "context_360"
14
14
  ],
15
- "spawn_pid": 66880,
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": 766.0,
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": 48620,
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": 609.0,
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": 67572,
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": 3313.0,
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": 32460,
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": 3547.0,
64
- "text_preview": "Chinese: 保险 -> English: ins insurance vehlife/insurance\n\n[Carmg-H5] 12 files\n Carmg-H5/src/.../main/List.vue -> data/code/Carmg-H5/src/modules/icsAbnormalCenter/module/main/List.vue\n Carmg-H5/src/",
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": true,
73
- "hit": true
86
+ "available": false,
87
+ "hit": false
74
88
  },
75
89
  {
76
90
  "provider": "FileMemoryCap",
77
91
  "available": true,
78
- "hit": false
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": 4,
91
- "total_servers": 4,
92
- "total_tools": 104
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
- 四载体: Qoder IDE / Quest / CLI / QoderWork。三者都是"四载体"这一个领域,
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
- # 完全缺失 先尝试 team_sync pull 拉管理员建好的 kg.duckdb
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 os.path.isfile(kg_path):
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