feat: 套餐折叠、小程序 staging API、首页去筛选、企微日志权限

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-08-03 20:17:52 +08:00
parent ac2f9f4793
commit ffa753707b
9 changed files with 558 additions and 261 deletions
@@ -95,6 +95,21 @@ export class WecomBotActionsService {
'',
);
}
if (wecomBotHasPermission(bot, 'server_log.view')) {
lines.push(
'**服务器日志**',
'`日志` 最近客户端报错 · `日志 <关键词>` 搜索',
'`三方日志 [provider]` 最近第三方调用日志',
'',
);
}
if (wecomBotHasPermission(bot, 'api.query')) {
lines.push(
'**业务查询(只读)**',
'`查订单 <订单号>` · `用户号 <用户号>`',
'',
);
}
if (bot.aiEnabled && bot.llmConfigId) {
lines.push(
'**智能问答**',
@@ -160,6 +175,30 @@ export class WecomBotActionsService {
return this.queryHandbook(q);
}
// 服务器日志
if (/^(日志|错误日志|服务端日志)/i.test(content)) {
this.requirePerm(bot, 'server_log.view');
const q = content.replace(/^(日志|错误日志|服务端日志)\s*/i, '').trim();
return this.queryServerLogs(q);
}
if (/^(三方日志|第三方日志)/i.test(content)) {
this.requirePerm(bot, 'server_log.view');
const q = content.replace(/^(三方日志|第三方日志)\s*/i, '').trim();
return this.queryThirdPartyLogs(q);
}
// 业务 API 只读查询
if (/^(查订单|订单查询)\s+/i.test(content)) {
this.requirePerm(bot, 'api.query');
const orderNo = content.replace(/^(查订单|订单查询)\s+/i, '').trim();
return this.queryOrder(orderNo);
}
if (/^(用户号|查用户号)\s+/i.test(content)) {
this.requirePerm(bot, 'api.query');
const userNo = content.replace(/^(用户号|查用户号)\s+/i, '').trim();
return this.queryUserByNo(userNo);
}
// 自然语言手册(仅团队助手有 handbook 权限时;开启 AI 时改由模型+知识库回答)
if (
!opts?.skipNaturalFallback &&
@@ -448,6 +487,121 @@ export class WecomBotActionsService {
return formatHandbook(hits);
}
private async queryServerLogs(keyword: string) {
const rows = await this.prisma.logUserAnalytics.findMany({
where: keyword
? {
eventName: 'client_error',
OR: [
{ pagePath: { contains: keyword } },
{ extraJson: { string_contains: keyword } },
],
}
: { eventName: 'client_error' },
orderBy: { createdAt: 'desc' },
take: 8,
});
if (!rows.length) {
return keyword ? `未找到与「${keyword}」相关的客户端报错日志` : '暂无近期客户端报错日志';
}
return [
`**最近客户端报错**${keyword ? `(关键词:${keyword}` : ''}`,
...rows.map((row, i) => formatClientErrorLog(row, i + 1)),
].join('\n\n');
}
private async queryThirdPartyLogs(provider?: string) {
const where = provider ? { provider: provider.toUpperCase() as never } : {};
const rows = await this.prisma.logThirdParty.findMany({
where,
orderBy: { createdAt: 'desc' },
take: 8,
});
if (!rows.length) {
return provider ? `未找到 provider=${provider} 的第三方日志` : '暂无近期第三方调用日志';
}
return [
`**最近第三方日志**${provider ? `${provider}` : ''}`,
...rows.map((row, i) => {
const err = row.errorMessage ? `\n- 错误:${row.errorMessage.slice(0, 120)}` : '';
return [
`**${i + 1}. ${row.provider}/${row.scene}**`,
`- 状态:${row.status}`,
`- 关联:${row.refType || '—'} ${row.refId?.toString() || ''}`,
`- 外部单号:${row.externalNo || '—'}${err}`,
`- 时间:${row.createdAt.toISOString().slice(0, 19).replace('T', ' ')}`,
].join('\n');
}),
].join('\n\n');
}
private async queryOrder(orderNo: string) {
if (!orderNo) return '请提供订单号,例如:`查订单 DK123456`';
const order = await this.prisma.order.findFirst({
where: { orderNo: { contains: orderNo } },
include: {
user: { select: { userNo: true, phone: true, nickname: true } },
delivery: true,
},
});
if (!order) return `未找到订单:${orderNo}`;
const deliveryLines = order.delivery
? [`- ${order.delivery.provider} ${order.delivery.trackingNo || '—'}`]
: ['- 暂无配送单'];
return [
'**订单摘要**',
`- 订单号:${order.orderNo}`,
`- 状态:${order.status}`,
`- 商品:${order.productName}`,
`- 数量:${order.quantity}`,
`- 实付:¥${Number(order.payAmount).toFixed(2)}`,
`- 履约:${order.deliveryType}`,
`- 用户:${order.user?.nickname || '—'} / ${order.user?.userNo || '—'} / ${maskPhone(order.user?.phone || '')}`,
`- 下单:${order.createdAt.toISOString().slice(0, 19).replace('T', ' ')}`,
'**配送**',
...deliveryLines,
].join('\n');
}
private async queryUserByNo(userNo: string) {
if (!userNo) return '请提供用户号,例如:`用户号 U123456`';
const user = await this.prisma.user.findFirst({
where: { userNo: { contains: userNo } },
select: {
id: true,
userNo: true,
phone: true,
nickname: true,
status: true,
phoneVerifiedAt: true,
createdAt: true,
_count: { select: { orders: true } },
},
});
if (!user) return `未找到用户号:${userNo}`;
const coupons = await this.prisma.benefitCoupon.aggregate({
where: { userId: user.id, status: 'ACTIVE' },
_sum: { balance: true },
});
return [
'**用户摘要**',
`- 用户号:${user.userNo}`,
`- 昵称:${user.nickname || '—'}`,
`- 手机:${maskPhone(user.phone || '')}`,
`- 手机已验:${user.phoneVerifiedAt ? '是' : '否'}`,
`- 状态:${user.status}`,
`- 订单数:${user._count.orders}`,
`- 权益余额:¥${Number(coupons._sum.balance ?? 0).toFixed(2)}`,
`- 注册:${user.createdAt.toISOString().slice(0, 19).replace('T', ' ')}`,
].join('\n');
}
private async resolveCreator(wecomUserId: string) {
const admin = await this.prisma.hqAccount.findFirst({
where: { status: 'ACTIVE' },
@@ -470,3 +624,30 @@ function maskPhone(phone: string): string {
function formatHandbook(entries: ReturnType<typeof searchHandbook>): string {
return entries.map((e) => `**${e.title}**\n${e.body}`).join('\n\n---\n\n');
}
type ClientErrorLogRow = {
id: bigint;
clientApp: string | null;
pagePath: string | null;
extraJson: unknown;
createdAt: Date;
};
function formatClientErrorLog(row: ClientErrorLogRow, index: number): string {
const extra =
row.extraJson && typeof row.extraJson === 'object'
? (row.extraJson as Record<string, unknown>)
: {};
const level = typeof extra.level === 'string' ? extra.level : '—';
const category = typeof extra.category === 'string' ? extra.category : '—';
const message = typeof extra.message === 'string' ? extra.message.slice(0, 160) : '—';
return [
`**${index}. [${level}/${category}]**`,
`- 端:${row.clientApp || '—'}`,
row.pagePath ? `- 页面:${row.pagePath}` : null,
`- 消息:${message}`,
`- 时间:${row.createdAt.toISOString().slice(0, 19).replace('T', ' ')}`,
]
.filter(Boolean)
.join('\n');
}