/**
* 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 = `
`
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) }