feat(admin): LLM config, knowledge base, and WeCom AI binding
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:
2026-07-26 09:31:22 +08:00
parent 745c192693
commit 2b7ef65cce
30 changed files with 2507 additions and 56 deletions
@@ -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(),