Files
AIclinicalresearch/backend/src/modules/iit-manager/agents/SessionMemory.ts
HaHafeng 303dd78c54 feat(aia): Protocol Agent MVP complete with one-click generation and Word export
- Add one-click research protocol generation with streaming output

- Implement Word document export via Pandoc integration

- Add dynamic dual-panel layout with resizable split pane

- Implement collapsible content for StatePanel stages

- Add conversation history management with title auto-update

- Fix scroll behavior, markdown rendering, and UI layout issues

- Simplify conversation creation logic for reliability
2026-01-25 19:16:36 +08:00

205 lines
4.2 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* SessionMemory - 会话记忆管理器(内存版)
*
* 功能:
* - 存储用户最近3轮对话
* - 提供上下文查询
* - 自动清理过期会话1小时
*
* 设计原则:
* - Node.js内存存储MVP阶段无需数据库
* - 单例模式(全局共享)
* - 轻量级(<100行代码
*/
import { logger } from '../../../common/logging/index.js';
/**
* 对话消息
*/
export interface ConversationMessage {
role: 'user' | 'assistant';
content: string;
timestamp: Date;
}
/**
* 对话历史
*/
export interface ConversationHistory {
userId: string;
messages: ConversationMessage[];
createdAt: Date;
updatedAt: Date;
}
/**
* 会话记忆管理器
*/
export class SessionMemory {
// 内存存储:{ userId: ConversationHistory }
private sessions: Map<string, ConversationHistory> = new Map();
private readonly MAX_HISTORY = 3; // 只保留最近3轮6条消息
private readonly SESSION_TIMEOUT = 3600000; // 1小时毫秒
/**
* 添加对话记录
*/
addMessage(userId: string, role: 'user' | 'assistant', content: string): void {
if (!this.sessions.has(userId)) {
this.sessions.set(userId, {
userId,
messages: [],
createdAt: new Date(),
updatedAt: new Date(),
});
logger.debug('[SessionMemory] 创建新会话', { userId });
}
const session = this.sessions.get(userId)!;
session.messages.push({
role,
content,
timestamp: new Date(),
});
// 只保留最近3轮6条消息3个user + 3个assistant
if (session.messages.length > this.MAX_HISTORY * 2) {
const removed = session.messages.length - this.MAX_HISTORY * 2;
session.messages = session.messages.slice(-this.MAX_HISTORY * 2);
logger.debug('[SessionMemory] 清理历史消息', { userId, removedCount: removed });
}
session.updatedAt = new Date();
logger.debug('[SessionMemory] 添加消息', {
userId,
role,
messageLength: content.length,
totalMessages: session.messages.length,
});
}
/**
* 获取用户对话历史最近N轮
*/
getHistory(userId: string, maxTurns: number = 3): ConversationMessage[] {
const session = this.sessions.get(userId);
if (!session) {
return [];
}
// 返回最近N轮2N条消息
const maxMessages = maxTurns * 2;
return session.messages.length > maxMessages
? session.messages.slice(-maxMessages)
: session.messages;
}
/**
* 获取用户上下文格式化为字符串用于LLM Prompt
*/
getContext(userId: string): string {
const history = this.getHistory(userId, 2); // 只取最近2轮
if (history.length === 0) {
return '';
}
return history
.map((m) => `${m.role === 'user' ? 'PI' : 'Assistant'}: ${m.content}`)
.join('\n');
}
/**
* 清除用户会话
*/
clearSession(userId: string): void {
const existed = this.sessions.delete(userId);
if (existed) {
logger.info('[SessionMemory] 清除会话', { userId });
}
}
/**
* 清理过期会话超过1小时未使用
*/
cleanupExpiredSessions(): void {
const now = Date.now();
let cleanedCount = 0;
for (const [userId, session] of this.sessions.entries()) {
if (now - session.updatedAt.getTime() > this.SESSION_TIMEOUT) {
this.sessions.delete(userId);
cleanedCount++;
}
}
if (cleanedCount > 0) {
logger.info('[SessionMemory] 清理过期会话', { cleanedCount });
}
}
/**
* 获取统计信息(用于监控)
*/
getStats() {
return {
totalSessions: this.sessions.size,
sessions: Array.from(this.sessions.entries()).map(([userId, session]) => ({
userId,
messageCount: session.messages.length,
createdAt: session.createdAt,
updatedAt: session.updatedAt,
})),
};
}
}
// 全局单例
export const sessionMemory = new SessionMemory();
// 定时清理过期会话(每小时)
setInterval(() => {
sessionMemory.cleanupExpiredSessions();
}, 3600000);
logger.info('[SessionMemory] 会话记忆管理器已启动', {
maxHistory: 3,
timeout: '1小时',
});