foxclaw 0.1.0 → 0.1.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (495) hide show
  1. package/README.md +54 -29
  2. package/bin/foxclaw.cjs +51 -0
  3. package/index.js +11 -10
  4. package/package.json +46 -38
  5. package/python/.dockerignore +35 -0
  6. package/python/.env.example +165 -0
  7. package/python/.gitattributes +46 -0
  8. package/python/.github/workflows/ci.yml +122 -0
  9. package/python/.github/workflows/release.yml +183 -0
  10. package/python/Dockerfile +64 -0
  11. package/python/LICENSE +21 -0
  12. package/python/MANIFEST.in +14 -0
  13. package/python/README.md +192 -0
  14. package/python/README_CN.md +324 -0
  15. package/python/bench/bm25_audit.py +199 -0
  16. package/python/bench/bm25_regression.py +178 -0
  17. package/python/bench/bm25_stress.py +159 -0
  18. package/python/bench/cd_audit.py +238 -0
  19. package/python/bench/chaos.py +142 -0
  20. package/python/bench/concurrency.py +103 -0
  21. package/python/bench/e2e_real_models.py +95 -0
  22. package/python/bench/llm_retry_test.py +248 -0
  23. package/python/bench/load_test.py +196 -0
  24. package/python/bench/vector_ivf_upgrade.py +134 -0
  25. package/python/docs/CHANNELS.md +195 -0
  26. package/python/docs/GOVERNANCE.md +164 -0
  27. package/python/docs/RELEASE.md +109 -0
  28. package/python/docs/RUN.md +75 -0
  29. package/python/docs/RUN_CN.md +121 -0
  30. package/python/docs//351/205/215/347/275/256/346/214/207/345/215/227.md +152 -0
  31. package/python/evals/__init__.py +0 -0
  32. package/python/evals/governance/README.md +61 -0
  33. package/python/evals/governance/__init__.py +0 -0
  34. package/python/evals/governance/cases.py +152 -0
  35. package/python/evals/governance/compare_embeddings.py +94 -0
  36. package/python/evals/governance/convergence.svg +11 -0
  37. package/python/evals/governance/report.json +371 -0
  38. package/python/evals/governance/run_evals.py +400 -0
  39. package/python/examples/governance_middleware_demo.py +77 -0
  40. package/python/examples/langgraph_governance_demo.py +108 -0
  41. package/python/examples/sample_itinerary.csv +6 -0
  42. package/python/examples/sample_knowledge.csv +9 -0
  43. package/python/examples/sample_transactions.csv +14 -0
  44. package/python/foxclaw/__init__.py +526 -0
  45. package/python/foxclaw/agents/__init__.py +12 -0
  46. package/python/foxclaw/agents/agents.py +35 -0
  47. package/python/foxclaw/agents/base.py +865 -0
  48. package/python/foxclaw/agents/impl/__init__.py +1 -0
  49. package/python/foxclaw/agents/impl/_helpers.py +78 -0
  50. package/python/foxclaw/agents/impl/finance.py +248 -0
  51. package/python/foxclaw/agents/impl/office.py +367 -0
  52. package/python/foxclaw/agents/impl/supervisor.py +659 -0
  53. package/python/foxclaw/agents/mixins/_context.py +151 -0
  54. package/python/foxclaw/agents/mixins/augment.py +149 -0
  55. package/python/foxclaw/agents/mixins/llm.py +66 -0
  56. package/python/foxclaw/agents/mixins/memory.py +35 -0
  57. package/python/foxclaw/agents/mixins/preference.py +81 -0
  58. package/python/foxclaw/agents/mixins/prompt.py +77 -0
  59. package/python/foxclaw/agents/mixins/skillbox.py +58 -0
  60. package/python/foxclaw/agents/mixins/tool_exec.py +443 -0
  61. package/python/foxclaw/api/__init__.py +283 -0
  62. package/python/foxclaw/api/app.py +123 -0
  63. package/python/foxclaw/api/middleware.py +317 -0
  64. package/python/foxclaw/api/routes/ahs.py +417 -0
  65. package/python/foxclaw/api/routes/attachments.py +117 -0
  66. package/python/foxclaw/api/routes/browser.py +39 -0
  67. package/python/foxclaw/api/routes/domain.py +314 -0
  68. package/python/foxclaw/api/routes/knowledge.py +163 -0
  69. package/python/foxclaw/api/routes/llm_admin.py +461 -0
  70. package/python/foxclaw/api/routes/memory.py +89 -0
  71. package/python/foxclaw/api/routes/orchestration.py +41 -0
  72. package/python/foxclaw/api/routes/recovery.py +43 -0
  73. package/python/foxclaw/api/routes/review.py +275 -0
  74. package/python/foxclaw/api/routes/search.py +37 -0
  75. package/python/foxclaw/api/routes/system.py +327 -0
  76. package/python/foxclaw/api/routes/task.py +281 -0
  77. package/python/foxclaw/api/routes/ws.py +68 -0
  78. package/python/foxclaw/api/spine.py +55 -0
  79. package/python/foxclaw/browser/__init__.py +13 -0
  80. package/python/foxclaw/browser/action/__init__.py +1 -0
  81. package/python/foxclaw/browser/action/action_executor.py +54 -0
  82. package/python/foxclaw/browser/action/action_types.py +45 -0
  83. package/python/foxclaw/browser/action/action_verifier.py +68 -0
  84. package/python/foxclaw/browser/action/url_policy.py +147 -0
  85. package/python/foxclaw/browser/adapter/__init__.py +1 -0
  86. package/python/foxclaw/browser/adapter/base_adapter.py +23 -0
  87. package/python/foxclaw/browser/adapter/web_adapter.py +46 -0
  88. package/python/foxclaw/browser/driver/__init__.py +1 -0
  89. package/python/foxclaw/browser/driver/base_driver.py +61 -0
  90. package/python/foxclaw/browser/driver/playwright_driver.py +166 -0
  91. package/python/foxclaw/browser/perception/__init__.py +1 -0
  92. package/python/foxclaw/browser/perception/dom_extractor.py +43 -0
  93. package/python/foxclaw/browser/perception/element_indexer.py +57 -0
  94. package/python/foxclaw/browser/perception/semantic_reducer.py +33 -0
  95. package/python/foxclaw/browser/perception/snapshot.py +29 -0
  96. package/python/foxclaw/browser/service.py +148 -0
  97. package/python/foxclaw/browser/session/__init__.py +1 -0
  98. package/python/foxclaw/browser/session/session_manager.py +57 -0
  99. package/python/foxclaw/capability/__init__.py +12 -0
  100. package/python/foxclaw/capability/embedding_service.py +67 -0
  101. package/python/foxclaw/channels/__init__.py +140 -0
  102. package/python/foxclaw/channels/discord.py +184 -0
  103. package/python/foxclaw/channels/dispatcher.py +204 -0
  104. package/python/foxclaw/channels/feishu.py +225 -0
  105. package/python/foxclaw/channels/openclaw_bridge.py +156 -0
  106. package/python/foxclaw/channels/signing.py +187 -0
  107. package/python/foxclaw/channels/telegram.py +121 -0
  108. package/python/foxclaw/channels/weixin.py +225 -0
  109. package/python/foxclaw/cli.py +154 -0
  110. package/python/foxclaw/cognitive/__init__.py +4 -0
  111. package/python/foxclaw/cognitive/reasoner.py +82 -0
  112. package/python/foxclaw/cognitive/state.py +147 -0
  113. package/python/foxclaw/core/__init__.py +9 -0
  114. package/python/foxclaw/core/audit_chain.py +133 -0
  115. package/python/foxclaw/core/audit_sink.py +100 -0
  116. package/python/foxclaw/core/compaction.py +283 -0
  117. package/python/foxclaw/core/config.py +65 -0
  118. package/python/foxclaw/core/confirm_nonce.py +157 -0
  119. package/python/foxclaw/core/dispatch.py +27 -0
  120. package/python/foxclaw/core/envfile.py +48 -0
  121. package/python/foxclaw/core/failure_policy.py +151 -0
  122. package/python/foxclaw/core/gateway.py +85 -0
  123. package/python/foxclaw/core/governance_audit.py +175 -0
  124. package/python/foxclaw/core/idempotency.py +289 -0
  125. package/python/foxclaw/core/injection_guard.py +221 -0
  126. package/python/foxclaw/core/memory_adapter.py +516 -0
  127. package/python/foxclaw/core/metrics.py +89 -0
  128. package/python/foxclaw/core/model_router.py +1097 -0
  129. package/python/foxclaw/core/paths.py +103 -0
  130. package/python/foxclaw/core/recovery.py +362 -0
  131. package/python/foxclaw/core/scheduler.py +144 -0
  132. package/python/foxclaw/core/secrets.py +134 -0
  133. package/python/foxclaw/core/sessions.py +157 -0
  134. package/python/foxclaw/core/settings.py +430 -0
  135. package/python/foxclaw/core/shared_write.py +198 -0
  136. package/python/foxclaw/core/steering_queue.py +153 -0
  137. package/python/foxclaw/core/system_bus.py +84 -0
  138. package/python/foxclaw/core/thought_chain.py +318 -0
  139. package/python/foxclaw/core/token_meter.py +235 -0
  140. package/python/foxclaw/core/tools.py +172 -0
  141. package/python/foxclaw/core/tracing.py +390 -0
  142. package/python/foxclaw/core/truth_pipeline.py +114 -0
  143. package/python/foxclaw/core/vec_recall_adapter.py +503 -0
  144. package/python/foxclaw/core/verify.py +181 -0
  145. package/python/foxclaw/core/web_search.py +242 -0
  146. package/python/foxclaw/finance/__init__.py +15 -0
  147. package/python/foxclaw/finance/evidence.py +153 -0
  148. package/python/foxclaw/finance/fx.py +100 -0
  149. package/python/foxclaw/finance/graph.py +188 -0
  150. package/python/foxclaw/finance/models.py +150 -0
  151. package/python/foxclaw/finance/nlparse.py +575 -0
  152. package/python/foxclaw/finance/policy.py +329 -0
  153. package/python/foxclaw/finance/reasoner.py +129 -0
  154. package/python/foxclaw/finance/reasoning.py +340 -0
  155. package/python/foxclaw/finance/reconcile.py +180 -0
  156. package/python/foxclaw/finance/runtime.py +527 -0
  157. package/python/foxclaw/governance/__init__.py +24 -0
  158. package/python/foxclaw/governance/middleware.py +184 -0
  159. package/python/foxclaw/harness/__init__.py +17 -0
  160. package/python/foxclaw/harness/_base.py +72 -0
  161. package/python/foxclaw/harness/_policy.py +359 -0
  162. package/python/foxclaw/harness/drift.py +89 -0
  163. package/python/foxclaw/harness/event_bus.py +31 -0
  164. package/python/foxclaw/harness/harness.py +446 -0
  165. package/python/foxclaw/harness/loop_detection.py +152 -0
  166. package/python/foxclaw/itinerary/__init__.py +10 -0
  167. package/python/foxclaw/itinerary/amap_client.py +117 -0
  168. package/python/foxclaw/itinerary/models.py +80 -0
  169. package/python/foxclaw/itinerary/reasoner.py +101 -0
  170. package/python/foxclaw/itinerary/reasoning.py +174 -0
  171. package/python/foxclaw/itinerary/runtime.py +148 -0
  172. package/python/foxclaw/kernel/__init__.py +13 -0
  173. package/python/foxclaw/kernel/capabilities.py +73 -0
  174. package/python/foxclaw/kernel/policy_gate.py +114 -0
  175. package/python/foxclaw/kernel/process_manager.py +205 -0
  176. package/python/foxclaw/kernel/scheduler.py +131 -0
  177. package/python/foxclaw/kernel/sentinel.py +453 -0
  178. package/python/foxclaw/kernel/sentinel_client.py +246 -0
  179. package/python/foxclaw/kernel/spine.py +97 -0
  180. package/python/foxclaw/knowledge/__init__.py +376 -0
  181. package/python/foxclaw/knowledge/truth_crystallizer.py +92 -0
  182. package/python/foxclaw/learning/__init__.py +12 -0
  183. package/python/foxclaw/learning/knowledge_graph.py +149 -0
  184. package/python/foxclaw/learning/models.py +93 -0
  185. package/python/foxclaw/learning/reasoner.py +70 -0
  186. package/python/foxclaw/learning/runtime.py +363 -0
  187. package/python/foxclaw/learning/spaced_repetition.py +96 -0
  188. package/python/foxclaw/mcp_server.py +189 -0
  189. package/python/foxclaw/orchestration/__init__.py +184 -0
  190. package/python/foxclaw/pipeline/__init__.py +23 -0
  191. package/python/foxclaw/pipeline/cognitive_pipeline.py +481 -0
  192. package/python/foxclaw/pipeline/supervisor_stages.py +230 -0
  193. package/python/foxclaw/planning/__init__.py +17 -0
  194. package/python/foxclaw/planning/llm_planner.py +232 -0
  195. package/python/foxclaw/planning/planner.py +99 -0
  196. package/python/foxclaw/search/__init__.py +24 -0
  197. package/python/foxclaw/search/agent_port.py +194 -0
  198. package/python/foxclaw/search/evidence.py +113 -0
  199. package/python/foxclaw/search/external_search.py +50 -0
  200. package/python/foxclaw/search/hkc_read_port.py +60 -0
  201. package/python/foxclaw/search/internal_search.py +73 -0
  202. package/python/foxclaw/search/models.py +129 -0
  203. package/python/foxclaw/search/planner.py +44 -0
  204. package/python/foxclaw/search/retriever.py +39 -0
  205. package/python/foxclaw/search/runtime.py +134 -0
  206. package/python/foxclaw/search/strategy.py +44 -0
  207. package/python/foxclaw/skills/__init__.py +613 -0
  208. package/python/foxclaw/skills/hooks_spec.py +73 -0
  209. package/python/foxclaw/skills/lifecycle.py +278 -0
  210. package/python/foxclaw/skills/markdown_skill.py +174 -0
  211. package/python/foxclaw/skills/markdown_store.py +39 -0
  212. package/python/foxclaw/skills/mcp_client.py +190 -0
  213. package/python/foxclaw/skills/mcp_store.py +43 -0
  214. package/python/foxclaw/skills/permission.py +180 -0
  215. package/python/foxclaw/skills/pipeline.py +386 -0
  216. package/python/foxclaw/skills/policy.py +80 -0
  217. package/python/foxclaw/skills/review.py +72 -0
  218. package/python/foxclaw/skills/store.py +57 -0
  219. package/python/frontend/assets/core_bg.jpg +0 -0
  220. package/python/frontend/deck/auth.js +32 -0
  221. package/python/frontend/deck/chat_model_picker.js +537 -0
  222. package/python/frontend/deck/domain.js +296 -0
  223. package/python/frontend/deck/main.js +1950 -0
  224. package/python/frontend/deck/markdown.js +48 -0
  225. package/python/frontend/deck/settings.js +324 -0
  226. package/python/frontend/deck/state.js +6 -0
  227. package/python/frontend/deck/util.js +6 -0
  228. package/python/frontend/deck/visuals.js +212 -0
  229. package/python/frontend/deck.css +855 -0
  230. package/python/frontend/deck.html +686 -0
  231. package/python/frontend/deck_v2.html +341 -0
  232. package/python/frontend/planet/index.html +266 -0
  233. package/python/frontend/planet/planet.css +579 -0
  234. package/python/frontend/planet/planet.js +1637 -0
  235. package/python/frontend/planet/vendor/three.core.min.js +6 -0
  236. package/python/frontend/planet/vendor/three.module.min.js +6 -0
  237. package/python/frontend/v3/assets/index-BD7VPbMV.js +33 -0
  238. package/python/frontend/v3/assets/index-CAvnYWGH.css +1 -0
  239. package/python/frontend/v3/index.html +15 -0
  240. package/python/frontend/widget_runtime.js +295 -0
  241. package/python/frontend/widgets.js +696 -0
  242. package/python/frontend_src/index.html +14 -0
  243. package/python/frontend_src/package-lock.json +1376 -0
  244. package/python/frontend_src/package.json +19 -0
  245. package/python/frontend_src/src/App.vue +62 -0
  246. package/python/frontend_src/src/api.js +89 -0
  247. package/python/frontend_src/src/components/ChatDock.vue +377 -0
  248. package/python/frontend_src/src/components/ComposerMenu.vue +75 -0
  249. package/python/frontend_src/src/components/DocViewer.vue +171 -0
  250. package/python/frontend_src/src/components/Inspector.vue +241 -0
  251. package/python/frontend_src/src/components/LeftRail.vue +394 -0
  252. package/python/frontend_src/src/components/MenuPopover.vue +130 -0
  253. package/python/frontend_src/src/components/MessageStream.vue +401 -0
  254. package/python/frontend_src/src/components/ModelPicker.vue +355 -0
  255. package/python/frontend_src/src/components/QuickIngest.vue +118 -0
  256. package/python/frontend_src/src/components/Stage.vue +226 -0
  257. package/python/frontend_src/src/components/ToolMenu.vue +144 -0
  258. package/python/frontend_src/src/components/TopBar.vue +90 -0
  259. package/python/frontend_src/src/components/panels/ActivityPanel.vue +84 -0
  260. package/python/frontend_src/src/components/panels/AgentsPanel.vue +86 -0
  261. package/python/frontend_src/src/components/panels/KnowledgePanel.vue +441 -0
  262. package/python/frontend_src/src/components/panels/MemoryPanel.vue +118 -0
  263. package/python/frontend_src/src/components/panels/PanelBox.vue +51 -0
  264. package/python/frontend_src/src/components/panels/ReviewPanel.vue +308 -0
  265. package/python/frontend_src/src/components/panels/SettingsPanel.vue +254 -0
  266. package/python/frontend_src/src/components/panels/TasksPanel.vue +74 -0
  267. package/python/frontend_src/src/components/panels/ToolsPanel.vue +640 -0
  268. package/python/frontend_src/src/components/panels/UsagePanel.vue +105 -0
  269. package/python/frontend_src/src/lib/fmt.js +48 -0
  270. package/python/frontend_src/src/lib/md.js +204 -0
  271. package/python/frontend_src/src/main.js +7 -0
  272. package/python/frontend_src/src/stores/app.js +106 -0
  273. package/python/frontend_src/src/stores/chat.js +484 -0
  274. package/python/frontend_src/src/stores/model.js +104 -0
  275. package/python/frontend_src/src/stores/panels.js +330 -0
  276. package/python/frontend_src/src/styles/base.css +138 -0
  277. package/python/frontend_src/src/styles/panel.css +117 -0
  278. package/python/frontend_src/src/styles/tokens.css +78 -0
  279. package/python/frontend_src/vite.config.js +46 -0
  280. package/python/pyproject.toml +265 -0
  281. package/python/skills/README.md +123 -0
  282. package/python/skills/execution-contract/SKILL.md +30 -0
  283. package/python/skills/general-assistant/SKILL.md +34 -0
  284. package/python/skills/planner/SKILL.md +33 -0
  285. package/python/skills/schedule-itinerary/SKILL.md +35 -0
  286. package/python/skills/skill-composition/SKILL.md +31 -0
  287. package/python/skills/skill-guard/SKILL.md +67 -0
  288. package/python/skills/skill-memory/SKILL.md +50 -0
  289. package/python/skills/skill-router/SKILL.md +35 -0
  290. package/python/tests/conftest.py +76 -0
  291. package/python/tests/run_all.py +263 -0
  292. package/python/tests/test_agent_lifecycle.py +360 -0
  293. package/python/tests/test_ahs_p2.py +414 -0
  294. package/python/tests/test_anthropic_tools.py +206 -0
  295. package/python/tests/test_api.py +87 -0
  296. package/python/tests/test_api_auth.py +83 -0
  297. package/python/tests/test_api_host_guard.py +69 -0
  298. package/python/tests/test_attachments.py +167 -0
  299. package/python/tests/test_audit_chain.py +121 -0
  300. package/python/tests/test_audit_sink.py +136 -0
  301. package/python/tests/test_browser.py +153 -0
  302. package/python/tests/test_browser_request_guard.py +184 -0
  303. package/python/tests/test_capabilities.py +158 -0
  304. package/python/tests/test_channel_signing.py +192 -0
  305. package/python/tests/test_channel_trust.py +109 -0
  306. package/python/tests/test_channels.py +184 -0
  307. package/python/tests/test_cli.py +46 -0
  308. package/python/tests/test_compaction_toolpair.py +119 -0
  309. package/python/tests/test_config_layering.py +65 -0
  310. package/python/tests/test_confirm_nonce.py +197 -0
  311. package/python/tests/test_context_compaction_wiring.py +130 -0
  312. package/python/tests/test_dispatch.py +95 -0
  313. package/python/tests/test_domain_evals.py +403 -0
  314. package/python/tests/test_domain_import.py +78 -0
  315. package/python/tests/test_domain_reasoners.py +148 -0
  316. package/python/tests/test_e2e.py +178 -0
  317. package/python/tests/test_emb_cache.py +107 -0
  318. package/python/tests/test_embedding_singleton.py +89 -0
  319. package/python/tests/test_envfile.py +83 -0
  320. package/python/tests/test_evals_smoke.py +114 -0
  321. package/python/tests/test_event_spine.py +182 -0
  322. package/python/tests/test_evidence_semantic.py +101 -0
  323. package/python/tests/test_evidence_sse.py +97 -0
  324. package/python/tests/test_file_ingest.py +146 -0
  325. package/python/tests/test_finance.py +153 -0
  326. package/python/tests/test_finance_ledger.py +252 -0
  327. package/python/tests/test_finance_persist.py +178 -0
  328. package/python/tests/test_gateway.py +144 -0
  329. package/python/tests/test_governance.py +468 -0
  330. package/python/tests/test_governance_audit.py +359 -0
  331. package/python/tests/test_governance_middleware.py +143 -0
  332. package/python/tests/test_hkc.py +286 -0
  333. package/python/tests/test_hkc_index_build_manager.py +339 -0
  334. package/python/tests/test_hkc_manifest_reuse.py +317 -0
  335. package/python/tests/test_hkc_search_event_sync.py +533 -0
  336. package/python/tests/test_import_path_guard.py +161 -0
  337. package/python/tests/test_injection_guard.py +211 -0
  338. package/python/tests/test_integration.py +141 -0
  339. package/python/tests/test_itinerary.py +103 -0
  340. package/python/tests/test_kee_semantic.py +89 -0
  341. package/python/tests/test_kernel.py +222 -0
  342. package/python/tests/test_learning.py +110 -0
  343. package/python/tests/test_llm_admin.py +160 -0
  344. package/python/tests/test_llm_planner.py +366 -0
  345. package/python/tests/test_markdown_skills.py +73 -0
  346. package/python/tests/test_markdown_skills_openclaw.py +71 -0
  347. package/python/tests/test_mcp_connectors.py +109 -0
  348. package/python/tests/test_mcp_guardian_discovery.py +91 -0
  349. package/python/tests/test_mcp_no_memory.py +43 -0
  350. package/python/tests/test_mcp_policy_gate.py +77 -0
  351. package/python/tests/test_mcp_server.py +95 -0
  352. package/python/tests/test_memory_recall.py +95 -0
  353. package/python/tests/test_metrics.py +66 -0
  354. package/python/tests/test_multi_chain.py +102 -0
  355. package/python/tests/test_multimodal.py +135 -0
  356. package/python/tests/test_negotiation.py +146 -0
  357. package/python/tests/test_orchestration.py +119 -0
  358. package/python/tests/test_orchestration_decoupled.py +109 -0
  359. package/python/tests/test_p3_p5_hardening.py +338 -0
  360. package/python/tests/test_parallel_dispatch.py +215 -0
  361. package/python/tests/test_pipeline.py +533 -0
  362. package/python/tests/test_plan_visible.py +86 -0
  363. package/python/tests/test_planner.py +95 -0
  364. package/python/tests/test_policy_gate.py +109 -0
  365. package/python/tests/test_reasoner.py +185 -0
  366. package/python/tests/test_release_check.py +263 -0
  367. package/python/tests/test_resume_checkpoint.py +156 -0
  368. package/python/tests/test_routing.py +95 -0
  369. package/python/tests/test_scheduler.py +141 -0
  370. package/python/tests/test_search.py +204 -0
  371. package/python/tests/test_secrets.py +159 -0
  372. package/python/tests/test_semantic.py +78 -0
  373. package/python/tests/test_sessions.py +91 -0
  374. package/python/tests/test_settings.py +281 -0
  375. package/python/tests/test_shadow_learning.py +330 -0
  376. package/python/tests/test_shared_write.py +207 -0
  377. package/python/tests/test_skill_taint_gate.py +154 -0
  378. package/python/tests/test_skills.py +133 -0
  379. package/python/tests/test_stats_and_events.py +165 -0
  380. package/python/tests/test_streaming.py +60 -0
  381. package/python/tests/test_supervisor_pipeline.py +177 -0
  382. package/python/tests/test_supervisor_triage.py +108 -0
  383. package/python/tests/test_taint.py +117 -0
  384. package/python/tests/test_task_stream.py +85 -0
  385. package/python/tests/test_thinking_sse.py +67 -0
  386. package/python/tests/test_thinking_stream.py +76 -0
  387. package/python/tests/test_tool_center.py +129 -0
  388. package/python/tests/test_tool_parallel.py +113 -0
  389. package/python/tests/test_tool_params.py +103 -0
  390. package/python/tests/test_tool_use.py +242 -0
  391. package/python/tests/test_toplevel_concurrency.py +117 -0
  392. package/python/tests/test_tracing.py +290 -0
  393. package/python/tests/test_truth_crystallizer.py +127 -0
  394. package/python/tests/test_truth_flow.py +93 -0
  395. package/python/tests/test_url_policy.py +53 -0
  396. package/python/tests/test_verify_evidence.py +225 -0
  397. package/python/tests/test_version_sync.py +71 -0
  398. package/python/tests/test_web_trigger.py +99 -0
  399. package/python/tools/check_faiss_type.py +31 -0
  400. package/python/tools/check_log.ps1 +46 -0
  401. package/python/tools/ivf_status.py +72 -0
  402. package/python/tools/mypy_baseline.txt +5 -0
  403. package/python/tools/mypy_ratchet.py +97 -0
  404. package/python/tools/pack.py +178 -0
  405. package/python/tools/release_check.py +349 -0
  406. package/python/tools/restart_foxclaw.ps1 +74 -0
  407. package/python/tools/shared_write_audit.py +204 -0
  408. package/python/tools/size_baseline.txt +28 -0
  409. package/python/tools/size_check.py +102 -0
  410. package/python/tools/verify_env.bat +2 -0
  411. package/python/tools/verify_env.py +25 -0
  412. package/python/vendor/HMR_LICENSE +37 -0
  413. package/python/vendor/HMR_README.md +65 -0
  414. package/python/vendor/hkc/LICENSE +37 -0
  415. package/python/vendor/hkc/hkc_ace/__init__.py +0 -0
  416. package/python/vendor/hkc/hkc_ace/ace.py +457 -0
  417. package/python/vendor/hkc/hkc_ace/skill_taxonomy.json +465 -0
  418. package/python/vendor/hkc/hkc_api/__init__.py +0 -0
  419. package/python/vendor/hkc/hkc_api/container.py +416 -0
  420. package/python/vendor/hkc/hkc_api/ingest_jobs.py +103 -0
  421. package/python/vendor/hkc/hkc_api/main.py +157 -0
  422. package/python/vendor/hkc/hkc_api/routes/__init__.py +0 -0
  423. package/python/vendor/hkc/hkc_api/routes/ability.py +151 -0
  424. package/python/vendor/hkc/hkc_api/routes/config.py +51 -0
  425. package/python/vendor/hkc/hkc_api/routes/knowledge.py +630 -0
  426. package/python/vendor/hkc/hkc_api/routes/search.py +88 -0
  427. package/python/vendor/hkc/hkc_api/schemas.py +153 -0
  428. package/python/vendor/hkc/hkc_api/synthesis.py +179 -0
  429. package/python/vendor/hkc/hkc_core/__init__.py +0 -0
  430. package/python/vendor/hkc/hkc_core/graph/__init__.py +0 -0
  431. package/python/vendor/hkc/hkc_core/graph/base.py +188 -0
  432. package/python/vendor/hkc/hkc_core/graph/sqlite_store.py +789 -0
  433. package/python/vendor/hkc/hkc_core/models/__init__.py +0 -0
  434. package/python/vendor/hkc/hkc_core/models/enums.py +65 -0
  435. package/python/vendor/hkc/hkc_core/models/ku.py +307 -0
  436. package/python/vendor/hkc/hkc_core/utils/__init__.py +0 -0
  437. package/python/vendor/hkc/hkc_core/utils/id_gen.py +113 -0
  438. package/python/vendor/hkc/hkc_crystallizer/__init__.py +44 -0
  439. package/python/vendor/hkc/hkc_crystallizer/adapters.py +94 -0
  440. package/python/vendor/hkc/hkc_crystallizer/candidate.py +145 -0
  441. package/python/vendor/hkc/hkc_crystallizer/crystallizer.py +179 -0
  442. package/python/vendor/hkc/hkc_crystallizer/ingress.py +90 -0
  443. package/python/vendor/hkc/hkc_kde/__init__.py +0 -0
  444. package/python/vendor/hkc/hkc_kde/chunker.py +197 -0
  445. package/python/vendor/hkc/hkc_kde/extractor.py +623 -0
  446. package/python/vendor/hkc/hkc_kde/kde.py +298 -0
  447. package/python/vendor/hkc/hkc_kde/loaders/__init__.py +0 -0
  448. package/python/vendor/hkc/hkc_kde/loaders/ebook_loader.py +153 -0
  449. package/python/vendor/hkc/hkc_kde/loaders/office_loader.py +194 -0
  450. package/python/vendor/hkc/hkc_kde/loaders/pdf_loader.py +162 -0
  451. package/python/vendor/hkc/hkc_kde/loaders/text_loader.py +157 -0
  452. package/python/vendor/hkc/hkc_kde/loaders/web_loader.py +91 -0
  453. package/python/vendor/hkc/hkc_kde/models.py +74 -0
  454. package/python/vendor/hkc/hkc_kde/packager.py +329 -0
  455. package/python/vendor/hkc/hkc_kde/relation_disc.py +151 -0
  456. package/python/vendor/hkc/hkc_kee/__init__.py +0 -0
  457. package/python/vendor/hkc/hkc_kee/dedup.py +227 -0
  458. package/python/vendor/hkc/hkc_kee/fingerprint.py +106 -0
  459. package/python/vendor/hkc/hkc_kee/kee.py +541 -0
  460. package/python/vendor/hkc/hkc_kep/__init__.py +0 -0
  461. package/python/vendor/hkc/hkc_kep/event_bus.py +205 -0
  462. package/python/vendor/hkc/hkc_sdk/__init__.py +63 -0
  463. package/python/vendor/hkc/hkc_sdk/base.py +151 -0
  464. package/python/vendor/hkc/hkc_sdk/direct_client.py +153 -0
  465. package/python/vendor/hkc/hkc_sdk/exceptions.py +39 -0
  466. package/python/vendor/hkc/hkc_sdk/http_client.py +166 -0
  467. package/python/vendor/hkc/hkc_sdk/models.py +171 -0
  468. package/python/vendor/hkc/hkc_search/__init__.py +0 -0
  469. package/python/vendor/hkc/hkc_search/bm25.py +325 -0
  470. package/python/vendor/hkc/hkc_search/embedding_backends.py +374 -0
  471. package/python/vendor/hkc/hkc_search/graph_search.py +132 -0
  472. package/python/vendor/hkc/hkc_search/hybrid.py +331 -0
  473. package/python/vendor/hkc/hkc_search/index_build_manager.py +378 -0
  474. package/python/vendor/hkc/hkc_search/vector_search.py +484 -0
  475. package/python/vendor/hmr/__init__.py +0 -0
  476. package/python/vendor/hmr/core/__init__.py +0 -0
  477. package/python/vendor/hmr/core/hmr.py +684 -0
  478. package/python/vendor/hmr/core/models.py +310 -0
  479. package/python/vendor/hmr/engines/__init__.py +0 -0
  480. package/python/vendor/hmr/engines/evolution.py +710 -0
  481. package/python/vendor/hmr/engines/jit_compiler.py +472 -0
  482. package/python/vendor/hmr/engines/lifecycle.py +264 -0
  483. package/python/vendor/hmr/engines/policy.py +625 -0
  484. package/python/vendor/hmr/engines/recall.py +260 -0
  485. package/python/vendor/hmr/engines/runtime_state.py +158 -0
  486. package/python/vendor/hmr/engines/scheduler.py +323 -0
  487. package/python/vendor/hmr/engines/semantic.py +150 -0
  488. package/python/vendor/hmr/engines/temporal.py +261 -0
  489. package/python/vendor/hmr/engines/thought_chain.py +635 -0
  490. package/python/vendor/hmr/graph/__init__.py +0 -0
  491. package/python/vendor/hmr/graph/cwg.py +338 -0
  492. package/python/vendor/hmr/graph/memory_graph.py +530 -0
  493. package/python/vendor/hmr/storage/__init__.py +0 -0
  494. package/python/vendor/hmr/storage/memory_fs.py +300 -0
  495. package/python/vendor/hmr/storage/vector_store.py +520 -0
@@ -0,0 +1,340 @@
1
+ """
2
+ Foxclaw 财务子系统 — 推理层(财务 Agent 的大脑)
3
+ 纯算法,不依赖外部服务。三块:预算推理、风险判断、预测。
4
+ 核心思想:钱 = 状态,不是记录;财务 Agent 是时间连续系统。
5
+ """
6
+
7
+ from collections import defaultdict
8
+ from datetime import datetime, timedelta
9
+
10
+ from .models import Budget
11
+
12
+
13
+ class FinanceReasoning:
14
+ """财务推理引擎:把交易流变成可决策的洞察。"""
15
+
16
+ # ── 预算推理 ──────────────────────────────────────────
17
+ @staticmethod
18
+ def spend_by_category(transactions: list, period: str = None) -> dict:
19
+ """按分类汇总支出(绝对值)。period 形如 '2026-06',None=全部。"""
20
+ out = defaultdict(float)
21
+ for t in transactions:
22
+ if not t.is_expense:
23
+ continue
24
+ if period and not t.timestamp.startswith(period):
25
+ continue
26
+ out[t.category] += abs(t.amount)
27
+ return dict(out)
28
+
29
+ @staticmethod
30
+ def budget_status(transactions: list, budgets: list, period: str = None) -> list:
31
+ """计算每个预算分类的使用状态。"""
32
+ period = period or datetime.now().strftime("%Y-%m")
33
+ spent = FinanceReasoning.spend_by_category(transactions, period)
34
+ result = []
35
+ for b in budgets:
36
+ b.spent = spent.get(b.category, 0.0)
37
+ b.period = period
38
+ result.append(b.to_dict())
39
+ return result
40
+
41
+ @staticmethod
42
+ def overspend_forecast(transactions: list, budget: Budget,
43
+ period: str = None, now=None) -> dict:
44
+ """分类超支预测:按当前消费速度,推算月底是否超预算。"""
45
+ now = now or datetime.now()
46
+ period = period or now.strftime("%Y-%m")
47
+ spent = FinanceReasoning.spend_by_category(transactions, period).get(
48
+ budget.category, 0.0)
49
+ # 当前是本月第几天
50
+ day = now.day
51
+ days_in_month = 30 # 简化
52
+ if day <= 0:
53
+ day = 1
54
+ daily_rate = spent / day
55
+ projected = daily_rate * days_in_month
56
+ # 月初外推不可信:day=1 时一笔正常消费就被 ×30 放大成"月底将超支"。
57
+ # 前 7 天样本太少,趋势是噪音 —— 标记 reliable=False,让裁决层(overspend_trend_alert)
58
+ # 这一周内闭嘴。宁可晚一周提醒真超支,也不要天天误报把用户逼到关掉守门人。
59
+ reliable = day >= 7
60
+ return {
61
+ "category": budget.category,
62
+ "spent_so_far": round(spent, 2),
63
+ "daily_rate": round(daily_rate, 2),
64
+ "projected_month_end": round(projected, 2),
65
+ "monthly_limit": budget.monthly_limit,
66
+ "will_overspend": projected > budget.monthly_limit,
67
+ "projected_overage": round(max(0, projected - budget.monthly_limit), 2),
68
+ "reliable": reliable,
69
+ "day_of_month": day,
70
+ }
71
+
72
+ # ── 风险判断 ──────────────────────────────────────────
73
+ @staticmethod
74
+ def detect_anomalies(transactions: list, threshold: float = 3.5) -> list:
75
+ """异常消费检测:用 MAD(中位数绝对偏差)而非均值/标准差,
76
+ 小样本下抗离群值更稳健(一笔大额不会把自己的判定基线抬高)。
77
+ threshold 是修正 Z 分数阈值(3.5 是常用经验值)。"""
78
+ from statistics import median
79
+ by_cat = defaultdict(list)
80
+ for t in transactions:
81
+ if t.is_expense:
82
+ by_cat[t.category].append(t)
83
+ anomalies = []
84
+ for cat, txns in by_cat.items():
85
+ amounts = [abs(t.amount) for t in txns]
86
+ if len(amounts) < 3:
87
+ continue
88
+ med = median(amounts)
89
+ # MAD = 偏离中位数的中位数
90
+ mad = median([abs(a - med) for a in amounts])
91
+ if mad == 0:
92
+ # 退化:用均值兜底
93
+ mad = (sum(abs(a - med) for a in amounts) / len(amounts)) or 1e-9
94
+ for t in txns:
95
+ # 修正 Z 分数(0.6745 是正态分布常数)
96
+ mz = 0.6745 * (abs(t.amount) - med) / mad
97
+ if mz >= threshold:
98
+ anomalies.append({
99
+ "transaction_id": t.id,
100
+ "entity": t.entity,
101
+ "amount": t.amount,
102
+ "category": cat,
103
+ "z_score": round(mz, 2),
104
+ "category_median": round(med, 2),
105
+ # med>0 才显示"倍数"(当前 is_expense 排除非负额已保证 med>0,
106
+ # 此处显式防御,避免未来改动 is_expense 定义后除零)
107
+ "reason": (f"{cat} 类异常消费,金额是中位数的 {abs(t.amount)/med:.1f} 倍"
108
+ if med > 0 else f"{cat} 类异常消费(金额 {abs(t.amount):.0f})"),
109
+ })
110
+ return anomalies
111
+
112
+ @staticmethod
113
+ def detect_duplicate_charges(transactions: list,
114
+ window_hours: int = 48) -> list:
115
+ """重复扣款识别:同商户、同金额、短时间内多次。"""
116
+ dups = []
117
+ seen = defaultdict(list)
118
+ for t in transactions:
119
+ if not t.is_expense:
120
+ continue
121
+ key = (t.entity, round(abs(t.amount), 2))
122
+ seen[key].append(t)
123
+ for (entity, amount), txns in seen.items():
124
+ if len(txns) < 2:
125
+ continue
126
+ txns_sorted = sorted(txns, key=lambda x: x.dt)
127
+ for i in range(1, len(txns_sorted)):
128
+ gap = (txns_sorted[i].dt - txns_sorted[i-1].dt).total_seconds() / 3600
129
+ if gap <= window_hours:
130
+ dups.append({
131
+ "entity": entity,
132
+ "amount": amount,
133
+ "count": len(txns),
134
+ "gap_hours": round(gap, 1),
135
+ "reason": f"{entity} 在 {gap:.0f} 小时内重复扣款 {amount}",
136
+ })
137
+ break
138
+ return dups
139
+
140
+ @staticmethod
141
+ def detect_subscription_creep(transactions: list) -> dict:
142
+ """订阅膨胀检测:统计周期性订阅总额与数量。
143
+
144
+ 同一商户按"最近一次扣款"代表其当前月度订阅价(订阅可能涨价,
145
+ 取最新而非遍历到的任意一条;也避免同商户多月记录被累加成虚高总额)。
146
+ """
147
+ latest = {} # entity -> (dt, amount)
148
+ for t in transactions:
149
+ if t.is_recurring and t.is_expense:
150
+ prev = latest.get(t.entity)
151
+ if prev is None or t.dt > prev[0]:
152
+ latest[t.entity] = (t.dt, abs(t.amount))
153
+ subs = {e: amt for e, (_, amt) in latest.items()}
154
+ total = sum(subs.values())
155
+ return {
156
+ "subscription_count": len(subs),
157
+ "monthly_total": round(total, 2),
158
+ "subscriptions": [{"entity": k, "amount": v}
159
+ for k, v in sorted(subs.items(), key=lambda x: -x[1])],
160
+ "creep_warning": len(subs) >= 5 or total >= 100,
161
+ }
162
+
163
+ @staticmethod
164
+ def detect_upcoming_renewals(transactions: list, horizon_days: int = 35,
165
+ now=None) -> list:
166
+ """订阅/年费**续订前**提醒:从历史扣款间隔推断周期,预测下次扣款日。
167
+
168
+ 周期靠**相邻扣款的中位间隔**判定(月/季/年),而非猜:
169
+ · 25~35 天 → 月订 · 85~95 → 季订 · 355~375 → 年订
170
+ 只有 ≥2 次扣款才能算间隔;不落在已知档位的(间隔紊乱)不预测——宁可不提醒,
171
+ 也不要为"猜错的日期"制造误报(误报会让用户关掉提醒)。
172
+
173
+ horizon_days=35 是**检测**窗口(默认放宽,足够覆盖月订下次扣款);**几天前提醒**
174
+ 由规则 renewal_reminder 的 threshold 精确控制(裁决层用 days_until<=threshold 过滤)。
175
+ 两者分离,否则用户把规则阈值改成 14 想提前两周提醒,却被这里的窗口硬封在 7 天内 ——
176
+ 改了没反应(静默失效)。检测放宽、裁决收窄,阈值才真正生效。
177
+ """
178
+ from statistics import median
179
+ now = now or datetime.now()
180
+ by_entity = defaultdict(list)
181
+ for t in transactions:
182
+ if t.is_recurring and t.is_expense:
183
+ by_entity[t.entity].append(t)
184
+ out = []
185
+ for entity, txns in by_entity.items():
186
+ if len(txns) < 2:
187
+ continue # 只有一次扣款 → 无法判周期
188
+ ts = sorted(txns, key=lambda x: x.dt)
189
+ gaps = [(ts[i].dt - ts[i - 1].dt).days for i in range(1, len(ts))]
190
+ gaps = [g for g in gaps if g > 0]
191
+ if not gaps:
192
+ continue
193
+ g = median(gaps)
194
+ if 25 <= g <= 35: cadence, period = "月订", 30
195
+ elif 85 <= g <= 95: cadence, period = "季订", 90
196
+ elif 355 <= g <= 375: cadence, period = "年订", 365
197
+ else:
198
+ continue # 间隔紊乱 → 不预测(不制造误报)
199
+ next_dt = ts[-1].dt + timedelta(days=period)
200
+ days_until = (next_dt - now).days
201
+ if 0 <= days_until <= horizon_days:
202
+ out.append({"entity": entity, "amount": abs(ts[-1].amount),
203
+ "cadence": cadence, "days_until": days_until,
204
+ "next_date": next_dt.strftime("%Y-%m-%d")})
205
+ return out
206
+
207
+ @staticmethod
208
+ def detect_pending_refunds(transactions: list, wait_days: int = 14,
209
+ now=None) -> list:
210
+ """退款未到账:标记了 `refund_expected` 的支出,超过 wait_days 仍无等额入账。
211
+
212
+ **必须由用户打 tag**:退款在流水里只是一笔入账,系统无法从交易流推断
213
+ "我期待一笔退款"。若靠"支出后没有等额入账"来猜,会对**每一笔正常消费**报警
214
+ —— 那是灾难性误报。所以宁可要求显式标记。
215
+ """
216
+ now = now or datetime.now()
217
+ credits = [t for t in transactions if not t.is_expense]
218
+ claimed = set() # 已被某笔退款认领的入账(按 id)
219
+ out = []
220
+ # 先处理较早的支出,让它优先认领入账(FIFO,符合"先欠先还")
221
+ expected = sorted((t for t in transactions
222
+ if t.is_expense and "refund_expected" in (t.tags or [])),
223
+ key=lambda x: x.dt)
224
+ for t in expected:
225
+ amt = abs(t.amount)
226
+ # 同商户、等额、在该支出之后、且**尚未被别的退款认领**的入账 → 退款到账
227
+ # 消耗式匹配:一笔入账只抵一笔支出。否则用户标了两笔同额 refund_expected、
228
+ # 只收到一笔退款时,两笔都被误判为已退款 → 第二笔漏报(其实还没到账)。
229
+ match = next((c for c in credits
230
+ if id(c) not in claimed and c.entity == t.entity
231
+ and abs(abs(c.amount) - amt) < 0.01 and c.dt >= t.dt), None)
232
+ if match is not None:
233
+ claimed.add(id(match))
234
+ continue
235
+ waiting = (now - t.dt).days
236
+ if waiting >= wait_days:
237
+ out.append({"entity": t.entity, "amount": amt,
238
+ "days_waiting": waiting, "transaction_id": t.id})
239
+ return out
240
+
241
+ # ── 阈值自校准 ────────────────────────────────────────
242
+ # 样本门槛:少于这些就不校准,继续用出厂默认。宁可用"公认的经验值",
243
+ # 也不要用 3 笔数据算出来的、听起来很个性化的垃圾阈值。
244
+ CALIB_MIN_TXNS = 20
245
+ CALIB_MIN_DAYS = 30
246
+
247
+ @staticmethod
248
+ def calibrate_thresholds(transactions: list) -> dict:
249
+ """从**用户自己的消费分布**推阈值,取代拍脑袋的经验值。
250
+
251
+ 只校准有分布依据的**金额类**阈值,且**必须抗离群**——否则一笔房租/买电脑
252
+ 就把门槛抬高,让大额检测在最该警觉时反而变迟钝。所以用 median+MAD 而非均值
253
+ (与本文件 detect_anomalies 同源:同一套 MAD 修正 Z 分数):
254
+ · large_purchase_check = median + 3.5×MAD 换算界
255
+ = 单笔支出**恰好会触发异常检测的那条线**。语义完全对口:"大额确认"和
256
+ "异常否决"用同一把尺量,只是动作不同(confirm vs veto)。它随消费方差
257
+ 自适应:平时消费越集中,门槛压得越贴身;偶发大额不会把它顶上去。
258
+ · low_balance_protection = 中位数日支出 × 7("撑一周"缓冲)
259
+ 按天聚合再取中位数,同样抗离群。一周缓冲是有含义的锚,500 只是个数字。
260
+
261
+ **时间类阈值(48h 重复扣款窗 / 7 天续订提醒 / 14 天退款等待)不校准** ——
262
+ 它们不是从消费分布里学得出来的,是语义常数(一周就是一周,退款该多久到账
263
+ 取决于商家不取决于你花多少钱)。假装校准它们,只是给猜测套上数学外衣。
264
+ 月订阅总额阈值同理:没有原理性的锚,故留给用户改 yaml。
265
+
266
+ 返回 {rule_id: (建议阈值, 依据说明)};样本不足返回 {}。
267
+ """
268
+ from statistics import median
269
+ expenses = [t for t in transactions if t.is_expense]
270
+ if len(expenses) < FinanceReasoning.CALIB_MIN_TXNS:
271
+ return {}
272
+ dts = [t.dt for t in expenses]
273
+ span_days = (max(dts) - min(dts)).days
274
+ if span_days < FinanceReasoning.CALIB_MIN_DAYS:
275
+ return {} # 跨度太短 → 分布不可信
276
+
277
+ def _round100(v):
278
+ # 取整到百:阈值是给人看的,"800" 比 "783.42" 可信也好记
279
+ return max(100.0, round(v / 100.0) * 100.0)
280
+
281
+ amts = [abs(t.amount) for t in expenses]
282
+ med = median(amts)
283
+ mad = median([abs(a - med) for a in amts])
284
+ if mad == 0: # 退化(消费高度一致)→ 用平均绝对偏差兜底
285
+ 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
+ large = min(_round100(med + (3.5 / 0.6745) * mad), 20000.0)
289
+
290
+ by_day = defaultdict(float)
291
+ for t in expenses:
292
+ by_day[t.dt.date()] += abs(t.amount)
293
+ daily_med = median(by_day.values())
294
+ low_bal = _round100(daily_med * 7)
295
+ return {
296
+ "large_purchase_check": (
297
+ large, f"单笔支出 median {med:.0f}+3.5×MAD(与异常检测同尺,基于 "
298
+ f"{len(expenses)} 笔 / {span_days} 天)"),
299
+ "low_balance_protection": (
300
+ low_bal, f"中位数日支出 {daily_med:.0f} × 7 天缓冲(基于 "
301
+ f"{len(expenses)} 笔 / {span_days} 天)"),
302
+ }
303
+
304
+ # ── 预测 ──────────────────────────────────────────────
305
+ @staticmethod
306
+ def burn_rate(transactions: list, days: int = 30) -> dict:
307
+ """烧钱速度:近 N 天的日均净流出。"""
308
+ cutoff = datetime.now() - timedelta(days=days)
309
+ recent = [t for t in transactions if t.dt >= cutoff]
310
+ expense = sum(abs(t.amount) for t in recent if t.is_expense)
311
+ income = sum(t.amount for t in recent if not t.is_expense and t.amount > 0)
312
+ net = income - expense
313
+ return {
314
+ "window_days": days,
315
+ "total_expense": round(expense, 2),
316
+ "total_income": round(income, 2),
317
+ "net_flow": round(net, 2),
318
+ "daily_burn": round(expense / days, 2),
319
+ "daily_net": round(net / days, 2),
320
+ }
321
+
322
+ @staticmethod
323
+ def cashflow_projection(transactions: list, current_balance: float,
324
+ months: int = 3) -> dict:
325
+ """现金流预测:按当前 burn rate 推算未来 N 个月余额走势。"""
326
+ br = FinanceReasoning.burn_rate(transactions, days=30)
327
+ monthly_net = br["daily_net"] * 30
328
+ trajectory = []
329
+ bal = current_balance
330
+ for m in range(1, months + 1):
331
+ bal += monthly_net
332
+ trajectory.append({"month": m, "projected_balance": round(bal, 2)})
333
+ return {
334
+ "current_balance": current_balance,
335
+ "monthly_net": round(monthly_net, 2),
336
+ "trajectory": trajectory,
337
+ "runway_months": (round(current_balance / abs(monthly_net), 1)
338
+ if monthly_net < 0 else None),
339
+ "trend": "下降" if monthly_net < 0 else "上升",
340
+ }
@@ -0,0 +1,180 @@
1
+ """
2
+ Foxclaw 财务子系统 — 自主对账 (Reconciliation)
3
+
4
+ 借鉴 2026 前沿财务 Agent 的杀手级用例:自主对账 —— 自动匹配系统记录的交易
5
+ 与外部流水(银行/支付宝账单),解决差异,生成审计日志。
6
+
7
+ 匹配策略(逐级放宽):
8
+ 1. 精确匹配:金额 + 商户 + 同日
9
+ 2. 模糊匹配:金额同 + 日期 ±tolerance_days + 商户相似
10
+ 3. 金额接近匹配:商户同 + 日期接近 + 金额小幅差异(可能手续费)
11
+ 四类结果:
12
+ matched 对上的
13
+ unmatched_internal 系统有、外部无(可能漏记/重复记)
14
+ unmatched_external 外部有、系统无(可能漏记交易/盗刷)
15
+ amount_mismatch 匹配上但金额不符(可能手续费/汇率/错记)
16
+ """
17
+ from __future__ import annotations
18
+ from dataclasses import dataclass, field
19
+ from datetime import datetime
20
+
21
+
22
+ @dataclass
23
+ class ExternalRecord:
24
+ """外部流水记录(来自银行/支付宝账单)。"""
25
+ amount: float
26
+ entity: str
27
+ date: str # ISO 日期或 YYYY-MM-DD
28
+ account: str = ""
29
+ ref: str = "" # 外部流水号
30
+
31
+ def to_dict(self) -> dict:
32
+ return {"amount": self.amount, "entity": self.entity,
33
+ "date": self.date, "account": self.account, "ref": self.ref}
34
+
35
+
36
+ def _date_only(ts: str) -> str:
37
+ """从 timestamp 提取日期部分(YYYY-MM-DD)。"""
38
+ return (ts or "")[:10]
39
+
40
+
41
+ def _days_apart(d1: str, d2: str) -> int:
42
+ """两个日期相差天数(解析失败返回大数)。"""
43
+ try:
44
+ a = datetime.fromisoformat(_date_only(d1))
45
+ b = datetime.fromisoformat(_date_only(d2))
46
+ return abs((a - b).days)
47
+ except Exception:
48
+ return 9999
49
+
50
+
51
+ def _entity_similar(a: str, b: str) -> bool:
52
+ """商户名相似(一方包含另一方,或去空格后相等)。"""
53
+ a, b = (a or "").strip().lower(), (b or "").strip().lower()
54
+ if not a or not b:
55
+ return False
56
+ return a == b or a in b or b in a
57
+
58
+
59
+ @dataclass
60
+ class ReconciliationResult:
61
+ matched: list = field(default_factory=list)
62
+ unmatched_internal: list = field(default_factory=list)
63
+ unmatched_external: list = field(default_factory=list)
64
+ amount_mismatch: list = field(default_factory=list)
65
+ audit_log: list = field(default_factory=list)
66
+
67
+ def summary(self) -> dict:
68
+ total = (len(self.matched) + len(self.unmatched_internal)
69
+ + len(self.unmatched_external) + len(self.amount_mismatch))
70
+ return {
71
+ "total_checked": total,
72
+ "matched": len(self.matched),
73
+ "unmatched_internal": len(self.unmatched_internal),
74
+ "unmatched_external": len(self.unmatched_external),
75
+ "amount_mismatch": len(self.amount_mismatch),
76
+ "match_rate": round(len(self.matched) / total, 3) if total else 0.0,
77
+ "needs_attention": (len(self.unmatched_internal)
78
+ + len(self.unmatched_external)
79
+ + len(self.amount_mismatch)),
80
+ }
81
+
82
+ def to_dict(self) -> dict:
83
+ return {
84
+ "summary": self.summary(),
85
+ "matched": self.matched,
86
+ "unmatched_internal": self.unmatched_internal,
87
+ "unmatched_external": self.unmatched_external,
88
+ "amount_mismatch": self.amount_mismatch,
89
+ "audit_log": self.audit_log,
90
+ }
91
+
92
+
93
+ class Reconciler:
94
+ """对账器:交叉核对系统交易与外部流水。"""
95
+
96
+ def __init__(self, tolerance_days: int = 3, amount_tolerance: float = 0.01):
97
+ self.tolerance_days = tolerance_days
98
+ self.amount_tolerance = amount_tolerance # 金额绝对差容忍(<=此值视为相等)
99
+
100
+ def _amount_eq(self, a: float, b: float) -> bool:
101
+ """金额模糊相等:绝对值在容差范围内 且 收支方向一致(都支出或都收入)。"""
102
+ same_direction = (a >= 0) == (b >= 0)
103
+ return same_direction and abs(abs(a) - abs(b)) <= self.amount_tolerance
104
+
105
+ def reconcile(self, transactions: list, external: list) -> ReconciliationResult:
106
+ """对账主流程。transactions=系统交易,external=[ExternalRecord]。"""
107
+ res = ReconciliationResult()
108
+ used_ext = set() # 已匹配的外部记录索引
109
+
110
+ for t in transactions:
111
+ t_date = _date_only(t.timestamp)
112
+ best = None # (ext_idx, match_type)
113
+ # 1. 精确匹配:金额 + 商户 + 同日
114
+ for j, ext in enumerate(external):
115
+ if j in used_ext:
116
+ continue
117
+ if (self._amount_eq(t.amount, ext.amount)
118
+ and _entity_similar(t.entity, ext.entity)
119
+ and _days_apart(t_date, ext.date) == 0):
120
+ best = (j, "exact"); break
121
+ # 2. 模糊匹配:金额同 + 日期容忍内 + 商户相似
122
+ if best is None:
123
+ for j, ext in enumerate(external):
124
+ if j in used_ext:
125
+ continue
126
+ if (self._amount_eq(t.amount, ext.amount)
127
+ and _entity_similar(t.entity, ext.entity)
128
+ and _days_apart(t_date, ext.date) <= self.tolerance_days):
129
+ best = (j, "fuzzy_date"); break
130
+ # 3. 金额不符匹配:商户同 + 日期接近 + 同方向 + 金额不等(可能手续费)
131
+ if best is None:
132
+ for j, ext in enumerate(external):
133
+ if j in used_ext:
134
+ continue
135
+ same_direction = (t.amount >= 0) == (ext.amount >= 0)
136
+ if (_entity_similar(t.entity, ext.entity)
137
+ and _days_apart(t_date, ext.date) <= self.tolerance_days
138
+ and same_direction
139
+ and not self._amount_eq(t.amount, ext.amount)):
140
+ best = (j, "amount_mismatch"); break
141
+
142
+ if best is None:
143
+ # 系统有、外部无
144
+ res.unmatched_internal.append(self._txn_brief(t))
145
+ res.audit_log.append(
146
+ f"[未匹配·内部] 「{t.entity} ¥{abs(t.amount):.0f}」"
147
+ f"({t_date}) 在外部流水中无对应,疑漏记或重复记录")
148
+ continue
149
+
150
+ j, mtype = best
151
+ used_ext.add(j)
152
+ ext = external[j]
153
+ if mtype == "amount_mismatch":
154
+ diff = abs(t.amount) - abs(ext.amount)
155
+ res.amount_mismatch.append({
156
+ "internal": self._txn_brief(t), "external": ext.to_dict(),
157
+ "diff": round(diff, 2)})
158
+ res.audit_log.append(
159
+ f"[金额不符] 「{t.entity}」系统¥{abs(t.amount):.0f} vs "
160
+ f"外部¥{abs(ext.amount):.0f},差¥{abs(diff):.0f}(疑手续费/汇率/错记)")
161
+ else:
162
+ res.matched.append({
163
+ "internal": self._txn_brief(t), "external": ext.to_dict(),
164
+ "match_type": mtype})
165
+ res.audit_log.append(
166
+ f"[已匹配·{mtype}] 「{t.entity} ¥{abs(t.amount):.0f}」({t_date})")
167
+
168
+ # 剩余未匹配的外部记录:外部有、系统无
169
+ for j, ext in enumerate(external):
170
+ if j not in used_ext:
171
+ res.unmatched_external.append(ext.to_dict())
172
+ res.audit_log.append(
173
+ f"[未匹配·外部] 外部流水「{ext.entity} ¥{abs(ext.amount):.0f}」"
174
+ f"({_date_only(ext.date)}) 系统未记录,疑漏记交易或盗刷")
175
+ return res
176
+
177
+ @staticmethod
178
+ def _txn_brief(t) -> dict:
179
+ return {"id": t.id, "amount": t.amount, "entity": t.entity,
180
+ "date": _date_only(t.timestamp), "account": t.account}