From ad00ffef3d7327dbda1e2566132887e469751bec Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=99=B4=E5=A4=A9?= Date: Tue, 7 Apr 2026 03:25:26 +0800 Subject: [PATCH] chore: release v0.11.5 feat: SkillHub skill store (SDK-based, no CLI dependency) - Rust SDK (skillhub.rs): HTTP search, index fetch, zip download+extract - Node.js SDK (skillhub-sdk.js): mirrors Rust SDK for Web/Docker mode - Skills page: new "Store" tab with full index browse + client-side filter - Remove 6 old CLI-dependent commands, add 3 SDK commands - Migrate assistant.js skill tools from ClawHub CLI to SkillHub SDK - Fix index decode error ({total,skills} wrapper vs bare array) - Fix skill name display (API field 'name' vs 'display_name') - Clean up 13 dead CSS rules from old skills hero/tips UI --- CHANGELOG.md | 17 + docs/dev/skills-refactor-plan.md | 559 +++++++++++++++++++++++++++++ docs/index.html | 18 +- package-lock.json | 4 +- package.json | 2 +- scripts/dev-api.js | 135 ++----- scripts/lib/skillhub-sdk.js | 216 +++++++++++ src-tauri/Cargo.lock | 2 +- src-tauri/Cargo.toml | 2 +- src-tauri/src/commands/mod.rs | 1 + src-tauri/src/commands/skillhub.rs | 260 ++++++++++++++ src-tauri/src/commands/skills.rs | 448 ++--------------------- src-tauri/src/lib.rs | 12 +- src-tauri/tauri.conf.json | 2 +- src/lib/tauri-api.js | 12 +- src/locales/modules/assistant.js | 4 +- src/locales/modules/skills.js | 22 +- src/pages/assistant.js | 38 +- src/pages/skills.js | 244 +++++-------- src/style/pages.css | 97 +---- 20 files changed, 1244 insertions(+), 851 deletions(-) create mode 100644 docs/dev/skills-refactor-plan.md create mode 100644 scripts/lib/skillhub-sdk.js create mode 100644 src-tauri/src/commands/skillhub.rs diff --git a/CHANGELOG.md b/CHANGELOG.md index fbcdb45..7e640ba 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,23 @@ 格式遵循 [Keep a Changelog](https://keepachangelog.com/zh-CN/1.1.0/), 版本号遵循 [语义化版本](https://semver.org/lang/zh-CN/)。 +## [0.11.5] - 2026-04-07 + +### 新功能 (Features) + +- **SkillHub 技能商店(SDK 化)** — Skills 页面新增"搜索安装"商店 tab,支持浏览全量索引、客户端实时过滤、一键安装;底层从 CLI 调用全面迁移到内置 HTTP SDK(Rust + Node.js 双端),不再依赖 OpenClaw CLI 即可搜索和安装社区 Skill + +### 改进 (Improvements) + +- **Skills 命令层精简** — 移除 6 个旧 CLI 依赖命令,新增 3 个 SkillHub SDK 命令(search / index / install),本地扫描命令改为纯文件系统操作 +- **AI 助手工具迁移** — 晴辰助手的 Skill 搜索/安装工具从 ClawHub CLI 调用迁移到 SkillHub SDK,工具定义、系统提示、handler、显示标签全部更新 +- **前端 CSS 清理** — 移除 13 条不再使用的旧 Skills 页面样式(hero 展示区、tips 区域) + +### 修复 (Fixes) + +- **技能索引加载失败** — 修复 SkillHub 索引 API 返回 `{total, skills}` 包装对象导致 JSON 解码失败的问题(Rust 端新增 `IndexResponse` 包装结构体,Node.js 端提取 `.skills` 字段) +- **技能名称显示** — 修复商店列表因 API 字段名不匹配(`name` vs `display_name`)导致只显示 slug 的问题 + ## [0.11.4] - 2026-04-06 ### 新功能 (Features) diff --git a/docs/dev/skills-refactor-plan.md b/docs/dev/skills-refactor-plan.md new file mode 100644 index 0000000..091c10b --- /dev/null +++ b/docs/dev/skills-refactor-plan.md @@ -0,0 +1,559 @@ +# Skills 页面重构规划 + +> 目标:**完全去掉 CLI 依赖**,用 Rust `reqwest` 内置 SkillHub API 调用 + zip 解压,用户无需安装任何额外工具即可搜索/安装/管理 Skills。 + +--- + +## 一、现状分析 + +### 现有架构(问题) + +``` +┌─────────────┐ ┌───────────────────┐ ┌──────────────┐ +│ skills.js │ ───→ │ skills.rs / dev- │ ───→ │ 外部 CLI 进程 │ +│ (前端 UI) │ │ api.js (后端) │ │ │ +└─────────────┘ └───────────────────┘ └──────┬───────┘ + │ + ┌────────────┼────────────┐ + ▼ ▼ ▼ + openclaw CLI skillhub CLI clawhub CLI + (skills list) (search/install) (npx) +``` + +**痛点清单:** + +| # | 问题 | 影响 | +|---|------|------| +| 1 | **必须安装 OpenClaw CLI** 才能查看已安装 Skills 列表 | 新用户看到空白页或报错 | +| 2 | **必须安装 SkillHub CLI** 才能从国内源搜索/安装 | 额外安装步骤,需要 npm | +| 3 | **ClawHub 用 `npx -y clawhub`** 每次冷启动 ~10s | 体验差,且海外源限流 | +| 4 | CLI 输出是人类可读文本,需要**正则解析** | 脆弱,CLI 版本更新就可能坏 | +| 5 | 前端有两个安装源下拉 + CLI 检测状态 UI | 复杂且令人困惑 | +| 6 | `skills_list` 依赖 CLI,超时/失败才 fallback 本地扫描 | 不可靠,延迟高 | + +### 可保留的部分 + +| 模块 | 保留? | 说明 | +|------|--------|------| +| `scan_local_skills()` / 本地扫描逻辑 | ✅ 保留 | 扫描 `~/.openclaw/skills/` 等目录,不依赖 CLI | +| `scan_single_skill()` | ✅ 保留 | 解析 SKILL.md frontmatter + package.json | +| `skills_uninstall()` | ✅ 保留 | 简单的 `rm -rf`,无 CLI 依赖 | +| `skills_validate()` | ✅ 保留 | 纯本地文件检查 | +| `skills_install_dep()` | ✅ 保留 | brew/npm/go/uv 本地包管理器 | +| 前端已安装 Tab 的分组渲染 | ✅ 保留 | eligible/missing/disabled/blocked 分组 | +| 前端过滤搜索 | ✅ 保留 | 实时 filter | + +--- + +## 二、SkillHub API 协议 + +从 CLI 源码逆向得到的接口(腾讯云 COS + API 后端): + +| 功能 | URL | 返回 | +|------|-----|------| +| **搜索** | `GET https://lightmake.site/api/v1/search?q={query}&limit={limit}` | `{ results: [{ slug, displayName, summary, version }] }` | +| **主下载** | `GET https://lightmake.site/api/v1/download?slug={slug}` | zip 二进制 | +| **COS 镜像下载** | `GET https://skillhub-1388575217.cos.ap-guangzhou.myqcloud.com/skills/{slug}.zip` | zip 二进制(国内加速) | +| **全量索引** | `GET https://skillhub-1388575217.cos.ap-guangzhou.myqcloud.com/skills.json` | JSON 数组 | + +### 安装流程(内置化) + +``` +搜索 → 选择 Skill → 下载 zip → 解压到 ~/.openclaw/skills/{slug}/ → 完成 +``` + +不需要任何 CLI 工具。 + +--- + +## 三、新架构设计(SDK 模式 + 双平台) + +关键设计:**抽出独立 SDK 层**,Tauri 桌面端和 Web/Docker 端各实现一份,上层 API 接口一致,后续调整只改 SDK 即可。 + +``` +┌──────────────────┐ +│ skills.js (前端) │ 统一 UI,不关心后端是 Rust 还是 Node +└────────┬─────────┘ + │ invoke / fetch + ▼ +┌──────────────────────────────────────────────────────────┐ +│ Tauri 命令层 / dev-api 路由层 │ +│ skills.rs (Tauri) dev-api.js (Web/Docker) │ +│ ↓ 调用 ↓ 调用 │ +│ skillhub.rs (Rust SDK) skillhub-sdk.js (Node SDK) │ +└──────────────────────────────────────────────────────────┘ + │ │ + ▼ ▼ +┌──────────────────────────────────────────────────────────┐ +│ SkillHub API + COS │ +│ 搜索: lightmake.site/api/v1/search │ +│ 索引: cos.ap-guangzhou.myqcloud.com/skills.json │ +│ 下载: cos.ap-guangzhou.myqcloud.com/skills/{slug}.zip │ +│ 回退: lightmake.site/api/v1/download │ +└──────────────────────────────────────────────────────────┘ +``` + +### 为什么用 SDK 模式? + +| 优势 | 说明 | +|------|------| +| **解耦** | SDK 只管 HTTP + zip,不涉及 Tauri/Express 框架 | +| **双平台** | Rust SDK 给 Tauri 桌面端用,Node SDK 给 Web/Docker 端用 | +| **易调整** | API 域名/路径/认证变了,只改 SDK 一个文件 | +| **可测试** | SDK 函数可单独写单元测试 | +| **可复用** | 未来其他模块要调 SkillHub 也直接引 SDK | + +--- + +## 四、Rust SDK(`src-tauri/src/commands/skillhub.rs`) + +独立模块,不依赖 Tauri 框架,纯 `reqwest` + `zip` + `serde`。 + +### 4.1 数据结构 + +```rust +use serde::{Deserialize, Serialize}; + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct SkillHubItem { + pub slug: String, + #[serde(alias = "displayName")] + pub display_name: Option, + pub summary: Option, + pub version: Option, +} + +#[derive(Debug, Deserialize)] +struct SearchResponse { + results: Vec, +} +``` + +### 4.2 SDK 公开接口 + +```rust +/// 搜索 SkillHub +pub async fn search(query: &str, limit: u32) -> Result, String> + +/// 拉取全量索引(带 10 分钟内存缓存) +pub async fn fetch_index() -> Result, String> + +/// 下载并安装 Skill(zip → 解压到 target_dir) +pub async fn install(slug: &str, skills_dir: &Path) -> Result + +/// 仅下载 zip 字节(COS 优先,回退主站) +pub async fn download_zip(slug: &str) -> Result, String> +``` + +### 4.3 下载策略 + +```rust +const COS_BASE: &str = "https://skillhub-1388575217.cos.ap-guangzhou.myqcloud.com"; +const API_BASE: &str = "https://lightmake.site/api/v1"; + +pub async fn download_zip(slug: &str) -> Result, String> { + // 1. 优先 COS 镜像(国内 CDN,毫秒级) + let cos_url = format!("{}/skills/{}.zip", COS_BASE, slug); + if let Ok(resp) = client().get(&cos_url).send().await { + if resp.status().is_success() { + return resp.bytes().await + .map(|b| b.to_vec()) + .map_err(|e| format!("COS 下载失败: {e}")); + } + } + // 2. 回退主站 API + let api_url = format!("{}/download?slug={}", API_BASE, slug); + let resp = client().get(&api_url).send().await + .map_err(|e| format!("主站下载失败: {e}"))?; + if !resp.status().is_success() { + return Err(format!("下载失败: HTTP {}", resp.status())); + } + resp.bytes().await + .map(|b| b.to_vec()) + .map_err(|e| format!("读取下载内容失败: {e}")) +} +``` + +### 4.4 全量索引缓存 + +```rust +use std::sync::Mutex; +use std::time::{Duration, Instant}; +use once_cell::sync::Lazy; + +static INDEX_CACHE: Lazy)>>> = + Lazy::new(|| Mutex::new(None)); + +pub async fn fetch_index() -> Result, String> { + // 命中缓存(10 分钟有效) + if let Ok(guard) = INDEX_CACHE.lock() { + if let Some((ts, ref items)) = *guard { + if ts.elapsed() < Duration::from_secs(600) { + return Ok(items.clone()); + } + } + } + // 拉取远程索引 + let url = format!("{}/skills.json", COS_BASE); + let items: Vec = client().get(&url).send().await + .map_err(|e| format!("拉取索引失败: {e}"))? + .json().await + .map_err(|e| format!("解析索引失败: {e}"))?; + // 写入缓存 + if let Ok(mut guard) = INDEX_CACHE.lock() { + *guard = Some((Instant::now(), items.clone())); + } + Ok(items) +} +``` + +### 4.5 zip 解压 + +```rust +pub fn extract_zip(zip_bytes: &[u8], target_dir: &Path) -> Result<(), String> { + use std::io::Cursor; + use zip::ZipArchive; + + if target_dir.exists() { + std::fs::remove_dir_all(target_dir) + .map_err(|e| format!("清理旧目录失败: {e}"))?; + } + std::fs::create_dir_all(target_dir) + .map_err(|e| format!("创建目录失败: {e}"))?; + + let reader = Cursor::new(zip_bytes); + let mut archive = ZipArchive::new(reader) + .map_err(|e| format!("打开 zip 失败: {e}"))?; + + for i in 0..archive.len() { + let mut file = archive.by_index(i) + .map_err(|e| format!("读取 zip 条目失败: {e}"))?; + let name = file.name().to_string(); + // 安全检查:防止路径穿越 + if name.contains("..") { continue; } + + let out_path = target_dir.join(&name); + if file.is_dir() { + std::fs::create_dir_all(&out_path).ok(); + } else { + if let Some(parent) = out_path.parent() { + std::fs::create_dir_all(parent).ok(); + } + let mut outfile = std::fs::File::create(&out_path) + .map_err(|e| format!("创建文件失败 {name}: {e}"))?; + std::io::copy(&mut file, &mut outfile) + .map_err(|e| format!("写入文件失败 {name}: {e}"))?; + } + } + Ok(()) +} +``` + +--- + +## 五、Node.js SDK(`scripts/lib/skillhub-sdk.js`) + +给 Web/Docker 端用,API 接口与 Rust SDK **完全对齐**。 + +### 5.1 模块结构 + +```javascript +// scripts/lib/skillhub-sdk.js +const COS_BASE = 'https://skillhub-1388575217.cos.ap-guangzhou.myqcloud.com' +const API_BASE = 'https://lightmake.site/api/v1' + +let _indexCache = null // { ts: Date.now(), items: [] } +const INDEX_TTL = 10 * 60 * 1000 // 10 分钟 + +module.exports = { search, fetchIndex, install, downloadZip } +``` + +### 5.2 SDK 公开接口 + +```javascript +/** + * 搜索 SkillHub + * @param {string} query + * @param {number} [limit=20] + * @returns {Promise>} + */ +async function search(query, limit = 20) { + const url = `${API_BASE}/search?q=${encodeURIComponent(query)}&limit=${limit}` + const resp = await fetch(url) + if (!resp.ok) throw new Error(`搜索失败: HTTP ${resp.status}`) + const data = await resp.json() + return data.results || [] +} + +/** + * 拉取全量索引(带缓存) + * @returns {Promise>} + */ +async function fetchIndex() { + if (_indexCache && Date.now() - _indexCache.ts < INDEX_TTL) { + return _indexCache.items + } + const resp = await fetch(`${COS_BASE}/skills.json`) + if (!resp.ok) throw new Error(`拉取索引失败: HTTP ${resp.status}`) + const items = await resp.json() + _indexCache = { ts: Date.now(), items } + return items +} + +/** + * 下载 zip(COS 优先,回退主站) + * @param {string} slug + * @returns {Promise} + */ +async function downloadZip(slug) { + // COS 优先 + try { + const resp = await fetch(`${COS_BASE}/skills/${slug}.zip`) + if (resp.ok) return Buffer.from(await resp.arrayBuffer()) + } catch {} + // 回退主站 + const resp = await fetch(`${API_BASE}/download?slug=${encodeURIComponent(slug)}`) + if (!resp.ok) throw new Error(`下载失败: HTTP ${resp.status}`) + return Buffer.from(await resp.arrayBuffer()) +} + +/** + * 下载并安装 Skill + * @param {string} slug + * @param {string} skillsDir - ~/.openclaw/skills/ + * @returns {Promise} 安装路径 + */ +async function install(slug, skillsDir) { + const zipBuf = await downloadZip(slug) + const targetDir = path.join(skillsDir, slug) + // 清理旧目录 + if (fs.existsSync(targetDir)) fs.rmSync(targetDir, { recursive: true, force: true }) + fs.mkdirSync(targetDir, { recursive: true }) + // 解压(用 Node.js 内置或 adm-zip) + const AdmZip = require('adm-zip') + const zip = new AdmZip(zipBuf) + zip.extractAllTo(targetDir, true) + return targetDir +} +``` + +### 5.3 dev-api.js 集成 + +```javascript +const skillhub = require('./lib/skillhub-sdk') + +// 路由处理器中直接调用 SDK +handlers = { + skillhub_search({ query, limit }) { return skillhub.search(query, limit) }, + skillhub_index() { return skillhub.fetchIndex() }, + skillhub_install({ slug }) { return skillhub.install(slug, SKILLS_DIR) }, + // skills_list → 纯本地扫描(已有 scanLocalSkillsFallback) + skills_list() { return scanLocalSkillsFallback() }, +} +``` + +--- + +## 六、命令层改造 + +### 6.1 skills.rs — Tauri 命令层(薄包装) + +改造后 `skills.rs` 只是 SDK 的 Tauri 命令薄包装: + +```rust +mod skillhub; // SDK 模块 + +#[tauri::command] +pub async fn skillhub_search(query: String, limit: Option) -> Result { + let items = skillhub::search(&query, limit.unwrap_or(20)).await?; + Ok(serde_json::to_value(items).unwrap()) +} + +#[tauri::command] +pub async fn skillhub_index() -> Result { + let items = skillhub::fetch_index().await?; + Ok(serde_json::to_value(items).unwrap()) +} + +#[tauri::command] +pub async fn skillhub_install(slug: String) -> Result { + let skills_dir = super::openclaw_dir().join("skills"); + let path = skillhub::install(&slug, &skills_dir).await?; + Ok(serde_json::json!({ "success": true, "slug": slug, "path": path.to_string_lossy() })) +} + +// skills_list → 纯本地扫描(复用已有 scan_local_skills) +#[tauri::command] +pub async fn skills_list() -> Result { + scan_local_skills(None) // 不再调 CLI +} +``` + +### 6.2 可删除的命令 + +| 命令 | 原因 | +|------|------| +| `skills_skillhub_check` | 不再需要检测 CLI | +| `skills_skillhub_setup` | 不再需要安装 CLI | +| `skills_skillhub_search` | 替换为 `skillhub_search`(SDK) | +| `skills_skillhub_install` | 替换为 `skillhub_install`(SDK) | +| `skills_clawhub_search` | 合并到 SkillHub | +| `skills_clawhub_install` | 合并到 SkillHub | + +### 6.3 保留的命令 + +| 命令 | 说明 | +|------|------| +| `skills_list` | 改为纯本地扫描 | +| `skills_info` | 改为纯本地文件解析 | +| `skills_uninstall` | 不变(删目录) | +| `skills_validate` | 不变(本地文件检查) | +| `skills_install_dep` | 不变(brew/npm/go/uv) | + +--- + +## 七、前端改造(`skills.js`) + +### 7.1 UI 简化 + +**删除:** +- 安装源下拉(` - - - - - - - ${t('skills.browse')} + + + ${t('skills.browse')} -
- -
-
-
${t('skills.searchEmpty')}
+
+
${t('skills.storeLoading')}
` @@ -87,22 +79,11 @@ function renderSkills(el, data) { const blocked = skills.filter(s => s.blockedByAllowlist && !s.disabled) const summary = t('skills.summaryDetail', { eligible: eligible.length, missing: missing.length, disabled: disabled.length }) - let sourceHint = '' - if (source === 'local-scan') { - if (cliDiag?.status === 'timeout') sourceHint = t('skills.sourceLocalScanTimeout') - else if (cliDiag?.status === 'parse-failed') sourceHint = t('skills.sourceLocalScanParseFailed') - else if (cliDiag?.status === 'exec-failed') sourceHint = t('skills.sourceLocalScanExecFailed') - else sourceHint = cliAvailable ? t('skills.sourceLocalScan') : t('skills.sourceLocalScanNoCli') - } else if (cliAvailable) { - sourceHint = t('skills.sourceCLI') - } el.innerHTML = `
- ClawHub - ${sourceHint ? `${esc(sourceHint)}` : ''}
@@ -279,76 +260,98 @@ async function handleInstallDep(page, btn) { } } -// ===== 统一源搜索/安装系统 ===== -let _installSource = 'skillhub' // 当前选中的安装源 -let _skillhubInstalled = false // SkillHub CLI 是否已安装 +// ===== 技能商店(SkillHub SDK)===== +let _storeIndex = null // 缓存的全量索引 +let _installedNames = new Set() // 已安装的 skill 名称 -function getInstallSource() { return _installSource } - -async function handleSourceSearch(page) { - const input = page.querySelector('#skill-install-search') - const results = page.querySelector('#install-source-results') - if (!input || !results) return - const q = input.value.trim() - if (!q) { results.innerHTML = `
${t('skills.searchKeyword')}
`; return } - const source = getInstallSource() - // SkillHub 未安装时友好提示(先实时检测一次,避免竞态误判) - if (source === 'skillhub' && !_skillhubInstalled) { - try { - const info = await api.skillsSkillHubCheck() - _skillhubInstalled = !!info.installed - } catch { /* ignore */ } - } - if (source === 'skillhub' && !_skillhubInstalled) { - results.innerHTML = `
-
${t('skills.skillhubNeedCLI')}
-
${t('skills.skillhubNeedCLIHint')}
- -
` - return - } - results.innerHTML = `
${t('skills.searching')}
` +async function loadStore(page) { + const results = page.querySelector('#store-results') + if (!results) return + results.innerHTML = `
${t('skills.storeLoading')}
` try { - const items = source === 'skillhub' ? await api.skillsSkillHubSearch(q) : await api.skillsClawHubSearch(q) - if (!items?.length) { results.innerHTML = `
${t('skills.noResults')}
`; return } - const installAction = source === 'skillhub' ? 'source-install-skillhub' : 'source-install-clawhub' - results.innerHTML = items.map(item => ` -
-
-
${esc(item.slug || item.name || '')}
-
${esc(item.description || item.summary || '')}
-
-
- -
-
- `).join('') + _storeIndex = await api.skillhubIndex() + // 获取已安装列表用于标记 + try { + const data = await api.skillsList() + _installedNames = new Set((data?.skills || []).map(s => s.name)) + } catch { _installedNames = new Set() } + renderStoreItems(results, _storeIndex) } catch (e) { - const errMsg = String(e?.message || e) - const isRateLimit = /rate.?limit|429|too many/i.test(errMsg) - if (isRateLimit) { - results.innerHTML = `
-
${t('skills.rateLimited')}
-
${source === 'clawhub' ? t('skills.rateLimitClawHub') : t('skills.rateLimitRetry')}
-
` - } else { - results.innerHTML = `
${t('skills.searchFailed')}: ${esc(errMsg)}
` - } + results.innerHTML = `
${t('skills.storeLoadFailed')}: ${esc(e?.message || e)}
` } } -async function handleSourceInstall(page, btn, source) { +function renderStoreItems(el, items) { + if (!items?.length) { + el.innerHTML = `
${t('skills.noResults')}
` + return + } + el.innerHTML = items.map(item => { + const slug = item.slug || '' + const name = item.display_name || item.displayName || item.name || slug + const desc = item.summary || item.description || '' + const installed = _installedNames.has(slug) + return ` +
+
+
📦 ${esc(name)}
+
${esc(desc)}
+ ${item.version ? `
v${esc(item.version)}${item.author ? ` · ${esc(item.author)}` : ''}
` : ''} +
+
+ ${installed + ? `${t('skills.installed')}` + : `` + } +
+
+ ` + }).join('') +} + +async function handleStoreSearch(page) { + const input = page.querySelector('#skill-store-search') + const results = page.querySelector('#store-results') + if (!input || !results) return + const q = input.value.trim().toLowerCase() + if (!q && _storeIndex) { + renderStoreItems(results, _storeIndex) + return + } + if (!q) return + // 客户端过滤已有索引 + if (_storeIndex) { + const filtered = _storeIndex.filter(item => { + const slug = (item.slug || '').toLowerCase() + const name = (item.display_name || item.displayName || '').toLowerCase() + const desc = (item.summary || item.description || '').toLowerCase() + const tags = (item.tags || []).join(' ').toLowerCase() + return slug.includes(q) || name.includes(q) || desc.includes(q) || tags.includes(q) + }) + renderStoreItems(results, filtered) + return + } + // 没有索引时走服务端搜索 + results.innerHTML = `
${t('skills.searching')}
` + try { + const items = await api.skillhubSearch(input.value.trim()) + renderStoreItems(results, items) + } catch (e) { + results.innerHTML = `
${t('skills.searchFailed')}: ${esc(e?.message || e)}
` + } +} + +async function handleStoreInstall(page, btn) { const slug = btn.dataset.slug btn.disabled = true btn.textContent = t('skills.installing') try { - if (source === 'skillhub') await api.skillsSkillHubInstall(slug) - else await api.skillsClawHubInstall(slug) + await api.skillhubInstall(slug) toast(t('skills.skillInstalled', { name: slug }), 'success') btn.textContent = t('skills.installed') btn.classList.remove('btn-primary') btn.classList.add('btn-secondary') - // 后台刷新已安装列表(不阻塞 UI) + _installedNames.add(slug) loadSkills(page).catch(() => {}) } catch (e) { toast(`${t('skills.installFailed')}: ${e?.message || e}`, 'error') @@ -374,57 +377,6 @@ async function handleSkillUninstall(page, btn) { } } -async function handleSkillHubSetup(page) { - const statusEl = page.querySelector('#skillhub-status') - if (statusEl) statusEl.textContent = t('skills.skillhubInstalling') - try { - await api.skillsSkillHubSetup(true) - _skillhubInstalled = true - toast(t('skills.skillhubInstalled'), 'success') - if (statusEl) statusEl.textContent = '✅' - // 隐藏安装按钮 - const setupBtn = page.querySelector('#btn-skillhub-setup') - if (setupBtn) setupBtn.style.display = 'none' - } catch (e) { - toast(`${t('skills.skillhubInstallFailed')}: ${e?.message || e}`, 'error') - if (statusEl) statusEl.textContent = '❌' - } -} - -async function checkSkillHubStatus(page) { - const statusEl = page.querySelector('#skillhub-status') - const setupBtn = page.querySelector('#btn-skillhub-setup') - if (!statusEl) return - try { - const info = await api.skillsSkillHubCheck() - _skillhubInstalled = !!info.installed - if (info.installed) { - statusEl.innerHTML = `✅ v${info.version}` - if (setupBtn) setupBtn.style.display = 'none' - } else { - statusEl.innerHTML = `${t('skills.skillhubNeedCLI')}` - if (setupBtn && _installSource === 'skillhub') setupBtn.style.display = '' - } - } catch { - statusEl.textContent = '' - } -} - -function switchInstallSource(page, source) { - _installSource = source - const results = page.querySelector('#install-source-results') - const setupBtn = page.querySelector('#btn-skillhub-setup') - const browseBtn = page.querySelector('#btn-browse-source') - if (results) results.innerHTML = `
${t('skills.searchKeyword')}
` - if (source === 'skillhub') { - if (browseBtn) browseBtn.href = 'https://skillhub.tencent.com' - checkSkillHubStatus(page) - } else { - if (setupBtn) setupBtn.style.display = 'none' - if (browseBtn) browseBtn.href = 'https://clawhub.ai/skills' - } -} - function bindEvents(page) { // 主 Tab 切换(已安装 / 搜索安装) page.querySelectorAll('#skills-main-tabs .tab').forEach(tab => { @@ -434,17 +386,11 @@ function bindEvents(page) { const key = tab.dataset.mainTab page.querySelector('#skills-tab-installed').style.display = key === 'installed' ? '' : 'none' page.querySelector('#skills-tab-store').style.display = key === 'store' ? '' : 'none' - // 切到商店 tab 时检测 SkillHub 状态 - if (key === 'store') checkSkillHubStatus(page) + // 切到商店 tab 时加载全量索引 + if (key === 'store') loadStore(page) } }) - // 安装源下拉切换 - const sourceSelect = page.querySelector('#install-source-select') - if (sourceSelect) { - sourceSelect.onchange = () => switchInstallSource(page, sourceSelect.value) - } - page.addEventListener('click', async (e) => { const btn = e.target.closest('[data-action]') if (!btn) return @@ -458,17 +404,11 @@ function bindEvents(page) { case 'skill-install-dep': await handleInstallDep(page, btn) break - case 'install-source-search': - await handleSourceSearch(page) + case 'store-search': + await handleStoreSearch(page) break - case 'source-install-skillhub': - await handleSourceInstall(page, btn, 'skillhub') - break - case 'source-install-clawhub': - await handleSourceInstall(page, btn, 'clawhub') - break - case 'skillhub-setup': - await handleSkillHubSetup(page) + case 'store-install': + await handleStoreInstall(page, btn) break case 'skill-uninstall': await handleSkillUninstall(page, btn) @@ -484,9 +424,9 @@ function bindEvents(page) { }) page.addEventListener('keydown', async (e) => { - if (e.key === 'Enter' && e.target?.id === 'skill-install-search') { + if (e.key === 'Enter' && e.target?.id === 'skill-store-search') { e.preventDefault() - await handleSourceSearch(page) + await handleStoreSearch(page) } }) } diff --git a/src/style/pages.css b/src/style/pages.css index b7c1b82..c5dc858 100644 --- a/src/style/pages.css +++ b/src/style/pages.css @@ -493,100 +493,6 @@ font-size: var(--font-size-sm); } -.skills-hero-panel { - position: relative; - overflow: hidden; -} - -.skills-hero-grid { - display: grid; - grid-template-columns: repeat(2, minmax(0, 1fr)); - gap: var(--space-md); -} - -.skill-hero-card { - position: relative; - border: 1px solid var(--border-primary); - border-radius: var(--radius-lg); - padding: var(--space-lg); - background: - radial-gradient(circle at top right, rgba(99, 102, 241, 0.12), transparent 32%), - linear-gradient(180deg, rgba(255,255,255,0.02), transparent), - var(--bg-card); - box-shadow: 0 12px 30px rgba(0,0,0,0.12); -} - -.skill-hero-top { - display: flex; - justify-content: space-between; - align-items: flex-start; - gap: var(--space-md); - margin-bottom: var(--space-md); -} - -.skill-hero-title { - font-size: var(--font-size-lg); - font-weight: 700; - margin-bottom: 4px; -} - -.skill-hero-meta { - font-size: var(--font-size-xs); - color: var(--text-tertiary); -} - -.skill-hero-badges { - display: flex; - gap: var(--space-sm); - flex-wrap: wrap; - justify-content: flex-end; -} - -.clawhub-badge.hot { - background: rgba(99, 102, 241, 0.14); - color: #6366f1; -} - -.skill-hero-desc { - color: var(--text-secondary); - line-height: 1.6; - min-height: 48px; -} - -.skill-hero-actions { - margin-top: var(--space-lg); - display: flex; - align-items: center; - justify-content: space-between; - gap: var(--space-md); - flex-wrap: wrap; -} - -.skill-hero-installed { - font-size: var(--font-size-sm); - color: var(--text-tertiary); -} - -.skills-tips-panel { - background: linear-gradient(180deg, rgba(99, 102, 241, 0.06), transparent), var(--bg-secondary); -} - -.skills-tip-list { - display: flex; - flex-direction: column; - gap: var(--space-md); -} - -.skills-tip-item { - color: var(--text-secondary); - line-height: 1.7; - padding: var(--space-sm) 0; - border-bottom: 1px dashed var(--border-secondary); -} - -.skills-tip-item:last-child { - border-bottom: none; -} .skills-loading-panel, .skills-load-error { @@ -597,8 +503,7 @@ } @media (max-width: 900px) { - .clawhub-grid, - .skills-hero-grid { + .clawhub-grid { grid-template-columns: 1fr; }