feat: 添加消息历史持久化和可视化查看功能

- 新增 messageStore.js 消息存储模块,支持自动保存所有收发消息
- 修改 main.js,在消息转发时自动记录到本地存储
- 修改 preload.js,暴露消息管理 IPC API
- 修改 App.js,添加消息历史查看界面
  - 统计信息面板(总数/接收/发送/会话数)
  - 会话列表和消息详情
  - 搜索、过滤、分页功能
  - 导出 JSON 和清空历史
- 新增完整文档(MESSAGE_HISTORY.md 等)
- 新增测试脚本 test-message-history.js

版本:v1.0.1
This commit is contained in:
2026-03-10 04:09:26 +08:00
parent 7c826af5d1
commit 0880813355
9 changed files with 1809 additions and 3 deletions

View File

@@ -8,6 +8,7 @@ const { v4: uuidv4 } = require('uuid');
const { WSClient } = require('@wecom/aibot-node-sdk');
const WebSocket = require('ws');
const crypto = require('crypto');
const messageStore = require('./messageStore');
// 初始化配置存储
const store = new Store({
@@ -221,6 +222,24 @@ class WeComConnection {
console.log('[OpenClaw] Sending via chat.send:', sessionKey);
openclawConnection.send(JSON.stringify(chatSendMessage));
console.log(`[Forward] WeCom -> OpenClaw: ${messageId}`);
// 记录消息到存储
messageStore.saveMessage({
direction: 'inbound',
source: 'wecom',
sessionId: sessionKey,
chatId: chatId,
senderId: body.from?.userid || '',
senderName: body.from?.name || body.from?.userid || '',
messageType: body.image ? 'image' : body.file ? 'file' : body.voice ? 'voice' : 'text',
content: text,
attachments: attachments,
metadata: {
wecomMsgId: messageId,
chatType: chatType,
reqId: reqId
}
});
}
scheduleReconnect() {
@@ -467,6 +486,24 @@ class OpenClawConnection {
try {
await wecomConn.sendMessage(chatId, text, finish, streamId);
console.log(`[Forward] OpenClaw -> WeCom: ${chatId}`);
// 记录消息到存储
const sessionKey = `${botId}:${chatId}`;
messageStore.saveMessage({
direction: 'outbound',
source: 'openclaw',
sessionId: sessionKey,
chatId: chatId,
senderId: 'openclaw',
senderName: 'OpenClaw AI',
messageType: 'text',
content: text,
metadata: {
streamId: streamId,
finish: finish,
botId: botId
}
});
} catch (error) {
console.error('[Forward] Error:', error);
}
@@ -646,6 +683,44 @@ function setupIpcHandlers() {
return { success: false, message: `发送失败:${error.message}` };
}
});
// ============ 消息存储相关 IPC 处理器 ============
// 获取消息列表
ipcMain.handle('get-messages', (event, options = {}) => {
return messageStore.getMessages(options);
});
// 获取会话列表
ipcMain.handle('get-sessions', () => {
return messageStore.getSessions();
});
// 搜索消息
ipcMain.handle('search-messages', (event, query, options = {}) => {
return messageStore.searchMessages(query, options);
});
// 获取统计数据
ipcMain.handle('get-message-stats', () => {
return messageStore.getStats();
});
// 标记消息为已读
ipcMain.handle('mark-messages-read', (event, messageIds) => {
return messageStore.markAsRead(messageIds);
});
// 导出消息
ipcMain.handle('export-messages', (event, options = {}) => {
return messageStore.exportMessages(options);
});
// 清空消息
ipcMain.handle('clear-messages', (event, options = {}) => {
messageStore.clear(options);
return { success: true };
});
}
app.whenReady().then(() => {

264
electron/messageStore.js Normal file
View File

@@ -0,0 +1,264 @@
const fs = require('fs');
const path = require('path');
const { app } = require('electron');
class MessageStore {
constructor() {
this.dataDir = path.join(app.getPath('userData'), 'messages');
this.messagesFile = path.join(this.dataDir, 'messages.json');
this.messages = [];
this.maxMessages = 10000; // 最多保留 10000 条消息
// 初始化存储
this.init();
}
init() {
// 创建目录
if (!fs.existsSync(this.dataDir)) {
fs.mkdirSync(this.dataDir, { recursive: true });
}
// 加载现有消息
if (fs.existsSync(this.messagesFile)) {
try {
const data = fs.readFileSync(this.messagesFile, 'utf-8');
this.messages = JSON.parse(data);
console.log(`[MessageStore] 加载了 ${this.messages.length} 条历史消息`);
} catch (error) {
console.error('[MessageStore] 加载消息失败:', error.message);
this.messages = [];
}
}
}
// 保存消息
saveMessage(message) {
const msg = {
id: message.id || `msg_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`,
timestamp: message.timestamp || new Date().toISOString(),
direction: message.direction, // 'inbound' | 'outbound'
source: message.source, // 'wecom' | 'openclaw'
sessionId: message.sessionId || '',
chatId: message.chatId || '',
senderId: message.senderId || '',
senderName: message.senderName || '',
messageType: message.messageType || 'text', // 'text' | 'image' | 'file' | 'voice'
content: message.content || '',
attachments: message.attachments || [],
metadata: message.metadata || {},
delivered: message.delivered !== false,
read: message.read || false
};
this.messages.push(msg);
// 限制消息数量
if (this.messages.length > this.maxMessages) {
this.messages = this.messages.slice(-this.maxMessages);
}
// 异步保存到文件
this.persist();
return msg;
}
// 批量保存消息
saveMessages(messages) {
messages.forEach(msg => this.saveMessage(msg));
}
// 持久化到文件
persist() {
// 使用异步写入,避免阻塞
setImmediate(() => {
try {
const tempFile = this.messagesFile + '.tmp';
fs.writeFileSync(tempFile, JSON.stringify(this.messages, null, 2), 'utf-8');
fs.renameSync(tempFile, this.messagesFile);
console.log(`[MessageStore] 已保存 ${this.messages.length} 条消息`);
} catch (error) {
console.error('[MessageStore] 保存消息失败:', error.message);
}
});
}
// 获取消息列表
getMessages(options = {}) {
let filtered = [...this.messages];
// 按会话过滤
if (options.sessionId) {
filtered = filtered.filter(m => m.sessionId === options.sessionId);
}
// 按方向过滤
if (options.direction) {
filtered = filtered.filter(m => m.direction === options.direction);
}
// 按来源过滤
if (options.source) {
filtered = filtered.filter(m => m.source === options.source);
}
// 按时间范围过滤
if (options.startTime) {
filtered = filtered.filter(m => new Date(m.timestamp) >= new Date(options.startTime));
}
if (options.endTime) {
filtered = filtered.filter(m => new Date(m.timestamp) <= new Date(options.endTime));
}
// 分页
const limit = options.limit || 100;
const offset = options.offset || 0;
const paginated = filtered.slice(offset, offset + limit);
// 按时间倒序(最新的在前)
paginated.sort((a, b) => new Date(b.timestamp) - new Date(a.timestamp));
return {
messages: paginated,
total: filtered.length,
limit,
offset
};
}
// 获取会话列表
getSessions() {
const sessionMap = new Map();
this.messages.forEach(msg => {
if (!msg.sessionId) return;
if (!sessionMap.has(msg.sessionId)) {
sessionMap.set(msg.sessionId, {
sessionId: msg.sessionId,
chatId: msg.chatId,
lastMessageTime: msg.timestamp,
messageCount: 0,
unreadCount: 0
});
}
const session = sessionMap.get(msg.sessionId);
session.messageCount++;
if (!msg.read) {
session.unreadCount++;
}
if (new Date(msg.timestamp) > new Date(session.lastMessageTime)) {
session.lastMessageTime = msg.timestamp;
}
});
// 按最后消息时间排序
return Array.from(sessionMap.values())
.sort((a, b) => new Date(b.lastMessageTime) - new Date(a.lastMessageTime));
}
// 标记消息为已读
markAsRead(messageIds) {
const ids = Array.isArray(messageIds) ? messageIds : [messageIds];
let updated = 0;
this.messages.forEach(msg => {
if (ids.includes(msg.id) && !msg.read) {
msg.read = true;
updated++;
}
});
if (updated > 0) {
this.persist();
}
return updated;
}
// 搜索消息
searchMessages(query, options = {}) {
const lowerQuery = query.toLowerCase();
const filtered = this.messages.filter(msg => {
// 搜索内容
if (msg.content && msg.content.toLowerCase().includes(lowerQuery)) {
return true;
}
// 搜索发送者
if (msg.senderName && msg.senderName.toLowerCase().includes(lowerQuery)) {
return true;
}
// 搜索会话 ID
if (msg.sessionId && msg.sessionId.toLowerCase().includes(lowerQuery)) {
return true;
}
return false;
});
// 按相关性排序(内容匹配优先)
filtered.sort((a, b) => {
const aScore = (a.content.toLowerCase().includes(lowerQuery) ? 2 : 0) +
(a.senderName.toLowerCase().includes(lowerQuery) ? 1 : 0);
const bScore = (b.content.toLowerCase().includes(lowerQuery) ? 2 : 0) +
(b.senderName.toLowerCase().includes(lowerQuery) ? 1 : 0);
return bScore - aScore;
});
const limit = options.limit || 50;
const offset = options.offset || 0;
return {
messages: filtered.slice(offset, offset + limit),
total: filtered.length,
query
};
}
// 导出消息
exportMessages(options = {}) {
const result = this.getMessages(options);
return JSON.stringify(result.messages, null, 2);
}
// 清空消息
clear(options = {}) {
if (options.sessionId) {
this.messages = this.messages.filter(m => m.sessionId !== options.sessionId);
} else {
this.messages = [];
}
this.persist();
}
// 获取统计数据
getStats() {
const now = new Date();
const today = new Date(now.getFullYear(), now.getMonth(), now.getDate());
const yesterday = new Date(today);
yesterday.setDate(yesterday.getDate() - 1);
return {
total: this.messages.length,
today: this.messages.filter(m => new Date(m.timestamp) >= today).length,
yesterday: this.messages.filter(m =>
new Date(m.timestamp) >= yesterday && new Date(m.timestamp) < today
).length,
inbound: this.messages.filter(m => m.direction === 'inbound').length,
outbound: this.messages.filter(m => m.direction === 'outbound').length,
wecom: this.messages.filter(m => m.source === 'wecom').length,
openclaw: this.messages.filter(m => m.source === 'openclaw').length,
sessions: this.getSessions().length
};
}
}
// 导出单例
module.exports = new MessageStore();

View File

@@ -44,5 +44,28 @@ contextBridge.exposeInMainWorld('electronAPI', {
// 移除事件监听
removeAllListeners: (channel) => {
ipcRenderer.removeAllListeners(channel);
}
},
// ============ 消息存储相关 API ============
// 获取消息列表
getMessages: (options) => ipcRenderer.invoke('get-messages', options),
// 获取会话列表
getSessions: () => ipcRenderer.invoke('get-sessions'),
// 搜索消息
searchMessages: (query, options) => ipcRenderer.invoke('search-messages', query, options),
// 获取统计数据
getMessageStats: () => ipcRenderer.invoke('get-message-stats'),
// 标记消息为已读
markMessagesRead: (messageIds) => ipcRenderer.invoke('mark-messages-read', messageIds),
// 导出消息
exportMessages: (options) => ipcRenderer.invoke('export-messages', options),
// 清空消息
clearMessages: (options) => ipcRenderer.invoke('clear-messages', options)
});