import { BadRequestException, Injectable, NotFoundException, } from '@nestjs/common'; import { WECOM_BOT_ROLES, normalizeWecomBotRole, parseWecomBotPermissions, parseWecomUserIdList, WECOM_BOT_ROLE_DEFAULT_PERMISSIONS, type WecomBotDto, type WecomBotPermission, type WecomBotRole, } from '@dukang/shared-types'; import { PrismaService } from '../../common/prisma/prisma.module'; import { serializeBigInt } from '../../common/decorators/current-user.decorator'; import { WecomAibotService } from '../../integrations/wecom/wecom-aibot.service'; import type { CreateWecomBotDto, UpdateWecomBotDto } from './dto/wecom-bot.dto'; 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; reviewSuperAdminWecomUserIds: string | null; 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( private readonly prisma: PrismaService, private readonly wecomAibot: WecomAibotService, ) {} async list(query: { name?: string; role?: string; enabled?: string; page?: number; pageSize?: number }) { const page = query.page ?? 1; const pageSize = query.pageSize ?? 20; const where: { name?: { contains: string }; role?: string; enabled?: boolean; } = {}; if (query.name?.trim()) where.name = { contains: query.name.trim() }; if (query.role?.trim()) where.role = query.role.trim(); if (query.enabled === 'true' || query.enabled === 'false') { where.enabled = query.enabled === 'true'; } const [items, total] = await Promise.all([ this.prisma.wecomBot.findMany({ where, 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 }), ]); return serializeBigInt({ items: items.map((row) => this.toDto(row)), total, page, pageSize, }); } getRuntime() { return this.wecomAibot.getStatus(); } async detail(id: bigint) { 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); } async create(dto: CreateWecomBotDto) { const name = dto.name?.trim(); const botId = dto.botId?.trim(); const secret = dto.secret?.trim(); if (!name) throw new BadRequestException('请填写名称'); if (!botId) throw new BadRequestException('请填写 BotID'); if (!secret) throw new BadRequestException('请填写 Secret'); if (!isWecomRole(dto.role)) throw new BadRequestException('无效角色'); 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 = dto.permissions?.length ? parseWecomBotPermissions(dto.permissions) : parseWecomBotPermissions(WECOM_BOT_ROLE_DEFAULT_PERMISSIONS[dto.role]); if (!permissions.length) throw new BadRequestException('请至少选择一项权限'); const reviewIds = dto.reviewSuperAdminWecomUserIds ?? []; const row = await this.prisma.wecomBot.create({ data: { name, role: dto.role, botId, secret, avatarUrl: dto.avatarUrl?.trim() || null, welcome: dto.welcome?.trim() || null, permissions: JSON.stringify(permissions), reviewSuperAdminWecomUserIds: reviewIds.length ? JSON.stringify(reviewIds) : null, 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); } async update(id: bigint, dto: UpdateWecomBotDto) { const existing = await this.prisma.wecomBot.findUnique({ where: { id } }); if (!existing) throw new NotFoundException('机器人不存在'); const role = dto.role && isWecomRole(dto.role) ? dto.role : normalizeWecomBotRole(existing.role); if (dto.role && !isWecomRole(dto.role)) throw new BadRequestException('无效角色'); let botId = existing.botId; if (dto.botId !== undefined) { botId = dto.botId.trim(); if (!botId) throw new BadRequestException('BotID 不能为空'); if (botId !== existing.botId) { const dup = await this.prisma.wecomBot.findUnique({ where: { botId } }); if (dup) throw new BadRequestException('BotID 已存在'); } } let permissionsJson = existing.permissions; if (dto.permissions !== undefined) { const permissions = parseWecomBotPermissions(dto.permissions); if (!permissions.length) { throw new BadRequestException('请至少选择一项权限'); } permissionsJson = JSON.stringify(permissions); } else if (dto.role !== undefined && dto.role !== normalizeWecomBotRole(existing.role)) { // 仅改角色且未传 permissions 时,保持库内已存权限,不自动覆盖 permissionsJson = existing.permissions; } 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; let reviewIdsJson = existing.reviewSuperAdminWecomUserIds; if (dto.reviewSuperAdminWecomUserIds !== undefined) { const ids = dto.reviewSuperAdminWecomUserIds ?? []; reviewIdsJson = ids.length ? JSON.stringify(ids) : null; } const row = await this.prisma.wecomBot.update({ where: { id }, data: { name: dto.name !== undefined ? dto.name.trim() : undefined, role: dto.role ? normalizeWecomBotRole(dto.role) : undefined, botId, secret, avatarUrl: dto.avatarUrl === undefined ? undefined : dto.avatarUrl?.trim() || null, welcome: dto.welcome === undefined ? undefined : dto.welcome?.trim() || null, permissions: permissionsJson, reviewSuperAdminWecomUserIds: reviewIdsJson, 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); } async remove(id: bigint) { const existing = await this.prisma.wecomBot.findUnique({ where: { id } }); if (!existing) throw new NotFoundException('机器人不存在'); await this.prisma.wecomBot.delete({ where: { id } }); await this.wecomAibot.reload('bot-delete'); return { ok: true }; } async reloadRuntime() { return this.wecomAibot.reload('manual'); } private async resolveLlmId(raw?: string | null): Promise { 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 { 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 = normalizeWecomBotRole(row.role); const stored = parseWecomBotPermissions(row.permissions) as WecomBotPermission[]; const permissions = stored.length > 0 ? stored : ([...WECOM_BOT_ROLE_DEFAULT_PERMISSIONS[role]] as WecomBotPermission[]); return { id: row.id.toString(), name: row.name, role, botId: row.botId, secretConfigured: !!row.secret, avatarUrl: row.avatarUrl, welcome: row.welcome, permissions, reviewSuperAdminWecomUserIds: parseWecomUserIdList(row.reviewSuperAdminWecomUserIds), 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(), updatedAt: row.updatedAt.toISOString(), }; } }