企业微信智能机器人API插件

This commit is contained in:
2026-09-03 21:29:10 +08:00
parent ec9b7efcd6
commit 418e13a5e3
22 changed files with 1154 additions and 33 deletions
+4
View File
@@ -67,6 +67,10 @@ WX_MINI_MSG_AES_KEY=
# 企业微信智能机器人总开关(Bot 实例在 HQ「企微机器人」模块创建)
WECOM_AIBOT_ENABLED=false
# 企微「API 插件」只读数据面(与长连接 Bot 独立;密钥勿提交)
WECOM_PLUGIN_ENABLED=false
# WECOM_PLUGIN_API_KEY=
# 运营告警 Webhook(已废弃运行时读取,仅 seed 一次性导入到 HQ「消息推送」)
# 配置后执行 pnpm prisma:seed-wecom-push 或 API 启动时自动 ensureDefaults
# WECOM_ALERT_ENABLED=false
@@ -54,6 +54,10 @@ WX_MINI_MSG_AES_KEY=
# 企业微信机器人总开关(实例在 HQ 企微机器人模块维护)
WECOM_AIBOT_ENABLED=false
# 企微 API 插件只读数据面(与长连接 Bot 独立)
WECOM_PLUGIN_ENABLED=false
# WECOM_PLUGIN_API_KEY=
# 运营告警:企业微信群机器人 Webhook
# 运营告警 Webhook(已废弃运行时读取,仅 seed 导入 HQ「消息推送」)
# WECOM_ALERT_ENABLED=false
+4
View File
@@ -54,6 +54,10 @@ WX_MINI_MSG_AES_KEY=
WECOM_AIBOT_ENABLED=false
# 企微 API 插件(测试环境单独一把 Key)
WECOM_PLUGIN_ENABLED=false
# WECOM_PLUGIN_API_KEY=
# 运营告警:企业微信群机器人 Webhook
# 运营告警 Webhook(已废弃运行时读取,仅 seed 导入 HQ「消息推送」)
# WECOM_ALERT_ENABLED=false
@@ -51,6 +51,7 @@ const fixed = {
WX_PAY_NOTIFY_URL: 'https://api-test.dukanghaoke.com/api/v1/callbacks/wechat/pay',
OSS_UPLOAD_PREFIX: 'staging/uploads',
WECOM_AIBOT_ENABLED: 'false',
WECOM_PLUGIN_ENABLED: 'false',
};
const preferFromProd = [
@@ -6,10 +6,19 @@ import {
} from '@nestjs/common';
import { Observable, map } from 'rxjs';
function skipResponseWrap(url?: string): boolean {
const path = (url || '').split('?')[0];
return path.endsWith('/wecom/plugin/openapi.json');
}
@Injectable()
export class ResponseInterceptor implements NestInterceptor {
intercept(context: ExecutionContext, next: CallHandler): Observable<unknown> {
const req = context.switchToHttp().getRequest<{ originalUrl?: string; url?: string }>();
const res = context.switchToHttp().getResponse<{ headersSent?: boolean }>();
if (skipResponseWrap(req.originalUrl || req.url)) {
return next.handle();
}
return next.handle().pipe(
map((data) => {
if (res.headersSent) return data;
@@ -22,3 +31,4 @@ export class ResponseInterceptor implements NestInterceptor {
);
}
}
@@ -0,0 +1,418 @@
import { BadRequestException, Injectable, NotFoundException } from '@nestjs/common';
import {
clampWecomPluginPage,
clampWecomPluginPageSize,
maskContactPhone,
MOBILE_PHONE_RE,
parseWecomPluginMetricsKind,
toWecomPluginUserView,
wecomPluginMetricsPeriod,
type WecomReportStats,
} from '@dukang/domain';
import { Prisma } from '@prisma/client';
import { PrismaService } from '../../common/prisma/prisma.module';
import { PromoCodeService } from '../../modules/promo/promo-code.service';
import { WecomBotAuditService } from './wecom-bot-audit.service';
import { WECOM_PLUGIN_AUDIT_BOT } from './wecom-plugin.constants';
function asNumber(v: Prisma.Decimal | number | null | undefined): number {
if (v == null) return 0;
if (typeof v === 'number') return Number.isFinite(v) ? v : 0;
return Number(v);
}
function requireQuery(q?: string): string {
const v = String(q ?? '').trim();
if (!v) throw new BadRequestException('请提供查询关键词 q');
return v;
}
@Injectable()
export class WecomPluginQueryService {
constructor(
private readonly prisma: PrismaService,
private readonly promo: PromoCodeService,
private readonly audit: WecomBotAuditService,
) {}
info() {
return {
name: '杜康好客运营查询',
description: '企微智能机器人只读 API 插件。查询订单、用户、门店、核销、推广码与经营指标。',
auth: { header: 'X-Api-Key' },
tools: ['orders', 'users', 'stores', 'redeems', 'promo-codes', 'metrics'],
};
}
queryOrders(wecomUserId: string, q?: string, page?: string, pageSize?: string) {
const keyword = requireQuery(q);
const take = clampWecomPluginPageSize(pageSize);
const skip = (clampWecomPluginPage(page) - 1) * take;
return this.audit.run(
{
bot: WECOM_PLUGIN_AUDIT_BOT,
wecomUserId,
action: 'plugin.order.read',
permission: 'order.read',
inputSummary: keyword,
},
async () => {
const [items, total] = await Promise.all([
this.prisma.order.findMany({
where: { orderNo: { contains: keyword } },
orderBy: { createdAt: 'desc' },
skip,
take,
include: {
user: { select: { userNo: true, nickname: true, phone: true } },
delivery: { select: { trackingNo: true, provider: true } },
},
}),
this.prisma.order.count({ where: { orderNo: { contains: keyword } } }),
]);
return {
total,
items: items.map((o) => ({
orderNo: o.orderNo,
status: o.status,
payStatus: o.payStatus,
deliveryType: o.deliveryType,
productName: o.productName,
quantity: o.quantity,
payAmount: asNumber(o.payAmount),
user: toWecomPluginUserView({
userNo: o.user?.userNo || '—',
nickname: o.user?.nickname,
phone: o.user?.phone,
}),
receiverName: o.receiverName,
receiverPhone: maskContactPhone(o.receiverPhone),
receiverCity: o.receiverCity,
trackingNo: o.delivery?.trackingNo || null,
createdAt: o.createdAt.toISOString(),
})),
};
},
);
}
queryUsers(wecomUserId: string, q?: string, page?: string, pageSize?: string) {
const keyword = requireQuery(q);
const take = clampWecomPluginPageSize(pageSize);
const skip = (clampWecomPluginPage(page) - 1) * take;
return this.audit.run(
{
bot: WECOM_PLUGIN_AUDIT_BOT,
wecomUserId,
action: 'plugin.user.read',
permission: 'user.read',
inputSummary: MOBILE_PHONE_RE.test(keyword) ? maskContactPhone(keyword) : keyword,
},
async () => {
const where = MOBILE_PHONE_RE.test(keyword)
? { phone: keyword, mergedIntoUserId: null }
: { userNo: { contains: keyword }, mergedIntoUserId: null };
const [rows, total] = await Promise.all([
this.prisma.user.findMany({
where,
orderBy: { createdAt: 'desc' },
skip,
take,
select: {
id: true,
userNo: true,
nickname: true,
phone: true,
status: true,
createdAt: true,
_count: { select: { orders: true } },
},
}),
this.prisma.user.count({ where }),
]);
const balances = await Promise.all(
rows.map((u) =>
this.prisma.benefitCoupon.aggregate({
where: { userId: u.id, status: 'ACTIVE' },
_sum: { balance: true },
}),
),
);
return {
total,
items: rows.map((u, i) => ({
...toWecomPluginUserView(u),
status: u.status,
orderCount: u._count.orders,
benefitBalance: asNumber(balances[i]?._sum.balance),
createdAt: u.createdAt.toISOString(),
})),
};
},
);
}
queryStores(wecomUserId: string, q?: string, page?: string, pageSize?: string) {
const keyword = requireQuery(q);
const take = clampWecomPluginPageSize(pageSize);
const skip = (clampWecomPluginPage(page) - 1) * take;
return this.audit.run(
{
bot: WECOM_PLUGIN_AUDIT_BOT,
wecomUserId,
action: 'plugin.store.read',
permission: 'store.read',
inputSummary: keyword,
},
async () => {
const where = { name: { contains: keyword } };
const [rows, total] = await Promise.all([
this.prisma.store.findMany({
where,
orderBy: { updatedAt: 'desc' },
skip,
take,
select: {
name: true,
status: true,
cityName: true,
district: true,
address: true,
contactPhone: true,
phone: true,
},
}),
this.prisma.store.count({ where }),
]);
return {
total,
items: rows.map((s) => ({
name: s.name,
status: s.status,
cityName: s.cityName,
district: s.district,
address: s.address,
contactPhone: maskContactPhone(s.contactPhone || s.phone),
})),
};
},
);
}
queryRedeems(wecomUserId: string, q?: string, page?: string, pageSize?: string) {
const keyword = requireQuery(q);
const take = clampWecomPluginPageSize(pageSize);
const skip = (clampWecomPluginPage(page) - 1) * take;
return this.audit.run(
{
bot: WECOM_PLUGIN_AUDIT_BOT,
wecomUserId,
action: 'plugin.redeem.read',
permission: 'redeem.read',
inputSummary: keyword,
},
async () => {
const byNo = await this.prisma.redeemRecord.findMany({
where: { redeemNo: { contains: keyword } },
orderBy: { createdAt: 'desc' },
skip,
take,
include: { store: { select: { name: true } } },
});
const rows =
byNo.length > 0
? byNo
: await this.prisma.redeemRecord.findMany({
where: { store: { name: { contains: keyword } } },
orderBy: { createdAt: 'desc' },
skip,
take,
include: { store: { select: { name: true } } },
});
const total =
byNo.length > 0
? await this.prisma.redeemRecord.count({ where: { redeemNo: { contains: keyword } } })
: await this.prisma.redeemRecord.count({
where: { store: { name: { contains: keyword } } },
});
return {
total,
items: rows.map((r) => ({
redeemNo: r.redeemNo,
amount: asNumber(r.amount),
channel: r.channel,
storeName: r.store?.name || '—',
createdAt: r.createdAt.toISOString(),
})),
};
},
);
}
queryPromoCodes(wecomUserId: string, q?: string, page?: string, pageSize?: string) {
const keyword = requireQuery(q);
const take = clampWecomPluginPageSize(pageSize);
const skip = (clampWecomPluginPage(page) - 1) * take;
return this.audit.run(
{
bot: WECOM_PLUGIN_AUDIT_BOT,
wecomUserId,
action: 'plugin.promo.read',
inputSummary: keyword,
},
async () => {
const where = {
OR: [
{ code: { contains: keyword.toUpperCase() } },
{ name: { contains: keyword } },
],
};
const [rows, total] = await Promise.all([
this.prisma.commonPromoCode.findMany({
where,
orderBy: { createdAt: 'desc' },
skip,
take,
select: {
code: true,
name: true,
scene: true,
status: true,
scanCount: true,
orderCount: true,
},
}),
this.prisma.commonPromoCode.count({ where }),
]);
return { total, items: rows };
},
);
}
queryPromoCodeStats(wecomUserId: string, code?: string) {
const keyword = requireQuery(code);
return this.audit.run(
{
bot: WECOM_PLUGIN_AUDIT_BOT,
wecomUserId,
action: 'plugin.promo.stats',
inputSummary: keyword,
},
async () => {
const row =
(await this.prisma.commonPromoCode.findUnique({
where: { code: keyword.toUpperCase() },
select: { id: true, code: true, name: true, status: true },
})) ||
(await this.prisma.commonPromoCode.findFirst({
where: { code: { contains: keyword.toUpperCase() } },
select: { id: true, code: true, name: true, status: true },
}));
if (!row) throw new NotFoundException(`未找到推广码:${keyword}`);
const stats = await this.promo.stats(row.id);
return { code: row.code, name: row.name, status: row.status, stats };
},
);
}
queryMetrics(wecomUserId: string, kindRaw?: string) {
const kind = parseWecomPluginMetricsKind(kindRaw);
if (!kind) {
throw new BadRequestException('kind 须为 today | daily | weekly | monthly');
}
return this.audit.run(
{
bot: WECOM_PLUGIN_AUDIT_BOT,
wecomUserId,
action: 'plugin.metrics.read',
inputSummary: kind,
},
async () => {
const period = wecomPluginMetricsPeriod(kind);
const stats = await this.loadStats(period.start, period.endExclusive);
return {
kind: period.kind,
title: period.title,
rangeLabel: period.rangeLabel,
incrementLabel: period.incrementLabel,
periodKey: period.periodKey,
stats,
};
},
);
}
/** 日报口径:用户=有效未合并;订单金额=已付 payAmount;核销=RedeemRecord。today 期末为当前时刻。 */
private async loadStats(start: Date, cutoff: Date): Promise<WecomReportStats> {
const userBase = { status: 1, mergedIntoUserId: null } as const;
const partnerBase = { isPrimary: 1 } as const;
const paid = { payStatus: 'PAID' as const };
const [
usersTotal,
usersIncrement,
partnersTotal,
partnersIncrement,
storesTotal,
storesIncrement,
ordersTotal,
ordersIncrement,
orderAmountTotal,
orderAmountIncrement,
redeemsTotal,
redeemsIncrement,
redeemAmountTotal,
redeemAmountIncrement,
] = await Promise.all([
this.prisma.user.count({ where: { ...userBase, createdAt: { lt: cutoff } } }),
this.prisma.user.count({
where: { ...userBase, createdAt: { gte: start, lt: cutoff } },
}),
this.prisma.partnerAccount.count({
where: { ...partnerBase, createdAt: { lt: cutoff } },
}),
this.prisma.partnerAccount.count({
where: { ...partnerBase, createdAt: { gte: start, lt: cutoff } },
}),
this.prisma.store.count({ where: { createdAt: { lt: cutoff } } }),
this.prisma.store.count({ where: { createdAt: { gte: start, lt: cutoff } } }),
this.prisma.order.count({ where: { createdAt: { lt: cutoff } } }),
this.prisma.order.count({ where: { createdAt: { gte: start, lt: cutoff } } }),
this.prisma.order.aggregate({
_sum: { payAmount: true },
where: { ...paid, paidAt: { lt: cutoff } },
}),
this.prisma.order.aggregate({
_sum: { payAmount: true },
where: { ...paid, paidAt: { gte: start, lt: cutoff } },
}),
this.prisma.redeemRecord.count({ where: { createdAt: { lt: cutoff } } }),
this.prisma.redeemRecord.count({ where: { createdAt: { gte: start, lt: cutoff } } }),
this.prisma.redeemRecord.aggregate({
_sum: { amount: true },
where: { createdAt: { lt: cutoff } },
}),
this.prisma.redeemRecord.aggregate({
_sum: { amount: true },
where: { createdAt: { gte: start, lt: cutoff } },
}),
]);
return {
usersTotal,
usersIncrement,
partnersTotal,
partnersIncrement,
storesTotal,
storesIncrement,
ordersTotal,
ordersIncrement,
orderAmountTotal: asNumber(orderAmountTotal._sum.payAmount),
orderAmountIncrement: asNumber(orderAmountIncrement._sum.payAmount),
redeemsTotal,
redeemsIncrement,
redeemAmountTotal: asNumber(redeemAmountTotal._sum.amount),
redeemAmountIncrement: asNumber(redeemAmountIncrement._sum.amount),
};
}
}
@@ -0,0 +1,19 @@
import type { WecomBotRuntimeConfig } from './wecom-bot.types';
/** 审计占位:插件无长连接 Bot 行,botKey 固定为 plugin */
export const WECOM_PLUGIN_AUDIT_BOT: WecomBotRuntimeConfig = {
id: '',
key: 'plugin',
role: 'OPERATIONS',
name: '企微 API 插件',
enabled: true,
botId: '',
secret: '',
welcome: '',
avatarUrl: null,
permissions: [],
reviewSuperAdminWecomUserIds: [],
aiEnabled: false,
llmConfigId: null,
knowledgeBaseId: null,
};
@@ -0,0 +1,87 @@
import { Controller, Get, Param, Query, Req, UseGuards } from '@nestjs/common';
import type { Request } from 'express';
import { WecomPluginGuard } from './wecom-plugin.guard';
import { WecomPluginQueryService } from './wecom-plugin-query.service';
import { WECOM_PLUGIN_OPENAPI } from './wecom-plugin.openapi';
@Controller('wecom/plugin')
@UseGuards(WecomPluginGuard)
export class WecomPluginController {
constructor(private readonly query: WecomPluginQueryService) {}
@Get()
info() {
return this.query.info();
}
@Get('openapi.json')
openapi() {
return WECOM_PLUGIN_OPENAPI;
}
@Get('orders')
orders(
@Req() req: Request,
@Query('q') q?: string,
@Query('page') page?: string,
@Query('pageSize') pageSize?: string,
) {
return this.query.queryOrders(pluginCaller(req), q, page, pageSize);
}
@Get('users')
users(
@Req() req: Request,
@Query('q') q?: string,
@Query('page') page?: string,
@Query('pageSize') pageSize?: string,
) {
return this.query.queryUsers(pluginCaller(req), q, page, pageSize);
}
@Get('stores')
stores(
@Req() req: Request,
@Query('q') q?: string,
@Query('page') page?: string,
@Query('pageSize') pageSize?: string,
) {
return this.query.queryStores(pluginCaller(req), q, page, pageSize);
}
@Get('redeems')
redeems(
@Req() req: Request,
@Query('q') q?: string,
@Query('page') page?: string,
@Query('pageSize') pageSize?: string,
) {
return this.query.queryRedeems(pluginCaller(req), q, page, pageSize);
}
@Get('promo-codes')
promoCodes(
@Req() req: Request,
@Query('q') q?: string,
@Query('page') page?: string,
@Query('pageSize') pageSize?: string,
) {
return this.query.queryPromoCodes(pluginCaller(req), q, page, pageSize);
}
@Get('promo-codes/:code/stats')
promoStats(@Req() req: Request, @Param('code') code: string) {
return this.query.queryPromoCodeStats(pluginCaller(req), code);
}
@Get('metrics')
metrics(@Req() req: Request, @Query('kind') kind?: string) {
return this.query.queryMetrics(pluginCaller(req), kind);
}
}
function pluginCaller(req: Request): string {
const raw = req.headers['x-wecom-userid'] ?? req.headers['userid'];
const v = Array.isArray(raw) ? raw[0] : raw;
return String(v || 'plugin').trim() || 'plugin';
}
@@ -0,0 +1,28 @@
import {
CanActivate,
ExecutionContext,
Injectable,
UnauthorizedException,
} from '@nestjs/common';
import { isWecomPluginEnabled, verifyWecomPluginApiKey } from '@dukang/domain';
@Injectable()
export class WecomPluginGuard implements CanActivate {
canActivate(context: ExecutionContext): boolean {
if (!isWecomPluginEnabled(process.env)) {
throw new UnauthorizedException('Unauthorized');
}
const req = context.switchToHttp().getRequest<{ headers: Record<string, unknown> }>();
const provided = headerValue(req.headers, 'x-api-key');
if (!verifyWecomPluginApiKey(provided, process.env.WECOM_PLUGIN_API_KEY)) {
throw new UnauthorizedException('Unauthorized');
}
return true;
}
}
function headerValue(headers: Record<string, unknown>, name: string): string {
const raw = headers[name];
if (Array.isArray(raw)) return String(raw[0] ?? '').trim();
return String(raw ?? '').trim();
}
@@ -0,0 +1,157 @@
const envelope = (dataSchema: Record<string, unknown>) => ({
type: 'object',
properties: {
code: { type: 'integer', example: 0 },
message: { type: 'string', example: 'ok' },
data: dataSchema,
},
required: ['code', 'message', 'data'],
});
const qParam = {
name: 'q',
in: 'query',
required: true,
schema: { type: 'string' },
description: '查询关键词',
};
const pageParams = [
{
name: 'page',
in: 'query',
required: false,
schema: { type: 'integer', default: 1, minimum: 1 },
},
{
name: 'pageSize',
in: 'query',
required: false,
schema: { type: 'integer', default: 5, minimum: 1, maximum: 10 },
description: '默认 5,最大 10',
},
];
const unauthorized = {
description: '缺少或错误的 X-Api-Key,或插件未启用',
content: {
'application/json': {
schema: {
type: 'object',
properties: {
code: { type: 'integer' },
message: { type: 'string' },
},
},
},
},
};
function listPath(summary: string, description: string, qDescription: string) {
return {
get: {
summary,
description,
operationId: summary,
parameters: [{ ...qParam, description: qDescription }, ...pageParams],
responses: {
200: {
description: '查询结果',
content: {
'application/json': {
schema: envelope({
type: 'object',
properties: {
total: { type: 'integer' },
items: { type: 'array', items: { type: 'object' } },
},
}),
},
},
},
401: unauthorized,
},
},
};
}
/** OpenAPI 3.0:企微「添加插件工具」可导入。须原样返回,不要套 {code,message,data}。 */
export const WECOM_PLUGIN_OPENAPI = {
openapi: '3.0.3',
info: {
title: '杜康好客运营查询',
description:
'企业内部只读查询。手机号已脱敏。鉴权:Header X-Api-Key。响应除本文件外均为 { code, message, data }。',
version: '1.0.0',
},
servers: [
{ url: 'https://api.dukanghaoke.com/api/v1/wecom/plugin', description: '生产' },
{ url: 'https://api-test.dukanghaoke.com/api/v1/wecom/plugin', description: '测试' },
],
security: [{ ApiKeyAuth: [] }],
components: {
securitySchemes: {
ApiKeyAuth: {
type: 'apiKey',
in: 'header',
name: 'X-Api-Key',
},
},
},
paths: {
'/orders': listPath('查询订单', '按订单号模糊查询', '订单号,如 DK20260903xxxx'),
'/users': listPath('查询用户', '按用户号或 11 位手机号查询;手机号脱敏', '用户号或手机号'),
'/stores': listPath('查询门店', '按门店名称模糊查询', '门店名称关键词'),
'/redeems': listPath('查询核销', '按核销单号或门店名查询', '核销单号或门店名'),
'/promo-codes': listPath('查询推广码', '按推广码 code 或名称查询', '推广码或名称'),
'/promo-codes/{code}/stats': {
get: {
summary: '推广码统计',
operationId: '查询推广码统计',
parameters: [
{
name: 'code',
in: 'path',
required: true,
schema: { type: 'string' },
description: '推广码 code',
},
],
responses: {
200: {
description: '扫码/成交统计',
content: { 'application/json': { schema: envelope({ type: 'object' }) } },
},
401: unauthorized,
},
},
},
'/metrics': {
get: {
summary: '经营指标',
description:
'today=今日截至当前;daily/weekly/monthly 与企微经营报告同一口径(用户有效未合并,订单金额=已付 payAmount)。',
operationId: '查询经营指标',
parameters: [
{
name: 'kind',
in: 'query',
required: false,
schema: {
type: 'string',
enum: ['today', 'daily', 'weekly', 'monthly'],
default: 'today',
},
},
],
responses: {
200: {
description: '存量与新增',
content: { 'application/json': { schema: envelope({ type: 'object' }) } },
},
401: unauthorized,
},
},
},
},
} as const;
@@ -1,33 +1,41 @@
import { Module, forwardRef } from '@nestjs/common';
import { CommonModule } from '../../modules/common/common.module';
import { DevPlanModule } from '../../modules/dev-plan/dev-plan.module';
import { SettlementModule } from '../../modules/settlement/settlement.module';
import { IntegrationsModule } from '../integrations.module';
import { LlmModule } from '../llm/llm.module';
import { WecomAibotService } from './wecom-aibot.service';
import { WecomBotActionsService } from './wecom-bot-actions.service';
import { WecomBotAiService } from './wecom-bot-ai.service';
import { WecomBotAuditService } from './wecom-bot-audit.service';
import { WecomBotCapabilityService } from './wecom-bot-capability.service';
import { WecomBotSessionService } from './wecom-bot-session.service';
/** 企微多机器人:依赖 Common(工单)+ Settlement/DevPlan + Integrations(短信)+ Llm */
@Module({
imports: [
forwardRef(() => CommonModule),
import { Module, forwardRef } from '@nestjs/common';
import { CommonModule } from '../../modules/common/common.module';
import { DevPlanModule } from '../../modules/dev-plan/dev-plan.module';
import { PromoModule } from '../../modules/promo/promo.module';
import { SettlementModule } from '../../modules/settlement/settlement.module';
import { IntegrationsModule } from '../integrations.module';
import { LlmModule } from '../llm/llm.module';
import { WecomAibotService } from './wecom-aibot.service';
import { WecomBotActionsService } from './wecom-bot-actions.service';
import { WecomBotAiService } from './wecom-bot-ai.service';
import { WecomBotAuditService } from './wecom-bot-audit.service';
import { WecomBotCapabilityService } from './wecom-bot-capability.service';
import { WecomBotSessionService } from './wecom-bot-session.service';
import { WecomPluginController } from './wecom-plugin.controller';
import { WecomPluginGuard } from './wecom-plugin.guard';
import { WecomPluginQueryService } from './wecom-plugin-query.service';
/** 企微多机器人:依赖 Common(工单)+ Settlement/DevPlan + Promo + Integrations(短信)+ Llm */
@Module({
imports: [
forwardRef(() => CommonModule),
forwardRef(() => IntegrationsModule),
DevPlanModule,
SettlementModule,
PromoModule,
LlmModule,
],
controllers: [WecomPluginController],
providers: [
WecomBotSessionService,
WecomBotAuditService,
WecomBotCapabilityService,
WecomBotActionsService,
WecomBotAiService,
WecomAibotService,
WecomPluginGuard,
WecomPluginQueryService,
],
exports: [WecomAibotService, WecomBotAuditService],
})
export class WecomModule {}