refactor(ai-chat): 拆分超大文件并通过 aislop 100 分门槛
- AiChatService 拆出抽象基类,职责不变 - submissions 按提交内容/评审确认/运行时/流程拆为 4 个模块并保持导出兼容 - 导入工具执行器抽取共享 runner,消除重复签名块 - 全仓 aislop 扫描 100/100,0 警告
This commit is contained in:
372
apps/server/src/ai-chat/ai-chat.service-base.ts
Normal file
372
apps/server/src/ai-chat/ai-chat.service-base.ts
Normal file
@@ -0,0 +1,372 @@
|
||||
import { DataSource, Repository } from 'typeorm';
|
||||
import { AiConfigService } from '../ai-config/ai-config.service';
|
||||
import { AgentToolExecutor } from '../agent-tools/agent-tool.executor';
|
||||
import {
|
||||
AgentToolContextFactory,
|
||||
type AgentSkillDescriptor,
|
||||
} from '../agent-tools/agent-tool.types';
|
||||
import { AuthorizationService, CaslAbilityFactory, type AuthenticatedUser } from '../authorization';
|
||||
import { AiAttachmentService } from './ai-attachment.service';
|
||||
import { ImportsService } from '../imports/imports.service';
|
||||
import { AiChartService } from './ai-chart.service';
|
||||
import { AiExcelReaderService } from './ai-excel-reader.service';
|
||||
import { AiFormService } from './ai-form.service';
|
||||
import { AiReviewService } from './ai-review.service';
|
||||
import { AiModelStreamService } from './ai-model-stream.service';
|
||||
import { OperationLogsService } from '../operation-logs/operation-logs.service';
|
||||
import {
|
||||
AiConversation,
|
||||
AiMessage,
|
||||
AiReview,
|
||||
AiToolRun,
|
||||
type AiReviewSectionType,
|
||||
} from './entities';
|
||||
import type {
|
||||
AiChatServiceContext,
|
||||
AiSseEmitter,
|
||||
GenerationInput,
|
||||
ModelContentPart,
|
||||
ModelMessage,
|
||||
ModelToolCall,
|
||||
PublicConversation,
|
||||
} from './ai-chat.types';
|
||||
import {
|
||||
listConversations,
|
||||
createConversation,
|
||||
updateConversation,
|
||||
deleteConversation,
|
||||
deleteAllConversations,
|
||||
getMessages,
|
||||
deleteMessage,
|
||||
} from './ai-chat.conversations';
|
||||
import {
|
||||
streamMessage,
|
||||
regenerateMessage,
|
||||
editMessage,
|
||||
} from './ai-chat.streaming';
|
||||
import {
|
||||
resolveFormConversationId,
|
||||
resolvePreflightConversationId,
|
||||
resolveReviewConversationId,
|
||||
resolveImportPreflight,
|
||||
submitForm,
|
||||
submitReview,
|
||||
confirmReviewStep,
|
||||
confirmReviewGroup,
|
||||
assertReviewImportPermissions,
|
||||
a2uiSubmitInfo,
|
||||
buildFormSubmitModelContent,
|
||||
markFormSubmittedOnMessage,
|
||||
a2uiReviewSubmitInfo,
|
||||
buildReviewSubmitModelContent,
|
||||
markReviewSubmittedOnMessage,
|
||||
} from './ai-chat.submissions';
|
||||
import { denyWriteTool, executeTool } from './ai-chat.tools';
|
||||
import {
|
||||
executePreflightImport,
|
||||
executeStartImportWizard,
|
||||
} from './ai-chat.tool-actions';
|
||||
export abstract class AiChatServiceBase implements AiChatServiceContext {
|
||||
readonly activeConversations = new Set<number>();
|
||||
|
||||
abstract listSkills(user: AuthenticatedUser): AgentSkillDescriptor[];
|
||||
abstract serializeMessage(message: AiMessage): Record<string, unknown>;
|
||||
abstract redactText(value: string): string;
|
||||
abstract summarize(value: unknown): string | null;
|
||||
abstract safeStructured(value: unknown): unknown;
|
||||
abstract parseToolArguments(value: string): unknown;
|
||||
abstract safeToolName(name: string): string;
|
||||
abstract throwIfAborted(signal: AbortSignal): void;
|
||||
abstract errorCode(error: unknown): string;
|
||||
abstract assertGeneratedLength(reasoning: string, content: string): void;
|
||||
abstract buildContext(
|
||||
conversationId: number,
|
||||
focusUserMessageId: number,
|
||||
focusContent: string | ModelContentPart[],
|
||||
skillKey: string | null,
|
||||
supportsVision: boolean,
|
||||
): Promise<ModelMessage[]>;
|
||||
abstract buildUserContent(
|
||||
text: string,
|
||||
attachments: any[],
|
||||
supportsVision: boolean,
|
||||
): Promise<string | ModelContentPart[]>;
|
||||
abstract truncateText(value: string, max: number): string;
|
||||
abstract metadataSkillKey(metadata: Record<string, unknown> | null): string | null;
|
||||
abstract normalizeTitle(title?: string): string;
|
||||
abstract titleFromMessage(message: string): string;
|
||||
abstract assertSkillAvailable(user: AuthenticatedUser, skillKey?: string | null): void;
|
||||
abstract requireOwnedConversation(userId: number, id: number): Promise<AiConversation>;
|
||||
abstract acquireConversation(conversationId: number): Promise<void>;
|
||||
abstract executeGeneration(input: GenerationInput): Promise<void>;
|
||||
|
||||
constructor(
|
||||
readonly conversations: Repository<AiConversation>,
|
||||
readonly messages: Repository<AiMessage>,
|
||||
readonly toolRuns: Repository<AiToolRun>,
|
||||
readonly dataSource: DataSource,
|
||||
readonly configService: AiConfigService,
|
||||
readonly toolExecutor: AgentToolExecutor,
|
||||
readonly modelStream: AiModelStreamService,
|
||||
readonly attachmentService: AiAttachmentService,
|
||||
readonly formService: AiFormService,
|
||||
readonly reviewService: AiReviewService,
|
||||
readonly chartService: AiChartService,
|
||||
readonly abilityFactory: CaslAbilityFactory,
|
||||
readonly authorization: AuthorizationService,
|
||||
readonly excelReader?: AiExcelReaderService,
|
||||
readonly importsService?: ImportsService,
|
||||
readonly opLog?: OperationLogsService,
|
||||
) {}
|
||||
|
||||
a2uiSubmitInfo(metadata: Record<string, unknown> | null) {
|
||||
return a2uiSubmitInfo(metadata);
|
||||
}
|
||||
|
||||
a2uiReviewSubmitInfo(metadata: Record<string, unknown> | null) {
|
||||
return a2uiReviewSubmitInfo(metadata);
|
||||
}
|
||||
|
||||
buildFormSubmitModelContent(submit: { title: string; values: Record<string, unknown> }): string {
|
||||
return buildFormSubmitModelContent(submit);
|
||||
}
|
||||
|
||||
buildReviewSubmitModelContent(submit: {
|
||||
reviewId: string;
|
||||
reviewTitle: string;
|
||||
resultMessage: string;
|
||||
}): string {
|
||||
return buildReviewSubmitModelContent(submit);
|
||||
}
|
||||
|
||||
markFormSubmittedOnMessage(assistantMessageId: number, conversationId: number): Promise<void> {
|
||||
return markFormSubmittedOnMessage(this, assistantMessageId, conversationId);
|
||||
}
|
||||
|
||||
markReviewSubmittedOnMessage(
|
||||
assistantMessageId: number,
|
||||
conversationId: number,
|
||||
review?: AiReview,
|
||||
): Promise<void> {
|
||||
return markReviewSubmittedOnMessage(this, assistantMessageId, conversationId, review);
|
||||
}
|
||||
|
||||
assertReviewImportPermissions(
|
||||
user: AuthenticatedUser,
|
||||
review: AiReview,
|
||||
sectionKey?: string,
|
||||
sectionType?: AiReviewSectionType,
|
||||
): void {
|
||||
return assertReviewImportPermissions(this, user, review, sectionKey, sectionType);
|
||||
}
|
||||
|
||||
executeTool(
|
||||
messageId: number,
|
||||
call: ModelToolCall,
|
||||
context: ReturnType<typeof AgentToolContextFactory.fromAuthenticatedUser>,
|
||||
allowedSkillKey: string | null,
|
||||
allowWriteTools: boolean,
|
||||
reviewSubmitted: boolean,
|
||||
userId: number,
|
||||
emit: AiSseEmitter,
|
||||
): Promise<string> {
|
||||
return executeTool(
|
||||
this,
|
||||
messageId,
|
||||
call,
|
||||
context,
|
||||
allowedSkillKey,
|
||||
allowWriteTools,
|
||||
reviewSubmitted,
|
||||
userId,
|
||||
emit,
|
||||
);
|
||||
}
|
||||
|
||||
denyWriteTool(
|
||||
messageId: number,
|
||||
call: ModelToolCall,
|
||||
emit: AiSseEmitter,
|
||||
): Promise<string> {
|
||||
return denyWriteTool(this, messageId, call, emit);
|
||||
}
|
||||
|
||||
executeStartImportWizard(
|
||||
messageId: number,
|
||||
call: ModelToolCall,
|
||||
context: ReturnType<typeof AgentToolContextFactory.fromAuthenticatedUser>,
|
||||
emit: AiSseEmitter,
|
||||
): Promise<string> {
|
||||
return executeStartImportWizard(this, messageId, call, context, emit);
|
||||
}
|
||||
|
||||
executePreflightImport(
|
||||
messageId: number,
|
||||
call: ModelToolCall,
|
||||
context: ReturnType<typeof AgentToolContextFactory.fromAuthenticatedUser>,
|
||||
emit: AiSseEmitter,
|
||||
): Promise<string> {
|
||||
return executePreflightImport(this, messageId, call, context, emit);
|
||||
}
|
||||
|
||||
listConversations(userId: number): Promise<PublicConversation[]> {
|
||||
return listConversations(this, userId);
|
||||
}
|
||||
|
||||
createConversation(
|
||||
user: AuthenticatedUser,
|
||||
title?: string,
|
||||
lockedSkillKey?: string | null,
|
||||
): Promise<PublicConversation> {
|
||||
return createConversation(this, user, title, lockedSkillKey);
|
||||
}
|
||||
|
||||
updateConversation(
|
||||
user: AuthenticatedUser,
|
||||
id: number,
|
||||
dto: { title?: string; lockedSkillKey?: string | null },
|
||||
): Promise<PublicConversation> {
|
||||
return updateConversation(this, user, id, dto);
|
||||
}
|
||||
|
||||
deleteConversation(userId: number, id: number): Promise<void> {
|
||||
return deleteConversation(this, userId, id);
|
||||
}
|
||||
|
||||
deleteAllConversations(userId: number): Promise<number> {
|
||||
return deleteAllConversations(this, userId);
|
||||
}
|
||||
|
||||
getMessages(userId: number, conversationId: number, page = 1, limit = 50) {
|
||||
return getMessages(this, userId, conversationId, page, limit);
|
||||
}
|
||||
|
||||
deleteMessage(
|
||||
userId: number,
|
||||
conversationId: number,
|
||||
messageId: number,
|
||||
): Promise<{ deletedIds: number[] }> {
|
||||
return deleteMessage(this, userId, conversationId, messageId);
|
||||
}
|
||||
|
||||
streamMessage(
|
||||
user: AuthenticatedUser,
|
||||
conversationId: number,
|
||||
dto: {
|
||||
message: string;
|
||||
attachmentIds?: number[];
|
||||
clientRequestId: string;
|
||||
skillKey?: string | null;
|
||||
reasoningEffort?: string | null;
|
||||
},
|
||||
signal: AbortSignal,
|
||||
emit: AiSseEmitter,
|
||||
onReady: () => void,
|
||||
): Promise<void> {
|
||||
return streamMessage(this, user, conversationId, dto, signal, emit, onReady);
|
||||
}
|
||||
|
||||
regenerateMessage(
|
||||
user: AuthenticatedUser,
|
||||
conversationId: number,
|
||||
assistantMessageId: number,
|
||||
clientRequestId: string,
|
||||
reasoningEffort: string | null | undefined,
|
||||
signal: AbortSignal,
|
||||
emit: AiSseEmitter,
|
||||
onReady: () => void,
|
||||
): Promise<void> {
|
||||
return regenerateMessage(
|
||||
this,
|
||||
user,
|
||||
conversationId,
|
||||
assistantMessageId,
|
||||
clientRequestId,
|
||||
reasoningEffort,
|
||||
signal,
|
||||
emit,
|
||||
onReady,
|
||||
);
|
||||
}
|
||||
|
||||
editMessage(
|
||||
user: AuthenticatedUser,
|
||||
conversationId: number,
|
||||
messageId: number,
|
||||
dto: { content: string; clientRequestId: string; reasoningEffort?: string | null },
|
||||
signal: AbortSignal,
|
||||
emit: AiSseEmitter,
|
||||
onReady: () => void,
|
||||
): Promise<void> {
|
||||
return editMessage(this, user, conversationId, messageId, dto, signal, emit, onReady);
|
||||
}
|
||||
|
||||
resolveFormConversationId(userId: number, formId: string): Promise<number> {
|
||||
return resolveFormConversationId(this, userId, formId);
|
||||
}
|
||||
|
||||
resolveReviewConversationId(userId: number, reviewId: string): Promise<number> {
|
||||
return resolveReviewConversationId(this, userId, reviewId);
|
||||
}
|
||||
|
||||
resolvePreflightConversationId(userId: number, messageId: number): Promise<number> {
|
||||
return resolvePreflightConversationId(this, userId, messageId);
|
||||
}
|
||||
|
||||
submitForm(
|
||||
user: AuthenticatedUser,
|
||||
formId: string,
|
||||
dto: {
|
||||
values: Record<string, unknown>;
|
||||
clientRequestId: string;
|
||||
reasoningEffort?: string | null;
|
||||
},
|
||||
signal: AbortSignal,
|
||||
emit: AiSseEmitter,
|
||||
onReady: () => void,
|
||||
): Promise<void> {
|
||||
return submitForm(this, user, formId, dto, signal, emit, onReady);
|
||||
}
|
||||
|
||||
submitReview(
|
||||
user: AuthenticatedUser,
|
||||
reviewId: string,
|
||||
dto: { clientRequestId: string; reasoningEffort?: string | null },
|
||||
signal: AbortSignal,
|
||||
emit: AiSseEmitter,
|
||||
onReady: () => void,
|
||||
): Promise<void> {
|
||||
return submitReview(this, user, reviewId, dto, signal, emit, onReady);
|
||||
}
|
||||
|
||||
resolveImportPreflight(
|
||||
user: AuthenticatedUser,
|
||||
messageId: number,
|
||||
dto: {
|
||||
clientRequestId: string;
|
||||
mapping?: Record<string, unknown>;
|
||||
settings?: Record<string, unknown>;
|
||||
},
|
||||
signal: AbortSignal,
|
||||
emit: AiSseEmitter,
|
||||
onReady: () => void,
|
||||
): Promise<void> {
|
||||
return resolveImportPreflight(this, user, messageId, dto, signal, emit, onReady);
|
||||
}
|
||||
|
||||
confirmReviewStep(
|
||||
user: AuthenticatedUser,
|
||||
reviewId: string,
|
||||
sectionKey: string,
|
||||
): Promise<Record<string, unknown>> {
|
||||
return confirmReviewStep(this, user, reviewId, sectionKey);
|
||||
}
|
||||
|
||||
confirmReviewGroup(
|
||||
user: AuthenticatedUser,
|
||||
reviewId: string,
|
||||
type: AiReviewSectionType,
|
||||
): Promise<Record<string, unknown>> {
|
||||
return confirmReviewGroup(this, user, reviewId, type);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user