Commit af669a1c by Archer Committed by GitHub

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>
parent 13681c92
......@@ -5,7 +5,7 @@
如果您发现了 FastGPT 的安全漏洞,请按照以下步骤进行报告:
1. **报告方式**
发送邮件至:yujinlong@sealos.io
发送邮件至:archer@fastgpt.io
请备注版本以及您的 GitHub 账号
3. **响应时间**
......
......@@ -58,5 +58,5 @@ Due to servers potentially being located in different countries/regions, you agr
**Contact Us**
1. For any questions, suggestions, or complaints about this policy, contact us at: yujinlong@sealos.io.
1. For any questions, suggestions, or complaints about this policy, contact us at: archer@fastgpt.io.
2. We will respond promptly and address your concerns.
......@@ -58,5 +58,5 @@ description: ' FastGPT 隐私政策'
**联系我们**
1. 如您对本隐私政策有任何疑问、建议或投诉,请通过以下方式与我们联系:yujinlong@sealos.io。
1. 如您对本隐私政策有任何疑问、建议或投诉,请通过以下方式与我们联系:archer@fastgpt.io。
2. 我们将尽快回复并解决您提出的问题。
......@@ -72,4 +72,4 @@ This FastGPT Service Agreement constitutes the terms and conditions agreed betwe
**Article 7 Additional Provisions**
1. If any clause is deemed unlawful or invalid, the remaining provisions shall remain enforceable.
2. Sealos retains final authority in interpreting this Agreement and privacy policies. For any inquiries, please contact us at yujinlong@sealos.io.
2. Sealos retains final authority in interpreting this Agreement and privacy policies. For any inquiries, please contact us at archer@fastgpt.io.
......@@ -67,4 +67,4 @@ FastGPT 服务协议是您与珠海环界云计算有限公司(以下简称“
**第7条 其他条款**
1. 如本协议中部分条款因违反法律法规而被视为无效,不影响其他条款的效力。
2. 本公司保留对本协议及隐私政策的最终解释权。如您对本协议或隐私政策有任何疑问,请联系我们:yujinlong@sealos.io。
2. 本公司保留对本协议及隐私政策的最终解释权。如您对本协议或隐私政策有任何疑问,请联系我们:archer@fastgpt.io。
......@@ -27,13 +27,15 @@ curl --location --request POST 'https://{{host}}/api/admin/initv4144' \
1. 工具调用支持配置流输出
2. AI 积分告警通知。
3. 对话日志支持展示 IP 地址归属地。
4. 通过 API 上传本地文件至知识库,保存至 S3。同时将旧版 Gridfs 代码全部移除。
5. 新版订阅套餐逻辑。
6. 支持配置对话文件白名单。
7. S3 支持 pathStyle 和 region 配置。
8. 支持通过 Sealos 来进行多租户自定义域名配置。
9. 工作流中引用工具时,文件输入支持手动填写(原本只支持变量引用)。
10. 支持网络代理(HTTP_PROXY,HTTPS_PROXY)
4. 对话日志支持展示应用版本名(如果对话中途修改成最新版本,则会被修改成最新版本)
5. 对话日志支持按点赞点踩过滤,并在对话详情里可以快速定位到赞/踩的记录。
6. 通过 API 上传本地文件至知识库,保存至 S3。同时将旧版 Gridfs 代码全部移除。
7. 新版订阅套餐逻辑。
8. 支持配置对话文件白名单。
9. S3 支持 pathStyle 和 region 配置。
10. 支持通过 Sealos 来进行多租户自定义域名配置。
11. 工作流中引用工具时,文件输入支持手动填写(原本只支持变量引用)。
12. 支持网络代理(HTTP_PROXY,HTTPS_PROXY)
## ⚙️ 优化
......@@ -44,6 +46,9 @@ curl --location --request POST 'https://{{host}}/api/admin/initv4144' \
5. LLM 请求时,图片无效报错提示。
6. completions 接口,非 stream 模式, detail=false 时,增加返回 reason_content。
7. 增加对于无效的 S3 key 检测。
8. 删除应用和知识库时,强制要求输入名称校验。
9. Mongo 慢操作日志,可以准确打印集合名和操作内容。
10. 分享链接,自定义鉴权返回的 uid,强制要求长度小于 200(太长会影响文件上传)。
## 🐛 修复
......@@ -64,6 +69,9 @@ curl --location --request POST 'https://{{host}}/api/admin/initv4144' \
15. 模型头像缺失情况下,默认 huggingface.svg 图标显示错误。
16. 日志导出时,结束时间会多出一天。
17. 表单输入,前端默认值未传递到实体值。
18. 工具调用时,未传递 max_tokens 参数。
19. 工作流判断器 value 值,未结合 condition 来综合获取数据类型。
20. 非直接分块模式的知识库数据,引用阅读器导航顺序异常。引用阅读器只会加载同一页。
## 插件
......
......@@ -97,10 +97,10 @@
"document/content/docs/protocol/index.mdx": "2025-07-30T15:38:30+08:00",
"document/content/docs/protocol/open-source.en.mdx": "2025-08-05T23:20:39+08:00",
"document/content/docs/protocol/open-source.mdx": "2025-08-05T23:20:39+08:00",
"document/content/docs/protocol/privacy.en.mdx": "2025-08-03T22:37:45+08:00",
"document/content/docs/protocol/privacy.mdx": "2025-08-03T22:37:45+08:00",
"document/content/docs/protocol/terms.en.mdx": "2025-08-03T22:37:45+08:00",
"document/content/docs/protocol/terms.mdx": "2025-08-03T22:37:45+08:00",
"document/content/docs/protocol/privacy.en.mdx": "2025-12-12T21:30:11+08:00",
"document/content/docs/protocol/privacy.mdx": "2025-12-12T21:30:11+08:00",
"document/content/docs/protocol/terms.en.mdx": "2025-12-12T21:30:11+08:00",
"document/content/docs/protocol/terms.mdx": "2025-12-12T21:30:11+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/upgrading/4-10/4100.mdx": "2025-08-02T19:38:37+08:00",
......@@ -119,7 +119,7 @@
"document/content/docs/upgrading/4-14/4141.mdx": "2025-11-19T10:15:27+08:00",
"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-12T15:28:03+08:00",
"document/content/docs/upgrading/4-14/4144.mdx": "2025-12-15T15:09:13+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",
......
import type { AppLogTimespanEnum } from './constants';
import type { AppChatLogAppData, AppChatLogChatData, AppChatLogUserData } from './type';
export type getChartDataBody = {
appId: string;
dateStart: Date;
dateEnd: Date;
source?: ChatSourceEnum[];
offset: number;
userTimespan: AppLogTimespanEnum;
chatTimespan: AppLogTimespanEnum;
appTimespan: AppLogTimespanEnum;
};
export type getChartDataResponse = {
userData: AppChatLogUserData;
chatData: AppChatLogChatData;
appData: AppChatLogAppData;
};
export type getTotalDataQuery = {
appId: string;
};
export type getTotalDataResponse = {
totalUsers: number;
totalChats: number;
totalPoints: number;
};
......@@ -14,7 +14,8 @@ export enum AppLogKeysEnum {
POINTS = 'points',
RESPONSE_TIME = 'responseTime',
ERROR_COUNT = 'errorCount',
REGION = 'region'
REGION = 'region',
VERSION_NAME = 'versionName'
}
export const AppLogKeysEnumMap = {
......@@ -31,7 +32,8 @@ export const AppLogKeysEnumMap = {
[AppLogKeysEnum.POINTS]: i18nT('app:logs_keys_points'),
[AppLogKeysEnum.RESPONSE_TIME]: i18nT('app:logs_keys_responseTime'),
[AppLogKeysEnum.ERROR_COUNT]: i18nT('app:logs_keys_errorCount'),
[AppLogKeysEnum.REGION]: i18nT('app:logs_keys_region')
[AppLogKeysEnum.REGION]: i18nT('app:logs_keys_region'),
[AppLogKeysEnum.VERSION_NAME]: i18nT('app:logs_keys_versionName')
};
export const DefaultAppLogKeys = [
......@@ -48,7 +50,8 @@ export const DefaultAppLogKeys = [
{ key: AppLogKeysEnum.POINTS, enable: false },
{ key: AppLogKeysEnum.RESPONSE_TIME, enable: false },
{ key: AppLogKeysEnum.ERROR_COUNT, enable: false },
{ key: AppLogKeysEnum.REGION, enable: true }
{ key: AppLogKeysEnum.REGION, enable: true },
{ key: AppLogKeysEnum.VERSION_NAME, enable: false }
];
export enum AppLogTimespanEnum {
......
import type { ChatSourceEnum } from '../../core/chat/constants';
import type { AppLogKeysEnum } from './constants';
export type AppLogKeysType = {
key: AppLogKeysEnum;
enable: boolean;
};
export type AppLogKeysSchemaType = {
teamId: string;
appId: string;
logKeys: AppLogKeysType[];
};
export type AppChatLogSchema = {
_id: string;
appId: string;
teamId: string;
chatId: string;
userId: string;
source: string;
sourceName?: string;
createTime: Date;
updateTime: Date;
chatItemCount: number;
errorCount: number;
totalPoints: number;
goodFeedbackCount: number;
badFeedbackCount: number;
totalResponseTime: number;
isFirstChat: boolean; // whether this is the user's first session in the app
};
export type AppChatLogUserData = {
timestamp: number;
summary: {
userCount: number;
newUserCount: number;
retentionUserCount: number;
points: number;
sourceCountMap: Record<ChatSourceEnum, number>;
};
}[];
export type AppChatLogChatData = {
timestamp: number;
summary: {
chatItemCount: number;
chatCount: number;
errorCount: number;
points: number;
};
}[];
export type AppChatLogAppData = {
timestamp: number;
summary: {
goodFeedBackCount: number;
badFeedBackCount: number;
chatCount: number;
totalResponseTime: number;
};
}[];
import { ObjectIdSchema } from '../../../common/type/mongo';
import { ChatSourceEnum } from '../../chat/constants';
import { AppLogKeysEnum } from './constants';
import { z } from 'zod';
export const AppLogKeysSchema = z.object({
key: z.enum(AppLogKeysEnum),
enable: z.boolean()
});
export type AppLogKeysType = z.infer<typeof AppLogKeysSchema>;
export const AppLogKeysSchemaType = z.object({
teamId: z.string(),
appId: z.string(),
logKeys: z.array(AppLogKeysSchema)
});
export type AppLogKeysSchemaType = z.infer<typeof AppLogKeysSchemaType>;
export const AppChatLogSchema = z.object({
_id: ObjectIdSchema,
appId: ObjectIdSchema,
teamId: ObjectIdSchema,
chatId: z.string(),
userId: z.string(),
source: z.enum(ChatSourceEnum),
sourceName: z.string().optional(),
createTime: z.date(),
updateTime: z.date(),
chatItemCount: z.number(),
errorCount: z.number(),
totalPoints: z.number(),
goodFeedbackCount: z.number(),
badFeedbackCount: z.number(),
totalResponseTime: z.number(),
isFirstChat: z.boolean() // whether this is the user's first session in the app
});
export type AppChatLogSchema = z.infer<typeof AppChatLogSchema>;
......@@ -28,6 +28,7 @@ export type ChatSchemaType = {
teamId: string;
tmbId: string;
appId: string;
appVersionId?: string;
createTime: Date;
updateTime: Date;
title: string;
......@@ -44,6 +45,12 @@ export type ChatSchemaType = {
variables: Record<string, any>;
pluginInputs?: FlowNodeInputItemType[];
metadata?: Record<string, any>;
// Boolean flags for efficient filtering
hasGoodFeedback?: boolean;
hasBadFeedback?: boolean;
hasUnreadGoodFeedback?: boolean;
hasUnreadBadFeedback?: boolean;
};
export type ChatWithAppSchema = Omit<ChatSchemaType, 'appId'> & {
......@@ -105,6 +112,7 @@ export type AIChatItemType = {
userBadFeedback?: string;
customFeedbacks?: string[];
adminFeedback?: AdminFbkType;
isFeedbackRead?: boolean;
durationSeconds?: number;
errorMsg?: string;
......@@ -152,6 +160,7 @@ export type ChatItemType = ChatItemMergeType & {
// Frontend type
export type ChatSiteItemType = ChatItemMergeType & {
_id?: string;
id: string;
dataId: string;
status: `${ChatStatusEnum}`;
moduleName?: string;
......
import type { OpenAPIPath } from '../../type';
import { AppLogPath } from './log';
export const AppPath: OpenAPIPath = {
...AppLogPath
};
import type { OpenAPIPath } from '../../../type';
import { TagsMap } from '../../../tag';
import { z } from 'zod';
import {
GetAppChatLogsBodySchema,
GetAppChatLogsResponseSchema,
ExportChatLogsBodySchema,
GetChartDataBodySchema,
GetChartDataResponseSchema,
GetTotalDataQuerySchema,
GetTotalDataResponseSchema,
GetLogKeysQuerySchema,
GetLogKeysResponseSchema,
UpdateLogKeysBodySchema
} from './api';
export const AppLogPath: OpenAPIPath = {
'/core/app/logs/getLogKeys': {
get: {
summary: '获取应用日志键',
description: '获取应用的日志键列表',
tags: [TagsMap.appLog],
requestParams: {
query: GetLogKeysQuerySchema
},
responses: {
200: {
description: '成功获取应用日志键',
content: {
'application/json': {
schema: GetLogKeysResponseSchema
}
}
}
}
}
},
'/core/app/logs/updateLogKeys': {
post: {
summary: '更新应用日志键',
description: '更新应用的日志键列表',
tags: [TagsMap.appLog],
requestBody: {
content: {
'application/json': {
schema: UpdateLogKeysBodySchema
}
}
},
responses: {
200: {
description: '成功更新应用日志键',
content: {
'application/json': {
schema: z.object({})
}
}
}
}
}
},
'/core/app/logs/list': {
post: {
summary: '获取应用日志列表',
description: '分页获取应用的对话日志列表,支持按时间范围、来源、用户等条件筛选',
tags: [TagsMap.appLog],
requestBody: {
content: {
'application/json': {
schema: GetAppChatLogsBodySchema
}
}
},
responses: {
200: {
description: '成功获取应用日志列表',
content: {
'application/json': {
schema: GetAppChatLogsResponseSchema
}
}
}
}
}
},
'/core/app/logs/exportLogs': {
post: {
summary: '导出应用日志',
description: '导出应用的对话日志为 CSV 文件,支持自定义导出字段和筛选条件',
tags: [TagsMap.appLog],
requestBody: {
content: {
'application/json': {
schema: ExportChatLogsBodySchema
}
}
},
responses: {
200: {
description: '成功导出应用日志,返回 CSV 文件',
content: {
'text/csv': {
schema: z.string()
}
}
}
}
}
},
'/proApi/core/app/logs/getTotalData': {
get: {
summary: '获取应用总体数据统计',
description: '获取应用的总体数据统计,包括总用户数、总对话数、总积分消耗',
tags: [TagsMap.appLog],
requestParams: {
query: GetTotalDataQuerySchema
},
responses: {
200: {
description: '成功获取应用总体数据',
content: {
'application/json': {
schema: GetTotalDataResponseSchema
}
}
}
}
}
},
'/proApi/core/app/logs/getChartData': {
post: {
summary: '获取应用图表数据',
description: '获取应用的图表统计数据,包括用户数据、对话数据、应用数据的时序统计',
tags: [TagsMap.appLog],
requestBody: {
content: {
'application/json': {
schema: GetChartDataBodySchema
}
}
},
responses: {
200: {
description: '成功获取应用图表数据',
content: {
'application/json': {
schema: GetChartDataResponseSchema
}
}
}
}
}
}
};
import { z } from 'zod';
/* =============== updateFeedbackReadStatus =============== */
export const UpdateFeedbackReadStatusBodySchema = z.object({
appId: z.string().min(1).meta({
example: '68ad85a7463006c963799a05',
description: '应用 ID'
}),
chatId: z.string().min(1).meta({
example: 'chat123',
description: '对话 ID'
}),
dataId: z.string().min(1).meta({
example: 'data123',
description: '消息数据 ID'
}),
isRead: z.boolean().meta({
example: true,
description: '是否已读'
})
});
export type UpdateFeedbackReadStatusBodyType = z.infer<typeof UpdateFeedbackReadStatusBodySchema>;
export const UpdateFeedbackReadStatusResponseSchema = z.object({
success: z.boolean().meta({
example: true,
description: '操作是否成功'
})
});
export type UpdateFeedbackReadStatusResponseType = z.infer<
typeof UpdateFeedbackReadStatusResponseSchema
>;
/* =============== adminUpdate =============== */
export const AdminUpdateFeedbackBodySchema = z.object({
appId: z.string().min(1).meta({
example: '68ad85a7463006c963799a05',
description: '应用 ID'
}),
chatId: z.string().min(1).meta({
example: 'chat123',
description: '对话 ID'
}),
dataId: z.string().min(1).meta({
example: 'data123',
description: '消息数据 ID'
}),
datasetId: z.string().min(1).meta({
example: 'dataset123',
description: '数据集 ID'
}),
feedbackDataId: z.string().min(1).meta({
example: 'feedback123',
description: '反馈数据 ID'
}),
q: z.string().min(1).meta({
example: '用户问题',
description: '问题内容'
}),
a: z.string().optional().meta({
example: 'AI 回答',
description: '答案内容(可选)'
})
});
export type AdminUpdateFeedbackBodyType = z.infer<typeof AdminUpdateFeedbackBodySchema>;
export const AdminUpdateFeedbackResponseSchema = z.object({});
export type AdminUpdateFeedbackResponseType = z.infer<typeof AdminUpdateFeedbackResponseSchema>;
/* =============== closeCustom =============== */
export const CloseCustomFeedbackBodySchema = z.object({
appId: z.string().min(1).meta({
example: '68ad85a7463006c963799a05',
description: '应用 ID'
}),
chatId: z.string().min(1).meta({
example: 'chat123',
description: '对话 ID'
}),
dataId: z.string().min(1).meta({
example: 'data123',
description: '消息数据 ID'
}),
index: z.number().int().nonnegative().meta({
example: 0,
description: '自定义反馈的索引位置'
})
});
export type CloseCustomFeedbackBodyType = z.infer<typeof CloseCustomFeedbackBodySchema>;
export const CloseCustomFeedbackResponseSchema = z.object({});
export type CloseCustomFeedbackResponseType = z.infer<typeof CloseCustomFeedbackResponseSchema>;
/* =============== updateUserFeedback =============== */
export const UpdateUserFeedbackBodySchema = z.object({
appId: z.string().min(1).meta({
example: '68ad85a7463006c963799a05',
description: '应用 ID'
}),
chatId: z.string().min(1).meta({
example: 'chat123',
description: '对话 ID'
}),
dataId: z.string().min(1).meta({
example: 'data123',
description: '消息数据 ID'
}),
userGoodFeedback: z.string().optional().nullable().meta({
example: '回答很好',
description: '用户好评反馈内容'
}),
userBadFeedback: z.string().optional().nullable().meta({
example: '回答不准确',
description: '用户差评反馈内容'
})
});
export type UpdateUserFeedbackBodyType = z.infer<typeof UpdateUserFeedbackBodySchema>;
export const UpdateUserFeedbackResponseSchema = z.object({});
export type UpdateUserFeedbackResponseType = z.infer<typeof UpdateUserFeedbackResponseSchema>;
/* =============== getFeedbackRecordIds =============== */
export const GetFeedbackRecordIdsBodySchema = z.object({
appId: z.string().meta({
example: '68ad85a7463006c963799a05',
description: '应用 ID'
}),
chatId: z.string().meta({
example: 'chat123',
description: '对话 ID'
}),
feedbackType: z.enum(['has_feedback', 'good', 'bad']).meta({
example: 'has_feedback',
description: '反馈类型:has_feedback-所有反馈, good-好评, bad-差评'
}),
unreadOnly: z.boolean().optional().meta({
example: false,
description: '是否只返回未读的反馈'
})
});
export type GetFeedbackRecordIdsBodyType = z.infer<typeof GetFeedbackRecordIdsBodySchema>;
export const GetFeedbackRecordIdsResponseSchema = z.object({
total: z.number().int().nonnegative().meta({
example: 10,
description: '符合条件的反馈总数'
}),
dataIds: z.array(z.string()).meta({
example: ['data123', 'data456'],
description: '反馈记录的数据 ID 列表'
})
});
export type GetFeedbackRecordIdsResponseType = z.infer<typeof GetFeedbackRecordIdsResponseSchema>;
import type { OpenAPIPath } from '../../../type';
import { TagsMap } from '../../../tag';
import {
UpdateFeedbackReadStatusBodySchema,
UpdateFeedbackReadStatusResponseSchema,
AdminUpdateFeedbackBodySchema,
AdminUpdateFeedbackResponseSchema,
CloseCustomFeedbackBodySchema,
CloseCustomFeedbackResponseSchema,
UpdateUserFeedbackBodySchema,
UpdateUserFeedbackResponseSchema,
GetFeedbackRecordIdsBodySchema,
GetFeedbackRecordIdsResponseSchema
} from './api';
export const ChatFeedbackPath: OpenAPIPath = {
'/core/chat/feedback/updateFeedbackReadStatus': {
post: {
summary: '更新反馈阅读状态',
description: '标记指定消息的反馈为已读或未读状态',
tags: [TagsMap.chatFeedback],
requestBody: {
content: {
'application/json': {
schema: UpdateFeedbackReadStatusBodySchema
}
}
},
responses: {
200: {
description: '成功更新反馈阅读状态',
content: {
'application/json': {
schema: UpdateFeedbackReadStatusResponseSchema
}
}
}
}
}
},
'/core/chat/feedback/adminUpdate': {
post: {
summary: '管理员标注反馈',
description: '管理员为指定消息添加或更新标注反馈,包含数据集关联信息',
tags: [TagsMap.chatFeedback],
requestBody: {
content: {
'application/json': {
schema: AdminUpdateFeedbackBodySchema
}
}
},
responses: {
200: {
description: '成功更新管理员反馈标注',
content: {
'application/json': {
schema: AdminUpdateFeedbackResponseSchema
}
}
}
}
}
},
'/core/chat/feedback/closeCustom': {
post: {
summary: '关闭自定义反馈',
description: '删除或关闭指定索引位置的自定义反馈条目',
tags: [TagsMap.chatFeedback],
requestBody: {
content: {
'application/json': {
schema: CloseCustomFeedbackBodySchema
}
}
},
responses: {
200: {
description: '成功关闭自定义反馈',
content: {
'application/json': {
schema: CloseCustomFeedbackResponseSchema
}
}
}
}
}
},
'/core/chat/feedback/updateUserFeedback': {
post: {
summary: '更新用户反馈',
description: '用户对消息添加或更新好评/差评反馈',
tags: [TagsMap.chatFeedback],
requestBody: {
content: {
'application/json': {
schema: UpdateUserFeedbackBodySchema
}
}
},
responses: {
200: {
description: '成功更新用户反馈',
content: {
'application/json': {
schema: UpdateUserFeedbackResponseSchema
}
}
}
}
}
},
'/core/chat/feedback/getFeedbackRecordIds': {
post: {
summary: '获取反馈记录ID列表',
description: '根据反馈类型和已读状态,获取符合条件的消息ID列表',
tags: [TagsMap.chatFeedback],
requestBody: {
content: {
'application/json': {
schema: GetFeedbackRecordIdsBodySchema
}
}
},
responses: {
200: {
description: '成功获取反馈记录ID列表',
content: {
'application/json': {
schema: GetFeedbackRecordIdsResponseSchema
}
}
}
}
}
}
};
import type { OpenAPIPath } from '../../type';
import { ChatSettingPath } from './setting';
import { ChatFavouriteAppPath } from './favourite/index';
import { ChatFeedbackPath } from './feedback/index';
import { z } from 'zod';
import { CreatePostPresignedUrlResultSchema } from '../../../../service/common/s3/type';
import { PresignChatFileGetUrlSchema, PresignChatFilePostUrlSchema } from '../../../core/chat/api';
......@@ -9,6 +10,7 @@ import { TagsMap } from '../../tag';
export const ChatPath: OpenAPIPath = {
...ChatSettingPath,
...ChatFavouriteAppPath,
...ChatFeedbackPath,
'/core/chat/presignChatFileGetUrl': {
post: {
......
......@@ -5,13 +5,13 @@ import { TagsMap } from '../../../tag';
export const ChatSettingPath: OpenAPIPath = {
'/proApi/core/chat/setting/detail': {
get: {
summary: '获取对话页设置',
summary: '获取门户页设置',
description:
'获取当前团队的对话页设置,包括 slogan、对话提示、Logo、快捷应用、已选工具和精选应用标签等配置信息',
'获取当前团队的门户页设置,包括 slogan、对话提示、Logo、快捷应用、已选工具和精选应用标签等配置信息',
tags: [TagsMap.chatSetting],
responses: {
200: {
description: '成功返回对话页设置信息',
description: '成功返回门户页设置信息',
content: {
'application/json': {
schema: ChatSettingSchema
......@@ -23,9 +23,9 @@ export const ChatSettingPath: OpenAPIPath = {
},
'/proApi/core/chat/setting/update': {
post: {
summary: '更新对话页设置',
summary: '更新门户页设置',
description:
'更新团队的对话页设置配置,包括 slogan、对话提示、Logo、快捷应用、已选工具和精选应用标签等信息',
'更新团队的门户页设置配置,包括 slogan、对话提示、Logo、快捷应用、已选工具和精选应用标签等信息',
tags: [TagsMap.chatSetting],
requestBody: {
content: {
......@@ -36,7 +36,7 @@ export const ChatSettingPath: OpenAPIPath = {
},
responses: {
200: {
description: '成功更新对话页设置',
description: '成功更新门户页设置',
content: {
'application/json': {
schema: ChatSettingSchema
......
......@@ -5,6 +5,7 @@ import { TagsMap } from './tag';
import { PluginPath } from './core/plugin';
import { WalletPath } from './support/wallet';
import { CustomDomainPath } from './support/customDomain';
import { AppPath } from './core/app';
export const openAPIDocument = createDocument({
openapi: '3.1.0',
......@@ -14,6 +15,7 @@ export const openAPIDocument = createDocument({
description: 'FastGPT API 文档'
},
paths: {
...AppPath,
...ChatPath,
...ApiKeyPath,
...PluginPath,
......@@ -23,28 +25,28 @@ export const openAPIDocument = createDocument({
servers: [{ url: '/api' }],
'x-tagGroups': [
{
name: '对话',
tags: [TagsMap.chatSetting, TagsMap.chatPage]
name: 'Agent 应用',
tags: [TagsMap.appLog]
},
{
name: '插件相关',
tags: [TagsMap.pluginToolTag, TagsMap.pluginTeam]
name: '对话管理',
tags: [TagsMap.chatSetting, TagsMap.chatPage, TagsMap.chatFeedback]
},
{
name: '插件-管理员',
tags: [TagsMap.pluginAdmin, TagsMap.pluginMarketplace, TagsMap.pluginToolAdmin]
name: '插件系统',
tags: [TagsMap.pluginToolTag, TagsMap.pluginTeam]
},
{
name: 'ApiKey',
tags: [TagsMap.apiKey]
name: '支付系统',
tags: [TagsMap.walletBill, TagsMap.walletDiscountCoupon]
},
{
name: '支付',
tags: [TagsMap.walletBill, TagsMap.walletDiscountCoupon]
name: '通用-辅助功能',
tags: [TagsMap.customDomain, TagsMap.apiKey]
},
{
name: '自定义域名',
tags: [TagsMap.customDomain]
name: '管理员-插件管理',
tags: [TagsMap.pluginAdmin, TagsMap.pluginMarketplace, TagsMap.pluginToolAdmin]
}
]
});
export const TagsMap = {
/* Core */
// Agent - log
appLog: 'Agent 日志',
// Chat - home
chatPage: '对话页',
chatSetting: '对话页配置',
pluginMarketplace: '插件市场(管理员视角)',
chatSetting: '门户页配置',
chatFeedback: '对话反馈',
// Plugin
pluginToolTag: '工具标签',
pluginAdmin: '管理员插件管理',
pluginToolAdmin: '管理员系统工具管理',
pluginTeam: '团队插件管理',
apiKey: 'APIKey',
/* Support */
// Wallet
walletBill: '订单',
walletDiscountCoupon: '优惠券',
customDomain: '自定义域名',
/* Common */
// APIKey
apiKey: 'APIKey',
/* Admin */
// Plugin
pluginMarketplace: '插件市场',
pluginAdmin: '管理员插件管理',
pluginToolAdmin: '管理员系统工具管理',
// Data
adminDashboard: '管理员仪表盘'
};
......@@ -45,7 +45,7 @@ export type TeamMemberSchema = {
updateTime?: Date;
name: string;
role: `${TeamMemberRoleEnum}`;
status: `${TeamMemberStatusEnum}`;
status: TeamMemberStatusEnum;
avatar: string;
};
......
import type { LangEnum } from '../common/i18n/type';
import type { LangEnum } from '../../common/i18n/type';
import type { TeamPermission } from '../permission/user/controller';
import type { UserStatusEnum } from './constant';
import type { TeamMemberStatusEnum } from './team/constant';
import { TeamMemberStatusEnum } from './team/constant';
import type { TeamTmbItemType } from './team/type';
import z from 'zod';
export type UserModelSchema = {
_id: string;
......@@ -35,8 +36,13 @@ export type UserType = {
contact?: string;
};
export type SourceMemberType = {
name: string;
avatar: string;
status: `${TeamMemberStatusEnum}`;
};
export const SourceMemberSchema = z.object({
name: z.string().meta({ example: '张三', description: '成员名称' }),
avatar: z
.string()
.meta({ example: 'https://cloud.fastgpt.cn/avatar.png', description: '成员头像' }),
status: z
.enum(TeamMemberStatusEnum)
.meta({ example: TeamMemberStatusEnum.active, description: '成员状态' })
});
export type SourceMemberType = z.infer<typeof SourceMemberSchema>;
......@@ -49,17 +49,34 @@ const addCommonMiddleware = (schema: mongoose.Schema) => {
schema.post(op, function (this: any, result: any, next) {
if (this._startTime) {
const duration = Date.now() - this._startTime;
const warnLogData = {
collectionName: this.collection?.name,
op: this.op,
...(this._query && { query: this._query }),
...(this._update && { update: this._update }),
...(this._delete && { delete: this._delete }),
duration
const getLogData = () => {
const collectionName = this.model?.collection?.name || this._model?.collection?.name;
const op = (() => {
if (this.op) return this.op;
if (this._pipeline) {
return 'aggregate';
}
if (this.constructor?.name === 'model') {
return 'save/create';
}
return this.constructor?.name || 'unknown';
})();
return {
duration,
collectionName,
op,
...(this._query && { query: this._query }),
...(this._pipeline && { pipeline: this._pipeline }),
...(this._update && { update: this._update }),
...(this._delete && { delete: this._delete })
};
};
if (duration > 1000) {
addLog.warn(`Slow operation ${duration}ms`, warnLogData);
if (duration > 2000) {
addLog.warn(`[Mongo Slow] Level2`, getLogData());
} else if (duration > 500) {
addLog.warn(`[Mongo Slow] Level1`, getLogData());
}
}
next();
......@@ -112,7 +129,6 @@ export const getMongoModel = <T>(name: string, schema: mongoose.Schema) => {
export const getMongoLogModel = <T>(name: string, schema: mongoose.Schema) => {
if (connectionLogMongo.models[name]) return connectionLogMongo.models[name] as Model<T>;
console.log('Load model======', name);
// addCommonMiddleware(schema);
const model = connectionLogMongo.model<T>(name, schema);
......
......@@ -2,7 +2,9 @@ import { ReadPreference } from './index';
export const readFromSecondary = {
readPreference: ReadPreference.SECONDARY_PREFERRED, // primary | primaryPreferred | secondary | secondaryPreferred | nearest
readConcern: 'local' as any // local | majority | linearizable | available
readConcern: {
level: 'local' as any
} // local | majority | linearizable | available
};
export const writePrimary = {
......
......@@ -217,6 +217,7 @@ export const runAgentCall = async ({
} = await createLLMResponse({
body: {
...body,
max_tokens: maxTokens,
model,
messages: requestMessages,
tool_choice: 'auto',
......
......@@ -10,7 +10,12 @@ import type {
StreamChatType,
UnStreamChatType
} from '@fastgpt/global/core/ai/type';
import { computedTemperature, parseLLMStreamResponse, parseReasoningContent } from '../utils';
import {
computedMaxToken,
computedTemperature,
parseLLMStreamResponse,
parseReasoningContent
} from '../utils';
import { removeDatasetCiteText } from '@fastgpt/global/core/ai/llm/utils';
import { getAIApi } from '../config';
import type { OpenaiAccountType } from '@fastgpt/global/support/user/team/type';
......@@ -525,8 +530,14 @@ const llmCompletionsBodyFormat = async <T extends CompletionsBodyType>({
})();
const stop = body.stop ?? undefined;
const maxTokens = computedMaxToken({
model: modelData,
maxToken: body.max_tokens || undefined
});
const requestBody = {
...body,
max_tokens: maxTokens,
model: modelData.model,
temperature:
typeof body.temperature === 'number'
......@@ -567,7 +578,7 @@ const createChatCompletion = async ({
timeout,
options
}: {
modelData?: LLMModelItemType;
modelData: LLMModelItemType;
body: ChatCompletionCreateParamsNonStreaming | ChatCompletionCreateParamsStreaming;
userKey?: OpenaiAccountType;
timeout?: number;
......@@ -587,13 +598,10 @@ const createChatCompletion = async ({
)
> => {
try {
// Rewrite model
const modelConstantsData = modelData || getLLMModel(body.model);
if (!modelConstantsData) {
if (!modelData) {
return Promise.reject(`${body.model} not found`);
}
body.model = modelConstantsData.model;
body.model = modelData.model;
const formatTimeout = timeout ? timeout : 600000;
const ai = getAIApi({
......@@ -607,12 +615,10 @@ const createChatCompletion = async ({
const response = await ai.chat.completions.create(body, {
...options,
...(modelConstantsData.requestUrl ? { path: modelConstantsData.requestUrl } : {}),
...(modelData.requestUrl ? { path: modelData.requestUrl } : {}),
headers: {
...options?.headers,
...(modelConstantsData.requestAuth
? { Authorization: `Bearer ${modelConstantsData.requestAuth}` }
: {})
...(modelData.requestAuth ? { Authorization: `Bearer ${modelData.requestAuth}` } : {})
}
});
......
......@@ -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 });
......
......@@ -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 });
......
......@@ -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,
......
......@@ -110,7 +110,7 @@ export const dispatchRunPlugin = async (props: RunPluginProps): Promise<RunPlugi
Array.isArray(val) &&
data[input.key]
) {
data[input.key] = val.map((item) => item.url);
data[input.key] = val.map((item) => (typeof item === 'string' ? item : item.url));
}
return {
......
......@@ -12,21 +12,16 @@ type PaginationResponse<T = {}> = {
list: T[];
};
type LinkedPaginationProps<T = {}> = T & {
type LinkedPaginationProps<T = {}, A = any> = T & {
pageSize: number;
} & RequireOnlyOne<{
initialId: string;
nextId: string;
prevId: string;
}> &
RequireOnlyOne<{
initialIndex: number;
nextIndex: number;
prevIndex: number;
}>;
anchor?: A;
initialId?: string;
nextId?: string;
prevId?: string;
};
type LinkedListResponse<T = {}> = {
list: Array<T & { _id: string; index: number }>;
type LinkedListResponse<T = {}, A = any> = {
list: Array<T & { id: string; anchor?: A }>;
hasMorePrev: boolean;
hasMoreNext: boolean;
};
import React, { useEffect, useMemo, useRef, useState } from 'react';
import { useDisclosure, Button, ModalBody, ModalFooter, type ImageProps } from '@chakra-ui/react';
import { useTranslation } from 'next-i18next';
import {
useDisclosure,
Button,
ModalBody,
ModalFooter,
Input,
VStack,
Box,
type ImageProps
} from '@chakra-ui/react';
import { Trans, useTranslation } from 'next-i18next';
import MyModal from '../components/common/MyModal';
import { useMemoizedFn } from 'ahooks';
import { useMemoEnhance } from './useMemoEnhance';
export const useConfirm = (props?: {
title?: string;
......@@ -12,10 +22,11 @@ export const useConfirm = (props?: {
type?: 'common' | 'delete';
hideFooter?: boolean;
iconColor?: ImageProps['color'];
inputConfirmText?: string;
}) => {
const { t } = useTranslation();
const map = useMemo(() => {
const map = useMemoEnhance(() => {
const map = {
common: {
title: t('common:action_confirm'),
......@@ -38,9 +49,13 @@ export const useConfirm = (props?: {
iconColor,
content,
showCancel = true,
hideFooter = false
hideFooter = false,
inputConfirmText: initialInputConfirmText
} = props || {};
const [customContent, setCustomContent] = useState<string | React.ReactNode>(content);
const [customContentInputConfirmText, setCustomContentInputConfirmText] = useState<
string | undefined
>(initialInputConfirmText);
const { isOpen, onOpen, onClose } = useDisclosure();
......@@ -48,11 +63,22 @@ export const useConfirm = (props?: {
const cancelCb = useRef<any>();
const openConfirm = useMemoizedFn(
(confirm?: Function, cancel?: any, customContent?: string | React.ReactNode) => {
confirmCb.current = confirm;
cancelCb.current = cancel;
({
onConfirm,
onCancel,
customContent,
inputConfirmText
}: {
onConfirm?: Function;
onCancel?: any;
customContent?: string | React.ReactNode;
inputConfirmText?: string;
}) => {
confirmCb.current = onConfirm;
cancelCb.current = onCancel;
customContent && setCustomContent(customContent);
setCustomContent(customContent || content);
setCustomContentInputConfirmText(inputConfirmText || initialInputConfirmText);
return onOpen;
}
......@@ -63,22 +89,24 @@ export const useConfirm = (props?: {
closeText = t('common:Cancel'),
confirmText = t('common:Confirm'),
isLoading,
bg,
countDown = 0
}: {
closeText?: string;
confirmText?: string;
isLoading?: boolean;
bg?: string;
countDown?: number;
}) => {
const isInputDelete = !!customContentInputConfirmText;
const timer = useRef<any>();
const [countDownAmount, setCountDownAmount] = useState(countDown);
const [requesting, setRequesting] = useState(false);
const [inputValue, setInputValue] = useState('');
useEffect(() => {
if (isOpen) {
setCountDownAmount(countDown);
setInputValue('');
timer.current = setInterval(() => {
setCountDownAmount((val) => {
if (val <= 0) {
......@@ -94,6 +122,10 @@ export const useConfirm = (props?: {
}
}, [isOpen]);
const isInputDeleteConfirmValid = !isInputDelete
? true
: !!customContentInputConfirmText && inputValue.trim() === customContentInputConfirmText;
return (
<MyModal
isOpen={isOpen}
......@@ -103,7 +135,30 @@ export const useConfirm = (props?: {
maxW={['90vw', '400px']}
>
<ModalBody pt={5} whiteSpace={'pre-wrap'} fontSize={'sm'}>
{customContent}
{isInputDelete ? (
<VStack align={'stretch'} spacing={3}>
<Box whiteSpace={'pre-wrap'}>{customContent}</Box>
<Box>
<Trans
i18nKey={'common:confirm_input_delete_tip'}
values={{ confirmText: customContentInputConfirmText }}
components={{
bold: <Box as={'span'} fontWeight={'bold'} userSelect={'all'} />
}}
/>
</Box>
<Input
size={'sm'}
value={inputValue}
onChange={(e) => setInputValue(e.target.value)}
placeholder={t('common:confirm_input_delete_placeholder', {
confirmText: customContentInputConfirmText
})}
/>
</VStack>
) : (
customContent
)}
</ModalBody>
{!hideFooter && (
<ModalFooter>
......@@ -124,7 +179,7 @@ export const useConfirm = (props?: {
<Button
size={'sm'}
variant={map.variant}
isDisabled={countDownAmount > 0}
isDisabled={countDownAmount > 0 || (isInputDelete && !isInputDeleteConfirmValid)}
ml={3}
isLoading={isLoading || requesting}
px={5}
......
import { useEffect, useRef, useState, type ReactNode } from 'react';
import { useCallback, useEffect, useRef, useState, type ReactNode } from 'react';
import { type LinkedListResponse, type LinkedPaginationProps } from '../common/fetch/type';
import { Box, type BoxProps } from '@chakra-ui/react';
import { useTranslation } from 'next-i18next';
import { useScroll, useMemoizedFn, useDebounceEffect } from 'ahooks';
import MyBox from '../components/common/MyBox';
import { useRequest2 } from './useRequest';
import { delay } from '../../global/common/system/utils';
const threshold = 100;
const threshold = 200;
export function useLinkedScroll<
TParams extends LinkedPaginationProps & { isInitialLoad?: boolean },
TParams extends LinkedPaginationProps,
TData extends LinkedListResponse
>(
api: (data: TParams) => Promise<TData>,
{
pageSize = 15,
pageSize = 10,
params = {},
currentData
currentData,
defaultScroll = 'top'
}: {
pageSize?: number;
params?: Record<string, any>;
currentData?: { id: string; index: number };
currentData?: { id: string; anchor?: any };
defaultScroll?: 'top' | 'bottom';
}
) {
const { t } = useTranslation();
......@@ -29,53 +30,61 @@ export function useLinkedScroll<
const [hasMorePrev, setHasMorePrev] = useState(true);
const [hasMoreNext, setHasMoreNext] = useState(true);
// 锚点,用于记录顶部和底部的数据
const anchorRef = useRef({
top: null as { _id: string; index: number } | null,
bottom: null as { _id: string; index: number } | null
top: null as TData['list'][number] | null,
bottom: null as TData['list'][number] | null
});
const containerRef = useRef<HTMLDivElement>(null);
const itemRefs = useRef<Map<string, HTMLElement | null>>(new Map());
const isInit = useRef(false);
const scrollToItem = async (id: string, retry = 3) => {
const itemIndex = dataList.findIndex((item) => item._id === id);
if (itemIndex === -1) return;
const scrollToItem = useCallback(
async (id?: string) => {
if (!id) {
id = defaultScroll === 'top' ? dataList[0]?.id : dataList[dataList.length - 1]?.id;
}
const element = itemRefs.current.get(id);
const itemIndex = dataList.findIndex((item) => item.id === id);
if (itemIndex === -1) {
return;
}
if (!element || !containerRef.current) {
if (retry > 0) {
await delay(500);
return scrollToItem(id, retry - 1);
const element = itemRefs.current.get(id);
if (!element || !containerRef.current) {
requestAnimationFrame(() => scrollToItem(id));
return;
}
return;
}
const elementRect = element.getBoundingClientRect();
const containerRect = containerRef.current.getBoundingClientRect();
const elementRect = element.getBoundingClientRect();
const containerRect = containerRef.current.getBoundingClientRect();
const scrollTop = containerRef.current.scrollTop + elementRect.top - containerRect.top;
const scrollTop = containerRef.current.scrollTop + elementRect.top - containerRect.top;
containerRef.current.scrollTo({
top: scrollTop
});
};
containerRef.current.scrollTo({
top: scrollTop
});
},
[dataList, defaultScroll]
);
const { runAsync: callApi, loading: isLoading } = useRequest2(api);
let scroolSign = useRef(false);
let scrollSign = useRef(false);
const { runAsync: loadInitData } = useRequest2(
async ({ scrollWhenFinish, refresh } = { scrollWhenFinish: true, refresh: false }) => {
if (!currentData || isLoading) return;
if (isLoading) return;
const item = dataList.find((item) => item._id === currentData.id);
// 已经被加载的数据,直接滚动到该位置
const item = dataList.find((item) => item.id === currentData?.id);
if (item && !refresh) {
scrollToItem(item._id);
scrollToItem(item.id);
return;
}
const response = await callApi({
initialId: currentData.id,
initialIndex: currentData.index,
initialId: currentData?.id,
anchor: currentData?.anchor,
pageSize,
...params
} as TParams);
......@@ -83,7 +92,7 @@ export function useLinkedScroll<
setHasMorePrev(response.hasMorePrev);
setHasMoreNext(response.hasMoreNext);
scroolSign.current = scrollWhenFinish;
scrollSign.current = scrollWhenFinish;
setDataList(response.list);
if (response.list.length > 0) {
......@@ -93,13 +102,20 @@ export function useLinkedScroll<
},
{
refreshDeps: [currentData],
onFinally() {
isInit.current = true;
},
manual: false
}
);
useEffect(() => {
if (scroolSign.current && currentData) {
scroolSign.current = false;
scrollToItem(currentData.id);
if (!isInit.current) return;
loadInitData({ refresh: true, scrollWhenFinish: true });
}, [params]);
useEffect(() => {
if (scrollSign.current) {
scrollSign.current = false;
scrollToItem(currentData?.id);
}
}, [dataList]);
......@@ -111,8 +127,8 @@ export function useLinkedScroll<
const prevScrollHeight = scrollRef?.current?.scrollHeight || 0;
const response = await callApi({
prevId: anchorRef.current.top._id,
prevIndex: anchorRef.current.top.index,
prevId: anchorRef.current.top.id,
anchor: anchorRef.current.top.anchor,
pageSize,
...params
} as TParams);
......@@ -148,8 +164,8 @@ export function useLinkedScroll<
const prevScrollTop = scrollRef?.current?.scrollTop || 0;
const response = await callApi({
nextId: anchorRef.current.bottom._id,
nextIndex: anchorRef.current.bottom.index,
nextId: anchorRef.current.bottom.id,
anchor: anchorRef.current.bottom.anchor,
pageSize,
...params
} as TParams);
......@@ -185,23 +201,37 @@ export function useLinkedScroll<
children: ReactNode;
ScrollContainerRef?: React.RefObject<HTMLDivElement>;
} & BoxProps) => {
const ref = ScrollContainerRef || containerRef;
const scroll = useScroll(ref);
// If external ref is provided, use it; otherwise use internal ref
const actualContainerRef = ScrollContainerRef || containerRef;
const scroll = useScroll(actualContainerRef);
// Merge refs: set both internal and external refs when element mounts
const setRefs = useCallback(
(el: HTMLDivElement | null) => {
// @ts-ignore - RefObject.current is readonly, but we need to set it
containerRef.current = el;
if (ScrollContainerRef) {
// @ts-ignore - RefObject.current is readonly, but we need to set it
ScrollContainerRef.current = el;
}
},
[ScrollContainerRef]
);
useDebounceEffect(
() => {
if (!ref?.current || isLoading) return;
if (!actualContainerRef?.current || isLoading) return;
const { scrollTop, scrollHeight, clientHeight } = ref.current;
const { scrollTop, scrollHeight, clientHeight } = actualContainerRef.current;
// 滚动到底部附近,加载更多下方数据
if (scrollTop + clientHeight >= scrollHeight - threshold) {
loadNextData(ref);
loadNextData(actualContainerRef);
}
// 滚动到顶部附近,加载更多上方数据
if (scrollTop <= threshold) {
loadPrevData(ref);
loadPrevData(actualContainerRef);
}
},
[scroll],
......@@ -209,7 +239,7 @@ export function useLinkedScroll<
);
return (
<MyBox ref={ref} h={'100%'} overflow={'auto'} isLoading={isLoading} {...props}>
<MyBox ref={setRefs} h={'100%'} overflow={'auto'} isLoading={isLoading} {...props}>
{hasMorePrev && prevLoading && (
<Box mt={2} fontSize={'xs'} color={'blackAlpha.500'} textAlign={'center'}>
{t('common:is_requesting')}
......
......@@ -69,8 +69,8 @@
"config_file_upload": "Click to Configure File Upload Rules",
"config_question_guide": "Configuration guess you want to ask",
"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": "Are you sure you want to delete 【{{name}}】 and all of its chat history?",
"confirm_delete_folder_tip": "Confirm to delete this folder? All apps and corresponding conversation records under it will be deleted. Please confirm!",
"confirm_del_app_tip": "Confirm to delete this app? \nDeleting an app will delete its associated conversation records as well.",
"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}}",
"copilot_confirm_message": "The original configuration has been received to understand the current code structure and input and output parameters. \nPlease explain your optimization requirements.",
......@@ -182,10 +182,17 @@
"local_upload": "Local upload",
"log_chat_logs": "Dialogue log",
"log_detail": "Log details",
"logs_all_feedback": "All feedback",
"logs_all_records": "All Records",
"logs_has_any_feedback": "Has Feedback",
"logs_has_good_feedback": "Has Good Feedback",
"logs_has_bad_feedback": "Has Bad Feedback",
"logs_app_data": "Data board",
"logs_app_result": "Application effect",
"logs_average_response_time": "Average run time",
"logs_average_response_time_description": "Average of total workflow run time",
"logs_bad_feedback": "Bad Feedback",
"logs_bad_feedback_only": "Bad Feedback Only",
"logs_chat_count": "Number of sessions",
"logs_chat_count_description": "How many new sessions does this application create? \nSession definition: When the interval between the previous message exceeds 15 minutes, it is considered to be a new session (this definition only takes effect here)",
"logs_chat_data": "chat data",
......@@ -199,7 +206,8 @@
"logs_error_rate_description": "The proportion of the total number of dialogues reported in error",
"logs_export_confirm_tip": "There are currently {{total}} conversation records, and each conversation can export up to 100 latest messages. \nConfirm export?",
"logs_export_title": "Time, source, user, contact, title, total number of messages, user good feedback, user bad feedback, custom feedback, labeled answers, conversation details",
"logs_good_feedback": "Like",
"logs_good_feedback": "Good Feedback",
"logs_good_feedback_only": "Good Feedback Only",
"logs_key_config": "Field Configuration",
"logs_keys_annotatedCount": "Annotated Answer Count",
"logs_keys_chatDetails": "Conversation details",
......@@ -216,6 +224,7 @@
"logs_keys_source": "Source",
"logs_keys_title": "Title",
"logs_keys_user": "User",
"logs_keys_versionName": "Version Name",
"logs_message_total": "Total Messages",
"logs_new_user_count": "New users",
"logs_points": "Points Consumed",
......@@ -234,6 +243,7 @@
"logs_total_points": "Accumulated points consumption",
"logs_total_tips": "Cumulative indicators are not affected by time filtering",
"logs_total_users": "Cumulative number of users",
"logs_unread_only": "Unread Only",
"logs_user_count": "Number of users",
"logs_user_count_description": "Number of people who have a conversation with the app in unit time",
"logs_user_data": "User data",
......
......@@ -2,6 +2,8 @@
"AI_input_is_empty": "The content passed to the AI ​​node is empty",
"Delete_all": "Clear All Lexicon",
"LLM_model_response_empty": "The model flow response is empty, please check whether the model flow output is normal.",
"Next": "Next",
"Previous": "Previous",
"ai_reasoning": "Thinking process",
"back_to_text": "Text input",
"balance_not_enough_pause": "Workflow paused due to insufficient AI points",
......@@ -63,6 +65,12 @@
"is_chatting": "Chatting in progress... please wait until it finishes",
"items": "Items",
"llm_tokens": "LLM tokens",
"log.feedback.hide_feedback": "hide feedback",
"log.feedback.mark_as_read": "Mark as Read",
"log.feedback.read": "Read",
"log.feedback.show_feedback": "Show Feedback",
"log.navigation.next": "Next",
"log.navigation.previous": "Previous",
"module_runtime_and": "Total Module Runtime",
"multiple_AI_conversations": "Multiple AI Conversations",
"new_input_guide_lexicon": "New Lexicon",
......
......@@ -215,6 +215,8 @@
"compliance.chat": "The content is generated by third-party AI and cannot be guaranteed to be true and accurate. It is for reference only.",
"compliance.dataset": "Please ensure that your content strictly complies with relevant laws and regulations and avoid containing any illegal or infringing content. \nPlease be careful when uploading materials that may contain sensitive information.",
"confirm_choice": "Confirm Choice",
"confirm_input_delete_placeholder": "Please enter: {{confirmText}}",
"confirm_input_delete_tip": "Please type <bold>{{confirmText}}</bold> to confirm",
"confirm_logout": "Confirm to log out?",
"confirm_move": "Move Here",
"confirm_update": "confirm_update",
......@@ -946,11 +948,14 @@
"n_agent_amount": "{{amount}} Agent limit",
"n_ai_points": "{{amount}} points",
"n_chat_records_retain": "{{amount}} Days of Chat History Retention",
"n_custom_domain_amount": "{{amount}} Custom domains",
"n_custom_domain_amount tip": "The number of custom domain names that the team can configure, which can currently be used to access Wecom intelligent robots",
"n_dataset_amount": "{{amount}} Dataset limit",
"n_dataset_size": "{{amount}} Dataset Indexes",
"n_team_audit_day": "{{amount}} days team operation log records",
"n_team_members": "{{amount}} Member",
"n_team_qpm": "{{amount}} QPM",
"n_website_sync_max_pages": "Single knowledge base {{amount}} web pages synchronized",
"name": "name",
"name_is_empty": "Name Cannot Be Empty",
"navbar.Account": "Account",
......@@ -985,7 +990,6 @@
"option": "Option",
"page": "Page",
"page_center": "Page Center",
"page_error": "An uncaught exception occurred.\n\n1. For private deployment users, 90% of cases are caused by incorrect model configuration/model not enabled. \n.\n\n2. Some systems are not compatible with related APIs. \nMost of the time it's caused by Apple's Safari browser, you can try changing it to Chrome.\n\n3. Please turn off the browser translation function. Some translations may cause the page to crash.\n\n\nAfter eliminating 3, open the console to view the specific error information.\n\nIf it prompts xxx undefined, the model configuration is incorrect. Check:\n\n1. Please ensure that at least one model of each series is available in the system, which can be checked in [Account - Model Provider].\n\n2. Please ensure that there is at least one knowledge base file processing model (there is a switch in the language model), otherwise an error will be reported when creating the knowledge base.\n\n2. Check whether some \"object\" parameters in the model are abnormal (arrays and objects). If they are empty, you can try to give an empty array or empty object.",
"pay.amount": "Amount",
"pay.error_desc": "There was a problem when converting payment routes",
"pay.noclose": "After payment is completed, please wait for the system to update automatically",
......@@ -1227,9 +1231,6 @@
"support.wallet.subscription.Upgrade plan": "Upgrade Package",
"support.wallet.subscription.ai_model": "AI Language Model",
"support.wallet.subscription.function.Community support tip": "Visit the FastGPT community for free help and technical support",
"n_custom_domain_amount": "{{amount}} Custom domains",
"n_website_sync_max_pages": "Single knowledge base {{amount}} web pages synchronized",
"n_custom_domain_amount tip": "The number of custom domain names that the team can configure, which can currently be used to access Wecom intelligent robots",
"support.wallet.subscription.mode.Month": "Month",
"support.wallet.subscription.mode.Period": "Subscription Period",
"support.wallet.subscription.mode.Year": "Year",
......
......@@ -71,8 +71,8 @@
"config_file_upload": "点击配置文件上传规则",
"config_question_guide": "配置猜你想问",
"confirm_copy_app_tip": "系统将为您创建一个相同配置应用,但权限不会进行复制,请确认!",
"confirm_del_app_tip": "确认删除 【{{name}}】 及其所有聊天记录?",
"confirm_delete_folder_tip": "确认删除该文件夹?将会删除它下面所有应用及对应的聊天记录,请确认!",
"confirm_del_app_tip": "确认删除该应用?删除应用会将其关联的对话记录一并删除。",
"confirm_delete_folder_tip": "删除该文件夹时,将会删除它下面所有应用及对应的聊天记录。",
"confirm_delete_tool": "确认删除该工具?",
"copilot_config_message": "`当前节点配置信息: \n代码类型:{{codeType}} \n当前代码: \\`\\`\\`{{codeType}} \n{{code}} \\`\\`\\` \n输入参数: {{inputs}} \n输出参数: {{outputs}}`",
"copilot_confirm_message": "已接收到原始配置,了解当前代码结构和输入输出参数。请说明您的优化需求。",
......@@ -186,11 +186,17 @@
"local_upload": "本地上传",
"log_chat_logs": "对话日志",
"log_detail": "日志详情",
"logs_all_feedback": "全部反馈",
"logs_all_records": "全部记录",
"logs_has_any_feedback": "包含反馈",
"logs_has_good_feedback": "包含赞",
"logs_has_bad_feedback": "包含踩",
"logs_app_data": "数据看板",
"logs_app_result": "应用效果",
"logs_average_response_time": "平均运行时长(s)",
"logs_average_response_time_description": "工作流总运行时间的平均值",
"logs_bad_feedback": "点踩",
"logs_bad_feedback_only": "仅看踩",
"logs_chat_count": "会话次数",
"logs_chat_count_description": "该应用共新建多少个会话。 会话定义:当与上条消息间隔超过15min,认为是产生新会话(该定义仅在此生效)",
"logs_chat_data": "对话数据",
......@@ -205,6 +211,7 @@
"logs_export_confirm_tip": "当前共有 {{total}} 条对话记录,每条对话最多可导出最新 100 条消息。确认导出?",
"logs_export_title": "时间,来源,使用者,联系方式,标题,消息总数,用户赞同反馈,用户反对反馈,自定义反馈,标注答案,对话详情",
"logs_good_feedback": "点赞",
"logs_good_feedback_only": "仅看赞",
"logs_key_config": "字段配置",
"logs_keys_annotatedCount": "标注答案数量",
"logs_keys_chatDetails": "对话详情",
......@@ -221,6 +228,7 @@
"logs_keys_source": "来源",
"logs_keys_title": "标题",
"logs_keys_user": "使用者",
"logs_keys_versionName": "版本名",
"logs_message_total": "消息总数",
"logs_new_user_count": "新增用户",
"logs_points": "积分消耗",
......@@ -246,6 +254,7 @@
"logs_total_points": "累计积分消耗",
"logs_total_tips": "累计指标不受时间筛选影响",
"logs_total_users": "累计用户数",
"logs_unread_only": "仅看未读",
"logs_user_callback": "用户反馈",
"logs_user_count": "用户数",
"logs_user_count_description": "单位时间内与该应用产生对话的人数",
......
......@@ -2,6 +2,8 @@
"AI_input_is_empty": "传入 AI 节点的内容为空",
"Delete_all": "清空词库",
"LLM_model_response_empty": "模型流响应为空,请检查模型流输出是否正常",
"Next": "下一个",
"Previous": "上一个",
"ai_reasoning": "思考过程",
"back_to_text": "返回输入",
"balance_not_enough_pause": "由于 AI 积分不足,暂停运行工作流",
......@@ -63,6 +65,12 @@
"is_chatting": "正在聊天中...请等待结束",
"items": "条",
"llm_tokens": "LLM tokens",
"log.feedback.hide_feedback": "隐藏反馈",
"log.feedback.mark_as_read": "标为已读",
"log.feedback.read": "已读",
"log.feedback.show_feedback": "显示反馈",
"log.navigation.next": "下一条",
"log.navigation.previous": "上一条",
"module_runtime_and": "工作流总运行时间",
"multiple_AI_conversations": "多组 AI 对话",
"new_input_guide_lexicon": "新词库",
......
......@@ -102,6 +102,7 @@
"add_new": "新增",
"add_new_param": "新增参数",
"add_success": "添加成功",
"aipoint_desc": "每次调用 AI 模型时,都会消耗一定的 AI 积分(类似于 token)。点击可查看详细计算规则。",
"all_quotes": "全部引用",
"all_result": "完整结果",
"app_evaluation": "Agent 评测(Beta)",
......@@ -210,10 +211,13 @@
"comfirm_leave_page": "确认离开该页面?",
"comfirn_create": "确认创建",
"commercial_function_tip": "请升级商业版后使用该功能:https://doc.fastgpt.cn/docs/introduction/commercial/",
"community_support": "社区免费支持",
"comon.Continue_Adding": "继续添加",
"compliance.chat": "内容由第三方 AI 生成,无法确保真实准确,仅供参考",
"compliance.dataset": "请确保您的内容严格遵守相关法律法规,避免包含任何违法或侵权的内容。请谨慎上传可能涉及敏感信息的资料。",
"confirm_choice": "确认选择",
"confirm_input_delete_placeholder": "请输入: {{confirmText}}",
"confirm_input_delete_tip": "请输入 <bold>{{confirmText}}</bold> 确认",
"confirm_logout": "确认退出登录?",
"confirm_move": "移动到这",
"confirm_update": "确认更新",
......@@ -946,6 +950,18 @@
"move.confirm": "确认移动",
"move_success": "移动成功",
"move_to": "移动到",
"n_agent_amount": "{{amount}} 个 Agent",
"n_ai_points": "{{amount}} 积分",
"n_app_registration_amount": "{{amount}} 个应用备案",
"n_chat_records_retain": "{{amount}} 天对话记录保留",
"n_custom_domain_amount": "{{amount}} 个自定义域名",
"n_custom_domain_amount tip": "团队可以配置的自定义域名数量,目前可用于接入企微智能机器人",
"n_dataset_amount": "{{amount}} 个知识库",
"n_dataset_size": "{{amount}} 组知识库索引",
"n_team_audit_day": "{{amount}} 天团队操作日志记录",
"n_team_members": "{{amount}} 个团队成员",
"n_team_qpm": "{{amount}} QPM",
"n_website_sync_max_pages": "站点同步最大 {{amount}} 页",
"name": "名称",
"name_is_empty": "名称不能为空",
"navbar.Account": "账号",
......@@ -981,7 +997,6 @@
"option": "选项",
"page": "页",
"page_center": "页面居中",
"page_error": "出现未捕获的异常。\n1. 私有部署用户,90%是由于模型配置不正确/模型未启用导致。。\n2. 部分系统不兼容相关API。大部分是苹果的safari 浏览器导致,可以尝试更换 chrome。\n3. 请关闭浏览器翻译功能,部分翻译导致页面崩溃。\n\n排除3后,打开控制台的 console 查看具体报错信息。\n如果提示 xxx undefined 的话,就是模型配置不正确,检查:\n1. 请确保系统内每个系列模型至少有一个可用,可以在【账号-模型提供商】中检查。\n2. 请确保至少有一个知识库文件处理模型(语言模型中有一个开关),否则知识库创建会报错。\n2. 检查模型中一些“对象”参数是否异常(数组和对象),如果为空,可以尝试给个空数组或空对象。",
"pay.amount": "金额",
"pay.error_desc": "转换支付途径时出现了问题",
"pay.noclose": "支付完成后,请等待系统自动更新",
......@@ -1047,6 +1062,7 @@
"price_over_wx_limit": "超出支付提供商限额:微信支付仅支持 6000 元以下",
"prompt_input_placeholder": "请输入提示词",
"psw_inconsistency": "两次密码不一致",
"qpm_desc": "主要指团队每分钟请求 Agent 的最大次数,与单个 Agent 复杂度无关。其他 OpenAPI 接口也受此影响,每个接口单独计算",
"question_feedback": "工单咨询",
"read_course": "查看教程",
"read_doc": "查看文档",
......@@ -1194,7 +1210,6 @@
"support.wallet.noBill": "无账单记录~",
"support.wallet.no_invoice": "暂无开票记录",
"support.wallet.subscription.AI points": "AI 积分",
"aipoint_desc": "每次调用 AI 模型时,都会消耗一定的 AI 积分(类似于 token)。点击可查看详细计算规则。",
"support.wallet.subscription.AI points usage": "AI 积分使用量",
"support.wallet.subscription.AI points usage tip": "每次调用 AI 模型时,都会消耗一定的 AI 积分。具体的计算标准可参考上方的“计费标准”",
"support.wallet.subscription.Ai points": "AI 积分计算标准",
......@@ -1224,22 +1239,7 @@
"support.wallet.subscription.Upgrade plan": "升级套餐",
"support.wallet.subscription.ai_model": "AI语言模型",
"support.wallet.subscription.eval_items_count": "单次评测数据条数: {{count}} 条",
"n_app_registration_amount": "{{amount}} 个应用备案",
"n_team_audit_day": "{{amount}} 天团队操作日志记录",
"n_custom_domain_amount": "{{amount}} 个自定义域名",
"n_chat_records_retain": "{{amount}} 天对话记录保留",
"n_agent_amount": "{{amount}} 个 Agent",
"n_dataset_amount": "{{amount}} 个知识库",
"n_dataset_size": "{{amount}} 组知识库索引",
"n_team_members": "{{amount}} 个团队成员",
"n_ai_points": "{{amount}} 积分",
"n_team_qpm": "{{amount}} QPM",
"worker_order_support_time": "{{amount}} 小时工单支持响应",
"community_support": "社区免费支持",
"support.wallet.subscription.function.Community support tip": "可前往 FastGPT 社区免费获取帮助和技术支持",
"n_website_sync_max_pages": "站点同步最大 {{amount}} 页",
"n_custom_domain_amount tip": "团队可以配置的自定义域名数量,目前可用于接入企微智能机器人",
"qpm_desc": "主要指团队每分钟请求 Agent 的最大次数,与单个 Agent 复杂度无关。其他 OpenAPI 接口也受此影响,每个接口单独计算",
"support.wallet.subscription.mode.Month": "按月",
"support.wallet.subscription.mode.Period": "订阅周期",
"support.wallet.subscription.mode.Year": "按年",
......@@ -1397,6 +1397,7 @@
"user_leaved": "离开",
"value": "值",
"verification": "验证",
"worker_order_support_time": "{{amount}} 小时工单支持响应",
"xx_search_result": "{{key}} 的搜索结果",
"yes": "是",
"yesterday": "昨天",
......
......@@ -69,8 +69,8 @@
"config_file_upload": "點選設定檔案上傳規則",
"config_question_guide": "設定猜你想問",
"confirm_copy_app_tip": "系統將為您建立一個相同設定的應用程式,但權限不會複製,請確認!",
"confirm_del_app_tip": "確認刪除【{{name}}】及其所有聊天紀錄?",
"confirm_delete_folder_tip": "確認刪除這個資料夾?將會刪除它底下所有應用程式及對應的對話紀錄,請確認!",
"confirm_del_app_tip": "確認刪除該應用?\n刪除應用會將其關聯的對話記錄一併刪除。",
"confirm_delete_folder_tip": "刪除該文件夾時,將會刪除它下面所有應用及對應的聊天記錄。",
"confirm_delete_tool": "確認刪除該工具?",
"copilot_config_message": "當前節點配置信息: \n代碼類型:{{codeType}} \n當前代碼: \\\\`\\\\`\\\\`{{codeType}} \n{{code}} \\\\`\\\\`\\\\` \n輸入參數: {{inputs}} \n輸出參數: {{outputs}}",
"copilot_confirm_message": "已接收到原始配置,了解當前代碼結構和輸入輸出參數。\n請說明您的優化需求。",
......@@ -181,10 +181,17 @@
"local_upload": "本地上傳",
"log_chat_logs": "對話日誌",
"log_detail": "日誌詳情",
"logs_all_feedback": "全部反饋",
"logs_all_records": "全部記錄",
"logs_has_any_feedback": "包含反饋",
"logs_has_good_feedback": "包含贊",
"logs_has_bad_feedback": "包含踩",
"logs_app_data": "數據看板",
"logs_app_result": "應用效果",
"logs_average_response_time": "平均運行時長",
"logs_average_response_time_description": "工作流總運行時間的平均值",
"logs_bad_feedback": "差評",
"logs_bad_feedback_only": "僅差評",
"logs_chat_count": "會話次數",
"logs_chat_count_description": "該應用共新建多少個會話。 \n會話定義:當與上條消息間隔超過15min,認為是產生新會話(該定義僅在此生效)",
"logs_chat_data": "對話數據",
......@@ -198,7 +205,8 @@
"logs_error_rate_description": "報錯對話佔總對話數量的比例",
"logs_export_confirm_tip": "當前共有 {{total}} 條對話記錄,每條對話最多可導出最新 100 條消息。\n確認導出?",
"logs_export_title": "時間,來源,使用者,聯絡方式,標題,訊息總數,使用者贊同回饋,使用者反對回饋,自定義回饋,標註答案,對話詳細資訊",
"logs_good_feedback": "點贊",
"logs_good_feedback": "好評",
"logs_good_feedback_only": "僅好評",
"logs_key_config": "字段配置",
"logs_keys_annotatedCount": "標記答案數量",
"logs_keys_chatDetails": "對話詳情",
......@@ -214,6 +222,7 @@
"logs_keys_source": "來源",
"logs_keys_title": "標題",
"logs_keys_user": "使用者",
"logs_keys_versionName": "版本名稱",
"logs_message_total": "訊息總數",
"logs_new_user_count": "新增用戶",
"logs_points": "積分消耗",
......@@ -232,6 +241,7 @@
"logs_total_points": "累計積分消耗",
"logs_total_tips": "累計指標不受時間篩選影響",
"logs_total_users": "累計用戶數",
"logs_unread_only": "僅看未讀",
"logs_user_count": "用戶數",
"logs_user_count_description": "單位時間內與該應用產生對話的人數",
"logs_user_data": "用戶數據",
......
......@@ -2,6 +2,8 @@
"AI_input_is_empty": "傳送至 AI 節點的內容為空",
"Delete_all": "清除所有詞彙",
"LLM_model_response_empty": "模型流程回應為空,請檢查模型流程輸出是否正常",
"Next": "下一個",
"Previous": "上一個",
"ai_reasoning": "思考過程",
"back_to_text": "返回輸入",
"balance_not_enough_pause": "由於 AI 積分不足,暫停運行工作流",
......@@ -63,6 +65,12 @@
"is_chatting": "對話進行中...請稍候",
"items": "筆",
"llm_tokens": "LLM tokens",
"log.feedback.hide_feedback": "隱藏反饋",
"log.feedback.mark_as_read": "標為已讀",
"log.feedback.read": "已讀",
"log.feedback.show_feedback": "顯示反饋",
"log.navigation.next": "下一條",
"log.navigation.previous": "上一條",
"module_runtime_and": "模組執行總時間",
"multiple_AI_conversations": "多組 AI 對話",
"new_input_guide_lexicon": "新增詞彙庫",
......
......@@ -215,6 +215,8 @@
"compliance.chat": "內容由第三方 AI 產生,無法保證其真實性與準確性,僅供參考。",
"compliance.dataset": "請確保您的內容嚴格遵守相關法律法規,避免包含任何違法或侵權的內容。\n在上傳可能涉及敏感資訊的資料時請務必謹慎。",
"confirm_choice": "確認選擇",
"confirm_input_delete_placeholder": "請輸入: {{confirmText}}",
"confirm_input_delete_tip": "請輸入 <bold>{{confirmText}}</bold> 確認",
"confirm_logout": "確認退出登錄?",
"confirm_move": "移動至此",
"confirm_update": "確認更新",
......@@ -944,11 +946,14 @@
"n_agent_amount": "{{amount}} 個 Agent",
"n_ai_points": "{{amount}} 積分",
"n_chat_records_retain": "{{amount}} 天對話紀錄保留",
"n_custom_domain_amount": "{{amount}} 個自定義域名",
"n_custom_domain_amount tip": "團隊可以配置的自定義域名數量,目前可用於接入企微智能機器人",
"n_dataset_amount": "{{amount}} 個知識庫",
"n_dataset_size": "{{amount}} 組知識庫索引",
"n_team_audit_day": "{{amount}} 天團隊操作日誌記錄",
"n_team_members": "{{amount}} 個團隊成員",
"n_team_qpm": "{{amount}} QPM",
"n_website_sync_max_pages": "單知識庫 {{amount}} 個網頁同步",
"name": "名稱",
"name_is_empty": "名稱不能為空",
"navbar.Account": "帳戶",
......@@ -982,7 +987,6 @@
"option": "選項",
"page": "頁",
"page_center": "頁面置中",
"page_error": "出現未捕獲的異常。\n\n1. 私有部署用戶,90%是由於模型配置不正確/模型未啟用導致。 \n\n\n2. 部分系統不兼容相關API。\n大部分是蘋果的safari 瀏覽器導致,可以嘗試更換 chrome。\n\n3. 請關閉瀏覽器翻譯功能,部分翻譯導致頁面崩潰。\n\n\n排除3後,打開控制台的 console 查看具體報錯信息。\n\n如果提示 xxx undefined 的話,就是模型配置不正確,檢查:\n1. 請確保系統內每個系列模型至少有一個可用,可以在【賬號-模型提供商】中檢查。\n\n2. 請確保至少有一個知識庫文件處理模型(語言模型中有一個開關),否則知識庫創建會報錯。\n\n2. 檢查模型中一些“對象”參數是否異常(數組和對象),如果為空,可以嘗試給個空數組或空對象。",
"pay.amount": "金額",
"pay.error_desc": "轉換支付途徑時出現了問題",
"pay.noclose": "支付完成後,請等待系統自動更新",
......@@ -1224,9 +1228,6 @@
"support.wallet.subscription.Upgrade plan": "升級方案",
"support.wallet.subscription.ai_model": "AI 語言模型",
"support.wallet.subscription.function.Community support tip": "可前往 FastGPT 社群免費獲取幫助和技術支持",
"n_custom_domain_amount": "{{amount}} 個自定義域名",
"n_website_sync_max_pages": "單知識庫 {{amount}} 個網頁同步",
"n_custom_domain_amount tip": "團隊可以配置的自定義域名數量,目前可用於接入企微智能機器人",
"support.wallet.subscription.mode.Month": "按月",
"support.wallet.subscription.mode.Period": "訂閱週期",
"support.wallet.subscription.mode.Year": "按年",
......
### 常见问题
- [**Git 地址**,点击查看项目地址](https://github.com/labring/FastGPT)
- [点击查看官方文档](https://doc.fastgpt.io/docs/)
- [点击查看商业版文档](https://doc.fastgpt.io/docs/commercial/)
- [计费规则](https://doc.fastgpt.io/docs/pricing/)
**其他问题**
| 扫码进入交流群 |
| ----------------------- |
| ![](https://oss.laf.run/otnvvf-imgs/fastgpt-feishu1.png) |
### FastGPT V4.11.0 更新说明
## 🚀 新增内容
1. 商业版增加应用评测 Beta 版功能,可对应用进行有监督评分。
2. 工作流部分节点支持报错捕获分支。
3. 对话页独立 tab 页面UX。
4. 支持 Signoz traces 和 logs 系统追踪。
5. 新增 Gemini2.5, grok4, kimi 模型配置。
6. 模型调用日志增加首字响应时长和请求 IP。
## ⚙️ 优化
1. 优化代码,避免递归造成的内存堆积。
2. 知识库训练:支持全部重试当前集合异常数据。
3. 工作流 valueTypeFormat,避免数据类型不一致。
## 🐛 修复
1. 问题分类和内容提取节点,默认模型无法通过前端校验,导致工作流无法运行和保存发布。
## 🔨 工具更新
1. Markdown 文本转 Docx 和 Xlsx 文件。
\ No newline at end of file
import React, { useMemo } from 'react';
import React, { useEffect, useMemo } from 'react';
import { Box, Flex } from '@chakra-ui/react';
import { useRouter } from 'next/router';
import { useLoading } from '@fastgpt/web/hooks/useLoading';
......@@ -70,11 +70,12 @@ const Layout = ({ children }: { children: JSX.Element }) => {
const { toast } = useToast();
const { t } = useTranslation();
const { Loading } = useLoading();
const { loading, feConfigs, llmModelList, embeddingModelList } = useSystemStore();
const { setLastRoute, loading, feConfigs, llmModelList, embeddingModelList } = useSystemStore();
const { isPc } = useSystem();
const { userInfo, isUpdateNotification, setIsUpdateNotification } = useUserStore();
const { setUserDefaultLng } = useI18nLng();
// Auto redeem coupon
useCheckCoupon();
const isChatPage = useMemo(
......@@ -127,6 +128,11 @@ const Layout = ({ children }: { children: JSX.Element }) => {
}
);
// Route watch
useEffect(() => {
setLastRoute(router.pathname);
}, [router.pathname]);
return (
<>
<Box h={'100%'} bg={'myGray.100'}>
......
import { Box, type BoxProps, Card, Flex } from '@chakra-ui/react';
import React, { useCallback, useEffect, useMemo, useRef } from 'react';
import { Box, type BoxProps, Card, Flex, Button } from '@chakra-ui/react';
import React, { useMemo, useState } from 'react';
import ChatController, { type ChatControllerProps } from './ChatController';
import ChatAvatar from './ChatAvatar';
import { MessageCardStyle } from '../constants';
......@@ -37,6 +37,7 @@ import { addStatisticalDataToHistoryItem } from '@/global/core/chat/utils';
import dynamic from 'next/dynamic';
import { useMemoizedFn } from 'ahooks';
import ChatBoxDivider from '../../../Divider';
import { useMemoEnhance } from '@fastgpt/web/hooks/useMemoEnhance';
const ResponseTags = dynamic(() => import('./ResponseTags'));
......@@ -131,29 +132,34 @@ const AIContentCard = React.memo(function AIContentCard({
const ChatItem = (props: Props) => {
const { type, avatar, statusBoxData, children, isLastChild, questionGuides = [], chat } = props;
const { t } = useTranslation();
const { isPc } = useSystem();
const styleMap: BoxProps = {
...(type === ChatRoleEnum.Human
? {
order: 0,
borderRadius: '8px 0 8px 8px',
justifyContent: 'flex-end',
textAlign: 'right',
bg: 'primary.100'
}
: {
order: 1,
borderRadius: '0 8px 8px 8px',
justifyContent: 'flex-start',
textAlign: 'left',
bg: 'myGray.50'
}),
fontSize: 'mini',
fontWeight: '400',
color: 'myGray.500'
};
const { t } = useTranslation();
const [showFeedbackContent, setShowFeedbackContent] = useState(false);
const styleMap: BoxProps = useMemoEnhance(
() => ({
...(type === ChatRoleEnum.Human
? {
order: 0,
borderRadius: '8px 0 8px 8px',
justifyContent: 'flex-end',
textAlign: 'right',
bg: 'primary.100'
}
: {
order: 1,
borderRadius: '0 8px 8px 8px',
justifyContent: 'flex-start',
textAlign: 'left',
bg: 'myGray.50'
}),
fontSize: 'mini',
fontWeight: '400',
color: 'myGray.500'
}),
[type]
);
const isChatting = useContextSelector(ChatBoxContext, (v) => v.isChatting);
const chatType = useContextSelector(ChatBoxContext, (v) => v.chatType);
......@@ -280,6 +286,7 @@ const ChatItem = (props: Props) => {
return (
<Box
data-chat-id={chat.dataId}
_hover={{
'& .time-label': {
display: 'block'
......@@ -304,7 +311,12 @@ const ChatItem = (props: Props) => {
}).replace('#', ':')}
</Box>
)}
<ChatController {...props} isLastChild={isLastChild} />
<ChatController
{...props}
isLastChild={isLastChild}
showFeedbackContent={showFeedbackContent}
onToggleFeedbackContent={() => setShowFeedbackContent(!showFeedbackContent)}
/>
</Flex>
)}
<ChatAvatar src={avatar} type={type} />
......@@ -333,6 +345,37 @@ const ChatItem = (props: Props) => {
</Flex>
)}
</Flex>
{/* User Feedback Content: Admin log show */}
{isChatLog &&
showFeedbackContent &&
chat.obj === ChatRoleEnum.AI &&
(chat.userGoodFeedback || chat.userBadFeedback) && (
<Box
mt={2}
maxW={'250'}
border={'1px solid'}
borderColor={'myGray.250'}
borderRadius={'md'}
p={3}
>
<Box fontSize={'sm'} color={'myGray.900'} whiteSpace={'pre-wrap'}>
{chat.userBadFeedback || chat.userGoodFeedback}
</Box>
<Flex justifyContent={'flex-end'} mt={2}>
<Button
size={'xs'}
variant={'grayGhost'}
fontSize={'xs'}
onClick={() => setShowFeedbackContent(false)}
color={'primary.600'}
>
{t('chat:log.feedback.hide_feedback')}
</Button>
</Flex>
</Box>
)}
{/* content */}
{splitAiResponseResults.map((value, i) => (
<Box
......
import { useMarkdown } from '@/web/common/hooks/useMarkdown';
import { Box, Card } from '@chakra-ui/react';
import React from 'react';
import dynamic from 'next/dynamic';
const Markdown = dynamic(() => import('@/components/Markdown'), { ssr: false });
const Empty = () => {
const { data: chatProblem } = useMarkdown({ url: '/chatProblem.md' });
const { data: versionIntro } = useMarkdown({ url: '/versionIntro.md' });
return (
<Box py={6} w={'85%'} maxW={'600px'} m={'auto'} alignItems={'center'} justifyContent={'center'}>
{/* version intro */}
<Card p={4} mb={10} minH={'200px'}>
<Markdown source={versionIntro} />
</Card>
<Card p={4} minH={'600px'}>
<Markdown source={chatProblem} />
</Card>
</Box>
);
};
export default React.memo(Empty);
......@@ -3,7 +3,7 @@ import { ModalBody, Textarea, ModalFooter, Button } from '@chakra-ui/react';
import MyModal from '@fastgpt/web/components/common/MyModal';
import { useRequest } from '@fastgpt/web/hooks/useRequest';
import { useTranslation } from 'next-i18next';
import { updateChatUserFeedback } from '@/web/core/chat/api';
import { updateChatUserFeedback } from '@/web/core/chat/feedback/api';
import { useContextSelector } from 'use-context-selector';
import { WorkflowRuntimeContext } from '../../context/workflowRuntimeContext';
......
import React from 'react';
import { ModalBody, ModalFooter, Button } from '@chakra-ui/react';
import MyModal from '@fastgpt/web/components/common/MyModal';
import { useTranslation } from 'next-i18next';
const ReadFeedbackModal = ({
content,
onCloseFeedback,
onClose
}: {
content: string;
onCloseFeedback: () => void;
onClose: () => void;
}) => {
const { t } = useTranslation();
return (
<MyModal
isOpen={true}
onClose={onClose}
iconSrc="/imgs/modal/readFeedback.svg"
title={t('common:core.chat.Feedback Modal')}
>
<ModalBody>{content}</ModalBody>
<ModalFooter>
<Button mr={2} onClick={onCloseFeedback}>
{t('common:core.chat.feedback.Feedback Close')}
</Button>
</ModalFooter>
</MyModal>
);
};
export default React.memo(ReadFeedbackModal);
......@@ -16,7 +16,6 @@ import { useTranslation } from 'next-i18next';
import { type ChatBoxInputFormType } from '../ChatBox/type';
import { chats2GPTMessages } from '@fastgpt/global/core/chat/adapt';
import { clientGetWorkflowToolRunUserQuery } from '@fastgpt/global/core/workflow/utils';
import { cloneDeep } from 'lodash';
import { ChatItemContext } from '@/web/core/chat/context/chatItemContext';
import { ChatRecordContext } from '@/web/core/chat/context/chatRecordContext';
import { type AppFileSelectConfigType } from '@fastgpt/global/core/app/type';
......@@ -190,6 +189,7 @@ const PluginRunContextProvider = ({
abortRequest();
const abortSignal = new AbortController();
chatController.current = abortSignal;
const humanChatItemId = getNanoid(24);
const responseChatItemId = getNanoid(24);
setChatRecords([
......@@ -199,9 +199,12 @@ const PluginRunContextProvider = ({
variables,
files: files as RuntimeUserPromptType['files']
}),
id: humanChatItemId,
dataId: humanChatItemId,
status: 'finish'
},
{
id: responseChatItemId,
dataId: responseChatItemId,
obj: ChatRoleEnum.AI,
value: [
......
......@@ -220,7 +220,7 @@ const ApiKeyTable = ({ tips, appId }: { tips: string; appId?: string }) => {
label: t('common:Delete'),
icon: 'delete',
type: 'danger',
onClick: () => openConfirm(() => onclickRemove(_id))()
onClick: () => openConfirm({ onConfirm: () => onclickRemove(_id) })()
}
]
}
......
......@@ -51,11 +51,10 @@ const DefaultPermissionList = ({
value={per}
onChange={(per) => {
if (isInheritPermission && hasParent) {
openConfirm(
() => onRequestChange(per),
undefined,
t('common:permission.Remove InheritPermission Confirm')
)();
openConfirm({
onConfirm: () => onRequestChange(per),
customContent: t('common:permission.Remove InheritPermission Confirm')
})();
} else {
return onRequestChange(per);
}
......
......@@ -147,7 +147,7 @@ function MemberModal({
newChildClbs
});
if (isConflict && isInheritPermission) {
return openConfirmDisableInheritPer(_onConfirm)();
return openConfirmDisableInheritPer({ onConfirm: _onConfirm })();
} else {
return _onConfirm();
}
......
......@@ -23,17 +23,16 @@ const ResumeInherit = ({
cursor={'pointer'}
_hover={{ color: 'primary.600' }}
onClick={() => {
openCommonConfirm(
() =>
openCommonConfirm({
onConfirm: () =>
onResume()?.then(() => {
toast({
title: t('common:permission.Resume InheritPermission Success'),
status: 'success'
});
}),
undefined,
t('common:permission.Resume InheritPermission Confirm')
)();
customContent: t('common:permission.Resume InheritPermission Confirm')
})();
}}
>
{t('common:click_to_resume')}
......
import type { ChatSourceEnum } from '@fastgpt/global/core/chat/constants';
import { UsageSourceEnum } from '@fastgpt/global/support/wallet/usage/constants';
import type { PaginationProps } from '@fastgpt/web/common/fetch/type';
import type { I18nName } from '@fastgpt/service/common/geo/type';
export type GetAppChatLogsProps = {
appId: string;
dateStart: string | Date;
dateEnd: string | Date;
sources?: ChatSourceEnum[];
tmbIds?: string[];
chatSearch?: string;
};
export type GetAppChatLogsParams = PaginationProps<GetAppChatLogsProps>;
import type { UserType } from '@fastgpt/global/support/user/type.d';
import type { UserType } from '@fastgpt/global/support/user/type';
import type { PromotionRecordSchema } from '@fastgpt/global/support/activity/type.d';
export interface LoginSuccessResponse {
user: UserType;
......
......@@ -139,9 +139,11 @@ const AccountContainer = ({
const setCurrentTab = useCallback(
(tab: string) => {
if (tab === TabEnum.loginout) {
openConfirm(() => {
setUserInfo(null);
router.replace('/login');
openConfirm({
onConfirm: () => {
setUserInfo(null);
router.replace('/login');
}
})();
} else {
router.replace('/account/' + tab);
......
......@@ -215,13 +215,12 @@ const ChannelTable = ({ Tab }: { Tab: React.ReactNode }) => {
icon: 'delete',
label: t('common:Delete'),
onClick: () =>
openConfirm(
() => onDeleteChannel(item.id),
undefined,
t('account_model:confirm_delete_channel', {
openConfirm({
onConfirm: () => onDeleteChannel(item.id),
customContent: t('account_model:confirm_delete_channel', {
name: item.name
})
)()
})()
}
]
}
......
......@@ -188,7 +188,9 @@ function MemberTable({ Tabs }: { Tabs: React.ReactNode }) {
label: t('common:Delete'),
icon: 'delete',
onClick: () => {
openDeleteGroupModal(() => delDeleteGroup(group._id))();
openDeleteGroupModal({
onConfirm: () => delDeleteGroup(group._id)
})();
},
type: 'danger' as MenuItemType
}
......
......@@ -98,7 +98,8 @@ function OrgTable({ Tabs }: { Tabs: React.ReactNode }) {
type: 'delete',
content: t('account_team:confirm_delete_org')
});
const deleteOrgHandler = (orgId: string) => openDeleteOrgModal(() => deleteOrgReq(orgId))();
const deleteOrgHandler = (orgId: string) =>
openDeleteOrgModal({ onConfirm: () => deleteOrgReq(orgId) })();
const { runAsync: deleteOrgReq } = useRequest2(deleteOrg, {
onSuccess: refresh
});
......@@ -229,13 +230,15 @@ function OrgTable({ Tabs }: { Tabs: React.ReactNode }) {
username: member.memberName
}),
onClick: () => {
openDeleteMemberFromTeamModal(
() => deleteMemberFromTeamReq(member.tmbId),
undefined,
t('account_team:confirm_delete_from_team', {
username: member.memberName
})
)();
openDeleteMemberFromTeamModal({
onConfirm: () => deleteMemberFromTeamReq(member.tmbId),
customContent: t(
'account_team:confirm_delete_from_team',
{
username: member.memberName
}
)
})();
}
},
...(isSyncMember
......@@ -250,8 +253,8 @@ function OrgTable({ Tabs }: { Tabs: React.ReactNode }) {
},
label: t('account_team:delete_from_org'),
onClick: () =>
openDeleteMemberFromOrgModal(
() => {
openDeleteMemberFromOrgModal({
onConfirm: () => {
if (currentOrg) {
return deleteMemberReq(
currentOrg._id,
......@@ -259,11 +262,13 @@ function OrgTable({ Tabs }: { Tabs: React.ReactNode }) {
);
}
},
undefined,
t('account_team:confirm_delete_from_org', {
username: member.memberName
})
)()
customContent: t(
'account_team:confirm_delete_from_org',
{
username: member.memberName
}
)
})()
}
])
]
......
import React, { useMemo } from 'react';
import React, { useMemo, useState, useCallback } from 'react';
import { Flex, Box } from '@chakra-ui/react';
import { useTranslation } from 'next-i18next';
import { HUMAN_ICON } from '@fastgpt/global/common/system/constants';
......@@ -21,6 +21,7 @@ import { useRequest2 } from '@fastgpt/web/hooks/useRequest';
import { useContextSelector } from 'use-context-selector';
import ChatQuoteList from '@/pageComponents/chat/ChatQuoteList';
import { ChatTypeEnum } from '@/components/core/chat/ChatContainer/ChatBox/constants';
import { DetailLogsModalFeedbackTypeFilter } from './FeedbackTypeFilter';
const PluginRunBox = dynamic(() => import('@/components/core/chat/ChatContainer/PluginRunBox'));
const ChatBox = dynamic(() => import('@/components/core/chat/ChatContainer/ChatBox'));
......@@ -31,10 +32,24 @@ type Props = {
onClose: () => void;
};
const DetailLogsModal = ({ appId, chatId, onClose }: Props) => {
const DetailLogsModal = ({
appId,
chatId,
onClose,
feedbackRecordId,
handleRecordChange
}: Props & {
feedbackRecordId: string | undefined;
handleRecordChange: (recordId: string | undefined) => void;
}) => {
const { t } = useTranslation();
const { isPc } = useSystem();
const [refreshTrigger, setRefreshTrigger] = useState(false);
const [feedbackType, setFeedbackType] = useState<'all' | 'has_feedback' | 'good' | 'bad'>('all');
const [unreadOnly, setUnreadOnly] = useState<boolean>(false);
const resetVariables = useContextSelector(ChatItemContext, (v) => v.resetVariables);
const setChatBoxData = useContextSelector(ChatItemContext, (v) => v.setChatBoxData);
const pluginRunTab = useContextSelector(ChatItemContext, (v) => v.pluginRunTab);
......@@ -67,6 +82,15 @@ const DetailLogsModal = ({ appId, chatId, onClose }: Props) => {
}
);
const handleScrollToChatItem = React.useCallback((dataId: string) => {
setTimeout(() => {
const element = document.querySelector(`[data-chat-id="${dataId}"]`);
if (element) {
element.scrollIntoView({ behavior: 'smooth', block: 'center' });
}
}, 100);
}, []);
const title = chat?.title;
const chatModels = chat?.app?.chatModels;
const isPlugin = chat?.app.type === AppTypeEnum.workflowTool;
......@@ -152,45 +176,66 @@ const DetailLogsModal = ({ appId, chatId, onClose }: Props) => {
)}
{/* Chat container */}
<Flex pt={2} flex={'1 0 0'} h={0}>
<Box flex={'1 0 0'} h={'100%'} overflow={'auto'}>
{isPlugin ? (
<Box px={5} py={2}>
<PluginRunBox appId={appId} chatId={chatId} />
<Flex pt={2} flex={'1 0 0'} h={0} flexDirection={'column'}>
<Flex flex={'1 0 0'} h={0}>
<Box flex={'1 0 0'} h={'100%'} overflow={'auto'}>
{isPlugin ? (
<Box px={5} py={2}>
<PluginRunBox appId={appId} chatId={chatId} />
</Box>
) : (
<ChatBox
isReady
appId={appId}
chatId={chatId}
feedbackType={'admin'}
showMarkIcon
showVoiceIcon={false}
chatType={ChatTypeEnum.log}
onTriggerRefresh={() => setRefreshTrigger((prev) => !prev)}
/>
)}
</Box>
{datasetCiteData && (
<Box
flex={'1 0 0'}
w={0}
mr={4}
maxW={'460px'}
h={'98%'}
bg={'white'}
boxShadow={
'0px 4px 10px 0px rgba(19, 51, 107, 0.10), 0px 0px 1px 0px rgba(19, 51, 107, 0.10)'
}
borderRadius={'md'}
>
<ChatQuoteList
rawSearch={datasetCiteData.rawSearch}
metadata={datasetCiteData.metadata}
onClose={() => setCiteModalData(undefined)}
/>
</Box>
) : (
<ChatBox
isReady
appId={appId}
chatId={chatId}
feedbackType={'admin'}
showMarkIcon
showVoiceIcon={false}
chatType={ChatTypeEnum.log}
/>
)}
</Box>
{datasetCiteData && (
<Box
flex={'1 0 0'}
w={0}
mr={4}
maxW={'460px'}
h={'98%'}
bg={'white'}
boxShadow={
'0px 4px 10px 0px rgba(19, 51, 107, 0.10), 0px 0px 1px 0px rgba(19, 51, 107, 0.10)'
}
borderRadius={'md'}
>
<ChatQuoteList
rawSearch={datasetCiteData.rawSearch}
metadata={datasetCiteData.metadata}
onClose={() => setCiteModalData(undefined)}
/>
</Box>
)}
</Flex>
{/* Feedback filter bar - commented out, moved to Render component */}
<Flex bg="white" px={6} py={3} borderTop="1px solid" borderColor="gray.200">
<DetailLogsModalFeedbackTypeFilter
feedbackType={feedbackType}
setFeedbackType={setFeedbackType}
unreadOnly={unreadOnly}
setUnreadOnly={setUnreadOnly}
appId={appId}
chatId={chatId}
currentRecordId={feedbackRecordId}
onRecordChange={handleRecordChange}
menuButtonProps={{
color: 'myGray.700',
_active: {}
}}
/>
</Flex>
</Flex>
</MyBox>
......@@ -201,6 +246,8 @@ const DetailLogsModal = ({ appId, chatId, onClose }: Props) => {
const Render = (props: Props) => {
const { appId, chatId } = props;
const [feedbackRecordId, setFeedbackRecordId] = useState<string | undefined>(undefined);
const params = useMemo(() => {
return {
chatId,
......@@ -210,6 +257,10 @@ const Render = (props: Props) => {
};
}, [appId, chatId]);
const handleRecordChange = useCallback((recordId: string | undefined) => {
setFeedbackRecordId(recordId);
}, []);
return (
<ChatItemContextProvider
showRouteToDatasetDetail={true}
......@@ -218,8 +269,12 @@ const Render = (props: Props) => {
// isShowFullText={true}
showNodeStatus
>
<ChatRecordContextProvider params={params}>
<DetailLogsModal {...props} />
<ChatRecordContextProvider params={params} feedbackRecordId={feedbackRecordId}>
<DetailLogsModal
{...props}
feedbackRecordId={feedbackRecordId}
handleRecordChange={handleRecordChange}
/>
</ChatRecordContextProvider>
</ChatItemContextProvider>
);
......
import React, { useCallback, useEffect } from 'react';
import type { ButtonProps, PlacementWithLogical } from '@chakra-ui/react';
import {
Checkbox,
Menu,
MenuButton,
MenuList,
MenuItem,
Divider,
Flex,
Box,
Button,
useDisclosure
} from '@chakra-ui/react';
import { useTranslation } from 'next-i18next';
import MyIcon from '@fastgpt/web/components/common/Icon';
import { useRequest2 } from '@fastgpt/web/hooks/useRequest';
import { getFeedbackRecordIds } from '@/web/core/chat/feedback/api';
import { eventBus, EventNameEnum } from '@/web/common/utils/eventbus';
type FilterProps = {
feedbackType: 'all' | 'has_feedback' | 'good' | 'bad';
setFeedbackType: (feedbackType: 'all' | 'has_feedback' | 'good' | 'bad') => void;
unreadOnly: boolean;
setUnreadOnly: (unreadOnly: boolean) => void;
menuButtonProps?: ButtonProps;
placement?: PlacementWithLogical;
};
const FeedbackTypeFilter = ({
feedbackType,
setFeedbackType,
unreadOnly,
setUnreadOnly,
menuButtonProps,
placement
}: FilterProps) => {
const { t } = useTranslation();
const { isOpen, onOpen, onClose } = useDisclosure();
const feedbackOptions = [
{
value: 'all' as const,
label: t('app:logs_all_records')
},
{
value: 'has_feedback' as const,
label: t('app:logs_has_any_feedback')
},
{
value: 'good' as const,
label: t('app:logs_has_good_feedback')
},
{
value: 'bad' as const,
label: t('app:logs_has_bad_feedback')
}
];
return (
<Menu
isOpen={isOpen}
onOpen={onOpen}
onClose={onClose}
closeOnSelect={false}
strategy={'fixed'}
autoSelect={false}
placement={placement}
>
<MenuButton
as={Button}
variant={'grayGhost'}
size="sm"
rightIcon={<MyIcon name={'core/chat/chevronDown'} w={4} />}
fontWeight={'normal'}
{...menuButtonProps}
>
{feedbackType === 'all'
? t('app:logs_keys_feedback')
: feedbackOptions.find((option) => option.value === feedbackType)?.label}
</MenuButton>
<MenuList
minW={'120px'}
w={'120px'}
px={'6px'}
py={'6px'}
border={'1px solid #fff'}
boxShadow={
'0px 4px 10px 0px rgba(19, 51, 107, 0.10), 0px 0px 1px 0px rgba(19, 51, 107, 0.10)'
}
zIndex={99}
>
{/* Radio options */}
{feedbackOptions.map((option) => (
<MenuItem
key={option.value}
borderRadius="sm"
py={2}
px={3}
fontSize={'sm'}
fontWeight={'normal'}
color={feedbackType === option.value ? 'primary.600' : 'myGray.900'}
bg={feedbackType === option.value ? 'primary.50' : 'transparent'}
_hover={{ bg: 'myGray.100' }}
onClick={(e) => {
e.stopPropagation();
e.preventDefault();
// When switching from "all" to other options, keep unreadOnly state
// When switching to "all", unreadOnly state is preserved but ignored in query
setFeedbackType(option.value);
// Don't close the menu - allow user to continue selecting checkbox
}}
>
<Flex alignItems={'center'} gap={2}>
<Box
w={'18px'}
h={'18px'}
borderWidth={'2.4px'}
borderColor={feedbackType === option.value ? 'primary.015' : 'transparent'}
borderRadius={'50%'}
>
<Flex
w={'100%'}
h={'100%'}
borderWidth={'1px'}
borderColor={feedbackType === option.value ? 'primary.600' : 'borderColor.high'}
bg={feedbackType === option.value ? 'primary.1' : 'transparent'}
borderRadius={'50%'}
alignItems={'center'}
justifyContent={'center'}
>
<Box
w={'5px'}
h={'5px'}
borderRadius={'50%'}
bg={feedbackType === option.value ? 'primary.600' : 'transparent'}
/>
</Flex>
</Box>
{option.label}
</Flex>
</MenuItem>
))}
{/* Divider + Checkbox (only show when feedbackType is not "all") */}
{feedbackType !== 'all' && (
<>
<Divider my={2} borderColor="gray.200" />
<MenuItem
borderRadius="sm"
py={2}
_hover={{ bg: 'myGray.100' }}
onClick={(e) => {
e.stopPropagation();
e.preventDefault();
setUnreadOnly(!unreadOnly);
}}
autoFocus={false}
>
<Checkbox isChecked={unreadOnly} size="sm" colorScheme="primary" pointerEvents="none">
<Box fontSize={'sm'} fontWeight={'normal'} ml={0.5} whiteSpace={'nowrap'}>
{t('app:logs_unread_only')}
</Box>
</Checkbox>
</MenuItem>
</>
)}
</MenuList>
</Menu>
);
};
export default FeedbackTypeFilter;
// Enhanced FeedbackTypeFilter with navigation for DetailLogsModal
export const DetailLogsModalFeedbackTypeFilter = ({
feedbackType,
setFeedbackType,
unreadOnly,
setUnreadOnly,
menuButtonProps,
appId,
chatId,
currentRecordId,
onRecordChange
}: FilterProps & {
appId?: string;
chatId?: string;
currentRecordId?: string;
onRecordChange?: (recordId: string | undefined) => void;
}) => {
const { t } = useTranslation();
// Get feedback record IDs when in feedback mode
const { data: feedbackRecords, runAsync: loadFeedbackRecords } = useRequest2(
async (_feedbackType = feedbackType, _unreadOnly = unreadOnly) => {
if (!appId || !chatId || _feedbackType === 'all') return null;
return await getFeedbackRecordIds({
appId,
chatId,
feedbackType: _feedbackType,
unreadOnly: _unreadOnly
});
},
{
manual: false,
refreshDeps: [appId, chatId]
}
);
// Calculate current position
const currentIndex = feedbackRecords?.dataIds.findIndex((id) => id === currentRecordId) ?? -1;
const currentPosition = currentIndex >= 0 ? currentIndex + 1 : 0;
const totalCount = feedbackRecords?.total ?? 0;
// Handle feedback type change
const handleFeedbackTypeChange = useCallback(
(type: 'all' | 'has_feedback' | 'good' | 'bad') => {
setFeedbackType(type);
if (type === 'all') {
// Switch to all records - no feedbackRecordId
loadFeedbackRecords(type);
} else {
loadFeedbackRecords(type).then((records) => {
if (!records) return;
// Select the last (latest) feedback record
const lastRecordId = records.dataIds[records.dataIds.length - 1];
onRecordChange?.(lastRecordId);
});
}
},
[setFeedbackType, onRecordChange, loadFeedbackRecords]
);
const handleUnreadOnlyChange = useCallback(
(unreadOnly: boolean) => {
setUnreadOnly(unreadOnly);
loadFeedbackRecords(feedbackType, unreadOnly).then((records) => {
if (!records) return;
// Select the last (latest) feedback record
const lastRecordId = records.dataIds[records.dataIds.length - 1];
onRecordChange?.(lastRecordId);
});
},
[setUnreadOnly, loadFeedbackRecords, feedbackType, onRecordChange]
);
// Navigation handlers
const handlePrev = useCallback(() => {
if (!feedbackRecords) return;
if (currentIndex > 0) {
onRecordChange?.(feedbackRecords.dataIds[currentIndex - 1]);
} else {
onRecordChange?.(feedbackRecords.dataIds[feedbackRecords.dataIds.length - 1]);
}
}, [feedbackRecords, currentIndex, onRecordChange]);
const handleNext = useCallback(() => {
if (!feedbackRecords) return;
if (currentIndex < (feedbackRecords?.dataIds.length ?? 0) - 1) {
onRecordChange?.(feedbackRecords.dataIds[currentIndex + 1]);
} else {
onRecordChange?.(feedbackRecords.dataIds[0]);
}
}, [feedbackRecords, currentIndex, onRecordChange]);
const showNavigation = appId && chatId && feedbackType !== 'all';
useEffect(() => {
eventBus.on(EventNameEnum.refreshFeedback, () => {
loadFeedbackRecords();
});
return () => {
eventBus.off(EventNameEnum.refreshFeedback);
};
}, []);
return (
<Flex alignItems={'center'} gap={3} w={'100%'}>
<FeedbackTypeFilter
feedbackType={feedbackType}
setFeedbackType={handleFeedbackTypeChange}
unreadOnly={unreadOnly}
setUnreadOnly={handleUnreadOnlyChange}
menuButtonProps={menuButtonProps}
/>
{showNavigation && (
<>
{/* Current position indicator */}
<Box fontSize={'sm'} color={'myGray.600'} whiteSpace={'nowrap'} flex={1}>
{currentPosition}/{totalCount}
</Box>
{/* Previous button */}
<Button size="sm" w={'100px'} variant={'whiteBase'} onClick={handlePrev}>
{t('chat:Previous')}
</Button>
<Button size="sm" w={'100px'} variant={'whiteBase'} onClick={handleNext}>
{t('chat:Next')}
</Button>
</>
)}
</Flex>
);
};
......@@ -3,11 +3,13 @@ import { Box, Button, Flex } from '@chakra-ui/react';
import MyIcon from '@fastgpt/web/components/common/Icon';
import { useTranslation } from 'next-i18next';
import React from 'react';
import type { updateLogKeysBody } from '@/pages/api/core/app/logs/updateLogKeys';
import { useRequest2 } from '@fastgpt/web/hooks/useRequest';
import { updateLogKeys } from '@/web/core/app/api/log';
import type { AppLogKeysType } from '@fastgpt/global/core/app/logs/type';
import type { getLogKeysResponse } from '@/pages/api/core/app/logs/getLogKeys';
import type {
getLogKeysResponseType,
updateLogKeysBody
} from '@fastgpt/global/openapi/core/app/log/api';
import type { SetState } from 'ahooks/lib/createUseStorageState';
const SyncLogKeysPopover = ({
......@@ -20,7 +22,7 @@ const SyncLogKeysPopover = ({
logKeys: AppLogKeysType[];
setLogKeys: (value: SetState<AppLogKeysType[]>) => void;
teamLogKeys: AppLogKeysType[];
fetchLogKeys: () => Promise<getLogKeysResponse>;
fetchLogKeys: () => Promise<getLogKeysResponseType>;
appId: string;
}) => {
const { t } = useTranslation();
......
......@@ -33,8 +33,10 @@ const WorkflowEdit = () => {
useMount(() => {
if (!isV2Workflow) {
openConfirm(() => {
initData(JSON.parse(JSON.stringify(v1Workflow2V2((appDetail.modules || []) as any))));
openConfirm({
onConfirm: () => {
initData(JSON.parse(JSON.stringify(v1Workflow2V2((appDetail.modules || []) as any))));
}
})();
} else {
initData(
......
......@@ -194,15 +194,17 @@ const Share = ({ appId }: { appId: string; type: PublishChannelEnum }) => {
icon: 'delete',
type: 'danger',
onClick: () =>
openConfirm(async () => {
setIsLoading(true);
try {
await delShareChatById(item._id);
refetchShareChatList();
} catch (error) {
console.log(error);
openConfirm({
onConfirm: async () => {
setIsLoading(true);
try {
await delShareChatById(item._id);
refetchShareChatList();
} catch (error) {
console.log(error);
}
setIsLoading(false);
}
setIsLoading(false);
})()
}
]
......
......@@ -105,7 +105,10 @@ const AppCard = ({
variant={'whitePrimary'}
leftIcon={<MyIcon name={'core/chat/chatLight'} w={'16px'} />}
onClick={() =>
router.push(`/chat?appId=${appId}&pane=${ChatSidebarPaneEnum.RECENTLY_USED_APPS}`)
window.open(
`/chat?appId=${appId}&pane=${ChatSidebarPaneEnum.RECENTLY_USED_APPS}`,
'_blank'
)
}
>
{t('common:core.Chat')}
......
......@@ -82,7 +82,7 @@ const SimpleEdit = () => {
nodes: appDetail.modules,
chatConfig: {
...appDetail.chatConfig,
fileSelectConfig: appDetail.chatConfig.fileSelectConfig || {
fileSelectConfig: appDetail.chatConfig?.fileSelectConfig || {
...defaultAppSelectFileConfig,
canSelectFile: true
}
......
......@@ -36,8 +36,13 @@ const WorkflowEdit = () => {
useMount(() => {
if (!isV2Workflow) {
openConfirm(() => {
initData(JSON.parse(JSON.stringify(v1Workflow2V2((appDetail.modules || []) as any))), true);
openConfirm({
onConfirm: () => {
initData(
JSON.parse(JSON.stringify(v1Workflow2V2((appDetail.modules || []) as any))),
true
);
}
})();
} else {
initData(
......
......@@ -64,23 +64,25 @@ const NodeCode = ({ data, selected }: NodeProps<FlowNodeItemType>) => {
]}
value={codeType?.value}
onChange={(newLang) => {
openSwitchLangConfirm(() => {
onChangeNode({
nodeId,
type: 'updateInput',
key: NodeInputKeyEnum.codeType,
value: { ...codeType, value: newLang }
});
openSwitchLangConfirm({
onConfirm: () => {
onChangeNode({
nodeId,
type: 'updateInput',
key: NodeInputKeyEnum.codeType,
value: { ...codeType, value: newLang }
});
onChangeNode({
nodeId,
type: 'updateInput',
key: item.key,
value: {
...item,
value: SANDBOX_CODE_TEMPLATE[newLang]
}
});
onChangeNode({
nodeId,
type: 'updateInput',
key: item.key,
value: {
...item,
value: SANDBOX_CODE_TEMPLATE[newLang]
}
});
}
})();
}}
/>
......
......@@ -22,7 +22,7 @@ import {
stringConditionList
} from '@fastgpt/global/core/workflow/template/system/ifElse/constant';
import { useContextSelector } from 'use-context-selector';
import React, { useMemo } from 'react';
import React, { useCallback, useMemo } from 'react';
import { WorkflowBufferDataContext } from '../../../context/workflowInitContext';
import MySelect from '@fastgpt/web/components/common/MySelect';
import MyInput from '@/components/MyInput';
......@@ -452,10 +452,6 @@ const ConditionValueInput = ({
return output?.valueType;
}
}, [globalVariables, getNodeById, variable]);
const { referenceList } = useReference({
nodeId,
valueType
});
const showBooleanSelect = useMemo(() => {
return (
......@@ -473,6 +469,45 @@ const ConditionValueInput = ({
);
}, [condition, valueType]);
// Get array element type for include/notInclude operations
const getArrayElementType = useCallback((arrayType: WorkflowIOValueTypeEnum) => {
const typeMap: Record<string, WorkflowIOValueTypeEnum> = {
[WorkflowIOValueTypeEnum.arrayString]: WorkflowIOValueTypeEnum.string,
[WorkflowIOValueTypeEnum.arrayNumber]: WorkflowIOValueTypeEnum.number,
[WorkflowIOValueTypeEnum.arrayBoolean]: WorkflowIOValueTypeEnum.boolean,
[WorkflowIOValueTypeEnum.arrayObject]: WorkflowIOValueTypeEnum.object,
[WorkflowIOValueTypeEnum.arrayAny]: WorkflowIOValueTypeEnum.any
};
return typeMap[arrayType] || arrayType;
}, []);
// Adjust reference value type based on condition
const referenceValueType = useMemo(() => {
if (!valueType?.includes('array') || !condition) {
return valueType;
}
// Array length operations need number type
if (renderNumberConditionList.has(condition)) {
return WorkflowIOValueTypeEnum.number;
}
// Array include/notInclude operations need element type
if (
condition === VariableConditionEnum.include ||
condition === VariableConditionEnum.notInclude
) {
return getArrayElementType(valueType);
}
return valueType;
}, [condition, valueType, getArrayElementType]);
const { referenceList } = useReference({
nodeId,
valueType: referenceValueType
});
const RenderInput = useMemo(() => {
if (showBooleanSelect) {
return (
......
......@@ -207,11 +207,10 @@ const AppContextProvider = ({ children }: { children: ReactNode }) => {
);
const onDelApp = useCallback(
() =>
openConfirmDel(
deleteApp,
undefined,
t('app:confirm_del_app_tip', { name: appDetail.name })
)(),
openConfirmDel({
onConfirm: deleteApp,
customContent: t('app:confirm_del_app_tip', { name: appDetail.name })
})(),
[appDetail.name, deleteApp, openConfirmDel, t]
);
......
......@@ -49,7 +49,12 @@ const CollectionReader = ({
const filterResults = useMemo(() => {
const res = rawSearch
.filter((item) => item.collectionId === collectionId)
.sort((a, b) => (a.chunkIndex || 0) - (b.chunkIndex || 0));
.sort((a, b) => {
const chunkDiff = (a.chunkIndex || 0) - (b.chunkIndex || 0);
if (chunkDiff !== 0) return chunkDiff;
return a.id.localeCompare(b.id);
});
if (quoteId) {
setQuoteIndex(res.findIndex((item) => item.id === quoteId));
......@@ -65,7 +70,7 @@ const CollectionReader = ({
if (item) {
return {
id: item.id,
index: item.chunkIndex,
anchor: item.chunkIndex,
score: item.score
};
}
......
......@@ -65,8 +65,10 @@ const EditableTagItem = React.memo(function EditableTagItem({
});
const handleConfirmDelete = useCallback(() => {
openConfirm(() => {
onConfirmDelete(tag);
openConfirm({
onConfirm: () => {
onConfirmDelete(tag);
}
})();
}, [openConfirm, onConfirmDelete, tag]);
......
......@@ -5,13 +5,16 @@ import { ChatSourceEnum } from '@fastgpt/global/core/chat/constants';
import type { DateRangeType } from '@fastgpt/web/components/common/DateRangePicker';
import { useMultipleSelect } from '@fastgpt/web/components/common/MySelect/MultipleSelect';
import { addDays } from 'date-fns';
import React, { useState } from 'react';
import React, { useMemo, useState } from 'react';
import { useContextSelector } from 'use-context-selector';
type Props = {
Header: React.FC<{ children?: React.ReactNode }>;
};
// Cache the chat source enum values to avoid creating new array on every render
const chatSourceValues = Object.values(ChatSourceEnum);
const LogDetails = ({ Header }: Props) => {
const appId = useContextSelector(ChatSettingContext, (v) => v.chatSettings?.appId || '');
......@@ -25,12 +28,11 @@ const LogDetails = ({ Header }: Props) => {
setValue: setChatSources,
isSelectAll: isSelectAllSource,
setIsSelectAll: setIsSelectAllSource
} = useMultipleSelect<ChatSourceEnum>(Object.values(ChatSourceEnum), true);
} = useMultipleSelect<ChatSourceEnum>(chatSourceValues, true);
return (
<Flex gap={'13px'} flexDir="column" h={['calc(100vh - 69px)', 'full']}>
<Header />
<LogTable
px={[2, 0]}
showSourceSelector={false}
......
......@@ -172,7 +172,6 @@ const AppChatWindow = ({ myApps }: Props) => {
/>
) : (
<ChatBox
showEmptyIntro
appId={appId}
chatId={chatId}
isReady={!loading}
......
import React from 'react';
import { Card, Box, Flex } from '@chakra-ui/react';
import { useMarkdown } from '@/web/common/hooks/useMarkdown';
import dynamic from 'next/dynamic';
const Markdown = dynamic(() => import('@/components/Markdown'));
const Avatar = dynamic(() => import('@fastgpt/web/components/common/Avatar'));
const Empty = ({
showChatProblem,
model: { name, intro, avatar }
}: {
showChatProblem: boolean;
model: {
name: string;
intro: string;
avatar: string;
};
}) => {
const { data: chatProblem } = useMarkdown({ url: '/chatProblem.md' });
const { data: versionIntro } = useMarkdown({ url: '/versionIntro.md' });
return (
<Box
minH={'100%'}
w={'85%'}
maxW={'600px'}
m={'auto'}
py={'5vh'}
alignItems={'center'}
justifyContent={'center'}
>
{name && (
<Card p={4} mb={10}>
<Flex mb={2} alignItems={'center'} justifyContent={'center'}>
<Avatar src={avatar} w={'32px'} h={'32px'} />
<Box ml={3} fontSize={'3xl'} fontWeight={'bold'}>
{name}
</Box>
</Flex>
<Box whiteSpace={'pre-line'}>{intro}</Box>
</Card>
)}
{showChatProblem && (
<>
{/* version intro */}
<Card p={4} mb={10}>
<Markdown source={versionIntro} />
</Card>
<Card p={4}>
<Markdown source={chatProblem} />
</Card>
</>
)}
</Box>
);
};
export default Empty;
......@@ -46,7 +46,7 @@ const UserAvatarPopover = ({
{({ onClose }) => {
const onLogout = useCallback(() => {
onClose();
openConfirm(handleLogout)();
openConfirm({ onConfirm: handleLogout })();
}, [onClose]);
return (
......
......@@ -96,7 +96,7 @@ const List = () => {
borderColor: 'primary.600'
},
onDrop: (dragId: string, targetId: string) => {
openMoveConfirm(async () => onPutAppById(dragId, { parentId: targetId }))();
openMoveConfirm({ onConfirm: async () => onPutAppById(dragId, { parentId: targetId }) })();
}
});
......@@ -397,7 +397,9 @@ const List = () => {
type: 'grayBg' as MenuItemType,
label: t('app:copy_one_app'),
onClick: () =>
openConfirmCopy(() => onclickCopy({ appId: app._id }))()
openConfirmCopy({
onConfirm: () => onclickCopy({ appId: app._id })
})()
}
]
}
......@@ -411,13 +413,14 @@ const List = () => {
icon: 'delete',
label: t('common:Delete'),
onClick: () =>
openConfirmDel(
() => onclickDelApp(app._id),
undefined,
app.type === AppTypeEnum.folder
? t('app:confirm_delete_folder_tip')
: t('app:confirm_del_app_tip', { name: app.name })
)()
openConfirmDel({
onConfirm: () => onclickDelApp(app._id),
inputConfirmText: app.name,
customContent:
app.type === AppTypeEnum.folder
? t('app:confirm_delete_folder_tip')
: t('app:confirm_del_app_tip')
})()
}
]
}
......@@ -499,8 +502,11 @@ const CreateButton = ({ appType }: { appType: AppTypeEnum | 'all' }) => {
const router = useRouter();
const parentId = router.query.parentId;
const createAppType =
createAppTypeMap[appType as CreateAppType]?.type ||
(router.pathname.includes('/agent') ? AppTypeEnum.workflow : AppTypeEnum.workflowTool);
appType !== 'all' && appType in createAppTypeMap
? createAppTypeMap[appType as keyof typeof createAppTypeMap].type
: router.pathname.includes('/agent')
? AppTypeEnum.workflow
: AppTypeEnum.workflowTool;
const isToolType = ToolTypeList.includes(createAppType);
return (
......@@ -571,8 +577,11 @@ const ListCreateButton = ({ appType }: { appType: AppTypeEnum | 'all' }) => {
const router = useRouter();
const parentId = router.query.parentId;
const createAppType =
createAppTypeMap[appType as CreateAppType]?.type ||
(router.pathname.includes('/agent') ? AppTypeEnum.workflow : AppTypeEnum.workflowTool);
appType !== 'all' && appType in createAppTypeMap
? createAppTypeMap[appType as keyof typeof createAppTypeMap].type
: router.pathname.includes('/agent')
? AppTypeEnum.workflow
: AppTypeEnum.workflowTool;
return (
<MyBox
......
import { useConfirm } from '@fastgpt/web/hooks/useConfirm';
import { type Dispatch, type ReactNode, type SetStateAction, useState } from 'react';
import {
type Dispatch,
type ReactNode,
type SetStateAction,
useState,
useMemo,
useCallback
} from 'react';
import { useTranslation } from 'next-i18next';
import { createContext, useContextSelector } from 'use-context-selector';
import { DatasetTypeEnum } from '@fastgpt/global/core/dataset/constants';
......@@ -95,7 +102,7 @@ const CollectionPageContextProvider = ({ children }: { children: ReactNode }) =>
refreshDeps: [parentId, searchText, filterTags]
});
const syncDataset = async () => {
const syncDataset = useCallback(async () => {
if (datasetDetail.type === DatasetTypeEnum.websiteDataset) {
await checkTeamWebSyncLimit();
}
......@@ -110,7 +117,7 @@ const CollectionPageContextProvider = ({ children }: { children: ReactNode }) =>
status: 'success',
title: t('dataset:collection.sync.submit')
});
};
}, [datasetDetail.type, datasetId, getData, loadDatasetDetail, pageNum, t, toast]);
// dataset sync confirm
const { openConfirm: openDatasetSyncConfirm, ConfirmModal: ConfirmDatasetSyncModal } = useConfirm(
......@@ -141,22 +148,38 @@ const CollectionPageContextProvider = ({ children }: { children: ReactNode }) =>
}
);
const contextValue: CollectionPageContextType = {
openDatasetSyncConfirm: openDatasetSyncConfirm(syncDataset),
onOpenWebsiteModal,
const contextValue: CollectionPageContextType = useMemo(
() => ({
openDatasetSyncConfirm: openDatasetSyncConfirm({ onConfirm: syncDataset }),
onOpenWebsiteModal,
searchText,
setSearchText,
filterTags,
setFilterTags,
collections,
Pagination,
total,
getData,
isGetting,
pageNum,
pageSize
};
searchText,
setSearchText,
filterTags,
setFilterTags,
collections,
Pagination,
total,
getData,
isGetting,
pageNum,
pageSize
}),
[
Pagination,
collections,
filterTags,
getData,
isGetting,
onOpenWebsiteModal,
openDatasetSyncConfirm,
pageNum,
pageSize,
searchText,
syncDataset,
total
]
);
return (
<CollectionPageContext.Provider value={contextValue}>
......
......@@ -372,8 +372,10 @@ const CollectionCard = () => {
</Flex>
),
onClick: () =>
openSyncConfirm(() => {
onclickStartSync(collection._id);
openSyncConfirm({
onConfirm: () => {
onclickStartSync(collection._id);
}
})()
}
]
......@@ -423,13 +425,15 @@ const CollectionCard = () => {
),
type: 'danger',
onClick: () =>
openDeleteConfirm(
() => onDelCollection([collection._id]),
undefined,
collection.type === DatasetCollectionTypeEnum.folder
? t('common:dataset.collections.Confirm to delete the folder')
: t('common:dataset.Confirm to delete the file')
)()
openDeleteConfirm({
onConfirm: () => onDelCollection([collection._id]),
customContent:
collection.type === DatasetCollectionTypeEnum.folder
? t(
'common:dataset.collections.Confirm to delete the folder'
)
: t('common:dataset.Confirm to delete the file')
})()
}
]
}
......@@ -451,16 +455,15 @@ const CollectionCard = () => {
<Button
variant={'whiteBase'}
onClick={() =>
openDeleteConfirm(
() =>
openDeleteConfirm({
onConfirm: () =>
onDelCollection(selectedItems.map((e) => e._id)).then(() =>
setSelectedItems([])
),
undefined,
t('dataset:confirm_delete_collection', {
customContent: t('dataset:confirm_delete_collection', {
num: selectedItems.length
})
)()
})()
}
>
{t('dataset:batch_delete')}
......
......@@ -195,9 +195,11 @@ const Info = ({ datasetId }: { datasetId: string }) => {
onChange={(e) => {
const vectorModel = embeddingModelList.find((item) => item.model === e);
if (!vectorModel) return;
return onOpenConfirmRebuild(async () => {
await onRebuilding(vectorModel);
setValue('vectorModel', vectorModel);
return onOpenConfirmRebuild({
onConfirm: async () => {
await onRebuilding(vectorModel);
setValue('vectorModel', vectorModel);
}
})();
}}
/>
......@@ -264,16 +266,15 @@ const Info = ({ datasetId }: { datasetId: string }) => {
const autoSync = e.target.checked;
const text = autoSync ? t('dataset:open_auto_sync') : t('dataset:close_auto_sync');
onOpenConfirmSyncSchedule(
async () => {
onOpenConfirmSyncSchedule({
onConfirm: async () => {
return updateDataset({
id: datasetId,
autoSync
});
},
undefined,
text
)();
customContent: text
})();
}}
/>
</Flex>
......
......@@ -8,8 +8,7 @@ import { useRouter } from 'next/router';
import PermissionIconText from '@/components/support/permission/IconText';
import Avatar from '@fastgpt/web/components/common/Avatar';
import { useConfirm } from '@fastgpt/web/hooks/useConfirm';
import { useRequest, useRequest2 } from '@fastgpt/web/hooks/useRequest';
import { type DatasetItemType } from '@fastgpt/global/core/dataset/type';
import { useRequest2 } from '@fastgpt/web/hooks/useRequest';
import { useSystemStore } from '@/web/common/system/useSystemStore';
import { checkTeamExportDatasetLimit } from '@/web/support/user/team/api';
import { downloadFetch } from '@/web/common/system/utils';
......@@ -72,12 +71,13 @@ function List() {
borderColor: 'primary.600'
},
onDrop: (dragId: string, targetId: string) => {
openMoveConfirm(() =>
updateDataset({
id: dragId,
parentId: targetId
})
)();
openMoveConfirm({
onConfirm: () =>
updateDataset({
id: dragId,
parentId: targetId
})
})();
}
});
......@@ -86,22 +86,27 @@ function List() {
[editPerDatasetId, myDatasets]
);
const { mutate: exportDataset } = useRequest({
mutationFn: async (dataset: DatasetItemType) => {
setLoading(true);
await checkTeamExportDatasetLimit(dataset._id);
const { runAsync: exportDataset } = useRequest2(
async ({ _id, name }: { _id: string; name: string }) => {
await checkTeamExportDatasetLimit(_id);
await downloadFetch({
url: `/api/core/dataset/exportAll?datasetId=${dataset._id}`,
filename: `${dataset.name}.csv`
url: `/api/core/dataset/exportAll?datasetId=${_id}`,
filename: `${name}.csv`
});
},
onSettled() {
setLoading(false);
},
successToast: t('common:core.dataset.Start export'),
errorToast: t('common:dataset.Export Dataset Limit Error')
});
{
manual: true,
onBefore: () => {
setLoading(true);
},
onFinally() {
setLoading(false);
},
successToast: t('common:core.dataset.Start export'),
errorToast: t('common:dataset.Export Dataset Limit Error')
}
);
const DeleteTipsMap = useRef({
[DatasetTypeEnum.folder]: t('common:dataset.deleteFolderTips'),
......@@ -126,18 +131,6 @@ function List() {
type: 'delete'
});
const onClickDeleteDataset = (id: string) => {
openConfirm(
() =>
onDelDataset(id).then(() => {
refetchPaths();
loadMyDatasets();
}),
undefined,
DeleteTipsMap.current[DatasetTypeEnum.dataset]
)();
};
return (
<>
{formatDatasets.length > 0 && (
......@@ -375,7 +368,17 @@ function List() {
icon: 'delete',
label: t('common:Delete'),
type: 'danger' as 'danger',
onClick: () => onClickDeleteDataset(dataset._id)
onClick: () =>
openConfirm({
onConfirm: () =>
onDelDataset(dataset._id).then(() => {
refetchPaths();
loadMyDatasets();
}),
customContent:
DeleteTipsMap.current[DatasetTypeEnum.dataset],
inputConfirmText: dataset.name
})()
}
]
}
......
......@@ -7,9 +7,19 @@ import { webPushTrack } from '@/web/common/middle/tracks/utils';
import { useTranslation } from 'next-i18next';
import { errorLogger } from '@/web/common/utils/errorLogger';
import { useMount } from 'ahooks';
import type { I18nStringType } from '@fastgpt/global/common/i18n/type';
const errorText: I18nStringType = {
'zh-CN':
'出现未捕获的异常。\n1. 私有部署用户,90%是由于模型配置不正确/模型未启用导致。\n2. 部分系统不兼容相关API。大部分是苹果的safari 浏览器导致,可以尝试更换 chrome。\n3. 请关闭浏览器翻译功能,部分翻译导致页面崩溃。\n\n排除3后,打开控制台的 console 查看具体报错信息。\n如果提示 xxx undefined 的话,就是模型配置不正确,检查:\n1. 请确保系统内每个系列模型至少有一个可用,可以在【账号-模型提供商】中检查。\n2. 请确保至少有一个知识库文件处理模型(语言模型中有一个开关),否则知识库创建会报错。\n2. 检查模型中一些“对象”参数是否异常(数组和对象),如果为空,可以尝试给个空数组或空对象。',
en: `An uncaught exception occurred.\n\n1. For private deployment users, 90% of cases are caused by incorrect model configuration/model not enabled. \n.\n\n2. Some systems are not compatible with related APIs. \nMost of the time it's caused by Apple's Safari browser, you can try changing it to Chrome.\n\n3. Please turn off the browser translation function. Some translations may cause the page to crash.\n\n\nAfter eliminating 3, open the console to view the specific error information.\n\nIf it prompts xxx undefined, the model configuration is incorrect. Check:\n\n1. Please ensure that at least one model of each series is available in the system, which can be checked in [Account - Model Provider].\n\n2. Please ensure that there is at least one knowledge base file processing model (there is a switch in the language model), otherwise an error will be reported when creating the knowledge base.\n\n2. Check whether some \"object\" parameters in the model are abnormal (arrays and objects). If they are empty, you can try to give an empty array or empty object.`,
'zh-Hant':
'出現未捕獲的異常。\n\n1. 私有部署用戶,90%是由於模型配置不正確/模型未啟用導致。 \n。\n\n2. 部分系統不兼容相關API。\n大部分是蘋果的safari 瀏覽器導致,可以嘗試更換 chrome。\n\n3. 請關閉瀏覽器翻譯功能,部分翻譯導致頁面崩潰。\n\n\n排除3後,打開控制台的 console 查看具體報錯信息。\n\n如果提示 xxx undefined 的話,就是模型配置不正確,檢查:\n1. 請確保系統內每個系列模型至少有一個可用,可以在【賬號-模型提供商】中檢查。\n\n2. 請確保至少有一個知識庫文件處理模型(語言模型中有一個開關),否則知識庫創建會報錯。\n\n2. 檢查模型中一些“對象”參數是否異常(數組和對象),如果為空,可以嘗試給個空數組或空對象。'
};
function Error() {
const { t } = useTranslation();
const { t, i18n } = useTranslation();
const lang = i18n.language;
const router = useRouter();
const { toast } = useToast();
const { lastRoute, llmModelList, embeddingModelList } = useSystemStore();
......@@ -52,7 +62,7 @@ function Error() {
}, 2000);
});
return <Box whiteSpace={'pre-wrap'}>{t('common:page_error')}</Box>;
return <Box whiteSpace={'pre-wrap'}>{errorText[lang as keyof typeof errorText]}</Box>;
}
export async function getServerSideProps(context: any) {
......
......@@ -139,7 +139,9 @@ const CustomDomain = () => {
<Button
variant="whiteDanger"
onClick={() => {
return openConfirm(() => onDelete(customDomain.domain))();
return openConfirm({
onConfirm: () => onDelete(customDomain.domain)
})();
}}
>
{t('common:Delete')}
......
......@@ -15,7 +15,7 @@ import { useForm } from 'react-hook-form';
import { type UserUpdateParams } from '@/types/user';
import { useToast } from '@fastgpt/web/hooks/useToast';
import { useUserStore } from '@/web/support/user/useUserStore';
import type { UserType } from '@fastgpt/global/support/user/type.d';
import type { UserType } from '@fastgpt/global/support/user/type';
import dynamic from 'next/dynamic';
import { useSystemStore } from '@/web/common/system/useSystemStore';
import { useTranslation } from 'next-i18next';
......
import { NextAPI } from '@/service/middleware/entry';
import { addLog } from '@fastgpt/service/common/system/log';
import { authCert } from '@fastgpt/service/support/permission/auth/common';
import { MongoChatItem } from '@fastgpt/service/core/chat/chatItemSchema';
import { updateChatFeedbackCount } from '@fastgpt/service/core/chat/controller';
import { batchRun } from '@fastgpt/global/common/system/utils';
/**
* Initialize feedback flags for chat records
*
* Optimized strategy:
* 1. Create temporary indexes for migration
* 2. Aggregate all chats with feedback from chatItem (only ~1% of total)
* 3. Use batchRun to update feedback counts concurrently
* 4. Mark each chat as migrated
* 5. Drop temporary indexes after completion
*/
const CONCURRENCY = 10;
type ChatIdentifier = {
teamId: string;
appId: string;
chatId: string;
};
/**
* Create temporary indexes for migration performance
*/
async function createTemporaryIndexes(): Promise<void> {
addLog.info('Creating temporary indexes for migration...');
try {
// Create index on ChatItem for finding chats with good feedback
await MongoChatItem.collection.createIndex(
{ userGoodFeedback: 1, teamId: 1, appId: 1, chatId: 1 },
{
name: 'temp_feedback_migration_good',
partialFilterExpression: { userGoodFeedback: { $exists: true, $ne: null } }
} as any
);
// Create index on ChatItem for finding chats with bad feedback
await MongoChatItem.collection.createIndex(
{ userBadFeedback: 1, teamId: 1, appId: 1, chatId: 1 },
{
name: 'temp_feedback_migration_bad',
partialFilterExpression: { userBadFeedback: { $exists: true, $ne: null } }
} as any
);
addLog.info('Temporary indexes created successfully');
} catch (error: any) {
// Index might already exist, log warning but continue
addLog.warn('Error creating indexes (may already exist):', error);
}
}
/**
* Get all unique chats that have feedback
*/
async function getChatsWithFeedback(): Promise<ChatIdentifier[]> {
addLog.info('Aggregating chats with feedback from chatItems...');
const result = await MongoChatItem.aggregate<ChatIdentifier>([
{
$match: {
$or: [
{ userGoodFeedback: { $exists: true, $ne: null } },
{ userBadFeedback: { $exists: true, $ne: null } }
]
}
},
{
$group: {
_id: {
teamId: '$teamId',
appId: '$appId',
chatId: '$chatId'
}
}
},
{
$project: {
_id: 0,
teamId: { $toString: '$_id.teamId' },
appId: { $toString: '$_id.appId' },
chatId: '$_id.chatId'
}
}
]);
addLog.info(`Found ${result.length.toLocaleString()} unique chats with feedback`);
return result;
}
/**
* Main migration function
*/
export async function migrateFeedbackFlags() {
const startTime = Date.now();
addLog.info('========================================');
addLog.info('Starting feedback flags migration');
addLog.info(`Concurrency: ${CONCURRENCY}`);
addLog.info('========================================');
// Step 1: Create temporary indexes
addLog.info('Creating temporary indexes for migration...');
await createTemporaryIndexes();
// Step 2: Get all chats with feedback
const chats = await getChatsWithFeedback();
if (chats.length === 0) {
addLog.info('No chats with feedback found');
return {
total: 0,
succeeded: 0,
failed: 0,
duration: 0
};
}
// Step 3: Process all chats using batchRun
addLog.info(`Processing ${chats.length.toLocaleString()} chats...`);
let succeeded = 0;
let failed = 0;
await batchRun(
chats,
async (chat) => {
try {
await updateChatFeedbackCount({
appId: chat.appId,
chatId: chat.chatId
});
succeeded++;
// Log progress every 1000 chats
if (succeeded % 1000 === 0) {
addLog.info(`Progress: ${succeeded.toLocaleString()} / ${chats.length.toLocaleString()}`);
}
} catch (error) {
failed++;
addLog.error(
`Failed to process chat ${chat.chatId}:`,
error instanceof Error ? error.message : String(error)
);
}
},
CONCURRENCY
);
const duration = Date.now() - startTime;
const durationMinutes = (duration / 1000 / 60).toFixed(2);
addLog.info('========================================');
addLog.info('Migration completed!');
addLog.info(`Total: ${chats.length.toLocaleString()}`);
addLog.info(`Succeeded: ${succeeded.toLocaleString()}`);
addLog.info(`Failed: ${failed.toLocaleString()}`);
addLog.info(`Duration: ${durationMinutes} minutes`);
addLog.info(`Average: ${(duration / chats.length).toFixed(0)}ms per chat`);
addLog.info('========================================');
return {
total: chats.length,
succeeded,
failed,
duration
};
}
export default NextAPI(async function handler(req, res) {
await authCert({ req, authRoot: true });
const result = await migrateFeedbackFlags();
return result;
});
......@@ -21,6 +21,7 @@ import {
truncateFilename
} from '@fastgpt/service/common/s3/utils';
import { connectionMongo, Types } from '@fastgpt/service/common/mongo';
import { migrateFeedbackFlags } from './initFeedbackFlags';
// 将 GridFS 的流转换为 Buffer
async function gridFSStreamToBuffer(
......@@ -977,6 +978,9 @@ async function handler(req: NextApiRequest, _res: NextApiResponse) {
addLog.info(`[Migration ${batchId}] Converted fileId: ${converted}`);
addLog.info(`[Migration ${batchId}] =======================================`);
// 重新统计每一个 chat 的反馈情况
await migrateFeedbackFlags();
return {
batchId,
migrationVersion,
......
......@@ -71,7 +71,7 @@ export default NextAPI(handler);
const testLLMModel = async (model: LLMModelItemType, headers: Record<string, string>) => {
const { answerText } = await createLLMResponse({
body: {
model,
model, // 传递实体 model 进去,保障底层不会去拿内存里的实体。
messages: [{ role: 'user', content: 'hi' }],
stream: true
},
......
......@@ -6,7 +6,6 @@ import dayjs from 'dayjs';
import { type ApiRequestProps } from '@fastgpt/service/type/next';
import { replaceRegChars } from '@fastgpt/global/common/string/tools';
import { NextAPI } from '@/service/middleware/entry';
import type { GetAppChatLogsProps } from '@/global/core/api/appReq';
import { authApp } from '@fastgpt/service/support/permission/app/auth';
import { Types } from '@fastgpt/service/common/mongo';
import { MongoChat } from '@fastgpt/service/core/chat/chatSchema';
......@@ -27,6 +26,8 @@ import { VariableInputEnum } from '@fastgpt/global/core/workflow/constants';
import { getTimezoneCodeFromStr } from '@fastgpt/global/common/time/timezone';
import { getLocationFromIp } from '@fastgpt/service/common/geo';
import { getLocale } from '@fastgpt/service/common/middle/i18n';
import { AppVersionCollectionName } from '@fastgpt/service/core/app/version/schema';
import { ExportChatLogsBodySchema } from '@fastgpt/global/openapi/core/app/log/api';
const formatJsonString = (data: any) => {
if (data == null) return '';
......@@ -36,13 +37,7 @@ const formatJsonString = (data: any) => {
return data;
};
export type ExportChatLogsBody = GetAppChatLogsProps & {
title: string;
sourcesMap: Record<string, { label: string }>;
logKeys: AppLogKeysEnum[];
};
async function handler(req: ApiRequestProps<ExportChatLogsBody, {}>, res: NextApiResponse) {
async function handler(req: ApiRequestProps, res: NextApiResponse) {
let {
appId,
dateStart,
......@@ -52,8 +47,10 @@ async function handler(req: ApiRequestProps<ExportChatLogsBody, {}>, res: NextAp
chatSearch,
title,
sourcesMap,
logKeys = []
} = req.body;
logKeys = [],
feedbackType,
unreadOnly
} = ExportChatLogsBodySchema.parse(req.body);
if (!appId) {
throw new Error('缺少参数');
......@@ -100,12 +97,37 @@ async function handler(req: ApiRequestProps<ExportChatLogsBody, {}>, res: NextAp
const where = {
teamId: new Types.ObjectId(teamId),
appId: new Types.ObjectId(appId),
source: sources ? { $in: sources } : { $exists: true },
tmbId: tmbIds ? { $in: tmbIds.map((item) => new Types.ObjectId(item)) } : { $exists: true },
// Feedback type filtering (BEFORE pagination for performance)
...(feedbackType === 'has_feedback' &&
!unreadOnly && {
$or: [{ hasGoodFeedback: true }, { hasBadFeedback: true }]
}),
...(feedbackType === 'has_feedback' &&
unreadOnly && {
$or: [{ hasUnreadGoodFeedback: true }, { hasUnreadBadFeedback: true }]
}),
...(feedbackType === 'good' &&
!unreadOnly && {
hasGoodFeedback: true
}),
...(feedbackType === 'good' &&
unreadOnly && {
hasUnreadGoodFeedback: true
}),
...(feedbackType === 'bad' &&
!unreadOnly && {
hasBadFeedback: true
}),
...(feedbackType === 'bad' &&
unreadOnly && {
hasUnreadBadFeedback: true
}),
updateTime: {
$gte: new Date(dateStart),
$lte: new Date(dateEnd)
},
...(sources && { source: { $in: sources } }),
...(tmbIds && { tmbId: { $in: tmbIds } }),
...(chatSearch
? {
$or: [
......@@ -255,6 +277,22 @@ async function handler(req: ApiRequestProps<ExportChatLogsBody, {}>, res: NextAp
}
},
{
$lookup: {
from: AppVersionCollectionName,
localField: 'appVersionId',
foreignField: '_id',
pipeline: [
{
$project: {
versionName: 1,
_id: 0
}
}
],
as: 'versionData'
}
},
{
$addFields: {
messageCount: { $ifNull: [{ $arrayElemAt: ['$chatData.messageCount', 0] }, 0] },
userGoodFeedbackCount: {
......@@ -341,7 +379,8 @@ async function handler(req: ApiRequestProps<ExportChatLogsBody, {}>, res: NextAp
value: '$$item.value'
}
}
}
},
versionName: { $ifNull: [{ $arrayElemAt: ['$versionData.versionName', 0] }, null] }
}
},
{
......@@ -363,6 +402,7 @@ async function handler(req: ApiRequestProps<ExportChatLogsBody, {}>, res: NextAp
totalPoints: 1,
outLinkUid: 1,
tmbId: 1,
versionName: 1,
userGoodFeedbackItems: 1,
userBadFeedbackItems: 1,
customFeedbackItems: 1,
......@@ -439,6 +479,7 @@ async function handler(req: ApiRequestProps<ExportChatLogsBody, {}>, res: NextAp
[AppLogKeysEnum.ERROR_COUNT]: () => doc.errorCount || 0,
[AppLogKeysEnum.POINTS]: () => (doc.totalPoints ? Number(doc.totalPoints).toFixed(2) : 0),
[AppLogKeysEnum.REGION]: () => region,
versionName: () => doc.versionName || '',
chatDetails: () => formatJsonString(doc.chatDetails || [])
};
......
import type { ApiRequestProps, ApiResponseType } from '@fastgpt/service/type/next';
import { NextAPI } from '@/service/middleware/entry';
import { MongoAppLogKeys } from '@fastgpt/service/core/app/logs/logkeysSchema';
import type { AppLogKeysSchemaType } from '@fastgpt/global/core/app/logs/type';
import { authApp } from '@fastgpt/service/support/permission/app/auth';
import { AppReadChatLogPerVal } from '@fastgpt/global/support/permission/app/constant';
import {
GetLogKeysQuerySchema,
GetLogKeysResponseSchema,
type getLogKeysResponseType
} from '@fastgpt/global/openapi/core/app/log/api';
export type getLogKeysQuery = {
appId: string;
};
export type getLogKeysQuery = {};
export type getLogKeysBody = {};
export type getLogKeysResponse = {
logKeys: AppLogKeysSchemaType['logKeys'];
};
async function handler(
req: ApiRequestProps<getLogKeysBody, getLogKeysQuery>,
req: ApiRequestProps,
res: ApiResponseType<any>
): Promise<getLogKeysResponse> {
const { appId } = req.query;
): Promise<getLogKeysResponseType> {
const { appId } = GetLogKeysQuerySchema.parse(req.query);
const { teamId } = await authApp({
req,
......@@ -30,7 +28,7 @@ async function handler(
const result = await MongoAppLogKeys.findOne({ teamId, appId });
return { logKeys: result?.logKeys || [] };
return GetLogKeysResponseSchema.parse({ logKeys: result?.logKeys || [] });
}
export default NextAPI(handler);
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or sign in to comment