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,417 @@
|
|
|
1
|
+
"""ToolExecMixin:LLM 自主工具选择的分发执行 + 工具调用循环。
|
|
2
|
+
|
|
3
|
+
从 `foxclaw/agents/base.py` 搬移出来的工具执行相关方法。
|
|
4
|
+
|
|
5
|
+
实现要点:
|
|
6
|
+
- 闸门/执行拆成**模块级函数**(`_guard_tool(agent, ...)`),而不是自调用 `self.xxx`。
|
|
7
|
+
原因:`_dispatch_tool` 一直被单测以 `CognitiveAgent._dispatch_tool(fake_agent, ...)` 的
|
|
8
|
+
方式用 SimpleNamespace 当 self 调用(鸭子类型),自调用新方法会 AttributeError。
|
|
9
|
+
- 每个工具调用跑在守护线程里,带单工具硬超时;只读且声明 parallel_safe 的工具同轮并发。
|
|
10
|
+
- 会话污点在 worker 线程里只收集、回主线程合并(contextvar 按线程隔离,直接 set 会丢)。
|
|
11
|
+
"""
|
|
12
|
+
import threading
|
|
13
|
+
import time
|
|
14
|
+
|
|
15
|
+
from ._context import _taint_get, _taint_raise, _untrusted_get
|
|
16
|
+
from ...core import settings
|
|
17
|
+
from ...core.injection_guard import redact_obj, redact_secrets
|
|
18
|
+
from typing import Any
|
|
19
|
+
|
|
20
|
+
MAX_PARALLEL = 4
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def _tool_timeout() -> float:
|
|
24
|
+
"""单工具硬超时(秒),FOXCLAW_TOOL_TIMEOUT 可调;默认 45s。
|
|
25
|
+
|
|
26
|
+
值写错不拖垮工具执行(退默认),但会 WARN 一次指出是哪个变量错了 ——
|
|
27
|
+
此前是 try/except 静默退默认,用户的设置有错也永远不知道。
|
|
28
|
+
"""
|
|
29
|
+
v = float(settings.get_safe("FOXCLAW_TOOL_TIMEOUT"))
|
|
30
|
+
return v if v > 0 else 45.0
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
class _Spawn:
|
|
34
|
+
"""把一次工具调用扔进守护线程:超时后工具轮照常继续,不被一个慢工具整轮拖死。
|
|
35
|
+
|
|
36
|
+
用 daemon 线程而非 ThreadPoolExecutor:被放弃的慢调用不会在解释器退出时阻塞进程
|
|
37
|
+
(线程杀不掉,但至少不挡收尾)。
|
|
38
|
+
"""
|
|
39
|
+
|
|
40
|
+
__slots__ = ("done", "box", "thread")
|
|
41
|
+
|
|
42
|
+
def __init__(self, fn):
|
|
43
|
+
self.done = threading.Event()
|
|
44
|
+
self.box: dict = {}
|
|
45
|
+
|
|
46
|
+
def _run():
|
|
47
|
+
try:
|
|
48
|
+
self.box["v"] = fn()
|
|
49
|
+
except BaseException as e:
|
|
50
|
+
self.box["e"] = e
|
|
51
|
+
finally:
|
|
52
|
+
self.done.set()
|
|
53
|
+
|
|
54
|
+
self.thread = threading.Thread(target=_run, daemon=True, name="sf-tool")
|
|
55
|
+
self.thread.start()
|
|
56
|
+
|
|
57
|
+
def wait(self, timeout: float):
|
|
58
|
+
"""返回 (是否按时完成, 结果);超时 (False, None),异常在主线程重抛。"""
|
|
59
|
+
if not self.done.wait(timeout):
|
|
60
|
+
return False, None
|
|
61
|
+
if "e" in self.box:
|
|
62
|
+
raise self.box["e"]
|
|
63
|
+
return True, self.box.get("v")
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def _guard_tool(agent, name: str, args: dict):
|
|
67
|
+
"""跑权限管线,返回 (提前结束文本 | None, 能力标签 dict)。
|
|
68
|
+
|
|
69
|
+
动作级安全阀(真正的兜底,不指望 prompt 隔离挡住弱模型):
|
|
70
|
+
① per-tool deny 闸(kill-switch);
|
|
71
|
+
② 未知工具 → 不执行;
|
|
72
|
+
③ **四阶段权限管线**(permission.evaluate):按能力分级裁决 allow/ask/deny——
|
|
73
|
+
只读放行;写/发送/不可逆/外部副作用/未声明的新工具 → **人在回路(HITL)**,只出草稿不自动执行;
|
|
74
|
+
④ web_search 的个人数据否决留到执行阶段(需要 query)。
|
|
75
|
+
纯函数式、无副作用,因此并发调度前可在主线程一次性裁决完。
|
|
76
|
+
"""
|
|
77
|
+
import types as _types
|
|
78
|
+
from ...core import tools as _tools
|
|
79
|
+
from ...core import injection_guard as _ig
|
|
80
|
+
from ...skills import permission as _perm
|
|
81
|
+
if _tools.is_tool_denied(name):
|
|
82
|
+
return f"(工具 {name} 已被策略禁用,未执行)", {}
|
|
83
|
+
_reg = _tools.default_registry()
|
|
84
|
+
if not _reg.has(name):
|
|
85
|
+
return f"(未知工具 {name},未执行)", {}
|
|
86
|
+
_errs = _reg.validate_args(name, args or {})
|
|
87
|
+
if _errs:
|
|
88
|
+
agent._think(content=f"工具 {name} 入参校验失败: {'; '.join(_errs)[:120]}",
|
|
89
|
+
node_type="tool_intent", conf=0.9)
|
|
90
|
+
return (f"(工具 {name} 入参不合法: {'; '.join(_errs)}。"
|
|
91
|
+
f"请按工具声明修正参数后重试。)"), {}
|
|
92
|
+
cap = _tools.tool_capability(name)
|
|
93
|
+
_schema = _reg.param_schema(name)
|
|
94
|
+
_req = set(_schema.get("required") or [])
|
|
95
|
+
_params = {k: {"required": k in _req, "type": v.get("type")}
|
|
96
|
+
for k, v in (_schema.get("properties") or {}).items()
|
|
97
|
+
if isinstance(v, dict)}
|
|
98
|
+
spec = _types.SimpleNamespace(id=f"tool:{name}", params=_params, **cap)
|
|
99
|
+
_taint = _taint_get(agent)
|
|
100
|
+
if _untrusted_get(agent):
|
|
101
|
+
_taint = max(_taint, _ig.TAINT_PERSONAL)
|
|
102
|
+
decision = _perm.evaluate(spec, args or {}, confirmed=False,
|
|
103
|
+
taint_level=_taint,
|
|
104
|
+
taint_threshold=_ig.TAINT_PERSONAL)
|
|
105
|
+
if decision.decision == "deny":
|
|
106
|
+
return f"(工具 {name} 被权限策略拒绝:{decision.reason},未执行)", cap
|
|
107
|
+
if decision.decision == "ask":
|
|
108
|
+
agent._think(content=f"工具 {name} 需用户确认({decision.reason}),已暂停执行",
|
|
109
|
+
node_type="decision", conf=0.9)
|
|
110
|
+
return (f"(工具 {name} 属于需确认的动作:{decision.reason}。按安全策略我不会自动执行——"
|
|
111
|
+
f"这类涉及发送/转账/删除/修改/对外调用的红线动作只能由你亲自确认后进行;"
|
|
112
|
+
f"我可以先把要做的操作拟成草稿供你审。"), cap
|
|
113
|
+
return None, cap
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
def _invoke_tool(agent, name: str, args: dict, taint_sink: list | None = None) -> str:
|
|
117
|
+
"""执行工具本体,返回回注模型的文本。
|
|
118
|
+
|
|
119
|
+
taint_sink 非空 = 当前在 worker 线程:会话污点在 contextvar 里按线程隔离,
|
|
120
|
+
直接 `_taint_raise` 会写进线程自己的副本然后丢失 → 先收集,交调用方在主线程合并。
|
|
121
|
+
"""
|
|
122
|
+
from ...core import injection_guard as _ig
|
|
123
|
+
|
|
124
|
+
def _raise(level: int) -> None:
|
|
125
|
+
if taint_sink is not None:
|
|
126
|
+
taint_sink.append(level)
|
|
127
|
+
else:
|
|
128
|
+
_taint_raise(agent, level)
|
|
129
|
+
|
|
130
|
+
try:
|
|
131
|
+
if name == "web_search":
|
|
132
|
+
from ...core import web_search as _ws
|
|
133
|
+
q = str((args or {}).get("query", "")).strip()
|
|
134
|
+
if not q:
|
|
135
|
+
return "(web_search 缺少 query 参数)"
|
|
136
|
+
if _ws.is_personal_query(q):
|
|
137
|
+
return "(该问题属于用户本地数据,已跳过联网;请依据本地领域信息回答)"
|
|
138
|
+
payload = _ws.search_answer(q)
|
|
139
|
+
if not payload or (not payload.get("answer") and not payload.get("references")):
|
|
140
|
+
return ("联网搜索未返回结果(可能未配置搜索服务或网络不可用);"
|
|
141
|
+
"请如实告知用户暂时无法查询实时信息,切勿编造版本号/新闻/价格等。")
|
|
142
|
+
agent._think(content=f"工具 web_search「{q}」命中", node_type="tool_use", conf=0.9)
|
|
143
|
+
_raise(_ig.taint_level("web_search"))
|
|
144
|
+
return _ws.format_for_prompt(q, payload)
|
|
145
|
+
if name == "recall_memory":
|
|
146
|
+
q = str((args or {}).get("query", "")).strip()
|
|
147
|
+
try:
|
|
148
|
+
top_k = max(1, min(20, int((args or {}).get("top_k", 5))))
|
|
149
|
+
except Exception:
|
|
150
|
+
top_k = 5
|
|
151
|
+
res = agent._recall_memory(q, top_k=top_k) if q else []
|
|
152
|
+
if not res:
|
|
153
|
+
return "(未召回到相关记忆)"
|
|
154
|
+
lines = "\n".join(f"- {r.get('content', '')}" for r in res[:top_k])
|
|
155
|
+
_raise(_ig.taint_level("memory"))
|
|
156
|
+
return "【召回的相关记忆】\n" + _ig.wrap_untrusted(lines, source="memory")
|
|
157
|
+
except Exception as e:
|
|
158
|
+
return f"(工具 {name} 执行异常:{type(e).__name__})"
|
|
159
|
+
return f"(未知工具 {name},未执行)"
|
|
160
|
+
|
|
161
|
+
|
|
162
|
+
def _timeout_text(name: str) -> str:
|
|
163
|
+
return (f"(工具 {name} 超过 {_tool_timeout():.0f}s 未返回,已跳过本次调用。"
|
|
164
|
+
f"请如实告知用户这一步没完成,不要编造结果。)")
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
def _merge_taint(agent, sink: list) -> None:
|
|
168
|
+
for lvl in sink:
|
|
169
|
+
try:
|
|
170
|
+
_taint_raise(agent, lvl)
|
|
171
|
+
except Exception:
|
|
172
|
+
pass
|
|
173
|
+
|
|
174
|
+
|
|
175
|
+
def _exec_guarded(agent, name: str, args: dict) -> str:
|
|
176
|
+
"""已过闸门的单个工具:带超时执行 + 主线程合并污点。"""
|
|
177
|
+
sink: list = []
|
|
178
|
+
sp = _Spawn(lambda: _invoke_tool(agent, name, args, sink))
|
|
179
|
+
try:
|
|
180
|
+
ok, out = sp.wait(_tool_timeout())
|
|
181
|
+
except Exception as e:
|
|
182
|
+
return f"(工具 {name} 执行异常:{type(e).__name__})"
|
|
183
|
+
if not ok:
|
|
184
|
+
return _timeout_text(name)
|
|
185
|
+
_merge_taint(agent, sink)
|
|
186
|
+
return out
|
|
187
|
+
|
|
188
|
+
|
|
189
|
+
def _run_tools_batch(agent, calls: list) -> list:
|
|
190
|
+
"""按能力调度一批 tool_calls,返回与 calls **同序**的结果文本。
|
|
191
|
+
|
|
192
|
+
只读 + 显式声明 parallel_safe + 全部过闸 → 并发(典型:web_search + recall_memory
|
|
193
|
+
同轮发出,不再一个等一个);其余(写/发送/需确认/未声明)一律串行,避免同一批里
|
|
194
|
+
副作用动作互相覆盖 HITL 裁决。
|
|
195
|
+
"""
|
|
196
|
+
plans = []
|
|
197
|
+
for c in calls:
|
|
198
|
+
n = c.get("name", "")
|
|
199
|
+
a = c.get("args") or {}
|
|
200
|
+
early, cap = _guard_tool(agent, n, a)
|
|
201
|
+
plans.append((n, a, early, cap))
|
|
202
|
+
parallel = (len(plans) > 1 and len(plans) <= MAX_PARALLEL
|
|
203
|
+
and all(p[2] is None and p[3].get("parallel_safe") for p in plans))
|
|
204
|
+
if not parallel:
|
|
205
|
+
out = []
|
|
206
|
+
for p in plans:
|
|
207
|
+
if p[2] is not None:
|
|
208
|
+
out.append(p[2])
|
|
209
|
+
continue
|
|
210
|
+
_disp = getattr(agent, "_dispatch_tool", None)
|
|
211
|
+
out.append(_disp(p[0], p[1]) if callable(_disp)
|
|
212
|
+
else _exec_guarded(agent, p[0], p[1]))
|
|
213
|
+
return out
|
|
214
|
+
spawns = []
|
|
215
|
+
for n, a, _early, _cap in plans:
|
|
216
|
+
sink: list = []
|
|
217
|
+
spawns.append((n, _Spawn(lambda n=n, a=a, s=sink: _invoke_tool(agent, n, a, s)), sink))
|
|
218
|
+
started = time.monotonic()
|
|
219
|
+
timeout = _tool_timeout()
|
|
220
|
+
out = []
|
|
221
|
+
for n, sp, sink in spawns:
|
|
222
|
+
left = max(0.0, timeout - (time.monotonic() - started))
|
|
223
|
+
try:
|
|
224
|
+
ok, val = sp.wait(left)
|
|
225
|
+
except Exception as e:
|
|
226
|
+
out.append(f"(工具 {n} 执行异常:{type(e).__name__})")
|
|
227
|
+
continue
|
|
228
|
+
if not ok:
|
|
229
|
+
out.append(_timeout_text(n))
|
|
230
|
+
continue
|
|
231
|
+
_merge_taint(agent, sink)
|
|
232
|
+
out.append(val)
|
|
233
|
+
return out
|
|
234
|
+
|
|
235
|
+
|
|
236
|
+
_STEP_RESULT_MAX = 4000
|
|
237
|
+
|
|
238
|
+
|
|
239
|
+
def _step_key(name: str, args: dict) -> str:
|
|
240
|
+
"""步骤指纹:同名 + 同参(与字典序无关)= 同一步。续跑靠它匹配「这步之前做过」。"""
|
|
241
|
+
import hashlib
|
|
242
|
+
import json
|
|
243
|
+
try:
|
|
244
|
+
payload = json.dumps(args or {}, sort_keys=True, ensure_ascii=False, default=str)
|
|
245
|
+
except Exception:
|
|
246
|
+
payload = repr(args)
|
|
247
|
+
return f"{name}:{hashlib.sha256(payload.encode('utf-8')).hexdigest()[:16]}"
|
|
248
|
+
|
|
249
|
+
|
|
250
|
+
def _step_ok(result: str) -> bool:
|
|
251
|
+
"""这一步是否值得记进 checkpoint(只有「真的做成了」的步才可复用)。
|
|
252
|
+
|
|
253
|
+
工具失败/超时/需确认/未执行的回注文本全部以 "(" 开头(见 _guard_tool/_timeout_text/
|
|
254
|
+
_invoke_tool 的兜底),据此判定;复用它们等于把一次失败当成已完成,绝不能干。
|
|
255
|
+
"""
|
|
256
|
+
r = (result or "").strip()
|
|
257
|
+
return bool(r) and not r.startswith("(")
|
|
258
|
+
|
|
259
|
+
|
|
260
|
+
def _record_step(agent, call: dict, result: str) -> None:
|
|
261
|
+
"""把一个已完成的工具步骤写进 recovery 日志(fail-safe:异常一律吞掉)。
|
|
262
|
+
|
|
263
|
+
落盘前对 args/result 过 `redact_secrets`:recovery 日志会被回放读回,长期留在
|
|
264
|
+
`{data_dir}/recovery/` 下,而工具参数里经常出现 token/密钥(如鉴权类 CLI、带 key 的
|
|
265
|
+
HTTP 请求)。`_step_key` 仍用**未脱敏**的 args 计算,否则同一步骤的 key 前后不一致,
|
|
266
|
+
缓存去重和回放对不上。
|
|
267
|
+
"""
|
|
268
|
+
rec = getattr(agent, "_recovery", None)
|
|
269
|
+
tid = getattr(agent, "_session_id", "") or ""
|
|
270
|
+
if rec is None or not tid:
|
|
271
|
+
return
|
|
272
|
+
try:
|
|
273
|
+
text = redact_secrets(result or "")
|
|
274
|
+
rec.record_step(tid, {
|
|
275
|
+
"kind": "tool",
|
|
276
|
+
"name": call.get("name", ""),
|
|
277
|
+
"key": _step_key(call.get("name", ""), call.get("args") or {}),
|
|
278
|
+
"args": redact_obj(call.get("args") or {}),
|
|
279
|
+
"result": text[:_STEP_RESULT_MAX],
|
|
280
|
+
"truncated": len(result or "") > _STEP_RESULT_MAX,
|
|
281
|
+
})
|
|
282
|
+
except Exception:
|
|
283
|
+
pass
|
|
284
|
+
|
|
285
|
+
|
|
286
|
+
def _apply_failure_policy(agent, call: dict, result: str) -> tuple[str, str]:
|
|
287
|
+
"""按策略表决定「这次失败怎么处理」,返回 (回注文本, 失败类型)。
|
|
288
|
+
|
|
289
|
+
· 只读工具遇到超时/网络/异常 → **自动重试一次**(读操作重复执行无副作用);
|
|
290
|
+
· 写/发送/支付类一律不重试(重试 = 可能重复扣款/重复发消息),交人;
|
|
291
|
+
· 其余(未配置/参数错/红线)只注入动作提示,不重试。
|
|
292
|
+
重试只是把「要不要再来一次」从模型手里拿走,并不隐瞒失败:两次都失败时文本里
|
|
293
|
+
仍写明未完成。
|
|
294
|
+
"""
|
|
295
|
+
from ...core import failure_policy as _policy
|
|
296
|
+
from ...core import tools as _tools
|
|
297
|
+
name = call.get("name", "")
|
|
298
|
+
cap = _tools.tool_capability(name)
|
|
299
|
+
d = _policy.decide(result, tool=name, capability=cap, attempt=1)
|
|
300
|
+
if d.action == _policy.RETRY:
|
|
301
|
+
try:
|
|
302
|
+
retry = _run_tools_batch(agent, [call])[0]
|
|
303
|
+
except Exception:
|
|
304
|
+
retry = result
|
|
305
|
+
d2 = _policy.decide(retry, tool=name, capability=cap, attempt=2)
|
|
306
|
+
return _policy.annotate(retry, d2), d2.kind
|
|
307
|
+
return _policy.annotate(result, d), d.kind
|
|
308
|
+
|
|
309
|
+
|
|
310
|
+
def _settle(agent, call: dict, result: str) -> str:
|
|
311
|
+
"""一个工具步的收尾:核验 → 记账证据 → 只把「真的可用」的步写进 checkpoint。
|
|
312
|
+
|
|
313
|
+
返回回注给模型的最终文本(失败时附动作提示,成功时原样返回)。
|
|
314
|
+
"""
|
|
315
|
+
from ...core import tools as _tools
|
|
316
|
+
from ...core import verify as _verify
|
|
317
|
+
from ._context import _evidence_add
|
|
318
|
+
name = call.get("name", "")
|
|
319
|
+
args = call.get("args") or {}
|
|
320
|
+
text, kind = _apply_failure_policy(agent, call, result)
|
|
321
|
+
try:
|
|
322
|
+
cap = _tools.tool_capability(name)
|
|
323
|
+
v = _verify.verify(name, args, result, cap)
|
|
324
|
+
note = _verify.note_for_model(v, read_only=bool(cap.get("read_only")))
|
|
325
|
+
if note:
|
|
326
|
+
text = f"{text}\n{note}"
|
|
327
|
+
_evidence_add(agent, {
|
|
328
|
+
"tool": name, "kind": kind, "status": v.status,
|
|
329
|
+
"reason": v.reason, "evidence": [e.to_dict() for e in v.evidence],
|
|
330
|
+
})
|
|
331
|
+
if v.status != _verify.FAILED and _step_ok(result):
|
|
332
|
+
_record_step(agent, call, result)
|
|
333
|
+
except Exception:
|
|
334
|
+
pass
|
|
335
|
+
return text
|
|
336
|
+
|
|
337
|
+
|
|
338
|
+
def _run_tool_calls(agent, calls: list) -> list:
|
|
339
|
+
"""执行一批 tool_calls,并按断点续跑语义处理(返回与 calls 同序的结果文本):
|
|
340
|
+
· 命中 `_resume_steps` 里的同指纹步骤 → 复用上次结果,不再执行(断连/崩溃后重跑不重复);
|
|
341
|
+
· 其余照常执行(只读并发 / 写串行,单工具带超时);
|
|
342
|
+
· 每个结果过一遍失败策略表(只读可重试一次)+ 后置核验,证据挂到会话槽。
|
|
343
|
+
"""
|
|
344
|
+
from ._context import _resume_steps
|
|
345
|
+
cache = _resume_steps.get() or {}
|
|
346
|
+
out: list = [None] * len(calls)
|
|
347
|
+
todo: list = []
|
|
348
|
+
for i, c in enumerate(calls):
|
|
349
|
+
hit = cache.get(_step_key(c.get("name", ""), c.get("args") or {}))
|
|
350
|
+
if isinstance(hit, dict) and isinstance(hit.get("result"), str):
|
|
351
|
+
out[i] = (f"(断点续跑:{c.get('name', '')} 这一步此前已完成,"
|
|
352
|
+
f"以下为上次的结果,不必重做)\n{hit['result']}")
|
|
353
|
+
else:
|
|
354
|
+
todo.append(i)
|
|
355
|
+
if todo:
|
|
356
|
+
results = _run_tools_batch(agent, [calls[i] for i in todo])
|
|
357
|
+
for i, r in zip(todo, results):
|
|
358
|
+
out[i] = _settle(agent, calls[i], r)
|
|
359
|
+
return out
|
|
360
|
+
|
|
361
|
+
|
|
362
|
+
class ToolExecMixin:
|
|
363
|
+
AGENT_ID: str
|
|
364
|
+
router: Any
|
|
365
|
+
system_prompt: str
|
|
366
|
+
def _guard_tool(self, name: str, args: dict):
|
|
367
|
+
return _guard_tool(self, name, args)
|
|
368
|
+
|
|
369
|
+
def _invoke_tool(self, name: str, args: dict, taint_sink: list | None = None) -> str:
|
|
370
|
+
return _invoke_tool(self, name, args, taint_sink)
|
|
371
|
+
|
|
372
|
+
def _exec_guarded(self, name: str, args: dict) -> str:
|
|
373
|
+
return _exec_guarded(self, name, args)
|
|
374
|
+
|
|
375
|
+
def _dispatch_tool(self, name: str, args: dict) -> str:
|
|
376
|
+
"""执行一个被 LLM 选中的工具,返回结果文本(注回模型)。"""
|
|
377
|
+
early, _cap = _guard_tool(self, name, args)
|
|
378
|
+
if early is not None:
|
|
379
|
+
return early
|
|
380
|
+
return _exec_guarded(self, name, args)
|
|
381
|
+
|
|
382
|
+
def _run_tools(self, calls: list) -> list:
|
|
383
|
+
return _run_tools_batch(self, calls)
|
|
384
|
+
|
|
385
|
+
def _reply_with_tools(self, task: str, messages: list, stop_event=None, max_rounds: int = 4):
|
|
386
|
+
"""LLM 自主工具调用循环(取代'调 LLM 前用关键词猜要不要联网/召回')。
|
|
387
|
+
返回最终答案文本;若 provider 实际不支持 tools → 返回 None(上层回退关键词增强链)。
|
|
388
|
+
工具轮用非流式解析,最终文本由上层 chunk 输出。"""
|
|
389
|
+
from ...core import tools as _tools
|
|
390
|
+
if not self.router.supports_tools():
|
|
391
|
+
return None
|
|
392
|
+
schemas = _tools.default_registry().schemas()
|
|
393
|
+
if not schemas:
|
|
394
|
+
return None
|
|
395
|
+
conv = list(messages)
|
|
396
|
+
for _ in range(max_rounds):
|
|
397
|
+
_compact = getattr(self, "_compact_context", None)
|
|
398
|
+
if _compact is not None:
|
|
399
|
+
conv = _compact(conv, "tool_loop")
|
|
400
|
+
if stop_event is not None and stop_event.is_set():
|
|
401
|
+
return None
|
|
402
|
+
res = self.router.call_tools(self.system_prompt, conv, schemas,
|
|
403
|
+
agent_id=self.AGENT_ID)
|
|
404
|
+
if not res.get("supported"):
|
|
405
|
+
return None
|
|
406
|
+
calls = res.get("tool_calls") or []
|
|
407
|
+
if not calls:
|
|
408
|
+
return res.get("text") or None
|
|
409
|
+
conv.append(res["assistant_msg"])
|
|
410
|
+
results = _run_tool_calls(self, calls)
|
|
411
|
+
for c, result in zip(calls, results):
|
|
412
|
+
conv.append(self.router.format_tool_result(c, result))
|
|
413
|
+
final = self.router.call_tools(self.system_prompt, conv, schemas,
|
|
414
|
+
tool_choice="none", agent_id=self.AGENT_ID)
|
|
415
|
+
if not final.get("supported"):
|
|
416
|
+
return None
|
|
417
|
+
return final.get("text") or None
|
|
@@ -0,0 +1,224 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Foxclaw API — FastAPI 后端
|
|
3
|
+
前端控制台通过这里获取所有数据
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
import threading
|
|
7
|
+
import os
|
|
8
|
+
import asyncio
|
|
9
|
+
import queue
|
|
10
|
+
from pathlib import Path
|
|
11
|
+
from contextlib import asynccontextmanager
|
|
12
|
+
|
|
13
|
+
try:
|
|
14
|
+
from ..core.envfile import load_env_file
|
|
15
|
+
_env_path = Path(__file__).parent.parent.parent / ".env"
|
|
16
|
+
if os.environ.get("FOXCLAW_SKIP_DOTENV", "").strip() in ("1", "true", "True", "yes"):
|
|
17
|
+
print("[Foxclaw] ○ FOXCLAW_SKIP_DOTENV=1 —— 跳过 .env,仅用进程环境变量")
|
|
18
|
+
elif _env_path.exists():
|
|
19
|
+
_shadows = load_env_file(_env_path)
|
|
20
|
+
print("[Foxclaw] ✓ 已加载配置文件 .env")
|
|
21
|
+
if _shadows:
|
|
22
|
+
print("[Foxclaw] ! .env 覆盖了系统环境变量: " + ", ".join(_shadows))
|
|
23
|
+
except ImportError:
|
|
24
|
+
pass
|
|
25
|
+
|
|
26
|
+
from fastapi import FastAPI
|
|
27
|
+
from typing import Optional
|
|
28
|
+
|
|
29
|
+
from foxclaw import Foxclaw, __version__
|
|
30
|
+
|
|
31
|
+
from .middleware import (
|
|
32
|
+
install_middleware,
|
|
33
|
+
_constant_time_eq as _constant_time_eq,
|
|
34
|
+
_is_guarded_path as _is_guarded_path,
|
|
35
|
+
_gateway_on as _gateway_on,
|
|
36
|
+
_GATEWAY as _GATEWAY,
|
|
37
|
+
_MAX_BODY as _MAX_BODY,
|
|
38
|
+
_api_key_guard as _api_key_guard,
|
|
39
|
+
_gateway_mw as _gateway_mw,
|
|
40
|
+
_trace_mw as _trace_mw,
|
|
41
|
+
_metrics_mw as _metrics_mw,
|
|
42
|
+
)
|
|
43
|
+
|
|
44
|
+
from .routes.ws import manager, harness_websocket as harness_websocket
|
|
45
|
+
|
|
46
|
+
from .spine import _ws_safe_payload, _prewarm_hkc
|
|
47
|
+
from ._state import current_os, set_os_instance
|
|
48
|
+
from ._state import os_instance as os_instance
|
|
49
|
+
|
|
50
|
+
from .bootstrap import _configure_cors, _mount_hkc, _mount_frontend
|
|
51
|
+
|
|
52
|
+
_event_queue: "queue.Queue" = queue.Queue()
|
|
53
|
+
_broadcast_task: Optional[asyncio.Task] = None
|
|
54
|
+
_prewarm_thread: "threading.Thread | None" = None
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def _register_spine_projection(os_instance: Foxclaw):
|
|
58
|
+
"""【P0·统一事件脊柱】注册**单一投影**:EventSpine 上的每一条事件 → 线程安全队列
|
|
59
|
+
→ WS 广播。取代旧的"逐 Agent 逐事件手工转发"(那时只有 harness 4 类事件能到前端,
|
|
60
|
+
truth/process/memory 等系统事件被漏掉)。现在 harness 事件与系统事件同经脊柱,
|
|
61
|
+
一个投影全量观测。payload 经 _ws_safe_payload 净化(JSON 安全 + 有界),避免脏/大
|
|
62
|
+
对象丢连接或泛洪。event/data 形状与旧版一致(前端不受影响,未知事件被忽略)。"""
|
|
63
|
+
_WS_MUTED = ("knowledge.created", "knowledge.updated")
|
|
64
|
+
|
|
65
|
+
def project(evt):
|
|
66
|
+
if evt.topic.startswith("agent.cognition.") or evt.topic in _WS_MUTED:
|
|
67
|
+
return
|
|
68
|
+
try:
|
|
69
|
+
_event_queue.put_nowait(
|
|
70
|
+
{"event": evt.topic, "data": _ws_safe_payload(evt.payload)})
|
|
71
|
+
except Exception:
|
|
72
|
+
pass
|
|
73
|
+
os_instance.event_spine.project(project)
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
@asynccontextmanager
|
|
77
|
+
async def lifespan(app: FastAPI):
|
|
78
|
+
global _broadcast_task
|
|
79
|
+
if not os.environ.get("FOXCLAW_API_KEY", "").strip():
|
|
80
|
+
print("[Foxclaw API] ⚠ 未设置 FOXCLAW_API_KEY —— 当前无鉴权,"
|
|
81
|
+
"仅本地回环安全;对外暴露前务必设置。")
|
|
82
|
+
os_inst = Foxclaw(
|
|
83
|
+
data_dir=os.environ.get("FOXCLAW_DATA_DIR", "./foxclaw_data"),
|
|
84
|
+
shadow_rounds=settings.get_int("FOXCLAW_SHADOW_ROUNDS"),
|
|
85
|
+
)
|
|
86
|
+
set_os_instance(os_inst)
|
|
87
|
+
_register_spine_projection(os_inst)
|
|
88
|
+
try:
|
|
89
|
+
from ..core.tracing import TRACES
|
|
90
|
+
TRACES.attach_db(Path(os_inst.data_dir) / "traces.db")
|
|
91
|
+
except Exception as e:
|
|
92
|
+
print(f"[Foxclaw API] ⚠ trace 持久化未启用: {e}")
|
|
93
|
+
_broadcast_task = asyncio.create_task(_drain_event_queue())
|
|
94
|
+
print("[Foxclaw API] ✓ 事件广播任务已启动")
|
|
95
|
+
_prewarm_hkc(os_inst)
|
|
96
|
+
if _foxclaw_mcp is not None:
|
|
97
|
+
async with _foxclaw_mcp.session_manager.run():
|
|
98
|
+
print("[Foxclaw API] ✓ 内置 MCP 服务器已挂载 /api/mcp/")
|
|
99
|
+
yield
|
|
100
|
+
else:
|
|
101
|
+
yield
|
|
102
|
+
if _broadcast_task:
|
|
103
|
+
_broadcast_task.cancel()
|
|
104
|
+
try:
|
|
105
|
+
await _broadcast_task
|
|
106
|
+
except asyncio.CancelledError:
|
|
107
|
+
pass
|
|
108
|
+
if _prewarm_thread is not None and _prewarm_thread.is_alive():
|
|
109
|
+
_prewarm_thread.join(timeout=120.0)
|
|
110
|
+
live = current_os()
|
|
111
|
+
if live is not None:
|
|
112
|
+
await live.aclose()
|
|
113
|
+
print("[Foxclaw API] 关闭中...")
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
async def _drain_event_queue():
|
|
117
|
+
"""后台 async 循环:从线程安全队列取事件,广播到 WebSocket。"""
|
|
118
|
+
import queue as _queue_mod
|
|
119
|
+
loop = asyncio.get_running_loop()
|
|
120
|
+
while True:
|
|
121
|
+
try:
|
|
122
|
+
try:
|
|
123
|
+
event = await loop.run_in_executor(
|
|
124
|
+
None, lambda: _event_queue.get(timeout=0.5))
|
|
125
|
+
except _queue_mod.Empty:
|
|
126
|
+
continue
|
|
127
|
+
await manager.broadcast(event)
|
|
128
|
+
except asyncio.CancelledError:
|
|
129
|
+
break
|
|
130
|
+
except Exception:
|
|
131
|
+
await asyncio.sleep(0.1)
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
_foxclaw_mcp = None
|
|
135
|
+
_foxclaw_mcp_app = None
|
|
136
|
+
if os.environ.get("FOXCLAW_MCP_SERVER", "0").strip() in ("1", "true", "True"):
|
|
137
|
+
from ..mcp_server import build_foxclaw_mcp
|
|
138
|
+
_mcp_agents = os.environ.get("FOXCLAW_MCP_AGENTS", "0").strip() in ("1", "true", "True")
|
|
139
|
+
_foxclaw_mcp = build_foxclaw_mcp(lambda: current_os(), expose_agents=_mcp_agents)
|
|
140
|
+
_foxclaw_mcp_app = _foxclaw_mcp.streamable_http_app()
|
|
141
|
+
if _mcp_agents:
|
|
142
|
+
print("[Foxclaw API] ⚠ MCP 已暴露 Agent 调度(ask_foxclaw)——外部客户端可驱动你的认知 Agent")
|
|
143
|
+
if not os.environ.get("FOXCLAW_API_KEY", "").strip():
|
|
144
|
+
print("[Foxclaw API] ⚠ 内置 MCP 服务器已开启但未设 FOXCLAW_API_KEY —— "
|
|
145
|
+
"对外暴露前务必设置,否则任何人可读你的知识/记忆")
|
|
146
|
+
if os.environ.get("FOXCLAW_CORS_ORIGINS", "").strip() == "*":
|
|
147
|
+
print("[Foxclaw API] ⚠ MCP 已开启但 CORS=* —— 生产建议用 FOXCLAW_CORS_ORIGINS 收紧允许来源")
|
|
148
|
+
|
|
149
|
+
app: FastAPI = FastAPI(
|
|
150
|
+
title="Foxclaw API",
|
|
151
|
+
version=__version__,
|
|
152
|
+
description="Cognitive Agent Operating System",
|
|
153
|
+
lifespan=lifespan,
|
|
154
|
+
)
|
|
155
|
+
if _foxclaw_mcp_app is not None:
|
|
156
|
+
app.mount("/api/mcp", _foxclaw_mcp_app)
|
|
157
|
+
|
|
158
|
+
_configure_cors(app)
|
|
159
|
+
|
|
160
|
+
|
|
161
|
+
install_middleware(app)
|
|
162
|
+
|
|
163
|
+
from .routes import ws as _ws
|
|
164
|
+
_ws.register(app)
|
|
165
|
+
|
|
166
|
+
from .routes import system # noqa: F401
|
|
167
|
+
|
|
168
|
+
from .routes import ahs # noqa: F401
|
|
169
|
+
|
|
170
|
+
from .routes import memory # noqa: F401
|
|
171
|
+
|
|
172
|
+
from .routes import task # noqa: F401
|
|
173
|
+
|
|
174
|
+
from .routes import domain # noqa: F401
|
|
175
|
+
|
|
176
|
+
from .routes import recovery # noqa: F401
|
|
177
|
+
|
|
178
|
+
from .routes import orchestration # noqa: F401
|
|
179
|
+
|
|
180
|
+
from .routes import browser # noqa: F401
|
|
181
|
+
|
|
182
|
+
from .routes import knowledge # noqa: F401
|
|
183
|
+
|
|
184
|
+
from .routes import review # noqa: F401
|
|
185
|
+
|
|
186
|
+
from .routes import search # noqa: F401
|
|
187
|
+
|
|
188
|
+
from .routes import attachments # noqa: F401
|
|
189
|
+
|
|
190
|
+
from .routes import llm_admin # noqa: F401
|
|
191
|
+
|
|
192
|
+
from .routes.task import _sanitize_images, _sanitize_attachments, _MAX_IMAGES, _MAX_IMG_B64, _STREAM_LIMITER # noqa: F401
|
|
193
|
+
from .routes.domain import _parse_import_rows # noqa: F401
|
|
194
|
+
from .routes.knowledge import knowledge_ability as knowledge_ability, _HKCInsuffCov as _HKCInsuffCov # noqa: F401
|
|
195
|
+
from ..core import settings
|
|
196
|
+
|
|
197
|
+
|
|
198
|
+
_mount_hkc(app)
|
|
199
|
+
|
|
200
|
+
|
|
201
|
+
_mount_frontend(app)
|
|
202
|
+
|
|
203
|
+
|
|
204
|
+
if __name__ == "__main__":
|
|
205
|
+
import uvicorn
|
|
206
|
+
|
|
207
|
+
_host = os.environ.get("FOXCLAW_HOST", "127.0.0.1").strip() or "127.0.0.1"
|
|
208
|
+
_port = settings.get_int("FOXCLAW_PORT")
|
|
209
|
+
_LOOPBACK = {"127.0.0.1", "localhost", "::1", "[::1]"}
|
|
210
|
+
if _host not in _LOOPBACK and not os.environ.get("FOXCLAW_API_KEY", "").strip():
|
|
211
|
+
print(
|
|
212
|
+
f"\n[Foxclaw API] ⚠⚠ 绑定 {_host} 但未设置 FOXCLAW_API_KEY:\n"
|
|
213
|
+
" 该端口上任何可达者都能读写记忆、执行技能、摄入文件。\n"
|
|
214
|
+
" 请设置 FOXCLAW_API_KEY,或改用 FOXCLAW_HOST=127.0.0.1。\n"
|
|
215
|
+
" 确认自担风险,可设 FOXCLAW_ALLOW_INSECURE=1 跳过此检查。\n"
|
|
216
|
+
)
|
|
217
|
+
if os.environ.get("FOXCLAW_ALLOW_INSECURE", "").strip() != "1":
|
|
218
|
+
raise SystemExit("[Foxclaw API] 已拒绝启动:非回环绑定 + 无鉴权。")
|
|
219
|
+
uvicorn.run(
|
|
220
|
+
"foxclaw.api:app",
|
|
221
|
+
host=_host,
|
|
222
|
+
port=_port,
|
|
223
|
+
reload=True,
|
|
224
|
+
)
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
"""API 层共享 OS 实例的状态与访问器。
|
|
2
|
+
|
|
3
|
+
为什么独立成模块
|
|
4
|
+
----------------
|
|
5
|
+
`foxclaw/api/__init__.py` 受 15KB 单文件体积门(tools/size_check.py)约束,装配逻辑
|
|
6
|
+
已贴着上限。把「状态 + 访问器」放这里,既让 13 个路由文件能统一取实例,也不让主装配
|
|
7
|
+
文件继续膨胀。
|
|
8
|
+
|
|
9
|
+
读写契约
|
|
10
|
+
--------
|
|
11
|
+
写入只有 `set_os_instance()`(lifespan 启动时调用一次);读取两个入口:
|
|
12
|
+
|
|
13
|
+
- `os_instance()`:要求实例必须就绪,未就绪抛 503(可重试),用于正常请求路径
|
|
14
|
+
- `current_os()`:允许未就绪返回 None,用于「无 OS 也要降级返回」的只读路径
|
|
15
|
+
"""
|
|
16
|
+
from __future__ import annotations
|
|
17
|
+
|
|
18
|
+
from typing import TYPE_CHECKING, Optional
|
|
19
|
+
|
|
20
|
+
from fastapi import HTTPException
|
|
21
|
+
|
|
22
|
+
if TYPE_CHECKING:
|
|
23
|
+
from foxclaw import Foxclaw
|
|
24
|
+
|
|
25
|
+
_os_instance: Optional["Foxclaw"] = None
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def set_os_instance(inst: "Foxclaw") -> None:
|
|
29
|
+
global _os_instance
|
|
30
|
+
_os_instance = inst
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def current_os() -> Optional["Foxclaw"]:
|
|
34
|
+
return _os_instance
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def os_instance() -> "Foxclaw":
|
|
38
|
+
"""取共享 OS 实例;未就绪时抛 503 而不是让调用方吃到 NoneType 属性错误。"""
|
|
39
|
+
inst = _os_instance
|
|
40
|
+
if inst is None:
|
|
41
|
+
raise HTTPException(
|
|
42
|
+
status_code=503, detail="service is starting up, retry shortly")
|
|
43
|
+
return inst
|