/** * 插件中心 — OpenClaw 扩展插件管理与浏览 */ import { api } from '../lib/tauri-api.js' import { toast } from '../components/toast.js' import { navigate } from '../router.js' import { t } from '../lib/i18n.js' import { openAIDrawerWithError } from '../components/ai-drawer.js' const PLUGIN_ICONS = { qqbot: '🐧', feishu: '🪶', dingtalk: '📌', telegram: '✈️', discord: '🎮', slack: '💬', weixin: '💚', wechat: '💚', webchat: '🌐', whatsapp: '📱', signal: '🔒', line: '🟢', teams: '👥', matrix: '🔗', irc: '📡', } let _allPlugins = [] let _searchQuery = '' function esc(s) { return String(s || '').replace(/&/g, '&').replace(/"/g, '"').replace(/

${t('extensions.title')}

${t('extensions.subtitle')}

` page.querySelector('#ph-refresh').onclick = () => loadPlugins(page) page.querySelector('#ph-go-channels').onclick = () => navigate('/channels') page.querySelector('#ph-install-btn').onclick = () => handleInstall(page) page.querySelector('#ph-pkg-input').onkeydown = (e) => { if (e.key === 'Enter') handleInstall(page) } page.querySelector('#ph-search').oninput = (e) => { _searchQuery = e.target.value.trim().toLowerCase() renderPluginList(page) } // Event delegation for toggle buttons page.addEventListener('click', async (e) => { const btn = e.target.closest('[data-toggle-plugin]') if (!btn) return const pluginId = btn.dataset.togglePlugin const newEnabled = btn.dataset.toggleTo === 'true' btn.disabled = true btn.textContent = '...' try { await api.togglePlugin(pluginId, newEnabled) toast(t('extensions.toggleSuccess'), 'success') await loadPlugins(page) } catch (err) { toast(`${t('extensions.toggleFailed')}: ${err}`, 'error') btn.disabled = false btn.textContent = newEnabled ? t('extensions.enable') : t('extensions.disable') } }) // Expand/collapse install messages page.addEventListener('click', (e) => { if (e.target.closest('#ph-install-msg-toggle')) { const detail = page.querySelector('#ph-install-msg-detail') const toggle = page.querySelector('#ph-install-msg-toggle') if (detail && toggle) { const expanded = detail.style.display !== 'none' detail.style.display = expanded ? 'none' : 'block' toggle.textContent = expanded ? t('extensions.showDetail') : t('extensions.hideDetail') } } }) setTimeout(() => loadPlugins(page), 0) return page } async function handleInstall(page) { const input = page.querySelector('#ph-pkg-input') const btn = page.querySelector('#ph-install-btn') const msgEl = page.querySelector('#ph-install-msg') const pkg = input.value.trim() if (!pkg) return btn.disabled = true btn.textContent = t('extensions.installing') msgEl.style.display = 'block' msgEl.innerHTML = `
${t('extensions.installing')}
` try { const result = await api.installPlugin(pkg) const output = result.output ? esc(result.output).substring(0, 120) : '' msgEl.innerHTML = `
✅ ${t('extensions.installSuccess')}${output ? ' — ' + output : ''}
` toast(t('extensions.installSuccess'), 'success') input.value = '' await loadPlugins(page) setTimeout(() => { msgEl.style.display = 'none' }, 5000) } catch (e) { const errStr = String(e.message || e) const short = errStr.length > 100 ? errStr.substring(0, 100) + '...' : errStr const hasDetail = errStr.length > 100 msgEl.innerHTML = `
❌ ${t('extensions.installFailed')}: ${esc(short)} ${hasDetail ? `` : ''}
${hasDetail ? `` : ''}
` toast(t('extensions.installFailed'), 'error') openAIDrawerWithError({ scene: 'plugin-install', title: t('extensions.installFailed') + ': ' + pkg, hint: t('extensions.installPlaceholder'), error: errStr, }) } finally { btn.disabled = false btn.textContent = t('extensions.installBtn') } } async function loadPlugins(page) { const listEl = page.querySelector('#ph-list') const statsEl = page.querySelector('#ph-stats') listEl.innerHTML = `
${t('extensions.loading')}
` try { const result = await api.listAllPlugins() _allPlugins = result?.plugins || [] if (_allPlugins.length === 0) { statsEl.innerHTML = '' listEl.innerHTML = `
${t('extensions.noPlugins')}
` return } const enabled = _allPlugins.filter(p => p.enabled).length const builtin = _allPlugins.filter(p => p.builtin).length statsEl.innerHTML = `
${_allPlugins.length}${t('extensions.statsInstalled')}
${enabled}${t('extensions.statsEnabled')}
${builtin ? `
${builtin}${t('extensions.statsBuiltin')}
` : ''} ` renderPluginList(page) } catch (e) { listEl.innerHTML = `
${esc(e.message || e)}
` } } function renderPluginList(page) { const listEl = page.querySelector('#ph-list') if (!listEl) return const filtered = _searchQuery ? _allPlugins.filter(p => { const q = _searchQuery return (p.id || '').toLowerCase().includes(q) || (p.description || '').toLowerCase().includes(q) || (p.version || '').toLowerCase().includes(q) }) : _allPlugins if (filtered.length === 0 && _searchQuery) { listEl.innerHTML = `
${t('extensions.noSearchResults', { query: esc(_searchQuery) })}
` return } listEl.innerHTML = `
${filtered.map(p => renderPluginCard(p)).join('')}
${t('extensions.restartHint')}
` } function renderPluginCard(p) { const icon = PLUGIN_ICONS[p.id.toLowerCase()] || '🧩' const statusClass = p.enabled ? 'plugin-status-enabled' : (p.installed ? 'plugin-status-disabled' : 'plugin-status-missing') const statusText = p.enabled ? t('extensions.enabled') : (p.installed ? t('extensions.disabled') : t('extensions.notInstalled')) const badges = [] if (p.builtin) badges.push(`${t('extensions.builtin')}`) if (p.version) badges.push(`${t('extensions.version')} ${esc(p.version)}`) // Toggle button: installed plugins can be enabled/disabled let toggleBtn = '' if (p.installed) { if (p.enabled) { toggleBtn = `` } else { toggleBtn = `` } } return `
${icon}
${esc(p.id)}
${badges.join('')}
${esc(p.description) || t('extensions.noDescription')}
` }