foxclaw 0.1.2 → 0.1.3

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 (344) hide show
  1. package/index.js +1 -8
  2. package/package.json +1 -1
  3. package/python/.env.example +8 -0
  4. package/python/.github/workflows/ci.yml +47 -2
  5. package/python/CHANGELOG.md +57 -0
  6. package/python/CHANGELOG_CN.md +57 -0
  7. package/python/README.md +11 -2
  8. package/python/README_CN.md +32 -20
  9. package/python/bench/bm25_audit.py +1 -14
  10. package/python/bench/bm25_regression.py +0 -1
  11. package/python/bench/bm25_stress.py +0 -4
  12. package/python/bench/cd_audit.py +0 -23
  13. package/python/bench/chaos.py +6 -8
  14. package/python/bench/e2e_real_models.py +0 -1
  15. package/python/bench/llm_retry_test.py +2 -9
  16. package/python/bench/load_test.py +5 -8
  17. package/python/bench/vector_ivf_upgrade.py +1 -7
  18. package/python/docs/RELEASE.md +4 -3
  19. package/python/docs/RUN_CN.md +2 -2
  20. package/python/evals/governance/cases.py +0 -37
  21. package/python/evals/governance/run_evals.py +5 -37
  22. package/python/examples/governance_middleware_demo.py +1 -6
  23. package/python/examples/langgraph_governance_demo.py +7 -11
  24. package/python/foxclaw/__init__.py +34 -152
  25. package/python/foxclaw/agents/base.py +65 -136
  26. package/python/foxclaw/agents/impl/_helpers.py +2 -10
  27. package/python/foxclaw/agents/impl/finance.py +4 -21
  28. package/python/foxclaw/agents/impl/office.py +7 -24
  29. package/python/foxclaw/agents/impl/supervisor.py +12 -79
  30. package/python/foxclaw/agents/mixins/_context.py +0 -23
  31. package/python/foxclaw/agents/mixins/augment.py +10 -16
  32. package/python/foxclaw/agents/mixins/llm.py +7 -6
  33. package/python/foxclaw/agents/mixins/memory.py +7 -1
  34. package/python/foxclaw/agents/mixins/preference.py +58 -8
  35. package/python/foxclaw/agents/mixins/prompt.py +0 -6
  36. package/python/foxclaw/agents/mixins/skillbox.py +6 -6
  37. package/python/foxclaw/agents/mixins/tool_exec.py +16 -42
  38. package/python/foxclaw/api/__init__.py +18 -77
  39. package/python/foxclaw/api/_state.py +43 -0
  40. package/python/foxclaw/api/{app.py → bootstrap.py} +5 -32
  41. package/python/foxclaw/api/middleware.py +5 -28
  42. package/python/foxclaw/api/routes/ahs.py +60 -73
  43. package/python/foxclaw/api/routes/attachments.py +5 -6
  44. package/python/foxclaw/api/routes/browser.py +7 -11
  45. package/python/foxclaw/api/routes/domain.py +18 -26
  46. package/python/foxclaw/api/routes/knowledge.py +3 -21
  47. package/python/foxclaw/api/routes/llm_admin.py +21 -33
  48. package/python/foxclaw/api/routes/memory.py +28 -12
  49. package/python/foxclaw/api/routes/orchestration.py +3 -5
  50. package/python/foxclaw/api/routes/recovery.py +4 -3
  51. package/python/foxclaw/api/routes/review.py +2 -11
  52. package/python/foxclaw/api/routes/search.py +3 -7
  53. package/python/foxclaw/api/routes/system.py +33 -49
  54. package/python/foxclaw/api/routes/task.py +31 -49
  55. package/python/foxclaw/api/routes/ws.py +2 -6
  56. package/python/foxclaw/api/spine.py +1 -4
  57. package/python/foxclaw/browser/action/action_executor.py +1 -5
  58. package/python/foxclaw/browser/action/action_types.py +0 -1
  59. package/python/foxclaw/browser/action/url_policy.py +1 -5
  60. package/python/foxclaw/browser/adapter/web_adapter.py +1 -1
  61. package/python/foxclaw/browser/driver/playwright_driver.py +20 -16
  62. package/python/foxclaw/browser/perception/dom_extractor.py +0 -1
  63. package/python/foxclaw/browser/perception/element_indexer.py +1 -2
  64. package/python/foxclaw/browser/service.py +7 -11
  65. package/python/foxclaw/capability/embedding_service.py +4 -6
  66. package/python/foxclaw/channels/__init__.py +9 -11
  67. package/python/foxclaw/channels/discord.py +4 -13
  68. package/python/foxclaw/channels/dispatcher.py +2 -27
  69. package/python/foxclaw/channels/feishu.py +10 -20
  70. package/python/foxclaw/channels/openclaw_bridge.py +4 -9
  71. package/python/foxclaw/channels/signing.py +9 -9
  72. package/python/foxclaw/channels/telegram.py +3 -4
  73. package/python/foxclaw/channels/weixin.py +7 -17
  74. package/python/foxclaw/cli.py +42 -13
  75. package/python/foxclaw/cognitive/reasoner.py +9 -10
  76. package/python/foxclaw/cognitive/state.py +18 -24
  77. package/python/foxclaw/core/audit_chain.py +1 -3
  78. package/python/foxclaw/core/audit_sink.py +3 -3
  79. package/python/foxclaw/core/compaction.py +8 -26
  80. package/python/foxclaw/core/config.py +2 -2
  81. package/python/foxclaw/core/confirm_nonce.py +6 -12
  82. package/python/foxclaw/core/envfile.py +2 -2
  83. package/python/foxclaw/core/failure_policy.py +17 -26
  84. package/python/foxclaw/core/gateway.py +6 -10
  85. package/python/foxclaw/core/governance_audit.py +3 -7
  86. package/python/foxclaw/core/idempotency.py +15 -22
  87. package/python/foxclaw/core/injection_guard.py +10 -29
  88. package/python/foxclaw/core/memory_adapter.py +30 -57
  89. package/python/foxclaw/core/metrics.py +4 -5
  90. package/python/foxclaw/core/model_router.py +36 -148
  91. package/python/foxclaw/core/paths.py +1 -1
  92. package/python/foxclaw/core/recovery.py +21 -34
  93. package/python/foxclaw/core/scheduler.py +23 -26
  94. package/python/foxclaw/core/secrets.py +0 -3
  95. package/python/foxclaw/core/sessions.py +4 -6
  96. package/python/foxclaw/core/settings.py +16 -32
  97. package/python/foxclaw/core/shared_write.py +11 -23
  98. package/python/foxclaw/core/steering_queue.py +12 -11
  99. package/python/foxclaw/core/system_bus.py +1 -1
  100. package/python/foxclaw/core/thought_chain.py +30 -32
  101. package/python/foxclaw/core/token_meter.py +9 -13
  102. package/python/foxclaw/core/tools.py +4 -12
  103. package/python/foxclaw/core/tracing.py +25 -40
  104. package/python/foxclaw/core/truth_pipeline.py +5 -9
  105. package/python/foxclaw/core/update_check.py +149 -0
  106. package/python/foxclaw/core/vec_recall_adapter.py +11 -37
  107. package/python/foxclaw/core/verify.py +6 -14
  108. package/python/foxclaw/core/web_search.py +4 -28
  109. package/python/foxclaw/finance/evidence.py +14 -18
  110. package/python/foxclaw/finance/fx.py +1 -3
  111. package/python/foxclaw/finance/graph.py +9 -18
  112. package/python/foxclaw/finance/models.py +20 -24
  113. package/python/foxclaw/finance/nlparse.py +12 -72
  114. package/python/foxclaw/finance/policy.py +8 -36
  115. package/python/foxclaw/finance/reasoner.py +3 -11
  116. package/python/foxclaw/finance/reasoning.py +13 -34
  117. package/python/foxclaw/finance/reconcile.py +5 -10
  118. package/python/foxclaw/finance/runtime.py +19 -46
  119. package/python/foxclaw/governance/middleware.py +13 -21
  120. package/python/foxclaw/harness/_base.py +2 -8
  121. package/python/foxclaw/harness/_policy.py +19 -38
  122. package/python/foxclaw/harness/drift.py +1 -13
  123. package/python/foxclaw/harness/event_bus.py +2 -5
  124. package/python/foxclaw/harness/harness.py +9 -97
  125. package/python/foxclaw/harness/loop_detection.py +11 -18
  126. package/python/foxclaw/itinerary/amap_client.py +2 -5
  127. package/python/foxclaw/itinerary/models.py +7 -7
  128. package/python/foxclaw/itinerary/reasoner.py +1 -6
  129. package/python/foxclaw/itinerary/reasoning.py +15 -21
  130. package/python/foxclaw/itinerary/runtime.py +8 -12
  131. package/python/foxclaw/kernel/capabilities.py +5 -6
  132. package/python/foxclaw/kernel/policy_gate.py +4 -10
  133. package/python/foxclaw/kernel/process_manager.py +17 -36
  134. package/python/foxclaw/kernel/scheduler.py +3 -9
  135. package/python/foxclaw/kernel/sentinel.py +13 -23
  136. package/python/foxclaw/kernel/sentinel_client.py +12 -15
  137. package/python/foxclaw/kernel/spine.py +9 -9
  138. package/python/foxclaw/knowledge/__init__.py +4 -20
  139. package/python/foxclaw/knowledge/truth_crystallizer.py +5 -8
  140. package/python/foxclaw/learning/knowledge_graph.py +4 -9
  141. package/python/foxclaw/learning/models.py +13 -13
  142. package/python/foxclaw/learning/reasoner.py +1 -4
  143. package/python/foxclaw/learning/runtime.py +19 -31
  144. package/python/foxclaw/learning/spaced_repetition.py +2 -7
  145. package/python/foxclaw/mcp_server.py +2 -3
  146. package/python/foxclaw/orchestration/__init__.py +16 -33
  147. package/python/foxclaw/pipeline/cognitive_pipeline.py +45 -97
  148. package/python/foxclaw/pipeline/stage_meta.py +21 -0
  149. package/python/foxclaw/pipeline/supervisor_stages.py +13 -42
  150. package/python/foxclaw/planning/llm_planner.py +3 -3
  151. package/python/foxclaw/planning/planner.py +6 -11
  152. package/python/foxclaw/search/agent_port.py +7 -11
  153. package/python/foxclaw/search/evidence.py +0 -4
  154. package/python/foxclaw/search/internal_search.py +3 -5
  155. package/python/foxclaw/search/models.py +11 -13
  156. package/python/foxclaw/search/planner.py +3 -3
  157. package/python/foxclaw/search/retriever.py +1 -1
  158. package/python/foxclaw/search/runtime.py +1 -8
  159. package/python/foxclaw/skills/__init__.py +46 -100
  160. package/python/foxclaw/skills/archive.py +111 -0
  161. package/python/foxclaw/skills/fetch.py +226 -0
  162. package/python/foxclaw/skills/hooks_spec.py +21 -25
  163. package/python/foxclaw/skills/install_cmd.py +216 -0
  164. package/python/foxclaw/skills/lifecycle.py +16 -42
  165. package/python/foxclaw/skills/markdown_skill.py +10 -9
  166. package/python/foxclaw/skills/mcp_client.py +5 -11
  167. package/python/foxclaw/skills/mcp_store.py +0 -3
  168. package/python/foxclaw/skills/permission.py +13 -48
  169. package/python/foxclaw/skills/pipeline.py +21 -53
  170. package/python/foxclaw/skills/policy.py +4 -10
  171. package/python/foxclaw/skills/review.py +3 -3
  172. package/python/frontend/deck/auth.js +0 -7
  173. package/python/frontend/deck/chat_model_picker.js +0 -54
  174. package/python/frontend/deck/domain.js +0 -20
  175. package/python/frontend/deck/main.js +0 -144
  176. package/python/frontend/deck/markdown.js +0 -4
  177. package/python/frontend/deck/settings.js +1 -18
  178. package/python/frontend/deck/state.js +0 -1
  179. package/python/frontend/deck/util.js +0 -2
  180. package/python/frontend/deck/visuals.js +0 -22
  181. package/python/frontend/planet/planet.js +0 -201
  182. package/python/frontend/v3/assets/index-CQSBdxk4.js +34 -0
  183. package/python/frontend/v3/assets/index-D5nIsjms.css +1 -0
  184. package/python/frontend/v3/index.html +3 -3
  185. package/python/frontend/widget_runtime.js +0 -43
  186. package/python/frontend/widgets.js +0 -57
  187. package/python/frontend_src/index.html +1 -1
  188. package/python/frontend_src/package-lock.json +2 -2
  189. package/python/frontend_src/package.json +1 -1
  190. package/python/frontend_src/src/App.vue +3 -7
  191. package/python/frontend_src/src/api.js +0 -7
  192. package/python/frontend_src/src/components/ChatDock.vue +27 -39
  193. package/python/frontend_src/src/components/DocViewer.vue +0 -9
  194. package/python/frontend_src/src/components/Inspector.vue +0 -6
  195. package/python/frontend_src/src/components/LeftRail.vue +60 -39
  196. package/python/frontend_src/src/components/MenuPopover.vue +9 -11
  197. package/python/frontend_src/src/components/MessageStream.vue +18 -30
  198. package/python/frontend_src/src/components/ModelPicker.vue +0 -5
  199. package/python/frontend_src/src/components/QuickIngest.vue +0 -6
  200. package/python/frontend_src/src/components/Stage.vue +71 -25
  201. package/python/frontend_src/src/components/ToolMenu.vue +0 -3
  202. package/python/frontend_src/src/components/TopBar.vue +38 -10
  203. package/python/frontend_src/src/components/panels/ActivityPanel.vue +0 -2
  204. package/python/frontend_src/src/components/panels/AgentsPanel.vue +2 -5
  205. package/python/frontend_src/src/components/panels/KnowledgePanel.vue +0 -16
  206. package/python/frontend_src/src/components/panels/MemoryPanel.vue +0 -1
  207. package/python/frontend_src/src/components/panels/PanelBox.vue +6 -7
  208. package/python/frontend_src/src/components/panels/ReviewPanel.vue +0 -16
  209. package/python/frontend_src/src/components/panels/SettingsPanel.vue +142 -38
  210. package/python/frontend_src/src/components/panels/TasksPanel.vue +0 -1
  211. package/python/frontend_src/src/components/panels/ToolsPanel.vue +125 -19
  212. package/python/frontend_src/src/lib/fmt.js +0 -4
  213. package/python/frontend_src/src/lib/i18n.js +53 -0
  214. package/python/frontend_src/src/lib/locales/en.js +173 -0
  215. package/python/frontend_src/src/lib/locales/zh.js +174 -0
  216. package/python/frontend_src/src/lib/md.js +0 -14
  217. package/python/frontend_src/src/main.js +2 -0
  218. package/python/frontend_src/src/stores/app.js +17 -32
  219. package/python/frontend_src/src/stores/chat.js +0 -34
  220. package/python/frontend_src/src/stores/model.js +0 -8
  221. package/python/frontend_src/src/stores/panels.js +0 -41
  222. package/python/frontend_src/src/stores/profile.js +31 -0
  223. package/python/frontend_src/src/stores/system.js +25 -0
  224. package/python/frontend_src/vite.config.js +0 -7
  225. package/python/pyproject.toml +6 -3
  226. package/python/requirements.txt +311 -0
  227. package/python/tests/conftest.py +0 -22
  228. package/python/tests/run_all.py +3 -66
  229. package/python/tests/test_agent_lifecycle.py +4 -38
  230. package/python/tests/test_ahs_p2.py +4 -25
  231. package/python/tests/test_anthropic_tools.py +1 -15
  232. package/python/tests/test_api.py +19 -9
  233. package/python/tests/test_api_auth.py +1 -5
  234. package/python/tests/test_attachments.py +9 -25
  235. package/python/tests/test_audit_chain.py +2 -7
  236. package/python/tests/test_audit_sink.py +0 -6
  237. package/python/tests/test_browser.py +2 -6
  238. package/python/tests/test_browser_request_guard.py +0 -4
  239. package/python/tests/test_capabilities.py +0 -6
  240. package/python/tests/test_channel_signing.py +2 -9
  241. package/python/tests/test_channels.py +1 -11
  242. package/python/tests/test_config_layering.py +0 -4
  243. package/python/tests/test_confirm_nonce.py +1 -12
  244. package/python/tests/test_context_compaction_wiring.py +1 -7
  245. package/python/tests/test_dispatch.py +0 -6
  246. package/python/tests/test_domain_evals.py +11 -80
  247. package/python/tests/test_domain_import.py +0 -6
  248. package/python/tests/test_domain_reasoners.py +0 -14
  249. package/python/tests/test_e2e.py +0 -17
  250. package/python/tests/test_emb_cache.py +7 -17
  251. package/python/tests/test_embedding_singleton.py +1 -3
  252. package/python/tests/test_evals_smoke.py +0 -24
  253. package/python/tests/test_event_spine.py +10 -18
  254. package/python/tests/test_evidence_semantic.py +1 -14
  255. package/python/tests/test_evidence_sse.py +3 -6
  256. package/python/tests/test_file_ingest.py +0 -11
  257. package/python/tests/test_finance.py +0 -9
  258. package/python/tests/test_finance_ledger.py +5 -12
  259. package/python/tests/test_finance_persist.py +8 -12
  260. package/python/tests/test_gateway.py +3 -22
  261. package/python/tests/test_governance.py +5 -52
  262. package/python/tests/test_governance_audit.py +4 -39
  263. package/python/tests/test_governance_middleware.py +1 -7
  264. package/python/tests/test_hkc.py +6 -24
  265. package/python/tests/test_hkc_index_build_manager.py +0 -33
  266. package/python/tests/test_hkc_manifest_reuse.py +4 -27
  267. package/python/tests/test_hkc_search_event_sync.py +3 -54
  268. package/python/tests/test_import_path_guard.py +0 -6
  269. package/python/tests/test_injection_guard.py +18 -36
  270. package/python/tests/test_integration.py +5 -29
  271. package/python/tests/test_itinerary.py +0 -3
  272. package/python/tests/test_kee_semantic.py +0 -5
  273. package/python/tests/test_kernel.py +1 -11
  274. package/python/tests/test_learning.py +3 -12
  275. package/python/tests/test_llm_admin.py +4 -19
  276. package/python/tests/test_llm_planner.py +1 -23
  277. package/python/tests/test_markdown_skills.py +3 -14
  278. package/python/tests/test_markdown_skills_openclaw.py +4 -9
  279. package/python/tests/test_mcp_connectors.py +3 -15
  280. package/python/tests/test_mcp_guardian_discovery.py +2 -6
  281. package/python/tests/test_mcp_no_memory.py +3 -3
  282. package/python/tests/test_mcp_policy_gate.py +0 -1
  283. package/python/tests/test_mcp_server.py +6 -9
  284. package/python/tests/test_memory_recall.py +1 -4
  285. package/python/tests/test_metrics.py +0 -8
  286. package/python/tests/test_multi_chain.py +1 -13
  287. package/python/tests/test_multimodal.py +5 -18
  288. package/python/tests/test_negotiation.py +3 -13
  289. package/python/tests/test_orchestration.py +1 -11
  290. package/python/tests/test_orchestration_decoupled.py +0 -6
  291. package/python/tests/test_p3_p5_hardening.py +3 -17
  292. package/python/tests/test_parallel_dispatch.py +4 -12
  293. package/python/tests/test_pipeline.py +17 -58
  294. package/python/tests/test_plan_visible.py +4 -4
  295. package/python/tests/test_planner.py +0 -7
  296. package/python/tests/test_policy_gate.py +2 -3
  297. package/python/tests/test_profile.py +67 -0
  298. package/python/tests/test_readme_claims.py +163 -0
  299. package/python/tests/test_reasoner.py +2 -17
  300. package/python/tests/test_release_check.py +2 -13
  301. package/python/tests/test_resume_checkpoint.py +7 -15
  302. package/python/tests/test_routing.py +2 -12
  303. package/python/tests/test_scheduler.py +7 -10
  304. package/python/tests/test_search.py +1 -11
  305. package/python/tests/test_secrets.py +4 -10
  306. package/python/tests/test_semantic.py +1 -10
  307. package/python/tests/test_sessions.py +3 -4
  308. package/python/tests/test_settings.py +2 -18
  309. package/python/tests/test_shadow_learning.py +3 -43
  310. package/python/tests/test_shared_write.py +0 -7
  311. package/python/tests/test_skill_taint_gate.py +1 -10
  312. package/python/tests/test_skills.py +2 -18
  313. package/python/tests/test_skills_fetch.py +282 -0
  314. package/python/tests/test_stats_and_events.py +2 -5
  315. package/python/tests/test_streaming.py +2 -7
  316. package/python/tests/test_supervisor_pipeline.py +4 -5
  317. package/python/tests/test_supervisor_triage.py +4 -10
  318. package/python/tests/test_taint.py +2 -7
  319. package/python/tests/test_task_stream.py +1 -12
  320. package/python/tests/test_thinking_sse.py +0 -1
  321. package/python/tests/test_tool_center.py +1 -17
  322. package/python/tests/test_tool_parallel.py +2 -2
  323. package/python/tests/test_tool_params.py +1 -5
  324. package/python/tests/test_tool_use.py +0 -13
  325. package/python/tests/test_toplevel_concurrency.py +3 -4
  326. package/python/tests/test_tracing.py +7 -41
  327. package/python/tests/test_truth_crystallizer.py +4 -6
  328. package/python/tests/test_truth_flow.py +2 -7
  329. package/python/tests/test_update_check.py +95 -0
  330. package/python/tests/test_verify_evidence.py +8 -21
  331. package/python/tests/test_version_sync.py +35 -6
  332. package/python/tests/test_web_trigger.py +0 -17
  333. package/python/tools/check_faiss_type.py +0 -1
  334. package/python/tools/check_log.ps1 +0 -8
  335. package/python/tools/mypy_baseline.txt +12 -4
  336. package/python/tools/mypy_ratchet.py +52 -6
  337. package/python/tools/pack.py +0 -5
  338. package/python/tools/release_check.py +7 -7
  339. package/python/tools/restart_foxclaw.ps1 +5 -8
  340. package/python/tools/shared_write_audit.py +1 -15
  341. package/python/tools/size_check.py +1 -1
  342. package/python/tools/strip_comments.py +228 -0
  343. package/python/frontend/v3/assets/index-BD7VPbMV.js +0 -33
  344. package/python/frontend/v3/assets/index-CAvnYWGH.css +0 -1
@@ -64,7 +64,6 @@ def _read_custom_model(data_dir, name):
64
64
  if not match:
65
65
  return None
66
66
  key = ""
67
- # OS 凭据库优先,回退 base64(旧记录)/ 更早的明文字段
68
67
  from ..core.secrets import load as _load_secret
69
68
  key = _load_secret(name, match.get("api_key_obf") or "")
70
69
  key = key or (match.get("api_key") or "")
@@ -101,7 +100,6 @@ def _selected_llm():
101
100
  return None
102
101
 
103
102
  source = (sel.get("source") or "").strip().lower()
104
- # preset 落盘的是 provider;custom 落盘的是 name。两者都当"选中的名字"用。
105
103
  name = (sel.get("name") or sel.get("provider") or "").strip()
106
104
  model = (sel.get("model_id") or sel.get("model") or "").strip()
107
105
 
@@ -113,7 +111,6 @@ def _selected_llm():
113
111
  return _to_hkc_provider("generic"), key, base, model or custom_model
114
112
 
115
113
  if source == "preset" and name:
116
- # 界面里配过的同名自定义项(更新的 key)优先,env 只作最后兜底
117
114
  got = _read_custom_model(data_dir, name)
118
115
  if got:
119
116
  key, base, custom_model = got
@@ -144,7 +141,6 @@ def _detect_llm(api_key=None):
144
141
 
145
142
  返回 ``(provider, key, base_url, model)``。
146
143
  """
147
- # 1. 显式 HKC_LLM_PROVIDER 覆盖(向后兼容「HKC 独立配」场景)
148
144
  env_provider = (os.environ.get("HKC_LLM_PROVIDER") or "").strip().lower()
149
145
  if env_provider in ("anthropic", "deepseek", "openai", "openai-compatible"):
150
146
  key = (os.environ.get("HKC_LLM_API_KEY")
@@ -158,15 +154,11 @@ def _detect_llm(api_key=None):
158
154
  or "")
159
155
  return env_provider, key or api_key, base, model
160
156
 
161
- # 2. 界面里当前选中的模型(用户显式动作,优先于 env 默认)
162
157
  selected = _selected_llm()
163
158
  if selected:
164
159
  provider, key, base, model = selected
165
160
  return provider, (key or api_key or "").strip(), (base or "").strip(), model
166
161
 
167
- # 3. 单入口 env。未显式指定 provider 时**复用主对话的自动探测**,
168
- # 不能默认成 "generic" —— 那是个空 key 的壳,会让抽取打 api.openai.com
169
- # 拿 Missing credentials,每次摄入都 0 条。
170
162
  provider = (os.environ.get("LLM_PROVIDER") or "").strip().lower()
171
163
  key = (os.environ.get("LLM_API_KEY") or api_key or "").strip()
172
164
  base = os.environ.get("LLM_BASE_URL", "").strip()
@@ -194,7 +186,7 @@ class HKCService:
194
186
 
195
187
  def __init__(self, container, client):
196
188
  self._container = container
197
- self.client = client # DirectClient;供 HSR 检索 / /api/knowledge 路由使用
189
+ self.client = client
198
190
 
199
191
  @property
200
192
  def available(self) -> bool:
@@ -245,22 +237,18 @@ def init_hkc(data_dir, api_key=None):
245
237
  return None
246
238
  try:
247
239
  provider, llm_key, llm_base_url, llm_model = _detect_llm(api_key)
248
- # 【P2·单 LLM 入口】embedding 模型名优先 EMBEDDING_MODEL(系统唯一本地入口),
249
- # 再 HKC_EMBEDDING_MODEL(向后兼容);其他 HKC 行为开关不变。
250
240
  _emb_model = (os.environ.get("EMBEDDING_MODEL")
251
241
  or os.environ.get("HKC_EMBEDDING_MODEL")) or None
252
242
  container = HKCContainer(
253
243
  data_dir=str(Path(data_dir) / "hkc"),
254
- embedding_kind=os.environ.get("HKC_EMBEDDING", "auto"), # 默认真语义(本地优先,无 ST 回退 stub)
244
+ embedding_kind=os.environ.get("HKC_EMBEDDING", "auto"),
255
245
  embedding_url=os.environ.get("HKC_EMBEDDING_URL", "http://localhost:8080"),
256
- embedding_model=_emb_model, # local/auto 模型名(如 bge-m3)
246
+ embedding_model=_emb_model,
257
247
  llm_api_key=llm_key or None,
258
248
  llm_provider=provider,
259
249
  llm_model=llm_model or None,
260
250
  llm_base_url=llm_base_url or None,
261
251
  ).assemble()
262
- # 注册为 hkc_api 全局单例:若将来挂 HKC 自身 /hkc REST 路由,
263
- # 其 Depends(get_container) 能取到这同一个容器,而非"未初始化"。
264
252
  from hkc_api import container as _cm
265
253
  _cm._container = container
266
254
  print(f"[Foxclaw] ✓ HKC 知识核心已进程内融合 (provider={provider}, "
@@ -275,8 +263,6 @@ def init_hkc(data_dir, api_key=None):
275
263
  from .truth_crystallizer import TruthCrystallizer
276
264
 
277
265
 
278
- # HKC 知识生命周期事件 → 桥接到 EventSpine 的类型。**刻意不含 memory.***(那是 HMR 域,
279
- # Foxclaw 已有自己的记忆事件,转发 HKC 的会重复/混淆)。
280
266
  _HKC_BRIDGED_EVENTS = (
281
267
  "knowledge.created", "knowledge.updated", "knowledge.merged", "knowledge.deleted",
282
268
  "conflict.detected", "conflict.resolved", "claim.status_changed",
@@ -351,7 +337,7 @@ def register_abilities_as_skills(registry, hkc) -> list:
351
337
  registered = []
352
338
  for key in keys:
353
339
  try:
354
- ab = client.compile_ability(key) # 覆盖不足抛异常 → 证据 gate 跳过、不注册
340
+ ab = client.compile_ability(key)
355
341
  except Exception:
356
342
  continue
357
343
  sid = f"ability:{key}"
@@ -363,8 +349,6 @@ def register_abilities_as_skills(registry, hkc) -> list:
363
349
  handler=_make_ability_handler(client, ab),
364
350
  params={"query": "str"}))
365
351
  registered.append(sid)
366
- # 【深审修复·陈旧清理】重新同步时,移除此前注册但**本轮不再达标**的 ability skill
367
- # (coverage 掉了/能力定义删了)——register-only 会残留陈旧项;prune 使 registry 反映当前覆盖。
368
352
  fresh = set(registered)
369
353
  for spec in registry.all():
370
354
  if spec.id.startswith("ability:") and spec.id not in fresh:
@@ -15,17 +15,17 @@ Step1(叠加、默认关、行为不变):
15
15
  from __future__ import annotations
16
16
  import threading
17
17
  from concurrent.futures import ThreadPoolExecutor
18
- from typing import Callable, Optional
18
+ from typing import Any, Callable, cast, Optional
19
19
 
20
20
 
21
21
  class TruthCrystallizer:
22
22
  def __init__(self, hkc_provider: Callable[[], object], enabled: bool = False):
23
- self._hkc_provider = hkc_provider # 惰性取 HKCService(或 None)
23
+ self._hkc_provider = hkc_provider
24
24
  self.enabled = enabled
25
25
  self._executor: Optional[ThreadPoolExecutor] = None
26
26
  self._closed = False
27
27
  self._lock = threading.Lock()
28
- self._pending: list = [] # 供测试 drain
28
+ self._pending: list = []
29
29
 
30
30
  def _ex(self) -> Optional[ThreadPoolExecutor]:
31
31
  with self._lock:
@@ -41,7 +41,7 @@ class TruthCrystallizer:
41
41
  if not self.enabled:
42
42
  return
43
43
  p = getattr(msg, "payload", None) or {}
44
- if p.get("blocked"): # 被拦截任务不结晶(与 TruthPipeline 一致)
44
+ if p.get("blocked"):
45
45
  return
46
46
  fact = ((p.get("truth") or {}).get("fact") or "").strip()
47
47
  if not fact:
@@ -52,16 +52,13 @@ class TruthCrystallizer:
52
52
  return
53
53
  fut = ex.submit(self._crystallize, fact, domain)
54
54
  with self._lock:
55
- # 【深审修复·长期运行泄漏】_pending 原先只增不减(drain 仅在 close/测试时调用),
56
- # 启用后每来一次 truth.appended 就永久累积一个已完成 Future → 无界内存泄漏。
57
- # 改为每次提交时 prune 掉已完成的,_pending 只保留在飞行中的任务(仍支持测试 drain)。
58
55
  self._pending = [f for f in self._pending if not f.done()]
59
56
  self._pending.append(fut)
60
57
 
61
58
  def _crystallize(self, fact: str, domain: str) -> bool:
62
59
  """后台执行:惰性取 HKC → ingest_text(KDE→KEE)。任何失败都吞掉,不影响主流程。"""
63
60
  try:
64
- hkc = self._hkc_provider()
61
+ hkc = cast(Any, self._hkc_provider())
65
62
  except Exception:
66
63
  hkc = None
67
64
  if hkc is None or not getattr(hkc, "available", False):
@@ -17,9 +17,9 @@ class KnowledgeGraph:
17
17
  DIM = 256
18
18
 
19
19
  def __init__(self, embedder=None):
20
- self.items: dict = {} # id -> KnowledgeItem
21
- self.edges: list = [] # KnowledgeEdge
22
- self._embedder = embedder # 可选:HMR 的向量能力
20
+ self.items: dict = {}
21
+ self.edges: list = []
22
+ self._embedder = embedder
23
23
 
24
24
  def add_item(self, item: KnowledgeItem):
25
25
  self.items[item.id] = item
@@ -30,7 +30,6 @@ class KnowledgeGraph:
30
30
  self.edges = [e for e in self.edges
31
31
  if e.source_id != item_id and e.target_id != item_id]
32
32
 
33
- # ── 三种连接 ──────────────────────────────────────────
34
33
  def link_manual(self, source_id: str, target_id: str, reason: str = ""):
35
34
  self.edges.append(KnowledgeEdge(
36
35
  source_id=source_id, target_id=target_id,
@@ -72,7 +71,6 @@ class KnowledgeGraph:
72
71
  new_edges += 1
73
72
  return new_edges
74
73
 
75
- # ── 图分析 ────────────────────────────────────────────
76
74
  def neighbors(self, item_id: str) -> list:
77
75
  """某知识点的所有关联点。"""
78
76
  out = []
@@ -105,7 +103,7 @@ class KnowledgeGraph:
105
103
  return result
106
104
 
107
105
  def stats(self) -> dict:
108
- type_count = defaultdict(int)
106
+ type_count: defaultdict[str, int] = defaultdict(int)
109
107
  for e in self.edges:
110
108
  type_count[e.edge_type] += 1
111
109
  return {
@@ -115,7 +113,6 @@ class KnowledgeGraph:
115
113
  "clusters": len(self.clusters()),
116
114
  }
117
115
 
118
- # ── 向量工具 ──────────────────────────────────────────
119
116
  def _embed(self, text: str):
120
117
  if self._embedder is not None:
121
118
  try:
@@ -138,8 +135,6 @@ class KnowledgeGraph:
138
135
 
139
136
  @staticmethod
140
137
  def _cosine(a, b) -> float:
141
- # 自己归一化,不假设输入已是单位向量:外部 embedder(如 HMR)返回的向量
142
- # 未必归一化,直接用点积当余弦会系统性偏高(threshold 失效,什么都连上)。
143
138
  import math
144
139
  dot = sum(x * y for x, y in zip(a, b))
145
140
  na = math.sqrt(sum(x * x for x in a))
@@ -13,17 +13,17 @@ import uuid
13
13
 
14
14
 
15
15
  EdgeType = Literal["same_topic", "concept_ref", "semantic", "manual"]
16
- Grade = Literal[0, 1, 2, 3, 4, 5] # SM-2 回忆质量评分 0=完全忘 5=完美
16
+ Grade = Literal[0, 1, 2, 3, 4, 5]
17
17
 
18
18
 
19
19
  @dataclass
20
20
  class KnowledgeItem:
21
21
  """一个知识条目 = 一次学习采集"""
22
- content: str # 知识点内容
23
- topic: str = "general" # 主题
24
- source: str = "" # 来源:书名/课程/文章/Readwise
22
+ content: str
23
+ topic: str = "general"
24
+ source: str = ""
25
25
  tags: list = field(default_factory=list)
26
- notes: list = field(default_factory=list) # 用户读时加的笔记 [{text, ts}]
26
+ notes: list = field(default_factory=list)
27
27
  created_at: str = field(default_factory=lambda: datetime.now().isoformat())
28
28
  id: str = field(default_factory=lambda: f"ki_{uuid.uuid4().hex[:10]}")
29
29
 
@@ -35,19 +35,19 @@ class KnowledgeItem:
35
35
  class ReviewState:
36
36
  """SM-2 间隔重复状态(固化层核心)"""
37
37
  item_id: str
38
- repetitions: int = 0 # 连续正确次数
39
- interval: int = 0 # 当前间隔(天)
40
- ease_factor: float = 2.5 # 难度因子(SM-2 起始 2.5)
38
+ repetitions: int = 0
39
+ interval: int = 0
40
+ ease_factor: float = 2.5
41
41
  due_date: str = field(default_factory=lambda: datetime.now().isoformat())
42
42
  last_review: Optional[str] = None
43
43
  total_reviews: int = 0
44
- lapses: int = 0 # 遗忘次数(评分<3)
44
+ lapses: int = 0
45
45
 
46
46
  @property
47
47
  def mastery(self) -> float:
48
48
  """掌握度 0~1:综合连续正确次数和难度因子。"""
49
- rep_score = min(self.repetitions / 6.0, 1.0) # 6 次连对算掌握
50
- ease_score = (self.ease_factor - 1.3) / (2.5 - 1.3) # 1.3~2.5 归一
49
+ rep_score = min(self.repetitions / 6.0, 1.0)
50
+ ease_score = (self.ease_factor - 1.3) / (2.5 - 1.3)
51
51
  ease_score = max(0.0, min(1.0, ease_score))
52
52
  return round(0.6 * rep_score + 0.4 * ease_score, 3)
53
53
 
@@ -73,8 +73,8 @@ class KnowledgeEdge:
73
73
  source_id: str
74
74
  target_id: str
75
75
  edge_type: EdgeType
76
- weight: float = 1.0 # 关联强度
77
- reason: str = "" # 为什么连接(可解释)
76
+ weight: float = 1.0
77
+ reason: str = ""
78
78
 
79
79
  def to_dict(self) -> dict:
80
80
  return asdict(self)
@@ -11,7 +11,6 @@ from __future__ import annotations
11
11
  from ..cognitive.reasoner import DomainReasoner, ReasonedVerdict, Evidence
12
12
  from .spaced_repetition import SpacedRepetition
13
13
 
14
- # 负荷偏重阈值:未来窗口日均复习量 ≥ 该值即算偏重,交给可选 LLM 复核是否提醒。
15
14
  HEAVY_DAILY = 20.0
16
15
 
17
16
 
@@ -37,13 +36,12 @@ class LearningReviewReasoner(DomainReasoner):
37
36
  return h or ["学习状态可能健康"]
38
37
 
39
38
  def gather_evidence(self, states, obs: dict) -> list:
40
- risky = SpacedRepetition.forgetting_risk(states, self.horizon_days) # 规则=裁决证据
39
+ risky = SpacedRepetition.forgetting_risk(states, self.horizon_days)
41
40
  ev = [Evidence(factor="forgetting_risk",
42
41
  detail=r["reason"] + f"(item {r['item_id']})", weight=1.0, raw=r)
43
42
  for r in risky]
44
43
  if not risky:
45
44
  ev.append(Evidence("no_urgent_risk", "无即将到期且掌握度低的知识点", 0.5))
46
- # informational(raw=None):学习负荷,不参与裁决
47
45
  ev.append(Evidence("learning_load",
48
46
  f"未来窗口共 {obs['total_due']} 项待复习,日均 {obs['daily_avg']}", 0.3))
49
47
  return ev
@@ -51,7 +49,6 @@ class LearningReviewReasoner(DomainReasoner):
51
49
  def arbitrate(self, states, obs, hyps, ev) -> ReasonedVerdict:
52
50
  triggered = [e for e in ev if e.raw is not None]
53
51
  if triggered:
54
- # 【规则权威】存在高遗忘风险 → confirm(建议立即复习),绝不被 LLM 推翻/隐藏
55
52
  top = triggered[:3]
56
53
  return ReasonedVerdict(
57
54
  "confirm", f"发现 {len(triggered)} 个高遗忘风险知识点,建议优先复习:"
@@ -20,21 +20,20 @@ class LearningRuntime:
20
20
  """office Agent 的学习内核。"""
21
21
 
22
22
  def __init__(self, hmr=None, hkc_provider=None, persist_path=None):
23
- self.items: dict = {} # id -> KnowledgeItem(内存缓存;知识真源=HKC)
24
- self.review_states: dict = {} # item_id -> ReviewState
23
+ self.items: dict = {}
24
+ self.review_states: dict = {}
25
25
  self.sr = SpacedRepetition()
26
26
  self.hmr = hmr
27
- self._hkc_provider = hkc_provider # 惰性拿 HKC(知识真源);None=纯内存(测试用)
28
- self._lock = threading.RLock() # 跨线程写/改内存加锁(与全系统线程安全铁律一致)
27
+ self._hkc_provider = hkc_provider
28
+ self._lock = threading.RLock()
29
29
  embedder = None
30
30
  if hmr is not None and hasattr(hmr, "_hmr"):
31
31
  embedder = self._make_embedder(hmr)
32
32
  self.graph = KnowledgeGraph(embedder=embedder)
33
- # persist_path 现在只作「离线兜底队列 + 旧 learning.json 迁移源」;知识真源是 HKC。
34
33
  self._persist_path = Path(persist_path) if persist_path else None
35
- self._hkc_loaded = False # HKC 首次成功载入的门闩(惰性,见 _ensure_hkc_loaded)
36
- self._load_file() # 先把离线队列/旧文件载入内存(__init__ 时 HKC 常未就绪)
37
- self._ensure_hkc_loaded() # HKC 若已就绪则立即从真源载入;否则留待后续调用重试
34
+ self._hkc_loaded = False
35
+ self._load_file()
36
+ self._ensure_hkc_loaded()
38
37
 
39
38
  def _make_embedder(self, hmr):
40
39
  """尝试复用 HMR 的 embedding;失败则返回 None(图谱自动降级哈希)。"""
@@ -46,7 +45,6 @@ class LearningRuntime:
46
45
  pass
47
46
  return None
48
47
 
49
- # ── 存储:知识内容+笔记+复习状态 全落 HKC(真源);HKC 拿不到时落离线队列(兜底 A)──
50
48
  def _graph_store(self):
51
49
  """惰性拿 HKC 的 graph_store。拿不到(HKC 未装配/未配/异常)返回 None。"""
52
50
  if not self._hkc_provider:
@@ -126,9 +124,9 @@ class LearningRuntime:
126
124
  if self._hkc_loaded:
127
125
  return
128
126
  gs = self._graph_store()
129
- if gs is None: # HKC 还没就绪 → 下次调用再试
127
+ if gs is None:
130
128
  return
131
- try: # 1) 从 HKC 载入(内存已有的以文件版为准,不覆盖)
129
+ try:
132
130
  for ku in gs.query_all(limit=5000):
133
131
  ex = getattr(ku, "extra", {}) or {}
134
132
  if ex.get("kind") != "learning_note":
@@ -146,9 +144,9 @@ class LearningRuntime:
146
144
  except Exception:
147
145
  pass
148
146
  except Exception:
149
- return # 载入失败不置门闩,留待下次重试
147
+ return
150
148
  self._hkc_loaded = True
151
- if self._persist_path and self._persist_path.exists(): # 2) 文件残留回迁 HKC + 删文件
149
+ if self._persist_path and self._persist_path.exists():
152
150
  self._save()
153
151
 
154
152
  def _save(self):
@@ -166,11 +164,11 @@ class LearningRuntime:
166
164
  if ok:
167
165
  if self._persist_path and self._persist_path.exists():
168
166
  try:
169
- self._persist_path.unlink() # 已全部进 HKC → 删掉本地队列文件
167
+ self._persist_path.unlink()
170
168
  except Exception:
171
169
  pass
172
170
  return
173
- if self._persist_path: # 离线兜底:HKC 不可用/部分失败 → 落队列文件
171
+ if self._persist_path:
174
172
  try:
175
173
  data = {
176
174
  "items": [asdict(it) for it in items],
@@ -183,9 +181,8 @@ class LearningRuntime:
183
181
  except Exception:
184
182
  pass
185
183
 
186
- # ── 采集 + 沉淀 ───────────────────────────────────────
187
184
  def learn(self, content: str, topic: str = "general",
188
- source: str = "", tags: list = None) -> dict:
185
+ source: str = "", tags: list | None = None) -> dict:
189
186
  """采集一个知识点 → 存进 HKC(知识真源)+ 间隔重复队列 + 图谱。
190
187
  铁律:知识内容+笔记归 HKC,不进 HMR(HMR 只装 token 上下文/工作运行记忆)。"""
191
188
  self._ensure_hkc_loaded()
@@ -195,7 +192,7 @@ class LearningRuntime:
195
192
  self.items[item.id] = item
196
193
  self.review_states[item.id] = ReviewState(item_id=item.id)
197
194
  self.graph.add_item(item)
198
- self._save() # 存进 HKC(HKC 不可用则离线队列兜底,恢复后自动补进)
195
+ self._save()
199
196
  return {"item": item.to_dict(),
200
197
  "review": self.review_states[item.id].to_dict()}
201
198
 
@@ -212,7 +209,6 @@ class LearningRuntime:
212
209
  count += 1
213
210
  return count
214
211
 
215
- # ── 固化:复习 ────────────────────────────────────────
216
212
  def review(self, item_id: str, grade: int) -> dict:
217
213
  """复习一个知识点,提交回忆质量评分(0~5),更新掌握度。"""
218
214
  self._ensure_hkc_loaded()
@@ -237,7 +233,7 @@ class LearningRuntime:
237
233
  gs = self._graph_store()
238
234
  if gs is not None:
239
235
  try:
240
- gs.delete(item_id) # HKC 软删除(status='deleted')
236
+ gs.delete(item_id)
241
237
  except Exception:
242
238
  pass
243
239
  if found:
@@ -256,7 +252,7 @@ class LearningRuntime:
256
252
  return {"error": "未知知识点"}
257
253
  item.notes.append({"text": text, "ts": datetime.now().isoformat()})
258
254
  notes = list(item.notes)
259
- self._save() # 只有新增了笔记才落盘
255
+ self._save()
260
256
  return {"notes": notes}
261
257
 
262
258
  def due_today(self) -> list:
@@ -265,7 +261,6 @@ class LearningRuntime:
265
261
  return [{"item": self.items[s.item_id].to_dict(),
266
262
  "review": s.to_dict()} for s in due]
267
263
 
268
- # ── 连接:构建图谱 ────────────────────────────────────
269
264
  def build_connections(self, semantic_threshold: float = 0.55) -> dict:
270
265
  """自动构建知识连接(同主题 + 语义)。"""
271
266
  topic_edges = self.graph.auto_link_by_topic()
@@ -283,13 +278,11 @@ class LearningRuntime:
283
278
  "edge_type": etype, "weight": weight, "reason": reason})
284
279
  return out
285
280
 
286
- # ── 学习策略(对应财务守门人)─────────────────────────
287
281
  def strategy_check(self, daily_cap: int = 20) -> list:
288
282
  """学习策略裁决:复习提醒、薄弱主题、负荷预警、连接建议。"""
289
283
  verdicts = []
290
284
  states = list(self.review_states.values())
291
285
 
292
- # 到期复习
293
286
  due = self.sr.due_items(states)
294
287
  if due:
295
288
  verdicts.append(LearningVerdict(
@@ -297,7 +290,6 @@ class LearningRuntime:
297
290
  message=f"今天有 {len(due)} 个知识点待复习",
298
291
  payload={"count": len(due)}))
299
292
 
300
- # 遗忘风险(薄弱项)
301
293
  risky = self.sr.forgetting_risk(states)
302
294
  if risky:
303
295
  verdicts.append(LearningVerdict(
@@ -305,7 +297,6 @@ class LearningRuntime:
305
297
  message=f"{len(risky)} 个知识点处于高遗忘风险,建议优先复习",
306
298
  payload={"risky": risky[:5]}))
307
299
 
308
- # 负荷预警
309
300
  load = self.sr.learning_load(states)
310
301
  if load["total_due"] > daily_cap:
311
302
  verdicts.append(LearningVerdict(
@@ -313,7 +304,6 @@ class LearningRuntime:
313
304
  message=f"未来复习量 {load['total_due']} 超过每日上限 {daily_cap},建议分批",
314
305
  payload=load))
315
306
 
316
- # 孤立知识点(无连接)→ 建议建立关联
317
307
  connected = set()
318
308
  for e in self.graph.edges:
319
309
  connected.add(e.source_id)
@@ -326,14 +316,12 @@ class LearningRuntime:
326
316
  payload={"isolated_count": len(isolated)}))
327
317
  return verdicts
328
318
 
329
- # ── 综合分析(office Agent 核心输出)──────────────────
330
319
  def analyze(self) -> dict:
331
- self._ensure_hkc_loaded() # HKC 就绪后从真源载回知识(迁移删文件后重启也不丢)
320
+ self._ensure_hkc_loaded()
332
321
  states = list(self.review_states.values())
333
322
  masteries = [s.mastery for s in states]
334
323
  avg_mastery = round(sum(masteries) / len(masteries), 3) if masteries else 0.0
335
324
 
336
- # 按主题统计掌握度
337
325
  from collections import defaultdict
338
326
  topic_mastery = defaultdict(list)
339
327
  for iid, item in self.items.items():
@@ -346,7 +334,7 @@ class LearningRuntime:
346
334
  "total_items": len(self.items),
347
335
  "avg_mastery": avg_mastery,
348
336
  "topic_mastery": topics,
349
- "weakest_topic": (min(topics, key=topics.get) if topics else None),
337
+ "weakest_topic": (min(topics, key=lambda t: topics[t]) if topics else None),
350
338
  "due_today": len(self.sr.due_items(states)),
351
339
  "forgetting_risk": self.sr.forgetting_risk(states),
352
340
  "learning_load": self.sr.learning_load(states),
@@ -22,19 +22,16 @@ class SpacedRepetition:
22
22
  @staticmethod
23
23
  def review(state: ReviewState, grade: int) -> ReviewState:
24
24
  """根据本次回忆质量,更新间隔重复状态。返回更新后的 state。"""
25
- # grade 钳制到 SM-2 定义域 [0,5],防止越界输入让 EF 公式算出无意义值
26
25
  grade = max(0, min(5, int(grade)))
27
26
  now = datetime.now()
28
27
  state.total_reviews += 1
29
28
  state.last_review = now.isoformat()
30
29
 
31
30
  if grade < 3:
32
- # 没记住:重置,短期内再复习
33
31
  state.repetitions = 0
34
32
  state.interval = 1
35
33
  state.lapses += 1
36
34
  else:
37
- # 记住了:按 SM-2 推进间隔
38
35
  if state.repetitions == 0:
39
36
  state.interval = 1
40
37
  elif state.repetitions == 1:
@@ -43,11 +40,9 @@ class SpacedRepetition:
43
40
  state.interval = round(state.interval * state.ease_factor)
44
41
  state.repetitions += 1
45
42
 
46
- # 更新难度因子(SM-2 公式)
47
43
  ef = state.ease_factor + (0.1 - (5 - grade) * (0.08 + (5 - grade) * 0.02))
48
44
  state.ease_factor = max(1.3, round(ef, 3))
49
45
 
50
- # 下次复习时间
51
46
  state.due_date = (now + timedelta(days=state.interval)).isoformat()
52
47
  return state
53
48
 
@@ -79,13 +74,13 @@ class SpacedRepetition:
79
74
  def learning_load(states: list, days: int = 7) -> dict:
80
75
  """学习负荷(对应财务烧钱速度):未来 N 天的复习量。"""
81
76
  from collections import defaultdict
82
- by_day = defaultdict(int)
77
+ by_day: defaultdict[int, int] = defaultdict(int)
83
78
  for s in states:
84
79
  d = s.days_until_due
85
80
  if 0 <= d <= days:
86
81
  by_day[d] += 1
87
82
  elif d < 0:
88
- by_day[0] += 1 # 逾期的算今天
83
+ by_day[0] += 1
89
84
  total_due = sum(by_day.values())
90
85
  return {
91
86
  "window_days": days,
@@ -90,7 +90,6 @@ def build_foxclaw_mcp(os_getter, expose_agents: bool = False) -> FastMCP:
90
90
  """os_getter:无参回调,返回当前 Foxclaw 实例(惰性,启动后才有)。
91
91
  expose_agents:另开一道开关才暴露 Agent 调度(有副作用面,默认关)。"""
92
92
  mcp = FastMCP("Foxclaw Knowledge", stateless_http=True, streamable_http_path="/")
93
- # 限流(env 可配):读工具较宽松、ask_foxclaw(驱动 LLM,贵)较紧
94
93
  read_limiter = _RateLimiter(_env_int("FOXCLAW_MCP_RATE_READ", 120))
95
94
  agent_limiter = _RateLimiter(_env_int("FOXCLAW_MCP_RATE_AGENT", 20))
96
95
 
@@ -111,7 +110,7 @@ def build_foxclaw_mcp(os_getter, expose_agents: bool = False) -> FastMCP:
111
110
  "type": str(getattr(h, "ku_type", ""))}
112
111
  for h in (hits or [])]
113
112
  except Exception:
114
- return [{"error": "internal error"}] # 对外脱敏:不回传 str(e)
113
+ return [{"error": "internal error"}]
115
114
 
116
115
  @mcp.tool()
117
116
  def knowledge_get(ku_id: str) -> dict:
@@ -172,7 +171,7 @@ def build_foxclaw_mcp(os_getter, expose_agents: bool = False) -> FastMCP:
172
171
 
173
172
  def _work():
174
173
  from foxclaw.agents.base import _suppress_memory
175
- _suppress_memory.set(True) # 外部驱动的任务不写 HMR(防污染),随上下文传到 run_task
174
+ _suppress_memory.set(True)
176
175
  return os_.scheduler.run(
177
176
  lambda: _a.run(os_.run(task, agent)),
178
177
  agent_id=agent, task=task)