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
|
@@ -18,22 +18,20 @@ import re
|
|
|
18
18
|
from dataclasses import dataclass, field
|
|
19
19
|
from typing import Optional
|
|
20
20
|
|
|
21
|
-
from ..planning import planner as P
|
|
21
|
+
from ..planning import planner as P
|
|
22
22
|
|
|
23
23
|
|
|
24
|
-
# ============ skill-router:委托统一引擎,候选空间=skill ============
|
|
25
|
-
|
|
26
24
|
@dataclass
|
|
27
25
|
class RouteResult:
|
|
28
|
-
primary: Optional[str] = None
|
|
26
|
+
primary: Optional[str] = None
|
|
29
27
|
confidence: float = 0.0
|
|
30
|
-
candidates: list = field(default_factory=list)
|
|
28
|
+
candidates: list = field(default_factory=list)
|
|
31
29
|
fallback: Optional[str] = None
|
|
32
30
|
needs_composition: bool = False
|
|
33
31
|
reason: str = ""
|
|
34
32
|
|
|
35
33
|
|
|
36
|
-
def route(task: str, registry, fallback_id: str = None) -> RouteResult:
|
|
34
|
+
def route(task: str, registry, fallback_id: str | None = None) -> RouteResult:
|
|
37
35
|
"""skill-router:把 skill 适配成通用候选,交统一引擎 trigger_route 打分选择。
|
|
38
36
|
算法(否定过滤触发命中 0.8 + 描述领域匹配 0.2)现由 foxclaw.planning 单一真源提供。"""
|
|
39
37
|
skills = registry.all()
|
|
@@ -50,8 +48,6 @@ def route(task: str, registry, fallback_id: str = None) -> RouteResult:
|
|
|
50
48
|
needs_composition=r.needs_composition, reason=r.reason)
|
|
51
49
|
|
|
52
50
|
|
|
53
|
-
# ============ planner:任务拆解 ============
|
|
54
|
-
|
|
55
51
|
@dataclass
|
|
56
52
|
class PlanTask:
|
|
57
53
|
id: str
|
|
@@ -63,9 +59,9 @@ class PlanTask:
|
|
|
63
59
|
@dataclass
|
|
64
60
|
class Plan:
|
|
65
61
|
intent: str
|
|
66
|
-
tasks: list = field(default_factory=list)
|
|
67
|
-
flow_type: str = "sequential"
|
|
68
|
-
steps: list = field(default_factory=list)
|
|
62
|
+
tasks: list = field(default_factory=list)
|
|
63
|
+
flow_type: str = "sequential"
|
|
64
|
+
steps: list = field(default_factory=list)
|
|
69
65
|
needs_input: bool = False
|
|
70
66
|
missing: list = field(default_factory=list)
|
|
71
67
|
|
|
@@ -95,7 +91,6 @@ def plan(task: str, registry) -> Plan:
|
|
|
95
91
|
for i, p in enumerate(parts, 1):
|
|
96
92
|
r = route(p, registry)
|
|
97
93
|
tid = f"T{i}"
|
|
98
|
-
# 顺序:依赖前一步;并行:无依赖(可同时 fork)
|
|
99
94
|
deps = [f"T{i-1}"] if (i > 1 and flow == "sequential") else []
|
|
100
95
|
tasks.append(PlanTask(id=tid, name=p, suggested_skill=r.primary,
|
|
101
96
|
dependencies=deps))
|
|
@@ -104,8 +99,6 @@ def plan(task: str, registry) -> Plan:
|
|
|
104
99
|
return Plan(intent=task, tasks=tasks, flow_type=flow, steps=steps)
|
|
105
100
|
|
|
106
101
|
|
|
107
|
-
# ============ composition:多 skill 编排执行 ============
|
|
108
|
-
|
|
109
102
|
@dataclass
|
|
110
103
|
class StageResult:
|
|
111
104
|
stage: int
|
|
@@ -116,16 +109,14 @@ class StageResult:
|
|
|
116
109
|
@dataclass
|
|
117
110
|
class CompositionResult:
|
|
118
111
|
mode: str
|
|
119
|
-
stages: list = field(default_factory=list)
|
|
112
|
+
stages: list = field(default_factory=list)
|
|
120
113
|
final_output: dict = field(default_factory=dict)
|
|
121
114
|
failed: list = field(default_factory=list)
|
|
122
115
|
rolled_back: bool = False
|
|
123
|
-
rollback_clean: bool = True
|
|
124
|
-
rollback_log: list = field(default_factory=list)
|
|
125
|
-
needs_manual: list = field(default_factory=list)
|
|
126
|
-
|
|
127
|
-
pending_confirmation: dict = None # {task_id,skill_id,stage,message,args,completed_steps,resume_at}
|
|
128
|
-
# 【P2-2】已完成步骤(断点续跑所需的现场): [(task_id, skill_id, output)]
|
|
116
|
+
rollback_clean: bool = True
|
|
117
|
+
rollback_log: list = field(default_factory=list)
|
|
118
|
+
needs_manual: list = field(default_factory=list)
|
|
119
|
+
pending_confirmation: Optional[dict] = None
|
|
129
120
|
completed_steps: list = field(default_factory=list)
|
|
130
121
|
|
|
131
122
|
@property
|
|
@@ -148,16 +139,13 @@ def _build_args(skill, task_text: str, upstream: dict) -> dict:
|
|
|
148
139
|
params = skill.params or {}
|
|
149
140
|
if not params:
|
|
150
141
|
return {}
|
|
151
|
-
# 计算器类:从文本提取数学表达式
|
|
152
142
|
if "expression" in params:
|
|
153
143
|
m = re.search(r"[-+*/().\d\s]+", task_text)
|
|
154
144
|
expr = (m.group(0).strip() if m else task_text)
|
|
155
145
|
return {"expression": expr or task_text}
|
|
156
|
-
# 单参数 skill:把整段任务文本作为该参数
|
|
157
146
|
if len(params) == 1:
|
|
158
147
|
only = next(iter(params))
|
|
159
148
|
return {only: task_text}
|
|
160
|
-
# 多参数:传 task,让下游(或 LLM)解析;附带上游
|
|
161
149
|
return {"task": task_text, "upstream_outputs": dict(upstream)}
|
|
162
150
|
|
|
163
151
|
|
|
@@ -174,7 +162,7 @@ def _rollback(executed, registry, res: CompositionResult):
|
|
|
174
162
|
continue
|
|
175
163
|
side_effect = getattr(skill, "side_effect", "read")
|
|
176
164
|
if side_effect == "read":
|
|
177
|
-
continue
|
|
165
|
+
continue
|
|
178
166
|
comp = getattr(skill, "compensate", None)
|
|
179
167
|
if comp is not None:
|
|
180
168
|
try:
|
|
@@ -184,12 +172,12 @@ def _rollback(executed, registry, res: CompositionResult):
|
|
|
184
172
|
except Exception as e:
|
|
185
173
|
res.rollback_log.append({"skill": sid, "status": "compensate_failed",
|
|
186
174
|
"detail": f"补偿失败: {e}"})
|
|
187
|
-
res.rollback_clean = False
|
|
175
|
+
res.rollback_clean = False
|
|
188
176
|
res.needs_manual.append(sid)
|
|
189
177
|
else:
|
|
190
178
|
res.rollback_log.append({"skill": sid, "status": "manual_required",
|
|
191
179
|
"detail": f"{side_effect} 操作无补偿动作,需人工处理"})
|
|
192
|
-
res.rollback_clean = False
|
|
180
|
+
res.rollback_clean = False
|
|
193
181
|
res.needs_manual.append(sid)
|
|
194
182
|
|
|
195
183
|
|
|
@@ -212,7 +200,7 @@ def _invoke(adapter, skill, args: dict, confirmed: bool, confirm_nonce: str):
|
|
|
212
200
|
|
|
213
201
|
|
|
214
202
|
def compose(plan_obj: Plan, registry, adapter, confirmed: bool = False,
|
|
215
|
-
resume_state: dict = None) -> CompositionResult:
|
|
203
|
+
resume_state: dict | None = None) -> CompositionResult:
|
|
216
204
|
"""skill-composition:按计划顺序执行各 skill。
|
|
217
205
|
|
|
218
206
|
结束态三种:
|
|
@@ -226,10 +214,9 @@ def compose(plan_obj: Plan, registry, adapter, confirmed: bool = False,
|
|
|
226
214
|
恢复已完成步骤的 upstream/executed 现场,跳过 resume_at 之前的步骤。
|
|
227
215
|
"""
|
|
228
216
|
res = CompositionResult(mode=plan_obj.flow_type)
|
|
229
|
-
upstream = {}
|
|
230
|
-
executed = []
|
|
217
|
+
upstream = {}
|
|
218
|
+
executed = []
|
|
231
219
|
|
|
232
|
-
# 【P2-2】恢复已完成步骤(断点续跑)
|
|
233
220
|
resume_at = None
|
|
234
221
|
if resume_state:
|
|
235
222
|
for tid, sid, output in resume_state.get("completed_steps", []):
|
|
@@ -239,9 +226,6 @@ def compose(plan_obj: Plan, registry, adapter, confirmed: bool = False,
|
|
|
239
226
|
resume_at = resume_state.get("resume_at")
|
|
240
227
|
|
|
241
228
|
for i, t in enumerate(plan_obj.tasks, 1):
|
|
242
|
-
# 【P2-2】跳过已完成的步骤(断点续跑):resume_at 之前的步骤不重跑;
|
|
243
|
-
# 命中断点后清空 resume_at,让断点及其之后的步骤正常执行。
|
|
244
|
-
# (若照抄"!= resume_at 就 continue",断点之后的步骤会被一并跳过)
|
|
245
229
|
if resume_at:
|
|
246
230
|
if t.id != resume_at:
|
|
247
231
|
continue
|
|
@@ -251,7 +235,6 @@ def compose(plan_obj: Plan, registry, adapter, confirmed: bool = False,
|
|
|
251
235
|
res.failed.append(t.id); continue
|
|
252
236
|
skill = registry.get(sid)
|
|
253
237
|
call_args = _build_args(skill, t.name, upstream)
|
|
254
|
-
# 关键操作二次确认码随 resume_state 回灌:同一 nonce 只能在原动作+原参数上用一次
|
|
255
238
|
_nonce = (resume_state or {}).get("confirm_nonce", "") or ""
|
|
256
239
|
out = _invoke(adapter, skill, call_args, confirmed, _nonce)
|
|
257
240
|
res.stages.append(StageResult(stage=i, skill_id=sid, result=out.to_dict()))
|
|
@@ -260,7 +243,6 @@ def compose(plan_obj: Plan, registry, adapter, confirmed: bool = False,
|
|
|
260
243
|
executed.append((sid, out.output))
|
|
261
244
|
res.completed_steps.append((t.id, sid, out.output))
|
|
262
245
|
elif out.status == "needs_confirmation":
|
|
263
|
-
# 【P2-2】待确认 → 暂停,不回滚;把断点现场打包给调用方
|
|
264
246
|
res.pending_confirmation = {
|
|
265
247
|
"task_id": t.id,
|
|
266
248
|
"skill_id": sid,
|
|
@@ -269,8 +251,6 @@ def compose(plan_obj: Plan, registry, adapter, confirmed: bool = False,
|
|
|
269
251
|
"args": call_args,
|
|
270
252
|
"completed_steps": list(res.completed_steps),
|
|
271
253
|
"resume_at": t.id,
|
|
272
|
-
# 关键操作:一次性确认码(绑定 skill + 本次 args,过期即废)。
|
|
273
|
-
# 续跑时把它塞回 resume_state 一起传,才能通过 adapter 的二次确认校验。
|
|
274
254
|
"confirm_nonce": getattr(out, "confirm_nonce", "") or "",
|
|
275
255
|
"confirm_expires_at": getattr(out, "confirm_expires_at", 0.0) or 0.0,
|
|
276
256
|
}
|
|
@@ -281,7 +261,6 @@ def compose(plan_obj: Plan, registry, adapter, confirmed: bool = False,
|
|
|
281
261
|
}
|
|
282
262
|
return res
|
|
283
263
|
else:
|
|
284
|
-
# blocked/circuit_open/error/timeout → 关键路径失败 → 逆序回滚
|
|
285
264
|
res.failed.append(t.id)
|
|
286
265
|
res.final_output = {"error": out.message, "failed_at": t.id}
|
|
287
266
|
_rollback(executed, registry, res)
|
|
@@ -290,8 +269,6 @@ def compose(plan_obj: Plan, registry, adapter, confirmed: bool = False,
|
|
|
290
269
|
return res
|
|
291
270
|
|
|
292
271
|
|
|
293
|
-
# ============ 编排器:把 6 个元 skill 串起来 ============
|
|
294
|
-
|
|
295
272
|
@dataclass
|
|
296
273
|
class PipelineResult:
|
|
297
274
|
task: str
|
|
@@ -302,7 +279,7 @@ class PipelineResult:
|
|
|
302
279
|
summary: str
|
|
303
280
|
|
|
304
281
|
@property
|
|
305
|
-
def pending_confirmation(self) -> dict:
|
|
282
|
+
def pending_confirmation(self) -> dict | None:
|
|
306
283
|
"""【P2-2】待确认状态(若无则 None)。结构见 compose 的 pending_confirmation。"""
|
|
307
284
|
return self.composition.get("pending_confirmation")
|
|
308
285
|
|
|
@@ -320,7 +297,7 @@ class AHSPipeline:
|
|
|
320
297
|
self.adapter = adapter
|
|
321
298
|
|
|
322
299
|
def run(self, task: str, confirmed: bool = False,
|
|
323
|
-
resume_state: dict = None, confirm_nonce: str = "") -> PipelineResult:
|
|
300
|
+
resume_state: dict | None = None, confirm_nonce: str = "") -> PipelineResult:
|
|
324
301
|
"""同步执行。【P2-2】resume_state 用于从待确认状态断点续跑
|
|
325
302
|
(传上次返回的 pending_confirmation;确认后调 run(task, confirmed=True, resume_state=...))。
|
|
326
303
|
|
|
@@ -329,25 +306,17 @@ class AHSPipeline:
|
|
|
329
306
|
"""
|
|
330
307
|
if confirm_nonce:
|
|
331
308
|
resume_state = {**(resume_state or {}), "confirm_nonce": confirm_nonce}
|
|
332
|
-
# session_start 钩子
|
|
333
309
|
self.adapter.hooks.fire("session_start", "", {"task": task})
|
|
334
310
|
|
|
335
|
-
# 1. router 选主 skill
|
|
336
311
|
r = route(task, self.registry)
|
|
337
312
|
|
|
338
|
-
# 2. planner 拆解(总是出计划;简单任务=单步)
|
|
339
313
|
p = plan(task, self.registry)
|
|
340
314
|
|
|
341
|
-
# 3. composition 编排执行(每步内部过 guard+breaker+hooks)
|
|
342
315
|
c = compose(p, self.registry, self.adapter,
|
|
343
316
|
confirmed=confirmed, resume_state=resume_state)
|
|
344
317
|
|
|
345
|
-
# session_end 钩子
|
|
346
318
|
self.adapter.hooks.fire("session_end", "", {"task": task})
|
|
347
319
|
|
|
348
|
-
# 【P2-2】success 判定区分 pending:待确认不是失败,但也不是成功
|
|
349
|
-
# (旧语义:pending 会进 failed → success=False;新语义:pending 非空 → success=False。
|
|
350
|
-
# 外向结果一致,语义不同——pending 的步骤没有被回滚,可以续跑)
|
|
351
320
|
success = not c.failed and bool(c.stages) and not c.pending_confirmation
|
|
352
321
|
if c.pending_confirmation:
|
|
353
322
|
summary = (f"执行 {len(c.stages)} 步,待确认:"
|
|
@@ -367,13 +336,12 @@ class AHSPipeline:
|
|
|
367
336
|
"failed": c.failed, "final": c.final_output,
|
|
368
337
|
"rolled_back": c.rolled_back, "rollback_clean": c.rollback_clean,
|
|
369
338
|
"rollback_log": c.rollback_log, "needs_manual": c.needs_manual,
|
|
370
|
-
# 【P2-2】待确认 + 已完成步骤(供前端识别/续跑)
|
|
371
339
|
"pending_confirmation": c.pending_confirmation,
|
|
372
340
|
"completed_steps": [s[0] for s in c.completed_steps]},
|
|
373
341
|
success=success, summary=summary)
|
|
374
342
|
|
|
375
343
|
async def run_async(self, task: str, confirmed: bool = False,
|
|
376
|
-
resume_state: dict = None,
|
|
344
|
+
resume_state: dict | None = None,
|
|
377
345
|
confirm_nonce: str = "") -> PipelineResult:
|
|
378
346
|
"""【P2-1】异步入口:把同步 run 放到线程池,避免阻塞事件循环。
|
|
379
347
|
|
|
@@ -12,19 +12,15 @@ class SkillPolicy:
|
|
|
12
12
|
def __init__(self, data_dir):
|
|
13
13
|
self._path = Path(data_dir) / "skill_policy.json"
|
|
14
14
|
self._lock = threading.RLock()
|
|
15
|
-
self._deny: set = set()
|
|
16
|
-
self._allow: set = set()
|
|
17
|
-
# E1 分层:policy 层 deny(管理员锁定,env FOXCLAW_POLICY_DENY + <data_dir>/policy.json)。
|
|
18
|
-
# 与 user 层物理分开、不写回 skill_policy.json;**不可被 undeny 撤销**。默认空 = 现状。
|
|
15
|
+
self._deny: set = set()
|
|
16
|
+
self._allow: set = set()
|
|
19
17
|
from ..core.config import LayeredConfig, load_policy_layer
|
|
20
18
|
policy_layer = load_policy_layer(data_dir)
|
|
21
|
-
# user 层传 self._deny(set 引用)→ 运行时增删被 merged_set 实时反映,无需重建 LayeredConfig
|
|
22
19
|
self._layers = LayeredConfig(policy=policy_layer, user={"deny": self._deny})
|
|
23
20
|
self._load()
|
|
24
21
|
|
|
25
22
|
def is_blocked(self, skill_id: str) -> bool:
|
|
26
23
|
with self._lock:
|
|
27
|
-
# deny = policy 层 ∪ user 层(只增不减);deny 压倒 allow
|
|
28
24
|
if skill_id in self._layers.merged_set("deny"):
|
|
29
25
|
return True
|
|
30
26
|
if self._allow and skill_id not in self._allow:
|
|
@@ -39,7 +35,7 @@ class SkillPolicy:
|
|
|
39
35
|
def undeny(self, skill_id: str) -> bool:
|
|
40
36
|
with self._lock:
|
|
41
37
|
if skill_id in self._layers.policy_locked("deny"):
|
|
42
|
-
return False
|
|
38
|
+
return False
|
|
43
39
|
existed = skill_id in self._deny
|
|
44
40
|
self._deny.discard(skill_id)
|
|
45
41
|
self._save()
|
|
@@ -53,15 +49,13 @@ class SkillPolicy:
|
|
|
53
49
|
def snapshot(self) -> dict:
|
|
54
50
|
with self._lock:
|
|
55
51
|
return {"deny": sorted(self._deny), "allow": sorted(self._allow),
|
|
56
|
-
"policy_deny": sorted(self._layers.policy_locked("deny"))}
|
|
52
|
+
"policy_deny": sorted(self._layers.policy_locked("deny"))}
|
|
57
53
|
|
|
58
54
|
def _load(self):
|
|
59
55
|
if not self._path.exists():
|
|
60
56
|
return
|
|
61
57
|
try:
|
|
62
58
|
d = json.loads(self._path.read_text(encoding="utf-8"))
|
|
63
|
-
# 就地更新 self._deny(不重新赋值!)——LayeredConfig.user 持有本 set 的引用,
|
|
64
|
-
# 重新赋值会断开引用,导致从盘加载的 user deny 不被 merged_set 反映(E1 集成的坑)。
|
|
65
59
|
self._deny.clear()
|
|
66
60
|
self._deny.update(d.get("deny") or [])
|
|
67
61
|
self._allow = set(d.get("allow") or [])
|
|
@@ -20,7 +20,7 @@ def _is_internal_host(host: str) -> bool:
|
|
|
20
20
|
try:
|
|
21
21
|
infos = socket.getaddrinfo(host, None)
|
|
22
22
|
except Exception:
|
|
23
|
-
return False
|
|
23
|
+
return False
|
|
24
24
|
for info in infos:
|
|
25
25
|
try:
|
|
26
26
|
addr = ipaddress.ip_address(info[4][0])
|
|
@@ -28,7 +28,7 @@ def _is_internal_host(host: str) -> bool:
|
|
|
28
28
|
continue
|
|
29
29
|
if addr.is_private or addr.is_loopback or addr.is_link_local or addr.is_reserved:
|
|
30
30
|
return True
|
|
31
|
-
if str(addr) == "169.254.169.254":
|
|
31
|
+
if str(addr) == "169.254.169.254":
|
|
32
32
|
return True
|
|
33
33
|
return False
|
|
34
34
|
|
|
@@ -69,4 +69,4 @@ def review_mcp_server(url: str) -> dict:
|
|
|
69
69
|
warnings.append("接入外部 MCP server,其返回内容为外部不可信数据,不应被当作指令执行")
|
|
70
70
|
if reasons:
|
|
71
71
|
return {"verdict": "deny", "reasons": reasons, "warnings": warnings}
|
|
72
|
-
return {"verdict": "warn", "reasons": [], "warnings": warnings}
|
|
72
|
+
return {"verdict": "warn", "reasons": [], "warnings": warnings}
|
|
@@ -1,7 +1,3 @@
|
|
|
1
|
-
/* 鉴权注入:后端若设了 FOXCLAW_API_KEY,同源 /api、/knowledge 请求需带 X-API-Key。
|
|
2
|
-
从 localStorage.foxclawApiKey 读取并包一层 fetch 自动注入。
|
|
3
|
-
必须最先加载(在 settings.js 等业务模块之前),否则模块顶层提前发出的请求
|
|
4
|
-
(如 settings.js 的 loadModel → /api/model)会漏带 key 被 401。 */
|
|
5
1
|
(function(){
|
|
6
2
|
if(window.__foxclawFetchPatched) return;
|
|
7
3
|
const _fetch=window.fetch.bind(window);
|
|
@@ -10,14 +6,11 @@
|
|
|
10
6
|
try{
|
|
11
7
|
const key=localStorage.getItem('foxclawApiKey');
|
|
12
8
|
const u=String(url||'');
|
|
13
|
-
// /knowledge/* 与 /api/* 一样受后端可选鉴权守护,两者都要注入 key
|
|
14
|
-
// /stats 是 HKC 统计数据面,后端已纳入守护,同样需要注入
|
|
15
9
|
if(key && (u.indexOf('/api')>=0 || u.indexOf('/knowledge')>=0 || u.indexOf('/stats')>=0)){
|
|
16
10
|
opts=opts||{}; opts.headers=Object.assign({}, opts.headers, {'X-API-Key':key});
|
|
17
11
|
}
|
|
18
12
|
}catch(e){}
|
|
19
13
|
const p=_fetch(url,opts);
|
|
20
|
-
// 401 检测:后端开了鉴权但本机浏览器没配 key(或 key 错)→ 弹提示条引导(只弹一次)
|
|
21
14
|
try{
|
|
22
15
|
p.then(r=>{
|
|
23
16
|
if(r && r.status===401 && !_authHinted){
|
|
@@ -1,11 +1,3 @@
|
|
|
1
|
-
// chat_model_picker.js — chat 输入框旁的「下拉式模型选择器」
|
|
2
|
-
//
|
|
3
|
-
// 视觉:参考图1 (CodeBuddy 模型列表) + 图2 (「添加自定义」对话框)。
|
|
4
|
-
// 数据:全部走 foxclaw/api/routes/llm_admin.py 的 REST 端点(GET/POST/DELETE)。
|
|
5
|
-
// 状态:切换写入 FOXCLAW_DATA_DIR/.current_model,model_router 用 mtime 缓存 lazy reload,
|
|
6
|
-
// 用户改完**无需重启 foxclaw**,下次 chat 自动走新模型。
|
|
7
|
-
//
|
|
8
|
-
// 不会外泄:GET /api/llm/models 只返 has_key 布尔;POST /api/llm/custom/add 只在服务端保存。
|
|
9
1
|
|
|
10
2
|
const API_BASE = (typeof window !== 'undefined' && window.API_BASE) || '';
|
|
11
3
|
const _api = (p) => `${API_BASE}/api/llm${p}`;
|
|
@@ -13,18 +5,12 @@ const _api = (p) => `${API_BASE}/api/llm${p}`;
|
|
|
13
5
|
let _models = { providers: [], custom: [], current: null };
|
|
14
6
|
let _menuOpen = false;
|
|
15
7
|
|
|
16
|
-
// ── 工具 ─────────────────────────────────────────────
|
|
17
|
-
|
|
18
8
|
function _esc(s) {
|
|
19
9
|
return String(s == null ? '' : s)
|
|
20
10
|
.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>')
|
|
21
11
|
.replace(/"/g, '"').replace(/'/g, ''');
|
|
22
12
|
}
|
|
23
13
|
|
|
24
|
-
// BASE URL 规范化(与后端 core/model_router.normalize_base_url 同规则):
|
|
25
|
-
// 无 scheme → 补 https://;path 为空 → 补 /v1;已有 path(/v1、/compatible-mode/v1…)原样保留。
|
|
26
|
-
// 病根:用户照厂商文档填 https://api.deepseek.com,SDK 拼出 /chat/completions → 多数网关 404/401,
|
|
27
|
-
// 用户以为是"模型填不上"。前端先收拾干净,后端再兜一层(旧记录 / 直接调 API 的场景)。
|
|
28
14
|
function _normalizeBaseUrl(u) {
|
|
29
15
|
let s = String(u || '').trim();
|
|
30
16
|
if (!s) return s;
|
|
@@ -37,7 +23,6 @@ function _normalizeBaseUrl(u) {
|
|
|
37
23
|
return `${parsed.protocol}//${parsed.host}${path}${parsed.search || ''}`;
|
|
38
24
|
}
|
|
39
25
|
|
|
40
|
-
// 常见厂商的模型 ID 提示(只做 placeholder 引导,不阻断保存 —— 自建网关可能有自定义名)
|
|
41
26
|
const _MODEL_HINTS = [
|
|
42
27
|
{ re: /api\.deepseek\.com/i, hint: 'deepseek-chat', note: 'DeepSeek 官方 ID:deepseek-chat / deepseek-reasoner' },
|
|
43
28
|
{ re: /api\.openai\.com/i, hint: 'gpt-4o-mini', note: 'OpenAI 官方 ID:gpt-4o-mini / gpt-4o' },
|
|
@@ -61,7 +46,6 @@ function _autoCurrent() {
|
|
|
61
46
|
return !_models.current;
|
|
62
47
|
}
|
|
63
48
|
|
|
64
|
-
// 当前显示在 pill 上的名称 + 颜色
|
|
65
49
|
function _currentLabel() {
|
|
66
50
|
if (_isCustomCurrent()) {
|
|
67
51
|
const m = _models.custom.find(m => m.name === _models.current.name);
|
|
@@ -76,15 +60,12 @@ function _currentLabel() {
|
|
|
76
60
|
return { name: 'Auto', color: '#7e93b8', isCustom: false };
|
|
77
61
|
}
|
|
78
62
|
|
|
79
|
-
// ── 渲染:菜单内容 ─────────────────────────────────────
|
|
80
|
-
|
|
81
63
|
function renderMenu() {
|
|
82
64
|
const menu = document.getElementById('chatModelMenu');
|
|
83
65
|
if (!menu) return;
|
|
84
66
|
const cur = _currentLabel();
|
|
85
67
|
let h = '';
|
|
86
68
|
|
|
87
|
-
// Auto(让用户能清掉选择,退回 .env 默认)
|
|
88
69
|
h += `<div class="mp-item ${_autoCurrent() ? 'active' : ''}" data-act="auto">
|
|
89
70
|
<span class="mp-dot" style="background:#7e93b8"></span>
|
|
90
71
|
<span class="mp-name"><b>Auto</b><span>跟随 .env 默认</span></span>
|
|
@@ -107,9 +88,6 @@ function renderMenu() {
|
|
|
107
88
|
h += '<div class="mp-sep">自定义</div>';
|
|
108
89
|
_models.custom.forEach(m => {
|
|
109
90
|
const isCur = _isCustomCurrent() && _models.current.name === m.name;
|
|
110
|
-
// 三个 tag 都常驻:当前(按需显隐)+ ✎ 编辑 + ✕ 删除
|
|
111
|
-
// 即使是当前选中的,用户也应该能直接编辑它(改 key / 加 group_id),不能强制切走
|
|
112
|
-
// 删除当前选中的也安全 —— 后端 DELETE 已处理清 .current_model
|
|
113
91
|
h += `<div class="mp-item ${isCur ? 'active' : ''}" data-act="custom" data-name="${_esc(m.name)}">
|
|
114
92
|
<span class="mp-dot" style="background:#a78bfa;box-shadow:0 0 6px #a78bfa"></span>
|
|
115
93
|
<span class="mp-name"><b>${_esc(m.name)}</b><span>${_esc(m.model)}</span></span>
|
|
@@ -127,7 +105,6 @@ function renderMenu() {
|
|
|
127
105
|
|
|
128
106
|
menu.innerHTML = h;
|
|
129
107
|
|
|
130
|
-
// 事件
|
|
131
108
|
menu.querySelectorAll('.mp-item').forEach(el => {
|
|
132
109
|
el.addEventListener('click', (e) => {
|
|
133
110
|
if (e.target.classList.contains('mp-del')) {
|
|
@@ -164,8 +141,6 @@ function renderPill() {
|
|
|
164
141
|
if (name) name.textContent = cur.name;
|
|
165
142
|
}
|
|
166
143
|
|
|
167
|
-
// ── API ─────────────────────────────────────────────
|
|
168
|
-
|
|
169
144
|
async function loadModels() {
|
|
170
145
|
try {
|
|
171
146
|
const r = await fetch(_api('/models'));
|
|
@@ -230,14 +205,12 @@ async function addCustom(payload) {
|
|
|
230
205
|
}
|
|
231
206
|
|
|
232
207
|
async function getCustom(name) {
|
|
233
|
-
// 取单个自定义模型详情(后端不返回 api_key 明文,只返 has_key)
|
|
234
208
|
const r = await fetch(_api(`/custom/${encodeURIComponent(name)}`));
|
|
235
209
|
if (!r.ok) throw new Error(`HTTP ${r.status}`);
|
|
236
210
|
return r.json();
|
|
237
211
|
}
|
|
238
212
|
|
|
239
213
|
async function updateCustom(name, payload) {
|
|
240
|
-
// payload 字段语义:填了非空 = 用新值;没传(或不传该字段)= 不变;api_key 留空=保留原值
|
|
241
214
|
const r = await fetch(_api(`/custom/${encodeURIComponent(name)}`), {
|
|
242
215
|
method: 'PUT',
|
|
243
216
|
headers: { 'Content-Type': 'application/json' },
|
|
@@ -252,7 +225,6 @@ async function updateCustom(name, payload) {
|
|
|
252
225
|
}
|
|
253
226
|
|
|
254
227
|
async function editCustom(name) {
|
|
255
|
-
// 加载详情 → 复用 openAddDialog 切到 edit 模式预填表单
|
|
256
228
|
try {
|
|
257
229
|
const m = await getCustom(name);
|
|
258
230
|
openAddDialog('edit', { ...m, _editingName: name });
|
|
@@ -274,9 +246,6 @@ async function testModel(source, provider, name) {
|
|
|
274
246
|
return r.json();
|
|
275
247
|
}
|
|
276
248
|
|
|
277
|
-
// ── 添加对话框(图2 风格)────────────────────────────
|
|
278
|
-
|
|
279
|
-
// 对话框全局状态:add / edit 模式共用同一表单
|
|
280
249
|
let _cmMode = 'add'; // 'add' | 'edit'
|
|
281
250
|
let _cmEditingName = null; // edit 模式下的原名(PUT URL 用);改名后仍指向磁盘上那条记录
|
|
282
251
|
|
|
@@ -289,7 +258,6 @@ function openAddDialog(mode = 'add', preset = null) {
|
|
|
289
258
|
const mask = document.getElementById('cmAddMask');
|
|
290
259
|
if (!mask) return;
|
|
291
260
|
|
|
292
|
-
// 标题 + 副标题
|
|
293
261
|
const title = document.getElementById('cmAddTitle');
|
|
294
262
|
if (title) {
|
|
295
263
|
title.textContent = mode === 'edit' && preset?.name
|
|
@@ -297,7 +265,6 @@ function openAddDialog(mode = 'add', preset = null) {
|
|
|
297
265
|
: '添加自定义模型';
|
|
298
266
|
}
|
|
299
267
|
|
|
300
|
-
// 填表单 — api_key 永远不清空回填(安全考虑)
|
|
301
268
|
const set = (id, v) => { const el = document.getElementById(id); if (el) el.value = v || ''; };
|
|
302
269
|
set('cm_name', preset?.name);
|
|
303
270
|
set('cm_base_url', preset?.base_url);
|
|
@@ -309,7 +276,6 @@ function openAddDialog(mode = 'add', preset = null) {
|
|
|
309
276
|
set('cm_note', preset?.note);
|
|
310
277
|
_applyModelHint(preset?.base_url);
|
|
311
278
|
|
|
312
|
-
// API KEY 字段 placeholder 区分 add / edit
|
|
313
279
|
const keyInp = document.getElementById('cm_api_key');
|
|
314
280
|
if (keyInp) {
|
|
315
281
|
keyInp.placeholder = (mode === 'edit' && preset?.has_key)
|
|
@@ -323,7 +289,6 @@ function openAddDialog(mode = 'add', preset = null) {
|
|
|
323
289
|
setTimeout(() => document.getElementById('cm_name')?.focus(), 50);
|
|
324
290
|
}
|
|
325
291
|
|
|
326
|
-
// 已知厂商的"模型 ID 写错"软提示(只警告不阻断 —— 自建网关可以有自定义别名)
|
|
327
292
|
function _modelWarning(baseUrl, modelId) {
|
|
328
293
|
const s = String(baseUrl || ''), m = String(modelId || '').trim();
|
|
329
294
|
if (!m) return '';
|
|
@@ -332,7 +297,6 @@ function _modelWarning(baseUrl, modelId) {
|
|
|
332
297
|
return '';
|
|
333
298
|
}
|
|
334
299
|
|
|
335
|
-
// 按 BASE URL 域名更新「模型 ID」的 placeholder + 副标题提示(不阻断保存)
|
|
336
300
|
function _applyModelHint(baseUrl) {
|
|
337
301
|
const hint = _hintForBaseUrl(baseUrl);
|
|
338
302
|
const inp = document.getElementById('cm_model');
|
|
@@ -360,7 +324,6 @@ function _readForm() {
|
|
|
360
324
|
const v = (id) => (document.getElementById(id)?.value || '').trim();
|
|
361
325
|
const out = {
|
|
362
326
|
name: v('cm_name'),
|
|
363
|
-
// 保存前再规范化一次(用户可能没触发 blur 直接点保存);后端也会兜一层
|
|
364
327
|
base_url: _normalizeBaseUrl(v('cm_base_url')),
|
|
365
328
|
api_key: v('cm_api_key'),
|
|
366
329
|
model: v('cm_model'),
|
|
@@ -384,7 +347,6 @@ function _readForm() {
|
|
|
384
347
|
async function _onSave() {
|
|
385
348
|
try {
|
|
386
349
|
const payload = _readForm();
|
|
387
|
-
// name / base_url / model 必填;api_key 在 edit 模式下留空 = 保留(后端校验)
|
|
388
350
|
if (!payload.name || !payload.base_url || !payload.model) {
|
|
389
351
|
_setMsg('请填全 * 号必填项', 'err');
|
|
390
352
|
return;
|
|
@@ -401,13 +363,9 @@ async function _onSave() {
|
|
|
401
363
|
} else {
|
|
402
364
|
const r = await updateCustom(_cmEditingName, payload);
|
|
403
365
|
savedName = r.name;
|
|
404
|
-
// 改名后,_cmEditingName 失效 → 下次 _onTest 会失败 → 清掉,避免误测
|
|
405
366
|
_cmEditingName = r.name;
|
|
406
367
|
}
|
|
407
368
|
await loadModels();
|
|
408
|
-
// 保存即生效 —— 用户点「保存」的诉求就是「用它」。
|
|
409
|
-
// 病根:此前只写盘不切换,聊天仍走 .env 的旧 provider,用户看到 AuthenticationError/stub,
|
|
410
|
-
// 以为是"模型填不上";实际上是要再回菜单点一次这个条目。现在省掉这一步。
|
|
411
369
|
_setMsg('已保存,正在切换…', 'info');
|
|
412
370
|
const ok = await selectModel('custom', null, savedName);
|
|
413
371
|
_setMsg(ok ? `已保存并切换到 ${savedName}` : `已保存 ${savedName},但切换失败(见弹窗)`,
|
|
@@ -426,8 +384,6 @@ async function _onTest() {
|
|
|
426
384
|
return;
|
|
427
385
|
}
|
|
428
386
|
if (_cmMode === 'edit') {
|
|
429
|
-
// edit 模式:测磁盘上**已保存**的配置(避免被 form 里未保存的改动误导)
|
|
430
|
-
// 若想测表单里的新值,先点「保存」再点「测试连通性」
|
|
431
387
|
_setMsg(`测试已保存配置(${_cmEditingName})…`, 'info');
|
|
432
388
|
const r = await testModel('custom', null, _cmEditingName);
|
|
433
389
|
if (r.ok) {
|
|
@@ -437,7 +393,6 @@ async function _onTest() {
|
|
|
437
393
|
}
|
|
438
394
|
return;
|
|
439
395
|
}
|
|
440
|
-
// add 模式:必须 name + api_key,临时 add 一个 tmp 名测,失败保留
|
|
441
396
|
if (!payload.name || !payload.api_key) {
|
|
442
397
|
_setMsg('add 模式请先填「模型名称」+「API KEY」再测试', 'err');
|
|
443
398
|
return;
|
|
@@ -446,7 +401,6 @@ async function _onTest() {
|
|
|
446
401
|
await addCustom(payload);
|
|
447
402
|
const r = await testModel('custom', null, payload.name);
|
|
448
403
|
if (r.ok) {
|
|
449
|
-
// 测试通过 = 这份配置真的能用 → 直接切过去,不用用户再回菜单点一次
|
|
450
404
|
await loadModels();
|
|
451
405
|
await selectModel('custom', null, payload.name);
|
|
452
406
|
_setMsg(`✓ 连通 (${r.latency_ms} ms, model=${r.model}) — 已切换`, 'ok');
|
|
@@ -459,8 +413,6 @@ async function _onTest() {
|
|
|
459
413
|
}
|
|
460
414
|
}
|
|
461
415
|
|
|
462
|
-
// ── 事件绑定 ─────────────────────────────────────
|
|
463
|
-
|
|
464
416
|
function bindEvents() {
|
|
465
417
|
const pill = document.getElementById('chatModelPill');
|
|
466
418
|
const menu = document.getElementById('chatModelMenu');
|
|
@@ -470,7 +422,6 @@ function bindEvents() {
|
|
|
470
422
|
_menuOpen = !_menuOpen;
|
|
471
423
|
menu.classList.toggle('show', _menuOpen);
|
|
472
424
|
});
|
|
473
|
-
// 点外面收起
|
|
474
425
|
document.addEventListener('click', (e) => {
|
|
475
426
|
if (!_menuOpen) return;
|
|
476
427
|
if (e.target.closest('#chatModelPick')) return;
|
|
@@ -500,7 +451,6 @@ function bindEvents() {
|
|
|
500
451
|
if (e.target === mask) closeAddDialog();
|
|
501
452
|
});
|
|
502
453
|
}
|
|
503
|
-
// BASE URL 失焦即规范化(补 https:// / 补 /v1),并联动模型 ID 提示
|
|
504
454
|
const baseInp = document.getElementById('cm_base_url');
|
|
505
455
|
if (baseInp) {
|
|
506
456
|
baseInp.addEventListener('blur', () => {
|
|
@@ -521,16 +471,12 @@ function bindEvents() {
|
|
|
521
471
|
});
|
|
522
472
|
}
|
|
523
473
|
|
|
524
|
-
// ── 启动 ─────────────────────────────────────────
|
|
525
|
-
|
|
526
474
|
(function init() {
|
|
527
475
|
bindEvents();
|
|
528
476
|
loadModels();
|
|
529
|
-
// 多 tab 同步:监听 storage 事件
|
|
530
477
|
window.addEventListener('storage', (e) => {
|
|
531
478
|
if (e.key === '__foxclaw_model_changed__') loadModels();
|
|
532
479
|
});
|
|
533
|
-
// 切完模型后,广播给其它 tab(简版:周期性 reload)
|
|
534
480
|
setInterval(() => {
|
|
535
481
|
if (_menuOpen) loadModels();
|
|
536
482
|
}, 5000);
|