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.
Files changed (344) hide show
  1. package/index.js +1 -8
  2. package/package.json +1 -1
  3. package/python/.env.example +8 -0
  4. package/python/.github/workflows/ci.yml +47 -2
  5. package/python/CHANGELOG.md +57 -0
  6. package/python/CHANGELOG_CN.md +57 -0
  7. package/python/README.md +11 -2
  8. package/python/README_CN.md +32 -20
  9. package/python/bench/bm25_audit.py +1 -14
  10. package/python/bench/bm25_regression.py +0 -1
  11. package/python/bench/bm25_stress.py +0 -4
  12. package/python/bench/cd_audit.py +0 -23
  13. package/python/bench/chaos.py +6 -8
  14. package/python/bench/e2e_real_models.py +0 -1
  15. package/python/bench/llm_retry_test.py +2 -9
  16. package/python/bench/load_test.py +5 -8
  17. package/python/bench/vector_ivf_upgrade.py +1 -7
  18. package/python/docs/RELEASE.md +4 -3
  19. package/python/docs/RUN_CN.md +2 -2
  20. package/python/evals/governance/cases.py +0 -37
  21. package/python/evals/governance/run_evals.py +5 -37
  22. package/python/examples/governance_middleware_demo.py +1 -6
  23. package/python/examples/langgraph_governance_demo.py +7 -11
  24. package/python/foxclaw/__init__.py +34 -152
  25. package/python/foxclaw/agents/base.py +65 -136
  26. package/python/foxclaw/agents/impl/_helpers.py +2 -10
  27. package/python/foxclaw/agents/impl/finance.py +4 -21
  28. package/python/foxclaw/agents/impl/office.py +7 -24
  29. package/python/foxclaw/agents/impl/supervisor.py +12 -79
  30. package/python/foxclaw/agents/mixins/_context.py +0 -23
  31. package/python/foxclaw/agents/mixins/augment.py +10 -16
  32. package/python/foxclaw/agents/mixins/llm.py +7 -6
  33. package/python/foxclaw/agents/mixins/memory.py +7 -1
  34. package/python/foxclaw/agents/mixins/preference.py +58 -8
  35. package/python/foxclaw/agents/mixins/prompt.py +0 -6
  36. package/python/foxclaw/agents/mixins/skillbox.py +6 -6
  37. package/python/foxclaw/agents/mixins/tool_exec.py +16 -42
  38. package/python/foxclaw/api/__init__.py +18 -77
  39. package/python/foxclaw/api/_state.py +43 -0
  40. package/python/foxclaw/api/{app.py → bootstrap.py} +5 -32
  41. package/python/foxclaw/api/middleware.py +5 -28
  42. package/python/foxclaw/api/routes/ahs.py +60 -73
  43. package/python/foxclaw/api/routes/attachments.py +5 -6
  44. package/python/foxclaw/api/routes/browser.py +7 -11
  45. package/python/foxclaw/api/routes/domain.py +18 -26
  46. package/python/foxclaw/api/routes/knowledge.py +3 -21
  47. package/python/foxclaw/api/routes/llm_admin.py +21 -33
  48. package/python/foxclaw/api/routes/memory.py +28 -12
  49. package/python/foxclaw/api/routes/orchestration.py +3 -5
  50. package/python/foxclaw/api/routes/recovery.py +4 -3
  51. package/python/foxclaw/api/routes/review.py +2 -11
  52. package/python/foxclaw/api/routes/search.py +3 -7
  53. package/python/foxclaw/api/routes/system.py +33 -49
  54. package/python/foxclaw/api/routes/task.py +31 -49
  55. package/python/foxclaw/api/routes/ws.py +2 -6
  56. package/python/foxclaw/api/spine.py +1 -4
  57. package/python/foxclaw/browser/action/action_executor.py +1 -5
  58. package/python/foxclaw/browser/action/action_types.py +0 -1
  59. package/python/foxclaw/browser/action/url_policy.py +1 -5
  60. package/python/foxclaw/browser/adapter/web_adapter.py +1 -1
  61. package/python/foxclaw/browser/driver/playwright_driver.py +20 -16
  62. package/python/foxclaw/browser/perception/dom_extractor.py +0 -1
  63. package/python/foxclaw/browser/perception/element_indexer.py +1 -2
  64. package/python/foxclaw/browser/service.py +7 -11
  65. package/python/foxclaw/capability/embedding_service.py +4 -6
  66. package/python/foxclaw/channels/__init__.py +9 -11
  67. package/python/foxclaw/channels/discord.py +4 -13
  68. package/python/foxclaw/channels/dispatcher.py +2 -27
  69. package/python/foxclaw/channels/feishu.py +10 -20
  70. package/python/foxclaw/channels/openclaw_bridge.py +4 -9
  71. package/python/foxclaw/channels/signing.py +9 -9
  72. package/python/foxclaw/channels/telegram.py +3 -4
  73. package/python/foxclaw/channels/weixin.py +7 -17
  74. package/python/foxclaw/cli.py +42 -13
  75. package/python/foxclaw/cognitive/reasoner.py +9 -10
  76. package/python/foxclaw/cognitive/state.py +18 -24
  77. package/python/foxclaw/core/audit_chain.py +1 -3
  78. package/python/foxclaw/core/audit_sink.py +3 -3
  79. package/python/foxclaw/core/compaction.py +8 -26
  80. package/python/foxclaw/core/config.py +2 -2
  81. package/python/foxclaw/core/confirm_nonce.py +6 -12
  82. package/python/foxclaw/core/envfile.py +2 -2
  83. package/python/foxclaw/core/failure_policy.py +17 -26
  84. package/python/foxclaw/core/gateway.py +6 -10
  85. package/python/foxclaw/core/governance_audit.py +3 -7
  86. package/python/foxclaw/core/idempotency.py +15 -22
  87. package/python/foxclaw/core/injection_guard.py +10 -29
  88. package/python/foxclaw/core/memory_adapter.py +30 -57
  89. package/python/foxclaw/core/metrics.py +4 -5
  90. package/python/foxclaw/core/model_router.py +36 -148
  91. package/python/foxclaw/core/paths.py +1 -1
  92. package/python/foxclaw/core/recovery.py +21 -34
  93. package/python/foxclaw/core/scheduler.py +23 -26
  94. package/python/foxclaw/core/secrets.py +0 -3
  95. package/python/foxclaw/core/sessions.py +4 -6
  96. package/python/foxclaw/core/settings.py +16 -32
  97. package/python/foxclaw/core/shared_write.py +11 -23
  98. package/python/foxclaw/core/steering_queue.py +12 -11
  99. package/python/foxclaw/core/system_bus.py +1 -1
  100. package/python/foxclaw/core/thought_chain.py +30 -32
  101. package/python/foxclaw/core/token_meter.py +9 -13
  102. package/python/foxclaw/core/tools.py +4 -12
  103. package/python/foxclaw/core/tracing.py +25 -40
  104. package/python/foxclaw/core/truth_pipeline.py +5 -9
  105. package/python/foxclaw/core/update_check.py +149 -0
  106. package/python/foxclaw/core/vec_recall_adapter.py +11 -37
  107. package/python/foxclaw/core/verify.py +6 -14
  108. package/python/foxclaw/core/web_search.py +4 -28
  109. package/python/foxclaw/finance/evidence.py +14 -18
  110. package/python/foxclaw/finance/fx.py +1 -3
  111. package/python/foxclaw/finance/graph.py +9 -18
  112. package/python/foxclaw/finance/models.py +20 -24
  113. package/python/foxclaw/finance/nlparse.py +12 -72
  114. package/python/foxclaw/finance/policy.py +8 -36
  115. package/python/foxclaw/finance/reasoner.py +3 -11
  116. package/python/foxclaw/finance/reasoning.py +13 -34
  117. package/python/foxclaw/finance/reconcile.py +5 -10
  118. package/python/foxclaw/finance/runtime.py +19 -46
  119. package/python/foxclaw/governance/middleware.py +13 -21
  120. package/python/foxclaw/harness/_base.py +2 -8
  121. package/python/foxclaw/harness/_policy.py +19 -38
  122. package/python/foxclaw/harness/drift.py +1 -13
  123. package/python/foxclaw/harness/event_bus.py +2 -5
  124. package/python/foxclaw/harness/harness.py +9 -97
  125. package/python/foxclaw/harness/loop_detection.py +11 -18
  126. package/python/foxclaw/itinerary/amap_client.py +2 -5
  127. package/python/foxclaw/itinerary/models.py +7 -7
  128. package/python/foxclaw/itinerary/reasoner.py +1 -6
  129. package/python/foxclaw/itinerary/reasoning.py +15 -21
  130. package/python/foxclaw/itinerary/runtime.py +8 -12
  131. package/python/foxclaw/kernel/capabilities.py +5 -6
  132. package/python/foxclaw/kernel/policy_gate.py +4 -10
  133. package/python/foxclaw/kernel/process_manager.py +17 -36
  134. package/python/foxclaw/kernel/scheduler.py +3 -9
  135. package/python/foxclaw/kernel/sentinel.py +13 -23
  136. package/python/foxclaw/kernel/sentinel_client.py +12 -15
  137. package/python/foxclaw/kernel/spine.py +9 -9
  138. package/python/foxclaw/knowledge/__init__.py +4 -20
  139. package/python/foxclaw/knowledge/truth_crystallizer.py +5 -8
  140. package/python/foxclaw/learning/knowledge_graph.py +4 -9
  141. package/python/foxclaw/learning/models.py +13 -13
  142. package/python/foxclaw/learning/reasoner.py +1 -4
  143. package/python/foxclaw/learning/runtime.py +19 -31
  144. package/python/foxclaw/learning/spaced_repetition.py +2 -7
  145. package/python/foxclaw/mcp_server.py +2 -3
  146. package/python/foxclaw/orchestration/__init__.py +16 -33
  147. package/python/foxclaw/pipeline/cognitive_pipeline.py +45 -97
  148. package/python/foxclaw/pipeline/stage_meta.py +21 -0
  149. package/python/foxclaw/pipeline/supervisor_stages.py +13 -42
  150. package/python/foxclaw/planning/llm_planner.py +3 -3
  151. package/python/foxclaw/planning/planner.py +6 -11
  152. package/python/foxclaw/search/agent_port.py +7 -11
  153. package/python/foxclaw/search/evidence.py +0 -4
  154. package/python/foxclaw/search/internal_search.py +3 -5
  155. package/python/foxclaw/search/models.py +11 -13
  156. package/python/foxclaw/search/planner.py +3 -3
  157. package/python/foxclaw/search/retriever.py +1 -1
  158. package/python/foxclaw/search/runtime.py +1 -8
  159. package/python/foxclaw/skills/__init__.py +46 -100
  160. package/python/foxclaw/skills/archive.py +111 -0
  161. package/python/foxclaw/skills/fetch.py +226 -0
  162. package/python/foxclaw/skills/hooks_spec.py +21 -25
  163. package/python/foxclaw/skills/install_cmd.py +216 -0
  164. package/python/foxclaw/skills/lifecycle.py +16 -42
  165. package/python/foxclaw/skills/markdown_skill.py +10 -9
  166. package/python/foxclaw/skills/mcp_client.py +5 -11
  167. package/python/foxclaw/skills/mcp_store.py +0 -3
  168. package/python/foxclaw/skills/permission.py +13 -48
  169. package/python/foxclaw/skills/pipeline.py +21 -53
  170. package/python/foxclaw/skills/policy.py +4 -10
  171. package/python/foxclaw/skills/review.py +3 -3
  172. package/python/frontend/deck/auth.js +0 -7
  173. package/python/frontend/deck/chat_model_picker.js +0 -54
  174. package/python/frontend/deck/domain.js +0 -20
  175. package/python/frontend/deck/main.js +0 -144
  176. package/python/frontend/deck/markdown.js +0 -4
  177. package/python/frontend/deck/settings.js +1 -18
  178. package/python/frontend/deck/state.js +0 -1
  179. package/python/frontend/deck/util.js +0 -2
  180. package/python/frontend/deck/visuals.js +0 -22
  181. package/python/frontend/planet/planet.js +0 -201
  182. package/python/frontend/v3/assets/index-CQSBdxk4.js +34 -0
  183. package/python/frontend/v3/assets/index-D5nIsjms.css +1 -0
  184. package/python/frontend/v3/index.html +3 -3
  185. package/python/frontend/widget_runtime.js +0 -43
  186. package/python/frontend/widgets.js +0 -57
  187. package/python/frontend_src/index.html +1 -1
  188. package/python/frontend_src/package-lock.json +2 -2
  189. package/python/frontend_src/package.json +1 -1
  190. package/python/frontend_src/src/App.vue +3 -7
  191. package/python/frontend_src/src/api.js +0 -7
  192. package/python/frontend_src/src/components/ChatDock.vue +27 -39
  193. package/python/frontend_src/src/components/DocViewer.vue +0 -9
  194. package/python/frontend_src/src/components/Inspector.vue +0 -6
  195. package/python/frontend_src/src/components/LeftRail.vue +60 -39
  196. package/python/frontend_src/src/components/MenuPopover.vue +9 -11
  197. package/python/frontend_src/src/components/MessageStream.vue +18 -30
  198. package/python/frontend_src/src/components/ModelPicker.vue +0 -5
  199. package/python/frontend_src/src/components/QuickIngest.vue +0 -6
  200. package/python/frontend_src/src/components/Stage.vue +71 -25
  201. package/python/frontend_src/src/components/ToolMenu.vue +0 -3
  202. package/python/frontend_src/src/components/TopBar.vue +38 -10
  203. package/python/frontend_src/src/components/panels/ActivityPanel.vue +0 -2
  204. package/python/frontend_src/src/components/panels/AgentsPanel.vue +2 -5
  205. package/python/frontend_src/src/components/panels/KnowledgePanel.vue +0 -16
  206. package/python/frontend_src/src/components/panels/MemoryPanel.vue +0 -1
  207. package/python/frontend_src/src/components/panels/PanelBox.vue +6 -7
  208. package/python/frontend_src/src/components/panels/ReviewPanel.vue +0 -16
  209. package/python/frontend_src/src/components/panels/SettingsPanel.vue +142 -38
  210. package/python/frontend_src/src/components/panels/TasksPanel.vue +0 -1
  211. package/python/frontend_src/src/components/panels/ToolsPanel.vue +125 -19
  212. package/python/frontend_src/src/lib/fmt.js +0 -4
  213. package/python/frontend_src/src/lib/i18n.js +53 -0
  214. package/python/frontend_src/src/lib/locales/en.js +173 -0
  215. package/python/frontend_src/src/lib/locales/zh.js +174 -0
  216. package/python/frontend_src/src/lib/md.js +0 -14
  217. package/python/frontend_src/src/main.js +2 -0
  218. package/python/frontend_src/src/stores/app.js +17 -32
  219. package/python/frontend_src/src/stores/chat.js +0 -34
  220. package/python/frontend_src/src/stores/model.js +0 -8
  221. package/python/frontend_src/src/stores/panels.js +0 -41
  222. package/python/frontend_src/src/stores/profile.js +31 -0
  223. package/python/frontend_src/src/stores/system.js +25 -0
  224. package/python/frontend_src/vite.config.js +0 -7
  225. package/python/pyproject.toml +6 -3
  226. package/python/requirements.txt +311 -0
  227. package/python/tests/conftest.py +0 -22
  228. package/python/tests/run_all.py +3 -66
  229. package/python/tests/test_agent_lifecycle.py +4 -38
  230. package/python/tests/test_ahs_p2.py +4 -25
  231. package/python/tests/test_anthropic_tools.py +1 -15
  232. package/python/tests/test_api.py +19 -9
  233. package/python/tests/test_api_auth.py +1 -5
  234. package/python/tests/test_attachments.py +9 -25
  235. package/python/tests/test_audit_chain.py +2 -7
  236. package/python/tests/test_audit_sink.py +0 -6
  237. package/python/tests/test_browser.py +2 -6
  238. package/python/tests/test_browser_request_guard.py +0 -4
  239. package/python/tests/test_capabilities.py +0 -6
  240. package/python/tests/test_channel_signing.py +2 -9
  241. package/python/tests/test_channels.py +1 -11
  242. package/python/tests/test_config_layering.py +0 -4
  243. package/python/tests/test_confirm_nonce.py +1 -12
  244. package/python/tests/test_context_compaction_wiring.py +1 -7
  245. package/python/tests/test_dispatch.py +0 -6
  246. package/python/tests/test_domain_evals.py +11 -80
  247. package/python/tests/test_domain_import.py +0 -6
  248. package/python/tests/test_domain_reasoners.py +0 -14
  249. package/python/tests/test_e2e.py +0 -17
  250. package/python/tests/test_emb_cache.py +7 -17
  251. package/python/tests/test_embedding_singleton.py +1 -3
  252. package/python/tests/test_evals_smoke.py +0 -24
  253. package/python/tests/test_event_spine.py +10 -18
  254. package/python/tests/test_evidence_semantic.py +1 -14
  255. package/python/tests/test_evidence_sse.py +3 -6
  256. package/python/tests/test_file_ingest.py +0 -11
  257. package/python/tests/test_finance.py +0 -9
  258. package/python/tests/test_finance_ledger.py +5 -12
  259. package/python/tests/test_finance_persist.py +8 -12
  260. package/python/tests/test_gateway.py +3 -22
  261. package/python/tests/test_governance.py +5 -52
  262. package/python/tests/test_governance_audit.py +4 -39
  263. package/python/tests/test_governance_middleware.py +1 -7
  264. package/python/tests/test_hkc.py +6 -24
  265. package/python/tests/test_hkc_index_build_manager.py +0 -33
  266. package/python/tests/test_hkc_manifest_reuse.py +4 -27
  267. package/python/tests/test_hkc_search_event_sync.py +3 -54
  268. package/python/tests/test_import_path_guard.py +0 -6
  269. package/python/tests/test_injection_guard.py +18 -36
  270. package/python/tests/test_integration.py +5 -29
  271. package/python/tests/test_itinerary.py +0 -3
  272. package/python/tests/test_kee_semantic.py +0 -5
  273. package/python/tests/test_kernel.py +1 -11
  274. package/python/tests/test_learning.py +3 -12
  275. package/python/tests/test_llm_admin.py +4 -19
  276. package/python/tests/test_llm_planner.py +1 -23
  277. package/python/tests/test_markdown_skills.py +3 -14
  278. package/python/tests/test_markdown_skills_openclaw.py +4 -9
  279. package/python/tests/test_mcp_connectors.py +3 -15
  280. package/python/tests/test_mcp_guardian_discovery.py +2 -6
  281. package/python/tests/test_mcp_no_memory.py +3 -3
  282. package/python/tests/test_mcp_policy_gate.py +0 -1
  283. package/python/tests/test_mcp_server.py +6 -9
  284. package/python/tests/test_memory_recall.py +1 -4
  285. package/python/tests/test_metrics.py +0 -8
  286. package/python/tests/test_multi_chain.py +1 -13
  287. package/python/tests/test_multimodal.py +5 -18
  288. package/python/tests/test_negotiation.py +3 -13
  289. package/python/tests/test_orchestration.py +1 -11
  290. package/python/tests/test_orchestration_decoupled.py +0 -6
  291. package/python/tests/test_p3_p5_hardening.py +3 -17
  292. package/python/tests/test_parallel_dispatch.py +4 -12
  293. package/python/tests/test_pipeline.py +17 -58
  294. package/python/tests/test_plan_visible.py +4 -4
  295. package/python/tests/test_planner.py +0 -7
  296. package/python/tests/test_policy_gate.py +2 -3
  297. package/python/tests/test_profile.py +67 -0
  298. package/python/tests/test_readme_claims.py +163 -0
  299. package/python/tests/test_reasoner.py +2 -17
  300. package/python/tests/test_release_check.py +2 -13
  301. package/python/tests/test_resume_checkpoint.py +7 -15
  302. package/python/tests/test_routing.py +2 -12
  303. package/python/tests/test_scheduler.py +7 -10
  304. package/python/tests/test_search.py +1 -11
  305. package/python/tests/test_secrets.py +4 -10
  306. package/python/tests/test_semantic.py +1 -10
  307. package/python/tests/test_sessions.py +3 -4
  308. package/python/tests/test_settings.py +2 -18
  309. package/python/tests/test_shadow_learning.py +3 -43
  310. package/python/tests/test_shared_write.py +0 -7
  311. package/python/tests/test_skill_taint_gate.py +1 -10
  312. package/python/tests/test_skills.py +2 -18
  313. package/python/tests/test_skills_fetch.py +282 -0
  314. package/python/tests/test_stats_and_events.py +2 -5
  315. package/python/tests/test_streaming.py +2 -7
  316. package/python/tests/test_supervisor_pipeline.py +4 -5
  317. package/python/tests/test_supervisor_triage.py +4 -10
  318. package/python/tests/test_taint.py +2 -7
  319. package/python/tests/test_task_stream.py +1 -12
  320. package/python/tests/test_thinking_sse.py +0 -1
  321. package/python/tests/test_tool_center.py +1 -17
  322. package/python/tests/test_tool_parallel.py +2 -2
  323. package/python/tests/test_tool_params.py +1 -5
  324. package/python/tests/test_tool_use.py +0 -13
  325. package/python/tests/test_toplevel_concurrency.py +3 -4
  326. package/python/tests/test_tracing.py +7 -41
  327. package/python/tests/test_truth_crystallizer.py +4 -6
  328. package/python/tests/test_truth_flow.py +2 -7
  329. package/python/tests/test_update_check.py +95 -0
  330. package/python/tests/test_verify_evidence.py +8 -21
  331. package/python/tests/test_version_sync.py +35 -6
  332. package/python/tests/test_web_trigger.py +0 -17
  333. package/python/tools/check_faiss_type.py +0 -1
  334. package/python/tools/check_log.ps1 +0 -8
  335. package/python/tools/mypy_baseline.txt +12 -4
  336. package/python/tools/mypy_ratchet.py +52 -6
  337. package/python/tools/pack.py +0 -5
  338. package/python/tools/release_check.py +7 -7
  339. package/python/tools/restart_foxclaw.ps1 +5 -8
  340. package/python/tools/shared_write_audit.py +1 -15
  341. package/python/tools/size_check.py +1 -1
  342. package/python/tools/strip_comments.py +228 -0
  343. package/python/frontend/v3/assets/index-BD7VPbMV.js +0 -33
  344. package/python/frontend/v3/assets/index-CAvnYWGH.css +0 -1
@@ -11,18 +11,17 @@ Foxclaw Skill 接入层 — 助手的"万能工具箱",同时是对外生态兼
11
11
  import contextvars
12
12
  import threading
13
13
  from dataclasses import dataclass, field, asdict
14
- from typing import Optional, Callable, Literal
14
+ from typing import TYPE_CHECKING, Any, Callable, Literal, Optional
15
+
16
+ if TYPE_CHECKING:
17
+ from .mcp_client import McpManager
18
+ from .policy import SkillPolicy
15
19
 
16
20
 
17
21
  SkillKind = Literal["local", "mcp", "web_api"]
18
22
  SideEffect = Literal["read", "write", "send", "pay"]
19
23
 
20
24
 
21
- # ── 循环检测的会话分槽 key(阶段四·步骤13:loop_detector per-session 化)────────────────
22
- # SkillAdapter 是全局单例(Foxclaw 装配一个,所有 Agent/会话共享),其循环检测历史若共享,
23
- # A 会话的工具调用会污染 B 会话的"重复/横跳/无进展"判定。故用 contextvar 承载"当前会话
24
- # 标识",随 asyncio Task / asyncio.to_thread 自动传播;循环检测按它分槽,消除跨会话污染。
25
- # 调用方在会话/执行入口 set_loop_session(session_id),退出时 reset_loop_session(token)。
26
25
  _loop_session: contextvars.ContextVar = contextvars.ContextVar(
27
26
  "foxclaw_skill_loop_session", default=None)
28
27
 
@@ -43,33 +42,26 @@ class SkillSpec:
43
42
  name: str
44
43
  description: str
45
44
  kind: SkillKind = "local"
46
- triggers: list = field(default_factory=list) # 触发关键词
47
- # 【fail-closed】未声明 = 按有副作用处理。漏写一行不该等于「放行」:
48
- # 工具侧 core/tools.py 的未声明能力兜底为 write+external → 强制确认,这里对齐同一口径。
49
- side_effect: SideEffect = "write" # read/write/send/pay → 守门人据此裁决
50
- destructive: bool = False # 不可逆操作(read_only 一票否决);write 已触发确认
51
- external: bool = False # 是否调用外部服务(联网/API)
52
- read_only: Optional[bool] = None # 是否只读(None=按 side_effect 推断)
53
- params: dict = field(default_factory=dict) # 参数 schema
54
- # 调用配置(按 kind 不同)
55
- handler: Optional[Callable] = None # local: 直接函数
56
- compensate: Optional[Callable] = None # 回滚补偿:撤销本 skill 的副作用(逆操作)。
57
- # ⚠【预留·未接线】(2026-07-17 V3·B1):SkillAdapter 从不调用本字段(失败分支只 record_failure
58
- # +on_error,无回滚);且**无任何内置 skill 提供 compensate**。是"未来副作用型能力回滚接口"
59
- # 的预留,非在工作的机制。要真回滚须:① 副作用 skill 提供逆操作 ② invoke 失败分支调它。
60
- # 见 docs/internal/AGENT_HARNESS_REMEDIATION_PLAN.md「未接线清单」。
61
- mcp_server: str = "" # mcp: server 名/URL
62
- mcp_tool: str = "" # mcp: tool 名
63
- endpoint: str = "" # web_api: URL
64
- http_method: str = "GET" # web_api: GET/POST/PUT/DELETE
65
- http_headers: dict = field(default_factory=dict) # web_api: 请求头(如 auth)
66
- secret_ref: str = "" # web_api: 凭证引用名(存守门人 vault);空=凭证仍在 http_headers(现状)
67
- auth_scheme: str = "bearer" # web_api: 凭证注入方式 bearer / header:<name> / query:<name>
68
- param_location: str = "auto" # web_api: query/json/auto
69
- timeout: float = 10.0 # web_api: 超时秒
70
- allow_local: bool = False # web_api: 是否信任本地/内网地址(默认拒,防SSRF)
45
+ triggers: list = field(default_factory=list)
46
+ side_effect: SideEffect = "write"
47
+ destructive: bool = False
48
+ external: bool = False
49
+ read_only: Optional[bool] = None
50
+ params: dict = field(default_factory=dict)
51
+ handler: Optional[Callable] = None
52
+ compensate: Optional[Callable] = None
53
+ mcp_server: str = ""
54
+ mcp_tool: str = ""
55
+ endpoint: str = ""
56
+ http_method: str = "GET"
57
+ http_headers: dict = field(default_factory=dict)
58
+ secret_ref: str = ""
59
+ auth_scheme: str = "bearer"
60
+ param_location: str = "auto"
61
+ timeout: float = 10.0
62
+ allow_local: bool = False
71
63
  enabled: bool = True
72
- source: str = "builtin" # builtin(内置) / user(UI 安装,可删)
64
+ source: str = "builtin"
73
65
 
74
66
  def trust_level(self) -> str:
75
67
  """信任分级(治理信号)。外部 kind 优先(不管谁注册):mcp 调外部 server=低、
@@ -94,10 +86,9 @@ class SkillResult:
94
86
  skill_id: str
95
87
  success: bool
96
88
  output: dict = field(default_factory=dict)
97
- status: str = "done" # done / needs_confirmation / blocked
89
+ status: str = "done"
98
90
  message: str = ""
99
- loop_warning: dict = None # 循环检测告警(卡死时填充)
100
- # 关键操作二次确认:status=needs_confirmation 时携带一次性确认码(绑定本次动作+参数)
91
+ loop_warning: Optional[dict] = None
101
92
  confirm_nonce: str = ""
102
93
  confirm_expires_at: float = 0.0
103
94
 
@@ -110,9 +101,6 @@ class SkillRegistry:
110
101
 
111
102
  def __init__(self):
112
103
  self._skills: dict = {}
113
- # 【深审修复·线程安全】Step4 引入运行时注册(sync_abilities_to_skills)后,register 会与
114
- # 每次路由的 all()/match() 并发——无锁则 list 推导迭代 _skills 时若并发 register/unregister
115
- # 会 "dict changed size during iteration"。RLock 保护;all() 返回快照,迭代在锁外(安全)。
116
104
  self._lock = threading.RLock()
117
105
 
118
106
  def register(self, spec: SkillSpec):
@@ -140,7 +128,7 @@ class SkillRegistry:
140
128
  def match(self, task: str) -> Optional[SkillSpec]:
141
129
  """按触发词匹配最合适的 skill(命中最多者胜)。"""
142
130
  best, best_score = None, 0
143
- for s in self.all(): # all() 已在锁内取快照,这里迭代快照(安全)
131
+ for s in self.all():
144
132
  score = sum(1 for t in s.triggers if t in task)
145
133
  if score > best_score:
146
134
  best, best_score = s, score
@@ -173,31 +161,23 @@ def _mark_untrusted(out):
173
161
  class SkillAdapter:
174
162
  """统一调用器 + 守门人。"""
175
163
 
176
- def __init__(self, registry: SkillRegistry, mcp_clients: dict = None,
164
+ def __init__(self, registry: SkillRegistry, mcp_clients: dict | None = None,
177
165
  rules=None, breaker=None, hooks=None, policy_gate=None, sentinel=None,
178
166
  confirms=None):
179
167
  self.registry = registry
180
- # 关键操作(send/pay/destructive)的会话 nonce 二次确认器(core.confirm_nonce)。
181
- # None → 不启用:confirmed=True 直通(既有语义,进程内调用/测试不受影响)。
182
- # 外部入口(HTTP)应注入,否则「确认」只是可重放的布尔。
183
168
  self.confirms = confirms
184
- self.mcp_clients = mcp_clients or {} # server名 -> 客户端(本地配)
185
- self.policy_gate = policy_gate # 【Step4】统一准入门(可空:standalone 直接 evaluate)
186
- self.sentinel = sentinel # 【P-Muse】守门人客户端(可空):非空且 skill.secret_ref 时,web_api 走代执行
187
- self.skill_policy = None # 全局封杀策略(deny/allow kill-switch),宿主注入
188
- # 权限规则集(deny>ask>allow)。宿主可注入,默认空规则。
169
+ self.mcp_clients: dict | McpManager = mcp_clients or {}
170
+ self.policy_gate = policy_gate
171
+ self.sentinel = sentinel
172
+ self.skill_policy: SkillPolicy | None = None
189
173
  from .permission import PermissionRules
190
174
  self.rules = rules or PermissionRules()
191
- # 断路器 + 钩子(AHS FRAMEWORK 横切机制)
192
175
  from .lifecycle import CircuitBreaker, HookManager
193
176
  self.breaker = breaker if breaker is not None else CircuitBreaker()
194
177
  self.hooks = hooks if hooks is not None else HookManager()
195
- # 工具循环检测(防无进展死循环,与断路器互补:断路器管失败,它管卡死)
196
- # 【阶段四·步骤13】按会话 keyed 的检测器池:全局单例 adapter 被所有 Agent/会话共享,
197
- # 循环检测历史必须按 session 分槽(见 _detector_for),否则跨会话交叉污染。
198
- self._loop_detectors: dict = {} # session_key -> ToolLoopDetector
178
+ self._loop_detectors: dict = {}
199
179
  self._loop_lock = threading.RLock()
200
- self._loop_detectors_max = 256 # 会话槽上限:淘汰最久未用的槽,防无限增长
180
+ self._loop_detectors_max = 256
201
181
 
202
182
  def _detector_for(self, session_key: Optional[str]):
203
183
  """取当前会话的循环检测器(按 session 分槽);无会话用共享默认槽(向后兼容)。"""
@@ -206,7 +186,6 @@ class SkillAdapter:
206
186
  d = self._loop_detectors.get(key)
207
187
  if d is None:
208
188
  if len(self._loop_detectors) >= self._loop_detectors_max:
209
- # 淘汰最久未用(插入序最前)的非默认槽,防 session 无限增长
210
189
  for k in list(self._loop_detectors):
211
190
  if k != "__default__":
212
191
  del self._loop_detectors[k]
@@ -240,7 +219,7 @@ class SkillAdapter:
240
219
  taint_threshold=taint_threshold)
241
220
 
242
221
  def _audit_terminal(self, skill: SkillSpec, verdict, outcome: str,
243
- reason: str, detail: dict = None) -> None:
222
+ reason: str, detail: dict | None = None) -> None:
244
223
  """【P2-0】把 skill 执行终态记入统一治理审计流(governance.decision, layer=skill)。
245
224
 
246
225
  审计器从 policy_gate.auditor 取(standalone 无 gate → 无审计,与既有降级策略一致);
@@ -261,14 +240,12 @@ class SkillAdapter:
261
240
  confirm_nonce: str = "", taint_level: int = 0,
262
241
  taint_threshold: int = 0) -> SkillResult:
263
242
 
264
- # —— 全局封杀策略(deny 压倒一切,kill-switch:所有结构化技能执行的唯一咽喉)——
265
243
  pol = self.skill_policy
266
244
  if pol is not None and pol.is_blocked(skill.id):
267
245
  self._audit_terminal(skill, None, "deny", "该技能已被策略封杀(deny)",
268
246
  {"gate": "skill_policy"})
269
247
  return SkillResult(skill.id, False, status="blocked", message="该技能已被策略封杀(deny)")
270
248
 
271
- # —— pre_skill 钩子(可阻断 / 可改 args)——
272
249
  pre = self.hooks.fire("pre_skill", skill.id, {"args": args, "skill": skill.to_dict()})
273
250
  if pre.blocked:
274
251
  self._audit_terminal(skill, None, "deny", f"pre钩子阻断: {pre.reason}",
@@ -278,10 +255,6 @@ class SkillAdapter:
278
255
  if pre.modified_args is not None:
279
256
  args = pre.modified_args
280
257
 
281
- # —— 关键操作会话 nonce 二次确认(必须在权限裁决**之前**)——
282
- # `confirmed=True` 只说明"调用方说确认了",对 send/pay/destructive 还不够:确认码
283
- # 绑定 (会话, 动作, 参数) 且单次使用 → 堵住「确认了 A 却执行 B」和「同一个 true 反复重放」。
284
- # 无效/缺失 → 降级为**未确认**,走 ask 并签发新码(绝不静默放行)。
285
258
  from ..core.confirm_nonce import is_critical as _is_critical
286
259
  _session = _loop_session.get() or ""
287
260
  _critical = _is_critical(getattr(skill, "side_effect", "write"),
@@ -294,10 +267,6 @@ class SkillAdapter:
294
267
  confirmed = False
295
268
  _nonce_reject = _why
296
269
 
297
- # —— 四阶段权限管线(先判权限,再占断路器探针)。经统一 PolicyGate(Step4);
298
- # 拿到的仍是同一 PermissionDecision(行为一致),门面额外发统一 policy.decision 事件。——
299
- # 【P3-4】保留整个 Verdict:decision_id 要作为本层执行审计的 correlation_id,
300
- # 把「谁批准的(permission 裁决)」与「执行结果如何(skill 终态)」join 起来。
301
270
  verdict = None
302
271
  if self.policy_gate is not None:
303
272
  verdict = self.policy_gate.check_action(
@@ -326,8 +295,6 @@ class SkillAdapter:
326
295
  message=_msg, **_extra)
327
296
  _downgrade = decision.trust_downgrade
328
297
 
329
- # —— 断路器:open 且未过冷却 → 走降级,不调用 ——
330
- # (放在权限判定之后:deny/ask 不会执行,就不该占用 half_open 探针名额)
331
298
  if not self.breaker.allow(skill.id):
332
299
  st = self.breaker.status(skill.id)
333
300
  self._audit_terminal(skill, verdict, "deny",
@@ -336,7 +303,6 @@ class SkillAdapter:
336
303
  return SkillResult(skill.id, False, status="circuit_open",
337
304
  message=f"「{skill.name}」连续失败 {st['failures']} 次,断路器已开启,暂停调用")
338
305
 
339
- # —— 执行 + 记账(成功/失败驱动断路器)——
340
306
  try:
341
307
  res = self._dispatch(skill, args, _downgrade)
342
308
  if res.success:
@@ -345,12 +311,10 @@ class SkillAdapter:
345
311
  else:
346
312
  self.breaker.record_failure(skill.id)
347
313
  self.hooks.fire("on_error", skill.id, {"result": res.to_dict()})
348
- # 【P2-0】执行终态审计(成功/失败都记;deny/ask 已由 permission 层记录,不重复)
349
314
  self._audit_terminal(
350
315
  skill, verdict, "success" if res.success else "failed",
351
316
  res.message, {"kind": skill.kind,
352
317
  "trust_downgrade": _downgrade})
353
- # 循环检测:记录本次调用(按会话 keyed,消除跨会话污染),若检测到死循环附警告
354
318
  loop = self._detector_for(_loop_session.get()).record(skill.id, args, res.output)
355
319
  if loop.stuck:
356
320
  res.loop_warning = loop.to_dict()
@@ -371,7 +335,6 @@ class SkillAdapter:
371
335
  return SkillResult(skill.id, True, output=out if isinstance(out, dict)
372
336
  else {"result": out}, message="本地 skill 执行完成")
373
337
  elif skill.kind == "mcp":
374
- # 【P-Muse·第3步】挂了守门人 → 代执行:守门人持有 MCP 凭证并调用,主进程只发意图。
375
338
  if self.sentinel is not None:
376
339
  res = self._invoke_mcp_via_sentinel(skill, args)
377
340
  else:
@@ -381,8 +344,6 @@ class SkillAdapter:
381
344
  res.message += "(来源不可信,结果不可驱动后续危险操作)"
382
345
  return res
383
346
  elif skill.kind == "web_api":
384
- # 【P-Muse】声明了凭证 ref 且挂了守门人 → 代执行:真凭证只在守门人 vault,
385
- # 主进程不直发请求(拿不到真值),改走 IPC 由守门人注入凭证后执行。
386
347
  if skill.secret_ref and self.sentinel is not None:
387
348
  return self._invoke_web_via_sentinel(skill, args)
388
349
  return self._invoke_web(skill, args)
@@ -400,7 +361,7 @@ class SkillAdapter:
400
361
  f"本地配置后即可调用 {skill.mcp_tool}")
401
362
  try:
402
363
  out = client.call_tool(skill.mcp_tool, args)
403
- out = _mark_untrusted(out) # 外部 MCP 返回=不可信,框住内容防被当指令(提示注入)
364
+ out = _mark_untrusted(out)
404
365
  return SkillResult(skill.id, True, output=out, message="MCP 调用完成(外部不可信数据)")
405
366
  except Exception as e:
406
367
  return SkillResult(skill.id, False, status="blocked",
@@ -431,7 +392,6 @@ class SkillAdapter:
431
392
  return SkillResult(skill.id, False, status="blocked",
432
393
  message="缺少 httpx 依赖,无法发起 Web API 调用")
433
394
 
434
- # —— SSRF 防护:只允许 http/https + 拦截内网/环回/元数据地址 ——
435
395
  allow_local = getattr(skill, "allow_local", False)
436
396
  block = self._ssrf_check(skill.endpoint, allow_local=allow_local)
437
397
  if block:
@@ -439,15 +399,14 @@ class SkillAdapter:
439
399
  message=f"端点被安全策略拒绝: {block}")
440
400
 
441
401
  method = (skill.http_method or "GET").upper()
442
- headers = {"User-Agent": "Foxclaw/1.0"} # 默认 UA(很多 API 必需)
443
- headers.update(skill.http_headers or {}) # 用户头可覆盖
402
+ headers = {"User-Agent": "Foxclaw/1.0"}
403
+ headers.update(skill.http_headers or {})
444
404
  args = args or {}
445
- # 参数位置:auto → GET/DELETE 走 query,POST/PUT 走 json body
446
405
  loc = skill.param_location
447
406
  if loc == "auto":
448
407
  loc = "query" if method in ("GET", "DELETE") else "json"
449
- req_kwargs = {"headers": headers, "timeout": skill.timeout,
450
- "follow_redirects": False} # 禁止重定向(防绕过 SSRF 校验)
408
+ req_kwargs: dict[str, Any] = {"headers": headers, "timeout": skill.timeout,
409
+ "follow_redirects": False}
451
410
  if loc == "query":
452
411
  req_kwargs["params"] = args
453
412
  else:
@@ -463,17 +422,14 @@ class SkillAdapter:
463
422
  return SkillResult(skill.id, False, status="blocked",
464
423
  message=f"Web API 请求失败: {e}")
465
424
 
466
- # 3xx 重定向(已禁跟随)→ 视为需人工确认的可疑响应
467
425
  if 300 <= resp.status_code < 400:
468
426
  return SkillResult(skill.id, False, status="blocked",
469
427
  message=f"Web API 返回重定向 HTTP {resp.status_code}(已阻止跟随)")
470
- # 非 2xx → 失败(交给断路器计数)
471
428
  if resp.status_code >= 400:
472
429
  return SkillResult(skill.id, False, status="blocked",
473
430
  message=f"Web API 返回 HTTP {resp.status_code}",
474
431
  output={"status_code": resp.status_code,
475
432
  "body": resp.text[:500]})
476
- # 解析响应:优先 JSON,失败则文本
477
433
  try:
478
434
  data = resp.json()
479
435
  except Exception:
@@ -494,12 +450,12 @@ class SkillAdapter:
494
450
  message=f"守门人代执行返回异常: {d}")
495
451
 
496
452
  @staticmethod
497
- def _ssrf_check(url: str, allow_local: bool = False) -> str:
453
+ def _ssrf_check(url: str, allow_local: bool = False) -> str | None:
498
454
  """返回拦截原因(非空=拒绝),None=放行。委托模块级 ssrf_check(单一真源)。"""
499
455
  return ssrf_check(url, allow_local=allow_local)
500
456
 
501
457
 
502
- def ssrf_check(url: str, allow_local: bool = False) -> str:
458
+ def ssrf_check(url: str, allow_local: bool = False) -> str | None:
503
459
  """SSRF 防护(单一真源):返回拦截原因(非空=拒绝),None=放行。拦截非 http(s)/内网/环回/元数据。
504
460
 
505
461
  主进程 SkillAdapter 与守门人进程共用 —— 守门人代执行时,主进程可能被攻破传内网地址,
@@ -517,39 +473,31 @@ def ssrf_check(url: str, allow_local: bool = False) -> str:
517
473
  if not host:
518
474
  return "缺少 host"
519
475
  if allow_local:
520
- return None # 显式信任本地/内网(对接本地服务时)
521
- # 明确的危险主机名
476
+ return None
522
477
  if host.lower() in ("localhost", "metadata.google.internal"):
523
478
  return f"内部主机: {host}"
524
- # 显式的云元数据 IP(各云厂商,标准库可能漏判)
525
- CLOUD_METADATA = {"169.254.169.254", "100.100.100.200", # AWS/GCP/Azure, 阿里云
526
- "fd00:ec2::254"} # IPv6 云元数据
479
+ CLOUD_METADATA = {"169.254.169.254", "100.100.100.200",
480
+ "fd00:ec2::254"}
527
481
  if host in CLOUD_METADATA:
528
482
  return f"云元数据地址: {host}"
529
- # IP 地址 → 拦截私网/环回/链路本地/元数据
530
483
  try:
531
484
  ip = ipaddress.ip_address(host)
532
485
  if (ip.is_private or ip.is_loopback or ip.is_link_local
533
486
  or ip.is_reserved or ip.is_multicast):
534
487
  return f"内网/保留地址: {host}"
535
- # carrier-grade NAT 100.64.0.0/10(标准 is_private 不含,阿里云元数据在此段)
536
488
  if isinstance(ip, ipaddress.IPv4Address) and \
537
489
  ip in ipaddress.ip_network("100.64.0.0/10"):
538
490
  return f"运营商级 NAT 段: {host}"
539
491
  except ValueError:
540
- pass # 不是 IP(是域名),放行(域名解析后的 IP 拦截需更复杂方案,此处够用)
492
+ pass
541
493
  return None
542
494
 
543
495
 
544
- # ── 内置示例 skill(演示用,真实 MCP/API 留接口)─────────────
545
496
  def _demo_calc(expression: str = "") -> dict:
546
497
  """安全的演示计算 skill。"""
547
498
  allowed = set("0123456789+-*/(). ")
548
499
  if not set(expression) <= allowed:
549
500
  return {"error": "仅支持数字和基本运算符"}
550
- # 字符白名单只挡住"取名字/取属性"(字母被禁),挡不住**计算量攻击**:
551
- # `9**9**9` 这类表达式在 eval 阶段就会长时间计算并吃满内存 —— 属 R2 检测出的低危项。
552
- # 故在 eval 之前限长度、限连续乘方、限指数大小;eval 之后再兜底检查结果规模。
553
501
  if len(expression) > 200:
554
502
  return {"error": "表达式过长"}
555
503
  if expression.count("**") > 1:
@@ -574,14 +522,12 @@ def build_default_registry() -> SkillRegistry:
574
522
  """构建默认 skill 注册表:演示本地 skill + 留口的 MCP skill。"""
575
523
  reg = SkillRegistry()
576
524
 
577
- # 本地只读 skill(直接可用)
578
525
  reg.register(SkillSpec(
579
526
  id="calculator", name="计算器", description="数学表达式计算",
580
527
  kind="local", triggers=["计算", "算一下", "算", "等于多少", "加", "减", "乘", "除"],
581
528
  side_effect="read", handler=_demo_calc,
582
529
  params={"expression": "str"}))
583
530
 
584
- # MCP skill(留接口,本地配 server 后可用)——对齐我们接过的生态
585
531
  reg.register(SkillSpec(
586
532
  id="amap_route", name="高德路线", description="查询两地真实路线和时间",
587
533
  kind="mcp", triggers=["路线", "怎么走", "多久到", "导航"],
@@ -0,0 +1,111 @@
1
+ """技能包归档读取:zip / tar.gz 统一「按名读」,不落盘、不 extractall。
2
+
3
+ `markdown_skill.extract_skill_package` 只认 zip,而 ClawHub 发 zip、GitHub 发 tar.gz。
4
+ 两者共用同一套规则:拒路径穿越、限解压总量(防 zip bomb)、挑**最浅**的 SKILL.md
5
+ (GitHub 仓库里它常埋在 `skills/<name>/` 多级子目录下)。
6
+ """
7
+ from __future__ import annotations
8
+
9
+ import io
10
+ import json
11
+ import tarfile
12
+ import zipfile
13
+
14
+ MAX_TOTAL = 8 * 1024 * 1024
15
+
16
+ _CODE_EXT = (".py", ".sh", ".bash", ".ps1", ".js", ".mjs", ".cjs", ".ts",
17
+ ".rb", ".pl", ".bat", ".cmd", ".exe")
18
+
19
+
20
+ class PackageError(ValueError):
21
+ """包不可用:格式坏 / 缺 SKILL.md / 超体积。"""
22
+
23
+
24
+ def safe_name(name: str) -> bool:
25
+ """拒绝对路径、盘符与 `..` 段。按名读虽不落盘,但脏名字会流进日志与提示词。"""
26
+ n = (name or "").replace("\\", "/")
27
+ while n.startswith("./"):
28
+ n = n[2:]
29
+ if not n or n.startswith("/") or ".." in n.split("/"):
30
+ return False
31
+ return not (len(n) > 1 and n[1] == ":")
32
+
33
+
34
+ def _shallowest(names: list[str], base: str, subpath: str = "") -> str | None:
35
+ """挑最浅的同名文件(全小写比较)。subpath 非空时只在其下找。"""
36
+ want = f"/{subpath.strip('/')}/" if subpath.strip("/") else ""
37
+ cands = [
38
+ n for n in names
39
+ if safe_name(n)
40
+ and n.rsplit("/", 1)[-1].lower() == base
41
+ and (not want or want in f"/{n}")
42
+ ]
43
+ if not cands:
44
+ return None
45
+ return min(cands, key=lambda n: (n.count("/"), len(n)))
46
+
47
+
48
+ def pick_skill_md(names: list[str], subpath: str = "") -> str | None:
49
+ """挑最浅的 SKILL.md:ClawHub 在根,GitHub 仓库常在 skills/<name>/ 下。"""
50
+ return _shallowest(names, "skill.md", subpath)
51
+
52
+
53
+ def has_code(files: list[str]) -> bool:
54
+ """包内是否带可执行脚本 / 独立的 scripts/ 目录。"""
55
+ for n in files:
56
+ low = n.lower()
57
+ if low.endswith(_CODE_EXT) or "/scripts/" in f"/{low}":
58
+ return True
59
+ return False
60
+
61
+
62
+ def read_package(data: bytes, subpath: str = "", max_total: int = MAX_TOTAL) -> dict:
63
+ """解一个技能包,返回 {files, skill_md, meta}。"""
64
+ if not data:
65
+ raise PackageError("空包")
66
+ if data[:2] == b"PK":
67
+ names, read = _open_zip(data, max_total)
68
+ else:
69
+ names, read = _open_tar(data, max_total)
70
+ md = pick_skill_md(names, subpath)
71
+ if md is None:
72
+ where = f"(子路径 {subpath} 下)" if subpath else ""
73
+ raise PackageError(f"包内未找到 SKILL.md{where}")
74
+ meta: dict = {}
75
+ meta_name = _shallowest(names, "_meta.json")
76
+ if meta_name:
77
+ try:
78
+ meta = json.loads(read(meta_name).decode("utf-8", "replace")) or {}
79
+ except Exception:
80
+ meta = {}
81
+ return {"files": names, "skill_md": read(md).decode("utf-8", "replace"), "meta": meta}
82
+
83
+
84
+ def _open_zip(data: bytes, max_total: int):
85
+ try:
86
+ zf = zipfile.ZipFile(io.BytesIO(data))
87
+ except Exception as e:
88
+ raise PackageError(f"zip 解析失败: {e}") from None
89
+ infos = [i for i in zf.infolist() if not i.is_dir()]
90
+ total = sum(i.file_size for i in infos)
91
+ if total > max_total:
92
+ raise PackageError(f"解压体积超上限({total} > {max_total})")
93
+ return [i.filename for i in infos], zf.read
94
+
95
+
96
+ def _open_tar(data: bytes, max_total: int):
97
+ try:
98
+ tf = tarfile.open(fileobj=io.BytesIO(data), mode="r:*")
99
+ members = [m for m in tf.getmembers() if m.isfile()]
100
+ except Exception as e:
101
+ raise PackageError(f"既不是 zip 也不是 tar 包: {e}") from None
102
+ total = sum(m.size for m in members)
103
+ if total > max_total:
104
+ raise PackageError(f"解压体积超上限({total} > {max_total})")
105
+ by_name = {m.name: m for m in members}
106
+
107
+ def read(name: str) -> bytes:
108
+ fp = tf.extractfile(by_name[name])
109
+ return fp.read() if fp else b""
110
+
111
+ return [m.name for m in members], read