/** * 会话存储(A3.7:内存态 + SQLite 持久化)。 * * - 持久部分(会话、消息、幂等键)写进 SQLite:重启后仍可查(ADR-0007) * - 运行态部分(事件总线、进行中的运行)留在内存:进程重启后由平台历史恢复(架构设计 5.1) * ADR-0003:个人会话只读,写操作必须在项目会话内——创建时就把住。 */ import { randomUUID } from 'node:crypto'; import type { ChatMessage, SessionSummary } from '@aps/contracts'; import type { SessionRepository } from '../core/storage/sessions.js'; import { badRequest, notFound } from './errors.js'; import type { Subject } from './subject.js'; import { EventBus } from './events.js'; export type RunState = 'idle' | 'running' | 'cancelling' | 'cancelled'; export interface RunHandle { runId: string; abort: AbortController; } export interface SessionRecord { summary: SessionSummary; /** 会话归属者:引擎回调时的身份来源(ADR-0003:写操作必须在项目会话内) */ owner: Subject; messages: ChatMessage[]; bus: EventBus; state: RunState; runs: Map; /** 幂等键 → 已受理的消息与运行(同一 key 重试不产生第二个 run) */ accepted: Map; } export interface CreateSessionInput { scope: 'personal' | 'project'; projectId?: string | undefined; title?: string | undefined; } export function newId(prefix: string): string { return `${prefix}-${randomUUID()}`; } export class SessionStore { private readonly sessions = new Map(); constructor(private readonly repository?: SessionRepository) {} create(input: CreateSessionInput, owner: Subject): SessionRecord { if (input.scope === 'project' && !input.projectId) throw badRequest('项目会话必须带 projectId(ADR-0003)'); if (input.scope === 'personal' && input.projectId) throw badRequest('个人会话不能带 projectId'); const summary: SessionSummary = { sessionId: newId('s'), projectId: input.scope === 'project' ? (input.projectId ?? null) : null, scope: input.scope, status: 'active', title: input.title ?? (input.scope === 'project' ? '新会话' : '个人会话'), createdAt: Date.now(), }; const record: SessionRecord = { summary, owner, messages: [], bus: new EventBus(), state: 'idle', runs: new Map(), accepted: new Map(), }; this.sessions.set(summary.sessionId, record); this.repository?.insertSession({ ...summary, owner }); return record; } get(sessionId: string): SessionRecord | undefined { const cached = this.sessions.get(sessionId); if (cached) return cached; // 进程重启后按库里的记录“补建”运行态:历史与归属都从存储恢复 const stored = this.repository?.getSession(sessionId); if (!stored) return undefined; const { owner, ...summary } = stored; const record: SessionRecord = { summary, owner, messages: this.repository?.listMessages(sessionId) ?? [], bus: new EventBus(), state: 'idle', runs: new Map(), accepted: new Map(), }; this.sessions.set(sessionId, record); return record; } /** 追加消息:内存与库一起写 */ appendMessage(sessionId: string, message: ChatMessage): void { const record = this.require(sessionId); record.messages.push(message); this.repository?.appendMessage(sessionId, message); } /** 幂等键:受理时记录,重试与重启后都能命中 */ rememberIdempotencyKey(sessionId: string, key: string, messageId: string, runId: string): void { this.require(sessionId).accepted.set(key, { messageId, runId }); this.repository?.rememberIdempotencyKey(key, sessionId, messageId, runId); } findIdempotencyKey(sessionId: string, key: string): { messageId: string; runId: string } | undefined { const cached = this.require(sessionId).accepted.get(key); if (cached) return cached; const stored = this.repository?.findByIdempotencyKey(key); if (stored && stored.sessionId === sessionId) { const value = { messageId: stored.messageId, runId: stored.runId }; this.require(sessionId).accepted.set(key, value); return value; } return undefined; } require(sessionId: string): SessionRecord { const record = this.get(sessionId); if (!record) throw notFound(`会话 ${sessionId}`); return record; } list(projectId?: string): SessionSummary[] { if (this.repository) { return this.repository.listSessions(projectId).map(({ owner: _owner, ...summary }) => summary); } return [...this.sessions.values()] .filter((record) => (projectId ? record.summary.projectId === projectId : true)) .map((record) => record.summary) .sort((a, b) => b.createdAt - a.createdAt); } }