feat: 第五轮开发 - 14项未来路线图功能完整实现

W1-W14 全部完成:
- W1: 消息搜索 (ILIKE全文检索 + SearchModal)
- W2: 对话导出 (JSON/Markdown/TXT三格式)
- W3: 记忆时间线 DevTools 可视化
- W4: 通知推送系统 (WebSocket + Browser Notification API)
- W5: 定时提醒 (30s轮询 + 重复提醒 + WebSocket推送)
- W6: 每日简报 (08:00自动生成: 天气+新闻+提醒+AI摘要)
- W7: IoT场景自动化 (规则引擎 10s轮询 + 条件评估 + 场景执行)
- W8: 语音输入 (浏览器 Speech Recognition API)
- W9: STT服务 (voice-service + whisper.cpp)
- W10: TTS服务 (浏览器 Speech Synthesis + edge-tts三档回退)
- W11: 文件管理 (上传/下载/缩略图/纯Go bilinear缩放)
- W12: 知识库RAG (PostgreSQL tsvector + 文档分块 + 检索)
- W13: 多模态 (图片上传+分析: Vision API + 本地Go分析回退)
- W14: PWA (Service Worker + 离线页 + install prompt)

总计: 6个Go微服务 + 10+前端组件 + 10+ PostgreSQL表 + 4个后台调度器
This commit is contained in:
2026-05-19 12:01:09 +08:00
parent 78e3f450c2
commit bcf4d4e621
69 changed files with 14599 additions and 150 deletions
+449 -10
View File
@@ -419,6 +419,181 @@ input[type="range"] { accent-color: var(--accent); padding: 0; }
border-color: var(--accent) !important;
font-weight: 600;
}
/* ========== 记忆时间线样式 ========== */
.timeline-container {
position: relative;
padding-left: 40px;
}
.timeline-container::before {
content: '';
position: absolute;
left: 16px;
top: 0;
bottom: 0;
width: 2px;
background: linear-gradient(180deg, var(--blue) 0%, var(--border2) 50%, #a855f7 100%);
border-radius: 1px;
}
.timeline-item {
position: relative;
margin-bottom: 20px;
transition: all .2s ease;
}
.timeline-item:hover {
transform: translateX(2px);
}
.timeline-dot {
position: absolute;
left: -28px;
top: 14px;
width: 26px;
height: 26px;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
font-size: 14px;
z-index: 2;
border: 2px solid var(--border2);
background: var(--bg2);
transition: all .2s;
}
.timeline-dot.memory {
border-color: var(--blue);
background: var(--blue-bg);
box-shadow: 0 0 8px rgba(59,130,246,.3);
}
.timeline-dot.thinking {
border-color: #a855f7;
background: rgba(168,85,247,.12);
box-shadow: 0 0 8px rgba(168,85,247,.3);
}
.timeline-card {
background: var(--bg2);
border: 1px solid var(--border);
border-radius: var(--radius);
padding: 14px 16px;
cursor: pointer;
transition: all .2s ease;
}
.timeline-card:hover {
border-color: var(--accent);
box-shadow: 0 4px 16px rgba(0,0,0,.2);
}
.timeline-card.memory-card {
border-left: 3px solid var(--blue);
}
.timeline-card.thinking-card {
border-left: 3px solid #a855f7;
}
.timeline-card-header {
display: flex;
align-items: center;
justify-content: space-between;
gap: 10px;
margin-bottom: 8px;
}
.timeline-card-title {
font-weight: 600;
font-size: 13px;
flex: 1;
line-height: 1.4;
}
.timeline-card-title.memory { color: #60a5fa; }
.timeline-card-title.thinking { color: #c084fc; }
.timeline-card-meta {
display: flex;
align-items: center;
gap: 10px;
font-size: 11px;
color: var(--text3);
flex-shrink: 0;
}
.timeline-card-body {
font-size: 12px;
color: var(--text2);
line-height: 1.6;
margin-bottom: 8px;
}
.timeline-card-footer {
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;
}
.timeline-importance-stars {
color: #f59e0b;
font-size: 11px;
white-space: nowrap;
}
.timeline-trigger-badge {
display: inline-block;
padding: 1px 8px;
border-radius: 10px;
font-size: 10px;
font-weight: 500;
}
.timeline-trigger-badge.scheduled { background: var(--blue-bg); color: var(--blue); }
.timeline-trigger-badge.manual { background: var(--orange-bg); color: var(--orange); }
.timeline-detail {
display: none;
margin-top: 10px;
padding: 12px;
background: var(--bg);
border: 1px solid var(--border2);
border-radius: var(--radius-sm);
font-size: 12px;
line-height: 1.7;
}
.timeline-detail.open {
display: block;
}
.timeline-detail .detail-section {
margin-bottom: 10px;
}
.timeline-detail .detail-section:last-child {
margin-bottom: 0;
}
.timeline-detail .detail-label {
font-weight: 600;
font-size: 11px;
color: var(--text3);
text-transform: uppercase;
letter-spacing: 0.5px;
margin-bottom: 4px;
}
.timeline-detail .detail-content {
color: var(--text);
white-space: pre-wrap;
word-break: break-word;
}
.timeline-detail .tool-call-item {
padding: 6px 10px;
background: var(--bg3);
border-radius: var(--radius-sm);
margin-bottom: 4px;
font-size: 11px;
font-family: 'JetBrains Mono', monospace;
}
/* 筛选标签样式 */
.timeline-filter-tab {
padding: 4px 14px;
border-radius: 16px;
border: 1px solid var(--border);
cursor: pointer;
font-size: 12px;
background: var(--bg3);
color: var(--text2);
transition: all .15s;
font-family: inherit;
}
.timeline-filter-tab:hover { background: var(--bg4); color: var(--text); }
.timeline-filter-tab.active { background: var(--accent); color: #fff; border-color: var(--accent); font-weight: 600; }
</style>
</head>
<body>
@@ -460,6 +635,9 @@ input[type="range"] { accent-color: var(--accent); padding: 0; }
<button class="nav-item" data-panel="thinking">
<span class="nav-icon">💭</span><span class="nav-label">自主思考</span>
</button>
<button class="nav-item" data-panel="timeline">
<span class="nav-icon">⏱️</span><span class="nav-label">记忆时间线</span>
</button>
</nav>
<div class="sidebar-footer">
<span id="ws-dot" class="disconnected"></span>
@@ -492,6 +670,8 @@ input[type="range"] { accent-color: var(--accent); padding: 0; }
<div class="panel" id="panel-toolCalls"></div>
<!-- 自主思考日志 -->
<div class="panel" id="panel-thinking"></div>
<!-- 记忆时间线 -->
<div class="panel" id="panel-timeline"></div>
</div>
</div>
@@ -534,6 +714,12 @@ const STATE = {
memoryFilterImportance: 0,
memorySearchText: '',
memoryPanelInitialized: false,
// 时间线面板状态
timelineData: [],
timelineUserId: 'admin_admin',
timelineFilterType: 'all',
timelineAutoRefresh: null,
timelineLimit: 100,
};
// ========== WebSocket ==========
@@ -705,7 +891,7 @@ function switchPanel(name) {
const titles = {
dashboard: '🏠 仪表盘', memory: '🧠 记忆管理', sessions: '💬 会话监看',
services: '🖥 服务管理', iot: '🏠 IoT 设备控制', performance: '📊 性能监控', database: '🗄️ 数据库监看',
toolCalls: '🔧 工具调用记录', thinking: '💭 自主思考',
toolCalls: '🔧 工具调用记录', thinking: '💭 自主思考', timeline: '⏱️ 记忆时间线',
};
document.getElementById('panel-title').textContent = titles[name] || name;
@@ -718,15 +904,16 @@ function switchPanel(name) {
// 渲染面板
switch (name) {
case 'dashboard': renderDashboard(); startDashboardAutoRefresh(); stopDbAutoRefresh(); stopIoTRefresh(); break;
case 'memory': renderMemoryPanel(); stopDashboardAutoRefresh(); stopDbAutoRefresh(); stopIoTRefresh(); break;
case 'sessions': renderSessionsPanel(); startSessionsAutoRefresh(); stopDashboardAutoRefresh(); stopDbAutoRefresh(); stopIoTRefresh(); break;
case 'services': renderServicesPanel(); stopSessionsAutoRefresh(); stopDashboardAutoRefresh(); stopDbAutoRefresh(); stopIoTRefresh(); break;
case 'iot': renderIoTPanel(); stopSessionsAutoRefresh(); stopDashboardAutoRefresh(); stopDbAutoRefresh(); startIoTRefresh(); break;
case 'performance': renderPerformancePanel(); stopSessionsAutoRefresh(); stopDashboardAutoRefresh(); stopDbAutoRefresh(); stopIoTRefresh(); break;
case 'database': renderDatabasePanel(); stopSessionsAutoRefresh(); stopDashboardAutoRefresh(); startDbAutoRefresh(); stopIoTRefresh(); break;
case 'toolCalls': renderToolCallsPanel(); stopSessionsAutoRefresh(); stopDashboardAutoRefresh(); stopDbAutoRefresh(); stopIoTRefresh(); break;
case 'thinking': renderThinkingPanel(); stopSessionsAutoRefresh(); stopDashboardAutoRefresh(); stopDbAutoRefresh(); stopIoTRefresh(); break;
case 'dashboard': renderDashboard(); startDashboardAutoRefresh(); stopDbAutoRefresh(); stopIoTRefresh(); stopTimelineAutoRefresh(); break;
case 'memory': renderMemoryPanel(); stopDashboardAutoRefresh(); stopDbAutoRefresh(); stopIoTRefresh(); stopTimelineAutoRefresh(); break;
case 'sessions': renderSessionsPanel(); startSessionsAutoRefresh(); stopDashboardAutoRefresh(); stopDbAutoRefresh(); stopIoTRefresh(); stopTimelineAutoRefresh(); break;
case 'services': renderServicesPanel(); stopSessionsAutoRefresh(); stopDashboardAutoRefresh(); stopDbAutoRefresh(); stopIoTRefresh(); stopTimelineAutoRefresh(); break;
case 'iot': renderIoTPanel(); stopSessionsAutoRefresh(); stopDashboardAutoRefresh(); stopDbAutoRefresh(); startIoTRefresh(); stopTimelineAutoRefresh(); break;
case 'performance': renderPerformancePanel(); stopSessionsAutoRefresh(); stopDashboardAutoRefresh(); stopDbAutoRefresh(); stopIoTRefresh(); stopTimelineAutoRefresh(); break;
case 'database': renderDatabasePanel(); stopSessionsAutoRefresh(); stopDashboardAutoRefresh(); startDbAutoRefresh(); stopIoTRefresh(); stopTimelineAutoRefresh(); break;
case 'toolCalls': renderToolCallsPanel(); stopSessionsAutoRefresh(); stopDashboardAutoRefresh(); stopDbAutoRefresh(); stopIoTRefresh(); stopTimelineAutoRefresh(); break;
case 'thinking': renderThinkingPanel(); stopSessionsAutoRefresh(); stopDashboardAutoRefresh(); stopDbAutoRefresh(); stopIoTRefresh(); stopTimelineAutoRefresh(); break;
case 'timeline': renderTimelinePanel(); stopSessionsAutoRefresh(); stopDashboardAutoRefresh(); stopDbAutoRefresh(); stopIoTRefresh(); startTimelineAutoRefresh(); break;
}
}
@@ -2628,6 +2815,258 @@ function toggleThinkingAutoRefresh(on) {
}
}
// ========== 面板10: 记忆时间线 ==========
function stopTimelineAutoRefresh() {
if (STATE.timelineAutoRefresh) { clearInterval(STATE.timelineAutoRefresh); STATE.timelineAutoRefresh = null; }
}
function startTimelineAutoRefresh() {
stopTimelineAutoRefresh();
STATE.timelineAutoRefresh = setInterval(function() {
if (STATE.activePanel === 'timeline') renderTimelinePanel();
}, 30000);
}
function importanceToStarsTimeline(imp) {
var full = Math.round(imp / 2);
var empty = 5 - full;
return '<span style="color:#f59e0b">' + '★'.repeat(full) + '</span><span style="color:var(--text3)">' + '☆'.repeat(empty) + '</span>';
}
async function renderTimelinePanel() {
var container = document.getElementById('panel-timeline');
if (!container) return;
var actionsEl = document.getElementById('panel-actions');
var autoRefreshOn = STATE.timelineAutoRefresh !== null;
actionsEl.innerHTML = '<label style="display:flex;align-items:center;gap:6px;font-size:12px;color:var(--text2);cursor:pointer;">' +
'<input type="checkbox" id="timeline-autorefresh" ' + (autoRefreshOn ? 'checked' : '') + ' onchange="toggleTimelineAutoRefresh(this.checked)">' +
'自动刷新 (30s)</label>' +
'<button class="btn btn-sm" onclick="renderTimelinePanel()" style="margin-left:8px">🔄 刷新</button>';
// 加载数据
var userId = STATE.timelineUserId || 'admin_admin';
var data = await api('/api/memory-timeline?user_id=' + encodeURIComponent(userId) + '&limit=' + STATE.timelineLimit);
if (data.error) {
container.innerHTML = '<div class="empty-state"><div class="icon">⚠️</div>' + escHtml(data.error) +
(data.hint ? '<br><small>' + escHtml(data.hint) + '</small>' : '') + '</div>';
return;
}
var timeline = data.timeline || [];
var stats = data.stats || {};
STATE.timelineData = timeline;
// 筛选
var filtered = timeline;
if (STATE.timelineFilterType && STATE.timelineFilterType !== 'all') {
filtered = timeline.filter(function(item) { return item.type === STATE.timelineFilterType; });
}
// 统计卡片
var memCount = stats.total_memories || 0;
var thinkCount = stats.total_thinking || 0;
var latestMemTime = stats.latest_memory_time ? formatTime(stats.latest_memory_time) : '—';
var latestThinkTime = stats.latest_thinking_time ? formatTime(stats.latest_thinking_time) : '—';
var statsCardsHtml = '<div class="cards-grid cards-4" style="margin-bottom:14px;">' +
'<div class="stat-card accent"><div class="stat-value">' + memCount + '</div><div class="stat-label">🧠 总记忆数</div></div>' +
'<div class="stat-card blue"><div class="stat-value">' + thinkCount + '</div><div class="stat-label">💭 总思考次数</div></div>' +
'<div class="stat-card green"><div class="stat-value">' + latestMemTime + '</div><div class="stat-label">📅 最新记忆</div></div>' +
'<div class="stat-card orange"><div class="stat-value">' + latestThinkTime + '</div><div class="stat-label">🕐 最新思考</div></div>' +
'</div>';
// 筛选栏
var allActive = STATE.timelineFilterType === 'all' ? ' active' : '';
var memActive = STATE.timelineFilterType === 'memory' ? ' active' : '';
var thinkActive = STATE.timelineFilterType === 'thinking' ? ' active' : '';
var filterHtml = '<div style="display:flex;align-items:center;gap:10px;margin-bottom:14px;flex-wrap:wrap;">' +
'<span style="font-size:12px;color:var(--text2);">筛选类型:</span>' +
'<button class="timeline-filter-tab' + allActive + '" onclick="filterTimeline(\'all\')">📋 全部</button>' +
'<button class="timeline-filter-tab' + memActive + '" onclick="filterTimeline(\'memory\')">🧠 记忆</button>' +
'<button class="timeline-filter-tab' + thinkActive + '" onclick="filterTimeline(\'thinking\')">💭 思考</button>' +
'<span style="font-size:11px;color:var(--text3);margin-left:auto;">显示 ' + filtered.length + ' / ' + timeline.length + ' 条</span>' +
'</div>';
// 时间线主体
var timelineHtml = '';
if (filtered.length === 0) {
timelineHtml = '<div class="empty-state"><div class="icon">📭</div>暂无匹配的时间线条目</div>';
} else {
timelineHtml = '<div class="timeline-container">';
for (var i = 0; i < filtered.length; i++) {
var item = filtered[i];
var itemId = 'tl-' + i;
var isMemory = item.type === 'memory';
// 圆点图标
var dotIcon = isMemory ? '🧠' : '💭';
var dotClass = isMemory ? 'memory' : 'thinking';
var cardClass = isMemory ? 'memory-card' : 'thinking-card';
var titleClass = isMemory ? 'memory' : 'thinking';
// 标题
var title = item.title || (isMemory ? '记忆' : '思考');
// 摘要
var summary = '';
if (isMemory) {
summary = item.summary || item.content || '';
if (summary.length > 200) summary = summary.substring(0, 197) + '...';
} else {
summary = item.summary || '';
}
// 重要性星级
var starsHtml = '';
if (isMemory && item.importance) {
starsHtml = '<span class="timeline-importance-stars">' + importanceToStarsTimeline(item.importance) + '</span>';
}
// 分类标签
var catLabel = '';
if (isMemory && item.category) {
var cc = getCatColor(item.category);
catLabel = '<span style="display:inline-block;padding:1px 8px;border-radius:10px;font-size:10px;font-weight:500;background:' + cc.bg + ';color:' + cc.text + ';">' + cc.icon + ' ' + cc.name + '</span>';
}
// 工具调用数
var toolCallLabel = '';
if (!isMemory && item.tool_call_count > 0) {
toolCallLabel = '<span style="display:inline-block;padding:1px 8px;border-radius:10px;font-size:10px;font-weight:500;background:var(--accent-bg);color:var(--accent);">🔧 ' + item.tool_call_count + ' 次工具调用</span>';
}
// 触发方式
var triggerLabel = '';
if (!isMemory) {
var trigger = item.trigger || '定时';
var triggerClass = trigger === '手动' ? 'manual' : 'scheduled';
triggerLabel = '<span class="timeline-trigger-badge ' + triggerClass + '">' + (trigger === '手动' ? '👆 手动' : '⏰ 定时') + '</span>';
}
// 来源
var sourceLabel = '';
if (isMemory) {
var srcText = item.source === 'thinking' ? '🤔 后台思考' : item.source === 'conversation' ? '💬 对话' : '📝 ' + (item.source || '未知');
sourceLabel = '<span>' + srcText + '</span>';
}
timelineHtml += '<div class="timeline-item" id="' + itemId + '">' +
'<div class="timeline-dot ' + dotClass + '">' + dotIcon + '</div>' +
'<div class="timeline-card ' + cardClass + '" onclick="toggleTimelineDetail(\'' + itemId + '\')">' +
'<div class="timeline-card-header">' +
'<span class="timeline-card-title ' + titleClass + '">' + escHtml(title) + '</span>' +
'<div class="timeline-card-meta">' +
starsHtml +
'<span style="font-size:10px;white-space:nowrap;">' + formatTime(item.timestamp) + '</span>' +
'</div>' +
'</div>' +
(summary ? '<div class="timeline-card-body">' + escHtml(summary) + '</div>' : '') +
'<div class="timeline-card-footer">' +
catLabel +
toolCallLabel +
triggerLabel +
sourceLabel +
(item.session_id ? '<span style="font-size:10px;color:var(--text3);max-width:140px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;">💬 ' + escHtml((item.session_id || '').substring(0, 20)) + '</span>' : '') +
'</div>' +
'</div>' +
// 详情展开区
'<div class="timeline-detail" id="' + itemId + '-detail">' +
renderTimelineDetail(item) +
'</div>' +
'</div>';
}
timelineHtml += '</div>';
}
container.innerHTML = statsCardsHtml + filterHtml + timelineHtml;
}
function renderTimelineDetail(item) {
var html = '';
if (item.type === 'memory') {
// 记忆详情
html += '<div class="detail-section">' +
'<div class="detail-label">📝 完整内容</div>' +
'<div class="detail-content">' + escHtml(item.content || '') + '</div>' +
'</div>';
if (item.keywords && item.keywords.length > 0) {
html += '<div class="detail-section">' +
'<div class="detail-label">🏷️ 关键词</div>' +
'<div style="display:flex;gap:4px;flex-wrap:wrap;">' +
item.keywords.map(function(k) { return '<span style="padding:2px 8px;background:var(--bg3);border-radius:10px;font-size:10px;">' + escHtml(k) + '</span>'; }).join('') +
'</div>' +
'</div>';
}
html += '<div class="detail-section" style="display:flex;gap:20px;flex-wrap:wrap;">' +
'<span><strong>重要性:</strong> ' + (item.importance || 0) + '/10</span>' +
'<span><strong>访问次数:</strong> ' + (item.access_count || 0) + '</span>' +
'<span><strong>来源:</strong> ' + escHtml(item.source || '未知') + '</span>' +
'<span><strong>ID:</strong> <code style="font-size:10px;">' + escHtml((item.id || '').substring(0, 16)) + '</code></span>' +
'</div>';
} else {
// 思考详情
html += '<div class="detail-section">' +
'<div class="detail-label">💭 思考内容</div>' +
'<div class="detail-content">' + escHtml(item.content || '') + '</div>' +
'</div>';
if (item.tool_calls) {
var toolCalls = item.tool_calls;
try {
if (typeof toolCalls === 'string') toolCalls = JSON.parse(toolCalls);
} catch(e) { toolCalls = null; }
if (toolCalls && Array.isArray(toolCalls) && toolCalls.length > 0) {
html += '<div class="detail-section">' +
'<div class="detail-label">🔧 工具调用详情 (' + toolCalls.length + ')</div>';
for (var i = 0; i < toolCalls.length; i++) {
var tc = toolCalls[i];
var tcName = tc.function ? (tc.function.name || '未知') : (tc.name || '未知');
var tcArgs = tc.function ? (tc.function.arguments || '') : (tc.arguments || '');
if (typeof tcArgs === 'object') tcArgs = JSON.stringify(tcArgs);
html += '<div class="tool-call-item">' +
'<strong style="color:var(--accent2);">' + escHtml(tcName) + '</strong>' +
'<pre style="font-size:10px;margin:4px 0 0;white-space:pre-wrap;color:var(--text2);">' + escHtml(String(tcArgs).slice(0, 300)) + '</pre>' +
'</div>';
}
html += '</div>';
}
}
html += '<div class="detail-section" style="display:flex;gap:20px;flex-wrap:wrap;">' +
'<span><strong>内容长度:</strong> ' + (item.content_length || 0) + ' 字符</span>' +
'<span><strong>工具调用数:</strong> ' + (item.tool_call_count || 0) + '</span>' +
'<span><strong>触发方式:</strong> ' + escHtml(item.trigger || '定时') + '</span>' +
'<span><strong>ID:</strong> <code style="font-size:10px;">' + escHtml((item.id || '').substring(0, 16)) + '</code></span>' +
'</div>';
}
return html;
}
function toggleTimelineDetail(itemId) {
var detail = document.getElementById(itemId + '-detail');
if (detail) {
detail.classList.toggle('open');
}
}
function filterTimeline(type) {
STATE.timelineFilterType = type;
renderTimelinePanel();
}
function toggleTimelineAutoRefresh(on) {
if (STATE.timelineAutoRefresh) {
clearInterval(STATE.timelineAutoRefresh);
STATE.timelineAutoRefresh = null;
}
if (on) {
STATE.timelineAutoRefresh = setInterval(function() {
if (STATE.activePanel === 'timeline') renderTimelinePanel();
}, 30000);
}
}
</script>
<script src="iot-panel.js"></script>
<script>