- 删除 preflight_import 工具、预检卡、resolve 接口与 ui.import_preflight 事件 - 删除 imports.preflight 解析器与 PreflightReport 类型 - SYSTEM_PROMPT 改为上传 Excel 后直接确认列映射/策略并调用 start_import_wizard - 前端同步移除预检类型/组件/测试,保留导入向导
349 lines
10 KiB
TypeScript
349 lines
10 KiB
TypeScript
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 { A2uiSubmissionsService } from './ai-a2ui-submissions.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,
|
|
resolveReviewConversationId,
|
|
submitForm,
|
|
submitReview,
|
|
confirmReviewStep,
|
|
confirmReviewGroup,
|
|
assertReviewImportPermissions,
|
|
a2uiSubmitInfo,
|
|
buildFormSubmitModelContent,
|
|
markFormSubmittedOnMessage,
|
|
a2uiReviewSubmitInfo,
|
|
buildReviewSubmitModelContent,
|
|
markReviewSubmittedOnMessage,
|
|
} from './ai-chat.submissions';
|
|
import { denyWriteTool, executeTool } from './ai-chat.tools';
|
|
import { 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 a2uiSubmissions?: A2uiSubmissionsService,
|
|
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>;
|
|
submissionId?: string;
|
|
fieldErrors?: Array<{ field: string; message: string }>;
|
|
}): string {
|
|
return buildFormSubmitModelContent(submit);
|
|
}
|
|
|
|
buildReviewSubmitModelContent(submit: {
|
|
reviewId: string;
|
|
reviewTitle: string;
|
|
resultMessage: string;
|
|
submissionId?: string;
|
|
nextSteps?: Array<{ key: string; label: 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);
|
|
}
|
|
|
|
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);
|
|
}
|
|
|
|
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);
|
|
}
|
|
|
|
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);
|
|
}
|
|
}
|