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,187 @@
1
+ """渠道出站签名 / 入站验签(HMAC-SHA256,时间窗 + nonce 防重放)。
2
+
3
+ **为什么需要**:渠道消息目前是"明文 HTTPS + Bearer token"。TLS 只保护链路,保护不了
4
+ 两端之间被中间人代理/日志重放的**消息语义**:网关收到一条 `{"conversation":…, "text":
5
+ "转账 5000"}` 无法判断它真是 Foxclaw 发的、还是别人照着抓包重放的。
6
+
7
+ **为什么只在「我们两端都控制」的方向签**:Telegram/Discord/飞书/微信的 API 不验我们的
8
+ 签名 —— 给它们的请求加 `X-Foxclaw-Signature` 纯属表演(它们会忽略,攻击者也不需要
9
+ 伪造,拿 bot token 直连即可)。真正有威胁的是 Foxclaw ↔ 自建网关(OpenClaw 桥)这条
10
+ 链路:两端都是自己的代码,共享密钥可落地,签名才有验证方。
11
+
12
+ **签名结构**(canonical string,避免"把 header 挪来挪去"各种拼接口径不一致):
13
+
14
+ v1 \n {timestamp} \n {nonce} \n {sha256(body) 的 hex}
15
+
16
+ - `body` 摘要进签名 → 改正文即失效(不只是改 header 才算篡改);
17
+ - `timestamp` 进签名 → 超出容差(默认 300s)直接拒,缩小重放窗口;
18
+ - `nonce` 进签名 + 验方 `ReplayGuard` 记账 → 窗口内也不能重放同一请求;
19
+ - `v1` 前缀 → 将来换算法可并存,不炸老客户端。
20
+
21
+ 配置:`FOXCLAW_CHANNEL_SIGNING_SECRET`(共享密钥)、可选
22
+ `FOXCLAW_CHANNEL_SIGNING_KEY_ID`(轮换时对账用)。未配置 = 不签(opt-in);
23
+ 一旦配置,签名失败就**不发**(fail-closed,绝不静默降级成明文)。
24
+ """
25
+ from __future__ import annotations
26
+
27
+ import hashlib
28
+ import hmac
29
+ import os
30
+ import secrets
31
+ import threading
32
+ import time
33
+ from typing import Mapping, Optional, Tuple
34
+
35
+ SIGN_VERSION = "v1"
36
+ SIGNATURE_HEADER = "X-Foxclaw-Signature"
37
+ TIMESTAMP_HEADER = "X-Foxclaw-Timestamp"
38
+ NONCE_HEADER = "X-Foxclaw-Nonce"
39
+ KEY_ID_HEADER = "X-Foxclaw-Key-Id"
40
+
41
+ ENV_SECRET = "FOXCLAW_CHANNEL_SIGNING_SECRET"
42
+ ENV_KEY_ID = "FOXCLAW_CHANNEL_SIGNING_KEY_ID"
43
+
44
+ DEFAULT_TOLERANCE = 300.0
45
+
46
+
47
+ def body_digest(body: bytes) -> str:
48
+ """正文摘要(绑定进签名,防正文被改)。"""
49
+ return hashlib.sha256(body or b"").hexdigest()
50
+
51
+
52
+ def canonical(ts: str, nonce: str, body: bytes) -> bytes:
53
+ """签名原文。字段顺序/分隔符改动 = 破坏兼容,测试里锁死。"""
54
+ return f"{SIGN_VERSION}\n{ts}\n{nonce}\n{body_digest(body)}".encode()
55
+
56
+
57
+ def sign(secret: str, ts: str, nonce: str, body: bytes) -> str:
58
+ """计算签名值(hex)。"""
59
+ return hmac.new((secret or "").encode("utf-8"),
60
+ canonical(ts, nonce, body), hashlib.sha256).hexdigest()
61
+
62
+
63
+ class ReplayGuard:
64
+ """验方 nonce 记账:同一 nonce 在 TTL 内只接受一次。
65
+
66
+ 线程安全 + 有界(超过上限丢最旧),避免被刷爆内存:nonce 是攻击者可控输入,
67
+ 无界集合等于给了一个内存耗尽面。
68
+ """
69
+
70
+ def __init__(self, ttl: float = DEFAULT_TOLERANCE, max_entries: int = 4096):
71
+ self.ttl = float(ttl)
72
+ self.max_entries = max(16, int(max_entries))
73
+ self._seen: dict[str, float] = {}
74
+ self._lock = threading.Lock()
75
+
76
+ def purge(self, now: float | None = None) -> int:
77
+ """清掉过期 nonce,返回清理条数。"""
78
+ cut = (now if now is not None else time.time()) - self.ttl
79
+ with self._lock:
80
+ dead = [k for k, t in self._seen.items() if t < cut]
81
+ for k in dead:
82
+ self._seen.pop(k, None)
83
+ return len(dead)
84
+
85
+ def seen(self, nonce: str, now: float | None = None) -> bool:
86
+ """见过 → True(**这是重放**);没见过则登记并返回 False。"""
87
+ if not nonce:
88
+ return True
89
+ now = now if now is not None else time.time()
90
+ with self._lock:
91
+ cut = now - self.ttl
92
+ dead = [k for k, t in self._seen.items() if t < cut]
93
+ for k in dead:
94
+ self._seen.pop(k, None)
95
+ if nonce in self._seen:
96
+ return True
97
+ if len(self._seen) >= self.max_entries:
98
+ oldest = min(self._seen.items(), key=lambda kv: kv[1])[0]
99
+ self._seen.pop(oldest, None)
100
+ self._seen[nonce] = now
101
+ return False
102
+
103
+ def __len__(self) -> int:
104
+ return len(self._seen)
105
+
106
+
107
+ class OutboundSigner:
108
+ """出站签名器:给一次请求生成 4 个 header。"""
109
+
110
+ def __init__(self, secret: str, key_id: str = ""):
111
+ self.secret = secret or ""
112
+ self.key_id = key_id or ""
113
+
114
+ @property
115
+ def enabled(self) -> bool:
116
+ return bool(self.secret)
117
+
118
+ def headers(self, body: bytes, nonce: str = "", now: float | None = None) -> dict:
119
+ """生成签名头。未配置密钥直接抛错(fail-closed:调用方必须先判 `enabled`)。"""
120
+ if not self.enabled:
121
+ raise ValueError("未配置渠道签名密钥(FOXCLAW_CHANNEL_SIGNING_SECRET)")
122
+ ts = str(int(now if now is not None else time.time()))
123
+ nonce = nonce or secrets.token_hex(12)
124
+ h = {
125
+ SIGNATURE_HEADER: f"{SIGN_VERSION}={sign(self.secret, ts, nonce, body)}",
126
+ TIMESTAMP_HEADER: ts,
127
+ NONCE_HEADER: nonce,
128
+ }
129
+ if self.key_id:
130
+ h[KEY_ID_HEADER] = self.key_id
131
+ return h
132
+
133
+
134
+ def _header(headers: Mapping, name: str) -> str:
135
+ """大小写无关取 header(HTTP header 名不区分大小写,不同框架给的大小写不同)。"""
136
+ if not headers:
137
+ return ""
138
+ if name in headers:
139
+ return str(headers[name] or "")
140
+ low = name.lower()
141
+ for k, v in headers.items():
142
+ if str(k).lower() == low:
143
+ return str(v or "")
144
+ return ""
145
+
146
+
147
+ def verify(headers: Mapping, body: bytes, secret: str,
148
+ tolerance: float = DEFAULT_TOLERANCE, replay: ReplayGuard | None = None,
149
+ now: float | None = None) -> Tuple[bool, str]:
150
+ """验签。返回 (是否通过, 原因码)。
151
+
152
+ 原因码:``ok`` / ``missing_secret`` / ``missing_header`` / ``bad_version`` /
153
+ ``expired`` / ``replay`` / ``bad_signature``(排错时能一眼看出是哪一层挂了)。
154
+ ``replay`` 传 None 则**不做**防重放(只验真伪),由调用方决定是否记账。
155
+ """
156
+ if not secret:
157
+ return False, "missing_secret"
158
+ sig_raw = _header(headers, SIGNATURE_HEADER)
159
+ ts = _header(headers, TIMESTAMP_HEADER)
160
+ nonce = _header(headers, NONCE_HEADER)
161
+ if not sig_raw or not ts or not nonce:
162
+ return False, "missing_header"
163
+ ver, _, sig = sig_raw.partition("=")
164
+ if ver != SIGN_VERSION or not sig:
165
+ return False, "bad_version"
166
+ try:
167
+ ts_f = float(ts)
168
+ except (TypeError, ValueError):
169
+ return False, "bad_version"
170
+ now = now if now is not None else time.time()
171
+ if abs(now - ts_f) > float(tolerance):
172
+ return False, "expired"
173
+ expect = sign(secret, ts, nonce, body)
174
+ if not hmac.compare_digest(expect, sig):
175
+ return False, "bad_signature"
176
+ if replay is not None and replay.seen(nonce, now=now):
177
+ return False, "replay"
178
+ return True, "ok"
179
+
180
+
181
+ def from_env(env: Mapping | None = None) -> Optional[OutboundSigner]:
182
+ """按环境变量构造签名器;未配置密钥 → None(不签,opt-in)。"""
183
+ env = env if env is not None else os.environ
184
+ secret = (env.get(ENV_SECRET) or "").strip()
185
+ if not secret:
186
+ return None
187
+ return OutboundSigner(secret, key_id=(env.get(ENV_KEY_ID) or "").strip())
@@ -0,0 +1,120 @@
1
+ """
2
+ Foxclaw Channels — Telegram 适配器
3
+
4
+ 用 Telegram 官方 Bot API(api.telegram.org),完全合规:
5
+ · 接入:在 @BotFather 创建 Bot,拿到 bot token
6
+ · 收消息:getUpdates 长轮询(或 webhook,此处用轮询,本地最易跑通)
7
+ · 发消息:sendMessage
8
+
9
+ 凭证(bot token)由宿主注入,不硬编码。借鉴 OpenClaw 的 normalize/send 对称结构。
10
+ """
11
+ from __future__ import annotations
12
+ from typing import Optional
13
+ from . import ChannelAdapter, InboundMessage, OutboundMessage, SendResult
14
+
15
+ TELEGRAM_API = "https://api.telegram.org"
16
+
17
+
18
+ class TelegramAdapter(ChannelAdapter):
19
+ CHANNEL = "telegram"
20
+
21
+ def __init__(self, bot_token: str, on_message=None, timeout: float = 30.0):
22
+ super().__init__(on_message=on_message)
23
+ self.bot_token = bot_token
24
+ self.timeout = timeout
25
+ self._offset = 0
26
+
27
+ @property
28
+ def _base(self) -> str:
29
+ return f"{TELEGRAM_API}/bot{self.bot_token}"
30
+
31
+ def normalize(self, raw: dict) -> Optional[InboundMessage]:
32
+ """Telegram update → 标准 InboundMessage。非文本消息返回 None。"""
33
+ msg = raw.get("message") or raw.get("edited_message")
34
+ if not msg or "text" not in msg:
35
+ return None
36
+ chat = msg.get("chat", {})
37
+ sender = msg.get("from", {})
38
+ chat_type = chat.get("type", "private")
39
+ return InboundMessage(
40
+ channel=self.CHANNEL,
41
+ message_id=str(msg.get("message_id", "")),
42
+ text=msg.get("text", ""),
43
+ sender_id=str(sender.get("id", "")),
44
+ sender_name=(sender.get("username")
45
+ or sender.get("first_name", "") or "").strip(),
46
+ conversation_id=str(chat.get("id", "")),
47
+ is_group=chat_type in ("group", "supergroup"),
48
+ timestamp=float(msg.get("date", 0)),
49
+ raw=raw)
50
+
51
+ def send(self, msg: OutboundMessage) -> SendResult:
52
+ """通过 sendMessage 发出。"""
53
+ try:
54
+ import httpx
55
+ except ImportError:
56
+ return SendResult(False, error="缺少 httpx 依赖")
57
+ if not self.bot_token:
58
+ return SendResult(False, error="未配置 bot token")
59
+ payload = {"chat_id": msg.conversation_id, "text": msg.text}
60
+ if msg.reply_to:
61
+ payload["reply_to_message_id"] = msg.reply_to
62
+ try:
63
+ with httpx.Client() as client:
64
+ resp = client.post(f"{self._base}/sendMessage",
65
+ json=payload, timeout=self.timeout)
66
+ except Exception as e:
67
+ return SendResult(False, error=f"发送失败: {e}")
68
+ if resp.status_code >= 400:
69
+ return SendResult(False, error=f"Telegram API HTTP {resp.status_code}: {resp.text[:200]}")
70
+ data = resp.json()
71
+ if not data.get("ok"):
72
+ return SendResult(False, error=f"Telegram: {data.get('description','未知错误')}")
73
+ return SendResult(True, message_id=str(data["result"].get("message_id", "")))
74
+
75
+ def poll_once(self) -> list[InboundMessage]:
76
+ """拉取一批新消息(getUpdates 长轮询一次)。返回标准化后的消息列表。
77
+
78
+ 宿主可在循环里反复调用实现持续接收;本地最易跑通的方式。
79
+ """
80
+ try:
81
+ import httpx
82
+ except ImportError:
83
+ return []
84
+ if not self.bot_token:
85
+ return []
86
+ try:
87
+ with httpx.Client() as client:
88
+ resp = client.get(f"{self._base}/getUpdates",
89
+ params={"offset": self._offset, "timeout": 25},
90
+ timeout=self.timeout + 5)
91
+ except Exception:
92
+ return []
93
+ if resp.status_code != 200:
94
+ return []
95
+ data = resp.json()
96
+ if not data.get("ok"):
97
+ return []
98
+ out = []
99
+ for update in data.get("result", []):
100
+ uid = update.get("update_id", 0)
101
+ if uid >= self._offset:
102
+ self._offset = uid + 1
103
+ inbound = self.handle_raw(update)
104
+ if inbound:
105
+ out.append(inbound)
106
+ return out
107
+
108
+ def verify_token(self) -> SendResult:
109
+ """getMe 验证 bot token 是否有效(本地接入时先确认凭证)。"""
110
+ try:
111
+ import httpx
112
+ with httpx.Client() as client:
113
+ resp = client.get(f"{self._base}/getMe", timeout=self.timeout)
114
+ data = resp.json()
115
+ if data.get("ok"):
116
+ bot = data["result"]
117
+ return SendResult(True, message_id=bot.get("username", ""))
118
+ return SendResult(False, error=data.get("description", "token 无效"))
119
+ except Exception as e:
120
+ return SendResult(False, error=str(e))
@@ -0,0 +1,215 @@
1
+ """
2
+ Foxclaw Channels — 微信 (Weixin) 适配器
3
+
4
+ 照腾讯官方开源插件 Tencent/openclaw-weixin 公开的后端 API 协议(iLink Bot API)
5
+ 用 Python 实现。该协议由腾讯官方文档化,README 明确支持开发者用自己的后端对接,
6
+ 故这是官方公开协议的合规实现,非逆向。
7
+
8
+ 接入模型(与飞书/Telegram 又不同,不套同一模子):
9
+ · 网关:https://ilinkai.weixin.qq.com,端点前缀 ilink/bot/
10
+ · 鉴权:AuthorizationType: ilink_bot_token + Authorization: Bearer <token>
11
+ + X-WECHAT-UIN(随机 uint32 → 十进制串 → base64)
12
+ · 收消息:getupdates 长轮询(带 get_updates_buf 游标)
13
+ · 发消息:sendmessage(带 to_user_id + context_token + item_list)
14
+ · 每个请求体带 base_info: {channel_version, bot_agent}
15
+
16
+ 凭证(token)由宿主注入:用户需先用 openclaw 官方 CLI 扫码登录拿到 token
17
+ (扫码登录这一步在微信侧,本适配器不复现登录,只复现已登录后的消息收发协议)。
18
+ """
19
+ from __future__ import annotations
20
+ import base64
21
+ import secrets
22
+ from typing import Optional
23
+ from . import ChannelAdapter, InboundMessage, OutboundMessage, SendResult
24
+
25
+ WEIXIN_BASE_URL = "https://ilinkai.weixin.qq.com"
26
+ EP_GET_UPDATES = "ilink/bot/getupdates"
27
+ EP_SEND_MESSAGE = "ilink/bot/sendmessage"
28
+ EP_GET_CONFIG = "ilink/bot/getconfig"
29
+ EP_SEND_TYPING = "ilink/bot/sendtyping"
30
+
31
+ ITEM_TEXT = 1
32
+ ITEM_IMAGE = 2
33
+ ITEM_VOICE = 3
34
+ ITEM_FILE = 4
35
+ ITEM_VIDEO = 5
36
+ MSG_FROM_USER = 1
37
+ MSG_FROM_BOT = 2
38
+
39
+
40
+ class WeixinAdapter(ChannelAdapter):
41
+ CHANNEL = "weixin"
42
+
43
+ def __init__(self, token: str, base_url: str = WEIXIN_BASE_URL,
44
+ bot_agent: str = "Foxclaw", channel_version: str = "2.4.3",
45
+ ilink_app_id: str = "bot", on_message=None,
46
+ long_poll_timeout_ms: int = 35000, timeout: float = 40.0):
47
+ super().__init__(on_message=on_message)
48
+ self.token = token
49
+ self.base_url = base_url.rstrip("/")
50
+ self.bot_agent = bot_agent
51
+ self.channel_version = channel_version
52
+ self.ilink_app_id = ilink_app_id
53
+ self.long_poll_timeout_ms = long_poll_timeout_ms
54
+ self.timeout = timeout
55
+ self._cursor = ""
56
+ self._context_tokens: dict[str, str] = {}
57
+
58
+ @staticmethod
59
+ def _build_client_version(version: str) -> int:
60
+ """照官方 buildClientVersion: (major<<16)|(minor<<8)|patch。"""
61
+ parts = (version.split(".") + ["0", "0", "0"])[:3]
62
+ try:
63
+ major, minor, patch = (int(p) for p in parts)
64
+ except ValueError:
65
+ major = minor = patch = 0
66
+ return ((major & 0xFF) << 16) | ((minor & 0xFF) << 8) | (patch & 0xFF)
67
+
68
+ @staticmethod
69
+ def _random_wechat_uin() -> str:
70
+ """X-WECHAT-UIN: 随机 uint32 → 十进制字符串 → base64。"""
71
+ uint32 = int.from_bytes(secrets.token_bytes(4), "big")
72
+ return base64.b64encode(str(uint32).encode()).decode()
73
+
74
+ def _headers(self) -> dict:
75
+ headers = {
76
+ "Content-Type": "application/json",
77
+ "AuthorizationType": "ilink_bot_token",
78
+ "X-WECHAT-UIN": self._random_wechat_uin(),
79
+ "iLink-App-Id": self.ilink_app_id,
80
+ "iLink-App-ClientVersion": str(self._build_client_version(self.channel_version)),
81
+ }
82
+ if self.token.strip():
83
+ headers["Authorization"] = f"Bearer {self.token.strip()}"
84
+ return headers
85
+
86
+ def _base_info(self) -> dict:
87
+ return {"channel_version": self.channel_version, "bot_agent": self.bot_agent}
88
+
89
+ def _post(self, endpoint: str, body: dict, timeout: float | None = None):
90
+ import httpx
91
+ url = f"{self.base_url}/{endpoint}"
92
+ payload = {**body, "base_info": self._base_info()}
93
+ with httpx.Client() as client:
94
+ resp = client.post(url, json=payload, headers=self._headers(),
95
+ timeout=timeout or self.timeout)
96
+ return resp
97
+
98
+ def poll_once(self) -> list[InboundMessage]:
99
+ """长轮询拉一批新消息。宿主在循环里反复调用即可持续接收。"""
100
+ try:
101
+ import httpx # noqa
102
+ except ImportError:
103
+ return []
104
+ if not self.token:
105
+ return []
106
+ try:
107
+ resp = self._post(EP_GET_UPDATES,
108
+ {"get_updates_buf": self._cursor},
109
+ timeout=self.long_poll_timeout_ms / 1000 + 5)
110
+ except Exception:
111
+ return []
112
+ if resp.status_code != 200:
113
+ return []
114
+ try:
115
+ data = resp.json()
116
+ except Exception:
117
+ return []
118
+ if data.get("ret") != 0:
119
+ return []
120
+ new_buf = data.get("get_updates_buf")
121
+ if new_buf is not None:
122
+ self._cursor = new_buf
123
+ out = []
124
+ for raw_msg in data.get("msgs", []):
125
+ inbound = self._normalize_msg(raw_msg)
126
+ if inbound:
127
+ if self.on_message:
128
+ self.on_message(inbound)
129
+ out.append(inbound)
130
+ return out
131
+
132
+ def normalize(self, raw: dict) -> Optional[InboundMessage]:
133
+ """单条 WeixinMessage → 标准 InboundMessage(供 handle_raw 用)。"""
134
+ return self._normalize_msg(raw)
135
+
136
+ def _normalize_msg(self, msg: dict) -> Optional[InboundMessage]:
137
+ if msg.get("message_type") not in (None, MSG_FROM_USER):
138
+ return None
139
+ text = ""
140
+ for item in msg.get("item_list", []) or []:
141
+ if item.get("type") == ITEM_TEXT and item.get("text_item"):
142
+ text += item["text_item"].get("text", "")
143
+ if not text:
144
+ return None
145
+ from_user = str(msg.get("from_user_id", ""))
146
+ ctx = msg.get("context_token", "")
147
+ if from_user and ctx:
148
+ self._context_tokens[from_user] = ctx
149
+ return InboundMessage(
150
+ channel=self.CHANNEL,
151
+ message_id=str(msg.get("message_id", "")),
152
+ text=text,
153
+ sender_id=from_user,
154
+ sender_name="",
155
+ conversation_id=str(msg.get("session_id", "") or from_user),
156
+ is_group=False,
157
+ timestamp=float(msg.get("create_time_ms", 0)) / 1000 or 0.0,
158
+ raw=msg)
159
+
160
+ def send(self, msg: OutboundMessage) -> SendResult:
161
+ """发文本消息。conversation_id 作为 to_user_id;context_token 从记录里取或由 extra 传入。"""
162
+ try:
163
+ import httpx # noqa
164
+ except ImportError:
165
+ return SendResult(False, error="缺少 httpx 依赖")
166
+ if not self.token:
167
+ return SendResult(False, error="未配置微信 token(需先扫码登录)")
168
+ to_user = msg.conversation_id
169
+ ctx = msg.extra.get("context_token") or self._context_tokens.get(to_user, "")
170
+ body = {
171
+ "msg": {
172
+ "to_user_id": to_user,
173
+ "context_token": ctx,
174
+ "item_list": [
175
+ {"type": ITEM_TEXT, "text_item": {"text": msg.text}}
176
+ ],
177
+ }
178
+ }
179
+ try:
180
+ resp = self._post(EP_SEND_MESSAGE, body)
181
+ except Exception as e:
182
+ return SendResult(False, error=f"微信发送失败: {e}")
183
+ if resp.status_code >= 400:
184
+ return SendResult(False, error=f"微信 API HTTP {resp.status_code}: {resp.text[:200]}")
185
+ try:
186
+ data = resp.json()
187
+ except Exception:
188
+ data = {}
189
+ if data.get("ret", 0) != 0:
190
+ return SendResult(False, error=f"微信: ret={data.get('ret')} {data.get('errmsg','')}")
191
+ return SendResult(True)
192
+
193
+ def reply(self, inbound: InboundMessage, text: str) -> SendResult:
194
+ """便捷回复:自动带回该用户的 context_token(微信要求回复必须带)。"""
195
+ ctx = inbound.raw.get("context_token", "") if inbound.raw else ""
196
+ return self.send(OutboundMessage(
197
+ conversation_id=inbound.conversation_id, text=text,
198
+ extra={"context_token": ctx} if ctx else {}))
199
+
200
+ def verify_token(self, ilink_user_id: str = "") -> SendResult:
201
+ """探测 token 是否有效(调 getconfig)。
202
+
203
+ 官方 getconfig 需要 ilink_user_id;无则取最近一个已记录的用户。
204
+ 若从未收到过消息且未传 user_id,无法探测(返回提示)。
205
+ """
206
+ user_id = ilink_user_id or next(iter(self._context_tokens), "")
207
+ if not user_id:
208
+ return SendResult(False,
209
+ error="需提供 ilink_user_id(或先收到一条消息)才能探测")
210
+ try:
211
+ resp = self._post(EP_GET_CONFIG, {"ilink_user_id": user_id})
212
+ ok = resp.status_code == 200 and resp.json().get("ret") == 0
213
+ return SendResult(ok, error="" if ok else "token 无效或已过期(需重新扫码登录)")
214
+ except Exception as e:
215
+ return SendResult(False, error=str(e))