feat(admin): LLM config, knowledge base, and WeCom AI binding
CI / verify (pull_request) Has been cancelled
CI / verify (pull_request) Has been cancelled
Add owner-scoped model API settings, uploadable knowledge bases, and WeCom bot AI/KB wiring with OpenAI-compatible URL normalization. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -0,0 +1,136 @@
|
||||
import {
|
||||
Body,
|
||||
Controller,
|
||||
Delete,
|
||||
Get,
|
||||
Param,
|
||||
Post,
|
||||
Put,
|
||||
Query,
|
||||
UseGuards,
|
||||
} from '@nestjs/common';
|
||||
import type {
|
||||
CreateKnowledgeBaseRequest,
|
||||
CreateKnowledgeDocumentRequest,
|
||||
UpdateKnowledgeBaseRequest,
|
||||
} from '@dukang/shared-types';
|
||||
import { HqAuthGuard } from '../../common/guards/hq-auth.guard';
|
||||
import {
|
||||
HqPermissionGuard,
|
||||
RequireHqPermissions,
|
||||
} from '../../common/guards/hq-permission.guard';
|
||||
import { CurrentUser } from '../../common/decorators/current-user.decorator';
|
||||
import type { AuthUser } from '../../common/guards/jwt-auth.guard';
|
||||
import { HqOperation } from '../../common/hq-operation/hq-operation.decorator';
|
||||
import { HqOperationAction } from '../../common/hq-operation/hq-operation.constants';
|
||||
import { AdminKnowledgeBasesService } from './admin-knowledge-bases.service';
|
||||
|
||||
@Controller('admin/knowledge-bases')
|
||||
@UseGuards(HqAuthGuard, HqPermissionGuard)
|
||||
@RequireHqPermissions('knowledge_bases')
|
||||
export class AdminKnowledgeBasesController {
|
||||
constructor(private readonly service: AdminKnowledgeBasesService) {}
|
||||
|
||||
@Get()
|
||||
async list(
|
||||
@CurrentUser() user: AuthUser,
|
||||
@Query('name') name?: string,
|
||||
@Query('enabled') enabled?: string,
|
||||
@Query('page') page?: string,
|
||||
@Query('pageSize') pageSize?: string,
|
||||
) {
|
||||
const actor = await this.service.resolveActor(user.actorId);
|
||||
return this.service.list(actor, {
|
||||
name,
|
||||
enabled,
|
||||
page: page ? Number(page) : undefined,
|
||||
pageSize: pageSize ? Number(pageSize) : undefined,
|
||||
});
|
||||
}
|
||||
|
||||
@Get('options')
|
||||
async options(@CurrentUser() user: AuthUser) {
|
||||
const actor = await this.service.resolveActor(user.actorId);
|
||||
return this.service.options(actor);
|
||||
}
|
||||
|
||||
@Get(':id')
|
||||
async detail(@CurrentUser() user: AuthUser, @Param('id') id: string) {
|
||||
const actor = await this.service.resolveActor(user.actorId);
|
||||
return this.service.detail(actor, BigInt(id));
|
||||
}
|
||||
|
||||
@Post()
|
||||
@HqOperation({
|
||||
action: HqOperationAction.KNOWLEDGE_BASE_CREATE,
|
||||
refType: 'KNOWLEDGE_BASE',
|
||||
includeBody: true,
|
||||
})
|
||||
async create(@CurrentUser() user: AuthUser, @Body() body: CreateKnowledgeBaseRequest) {
|
||||
const actor = await this.service.resolveActor(user.actorId);
|
||||
return this.service.create(actor, body);
|
||||
}
|
||||
|
||||
@Put(':id')
|
||||
@HqOperation({
|
||||
action: HqOperationAction.KNOWLEDGE_BASE_UPDATE,
|
||||
refType: 'KNOWLEDGE_BASE',
|
||||
refIdField: 'id',
|
||||
includeBody: true,
|
||||
})
|
||||
async update(
|
||||
@CurrentUser() user: AuthUser,
|
||||
@Param('id') id: string,
|
||||
@Body() body: UpdateKnowledgeBaseRequest,
|
||||
) {
|
||||
const actor = await this.service.resolveActor(user.actorId);
|
||||
return this.service.update(actor, BigInt(id), body);
|
||||
}
|
||||
|
||||
@Delete(':id')
|
||||
@HqOperation({
|
||||
action: HqOperationAction.KNOWLEDGE_BASE_DELETE,
|
||||
refType: 'KNOWLEDGE_BASE',
|
||||
refIdField: 'id',
|
||||
})
|
||||
async remove(@CurrentUser() user: AuthUser, @Param('id') id: string) {
|
||||
const actor = await this.service.resolveActor(user.actorId);
|
||||
return this.service.remove(actor, BigInt(id));
|
||||
}
|
||||
|
||||
@Get(':id/documents')
|
||||
async listDocuments(@CurrentUser() user: AuthUser, @Param('id') id: string) {
|
||||
const actor = await this.service.resolveActor(user.actorId);
|
||||
return this.service.listDocuments(actor, BigInt(id));
|
||||
}
|
||||
|
||||
@Post(':id/documents')
|
||||
@HqOperation({
|
||||
action: HqOperationAction.KNOWLEDGE_DOC_CREATE,
|
||||
refType: 'KNOWLEDGE_DOCUMENT',
|
||||
includeBody: true,
|
||||
})
|
||||
async addDocument(
|
||||
@CurrentUser() user: AuthUser,
|
||||
@Param('id') id: string,
|
||||
@Body() body: CreateKnowledgeDocumentRequest,
|
||||
) {
|
||||
const actor = await this.service.resolveActor(user.actorId);
|
||||
return this.service.addDocument(actor, BigInt(id), body);
|
||||
}
|
||||
|
||||
@Delete(':id/documents/:docId')
|
||||
@HqOperation({
|
||||
action: HqOperationAction.KNOWLEDGE_DOC_DELETE,
|
||||
refType: 'KNOWLEDGE_DOCUMENT',
|
||||
refIdField: 'docId',
|
||||
})
|
||||
async removeDocument(
|
||||
@CurrentUser() user: AuthUser,
|
||||
@Param('id') id: string,
|
||||
@Param('docId') docId: string,
|
||||
) {
|
||||
const actor = await this.service.resolveActor(user.actorId);
|
||||
return this.service.removeDocument(actor, BigInt(id), BigInt(docId));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,341 @@
|
||||
import {
|
||||
BadRequestException,
|
||||
ForbiddenException,
|
||||
Injectable,
|
||||
NotFoundException,
|
||||
} from '@nestjs/common';
|
||||
import type {
|
||||
CreateKnowledgeBaseRequest,
|
||||
CreateKnowledgeDocumentRequest,
|
||||
KnowledgeBaseDto,
|
||||
KnowledgeBaseOptionDto,
|
||||
KnowledgeDocumentDto,
|
||||
UpdateKnowledgeBaseRequest,
|
||||
} from '@dukang/shared-types';
|
||||
import { PrismaService } from '../../common/prisma/prisma.module';
|
||||
import { serializeBigInt } from '../../common/decorators/current-user.decorator';
|
||||
|
||||
type ActorCtx = { actorId: bigint; isSuperAdmin: boolean };
|
||||
|
||||
const TEXT_EXT = /\.(txt|md|markdown|csv|json|log)$/i;
|
||||
|
||||
@Injectable()
|
||||
export class AdminKnowledgeBasesService {
|
||||
constructor(private readonly prisma: PrismaService) {}
|
||||
|
||||
async resolveActor(actorId: bigint): Promise<ActorCtx> {
|
||||
const account = await this.prisma.hqAccount.findUnique({
|
||||
where: { id: actorId },
|
||||
select: { adminRole: true, status: true },
|
||||
});
|
||||
if (!account || account.status !== 'ACTIVE') {
|
||||
throw new ForbiddenException('账号不可用');
|
||||
}
|
||||
return {
|
||||
actorId,
|
||||
isSuperAdmin: account.adminRole === 'SUPER_ADMIN',
|
||||
};
|
||||
}
|
||||
|
||||
async list(
|
||||
actor: ActorCtx,
|
||||
query: { name?: string; enabled?: string; page?: number; pageSize?: number },
|
||||
) {
|
||||
const page = query.page ?? 1;
|
||||
const pageSize = query.pageSize ?? 20;
|
||||
const where: {
|
||||
name?: { contains: string };
|
||||
enabled?: boolean;
|
||||
createdByHqAccountId?: bigint;
|
||||
} = {};
|
||||
if (!actor.isSuperAdmin) where.createdByHqAccountId = actor.actorId;
|
||||
if (query.name?.trim()) where.name = { contains: query.name.trim() };
|
||||
if (query.enabled === 'true' || query.enabled === 'false') {
|
||||
where.enabled = query.enabled === 'true';
|
||||
}
|
||||
|
||||
const [items, total] = await Promise.all([
|
||||
this.prisma.knowledgeBase.findMany({
|
||||
where,
|
||||
orderBy: [{ id: 'desc' }],
|
||||
skip: (page - 1) * pageSize,
|
||||
take: pageSize,
|
||||
include: { _count: { select: { documents: true } } },
|
||||
}),
|
||||
this.prisma.knowledgeBase.count({ where }),
|
||||
]);
|
||||
|
||||
const ownerIds = [...new Set(items.map((i) => i.createdByHqAccountId))];
|
||||
const owners = ownerIds.length
|
||||
? await this.prisma.hqAccount.findMany({
|
||||
where: { id: { in: ownerIds } },
|
||||
select: { id: true, name: true },
|
||||
})
|
||||
: [];
|
||||
const ownerMap = new Map(owners.map((o) => [o.id.toString(), o.name]));
|
||||
|
||||
return serializeBigInt({
|
||||
items: items.map((row) =>
|
||||
this.toKbDto(row, actor, ownerMap.get(row.createdByHqAccountId.toString()) ?? null, row._count.documents),
|
||||
),
|
||||
total,
|
||||
page,
|
||||
pageSize,
|
||||
});
|
||||
}
|
||||
|
||||
async options(actor: ActorCtx): Promise<KnowledgeBaseOptionDto[]> {
|
||||
const where: { enabled: boolean; createdByHqAccountId?: bigint } = { enabled: true };
|
||||
if (!actor.isSuperAdmin) where.createdByHqAccountId = actor.actorId;
|
||||
const rows = await this.prisma.knowledgeBase.findMany({
|
||||
where,
|
||||
orderBy: [{ id: 'desc' }],
|
||||
include: { _count: { select: { documents: true } } },
|
||||
});
|
||||
return rows.map((r) => ({
|
||||
id: r.id.toString(),
|
||||
name: r.name,
|
||||
enabled: r.enabled,
|
||||
documentCount: r._count.documents,
|
||||
}));
|
||||
}
|
||||
|
||||
async detail(actor: ActorCtx, id: bigint) {
|
||||
const row = await this.requireKb(actor, id);
|
||||
const count = await this.prisma.knowledgeDocument.count({ where: { knowledgeBaseId: id } });
|
||||
const owner = await this.prisma.hqAccount.findUnique({
|
||||
where: { id: row.createdByHqAccountId },
|
||||
select: { name: true },
|
||||
});
|
||||
return this.toKbDto(row, actor, owner?.name ?? null, count);
|
||||
}
|
||||
|
||||
async create(actor: ActorCtx, dto: CreateKnowledgeBaseRequest) {
|
||||
const name = dto.name?.trim();
|
||||
if (!name) throw new BadRequestException('请填写名称');
|
||||
const row = await this.prisma.knowledgeBase.create({
|
||||
data: {
|
||||
name,
|
||||
description: dto.description?.trim() || null,
|
||||
enabled: dto.enabled !== false,
|
||||
createdByHqAccountId: actor.actorId,
|
||||
},
|
||||
});
|
||||
return this.toKbDto(row, actor, null, 0);
|
||||
}
|
||||
|
||||
async update(actor: ActorCtx, id: bigint, dto: UpdateKnowledgeBaseRequest) {
|
||||
const row = await this.requireKb(actor, id);
|
||||
this.requireWrite(actor, row);
|
||||
|
||||
if (!actor.isSuperAdmin) {
|
||||
// 创建人可改名称/描述/启用
|
||||
const data: {
|
||||
name?: string;
|
||||
description?: string | null;
|
||||
enabled?: boolean;
|
||||
} = {};
|
||||
if (dto.name !== undefined) {
|
||||
const name = dto.name.trim();
|
||||
if (!name) throw new BadRequestException('名称不能为空');
|
||||
data.name = name;
|
||||
}
|
||||
if (dto.description !== undefined) data.description = dto.description?.trim() || null;
|
||||
if (dto.enabled !== undefined) data.enabled = dto.enabled;
|
||||
const updated = await this.prisma.knowledgeBase.update({ where: { id }, data });
|
||||
const count = await this.prisma.knowledgeDocument.count({ where: { knowledgeBaseId: id } });
|
||||
return this.toKbDto(updated, actor, null, count);
|
||||
}
|
||||
|
||||
const data: {
|
||||
name?: string;
|
||||
description?: string | null;
|
||||
enabled?: boolean;
|
||||
} = {};
|
||||
if (dto.name !== undefined) {
|
||||
const name = dto.name.trim();
|
||||
if (!name) throw new BadRequestException('名称不能为空');
|
||||
data.name = name;
|
||||
}
|
||||
if (dto.description !== undefined) data.description = dto.description?.trim() || null;
|
||||
if (dto.enabled !== undefined) data.enabled = dto.enabled;
|
||||
const updated = await this.prisma.knowledgeBase.update({ where: { id }, data });
|
||||
const count = await this.prisma.knowledgeDocument.count({ where: { knowledgeBaseId: id } });
|
||||
return this.toKbDto(updated, actor, null, count);
|
||||
}
|
||||
|
||||
async remove(actor: ActorCtx, id: bigint) {
|
||||
const row = await this.requireKb(actor, id);
|
||||
this.requireWrite(actor, row);
|
||||
await this.prisma.knowledgeBase.delete({ where: { id } });
|
||||
return { ok: true };
|
||||
}
|
||||
|
||||
async listDocuments(actor: ActorCtx, kbId: bigint) {
|
||||
await this.requireKb(actor, kbId);
|
||||
const rows = await this.prisma.knowledgeDocument.findMany({
|
||||
where: { knowledgeBaseId: kbId },
|
||||
orderBy: [{ id: 'desc' }],
|
||||
});
|
||||
return serializeBigInt({ items: rows.map((r) => this.toDocDto(r)) });
|
||||
}
|
||||
|
||||
async addDocument(actor: ActorCtx, kbId: bigint, dto: CreateKnowledgeDocumentRequest) {
|
||||
const kb = await this.requireKb(actor, kbId);
|
||||
this.requireWrite(actor, kb);
|
||||
|
||||
const title = dto.title?.trim();
|
||||
if (!title) throw new BadRequestException('请填写标题');
|
||||
|
||||
let contentText = dto.contentText?.trim() || '';
|
||||
let status: 'READY' | 'EMPTY' | 'FAILED' = 'EMPTY';
|
||||
let errorMessage: string | null = null;
|
||||
|
||||
if (contentText) {
|
||||
status = 'READY';
|
||||
} else if (dto.fileUrl?.trim()) {
|
||||
const fileName = dto.fileName?.trim() || '';
|
||||
if (TEXT_EXT.test(fileName) || isLikelyTextMime(dto.mimeType)) {
|
||||
try {
|
||||
contentText = await fetchText(dto.fileUrl.trim());
|
||||
status = contentText.trim() ? 'READY' : 'EMPTY';
|
||||
if (!contentText.trim()) errorMessage = '文件内容为空';
|
||||
} catch (e) {
|
||||
status = 'FAILED';
|
||||
errorMessage = e instanceof Error ? e.message : String(e);
|
||||
}
|
||||
} else {
|
||||
status = 'EMPTY';
|
||||
errorMessage = '非文本文件未抽取正文,请粘贴文本或上传 .txt/.md';
|
||||
}
|
||||
} else {
|
||||
throw new BadRequestException('请粘贴正文或上传文件');
|
||||
}
|
||||
|
||||
const row = await this.prisma.knowledgeDocument.create({
|
||||
data: {
|
||||
knowledgeBaseId: kbId,
|
||||
title,
|
||||
fileName: dto.fileName?.trim() || null,
|
||||
fileUrl: dto.fileUrl?.trim() || null,
|
||||
mimeType: dto.mimeType?.trim() || null,
|
||||
sizeBytes: dto.sizeBytes ?? null,
|
||||
contentText: contentText || null,
|
||||
status,
|
||||
errorMessage,
|
||||
},
|
||||
});
|
||||
return this.toDocDto(row);
|
||||
}
|
||||
|
||||
async removeDocument(actor: ActorCtx, kbId: bigint, docId: bigint) {
|
||||
const kb = await this.requireKb(actor, kbId);
|
||||
this.requireWrite(actor, kb);
|
||||
const doc = await this.prisma.knowledgeDocument.findFirst({
|
||||
where: { id: docId, knowledgeBaseId: kbId },
|
||||
});
|
||||
if (!doc) throw new NotFoundException('文档不存在');
|
||||
await this.prisma.knowledgeDocument.delete({ where: { id: docId } });
|
||||
return { ok: true };
|
||||
}
|
||||
|
||||
private async requireKb(actor: ActorCtx, id: bigint) {
|
||||
const row = await this.prisma.knowledgeBase.findUnique({ where: { id } });
|
||||
if (!row) throw new NotFoundException('知识库不存在');
|
||||
if (!actor.isSuperAdmin && row.createdByHqAccountId !== actor.actorId) {
|
||||
throw new ForbiddenException('无权查看该知识库');
|
||||
}
|
||||
return row;
|
||||
}
|
||||
|
||||
private requireWrite(
|
||||
actor: ActorCtx,
|
||||
row: { createdByHqAccountId: bigint },
|
||||
) {
|
||||
if (actor.isSuperAdmin) return;
|
||||
if (row.createdByHqAccountId !== actor.actorId) {
|
||||
throw new ForbiddenException('只能操作自己创建的知识库');
|
||||
}
|
||||
}
|
||||
|
||||
private toKbDto(
|
||||
row: {
|
||||
id: bigint;
|
||||
name: string;
|
||||
description: string | null;
|
||||
enabled: boolean;
|
||||
createdByHqAccountId: bigint;
|
||||
createdAt: Date;
|
||||
updatedAt: Date;
|
||||
},
|
||||
actor: ActorCtx,
|
||||
createdByName: string | null,
|
||||
documentCount: number,
|
||||
): KnowledgeBaseDto {
|
||||
const isOwner = row.createdByHqAccountId === actor.actorId;
|
||||
return {
|
||||
id: row.id.toString(),
|
||||
name: row.name,
|
||||
description: row.description,
|
||||
enabled: row.enabled,
|
||||
documentCount,
|
||||
createdByHqAccountId: row.createdByHqAccountId.toString(),
|
||||
createdByName,
|
||||
isOwner,
|
||||
canEditFull: actor.isSuperAdmin || isOwner,
|
||||
createdAt: row.createdAt.toISOString(),
|
||||
updatedAt: row.updatedAt.toISOString(),
|
||||
};
|
||||
}
|
||||
|
||||
private toDocDto(row: {
|
||||
id: bigint;
|
||||
knowledgeBaseId: bigint;
|
||||
title: string;
|
||||
fileName: string | null;
|
||||
fileUrl: string | null;
|
||||
mimeType: string | null;
|
||||
sizeBytes: number | null;
|
||||
contentText: string | null;
|
||||
status: string;
|
||||
errorMessage: string | null;
|
||||
createdAt: Date;
|
||||
updatedAt: Date;
|
||||
}): KnowledgeDocumentDto {
|
||||
const status =
|
||||
row.status === 'READY' || row.status === 'FAILED' || row.status === 'EMPTY'
|
||||
? row.status
|
||||
: 'EMPTY';
|
||||
return {
|
||||
id: row.id.toString(),
|
||||
knowledgeBaseId: row.knowledgeBaseId.toString(),
|
||||
title: row.title,
|
||||
fileName: row.fileName,
|
||||
fileUrl: row.fileUrl,
|
||||
mimeType: row.mimeType,
|
||||
sizeBytes: row.sizeBytes,
|
||||
hasContent: !!(row.contentText && row.contentText.trim()),
|
||||
status,
|
||||
errorMessage: row.errorMessage,
|
||||
createdAt: row.createdAt.toISOString(),
|
||||
updatedAt: row.updatedAt.toISOString(),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
function isLikelyTextMime(mime?: string | null) {
|
||||
if (!mime) return false;
|
||||
return (
|
||||
mime.startsWith('text/') ||
|
||||
mime === 'application/json' ||
|
||||
mime === 'application/markdown'
|
||||
);
|
||||
}
|
||||
|
||||
async function fetchText(url: string): Promise<string> {
|
||||
const res = await fetch(url);
|
||||
if (!res.ok) throw new Error(`下载文件失败 HTTP ${res.status}`);
|
||||
const buf = await res.arrayBuffer();
|
||||
if (buf.byteLength > 2 * 1024 * 1024) throw new Error('文本文件超过 2MB');
|
||||
return new TextDecoder('utf-8', { fatal: false }).decode(buf);
|
||||
}
|
||||
@@ -0,0 +1,110 @@
|
||||
import {
|
||||
Body,
|
||||
Controller,
|
||||
Delete,
|
||||
Get,
|
||||
Param,
|
||||
Post,
|
||||
Put,
|
||||
Query,
|
||||
UseGuards,
|
||||
} from '@nestjs/common';
|
||||
import type {
|
||||
CreateLlmApiConfigRequest,
|
||||
UpdateLlmApiConfigRequest,
|
||||
} from '@dukang/shared-types';
|
||||
import { HqAuthGuard } from '../../common/guards/hq-auth.guard';
|
||||
import {
|
||||
HqPermissionGuard,
|
||||
RequireHqPermissions,
|
||||
} from '../../common/guards/hq-permission.guard';
|
||||
import { CurrentUser } from '../../common/decorators/current-user.decorator';
|
||||
import type { AuthUser } from '../../common/guards/jwt-auth.guard';
|
||||
import { HqOperation } from '../../common/hq-operation/hq-operation.decorator';
|
||||
import { HqOperationAction } from '../../common/hq-operation/hq-operation.constants';
|
||||
import { AdminLlmConfigsService } from './admin-llm-configs.service';
|
||||
|
||||
@Controller('admin/llm-configs')
|
||||
@UseGuards(HqAuthGuard, HqPermissionGuard)
|
||||
@RequireHqPermissions('llm_configs')
|
||||
export class AdminLlmConfigsController {
|
||||
constructor(private readonly service: AdminLlmConfigsService) {}
|
||||
|
||||
@Get()
|
||||
async list(
|
||||
@CurrentUser() user: AuthUser,
|
||||
@Query('name') name?: string,
|
||||
@Query('enabled') enabled?: string,
|
||||
@Query('page') page?: string,
|
||||
@Query('pageSize') pageSize?: string,
|
||||
) {
|
||||
const actor = await this.service.resolveActor(user.actorId);
|
||||
return this.service.list(actor, {
|
||||
name,
|
||||
enabled,
|
||||
page: page ? Number(page) : undefined,
|
||||
pageSize: pageSize ? Number(pageSize) : undefined,
|
||||
});
|
||||
}
|
||||
|
||||
@Get('options')
|
||||
async options(@CurrentUser() user: AuthUser) {
|
||||
const actor = await this.service.resolveActor(user.actorId);
|
||||
return this.service.options(actor);
|
||||
}
|
||||
|
||||
@Get(':id')
|
||||
async detail(@CurrentUser() user: AuthUser, @Param('id') id: string) {
|
||||
const actor = await this.service.resolveActor(user.actorId);
|
||||
return this.service.detail(actor, BigInt(id));
|
||||
}
|
||||
|
||||
@Post()
|
||||
@HqOperation({
|
||||
action: HqOperationAction.LLM_CONFIG_CREATE,
|
||||
refType: 'LLM_CONFIG',
|
||||
includeBody: true,
|
||||
})
|
||||
async create(@CurrentUser() user: AuthUser, @Body() body: CreateLlmApiConfigRequest) {
|
||||
const actor = await this.service.resolveActor(user.actorId);
|
||||
return this.service.create(actor, body);
|
||||
}
|
||||
|
||||
@Put(':id')
|
||||
@HqOperation({
|
||||
action: HqOperationAction.LLM_CONFIG_UPDATE,
|
||||
refType: 'LLM_CONFIG',
|
||||
refIdField: 'id',
|
||||
includeBody: true,
|
||||
})
|
||||
async update(
|
||||
@CurrentUser() user: AuthUser,
|
||||
@Param('id') id: string,
|
||||
@Body() body: UpdateLlmApiConfigRequest,
|
||||
) {
|
||||
const actor = await this.service.resolveActor(user.actorId);
|
||||
return this.service.update(actor, BigInt(id), body);
|
||||
}
|
||||
|
||||
@Delete(':id')
|
||||
@HqOperation({
|
||||
action: HqOperationAction.LLM_CONFIG_DELETE,
|
||||
refType: 'LLM_CONFIG',
|
||||
refIdField: 'id',
|
||||
})
|
||||
async remove(@CurrentUser() user: AuthUser, @Param('id') id: string) {
|
||||
const actor = await this.service.resolveActor(user.actorId);
|
||||
return this.service.remove(actor, BigInt(id));
|
||||
}
|
||||
|
||||
@Post(':id/test')
|
||||
@HqOperation({
|
||||
action: HqOperationAction.LLM_CONFIG_TEST,
|
||||
refType: 'LLM_CONFIG',
|
||||
refIdField: 'id',
|
||||
})
|
||||
async test(@CurrentUser() user: AuthUser, @Param('id') id: string) {
|
||||
const actor = await this.service.resolveActor(user.actorId);
|
||||
return this.service.test(actor, BigInt(id));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,290 @@
|
||||
import {
|
||||
BadRequestException,
|
||||
ForbiddenException,
|
||||
Injectable,
|
||||
NotFoundException,
|
||||
} from '@nestjs/common';
|
||||
import {
|
||||
LLM_PROVIDERS,
|
||||
LLM_PROVIDER_PRESETS,
|
||||
type CreateLlmApiConfigRequest,
|
||||
type LlmApiConfigDto,
|
||||
type LlmApiConfigOptionDto,
|
||||
type LlmProvider,
|
||||
type UpdateLlmApiConfigRequest,
|
||||
} from '@dukang/shared-types';
|
||||
import { PrismaService } from '../../common/prisma/prisma.module';
|
||||
import { serializeBigInt } from '../../common/decorators/current-user.decorator';
|
||||
import { LlmChatClient, normalizeLlmBaseUrl } from '../../integrations/llm/llm-chat.client';
|
||||
|
||||
type ActorCtx = { actorId: bigint; isSuperAdmin: boolean };
|
||||
|
||||
function isProvider(v: string): v is LlmProvider {
|
||||
return (LLM_PROVIDERS as readonly string[]).includes(v);
|
||||
}
|
||||
|
||||
@Injectable()
|
||||
export class AdminLlmConfigsService {
|
||||
constructor(
|
||||
private readonly prisma: PrismaService,
|
||||
private readonly llm: LlmChatClient,
|
||||
) {}
|
||||
|
||||
async resolveActor(actorId: bigint): Promise<ActorCtx> {
|
||||
const account = await this.prisma.hqAccount.findUnique({
|
||||
where: { id: actorId },
|
||||
select: { adminRole: true, status: true },
|
||||
});
|
||||
if (!account || account.status !== 'ACTIVE') {
|
||||
throw new ForbiddenException('账号不可用');
|
||||
}
|
||||
return {
|
||||
actorId,
|
||||
isSuperAdmin: account.adminRole === 'SUPER_ADMIN',
|
||||
};
|
||||
}
|
||||
|
||||
async list(
|
||||
actor: ActorCtx,
|
||||
query: { name?: string; enabled?: string; page?: number; pageSize?: number },
|
||||
) {
|
||||
const page = query.page ?? 1;
|
||||
const pageSize = query.pageSize ?? 20;
|
||||
const where: {
|
||||
name?: { contains: string };
|
||||
enabled?: boolean;
|
||||
createdByHqAccountId?: bigint;
|
||||
} = {};
|
||||
if (!actor.isSuperAdmin) where.createdByHqAccountId = actor.actorId;
|
||||
if (query.name?.trim()) where.name = { contains: query.name.trim() };
|
||||
if (query.enabled === 'true' || query.enabled === 'false') {
|
||||
where.enabled = query.enabled === 'true';
|
||||
}
|
||||
|
||||
const [items, total] = await Promise.all([
|
||||
this.prisma.llmApiConfig.findMany({
|
||||
where,
|
||||
orderBy: [{ id: 'desc' }],
|
||||
skip: (page - 1) * pageSize,
|
||||
take: pageSize,
|
||||
}),
|
||||
this.prisma.llmApiConfig.count({ where }),
|
||||
]);
|
||||
|
||||
const ownerIds = [...new Set(items.map((i) => i.createdByHqAccountId))];
|
||||
const owners = ownerIds.length
|
||||
? await this.prisma.hqAccount.findMany({
|
||||
where: { id: { in: ownerIds } },
|
||||
select: { id: true, name: true },
|
||||
})
|
||||
: [];
|
||||
const ownerMap = new Map(owners.map((o) => [o.id.toString(), o.name]));
|
||||
|
||||
return serializeBigInt({
|
||||
items: items.map((row) => this.toDto(row, actor, ownerMap.get(row.createdByHqAccountId.toString()) ?? null)),
|
||||
total,
|
||||
page,
|
||||
pageSize,
|
||||
});
|
||||
}
|
||||
|
||||
/** 企微绑定下拉:已启用;非超管仅自己的 */
|
||||
async options(actor: ActorCtx): Promise<LlmApiConfigOptionDto[]> {
|
||||
const where: { enabled: boolean; createdByHqAccountId?: bigint } = { enabled: true };
|
||||
if (!actor.isSuperAdmin) where.createdByHqAccountId = actor.actorId;
|
||||
const rows = await this.prisma.llmApiConfig.findMany({
|
||||
where,
|
||||
orderBy: [{ id: 'desc' }],
|
||||
select: { id: true, name: true, provider: true, modelName: true, enabled: true },
|
||||
});
|
||||
return rows.map((r) => ({
|
||||
id: r.id.toString(),
|
||||
name: r.name,
|
||||
provider: (isProvider(r.provider) ? r.provider : 'CUSTOM') as LlmProvider,
|
||||
modelName: r.modelName,
|
||||
enabled: r.enabled,
|
||||
}));
|
||||
}
|
||||
|
||||
async detail(actor: ActorCtx, id: bigint) {
|
||||
const row = await this.requireReadable(actor, id);
|
||||
const owner = await this.prisma.hqAccount.findUnique({
|
||||
where: { id: row.createdByHqAccountId },
|
||||
select: { name: true },
|
||||
});
|
||||
return this.toDto(row, actor, owner?.name ?? null);
|
||||
}
|
||||
|
||||
async create(actor: ActorCtx, dto: CreateLlmApiConfigRequest) {
|
||||
const name = dto.name?.trim();
|
||||
if (!name) throw new BadRequestException('请填写名称');
|
||||
if (!isProvider(dto.provider)) throw new BadRequestException('无效提供商');
|
||||
const apiKey = dto.apiKey?.trim();
|
||||
if (!apiKey) throw new BadRequestException('请填写 API Key');
|
||||
|
||||
const preset = LLM_PROVIDER_PRESETS[dto.provider];
|
||||
const baseUrl = normalizeLlmBaseUrl(dto.baseUrl?.trim() || preset.defaultBaseUrl);
|
||||
const modelName = dto.modelName?.trim() || preset.defaultModel;
|
||||
if (!baseUrl) throw new BadRequestException('请填写 Base URL');
|
||||
if (!modelName) throw new BadRequestException('请填写模型名');
|
||||
|
||||
const row = await this.prisma.llmApiConfig.create({
|
||||
data: {
|
||||
name,
|
||||
provider: dto.provider,
|
||||
baseUrl,
|
||||
apiKey,
|
||||
modelName,
|
||||
temperature: dto.temperature ?? null,
|
||||
maxTokens: dto.maxTokens ?? null,
|
||||
systemPrompt: dto.systemPrompt?.trim() || null,
|
||||
enabled: dto.enabled !== false,
|
||||
createdByHqAccountId: actor.actorId,
|
||||
},
|
||||
});
|
||||
return this.toDto(row, actor, null);
|
||||
}
|
||||
|
||||
async update(actor: ActorCtx, id: bigint, dto: UpdateLlmApiConfigRequest) {
|
||||
const row = await this.requireReadable(actor, id);
|
||||
const isOwner = row.createdByHqAccountId === actor.actorId;
|
||||
|
||||
if (!actor.isSuperAdmin) {
|
||||
if (!isOwner) throw new ForbiddenException('只能操作自己创建的配置');
|
||||
// 非超管仅可改 enabled
|
||||
const keys = Object.keys(dto).filter((k) => (dto as Record<string, unknown>)[k] !== undefined);
|
||||
if (keys.some((k) => k !== 'enabled')) {
|
||||
throw new ForbiddenException('非超级管理员只能修改配置是否生效');
|
||||
}
|
||||
if (dto.enabled === undefined) throw new BadRequestException('请指定 enabled');
|
||||
const updated = await this.prisma.llmApiConfig.update({
|
||||
where: { id },
|
||||
data: { enabled: dto.enabled },
|
||||
});
|
||||
return this.toDto(updated, actor, null);
|
||||
}
|
||||
|
||||
// 超管全量
|
||||
let provider = row.provider as LlmProvider;
|
||||
if (dto.provider !== undefined) {
|
||||
if (!isProvider(dto.provider)) throw new BadRequestException('无效提供商');
|
||||
provider = dto.provider;
|
||||
}
|
||||
const preset = LLM_PROVIDER_PRESETS[provider];
|
||||
const data: {
|
||||
name?: string;
|
||||
provider?: string;
|
||||
baseUrl?: string;
|
||||
apiKey?: string;
|
||||
modelName?: string;
|
||||
temperature?: number | null;
|
||||
maxTokens?: number | null;
|
||||
systemPrompt?: string | null;
|
||||
enabled?: boolean;
|
||||
} = {};
|
||||
if (dto.name !== undefined) {
|
||||
const name = dto.name.trim();
|
||||
if (!name) throw new BadRequestException('名称不能为空');
|
||||
data.name = name;
|
||||
}
|
||||
if (dto.provider !== undefined) data.provider = provider;
|
||||
if (dto.baseUrl !== undefined) {
|
||||
data.baseUrl = normalizeLlmBaseUrl(dto.baseUrl.trim() || preset.defaultBaseUrl);
|
||||
if (!data.baseUrl) throw new BadRequestException('Base URL 不能为空');
|
||||
}
|
||||
if (dto.apiKey !== undefined && dto.apiKey.trim()) data.apiKey = dto.apiKey.trim();
|
||||
if (dto.modelName !== undefined) {
|
||||
data.modelName = dto.modelName.trim() || preset.defaultModel;
|
||||
if (!data.modelName) throw new BadRequestException('模型名不能为空');
|
||||
}
|
||||
if (dto.temperature !== undefined) data.temperature = dto.temperature;
|
||||
if (dto.maxTokens !== undefined) data.maxTokens = dto.maxTokens;
|
||||
if (dto.systemPrompt !== undefined) data.systemPrompt = dto.systemPrompt?.trim() || null;
|
||||
if (dto.enabled !== undefined) data.enabled = dto.enabled;
|
||||
|
||||
const updated = await this.prisma.llmApiConfig.update({ where: { id }, data });
|
||||
return this.toDto(updated, actor, null);
|
||||
}
|
||||
|
||||
async remove(actor: ActorCtx, id: bigint) {
|
||||
if (!actor.isSuperAdmin) {
|
||||
throw new ForbiddenException('仅超级管理员可删除语言模型配置');
|
||||
}
|
||||
const row = await this.prisma.llmApiConfig.findUnique({ where: { id } });
|
||||
if (!row) throw new NotFoundException('配置不存在');
|
||||
await this.prisma.llmApiConfig.delete({ where: { id } });
|
||||
return { ok: true };
|
||||
}
|
||||
|
||||
async test(actor: ActorCtx, id: bigint) {
|
||||
const row = await this.requireReadable(actor, id);
|
||||
if (!row.enabled) throw new BadRequestException('配置未启用');
|
||||
const reply = await this.llm.chat({
|
||||
baseUrl: row.baseUrl,
|
||||
apiKey: row.apiKey,
|
||||
model: row.modelName,
|
||||
temperature: row.temperature != null ? Number(row.temperature) : 0.2,
|
||||
maxTokens: row.maxTokens ?? 64,
|
||||
messages: [
|
||||
{ role: 'system', content: '用一句话回复:连接成功。' },
|
||||
{ role: 'user', content: 'ping' },
|
||||
],
|
||||
});
|
||||
return { ok: true, reply };
|
||||
}
|
||||
|
||||
private async requireReadable(actor: ActorCtx, id: bigint) {
|
||||
const row = await this.prisma.llmApiConfig.findUnique({ where: { id } });
|
||||
if (!row) throw new NotFoundException('配置不存在');
|
||||
if (!actor.isSuperAdmin && row.createdByHqAccountId !== actor.actorId) {
|
||||
throw new ForbiddenException('无权查看该配置');
|
||||
}
|
||||
return row;
|
||||
}
|
||||
|
||||
private toDto(
|
||||
row: {
|
||||
id: bigint;
|
||||
name: string;
|
||||
provider: string;
|
||||
baseUrl: string;
|
||||
apiKey: string;
|
||||
modelName: string;
|
||||
temperature: { toNumber?: () => number } | number | null;
|
||||
maxTokens: number | null;
|
||||
systemPrompt: string | null;
|
||||
enabled: boolean;
|
||||
createdByHqAccountId: bigint;
|
||||
createdAt: Date;
|
||||
updatedAt: Date;
|
||||
},
|
||||
actor: ActorCtx,
|
||||
createdByName: string | null,
|
||||
): LlmApiConfigDto {
|
||||
const isOwner = row.createdByHqAccountId === actor.actorId;
|
||||
const temp =
|
||||
row.temperature == null
|
||||
? null
|
||||
: typeof row.temperature === 'number'
|
||||
? row.temperature
|
||||
: Number(row.temperature);
|
||||
return {
|
||||
id: row.id.toString(),
|
||||
name: row.name,
|
||||
provider: (isProvider(row.provider) ? row.provider : 'CUSTOM') as LlmProvider,
|
||||
baseUrl: row.baseUrl,
|
||||
modelName: row.modelName,
|
||||
apiKeyConfigured: !!row.apiKey,
|
||||
temperature: temp,
|
||||
maxTokens: row.maxTokens,
|
||||
systemPrompt: row.systemPrompt,
|
||||
enabled: row.enabled,
|
||||
createdByHqAccountId: row.createdByHqAccountId.toString(),
|
||||
createdByName,
|
||||
isOwner,
|
||||
canEditFull: actor.isSuperAdmin,
|
||||
createdAt: row.createdAt.toISOString(),
|
||||
updatedAt: row.updatedAt.toISOString(),
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -17,13 +17,21 @@ import {
|
||||
} from '../../common/guards/hq-permission.guard';
|
||||
import { HqOperation } from '../../common/hq-operation/hq-operation.decorator';
|
||||
import { HqOperationAction } from '../../common/hq-operation/hq-operation.constants';
|
||||
import { CurrentUser } from '../../common/decorators/current-user.decorator';
|
||||
import type { AuthUser } from '../../common/guards/jwt-auth.guard';
|
||||
import { AdminWecomBotsService } from './admin-wecom-bots.service';
|
||||
import { AdminLlmConfigsService } from './admin-llm-configs.service';
|
||||
import { AdminKnowledgeBasesService } from './admin-knowledge-bases.service';
|
||||
|
||||
@Controller('admin/wecom-bots')
|
||||
@UseGuards(HqAuthGuard, HqPermissionGuard)
|
||||
@RequireHqPermissions('wecom_bots')
|
||||
export class AdminWecomBotsController {
|
||||
constructor(private readonly service: AdminWecomBotsService) {}
|
||||
constructor(
|
||||
private readonly service: AdminWecomBotsService,
|
||||
private readonly llmConfigs: AdminLlmConfigsService,
|
||||
private readonly knowledgeBases: AdminKnowledgeBasesService,
|
||||
) {}
|
||||
|
||||
@Get()
|
||||
list(
|
||||
@@ -52,6 +60,16 @@ export class AdminWecomBotsController {
|
||||
return this.service.reloadRuntime();
|
||||
}
|
||||
|
||||
@Get('ai-options')
|
||||
async aiOptions(@CurrentUser() user: AuthUser) {
|
||||
const actor = await this.llmConfigs.resolveActor(user.actorId);
|
||||
const [llmConfigs, knowledgeBases] = await Promise.all([
|
||||
this.llmConfigs.options(actor),
|
||||
this.knowledgeBases.options(actor),
|
||||
]);
|
||||
return { llmConfigs, knowledgeBases };
|
||||
}
|
||||
|
||||
@Get(':id')
|
||||
detail(@Param('id') id: string) {
|
||||
return this.service.detail(BigInt(id));
|
||||
|
||||
@@ -21,6 +21,26 @@ function isWecomRole(v: string): v is WecomBotRole {
|
||||
return (WECOM_BOT_ROLES as readonly string[]).includes(v);
|
||||
}
|
||||
|
||||
type WecomRow = {
|
||||
id: bigint;
|
||||
name: string;
|
||||
role: string;
|
||||
botId: string;
|
||||
secret: string;
|
||||
avatarUrl: string | null;
|
||||
welcome: string | null;
|
||||
permissions: string;
|
||||
aiEnabled: boolean;
|
||||
llmConfigId: bigint | null;
|
||||
knowledgeBaseId: bigint | null;
|
||||
enabled: boolean;
|
||||
sortOrder: number;
|
||||
createdAt: Date;
|
||||
updatedAt: Date;
|
||||
llmConfig?: { id: bigint; name: string } | null;
|
||||
knowledgeBase?: { id: bigint; name: string } | null;
|
||||
};
|
||||
|
||||
@Injectable()
|
||||
export class AdminWecomBotsService {
|
||||
constructor(
|
||||
@@ -48,6 +68,10 @@ export class AdminWecomBotsService {
|
||||
orderBy: [{ sortOrder: 'asc' }, { id: 'asc' }],
|
||||
skip: (page - 1) * pageSize,
|
||||
take: pageSize,
|
||||
include: {
|
||||
llmConfig: { select: { id: true, name: true } },
|
||||
knowledgeBase: { select: { id: true, name: true } },
|
||||
},
|
||||
}),
|
||||
this.prisma.wecomBot.count({ where }),
|
||||
]);
|
||||
@@ -62,7 +86,13 @@ export class AdminWecomBotsService {
|
||||
}
|
||||
|
||||
async detail(id: bigint) {
|
||||
const row = await this.prisma.wecomBot.findUnique({ where: { id } });
|
||||
const row = await this.prisma.wecomBot.findUnique({
|
||||
where: { id },
|
||||
include: {
|
||||
llmConfig: { select: { id: true, name: true } },
|
||||
knowledgeBase: { select: { id: true, name: true } },
|
||||
},
|
||||
});
|
||||
if (!row) throw new NotFoundException('机器人不存在');
|
||||
return this.toDto(row);
|
||||
}
|
||||
@@ -79,6 +109,8 @@ export class AdminWecomBotsService {
|
||||
const exists = await this.prisma.wecomBot.findUnique({ where: { botId } });
|
||||
if (exists) throw new BadRequestException('BotID 已存在');
|
||||
|
||||
const llmConfigId = await this.resolveLlmId(dto.llmConfigId);
|
||||
const knowledgeBaseId = await this.resolveKbId(dto.knowledgeBaseId);
|
||||
const permissions = resolveWecomBotPermissions(dto.role, dto.permissions);
|
||||
const row = await this.prisma.wecomBot.create({
|
||||
data: {
|
||||
@@ -89,9 +121,16 @@ export class AdminWecomBotsService {
|
||||
avatarUrl: dto.avatarUrl?.trim() || null,
|
||||
welcome: dto.welcome?.trim() || null,
|
||||
permissions: JSON.stringify(permissions),
|
||||
aiEnabled: dto.aiEnabled === true,
|
||||
llmConfigId,
|
||||
knowledgeBaseId,
|
||||
enabled: dto.enabled !== false,
|
||||
sortOrder: dto.sortOrder ?? 0,
|
||||
},
|
||||
include: {
|
||||
llmConfig: { select: { id: true, name: true } },
|
||||
knowledgeBase: { select: { id: true, name: true } },
|
||||
},
|
||||
});
|
||||
await this.wecomAibot.reload('bot-create');
|
||||
return this.toDto(row);
|
||||
@@ -132,6 +171,11 @@ export class AdminWecomBotsService {
|
||||
const secret =
|
||||
dto.secret !== undefined && dto.secret.trim() ? dto.secret.trim() : existing.secret;
|
||||
|
||||
const llmConfigId =
|
||||
dto.llmConfigId !== undefined ? await this.resolveLlmId(dto.llmConfigId) : undefined;
|
||||
const knowledgeBaseId =
|
||||
dto.knowledgeBaseId !== undefined ? await this.resolveKbId(dto.knowledgeBaseId) : undefined;
|
||||
|
||||
const row = await this.prisma.wecomBot.update({
|
||||
where: { id },
|
||||
data: {
|
||||
@@ -143,9 +187,16 @@ export class AdminWecomBotsService {
|
||||
dto.avatarUrl === undefined ? undefined : dto.avatarUrl?.trim() || null,
|
||||
welcome: dto.welcome === undefined ? undefined : dto.welcome?.trim() || null,
|
||||
permissions: permissionsJson,
|
||||
aiEnabled: dto.aiEnabled,
|
||||
llmConfigId,
|
||||
knowledgeBaseId,
|
||||
enabled: dto.enabled,
|
||||
sortOrder: dto.sortOrder,
|
||||
},
|
||||
include: {
|
||||
llmConfig: { select: { id: true, name: true } },
|
||||
knowledgeBase: { select: { id: true, name: true } },
|
||||
},
|
||||
});
|
||||
await this.wecomAibot.reload('bot-update');
|
||||
return this.toDto(row);
|
||||
@@ -163,20 +214,25 @@ export class AdminWecomBotsService {
|
||||
return this.wecomAibot.reload('manual');
|
||||
}
|
||||
|
||||
private toDto(row: {
|
||||
id: bigint;
|
||||
name: string;
|
||||
role: string;
|
||||
botId: string;
|
||||
secret: string;
|
||||
avatarUrl: string | null;
|
||||
welcome: string | null;
|
||||
permissions: string;
|
||||
enabled: boolean;
|
||||
sortOrder: number;
|
||||
createdAt: Date;
|
||||
updatedAt: Date;
|
||||
}): WecomBotDto {
|
||||
private async resolveLlmId(raw?: string | null): Promise<bigint | null> {
|
||||
if (raw === undefined) return null;
|
||||
if (raw === null || raw === '') return null;
|
||||
const id = BigInt(raw);
|
||||
const row = await this.prisma.llmApiConfig.findUnique({ where: { id } });
|
||||
if (!row) throw new BadRequestException('语言模型配置不存在');
|
||||
return id;
|
||||
}
|
||||
|
||||
private async resolveKbId(raw?: string | null): Promise<bigint | null> {
|
||||
if (raw === undefined) return null;
|
||||
if (raw === null || raw === '') return null;
|
||||
const id = BigInt(raw);
|
||||
const row = await this.prisma.knowledgeBase.findUnique({ where: { id } });
|
||||
if (!row) throw new BadRequestException('知识库不存在');
|
||||
return id;
|
||||
}
|
||||
|
||||
private toDto(row: WecomRow): WecomBotDto {
|
||||
const role = (isWecomRole(row.role) ? row.role : 'CUSTOM') as WecomBotRole;
|
||||
const permissions = resolveWecomBotPermissions(role, row.permissions) as WecomBotPermission[];
|
||||
return {
|
||||
@@ -188,6 +244,11 @@ export class AdminWecomBotsService {
|
||||
avatarUrl: row.avatarUrl,
|
||||
welcome: row.welcome,
|
||||
permissions,
|
||||
aiEnabled: row.aiEnabled,
|
||||
llmConfigId: row.llmConfigId?.toString() ?? null,
|
||||
llmConfigName: row.llmConfig?.name ?? null,
|
||||
knowledgeBaseId: row.knowledgeBaseId?.toString() ?? null,
|
||||
knowledgeBaseName: row.knowledgeBase?.name ?? null,
|
||||
enabled: row.enabled,
|
||||
sortOrder: row.sortOrder,
|
||||
createdAt: row.createdAt.toISOString(),
|
||||
|
||||
@@ -45,6 +45,7 @@ import { BenefitModule } from '../benefit/benefit.module';
|
||||
import { CommonModule } from '../common/common.module';
|
||||
import { IntegrationsModule } from '../../integrations/integrations.module';
|
||||
import { WecomModule } from '../../integrations/wecom/wecom.module';
|
||||
import { LlmModule } from '../../integrations/llm/llm.module';
|
||||
import { AdminXiaofeixiaController } from './admin-xiaofeixia.controller';
|
||||
import { AdminXiaofeixiaService } from './admin-xiaofeixia.service';
|
||||
import { AdminProductDetailTemplatesController } from './admin-product-detail-templates.controller';
|
||||
@@ -62,10 +63,14 @@ import { AdminDeployService } from './admin-deploy.service';
|
||||
import { AdminSystemConfigController } from './admin-system-config.controller';
|
||||
import { AdminWecomBotsController } from './admin-wecom-bots.controller';
|
||||
import { AdminWecomBotsService } from './admin-wecom-bots.service';
|
||||
import { AdminLlmConfigsController } from './admin-llm-configs.controller';
|
||||
import { AdminLlmConfigsService } from './admin-llm-configs.service';
|
||||
import { AdminKnowledgeBasesController } from './admin-knowledge-bases.controller';
|
||||
import { AdminKnowledgeBasesService } from './admin-knowledge-bases.service';
|
||||
import { AdminFulfillmentProvidersController } from './admin-fulfillment-providers.controller';
|
||||
|
||||
@Module({
|
||||
imports: [CityScopeModule, IamModule, TradeModule, FulfillmentModule, BenefitModule, CommonModule, IntegrationsModule, WecomModule, RedeemModule, StoreModule],
|
||||
imports: [CityScopeModule, IamModule, TradeModule, FulfillmentModule, BenefitModule, CommonModule, IntegrationsModule, WecomModule, LlmModule, RedeemModule, StoreModule],
|
||||
controllers: [
|
||||
AdminDashboardController,
|
||||
AdminDeployController,
|
||||
@@ -102,6 +107,8 @@ import { AdminFulfillmentProvidersController } from './admin-fulfillment-provide
|
||||
AdminHqPermissionsController,
|
||||
AdminSystemConfigController,
|
||||
AdminWecomBotsController,
|
||||
AdminLlmConfigsController,
|
||||
AdminKnowledgeBasesController,
|
||||
AdminFulfillmentProvidersController,
|
||||
],
|
||||
providers: [
|
||||
@@ -129,6 +136,8 @@ import { AdminFulfillmentProvidersController } from './admin-fulfillment-provide
|
||||
AdminHqPermissionsService,
|
||||
AdminDeployService,
|
||||
AdminWecomBotsService,
|
||||
AdminLlmConfigsService,
|
||||
AdminKnowledgeBasesService,
|
||||
SuperAdminGuard,
|
||||
],
|
||||
exports: [CityScopeModule],
|
||||
|
||||
Reference in New Issue
Block a user