foxclaw 0.1.0 → 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 (514) hide show
  1. package/README.md +54 -29
  2. package/bin/foxclaw.cjs +51 -0
  3. package/index.js +4 -10
  4. package/package.json +46 -38
  5. package/python/.dockerignore +35 -0
  6. package/python/.env.example +173 -0
  7. package/python/.gitattributes +46 -0
  8. package/python/.github/workflows/ci.yml +167 -0
  9. package/python/.github/workflows/release.yml +183 -0
  10. package/python/CHANGELOG.md +57 -0
  11. package/python/CHANGELOG_CN.md +57 -0
  12. package/python/Dockerfile +64 -0
  13. package/python/LICENSE +21 -0
  14. package/python/MANIFEST.in +14 -0
  15. package/python/README.md +201 -0
  16. package/python/README_CN.md +336 -0
  17. package/python/bench/bm25_audit.py +186 -0
  18. package/python/bench/bm25_regression.py +177 -0
  19. package/python/bench/bm25_stress.py +155 -0
  20. package/python/bench/cd_audit.py +215 -0
  21. package/python/bench/chaos.py +140 -0
  22. package/python/bench/concurrency.py +103 -0
  23. package/python/bench/e2e_real_models.py +94 -0
  24. package/python/bench/llm_retry_test.py +241 -0
  25. package/python/bench/load_test.py +193 -0
  26. package/python/bench/vector_ivf_upgrade.py +128 -0
  27. package/python/docs/CHANNELS.md +195 -0
  28. package/python/docs/GOVERNANCE.md +164 -0
  29. package/python/docs/RELEASE.md +110 -0
  30. package/python/docs/RUN.md +75 -0
  31. package/python/docs/RUN_CN.md +121 -0
  32. package/python/docs//351/205/215/347/275/256/346/214/207/345/215/227.md +152 -0
  33. package/python/evals/__init__.py +0 -0
  34. package/python/evals/governance/README.md +61 -0
  35. package/python/evals/governance/__init__.py +0 -0
  36. package/python/evals/governance/cases.py +115 -0
  37. package/python/evals/governance/compare_embeddings.py +94 -0
  38. package/python/evals/governance/convergence.svg +11 -0
  39. package/python/evals/governance/report.json +371 -0
  40. package/python/evals/governance/run_evals.py +368 -0
  41. package/python/examples/governance_middleware_demo.py +72 -0
  42. package/python/examples/langgraph_governance_demo.py +104 -0
  43. package/python/examples/sample_itinerary.csv +6 -0
  44. package/python/examples/sample_knowledge.csv +9 -0
  45. package/python/examples/sample_transactions.csv +14 -0
  46. package/python/foxclaw/__init__.py +408 -0
  47. package/python/foxclaw/agents/__init__.py +12 -0
  48. package/python/foxclaw/agents/agents.py +35 -0
  49. package/python/foxclaw/agents/base.py +794 -0
  50. package/python/foxclaw/agents/impl/__init__.py +1 -0
  51. package/python/foxclaw/agents/impl/_helpers.py +70 -0
  52. package/python/foxclaw/agents/impl/finance.py +231 -0
  53. package/python/foxclaw/agents/impl/office.py +350 -0
  54. package/python/foxclaw/agents/impl/supervisor.py +592 -0
  55. package/python/foxclaw/agents/mixins/_context.py +128 -0
  56. package/python/foxclaw/agents/mixins/augment.py +143 -0
  57. package/python/foxclaw/agents/mixins/llm.py +67 -0
  58. package/python/foxclaw/agents/mixins/memory.py +41 -0
  59. package/python/foxclaw/agents/mixins/preference.py +131 -0
  60. package/python/foxclaw/agents/mixins/prompt.py +71 -0
  61. package/python/foxclaw/agents/mixins/skillbox.py +58 -0
  62. package/python/foxclaw/agents/mixins/tool_exec.py +417 -0
  63. package/python/foxclaw/api/__init__.py +224 -0
  64. package/python/foxclaw/api/_state.py +43 -0
  65. package/python/foxclaw/api/bootstrap.py +96 -0
  66. package/python/foxclaw/api/middleware.py +294 -0
  67. package/python/foxclaw/api/routes/ahs.py +404 -0
  68. package/python/foxclaw/api/routes/attachments.py +116 -0
  69. package/python/foxclaw/api/routes/browser.py +35 -0
  70. package/python/foxclaw/api/routes/domain.py +306 -0
  71. package/python/foxclaw/api/routes/knowledge.py +145 -0
  72. package/python/foxclaw/api/routes/llm_admin.py +449 -0
  73. package/python/foxclaw/api/routes/memory.py +105 -0
  74. package/python/foxclaw/api/routes/orchestration.py +39 -0
  75. package/python/foxclaw/api/routes/recovery.py +44 -0
  76. package/python/foxclaw/api/routes/review.py +266 -0
  77. package/python/foxclaw/api/routes/search.py +33 -0
  78. package/python/foxclaw/api/routes/system.py +311 -0
  79. package/python/foxclaw/api/routes/task.py +263 -0
  80. package/python/foxclaw/api/routes/ws.py +64 -0
  81. package/python/foxclaw/api/spine.py +52 -0
  82. package/python/foxclaw/browser/__init__.py +13 -0
  83. package/python/foxclaw/browser/action/__init__.py +1 -0
  84. package/python/foxclaw/browser/action/action_executor.py +50 -0
  85. package/python/foxclaw/browser/action/action_types.py +44 -0
  86. package/python/foxclaw/browser/action/action_verifier.py +68 -0
  87. package/python/foxclaw/browser/action/url_policy.py +143 -0
  88. package/python/foxclaw/browser/adapter/__init__.py +1 -0
  89. package/python/foxclaw/browser/adapter/base_adapter.py +23 -0
  90. package/python/foxclaw/browser/adapter/web_adapter.py +46 -0
  91. package/python/foxclaw/browser/driver/__init__.py +1 -0
  92. package/python/foxclaw/browser/driver/base_driver.py +61 -0
  93. package/python/foxclaw/browser/driver/playwright_driver.py +170 -0
  94. package/python/foxclaw/browser/perception/__init__.py +1 -0
  95. package/python/foxclaw/browser/perception/dom_extractor.py +42 -0
  96. package/python/foxclaw/browser/perception/element_indexer.py +56 -0
  97. package/python/foxclaw/browser/perception/semantic_reducer.py +33 -0
  98. package/python/foxclaw/browser/perception/snapshot.py +29 -0
  99. package/python/foxclaw/browser/service.py +144 -0
  100. package/python/foxclaw/browser/session/__init__.py +1 -0
  101. package/python/foxclaw/browser/session/session_manager.py +57 -0
  102. package/python/foxclaw/capability/__init__.py +12 -0
  103. package/python/foxclaw/capability/embedding_service.py +65 -0
  104. package/python/foxclaw/channels/__init__.py +138 -0
  105. package/python/foxclaw/channels/discord.py +175 -0
  106. package/python/foxclaw/channels/dispatcher.py +179 -0
  107. package/python/foxclaw/channels/feishu.py +215 -0
  108. package/python/foxclaw/channels/openclaw_bridge.py +151 -0
  109. package/python/foxclaw/channels/signing.py +187 -0
  110. package/python/foxclaw/channels/telegram.py +120 -0
  111. package/python/foxclaw/channels/weixin.py +215 -0
  112. package/python/foxclaw/cli.py +183 -0
  113. package/python/foxclaw/cognitive/__init__.py +4 -0
  114. package/python/foxclaw/cognitive/reasoner.py +81 -0
  115. package/python/foxclaw/cognitive/state.py +141 -0
  116. package/python/foxclaw/core/__init__.py +9 -0
  117. package/python/foxclaw/core/audit_chain.py +131 -0
  118. package/python/foxclaw/core/audit_sink.py +100 -0
  119. package/python/foxclaw/core/compaction.py +265 -0
  120. package/python/foxclaw/core/config.py +65 -0
  121. package/python/foxclaw/core/confirm_nonce.py +151 -0
  122. package/python/foxclaw/core/dispatch.py +27 -0
  123. package/python/foxclaw/core/envfile.py +48 -0
  124. package/python/foxclaw/core/failure_policy.py +142 -0
  125. package/python/foxclaw/core/gateway.py +81 -0
  126. package/python/foxclaw/core/governance_audit.py +171 -0
  127. package/python/foxclaw/core/idempotency.py +282 -0
  128. package/python/foxclaw/core/injection_guard.py +202 -0
  129. package/python/foxclaw/core/memory_adapter.py +489 -0
  130. package/python/foxclaw/core/metrics.py +88 -0
  131. package/python/foxclaw/core/model_router.py +985 -0
  132. package/python/foxclaw/core/paths.py +103 -0
  133. package/python/foxclaw/core/recovery.py +349 -0
  134. package/python/foxclaw/core/scheduler.py +141 -0
  135. package/python/foxclaw/core/secrets.py +131 -0
  136. package/python/foxclaw/core/sessions.py +155 -0
  137. package/python/foxclaw/core/settings.py +414 -0
  138. package/python/foxclaw/core/shared_write.py +186 -0
  139. package/python/foxclaw/core/steering_queue.py +154 -0
  140. package/python/foxclaw/core/system_bus.py +84 -0
  141. package/python/foxclaw/core/thought_chain.py +316 -0
  142. package/python/foxclaw/core/token_meter.py +231 -0
  143. package/python/foxclaw/core/tools.py +164 -0
  144. package/python/foxclaw/core/tracing.py +375 -0
  145. package/python/foxclaw/core/truth_pipeline.py +110 -0
  146. package/python/foxclaw/core/update_check.py +149 -0
  147. package/python/foxclaw/core/vec_recall_adapter.py +477 -0
  148. package/python/foxclaw/core/verify.py +173 -0
  149. package/python/foxclaw/core/web_search.py +218 -0
  150. package/python/foxclaw/finance/__init__.py +15 -0
  151. package/python/foxclaw/finance/evidence.py +149 -0
  152. package/python/foxclaw/finance/fx.py +98 -0
  153. package/python/foxclaw/finance/graph.py +179 -0
  154. package/python/foxclaw/finance/models.py +146 -0
  155. package/python/foxclaw/finance/nlparse.py +515 -0
  156. package/python/foxclaw/finance/policy.py +301 -0
  157. package/python/foxclaw/finance/reasoner.py +121 -0
  158. package/python/foxclaw/finance/reasoning.py +319 -0
  159. package/python/foxclaw/finance/reconcile.py +175 -0
  160. package/python/foxclaw/finance/runtime.py +500 -0
  161. package/python/foxclaw/governance/__init__.py +24 -0
  162. package/python/foxclaw/governance/middleware.py +176 -0
  163. package/python/foxclaw/harness/__init__.py +17 -0
  164. package/python/foxclaw/harness/_base.py +66 -0
  165. package/python/foxclaw/harness/_policy.py +340 -0
  166. package/python/foxclaw/harness/drift.py +77 -0
  167. package/python/foxclaw/harness/event_bus.py +28 -0
  168. package/python/foxclaw/harness/harness.py +358 -0
  169. package/python/foxclaw/harness/loop_detection.py +145 -0
  170. package/python/foxclaw/itinerary/__init__.py +10 -0
  171. package/python/foxclaw/itinerary/amap_client.py +114 -0
  172. package/python/foxclaw/itinerary/models.py +80 -0
  173. package/python/foxclaw/itinerary/reasoner.py +96 -0
  174. package/python/foxclaw/itinerary/reasoning.py +168 -0
  175. package/python/foxclaw/itinerary/runtime.py +144 -0
  176. package/python/foxclaw/kernel/__init__.py +13 -0
  177. package/python/foxclaw/kernel/capabilities.py +72 -0
  178. package/python/foxclaw/kernel/policy_gate.py +108 -0
  179. package/python/foxclaw/kernel/process_manager.py +186 -0
  180. package/python/foxclaw/kernel/scheduler.py +125 -0
  181. package/python/foxclaw/kernel/sentinel.py +443 -0
  182. package/python/foxclaw/kernel/sentinel_client.py +243 -0
  183. package/python/foxclaw/kernel/spine.py +97 -0
  184. package/python/foxclaw/knowledge/__init__.py +360 -0
  185. package/python/foxclaw/knowledge/truth_crystallizer.py +89 -0
  186. package/python/foxclaw/learning/__init__.py +12 -0
  187. package/python/foxclaw/learning/knowledge_graph.py +144 -0
  188. package/python/foxclaw/learning/models.py +93 -0
  189. package/python/foxclaw/learning/reasoner.py +67 -0
  190. package/python/foxclaw/learning/runtime.py +351 -0
  191. package/python/foxclaw/learning/spaced_repetition.py +91 -0
  192. package/python/foxclaw/mcp_server.py +188 -0
  193. package/python/foxclaw/orchestration/__init__.py +167 -0
  194. package/python/foxclaw/pipeline/__init__.py +23 -0
  195. package/python/foxclaw/pipeline/cognitive_pipeline.py +429 -0
  196. package/python/foxclaw/pipeline/stage_meta.py +21 -0
  197. package/python/foxclaw/pipeline/supervisor_stages.py +201 -0
  198. package/python/foxclaw/planning/__init__.py +17 -0
  199. package/python/foxclaw/planning/llm_planner.py +232 -0
  200. package/python/foxclaw/planning/planner.py +94 -0
  201. package/python/foxclaw/search/__init__.py +24 -0
  202. package/python/foxclaw/search/agent_port.py +190 -0
  203. package/python/foxclaw/search/evidence.py +109 -0
  204. package/python/foxclaw/search/external_search.py +50 -0
  205. package/python/foxclaw/search/hkc_read_port.py +60 -0
  206. package/python/foxclaw/search/internal_search.py +71 -0
  207. package/python/foxclaw/search/models.py +127 -0
  208. package/python/foxclaw/search/planner.py +44 -0
  209. package/python/foxclaw/search/retriever.py +39 -0
  210. package/python/foxclaw/search/runtime.py +127 -0
  211. package/python/foxclaw/search/strategy.py +44 -0
  212. package/python/foxclaw/skills/__init__.py +559 -0
  213. package/python/foxclaw/skills/archive.py +111 -0
  214. package/python/foxclaw/skills/fetch.py +226 -0
  215. package/python/foxclaw/skills/hooks_spec.py +69 -0
  216. package/python/foxclaw/skills/install_cmd.py +216 -0
  217. package/python/foxclaw/skills/lifecycle.py +252 -0
  218. package/python/foxclaw/skills/markdown_skill.py +175 -0
  219. package/python/foxclaw/skills/markdown_store.py +39 -0
  220. package/python/foxclaw/skills/mcp_client.py +184 -0
  221. package/python/foxclaw/skills/mcp_store.py +40 -0
  222. package/python/foxclaw/skills/permission.py +145 -0
  223. package/python/foxclaw/skills/pipeline.py +354 -0
  224. package/python/foxclaw/skills/policy.py +74 -0
  225. package/python/foxclaw/skills/review.py +72 -0
  226. package/python/foxclaw/skills/store.py +57 -0
  227. package/python/frontend/assets/core_bg.jpg +0 -0
  228. package/python/frontend/deck/auth.js +25 -0
  229. package/python/frontend/deck/chat_model_picker.js +483 -0
  230. package/python/frontend/deck/domain.js +276 -0
  231. package/python/frontend/deck/main.js +1806 -0
  232. package/python/frontend/deck/markdown.js +44 -0
  233. package/python/frontend/deck/settings.js +307 -0
  234. package/python/frontend/deck/state.js +5 -0
  235. package/python/frontend/deck/util.js +4 -0
  236. package/python/frontend/deck/visuals.js +190 -0
  237. package/python/frontend/deck.css +855 -0
  238. package/python/frontend/deck.html +686 -0
  239. package/python/frontend/deck_v2.html +341 -0
  240. package/python/frontend/planet/index.html +266 -0
  241. package/python/frontend/planet/planet.css +579 -0
  242. package/python/frontend/planet/planet.js +1436 -0
  243. package/python/frontend/planet/vendor/three.core.min.js +6 -0
  244. package/python/frontend/planet/vendor/three.module.min.js +6 -0
  245. package/python/frontend/v3/assets/index-CQSBdxk4.js +34 -0
  246. package/python/frontend/v3/assets/index-D5nIsjms.css +1 -0
  247. package/python/frontend/v3/index.html +15 -0
  248. package/python/frontend/widget_runtime.js +252 -0
  249. package/python/frontend/widgets.js +639 -0
  250. package/python/frontend_src/index.html +14 -0
  251. package/python/frontend_src/package-lock.json +1376 -0
  252. package/python/frontend_src/package.json +19 -0
  253. package/python/frontend_src/src/App.vue +58 -0
  254. package/python/frontend_src/src/api.js +82 -0
  255. package/python/frontend_src/src/components/ChatDock.vue +365 -0
  256. package/python/frontend_src/src/components/ComposerMenu.vue +75 -0
  257. package/python/frontend_src/src/components/DocViewer.vue +162 -0
  258. package/python/frontend_src/src/components/Inspector.vue +235 -0
  259. package/python/frontend_src/src/components/LeftRail.vue +415 -0
  260. package/python/frontend_src/src/components/MenuPopover.vue +128 -0
  261. package/python/frontend_src/src/components/MessageStream.vue +389 -0
  262. package/python/frontend_src/src/components/ModelPicker.vue +350 -0
  263. package/python/frontend_src/src/components/QuickIngest.vue +112 -0
  264. package/python/frontend_src/src/components/Stage.vue +272 -0
  265. package/python/frontend_src/src/components/ToolMenu.vue +141 -0
  266. package/python/frontend_src/src/components/TopBar.vue +118 -0
  267. package/python/frontend_src/src/components/panels/ActivityPanel.vue +82 -0
  268. package/python/frontend_src/src/components/panels/AgentsPanel.vue +83 -0
  269. package/python/frontend_src/src/components/panels/KnowledgePanel.vue +425 -0
  270. package/python/frontend_src/src/components/panels/MemoryPanel.vue +117 -0
  271. package/python/frontend_src/src/components/panels/PanelBox.vue +50 -0
  272. package/python/frontend_src/src/components/panels/ReviewPanel.vue +292 -0
  273. package/python/frontend_src/src/components/panels/SettingsPanel.vue +358 -0
  274. package/python/frontend_src/src/components/panels/TasksPanel.vue +73 -0
  275. package/python/frontend_src/src/components/panels/ToolsPanel.vue +746 -0
  276. package/python/frontend_src/src/components/panels/UsagePanel.vue +105 -0
  277. package/python/frontend_src/src/lib/fmt.js +44 -0
  278. package/python/frontend_src/src/lib/i18n.js +53 -0
  279. package/python/frontend_src/src/lib/locales/en.js +173 -0
  280. package/python/frontend_src/src/lib/locales/zh.js +174 -0
  281. package/python/frontend_src/src/lib/md.js +190 -0
  282. package/python/frontend_src/src/main.js +9 -0
  283. package/python/frontend_src/src/stores/app.js +91 -0
  284. package/python/frontend_src/src/stores/chat.js +450 -0
  285. package/python/frontend_src/src/stores/model.js +96 -0
  286. package/python/frontend_src/src/stores/panels.js +289 -0
  287. package/python/frontend_src/src/stores/profile.js +31 -0
  288. package/python/frontend_src/src/stores/system.js +25 -0
  289. package/python/frontend_src/src/styles/base.css +138 -0
  290. package/python/frontend_src/src/styles/panel.css +117 -0
  291. package/python/frontend_src/src/styles/tokens.css +78 -0
  292. package/python/frontend_src/vite.config.js +39 -0
  293. package/python/pyproject.toml +268 -0
  294. package/python/requirements.txt +311 -0
  295. package/python/skills/README.md +123 -0
  296. package/python/skills/execution-contract/SKILL.md +30 -0
  297. package/python/skills/general-assistant/SKILL.md +34 -0
  298. package/python/skills/planner/SKILL.md +33 -0
  299. package/python/skills/schedule-itinerary/SKILL.md +35 -0
  300. package/python/skills/skill-composition/SKILL.md +31 -0
  301. package/python/skills/skill-guard/SKILL.md +67 -0
  302. package/python/skills/skill-memory/SKILL.md +50 -0
  303. package/python/skills/skill-router/SKILL.md +35 -0
  304. package/python/tests/conftest.py +54 -0
  305. package/python/tests/run_all.py +200 -0
  306. package/python/tests/test_agent_lifecycle.py +326 -0
  307. package/python/tests/test_ahs_p2.py +393 -0
  308. package/python/tests/test_anthropic_tools.py +192 -0
  309. package/python/tests/test_api.py +97 -0
  310. package/python/tests/test_api_auth.py +79 -0
  311. package/python/tests/test_api_host_guard.py +69 -0
  312. package/python/tests/test_attachments.py +151 -0
  313. package/python/tests/test_audit_chain.py +116 -0
  314. package/python/tests/test_audit_sink.py +130 -0
  315. package/python/tests/test_browser.py +149 -0
  316. package/python/tests/test_browser_request_guard.py +180 -0
  317. package/python/tests/test_capabilities.py +152 -0
  318. package/python/tests/test_channel_signing.py +185 -0
  319. package/python/tests/test_channel_trust.py +109 -0
  320. package/python/tests/test_channels.py +174 -0
  321. package/python/tests/test_cli.py +46 -0
  322. package/python/tests/test_compaction_toolpair.py +119 -0
  323. package/python/tests/test_config_layering.py +61 -0
  324. package/python/tests/test_confirm_nonce.py +186 -0
  325. package/python/tests/test_context_compaction_wiring.py +124 -0
  326. package/python/tests/test_dispatch.py +89 -0
  327. package/python/tests/test_domain_evals.py +334 -0
  328. package/python/tests/test_domain_import.py +72 -0
  329. package/python/tests/test_domain_reasoners.py +134 -0
  330. package/python/tests/test_e2e.py +161 -0
  331. package/python/tests/test_emb_cache.py +97 -0
  332. package/python/tests/test_embedding_singleton.py +87 -0
  333. package/python/tests/test_envfile.py +83 -0
  334. package/python/tests/test_evals_smoke.py +90 -0
  335. package/python/tests/test_event_spine.py +174 -0
  336. package/python/tests/test_evidence_semantic.py +88 -0
  337. package/python/tests/test_evidence_sse.py +94 -0
  338. package/python/tests/test_file_ingest.py +135 -0
  339. package/python/tests/test_finance.py +144 -0
  340. package/python/tests/test_finance_ledger.py +245 -0
  341. package/python/tests/test_finance_persist.py +174 -0
  342. package/python/tests/test_gateway.py +125 -0
  343. package/python/tests/test_governance.py +421 -0
  344. package/python/tests/test_governance_audit.py +324 -0
  345. package/python/tests/test_governance_middleware.py +137 -0
  346. package/python/tests/test_hkc.py +268 -0
  347. package/python/tests/test_hkc_index_build_manager.py +306 -0
  348. package/python/tests/test_hkc_manifest_reuse.py +294 -0
  349. package/python/tests/test_hkc_search_event_sync.py +482 -0
  350. package/python/tests/test_import_path_guard.py +155 -0
  351. package/python/tests/test_injection_guard.py +193 -0
  352. package/python/tests/test_integration.py +117 -0
  353. package/python/tests/test_itinerary.py +100 -0
  354. package/python/tests/test_kee_semantic.py +84 -0
  355. package/python/tests/test_kernel.py +212 -0
  356. package/python/tests/test_learning.py +101 -0
  357. package/python/tests/test_llm_admin.py +145 -0
  358. package/python/tests/test_llm_planner.py +344 -0
  359. package/python/tests/test_markdown_skills.py +62 -0
  360. package/python/tests/test_markdown_skills_openclaw.py +66 -0
  361. package/python/tests/test_mcp_connectors.py +97 -0
  362. package/python/tests/test_mcp_guardian_discovery.py +87 -0
  363. package/python/tests/test_mcp_no_memory.py +43 -0
  364. package/python/tests/test_mcp_policy_gate.py +76 -0
  365. package/python/tests/test_mcp_server.py +92 -0
  366. package/python/tests/test_memory_recall.py +92 -0
  367. package/python/tests/test_metrics.py +58 -0
  368. package/python/tests/test_multi_chain.py +90 -0
  369. package/python/tests/test_multimodal.py +122 -0
  370. package/python/tests/test_negotiation.py +136 -0
  371. package/python/tests/test_orchestration.py +109 -0
  372. package/python/tests/test_orchestration_decoupled.py +103 -0
  373. package/python/tests/test_p3_p5_hardening.py +324 -0
  374. package/python/tests/test_parallel_dispatch.py +207 -0
  375. package/python/tests/test_pipeline.py +492 -0
  376. package/python/tests/test_plan_visible.py +86 -0
  377. package/python/tests/test_planner.py +88 -0
  378. package/python/tests/test_policy_gate.py +108 -0
  379. package/python/tests/test_profile.py +67 -0
  380. package/python/tests/test_readme_claims.py +163 -0
  381. package/python/tests/test_reasoner.py +170 -0
  382. package/python/tests/test_release_check.py +252 -0
  383. package/python/tests/test_resume_checkpoint.py +148 -0
  384. package/python/tests/test_routing.py +85 -0
  385. package/python/tests/test_scheduler.py +138 -0
  386. package/python/tests/test_search.py +194 -0
  387. package/python/tests/test_secrets.py +153 -0
  388. package/python/tests/test_semantic.py +69 -0
  389. package/python/tests/test_sessions.py +90 -0
  390. package/python/tests/test_settings.py +265 -0
  391. package/python/tests/test_shadow_learning.py +290 -0
  392. package/python/tests/test_shared_write.py +200 -0
  393. package/python/tests/test_skill_taint_gate.py +145 -0
  394. package/python/tests/test_skills.py +117 -0
  395. package/python/tests/test_skills_fetch.py +282 -0
  396. package/python/tests/test_stats_and_events.py +162 -0
  397. package/python/tests/test_streaming.py +55 -0
  398. package/python/tests/test_supervisor_pipeline.py +176 -0
  399. package/python/tests/test_supervisor_triage.py +102 -0
  400. package/python/tests/test_taint.py +112 -0
  401. package/python/tests/test_task_stream.py +74 -0
  402. package/python/tests/test_thinking_sse.py +66 -0
  403. package/python/tests/test_thinking_stream.py +76 -0
  404. package/python/tests/test_tool_center.py +113 -0
  405. package/python/tests/test_tool_parallel.py +113 -0
  406. package/python/tests/test_tool_params.py +99 -0
  407. package/python/tests/test_tool_use.py +229 -0
  408. package/python/tests/test_toplevel_concurrency.py +116 -0
  409. package/python/tests/test_tracing.py +256 -0
  410. package/python/tests/test_truth_crystallizer.py +125 -0
  411. package/python/tests/test_truth_flow.py +88 -0
  412. package/python/tests/test_update_check.py +95 -0
  413. package/python/tests/test_url_policy.py +53 -0
  414. package/python/tests/test_verify_evidence.py +212 -0
  415. package/python/tests/test_version_sync.py +100 -0
  416. package/python/tests/test_web_trigger.py +82 -0
  417. package/python/tools/check_faiss_type.py +30 -0
  418. package/python/tools/check_log.ps1 +38 -0
  419. package/python/tools/ivf_status.py +72 -0
  420. package/python/tools/mypy_baseline.txt +13 -0
  421. package/python/tools/mypy_ratchet.py +143 -0
  422. package/python/tools/pack.py +173 -0
  423. package/python/tools/release_check.py +349 -0
  424. package/python/tools/restart_foxclaw.ps1 +71 -0
  425. package/python/tools/shared_write_audit.py +190 -0
  426. package/python/tools/size_baseline.txt +28 -0
  427. package/python/tools/size_check.py +102 -0
  428. package/python/tools/strip_comments.py +228 -0
  429. package/python/tools/verify_env.bat +2 -0
  430. package/python/tools/verify_env.py +25 -0
  431. package/python/vendor/HMR_LICENSE +37 -0
  432. package/python/vendor/HMR_README.md +65 -0
  433. package/python/vendor/hkc/LICENSE +37 -0
  434. package/python/vendor/hkc/hkc_ace/__init__.py +0 -0
  435. package/python/vendor/hkc/hkc_ace/ace.py +457 -0
  436. package/python/vendor/hkc/hkc_ace/skill_taxonomy.json +465 -0
  437. package/python/vendor/hkc/hkc_api/__init__.py +0 -0
  438. package/python/vendor/hkc/hkc_api/container.py +416 -0
  439. package/python/vendor/hkc/hkc_api/ingest_jobs.py +103 -0
  440. package/python/vendor/hkc/hkc_api/main.py +157 -0
  441. package/python/vendor/hkc/hkc_api/routes/__init__.py +0 -0
  442. package/python/vendor/hkc/hkc_api/routes/ability.py +151 -0
  443. package/python/vendor/hkc/hkc_api/routes/config.py +51 -0
  444. package/python/vendor/hkc/hkc_api/routes/knowledge.py +630 -0
  445. package/python/vendor/hkc/hkc_api/routes/search.py +88 -0
  446. package/python/vendor/hkc/hkc_api/schemas.py +153 -0
  447. package/python/vendor/hkc/hkc_api/synthesis.py +179 -0
  448. package/python/vendor/hkc/hkc_core/__init__.py +0 -0
  449. package/python/vendor/hkc/hkc_core/graph/__init__.py +0 -0
  450. package/python/vendor/hkc/hkc_core/graph/base.py +188 -0
  451. package/python/vendor/hkc/hkc_core/graph/sqlite_store.py +789 -0
  452. package/python/vendor/hkc/hkc_core/models/__init__.py +0 -0
  453. package/python/vendor/hkc/hkc_core/models/enums.py +65 -0
  454. package/python/vendor/hkc/hkc_core/models/ku.py +307 -0
  455. package/python/vendor/hkc/hkc_core/utils/__init__.py +0 -0
  456. package/python/vendor/hkc/hkc_core/utils/id_gen.py +113 -0
  457. package/python/vendor/hkc/hkc_crystallizer/__init__.py +44 -0
  458. package/python/vendor/hkc/hkc_crystallizer/adapters.py +94 -0
  459. package/python/vendor/hkc/hkc_crystallizer/candidate.py +145 -0
  460. package/python/vendor/hkc/hkc_crystallizer/crystallizer.py +179 -0
  461. package/python/vendor/hkc/hkc_crystallizer/ingress.py +90 -0
  462. package/python/vendor/hkc/hkc_kde/__init__.py +0 -0
  463. package/python/vendor/hkc/hkc_kde/chunker.py +197 -0
  464. package/python/vendor/hkc/hkc_kde/extractor.py +623 -0
  465. package/python/vendor/hkc/hkc_kde/kde.py +298 -0
  466. package/python/vendor/hkc/hkc_kde/loaders/__init__.py +0 -0
  467. package/python/vendor/hkc/hkc_kde/loaders/ebook_loader.py +153 -0
  468. package/python/vendor/hkc/hkc_kde/loaders/office_loader.py +194 -0
  469. package/python/vendor/hkc/hkc_kde/loaders/pdf_loader.py +162 -0
  470. package/python/vendor/hkc/hkc_kde/loaders/text_loader.py +157 -0
  471. package/python/vendor/hkc/hkc_kde/loaders/web_loader.py +91 -0
  472. package/python/vendor/hkc/hkc_kde/models.py +74 -0
  473. package/python/vendor/hkc/hkc_kde/packager.py +329 -0
  474. package/python/vendor/hkc/hkc_kde/relation_disc.py +151 -0
  475. package/python/vendor/hkc/hkc_kee/__init__.py +0 -0
  476. package/python/vendor/hkc/hkc_kee/dedup.py +227 -0
  477. package/python/vendor/hkc/hkc_kee/fingerprint.py +106 -0
  478. package/python/vendor/hkc/hkc_kee/kee.py +541 -0
  479. package/python/vendor/hkc/hkc_kep/__init__.py +0 -0
  480. package/python/vendor/hkc/hkc_kep/event_bus.py +205 -0
  481. package/python/vendor/hkc/hkc_sdk/__init__.py +63 -0
  482. package/python/vendor/hkc/hkc_sdk/base.py +151 -0
  483. package/python/vendor/hkc/hkc_sdk/direct_client.py +153 -0
  484. package/python/vendor/hkc/hkc_sdk/exceptions.py +39 -0
  485. package/python/vendor/hkc/hkc_sdk/http_client.py +166 -0
  486. package/python/vendor/hkc/hkc_sdk/models.py +171 -0
  487. package/python/vendor/hkc/hkc_search/__init__.py +0 -0
  488. package/python/vendor/hkc/hkc_search/bm25.py +325 -0
  489. package/python/vendor/hkc/hkc_search/embedding_backends.py +374 -0
  490. package/python/vendor/hkc/hkc_search/graph_search.py +132 -0
  491. package/python/vendor/hkc/hkc_search/hybrid.py +331 -0
  492. package/python/vendor/hkc/hkc_search/index_build_manager.py +378 -0
  493. package/python/vendor/hkc/hkc_search/vector_search.py +484 -0
  494. package/python/vendor/hmr/__init__.py +0 -0
  495. package/python/vendor/hmr/core/__init__.py +0 -0
  496. package/python/vendor/hmr/core/hmr.py +684 -0
  497. package/python/vendor/hmr/core/models.py +310 -0
  498. package/python/vendor/hmr/engines/__init__.py +0 -0
  499. package/python/vendor/hmr/engines/evolution.py +710 -0
  500. package/python/vendor/hmr/engines/jit_compiler.py +472 -0
  501. package/python/vendor/hmr/engines/lifecycle.py +264 -0
  502. package/python/vendor/hmr/engines/policy.py +625 -0
  503. package/python/vendor/hmr/engines/recall.py +260 -0
  504. package/python/vendor/hmr/engines/runtime_state.py +158 -0
  505. package/python/vendor/hmr/engines/scheduler.py +323 -0
  506. package/python/vendor/hmr/engines/semantic.py +150 -0
  507. package/python/vendor/hmr/engines/temporal.py +261 -0
  508. package/python/vendor/hmr/engines/thought_chain.py +635 -0
  509. package/python/vendor/hmr/graph/__init__.py +0 -0
  510. package/python/vendor/hmr/graph/cwg.py +338 -0
  511. package/python/vendor/hmr/graph/memory_graph.py +530 -0
  512. package/python/vendor/hmr/storage/__init__.py +0 -0
  513. package/python/vendor/hmr/storage/memory_fs.py +300 -0
  514. package/python/vendor/hmr/storage/vector_store.py +520 -0
@@ -0,0 +1,449 @@
1
+ """LLM 模型管理 — 前端下拉式选模型 + 自定义模型(不污染 .env)。
2
+
3
+ 设计要点
4
+ ────────
5
+ - 自定义模型存到 ``FOXCLAW_DATA_DIR/custom_models.json``(API key 简单 base64 混淆,
6
+ 本地单用户足够 — 真实部署应换 secret manager)。
7
+ - 当前选中存到 ``FOXCLAW_DATA_DIR/.current_model``;``model_router`` 用 mtime 缓存
8
+ 做 lazy reload,**用户改完不用重启 foxclaw**。
9
+ - 所有 GET 都不返回完整 API key(只返回 ``has_key`` 布尔);POST ``/test`` 也不外泄 key。
10
+ """
11
+ import os
12
+ import json
13
+ import time
14
+ from pathlib import Path
15
+ from typing import Optional, Dict
16
+
17
+ from fastapi import HTTPException
18
+ from pydantic import BaseModel
19
+
20
+ import foxclaw.api as _api
21
+ from .. import app
22
+ from ...core import secrets as _secrets
23
+ from ...core.model_router import normalize_base_url
24
+
25
+
26
+ _DATA_DIR = Path(os.environ.get("FOXCLAW_DATA_DIR", "./foxclaw_data"))
27
+ CUSTOM_MODELS_PATH = _DATA_DIR / "custom_models.json"
28
+ CURRENT_MODEL_PATH = _DATA_DIR / ".current_model"
29
+
30
+
31
+ _PRESET_PROVIDERS = [
32
+ {"provider": "generic", "label": "通用 OpenAI 兼容", "color": "#7e93b8"},
33
+ {"provider": "anthropic", "label": "Claude", "color": "#ff9d00"},
34
+ {"provider": "openai", "label": "GPT", "color": "#00ffb2"},
35
+ {"provider": "deepseek", "label": "DeepSeek", "color": "#5b8cff"},
36
+ {"provider": "google", "label": "Gemini", "color": "#38bdf8"},
37
+ {"provider": "qwen", "label": "通义千问", "color": "#ffc24b"},
38
+ {"provider": "minimax", "label": "MiniMax", "color": "#a78bfa"},
39
+ {"provider": "local", "label": "本地模型", "color": "#d99a3c"},
40
+ ]
41
+
42
+ _PROVIDER_KEY_ENV = {
43
+ "anthropic": "ANTHROPIC_API_KEY",
44
+ "openai": "OPENAI_API_KEY",
45
+ "deepseek": "DEEPSEEK_API_KEY",
46
+ "google": "GOOGLE_API_KEY",
47
+ "qwen": "DASHSCOPE_API_KEY",
48
+ "minimax": "MINIMAX_API_KEY",
49
+ "local": "LOCAL_LLM_BASE",
50
+ "generic": "GENERIC_LLM_API_KEY",
51
+ }
52
+
53
+
54
+ def _save_key(name: str, key: str) -> dict:
55
+ """把 key 存到最安全的地方,返回该写进 custom_models.json 的字段。
56
+
57
+ 有 OS 凭据库 → 只留 `api_key_kr: True` 标记,JSON 里不再有任何可还原的密文;
58
+ 没有 → 保持 base64(导入/删除/无桌面 Linux 等场景)。两个键总是成对写,
59
+ 免得「旧密文 + 新标记」并存导致 has_key 与读取路径对不上。
60
+ """
61
+ key = (key or "").strip()
62
+ if not key:
63
+ return {"api_key_kr": False, "api_key_obf": ""}
64
+ if _secrets.store(name, key):
65
+ return {"api_key_kr": True, "api_key_obf": ""}
66
+ return {"api_key_kr": False, "api_key_obf": _secrets.obf(key)}
67
+
68
+
69
+ def _has_key(m: dict) -> bool:
70
+ """列表/详情只答「有没有 key」,绝不把 key 本身回给前端。"""
71
+ return bool(m.get("api_key_obf") or m.get("api_key") or m.get("api_key_kr"))
72
+
73
+ def _load_custom() -> list:
74
+ if not CUSTOM_MODELS_PATH.exists():
75
+ return []
76
+ try:
77
+ return json.loads(CUSTOM_MODELS_PATH.read_text(encoding="utf-8"))
78
+ except Exception:
79
+ return []
80
+
81
+ def _save_custom(models: list):
82
+ CUSTOM_MODELS_PATH.parent.mkdir(parents=True, exist_ok=True)
83
+ CUSTOM_MODELS_PATH.write_text(
84
+ json.dumps(models, ensure_ascii=False, indent=2),
85
+ encoding="utf-8",
86
+ )
87
+
88
+
89
+ def _sync_hkc_llm() -> None:
90
+ """把「界面当前选中的模型」推给知识抽取(HKC KDE extractor)。
91
+
92
+ 病根:``init_hkc`` 只在启动时 ``_detect_llm()`` 一次,extractor 拿的是那一刻的快照。
93
+ 用户在界面切了模型(或换掉失效的 key)→ 主对话走 mtime lazy reload 立刻生效,
94
+ 知识抽取却还用旧的失效 key,于是摄入永远 401 → 0 条 KU → 复习板块永远 0 卡。
95
+ extractor 自带 ``reconfigure``(HKC 自己的界面也用它),这里复用同一条路径。
96
+ """
97
+ try:
98
+ os_inst = _api.current_os()
99
+ hkc = os_inst.hkc if os_inst is not None else None
100
+ if hkc is None or not getattr(hkc, "available", False):
101
+ return
102
+ from foxclaw.knowledge import _detect_llm
103
+ provider, key, base, model = _detect_llm()
104
+ hkc.container.kde.extractor.reconfigure(
105
+ provider=provider,
106
+ api_key=key or None,
107
+ model=model or None,
108
+ base_url=base or None,
109
+ )
110
+ except Exception as e:
111
+ print(f"[llm_admin] 同步知识抽取(HKC)配置失败: {e}")
112
+
113
+
114
+ @app.get("/api/llm/models")
115
+ async def list_models():
116
+ """列出所有可用模型:预置(按 .env 探测) + 自定义(从 custom_models.json) + 当前选中。"""
117
+ available = []
118
+ for p in _PRESET_PROVIDERS:
119
+ env_key = _PROVIDER_KEY_ENV[p["provider"]]
120
+ configured = bool(os.environ.get(env_key, "").strip())
121
+ available.append({**p, "configured": configured, "source": "preset"})
122
+ custom = []
123
+ for m in _load_custom():
124
+ custom.append({
125
+ "name": m.get("name", ""),
126
+ "model": m.get("model", ""),
127
+ "base_url": m.get("base_url", ""),
128
+ "note": m.get("note", ""),
129
+ "has_key": _has_key(m),
130
+ "source": "custom",
131
+ })
132
+ current = None
133
+ if CURRENT_MODEL_PATH.exists():
134
+ try:
135
+ current = json.loads(CURRENT_MODEL_PATH.read_text(encoding="utf-8"))
136
+ except Exception:
137
+ pass
138
+ return {"providers": available, "custom": custom, "current": current}
139
+
140
+
141
+ class _SelectReq(BaseModel):
142
+ source: str
143
+ provider: Optional[str] = None
144
+ name: Optional[str] = None
145
+
146
+ @app.post("/api/llm/select")
147
+ async def select_model(req: _SelectReq):
148
+ """记录用户选择 *并立即生效*。
149
+
150
+ 病根:此前只写 ``.current_model`` 就算成功 —— 若该自定义模型其实构造不出 client
151
+ (key 空 / BASE URL 非法 / 记录已被删),前端显示"已切换",聊天却继续走旧 provider,
152
+ 用户看到的是"填了填不上"。现在选中前先自检,失败直接 400 带原因。
153
+ """
154
+ os_inst = _api.current_os()
155
+ router = os_inst.router if os_inst is not None else None
156
+ if req.source == "auto":
157
+ if CURRENT_MODEL_PATH.exists():
158
+ CURRENT_MODEL_PATH.unlink()
159
+ if router is not None:
160
+ router._maybe_reload_selection()
161
+ _sync_hkc_llm()
162
+ return {"ok": True, "current": None}
163
+ if req.source == "preset":
164
+ if not req.provider or req.provider not in _PROVIDER_KEY_ENV:
165
+ raise HTTPException(400, f"未知 provider: {req.provider}")
166
+ env_key = _PROVIDER_KEY_ENV[req.provider]
167
+ if not os.environ.get(env_key, "").strip():
168
+ raise HTTPException(400, f"{req.provider} 未在 .env 配置 Key({env_key})")
169
+ data = {"source": "preset", "provider": req.provider}
170
+ elif req.source == "custom":
171
+ name = req.name
172
+ if not name:
173
+ raise HTTPException(400, "缺少自定义模型名称 name")
174
+ models = _load_custom()
175
+ match = next((m for m in models if m.get("name") == name), None)
176
+ if not match:
177
+ raise HTTPException(404, f"未找到自定义模型: {name}")
178
+ if router is not None:
179
+ client, _model, err = router._build_custom_client(name)
180
+ if client is None:
181
+ raise HTTPException(400, f"该自定义模型不可用: {err}")
182
+ data = {"source": "custom", "name": name, "model_id": match.get("model", "")}
183
+ else:
184
+ raise HTTPException(400, f"未知 source: {req.source}")
185
+ CURRENT_MODEL_PATH.parent.mkdir(parents=True, exist_ok=True)
186
+ CURRENT_MODEL_PATH.write_text(json.dumps(data, ensure_ascii=False), encoding="utf-8")
187
+ if router is not None:
188
+ router._maybe_reload_selection()
189
+ _sync_hkc_llm()
190
+ return {"ok": True, "current": data,
191
+ "runtime": ({"provider": router.provider,
192
+ "model": getattr(router, "model", ""),
193
+ "client_ok": getattr(router, "_client", None) is not None}
194
+ if router is not None else None)}
195
+
196
+
197
+ class _CustomAddReq(BaseModel):
198
+ name: str
199
+ base_url: str
200
+ api_key: str
201
+ model: str
202
+ extra_headers: Optional[Dict[str, str]] = None
203
+ group_id: Optional[str] = None
204
+ note: Optional[str] = ""
205
+
206
+ @app.post("/api/llm/custom/add")
207
+ async def add_custom_model(req: _CustomAddReq):
208
+ """添加自定义模型(不污染 .env,存到 FOXCLAW_DATA_DIR/custom_models.json)。"""
209
+ name = req.name.strip()
210
+ if not name:
211
+ raise HTTPException(400, "name 不能为空")
212
+ if not req.base_url.strip() or not req.api_key.strip() or not req.model.strip():
213
+ raise HTTPException(400, "base_url / api_key / model 都必填")
214
+ try:
215
+ base_url = normalize_base_url(req.base_url)
216
+ except ValueError as e:
217
+ raise HTTPException(400, str(e))
218
+ models = _load_custom()
219
+ if any(m.get("name") == name for m in models):
220
+ raise HTTPException(409, f"已存在同名模型: {name}")
221
+ extra = dict(req.extra_headers or {})
222
+ if req.group_id and req.group_id.strip():
223
+ extra["MiniMax-Group-Id"] = req.group_id.strip()
224
+ models.append({
225
+ "name": name,
226
+ "base_url": base_url,
227
+ **_save_key(name, req.api_key),
228
+ "model": req.model.strip(),
229
+ "extra_headers": extra,
230
+ "note": req.note or "",
231
+ "created": time.time(),
232
+ })
233
+ _save_custom(models)
234
+ return {"ok": True, "name": name}
235
+
236
+
237
+ class _CustomUpdateReq(BaseModel):
238
+ """编辑自定义模型:None / 空字符串 语义不同。
239
+ - 字段没传(= None):该字段不动
240
+ - 字段传了但空字符串:对 note/group_id/extra_headers 是 '清空' 语义;
241
+ 对 api_key 是 '保留原值'(防误清)
242
+ - 字段传了非空:用新值覆盖
243
+ """
244
+ name: Optional[str] = None
245
+ base_url: Optional[str] = None
246
+ api_key: Optional[str] = None
247
+ model: Optional[str] = None
248
+ extra_headers: Optional[Dict[str, str]] = None
249
+ group_id: Optional[str] = None
250
+ note: Optional[str] = None
251
+
252
+
253
+ @app.get("/api/llm/custom/{name}")
254
+ async def get_custom_model(name: str):
255
+ """取单个自定义模型详情(供前端编辑表单预填)。
256
+ api_key 永不出网 — 只返 ``has_key`` 布尔(与 list_models 一致)。"""
257
+ models = _load_custom()
258
+ match = next((m for m in models if m.get("name") == name), None)
259
+ if not match:
260
+ raise HTTPException(404, f"未找到: {name}")
261
+ extra = dict(match.get("extra_headers") or {})
262
+ gid = extra.get("MiniMax-Group-Id", "")
263
+ extra_only = {k: v for k, v in extra.items() if k != "MiniMax-Group-Id"}
264
+ return {
265
+ "name": match.get("name", ""),
266
+ "base_url": match.get("base_url", ""),
267
+ "model": match.get("model", ""),
268
+ "has_key": _has_key(match),
269
+ "group_id": gid,
270
+ "extra_headers": extra_only,
271
+ "note": match.get("note", ""),
272
+ }
273
+
274
+
275
+ @app.put("/api/llm/custom/{name}")
276
+ async def update_custom_model(name: str, req: _CustomUpdateReq):
277
+ """编辑自定义模型。
278
+ 设计要点
279
+ --------
280
+ 1. api_key 留空 = **保留原值**(防误清 — 这是用户改 key 时最常踩的坑)
281
+ 2. extra_headers **合并**而非替换(避免覆盖 group_id)
282
+ 3. group_id 显式空字符串 = 移除 MiniMax-Group-Id header;非空 = 设置 / 覆盖
283
+ 4. 改名时同步更新 ``.current_model``,避免选择状态指向不存在的名字
284
+ """
285
+ models = _load_custom()
286
+ idx = next((i for i, m in enumerate(models) if m.get("name") == name), None)
287
+ if idx is None:
288
+ raise HTTPException(404, f"未找到: {name}")
289
+ target = models[idx]
290
+
291
+ new_name = (req.name or "").strip() or target["name"]
292
+ if new_name != name and any(m.get("name") == new_name for m in models):
293
+ raise HTTPException(409, f"已存在同名模型: {new_name}")
294
+
295
+ if req.base_url is not None and not req.base_url.strip():
296
+ raise HTTPException(400, "base_url 不能为空")
297
+ if req.model is not None and not req.model.strip():
298
+ raise HTTPException(400, "model 不能为空")
299
+
300
+ if req.base_url is not None:
301
+ try:
302
+ target["base_url"] = normalize_base_url(req.base_url)
303
+ except ValueError as e:
304
+ raise HTTPException(400, str(e))
305
+ if req.model is not None:
306
+ target["model"] = req.model.strip()
307
+ if req.api_key is not None and req.api_key.strip():
308
+ target.update(_save_key(new_name, req.api_key))
309
+ if req.note is not None:
310
+ target["note"] = req.note
311
+
312
+ if req.extra_headers is not None or req.group_id is not None:
313
+ extra = dict(target.get("extra_headers") or {})
314
+ if req.extra_headers is not None:
315
+ extra.update(req.extra_headers)
316
+ if req.group_id is not None:
317
+ gid = req.group_id.strip()
318
+ if gid:
319
+ extra["MiniMax-Group-Id"] = gid
320
+ else:
321
+ extra.pop("MiniMax-Group-Id", None)
322
+ target["extra_headers"] = extra
323
+
324
+ if new_name != name:
325
+ if req.api_key is not None and req.api_key.strip():
326
+ _secrets.delete(name)
327
+ else:
328
+ _secrets.rename(name, new_name)
329
+ target["name"] = new_name
330
+ if CURRENT_MODEL_PATH.exists():
331
+ try:
332
+ cur = json.loads(CURRENT_MODEL_PATH.read_text(encoding="utf-8"))
333
+ if cur.get("source") == "custom" and cur.get("name") == name:
334
+ cur["name"] = new_name
335
+ CURRENT_MODEL_PATH.write_text(
336
+ json.dumps(cur, ensure_ascii=False), encoding="utf-8"
337
+ )
338
+ except Exception:
339
+ pass
340
+
341
+ target["updated"] = time.time()
342
+ _save_custom(models)
343
+ return {"ok": True, "name": target["name"]}
344
+
345
+
346
+ @app.delete("/api/llm/custom/{name}")
347
+ async def delete_custom_model(name: str):
348
+ models = _load_custom()
349
+ new_models = [m for m in models if m.get("name") != name]
350
+ if len(new_models) == len(models):
351
+ raise HTTPException(404, f"未找到: {name}")
352
+ _save_custom(new_models)
353
+ _secrets.delete(name)
354
+ if CURRENT_MODEL_PATH.exists():
355
+ try:
356
+ cur = json.loads(CURRENT_MODEL_PATH.read_text(encoding="utf-8"))
357
+ if cur.get("source") == "custom" and cur.get("name") == name:
358
+ CURRENT_MODEL_PATH.unlink()
359
+ except Exception:
360
+ pass
361
+ return {"ok": True}
362
+
363
+
364
+ class _TestReq(BaseModel):
365
+ source: str
366
+ provider: Optional[str] = None
367
+ name: Optional[str] = None
368
+
369
+ @app.post("/api/llm/test")
370
+ async def test_model(req: _TestReq):
371
+ """连通性测试:发个 'hi' 看延迟/错误,不发实际聊天(避免消耗大量 token)。"""
372
+ t0 = time.time()
373
+ try:
374
+ from openai import OpenAI
375
+ except ImportError:
376
+ raise HTTPException(500, "未安装 openai 库")
377
+ client = None
378
+ model = "default"
379
+ if req.source == "preset":
380
+ if not req.provider:
381
+ raise HTTPException(400, "preset 必须给 provider")
382
+ os_inst = _api.current_os()
383
+ router = os_inst.router if os_inst is not None else None
384
+ if router is None:
385
+ raise HTTPException(503, "OS 未就绪")
386
+ client, err = router._build_client(req.provider)
387
+ if client is None:
388
+ return {"ok": False, "error": err or "构造 client 失败", "latency_ms": 0}
389
+ try:
390
+ model = router._model_for(req.provider, "test", [{"role": "user", "content": "hi"}])
391
+ except Exception:
392
+ model = "default"
393
+ elif req.source == "custom":
394
+ models = _load_custom()
395
+ match = next((m for m in models if m.get("name") == req.name), None)
396
+ if not match:
397
+ raise HTTPException(404, f"未找到自定义模型: {req.name}")
398
+ key = _secrets.load(req.name or "", match.get("api_key_obf", ""))
399
+ try:
400
+ base_url = normalize_base_url(match.get("base_url", ""))
401
+ except ValueError as e:
402
+ return {"ok": False, "error": f"BASE URL 非法: {e}", "latency_ms": 0}
403
+ try:
404
+ client = OpenAI(
405
+ api_key=key,
406
+ base_url=base_url,
407
+ timeout=15.0,
408
+ default_headers=match.get("extra_headers") or {},
409
+ )
410
+ except Exception as e:
411
+ return {"ok": False, "error": f"构造 client 失败: {e}", "latency_ms": 0}
412
+ model = match["model"]
413
+ else:
414
+ raise HTTPException(400, f"未知 source: {req.source}")
415
+ try:
416
+ resp = client.chat.completions.create(
417
+ model=model,
418
+ messages=[{"role": "user", "content": "hi"}],
419
+ max_tokens=5,
420
+ )
421
+ dt = int((time.time() - t0) * 1000)
422
+ text = ""
423
+ try:
424
+ text = (resp.choices[0].message.content or "").strip()
425
+ except Exception:
426
+ pass
427
+ return {"ok": True, "latency_ms": dt, "model": model, "reply": text}
428
+ except Exception as e:
429
+ dt = int((time.time() - t0) * 1000)
430
+ return {"ok": False, "latency_ms": dt, "error": str(e)[:500]}
431
+
432
+
433
+ @app.get("/api/llm/runtime")
434
+ async def get_runtime():
435
+ """当前真正生效的 provider/model(供前端高亮 + 调试)。"""
436
+ os_inst = _api.current_os()
437
+ router = os_inst.router if os_inst is not None else None
438
+ if router is None:
439
+ return {"ok": False, "error": "OS 未就绪"}
440
+ try:
441
+ router._maybe_reload_selection()
442
+ except Exception:
443
+ pass
444
+ return {
445
+ "ok": True,
446
+ "provider": router.provider,
447
+ "model": getattr(router, "model", ""),
448
+ "client_ok": getattr(router, "_client", None) is not None,
449
+ }
@@ -0,0 +1,105 @@
1
+ """记忆统计 / 会话历史 / 用户偏好 路由。
2
+
3
+ 从 foxclaw/api.py 原样搬移(纯搬移,零行为变更)。共享 OS 实例经 `_api.current_os()`
4
+ 晚绑定读取(状态在 `_state`,测试可重绑 `_state._os_instance`);路由用 `@app` 装饰器在 import 时注册。
5
+ """
6
+
7
+ from fastapi import HTTPException
8
+ from pydantic import BaseModel
9
+
10
+ import foxclaw.api as _api
11
+ from .. import app
12
+
13
+
14
+ @app.get("/api/memory/stats")
15
+ async def get_memory_stats():
16
+ """记忆中心统计:供前端 vecRecall 面板展示真实数字。"""
17
+ try:
18
+ return _api.os_instance().hmr.stats()
19
+ except Exception:
20
+ return {"backend": "?", "total_memories": 0, "relations": 0}
21
+
22
+
23
+ class SessionCreate(BaseModel):
24
+ title: str = ""
25
+ agent_id: str = "supervisor"
26
+
27
+
28
+ class SessionRename(BaseModel):
29
+ title: str
30
+
31
+
32
+ @app.get("/api/sessions")
33
+ async def list_sessions(limit: int = 50):
34
+ """会话列表(按更新时间倒序,轻字段不含 messages)。"""
35
+ try:
36
+ return {"sessions": _api.os_instance().sessions.list(limit)}
37
+ except Exception:
38
+ return {"sessions": []}
39
+
40
+
41
+ @app.post("/api/sessions")
42
+ async def create_session(req: SessionCreate):
43
+ """新建会话。"""
44
+ return _api.os_instance().sessions.create(req.title, req.agent_id)
45
+
46
+
47
+ @app.get("/api/sessions/{sid}")
48
+ async def get_session(sid: str):
49
+ """取单个会话(含完整 messages),供前端恢复对话。"""
50
+ d = _api.os_instance().sessions.get(sid)
51
+ if d is None:
52
+ raise HTTPException(404, "会话不存在")
53
+ return d
54
+
55
+
56
+ @app.post("/api/sessions/{sid}/rename")
57
+ async def rename_session(sid: str, req: SessionRename):
58
+ return {"ok": _api.os_instance().sessions.rename(sid, req.title)}
59
+
60
+
61
+ @app.delete("/api/sessions/{sid}")
62
+ async def delete_session(sid: str):
63
+ return {"ok": _api.os_instance().sessions.delete(sid)}
64
+
65
+
66
+ @app.get("/api/memory/recent")
67
+ async def get_recent_memories(limit: int = 20):
68
+ """最近的记忆条目(倒序),供前端右栏"记忆信息条"。轻字段,不含 embedding。"""
69
+ try:
70
+ fn = getattr(_api.os_instance().hmr, "recent", None)
71
+ return {"memories": fn(limit) if callable(fn) else []}
72
+ except Exception:
73
+ return {"memories": []}
74
+
75
+
76
+ @app.get("/api/preferences")
77
+ async def list_preferences():
78
+ """列出已记住的用户偏好(供查看/清理误存)。偏好是 shared,任一 Agent 视角一致,
79
+ 这里用主管取全量(偏好是 shared,任一 Agent 视角一致)。返回 [{id, content, ...}]。"""
80
+ ag = _api.os_instance().supervisor
81
+ return {"preferences": ag.list_preferences()}
82
+
83
+
84
+ @app.delete("/api/preferences/{pref_id}")
85
+ async def delete_preference(pref_id: str):
86
+ """删一条误存的用户偏好(F3:防误报偏好持久注入污染)。返回 {ok}。"""
87
+ ag = _api.os_instance().supervisor
88
+ return {"ok": ag.delete_preference(pref_id)}
89
+
90
+
91
+ class ProfilePayload(BaseModel):
92
+ nickname: str = ""
93
+
94
+
95
+ @app.get("/api/profile")
96
+ async def get_profile():
97
+ """当前称呼。没设置过返回空串(前端据此决定要不要显示首次引导)。"""
98
+ return {"nickname": _api.os_instance().supervisor.get_nickname()}
99
+
100
+
101
+ @app.put("/api/profile")
102
+ async def update_profile(payload: ProfilePayload):
103
+ """设置称呼。传空串 = 清除。返回生效后的值(已 trim/截断)。"""
104
+ nick = _api.os_instance().supervisor.set_nickname(payload.nickname)
105
+ return {"ok": True, "nickname": nick}
@@ -0,0 +1,39 @@
1
+ """主管编排端点(全局态势 + 跨 Agent)。
2
+
3
+ 从 foxclaw/api.py 原样搬移(纯搬移,零行为变更)。共享 OS 实例经 `_api.current_os()`
4
+ 晚绑定读取;路由用 `@app` 装饰器在 import 时注册。
5
+ """
6
+
7
+ from fastapi import HTTPException
8
+ from pydantic import BaseModel
9
+
10
+ import foxclaw.api as _api
11
+ from .. import app
12
+
13
+
14
+ @app.get("/api/orchestration/situation")
15
+ async def global_situation():
16
+ """主管全局态势:三内核状态汇总 + 跨域告警(驾驶舱视图)。"""
17
+ try:
18
+ return _api.os_instance().supervisor.global_situation()
19
+ except Exception as e:
20
+ raise HTTPException(500, f"态势汇总失败: {e}")
21
+
22
+
23
+ class CrossReqPayload(BaseModel):
24
+ from_agent: str
25
+ to_agent: str
26
+ request_type: str
27
+ payload: dict
28
+ reason: str = ""
29
+
30
+
31
+ @app.post("/api/orchestration/request")
32
+ async def cross_request(p: CrossReqPayload):
33
+ """跨 Agent 请求:目标 Agent 用守门人裁决(如行程申请差旅预算)。"""
34
+ try:
35
+ return _api.os_instance().supervisor.cross_request(
36
+ from_agent=p.from_agent, to_agent=p.to_agent,
37
+ request_type=p.request_type, payload=p.payload, reason=p.reason)
38
+ except Exception as e:
39
+ raise HTTPException(500, f"跨 Agent 请求失败: {e}")
@@ -0,0 +1,44 @@
1
+ """断点续跑现场(P1):上次被中断的任务 + 各自跑到哪一步。
2
+
3
+ 只读端点,不触发新的日志写入。数据源:
4
+ · `Foxclaw.interrupted_tasks` —— 启动时 `RestartRecovery.scan_interrupted()` 的快照
5
+ (进行中且 boot_id 非本次启动 → 必然是被中断的);
6
+ · `RestartRecovery.steps_of(task_id)` —— 步骤级 checkpoint 回放:`kind=stage` 是管线阶段,
7
+ `kind=tool` 是工具调用(带指纹,续跑时可直接复用结果)。
8
+
9
+ 续跑方式:`POST /api/task`,原 task 文本 + `context={"_resume_task_id": "<task_id>"}`。
10
+ """
11
+
12
+ import foxclaw.api as _api
13
+ from .. import app
14
+
15
+
16
+ @app.get("/api/recovery/status")
17
+ async def recovery_status():
18
+ """中断任务清单 + 每任务最近步骤 + 续跑所需 context(直接可回灌 /api/task)。"""
19
+ os_ = _api.current_os()
20
+ if os_ is None:
21
+ return {"enabled": False, "interrupted": [], "plan": {}}
22
+ rec = getattr(os_, "recovery", None)
23
+ out = []
24
+ for t in getattr(os_, "interrupted_tasks", []) or []:
25
+ steps: list = []
26
+ try:
27
+ steps = rec.steps_of(t.task_id) if rec is not None else []
28
+ except Exception:
29
+ steps = []
30
+ out.append({
31
+ "task_id": t.task_id,
32
+ "description": t.description,
33
+ "session_id": t.session_id,
34
+ "channel": t.channel,
35
+ "status": t.status,
36
+ "steps_count": len(steps),
37
+ "last_steps": steps[-10:],
38
+ "resume_hint": {"endpoint": "/api/task",
39
+ "context": {"_resume_task_id": t.task_id}},
40
+ })
41
+ plan = getattr(os_, "recovery_plan", None)
42
+ as_dict = getattr(plan, "to_dict", None)
43
+ return {"enabled": rec is not None, "interrupted": out,
44
+ "plan": as_dict() if callable(as_dict) else {}}