feat: 主对话仅限管理员访问 + 记忆面板按时间排序与话题关联
- chat_handler.go: HandleWebSocket 新增 admin_ 前缀检查,非管理员返回 403 - index.html: 记忆面板新增时间排序下拉框和话题(会话)列 - renderMemoryResults 拆分为缓存+排序渲染两阶段 - 修复: 修复 typo 'seession_id' -> 'session_id'
This commit is contained in:
@@ -66,6 +66,16 @@ func (h *ChatHandler) HandleWebSocket(c *gin.Context) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 主对话仅限管理员访问
|
||||||
|
if !strings.HasPrefix(userID, "admin_") {
|
||||||
|
c.JSON(http.StatusForbidden, gin.H{
|
||||||
|
"error": "主对话仅限管理员使用",
|
||||||
|
"errorType": "admin_only",
|
||||||
|
"hint": "请使用管理员账号 (admin_ 前缀) 登录以访问主对话功能",
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
if sessionID == "" {
|
if sessionID == "" {
|
||||||
sessionID = "session_" + generateID()
|
sessionID = "session_" + generateID()
|
||||||
}
|
}
|
||||||
|
|||||||
+93
-26
@@ -368,11 +368,11 @@ const STATE = {
|
|||||||
serviceStatus: {},
|
serviceStatus: {},
|
||||||
// 日志
|
// 日志
|
||||||
activeLogTab: 'ai-core',
|
activeLogTab: 'ai-core',
|
||||||
logLines: { 'ai-core': [], 'gateway': [], 'frontend': [] },
|
logLines: { 'ai-core': [], 'gateway': [], 'frontend': [], 'iot-debug-service': [] },
|
||||||
maxLogLines: 500,
|
maxLogLines: 500,
|
||||||
logLayout: 'tabs',
|
logLayout: 'tabs',
|
||||||
// 性能
|
// 性能
|
||||||
perfHistory: { 'ai-core': [], 'gateway': [], 'frontend': [] },
|
perfHistory: { 'ai-core': [], 'gateway': [], 'iot-debug-service': [], 'frontend': [] },
|
||||||
// 会话
|
// 会话
|
||||||
sessionsData: [],
|
sessionsData: [],
|
||||||
sessionsAutoRefresh: null,
|
sessionsAutoRefresh: null,
|
||||||
@@ -468,7 +468,7 @@ function statusBadge(status) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function escapeId(id) {
|
function escapeId(id) {
|
||||||
const map = { 'ai-core': 'AI-Core', 'gateway': 'Gateway', 'frontend': 'Frontend' };
|
const map = { 'ai-core': 'AI-Core', 'gateway': 'Gateway', 'frontend': 'Frontend', 'iot-debug-service': 'IoT Debug' };
|
||||||
return map[id] || id;
|
return map[id] || id;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -477,13 +477,23 @@ async function api(url, opts = {}) {
|
|||||||
const res = await fetch(url, { headers: { 'Content-Type': 'application/json' }, ...opts });
|
const res = await fetch(url, { headers: { 'Content-Type': 'application/json' }, ...opts });
|
||||||
if (!res.ok) {
|
if (!res.ok) {
|
||||||
const text = await res.text();
|
const text = await res.text();
|
||||||
let err;
|
let parsed;
|
||||||
try { err = JSON.parse(text).error || text; } catch { err = text; }
|
try { parsed = JSON.parse(text); } catch { parsed = null; }
|
||||||
return { error: err, status: res.status };
|
return {
|
||||||
|
error: parsed?.error || text,
|
||||||
|
errorType: parsed?.errorType || null,
|
||||||
|
hint: parsed?.hint || null,
|
||||||
|
status: res.status,
|
||||||
|
};
|
||||||
}
|
}
|
||||||
return await res.json();
|
const body = await res.json();
|
||||||
|
// 即使 HTTP 200,body 中也可能包含 error 字段(如 Gateway 代理失败返回的 502)
|
||||||
|
if (body && body.error) {
|
||||||
|
return { ...body, status: res.status };
|
||||||
|
}
|
||||||
|
return body;
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
return { error: err.message };
|
return { error: err.message, status: 0 };
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -606,7 +616,7 @@ async function renderDashboard() {
|
|||||||
<button class="btn btn-sm btn-red" onclick="svcAction('stop-all')">⏹ 全部停止</button>
|
<button class="btn btn-sm btn-red" onclick="svcAction('stop-all')">⏹ 全部停止</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="cards-grid cards-3" id="dashboard-svc-cards"></div>
|
<div class="cards-grid cards-4" id="dashboard-svc-cards"></div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 性能快照 + 性能仪表盘 -->
|
<!-- 性能快照 + 性能仪表盘 -->
|
||||||
@@ -826,13 +836,19 @@ function renderMemoryPanel() {
|
|||||||
<div class="card">
|
<div class="card">
|
||||||
<div class="card-header">
|
<div class="card-header">
|
||||||
<span class="card-title">📋 记忆列表</span>
|
<span class="card-title">📋 记忆列表</span>
|
||||||
<span id="mem-result-count" style="font-size:11px;color:var(--text2)"></span>
|
<span style="display:flex;align-items:center;gap:8px">
|
||||||
|
<select id="mem-sort-order" onchange="sortAndRenderMemories()" style="background:var(--bg3);color:var(--text);border:1px solid var(--border);border-radius:4px;padding:2px 6px;font-size:11px">
|
||||||
|
<option value="desc">🕐 最新优先</option>
|
||||||
|
<option value="asc">🕐 最早优先</option>
|
||||||
|
</select>
|
||||||
|
<span id="mem-result-count" style="font-size:11px;color:var(--text2)"></span>
|
||||||
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="table-wrap">
|
<div class="table-wrap">
|
||||||
<table>
|
<table>
|
||||||
<thead><tr><th>内容</th><th>分类</th><th>优先级</th><th>用户</th><th>创建时间</th><th style="width:50px">操作</th></tr></thead>
|
<thead><tr><th>内容</th><th>分类</th><th>优先级</th><th>用户</th><th>话题 (会话)</th><th>创建时间</th><th style="width:50px">操作</th></tr></thead>
|
||||||
<tbody id="mem-table-body">
|
<tbody id="mem-table-body">
|
||||||
<tr><td colspan="6"><div class="empty-state"><div class="icon">🧠</div>使用搜索或列表按钮加载记忆</div></td></tr>
|
<tr><td colspan="7"><div class="empty-state"><div class="icon">🧠</div>使用搜索或列表按钮加载记忆</div></td></tr>
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
@@ -863,8 +879,17 @@ function renderMemoryResults(data) {
|
|||||||
const countEl = document.getElementById('mem-result-count');
|
const countEl = document.getElementById('mem-result-count');
|
||||||
|
|
||||||
if (data.error) {
|
if (data.error) {
|
||||||
tbody.innerHTML = `<tr><td colspan="6"><div class="empty-state"><div class="icon">⚠️</div>${escHtml(data.error)}</div></td></tr>`;
|
let hint = '';
|
||||||
|
if (data.errorType === 'gateway_not_running' || data.errorType === 'gateway_auth_failed') {
|
||||||
|
hint = '<br><span style="font-size:11px">💡 提示: 请先在「服务管理」面板中启动 Gateway 服务</span>';
|
||||||
|
} else if (data.errorType === 'gateway_unreachable') {
|
||||||
|
hint = '<br><span style="font-size:11px">💡 提示: Gateway 服务无响应,请检查网络连接和服务状态</span>';
|
||||||
|
} else if (data.status === 502) {
|
||||||
|
hint = '<br><span style="font-size:11px">💡 提示: 请确认 Gateway 和 AI-Core 服务已启动</span>';
|
||||||
|
}
|
||||||
|
tbody.innerHTML = `<tr><td colspan="7"><div class="empty-state"><div class="icon">⚠️</div>${escHtml(data.error)}${hint}</div></td></tr>`;
|
||||||
countEl.textContent = '';
|
countEl.textContent = '';
|
||||||
|
STATE.memoryCache = [];
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -874,23 +899,47 @@ function renderMemoryResults(data) {
|
|||||||
else if (data.memories) memories = data.memories;
|
else if (data.memories) memories = data.memories;
|
||||||
else if (data.results) memories = data.results;
|
else if (data.results) memories = data.results;
|
||||||
|
|
||||||
|
// 缓存记忆数据用于排序
|
||||||
|
STATE.memoryCache = memories;
|
||||||
|
sortAndRenderMemories();
|
||||||
|
}
|
||||||
|
|
||||||
|
function sortAndRenderMemories() {
|
||||||
|
const tbody = document.getElementById('mem-table-body');
|
||||||
|
const countEl = document.getElementById('mem-result-count');
|
||||||
|
const sortOrder = document.getElementById('mem-sort-order')?.value || 'desc';
|
||||||
|
|
||||||
|
let memories = [...(STATE.memoryCache || [])];
|
||||||
|
|
||||||
|
// 按创建时间排序
|
||||||
|
memories.sort((a, b) => {
|
||||||
|
const ta = new Date(a.created_at || 0).getTime();
|
||||||
|
const tb = new Date(b.created_at || 0).getTime();
|
||||||
|
return sortOrder === 'asc' ? ta - tb : tb - ta;
|
||||||
|
});
|
||||||
|
|
||||||
countEl.textContent = `共 ${memories.length} 条`;
|
countEl.textContent = `共 ${memories.length} 条`;
|
||||||
|
|
||||||
if (memories.length === 0) {
|
if (memories.length === 0) {
|
||||||
tbody.innerHTML = '<tr><td colspan="6"><div class="empty-state"><div class="icon">📭</div>没有找到记忆</div></td></tr>';
|
tbody.innerHTML = '<tr><td colspan="7"><div class="empty-state"><div class="icon">📭</div>没有找到记忆</div></td></tr>';
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
tbody.innerHTML = memories.map(m => `
|
tbody.innerHTML = memories.map(m => {
|
||||||
|
// 会话ID 简短显示
|
||||||
|
const sid = m.session_id || '—';
|
||||||
|
const sidShort = sid.length > 16 ? sid.substring(0, 14) + '…' : sid;
|
||||||
|
return `
|
||||||
<tr>
|
<tr>
|
||||||
<td style="max-width:300px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap" title="${escHtml(m.content || '')}">${escHtml((m.content || '').substring(0, 80))}</td>
|
<td style="max-width:260px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap" title="${escHtml(m.content || '')}">${escHtml((m.content || '').substring(0, 80))}</td>
|
||||||
<td><span class="badge badge-idle">${escHtml(m.category || 'other')}</span></td>
|
<td><span class="badge badge-idle">${escHtml(m.category || 'other')}</span></td>
|
||||||
<td>${m.priority ?? 1}</td>
|
<td>${m.priority ?? 1}</td>
|
||||||
<td style="color:var(--text2)">${escHtml(m.user_id || '—')}</td>
|
<td style="color:var(--text2)">${escHtml(m.user_id || '—')}</td>
|
||||||
|
<td style="color:var(--text2);font-size:11px" title="${escHtml(sid)}">${escHtml(sidShort)}</td>
|
||||||
<td style="color:var(--text2);font-size:11px">${formatTime(m.created_at)}</td>
|
<td style="color:var(--text2);font-size:11px">${formatTime(m.created_at)}</td>
|
||||||
<td><button class="btn btn-xs btn-red" onclick="deleteMemory('${escHtml(m.id || m.ID || '')}')" title="删除">🗑</button></td>
|
<td><button class="btn btn-xs btn-red" onclick="deleteMemory('${escHtml(m.id || m.ID || '')}')" title="删除">🗑</button></td>
|
||||||
</tr>
|
</tr>
|
||||||
`).join('');
|
`}).join('');
|
||||||
}
|
}
|
||||||
|
|
||||||
async function addMemory() {
|
async function addMemory() {
|
||||||
@@ -981,7 +1030,16 @@ async function fetchActiveSessions() {
|
|||||||
if (!container) return;
|
if (!container) return;
|
||||||
|
|
||||||
if (data.error) {
|
if (data.error) {
|
||||||
container.innerHTML = `<div class="empty-state"><div class="icon">⚠️</div>${escHtml(data.error)}<br><span style="font-size:11px">请确认 Gateway 服务已启动</span></div>`;
|
const errMsg = escHtml(data.error);
|
||||||
|
let hint = '';
|
||||||
|
if (data.errorType === 'gateway_not_running' || data.errorType === 'gateway_auth_failed') {
|
||||||
|
hint = '<br><span style="font-size:11px">💡 提示: 请先在「服务管理」面板中启动 Gateway 服务</span>';
|
||||||
|
} else if (data.errorType === 'gateway_unreachable') {
|
||||||
|
hint = '<br><span style="font-size:11px">💡 提示: Gateway 服务无响应,请检查网络连接和服务状态</span>';
|
||||||
|
} else if (data.status === 502) {
|
||||||
|
hint = '<br><span style="font-size:11px">💡 提示: 请确认 Gateway 服务已启动</span>';
|
||||||
|
}
|
||||||
|
container.innerHTML = `<div class="empty-state"><div class="icon">⚠️</div>${errMsg}${hint}</div>`;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1048,7 +1106,15 @@ async function toggleSessionDetail(index) {
|
|||||||
const data = await api(`/api/sessions/${session.session_id}`);
|
const data = await api(`/api/sessions/${session.session_id}`);
|
||||||
|
|
||||||
if (data.error) {
|
if (data.error) {
|
||||||
contentEl.innerHTML = `<div style="color:var(--red);text-align:center">${escHtml(data.error)}</div>`;
|
let hint = '';
|
||||||
|
if (data.errorType === 'gateway_not_running' || data.errorType === 'gateway_auth_failed') {
|
||||||
|
hint = '<br><span style="font-size:11px;color:var(--text2)">💡 提示: 请先在「服务管理」面板中启动 Gateway 服务</span>';
|
||||||
|
} else if (data.errorType === 'gateway_unreachable') {
|
||||||
|
hint = '<br><span style="font-size:11px;color:var(--text2)">💡 提示: Gateway 服务无响应,请检查网络连接和服务状态</span>';
|
||||||
|
} else if (data.status === 502) {
|
||||||
|
hint = '<br><span style="font-size:11px;color:var(--text2)">💡 提示: 请确认 Gateway 服务已启动</span>';
|
||||||
|
}
|
||||||
|
contentEl.innerHTML = `<div style="color:var(--red);text-align:center">${escHtml(data.error)}${hint}</div>`;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1105,7 +1171,7 @@ function renderServicesPanel() {
|
|||||||
<!-- 服务状态卡片 -->
|
<!-- 服务状态卡片 -->
|
||||||
<div class="card">
|
<div class="card">
|
||||||
<div class="card-header"><span class="card-title">📡 服务管理</span></div>
|
<div class="card-header"><span class="card-title">📡 服务管理</span></div>
|
||||||
<div class="cards-grid cards-3" id="services-svc-cards"></div>
|
<div class="cards-grid cards-4" id="services-svc-cards"></div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 实时日志 -->
|
<!-- 实时日志 -->
|
||||||
@@ -1124,9 +1190,10 @@ function renderServicesPanel() {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div id="services-log-grid" style="display:none">
|
<div id="services-log-grid" style="display:none">
|
||||||
<div class="cards-grid cards-3">
|
<div class="cards-grid cards-4">
|
||||||
<div class="log-container" id="log-panel-ai-core" style="height:280px"><div class="empty-state"><div class="icon">📝</div>AI-Core</div></div>
|
<div class="log-container" id="log-panel-ai-core" style="height:280px"><div class="empty-state"><div class="icon">📝</div>AI-Core</div></div>
|
||||||
<div class="log-container" id="log-panel-gateway" style="height:280px"><div class="empty-state"><div class="icon">📝</div>Gateway</div></div>
|
<div class="log-container" id="log-panel-gateway" style="height:280px"><div class="empty-state"><div class="icon">📝</div>Gateway</div></div>
|
||||||
|
<div class="log-container" id="log-panel-iot-debug-service" style="height:280px"><div class="empty-state"><div class="icon">📝</div>IoT Debug</div></div>
|
||||||
<div class="log-container" id="log-panel-frontend" style="height:280px"><div class="empty-state"><div class="icon">📝</div>Frontend</div></div>
|
<div class="log-container" id="log-panel-frontend" style="height:280px"><div class="empty-state"><div class="icon">📝</div>Frontend</div></div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -1142,7 +1209,7 @@ function renderServiceCards() {
|
|||||||
const container = document.getElementById('services-svc-cards');
|
const container = document.getElementById('services-svc-cards');
|
||||||
if (!container) return;
|
if (!container) return;
|
||||||
const status = STATE.serviceStatus;
|
const status = STATE.serviceStatus;
|
||||||
const ids = Object.keys(status).length > 0 ? Object.keys(status) : ['ai-core', 'gateway', 'frontend'];
|
const ids = Object.keys(status).length > 0 ? Object.keys(status) : ['ai-core', 'gateway', 'iot-debug-service', 'frontend'];
|
||||||
|
|
||||||
container.innerHTML = ids.map(id => {
|
container.innerHTML = ids.map(id => {
|
||||||
const svc = status[id] || { name: escapeId(id), status: 'unknown', pid: null, port: '—', uptime: 0, healthUrl: null };
|
const svc = status[id] || { name: escapeId(id), status: 'unknown', pid: null, port: '—', uptime: 0, healthUrl: null };
|
||||||
@@ -1176,7 +1243,7 @@ function renderServiceCards() {
|
|||||||
function initSvcLogTabs() {
|
function initSvcLogTabs() {
|
||||||
const tabs = document.getElementById('services-log-tabs');
|
const tabs = document.getElementById('services-log-tabs');
|
||||||
if (!tabs) return;
|
if (!tabs) return;
|
||||||
tabs.innerHTML = ['ai-core', 'gateway', 'frontend'].map(id =>
|
tabs.innerHTML = ['ai-core', 'gateway', 'iot-debug-service', 'frontend'].map(id =>
|
||||||
`<button class="log-tab ${id === STATE.activeLogTab ? 'active' : ''}" onclick="switchSvcLogTab('${id}')">${escapeId(id)}</button>`
|
`<button class="log-tab ${id === STATE.activeLogTab ? 'active' : ''}" onclick="switchSvcLogTab('${id}')">${escapeId(id)}</button>`
|
||||||
).join('');
|
).join('');
|
||||||
}
|
}
|
||||||
@@ -1227,7 +1294,7 @@ function toggleSvcLogLayout() {
|
|||||||
gridPanel.style.display = '';
|
gridPanel.style.display = '';
|
||||||
logTabs.style.display = 'none';
|
logTabs.style.display = 'none';
|
||||||
btn.textContent = '📋 标签页';
|
btn.textContent = '📋 标签页';
|
||||||
['ai-core', 'gateway', 'frontend'].forEach(id => renderGridLog(id));
|
['ai-core', 'gateway', 'iot-debug-service', 'frontend'].forEach(id => renderGridLog(id));
|
||||||
} else {
|
} else {
|
||||||
STATE.logLayout = 'tabs';
|
STATE.logLayout = 'tabs';
|
||||||
tabsPanel.style.display = '';
|
tabsPanel.style.display = '';
|
||||||
@@ -1289,7 +1356,7 @@ function renderPerformancePanel() {
|
|||||||
<span class="legend-item"><span class="legend-dot mem"></span> 内存 MB</span>
|
<span class="legend-item"><span class="legend-dot mem"></span> 内存 MB</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="cards-grid cards-3" id="perf-panels"></div>
|
<div class="cards-grid cards-4" id="perf-panels"></div>
|
||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
refreshPerf();
|
refreshPerf();
|
||||||
@@ -1313,7 +1380,7 @@ async function refreshPerf() {
|
|||||||
function renderPerfPanels(snap) {
|
function renderPerfPanels(snap) {
|
||||||
const container = document.getElementById('perf-panels');
|
const container = document.getElementById('perf-panels');
|
||||||
if (!container) return;
|
if (!container) return;
|
||||||
const ids = Object.keys(snap).length > 0 ? Object.keys(snap) : ['ai-core', 'gateway', 'frontend'];
|
const ids = Object.keys(snap).length > 0 ? Object.keys(snap) : ['ai-core', 'gateway', 'iot-debug-service', 'frontend'];
|
||||||
|
|
||||||
container.innerHTML = ids.map(id => {
|
container.innerHTML = ids.map(id => {
|
||||||
const s = snap[id] || { pid: null, cpu: 0, mem: 0 };
|
const s = snap[id] || { pid: null, cpu: 0, mem: 0 };
|
||||||
|
|||||||
Reference in New Issue
Block a user