4.14.4 features (#6090)

* perf: zod with app log (#6083)

* perf: safe decode

* perf: zod with app log

* fix: text

* remove log

* rename field

* refactor: improve like/dislike interaction (#6080)

* refactor: improve like/dislike interaction

* button style & merge status

* perf

* fix

* i18n

* feedback ui

* format

* api optimize

* openapi

* read status

---------

Co-authored-by: archer <545436317@qq.com>

* perf: remove empty chat

* perf: delete resource tip

* fix: confirm

* feedback filter

* fix: ts

* perf: linker scroll

* perf: feedback ui

* fix: plugin file input store

* fix: max tokens

* update comment

* fix: condition value type

* fix feedback (#6095)

* fix feedback

* text

* list

* fix: versionid

---------

Co-authored-by: archer <545436317@qq.com>

* fix: chat setting render;export logs filter

* add test

* perf: log list api

* perf: redirect check

* perf: log list

* create ui

* create ui

---------

Co-authored-by: heheer <heheer@sealos.io>
This commit is contained in:
Archer
2025-12-15 23:36:54 +08:00
committed by GitHub
parent 13681c9246
commit af669a1cfc
135 changed files with 6363 additions and 2021 deletions
+7 -2
View File
@@ -64,6 +64,10 @@ const ChatItemSchema = new Schema({
// Field memory
memories: Object,
errorMsg: String,
durationSeconds: Number,
citeCollectionIds: [String],
// Feedback
userGoodFeedback: String,
userBadFeedback: String,
customFeedbacks: [String],
@@ -76,8 +80,7 @@ const ChatItemSchema = new Schema({
a: String
}
},
durationSeconds: Number,
citeCollectionIds: [String],
isFeedbackRead: Boolean,
// @deprecated
[DispatchNodeResponseKeyEnum.nodeResponse]: Array
@@ -91,6 +94,8 @@ const ChatItemSchema = new Schema({
close custom feedback;
*/
ChatItemSchema.index({ appId: 1, chatId: 1, dataId: 1 });
// Anchor filter
ChatItemSchema.index({ appId: 1, chatId: 1, _id: -1 });
// timer, clear history
ChatItemSchema.index({ teamId: 1, time: -1 });
+82 -5
View File
@@ -8,6 +8,7 @@ import {
} from '@fastgpt/global/support/user/team/constant';
import { AppCollectionName } from '../app/schema';
import { chatCollectionName } from './constants';
import { AppVersionCollectionName } from '../app/version/schema';
const ChatSchema = new Schema({
chatId: {
@@ -33,6 +34,10 @@ const ChatSchema = new Schema({
ref: AppCollectionName,
required: true
},
appVersionId: {
type: Schema.Types.ObjectId,
ref: AppVersionCollectionName
},
createTime: {
type: Date,
default: () => new Date()
@@ -84,12 +89,16 @@ const ChatSchema = new Schema({
default: {}
},
initStatistics: Boolean
// Feedback count statistics (redundant fields for performance)
// Boolean flags for efficient filtering
hasGoodFeedback: Boolean,
hasBadFeedback: Boolean,
hasUnreadGoodFeedback: Boolean,
hasUnreadBadFeedback: Boolean
});
try {
// Tmp
ChatSchema.index({ initStatistics: 1, _id: -1 });
ChatSchema.index({ appId: 1, tmbId: 1, outLinkUid: 1 });
ChatSchema.index({ chatId: 1 });
@@ -98,8 +107,76 @@ try {
// delete by appid; clear history; init chat; update chat; auth chat; get chat;
ChatSchema.index({ appId: 1, chatId: 1 });
// get chat logs;
ChatSchema.index({ teamId: 1, appId: 1, sources: 1, tmbId: 1, updateTime: -1 });
/* get chat logs */
// 1. No feedback filter
ChatSchema.index({ teamId: 1, appId: 1, source: 1, tmbId: 1, updateTime: -1 });
/* 反馈过滤的索引 */
// 2. Has good feedback filter
ChatSchema.index(
{
teamId: 1,
appId: 1,
source: 1,
tmbId: 1,
hasGoodFeedback: 1,
updateTime: -1
},
{
partialFilterExpression: {
hasGoodFeedback: true
}
}
);
// 3. Has bad feedback filter
ChatSchema.index(
{
teamId: 1,
appId: 1,
source: 1,
tmbId: 1,
hasBadFeedback: 1,
updateTime: -1
},
{
partialFilterExpression: {
hasBadFeedback: true
}
}
);
// 4. Has unread good feedback filter
ChatSchema.index(
{
teamId: 1,
appId: 1,
source: 1,
tmbId: 1,
hasUnreadGoodFeedback: 1,
updateTime: -1
},
{
partialFilterExpression: {
hasUnreadGoodFeedback: true
}
}
);
// 5. Has unread bad feedback filter
ChatSchema.index(
{
teamId: 1,
appId: 1,
source: 1,
tmbId: 1,
hasUnreadBadFeedback: 1,
updateTime: -1
},
{
partialFilterExpression: {
hasUnreadBadFeedback: true
}
}
);
// get share chat history
ChatSchema.index({ shareId: 1, outLinkUid: 1, updateTime: -1 });
+337 -40
View File
@@ -1,66 +1,197 @@
import type { ChatHistoryItemResType, ChatItemType } from '@fastgpt/global/core/chat/type';
import { MongoChatItem } from './chatItemSchema';
import { MongoChat } from './chatSchema';
import { addLog } from '../../common/system/log';
import { DispatchNodeResponseKeyEnum } from '@fastgpt/global/core/workflow/runtime/constants';
import { ChatRoleEnum } from '@fastgpt/global/core/chat/constants';
import { MongoChatItemResponse } from './chatItemResponseSchema';
import type { ClientSession } from '../../common/mongo';
import { Types } from '../../common/mongo';
import { mongoSessionRun } from '../../common/mongo/sessionRun';
import { UserError } from '@fastgpt/global/common/error/utils';
export async function getChatItems({
appId,
chatId,
offset,
field,
limit,
field
offset,
initialId,
prevId,
nextId
}: {
appId: string;
chatId?: string;
offset: number;
limit: number;
field: string;
}): Promise<{ histories: ChatItemType[]; total: number }> {
limit: number;
offset?: number;
initialId?: string;
prevId?: string;
nextId?: string;
}): Promise<{
histories: ChatItemType[];
total: number;
hasMorePrev: boolean;
hasMoreNext: boolean;
}> {
if (!chatId) {
return { histories: [], total: 0 };
return { histories: [], total: 0, hasMorePrev: false, hasMoreNext: false };
}
// Extend dataId
field = `dataId ${field}`;
const [histories, total] = await Promise.all([
MongoChatItem.find({ appId, chatId }, field).sort({ _id: -1 }).skip(offset).limit(limit).lean(),
MongoChatItem.countDocuments({ appId, chatId })
]);
histories.reverse();
const baseCondition = { appId, chatId };
const { histories, total, hasMorePrev, hasMoreNext } = await (async () => {
// Mode 1: offset pagination (original logic)
if (offset !== undefined) {
const [foundHistories, count] = await Promise.all([
MongoChatItem.find(baseCondition, field).sort({ _id: -1 }).skip(offset).limit(limit).lean(),
MongoChatItem.countDocuments(baseCondition)
]);
return {
histories: foundHistories.reverse(),
total: count,
hasMorePrev: count > limit,
hasMoreNext: offset > 0
};
}
// Mode 2: prevId - get records before the target
else if (prevId) {
const prevItem = await MongoChatItem.findOne(
{
...baseCondition,
dataId: prevId
},
{ _id: 1 }
).lean();
if (!prevItem) return Promise.reject(new UserError('Prev item not found'));
const [items, count] = await Promise.all([
MongoChatItem.find({ ...baseCondition, _id: { $lt: prevItem._id } }, field)
.sort({ _id: -1 })
.limit(limit + 1)
.lean(),
MongoChatItem.countDocuments({ ...baseCondition })
]);
return {
histories: items.slice(0, limit).reverse(),
total: count,
hasMorePrev: items.length > limit,
hasMoreNext: true
};
}
// Mode 3: nextId - get records after the target
else if (nextId) {
const nextItem = await MongoChatItem.findOne(
{
...baseCondition,
dataId: nextId
},
{ _id: 1 }
).lean();
if (!nextItem) return Promise.reject(new UserError('Next item not found'));
const [items, total] = await Promise.all([
MongoChatItem.find({ ...baseCondition, _id: { $gt: nextItem._id } }, field)
.sort({ _id: 1 })
.limit(limit + 1)
.lean(),
MongoChatItem.countDocuments({ ...baseCondition })
]);
return {
histories: items.slice(0, limit),
total,
hasMorePrev: true,
hasMoreNext: items.length > limit
};
}
// Mode 2: initialId - get records around the target
else {
if (!initialId) {
const [foundHistories, count] = await Promise.all([
MongoChatItem.find(baseCondition, field).sort({ _id: -1 }).skip(0).limit(limit).lean(),
MongoChatItem.countDocuments(baseCondition)
]);
return {
histories: foundHistories.reverse(),
total: count,
hasMorePrev: count > limit,
hasMoreNext: false
};
}
const halfLimit = Math.floor(limit / 2);
const ceilLimit = Math.ceil(limit / 2);
const targetItem = await MongoChatItem.findOne(
{ ...baseCondition, dataId: initialId },
field
).lean();
if (!targetItem) return Promise.reject(new UserError('Target item not found'));
const [prevItems, nextItems, count] = await Promise.all([
MongoChatItem.find({ ...baseCondition, _id: { $lt: targetItem._id } }, field)
.sort({ _id: -1 })
.limit(halfLimit + 1)
.lean(),
MongoChatItem.find({ ...baseCondition, _id: { $gt: targetItem._id } }, field)
.sort({ _id: 1 })
.limit(ceilLimit + 1)
.lean(),
MongoChatItem.countDocuments(baseCondition)
]);
return {
histories: [
...prevItems.slice(0, halfLimit).reverse(),
targetItem,
...nextItems.slice(0, ceilLimit)
].filter(Boolean),
total: count,
hasMorePrev: prevItems.length > halfLimit,
hasMoreNext: nextItems.length > ceilLimit
};
}
})();
// Add node responses field
if (field.includes(DispatchNodeResponseKeyEnum.nodeResponse)) {
if (field.includes(DispatchNodeResponseKeyEnum.nodeResponse) && histories.length > 0) {
const chatItemDataIds = histories
.filter((item) => item.obj === ChatRoleEnum.AI && !item.responseData?.length)
.map((item) => item.dataId);
const chatItemResponsesMap = await MongoChatItemResponse.find(
{ appId, chatId, chatItemDataId: { $in: chatItemDataIds } },
{ chatItemDataId: 1, data: 1 }
)
.lean()
.then((res) => {
const map = new Map<string, ChatHistoryItemResType[]>();
res.forEach((item) => {
const val = map.get(item.chatItemDataId) || [];
val.push(item.data);
map.set(item.chatItemDataId, val);
if (chatItemDataIds.length > 0) {
const chatItemResponsesMap = await MongoChatItemResponse.find(
{ appId, chatId, chatItemDataId: { $in: chatItemDataIds } },
{ chatItemDataId: 1, data: 1 }
)
.lean()
.then((res) => {
const map = new Map<string, ChatHistoryItemResType[]>();
res.forEach((item) => {
const val = map.get(item.chatItemDataId) || [];
val.push(item.data);
map.set(item.chatItemDataId, val);
});
return map;
});
return map;
});
histories.forEach((item) => {
const val = chatItemResponsesMap.get(String(item.dataId));
if (item.obj === ChatRoleEnum.AI && val) {
item.responseData = val;
}
});
histories.forEach((item) => {
const val = chatItemResponsesMap.get(String(item.dataId));
if (item.obj === ChatRoleEnum.AI && val) {
item.responseData = val;
}
});
}
}
return { histories, total };
return { histories, total, hasMorePrev, hasMoreNext };
}
export const addCustomFeedbacks = async ({
@@ -77,17 +208,183 @@ export const addCustomFeedbacks = async ({
if (!chatId || !dataId) return;
try {
await MongoChatItem.findOneAndUpdate(
{
await mongoSessionRun(async (session) => {
// Add custom feedbacks to ChatItem
await MongoChatItem.updateOne(
{
appId,
chatId,
dataId
},
{
$push: { customFeedbacks: { $each: feedbacks } }
},
{ session }
);
// Update ChatLog feedback statistics
await updateChatFeedbackCount({
appId,
chatId,
dataId
},
{
$push: { customFeedbacks: { $each: feedbacks } }
}
);
session
});
});
} catch (error) {
addLog.error('addCustomFeedbacks error', error);
throw error;
}
};
/**
* Update feedback count statistics for a chat in Chat table
* This method aggregates feedback data from chatItems and updates the Chat table
*
* @param appId - Application ID
* @param chatId - Chat ID
* @param session - Optional MongoDB session for transaction support
*/
export async function updateChatFeedbackCount({
appId,
chatId,
session
}: {
appId: string;
chatId: string;
session?: ClientSession;
}): Promise<void> {
try {
// Aggregate feedback statistics from chatItems
const stats = await MongoChatItem.aggregate(
[
{
$match: {
appId: new Types.ObjectId(appId),
chatId,
obj: ChatRoleEnum.AI
}
},
{
$group: {
_id: null,
goodFeedbackCount: {
$sum: {
$cond: [{ $ifNull: ['$userGoodFeedback', false] }, 1, 0]
}
},
badFeedbackCount: {
$sum: {
$cond: [{ $ifNull: ['$userBadFeedback', false] }, 1, 0]
}
},
// Calculate unread good feedback count
unreadGoodFeedbackCount: {
$sum: {
$cond: [
{
$and: [
{ $ne: [{ $ifNull: ['$isFeedbackRead', false] }, true] },
{ $ne: [{ $ifNull: ['$userGoodFeedback', null] }, null] }
]
},
1,
0
]
}
},
// Calculate unread bad feedback count
unreadBadFeedbackCount: {
$sum: {
$cond: [
{
$and: [
{ $ne: [{ $ifNull: ['$isFeedbackRead', false] }, true] },
{ $ne: [{ $ifNull: ['$userBadFeedback', null] }, null] }
]
},
1,
0
]
}
}
}
}
],
{ session }
);
const feedbackStats = stats[0] || {
goodFeedbackCount: 0,
badFeedbackCount: 0,
unreadGoodFeedbackCount: 0,
unreadBadFeedbackCount: 0
};
// Calculate boolean flags
const hasGoodFeedback = feedbackStats.goodFeedbackCount > 0;
const hasBadFeedback = feedbackStats.badFeedbackCount > 0;
const hasUnreadGoodFeedback = feedbackStats.unreadGoodFeedbackCount > 0;
const hasUnreadBadFeedback = feedbackStats.unreadBadFeedbackCount > 0;
// Build update object - only set fields that are true, unset fields that are false
const updateObj: Record<string, any> = {};
const unsetObj: Record<string, any> = {};
if (hasGoodFeedback) {
updateObj.hasGoodFeedback = true;
} else {
unsetObj.hasGoodFeedback = '';
}
if (hasBadFeedback) {
updateObj.hasBadFeedback = true;
} else {
unsetObj.hasBadFeedback = '';
}
if (hasUnreadGoodFeedback) {
updateObj.hasUnreadGoodFeedback = true;
} else {
unsetObj.hasUnreadGoodFeedback = '';
}
if (hasUnreadBadFeedback) {
updateObj.hasUnreadBadFeedback = true;
} else {
unsetObj.hasUnreadBadFeedback = '';
}
// Build the final update query
const updateQuery: Record<string, any> = {};
if (Object.keys(updateObj).length > 0) {
updateQuery.$set = updateObj;
}
if (Object.keys(unsetObj).length > 0) {
updateQuery.$unset = unsetObj;
}
// Update Chat table with aggregated statistics and boolean flags
await MongoChat.updateOne(
{
appId,
chatId
},
updateQuery,
{
session
}
);
addLog.debug('updateChatFeedbackCount success', {
appId,
chatId,
stats: feedbackStats,
hasGoodFeedback,
hasBadFeedback,
hasUnreadGoodFeedback,
hasUnreadBadFeedback
});
} catch (error) {
addLog.error('updateChatFeedbackCount error', error);
throw error;
}
}
+4 -1
View File
@@ -26,9 +26,10 @@ import { VariableInputEnum } from '@fastgpt/global/core/workflow/constants';
import { encryptSecretValue, anyValueDecrypt } from '../../common/secret/utils';
import type { SecretValueType } from '@fastgpt/global/common/secret/type';
type Props = {
export type Props = {
chatId: string;
appId: string;
versionId?: string;
teamId: string;
tmbId: string;
nodes: StoreNodeItemType[];
@@ -212,6 +213,7 @@ export async function saveChat(props: Props) {
const {
chatId,
appId,
versionId,
teamId,
tmbId,
nodes,
@@ -299,6 +301,7 @@ export async function saveChat(props: Props) {
teamId,
tmbId,
appId,
appVersionId: versionId,
chatId,
variableList,
welcomeText,