276 lines
9.6 KiB
TypeScript
276 lines
9.6 KiB
TypeScript
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<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 = 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(),
|
|
};
|
|
}
|
|
}
|