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,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] = {} # nonce -> 首次见到的时间
74
+ self._lock = threading.Lock()
75
+
76
+ def purge(self, now: float = 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) -> bool:
86
+ """见过 → True(**这是重放**);没见过则登记并返回 False。"""
87
+ if not nonce:
88
+ return True # 空 nonce 一律当重放/fail-closed
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) -> 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,
149
+ now: float = 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) -> 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,121 @@
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 # getUpdates 增量游标(避免重复拉取)
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
+ # 推进游标:下次从最大 update_id + 1 开始,避免重复
101
+ uid = update.get("update_id", 0)
102
+ if uid >= self._offset:
103
+ self._offset = uid + 1
104
+ inbound = self.handle_raw(update) # normalize + 触发 on_message
105
+ if inbound:
106
+ out.append(inbound)
107
+ return out
108
+
109
+ def verify_token(self) -> SendResult:
110
+ """getMe 验证 bot token 是否有效(本地接入时先确认凭证)。"""
111
+ try:
112
+ import httpx
113
+ with httpx.Client() as client:
114
+ resp = client.get(f"{self._base}/getMe", timeout=self.timeout)
115
+ data = resp.json()
116
+ if data.get("ok"):
117
+ bot = data["result"]
118
+ return SendResult(True, message_id=bot.get("username", ""))
119
+ return SendResult(False, error=data.get("description", "token 无效"))
120
+ except Exception as e:
121
+ return SendResult(False, error=str(e))
@@ -0,0 +1,225 @@
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
+ # 官方网关与端点(来自 Tencent/openclaw-weixin 源码 src/auth/accounts.ts)
26
+ WEIXIN_BASE_URL = "https://ilinkai.weixin.qq.com"
27
+ EP_GET_UPDATES = "ilink/bot/getupdates"
28
+ EP_SEND_MESSAGE = "ilink/bot/sendmessage"
29
+ EP_GET_CONFIG = "ilink/bot/getconfig"
30
+ EP_SEND_TYPING = "ilink/bot/sendtyping"
31
+
32
+ # 消息 item 类型(来自 README MessageItem 表)
33
+ ITEM_TEXT = 1
34
+ ITEM_IMAGE = 2
35
+ ITEM_VOICE = 3
36
+ ITEM_FILE = 4
37
+ ITEM_VIDEO = 5
38
+ # message_type
39
+ MSG_FROM_USER = 1
40
+ MSG_FROM_BOT = 2
41
+
42
+
43
+ class WeixinAdapter(ChannelAdapter):
44
+ CHANNEL = "weixin"
45
+
46
+ def __init__(self, token: str, base_url: str = WEIXIN_BASE_URL,
47
+ bot_agent: str = "Foxclaw", channel_version: str = "2.4.3",
48
+ ilink_app_id: str = "bot", on_message=None,
49
+ long_poll_timeout_ms: int = 35000, timeout: float = 40.0):
50
+ super().__init__(on_message=on_message)
51
+ self.token = token
52
+ self.base_url = base_url.rstrip("/")
53
+ self.bot_agent = bot_agent
54
+ self.channel_version = channel_version
55
+ self.ilink_app_id = ilink_app_id
56
+ self.long_poll_timeout_ms = long_poll_timeout_ms
57
+ self.timeout = timeout
58
+ self._cursor = "" # get_updates_buf 同步游标
59
+ # 记录每个用户最近的 context_token(回复时必须带回)
60
+ self._context_tokens: dict[str, str] = {}
61
+
62
+ @staticmethod
63
+ def _build_client_version(version: str) -> int:
64
+ """照官方 buildClientVersion: (major<<16)|(minor<<8)|patch。"""
65
+ parts = (version.split(".") + ["0", "0", "0"])[:3]
66
+ try:
67
+ major, minor, patch = (int(p) for p in parts)
68
+ except ValueError:
69
+ major = minor = patch = 0
70
+ return ((major & 0xFF) << 16) | ((minor & 0xFF) << 8) | (patch & 0xFF)
71
+
72
+ # ── 鉴权头(照官方 buildHeaders)──────────────────────
73
+ @staticmethod
74
+ def _random_wechat_uin() -> str:
75
+ """X-WECHAT-UIN: 随机 uint32 → 十进制字符串 → base64。"""
76
+ uint32 = int.from_bytes(secrets.token_bytes(4), "big")
77
+ return base64.b64encode(str(uint32).encode()).decode()
78
+
79
+ def _headers(self) -> dict:
80
+ headers = {
81
+ "Content-Type": "application/json",
82
+ "AuthorizationType": "ilink_bot_token",
83
+ "X-WECHAT-UIN": self._random_wechat_uin(),
84
+ "iLink-App-Id": self.ilink_app_id,
85
+ "iLink-App-ClientVersion": str(self._build_client_version(self.channel_version)),
86
+ }
87
+ if self.token.strip():
88
+ headers["Authorization"] = f"Bearer {self.token.strip()}"
89
+ return headers
90
+
91
+ def _base_info(self) -> dict:
92
+ return {"channel_version": self.channel_version, "bot_agent": self.bot_agent}
93
+
94
+ def _post(self, endpoint: str, body: dict, timeout: float = None):
95
+ import httpx
96
+ url = f"{self.base_url}/{endpoint}"
97
+ payload = {**body, "base_info": self._base_info()}
98
+ with httpx.Client() as client:
99
+ resp = client.post(url, json=payload, headers=self._headers(),
100
+ timeout=timeout or self.timeout)
101
+ return resp
102
+
103
+ # ── 入站:getUpdates 长轮询 ────────────────────────────
104
+ def poll_once(self) -> list[InboundMessage]:
105
+ """长轮询拉一批新消息。宿主在循环里反复调用即可持续接收。"""
106
+ try:
107
+ import httpx # noqa
108
+ except ImportError:
109
+ return []
110
+ if not self.token:
111
+ return []
112
+ try:
113
+ resp = self._post(EP_GET_UPDATES,
114
+ {"get_updates_buf": self._cursor},
115
+ timeout=self.long_poll_timeout_ms / 1000 + 5)
116
+ except Exception:
117
+ return [] # 长轮询超时/中断 → 空,下次重试
118
+ if resp.status_code != 200:
119
+ return []
120
+ try:
121
+ data = resp.json()
122
+ except Exception:
123
+ return []
124
+ if data.get("ret") != 0:
125
+ return [] # errcode -14 = 会话超时,需重新登录
126
+ # 推进游标
127
+ new_buf = data.get("get_updates_buf")
128
+ if new_buf is not None:
129
+ self._cursor = new_buf
130
+ out = []
131
+ for raw_msg in data.get("msgs", []):
132
+ inbound = self._normalize_msg(raw_msg)
133
+ if inbound:
134
+ if self.on_message:
135
+ self.on_message(inbound)
136
+ out.append(inbound)
137
+ return out
138
+
139
+ def normalize(self, raw: dict) -> Optional[InboundMessage]:
140
+ """单条 WeixinMessage → 标准 InboundMessage(供 handle_raw 用)。"""
141
+ return self._normalize_msg(raw)
142
+
143
+ def _normalize_msg(self, msg: dict) -> Optional[InboundMessage]:
144
+ # 只处理用户发来的消息(message_type=1),忽略 bot 自己的回显
145
+ if msg.get("message_type") not in (None, MSG_FROM_USER):
146
+ return None
147
+ # 从 item_list 提取文本
148
+ text = ""
149
+ for item in msg.get("item_list", []) or []:
150
+ if item.get("type") == ITEM_TEXT and item.get("text_item"):
151
+ text += item["text_item"].get("text", "")
152
+ if not text:
153
+ return None # 非文本(图片/语音等)暂忽略
154
+ from_user = str(msg.get("from_user_id", ""))
155
+ ctx = msg.get("context_token", "")
156
+ if from_user and ctx:
157
+ self._context_tokens[from_user] = ctx # 记住,回复时带回
158
+ return InboundMessage(
159
+ channel=self.CHANNEL,
160
+ message_id=str(msg.get("message_id", "")),
161
+ text=text,
162
+ sender_id=from_user,
163
+ sender_name="",
164
+ conversation_id=str(msg.get("session_id", "") or from_user),
165
+ is_group=False, # 个人号 DM;群聊当前插件能力未公开
166
+ timestamp=float(msg.get("create_time_ms", 0)) / 1000 or 0.0,
167
+ raw=msg)
168
+
169
+ # ── 出站:sendMessage ──────────────────────────────────
170
+ def send(self, msg: OutboundMessage) -> SendResult:
171
+ """发文本消息。conversation_id 作为 to_user_id;context_token 从记录里取或由 extra 传入。"""
172
+ try:
173
+ import httpx # noqa
174
+ except ImportError:
175
+ return SendResult(False, error="缺少 httpx 依赖")
176
+ if not self.token:
177
+ return SendResult(False, error="未配置微信 token(需先扫码登录)")
178
+ to_user = msg.conversation_id
179
+ ctx = msg.extra.get("context_token") or self._context_tokens.get(to_user, "")
180
+ body = {
181
+ "msg": {
182
+ "to_user_id": to_user,
183
+ "context_token": ctx,
184
+ "item_list": [
185
+ {"type": ITEM_TEXT, "text_item": {"text": msg.text}}
186
+ ],
187
+ }
188
+ }
189
+ try:
190
+ resp = self._post(EP_SEND_MESSAGE, body)
191
+ except Exception as e:
192
+ return SendResult(False, error=f"微信发送失败: {e}")
193
+ if resp.status_code >= 400:
194
+ return SendResult(False, error=f"微信 API HTTP {resp.status_code}: {resp.text[:200]}")
195
+ try:
196
+ data = resp.json()
197
+ except Exception:
198
+ data = {}
199
+ if data.get("ret", 0) != 0:
200
+ return SendResult(False, error=f"微信: ret={data.get('ret')} {data.get('errmsg','')}")
201
+ return SendResult(True)
202
+
203
+ def reply(self, inbound: InboundMessage, text: str) -> SendResult:
204
+ """便捷回复:自动带回该用户的 context_token(微信要求回复必须带)。"""
205
+ ctx = inbound.raw.get("context_token", "") if inbound.raw else ""
206
+ return self.send(OutboundMessage(
207
+ conversation_id=inbound.conversation_id, text=text,
208
+ extra={"context_token": ctx} if ctx else {}))
209
+
210
+ def verify_token(self, ilink_user_id: str = "") -> SendResult:
211
+ """探测 token 是否有效(调 getconfig)。
212
+
213
+ 官方 getconfig 需要 ilink_user_id;无则取最近一个已记录的用户。
214
+ 若从未收到过消息且未传 user_id,无法探测(返回提示)。
215
+ """
216
+ user_id = ilink_user_id or next(iter(self._context_tokens), "")
217
+ if not user_id:
218
+ return SendResult(False,
219
+ error="需提供 ilink_user_id(或先收到一条消息)才能探测")
220
+ try:
221
+ resp = self._post(EP_GET_CONFIG, {"ilink_user_id": user_id})
222
+ ok = resp.status_code == 200 and resp.json().get("ret") == 0
223
+ return SendResult(ok, error="" if ok else "token 无效或已过期(需重新扫码登录)")
224
+ except Exception as e:
225
+ return SendResult(False, error=str(e))
@@ -0,0 +1,154 @@
1
+ """命令行入口 `foxclaw`。
2
+
3
+ 与 PyPI 上 `foxclaw` 0.1.0 占位包发布的入口保持一致:包名 `foxclaw`,
4
+ 命令名 `foxclaw`。
5
+
6
+ foxclaw # 打印版本与提示
7
+ foxclaw serve # 启动 API 服务(默认 http://127.0.0.1:8000)
8
+ foxclaw config check # 检查 .env / 环境变量:类型写错、拼错的键、互相矛盾的组合
9
+ foxclaw config show # 打印当前生效配置(密钥默认脱敏)
10
+ foxclaw --version # 打印版本
11
+
12
+ `serve` 用字符串目标 `foxclaw.api:app` 交给 uvicorn 导入,避免 CLI 进程
13
+ 自己先把整个应用栈加载一遍(含 HKC/HMR 与模型);`--reload` 时 uvicorn 也需要
14
+ 字符串目标才能重建子进程。
15
+ """
16
+
17
+ from __future__ import annotations
18
+
19
+ import argparse
20
+ import os
21
+ import sys
22
+ from pathlib import Path
23
+
24
+ from foxclaw import __version__
25
+
26
+ APP_TARGET = "foxclaw.api:app"
27
+
28
+
29
+ def _repo_root() -> Path:
30
+ """仓库根目录(可编辑安装时即包含 foxclaw/ 的那一层)。"""
31
+ return Path(__file__).resolve().parents[1]
32
+
33
+
34
+ def _cmd_serve(args: argparse.Namespace) -> int:
35
+ from foxclaw.core import settings
36
+
37
+ # 启动前先查配置:类型写错的值今天会崩在很远的地方(uvicorn 40 行 traceback 中间的
38
+ # `int()`),用户看不出是自己 .env 写错了。这里提前拦下并说清怎么改。
39
+ issues = settings.check()
40
+ fatal = settings.fatal_issues(issues)
41
+ if fatal:
42
+ print(settings.format_issues(issues), file=sys.stderr)
43
+ print("\n以上配置会让启动直接失败,已中止。改好后重试。", file=sys.stderr)
44
+ return 2
45
+ if issues:
46
+ # 不影响启动的问题(可疑但能跑)只提醒一次,不拦
47
+ print(settings.format_issues(issues, limit=8), file=sys.stderr)
48
+ print("(以上不阻断启动;`foxclaw config check` 看完整诊断)", file=sys.stderr)
49
+
50
+ import uvicorn
51
+
52
+ kwargs: dict[str, object] = {
53
+ "host": args.host,
54
+ "port": args.port,
55
+ "log_level": args.log_level,
56
+ }
57
+ if args.reload:
58
+ # 只监听源码目录,避免 vendor/ 与测试临时文件触发重启
59
+ kwargs["reload"] = True
60
+ kwargs["reload_dirs"] = [str(_repo_root() / "foxclaw")]
61
+ uvicorn.run(APP_TARGET, **kwargs)
62
+ return 0
63
+
64
+
65
+ def _cmd_config_check(args: argparse.Namespace) -> int:
66
+ from foxclaw.core import settings
67
+
68
+ issues = settings.check()
69
+ if args.json:
70
+ import json as _json
71
+ print(_json.dumps(
72
+ [{"level": i.level, "code": i.code, "name": i.name,
73
+ "message": i.message, "hint": i.hint} for i in issues],
74
+ ensure_ascii=False, indent=2))
75
+ else:
76
+ print(settings.format_issues(issues))
77
+ return 1 if any(i.level == "error" for i in issues) else 0
78
+
79
+
80
+ def _cmd_config_show(args: argparse.Namespace) -> int:
81
+ from foxclaw.core import settings
82
+
83
+ current = ""
84
+ for opt in settings.SPECS:
85
+ if opt.group != current:
86
+ current = opt.group
87
+ print(f"[{current}]")
88
+ raw = os.environ.get(opt.name)
89
+ if raw is None or raw.strip() == "":
90
+ shown = f"{settings.mask(opt.default) if opt.secret else opt.default}" \
91
+ if opt.default not in ("", None) else "(未设置)"
92
+ suffix = " 默认"
93
+ else:
94
+ shown = settings.mask(raw) if (opt.secret and not args.reveal) else raw
95
+ suffix = " 已设置"
96
+ print(f" {opt.name} = {shown}{suffix}")
97
+ return 0
98
+
99
+
100
+ def build_parser() -> argparse.ArgumentParser:
101
+ parser = argparse.ArgumentParser(
102
+ prog="foxclaw",
103
+ description="Foxclaw —— 认知 Agent 操作系统",
104
+ )
105
+ parser.add_argument(
106
+ "-V",
107
+ "--version",
108
+ action="version",
109
+ version=f"foxclaw {__version__}",
110
+ )
111
+
112
+ subparsers = parser.add_subparsers(dest="command", metavar="<command>")
113
+
114
+ serve = subparsers.add_parser("serve", help="启动 API 服务")
115
+ serve.add_argument("--host", default="127.0.0.1", help="监听地址,默认仅本机回环")
116
+ serve.add_argument("--port", type=int, default=8000, help="监听端口,默认 8000")
117
+ serve.add_argument("--reload", action="store_true", help="改代码自动重启(开发用)")
118
+ serve.add_argument("--log-level", default="info", help="uvicorn 日志级别")
119
+ serve.set_defaults(handler=_cmd_serve)
120
+
121
+ config = subparsers.add_parser("config", help="查看/检查配置(.env 与环境变量)")
122
+ config_sub = config.add_subparsers(dest="config_action", metavar="<action>")
123
+
124
+ c_check = config_sub.add_parser(
125
+ "check", help="检查配置:类型、取值范围、拼错的键、互相矛盾的组合")
126
+ c_check.add_argument("--json", action="store_true", help="输出 JSON(便于脚本/前端消费)")
127
+ c_check.set_defaults(handler=_cmd_config_check)
128
+
129
+ c_show = config_sub.add_parser("show", help="打印当前生效配置")
130
+ c_show.add_argument("--reveal", action="store_true",
131
+ help="显示密钥原文(默认脱敏;别在截图/日志里用)")
132
+ c_show.set_defaults(handler=_cmd_config_show)
133
+
134
+ return parser
135
+
136
+
137
+ def main(argv: list[str] | None = None) -> int:
138
+ parser = build_parser()
139
+ args = parser.parse_args(argv)
140
+
141
+ handler = getattr(args, "handler", None)
142
+ if handler is None:
143
+ if getattr(args, "command", None) == "config":
144
+ print("用法:`foxclaw config check` 或 `foxclaw config show`")
145
+ return 2
146
+ print(f"foxclaw {__version__}")
147
+ print("用 `foxclaw serve` 启动本地服务;`foxclaw config check` 检查配置;"
148
+ "`foxclaw --help` 查看全部子命令。")
149
+ return 0
150
+ return int(handler(args))
151
+
152
+
153
+ if __name__ == "__main__":
154
+ sys.exit(main())