/** * Hermes lazy_deps 依赖管理(P1-3) * * 列出 Hermes 内核 LAZY_DEPS allowlist 的所有 feature(platform.* / tts.* / stt.* / * search.* / provider.* / memory.* / image.*),显示安装状态,提供「装」按钮。 * * 解决「用户配好渠道首次启动 Gateway 卡 30 秒后崩」的常见 bug —— * 让用户能在「启动 Gateway 之前」主动预装。 */ import { t } from '../../../lib/i18n.js' import { api } from '../../../lib/tauri-api.js' import { toast } from '../../../components/toast.js' import { humanizeError } from '../../../lib/humanize-error.js' import { svgIcon } from '../lib/svg-icons.js' // feature 分类配置(决定分组顺序 + 图标 + 文案) const CATEGORIES = [ { prefix: 'platform.', icon: 'message-square', titleKey: 'hermesLazyDeps.catPlatform' }, { prefix: 'tts.', icon: 'volume', titleKey: 'hermesLazyDeps.catTts' }, { prefix: 'stt.', icon: 'mic', titleKey: 'hermesLazyDeps.catStt' }, { prefix: 'search.', icon: 'search', titleKey: 'hermesLazyDeps.catSearch' }, { prefix: 'provider.', icon: 'shield', titleKey: 'hermesLazyDeps.catProvider' }, { prefix: 'memory.', icon: 'inbox', titleKey: 'hermesLazyDeps.catMemory' }, { prefix: 'image.', icon: 'image', titleKey: 'hermesLazyDeps.catImage' }, ] const DESC_OVERRIDE_KEY = 'hermesLazyDeps.descOverride' // i18n.key 下的 feature → 描述 // 把 feature 按分类分组 function groupByCategory(features) { const groups = CATEGORIES.map(c => ({ ...c, items: [] })) const other = { prefix: '', icon: 'file', titleKey: 'hermesLazyDeps.catOther', items: [] } for (const f of features) { const cat = groups.find(g => f.feature.startsWith(g.prefix)) if (cat) cat.items.push(f) else other.items.push(f) } return [...groups.filter(g => g.items.length > 0), ...(other.items.length ? [other] : [])] } export async function render() { const page = document.createElement('div') page.className = 'page' page.dataset.engine = 'hermes' page.innerHTML = `
${t('common.loading')}…
` loadAndRender(page) page.querySelector('#btn-refresh').onclick = () => loadAndRender(page) return page } async function loadAndRender(page) { const content = page.querySelector('#lazy-deps-content') content.innerHTML = `
${t('common.loading')}…
` let featuresResp try { featuresResp = await api.hermesLazyDepsFeatures() } catch (e) { content.innerHTML = `
${escapeHtml(humanizeError(e, t('hermesLazyDeps.loadFailed')))}
` return } if (!featuresResp?.ok) { content.innerHTML = `
${escapeHtml(t('hermesLazyDeps.loadFailed'))}: ${escapeHtml(featuresResp?.error || 'unknown')}
` return } const features = featuresResp.features || [] if (!features.length) { content.innerHTML = `
${svgIcon('inbox', { size: 32 })}
${escapeHtml(t('hermesLazyDeps.emptyTitle'))}
` return } // 批量查状态 let status = {} try { const statusResp = await api.hermesLazyDepsStatus(features.map(f => f.feature)) status = statusResp?.ok ? (statusResp.status || {}) : {} } catch (e) { // 状态查询失败也允许渲染(按未知处理) console.warn('lazy_deps status failed:', e) } const groups = groupByCategory(features) content.innerHTML = groups.map(g => renderGroup(g, status)).join('') // 绑定每个 feature 的「装」按钮 content.querySelectorAll('button[data-feature]').forEach(btn => { btn.onclick = () => onEnsureClick(page, btn.dataset.feature, btn) }) } function renderGroup(group, status) { const items = group.items.map(f => renderItem(f, status[f.feature])).join('') return `
${svgIcon(group.icon, { size: 18 })} ${escapeHtml(t(group.titleKey))}
${items}
` } function renderItem(f, st) { const satisfied = st && st.satisfied const known = st ? st.known : true const missing = st?.missing || [] const specsTitle = (f.specs || []).join('\n') const featureLabel = featureDisplayName(f.feature) const stateBadge = satisfied ? `${svgIcon('check', { size: 11 })} ${escapeHtml(t('hermesLazyDeps.installed'))}` : (known ? `${escapeHtml(t('hermesLazyDeps.notInstalled'))}` : `?`) const installBtn = satisfied ? `` : `` const missingHint = !satisfied && missing.length ? `
${escapeHtml(t('hermesLazyDeps.missingCount', { n: missing.length }))}
` : '' return `
${escapeHtml(featureLabel)}
${stateBadge}
${escapeHtml((f.specs || []).join(', '))}
${missingHint}
${installBtn}
` } // 映射 feature → 友好显示名(兼容 i18n 缺词时 fallback 到原名) function featureDisplayName(feature) { const friendly = t('hermesLazyDeps.featureName.' + feature) // i18n 没翻译时 t() 返回 key 本身,做 fallback if (friendly && !friendly.endsWith('.' + feature)) return friendly return feature } async function onEnsureClick(page, feature, btn) { const origText = btn.textContent btn.disabled = true btn.textContent = t('hermesLazyDeps.installing') + '…' try { const resp = await api.hermesLazyDepsEnsure(feature) if (resp?.ok) { const installed = resp.installed || [] if (resp.alreadySatisfied) { toast(t('hermesLazyDeps.alreadyInstalled', { feature }), 'success') } else { toast({ message: t('hermesLazyDeps.installSuccess', { feature }), hint: installed.length ? t('hermesLazyDeps.installedSpecs', { specs: installed.join(', ') }) : '', }, 'success') } } else { toast(humanizeError(resp?.error || 'unknown', t('hermesLazyDeps.installFailed', { feature })), 'error') } } catch (e) { toast(humanizeError(e, t('hermesLazyDeps.installFailed', { feature })), 'error') } finally { btn.disabled = false btn.textContent = origText // 装完刷新整张页面状态 setTimeout(() => loadAndRender(page), 600) } } function escapeHtml(s) { return String(s ?? '').replace(/&/g, '&').replace(//g, '>').replace(/"/g, '"') } function escapeAttr(s) { return escapeHtml(s) }