aps-agent/server/app/sessions.ts

85 lines
2.6 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.

/**
* 会话存储(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<string, RunHandle>;
/** 幂等键 → 已受理的消息与运行(同一 key 重试不产生第二个 run) */
accepted: Map<string, { messageId: string; runId: string }>;
}
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<string, SessionRecord>();
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);
}
}