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
|
||||
}
|
||||
|
||||
// 主对话仅限管理员访问
|
||||
if !strings.HasPrefix(userID, "admin_") {
|
||||
c.JSON(http.StatusForbidden, gin.H{
|
||||
"error": "主对话仅限管理员使用",
|
||||
"errorType": "admin_only",
|
||||
"hint": "请使用管理员账号 (admin_ 前缀) 登录以访问主对话功能",
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
if sessionID == "" {
|
||||
sessionID = "session_" + generateID()
|
||||
}
|
||||
|
||||
+93
-26
@@ -368,11 +368,11 @@ const STATE = {
|
||||
serviceStatus: {},
|
||||
// 日志
|
||||
activeLogTab: 'ai-core',
|
||||
logLines: { 'ai-core': [], 'gateway': [], 'frontend': [] },
|
||||
logLines: { 'ai-core': [], 'gateway': [], 'frontend': [], 'iot-debug-service': [] },
|
||||
maxLogLines: 500,
|
||||
logLayout: 'tabs',
|
||||
// 性能
|
||||
perfHistory: { 'ai-core': [], 'gateway': [], 'frontend': [] },
|
||||
perfHistory: { 'ai-core': [], 'gateway': [], 'iot-debug-service': [], 'frontend': [] },
|
||||
// 会话
|
||||
sessionsData: [],
|
||||
sessionsAutoRefresh: null,
|
||||
@@ -468,7 +468,7 @@ function statusBadge(status) {
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
@@ -477,13 +477,23 @@ async function api(url, opts = {}) {
|
||||
const res = await fetch(url, { headers: { 'Content-Type': 'application/json' }, ...opts });
|
||||
if (!res.ok) {
|
||||
const text = await res.text();
|
||||
let err;
|
||||
try { err = JSON.parse(text).error || text; } catch { err = text; }
|
||||
return { error: err, status: res.status };
|
||||
let parsed;
|
||||
try { parsed = JSON.parse(text); } catch { parsed = null; }
|
||||
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) {
|
||||
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>
|
||||
</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>
|
||||
|
||||
<!-- 性能快照 + 性能仪表盘 -->
|
||||
@@ -826,13 +836,19 @@ function renderMemoryPanel() {
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<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 class="table-wrap">
|
||||
<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">
|
||||
<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>
|
||||
</table>
|
||||
</div>
|
||||
@@ -863,8 +879,17 @@ function renderMemoryResults(data) {
|
||||
const countEl = document.getElementById('mem-result-count');
|
||||
|
||||
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 = '';
|
||||
STATE.memoryCache = [];
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -874,23 +899,47 @@ function renderMemoryResults(data) {
|
||||
else if (data.memories) memories = data.memories;
|
||||
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} 条`;
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
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>
|
||||
<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>${m.priority ?? 1}</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><button class="btn btn-xs btn-red" onclick="deleteMemory('${escHtml(m.id || m.ID || '')}')" title="删除">🗑</button></td>
|
||||
</tr>
|
||||
`).join('');
|
||||
`}).join('');
|
||||
}
|
||||
|
||||
async function addMemory() {
|
||||
@@ -981,7 +1030,16 @@ async function fetchActiveSessions() {
|
||||
if (!container) return;
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
@@ -1048,7 +1106,15 @@ async function toggleSessionDetail(index) {
|
||||
const data = await api(`/api/sessions/${session.session_id}`);
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
@@ -1105,7 +1171,7 @@ function renderServicesPanel() {
|
||||
<!-- 服务状态卡片 -->
|
||||
<div class="card">
|
||||
<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>
|
||||
|
||||
<!-- 实时日志 -->
|
||||
@@ -1124,9 +1190,10 @@ function renderServicesPanel() {
|
||||
</div>
|
||||
</div>
|
||||
<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-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>
|
||||
</div>
|
||||
@@ -1142,7 +1209,7 @@ function renderServiceCards() {
|
||||
const container = document.getElementById('services-svc-cards');
|
||||
if (!container) return;
|
||||
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 => {
|
||||
const svc = status[id] || { name: escapeId(id), status: 'unknown', pid: null, port: '—', uptime: 0, healthUrl: null };
|
||||
@@ -1176,7 +1243,7 @@ function renderServiceCards() {
|
||||
function initSvcLogTabs() {
|
||||
const tabs = document.getElementById('services-log-tabs');
|
||||
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>`
|
||||
).join('');
|
||||
}
|
||||
@@ -1227,7 +1294,7 @@ function toggleSvcLogLayout() {
|
||||
gridPanel.style.display = '';
|
||||
logTabs.style.display = 'none';
|
||||
btn.textContent = '📋 标签页';
|
||||
['ai-core', 'gateway', 'frontend'].forEach(id => renderGridLog(id));
|
||||
['ai-core', 'gateway', 'iot-debug-service', 'frontend'].forEach(id => renderGridLog(id));
|
||||
} else {
|
||||
STATE.logLayout = 'tabs';
|
||||
tabsPanel.style.display = '';
|
||||
@@ -1289,7 +1356,7 @@ function renderPerformancePanel() {
|
||||
<span class="legend-item"><span class="legend-dot mem"></span> 内存 MB</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="cards-grid cards-3" id="perf-panels"></div>
|
||||
<div class="cards-grid cards-4" id="perf-panels"></div>
|
||||
</div>
|
||||
`;
|
||||
refreshPerf();
|
||||
@@ -1313,7 +1380,7 @@ async function refreshPerf() {
|
||||
function renderPerfPanels(snap) {
|
||||
const container = document.getElementById('perf-panels');
|
||||
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 => {
|
||||
const s = snap[id] || { pid: null, cpu: 0, mem: 0 };
|
||||
|
||||
Reference in New Issue
Block a user