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:
2026-05-31 09:37:18 +08:00
parent 71f0a1abdb
commit 47dce276a4
22 changed files with 2375 additions and 313 deletions
+541 -65
View File
@@ -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('');
}
+102
View File
@@ -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 — 获取已知客户端列表