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
@@ -15,7 +15,6 @@ from typing import Optional
15
15
  from .models import FinancePolicyRule, PolicyVerdict, Transaction
16
16
 
17
17
 
18
- # 内置默认规则(对应用户设计里的示例规则)
19
18
  DEFAULT_RULES = [
20
19
  FinancePolicyRule(
21
20
  id="subscription_limit",
@@ -27,9 +26,6 @@ DEFAULT_RULES = [
27
26
  source="builtin",
28
27
  ),
29
28
  FinancePolicyRule(
30
- # 阈值 500(原 200):200 会让几乎每次日常消费都要确认 → 误报爆炸 → 用户索性
31
- # 把守门人关掉,那它就形同虚设。守门人的价值不在"规则多",而在**误报低到你愿意一直开着**。
32
- # 这是可调项:按自己的消费水平改 finance_policy.yaml 即可,不用动代码。
33
29
  id="large_purchase_check",
34
30
  condition="单笔支出 > 500",
35
31
  action="require_confirmation",
@@ -47,14 +43,11 @@ DEFAULT_RULES = [
47
43
  severity="critical",
48
44
  source="builtin",
49
45
  ),
50
- # ── 用户点名要的 4 条(2026-07-16)────────────────────────────────
51
- # 前两条的**检测早就实现了**(detect_duplicate_charges / overspend_forecast),
52
- # 但此前只进分析报告、从不产生裁决 —— 检测到了却不吭声。现接成规则。
53
46
  FinancePolicyRule(
54
47
  id="duplicate_charge_alert",
55
48
  condition="同商户短时间内重复扣款 ≥2 次",
56
49
  action="require_confirmation",
57
- threshold=2.0, # 次数阈值
50
+ threshold=2.0,
58
51
  scope="merchant",
59
52
  severity="warn",
60
53
  source="builtin",
@@ -72,18 +65,16 @@ DEFAULT_RULES = [
72
65
  id="renewal_reminder",
73
66
  condition="订阅/年费将在 7 天内自动续订",
74
67
  action="alert",
75
- threshold=7.0, # 提前几天提醒
68
+ threshold=7.0,
76
69
  scope="subscription",
77
70
  severity="info",
78
71
  source="builtin",
79
72
  ),
80
73
  FinancePolicyRule(
81
- # 注:必须由用户给交易打 tag `refund_expected` —— 系统无法从交易流推断
82
- # "我期待一笔退款"(退款只是一笔入账)。不标记则此规则恒不触发。
83
74
  id="refund_overdue_alert",
84
75
  condition="标记了 refund_expected 的支出,14 天内未收到等额退款",
85
76
  action="alert",
86
- threshold=14.0, # 等待天数
77
+ threshold=14.0,
87
78
  scope="merchant",
88
79
  severity="warn",
89
80
  source="builtin",
@@ -100,8 +91,6 @@ DEFAULT_RULES = [
100
91
  ]
101
92
 
102
93
 
103
- # 出厂阈值快照(模块加载时取,此后 DEFAULT_RULES 不再被改 —— 见 _load_or_init 的 deepcopy)。
104
- # 自校准靠它判断某条规则"是否还停在出厂状态"。
105
94
  FACTORY_THRESHOLDS = {r.id: r.threshold for r in DEFAULT_RULES}
106
95
 
107
96
 
@@ -111,10 +100,6 @@ class FinancePolicyEngine:
111
100
  def __init__(self, policy_path: str = "./finance_policy.yaml"):
112
101
  self.policy_path = Path(policy_path).resolve()
113
102
  self.rules: list = []
114
- # 线程安全铁律:finance_runtime 是单例(Foxclaw.__init__),analyze()→apply_calibration
115
- # 会被请求线程并发调用,改 rule.threshold 并 save() 写 yaml(非原子)。写侧必须串行,
116
- # 否则并发 save 可能把 yaml 写坏。RLock:apply_calibration 持锁时还要调 save()。
117
- # 读侧(evaluate_*)只读单个 float,GIL 下原子(读到旧值或新值都合法),不加锁省热路径开销。
118
103
  self._write_lock = threading.RLock()
119
104
  self._load_or_init()
120
105
 
@@ -122,9 +107,6 @@ class FinancePolicyEngine:
122
107
  if self.policy_path.exists():
123
108
  self._load()
124
109
  else:
125
- # deepcopy 而非 list():list() 只复制**列表**、不复制里面的 FinancePolicyRule 对象,
126
- # 于是所有实例共享同一批可变规则 —— 任何一处改阈值(rule.threshold=...)会污染
127
- # 其它 runtime **以及模块级 DEFAULT_RULES**(要重启才恢复)。规则是可变对象,必须深拷。
128
110
  self.rules = copy.deepcopy(DEFAULT_RULES)
129
111
  self.save()
130
112
 
@@ -135,8 +117,6 @@ class FinancePolicyEngine:
135
117
  if not self.rules:
136
118
  self.rules = copy.deepcopy(DEFAULT_RULES)
137
119
  except Exception:
138
- # 同 _load_or_init:必须 deepcopy。这两个兜底分支是上次漏改的 ——
139
- # 自校准会写 rule.threshold,一旦走到这里就会污染模块级 DEFAULT_RULES。
140
120
  self.rules = copy.deepcopy(DEFAULT_RULES)
141
121
 
142
122
  def save(self):
@@ -147,7 +127,6 @@ class FinancePolicyEngine:
147
127
  "domain": "finance",
148
128
  "rules": [r.to_dict() for r in self.rules],
149
129
  }
150
- # 写临时文件再原子 rename:避免并发/崩溃时读到写了一半的 yaml
151
130
  tmp = self.policy_path.with_suffix(self.policy_path.suffix + ".tmp")
152
131
  tmp.write_text(
153
132
  yaml.safe_dump(data, allow_unicode=True, sort_keys=False),
@@ -161,7 +140,6 @@ class FinancePolicyEngine:
161
140
  self.rules.append(rule)
162
141
  self.save()
163
142
 
164
- # ── 阈值自校准 ────────────────────────────────────────
165
143
  def _may_calibrate(self, rule: FinancePolicyRule) -> bool:
166
144
  """这条规则的阈值,自动校准能不能动?
167
145
 
@@ -196,18 +174,17 @@ class FinancePolicyEngine:
196
174
  continue
197
175
  new_val, basis = s
198
176
  if new_val == rule.threshold:
199
- continue # 值没变 → 不写盘(analyze 每次都会调到这里)
177
+ continue
200
178
  changed.append({"rule_id": rule.id, "from": rule.threshold,
201
179
  "to": new_val, "basis": basis})
202
180
  rule.threshold = new_val
203
- rule.calibrated_value = new_val # 留痕:此后有人改动就能认出来
181
+ rule.calibrated_value = new_val
204
182
  rule.calibrated_from = basis
205
183
  rule.source = "learned"
206
184
  if changed:
207
185
  self.save()
208
186
  return changed
209
187
 
210
- # ── 守门人裁决 ────────────────────────────────────────
211
188
  def evaluate_transaction(self, txn: Transaction,
212
189
  account_balance: Optional[float] = None) -> list:
213
190
  """对单笔交易做规则裁决,返回所有触发的 verdict。"""
@@ -243,9 +220,9 @@ class FinancePolicyEngine:
243
220
  return None
244
221
 
245
222
  def evaluate_state(self, subscription_total: float = 0.0,
246
- anomalies: list = None, duplicates: list = None,
247
- forecasts: list = None, renewals: list = None,
248
- pending_refunds: list = None) -> list:
223
+ anomalies: list | None = None, duplicates: list | None = None,
224
+ forecasts: list | None = None, renewals: list | None = None,
225
+ pending_refunds: list | None = None) -> list:
249
226
  """对整体财务状态做裁决。
250
227
 
251
228
  分工:**runtime 算派生信号,本引擎只裁决**(沿用既有纹理)。
@@ -262,7 +239,6 @@ class FinancePolicyEngine:
262
239
  if not rule.enabled:
263
240
  continue
264
241
 
265
- # 同一商家高频/重复扣款(悄悄连扣)
266
242
  if rule.id == "duplicate_charge_alert":
267
243
  for d in duplicates:
268
244
  if d.get("count", 0) >= (rule.threshold or 2):
@@ -271,10 +247,8 @@ class FinancePolicyEngine:
271
247
  message=f"疑似重复扣款:{d.get('reason', '')}",
272
248
  requires_user_action=True))
273
249
 
274
- # 跨月预算超支**趋势**(还没超,但按当前速度月底会超)
275
250
  if rule.id == "overspend_trend_alert":
276
251
  for f in forecasts:
277
- # reliable 闸:月初 7 天内样本太少,外推是噪音,不报(见 overspend_forecast)
278
252
  if (f.get("reliable", True) and f.get("will_overspend")
279
253
  and f.get("spent_so_far", 0) <= f.get("monthly_limit", 0)):
280
254
  verdicts.append(PolicyVerdict(
@@ -285,7 +259,6 @@ class FinancePolicyEngine:
285
259
  f"限额 {f.get('monthly_limit', 0):.0f})"),
286
260
  requires_user_action=False))
287
261
 
288
- # 订阅/年费自动续订**前**提醒
289
262
  if rule.id == "renewal_reminder":
290
263
  for r in renewals:
291
264
  if r.get("days_until", 999) <= (rule.threshold or 7):
@@ -295,7 +268,6 @@ class FinancePolicyEngine:
295
268
  f"{r.get('amount', 0):.0f}({r.get('cadence', '')})"),
296
269
  requires_user_action=True))
297
270
 
298
- # 退款未到账(需用户显式标记 refund_expected —— 见 detect_pending_refunds)
299
271
  if rule.id == "refund_overdue_alert":
300
272
  for p in pending_refunds:
301
273
  if p.get("days_waiting", 0) >= (rule.threshold or 14):
@@ -13,14 +13,11 @@ from ..cognitive.reasoner import DomainReasoner, ReasonedVerdict, Evidence
13
13
  class FinanceGatekeeperReasoner(DomainReasoner):
14
14
  """subject = (txn, account_balance)。"""
15
15
 
16
- # Step3:LLM 只裁决"逼近阈值"的模糊带 [band×阈值, 阈值)——低于此=明显安全(省成本),
17
- # 高于阈值=规则已触发确认(不在本路径)。
18
16
  AMBIGUITY_BAND = 0.7
19
17
 
20
18
  def __init__(self, policy, runtime=None, llm=None, llm_enabled: bool = False):
21
19
  self.policy = policy
22
- self.runtime = runtime # 可选:FinanceRuntime,用于 Step2 的情境证据(历史/预算/频率)
23
- # Step3:模糊情形的 LLM 裁决器(可选、默认关)。llm 需有 complete(system,messages)->str + is_stub。
20
+ self.runtime = runtime
24
21
  self.llm = llm
25
22
  self.llm_enabled = bool(llm_enabled) and llm is not None and not getattr(llm, "is_stub", True)
26
23
 
@@ -39,12 +36,11 @@ class FinanceGatekeeperReasoner(DomainReasoner):
39
36
 
40
37
  def gather_evidence(self, subject, obs: dict) -> list:
41
38
  txn, bal = subject
42
- verdicts = self.policy.evaluate_transaction(txn, bal) # 规则=裁决证据(raw 非空)
39
+ verdicts = self.policy.evaluate_transaction(txn, bal)
43
40
  ev = [Evidence(factor=v.rule_id, detail=v.message, weight=1.0, raw=v)
44
41
  for v in verdicts]
45
42
  if not verdicts:
46
43
  ev.append(Evidence(factor="rules_clear", detail="未触发任何财务规则", weight=0.5))
47
- # Step2:结构化情境证据(informational,raw=None → **不参与裁决**,只让证据链更丰满)
48
44
  ev += self._context_evidence(txn)
49
45
  return ev
50
46
 
@@ -79,15 +75,12 @@ class FinanceGatekeeperReasoner(DomainReasoner):
79
75
  def arbitrate(self, subject, obs: dict, hyps: list, ev: list) -> ReasonedVerdict:
80
76
  triggered = [e.raw for e in ev if e.raw is not None]
81
77
  if triggered:
82
- # 【硬约束权威】规则触发 → 规则说了算(reject/confirm),**绝不问 LLM**。
83
78
  actions = [v.action for v in triggered]
84
79
  decision = "reject" if ("veto" in actions or "freeze" in actions) else "confirm"
85
80
  return ReasonedVerdict(decision, " / ".join(v.message for v in triggered),
86
81
  ev, hyps, "rules")
87
- # 规则批准:若"逼近阈值"的模糊情形且启用了 LLM,让 LLM 就证据复核(**只能 approve/confirm**)。
88
82
  if self.llm_enabled and self._is_ambiguous(obs):
89
83
  return self._llm_arbitrate(obs, hyps, ev)
90
- # 明显安全 / 未启用 LLM → 规则批准(rationale 与原守门人逐字一致)
91
84
  return ReasonedVerdict(
92
85
  "approve", f"财务批准:{obs['category']} 预算 {obs['amount']} 在规则内",
93
86
  ev, hyps, "rules")
@@ -102,7 +95,7 @@ class FinanceGatekeeperReasoner(DomainReasoner):
102
95
  thr = self._large_threshold()
103
96
  if thr <= 0 or not obs.get("is_expense"):
104
97
  return False
105
- return self.AMBIGUITY_BAND * thr <= obs["amount"] < thr # 逼近阈值但未触发规则
98
+ return self.AMBIGUITY_BAND * thr <= obs["amount"] < thr
106
99
 
107
100
  def _llm_arbitrate(self, obs: dict, hyps: list, ev: list) -> ReasonedVerdict:
108
101
  """【安全不变量】只在"规则已批准"的模糊情形被调;**只能返回 approve 或 confirm**,
@@ -124,6 +117,5 @@ class FinanceGatekeeperReasoner(DomainReasoner):
124
117
  return ReasonedVerdict(
125
118
  "approve", f"财务批准:{obs['category']} 预算 {obs['amount']}(LLM 复核无异常)",
126
119
  ev, hyps, "llm")
127
- # 响应不明 → fail-safe(绝不因 LLM 含糊而自动放行)
128
120
  return ReasonedVerdict("confirm", "LLM 响应不明,保守转确认(fail-safe)",
129
121
  ev, hyps, "llm_failsafe")
@@ -8,16 +8,16 @@ from collections import defaultdict
8
8
  from datetime import datetime, timedelta
9
9
 
10
10
  from .models import Budget
11
+ from typing import Any
11
12
 
12
13
 
13
14
  class FinanceReasoning:
14
15
  """财务推理引擎:把交易流变成可决策的洞察。"""
15
16
 
16
- # ── 预算推理 ──────────────────────────────────────────
17
17
  @staticmethod
18
- def spend_by_category(transactions: list, period: str = None) -> dict:
18
+ def spend_by_category(transactions: list, period: str | None = None) -> dict:
19
19
  """按分类汇总支出(绝对值)。period 形如 '2026-06',None=全部。"""
20
- out = defaultdict(float)
20
+ out: defaultdict[str, float] = defaultdict(float)
21
21
  for t in transactions:
22
22
  if not t.is_expense:
23
23
  continue
@@ -27,7 +27,7 @@ class FinanceReasoning:
27
27
  return dict(out)
28
28
 
29
29
  @staticmethod
30
- def budget_status(transactions: list, budgets: list, period: str = None) -> list:
30
+ def budget_status(transactions: list, budgets: list, period: str | None = None) -> list:
31
31
  """计算每个预算分类的使用状态。"""
32
32
  period = period or datetime.now().strftime("%Y-%m")
33
33
  spent = FinanceReasoning.spend_by_category(transactions, period)
@@ -40,22 +40,18 @@ class FinanceReasoning:
40
40
 
41
41
  @staticmethod
42
42
  def overspend_forecast(transactions: list, budget: Budget,
43
- period: str = None, now=None) -> dict:
43
+ period: str | None = None, now=None) -> dict:
44
44
  """分类超支预测:按当前消费速度,推算月底是否超预算。"""
45
45
  now = now or datetime.now()
46
46
  period = period or now.strftime("%Y-%m")
47
47
  spent = FinanceReasoning.spend_by_category(transactions, period).get(
48
48
  budget.category, 0.0)
49
- # 当前是本月第几天
50
49
  day = now.day
51
- days_in_month = 30 # 简化
50
+ days_in_month = 30
52
51
  if day <= 0:
53
52
  day = 1
54
53
  daily_rate = spent / day
55
54
  projected = daily_rate * days_in_month
56
- # 月初外推不可信:day=1 时一笔正常消费就被 ×30 放大成"月底将超支"。
57
- # 前 7 天样本太少,趋势是噪音 —— 标记 reliable=False,让裁决层(overspend_trend_alert)
58
- # 这一周内闭嘴。宁可晚一周提醒真超支,也不要天天误报把用户逼到关掉守门人。
59
55
  reliable = day >= 7
60
56
  return {
61
57
  "category": budget.category,
@@ -69,7 +65,6 @@ class FinanceReasoning:
69
65
  "day_of_month": day,
70
66
  }
71
67
 
72
- # ── 风险判断 ──────────────────────────────────────────
73
68
  @staticmethod
74
69
  def detect_anomalies(transactions: list, threshold: float = 3.5) -> list:
75
70
  """异常消费检测:用 MAD(中位数绝对偏差)而非均值/标准差,
@@ -86,13 +81,10 @@ class FinanceReasoning:
86
81
  if len(amounts) < 3:
87
82
  continue
88
83
  med = median(amounts)
89
- # MAD = 偏离中位数的中位数
90
84
  mad = median([abs(a - med) for a in amounts])
91
85
  if mad == 0:
92
- # 退化:用均值兜底
93
86
  mad = (sum(abs(a - med) for a in amounts) / len(amounts)) or 1e-9
94
87
  for t in txns:
95
- # 修正 Z 分数(0.6745 是正态分布常数)
96
88
  mz = 0.6745 * (abs(t.amount) - med) / mad
97
89
  if mz >= threshold:
98
90
  anomalies.append({
@@ -102,8 +94,6 @@ class FinanceReasoning:
102
94
  "category": cat,
103
95
  "z_score": round(mz, 2),
104
96
  "category_median": round(med, 2),
105
- # med>0 才显示"倍数"(当前 is_expense 排除非负额已保证 med>0,
106
- # 此处显式防御,避免未来改动 is_expense 定义后除零)
107
97
  "reason": (f"{cat} 类异常消费,金额是中位数的 {abs(t.amount)/med:.1f} 倍"
108
98
  if med > 0 else f"{cat} 类异常消费(金额 {abs(t.amount):.0f})"),
109
99
  })
@@ -144,7 +134,7 @@ class FinanceReasoning:
144
134
  同一商户按"最近一次扣款"代表其当前月度订阅价(订阅可能涨价,
145
135
  取最新而非遍历到的任意一条;也避免同商户多月记录被累加成虚高总额)。
146
136
  """
147
- latest = {} # entity -> (dt, amount)
137
+ latest: dict[str, tuple[Any, Any]] = {}
148
138
  for t in transactions:
149
139
  if t.is_recurring and t.is_expense:
150
140
  prev = latest.get(t.entity)
@@ -184,7 +174,7 @@ class FinanceReasoning:
184
174
  out = []
185
175
  for entity, txns in by_entity.items():
186
176
  if len(txns) < 2:
187
- continue # 只有一次扣款 → 无法判周期
177
+ continue
188
178
  ts = sorted(txns, key=lambda x: x.dt)
189
179
  gaps = [(ts[i].dt - ts[i - 1].dt).days for i in range(1, len(ts))]
190
180
  gaps = [g for g in gaps if g > 0]
@@ -195,7 +185,7 @@ class FinanceReasoning:
195
185
  elif 85 <= g <= 95: cadence, period = "季订", 90
196
186
  elif 355 <= g <= 375: cadence, period = "年订", 365
197
187
  else:
198
- continue # 间隔紊乱 → 不预测(不制造误报)
188
+ continue
199
189
  next_dt = ts[-1].dt + timedelta(days=period)
200
190
  days_until = (next_dt - now).days
201
191
  if 0 <= days_until <= horizon_days:
@@ -215,17 +205,13 @@ class FinanceReasoning:
215
205
  """
216
206
  now = now or datetime.now()
217
207
  credits = [t for t in transactions if not t.is_expense]
218
- claimed = set() # 已被某笔退款认领的入账(按 id)
208
+ claimed = set()
219
209
  out = []
220
- # 先处理较早的支出,让它优先认领入账(FIFO,符合"先欠先还")
221
210
  expected = sorted((t for t in transactions
222
211
  if t.is_expense and "refund_expected" in (t.tags or [])),
223
212
  key=lambda x: x.dt)
224
213
  for t in expected:
225
214
  amt = abs(t.amount)
226
- # 同商户、等额、在该支出之后、且**尚未被别的退款认领**的入账 → 退款到账
227
- # 消耗式匹配:一笔入账只抵一笔支出。否则用户标了两笔同额 refund_expected、
228
- # 只收到一笔退款时,两笔都被误判为已退款 → 第二笔漏报(其实还没到账)。
229
215
  match = next((c for c in credits
230
216
  if id(c) not in claimed and c.entity == t.entity
231
217
  and abs(abs(c.amount) - amt) < 0.01 and c.dt >= t.dt), None)
@@ -238,9 +224,6 @@ class FinanceReasoning:
238
224
  "days_waiting": waiting, "transaction_id": t.id})
239
225
  return out
240
226
 
241
- # ── 阈值自校准 ────────────────────────────────────────
242
- # 样本门槛:少于这些就不校准,继续用出厂默认。宁可用"公认的经验值",
243
- # 也不要用 3 笔数据算出来的、听起来很个性化的垃圾阈值。
244
227
  CALIB_MIN_TXNS = 20
245
228
  CALIB_MIN_DAYS = 30
246
229
 
@@ -272,22 +255,19 @@ class FinanceReasoning:
272
255
  dts = [t.dt for t in expenses]
273
256
  span_days = (max(dts) - min(dts)).days
274
257
  if span_days < FinanceReasoning.CALIB_MIN_DAYS:
275
- return {} # 跨度太短 → 分布不可信
258
+ return {}
276
259
 
277
260
  def _round100(v):
278
- # 取整到百:阈值是给人看的,"800" 比 "783.42" 可信也好记
279
261
  return max(100.0, round(v / 100.0) * 100.0)
280
262
 
281
263
  amts = [abs(t.amount) for t in expenses]
282
264
  med = median(amts)
283
265
  mad = median([abs(a - med) for a in amts])
284
- if mad == 0: # 退化(消费高度一致)→ 用平均绝对偏差兜底
266
+ if mad == 0:
285
267
  mad = (sum(abs(a - med) for a in amts) / len(amts)) or 1e-9
286
- # 3.5/0.6745:把 detect_anomalies 的修正 Z 阈值(3.5)换算回原尺度偏移量,
287
- # 使 large 阈值 = 恰好触发异常检测的单笔金额。两条规则从此共用一把尺。
288
268
  large = min(_round100(med + (3.5 / 0.6745) * mad), 20000.0)
289
269
 
290
- by_day = defaultdict(float)
270
+ by_day: defaultdict[Any, float] = defaultdict(float)
291
271
  for t in expenses:
292
272
  by_day[t.dt.date()] += abs(t.amount)
293
273
  daily_med = median(by_day.values())
@@ -301,7 +281,6 @@ class FinanceReasoning:
301
281
  f"{len(expenses)} 笔 / {span_days} 天)"),
302
282
  }
303
283
 
304
- # ── 预测 ──────────────────────────────────────────────
305
284
  @staticmethod
306
285
  def burn_rate(transactions: list, days: int = 30) -> dict:
307
286
  """烧钱速度:近 N 天的日均净流出。"""
@@ -24,9 +24,9 @@ class ExternalRecord:
24
24
  """外部流水记录(来自银行/支付宝账单)。"""
25
25
  amount: float
26
26
  entity: str
27
- date: str # ISO 日期或 YYYY-MM-DD
27
+ date: str
28
28
  account: str = ""
29
- ref: str = "" # 外部流水号
29
+ ref: str = ""
30
30
 
31
31
  def to_dict(self) -> dict:
32
32
  return {"amount": self.amount, "entity": self.entity,
@@ -95,7 +95,7 @@ class Reconciler:
95
95
 
96
96
  def __init__(self, tolerance_days: int = 3, amount_tolerance: float = 0.01):
97
97
  self.tolerance_days = tolerance_days
98
- self.amount_tolerance = amount_tolerance # 金额绝对差容忍(<=此值视为相等)
98
+ self.amount_tolerance = amount_tolerance
99
99
 
100
100
  def _amount_eq(self, a: float, b: float) -> bool:
101
101
  """金额模糊相等:绝对值在容差范围内 且 收支方向一致(都支出或都收入)。"""
@@ -105,12 +105,11 @@ class Reconciler:
105
105
  def reconcile(self, transactions: list, external: list) -> ReconciliationResult:
106
106
  """对账主流程。transactions=系统交易,external=[ExternalRecord]。"""
107
107
  res = ReconciliationResult()
108
- used_ext = set() # 已匹配的外部记录索引
108
+ used_ext = set()
109
109
 
110
110
  for t in transactions:
111
111
  t_date = _date_only(t.timestamp)
112
- best = None # (ext_idx, match_type)
113
- # 1. 精确匹配:金额 + 商户 + 同日
112
+ best = None
114
113
  for j, ext in enumerate(external):
115
114
  if j in used_ext:
116
115
  continue
@@ -118,7 +117,6 @@ class Reconciler:
118
117
  and _entity_similar(t.entity, ext.entity)
119
118
  and _days_apart(t_date, ext.date) == 0):
120
119
  best = (j, "exact"); break
121
- # 2. 模糊匹配:金额同 + 日期容忍内 + 商户相似
122
120
  if best is None:
123
121
  for j, ext in enumerate(external):
124
122
  if j in used_ext:
@@ -127,7 +125,6 @@ class Reconciler:
127
125
  and _entity_similar(t.entity, ext.entity)
128
126
  and _days_apart(t_date, ext.date) <= self.tolerance_days):
129
127
  best = (j, "fuzzy_date"); break
130
- # 3. 金额不符匹配:商户同 + 日期接近 + 同方向 + 金额不等(可能手续费)
131
128
  if best is None:
132
129
  for j, ext in enumerate(external):
133
130
  if j in used_ext:
@@ -140,7 +137,6 @@ class Reconciler:
140
137
  best = (j, "amount_mismatch"); break
141
138
 
142
139
  if best is None:
143
- # 系统有、外部无
144
140
  res.unmatched_internal.append(self._txn_brief(t))
145
141
  res.audit_log.append(
146
142
  f"[未匹配·内部] 「{t.entity} ¥{abs(t.amount):.0f}」"
@@ -165,7 +161,6 @@ class Reconciler:
165
161
  res.audit_log.append(
166
162
  f"[已匹配·{mtype}] 「{t.entity} ¥{abs(t.amount):.0f}」({t_date})")
167
163
 
168
- # 剩余未匹配的外部记录:外部有、系统无
169
164
  for j, ext in enumerate(external):
170
165
  if j not in used_ext:
171
166
  res.unmatched_external.append(ext.to_dict())