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,263 @@
|
|
|
1
|
+
"""任务执行 / 流式直答 / 流式任务 路由与多模态护栏。
|
|
2
|
+
|
|
3
|
+
从 foxclaw/api.py 原样搬移(纯搬移,零行为变更)。共享 OS 实例经 `_api.current_os()`
|
|
4
|
+
晚绑定读取;路由用 `@app` 装饰器在 import 时注册。流式限流器 `_STREAM_LIMITER` 为
|
|
5
|
+
模块级单例,`__init__.py` 会 re-export(测试复位其桶容量)。
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import json
|
|
9
|
+
import asyncio
|
|
10
|
+
import threading as _threading
|
|
11
|
+
|
|
12
|
+
from fastapi import Request
|
|
13
|
+
from fastapi.responses import StreamingResponse
|
|
14
|
+
from pydantic import BaseModel
|
|
15
|
+
|
|
16
|
+
import foxclaw.api as _api
|
|
17
|
+
from .. import app
|
|
18
|
+
from ...mcp_server import _RateLimiter as _RL
|
|
19
|
+
from ...core import settings
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class TaskRequest(BaseModel):
|
|
23
|
+
task: str
|
|
24
|
+
agent_id: str = "supervisor"
|
|
25
|
+
context: dict = {}
|
|
26
|
+
images: list = []
|
|
27
|
+
history: list = []
|
|
28
|
+
session_id: str = ""
|
|
29
|
+
attachments: list = []
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
class GoalRequest(BaseModel):
|
|
33
|
+
agent_id: str
|
|
34
|
+
goal: str
|
|
35
|
+
allowed_tools: list[str] = []
|
|
36
|
+
entities: list[str] = []
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
@app.post("/api/task")
|
|
40
|
+
async def run_task(req: TaskRequest):
|
|
41
|
+
"""执行任务,返回结果 + ThoughtChain 摘要。
|
|
42
|
+
|
|
43
|
+
把认知处理 offload 到工作线程:每请求的 CPU 密集同步工作(harness 语义嵌入等)不再
|
|
44
|
+
阻塞事件循环,并发请求的嵌入计算可真正并行(torch/ST CPU 前向释放 GIL),吞吐随核数扩展。
|
|
45
|
+
· contextvars 随 to_thread 复制到工作线程 → 每请求会话状态仍隔离(见 base.py);
|
|
46
|
+
· 内层 LLM 的 to_thread 用各自 asyncio.run 的独立事件循环 executor,不与外层竞争
|
|
47
|
+
同一线程池,故无嵌套线程池死锁;
|
|
48
|
+
· harness 累积态已在 harness.py 用 RLock 保护(嵌入在锁外,故并行不受锁影响)。"""
|
|
49
|
+
def _work():
|
|
50
|
+
return _api.os_instance().scheduler.run(
|
|
51
|
+
lambda: asyncio.run(_api.os_instance().run(req.task, req.agent_id,
|
|
52
|
+
context=req.context)),
|
|
53
|
+
agent_id=req.agent_id, task=req.task)
|
|
54
|
+
return await asyncio.to_thread(_work)
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
_STREAM_LIMITER = _RL(settings.get_int("FOXCLAW_STREAM_RATE"))
|
|
58
|
+
_STREAM_SEM = _threading.Semaphore(settings.get_int("FOXCLAW_STREAM_CONCURRENCY"))
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
_MAX_IMAGES = 6
|
|
62
|
+
_MAX_IMG_B64 = 10 * 1024 * 1024
|
|
63
|
+
_MAX_IMG_TOTAL = 20 * 1024 * 1024
|
|
64
|
+
_ALLOWED_IMG_MT = {"image/png", "image/jpeg", "image/webp", "image/gif"}
|
|
65
|
+
|
|
66
|
+
_MAX_ATTACHMENTS = 6
|
|
67
|
+
_ATTACH_TEXT_MAX = 20000
|
|
68
|
+
_ATTACH_TEXT_TOTAL = 60000
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def _sanitize_images(images):
|
|
72
|
+
"""校验/裁剪前端传来的多模态图片:限数量、限单图大小、限累计总量、白名单类型。
|
|
73
|
+
防超大 payload/OOM(前端已缩放,这是对直连/恶意客户端的后端自保)。"""
|
|
74
|
+
out: list[dict] = []
|
|
75
|
+
total = 0
|
|
76
|
+
if not isinstance(images, list):
|
|
77
|
+
return out
|
|
78
|
+
for im in images[:_MAX_IMAGES]:
|
|
79
|
+
if not isinstance(im, dict):
|
|
80
|
+
continue
|
|
81
|
+
mt = str(im.get("media_type", "")).lower()
|
|
82
|
+
d = im.get("data", "")
|
|
83
|
+
if mt not in _ALLOWED_IMG_MT:
|
|
84
|
+
continue
|
|
85
|
+
if not isinstance(d, str) or not d or len(d) > _MAX_IMG_B64:
|
|
86
|
+
continue
|
|
87
|
+
if total + len(d) > _MAX_IMG_TOTAL:
|
|
88
|
+
break
|
|
89
|
+
total += len(d)
|
|
90
|
+
out.append({"media_type": mt, "data": d})
|
|
91
|
+
return out
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
def _sanitize_attachments(attachments):
|
|
95
|
+
"""校验/裁剪前端传来的聊天附件:限数量、字段清洗、正文长度上限 + 累计上限。
|
|
96
|
+
附件是前端经 /api/upload 上传后带回的引用({name,size,mime,url,text});这里只做
|
|
97
|
+
直连/恶意客户端的自保,不信任 text 之外任何字段用于文件系统操作(下载按 url 里的 id 白名单)。"""
|
|
98
|
+
out: list[dict] = []
|
|
99
|
+
total = 0
|
|
100
|
+
if not isinstance(attachments, list):
|
|
101
|
+
return out
|
|
102
|
+
for a in attachments[:_MAX_ATTACHMENTS]:
|
|
103
|
+
if not isinstance(a, dict):
|
|
104
|
+
continue
|
|
105
|
+
name = str(a.get("name") or "")[:200]
|
|
106
|
+
text = str(a.get("text") or "")[:_ATTACH_TEXT_MAX]
|
|
107
|
+
if not name and not text:
|
|
108
|
+
continue
|
|
109
|
+
if total + len(text) > _ATTACH_TEXT_TOTAL:
|
|
110
|
+
text = text[:_ATTACH_TEXT_TOTAL - total]
|
|
111
|
+
total += len(text)
|
|
112
|
+
try:
|
|
113
|
+
raw_size = a.get("size")
|
|
114
|
+
size = int(raw_size) if raw_size else 0
|
|
115
|
+
except (TypeError, ValueError):
|
|
116
|
+
size = 0
|
|
117
|
+
out.append({
|
|
118
|
+
"name": name,
|
|
119
|
+
"size": size,
|
|
120
|
+
"mime": str(a.get("mime") or "")[:100],
|
|
121
|
+
"url": str(a.get("url") or "")[:500],
|
|
122
|
+
"text": text,
|
|
123
|
+
})
|
|
124
|
+
return out
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
@app.post("/api/chat/stream")
|
|
128
|
+
async def chat_stream(req: TaskRequest, request: Request):
|
|
129
|
+
"""流式直答(方案A):SSE 逐字返回 Agent LLM 输出。用于对话/问答;走完整认知管线的任务执行
|
|
130
|
+
用 /api/task(阻塞)或 /api/task/stream(流式,D1);此端点不跑 recall/verify/reflect 等 stage。
|
|
131
|
+
背压:令牌桶限流 + 并发信号量;客户端断连 → stop_event 终止 LLM 流(省 token)。
|
|
132
|
+
images:多模态对话,随问题发给视觉模型(非视觉模型 router 层报错→降级)。"""
|
|
133
|
+
if not _STREAM_LIMITER.allow():
|
|
134
|
+
async def _rl():
|
|
135
|
+
yield "data: " + json.dumps({"error": "rate limited"}) + "\n\n"
|
|
136
|
+
return StreamingResponse(_rl(), media_type="text/event-stream")
|
|
137
|
+
|
|
138
|
+
agent = _api.os_instance()._agents.get(req.agent_id) or _api.os_instance().supervisor
|
|
139
|
+
imgs = _sanitize_images(req.images)
|
|
140
|
+
atts = _sanitize_attachments(req.attachments)
|
|
141
|
+
|
|
142
|
+
async def async_sse():
|
|
143
|
+
if not _STREAM_SEM.acquire(blocking=False):
|
|
144
|
+
yield "data: " + json.dumps({"error": "busy, try again later"}) + "\n\n"
|
|
145
|
+
return
|
|
146
|
+
q: asyncio.Queue = asyncio.Queue()
|
|
147
|
+
stop = _threading.Event()
|
|
148
|
+
loop = asyncio.get_running_loop()
|
|
149
|
+
|
|
150
|
+
def _producer():
|
|
151
|
+
_parts = []
|
|
152
|
+
|
|
153
|
+
def _on_thinking(delta: str) -> None:
|
|
154
|
+
loop.call_soon_threadsafe(q.put_nowait, {"thinking": delta})
|
|
155
|
+
|
|
156
|
+
def _on_evidence(evs: list) -> None:
|
|
157
|
+
loop.call_soon_threadsafe(q.put_nowait, {"evidence": evs})
|
|
158
|
+
|
|
159
|
+
try:
|
|
160
|
+
for chunk in agent.stream_reply(req.task, stop_event=stop, images=imgs,
|
|
161
|
+
history=req.history, attachments=atts,
|
|
162
|
+
on_thinking=_on_thinking,
|
|
163
|
+
on_evidence=_on_evidence):
|
|
164
|
+
_parts.append(chunk)
|
|
165
|
+
loop.call_soon_threadsafe(q.put_nowait, {"delta": chunk})
|
|
166
|
+
loop.call_soon_threadsafe(q.put_nowait, {"done": True})
|
|
167
|
+
except Exception as e:
|
|
168
|
+
loop.call_soon_threadsafe(q.put_nowait, {"error": str(e)})
|
|
169
|
+
finally:
|
|
170
|
+
if req.session_id:
|
|
171
|
+
try:
|
|
172
|
+
_api.os_instance().sessions.append(req.session_id, "user", req.task, attachments=atts)
|
|
173
|
+
_reply = "".join(_parts)
|
|
174
|
+
if _reply:
|
|
175
|
+
_api.os_instance().sessions.append(req.session_id, "assistant", _reply)
|
|
176
|
+
except Exception:
|
|
177
|
+
pass
|
|
178
|
+
loop.call_soon_threadsafe(q.put_nowait, None)
|
|
179
|
+
|
|
180
|
+
_threading.Thread(target=_producer, daemon=True, name="chat-stream").start()
|
|
181
|
+
try:
|
|
182
|
+
while True:
|
|
183
|
+
if await request.is_disconnected():
|
|
184
|
+
stop.set(); break
|
|
185
|
+
try:
|
|
186
|
+
item = await asyncio.wait_for(q.get(), timeout=0.5)
|
|
187
|
+
except asyncio.TimeoutError:
|
|
188
|
+
continue
|
|
189
|
+
if item is None:
|
|
190
|
+
break
|
|
191
|
+
if "thinking" in item:
|
|
192
|
+
yield ("event: thinking\ndata: "
|
|
193
|
+
+ json.dumps({"thinking": item["thinking"]}, ensure_ascii=False)
|
|
194
|
+
+ "\n\n")
|
|
195
|
+
continue
|
|
196
|
+
if "evidence" in item:
|
|
197
|
+
yield ("event: evidence\ndata: "
|
|
198
|
+
+ json.dumps({"evidence": item["evidence"]}, ensure_ascii=False)
|
|
199
|
+
+ "\n\n")
|
|
200
|
+
continue
|
|
201
|
+
yield "data: " + json.dumps(item, ensure_ascii=False) + "\n\n"
|
|
202
|
+
finally:
|
|
203
|
+
stop.set()
|
|
204
|
+
_STREAM_SEM.release()
|
|
205
|
+
|
|
206
|
+
return StreamingResponse(async_sse(), media_type="text/event-stream",
|
|
207
|
+
headers={"Cache-Control": "no-cache", "X-Accel-Buffering": "no"})
|
|
208
|
+
|
|
209
|
+
|
|
210
|
+
@app.post("/api/task/stream")
|
|
211
|
+
async def task_stream(req: TaskRequest, request: Request):
|
|
212
|
+
"""流式任务执行(D1):SSE 逐 stage 进度 + act 逐 token + 最终结果。用于任务/分析等走**完整
|
|
213
|
+
认知管线**的场景(对话直答走 /api/chat/stream)。事件:`{stage}`/`{plan}`/`{delta}`/`{result}`/`{error}`。
|
|
214
|
+
【P3-3】主管(P1-1 pipeline 化后)同样可走本端点:stage 事件为主管管线的
|
|
215
|
+
recall/meta_check/schedule/route/plan/dispatch/aggregate;分发场景下子 Agent 的 act
|
|
216
|
+
token 随 contextvar 传播一并流出。
|
|
217
|
+
背压与断连处理与 chat_stream 一致:断连 → stop_event 同时停管线 stage(P3)+ 停 act LLM 生成。"""
|
|
218
|
+
if not _STREAM_LIMITER.allow():
|
|
219
|
+
async def _rl():
|
|
220
|
+
yield "data: " + json.dumps({"error": "rate limited"}) + "\n\n"
|
|
221
|
+
return StreamingResponse(_rl(), media_type="text/event-stream")
|
|
222
|
+
|
|
223
|
+
agent = _api.os_instance()._agents.get(req.agent_id)
|
|
224
|
+
if agent is None:
|
|
225
|
+
print(f"[Foxclaw API] ⚠ /api/task/stream 收到未知 agent_id={req.agent_id},回落 supervisor")
|
|
226
|
+
agent = _api.os_instance().supervisor
|
|
227
|
+
|
|
228
|
+
async def async_sse():
|
|
229
|
+
if not _STREAM_SEM.acquire(blocking=False):
|
|
230
|
+
yield "data: " + json.dumps({"error": "busy, try again later"}) + "\n\n"
|
|
231
|
+
return
|
|
232
|
+
q: asyncio.Queue = asyncio.Queue()
|
|
233
|
+
stop = _threading.Event()
|
|
234
|
+
loop = asyncio.get_running_loop()
|
|
235
|
+
|
|
236
|
+
def _producer():
|
|
237
|
+
try:
|
|
238
|
+
for evt in agent.run_task_stream(req.task, context=req.context,
|
|
239
|
+
stop_event=stop):
|
|
240
|
+
loop.call_soon_threadsafe(q.put_nowait, evt)
|
|
241
|
+
except Exception as e:
|
|
242
|
+
loop.call_soon_threadsafe(q.put_nowait, {"error": str(e)})
|
|
243
|
+
finally:
|
|
244
|
+
loop.call_soon_threadsafe(q.put_nowait, None)
|
|
245
|
+
|
|
246
|
+
_threading.Thread(target=_producer, daemon=True, name="task-stream-sse").start()
|
|
247
|
+
try:
|
|
248
|
+
while True:
|
|
249
|
+
if await request.is_disconnected():
|
|
250
|
+
stop.set(); break
|
|
251
|
+
try:
|
|
252
|
+
item = await asyncio.wait_for(q.get(), timeout=0.5)
|
|
253
|
+
except asyncio.TimeoutError:
|
|
254
|
+
continue
|
|
255
|
+
if item is None:
|
|
256
|
+
break
|
|
257
|
+
yield "data: " + json.dumps(item, ensure_ascii=False) + "\n\n"
|
|
258
|
+
finally:
|
|
259
|
+
stop.set()
|
|
260
|
+
_STREAM_SEM.release()
|
|
261
|
+
|
|
262
|
+
return StreamingResponse(async_sse(), media_type="text/event-stream",
|
|
263
|
+
headers={"Cache-Control": "no-cache", "X-Accel-Buffering": "no"})
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
"""WebSocket:实时 Harness 事件流(连接管理 + 端点)。
|
|
2
|
+
|
|
3
|
+
从 foxclaw/api.py 原样搬移(纯搬移,零行为变更)。manager 由 api/__init__.py re-export
|
|
4
|
+
给后台广播任务 _drain_event_queue 使用。
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import os
|
|
8
|
+
|
|
9
|
+
from fastapi import WebSocket, WebSocketDisconnect
|
|
10
|
+
|
|
11
|
+
from ..middleware import _constant_time_eq, _loopback_bypass_on, _is_loopback_client
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class ConnectionManager:
|
|
15
|
+
def __init__(self):
|
|
16
|
+
self.connections: list[WebSocket] = []
|
|
17
|
+
|
|
18
|
+
async def connect(self, ws: WebSocket):
|
|
19
|
+
await ws.accept()
|
|
20
|
+
self.connections.append(ws)
|
|
21
|
+
|
|
22
|
+
def disconnect(self, ws: WebSocket):
|
|
23
|
+
self.connections.remove(ws)
|
|
24
|
+
|
|
25
|
+
async def broadcast(self, data: dict):
|
|
26
|
+
for ws in self.connections[:]:
|
|
27
|
+
try:
|
|
28
|
+
await ws.send_json(data)
|
|
29
|
+
except Exception:
|
|
30
|
+
self.connections.remove(ws)
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
manager = ConnectionManager()
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
async def harness_websocket(websocket: WebSocket):
|
|
37
|
+
"""
|
|
38
|
+
实时 Harness 事件流。
|
|
39
|
+
事件转发器在启动时已注册到线程安全队列,这里只负责接受连接并保活。
|
|
40
|
+
|
|
41
|
+
鉴权:HTTP 中间件管不到 websocket scope,故 WS 的鉴权必须在端点内做。
|
|
42
|
+
浏览器无法给 WS 握手设请求头,因此接受 ?key= 查询参数(也兼容能设头的非浏览器
|
|
43
|
+
客户端的 X-API-Key 头)。未设 FOXCLAW_API_KEY 时开放;设了时本机(127.0.0.1/::1)
|
|
44
|
+
客户端也免 ?key=(与 HTTP 中间件同口径的 loopback bypass)。
|
|
45
|
+
"""
|
|
46
|
+
api_key = os.environ.get("FOXCLAW_API_KEY", "").strip()
|
|
47
|
+
if api_key:
|
|
48
|
+
ws_host = websocket.client.host if websocket.client else ""
|
|
49
|
+
if not (_loopback_bypass_on() and _is_loopback_client(ws_host)):
|
|
50
|
+
provided = (websocket.query_params.get("key")
|
|
51
|
+
or websocket.headers.get("x-api-key"))
|
|
52
|
+
if not _constant_time_eq(provided or "", api_key):
|
|
53
|
+
await websocket.close(code=1008)
|
|
54
|
+
return
|
|
55
|
+
await manager.connect(websocket)
|
|
56
|
+
try:
|
|
57
|
+
while True:
|
|
58
|
+
await websocket.receive_text()
|
|
59
|
+
except WebSocketDisconnect:
|
|
60
|
+
manager.disconnect(websocket)
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def register(app):
|
|
64
|
+
app.add_api_websocket_route("/ws/harness", harness_websocket)
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
"""事件脊柱辅助:WS payload 净化 + HKC 后台预热。
|
|
2
|
+
|
|
3
|
+
从 `foxclaw/api/__init__.py` 搬移出来的纯辅助函数。`_register_spine_projection`
|
|
4
|
+
因 `_WS_MUTED` 受测试源码级断言约束(需留在主文件 `__init__.py` 源码内)而未搬移。
|
|
5
|
+
"""
|
|
6
|
+
import json
|
|
7
|
+
import threading
|
|
8
|
+
import time as _t
|
|
9
|
+
|
|
10
|
+
import foxclaw.api as _api
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def _ws_safe_payload(payload, _maxlen: int = 4000):
|
|
14
|
+
"""把任意事件 payload 收敛成 **JSON 安全且有界** 的形式,供 WS 广播。
|
|
15
|
+
统一投影会把系统事件(execution.completed 等含原始 result/任意对象)也推给 WS,
|
|
16
|
+
必须防两类真实故障:
|
|
17
|
+
· 非 JSON 可序列化对象 → ws.send_json 抛异常 → broadcast 静默丢弃该连接;
|
|
18
|
+
· 超大 payload(整段 LLM 回复 / 大 result,且随真值链被广播多次)→ 向所有 WS 泛洪。
|
|
19
|
+
脊柱本身仍保留全保真事件(供 history / 审计 / 其它投影),净化只发生在 WS 出口。"""
|
|
20
|
+
try:
|
|
21
|
+
raw = json.dumps(payload, default=str, ensure_ascii=False)
|
|
22
|
+
except Exception:
|
|
23
|
+
return {"_repr": str(payload)[:_maxlen]}
|
|
24
|
+
if len(raw) > _maxlen:
|
|
25
|
+
return {"_truncated": True, "_preview": raw[:_maxlen]}
|
|
26
|
+
return json.loads(raw)
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def _prewarm_hkc(osi):
|
|
30
|
+
"""后台预热 HKC:装配容器 + 建搜索索引(bm25+faiss over 全量 KU,顺带加载 HKC
|
|
31
|
+
多语言 embedding 模型)+ 载入 learning。把"首次知识查询"的一次性冷成本(~几十秒到
|
|
32
|
+
1 分钟)挪到开机后台完成,之后所有知识查询都不再冷。守护线程、失败静默,绝不影响主应用。
|
|
33
|
+
索引重建与查询共用 HybridSearch 锁 → 预热期间若有查询会短暂等待,预热完即快。"""
|
|
34
|
+
|
|
35
|
+
def _run():
|
|
36
|
+
t0 = _t.time()
|
|
37
|
+
try:
|
|
38
|
+
svc = getattr(osi, "hkc", None)
|
|
39
|
+
if not svc or not getattr(svc, "available", False):
|
|
40
|
+
return
|
|
41
|
+
try:
|
|
42
|
+
lr = getattr(osi, "learning_runtime", None)
|
|
43
|
+
if lr is not None:
|
|
44
|
+
lr._ensure_hkc_loaded()
|
|
45
|
+
except Exception:
|
|
46
|
+
pass
|
|
47
|
+
print(f"[Foxclaw API] ✓ HKC 预热完成({_t.time()-t0:.1f}s): 首次知识查询不再冷装配")
|
|
48
|
+
except Exception as e:
|
|
49
|
+
print(f"[Foxclaw API] · HKC 预热未启用(可选): {e}")
|
|
50
|
+
|
|
51
|
+
_api._prewarm_thread = threading.Thread(target=_run, name="hkc-prewarm", daemon=True)
|
|
52
|
+
_api._prewarm_thread.start()
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
"""
|
|
2
|
+
foxclaw.browser —— HIR-Browser 融入 Foxclaw 的浏览器子系统("AI 用的浏览器")。
|
|
3
|
+
|
|
4
|
+
只做一件事:感知网页 → 压缩成 LLM 可读的语义结构 → 把动作翻译成真实浏览器
|
|
5
|
+
操作 → 校验执行结果。不做规划、不做决策——那是 Agent/Foxclaw 控制面的职责。
|
|
6
|
+
|
|
7
|
+
对外入口是 BrowserService(async 安全、可选依赖优雅降级、专用工作线程)。
|
|
8
|
+
`import foxclaw.browser` 不会拉入 playwright;只有真正开会话时才需要。
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from .service import BrowserService
|
|
12
|
+
|
|
13
|
+
__all__ = ["BrowserService"]
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""动作层:WebAction 数据形状 + 执行器(翻译成真实操作)+ 校验器(快照 diff 判定生效)。"""
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
"""
|
|
2
|
+
ActionExecutor —— 把 WebAction 翻译成真实浏览器操作。
|
|
3
|
+
|
|
4
|
+
只做翻译和调用,不做判断(判断动作是否该执行、是否合理,
|
|
5
|
+
不属于这一层,也不属于 HIR-Browser 整体,那是 Foxclaw/HAR 的职责)。
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from .action_types import WebAction
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class ActionExecutor:
|
|
12
|
+
|
|
13
|
+
def __init__(self, driver, element_indexer):
|
|
14
|
+
self.driver = driver
|
|
15
|
+
self.indexer = element_indexer
|
|
16
|
+
|
|
17
|
+
def run(self, action: WebAction):
|
|
18
|
+
handler = getattr(self, f"_do_{action.type}", None)
|
|
19
|
+
if handler is None:
|
|
20
|
+
raise NotImplementedError(f"未实现的动作类型: {action.type}")
|
|
21
|
+
return handler(action)
|
|
22
|
+
|
|
23
|
+
def _do_navigate(self, action: WebAction):
|
|
24
|
+
from .url_policy import validate_navigate_url
|
|
25
|
+
target = validate_navigate_url(action.target or "")
|
|
26
|
+
self.driver.navigate(target)
|
|
27
|
+
return {"navigated_to": target}
|
|
28
|
+
|
|
29
|
+
def _do_click(self, action: WebAction):
|
|
30
|
+
handle = self.indexer.resolve(action.target)
|
|
31
|
+
self.driver.click(handle)
|
|
32
|
+
return {"clicked": action.target}
|
|
33
|
+
|
|
34
|
+
def _do_type(self, action: WebAction):
|
|
35
|
+
handle = self.indexer.resolve(action.target)
|
|
36
|
+
self.driver.type_text(handle, action.value or "")
|
|
37
|
+
return {"typed": action.value, "into": action.target}
|
|
38
|
+
|
|
39
|
+
def _do_scroll(self, action: WebAction):
|
|
40
|
+
direction = action.value or "down"
|
|
41
|
+
self.driver.scroll(direction)
|
|
42
|
+
return {"scrolled": direction}
|
|
43
|
+
|
|
44
|
+
def _do_extract(self, action: WebAction):
|
|
45
|
+
return {"extract_requested": True}
|
|
46
|
+
|
|
47
|
+
def _do_wait(self, action: WebAction):
|
|
48
|
+
seconds = float(action.value or 1.0)
|
|
49
|
+
self.driver.wait(seconds)
|
|
50
|
+
return {"waited": seconds}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Action Graph 节点在 Web 场景下的具体形态。
|
|
3
|
+
|
|
4
|
+
这里只定义"数据形状",不含任何执行逻辑,也不含任何"该不该执行"的判断——
|
|
5
|
+
那是 HAR / Foxclaw 的职责。
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from dataclasses import dataclass, field
|
|
9
|
+
from typing import Optional, Any
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
SUPPORTED_ACTIONS = ("navigate", "click", "type", "scroll", "extract", "wait")
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
@dataclass
|
|
16
|
+
class WebAction:
|
|
17
|
+
"""
|
|
18
|
+
一个标准化的 Web 动作节点。
|
|
19
|
+
|
|
20
|
+
type: navigate | click | type | scroll | extract | wait
|
|
21
|
+
target: 元素编号(由 SemanticReducer 分配的 id),navigate 时为 URL
|
|
22
|
+
value: type 动作的输入文本;scroll 的方向/距离
|
|
23
|
+
meta: 透传字段,供上层(HAR/Foxclaw)附加调试信息,Web Adapter 不解析它
|
|
24
|
+
"""
|
|
25
|
+
|
|
26
|
+
type: str
|
|
27
|
+
target: Optional[str] = None
|
|
28
|
+
value: Optional[Any] = None
|
|
29
|
+
meta: dict = field(default_factory=dict)
|
|
30
|
+
|
|
31
|
+
def __post_init__(self):
|
|
32
|
+
if self.type not in SUPPORTED_ACTIONS:
|
|
33
|
+
raise ValueError(
|
|
34
|
+
f"不支持的动作类型: {self.type},仅支持 {SUPPORTED_ACTIONS}"
|
|
35
|
+
)
|
|
36
|
+
|
|
37
|
+
@classmethod
|
|
38
|
+
def from_dict(cls, d: dict) -> "WebAction":
|
|
39
|
+
return cls(
|
|
40
|
+
type=d["type"],
|
|
41
|
+
target=d.get("target"),
|
|
42
|
+
value=d.get("value"),
|
|
43
|
+
meta=d.get("meta", {}),
|
|
44
|
+
)
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
"""
|
|
2
|
+
ActionVerifier —— HIR-Browser 的第二个核心壁垒。
|
|
3
|
+
|
|
4
|
+
问题:LLM 不知道自己的操作有没有生效。selector 没报错 ≠ 操作成功。
|
|
5
|
+
方案:执行前后各拍一次语义快照,做 diff,用 diff 结果去校验
|
|
6
|
+
"这次动作是否符合预期",而不是简单地"没抛异常就算成功"。
|
|
7
|
+
|
|
8
|
+
这里的判断规则刻意保持简单、可解释,不引入模型推理——
|
|
9
|
+
"是否成功"这个判断本身应该是确定性的、可复现的。
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
from ..perception.snapshot import Snapshot
|
|
13
|
+
from .action_types import WebAction
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class ActionVerifier:
|
|
17
|
+
|
|
18
|
+
def verify(self, before: Snapshot, after: Snapshot, action: WebAction) -> dict:
|
|
19
|
+
diff = self._diff(before, after)
|
|
20
|
+
|
|
21
|
+
success = self._matches_expectation(diff, action)
|
|
22
|
+
|
|
23
|
+
return {
|
|
24
|
+
"success": success,
|
|
25
|
+
"diff": diff,
|
|
26
|
+
"url_changed": before.url != after.url,
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
def _diff(self, before: Snapshot, after: Snapshot) -> dict:
|
|
30
|
+
before_ids = before.element_ids()
|
|
31
|
+
after_ids = after.element_ids()
|
|
32
|
+
|
|
33
|
+
return {
|
|
34
|
+
"url_before": before.url,
|
|
35
|
+
"url_after": after.url,
|
|
36
|
+
"elements_added": len(after_ids - before_ids),
|
|
37
|
+
"elements_removed": len(before_ids - after_ids),
|
|
38
|
+
"title_before": before.title,
|
|
39
|
+
"title_after": after.title,
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
def _matches_expectation(self, diff: dict, action: WebAction) -> bool:
|
|
43
|
+
"""
|
|
44
|
+
按动作类型给出最基本的"是否生效"判断:
|
|
45
|
+
|
|
46
|
+
- navigate: URL 必须变化
|
|
47
|
+
- click: URL 变化,或页面元素结构发生变化(新增/移除元素),
|
|
48
|
+
或标题变化——三者任一即视为"页面对点击有反应"
|
|
49
|
+
- type: 无法通过快照diff直接验证输入内容是否写入(需要额外读取
|
|
50
|
+
目标元素的 value),这里保守返回 True,交由上层结合
|
|
51
|
+
extract 结果自行判断
|
|
52
|
+
- scroll / wait / extract: 不强制要求页面发生变化
|
|
53
|
+
"""
|
|
54
|
+
if action.type == "navigate":
|
|
55
|
+
return diff["url_before"] != diff["url_after"]
|
|
56
|
+
|
|
57
|
+
if action.type == "click":
|
|
58
|
+
return (
|
|
59
|
+
diff["url_before"] != diff["url_after"]
|
|
60
|
+
or diff["elements_added"] > 0
|
|
61
|
+
or diff["elements_removed"] > 0
|
|
62
|
+
or diff["title_before"] != diff["title_after"]
|
|
63
|
+
)
|
|
64
|
+
|
|
65
|
+
if action.type in ("type", "scroll", "wait", "extract"):
|
|
66
|
+
return True
|
|
67
|
+
|
|
68
|
+
return True
|