Files
gongxue-base/apps/server/src/ai-chat/ai-chat.service.ts
wangziqi 14db28afc6 fix(ai): 修复 Nest DI 启动失败
- A2uiSubmissionsService/DataSource/BusinessContextService 等构造参数
  由 import type 改为值导入,确保 Nest 能解析依赖
- 验证:dev 模式后端正常启动,相关 243 个测试通过
2026-08-06 15:39:33 +08:00

198 lines
5.8 KiB
TypeScript

import { Injectable } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
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, AiToolRun } from './entities';
import type {
GenerationInput,
ModelContentPart,
ModelMessage,
} from './ai-chat.types';
import {
requireOwnedConversation,
acquireConversation,
normalizeTitle,
titleFromMessage,
metadataSkillKey,
assertSkillAvailable,
truncateText,
serializeMessage,
} from './ai-chat.conversations';
import { buildContext, buildUserContent } from './ai-chat.streaming';
import { executeGeneration } from './ai-chat.generation';
import {
assertGeneratedLength,
errorCode,
makeRedactingReplacer,
parseToolArguments,
redactText,
safeToolName,
throwIfAborted,
} from './ai-chat.helpers';
import { AiChatServiceBase } from './ai-chat.service-base';
@Injectable()
export class AiChatService extends AiChatServiceBase {
private readonly redactingReplacer = makeRedactingReplacer((value) => this.redactText(value));
constructor(
@InjectRepository(AiConversation)
conversations: Repository<AiConversation>,
@InjectRepository(AiMessage)
messages: Repository<AiMessage>,
@InjectRepository(AiToolRun)
toolRuns: Repository<AiToolRun>,
dataSource: DataSource,
configService: AiConfigService,
toolExecutor: AgentToolExecutor,
modelStream: AiModelStreamService,
attachmentService: AiAttachmentService,
formService: AiFormService,
reviewService: AiReviewService,
chartService: AiChartService,
abilityFactory: CaslAbilityFactory,
authorization: AuthorizationService,
a2uiSubmissions?: A2uiSubmissionsService,
excelReader?: AiExcelReaderService,
importsService?: ImportsService,
opLog?: OperationLogsService,
) {
super(
conversations,
messages,
toolRuns,
dataSource,
configService,
toolExecutor,
modelStream,
attachmentService,
formService,
reviewService,
chartService,
abilityFactory,
authorization,
a2uiSubmissions,
excelReader,
importsService,
opLog,
);
}
listSkills(user: AuthenticatedUser): AgentSkillDescriptor[] {
return this.toolExecutor.listSkills(AgentToolContextFactory.fromAuthenticatedUser(user));
}
serializeMessage(message: AiMessage): Record<string, unknown> {
return serializeMessage(this, message);
}
redactText(value: string): string {
return redactText(value);
}
summarize(value: unknown): string | null {
if (value === undefined || value === null) return null;
let json: string;
try {
json = JSON.stringify(value, this.redactingReplacer);
} catch {
return '[无法序列化]';
}
return this.redactText(json).slice(0, 2000);
}
safeStructured(value: unknown): unknown {
if (value === undefined || value === null) return null;
try {
return JSON.parse(JSON.stringify(value, this.redactingReplacer)) as unknown;
} catch {
return null;
}
}
parseToolArguments(value: string): unknown {
return parseToolArguments(value);
}
safeToolName(name: string): string {
return safeToolName(name);
}
throwIfAborted(signal: AbortSignal): void {
return throwIfAborted(signal);
}
errorCode(error: unknown): string {
return errorCode(error);
}
assertGeneratedLength(reasoning: string, content: string): void {
return assertGeneratedLength(reasoning, content);
}
buildContext(
conversationId: number,
focusUserMessageId: number,
focusContent: string | ModelContentPart[],
skillKey: string | null,
supportsVision: boolean,
): Promise<ModelMessage[]> {
return buildContext(this, conversationId, focusUserMessageId, focusContent, skillKey, supportsVision);
}
buildUserContent(
text: string,
attachments: any[],
supportsVision: boolean,
): Promise<string | ModelContentPart[]> {
return buildUserContent(this, text, attachments, supportsVision);
}
truncateText(value: string, max: number): string {
return truncateText(this, value, max);
}
metadataSkillKey(metadata: Record<string, unknown> | null): string | null {
return metadataSkillKey(this, metadata);
}
normalizeTitle(title?: string): string {
return normalizeTitle(this, title);
}
titleFromMessage(message: string): string {
return titleFromMessage(this, message);
}
assertSkillAvailable(user: AuthenticatedUser, skillKey?: string | null): void {
return assertSkillAvailable(this, user, skillKey);
}
requireOwnedConversation(userId: number, id: number): Promise<AiConversation> {
return requireOwnedConversation(this, userId, id);
}
acquireConversation(conversationId: number): Promise<void> {
return acquireConversation(this, conversationId);
}
executeGeneration(input: GenerationInput): Promise<void> {
return executeGeneration(this, input);
}
}