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

版本:v1.0.1
2026-03-10 04:09:26 +08:00

265 lines
7.3 KiB
JavaScript

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();