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
@@ -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 条(供诊断/工具用)。坏行跳过 —— 落盘绝不该反过来卡死读取。"""
@@ -25,7 +25,6 @@ import re
25
25
  from dataclasses import dataclass, field
26
26
 
27
27
 
28
- # 粗略 token 估算:中文约 1 字符 ≈ 1 token,英文约 4 字符 ≈ 1 token。
29
28
  def estimate_tokens(text: str) -> int:
30
29
  if not text:
31
30
  return 0
@@ -38,8 +37,6 @@ def estimate_messages_tokens(messages: list) -> int:
38
37
  return sum(estimate_tokens(str(m.get("content", ""))) for m in messages)
39
38
 
40
39
 
41
- # 关键标识符:不能在摘要中丢失的(ID、长数字、订单号、哈希、金额等)
42
- # 不用外层 \b:¥/逗号不是单词字符,会导致金额匹配失败。
43
40
  _IDENTIFIER_PAT = re.compile(
44
41
  r"[a-zA-Z]{2,}_[a-zA-Z0-9]{4,}|[a-f0-9]{8,}|[0-9]{6,}|¥[\d,]+(?:\.\d+)?")
45
42
 
@@ -56,14 +53,14 @@ def extract_identifiers(messages: list) -> list[str]:
56
53
 
57
54
  @dataclass
58
55
  class CompactionResult:
59
- messages: list = field(default_factory=list) # 压缩后的消息列表
60
- method: str = "none" # llm_summary/partial_summary/truncate/none
56
+ messages: list = field(default_factory=list)
57
+ method: str = "none"
61
58
  original_count: int = 0
62
59
  compacted_count: int = 0
63
60
  original_tokens: int = 0
64
61
  compacted_tokens: int = 0
65
62
  preserved_identifiers: list = field(default_factory=list)
66
- note: str = "" # 说明(如压缩跳过原因)
63
+ note: str = ""
67
64
 
68
65
  def to_dict(self) -> dict:
69
66
  return {
@@ -101,8 +98,6 @@ def group_tool_calls(messages: list) -> list:
101
98
  m = messages[i]
102
99
  buf.append(m)
103
100
  if m.get("role") == "assistant" and (m.get("tool_calls") or []):
104
- # 本 assistant 声明了工具调用 → 把紧随其后、id 匹配的 tool 结果
105
- # 全部并入同一组,确保「声明与结果同进同出」。
106
101
  ids = {tc.get("id") for tc in (m.get("tool_calls") or [])}
107
102
  j = i + 1
108
103
  while j < n:
@@ -125,10 +120,10 @@ class Compactor:
125
120
 
126
121
  def __init__(self, router=None, max_tokens: int = 6000,
127
122
  keep_recent: int = 4, oversized_token: int = 2000):
128
- self.router = router # ModelRouter,用于 LLM 摘要(可空→走截断兜底)
129
- self.max_tokens = max_tokens # 超过此 token 触发压缩
130
- self.keep_recent = keep_recent # 最近保留几条原样
131
- self.oversized_token = oversized_token # 单条超此值视为"超大消息"
123
+ self.router = router
124
+ self.max_tokens = max_tokens
125
+ self.keep_recent = keep_recent
126
+ self.oversized_token = oversized_token
132
127
 
133
128
  def needs_compaction(self, messages: list) -> bool:
134
129
  return estimate_messages_tokens(messages) > self.max_tokens
@@ -146,13 +141,9 @@ class Compactor:
146
141
  res.compacted_tokens = res.original_tokens
147
142
  return res
148
143
 
149
- # 分区:系统消息 / 待压缩的早期消息 / 保留的近期消息
150
144
  system_msgs = [m for m in messages if m.get("role") == "system"]
151
145
  non_system = [m for m in messages if m.get("role") != "system"]
152
146
  keep = max(0, self.keep_recent)
153
- # 按**原子组**保留(而非按条数),保证 assistant.tool_calls 与其 tool 结果
154
- # 同进同出。按条数切会在「一轮多工具」时把 tool 结果与其声明切散 →
155
- # provider 报 400。详见 group_tool_calls docstring。
156
147
  _groups = group_tool_calls(non_system)
157
148
  if keep and _groups:
158
149
  recent = [m for g in _groups[-keep:] for m in g]
@@ -161,8 +152,6 @@ class Compactor:
161
152
  recent, to_compress = [], non_system
162
153
  else:
163
154
  recent, to_compress = [], non_system
164
- # 防御:若仍不配对(极端边界/上游已产生的畸形序列),宁可放弃压缩,
165
- # 也不产出非法消息序列 —— 压缩不生效只是浪费 token,发出去 400 是整轮失败。
166
155
  _declared = {tc.get("id") for m in (recent + to_compress)
167
156
  for tc in (m.get("tool_calls") or [])}
168
157
  _orphans = [m for m in (recent + to_compress)
@@ -185,15 +174,12 @@ class Compactor:
185
174
 
186
175
  res.preserved_identifiers = extract_identifiers(to_compress)
187
176
 
188
- # —— 渐进降级 ——
189
177
  summary = None
190
178
  method = "truncate"
191
- # 1. LLM 完整摘要
192
179
  if self.router is not None:
193
180
  summary = self._try_summarize(to_compress, res.preserved_identifiers)
194
181
  if summary:
195
182
  method = "llm_summary"
196
- # 2. 部分摘要:排除超大消息后再试
197
183
  if summary is None and self.router is not None:
198
184
  trimmed = [m for m in to_compress
199
185
  if estimate_tokens(str(m.get("content", ""))) <= self.oversized_token]
@@ -201,7 +187,6 @@ class Compactor:
201
187
  summary = self._try_summarize(trimmed, res.preserved_identifiers)
202
188
  if summary:
203
189
  method = "partial_summary"
204
- # 3. 截断兜底
205
190
  if summary is None:
206
191
  summary = self._truncate_fallback(to_compress, res.preserved_identifiers)
207
192
  method = "truncate"
@@ -211,14 +196,11 @@ class Compactor:
211
196
  compacted = system_msgs + [summary_msg] + recent
212
197
  compacted_tokens = estimate_messages_tokens(compacted)
213
198
 
214
- # 校验:压缩须真的显著变小(至少省 10%),否则放弃——避免系统消息过大时
215
- # 压缩无效还白加摘要导致反而更大。
216
199
  if compacted_tokens >= res.original_tokens * 0.9:
217
200
  res.messages = list(messages)
218
201
  res.method = "skipped_ineffective"
219
202
  res.compacted_count = len(messages)
220
203
  res.compacted_tokens = res.original_tokens
221
- # 提示:压缩无效通常因系统消息/近期消息本身已占满预算
222
204
  sys_tokens = estimate_messages_tokens(system_msgs)
223
205
  res.note = (f"压缩跳过:有效压缩空间不足(系统消息占 {sys_tokens} token)。"
224
206
  f"建议精简系统提示或调大上下文预算。")
@@ -242,7 +224,7 @@ class Compactor:
242
224
  return list(messages), None
243
225
  res = self.compact(messages)
244
226
  if res.method == "none" or res.method.startswith("skipped"):
245
- return list(messages), res # 压缩没生效 → 原样,结果仅用于观测
227
+ return list(messages), res
246
228
  return res.messages, res
247
229
  except Exception:
248
230
  return list(messages), None
@@ -22,7 +22,7 @@ class LayeredConfig:
22
22
  """三层配置合并原语。持有各层 dict 的引用 —— 若某层的集合值是可变对象(如 SkillPolicy 的
23
23
  user deny set),原地增删会被 merged_set/policy_locked 实时反映,无需重建。"""
24
24
 
25
- def __init__(self, policy: dict = None, user: dict = None, runtime: dict = None):
25
+ def __init__(self, policy: dict | None = None, user: dict | None = None, runtime: dict | None = None):
26
26
  self.policy = dict(policy or {})
27
27
  self.user = dict(user or {})
28
28
  self.runtime = dict(runtime or {})
@@ -61,5 +61,5 @@ def load_policy_layer(data_dir=None) -> dict:
61
61
  d = json.loads(p.read_text(encoding="utf-8"))
62
62
  deny |= set(d.get("deny") or [])
63
63
  except Exception:
64
- pass # policy.json 损坏不应拖垮启动;宁可少锁不错锁,由运维排查
64
+ pass
65
65
  return {"deny": sorted(deny)}
@@ -25,10 +25,9 @@ import time
25
25
  from dataclasses import dataclass, field
26
26
  from typing import Optional, Tuple
27
27
 
28
- #: 视为「关键操作」的副作用级别(发送/支付);destructive 无条件算关键
29
28
  CRITICAL_SIDE_EFFECTS = ("send", "pay")
30
29
 
31
- DEFAULT_TTL = 300.0 # 秒;确认码有效期
30
+ DEFAULT_TTL = 300.0
32
31
 
33
32
 
34
33
  def action_fingerprint(action: str, args: dict) -> str:
@@ -78,14 +77,13 @@ class ConfirmationBroker:
78
77
  self._items: dict[str, PendingConfirm] = {}
79
78
  self._lock = threading.Lock()
80
79
 
81
- # ── 签发 ────────────────────────────────────────────
82
80
  def issue(self, session: str, action: str, args: dict,
83
- ttl: float = None) -> dict:
81
+ ttl: float | None = None) -> dict:
84
82
  """签发一个绑定到 (会话, 动作, 参数) 的一次性确认码。"""
85
83
  now = self._clock()
86
84
  ttl = self.ttl if ttl is None else float(ttl)
87
85
  item = PendingConfirm(
88
- nonce=secrets.token_hex(8), # 16 hex 字符,人类也能念出来
86
+ nonce=secrets.token_hex(8),
89
87
  action=action or "",
90
88
  fingerprint=action_fingerprint(action or "", args or {}),
91
89
  session=session or "",
@@ -99,9 +97,8 @@ class ConfirmationBroker:
99
97
  return {"nonce": item.nonce, "action": item.action,
100
98
  "expires_at": item.expires, "ttl": ttl}
101
99
 
102
- # ── 校验并消费 ──────────────────────────────────────
103
100
  def consume(self, session: str, nonce: str, action: str = "",
104
- args: dict = None, now: float = None) -> Tuple[bool, str]:
101
+ args: dict | None = None, now: float | None = None) -> Tuple[bool, str]:
105
102
  """校验确认码并**立即作废**。返回 (是否通过, 原因码)。
106
103
 
107
104
  原因码:``ok`` / ``missing_nonce`` / ``unknown_nonce`` / ``already_used`` /
@@ -113,8 +110,6 @@ class ConfirmationBroker:
113
110
  now = now if now is not None else self._clock()
114
111
  with self._lock:
115
112
  item: Optional[PendingConfirm] = self._items.get(nonce)
116
- # 先看这一条再清理:否则过期的码会被 _purge 抹掉,排错时只会看到
117
- # unknown_nonce,分不清"从没发过"和"发过但过期了"
118
113
  if item is not None and item.expires <= now:
119
114
  self._items.pop(nonce, None)
120
115
  return False, "expired"
@@ -130,10 +125,9 @@ class ConfirmationBroker:
130
125
  if args is not None and item.fingerprint != action_fingerprint(item.action, args):
131
126
  return False, "args_mismatch"
132
127
  item.used = True
133
- self._items.pop(nonce, None) # 单次使用:消费即删
128
+ self._items.pop(nonce, None)
134
129
  return True, "ok"
135
130
 
136
- # ── 观测 ────────────────────────────────────────────
137
131
  def pending(self, session: str = "") -> list:
138
132
  """未消费且未过期的确认码(可按会话过滤),供排查/前端展示。"""
139
133
  now = self._clock()
@@ -142,7 +136,7 @@ class ConfirmationBroker:
142
136
  return [it.to_dict() for it in self._items.values()
143
137
  if not it.used and (not session or it.session == session)]
144
138
 
145
- def purge(self, now: float = None) -> int:
139
+ def purge(self, now: float | None = None) -> int:
146
140
  now = now if now is not None else self._clock()
147
141
  with self._lock:
148
142
  return self._purge_locked(now)