@hupan56/wlkj 3.2.0 → 3.3.0

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 (122) hide show
  1. package/bin/cli.js +117 -0
  2. package/package.json +1 -1
  3. package/templates/qoder/commands/optional/wl-insight.md +276 -276
  4. package/templates/qoder/commands/optional/wl-report.md +1 -1
  5. package/templates/qoder/commands/optional/wl-spec.md +13 -1
  6. package/templates/qoder/commands/optional/wl-status.md +12 -1
  7. package/templates/qoder/commands/wl-code.md +76 -4
  8. package/templates/qoder/commands/wl-commit.md +12 -1
  9. package/templates/qoder/commands/wl-design.md +69 -5
  10. package/templates/qoder/commands/wl-init.md +27 -0
  11. package/templates/qoder/commands/wl-prd.md +145 -4
  12. package/templates/qoder/commands/wl-search.md +74 -20
  13. package/templates/qoder/commands/wl-task.md +613 -613
  14. package/templates/qoder/commands/wl-test.md +16 -1
  15. package/templates/qoder/contracts/spec.md +4 -0
  16. package/templates/qoder/hooks/post-tool-use.py +41 -0
  17. package/templates/qoder/hooks/session-start.py +34 -66
  18. package/templates/qoder/scripts/capability/__pycache__/__init__.cpython-39.pyc +0 -0
  19. package/templates/qoder/scripts/capability/__pycache__/registry.cpython-39.pyc +0 -0
  20. package/templates/qoder/scripts/capability/__pycache__/registry_mcp.cpython-39.pyc +0 -0
  21. package/templates/qoder/scripts/capability/adapters/__init__.py +1 -1
  22. package/templates/qoder/scripts/capability/adapters/__pycache__/__init__.cpython-39.pyc +0 -0
  23. package/templates/qoder/scripts/capability/adapters/__pycache__/cli.cpython-39.pyc +0 -0
  24. package/templates/qoder/scripts/capability/adapters/__pycache__/mcp.cpython-39.pyc +0 -0
  25. package/templates/qoder/scripts/capability/adapters/__pycache__/qw.cpython-39.pyc +0 -0
  26. package/templates/qoder/scripts/capability/adapters/mcp.py +76 -100
  27. package/templates/qoder/scripts/capability/adapters/qw.py +295 -295
  28. package/templates/qoder/scripts/capability/caps/__init__.py +1 -1
  29. package/templates/qoder/scripts/capability/caps/__pycache__/__init__.cpython-39.pyc +0 -0
  30. package/templates/qoder/scripts/capability/caps/__pycache__/context.cpython-39.pyc +0 -0
  31. package/templates/qoder/scripts/capability/caps/__pycache__/cron.cpython-39.pyc +0 -0
  32. package/templates/qoder/scripts/capability/caps/__pycache__/identity.cpython-39.pyc +0 -0
  33. package/templates/qoder/scripts/capability/caps/__pycache__/memory.cpython-39.pyc +0 -0
  34. package/templates/qoder/scripts/capability/caps/__pycache__/notify.cpython-39.pyc +0 -0
  35. package/templates/qoder/scripts/capability/caps/__pycache__/present.cpython-39.pyc +0 -0
  36. package/templates/qoder/scripts/capability/caps/__pycache__/repo.cpython-39.pyc +0 -0
  37. package/templates/qoder/scripts/capability/caps/__pycache__/sandbox.cpython-39.pyc +0 -0
  38. package/templates/qoder/scripts/capability/caps/memory.py +1 -1
  39. package/templates/qoder/scripts/capability/registry.py +21 -23
  40. package/templates/qoder/scripts/capability/registry_mcp.py +69 -5
  41. package/templates/qoder/scripts/capability/smoke_test_report.json +34 -20
  42. package/templates/qoder/scripts/deployment/setup/carriers.py +3 -1
  43. package/templates/qoder/scripts/deployment/setup/init_doctor.py +10 -3
  44. package/templates/qoder/scripts/domain/__pycache__/__init__.cpython-39.pyc +0 -0
  45. package/templates/qoder/scripts/domain/integration/__init__.py +0 -0
  46. package/templates/qoder/scripts/domain/integration/__pycache__/__init__.cpython-39.pyc +0 -0
  47. package/templates/qoder/scripts/domain/integration/__pycache__/return_to_platform.cpython-39.pyc +0 -0
  48. package/templates/qoder/scripts/domain/integration/return_to_platform.py +392 -0
  49. package/templates/qoder/scripts/domain/integration/spec_upload.py +209 -0
  50. package/templates/qoder/scripts/domain/kg/build/kg_build.py +27 -3
  51. package/templates/qoder/scripts/domain/kg/extract/asset/__init__.py +10 -0
  52. package/templates/qoder/scripts/domain/kg/extract/asset/asset_tree.py +57 -0
  53. package/templates/qoder/scripts/domain/kg/extract/asset/discussion_importer.py +62 -0
  54. package/templates/qoder/scripts/domain/kg/extract/asset/prd_importer.py +146 -0
  55. package/templates/qoder/scripts/domain/kg/extract/asset/prototype_importer.py +64 -0
  56. package/templates/qoder/scripts/domain/kg/extract/asset/returns_importer.py +52 -0
  57. package/templates/qoder/scripts/domain/kg/extract/build_goal3.py +104 -0
  58. package/templates/qoder/scripts/domain/kg/extract/build_goal4.py +55 -0
  59. package/templates/qoder/scripts/domain/kg/extract/build_goal5.py +95 -0
  60. package/templates/qoder/scripts/domain/kg/extract/db/__init__.py +8 -0
  61. package/templates/qoder/scripts/domain/kg/extract/db/data_profile.py +22 -0
  62. package/templates/qoder/scripts/domain/kg/extract/db/fk_extractor.py +55 -0
  63. package/templates/qoder/scripts/domain/kg/extract/db/schema_extractor.py +90 -0
  64. package/templates/qoder/scripts/domain/kg/extract/extract.py +84 -0
  65. package/templates/qoder/scripts/domain/kg/extract/extract.py.bak +430 -0
  66. package/templates/qoder/scripts/domain/kg/extract/inference/__init__.py +9 -0
  67. package/templates/qoder/scripts/domain/kg/extract/inference/community_summarizer.py +206 -0
  68. package/templates/qoder/scripts/domain/kg/extract/inference/embed_builder.py +132 -0
  69. package/templates/qoder/scripts/domain/kg/extract/inference/naming_matcher.py +80 -0
  70. package/templates/qoder/scripts/domain/kg/extract/inference/promote.py +59 -0
  71. package/templates/qoder/scripts/domain/kg/extract/inference/recompute.py +93 -0
  72. package/templates/qoder/scripts/domain/kg/extract/inference/weak_link.py +421 -0
  73. package/templates/qoder/scripts/domain/kg/extract/java/__init__.py +15 -0
  74. package/templates/qoder/scripts/domain/kg/extract/java/_parser.py +271 -0
  75. package/templates/qoder/scripts/domain/kg/extract/java/all.py +145 -0
  76. package/templates/qoder/scripts/domain/kg/extract/java/build_java_to_pg.py +102 -0
  77. package/templates/qoder/scripts/domain/kg/extract/java/call_chain.py +49 -0
  78. package/templates/qoder/scripts/domain/kg/extract/java/class_extractor.py +141 -0
  79. package/templates/qoder/scripts/domain/kg/extract/java/domain_extractor.py +148 -0
  80. package/templates/qoder/scripts/domain/kg/extract/java/dubbo_extractor.py +33 -0
  81. package/templates/qoder/scripts/domain/kg/extract/java/endpoint_extractor.py +36 -0
  82. package/templates/qoder/scripts/domain/kg/extract/java/javadoc_extractor.py +110 -0
  83. package/templates/qoder/scripts/domain/kg/extract/java/llm_cn_filler.py +150 -0
  84. package/templates/qoder/scripts/domain/kg/extract/java/member_extractor.py +157 -0
  85. package/templates/qoder/scripts/domain/kg/extract/java/mybatisplus_extractor.py +34 -0
  86. package/templates/qoder/scripts/domain/kg/extract/java/pg_upsert.py +165 -0
  87. package/templates/qoder/scripts/domain/kg/extract/java/satoken_extractor.py +30 -0
  88. package/templates/qoder/scripts/domain/kg/extract/java/spring_extractor.py +39 -0
  89. package/templates/qoder/scripts/domain/kg/extract/java/validation_extractor.py +33 -0
  90. package/templates/qoder/scripts/domain/kg/extract/mybatis/__init__.py +9 -0
  91. package/templates/qoder/scripts/domain/kg/extract/mybatis/all.py +79 -0
  92. package/templates/qoder/scripts/domain/kg/extract/mybatis/mapper_parser.py +99 -0
  93. package/templates/qoder/scripts/domain/kg/extract/mybatis/relation_builder.py +69 -0
  94. package/templates/qoder/scripts/domain/kg/extract/mybatis/sql_extractor.py +78 -0
  95. package/templates/qoder/scripts/domain/kg/extract/prd/__init__.py +8 -0
  96. package/templates/qoder/scripts/domain/kg/extract/prd/prd_chunk_embed.py +105 -0
  97. package/templates/qoder/scripts/domain/kg/extract/prd/prd_llm_extract.py +153 -0
  98. package/templates/qoder/scripts/domain/kg/extract/prd/req_anchor.py +120 -0
  99. package/templates/qoder/scripts/domain/kg/graph/kg_semantic.py +4 -2
  100. package/templates/qoder/scripts/domain/kg/kg.py +42 -5
  101. package/templates/qoder/scripts/domain/kg/search/_remote.py +187 -0
  102. package/templates/qoder/scripts/domain/kg/search/context_pack.py +32 -2
  103. package/templates/qoder/scripts/domain/kg/search/search_index.py +74 -20
  104. package/templates/qoder/scripts/domain/kg/switch_project.py +159 -0
  105. package/templates/qoder/scripts/domain/kg/sync_repowiki.py +109 -0
  106. package/templates/qoder/scripts/domain/task/__pycache__/wlkj_panel.cpython-39.pyc +0 -0
  107. package/templates/qoder/scripts/domain/task/{zentao_panel.py → wlkj_panel.py} +315 -53
  108. package/templates/qoder/scripts/engine/poller.py +219 -0
  109. package/templates/qoder/scripts/foundation/__pycache__/__init__.cpython-39.pyc +0 -0
  110. package/templates/qoder/scripts/foundation/core/__pycache__/__init__.cpython-39.pyc +0 -0
  111. package/templates/qoder/scripts/foundation/core/__pycache__/paths.cpython-39.pyc +0 -0
  112. package/templates/qoder/scripts/foundation/integrations/active_task.py +2 -1
  113. package/templates/qoder/scripts/orchestration/wlkj.py +4 -0
  114. package/templates/qoder/scripts/protocol/__pycache__/__init__.cpython-39.pyc +0 -0
  115. package/templates/qoder/scripts/protocol/mcp/zentao_mcp_server.py +1 -1
  116. package/templates/qoder/scripts/protocol/transports/__pycache__/__init__.cpython-39.pyc +0 -0
  117. package/templates/qoder/scripts/protocol/transports/__pycache__/base.cpython-39.pyc +0 -0
  118. package/templates/qoder/scripts/protocol/transports/__pycache__/cli.cpython-39.pyc +0 -0
  119. package/templates/qoder/scripts/protocol/transports/__pycache__/http.cpython-39.pyc +0 -0
  120. package/templates/qoder/scripts/protocol/transports/__pycache__/stdio.cpython-39.pyc +0 -0
  121. package/templates/qoder/scripts/protocol/transports/http.py +7 -1
  122. package/templates/root/AGENTS.md +9 -10
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: wl-test
3
- description: "测试: quick(意图式用例,默认) / browser(Playwright浏览器) / unit(单元) / coverage(覆盖矩阵)。"
3
+ description: "测试: quick(意图式用例,默认) / browser(Playwright浏览器) / unit(单元) / coverage(测试覆盖矩阵)。"
4
4
  argument-hint: "[quick|browser|unit|coverage] <描述或任务>"
5
5
  auto-approve: false
6
6
  allowed-tools: [Read, Glob, Grep, Bash, Write, Edit]
@@ -91,6 +91,10 @@ $PY "$R/.qoder/scripts/orchestration/wlkj.py" test quick \
91
91
  $PY "$R/.qoder/scripts/orchestration/wlkj.py" test quick --record '{"Q-1":"pass","Q-2":"fail"}'
92
92
  ```
93
93
 
94
+ ### 下一步
95
+ - 用例全过 → `/wl-commit <[#REQ-xxx] message>` 提交回流(双通飞轮:quick 测 → commit 回流 → `/wl-task finish <REQ-ID>` 收尾)
96
+ - 有失败 → 列原因,修复后重跑 quick,再 commit
97
+
94
98
  ---
95
99
 
96
100
  ## browser 基于任务的浏览器测试(需先有任务)
@@ -124,6 +128,17 @@ python -c "import json; d=json.load(open('$R/data/index/test-pages.json')); prin
124
128
  输出:哪些功能/页面有测试用例(test-pages.json + test-assertions.json),哪些是空白。
125
129
  AI 据此建议"优先补 XX 功能的测试"。
126
130
 
131
+ ### 改动驱动的必跑测试(提交前门禁 · S4 覆盖推理)
132
+
133
+ 改完代码要确定"必跑哪些测试"时,用覆盖推理四件套(已注册 MCP):
134
+ ```python
135
+ cap.mcp.call("impact_coverage", {"entity_ids": ["<改动实体id1>", "<id2>"]}) # 改动集→必跑测试
136
+ cap.mcp.call("find_tests_of", {"entity_id": "<某实体id>"}) # 查某实体的测试
137
+ cap.mcp.call("find_coverage_of", {"req_id": "<REQ-ID>"}) # 查某需求的覆盖
138
+ ```
139
+ - `impact_coverage` 拿"这次改动必须跑哪些测试"——提交前用它圈定回归范围,不漏不改崩。
140
+ - 改动实体 id 从 `rag_search`/`search_code` 命中里取。
141
+
127
142
  ---
128
143
 
129
144
  ## unit 单元测试
@@ -50,12 +50,16 @@ module: policy
50
50
  endpoints: [POST /api/policy/amend, GET /api/policy/amend/{id}]
51
51
  status: draft
52
52
  zentao: task=1453 story=761
53
+ zentao_id: 761
54
+ platform_spec_id:
53
55
  -->
54
56
  ```
55
57
 
56
58
  **字段说明**:
57
59
  - `status`:状态机见下表,/wl-code spec gate 的判据。
58
60
  - `zentao`:禅道 task 来的必须填(task+story);本地来的留空,待回填时补。回填 task 后 spec 摘要写入该 task 的 `desc`。
61
+ - `zentao_id`:禅道 story 号(关联键,= zentao 的 story)。spec_upload 据此让平台反查 Requirement 挂载;缺失则上传暂存待产品推禅道后回填。
62
+ - `platform_spec_id`:平台 spec id(幂等锚)。spec_upload 首次 create 后自动回写,**勿手填**;有值则下次上传走 update_spec(新增版本不重复建)。
59
63
 
60
64
  **`status` 状态机**(/wl-code spec gate 的判据):
61
65
 
@@ -107,6 +107,42 @@ def _dev():
107
107
  return None
108
108
 
109
109
 
110
+ def _auto_learn_from_prd(prd_path):
111
+ """PRD 写完后自动调 MCP learn 沉淀业务规则(宿主能力接入)。
112
+
113
+ 读 PRD 标题 → 调工作台 MCP learn → 知识层自动积累。
114
+ 失败静默(铁律:hook 崩溃不阻塞会话)。
115
+ """
116
+ try:
117
+ title = os.path.basename(prd_path).replace('.md', '')
118
+ # 读 mcp_config 拿 token
119
+ cfg_path = os.path.join(BASE, 'wlinkj-workflow', 'mcp_config.json')
120
+ if not os.path.isfile(cfg_path):
121
+ return
122
+ import json as _j
123
+ cfg = _j.load(open(cfg_path, encoding='utf-8'))
124
+ srv = cfg.get('mcpServers', {}).get('wlinkj-knowledge', {})
125
+ token = srv.get('token', '')
126
+ url = srv.get('url', '').replace('/mcp', '/mcp') # 保持 /mcp
127
+ pid = srv.get('env', {}).get('WLKJ_PROJECT_ID', '')
128
+ if not token or not url or not pid:
129
+ return
130
+ # 调 MCP learn(POST /mcp JSON-RPC)
131
+ import urllib.request
132
+ rpc = {"jsonrpc": "2.0", "id": 1, "method": "tools/call",
133
+ "params": {"name": "learn",
134
+ "arguments": {"project_id": pid, "category": "decision",
135
+ "pattern": "PRD更新: " + title,
136
+ "source": "qoder-hook-auto"}}}
137
+ req = urllib.request.Request(
138
+ url, data=_j.dumps(rpc).encode(),
139
+ headers={"Content-Type": "application/json", "X-MCP-Token": token},
140
+ method="POST")
141
+ urllib.request.urlopen(req, timeout=5).read() # 5s 超时,不阻塞
142
+ except Exception:
143
+ pass # 铁律:静默
144
+
145
+
110
146
  def _append_event(event, data):
111
147
  """best-effort append 一条事件到当前 dev 的 feedback.jsonl。避开 learn import 断链, 直接写文件。"""
112
148
  try:
@@ -160,8 +196,13 @@ def main():
160
196
  plow = p.lower().replace('\\', '/')
161
197
  if 'prd' in os.path.basename(plow).lower() and plow.endswith('.md'):
162
198
  _append_event('prd_written', {'prd': os.path.basename(p), 'tool': tool_name})
199
+ # ★ 宿主能力接入:PRD 写完 → 自动调 MCP learn 沉淀业务规则
200
+ _auto_learn_from_prd(p)
163
201
  if plow.endswith('task.json'):
164
202
  _append_event('task_touched', {'task_json': os.path.basename(os.path.dirname(p)), 'tool': tool_name})
203
+ # ★ 宿主能力接入:代码文件改完 → 自动记 code_changed(知识层下次检测过时用)
204
+ if any(plow.endswith(ext) for ext in ('.java', '.vue', '.ts', '.js', '.py')):
205
+ _append_event('code_changed', {'file': os.path.basename(p), 'path': plow[:200]})
165
206
  # v3.1 软提示: prototype HTML 写完但缺来源锚定 → stderr 提醒(不阻断)
166
207
  if 'prototype' in os.path.basename(plow) and plow.endswith('.html'):
167
208
  try:
@@ -134,7 +134,7 @@ def get_index_info():
134
134
  pass
135
135
 
136
136
  lines.append('## Knowledge Graph')
137
- lines.append('MCP工具 (QoderWork自动调用): search_code / get_impact / coverage_matrix / get_workflow17个')
137
+ lines.append('MCP工具 (Qoder/QoderWork自动调用): search_code / rag_search / get_impact / project_brief / graphrag_ask43个')
138
138
  lines.append(' python .qoder/scripts/orchestration/wlkj.py search <keyword> [--platform web|app]')
139
139
  lines.append(' python .qoder/scripts/orchestration/wlkj.py context <keyword> # 一次打包全部上下文')
140
140
 
@@ -149,72 +149,40 @@ def get_index_info():
149
149
  role = _line.split('=', 1)[1].strip()
150
150
  except Exception:
151
151
  pass
152
- db_path = os.path.join(BASE, 'data', 'index', 'kg.duckdb')
153
-
154
- if os.path.isfile(db_path):
155
- import time as _time
156
- db_mtime = os.path.getmtime(db_path)
157
- db_age_hours = (_time.time() - db_mtime) / 3600
158
-
159
- # 检查代码是否比 DuckDB (采样几个文件)
160
- code_dir = os.path.join(BASE, 'data', 'code')
161
- code_newer = False
162
- if os.path.isdir(code_dir):
163
- for root, dirs, files in os.walk(code_dir):
164
- dirs[:] = [d for d in dirs if d not in ('node_modules', '.git', 'dist', '__pycache__')]
165
- depth = root[len(code_dir):].count(os.sep)
166
- if depth >= 3:
167
- dirs[:] = []
168
- continue
169
- for fn in files:
170
- if fn.endswith(('.vue', '.java', '.ts')):
171
- try:
172
- if os.path.getmtime(os.path.join(root, fn)) > db_mtime:
173
- code_newer = True
174
- break
175
- except OSError:
176
- pass
177
- if code_newer:
178
- break
179
-
180
- lines.append(' kg.duckdb: %.0f小时前构建' % db_age_hours)
181
-
182
- if role in ('admin', 'dev'):
183
- # 管理员/开发: 能构建
184
- if code_newer or db_age_hours > 168: # 7天 = 168小时
185
- lines.append(' [WARN] 知识图谱过期! 代码有新变更.')
186
- lines.append(' → 管理员请运行: python .qoder/scripts/kg/kg_build.py')
187
- lines.append(' → 完成后: python .qoder/scripts/orchestration/wlkj.py sync push (共享给团队)')
188
- else:
189
- lines.append(' ✓ 知识图谱最新')
190
- else:
191
- # 非管理员: 只拉取
192
- if code_newer or db_age_hours > 168:
193
- lines.append(' [WARN] 知识图谱可能过期.')
194
- lines.append(' → 运行: python .qoder/scripts/orchestration/wlkj.py sync pull (获取管理员最新构建)')
195
- else:
196
- lines.append(' ✓ 知识图谱最新')
197
-
198
- # 检查是否有未标注的模块 (提示AI做语义标注)
199
- try:
200
- import subprocess as _sp
201
- # v3.x: kg_semantic.py 已迁移到 domain/kg/graph/ (旧扁平路径不存在)
202
- r = _sp.run([sys.executable, os.path.join(BASE, '.qoder', 'scripts', 'domain', 'kg', 'graph', 'kg_semantic.py'), '--check'],
203
- capture_output=True, text=True, encoding='utf-8', timeout=5)
204
- if r.stdout and 'semantic_labeling_needed' in r.stdout:
205
- import json as _json
206
- data = _json.loads(r.stdout)
207
- unlabeled = len(data.get('modules', []))
208
- if unlabeled > 0:
209
- lines.append(' [提示] %d个模块未标注中文名, 运行 kg_semantic.py --check 查看' % unlabeled)
210
- except Exception:
211
- pass
152
+ # 知识层健康检查:全迁 PG 后不再检查本地 kg.duckdb(已删除),改探活 MCP 知识层。
153
+ # 复用 wlinkj-workflow/mcp_config.json 的 token+url,调 get_knowledge_health 拿真实健康分。
154
+ # best-effort:任何失败都不阻塞会话启动(与其它 hook 铁律一致)。
155
+ kg_info = None
156
+ try:
157
+ import json as _json
158
+ import urllib.request as _url
159
+ cfg_path = os.path.join(BASE, 'wlinkj-workflow', 'mcp_config.json')
160
+ if os.path.isfile(cfg_path):
161
+ _cfg = _json.load(open(cfg_path, encoding='utf-8'))
162
+ _srv = _cfg.get('mcpServers', {}).get('wlinkj-knowledge', {})
163
+ _url_s = _srv.get('url', '')
164
+ _tok = _srv.get('token', '')
165
+ if _url_s and _tok and _url_s != 'WILL_BE_FILLED_BY_WL_INIT':
166
+ _rpc = {"jsonrpc": "2.0", "id": 1, "method": "tools/call",
167
+ "params": {"name": "get_knowledge_health", "arguments": {}}}
168
+ _req = _url.Request(_url_s, data=_json.dumps(_rpc).encode(),
169
+ headers={"Content-Type": "application/json",
170
+ "X-MCP-Token": _tok}, method="POST")
171
+ _resp = _url.urlopen(_req, timeout=4).read()
172
+ kg_info = _json.loads(_json.loads(_resp)["result"]["content"][0]["text"])
173
+ except Exception:
174
+ kg_info = None
175
+
176
+ if kg_info and isinstance(kg_info, dict) and "error" not in kg_info:
177
+ lines.append(' ✓ 知识层在线 (PG@5432): 健康分 %s / 覆盖 %s / 新鲜 %s / 完整 %s' % (
178
+ kg_info.get("score", "?"), kg_info.get("coverage", "?"),
179
+ kg_info.get("freshness", "?"), kg_info.get("completeness", "?")))
180
+ issues = kg_info.get("issues", []) or []
181
+ if issues and role in ('admin', 'dev'):
182
+ lines.append(' [提示] 健康问题: ' + '; '.join(str(i)[:60] for i in issues[:2]))
212
183
  else:
213
- lines.append(' [MISSING] kg.duckdb 不存在!')
214
- if role in ('admin', 'dev'):
215
- lines.append(' → 运行: python .qoder/scripts/kg/kg_build.py')
216
- else:
217
- lines.append(' → 运行: python .qoder/scripts/orchestration/wlkj.py sync pull')
184
+ lines.append(' [WARN] 知识层未响应(MCP 后端未启动?)')
185
+ lines.append(' → 启动: cd wlinkj-workspace && start_pg.bat(后端 :10010)')
218
186
 
219
187
  return NL.join(lines)
220
188
 
@@ -1,6 +1,6 @@
1
1
  # -*- coding: utf-8 -*-
2
2
  """
3
- nexus.adapters —— 各宿主/降级路径的 Cap 实现。
3
+ capability.adapters —— 各宿主/降级路径的 Cap 实现。
4
4
 
5
5
  设计模式: Adapter + Bridge(Cap 抽象 × 宿主实现 正交)
6
6
 
@@ -23,7 +23,7 @@ from __future__ import annotations
23
23
 
24
24
  from typing import Any, Optional
25
25
 
26
- from ..registry_mcp import McpRegistry
26
+ from ..registry_mcp import McpRegistry, _load_servers
27
27
  from protocol.transports import ToolResult
28
28
 
29
29
 
@@ -42,7 +42,7 @@ class McpCap:
42
42
  """L1 MCP 能力统一入口 —— 复用已有 MCP server(禅道/知识图谱/MySQL/蓝湖)。
43
43
 
44
44
  工作流调用:
45
- cap = nexus.resolve()
45
+ cap = resolve()
46
46
  result = cap.mcp.call("search_code", {"keyword": "保险"})
47
47
  # 自动路由:MCP 可用走 MCP,不可用走 wlkj.py(CLI 降级)
48
48
  """
@@ -54,8 +54,9 @@ class McpCap:
54
54
  # spawn 子进程, 不依赖宿主是否原生支持 MCP —— CLI / Codex / Claude 同样能起 server。
55
55
  # 故: 宿主未声明 mcp_servers 或声明为空 = "没有宿主原生限制, 用全部本地可起的"。
56
56
  if mcp_servers: # 非空列表才过滤
57
- from ..registry_mcp import SERVERS
58
- filtered = {k: v for k, v in SERVERS.items() if k in mcp_servers}
57
+ # _load_servers() 而非硬编码 SERVERS —— 这样 mcp_config.json 改配置才生效
58
+ all_servers = _load_servers()
59
+ filtered = {k: v for k, v in all_servers.items() if k in mcp_servers}
59
60
  self._registry = McpRegistry(servers=filtered)
60
61
  else:
61
62
  self._registry = _get_registry()
@@ -117,6 +118,12 @@ class McpCap:
117
118
  "search_wiki": ("kg", ["wiki"]),
118
119
  "fill_prototype": ("kg", ["fill-prototype"]),
119
120
  "get_design_system": ("kg", ["design-system"]),
121
+ # RAG/GraphRAG 降级映射(MCP 挂时走本地 wlkj.py kg search,语义降级为关键词)
122
+ "rag_search": ("kg", ["search"]),
123
+ "ask_corpus": ("kg", ["search"]),
124
+ # 学习工具降级(MCP 挂时本地无 learning 表,返回提示)
125
+ "learn": ("noop", []),
126
+ "get_learnings": ("noop", []),
120
127
  "list_tasks": ("task", ["list"]),
121
128
  "create_task": ("task", ["create"]),
122
129
  "start_task": ("task", ["start"]),
@@ -129,10 +136,16 @@ class McpCap:
129
136
  if entry is None:
130
137
  return None
131
138
  cmd, base_args = entry
139
+ # noop 类工具(learn/get_learnings 等):本地无等价 CLI,返回提示不崩
140
+ if cmd == "noop":
141
+ return json.dumps({"degraded": True, "message": "MCP不可用,该工具(learning)降级跳过"}, ensure_ascii=False)
132
142
  cli_args = list(base_args)
133
- # 从 MCP args 构建命令行参数
134
- if "keyword" in args:
135
- cli_args.append(str(args["keyword"]))
143
+ # 从 MCP args 构建命令行参数。
144
+ # 参数名对齐: 平台 MCP search 工具入参是 query, 引擎命令文档用 keyword。
145
+ # 这里两边都取值, 确保无论上游传哪个都能落到 CLI 查询词上。
146
+ kw = args.get("keyword") or args.get("query")
147
+ if kw is not None:
148
+ cli_args.append(str(kw))
136
149
  if "platform" in args and args["platform"]:
137
150
  cli_args.extend(["--platform", str(args["platform"])])
138
151
  if "role" in args and args["role"]:
@@ -211,128 +224,91 @@ class McpCap:
211
224
 
212
225
 
213
226
  class McpMemoryProvider:
214
- """memory 职责链的 MCP provider —— 接通 kg.duckdb learning_events 表。
227
+ """memory 职责链的 MCP provider —— 接通平台 PG.learning_patterns。
215
228
 
216
- 设计模式: Adapter(把 kg.duckdb 适配成 MemoryCap 契约)
229
+ 设计模式: Adapter(把平台 MCP get_learnings/learn 适配成 MemoryCap 契约)
217
230
 
218
- 职责链位置: 链头优先(available=True),纯 CLI 环境也能用。
219
- mem0/Zep MCP server 更成熟时,可替换为 MCP 版本,接口不变。
231
+ 职责链位置: 链头优先。平台 MCP 可用(has_tool get_learnings)时抢占,
232
+ 复用项目级学习表(PG.learning_patterns,按 token 解析 pid 隔离,跨会话积累)。
220
233
 
221
- 数据源: kg.duckdb learning_events 表(id, ts, dev, event, data)。
222
- 复用 learn_aggregate.py 的写入逻辑,保证 CLI 和 MCP 行为一致。
234
+ 数据源: 平台 MCP 工具 get_learnings / learn(后端 services.knowledge.learn)。
223
235
  """
224
236
 
225
237
  def __init__(self):
226
- self._available: bool | None = None # 懒探测
238
+ self._registry = None # 懒取 MCP 单例(避免 import 时即连后端)
239
+
240
+ def _reg(self):
241
+ """复用 McpCap 同款模块级单例(_get_registry),零重复 spawn。"""
242
+ if self._registry is None:
243
+ self._registry = _get_registry()
244
+ return self._registry
227
245
 
228
246
  @property
229
247
  def available(self) -> bool:
230
- """kg.duckdb 存在且可读写即为可用。"""
231
- if self._available is None:
232
- try:
233
- from foundation.protocol.duckdb_conn import get_db
234
- con = get_db()
235
- con.execute("""CREATE TABLE IF NOT EXISTS learning_events (
236
- id TEXT PRIMARY KEY, ts TEXT NOT NULL, dev TEXT NOT NULL,
237
- event TEXT NOT NULL, data JSON)""")
238
- con.close()
239
- self._available = True
240
- except Exception:
241
- self._available = False
242
- return self._available
248
+ """平台 MCP 可用且 get_learnings 工具在索引中即为可用。
249
+ 全迁 PG 后记忆走平台 get_learnings(PG.learning_patterns,按项目隔离)。
250
+ """
251
+ try:
252
+ return self._reg().has_tool("get_learnings")
253
+ except Exception:
254
+ return False
243
255
 
244
256
  def recall(self, query: str) -> list[dict]:
245
- """语义检索记忆: event data 模糊匹配。"""
257
+ """检索记忆: 取平台学习记录,按 query 模糊过滤 pattern/category。"""
246
258
  if not self.available:
247
259
  return []
248
260
  try:
249
- from foundation.protocol.duckdb_conn import get_db
250
- con = get_db()
251
- rows = con.execute(
252
- """SELECT id, ts, dev, event, data FROM learning_events
253
- WHERE event LIKE '%' || ? || '%' OR data LIKE '%' || ? || '%'
254
- ORDER BY ts DESC LIMIT 20""",
255
- [query, query],
256
- ).fetchall()
257
- con.close()
258
- return [
259
- {"id": r[0], "ts": r[1], "dev": r[2], "event": r[3], "data": r[4]}
260
- for r in rows
261
- ]
261
+ import json as _json
262
+ result = self._reg().call("get_learnings", {"limit": 50})
263
+ text = getattr(result, "text", "") or ""
264
+ data = _json.loads(text) if text else {}
265
+ items = data.get("items", []) if isinstance(data, dict) else []
266
+ q = (query or "").lower()
267
+ out = []
268
+ for it in items:
269
+ hay = "%s %s" % (it.get("category", ""), it.get("pattern", ""))
270
+ if not q or q in hay.lower():
271
+ out.append({
272
+ "event": it.get("category", ""),
273
+ "data": it.get("pattern", ""),
274
+ "ts": it.get("last_seen", ""),
275
+ "dev": ",".join(it.get("source_devs", []) or []),
276
+ })
277
+ return out[:20]
262
278
  except Exception:
263
279
  return []
264
280
 
265
281
  def remember(self, key: str, content: str) -> None:
266
- """写入一条记忆: 插入 learning_events 表。"""
282
+ """写入一条记忆: 调平台 learn(PG.learning_patterns 累加)。"""
267
283
  if not self.available:
268
284
  return
269
285
  try:
270
- import hashlib
271
- import json
272
- import time as _time
273
- # ⚠ 不能 import init_schema! duckdb_conn 故意不导出它 (领域 DDL 留在各域)。
274
- # 旧版 from ...duckdb_conn import init_schema → ImportError → 被下面 except 吞掉
275
- # → INSERT 永不执行 → remember() 静默丢全部写入。
276
- # learning_events 表已由 self.available 的 CREATE TABLE IF NOT EXISTS 保证存在,
277
- # 这里直接 INSERT 即可。
278
- from foundation.protocol.duckdb_conn import get_db
279
- con = get_db()
280
- eid = hashlib.md5(("%s:%s:%s" % (key, content, _time.time())).encode()).hexdigest()[:16]
281
- ts = _time.strftime("%Y-%m-%dT%H:%M:%S")
282
- data = json.dumps({"content": content}, ensure_ascii=False)
283
- con.execute(
284
- "INSERT OR IGNORE INTO learning_events(id, ts, dev, event, data) VALUES (?,?,?,?,?)",
285
- [eid, ts, "", key, data],
286
- )
287
- con.close()
286
+ self._reg().call("learn", {
287
+ "category": "rule",
288
+ "pattern": "%s: %s" % (key, content[:500]),
289
+ "source": "engine/memory",
290
+ })
288
291
  except Exception:
289
- pass
292
+ pass # 写入失败不阻塞
290
293
 
291
294
  def get_fact(self, key: str) -> str | None:
292
- """按 event 精确查找,返回 data 字段(JSON 字符串)。"""
293
- if not self.available:
294
- return None
295
- try:
296
- from foundation.protocol.duckdb_conn import get_db
297
- con = get_db()
298
- row = con.execute(
299
- "SELECT data FROM learning_events WHERE event = ? ORDER BY ts DESC LIMIT 1",
300
- [key],
301
- ).fetchone()
302
- con.close()
303
- if not row:
304
- return None
305
- data = row[0]
306
- if isinstance(data, str):
307
- return data
308
- import json as _json
309
- return _json.dumps(data, ensure_ascii=False) if data else None
310
- except Exception:
311
- return None
295
+ """按 key 取一条事实,无则 None。"""
296
+ results = self.recall(key)
297
+ return results[0].get("data") if results else None
312
298
 
313
299
  def list_recent(self, limit: int = 10) -> list[dict]:
314
- """最近 N 条记忆。"""
300
+ """最近 N 条记忆(平台按 frequency 倒序返回)。"""
315
301
  if not self.available:
316
302
  return []
317
303
  try:
318
304
  import json as _json
319
- from foundation.protocol.duckdb_conn import get_db
320
- con = get_db()
321
- rows = con.execute(
322
- "SELECT id, ts, dev, event, data FROM learning_events ORDER BY ts DESC LIMIT ?",
323
- [limit],
324
- ).fetchall()
325
- con.close()
326
- results = []
327
- for r in rows:
328
- data = r[4]
329
- # DuckDB JSON 列可能返回字符串,需解析
330
- if isinstance(data, str):
331
- try:
332
- data = _json.loads(data)
333
- except (_json.JSONDecodeError, TypeError):
334
- data = {"raw": data}
335
- results.append({"id": r[0], "ts": r[1], "dev": r[2], "event": r[3], "data": data or {}})
336
- return results
305
+ result = self._reg().call("get_learnings", {"limit": limit})
306
+ text = getattr(result, "text", "") or ""
307
+ data = _json.loads(text) if text else {}
308
+ items = data.get("items", []) if isinstance(data, dict) else []
309
+ return [{"event": it.get("category", ""), "data": it.get("pattern", ""),
310
+ "ts": it.get("last_seen", ""), "dev": ",".join(it.get("source_devs", []) or [])}
311
+ for it in items[:limit]]
337
312
  except Exception:
338
313
  return []
314
+