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

1""" 

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

3 

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. 

6 

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 

12 

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 

17 

18Requirements: pip install fastapi uvicorn aiohttp 

19""" 

20 

21from __future__ import annotations 

22 

23import asyncio 

24import json 

25import os 

26import sys 

27import webbrowser 

28from dataclasses import dataclass, field 

29from pathlib import Path 

30from typing import Optional 

31 

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 

40 

41 

42# ── Constants ── 

43DEFAULT_PORT = 18900 

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

45 

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] 

132 

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] 

150 

151 

152# ── Server ── 

153 

154def 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 

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. 

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 

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)