feat: 第四轮功能增强 - LLM 思维记忆优化、DevTools 记忆UI、9个新工具、5分钟自我思考

- 优化 LLM 思维方式和记忆方法(类别/重要性/关键词/相似度合并/衰减)
- DevTools 记忆查询 UI 重新设计(类别筛选/排序/星标/搜索)
- 新增 9 个 LLM 工具:calculator, datetime, file_ops, http_request, json_ops, text, random, crypto, markdown
- 管理员主对话 5 分钟自我思考增强(工具调用/记忆提取/记忆维护)
This commit is contained in:
2026-05-18 12:13:49 +08:00
parent 07781eda0e
commit b6ec36886c
20 changed files with 4654 additions and 320 deletions
+412 -105
View File
@@ -399,6 +399,26 @@ input[type="range"] { accent-color: var(--accent); padding: 0; }
display: flex; align-items: center; gap: 10px; margin-bottom: 14px;
}
.iot-last-update { font-size: 11px; color: var(--text3); }
/* ========== 记忆卡片样式 ========== */
.mem-card {
transition: all 0.2s ease;
}
.mem-card:hover {
box-shadow: 0 4px 20px rgba(0,0,0,.35);
transform: translateY(-2px);
border-color: var(--accent) !important;
}
.mem-card.mem-card-high:hover {
border-color: #f59e0b !important;
box-shadow: 0 4px 24px rgba(245,158,11,.25);
}
.mem-cat-tab.active {
background: var(--accent) !important;
color: #fff !important;
border-color: var(--accent) !important;
font-weight: 600;
}
</style>
</head>
<body>
@@ -495,6 +515,15 @@ const STATE = {
dashboardRenderCount: 0,
// 资源使用 60s 滑动窗口历史 (Bug 6)
resourceHistory: {},
// 记忆面板状态
memoryCache: [],
memoryUserId: 'admin_admin',
memoryFilterCategory: 'all',
memorySortBy: 'importance',
memorySortDir: 'desc',
memoryFilterImportance: 0,
memorySearchText: '',
memoryPanelInitialized: false,
};
// ========== WebSocket ==========
@@ -1038,92 +1067,175 @@ function renderDashboardSvcCards(svcs) {
`).join('');
}
// ========== 记忆分类颜色映射 ==========
const MEMORY_CAT_COLORS = {
'user_preference': { bg: 'rgba(168,85,247,.15)', text: '#a855f7', name: '用户偏好', icon: '💜' },
'personal_info': { bg: 'rgba(59,130,246,.15)', text: '#3b82f6', name: '个人信息', icon: '👤' },
'conversation': { bg: 'rgba(34,197,94,.15)', text: '#22c55e', name: '对话摘要', icon: '💬' },
'knowledge': { bg: 'rgba(249,115,22,.15)', text: '#f97316', name: '知识信息', icon: '📚' },
'event': { bg: 'rgba(239,68,68,.15)', text: '#ef4444', name: '事件记录', icon: '📅' },
'task': { bg: 'rgba(234,179,8,.15)', text: '#eab308', name: '任务计划', icon: '✅' },
'relationship': { bg: 'rgba(236,72,153,.15)', text: '#ec4899', name: '关系情感', icon: '💕' },
// 向后兼容旧分类
'preference': { bg: 'rgba(168,85,247,.15)', text: '#a855f7', name: '偏好', icon: '💜' },
'fact': { bg: 'rgba(59,130,246,.15)', text: '#3b82f6', name: '事实', icon: '👤' },
'experience': { bg: 'rgba(249,115,22,.15)', text: '#f97316', name: '经验', icon: '📚' },
'other': { bg: 'rgba(139,148,158,.15)', text: '#8b949e', name: '其他', icon: '📌' },
'habit': { bg: 'rgba(168,85,247,.15)', text: '#a855f7', name: '习惯', icon: '💜' },
};
function getCatColor(cat) {
return MEMORY_CAT_COLORS[cat] || MEMORY_CAT_COLORS['other'];
}
// ========== 面板2: 记忆管理 ==========
function renderMemoryPanel() {
document.getElementById('panel-memory').innerHTML = `
<div class="cards-grid cards-2">
<!-- 搜索区域 -->
<div class="card">
<div class="card-header"><span class="card-title">🔍 搜索记忆</span></div>
<div class="form-row">
<div class="form-group"><label>用户ID</label><input type="text" id="mem-user-id" placeholder="admin_admin" value="admin_admin"></div>
<div class="form-group"><label>关键词</label><input type="text" id="mem-search-q" placeholder="输入搜索关键词..."></div>
const isFirst = !STATE.memoryPanelInitialized;
STATE.memoryPanelInitialized = true;
// 首次渲染完整 DOM 结构
if (isFirst) {
document.getElementById('panel-memory').innerHTML = `
<!-- 搜索 & 添加行 -->
<div class="cards-grid cards-2" style="margin-bottom:14px">
<div class="card" style="margin:0">
<div class="card-header"><span class="card-title">🔍 搜索与筛选</span></div>
<div class="form-row">
<div class="form-group" style="flex:1">
<label>用户ID</label>
<input type="text" id="mem-user-id" placeholder="admin_admin" value="${escHtml(STATE.memoryUserId)}">
</div>
<div class="form-group" style="flex:2">
<label>全文搜索</label>
<input type="text" id="mem-search-text" placeholder="输入关键词搜索记忆内容..." value="${escHtml(STATE.memorySearchText)}"
oninput="STATE.memorySearchText=this.value;filterAndRenderMemories()">
</div>
</div>
<div class="form-row" style="margin-top:4px">
<div class="form-group" style="flex:1">
<label>最低重要性 ≥ <span id="mem-imp-val">${STATE.memoryFilterImportance}</span></label>
<input type="range" id="mem-filter-importance" min="0" max="10" value="${STATE.memoryFilterImportance}"
oninput="document.getElementById('mem-imp-val').textContent=this.value;STATE.memoryFilterImportance=parseInt(this.value);filterAndRenderMemories()">
</div>
<div class="form-group" style="flex:1;display:flex;align-items:flex-end">
<button class="btn btn-accent btn-sm" onclick="loadMemories()" style="width:100%">🔍 查询记忆</button>
</div>
</div>
</div>
<div class="btn-group" style="margin-top:4px">
<button class="btn btn-accent btn-sm" onclick="searchMemory()">🔍 搜索</button>
<button class="btn btn-sm" onclick="listMemory()">📋 列表全部</button>
<div class="card" style="margin:0">
<div class="card-header"><span class="card-title"> 添加记忆</span></div>
<div class="form-group"><label>用户ID</label><input type="text" id="mem-add-user-id" placeholder="admin_admin" value="admin_admin"></div>
<div class="form-group"><label>内容</label><textarea id="mem-add-content" placeholder="输入记忆内容..." rows="2"></textarea></div>
<div class="form-row">
<div class="form-group">
<label>分类</label>
<select id="mem-add-category">
<option value="user_preference">用户偏好</option>
<option value="personal_info">个人信息</option>
<option value="conversation">对话摘要</option>
<option value="knowledge">知识信息</option>
<option value="event">事件记录</option>
<option value="task">任务计划</option>
<option value="relationship">关系情感</option>
</select>
</div>
<div class="form-group">
<label>重要程度: <span id="mem-priority-val">5</span></label>
<input type="range" id="mem-add-importance" min="1" max="10" value="5" oninput="document.getElementById('mem-priority-val').textContent=this.value">
</div>
</div>
<button class="btn btn-accent btn-sm" onclick="addMemory()"> 添加</button>
</div>
</div>
<!-- 添加记忆 -->
<div class="card">
<div class="card-header"><span class="card-title"> 添加记忆</span></div>
<div class="form-group"><label>用户ID</label><input type="text" id="mem-add-user-id" placeholder="admin_admin" value="admin_admin"></div>
<div class="form-group"><label>内容</label><textarea id="mem-add-content" placeholder="输入记忆内容..."></textarea></div>
<div class="form-row">
<div class="form-group">
<label>分类</label>
<select id="mem-add-category">
<option value="preference">偏好</option>
<option value="fact">事实</option>
<option value="experience">经验</option>
<option value="other">其他</option>
<!-- 统计面板 -->
<div class="card" id="mem-stats-card" style="margin-bottom:14px">
<div class="card-header"><span class="card-title">📊 记忆统计</span></div>
<div id="mem-stats-content">
<div class="empty-state"><div class="icon">📊</div>加载记忆后显示统计</div>
</div>
</div>
<!-- 分类筛选标签栏 -->
<div class="card" style="margin-bottom:10px;padding:12px 16px">
<div style="display:flex;align-items:center;justify-content:space-between;flex-wrap:wrap;gap:8px">
<div id="mem-cat-tabs" style="display:flex;gap:6px;flex-wrap:wrap">
<!-- 动态填充 -->
</div>
<div style="display:flex;align-items:center;gap:8px">
<select id="mem-sort-by" onchange="STATE.memorySortBy=this.value;filterAndRenderMemories()" 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="importance" ${STATE.memorySortBy==='importance'?'selected':''}>⭐ 重要性</option>
<option value="created_at" ${STATE.memorySortBy==='created_at'?'selected':''}>🕐 创建时间</option>
<option value="updated_at" ${STATE.memorySortBy==='updated_at'?'selected':''}>🕐 更新时间</option>
<option value="category" ${STATE.memorySortBy==='category'?'selected':''}>🏷️ 分类</option>
<option value="access_count" ${STATE.memorySortBy==='access_count'?'selected':''}>📊 访问次数</option>
</select>
</div>
<div class="form-group">
<label>优先级: <span id="mem-priority-val">3</span></label>
<input type="range" id="mem-add-priority" min="1" max="5" value="3" oninput="document.getElementById('mem-priority-val').textContent=this.value">
<select id="mem-sort-dir" onchange="STATE.memorySortDir=this.value;filterAndRenderMemories()" 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="desc" ${STATE.memorySortDir==='desc'?'selected':''}>↓ 降序</option>
<option value="asc" ${STATE.memorySortDir==='asc'?'selected':''}>↑ 升序</option>
</select>
<span id="mem-result-count" style="font-size:11px;color:var(--text2);white-space:nowrap"></span>
</div>
</div>
<button class="btn btn-accent btn-sm" onclick="addMemory()"> 添加</button>
</div>
</div>
<!-- 结果表格 -->
<div class="card">
<div class="card-header">
<span class="card-title">📋 记忆列表</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 id="mem-cards-grid" style="display:grid;grid-template-columns:repeat(auto-fill,minmax(340px,1fr));gap:14px">
<div class="empty-state" style="grid-column:1/-1"><div class="icon">🧠</div>点击「查询记忆」加载记忆数据</div>
</div>
<div class="table-wrap">
<table>
<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="7"><div class="empty-state"><div class="icon">🧠</div>使用搜索或列表按钮加载记忆</div></td></tr>
</tbody>
</table>
</div>
</div>
`;
`;
// 初始化分类标签
renderCategoryTabs();
}
// 如果有缓存数据,直接渲染
if (STATE.memoryCache.length > 0) {
filterAndRenderMemories();
}
}
async function searchMemory() {
const userId = document.getElementById('mem-user-id').value.trim();
const q = document.getElementById('mem-search-q').value.trim();
if (!userId) { showToast('请输入用户ID', 'error'); return; }
if (!q) { showToast('请输入搜索关键词', 'error'); return; }
function renderCategoryTabs() {
const container = document.getElementById('mem-cat-tabs');
if (!container) return;
const data = await api(`/api/memory/search?user_id=${encodeURIComponent(userId)}&q=${encodeURIComponent(q)}`);
renderMemoryResults(data);
const categories = [
{ key: 'all', name: '全部', icon: '📋' },
{ key: 'user_preference', name: '用户偏好', icon: '💜' },
{ key: 'personal_info', name: '个人信息', icon: '👤' },
{ key: 'conversation', name: '对话摘要', icon: '💬' },
{ key: 'knowledge', name: '知识信息', icon: '📚' },
{ key: 'event', name: '事件记录', icon: '📅' },
{ key: 'task', name: '任务计划', icon: '✅' },
{ key: 'relationship', name: '关系情感', icon: '💕' },
];
container.innerHTML = categories.map(c => {
const active = STATE.memoryFilterCategory === c.key;
const style = active
? 'background:var(--accent);color:#fff;border-color:var(--accent);font-weight:600'
: 'background:var(--bg3);color:var(--text2);border-color:var(--border)';
return `<button class="mem-cat-tab" data-cat="${c.key}"
style="padding:4px 12px;border-radius:16px;border:1px solid;cursor:pointer;font-size:11px;transition:all .15s;font-family:inherit;${style}"
onmouseenter="if(!this.classList.contains('active')){this.style.background='var(--bg4)';this.style.color='var(--text)'}"
onmouseleave="if(!this.classList.contains('active')){this.style.background='var(--bg3)';this.style.color='var(--text2)'}"
onclick="switchMemoryCategory('${c.key}')">${c.icon} ${c.name}</button>`;
}).join('');
}
async function listMemory() {
function switchMemoryCategory(cat) {
STATE.memoryFilterCategory = cat;
renderCategoryTabs();
filterAndRenderMemories();
}
async function loadMemories() {
const userId = document.getElementById('mem-user-id').value.trim();
if (!userId) { showToast('请输入用户ID', 'error'); return; }
STATE.memoryUserId = userId;
const data = await api(`/api/memory/list?user_id=${encodeURIComponent(userId)}`);
renderMemoryResults(data);
}
function renderMemoryResults(data) {
const tbody = document.getElementById('mem-table-body');
const countEl = document.getElementById('mem-result-count');
if (data.error) {
let hint = '';
@@ -1134,72 +1246,266 @@ function renderMemoryResults(data) {
} 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 = '';
const grid = document.getElementById('mem-cards-grid');
if (grid) grid.innerHTML = `<div class="empty-state" style="grid-column:1/-1"><div class="icon">⚠️</div>${escHtml(data.error)}${hint}</div>`;
document.getElementById('mem-result-count').textContent = '';
STATE.memoryCache = [];
renderStatsPanel();
return;
}
// 兼容不同返回格式
let memories = [];
if (Array.isArray(data)) memories = data;
else if (data.memories) memories = data.memories;
else if (data.results) memories = data.results;
// 缓存记忆数据用于排序
STATE.memoryCache = memories;
sortAndRenderMemories();
filterAndRenderMemories();
}
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';
async function searchMemory() {
const userId = document.getElementById('mem-user-id').value.trim();
const q = STATE.memorySearchText || document.getElementById('mem-search-text')?.value?.trim() || '';
if (!userId) { showToast('请输入用户ID', 'error'); return; }
if (!q) { showToast('请输入搜索关键词', 'error'); return; }
let memories = [...(STATE.memoryCache || [])];
STATE.memoryUserId = userId;
// 按创建时间排序
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;
});
const data = await api(`/api/memory/search?user_id=${encodeURIComponent(userId)}&q=${encodeURIComponent(q)}`);
countEl.textContent = `${memories.length}`;
if (memories.length === 0) {
tbody.innerHTML = '<tr><td colspan="7"><div class="empty-state"><div class="icon">📭</div>没有找到记忆</div></td></tr>';
if (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 和 AI-Core 服务已启动</span>';
}
const grid = document.getElementById('mem-cards-grid');
if (grid) grid.innerHTML = `<div class="empty-state" style="grid-column:1/-1"><div class="icon">⚠️</div>${escHtml(data.error)}${hint}</div>`;
document.getElementById('mem-result-count').textContent = '';
STATE.memoryCache = [];
renderStatsPanel();
return;
}
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: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('');
let memories = [];
if (Array.isArray(data)) memories = data;
else if (data.memories) memories = data.memories;
else if (data.results) memories = data.results;
STATE.memoryCache = memories;
filterAndRenderMemories();
}
// 兼容旧的 listMemory 调用
async function listMemory() {
loadMemories();
}
function filterAndRenderMemories() {
const memories = STATE.memoryCache || [];
// 1. 分类筛选
let filtered = memories;
if (STATE.memoryFilterCategory !== 'all') {
filtered = filtered.filter(m => m.category === STATE.memoryFilterCategory);
}
// 2. 重要性筛选
if (STATE.memoryFilterImportance > 0) {
filtered = filtered.filter(m => (m.importance || 0) >= STATE.memoryFilterImportance);
}
// 3. 全文搜索 (客户端二次过滤)
if (STATE.memorySearchText) {
const q = STATE.memorySearchText.toLowerCase();
filtered = filtered.filter(m => {
const content = (m.content || '').toLowerCase();
const summary = (m.summary || '').toLowerCase();
const keywords = (m.keywords || []).join(' ').toLowerCase();
return content.includes(q) || summary.includes(q) || keywords.includes(q);
});
}
// 4. 排序
const sortBy = STATE.memorySortBy;
const sortDir = STATE.memorySortDir === 'asc' ? 1 : -1;
filtered.sort((a, b) => {
let va, vb;
switch (sortBy) {
case 'importance':
va = a.importance || 0; vb = b.importance || 0; break;
case 'created_at':
va = new Date(a.created_at || 0).getTime(); vb = new Date(b.created_at || 0).getTime(); break;
case 'updated_at':
va = new Date(a.updated_at || a.created_at || 0).getTime();
vb = new Date(b.updated_at || b.created_at || 0).getTime(); break;
case 'category':
va = a.category || ''; vb = b.category || '';
return sortDir * va.localeCompare(vb);
case 'access_count':
va = a.access_count || 0; vb = b.access_count || 0; break;
default:
va = a.importance || 0; vb = b.importance || 0;
}
return sortDir * (va - vb);
});
// 5. 渲染统计
renderStatsPanel();
// 6. 渲染卡片
renderMemoryCards(filtered);
// 更新计数
const countEl = document.getElementById('mem-result-count');
if (countEl) {
countEl.textContent = `显示 ${filtered.length} / ${memories.length}`;
}
}
function renderStatsPanel() {
const container = document.getElementById('mem-stats-content');
if (!container) return;
const memories = STATE.memoryCache || [];
if (memories.length === 0) {
container.innerHTML = '<div class="empty-state"><div class="icon">📊</div>暂无记忆数据</div>';
return;
}
// 计算各分类数量
const catCount = {};
let totalImportance = 0;
let totalAccess = 0;
memories.forEach(m => {
const cat = m.category || 'other';
catCount[cat] = (catCount[cat] || 0) + 1;
totalImportance += (m.importance || 0);
totalAccess += (m.access_count || 0);
});
const avgImportance = (totalImportance / memories.length).toFixed(1);
const maxCatCount = Math.max(1, ...Object.values(catCount));
// 分类分布条
const catOrder = ['user_preference', 'personal_info', 'conversation', 'knowledge', 'event', 'task', 'relationship'];
const barHtml = catOrder.map(cat => {
const count = catCount[cat] || 0;
const pct = Math.round((count / Math.max(1, memories.length)) * 100);
const cc = getCatColor(cat);
return count > 0 ? `<div style="display:flex;align-items:center;gap:4px;font-size:10px">
<span style="color:${cc.text};min-width:52px">${cc.icon} ${cc.name}</span>
<div style="flex:1;background:var(--bg);border-radius:3px;height:14px;overflow:hidden">
<div style="height:100%;width:${pct}%;background:${cc.text};border-radius:3px;transition:width .3s;min-width:2px"></div>
</div>
<span style="color:var(--text2);min-width:32px;text-align:right;font-family:'JetBrains Mono',monospace">${count}</span>
</div>` : '';
}).join('');
container.innerHTML = `
<div class="cards-grid cards-4" style="margin-bottom:10px">
<div class="stat-card accent"><div class="stat-value">${memories.length}</div><div class="stat-label">📦 总记忆数</div></div>
<div class="stat-card blue"><div class="stat-value">${avgImportance}</div><div class="stat-label">⭐ 平均重要性</div></div>
<div class="stat-card green"><div class="stat-value">${totalAccess}</div><div class="stat-label">📊 总访问次数</div></div>
<div class="stat-card orange"><div class="stat-value">${Object.values(catCount).filter(n=>n>0).length}</div><div class="stat-label">🏷️ 分类数</div></div>
</div>
<div style="display:flex;flex-direction:column;gap:3px">${barHtml}</div>
`;
}
function renderMemoryCards(memories) {
const grid = document.getElementById('mem-cards-grid');
if (!grid) return;
if (memories.length === 0) {
const catName = STATE.memoryFilterCategory !== 'all'
? (getCatColor(STATE.memoryFilterCategory).name || STATE.memoryFilterCategory)
: '';
const msg = catName ? `${catName}」分类下暂无记忆` : '没有匹配的记忆';
grid.innerHTML = `<div class="empty-state" style="grid-column:1/-1"><div class="icon">📭</div>${msg}</div>`;
return;
}
grid.innerHTML = memories.map(m => renderMemoryCard(m)).join('');
}
function renderMemoryCard(m) {
const cat = m.category || 'other';
const cc = getCatColor(cat);
const importance = m.importance || 1;
const isHighImportance = importance >= 8;
// 星级
const stars = importanceToStars(importance);
// 关键词标签
const keywords = m.keywords || [];
const kwTags = keywords.length > 0
? keywords.slice(0, 5).map(k => `<span style="display:inline-block;padding:1px 7px;background:var(--bg3);border-radius:10px;font-size:10px;color:var(--text2);margin:1px">${escHtml(k)}</span>`).join('')
: '';
// 来源
const sourceLabel = m.source === 'thinking' ? '🤔 后台思考' : m.source === 'conversation' ? '💬 对话' : '📝 ' + (m.source || '未知');
// 会话 ID 简短
const sid = m.session_id || '';
const sidShort = sid.length > 20 ? sid.substring(0, 18) + '…' : sid;
return `
<div class="mem-card ${isHighImportance ? 'mem-card-high' : ''}"
style="background:var(--bg2);border:1px solid ${isHighImportance ? '#f59e0b' : 'var(--border)'};border-radius:var(--radius);padding:16px;display:flex;flex-direction:column;gap:10px;transition:all .2s">
<!-- 头部: 分类标签 + 重要性 -->
<div style="display:flex;align-items:center;justify-content:space-between">
<span style="display:inline-flex;align-items:center;gap:4px;padding:2px 10px;border-radius:12px;font-size:11px;font-weight:600;background:${cc.bg};color:${cc.text}">
${cc.icon} ${cc.name}
</span>
<span style="font-size:13px;color:#f59e0b" title="重要程度: ${importance}/10">${stars}</span>
</div>
<!-- 记忆内容 -->
<div style="flex:1">
<div style="color:var(--text);font-size:13px;line-height:1.6;word-break:break-word">
${escHtml(m.content || '')}
</div>
${m.summary ? `<div style="color:var(--text2);font-size:11px;margin-top:4px;font-style:italic">📌 ${escHtml(m.summary)}</div>` : ''}
</div>
<!-- 关键词标签 -->
${kwTags ? `<div style="display:flex;flex-wrap:wrap;gap:3px">${kwTags}</div>` : ''}
<!-- 底部元信息 -->
<div style="display:flex;align-items:center;flex-wrap:wrap;gap:8px;font-size:10px;color:var(--text3);border-top:1px solid var(--border);padding-top:8px">
<span title="来源">${sourceLabel}</span>
${sid ? `<span title="会话: ${escHtml(sid)}" style="max-width:140px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap">💬 ${escHtml(sidShort)}</span>` : ''}
<span title="访问次数">📊 ${m.access_count || 0}</span>
<span title="创建时间">📅 ${formatTime(m.created_at)}</span>
<span title="更新时间" style="${(m.updated_at && m.updated_at !== m.created_at) ? '' : 'display:none'}">🔄 ${formatTime(m.updated_at)}</span>
<button class="btn btn-xs btn-red" onclick="deleteMemory('${escHtml(m.id || m.ID || '')}')" title="删除" style="margin-left:auto">🗑</button>
</div>
</div>
`;
}
function importanceToStars(imp) {
const full = Math.round(imp / 2); // 1-10 映射到 1-5 星
const empty = 5 - full;
return '★'.repeat(full) + '☆'.repeat(empty);
}
async function addMemory() {
const user_id = document.getElementById('mem-add-user-id').value.trim();
const content = document.getElementById('mem-add-content').value.trim();
const category = document.getElementById('mem-add-category').value;
const priority = parseInt(document.getElementById('mem-add-priority').value);
const importance = parseInt(document.getElementById('mem-add-importance').value);
if (!user_id || !content) { showToast('请填写用户ID和内容', 'error'); return; }
const data = await api('/api/memory/add', {
method: 'POST',
body: JSON.stringify({ user_id, content, category, priority }),
body: JSON.stringify({ user_id, content, category, importance }),
});
if (data.error) { showToast(`添加失败: ${data.error}`, 'error'); return; }
@@ -1207,7 +1513,7 @@ async function addMemory() {
showToast('记忆添加成功!', 'success');
document.getElementById('mem-add-content').value = '';
// 自动刷新列表
listMemory();
loadMemories();
}
async function deleteMemory(memoryId) {
@@ -1219,8 +1525,9 @@ async function deleteMemory(memoryId) {
if (data.error) { showToast(`删除失败: ${data.error}`, 'error'); return; }
showToast('记忆删除成功!', 'success');
// 自动刷新列表
listMemory();
// 从缓存中移除并重新渲染
STATE.memoryCache = (STATE.memoryCache || []).filter(m => (m.id || m.ID) !== memoryId);
filterAndRenderMemories();
}
// ========== 面板3: 会话监看 ==========