foxclaw 0.1.0 → 0.1.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (495) hide show
  1. package/README.md +54 -29
  2. package/bin/foxclaw.cjs +51 -0
  3. package/index.js +11 -10
  4. package/package.json +46 -38
  5. package/python/.dockerignore +35 -0
  6. package/python/.env.example +165 -0
  7. package/python/.gitattributes +46 -0
  8. package/python/.github/workflows/ci.yml +122 -0
  9. package/python/.github/workflows/release.yml +183 -0
  10. package/python/Dockerfile +64 -0
  11. package/python/LICENSE +21 -0
  12. package/python/MANIFEST.in +14 -0
  13. package/python/README.md +192 -0
  14. package/python/README_CN.md +324 -0
  15. package/python/bench/bm25_audit.py +199 -0
  16. package/python/bench/bm25_regression.py +178 -0
  17. package/python/bench/bm25_stress.py +159 -0
  18. package/python/bench/cd_audit.py +238 -0
  19. package/python/bench/chaos.py +142 -0
  20. package/python/bench/concurrency.py +103 -0
  21. package/python/bench/e2e_real_models.py +95 -0
  22. package/python/bench/llm_retry_test.py +248 -0
  23. package/python/bench/load_test.py +196 -0
  24. package/python/bench/vector_ivf_upgrade.py +134 -0
  25. package/python/docs/CHANNELS.md +195 -0
  26. package/python/docs/GOVERNANCE.md +164 -0
  27. package/python/docs/RELEASE.md +109 -0
  28. package/python/docs/RUN.md +75 -0
  29. package/python/docs/RUN_CN.md +121 -0
  30. package/python/docs//351/205/215/347/275/256/346/214/207/345/215/227.md +152 -0
  31. package/python/evals/__init__.py +0 -0
  32. package/python/evals/governance/README.md +61 -0
  33. package/python/evals/governance/__init__.py +0 -0
  34. package/python/evals/governance/cases.py +152 -0
  35. package/python/evals/governance/compare_embeddings.py +94 -0
  36. package/python/evals/governance/convergence.svg +11 -0
  37. package/python/evals/governance/report.json +371 -0
  38. package/python/evals/governance/run_evals.py +400 -0
  39. package/python/examples/governance_middleware_demo.py +77 -0
  40. package/python/examples/langgraph_governance_demo.py +108 -0
  41. package/python/examples/sample_itinerary.csv +6 -0
  42. package/python/examples/sample_knowledge.csv +9 -0
  43. package/python/examples/sample_transactions.csv +14 -0
  44. package/python/foxclaw/__init__.py +526 -0
  45. package/python/foxclaw/agents/__init__.py +12 -0
  46. package/python/foxclaw/agents/agents.py +35 -0
  47. package/python/foxclaw/agents/base.py +865 -0
  48. package/python/foxclaw/agents/impl/__init__.py +1 -0
  49. package/python/foxclaw/agents/impl/_helpers.py +78 -0
  50. package/python/foxclaw/agents/impl/finance.py +248 -0
  51. package/python/foxclaw/agents/impl/office.py +367 -0
  52. package/python/foxclaw/agents/impl/supervisor.py +659 -0
  53. package/python/foxclaw/agents/mixins/_context.py +151 -0
  54. package/python/foxclaw/agents/mixins/augment.py +149 -0
  55. package/python/foxclaw/agents/mixins/llm.py +66 -0
  56. package/python/foxclaw/agents/mixins/memory.py +35 -0
  57. package/python/foxclaw/agents/mixins/preference.py +81 -0
  58. package/python/foxclaw/agents/mixins/prompt.py +77 -0
  59. package/python/foxclaw/agents/mixins/skillbox.py +58 -0
  60. package/python/foxclaw/agents/mixins/tool_exec.py +443 -0
  61. package/python/foxclaw/api/__init__.py +283 -0
  62. package/python/foxclaw/api/app.py +123 -0
  63. package/python/foxclaw/api/middleware.py +317 -0
  64. package/python/foxclaw/api/routes/ahs.py +417 -0
  65. package/python/foxclaw/api/routes/attachments.py +117 -0
  66. package/python/foxclaw/api/routes/browser.py +39 -0
  67. package/python/foxclaw/api/routes/domain.py +314 -0
  68. package/python/foxclaw/api/routes/knowledge.py +163 -0
  69. package/python/foxclaw/api/routes/llm_admin.py +461 -0
  70. package/python/foxclaw/api/routes/memory.py +89 -0
  71. package/python/foxclaw/api/routes/orchestration.py +41 -0
  72. package/python/foxclaw/api/routes/recovery.py +43 -0
  73. package/python/foxclaw/api/routes/review.py +275 -0
  74. package/python/foxclaw/api/routes/search.py +37 -0
  75. package/python/foxclaw/api/routes/system.py +327 -0
  76. package/python/foxclaw/api/routes/task.py +281 -0
  77. package/python/foxclaw/api/routes/ws.py +68 -0
  78. package/python/foxclaw/api/spine.py +55 -0
  79. package/python/foxclaw/browser/__init__.py +13 -0
  80. package/python/foxclaw/browser/action/__init__.py +1 -0
  81. package/python/foxclaw/browser/action/action_executor.py +54 -0
  82. package/python/foxclaw/browser/action/action_types.py +45 -0
  83. package/python/foxclaw/browser/action/action_verifier.py +68 -0
  84. package/python/foxclaw/browser/action/url_policy.py +147 -0
  85. package/python/foxclaw/browser/adapter/__init__.py +1 -0
  86. package/python/foxclaw/browser/adapter/base_adapter.py +23 -0
  87. package/python/foxclaw/browser/adapter/web_adapter.py +46 -0
  88. package/python/foxclaw/browser/driver/__init__.py +1 -0
  89. package/python/foxclaw/browser/driver/base_driver.py +61 -0
  90. package/python/foxclaw/browser/driver/playwright_driver.py +166 -0
  91. package/python/foxclaw/browser/perception/__init__.py +1 -0
  92. package/python/foxclaw/browser/perception/dom_extractor.py +43 -0
  93. package/python/foxclaw/browser/perception/element_indexer.py +57 -0
  94. package/python/foxclaw/browser/perception/semantic_reducer.py +33 -0
  95. package/python/foxclaw/browser/perception/snapshot.py +29 -0
  96. package/python/foxclaw/browser/service.py +148 -0
  97. package/python/foxclaw/browser/session/__init__.py +1 -0
  98. package/python/foxclaw/browser/session/session_manager.py +57 -0
  99. package/python/foxclaw/capability/__init__.py +12 -0
  100. package/python/foxclaw/capability/embedding_service.py +67 -0
  101. package/python/foxclaw/channels/__init__.py +140 -0
  102. package/python/foxclaw/channels/discord.py +184 -0
  103. package/python/foxclaw/channels/dispatcher.py +204 -0
  104. package/python/foxclaw/channels/feishu.py +225 -0
  105. package/python/foxclaw/channels/openclaw_bridge.py +156 -0
  106. package/python/foxclaw/channels/signing.py +187 -0
  107. package/python/foxclaw/channels/telegram.py +121 -0
  108. package/python/foxclaw/channels/weixin.py +225 -0
  109. package/python/foxclaw/cli.py +154 -0
  110. package/python/foxclaw/cognitive/__init__.py +4 -0
  111. package/python/foxclaw/cognitive/reasoner.py +82 -0
  112. package/python/foxclaw/cognitive/state.py +147 -0
  113. package/python/foxclaw/core/__init__.py +9 -0
  114. package/python/foxclaw/core/audit_chain.py +133 -0
  115. package/python/foxclaw/core/audit_sink.py +100 -0
  116. package/python/foxclaw/core/compaction.py +283 -0
  117. package/python/foxclaw/core/config.py +65 -0
  118. package/python/foxclaw/core/confirm_nonce.py +157 -0
  119. package/python/foxclaw/core/dispatch.py +27 -0
  120. package/python/foxclaw/core/envfile.py +48 -0
  121. package/python/foxclaw/core/failure_policy.py +151 -0
  122. package/python/foxclaw/core/gateway.py +85 -0
  123. package/python/foxclaw/core/governance_audit.py +175 -0
  124. package/python/foxclaw/core/idempotency.py +289 -0
  125. package/python/foxclaw/core/injection_guard.py +221 -0
  126. package/python/foxclaw/core/memory_adapter.py +516 -0
  127. package/python/foxclaw/core/metrics.py +89 -0
  128. package/python/foxclaw/core/model_router.py +1097 -0
  129. package/python/foxclaw/core/paths.py +103 -0
  130. package/python/foxclaw/core/recovery.py +362 -0
  131. package/python/foxclaw/core/scheduler.py +144 -0
  132. package/python/foxclaw/core/secrets.py +134 -0
  133. package/python/foxclaw/core/sessions.py +157 -0
  134. package/python/foxclaw/core/settings.py +430 -0
  135. package/python/foxclaw/core/shared_write.py +198 -0
  136. package/python/foxclaw/core/steering_queue.py +153 -0
  137. package/python/foxclaw/core/system_bus.py +84 -0
  138. package/python/foxclaw/core/thought_chain.py +318 -0
  139. package/python/foxclaw/core/token_meter.py +235 -0
  140. package/python/foxclaw/core/tools.py +172 -0
  141. package/python/foxclaw/core/tracing.py +390 -0
  142. package/python/foxclaw/core/truth_pipeline.py +114 -0
  143. package/python/foxclaw/core/vec_recall_adapter.py +503 -0
  144. package/python/foxclaw/core/verify.py +181 -0
  145. package/python/foxclaw/core/web_search.py +242 -0
  146. package/python/foxclaw/finance/__init__.py +15 -0
  147. package/python/foxclaw/finance/evidence.py +153 -0
  148. package/python/foxclaw/finance/fx.py +100 -0
  149. package/python/foxclaw/finance/graph.py +188 -0
  150. package/python/foxclaw/finance/models.py +150 -0
  151. package/python/foxclaw/finance/nlparse.py +575 -0
  152. package/python/foxclaw/finance/policy.py +329 -0
  153. package/python/foxclaw/finance/reasoner.py +129 -0
  154. package/python/foxclaw/finance/reasoning.py +340 -0
  155. package/python/foxclaw/finance/reconcile.py +180 -0
  156. package/python/foxclaw/finance/runtime.py +527 -0
  157. package/python/foxclaw/governance/__init__.py +24 -0
  158. package/python/foxclaw/governance/middleware.py +184 -0
  159. package/python/foxclaw/harness/__init__.py +17 -0
  160. package/python/foxclaw/harness/_base.py +72 -0
  161. package/python/foxclaw/harness/_policy.py +359 -0
  162. package/python/foxclaw/harness/drift.py +89 -0
  163. package/python/foxclaw/harness/event_bus.py +31 -0
  164. package/python/foxclaw/harness/harness.py +446 -0
  165. package/python/foxclaw/harness/loop_detection.py +152 -0
  166. package/python/foxclaw/itinerary/__init__.py +10 -0
  167. package/python/foxclaw/itinerary/amap_client.py +117 -0
  168. package/python/foxclaw/itinerary/models.py +80 -0
  169. package/python/foxclaw/itinerary/reasoner.py +101 -0
  170. package/python/foxclaw/itinerary/reasoning.py +174 -0
  171. package/python/foxclaw/itinerary/runtime.py +148 -0
  172. package/python/foxclaw/kernel/__init__.py +13 -0
  173. package/python/foxclaw/kernel/capabilities.py +73 -0
  174. package/python/foxclaw/kernel/policy_gate.py +114 -0
  175. package/python/foxclaw/kernel/process_manager.py +205 -0
  176. package/python/foxclaw/kernel/scheduler.py +131 -0
  177. package/python/foxclaw/kernel/sentinel.py +453 -0
  178. package/python/foxclaw/kernel/sentinel_client.py +246 -0
  179. package/python/foxclaw/kernel/spine.py +97 -0
  180. package/python/foxclaw/knowledge/__init__.py +376 -0
  181. package/python/foxclaw/knowledge/truth_crystallizer.py +92 -0
  182. package/python/foxclaw/learning/__init__.py +12 -0
  183. package/python/foxclaw/learning/knowledge_graph.py +149 -0
  184. package/python/foxclaw/learning/models.py +93 -0
  185. package/python/foxclaw/learning/reasoner.py +70 -0
  186. package/python/foxclaw/learning/runtime.py +363 -0
  187. package/python/foxclaw/learning/spaced_repetition.py +96 -0
  188. package/python/foxclaw/mcp_server.py +189 -0
  189. package/python/foxclaw/orchestration/__init__.py +184 -0
  190. package/python/foxclaw/pipeline/__init__.py +23 -0
  191. package/python/foxclaw/pipeline/cognitive_pipeline.py +481 -0
  192. package/python/foxclaw/pipeline/supervisor_stages.py +230 -0
  193. package/python/foxclaw/planning/__init__.py +17 -0
  194. package/python/foxclaw/planning/llm_planner.py +232 -0
  195. package/python/foxclaw/planning/planner.py +99 -0
  196. package/python/foxclaw/search/__init__.py +24 -0
  197. package/python/foxclaw/search/agent_port.py +194 -0
  198. package/python/foxclaw/search/evidence.py +113 -0
  199. package/python/foxclaw/search/external_search.py +50 -0
  200. package/python/foxclaw/search/hkc_read_port.py +60 -0
  201. package/python/foxclaw/search/internal_search.py +73 -0
  202. package/python/foxclaw/search/models.py +129 -0
  203. package/python/foxclaw/search/planner.py +44 -0
  204. package/python/foxclaw/search/retriever.py +39 -0
  205. package/python/foxclaw/search/runtime.py +134 -0
  206. package/python/foxclaw/search/strategy.py +44 -0
  207. package/python/foxclaw/skills/__init__.py +613 -0
  208. package/python/foxclaw/skills/hooks_spec.py +73 -0
  209. package/python/foxclaw/skills/lifecycle.py +278 -0
  210. package/python/foxclaw/skills/markdown_skill.py +174 -0
  211. package/python/foxclaw/skills/markdown_store.py +39 -0
  212. package/python/foxclaw/skills/mcp_client.py +190 -0
  213. package/python/foxclaw/skills/mcp_store.py +43 -0
  214. package/python/foxclaw/skills/permission.py +180 -0
  215. package/python/foxclaw/skills/pipeline.py +386 -0
  216. package/python/foxclaw/skills/policy.py +80 -0
  217. package/python/foxclaw/skills/review.py +72 -0
  218. package/python/foxclaw/skills/store.py +57 -0
  219. package/python/frontend/assets/core_bg.jpg +0 -0
  220. package/python/frontend/deck/auth.js +32 -0
  221. package/python/frontend/deck/chat_model_picker.js +537 -0
  222. package/python/frontend/deck/domain.js +296 -0
  223. package/python/frontend/deck/main.js +1950 -0
  224. package/python/frontend/deck/markdown.js +48 -0
  225. package/python/frontend/deck/settings.js +324 -0
  226. package/python/frontend/deck/state.js +6 -0
  227. package/python/frontend/deck/util.js +6 -0
  228. package/python/frontend/deck/visuals.js +212 -0
  229. package/python/frontend/deck.css +855 -0
  230. package/python/frontend/deck.html +686 -0
  231. package/python/frontend/deck_v2.html +341 -0
  232. package/python/frontend/planet/index.html +266 -0
  233. package/python/frontend/planet/planet.css +579 -0
  234. package/python/frontend/planet/planet.js +1637 -0
  235. package/python/frontend/planet/vendor/three.core.min.js +6 -0
  236. package/python/frontend/planet/vendor/three.module.min.js +6 -0
  237. package/python/frontend/v3/assets/index-BD7VPbMV.js +33 -0
  238. package/python/frontend/v3/assets/index-CAvnYWGH.css +1 -0
  239. package/python/frontend/v3/index.html +15 -0
  240. package/python/frontend/widget_runtime.js +295 -0
  241. package/python/frontend/widgets.js +696 -0
  242. package/python/frontend_src/index.html +14 -0
  243. package/python/frontend_src/package-lock.json +1376 -0
  244. package/python/frontend_src/package.json +19 -0
  245. package/python/frontend_src/src/App.vue +62 -0
  246. package/python/frontend_src/src/api.js +89 -0
  247. package/python/frontend_src/src/components/ChatDock.vue +377 -0
  248. package/python/frontend_src/src/components/ComposerMenu.vue +75 -0
  249. package/python/frontend_src/src/components/DocViewer.vue +171 -0
  250. package/python/frontend_src/src/components/Inspector.vue +241 -0
  251. package/python/frontend_src/src/components/LeftRail.vue +394 -0
  252. package/python/frontend_src/src/components/MenuPopover.vue +130 -0
  253. package/python/frontend_src/src/components/MessageStream.vue +401 -0
  254. package/python/frontend_src/src/components/ModelPicker.vue +355 -0
  255. package/python/frontend_src/src/components/QuickIngest.vue +118 -0
  256. package/python/frontend_src/src/components/Stage.vue +226 -0
  257. package/python/frontend_src/src/components/ToolMenu.vue +144 -0
  258. package/python/frontend_src/src/components/TopBar.vue +90 -0
  259. package/python/frontend_src/src/components/panels/ActivityPanel.vue +84 -0
  260. package/python/frontend_src/src/components/panels/AgentsPanel.vue +86 -0
  261. package/python/frontend_src/src/components/panels/KnowledgePanel.vue +441 -0
  262. package/python/frontend_src/src/components/panels/MemoryPanel.vue +118 -0
  263. package/python/frontend_src/src/components/panels/PanelBox.vue +51 -0
  264. package/python/frontend_src/src/components/panels/ReviewPanel.vue +308 -0
  265. package/python/frontend_src/src/components/panels/SettingsPanel.vue +254 -0
  266. package/python/frontend_src/src/components/panels/TasksPanel.vue +74 -0
  267. package/python/frontend_src/src/components/panels/ToolsPanel.vue +640 -0
  268. package/python/frontend_src/src/components/panels/UsagePanel.vue +105 -0
  269. package/python/frontend_src/src/lib/fmt.js +48 -0
  270. package/python/frontend_src/src/lib/md.js +204 -0
  271. package/python/frontend_src/src/main.js +7 -0
  272. package/python/frontend_src/src/stores/app.js +106 -0
  273. package/python/frontend_src/src/stores/chat.js +484 -0
  274. package/python/frontend_src/src/stores/model.js +104 -0
  275. package/python/frontend_src/src/stores/panels.js +330 -0
  276. package/python/frontend_src/src/styles/base.css +138 -0
  277. package/python/frontend_src/src/styles/panel.css +117 -0
  278. package/python/frontend_src/src/styles/tokens.css +78 -0
  279. package/python/frontend_src/vite.config.js +46 -0
  280. package/python/pyproject.toml +265 -0
  281. package/python/skills/README.md +123 -0
  282. package/python/skills/execution-contract/SKILL.md +30 -0
  283. package/python/skills/general-assistant/SKILL.md +34 -0
  284. package/python/skills/planner/SKILL.md +33 -0
  285. package/python/skills/schedule-itinerary/SKILL.md +35 -0
  286. package/python/skills/skill-composition/SKILL.md +31 -0
  287. package/python/skills/skill-guard/SKILL.md +67 -0
  288. package/python/skills/skill-memory/SKILL.md +50 -0
  289. package/python/skills/skill-router/SKILL.md +35 -0
  290. package/python/tests/conftest.py +76 -0
  291. package/python/tests/run_all.py +263 -0
  292. package/python/tests/test_agent_lifecycle.py +360 -0
  293. package/python/tests/test_ahs_p2.py +414 -0
  294. package/python/tests/test_anthropic_tools.py +206 -0
  295. package/python/tests/test_api.py +87 -0
  296. package/python/tests/test_api_auth.py +83 -0
  297. package/python/tests/test_api_host_guard.py +69 -0
  298. package/python/tests/test_attachments.py +167 -0
  299. package/python/tests/test_audit_chain.py +121 -0
  300. package/python/tests/test_audit_sink.py +136 -0
  301. package/python/tests/test_browser.py +153 -0
  302. package/python/tests/test_browser_request_guard.py +184 -0
  303. package/python/tests/test_capabilities.py +158 -0
  304. package/python/tests/test_channel_signing.py +192 -0
  305. package/python/tests/test_channel_trust.py +109 -0
  306. package/python/tests/test_channels.py +184 -0
  307. package/python/tests/test_cli.py +46 -0
  308. package/python/tests/test_compaction_toolpair.py +119 -0
  309. package/python/tests/test_config_layering.py +65 -0
  310. package/python/tests/test_confirm_nonce.py +197 -0
  311. package/python/tests/test_context_compaction_wiring.py +130 -0
  312. package/python/tests/test_dispatch.py +95 -0
  313. package/python/tests/test_domain_evals.py +403 -0
  314. package/python/tests/test_domain_import.py +78 -0
  315. package/python/tests/test_domain_reasoners.py +148 -0
  316. package/python/tests/test_e2e.py +178 -0
  317. package/python/tests/test_emb_cache.py +107 -0
  318. package/python/tests/test_embedding_singleton.py +89 -0
  319. package/python/tests/test_envfile.py +83 -0
  320. package/python/tests/test_evals_smoke.py +114 -0
  321. package/python/tests/test_event_spine.py +182 -0
  322. package/python/tests/test_evidence_semantic.py +101 -0
  323. package/python/tests/test_evidence_sse.py +97 -0
  324. package/python/tests/test_file_ingest.py +146 -0
  325. package/python/tests/test_finance.py +153 -0
  326. package/python/tests/test_finance_ledger.py +252 -0
  327. package/python/tests/test_finance_persist.py +178 -0
  328. package/python/tests/test_gateway.py +144 -0
  329. package/python/tests/test_governance.py +468 -0
  330. package/python/tests/test_governance_audit.py +359 -0
  331. package/python/tests/test_governance_middleware.py +143 -0
  332. package/python/tests/test_hkc.py +286 -0
  333. package/python/tests/test_hkc_index_build_manager.py +339 -0
  334. package/python/tests/test_hkc_manifest_reuse.py +317 -0
  335. package/python/tests/test_hkc_search_event_sync.py +533 -0
  336. package/python/tests/test_import_path_guard.py +161 -0
  337. package/python/tests/test_injection_guard.py +211 -0
  338. package/python/tests/test_integration.py +141 -0
  339. package/python/tests/test_itinerary.py +103 -0
  340. package/python/tests/test_kee_semantic.py +89 -0
  341. package/python/tests/test_kernel.py +222 -0
  342. package/python/tests/test_learning.py +110 -0
  343. package/python/tests/test_llm_admin.py +160 -0
  344. package/python/tests/test_llm_planner.py +366 -0
  345. package/python/tests/test_markdown_skills.py +73 -0
  346. package/python/tests/test_markdown_skills_openclaw.py +71 -0
  347. package/python/tests/test_mcp_connectors.py +109 -0
  348. package/python/tests/test_mcp_guardian_discovery.py +91 -0
  349. package/python/tests/test_mcp_no_memory.py +43 -0
  350. package/python/tests/test_mcp_policy_gate.py +77 -0
  351. package/python/tests/test_mcp_server.py +95 -0
  352. package/python/tests/test_memory_recall.py +95 -0
  353. package/python/tests/test_metrics.py +66 -0
  354. package/python/tests/test_multi_chain.py +102 -0
  355. package/python/tests/test_multimodal.py +135 -0
  356. package/python/tests/test_negotiation.py +146 -0
  357. package/python/tests/test_orchestration.py +119 -0
  358. package/python/tests/test_orchestration_decoupled.py +109 -0
  359. package/python/tests/test_p3_p5_hardening.py +338 -0
  360. package/python/tests/test_parallel_dispatch.py +215 -0
  361. package/python/tests/test_pipeline.py +533 -0
  362. package/python/tests/test_plan_visible.py +86 -0
  363. package/python/tests/test_planner.py +95 -0
  364. package/python/tests/test_policy_gate.py +109 -0
  365. package/python/tests/test_reasoner.py +185 -0
  366. package/python/tests/test_release_check.py +263 -0
  367. package/python/tests/test_resume_checkpoint.py +156 -0
  368. package/python/tests/test_routing.py +95 -0
  369. package/python/tests/test_scheduler.py +141 -0
  370. package/python/tests/test_search.py +204 -0
  371. package/python/tests/test_secrets.py +159 -0
  372. package/python/tests/test_semantic.py +78 -0
  373. package/python/tests/test_sessions.py +91 -0
  374. package/python/tests/test_settings.py +281 -0
  375. package/python/tests/test_shadow_learning.py +330 -0
  376. package/python/tests/test_shared_write.py +207 -0
  377. package/python/tests/test_skill_taint_gate.py +154 -0
  378. package/python/tests/test_skills.py +133 -0
  379. package/python/tests/test_stats_and_events.py +165 -0
  380. package/python/tests/test_streaming.py +60 -0
  381. package/python/tests/test_supervisor_pipeline.py +177 -0
  382. package/python/tests/test_supervisor_triage.py +108 -0
  383. package/python/tests/test_taint.py +117 -0
  384. package/python/tests/test_task_stream.py +85 -0
  385. package/python/tests/test_thinking_sse.py +67 -0
  386. package/python/tests/test_thinking_stream.py +76 -0
  387. package/python/tests/test_tool_center.py +129 -0
  388. package/python/tests/test_tool_parallel.py +113 -0
  389. package/python/tests/test_tool_params.py +103 -0
  390. package/python/tests/test_tool_use.py +242 -0
  391. package/python/tests/test_toplevel_concurrency.py +117 -0
  392. package/python/tests/test_tracing.py +290 -0
  393. package/python/tests/test_truth_crystallizer.py +127 -0
  394. package/python/tests/test_truth_flow.py +93 -0
  395. package/python/tests/test_url_policy.py +53 -0
  396. package/python/tests/test_verify_evidence.py +225 -0
  397. package/python/tests/test_version_sync.py +71 -0
  398. package/python/tests/test_web_trigger.py +99 -0
  399. package/python/tools/check_faiss_type.py +31 -0
  400. package/python/tools/check_log.ps1 +46 -0
  401. package/python/tools/ivf_status.py +72 -0
  402. package/python/tools/mypy_baseline.txt +5 -0
  403. package/python/tools/mypy_ratchet.py +97 -0
  404. package/python/tools/pack.py +178 -0
  405. package/python/tools/release_check.py +349 -0
  406. package/python/tools/restart_foxclaw.ps1 +74 -0
  407. package/python/tools/shared_write_audit.py +204 -0
  408. package/python/tools/size_baseline.txt +28 -0
  409. package/python/tools/size_check.py +102 -0
  410. package/python/tools/verify_env.bat +2 -0
  411. package/python/tools/verify_env.py +25 -0
  412. package/python/vendor/HMR_LICENSE +37 -0
  413. package/python/vendor/HMR_README.md +65 -0
  414. package/python/vendor/hkc/LICENSE +37 -0
  415. package/python/vendor/hkc/hkc_ace/__init__.py +0 -0
  416. package/python/vendor/hkc/hkc_ace/ace.py +457 -0
  417. package/python/vendor/hkc/hkc_ace/skill_taxonomy.json +465 -0
  418. package/python/vendor/hkc/hkc_api/__init__.py +0 -0
  419. package/python/vendor/hkc/hkc_api/container.py +416 -0
  420. package/python/vendor/hkc/hkc_api/ingest_jobs.py +103 -0
  421. package/python/vendor/hkc/hkc_api/main.py +157 -0
  422. package/python/vendor/hkc/hkc_api/routes/__init__.py +0 -0
  423. package/python/vendor/hkc/hkc_api/routes/ability.py +151 -0
  424. package/python/vendor/hkc/hkc_api/routes/config.py +51 -0
  425. package/python/vendor/hkc/hkc_api/routes/knowledge.py +630 -0
  426. package/python/vendor/hkc/hkc_api/routes/search.py +88 -0
  427. package/python/vendor/hkc/hkc_api/schemas.py +153 -0
  428. package/python/vendor/hkc/hkc_api/synthesis.py +179 -0
  429. package/python/vendor/hkc/hkc_core/__init__.py +0 -0
  430. package/python/vendor/hkc/hkc_core/graph/__init__.py +0 -0
  431. package/python/vendor/hkc/hkc_core/graph/base.py +188 -0
  432. package/python/vendor/hkc/hkc_core/graph/sqlite_store.py +789 -0
  433. package/python/vendor/hkc/hkc_core/models/__init__.py +0 -0
  434. package/python/vendor/hkc/hkc_core/models/enums.py +65 -0
  435. package/python/vendor/hkc/hkc_core/models/ku.py +307 -0
  436. package/python/vendor/hkc/hkc_core/utils/__init__.py +0 -0
  437. package/python/vendor/hkc/hkc_core/utils/id_gen.py +113 -0
  438. package/python/vendor/hkc/hkc_crystallizer/__init__.py +44 -0
  439. package/python/vendor/hkc/hkc_crystallizer/adapters.py +94 -0
  440. package/python/vendor/hkc/hkc_crystallizer/candidate.py +145 -0
  441. package/python/vendor/hkc/hkc_crystallizer/crystallizer.py +179 -0
  442. package/python/vendor/hkc/hkc_crystallizer/ingress.py +90 -0
  443. package/python/vendor/hkc/hkc_kde/__init__.py +0 -0
  444. package/python/vendor/hkc/hkc_kde/chunker.py +197 -0
  445. package/python/vendor/hkc/hkc_kde/extractor.py +623 -0
  446. package/python/vendor/hkc/hkc_kde/kde.py +298 -0
  447. package/python/vendor/hkc/hkc_kde/loaders/__init__.py +0 -0
  448. package/python/vendor/hkc/hkc_kde/loaders/ebook_loader.py +153 -0
  449. package/python/vendor/hkc/hkc_kde/loaders/office_loader.py +194 -0
  450. package/python/vendor/hkc/hkc_kde/loaders/pdf_loader.py +162 -0
  451. package/python/vendor/hkc/hkc_kde/loaders/text_loader.py +157 -0
  452. package/python/vendor/hkc/hkc_kde/loaders/web_loader.py +91 -0
  453. package/python/vendor/hkc/hkc_kde/models.py +74 -0
  454. package/python/vendor/hkc/hkc_kde/packager.py +329 -0
  455. package/python/vendor/hkc/hkc_kde/relation_disc.py +151 -0
  456. package/python/vendor/hkc/hkc_kee/__init__.py +0 -0
  457. package/python/vendor/hkc/hkc_kee/dedup.py +227 -0
  458. package/python/vendor/hkc/hkc_kee/fingerprint.py +106 -0
  459. package/python/vendor/hkc/hkc_kee/kee.py +541 -0
  460. package/python/vendor/hkc/hkc_kep/__init__.py +0 -0
  461. package/python/vendor/hkc/hkc_kep/event_bus.py +205 -0
  462. package/python/vendor/hkc/hkc_sdk/__init__.py +63 -0
  463. package/python/vendor/hkc/hkc_sdk/base.py +151 -0
  464. package/python/vendor/hkc/hkc_sdk/direct_client.py +153 -0
  465. package/python/vendor/hkc/hkc_sdk/exceptions.py +39 -0
  466. package/python/vendor/hkc/hkc_sdk/http_client.py +166 -0
  467. package/python/vendor/hkc/hkc_sdk/models.py +171 -0
  468. package/python/vendor/hkc/hkc_search/__init__.py +0 -0
  469. package/python/vendor/hkc/hkc_search/bm25.py +325 -0
  470. package/python/vendor/hkc/hkc_search/embedding_backends.py +374 -0
  471. package/python/vendor/hkc/hkc_search/graph_search.py +132 -0
  472. package/python/vendor/hkc/hkc_search/hybrid.py +331 -0
  473. package/python/vendor/hkc/hkc_search/index_build_manager.py +378 -0
  474. package/python/vendor/hkc/hkc_search/vector_search.py +484 -0
  475. package/python/vendor/hmr/__init__.py +0 -0
  476. package/python/vendor/hmr/core/__init__.py +0 -0
  477. package/python/vendor/hmr/core/hmr.py +684 -0
  478. package/python/vendor/hmr/core/models.py +310 -0
  479. package/python/vendor/hmr/engines/__init__.py +0 -0
  480. package/python/vendor/hmr/engines/evolution.py +710 -0
  481. package/python/vendor/hmr/engines/jit_compiler.py +472 -0
  482. package/python/vendor/hmr/engines/lifecycle.py +264 -0
  483. package/python/vendor/hmr/engines/policy.py +625 -0
  484. package/python/vendor/hmr/engines/recall.py +260 -0
  485. package/python/vendor/hmr/engines/runtime_state.py +158 -0
  486. package/python/vendor/hmr/engines/scheduler.py +323 -0
  487. package/python/vendor/hmr/engines/semantic.py +150 -0
  488. package/python/vendor/hmr/engines/temporal.py +261 -0
  489. package/python/vendor/hmr/engines/thought_chain.py +635 -0
  490. package/python/vendor/hmr/graph/__init__.py +0 -0
  491. package/python/vendor/hmr/graph/cwg.py +338 -0
  492. package/python/vendor/hmr/graph/memory_graph.py +530 -0
  493. package/python/vendor/hmr/storage/__init__.py +0 -0
  494. package/python/vendor/hmr/storage/memory_fs.py +300 -0
  495. package/python/vendor/hmr/storage/vector_store.py +520 -0
@@ -0,0 +1,148 @@
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
27
+
28
+
29
+ class BrowserService:
30
+
31
+ def __init__(self, headless: bool = True):
32
+ self._headless = headless
33
+ self._session = None
34
+ self._executor: Optional[ThreadPoolExecutor] = None
35
+ self._closed = False
36
+ self._launch_error: Optional[str] = None
37
+ # 保护 executor/session 的创建与关闭(仅协调,不进浏览器调用)
38
+ self._guard = threading.Lock()
39
+
40
+ # ── 能力探测 ────────────────────────────────────────────────
41
+ @staticmethod
42
+ def available() -> bool:
43
+ """playwright 包是否可导入。注意:这不保证 chromium 内核已 install,
44
+ 内核缺失会在首次 launch 时暴露,由 _launch_error 记录并透出。"""
45
+ return importlib.util.find_spec("playwright") is not None
46
+
47
+ def status(self) -> dict:
48
+ return {
49
+ "available": self.available(),
50
+ "headless": self._headless,
51
+ "session_active": self._session is not None,
52
+ "closed": self._closed,
53
+ "launch_error": self._launch_error,
54
+ }
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 API ─────────────────────────────────────────
93
+ async def perceive(self) -> dict:
94
+ """只感知当前页面语义结构,不改变页面状态。"""
95
+ if not self.available():
96
+ return self._unavailable()
97
+ try:
98
+ return {"ok": True, "semantic_state": await self._run(self._perceive_sync)}
99
+ except Exception as e:
100
+ return self._handle_error(e)
101
+
102
+ async def act(self, action: dict) -> dict:
103
+ """执行一个 Web 动作(navigate/click/type/scroll/extract/wait),返回校验后最新语义结构。"""
104
+ if not self.available():
105
+ return self._unavailable()
106
+ if not isinstance(action, dict) or "type" not in action:
107
+ return {"ok": False, "error": "非法动作:需 {type, target?, value?}"}
108
+ try:
109
+ return {"ok": True, **await self._run(self._act_sync, action)}
110
+ except ValueError as e: # 不支持的动作类型 / 非法编号
111
+ return {"ok": False, "error": str(e)}
112
+ except KeyError as e: # 元素编号过期
113
+ return {"ok": False, "error": str(e).strip('"')}
114
+ except Exception as e:
115
+ return self._handle_error(e)
116
+
117
+ async def close(self):
118
+ """释放浏览器会话并关停专用工作线程(幂等)。挂到 Foxclaw teardown。"""
119
+ with self._guard:
120
+ if self._closed:
121
+ return
122
+ self._closed = True
123
+ executor = self._executor
124
+ if executor is not None:
125
+ try:
126
+ # 会话的 close 必须回到工作线程执行(Playwright 对象线程亲和)
127
+ loop = asyncio.get_running_loop()
128
+ await loop.run_in_executor(executor, self._close_sync)
129
+ finally:
130
+ executor.shutdown(wait=True)
131
+
132
+ # ── 错误/降级处理 ──────────────────────────────────────────
133
+ def _unavailable(self) -> dict:
134
+ return {
135
+ "ok": False,
136
+ "available": False,
137
+ "error": "浏览器子系统不可用:未安装 playwright(可选依赖)。"
138
+ "安装:pip install 'foxclaw[browser]' 且 playwright install chromium",
139
+ }
140
+
141
+ def _handle_error(self, e: Exception) -> dict:
142
+ msg = str(e)
143
+ # chromium 内核未安装是最常见的 launch 失败,给出可操作提示
144
+ if "playwright install" in msg or "Executable doesn't exist" in msg:
145
+ self._launch_error = msg
146
+ return {"ok": False, "available": False,
147
+ "error": "playwright 已装但缺 chromium 内核,请运行:playwright install chromium"}
148
+ 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,67 @@
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] = {} # model_name -> model 实例 或 None(加载失败/缺库)
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: # 本服务缓存命中(不重复走 HKC):绝不重复加载
46
+ return _cache[name]
47
+ _load_attempts += 1
48
+ model = None
49
+ try:
50
+ # 【P2·share-bge-m3】走 HKC 共享进程缓存 —— 同名 model 全 Foxclaw 只 load 1 次
51
+ # (HKC 检索 / HMR VectorStore / Harness SemanticDrift 共用同一份 instance)
52
+ from hkc_search.embedding_backends import get_st_model as _hk_get
53
+ model = _hk_get(name)
54
+ except Exception:
55
+ model = None # HKC 不可用 / 缺库 / 离线 → 降级哈希嵌入
56
+ _cache[name] = model
57
+ return model
58
+
59
+
60
+ def embedding_stats() -> dict:
61
+ """可观测:当前缓存了哪些模型、发起过多少次加载。用于证明"同名只加载一次"。"""
62
+ with _lock:
63
+ return {
64
+ "cached_models": {n: (m is not None) for n, m in _cache.items()},
65
+ "distinct_models": len(_cache),
66
+ "load_attempts": _load_attempts,
67
+ }
@@ -0,0 +1,140 @@
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 # 来源渠道(telegram/slack/...)
24
+ message_id: str
25
+ text: str
26
+ sender_id: str # 发送者唯一 ID(平台内)
27
+ sender_name: str = ""
28
+ conversation_id: str = "" # 会话 ID(群=群ID,私聊=对方ID)
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
+ # 子类覆盖:渠道名
67
+ CHANNEL = "base"
68
+
69
+ def __init__(self, on_message: Callable[[InboundMessage], None] = None):
70
+ self.on_message = on_message
71
+ self._running = False
72
+ self.last_error = "" # 最近一次处理错误(便于排查,不抛断轮询)
73
+
74
+ @abstractmethod
75
+ def normalize(self, raw: dict) -> Optional[InboundMessage]:
76
+ """把平台原始消息转成标准 InboundMessage。非消息事件返回 None。"""
77
+ ...
78
+
79
+ @abstractmethod
80
+ def send(self, msg: OutboundMessage) -> SendResult:
81
+ """把标准 OutboundMessage 通过平台官方 API 发出。"""
82
+ ...
83
+
84
+ def handle_raw(self, raw: dict):
85
+ """收到平台原始消息时调用:标准化 → 触发 on_message 回调。
86
+
87
+ 全程异常隔离:单条消息处理失败不会抛出(否则会中断整批轮询,
88
+ 且已推进的游标不回滚 → 后续消息全部丢失)。失败记录在 last_error。
89
+ """
90
+ try:
91
+ inbound = self.normalize(raw)
92
+ except Exception as e:
93
+ self.last_error = f"normalize 失败: {e}"
94
+ return None
95
+ if inbound is None:
96
+ return None # 非消息事件(如已读回执),忽略
97
+ if self.on_message:
98
+ try:
99
+ self.on_message(inbound)
100
+ except Exception as e:
101
+ # on_message(主管处理)失败:记录但不抛,保证整批轮询不中断
102
+ self.last_error = f"on_message 失败: {e}"
103
+ return inbound
104
+
105
+ def reply(self, inbound: InboundMessage, text: str) -> SendResult:
106
+ """便捷回复:回到消息来源的会话。"""
107
+ return self.send(OutboundMessage(
108
+ conversation_id=inbound.conversation_id, text=text,
109
+ reply_to=inbound.message_id))
110
+
111
+ @property
112
+ def running(self) -> bool:
113
+ return self._running
114
+
115
+
116
+ class ChannelRegistry:
117
+ """渠道注册表。管理多个已接入的适配器,统一分派。"""
118
+
119
+ def __init__(self):
120
+ self._adapters: dict[str, ChannelAdapter] = {}
121
+
122
+ def register(self, adapter: ChannelAdapter):
123
+ self._adapters[adapter.CHANNEL] = adapter
124
+ return adapter
125
+
126
+ def get(self, channel: str) -> Optional[ChannelAdapter]:
127
+ return self._adapters.get(channel)
128
+
129
+ def all(self) -> list:
130
+ return list(self._adapters.values())
131
+
132
+ def send(self, channel: str, msg: OutboundMessage) -> SendResult:
133
+ adapter = self.get(channel)
134
+ if adapter is None:
135
+ return SendResult(False, error=f"渠道未接入: {channel}")
136
+ return adapter.send(msg)
137
+
138
+ def status(self) -> dict:
139
+ return {ch: {"running": a.running, "channel": a.CHANNEL}
140
+ for ch, a in self._adapters.items()}
@@ -0,0 +1,184 @@
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 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
+ # Gateway opcodes
28
+ OP_DISPATCH = 0
29
+ OP_HEARTBEAT = 1
30
+ OP_IDENTIFY = 2
31
+ OP_HELLO = 10
32
+ OP_HEARTBEAT_ACK = 11
33
+
34
+ # Intents(收消息内容需要 GUILD_MESSAGES + MESSAGE_CONTENT)
35
+ INTENT_GUILD_MESSAGES = 1 << 9
36
+ INTENT_DIRECT_MESSAGES = 1 << 12
37
+ INTENT_MESSAGE_CONTENT = 1 << 15
38
+ DEFAULT_INTENTS = INTENT_GUILD_MESSAGES | INTENT_DIRECT_MESSAGES | INTENT_MESSAGE_CONTENT
39
+
40
+
41
+ class DiscordAdapter(ChannelAdapter):
42
+ CHANNEL = "discord"
43
+
44
+ def __init__(self, bot_token: str, intents: int = DEFAULT_INTENTS,
45
+ on_message=None, timeout: float = 30.0):
46
+ super().__init__(on_message=on_message)
47
+ self.bot_token = bot_token
48
+ self.intents = intents
49
+ self.timeout = timeout
50
+ self._bot_user_id = "" # 自己的 bot user id(用于忽略自己的消息)
51
+
52
+ def _headers(self) -> dict:
53
+ return {"Authorization": f"Bot {self.bot_token}",
54
+ "Content-Type": "application/json",
55
+ "User-Agent": "Foxclaw (https://github.com, 1.0)"}
56
+
57
+ # ── 入站:标准化 Gateway MESSAGE_CREATE 事件 ───────────
58
+ def normalize(self, raw: dict) -> Optional[InboundMessage]:
59
+ """Discord Gateway 事件 → 标准 InboundMessage。
60
+
61
+ raw 可以是完整 Gateway 帧({op,t,d})或直接是 MESSAGE_CREATE 的 data。
62
+ """
63
+ # 取出 data 部分
64
+ if "op" in raw:
65
+ if raw.get("op") != OP_DISPATCH or raw.get("t") != "MESSAGE_CREATE":
66
+ return None
67
+ data = raw.get("d", {})
68
+ else:
69
+ data = raw
70
+ if not data:
71
+ return None
72
+ author = data.get("author", {})
73
+ # 忽略机器人消息(含自己),避免自循环
74
+ if author.get("bot"):
75
+ return None
76
+ if self._bot_user_id and str(author.get("id")) == self._bot_user_id:
77
+ return None
78
+ text = data.get("content", "")
79
+ if not text:
80
+ return None # 非文本(纯附件/嵌入)暂忽略
81
+ # 有 guild_id = 服务器频道(群),无 = 私信
82
+ is_group = bool(data.get("guild_id"))
83
+ return InboundMessage(
84
+ channel=self.CHANNEL,
85
+ message_id=str(data.get("id", "")),
86
+ text=text,
87
+ sender_id=str(author.get("id", "")),
88
+ sender_name=author.get("username", ""),
89
+ conversation_id=str(data.get("channel_id", "")),
90
+ is_group=is_group,
91
+ raw=data)
92
+
93
+ # ── 出站:REST 发送 ────────────────────────────────────
94
+ def send(self, msg: OutboundMessage) -> SendResult:
95
+ """POST /channels/{channel_id}/messages 发文本。"""
96
+ try:
97
+ import httpx
98
+ except ImportError:
99
+ return SendResult(False, error="缺少 httpx 依赖")
100
+ if not self.bot_token:
101
+ return SendResult(False, error="未配置 Discord bot token")
102
+ payload = {"content": msg.text}
103
+ if msg.reply_to:
104
+ payload["message_reference"] = {"message_id": msg.reply_to}
105
+ url = f"{DISCORD_API_BASE}/channels/{msg.conversation_id}/messages"
106
+ try:
107
+ with httpx.Client() as client:
108
+ resp = client.post(url, json=payload, headers=self._headers(),
109
+ timeout=self.timeout)
110
+ except Exception as e:
111
+ return SendResult(False, error=f"Discord 发送失败: {e}")
112
+ if resp.status_code == 429:
113
+ return SendResult(False, error="Discord 限流(429),请降低发送频率")
114
+ if resp.status_code >= 400:
115
+ return SendResult(False, error=f"Discord API HTTP {resp.status_code}: {resp.text[:200]}")
116
+ try:
117
+ data = resp.json()
118
+ except Exception:
119
+ data = {}
120
+ return SendResult(True, message_id=str(data.get("id", "")))
121
+
122
+ def verify_token(self) -> SendResult:
123
+ """GET /users/@me 验证 bot token,并记录自己的 user id。"""
124
+ try:
125
+ import httpx
126
+ with httpx.Client() as client:
127
+ resp = client.get(f"{DISCORD_API_BASE}/users/@me",
128
+ headers=self._headers(), timeout=self.timeout)
129
+ if resp.status_code == 200:
130
+ me = resp.json()
131
+ self._bot_user_id = str(me.get("id", ""))
132
+ return SendResult(True, message_id=me.get("username", ""))
133
+ return SendResult(False, error=f"token 无效 (HTTP {resp.status_code})")
134
+ except Exception as e:
135
+ return SendResult(False, error=str(e))
136
+
137
+ def build_identify(self) -> dict:
138
+ """构造 Gateway IDENTIFY 帧(连接后发送)。"""
139
+ return {
140
+ "op": OP_IDENTIFY,
141
+ "d": {
142
+ "token": self.bot_token,
143
+ "intents": self.intents,
144
+ "properties": {"os": "linux", "browser": "foxclaw", "device": "foxclaw"},
145
+ },
146
+ }
147
+
148
+ async def connect_gateway(self):
149
+ """Gateway WebSocket 接收循环(异步)。需要 websockets 库。
150
+
151
+ 实现 HELLO→IDENTIFY→心跳→DISPATCH 的最小闭环。生产可换 discord.py。
152
+ """
153
+ try:
154
+ import websockets
155
+ except ImportError:
156
+ raise RuntimeError("需要 websockets 库:pip install websockets")
157
+ import asyncio
158
+ async with websockets.connect(DISCORD_GATEWAY, max_size=None) as ws:
159
+ # 1. HELLO:拿心跳间隔
160
+ hello = json.loads(await ws.recv())
161
+ heartbeat_ms = hello["d"]["heartbeat_interval"]
162
+ # 2. IDENTIFY
163
+ await ws.send(json.dumps(self.build_identify()))
164
+ self._running = True
165
+
166
+ async def heartbeat():
167
+ while self._running:
168
+ await asyncio.sleep(heartbeat_ms / 1000)
169
+ try:
170
+ await ws.send(json.dumps({"op": OP_HEARTBEAT, "d": None}))
171
+ except Exception:
172
+ break
173
+
174
+ hb_task = asyncio.create_task(heartbeat())
175
+ try:
176
+ async for raw in ws:
177
+ frame = json.loads(raw)
178
+ if frame.get("op") == OP_DISPATCH and frame.get("t") == "MESSAGE_CREATE":
179
+ inbound = self.normalize(frame)
180
+ if inbound and self.on_message:
181
+ self.on_message(inbound)
182
+ finally:
183
+ self._running = False
184
+ hb_task.cancel()