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,144 @@
1
+ """
2
+ BrowserService —— HIR-Browser 在 Foxclaw 内的 async 安全接入层。
3
+
4
+ 它解决把"同步 Playwright 浏览器"塞进"async FastAPI"的三个硬约束:
5
+
6
+ A1 可选依赖:playwright(及 chromium 内核)是可选 extra。未安装时
7
+ available()=False,perceive/act 返回结构化"不可用"而非崩溃,
8
+ 与 search 子系统的优雅降级一致。
9
+
10
+ A2 线程亲和 + 事件循环禁区:Playwright sync API 不能在 asyncio 事件
11
+ 循环线程里运行,且其对象绑定创建它的线程。故本 Service 用一条
12
+ max_workers=1 的**专用工作线程**独占持有会话——所有浏览器操作都
13
+ 被 run_in_executor 派发到这同一条线程,既满足线程亲和,也天然把
14
+ 单会话(v1 只支持单会话)的访问串行化。
15
+
16
+ A3 感知入口:额外暴露 perceive()(只感知、不动作),供 agent/前端在
17
+ 决策前读取页面语义结构;原 WebAdapter 只有 execute。
18
+
19
+ 对外方法都是 async,直接 await 即可;内部 _*_sync 方法只在工作线程上跑。
20
+ """
21
+
22
+ import asyncio
23
+ import importlib.util
24
+ import threading
25
+ from concurrent.futures import ThreadPoolExecutor
26
+ from typing import Optional, TYPE_CHECKING
27
+
28
+
29
+ if TYPE_CHECKING:
30
+ from .session.session_manager import SessionManager
31
+
32
+ class BrowserService:
33
+
34
+ def __init__(self, headless: bool = True):
35
+ self._headless = headless
36
+ self._session: "SessionManager | None" = None
37
+ self._executor: Optional[ThreadPoolExecutor] = None
38
+ self._closed = False
39
+ self._launch_error: Optional[str] = None
40
+ self._guard = threading.Lock()
41
+
42
+ @staticmethod
43
+ def available() -> bool:
44
+ """playwright 包是否可导入。注意:这不保证 chromium 内核已 install,
45
+ 内核缺失会在首次 launch 时暴露,由 _launch_error 记录并透出。"""
46
+ return importlib.util.find_spec("playwright") is not None
47
+
48
+ def status(self) -> dict:
49
+ return {
50
+ "available": self.available(),
51
+ "headless": self._headless,
52
+ "session_active": self._session is not None,
53
+ "closed": self._closed,
54
+ "launch_error": self._launch_error,
55
+ }
56
+
57
+ def _get_executor(self) -> ThreadPoolExecutor:
58
+ with self._guard:
59
+ if self._closed:
60
+ raise RuntimeError("BrowserService 已关闭")
61
+ if self._executor is None:
62
+ self._executor = ThreadPoolExecutor(
63
+ max_workers=1, thread_name_prefix="foxclaw-browser")
64
+ return self._executor
65
+
66
+ def _ensure_session_sync(self):
67
+ """在工作线程上惰性构造会话(首次 launch chromium)。"""
68
+ if self._session is None:
69
+ from .session.session_manager import SessionManager
70
+ self._session = SessionManager(headless=self._headless)
71
+ return self._session
72
+
73
+ def _perceive_sync(self) -> dict:
74
+ return self._ensure_session_sync().snapshot().to_dict()
75
+
76
+ def _act_sync(self, action_dict: dict) -> dict:
77
+ from .action.action_types import WebAction
78
+ session = self._ensure_session_sync()
79
+ return session.execute(WebAction.from_dict(action_dict))
80
+
81
+ def _close_sync(self):
82
+ if self._session is not None:
83
+ try:
84
+ self._session.close()
85
+ finally:
86
+ self._session = None
87
+
88
+ async def _run(self, fn, *args):
89
+ loop = asyncio.get_running_loop()
90
+ return await loop.run_in_executor(self._get_executor(), fn, *args)
91
+
92
+ async def perceive(self) -> dict:
93
+ """只感知当前页面语义结构,不改变页面状态。"""
94
+ if not self.available():
95
+ return self._unavailable()
96
+ try:
97
+ return {"ok": True, "semantic_state": await self._run(self._perceive_sync)}
98
+ except Exception as e:
99
+ return self._handle_error(e)
100
+
101
+ async def act(self, action: dict) -> dict:
102
+ """执行一个 Web 动作(navigate/click/type/scroll/extract/wait),返回校验后最新语义结构。"""
103
+ if not self.available():
104
+ return self._unavailable()
105
+ if not isinstance(action, dict) or "type" not in action:
106
+ return {"ok": False, "error": "非法动作:需 {type, target?, value?}"}
107
+ try:
108
+ return {"ok": True, **await self._run(self._act_sync, action)}
109
+ except ValueError as e:
110
+ return {"ok": False, "error": str(e)}
111
+ except KeyError as e:
112
+ return {"ok": False, "error": str(e).strip('"')}
113
+ except Exception as e:
114
+ return self._handle_error(e)
115
+
116
+ async def close(self):
117
+ """释放浏览器会话并关停专用工作线程(幂等)。挂到 Foxclaw teardown。"""
118
+ with self._guard:
119
+ if self._closed:
120
+ return
121
+ self._closed = True
122
+ executor = self._executor
123
+ if executor is not None:
124
+ try:
125
+ loop = asyncio.get_running_loop()
126
+ await loop.run_in_executor(executor, self._close_sync)
127
+ finally:
128
+ executor.shutdown(wait=True)
129
+
130
+ def _unavailable(self) -> dict:
131
+ return {
132
+ "ok": False,
133
+ "available": False,
134
+ "error": "浏览器子系统不可用:未安装 playwright(可选依赖)。"
135
+ "安装:pip install 'foxclaw[browser]' 且 playwright install chromium",
136
+ }
137
+
138
+ def _handle_error(self, e: Exception) -> dict:
139
+ msg = str(e)
140
+ if "playwright install" in msg or "Executable doesn't exist" in msg:
141
+ self._launch_error = msg
142
+ return {"ok": False, "available": False,
143
+ "error": "playwright 已装但缺 chromium 内核,请运行:playwright install chromium"}
144
+ return {"ok": False, "error": f"{type(e).__name__}: {msg}"}
@@ -0,0 +1 @@
1
+ """会话层:单浏览器会话的生命周期与组件组装(感知/执行/校验的统一入口)。"""
@@ -0,0 +1,57 @@
1
+ """
2
+ SessionManager —— 单浏览器会话的生命周期管理与组件组装。
3
+
4
+ v1.0 明确只支持单会话(对应审计里"v1不做分布式/多浏览器集群"的边界)。
5
+ 多会话池、并发调度属于 Infrastructure Plane,v2 再做。
6
+
7
+ 【融合注记 A2】本类构造即 launch chromium(有线程亲和性),
8
+ 必须始终在 BrowserService 的同一条专用工作线程上构造与调用。
9
+ """
10
+
11
+ from ..driver.playwright_driver import PlaywrightDriver
12
+ from ..perception.dom_extractor import DomExtractor
13
+ from ..perception.element_indexer import ElementIndexer
14
+ from ..perception.semantic_reducer import SemanticReducer
15
+ from ..action.action_executor import ActionExecutor
16
+ from ..action.action_verifier import ActionVerifier
17
+ from ..action.action_types import WebAction
18
+
19
+
20
+ class SessionManager:
21
+
22
+ def __init__(self, headless: bool = True):
23
+ self.driver = PlaywrightDriver(headless=headless)
24
+ self.indexer = ElementIndexer()
25
+ self.dom_extractor = DomExtractor(self.driver)
26
+ self.reducer = SemanticReducer(self.dom_extractor, self.indexer)
27
+ self.executor = ActionExecutor(self.driver, self.indexer)
28
+ self.verifier = ActionVerifier()
29
+
30
+ def snapshot(self):
31
+ """对外暴露的唯一"感知"入口"""
32
+ return self.reducer.reduce()
33
+
34
+ def execute(self, action: WebAction) -> dict:
35
+ """
36
+ 对外暴露的唯一"执行"入口:
37
+ 执行前拍快照 -> 执行 -> 执行后拍快照 -> 校验 -> 返回最新语义结构
38
+
39
+ 注:执行前的 before 快照会重建元素编号表——这既服务于 diff 校验,
40
+ 也顺带刷新了可能已 detach 的旧句柄(页面在两次调用间可能已变化)。
41
+ 因此上层引用的编号应来自"上一次 execute/perceive 返回的 semantic_state",
42
+ 且假定两次调用之间页面结构稳定(编号按 DOM 顺序稳定复现)。
43
+ """
44
+ before = self.snapshot()
45
+ raw_result = self.executor.run(action)
46
+ after = self.snapshot()
47
+
48
+ verification = self.verifier.verify(before, after, action)
49
+
50
+ return {
51
+ "raw": raw_result,
52
+ "semantic_state": after.to_dict(),
53
+ "verified": verification,
54
+ }
55
+
56
+ def close(self):
57
+ self.driver.close()
@@ -0,0 +1,12 @@
1
+ """
2
+ foxclaw.capability —— 全系统共享的能力层(Capability Plane)。
3
+
4
+ 把"重、且应当全进程共享一份"的能力收敛到这里,避免各模块各自加载:
5
+ · EmbeddingService:sentence-transformers 文本向量,按模型名缓存,全进程只加载一次
6
+ (此前 5 个 Harness 各建一个 SemanticDrift → 各自 load 一次模型,启动 6×→现 1×)
7
+
8
+ (后续 v2 的 LLMRouter 也应上浮到这一层,与 EmbeddingService 并列。)
9
+ """
10
+ from .embedding_service import get_text_embedder, embedding_stats
11
+
12
+ __all__ = ["get_text_embedder", "embedding_stats"]
@@ -0,0 +1,65 @@
1
+ """
2
+ EmbeddingService —— 进程内共享的文本向量能力(P0 性能修复)。
3
+
4
+ 问题:此前每个 Harness 各建一个 SemanticDrift → 各自 `SentenceTransformer(model)`。
5
+ 5 个 Agent = 5 份同名模型驻留内存 + 5 次加载(启动慢的主因,run_all 实测 44s→7s
6
+ 还是靠离线缓存,加载次数没减)。
7
+
8
+ 方案:按模型名进程内缓存,同名模型只加载一次,全系统共享同一个只读实例。
9
+ · sentence-transformers 的 encode 是无梯度前向,多线程对同一只读模型并发 encode
10
+ 是安全的(标准的"一个模型服务多请求"模式),故共享比每 Agent 各持一份更优:
11
+ 内存 5×→1×,加载 5×→1×。
12
+ · 缺库/加载失败 → 返回 None,调用方(SemanticDrift)自动降级 n-gram 哈希嵌入。
13
+
14
+ 【P2·单 embedding 入口·share-bge-m3】
15
+ ★ 改用 HKC 进程缓存(hkc_search.embedding_backends.get_st_model)取模型 instance,
16
+ 与 HKC 检索 / HMR VectorStore / Harness SemanticDrift 全部走同一份 instance,
17
+ 全 Foxclaw 真正只 load bge-m3 **1 次**。之前这里自己 `SentenceTransformer(name)`,
18
+ 导致 Harness 启动期独立 Loading 第 2 次(浪费 ~1GB 内存)。
19
+ """
20
+ from __future__ import annotations
21
+ import os
22
+ import threading
23
+
24
+ _lock = threading.Lock()
25
+ _cache: dict[str, object] = {}
26
+ _load_attempts = 0
27
+
28
+
29
+ def get_text_embedder(model_name: str | None = None):
30
+ """返回按 model_name 缓存的共享文本向量模型(sentence-transformers)。
31
+ 走 HKC 进程缓存(hkc_search.embedding_backends.get_st_model),同名只加载一次,
32
+ 与 HKC / HMR 共用同一 instance(share-bge-m3)。缺库/失败返回 None。线程安全。
33
+
34
+ model_name 解析链(【P2·单 embedding 入口】)——
35
+ 显式传参 > EMBEDDING_MODEL(系统唯一本地入口) > HKC_EMBEDDING_MODEL > FOXCLAW_EMBEDDING_MODEL
36
+ > all-MiniLM-L6-v2(兜底)。与 HKC init / HMR VectorStore / harness.py 完全一致,
37
+ 保证 get_st_model 缓存 key 命中同一份 instance。"""
38
+ global _load_attempts
39
+ name = (model_name
40
+ or os.environ.get("EMBEDDING_MODEL")
41
+ or os.environ.get("HKC_EMBEDDING_MODEL")
42
+ or os.environ.get("FOXCLAW_EMBEDDING_MODEL")
43
+ or "all-MiniLM-L6-v2").strip()
44
+ with _lock:
45
+ if name in _cache:
46
+ return _cache[name]
47
+ _load_attempts += 1
48
+ model = None
49
+ try:
50
+ from hkc_search.embedding_backends import get_st_model as _hk_get
51
+ model = _hk_get(name)
52
+ except Exception:
53
+ model = None
54
+ _cache[name] = model
55
+ return model
56
+
57
+
58
+ def embedding_stats() -> dict:
59
+ """可观测:当前缓存了哪些模型、发起过多少次加载。用于证明"同名只加载一次"。"""
60
+ with _lock:
61
+ return {
62
+ "cached_models": {n: (m is not None) for n, m in _cache.items()},
63
+ "distinct_models": len(_cache),
64
+ "load_attempts": _load_attempts,
65
+ }
@@ -0,0 +1,138 @@
1
+ """
2
+ Foxclaw Channels — 渠道抽象层 (Channel Abstraction)
3
+
4
+ 借鉴 OpenClaw 的渠道架构:适配器模式 + 消息标准化。
5
+ 每个 IM 平台(Telegram/Slack/飞书…)一个适配器,封装该平台官方 API;
6
+ 入站消息统一标准化成 InboundMessage,出站统一从 OutboundMessage 分派。
7
+ 上层(主管 Agent)只跟标准消息打交道,不关心是哪个平台。
8
+
9
+ 接入流程(对称):
10
+ 平台原始消息 → adapter.normalize() → InboundMessage → on_message 回调
11
+ OutboundMessage → adapter.send() → 平台官方 API
12
+ """
13
+ from __future__ import annotations
14
+ from abc import ABC, abstractmethod
15
+ from dataclasses import dataclass, field
16
+ from typing import Callable, Optional
17
+ import time
18
+
19
+
20
+ @dataclass
21
+ class InboundMessage:
22
+ """标准化的入站消息(各平台 normalize 后的统一格式)。"""
23
+ channel: str
24
+ message_id: str
25
+ text: str
26
+ sender_id: str
27
+ sender_name: str = ""
28
+ conversation_id: str = ""
29
+ is_group: bool = False
30
+ timestamp: float = field(default_factory=time.time)
31
+ raw: dict = field(default_factory=dict)
32
+
33
+ def to_dict(self) -> dict:
34
+ return {"channel": self.channel, "message_id": self.message_id,
35
+ "text": self.text, "sender_id": self.sender_id,
36
+ "sender_name": self.sender_name,
37
+ "conversation_id": self.conversation_id,
38
+ "is_group": self.is_group, "timestamp": self.timestamp}
39
+
40
+
41
+ @dataclass
42
+ class OutboundMessage:
43
+ """标准化的出站消息(上层产出,各适配器转成平台格式发送)。"""
44
+ conversation_id: str
45
+ text: str
46
+ reply_to: str = ""
47
+ extra: dict = field(default_factory=dict)
48
+
49
+
50
+ @dataclass
51
+ class SendResult:
52
+ ok: bool
53
+ message_id: str = ""
54
+ error: str = ""
55
+
56
+
57
+ class ChannelAdapter(ABC):
58
+ """渠道适配器基类。每个平台实现 normalize/send/start。
59
+
60
+ 设计原则(借鉴 OpenClaw):
61
+ - 适配器只负责"翻译"(平台格式 ↔ 标准格式)和"收发",不含业务逻辑
62
+ - 凭证由宿主注入(token 等),适配器不硬编码任何密钥
63
+ - 入站统一回调 on_message,上层无需关心平台差异
64
+ """
65
+
66
+ CHANNEL = "base"
67
+
68
+ def __init__(self, on_message: Callable[[InboundMessage], None] | None = None):
69
+ self.on_message = on_message
70
+ self._running = False
71
+ self.last_error = ""
72
+
73
+ @abstractmethod
74
+ def normalize(self, raw: dict) -> Optional[InboundMessage]:
75
+ """把平台原始消息转成标准 InboundMessage。非消息事件返回 None。"""
76
+ ...
77
+
78
+ @abstractmethod
79
+ def send(self, msg: OutboundMessage) -> SendResult:
80
+ """把标准 OutboundMessage 通过平台官方 API 发出。"""
81
+ ...
82
+
83
+ def handle_raw(self, raw: dict):
84
+ """收到平台原始消息时调用:标准化 → 触发 on_message 回调。
85
+
86
+ 全程异常隔离:单条消息处理失败不会抛出(否则会中断整批轮询,
87
+ 且已推进的游标不回滚 → 后续消息全部丢失)。失败记录在 last_error。
88
+ """
89
+ try:
90
+ inbound = self.normalize(raw)
91
+ except Exception as e:
92
+ self.last_error = f"normalize 失败: {e}"
93
+ return None
94
+ if inbound is None:
95
+ return None
96
+ if self.on_message:
97
+ try:
98
+ self.on_message(inbound)
99
+ except Exception as e:
100
+ self.last_error = f"on_message 失败: {e}"
101
+ return inbound
102
+
103
+ def reply(self, inbound: InboundMessage, text: str) -> SendResult:
104
+ """便捷回复:回到消息来源的会话。"""
105
+ return self.send(OutboundMessage(
106
+ conversation_id=inbound.conversation_id, text=text,
107
+ reply_to=inbound.message_id))
108
+
109
+ @property
110
+ def running(self) -> bool:
111
+ return self._running
112
+
113
+
114
+ class ChannelRegistry:
115
+ """渠道注册表。管理多个已接入的适配器,统一分派。"""
116
+
117
+ def __init__(self):
118
+ self._adapters: dict[str, ChannelAdapter] = {}
119
+
120
+ def register(self, adapter: ChannelAdapter):
121
+ self._adapters[adapter.CHANNEL] = adapter
122
+ return adapter
123
+
124
+ def get(self, channel: str) -> Optional[ChannelAdapter]:
125
+ return self._adapters.get(channel)
126
+
127
+ def all(self) -> list:
128
+ return list(self._adapters.values())
129
+
130
+ def send(self, channel: str, msg: OutboundMessage) -> SendResult:
131
+ adapter = self.get(channel)
132
+ if adapter is None:
133
+ return SendResult(False, error=f"渠道未接入: {channel}")
134
+ return adapter.send(msg)
135
+
136
+ def status(self) -> dict:
137
+ return {ch: {"running": a.running, "channel": a.CHANNEL}
138
+ for ch, a in self._adapters.items()}
@@ -0,0 +1,175 @@
1
+ """
2
+ Foxclaw Channels — Discord 适配器
3
+
4
+ 用 Discord 官方 API v10。接入模型和前三个又不同(不套同一模子):
5
+ · 收消息:Gateway WebSocket 长连接 —— Discord 主动推事件(MESSAGE_CREATE 等)
6
+ - 连接后要 IDENTIFY(带 bot token + intents)
7
+ - 要按 Discord 指定的间隔发心跳(HEARTBEAT)维持连接
8
+ - intents 决定能收哪些事件(收消息内容需 MESSAGE_CONTENT intent)
9
+ · 发消息:REST API POST /channels/{id}/messages(带 Bot token)
10
+
11
+ 凭证:bot token(Discord Developer Portal 创建应用 → Bot),由宿主注入。
12
+
13
+ 说明:完整 Gateway 长连接(心跳/重连/分片)较重,本适配器提供:
14
+ - send():REST 发送(完整可用)
15
+ - normalize():把 Discord MESSAGE_CREATE 事件标准化
16
+ - gateway 连接循环骨架(connect_gateway),宿主可在异步环境跑
17
+ 对收消息,推荐宿主用成熟的 discord.py/WebSocket 接 on_message,再喂给 normalize。
18
+ """
19
+ from __future__ import annotations
20
+ import json
21
+ from typing import Any, Optional
22
+ from . import ChannelAdapter, InboundMessage, OutboundMessage, SendResult
23
+
24
+ DISCORD_API_BASE = "https://discord.com/api/v10"
25
+ DISCORD_GATEWAY = "wss://gateway.discord.gg/?v=10&encoding=json"
26
+
27
+ OP_DISPATCH = 0
28
+ OP_HEARTBEAT = 1
29
+ OP_IDENTIFY = 2
30
+ OP_HELLO = 10
31
+ OP_HEARTBEAT_ACK = 11
32
+
33
+ INTENT_GUILD_MESSAGES = 1 << 9
34
+ INTENT_DIRECT_MESSAGES = 1 << 12
35
+ INTENT_MESSAGE_CONTENT = 1 << 15
36
+ DEFAULT_INTENTS = INTENT_GUILD_MESSAGES | INTENT_DIRECT_MESSAGES | INTENT_MESSAGE_CONTENT
37
+
38
+
39
+ class DiscordAdapter(ChannelAdapter):
40
+ CHANNEL = "discord"
41
+
42
+ def __init__(self, bot_token: str, intents: int = DEFAULT_INTENTS,
43
+ on_message=None, timeout: float = 30.0):
44
+ super().__init__(on_message=on_message)
45
+ self.bot_token = bot_token
46
+ self.intents = intents
47
+ self.timeout = timeout
48
+ self._bot_user_id = ""
49
+
50
+ def _headers(self) -> dict:
51
+ return {"Authorization": f"Bot {self.bot_token}",
52
+ "Content-Type": "application/json",
53
+ "User-Agent": "Foxclaw (https://github.com, 1.0)"}
54
+
55
+ def normalize(self, raw: dict) -> Optional[InboundMessage]:
56
+ """Discord Gateway 事件 → 标准 InboundMessage。
57
+
58
+ raw 可以是完整 Gateway 帧({op,t,d})或直接是 MESSAGE_CREATE 的 data。
59
+ """
60
+ if "op" in raw:
61
+ if raw.get("op") != OP_DISPATCH or raw.get("t") != "MESSAGE_CREATE":
62
+ return None
63
+ data = raw.get("d", {})
64
+ else:
65
+ data = raw
66
+ if not data:
67
+ return None
68
+ author = data.get("author", {})
69
+ if author.get("bot"):
70
+ return None
71
+ if self._bot_user_id and str(author.get("id")) == self._bot_user_id:
72
+ return None
73
+ text = data.get("content", "")
74
+ if not text:
75
+ return None
76
+ is_group = bool(data.get("guild_id"))
77
+ return InboundMessage(
78
+ channel=self.CHANNEL,
79
+ message_id=str(data.get("id", "")),
80
+ text=text,
81
+ sender_id=str(author.get("id", "")),
82
+ sender_name=author.get("username", ""),
83
+ conversation_id=str(data.get("channel_id", "")),
84
+ is_group=is_group,
85
+ raw=data)
86
+
87
+ def send(self, msg: OutboundMessage) -> SendResult:
88
+ """POST /channels/{channel_id}/messages 发文本。"""
89
+ try:
90
+ import httpx
91
+ except ImportError:
92
+ return SendResult(False, error="缺少 httpx 依赖")
93
+ if not self.bot_token:
94
+ return SendResult(False, error="未配置 Discord bot token")
95
+ payload: dict[str, Any] = {"content": msg.text}
96
+ if msg.reply_to:
97
+ payload["message_reference"] = {"message_id": msg.reply_to}
98
+ url = f"{DISCORD_API_BASE}/channels/{msg.conversation_id}/messages"
99
+ try:
100
+ with httpx.Client() as client:
101
+ resp = client.post(url, json=payload, headers=self._headers(),
102
+ timeout=self.timeout)
103
+ except Exception as e:
104
+ return SendResult(False, error=f"Discord 发送失败: {e}")
105
+ if resp.status_code == 429:
106
+ return SendResult(False, error="Discord 限流(429),请降低发送频率")
107
+ if resp.status_code >= 400:
108
+ return SendResult(False, error=f"Discord API HTTP {resp.status_code}: {resp.text[:200]}")
109
+ try:
110
+ data = resp.json()
111
+ except Exception:
112
+ data = {}
113
+ return SendResult(True, message_id=str(data.get("id", "")))
114
+
115
+ def verify_token(self) -> SendResult:
116
+ """GET /users/@me 验证 bot token,并记录自己的 user id。"""
117
+ try:
118
+ import httpx
119
+ with httpx.Client() as client:
120
+ resp = client.get(f"{DISCORD_API_BASE}/users/@me",
121
+ headers=self._headers(), timeout=self.timeout)
122
+ if resp.status_code == 200:
123
+ me = resp.json()
124
+ self._bot_user_id = str(me.get("id", ""))
125
+ return SendResult(True, message_id=me.get("username", ""))
126
+ return SendResult(False, error=f"token 无效 (HTTP {resp.status_code})")
127
+ except Exception as e:
128
+ return SendResult(False, error=str(e))
129
+
130
+ def build_identify(self) -> dict:
131
+ """构造 Gateway IDENTIFY 帧(连接后发送)。"""
132
+ return {
133
+ "op": OP_IDENTIFY,
134
+ "d": {
135
+ "token": self.bot_token,
136
+ "intents": self.intents,
137
+ "properties": {"os": "linux", "browser": "foxclaw", "device": "foxclaw"},
138
+ },
139
+ }
140
+
141
+ async def connect_gateway(self):
142
+ """Gateway WebSocket 接收循环(异步)。需要 websockets 库。
143
+
144
+ 实现 HELLO→IDENTIFY→心跳→DISPATCH 的最小闭环。生产可换 discord.py。
145
+ """
146
+ try:
147
+ import websockets
148
+ except ImportError:
149
+ raise RuntimeError("需要 websockets 库:pip install websockets")
150
+ import asyncio
151
+ async with websockets.connect(DISCORD_GATEWAY, max_size=None) as ws:
152
+ hello = json.loads(await ws.recv())
153
+ heartbeat_ms = hello["d"]["heartbeat_interval"]
154
+ await ws.send(json.dumps(self.build_identify()))
155
+ self._running = True
156
+
157
+ async def heartbeat():
158
+ while self._running:
159
+ await asyncio.sleep(heartbeat_ms / 1000)
160
+ try:
161
+ await ws.send(json.dumps({"op": OP_HEARTBEAT, "d": None}))
162
+ except Exception:
163
+ break
164
+
165
+ hb_task = asyncio.create_task(heartbeat())
166
+ try:
167
+ async for raw in ws:
168
+ frame = json.loads(raw)
169
+ if frame.get("op") == OP_DISPATCH and frame.get("t") == "MESSAGE_CREATE":
170
+ inbound = self.normalize(frame)
171
+ if inbound and self.on_message:
172
+ self.on_message(inbound)
173
+ finally:
174
+ self._running = False
175
+ hb_task.cancel()