/** * 会话存储(A1 阶段内存实现)。 * * ADR-0003:个人会话只读,写操作必须在项目会话内——这里在创建时就把住: * scope=project 必须带 projectId,scope=personal 不允许带。 * A3.7 落库后换成持久层;主键按 ADR-0007 用应用侧生成。 */ import { randomUUID } from 'node:crypto'; import type { ChatMessage, SessionSummary } from '@aps/contracts'; import { badRequest, notFound } from './errors.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; 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(); create(input: CreateSessionInput): 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, messages: [], bus: new EventBus(), state: 'idle', runs: new Map(), accepted: new Map(), }; this.sessions.set(summary.sessionId, record); return record; } get(sessionId: string): SessionRecord | undefined { return this.sessions.get(sessionId); } require(sessionId: string): SessionRecord { const record = this.get(sessionId); if (!record) throw notFound(`会话 ${sessionId}`); return record; } list(projectId?: string): SessionSummary[] { return [...this.sessions.values()] .filter((record) => (projectId ? record.summary.projectId === projectId : true)) .map((record) => record.summary) .sort((a, b) => b.createdAt - a.createdAt); } }