Files
gongxue-base/apps/server/src/ai-chat/ai-chat.service-base.ts
wangziqi 9048816abc feat(ai): 移除 Excel 导入预检链路
- 删除 preflight_import 工具、预检卡、resolve 接口与 ui.import_preflight 事件
- 删除 imports.preflight 解析器与 PreflightReport 类型
- SYSTEM_PROMPT 改为上传 Excel 后直接确认列映射/策略并调用 start_import_wizard
- 前端同步移除预检类型/组件/测试,保留导入向导
2026-08-06 15:50:38 +08:00

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);
}
}