export chat detail (#5454)

* export chat detail

* fix

* key name
This commit is contained in:
heheer
2025-08-14 16:25:40 +08:00
committed by GitHub
parent 8f3424cea1
commit c5cabd0efc
5 changed files with 115 additions and 14 deletions

View File

@@ -124,6 +124,7 @@
"logs_good_feedback": "Like",
"logs_key_config": "Field Configuration",
"logs_keys_annotatedCount": "Annotated Answer Count",
"logs_keys_chatDetails": "Conversation details",
"logs_keys_createdTime": "Created Time",
"logs_keys_customFeedback": "Custom Feedback",
"logs_keys_errorCount": "Error Count",

View File

@@ -125,6 +125,7 @@
"logs_good_feedback": "点赞",
"logs_key_config": "字段配置",
"logs_keys_annotatedCount": "标注答案数量",
"logs_keys_chatDetails": "对话详情",
"logs_keys_createdTime": "创建时间",
"logs_keys_customFeedback": "自定义反馈",
"logs_keys_errorCount": "报错数量",

View File

@@ -124,6 +124,7 @@
"logs_good_feedback": "點贊",
"logs_key_config": "字段配置",
"logs_keys_annotatedCount": "標記答案數量",
"logs_keys_chatDetails": "對話詳情",
"logs_keys_createdTime": "建立時間",
"logs_keys_customFeedback": "自訂回饋",
"logs_keys_errorCount": "錯誤數量",

View File

@@ -153,7 +153,7 @@ const LogTable = ({
tmbIds: isSelectAllTmb ? undefined : selectTmbIds,
chatSearch,
title: headerTitle,
title: headerTitle + ',' + t('app:logs_keys_chatDetails'),
logKeys: enabledKeys,
sourcesMap: Object.fromEntries(
Object.entries(ChatSourceMap).map(([key, config]) => [

View File

@@ -14,11 +14,16 @@ import { Types } from 'mongoose';
import { MongoChat } from '@fastgpt/service/core/chat/chatSchema';
import { ChatItemCollectionName } from '@fastgpt/service/core/chat/chatItemSchema';
import { MongoTeamMember } from '@fastgpt/service/support/user/team/teamMemberSchema';
import type { ChatSourceEnum } from '@fastgpt/global/core/chat/constants';
import { type ChatSourceEnum } from '@fastgpt/global/core/chat/constants';
import { AppLogKeysEnum } from '@fastgpt/global/core/app/logs/constants';
import { sanitizeCsvField } from '@fastgpt/service/common/file/csv';
import { AppReadChatLogPerVal } from '@fastgpt/global/support/permission/app/constant';
const formatJsonString = (data: any) => {
if (data == null) return '';
return JSON.stringify(data).replace(/\n/g, '\\n');
};
export type ExportChatLogsBody = GetAppChatLogsProps & {
title: string;
sourcesMap: Record<string, { label: string }>;
@@ -178,6 +183,35 @@ async function handler(req: ApiRequestProps<ExportChatLogsBody, {}>, res: NextAp
as: 'chatItemsData'
}
},
{
$lookup: {
from: ChatItemCollectionName,
let: { chatId: '$chatId' },
pipeline: [
{
$match: {
$expr: {
$and: [
{ $eq: ['$appId', new Types.ObjectId(appId)] },
{ $eq: ['$chatId', '$$chatId'] }
]
}
}
},
{ $sort: { _id: 1 } },
{
$project: {
value: 1,
userGoodFeedback: 1,
userBadFeedback: 1,
customFeedbacks: 1,
adminFeedback: 1
}
}
],
as: 'chatitems'
}
},
{
$addFields: {
messageCount: { $ifNull: [{ $arrayElemAt: ['$chatItemsData.messageCount', 0] }, 0] },
@@ -206,7 +240,45 @@ async function handler(req: ApiRequestProps<ExportChatLogsBody, {}>, res: NextAp
]
},
errorCount: { $ifNull: [{ $arrayElemAt: ['$chatItemsData.errorCount', 0] }, 0] },
totalPoints: { $ifNull: [{ $arrayElemAt: ['$chatItemsData.totalPoints', 0] }, 0] }
totalPoints: { $ifNull: [{ $arrayElemAt: ['$chatItemsData.totalPoints', 0] }, 0] },
userGoodFeedbackItems: {
$filter: {
input: '$chatitems',
as: 'item',
cond: { $ifNull: ['$$item.userGoodFeedback', false] }
}
},
userBadFeedbackItems: {
$filter: {
input: '$chatitems',
as: 'item',
cond: { $ifNull: ['$$item.userBadFeedback', false] }
}
},
customFeedbackItems: {
$filter: {
input: '$chatitems',
as: 'item',
cond: { $gt: [{ $size: { $ifNull: ['$$item.customFeedbacks', []] } }, 0] }
}
},
markItems: {
$filter: {
input: '$chatitems',
as: 'item',
cond: { $ifNull: ['$$item.adminFeedback', false] }
}
},
chatDetails: {
$map: {
input: { $slice: ['$chatitems', -1000] },
as: 'item',
in: {
id: '$$item._id',
value: '$$item.value'
}
}
}
}
},
{
@@ -227,11 +299,18 @@ async function handler(req: ApiRequestProps<ExportChatLogsBody, {}>, res: NextAp
errorCount: 1,
totalPoints: 1,
outLinkUid: 1,
tmbId: 1
tmbId: 1,
userGoodFeedbackItems: 1,
userBadFeedbackItems: 1,
customFeedbackItems: 1,
markItems: 1,
chatDetails: 1
}
}
],
{ ...readFromSecondary }
{
...readFromSecondary
}
).cursor({ batchSize: 1000 });
const write = responseWriteController({
@@ -254,7 +333,7 @@ async function handler(req: ApiRequestProps<ExportChatLogsBody, {}>, res: NextAp
? doc.outLinkUid
: teamMemberWithContact.find((member) => String(member.memberId) === String(doc.tmbId))?.name;
const valueMap: Partial<Record<AppLogKeysEnum, () => any>> = {
const valueMap: Record<string, () => any> = {
[AppLogKeysEnum.SOURCE]: () => source,
[AppLogKeysEnum.CREATED_TIME]: () => createdTime,
[AppLogKeysEnum.LAST_CONVERSATION_TIME]: () => lastConversationTime,
@@ -263,21 +342,40 @@ async function handler(req: ApiRequestProps<ExportChatLogsBody, {}>, res: NextAp
[AppLogKeysEnum.SESSION_ID]: () => doc.id || '-',
[AppLogKeysEnum.MESSAGE_COUNT]: () => doc.messageCount,
[AppLogKeysEnum.FEEDBACK]: () => {
const good = doc.userGoodFeedbackCount || 0;
const bad = doc.userBadFeedbackCount || 0;
return `good: ${good}, bad: ${bad}`;
const goodItems = (doc.userGoodFeedbackItems || []).map((item: any) => ({
chatItemId: item._id,
feedback: item.userGoodFeedback
}));
const badItems = (doc.userBadFeedbackItems || []).map((item: any) => ({
chatItemId: item._id,
feedback: item.userBadFeedback
}));
return formatJsonString({ good: goodItems, bad: badItems });
},
[AppLogKeysEnum.CUSTOM_FEEDBACK]: () => {
const customItems = (doc.customFeedbackItems || []).map((item: any) => ({
chatItemId: item._id,
feedbacks: item.customFeedbacks || []
}));
return formatJsonString(customItems);
},
[AppLogKeysEnum.ANNOTATED_COUNT]: () => {
const markItems = (doc.markItems || []).map((item: any) => ({
chatItemId: item._id,
feedback: item.adminFeedback
}));
return formatJsonString(markItems);
},
[AppLogKeysEnum.CUSTOM_FEEDBACK]: () => doc.customFeedbacksCount || 0,
[AppLogKeysEnum.ANNOTATED_COUNT]: () => doc.markCount || 0,
[AppLogKeysEnum.RESPONSE_TIME]: () =>
doc.averageResponseTime ? Number(doc.averageResponseTime).toFixed(2) : 0,
[AppLogKeysEnum.ERROR_COUNT]: () => doc.errorCount || 0,
[AppLogKeysEnum.POINTS]: () => (doc.totalPoints ? Number(doc.totalPoints).toFixed(2) : 0)
[AppLogKeysEnum.POINTS]: () => (doc.totalPoints ? Number(doc.totalPoints).toFixed(2) : 0),
chatDetails: () => formatJsonString(doc.chatDetails || [])
};
const row = logKeys
const row = [...logKeys, 'chatDetails']
.map((key) => {
const getter = valueMap[key as AppLogKeysEnum];
const getter = valueMap[key];
const val = getter ? getter() : '';
return sanitizeCsvField(val ?? '');
})