1"""

2 Skill Store Server — Web-based skill marketplace with embedded browser support.

3

4 Serves a local web UI that lists skills from multiple sources (OpenClaw, ClawHub,

5 SkillsMP, LobeHub, etc.) and provides one-click install via the marketplace importer.

6

7 Architecture:

8 - FastAPI server (localhost:18900 by default)

9 - Web UI with embedded iframe links to external skill stores

10 - REST API: GET /api/skills, POST /api/install, GET /api/sources

11 - WebSocket for real-time install progress

12

13 Usage:

14 agentos skill-store # Start skill store server

15 agentos skill-store --port 18900 # Custom port

16 agentos skill-store --open # Auto-open in browser

17

18 Requirements: pip install fastapi uvicorn aiohttp

19"""

20

21 from __future__ import annotations

22

23 import asyncio

24 import json

25 import os

26 import sys

27 import webbrowser

28 from dataclasses import dataclass, field

29 from pathlib import Path

30 from typing import Optional

31

32 try:

33 from fastapi import FastAPI, WebSocket, WebSocketDisconnect

34 from fastapi.responses import HTMLResponse, JSONResponse, FileResponse

35 from fastapi.staticfiles import StaticFiles

36 import uvicorn

37 FASTAPI_AVAILABLE = True

38 except ImportError:

39 FASTAPI_AVAILABLE = False

40

41

42# ── Constants ──

43 DEFAULT_PORT = 18900

44 STATIC_DIR = Path(__file__).parent / "static"

45

46 SKILL_SOURCES: list[dict] = [

47 {

48 "id": "openclaw",

49 "name": "OpenClaw Skill Store",

50 "url": "https://github.com/nicepkg/openclaw-skill-store",

51 "web_url": "https://github.com/nicepkg/openclaw-skill-store/tree/main/skills",

52 "description": "OpenClaw 官方社区技能商店，14+ 核心技能",

53 "skill_count": "14+",

54 "icon": "openclaw",

55 "tags": ["官方", "社区", "文档处理"],

56 "installable": True,

57 "source_type": "openclaw",

58 },

59 {

60 "id": "clawhub",

61 "name": "ClawHub",

62 "url": "https://github.com/clawhub-community/skills",

63 "web_url": "https://github.com/clawhub-community/skills",

64 "description": "ClawHub 社区技能聚合，5,700+ 技能",

65 "skill_count": "5,700+",

66 "icon": "clawhub",

67 "tags": ["社区", "聚合", "高质量"],

68 "installable": False,

69 "source_type": "github",

70 },

71 {

72 "id": "skillsmp",

73 "name": "SkillsMP",

74 "url": "https://skills.mp/",

75 "web_url": "https://skills.mp/",

76 "description": "技能界的 Google，164 万技能文件索引",

77 "skill_count": "164万+",

78 "icon": "skillsmp",

79 "tags": ["索引", "搜索", "规模最大"],

80 "installable": False,

81 "source_type": "web",

82 },

83 {

84 "id": "lobehub",

85 "name": "LobeHub Skills",

86 "url": "https://lobehub.com/skills",

87 "web_url": "https://lobehub.com/skills",

88 "description": "LobeHub 生态精品技能平台，28 万+",

89 "skill_count": "28万+",

90 "icon": "lobehub",

91 "tags": ["精品", "集成", "多模态"],

92 "installable": False,

93 "source_type": "web",

94 },

95 {

96 "id": "skillhub",

97 "name": "SkillHub Club",

98 "url": "https://skillhub.club/",

99 "web_url": "https://skillhub.club/",

100 "description": "AI 评分驱动的品质筛选市集",

101 "skill_count": "1.6万+",

102 "icon": "skillhub",

103 "tags": ["品质", "AI评分", "精选"],

104 "installable": False,

105 "source_type": "web",

106 },

107 {

108 "id": "skills_sh",

109 "name": "skills.sh",

110 "url": "https://skills.sh/",

111 "web_url": "https://skills.sh/",

112 "description": "Vercel Labs 运营，npx skills add 一键安装",

113 "skill_count": "67万+",

114 "icon": "skills_sh",

115 "tags": ["一键安装", "CLI", "多平台"],

116 "installable": False,

117 "source_type": "web",

118 },

119 {

120 "id": "awesome_agent_skills",

121 "name": "awesome-agent-skills",

122 "url": "https://github.com/nicepkg/awesome-agent-skills",

123 "web_url": "https://github.com/nicepkg/awesome-agent-skills",

124 "description": "人工审核的优质技能合集，380+ 精选",

125 "skill_count": "380+",

126 "icon": "awesome",

127 "tags": ["人工审核", "安全", "精选"],

128 "installable": False,

129 "source_type": "github",

130 },

131]

132

133# Known OpenClaw skills (from importer catalog + community)

134 OPENCLAW_SKILLS: list[dict] = [

135 {"name": "skill-creator", "description": "Create new skills from templates", "tags": ["meta", "development"]},

136 {"name": "pdf-tools", "description": "PDF manipulation, merge, split, extract text", "tags": ["document", "pdf"]},

137 {"name": "xlsx-tools", "description": "Excel/Spreadsheet creation and editing", "tags": ["document", "excel"]},

138 {"name": "docx-tools", "description": "Word document processing", "tags": ["document", "word"]},

139 {"name": "pptx-tools", "description": "PowerPoint presentation generation", "tags": ["document", "ppt"]},

140 {"name": "image-tools", "description": "Image processing, resize, convert, OCR", "tags": ["media", "image"]},

141 {"name": "web-search", "description": "Advanced web search with multiple engines", "tags": ["search", "web"]},

142 {"name": "browser-automation", "description": "Browser automation with Playwright", "tags": ["browser", "automation"]},

143 {"name": "code-review", "description": "Automated code review and suggestions", "tags": ["code", "quality"]},

144 {"name": "git-tools", "description": "Git workflow automation and helpers", "tags": ["git", "devops"]},

145 {"name": "file-organizer", "description": "Automated file organization and cleanup", "tags": ["files", "automation"]},

146 {"name": "data-analysis", "description": "Data analysis and visualization", "tags": ["data", "analytics"]},

147 {"name": "api-tester", "description": "API testing and documentation generation", "tags": ["api", "testing"]},

148 {"name": "markdown-tools", "description": "Markdown editing, preview, and conversion", "tags": ["document", "markdown"]},

149]

150

151

152# ── Server ──

153

154 def create_app() -> FastAPI:

155 """Create the FastAPI application for the skill store."""

156 app = FastAPI(title="NexusAgentOS Skill Store", version="1.7.5")

157

158 # ── API Routes ──

159

160 @app.get("/api/sources")

161 async def list_sources():

162 """List all skill sources (marketplaces)."""

163 return JSONResponse(SKILL_SOURCES)

164

165 @app.get("/api/skills")

166 async def list_skills(source: str = "openclaw", search: str = ""):

167 """List skills from a specific source."""

168 if source == "openclaw":

169 skills = OPENCLAW_SKILLS

170 if search:

171 skills = [

172 s for s in skills

173 if search.lower() in s["name"].lower()

174 or search.lower() in s["description"].lower()

175 or any(search.lower() in t.lower() for t in s.get("tags", []))

176 ]

177 return JSONResponse({

178 "source": "openclaw",

179 "source_name": "OpenClaw Skill Store",

180 "total": len(skills),

181 "skills": skills,

182 })

183 return JSONResponse({

184 "source": source,

185 "total": 0,

186 "skills": [],

187 "message": f"Source '{source}' is not locally installable. Open the marketplace URL to browse.",

188 })

189

190 @app.post("/api/install")

191 async def install_skill(skill_name: str, source: str = "openclaw"):

192 """Install a skill from a source. Uses the marketplace importer."""

193 try:

194 # Add agentos to path

195 agentos_root = str(Path(__file__).parent.parent.parent)

196 if agentos_root not in sys.path:

197 sys.path.insert(0, agentos_root)

198

199 from agentos.marketplace.importer import UnifiedImporter, OpenClawImporter

200 from agentos.marketplace.registry import SkillRegistry

201 from agentos.marketplace.manifest import SkillManifest

202

203 install_dir = Path.home() / ".agentos" / "skills"

204 registry = SkillRegistry(install_dir=str(install_dir))

205

206 if source == "openclaw":

207 importer = OpenClawImporter(registry)

208 skill = await importer.import_skill(skill_name)

209 if skill:

210 return JSONResponse({

211 "status": "installed",

212 "skill": skill_name,

213 "path": str(skill.path) if hasattr(skill, 'path') else str(install_dir / skill_name),

214 })

215 return JSONResponse({

216 "status": "failed",

217 "skill": skill_name,

218 "error": "Skill not found in OpenClaw store",

219 }, status_code=404)

220

221 return JSONResponse({

222 "status": "not_installable",

223 "skill": skill_name,

224 "message": f"Source '{source}' requires manual installation.",

225 })

226 except Exception as e:

227 return JSONResponse({

228 "status": "error",

229 "skill": skill_name,

230 "error": str(e),

231 }, status_code=500)

232

233 @app.post("/api/install-all")

234 async def install_all(source: str = "openclaw"):

235 """Batch install all skills from a source."""

236 try:

237 agentos_root = str(Path(__file__).parent.parent.parent)

238 if agentos_root not in sys.path:

239 sys.path.insert(0, agentos_root)

240

241 from agentos.marketplace.importer import OpenClawImporter

242 from agentos.marketplace.registry import SkillRegistry

243

244 install_dir = Path.home() / ".agentos" / "skills"

245 registry = SkillRegistry(install_dir=str(install_dir))

246

247 if source == "openclaw":

248 importer = OpenClawImporter(registry)

249 results = await importer.import_all()

250 return JSONResponse({

251 "status": "completed",

252 "total": len(results),

253 "installed": [r.get("name", "") for r in results],

254 "failed": [],

255 })

256

257 return JSONResponse({"status": "error", "error": f"Cannot batch install from {source}"}, status_code=400)

258 except Exception as e:

259 return JSONResponse({"status": "error", "error": str(e)}, status_code=500)

260

261 @app.get("/api/health")

262 async def health():

263 return {"status": "ok", "version": "1.7.5"}

264

265 # ── Static Files ──

266 if STATIC_DIR.exists():

267 app.mount("/static", StaticFiles(directory=str(STATIC_DIR)), name="static")

268

269 @app.get("/", response_class=HTMLResponse)

270 async def index():

271 """Serve the skill store web UI."""

272 html_path = STATIC_DIR / "index.html"

273 if html_path.exists():

274 return FileResponse(str(html_path), media_type="text/html")

275 return HTMLResponse(_FALLBACK_HTML)

276

277 return app

278

279

280# ── Fallback HTML (when static/index.html is missing) ──

281_FALLBACK_HTML = """<!DOCTYPE html>

282<html lang="zh-CN">

283<head>

284<meta charset="UTF-8">

285<meta name="viewport" content="width=device-width, initial-scale=1.0">

286<title>NexusAgentOS Skill Store</title>

287<style>

288 :root { --bg: #0d1117; --card: #161b22; --border: #30363d; --text: #c9d1d9; --accent: #58a6ff; }

289 * { margin: 0; padding: 0; box-sizing: border-box; }

290 body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif; background: var(--bg); color: var(--text); padding: 2rem; }

291 h1 { font-size: 1.5rem; margin-bottom: 0.5rem; }

292 .subtitle { color: #8b949e; margin-bottom: 2rem; }

293 .grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(320px, 1fr)); gap: 1rem; }

294 .card { background: var(--card); border: 1px solid var(--border); border-radius: 8px; padding: 1.25rem; }

295 .card h2 { font-size: 1rem; color: var(--accent); margin-bottom: 0.5rem; }

296 .card p { font-size: 0.875rem; color: #8b949e; margin-bottom: 0.75rem; }

297 .tags { display: flex; gap: 0.375rem; flex-wrap: wrap; margin-bottom: 0.75rem; }

298 .tag { background: #1f6feb22; color: var(--accent); padding: 0.125rem 0.5rem; border-radius: 12px; font-size: 0.75rem; }

299 .btn { display: inline-block; padding: 0.5rem 1rem; border-radius: 6px; font-size: 0.875rem; cursor: pointer; border: 1px solid var(--border); text-decoration: none; }

300 .btn-primary { background: #238636; border-color: #238636; color: #fff; }

301 .btn-outline { background: transparent; color: var(--text); }

302 .btn-outline:hover { background: #30363d; }

303 .count { font-size: 0.75rem; color: #8b949e; }

304</style>

305</head>

306<body>

307<h1>NexusAgentOS Skill Store</h1>

308<p class="subtitle">从社区市场发现和安装技能。启动完整 UI：pip install textual && agentos tui --market</p>

309<div class="grid" id="sources"></div>

310<script>

311 fetch('/api/sources').then(r => r.json()).then(sources => {

312 const grid = document.getElementById('sources');

313 sources.forEach(s => {

314 const card = document.createElement('div');

315 card.className = 'card';

316 card.innerHTML = `<h2>${s.name} <span class="count">(${s.skill_count})</span></h2>

317 <p>${s.description}</p>

318 <div class="tags">${s.tags.map(t => `<span class="tag">${t}</span>`).join('')}</div>

319 ${s.installable

320 ? `<button class="btn btn-primary" onclick="installAll('${s.id}')">安装全部</button>`

321 : `<a href="${s.web_url}" target="_blank" class="btn btn-outline">打开市场</a>`}`;

322 grid.appendChild(card);

323 });

324 });

325 function installAll(src) {

326 fetch('/api/install-all?source=' + src, { method: 'POST' })

327 .then(r => r.json()).then(d => alert('安装完成: ' + d.installed?.length + ' 个技能'));

328 }

329</script>

330</body>

331</html>"""

332

333

334# ── Entry Point ──

335

336 def launch_skill_store(

337 port: int = DEFAULT_PORT,

338 host: str = "127.0.0.1",

339 open_browser: bool = False,

340) -> None:

341 """Launch the skill store web server.

342

343 Args:

344 port: HTTP port to listen on.

345 host: Host to bind to.

346 open_browser: Auto-open in system browser.

347 """

348 if not FASTAPI_AVAILABLE:

349 print("ERROR: fastapi/uvicorn not installed. Run: pip install fastapi uvicorn")

350 return

351

352 app = create_app()

353

354 url = f"http://{host}:{port}"

355 print(f"NexusAgentOS Skill Store starting at {url}")

356

357 if open_browser:

358 webbrowser.open(url)

359

360 uvicorn.run(app, host=host, port=port, log_level="info")

361

362

363 if __name__ == "__main__":

364 import argparse

365 parser = argparse.ArgumentParser(description="NexusAgentOS Skill Store Server")

366 parser.add_argument("--port", type=int, default=DEFAULT_PORT, help="Server port")

367 parser.add_argument("--host", default="127.0.0.1", help="Server host")

368 parser.add_argument("--open", action="store_true", dest="open_browser", help="Open in browser")

369 args = parser.parse_args()

370 launch_skill_store(port=args.port, host=args.host, open_browser=args.open_browser)