mirror of
https://github.com/labring/FastGPT.git
synced 2025-12-25 20:02:47 +00:00
chat log soft delete (#6110)
* chat log soft delete * perf: history api * add history test * Update packages/web/i18n/en/app.json Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * zod parse error * fix: ts --------- Co-authored-by: archer <545436317@qq.com> Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
This commit is contained in:
parent
463b02d127
commit
09b9fa517b
|
|
@ -6,7 +6,7 @@ description: 'FastGPT V4.14.5 更新说明'
|
|||
|
||||
## 🚀 新增内容
|
||||
|
||||
|
||||
1. 对话记录使用侧改成软删除,增加从日志管理里删除对话记录。
|
||||
|
||||
## ⚙️ 优化
|
||||
|
||||
|
|
|
|||
|
|
@ -102,7 +102,7 @@
|
|||
"document/content/docs/protocol/terms.en.mdx": "2025-12-15T23:36:54+08:00",
|
||||
"document/content/docs/protocol/terms.mdx": "2025-12-15T23:36:54+08:00",
|
||||
"document/content/docs/toc.en.mdx": "2025-08-04T13:42:36+08:00",
|
||||
"document/content/docs/toc.mdx": "2025-12-09T23:33:32+08:00",
|
||||
"document/content/docs/toc.mdx": "2025-12-17T17:44:38+08:00",
|
||||
"document/content/docs/upgrading/4-10/4100.mdx": "2025-08-02T19:38:37+08:00",
|
||||
"document/content/docs/upgrading/4-10/4101.mdx": "2025-09-08T20:07:20+08:00",
|
||||
"document/content/docs/upgrading/4-11/4110.mdx": "2025-08-05T23:20:39+08:00",
|
||||
|
|
@ -120,6 +120,7 @@
|
|||
"document/content/docs/upgrading/4-14/4142.mdx": "2025-11-18T19:27:14+08:00",
|
||||
"document/content/docs/upgrading/4-14/4143.mdx": "2025-11-26T20:52:05+08:00",
|
||||
"document/content/docs/upgrading/4-14/4144.mdx": "2025-12-16T14:56:04+08:00",
|
||||
"document/content/docs/upgrading/4-14/4145.mdx": "2025-12-17T17:44:38+08:00",
|
||||
"document/content/docs/upgrading/4-8/40.mdx": "2025-08-02T19:38:37+08:00",
|
||||
"document/content/docs/upgrading/4-8/41.mdx": "2025-08-02T19:38:37+08:00",
|
||||
"document/content/docs/upgrading/4-8/42.mdx": "2025-08-02T19:38:37+08:00",
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import type { OutLinkChatAuthProps } from '../../support/permission/chat.d';
|
||||
import type { OutLinkChatAuthProps } from '../../support/permission/chat';
|
||||
|
||||
export type preUploadImgProps = OutLinkChatAuthProps & {
|
||||
// expiredTime?: Date;
|
||||
|
|
|
|||
|
|
@ -37,7 +37,7 @@ export const AppLogKeysEnumMap = {
|
|||
};
|
||||
|
||||
export const DefaultAppLogKeys = [
|
||||
{ key: AppLogKeysEnum.SOURCE, enable: true },
|
||||
{ key: AppLogKeysEnum.SOURCE, enable: false },
|
||||
{ key: AppLogKeysEnum.USER, enable: true },
|
||||
{ key: AppLogKeysEnum.TITLE, enable: true },
|
||||
{ key: AppLogKeysEnum.SESSION_ID, enable: false },
|
||||
|
|
|
|||
|
|
@ -51,6 +51,8 @@ export type ChatSchemaType = {
|
|||
hasBadFeedback?: boolean;
|
||||
hasUnreadGoodFeedback?: boolean;
|
||||
hasUnreadBadFeedback?: boolean;
|
||||
|
||||
deleteTime?: Date | null;
|
||||
};
|
||||
|
||||
export type ChatWithAppSchema = Omit<ChatSchemaType, 'appId'> & {
|
||||
|
|
@ -197,7 +199,7 @@ export type HistoryItemType = {
|
|||
};
|
||||
export type ChatHistoryItemType = HistoryItemType & {
|
||||
appId: string;
|
||||
top: boolean;
|
||||
top?: boolean;
|
||||
};
|
||||
|
||||
/* ------- response data ------------ */
|
||||
|
|
|
|||
|
|
@ -1,7 +1,17 @@
|
|||
import { z } from 'zod';
|
||||
|
||||
export const PaginationSchema = z.object({
|
||||
pageSize: z.union([z.number(), z.string()]),
|
||||
pageSize: z.union([z.number(), z.string()]).optional(),
|
||||
offset: z.union([z.number(), z.string()]).optional(),
|
||||
pageNum: z.union([z.number(), z.string()]).optional()
|
||||
});
|
||||
export type PaginationType = z.infer<typeof PaginationSchema>;
|
||||
|
||||
export const PaginationResponseSchema = <T extends z.ZodTypeAny>(itemSchema: T) =>
|
||||
z.object({
|
||||
total: z.number().optional().default(0),
|
||||
list: z.array(itemSchema).optional().default([])
|
||||
});
|
||||
export type PaginationResponseType<T extends z.ZodTypeAny> = z.infer<
|
||||
ReturnType<typeof PaginationResponseSchema<T>>
|
||||
>;
|
||||
|
|
|
|||
|
|
@ -34,7 +34,7 @@ export const ChatLogItemSchema = z.object({
|
|||
title: z.string().optional().meta({ example: '用户对话', description: '对话标题' }),
|
||||
customTitle: z.string().nullish().meta({ example: '自定义标题', description: '自定义对话标题' }),
|
||||
source: z.enum(ChatSourceEnum).meta({ example: ChatSourceEnum.api, description: '对话来源' }),
|
||||
sourceName: z.string().optional().meta({ example: 'API调用', description: '来源名称' }),
|
||||
sourceName: z.string().nullish().meta({ example: 'API调用', description: '来源名称' }),
|
||||
updateTime: z.date().meta({ example: '2024-01-01T00:30:00.000Z', description: '更新时间' }),
|
||||
createTime: z.date().meta({ example: '2024-01-01T00:00:00.000Z', description: '创建时间' }),
|
||||
messageCount: z.int().nullish().meta({ example: 10, description: '消息数量' }),
|
||||
|
|
@ -50,7 +50,7 @@ export const ChatLogItemSchema = z.object({
|
|||
totalPoints: z.number().nullish().meta({ example: 150.5, description: '总积分消耗' }),
|
||||
outLinkUid: z.string().nullish().meta({ example: 'outLink123', description: '外链用户 ID' }),
|
||||
tmbId: z.string().nullish().meta({ example: 'tmb123', description: '团队成员 ID' }),
|
||||
sourceMember: SourceMemberSchema.optional().meta({ description: '来源成员信息' }),
|
||||
sourceMember: SourceMemberSchema.nullish().meta({ description: '来源成员信息' }),
|
||||
versionName: z.string().nullish().meta({ example: 'v1.0.0', description: '版本名称' }),
|
||||
region: z.string().nullish().meta({ example: '中国', description: '区域' })
|
||||
});
|
||||
|
|
|
|||
|
|
@ -105,11 +105,11 @@ export const UpdateUserFeedbackBodySchema = z.object({
|
|||
example: 'data123',
|
||||
description: '消息数据 ID'
|
||||
}),
|
||||
userGoodFeedback: z.string().optional().nullable().meta({
|
||||
userGoodFeedback: z.string().nullish().meta({
|
||||
example: '回答很好',
|
||||
description: '用户好评反馈内容'
|
||||
}),
|
||||
userBadFeedback: z.string().optional().nullable().meta({
|
||||
userBadFeedback: z.string().nullish().meta({
|
||||
example: '回答不准确',
|
||||
description: '用户差评反馈内容'
|
||||
})
|
||||
|
|
|
|||
|
|
@ -0,0 +1,73 @@
|
|||
import z from 'zod';
|
||||
import { ObjectIdSchema } from '../../../../common/type/mongo';
|
||||
import { OutLinkChatAuthSchema } from '../../../../support/permission/chat';
|
||||
import { ChatSourceEnum } from '../../../../core/chat/constants';
|
||||
import { PaginationSchema, PaginationResponseSchema } from '../../../api';
|
||||
|
||||
// Get chat histories schema
|
||||
export const GetHistoriesBodySchema = PaginationSchema.and(
|
||||
OutLinkChatAuthSchema.and(
|
||||
z.object({
|
||||
appId: ObjectIdSchema.optional().describe('应用ID'),
|
||||
source: z.enum(ChatSourceEnum).optional().describe('对话来源'),
|
||||
startCreateTime: z.string().optional().describe('创建时间开始'),
|
||||
endCreateTime: z.string().optional().describe('创建时间结束'),
|
||||
startUpdateTime: z.string().optional().describe('更新时间开始'),
|
||||
endUpdateTime: z.string().optional().describe('更新时间结束')
|
||||
})
|
||||
)
|
||||
);
|
||||
export type GetHistoriesBodyType = z.infer<typeof GetHistoriesBodySchema>;
|
||||
export const GetHistoriesResponseSchema = PaginationResponseSchema(
|
||||
z.object({
|
||||
chatId: z.string(),
|
||||
updateTime: z.date(),
|
||||
appId: z.string(),
|
||||
customTitle: z.string().optional(),
|
||||
title: z.string(),
|
||||
top: z.boolean().optional()
|
||||
})
|
||||
);
|
||||
export type GetHistoriesResponseType = z.infer<typeof GetHistoriesResponseSchema>;
|
||||
|
||||
// Update chat history schema
|
||||
export const UpdateHistoryBodySchema = OutLinkChatAuthSchema.and(
|
||||
z.object({
|
||||
appId: ObjectIdSchema.describe('应用ID'),
|
||||
chatId: z.string().min(1).describe('对话ID'),
|
||||
title: z.string().optional().describe('标题'),
|
||||
customTitle: z.string().optional().describe('自定义标题'),
|
||||
top: z.boolean().optional().describe('是否置顶')
|
||||
})
|
||||
);
|
||||
export type UpdateHistoryBodyType = z.infer<typeof UpdateHistoryBodySchema>;
|
||||
|
||||
// Delete single chat history schema
|
||||
export const DelChatHistorySchema = OutLinkChatAuthSchema.and(
|
||||
z.object({
|
||||
appId: ObjectIdSchema.describe('应用ID'),
|
||||
chatId: z.string().min(1).describe('对话ID')
|
||||
})
|
||||
);
|
||||
export type DelChatHistoryType = z.infer<typeof DelChatHistorySchema>;
|
||||
|
||||
// Clear all chat histories schema
|
||||
export const ClearChatHistoriesSchema = OutLinkChatAuthSchema.and(
|
||||
z.object({
|
||||
appId: ObjectIdSchema.describe('应用ID')
|
||||
})
|
||||
);
|
||||
export type ClearChatHistoriesType = z.infer<typeof ClearChatHistoriesSchema>;
|
||||
|
||||
// Batch delete chat histories schema (for log manager)
|
||||
export const ChatBatchDeleteBodySchema = z.object({
|
||||
appId: ObjectIdSchema,
|
||||
chatIds: z
|
||||
.array(z.string().min(1))
|
||||
.min(1)
|
||||
.meta({
|
||||
description: '对话ID列表',
|
||||
example: ['chat_123456', 'chat_789012']
|
||||
})
|
||||
});
|
||||
export type ChatBatchDeleteBodyType = z.infer<typeof ChatBatchDeleteBodySchema>;
|
||||
|
|
@ -0,0 +1,109 @@
|
|||
import type { OpenAPIPath } from '../../../type';
|
||||
import { TagsMap } from '../../../tag';
|
||||
import {
|
||||
GetHistoriesBodySchema,
|
||||
GetHistoriesResponseSchema,
|
||||
UpdateHistoryBodySchema,
|
||||
ChatBatchDeleteBodySchema,
|
||||
DelChatHistorySchema,
|
||||
ClearChatHistoriesSchema
|
||||
} from './api';
|
||||
|
||||
export const ChatHistoryPath: OpenAPIPath = {
|
||||
'/core/chat/history/getHistories': {
|
||||
post: {
|
||||
summary: '获取对话历史列表',
|
||||
description: '分页获取指定应用的对话历史记录',
|
||||
tags: [TagsMap.chatHistory],
|
||||
requestBody: {
|
||||
content: {
|
||||
'application/json': {
|
||||
schema: GetHistoriesBodySchema
|
||||
}
|
||||
}
|
||||
},
|
||||
responses: {
|
||||
200: {
|
||||
description: '成功获取对话历史列表',
|
||||
content: {
|
||||
'application/json': {
|
||||
schema: GetHistoriesResponseSchema
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
'/core/chat/history/updateHistory': {
|
||||
put: {
|
||||
summary: '修改对话历史',
|
||||
description: '修改对话历史的标题、自定义标题或置顶状态',
|
||||
tags: [TagsMap.chatHistory],
|
||||
requestBody: {
|
||||
content: {
|
||||
'application/json': {
|
||||
schema: UpdateHistoryBodySchema
|
||||
}
|
||||
}
|
||||
},
|
||||
responses: {
|
||||
200: {
|
||||
description: '成功修改对话历史'
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
'/core/chat/history/delHistory': {
|
||||
delete: {
|
||||
summary: '删除单个对话历史',
|
||||
description: '软删除指定的单个对话记录',
|
||||
tags: [TagsMap.chatHistory],
|
||||
requestBody: {
|
||||
content: {
|
||||
'application/json': {
|
||||
schema: DelChatHistorySchema
|
||||
}
|
||||
}
|
||||
},
|
||||
responses: {
|
||||
200: {
|
||||
description: '成功删除对话'
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
'/core/chat/history/clearHistories': {
|
||||
delete: {
|
||||
summary: '清空应用对话历史',
|
||||
description: '清空指定应用的所有对话记录(软删除)',
|
||||
tags: [TagsMap.chatHistory],
|
||||
requestParams: {
|
||||
query: ClearChatHistoriesSchema
|
||||
},
|
||||
responses: {
|
||||
200: {
|
||||
description: '成功清空对话历史'
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
'/core/chat/history/batchDelete': {
|
||||
post: {
|
||||
summary: '批量删除对话历史',
|
||||
description: '批量删除指定应用的多个对话记录(真实删除),需应用日志权限。',
|
||||
tags: [TagsMap.chatHistory],
|
||||
requestBody: {
|
||||
content: {
|
||||
'application/json': {
|
||||
schema: ChatBatchDeleteBodySchema
|
||||
}
|
||||
}
|
||||
},
|
||||
responses: {
|
||||
200: {
|
||||
description: '成功删除对话'
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
|
@ -2,6 +2,7 @@ import type { OpenAPIPath } from '../../type';
|
|||
import { ChatSettingPath } from './setting';
|
||||
import { ChatFavouriteAppPath } from './favourite/index';
|
||||
import { ChatFeedbackPath } from './feedback/index';
|
||||
import { ChatHistoryPath } from './history/index';
|
||||
import { z } from 'zod';
|
||||
import { CreatePostPresignedUrlResultSchema } from '../../../../service/common/s3/type';
|
||||
import { PresignChatFileGetUrlSchema, PresignChatFilePostUrlSchema } from '../../../core/chat/api';
|
||||
|
|
@ -11,6 +12,7 @@ export const ChatPath: OpenAPIPath = {
|
|||
...ChatSettingPath,
|
||||
...ChatFavouriteAppPath,
|
||||
...ChatFeedbackPath,
|
||||
...ChatHistoryPath,
|
||||
|
||||
'/core/chat/presignChatFileGetUrl': {
|
||||
post: {
|
||||
|
|
|
|||
|
|
@ -30,7 +30,7 @@ export const openAPIDocument = createDocument({
|
|||
},
|
||||
{
|
||||
name: '对话管理',
|
||||
tags: [TagsMap.chatSetting, TagsMap.chatPage, TagsMap.chatFeedback]
|
||||
tags: [TagsMap.chatHistory, TagsMap.chatPage, TagsMap.chatFeedback, TagsMap.chatSetting]
|
||||
},
|
||||
{
|
||||
name: '插件系统',
|
||||
|
|
|
|||
|
|
@ -4,7 +4,8 @@ export const TagsMap = {
|
|||
appLog: 'Agent 日志',
|
||||
|
||||
// Chat - home
|
||||
chatPage: '对话页',
|
||||
chatPage: '对话页操作',
|
||||
chatHistory: '对话历史管理',
|
||||
chatSetting: '门户页配置',
|
||||
chatFeedback: '对话反馈',
|
||||
|
||||
|
|
|
|||
|
|
@ -1,9 +0,0 @@
|
|||
type ShareChatAuthProps = {
|
||||
shareId?: string;
|
||||
outLinkUid?: string;
|
||||
};
|
||||
type TeamChatAuthProps = {
|
||||
teamId?: string;
|
||||
teamToken?: string;
|
||||
};
|
||||
export type OutLinkChatAuthProps = ShareChatAuthProps & TeamChatAuthProps;
|
||||
|
|
@ -0,0 +1,16 @@
|
|||
import z from 'zod';
|
||||
|
||||
export const ShareChatAuthSchema = z.object({
|
||||
shareId: z.string().optional().describe('分享链接ID'),
|
||||
outLinkUid: z.string().optional().describe('外链用户ID')
|
||||
});
|
||||
export type ShareChatAuthProps = z.infer<typeof ShareChatAuthSchema>;
|
||||
|
||||
export const TeamChatAuthSchema = z.object({
|
||||
teamId: z.string().optional().describe('团队ID'),
|
||||
teamToken: z.string().optional().describe('团队Token')
|
||||
});
|
||||
export type TeamChatAuthProps = z.infer<typeof TeamChatAuthSchema>;
|
||||
|
||||
export const OutLinkChatAuthSchema = ShareChatAuthSchema.and(TeamChatAuthSchema);
|
||||
export type OutLinkChatAuthProps = z.infer<typeof OutLinkChatAuthSchema>;
|
||||
|
|
@ -38,9 +38,7 @@ export type UserType = {
|
|||
|
||||
export const SourceMemberSchema = z.object({
|
||||
name: z.string().meta({ example: '张三', description: '成员名称' }),
|
||||
avatar: z
|
||||
.string()
|
||||
.meta({ example: 'https://cloud.fastgpt.cn/avatar.png', description: '成员头像' }),
|
||||
avatar: z.string().nullish().meta({ description: '成员头像' }),
|
||||
status: z
|
||||
.enum(TeamMemberStatusEnum)
|
||||
.meta({ example: TeamMemberStatusEnum.active, description: '成员状态' })
|
||||
|
|
|
|||
|
|
@ -54,10 +54,8 @@ export const NextEntry = ({
|
|||
if (error instanceof ZodError) {
|
||||
return jsonRes(res, {
|
||||
code: 400,
|
||||
error: {
|
||||
message: 'Validation error',
|
||||
details: error.message
|
||||
},
|
||||
message: 'Data validation error',
|
||||
error,
|
||||
url: req.url
|
||||
});
|
||||
}
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ import { addLog } from '../system/log';
|
|||
import { replaceSensitiveText } from '@fastgpt/global/common/string/tools';
|
||||
import { UserError } from '@fastgpt/global/common/error/utils';
|
||||
import { clearCookie } from '../../support/permission/auth/common';
|
||||
import { ZodError } from 'zod';
|
||||
|
||||
export interface ResponseType<T = any> {
|
||||
code: number;
|
||||
|
|
@ -65,6 +66,9 @@ export function processError(params: {
|
|||
// 3. 根据错误类型记录不同级别的日志
|
||||
if (error instanceof UserError) {
|
||||
addLog.info(`Request error: ${url}, ${msg}`);
|
||||
} else if (error instanceof ZodError) {
|
||||
addLog.error(`[Zod] Error in ${url}`, error.message);
|
||||
msg = error.message;
|
||||
} else {
|
||||
addLog.error(`System unexpected error: ${url}, ${msg}`, error);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -233,7 +233,6 @@ export const onDelOneApp = async ({
|
|||
await MongoChat.deleteMany({
|
||||
appId
|
||||
});
|
||||
await getS3ChatSource().deleteChatFilesByPrefix({ appId });
|
||||
}
|
||||
|
||||
for await (const app of apps) {
|
||||
|
|
|
|||
|
|
@ -95,7 +95,13 @@ const ChatSchema = new Schema({
|
|||
hasGoodFeedback: Boolean,
|
||||
hasBadFeedback: Boolean,
|
||||
hasUnreadGoodFeedback: Boolean,
|
||||
hasUnreadBadFeedback: Boolean
|
||||
hasUnreadBadFeedback: Boolean,
|
||||
|
||||
deleteTime: {
|
||||
type: Date,
|
||||
default: null,
|
||||
select: false
|
||||
}
|
||||
});
|
||||
|
||||
try {
|
||||
|
|
@ -103,13 +109,13 @@ try {
|
|||
|
||||
ChatSchema.index({ chatId: 1 });
|
||||
// get user history
|
||||
ChatSchema.index({ tmbId: 1, appId: 1, top: -1, updateTime: -1 });
|
||||
ChatSchema.index({ tmbId: 1, appId: 1, deleteTime: 1, top: -1, updateTime: -1 });
|
||||
// delete by appid; clear history; init chat; update chat; auth chat; get chat;
|
||||
ChatSchema.index({ appId: 1, chatId: 1 });
|
||||
|
||||
/* get chat logs */
|
||||
// 1. No feedback filter
|
||||
ChatSchema.index({ teamId: 1, appId: 1, source: 1, tmbId: 1, updateTime: -1 });
|
||||
ChatSchema.index({ teamId: 1, appId: 1, source: 1, tmbId: 1, deleteTime: 1, updateTime: -1 });
|
||||
|
||||
/* 反馈过滤的索引 */
|
||||
// 2. Has good feedback filter
|
||||
|
|
@ -120,11 +126,13 @@ try {
|
|||
source: 1,
|
||||
tmbId: 1,
|
||||
hasGoodFeedback: 1,
|
||||
deleteTime: 1,
|
||||
updateTime: -1
|
||||
},
|
||||
{
|
||||
partialFilterExpression: {
|
||||
hasGoodFeedback: true
|
||||
hasGoodFeedback: true,
|
||||
deleteTime: null
|
||||
}
|
||||
}
|
||||
);
|
||||
|
|
@ -136,11 +144,13 @@ try {
|
|||
source: 1,
|
||||
tmbId: 1,
|
||||
hasBadFeedback: 1,
|
||||
deleteTime: 1,
|
||||
updateTime: -1
|
||||
},
|
||||
{
|
||||
partialFilterExpression: {
|
||||
hasBadFeedback: true
|
||||
hasBadFeedback: true,
|
||||
deleteTime: null
|
||||
}
|
||||
}
|
||||
);
|
||||
|
|
@ -152,11 +162,13 @@ try {
|
|||
source: 1,
|
||||
tmbId: 1,
|
||||
hasUnreadGoodFeedback: 1,
|
||||
deleteTime: 1,
|
||||
updateTime: -1
|
||||
},
|
||||
{
|
||||
partialFilterExpression: {
|
||||
hasUnreadGoodFeedback: true
|
||||
hasUnreadGoodFeedback: true,
|
||||
deleteTime: null
|
||||
}
|
||||
}
|
||||
);
|
||||
|
|
@ -168,11 +180,13 @@ try {
|
|||
source: 1,
|
||||
tmbId: 1,
|
||||
hasUnreadBadFeedback: 1,
|
||||
deleteTime: 1,
|
||||
updateTime: -1
|
||||
},
|
||||
{
|
||||
partialFilterExpression: {
|
||||
hasUnreadBadFeedback: true
|
||||
hasUnreadBadFeedback: true,
|
||||
deleteTime: null
|
||||
}
|
||||
}
|
||||
);
|
||||
|
|
|
|||
|
|
@ -6,7 +6,11 @@ import MyIcon from '../Icon';
|
|||
import { iconPaths } from '../Icon/constants';
|
||||
import MyImage from '../Image/MyImage';
|
||||
|
||||
const Avatar = ({ w = '30px', src, ...props }: ImageProps) => {
|
||||
const Avatar = ({
|
||||
w = '30px',
|
||||
src,
|
||||
...props
|
||||
}: Omit<ImageProps, 'src'> & { src?: string | null }) => {
|
||||
// @ts-ignore
|
||||
const isIcon = !!iconPaths[src as any];
|
||||
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import React, { useMemo } from 'react';
|
||||
import React from 'react';
|
||||
import { useTranslation } from 'next-i18next';
|
||||
import MyIcon from '../Icon';
|
||||
import { useRequest2 } from '../../../hooks/useRequest';
|
||||
|
|
@ -11,9 +11,9 @@ import {
|
|||
HStack,
|
||||
Box,
|
||||
Button,
|
||||
PopoverArrow,
|
||||
Portal
|
||||
PopoverArrow
|
||||
} from '@chakra-ui/react';
|
||||
import { useMemoEnhance } from '../../../hooks/useMemoEnhance';
|
||||
|
||||
const PopoverConfirm = ({
|
||||
content,
|
||||
|
|
@ -32,13 +32,13 @@ const PopoverConfirm = ({
|
|||
Trigger: React.ReactNode;
|
||||
placement?: PlacementWithLogical;
|
||||
offset?: [number, number];
|
||||
onConfirm: () => any;
|
||||
onConfirm: () => Promise<any> | any;
|
||||
confirmText?: string;
|
||||
cancelText?: string;
|
||||
}) => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
const map = useMemo(() => {
|
||||
const map = useMemoEnhance(() => {
|
||||
const map = {
|
||||
info: {
|
||||
variant: 'primary',
|
||||
|
|
@ -56,7 +56,7 @@ const PopoverConfirm = ({
|
|||
const firstFieldRef = React.useRef(null);
|
||||
const { onOpen, onClose, isOpen } = useDisclosure();
|
||||
|
||||
const { runAsync: onclickConfirm, loading } = useRequest2(onConfirm, {
|
||||
const { runAsync: onclickConfirm, loading } = useRequest2(async () => onConfirm(), {
|
||||
onSuccess: onClose
|
||||
});
|
||||
|
||||
|
|
@ -90,7 +90,14 @@ const PopoverConfirm = ({
|
|||
</HStack>
|
||||
<HStack mt={2} justifyContent={'flex-end'}>
|
||||
{showCancel && (
|
||||
<Button variant={'whiteBase'} size="sm" onClick={onClose}>
|
||||
<Button
|
||||
variant={'whiteBase'}
|
||||
size="sm"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
onClose();
|
||||
}}
|
||||
>
|
||||
{cancelText || t('common:Cancel')}
|
||||
</Button>
|
||||
)}
|
||||
|
|
|
|||
|
|
@ -64,15 +64,20 @@ export const useTableMultipleSelect = <T = any,>({
|
|||
children,
|
||||
Controler,
|
||||
activedStyles,
|
||||
activeBg,
|
||||
...props
|
||||
}: { children?: ReactNode; activedStyles?: FlexProps; Controler: ReactNode } & FlexProps) => {
|
||||
}: {
|
||||
children?: ReactNode;
|
||||
activeBg?: string;
|
||||
activedStyles?: FlexProps;
|
||||
Controler: ReactNode;
|
||||
} & FlexProps) => {
|
||||
return hasSelections || !!children ? (
|
||||
<Flex
|
||||
w={'100%'}
|
||||
bg="white"
|
||||
bg={selectedCount > 0 ? activeBg : 'transparent'}
|
||||
px={6}
|
||||
pt={4}
|
||||
pb={2}
|
||||
py={2}
|
||||
alignItems="center"
|
||||
{...props}
|
||||
{...activedStyles}
|
||||
|
|
|
|||
|
|
@ -71,6 +71,8 @@
|
|||
"confirm_copy_app_tip": "The system will create an app with the same configuration for you, but permissions will not be copied. Please confirm!",
|
||||
"confirm_del_app_tip": "Confirm to delete this Agent? \nDeleting an Agent will delete its associated conversation records as well.",
|
||||
"confirm_del_tool_tip": "Confirm to delete this tool? \nDeleting a tool will delete its associated conversation records, and the Agent that uses the tool may not function properly.",
|
||||
"confirm_delete_chat_content": "Are you sure you want to delete this chat? This action cannot be undone!",
|
||||
"confirm_delete_chats": "Are you sure you want to delete {{n}} conversation records? \nThe records will be permanently deleted!",
|
||||
"confirm_delete_folder_tip": "When you delete this folder, all applications and corresponding chat records under it will be deleted.",
|
||||
"confirm_delete_tool": "Confirm to delete this tool?",
|
||||
"copilot_config_message": "Current Node Configuration Information: \n Code Type: {{codeType}} \n Current Code: \\\\`\\\\`\\\\`{{codeType}} \n{{code}} \\\\`\\\\`\\\\` \n Input Parameters: {{inputs}} \n Output Parameters: {{outputs}}",
|
||||
|
|
|
|||
|
|
@ -73,6 +73,8 @@
|
|||
"confirm_copy_app_tip": "系统将为您创建一个相同配置应用,但权限不会进行复制,请确认!",
|
||||
"confirm_del_app_tip": "确认删除该 Agent?删除 Agent 会将其关联的对话记录一并删除。",
|
||||
"confirm_del_tool_tip": "确认删除该工具?删除工具会将其关联的对话记录一并删除,并且使用到该工具的 Agent 可能无法正常运行。",
|
||||
"confirm_delete_chat_content": "确定要删除这个对话吗?此操作不可恢复!",
|
||||
"confirm_delete_chats": "确认删除 {{n}} 组对话记录?记录将会永久删除!",
|
||||
"confirm_delete_folder_tip": "删除该文件夹时,将会删除它下面所有应用及对应的聊天记录。",
|
||||
"confirm_delete_tool": "确认删除该工具?",
|
||||
"copilot_config_message": "`当前节点配置信息: \n代码类型:{{codeType}} \n当前代码: \\`\\`\\`{{codeType}} \n{{code}} \\`\\`\\` \n输入参数: {{inputs}} \n输出参数: {{outputs}}`",
|
||||
|
|
|
|||
|
|
@ -71,6 +71,8 @@
|
|||
"confirm_copy_app_tip": "系統將為您建立一個相同設定的應用程式,但權限不會複製,請確認!",
|
||||
"confirm_del_app_tip": "確認刪除該 Agent?\n刪除 Agent 會將其關聯的對話記錄一併刪除。",
|
||||
"confirm_del_tool_tip": "確認刪除該工具?\n刪除工具會將其關聯的對話記錄一併刪除,並且使用到該工具的 Agent 可能無法正常運行。",
|
||||
"confirm_delete_chat_content": "確定要刪除這個對話嗎?\n此操作不可恢復!",
|
||||
"confirm_delete_chats": "確認刪除 {{n}} 組對話記錄?\n記錄將會永久刪除!",
|
||||
"confirm_delete_folder_tip": "刪除該文件夾時,將會刪除它下面所有應用及對應的聊天記錄。",
|
||||
"confirm_delete_tool": "確認刪除該工具?",
|
||||
"copilot_config_message": "當前節點配置信息: \n代碼類型:{{codeType}} \n當前代碼: \\\\`\\\\`\\\\`{{codeType}} \n{{code}} \\\\`\\\\`\\\\` \n輸入參數: {{inputs}} \n輸出參數: {{outputs}}",
|
||||
|
|
|
|||
|
|
@ -1,11 +1,12 @@
|
|||
import type { AppChatConfigType, AppTTSConfigType } from '@fastgpt/global/core/app/type.d';
|
||||
import type { AdminFbkType } from '@fastgpt/global/core/chat/type';
|
||||
import { ChatItemType } from '@fastgpt/global/core/chat/type';
|
||||
import type { OutLinkChatAuthProps } from '@fastgpt/global/support/permission/chat.d';
|
||||
import type { OutLinkChatAuthProps } from '@fastgpt/global/support/permission/chat';
|
||||
import type { AppTypeEnum } from '@fastgpt/global/core/app/constants';
|
||||
import { RequestPaging } from '@/types';
|
||||
import type { GetChatTypeEnum } from '@/global/core/chat/constants';
|
||||
import type { ChatSourceEnum } from '@fastgpt/global/core/chat/constants';
|
||||
|
||||
export type GetChatSpeechProps = OutLinkChatAuthProps & {
|
||||
appId: string;
|
||||
ttsConfig: AppTTSConfigType;
|
||||
|
|
@ -56,33 +57,6 @@ export type InitChatResponse = {
|
|||
};
|
||||
};
|
||||
|
||||
/* ---------- history ----------- */
|
||||
export type GetHistoriesProps = OutLinkChatAuthProps & {
|
||||
appId?: string;
|
||||
source?: `${ChatSourceEnum}`;
|
||||
|
||||
startCreateTime?: string;
|
||||
endCreateTime?: string;
|
||||
startUpdateTime?: string;
|
||||
endUpdateTime?: string;
|
||||
};
|
||||
|
||||
export type UpdateHistoryProps = OutLinkChatAuthProps & {
|
||||
appId: string;
|
||||
chatId: string;
|
||||
title?: string;
|
||||
customTitle?: string;
|
||||
top?: boolean;
|
||||
};
|
||||
|
||||
export type DelHistoryProps = OutLinkChatAuthProps & {
|
||||
appId: string;
|
||||
chatId: string;
|
||||
};
|
||||
export type ClearHistoriesProps = OutLinkChatAuthProps & {
|
||||
appId: string;
|
||||
};
|
||||
|
||||
/* -------- chat item ---------- */
|
||||
export type DeleteChatItemProps = OutLinkChatAuthProps & {
|
||||
appId: string;
|
||||
|
|
@ -90,16 +64,3 @@ export type DeleteChatItemProps = OutLinkChatAuthProps & {
|
|||
contentId?: string;
|
||||
delFile?: boolean;
|
||||
};
|
||||
|
||||
export type AdminUpdateFeedbackParams = AdminFbkType & {
|
||||
appId: string;
|
||||
chatId: string;
|
||||
dataId: string;
|
||||
};
|
||||
|
||||
export type CloseCustomFeedbackParams = {
|
||||
appId: string;
|
||||
chatId: string;
|
||||
dataId: string;
|
||||
index: number;
|
||||
};
|
||||
|
|
|
|||
|
|
@ -10,7 +10,8 @@ import {
|
|||
Td,
|
||||
Th,
|
||||
Thead,
|
||||
Tr
|
||||
Tr,
|
||||
Checkbox
|
||||
} from '@chakra-ui/react';
|
||||
import type { ChatSourceEnum } from '@fastgpt/global/core/chat/constants';
|
||||
import { ChatSourceMap } from '@fastgpt/global/core/chat/constants';
|
||||
|
|
@ -53,6 +54,10 @@ import MyBox from '@fastgpt/web/components/common/MyBox';
|
|||
import { useContextSelector } from 'use-context-selector';
|
||||
import { AppContext } from '../context';
|
||||
import { useMemoEnhance } from '@fastgpt/web/hooks/useMemoEnhance';
|
||||
import { batchDeleteChatHistories } from '@/web/core/chat/history/api';
|
||||
import { useTableMultipleSelect } from '@fastgpt/web/hooks/useTableMultipleSelect';
|
||||
import MyIconButton from '@fastgpt/web/components/common/Icon/button';
|
||||
import { useConfirm } from '@fastgpt/web/hooks/useConfirm';
|
||||
|
||||
const DetailLogsModal = dynamic(() => import('./DetailLogsModal'));
|
||||
|
||||
|
|
@ -213,7 +218,34 @@ const LogTable = ({
|
|||
refreshDeps: [params]
|
||||
});
|
||||
|
||||
const HeaderRenderMap = useMemo(
|
||||
const {
|
||||
selectedItems,
|
||||
toggleSelect,
|
||||
isSelected,
|
||||
FloatingActionBar,
|
||||
isSelecteAll,
|
||||
selectAllTrigger
|
||||
} = useTableMultipleSelect({
|
||||
list: logs,
|
||||
getItemId: (item) => item._id
|
||||
});
|
||||
const chatIds = useMemoEnhance(() => selectedItems.map((item) => item.chatId), [selectedItems]);
|
||||
|
||||
const { openConfirm: openConfirmDelete, ConfirmModal: ConfirmDeleteModal } = useConfirm({
|
||||
type: 'delete'
|
||||
});
|
||||
|
||||
const { runAsync: handleDelete } = useRequest2(
|
||||
async (chatIds: string[]) => {
|
||||
await batchDeleteChatHistories({ appId, chatIds });
|
||||
await getData(pageNum);
|
||||
},
|
||||
{
|
||||
successToast: t('common:delete_success')
|
||||
}
|
||||
);
|
||||
|
||||
const HeaderRenderMap = useMemoEnhance(
|
||||
() => ({
|
||||
[AppLogKeysEnum.SOURCE]: <Th key={AppLogKeysEnum.SOURCE}>{t('app:logs_keys_source')}</Th>,
|
||||
[AppLogKeysEnum.CREATED_TIME]: (
|
||||
|
|
@ -503,9 +535,13 @@ const LogTable = ({
|
|||
<Table variant={'simple'} fontSize={'sm'}>
|
||||
<Thead>
|
||||
<Tr>
|
||||
<Th>
|
||||
<Checkbox isChecked={isSelecteAll} onChange={selectAllTrigger} />
|
||||
</Th>
|
||||
{logKeys
|
||||
.filter((logKey) => logKey.enable)
|
||||
.map((logKey) => HeaderRenderMap[logKey.key])}
|
||||
<Th>{t('common:Action')}</Th>
|
||||
</Tr>
|
||||
</Thead>
|
||||
<Tbody fontSize={'xs'}>
|
||||
|
|
@ -516,12 +552,28 @@ const LogTable = ({
|
|||
key={item._id}
|
||||
_hover={{ bg: 'myWhite.600' }}
|
||||
cursor={'pointer'}
|
||||
title={t('common:core.view_chat_detail')}
|
||||
onClick={() => setDetailLogsId(item.chatId)}
|
||||
>
|
||||
<Td>
|
||||
<HStack onClick={(e) => e.stopPropagation()}>
|
||||
<Checkbox isChecked={isSelected(item)} onChange={() => toggleSelect(item)} />
|
||||
</HStack>
|
||||
</Td>
|
||||
{logKeys
|
||||
.filter((logKey) => logKey.enable)
|
||||
.map((logKey) => cellRenderMap[logKey.key as AppLogKeysEnum])}
|
||||
<Td onClick={(e) => e.stopPropagation()}>
|
||||
<PopoverConfirm
|
||||
content={t('app:confirm_delete_chat_content')}
|
||||
type="delete"
|
||||
onConfirm={() => handleDelete([item.chatId])}
|
||||
Trigger={
|
||||
<Flex>
|
||||
<MyIconButton icon={'delete'} hoverColor={'red.600'} hoverBg="red.100" />
|
||||
</Flex>
|
||||
}
|
||||
/>
|
||||
</Td>
|
||||
</Tr>
|
||||
);
|
||||
})}
|
||||
|
|
@ -530,11 +582,32 @@ const LogTable = ({
|
|||
{logs.length === 0 && !isLoading && <EmptyTip text={t('app:logs_empty')}></EmptyTip>}
|
||||
</TableContainer>
|
||||
|
||||
{total >= pageSize && (
|
||||
<Flex mt={3} justifyContent={'center'}>
|
||||
<Pagination />
|
||||
</Flex>
|
||||
)}
|
||||
<FloatingActionBar
|
||||
pb={0}
|
||||
Controler={
|
||||
<HStack>
|
||||
<Button
|
||||
variant={'whiteDanger'}
|
||||
onClick={() =>
|
||||
openConfirmDelete({
|
||||
onConfirm: () => handleDelete(chatIds),
|
||||
customContent: t('app:confirm_delete_chats', {
|
||||
n: chatIds.length
|
||||
})
|
||||
})()
|
||||
}
|
||||
>
|
||||
{t('common:Delete')} ({chatIds.length})
|
||||
</Button>
|
||||
</HStack>
|
||||
}
|
||||
>
|
||||
{total > pageSize && (
|
||||
<Flex justifyContent={'center'}>
|
||||
<Pagination />
|
||||
</Flex>
|
||||
)}
|
||||
</FloatingActionBar>
|
||||
|
||||
{!!detailLogsId && (
|
||||
<DetailLogsModal
|
||||
|
|
@ -546,6 +619,8 @@ const LogTable = ({
|
|||
}}
|
||||
/>
|
||||
)}
|
||||
|
||||
<ConfirmDeleteModal />
|
||||
</MyBox>
|
||||
);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -450,6 +450,7 @@ const CollectionCard = () => {
|
|||
</TableContainer>
|
||||
|
||||
<FloatingActionBar
|
||||
pt={4}
|
||||
Controler={
|
||||
<HStack>
|
||||
<Button
|
||||
|
|
|
|||
|
|
@ -1,5 +1,3 @@
|
|||
import '@scalar/api-reference-react/style.css';
|
||||
|
||||
import type { AppProps } from 'next/app';
|
||||
import Script from 'next/script';
|
||||
|
||||
|
|
|
|||
|
|
@ -340,7 +340,6 @@ async function handler(
|
|||
});
|
||||
// 获取没有 tmbId 的人员
|
||||
const listWithoutTmbId = listWithRegion.filter((item) => !item.tmbId);
|
||||
|
||||
return GetAppChatLogsResponseSchema.parse({
|
||||
list: listWithSourceMember.concat(listWithoutTmbId),
|
||||
total
|
||||
|
|
|
|||
|
|
@ -1,48 +0,0 @@
|
|||
import type { NextApiResponse } from 'next';
|
||||
import { MongoChat } from '@fastgpt/service/core/chat/chatSchema';
|
||||
import { MongoChatItem } from '@fastgpt/service/core/chat/chatItemSchema';
|
||||
import { type DelHistoryProps } from '@/global/core/chat/api';
|
||||
import { authChatCrud } from '@/service/support/permission/auth/chat';
|
||||
import { mongoSessionRun } from '@fastgpt/service/common/mongo/sessionRun';
|
||||
import { NextAPI } from '@/service/middleware/entry';
|
||||
import { type ApiRequestProps } from '@fastgpt/service/type/next';
|
||||
import { MongoChatItemResponse } from '@fastgpt/service/core/chat/chatItemResponseSchema';
|
||||
import { getS3ChatSource } from '@fastgpt/service/common/s3/sources/chat';
|
||||
|
||||
/* clear chat history */
|
||||
async function handler(req: ApiRequestProps<{}, DelHistoryProps>, res: NextApiResponse) {
|
||||
const { appId, chatId } = req.query;
|
||||
|
||||
const { uid: uId } = await authChatCrud({
|
||||
req,
|
||||
authToken: true,
|
||||
authApiKey: true,
|
||||
...req.query
|
||||
});
|
||||
|
||||
await mongoSessionRun(async (session) => {
|
||||
await MongoChatItemResponse.deleteMany({
|
||||
appId,
|
||||
chatId
|
||||
});
|
||||
await MongoChatItem.deleteMany(
|
||||
{
|
||||
appId,
|
||||
chatId
|
||||
},
|
||||
{ session }
|
||||
);
|
||||
await MongoChat.deleteOne(
|
||||
{
|
||||
appId,
|
||||
chatId
|
||||
},
|
||||
{ session }
|
||||
);
|
||||
await getS3ChatSource().deleteChatFilesByPrefix({ appId, chatId, uId });
|
||||
});
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
export default NextAPI(handler);
|
||||
|
|
@ -20,10 +20,6 @@ async function handler(
|
|||
const { appId, chatId, dataId, userBadFeedback, userGoodFeedback } =
|
||||
UpdateUserFeedbackBodySchema.parse(req.body);
|
||||
|
||||
if (!chatId || !dataId) {
|
||||
return Promise.reject('chatId or dataId is empty');
|
||||
}
|
||||
|
||||
const { teamId } = await authChatCrud({
|
||||
req,
|
||||
authToken: true,
|
||||
|
|
|
|||
|
|
@ -0,0 +1,68 @@
|
|||
import type { NextApiResponse } from 'next';
|
||||
import { MongoChat } from '@fastgpt/service/core/chat/chatSchema';
|
||||
import { MongoChatItem } from '@fastgpt/service/core/chat/chatItemSchema';
|
||||
import { mongoSessionRun } from '@fastgpt/service/common/mongo/sessionRun';
|
||||
import { NextAPI } from '@/service/middleware/entry';
|
||||
import { type ApiRequestProps } from '@fastgpt/service/type/next';
|
||||
import { MongoChatItemResponse } from '@fastgpt/service/core/chat/chatItemResponseSchema';
|
||||
import { getS3ChatSource } from '@fastgpt/service/common/s3/sources/chat';
|
||||
import { authApp } from '@fastgpt/service/support/permission/app/auth';
|
||||
import { AppReadChatLogPerVal } from '@fastgpt/global/support/permission/app/constant';
|
||||
import { ChatBatchDeleteBodySchema } from '@fastgpt/global/openapi/core/chat/history/api';
|
||||
import { UserError } from '@fastgpt/global/common/error/utils';
|
||||
|
||||
async function handler(req: ApiRequestProps, res: NextApiResponse) {
|
||||
const { appId, chatIds } = ChatBatchDeleteBodySchema.parse(req.body);
|
||||
|
||||
await authApp({
|
||||
req,
|
||||
authToken: true,
|
||||
authApiKey: true,
|
||||
appId,
|
||||
per: AppReadChatLogPerVal
|
||||
});
|
||||
|
||||
await MongoChatItemResponse.deleteMany({
|
||||
appId,
|
||||
chatId: { $in: chatIds }
|
||||
});
|
||||
await mongoSessionRun(async (session) => {
|
||||
const chatList = await MongoChat.find(
|
||||
{
|
||||
appId,
|
||||
chatId: { $in: chatIds }
|
||||
},
|
||||
'chatId tmbId outLinkUid'
|
||||
)
|
||||
.lean()
|
||||
.session(session);
|
||||
|
||||
await MongoChatItem.deleteMany(
|
||||
{
|
||||
appId,
|
||||
chatId: { $in: chatIds }
|
||||
},
|
||||
{ session }
|
||||
);
|
||||
await MongoChat.deleteMany(
|
||||
{
|
||||
appId,
|
||||
chatId: { $in: chatIds }
|
||||
},
|
||||
{ session }
|
||||
);
|
||||
await Promise.all(
|
||||
chatList.map((item) => {
|
||||
return getS3ChatSource().deleteChatFilesByPrefix({
|
||||
appId,
|
||||
chatId: item.chatId,
|
||||
uId: String(item.outLinkUid || item.tmbId)
|
||||
});
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
export default NextAPI(handler);
|
||||
|
|
@ -1,17 +1,16 @@
|
|||
import type { NextApiResponse } from 'next';
|
||||
import { MongoChat } from '@fastgpt/service/core/chat/chatSchema';
|
||||
import { MongoChatItem } from '@fastgpt/service/core/chat/chatItemSchema';
|
||||
import { type ClearHistoriesProps } from '@/global/core/chat/api';
|
||||
import { ClearChatHistoriesSchema } from '@fastgpt/global/openapi/core/chat/history/api';
|
||||
import { ChatSourceEnum } from '@fastgpt/global/core/chat/constants';
|
||||
import { NextAPI } from '@/service/middleware/entry';
|
||||
import { type ApiRequestProps } from '@fastgpt/service/type/next';
|
||||
import { authChatCrud } from '@/service/support/permission/auth/chat';
|
||||
import { MongoChatItemResponse } from '@fastgpt/service/core/chat/chatItemResponseSchema';
|
||||
import { getS3ChatSource } from '@fastgpt/service/common/s3/sources/chat';
|
||||
|
||||
/* clear chat history */
|
||||
async function handler(req: ApiRequestProps<{}, ClearHistoriesProps>, res: NextApiResponse) {
|
||||
const { appId, shareId, outLinkUid, teamId, teamToken } = req.query;
|
||||
/* clear all chat histories of an app */
|
||||
async function handler(req: ApiRequestProps, res: NextApiResponse) {
|
||||
const { appId, shareId, outLinkUid, teamId, teamToken } = ClearChatHistoriesSchema.parse(
|
||||
req.query
|
||||
);
|
||||
|
||||
const {
|
||||
teamId: chatTeamId,
|
||||
|
|
@ -61,22 +60,18 @@ async function handler(req: ApiRequestProps<{}, ClearHistoriesProps>, res: NextA
|
|||
|
||||
// find chatIds
|
||||
const list = await MongoChat.find(match, 'chatId').lean();
|
||||
const idList = list.map((item) => item.chatId);
|
||||
|
||||
await getS3ChatSource().deleteChatFilesByPrefix({ appId, uId: uid });
|
||||
|
||||
await MongoChatItemResponse.deleteMany({
|
||||
appId,
|
||||
chatId: { $in: idList }
|
||||
});
|
||||
await MongoChatItem.deleteMany({
|
||||
appId,
|
||||
chatId: { $in: idList }
|
||||
});
|
||||
await MongoChat.deleteMany({
|
||||
appId,
|
||||
chatId: { $in: idList }
|
||||
});
|
||||
await MongoChat.updateMany(
|
||||
{
|
||||
appId,
|
||||
chatId: { $in: list.map((item) => item.chatId) }
|
||||
},
|
||||
{
|
||||
$set: {
|
||||
deleteTime: new Date()
|
||||
}
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
export default NextAPI(handler);
|
||||
|
|
@ -0,0 +1,34 @@
|
|||
import type { NextApiResponse } from 'next';
|
||||
import { MongoChat } from '@fastgpt/service/core/chat/chatSchema';
|
||||
import { DelChatHistorySchema } from '@fastgpt/global/openapi/core/chat/history/api';
|
||||
import { authChatCrud } from '@/service/support/permission/auth/chat';
|
||||
import { NextAPI } from '@/service/middleware/entry';
|
||||
import { type ApiRequestProps } from '@fastgpt/service/type/next';
|
||||
|
||||
/* delete single chat history (soft delete) */
|
||||
async function handler(req: ApiRequestProps, res: NextApiResponse) {
|
||||
const { appId, chatId } = DelChatHistorySchema.parse(req.query);
|
||||
|
||||
await authChatCrud({
|
||||
req,
|
||||
authToken: true,
|
||||
authApiKey: true,
|
||||
...req.query
|
||||
});
|
||||
|
||||
await MongoChat.updateOne(
|
||||
{
|
||||
appId,
|
||||
chatId
|
||||
},
|
||||
{
|
||||
$set: {
|
||||
deleteTime: new Date()
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
export default NextAPI(handler);
|
||||
|
|
@ -5,21 +5,19 @@ import { authCert } from '@fastgpt/service/support/permission/auth/common';
|
|||
import { authTeamSpaceToken } from '@/service/support/permission/auth/team';
|
||||
import { NextAPI } from '@/service/middleware/entry';
|
||||
import { type ApiRequestProps, type ApiResponseType } from '@fastgpt/service/type/next';
|
||||
import { type PaginationProps, type PaginationResponse } from '@fastgpt/web/common/fetch/type';
|
||||
import { type GetHistoriesProps } from '@/global/core/chat/api';
|
||||
import {
|
||||
GetHistoriesBodySchema,
|
||||
GetHistoriesResponseSchema,
|
||||
type GetHistoriesResponseType
|
||||
} from '@fastgpt/global/openapi/core/chat/history/api';
|
||||
import { parsePaginationRequest } from '@fastgpt/service/common/api/pagination';
|
||||
import { addMonths } from 'date-fns';
|
||||
|
||||
export type getHistoriesQuery = {};
|
||||
|
||||
export type getHistoriesBody = PaginationProps<GetHistoriesProps>;
|
||||
|
||||
export type getHistoriesResponse = {};
|
||||
|
||||
/* get chat histories list */
|
||||
async function handler(
|
||||
req: ApiRequestProps<getHistoriesBody, getHistoriesQuery>,
|
||||
_res: ApiResponseType<any>
|
||||
): Promise<PaginationResponse<getHistoriesResponse>> {
|
||||
req: ApiRequestProps,
|
||||
_res: ApiResponseType
|
||||
): Promise<GetHistoriesResponseType> {
|
||||
const {
|
||||
appId,
|
||||
shareId,
|
||||
|
|
@ -31,7 +29,7 @@ async function handler(
|
|||
endCreateTime,
|
||||
startUpdateTime,
|
||||
endUpdateTime
|
||||
} = req.body;
|
||||
} = GetHistoriesBodySchema.parse(req.body);
|
||||
const { offset, pageSize } = parsePaginationRequest(req);
|
||||
|
||||
const match = await (async () => {
|
||||
|
|
@ -86,7 +84,7 @@ async function handler(
|
|||
};
|
||||
}
|
||||
|
||||
const mergeMatch = { ...match, ...timeMatch };
|
||||
const mergeMatch = { ...match, ...timeMatch, deleteTime: null };
|
||||
|
||||
const [data, total] = await Promise.all([
|
||||
await MongoChat.find(mergeMatch, 'chatId title top customTitle appId updateTime')
|
||||
|
|
@ -97,7 +95,7 @@ async function handler(
|
|||
MongoChat.countDocuments(mergeMatch)
|
||||
]);
|
||||
|
||||
return {
|
||||
return GetHistoriesResponseSchema.parse({
|
||||
list: data.map((item) => ({
|
||||
chatId: item.chatId,
|
||||
updateTime: item.updateTime,
|
||||
|
|
@ -107,7 +105,7 @@ async function handler(
|
|||
top: item.top
|
||||
})),
|
||||
total
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
export default NextAPI(handler);
|
||||
|
|
@ -1,15 +1,14 @@
|
|||
import type { NextApiResponse } from 'next';
|
||||
import { jsonRes } from '@fastgpt/service/common/response';
|
||||
import { type UpdateHistoryProps } from '@/global/core/chat/api.d';
|
||||
import { UpdateHistoryBodySchema } from '@fastgpt/global/openapi/core/chat/history/api';
|
||||
import { MongoChat } from '@fastgpt/service/core/chat/chatSchema';
|
||||
import { authChatCrud } from '@/service/support/permission/auth/chat';
|
||||
import { NextAPI } from '@/service/middleware/entry';
|
||||
import { type ApiRequestProps } from '@fastgpt/service/type/next';
|
||||
import { WritePermissionVal } from '@fastgpt/global/support/permission/constant';
|
||||
|
||||
/* update chat top, custom title */
|
||||
async function handler(req: ApiRequestProps<UpdateHistoryProps>, res: NextApiResponse) {
|
||||
const { appId, chatId, title, customTitle, top } = req.body;
|
||||
/* update chat history: title, customTitle, top */
|
||||
async function handler(req: ApiRequestProps, res: NextApiResponse) {
|
||||
const { appId, chatId, title, customTitle, top } = UpdateHistoryBodySchema.parse(req.body);
|
||||
await authChatCrud({
|
||||
req,
|
||||
authToken: true,
|
||||
|
|
@ -18,7 +17,7 @@ async function handler(req: ApiRequestProps<UpdateHistoryProps>, res: NextApiRes
|
|||
per: WritePermissionVal
|
||||
});
|
||||
|
||||
await MongoChat.findOneAndUpdate(
|
||||
await MongoChat.updateOne(
|
||||
{ appId, chatId },
|
||||
{
|
||||
updateTime: new Date(),
|
||||
|
|
@ -27,7 +26,6 @@ async function handler(req: ApiRequestProps<UpdateHistoryProps>, res: NextApiRes
|
|||
...(top !== undefined && { top })
|
||||
}
|
||||
);
|
||||
jsonRes(res);
|
||||
}
|
||||
|
||||
export default NextAPI(handler);
|
||||
|
|
@ -6,7 +6,7 @@ import { getErrText } from '@fastgpt/global/common/error/utils';
|
|||
import type { AppTTSConfigType } from '@fastgpt/global/core/app/type.d';
|
||||
import { TTSTypeEnum } from '@/web/core/app/constants';
|
||||
import { useTranslation } from 'next-i18next';
|
||||
import type { OutLinkChatAuthProps } from '@fastgpt/global/support/permission/chat.d';
|
||||
import type { OutLinkChatAuthProps } from '@fastgpt/global/support/permission/chat';
|
||||
import { useMount } from 'ahooks';
|
||||
import { getWebReqUrl } from '@fastgpt/web/common/system/utils';
|
||||
|
||||
|
|
|
|||
|
|
@ -1,23 +1,14 @@
|
|||
import { GET, POST, DELETE, PUT } from '@/web/common/api/request';
|
||||
import type { ChatHistoryItemType, ChatHistoryItemResType } from '@fastgpt/global/core/chat/type.d';
|
||||
import type { ChatHistoryItemResType } from '@fastgpt/global/core/chat/type.d';
|
||||
import type { getResDataQuery } from '@/pages/api/core/chat/getResData';
|
||||
import type {
|
||||
InitChatProps,
|
||||
InitChatResponse,
|
||||
InitOutLinkChatProps,
|
||||
GetHistoriesProps,
|
||||
InitTeamChatProps
|
||||
} from '@/global/core/chat/api.d';
|
||||
|
||||
import type {
|
||||
ClearHistoriesProps,
|
||||
DelHistoryProps,
|
||||
DeleteChatItemProps,
|
||||
UpdateHistoryProps
|
||||
} from '@/global/core/chat/api.d';
|
||||
import type { AuthTeamTagTokenProps } from '@fastgpt/global/support/user/team/tag';
|
||||
import type { AppListItemType } from '@fastgpt/global/core/app/type';
|
||||
import type { PaginationProps, PaginationResponse } from '@fastgpt/web/common/fetch/type';
|
||||
import type { DeleteChatItemProps } from '@/global/core/chat/api.d';
|
||||
import type {
|
||||
getChatRecordsBody,
|
||||
getChatRecordsResponse
|
||||
|
|
@ -44,11 +35,6 @@ export const getInitOutLinkChatInfo = (data: InitOutLinkChatProps) =>
|
|||
export const getTeamChatInfo = (data: InitTeamChatProps) =>
|
||||
GET<InitChatResponse>(`/core/chat/team/init`, data);
|
||||
|
||||
/**
|
||||
* get current window history(appid or shareId)
|
||||
*/
|
||||
export const getChatHistories = (data: PaginationProps<GetHistoriesProps>) =>
|
||||
POST<PaginationResponse<ChatHistoryItemType>>('/core/chat/getHistories', data);
|
||||
/**
|
||||
* get detail responseData by dataId appId chatId
|
||||
*/
|
||||
|
|
@ -58,42 +44,12 @@ export const getChatResData = (data: getResDataQuery) =>
|
|||
export const getChatRecords = (data: getChatRecordsBody) =>
|
||||
POST<getChatRecordsResponse>('/core/chat/getRecords_v2', data);
|
||||
|
||||
/**
|
||||
* delete one history
|
||||
*/
|
||||
export const delChatHistoryById = (data: DelHistoryProps) => DELETE(`/core/chat/delHistory`, data);
|
||||
/**
|
||||
* clear all history by appid
|
||||
*/
|
||||
export const delClearChatHistories = (data: ClearHistoriesProps) =>
|
||||
DELETE(`/core/chat/clearHistories`, data);
|
||||
|
||||
/**
|
||||
* delete one chat record
|
||||
*/
|
||||
export const delChatRecordById = (data: DeleteChatItemProps) =>
|
||||
POST(`/core/chat/item/delete`, data);
|
||||
|
||||
/**
|
||||
* 修改历史记录: 标题/置顶
|
||||
*/
|
||||
export const putChatHistory = (data: UpdateHistoryProps) => PUT('/core/chat/updateHistory', data);
|
||||
|
||||
/* team chat */
|
||||
/**
|
||||
* Get the app that can be used with this token
|
||||
*/
|
||||
export const getMyTokensApps = (data: AuthTeamTagTokenProps) =>
|
||||
GET<AppListItemType[]>(`/proApi/support/user/team/tag/getAppsByTeamTokens`, data);
|
||||
|
||||
/**
|
||||
* 获取团队分享的对话列表 initTeamChat
|
||||
* @param data
|
||||
* @returns
|
||||
*/
|
||||
export const getinitTeamChat = (data: { teamId: string; authToken: string; appId: string }) =>
|
||||
GET(`/proApi/core/chat/initTeamChat`, data);
|
||||
|
||||
export const getQuoteDataList = (data: GetQuoteProps) =>
|
||||
POST<GetQuotesRes>(`/core/chat/quote/getQuote`, data);
|
||||
|
||||
|
|
|
|||
|
|
@ -7,19 +7,15 @@ import {
|
|||
delChatHistoryById,
|
||||
putChatHistory,
|
||||
getChatHistories
|
||||
} from '../api';
|
||||
} from '../history/api';
|
||||
import { type ChatHistoryItemType } from '@fastgpt/global/core/chat/type';
|
||||
import { type UpdateHistoryProps } from '@/global/core/chat/api';
|
||||
import { type BoxProps, useDisclosure } from '@chakra-ui/react';
|
||||
import { useChatStore } from './useChatStore';
|
||||
import { getNanoid } from '@fastgpt/global/common/string/tools';
|
||||
import { useScrollPagination } from '@fastgpt/web/hooks/useScrollPagination';
|
||||
import type { UpdateHistoryBodyType } from '@fastgpt/global/openapi/core/chat/history/api';
|
||||
|
||||
type UpdateHistoryParams = {
|
||||
chatId: UpdateHistoryProps['chatId'];
|
||||
customTitle?: UpdateHistoryProps['customTitle'];
|
||||
top?: UpdateHistoryProps['top'];
|
||||
};
|
||||
type UpdateHistoryParams = Pick<UpdateHistoryBodyType, 'chatId' | 'customTitle' | 'top'>;
|
||||
|
||||
type ChatContextValueType = {
|
||||
params: Record<string, string | number | boolean>;
|
||||
|
|
|
|||
|
|
@ -0,0 +1,28 @@
|
|||
import { POST, PUT, DELETE } from '@/web/common/api/request';
|
||||
import type {
|
||||
ChatBatchDeleteBodyType,
|
||||
DelChatHistoryType,
|
||||
ClearChatHistoriesType,
|
||||
GetHistoriesBodyType,
|
||||
GetHistoriesResponseType,
|
||||
UpdateHistoryBodyType
|
||||
} from '@fastgpt/global/openapi/core/chat/history/api';
|
||||
|
||||
export const getChatHistories = (data: GetHistoriesBodyType) =>
|
||||
POST<GetHistoriesResponseType>('/core/chat/history/getHistories', data);
|
||||
|
||||
// 修改历史记录: 标题/置顶
|
||||
export const putChatHistory = (data: UpdateHistoryBodyType) =>
|
||||
PUT('/core/chat/history/updateHistory', data);
|
||||
|
||||
// delete one history (soft delete)
|
||||
export const delChatHistoryById = (data: DelChatHistoryType) =>
|
||||
DELETE(`/core/chat/history/delHistory`, data);
|
||||
|
||||
// clear all history by appId
|
||||
export const delClearChatHistories = (data: ClearChatHistoriesType) =>
|
||||
DELETE(`/core/chat/history/clearHistories`, data);
|
||||
|
||||
// Log manger
|
||||
export const batchDeleteChatHistories = (data: ChatBatchDeleteBodyType) =>
|
||||
POST<void>(`/core/chat/history/batchDelete`, data);
|
||||
|
|
@ -0,0 +1,348 @@
|
|||
import handler from '@/pages/api/core/chat/history/batchDelete';
|
||||
import type { ChatBatchDeleteBodyType } from '@fastgpt/global/openapi/core/chat/history/api';
|
||||
import { AppTypeEnum } from '@fastgpt/global/core/app/constants';
|
||||
import { ChatRoleEnum, ChatSourceEnum } from '@fastgpt/global/core/chat/constants';
|
||||
import { getNanoid } from '@fastgpt/global/common/string/tools';
|
||||
import { MongoApp } from '@fastgpt/service/core/app/schema';
|
||||
import { MongoChat } from '@fastgpt/service/core/chat/chatSchema';
|
||||
import { MongoChatItem } from '@fastgpt/service/core/chat/chatItemSchema';
|
||||
import { MongoChatItemResponse } from '@fastgpt/service/core/chat/chatItemResponseSchema';
|
||||
import { getUser } from '@test/datas/users';
|
||||
import { Call } from '@test/utils/request';
|
||||
import { describe, expect, it, beforeEach } from 'vitest';
|
||||
import { MongoResourcePermission } from '@fastgpt/service/support/permission/schema';
|
||||
import { AppReadChatLogPerVal } from '@fastgpt/global/support/permission/app/constant';
|
||||
import { PerResourceTypeEnum } from '@fastgpt/global/support/permission/constant';
|
||||
|
||||
describe('batchDelete api test', () => {
|
||||
let testUser: Awaited<ReturnType<typeof getUser>>;
|
||||
let appId: string;
|
||||
let chatIds: string[];
|
||||
|
||||
beforeEach(async () => {
|
||||
testUser = await getUser('test-user-batch-delete');
|
||||
|
||||
// Create test app
|
||||
const app = await MongoApp.create({
|
||||
name: 'Test App',
|
||||
type: AppTypeEnum.simple,
|
||||
teamId: testUser.teamId,
|
||||
tmbId: testUser.tmbId,
|
||||
modules: []
|
||||
});
|
||||
appId = String(app._id);
|
||||
// Create log permission
|
||||
await MongoResourcePermission.create({
|
||||
teamId: testUser.teamId,
|
||||
tmbId: testUser.tmbId,
|
||||
resourceId: appId,
|
||||
permission: AppReadChatLogPerVal,
|
||||
resourceType: PerResourceTypeEnum.app
|
||||
});
|
||||
|
||||
// Create multiple chats
|
||||
chatIds = [getNanoid(), getNanoid(), getNanoid()];
|
||||
|
||||
await Promise.all(
|
||||
chatIds.map((chatId) =>
|
||||
MongoChat.create({
|
||||
teamId: testUser.teamId,
|
||||
tmbId: testUser.tmbId,
|
||||
appId,
|
||||
chatId,
|
||||
source: ChatSourceEnum.test,
|
||||
title: `Test Chat ${chatId}`
|
||||
})
|
||||
)
|
||||
);
|
||||
|
||||
// Create chat items for each chat
|
||||
await Promise.all(
|
||||
chatIds.map((chatId) =>
|
||||
MongoChatItem.create({
|
||||
teamId: testUser.teamId,
|
||||
tmbId: testUser.tmbId,
|
||||
userId: testUser.userId,
|
||||
appId,
|
||||
chatId,
|
||||
dataId: getNanoid(),
|
||||
obj: ChatRoleEnum.AI,
|
||||
value: [
|
||||
{
|
||||
type: 'text',
|
||||
text: {
|
||||
content: `Response for ${chatId}`
|
||||
}
|
||||
}
|
||||
]
|
||||
})
|
||||
)
|
||||
);
|
||||
|
||||
// Create chat item responses for each chat
|
||||
await Promise.all(
|
||||
chatIds.map((chatId) =>
|
||||
MongoChatItemResponse.create({
|
||||
teamId: testUser.teamId,
|
||||
tmbId: testUser.tmbId,
|
||||
appId,
|
||||
chatId,
|
||||
dataId: getNanoid(),
|
||||
text: `Response text for ${chatId}`
|
||||
})
|
||||
)
|
||||
);
|
||||
});
|
||||
|
||||
it('should batch delete multiple chats successfully', async () => {
|
||||
const deleteIds = [chatIds[0], chatIds[1]];
|
||||
|
||||
const res = await Call<ChatBatchDeleteBodyType, {}>(handler, {
|
||||
auth: testUser,
|
||||
body: {
|
||||
appId,
|
||||
chatIds: deleteIds
|
||||
}
|
||||
});
|
||||
|
||||
expect(res.code).toBe(200);
|
||||
expect(res.error).toBeUndefined();
|
||||
|
||||
// Verify that chats were deleted
|
||||
const remainingChats = await MongoChat.find({
|
||||
appId,
|
||||
chatId: { $in: deleteIds }
|
||||
});
|
||||
expect(remainingChats).toHaveLength(0);
|
||||
|
||||
// Verify that chat items were deleted
|
||||
const remainingChatItems = await MongoChatItem.find({
|
||||
appId,
|
||||
chatId: { $in: deleteIds }
|
||||
});
|
||||
expect(remainingChatItems).toHaveLength(0);
|
||||
|
||||
// Verify that chat item responses were deleted
|
||||
const remainingChatItemResponses = await MongoChatItemResponse.find({
|
||||
appId,
|
||||
chatId: { $in: deleteIds }
|
||||
});
|
||||
expect(remainingChatItemResponses).toHaveLength(0);
|
||||
|
||||
// Verify that non-deleted chat still exists
|
||||
const nonDeletedChat = await MongoChat.findOne({
|
||||
appId,
|
||||
chatId: chatIds[2]
|
||||
});
|
||||
expect(nonDeletedChat).toBeDefined();
|
||||
});
|
||||
|
||||
it('should delete single chat', async () => {
|
||||
const deleteIds = [chatIds[0]];
|
||||
|
||||
const res = await Call<ChatBatchDeleteBodyType, {}>(handler, {
|
||||
auth: testUser,
|
||||
body: {
|
||||
appId,
|
||||
chatIds: deleteIds
|
||||
}
|
||||
});
|
||||
|
||||
expect(res.code).toBe(200);
|
||||
expect(res.error).toBeUndefined();
|
||||
|
||||
// Verify that chat was deleted
|
||||
const deletedChat = await MongoChat.findOne({
|
||||
appId,
|
||||
chatId: chatIds[0]
|
||||
});
|
||||
expect(deletedChat).toBeNull();
|
||||
|
||||
// Verify that other chats still exist
|
||||
const remainingChats = await MongoChat.find({
|
||||
appId,
|
||||
chatId: { $in: [chatIds[1], chatIds[2]] }
|
||||
});
|
||||
expect(remainingChats).toHaveLength(2);
|
||||
});
|
||||
|
||||
it('should delete all chats when all chatIds are provided', async () => {
|
||||
const res = await Call<ChatBatchDeleteBodyType, {}>(handler, {
|
||||
auth: testUser,
|
||||
body: {
|
||||
appId,
|
||||
chatIds
|
||||
}
|
||||
});
|
||||
|
||||
expect(res.code).toBe(200);
|
||||
expect(res.error).toBeUndefined();
|
||||
|
||||
// Verify that all chats were deleted
|
||||
const remainingChats = await MongoChat.find({
|
||||
appId,
|
||||
chatId: { $in: chatIds }
|
||||
});
|
||||
expect(remainingChats).toHaveLength(0);
|
||||
});
|
||||
|
||||
it('should fail when chatIds is empty array', async () => {
|
||||
const res = await Call<ChatBatchDeleteBodyType, {}>(handler, {
|
||||
auth: testUser,
|
||||
body: {
|
||||
appId,
|
||||
chatIds: []
|
||||
}
|
||||
});
|
||||
|
||||
expect(res.code).toBe(500);
|
||||
expect(res.error).toBeDefined();
|
||||
});
|
||||
|
||||
it('should fail when chatIds is not an array', async () => {
|
||||
const res = await Call<any, {}, any>(handler, {
|
||||
auth: testUser,
|
||||
body: {
|
||||
appId,
|
||||
chatIds: 'not-an-array'
|
||||
}
|
||||
});
|
||||
|
||||
expect(res.code).toBe(500);
|
||||
expect(res.error).toBeDefined();
|
||||
});
|
||||
|
||||
it('should fail when appId is missing', async () => {
|
||||
const res = await Call<ChatBatchDeleteBodyType, {}>(handler, {
|
||||
auth: testUser,
|
||||
body: {
|
||||
appId: '',
|
||||
chatIds: [chatIds[0]]
|
||||
}
|
||||
});
|
||||
|
||||
expect(res.code).toBe(500);
|
||||
expect(res.error).toBeDefined();
|
||||
});
|
||||
|
||||
it('should fail when user does not have permission', async () => {
|
||||
const unauthorizedUser = await getUser('unauthorized-user-batch-delete');
|
||||
|
||||
const res = await Call<ChatBatchDeleteBodyType, {}>(handler, {
|
||||
auth: unauthorizedUser,
|
||||
body: {
|
||||
appId,
|
||||
chatIds: [chatIds[0]]
|
||||
}
|
||||
});
|
||||
|
||||
expect(res.code).toBe(500);
|
||||
expect(res.error).toBeDefined();
|
||||
});
|
||||
|
||||
it('should succeed even when some chatIds do not exist', async () => {
|
||||
const nonExistentChatId = getNanoid();
|
||||
const deleteIds = [chatIds[0], nonExistentChatId];
|
||||
|
||||
const res = await Call<ChatBatchDeleteBodyType, {}>(handler, {
|
||||
auth: testUser,
|
||||
body: {
|
||||
appId,
|
||||
chatIds: deleteIds
|
||||
}
|
||||
});
|
||||
|
||||
expect(res.code).toBe(200);
|
||||
expect(res.error).toBeUndefined();
|
||||
|
||||
// Verify that existing chat was deleted
|
||||
const deletedChat = await MongoChat.findOne({
|
||||
appId,
|
||||
chatId: chatIds[0]
|
||||
});
|
||||
expect(deletedChat).toBeNull();
|
||||
});
|
||||
|
||||
it('should only delete chats belonging to the specified app', async () => {
|
||||
// Create another app with a chat
|
||||
const otherApp = await MongoApp.create({
|
||||
name: 'Other App',
|
||||
type: AppTypeEnum.simple,
|
||||
teamId: testUser.teamId,
|
||||
tmbId: testUser.tmbId,
|
||||
modules: []
|
||||
});
|
||||
const otherAppId = String(otherApp._id);
|
||||
const otherChatId = getNanoid();
|
||||
|
||||
await MongoChat.create({
|
||||
teamId: testUser.teamId,
|
||||
tmbId: testUser.tmbId,
|
||||
appId: otherAppId,
|
||||
chatId: otherChatId,
|
||||
source: ChatSourceEnum.test,
|
||||
title: 'Other App Chat'
|
||||
});
|
||||
|
||||
// Try to delete a chat from the first app
|
||||
const res = await Call<ChatBatchDeleteBodyType, {}>(handler, {
|
||||
auth: testUser,
|
||||
body: {
|
||||
appId,
|
||||
chatIds: [chatIds[0], otherChatId]
|
||||
}
|
||||
});
|
||||
|
||||
expect(res.code).toBe(200);
|
||||
|
||||
// Verify that only the chat from the specified app was deleted
|
||||
const deletedChat = await MongoChat.findOne({
|
||||
appId,
|
||||
chatId: chatIds[0]
|
||||
});
|
||||
expect(deletedChat).toBeNull();
|
||||
|
||||
// Verify that the other app's chat still exists
|
||||
const otherChat = await MongoChat.findOne({
|
||||
appId: otherAppId,
|
||||
chatId: otherChatId
|
||||
});
|
||||
expect(otherChat).toBeDefined();
|
||||
});
|
||||
|
||||
it('should handle large batch of chatIds', async () => {
|
||||
// Create 20 more chats
|
||||
const largeBatch = Array.from({ length: 20 }, () => getNanoid());
|
||||
|
||||
await Promise.all(
|
||||
largeBatch.map((chatId) =>
|
||||
MongoChat.create({
|
||||
teamId: testUser.teamId,
|
||||
tmbId: testUser.tmbId,
|
||||
appId,
|
||||
chatId,
|
||||
source: ChatSourceEnum.test,
|
||||
title: `Test Chat ${chatId}`
|
||||
})
|
||||
)
|
||||
);
|
||||
|
||||
const res = await Call<ChatBatchDeleteBodyType, {}>(handler, {
|
||||
auth: testUser,
|
||||
body: {
|
||||
appId,
|
||||
chatIds: largeBatch
|
||||
}
|
||||
});
|
||||
|
||||
expect(res.code).toBe(200);
|
||||
expect(res.error).toBeUndefined();
|
||||
|
||||
// Verify that all chats were deleted
|
||||
const remainingChats = await MongoChat.find({
|
||||
appId,
|
||||
chatId: { $in: largeBatch }
|
||||
});
|
||||
expect(remainingChats).toHaveLength(0);
|
||||
});
|
||||
});
|
||||
|
|
@ -0,0 +1,222 @@
|
|||
import handler from '@/pages/api/core/chat/history/clearHistories';
|
||||
import { AppTypeEnum } from '@fastgpt/global/core/app/constants';
|
||||
import { ChatSourceEnum } from '@fastgpt/global/core/chat/constants';
|
||||
import { getNanoid } from '@fastgpt/global/common/string/tools';
|
||||
import { MongoApp } from '@fastgpt/service/core/app/schema';
|
||||
import { MongoChat } from '@fastgpt/service/core/chat/chatSchema';
|
||||
import { getUser } from '@test/datas/users';
|
||||
import { Call } from '@test/utils/request';
|
||||
import { describe, expect, it, beforeEach } from 'vitest';
|
||||
import { MongoResourcePermission } from '@fastgpt/service/support/permission/schema';
|
||||
import { AppReadChatLogPerVal } from '@fastgpt/global/support/permission/app/constant';
|
||||
import { PerResourceTypeEnum } from '@fastgpt/global/support/permission/constant';
|
||||
|
||||
describe('clearHistories api test', () => {
|
||||
let testUser: Awaited<ReturnType<typeof getUser>>;
|
||||
let appId: string;
|
||||
let chatIds: string[];
|
||||
|
||||
beforeEach(async () => {
|
||||
testUser = await getUser('test-user-clear-histories');
|
||||
|
||||
// Create test app
|
||||
const app = await MongoApp.create({
|
||||
name: 'Test App',
|
||||
type: AppTypeEnum.simple,
|
||||
teamId: testUser.teamId,
|
||||
tmbId: testUser.tmbId,
|
||||
modules: []
|
||||
});
|
||||
appId = String(app._id);
|
||||
// Create log permission
|
||||
await MongoResourcePermission.create({
|
||||
teamId: testUser.teamId,
|
||||
tmbId: testUser.tmbId,
|
||||
resourceId: appId,
|
||||
permission: AppReadChatLogPerVal,
|
||||
resourceType: PerResourceTypeEnum.app
|
||||
});
|
||||
|
||||
// Create multiple chats
|
||||
chatIds = [getNanoid(), getNanoid(), getNanoid()];
|
||||
|
||||
await Promise.all(
|
||||
chatIds.map((chatId) =>
|
||||
MongoChat.create({
|
||||
teamId: testUser.teamId,
|
||||
tmbId: testUser.tmbId,
|
||||
appId,
|
||||
chatId,
|
||||
source: ChatSourceEnum.online
|
||||
})
|
||||
)
|
||||
);
|
||||
});
|
||||
|
||||
it('should clear all chat histories with token auth successfully', async () => {
|
||||
const res = await Call<any, any, any>(handler, {
|
||||
auth: testUser,
|
||||
query: {
|
||||
appId
|
||||
}
|
||||
});
|
||||
|
||||
expect(res.code).toBe(200);
|
||||
expect(res.error).toBeUndefined();
|
||||
|
||||
// Verify that all chats were soft deleted
|
||||
const chats = await MongoChat.find(
|
||||
{
|
||||
appId,
|
||||
chatId: { $in: chatIds }
|
||||
},
|
||||
{ deleteTime: 1 }
|
||||
);
|
||||
|
||||
chats.forEach((chat) => {
|
||||
expect(chat.deleteTime).toBeDefined();
|
||||
expect(chat.deleteTime).toBeInstanceOf(Date);
|
||||
});
|
||||
});
|
||||
|
||||
it('should only clear chats for the specific user', async () => {
|
||||
// Create another user's chat
|
||||
const otherUser = await getUser('other-user-clear-histories');
|
||||
const otherChatId = getNanoid();
|
||||
|
||||
await MongoChat.create({
|
||||
teamId: otherUser.teamId,
|
||||
tmbId: otherUser.tmbId,
|
||||
appId,
|
||||
chatId: otherChatId,
|
||||
source: ChatSourceEnum.online
|
||||
});
|
||||
|
||||
// Clear current user's chats
|
||||
const res = await Call<any, any, any>(handler, {
|
||||
auth: testUser,
|
||||
query: {
|
||||
appId
|
||||
}
|
||||
});
|
||||
|
||||
expect(res.code).toBe(200);
|
||||
|
||||
// Verify that current user's chats were deleted
|
||||
const userChats = await MongoChat.find(
|
||||
{
|
||||
appId,
|
||||
chatId: { $in: chatIds }
|
||||
},
|
||||
{ deleteTime: 1 }
|
||||
);
|
||||
|
||||
userChats.forEach((chat) => {
|
||||
expect(chat.deleteTime).toBeDefined();
|
||||
});
|
||||
|
||||
// Verify that other user's chat was NOT deleted
|
||||
const otherChat = await MongoChat.findOne(
|
||||
{
|
||||
appId,
|
||||
chatId: otherChatId
|
||||
},
|
||||
{ deleteTime: 1 }
|
||||
);
|
||||
|
||||
expect(otherChat?.deleteTime).toBeNull();
|
||||
});
|
||||
|
||||
it('should filter by source when clearing API chats', async () => {
|
||||
// Create API source chat
|
||||
const apiChatId = getNanoid();
|
||||
await MongoChat.create({
|
||||
teamId: testUser.teamId,
|
||||
tmbId: testUser.tmbId,
|
||||
appId,
|
||||
chatId: apiChatId,
|
||||
source: ChatSourceEnum.api
|
||||
});
|
||||
|
||||
// Clear with API key (simulated by authType in query)
|
||||
const res = await Call<any, any, any>(handler, {
|
||||
auth: { ...testUser, authType: 'apikey' },
|
||||
query: {
|
||||
appId
|
||||
}
|
||||
});
|
||||
|
||||
expect(res.code).toBe(200);
|
||||
|
||||
// Verify API chat was cleared
|
||||
const apiChat = await MongoChat.findOne(
|
||||
{
|
||||
appId,
|
||||
chatId: apiChatId
|
||||
},
|
||||
{ deleteTime: 1 }
|
||||
);
|
||||
expect(apiChat?.deleteTime).toBeDefined();
|
||||
|
||||
// Online chats should not be affected (different source)
|
||||
const onlineChats = await MongoChat.find(
|
||||
{
|
||||
appId,
|
||||
chatId: { $in: chatIds }
|
||||
},
|
||||
{ deleteTime: 1 }
|
||||
);
|
||||
|
||||
// Since we're using apikey auth, it will clear chats with api source
|
||||
// The online chats should not be affected
|
||||
onlineChats.forEach((chat) => {
|
||||
expect(chat.deleteTime).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
it('should fail when appId is missing', async () => {
|
||||
const res = await Call<any, any, any>(handler, {
|
||||
auth: testUser,
|
||||
query: {}
|
||||
});
|
||||
|
||||
expect(res.code).toBe(500);
|
||||
expect(res.error).toBeDefined();
|
||||
});
|
||||
|
||||
it('should fail when user does not have permission', async () => {
|
||||
const unauthorizedUser = await getUser('unauthorized-user-clear-histories');
|
||||
|
||||
const res = await Call<any, any, any>(handler, {
|
||||
auth: unauthorizedUser,
|
||||
query: {
|
||||
appId
|
||||
}
|
||||
});
|
||||
|
||||
expect(res.code).toBe(500);
|
||||
expect(res.error).toBeDefined();
|
||||
});
|
||||
|
||||
it('should succeed even when there are no chats to clear', async () => {
|
||||
// Create a new app with no chats
|
||||
const newApp = await MongoApp.create({
|
||||
name: 'Empty App',
|
||||
type: AppTypeEnum.simple,
|
||||
teamId: testUser.teamId,
|
||||
tmbId: testUser.tmbId,
|
||||
modules: []
|
||||
});
|
||||
const emptyAppId = String(newApp._id);
|
||||
|
||||
const res = await Call<any, any, any>(handler, {
|
||||
auth: testUser,
|
||||
query: {
|
||||
appId: emptyAppId
|
||||
}
|
||||
});
|
||||
|
||||
expect(res.code).toBe(200);
|
||||
expect(res.error).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
|
@ -0,0 +1,143 @@
|
|||
import handler from '@/pages/api/core/chat/history/delHistory';
|
||||
import { AppTypeEnum } from '@fastgpt/global/core/app/constants';
|
||||
import { ChatSourceEnum } from '@fastgpt/global/core/chat/constants';
|
||||
import { getNanoid } from '@fastgpt/global/common/string/tools';
|
||||
import { MongoApp } from '@fastgpt/service/core/app/schema';
|
||||
import { MongoChat } from '@fastgpt/service/core/chat/chatSchema';
|
||||
import { getUser } from '@test/datas/users';
|
||||
import { Call } from '@test/utils/request';
|
||||
import { describe, expect, it, beforeEach } from 'vitest';
|
||||
import { MongoResourcePermission } from '@fastgpt/service/support/permission/schema';
|
||||
import { AppReadChatLogPerVal } from '@fastgpt/global/support/permission/app/constant';
|
||||
import { PerResourceTypeEnum } from '@fastgpt/global/support/permission/constant';
|
||||
|
||||
describe('delHistory api test', () => {
|
||||
let testUser: Awaited<ReturnType<typeof getUser>>;
|
||||
let appId: string;
|
||||
let chatId: string;
|
||||
|
||||
beforeEach(async () => {
|
||||
testUser = await getUser('test-user-del-history');
|
||||
|
||||
// Create test app
|
||||
const app = await MongoApp.create({
|
||||
name: 'Test App',
|
||||
type: AppTypeEnum.simple,
|
||||
teamId: testUser.teamId,
|
||||
tmbId: testUser.tmbId,
|
||||
modules: []
|
||||
});
|
||||
appId = String(app._id);
|
||||
// Create log permission
|
||||
await MongoResourcePermission.create({
|
||||
teamId: testUser.teamId,
|
||||
tmbId: testUser.tmbId,
|
||||
resourceId: appId,
|
||||
permission: AppReadChatLogPerVal,
|
||||
resourceType: PerResourceTypeEnum.app
|
||||
});
|
||||
|
||||
chatId = getNanoid();
|
||||
|
||||
// Create chat
|
||||
await MongoChat.create({
|
||||
teamId: testUser.teamId,
|
||||
tmbId: testUser.tmbId,
|
||||
appId,
|
||||
chatId,
|
||||
source: ChatSourceEnum.test
|
||||
});
|
||||
});
|
||||
|
||||
it('should soft delete chat history successfully', async () => {
|
||||
// Verify chat exists before deletion
|
||||
const chatBefore = await MongoChat.findOne(
|
||||
{
|
||||
appId,
|
||||
chatId
|
||||
},
|
||||
{
|
||||
deleteTime: 1
|
||||
}
|
||||
).lean();
|
||||
expect(chatBefore).toBeDefined();
|
||||
expect(chatBefore?.deleteTime).toBeNull();
|
||||
|
||||
const res = await Call<any, { appId: string; chatId: string }, any>(handler, {
|
||||
auth: testUser,
|
||||
query: {
|
||||
appId,
|
||||
chatId
|
||||
}
|
||||
});
|
||||
|
||||
expect(res.code).toBe(200);
|
||||
expect(res.error).toBeUndefined();
|
||||
|
||||
// Verify that chat was soft deleted (deleteTime is set)
|
||||
const deletedChat = await MongoChat.findOne(
|
||||
{
|
||||
appId,
|
||||
chatId
|
||||
},
|
||||
{ deleteTime: 1 }
|
||||
).lean();
|
||||
|
||||
expect(deletedChat).toBeDefined();
|
||||
expect(deletedChat?.deleteTime).not.toBeNull();
|
||||
});
|
||||
|
||||
it('should fail when chatId is missing', async () => {
|
||||
const res = await Call<any, { appId: string; chatId?: string }, any>(handler, {
|
||||
auth: testUser,
|
||||
query: {
|
||||
appId
|
||||
}
|
||||
});
|
||||
|
||||
expect(res.code).toBe(500);
|
||||
expect(res.error).toBeDefined();
|
||||
});
|
||||
|
||||
it('should fail when appId is missing', async () => {
|
||||
const res = await Call<any, { appId?: string; chatId: string }, any>(handler, {
|
||||
auth: testUser,
|
||||
query: {
|
||||
chatId
|
||||
}
|
||||
});
|
||||
|
||||
expect(res.code).toBe(500);
|
||||
expect(res.error).toBeDefined();
|
||||
});
|
||||
|
||||
it('should fail when user does not have permission', async () => {
|
||||
const unauthorizedUser = await getUser('unauthorized-user-del-history');
|
||||
|
||||
const res = await Call<any, { appId: string; chatId: string }, any>(handler, {
|
||||
auth: unauthorizedUser,
|
||||
query: {
|
||||
appId,
|
||||
chatId
|
||||
}
|
||||
});
|
||||
|
||||
expect(res.code).toBe(500);
|
||||
expect(res.error).toBeDefined();
|
||||
});
|
||||
|
||||
it('should succeed even if chat does not exist', async () => {
|
||||
const nonExistentChatId = getNanoid();
|
||||
|
||||
const res = await Call<any, { appId: string; chatId: string }, any>(handler, {
|
||||
auth: testUser,
|
||||
query: {
|
||||
appId,
|
||||
chatId: nonExistentChatId
|
||||
}
|
||||
});
|
||||
|
||||
expect(res.code).toBe(200);
|
||||
expect(res.error).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
|
@ -0,0 +1,344 @@
|
|||
import handler from '@/pages/api/core/chat/history/getHistories';
|
||||
import type {
|
||||
GetHistoriesBodyType,
|
||||
GetHistoriesResponseType
|
||||
} from '@fastgpt/global/openapi/core/chat/history/api';
|
||||
import { AppTypeEnum } from '@fastgpt/global/core/app/constants';
|
||||
import { ChatSourceEnum } from '@fastgpt/global/core/chat/constants';
|
||||
import { getNanoid } from '@fastgpt/global/common/string/tools';
|
||||
import { MongoApp } from '@fastgpt/service/core/app/schema';
|
||||
import { MongoChat } from '@fastgpt/service/core/chat/chatSchema';
|
||||
import { getUser } from '@test/datas/users';
|
||||
import { Call } from '@test/utils/request';
|
||||
import { describe, expect, it, beforeEach } from 'vitest';
|
||||
import { MongoResourcePermission } from '@fastgpt/service/support/permission/schema';
|
||||
import { AppReadChatLogPerVal } from '@fastgpt/global/support/permission/app/constant';
|
||||
import { PerResourceTypeEnum } from '@fastgpt/global/support/permission/constant';
|
||||
|
||||
describe('getHistories api test', () => {
|
||||
let testUser: Awaited<ReturnType<typeof getUser>>;
|
||||
let appId: string;
|
||||
let chatIds: string[];
|
||||
|
||||
beforeEach(async () => {
|
||||
testUser = await getUser('test-user-get-histories');
|
||||
|
||||
// Create test app
|
||||
const app = await MongoApp.create({
|
||||
name: 'Test App',
|
||||
type: AppTypeEnum.simple,
|
||||
teamId: testUser.teamId,
|
||||
tmbId: testUser.tmbId,
|
||||
modules: []
|
||||
});
|
||||
appId = String(app._id);
|
||||
// Create log permission
|
||||
await MongoResourcePermission.create({
|
||||
teamId: testUser.teamId,
|
||||
tmbId: testUser.tmbId,
|
||||
resourceId: appId,
|
||||
permission: AppReadChatLogPerVal,
|
||||
resourceType: PerResourceTypeEnum.app
|
||||
});
|
||||
// Create multiple chats with different attributes
|
||||
chatIds = [getNanoid(), getNanoid(), getNanoid()];
|
||||
|
||||
await Promise.all([
|
||||
MongoChat.create({
|
||||
teamId: testUser.teamId,
|
||||
tmbId: testUser.tmbId,
|
||||
appId,
|
||||
chatId: chatIds[0],
|
||||
source: ChatSourceEnum.online,
|
||||
title: 'Chat 1',
|
||||
top: true
|
||||
}),
|
||||
MongoChat.create({
|
||||
teamId: testUser.teamId,
|
||||
tmbId: testUser.tmbId,
|
||||
appId,
|
||||
chatId: chatIds[1],
|
||||
source: ChatSourceEnum.online,
|
||||
title: 'Chat 2',
|
||||
customTitle: 'Custom Chat 2'
|
||||
}),
|
||||
MongoChat.create({
|
||||
teamId: testUser.teamId,
|
||||
tmbId: testUser.tmbId,
|
||||
appId,
|
||||
chatId: chatIds[2],
|
||||
source: ChatSourceEnum.online,
|
||||
title: 'Chat 3'
|
||||
})
|
||||
]);
|
||||
});
|
||||
|
||||
it('should get chat histories successfully', async () => {
|
||||
const res = await Call<GetHistoriesBodyType, any, GetHistoriesResponseType>(handler, {
|
||||
auth: testUser,
|
||||
body: {
|
||||
appId
|
||||
},
|
||||
query: {
|
||||
offset: 0,
|
||||
pageSize: 10
|
||||
}
|
||||
});
|
||||
|
||||
expect(res.code).toBe(200);
|
||||
expect(res.error).toBeUndefined();
|
||||
expect(res.data).toBeDefined();
|
||||
expect(res.data.list).toHaveLength(3);
|
||||
expect(res.data.total).toBe(3);
|
||||
|
||||
// Verify top chat is first
|
||||
expect(res.data.list[0].top).toBe(true);
|
||||
});
|
||||
|
||||
it('should return paginated results', async () => {
|
||||
const res = await Call<GetHistoriesBodyType, any, GetHistoriesResponseType>(handler, {
|
||||
auth: testUser,
|
||||
body: {
|
||||
appId
|
||||
},
|
||||
query: {
|
||||
offset: 0,
|
||||
pageSize: 2
|
||||
}
|
||||
});
|
||||
|
||||
expect(res.code).toBe(200);
|
||||
expect(res.data.list).toHaveLength(2);
|
||||
expect(res.data.total).toBe(3);
|
||||
|
||||
// Get second page
|
||||
const res2 = await Call<GetHistoriesBodyType, any, GetHistoriesResponseType>(handler, {
|
||||
auth: testUser,
|
||||
body: {
|
||||
appId
|
||||
},
|
||||
query: {
|
||||
offset: 2,
|
||||
pageSize: 2
|
||||
}
|
||||
});
|
||||
|
||||
expect(res2.code).toBe(200);
|
||||
expect(res2.data.list).toHaveLength(1);
|
||||
expect(res2.data.total).toBe(3);
|
||||
});
|
||||
|
||||
it('should filter by source', async () => {
|
||||
// Create API source chat
|
||||
const apiChatId = getNanoid();
|
||||
await MongoChat.create({
|
||||
teamId: testUser.teamId,
|
||||
tmbId: testUser.tmbId,
|
||||
appId,
|
||||
chatId: apiChatId,
|
||||
source: ChatSourceEnum.api,
|
||||
title: 'API Chat'
|
||||
});
|
||||
|
||||
const res = await Call<GetHistoriesBodyType, any, GetHistoriesResponseType>(handler, {
|
||||
auth: testUser,
|
||||
body: {
|
||||
appId,
|
||||
source: ChatSourceEnum.api
|
||||
},
|
||||
query: {
|
||||
offset: 0,
|
||||
pageSize: 10
|
||||
}
|
||||
});
|
||||
|
||||
expect(res.code).toBe(200);
|
||||
expect(res.data.list).toHaveLength(1);
|
||||
expect(res.data.list[0].chatId).toBe(apiChatId);
|
||||
});
|
||||
|
||||
it('should filter by createTime range', async () => {
|
||||
const now = new Date();
|
||||
const yesterday = new Date(now.getTime() - 24 * 60 * 60 * 1000);
|
||||
const tomorrow = new Date(now.getTime() + 24 * 60 * 60 * 1000);
|
||||
|
||||
const res = await Call<GetHistoriesBodyType, any, GetHistoriesResponseType>(handler, {
|
||||
auth: testUser,
|
||||
body: {
|
||||
appId,
|
||||
startCreateTime: yesterday.toISOString(),
|
||||
endCreateTime: tomorrow.toISOString()
|
||||
},
|
||||
query: {
|
||||
offset: 0,
|
||||
pageSize: 10
|
||||
}
|
||||
});
|
||||
|
||||
expect(res.code).toBe(200);
|
||||
expect(res.data.list).toHaveLength(3);
|
||||
});
|
||||
|
||||
it('should filter by updateTime range', async () => {
|
||||
const now = new Date();
|
||||
const yesterday = new Date(now.getTime() - 24 * 60 * 60 * 1000);
|
||||
const tomorrow = new Date(now.getTime() + 24 * 60 * 60 * 1000);
|
||||
|
||||
const res = await Call<GetHistoriesBodyType, any, GetHistoriesResponseType>(handler, {
|
||||
auth: testUser,
|
||||
body: {
|
||||
appId,
|
||||
startUpdateTime: yesterday.toISOString(),
|
||||
endUpdateTime: tomorrow.toISOString()
|
||||
},
|
||||
query: {
|
||||
offset: 0,
|
||||
pageSize: 10
|
||||
}
|
||||
});
|
||||
|
||||
expect(res.code).toBe(200);
|
||||
expect(res.data.list).toHaveLength(3);
|
||||
});
|
||||
|
||||
it('should exclude soft-deleted chats', async () => {
|
||||
// Soft delete one chat
|
||||
await MongoChat.updateOne({ appId, chatId: chatIds[0] }, { deleteTime: new Date() });
|
||||
|
||||
const res = await Call<GetHistoriesBodyType, any, GetHistoriesResponseType>(handler, {
|
||||
auth: testUser,
|
||||
body: {
|
||||
appId
|
||||
},
|
||||
query: {
|
||||
offset: 0,
|
||||
pageSize: 10
|
||||
}
|
||||
});
|
||||
|
||||
expect(res.code).toBe(200);
|
||||
expect(res.data.list).toHaveLength(2);
|
||||
expect(res.data.total).toBe(2);
|
||||
expect(res.data.list.find((chat) => chat.chatId === chatIds[0])).toBeUndefined();
|
||||
});
|
||||
|
||||
it('should only return chats for the specific user', async () => {
|
||||
// Create another user's chat
|
||||
const otherUser = await getUser('other-user-get-histories');
|
||||
const otherChatId = getNanoid();
|
||||
|
||||
// Create app for other user
|
||||
const otherApp = await MongoApp.create({
|
||||
name: 'Other App',
|
||||
type: AppTypeEnum.simple,
|
||||
teamId: otherUser.teamId,
|
||||
tmbId: otherUser.tmbId,
|
||||
modules: []
|
||||
});
|
||||
|
||||
await MongoChat.create({
|
||||
teamId: otherUser.teamId,
|
||||
tmbId: otherUser.tmbId,
|
||||
appId: String(otherApp._id),
|
||||
chatId: otherChatId,
|
||||
source: ChatSourceEnum.online,
|
||||
title: 'Other User Chat'
|
||||
});
|
||||
|
||||
// Get current user's chats
|
||||
const res = await Call<GetHistoriesBodyType, any, GetHistoriesResponseType>(handler, {
|
||||
auth: testUser,
|
||||
body: {
|
||||
appId
|
||||
},
|
||||
query: {
|
||||
offset: 0,
|
||||
pageSize: 10
|
||||
}
|
||||
});
|
||||
|
||||
expect(res.code).toBe(200);
|
||||
expect(res.data.list).toHaveLength(3);
|
||||
expect(res.data.list.find((chat) => chat.chatId === otherChatId)).toBeUndefined();
|
||||
});
|
||||
|
||||
it('should return empty list when appId does not exist', async () => {
|
||||
const nonExistentAppId = getNanoid(24);
|
||||
|
||||
const res = await Call<GetHistoriesBodyType, any, GetHistoriesResponseType>(handler, {
|
||||
auth: testUser,
|
||||
body: {
|
||||
appId: nonExistentAppId
|
||||
},
|
||||
query: {
|
||||
offset: 0,
|
||||
pageSize: 10
|
||||
}
|
||||
});
|
||||
|
||||
expect(res.code).toBe(500);
|
||||
expect(res.error).toBeDefined();
|
||||
expect(res.error?.name).toBe('ZodError');
|
||||
});
|
||||
|
||||
it('should fail when appId is missing', async () => {
|
||||
const res = await Call<GetHistoriesBodyType, any, GetHistoriesResponseType>(handler, {
|
||||
auth: testUser,
|
||||
body: {},
|
||||
query: {
|
||||
offset: 0,
|
||||
pageSize: 10
|
||||
}
|
||||
});
|
||||
|
||||
expect(res.code).toBe(200);
|
||||
expect(res.data.list).toHaveLength(0);
|
||||
});
|
||||
|
||||
it('should include all required fields in response', async () => {
|
||||
const res = await Call<GetHistoriesBodyType, any, GetHistoriesResponseType>(handler, {
|
||||
auth: testUser,
|
||||
body: {
|
||||
appId
|
||||
},
|
||||
query: {
|
||||
offset: 0,
|
||||
pageSize: 10
|
||||
}
|
||||
});
|
||||
|
||||
expect(res.code).toBe(200);
|
||||
|
||||
const firstChat = res.data.list[0];
|
||||
expect(firstChat).toHaveProperty('chatId');
|
||||
expect(firstChat).toHaveProperty('updateTime');
|
||||
expect(firstChat).toHaveProperty('appId');
|
||||
expect(firstChat).toHaveProperty('title');
|
||||
expect(firstChat.appId).toBe(appId);
|
||||
});
|
||||
|
||||
it('should order by top and updateTime', async () => {
|
||||
// Update one chat to be newer
|
||||
await new Promise((resolve) => setTimeout(resolve, 10));
|
||||
await MongoChat.updateOne({ appId, chatId: chatIds[2] }, { updateTime: new Date() });
|
||||
|
||||
const res = await Call<GetHistoriesBodyType, any, GetHistoriesResponseType>(handler, {
|
||||
auth: testUser,
|
||||
body: {
|
||||
appId
|
||||
},
|
||||
query: {
|
||||
offset: 0,
|
||||
pageSize: 10
|
||||
}
|
||||
});
|
||||
|
||||
expect(res.code).toBe(200);
|
||||
|
||||
// First should be the top chat
|
||||
expect(res.data.list[0].chatId).toBe(chatIds[0]);
|
||||
|
||||
// Second should be the most recently updated non-top chat
|
||||
expect(res.data.list[1].chatId).toBe(chatIds[2]);
|
||||
});
|
||||
});
|
||||
|
|
@ -0,0 +1,218 @@
|
|||
import handler from '@/pages/api/core/chat/history/updateHistory';
|
||||
import type { UpdateHistoryBodyType } from '@fastgpt/global/openapi/core/chat/history/api';
|
||||
import { AppTypeEnum } from '@fastgpt/global/core/app/constants';
|
||||
import { ChatSourceEnum } from '@fastgpt/global/core/chat/constants';
|
||||
import { getNanoid } from '@fastgpt/global/common/string/tools';
|
||||
import { MongoApp } from '@fastgpt/service/core/app/schema';
|
||||
import { MongoChat } from '@fastgpt/service/core/chat/chatSchema';
|
||||
import { getUser } from '@test/datas/users';
|
||||
import { Call } from '@test/utils/request';
|
||||
import { describe, expect, it, beforeEach } from 'vitest';
|
||||
import { MongoResourcePermission } from '@fastgpt/service/support/permission/schema';
|
||||
import { AppReadChatLogPerVal } from '@fastgpt/global/support/permission/app/constant';
|
||||
import { PerResourceTypeEnum } from '@fastgpt/global/support/permission/constant';
|
||||
|
||||
describe('updateHistory api test', () => {
|
||||
let testUser: Awaited<ReturnType<typeof getUser>>;
|
||||
let appId: string;
|
||||
let chatId: string;
|
||||
|
||||
beforeEach(async () => {
|
||||
testUser = await getUser('test-user-update-history');
|
||||
|
||||
// Create test app
|
||||
const app = await MongoApp.create({
|
||||
name: 'Test App',
|
||||
type: AppTypeEnum.simple,
|
||||
teamId: testUser.teamId,
|
||||
tmbId: testUser.tmbId,
|
||||
modules: []
|
||||
});
|
||||
appId = String(app._id);
|
||||
// Create log permission
|
||||
await MongoResourcePermission.create({
|
||||
teamId: testUser.teamId,
|
||||
tmbId: testUser.tmbId,
|
||||
resourceId: appId,
|
||||
permission: AppReadChatLogPerVal,
|
||||
resourceType: PerResourceTypeEnum.app
|
||||
});
|
||||
chatId = getNanoid();
|
||||
|
||||
// Create chat
|
||||
await MongoChat.create({
|
||||
teamId: testUser.teamId,
|
||||
tmbId: testUser.tmbId,
|
||||
appId,
|
||||
chatId,
|
||||
source: ChatSourceEnum.test,
|
||||
title: 'Original Title'
|
||||
});
|
||||
});
|
||||
|
||||
it('should update chat title successfully', async () => {
|
||||
const newTitle = 'Updated Title';
|
||||
|
||||
const res = await Call<UpdateHistoryBodyType, {}>(handler, {
|
||||
auth: testUser,
|
||||
body: {
|
||||
appId,
|
||||
chatId,
|
||||
title: newTitle
|
||||
}
|
||||
});
|
||||
|
||||
expect(res.code).toBe(200);
|
||||
expect(res.error).toBeUndefined();
|
||||
|
||||
// Verify that chat title was updated
|
||||
const updatedChat = await MongoChat.findOne({
|
||||
appId,
|
||||
chatId
|
||||
});
|
||||
|
||||
expect(updatedChat?.title).toBe(newTitle);
|
||||
});
|
||||
|
||||
it('should update customTitle successfully', async () => {
|
||||
const customTitle = 'Custom Title';
|
||||
|
||||
const res = await Call<UpdateHistoryBodyType, {}>(handler, {
|
||||
auth: testUser,
|
||||
body: {
|
||||
appId,
|
||||
chatId,
|
||||
customTitle
|
||||
}
|
||||
});
|
||||
|
||||
expect(res.code).toBe(200);
|
||||
expect(res.error).toBeUndefined();
|
||||
|
||||
// Verify that customTitle was updated
|
||||
const updatedChat = await MongoChat.findOne({
|
||||
appId,
|
||||
chatId
|
||||
});
|
||||
|
||||
expect(updatedChat?.customTitle).toBe(customTitle);
|
||||
});
|
||||
|
||||
it('should update top status successfully', async () => {
|
||||
const res = await Call<UpdateHistoryBodyType, {}>(handler, {
|
||||
auth: testUser,
|
||||
body: {
|
||||
appId,
|
||||
chatId,
|
||||
top: true
|
||||
}
|
||||
});
|
||||
|
||||
expect(res.code).toBe(200);
|
||||
expect(res.error).toBeUndefined();
|
||||
|
||||
// Verify that top was updated
|
||||
const updatedChat = await MongoChat.findOne({
|
||||
appId,
|
||||
chatId
|
||||
});
|
||||
|
||||
expect(updatedChat?.top).toBe(true);
|
||||
});
|
||||
|
||||
it('should update multiple fields at once', async () => {
|
||||
const newTitle = 'New Title';
|
||||
const customTitle = 'New Custom Title';
|
||||
const top = true;
|
||||
|
||||
const res = await Call<UpdateHistoryBodyType, {}>(handler, {
|
||||
auth: testUser,
|
||||
body: {
|
||||
appId,
|
||||
chatId,
|
||||
title: newTitle,
|
||||
customTitle,
|
||||
top
|
||||
}
|
||||
});
|
||||
|
||||
expect(res.code).toBe(200);
|
||||
expect(res.error).toBeUndefined();
|
||||
|
||||
// Verify that all fields were updated
|
||||
const updatedChat = await MongoChat.findOne({
|
||||
appId,
|
||||
chatId
|
||||
});
|
||||
|
||||
expect(updatedChat?.title).toBe(newTitle);
|
||||
expect(updatedChat?.customTitle).toBe(customTitle);
|
||||
expect(updatedChat?.top).toBe(top);
|
||||
});
|
||||
|
||||
it('should update updateTime when updating', async () => {
|
||||
const originalChat = await MongoChat.findOne({ appId, chatId });
|
||||
const originalUpdateTime = originalChat?.updateTime;
|
||||
|
||||
// Wait a bit to ensure time difference
|
||||
await new Promise((resolve) => setTimeout(resolve, 10));
|
||||
|
||||
const res = await Call<UpdateHistoryBodyType, {}>(handler, {
|
||||
auth: testUser,
|
||||
body: {
|
||||
appId,
|
||||
chatId,
|
||||
title: 'New Title'
|
||||
}
|
||||
});
|
||||
|
||||
expect(res.code).toBe(200);
|
||||
|
||||
const updatedChat = await MongoChat.findOne({ appId, chatId });
|
||||
expect(updatedChat?.updateTime.getTime()).toBeGreaterThan(originalUpdateTime?.getTime() || 0);
|
||||
});
|
||||
|
||||
it('should fail when chatId is missing', async () => {
|
||||
const res = await Call<UpdateHistoryBodyType, {}>(handler, {
|
||||
auth: testUser,
|
||||
body: {
|
||||
appId,
|
||||
chatId: '',
|
||||
title: 'New Title'
|
||||
}
|
||||
});
|
||||
|
||||
expect(res.code).toBe(500);
|
||||
expect(res.error).toBeDefined();
|
||||
});
|
||||
|
||||
it('should fail when appId is missing', async () => {
|
||||
const res = await Call<UpdateHistoryBodyType, {}>(handler, {
|
||||
auth: testUser,
|
||||
body: {
|
||||
appId: '',
|
||||
chatId,
|
||||
title: 'New Title'
|
||||
}
|
||||
});
|
||||
|
||||
expect(res.code).toBe(500);
|
||||
expect(res.error).toBeDefined();
|
||||
});
|
||||
|
||||
it('should fail when user does not have permission', async () => {
|
||||
const unauthorizedUser = await getUser('unauthorized-user-update-history');
|
||||
|
||||
const res = await Call<UpdateHistoryBodyType, {}>(handler, {
|
||||
auth: unauthorizedUser,
|
||||
body: {
|
||||
appId,
|
||||
chatId,
|
||||
title: 'New Title'
|
||||
}
|
||||
});
|
||||
|
||||
expect(res.code).toBe(500);
|
||||
expect(res.error).toBeDefined();
|
||||
});
|
||||
});
|
||||
|
|
@ -151,7 +151,16 @@ vi.mock('@fastgpt/service/common/s3/sources/dataset/index', () => ({
|
|||
}));
|
||||
|
||||
vi.mock('@fastgpt/service/common/s3/sources/chat/index', () => ({
|
||||
S3ChatSource: vi.fn()
|
||||
S3ChatSource: vi.fn(),
|
||||
getS3ChatSource: vi.fn(() => ({
|
||||
createUploadChatFileURL: vi.fn().mockResolvedValue({
|
||||
url: 'http://localhost:9000/mock-bucket',
|
||||
fields: { key: 'mock-key' },
|
||||
maxSize: 5 * 1024 * 1024
|
||||
}),
|
||||
deleteChatFilesByPrefix: vi.fn().mockResolvedValue(undefined),
|
||||
deleteChatFile: vi.fn().mockResolvedValue(undefined)
|
||||
}))
|
||||
}));
|
||||
|
||||
// Mock S3 initialization
|
||||
|
|
|
|||
Loading…
Reference in New Issue