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
@@ -0,0 +1,149 @@
1
+ """可选的新版本检查(默认关)。
2
+
3
+ 为什么默认关:foxclaw 的定位是本地优先 —— 默认外呼会让「断网也能用」多一个
4
+ 例外,也给安全审计多一个出网点。**只有** 设置 ``FOXCLAW_UPDATE_CHECK=1`` 时才会
5
+ 访问 PyPI,其余情况 ``status()`` 里 ``enabled=False``、``update_available=False``。
6
+
7
+ 失败一律静默:查版本是锦上添花,不能因为它让 ``/api/system/info`` 变慢或报错。
8
+ - 结果按 24h 缓存到 ``<FOXCLAW_DATA_DIR>/update_check.json``;
9
+ - 查询在 daemon 线程里跑,不阻塞请求;
10
+ - 查失败时保留上次成功的结果(只是不刷新时间),避免一次网络抖动让提示消失。
11
+
12
+ 只发一个 GET ``https://pypi.org/pypi/foxclaw/json``,不带任何本机信息。
13
+ """
14
+
15
+ from __future__ import annotations
16
+
17
+ import json
18
+ import os
19
+ import threading
20
+ import time
21
+ from pathlib import Path
22
+ from typing import Any
23
+
24
+ _PYPI_URL = "https://pypi.org/pypi/foxclaw/json"
25
+ _TTL = 24 * 3600
26
+ _TIMEOUT = 3.0
27
+
28
+ _lock = threading.Lock()
29
+ _inflight = False
30
+ _last_error: str | None = None
31
+
32
+
33
+ def _enabled() -> bool:
34
+ return os.environ.get("FOXCLAW_UPDATE_CHECK", "").strip().lower() in {"1", "true", "yes", "on"}
35
+
36
+
37
+ def _current() -> str:
38
+ from foxclaw import __version__
39
+
40
+ return __version__
41
+
42
+
43
+ def _data_dir() -> Path:
44
+ return Path(os.environ.get("FOXCLAW_DATA_DIR") or "./foxclaw_data").expanduser()
45
+
46
+
47
+ def _cache_path() -> Path:
48
+ return _data_dir() / "update_check.json"
49
+
50
+
51
+ def parse_version(value: str) -> tuple[int, ...]:
52
+ """``"0.1.10"`` → ``(0, 1, 10)``。只取各段的前导数字,预发布/本地后缀丢掉。
53
+
54
+ 不引入 ``packaging``:这里只需要「谁更新」,自己比数字段足够,也少一个依赖。
55
+ """
56
+ parts: list[int] = []
57
+ for chunk in str(value or "").strip().split("."):
58
+ digits = ""
59
+ for ch in chunk:
60
+ if not ch.isdigit():
61
+ break
62
+ digits += ch
63
+ if not digits:
64
+ break
65
+ parts.append(int(digits))
66
+ return tuple(parts)
67
+
68
+
69
+ def is_newer(latest: str, current: str) -> bool:
70
+ """latest 是否严格新于 current(逐段比,缺的段补 0)。"""
71
+ left, right = parse_version(latest), parse_version(current)
72
+ width = max(len(left), len(right))
73
+ return left + (0,) * (width - len(left)) > right + (0,) * (width - len(right))
74
+
75
+
76
+ def _read_cache() -> dict[str, Any]:
77
+ try:
78
+ data = json.loads(_cache_path().read_text(encoding="utf-8"))
79
+ except Exception:
80
+ return {}
81
+ return data if isinstance(data, dict) else {}
82
+
83
+
84
+ def _write_cache(data: dict[str, Any]) -> None:
85
+ try:
86
+ path = _cache_path()
87
+ path.parent.mkdir(parents=True, exist_ok=True)
88
+ path.write_text(json.dumps(data, ensure_ascii=False), encoding="utf-8")
89
+ except Exception:
90
+ pass
91
+
92
+
93
+ def _fetch_latest() -> str:
94
+ import urllib.request
95
+
96
+ req = urllib.request.Request(
97
+ _PYPI_URL,
98
+ headers={"Accept": "application/json", "User-Agent": f"foxclaw/{_current()}"},
99
+ )
100
+ with urllib.request.urlopen(req, timeout=_TIMEOUT) as resp: # noqa: S310 - 地址是常量 https
101
+ payload = json.loads(resp.read().decode("utf-8", "replace"))
102
+ version = ((payload or {}).get("info") or {}).get("version") or ""
103
+ if not version:
104
+ raise ValueError("PyPI 响应里没有 info.version")
105
+ return str(version)
106
+
107
+
108
+ def _refresh() -> None:
109
+ global _inflight, _last_error
110
+ try:
111
+ latest = _fetch_latest()
112
+ _write_cache({"latest": latest, "checked_at": time.time(), "current": _current()})
113
+ _last_error = None
114
+ except Exception as exc:
115
+ _last_error = f"{type(exc).__name__}: {exc}"
116
+ finally:
117
+ with _lock:
118
+ _inflight = False
119
+
120
+
121
+ def maybe_refresh() -> None:
122
+ """启用且缓存过期 → 后台查一次。幂等、非阻塞,可放心在每个 system/info 里调。"""
123
+ global _inflight
124
+ if not _enabled():
125
+ return
126
+ checked = _read_cache().get("checked_at")
127
+ if isinstance(checked, (int, float)) and time.time() - checked < _TTL:
128
+ return
129
+ with _lock:
130
+ if _inflight:
131
+ return
132
+ _inflight = True
133
+ threading.Thread(target=_refresh, name="foxclaw-update-check", daemon=True).start()
134
+
135
+
136
+ def status() -> dict[str, Any]:
137
+ """给 ``/api/system/info`` 的只读快照。未启用时 latest 仍是缓存值,但不报可更新。"""
138
+ enabled = _enabled()
139
+ current = _current()
140
+ cache = _read_cache()
141
+ latest = str(cache.get("latest") or "")
142
+ return {
143
+ "enabled": enabled,
144
+ "current": current,
145
+ "latest": latest or None,
146
+ "update_available": bool(enabled and latest and is_newer(latest, current)),
147
+ "checked_at": cache.get("checked_at"),
148
+ "error": _last_error,
149
+ }
@@ -31,9 +31,8 @@ from pathlib import Path
31
31
  from types import SimpleNamespace
32
32
 
33
33
  from .shared_write import guard_shared_write
34
+ from typing import Any
34
35
 
35
- # Foxclaw 的 type 白名单 → vecRecall 的 memory_type(episodic/semantic/procedural)。
36
- # 分错不影响读写,但影响 vecRecall 的遗忘曲线与 procedural 精确匹配。
37
36
  _FOXCLAW_TO_VR = {
38
37
  "concept": "semantic",
39
38
  "project": "semantic",
@@ -52,7 +51,6 @@ _VR_TO_FOXCLAW = {
52
51
  }
53
52
  _VALID_TYPES = set(_FOXCLAW_TO_VR)
54
53
 
55
- # 偏好类记忆走 topic_key(精确查询),因为"总是注入的偏好"不该做语义模糊匹配。
56
54
  _PREFERENCE_TAG = "preference"
57
55
 
58
56
 
@@ -77,7 +75,7 @@ class _HKCSharedEmbedding:
77
75
  def __init__(self, model_name: str):
78
76
  self.model_name = model_name
79
77
  self._model = None
80
- self._dim = None
78
+ self._dim: int | None = None
81
79
 
82
80
  def _load(self):
83
81
  if self._model is None:
@@ -85,8 +83,6 @@ class _HKCSharedEmbedding:
85
83
  from hkc_search.embedding_backends import get_st_model
86
84
  model = get_st_model(self.model_name)
87
85
  if model is None:
88
- # 不回退到直连 SentenceTransformer:那会绕过 HKC 的进程级共享缓存、
89
- # 多加载一份模型(bge-m3 常驻约 1GB)。失败就明确报错让工厂回退 HMR。
90
86
  raise RuntimeError(
91
87
  f"embedding 模型不可用: {self.model_name}(HKC 共享缓存加载失败)")
92
88
  self._model = model
@@ -105,7 +101,6 @@ class _HKCSharedEmbedding:
105
101
  if self._dim is None:
106
102
  try:
107
103
  model = self._load()
108
- # ST 6.x 把 get_sentence_embedding_dimension 改名了,两个都试
109
104
  getter = (getattr(model, "get_embedding_dimension", None)
110
105
  or getattr(model, "get_sentence_embedding_dimension", None))
111
106
  self._dim = (int(getter() or 0) or None) if getter else None
@@ -120,7 +115,7 @@ class VecRecallAdapter:
120
115
  backend = "vecRecall"
121
116
 
122
117
  def __init__(self, storage_path: str = "./vecrecall_data", wing: str = "foxclaw",
123
- use_chain: bool = False, embedding_model: str = None,
118
+ use_chain: bool = False, embedding_model: str | None = None,
124
119
  dedup: bool = True, embedding_backend=None, vector_backend=None):
125
120
  """`embedding_backend` / `vector_backend` 用于注入(测试传轻量实现)。
126
121
 
@@ -133,9 +128,6 @@ class VecRecallAdapter:
133
128
  self.wing = wing
134
129
  self.storage_path = str(storage_path)
135
130
  self.version = self._resolve_version()
136
- # vecRecall 本体没有任何锁(只有 REST server 层有 RLock),而它用的是
137
- # check_same_thread=False 的单条 sqlite 连接 + 内存 dict 索引。并行子 Agent
138
- # 同时 add/search 属于未定义行为,和 HMR 是同一类坑 —— 这里沿用同一套串行化。
139
131
  self._lock = threading.RLock()
140
132
 
141
133
  self._emb = embedding_backend or _HKCSharedEmbedding(
@@ -152,7 +144,6 @@ class VecRecallAdapter:
152
144
  **extra,
153
145
  )
154
146
 
155
- # ── 构造辅助 ──────────────────────────────────────────
156
147
  @staticmethod
157
148
  def _resolve_version() -> str:
158
149
  try:
@@ -162,7 +153,7 @@ class VecRecallAdapter:
162
153
  return "unknown"
163
154
 
164
155
  @staticmethod
165
- def _resolve_model(embedding_model: str = None) -> str:
156
+ def _resolve_model(embedding_model: str | None = None) -> str:
166
157
  """与 HMRAdapter 同一条解析链,保证两套后端用同一个模型(分数与维度可比)。"""
167
158
  return (embedding_model
168
159
  or os.environ.get("EMBEDDING_MODEL")
@@ -188,14 +179,13 @@ class VecRecallAdapter:
188
179
  except Exception:
189
180
  return
190
181
  dims = {int(r[0]) for r in rows}
191
- want = getattr(self._emb, "dim", None) # 轻量后端无 dim 属性时跳过
182
+ want = getattr(self._emb, "dim", None)
192
183
  if dims and want and dims != {int(want)}:
193
184
  raise RuntimeError(
194
185
  f"已有向量维度 {sorted(dims)} 与当前嵌入维度 {want} 不一致。"
195
186
  f"换 embedding 模型必须重建索引:删除 {self.storage_path} 后重启"
196
187
  )
197
188
 
198
- # ── 摄入 ──────────────────────────────────────────────
199
189
  _AUTO_TITLE = re.compile(r"^[A-Za-z_]+\s+\d{1,2}:\d{2}$")
200
190
 
201
191
  @staticmethod
@@ -214,8 +204,8 @@ class VecRecallAdapter:
214
204
  s = re.split(r"[。!?!?\n\r]", s, 1)[0].strip()
215
205
  return " ".join(s.split())[:80]
216
206
 
217
- def ingest(self, content: str, type: str = "concept", agent_id: str = None,
218
- title: str = None, tags: list = None, shared: bool = False, **kwargs):
207
+ def ingest(self, content: str, type: str = "concept", agent_id: str | None = None,
208
+ title: str | None = None, tags: list | None = None, shared: bool = False, **kwargs):
219
209
  """统一摄入。签名与 `HMRAdapter.ingest` 一致。
220
210
 
221
211
  隔离承载:`agent_id` → owner,`shared=True` → visibility="shared"。
@@ -231,8 +221,6 @@ class VecRecallAdapter:
231
221
  if not title:
232
222
  title = self._derive_title(content) or ""
233
223
 
234
- # 【P0-3】共享层写入闸门:必须在落库之前判定,语义与 HMR 路径完全一致,
235
- # 否则同一个策略在两个后端上表现不同。
236
224
  if shared:
237
225
  guard_shared_write(self, agent_id, content, mtype, meta_tags)
238
226
 
@@ -240,8 +228,6 @@ class VecRecallAdapter:
240
228
  if "confidence" in kwargs:
241
229
  metadata["confidence"] = kwargs["confidence"]
242
230
 
243
- # execution 是审计证据:被遗忘曲线衰减或被去重合并,就无法再证明"当时确实
244
- # 这么做了"。vecRecall 的 immutable 同时免于衰减与去重,正好对上这个语义。
245
231
  retention = "immutable" if mtype == "execution" else "decay"
246
232
  topic_key = kwargs.get("topic_key") or (
247
233
  _PREFERENCE_TAG if _PREFERENCE_TAG in meta_tags else "")
@@ -260,8 +246,7 @@ class VecRecallAdapter:
260
246
  topic_key=topic_key,
261
247
  )
262
248
 
263
- # ── 召回 ──────────────────────────────────────────────
264
- def _visible(self, mem, agent_id: str, include_shared: bool) -> bool:
249
+ def _visible(self, mem, agent_id: str | None, include_shared: bool) -> bool:
265
250
  """可见性判定,与 vecRecall `_visible_to` 规则保持同源。
266
251
 
267
252
  - shared 记忆:仅当 include_shared 时可见
@@ -276,7 +261,7 @@ class VecRecallAdapter:
276
261
  return include_shared
277
262
  return owner == agent_id or not owner
278
263
 
279
- def recall(self, query: str = "", agent_id: str = None, top_k: int = 5,
264
+ def recall(self, query: str = "", agent_id: str | None = None, top_k: int = 5,
280
265
  include_shared: bool = True, **kwargs):
281
266
  """统一召回,返回 list[dict]。
282
267
 
@@ -300,9 +285,6 @@ class VecRecallAdapter:
300
285
  results = list(getattr(bundle, "l2_topic_context", []) or []) + \
301
286
  list(getattr(bundle, "l3_deep_results", []) or [])
302
287
  if not results and (query or "").strip():
303
- # L2 有 0.55 阈值:命中偏弱时整层为空。HMR 的 recall 是无条件
304
- # 返回 top_k,为不改变上层"总能拿到几条"的预期,退化到全量语义
305
- # 检索,再在下面用 _visible 补上隔离(search 本身不隔离)。
306
288
  results = list(self._vr.search(query, n=fetch_k) or [])
307
289
  except Exception:
308
290
  return []
@@ -384,8 +366,7 @@ class VecRecallAdapter:
384
366
  row["_ts"] = getattr(m, "timestamp", "") or ""
385
367
  rows.append(row)
386
368
 
387
- rows.sort(key=lambda x: str(x.get("_ts")), reverse=True) # 新近优先
388
- # 同一偏好反复表达会存成多条,去重保留最新,否则会挤满 limit
369
+ rows.sort(key=lambda x: str(x.get("_ts")), reverse=True)
389
370
  deduped, seen_c = [], set()
390
371
  for x in rows:
391
372
  if x["content"] in seen_c:
@@ -401,7 +382,6 @@ class VecRecallAdapter:
401
382
  except Exception:
402
383
  return False
403
384
 
404
- # ── 展示 ──────────────────────────────────────────────
405
385
  def recent(self, limit: int = 20) -> list:
406
386
  """最近的记忆条目(新→旧),供前端右栏"记忆信息条"。"""
407
387
  rows = []
@@ -435,11 +415,6 @@ class VecRecallAdapter:
435
415
  })
436
416
  return rows
437
417
 
438
- # ── HMR 专有接口的占位 ────────────────────────────────
439
- # vecRecall 没有"会话工作区 / 运行时快照"这两个概念。留占位而不是让调用方
440
- # AttributeError:上层有统一的"后端接口"假设,缺方法会炸在一个与被调用能力
441
- # 无关的地方。agents/base.py 取到 workspace 后并不读其字段(active_goal 的
442
- # 写入已移除),所以空壳没有实际成本。
443
418
  def get_workspace(self, agent_id: str):
444
419
  return SimpleNamespace(agent_id=agent_id, active_goal="", status="idle",
445
420
  push_task=lambda _t: None)
@@ -453,14 +428,13 @@ class VecRecallAdapter:
453
428
  def restore_runtime_state(self):
454
429
  return None
455
430
 
456
- # ── 状态 ──────────────────────────────────────────────
457
431
  def get_system_status(self):
458
432
  return {"backend": self.backend, "version": self.version, "wing": self.wing}
459
433
 
460
434
  def stats(self) -> dict:
461
435
  """记忆统计:前端记忆中心展示真实数字。字段与 HMRAdapter.stats 一致。"""
462
436
  total = shared_count = relations = 0
463
- by_agent = {}
437
+ by_agent: dict[str, Any] = {}
464
438
  embedding = type(self._emb).__name__
465
439
  try:
466
440
  with self._lock:
@@ -25,17 +25,15 @@ from dataclasses import dataclass, field
25
25
  from urllib.parse import urlparse
26
26
 
27
27
  from . import failure_policy as _policy
28
+ from typing import Callable
28
29
 
29
30
  VERIFIED = "verified"
30
31
  UNVERIFIED = "unverified"
31
32
  FAILED = "failed"
32
33
 
33
- #: 每条工具结果最多挂几条证据(再多就成了噪声,用户不会看)
34
34
  MAX_EVIDENCE = 8
35
35
 
36
36
  _URL_RE = re.compile(r"https?://[^\s<>\"'))\]}]+")
37
- #: 只保留"真实存在"的路径 —— 正则从文本里认出来的路径九成是误报(URL 片段、示例路径),
38
- #: 只有能在磁盘上 stat 到的才算证据。这不是限制,是"证据"二字的含义。
39
37
  _WIN_PATH_RE = re.compile(r"[A-Za-z]:[\\/][^\s\"'<>|*?,。;]+")
40
38
  _POSIX_PATH_RE = re.compile(r"(?<![\w:])/(?:[\w.\-]+/)+[\w.\-]+")
41
39
 
@@ -44,10 +42,10 @@ _POSIX_PATH_RE = re.compile(r"(?<![\w:])/(?:[\w.\-]+/)+[\w.\-]+")
44
42
  class Evidence:
45
43
  """一条可核对的凭据:一个链接、一个真实存在的文件、或一次工具的原始命中。"""
46
44
 
47
- kind: str # url / file
48
- ref: str # 链接或绝对路径
49
- label: str # 展示名(域名 / 文件名)
50
- source: str # 哪个工具产出的
45
+ kind: str
46
+ ref: str
47
+ label: str
48
+ source: str
51
49
  at: float = field(default_factory=time.time)
52
50
 
53
51
  def to_dict(self) -> dict:
@@ -121,14 +119,10 @@ def extract_evidence(source: str, text: str, limit: int = MAX_EVIDENCE) -> list[
121
119
  return out
122
120
 
123
121
 
124
- #: 工具专属的后置条件检查。返回 (是否通过, 不通过的原因)。
125
- #: 没登记的工具走通用规则(见 verify),不会被当成"已验证"。
126
- _CHECKERS: dict[str, "callable"] = {
127
- # 联网搜索:结论必须能指回来源,否则就是"模型自己写的" —— 标未核验而不是验证通过
122
+ _CHECKERS: dict[str, Callable[..., tuple[bool, str]]] = {
128
123
  "web_search": lambda args, text: (
129
124
  (True, "") if _URL_RE.search(text or "")
130
125
  else (False, "搜索结果没有可核对的来源链接")),
131
- # 个人记忆:召回为空是合法结果(确实没记过),非空即有凭据
132
126
  "recall_memory": lambda args, text: (
133
127
  (True, "") if (text or "").strip().startswith("【召回的相关记忆】")
134
128
  else (False, "未返回记忆条目")),
@@ -157,9 +151,7 @@ def verify(name: str, args: dict | None, text: str,
157
151
 
158
152
  read_only = bool((capability or {}).get("read_only"))
159
153
  if read_only:
160
- # 只读 + 有返回 = 这一步确实发生过(内容是否可信由污点/证据层管)
161
154
  return Verification(VERIFIED, evidence=ev)
162
- # 有副作用但没登记后置条件:不能证明效果已发生 → 未核验(不许声称完成)
163
155
  return Verification(UNVERIFIED, reason="该工具未声明可观察的后置条件", evidence=ev)
164
156
 
165
157
 
@@ -23,40 +23,23 @@ import urllib.error
23
23
  from . import settings
24
24
 
25
25
 
26
- # 触发联网搜索的关键词:问到实时/时效性信息时才搜
27
- # ── 是否需要联网:话题信号(强) vs 时间词(弱) vs 个人数据(否决)──────────────
28
- # 教训:曾把纯时间词(今天/本月/最近…)也当触发,结果「分析一下本月支出」这类**问自己账本**
29
- # 的任务被误判为需要联网 → 白跑一次 ~22s 的付费深度搜索。而 Foxclaw 的领域(个人财务/
30
- # 行程/学习)天生满是时间词,这不是个别误判,是**一整类合法任务**在白交时间和钱。
31
- # 现在:时间词单独**不足以**触发;必须出现"外部世界话题";且个人数据一律否决。
32
- # (与既有的 now⊂knowledge / current⊂concurrent 词边界修复同源:都是过度触发。)
33
-
34
- # 强触发:出现即说明要的是外部实时信息
35
26
  TOPIC_TRIGGERS = [
36
27
  "版本", "几个版本", "第几", "价格", "多少钱", "汇率", "股价", "新闻",
37
28
  "发布", "上线", "更新到", "迭代", "排行", "排名", "天气",
38
- # 问"某产品/技术的模型/型号/出没出/更没更"——都是外部时效信息,过去漏搜→编造。
39
- # 只留明确指向外部产品的词;"有哪些/可用/支持"太宽(会误伤"我有哪些任务"),不加。
40
29
  "模型", "型号", "出了没", "发布没", "更新没", "新版",
41
30
  "latest", "news", "version", "price", "models",
42
31
  ]
43
- # 弱信号:纯时间词。单独出现不触发(个人数据问题天生带这些词)
44
32
  TIME_WORDS = [
45
33
  "最新", "现在", "目前", "今天", "今日", "昨天", "本周", "本月", "今年",
46
34
  "最近", "当前", "today", "current", "now",
47
35
  ]
48
- # 否决:问的是用户自己的账/行程/学习/记忆 —— 这些答案在本地领域内核里,不在网上。
49
- # 注意别放"我的"这类过宽的词:「我的股票现在多少钱」是真需要联网的。
50
36
  PERSONAL_MARKERS = [
51
37
  "支出", "收入", "预算", "交易", "账单", "花了", "消费", "记账", "结余", "余额",
52
38
  "行程", "日程", "出行", "复习", "知识点", "学习计划", "记忆",
53
39
  ]
54
40
 
55
- SEARCH_TRIGGERS = TOPIC_TRIGGERS # 对外保持同名(旧引用不破)
41
+ SEARCH_TRIGGERS = TOPIC_TRIGGERS
56
42
 
57
- # 中文触发词按「子串」匹配(中文无词边界);英文触发词按「词边界」匹配,
58
- # 否则短词会误命中常见词:"now"⊂"knowledge"、"current"⊂"concurrent"、
59
- # "now"⊂"known" —— 在这个满口 knowledge 的项目里会频繁无谓地打百度。
60
43
  _CJK_TRIGGERS = [k for k in SEARCH_TRIGGERS if not k.isascii()]
61
44
  _ASCII_TRIGGERS = [k.lower() for k in SEARCH_TRIGGERS if k.isascii()]
62
45
  _ASCII_RE = (
@@ -85,16 +68,12 @@ def needs_search(query: str) -> bool:
85
68
  return False
86
69
  q = query.lower()
87
70
  if is_personal_query(q):
88
- return False # 问的是自己的数据 → 答案在本地内核,绝不联网
89
- # 自我模型/身份问题(你是什么模型 / 你现在是哪个模型 …)→ 由 ModelRouter 运行时注入本地即答,不联网。
90
- # 否则会因含"模型"触发词白跑一次 ~20s 百度搜索。"你"与"模型"相邻(≤8字)才算自问;
91
- # "你帮我查 X 的模型"(相距远)仍走搜索。
71
+ return False
92
72
  if re.search(r"你[^,。?!]{0,8}模型", q):
93
73
  return False
94
74
  if any(k in q for k in _CJK_TRIGGERS):
95
- return True # 外部世界话题 → 联网
75
+ return True
96
76
  return bool(_ASCII_RE and _ASCII_RE.search(q))
97
- # 注:只含时间词(今天/本月/最近…)而无话题 → 不触发(见上方 TIME_WORDS 说明)
98
77
 
99
78
 
100
79
  def _key() -> str:
@@ -139,7 +118,7 @@ def _search_answer_inner(query: str, key: str) -> dict:
139
118
  "stream": False,
140
119
  "model": _model(),
141
120
  "enable_deep_search": _deep(),
142
- "enable_corner_markers": False, # 不要答案里的 ^[1]^ 角标(参考资料已单列)
121
+ "enable_corner_markers": False,
143
122
  }).encode("utf-8")
144
123
  req = urllib.request.Request(
145
124
  f"{_base()}/chat/completions",
@@ -193,7 +172,6 @@ def format_for_prompt(query: str, payload) -> str:
193
172
  refs = payload.get("references", [])
194
173
  if not answer and not refs:
195
174
  return ""
196
- # 我方的作答指令(可信,放在 <untrusted> 之外);检索到的外部内容(不可信,放在 <untrusted> 之内)。
197
175
  framing = (
198
176
  f"以下是针对「{query}」的实时联网搜索结果(来自百度 AI 搜索)。\n"
199
177
  f"【严格要求】只能依据下面 <untrusted> 数据块里的搜索结果作答:结果里没有出现的型号 / "
@@ -215,13 +193,11 @@ def format_for_prompt(query: str, payload) -> str:
215
193
  if u:
216
194
  inner += f"\n链接:{u}"
217
195
  inner += f"\n[webpage {i} end]\n"
218
- # 隔离外部内容:搜索结果是外部不可控数据,包裹进 <untrusted> 防间接注入(见 injection_guard)。
219
196
  from . import injection_guard as _ig
220
197
  block = _ig.wrap_untrusted(inner.rstrip(), source="web_search")
221
198
  closing = ("\n请**严格基于以上 <untrusted> 内的搜索结果**回答用户的问题;结果里没提到的内容,"
222
199
  "就明确说「搜索结果中未提及」,绝不编造型号/版本号。")
223
200
  return framing + block + closing
224
- # 列表形式(兼容)
225
201
  results = payload or []
226
202
  if not results:
227
203
  return ""
@@ -17,9 +17,9 @@ from datetime import datetime
17
17
  @dataclass
18
18
  class RiskFactor:
19
19
  """单个风险因子:是什么、证据、权重。"""
20
- factor: str # 风险因子名(如"超预算")
21
- evidence: str # 人类可读证据(如"餐饮已用 120%,超 ¥240")
22
- weight: float # 权重 0-1(对总风险的贡献)
20
+ factor: str
21
+ evidence: str
22
+ weight: float
23
23
 
24
24
  def to_dict(self) -> dict:
25
25
  return {"factor": self.factor, "evidence": self.evidence,
@@ -29,14 +29,14 @@ class RiskFactor:
29
29
  @dataclass
30
30
  class EvidenceChain:
31
31
  """结构化决策证据链 —— 财务决策的可审计输出。"""
32
- decision_type: str # 决策类型(如 budget_risk / anomaly)
33
- risk_score: float = 0.0 # 综合风险分 0-1
34
- risk_factors: list = field(default_factory=list) # [RiskFactor]
35
- mitigating_factors: list = field(default_factory=list) # 缓解因素(降低风险的)
36
- rationale: str = "" # 决策理由(人类可读)
37
- recommended_action: str = "" # 建议动作
38
- confidence: float = 0.0 # 置信度 0-1
39
- additional_checks: list = field(default_factory=list) # 还需补充的检查
32
+ decision_type: str
33
+ risk_score: float = 0.0
34
+ risk_factors: list = field(default_factory=list)
35
+ mitigating_factors: list = field(default_factory=list)
36
+ rationale: str = ""
37
+ recommended_action: str = ""
38
+ confidence: float = 0.0
39
+ additional_checks: list = field(default_factory=list)
40
40
  created_at: str = field(default_factory=lambda: datetime.now().isoformat(timespec="seconds"))
41
41
 
42
42
  def add_factor(self, factor: str, evidence: str, weight: float):
@@ -53,10 +53,10 @@ class EvidenceChain:
53
53
  self.risk_score = 0.0
54
54
  return 0.0
55
55
  weights = sorted((f.weight for f in self.risk_factors), reverse=True)
56
- score = weights[0] # 最大因子为主
56
+ score = weights[0]
57
57
  for w in weights[1:]:
58
- score += w * (1 - score) * 0.5 # 其余因子衰减叠加(饱和,不会超过1)
59
- mitigation = min(0.2, 0.1 * len(self.mitigating_factors)) # 缓解最多减0.2
58
+ score += w * (1 - score) * 0.5
59
+ mitigation = min(0.2, 0.1 * len(self.mitigating_factors))
60
60
  self.risk_score = max(0.0, min(1.0, round(score - mitigation, 3)))
61
61
  return self.risk_score
62
62
 
@@ -100,7 +100,6 @@ class FinanceExplainer:
100
100
  cat = b.get("category", "?")
101
101
  if ratio >= 1.0:
102
102
  over = b.get("spent", 0) - b.get("monthly_limit", 0)
103
- # 权重随超支程度递增:超10%→0.5,超50%→0.65,超100%+→0.8(上限)
104
103
  w = min(0.8, 0.45 + (ratio - 1.0) * 0.35)
105
104
  ec.add_factor("超预算", f"{cat} 已用 {ratio*100:.0f}%,超支 ¥{over:.0f}",
106
105
  weight=w)
@@ -109,13 +108,11 @@ class FinanceExplainer:
109
108
  ec.add_factor("接近预算上限",
110
109
  f"{cat} 已用 {ratio*100:.0f}%,接近上限", weight=0.25)
111
110
  near_budgets.append(cat)
112
- # 缓解因素:有预算分类未超支
113
111
  healthy = [b.get("category") for b in budget_status
114
112
  if b.get("usage_ratio", 0) < 0.8]
115
113
  if healthy:
116
114
  ec.mitigating_factors.append(f"{len(healthy)} 个分类预算健康")
117
115
  ec.compute_risk()
118
- # 理由 + 建议
119
116
  if over_budgets:
120
117
  ec.rationale = f"{','.join(over_budgets)} 已超预算,需立即关注"
121
118
  ec.recommended_action = f"削减 {over_budgets[0]} 支出或调整预算"
@@ -134,7 +131,6 @@ class FinanceExplainer:
134
131
  ec = EvidenceChain(decision_type="anomaly", confidence=0.85)
135
132
  for a in anomalies[:5]:
136
133
  z = a.get("z_score", 0)
137
- # z 分数越高,权重越大(裁剪到 0.6)
138
134
  w = min(0.6, 0.15 + z * 0.05)
139
135
  ec.add_factor("异常大额支出",
140
136
  f"{a.get('entity','?')} ¥{abs(a.get('amount',0)):.0f}"
@@ -15,7 +15,6 @@ from pathlib import Path
15
15
 
16
16
  BASE_CURRENCY = "CNY"
17
17
 
18
- # 参考价(1 单位外币 = 多少 CNY)。宁可粗,但要能改。
19
18
  DEFAULT_RATES = {
20
19
  "CNY": 1.0,
21
20
  "USD": 7.2,
@@ -33,7 +32,7 @@ DEFAULT_RATES = {
33
32
  class FxTable:
34
33
  """币种 → 本位币折算率,带落盘(原子写)。"""
35
34
 
36
- def __init__(self, path: str = None):
35
+ def __init__(self, path: str | None = None):
37
36
  self.path = Path(path) if path else None
38
37
  self.rates = dict(DEFAULT_RATES)
39
38
  self.note = "static"
@@ -54,7 +53,6 @@ class FxTable:
54
53
  if data.get("note"):
55
54
  self.note = data["note"]
56
55
  except Exception:
57
- # 汇率表坏了不能拖垮记账:退回出厂参考价,下次保存时自动覆盖
58
56
  pass
59
57
 
60
58
  def save(self) -> bool: