diff --git a/pkg/nodes/registry_server.go b/pkg/nodes/registry_server.go index b00a97b..d01eb70 100644 --- a/pkg/nodes/registry_server.go +++ b/pkg/nodes/registry_server.go @@ -795,7 +795,9 @@ func (s *RegistryServer) handleWebUISkills(w http.ResponseWriter, r *http.Reques seenDirs := map[string]struct{}{} seenSkills := map[string]struct{}{} items := make([]skillItem, 0) - checkUpdates := strings.TrimSpace(r.URL.Query().Get("check_updates")) != "0" + // Default off to avoid hammering clawhub search API on each UI refresh. + // Enable explicitly with ?check_updates=1 when needed. + checkUpdates := strings.TrimSpace(r.URL.Query().Get("check_updates")) == "1" for _, dir := range candDirs { dir = strings.TrimSpace(dir) @@ -918,7 +920,13 @@ func (s *RegistryServer) handleWebUISkills(w http.ResponseWriter, r *http.Reques cmd.Dir = strings.TrimSpace(s.workspacePath) out, err := cmd.CombinedOutput() if err != nil { - http.Error(w, fmt.Sprintf("install failed: %v\n%s", err, string(out)), http.StatusInternalServerError) + outText := string(out) + lower := strings.ToLower(outText) + if strings.Contains(lower, "rate limit exceeded") || strings.Contains(lower, "too many requests") { + http.Error(w, fmt.Sprintf("clawhub rate limit exceeded. please retry later or configure auth token.\n%s", outText), http.StatusTooManyRequests) + return + } + http.Error(w, fmt.Sprintf("install failed: %v\n%s", err, outText), http.StatusInternalServerError) return } _ = json.NewEncoder(w).Encode(map[string]interface{}{"ok": true, "installed": name, "output": string(out)}) diff --git a/webui/src/i18n/index.ts b/webui/src/i18n/index.ts index cfbb6f9..c87434a 100644 --- a/webui/src/i18n/index.ts +++ b/webui/src/i18n/index.ts @@ -175,6 +175,9 @@ const resources = { skillsInstallFailedMessage: 'Failed to install clawhub', skillsInstallDoneTitle: 'Install complete', skillsInstallDoneMessage: 'clawhub is installed. You can continue installing skills.', + skillsInstallingSkill: 'Installing skill...', + skillsInstallSkillDoneTitle: 'Skill installed', + skillsInstallSkillDoneMessage: 'Skill "{{name}}" installed successfully.', skillsAddTitle: 'Add Skill', skillsAddMessage: 'Upload skill archive (.zip / .tar.gz / .tgz / .tar). It will be extracted into skills folder and archive will be removed.', skillsSelectArchive: 'Select archive', @@ -474,6 +477,9 @@ const resources = { skillsInstallFailedMessage: '安装 clawhub 失败', skillsInstallDoneTitle: '安装完成', skillsInstallDoneMessage: 'clawhub 已安装,可继续安装技能。', + skillsInstallingSkill: '正在安装技能...', + skillsInstallSkillDoneTitle: '技能安装完成', + skillsInstallSkillDoneMessage: '技能 "{{name}}" 安装成功。', skillsAddTitle: '添加技能', skillsAddMessage: '请上传技能压缩包(.zip / .tar.gz / .tgz / .tar)。上传后将自动解压到 skills 目录,并删除上传压缩包。', skillsSelectArchive: '选择压缩包', diff --git a/webui/src/pages/Skills.tsx b/webui/src/pages/Skills.tsx index 7db5aa6..82fbb3c 100644 --- a/webui/src/pages/Skills.tsx +++ b/webui/src/pages/Skills.tsx @@ -10,6 +10,7 @@ const Skills: React.FC = () => { const { skills, refreshSkills, q, clawhubInstalled, clawhubPath } = useAppContext(); const ui = useUI(); const [installName, setInstallName] = useState(''); + const [installingSkill, setInstallingSkill] = useState(false); const qp = (k: string, v: string) => `${q}${q ? '&' : '?'}${k}=${encodeURIComponent(v)}`; const [isFileModalOpen, setIsFileModalOpen] = useState(false); @@ -47,35 +48,49 @@ const Skills: React.FC = () => { }); const text = await r.text(); if (!r.ok) { + ui.hideLoading(); await ui.notify({ title: t('skillsInstallFailedTitle'), message: text || t('skillsInstallFailedMessage') }); return false; } + ui.hideLoading(); await ui.notify({ title: t('skillsInstallDoneTitle'), message: t('skillsInstallDoneMessage') }); await refreshSkills(); return true; } finally { + // loading is explicitly closed before notify, keep this as fallback. ui.hideLoading(); } } async function installSkill() { + if (installingSkill) return; const name = installName.trim(); if (!name) return; - const ready = await installClawHubIfNeeded(); - if (!ready) return; + setInstallingSkill(true); + ui.showLoading(t('skillsInstallingSkill')); + try { + const ready = await installClawHubIfNeeded(); + if (!ready) return; - const r = await fetch(`/webui/api/skills${q}`, { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ action: 'install', name }), - }); - if (!r.ok) { - await ui.notify({ title: t('requestFailed'), message: await r.text() }); - return; + const r = await fetch(`/webui/api/skills${q}`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ action: 'install', name }), + }); + if (!r.ok) { + ui.hideLoading(); + await ui.notify({ title: t('requestFailed'), message: await r.text() }); + return; + } + setInstallName(''); + await refreshSkills(); + ui.hideLoading(); + await ui.notify({ title: t('skillsInstallSkillDoneTitle'), message: t('skillsInstallSkillDoneMessage', { name }) }); + } finally { + ui.hideLoading(); + setInstallingSkill(false); } - setInstallName(''); - await refreshSkills(); } async function onAddSkillClick() { @@ -173,8 +188,8 @@ const Skills: React.FC = () => {