Coverage for agentos/desktop/skill_store_server.py: 0%
102 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"""
2Skill Store Server — Web-based skill marketplace with embedded browser support.
4Serves a local web UI that lists skills from multiple sources (OpenClaw, ClawHub,
5SkillsMP, LobeHub, etc.) and provides one-click install via the marketplace importer.
7Architecture:
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
13Usage:
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
18Requirements: pip install fastapi uvicorn aiohttp
19"""
21from __future__ import annotations
23import asyncio
24import json
25import os
26import sys
27import webbrowser
28from dataclasses import dataclass, field
29from pathlib import Path
30from typing import Optional
32try:
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
38except ImportError:
39 FASTAPI_AVAILABLE = False
42# ── Constants ──
43DEFAULT_PORT = 18900
44STATIC_DIR = Path(__file__).parent / "static"
46SKILL_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]
133# Known OpenClaw skills (from importer catalog + community)
134OPENCLAW_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]
152# ── Server ──
154def create_app() -> FastAPI:
155 """Create the FastAPI application for the skill store."""
156 app = FastAPI(title="NexusAgentOS Skill Store", version="1.7.5")
158 # ── API Routes ──
160 @app.get("/api/sources")
161 async def list_sources():
162 """List all skill sources (marketplaces)."""
163 return JSONResponse(SKILL_SOURCES)
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 })
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)
199 from agentos.marketplace.importer import UnifiedImporter, OpenClawImporter
200 from agentos.marketplace.registry import SkillRegistry
201 from agentos.marketplace.manifest import SkillManifest
203 install_dir = Path.home() / ".agentos" / "skills"
204 registry = SkillRegistry(install_dir=str(install_dir))
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)
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)
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)
241 from agentos.marketplace.importer import OpenClawImporter
242 from agentos.marketplace.registry import SkillRegistry
244 install_dir = Path.home() / ".agentos" / "skills"
245 registry = SkillRegistry(install_dir=str(install_dir))
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 })
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)
261 @app.get("/api/health")
262 async def health():
263 return {"status": "ok", "version": "1.7.5"}
265 # ── Static Files ──
266 if STATIC_DIR.exists():
267 app.mount("/static", StaticFiles(directory=str(STATIC_DIR)), name="static")
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)
277 return app
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>"""
334# ── Entry Point ──
336def 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.
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
352 app = create_app()
354 url = f"http://{host}:{port}"
355 print(f"NexusAgentOS Skill Store starting at {url}")
357 if open_browser:
358 webbrowser.open(url)
360 uvicorn.run(app, host=host, port=port, log_level="info")
363if __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)