foxclaw 0.1.2 → 0.1.4

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 (368) hide show
  1. package/index.js +1 -8
  2. package/package.json +1 -1
  3. package/python/.env.example +8 -0
  4. package/python/.github/workflows/ci.yml +47 -2
  5. package/python/.github/workflows/release.yml +5 -0
  6. package/python/CHANGELOG.md +100 -0
  7. package/python/CHANGELOG_CN.md +100 -0
  8. package/python/README.md +17 -2
  9. package/python/README_CN.md +38 -20
  10. package/python/bench/bm25_audit.py +1 -14
  11. package/python/bench/bm25_regression.py +0 -1
  12. package/python/bench/bm25_stress.py +0 -4
  13. package/python/bench/cd_audit.py +0 -23
  14. package/python/bench/chaos.py +6 -8
  15. package/python/bench/e2e_real_models.py +0 -1
  16. package/python/bench/llm_retry_test.py +2 -9
  17. package/python/bench/load_test.py +5 -8
  18. package/python/bench/vector_ivf_upgrade.py +1 -7
  19. package/python/docs/CONTENT_GUARD.md +189 -0
  20. package/python/docs/CONTENT_GUARD_CN.md +189 -0
  21. package/python/docs/RELEASE.md +78 -37
  22. package/python/docs/RUN_CN.md +2 -2
  23. package/python/evals/governance/cases.py +0 -37
  24. package/python/evals/governance/run_evals.py +5 -37
  25. package/python/examples/governance_middleware_demo.py +1 -6
  26. package/python/examples/langgraph_governance_demo.py +7 -11
  27. package/python/foxclaw/__init__.py +74 -152
  28. package/python/foxclaw/agents/base.py +82 -136
  29. package/python/foxclaw/agents/impl/_helpers.py +2 -10
  30. package/python/foxclaw/agents/impl/finance.py +4 -21
  31. package/python/foxclaw/agents/impl/office.py +7 -24
  32. package/python/foxclaw/agents/impl/supervisor.py +12 -79
  33. package/python/foxclaw/agents/mixins/_context.py +0 -23
  34. package/python/foxclaw/agents/mixins/augment.py +10 -16
  35. package/python/foxclaw/agents/mixins/llm.py +7 -6
  36. package/python/foxclaw/agents/mixins/memory.py +23 -3
  37. package/python/foxclaw/agents/mixins/preference.py +58 -8
  38. package/python/foxclaw/agents/mixins/prompt.py +59 -12
  39. package/python/foxclaw/agents/mixins/skillbox.py +6 -6
  40. package/python/foxclaw/agents/mixins/tool_exec.py +16 -42
  41. package/python/foxclaw/agents/rules/finance/RULES.md +131 -0
  42. package/python/foxclaw/api/__init__.py +18 -77
  43. package/python/foxclaw/api/_state.py +43 -0
  44. package/python/foxclaw/api/bootstrap.py +114 -0
  45. package/python/foxclaw/api/middleware.py +5 -28
  46. package/python/foxclaw/api/routes/ahs.py +60 -73
  47. package/python/foxclaw/api/routes/attachments.py +5 -6
  48. package/python/foxclaw/api/routes/browser.py +7 -11
  49. package/python/foxclaw/api/routes/domain.py +18 -26
  50. package/python/foxclaw/api/routes/knowledge.py +3 -21
  51. package/python/foxclaw/api/routes/llm_admin.py +21 -33
  52. package/python/foxclaw/api/routes/memory.py +50 -12
  53. package/python/foxclaw/api/routes/orchestration.py +3 -5
  54. package/python/foxclaw/api/routes/recovery.py +4 -3
  55. package/python/foxclaw/api/routes/review.py +2 -11
  56. package/python/foxclaw/api/routes/search.py +3 -7
  57. package/python/foxclaw/api/routes/system.py +33 -49
  58. package/python/foxclaw/api/routes/task.py +31 -49
  59. package/python/foxclaw/api/routes/ws.py +2 -6
  60. package/python/foxclaw/api/spine.py +1 -4
  61. package/python/foxclaw/behavior/__init__.py +20 -0
  62. package/python/foxclaw/behavior/behavior.yaml +62 -0
  63. package/python/foxclaw/behavior/spec.py +287 -0
  64. package/python/foxclaw/browser/action/action_executor.py +1 -5
  65. package/python/foxclaw/browser/action/action_types.py +0 -1
  66. package/python/foxclaw/browser/action/url_policy.py +1 -5
  67. package/python/foxclaw/browser/adapter/web_adapter.py +1 -1
  68. package/python/foxclaw/browser/driver/playwright_driver.py +20 -16
  69. package/python/foxclaw/browser/perception/dom_extractor.py +0 -1
  70. package/python/foxclaw/browser/perception/element_indexer.py +1 -2
  71. package/python/foxclaw/browser/service.py +7 -11
  72. package/python/foxclaw/capability/embedding_service.py +4 -6
  73. package/python/foxclaw/channels/__init__.py +9 -11
  74. package/python/foxclaw/channels/discord.py +4 -13
  75. package/python/foxclaw/channels/dispatcher.py +2 -27
  76. package/python/foxclaw/channels/feishu.py +10 -20
  77. package/python/foxclaw/channels/openclaw_bridge.py +4 -9
  78. package/python/foxclaw/channels/signing.py +9 -9
  79. package/python/foxclaw/channels/telegram.py +3 -4
  80. package/python/foxclaw/channels/weixin.py +7 -17
  81. package/python/foxclaw/cli.py +42 -13
  82. package/python/foxclaw/cognitive/reasoner.py +9 -10
  83. package/python/foxclaw/cognitive/state.py +18 -24
  84. package/python/foxclaw/core/audit_chain.py +1 -3
  85. package/python/foxclaw/core/audit_sink.py +3 -3
  86. package/python/foxclaw/core/compaction.py +8 -26
  87. package/python/foxclaw/core/config.py +2 -2
  88. package/python/foxclaw/core/confirm_nonce.py +6 -12
  89. package/python/foxclaw/core/content_guard.py +341 -0
  90. package/python/foxclaw/core/envfile.py +2 -2
  91. package/python/foxclaw/core/failure_policy.py +17 -26
  92. package/python/foxclaw/core/gateway.py +6 -10
  93. package/python/foxclaw/core/governance_audit.py +3 -7
  94. package/python/foxclaw/core/idempotency.py +15 -22
  95. package/python/foxclaw/core/injection_guard.py +10 -29
  96. package/python/foxclaw/core/memory_adapter.py +53 -60
  97. package/python/foxclaw/core/metrics.py +4 -5
  98. package/python/foxclaw/core/model_router.py +74 -150
  99. package/python/foxclaw/core/paths.py +1 -1
  100. package/python/foxclaw/core/recovery.py +21 -34
  101. package/python/foxclaw/core/scheduler.py +23 -26
  102. package/python/foxclaw/core/secrets.py +0 -3
  103. package/python/foxclaw/core/sensitive_write.py +283 -0
  104. package/python/foxclaw/core/sessions.py +4 -6
  105. package/python/foxclaw/core/settings.py +16 -32
  106. package/python/foxclaw/core/shared_write.py +11 -23
  107. package/python/foxclaw/core/steering_queue.py +12 -11
  108. package/python/foxclaw/core/system_bus.py +1 -1
  109. package/python/foxclaw/core/thought_chain.py +30 -32
  110. package/python/foxclaw/core/token_meter.py +9 -13
  111. package/python/foxclaw/core/tools.py +4 -12
  112. package/python/foxclaw/core/tracing.py +25 -40
  113. package/python/foxclaw/core/truth_pipeline.py +5 -9
  114. package/python/foxclaw/core/update_check.py +149 -0
  115. package/python/foxclaw/core/vec_recall_adapter.py +11 -37
  116. package/python/foxclaw/core/verify.py +6 -14
  117. package/python/foxclaw/core/web_search.py +4 -28
  118. package/python/foxclaw/finance/evidence.py +14 -18
  119. package/python/foxclaw/finance/fx.py +1 -3
  120. package/python/foxclaw/finance/graph.py +9 -18
  121. package/python/foxclaw/finance/models.py +20 -24
  122. package/python/foxclaw/finance/nlparse.py +12 -72
  123. package/python/foxclaw/finance/policy.py +8 -36
  124. package/python/foxclaw/finance/reasoner.py +3 -11
  125. package/python/foxclaw/finance/reasoning.py +13 -34
  126. package/python/foxclaw/finance/reconcile.py +5 -10
  127. package/python/foxclaw/finance/runtime.py +19 -46
  128. package/python/foxclaw/governance/middleware.py +13 -21
  129. package/python/foxclaw/harness/_base.py +2 -8
  130. package/python/foxclaw/harness/_policy.py +19 -38
  131. package/python/foxclaw/harness/drift.py +1 -13
  132. package/python/foxclaw/harness/event_bus.py +2 -5
  133. package/python/foxclaw/harness/harness.py +9 -97
  134. package/python/foxclaw/harness/loop_detection.py +11 -18
  135. package/python/foxclaw/itinerary/amap_client.py +2 -5
  136. package/python/foxclaw/itinerary/models.py +7 -7
  137. package/python/foxclaw/itinerary/reasoner.py +1 -6
  138. package/python/foxclaw/itinerary/reasoning.py +15 -21
  139. package/python/foxclaw/itinerary/runtime.py +8 -12
  140. package/python/foxclaw/kernel/capabilities.py +5 -6
  141. package/python/foxclaw/kernel/policy_gate.py +4 -10
  142. package/python/foxclaw/kernel/process_manager.py +17 -36
  143. package/python/foxclaw/kernel/scheduler.py +3 -9
  144. package/python/foxclaw/kernel/sentinel.py +13 -23
  145. package/python/foxclaw/kernel/sentinel_client.py +12 -15
  146. package/python/foxclaw/kernel/spine.py +9 -9
  147. package/python/foxclaw/knowledge/__init__.py +4 -20
  148. package/python/foxclaw/knowledge/truth_crystallizer.py +5 -8
  149. package/python/foxclaw/learning/knowledge_graph.py +4 -9
  150. package/python/foxclaw/learning/models.py +13 -13
  151. package/python/foxclaw/learning/reasoner.py +1 -4
  152. package/python/foxclaw/learning/runtime.py +19 -31
  153. package/python/foxclaw/learning/spaced_repetition.py +2 -7
  154. package/python/foxclaw/mcp_server.py +2 -3
  155. package/python/foxclaw/orchestration/__init__.py +16 -33
  156. package/python/foxclaw/pipeline/cognitive_pipeline.py +45 -97
  157. package/python/foxclaw/pipeline/stage_meta.py +21 -0
  158. package/python/foxclaw/pipeline/supervisor_stages.py +13 -42
  159. package/python/foxclaw/planning/llm_planner.py +3 -3
  160. package/python/foxclaw/planning/planner.py +6 -11
  161. package/python/foxclaw/search/agent_port.py +7 -11
  162. package/python/foxclaw/search/evidence.py +0 -4
  163. package/python/foxclaw/search/internal_search.py +3 -5
  164. package/python/foxclaw/search/models.py +11 -13
  165. package/python/foxclaw/search/planner.py +3 -3
  166. package/python/foxclaw/search/retriever.py +1 -1
  167. package/python/foxclaw/search/runtime.py +1 -8
  168. package/python/foxclaw/skills/__init__.py +46 -100
  169. package/python/foxclaw/skills/archive.py +111 -0
  170. package/python/foxclaw/skills/fetch.py +226 -0
  171. package/python/foxclaw/skills/hooks_spec.py +21 -25
  172. package/python/foxclaw/skills/install_cmd.py +216 -0
  173. package/python/foxclaw/skills/lifecycle.py +16 -42
  174. package/python/foxclaw/skills/markdown_skill.py +10 -9
  175. package/python/foxclaw/skills/mcp_client.py +5 -11
  176. package/python/foxclaw/skills/mcp_store.py +0 -3
  177. package/python/foxclaw/skills/permission.py +13 -48
  178. package/python/foxclaw/skills/pipeline.py +21 -53
  179. package/python/foxclaw/skills/policy.py +4 -10
  180. package/python/foxclaw/skills/review.py +3 -3
  181. package/python/frontend/deck/auth.js +0 -7
  182. package/python/frontend/deck/chat_model_picker.js +0 -54
  183. package/python/frontend/deck/domain.js +0 -20
  184. package/python/frontend/deck/main.js +0 -144
  185. package/python/frontend/deck/markdown.js +0 -4
  186. package/python/frontend/deck/settings.js +1 -18
  187. package/python/frontend/deck/state.js +0 -1
  188. package/python/frontend/deck/util.js +0 -2
  189. package/python/frontend/deck/visuals.js +0 -22
  190. package/python/frontend/planet/planet.js +18 -201
  191. package/python/frontend/v3/assets/index-JKfHlb-a.css +1 -0
  192. package/python/frontend/v3/assets/index-tN90dpUH.js +34 -0
  193. package/python/frontend/v3/index.html +3 -3
  194. package/python/frontend/widget_runtime.js +0 -43
  195. package/python/frontend/widgets.js +0 -57
  196. package/python/frontend_src/index.html +1 -1
  197. package/python/frontend_src/package-lock.json +2 -2
  198. package/python/frontend_src/package.json +1 -1
  199. package/python/frontend_src/src/App.vue +3 -7
  200. package/python/frontend_src/src/api.js +0 -7
  201. package/python/frontend_src/src/components/ChatDock.vue +27 -39
  202. package/python/frontend_src/src/components/DocViewer.vue +0 -9
  203. package/python/frontend_src/src/components/Inspector.vue +0 -6
  204. package/python/frontend_src/src/components/LeftRail.vue +60 -39
  205. package/python/frontend_src/src/components/MenuPopover.vue +9 -11
  206. package/python/frontend_src/src/components/MessageStream.vue +18 -30
  207. package/python/frontend_src/src/components/ModelPicker.vue +0 -5
  208. package/python/frontend_src/src/components/QuickIngest.vue +0 -6
  209. package/python/frontend_src/src/components/Stage.vue +71 -25
  210. package/python/frontend_src/src/components/ToolMenu.vue +0 -3
  211. package/python/frontend_src/src/components/TopBar.vue +38 -10
  212. package/python/frontend_src/src/components/panels/ActivityPanel.vue +0 -2
  213. package/python/frontend_src/src/components/panels/AgentsPanel.vue +2 -5
  214. package/python/frontend_src/src/components/panels/KnowledgePanel.vue +0 -16
  215. package/python/frontend_src/src/components/panels/MemoryPanel.vue +42 -3
  216. package/python/frontend_src/src/components/panels/PanelBox.vue +6 -7
  217. package/python/frontend_src/src/components/panels/ReviewPanel.vue +0 -16
  218. package/python/frontend_src/src/components/panels/SettingsPanel.vue +142 -38
  219. package/python/frontend_src/src/components/panels/TasksPanel.vue +0 -1
  220. package/python/frontend_src/src/components/panels/ToolsPanel.vue +125 -19
  221. package/python/frontend_src/src/lib/fmt.js +0 -4
  222. package/python/frontend_src/src/lib/i18n.js +53 -0
  223. package/python/frontend_src/src/lib/locales/en.js +173 -0
  224. package/python/frontend_src/src/lib/locales/zh.js +174 -0
  225. package/python/frontend_src/src/lib/md.js +0 -14
  226. package/python/frontend_src/src/main.js +2 -0
  227. package/python/frontend_src/src/stores/app.js +17 -32
  228. package/python/frontend_src/src/stores/chat.js +0 -34
  229. package/python/frontend_src/src/stores/model.js +0 -8
  230. package/python/frontend_src/src/stores/panels.js +16 -44
  231. package/python/frontend_src/src/stores/profile.js +31 -0
  232. package/python/frontend_src/src/stores/system.js +25 -0
  233. package/python/frontend_src/vite.config.js +0 -7
  234. package/python/pyproject.toml +17 -3
  235. package/python/requirements.txt +311 -0
  236. package/python/tests/conftest.py +0 -22
  237. package/python/tests/run_all.py +4 -66
  238. package/python/tests/test_agent_lifecycle.py +4 -38
  239. package/python/tests/test_agent_rules.py +81 -0
  240. package/python/tests/test_ahs_p2.py +4 -25
  241. package/python/tests/test_anthropic_tools.py +1 -15
  242. package/python/tests/test_api.py +19 -9
  243. package/python/tests/test_api_auth.py +1 -5
  244. package/python/tests/test_attachments.py +9 -25
  245. package/python/tests/test_audit_chain.py +2 -7
  246. package/python/tests/test_audit_sink.py +0 -6
  247. package/python/tests/test_behavior_spec.py +198 -0
  248. package/python/tests/test_browser.py +2 -6
  249. package/python/tests/test_browser_request_guard.py +0 -4
  250. package/python/tests/test_capabilities.py +0 -6
  251. package/python/tests/test_channel_signing.py +2 -9
  252. package/python/tests/test_channels.py +1 -11
  253. package/python/tests/test_config_layering.py +0 -4
  254. package/python/tests/test_confirm_nonce.py +1 -12
  255. package/python/tests/test_conflict_dismiss.py +143 -0
  256. package/python/tests/test_content_guard.py +177 -0
  257. package/python/tests/test_context_compaction_wiring.py +1 -7
  258. package/python/tests/test_dispatch.py +0 -6
  259. package/python/tests/test_domain_evals.py +11 -80
  260. package/python/tests/test_domain_import.py +0 -6
  261. package/python/tests/test_domain_reasoners.py +0 -14
  262. package/python/tests/test_e2e.py +0 -17
  263. package/python/tests/test_emb_cache.py +7 -17
  264. package/python/tests/test_embedding_singleton.py +1 -3
  265. package/python/tests/test_evals_smoke.py +0 -24
  266. package/python/tests/test_event_spine.py +10 -18
  267. package/python/tests/test_evidence_semantic.py +1 -14
  268. package/python/tests/test_evidence_sse.py +3 -6
  269. package/python/tests/test_file_ingest.py +0 -11
  270. package/python/tests/test_finance.py +0 -9
  271. package/python/tests/test_finance_ledger.py +5 -12
  272. package/python/tests/test_finance_persist.py +8 -12
  273. package/python/tests/test_gateway.py +3 -22
  274. package/python/tests/test_governance.py +5 -52
  275. package/python/tests/test_governance_audit.py +4 -39
  276. package/python/tests/test_governance_middleware.py +1 -7
  277. package/python/tests/test_hkc.py +6 -24
  278. package/python/tests/test_hkc_index_build_manager.py +0 -33
  279. package/python/tests/test_hkc_manifest_reuse.py +4 -27
  280. package/python/tests/test_hkc_search_event_sync.py +3 -54
  281. package/python/tests/test_import_path_guard.py +0 -6
  282. package/python/tests/test_injection_guard.py +18 -36
  283. package/python/tests/test_integration.py +5 -29
  284. package/python/tests/test_itinerary.py +0 -3
  285. package/python/tests/test_kee_semantic.py +0 -5
  286. package/python/tests/test_kernel.py +1 -11
  287. package/python/tests/test_learning.py +3 -12
  288. package/python/tests/test_llm_admin.py +4 -19
  289. package/python/tests/test_llm_planner.py +1 -23
  290. package/python/tests/test_markdown_skills.py +3 -14
  291. package/python/tests/test_markdown_skills_openclaw.py +4 -9
  292. package/python/tests/test_mcp_connectors.py +3 -15
  293. package/python/tests/test_mcp_guardian_discovery.py +2 -6
  294. package/python/tests/test_mcp_no_memory.py +3 -3
  295. package/python/tests/test_mcp_policy_gate.py +0 -1
  296. package/python/tests/test_mcp_server.py +6 -9
  297. package/python/tests/test_memory_recall.py +1 -4
  298. package/python/tests/test_metrics.py +0 -8
  299. package/python/tests/test_multi_chain.py +1 -13
  300. package/python/tests/test_multimodal.py +5 -18
  301. package/python/tests/test_negotiation.py +3 -13
  302. package/python/tests/test_orchestration.py +1 -11
  303. package/python/tests/test_orchestration_decoupled.py +0 -6
  304. package/python/tests/test_p3_p5_hardening.py +3 -17
  305. package/python/tests/test_parallel_dispatch.py +4 -12
  306. package/python/tests/test_pipeline.py +17 -58
  307. package/python/tests/test_plan_visible.py +4 -4
  308. package/python/tests/test_planner.py +0 -7
  309. package/python/tests/test_policy_gate.py +2 -3
  310. package/python/tests/test_profile.py +67 -0
  311. package/python/tests/test_readme_claims.py +163 -0
  312. package/python/tests/test_reasoner.py +2 -17
  313. package/python/tests/test_release_check.py +2 -13
  314. package/python/tests/test_resume_checkpoint.py +7 -15
  315. package/python/tests/test_routing.py +2 -12
  316. package/python/tests/test_scheduler.py +7 -10
  317. package/python/tests/test_search.py +1 -11
  318. package/python/tests/test_secrets.py +4 -10
  319. package/python/tests/test_semantic.py +1 -10
  320. package/python/tests/test_sensitive_write.py +181 -0
  321. package/python/tests/test_sessions.py +3 -4
  322. package/python/tests/test_settings.py +2 -18
  323. package/python/tests/test_shadow_learning.py +3 -43
  324. package/python/tests/test_shared_write.py +0 -7
  325. package/python/tests/test_skill_taint_gate.py +1 -10
  326. package/python/tests/test_skills.py +2 -18
  327. package/python/tests/test_skills_fetch.py +282 -0
  328. package/python/tests/test_stats_and_events.py +2 -5
  329. package/python/tests/test_streaming.py +2 -7
  330. package/python/tests/test_supervisor_pipeline.py +4 -5
  331. package/python/tests/test_supervisor_triage.py +4 -10
  332. package/python/tests/test_taint.py +2 -7
  333. package/python/tests/test_task_stream.py +1 -12
  334. package/python/tests/test_thinking_sse.py +0 -1
  335. package/python/tests/test_tool_center.py +1 -17
  336. package/python/tests/test_tool_parallel.py +2 -2
  337. package/python/tests/test_tool_params.py +1 -5
  338. package/python/tests/test_tool_use.py +0 -13
  339. package/python/tests/test_toplevel_concurrency.py +3 -4
  340. package/python/tests/test_tracing.py +7 -41
  341. package/python/tests/test_truth_crystallizer.py +4 -6
  342. package/python/tests/test_truth_flow.py +2 -7
  343. package/python/tests/test_update_check.py +95 -0
  344. package/python/tests/test_verify_evidence.py +8 -21
  345. package/python/tests/test_version_sync.py +35 -6
  346. package/python/tests/test_web_trigger.py +0 -17
  347. package/python/tools/build_content_lexicon.py +218 -0
  348. package/python/tools/bundle_web.py +98 -0
  349. package/python/tools/check_faiss_type.py +0 -1
  350. package/python/tools/check_log.ps1 +0 -8
  351. package/python/tools/mypy_baseline.txt +12 -4
  352. package/python/tools/mypy_ratchet.py +52 -6
  353. package/python/tools/pack.py +0 -5
  354. package/python/tools/recheck_conflicts.py +156 -0
  355. package/python/tools/release_check.py +24 -15
  356. package/python/tools/restart_foxclaw.ps1 +5 -8
  357. package/python/tools/shared_write_audit.py +1 -15
  358. package/python/tools/size_check.py +1 -1
  359. package/python/tools/strip_comments.py +228 -0
  360. package/python/vendor/hkc/hkc_api/routes/ability.py +31 -0
  361. package/python/vendor/hkc/hkc_api/schemas.py +11 -0
  362. package/python/vendor/hkc/hkc_kee/kee.py +124 -3
  363. package/python/vendor/hkc/hkc_sdk/base.py +22 -0
  364. package/python/vendor/hkc/hkc_sdk/direct_client.py +10 -0
  365. package/python/vendor/hkc/hkc_sdk/http_client.py +19 -0
  366. package/python/foxclaw/api/app.py +0 -123
  367. package/python/frontend/v3/assets/index-BD7VPbMV.js +0 -33
  368. package/python/frontend/v3/assets/index-CAvnYWGH.css +0 -1
@@ -18,20 +18,18 @@ Foxclaw Channels — Discord 适配器
18
18
  """
19
19
  from __future__ import annotations
20
20
  import json
21
- from typing import Optional
21
+ from typing import Any, Optional
22
22
  from . import ChannelAdapter, InboundMessage, OutboundMessage, SendResult
23
23
 
24
24
  DISCORD_API_BASE = "https://discord.com/api/v10"
25
25
  DISCORD_GATEWAY = "wss://gateway.discord.gg/?v=10&encoding=json"
26
26
 
27
- # Gateway opcodes
28
27
  OP_DISPATCH = 0
29
28
  OP_HEARTBEAT = 1
30
29
  OP_IDENTIFY = 2
31
30
  OP_HELLO = 10
32
31
  OP_HEARTBEAT_ACK = 11
33
32
 
34
- # Intents(收消息内容需要 GUILD_MESSAGES + MESSAGE_CONTENT)
35
33
  INTENT_GUILD_MESSAGES = 1 << 9
36
34
  INTENT_DIRECT_MESSAGES = 1 << 12
37
35
  INTENT_MESSAGE_CONTENT = 1 << 15
@@ -47,20 +45,18 @@ class DiscordAdapter(ChannelAdapter):
47
45
  self.bot_token = bot_token
48
46
  self.intents = intents
49
47
  self.timeout = timeout
50
- self._bot_user_id = "" # 自己的 bot user id(用于忽略自己的消息)
48
+ self._bot_user_id = ""
51
49
 
52
50
  def _headers(self) -> dict:
53
51
  return {"Authorization": f"Bot {self.bot_token}",
54
52
  "Content-Type": "application/json",
55
53
  "User-Agent": "Foxclaw (https://github.com, 1.0)"}
56
54
 
57
- # ── 入站:标准化 Gateway MESSAGE_CREATE 事件 ───────────
58
55
  def normalize(self, raw: dict) -> Optional[InboundMessage]:
59
56
  """Discord Gateway 事件 → 标准 InboundMessage。
60
57
 
61
58
  raw 可以是完整 Gateway 帧({op,t,d})或直接是 MESSAGE_CREATE 的 data。
62
59
  """
63
- # 取出 data 部分
64
60
  if "op" in raw:
65
61
  if raw.get("op") != OP_DISPATCH or raw.get("t") != "MESSAGE_CREATE":
66
62
  return None
@@ -70,15 +66,13 @@ class DiscordAdapter(ChannelAdapter):
70
66
  if not data:
71
67
  return None
72
68
  author = data.get("author", {})
73
- # 忽略机器人消息(含自己),避免自循环
74
69
  if author.get("bot"):
75
70
  return None
76
71
  if self._bot_user_id and str(author.get("id")) == self._bot_user_id:
77
72
  return None
78
73
  text = data.get("content", "")
79
74
  if not text:
80
- return None # 非文本(纯附件/嵌入)暂忽略
81
- # 有 guild_id = 服务器频道(群),无 = 私信
75
+ return None
82
76
  is_group = bool(data.get("guild_id"))
83
77
  return InboundMessage(
84
78
  channel=self.CHANNEL,
@@ -90,7 +84,6 @@ class DiscordAdapter(ChannelAdapter):
90
84
  is_group=is_group,
91
85
  raw=data)
92
86
 
93
- # ── 出站:REST 发送 ────────────────────────────────────
94
87
  def send(self, msg: OutboundMessage) -> SendResult:
95
88
  """POST /channels/{channel_id}/messages 发文本。"""
96
89
  try:
@@ -99,7 +92,7 @@ class DiscordAdapter(ChannelAdapter):
99
92
  return SendResult(False, error="缺少 httpx 依赖")
100
93
  if not self.bot_token:
101
94
  return SendResult(False, error="未配置 Discord bot token")
102
- payload = {"content": msg.text}
95
+ payload: dict[str, Any] = {"content": msg.text}
103
96
  if msg.reply_to:
104
97
  payload["message_reference"] = {"message_id": msg.reply_to}
105
98
  url = f"{DISCORD_API_BASE}/channels/{msg.conversation_id}/messages"
@@ -156,10 +149,8 @@ class DiscordAdapter(ChannelAdapter):
156
149
  raise RuntimeError("需要 websockets 库:pip install websockets")
157
150
  import asyncio
158
151
  async with websockets.connect(DISCORD_GATEWAY, max_size=None) as ws:
159
- # 1. HELLO:拿心跳间隔
160
152
  hello = json.loads(await ws.recv())
161
153
  heartbeat_ms = hello["d"]["heartbeat_interval"]
162
- # 2. IDENTIFY
163
154
  await ws.send(json.dumps(self.build_identify()))
164
155
  self._running = True
165
156
 
@@ -18,7 +18,7 @@ from . import InboundMessage, ChannelRegistry, ChannelAdapter
18
18
  class ChannelDispatcher:
19
19
  """渠道闭环:把入站消息路由到主管,回复发回原渠道。"""
20
20
 
21
- def __init__(self, supervisor, registry: ChannelRegistry = None,
21
+ def __init__(self, supervisor, registry: ChannelRegistry | None = None,
22
22
  idempotency=None):
23
23
  """
24
24
  supervisor: 主管 Agent(有 async run_task 方法)
@@ -27,7 +27,7 @@ class ChannelDispatcher:
27
27
  """
28
28
  self.supervisor = supervisor
29
29
  self.registry = registry or ChannelRegistry()
30
- self._pending: set = set() # 持有运行中的 task 引用,防止被 GC 回收
30
+ self._pending: set = set()
31
31
  if idempotency is None:
32
32
  from ..core.idempotency import IdempotencyGuard
33
33
  idempotency = IdempotencyGuard(ttl_seconds=3600)
@@ -61,23 +61,18 @@ class ChannelDispatcher:
61
61
  running = None
62
62
 
63
63
  if running is not None:
64
- # 在异步环境:发任务 + 持有引用(防止无引用 task 被 GC 静默回收)
65
64
  task = running.create_task(self.handle(msg))
66
65
  self._pending.add(task)
67
66
  task.add_done_callback(self._pending.discard)
68
67
  else:
69
- # 同步环境(轮询渠道):跑到完成,调用方得到完整处理结果
70
68
  asyncio.run(self.handle(msg))
71
69
 
72
70
  async def handle(self, msg: InboundMessage) -> dict:
73
71
  """处理一条入站消息:主管思考 → 回复原渠道。返回处理结果。"""
74
- # 幂等检查:同一渠道+消息ID只处理一次(防平台重发/轮询游标边界导致重复回复)
75
72
  from ..core.idempotency import make_key
76
73
  if msg.message_id:
77
74
  idem_key = make_key("message", msg.channel, msg.message_id, msg.sender_id)
78
75
  else:
79
- # 无稳定 message_id 的渠道/消息:用 渠道+发送者+内容+时间窗(10秒)
80
- # 算 fallback key——退化但仍能挡住短时间内的重发,不留盲区。
81
76
  import time as _t
82
77
  window = int(_t.time() // 10)
83
78
  idem_key = make_key("message_fallback", msg.channel, msg.sender_id,
@@ -86,7 +81,6 @@ class ChannelDispatcher:
86
81
  return {"handled": True, "replied": False, "reason": "重复消息,已跳过",
87
82
  "idempotent_skip": True}
88
83
 
89
- # 交给主管处理,带上渠道上下文(便于主管知道来源)
90
84
  context = {
91
85
  "channel": msg.channel,
92
86
  "session_id": self._session_key(msg),
@@ -94,14 +88,7 @@ class ChannelDispatcher:
94
88
  "sender_name": msg.sender_name,
95
89
  "is_group": msg.is_group,
96
90
  }
97
- # 渠道入站文本来自外部(Telegram/Discord/…),默认按**不可信数据**处理:
98
- # 此前 msg.text 原样当任务喂主管,等于把任意外部聊天用户当指令源(无发送者
99
- # 白名单、无 <untrusted> 包裹)。可信发送者用 FOXCLAW_CHANNEL_TRUSTED_SENDERS
100
- # 显式声明,逗号分隔,支持 `sender_id` 或 `channel:sender_id`,`*` 为该渠道全信任。
101
91
  context["sender_trusted"] = self._sender_trusted(msg)
102
- # 下传给权限判定:不可信来源驱动的任务里,写/发送/外发动作一律按「已接触敏感数据」
103
- # 处理(见 agents/_context._untrusted_get → tool_exec._guard_tool / skillbox)。
104
- # 只影响带此键的入口(渠道),本地 HTTP 对话不带 → 行为不变。
105
92
  context["untrusted_source"] = not context["sender_trusted"]
106
93
  text = msg.text
107
94
  if not context["sender_trusted"]:
@@ -114,12 +101,10 @@ class ChannelDispatcher:
114
101
  except Exception as e:
115
102
  result = {"error": f"主管处理失败: {e}"}
116
103
 
117
- # 提取回复文本
118
104
  reply_text = self._extract_reply(result)
119
105
  if not reply_text:
120
106
  return {"handled": True, "replied": False, "reason": "无回复内容"}
121
107
 
122
- # 通过原渠道回复
123
108
  adapter = self.registry.get(msg.channel)
124
109
  if adapter is None:
125
110
  return {"handled": True, "replied": False, "reason": f"渠道 {msg.channel} 未注册"}
@@ -141,9 +126,6 @@ class ChannelDispatcher:
141
126
  return False
142
127
  sid = str(getattr(msg, "sender_id", "") or "")
143
128
  chan = str(getattr(msg, "channel", "") or "")
144
- # 裸 `*` 曾等于「所有渠道的所有发送者都可信」——一次手滑就把任意外部聊天用户提成
145
- # 指令源,而列表里看不出它比 channel:sender_id 宽多少。现在必须二选一:
146
- # 写 `channel:*`(限定渠道,推荐),或用 FOXCLAW_CHANNEL_TRUST_ALL=1 显式确认全局信任。
147
129
  trust_all = _os.environ.get("FOXCLAW_CHANNEL_TRUST_ALL", "").strip() in ("1", "true", "True")
148
130
  for item in (x.strip() for x in raw.split(",")):
149
131
  if not item:
@@ -169,16 +151,12 @@ class ChannelDispatcher:
169
151
  return str(result) if result else ""
170
152
 
171
153
  inner = result.get("result")
172
- # 情况1:result 直接是字符串
173
154
  if isinstance(inner, str) and inner.strip():
174
155
  return inner
175
- # 情况2:result 是嵌套字典(主管分诊结果)
176
156
  if isinstance(inner, dict):
177
- # 单 Agent:sub_result
178
157
  sub = inner.get("sub_result")
179
158
  if isinstance(sub, str) and sub.strip():
180
159
  return sub
181
- # 多 Agent:拼接每步输出
182
160
  steps = inner.get("multi_steps")
183
161
  if isinstance(steps, list) and steps:
184
162
  parts = []
@@ -188,17 +166,14 @@ class ChannelDispatcher:
188
166
  parts.append(str(out))
189
167
  if parts:
190
168
  return "\n\n".join(parts)
191
- # 元问题自答等:answer / summary
192
169
  for k in ("answer", "summary", "response"):
193
170
  v = inner.get(k)
194
171
  if isinstance(v, str) and v.strip():
195
172
  return v
196
- # 情况3:顶层的 summary/response/output
197
173
  for key in ("summary", "response", "output"):
198
174
  val = result.get(key)
199
175
  if isinstance(val, str) and val.strip():
200
176
  return val
201
- # 出错兜底
202
177
  if result.get("error"):
203
178
  return f"抱歉,处理时遇到问题:{result['error']}"
204
179
  return ""
@@ -30,15 +30,14 @@ class FeishuAdapter(ChannelAdapter):
30
30
  super().__init__(on_message=on_message)
31
31
  self.app_id = app_id
32
32
  self.app_secret = app_secret
33
- self.verification_token = verification_token # webhook 模式校验
34
- self.encrypt_key = encrypt_key # 若开启加密
33
+ self.verification_token = verification_token
34
+ self.encrypt_key = encrypt_key
35
35
  self.base = FEISHU_BASE if domain == "feishu" else LARK_BASE
36
36
  self.timeout = timeout
37
- self._token = "" # 缓存的 tenant_access_token
38
- self._token_exp = 0.0 # 过期时间戳
39
- self._last_error = "" # 最近一次解密/处理错误(便于排查)
37
+ self._token = ""
38
+ self._token_exp = 0.0
39
+ self._last_error = ""
40
40
 
41
- # ── 入站:事件订阅 ──────────────────────────────────────
42
41
  def decrypt_event(self, encrypt: str) -> Optional[dict]:
43
42
  """解密飞书加密事件(AES-256-CBC,key=sha256(encrypt_key))。
44
43
 
@@ -60,7 +59,6 @@ class FeishuAdapter(ChannelAdapter):
60
59
  plain = self._aes_cbc_decrypt(key, iv, ciphertext)
61
60
  if plain is None:
62
61
  return None
63
- # PKCS7 去 padding(带校验,防损坏数据)
64
62
  if not plain:
65
63
  return None
66
64
  pad = plain[-1]
@@ -76,7 +74,6 @@ class FeishuAdapter(ChannelAdapter):
76
74
 
77
75
  def _aes_cbc_decrypt(self, key: bytes, iv: bytes, ciphertext: bytes) -> Optional[bytes]:
78
76
  """AES-256-CBC 解密。优先用 cryptography,回退 pycryptodome。"""
79
- # 优先 cryptography(更常见)
80
77
  try:
81
78
  from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes
82
79
  cipher = Cipher(algorithms.AES(key), modes.CBC(iv))
@@ -85,7 +82,7 @@ class FeishuAdapter(ChannelAdapter):
85
82
  except ImportError:
86
83
  pass
87
84
  try:
88
- from Crypto.Cipher import AES # pycryptodome
85
+ from Crypto.Cipher import AES
89
86
  return AES.new(key, AES.MODE_CBC, iv).decrypt(ciphertext)
90
87
  except ImportError:
91
88
  self._last_error = ("缺少 AES 库,无法解密飞书加密事件。"
@@ -99,12 +96,12 @@ class FeishuAdapter(ChannelAdapter):
99
96
  signature: str) -> bool:
100
97
  """验签:sha256(timestamp + nonce + encrypt_key + body)。"""
101
98
  if not self.encrypt_key:
102
- return True # 未开启加密则不验签(飞书此时也不发签名)
99
+ return True
103
100
  content = (timestamp + nonce + self.encrypt_key).encode() + body
104
101
  digest = hashlib.sha256(content).hexdigest()
105
102
  return digest == signature
106
103
 
107
- def handle_event(self, payload: dict, headers: dict = None,
104
+ def handle_event(self, payload: dict, headers: dict | None = None,
108
105
  raw_body: bytes = b"") -> dict:
109
106
  """处理飞书事件回调入口。返回应答给飞书的 dict。
110
107
 
@@ -114,23 +111,19 @@ class FeishuAdapter(ChannelAdapter):
114
111
  - 验签 → normalize → on_message
115
112
  """
116
113
  headers = headers or {}
117
- # 1. 加密:先解密外层
118
114
  if "encrypt" in payload:
119
115
  dec = self.decrypt_event(payload["encrypt"])
120
116
  if dec is None:
121
117
  return {"error": "decrypt_failed"}
122
118
  payload = dec
123
- # 2. URL 验证(首次配置回调地址时)
124
119
  if payload.get("type") == "url_verification":
125
120
  return {"challenge": payload.get("challenge", "")}
126
- # 3. 验签(若有签名头)
127
121
  sig = headers.get("X-Lark-Signature") or headers.get("x-lark-signature")
128
122
  if sig:
129
123
  ts = headers.get("X-Lark-Request-Timestamp", "")
130
124
  nonce = headers.get("X-Lark-Request-Nonce", "")
131
125
  if not self.verify_signature(ts, nonce, raw_body, sig):
132
126
  return {"error": "invalid_signature"}
133
- # 4. 消息事件 → normalize → 回调
134
127
  inbound = self.normalize(payload)
135
128
  if inbound and self.on_message:
136
129
  self.on_message(inbound)
@@ -145,7 +138,6 @@ class FeishuAdapter(ChannelAdapter):
145
138
  message = event.get("message", {})
146
139
  sender = event.get("sender", {})
147
140
  sender_sid = sender.get("sender_id", {})
148
- # 飞书消息内容是 JSON 字符串,文本在 {"text": "..."}
149
141
  text = ""
150
142
  try:
151
143
  import json
@@ -155,19 +147,18 @@ class FeishuAdapter(ChannelAdapter):
155
147
  pass
156
148
  if not text:
157
149
  return None
158
- chat_type = message.get("chat_type", "p2p") # p2p=私聊, group=群
150
+ chat_type = message.get("chat_type", "p2p")
159
151
  return InboundMessage(
160
152
  channel=self.CHANNEL,
161
153
  message_id=message.get("message_id", ""),
162
154
  text=text,
163
155
  sender_id=sender_sid.get("open_id", ""),
164
- sender_name="", # 飞书需额外调 API 取名,默认留空(可选 resolveSenderNames)
156
+ sender_name="",
165
157
  conversation_id=message.get("chat_id", ""),
166
158
  is_group=(chat_type == "group"),
167
159
  timestamp=float(message.get("create_time", 0)) / 1000 or time.time(),
168
160
  raw=raw)
169
161
 
170
- # ── 出站:tenant_access_token + 发消息 ──────────────────
171
162
  def _get_token(self) -> str:
172
163
  """取 tenant_access_token,带缓存与自动刷新(过期前 5 分钟刷新)。"""
173
164
  if self._token and time.time() < self._token_exp - 300:
@@ -197,7 +188,6 @@ class FeishuAdapter(ChannelAdapter):
197
188
  token = self._get_token()
198
189
  if not token:
199
190
  return SendResult(False, error="获取 tenant_access_token 失败")
200
- # receive_id_type 用 chat_id(群和私聊的会话 ID 都用 chat_id 发)
201
191
  payload = {
202
192
  "receive_id": msg.conversation_id,
203
193
  "msg_type": "text",
@@ -47,13 +47,12 @@ class OpenClawBridgeAdapter(ChannelAdapter):
47
47
  关键:保留 OpenClaw 侧的 channel(真实渠道,如 wechat)和 conversation 引用,
48
48
  以便回复时让 OpenClaw 发回正确的原渠道。
49
49
  """
50
- # OpenClaw 消息常见结构:{channel, message:{id,text}, sender:{id,name}, conversation:{id,isGroup}}
51
50
  msg = raw.get("message") or raw
52
51
  if not isinstance(msg, dict):
53
52
  return None
54
53
  text = msg.get("text") or msg.get("content") or ""
55
54
  if not text:
56
- return None # 非文本事件忽略
55
+ return None
57
56
  sender = raw.get("sender") or msg.get("sender") or {}
58
57
  conv = raw.get("conversation") or msg.get("conversation") or {}
59
58
  return InboundMessage(
@@ -64,7 +63,7 @@ class OpenClawBridgeAdapter(ChannelAdapter):
64
63
  sender_name=str(sender.get("name") or sender.get("username") or ""),
65
64
  conversation_id=str(conv.get("id") or conv.get("conversation_id") or ""),
66
65
  is_group=bool(conv.get("isGroup") or conv.get("is_group") or False),
67
- raw=raw) # 原始 payload 留底(含 upstream channel 等回发所需信息)
66
+ raw=raw)
68
67
 
69
68
  def _upstream_channel(self, inbound_raw: dict) -> str:
70
69
  return (inbound_raw.get("channel")
@@ -86,21 +85,17 @@ class OpenClawBridgeAdapter(ChannelAdapter):
86
85
  }
87
86
  if msg.reply_to:
88
87
  payload["reply_to"] = msg.reply_to
89
- # 若上层指明了真实渠道(从入站 raw 透传),带上,确保发回正确渠道
90
88
  if msg.extra.get("upstream_channel"):
91
89
  payload["channel"] = msg.extra["upstream_channel"]
92
90
  headers = {}
93
91
  if self.api_token:
94
92
  headers["Authorization"] = f"Bearer {self.api_token}"
95
- # 先序列化成 bytes 再发:签名必须覆盖**实际发出去的字节**,否则网关侧
96
- # 重新序列化(键序/转义差异)会导致验签失败。content= 而非 json=,
97
- # 就是为了保证"签的就是发的"。
98
93
  try:
99
94
  body = json.dumps(payload, ensure_ascii=False,
100
95
  sort_keys=True).encode("utf-8")
101
96
  except Exception as e:
102
97
  return SendResult(False, error=f"出站 payload 序列化失败: {e}")
103
- headers["Content-Type"] = "application/json" # content= 不走 httpx 的 json= 推断
98
+ headers["Content-Type"] = "application/json"
104
99
  if self.signer is not None and self.signer.enabled:
105
100
  try:
106
101
  headers.update(self.signer.headers(body))
@@ -130,7 +125,7 @@ class OpenClawBridgeAdapter(ChannelAdapter):
130
125
 
131
126
  def verify_webhook(self, headers, body: bytes,
132
127
  tolerance: float = _signing.DEFAULT_TOLERANCE,
133
- replay: "_signing.ReplayGuard" = None):
128
+ replay: "_signing.ReplayGuard | None" = None):
134
129
  """验 OpenClaw 转发来的 webhook 签名,返回 (是否通过, 原因码)。
135
130
 
136
131
  与出站签名同一套 canonical 结构、同一个共享密钥(`signing.verify`)。
@@ -41,7 +41,7 @@ KEY_ID_HEADER = "X-Foxclaw-Key-Id"
41
41
  ENV_SECRET = "FOXCLAW_CHANNEL_SIGNING_SECRET"
42
42
  ENV_KEY_ID = "FOXCLAW_CHANNEL_SIGNING_KEY_ID"
43
43
 
44
- DEFAULT_TOLERANCE = 300.0 # 秒;超过即判过期(时钟漂移容忍)
44
+ DEFAULT_TOLERANCE = 300.0
45
45
 
46
46
 
47
47
  def body_digest(body: bytes) -> str:
@@ -70,10 +70,10 @@ class ReplayGuard:
70
70
  def __init__(self, ttl: float = DEFAULT_TOLERANCE, max_entries: int = 4096):
71
71
  self.ttl = float(ttl)
72
72
  self.max_entries = max(16, int(max_entries))
73
- self._seen: dict[str, float] = {} # nonce -> 首次见到的时间
73
+ self._seen: dict[str, float] = {}
74
74
  self._lock = threading.Lock()
75
75
 
76
- def purge(self, now: float = None) -> int:
76
+ def purge(self, now: float | None = None) -> int:
77
77
  """清掉过期 nonce,返回清理条数。"""
78
78
  cut = (now if now is not None else time.time()) - self.ttl
79
79
  with self._lock:
@@ -82,10 +82,10 @@ class ReplayGuard:
82
82
  self._seen.pop(k, None)
83
83
  return len(dead)
84
84
 
85
- def seen(self, nonce: str, now: float = None) -> bool:
85
+ def seen(self, nonce: str, now: float | None = None) -> bool:
86
86
  """见过 → True(**这是重放**);没见过则登记并返回 False。"""
87
87
  if not nonce:
88
- return True # 空 nonce 一律当重放/fail-closed
88
+ return True
89
89
  now = now if now is not None else time.time()
90
90
  with self._lock:
91
91
  cut = now - self.ttl
@@ -115,7 +115,7 @@ class OutboundSigner:
115
115
  def enabled(self) -> bool:
116
116
  return bool(self.secret)
117
117
 
118
- def headers(self, body: bytes, nonce: str = "", now: float = None) -> dict:
118
+ def headers(self, body: bytes, nonce: str = "", now: float | None = None) -> dict:
119
119
  """生成签名头。未配置密钥直接抛错(fail-closed:调用方必须先判 `enabled`)。"""
120
120
  if not self.enabled:
121
121
  raise ValueError("未配置渠道签名密钥(FOXCLAW_CHANNEL_SIGNING_SECRET)")
@@ -145,8 +145,8 @@ def _header(headers: Mapping, name: str) -> str:
145
145
 
146
146
 
147
147
  def verify(headers: Mapping, body: bytes, secret: str,
148
- tolerance: float = DEFAULT_TOLERANCE, replay: ReplayGuard = None,
149
- now: float = None) -> Tuple[bool, str]:
148
+ tolerance: float = DEFAULT_TOLERANCE, replay: ReplayGuard | None = None,
149
+ now: float | None = None) -> Tuple[bool, str]:
150
150
  """验签。返回 (是否通过, 原因码)。
151
151
 
152
152
  原因码:``ok`` / ``missing_secret`` / ``missing_header`` / ``bad_version`` /
@@ -178,7 +178,7 @@ def verify(headers: Mapping, body: bytes, secret: str,
178
178
  return True, "ok"
179
179
 
180
180
 
181
- def from_env(env: Mapping = None) -> Optional[OutboundSigner]:
181
+ def from_env(env: Mapping | None = None) -> Optional[OutboundSigner]:
182
182
  """按环境变量构造签名器;未配置密钥 → None(不签,opt-in)。"""
183
183
  env = env if env is not None else os.environ
184
184
  secret = (env.get(ENV_SECRET) or "").strip()
@@ -22,7 +22,7 @@ class TelegramAdapter(ChannelAdapter):
22
22
  super().__init__(on_message=on_message)
23
23
  self.bot_token = bot_token
24
24
  self.timeout = timeout
25
- self._offset = 0 # getUpdates 增量游标(避免重复拉取)
25
+ self._offset = 0
26
26
 
27
27
  @property
28
28
  def _base(self) -> str:
@@ -32,7 +32,7 @@ class TelegramAdapter(ChannelAdapter):
32
32
  """Telegram update → 标准 InboundMessage。非文本消息返回 None。"""
33
33
  msg = raw.get("message") or raw.get("edited_message")
34
34
  if not msg or "text" not in msg:
35
- return None # 非文本消息(贴纸/图片/已读等)暂忽略
35
+ return None
36
36
  chat = msg.get("chat", {})
37
37
  sender = msg.get("from", {})
38
38
  chat_type = chat.get("type", "private")
@@ -97,11 +97,10 @@ class TelegramAdapter(ChannelAdapter):
97
97
  return []
98
98
  out = []
99
99
  for update in data.get("result", []):
100
- # 推进游标:下次从最大 update_id + 1 开始,避免重复
101
100
  uid = update.get("update_id", 0)
102
101
  if uid >= self._offset:
103
102
  self._offset = uid + 1
104
- inbound = self.handle_raw(update) # normalize + 触发 on_message
103
+ inbound = self.handle_raw(update)
105
104
  if inbound:
106
105
  out.append(inbound)
107
106
  return out
@@ -22,20 +22,17 @@ import secrets
22
22
  from typing import Optional
23
23
  from . import ChannelAdapter, InboundMessage, OutboundMessage, SendResult
24
24
 
25
- # 官方网关与端点(来自 Tencent/openclaw-weixin 源码 src/auth/accounts.ts)
26
25
  WEIXIN_BASE_URL = "https://ilinkai.weixin.qq.com"
27
26
  EP_GET_UPDATES = "ilink/bot/getupdates"
28
27
  EP_SEND_MESSAGE = "ilink/bot/sendmessage"
29
28
  EP_GET_CONFIG = "ilink/bot/getconfig"
30
29
  EP_SEND_TYPING = "ilink/bot/sendtyping"
31
30
 
32
- # 消息 item 类型(来自 README MessageItem 表)
33
31
  ITEM_TEXT = 1
34
32
  ITEM_IMAGE = 2
35
33
  ITEM_VOICE = 3
36
34
  ITEM_FILE = 4
37
35
  ITEM_VIDEO = 5
38
- # message_type
39
36
  MSG_FROM_USER = 1
40
37
  MSG_FROM_BOT = 2
41
38
 
@@ -55,8 +52,7 @@ class WeixinAdapter(ChannelAdapter):
55
52
  self.ilink_app_id = ilink_app_id
56
53
  self.long_poll_timeout_ms = long_poll_timeout_ms
57
54
  self.timeout = timeout
58
- self._cursor = "" # get_updates_buf 同步游标
59
- # 记录每个用户最近的 context_token(回复时必须带回)
55
+ self._cursor = ""
60
56
  self._context_tokens: dict[str, str] = {}
61
57
 
62
58
  @staticmethod
@@ -69,7 +65,6 @@ class WeixinAdapter(ChannelAdapter):
69
65
  major = minor = patch = 0
70
66
  return ((major & 0xFF) << 16) | ((minor & 0xFF) << 8) | (patch & 0xFF)
71
67
 
72
- # ── 鉴权头(照官方 buildHeaders)──────────────────────
73
68
  @staticmethod
74
69
  def _random_wechat_uin() -> str:
75
70
  """X-WECHAT-UIN: 随机 uint32 → 十进制字符串 → base64。"""
@@ -91,7 +86,7 @@ class WeixinAdapter(ChannelAdapter):
91
86
  def _base_info(self) -> dict:
92
87
  return {"channel_version": self.channel_version, "bot_agent": self.bot_agent}
93
88
 
94
- def _post(self, endpoint: str, body: dict, timeout: float = None):
89
+ def _post(self, endpoint: str, body: dict, timeout: float | None = None):
95
90
  import httpx
96
91
  url = f"{self.base_url}/{endpoint}"
97
92
  payload = {**body, "base_info": self._base_info()}
@@ -100,7 +95,6 @@ class WeixinAdapter(ChannelAdapter):
100
95
  timeout=timeout or self.timeout)
101
96
  return resp
102
97
 
103
- # ── 入站:getUpdates 长轮询 ────────────────────────────
104
98
  def poll_once(self) -> list[InboundMessage]:
105
99
  """长轮询拉一批新消息。宿主在循环里反复调用即可持续接收。"""
106
100
  try:
@@ -114,7 +108,7 @@ class WeixinAdapter(ChannelAdapter):
114
108
  {"get_updates_buf": self._cursor},
115
109
  timeout=self.long_poll_timeout_ms / 1000 + 5)
116
110
  except Exception:
117
- return [] # 长轮询超时/中断 → 空,下次重试
111
+ return []
118
112
  if resp.status_code != 200:
119
113
  return []
120
114
  try:
@@ -122,8 +116,7 @@ class WeixinAdapter(ChannelAdapter):
122
116
  except Exception:
123
117
  return []
124
118
  if data.get("ret") != 0:
125
- return [] # errcode -14 = 会话超时,需重新登录
126
- # 推进游标
119
+ return []
127
120
  new_buf = data.get("get_updates_buf")
128
121
  if new_buf is not None:
129
122
  self._cursor = new_buf
@@ -141,20 +134,18 @@ class WeixinAdapter(ChannelAdapter):
141
134
  return self._normalize_msg(raw)
142
135
 
143
136
  def _normalize_msg(self, msg: dict) -> Optional[InboundMessage]:
144
- # 只处理用户发来的消息(message_type=1),忽略 bot 自己的回显
145
137
  if msg.get("message_type") not in (None, MSG_FROM_USER):
146
138
  return None
147
- # 从 item_list 提取文本
148
139
  text = ""
149
140
  for item in msg.get("item_list", []) or []:
150
141
  if item.get("type") == ITEM_TEXT and item.get("text_item"):
151
142
  text += item["text_item"].get("text", "")
152
143
  if not text:
153
- return None # 非文本(图片/语音等)暂忽略
144
+ return None
154
145
  from_user = str(msg.get("from_user_id", ""))
155
146
  ctx = msg.get("context_token", "")
156
147
  if from_user and ctx:
157
- self._context_tokens[from_user] = ctx # 记住,回复时带回
148
+ self._context_tokens[from_user] = ctx
158
149
  return InboundMessage(
159
150
  channel=self.CHANNEL,
160
151
  message_id=str(msg.get("message_id", "")),
@@ -162,11 +153,10 @@ class WeixinAdapter(ChannelAdapter):
162
153
  sender_id=from_user,
163
154
  sender_name="",
164
155
  conversation_id=str(msg.get("session_id", "") or from_user),
165
- is_group=False, # 个人号 DM;群聊当前插件能力未公开
156
+ is_group=False,
166
157
  timestamp=float(msg.get("create_time_ms", 0)) / 1000 or 0.0,
167
158
  raw=msg)
168
159
 
169
- # ── 出站:sendMessage ──────────────────────────────────
170
160
  def send(self, msg: OutboundMessage) -> SendResult:
171
161
  """发文本消息。conversation_id 作为 to_user_id;context_token 从记录里取或由 extra 传入。"""
172
162
  try: