/** 会话与消息的读写(A3.7)。只碰平台库,不涉及业务数据(ADR-0008)。 */ import type { ChatMessage, SessionSummary } from '@aps/contracts'; import type { Database } from './db.js'; export interface StoredSession extends SessionSummary { owner: { userId: string; tenantId: string; displayName: string; clientKind: 'desktop' | 'browser' }; } interface SessionRow { session_id: string; project_id: string | null; scope: string; status: string; title: string; owner_user_id: string; owner_tenant_id: string; owner_display_name: string; owner_client_kind: string; created_at: number; } interface MessageRow { message_id: string; session_id: string; role: string; text: string; ts: number; } const toSession = (row: SessionRow): StoredSession => ({ sessionId: row.session_id, projectId: row.project_id, scope: row.scope as SessionSummary['scope'], status: row.status as SessionSummary['status'], title: row.title, createdAt: row.created_at, owner: { userId: row.owner_user_id, tenantId: row.owner_tenant_id, displayName: row.owner_display_name, clientKind: row.owner_client_kind as 'desktop' | 'browser', }, }); const toMessage = (row: MessageRow): ChatMessage => ({ messageId: row.message_id, role: row.role as ChatMessage['role'], ts: row.ts, text: row.text, }); export class SessionRepository { constructor(private readonly db: Database) {} insertSession(session: StoredSession): void { this.db .prepare( 'insert into sessions (session_id, project_id, scope, status, title, owner_user_id, owner_tenant_id, owner_display_name, owner_client_kind, created_at) values (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)', ) .run( session.sessionId, session.projectId, session.scope, session.status, session.title, session.owner.userId, session.owner.tenantId, session.owner.displayName, session.owner.clientKind, session.createdAt, ); } getSession(sessionId: string): StoredSession | undefined { const row = this.db.prepare('select * from sessions where session_id = ?').get(sessionId) as unknown as SessionRow | undefined; return row ? toSession(row) : undefined; } listSessions(projectId?: string): StoredSession[] { const rows = ( projectId === undefined ? this.db.prepare('select * from sessions order by created_at desc').all() : this.db.prepare('select * from sessions where project_id = ? order by created_at desc').all(projectId) ) as unknown as SessionRow[]; return rows.map(toSession); } appendMessage(sessionId: string, message: ChatMessage): void { this.db .prepare('insert into messages (message_id, session_id, role, text, ts) values (?, ?, ?, ?, ?)') .run(message.messageId, sessionId, message.role, message.text, message.ts); } listMessages(sessionId: string): ChatMessage[] { const rows = this.db .prepare('select * from messages where session_id = ? order by ts asc, rowid asc') .all(sessionId) as unknown as MessageRow[]; return rows.map(toMessage); } rememberIdempotencyKey(key: string, sessionId: string, messageId: string, runId: string): void { this.db .prepare( 'insert or ignore into idempotency_keys (idempotency_key, session_id, message_id, run_id, created_at) values (?, ?, ?, ?, ?)', ) .run(key, sessionId, messageId, runId, Date.now()); } findByIdempotencyKey(key: string): { sessionId: string; messageId: string; runId: string } | undefined { const row = this.db.prepare('select session_id, message_id, run_id from idempotency_keys where idempotency_key = ?').get(key) as | { session_id: string; message_id: string; run_id: string } | undefined; return row ? { sessionId: row.session_id, messageId: row.message_id, runId: row.run_id } : undefined; } }