/**
* 插件中心 — 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 ? `
${esc(errStr)}` : ''}
`
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 `
${esc(p.description) || t('extensions.noDescription')}
`
}