Coverage for agentos/desktop/skill_store_server.py: 0%
96 statements
« prev ^ index » next coverage.py v7.14.3, created at 2026-07-06 19:15 +0800
« prev ^ index » next coverage.py v7.14.3, created at 2026-07-06 19:15 +0800
1"""
2Skill Store Server — Web-based skill marketplace with embedded browser support.
3Serves a local web UI that lists skills from multiple sources (OpenClaw, ClawHub,
4SkillsMP, LobeHub, etc.) and provides one-click install via the marketplace importer.
6Architecture:
7 - FastAPI server (localhost:18900 by default)
8 - Web UI with embedded iframe links to external skill stores
9 - REST API: GET /api/skills, POST /api/install, GET /api/sources
10 - WebSocket for real-time install progress
12Usage:
13 agentos skill-store # Start skill store server
14 agentos skill-store --port 18900 # Custom port
15 agentos skill-store --open # Auto-open in browser
17Requirements: pip install fastapi uvicorn aiohttp
18"""
20from __future__ import annotations
22import sys
23import webbrowser
24from pathlib import Path
26try:
27 import uvicorn
28 from fastapi import FastAPI
29 from fastapi.responses import FileResponse, HTMLResponse, JSONResponse
30 from fastapi.staticfiles import StaticFiles
32 FASTAPI_AVAILABLE = True
33except ImportError:
34 FASTAPI_AVAILABLE = False
37# ── Constants ──
38DEFAULT_PORT = 18900
39STATIC_DIR = Path(__file__).parent / "static"
41SKILL_SOURCES: list[dict] = [
42 {
43 "id": "openclaw",
44 "name": "OpenClaw Skill Store",
45 "url": "https://github.com/nicepkg/openclaw-skill-store",
46 "web_url": "https://github.com/nicepkg/openclaw-skill-store/tree/main/skills",
47 "description": "OpenClaw 官方社区技能商店,14+ 核心技能",
48 "skill_count": "14+",
49 "icon": "openclaw",
50 "tags": ["官方", "社区", "文档处理"],
51 "installable": True,
52 "source_type": "openclaw",
53 },
54 {
55 "id": "clawhub",
56 "name": "ClawHub",
57 "url": "https://github.com/clawhub-community/skills",
58 "web_url": "https://github.com/clawhub-community/skills",
59 "description": "ClawHub 社区技能聚合,5,700+ 技能",
60 "skill_count": "5,700+",
61 "icon": "clawhub",
62 "tags": ["社区", "聚合", "高质量"],
63 "installable": False,
64 "source_type": "github",
65 },
66 {
67 "id": "skillsmp",
68 "name": "SkillsMP",
69 "url": "https://skills.mp/",
70 "web_url": "https://skills.mp/",
71 "description": "技能界的 Google,164 万技能文件索引",
72 "skill_count": "164万+",
73 "icon": "skillsmp",
74 "tags": ["索引", "搜索", "规模最大"],
75 "installable": False,
76 "source_type": "web",
77 },
78 {
79 "id": "lobehub",
80 "name": "LobeHub Skills",
81 "url": "https://lobehub.com/skills",
82 "web_url": "https://lobehub.com/skills",
83 "description": "LobeHub 生态精品技能平台,28 万+",
84 "skill_count": "28万+",
85 "icon": "lobehub",
86 "tags": ["精品", "集成", "多模态"],
87 "installable": False,
88 "source_type": "web",
89 },
90 {
91 "id": "skillhub",
92 "name": "SkillHub Club",
93 "url": "https://skillhub.club/",
94 "web_url": "https://skillhub.club/",
95 "description": "AI 评分驱动的品质筛选市集",
96 "skill_count": "1.6万+",
97 "icon": "skillhub",
98 "tags": ["品质", "AI评分", "精选"],
99 "installable": False,
100 "source_type": "web",
101 },
102 {
103 "id": "skills_sh",
104 "name": "skills.sh",
105 "url": "https://skills.sh/",
106 "web_url": "https://skills.sh/",
107 "description": "Vercel Labs 运营,npx skills add 一键安装",
108 "skill_count": "67万+",
109 "icon": "skills_sh",
110 "tags": ["一键安装", "CLI", "多平台"],
111 "installable": False,
112 "source_type": "web",
113 },
114 {
115 "id": "awesome_agent_skills",
116 "name": "awesome-agent-skills",
117 "url": "https://github.com/nicepkg/awesome-agent-skills",
118 "web_url": "https://github.com/nicepkg/awesome-agent-skills",
119 "description": "人工审核的优质技能合集,380+ 精选",
120 "skill_count": "380+",
121 "icon": "awesome",
122 "tags": ["人工审核", "安全", "精选"],
123 "installable": False,
124 "source_type": "github",
125 },
126]
128# Known OpenClaw skills (from importer catalog + community)
129OPENCLAW_SKILLS: list[dict] = [
130 {
131 "name": "skill-creator",
132 "description": "Create new skills from templates",
133 "tags": ["meta", "development"],
134 },
135 {
136 "name": "pdf-tools",
137 "description": "PDF manipulation, merge, split, extract text",
138 "tags": ["document", "pdf"],
139 },
140 {
141 "name": "xlsx-tools",
142 "description": "Excel/Spreadsheet creation and editing",
143 "tags": ["document", "excel"],
144 },
145 {"name": "docx-tools", "description": "Word document processing", "tags": ["document", "word"]},
146 {
147 "name": "pptx-tools",
148 "description": "PowerPoint presentation generation",
149 "tags": ["document", "ppt"],
150 },
151 {
152 "name": "image-tools",
153 "description": "Image processing, resize, convert, OCR",
154 "tags": ["media", "image"],
155 },
156 {
157 "name": "web-search",
158 "description": "Advanced web search with multiple engines",
159 "tags": ["search", "web"],
160 },
161 {
162 "name": "browser-automation",
163 "description": "Browser automation with Playwright",
164 "tags": ["browser", "automation"],
165 },
166 {
167 "name": "code-review",
168 "description": "Automated code review and suggestions",
169 "tags": ["code", "quality"],
170 },
171 {
172 "name": "git-tools",
173 "description": "Git workflow automation and helpers",
174 "tags": ["git", "devops"],
175 },
176 {
177 "name": "file-organizer",
178 "description": "Automated file organization and cleanup",
179 "tags": ["files", "automation"],
180 },
181 {
182 "name": "data-analysis",
183 "description": "Data analysis and visualization",
184 "tags": ["data", "analytics"],
185 },
186 {
187 "name": "api-tester",
188 "description": "API testing and documentation generation",
189 "tags": ["api", "testing"],
190 },
191 {
192 "name": "markdown-tools",
193 "description": "Markdown editing, preview, and conversion",
194 "tags": ["document", "markdown"],
195 },
196]
199# ── Server ──
202def create_app() -> FastAPI:
203 """Create the FastAPI application for the skill store."""
204 app = FastAPI(title="NexusAgentOS Skill Store", version="1.7.5")
206 # ── API Routes ──
208 @app.get("/api/sources")
209 async def list_sources():
210 """List all skill sources (marketplaces)."""
211 return JSONResponse(SKILL_SOURCES)
213 @app.get("/api/skills")
214 async def list_skills(source: str = "openclaw", search: str = ""):
215 """List skills from a specific source."""
216 if source == "openclaw":
217 skills = OPENCLAW_SKILLS
218 if search:
219 skills = [
220 s
221 for s in skills
222 if search.lower() in s["name"].lower()
223 or search.lower() in s["description"].lower()
224 or any(search.lower() in t.lower() for t in s.get("tags", []))
225 ]
226 return JSONResponse(
227 {
228 "source": "openclaw",
229 "source_name": "OpenClaw Skill Store",
230 "total": len(skills),
231 "skills": skills,
232 }
233 )
234 return JSONResponse(
235 {
236 "source": source,
237 "total": 0,
238 "skills": [],
239 "message": f"Source '{source}' is not locally installable. Open the marketplace URL to browse.",
240 }
241 )
243 @app.post("/api/install")
244 async def install_skill(skill_name: str, source: str = "openclaw"):
245 """Install a skill from a source. Uses the marketplace importer."""
246 try:
247 # Add agentos to path
248 agentos_root = str(Path(__file__).parent.parent.parent)
249 if agentos_root not in sys.path:
250 sys.path.insert(0, agentos_root)
252 from agentos.marketplace.importer import OpenClawImporter
253 from agentos.marketplace.registry import SkillRegistry
255 install_dir = Path.home() / ".agentos" / "skills"
256 registry = SkillRegistry(install_dir=str(install_dir))
258 if source == "openclaw":
259 importer = OpenClawImporter(registry)
260 skill = await importer.import_skill(skill_name)
261 if skill:
262 return JSONResponse(
263 {
264 "status": "installed",
265 "skill": skill_name,
266 "path": (
267 str(skill.path)
268 if hasattr(skill, "path")
269 else str(install_dir / skill_name)
270 ),
271 }
272 )
273 return JSONResponse(
274 {
275 "status": "failed",
276 "skill": skill_name,
277 "error": "Skill not found in OpenClaw store",
278 },
279 status_code=404,
280 )
282 return JSONResponse(
283 {
284 "status": "not_installable",
285 "skill": skill_name,
286 "message": f"Source '{source}' requires manual installation.",
287 }
288 )
289 except Exception as e:
290 return JSONResponse(
291 {
292 "status": "error",
293 "skill": skill_name,
294 "error": str(e),
295 },
296 status_code=500,
297 )
299 @app.post("/api/install-all")
300 async def install_all(source: str = "openclaw"):
301 """Batch install all skills from a source."""
302 try:
303 agentos_root = str(Path(__file__).parent.parent.parent)
304 if agentos_root not in sys.path:
305 sys.path.insert(0, agentos_root)
307 from agentos.marketplace.importer import OpenClawImporter
308 from agentos.marketplace.registry import SkillRegistry
310 install_dir = Path.home() / ".agentos" / "skills"
311 registry = SkillRegistry(install_dir=str(install_dir))
313 if source == "openclaw":
314 importer = OpenClawImporter(registry)
315 results = await importer.import_all()
316 return JSONResponse(
317 {
318 "status": "completed",
319 "total": len(results),
320 "installed": [r.get("name", "") for r in results],
321 "failed": [],
322 }
323 )
325 return JSONResponse(
326 {"status": "error", "error": f"Cannot batch install from {source}"}, status_code=400
327 )
328 except Exception as e:
329 return JSONResponse({"status": "error", "error": str(e)}, status_code=500)
331 @app.get("/api/health")
332 async def health():
333 return {"status": "ok", "version": "1.7.5"}
335 # ── Static Files ──
336 if STATIC_DIR.exists():
337 app.mount("/static", StaticFiles(directory=str(STATIC_DIR)), name="static")
339 @app.get("/", response_class=HTMLResponse)
340 async def index():
341 """Serve the skill store web UI."""
342 html_path = STATIC_DIR / "index.html"
343 if html_path.exists():
344 return FileResponse(str(html_path), media_type="text/html")
345 return HTMLResponse(_FALLBACK_HTML)
347 return app
350# ── Fallback HTML (when static/index.html is missing) ──
351_FALLBACK_HTML = """<!DOCTYPE html>
352<html lang="zh-CN">
353<head>
354<meta charset="UTF-8">
355<meta name="viewport" content="width=device-width, initial-scale=1.0">
356<title>NexusAgentOS Skill Store</title>
357<style>
358 :root { --bg: #0d1117; --card: #161b22; --border: #30363d; --text: #c9d1d9; --accent: #58a6ff; }
359 * { margin: 0; padding: 0; box-sizing: border-box; }
360 h1 { font-size: 1.5rem; margin-bottom: 0.5rem; }
361 .subtitle { color: #8b949e; margin-bottom: 2rem; }
362 .grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(320px, 1fr)); gap: 1rem; }
363 .card { background: var(--card); border: 1px solid var(--border); border-radius: 8px; padding: 1.25rem; }
364 .card h2 { font-size: 1rem; color: var(--accent); margin-bottom: 0.5rem; }
365 .card p { font-size: 0.875rem; color: #8b949e; margin-bottom: 0.75rem; }
366 .tags { display: flex; gap: 0.375rem; flex-wrap: wrap; margin-bottom: 0.75rem; }
367 .btn-primary { background: #238636; border-color: #238636; color: #fff; }
368 .btn-outline { background: transparent; color: var(--text); }
369 .btn-outline:hover { background: #30363d; }
370 .count { font-size: 0.75rem; color: #8b949e; }
371</style>
372</head>
373<body>
374<h1>NexusAgentOS Skill Store</h1>
375<p class="subtitle">从社区市场发现和安装技能。启动完整 UI:pip install textual && agentos tui --market</p>
376<div class="grid" id="sources"></div>
377<script>
378 fetch('/api/sources').then(r => r.json()).then(sources => {
379 const grid = document.getElementById('sources');
380 sources.forEach(s => {
381 const card = document.createElement('div');
382 card.className = 'card';
383 card.innerHTML = `<h2>${s.name} <span class="count">(${s.skill_count})</span></h2>
384 <p>${s.description}</p>
385 <div class="tags">${s.tags.map(t => `<span class="tag">${t}</span>`).join('')}</div>
386 ${s.installable
387 ? `<button class="btn btn-primary" onclick="installAll('${s.id}')">安装全部</button>`
388 : `<a href="${s.web_url}" target="_blank" class="btn btn-outline">打开市场</a>`}`;
389 grid.appendChild(card);
390 });
391 });
392 function installAll(src) {
393 fetch('/api/install-all?source=' + src, { method: 'POST' })
394 .then(r => r.json()).then(d => alert('安装完成: ' + d.installed?.length + ' 个技能'));
395 }
396</script>
397</body>
398</html>"""
401# ── Entry Point ──
404def launch_skill_store(
405 port: int = DEFAULT_PORT,
406 host: str = "127.0.0.1",
407 open_browser: bool = False,
408) -> None:
409 """Launch the skill store web server.
411 Args:
412 port: HTTP port to listen on.
413 host: Host to bind to.
414 open_browser: Auto-open in system browser.
415 """
416 if not FASTAPI_AVAILABLE:
417 print("ERROR: fastapi/uvicorn not installed. Run: pip install fastapi uvicorn")
418 return
420 app = create_app()
422 url = f"http://{host}:{port}"
423 print(f"NexusAgentOS Skill Store starting at {url}")
425 if open_browser:
426 webbrowser.open(url)
428 uvicorn.run(app, host=host, port=port, log_level="info")
431if __name__ == "__main__":
432 import argparse
434 parser = argparse.ArgumentParser(description="NexusAgentOS Skill Store Server")
435 parser.add_argument("--port", type=int, default=DEFAULT_PORT, help="Server port")
436 parser.add_argument("--host", default="127.0.0.1", help="Server host")
437 parser.add_argument("--open", action="store_true", dest="open_browser", help="Open in browser")
438 args = parser.parse_args()
439 launch_skill_store(port=args.port, host=args.host, open_browser=args.open_browser)