mirror of
https://github.com/qingchencloud/clawpanel.git
synced 2026-06-28 03:01:54 +08:00
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
This commit is contained in:
@@ -321,18 +321,16 @@ export const api = {
|
||||
assistantWebSearch: (query, maxResults) => invoke('assistant_web_search', { query, max_results: maxResults || 5 }),
|
||||
assistantFetchUrl: (url) => invoke('assistant_fetch_url', { url }),
|
||||
|
||||
// Skills 管理(openclaw skills CLI)
|
||||
// Skills 管理
|
||||
skillsList: () => invoke('skills_list'),
|
||||
skillsInfo: (name) => invoke('skills_info', { name }),
|
||||
skillsCheck: () => invoke('skills_check'),
|
||||
skillsInstallDep: (kind, spec) => invoke('skills_install_dep', { kind, spec }),
|
||||
skillsSkillHubCheck: () => invoke('skills_skillhub_check'),
|
||||
skillsSkillHubSetup: (cliOnly = true) => invoke('skills_skillhub_setup', { cliOnly }),
|
||||
skillsSkillHubSearch: (query) => invoke('skills_skillhub_search', { query }),
|
||||
skillsSkillHubInstall: (slug) => invoke('skills_skillhub_install', { slug }),
|
||||
skillsClawHubSearch: (query) => invoke('skills_clawhub_search', { query }),
|
||||
skillsClawHubInstall: (slug) => invoke('skills_clawhub_install', { slug }),
|
||||
skillsUninstall: (name) => invoke('skills_uninstall', { name }),
|
||||
// SkillHub SDK(内置 HTTP,不依赖 CLI)
|
||||
skillhubSearch: (query, limit) => invoke('skillhub_search', { query, limit }),
|
||||
skillhubIndex: () => invoke('skillhub_index'),
|
||||
skillhubInstall: (slug) => invoke('skillhub_install', { slug }),
|
||||
|
||||
// 实例管理
|
||||
instanceList: () => cachedInvoke('instance_list', {}, 10000),
|
||||
|
||||
@@ -72,8 +72,8 @@ export default {
|
||||
toolProcessList: _('进程列表', 'Process list', '處理程序列表'),
|
||||
toolWebSearch: _('网页搜索', 'Web search', '網頁搜尋'),
|
||||
toolWebSearchDesc: _('搜索网页获取信息', 'Search the web for information', '搜尋網頁取得資訊'),
|
||||
toolClawHubSearch: _('搜索 ClawHub', 'Search ClawHub', '搜尋 ClawHub'),
|
||||
toolClawHubInstall: _('安装 Skill', 'Install Skill', '安裝 Skill'),
|
||||
toolSkillHubSearch: _('搜索 SkillHub', 'Search SkillHub', '搜尋 SkillHub'),
|
||||
toolSkillHubInstall: _('安装 Skill', 'Install Skill', '安裝 Skill'),
|
||||
toolInstallDep: _('安装依赖', 'Install dependency', '安裝依赖'),
|
||||
toolFileOps: _('文件操作', 'File Operations', '檔案操作'),
|
||||
toolFileOpsDesc: _('读写文件和目录', 'Read/write files and directories', '讀写檔案和目錄'),
|
||||
|
||||
@@ -46,35 +46,17 @@ export default {
|
||||
installFailed: _('安装失败', 'Install failed', '安裝失敗', 'インストール失敗', '설치 실패', 'Cài đặt thất bại', 'Error al instalar', 'Falha ao instalar', 'Ошибка установки', 'Échec de l\'installation', 'Installation fehlgeschlagen'),
|
||||
searchPlaceholder: _('搜索技能,如 weather / github / tavily', 'Search skills, e.g. weather / github / tavily', '搜尋技能,如 weather / github / tavily'),
|
||||
search: _('搜索', 'Search', '搜尋', 'Skills を検索...', 'Skills 검색...', 'Tìm kiếm Skills...', 'Buscar Skills...', 'Pesquisar Skills...', 'Поиск Skills...', 'Rechercher Skills...', 'Skills suchen...'),
|
||||
installCLI: _('安装 CLI', 'Install CLI', '安裝 CLI'),
|
||||
browse: _('浏览', 'Browse', '瀏覽'),
|
||||
searchEmpty: _('输入关键词搜索社区 Skills,然后一键安装', 'Enter keywords to search community Skills, then install with one click', '輸入關鍵詞搜尋社區 Skills,然後一鍵安裝'),
|
||||
searchKeyword: _('输入关键词搜索社区 Skills', 'Enter keywords to search community Skills', '輸入關鍵詞搜尋社區 Skills'),
|
||||
storeLoading: _('正在加载技能商店...', 'Loading skill store...', '正在載入技能商店...', 'スキルストアを読み込み中...', '스킬 스토어 로딩 중...'),
|
||||
storeLoadFailed: _('技能商店加载失败', 'Failed to load skill store', '技能商店載入失敗'),
|
||||
searching: _('正在搜索...', 'Searching...', '正在搜尋...', '検索中...', '검색 중...'),
|
||||
noResults: _('没有找到匹配的 Skill', 'No matching Skills found', '沒有找到匹配的 Skill', '一致するスキルなし', '일치하는 스킬 없음', 'Không có kết quả', 'Sin resultados', 'Sem resultados', 'Ничего не найдено', 'Aucun résultat', 'Keine Ergebnisse'),
|
||||
install: _('安装', 'Install', '安裝', 'インストール', '설치', 'Cài đặt', 'Instalar', 'Instalar', 'Установить', 'Installer', 'Installieren'),
|
||||
installed: _('已安装', 'Installed', '已安裝', 'インストール済み', '설치됨', 'Đã cài', 'Instalados', 'Instalados', 'Установленные', 'Installés', 'Installiert'),
|
||||
searchFailed: _('搜索失败', 'Search failed', '搜尋失敗'),
|
||||
rateLimited: _('⚠️ 请求频率超限', '⚠️ Rate limited', '⚠️ 請求頻率超限'),
|
||||
rateLimitClawHub: _('ClawHub 海外源限流,建议切换到 SkillHub(国内加速)', 'ClawHub rate limited, try switching to SkillHub (China accelerated)', 'ClawHub 海外源限流,建議切換到 SkillHub(國內加速)'),
|
||||
rateLimitRetry: _('请稍后再试', 'Please try again later', '請稍后再試'),
|
||||
skillhubNeedCLI: _('⚠️ 请先安装 SkillHub CLI', '⚠️ Please install SkillHub CLI first', '⚠️ 請先安裝 SkillHub CLI'),
|
||||
skillhubNeedCLIHint: _('点击上方「安装 CLI」按钮,或切换到 ClawHub 源搜索', 'Click "Install CLI" above, or switch to ClawHub source', '点擊上方「安裝 CLI」按鈕,或切換到 ClawHub 源搜尋'),
|
||||
skillhubSetup: _('一键安装 SkillHub CLI', 'Install SkillHub CLI', '一鍵安裝 SkillHub CLI'),
|
||||
skillhubInstalling: _('正在安装 SkillHub CLI...', 'Installing SkillHub CLI...', '正在安裝 SkillHub CLI...'),
|
||||
skillhubInstalled: _('SkillHub CLI 安装成功', 'SkillHub CLI installed', 'SkillHub CLI 安裝成功'),
|
||||
skillhubInstallFailed: _('SkillHub CLI 安装失败', 'SkillHub CLI installation failed', 'SkillHub CLI 安裝失敗'),
|
||||
confirmUninstall: _('确定卸载 Skill「{name}」?', 'Uninstall Skill "{name}"?', '確定卸載 Skill「{name}」?', 'スキル「{name}」をアンインストールしますか?', '스킬「{name}」을 제거하시겠습니까?'),
|
||||
uninstalling: _('卸载中...', 'Uninstalling...', '卸載中...', 'アンインストール中...', '제거 중...'),
|
||||
uninstalled: _('已卸载 {name}', 'Uninstalled {name}', '已卸載 {name}'),
|
||||
uninstallFailed: _('卸载失败', 'Uninstall failed', '卸載失敗', 'アンインストール失敗', '제거 실패'),
|
||||
skillInstalled: _('Skill {name} 安装成功', 'Skill {name} installed', 'Skill {name} 安裝成功'),
|
||||
sourceSkillHub: _('SkillHub(国内加速)', 'SkillHub (China accelerated)', 'SkillHub(國內加速)'),
|
||||
sourceClawHub: _('ClawHub(原版海外)', 'ClawHub (Original overseas)'),
|
||||
sourceLocalScanTimeout: _('CLI 可用,但本次调用超时,当前显示本地扫描结果', 'CLI available but timed out, showing local scan results', 'CLI 可用,但本次呼叫逾時,目前顯示本地掃描結果'),
|
||||
sourceLocalScanParseFailed: _('CLI 可用,但返回结果解析失败,当前显示本地扫描结果', 'CLI available but output parse failed, showing local scan results', 'CLI 可用,但返回結果解析失敗,目前顯示本地掃描結果'),
|
||||
sourceLocalScanExecFailed: _('CLI 调用失败,当前显示本地扫描结果', 'CLI execution failed, showing local scan results', 'CLI 呼叫失敗,目前顯示本地掃描結果'),
|
||||
sourceLocalScan: _('当前显示本地扫描结果', 'Showing local scan results', '目前顯示本地掃描結果'),
|
||||
sourceLocalScanNoCli: _('CLI 不可用,当前显示本地扫描结果', 'CLI not available, showing local scan results', 'CLI 不可用,目前顯示本地掃描結果'),
|
||||
sourceCLI: _('当前已使用 OpenClaw CLI 结果', 'Using OpenClaw CLI results', '目前已使用 OpenClaw CLI 結果'),
|
||||
}
|
||||
|
||||
@@ -153,7 +153,7 @@ ${personality}
|
||||
- openclaw skills info <name> — 查看某个 Skill 详情
|
||||
- openclaw skills check — 检查所有 Skills 的依赖是否满足
|
||||
- Skill 依赖安装: 根据 install spec 执行 brew/npm/go/uv 安装缺少的命令行工具
|
||||
- ClawHub (clawhub.com): 社区 Skill 市场,可搜索和安装新 Skill
|
||||
- SkillHub: 技能商店,可搜索和安装新 Skill(内置 HTTP,不依赖 CLI)
|
||||
- Skills 目录: 捆绑 Skills 在 openclaw 安装包内,自定义 Skills 通常位于 ~/.openclaw/skills/<name>/ 或 ~/.claude/skills/<name>/
|
||||
|
||||
### 聊天与调试
|
||||
@@ -432,8 +432,8 @@ const TOOL_DEFS = {
|
||||
{
|
||||
type: 'function',
|
||||
function: {
|
||||
name: 'skills_clawhub_search',
|
||||
description: '在 ClawHub 社区市场中搜索 Skills。返回匹配的 Skill 列表(slug 和描述)。',
|
||||
name: 'skillhub_search',
|
||||
description: '在 SkillHub 技能商店中搜索 Skills。返回匹配的 Skill 列表(slug 和描述)。',
|
||||
parameters: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
@@ -446,12 +446,12 @@ const TOOL_DEFS = {
|
||||
{
|
||||
type: 'function',
|
||||
function: {
|
||||
name: 'skills_clawhub_install',
|
||||
description: '从 ClawHub 社区市场安装一个 Skill 到本地自定义 Skills 目录(通常为 ~/.openclaw/skills/ 或 ~/.claude/skills/)。',
|
||||
name: 'skillhub_install',
|
||||
description: '从 SkillHub 技能商店安装一个 Skill 到本地自定义 Skills 目录(通常为 ~/.openclaw/skills/ 或 ~/.claude/skills/)。',
|
||||
parameters: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
slug: { type: 'string', description: 'ClawHub 上的 Skill slug(名称标识)' },
|
||||
slug: { type: 'string', description: 'SkillHub 上的 Skill slug(名称标识)' },
|
||||
},
|
||||
required: ['slug'],
|
||||
},
|
||||
@@ -507,7 +507,7 @@ const TOOL_DEFS = {
|
||||
|
||||
// 危险工具(需要用户确认)
|
||||
const INTERACTIVE_TOOLS = new Set(['ask_user']) // 交互式工具,不走 confirmToolCall
|
||||
const DANGEROUS_TOOLS = new Set(['run_command', 'write_file', 'skills_install_dep', 'skills_clawhub_install'])
|
||||
const DANGEROUS_TOOLS = new Set(['run_command', 'write_file', 'skills_install_dep', 'skillhub_install'])
|
||||
|
||||
// 安全围栏:极端危险命令模式(任何模式都必须确认,包括无限模式)
|
||||
const CRITICAL_PATTERNS = [
|
||||
@@ -710,7 +710,7 @@ const BUILTIN_SKILLS = [
|
||||
注意:
|
||||
- 安装依赖可能需要特定的包管理器(brew 仅限 macOS,Windows 用 npm/go 等)
|
||||
- 先调用 get_system_info 判断操作系统,过滤出适合当前平台的安装选项
|
||||
- 如果用户想从 ClawHub 搜索安装新 Skill,使用 skills_clawhub_search 和 skills_clawhub_install`,
|
||||
- 如果用户想从 SkillHub 搜索安装新 Skill,使用 skillhub_search 和 skillhub_install`,
|
||||
},
|
||||
]
|
||||
|
||||
@@ -742,7 +742,7 @@ function getEnabledTools() {
|
||||
|
||||
// Skills 管理工具:始终启用(规划模式下排除安装操作)
|
||||
if (mode.readOnly) {
|
||||
tools.push(...TOOL_DEFS.skills.filter(td => !['skills_install_dep', 'skills_clawhub_install'].includes(td.function.name)))
|
||||
tools.push(...TOOL_DEFS.skills.filter(td => !['skills_install_dep', 'skillhub_install'].includes(td.function.name)))
|
||||
} else {
|
||||
tools.push(...TOOL_DEFS.skills)
|
||||
}
|
||||
@@ -1880,14 +1880,14 @@ async function executeTool(name, args) {
|
||||
const result = await api.skillsInstallDep(args.kind, args.spec)
|
||||
return result?.success ? `${t('assistant.toolInstallSuccess')}\n${result.output || ''}` : t('assistant.toolInstallFail')
|
||||
}
|
||||
case 'skills_clawhub_search': {
|
||||
const items = await api.skillsClawHubSearch(args.query)
|
||||
case 'skillhub_search': {
|
||||
const items = await api.skillhubSearch(args.query)
|
||||
if (!items?.length) return t('assistant.toolNoSkillFound')
|
||||
return items.map(i => `- **${i.slug}**: ${i.description || t('assistant.toolNoDesc')}`).join('\n')
|
||||
return items.map(i => `- **${i.slug}**: ${i.description || i.summary || t('assistant.toolNoDesc')}`).join('\n')
|
||||
}
|
||||
case 'skills_clawhub_install': {
|
||||
const result = await api.skillsClawHubInstall(args.slug)
|
||||
return result?.success ? `Skill "${args.slug}" ${t('assistant.toolInstallSuccess')}\n${result.output || ''}` : t('assistant.toolInstallFail')
|
||||
case 'skillhub_install': {
|
||||
const result = await api.skillhubInstall(args.slug)
|
||||
return result?.success ? `Skill "${args.slug}" ${t('assistant.toolInstallSuccess')}\n${result.path || ''}` : t('assistant.toolInstallFail')
|
||||
}
|
||||
default:
|
||||
return `${t('assistant.toolUnknown')}: ${name}`
|
||||
@@ -2275,8 +2275,8 @@ function renderToolBlocks(toolHistory) {
|
||||
// ask_user 工具不显示在工具块中(它有自己的交互卡片)
|
||||
if (tc.name === 'ask_user') return ''
|
||||
|
||||
const tcIcon = { run_command: icon('terminal', 14), write_file: icon('edit', 14), read_file: icon('file', 14), list_directory: icon('folder', 14), get_system_info: icon('monitor', 14), list_processes: icon('list', 14), check_port: icon('plug', 14), skills_list: icon('box', 14), skills_info: icon('box', 14), skills_check: icon('box', 14), skills_install_dep: icon('download', 14), skills_clawhub_search: icon('search', 14), skills_clawhub_install: icon('download', 14) }[tc.name] || icon('wrench', 14)
|
||||
const label = { run_command: t('assistant.toolRunCmd'), read_file: t('assistant.toolReadFile'), write_file: t('assistant.toolWriteFile'), list_directory: t('assistant.toolListDir'), get_system_info: t('assistant.toolSysInfo'), list_processes: t('assistant.toolProcessList'), check_port: t('assistant.toolCheckPort'), skills_list: t('assistant.toolSkillsList'), skills_info: t('assistant.toolSkillInfo'), skills_check: t('assistant.toolSkillsCheck'), skills_install_dep: t('assistant.toolInstallDep'), skills_clawhub_search: t('assistant.toolClawHubSearch'), skills_clawhub_install: t('assistant.toolClawHubInstall') }[tc.name] || tc.name
|
||||
const tcIcon = { run_command: icon('terminal', 14), write_file: icon('edit', 14), read_file: icon('file', 14), list_directory: icon('folder', 14), get_system_info: icon('monitor', 14), list_processes: icon('list', 14), check_port: icon('plug', 14), skills_list: icon('box', 14), skills_info: icon('box', 14), skills_check: icon('box', 14), skills_install_dep: icon('download', 14), skillhub_search: icon('search', 14), skillhub_install: icon('download', 14) }[tc.name] || icon('wrench', 14)
|
||||
const label = { run_command: t('assistant.toolRunCmd'), read_file: t('assistant.toolReadFile'), write_file: t('assistant.toolWriteFile'), list_directory: t('assistant.toolListDir'), get_system_info: t('assistant.toolSysInfo'), list_processes: t('assistant.toolProcessList'), check_port: t('assistant.toolCheckPort'), skills_list: t('assistant.toolSkillsList'), skills_info: t('assistant.toolSkillInfo'), skills_check: t('assistant.toolSkillsCheck'), skills_install_dep: t('assistant.toolInstallDep'), skillhub_search: t('assistant.toolSkillHubSearch'), skillhub_install: t('assistant.toolSkillHubInstall') }[tc.name] || tc.name
|
||||
const argsStr = tc.name === 'run_command' ? escHtml(tc.args.command || '')
|
||||
: tc.name === 'read_file' ? escHtml(tc.args.path || '')
|
||||
: tc.name === 'write_file' ? escHtml(tc.args.path || '')
|
||||
@@ -2286,8 +2286,8 @@ function renderToolBlocks(toolHistory) {
|
||||
: tc.name === 'check_port' ? escHtml(String(tc.args.port || ''))
|
||||
: tc.name === 'skills_info' ? escHtml(tc.args.name || '')
|
||||
: tc.name === 'skills_install_dep' ? escHtml(`${tc.args.kind}: ${tc.args.spec?.formula || tc.args.spec?.package || tc.args.spec?.module || ''}`)
|
||||
: tc.name === 'skills_clawhub_search' ? escHtml(tc.args.query || '')
|
||||
: tc.name === 'skills_clawhub_install' ? escHtml(tc.args.slug || '')
|
||||
: tc.name === 'skillhub_search' ? escHtml(tc.args.query || '')
|
||||
: tc.name === 'skillhub_install' ? escHtml(tc.args.slug || '')
|
||||
: ['skills_list', 'skills_check'].includes(tc.name) ? ''
|
||||
: escHtml(JSON.stringify(tc.args))
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
/**
|
||||
* Skills 页面
|
||||
* 基于 openclaw skills CLI,按状态分组展示所有 Skills
|
||||
* 本地扫描已安装 Skills + SkillHub SDK 技能商店
|
||||
*/
|
||||
import { api } from '../lib/tauri-api.js'
|
||||
import { toast } from '../components/toast.js'
|
||||
@@ -30,20 +30,12 @@ export async function render() {
|
||||
</div>
|
||||
<div id="skills-tab-store" class="config-section" style="display:none">
|
||||
<div class="clawhub-toolbar" style="margin-bottom:var(--space-sm)">
|
||||
<select class="form-input" id="install-source-select" style="width:auto;min-width:160px">
|
||||
<option value="skillhub">${t('skills.sourceSkillHub')}</option>
|
||||
<option value="clawhub">${t('skills.sourceClawHub')}</option>
|
||||
</select>
|
||||
<input class="input clawhub-search-input" id="skill-install-search" placeholder="${t('skills.searchPlaceholder')}" type="text" style="flex:1">
|
||||
<button class="btn btn-primary btn-sm" data-action="install-source-search">${t('skills.search')}</button>
|
||||
<button class="btn btn-secondary btn-sm" data-action="skillhub-setup" id="btn-skillhub-setup" style="display:none">${t('skills.installCLI')}</button>
|
||||
<a class="btn btn-secondary btn-sm" id="btn-browse-source" href="https://skillhub.tencent.com" target="_blank" rel="noopener">${t('skills.browse')}</a>
|
||||
<input class="input clawhub-search-input" id="skill-store-search" placeholder="${t('skills.searchPlaceholder')}" type="text" style="flex:1">
|
||||
<button class="btn btn-primary btn-sm" data-action="store-search">${t('skills.search')}</button>
|
||||
<a class="btn btn-secondary btn-sm" href="https://skillhub.tencent.com" target="_blank" rel="noopener">${t('skills.browse')}</a>
|
||||
</div>
|
||||
<div class="form-hint" id="store-hint" style="margin-bottom:var(--space-sm);display:flex;align-items:center;gap:var(--space-xs)">
|
||||
<span id="skillhub-status"></span>
|
||||
</div>
|
||||
<div id="install-source-results" class="clawhub-list" style="max-height:calc(100vh - 320px);overflow-y:auto">
|
||||
<div class="clawhub-empty" style="padding:var(--space-xl);text-align:center">${t('skills.searchEmpty')}</div>
|
||||
<div id="store-results" class="clawhub-list" style="max-height:calc(100vh - 300px);overflow-y:auto">
|
||||
<div class="form-hint" style="padding:var(--space-xl);text-align:center">${t('skills.storeLoading')}</div>
|
||||
</div>
|
||||
</div>
|
||||
`
|
||||
@@ -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 = `
|
||||
<div class="clawhub-toolbar">
|
||||
<input class="input clawhub-search-input" id="skill-filter-input" placeholder="${t('skills.filterPlaceholder')}" type="text">
|
||||
<button class="btn btn-secondary btn-sm" data-action="skill-retry">${t('skills.refresh')}</button>
|
||||
<a class="btn btn-secondary btn-sm" href="https://clawhub.ai/skills" target="_blank" rel="noopener">ClawHub</a>
|
||||
${sourceHint ? `<span class="form-hint" style="margin-left:auto;color:${source === 'local-scan' ? 'var(--warning)' : 'var(--text-tertiary)'}">${esc(sourceHint)}</span>` : ''}
|
||||
</div>
|
||||
|
||||
<div class="skills-summary" style="margin-bottom:var(--space-lg);color:var(--text-secondary);font-size:var(--font-size-sm)">
|
||||
@@ -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 = `<div class="clawhub-empty">${t('skills.searchKeyword')}</div>`; 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 = `<div style="padding:var(--space-lg);text-align:center">
|
||||
<div style="color:var(--warning);margin-bottom:8px">${t('skills.skillhubNeedCLI')}</div>
|
||||
<div class="form-hint" style="margin-bottom:12px">${t('skills.skillhubNeedCLIHint')}</div>
|
||||
<button class="btn btn-primary btn-sm" data-action="skillhub-setup">${t('skills.skillhubSetup')}</button>
|
||||
</div>`
|
||||
return
|
||||
}
|
||||
results.innerHTML = `<div class="form-hint">${t('skills.searching')}</div>`
|
||||
async function loadStore(page) {
|
||||
const results = page.querySelector('#store-results')
|
||||
if (!results) return
|
||||
results.innerHTML = `<div class="form-hint" style="padding:var(--space-xl);text-align:center">${t('skills.storeLoading')}</div>`
|
||||
try {
|
||||
const items = source === 'skillhub' ? await api.skillsSkillHubSearch(q) : await api.skillsClawHubSearch(q)
|
||||
if (!items?.length) { results.innerHTML = `<div class="clawhub-empty">${t('skills.noResults')}</div>`; return }
|
||||
const installAction = source === 'skillhub' ? 'source-install-skillhub' : 'source-install-clawhub'
|
||||
results.innerHTML = items.map(item => `
|
||||
<div class="clawhub-item">
|
||||
<div class="clawhub-item-main">
|
||||
<div class="clawhub-item-title">${esc(item.slug || item.name || '')}</div>
|
||||
<div class="clawhub-item-desc">${esc(item.description || item.summary || '')}</div>
|
||||
</div>
|
||||
<div class="clawhub-item-actions">
|
||||
<button class="btn btn-primary btn-sm" data-action="${installAction}" data-slug="${esc(item.slug || item.name || '')}">${t('skills.install')}</button>
|
||||
</div>
|
||||
</div>
|
||||
`).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 = `<div style="padding:var(--space-lg);text-align:center">
|
||||
<div style="color:var(--warning);margin-bottom:8px">${t('skills.rateLimited')}</div>
|
||||
<div class="form-hint">${source === 'clawhub' ? t('skills.rateLimitClawHub') : t('skills.rateLimitRetry')}</div>
|
||||
</div>`
|
||||
} else {
|
||||
results.innerHTML = `<div style="color:var(--error);padding:var(--space-sm)">${t('skills.searchFailed')}: ${esc(errMsg)}</div>`
|
||||
}
|
||||
results.innerHTML = `<div style="color:var(--error);padding:var(--space-lg);text-align:center">${t('skills.storeLoadFailed')}: ${esc(e?.message || e)}</div>`
|
||||
}
|
||||
}
|
||||
|
||||
async function handleSourceInstall(page, btn, source) {
|
||||
function renderStoreItems(el, items) {
|
||||
if (!items?.length) {
|
||||
el.innerHTML = `<div class="clawhub-empty" style="padding:var(--space-xl);text-align:center">${t('skills.noResults')}</div>`
|
||||
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 `
|
||||
<div class="clawhub-item store-item" data-slug="${esc(slug)}" data-name="${esc(name)}" data-desc="${esc(desc)}">
|
||||
<div class="clawhub-item-main">
|
||||
<div class="clawhub-item-title">📦 ${esc(name)}</div>
|
||||
<div class="clawhub-item-desc">${esc(desc)}</div>
|
||||
${item.version ? `<div class="clawhub-item-meta">v${esc(item.version)}${item.author ? ` · ${esc(item.author)}` : ''}</div>` : ''}
|
||||
</div>
|
||||
<div class="clawhub-item-actions">
|
||||
${installed
|
||||
? `<span class="clawhub-badge installed">${t('skills.installed')}</span>`
|
||||
: `<button class="btn btn-primary btn-sm" data-action="store-install" data-slug="${esc(slug)}">${t('skills.install')}</button>`
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
`
|
||||
}).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 = `<div class="form-hint" style="padding:var(--space-sm)">${t('skills.searching')}</div>`
|
||||
try {
|
||||
const items = await api.skillhubSearch(input.value.trim())
|
||||
renderStoreItems(results, items)
|
||||
} catch (e) {
|
||||
results.innerHTML = `<div style="color:var(--error);padding:var(--space-sm)">${t('skills.searchFailed')}: ${esc(e?.message || e)}</div>`
|
||||
}
|
||||
}
|
||||
|
||||
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 = `<span style="color:var(--success)">✅ v${info.version}</span>`
|
||||
if (setupBtn) setupBtn.style.display = 'none'
|
||||
} else {
|
||||
statusEl.innerHTML = `<span style="color:var(--warning)">${t('skills.skillhubNeedCLI')}</span>`
|
||||
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 = `<div class="clawhub-empty">${t('skills.searchKeyword')}</div>`
|
||||
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)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user