iconsulting/packages/services/conversation-service/src/domain/entities/conversation.entity.ts

247 lines
6.1 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.

/**
* 对话状态常量
*/
export const ConversationStatus = {
ACTIVE: 'ACTIVE',
ENDED: 'ENDED',
ARCHIVED: 'ARCHIVED',
} as const;
export type ConversationStatusType =
(typeof ConversationStatus)[keyof typeof ConversationStatus];
/**
* 咨询阶段常量
*/
export const ConsultingStage = {
GREETING: 'greeting',
NEEDS_DISCOVERY: 'needs_discovery',
INFO_COLLECTION: 'info_collection',
ASSESSMENT: 'assessment',
RECOMMENDATION: 'recommendation',
OBJECTION_HANDLING: 'objection_handling',
CONVERSION: 'conversion',
HANDOFF: 'handoff',
} as const;
export type ConsultingStageType =
(typeof ConsultingStage)[keyof typeof ConsultingStage];
/**
* 咨询状态结构存储为JSON
*/
export interface ConsultingStateJson {
strategyId: string;
currentStageId: string;
stageTurnCount: number;
collectedInfo: Record<string, unknown>;
assessmentResult?: {
recommendedPrograms: string[];
suitabilityScore: number;
highlights: string[];
concerns: string[];
};
conversionPath?: string;
stageHistory: Array<{
stageId: string;
enteredAt: string;
exitedAt?: string;
turnsInStage: number;
}>;
}
/**
* Device info structure
*/
export interface DeviceInfo {
ip?: string;
userAgent?: string;
fingerprint?: string;
region?: string;
}
/**
* Conversation Domain Entity
*/
export class ConversationEntity {
readonly id: string;
userId: string;
status: ConversationStatusType;
title: string | null;
summary: string | null;
category: string | null;
messageCount: number;
userMessageCount: number;
assistantMessageCount: number;
totalInputTokens: number;
totalOutputTokens: number;
rating: number | null;
feedback: string | null;
hasConverted: boolean;
consultingStage: ConsultingStageType | null;
consultingState: ConsultingStateJson | null;
collectedInfo: Record<string, unknown> | null;
recommendedPrograms: string[] | null;
conversionPath: string | null;
deviceInfo: DeviceInfo | null;
readonly createdAt: Date;
updatedAt: Date;
endedAt: Date | null;
private constructor(props: {
id: string;
userId: string;
status: ConversationStatusType;
title: string | null;
summary: string | null;
category: string | null;
messageCount: number;
userMessageCount: number;
assistantMessageCount: number;
totalInputTokens: number;
totalOutputTokens: number;
rating: number | null;
feedback: string | null;
hasConverted: boolean;
consultingStage: ConsultingStageType | null;
consultingState: ConsultingStateJson | null;
collectedInfo: Record<string, unknown> | null;
recommendedPrograms: string[] | null;
conversionPath: string | null;
deviceInfo: DeviceInfo | null;
createdAt: Date;
updatedAt: Date;
endedAt: Date | null;
}) {
Object.assign(this, props);
}
static create(props: {
id: string;
userId: string;
title?: string;
category?: string;
deviceInfo?: DeviceInfo;
}): ConversationEntity {
const now = new Date();
return new ConversationEntity({
id: props.id,
userId: props.userId,
status: ConversationStatus.ACTIVE,
title: props.title || null,
summary: null,
category: props.category || null,
messageCount: 0,
userMessageCount: 0,
assistantMessageCount: 0,
totalInputTokens: 0,
totalOutputTokens: 0,
rating: null,
feedback: null,
hasConverted: false,
consultingStage: ConsultingStage.GREETING,
consultingState: null,
collectedInfo: null,
recommendedPrograms: null,
conversionPath: null,
deviceInfo: props.deviceInfo || null,
createdAt: now,
updatedAt: now,
endedAt: null,
});
}
static fromPersistence(props: {
id: string;
userId: string;
status: ConversationStatusType;
title: string | null;
summary: string | null;
category: string | null;
messageCount: number;
userMessageCount: number;
assistantMessageCount: number;
totalInputTokens: number;
totalOutputTokens: number;
rating: number | null;
feedback: string | null;
hasConverted: boolean;
consultingStage: ConsultingStageType | null;
consultingState: ConsultingStateJson | null;
collectedInfo: Record<string, unknown> | null;
recommendedPrograms: string[] | null;
conversionPath: string | null;
deviceInfo: DeviceInfo | null;
createdAt: Date;
updatedAt: Date;
endedAt: Date | null;
}): ConversationEntity {
return new ConversationEntity(props);
}
incrementMessageCount(role: 'user' | 'assistant'): void {
this.messageCount++;
if (role === 'user') {
this.userMessageCount++;
} else {
this.assistantMessageCount++;
}
this.updatedAt = new Date();
}
addTokens(inputTokens: number, outputTokens: number): void {
this.totalInputTokens += inputTokens;
this.totalOutputTokens += outputTokens;
this.updatedAt = new Date();
}
updateConsultingStage(stage: ConsultingStageType): void {
this.consultingStage = stage;
this.updatedAt = new Date();
}
updateConsultingState(state: ConsultingStateJson): void {
this.consultingState = state;
this.collectedInfo = state.collectedInfo;
if (state.assessmentResult?.recommendedPrograms) {
this.recommendedPrograms = state.assessmentResult.recommendedPrograms;
}
if (state.conversionPath) {
this.conversionPath = state.conversionPath;
}
this.updatedAt = new Date();
}
setRating(rating: number, feedback?: string): void {
this.rating = rating;
if (feedback) {
this.feedback = feedback;
}
this.updatedAt = new Date();
}
markAsConverted(): void {
this.hasConverted = true;
this.updatedAt = new Date();
}
end(): void {
this.status = ConversationStatus.ENDED;
this.endedAt = new Date();
this.updatedAt = new Date();
}
archive(): void {
this.status = ConversationStatus.ARCHIVED;
this.updatedAt = new Date();
}
isActive(): boolean {
return this.status === ConversationStatus.ACTIVE;
}
isEnded(): boolean {
return this.status === ConversationStatus.ENDED;
}
}