fix: platform_silent记忆提取 + 群聊上下文整合 + 多QQ实例支持
- platform_silent模式接入Orchestrator记忆提取:被动观察群聊时提取值得记住的信息到对应命名空间 - post_chat后台思考注入平台观察:对话后思考也能看到群聊摘要 - QQ适配器:OneBot v11 self_id动态捕获、CQ图片URL提取、视觉+OCR并行处理 - Router解耦:ConfigName/PlatformName分离,支持多QQ实例独立连接 - 黑白名单功能:后端API + Ethend代理 + UI面板 - \n\n双换行断句:AI回复按双换行分割为多条消息按间隔发送 - @提及修复:bot自感知UID进行@检测 - 群聊上下文共享:channel-based userID避免记忆碎片化 - 消息日志显示处理后内容而非原始SSE数据 - platform-bridge Dockerfile + docker-compose.yml更新 Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
+541
-65
@@ -282,6 +282,14 @@ input[type="range"] { accent-color: var(--accent); padding: 0; }
|
||||
}
|
||||
.empty-state .icon { font-size: 36px; margin-bottom: 8px; }
|
||||
|
||||
/* 概览统计条 */
|
||||
.overview-stat {
|
||||
display: flex; flex-direction: column; align-items: center; gap: 2px;
|
||||
min-width: 80px;
|
||||
}
|
||||
.overview-stat-label { font-size: 10px; color: var(--text3); text-transform: uppercase; letter-spacing: .5px; }
|
||||
.overview-stat-value { font-size: 15px; font-weight: 700; }
|
||||
|
||||
/* 会话详情展开 */
|
||||
.session-detail {
|
||||
background: var(--bg); border: 1px solid var(--border2); border-radius: var(--radius-sm);
|
||||
@@ -997,6 +1005,7 @@ function connectWS() {
|
||||
if (msg.type === 'log') handleWSLog(msg.data);
|
||||
if (msg.type === 'stt-log') handleSTTLog(msg);
|
||||
if (msg.type === 'voice_transcript') handleVoiceTranscript(msg);
|
||||
if (msg.type === 'chat-log') handleChatLog(msg.data);
|
||||
if (msg.type === 'status') {
|
||||
STATE.serviceStatus = msg.data;
|
||||
if (STATE.activePanel === 'services') renderServiceCards();
|
||||
@@ -3551,6 +3560,70 @@ async function processVoiceRecording() {
|
||||
reader.readAsDataURL(blob);
|
||||
}
|
||||
|
||||
function handleChatLog(entry) {
|
||||
if (!entry || !entry.platform) return;
|
||||
// Add to local log cache.
|
||||
STATE.chatLogs = STATE.chatLogs || {};
|
||||
STATE.chatLogs[entry.platform] = STATE.chatLogs[entry.platform] || [];
|
||||
var logs = STATE.chatLogs[entry.platform];
|
||||
// Dedup by timestamp+content.
|
||||
var dup = logs.find(function(l) {
|
||||
return l.timestamp === entry.timestamp && l.content === entry.content && l.direction === entry.direction;
|
||||
});
|
||||
if (dup) return;
|
||||
logs.unshift(entry);
|
||||
if (logs.length > 500) logs.length = 500;
|
||||
// If viewing this platform's detail, prepend to the log container.
|
||||
if (STATE.activePanel === 'chatPlatforms' && STATE.chatActivePlatform) {
|
||||
// Check if the entry's platform matches the active config's platform type.
|
||||
var activeCfg = (STATE.chatConfigs || []).find(function(c) { return c.name === STATE.chatActivePlatform; }) || null;
|
||||
var activePtype = (activeCfg && activeCfg.platform) || STATE.chatActivePlatform;
|
||||
if (entry.platform === activePtype) {
|
||||
prependChatLogEntry(entry);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function prependChatLogEntry(l) {
|
||||
var container = document.getElementById('chat-log-container');
|
||||
if (!container) return;
|
||||
// Check if the container is showing an empty state; if so, clear it first.
|
||||
var emptyState = container.querySelector('.empty-state');
|
||||
if (emptyState) container.innerHTML = '';
|
||||
|
||||
var filter = STATE.chatLogFilter || 'all';
|
||||
if (filter === 'incoming' && l.direction !== 'incoming') return;
|
||||
if (filter === 'outgoing' && l.direction !== 'outgoing') return;
|
||||
if (filter === 'error' && !l.error && l.success !== false) return;
|
||||
|
||||
var arrow = l.direction === 'incoming' ? '← 收到' : '→ 发送';
|
||||
var color = l.direction === 'incoming' ? 'var(--blue)' : 'var(--green)';
|
||||
var time = new Date(l.timestamp).toLocaleString('zh-CN', { hour12: false });
|
||||
var content = (l.content || '').length > 300 ? (l.content || '').substring(0, 297) + '...' : (l.content || '');
|
||||
var errorTag = (l.error || l.success === false)
|
||||
? ' <span style="color:var(--red);cursor:help" title="' + escHtml(l.error || '发送失败') + '">⚠</span>' : '';
|
||||
var sender = escHtml(l.sender_name || l.sender_id || '-');
|
||||
if (l.sender_name && l.sender_id && l.sender_name !== l.sender_id) {
|
||||
sender = escHtml(l.sender_name) + ' <span style="color:var(--text3);font-size:10px">(' + escHtml(l.sender_id) + ')</span>';
|
||||
}
|
||||
var ctxTag = '';
|
||||
if (l.channel_id && l.channel_id.indexOf('private_') !== 0 && l.direction === 'incoming') {
|
||||
ctxTag = ' <span style="color:var(--text3);font-size:10px">[群:' + escHtml(l.channel_id) + ']</span> ';
|
||||
}
|
||||
var entryHTML = '<div style="padding:6px 10px;border-bottom:1px solid var(--border);font-size:12px">' +
|
||||
'<span style="color:' + color + ';font-weight:600">' + arrow + '</span> ' +
|
||||
'<span style="color:var(--text3)">' + time + '</span> ' +
|
||||
ctxTag +
|
||||
'<span style="color:var(--text2)">' + sender + '</span> ' +
|
||||
'<span>' + escHtml(content) + '</span>' + errorTag +
|
||||
'</div>';
|
||||
container.insertAdjacentHTML('afterbegin', entryHTML);
|
||||
// Keep max 200 entries in DOM.
|
||||
while (container.children.length > 200) {
|
||||
container.removeChild(container.lastChild);
|
||||
}
|
||||
}
|
||||
|
||||
function handleVoiceTranscript(msg) {
|
||||
var resultEl = document.getElementById('voice-result');
|
||||
if (!resultEl) return;
|
||||
@@ -4155,42 +4228,72 @@ function toggleTimelineAutoRefresh(on) {
|
||||
// ========== 面板: 第三方聊天配置 ==========
|
||||
|
||||
var PLATFORM_FIELDS = {
|
||||
qq: [{ key: 'bot_port', label: 'Bot WebSocket 端口', placeholder: '8096' }],
|
||||
qq: [
|
||||
{ key: 'mode', label: '连接模式', type: 'select', options: [
|
||||
{ value: 'client', label: '客户端模式 (主动连接 NapCat)' },
|
||||
{ value: 'server', label: '服务端模式 (等待 NapCat 连接)' }
|
||||
]},
|
||||
{ key: 'remote_url', label: 'NapCat OneBot WS 地址', placeholder: '如: ws://127.0.0.1:10311', showIf: { key: 'mode', value: 'client' } },
|
||||
{ key: 'access_token', label: 'Access Token (可选)', placeholder: '留空则不验证' },
|
||||
{ key: 'bot_port', label: '本地监听端口', placeholder: '8096', showIf: { key: 'mode', value: 'server' } },
|
||||
{ key: 'send_interval_ms', label: '消息发送间隔 (毫秒, 防封控)', placeholder: '2000' },
|
||||
{ key: 'admin_uids', label: '管理员账号ID (多个用逗号分隔)', placeholder: '如: 123456789,987654321' }
|
||||
],
|
||||
telegram: [
|
||||
{ key: 'bot_token', label: 'Bot Token', placeholder: '123456:ABC-DEF...' },
|
||||
{ key: 'webhook_url', label: 'Webhook URL', placeholder: 'https://your-domain.com' }
|
||||
{ key: 'webhook_url', label: 'Webhook URL', placeholder: 'https://your-domain.com' },
|
||||
{ key: 'admin_uids', label: '管理员账号ID (多个用逗号分隔)', placeholder: '如: 123456789,987654321' }
|
||||
],
|
||||
webhook: [
|
||||
{ key: 'webhook_url', label: 'Webhook URL', placeholder: 'https://hook.example.com/chat' },
|
||||
{ key: 'secret', label: 'Secret Token', placeholder: '(可选)' }
|
||||
{ key: 'secret', label: 'Secret Token', placeholder: '(可选)' },
|
||||
{ key: 'admin_uids', label: '管理员账号ID (多个用逗号分隔)', placeholder: '如: user_abc,user_xyz' }
|
||||
],
|
||||
wechat: [
|
||||
{ key: 'corp_id', label: '企业ID (Corp ID)', placeholder: 'ww...' },
|
||||
{ key: 'corp_secret', label: '应用Secret', placeholder: '' },
|
||||
{ key: 'agent_id', label: 'Agent ID', placeholder: '1000001' },
|
||||
{ key: 'webhook_url', label: 'Webhook URL', placeholder: 'https://...' }
|
||||
{ key: 'webhook_url', label: 'Webhook URL', placeholder: 'https://...' },
|
||||
{ key: 'admin_uids', label: '管理员账号ID (多个用逗号分隔)', placeholder: '如: user_abc,user_xyz' }
|
||||
],
|
||||
feishu: [
|
||||
{ key: 'app_id', label: 'App ID', placeholder: 'cli_...' },
|
||||
{ key: 'app_secret', label: 'App Secret', placeholder: '' },
|
||||
{ key: 'verification_token', label: 'Verification Token', placeholder: '' },
|
||||
{ key: 'webhook_url', label: 'Webhook URL', placeholder: 'https://...' }
|
||||
{ key: 'webhook_url', label: 'Webhook URL', placeholder: 'https://...' },
|
||||
{ key: 'admin_uids', label: '管理员账号ID (多个用逗号分隔)', placeholder: '如: user_abc,user_xyz' }
|
||||
],
|
||||
discord: [
|
||||
{ key: 'bot_token', label: 'Bot Token', placeholder: 'MT...' },
|
||||
{ key: 'application_id', label: 'Application ID', placeholder: '123456789...' }
|
||||
{ key: 'application_id', label: 'Application ID', placeholder: '123456789...' },
|
||||
{ key: 'admin_uids', label: '管理员账号ID (多个用逗号分隔)', placeholder: '如: 123456789,987654321' }
|
||||
]
|
||||
};
|
||||
|
||||
var PLATFORM_ICONS = { qq: '🐧', telegram: '✈️', webhook: '🪝', wechat: '💚', feishu: '🕊️', discord: '🎮' };
|
||||
var PLATFORM_LABELS = { qq: 'QQ', telegram: 'Telegram', webhook: 'Webhook', wechat: 'WeChat', feishu: 'Feishu', discord: 'Discord' };
|
||||
// 已完整实现的平台 (非桩代码)
|
||||
var PLATFORM_REAL = { qq: true, telegram: true, webhook: true };
|
||||
|
||||
// 静态能力声明 (来自各适配器 Capabilities() 返回值,桥接离线时展示)
|
||||
var PLATFORM_STATIC_CAPS = {
|
||||
qq: { max_message_length: 4500, supports_markdown: false, supports_image: true, supports_voice: false, supports_emoji: true, supports_reaction: false, supports_typing_hint: false, recommend_burst_max: 3 },
|
||||
telegram: { max_message_length: 4096, supports_markdown: true, supports_image: true, supports_voice: true, supports_emoji: true, supports_reaction: false, supports_typing_hint: true, recommend_burst_max: 5 },
|
||||
webhook: { max_message_length: 4000, supports_markdown: true, supports_image: true, supports_voice: true, supports_emoji: true, supports_reaction: false, supports_typing_hint: false, recommend_burst_max: 3 },
|
||||
wechat: { max_message_length: 2048, supports_markdown: false, supports_image: true, supports_voice: true, supports_emoji: false, supports_reaction: false, supports_typing_hint: false, recommend_burst_max: 3 },
|
||||
feishu: { max_message_length: 30000, supports_markdown: true, supports_image: true, supports_voice: false, supports_emoji: true, supports_reaction: true, supports_typing_hint: false, recommend_burst_max: 5 },
|
||||
discord: { max_message_length: 2000, supports_markdown: true, supports_image: true, supports_voice: false, supports_emoji: true, supports_reaction: true, supports_typing_hint: true, recommend_burst_max: 3 }
|
||||
};
|
||||
|
||||
function startChatAutoRefresh() {
|
||||
stopChatAutoRefresh();
|
||||
// Periodic refresh for overview + connection status (logs are now real-time via WebSocket).
|
||||
STATE.chatConfigsAutoRefresh = setInterval(function() {
|
||||
if (STATE.activePanel === 'chatPlatforms') {
|
||||
loadChatConfigs();
|
||||
if (STATE.chatActivePlatform) refreshChatLogs(STATE.chatActivePlatform);
|
||||
loadChatOverview();
|
||||
if (STATE.chatActivePlatform) {
|
||||
loadChatPlatformInfo(STATE.chatActivePlatform);
|
||||
}
|
||||
}
|
||||
}, 10000);
|
||||
}
|
||||
@@ -4199,59 +4302,131 @@ function stopChatAutoRefresh() {
|
||||
if (STATE.chatConfigsAutoRefresh) { clearInterval(STATE.chatConfigsAutoRefresh); STATE.chatConfigsAutoRefresh = null; }
|
||||
}
|
||||
|
||||
function renderChatPlatformsPanel() {
|
||||
if (STATE.chatActivePlatform) { renderChatPlatformDetail(STATE.chatActivePlatform); return; }
|
||||
var panel = document.getElementById('panel-chatPlatforms');
|
||||
panel.innerHTML = '<div class="card"><div class="card-header"><span class="card-title">🔗 平台配置列表</span>' +
|
||||
'<button class="btn btn-sm btn-accent" onclick="showChatConfigForm()">+ 添加配置</button></div>' +
|
||||
'<div class="table-wrap"><table id="chat-configs-table"><thead><tr>' +
|
||||
'<th>平台</th><th>启用</th><th>连接</th><th>关键配置</th><th>更新时间</th><th>操作</th>' +
|
||||
'</tr></thead><tbody id="chat-configs-tbody">' +
|
||||
'<tr><td colspan="6"><div class="empty-state"><div class="icon">💬</div>加载中...</div></td></tr></tbody></table></div></div>';
|
||||
document.getElementById('panel-actions').innerHTML = '<button class="btn btn-sm" onclick="refreshChatConfigs()">🔄 刷新</button>';
|
||||
loadChatConfigs();
|
||||
// ---- 概览栏 ----
|
||||
|
||||
async function loadChatOverview() {
|
||||
var bar = document.getElementById('chat-overview-bar');
|
||||
if (!bar) return;
|
||||
// 并行获取平台状态 + 配置列表
|
||||
var [platResp, cfgResp] = await Promise.all([
|
||||
api('/api/chat-platforms/platforms').catch(function() { return { error: true }; }),
|
||||
api('/api/chat-platforms/configs').catch(function() { return { error: true }; })
|
||||
]);
|
||||
var platforms = platResp.platforms || [];
|
||||
var configs = cfgResp.configs || [];
|
||||
var bridgeDown = !!platResp.error;
|
||||
STATE.chatPlatforms = platforms;
|
||||
STATE.chatConfigs = configs;
|
||||
|
||||
var connected = 0, realCount = 0;
|
||||
platforms.forEach(function(p) {
|
||||
if (p.connected) connected++;
|
||||
if (PLATFORM_REAL[p.name]) realCount++;
|
||||
});
|
||||
|
||||
bar.innerHTML =
|
||||
'<div class="overview-stat"><span class="overview-stat-label">桥接服务</span>' +
|
||||
'<span class="overview-stat-value">' + (bridgeDown ? '<span style="color:var(--red)">离线</span>' : '<span style="color:var(--green)">运行中</span>') + '</span></div>' +
|
||||
'<div class="overview-stat"><span class="overview-stat-label">已连接</span>' +
|
||||
'<span class="overview-stat-value" style="color:' + (connected > 0 ? 'var(--green)' : 'var(--text2)') + '">' + connected + '/' + platforms.length + '</span></div>' +
|
||||
'<div class="overview-stat"><span class="overview-stat-label">已实现</span>' +
|
||||
'<span class="overview-stat-value">' + realCount + '/6 (3桩)</span></div>' +
|
||||
'<div class="overview-stat"><span class="overview-stat-label">身份映射</span>' +
|
||||
'<span class="overview-stat-value" id="ov-ident-count">—</span></div>';
|
||||
// 异步加载身份数量
|
||||
api('/api/chat-platforms/identities').then(function(d) {
|
||||
var el = document.getElementById('ov-ident-count');
|
||||
if (!el) return;
|
||||
var total = 0;
|
||||
if (d && !d.error) { for (var k in d) { if (d.hasOwnProperty(k)) total += d[k].length; } }
|
||||
el.textContent = total;
|
||||
}).catch(function() {});
|
||||
}
|
||||
|
||||
async function loadChatConfigs() {
|
||||
var data = await api('/api/chat-platforms/configs');
|
||||
var tbody = document.getElementById('chat-configs-tbody');
|
||||
if (!tbody) return;
|
||||
if (data.error) { tbody.innerHTML = '<tr><td colspan="6"><div class="empty-state"><div class="icon">⚠️</div>' + escHtml(data.error) + '</div></td></tr>'; return; }
|
||||
STATE.chatConfigs = data.configs || [];
|
||||
// ---- 列表视图 ----
|
||||
|
||||
function renderChatPlatformsPanel() {
|
||||
if (STATE.chatActivePlatform) { renderChatPlatformDetail(STATE.chatActivePlatform); return; }
|
||||
STATE.chatLogFilter = 'all';
|
||||
var panel = document.getElementById('panel-chatPlatforms');
|
||||
panel.innerHTML =
|
||||
'<div class="card" style="margin-bottom:14px"><div class="card-body" id="chat-overview-bar" style="display:flex;gap:24px;flex-wrap:wrap;padding:12px 16px">' +
|
||||
'<div class="empty-state">加载中...</div></div></div>' +
|
||||
'<div class="card"><div class="card-header"><span class="card-title">🔗 平台配置列表</span>' +
|
||||
'<button class="btn btn-sm btn-accent" onclick="showChatConfigForm()">+ 添加配置</button></div>' +
|
||||
'<div class="table-wrap"><table id="chat-configs-table"><thead><tr>' +
|
||||
'<th>平台</th><th>状态</th><th>能力</th><th>关键配置</th><th>更新时间</th><th>操作</th>' +
|
||||
'</tr></thead><tbody id="chat-configs-tbody">' +
|
||||
'<tr><td colspan="6"><div class="empty-state"><div class="icon">💬</div>加载中...</div></td></tr></tbody></table></div></div>';
|
||||
document.getElementById('panel-actions').innerHTML =
|
||||
'<button class="btn btn-sm" onclick="refreshChatAll()">🔄 刷新全部</button>' +
|
||||
'<button class="btn btn-sm" onclick="loadChatIdentities()">👤 身份映射</button>' +
|
||||
'<button class="btn btn-sm" onclick="showBlocklistSettings()">🚫 黑白名单</button>';
|
||||
loadChatOverview();
|
||||
renderChatConfigsTable();
|
||||
}
|
||||
|
||||
function refreshChatAll() {
|
||||
loadChatOverview();
|
||||
renderChatConfigsTable();
|
||||
}
|
||||
|
||||
function renderChatConfigsTable() {
|
||||
var tbody = document.getElementById('chat-configs-tbody');
|
||||
if (!tbody) return;
|
||||
// 优先使用刚从 loadChatOverview 拉到的数据
|
||||
var configs = STATE.chatConfigs;
|
||||
if (configs.length === 0) {
|
||||
if (!configs || configs.length === 0) {
|
||||
tbody.innerHTML = '<tr><td colspan="6"><div class="empty-state"><div class="icon">💬</div>暂无配置,点击「添加配置」创建</div></td></tr>';
|
||||
return;
|
||||
}
|
||||
tbody.innerHTML = configs.map(function(c) {
|
||||
var icon = PLATFORM_ICONS[c.name] || '🔗';
|
||||
var label = c.label || PLATFORM_LABELS[c.name] || c.name;
|
||||
var connBadge = c.connected ? '<span class="badge badge-running">已连接</span>' : '<span class="badge badge-stopped">未连接</span>';
|
||||
var enabledBadge = c.enabled !== false ? '<span class="badge badge-running">启用</span>' : '<span class="badge badge-stopped">禁用</span>';
|
||||
var ptype = c.platform || c.name;
|
||||
var icon = PLATFORM_ICONS[ptype] || '🔗';
|
||||
var pfx = (c.platform && c.platform !== c.name) ? ('<span style="color:var(--text3);font-size:10px">' + escHtml(c.platform) + '/</span>') : '';
|
||||
var label = c.label || (PLATFORM_LABELS[ptype] ? (PLATFORM_LABELS[ptype] + ' (' + escHtml(c.name) + ')') : c.name);
|
||||
var isReal = PLATFORM_REAL[ptype];
|
||||
var implBadge = isReal ? '<span class="badge badge-running" title="完整实现">✅</span>'
|
||||
: '<span class="badge badge-stopped" title="桩代码 (待开发)" style="opacity:.7">🔧 桩</span>';
|
||||
var connBadge = c.connected
|
||||
? '<span class="badge badge-running">● 已连接</span>'
|
||||
: '<span class="badge badge-stopped">○ 未连接</span>';
|
||||
var enabledBadge = c.enabled !== false
|
||||
? '<span class="badge badge-running">启用</span>'
|
||||
: '<span class="badge badge-stopped">禁用</span>';
|
||||
var keys = (c.fields && Object.keys(c.fields).length > 0)
|
||||
? Object.keys(c.fields).map(function(k) { return k + '=' + (c.fields[k] ? '***' : '(空)'); }).join(', ')
|
||||
: '—';
|
||||
var capsHTML = buildCapsHTML(ptype);
|
||||
if (!capsHTML) capsHTML = '<span style="color:var(--text3)">—</span>';
|
||||
var updated = c.updated_at ? timeAgo(c.updated_at) : '—';
|
||||
return '<tr>' +
|
||||
'<td><strong>' + icon + ' ' + escHtml(label) + '</strong></td>' +
|
||||
'<td>' + enabledBadge + '</td>' +
|
||||
'<td>' + connBadge + '</td>' +
|
||||
'<td style="max-width:200px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap">' + escHtml(keys) + '</td>' +
|
||||
return '<tr style="cursor:pointer" onclick="editChatConfig(\'' + escHtml(c.name) + '\')">' +
|
||||
'<td><strong>' + pfx + icon + ' ' + escHtml(label) + '</strong> ' + implBadge + '</td>' +
|
||||
'<td>' + enabledBadge + ' ' + connBadge + '</td>' +
|
||||
'<td style="font-size:11px">' + capsHTML + '</td>' +
|
||||
'<td style="max-width:180px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap">' + escHtml(keys) + '</td>' +
|
||||
'<td>' + updated + '</td>' +
|
||||
'<td><div class="btn-group">' +
|
||||
'<td><div class="btn-group" onclick="event.stopPropagation()">' +
|
||||
'<button class="btn btn-xs" onclick="editChatConfig(\'' + escHtml(c.name) + '\')">✏️ 编辑</button>' +
|
||||
'<button class="btn btn-xs btn-red" onclick="deleteChatConfig(\'' + escHtml(c.name) + '\')">🗑</button>' +
|
||||
'</div></td></tr>';
|
||||
}).join('');
|
||||
}
|
||||
|
||||
function refreshChatConfigs() { loadChatConfigs(); }
|
||||
function buildCapsHTML(name) {
|
||||
var caps = (STATE.chatCaps && STATE.chatCaps[name]) || PLATFORM_STATIC_CAPS[name];
|
||||
if (!caps) return '';
|
||||
var items = [];
|
||||
if (caps.supports_markdown) items.push('<span title="支持 Markdown">📝</span>');
|
||||
if (caps.supports_image) items.push('<span title="支持图片">🖼️</span>');
|
||||
if (caps.supports_voice) items.push('<span title="支持语音">🎤</span>');
|
||||
if (caps.supports_emoji) items.push('<span title="支持表情">😊</span>');
|
||||
if (caps.supports_reaction) items.push('<span title="支持回应">👍</span>');
|
||||
if (caps.supports_typing_hint) items.push('<span title="支持输入状态">⌨️</span>');
|
||||
items.push('<span style="color:var(--text3)">' + caps.max_message_length + '字</span>');
|
||||
return items.join(' ');
|
||||
}
|
||||
function refreshChatConfigs() { loadChatOverview(); renderChatConfigsTable(); }
|
||||
|
||||
function showChatConfigForm() {
|
||||
var panel = document.getElementById('panel-chatPlatforms');
|
||||
@@ -4260,58 +4435,165 @@ function showChatConfigForm() {
|
||||
'<button class="btn btn-sm" onclick="STATE.chatActivePlatform=null;renderChatPlatformsPanel();">← 取消</button></div>' +
|
||||
'<div class="cards-grid cards-3">' +
|
||||
options.map(function(p) {
|
||||
var isReal = PLATFORM_REAL[p];
|
||||
return '<div class="card" style="cursor:pointer;text-align:center;padding:20px" onclick="startNewConfig(\'' + p + '\')">' +
|
||||
'<div style="font-size:32px;margin-bottom:8px">' + (PLATFORM_ICONS[p] || '🔗') + '</div>' +
|
||||
'<div style="font-weight:600">' + (PLATFORM_LABELS[p] || p) + '</div></div>';
|
||||
'<div style="font-weight:600">' + (PLATFORM_LABELS[p] || p) + '</div>' +
|
||||
'<div style="font-size:10px;color:var(--text3);margin-top:4px">' + (isReal ? '完整实现' : '桩代码') + '</div></div>';
|
||||
}).join('') + '</div></div>';
|
||||
document.getElementById('panel-actions').innerHTML = '';
|
||||
}
|
||||
|
||||
function startNewConfig(name) { STATE.chatActivePlatform = name; renderChatPlatformsPanel(); }
|
||||
function startNewConfig(name) {
|
||||
if (name === 'qq') { showNewQQConfigDialog(); return; }
|
||||
STATE.chatActivePlatform = name;
|
||||
renderChatPlatformsPanel();
|
||||
}
|
||||
|
||||
function showNewQQConfigDialog() {
|
||||
var defaultName = 'qq-' + Date.now().toString(36);
|
||||
var panel = document.getElementById('panel-chatPlatforms');
|
||||
panel.innerHTML =
|
||||
'<div class="card"><div class="card-header"><span class="card-title">🐧 新建 QQ 配置</span>' +
|
||||
'<button class="btn btn-sm" onclick="STATE.chatActivePlatform=null;renderChatPlatformsPanel();">← 取消</button></div>' +
|
||||
'<div class="card-body">' +
|
||||
'<div class="form-group"><label>配置名称</label>' +
|
||||
'<input type="text" id="new-qq-name" value="' + defaultName + '" placeholder="如: qq-home, qq-work"></div>' +
|
||||
'<div class="form-group"><label>连接模式</label>' +
|
||||
'<select id="new-qq-mode" style="width:100%;padding:8px 12px;background:var(--bg3);color:var(--text);border:1px solid var(--border);border-radius:4px">' +
|
||||
'<option value="client">客户端模式 (主动连接 NapCat)</option>' +
|
||||
'<option value="server">服务端模式 (等待 NapCat 连接)</option></select></div>' +
|
||||
'<div class="btn-group" style="margin-top:12px">' +
|
||||
'<button class="btn btn-accent" onclick="createQQConfig()">创建配置</button></div></div></div>';
|
||||
document.getElementById('panel-actions').innerHTML = '';
|
||||
}
|
||||
|
||||
function createQQConfig() {
|
||||
var nameEl = document.getElementById('new-qq-name');
|
||||
var modeEl = document.getElementById('new-qq-mode');
|
||||
var name = (nameEl && nameEl.value.trim()) || ('qq-' + Date.now().toString(36));
|
||||
var mode = modeEl ? modeEl.value : 'client';
|
||||
var newCfg = { name: name, platform: 'qq', enabled: true, label: '', fields: { mode: mode }, connected: false };
|
||||
var configs = STATE.chatConfigs || [];
|
||||
configs.push(newCfg);
|
||||
STATE.chatConfigs = configs;
|
||||
STATE.chatActivePlatform = name;
|
||||
renderChatPlatformsPanel();
|
||||
}
|
||||
|
||||
function editChatConfig(name) {
|
||||
if (!STATE.chatConfigs.some(function(c) { return c.name === name; })) {
|
||||
STATE.chatActivePlatform = name;
|
||||
renderChatPlatformsPanel();
|
||||
} else {
|
||||
STATE.chatActivePlatform = name;
|
||||
renderChatPlatformsPanel();
|
||||
STATE.chatActivePlatform = name;
|
||||
STATE.chatLogFilter = 'all';
|
||||
renderChatPlatformsPanel();
|
||||
}
|
||||
|
||||
// ---- 平台详情页 ----
|
||||
|
||||
async function loadChatPlatformInfo(name) {
|
||||
var data = await api('/api/chat-platforms/platforms/' + encodeURIComponent(name)).catch(function() { return { error: true }; });
|
||||
STATE.chatBridgeDown = !!data.error;
|
||||
// 桥接服务离线提示
|
||||
var banner = document.getElementById('chat-bridge-banner');
|
||||
if (banner) {
|
||||
banner.innerHTML = data.error
|
||||
? '<div class="card" style="border-color:var(--yellow);background:var(--yellow-bg);padding:12px 16px">' +
|
||||
'<strong>⚠️ 平台桥接服务未运行</strong> — 以下配置和日志依赖 platform-bridge 服务。' +
|
||||
'请在 <a href="javascript:switchPanel(\'services\')" style="color:var(--accent);text-decoration:underline">服务管理</a> 中启动 platform-bridge 或将其加入 docker-compose.yml。</div>'
|
||||
: '';
|
||||
}
|
||||
// 更新能力缓存
|
||||
STATE.chatCaps = STATE.chatCaps || {};
|
||||
if (!data.error) {
|
||||
STATE.chatCaps[name] = data.capabilities;
|
||||
STATE.chatPlatformInfo = STATE.chatPlatformInfo || {};
|
||||
STATE.chatPlatformInfo[name] = data;
|
||||
}
|
||||
// 更新页面中的状态指示
|
||||
var statusEl = document.getElementById('platform-status-indicator');
|
||||
if (statusEl && !data.error) {
|
||||
var conn = data.connected;
|
||||
statusEl.innerHTML = conn
|
||||
? '<span class="badge badge-running">● 已连接</span>'
|
||||
: '<span class="badge badge-stopped">○ 未连接</span>';
|
||||
}
|
||||
var cfgForCaps = (STATE.chatConfigs || []).find(function(c) { return c.name === name; }) || null;
|
||||
var ptypeForCaps = (cfgForCaps && cfgForCaps.platform) || name;
|
||||
var capsHTML = buildCapsHTML(ptypeForCaps);
|
||||
var capsEl = document.getElementById('platform-caps');
|
||||
if (capsEl) capsEl.innerHTML = capsHTML || '<span style="color:var(--text3)">—</span>';
|
||||
}
|
||||
|
||||
function renderChatPlatformDetail(name) {
|
||||
var cfg = null;
|
||||
for (var i = 0; i < STATE.chatConfigs.length; i++) {
|
||||
if (STATE.chatConfigs[i].name === name) { cfg = STATE.chatConfigs[i]; break; }
|
||||
}
|
||||
var icon = PLATFORM_ICONS[name] || '🔗';
|
||||
var cfg = (STATE.chatConfigs || []).find(function(c) { return c.name === name; }) || null;
|
||||
STATE.chatLogFilter = STATE.chatLogFilter || 'all';
|
||||
var ptype = (cfg && cfg.platform) || name;
|
||||
var icon = PLATFORM_ICONS[ptype] || '🔗';
|
||||
var isReal = PLATFORM_REAL[ptype];
|
||||
var panel = document.getElementById('panel-chatPlatforms');
|
||||
var logLimit = STATE.chatLogLimit || 100;
|
||||
var filterOpts = '<option value="all"' + (STATE.chatLogFilter === 'all' ? ' selected' : '') + '>全部</option>' +
|
||||
'<option value="incoming"' + (STATE.chatLogFilter === 'incoming' ? ' selected' : '') + '>← 收到</option>' +
|
||||
'<option value="outgoing"' + (STATE.chatLogFilter === 'outgoing' ? ' selected' : '') + '>→ 发送</option>' +
|
||||
'<option value="error"' + (STATE.chatLogFilter === 'error' ? ' selected' : '') + '>⚠ 错误</option>';
|
||||
panel.innerHTML =
|
||||
'<div style="margin-bottom:14px"><button class="btn btn-sm" onclick="STATE.chatActivePlatform=null;renderChatPlatformsPanel();">← 返回列表</button></div>' +
|
||||
'<div class="card"><div class="card-header"><span class="card-title">' + icon + ' ' + escHtml(name) + ' 配置</span><span id="cfg-save-status"></span></div>' +
|
||||
'<div id="chat-bridge-banner" style="margin-bottom:14px"></div>' +
|
||||
// 状态卡片
|
||||
'<div class="card" style="margin-bottom:14px"><div class="card-header"><span class="card-title">' + icon + ' ' + (ptype !== name ? escHtml(ptype) + '/' : '') + escHtml(name) + ' 状态</span></div>' +
|
||||
'<div class="card-body" style="display:flex;gap:24px;flex-wrap:wrap;align-items:center">' +
|
||||
'<div><span style="color:var(--text3)">连接: </span><span id="platform-status-indicator">' +
|
||||
(cfg && cfg.connected ? '<span class="badge badge-running">● 已连接</span>' : '<span class="badge badge-stopped">○ 未连接</span>') + '</span></div>' +
|
||||
'<div><span style="color:var(--text3)">实现: </span>' + (isReal ? '<span class="badge badge-running">完整实现</span>' : '<span class="badge badge-stopped" style="opacity:.7">🔧 桩代码 (待开发)</span>') + '</div>' +
|
||||
'<div><span style="color:var(--text3)">能力: </span><span id="platform-caps">' + (buildCapsHTML(ptype) || '<span style="color:var(--text3)">—</span>') + '</span></div>' +
|
||||
'<div style="margin-left:auto"><button class="btn btn-xs" onclick="loadChatPlatformInfo(\'' + escHtml(name) + '\')">🔄 刷新状态</button></div>' +
|
||||
'</div></div>' +
|
||||
// 配置 + 身份映射 并排
|
||||
'<div style="display:grid;grid-template-columns:1fr 1fr;gap:14px;margin-bottom:14px">' +
|
||||
'<div class="card"><div class="card-header"><span class="card-title">⚙️ 配置</span><span id="cfg-save-status"></span></div>' +
|
||||
'<div class="card-body" id="chat-config-form"></div></div>' +
|
||||
'<div class="card" style="margin-top:14px"><div class="card-header"><span class="card-title">📋 消息日志 (最近 ' + STATE.chatLogLimit + ' 条)</span>' +
|
||||
'<div class="card" id="chat-identity-card"><div class="card-header"><span class="card-title">👤 身份映射</span></div>' +
|
||||
'<div class="card-body" id="chat-identity-body"><div class="empty-state">加载中...</div></div></div>' +
|
||||
'</div>' +
|
||||
// 消息日志
|
||||
'<div class="card"><div class="card-header"><span class="card-title">📋 消息日志 (最近 ' + logLimit + ' 条)</span>' +
|
||||
'<div class="btn-group">' +
|
||||
'<button class="btn btn-xs" onclick="refreshChatLogs(\'' + escHtml(name) + '\')">🔄 刷新</button>' +
|
||||
'<select id="chat-log-filter" onchange="STATE.chatLogFilter=this.value;refreshChatLogs(\'' + escHtml(name) + '\')" ' +
|
||||
'style="width:auto;padding:4px 8px;font-size:11px;background:var(--bg3);color:var(--text);border:1px solid var(--border);border-radius:4px">' + filterOpts + '</select>' +
|
||||
'<select id="chat-log-limit" onchange="STATE.chatLogLimit=parseInt(this.value);refreshChatLogs(\'' + escHtml(name) + '\')" ' +
|
||||
'style="width:auto;padding:4px 8px;font-size:11px;background:var(--bg3);color:var(--text);border:1px solid var(--border);border-radius:4px">' +
|
||||
'<option value="50">50条</option><option value="100" selected>100条</option><option value="200">200条</option><option value="500">500条</option></select></div></div>' +
|
||||
'<option value="50"' + (logLimit === 50 ? ' selected' : '') + '>50条</option><option value="100"' + (logLimit === 100 ? ' selected' : '') + '>100条</option><option value="200"' + (logLimit === 200 ? ' selected' : '') + '>200条</option><option value="500"' + (logLimit === 500 ? ' selected' : '') + '>500条</option></select>' +
|
||||
'<button class="btn btn-xs" onclick="refreshChatLogs(\'' + escHtml(name) + '\')">🔄 刷新</button></div></div>' +
|
||||
'<div id="chat-log-container" style="max-height:400px;overflow-y:auto;background:var(--bg);border:1px solid var(--border);border-radius:var(--radius-sm);padding:8px">' +
|
||||
'<div class="empty-state"><div class="icon">📝</div>加载中...</div></div></div>';
|
||||
document.getElementById('panel-actions').innerHTML = '';
|
||||
renderChatConfigForm(name, cfg);
|
||||
loadChatPlatformInfo(name);
|
||||
loadChatIdentitiesForPlatform(name);
|
||||
refreshChatLogs(name);
|
||||
}
|
||||
|
||||
function renderChatConfigForm(name, cfg) {
|
||||
var fields = PLATFORM_FIELDS[name] || [];
|
||||
var platformType = (cfg && cfg.platform) || name;
|
||||
var fields = PLATFORM_FIELDS[platformType] || [];
|
||||
var container = document.getElementById('chat-config-form');
|
||||
if (!container) return;
|
||||
var currentFields = (cfg && cfg.fields) || {};
|
||||
var enabled = cfg ? (cfg.enabled !== false) : true;
|
||||
var fieldsHTML = fields.map(function(f) {
|
||||
var val = currentFields[f.key] || '';
|
||||
return '<div class="form-group"><label>' + escHtml(f.label) + '</label>' +
|
||||
var display = '';
|
||||
if (f.showIf) {
|
||||
var condVal = currentFields[f.showIf.key] || '';
|
||||
if (condVal !== f.showIf.value) display = 'display:none';
|
||||
}
|
||||
if (f.type === 'select') {
|
||||
return '<div class="form-group" style="' + display + '" data-cond="' + escHtml(f.showIf ? f.showIf.key + ':' + f.showIf.value : '') + '"><label>' + escHtml(f.label) + '</label>' +
|
||||
'<select id="cfg-field-' + escHtml(f.key) + '" onchange="onCfgFieldChange(\'' + escHtml(name) + '\')" style="width:100%;padding:8px 12px;background:var(--bg3);color:var(--text);border:1px solid var(--border);border-radius:4px">' +
|
||||
(f.options || []).map(function(o) {
|
||||
return '<option value="' + escHtml(o.value) + '"' + (val === o.value ? ' selected' : '') + '>' + escHtml(o.label) + '</option>';
|
||||
}).join('') + '</select></div>';
|
||||
}
|
||||
return '<div class="form-group" style="' + display + '" data-cond="' + escHtml(f.showIf ? f.showIf.key + ':' + f.showIf.value : '') + '"><label>' + escHtml(f.label) + '</label>' +
|
||||
'<input type="text" id="cfg-field-' + escHtml(f.key) + '" value="' + escHtml(val) + '" placeholder="' + escHtml(f.placeholder || '') + '"></div>';
|
||||
}).join('');
|
||||
container.innerHTML =
|
||||
@@ -4323,9 +4605,26 @@ function renderChatConfigForm(name, cfg) {
|
||||
'<div class="btn-group" style="margin-top:12px"><button class="btn btn-sm btn-accent" onclick="saveChatConfig(\'' + escHtml(name) + '\')">💾 保存配置</button></div>';
|
||||
}
|
||||
|
||||
function onCfgFieldChange(name) {
|
||||
var cfg = (STATE.chatConfigs || []).find(function(c) { return c.name === name; }) || null;
|
||||
var platformType = (cfg && cfg.platform) || name;
|
||||
var fieldDefs = PLATFORM_FIELDS[platformType] || [];
|
||||
var tempFields = {};
|
||||
fieldDefs.forEach(function(f) {
|
||||
var el = document.getElementById('cfg-field-' + f.key);
|
||||
if (el) tempFields[f.key] = el.value;
|
||||
});
|
||||
var enabledEl = document.getElementById('cfg-field-enabled');
|
||||
var labelEl = document.getElementById('cfg-field-label');
|
||||
var tempCfg = { name: name, platform: (cfg && cfg.platform) || name, fields: tempFields, enabled: enabledEl ? enabledEl.checked : true, label: labelEl ? labelEl.value : '' };
|
||||
renderChatConfigForm(name, tempCfg);
|
||||
}
|
||||
|
||||
async function saveChatConfig(name) {
|
||||
var cfg = (STATE.chatConfigs || []).find(function(c) { return c.name === name; }) || null;
|
||||
var platformType = (cfg && cfg.platform) || name;
|
||||
var fields = {};
|
||||
var fieldDefs = PLATFORM_FIELDS[name] || [];
|
||||
var fieldDefs = PLATFORM_FIELDS[platformType] || [];
|
||||
fieldDefs.forEach(function(f) {
|
||||
var el = document.getElementById('cfg-field-' + f.key);
|
||||
if (el) fields[f.key] = el.value;
|
||||
@@ -4336,11 +4635,17 @@ async function saveChatConfig(name) {
|
||||
var label = labelEl ? labelEl.value : '';
|
||||
var data = await api('/api/chat-platforms/configs/' + encodeURIComponent(name), {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({ name: name, enabled: enabled, label: label, fields: fields })
|
||||
body: JSON.stringify({ name: name, platform: platformType, enabled: enabled, label: label, fields: fields })
|
||||
});
|
||||
if (data.error) { showToast('保存失败: ' + data.error, 'error'); return; }
|
||||
showToast('配置已保存 (需重启平台桥接服务生效)', 'success');
|
||||
await loadChatConfigs();
|
||||
showToast('配置已保存 (实时生效)', 'success');
|
||||
// Update STATE with saved config so re-render preserves form inputs.
|
||||
var oldCfg = (STATE.chatConfigs || []).find(function(c) { return c.name === name; }) || null;
|
||||
var savedCfg = { name: name, platform: platformType, enabled: enabled, label: label, fields: fields, connected: (oldCfg && oldCfg.connected) || false };
|
||||
var configs = STATE.chatConfigs || [];
|
||||
var idx = configs.findIndex(function(c) { return c.name === name; });
|
||||
if (idx >= 0) { configs[idx] = savedCfg; } else { configs.push(savedCfg); }
|
||||
STATE.chatConfigs = configs;
|
||||
renderChatPlatformDetail(name);
|
||||
}
|
||||
|
||||
@@ -4350,31 +4655,202 @@ async function deleteChatConfig(name) {
|
||||
if (data.error) { showToast('删除失败: ' + data.error, 'error'); return; }
|
||||
showToast('配置已删除', 'success');
|
||||
STATE.chatActivePlatform = null;
|
||||
await loadChatConfigs();
|
||||
loadChatOverview();
|
||||
renderChatPlatformsPanel();
|
||||
}
|
||||
|
||||
// ---- 身份映射 ----
|
||||
|
||||
async function loadChatIdentities() {
|
||||
var data = await api('/api/chat-platforms/identities').catch(function() { return { error: true }; });
|
||||
STATE.chatIdentities = data;
|
||||
// 以弹窗形式展示
|
||||
var html = '<div class="card"><div class="card-header"><span class="card-title">👤 身份映射列表</span>' +
|
||||
'<button class="btn btn-sm" onclick="this.closest(\'.card\').remove()">✕ 关闭</button></div>' +
|
||||
'<div class="card-body">';
|
||||
if (data.error) {
|
||||
html += '<div class="empty-state"><div class="icon">⚠️</div>桥接服务不可达</div>';
|
||||
} else {
|
||||
var total = 0;
|
||||
for (var plat in data) { if (data.hasOwnProperty(plat)) total += data[plat].length; }
|
||||
if (total === 0) {
|
||||
html += '<div class="empty-state"><div class="icon">👤</div>暂无身份映射 (在 platform-bridge 启动时通过环境变量 QQ_ADMIN_UID / TELEGRAM_ADMIN_UID 预设)</div>';
|
||||
} else {
|
||||
html += '<div class="table-wrap"><table><thead><tr><th>平台</th><th>平台UID</th><th>Cyrene用户</th><th>昵称</th><th>权限级别</th></tr></thead><tbody>';
|
||||
for (var plat in data) {
|
||||
if (!data.hasOwnProperty(plat)) continue;
|
||||
(data[plat] || []).forEach(function(id) {
|
||||
var permBadge = id.permission_level === 'admin'
|
||||
? '<span class="badge badge-running">管理员</span>'
|
||||
: '<span class="badge">' + escHtml(id.permission_level || '—') + '</span>';
|
||||
html += '<tr><td>' + (PLATFORM_ICONS[plat] || '') + ' ' + escHtml(plat) + '</td>' +
|
||||
'<td><code>' + escHtml(id.platform_uid) + '</code></td>' +
|
||||
'<td>' + escHtml(id.cyrene_user_id) + '</td>' +
|
||||
'<td>' + escHtml(id.nickname || '—') + '</td>' +
|
||||
'<td>' + permBadge + '</td></tr>';
|
||||
});
|
||||
}
|
||||
html += '</tbody></table></div>';
|
||||
}
|
||||
}
|
||||
html += '</div></div>';
|
||||
// 插入到列表上方
|
||||
var panel = document.getElementById('panel-chatPlatforms');
|
||||
var existing = panel.querySelector('.card:first-child');
|
||||
var div = document.createElement('div');
|
||||
div.style.cssText = 'margin-bottom:14px';
|
||||
div.innerHTML = html;
|
||||
panel.insertBefore(div, existing);
|
||||
}
|
||||
|
||||
async function loadChatIdentitiesForPlatform(name) {
|
||||
var bodyEl = document.getElementById('chat-identity-body');
|
||||
if (!bodyEl) return;
|
||||
var cfg = (STATE.chatConfigs || []).find(function(c) { return c.name === name; }) || null;
|
||||
var ptype = (cfg && cfg.platform) || name;
|
||||
var data = await api('/api/chat-platforms/identities').catch(function() { return { error: true }; });
|
||||
if (data.error) {
|
||||
bodyEl.innerHTML = '<div class="empty-state"><div class="icon">⚠️</div>桥接服务不可达</div>';
|
||||
return;
|
||||
}
|
||||
var identities = data[ptype] || [];
|
||||
STATE.chatIdentities = data;
|
||||
if (identities.length === 0) {
|
||||
bodyEl.innerHTML = '<div class="empty-state" style="padding:20px"><div class="icon">👤</div>暂无此平台的身份映射' +
|
||||
'<div style="font-size:11px;color:var(--text3);margin-top:4px">通过环境变量 ' + ptype.toUpperCase() + '_ADMIN_UID 预设管理员身份</div></div>';
|
||||
return;
|
||||
}
|
||||
bodyEl.innerHTML = '<div style="max-height:200px;overflow-y:auto">' +
|
||||
identities.map(function(id) {
|
||||
var permBadge = id.permission_level === 'admin'
|
||||
? '<span class="badge badge-running">管理员</span>'
|
||||
: '<span class="badge">' + escHtml(id.permission_level || '—') + '</span>';
|
||||
return '<div style="padding:8px;border-bottom:1px solid var(--border);font-size:12px;display:flex;justify-content:space-between;align-items:center">' +
|
||||
'<div><code>' + escHtml(id.platform_uid) + '</code> → <strong>' + escHtml(id.cyrene_user_id) + '</strong>' +
|
||||
(id.nickname ? ' (' + escHtml(id.nickname) + ')' : '') + '</div>' +
|
||||
'<div>' + permBadge + '</div></div>';
|
||||
}).join('') + '</div>';
|
||||
}
|
||||
|
||||
// ---- 黑名单/白名单设置 ----
|
||||
|
||||
async function showBlocklistSettings() {
|
||||
var panel = document.getElementById('panel-chatPlatforms');
|
||||
var data = await api('/api/chat-platforms/settings/blocklist').catch(function() { return null; });
|
||||
var settings = (data && !data.error) ? data : { mode: 'blacklist', group_ids: [], user_ids: [] };
|
||||
STATE._blocklistSettings = settings;
|
||||
var groupIDs = (settings.group_ids || []).join('\n');
|
||||
var userIDs = (settings.user_ids || []).join('\n');
|
||||
var div = document.createElement('div');
|
||||
div.id = 'blocklist-settings-card';
|
||||
div.style.cssText = 'margin-bottom:14px';
|
||||
div.innerHTML =
|
||||
'<div class="card"><div class="card-header"><span class="card-title">🚫 黑名单/白名单设置</span>' +
|
||||
'<button class="btn btn-sm" onclick="hideBlocklistSettings()">✕ 关闭</button></div>' +
|
||||
'<div class="card-body">' +
|
||||
'<div class="form-group"><label>模式</label>' +
|
||||
'<select id="blocklist-mode" style="width:100%;padding:8px 12px;background:var(--bg3);color:var(--text);border:1px solid var(--border);border-radius:4px">' +
|
||||
'<option value="blacklist"' + (settings.mode === 'blacklist' ? ' selected' : '') + '>黑名单模式 (屏蔽列表中的群号/用户)</option>' +
|
||||
'<option value="whitelist"' + (settings.mode === 'whitelist' ? ' selected' : '') + '>白名单模式 (仅回复列表中的群号/用户)</option>' +
|
||||
'</select></div>' +
|
||||
'<div style="color:var(--text3);font-size:11px;margin-bottom:12px">' +
|
||||
(settings.mode === 'blacklist'
|
||||
? '黑名单模式: 不对名单内的群号或私聊用户进行回复,但消息仍会显示在日志中'
|
||||
: '白名单模式: 只对白名单内的群号和私聊用户进行回复,消息仍会显示在日志中') +
|
||||
'</div>' +
|
||||
'<div style="display:grid;grid-template-columns:1fr 1fr;gap:14px">' +
|
||||
'<div class="form-group"><label>群号列表 (每行一个)</label>' +
|
||||
'<textarea id="blocklist-group-ids" rows="6" style="width:100%;padding:8px;background:var(--bg3);color:var(--text);border:1px solid var(--border);border-radius:4px;font-family:monospace;font-size:12px">' + escHtml(groupIDs) + '</textarea></div>' +
|
||||
'<div class="form-group"><label>私聊用户ID列表 (每行一个)</label>' +
|
||||
'<textarea id="blocklist-user-ids" rows="6" style="width:100%;padding:8px;background:var(--bg3);color:var(--text);border:1px solid var(--border);border-radius:4px;font-family:monospace;font-size:12px">' + escHtml(userIDs) + '</textarea></div>' +
|
||||
'</div>' +
|
||||
'<div class="btn-group" style="margin-top:12px">' +
|
||||
'<button class="btn btn-sm btn-accent" onclick="saveBlocklistSettings()">💾 保存设置</button>' +
|
||||
'<span id="blocklist-save-status" style="margin-left:8px"></span></div>' +
|
||||
'</div></div>';
|
||||
var existing = panel.querySelector('#blocklist-settings-card');
|
||||
if (existing) existing.remove();
|
||||
var firstChild = panel.firstChild;
|
||||
if (firstChild) { panel.insertBefore(div, firstChild); } else { panel.appendChild(div); }
|
||||
// 监听模式切换更新提示文字
|
||||
var modeEl = document.getElementById('blocklist-mode');
|
||||
if (modeEl) {
|
||||
modeEl.addEventListener('change', function() {
|
||||
var hint = this.value === 'blacklist'
|
||||
? '黑名单模式: 不对名单内的群号或私聊用户进行回复,但消息仍会显示在日志中'
|
||||
: '白名单模式: 只对白名单内的群号和私聊用户进行回复,消息仍会显示在日志中';
|
||||
var next = this.parentElement.parentElement.querySelector('div[style]');
|
||||
if (next) next.textContent = hint;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function hideBlocklistSettings() {
|
||||
var card = document.getElementById('blocklist-settings-card');
|
||||
if (card) card.remove();
|
||||
}
|
||||
|
||||
async function saveBlocklistSettings() {
|
||||
var modeEl = document.getElementById('blocklist-mode');
|
||||
var groupEl = document.getElementById('blocklist-group-ids');
|
||||
var userEl = document.getElementById('blocklist-user-ids');
|
||||
var statusEl = document.getElementById('blocklist-save-status');
|
||||
var mode = modeEl ? modeEl.value : 'blacklist';
|
||||
var groupIDs = (groupEl ? groupEl.value : '').split('\n').map(function(s) { return s.trim(); }).filter(function(s) { return s !== ''; });
|
||||
var userIDs = (userEl ? userEl.value : '').split('\n').map(function(s) { return s.trim(); }).filter(function(s) { return s !== ''; });
|
||||
var data = await api('/api/chat-platforms/settings/blocklist', {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({ mode: mode, group_ids: groupIDs, user_ids: userIDs })
|
||||
});
|
||||
if (data.error) {
|
||||
if (statusEl) { statusEl.innerHTML = '<span style="color:var(--red)">保存失败: ' + escHtml(data.error) + '</span>'; }
|
||||
return;
|
||||
}
|
||||
if (statusEl) { statusEl.innerHTML = '<span style="color:var(--green)">已保存</span>'; }
|
||||
STATE._blocklistSettings = data.settings || { mode: mode, group_ids: groupIDs, user_ids: userIDs };
|
||||
showToast('黑名单/白名单设置已保存', 'success');
|
||||
}
|
||||
|
||||
// ---- 消息日志 ----
|
||||
|
||||
async function refreshChatLogs(name) {
|
||||
var limit = STATE.chatLogLimit || 100;
|
||||
var filter = STATE.chatLogFilter || 'all';
|
||||
var data = await api('/api/chat-platforms/logs/' + encodeURIComponent(name) + '?limit=' + limit);
|
||||
var container = document.getElementById('chat-log-container');
|
||||
if (!container) return;
|
||||
if (data.error) { container.innerHTML = '<div class="empty-state"><div class="icon">⚠️</div>' + escHtml(data.error) + '</div>'; return; }
|
||||
var logs = data.logs || [];
|
||||
// 应用过滤
|
||||
if (filter === 'incoming') logs = logs.filter(function(l) { return l.direction === 'incoming'; });
|
||||
else if (filter === 'outgoing') logs = logs.filter(function(l) { return l.direction === 'outgoing'; });
|
||||
else if (filter === 'error') logs = logs.filter(function(l) { return l.error || l.success === false; });
|
||||
STATE.chatLogs = STATE.chatLogs || {};
|
||||
STATE.chatLogs[name] = logs;
|
||||
if (logs.length === 0) { container.innerHTML = '<div class="empty-state"><div class="icon">📝</div>暂无消息日志</div>'; return; }
|
||||
if (logs.length === 0) { container.innerHTML = '<div class="empty-state"><div class="icon">📝</div>暂无匹配的消息日志</div>'; return; }
|
||||
container.innerHTML = logs.map(function(l) {
|
||||
var arrow = l.direction === 'incoming' ? '← 收到' : '→ 发送';
|
||||
var color = l.direction === 'incoming' ? 'var(--blue)' : 'var(--green)';
|
||||
var time = new Date(l.timestamp).toLocaleString('zh-CN', { hour12: false });
|
||||
var content = (l.content || '').length > 300 ? (l.content || '').substring(0, 297) + '...' : (l.content || '');
|
||||
var errorTag = (l.error || l.success === false)
|
||||
? ' <span style="color:var(--red);cursor:help" title="' + escHtml(l.error || '发送失败') + '">⚠</span>' : '';
|
||||
// Build sender info: name (id).
|
||||
var sender = escHtml(l.sender_name || l.sender_id || '-');
|
||||
if (l.sender_name && l.sender_id && l.sender_name !== l.sender_id) {
|
||||
sender = escHtml(l.sender_name) + ' <span style="color:var(--text3);font-size:10px">(' + escHtml(l.sender_id) + ')</span>';
|
||||
}
|
||||
// Build channel context tag for group messages.
|
||||
var ctxTag = '';
|
||||
if (l.channel_id && l.channel_id.indexOf('private_') !== 0 && l.direction === 'incoming') {
|
||||
ctxTag = ' <span style="color:var(--text3);font-size:10px">[群:' + escHtml(l.channel_id) + ']</span> ';
|
||||
}
|
||||
return '<div style="padding:6px 10px;border-bottom:1px solid var(--border);font-size:12px">' +
|
||||
'<span style="color:' + color + ';font-weight:600">' + arrow + '</span> ' +
|
||||
'<span style="color:var(--text3)">' + time + '</span> ' +
|
||||
'<span style="color:var(--text2)">[' + escHtml(l.sender_name || l.sender_id || '-') + ']</span> ' +
|
||||
'<span>' + escHtml(content) + '</span>' +
|
||||
(l.error ? ' <span style="color:var(--red)">⚠ ' + escHtml(l.error) + '</span>' : '') +
|
||||
ctxTag +
|
||||
'<span style="color:var(--text2)">' + sender + '</span> ' +
|
||||
'<span>' + escHtml(content) + '</span>' + errorTag +
|
||||
'</div>';
|
||||
}).join('');
|
||||
}
|
||||
|
||||
@@ -79,6 +79,67 @@ processManager.on('log', (serviceId, stream, text) => {
|
||||
}
|
||||
});
|
||||
|
||||
// ========== 平台桥接实时日志流 ==========
|
||||
|
||||
let logStreamWs = null;
|
||||
let logStreamReconnectTimer = null;
|
||||
|
||||
function connectPlatformBridgeLogStream() {
|
||||
if (logStreamReconnectTimer) { clearTimeout(logStreamReconnectTimer); logStreamReconnectTimer = null; }
|
||||
if (logStreamWs && (logStreamWs.readyState === WebSocket.OPEN || logStreamWs.readyState === WebSocket.CONNECTING)) return;
|
||||
|
||||
const wsUrl = PLATFORM_BRIDGE_URL.replace(/^http/, 'ws') + '/ws/logs';
|
||||
console.log(`[LogStream] 连接 ${wsUrl} ...`);
|
||||
try {
|
||||
logStreamWs = new WebSocket(wsUrl);
|
||||
} catch (err) {
|
||||
console.error(`[LogStream] 创建连接失败: ${err.message}`);
|
||||
scheduleLogStreamReconnect();
|
||||
return;
|
||||
}
|
||||
|
||||
logStreamWs.on('open', () => {
|
||||
console.log('[LogStream] 已连接,实时日志推送中');
|
||||
});
|
||||
|
||||
logStreamWs.on('message', (raw) => {
|
||||
try {
|
||||
const entry = JSON.parse(raw.toString());
|
||||
broadcast('chat-log', entry);
|
||||
} catch {}
|
||||
});
|
||||
|
||||
logStreamWs.on('close', () => {
|
||||
console.log('[LogStream] 连接断开');
|
||||
logStreamWs = null;
|
||||
scheduleLogStreamReconnect();
|
||||
});
|
||||
|
||||
logStreamWs.on('error', (err) => {
|
||||
console.error(`[LogStream] 错误: ${err.message}`);
|
||||
logStreamWs = null;
|
||||
scheduleLogStreamReconnect();
|
||||
});
|
||||
}
|
||||
|
||||
function scheduleLogStreamReconnect() {
|
||||
if (logStreamReconnectTimer) return;
|
||||
logStreamReconnectTimer = setTimeout(() => {
|
||||
logStreamReconnectTimer = null;
|
||||
connectPlatformBridgeLogStream();
|
||||
}, 5000);
|
||||
}
|
||||
|
||||
// 启动时连接,后续 platform-bridge 重启时通过状态变化自动重连。
|
||||
connectPlatformBridgeLogStream();
|
||||
|
||||
// 监听服务状态:platform-bridge 上线后重连。
|
||||
setInterval(() => {
|
||||
if (!logStreamWs || logStreamWs.readyState === WebSocket.CLOSED || logStreamWs.readyState === WebSocket.CLOSING) {
|
||||
connectPlatformBridgeLogStream();
|
||||
}
|
||||
}, 15000);
|
||||
|
||||
// ========== Gateway 代理辅助函数 ==========
|
||||
|
||||
/** 缓存的 JWT token 和过期时间 */
|
||||
@@ -740,6 +801,47 @@ app.get('/api/chat-platforms/logs/:name', async (req, res) => {
|
||||
res.status(result.status).json(result.body);
|
||||
});
|
||||
|
||||
// GET /api/chat-platforms/platforms — 列出所有平台适配器 (含连接状态与能力)
|
||||
app.get('/api/chat-platforms/platforms', async (_req, res) => {
|
||||
const result = await proxyToPlatformBridge('/api/v1/platforms');
|
||||
res.status(result.status).json(result.body);
|
||||
});
|
||||
|
||||
// GET /api/chat-platforms/platforms/:name — 获取单个平台适配器详情
|
||||
app.get('/api/chat-platforms/platforms/:name', async (req, res) => {
|
||||
const result = await proxyToPlatformBridge(`/api/v1/platforms/${req.params.name}`);
|
||||
res.status(result.status).json(result.body);
|
||||
});
|
||||
|
||||
// GET /api/chat-platforms/identities — 列出所有已注册的身份映射
|
||||
app.get('/api/chat-platforms/identities', async (_req, res) => {
|
||||
const result = await proxyToPlatformBridge('/api/v1/identities');
|
||||
res.status(result.status).json(result.body);
|
||||
});
|
||||
|
||||
// GET /api/chat-platforms/health — platform-bridge 整体健康状态
|
||||
app.get('/api/chat-platforms/health', async (_req, res) => {
|
||||
const result = await proxyToPlatformBridge('/health');
|
||||
res.status(result.status).json(result.body);
|
||||
});
|
||||
|
||||
// ---- 黑名单/白名单设置代理 ----
|
||||
|
||||
// GET /api/chat-platforms/settings/blocklist
|
||||
app.get('/api/chat-platforms/settings/blocklist', async (_req, res) => {
|
||||
const result = await proxyToPlatformBridge('/api/v1/settings/blocklist');
|
||||
res.status(result.status).json(result.body);
|
||||
});
|
||||
|
||||
// POST /api/chat-platforms/settings/blocklist
|
||||
app.post('/api/chat-platforms/settings/blocklist', async (req, res) => {
|
||||
const result = await proxyToPlatformBridge('/api/v1/settings/blocklist', {
|
||||
method: 'POST',
|
||||
body: JSON.stringify(req.body),
|
||||
});
|
||||
res.status(result.status).json(result.body);
|
||||
});
|
||||
|
||||
// ---- 多端客户端管理代理 (转发到 Gateway) ----
|
||||
|
||||
// GET /api/clients — 获取已知客户端列表
|
||||
|
||||
Reference in New Issue
Block a user