89 lines
2.8 KiB
TypeScript
89 lines
2.8 KiB
TypeScript
/**
|
||
* 会话存储(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 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<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, 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);
|
||
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);
|
||
}
|
||
} |