foxclaw 0.1.2 → 0.1.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (344) 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/CHANGELOG.md +57 -0
  6. package/python/CHANGELOG_CN.md +57 -0
  7. package/python/README.md +11 -2
  8. package/python/README_CN.md +32 -20
  9. package/python/bench/bm25_audit.py +1 -14
  10. package/python/bench/bm25_regression.py +0 -1
  11. package/python/bench/bm25_stress.py +0 -4
  12. package/python/bench/cd_audit.py +0 -23
  13. package/python/bench/chaos.py +6 -8
  14. package/python/bench/e2e_real_models.py +0 -1
  15. package/python/bench/llm_retry_test.py +2 -9
  16. package/python/bench/load_test.py +5 -8
  17. package/python/bench/vector_ivf_upgrade.py +1 -7
  18. package/python/docs/RELEASE.md +4 -3
  19. package/python/docs/RUN_CN.md +2 -2
  20. package/python/evals/governance/cases.py +0 -37
  21. package/python/evals/governance/run_evals.py +5 -37
  22. package/python/examples/governance_middleware_demo.py +1 -6
  23. package/python/examples/langgraph_governance_demo.py +7 -11
  24. package/python/foxclaw/__init__.py +34 -152
  25. package/python/foxclaw/agents/base.py +65 -136
  26. package/python/foxclaw/agents/impl/_helpers.py +2 -10
  27. package/python/foxclaw/agents/impl/finance.py +4 -21
  28. package/python/foxclaw/agents/impl/office.py +7 -24
  29. package/python/foxclaw/agents/impl/supervisor.py +12 -79
  30. package/python/foxclaw/agents/mixins/_context.py +0 -23
  31. package/python/foxclaw/agents/mixins/augment.py +10 -16
  32. package/python/foxclaw/agents/mixins/llm.py +7 -6
  33. package/python/foxclaw/agents/mixins/memory.py +7 -1
  34. package/python/foxclaw/agents/mixins/preference.py +58 -8
  35. package/python/foxclaw/agents/mixins/prompt.py +0 -6
  36. package/python/foxclaw/agents/mixins/skillbox.py +6 -6
  37. package/python/foxclaw/agents/mixins/tool_exec.py +16 -42
  38. package/python/foxclaw/api/__init__.py +18 -77
  39. package/python/foxclaw/api/_state.py +43 -0
  40. package/python/foxclaw/api/{app.py → bootstrap.py} +5 -32
  41. package/python/foxclaw/api/middleware.py +5 -28
  42. package/python/foxclaw/api/routes/ahs.py +60 -73
  43. package/python/foxclaw/api/routes/attachments.py +5 -6
  44. package/python/foxclaw/api/routes/browser.py +7 -11
  45. package/python/foxclaw/api/routes/domain.py +18 -26
  46. package/python/foxclaw/api/routes/knowledge.py +3 -21
  47. package/python/foxclaw/api/routes/llm_admin.py +21 -33
  48. package/python/foxclaw/api/routes/memory.py +28 -12
  49. package/python/foxclaw/api/routes/orchestration.py +3 -5
  50. package/python/foxclaw/api/routes/recovery.py +4 -3
  51. package/python/foxclaw/api/routes/review.py +2 -11
  52. package/python/foxclaw/api/routes/search.py +3 -7
  53. package/python/foxclaw/api/routes/system.py +33 -49
  54. package/python/foxclaw/api/routes/task.py +31 -49
  55. package/python/foxclaw/api/routes/ws.py +2 -6
  56. package/python/foxclaw/api/spine.py +1 -4
  57. package/python/foxclaw/browser/action/action_executor.py +1 -5
  58. package/python/foxclaw/browser/action/action_types.py +0 -1
  59. package/python/foxclaw/browser/action/url_policy.py +1 -5
  60. package/python/foxclaw/browser/adapter/web_adapter.py +1 -1
  61. package/python/foxclaw/browser/driver/playwright_driver.py +20 -16
  62. package/python/foxclaw/browser/perception/dom_extractor.py +0 -1
  63. package/python/foxclaw/browser/perception/element_indexer.py +1 -2
  64. package/python/foxclaw/browser/service.py +7 -11
  65. package/python/foxclaw/capability/embedding_service.py +4 -6
  66. package/python/foxclaw/channels/__init__.py +9 -11
  67. package/python/foxclaw/channels/discord.py +4 -13
  68. package/python/foxclaw/channels/dispatcher.py +2 -27
  69. package/python/foxclaw/channels/feishu.py +10 -20
  70. package/python/foxclaw/channels/openclaw_bridge.py +4 -9
  71. package/python/foxclaw/channels/signing.py +9 -9
  72. package/python/foxclaw/channels/telegram.py +3 -4
  73. package/python/foxclaw/channels/weixin.py +7 -17
  74. package/python/foxclaw/cli.py +42 -13
  75. package/python/foxclaw/cognitive/reasoner.py +9 -10
  76. package/python/foxclaw/cognitive/state.py +18 -24
  77. package/python/foxclaw/core/audit_chain.py +1 -3
  78. package/python/foxclaw/core/audit_sink.py +3 -3
  79. package/python/foxclaw/core/compaction.py +8 -26
  80. package/python/foxclaw/core/config.py +2 -2
  81. package/python/foxclaw/core/confirm_nonce.py +6 -12
  82. package/python/foxclaw/core/envfile.py +2 -2
  83. package/python/foxclaw/core/failure_policy.py +17 -26
  84. package/python/foxclaw/core/gateway.py +6 -10
  85. package/python/foxclaw/core/governance_audit.py +3 -7
  86. package/python/foxclaw/core/idempotency.py +15 -22
  87. package/python/foxclaw/core/injection_guard.py +10 -29
  88. package/python/foxclaw/core/memory_adapter.py +30 -57
  89. package/python/foxclaw/core/metrics.py +4 -5
  90. package/python/foxclaw/core/model_router.py +36 -148
  91. package/python/foxclaw/core/paths.py +1 -1
  92. package/python/foxclaw/core/recovery.py +21 -34
  93. package/python/foxclaw/core/scheduler.py +23 -26
  94. package/python/foxclaw/core/secrets.py +0 -3
  95. package/python/foxclaw/core/sessions.py +4 -6
  96. package/python/foxclaw/core/settings.py +16 -32
  97. package/python/foxclaw/core/shared_write.py +11 -23
  98. package/python/foxclaw/core/steering_queue.py +12 -11
  99. package/python/foxclaw/core/system_bus.py +1 -1
  100. package/python/foxclaw/core/thought_chain.py +30 -32
  101. package/python/foxclaw/core/token_meter.py +9 -13
  102. package/python/foxclaw/core/tools.py +4 -12
  103. package/python/foxclaw/core/tracing.py +25 -40
  104. package/python/foxclaw/core/truth_pipeline.py +5 -9
  105. package/python/foxclaw/core/update_check.py +149 -0
  106. package/python/foxclaw/core/vec_recall_adapter.py +11 -37
  107. package/python/foxclaw/core/verify.py +6 -14
  108. package/python/foxclaw/core/web_search.py +4 -28
  109. package/python/foxclaw/finance/evidence.py +14 -18
  110. package/python/foxclaw/finance/fx.py +1 -3
  111. package/python/foxclaw/finance/graph.py +9 -18
  112. package/python/foxclaw/finance/models.py +20 -24
  113. package/python/foxclaw/finance/nlparse.py +12 -72
  114. package/python/foxclaw/finance/policy.py +8 -36
  115. package/python/foxclaw/finance/reasoner.py +3 -11
  116. package/python/foxclaw/finance/reasoning.py +13 -34
  117. package/python/foxclaw/finance/reconcile.py +5 -10
  118. package/python/foxclaw/finance/runtime.py +19 -46
  119. package/python/foxclaw/governance/middleware.py +13 -21
  120. package/python/foxclaw/harness/_base.py +2 -8
  121. package/python/foxclaw/harness/_policy.py +19 -38
  122. package/python/foxclaw/harness/drift.py +1 -13
  123. package/python/foxclaw/harness/event_bus.py +2 -5
  124. package/python/foxclaw/harness/harness.py +9 -97
  125. package/python/foxclaw/harness/loop_detection.py +11 -18
  126. package/python/foxclaw/itinerary/amap_client.py +2 -5
  127. package/python/foxclaw/itinerary/models.py +7 -7
  128. package/python/foxclaw/itinerary/reasoner.py +1 -6
  129. package/python/foxclaw/itinerary/reasoning.py +15 -21
  130. package/python/foxclaw/itinerary/runtime.py +8 -12
  131. package/python/foxclaw/kernel/capabilities.py +5 -6
  132. package/python/foxclaw/kernel/policy_gate.py +4 -10
  133. package/python/foxclaw/kernel/process_manager.py +17 -36
  134. package/python/foxclaw/kernel/scheduler.py +3 -9
  135. package/python/foxclaw/kernel/sentinel.py +13 -23
  136. package/python/foxclaw/kernel/sentinel_client.py +12 -15
  137. package/python/foxclaw/kernel/spine.py +9 -9
  138. package/python/foxclaw/knowledge/__init__.py +4 -20
  139. package/python/foxclaw/knowledge/truth_crystallizer.py +5 -8
  140. package/python/foxclaw/learning/knowledge_graph.py +4 -9
  141. package/python/foxclaw/learning/models.py +13 -13
  142. package/python/foxclaw/learning/reasoner.py +1 -4
  143. package/python/foxclaw/learning/runtime.py +19 -31
  144. package/python/foxclaw/learning/spaced_repetition.py +2 -7
  145. package/python/foxclaw/mcp_server.py +2 -3
  146. package/python/foxclaw/orchestration/__init__.py +16 -33
  147. package/python/foxclaw/pipeline/cognitive_pipeline.py +45 -97
  148. package/python/foxclaw/pipeline/stage_meta.py +21 -0
  149. package/python/foxclaw/pipeline/supervisor_stages.py +13 -42
  150. package/python/foxclaw/planning/llm_planner.py +3 -3
  151. package/python/foxclaw/planning/planner.py +6 -11
  152. package/python/foxclaw/search/agent_port.py +7 -11
  153. package/python/foxclaw/search/evidence.py +0 -4
  154. package/python/foxclaw/search/internal_search.py +3 -5
  155. package/python/foxclaw/search/models.py +11 -13
  156. package/python/foxclaw/search/planner.py +3 -3
  157. package/python/foxclaw/search/retriever.py +1 -1
  158. package/python/foxclaw/search/runtime.py +1 -8
  159. package/python/foxclaw/skills/__init__.py +46 -100
  160. package/python/foxclaw/skills/archive.py +111 -0
  161. package/python/foxclaw/skills/fetch.py +226 -0
  162. package/python/foxclaw/skills/hooks_spec.py +21 -25
  163. package/python/foxclaw/skills/install_cmd.py +216 -0
  164. package/python/foxclaw/skills/lifecycle.py +16 -42
  165. package/python/foxclaw/skills/markdown_skill.py +10 -9
  166. package/python/foxclaw/skills/mcp_client.py +5 -11
  167. package/python/foxclaw/skills/mcp_store.py +0 -3
  168. package/python/foxclaw/skills/permission.py +13 -48
  169. package/python/foxclaw/skills/pipeline.py +21 -53
  170. package/python/foxclaw/skills/policy.py +4 -10
  171. package/python/foxclaw/skills/review.py +3 -3
  172. package/python/frontend/deck/auth.js +0 -7
  173. package/python/frontend/deck/chat_model_picker.js +0 -54
  174. package/python/frontend/deck/domain.js +0 -20
  175. package/python/frontend/deck/main.js +0 -144
  176. package/python/frontend/deck/markdown.js +0 -4
  177. package/python/frontend/deck/settings.js +1 -18
  178. package/python/frontend/deck/state.js +0 -1
  179. package/python/frontend/deck/util.js +0 -2
  180. package/python/frontend/deck/visuals.js +0 -22
  181. package/python/frontend/planet/planet.js +0 -201
  182. package/python/frontend/v3/assets/index-CQSBdxk4.js +34 -0
  183. package/python/frontend/v3/assets/index-D5nIsjms.css +1 -0
  184. package/python/frontend/v3/index.html +3 -3
  185. package/python/frontend/widget_runtime.js +0 -43
  186. package/python/frontend/widgets.js +0 -57
  187. package/python/frontend_src/index.html +1 -1
  188. package/python/frontend_src/package-lock.json +2 -2
  189. package/python/frontend_src/package.json +1 -1
  190. package/python/frontend_src/src/App.vue +3 -7
  191. package/python/frontend_src/src/api.js +0 -7
  192. package/python/frontend_src/src/components/ChatDock.vue +27 -39
  193. package/python/frontend_src/src/components/DocViewer.vue +0 -9
  194. package/python/frontend_src/src/components/Inspector.vue +0 -6
  195. package/python/frontend_src/src/components/LeftRail.vue +60 -39
  196. package/python/frontend_src/src/components/MenuPopover.vue +9 -11
  197. package/python/frontend_src/src/components/MessageStream.vue +18 -30
  198. package/python/frontend_src/src/components/ModelPicker.vue +0 -5
  199. package/python/frontend_src/src/components/QuickIngest.vue +0 -6
  200. package/python/frontend_src/src/components/Stage.vue +71 -25
  201. package/python/frontend_src/src/components/ToolMenu.vue +0 -3
  202. package/python/frontend_src/src/components/TopBar.vue +38 -10
  203. package/python/frontend_src/src/components/panels/ActivityPanel.vue +0 -2
  204. package/python/frontend_src/src/components/panels/AgentsPanel.vue +2 -5
  205. package/python/frontend_src/src/components/panels/KnowledgePanel.vue +0 -16
  206. package/python/frontend_src/src/components/panels/MemoryPanel.vue +0 -1
  207. package/python/frontend_src/src/components/panels/PanelBox.vue +6 -7
  208. package/python/frontend_src/src/components/panels/ReviewPanel.vue +0 -16
  209. package/python/frontend_src/src/components/panels/SettingsPanel.vue +142 -38
  210. package/python/frontend_src/src/components/panels/TasksPanel.vue +0 -1
  211. package/python/frontend_src/src/components/panels/ToolsPanel.vue +125 -19
  212. package/python/frontend_src/src/lib/fmt.js +0 -4
  213. package/python/frontend_src/src/lib/i18n.js +53 -0
  214. package/python/frontend_src/src/lib/locales/en.js +173 -0
  215. package/python/frontend_src/src/lib/locales/zh.js +174 -0
  216. package/python/frontend_src/src/lib/md.js +0 -14
  217. package/python/frontend_src/src/main.js +2 -0
  218. package/python/frontend_src/src/stores/app.js +17 -32
  219. package/python/frontend_src/src/stores/chat.js +0 -34
  220. package/python/frontend_src/src/stores/model.js +0 -8
  221. package/python/frontend_src/src/stores/panels.js +0 -41
  222. package/python/frontend_src/src/stores/profile.js +31 -0
  223. package/python/frontend_src/src/stores/system.js +25 -0
  224. package/python/frontend_src/vite.config.js +0 -7
  225. package/python/pyproject.toml +6 -3
  226. package/python/requirements.txt +311 -0
  227. package/python/tests/conftest.py +0 -22
  228. package/python/tests/run_all.py +3 -66
  229. package/python/tests/test_agent_lifecycle.py +4 -38
  230. package/python/tests/test_ahs_p2.py +4 -25
  231. package/python/tests/test_anthropic_tools.py +1 -15
  232. package/python/tests/test_api.py +19 -9
  233. package/python/tests/test_api_auth.py +1 -5
  234. package/python/tests/test_attachments.py +9 -25
  235. package/python/tests/test_audit_chain.py +2 -7
  236. package/python/tests/test_audit_sink.py +0 -6
  237. package/python/tests/test_browser.py +2 -6
  238. package/python/tests/test_browser_request_guard.py +0 -4
  239. package/python/tests/test_capabilities.py +0 -6
  240. package/python/tests/test_channel_signing.py +2 -9
  241. package/python/tests/test_channels.py +1 -11
  242. package/python/tests/test_config_layering.py +0 -4
  243. package/python/tests/test_confirm_nonce.py +1 -12
  244. package/python/tests/test_context_compaction_wiring.py +1 -7
  245. package/python/tests/test_dispatch.py +0 -6
  246. package/python/tests/test_domain_evals.py +11 -80
  247. package/python/tests/test_domain_import.py +0 -6
  248. package/python/tests/test_domain_reasoners.py +0 -14
  249. package/python/tests/test_e2e.py +0 -17
  250. package/python/tests/test_emb_cache.py +7 -17
  251. package/python/tests/test_embedding_singleton.py +1 -3
  252. package/python/tests/test_evals_smoke.py +0 -24
  253. package/python/tests/test_event_spine.py +10 -18
  254. package/python/tests/test_evidence_semantic.py +1 -14
  255. package/python/tests/test_evidence_sse.py +3 -6
  256. package/python/tests/test_file_ingest.py +0 -11
  257. package/python/tests/test_finance.py +0 -9
  258. package/python/tests/test_finance_ledger.py +5 -12
  259. package/python/tests/test_finance_persist.py +8 -12
  260. package/python/tests/test_gateway.py +3 -22
  261. package/python/tests/test_governance.py +5 -52
  262. package/python/tests/test_governance_audit.py +4 -39
  263. package/python/tests/test_governance_middleware.py +1 -7
  264. package/python/tests/test_hkc.py +6 -24
  265. package/python/tests/test_hkc_index_build_manager.py +0 -33
  266. package/python/tests/test_hkc_manifest_reuse.py +4 -27
  267. package/python/tests/test_hkc_search_event_sync.py +3 -54
  268. package/python/tests/test_import_path_guard.py +0 -6
  269. package/python/tests/test_injection_guard.py +18 -36
  270. package/python/tests/test_integration.py +5 -29
  271. package/python/tests/test_itinerary.py +0 -3
  272. package/python/tests/test_kee_semantic.py +0 -5
  273. package/python/tests/test_kernel.py +1 -11
  274. package/python/tests/test_learning.py +3 -12
  275. package/python/tests/test_llm_admin.py +4 -19
  276. package/python/tests/test_llm_planner.py +1 -23
  277. package/python/tests/test_markdown_skills.py +3 -14
  278. package/python/tests/test_markdown_skills_openclaw.py +4 -9
  279. package/python/tests/test_mcp_connectors.py +3 -15
  280. package/python/tests/test_mcp_guardian_discovery.py +2 -6
  281. package/python/tests/test_mcp_no_memory.py +3 -3
  282. package/python/tests/test_mcp_policy_gate.py +0 -1
  283. package/python/tests/test_mcp_server.py +6 -9
  284. package/python/tests/test_memory_recall.py +1 -4
  285. package/python/tests/test_metrics.py +0 -8
  286. package/python/tests/test_multi_chain.py +1 -13
  287. package/python/tests/test_multimodal.py +5 -18
  288. package/python/tests/test_negotiation.py +3 -13
  289. package/python/tests/test_orchestration.py +1 -11
  290. package/python/tests/test_orchestration_decoupled.py +0 -6
  291. package/python/tests/test_p3_p5_hardening.py +3 -17
  292. package/python/tests/test_parallel_dispatch.py +4 -12
  293. package/python/tests/test_pipeline.py +17 -58
  294. package/python/tests/test_plan_visible.py +4 -4
  295. package/python/tests/test_planner.py +0 -7
  296. package/python/tests/test_policy_gate.py +2 -3
  297. package/python/tests/test_profile.py +67 -0
  298. package/python/tests/test_readme_claims.py +163 -0
  299. package/python/tests/test_reasoner.py +2 -17
  300. package/python/tests/test_release_check.py +2 -13
  301. package/python/tests/test_resume_checkpoint.py +7 -15
  302. package/python/tests/test_routing.py +2 -12
  303. package/python/tests/test_scheduler.py +7 -10
  304. package/python/tests/test_search.py +1 -11
  305. package/python/tests/test_secrets.py +4 -10
  306. package/python/tests/test_semantic.py +1 -10
  307. package/python/tests/test_sessions.py +3 -4
  308. package/python/tests/test_settings.py +2 -18
  309. package/python/tests/test_shadow_learning.py +3 -43
  310. package/python/tests/test_shared_write.py +0 -7
  311. package/python/tests/test_skill_taint_gate.py +1 -10
  312. package/python/tests/test_skills.py +2 -18
  313. package/python/tests/test_skills_fetch.py +282 -0
  314. package/python/tests/test_stats_and_events.py +2 -5
  315. package/python/tests/test_streaming.py +2 -7
  316. package/python/tests/test_supervisor_pipeline.py +4 -5
  317. package/python/tests/test_supervisor_triage.py +4 -10
  318. package/python/tests/test_taint.py +2 -7
  319. package/python/tests/test_task_stream.py +1 -12
  320. package/python/tests/test_thinking_sse.py +0 -1
  321. package/python/tests/test_tool_center.py +1 -17
  322. package/python/tests/test_tool_parallel.py +2 -2
  323. package/python/tests/test_tool_params.py +1 -5
  324. package/python/tests/test_tool_use.py +0 -13
  325. package/python/tests/test_toplevel_concurrency.py +3 -4
  326. package/python/tests/test_tracing.py +7 -41
  327. package/python/tests/test_truth_crystallizer.py +4 -6
  328. package/python/tests/test_truth_flow.py +2 -7
  329. package/python/tests/test_update_check.py +95 -0
  330. package/python/tests/test_verify_evidence.py +8 -21
  331. package/python/tests/test_version_sync.py +35 -6
  332. package/python/tests/test_web_trigger.py +0 -17
  333. package/python/tools/check_faiss_type.py +0 -1
  334. package/python/tools/check_log.ps1 +0 -8
  335. package/python/tools/mypy_baseline.txt +12 -4
  336. package/python/tools/mypy_ratchet.py +52 -6
  337. package/python/tools/pack.py +0 -5
  338. package/python/tools/release_check.py +7 -7
  339. package/python/tools/restart_foxclaw.ps1 +5 -8
  340. package/python/tools/shared_write_audit.py +1 -15
  341. package/python/tools/size_check.py +1 -1
  342. package/python/tools/strip_comments.py +228 -0
  343. package/python/frontend/v3/assets/index-BD7VPbMV.js +0 -33
  344. package/python/frontend/v3/assets/index-CAvnYWGH.css +0 -1
@@ -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:
@@ -7,6 +7,8 @@
7
7
  foxclaw serve # 启动 API 服务(默认 http://127.0.0.1:8000)
8
8
  foxclaw config check # 检查 .env / 环境变量:类型写错、拼错的键、互相矛盾的组合
9
9
  foxclaw config show # 打印当前生效配置(密钥默认脱敏)
10
+ foxclaw skills install @owner/slug # 装 SKILL.md 技能(ClawHub/GitHub/URL/本地包)
11
+ foxclaw skills list # 列出已装技能
10
12
  foxclaw --version # 打印版本
11
13
 
12
14
  `serve` 用字符串目标 `foxclaw.api:app` 交给 uvicorn 导入,避免 CLI 进程
@@ -34,8 +36,6 @@ def _repo_root() -> Path:
34
36
  def _cmd_serve(args: argparse.Namespace) -> int:
35
37
  from foxclaw.core import settings
36
38
 
37
- # 启动前先查配置:类型写错的值今天会崩在很远的地方(uvicorn 40 行 traceback 中间的
38
- # `int()`),用户看不出是自己 .env 写错了。这里提前拦下并说清怎么改。
39
39
  issues = settings.check()
40
40
  fatal = settings.fatal_issues(issues)
41
41
  if fatal:
@@ -43,22 +43,19 @@ def _cmd_serve(args: argparse.Namespace) -> int:
43
43
  print("\n以上配置会让启动直接失败,已中止。改好后重试。", file=sys.stderr)
44
44
  return 2
45
45
  if issues:
46
- # 不影响启动的问题(可疑但能跑)只提醒一次,不拦
47
46
  print(settings.format_issues(issues, limit=8), file=sys.stderr)
48
47
  print("(以上不阻断启动;`foxclaw config check` 看完整诊断)", file=sys.stderr)
49
48
 
50
49
  import uvicorn
51
50
 
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)
51
+ uvicorn.run(
52
+ APP_TARGET,
53
+ host=args.host,
54
+ port=args.port,
55
+ log_level=args.log_level,
56
+ reload=bool(args.reload),
57
+ reload_dirs=[str(_repo_root() / "foxclaw")] if args.reload else None,
58
+ )
62
59
  return 0
63
60
 
64
61
 
@@ -97,6 +94,12 @@ def _cmd_config_show(args: argparse.Namespace) -> int:
97
94
  return 0
98
95
 
99
96
 
97
+ def _cmd_skills(args: argparse.Namespace) -> int:
98
+ from foxclaw.skills.install_cmd import run
99
+
100
+ return run(args)
101
+
102
+
100
103
  def build_parser() -> argparse.ArgumentParser:
101
104
  parser = argparse.ArgumentParser(
102
105
  prog="foxclaw",
@@ -131,6 +134,28 @@ def build_parser() -> argparse.ArgumentParser:
131
134
  help="显示密钥原文(默认脱敏;别在截图/日志里用)")
132
135
  c_show.set_defaults(handler=_cmd_config_show)
133
136
 
137
+ skills = subparsers.add_parser("skills", help="安装/查看/卸载指令技能(SKILL.md)")
138
+ skills_sub = skills.add_subparsers(dest="skills_action", metavar="<action>")
139
+
140
+ s_install = skills_sub.add_parser("install", help="从 ClawHub/GitHub/URL/本地包装一个技能")
141
+ s_install.add_argument(
142
+ "ref",
143
+ help="@owner/slug(ClawHub) | github:owner/repo[/子路径][@ref] | "
144
+ "http(s)://包地址 | 本地 .zip/.tar.gz/SKILL.md/目录")
145
+ s_install.add_argument("--yes", action="store_true",
146
+ help="跳过确认(脚本/CI 必填;非终端环境没有它一定失败)")
147
+ s_install.add_argument("--id", default="", help="覆盖技能 id(默认取 SKILL.md 里的 name)")
148
+ s_install.add_argument("--json", action="store_true", help="输出 JSON")
149
+ s_install.set_defaults(handler=_cmd_skills)
150
+
151
+ s_list = skills_sub.add_parser("list", help="列出已安装的指令技能")
152
+ s_list.add_argument("--json", action="store_true", help="输出 JSON")
153
+ s_list.set_defaults(handler=_cmd_skills)
154
+
155
+ s_remove = skills_sub.add_parser("remove", help="卸载一个指令技能")
156
+ s_remove.add_argument("id", help="技能 id(`foxclaw skills list` 第一列)")
157
+ s_remove.set_defaults(handler=_cmd_skills)
158
+
134
159
  return parser
135
160
 
136
161
 
@@ -143,6 +168,10 @@ def main(argv: list[str] | None = None) -> int:
143
168
  if getattr(args, "command", None) == "config":
144
169
  print("用法:`foxclaw config check` 或 `foxclaw config show`")
145
170
  return 2
171
+ if getattr(args, "command", None) == "skills":
172
+ print("用法:`foxclaw skills install <ref>` / `foxclaw skills list` / "
173
+ "`foxclaw skills remove <id>`")
174
+ return 2
146
175
  print(f"foxclaw {__version__}")
147
176
  print("用 `foxclaw serve` 启动本地服务;`foxclaw config check` 检查配置;"
148
177
  "`foxclaw --help` 查看全部子命令。")
@@ -15,10 +15,10 @@ from typing import Any
15
15
 
16
16
  @dataclass
17
17
  class Evidence:
18
- factor: str # 证据因子(如规则 id / 历史频率 / 预算余量)
19
- detail: str = "" # 具体说明
20
- weight: float = 1.0 # 权重(0~1)
21
- raw: Any = None # 原始证据对象(如 PolicyVerdict),供裁决内部使用
18
+ factor: str
19
+ detail: str = ""
20
+ weight: float = 1.0
21
+ raw: Any = None
22
22
 
23
23
  def to_dict(self) -> dict:
24
24
  return {"factor": self.factor, "detail": self.detail, "weight": self.weight}
@@ -26,11 +26,11 @@ class Evidence:
26
26
 
27
27
  @dataclass
28
28
  class ReasonedVerdict:
29
- decision: str # approve | confirm | reject
30
- rationale: str # 裁决说明(人类可读)
31
- evidence: list = field(default_factory=list) # [Evidence]
32
- hypotheses: list = field(default_factory=list) # [str]
33
- source: str = "rules" # rules | llm(哪层做出的裁决)
29
+ decision: str
30
+ rationale: str
31
+ evidence: list = field(default_factory=list)
32
+ hypotheses: list = field(default_factory=list)
33
+ source: str = "rules"
34
34
 
35
35
  def to_dict(self) -> dict:
36
36
  return {"decision": self.decision, "rationale": self.rationale,
@@ -61,7 +61,6 @@ class DomainReasoner:
61
61
  ev = self.gather_evidence(subject, obs)
62
62
  return self.arbitrate(subject, obs, hyps, ev)
63
63
 
64
- # ── 双层增强的通用工具(v2 ③ Step4:itinerary/learning 复用)────────────
65
64
  @staticmethod
66
65
  def llm_available(llm, enabled: bool) -> bool:
67
66
  """LLM 是否真正可用:启用 + 非空 + 非 stub(无真实模型时视为不可用)。"""
@@ -10,6 +10,8 @@
10
10
  可驱动前端"系统在想什么"实时面板,也用于调试与回放。
11
11
  """
12
12
  from __future__ import annotations
13
+
14
+ from typing import Optional
13
15
  import time
14
16
  from dataclasses import dataclass, field
15
17
 
@@ -18,19 +20,18 @@ from dataclasses import dataclass, field
18
20
  class CognitiveState:
19
21
  """系统当前认知状态快照。随事件总线上的活动自动更新。"""
20
22
 
21
- active_goal: str = None # 当前活跃目标(最近任务)
22
- active_agent: str = None # 当前处理的 Agent
23
- semantic_focus: str = None # 语义焦点(调度意图类别)
24
- attention_scope: list = field(default_factory=list) # 注意力范围(关键词)
25
- reasoning_depth: float = 0.0 # 当前推理深度
26
- confidence_map: dict = field(default_factory=dict) # 置信度图(意图/优先级)
27
- activated_memories: list = field(default_factory=list) # 激活的记忆(最近召回/沉淀)
28
- last_route: str = None # 最近路由倾向
29
- active_processes: int = 0 # 活跃进程数
30
- temporal_window: str = "current_turn" # 时间窗口
23
+ active_goal: Optional[str] = None
24
+ active_agent: Optional[str] = None
25
+ semantic_focus: Optional[str] = None
26
+ attention_scope: list = field(default_factory=list)
27
+ reasoning_depth: float = 0.0
28
+ confidence_map: dict = field(default_factory=dict)
29
+ activated_memories: list = field(default_factory=list)
30
+ last_route: Optional[str] = None
31
+ active_processes: int = 0
32
+ temporal_window: str = "current_turn"
31
33
  updated_at: float = field(default_factory=time.time)
32
34
 
33
- # ── 信号摄入(由事件驱动)──
34
35
 
35
36
  def on_execution(self, payload: dict):
36
37
  """执行事件 → 更新目标/Agent/注意力。"""
@@ -54,13 +55,12 @@ class CognitiveState:
54
55
  content = mu.get("content")
55
56
  if content:
56
57
  snippet = content[:80]
57
- # 去重:已存在则先移除旧的,再插到最前(LRU 式)
58
58
  existing = [m for m in self.activated_memories if m != snippet]
59
59
  self.activated_memories = [snippet] + existing[:4]
60
60
  self.temporal_window = "memory_augmented"
61
61
  self.updated_at = time.time()
62
62
 
63
- def on_process_change(self, payload: dict, active_count: int = None):
63
+ def on_process_change(self, payload: dict, active_count: int | None = None):
64
64
  """进程状态变更 → 更新活跃进程数。"""
65
65
  if active_count is not None:
66
66
  self.active_processes = active_count
@@ -69,14 +69,12 @@ class CognitiveState:
69
69
  @staticmethod
70
70
  def _keywords(text: str, n: int = 6) -> list:
71
71
  import re
72
- # 先按标点/空格切;若切出的片段仍很长(中文无空格),按 2-4 字滑窗取要点
73
72
  rough = [w for w in re.split(r"[\s,,、。!?:;]", text or "") if len(w) >= 2]
74
73
  out = []
75
74
  for seg in rough:
76
75
  if len(seg) <= 4:
77
76
  out.append(seg)
78
77
  else:
79
- # 长片段取首尾 + 中部双字,作为注意力要点
80
78
  out.append(seg[:3])
81
79
  if len(seg) > 6:
82
80
  out.append(seg[len(seg)//2:len(seg)//2+2])
@@ -106,25 +104,23 @@ class CognitiveStateTracker:
106
104
  import threading
107
105
  self.state = CognitiveState()
108
106
  self.process_manager = process_manager
109
- self._lock = threading.Lock() # 事件可能跨线程触发,保护状态更新
107
+ self._lock = threading.Lock()
110
108
  if bus is not None:
111
- # 【P4-1】前两个是精确 channel 名 → exact=True;task.lifecycle 是刻意的前缀
112
- # 订阅(收 created/queued/running/... 一族),保持默认前缀匹配。
113
109
  bus.subscribe("execution.completed", self._on_exec, exact=True)
114
110
  bus.subscribe("memory.updated", self._on_mem, exact=True)
115
- bus.subscribe("task.lifecycle", self._on_proc) # Step3:统一生命周期词汇(前缀匹配)
111
+ bus.subscribe("task.lifecycle", self._on_proc)
116
112
 
117
113
  def _on_exec(self, msg):
118
114
  if msg is None:
119
115
  return
120
116
  with self._lock:
121
- self.state.on_execution(getattr(msg, "payload", None))
117
+ self.state.on_execution(getattr(msg, "payload", None) or {})
122
118
 
123
119
  def _on_mem(self, msg):
124
120
  if msg is None:
125
121
  return
126
122
  with self._lock:
127
- self.state.on_memory_updated(getattr(msg, "payload", None))
123
+ self.state.on_memory_updated(getattr(msg, "payload", None) or {})
128
124
 
129
125
  def _on_proc(self, msg):
130
126
  if msg is None:
@@ -132,15 +128,13 @@ class CognitiveStateTracker:
132
128
  active = None
133
129
  pm = self.process_manager
134
130
  if pm is not None:
135
- # 【深审优化】用 pm 的 O(1) active_count(),免去每次 lifecycle 事件 stats() 遍历
136
- # 全部进程(O(n))。旧 pm 无此方法则回退到 stats()(scheduled+running)。
137
131
  if hasattr(pm, "active_count"):
138
132
  active = pm.active_count()
139
133
  else:
140
134
  st = pm.stats().get("by_state", {})
141
135
  active = st.get("scheduled", 0) + st.get("running", 0)
142
136
  with self._lock:
143
- self.state.on_process_change(getattr(msg, "payload", None), active)
137
+ self.state.on_process_change(getattr(msg, "payload", None) or {}, active)
144
138
 
145
139
  def snapshot(self) -> dict:
146
140
  with self._lock:
@@ -28,10 +28,8 @@ import hashlib
28
28
  import json
29
29
  from pathlib import Path
30
30
 
31
- #: 首条记录的 prev_hash。用 64 个 0 而不是空串:一眼能看出「这是链头」。
32
31
  GENESIS = "0" * 64
33
32
 
34
- #: 从文件尾部回读的字节数:找 tail hash 只需最后一行,64KB 足够容下超长记录。
35
33
  _TAIL_WINDOW = 65536
36
34
 
37
35
 
@@ -100,7 +98,7 @@ def verify(path) -> tuple:
100
98
  return False, n, f"第 {i} 行不是合法 JSON"
101
99
  h = rec.get("hash")
102
100
  if not h:
103
- continue # legacy 段:无链可验,跳过
101
+ continue
104
102
  if rec.get("prev_hash") != prev:
105
103
  return False, n, (f"第 {i} 行链断:prev_hash 与上一条 hash 不符"
106
104
  "(中间记录被删或被挪动)")
@@ -50,10 +50,10 @@ class SharedWriteAuditSink:
50
50
  def __init__(self, path: Path):
51
51
  self.path = Path(path)
52
52
  self._lock = threading.Lock()
53
- self._last_hash: str | None = None # 链尾,首次写时从文件尾部补齐
53
+ self._last_hash: str | None = None
54
54
  self.path.parent.mkdir(parents=True, exist_ok=True)
55
55
 
56
- def __call__(self, evt): # EventSpine.subscribe 的 handler 形态:handler(event)
56
+ def __call__(self, evt):
57
57
  try:
58
58
  rec = {
59
59
  "ts": datetime.now().isoformat(timespec="seconds"),
@@ -69,7 +69,7 @@ class SharedWriteAuditSink:
69
69
  f.write(json.dumps(rec, ensure_ascii=False, default=str) + "\n")
70
70
  self._last_hash = rec["hash"]
71
71
  except Exception:
72
- pass # 审计失败绝不影响写入主流程
72
+ pass
73
73
 
74
74
  def read(self, limit: int = 200) -> list:
75
75
  """读最近 limit 条(供诊断/工具用)。坏行跳过 —— 落盘绝不该反过来卡死读取。"""