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,12 +1,12 @@
|
|
|
1
1
|
"""AHS 执行链:技能安装/管理、MCP 接入、Markdown 指令技能、策略封杀、运行、断路器、钩子审计。
|
|
2
2
|
|
|
3
|
-
从 foxclaw/api.py 原样搬移(纯搬移,零行为变更)。共享 OS 实例经 `_api.
|
|
3
|
+
从 foxclaw/api.py 原样搬移(纯搬移,零行为变更)。共享 OS 实例经 `_api.current_os()`
|
|
4
4
|
晚绑定读取;路由用 `@app` 装饰器在 import 时注册。
|
|
5
5
|
"""
|
|
6
6
|
|
|
7
7
|
import asyncio
|
|
8
8
|
import uuid
|
|
9
|
-
from typing import Optional
|
|
9
|
+
from typing import Optional, cast
|
|
10
10
|
|
|
11
11
|
from fastapi import HTTPException
|
|
12
12
|
from pydantic import BaseModel
|
|
@@ -15,24 +15,18 @@ import foxclaw.api as _api
|
|
|
15
15
|
from .. import app
|
|
16
16
|
|
|
17
17
|
|
|
18
|
-
# ============ AHS 执行链(Agent Harness Skills 标准)============
|
|
19
|
-
|
|
20
18
|
class AHSRunPayload(BaseModel):
|
|
21
19
|
task: str
|
|
22
20
|
confirmed: bool = False
|
|
23
|
-
# 【P2-2】断点续跑:传上次响应里的 pending_confirmation(确认后连 confirmed=True 一起传)。
|
|
24
|
-
# Optional:pydantic v2 下 dict=None 字段收到显式 null 会 422,必须 Optional。
|
|
25
21
|
resume_state: Optional[dict] = None
|
|
26
|
-
# 【P1】关键操作(send/pay/destructive)二次确认码:上次响应 pending_confirmation.confirm_nonce。
|
|
27
|
-
# 单次使用、绑定 skill+参数、默认 5 分钟过期。也可直接放在 resume_state 里,二者等价。
|
|
28
22
|
confirm_nonce: str = ""
|
|
29
23
|
|
|
30
24
|
|
|
31
25
|
@app.get("/api/ahs/skills")
|
|
32
26
|
async def ahs_list_skills():
|
|
33
27
|
"""列出注册的 skill(含信任分级 trust + 封杀标记 blocked)。"""
|
|
34
|
-
pol = _api.
|
|
35
|
-
out = _api.
|
|
28
|
+
pol = _api.os_instance().skill_policy
|
|
29
|
+
out = _api.os_instance().skill_registry.catalog()
|
|
36
30
|
for s in out:
|
|
37
31
|
s["blocked"] = pol.is_blocked(s.get("id", ""))
|
|
38
32
|
return {"skills": out}
|
|
@@ -45,22 +39,22 @@ class PolicyDenyPayload(BaseModel):
|
|
|
45
39
|
@app.get("/api/ahs/policy")
|
|
46
40
|
async def ahs_policy_get():
|
|
47
41
|
"""全局封杀策略快照(deny/allow)。"""
|
|
48
|
-
return _api.
|
|
42
|
+
return _api.os_instance().skill_policy.snapshot()
|
|
49
43
|
|
|
50
44
|
|
|
51
45
|
@app.post("/api/ahs/policy/deny")
|
|
52
46
|
async def ahs_policy_deny(p: PolicyDenyPayload):
|
|
53
47
|
"""封杀一个技能 id(deny 压倒 enable,立即对所有执行/注入生效)。"""
|
|
54
|
-
_api.
|
|
55
|
-
return {"ok": True, "policy": _api.
|
|
48
|
+
_api.os_instance().skill_policy.deny(p.id)
|
|
49
|
+
return {"ok": True, "policy": _api.os_instance().skill_policy.snapshot()}
|
|
56
50
|
|
|
57
51
|
|
|
58
52
|
@app.delete("/api/ahs/policy/deny/{skill_id}")
|
|
59
53
|
async def ahs_policy_undeny(skill_id: str):
|
|
60
54
|
"""解封。E1:被 policy 层(管理员 FOXCLAW_POLICY_DENY / policy.json)锁定的技能运行时无法解封,
|
|
61
55
|
诚实返回 ok=False + 原因(不再假报成功);其余情况与改前一致(ok=True)。"""
|
|
62
|
-
_api.
|
|
63
|
-
snap = _api.
|
|
56
|
+
_api.os_instance().skill_policy.undeny(skill_id)
|
|
57
|
+
snap = _api.os_instance().skill_policy.snapshot()
|
|
64
58
|
if skill_id in snap.get("policy_deny", []):
|
|
65
59
|
return {"ok": False,
|
|
66
60
|
"reason": f"「{skill_id}」被管理员策略(policy 层)锁定,运行时无法解封", "policy": snap}
|
|
@@ -71,23 +65,21 @@ class SkillInstallPayload(BaseModel):
|
|
|
71
65
|
id: str
|
|
72
66
|
name: str
|
|
73
67
|
description: str = ""
|
|
74
|
-
kind: str
|
|
68
|
+
kind: str
|
|
75
69
|
triggers: list = []
|
|
76
|
-
side_effect: str = "write"
|
|
70
|
+
side_effect: str = "write"
|
|
77
71
|
destructive: bool = False
|
|
78
72
|
external: bool = True
|
|
79
73
|
params: dict = {}
|
|
80
|
-
# web_api
|
|
81
74
|
endpoint: str = ""
|
|
82
75
|
http_method: str = "GET"
|
|
83
76
|
http_headers: dict = {}
|
|
84
77
|
param_location: str = "auto"
|
|
85
78
|
allow_local: bool = False
|
|
86
79
|
timeout: float = 10.0
|
|
87
|
-
# mcp
|
|
88
80
|
mcp_server: str = ""
|
|
89
81
|
mcp_tool: str = ""
|
|
90
|
-
acknowledged: bool = False
|
|
82
|
+
acknowledged: bool = False
|
|
91
83
|
|
|
92
84
|
|
|
93
85
|
class SkillPatchPayload(BaseModel):
|
|
@@ -98,9 +90,6 @@ class SkillTestPayload(BaseModel):
|
|
|
98
90
|
args: dict = {}
|
|
99
91
|
confirmed: bool = False
|
|
100
92
|
mode: str = "strict"
|
|
101
|
-
# 关键操作(send/pay/destructive)的二次确认码:上次响应里的 confirm_nonce。
|
|
102
|
-
# 宿主注入了 confirms(见 foxclaw/__init__.py)时,confirmed=True 只有配有效 nonce
|
|
103
|
-
# 才放行 —— 原先这个端点收不到 nonce,界面上点「确认」永远退回 needs_confirmation。
|
|
104
93
|
confirm_nonce: str = ""
|
|
105
94
|
|
|
106
95
|
|
|
@@ -124,66 +113,66 @@ def _validate_install(p: "SkillInstallPayload"):
|
|
|
124
113
|
async def ahs_install_skill(p: SkillInstallPayload):
|
|
125
114
|
"""安装/注册一个 web_api|mcp 技能(过安装审核门 + 持久化)。同 id 覆盖更新。"""
|
|
126
115
|
_validate_install(p)
|
|
127
|
-
from foxclaw.skills import SkillSpec
|
|
116
|
+
from foxclaw.skills import SideEffect, SkillKind, SkillSpec
|
|
128
117
|
from foxclaw.skills.review import review_install
|
|
129
|
-
reg = _api.
|
|
118
|
+
reg = _api.os_instance().skill_registry
|
|
130
119
|
_existing = reg.get(p.id)
|
|
131
120
|
if _existing is not None and _existing.source == "builtin":
|
|
132
121
|
raise HTTPException(409, "该 id 属于内置技能,不能覆盖")
|
|
133
122
|
existed = _existing is not None
|
|
134
123
|
spec = SkillSpec(
|
|
135
|
-
id=p.id, name=p.name, description=p.description,
|
|
136
|
-
|
|
124
|
+
id=p.id, name=p.name, description=p.description,
|
|
125
|
+
kind=cast(SkillKind, p.kind), triggers=list(p.triggers),
|
|
126
|
+
side_effect=cast(SideEffect, p.side_effect),
|
|
137
127
|
destructive=p.destructive, external=p.external, params=dict(p.params),
|
|
138
128
|
endpoint=p.endpoint, http_method=p.http_method, http_headers=dict(p.http_headers),
|
|
139
129
|
param_location=p.param_location, allow_local=p.allow_local, timeout=p.timeout,
|
|
140
130
|
mcp_server=p.mcp_server, mcp_tool=p.mcp_tool, source="user",
|
|
141
131
|
)
|
|
142
|
-
verdict = review_install(spec)
|
|
132
|
+
verdict = review_install(spec)
|
|
143
133
|
if verdict["verdict"] == "deny":
|
|
144
134
|
raise HTTPException(403, detail={"error": "install_denied", "reasons": verdict["reasons"]})
|
|
145
135
|
if verdict["verdict"] == "warn" and not p.acknowledged:
|
|
146
136
|
raise HTTPException(409, detail={"needs_ack": True, "warnings": verdict["warnings"]})
|
|
147
137
|
reg.register(spec)
|
|
148
|
-
_api.
|
|
138
|
+
_api.os_instance().skill_store.save(reg)
|
|
149
139
|
return {"ok": True, "skill": spec.to_dict(), "created": not existed, "warnings": verdict["warnings"]}
|
|
150
140
|
|
|
151
141
|
|
|
152
142
|
@app.patch("/api/ahs/skills/{skill_id}")
|
|
153
143
|
async def ahs_patch_skill(skill_id: str, p: SkillPatchPayload):
|
|
154
144
|
"""启停一个技能(内置/用户均可,持久化)。"""
|
|
155
|
-
reg = _api.
|
|
145
|
+
reg = _api.os_instance().skill_registry
|
|
156
146
|
sp = reg.get(skill_id)
|
|
157
147
|
if sp is None:
|
|
158
148
|
raise HTTPException(404, "技能不存在")
|
|
159
149
|
sp.enabled = bool(p.enabled)
|
|
160
|
-
_api.
|
|
150
|
+
_api.os_instance().skill_store.save(reg)
|
|
161
151
|
return {"ok": True, "skill": sp.to_dict()}
|
|
162
152
|
|
|
163
153
|
|
|
164
154
|
@app.delete("/api/ahs/skills/{skill_id}")
|
|
165
155
|
async def ahs_delete_skill(skill_id: str):
|
|
166
156
|
"""删除用户安装的技能;内置技能不可删(只能停用)。"""
|
|
167
|
-
reg = _api.
|
|
157
|
+
reg = _api.os_instance().skill_registry
|
|
168
158
|
sp = reg.get(skill_id)
|
|
169
159
|
if sp is None:
|
|
170
160
|
raise HTTPException(404, "技能不存在")
|
|
171
161
|
if sp.source == "builtin":
|
|
172
162
|
raise HTTPException(409, "内置技能不可删除,只能停用")
|
|
173
163
|
reg.unregister(skill_id)
|
|
174
|
-
_api.
|
|
164
|
+
_api.os_instance().skill_store.save(reg)
|
|
175
165
|
return {"ok": True}
|
|
176
166
|
|
|
177
167
|
|
|
178
168
|
@app.post("/api/ahs/skills/{skill_id}/test")
|
|
179
169
|
async def ahs_test_skill(skill_id: str, p: SkillTestPayload):
|
|
180
170
|
"""单技能测试调用:走真实权限管线/断路器/SSRF。副作用未确认 → needs_confirmation。"""
|
|
181
|
-
reg = _api.
|
|
171
|
+
reg = _api.os_instance().skill_registry
|
|
182
172
|
sp = reg.get(skill_id)
|
|
183
173
|
if sp is None:
|
|
184
174
|
raise HTTPException(404, "技能不存在")
|
|
185
|
-
|
|
186
|
-
res = await asyncio.to_thread(_api._os_instance.skill_adapter.invoke,
|
|
175
|
+
res = await asyncio.to_thread(_api.os_instance().skill_adapter.invoke,
|
|
187
176
|
sp, p.args, p.confirmed, p.mode,
|
|
188
177
|
confirm_nonce=p.confirm_nonce)
|
|
189
178
|
return res.to_dict()
|
|
@@ -194,7 +183,7 @@ class McpServerPayload(BaseModel):
|
|
|
194
183
|
name: str = ""
|
|
195
184
|
url: str
|
|
196
185
|
headers: dict = {}
|
|
197
|
-
transport: str = "streamable_http"
|
|
186
|
+
transport: str = "streamable_http"
|
|
198
187
|
acknowledged: bool = False
|
|
199
188
|
|
|
200
189
|
|
|
@@ -214,22 +203,21 @@ async def mcp_add_server(p: McpServerPayload):
|
|
|
214
203
|
raise HTTPException(409, detail={"needs_ack": True, "warnings": verdict["warnings"]})
|
|
215
204
|
cfg = {"id": p.id, "name": p.name or p.id, "url": p.url,
|
|
216
205
|
"headers": dict(p.headers), "transport": p.transport}
|
|
217
|
-
sentinel = getattr(_api.
|
|
206
|
+
sentinel = getattr(_api.current_os(), "sentinel", None)
|
|
218
207
|
try:
|
|
219
208
|
if sentinel is not None:
|
|
220
|
-
|
|
221
|
-
tools = await asyncio.to_thread(_api._os_instance.mcp_add_server_guardian, cfg)
|
|
209
|
+
tools = await asyncio.to_thread(_api.os_instance().mcp_add_server_guardian, cfg)
|
|
222
210
|
else:
|
|
223
|
-
tools = await asyncio.to_thread(_api.
|
|
224
|
-
_api.
|
|
211
|
+
tools = await asyncio.to_thread(_api.os_instance().mcp_manager.add_server, cfg)
|
|
212
|
+
_api.os_instance().mcp_store.save(_api.os_instance().mcp_manager.servers())
|
|
225
213
|
except Exception as e:
|
|
226
214
|
raise HTTPException(502, f"连接 MCP server 失败: {e}")
|
|
227
|
-
reg = _api.
|
|
215
|
+
reg = _api.os_instance().skill_registry
|
|
228
216
|
n = 0
|
|
229
217
|
for tool in tools:
|
|
230
218
|
reg.register(tool_to_spec(p.id, tool)); n += 1
|
|
231
|
-
_api.
|
|
232
|
-
_api.
|
|
219
|
+
_api.os_instance().skill_store.save(reg)
|
|
220
|
+
_api.os_instance().sync_mcp_servers_to_sentinel()
|
|
233
221
|
return {"ok": True, "server": {k: cfg[k] for k in ("id", "name", "url", "transport")},
|
|
234
222
|
"tools_registered": n, "warnings": verdict["warnings"]}
|
|
235
223
|
|
|
@@ -237,10 +225,10 @@ async def mcp_add_server(p: McpServerPayload):
|
|
|
237
225
|
@app.get("/api/ahs/mcp/servers")
|
|
238
226
|
async def mcp_list_servers():
|
|
239
227
|
"""列出已接入 MCP server(工具数由注册表派生,无需连接)。"""
|
|
240
|
-
reg = _api.
|
|
228
|
+
reg = _api.os_instance().skill_registry
|
|
241
229
|
all_sk = reg.all_including_disabled()
|
|
242
230
|
out = []
|
|
243
|
-
for c in _api.
|
|
231
|
+
for c in _api.os_instance().mcp_manager.servers():
|
|
244
232
|
tc = sum(1 for s in all_sk if s.kind == "mcp" and s.mcp_server == c["id"])
|
|
245
233
|
out.append({"id": c["id"], "name": c["name"], "url": c["url"],
|
|
246
234
|
"transport": c["transport"], "tool_count": tc})
|
|
@@ -250,17 +238,17 @@ async def mcp_list_servers():
|
|
|
250
238
|
@app.delete("/api/ahs/mcp/servers/{server_id}")
|
|
251
239
|
async def mcp_remove_server(server_id: str):
|
|
252
240
|
"""移除 server + 其注册的所有 mcp 技能。"""
|
|
253
|
-
mgr = _api.
|
|
241
|
+
mgr = _api.os_instance().mcp_manager
|
|
254
242
|
if not mgr.remove_server(server_id):
|
|
255
243
|
raise HTTPException(404, "server 不存在")
|
|
256
|
-
reg = _api.
|
|
244
|
+
reg = _api.os_instance().skill_registry
|
|
257
245
|
removed = [s.id for s in reg.all_including_disabled()
|
|
258
246
|
if s.kind == "mcp" and s.mcp_server == server_id]
|
|
259
247
|
for sid in removed:
|
|
260
248
|
reg.unregister(sid)
|
|
261
|
-
_api.
|
|
262
|
-
_api.
|
|
263
|
-
_api.
|
|
249
|
+
_api.os_instance().skill_store.save(reg)
|
|
250
|
+
_api.os_instance().mcp_store.save(mgr.servers())
|
|
251
|
+
_api.os_instance().sync_mcp_servers_to_sentinel()
|
|
264
252
|
return {"ok": True, "removed_skills": len(removed)}
|
|
265
253
|
|
|
266
254
|
|
|
@@ -268,38 +256,38 @@ async def mcp_remove_server(server_id: str):
|
|
|
268
256
|
async def mcp_refresh_server(server_id: str):
|
|
269
257
|
"""重连拉工具:新增/更新技能,删除已消失的工具技能。"""
|
|
270
258
|
from foxclaw.skills.mcp_client import tool_to_spec
|
|
271
|
-
mgr = _api.
|
|
259
|
+
mgr = _api.os_instance().mcp_manager
|
|
272
260
|
if not any(s["id"] == server_id for s in mgr.servers()):
|
|
273
261
|
raise HTTPException(404, "server 不存在")
|
|
274
|
-
sentinel = getattr(_api.
|
|
262
|
+
sentinel = getattr(_api.current_os(), "sentinel", None)
|
|
275
263
|
try:
|
|
276
264
|
if sentinel is not None:
|
|
277
|
-
tools = await asyncio.to_thread(sentinel.mcp_list_tools, server_id)
|
|
265
|
+
tools = await asyncio.to_thread(sentinel.mcp_list_tools, server_id)
|
|
278
266
|
else:
|
|
279
|
-
tools = await asyncio.to_thread(mgr.list_tools, server_id)
|
|
267
|
+
tools = await asyncio.to_thread(mgr.list_tools, server_id)
|
|
280
268
|
except Exception as e:
|
|
281
269
|
raise HTTPException(502, f"刷新失败: {e}")
|
|
282
|
-
reg = _api.
|
|
270
|
+
reg = _api.os_instance().skill_registry
|
|
283
271
|
fresh = set()
|
|
284
272
|
for tool in tools:
|
|
285
273
|
spec = tool_to_spec(server_id, tool); reg.register(spec); fresh.add(spec.id)
|
|
286
274
|
for s in [s for s in reg.all_including_disabled()
|
|
287
275
|
if s.kind == "mcp" and s.mcp_server == server_id and s.id not in fresh]:
|
|
288
276
|
reg.unregister(s.id)
|
|
289
|
-
_api.
|
|
277
|
+
_api.os_instance().skill_store.save(reg)
|
|
290
278
|
return {"ok": True, "tools": len(fresh)}
|
|
291
279
|
|
|
292
280
|
|
|
293
281
|
class MdSkillPayload(BaseModel):
|
|
294
282
|
content: str = ""
|
|
295
|
-
package_b64: str = ""
|
|
283
|
+
package_b64: str = ""
|
|
296
284
|
id: str = ""
|
|
297
285
|
acknowledged: bool = False
|
|
298
286
|
|
|
299
287
|
|
|
300
288
|
class MdSkillPatchPayload(BaseModel):
|
|
301
289
|
enabled: bool | None = None
|
|
302
|
-
triggers: list | None = None
|
|
290
|
+
triggers: list | None = None
|
|
303
291
|
|
|
304
292
|
|
|
305
293
|
@app.post("/api/ahs/md-skills")
|
|
@@ -326,17 +314,17 @@ async def md_install(p: MdSkillPayload):
|
|
|
326
314
|
if not p.acknowledged:
|
|
327
315
|
raise HTTPException(409, detail={"needs_ack": True, "warnings": [
|
|
328
316
|
"此技能会把内容注入你 AI 的提示词、影响其行为,仅从可信来源安装"]})
|
|
329
|
-
reg = _api.
|
|
317
|
+
reg = _api.os_instance().md_skills
|
|
330
318
|
existed = reg.get(skill.id) is not None
|
|
331
319
|
reg.register(skill)
|
|
332
|
-
_api.
|
|
320
|
+
_api.os_instance().md_store.save(reg)
|
|
333
321
|
return {"ok": True, "skill": skill.to_meta(), "created": not existed}
|
|
334
322
|
|
|
335
323
|
|
|
336
324
|
@app.get("/api/ahs/md-skills")
|
|
337
325
|
async def md_list():
|
|
338
|
-
pol = _api.
|
|
339
|
-
out = _api.
|
|
326
|
+
pol = _api.os_instance().skill_policy
|
|
327
|
+
out = _api.os_instance().md_skills.catalog()
|
|
340
328
|
for s in out:
|
|
341
329
|
s["blocked"] = pol.is_blocked(s.get("id", ""))
|
|
342
330
|
return {"skills": out}
|
|
@@ -344,7 +332,7 @@ async def md_list():
|
|
|
344
332
|
|
|
345
333
|
@app.get("/api/ahs/md-skills/{skill_id}")
|
|
346
334
|
async def md_get(skill_id: str):
|
|
347
|
-
s = _api.
|
|
335
|
+
s = _api.os_instance().md_skills.get(skill_id)
|
|
348
336
|
if s is None:
|
|
349
337
|
raise HTTPException(404, "指令技能不存在")
|
|
350
338
|
return s.to_dict()
|
|
@@ -352,7 +340,7 @@ async def md_get(skill_id: str):
|
|
|
352
340
|
|
|
353
341
|
@app.patch("/api/ahs/md-skills/{skill_id}")
|
|
354
342
|
async def md_patch(skill_id: str, p: MdSkillPatchPayload):
|
|
355
|
-
reg = _api.
|
|
343
|
+
reg = _api.os_instance().md_skills
|
|
356
344
|
s = reg.get(skill_id)
|
|
357
345
|
if s is None:
|
|
358
346
|
raise HTTPException(404, "指令技能不存在")
|
|
@@ -360,17 +348,17 @@ async def md_patch(skill_id: str, p: MdSkillPatchPayload):
|
|
|
360
348
|
s.enabled = bool(p.enabled)
|
|
361
349
|
if p.triggers is not None:
|
|
362
350
|
s.triggers = [str(t).strip() for t in p.triggers if str(t).strip()]
|
|
363
|
-
_api.
|
|
351
|
+
_api.os_instance().md_store.save(reg)
|
|
364
352
|
return {"ok": True, "skill": s.to_meta()}
|
|
365
353
|
|
|
366
354
|
|
|
367
355
|
@app.delete("/api/ahs/md-skills/{skill_id}")
|
|
368
356
|
async def md_delete(skill_id: str):
|
|
369
|
-
reg = _api.
|
|
357
|
+
reg = _api.os_instance().md_skills
|
|
370
358
|
if reg.get(skill_id) is None:
|
|
371
359
|
raise HTTPException(404, "指令技能不存在")
|
|
372
360
|
reg.unregister(skill_id)
|
|
373
|
-
_api.
|
|
361
|
+
_api.os_instance().md_store.save(reg)
|
|
374
362
|
return {"ok": True}
|
|
375
363
|
|
|
376
364
|
|
|
@@ -383,11 +371,10 @@ async def ahs_run(payload: AHSRunPayload):
|
|
|
383
371
|
【P1】关键操作还需带 confirm_nonce(响应里的 pending_confirmation.confirm_nonce);
|
|
384
372
|
缺/错/过期 → 仍返回 pending_confirmation 并换发新码,不会执行。"""
|
|
385
373
|
from foxclaw.skills import set_loop_session, reset_loop_session
|
|
386
|
-
# 循环检测按执行链分槽:断点续跑复用同一 task_id,否则每次请求独立(消除跨请求污染)
|
|
387
374
|
sid = (payload.resume_state or {}).get("task_id") or f"ahs_{uuid.uuid4().hex[:8]}"
|
|
388
375
|
token = set_loop_session(sid)
|
|
389
376
|
try:
|
|
390
|
-
res = await _api.
|
|
377
|
+
res = await _api.os_instance().ahs_pipeline.run_async(
|
|
391
378
|
payload.task, confirmed=payload.confirmed,
|
|
392
379
|
resume_state=payload.resume_state,
|
|
393
380
|
confirm_nonce=payload.confirm_nonce)
|
|
@@ -407,11 +394,11 @@ async def ahs_run(payload: AHSRunPayload):
|
|
|
407
394
|
@app.get("/api/ahs/breaker")
|
|
408
395
|
async def ahs_breaker_status():
|
|
409
396
|
"""断路器状态:各 skill 的失败计数与开闸情况。"""
|
|
410
|
-
br = _api.
|
|
397
|
+
br = _api.os_instance().skill_adapter.breaker
|
|
411
398
|
return {"breakers": {sid: br.status(sid) for sid in br._entries}}
|
|
412
399
|
|
|
413
400
|
|
|
414
401
|
@app.get("/api/ahs/hooks/audit")
|
|
415
402
|
async def ahs_hooks_audit():
|
|
416
403
|
"""钩子审计日志(最近 20 条)。"""
|
|
417
|
-
return {"audit": _api.
|
|
404
|
+
return {"audit": _api.os_instance().skill_adapter.hooks.recent_audit(20)}
|
|
@@ -23,11 +23,10 @@ import foxclaw.api as _api
|
|
|
23
23
|
from .. import app
|
|
24
24
|
|
|
25
25
|
|
|
26
|
-
_MAX_ATTACH_BYTES = 20 * 1024 * 1024
|
|
27
|
-
_MAX_TEXT_CHARS = 20000
|
|
26
|
+
_MAX_ATTACH_BYTES = 20 * 1024 * 1024
|
|
27
|
+
_MAX_TEXT_CHARS = 20000
|
|
28
28
|
_ID_RE = re.compile(r"^[A-Za-z0-9_-]{1,64}$")
|
|
29
29
|
|
|
30
|
-
# 文本类扩展名 → 直接读正文;.pdf 走 fitz(可选依赖);其余二进制 → 降级为元数据提示
|
|
31
30
|
_TEXT_EXTS = {
|
|
32
31
|
".txt", ".md", ".markdown", ".csv", ".json", ".yaml", ".yml", ".log",
|
|
33
32
|
".py", ".js", ".ts", ".html", ".css", ".xml", ".toml", ".ini", ".sh",
|
|
@@ -36,7 +35,7 @@ _TEXT_EXTS = {
|
|
|
36
35
|
|
|
37
36
|
|
|
38
37
|
def _att_dir() -> Path:
|
|
39
|
-
d = Path(_api.
|
|
38
|
+
d = Path(_api.os_instance().data_dir) / "attachments"
|
|
40
39
|
d.mkdir(parents=True, exist_ok=True)
|
|
41
40
|
return d
|
|
42
41
|
|
|
@@ -61,7 +60,7 @@ def _extract_text(path: Path, name: str) -> str:
|
|
|
61
60
|
return ""
|
|
62
61
|
if ext == ".pdf":
|
|
63
62
|
try:
|
|
64
|
-
import fitz
|
|
63
|
+
import fitz
|
|
65
64
|
except ImportError:
|
|
66
65
|
return ""
|
|
67
66
|
try:
|
|
@@ -85,7 +84,7 @@ async def upload_attachment(file: UploadFile = File(...)):
|
|
|
85
84
|
"""通用聊天附件上传:落盘 + 尽力提取正文,返回 {id,name,size,mime,url,text}。
|
|
86
85
|
text 为空表示不可提取(二进制/图片等),前端/LLM 降级为「文件名 + 元数据」提示。"""
|
|
87
86
|
raw_name = (file.filename or "attachment").strip()
|
|
88
|
-
name = Path(raw_name).name or "attachment"
|
|
87
|
+
name = Path(raw_name).name or "attachment"
|
|
89
88
|
data = await file.read()
|
|
90
89
|
if not data:
|
|
91
90
|
raise HTTPException(400, "空文件")
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"""浏览器子系统(HIR-Browser)端点。
|
|
2
2
|
|
|
3
|
-
从 foxclaw/api.py 原样搬移(纯搬移,零行为变更)。共享 OS 实例经 `_api.
|
|
3
|
+
从 foxclaw/api.py 原样搬移(纯搬移,零行为变更)。共享 OS 实例经 `_api.current_os()`
|
|
4
4
|
晚绑定读取;路由用 `@app` 装饰器在 import 时注册。
|
|
5
5
|
"""
|
|
6
6
|
|
|
@@ -10,30 +10,26 @@ import foxclaw.api as _api
|
|
|
10
10
|
from .. import app
|
|
11
11
|
|
|
12
12
|
|
|
13
|
-
# ── 浏览器子系统(HIR-Browser):AI 用的浏览器 ─────────────────
|
|
14
|
-
# 感知网页→压缩语义结构→执行动作→校验结果。async 安全(专用工作线程),
|
|
15
|
-
# playwright 缺失时结构化降级(available=False),不崩溃。受 /api 鉴权保护。
|
|
16
|
-
|
|
17
13
|
class BrowserActPayload(BaseModel):
|
|
18
|
-
type: str
|
|
19
|
-
target: str | None = None
|
|
20
|
-
value: object | None = None
|
|
14
|
+
type: str
|
|
15
|
+
target: str | None = None
|
|
16
|
+
value: object | None = None
|
|
21
17
|
meta: dict = {}
|
|
22
18
|
|
|
23
19
|
|
|
24
20
|
@app.get("/api/browser/status")
|
|
25
21
|
async def browser_status():
|
|
26
22
|
"""浏览器子系统能力/会话状态(available/session_active/launch_error)。"""
|
|
27
|
-
return _api.
|
|
23
|
+
return _api.os_instance().browser_service.status()
|
|
28
24
|
|
|
29
25
|
|
|
30
26
|
@app.get("/api/browser/perceive")
|
|
31
27
|
async def browser_perceive():
|
|
32
28
|
"""只感知当前页面语义结构(带编号的可交互元素 + 摘要),不改变页面。"""
|
|
33
|
-
return await _api.
|
|
29
|
+
return await _api.os_instance().browser_service.perceive()
|
|
34
30
|
|
|
35
31
|
|
|
36
32
|
@app.post("/api/browser/act")
|
|
37
33
|
async def browser_act(p: BrowserActPayload):
|
|
38
34
|
"""执行一个 Web 动作,返回校验后最新语义结构。"""
|
|
39
|
-
return await _api.
|
|
35
|
+
return await _api.os_instance().browser_service.act(p.model_dump())
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"""领域内核端点(B 阶段三内核):财务 / 学习 / 行程 + 多格式文件导入。
|
|
2
2
|
|
|
3
|
-
从 foxclaw/api.py 原样搬移(纯搬移,零行为变更)。共享 OS 实例经 `_api.
|
|
3
|
+
从 foxclaw/api.py 原样搬移(纯搬移,零行为变更)。共享 OS 实例经 `_api.current_os()`
|
|
4
4
|
晚绑定读取;路由用 `@app` 装饰器在 import 时注册。
|
|
5
5
|
"""
|
|
6
6
|
|
|
@@ -18,11 +18,9 @@ from foxclaw.core.paths import resolve_import_path
|
|
|
18
18
|
from .. import app
|
|
19
19
|
|
|
20
20
|
|
|
21
|
-
# ── 领域内核端点(B 阶段三内核)─────────────────────────────
|
|
22
|
-
|
|
23
21
|
class ImportPayload(BaseModel):
|
|
24
22
|
csv_path: Optional[str] = None
|
|
25
|
-
rows: Optional[list] = None
|
|
23
|
+
rows: Optional[list] = None
|
|
26
24
|
|
|
27
25
|
|
|
28
26
|
def _resolve_csv_path(raw: str) -> str:
|
|
@@ -43,7 +41,7 @@ async def finance_analysis(period: Optional[str] = None,
|
|
|
43
41
|
current_balance: Optional[float] = None):
|
|
44
42
|
"""财务内核综合分析:预算/异常/订阅/烧钱/守门人裁决。"""
|
|
45
43
|
try:
|
|
46
|
-
return _api.
|
|
44
|
+
return _api.os_instance().finance.analyze_finances(
|
|
47
45
|
period=period, current_balance=current_balance)
|
|
48
46
|
except Exception as e:
|
|
49
47
|
raise HTTPException(500, f"财务分析失败: {e}")
|
|
@@ -52,7 +50,7 @@ async def finance_analysis(period: Optional[str] = None,
|
|
|
52
50
|
@app.post("/api/finance/import")
|
|
53
51
|
async def finance_import(p: ImportPayload):
|
|
54
52
|
"""导入交易数据(CSV 路径或记录数组)。"""
|
|
55
|
-
rt = _api.
|
|
53
|
+
rt = _api.os_instance().finance_runtime
|
|
56
54
|
src = _resolve_csv_path(p.csv_path) if p.csv_path else ""
|
|
57
55
|
try:
|
|
58
56
|
if src:
|
|
@@ -61,7 +59,6 @@ async def finance_import(p: ImportPayload):
|
|
|
61
59
|
from foxclaw.finance import Transaction
|
|
62
60
|
n = 0
|
|
63
61
|
for row in p.rows:
|
|
64
|
-
# save=False:整批导完再落盘一次,别在循环里做 N 次磁盘 IO
|
|
65
62
|
rt.add_transaction(Transaction(**row), run_gatekeeper=False, save=False)
|
|
66
63
|
n += 1
|
|
67
64
|
if n:
|
|
@@ -73,12 +70,10 @@ async def finance_import(p: ImportPayload):
|
|
|
73
70
|
raise HTTPException(400, f"导入失败: {e}")
|
|
74
71
|
|
|
75
72
|
|
|
76
|
-
# ── 自然语言记账(第一线)────────────────────────────────────
|
|
77
|
-
|
|
78
73
|
class RecordPayload(BaseModel):
|
|
79
74
|
text: str
|
|
80
75
|
account: str = "default"
|
|
81
|
-
dry_run: bool = False
|
|
76
|
+
dry_run: bool = False
|
|
82
77
|
|
|
83
78
|
|
|
84
79
|
class FxPayload(BaseModel):
|
|
@@ -89,7 +84,7 @@ class FxPayload(BaseModel):
|
|
|
89
84
|
@app.post("/api/finance/record")
|
|
90
85
|
async def finance_record(p: RecordPayload):
|
|
91
86
|
"""一句话记账:「今天买了一件衣服花了 105 块」/「明天交房租 3000」。"""
|
|
92
|
-
rt = _api.
|
|
87
|
+
rt = _api.os_instance().finance_runtime
|
|
93
88
|
try:
|
|
94
89
|
return rt.record_text(p.text, account=p.account, dry_run=p.dry_run)
|
|
95
90
|
except Exception as e:
|
|
@@ -99,7 +94,7 @@ async def finance_record(p: RecordPayload):
|
|
|
99
94
|
@app.get("/api/finance/ledger")
|
|
100
95
|
async def finance_ledger(period: Optional[str] = None):
|
|
101
96
|
"""记账本:按月汇总(已发生/计划、按天、按分类、按币种)。"""
|
|
102
|
-
rt = _api.
|
|
97
|
+
rt = _api.os_instance().finance_runtime
|
|
103
98
|
try:
|
|
104
99
|
return rt.ledger(period)
|
|
105
100
|
except Exception as e:
|
|
@@ -109,14 +104,14 @@ async def finance_ledger(period: Optional[str] = None):
|
|
|
109
104
|
@app.get("/api/finance/fx")
|
|
110
105
|
async def finance_fx():
|
|
111
106
|
"""当前汇率表(本位币 CNY)。"""
|
|
112
|
-
return _api.
|
|
107
|
+
return _api.os_instance().finance_runtime.fx.snapshot()
|
|
113
108
|
|
|
114
109
|
|
|
115
110
|
@app.post("/api/finance/fx")
|
|
116
111
|
async def finance_set_fx(p: FxPayload):
|
|
117
112
|
"""设置某个币种的记账汇率(比如刷卡当天的实际结算价)。"""
|
|
118
113
|
try:
|
|
119
|
-
return _api.
|
|
114
|
+
return _api.os_instance().finance_runtime.fx.set_rate(p.currency, p.rate)
|
|
120
115
|
except ValueError as e:
|
|
121
116
|
raise HTTPException(400, str(e))
|
|
122
117
|
|
|
@@ -125,7 +120,7 @@ async def finance_set_fx(p: FxPayload):
|
|
|
125
120
|
async def learning_analysis():
|
|
126
121
|
"""学习内核综合分析:掌握度/遗忘风险/知识图谱/学习策略。"""
|
|
127
122
|
try:
|
|
128
|
-
return _api.
|
|
123
|
+
return _api.os_instance().office.analyze_learning()
|
|
129
124
|
except Exception as e:
|
|
130
125
|
raise HTTPException(500, f"学习分析失败: {e}")
|
|
131
126
|
|
|
@@ -134,7 +129,7 @@ async def learning_analysis():
|
|
|
134
129
|
async def learning_import(p: ImportPayload):
|
|
135
130
|
"""导入知识点(办公学习内核)。learn() 内部已把内容存进 HKC(连续 ConceptKU,知识真源),
|
|
136
131
|
不再另做桥接。"""
|
|
137
|
-
rt = _api.
|
|
132
|
+
rt = _api.os_instance().learning_runtime
|
|
138
133
|
src = _resolve_csv_path(p.csv_path) if p.csv_path else ""
|
|
139
134
|
try:
|
|
140
135
|
if src:
|
|
@@ -154,7 +149,7 @@ async def learning_import(p: ImportPayload):
|
|
|
154
149
|
@app.delete("/api/learning/item/{item_id}")
|
|
155
150
|
async def learning_delete(item_id: str):
|
|
156
151
|
"""删除一个学习知识点(内存+复习状态+图谱,并落盘)。"""
|
|
157
|
-
rt = _api.
|
|
152
|
+
rt = _api.os_instance().learning_runtime
|
|
158
153
|
ok = rt.delete(item_id)
|
|
159
154
|
return {"deleted": ok, "status": rt.status()}
|
|
160
155
|
|
|
@@ -167,7 +162,7 @@ class NotePayload(BaseModel):
|
|
|
167
162
|
@app.post("/api/learning/note")
|
|
168
163
|
async def learning_note(p: NotePayload):
|
|
169
164
|
"""给一个学习知识点追加一条笔记(有内容才存;没新增不写盘)。"""
|
|
170
|
-
rt = _api.
|
|
165
|
+
rt = _api.os_instance().learning_runtime
|
|
171
166
|
r = rt.add_note(p.item_id, p.note)
|
|
172
167
|
if "error" in r:
|
|
173
168
|
raise HTTPException(400, r["error"])
|
|
@@ -178,7 +173,7 @@ async def learning_note(p: NotePayload):
|
|
|
178
173
|
async def itinerary_plan(mode: str = "driving"):
|
|
179
174
|
"""行程内核规划:路线优化/可达性/冲突/守门人 + 高德可视化数据。"""
|
|
180
175
|
try:
|
|
181
|
-
return _api.
|
|
176
|
+
return _api.os_instance().office.plan_itinerary(mode=mode)
|
|
182
177
|
except Exception as e:
|
|
183
178
|
raise HTTPException(500, f"行程规划失败: {e}")
|
|
184
179
|
|
|
@@ -186,7 +181,7 @@ async def itinerary_plan(mode: str = "driving"):
|
|
|
186
181
|
@app.post("/api/itinerary/import")
|
|
187
182
|
async def itinerary_import(p: ImportPayload):
|
|
188
183
|
"""导入行程点。"""
|
|
189
|
-
rt = _api.
|
|
184
|
+
rt = _api.os_instance().itinerary_runtime
|
|
190
185
|
src = _resolve_csv_path(p.csv_path) if p.csv_path else ""
|
|
191
186
|
try:
|
|
192
187
|
if src:
|
|
@@ -202,9 +197,6 @@ async def itinerary_import(p: ImportPayload):
|
|
|
202
197
|
raise HTTPException(400, f"导入失败: {e}")
|
|
203
198
|
|
|
204
199
|
|
|
205
|
-
# ── 领域导入:超越 csv_path,支持上传 csv / xlsx / json ─────────────────────
|
|
206
|
-
# 三领域运行时的 import_csv 是"列→类型转换"的唯一权威。多格式文件在此统一解析成 rows,
|
|
207
|
-
# 再桥接成运行时认识的临时 CSV → 复用 import_csv 的转换逻辑(不碰运行时,单一真源)。
|
|
208
200
|
_IMPORT_MAX_BYTES = 10 * 1024 * 1024
|
|
209
201
|
_IMPORT_COLUMNS = {
|
|
210
202
|
"finance": ["amount", "category", "entity", "account", "timestamp", "tags"],
|
|
@@ -291,14 +283,14 @@ async def _domain_import_file(domain: str, runtime, file: UploadFile):
|
|
|
291
283
|
@app.post("/api/finance/import/file")
|
|
292
284
|
async def finance_import_file(file: UploadFile = File(...)):
|
|
293
285
|
"""上传 csv/xlsx/json 导入交易。列:amount,category,entity,account,timestamp,tags。"""
|
|
294
|
-
rt = _api.
|
|
286
|
+
rt = _api.os_instance().finance_runtime
|
|
295
287
|
return await _domain_import_file("finance", rt, file)
|
|
296
288
|
|
|
297
289
|
|
|
298
290
|
@app.post("/api/learning/import/file")
|
|
299
291
|
async def learning_import_file(file: UploadFile = File(...)):
|
|
300
292
|
"""上传 csv/xlsx/json 导入知识点。列:content,topic,source,tags。"""
|
|
301
|
-
rt = _api.
|
|
293
|
+
rt = _api.os_instance().learning_runtime
|
|
302
294
|
res = await _domain_import_file("learning", rt, file)
|
|
303
295
|
try:
|
|
304
296
|
rt.build_connections()
|
|
@@ -310,5 +302,5 @@ async def learning_import_file(file: UploadFile = File(...)):
|
|
|
310
302
|
@app.post("/api/itinerary/import/file")
|
|
311
303
|
async def itinerary_import_file(file: UploadFile = File(...)):
|
|
312
304
|
"""上传 csv/xlsx/json 导入行程点。列:name,address,lng,lat,start_time,duration_min,city。"""
|
|
313
|
-
rt = _api.
|
|
305
|
+
rt = _api.os_instance().itinerary_runtime
|
|
314
306
|
return await _domain_import_file("itinerary", rt, file)
|