Coverage for agentos/desktop/skill_store_server.py: 0%

96 statements  

« prev     ^ index     » next       coverage.py v7.14.3, created at 2026-07-08 21:26 +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. 

5 

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 

11 

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 

16 

17Requirements: pip install fastapi uvicorn aiohttp 

18""" 

19 

20from __future__ import annotations 

21 

22import sys 

23import webbrowser 

24from pathlib import Path 

25 

26try: 

27 import uvicorn 

28 from fastapi import FastAPI 

29 from fastapi.responses import FileResponse, HTMLResponse, JSONResponse 

30 from fastapi.staticfiles import StaticFiles 

31 

32 FASTAPI_AVAILABLE = True 

33except ImportError: 

34 FASTAPI_AVAILABLE = False 

35 

36 

37# ── Constants ── 

38DEFAULT_PORT = 18900 

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

40 

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] 

127 

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] 

197 

198 

199# ── Server ── 

200 

201 

202def create_app() -> FastAPI: 

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

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

205 

206 # ── API Routes ── 

207 

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

209 async def list_sources(): 

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

211 return JSONResponse(SKILL_SOURCES) 

212 

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 ) 

242 

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) 

251 

252 from agentos.marketplace.importer import OpenClawImporter 

253 from agentos.marketplace.registry import SkillRegistry 

254 

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

256 registry = SkillRegistry(install_dir=str(install_dir)) 

257 

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 ) 

281 

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 ) 

298 

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) 

306 

307 from agentos.marketplace.importer import OpenClawImporter 

308 from agentos.marketplace.registry import SkillRegistry 

309 

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

311 registry = SkillRegistry(install_dir=str(install_dir)) 

312 

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 ) 

324 

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) 

330 

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

332 async def health(): 

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

334 

335 # ── Static Files ── 

336 if STATIC_DIR.exists(): 

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

338 

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) 

346 

347 return app 

348 

349 

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>""" 

399 

400 

401# ── Entry Point ── 

402 

403 

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. 

410 

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 

419 

420 app = create_app() 

421 

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

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

424 

425 if open_browser: 

426 webbrowser.open(url) 

427 

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

429 

430 

431if __name__ == "__main__": 

432 import argparse 

433 

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)