Coverage for agentos/marketplace/manifest.py: 0%
118 statements
« prev ^ index » next coverage.py v7.14.3, created at 2026-07-02 09:59 +0800
« prev ^ index » next coverage.py v7.14.3, created at 2026-07-02 09:59 +0800
1"""
2AgentOS Skill Marketplace — Skill Manifest v1.0。
4兼容格式:
5 - agentos: 原生 AgentOS Skill 格式
6 - openclaw: OpenClaw 社区 Skill 格式(自动适配)
7 - mcp: MCP 协议 Skill(JSON-RPC stdio/sse 代理)
8 - generic: 通用 Python 包 Skill(无约束格式)
10参考:
11 OpenClaw Marketplace: https://github.com/openclaw/skills
12 MCP Specification: https://modelcontextprotocol.io
13"""
15from __future__ import annotations
17import hashlib
18import json
19from dataclasses import dataclass, field
20from enum import Enum
21from pathlib import Path
22from typing import Optional
25class SkillFormat(str, Enum):
26 AGENTOS = "agentos"
27 OPENCLAW = "openclaw"
28 MCP = "mcp"
29 GENERIC = "generic"
31 @classmethod
32 def detect(cls, raw: dict) -> "SkillFormat":
33 """从原始 manifest dict 自动检测格式。"""
34 if raw.get("mcpServers") or raw.get("tools") and isinstance(raw.get("tools"), list) and raw["tools"] and "server" in raw["tools"][0]:
35 return cls.MCP
36 if raw.get("format") == "openclaw" or raw.get("openclaw_version"):
37 return cls.OPENCLAW
38 if raw.get("format") == "agentos" or raw.get("entrypoint") or raw.get("tools") and isinstance(raw.get("tools"), list):
39 return cls.AGENTOS
40 return cls.GENERIC
43@dataclass
44class ToolDef:
45 """Skill 暴露的工具定义。"""
46 name: str
47 description: str = ""
48 parameters: dict = field(default_factory=dict)
49 returns: str = ""
52@dataclass
53class SkillManifest:
54 """统一的 Skill 清单 — 跨格式兼容。
56 支持从 agentos / openclaw / mcp / generic 四种格式的 manifest
57 自动解析为统一模型。安装和解依赖均基于本模型。
58 """
60 name: str
61 version: str = "0.1.0"
62 description: str = ""
63 author: str = "unknown"
64 license_: str = "MIT"
65 format: SkillFormat = SkillFormat.GENERIC
67 # AgentOS 原生字段
68 entrypoint: str = "" # "module:func" 格式的入口点
69 tools: list[ToolDef] = field(default_factory=list)
70 dependencies: list[str] = field(default_factory=list)
72 # MCP 兼容字段
73 mcp_command: str = "" # MCP server 启动命令,如 "npx -y @anthropic/mcp-server"
74 mcp_args: list[str] = field(default_factory=list)
75 mcp_env: dict = field(default_factory=dict)
76 mcp_type: str = "stdio" # stdio | sse
78 # OpenClaw 兼容字段
79 openclaw_version: str = ""
81 # 通用字段
82 tags: list[str] = field(default_factory=list)
83 homepage: str = ""
84 repository: str = ""
85 icon: str = ""
86 min_agentos_version: str = "1.7.0"
88 # 元数据
89 install_path: str = ""
90 source: str = "" # pypi | github | local | url
91 manifest_hash: str = ""
93 @classmethod
94 def from_dict(cls, raw: dict, source: str = "", install_path: str = "") -> "SkillManifest":
95 """从原始 dict 自动检测格式并解析。"""
96 fmt = SkillFormat.detect(raw)
97 m = cls(name="", description="")
99 m.name = raw.get("name", "")
100 m.version = str(raw.get("version", "0.1.0"))
101 m.description = raw.get("description", "")
102 m.author = raw.get("author", raw.get("maintainer", "unknown"))
103 m.license_ = raw.get("license", raw.get("license_", "MIT"))
104 m.format = fmt
105 m.source = source
106 m.install_path = install_path
107 m.tags = raw.get("tags", raw.get("keywords", []))
108 m.homepage = raw.get("homepage", raw.get("url", ""))
109 m.repository = raw.get("repository", raw.get("repo", ""))
110 m.icon = raw.get("icon", "")
111 m.min_agentos_version = raw.get("min_agentos_version", raw.get("requires_agentos", "1.7.0"))
113 if fmt == SkillFormat.AGENTOS:
114 m.entrypoint = raw.get("entrypoint", "")
115 m.dependencies = raw.get("dependencies", raw.get("requires", []))
116 tools_raw = raw.get("tools", [])
117 for t in tools_raw:
118 m.tools.append(ToolDef(
119 name=t.get("name", ""),
120 description=t.get("description", ""),
121 parameters=t.get("parameters", {}),
122 returns=t.get("returns", ""),
123 ))
125 elif fmt == SkillFormat.OPENCLAW:
126 # OpenClaw 格式:skill.yaml → agentos 适配
127 m.entrypoint = raw.get("entrypoint", raw.get("main", ""))
128 m.dependencies = raw.get("dependencies", raw.get("pip", []))
129 m.openclaw_version = raw.get("openclaw_version", raw.get("format_version", ""))
130 tools_raw = raw.get("tools", raw.get("functions", []))
131 for t in tools_raw:
132 m.tools.append(ToolDef(
133 name=t.get("name", ""),
134 description=t.get("description", ""),
135 parameters=t.get("parameters", t.get("input_schema", {})),
136 ))
138 elif fmt == SkillFormat.MCP:
139 # MCP 格式:mcpServers.{name} → agentos 适配
140 servers = raw.get("mcpServers", {})
141 if servers:
142 first = list(servers.values())[0] if servers else {}
143 m.mcp_command = first.get("command", "")
144 m.mcp_args = first.get("args", [])
145 m.mcp_env = first.get("env", {})
146 m.mcp_type = first.get("type", "stdio")
147 if not m.name and "server_name" in raw:
148 m.name = raw["server_name"]
149 if not m.description:
150 m.description = f"MCP Server: {m.mcp_command} {' '.join(m.mcp_args)}"
151 tools_raw = raw.get("tools", [])
152 for t in tools_raw:
153 m.tools.append(ToolDef(
154 name=t.get("name", ""),
155 description=t.get("description", ""),
156 parameters=t.get("inputSchema", {}),
157 ))
159 elif fmt == SkillFormat.GENERIC:
160 m.entrypoint = raw.get("entrypoint", raw.get("main", ""))
161 m.dependencies = raw.get("dependencies", raw.get("requires", raw.get("install_requires", [])))
162 m.description = raw.get("description", raw.get("summary", ""))
164 # 计算 manifest 哈希
165 m.manifest_hash = m._compute_hash()
166 return m
168 def to_dict(self) -> dict:
169 """导出为标准 agentos manifest dict。"""
170 return {
171 "name": self.name,
172 "version": self.version,
173 "description": self.description,
174 "author": self.author,
175 "license": self.license_,
176 "format": self.format.value,
177 "entrypoint": self.entrypoint,
178 "tools": [
179 {
180 "name": t.name,
181 "description": t.description,
182 "parameters": t.parameters,
183 "returns": t.returns,
184 }
185 for t in self.tools
186 ],
187 "dependencies": self.dependencies,
188 "tags": self.tags,
189 "homepage": self.homepage,
190 "repository": self.repository,
191 "icon": self.icon,
192 "min_agentos_version": self.min_agentos_version,
193 "mcp": {
194 "command": self.mcp_command,
195 "args": self.mcp_args,
196 "env": self.mcp_env,
197 "type": self.mcp_type,
198 } if self.mcp_command else None,
199 "manifest_hash": self.manifest_hash,
200 "source": self.source,
201 }
203 def _compute_hash(self) -> str:
204 raw = json.dumps(self.to_dict(), sort_keys=True, default=str)
205 return hashlib.sha256(raw.encode()).hexdigest()[:16]
207 @staticmethod
208 def load_from_path(manifest_path: str | Path, source: str = "local") -> Optional["SkillManifest"]:
209 """从本地 manifest 文件加载。支持 yaml/json。"""
210 p = Path(manifest_path)
211 if not p.exists():
212 return None
213 text = p.read_text(encoding="utf-8")
214 if p.suffix in (".yaml", ".yml"):
215 import yaml
216 raw = yaml.safe_load(text)
217 else:
218 raw = json.loads(text)
219 return SkillManifest.from_dict(raw, source=source, install_path=str(p.parent))