feat: 套餐折叠、小程序 staging API、首页去筛选、企微日志权限
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -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');
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user