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,144 @@
|
|
|
1
|
+
"""
|
|
2
|
+
BrowserService —— HIR-Browser 在 Foxclaw 内的 async 安全接入层。
|
|
3
|
+
|
|
4
|
+
它解决把"同步 Playwright 浏览器"塞进"async FastAPI"的三个硬约束:
|
|
5
|
+
|
|
6
|
+
A1 可选依赖:playwright(及 chromium 内核)是可选 extra。未安装时
|
|
7
|
+
available()=False,perceive/act 返回结构化"不可用"而非崩溃,
|
|
8
|
+
与 search 子系统的优雅降级一致。
|
|
9
|
+
|
|
10
|
+
A2 线程亲和 + 事件循环禁区:Playwright sync API 不能在 asyncio 事件
|
|
11
|
+
循环线程里运行,且其对象绑定创建它的线程。故本 Service 用一条
|
|
12
|
+
max_workers=1 的**专用工作线程**独占持有会话——所有浏览器操作都
|
|
13
|
+
被 run_in_executor 派发到这同一条线程,既满足线程亲和,也天然把
|
|
14
|
+
单会话(v1 只支持单会话)的访问串行化。
|
|
15
|
+
|
|
16
|
+
A3 感知入口:额外暴露 perceive()(只感知、不动作),供 agent/前端在
|
|
17
|
+
决策前读取页面语义结构;原 WebAdapter 只有 execute。
|
|
18
|
+
|
|
19
|
+
对外方法都是 async,直接 await 即可;内部 _*_sync 方法只在工作线程上跑。
|
|
20
|
+
"""
|
|
21
|
+
|
|
22
|
+
import asyncio
|
|
23
|
+
import importlib.util
|
|
24
|
+
import threading
|
|
25
|
+
from concurrent.futures import ThreadPoolExecutor
|
|
26
|
+
from typing import Optional, TYPE_CHECKING
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
if TYPE_CHECKING:
|
|
30
|
+
from .session.session_manager import SessionManager
|
|
31
|
+
|
|
32
|
+
class BrowserService:
|
|
33
|
+
|
|
34
|
+
def __init__(self, headless: bool = True):
|
|
35
|
+
self._headless = headless
|
|
36
|
+
self._session: "SessionManager | None" = None
|
|
37
|
+
self._executor: Optional[ThreadPoolExecutor] = None
|
|
38
|
+
self._closed = False
|
|
39
|
+
self._launch_error: Optional[str] = None
|
|
40
|
+
self._guard = threading.Lock()
|
|
41
|
+
|
|
42
|
+
@staticmethod
|
|
43
|
+
def available() -> bool:
|
|
44
|
+
"""playwright 包是否可导入。注意:这不保证 chromium 内核已 install,
|
|
45
|
+
内核缺失会在首次 launch 时暴露,由 _launch_error 记录并透出。"""
|
|
46
|
+
return importlib.util.find_spec("playwright") is not None
|
|
47
|
+
|
|
48
|
+
def status(self) -> dict:
|
|
49
|
+
return {
|
|
50
|
+
"available": self.available(),
|
|
51
|
+
"headless": self._headless,
|
|
52
|
+
"session_active": self._session is not None,
|
|
53
|
+
"closed": self._closed,
|
|
54
|
+
"launch_error": self._launch_error,
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
def _get_executor(self) -> ThreadPoolExecutor:
|
|
58
|
+
with self._guard:
|
|
59
|
+
if self._closed:
|
|
60
|
+
raise RuntimeError("BrowserService 已关闭")
|
|
61
|
+
if self._executor is None:
|
|
62
|
+
self._executor = ThreadPoolExecutor(
|
|
63
|
+
max_workers=1, thread_name_prefix="foxclaw-browser")
|
|
64
|
+
return self._executor
|
|
65
|
+
|
|
66
|
+
def _ensure_session_sync(self):
|
|
67
|
+
"""在工作线程上惰性构造会话(首次 launch chromium)。"""
|
|
68
|
+
if self._session is None:
|
|
69
|
+
from .session.session_manager import SessionManager
|
|
70
|
+
self._session = SessionManager(headless=self._headless)
|
|
71
|
+
return self._session
|
|
72
|
+
|
|
73
|
+
def _perceive_sync(self) -> dict:
|
|
74
|
+
return self._ensure_session_sync().snapshot().to_dict()
|
|
75
|
+
|
|
76
|
+
def _act_sync(self, action_dict: dict) -> dict:
|
|
77
|
+
from .action.action_types import WebAction
|
|
78
|
+
session = self._ensure_session_sync()
|
|
79
|
+
return session.execute(WebAction.from_dict(action_dict))
|
|
80
|
+
|
|
81
|
+
def _close_sync(self):
|
|
82
|
+
if self._session is not None:
|
|
83
|
+
try:
|
|
84
|
+
self._session.close()
|
|
85
|
+
finally:
|
|
86
|
+
self._session = None
|
|
87
|
+
|
|
88
|
+
async def _run(self, fn, *args):
|
|
89
|
+
loop = asyncio.get_running_loop()
|
|
90
|
+
return await loop.run_in_executor(self._get_executor(), fn, *args)
|
|
91
|
+
|
|
92
|
+
async def perceive(self) -> dict:
|
|
93
|
+
"""只感知当前页面语义结构,不改变页面状态。"""
|
|
94
|
+
if not self.available():
|
|
95
|
+
return self._unavailable()
|
|
96
|
+
try:
|
|
97
|
+
return {"ok": True, "semantic_state": await self._run(self._perceive_sync)}
|
|
98
|
+
except Exception as e:
|
|
99
|
+
return self._handle_error(e)
|
|
100
|
+
|
|
101
|
+
async def act(self, action: dict) -> dict:
|
|
102
|
+
"""执行一个 Web 动作(navigate/click/type/scroll/extract/wait),返回校验后最新语义结构。"""
|
|
103
|
+
if not self.available():
|
|
104
|
+
return self._unavailable()
|
|
105
|
+
if not isinstance(action, dict) or "type" not in action:
|
|
106
|
+
return {"ok": False, "error": "非法动作:需 {type, target?, value?}"}
|
|
107
|
+
try:
|
|
108
|
+
return {"ok": True, **await self._run(self._act_sync, action)}
|
|
109
|
+
except ValueError as e:
|
|
110
|
+
return {"ok": False, "error": str(e)}
|
|
111
|
+
except KeyError as e:
|
|
112
|
+
return {"ok": False, "error": str(e).strip('"')}
|
|
113
|
+
except Exception as e:
|
|
114
|
+
return self._handle_error(e)
|
|
115
|
+
|
|
116
|
+
async def close(self):
|
|
117
|
+
"""释放浏览器会话并关停专用工作线程(幂等)。挂到 Foxclaw teardown。"""
|
|
118
|
+
with self._guard:
|
|
119
|
+
if self._closed:
|
|
120
|
+
return
|
|
121
|
+
self._closed = True
|
|
122
|
+
executor = self._executor
|
|
123
|
+
if executor is not None:
|
|
124
|
+
try:
|
|
125
|
+
loop = asyncio.get_running_loop()
|
|
126
|
+
await loop.run_in_executor(executor, self._close_sync)
|
|
127
|
+
finally:
|
|
128
|
+
executor.shutdown(wait=True)
|
|
129
|
+
|
|
130
|
+
def _unavailable(self) -> dict:
|
|
131
|
+
return {
|
|
132
|
+
"ok": False,
|
|
133
|
+
"available": False,
|
|
134
|
+
"error": "浏览器子系统不可用:未安装 playwright(可选依赖)。"
|
|
135
|
+
"安装:pip install 'foxclaw[browser]' 且 playwright install chromium",
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
def _handle_error(self, e: Exception) -> dict:
|
|
139
|
+
msg = str(e)
|
|
140
|
+
if "playwright install" in msg or "Executable doesn't exist" in msg:
|
|
141
|
+
self._launch_error = msg
|
|
142
|
+
return {"ok": False, "available": False,
|
|
143
|
+
"error": "playwright 已装但缺 chromium 内核,请运行:playwright install chromium"}
|
|
144
|
+
return {"ok": False, "error": f"{type(e).__name__}: {msg}"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""会话层:单浏览器会话的生命周期与组件组装(感知/执行/校验的统一入口)。"""
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
"""
|
|
2
|
+
SessionManager —— 单浏览器会话的生命周期管理与组件组装。
|
|
3
|
+
|
|
4
|
+
v1.0 明确只支持单会话(对应审计里"v1不做分布式/多浏览器集群"的边界)。
|
|
5
|
+
多会话池、并发调度属于 Infrastructure Plane,v2 再做。
|
|
6
|
+
|
|
7
|
+
【融合注记 A2】本类构造即 launch chromium(有线程亲和性),
|
|
8
|
+
必须始终在 BrowserService 的同一条专用工作线程上构造与调用。
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from ..driver.playwright_driver import PlaywrightDriver
|
|
12
|
+
from ..perception.dom_extractor import DomExtractor
|
|
13
|
+
from ..perception.element_indexer import ElementIndexer
|
|
14
|
+
from ..perception.semantic_reducer import SemanticReducer
|
|
15
|
+
from ..action.action_executor import ActionExecutor
|
|
16
|
+
from ..action.action_verifier import ActionVerifier
|
|
17
|
+
from ..action.action_types import WebAction
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class SessionManager:
|
|
21
|
+
|
|
22
|
+
def __init__(self, headless: bool = True):
|
|
23
|
+
self.driver = PlaywrightDriver(headless=headless)
|
|
24
|
+
self.indexer = ElementIndexer()
|
|
25
|
+
self.dom_extractor = DomExtractor(self.driver)
|
|
26
|
+
self.reducer = SemanticReducer(self.dom_extractor, self.indexer)
|
|
27
|
+
self.executor = ActionExecutor(self.driver, self.indexer)
|
|
28
|
+
self.verifier = ActionVerifier()
|
|
29
|
+
|
|
30
|
+
def snapshot(self):
|
|
31
|
+
"""对外暴露的唯一"感知"入口"""
|
|
32
|
+
return self.reducer.reduce()
|
|
33
|
+
|
|
34
|
+
def execute(self, action: WebAction) -> dict:
|
|
35
|
+
"""
|
|
36
|
+
对外暴露的唯一"执行"入口:
|
|
37
|
+
执行前拍快照 -> 执行 -> 执行后拍快照 -> 校验 -> 返回最新语义结构
|
|
38
|
+
|
|
39
|
+
注:执行前的 before 快照会重建元素编号表——这既服务于 diff 校验,
|
|
40
|
+
也顺带刷新了可能已 detach 的旧句柄(页面在两次调用间可能已变化)。
|
|
41
|
+
因此上层引用的编号应来自"上一次 execute/perceive 返回的 semantic_state",
|
|
42
|
+
且假定两次调用之间页面结构稳定(编号按 DOM 顺序稳定复现)。
|
|
43
|
+
"""
|
|
44
|
+
before = self.snapshot()
|
|
45
|
+
raw_result = self.executor.run(action)
|
|
46
|
+
after = self.snapshot()
|
|
47
|
+
|
|
48
|
+
verification = self.verifier.verify(before, after, action)
|
|
49
|
+
|
|
50
|
+
return {
|
|
51
|
+
"raw": raw_result,
|
|
52
|
+
"semantic_state": after.to_dict(),
|
|
53
|
+
"verified": verification,
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
def close(self):
|
|
57
|
+
self.driver.close()
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
"""
|
|
2
|
+
foxclaw.capability —— 全系统共享的能力层(Capability Plane)。
|
|
3
|
+
|
|
4
|
+
把"重、且应当全进程共享一份"的能力收敛到这里,避免各模块各自加载:
|
|
5
|
+
· EmbeddingService:sentence-transformers 文本向量,按模型名缓存,全进程只加载一次
|
|
6
|
+
(此前 5 个 Harness 各建一个 SemanticDrift → 各自 load 一次模型,启动 6×→现 1×)
|
|
7
|
+
|
|
8
|
+
(后续 v2 的 LLMRouter 也应上浮到这一层,与 EmbeddingService 并列。)
|
|
9
|
+
"""
|
|
10
|
+
from .embedding_service import get_text_embedder, embedding_stats
|
|
11
|
+
|
|
12
|
+
__all__ = ["get_text_embedder", "embedding_stats"]
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
"""
|
|
2
|
+
EmbeddingService —— 进程内共享的文本向量能力(P0 性能修复)。
|
|
3
|
+
|
|
4
|
+
问题:此前每个 Harness 各建一个 SemanticDrift → 各自 `SentenceTransformer(model)`。
|
|
5
|
+
5 个 Agent = 5 份同名模型驻留内存 + 5 次加载(启动慢的主因,run_all 实测 44s→7s
|
|
6
|
+
还是靠离线缓存,加载次数没减)。
|
|
7
|
+
|
|
8
|
+
方案:按模型名进程内缓存,同名模型只加载一次,全系统共享同一个只读实例。
|
|
9
|
+
· sentence-transformers 的 encode 是无梯度前向,多线程对同一只读模型并发 encode
|
|
10
|
+
是安全的(标准的"一个模型服务多请求"模式),故共享比每 Agent 各持一份更优:
|
|
11
|
+
内存 5×→1×,加载 5×→1×。
|
|
12
|
+
· 缺库/加载失败 → 返回 None,调用方(SemanticDrift)自动降级 n-gram 哈希嵌入。
|
|
13
|
+
|
|
14
|
+
【P2·单 embedding 入口·share-bge-m3】
|
|
15
|
+
★ 改用 HKC 进程缓存(hkc_search.embedding_backends.get_st_model)取模型 instance,
|
|
16
|
+
与 HKC 检索 / HMR VectorStore / Harness SemanticDrift 全部走同一份 instance,
|
|
17
|
+
全 Foxclaw 真正只 load bge-m3 **1 次**。之前这里自己 `SentenceTransformer(name)`,
|
|
18
|
+
导致 Harness 启动期独立 Loading 第 2 次(浪费 ~1GB 内存)。
|
|
19
|
+
"""
|
|
20
|
+
from __future__ import annotations
|
|
21
|
+
import os
|
|
22
|
+
import threading
|
|
23
|
+
|
|
24
|
+
_lock = threading.Lock()
|
|
25
|
+
_cache: dict[str, object] = {}
|
|
26
|
+
_load_attempts = 0
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def get_text_embedder(model_name: str | None = None):
|
|
30
|
+
"""返回按 model_name 缓存的共享文本向量模型(sentence-transformers)。
|
|
31
|
+
走 HKC 进程缓存(hkc_search.embedding_backends.get_st_model),同名只加载一次,
|
|
32
|
+
与 HKC / HMR 共用同一 instance(share-bge-m3)。缺库/失败返回 None。线程安全。
|
|
33
|
+
|
|
34
|
+
model_name 解析链(【P2·单 embedding 入口】)——
|
|
35
|
+
显式传参 > EMBEDDING_MODEL(系统唯一本地入口) > HKC_EMBEDDING_MODEL > FOXCLAW_EMBEDDING_MODEL
|
|
36
|
+
> all-MiniLM-L6-v2(兜底)。与 HKC init / HMR VectorStore / harness.py 完全一致,
|
|
37
|
+
保证 get_st_model 缓存 key 命中同一份 instance。"""
|
|
38
|
+
global _load_attempts
|
|
39
|
+
name = (model_name
|
|
40
|
+
or os.environ.get("EMBEDDING_MODEL")
|
|
41
|
+
or os.environ.get("HKC_EMBEDDING_MODEL")
|
|
42
|
+
or os.environ.get("FOXCLAW_EMBEDDING_MODEL")
|
|
43
|
+
or "all-MiniLM-L6-v2").strip()
|
|
44
|
+
with _lock:
|
|
45
|
+
if name in _cache:
|
|
46
|
+
return _cache[name]
|
|
47
|
+
_load_attempts += 1
|
|
48
|
+
model = None
|
|
49
|
+
try:
|
|
50
|
+
from hkc_search.embedding_backends import get_st_model as _hk_get
|
|
51
|
+
model = _hk_get(name)
|
|
52
|
+
except Exception:
|
|
53
|
+
model = None
|
|
54
|
+
_cache[name] = model
|
|
55
|
+
return model
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def embedding_stats() -> dict:
|
|
59
|
+
"""可观测:当前缓存了哪些模型、发起过多少次加载。用于证明"同名只加载一次"。"""
|
|
60
|
+
with _lock:
|
|
61
|
+
return {
|
|
62
|
+
"cached_models": {n: (m is not None) for n, m in _cache.items()},
|
|
63
|
+
"distinct_models": len(_cache),
|
|
64
|
+
"load_attempts": _load_attempts,
|
|
65
|
+
}
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Foxclaw Channels — 渠道抽象层 (Channel Abstraction)
|
|
3
|
+
|
|
4
|
+
借鉴 OpenClaw 的渠道架构:适配器模式 + 消息标准化。
|
|
5
|
+
每个 IM 平台(Telegram/Slack/飞书…)一个适配器,封装该平台官方 API;
|
|
6
|
+
入站消息统一标准化成 InboundMessage,出站统一从 OutboundMessage 分派。
|
|
7
|
+
上层(主管 Agent)只跟标准消息打交道,不关心是哪个平台。
|
|
8
|
+
|
|
9
|
+
接入流程(对称):
|
|
10
|
+
平台原始消息 → adapter.normalize() → InboundMessage → on_message 回调
|
|
11
|
+
OutboundMessage → adapter.send() → 平台官方 API
|
|
12
|
+
"""
|
|
13
|
+
from __future__ import annotations
|
|
14
|
+
from abc import ABC, abstractmethod
|
|
15
|
+
from dataclasses import dataclass, field
|
|
16
|
+
from typing import Callable, Optional
|
|
17
|
+
import time
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
@dataclass
|
|
21
|
+
class InboundMessage:
|
|
22
|
+
"""标准化的入站消息(各平台 normalize 后的统一格式)。"""
|
|
23
|
+
channel: str
|
|
24
|
+
message_id: str
|
|
25
|
+
text: str
|
|
26
|
+
sender_id: str
|
|
27
|
+
sender_name: str = ""
|
|
28
|
+
conversation_id: str = ""
|
|
29
|
+
is_group: bool = False
|
|
30
|
+
timestamp: float = field(default_factory=time.time)
|
|
31
|
+
raw: dict = field(default_factory=dict)
|
|
32
|
+
|
|
33
|
+
def to_dict(self) -> dict:
|
|
34
|
+
return {"channel": self.channel, "message_id": self.message_id,
|
|
35
|
+
"text": self.text, "sender_id": self.sender_id,
|
|
36
|
+
"sender_name": self.sender_name,
|
|
37
|
+
"conversation_id": self.conversation_id,
|
|
38
|
+
"is_group": self.is_group, "timestamp": self.timestamp}
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
@dataclass
|
|
42
|
+
class OutboundMessage:
|
|
43
|
+
"""标准化的出站消息(上层产出,各适配器转成平台格式发送)。"""
|
|
44
|
+
conversation_id: str
|
|
45
|
+
text: str
|
|
46
|
+
reply_to: str = ""
|
|
47
|
+
extra: dict = field(default_factory=dict)
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
@dataclass
|
|
51
|
+
class SendResult:
|
|
52
|
+
ok: bool
|
|
53
|
+
message_id: str = ""
|
|
54
|
+
error: str = ""
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
class ChannelAdapter(ABC):
|
|
58
|
+
"""渠道适配器基类。每个平台实现 normalize/send/start。
|
|
59
|
+
|
|
60
|
+
设计原则(借鉴 OpenClaw):
|
|
61
|
+
- 适配器只负责"翻译"(平台格式 ↔ 标准格式)和"收发",不含业务逻辑
|
|
62
|
+
- 凭证由宿主注入(token 等),适配器不硬编码任何密钥
|
|
63
|
+
- 入站统一回调 on_message,上层无需关心平台差异
|
|
64
|
+
"""
|
|
65
|
+
|
|
66
|
+
CHANNEL = "base"
|
|
67
|
+
|
|
68
|
+
def __init__(self, on_message: Callable[[InboundMessage], None] | None = None):
|
|
69
|
+
self.on_message = on_message
|
|
70
|
+
self._running = False
|
|
71
|
+
self.last_error = ""
|
|
72
|
+
|
|
73
|
+
@abstractmethod
|
|
74
|
+
def normalize(self, raw: dict) -> Optional[InboundMessage]:
|
|
75
|
+
"""把平台原始消息转成标准 InboundMessage。非消息事件返回 None。"""
|
|
76
|
+
...
|
|
77
|
+
|
|
78
|
+
@abstractmethod
|
|
79
|
+
def send(self, msg: OutboundMessage) -> SendResult:
|
|
80
|
+
"""把标准 OutboundMessage 通过平台官方 API 发出。"""
|
|
81
|
+
...
|
|
82
|
+
|
|
83
|
+
def handle_raw(self, raw: dict):
|
|
84
|
+
"""收到平台原始消息时调用:标准化 → 触发 on_message 回调。
|
|
85
|
+
|
|
86
|
+
全程异常隔离:单条消息处理失败不会抛出(否则会中断整批轮询,
|
|
87
|
+
且已推进的游标不回滚 → 后续消息全部丢失)。失败记录在 last_error。
|
|
88
|
+
"""
|
|
89
|
+
try:
|
|
90
|
+
inbound = self.normalize(raw)
|
|
91
|
+
except Exception as e:
|
|
92
|
+
self.last_error = f"normalize 失败: {e}"
|
|
93
|
+
return None
|
|
94
|
+
if inbound is None:
|
|
95
|
+
return None
|
|
96
|
+
if self.on_message:
|
|
97
|
+
try:
|
|
98
|
+
self.on_message(inbound)
|
|
99
|
+
except Exception as e:
|
|
100
|
+
self.last_error = f"on_message 失败: {e}"
|
|
101
|
+
return inbound
|
|
102
|
+
|
|
103
|
+
def reply(self, inbound: InboundMessage, text: str) -> SendResult:
|
|
104
|
+
"""便捷回复:回到消息来源的会话。"""
|
|
105
|
+
return self.send(OutboundMessage(
|
|
106
|
+
conversation_id=inbound.conversation_id, text=text,
|
|
107
|
+
reply_to=inbound.message_id))
|
|
108
|
+
|
|
109
|
+
@property
|
|
110
|
+
def running(self) -> bool:
|
|
111
|
+
return self._running
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
class ChannelRegistry:
|
|
115
|
+
"""渠道注册表。管理多个已接入的适配器,统一分派。"""
|
|
116
|
+
|
|
117
|
+
def __init__(self):
|
|
118
|
+
self._adapters: dict[str, ChannelAdapter] = {}
|
|
119
|
+
|
|
120
|
+
def register(self, adapter: ChannelAdapter):
|
|
121
|
+
self._adapters[adapter.CHANNEL] = adapter
|
|
122
|
+
return adapter
|
|
123
|
+
|
|
124
|
+
def get(self, channel: str) -> Optional[ChannelAdapter]:
|
|
125
|
+
return self._adapters.get(channel)
|
|
126
|
+
|
|
127
|
+
def all(self) -> list:
|
|
128
|
+
return list(self._adapters.values())
|
|
129
|
+
|
|
130
|
+
def send(self, channel: str, msg: OutboundMessage) -> SendResult:
|
|
131
|
+
adapter = self.get(channel)
|
|
132
|
+
if adapter is None:
|
|
133
|
+
return SendResult(False, error=f"渠道未接入: {channel}")
|
|
134
|
+
return adapter.send(msg)
|
|
135
|
+
|
|
136
|
+
def status(self) -> dict:
|
|
137
|
+
return {ch: {"running": a.running, "channel": a.CHANNEL}
|
|
138
|
+
for ch, a in self._adapters.items()}
|
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Foxclaw Channels — Discord 适配器
|
|
3
|
+
|
|
4
|
+
用 Discord 官方 API v10。接入模型和前三个又不同(不套同一模子):
|
|
5
|
+
· 收消息:Gateway WebSocket 长连接 —— Discord 主动推事件(MESSAGE_CREATE 等)
|
|
6
|
+
- 连接后要 IDENTIFY(带 bot token + intents)
|
|
7
|
+
- 要按 Discord 指定的间隔发心跳(HEARTBEAT)维持连接
|
|
8
|
+
- intents 决定能收哪些事件(收消息内容需 MESSAGE_CONTENT intent)
|
|
9
|
+
· 发消息:REST API POST /channels/{id}/messages(带 Bot token)
|
|
10
|
+
|
|
11
|
+
凭证:bot token(Discord Developer Portal 创建应用 → Bot),由宿主注入。
|
|
12
|
+
|
|
13
|
+
说明:完整 Gateway 长连接(心跳/重连/分片)较重,本适配器提供:
|
|
14
|
+
- send():REST 发送(完整可用)
|
|
15
|
+
- normalize():把 Discord MESSAGE_CREATE 事件标准化
|
|
16
|
+
- gateway 连接循环骨架(connect_gateway),宿主可在异步环境跑
|
|
17
|
+
对收消息,推荐宿主用成熟的 discord.py/WebSocket 接 on_message,再喂给 normalize。
|
|
18
|
+
"""
|
|
19
|
+
from __future__ import annotations
|
|
20
|
+
import json
|
|
21
|
+
from typing import Any, Optional
|
|
22
|
+
from . import ChannelAdapter, InboundMessage, OutboundMessage, SendResult
|
|
23
|
+
|
|
24
|
+
DISCORD_API_BASE = "https://discord.com/api/v10"
|
|
25
|
+
DISCORD_GATEWAY = "wss://gateway.discord.gg/?v=10&encoding=json"
|
|
26
|
+
|
|
27
|
+
OP_DISPATCH = 0
|
|
28
|
+
OP_HEARTBEAT = 1
|
|
29
|
+
OP_IDENTIFY = 2
|
|
30
|
+
OP_HELLO = 10
|
|
31
|
+
OP_HEARTBEAT_ACK = 11
|
|
32
|
+
|
|
33
|
+
INTENT_GUILD_MESSAGES = 1 << 9
|
|
34
|
+
INTENT_DIRECT_MESSAGES = 1 << 12
|
|
35
|
+
INTENT_MESSAGE_CONTENT = 1 << 15
|
|
36
|
+
DEFAULT_INTENTS = INTENT_GUILD_MESSAGES | INTENT_DIRECT_MESSAGES | INTENT_MESSAGE_CONTENT
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
class DiscordAdapter(ChannelAdapter):
|
|
40
|
+
CHANNEL = "discord"
|
|
41
|
+
|
|
42
|
+
def __init__(self, bot_token: str, intents: int = DEFAULT_INTENTS,
|
|
43
|
+
on_message=None, timeout: float = 30.0):
|
|
44
|
+
super().__init__(on_message=on_message)
|
|
45
|
+
self.bot_token = bot_token
|
|
46
|
+
self.intents = intents
|
|
47
|
+
self.timeout = timeout
|
|
48
|
+
self._bot_user_id = ""
|
|
49
|
+
|
|
50
|
+
def _headers(self) -> dict:
|
|
51
|
+
return {"Authorization": f"Bot {self.bot_token}",
|
|
52
|
+
"Content-Type": "application/json",
|
|
53
|
+
"User-Agent": "Foxclaw (https://github.com, 1.0)"}
|
|
54
|
+
|
|
55
|
+
def normalize(self, raw: dict) -> Optional[InboundMessage]:
|
|
56
|
+
"""Discord Gateway 事件 → 标准 InboundMessage。
|
|
57
|
+
|
|
58
|
+
raw 可以是完整 Gateway 帧({op,t,d})或直接是 MESSAGE_CREATE 的 data。
|
|
59
|
+
"""
|
|
60
|
+
if "op" in raw:
|
|
61
|
+
if raw.get("op") != OP_DISPATCH or raw.get("t") != "MESSAGE_CREATE":
|
|
62
|
+
return None
|
|
63
|
+
data = raw.get("d", {})
|
|
64
|
+
else:
|
|
65
|
+
data = raw
|
|
66
|
+
if not data:
|
|
67
|
+
return None
|
|
68
|
+
author = data.get("author", {})
|
|
69
|
+
if author.get("bot"):
|
|
70
|
+
return None
|
|
71
|
+
if self._bot_user_id and str(author.get("id")) == self._bot_user_id:
|
|
72
|
+
return None
|
|
73
|
+
text = data.get("content", "")
|
|
74
|
+
if not text:
|
|
75
|
+
return None
|
|
76
|
+
is_group = bool(data.get("guild_id"))
|
|
77
|
+
return InboundMessage(
|
|
78
|
+
channel=self.CHANNEL,
|
|
79
|
+
message_id=str(data.get("id", "")),
|
|
80
|
+
text=text,
|
|
81
|
+
sender_id=str(author.get("id", "")),
|
|
82
|
+
sender_name=author.get("username", ""),
|
|
83
|
+
conversation_id=str(data.get("channel_id", "")),
|
|
84
|
+
is_group=is_group,
|
|
85
|
+
raw=data)
|
|
86
|
+
|
|
87
|
+
def send(self, msg: OutboundMessage) -> SendResult:
|
|
88
|
+
"""POST /channels/{channel_id}/messages 发文本。"""
|
|
89
|
+
try:
|
|
90
|
+
import httpx
|
|
91
|
+
except ImportError:
|
|
92
|
+
return SendResult(False, error="缺少 httpx 依赖")
|
|
93
|
+
if not self.bot_token:
|
|
94
|
+
return SendResult(False, error="未配置 Discord bot token")
|
|
95
|
+
payload: dict[str, Any] = {"content": msg.text}
|
|
96
|
+
if msg.reply_to:
|
|
97
|
+
payload["message_reference"] = {"message_id": msg.reply_to}
|
|
98
|
+
url = f"{DISCORD_API_BASE}/channels/{msg.conversation_id}/messages"
|
|
99
|
+
try:
|
|
100
|
+
with httpx.Client() as client:
|
|
101
|
+
resp = client.post(url, json=payload, headers=self._headers(),
|
|
102
|
+
timeout=self.timeout)
|
|
103
|
+
except Exception as e:
|
|
104
|
+
return SendResult(False, error=f"Discord 发送失败: {e}")
|
|
105
|
+
if resp.status_code == 429:
|
|
106
|
+
return SendResult(False, error="Discord 限流(429),请降低发送频率")
|
|
107
|
+
if resp.status_code >= 400:
|
|
108
|
+
return SendResult(False, error=f"Discord API HTTP {resp.status_code}: {resp.text[:200]}")
|
|
109
|
+
try:
|
|
110
|
+
data = resp.json()
|
|
111
|
+
except Exception:
|
|
112
|
+
data = {}
|
|
113
|
+
return SendResult(True, message_id=str(data.get("id", "")))
|
|
114
|
+
|
|
115
|
+
def verify_token(self) -> SendResult:
|
|
116
|
+
"""GET /users/@me 验证 bot token,并记录自己的 user id。"""
|
|
117
|
+
try:
|
|
118
|
+
import httpx
|
|
119
|
+
with httpx.Client() as client:
|
|
120
|
+
resp = client.get(f"{DISCORD_API_BASE}/users/@me",
|
|
121
|
+
headers=self._headers(), timeout=self.timeout)
|
|
122
|
+
if resp.status_code == 200:
|
|
123
|
+
me = resp.json()
|
|
124
|
+
self._bot_user_id = str(me.get("id", ""))
|
|
125
|
+
return SendResult(True, message_id=me.get("username", ""))
|
|
126
|
+
return SendResult(False, error=f"token 无效 (HTTP {resp.status_code})")
|
|
127
|
+
except Exception as e:
|
|
128
|
+
return SendResult(False, error=str(e))
|
|
129
|
+
|
|
130
|
+
def build_identify(self) -> dict:
|
|
131
|
+
"""构造 Gateway IDENTIFY 帧(连接后发送)。"""
|
|
132
|
+
return {
|
|
133
|
+
"op": OP_IDENTIFY,
|
|
134
|
+
"d": {
|
|
135
|
+
"token": self.bot_token,
|
|
136
|
+
"intents": self.intents,
|
|
137
|
+
"properties": {"os": "linux", "browser": "foxclaw", "device": "foxclaw"},
|
|
138
|
+
},
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
async def connect_gateway(self):
|
|
142
|
+
"""Gateway WebSocket 接收循环(异步)。需要 websockets 库。
|
|
143
|
+
|
|
144
|
+
实现 HELLO→IDENTIFY→心跳→DISPATCH 的最小闭环。生产可换 discord.py。
|
|
145
|
+
"""
|
|
146
|
+
try:
|
|
147
|
+
import websockets
|
|
148
|
+
except ImportError:
|
|
149
|
+
raise RuntimeError("需要 websockets 库:pip install websockets")
|
|
150
|
+
import asyncio
|
|
151
|
+
async with websockets.connect(DISCORD_GATEWAY, max_size=None) as ws:
|
|
152
|
+
hello = json.loads(await ws.recv())
|
|
153
|
+
heartbeat_ms = hello["d"]["heartbeat_interval"]
|
|
154
|
+
await ws.send(json.dumps(self.build_identify()))
|
|
155
|
+
self._running = True
|
|
156
|
+
|
|
157
|
+
async def heartbeat():
|
|
158
|
+
while self._running:
|
|
159
|
+
await asyncio.sleep(heartbeat_ms / 1000)
|
|
160
|
+
try:
|
|
161
|
+
await ws.send(json.dumps({"op": OP_HEARTBEAT, "d": None}))
|
|
162
|
+
except Exception:
|
|
163
|
+
break
|
|
164
|
+
|
|
165
|
+
hb_task = asyncio.create_task(heartbeat())
|
|
166
|
+
try:
|
|
167
|
+
async for raw in ws:
|
|
168
|
+
frame = json.loads(raw)
|
|
169
|
+
if frame.get("op") == OP_DISPATCH and frame.get("t") == "MESSAGE_CREATE":
|
|
170
|
+
inbound = self.normalize(frame)
|
|
171
|
+
if inbound and self.on_message:
|
|
172
|
+
self.on_message(inbound)
|
|
173
|
+
finally:
|
|
174
|
+
self._running = False
|
|
175
|
+
hb_task.cancel()
|