760 lines
31 KiB
JavaScript
760 lines
31 KiB
JavaScript
// ///
|
||
// app.js
|
||
// 描述:中控系统前端主逻辑
|
||
// 作者:AI Generated
|
||
// 创建日期:2026-04-05
|
||
// ///
|
||
|
||
let currentPage = 'dashboard';
|
||
let logPollInterval = null;
|
||
let lastLogTime = null;
|
||
|
||
document.addEventListener('DOMContentLoaded', () => {
|
||
renderApp();
|
||
});
|
||
|
||
function renderApp() {
|
||
document.getElementById('app').innerHTML = APP_TEMPLATE;
|
||
initNavEvents();
|
||
showPage('dashboard');
|
||
}
|
||
|
||
function initNavEvents() {
|
||
document.querySelectorAll('.nav-item').forEach(item => {
|
||
item.addEventListener('click', (e) => {
|
||
e.preventDefault();
|
||
showPage(item.dataset.page);
|
||
});
|
||
});
|
||
}
|
||
|
||
function showPage(pageName) {
|
||
currentPage = pageName;
|
||
document.querySelectorAll('.nav-item').forEach(n => n.classList.remove('active'));
|
||
document.querySelector(`[data-page="${pageName}"]`).classList.add('active');
|
||
|
||
const content = document.getElementById('page-content');
|
||
content.innerHTML = `<div class="page active">${PAGES[pageName] || '<p>页面不存在</p>'}</div>`;
|
||
|
||
if (pageName === 'dashboard') loadDashboard();
|
||
if (pageName === 'nodes') loadNodesForTable();
|
||
if (pageName === 'monitor') { loadMonitorConfig(); loadWebhookTable(); }
|
||
if (pageName === 'send') loadNodesForSend();
|
||
if (pageName === 'logs') startLogStream();
|
||
if (pageName === 'external') initExternalPage();
|
||
if (pageName === 'plugins') setTimeout(loadPluginTable, 50);
|
||
}
|
||
|
||
async function loadDashboard() {
|
||
const result = await api('/nodes/');
|
||
if (result.success) renderDashboard(result.data);
|
||
}
|
||
|
||
async function loadNodesForTable() {
|
||
const result = await api('/nodes/');
|
||
if (result.success) renderNodesTable(result.data);
|
||
}
|
||
|
||
function renderDashboard(nodes) {
|
||
const total = nodes.length;
|
||
const online = nodes.filter(n => n.status === 'active' && n.wechat_status === 'online').length;
|
||
const offline = nodes.filter(n => n.status !== 'active' || n.wechat_status !== 'online').length;
|
||
|
||
const setText = (id, val) => { const el = document.getElementById(id); if (el) el.textContent = val; };
|
||
setText('onlineCount', online);
|
||
setText('offlineCount', offline);
|
||
setText('totalMessages', total);
|
||
setText('todayMessages', '-');
|
||
}
|
||
|
||
function renderNodesTable(nodes) {
|
||
const container = document.getElementById('nodesTable');
|
||
if (!container) return;
|
||
if (!nodes || !nodes.length) {
|
||
container.innerHTML = '<p class="empty">暂无节点</p>';
|
||
return;
|
||
}
|
||
container.innerHTML = `
|
||
<table class="table">
|
||
<thead>
|
||
<tr><th>ID</th><th>名称</th><th>分组</th><th>API状态</th><th>微信状态</th><th>操作</th></tr>
|
||
</thead>
|
||
<tbody>
|
||
${nodes.map(n => {
|
||
const statusClass = n.status !== 'active' ? 'offline' : (n.wechat_status !== 'online' ? 'warning' : 'online');
|
||
return `<tr>
|
||
<td>${n.node_id}</td>
|
||
<td>${n.name}</td>
|
||
<td>${n.group}</td>
|
||
<td><span class="status-${n.status}">${n.status}</span></td>
|
||
<td><span class="status-wechat-${n.wechat_status}">${n.wechat_status}</span></td>
|
||
<td>
|
||
<button class="btn btn-xs" onclick="checkNodeStatus('${n.node_id}')">检测</button>
|
||
<button class="btn btn-xs btn-secondary" onclick="editNode('${n.node_id}')">编辑</button>
|
||
<button class="btn btn-xs btn-danger" onclick="deleteNode('${n.node_id}')">删除</button>
|
||
</td>
|
||
</tr>`;
|
||
}).join('')}
|
||
</tbody>
|
||
</table>
|
||
`;
|
||
}
|
||
|
||
async function loadNodesForSend() {
|
||
const result = await api('/nodes/');
|
||
if (result.success) {
|
||
const options = '<option value="">选择节点</option>' +
|
||
result.data.map(n => `<option value="${n.node_id}">${n.name} (${n.node_id})</option>`).join('');
|
||
document.getElementById('sendNode').innerHTML = options;
|
||
}
|
||
}
|
||
|
||
async function refreshAll() {
|
||
await loadDashboard();
|
||
showToast('刷新完成');
|
||
}
|
||
|
||
async function checkNodeStatus(nodeId) {
|
||
const result = await api(`/nodes/${nodeId}/status`);
|
||
if (result.success) {
|
||
showToast(`检测完成: API=${result.data.api_status}, 微信=${result.data.wechat_status || 'N/A'}`);
|
||
}
|
||
await loadNodesForTable();
|
||
}
|
||
|
||
async function sendQuickMessage() {
|
||
const nodeEl = document.getElementById('sendNode');
|
||
const whoEl = document.getElementById('sendWho');
|
||
const msgEl = document.getElementById('quickMsg');
|
||
if (!nodeEl || !whoEl || !msgEl) {
|
||
showToast('页面未正确加载', 'error');
|
||
return;
|
||
}
|
||
const nodeId = nodeEl.value;
|
||
const who = whoEl.value;
|
||
const msg = msgEl.value;
|
||
if (!nodeId || !who || !msg) {
|
||
showToast('请填写完整信息', 'error');
|
||
return;
|
||
}
|
||
const result = await api('/messages/send', {
|
||
method: 'POST',
|
||
body: JSON.stringify({ node_id: nodeId, who, msg })
|
||
});
|
||
if (result.success) {
|
||
showToast('发送成功');
|
||
msgEl.value = '';
|
||
} else {
|
||
showToast('发送失败: ' + (result.error || result.detail), 'error');
|
||
}
|
||
}
|
||
|
||
async function loadMonitorConfig() {
|
||
const result = await api('/config/monitor');
|
||
if (result.success) {
|
||
const enabledEl = document.getElementById('monitorEnabled');
|
||
const intervalEl = document.getElementById('checkInterval');
|
||
if (enabledEl) enabledEl.checked = result.data.enabled;
|
||
if (intervalEl) intervalEl.value = result.data.check_interval;
|
||
}
|
||
}
|
||
|
||
async function updateMonitorConfig() {
|
||
const config = {
|
||
enabled: document.getElementById('monitorEnabled').checked,
|
||
check_interval: parseInt(document.getElementById('checkInterval').value) || 60
|
||
};
|
||
const result = await api('/config/monitor', { method: 'POST', body: JSON.stringify(config) });
|
||
if (result.success) showToast('监控配置已更新');
|
||
}
|
||
|
||
async function loadWebhookTable() {
|
||
const result = await api('/webhook/');
|
||
if (result.success) renderWebhookTable(result.data || []);
|
||
}
|
||
|
||
function renderWebhookTable(webhooks) {
|
||
const container = document.getElementById('webhookTable');
|
||
if (!container) return;
|
||
if (!webhooks.length) {
|
||
container.innerHTML = '<p class="empty">暂无 Webhook,请点击添加</p>';
|
||
return;
|
||
}
|
||
container.innerHTML = `
|
||
<table class="table">
|
||
<thead>
|
||
<tr><th>名称</th><th>URL</th><th>格式</th><th>事件</th><th>状态</th><th>操作</th></tr>
|
||
</thead>
|
||
<tbody>
|
||
${webhooks.map(w => {
|
||
const eventLabels = {
|
||
'api_error': 'API错误',
|
||
'wechat_offline': '微信离线',
|
||
'wechat_online': '微信上线',
|
||
'node_offline': '节点离线',
|
||
'node_online': '节点上线'
|
||
};
|
||
const events = Array.isArray(w.event_types) ? w.event_types.map(e => eventLabels[e] || e).join(', ') : '-';
|
||
return `<tr>
|
||
<td>${w.name || '-'}</td>
|
||
<td style="max-width: 200px; overflow: hidden; text-overflow: ellipsis;">${w.url || '-'}</td>
|
||
<td>${w.format === 'bark' ? 'Bark' : '企业微信'}</td>
|
||
<td style="font-size: 12px;">${events}</td>
|
||
<td><span class="status-${w.enabled ? 'online' : 'offline'}">${w.enabled ? '启用' : '禁用'}</span></td>
|
||
<td>
|
||
<button class="btn btn-xs" onclick="editWebhook(${w.id})">编辑</button>
|
||
<button class="btn btn-xs btn-danger" onclick="deleteWebhook(${w.id})">删除</button>
|
||
</td>
|
||
</tr>`;
|
||
}).join('')}
|
||
</tbody>
|
||
</table>
|
||
`;
|
||
}
|
||
|
||
function showAddWebhookModal(webhook = null) {
|
||
const modal = document.getElementById('modal');
|
||
const title = document.getElementById('modalTitle');
|
||
const form = document.getElementById('nodeForm');
|
||
|
||
title.textContent = webhook ? '编辑 Webhook' : '添加 Webhook';
|
||
form.innerHTML = `
|
||
<input type="hidden" id="webhookId" value="${webhook ? webhook.id : ''}">
|
||
<div class="form-group">
|
||
<label>名称</label>
|
||
<input type="text" id="webhookName" class="input" value="${webhook ? webhook.name : ''}" placeholder="如:Bark告警">
|
||
</div>
|
||
<div class="form-group">
|
||
<label>URL</label>
|
||
<input type="text" id="webhookUrl" class="input" value="${webhook ? webhook.url : ''}" placeholder="https://api.day.app/xxx">
|
||
</div>
|
||
<div class="form-group">
|
||
<label>格式</label>
|
||
<select id="webhookFormat" class="select">
|
||
<option value="bark" ${webhook && webhook.format === 'bark' ? 'selected' : ''}>Bark</option>
|
||
<option value="wechat" ${webhook && webhook.format === 'wechat' ? 'selected' : ''}>企业微信</option>
|
||
</select>
|
||
</div>
|
||
<div class="form-group">
|
||
<label>启用</label>
|
||
<label class="checkbox-label">
|
||
<input type="checkbox" id="webhookEnabled" ${!webhook || webhook.enabled ? 'checked' : ''}>
|
||
启用此 Webhook
|
||
</label>
|
||
</div>
|
||
<div class="form-group">
|
||
<label>接收事件</label>
|
||
<div class="event-types">
|
||
<label class="checkbox-label"><input type="checkbox" class="webhook-event" value="api_error" ${webhook && webhook.event_types && webhook.event_types.includes('api_error') ? 'checked' : ''}> API错误</label>
|
||
<label class="checkbox-label"><input type="checkbox" class="webhook-event" value="wechat_offline" ${webhook && webhook.event_types && webhook.event_types.includes('wechat_offline') ? 'checked' : ''}> 微信离线</label>
|
||
<label class="checkbox-label"><input type="checkbox" class="webhook-event" value="wechat_online" ${webhook && webhook.event_types && webhook.event_types.includes('wechat_online') ? 'checked' : ''}> 微信上线</label>
|
||
<label class="checkbox-label"><input type="checkbox" class="webhook-event" value="node_offline" ${webhook && webhook.event_types && webhook.event_types.includes('node_offline') ? 'checked' : ''}> 节点离线</label>
|
||
<label class="checkbox-label"><input type="checkbox" class="webhook-event" value="node_online" ${webhook && webhook.event_types && webhook.event_types.includes('node_online') ? 'checked' : ''}> 节点上线</label>
|
||
<label class="checkbox-label"><input type="checkbox" class="webhook-event" value="plugin_notification" ${webhook && webhook.event_types && webhook.event_types.includes('plugin_notification') ? 'checked' : ''}> 插件通知</label>
|
||
</div>
|
||
</div>
|
||
<div class="form-actions">
|
||
<button type="button" class="btn" onclick="saveWebhook()">保存</button>
|
||
<button type="button" class="btn btn-secondary" onclick="closeModal()">取消</button>
|
||
</div>
|
||
`;
|
||
modal.style.display = 'flex';
|
||
}
|
||
|
||
async function editWebhook(id) {
|
||
const result = await api(`/webhook/${id}`);
|
||
if (result.success && result.data) {
|
||
showAddWebhookModal(result.data);
|
||
}
|
||
}
|
||
|
||
async function deleteWebhook(id) {
|
||
if (!confirm('确定删除此 Webhook?')) return;
|
||
const result = await api(`/webhook/${id}`, { method: 'DELETE' });
|
||
if (result.success) {
|
||
showToast('Webhook 已删除');
|
||
loadWebhookTable();
|
||
} else {
|
||
showToast('删除失败', 'error');
|
||
}
|
||
}
|
||
|
||
async function saveWebhook() {
|
||
const id = document.getElementById('webhookId').value;
|
||
const eventTypes = Array.from(document.querySelectorAll('.webhook-event:checked')).map(cb => cb.value);
|
||
const data = {
|
||
name: document.getElementById('webhookName').value,
|
||
url: document.getElementById('webhookUrl').value,
|
||
format: document.getElementById('webhookFormat').value,
|
||
enabled: document.getElementById('webhookEnabled').checked,
|
||
event_types: eventTypes
|
||
};
|
||
|
||
if (!data.url) {
|
||
showToast('请输入 URL', 'error');
|
||
return;
|
||
}
|
||
|
||
let result;
|
||
if (id) {
|
||
result = await api(`/webhook/${id}`, { method: 'PUT', body: JSON.stringify(data) });
|
||
} else {
|
||
result = await api('/webhook/', { method: 'POST', body: JSON.stringify(data) });
|
||
}
|
||
|
||
if (result.success) {
|
||
showToast(id ? 'Webhook 已更新' : 'Webhook 已添加');
|
||
closeModal();
|
||
loadWebhookTable();
|
||
} else {
|
||
showToast('保存失败: ' + (result.error || ''), 'error');
|
||
}
|
||
}
|
||
|
||
async function startLogStream() {
|
||
const result = await api('/logs/recent?count=200');
|
||
if (result.success && result.data) {
|
||
const container = document.getElementById('logContainer');
|
||
if (container) {
|
||
container.innerHTML = '';
|
||
if (result.data.length > 0) {
|
||
lastLogTime = result.data[0].timestamp;
|
||
result.data.forEach(entry => addLogEntry(entry));
|
||
}
|
||
}
|
||
}
|
||
if (logPollInterval) clearInterval(logPollInterval);
|
||
logPollInterval = setInterval(async () => {
|
||
const res = await api('/logs/recent?count=20');
|
||
if (res.success && res.data && res.data.length > 0) {
|
||
const newLogs = res.data.filter(log => {
|
||
if (!lastLogTime) return true;
|
||
return new Date(log.timestamp) > new Date(lastLogTime);
|
||
});
|
||
if (newLogs.length > 0) {
|
||
lastLogTime = newLogs[0].timestamp;
|
||
const container = document.getElementById('logContainer');
|
||
if (container) newLogs.forEach(entry => addLogEntry(entry));
|
||
}
|
||
}
|
||
}, 3000);
|
||
}
|
||
|
||
function addLogEntry(entry) {
|
||
const container = document.getElementById('logContainer');
|
||
if (!container) return;
|
||
const colors = { 'INFO': '#4a9eff', 'WARNING': '#fa8c16', 'ERROR': '#f5222d', 'SUCCESS': '#52c41a' };
|
||
const color = colors[entry.level] || '#666';
|
||
const html = `<div class="log-entry">
|
||
<span class="log-time">${entry.timestamp}</span>
|
||
<span class="log-level" style="color: ${color}">${entry.level}</span>
|
||
<span class="log-source">[${entry.source}]</span>
|
||
<span class="log-message">${entry.message}</span>
|
||
</div>`;
|
||
container.insertAdjacentHTML('afterbegin', html);
|
||
if (container.children.length > 500) container.lastChild.remove();
|
||
}
|
||
|
||
function clearLogs() {
|
||
const container = document.getElementById('logContainer');
|
||
if (container) container.innerHTML = '';
|
||
}
|
||
|
||
function initExternalPage() {
|
||
document.getElementById('externalApiUrl').textContent = `${window.location.origin}/api/v1/external/send`;
|
||
document.getElementById('webhookReceiveUrl').textContent = `${window.location.origin}/api/v1/external/webhook/{event}`;
|
||
}
|
||
|
||
async function loadPluginTable() {
|
||
const result = await api('/plugins/');
|
||
if (result.success) renderPluginTable(result.plugins || []);
|
||
}
|
||
|
||
function renderPluginTable(plugins) {
|
||
const container = document.getElementById('pluginTable');
|
||
if (!container) return;
|
||
if (!plugins.length) {
|
||
container.innerHTML = '<p class="empty">暂无插件</p>';
|
||
return;
|
||
}
|
||
const typeLabels = {
|
||
'message_handler': '消息处理',
|
||
'data_source': '数据源',
|
||
'action_trigger': '动作触发',
|
||
'ai_agent': 'AI智能体',
|
||
'custom': '自定义'
|
||
};
|
||
container.innerHTML = `
|
||
<table class="table">
|
||
<thead>
|
||
<tr><th>名称</th><th>版本</th><th>类型</th><th>描述</th><th>状态</th><th>操作</th></tr>
|
||
</thead>
|
||
<tbody>
|
||
${plugins.map(p => `
|
||
<tr>
|
||
<td>${p.name || '-'}</td>
|
||
<td>${p.version || '-'}</td>
|
||
<td>${typeLabels[p.type] || p.type || '-'}</td>
|
||
<td style="max-width: 200px; overflow: hidden; text-overflow: ellipsis;">${p.description || '-'}</td>
|
||
<td><span class="status-${p.enabled ? 'online' : 'offline'}">${p.enabled ? '启用' : '禁用'}</span></td>
|
||
<td>
|
||
<button class="btn btn-xs" onclick="togglePlugin('${p.name}')">${p.enabled ? '禁用' : '启用'}</button>
|
||
<button class="btn btn-xs" onclick="configPlugin('${p.name}')">配置</button>
|
||
<button class="btn btn-xs btn-secondary" onclick="showPluginLogs('${p.name}')">日志</button>
|
||
</td>
|
||
</tr>
|
||
`).join('')}
|
||
</tbody>
|
||
</table>
|
||
`;
|
||
}
|
||
|
||
async function togglePlugin(name) {
|
||
const result = await api(`/plugins/${name}`);
|
||
if (result.success && result.plugin) {
|
||
const action = result.plugin.enabled ? 'disable' : 'enable';
|
||
const res = await api(`/plugins/${name}/${action}`, { method: 'POST' });
|
||
if (res.success) {
|
||
showToast(res.message);
|
||
loadPluginTable();
|
||
}
|
||
}
|
||
}
|
||
|
||
function configPlugin(name) {
|
||
Promise.all([
|
||
api(`/plugins/${name}`),
|
||
api(`/plugins/${name}/schema`)
|
||
]).then(([pluginResult, schemaResult]) => {
|
||
if (pluginResult.success) {
|
||
if (schemaResult.success && schemaResult.has_form && schemaResult.schema) {
|
||
showPluginConfigForm(pluginResult.plugin, schemaResult.schema);
|
||
} else {
|
||
showPluginConfigModal(pluginResult.plugin);
|
||
}
|
||
}
|
||
});
|
||
}
|
||
|
||
function showPluginConfigForm(plugin, schema) {
|
||
const modal = document.getElementById('modal');
|
||
const title = document.getElementById('modalTitle');
|
||
const form = document.getElementById('nodeForm');
|
||
|
||
title.textContent = `配置插件: ${plugin.name}`;
|
||
let formHtml = '';
|
||
|
||
const currentConfig = plugin.config || {};
|
||
|
||
if (schema.sections) {
|
||
schema.sections.forEach((section, sIdx) => {
|
||
const sectionId = `plugin-section-${sIdx}`;
|
||
formHtml += `<div class="form-section">
|
||
<h4 class="section-header" onclick="togglePluginSection('${sectionId}')">
|
||
<span>${section.label}</span>
|
||
<span class="section-arrow" id="${sectionId}-arrow">▼</span>
|
||
</h4>
|
||
<div class="section-content" id="${sectionId}" style="display: block;">`;
|
||
|
||
section.fields.forEach(field => {
|
||
const value = currentConfig[field.name] !== undefined ? currentConfig[field.name] : field.default;
|
||
|
||
if (field.type === 'string' || field.type === 'password') {
|
||
formHtml += `<div class="form-group">
|
||
<label>${field.label}</label>
|
||
<input type="${field.type === 'password' ? 'password' : 'text'}"
|
||
id="plugin_field_${field.name}"
|
||
value="${escapeHtml(value || '')}"
|
||
placeholder="${field.placeholder || ''}"
|
||
${field.required ? 'required' : ''}>
|
||
${field.description ? `<small class="field-desc">${field.description}</small>` : ''}
|
||
</div>`;
|
||
} else if (field.type === 'number') {
|
||
formHtml += `<div class="form-group">
|
||
<label>${field.label}</label>
|
||
<input type="number"
|
||
id="plugin_field_${field.name}"
|
||
value="${value}"
|
||
min="${field.min_value || ''}"
|
||
max="${field.max_value || ''}"
|
||
${field.required ? 'required' : ''}>
|
||
${field.description ? `<small class="field-desc">${field.description}</small>` : ''}
|
||
</div>`;
|
||
} else if (field.type === 'boolean') {
|
||
formHtml += `<div class="form-group">
|
||
<label class="checkbox-label">
|
||
<input type="checkbox" id="plugin_field_${field.name}" ${value ? 'checked' : ''}>
|
||
${field.label}
|
||
</label>
|
||
${field.description ? `<small class="field-desc">${field.description}</small>` : ''}
|
||
</div>`;
|
||
} else if (field.type === 'textarea') {
|
||
formHtml += `<div class="form-group">
|
||
<label>${field.label}</label>
|
||
<textarea id="plugin_field_${field.name}"
|
||
rows="3"
|
||
placeholder="${field.placeholder || ''}">${escapeHtml(value || '')}</textarea>
|
||
${field.description ? `<small class="field-desc">${field.description}</small>` : ''}
|
||
</div>`;
|
||
} else if (field.type === 'select' && field.options) {
|
||
let optionsHtml = field.options.map(opt =>
|
||
`<option value="${opt.value}" ${value === opt.value ? 'selected' : ''}>${opt.label}</option>`
|
||
).join('');
|
||
formHtml += `<div class="form-group">
|
||
<label>${field.label}</label>
|
||
<select id="plugin_field_${field.name}">${optionsHtml}</select>
|
||
${field.description ? `<small class="field-desc">${field.description}</small>` : ''}
|
||
</div>`;
|
||
} else if (field.type === 'cron') {
|
||
formHtml += `<div class="form-group">
|
||
<label>${field.label}</label>
|
||
<input type="text"
|
||
id="plugin_field_${field.name}"
|
||
value="${escapeHtml(value || '')}"
|
||
placeholder="*/5 * * * *">
|
||
${field.description ? `<small class="field-desc">${field.description}</small>` : ''}
|
||
</div>`;
|
||
} else {
|
||
formHtml += `<div class="form-group">
|
||
<label>${field.label}</label>
|
||
<input type="text"
|
||
id="plugin_field_${field.name}"
|
||
value="${escapeHtml(value || '')}"
|
||
placeholder="${field.placeholder || ''}">
|
||
</div>`;
|
||
}
|
||
});
|
||
|
||
formHtml += `</div></div>`;
|
||
});
|
||
}
|
||
|
||
formHtml += `<input type="hidden" id="pluginName" value="${plugin.name}">`;
|
||
formHtml += `<input type="hidden" id="pluginSchema" value='${JSON.stringify(schema).replace(/'/g, "'")}'>`;
|
||
|
||
form.innerHTML = formHtml + `
|
||
<div class="form-actions">
|
||
<button type="button" class="btn" onclick="savePluginConfigFromForm()">保存</button>
|
||
<button type="button" class="btn btn-secondary" onclick="closeModal()">取消</button>
|
||
</div>
|
||
`;
|
||
modal.style.display = 'flex';
|
||
}
|
||
|
||
function togglePluginSection(sectionId) {
|
||
const content = document.getElementById(sectionId);
|
||
const arrow = document.getElementById(sectionId + '-arrow');
|
||
if (content.style.display === 'none') {
|
||
content.style.display = 'block';
|
||
arrow.textContent = '▼';
|
||
} else {
|
||
content.style.display = 'none';
|
||
arrow.textContent = '▶';
|
||
}
|
||
}
|
||
|
||
function escapeHtml(text) {
|
||
if (typeof text !== 'string') text = String(text);
|
||
const div = document.createElement('div');
|
||
div.textContent = text;
|
||
return div.innerHTML;
|
||
}
|
||
|
||
async function savePluginConfigFromForm() {
|
||
const name = document.getElementById('pluginName').value;
|
||
const schema = JSON.parse(document.getElementById('pluginSchema').value);
|
||
|
||
const config = {};
|
||
|
||
if (schema.sections) {
|
||
schema.sections.forEach(section => {
|
||
section.fields.forEach(field => {
|
||
const input = document.getElementById(`plugin_field_${field.name}`);
|
||
if (!input) return;
|
||
|
||
if (field.type === 'boolean') {
|
||
config[field.name] = input.checked;
|
||
} else if (field.type === 'number') {
|
||
config[field.name] = parseFloat(input.value) || 0;
|
||
} else {
|
||
config[field.name] = input.value;
|
||
}
|
||
});
|
||
});
|
||
}
|
||
|
||
const result = await api(`/plugins/${name}/config`, {
|
||
method: 'PUT',
|
||
body: JSON.stringify({ name, config })
|
||
});
|
||
if (result.success) {
|
||
showToast('配置已保存');
|
||
closeModal();
|
||
loadPluginTable();
|
||
} else {
|
||
showToast('保存失败: ' + (result.error || ''), 'error');
|
||
}
|
||
}
|
||
|
||
function showPluginConfigModal(plugin) {
|
||
const modal = document.getElementById('modal');
|
||
const title = document.getElementById('modalTitle');
|
||
const form = document.getElementById('nodeForm');
|
||
|
||
title.textContent = `配置插件: ${plugin.name} (JSON)`;
|
||
const configJson = JSON.stringify(plugin.config || {}, null, 2);
|
||
form.innerHTML = `
|
||
<div class="form-group">
|
||
<label>配置 (JSON)</label>
|
||
<textarea id="pluginConfigJson" class="textarea" rows="6" placeholder='{"key": "value"}'>${configJson}</textarea>
|
||
</div>
|
||
<input type="hidden" id="pluginName" value="${plugin.name}">
|
||
<div class="form-actions">
|
||
<button type="button" class="btn" onclick="savePluginConfig()">保存</button>
|
||
<button type="button" class="btn btn-secondary" onclick="closeModal()">取消</button>
|
||
</div>
|
||
`;
|
||
modal.style.display = 'flex';
|
||
}
|
||
|
||
async function savePluginConfig() {
|
||
const name = document.getElementById('pluginName').value;
|
||
let config;
|
||
try {
|
||
config = JSON.parse(document.getElementById('pluginConfigJson').value);
|
||
} catch (e) {
|
||
showToast('JSON格式错误', 'error');
|
||
return;
|
||
}
|
||
const result = await api(`/plugins/${name}/config`, {
|
||
method: 'PUT',
|
||
body: JSON.stringify({ name, config })
|
||
});
|
||
if (result.success) {
|
||
showToast('配置已保存');
|
||
closeModal();
|
||
loadPluginTable();
|
||
} else {
|
||
showToast('保存失败: ' + (result.error || ''), 'error');
|
||
}
|
||
}
|
||
|
||
async function showPluginLogs(pluginName) {
|
||
const modal = document.getElementById('modal');
|
||
const title = document.getElementById('modalTitle');
|
||
const form = document.getElementById('nodeForm');
|
||
|
||
title.textContent = `插件日志: ${pluginName}`;
|
||
form.innerHTML = `
|
||
<div class="form-group">
|
||
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 10px;">
|
||
<label>日志内容</label>
|
||
<button type="button" class="btn btn-xs" onclick="refreshPluginLogs('${pluginName}')">刷新</button>
|
||
</div>
|
||
<div id="pluginLogsContent" style="background: #1e1e1e; color: #d4d4d4; padding: 10px; border-radius: 4px; max-height: 400px; overflow-y: auto; font-family: monospace; font-size: 12px; white-space: pre-wrap; word-break: break-all;">
|
||
加载中...
|
||
</div>
|
||
</div>
|
||
<input type="hidden" id="logPluginName" value="${pluginName}">
|
||
<div class="form-actions">
|
||
<button type="button" class="btn btn-secondary" onclick="closeModal()">关闭</button>
|
||
</div>
|
||
`;
|
||
modal.style.display = 'flex';
|
||
|
||
await refreshPluginLogs(pluginName);
|
||
}
|
||
|
||
async function refreshPluginLogs(pluginName) {
|
||
const logsContent = document.getElementById('pluginLogsContent');
|
||
if (!logsContent) return;
|
||
|
||
logsContent.textContent = '加载中...';
|
||
|
||
const result = await api(`/plugins/${pluginName}/logs?lines=100`);
|
||
|
||
if (result.success) {
|
||
if (result.logs && result.logs.length > 0) {
|
||
logsContent.textContent = result.logs.join('\n');
|
||
logsContent.scrollTop = logsContent.scrollHeight;
|
||
} else {
|
||
logsContent.textContent = '暂无日志';
|
||
}
|
||
} else {
|
||
logsContent.textContent = '加载失败: ' + (result.error || '未知错误');
|
||
}
|
||
}
|
||
|
||
function showAddNodeModal(node = null) {
|
||
document.getElementById('modalTitle').textContent = '添加节点';
|
||
document.getElementById('nodeForm').reset();
|
||
document.getElementById('nodeIdOld').value = '';
|
||
document.getElementById('nodeId').disabled = false;
|
||
document.getElementById('modal').style.display = 'flex';
|
||
}
|
||
|
||
async function editNode(nodeId) {
|
||
const result = await api(`/nodes/${nodeId}`);
|
||
if (result.success) {
|
||
const node = result.data;
|
||
document.getElementById('modalTitle').textContent = '编辑节点';
|
||
document.getElementById('nodeIdOld').value = node.node_id;
|
||
document.getElementById('nodeId').value = node.node_id;
|
||
document.getElementById('nodeId').disabled = true;
|
||
document.getElementById('nodeName').value = node.name;
|
||
document.getElementById('nodeApiUrl').value = node.api_url;
|
||
document.getElementById('nodeApiKey').value = node.api_key;
|
||
document.getElementById('nodeGroup').value = node.group;
|
||
document.getElementById('nodeDesc').value = node.description || '';
|
||
document.getElementById('modal').style.display = 'flex';
|
||
}
|
||
}
|
||
|
||
async function deleteNode(nodeId) {
|
||
if (!confirm(`确定删除节点 ${nodeId}?`)) return;
|
||
const result = await api(`/nodes/${nodeId}`, { method: 'DELETE' });
|
||
if (result.success) {
|
||
showToast('节点已删除');
|
||
await loadNodesForTable();
|
||
}
|
||
}
|
||
|
||
document.getElementById('nodeForm').onsubmit = async (e) => {
|
||
e.preventDefault();
|
||
const oldId = document.getElementById('nodeIdOld').value;
|
||
const nodeData = {
|
||
node_id: document.getElementById('nodeId').value,
|
||
name: document.getElementById('nodeName').value,
|
||
api_url: document.getElementById('nodeApiUrl').value,
|
||
api_key: document.getElementById('nodeApiKey').value,
|
||
group: document.getElementById('nodeGroup').value,
|
||
description: document.getElementById('nodeDesc').value
|
||
};
|
||
let result;
|
||
if (oldId) {
|
||
result = await api(`/nodes/${oldId}`, { method: 'PUT', body: JSON.stringify(nodeData) });
|
||
} else {
|
||
result = await api('/nodes/', { method: 'POST', body: JSON.stringify(nodeData) });
|
||
}
|
||
if (result.success) {
|
||
showToast(oldId ? '节点已更新' : '节点已添加');
|
||
closeModal();
|
||
await loadNodesForTable();
|
||
} else {
|
||
showToast('失败: ' + (result.error || result.detail), 'error');
|
||
}
|
||
};
|
||
|
||
function closeModal() {
|
||
document.getElementById('modal').style.display = 'none';
|
||
}
|
||
|
||
function showToast(message, type = 'success') {
|
||
const toast = document.getElementById('toast');
|
||
toast.textContent = message;
|
||
toast.className = 'toast show ' + type;
|
||
setTimeout(() => toast.classList.remove('show'), 3000);
|
||
}
|
||
|
||
document.addEventListener('DOMContentLoaded', renderApp);
|