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
|
@@ -0,0 +1,226 @@
|
|
|
1
|
+
"""技能来源解析与下载:ClawHub / GitHub / http(s) 包地址 / 本地路径。
|
|
2
|
+
|
|
3
|
+
`foxclaw skills install <ref>` 的统一入口。ref 形式:
|
|
4
|
+
|
|
5
|
+
@owner/slug ClawHub 作用域包(与 `openclaw skills install` 同源)
|
|
6
|
+
owner/slug 同上,省略 @
|
|
7
|
+
github:owner/repo[/sub][@ref] GitHub 仓库里的 SKILL.md(默认 HEAD)
|
|
8
|
+
https://...zip|tar.gz|.md 直接给包地址
|
|
9
|
+
./skill.zip | ./SKILL.md 本地包 / 文件 / 目录
|
|
10
|
+
|
|
11
|
+
安全口径:ClawHub 在**下载前**先查 moderation,`isMalwareBlocked` 直接拒绝;
|
|
12
|
+
`isSuspicious` 只作告警交给调用方确认。包内带可执行脚本时标 `has_code` ——
|
|
13
|
+
foxclaw 的 SKILL.md 是纯提示词,不执行包内代码,这点必须让调用方说清楚。
|
|
14
|
+
"""
|
|
15
|
+
from __future__ import annotations
|
|
16
|
+
|
|
17
|
+
import json
|
|
18
|
+
import os
|
|
19
|
+
import re
|
|
20
|
+
import urllib.error
|
|
21
|
+
import urllib.parse
|
|
22
|
+
import urllib.request
|
|
23
|
+
from dataclasses import dataclass, field
|
|
24
|
+
from pathlib import Path
|
|
25
|
+
|
|
26
|
+
from .archive import PackageError, has_code, read_package
|
|
27
|
+
|
|
28
|
+
DEFAULT_REGISTRY = "https://clawhub.ai"
|
|
29
|
+
MAX_BYTES = 10 * 1024 * 1024
|
|
30
|
+
UA = "foxclaw-skills/1 (+https://github.com/snowfoxHQ/FoxClaw)"
|
|
31
|
+
_ARCHIVE_EXT = (".zip", ".tar.gz", ".tgz", ".tar")
|
|
32
|
+
_LOCAL_ID = re.compile(r"^@?([A-Za-z0-9][\w.-]*)/([\w.-]+)$")
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
class HttpError(PackageError):
|
|
36
|
+
"""远端返回非 2xx。status 保留下来供调用方区分 404(不存在)与其他错误。"""
|
|
37
|
+
|
|
38
|
+
def __init__(self, status: int, url: str, detail: str):
|
|
39
|
+
self.status = status
|
|
40
|
+
super().__init__(f"{url} 返回 {status}: {detail or '(无响应体)'}")
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
class MalwareBlocked(PackageError):
|
|
44
|
+
"""来源方已判定为恶意,任何 --yes 都不放行。"""
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
@dataclass
|
|
48
|
+
class SkillPackage:
|
|
49
|
+
skill_md: str
|
|
50
|
+
source: str
|
|
51
|
+
origin: str
|
|
52
|
+
files: list = field(default_factory=list)
|
|
53
|
+
meta: dict = field(default_factory=dict)
|
|
54
|
+
version: str = ""
|
|
55
|
+
moderation: dict = field(default_factory=dict)
|
|
56
|
+
|
|
57
|
+
@property
|
|
58
|
+
def has_code(self) -> bool:
|
|
59
|
+
return has_code(self.files)
|
|
60
|
+
|
|
61
|
+
@property
|
|
62
|
+
def suspicious(self) -> bool:
|
|
63
|
+
return bool(self.moderation.get("isSuspicious"))
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def registry() -> str:
|
|
67
|
+
"""ClawHub 地址。内网镜像走 FOXCLAW_CLAWHUB_REGISTRY,别改代码。"""
|
|
68
|
+
return (os.environ.get("FOXCLAW_CLAWHUB_REGISTRY", "").strip()
|
|
69
|
+
or DEFAULT_REGISTRY).rstrip("/")
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def _http_get(url: str, timeout: float = 25.0,
|
|
73
|
+
max_bytes: int = MAX_BYTES) -> tuple[bytes, str]:
|
|
74
|
+
"""GET 一个 URL,返回 (bytes, content-type)。超限/4xx 都抛 PackageError。"""
|
|
75
|
+
req = urllib.request.Request(url, headers={"User-Agent": UA})
|
|
76
|
+
try:
|
|
77
|
+
with urllib.request.urlopen(req, timeout=timeout) as resp:
|
|
78
|
+
ctype = (resp.headers.get("Content-Type") or "").split(";")[0].lower()
|
|
79
|
+
chunks, total = [], 0
|
|
80
|
+
while True:
|
|
81
|
+
buf = resp.read(65536)
|
|
82
|
+
if not buf:
|
|
83
|
+
break
|
|
84
|
+
total += len(buf)
|
|
85
|
+
if total > max_bytes:
|
|
86
|
+
raise PackageError(f"下载超过上限({max_bytes} 字节): {url}")
|
|
87
|
+
chunks.append(buf)
|
|
88
|
+
return b"".join(chunks), ctype
|
|
89
|
+
except urllib.error.HTTPError as e:
|
|
90
|
+
detail = e.read(300).decode("utf-8", "replace").strip()
|
|
91
|
+
raise HttpError(e.code, url, detail) from None
|
|
92
|
+
except urllib.error.URLError as e:
|
|
93
|
+
raise PackageError(f"下载失败 {url}: {e.reason}") from None
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
def _json_get(url: str, timeout: float = 25.0) -> dict:
|
|
97
|
+
raw, _ = _http_get(url, timeout)
|
|
98
|
+
try:
|
|
99
|
+
got = json.loads(raw.decode("utf-8", "replace"))
|
|
100
|
+
except Exception:
|
|
101
|
+
raise PackageError(f"{url} 返回的不是 JSON") from None
|
|
102
|
+
return got if isinstance(got, dict) else {}
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
def fetch(ref: str, timeout: float = 25.0) -> SkillPackage:
|
|
106
|
+
"""把一个 ref 解析成 SkillPackage(已下载并解出 SKILL.md)。"""
|
|
107
|
+
r = (ref or "").strip()
|
|
108
|
+
if not r:
|
|
109
|
+
raise PackageError("缺少 skill 来源")
|
|
110
|
+
low = r.lower()
|
|
111
|
+
if low.startswith(("http://", "https://")):
|
|
112
|
+
return _from_url(r, timeout)
|
|
113
|
+
if low.startswith("github:"):
|
|
114
|
+
return _from_github(r[len("github:"):], timeout)
|
|
115
|
+
local = Path(os.path.expanduser(r))
|
|
116
|
+
if local.exists():
|
|
117
|
+
return _from_local(local)
|
|
118
|
+
m = _LOCAL_ID.match(r)
|
|
119
|
+
if m:
|
|
120
|
+
return _from_clawhub(m.group(1), m.group(2), timeout)
|
|
121
|
+
raise PackageError(
|
|
122
|
+
"无法识别的来源。支持:@owner/slug(ClawHub)、github:owner/repo[/子路径][@ref]、"
|
|
123
|
+
"http(s):// 包地址、本地 .zip/.tar.gz/SKILL.md/目录")
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
def _from_clawhub(owner: str, slug: str, timeout: float) -> SkillPackage:
|
|
127
|
+
"""ClawHub:先查详情(拿版本 + 审核状态),再下载 zip。
|
|
128
|
+
|
|
129
|
+
先查详情而不是直接下载,是为了在落盘前拦住 `isMalwareBlocked` —— openclaw 自己
|
|
130
|
+
也把 moderation 摆在安装前的核验位置,我们不能比它更松。
|
|
131
|
+
"""
|
|
132
|
+
base = registry()
|
|
133
|
+
q_slug = urllib.parse.quote(slug, safe="")
|
|
134
|
+
q_owner = urllib.parse.quote(owner, safe="")
|
|
135
|
+
try:
|
|
136
|
+
detail = _json_get(f"{base}/api/v1/skills/{q_slug}?ownerHandle={q_owner}", timeout)
|
|
137
|
+
except HttpError as e:
|
|
138
|
+
if e.status == 404:
|
|
139
|
+
raise PackageError(f"ClawHub 上找不到 @{owner}/{slug}") from None
|
|
140
|
+
raise
|
|
141
|
+
mod = detail.get("moderation") or {}
|
|
142
|
+
if mod.get("isMalwareBlocked"):
|
|
143
|
+
reason = mod.get("summary") or "来源方未给说明"
|
|
144
|
+
raise MalwareBlocked(f"@{owner}/{slug} 已被 ClawHub 判为恶意并拦截:{reason}")
|
|
145
|
+
version = str((detail.get("latestVersion") or {}).get("version") or "")
|
|
146
|
+
data, ctype = _http_get(
|
|
147
|
+
f"{base}/api/v1/download?slug={q_slug}&ownerHandle={q_owner}", timeout)
|
|
148
|
+
if data[:1] == b"{" or "json" in ctype:
|
|
149
|
+
try:
|
|
150
|
+
handoff = json.loads(data.decode("utf-8", "replace")) or {}
|
|
151
|
+
except Exception:
|
|
152
|
+
handoff = {}
|
|
153
|
+
archive = str(handoff.get("archiveUrl") or "")
|
|
154
|
+
if not archive:
|
|
155
|
+
raise PackageError(f"@{owner}/{slug} 无托管版本,且返回里没有可下载地址")
|
|
156
|
+
data, _ = _http_get(archive, timeout)
|
|
157
|
+
pkg = read_package(data)
|
|
158
|
+
return SkillPackage(
|
|
159
|
+
skill_md=pkg["skill_md"], source="clawhub",
|
|
160
|
+
origin=f"@{owner}/{slug}" + (f"@{version}" if version else ""),
|
|
161
|
+
files=pkg["files"], meta=pkg["meta"], version=version, moderation=mod)
|
|
162
|
+
|
|
163
|
+
|
|
164
|
+
def _from_github(spec: str, timeout: float) -> SkillPackage:
|
|
165
|
+
"""github:owner/repo[/子路径][@ref] → codeload 的 tar.gz(不占 GitHub API 配额)。"""
|
|
166
|
+
body, _, ref = spec.partition("@")
|
|
167
|
+
parts = [p for p in body.split("/") if p]
|
|
168
|
+
if len(parts) < 2:
|
|
169
|
+
raise PackageError("github: 形式应为 github:owner/repo[/子路径][@ref]")
|
|
170
|
+
owner, repo = parts[0], parts[1]
|
|
171
|
+
sub = "/".join(parts[2:])
|
|
172
|
+
url = f"https://codeload.github.com/{owner}/{repo}/tar.gz/{ref or 'HEAD'}"
|
|
173
|
+
data, _ = _http_get(url, timeout)
|
|
174
|
+
pkg = read_package(data, subpath=sub)
|
|
175
|
+
origin = f"github:{owner}/{repo}" + (f"/{sub}" if sub else "") + (f"@{ref}" if ref else "")
|
|
176
|
+
return SkillPackage(skill_md=pkg["skill_md"], source="github", origin=origin,
|
|
177
|
+
files=pkg["files"], meta=pkg["meta"])
|
|
178
|
+
|
|
179
|
+
|
|
180
|
+
def _from_url(url: str, timeout: float) -> SkillPackage:
|
|
181
|
+
"""直链:归档当包解,markdown/纯文本当单文件 SKILL.md。"""
|
|
182
|
+
data, ctype = _http_get(url, timeout)
|
|
183
|
+
path = urllib.parse.urlparse(url).path.lower()
|
|
184
|
+
name = path.rsplit("/", 1)[-1] or "SKILL.md"
|
|
185
|
+
textish = path.endswith((".md", ".markdown")) or ctype in ("text/markdown", "text/plain")
|
|
186
|
+
is_archive = data[:2] == b"PK" or data[:2] == b"\x1f\x8b" or path.endswith(_ARCHIVE_EXT)
|
|
187
|
+
if textish and not is_archive:
|
|
188
|
+
text = data.decode("utf-8", "replace")
|
|
189
|
+
return SkillPackage(skill_md=text, source="url", origin=url, files=[name])
|
|
190
|
+
pkg = read_package(data)
|
|
191
|
+
return SkillPackage(skill_md=pkg["skill_md"], source="url", origin=url,
|
|
192
|
+
files=pkg["files"], meta=pkg["meta"])
|
|
193
|
+
|
|
194
|
+
|
|
195
|
+
def _from_local(path: Path) -> SkillPackage:
|
|
196
|
+
"""本地:目录(找 SKILL.md)、单 .md、或 .zip/.tar.gz 包。"""
|
|
197
|
+
origin = str(path)
|
|
198
|
+
if path.is_dir():
|
|
199
|
+
direct = path / "SKILL.md"
|
|
200
|
+
if direct.is_file():
|
|
201
|
+
text = direct.read_text(encoding="utf-8", errors="replace")
|
|
202
|
+
meta = _read_meta(path / "_meta.json")
|
|
203
|
+
else:
|
|
204
|
+
cands = sorted(path.glob("*/SKILL.md")) + sorted(path.glob("*/*/SKILL.md"))
|
|
205
|
+
if len(cands) != 1:
|
|
206
|
+
raise PackageError(
|
|
207
|
+
f"{path} 里没找到唯一的 SKILL.md(直接放 SKILL.md,或只有一个子目录带它)")
|
|
208
|
+
text = cands[0].read_text(encoding="utf-8", errors="replace")
|
|
209
|
+
meta = _read_meta(cands[0].parent / "_meta.json")
|
|
210
|
+
files = [p.relative_to(path).as_posix() for p in path.rglob("*") if p.is_file()]
|
|
211
|
+
return SkillPackage(skill_md=text, source="local", origin=origin,
|
|
212
|
+
files=files, meta=meta)
|
|
213
|
+
if path.suffix.lower() in (".md", ".markdown"):
|
|
214
|
+
return SkillPackage(skill_md=path.read_text(encoding="utf-8", errors="replace"),
|
|
215
|
+
source="local", origin=origin, files=[path.name])
|
|
216
|
+
pkg = read_package(path.read_bytes())
|
|
217
|
+
return SkillPackage(skill_md=pkg["skill_md"], source="local", origin=origin,
|
|
218
|
+
files=pkg["files"], meta=pkg["meta"])
|
|
219
|
+
|
|
220
|
+
|
|
221
|
+
def _read_meta(p: Path) -> dict:
|
|
222
|
+
try:
|
|
223
|
+
got = json.loads(p.read_text(encoding="utf-8"))
|
|
224
|
+
return got if isinstance(got, dict) else {}
|
|
225
|
+
except Exception:
|
|
226
|
+
return {}
|
|
@@ -13,60 +13,56 @@ Foxclaw — 标准生命周期钩子点 (Lifecycle Hooks Specification)
|
|
|
13
13
|
"""
|
|
14
14
|
from __future__ import annotations
|
|
15
15
|
|
|
16
|
+
from typing import Literal, cast
|
|
17
|
+
|
|
16
18
|
|
|
17
|
-
# ── 标准钩子点(生命周期顺序)──────────────────────────────
|
|
18
19
|
class Hooks:
|
|
19
20
|
"""标准钩子点常量。用 Hooks.BEFORE_TOOL_CALL 而非裸字符串,避免拼写错误。"""
|
|
20
21
|
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
SESSION_END = "session_end" # 一次任务结束
|
|
22
|
+
SESSION_START = "session_start"
|
|
23
|
+
AGENT_START = "agent_start"
|
|
24
|
+
AGENT_END = "agent_end"
|
|
25
|
+
SESSION_END = "session_end"
|
|
26
26
|
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
ON_LOOP = "on_loop" # 检测到死循环
|
|
27
|
+
BEFORE_TOOL_CALL = "before_tool_call"
|
|
28
|
+
AFTER_TOOL_CALL = "after_tool_call"
|
|
29
|
+
ON_ERROR = "on_error"
|
|
30
|
+
ON_LOOP = "on_loop"
|
|
32
31
|
|
|
33
|
-
|
|
34
|
-
BEFORE_MESSAGE_WRITE = "before_message_write" # 写入/发送消息前(可改写)
|
|
32
|
+
BEFORE_MESSAGE_WRITE = "before_message_write"
|
|
35
33
|
|
|
36
34
|
|
|
37
|
-
# 所有标准钩子点(供校验)
|
|
38
35
|
ALL_HOOKS = frozenset({
|
|
39
36
|
Hooks.SESSION_START, Hooks.AGENT_START, Hooks.AGENT_END, Hooks.SESSION_END,
|
|
40
37
|
Hooks.BEFORE_TOOL_CALL, Hooks.AFTER_TOOL_CALL, Hooks.ON_ERROR, Hooks.ON_LOOP,
|
|
41
38
|
Hooks.BEFORE_MESSAGE_WRITE,
|
|
42
39
|
})
|
|
43
40
|
|
|
44
|
-
# 拥有阻断权的钩子(只有这些返回 {block:True} 才生效)
|
|
45
41
|
BLOCKING_HOOKS = frozenset({Hooks.BEFORE_TOOL_CALL})
|
|
46
42
|
|
|
47
|
-
# 可注入 additionalContext 的钩子(E2:非阻断,但其返回的 additional_context 会被并入推理上下文)。
|
|
48
|
-
# agent_start = 一次 Agent 任务的推理开始点,对应范式 pre_reasoning —— 推理前注入上下文的落点。
|
|
49
43
|
CONTEXT_HOOKS = frozenset({Hooks.AGENT_START})
|
|
50
44
|
|
|
51
|
-
# 工具级钩子(按 skill_id 做 matcher 过滤)
|
|
52
45
|
TOOL_SCOPED_HOOKS = frozenset({
|
|
53
46
|
Hooks.BEFORE_TOOL_CALL, Hooks.AFTER_TOOL_CALL, Hooks.ON_ERROR, Hooks.ON_LOOP,
|
|
54
47
|
})
|
|
55
48
|
|
|
56
|
-
# 向后兼容 + 语义别名 → 标准名
|
|
57
49
|
ALIAS = {
|
|
58
50
|
"pre_skill": Hooks.BEFORE_TOOL_CALL,
|
|
59
51
|
"post_skill": Hooks.AFTER_TOOL_CALL,
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
"pre_reasoning": Hooks.AGENT_START, # 一次 Agent 任务推理开始前(可注入 additionalContext)
|
|
63
|
-
"task_complete": Hooks.AGENT_END, # 一次 Agent 任务结束后(只读观测/上报,对应范式 Stop)
|
|
52
|
+
"pre_reasoning": Hooks.AGENT_START,
|
|
53
|
+
"task_complete": Hooks.AGENT_END,
|
|
64
54
|
}
|
|
65
55
|
|
|
66
56
|
|
|
67
|
-
|
|
57
|
+
HookEvent = Literal["before_tool_call", "after_tool_call", "on_error", "on_loop",
|
|
58
|
+
"session_start", "agent_start", "agent_end", "session_end",
|
|
59
|
+
"before_message_write",
|
|
60
|
+
"pre_skill", "post_skill", "pre_reasoning", "task_complete"]
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def normalize_hook(event: str) -> HookEvent:
|
|
68
64
|
"""把旧钩子名规整为标准名;已是标准名则原样返回。"""
|
|
69
|
-
return ALIAS.get(event, event)
|
|
65
|
+
return cast(HookEvent, ALIAS.get(event, event))
|
|
70
66
|
|
|
71
67
|
|
|
72
68
|
def is_valid_hook(event: str) -> bool:
|
|
@@ -0,0 +1,216 @@
|
|
|
1
|
+
"""`foxclaw skills install|list|remove` 的实现。
|
|
2
|
+
|
|
3
|
+
为什么不放在 cli.py:那里只该有参数解析与分发(体积门 15KB,且它已承载 serve/config)。
|
|
4
|
+
装技能的真正复杂度在「服务在跑没跑」这一个分叉上:
|
|
5
|
+
|
|
6
|
+
* **在跑** → 必须打 /api/ahs/md-skills。直接改 markdown_skills.json 会被运行中的
|
|
7
|
+
注册表下一次 `save()` 用内存快照整份覆盖,用户的安装无声消失(极难查)。
|
|
8
|
+
* **没跑** → 直接写同一个 JSON,格式与 MarkdownStore 完全一致,下次启动即加载。
|
|
9
|
+
"""
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
import json
|
|
13
|
+
import os
|
|
14
|
+
import sys
|
|
15
|
+
import urllib.error
|
|
16
|
+
import urllib.parse
|
|
17
|
+
import urllib.request
|
|
18
|
+
from pathlib import Path
|
|
19
|
+
from typing import Any, cast
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def _data_dir() -> Path:
|
|
23
|
+
"""与 `foxclaw serve` 同源:FOXCLAW_DATA_DIR,否则 cwd 下的 foxclaw_data。"""
|
|
24
|
+
raw = os.environ.get("FOXCLAW_DATA_DIR", "").strip()
|
|
25
|
+
return Path(raw or "./foxclaw_data").resolve()
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def _api_base() -> str:
|
|
29
|
+
"""本地服务地址:FOXCLAW_API 可覆盖,否则回环 + FOXCLAW_PORT。"""
|
|
30
|
+
raw = os.environ.get("FOXCLAW_API", "").strip()
|
|
31
|
+
if raw:
|
|
32
|
+
return raw.rstrip("/")
|
|
33
|
+
port = os.environ.get("FOXCLAW_PORT", "").strip() or "8000"
|
|
34
|
+
return f"http://127.0.0.1:{port}"
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def _api_alive(base: str) -> bool:
|
|
38
|
+
try:
|
|
39
|
+
with urllib.request.urlopen(f"{base}/api/system/info", timeout=0.8) as r:
|
|
40
|
+
return 200 <= r.status < 300
|
|
41
|
+
except Exception:
|
|
42
|
+
return False
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def _api_json(base: str, method: str, path: str, payload: dict | None = None) -> dict:
|
|
46
|
+
body = json.dumps(payload, ensure_ascii=False).encode("utf-8") if payload is not None else None
|
|
47
|
+
headers = {"Content-Type": "application/json"}
|
|
48
|
+
key = os.environ.get("FOXCLAW_API_KEY", "").strip()
|
|
49
|
+
if key:
|
|
50
|
+
headers["X-API-Key"] = key
|
|
51
|
+
req = urllib.request.Request(f"{base}{path}", data=body, method=method, headers=headers)
|
|
52
|
+
try:
|
|
53
|
+
with urllib.request.urlopen(req, timeout=15) as r:
|
|
54
|
+
return json.loads(r.read().decode("utf-8", "replace") or "{}")
|
|
55
|
+
except urllib.error.HTTPError as e:
|
|
56
|
+
detail = e.read(400).decode("utf-8", "replace").strip()
|
|
57
|
+
raise RuntimeError(f"{method} {path} → {e.code}: {detail}") from None
|
|
58
|
+
except urllib.error.URLError as e:
|
|
59
|
+
raise RuntimeError(f"连不上服务: {e.reason}") from None
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def _confirm() -> bool:
|
|
63
|
+
"""非 TTY(管道/CI)一律不认,必须显式 --yes。"""
|
|
64
|
+
if not sys.stdin.isatty():
|
|
65
|
+
return False
|
|
66
|
+
try:
|
|
67
|
+
ans = input("安装后它会注入你 AI 的提示词、影响其行为。继续?[y/N] ")
|
|
68
|
+
except (EOFError, KeyboardInterrupt):
|
|
69
|
+
return False
|
|
70
|
+
return ans.strip().lower() in ("y", "yes")
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def _persist(skill, text: str) -> tuple[bool, str, bool]:
|
|
74
|
+
"""落地一个指令技能。返回 (是否新建, 位置说明, 是否已热加载)。"""
|
|
75
|
+
base = _api_base()
|
|
76
|
+
if _api_alive(base):
|
|
77
|
+
got = _api_json(base, "POST", "/api/ahs/md-skills",
|
|
78
|
+
{"content": text, "id": skill.id, "acknowledged": True})
|
|
79
|
+
return bool(got.get("created")), base, True
|
|
80
|
+
from .markdown_skill import MarkdownSkillRegistry
|
|
81
|
+
from .markdown_store import MarkdownStore
|
|
82
|
+
|
|
83
|
+
reg = MarkdownSkillRegistry()
|
|
84
|
+
store = MarkdownStore(_data_dir())
|
|
85
|
+
store.load(reg)
|
|
86
|
+
created = reg.get(skill.id) is None
|
|
87
|
+
reg.register(skill)
|
|
88
|
+
store.save(reg)
|
|
89
|
+
return created, str(_data_dir() / "markdown_skills.json"), False
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
def install(args) -> int:
|
|
93
|
+
from .fetch import MalwareBlocked, PackageError, fetch
|
|
94
|
+
from .markdown_skill import parse_skill_md
|
|
95
|
+
|
|
96
|
+
try:
|
|
97
|
+
pkg = fetch(args.ref)
|
|
98
|
+
except MalwareBlocked as e:
|
|
99
|
+
print(f"拒绝安装:{e}", file=sys.stderr)
|
|
100
|
+
return 3
|
|
101
|
+
except PackageError as e:
|
|
102
|
+
print(f"获取失败:{e}", file=sys.stderr)
|
|
103
|
+
return 2
|
|
104
|
+
want_id = (args.id or "").strip() or str(pkg.meta.get("slug") or "")
|
|
105
|
+
try:
|
|
106
|
+
skill = parse_skill_md(pkg.skill_md, id=want_id or None)
|
|
107
|
+
except ValueError as e:
|
|
108
|
+
print(f"SKILL.md 解析失败:{e}", file=sys.stderr)
|
|
109
|
+
return 2
|
|
110
|
+
if pkg.version and not skill.version:
|
|
111
|
+
skill.version = pkg.version
|
|
112
|
+
if not args.json:
|
|
113
|
+
ver = f" v{skill.version}" if skill.version else ""
|
|
114
|
+
print(f"来源 {pkg.source} · {pkg.origin}")
|
|
115
|
+
print(f"技能 {skill.name}(id={skill.id}){ver}")
|
|
116
|
+
print(f"描述 {skill.description[:100]}")
|
|
117
|
+
if pkg.has_code:
|
|
118
|
+
print("注意 包内带可执行脚本。foxclaw 只注入 SKILL.md、不运行这些脚本;"
|
|
119
|
+
"若正文要求 agent 去跑它们,该技能不会按预期工作。")
|
|
120
|
+
if pkg.suspicious:
|
|
121
|
+
print("警告 ClawHub 标记该技能可疑,请先读正文再决定。")
|
|
122
|
+
if not args.yes and not _confirm():
|
|
123
|
+
print("已取消:未确认。确认后加 --yes(或在本机终端里交互确认)。")
|
|
124
|
+
return 1
|
|
125
|
+
try:
|
|
126
|
+
created, where, hot = _persist(skill, pkg.skill_md)
|
|
127
|
+
except RuntimeError as e:
|
|
128
|
+
print(f"写入失败:{e}", file=sys.stderr)
|
|
129
|
+
return 4
|
|
130
|
+
if args.json:
|
|
131
|
+
print(json.dumps({"ok": True, "created": created, "skill": skill.to_meta(),
|
|
132
|
+
"source": pkg.source, "origin": pkg.origin,
|
|
133
|
+
"has_code": pkg.has_code, "where": where,
|
|
134
|
+
"hot_loaded": hot}, ensure_ascii=False))
|
|
135
|
+
return 0
|
|
136
|
+
print(f"{'已安装' if created else '已更新'}「{skill.name}」→ {where}")
|
|
137
|
+
if not hot:
|
|
138
|
+
print("提示:服务若正在运行,重启一次才会加载;不想重启就先起服务再装。")
|
|
139
|
+
return 0
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
def list_skills(args) -> int:
|
|
143
|
+
base = _api_base()
|
|
144
|
+
hot = _api_alive(base)
|
|
145
|
+
try:
|
|
146
|
+
if hot:
|
|
147
|
+
rows = _api_json(base, "GET", "/api/ahs/md-skills").get("skills") or []
|
|
148
|
+
else:
|
|
149
|
+
from .markdown_skill import MarkdownSkillRegistry
|
|
150
|
+
from .markdown_store import MarkdownStore
|
|
151
|
+
|
|
152
|
+
reg = MarkdownSkillRegistry()
|
|
153
|
+
MarkdownStore(_data_dir()).load(reg)
|
|
154
|
+
rows = reg.catalog()
|
|
155
|
+
except RuntimeError as e:
|
|
156
|
+
print(f"读取失败:{e}", file=sys.stderr)
|
|
157
|
+
return 4
|
|
158
|
+
if args.json:
|
|
159
|
+
print(json.dumps({"source": "api" if hot else "file", "skills": rows},
|
|
160
|
+
ensure_ascii=False))
|
|
161
|
+
return 0
|
|
162
|
+
if not rows:
|
|
163
|
+
print("还没有安装指令技能。`foxclaw skills install @owner/slug` 装一个。")
|
|
164
|
+
return 0
|
|
165
|
+
for s in rows:
|
|
166
|
+
ver = f" v{s.get('version')}" if s.get("version") else ""
|
|
167
|
+
off = "" if s.get("enabled", True) else " [已停用]"
|
|
168
|
+
blocked = " [被封杀]" if s.get("blocked") else ""
|
|
169
|
+
print(f"{str(s.get('id', '')).ljust(26)} {str(s.get('source', '?')):8}{ver}{off}{blocked}")
|
|
170
|
+
print(f" {str(s.get('description', ''))[:90]}")
|
|
171
|
+
return 0
|
|
172
|
+
|
|
173
|
+
|
|
174
|
+
def remove(args) -> int:
|
|
175
|
+
sid = (args.id or "").strip()
|
|
176
|
+
if not sid:
|
|
177
|
+
print("要卸载哪个?`foxclaw skills list` 看 id。", file=sys.stderr)
|
|
178
|
+
return 2
|
|
179
|
+
base = _api_base()
|
|
180
|
+
if _api_alive(base):
|
|
181
|
+
try:
|
|
182
|
+
_api_json(base, "DELETE", f"/api/ahs/md-skills/{urllib.parse.quote(sid, safe='')}")
|
|
183
|
+
except RuntimeError as e:
|
|
184
|
+
print(f"卸载失败:{e}", file=sys.stderr)
|
|
185
|
+
return 4
|
|
186
|
+
print(f"已卸载 {sid}(服务已热更新)")
|
|
187
|
+
return 0
|
|
188
|
+
from .markdown_skill import MarkdownSkillRegistry
|
|
189
|
+
from .markdown_store import MarkdownStore
|
|
190
|
+
|
|
191
|
+
reg = MarkdownSkillRegistry()
|
|
192
|
+
store = MarkdownStore(_data_dir())
|
|
193
|
+
store.load(reg)
|
|
194
|
+
if not reg.unregister(sid):
|
|
195
|
+
print(f"没找到 id={sid} 的指令技能(`foxclaw skills list` 看现有)", file=sys.stderr)
|
|
196
|
+
return 1
|
|
197
|
+
store.save(reg)
|
|
198
|
+
print(f"已卸载 {sid} → {_data_dir() / 'markdown_skills.json'}")
|
|
199
|
+
return 0
|
|
200
|
+
|
|
201
|
+
|
|
202
|
+
def run(args) -> int:
|
|
203
|
+
for stream in (sys.stdout, sys.stderr):
|
|
204
|
+
try:
|
|
205
|
+
cast(Any, stream).reconfigure(errors="replace")
|
|
206
|
+
except Exception:
|
|
207
|
+
pass
|
|
208
|
+
action = getattr(args, "skills_action", None)
|
|
209
|
+
if action == "install":
|
|
210
|
+
return install(args)
|
|
211
|
+
if action == "list":
|
|
212
|
+
return list_skills(args)
|
|
213
|
+
if action == "remove":
|
|
214
|
+
return remove(args)
|
|
215
|
+
print("用法:`foxclaw skills install <ref>` / `list` / `remove <id>`")
|
|
216
|
+
return 2
|