foxclaw 0.1.2 → 0.1.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/index.js +1 -8
- package/package.json +1 -1
- package/python/.env.example +8 -0
- package/python/.github/workflows/ci.yml +47 -2
- package/python/CHANGELOG.md +57 -0
- package/python/CHANGELOG_CN.md +57 -0
- package/python/README.md +11 -2
- package/python/README_CN.md +32 -20
- package/python/bench/bm25_audit.py +1 -14
- package/python/bench/bm25_regression.py +0 -1
- package/python/bench/bm25_stress.py +0 -4
- package/python/bench/cd_audit.py +0 -23
- package/python/bench/chaos.py +6 -8
- package/python/bench/e2e_real_models.py +0 -1
- package/python/bench/llm_retry_test.py +2 -9
- package/python/bench/load_test.py +5 -8
- package/python/bench/vector_ivf_upgrade.py +1 -7
- package/python/docs/RELEASE.md +4 -3
- package/python/docs/RUN_CN.md +2 -2
- package/python/evals/governance/cases.py +0 -37
- package/python/evals/governance/run_evals.py +5 -37
- package/python/examples/governance_middleware_demo.py +1 -6
- package/python/examples/langgraph_governance_demo.py +7 -11
- package/python/foxclaw/__init__.py +34 -152
- package/python/foxclaw/agents/base.py +65 -136
- package/python/foxclaw/agents/impl/_helpers.py +2 -10
- package/python/foxclaw/agents/impl/finance.py +4 -21
- package/python/foxclaw/agents/impl/office.py +7 -24
- package/python/foxclaw/agents/impl/supervisor.py +12 -79
- package/python/foxclaw/agents/mixins/_context.py +0 -23
- package/python/foxclaw/agents/mixins/augment.py +10 -16
- package/python/foxclaw/agents/mixins/llm.py +7 -6
- package/python/foxclaw/agents/mixins/memory.py +7 -1
- package/python/foxclaw/agents/mixins/preference.py +58 -8
- package/python/foxclaw/agents/mixins/prompt.py +0 -6
- package/python/foxclaw/agents/mixins/skillbox.py +6 -6
- package/python/foxclaw/agents/mixins/tool_exec.py +16 -42
- package/python/foxclaw/api/__init__.py +18 -77
- package/python/foxclaw/api/_state.py +43 -0
- package/python/foxclaw/api/{app.py → bootstrap.py} +5 -32
- package/python/foxclaw/api/middleware.py +5 -28
- package/python/foxclaw/api/routes/ahs.py +60 -73
- package/python/foxclaw/api/routes/attachments.py +5 -6
- package/python/foxclaw/api/routes/browser.py +7 -11
- package/python/foxclaw/api/routes/domain.py +18 -26
- package/python/foxclaw/api/routes/knowledge.py +3 -21
- package/python/foxclaw/api/routes/llm_admin.py +21 -33
- package/python/foxclaw/api/routes/memory.py +28 -12
- package/python/foxclaw/api/routes/orchestration.py +3 -5
- package/python/foxclaw/api/routes/recovery.py +4 -3
- package/python/foxclaw/api/routes/review.py +2 -11
- package/python/foxclaw/api/routes/search.py +3 -7
- package/python/foxclaw/api/routes/system.py +33 -49
- package/python/foxclaw/api/routes/task.py +31 -49
- package/python/foxclaw/api/routes/ws.py +2 -6
- package/python/foxclaw/api/spine.py +1 -4
- package/python/foxclaw/browser/action/action_executor.py +1 -5
- package/python/foxclaw/browser/action/action_types.py +0 -1
- package/python/foxclaw/browser/action/url_policy.py +1 -5
- package/python/foxclaw/browser/adapter/web_adapter.py +1 -1
- package/python/foxclaw/browser/driver/playwright_driver.py +20 -16
- package/python/foxclaw/browser/perception/dom_extractor.py +0 -1
- package/python/foxclaw/browser/perception/element_indexer.py +1 -2
- package/python/foxclaw/browser/service.py +7 -11
- package/python/foxclaw/capability/embedding_service.py +4 -6
- package/python/foxclaw/channels/__init__.py +9 -11
- package/python/foxclaw/channels/discord.py +4 -13
- package/python/foxclaw/channels/dispatcher.py +2 -27
- package/python/foxclaw/channels/feishu.py +10 -20
- package/python/foxclaw/channels/openclaw_bridge.py +4 -9
- package/python/foxclaw/channels/signing.py +9 -9
- package/python/foxclaw/channels/telegram.py +3 -4
- package/python/foxclaw/channels/weixin.py +7 -17
- package/python/foxclaw/cli.py +42 -13
- package/python/foxclaw/cognitive/reasoner.py +9 -10
- package/python/foxclaw/cognitive/state.py +18 -24
- package/python/foxclaw/core/audit_chain.py +1 -3
- package/python/foxclaw/core/audit_sink.py +3 -3
- package/python/foxclaw/core/compaction.py +8 -26
- package/python/foxclaw/core/config.py +2 -2
- package/python/foxclaw/core/confirm_nonce.py +6 -12
- package/python/foxclaw/core/envfile.py +2 -2
- package/python/foxclaw/core/failure_policy.py +17 -26
- package/python/foxclaw/core/gateway.py +6 -10
- package/python/foxclaw/core/governance_audit.py +3 -7
- package/python/foxclaw/core/idempotency.py +15 -22
- package/python/foxclaw/core/injection_guard.py +10 -29
- package/python/foxclaw/core/memory_adapter.py +30 -57
- package/python/foxclaw/core/metrics.py +4 -5
- package/python/foxclaw/core/model_router.py +36 -148
- package/python/foxclaw/core/paths.py +1 -1
- package/python/foxclaw/core/recovery.py +21 -34
- package/python/foxclaw/core/scheduler.py +23 -26
- package/python/foxclaw/core/secrets.py +0 -3
- package/python/foxclaw/core/sessions.py +4 -6
- package/python/foxclaw/core/settings.py +16 -32
- package/python/foxclaw/core/shared_write.py +11 -23
- package/python/foxclaw/core/steering_queue.py +12 -11
- package/python/foxclaw/core/system_bus.py +1 -1
- package/python/foxclaw/core/thought_chain.py +30 -32
- package/python/foxclaw/core/token_meter.py +9 -13
- package/python/foxclaw/core/tools.py +4 -12
- package/python/foxclaw/core/tracing.py +25 -40
- package/python/foxclaw/core/truth_pipeline.py +5 -9
- package/python/foxclaw/core/update_check.py +149 -0
- package/python/foxclaw/core/vec_recall_adapter.py +11 -37
- package/python/foxclaw/core/verify.py +6 -14
- package/python/foxclaw/core/web_search.py +4 -28
- package/python/foxclaw/finance/evidence.py +14 -18
- package/python/foxclaw/finance/fx.py +1 -3
- package/python/foxclaw/finance/graph.py +9 -18
- package/python/foxclaw/finance/models.py +20 -24
- package/python/foxclaw/finance/nlparse.py +12 -72
- package/python/foxclaw/finance/policy.py +8 -36
- package/python/foxclaw/finance/reasoner.py +3 -11
- package/python/foxclaw/finance/reasoning.py +13 -34
- package/python/foxclaw/finance/reconcile.py +5 -10
- package/python/foxclaw/finance/runtime.py +19 -46
- package/python/foxclaw/governance/middleware.py +13 -21
- package/python/foxclaw/harness/_base.py +2 -8
- package/python/foxclaw/harness/_policy.py +19 -38
- package/python/foxclaw/harness/drift.py +1 -13
- package/python/foxclaw/harness/event_bus.py +2 -5
- package/python/foxclaw/harness/harness.py +9 -97
- package/python/foxclaw/harness/loop_detection.py +11 -18
- package/python/foxclaw/itinerary/amap_client.py +2 -5
- package/python/foxclaw/itinerary/models.py +7 -7
- package/python/foxclaw/itinerary/reasoner.py +1 -6
- package/python/foxclaw/itinerary/reasoning.py +15 -21
- package/python/foxclaw/itinerary/runtime.py +8 -12
- package/python/foxclaw/kernel/capabilities.py +5 -6
- package/python/foxclaw/kernel/policy_gate.py +4 -10
- package/python/foxclaw/kernel/process_manager.py +17 -36
- package/python/foxclaw/kernel/scheduler.py +3 -9
- package/python/foxclaw/kernel/sentinel.py +13 -23
- package/python/foxclaw/kernel/sentinel_client.py +12 -15
- package/python/foxclaw/kernel/spine.py +9 -9
- package/python/foxclaw/knowledge/__init__.py +4 -20
- package/python/foxclaw/knowledge/truth_crystallizer.py +5 -8
- package/python/foxclaw/learning/knowledge_graph.py +4 -9
- package/python/foxclaw/learning/models.py +13 -13
- package/python/foxclaw/learning/reasoner.py +1 -4
- package/python/foxclaw/learning/runtime.py +19 -31
- package/python/foxclaw/learning/spaced_repetition.py +2 -7
- package/python/foxclaw/mcp_server.py +2 -3
- package/python/foxclaw/orchestration/__init__.py +16 -33
- package/python/foxclaw/pipeline/cognitive_pipeline.py +45 -97
- package/python/foxclaw/pipeline/stage_meta.py +21 -0
- package/python/foxclaw/pipeline/supervisor_stages.py +13 -42
- package/python/foxclaw/planning/llm_planner.py +3 -3
- package/python/foxclaw/planning/planner.py +6 -11
- package/python/foxclaw/search/agent_port.py +7 -11
- package/python/foxclaw/search/evidence.py +0 -4
- package/python/foxclaw/search/internal_search.py +3 -5
- package/python/foxclaw/search/models.py +11 -13
- package/python/foxclaw/search/planner.py +3 -3
- package/python/foxclaw/search/retriever.py +1 -1
- package/python/foxclaw/search/runtime.py +1 -8
- package/python/foxclaw/skills/__init__.py +46 -100
- package/python/foxclaw/skills/archive.py +111 -0
- package/python/foxclaw/skills/fetch.py +226 -0
- package/python/foxclaw/skills/hooks_spec.py +21 -25
- package/python/foxclaw/skills/install_cmd.py +216 -0
- package/python/foxclaw/skills/lifecycle.py +16 -42
- package/python/foxclaw/skills/markdown_skill.py +10 -9
- package/python/foxclaw/skills/mcp_client.py +5 -11
- package/python/foxclaw/skills/mcp_store.py +0 -3
- package/python/foxclaw/skills/permission.py +13 -48
- package/python/foxclaw/skills/pipeline.py +21 -53
- package/python/foxclaw/skills/policy.py +4 -10
- package/python/foxclaw/skills/review.py +3 -3
- package/python/frontend/deck/auth.js +0 -7
- package/python/frontend/deck/chat_model_picker.js +0 -54
- package/python/frontend/deck/domain.js +0 -20
- package/python/frontend/deck/main.js +0 -144
- package/python/frontend/deck/markdown.js +0 -4
- package/python/frontend/deck/settings.js +1 -18
- package/python/frontend/deck/state.js +0 -1
- package/python/frontend/deck/util.js +0 -2
- package/python/frontend/deck/visuals.js +0 -22
- package/python/frontend/planet/planet.js +0 -201
- 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 +3 -3
- package/python/frontend/widget_runtime.js +0 -43
- package/python/frontend/widgets.js +0 -57
- package/python/frontend_src/index.html +1 -1
- package/python/frontend_src/package-lock.json +2 -2
- package/python/frontend_src/package.json +1 -1
- package/python/frontend_src/src/App.vue +3 -7
- package/python/frontend_src/src/api.js +0 -7
- package/python/frontend_src/src/components/ChatDock.vue +27 -39
- package/python/frontend_src/src/components/DocViewer.vue +0 -9
- package/python/frontend_src/src/components/Inspector.vue +0 -6
- package/python/frontend_src/src/components/LeftRail.vue +60 -39
- package/python/frontend_src/src/components/MenuPopover.vue +9 -11
- package/python/frontend_src/src/components/MessageStream.vue +18 -30
- package/python/frontend_src/src/components/ModelPicker.vue +0 -5
- package/python/frontend_src/src/components/QuickIngest.vue +0 -6
- package/python/frontend_src/src/components/Stage.vue +71 -25
- package/python/frontend_src/src/components/ToolMenu.vue +0 -3
- package/python/frontend_src/src/components/TopBar.vue +38 -10
- package/python/frontend_src/src/components/panels/ActivityPanel.vue +0 -2
- package/python/frontend_src/src/components/panels/AgentsPanel.vue +2 -5
- package/python/frontend_src/src/components/panels/KnowledgePanel.vue +0 -16
- package/python/frontend_src/src/components/panels/MemoryPanel.vue +0 -1
- package/python/frontend_src/src/components/panels/PanelBox.vue +6 -7
- package/python/frontend_src/src/components/panels/ReviewPanel.vue +0 -16
- package/python/frontend_src/src/components/panels/SettingsPanel.vue +142 -38
- package/python/frontend_src/src/components/panels/TasksPanel.vue +0 -1
- package/python/frontend_src/src/components/panels/ToolsPanel.vue +125 -19
- package/python/frontend_src/src/lib/fmt.js +0 -4
- 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 +0 -14
- package/python/frontend_src/src/main.js +2 -0
- package/python/frontend_src/src/stores/app.js +17 -32
- package/python/frontend_src/src/stores/chat.js +0 -34
- package/python/frontend_src/src/stores/model.js +0 -8
- package/python/frontend_src/src/stores/panels.js +0 -41
- package/python/frontend_src/src/stores/profile.js +31 -0
- package/python/frontend_src/src/stores/system.js +25 -0
- package/python/frontend_src/vite.config.js +0 -7
- package/python/pyproject.toml +6 -3
- package/python/requirements.txt +311 -0
- package/python/tests/conftest.py +0 -22
- package/python/tests/run_all.py +3 -66
- package/python/tests/test_agent_lifecycle.py +4 -38
- package/python/tests/test_ahs_p2.py +4 -25
- package/python/tests/test_anthropic_tools.py +1 -15
- package/python/tests/test_api.py +19 -9
- package/python/tests/test_api_auth.py +1 -5
- package/python/tests/test_attachments.py +9 -25
- package/python/tests/test_audit_chain.py +2 -7
- package/python/tests/test_audit_sink.py +0 -6
- package/python/tests/test_browser.py +2 -6
- package/python/tests/test_browser_request_guard.py +0 -4
- package/python/tests/test_capabilities.py +0 -6
- package/python/tests/test_channel_signing.py +2 -9
- package/python/tests/test_channels.py +1 -11
- package/python/tests/test_config_layering.py +0 -4
- package/python/tests/test_confirm_nonce.py +1 -12
- package/python/tests/test_context_compaction_wiring.py +1 -7
- package/python/tests/test_dispatch.py +0 -6
- package/python/tests/test_domain_evals.py +11 -80
- package/python/tests/test_domain_import.py +0 -6
- package/python/tests/test_domain_reasoners.py +0 -14
- package/python/tests/test_e2e.py +0 -17
- package/python/tests/test_emb_cache.py +7 -17
- package/python/tests/test_embedding_singleton.py +1 -3
- package/python/tests/test_evals_smoke.py +0 -24
- package/python/tests/test_event_spine.py +10 -18
- package/python/tests/test_evidence_semantic.py +1 -14
- package/python/tests/test_evidence_sse.py +3 -6
- package/python/tests/test_file_ingest.py +0 -11
- package/python/tests/test_finance.py +0 -9
- package/python/tests/test_finance_ledger.py +5 -12
- package/python/tests/test_finance_persist.py +8 -12
- package/python/tests/test_gateway.py +3 -22
- package/python/tests/test_governance.py +5 -52
- package/python/tests/test_governance_audit.py +4 -39
- package/python/tests/test_governance_middleware.py +1 -7
- package/python/tests/test_hkc.py +6 -24
- package/python/tests/test_hkc_index_build_manager.py +0 -33
- package/python/tests/test_hkc_manifest_reuse.py +4 -27
- package/python/tests/test_hkc_search_event_sync.py +3 -54
- package/python/tests/test_import_path_guard.py +0 -6
- package/python/tests/test_injection_guard.py +18 -36
- package/python/tests/test_integration.py +5 -29
- package/python/tests/test_itinerary.py +0 -3
- package/python/tests/test_kee_semantic.py +0 -5
- package/python/tests/test_kernel.py +1 -11
- package/python/tests/test_learning.py +3 -12
- package/python/tests/test_llm_admin.py +4 -19
- package/python/tests/test_llm_planner.py +1 -23
- package/python/tests/test_markdown_skills.py +3 -14
- package/python/tests/test_markdown_skills_openclaw.py +4 -9
- package/python/tests/test_mcp_connectors.py +3 -15
- package/python/tests/test_mcp_guardian_discovery.py +2 -6
- package/python/tests/test_mcp_no_memory.py +3 -3
- package/python/tests/test_mcp_policy_gate.py +0 -1
- package/python/tests/test_mcp_server.py +6 -9
- package/python/tests/test_memory_recall.py +1 -4
- package/python/tests/test_metrics.py +0 -8
- package/python/tests/test_multi_chain.py +1 -13
- package/python/tests/test_multimodal.py +5 -18
- package/python/tests/test_negotiation.py +3 -13
- package/python/tests/test_orchestration.py +1 -11
- package/python/tests/test_orchestration_decoupled.py +0 -6
- package/python/tests/test_p3_p5_hardening.py +3 -17
- package/python/tests/test_parallel_dispatch.py +4 -12
- package/python/tests/test_pipeline.py +17 -58
- package/python/tests/test_plan_visible.py +4 -4
- package/python/tests/test_planner.py +0 -7
- package/python/tests/test_policy_gate.py +2 -3
- package/python/tests/test_profile.py +67 -0
- package/python/tests/test_readme_claims.py +163 -0
- package/python/tests/test_reasoner.py +2 -17
- package/python/tests/test_release_check.py +2 -13
- package/python/tests/test_resume_checkpoint.py +7 -15
- package/python/tests/test_routing.py +2 -12
- package/python/tests/test_scheduler.py +7 -10
- package/python/tests/test_search.py +1 -11
- package/python/tests/test_secrets.py +4 -10
- package/python/tests/test_semantic.py +1 -10
- package/python/tests/test_sessions.py +3 -4
- package/python/tests/test_settings.py +2 -18
- package/python/tests/test_shadow_learning.py +3 -43
- package/python/tests/test_shared_write.py +0 -7
- package/python/tests/test_skill_taint_gate.py +1 -10
- package/python/tests/test_skills.py +2 -18
- package/python/tests/test_skills_fetch.py +282 -0
- package/python/tests/test_stats_and_events.py +2 -5
- package/python/tests/test_streaming.py +2 -7
- package/python/tests/test_supervisor_pipeline.py +4 -5
- package/python/tests/test_supervisor_triage.py +4 -10
- package/python/tests/test_taint.py +2 -7
- package/python/tests/test_task_stream.py +1 -12
- package/python/tests/test_thinking_sse.py +0 -1
- package/python/tests/test_tool_center.py +1 -17
- package/python/tests/test_tool_parallel.py +2 -2
- package/python/tests/test_tool_params.py +1 -5
- package/python/tests/test_tool_use.py +0 -13
- package/python/tests/test_toplevel_concurrency.py +3 -4
- package/python/tests/test_tracing.py +7 -41
- package/python/tests/test_truth_crystallizer.py +4 -6
- package/python/tests/test_truth_flow.py +2 -7
- package/python/tests/test_update_check.py +95 -0
- package/python/tests/test_verify_evidence.py +8 -21
- package/python/tests/test_version_sync.py +35 -6
- package/python/tests/test_web_trigger.py +0 -17
- package/python/tools/check_faiss_type.py +0 -1
- package/python/tools/check_log.ps1 +0 -8
- package/python/tools/mypy_baseline.txt +12 -4
- package/python/tools/mypy_ratchet.py +52 -6
- package/python/tools/pack.py +0 -5
- package/python/tools/release_check.py +7 -7
- package/python/tools/restart_foxclaw.ps1 +5 -8
- package/python/tools/shared_write_audit.py +1 -15
- package/python/tools/size_check.py +1 -1
- package/python/tools/strip_comments.py +228 -0
- package/python/frontend/v3/assets/index-BD7VPbMV.js +0 -33
- package/python/frontend/v3/assets/index-CAvnYWGH.css +0 -1
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
"""系统状态 / 模型路由 / 认知调度 / 事件总线 / 真值 / 进程 / 思考链 / Harness / 探活 / 监控与追踪。
|
|
2
2
|
|
|
3
|
-
从 foxclaw/api.py 原样搬移(纯搬移,零行为变更)。共享 OS 实例经 `_api.
|
|
4
|
-
晚绑定读取(
|
|
3
|
+
从 foxclaw/api.py 原样搬移(纯搬移,零行为变更)。共享 OS 实例经 `_api.current_os()`
|
|
4
|
+
晚绑定读取(状态在 `_state`,测试可重绑 `_state._os_instance`);路由用 `@app` 装饰器在 import 时注册。
|
|
5
5
|
"""
|
|
6
6
|
|
|
7
7
|
import os
|
|
@@ -16,21 +16,19 @@ from foxclaw import __version__
|
|
|
16
16
|
import foxclaw.api as _api
|
|
17
17
|
from .. import app
|
|
18
18
|
from ..middleware import _gateway_on, _GATEWAY
|
|
19
|
-
from ...core import settings
|
|
19
|
+
from ...core import settings, update_check
|
|
20
20
|
|
|
21
21
|
|
|
22
|
-
# ── 系统状态 ───────────────────────────────────────────────
|
|
23
|
-
|
|
24
22
|
@app.get("/api/status")
|
|
25
23
|
async def get_system_status():
|
|
26
24
|
"""获取所有 Agent 的状态"""
|
|
27
|
-
return _api.
|
|
25
|
+
return _api.os_instance().get_all_status()
|
|
28
26
|
|
|
29
27
|
|
|
30
28
|
@app.get("/api/tokens")
|
|
31
29
|
async def get_token_summary():
|
|
32
30
|
"""Token 消耗摘要(成本可观测性):总量、按 Agent、按模型。"""
|
|
33
|
-
meter = getattr(_api.
|
|
31
|
+
meter = getattr(_api.current_os(), "token_meter", None)
|
|
34
32
|
if meter is None:
|
|
35
33
|
return {"error": "token meter 未启用"}
|
|
36
34
|
return {
|
|
@@ -43,18 +41,16 @@ async def get_token_summary():
|
|
|
43
41
|
@app.get("/api/tokens/report")
|
|
44
42
|
async def get_token_report():
|
|
45
43
|
"""Token 消耗的人类可读文本报告。"""
|
|
46
|
-
meter = getattr(_api.
|
|
44
|
+
meter = getattr(_api.current_os(), "token_meter", None)
|
|
47
45
|
if meter is None:
|
|
48
46
|
return {"error": "token meter 未启用"}
|
|
49
47
|
return {"report": meter.report()}
|
|
50
48
|
|
|
51
49
|
|
|
52
|
-
# ── 模型路由与系统信息 ─────────────────────────────────────
|
|
53
|
-
|
|
54
50
|
@app.get("/api/model")
|
|
55
51
|
async def get_model():
|
|
56
52
|
"""当前模型路由状态 + 可用 provider 列表。"""
|
|
57
|
-
return _api.
|
|
53
|
+
return _api.os_instance().router.info()
|
|
58
54
|
|
|
59
55
|
|
|
60
56
|
@app.get("/api/config/amap")
|
|
@@ -69,24 +65,24 @@ async def get_amap_config():
|
|
|
69
65
|
@app.get("/api/system/info")
|
|
70
66
|
async def get_system_info():
|
|
71
67
|
"""系统级只读信息:版本、运行参数、各 Agent、模块状态(供设置面板展示)。"""
|
|
68
|
+
update_check.maybe_refresh()
|
|
72
69
|
agents = {}
|
|
73
|
-
for aid, ag in _api.
|
|
70
|
+
for aid, ag in _api.os_instance()._agents.items():
|
|
74
71
|
h = ag.harness
|
|
75
72
|
agents[aid] = {
|
|
76
73
|
"name": getattr(ag, "AGENT_NAME", aid),
|
|
77
74
|
"shadow_mode": h.is_shadow_mode,
|
|
78
75
|
"shadow_rounds": h.shadow_rounds,
|
|
79
76
|
"run_count": getattr(h, "_run_count", 0),
|
|
80
|
-
# 降级可见性(B 类修复):语义维度停用时,漂移检测只剩 tool/scope 两维,
|
|
81
|
-
# 跑题不会被拦截 —— 前端需据此提示,不能只在服务端日志里静默。
|
|
82
77
|
"semantic_degraded": getattr(h, "semantic_degraded", False),
|
|
83
78
|
"embedding_mode": getattr(getattr(h, "sem_drift", None), "mode", "unknown"),
|
|
84
79
|
}
|
|
85
80
|
return {
|
|
86
81
|
"version": __version__,
|
|
82
|
+
"update": update_check.status(),
|
|
87
83
|
"data_dir": os.environ.get("FOXCLAW_DATA_DIR", "./foxclaw_data"),
|
|
88
84
|
"shadow_rounds": settings.get_int("FOXCLAW_SHADOW_ROUNDS"),
|
|
89
|
-
"model": _api.
|
|
85
|
+
"model": _api.os_instance().router.info(),
|
|
90
86
|
"amap": {
|
|
91
87
|
"web_key": bool(os.environ.get("AMAP_MAPS_API_KEY")),
|
|
92
88
|
"js_key": bool(os.environ.get("AMAP_JS_KEY")),
|
|
@@ -105,21 +101,19 @@ class ModelSwitchPayload(BaseModel):
|
|
|
105
101
|
async def switch_model(p: ModelSwitchPayload):
|
|
106
102
|
"""运行时切换模型 provider(多模型路由核心能力)。"""
|
|
107
103
|
try:
|
|
108
|
-
return _api.
|
|
104
|
+
return _api.os_instance().router.switch(p.provider, p.model)
|
|
109
105
|
except Exception as e:
|
|
110
106
|
raise HTTPException(400, f"切换失败: {e}")
|
|
111
107
|
|
|
112
108
|
|
|
113
109
|
@app.get("/api/agents/{agent_id}/status")
|
|
114
110
|
async def get_agent_status(agent_id: str):
|
|
115
|
-
agent = _api.
|
|
111
|
+
agent = _api.os_instance().get_agent(agent_id)
|
|
116
112
|
if not agent:
|
|
117
113
|
raise HTTPException(404, f"Agent {agent_id} 不存在")
|
|
118
114
|
return agent.get_status()
|
|
119
115
|
|
|
120
116
|
|
|
121
|
-
# ── 认知调度 / 事件总线 / 真值 / 进程 / 认知状态 ─────────────
|
|
122
|
-
|
|
123
117
|
class SchedulePreviewPayload(BaseModel):
|
|
124
118
|
task: str
|
|
125
119
|
|
|
@@ -136,14 +130,14 @@ async def scheduler_preview(payload: SchedulePreviewPayload):
|
|
|
136
130
|
@app.get("/api/scheduler/stats")
|
|
137
131
|
async def scheduler_stats():
|
|
138
132
|
"""调度器记账:并发配额 + 提交/运行/完成/失败/峰值(可观测性)。"""
|
|
139
|
-
return _api.
|
|
133
|
+
return _api.os_instance().scheduler.stats()
|
|
140
134
|
|
|
141
135
|
|
|
142
136
|
@app.get("/api/bus/history")
|
|
143
|
-
async def bus_history(channel: str = None, limit: int = 30):
|
|
137
|
+
async def bus_history(channel: str | None = None, limit: int = 30):
|
|
144
138
|
"""系统事件总线历史:观测执行→审计→记忆的真值流转。"""
|
|
145
139
|
try:
|
|
146
|
-
return {"events": _api.
|
|
140
|
+
return {"events": _api.os_instance().system_bus.history(channel, limit)}
|
|
147
141
|
except Exception:
|
|
148
142
|
return {"events": []}
|
|
149
143
|
|
|
@@ -151,16 +145,16 @@ async def bus_history(channel: str = None, limit: int = 30):
|
|
|
151
145
|
@app.get("/api/truth/last")
|
|
152
146
|
async def truth_last():
|
|
153
147
|
"""真值管线最近一次的 trace 与记忆更新。"""
|
|
154
|
-
tp = _api.
|
|
148
|
+
tp = _api.os_instance().truth_pipeline
|
|
155
149
|
return {"last_trace": tp.last_trace, "last_memory_update": tp.last_memory_update}
|
|
156
150
|
|
|
157
151
|
|
|
158
152
|
@app.get("/api/processes")
|
|
159
|
-
async def list_processes(state: str = None, limit: int = 30):
|
|
153
|
+
async def list_processes(state: str | None = None, limit: int = 30):
|
|
160
154
|
"""进程列表:观测任务作为"进程"的生命周期状态。"""
|
|
161
155
|
try:
|
|
162
|
-
return {"processes": _api.
|
|
163
|
-
"stats": _api.
|
|
156
|
+
return {"processes": _api.os_instance().process_manager.list(state, limit),
|
|
157
|
+
"stats": _api.os_instance().process_manager.stats()}
|
|
164
158
|
except Exception:
|
|
165
159
|
return {"processes": [], "stats": {}}
|
|
166
160
|
|
|
@@ -168,7 +162,7 @@ async def list_processes(state: str = None, limit: int = 30):
|
|
|
168
162
|
@app.get("/api/processes/{process_id}")
|
|
169
163
|
async def get_process(process_id: str):
|
|
170
164
|
"""单个进程详情:含完整状态历史(state_history)。"""
|
|
171
|
-
rec = _api.
|
|
165
|
+
rec = _api.os_instance().process_manager.get(process_id)
|
|
172
166
|
return rec.to_dict() if rec else {"error": "not_found"}
|
|
173
167
|
|
|
174
168
|
|
|
@@ -176,21 +170,18 @@ async def get_process(process_id: str):
|
|
|
176
170
|
async def cognitive_state():
|
|
177
171
|
"""认知状态快照:系统此刻在想什么(目标/焦点/注意力/激活记忆/活跃进程)。"""
|
|
178
172
|
try:
|
|
179
|
-
return _api.
|
|
173
|
+
return _api.os_instance().cognitive_state.snapshot()
|
|
180
174
|
except Exception as e:
|
|
181
175
|
return {"error": str(e)}
|
|
182
176
|
|
|
183
177
|
|
|
184
|
-
# ── ThoughtChain 查询 ──────────────────────────────────────
|
|
185
|
-
|
|
186
178
|
@app.get("/api/thoughtchain/{agent_id}/chain")
|
|
187
|
-
async def get_thought_chain(agent_id: str, session_id: str = None):
|
|
179
|
+
async def get_thought_chain(agent_id: str, session_id: str | None = None):
|
|
188
180
|
"""获取 Agent 的推理链"""
|
|
189
|
-
tc = _api.
|
|
181
|
+
tc = _api.os_instance().thought_chain
|
|
190
182
|
if session_id:
|
|
191
183
|
nodes = tc.get_chain(session_id, agent_id)
|
|
192
184
|
else:
|
|
193
|
-
# 返回最近 20 个节点
|
|
194
185
|
nodes = tc.get_blocked_nodes(agent_id, limit=20)
|
|
195
186
|
return {"nodes": [n.to_dict() for n in nodes]}
|
|
196
187
|
|
|
@@ -198,7 +189,7 @@ async def get_thought_chain(agent_id: str, session_id: str = None):
|
|
|
198
189
|
@app.get("/api/thoughtchain/{agent_id}/blocked")
|
|
199
190
|
async def get_blocked_nodes(agent_id: str, limit: int = 20):
|
|
200
191
|
"""获取被 Harness 截断的节点(审计用)"""
|
|
201
|
-
tc = _api.
|
|
192
|
+
tc = _api.os_instance().thought_chain
|
|
202
193
|
nodes = tc.get_blocked_nodes(agent_id, limit=limit)
|
|
203
194
|
return {"blocked": [n.to_dict() for n in nodes], "count": len(nodes)}
|
|
204
195
|
|
|
@@ -206,7 +197,7 @@ async def get_blocked_nodes(agent_id: str, limit: int = 20):
|
|
|
206
197
|
@app.get("/api/thoughtchain/{agent_id}/trend")
|
|
207
198
|
async def get_drift_trend(agent_id: str, limit: int = 100):
|
|
208
199
|
"""获取漂移趋势数据(前端可视化用)"""
|
|
209
|
-
tc = _api.
|
|
200
|
+
tc = _api.os_instance().thought_chain
|
|
210
201
|
trend = tc.get_delta_trend(agent_id, limit=limit)
|
|
211
202
|
return {"trend": trend, "agent_id": agent_id}
|
|
212
203
|
|
|
@@ -214,15 +205,13 @@ async def get_drift_trend(agent_id: str, limit: int = 100):
|
|
|
214
205
|
@app.get("/api/thoughtchain/{agent_id}/summary")
|
|
215
206
|
async def get_thought_summary(agent_id: str):
|
|
216
207
|
"""获取 ThoughtChain 健康摘要"""
|
|
217
|
-
return _api.
|
|
218
|
-
|
|
208
|
+
return _api.os_instance().thought_chain.get_summary(agent_id)
|
|
219
209
|
|
|
220
|
-
# ── Harness 状态 ───────────────────────────────────────────
|
|
221
210
|
|
|
222
211
|
@app.get("/api/harness/{agent_id}/status")
|
|
223
212
|
async def get_harness_status(agent_id: str):
|
|
224
213
|
"""获取 Harness 状态:阈值、shadow mode、policy 摘要"""
|
|
225
|
-
agent = _api.
|
|
214
|
+
agent = _api.os_instance().get_agent(agent_id)
|
|
226
215
|
if not agent:
|
|
227
216
|
raise HTTPException(404, f"Agent {agent_id} 不存在")
|
|
228
217
|
return {
|
|
@@ -236,15 +225,12 @@ async def get_harness_status(agent_id: str):
|
|
|
236
225
|
@app.get("/api/harness/{agent_id}/policy")
|
|
237
226
|
async def get_policy(agent_id: str):
|
|
238
227
|
"""获取 Agent 的 Policy YAML(完整内容)"""
|
|
239
|
-
agent = _api.
|
|
228
|
+
agent = _api.os_instance().get_agent(agent_id)
|
|
240
229
|
if not agent:
|
|
241
230
|
raise HTTPException(404, f"Agent {agent_id} 不存在")
|
|
242
231
|
return agent.harness._policy
|
|
243
232
|
|
|
244
233
|
|
|
245
|
-
# ── 健康探活 ───────────────────────────────────────────────
|
|
246
|
-
# 标准探活端点(开放,不在鉴权前缀内、不触发 HKC 装配)。星球 UI 的 connect() 用它探活,
|
|
247
|
-
# 也供部署/监控用。放在静态挂载("/")之前,否则会被 StaticFiles 的 catch-all 吞成 404。
|
|
248
234
|
@app.get("/health")
|
|
249
235
|
async def health():
|
|
250
236
|
"""存活探针(liveness):浅、快,不触发子系统。"""
|
|
@@ -255,7 +241,7 @@ async def health():
|
|
|
255
241
|
async def health_ready():
|
|
256
242
|
"""就绪探针(readiness):逐子系统深度检查。**开放**(探针无需鉴权),
|
|
257
243
|
组件只回 ok/stub/down,不泄露 provider 名等细节。不触发 HKC 惰性初始化。"""
|
|
258
|
-
o = _api.
|
|
244
|
+
o = _api.current_os()
|
|
259
245
|
comps = {}
|
|
260
246
|
comps["hmr"] = "ok" if getattr(o, "hmr", None) is not None else "down"
|
|
261
247
|
_router = getattr(o, "router", None)
|
|
@@ -264,9 +250,9 @@ async def health_ready():
|
|
|
264
250
|
elif getattr(_router, "is_stub", True):
|
|
265
251
|
comps["router"] = "stub"
|
|
266
252
|
else:
|
|
267
|
-
comps["router"] = "ok"
|
|
253
|
+
comps["router"] = "ok"
|
|
268
254
|
comps["scheduler"] = "ok" if getattr(o, "scheduler", None) is not None else "down"
|
|
269
|
-
hkc_svc = getattr(o, "_hkc_service", None)
|
|
255
|
+
hkc_svc = getattr(o, "_hkc_service", None)
|
|
270
256
|
comps["hkc"] = ("ok" if (hkc_svc is not None and getattr(hkc_svc, "available", False))
|
|
271
257
|
else ("uninitialized" if hkc_svc is None else "degraded"))
|
|
272
258
|
comps["mcp_server"] = "on" if getattr(o, "mcp_manager", None) is not None else "off"
|
|
@@ -274,8 +260,6 @@ async def health_ready():
|
|
|
274
260
|
return {"status": "degraded" if down else "ok", "components": comps, "version": __version__}
|
|
275
261
|
|
|
276
262
|
|
|
277
|
-
# ── 监控 / 追踪 / 网关 ──────────────────────────────────────
|
|
278
|
-
|
|
279
263
|
@app.get("/api/metrics")
|
|
280
264
|
async def metrics_json():
|
|
281
265
|
"""可观测:各端点/LLM 的计数、错误率、延迟分位(p50/p95/avg)+ uptime。"""
|
|
@@ -285,7 +269,7 @@ async def metrics_json():
|
|
|
285
269
|
|
|
286
270
|
@app.get("/api/traces")
|
|
287
271
|
async def list_traces(limit: int = 30, persisted: bool = False,
|
|
288
|
-
slow_ms: float = None, errors_only: bool = False):
|
|
272
|
+
slow_ms: float | None = None, errors_only: bool = False):
|
|
289
273
|
"""请求链路。默认看内存里最近若干条;persisted=1 查**已落库的历史**
|
|
290
274
|
(可加 slow_ms / errors_only 过滤)——「找出昨天那次慢请求」走这里。"""
|
|
291
275
|
from ...core.tracing import TRACES
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"""任务执行 / 流式直答 / 流式任务 路由与多模态护栏。
|
|
2
2
|
|
|
3
|
-
从 foxclaw/api.py 原样搬移(纯搬移,零行为变更)。共享 OS 实例经 `_api.
|
|
3
|
+
从 foxclaw/api.py 原样搬移(纯搬移,零行为变更)。共享 OS 实例经 `_api.current_os()`
|
|
4
4
|
晚绑定读取;路由用 `@app` 装饰器在 import 时注册。流式限流器 `_STREAM_LIMITER` 为
|
|
5
5
|
模块级单例,`__init__.py` 会 re-export(测试复位其桶容量)。
|
|
6
6
|
"""
|
|
@@ -19,18 +19,14 @@ from ...mcp_server import _RateLimiter as _RL
|
|
|
19
19
|
from ...core import settings
|
|
20
20
|
|
|
21
21
|
|
|
22
|
-
# ── 请求模型 ───────────────────────────────────────────────
|
|
23
|
-
|
|
24
22
|
class TaskRequest(BaseModel):
|
|
25
23
|
task: str
|
|
26
24
|
agent_id: str = "supervisor"
|
|
27
25
|
context: dict = {}
|
|
28
|
-
images: list = []
|
|
29
|
-
history: list = []
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
attachments: list = [] # 聊天附件:[{name,size,mime,url,text}];text 为提取正文(可为空),
|
|
33
|
-
# 仅 /chat/stream 消费;随问题注入 LLM + 落盘会话供回看
|
|
26
|
+
images: list = []
|
|
27
|
+
history: list = []
|
|
28
|
+
session_id: str = ""
|
|
29
|
+
attachments: list = []
|
|
34
30
|
|
|
35
31
|
|
|
36
32
|
class GoalRequest(BaseModel):
|
|
@@ -40,8 +36,6 @@ class GoalRequest(BaseModel):
|
|
|
40
36
|
entities: list[str] = []
|
|
41
37
|
|
|
42
38
|
|
|
43
|
-
# ── 任务执行 ───────────────────────────────────────────────
|
|
44
|
-
|
|
45
39
|
@app.post("/api/task")
|
|
46
40
|
async def run_task(req: TaskRequest):
|
|
47
41
|
"""执行任务,返回结果 + ThoughtChain 摘要。
|
|
@@ -53,33 +47,31 @@ async def run_task(req: TaskRequest):
|
|
|
53
47
|
同一线程池,故无嵌套线程池死锁;
|
|
54
48
|
· harness 累积态已在 harness.py 用 RLock 保护(嵌入在锁外,故并行不受锁影响)。"""
|
|
55
49
|
def _work():
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
lambda: asyncio.run(_api._os_instance.run(req.task, req.agent_id,
|
|
50
|
+
return _api.os_instance().scheduler.run(
|
|
51
|
+
lambda: asyncio.run(_api.os_instance().run(req.task, req.agent_id,
|
|
59
52
|
context=req.context)),
|
|
60
53
|
agent_id=req.agent_id, task=req.task)
|
|
61
54
|
return await asyncio.to_thread(_work)
|
|
62
55
|
|
|
63
56
|
|
|
64
|
-
|
|
65
|
-
_STREAM_LIMITER = _RL(settings.get_int("FOXCLAW_STREAM_RATE")) # 次/分
|
|
57
|
+
_STREAM_LIMITER = _RL(settings.get_int("FOXCLAW_STREAM_RATE"))
|
|
66
58
|
_STREAM_SEM = _threading.Semaphore(settings.get_int("FOXCLAW_STREAM_CONCURRENCY"))
|
|
67
59
|
|
|
68
60
|
|
|
69
61
|
_MAX_IMAGES = 6
|
|
70
|
-
_MAX_IMG_B64 = 10 * 1024 * 1024
|
|
71
|
-
_MAX_IMG_TOTAL = 20 * 1024 * 1024
|
|
62
|
+
_MAX_IMG_B64 = 10 * 1024 * 1024
|
|
63
|
+
_MAX_IMG_TOTAL = 20 * 1024 * 1024
|
|
72
64
|
_ALLOWED_IMG_MT = {"image/png", "image/jpeg", "image/webp", "image/gif"}
|
|
73
65
|
|
|
74
|
-
_MAX_ATTACHMENTS = 6
|
|
75
|
-
_ATTACH_TEXT_MAX = 20000
|
|
76
|
-
_ATTACH_TEXT_TOTAL = 60000
|
|
66
|
+
_MAX_ATTACHMENTS = 6
|
|
67
|
+
_ATTACH_TEXT_MAX = 20000
|
|
68
|
+
_ATTACH_TEXT_TOTAL = 60000
|
|
77
69
|
|
|
78
70
|
|
|
79
71
|
def _sanitize_images(images):
|
|
80
72
|
"""校验/裁剪前端传来的多模态图片:限数量、限单图大小、限累计总量、白名单类型。
|
|
81
73
|
防超大 payload/OOM(前端已缩放,这是对直连/恶意客户端的后端自保)。"""
|
|
82
|
-
out = []
|
|
74
|
+
out: list[dict] = []
|
|
83
75
|
total = 0
|
|
84
76
|
if not isinstance(images, list):
|
|
85
77
|
return out
|
|
@@ -92,7 +84,7 @@ def _sanitize_images(images):
|
|
|
92
84
|
continue
|
|
93
85
|
if not isinstance(d, str) or not d or len(d) > _MAX_IMG_B64:
|
|
94
86
|
continue
|
|
95
|
-
if total + len(d) > _MAX_IMG_TOTAL:
|
|
87
|
+
if total + len(d) > _MAX_IMG_TOTAL:
|
|
96
88
|
break
|
|
97
89
|
total += len(d)
|
|
98
90
|
out.append({"media_type": mt, "data": d})
|
|
@@ -103,7 +95,7 @@ def _sanitize_attachments(attachments):
|
|
|
103
95
|
"""校验/裁剪前端传来的聊天附件:限数量、字段清洗、正文长度上限 + 累计上限。
|
|
104
96
|
附件是前端经 /api/upload 上传后带回的引用({name,size,mime,url,text});这里只做
|
|
105
97
|
直连/恶意客户端的自保,不信任 text 之外任何字段用于文件系统操作(下载按 url 里的 id 白名单)。"""
|
|
106
|
-
out = []
|
|
98
|
+
out: list[dict] = []
|
|
107
99
|
total = 0
|
|
108
100
|
if not isinstance(attachments, list):
|
|
109
101
|
return out
|
|
@@ -114,11 +106,12 @@ def _sanitize_attachments(attachments):
|
|
|
114
106
|
text = str(a.get("text") or "")[:_ATTACH_TEXT_MAX]
|
|
115
107
|
if not name and not text:
|
|
116
108
|
continue
|
|
117
|
-
if total + len(text) > _ATTACH_TEXT_TOTAL:
|
|
109
|
+
if total + len(text) > _ATTACH_TEXT_TOTAL:
|
|
118
110
|
text = text[:_ATTACH_TEXT_TOTAL - total]
|
|
119
111
|
total += len(text)
|
|
120
|
-
try:
|
|
121
|
-
|
|
112
|
+
try:
|
|
113
|
+
raw_size = a.get("size")
|
|
114
|
+
size = int(raw_size) if raw_size else 0
|
|
122
115
|
except (TypeError, ValueError):
|
|
123
116
|
size = 0
|
|
124
117
|
out.append({
|
|
@@ -142,12 +135,12 @@ async def chat_stream(req: TaskRequest, request: Request):
|
|
|
142
135
|
yield "data: " + json.dumps({"error": "rate limited"}) + "\n\n"
|
|
143
136
|
return StreamingResponse(_rl(), media_type="text/event-stream")
|
|
144
137
|
|
|
145
|
-
agent = _api.
|
|
138
|
+
agent = _api.os_instance()._agents.get(req.agent_id) or _api.os_instance().supervisor
|
|
146
139
|
imgs = _sanitize_images(req.images)
|
|
147
140
|
atts = _sanitize_attachments(req.attachments)
|
|
148
141
|
|
|
149
142
|
async def async_sse():
|
|
150
|
-
if not _STREAM_SEM.acquire(blocking=False):
|
|
143
|
+
if not _STREAM_SEM.acquire(blocking=False):
|
|
151
144
|
yield "data: " + json.dumps({"error": "busy, try again later"}) + "\n\n"
|
|
152
145
|
return
|
|
153
146
|
q: asyncio.Queue = asyncio.Queue()
|
|
@@ -158,12 +151,9 @@ async def chat_stream(req: TaskRequest, request: Request):
|
|
|
158
151
|
_parts = []
|
|
159
152
|
|
|
160
153
|
def _on_thinking(delta: str) -> None:
|
|
161
|
-
# 推理走独立事件,不混进正文:前端折叠展示,正文仍只收 {"delta"}。
|
|
162
154
|
loop.call_soon_threadsafe(q.put_nowait, {"thinking": delta})
|
|
163
155
|
|
|
164
156
|
def _on_evidence(evs: list) -> None:
|
|
165
|
-
# 执行证据(核验结论 + 来源链接/文件)整块送:前端挂在答案下方供核对,
|
|
166
|
-
# 不进正文(正文是给用户读的答案,证据是给人查的凭据)。
|
|
167
157
|
loop.call_soon_threadsafe(q.put_nowait, {"evidence": evs})
|
|
168
158
|
|
|
169
159
|
try:
|
|
@@ -177,22 +167,21 @@ async def chat_stream(req: TaskRequest, request: Request):
|
|
|
177
167
|
except Exception as e:
|
|
178
168
|
loop.call_soon_threadsafe(q.put_nowait, {"error": str(e)})
|
|
179
169
|
finally:
|
|
180
|
-
# 落盘会话历史(问 + 答),供"会话/所有会话"回看。断连中止也保留已生成部分。
|
|
181
170
|
if req.session_id:
|
|
182
171
|
try:
|
|
183
|
-
_api.
|
|
172
|
+
_api.os_instance().sessions.append(req.session_id, "user", req.task, attachments=atts)
|
|
184
173
|
_reply = "".join(_parts)
|
|
185
174
|
if _reply:
|
|
186
|
-
_api.
|
|
175
|
+
_api.os_instance().sessions.append(req.session_id, "assistant", _reply)
|
|
187
176
|
except Exception:
|
|
188
177
|
pass
|
|
189
|
-
loop.call_soon_threadsafe(q.put_nowait, None)
|
|
178
|
+
loop.call_soon_threadsafe(q.put_nowait, None)
|
|
190
179
|
|
|
191
180
|
_threading.Thread(target=_producer, daemon=True, name="chat-stream").start()
|
|
192
181
|
try:
|
|
193
182
|
while True:
|
|
194
183
|
if await request.is_disconnected():
|
|
195
|
-
stop.set(); break
|
|
184
|
+
stop.set(); break
|
|
196
185
|
try:
|
|
197
186
|
item = await asyncio.wait_for(q.get(), timeout=0.5)
|
|
198
187
|
except asyncio.TimeoutError:
|
|
@@ -200,15 +189,11 @@ async def chat_stream(req: TaskRequest, request: Request):
|
|
|
200
189
|
if item is None:
|
|
201
190
|
break
|
|
202
191
|
if "thinking" in item:
|
|
203
|
-
# 命名事件(SSE 规范):event: thinking + data。老客户端只认 data: 行也不会被
|
|
204
|
-
# 误导成正文(delta 字段不存在);新前端按事件名折叠展示。
|
|
205
192
|
yield ("event: thinking\ndata: "
|
|
206
193
|
+ json.dumps({"thinking": item["thinking"]}, ensure_ascii=False)
|
|
207
194
|
+ "\n\n")
|
|
208
195
|
continue
|
|
209
196
|
if "evidence" in item:
|
|
210
|
-
# 与 thinking 同理走命名事件:老客户端拿不到这个事件名也不会把凭据
|
|
211
|
-
# 当成正文拼进回答。
|
|
212
197
|
yield ("event: evidence\ndata: "
|
|
213
198
|
+ json.dumps({"evidence": item["evidence"]}, ensure_ascii=False)
|
|
214
199
|
+ "\n\n")
|
|
@@ -235,16 +220,13 @@ async def task_stream(req: TaskRequest, request: Request):
|
|
|
235
220
|
yield "data: " + json.dumps({"error": "rate limited"}) + "\n\n"
|
|
236
221
|
return StreamingResponse(_rl(), media_type="text/event-stream")
|
|
237
222
|
|
|
238
|
-
agent = _api.
|
|
239
|
-
# 【P3-3】P1-1 落地后主管走统一 pipeline(supervisor_stages),支持 run_task_stream,
|
|
240
|
-
# 不再回落 assistant(助手已下放为技能)。未知 agent_id 仍回落主管兼容旧客户端
|
|
241
|
-
# (旧前端可能还传 "assistant"/"admin"),但打 warning 不静默。
|
|
223
|
+
agent = _api.os_instance()._agents.get(req.agent_id)
|
|
242
224
|
if agent is None:
|
|
243
225
|
print(f"[Foxclaw API] ⚠ /api/task/stream 收到未知 agent_id={req.agent_id},回落 supervisor")
|
|
244
|
-
agent = _api.
|
|
226
|
+
agent = _api.os_instance().supervisor
|
|
245
227
|
|
|
246
228
|
async def async_sse():
|
|
247
|
-
if not _STREAM_SEM.acquire(blocking=False):
|
|
229
|
+
if not _STREAM_SEM.acquire(blocking=False):
|
|
248
230
|
yield "data: " + json.dumps({"error": "busy, try again later"}) + "\n\n"
|
|
249
231
|
return
|
|
250
232
|
q: asyncio.Queue = asyncio.Queue()
|
|
@@ -259,13 +241,13 @@ async def task_stream(req: TaskRequest, request: Request):
|
|
|
259
241
|
except Exception as e:
|
|
260
242
|
loop.call_soon_threadsafe(q.put_nowait, {"error": str(e)})
|
|
261
243
|
finally:
|
|
262
|
-
loop.call_soon_threadsafe(q.put_nowait, None)
|
|
244
|
+
loop.call_soon_threadsafe(q.put_nowait, None)
|
|
263
245
|
|
|
264
246
|
_threading.Thread(target=_producer, daemon=True, name="task-stream-sse").start()
|
|
265
247
|
try:
|
|
266
248
|
while True:
|
|
267
249
|
if await request.is_disconnected():
|
|
268
|
-
stop.set(); break
|
|
250
|
+
stop.set(); break
|
|
269
251
|
try:
|
|
270
252
|
item = await asyncio.wait_for(q.get(), timeout=0.5)
|
|
271
253
|
except asyncio.TimeoutError:
|
|
@@ -45,21 +45,17 @@ async def harness_websocket(websocket: WebSocket):
|
|
|
45
45
|
"""
|
|
46
46
|
api_key = os.environ.get("FOXCLAW_API_KEY", "").strip()
|
|
47
47
|
if api_key:
|
|
48
|
-
# 【P3-loopback】与 HTTP 中间件同口径:本机(127.0.0.1 / ::1)WS 客户端免 ?key=
|
|
49
|
-
# 设 FOXCLAW_LOOPBACK_BYPASS=0 可关。详见 foxclaw.api.middleware._loopback_bypass_on
|
|
50
48
|
ws_host = websocket.client.host if websocket.client else ""
|
|
51
49
|
if not (_loopback_bypass_on() and _is_loopback_client(ws_host)):
|
|
52
50
|
provided = (websocket.query_params.get("key")
|
|
53
51
|
or websocket.headers.get("x-api-key"))
|
|
54
|
-
# 【P3-1】常量时间比较(与 HTTP 中间件同一辅助函数);None 由 or "" 兜底——
|
|
55
|
-
# api_key 非空时 not eq("", api_key) 必为 True,与原 None != api_key 行为一致
|
|
56
52
|
if not _constant_time_eq(provided or "", api_key):
|
|
57
|
-
await websocket.close(code=1008)
|
|
53
|
+
await websocket.close(code=1008)
|
|
58
54
|
return
|
|
59
55
|
await manager.connect(websocket)
|
|
60
56
|
try:
|
|
61
57
|
while True:
|
|
62
|
-
await websocket.receive_text()
|
|
58
|
+
await websocket.receive_text()
|
|
63
59
|
except WebSocketDisconnect:
|
|
64
60
|
manager.disconnect(websocket)
|
|
65
61
|
|
|
@@ -35,11 +35,9 @@ def _prewarm_hkc(osi):
|
|
|
35
35
|
def _run():
|
|
36
36
|
t0 = _t.time()
|
|
37
37
|
try:
|
|
38
|
-
svc = getattr(osi, "hkc", None)
|
|
38
|
+
svc = getattr(osi, "hkc", None)
|
|
39
39
|
if not svc or not getattr(svc, "available", False):
|
|
40
40
|
return
|
|
41
|
-
# 搜索索引已在 assemble() 内构建(向量索引持久化加载优先,避免每次启动全量
|
|
42
|
-
# embedding 把 CPU 打满、启动卡几分钟),这里不再重复 build_index。
|
|
43
41
|
try:
|
|
44
42
|
lr = getattr(osi, "learning_runtime", None)
|
|
45
43
|
if lr is not None:
|
|
@@ -50,6 +48,5 @@ def _prewarm_hkc(osi):
|
|
|
50
48
|
except Exception as e:
|
|
51
49
|
print(f"[Foxclaw API] · HKC 预热未启用(可选): {e}")
|
|
52
50
|
|
|
53
|
-
# 晚绑定:把守护线程挂到主模块 `__init__.py` 的 `_prewarm_thread`(lifespan 关闭前需 join)。
|
|
54
51
|
_api._prewarm_thread = threading.Thread(target=_run, name="hkc-prewarm", daemon=True)
|
|
55
52
|
_api._prewarm_thread.start()
|
|
@@ -21,10 +21,8 @@ class ActionExecutor:
|
|
|
21
21
|
return handler(action)
|
|
22
22
|
|
|
23
23
|
def _do_navigate(self, action: WebAction):
|
|
24
|
-
# URL 准入:此前直接把模型给的字符串交给 driver → file:// 读本机文件、
|
|
25
|
-
# javascript: 执行脚本、内网/云元数据 SSRF。ValueError 由 service.act 转成错误返回。
|
|
26
24
|
from .url_policy import validate_navigate_url
|
|
27
|
-
target = validate_navigate_url(action.target)
|
|
25
|
+
target = validate_navigate_url(action.target or "")
|
|
28
26
|
self.driver.navigate(target)
|
|
29
27
|
return {"navigated_to": target}
|
|
30
28
|
|
|
@@ -44,8 +42,6 @@ class ActionExecutor:
|
|
|
44
42
|
return {"scrolled": direction}
|
|
45
43
|
|
|
46
44
|
def _do_extract(self, action: WebAction):
|
|
47
|
-
# extract 本身不改变页面状态,只是触发一次重新语义化,
|
|
48
|
-
# 具体的语义化交给上层 WebAdapter 统一处理,这里返回空占位即可
|
|
49
45
|
return {"extract_requested": True}
|
|
50
46
|
|
|
51
47
|
def _do_wait(self, action: WebAction):
|
|
@@ -30,12 +30,8 @@ from urllib.parse import urlparse
|
|
|
30
30
|
|
|
31
31
|
ALLOWED_SCHEMES = ("http", "https")
|
|
32
32
|
|
|
33
|
-
# 页面内部 scheme:内容由页面自身生成(data:/blob:),或压根不是网络请求(about:)。
|
|
34
|
-
# 它们不经过网络栈,构不成 SSRF;拦下只会让内联图片 / 媒体源加载失败。
|
|
35
33
|
INTERNAL_SCHEMES = ("data", "blob", "about")
|
|
36
34
|
|
|
37
|
-
# 解析结果缓存:一个页面动辄几十个子资源,每次都 getaddrinfo 会让加载慢一个量级。
|
|
38
|
-
# 带 TTL 而不是永久缓存 —— 永久缓存会让「先公网后内网」的切换永远不被重查。
|
|
39
35
|
_RESOLVE_TTL = 60.0
|
|
40
36
|
_MAX_CACHE = 512
|
|
41
37
|
_resolve_cache: dict[str, tuple[bool, float]] = {}
|
|
@@ -67,7 +63,7 @@ def _resolve_internal(host: str) -> bool:
|
|
|
67
63
|
return False
|
|
68
64
|
internal = False
|
|
69
65
|
for info in infos:
|
|
70
|
-
addr = str(info[4][0]).split("%", 1)[0]
|
|
66
|
+
addr = str(info[4][0]).split("%", 1)[0]
|
|
71
67
|
try:
|
|
72
68
|
parsed = ipaddress.ip_address(addr)
|
|
73
69
|
except ValueError:
|
|
@@ -18,7 +18,7 @@ from ..action.action_types import WebAction, SUPPORTED_ACTIONS
|
|
|
18
18
|
class WebAdapter(BaseAdapter):
|
|
19
19
|
|
|
20
20
|
def __init__(self, headless: bool = True):
|
|
21
|
-
self._session = None
|
|
21
|
+
self._session: "SessionManager | None" = None
|
|
22
22
|
self._headless = headless
|
|
23
23
|
|
|
24
24
|
def _ensure_session(self):
|