foxclaw 0.1.0 → 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.
- package/README.md +54 -29
- package/bin/foxclaw.cjs +51 -0
- package/index.js +4 -10
- package/package.json +46 -38
- package/python/.dockerignore +35 -0
- package/python/.env.example +173 -0
- package/python/.gitattributes +46 -0
- package/python/.github/workflows/ci.yml +167 -0
- package/python/.github/workflows/release.yml +183 -0
- package/python/CHANGELOG.md +57 -0
- package/python/CHANGELOG_CN.md +57 -0
- package/python/Dockerfile +64 -0
- package/python/LICENSE +21 -0
- package/python/MANIFEST.in +14 -0
- package/python/README.md +201 -0
- package/python/README_CN.md +336 -0
- package/python/bench/bm25_audit.py +186 -0
- package/python/bench/bm25_regression.py +177 -0
- package/python/bench/bm25_stress.py +155 -0
- package/python/bench/cd_audit.py +215 -0
- package/python/bench/chaos.py +140 -0
- package/python/bench/concurrency.py +103 -0
- package/python/bench/e2e_real_models.py +94 -0
- package/python/bench/llm_retry_test.py +241 -0
- package/python/bench/load_test.py +193 -0
- package/python/bench/vector_ivf_upgrade.py +128 -0
- package/python/docs/CHANNELS.md +195 -0
- package/python/docs/GOVERNANCE.md +164 -0
- package/python/docs/RELEASE.md +110 -0
- package/python/docs/RUN.md +75 -0
- package/python/docs/RUN_CN.md +121 -0
- package/python/docs//351/205/215/347/275/256/346/214/207/345/215/227.md +152 -0
- package/python/evals/__init__.py +0 -0
- package/python/evals/governance/README.md +61 -0
- package/python/evals/governance/__init__.py +0 -0
- package/python/evals/governance/cases.py +115 -0
- package/python/evals/governance/compare_embeddings.py +94 -0
- package/python/evals/governance/convergence.svg +11 -0
- package/python/evals/governance/report.json +371 -0
- package/python/evals/governance/run_evals.py +368 -0
- package/python/examples/governance_middleware_demo.py +72 -0
- package/python/examples/langgraph_governance_demo.py +104 -0
- package/python/examples/sample_itinerary.csv +6 -0
- package/python/examples/sample_knowledge.csv +9 -0
- package/python/examples/sample_transactions.csv +14 -0
- package/python/foxclaw/__init__.py +408 -0
- package/python/foxclaw/agents/__init__.py +12 -0
- package/python/foxclaw/agents/agents.py +35 -0
- package/python/foxclaw/agents/base.py +794 -0
- package/python/foxclaw/agents/impl/__init__.py +1 -0
- package/python/foxclaw/agents/impl/_helpers.py +70 -0
- package/python/foxclaw/agents/impl/finance.py +231 -0
- package/python/foxclaw/agents/impl/office.py +350 -0
- package/python/foxclaw/agents/impl/supervisor.py +592 -0
- package/python/foxclaw/agents/mixins/_context.py +128 -0
- package/python/foxclaw/agents/mixins/augment.py +143 -0
- package/python/foxclaw/agents/mixins/llm.py +67 -0
- package/python/foxclaw/agents/mixins/memory.py +41 -0
- package/python/foxclaw/agents/mixins/preference.py +131 -0
- package/python/foxclaw/agents/mixins/prompt.py +71 -0
- package/python/foxclaw/agents/mixins/skillbox.py +58 -0
- package/python/foxclaw/agents/mixins/tool_exec.py +417 -0
- package/python/foxclaw/api/__init__.py +224 -0
- package/python/foxclaw/api/_state.py +43 -0
- package/python/foxclaw/api/bootstrap.py +96 -0
- package/python/foxclaw/api/middleware.py +294 -0
- package/python/foxclaw/api/routes/ahs.py +404 -0
- package/python/foxclaw/api/routes/attachments.py +116 -0
- package/python/foxclaw/api/routes/browser.py +35 -0
- package/python/foxclaw/api/routes/domain.py +306 -0
- package/python/foxclaw/api/routes/knowledge.py +145 -0
- package/python/foxclaw/api/routes/llm_admin.py +449 -0
- package/python/foxclaw/api/routes/memory.py +105 -0
- package/python/foxclaw/api/routes/orchestration.py +39 -0
- package/python/foxclaw/api/routes/recovery.py +44 -0
- package/python/foxclaw/api/routes/review.py +266 -0
- package/python/foxclaw/api/routes/search.py +33 -0
- package/python/foxclaw/api/routes/system.py +311 -0
- package/python/foxclaw/api/routes/task.py +263 -0
- package/python/foxclaw/api/routes/ws.py +64 -0
- package/python/foxclaw/api/spine.py +52 -0
- package/python/foxclaw/browser/__init__.py +13 -0
- package/python/foxclaw/browser/action/__init__.py +1 -0
- package/python/foxclaw/browser/action/action_executor.py +50 -0
- package/python/foxclaw/browser/action/action_types.py +44 -0
- package/python/foxclaw/browser/action/action_verifier.py +68 -0
- package/python/foxclaw/browser/action/url_policy.py +143 -0
- package/python/foxclaw/browser/adapter/__init__.py +1 -0
- package/python/foxclaw/browser/adapter/base_adapter.py +23 -0
- package/python/foxclaw/browser/adapter/web_adapter.py +46 -0
- package/python/foxclaw/browser/driver/__init__.py +1 -0
- package/python/foxclaw/browser/driver/base_driver.py +61 -0
- package/python/foxclaw/browser/driver/playwright_driver.py +170 -0
- package/python/foxclaw/browser/perception/__init__.py +1 -0
- package/python/foxclaw/browser/perception/dom_extractor.py +42 -0
- package/python/foxclaw/browser/perception/element_indexer.py +56 -0
- package/python/foxclaw/browser/perception/semantic_reducer.py +33 -0
- package/python/foxclaw/browser/perception/snapshot.py +29 -0
- package/python/foxclaw/browser/service.py +144 -0
- package/python/foxclaw/browser/session/__init__.py +1 -0
- package/python/foxclaw/browser/session/session_manager.py +57 -0
- package/python/foxclaw/capability/__init__.py +12 -0
- package/python/foxclaw/capability/embedding_service.py +65 -0
- package/python/foxclaw/channels/__init__.py +138 -0
- package/python/foxclaw/channels/discord.py +175 -0
- package/python/foxclaw/channels/dispatcher.py +179 -0
- package/python/foxclaw/channels/feishu.py +215 -0
- package/python/foxclaw/channels/openclaw_bridge.py +151 -0
- package/python/foxclaw/channels/signing.py +187 -0
- package/python/foxclaw/channels/telegram.py +120 -0
- package/python/foxclaw/channels/weixin.py +215 -0
- package/python/foxclaw/cli.py +183 -0
- package/python/foxclaw/cognitive/__init__.py +4 -0
- package/python/foxclaw/cognitive/reasoner.py +81 -0
- package/python/foxclaw/cognitive/state.py +141 -0
- package/python/foxclaw/core/__init__.py +9 -0
- package/python/foxclaw/core/audit_chain.py +131 -0
- package/python/foxclaw/core/audit_sink.py +100 -0
- package/python/foxclaw/core/compaction.py +265 -0
- package/python/foxclaw/core/config.py +65 -0
- package/python/foxclaw/core/confirm_nonce.py +151 -0
- package/python/foxclaw/core/dispatch.py +27 -0
- package/python/foxclaw/core/envfile.py +48 -0
- package/python/foxclaw/core/failure_policy.py +142 -0
- package/python/foxclaw/core/gateway.py +81 -0
- package/python/foxclaw/core/governance_audit.py +171 -0
- package/python/foxclaw/core/idempotency.py +282 -0
- package/python/foxclaw/core/injection_guard.py +202 -0
- package/python/foxclaw/core/memory_adapter.py +489 -0
- package/python/foxclaw/core/metrics.py +88 -0
- package/python/foxclaw/core/model_router.py +985 -0
- package/python/foxclaw/core/paths.py +103 -0
- package/python/foxclaw/core/recovery.py +349 -0
- package/python/foxclaw/core/scheduler.py +141 -0
- package/python/foxclaw/core/secrets.py +131 -0
- package/python/foxclaw/core/sessions.py +155 -0
- package/python/foxclaw/core/settings.py +414 -0
- package/python/foxclaw/core/shared_write.py +186 -0
- package/python/foxclaw/core/steering_queue.py +154 -0
- package/python/foxclaw/core/system_bus.py +84 -0
- package/python/foxclaw/core/thought_chain.py +316 -0
- package/python/foxclaw/core/token_meter.py +231 -0
- package/python/foxclaw/core/tools.py +164 -0
- package/python/foxclaw/core/tracing.py +375 -0
- package/python/foxclaw/core/truth_pipeline.py +110 -0
- package/python/foxclaw/core/update_check.py +149 -0
- package/python/foxclaw/core/vec_recall_adapter.py +477 -0
- package/python/foxclaw/core/verify.py +173 -0
- package/python/foxclaw/core/web_search.py +218 -0
- package/python/foxclaw/finance/__init__.py +15 -0
- package/python/foxclaw/finance/evidence.py +149 -0
- package/python/foxclaw/finance/fx.py +98 -0
- package/python/foxclaw/finance/graph.py +179 -0
- package/python/foxclaw/finance/models.py +146 -0
- package/python/foxclaw/finance/nlparse.py +515 -0
- package/python/foxclaw/finance/policy.py +301 -0
- package/python/foxclaw/finance/reasoner.py +121 -0
- package/python/foxclaw/finance/reasoning.py +319 -0
- package/python/foxclaw/finance/reconcile.py +175 -0
- package/python/foxclaw/finance/runtime.py +500 -0
- package/python/foxclaw/governance/__init__.py +24 -0
- package/python/foxclaw/governance/middleware.py +176 -0
- package/python/foxclaw/harness/__init__.py +17 -0
- package/python/foxclaw/harness/_base.py +66 -0
- package/python/foxclaw/harness/_policy.py +340 -0
- package/python/foxclaw/harness/drift.py +77 -0
- package/python/foxclaw/harness/event_bus.py +28 -0
- package/python/foxclaw/harness/harness.py +358 -0
- package/python/foxclaw/harness/loop_detection.py +145 -0
- package/python/foxclaw/itinerary/__init__.py +10 -0
- package/python/foxclaw/itinerary/amap_client.py +114 -0
- package/python/foxclaw/itinerary/models.py +80 -0
- package/python/foxclaw/itinerary/reasoner.py +96 -0
- package/python/foxclaw/itinerary/reasoning.py +168 -0
- package/python/foxclaw/itinerary/runtime.py +144 -0
- package/python/foxclaw/kernel/__init__.py +13 -0
- package/python/foxclaw/kernel/capabilities.py +72 -0
- package/python/foxclaw/kernel/policy_gate.py +108 -0
- package/python/foxclaw/kernel/process_manager.py +186 -0
- package/python/foxclaw/kernel/scheduler.py +125 -0
- package/python/foxclaw/kernel/sentinel.py +443 -0
- package/python/foxclaw/kernel/sentinel_client.py +243 -0
- package/python/foxclaw/kernel/spine.py +97 -0
- package/python/foxclaw/knowledge/__init__.py +360 -0
- package/python/foxclaw/knowledge/truth_crystallizer.py +89 -0
- package/python/foxclaw/learning/__init__.py +12 -0
- package/python/foxclaw/learning/knowledge_graph.py +144 -0
- package/python/foxclaw/learning/models.py +93 -0
- package/python/foxclaw/learning/reasoner.py +67 -0
- package/python/foxclaw/learning/runtime.py +351 -0
- package/python/foxclaw/learning/spaced_repetition.py +91 -0
- package/python/foxclaw/mcp_server.py +188 -0
- package/python/foxclaw/orchestration/__init__.py +167 -0
- package/python/foxclaw/pipeline/__init__.py +23 -0
- package/python/foxclaw/pipeline/cognitive_pipeline.py +429 -0
- package/python/foxclaw/pipeline/stage_meta.py +21 -0
- package/python/foxclaw/pipeline/supervisor_stages.py +201 -0
- package/python/foxclaw/planning/__init__.py +17 -0
- package/python/foxclaw/planning/llm_planner.py +232 -0
- package/python/foxclaw/planning/planner.py +94 -0
- package/python/foxclaw/search/__init__.py +24 -0
- package/python/foxclaw/search/agent_port.py +190 -0
- package/python/foxclaw/search/evidence.py +109 -0
- package/python/foxclaw/search/external_search.py +50 -0
- package/python/foxclaw/search/hkc_read_port.py +60 -0
- package/python/foxclaw/search/internal_search.py +71 -0
- package/python/foxclaw/search/models.py +127 -0
- package/python/foxclaw/search/planner.py +44 -0
- package/python/foxclaw/search/retriever.py +39 -0
- package/python/foxclaw/search/runtime.py +127 -0
- package/python/foxclaw/search/strategy.py +44 -0
- package/python/foxclaw/skills/__init__.py +559 -0
- package/python/foxclaw/skills/archive.py +111 -0
- package/python/foxclaw/skills/fetch.py +226 -0
- package/python/foxclaw/skills/hooks_spec.py +69 -0
- package/python/foxclaw/skills/install_cmd.py +216 -0
- package/python/foxclaw/skills/lifecycle.py +252 -0
- package/python/foxclaw/skills/markdown_skill.py +175 -0
- package/python/foxclaw/skills/markdown_store.py +39 -0
- package/python/foxclaw/skills/mcp_client.py +184 -0
- package/python/foxclaw/skills/mcp_store.py +40 -0
- package/python/foxclaw/skills/permission.py +145 -0
- package/python/foxclaw/skills/pipeline.py +354 -0
- package/python/foxclaw/skills/policy.py +74 -0
- package/python/foxclaw/skills/review.py +72 -0
- package/python/foxclaw/skills/store.py +57 -0
- package/python/frontend/assets/core_bg.jpg +0 -0
- package/python/frontend/deck/auth.js +25 -0
- package/python/frontend/deck/chat_model_picker.js +483 -0
- package/python/frontend/deck/domain.js +276 -0
- package/python/frontend/deck/main.js +1806 -0
- package/python/frontend/deck/markdown.js +44 -0
- package/python/frontend/deck/settings.js +307 -0
- package/python/frontend/deck/state.js +5 -0
- package/python/frontend/deck/util.js +4 -0
- package/python/frontend/deck/visuals.js +190 -0
- package/python/frontend/deck.css +855 -0
- package/python/frontend/deck.html +686 -0
- package/python/frontend/deck_v2.html +341 -0
- package/python/frontend/planet/index.html +266 -0
- package/python/frontend/planet/planet.css +579 -0
- package/python/frontend/planet/planet.js +1436 -0
- package/python/frontend/planet/vendor/three.core.min.js +6 -0
- package/python/frontend/planet/vendor/three.module.min.js +6 -0
- package/python/frontend/v3/assets/index-CQSBdxk4.js +34 -0
- package/python/frontend/v3/assets/index-D5nIsjms.css +1 -0
- package/python/frontend/v3/index.html +15 -0
- package/python/frontend/widget_runtime.js +252 -0
- package/python/frontend/widgets.js +639 -0
- package/python/frontend_src/index.html +14 -0
- package/python/frontend_src/package-lock.json +1376 -0
- package/python/frontend_src/package.json +19 -0
- package/python/frontend_src/src/App.vue +58 -0
- package/python/frontend_src/src/api.js +82 -0
- package/python/frontend_src/src/components/ChatDock.vue +365 -0
- package/python/frontend_src/src/components/ComposerMenu.vue +75 -0
- package/python/frontend_src/src/components/DocViewer.vue +162 -0
- package/python/frontend_src/src/components/Inspector.vue +235 -0
- package/python/frontend_src/src/components/LeftRail.vue +415 -0
- package/python/frontend_src/src/components/MenuPopover.vue +128 -0
- package/python/frontend_src/src/components/MessageStream.vue +389 -0
- package/python/frontend_src/src/components/ModelPicker.vue +350 -0
- package/python/frontend_src/src/components/QuickIngest.vue +112 -0
- package/python/frontend_src/src/components/Stage.vue +272 -0
- package/python/frontend_src/src/components/ToolMenu.vue +141 -0
- package/python/frontend_src/src/components/TopBar.vue +118 -0
- package/python/frontend_src/src/components/panels/ActivityPanel.vue +82 -0
- package/python/frontend_src/src/components/panels/AgentsPanel.vue +83 -0
- package/python/frontend_src/src/components/panels/KnowledgePanel.vue +425 -0
- package/python/frontend_src/src/components/panels/MemoryPanel.vue +117 -0
- package/python/frontend_src/src/components/panels/PanelBox.vue +50 -0
- package/python/frontend_src/src/components/panels/ReviewPanel.vue +292 -0
- package/python/frontend_src/src/components/panels/SettingsPanel.vue +358 -0
- package/python/frontend_src/src/components/panels/TasksPanel.vue +73 -0
- package/python/frontend_src/src/components/panels/ToolsPanel.vue +746 -0
- package/python/frontend_src/src/components/panels/UsagePanel.vue +105 -0
- package/python/frontend_src/src/lib/fmt.js +44 -0
- package/python/frontend_src/src/lib/i18n.js +53 -0
- package/python/frontend_src/src/lib/locales/en.js +173 -0
- package/python/frontend_src/src/lib/locales/zh.js +174 -0
- package/python/frontend_src/src/lib/md.js +190 -0
- package/python/frontend_src/src/main.js +9 -0
- package/python/frontend_src/src/stores/app.js +91 -0
- package/python/frontend_src/src/stores/chat.js +450 -0
- package/python/frontend_src/src/stores/model.js +96 -0
- package/python/frontend_src/src/stores/panels.js +289 -0
- package/python/frontend_src/src/stores/profile.js +31 -0
- package/python/frontend_src/src/stores/system.js +25 -0
- package/python/frontend_src/src/styles/base.css +138 -0
- package/python/frontend_src/src/styles/panel.css +117 -0
- package/python/frontend_src/src/styles/tokens.css +78 -0
- package/python/frontend_src/vite.config.js +39 -0
- package/python/pyproject.toml +268 -0
- package/python/requirements.txt +311 -0
- package/python/skills/README.md +123 -0
- package/python/skills/execution-contract/SKILL.md +30 -0
- package/python/skills/general-assistant/SKILL.md +34 -0
- package/python/skills/planner/SKILL.md +33 -0
- package/python/skills/schedule-itinerary/SKILL.md +35 -0
- package/python/skills/skill-composition/SKILL.md +31 -0
- package/python/skills/skill-guard/SKILL.md +67 -0
- package/python/skills/skill-memory/SKILL.md +50 -0
- package/python/skills/skill-router/SKILL.md +35 -0
- package/python/tests/conftest.py +54 -0
- package/python/tests/run_all.py +200 -0
- package/python/tests/test_agent_lifecycle.py +326 -0
- package/python/tests/test_ahs_p2.py +393 -0
- package/python/tests/test_anthropic_tools.py +192 -0
- package/python/tests/test_api.py +97 -0
- package/python/tests/test_api_auth.py +79 -0
- package/python/tests/test_api_host_guard.py +69 -0
- package/python/tests/test_attachments.py +151 -0
- package/python/tests/test_audit_chain.py +116 -0
- package/python/tests/test_audit_sink.py +130 -0
- package/python/tests/test_browser.py +149 -0
- package/python/tests/test_browser_request_guard.py +180 -0
- package/python/tests/test_capabilities.py +152 -0
- package/python/tests/test_channel_signing.py +185 -0
- package/python/tests/test_channel_trust.py +109 -0
- package/python/tests/test_channels.py +174 -0
- package/python/tests/test_cli.py +46 -0
- package/python/tests/test_compaction_toolpair.py +119 -0
- package/python/tests/test_config_layering.py +61 -0
- package/python/tests/test_confirm_nonce.py +186 -0
- package/python/tests/test_context_compaction_wiring.py +124 -0
- package/python/tests/test_dispatch.py +89 -0
- package/python/tests/test_domain_evals.py +334 -0
- package/python/tests/test_domain_import.py +72 -0
- package/python/tests/test_domain_reasoners.py +134 -0
- package/python/tests/test_e2e.py +161 -0
- package/python/tests/test_emb_cache.py +97 -0
- package/python/tests/test_embedding_singleton.py +87 -0
- package/python/tests/test_envfile.py +83 -0
- package/python/tests/test_evals_smoke.py +90 -0
- package/python/tests/test_event_spine.py +174 -0
- package/python/tests/test_evidence_semantic.py +88 -0
- package/python/tests/test_evidence_sse.py +94 -0
- package/python/tests/test_file_ingest.py +135 -0
- package/python/tests/test_finance.py +144 -0
- package/python/tests/test_finance_ledger.py +245 -0
- package/python/tests/test_finance_persist.py +174 -0
- package/python/tests/test_gateway.py +125 -0
- package/python/tests/test_governance.py +421 -0
- package/python/tests/test_governance_audit.py +324 -0
- package/python/tests/test_governance_middleware.py +137 -0
- package/python/tests/test_hkc.py +268 -0
- package/python/tests/test_hkc_index_build_manager.py +306 -0
- package/python/tests/test_hkc_manifest_reuse.py +294 -0
- package/python/tests/test_hkc_search_event_sync.py +482 -0
- package/python/tests/test_import_path_guard.py +155 -0
- package/python/tests/test_injection_guard.py +193 -0
- package/python/tests/test_integration.py +117 -0
- package/python/tests/test_itinerary.py +100 -0
- package/python/tests/test_kee_semantic.py +84 -0
- package/python/tests/test_kernel.py +212 -0
- package/python/tests/test_learning.py +101 -0
- package/python/tests/test_llm_admin.py +145 -0
- package/python/tests/test_llm_planner.py +344 -0
- package/python/tests/test_markdown_skills.py +62 -0
- package/python/tests/test_markdown_skills_openclaw.py +66 -0
- package/python/tests/test_mcp_connectors.py +97 -0
- package/python/tests/test_mcp_guardian_discovery.py +87 -0
- package/python/tests/test_mcp_no_memory.py +43 -0
- package/python/tests/test_mcp_policy_gate.py +76 -0
- package/python/tests/test_mcp_server.py +92 -0
- package/python/tests/test_memory_recall.py +92 -0
- package/python/tests/test_metrics.py +58 -0
- package/python/tests/test_multi_chain.py +90 -0
- package/python/tests/test_multimodal.py +122 -0
- package/python/tests/test_negotiation.py +136 -0
- package/python/tests/test_orchestration.py +109 -0
- package/python/tests/test_orchestration_decoupled.py +103 -0
- package/python/tests/test_p3_p5_hardening.py +324 -0
- package/python/tests/test_parallel_dispatch.py +207 -0
- package/python/tests/test_pipeline.py +492 -0
- package/python/tests/test_plan_visible.py +86 -0
- package/python/tests/test_planner.py +88 -0
- package/python/tests/test_policy_gate.py +108 -0
- package/python/tests/test_profile.py +67 -0
- package/python/tests/test_readme_claims.py +163 -0
- package/python/tests/test_reasoner.py +170 -0
- package/python/tests/test_release_check.py +252 -0
- package/python/tests/test_resume_checkpoint.py +148 -0
- package/python/tests/test_routing.py +85 -0
- package/python/tests/test_scheduler.py +138 -0
- package/python/tests/test_search.py +194 -0
- package/python/tests/test_secrets.py +153 -0
- package/python/tests/test_semantic.py +69 -0
- package/python/tests/test_sessions.py +90 -0
- package/python/tests/test_settings.py +265 -0
- package/python/tests/test_shadow_learning.py +290 -0
- package/python/tests/test_shared_write.py +200 -0
- package/python/tests/test_skill_taint_gate.py +145 -0
- package/python/tests/test_skills.py +117 -0
- package/python/tests/test_skills_fetch.py +282 -0
- package/python/tests/test_stats_and_events.py +162 -0
- package/python/tests/test_streaming.py +55 -0
- package/python/tests/test_supervisor_pipeline.py +176 -0
- package/python/tests/test_supervisor_triage.py +102 -0
- package/python/tests/test_taint.py +112 -0
- package/python/tests/test_task_stream.py +74 -0
- package/python/tests/test_thinking_sse.py +66 -0
- package/python/tests/test_thinking_stream.py +76 -0
- package/python/tests/test_tool_center.py +113 -0
- package/python/tests/test_tool_parallel.py +113 -0
- package/python/tests/test_tool_params.py +99 -0
- package/python/tests/test_tool_use.py +229 -0
- package/python/tests/test_toplevel_concurrency.py +116 -0
- package/python/tests/test_tracing.py +256 -0
- package/python/tests/test_truth_crystallizer.py +125 -0
- package/python/tests/test_truth_flow.py +88 -0
- package/python/tests/test_update_check.py +95 -0
- package/python/tests/test_url_policy.py +53 -0
- package/python/tests/test_verify_evidence.py +212 -0
- package/python/tests/test_version_sync.py +100 -0
- package/python/tests/test_web_trigger.py +82 -0
- package/python/tools/check_faiss_type.py +30 -0
- package/python/tools/check_log.ps1 +38 -0
- package/python/tools/ivf_status.py +72 -0
- package/python/tools/mypy_baseline.txt +13 -0
- package/python/tools/mypy_ratchet.py +143 -0
- package/python/tools/pack.py +173 -0
- package/python/tools/release_check.py +349 -0
- package/python/tools/restart_foxclaw.ps1 +71 -0
- package/python/tools/shared_write_audit.py +190 -0
- package/python/tools/size_baseline.txt +28 -0
- package/python/tools/size_check.py +102 -0
- package/python/tools/strip_comments.py +228 -0
- package/python/tools/verify_env.bat +2 -0
- package/python/tools/verify_env.py +25 -0
- package/python/vendor/HMR_LICENSE +37 -0
- package/python/vendor/HMR_README.md +65 -0
- package/python/vendor/hkc/LICENSE +37 -0
- package/python/vendor/hkc/hkc_ace/__init__.py +0 -0
- package/python/vendor/hkc/hkc_ace/ace.py +457 -0
- package/python/vendor/hkc/hkc_ace/skill_taxonomy.json +465 -0
- package/python/vendor/hkc/hkc_api/__init__.py +0 -0
- package/python/vendor/hkc/hkc_api/container.py +416 -0
- package/python/vendor/hkc/hkc_api/ingest_jobs.py +103 -0
- package/python/vendor/hkc/hkc_api/main.py +157 -0
- package/python/vendor/hkc/hkc_api/routes/__init__.py +0 -0
- package/python/vendor/hkc/hkc_api/routes/ability.py +151 -0
- package/python/vendor/hkc/hkc_api/routes/config.py +51 -0
- package/python/vendor/hkc/hkc_api/routes/knowledge.py +630 -0
- package/python/vendor/hkc/hkc_api/routes/search.py +88 -0
- package/python/vendor/hkc/hkc_api/schemas.py +153 -0
- package/python/vendor/hkc/hkc_api/synthesis.py +179 -0
- package/python/vendor/hkc/hkc_core/__init__.py +0 -0
- package/python/vendor/hkc/hkc_core/graph/__init__.py +0 -0
- package/python/vendor/hkc/hkc_core/graph/base.py +188 -0
- package/python/vendor/hkc/hkc_core/graph/sqlite_store.py +789 -0
- package/python/vendor/hkc/hkc_core/models/__init__.py +0 -0
- package/python/vendor/hkc/hkc_core/models/enums.py +65 -0
- package/python/vendor/hkc/hkc_core/models/ku.py +307 -0
- package/python/vendor/hkc/hkc_core/utils/__init__.py +0 -0
- package/python/vendor/hkc/hkc_core/utils/id_gen.py +113 -0
- package/python/vendor/hkc/hkc_crystallizer/__init__.py +44 -0
- package/python/vendor/hkc/hkc_crystallizer/adapters.py +94 -0
- package/python/vendor/hkc/hkc_crystallizer/candidate.py +145 -0
- package/python/vendor/hkc/hkc_crystallizer/crystallizer.py +179 -0
- package/python/vendor/hkc/hkc_crystallizer/ingress.py +90 -0
- package/python/vendor/hkc/hkc_kde/__init__.py +0 -0
- package/python/vendor/hkc/hkc_kde/chunker.py +197 -0
- package/python/vendor/hkc/hkc_kde/extractor.py +623 -0
- package/python/vendor/hkc/hkc_kde/kde.py +298 -0
- package/python/vendor/hkc/hkc_kde/loaders/__init__.py +0 -0
- package/python/vendor/hkc/hkc_kde/loaders/ebook_loader.py +153 -0
- package/python/vendor/hkc/hkc_kde/loaders/office_loader.py +194 -0
- package/python/vendor/hkc/hkc_kde/loaders/pdf_loader.py +162 -0
- package/python/vendor/hkc/hkc_kde/loaders/text_loader.py +157 -0
- package/python/vendor/hkc/hkc_kde/loaders/web_loader.py +91 -0
- package/python/vendor/hkc/hkc_kde/models.py +74 -0
- package/python/vendor/hkc/hkc_kde/packager.py +329 -0
- package/python/vendor/hkc/hkc_kde/relation_disc.py +151 -0
- package/python/vendor/hkc/hkc_kee/__init__.py +0 -0
- package/python/vendor/hkc/hkc_kee/dedup.py +227 -0
- package/python/vendor/hkc/hkc_kee/fingerprint.py +106 -0
- package/python/vendor/hkc/hkc_kee/kee.py +541 -0
- package/python/vendor/hkc/hkc_kep/__init__.py +0 -0
- package/python/vendor/hkc/hkc_kep/event_bus.py +205 -0
- package/python/vendor/hkc/hkc_sdk/__init__.py +63 -0
- package/python/vendor/hkc/hkc_sdk/base.py +151 -0
- package/python/vendor/hkc/hkc_sdk/direct_client.py +153 -0
- package/python/vendor/hkc/hkc_sdk/exceptions.py +39 -0
- package/python/vendor/hkc/hkc_sdk/http_client.py +166 -0
- package/python/vendor/hkc/hkc_sdk/models.py +171 -0
- package/python/vendor/hkc/hkc_search/__init__.py +0 -0
- package/python/vendor/hkc/hkc_search/bm25.py +325 -0
- package/python/vendor/hkc/hkc_search/embedding_backends.py +374 -0
- package/python/vendor/hkc/hkc_search/graph_search.py +132 -0
- package/python/vendor/hkc/hkc_search/hybrid.py +331 -0
- package/python/vendor/hkc/hkc_search/index_build_manager.py +378 -0
- package/python/vendor/hkc/hkc_search/vector_search.py +484 -0
- package/python/vendor/hmr/__init__.py +0 -0
- package/python/vendor/hmr/core/__init__.py +0 -0
- package/python/vendor/hmr/core/hmr.py +684 -0
- package/python/vendor/hmr/core/models.py +310 -0
- package/python/vendor/hmr/engines/__init__.py +0 -0
- package/python/vendor/hmr/engines/evolution.py +710 -0
- package/python/vendor/hmr/engines/jit_compiler.py +472 -0
- package/python/vendor/hmr/engines/lifecycle.py +264 -0
- package/python/vendor/hmr/engines/policy.py +625 -0
- package/python/vendor/hmr/engines/recall.py +260 -0
- package/python/vendor/hmr/engines/runtime_state.py +158 -0
- package/python/vendor/hmr/engines/scheduler.py +323 -0
- package/python/vendor/hmr/engines/semantic.py +150 -0
- package/python/vendor/hmr/engines/temporal.py +261 -0
- package/python/vendor/hmr/engines/thought_chain.py +635 -0
- package/python/vendor/hmr/graph/__init__.py +0 -0
- package/python/vendor/hmr/graph/cwg.py +338 -0
- package/python/vendor/hmr/graph/memory_graph.py +530 -0
- package/python/vendor/hmr/storage/__init__.py +0 -0
- package/python/vendor/hmr/storage/memory_fs.py +300 -0
- package/python/vendor/hmr/storage/vector_store.py +520 -0
|
@@ -0,0 +1,245 @@
|
|
|
1
|
+
"""自然语言记账 —— 用户随口报备的一句花费,要能自己识别、记账、汇总。
|
|
2
|
+
|
|
3
|
+
这是财务应用第一线的功能,输入是「今天买了一件衣服花了 105 块」这种大白话。
|
|
4
|
+
测试固定四件事:
|
|
5
|
+
1. 金额/币种/时间/分类识别对(含中文数字、美元/美金/块/块钱);
|
|
6
|
+
2. 过去/今天 → 已发生,明天/下个月 → 计划支出,且计划不污染"本月已花";
|
|
7
|
+
3. 一段话多笔要拆开,时间可继承;
|
|
8
|
+
4. 记账本汇总(按天/按分类/按币种)与汇率折算正确。
|
|
9
|
+
"""
|
|
10
|
+
import sys
|
|
11
|
+
from datetime import datetime
|
|
12
|
+
from pathlib import Path
|
|
13
|
+
|
|
14
|
+
sys.path.insert(0, str(Path(__file__).parent.parent))
|
|
15
|
+
|
|
16
|
+
from foxclaw.finance import FinanceRuntime
|
|
17
|
+
from foxclaw.finance.nlparse import (cn_to_number, has_expense_signal,
|
|
18
|
+
has_payment_verb, parse)
|
|
19
|
+
from foxclaw.agents.impl.finance import _should_autorecord
|
|
20
|
+
from foxclaw.agents.impl.supervisor import SupervisorAgent
|
|
21
|
+
|
|
22
|
+
NOW = datetime(2026, 11, 7, 20, 0)
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def _rt(tmp_path):
|
|
26
|
+
return FinanceRuntime(policy_path=str(tmp_path / "finance_policy.yaml"),
|
|
27
|
+
data_path=str(tmp_path / "finance_data.json"))
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def _one(text, now=NOW):
|
|
31
|
+
ps = parse(text, now=now)
|
|
32
|
+
assert len(ps) == 1, f"{text!r} 解析出 {len(ps)} 笔"
|
|
33
|
+
return ps[0]
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def test_今天买衣服105块():
|
|
37
|
+
p = _one("今天买了一件衣服花了105块")
|
|
38
|
+
assert (p.amount, p.currency, p.direction) == (105.0, "CNY", "expense")
|
|
39
|
+
assert p.when.date() == NOW.date() and not p.planned
|
|
40
|
+
assert p.category == "clothing"
|
|
41
|
+
assert p.entity == "衣服"
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def test_明天买大米45():
|
|
45
|
+
p = _one("明天买一袋大米花了45")
|
|
46
|
+
assert p.amount == 45.0 and p.planned
|
|
47
|
+
assert p.when.date() == datetime(2026, 11, 8).date()
|
|
48
|
+
assert p.category == "food"
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def test_后天洗车100():
|
|
52
|
+
p = _one("后天花了100洗车")
|
|
53
|
+
assert p.amount == 100.0 and p.planned
|
|
54
|
+
assert p.category == "transport"
|
|
55
|
+
assert p.when.date() == datetime(2026, 11, 9).date()
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def test_货币单位识别():
|
|
59
|
+
assert _one("花了105块").currency == "CNY"
|
|
60
|
+
assert _one("花了105块钱").currency == "CNY"
|
|
61
|
+
assert _one("花了105元").currency == "CNY"
|
|
62
|
+
assert _one("花了105人民币").currency == "CNY"
|
|
63
|
+
assert _one("花了105rmb").currency == "CNY"
|
|
64
|
+
assert _one("花了105美元").currency == "USD"
|
|
65
|
+
assert _one("花了105美金").currency == "USD"
|
|
66
|
+
assert _one("花了$105").currency == "USD"
|
|
67
|
+
assert _one("花了105港币").currency == "HKD"
|
|
68
|
+
assert _one("花了105欧元").currency == "EUR"
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def test_符号与前置币种():
|
|
72
|
+
assert _one("¥105").currency == "CNY"
|
|
73
|
+
assert _one("美元105").currency == "USD"
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def test_量级后缀():
|
|
77
|
+
assert _one("买了个包1.5k").amount == 1500.0
|
|
78
|
+
assert _one("买了个手机2万").amount == 20000.0
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def test_中文数字():
|
|
82
|
+
assert cn_to_number("一百零五") == 105
|
|
83
|
+
assert cn_to_number("两千三") == 2300
|
|
84
|
+
assert cn_to_number("三千五") == 3500
|
|
85
|
+
assert cn_to_number("十五") == 15
|
|
86
|
+
assert cn_to_number("十万") == 100000
|
|
87
|
+
assert _one("买了一百零五块的衣服").amount == 105.0
|
|
88
|
+
assert _one("两千三买了个手机").amount == 2300.0
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
def test_过去时间词():
|
|
92
|
+
assert _one("昨天买了个杯子30").when.date() == datetime(2026, 11, 6).date()
|
|
93
|
+
assert _one("前天买了个杯子30").when.date() == datetime(2026, 11, 5).date()
|
|
94
|
+
assert _one("大前天买了个杯子30").when.date() == datetime(2026, 11, 4).date()
|
|
95
|
+
assert _one("3天前买了个杯子30").when.date() == datetime(2026, 11, 4).date()
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
def test_未来时间词():
|
|
99
|
+
assert _one("大后天买了个杯子30").when.date() == datetime(2026, 11, 10).date()
|
|
100
|
+
assert _one("3天后买了个杯子30").when.date() == datetime(2026, 11, 10).date()
|
|
101
|
+
assert _one("下个月5号交房租3000").when.date() == datetime(2026, 12, 5).date()
|
|
102
|
+
assert _one("下个月5号交房租3000").planned
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
def test_上个月与上月():
|
|
106
|
+
p = _one("上个月花了2000")
|
|
107
|
+
assert p.when.strftime("%Y-%m") == "2026-10" and not p.planned
|
|
108
|
+
assert _one("上月5号买衣服300").when.date() == datetime(2026, 10, 5).date()
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
def test_几月与几号():
|
|
112
|
+
assert _one("9月买衣服105").when.strftime("%Y-%m") == "2026-09"
|
|
113
|
+
assert _one("5号买衣服105", now=datetime(2026, 11, 7)).when.strftime("%Y-%m") == "2026-11"
|
|
114
|
+
assert _one("28号买衣服105", now=datetime(2026, 11, 7)).when.strftime("%Y-%m") == "2026-10"
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
def test_时间词不会被当成金额():
|
|
118
|
+
p = _one("上个月5号买了双鞋300")
|
|
119
|
+
assert p.amount == 300.0
|
|
120
|
+
assert p.when.date() == datetime(2026, 10, 5).date()
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
def test_一句话多笔拆分():
|
|
124
|
+
ps = parse("今天买衣服105,还买了大米45", now=NOW)
|
|
125
|
+
assert [p.amount for p in ps] == [105.0, 45.0]
|
|
126
|
+
assert {p.category for p in ps} == {"clothing", "food"}
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
def test_时间可继承():
|
|
130
|
+
ps = parse("今天买衣服105,洗车100", now=NOW)
|
|
131
|
+
assert len(ps) == 2
|
|
132
|
+
assert all(p.when.date() == NOW.date() for p in ps)
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
def test_单笔带逗号不拆坏():
|
|
136
|
+
p = _one("今天,我买了一件衣服,花了105块")
|
|
137
|
+
assert p.amount == 105.0 and p.category == "clothing"
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
def test_收入方向():
|
|
141
|
+
p = _one("工资发了12000")
|
|
142
|
+
assert p.direction == "income" and p.amount == 12000.0
|
|
143
|
+
assert p.category == "income"
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
def test_无金额不解析():
|
|
147
|
+
assert parse("今天几号", now=NOW) == []
|
|
148
|
+
assert parse("本月预算怎么分配", now=NOW) == []
|
|
149
|
+
assert parse("今天买了几件衣服", now=NOW) == []
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
def test_花费信号闸门():
|
|
153
|
+
assert has_expense_signal("今天买了一件衣服花了105块")
|
|
154
|
+
assert has_expense_signal("洗车100")
|
|
155
|
+
assert not has_expense_signal("今天几号")
|
|
156
|
+
assert not has_expense_signal("3个月计划")
|
|
157
|
+
assert has_payment_verb("我买了105块的衣服")
|
|
158
|
+
assert not has_payment_verb("衣服预算800")
|
|
159
|
+
|
|
160
|
+
|
|
161
|
+
def test_预算句不自动记账():
|
|
162
|
+
assert not _should_autorecord("衣服预算800")
|
|
163
|
+
assert not _should_autorecord("给我定个800的餐饮预算")
|
|
164
|
+
assert _should_autorecord("今天买了一件衣服花了105块")
|
|
165
|
+
assert _should_autorecord("洗车100")
|
|
166
|
+
|
|
167
|
+
|
|
168
|
+
def test_主管把花费报备路由给财务():
|
|
169
|
+
assert SupervisorAgent._is_expense_report("今天买了一件衣服花了105块")
|
|
170
|
+
assert SupervisorAgent._is_expense_report("后天花了100洗车")
|
|
171
|
+
assert not SupervisorAgent._is_expense_report("帮我总结这篇文档")
|
|
172
|
+
assert not SupervisorAgent._is_expense_report("今天几号")
|
|
173
|
+
|
|
174
|
+
|
|
175
|
+
def test_记账回执与账本(tmp_path):
|
|
176
|
+
rt = _rt(tmp_path)
|
|
177
|
+
rec = rt.record_text("今天买了一件衣服花了105块")
|
|
178
|
+
assert len(rec["recorded"]) == 1
|
|
179
|
+
assert "已记账 1 笔" in rec["ack"] and "105.00" in rec["ack"]
|
|
180
|
+
|
|
181
|
+
led = rt.ledger("2026-11")
|
|
182
|
+
assert led["count"] == 1 and led["expense"] == 105.0
|
|
183
|
+
assert led["by_category"][0]["category"] == "clothing"
|
|
184
|
+
assert led["planned"]["count"] == 0
|
|
185
|
+
|
|
186
|
+
|
|
187
|
+
def test_计划支出不进本月已花(tmp_path):
|
|
188
|
+
rt = _rt(tmp_path)
|
|
189
|
+
rt.record_text("今天买了一件衣服花了105块")
|
|
190
|
+
rt.record_text("明天买一袋大米花了45")
|
|
191
|
+
|
|
192
|
+
led = rt.ledger()
|
|
193
|
+
assert led["expense"] == 105.0
|
|
194
|
+
assert led["planned"]["count"] == 1
|
|
195
|
+
assert led["planned"]["expense"] == 45.0
|
|
196
|
+
assert rt.analyze()["transaction_count"] == 1
|
|
197
|
+
|
|
198
|
+
|
|
199
|
+
def test_多币种折成本位币(tmp_path):
|
|
200
|
+
rt = _rt(tmp_path)
|
|
201
|
+
rt.record_text("花了100美元")
|
|
202
|
+
led = rt.ledger()
|
|
203
|
+
assert led["expense"] == 720.0
|
|
204
|
+
usd = [c for c in led["by_currency"] if c["currency"] == "USD"][0]
|
|
205
|
+
assert usd["original_total"] == -100.0 and usd["base_total"] == -720.0
|
|
206
|
+
assert rt.transactions[0].amount_original == -100.0
|
|
207
|
+
assert rt.transactions[0].fx_rate == 7.2
|
|
208
|
+
|
|
209
|
+
|
|
210
|
+
def test_汇率可改(tmp_path):
|
|
211
|
+
rt = _rt(tmp_path)
|
|
212
|
+
rt.fx.set_rate("USD", 7.0)
|
|
213
|
+
rt.record_text("花了100美金")
|
|
214
|
+
assert rt.ledger()["expense"] == 700.0
|
|
215
|
+
assert _rt(tmp_path).fx.rate("USD") == 7.0
|
|
216
|
+
|
|
217
|
+
|
|
218
|
+
def test_按天与分类汇总(tmp_path):
|
|
219
|
+
rt = _rt(tmp_path)
|
|
220
|
+
rt.record_text("今天买了一件衣服花了105块")
|
|
221
|
+
rt.record_text("今天在星巴克花了32块钱")
|
|
222
|
+
led = rt.ledger()
|
|
223
|
+
assert led["count"] == 2
|
|
224
|
+
day = led["by_day"][0]
|
|
225
|
+
assert day["count"] == 2 and day["expense"] == -137.0
|
|
226
|
+
cats = {c["category"]: c["total"] for c in led["by_category"]}
|
|
227
|
+
assert cats["clothing"] == 105.0 and cats["food"] == 32.0
|
|
228
|
+
|
|
229
|
+
|
|
230
|
+
def test_记账后账本可持久化(tmp_path):
|
|
231
|
+
rt = _rt(tmp_path)
|
|
232
|
+
rt.record_text("今天买了一件衣服花了105块")
|
|
233
|
+
rt.record_text("上个月5号买了双鞋300")
|
|
234
|
+
again = _rt(tmp_path)
|
|
235
|
+
assert len(again.transactions) == 2
|
|
236
|
+
assert again.ledger("2026-10")["expense"] == 300.0
|
|
237
|
+
assert again.ledger("2026-11")["expense"] == 105.0
|
|
238
|
+
|
|
239
|
+
|
|
240
|
+
def test_未配置汇率按1比1并提示(tmp_path):
|
|
241
|
+
rt = _rt(tmp_path)
|
|
242
|
+
rt.fx.rates.pop("JPY", None)
|
|
243
|
+
rec = rt.record_text("花了100日元")
|
|
244
|
+
assert any("汇率" in w for w in rec["recorded"][0]["warnings"])
|
|
245
|
+
assert rt.ledger()["expense"] == 100.0
|
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
"""财务内核持久化 — 重启不丢账、坏文件不阻断启动、批量导入不逐笔写盘。
|
|
2
|
+
|
|
3
|
+
背景:交易/预算/余额原先只在内存,后端一重启就清零,财务分析永远是"0 笔交易"。
|
|
4
|
+
这里固定住三件事:
|
|
5
|
+
1. 写进去的状态能被**新实例**读回来(真正的重启等价);
|
|
6
|
+
2. 文件损坏 / 单条记录坏 / 落盘失败时,一律降级并把错误暴露出来,不抛、不阻断启动;
|
|
7
|
+
3. 批量导入只做一次磁盘 IO。
|
|
8
|
+
"""
|
|
9
|
+
import json
|
|
10
|
+
import sys
|
|
11
|
+
from pathlib import Path
|
|
12
|
+
|
|
13
|
+
sys.path.insert(0, str(Path(__file__).parent.parent))
|
|
14
|
+
|
|
15
|
+
from foxclaw.finance import FinanceRuntime, Transaction
|
|
16
|
+
|
|
17
|
+
CSV = str(Path(__file__).parent.parent / "examples" / "sample_transactions.csv")
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def _rt(tmp_path, data_name="finance_data.json"):
|
|
21
|
+
"""临时目录里的 runtime:policy 与数据都写在 tmp_path,不污染工作区。"""
|
|
22
|
+
return FinanceRuntime(policy_path=str(tmp_path / "finance_policy.yaml"),
|
|
23
|
+
data_path=str(tmp_path / data_name))
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def test_落盘点默认与policy同目录(tmp_path):
|
|
27
|
+
rt = FinanceRuntime(policy_path=str(tmp_path / "finance_policy.yaml"))
|
|
28
|
+
assert rt._data_path == tmp_path / "finance_data.json"
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def test_交易预算余额重启后可恢复(tmp_path):
|
|
32
|
+
rt = _rt(tmp_path)
|
|
33
|
+
rt.add_transaction(Transaction(amount=-88.5, category="food",
|
|
34
|
+
entity="星巴克", account="debit",
|
|
35
|
+
tags=["recurring"], note="早餐"))
|
|
36
|
+
rt.add_transaction(Transaction(amount=12000, category="salary",
|
|
37
|
+
entity="公司", account="debit"))
|
|
38
|
+
rt.set_budget("food", 400)
|
|
39
|
+
rt.set_balance("debit", 11800)
|
|
40
|
+
|
|
41
|
+
assert rt.save() is True
|
|
42
|
+
assert (tmp_path / "finance_data.json").exists()
|
|
43
|
+
|
|
44
|
+
rt2 = _rt(tmp_path)
|
|
45
|
+
assert len(rt2.transactions) == 2
|
|
46
|
+
assert rt2.transactions[0].amount == -88.5
|
|
47
|
+
assert rt2.transactions[0].entity == "星巴克"
|
|
48
|
+
assert rt2.transactions[0].note == "早餐"
|
|
49
|
+
assert rt2.transactions[0].tags == ["recurring"]
|
|
50
|
+
assert rt2.transactions[1].amount == 12000
|
|
51
|
+
assert len(rt2.budgets) == 1 and rt2.budgets[0].monthly_limit == 400
|
|
52
|
+
assert rt2.account_balances == {"debit": 11800}
|
|
53
|
+
assert rt2.entities["星巴克"].txn_count == 1
|
|
54
|
+
assert rt2.entities["星巴克"].entity_type == "subscription"
|
|
55
|
+
assert rt2.entities["公司"].total_amount == 12000
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def test_写入即落盘下次实例自动加载(tmp_path):
|
|
59
|
+
rt = _rt(tmp_path)
|
|
60
|
+
rt.add_transaction(Transaction(amount=-20, category="transport",
|
|
61
|
+
entity="地铁", account="debit"))
|
|
62
|
+
rt.set_balance("debit", 100)
|
|
63
|
+
|
|
64
|
+
loaded = _rt(tmp_path)
|
|
65
|
+
assert loaded.load()
|
|
66
|
+
assert len(loaded.transactions) == 1
|
|
67
|
+
assert loaded.status()["persist"]["load_error"] == ""
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def test_文件格式合法且带版本(tmp_path):
|
|
71
|
+
rt = _rt(tmp_path)
|
|
72
|
+
rt.add_transaction(Transaction(amount=-1, category="misc",
|
|
73
|
+
entity="x", account="debit"))
|
|
74
|
+
data = json.loads((tmp_path / "finance_data.json").read_text(encoding="utf-8"))
|
|
75
|
+
assert data["version"] == 1
|
|
76
|
+
assert data["saved_at"]
|
|
77
|
+
assert len(data["transactions"]) == 1
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def test_损坏文件不阻断启动且可自愈(tmp_path):
|
|
81
|
+
p = tmp_path / "finance_data.json"
|
|
82
|
+
p.write_text("{ 这不是 json ", encoding="utf-8")
|
|
83
|
+
|
|
84
|
+
rt = _rt(tmp_path)
|
|
85
|
+
assert rt.transactions == [] and rt.budgets == []
|
|
86
|
+
assert rt.load_error
|
|
87
|
+
|
|
88
|
+
rt.add_transaction(Transaction(amount=-5, category="misc",
|
|
89
|
+
entity="y", account="debit"))
|
|
90
|
+
assert rt.save() is True
|
|
91
|
+
assert rt.load_error == ""
|
|
92
|
+
assert len(_rt(tmp_path).transactions) == 1
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
def test_顶层非对象也降级(tmp_path):
|
|
96
|
+
(tmp_path / "finance_data.json").write_text("[1, 2, 3]", encoding="utf-8")
|
|
97
|
+
rt = _rt(tmp_path)
|
|
98
|
+
assert rt.transactions == []
|
|
99
|
+
assert "JSON 对象" in rt.load_error
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
def test_单条坏记录只跳过自己不废掉整份(tmp_path):
|
|
103
|
+
(tmp_path / "finance_data.json").write_text(json.dumps({
|
|
104
|
+
"version": 1,
|
|
105
|
+
"transactions": [
|
|
106
|
+
{"amount": -10, "category": "food", "entity": "a", "account": "debit"},
|
|
107
|
+
{"category": "food"},
|
|
108
|
+
"not-a-dict",
|
|
109
|
+
{"amount": -20, "category": "food", "entity": "b", "account": "debit"},
|
|
110
|
+
],
|
|
111
|
+
"budgets": [{"category": "food", "monthly_limit": 300}],
|
|
112
|
+
"account_balances": {"debit": "not-a-number", "cash": 50},
|
|
113
|
+
}, ensure_ascii=False), encoding="utf-8")
|
|
114
|
+
|
|
115
|
+
rt = _rt(tmp_path)
|
|
116
|
+
assert [t.entity for t in rt.transactions] == ["a", "b"]
|
|
117
|
+
assert len(rt.budgets) == 1
|
|
118
|
+
assert rt.account_balances == {"cash": 50.0}
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
def test_实体段缺失时按交易重建(tmp_path):
|
|
122
|
+
(tmp_path / "finance_data.json").write_text(json.dumps({
|
|
123
|
+
"transactions": [
|
|
124
|
+
{"amount": -30, "category": "subscription", "entity": "Netflix",
|
|
125
|
+
"account": "card", "tags": ["recurring"]},
|
|
126
|
+
{"amount": -10, "category": "food", "entity": "Netflix", "account": "card"},
|
|
127
|
+
],
|
|
128
|
+
}, ensure_ascii=False), encoding="utf-8")
|
|
129
|
+
|
|
130
|
+
rt = _rt(tmp_path)
|
|
131
|
+
assert rt.entities["Netflix"].txn_count == 2
|
|
132
|
+
assert rt.entities["Netflix"].total_amount == -40
|
|
133
|
+
assert rt.entities["Netflix"].entity_type == "subscription"
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
def test_批量导入只写一次盘(tmp_path, monkeypatch):
|
|
137
|
+
rt = _rt(tmp_path)
|
|
138
|
+
calls = []
|
|
139
|
+
orig = rt.save
|
|
140
|
+
|
|
141
|
+
def _counting_save():
|
|
142
|
+
calls.append(1)
|
|
143
|
+
return orig()
|
|
144
|
+
|
|
145
|
+
monkeypatch.setattr(rt, "save", _counting_save)
|
|
146
|
+
n = rt.import_csv(CSV)
|
|
147
|
+
assert n > 0
|
|
148
|
+
assert len(calls) == 1, f"导入 {n} 笔应只落盘 1 次,实为 {len(calls)} 次"
|
|
149
|
+
assert len(_rt(tmp_path).transactions) == n
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
def test_落盘失败只回报不抛异常(tmp_path):
|
|
153
|
+
blocker = tmp_path / "blocker"
|
|
154
|
+
blocker.write_text("我是个文件,不是目录", encoding="utf-8")
|
|
155
|
+
rt = FinanceRuntime(policy_path=str(tmp_path / "finance_policy.yaml"),
|
|
156
|
+
data_path=str(blocker / "finance_data.json"))
|
|
157
|
+
assert rt.save() is False
|
|
158
|
+
assert rt.save_error
|
|
159
|
+
rt.add_transaction(Transaction(amount=-1, category="misc",
|
|
160
|
+
entity="z", account="debit"))
|
|
161
|
+
assert len(rt.transactions) == 1
|
|
162
|
+
assert rt.status()["persist"]["save_error"]
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
def test_分析使用持久化后的数据(tmp_path):
|
|
166
|
+
rt = _rt(tmp_path)
|
|
167
|
+
rt.set_budget("food", 100)
|
|
168
|
+
rt.add_transaction(Transaction(amount=-150, category="food",
|
|
169
|
+
entity="餐厅", account="debit"))
|
|
170
|
+
|
|
171
|
+
report = _rt(tmp_path).analyze(period="2026-11")
|
|
172
|
+
assert report["transaction_count"] == 1
|
|
173
|
+
food = [b for b in report["budget_status"] if b["category"] == "food"]
|
|
174
|
+
assert food and food[0]["is_over"] is True
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
"""统一 API 网关:per-client 令牌桶 + 配额 + 空闲淘汰 + 429/Retry-After + 启用哲学。
|
|
2
|
+
全离线(TestClient)。网关默认关,本测试显式 FOXCLAW_GATEWAY=1 + 低额度触发限流。"""
|
|
3
|
+
import os, sys, tempfile
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
sys.path.insert(0, str(Path(__file__).parent.parent))
|
|
6
|
+
import pytest
|
|
7
|
+
|
|
8
|
+
def test_gateway():
|
|
9
|
+
if "foxclaw.api" in sys.modules:
|
|
10
|
+
pytest.skip("需独立进程运行(模块级单例已固化),请用 tests/run_all.py")
|
|
11
|
+
os.environ["FOXCLAW_SKIP_DOTENV"] = "1"
|
|
12
|
+
os.environ["FOXCLAW_GATEWAY"] = "1"
|
|
13
|
+
os.environ["FOXCLAW_GATEWAY_RATE"] = "5"
|
|
14
|
+
os.environ["FOXCLAW_GATEWAY_QUOTA"] = "1000"
|
|
15
|
+
os.environ["FOXCLAW_MAX_BODY_MB"] = "1"
|
|
16
|
+
os.environ.pop("FOXCLAW_API_KEY", None)
|
|
17
|
+
os.environ["HKC_EMBEDDING"] = "stub"
|
|
18
|
+
os.environ["FOXCLAW_SHADOW_ROUNDS"] = "6"
|
|
19
|
+
os.environ["FOXCLAW_DATA_DIR"] = tempfile.mkdtemp(prefix="foxclaw_gw_")
|
|
20
|
+
|
|
21
|
+
print("=" * 60); print("统一 API 网关验证"); print("=" * 60)
|
|
22
|
+
|
|
23
|
+
from foxclaw.core.gateway import Gateway
|
|
24
|
+
|
|
25
|
+
g = Gateway(rate_per_min=3, quota_per_hour=100000)
|
|
26
|
+
allowed = [g.check("a")[0] for _ in range(5)]
|
|
27
|
+
assert allowed[:3] == [True, True, True] and allowed[3] is False, allowed
|
|
28
|
+
ok, retry, reason = g.check("a")
|
|
29
|
+
assert not ok and reason == "rate" and retry >= 1, (ok, retry, reason)
|
|
30
|
+
print("[1] 令牌桶突发限流(3 放行→限流,带 retry)OK")
|
|
31
|
+
|
|
32
|
+
assert g.check("b")[0] is True, "客户端之间不隔离"
|
|
33
|
+
print("[2] per-client 隔离 OK")
|
|
34
|
+
|
|
35
|
+
g2 = Gateway(rate_per_min=100000, quota_per_hour=4)
|
|
36
|
+
cnt = sum(1 for _ in range(6) if g2.check("z")[0])
|
|
37
|
+
assert cnt == 4, cnt
|
|
38
|
+
ok, retry, reason = g2.check("z")
|
|
39
|
+
assert not ok and reason == "quota" and retry >= 1, (ok, retry, reason)
|
|
40
|
+
print("[3] 每小时配额封顶(reason=quota)OK")
|
|
41
|
+
|
|
42
|
+
g3 = Gateway(rate_per_min=100, quota_per_hour=100000, max_clients=64)
|
|
43
|
+
for i in range(500):
|
|
44
|
+
g3.check(f"c{i}")
|
|
45
|
+
assert g3.stats()["tracked_clients"] <= 64, g3.stats()
|
|
46
|
+
print(f"[4] 空闲淘汰(500 client → 追踪 {g3.stats()['tracked_clients']}≤64,防内存 DoS)OK")
|
|
47
|
+
|
|
48
|
+
from fastapi.testclient import TestClient
|
|
49
|
+
from foxclaw.api import app, _gateway_on
|
|
50
|
+
|
|
51
|
+
with TestClient(app) as c:
|
|
52
|
+
s = c.get("/api/gateway/stats").json()
|
|
53
|
+
assert s["enabled"] is True and s["rate_per_min"] == 5, s
|
|
54
|
+
print(f"[5] /api/gateway/stats(enabled={s['enabled']}, rate={s['rate_per_min']}/min)OK")
|
|
55
|
+
|
|
56
|
+
codes = [c.get("/api/status").status_code for _ in range(12)]
|
|
57
|
+
assert 200 in codes and 429 in codes, codes
|
|
58
|
+
r429 = None
|
|
59
|
+
for _ in range(15):
|
|
60
|
+
r = c.get("/api/status")
|
|
61
|
+
if r.status_code == 429:
|
|
62
|
+
r429 = r; break
|
|
63
|
+
assert r429 is not None, "未触发 429"
|
|
64
|
+
assert r429.headers.get("Retry-After") and r429.json().get("retry_after", 0) >= 1, (r429.headers, r429.text)
|
|
65
|
+
print("[6] 突发超限 → 429 + Retry-After 头 OK")
|
|
66
|
+
|
|
67
|
+
assert c.get("/health").status_code == 200, "探针被网关误限"
|
|
68
|
+
print("[7] 探针 /health 豁免网关 OK")
|
|
69
|
+
|
|
70
|
+
big = c.post("/api/knowledge/ingest", json={"text": "x" * (1200 * 1024)})
|
|
71
|
+
assert big.status_code == 413, (big.status_code, big.text[:120])
|
|
72
|
+
print("[8] 超大请求体 → 413(读 body 前拦截)OK")
|
|
73
|
+
|
|
74
|
+
import asyncio
|
|
75
|
+
from starlette.requests import Request as _Req
|
|
76
|
+
from foxclaw.api.middleware import (
|
|
77
|
+
_gateway_mw, _PayloadTooLarge, _wrap_body_limit)
|
|
78
|
+
|
|
79
|
+
pending = [b"x" * 10, b"y" * 10, b"z" * 10]
|
|
80
|
+
|
|
81
|
+
async def _recv():
|
|
82
|
+
if pending:
|
|
83
|
+
return {"type": "http.request", "body": pending.pop(0),
|
|
84
|
+
"more_body": bool(pending)}
|
|
85
|
+
return {"type": "http.disconnect"}
|
|
86
|
+
|
|
87
|
+
req = _Req({"type": "http", "method": "POST", "path": "/api/x",
|
|
88
|
+
"scheme": "http", "server": ("test", 80),
|
|
89
|
+
"headers": [], "query_string": b""}, _recv)
|
|
90
|
+
_wrap_body_limit(req, 15)
|
|
91
|
+
|
|
92
|
+
async def _drain():
|
|
93
|
+
for _ in range(3):
|
|
94
|
+
await req.receive()
|
|
95
|
+
|
|
96
|
+
try:
|
|
97
|
+
asyncio.run(_drain())
|
|
98
|
+
except _PayloadTooLarge:
|
|
99
|
+
pass
|
|
100
|
+
else:
|
|
101
|
+
raise AssertionError("chunked 超限未被流上计数拦住")
|
|
102
|
+
|
|
103
|
+
async def _boom(_req):
|
|
104
|
+
raise _PayloadTooLarge(999)
|
|
105
|
+
|
|
106
|
+
req2 = _Req({"type": "http", "method": "POST", "path": "/api/x",
|
|
107
|
+
"scheme": "http", "server": ("test", 80),
|
|
108
|
+
"headers": [], "query_string": b""})
|
|
109
|
+
resp = asyncio.run(_gateway_mw(req2, _boom))
|
|
110
|
+
assert resp.status_code == 413, resp.status_code
|
|
111
|
+
print("[8b] chunked 超限 → 流上计数拦截并转 413 OK")
|
|
112
|
+
|
|
113
|
+
os.environ.pop("FOXCLAW_GATEWAY", None); os.environ.pop("FOXCLAW_API_KEY", None)
|
|
114
|
+
assert _gateway_on() is False, "本地默认应关"
|
|
115
|
+
os.environ["FOXCLAW_API_KEY"] = "x"
|
|
116
|
+
assert _gateway_on() is True, "配鉴权应自动开"
|
|
117
|
+
os.environ["FOXCLAW_GATEWAY"] = "0"
|
|
118
|
+
assert _gateway_on() is False, "FOXCLAW_GATEWAY=0 应强制关"
|
|
119
|
+
os.environ.pop("FOXCLAW_GATEWAY", None); os.environ.pop("FOXCLAW_API_KEY", None)
|
|
120
|
+
print("[9] 启用哲学(默认关 / 配 key 自动开 / =0 强制关)OK")
|
|
121
|
+
|
|
122
|
+
print("\n统一 API 网关验证全部通过")
|
|
123
|
+
|
|
124
|
+
if __name__ == "__main__":
|
|
125
|
+
test_gateway()
|