@hupan56/wlkj 2.7.12 → 3.1.2

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 (253) hide show
  1. package/bin/cli.js +1435 -1107
  2. package/package.json +29 -29
  3. package/templates/qoder/agents/insight-planning.md +1 -1
  4. package/templates/qoder/agents/insight-research.md +28 -15
  5. package/templates/qoder/commands/optional/wl-insight.md +161 -161
  6. package/templates/qoder/commands/optional/wl-report.md +20 -39
  7. package/templates/qoder/commands/optional/wl-status.md +4 -4
  8. package/templates/qoder/commands/wl-code.md +2 -2
  9. package/templates/qoder/commands/wl-design.md +6 -6
  10. package/templates/qoder/commands/wl-init.md +3 -3
  11. package/templates/qoder/commands/wl-prd.md +26 -13
  12. package/templates/qoder/commands/wl-req.md +43 -0
  13. package/templates/qoder/commands/wl-search.md +78 -47
  14. package/templates/qoder/commands/wl-task.md +343 -40
  15. package/templates/qoder/commands/wl-test.md +43 -17
  16. package/templates/qoder/config.yaml +52 -14
  17. package/templates/qoder/hooks/post-tool-use.py +181 -0
  18. package/templates/qoder/hooks/session-start.py +94 -23
  19. package/templates/qoder/hooks/stop-eval.py +207 -0
  20. package/templates/qoder/hooks/user-prompt-submit.py +139 -0
  21. package/templates/qoder/rules/wl-pipeline.md +85 -49
  22. package/templates/qoder/scripts/README.md +102 -0
  23. package/templates/qoder/scripts/capability/__init__.py +26 -0
  24. package/templates/qoder/scripts/capability/__main__.py +72 -0
  25. package/templates/qoder/scripts/capability/adapters/__init__.py +29 -0
  26. package/templates/qoder/scripts/capability/adapters/cli.py +316 -0
  27. package/templates/qoder/scripts/capability/adapters/mcp.py +334 -0
  28. package/templates/qoder/scripts/capability/adapters/qw.py +271 -0
  29. package/templates/qoder/scripts/capability/caps/__init__.py +27 -0
  30. package/templates/qoder/scripts/capability/caps/context.py +36 -0
  31. package/templates/qoder/scripts/capability/caps/cron.py +50 -0
  32. package/templates/qoder/scripts/capability/caps/identity.py +43 -0
  33. package/templates/qoder/scripts/capability/caps/memory.py +71 -0
  34. package/templates/qoder/scripts/capability/caps/notify.py +41 -0
  35. package/templates/qoder/scripts/capability/caps/present.py +48 -0
  36. package/templates/qoder/scripts/capability/caps/repo.py +31 -0
  37. package/templates/qoder/scripts/capability/caps/sandbox.py +43 -0
  38. package/templates/qoder/scripts/capability/chain.py +92 -0
  39. package/templates/qoder/scripts/capability/memory_chain.py +41 -0
  40. package/templates/qoder/scripts/capability/registry.py +246 -0
  41. package/templates/qoder/scripts/capability/registry_mcp.py +250 -0
  42. package/templates/qoder/scripts/capability/smoke_test.py +211 -0
  43. package/templates/qoder/scripts/capability/smoke_test_report.json +94 -0
  44. package/templates/qoder/scripts/deployment/setup/__init__.py +11 -0
  45. package/templates/qoder/scripts/deployment/setup/carriers.py +669 -0
  46. package/templates/qoder/scripts/deployment/setup/carriers_verify.py +148 -0
  47. package/templates/qoder/scripts/{init_doctor.py → deployment/setup/init_doctor.py} +100 -39
  48. package/templates/qoder/scripts/{install_qoderwork.py → deployment/setup/install_qoderwork.py} +48 -30
  49. package/templates/qoder/scripts/{platform_doctor.py → deployment/setup/platform_doctor.py} +271 -259
  50. package/templates/qoder/scripts/{repo_root.py → deployment/setup/repo_root.py} +9 -2
  51. package/templates/qoder/scripts/{setup.py → deployment/setup/setup.py} +143 -14
  52. package/templates/qoder/scripts/{setup_lanhu.py → deployment/setup/setup_lanhu.py} +979 -963
  53. package/templates/qoder/scripts/domain/design/__init__.py +0 -0
  54. package/templates/qoder/scripts/{fill_prototype.py → domain/design/fill_prototype.py} +17 -5
  55. package/templates/qoder/scripts/{gen_design_doc.py → domain/design/gen_design_doc.py} +406 -394
  56. package/templates/qoder/scripts/domain/kg/__init__.py +11 -0
  57. package/templates/qoder/scripts/domain/kg/build/__init__.py +0 -0
  58. package/templates/qoder/scripts/domain/kg/build/build_entity_registry.py +201 -0
  59. package/templates/qoder/scripts/domain/kg/build/build_relations.py +132 -0
  60. package/templates/qoder/scripts/{build_style_index.py → domain/kg/build/build_style_index.py} +50 -15
  61. package/templates/qoder/scripts/domain/kg/build/build_workflows.py +149 -0
  62. package/templates/qoder/scripts/{kg_build.py → domain/kg/build/kg_build.py} +685 -612
  63. package/templates/qoder/scripts/{kg_build_db.py → domain/kg/build/kg_build_db.py} +338 -327
  64. package/templates/qoder/scripts/{kg_incremental.py → domain/kg/build/kg_incremental.py} +425 -393
  65. package/templates/qoder/scripts/{learn_aggregate.py → domain/kg/build/learn_aggregate.py} +212 -201
  66. package/templates/qoder/scripts/domain/kg/extract/__init__.py +0 -0
  67. package/templates/qoder/scripts/{common → domain/kg/extract}/extract.py +430 -419
  68. package/templates/qoder/scripts/domain/kg/extract/test_extract.py +115 -0
  69. package/templates/qoder/scripts/{common → domain/kg/extract}/ts_extract.py +614 -536
  70. package/templates/qoder/scripts/domain/kg/graph/__init__.py +0 -0
  71. package/templates/qoder/scripts/{kg_link_db.py → domain/kg/graph/kg_link_db.py} +235 -224
  72. package/templates/qoder/scripts/domain/kg/graph/kg_semantic.py +683 -0
  73. package/templates/qoder/scripts/{kg.py → domain/kg/kg.py} +871 -708
  74. package/templates/qoder/scripts/domain/kg/kg_capabilities.py +182 -0
  75. package/templates/qoder/scripts/domain/kg/search/__init__.py +0 -0
  76. package/templates/qoder/scripts/{context_pack.py → domain/kg/search/context_pack.py} +45 -17
  77. package/templates/qoder/scripts/{enrich_prompt.py → domain/kg/search/enrich_prompt.py} +238 -226
  78. package/templates/qoder/scripts/domain/kg/search/prefetch.py +384 -0
  79. package/templates/qoder/scripts/{common → domain/kg/search}/search_engine.py +207 -205
  80. package/templates/qoder/scripts/{search_index.py → domain/kg/search/search_index.py} +210 -88
  81. package/templates/qoder/scripts/domain/kg/server/__init__.py +0 -0
  82. package/templates/qoder/scripts/domain/kg/server/kgd.py +549 -0
  83. package/templates/qoder/scripts/domain/kg/server/perf_bench.py +197 -0
  84. package/templates/qoder/scripts/domain/kg/storage/__init__.py +0 -0
  85. package/templates/qoder/scripts/{kg_duckdb.py → domain/kg/storage/kg_duckdb.py} +70 -45
  86. package/templates/qoder/scripts/domain/learning/__init__.py +0 -0
  87. package/templates/qoder/scripts/{learn.py → domain/learning/learn.py} +157 -146
  88. package/templates/qoder/scripts/domain/report/__init__.py +11 -0
  89. package/templates/qoder/scripts/{add_session.py → domain/report/add_session.py} +256 -244
  90. package/templates/qoder/scripts/{export.py → domain/report/export.py} +72 -4
  91. package/templates/qoder/scripts/{report.py → domain/report/report.py} +292 -281
  92. package/templates/qoder/scripts/domain/report/report_snapshot.py +261 -0
  93. package/templates/qoder/scripts/domain/report/role.py +39 -0
  94. package/templates/qoder/scripts/{status.py → domain/report/status.py} +737 -628
  95. package/templates/qoder/scripts/domain/report/status_snapshot.py +191 -0
  96. package/templates/qoder/scripts/domain/requirement/__init__.py +0 -0
  97. package/templates/qoder/scripts/{archive_prd.py → domain/requirement/archive_prd.py} +389 -377
  98. package/templates/qoder/scripts/domain/requirement/req.py +228 -0
  99. package/templates/qoder/scripts/domain/task/__init__.py +11 -0
  100. package/templates/qoder/scripts/{git_sync.py → domain/task/git_sync.py} +90 -50
  101. package/templates/qoder/scripts/{syncgate.py → domain/task/syncgate.py} +18 -7
  102. package/templates/qoder/scripts/domain/task/task.py +229 -0
  103. package/templates/qoder/scripts/domain/task/task_lifecycle.py +606 -0
  104. package/templates/qoder/scripts/domain/task/task_query.py +162 -0
  105. package/templates/qoder/scripts/domain/task/task_relations.py +425 -0
  106. package/templates/qoder/scripts/{team_sync.py → domain/task/team_sync.py} +101 -23
  107. package/templates/qoder/scripts/domain/task/zentao_sync.py +370 -0
  108. package/templates/qoder/scripts/foundation/__init__.py +0 -0
  109. package/templates/qoder/scripts/foundation/bootstrap.py +145 -0
  110. package/templates/qoder/scripts/foundation/core/__init__.py +0 -0
  111. package/templates/qoder/scripts/foundation/core/cmd_registry.py +112 -0
  112. package/templates/qoder/scripts/foundation/core/config.py +187 -0
  113. package/templates/qoder/scripts/{common → foundation/core}/paths.py +706 -400
  114. package/templates/qoder/scripts/foundation/data/__init__.py +0 -0
  115. package/templates/qoder/scripts/foundation/data/contract.py +317 -0
  116. package/templates/qoder/scripts/{common → foundation/data}/events.py +2 -2
  117. package/templates/qoder/scripts/foundation/data/result.py +223 -0
  118. package/templates/qoder/scripts/{common → foundation/data}/task_utils.py +427 -392
  119. package/templates/qoder/scripts/foundation/identity/__init__.py +0 -0
  120. package/templates/qoder/scripts/foundation/identity/check_publish.py +103 -0
  121. package/templates/qoder/scripts/{common → foundation/identity}/developer.py +239 -238
  122. package/templates/qoder/scripts/foundation/identity/guard.py +159 -0
  123. package/templates/qoder/scripts/{common → foundation/identity}/identity.py +132 -9
  124. package/templates/qoder/scripts/foundation/identity/roles.py +60 -0
  125. package/templates/qoder/scripts/foundation/integrations/__init__.py +0 -0
  126. package/templates/qoder/scripts/{common → foundation/integrations}/active_task.py +30 -15
  127. package/templates/qoder/scripts/{common → foundation/integrations}/feishu.py +11 -10
  128. package/templates/qoder/scripts/{common → foundation/integrations}/terms.py +3 -3
  129. package/templates/qoder/scripts/foundation/integrations/zentao_client.py +220 -0
  130. package/templates/qoder/scripts/foundation/io/__init__.py +0 -0
  131. package/templates/qoder/scripts/{common → foundation/io}/filelock.py +1 -1
  132. package/templates/qoder/scripts/{common → foundation/io}/reqid.py +1 -1
  133. package/templates/qoder/scripts/foundation/protocol/__init__.py +0 -0
  134. package/templates/qoder/scripts/foundation/protocol/duckdb_conn.py +39 -0
  135. package/templates/qoder/scripts/foundation/protocol/mcp_base.py +268 -0
  136. package/templates/qoder/scripts/foundation/utils/__init__.py +0 -0
  137. package/templates/qoder/scripts/{common → foundation/utils}/eval_api.py +12 -1
  138. package/templates/qoder/scripts/{common → foundation/utils}/pip_install.py +23 -6
  139. package/templates/qoder/scripts/{common → foundation/utils}/platform_guard.py +1 -1
  140. package/templates/qoder/scripts/orchestration/__init__.py +0 -0
  141. package/templates/qoder/scripts/orchestration/wlkj.py +185 -0
  142. package/templates/qoder/scripts/protocol/__init__.py +0 -0
  143. package/templates/qoder/scripts/protocol/browser/README.md +207 -0
  144. package/templates/qoder/scripts/protocol/browser/SKILL.md +265 -0
  145. package/templates/qoder/scripts/protocol/browser/config.env +9 -0
  146. package/templates/qoder/scripts/protocol/browser/references/cdp-api.md +110 -0
  147. package/templates/qoder/scripts/protocol/browser/references/migration-2.5.3.md +72 -0
  148. package/templates/qoder/scripts/protocol/browser/references/site-patterns/.gitkeep +0 -0
  149. package/templates/qoder/scripts/protocol/browser/scripts/browser-discovery.mjs +138 -0
  150. package/templates/qoder/scripts/protocol/browser/scripts/cdp-proxy.mjs +672 -0
  151. package/templates/qoder/scripts/protocol/browser/scripts/check-deps.mjs +206 -0
  152. package/templates/qoder/scripts/protocol/browser/scripts/find-url.mjs +253 -0
  153. package/templates/qoder/scripts/protocol/browser/scripts/match-site.mjs +46 -0
  154. package/templates/qoder/scripts/protocol/browser/templates/config.env.template +9 -0
  155. package/templates/qoder/scripts/protocol/mcp/__init__.py +11 -0
  156. package/templates/qoder/scripts/protocol/mcp/kg_mcp_server.py +478 -0
  157. package/templates/qoder/scripts/{lanhu_stdio_wrapper.py → protocol/mcp/lanhu_stdio_wrapper.py} +131 -119
  158. package/templates/qoder/scripts/protocol/mcp/mcp_doctor.py +548 -0
  159. package/templates/qoder/scripts/protocol/mcp/mcp_launcher.py +600 -0
  160. package/templates/qoder/scripts/protocol/mcp/mysql_mcp_server.py +461 -0
  161. package/templates/qoder/scripts/protocol/mcp/zentao_mcp_server.py +1920 -0
  162. package/templates/qoder/scripts/protocol/transports/__init__.py +56 -0
  163. package/templates/qoder/scripts/protocol/transports/base.py +87 -0
  164. package/templates/qoder/scripts/protocol/transports/cli.py +132 -0
  165. package/templates/qoder/scripts/protocol/transports/http.py +141 -0
  166. package/templates/qoder/scripts/protocol/transports/stdio.py +293 -0
  167. package/templates/qoder/scripts/run_weekly_update.bat +27 -18
  168. package/templates/qoder/scripts/run_weekly_update.sh +22 -13
  169. package/templates/qoder/scripts/validation/__init__.py +0 -0
  170. package/templates/qoder/scripts/validation/eval/__init__.py +0 -0
  171. package/templates/qoder/scripts/validation/eval/perf-report-2026-06-24.md +139 -0
  172. package/templates/qoder/scripts/validation/eval/qwork_harness.py +617 -0
  173. package/templates/qoder/scripts/validation/eval/report-commands.md +220 -0
  174. package/templates/qoder/scripts/validation/eval/transcript_timing.py +386 -0
  175. package/templates/qoder/scripts/validation/metrics/__init__.py +0 -0
  176. package/templates/qoder/scripts/{eval_prd.py → validation/metrics/eval_prd.py} +84 -15
  177. package/templates/qoder/scripts/validation/metrics/usability_score.py +750 -0
  178. package/templates/qoder/scripts/validation/test/__init__.py +11 -0
  179. package/templates/qoder/scripts/validation/test/assertion_gen.py +551 -0
  180. package/templates/qoder/scripts/{autotest.py → validation/test/autotest.py} +1234 -1751
  181. package/templates/qoder/scripts/validation/test/autotest_auth.py +109 -0
  182. package/templates/qoder/scripts/{autotest_batch.py → validation/test/autotest_batch.py} +255 -224
  183. package/templates/qoder/scripts/validation/test/autotest_data.py +680 -0
  184. package/templates/qoder/scripts/validation/test/autotest_opencli.py +646 -0
  185. package/templates/qoder/scripts/{autotest_run.py → validation/test/autotest_run.py} +323 -297
  186. package/templates/qoder/scripts/validation/test/autotest_webaccess.py +571 -0
  187. package/templates/qoder/scripts/{benchmark.py → validation/test/benchmark.py} +17 -5
  188. package/templates/qoder/scripts/{kg_auto_login.py → validation/test/kg_auto_login.py} +208 -196
  189. package/templates/qoder/scripts/{kg_test_runner.py → validation/test/kg_test_runner.py} +13 -2
  190. package/templates/qoder/scripts/{page_probe.py → validation/test/page_probe.py} +470 -459
  191. package/templates/qoder/scripts/validation/test/smoke_all_commands.py +170 -0
  192. package/templates/qoder/settings.json +23 -2
  193. package/templates/qoder/skills/design-import/SKILL.md +235 -226
  194. package/templates/qoder/skills/design-review/SKILL.md +91 -82
  195. package/templates/qoder/skills/prd-generator/SKILL.md +38 -24
  196. package/templates/qoder/skills/prd-review/SKILL.md +8 -6
  197. package/templates/qoder/skills/prototype-generator/SKILL.md +296 -247
  198. package/templates/qoder/skills/spec-coder/SKILL.md +24 -15
  199. package/templates/qoder/skills/spec-generator/SKILL.md +24 -12
  200. package/templates/qoder/skills/test-generator/SKILL.md +9 -7
  201. package/templates/qoder/skills/wl-code/SKILL.md +25 -13
  202. package/templates/qoder/skills/wl-commit/SKILL.md +10 -9
  203. package/templates/qoder/skills/wl-design/SKILL.md +7 -5
  204. package/templates/qoder/skills/wl-init/SKILL.md +8 -8
  205. package/templates/qoder/skills/wl-insight/SKILL.md +25 -12
  206. package/templates/qoder/skills/wl-prd-full/SKILL.md +59 -8
  207. package/templates/qoder/skills/wl-prd-quick/SKILL.md +7 -7
  208. package/templates/qoder/skills/wl-prd-review/SKILL.md +18 -6
  209. package/templates/qoder/skills/wl-report/SKILL.md +27 -25
  210. package/templates/qoder/skills/wl-search/SKILL.md +151 -80
  211. package/templates/qoder/skills/wl-spec/SKILL.md +9 -7
  212. package/templates/qoder/skills/wl-status/SKILL.md +50 -18
  213. package/templates/qoder/skills/wl-task/SKILL.md +94 -12
  214. package/templates/qoder/skills/wl-test/SKILL.md +154 -45
  215. package/templates/qoder/templates/prd-full-template.md +7 -0
  216. package/templates/root/AGENTS.md +275 -237
  217. package/templates/root/requirements.txt +3 -0
  218. package/templates/root//344/275/277/347/224/250/350/257/264/346/230/216.md +3 -3
  219. package/templates/root//346/226/260/346/211/213/346/214/207/345/215/227.md +2 -2
  220. package/templates/qoder/hooks/inject-workflow-state.py +0 -169
  221. package/templates/qoder/scripts/__pycache__/check_mcp_launch.cpython-39.pyc +0 -0
  222. package/templates/qoder/scripts/__pycache__/install_qoderwork.cpython-39.pyc +0 -0
  223. package/templates/qoder/scripts/__pycache__/mcp_launcher.cpython-39.pyc +0 -0
  224. package/templates/qoder/scripts/__pycache__/platform_doctor.cpython-39.pyc +0 -0
  225. package/templates/qoder/scripts/check_carriers.py +0 -238
  226. package/templates/qoder/scripts/check_mcp.py +0 -298
  227. package/templates/qoder/scripts/check_mcp_launch.py +0 -183
  228. package/templates/qoder/scripts/check_qoderwork_consistency.py +0 -166
  229. package/templates/qoder/scripts/collect_prds.py +0 -31
  230. package/templates/qoder/scripts/common/mentions.py +0 -134
  231. package/templates/qoder/scripts/common/utf8.py +0 -38
  232. package/templates/qoder/scripts/extract_api_params.py +0 -246
  233. package/templates/qoder/scripts/extract_routes.py +0 -54
  234. package/templates/qoder/scripts/extract_routes_tree.py +0 -78
  235. package/templates/qoder/scripts/handoff.py +0 -22
  236. package/templates/qoder/scripts/init_developer.py +0 -76
  237. package/templates/qoder/scripts/kg_mcp_server.py +0 -801
  238. package/templates/qoder/scripts/kg_semantic.py +0 -150
  239. package/templates/qoder/scripts/mcp_launcher.py +0 -414
  240. package/templates/qoder/scripts/mysql_mcp_server.py +0 -396
  241. package/templates/qoder/scripts/parse_prds.py +0 -33
  242. package/templates/qoder/scripts/role.py +0 -51
  243. package/templates/qoder/scripts/sync_carriers.py +0 -259
  244. package/templates/qoder/scripts/task.py +0 -1261
  245. package/templates/qoder/scripts/workspace_init.py +0 -102
  246. package/templates/qoder/scripts/zentao_mcp_server.py +0 -424
  247. package/templates/qoder/skills/prompt-enrich/SKILL.md +0 -90
  248. /package/templates/qoder/scripts/{common → deployment}/__init__.py +0 -0
  249. /package/templates/qoder/{skills/prototype-generator/SKILL.md.zcode-79180-2af4721f-f9a6-412c-88db-c0af680d211b.tmp → scripts/domain/__init__.py} +0 -0
  250. /package/templates/qoder/scripts/{common → domain/kg/graph}/graph_traverse.py +0 -0
  251. /package/templates/qoder/scripts/{common → domain/kg/server}/repowiki.py +0 -0
  252. /package/templates/qoder/scripts/{common → foundation/io}/atomicio.py +0 -0
  253. /package/templates/qoder/scripts/{secure-ls.js → validation/test/secure-ls.js} +0 -0
@@ -0,0 +1,334 @@
1
+ # -*- coding: utf-8 -*-
2
+ """
3
+ adapters.mcp —— L1 MCP 能力统一入口(已接通 transport 层)。
4
+
5
+ 设计模式: Adapter + Facade
6
+
7
+ 职责:
8
+ 把 McpRegistry(tool→server→transport 路由)适配成 McpCap 的
9
+ has_tool/call/list_tools 接口,供 Nexus Facade 统一暴露。
10
+
11
+ L1 能力定位:禅道/知识图谱/MySQL/蓝湖/playwright 已 MCP 化,
12
+ 通过本入口跨宿主统一调用,复用已有 MCP 资产,绝不重写它们。
13
+
14
+ 跨宿主行为:
15
+ - QoderWork / 任意宿主:通过 mcp_launcher spawn 本地 MCP server(stdio transport)
16
+ - 未来服务器:改 McpRegistry.SERVERS 配置 kind=http + url,工作流零改动
17
+ - 无可用 server:available=False,has_tool 返回 False(静默回退)
18
+
19
+ McpMemoryProvider:memory 职责链的 MCP provider,接通 kg.duckdb learning_events 表。
20
+ 当 mem0/Zep MCP server 更成熟时可替换为 MCP 版本,接口不变。
21
+ """
22
+ from __future__ import annotations
23
+
24
+ from typing import Any, Optional
25
+
26
+ from ..registry_mcp import McpRegistry
27
+ from protocol.transports import ToolResult
28
+
29
+
30
+ # 单例 Registry(同一进程复用进程池,避免重复 spawn)
31
+ _REGISTRY: Optional[McpRegistry] = None
32
+
33
+
34
+ def _get_registry() -> McpRegistry:
35
+ global _REGISTRY
36
+ if _REGISTRY is None:
37
+ _REGISTRY = McpRegistry()
38
+ return _REGISTRY
39
+
40
+
41
+ class McpCap:
42
+ """L1 MCP 能力统一入口 —— 复用已有 MCP server(禅道/知识图谱/MySQL/蓝湖)。
43
+
44
+ 工作流调用:
45
+ cap = nexus.resolve()
46
+ result = cap.mcp.call("search_code", {"keyword": "保险"})
47
+ # 自动路由:MCP 可用走 MCP,不可用走 wlkj.py(CLI 降级)
48
+ """
49
+
50
+ def __init__(self, mcp_servers=None):
51
+ # ★ 空 mcp_servers (= [] 或 None) 视为"不限制 = 用全部本地 server"。
52
+ # 原版: mcp_servers=[] (空列表, is not None) → filtered 成空 → _servers={} → 0 工具,
53
+ # 整个 cap.mcp 自废武功。但本地 stdio transport (StdioTransport) 通过 mcp_launcher
54
+ # spawn 子进程, 不依赖宿主是否原生支持 MCP —— CLI / Codex / Claude 同样能起 server。
55
+ # 故: 宿主未声明 mcp_servers 或声明为空 = "没有宿主原生限制, 用全部本地可起的"。
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}
59
+ self._registry = McpRegistry(servers=filtered)
60
+ else:
61
+ self._registry = _get_registry()
62
+
63
+ @property
64
+ def available(self) -> bool:
65
+ """是否有任一 MCP server 可用。"""
66
+ try:
67
+ return self._registry.available()
68
+ except Exception:
69
+ return False
70
+
71
+ # ------------------------------------------------------------------
72
+ # McpCap 接口
73
+ # ------------------------------------------------------------------
74
+
75
+ def has_tool(self, name: str) -> bool:
76
+ """某 MCP 工具是否可用(跨所有已配置 server 查找)。"""
77
+ try:
78
+ return self._registry.has_tool(name)
79
+ except Exception:
80
+ return False
81
+
82
+ def call(self, name: str, args: Optional[dict] = None,
83
+ server: Optional[str] = None) -> ToolResult:
84
+ """调用 MCP 工具(自动 CLI 降级)。
85
+
86
+ 优先走 MCP;MCP 不可用时自动降级到 wlkj.py CLI 脚本。
87
+ 工作流永远写 cap.mcp.call("tool_name", args),零宿主感知。
88
+ """
89
+ # 1. MCP 可用 → 走 MCP
90
+ result = self._registry.call(name, args, server=server)
91
+ if not result.is_error:
92
+ return result
93
+ # 2. CLI 降级(MCP 不可用或调用失败)
94
+ cli_text = self._cli_fallback(name, args or {})
95
+ if cli_text is not None:
96
+ return ToolResult(text=cli_text)
97
+ return result # 无降级映射,返回原始错误
98
+
99
+ # ------------------------------------------------------------------
100
+ # CLI 降级(MCP 不可用时走 wlkj.py)
101
+ # ------------------------------------------------------------------
102
+
103
+ # MCP 工具名 → (wlkj 命令, 基础参数) 映射
104
+ # 只映射有 wlkj.py 对应的知识图谱/任务工具。
105
+ # MySQL/禅道/蓝湖等 MCP 专属工具不映射(MCP 不可用时返回错误提示)。
106
+ _TOOL_TO_WLKJ = {
107
+ "search_code": ("kg", ["search"]),
108
+ "search_api": ("kg", ["api"]),
109
+ "search_prd": ("kg", ["prd"]),
110
+ "context_pack": ("kg", ["context"]),
111
+ "get_impact": ("kg", ["impact"]),
112
+ "context_360": ("kg", ["context360"]),
113
+ "coverage_matrix": ("kg", ["coverage"]),
114
+ "feature_overview": ("kg", ["feature"]),
115
+ "get_workflow": ("kg", ["workflow"]),
116
+ "multi_hop": ("kg", ["hop"]),
117
+ "search_wiki": ("kg", ["wiki"]),
118
+ "fill_prototype": ("kg", ["fill-prototype"]),
119
+ "get_design_system": ("kg", ["design-system"]),
120
+ "list_tasks": ("task", ["list"]),
121
+ "create_task": ("task", ["create"]),
122
+ "start_task": ("task", ["start"]),
123
+ "finish_task": ("task", ["finish"]),
124
+ }
125
+
126
+ def _cli_fallback(self, tool_name: str, args: dict) -> Optional[str]:
127
+ """MCP 工具名 → wlkj.py 执行。无映射返回 None。"""
128
+ entry = self._TOOL_TO_WLKJ.get(tool_name)
129
+ if entry is None:
130
+ return None
131
+ cmd, base_args = entry
132
+ cli_args = list(base_args)
133
+ # 从 MCP args 构建命令行参数
134
+ if "keyword" in args:
135
+ cli_args.append(str(args["keyword"]))
136
+ if "platform" in args and args["platform"]:
137
+ cli_args.extend(["--platform", str(args["platform"])])
138
+ if "role" in args and args["role"]:
139
+ cli_args.extend(["--role", str(args["role"])])
140
+ if "endpoint" in args:
141
+ cli_args.append(str(args["endpoint"]))
142
+ if "symbol" in args:
143
+ cli_args.append(str(args["symbol"]))
144
+ if "depth" in args:
145
+ cli_args.extend(["--depth", str(args["depth"])])
146
+ if "feature" in args:
147
+ cli_args.append(str(args["feature"]))
148
+ if "module" in args:
149
+ cli_args.append(str(args["module"]))
150
+ if "title" in args:
151
+ cli_args.append(str(args["title"]))
152
+ if "table" in args:
153
+ cli_args.append(str(args["table"]))
154
+ if "column" in args:
155
+ cli_args.append(str(args["column"]))
156
+ if "db" in args and args["db"]:
157
+ cli_args.extend(["--db", str(args["db"])])
158
+ if "env" in args and args["env"]:
159
+ cli_args.extend(["--env", str(args["env"])])
160
+ return self._run_wlkj(cmd, cli_args)
161
+
162
+ @staticmethod
163
+ def _run_wlkj(cmd: str, args: list) -> str:
164
+ """调 wlkj.py 执行,返回 stdout。"""
165
+ import subprocess, sys, os
166
+ try:
167
+ from foundation.core.paths import get_repo_root
168
+ repo = str(get_repo_root())
169
+ except Exception:
170
+ repo = os.getcwd()
171
+ wlkj = os.path.join(repo, ".qoder", "scripts", "orchestration", "wlkj.py")
172
+ try:
173
+ r = subprocess.run(
174
+ [sys.executable, wlkj, cmd] + args,
175
+ capture_output=True, text=True, timeout=60,
176
+ encoding="utf-8", errors="replace",
177
+ )
178
+ if r.returncode == 0:
179
+ return r.stdout.strip()
180
+ return "[cli-fallback error] %s" % (r.stderr or r.stdout)[:200]
181
+ except subprocess.TimeoutExpired:
182
+ return "[cli-fallback error] timeout (60s)"
183
+ except Exception as e:
184
+ return "[cli-fallback error] %s" % str(e)[:100]
185
+
186
+ def list_tools(self, keyword: str = "") -> list[dict]:
187
+ """枚举可用 MCP 工具(可选按 keyword 过滤)。"""
188
+ try:
189
+ self._registry._ensure_index()
190
+ tools = []
191
+ for server_name in self._registry._servers:
192
+ t = self._registry._pool.get(server_name)
193
+ if t is None:
194
+ continue
195
+ for tool in (t._tools_cache or []):
196
+ if keyword and keyword not in tool.get("name", ""):
197
+ continue
198
+ tool_copy = dict(tool)
199
+ tool_copy["_server"] = server_name
200
+ tools.append(tool_copy)
201
+ return tools
202
+ except Exception:
203
+ return []
204
+
205
+ def describe(self) -> dict:
206
+ """诊断:各 server 状态。"""
207
+ try:
208
+ return self._registry.describe()
209
+ except Exception as e:
210
+ return {"error": str(e)[:120]}
211
+
212
+
213
+ class McpMemoryProvider:
214
+ """memory 职责链的 MCP provider —— 接通 kg.duckdb learning_events 表。
215
+
216
+ 设计模式: Adapter(把 kg.duckdb 适配成 MemoryCap 契约)
217
+
218
+ 职责链位置: 链头优先(available=True),纯 CLI 环境也能用。
219
+ 当 mem0/Zep MCP server 更成熟时,可替换为 MCP 版本,接口不变。
220
+
221
+ 数据源: kg.duckdb 的 learning_events 表(id, ts, dev, event, data)。
222
+ 复用 learn_aggregate.py 的写入逻辑,保证 CLI 和 MCP 行为一致。
223
+ """
224
+
225
+ def __init__(self):
226
+ self._available: bool | None = None # 懒探测
227
+
228
+ @property
229
+ 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
243
+
244
+ def recall(self, query: str) -> list[dict]:
245
+ """语义检索记忆: 按 event 或 data 模糊匹配。"""
246
+ if not self.available:
247
+ return []
248
+ 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
+ ]
262
+ except Exception:
263
+ return []
264
+
265
+ def remember(self, key: str, content: str) -> None:
266
+ """写入一条记忆: 插入 learning_events 表。"""
267
+ if not self.available:
268
+ return
269
+ try:
270
+ import hashlib
271
+ import json
272
+ import time as _time
273
+ from foundation.protocol.duckdb_conn import get_db, init_schema
274
+ con = get_db()
275
+ init_schema(con)
276
+ eid = hashlib.md5(("%s:%s:%s" % (key, content, _time.time())).encode()).hexdigest()[:16]
277
+ ts = _time.strftime("%Y-%m-%dT%H:%M:%S")
278
+ data = json.dumps({"content": content}, ensure_ascii=False)
279
+ con.execute(
280
+ "INSERT OR IGNORE INTO learning_events(id, ts, dev, event, data) VALUES (?,?,?,?,?)",
281
+ [eid, ts, "", key, data],
282
+ )
283
+ con.close()
284
+ except Exception:
285
+ pass
286
+
287
+ def get_fact(self, key: str) -> str | None:
288
+ """按 event 精确查找,返回 data 字段(JSON 字符串)。"""
289
+ if not self.available:
290
+ return None
291
+ try:
292
+ from foundation.protocol.duckdb_conn import get_db
293
+ con = get_db()
294
+ row = con.execute(
295
+ "SELECT data FROM learning_events WHERE event = ? ORDER BY ts DESC LIMIT 1",
296
+ [key],
297
+ ).fetchone()
298
+ con.close()
299
+ if not row:
300
+ return None
301
+ data = row[0]
302
+ if isinstance(data, str):
303
+ return data
304
+ import json as _json
305
+ return _json.dumps(data, ensure_ascii=False) if data else None
306
+ except Exception:
307
+ return None
308
+
309
+ def list_recent(self, limit: int = 10) -> list[dict]:
310
+ """最近 N 条记忆。"""
311
+ if not self.available:
312
+ return []
313
+ try:
314
+ import json as _json
315
+ from foundation.protocol.duckdb_conn import get_db
316
+ con = get_db()
317
+ rows = con.execute(
318
+ "SELECT id, ts, dev, event, data FROM learning_events ORDER BY ts DESC LIMIT ?",
319
+ [limit],
320
+ ).fetchall()
321
+ con.close()
322
+ results = []
323
+ for r in rows:
324
+ data = r[4]
325
+ # DuckDB JSON 列可能返回字符串,需解析
326
+ if isinstance(data, str):
327
+ try:
328
+ data = _json.loads(data)
329
+ except (_json.JSONDecodeError, TypeError):
330
+ data = {"raw": data}
331
+ results.append({"id": r[0], "ts": r[1], "dev": r[2], "event": r[3], "data": data or {}})
332
+ return results
333
+ except Exception:
334
+ return []
@@ -0,0 +1,271 @@
1
+ # -*- coding: utf-8 -*-
2
+ """
3
+ adapters.qw —— QoderWork 原生能力 Adapter。
4
+
5
+ 设计模式: Adapter(适配 QoderWork 原生工具到 Cap 契约)
6
+
7
+ 实现 QoderWork 提供的 L2 宿主原生能力。其他宿主(ZCode/Codex)不适用,
8
+ 未实现的 Cap 由 NoOp 兜底。
9
+
10
+ QoderWork 提供的相关工具(参考):
11
+ memory / memory_search / memory_get → QwMemoryCap(转发到 MCP 层)
12
+ qoder_cron → QwCronCap(文件 + 系统 crontab)
13
+ qw_mcp_list/get/call → 走 McpCap(L1)
14
+ delegate_to_im → QwNotifyCap(后续补)
15
+ qoder_show_widget / present_files → QwPresentCap(后续补)
16
+
17
+ 本模块实现 QwMemoryCap + QwCronCap,其余后续补。
18
+ """
19
+ from __future__ import annotations
20
+
21
+ import os
22
+
23
+
24
+ def is_qoderwork() -> bool:
25
+ """探测当前是否运行在 QoderWork 宿主内。
26
+
27
+ QoderWork 注入 QODER_SESSION_ID(会话)或 QODER_MCP_AVAILABLE(MCP 网关)。
28
+ """
29
+ if os.environ.get("QODER_SESSION_ID"):
30
+ return True
31
+ return os.environ.get("QODER_MCP_AVAILABLE", "").lower() in ("1", "true", "yes")
32
+
33
+
34
+ class QwMemoryCap:
35
+ """QoderWork memory 工具适配器。
36
+
37
+ 设计模式: Adapter(QoderWork → MemoryCap 契约)
38
+
39
+ 策略: 优先使用 QoderWork 原生 memory 工具,降级到 MCP MemoryProvider。
40
+ 当前实现: 转发到 MCP 层的 McpMemoryProvider(接通 kg.duckdb)。
41
+ 未来: QoderWork 原生 memory 工具可用时,优先用原生。
42
+ """
43
+
44
+ def __init__(self):
45
+ self._env = is_qoderwork()
46
+ self._mcp_provider = None
47
+
48
+ def _get_mcp_provider(self):
49
+ """延迟获取 MCP MemoryProvider(避免循环导入)。"""
50
+ if self._mcp_provider is None:
51
+ try:
52
+ from .mcp import McpMemoryProvider
53
+ self._mcp_provider = McpMemoryProvider()
54
+ except Exception:
55
+ pass
56
+ return self._mcp_provider
57
+
58
+ @property
59
+ def available(self) -> bool:
60
+ """QoderWork 环境下可用(降级到 MCP 层)。"""
61
+ if self._env:
62
+ return True
63
+ # 非 QoderWork 环境,看 MCP provider 是否可用
64
+ provider = self._get_mcp_provider()
65
+ return bool(provider and provider.available)
66
+
67
+ def recall(self, query: str) -> list[dict]:
68
+ """语义检索记忆:优先 QoderWork 原生,降级 MCP。"""
69
+ # TODO: QoderWork 原生 memory_search 工具可用时优先调用
70
+ provider = self._get_mcp_provider()
71
+ if provider and provider.available:
72
+ return provider.recall(query)
73
+ return []
74
+
75
+ def remember(self, key: str, content: str) -> None:
76
+ """写入记忆:优先 QoderWork 原生,降级 MCP。"""
77
+ # TODO: QoderWork 原生 memory 工具可用时优先调用
78
+ provider = self._get_mcp_provider()
79
+ if provider and provider.available:
80
+ provider.remember(key, content)
81
+
82
+ def get_fact(self, key: str) -> str | None:
83
+ """按 key 取事实:优先 QoderWork 原生,降级 MCP。"""
84
+ provider = self._get_mcp_provider()
85
+ if provider and provider.available:
86
+ return provider.get_fact(key)
87
+ return None
88
+
89
+ def list_recent(self, limit: int = 10) -> list[dict]:
90
+ """最近 N 条记忆:优先 QoderWork 原生,降级 MCP。"""
91
+ provider = self._get_mcp_provider()
92
+ if provider and provider.available:
93
+ return provider.list_recent(limit)
94
+ return []
95
+
96
+
97
+ class QwCronCap:
98
+ """QoderWork cron 工具适配器。
99
+
100
+ 设计模式: Adapter(QoderWork → CronCap 契约)
101
+
102
+ 当前实现: 使用 common.events 的 cron 注册文件(.qoder/data/cron/)。
103
+ 未来: QoderWork 原生 qoder_cron 工具可用时优先用原生。
104
+ """
105
+
106
+ CRON_DIR = ".qoder" + os.sep + "data" + os.sep + "cron"
107
+
108
+ def __init__(self):
109
+ self._env = is_qoderwork()
110
+
111
+ @property
112
+ def available(self) -> bool:
113
+ """文件系统可写即可用。"""
114
+ try:
115
+ from pathlib import Path
116
+ repo_root = Path(__file__).resolve().parent.parent.parent.parent
117
+ cron_dir = repo_root / self.CRON_DIR
118
+ cron_dir.mkdir(parents=True, exist_ok=True)
119
+ return cron_dir.is_dir()
120
+ except Exception:
121
+ return False
122
+
123
+ def add(self, cron_expr: str, command: str, label: str = "") -> str:
124
+ """添加定时任务,返回 job_id。"""
125
+ import json
126
+ import time
127
+ from pathlib import Path
128
+
129
+ try:
130
+ repo_root = Path(__file__).resolve().parent.parent.parent.parent
131
+ cron_dir = repo_root / self.CRON_DIR
132
+ cron_dir.mkdir(parents=True, exist_ok=True)
133
+
134
+ job_id = "%s-%s" % (label or "job", int(time.time()))
135
+ job = {
136
+ "id": job_id,
137
+ "cron": cron_expr,
138
+ "command": command,
139
+ "label": label,
140
+ "enabled": True,
141
+ }
142
+ job_file = cron_dir / ("%s.json" % job_id)
143
+ with open(job_file, "w", encoding="utf-8") as f:
144
+ json.dump(job, f, ensure_ascii=False, indent=2)
145
+ return job_id
146
+ except Exception as e:
147
+ return "error: %s" % str(e)[:80]
148
+
149
+ def list(self) -> list[dict]:
150
+ """列出所有定时任务。"""
151
+ import json
152
+ from pathlib import Path
153
+
154
+ try:
155
+ repo_root = Path(__file__).resolve().parent.parent.parent.parent
156
+ cron_dir = repo_root / self.CRON_DIR
157
+ if not cron_dir.is_dir():
158
+ return []
159
+ jobs = []
160
+ for f in cron_dir.glob("*.json"):
161
+ try:
162
+ with open(f, encoding="utf-8") as fh:
163
+ jobs.append(json.load(fh))
164
+ except Exception:
165
+ continue
166
+ return sorted(jobs, key=lambda j: j.get("id", ""))
167
+ except Exception:
168
+ return []
169
+
170
+ def remove(self, job_id: str) -> bool:
171
+ """删除定时任务。"""
172
+ from pathlib import Path
173
+
174
+ try:
175
+ repo_root = Path(__file__).resolve().parent.parent.parent.parent
176
+ job_file = repo_root / self.CRON_DIR / ("%s.json" % job_id)
177
+ if job_file.is_file():
178
+ job_file.unlink()
179
+ return True
180
+ except Exception:
181
+ pass
182
+ return False
183
+
184
+
185
+ class QwPresentCap:
186
+ """QoderWork 富展示能力适配器。
187
+
188
+ 设计模式: Adapter(QoderWork qoder_show_widget → PresentCap 契约)
189
+
190
+ 把"渲染交互 HTML 到对话流"从宿主原生工具适配到 PresentCap.widget()。
191
+ 降级链: QoderWork 原生 qoder_show_widget → 落盘 HTML 文件(纯 CLI 兜底)。
192
+ """
193
+
194
+ def __init__(self):
195
+ self._env = is_qoderwork()
196
+
197
+ @property
198
+ def available(self) -> bool:
199
+ """QoderWork 环境且 qoder_show_widget 工具可达时可用。"""
200
+ if not self._env:
201
+ return False
202
+ # 探测宿主工具是否可达 (走 McpCap, 失败则降级)
203
+ try:
204
+ from .mcp import _get_registry
205
+ reg = _get_registry()
206
+ return reg.has_tool("qoder_show_widget")
207
+ except Exception:
208
+ return False
209
+
210
+ def _try_host_widget(self, html: str) -> bool:
211
+ """尝试调宿主 qoder_show_widget 把 HTML 嵌入对话流。成功返回 True。"""
212
+ try:
213
+ from .mcp import _get_registry
214
+ reg = _get_registry()
215
+ result = reg.call("qoder_show_widget", {"html": html})
216
+ return not result.is_error
217
+ except Exception:
218
+ return False
219
+
220
+ def widget(self, html: str) -> bool:
221
+ """展示交互 widget。优先嵌入对话流, 降级落盘 HTML 文件。"""
222
+ if self._try_host_widget(html):
223
+ return True
224
+ # 降级: 落盘到 journal (纯 CLI / 工具不可达时)
225
+ try:
226
+ from datetime import datetime
227
+ from pathlib import Path
228
+ repo_root = Path(__file__).resolve().parent.parent.parent.parent
229
+ # 找当前 dev
230
+ dev = "default"
231
+ dev_file = repo_root / ".qoder" / ".developer"
232
+ if dev_file.is_file():
233
+ for line in dev_file.read_text(encoding="utf-8").splitlines():
234
+ if "name=" in line:
235
+ dev = line.split("name=", 1)[1].strip().split()[0]
236
+ break
237
+ out_dir = repo_root / "workspace" / "members" / dev / "journal"
238
+ out_dir.mkdir(parents=True, exist_ok=True)
239
+ out = out_dir / ("dashboard-%s.html" % datetime.now().strftime("%Y%m%d-%H%M%S"))
240
+ out.write_text(html, encoding="utf-8")
241
+ print("[PresentCap] qoder_show_widget 不可达, 看板已落盘: %s" % out)
242
+ return False # 通知调用方: 未嵌入对话流(但已落盘)
243
+ except Exception:
244
+ return False
245
+
246
+ def table(self, rows: list, headers: list | None = None) -> str:
247
+ """渲染 markdown 表格 (兜底, 所有宿主都能看)。"""
248
+ if not rows:
249
+ return ""
250
+ headers = headers or (list(rows[0].keys()) if rows and isinstance(rows[0], dict) else [])
251
+ lines = ["| " + " | ".join(str(h) for h in headers) + " |",
252
+ "| " + " | ".join("---" for _ in headers) + " |"]
253
+ for r in rows:
254
+ if isinstance(r, dict):
255
+ lines.append("| " + " | ".join(str(r.get(h, "")) for h in headers) + " |")
256
+ else:
257
+ lines.append("| " + " | ".join(str(x) for x in r) + " |")
258
+ return "\n".join(lines)
259
+
260
+ def files(self, paths: list) -> bool:
261
+ """以文件卡片展示。降级: 打印路径列表。"""
262
+ try:
263
+ from .mcp import _get_registry
264
+ reg = _get_registry()
265
+ if reg.has_tool("present_files"):
266
+ result = reg.call("present_files", {"paths": paths})
267
+ return not result.is_error
268
+ except Exception:
269
+ pass
270
+ print("[PresentCap] present_files 不可达, 文件列表: " + ", ".join(str(p) for p in paths))
271
+ return False
@@ -0,0 +1,27 @@
1
+ # -*- coding: utf-8 -*-
2
+ """
3
+ nexus.caps —— L2 宿主原生能力契约层。
4
+
5
+ 每个能力 = Protocol(契约)+ NoOp(默认空实现)。
6
+ 新增能力(扩展场景 B)= 新建一个文件定义 Protocol + NoOp,在此导出,
7
+ 再在 registry.Nexus 加属性、_resolve_xxx 加装配。
8
+ """
9
+ from .memory import MemoryCap, NoOpMemoryCap
10
+ from .repo import RepoCap, NoOpRepoCap
11
+ from .identity import IdentityCap, NoOpIdentityCap
12
+ from .context import ContextCap, NoOpContextCap
13
+ from .cron import CronCap, NoOpCronCap
14
+ from .notify import NotifyCap, NoOpNotifyCap
15
+ from .present import PresentCap, NoOpPresentCap
16
+ from .sandbox import SandboxCap, NoOpSandboxCap
17
+
18
+ __all__ = [
19
+ "MemoryCap", "NoOpMemoryCap",
20
+ "RepoCap", "NoOpRepoCap",
21
+ "IdentityCap", "NoOpIdentityCap",
22
+ "ContextCap", "NoOpContextCap",
23
+ "CronCap", "NoOpCronCap",
24
+ "NotifyCap", "NoOpNotifyCap",
25
+ "PresentCap", "NoOpPresentCap",
26
+ "SandboxCap", "NoOpSandboxCap",
27
+ ]
@@ -0,0 +1,36 @@
1
+ # -*- coding: utf-8 -*-
2
+ """
3
+ ContextCap —— 上下文/偏好能力契约。
4
+
5
+ 核心能力(必有降级)。读取 USER 偏好、最近学到的经验等。
6
+ """
7
+ from __future__ import annotations
8
+
9
+ from typing import Protocol, runtime_checkable
10
+
11
+
12
+ @runtime_checkable
13
+ class ContextCap(Protocol):
14
+ """上下文/偏好能力。"""
15
+
16
+ available: bool
17
+
18
+ def preferences(self) -> dict:
19
+ """USER 偏好字典。"""
20
+ ...
21
+
22
+ def recent_learnings(self, limit: int = 5) -> list[dict]:
23
+ """最近学到的经验/事实。"""
24
+ ...
25
+
26
+
27
+ class NoOpContextCap:
28
+ """ContextCap 空实现。"""
29
+
30
+ available = False
31
+
32
+ def preferences(self) -> dict:
33
+ return {}
34
+
35
+ def recent_learnings(self, limit: int = 5) -> list[dict]:
36
+ return []