/** * 通信设置页面 — 消息、广播、命令、音频等 openclaw.json 配置的可视化编辑器 * 对应上游 Dashboard 的「通信」+「自动化」合并页 */ import { api } from '../lib/tauri-api.js' import { toast } from '../components/toast.js' import { icon } from '../lib/icons.js' import { t } from '../lib/i18n.js' import { wsClient } from '../lib/ws-client.js' let _page = null, _config = null, _dirty = false export async function render() { const page = document.createElement('div') page.className = 'page' _page = page page.innerHTML = `
` // Tab 切换 page.querySelectorAll('.comm-tab').forEach(tab => { tab.onclick = () => { page.querySelectorAll('.comm-tab').forEach(t => { t.classList.remove('active', 'btn-primary'); t.classList.add('btn-secondary') }) tab.classList.remove('btn-secondary'); tab.classList.add('active', 'btn-primary') renderTab(page, tab.dataset.tab) } }) // 保存按钮 page.querySelector('#btn-comm-save').onclick = saveConfig await loadConfig(page) return page } export function cleanup() { _page = null; _config = null; _dirty = false } async function loadConfig(page) { try { _config = await api.readOpenclawConfig() if (!_config) _config = {} renderTab(page, 'messages') } catch (e) { page.querySelector('#comm-content').innerHTML = `
${t('communication.loadFailed')}: ${esc(e?.message || e)}
` } } function markDirty() { _dirty = true const btn = _page?.querySelector('#btn-comm-save') if (btn) btn.disabled = false } async function saveConfig() { if (!_config || !_dirty) return const btn = _page?.querySelector('#btn-comm-save') if (btn) { btn.disabled = true; btn.textContent = t('communication.saving') } try { // 从当前表单收集值到 _config collectCurrentTab() await api.writeOpenclawConfig(_config) _dirty = false toast(t('communication.configSaved'), 'info') try { await api.reloadGateway(); toast(t('communication.gwReloaded'), 'success') } catch {} } catch (e) { toast(t('communication.saveFailed') + ': ' + e, 'error') } finally { if (btn) { btn.disabled = !_dirty; btn.innerHTML = `${icon('save', 14)} ${t('communication.save')}` } } } function collectCurrentTab() { if (!_page) return const activeTab = _page.querySelector('.comm-tab.active')?.dataset.tab if (activeTab === 'messages') collectMessages() else if (activeTab === 'broadcast') collectBroadcast() else if (activeTab === 'commands') collectCommands() else if (activeTab === 'hooks') collectHooks() else if (activeTab === 'approvals') collectApprovals() } // ── Tab 渲染 ── function renderTab(page, tab) { const el = page.querySelector('#comm-content') if (tab === 'messages') renderMessages(el) else if (tab === 'broadcast') renderBroadcast(el) else if (tab === 'commands') renderCommands(el) else if (tab === 'hooks') renderHooks(el) else if (tab === 'approvals') renderApprovals(el) } // ── 消息设置 ── function renderMessages(el) { const m = _config?.messages || {} const sr = m.statusReactions || {} el.innerHTML = `
${t('communication.replySettings')}
${t('communication.replyPrefixHint')}
${t('communication.ackReactionHint')}
${t('communication.removeAckAfterReplyHint')}
${t('communication.suppressToolErrorsHint')}
${t('communication.statusReactions')}
${t('communication.enableStatusReactionsHint')}
${t('communication.messageQueue')}
${t('communication.debounceMsHint')}
${t('communication.queueCapHint')}
${t('communication.groupChat')}
${t('communication.groupHistoryLimitHint')}
` el.querySelectorAll('input, select').forEach(inp => { inp.addEventListener('change', markDirty) inp.addEventListener('input', markDirty) }) } function collectMessages() { if (!_config) return const g = (id) => _page?.querySelector('#' + id) const v = (id) => g(id)?.value?.trim() || undefined const n = (id) => { const x = parseInt(g(id)?.value); return isNaN(x) ? undefined : x } const c = (id) => g(id)?.checked || false if (!_config.messages) _config.messages = {} const m = _config.messages m.responsePrefix = v('msg-responsePrefix') m.ackReaction = v('msg-ackReaction') m.ackReactionScope = v('msg-ackReactionScope') || undefined m.removeAckAfterReply = c('msg-removeAckAfterReply') || undefined m.suppressToolErrors = c('msg-suppressToolErrors') || undefined if (!m.statusReactions) m.statusReactions = {} m.statusReactions.enabled = c('msg-sr-enabled') || undefined const debounceMs = n('msg-debounceMs') if (debounceMs != null) { if (!m.inbound) m.inbound = {} m.inbound.debounceMs = debounceMs } const cap = n('msg-queueCap') if (cap != null) { if (!m.queue) m.queue = {} m.queue.cap = cap } const groupHistoryLimit = n('msg-groupHistoryLimit') if (groupHistoryLimit != null) { if (!m.groupChat) m.groupChat = {} m.groupChat.historyLimit = groupHistoryLimit } } // ── 广播设置 ── function renderBroadcast(el) { const b = _config?.broadcast || {} el.innerHTML = `
${t('communication.broadcastStrategy')}
${t('communication.broadcastHint')}
` el.querySelectorAll('input, select').forEach(inp => { inp.addEventListener('change', markDirty) }) } function collectBroadcast() { if (!_config) return const strategy = _page?.querySelector('#bc-strategy')?.value if (strategy) { if (!_config.broadcast) _config.broadcast = {} _config.broadcast.strategy = strategy } } // ── 命令配置 ── function renderCommands(el) { const cmd = _config?.commands || {} el.innerHTML = `
${t('communication.slashCommands')}
${toggleRow('cmd-text', t('communication.cmdText'), t('communication.cmdTextHint'), cmd.text !== false)} ${toggleRow('cmd-bash', t('communication.cmdBash'), t('communication.cmdBashHint'), !!cmd.bash)} ${toggleRow('cmd-config', t('communication.cmdConfig'), t('communication.cmdConfigHint'), !!cmd.config)} ${toggleRow('cmd-debug', t('communication.cmdDebug'), t('communication.cmdDebugHint'), !!cmd.debug)} ${toggleRow('cmd-restart', t('communication.cmdRestart'), t('communication.cmdRestartHint'), cmd.restart !== false)}
${t('communication.nativeCommands')}
${t('communication.nativeHint')}
` el.querySelectorAll('input, select').forEach(inp => { inp.addEventListener('change', markDirty) }) } function collectCommands() { if (!_config) return const c = (id) => _page?.querySelector('#' + id)?.checked if (!_config.commands) _config.commands = {} const cmd = _config.commands cmd.text = c('cmd-text') === false ? false : undefined cmd.bash = c('cmd-bash') || undefined cmd.config = c('cmd-config') || undefined cmd.debug = c('cmd-debug') || undefined cmd.restart = c('cmd-restart') === false ? false : undefined const native = _page?.querySelector('#cmd-native')?.value cmd.native = native === 'true' ? true : native === 'false' ? false : 'auto' } // ── Webhook ── function renderHooks(el) { const h = _config?.hooks || {} el.innerHTML = `
${t('communication.webhookSettings')}
${toggleRow('hooks-enabled', t('communication.webhookEnabled'), t('communication.webhookEnabledHint'), !!h.enabled)}
${t('communication.webhookPathHint')}
${t('communication.webhookTokenHint')}
${t('communication.webhookSessionKeyHint')}
` el.querySelectorAll('input, select').forEach(inp => { inp.addEventListener('change', markDirty) inp.addEventListener('input', markDirty) }) } function collectHooks() { if (!_config) return const v = (id) => _page?.querySelector('#' + id)?.value?.trim() || undefined const n = (id) => { const x = parseInt(_page?.querySelector('#' + id)?.value); return isNaN(x) ? undefined : x } const c = (id) => _page?.querySelector('#' + id)?.checked if (!_config.hooks) _config.hooks = {} const h = _config.hooks h.enabled = c('hooks-enabled') || undefined h.path = v('hooks-path') h.token = v('hooks-token') h.defaultSessionKey = v('hooks-defaultSessionKey') h.maxBodyBytes = n('hooks-maxBodyBytes') } // ── 执行审批 ── function renderApprovals(el) { const a = _config?.approvals?.exec || {} el.innerHTML = `
${t('communication.approvalsTitle')}
${t('communication.approvalsDesc')}
${toggleRow('approvals-enabled', t('communication.approvalsEnabled'), t('communication.approvalsEnabledHint'), !!a.enabled)}
${toggleRow('approvals-forwardExec', t('communication.approvalsForwardExec'), t('communication.approvalsForwardExecHint'), !!a.enabled)}
${t('communication.pendingApprovals')}
${t('communication.approvalsLoadingQueue')}
` el.querySelectorAll('input, select').forEach(inp => { inp.addEventListener('change', markDirty) }) el.querySelector('#btn-refresh-approvals')?.addEventListener('click', () => loadApprovalQueue(el)) loadApprovalQueue(el) } async function loadApprovalQueue(el) { const container = (el || _page)?.querySelector('#approval-queue') if (!container) return if (!wsClient.connected || !wsClient.gatewayReady) { container.innerHTML = `
${esc(t('communication.approvalsGwNotReady'))}
` return } container.innerHTML = `
${esc(t('communication.approvalsLoadingQueue'))}
` let execItems = [], pluginItems = [], unsupported = false try { const [execRes, pluginRes] = await Promise.allSettled([ wsClient.execApprovalList(), wsClient.pluginApprovalList(), ]) if (execRes.status === 'fulfilled') execItems = execRes.value?.approvals || execRes.value?.items || [] else { const msg = String(execRes.reason?.message || '').toLowerCase() if (msg.includes('unknown method') || msg.includes('not found')) unsupported = true } if (pluginRes.status === 'fulfilled') pluginItems = pluginRes.value?.approvals || pluginRes.value?.items || [] } catch {} if (unsupported) { container.innerHTML = `
${esc(t('communication.approvalsUnsupported'))}
` return } const allItems = [ ...execItems.map(i => ({ ...i, _type: 'exec' })), ...pluginItems.map(i => ({ ...i, _type: 'plugin' })), ] if (!allItems.length) { container.innerHTML = `
${esc(t('communication.approvalsQueueEmpty'))}
` return } container.innerHTML = allItems.map(item => { const id = item.id || item.approvalId || '' const type = item._type === 'plugin' ? 'Plugin' : 'Exec' const cmd = item.command || item.description || item.name || id const status = item.status || 'pending' const ts = item.createdAt || item.timestamp || 0 const timeStr = ts ? new Date(typeof ts === 'number' && ts < 1e12 ? ts * 1000 : ts).toLocaleString() : '' return `
${esc(type)}${esc(cmd)}
${esc(status)}${timeStr ? ' · ' + timeStr : ''}
` }).join('') } function collectApprovals() { if (!_config) return const c = (id) => _page?.querySelector('#' + id)?.checked const v = (id) => _page?.querySelector('#' + id)?.value if (!_config.approvals) _config.approvals = {} if (!_config.approvals.exec) _config.approvals.exec = {} const a = _config.approvals.exec a.enabled = c('approvals-enabled') || undefined a.mode = v('approvals-mode') || undefined } // ── 工具函数 ── function toggleRow(id, label, hint, checked) { return `
${hint}
` } function esc(str) { return (str || '').replace(/&/g, '&').replace(//g, '>').replace(/"/g, '"') }