4.14.4 features (#6090)

* perf: zod with app log (#6083)

* perf: safe decode

* perf: zod with app log

* fix: text

* remove log

* rename field

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

* refactor: improve like/dislike interaction

* button style & merge status

* perf

* fix

* i18n

* feedback ui

* format

* api optimize

* openapi

* read status

---------

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

* perf: remove empty chat

* perf: delete resource tip

* fix: confirm

* feedback filter

* fix: ts

* perf: linker scroll

* perf: feedback ui

* fix: plugin file input store

* fix: max tokens

* update comment

* fix: condition value type

* fix feedback (#6095)

* fix feedback

* text

* list

* fix: versionid

---------

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

* fix: chat setting render;export logs filter

* add test

* perf: log list api

* perf: redirect check

* perf: log list

* create ui

* create ui

---------

Co-authored-by: heheer <heheer@sealos.io>
This commit is contained in:
Archer 2025-12-15 23:36:54 +08:00 committed by GitHub
parent 13681c9246
commit af669a1cfc
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
135 changed files with 6363 additions and 2021 deletions

View File

@ -5,7 +5,7 @@
如果您发现了 FastGPT 的安全漏洞,请按照以下步骤进行报告: 如果您发现了 FastGPT 的安全漏洞,请按照以下步骤进行报告:
1. **报告方式** 1. **报告方式**
发送邮件至:yujinlong@sealos.io 发送邮件至:archer@fastgpt.io
请备注版本以及您的 GitHub 账号 请备注版本以及您的 GitHub 账号
3. **响应时间** 3. **响应时间**

View File

@ -58,5 +58,5 @@ Due to servers potentially being located in different countries/regions, you agr
**Contact Us** **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. 2. We will respond promptly and address your concerns.

View File

@ -58,5 +58,5 @@ description: ' FastGPT 隐私政策'
**联系我们** **联系我们**
1. 如您对本隐私政策有任何疑问、建议或投诉,请通过以下方式与我们联系:yujinlong@sealos.io。 1. 如您对本隐私政策有任何疑问、建议或投诉,请通过以下方式与我们联系:archer@fastgpt.io。
2. 我们将尽快回复并解决您提出的问题。 2. 我们将尽快回复并解决您提出的问题。

View File

@ -72,4 +72,4 @@ This FastGPT Service Agreement constitutes the terms and conditions agreed betwe
**Article 7 Additional Provisions** **Article 7 Additional Provisions**
1. If any clause is deemed unlawful or invalid, the remaining provisions shall remain enforceable. 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.

View File

@ -67,4 +67,4 @@ FastGPT 服务协议是您与珠海环界云计算有限公司(以下简称“
**第7条 其他条款** **第7条 其他条款**
1. 如本协议中部分条款因违反法律法规而被视为无效,不影响其他条款的效力。 1. 如本协议中部分条款因违反法律法规而被视为无效,不影响其他条款的效力。
2. 本公司保留对本协议及隐私政策的最终解释权。如您对本协议或隐私政策有任何疑问,请联系我们:yujinlong@sealos.io。 2. 本公司保留对本协议及隐私政策的最终解释权。如您对本协议或隐私政策有任何疑问,请联系我们:archer@fastgpt.io。

View File

@ -27,13 +27,15 @@ curl --location --request POST 'https://{{host}}/api/admin/initv4144' \
1. 工具调用支持配置流输出 1. 工具调用支持配置流输出
2. AI 积分告警通知。 2. AI 积分告警通知。
3. 对话日志支持展示 IP 地址归属地。 3. 对话日志支持展示 IP 地址归属地。
4. 通过 API 上传本地文件至知识库,保存至 S3。同时将旧版 Gridfs 代码全部移除。 4. 对话日志支持展示应用版本名(如果对话中途修改成最新版本,则会被修改成最新版本)
5. 新版订阅套餐逻辑。 5. 对话日志支持按点赞点踩过滤,并在对话详情里可以快速定位到赞/踩的记录。
6. 支持配置对话文件白名单。 6. 通过 API 上传本地文件至知识库,保存至 S3。同时将旧版 Gridfs 代码全部移除。
7. S3 支持 pathStyle 和 region 配置。 7. 新版订阅套餐逻辑。
8. 支持通过 Sealos 来进行多租户自定义域名配置。 8. 支持配置对话文件白名单。
9. 工作流中引用工具时,文件输入支持手动填写(原本只支持变量引用)。 9. S3 支持 pathStyle 和 region 配置。
10. 支持网络代理(HTTP_PROXY,HTTPS_PROXY) 10. 支持通过 Sealos 来进行多租户自定义域名配置。
11. 工作流中引用工具时,文件输入支持手动填写(原本只支持变量引用)。
12. 支持网络代理(HTTP_PROXY,HTTPS_PROXY)
## ⚙️ 优化 ## ⚙️ 优化
@ -44,6 +46,9 @@ curl --location --request POST 'https://{{host}}/api/admin/initv4144' \
5. LLM 请求时,图片无效报错提示。 5. LLM 请求时,图片无效报错提示。
6. completions 接口,非 stream 模式, detail=false 时,增加返回 reason_content。 6. completions 接口,非 stream 模式, detail=false 时,增加返回 reason_content。
7. 增加对于无效的 S3 key 检测。 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 图标显示错误。 15. 模型头像缺失情况下,默认 huggingface.svg 图标显示错误。
16. 日志导出时,结束时间会多出一天。 16. 日志导出时,结束时间会多出一天。
17. 表单输入,前端默认值未传递到实体值。 17. 表单输入,前端默认值未传递到实体值。
18. 工具调用时,未传递 max_tokens 参数。
19. 工作流判断器 value 值,未结合 condition 来综合获取数据类型。
20. 非直接分块模式的知识库数据,引用阅读器导航顺序异常。引用阅读器只会加载同一页。
## 插件 ## 插件

View File

@ -97,10 +97,10 @@
"document/content/docs/protocol/index.mdx": "2025-07-30T15:38:30+08:00", "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.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/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.en.mdx": "2025-12-12T21:30:11+08:00",
"document/content/docs/protocol/privacy.mdx": "2025-08-03T22:37:45+08:00", "document/content/docs/protocol/privacy.mdx": "2025-12-12T21:30:11+08:00",
"document/content/docs/protocol/terms.en.mdx": "2025-08-03T22:37:45+08:00", "document/content/docs/protocol/terms.en.mdx": "2025-12-12T21:30:11+08:00",
"document/content/docs/protocol/terms.mdx": "2025-08-03T22:37:45+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.en.mdx": "2025-08-04T13:42:36+08:00",
"document/content/docs/toc.mdx": "2025-12-09T23:33:32+08:00", "document/content/docs/toc.mdx": "2025-12-09T23:33:32+08:00",
"document/content/docs/upgrading/4-10/4100.mdx": "2025-08-02T19:38:37+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/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/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/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/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/41.mdx": "2025-08-02T19:38:37+08:00",
"document/content/docs/upgrading/4-8/42.mdx": "2025-08-02T19:38:37+08:00", "document/content/docs/upgrading/4-8/42.mdx": "2025-08-02T19:38:37+08:00",

View File

@ -1,29 +0,0 @@
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;
};

View File

@ -14,7 +14,8 @@ export enum AppLogKeysEnum {
POINTS = 'points', POINTS = 'points',
RESPONSE_TIME = 'responseTime', RESPONSE_TIME = 'responseTime',
ERROR_COUNT = 'errorCount', ERROR_COUNT = 'errorCount',
REGION = 'region' REGION = 'region',
VERSION_NAME = 'versionName'
} }
export const AppLogKeysEnumMap = { export const AppLogKeysEnumMap = {
@ -31,7 +32,8 @@ export const AppLogKeysEnumMap = {
[AppLogKeysEnum.POINTS]: i18nT('app:logs_keys_points'), [AppLogKeysEnum.POINTS]: i18nT('app:logs_keys_points'),
[AppLogKeysEnum.RESPONSE_TIME]: i18nT('app:logs_keys_responseTime'), [AppLogKeysEnum.RESPONSE_TIME]: i18nT('app:logs_keys_responseTime'),
[AppLogKeysEnum.ERROR_COUNT]: i18nT('app:logs_keys_errorCount'), [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 = [ export const DefaultAppLogKeys = [
@ -48,7 +50,8 @@ export const DefaultAppLogKeys = [
{ key: AppLogKeysEnum.POINTS, enable: false }, { key: AppLogKeysEnum.POINTS, enable: false },
{ key: AppLogKeysEnum.RESPONSE_TIME, enable: false }, { key: AppLogKeysEnum.RESPONSE_TIME, enable: false },
{ key: AppLogKeysEnum.ERROR_COUNT, 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 { export enum AppLogTimespanEnum {

View File

@ -1,65 +0,0 @@
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;
};
}[];

View File

@ -0,0 +1,39 @@
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>;

View File

@ -28,6 +28,7 @@ export type ChatSchemaType = {
teamId: string; teamId: string;
tmbId: string; tmbId: string;
appId: string; appId: string;
appVersionId?: string;
createTime: Date; createTime: Date;
updateTime: Date; updateTime: Date;
title: string; title: string;
@ -44,6 +45,12 @@ export type ChatSchemaType = {
variables: Record<string, any>; variables: Record<string, any>;
pluginInputs?: FlowNodeInputItemType[]; pluginInputs?: FlowNodeInputItemType[];
metadata?: Record<string, any>; metadata?: Record<string, any>;
// Boolean flags for efficient filtering
hasGoodFeedback?: boolean;
hasBadFeedback?: boolean;
hasUnreadGoodFeedback?: boolean;
hasUnreadBadFeedback?: boolean;
}; };
export type ChatWithAppSchema = Omit<ChatSchemaType, 'appId'> & { export type ChatWithAppSchema = Omit<ChatSchemaType, 'appId'> & {
@ -105,6 +112,7 @@ export type AIChatItemType = {
userBadFeedback?: string; userBadFeedback?: string;
customFeedbacks?: string[]; customFeedbacks?: string[];
adminFeedback?: AdminFbkType; adminFeedback?: AdminFbkType;
isFeedbackRead?: boolean;
durationSeconds?: number; durationSeconds?: number;
errorMsg?: string; errorMsg?: string;
@ -152,6 +160,7 @@ export type ChatItemType = ChatItemMergeType & {
// Frontend type // Frontend type
export type ChatSiteItemType = ChatItemMergeType & { export type ChatSiteItemType = ChatItemMergeType & {
_id?: string; _id?: string;
id: string;
dataId: string; dataId: string;
status: `${ChatStatusEnum}`; status: `${ChatStatusEnum}`;
moduleName?: string; moduleName?: string;

View File

@ -0,0 +1,6 @@
import type { OpenAPIPath } from '../../type';
import { AppLogPath } from './log';
export const AppPath: OpenAPIPath = {
...AppLogPath
};

View File

@ -0,0 +1,248 @@
import { z } from 'zod';
import { PaginationSchema } from '../../../api';
import { AppLogKeysEnum, AppLogTimespanEnum } from '../../../../core/app/logs/constants';
import { ChatSourceEnum } from '../../../../core/chat/constants';
import { AppLogKeysSchema } from '../../../../core/app/logs/type';
import { SourceMemberSchema } from '../../../../support/user/type';
/* Log key mange */
export const GetLogKeysQuerySchema = z.object({
appId: z.string().meta({ example: '68ad85a7463006c963799a05', description: '应用 ID' })
});
export type getLogKeysQuery = z.infer<typeof GetLogKeysQuerySchema>;
export const GetLogKeysResponseSchema = z.object({
logKeys: z
.array(AppLogKeysSchema)
.default([])
.meta({ example: [AppLogKeysEnum.SOURCE, AppLogKeysEnum.CREATED_TIME], description: '日志键' })
});
export type getLogKeysResponseType = z.infer<typeof GetLogKeysResponseSchema>;
export const UpdateLogKeysBodySchema = z.object({
appId: z.string().meta({ example: '68ad85a7463006c963799a05', description: '应用 ID' }),
logKeys: z
.array(AppLogKeysSchema)
.meta({ example: [AppLogKeysEnum.SOURCE, AppLogKeysEnum.CREATED_TIME], description: '日志键' })
});
export type updateLogKeysBody = z.infer<typeof UpdateLogKeysBodySchema>;
// Chat Log Item Schema (based on AppChatLogSchema)
export const ChatLogItemSchema = z.object({
_id: z.string().meta({ example: '68ad85a7463006c963799a05', description: '对话日志 ID' }),
chatId: z.string().meta({ example: 'chat123', description: '对话 ID' }),
title: z.string().optional().meta({ example: '用户对话', description: '对话标题' }),
customTitle: z.string().nullish().meta({ example: '自定义标题', description: '自定义对话标题' }),
source: z.enum(ChatSourceEnum).meta({ example: ChatSourceEnum.api, description: '对话来源' }),
sourceName: z.string().optional().meta({ example: 'API调用', description: '来源名称' }),
updateTime: z.date().meta({ example: '2024-01-01T00:30:00.000Z', description: '更新时间' }),
createTime: z.date().meta({ example: '2024-01-01T00:00:00.000Z', description: '创建时间' }),
messageCount: z.int().nullish().meta({ example: 10, description: '消息数量' }),
userGoodFeedbackCount: z.int().nullish().meta({ example: 3, description: '好评反馈数量' }),
userBadFeedbackCount: z.int().nullish().meta({ example: 1, description: '差评反馈数量' }),
customFeedbacksCount: z.int().nullish().meta({ example: 2, description: '自定义反馈数量' }),
markCount: z.int().nullish().meta({ example: 0, description: '标记数量' }),
averageResponseTime: z
.number()
.nullish()
.meta({ example: 1500, description: '平均响应时间(毫秒)' }),
errorCount: z.int().nullish().meta({ example: 0, description: '错误次数' }),
totalPoints: z.number().nullish().meta({ example: 150.5, description: '总积分消耗' }),
outLinkUid: z.string().nullish().meta({ example: 'outLink123', description: '外链用户 ID' }),
tmbId: z.string().nullish().meta({ example: 'tmb123', description: '团队成员 ID' }),
sourceMember: SourceMemberSchema.optional().meta({ description: '来源成员信息' }),
versionName: z.string().nullish().meta({ example: 'v1.0.0', description: '版本名称' }),
region: z.string().nullish().meta({ example: '中国', description: '区域' })
});
export type AppLogsListItemType = z.infer<typeof ChatLogItemSchema>;
/* Get chat logs */
const FeedbackLogParamSchema = z.object({
feedbackType: z.enum(['all', 'has_feedback', 'good', 'bad']).optional().meta({
example: 'good',
description: '反馈类型all-全部记录has_feedback-包含反馈good-包含赞bad-包含踩'
}),
unreadOnly: z.boolean().optional().meta({
example: false,
description: '是否仅显示未读反馈(当 feedbackType 为 all 时忽略)'
})
});
// Get App Chat Logs Query Parameters (based on GetAppChatLogsProps)
export const GetAppChatLogsBodySchema = PaginationSchema.extend(
FeedbackLogParamSchema.shape
).extend({
appId: z.string().meta({
example: '68ad85a7463006c963799a05',
description: '应用 ID'
}),
dateStart: z.union([z.string(), z.date()]).meta({
example: '2024-01-01T00:00:00.000Z',
description: '开始时间'
}),
dateEnd: z.union([z.string(), z.date()]).meta({
example: '2024-12-31T23:59:59.999Z',
description: '结束时间'
}),
sources: z
.array(z.nativeEnum(ChatSourceEnum))
.optional()
.meta({
example: [ChatSourceEnum.api, ChatSourceEnum.online],
description: '对话来源筛选'
}),
tmbIds: z
.array(z.string())
.optional()
.meta({
example: ['tmb123', 'tmb456'],
description: '团队成员 ID 列表'
}),
chatSearch: z.string().optional().meta({
example: 'hello',
description: '对话内容搜索关键词'
})
});
export type getAppChatLogsBody = z.infer<typeof GetAppChatLogsBodySchema>;
// Get App Chat Logs Response
export const GetAppChatLogsResponseSchema = z
.object({
total: z.number().meta({ example: 100, description: '总记录数' }),
list: z.array(ChatLogItemSchema)
})
.meta({ example: { total: 100, list: [] }, description: '应用对话日志列表' });
export type getAppChatLogsResponseType = z.infer<typeof GetAppChatLogsResponseSchema>;
/* Export chat log */
export const ExportChatLogsBodySchema = GetAppChatLogsBodySchema.omit({
pageSize: true,
offset: true,
pageNum: true
}).safeExtend({
title: z.string().meta({
example: 'chat logs',
description: '标题'
}),
sourcesMap: z.record(z.string(), z.object({ label: z.string() })).meta({
example: { api: { label: 'API' }, online: { label: '在线' } },
description: '来源映射'
}),
logKeys: z.array(z.enum(AppLogKeysEnum)).meta({
example: [AppLogKeysEnum.SOURCE, AppLogKeysEnum.CREATED_TIME],
description: '日志键'
})
});
/* Get chart data */
// Get Chart Data Request Body (based on getChartDataBody)
export const GetChartDataBodySchema = z.object({
appId: z.string().meta({
example: '68ad85a7463006c963799a05',
description: '应用 ID'
}),
dateStart: z.date().meta({
example: '2024-01-01T00:00:00.000Z',
description: '开始日期'
}),
dateEnd: z.date().meta({
example: '2024-12-31T23:59:59.999Z',
description: '结束日期'
}),
source: z
.array(z.nativeEnum(ChatSourceEnum))
.optional()
.meta({
example: [ChatSourceEnum.api, ChatSourceEnum.online],
description: '对话来源筛选'
}),
offset: z.number().meta({
example: 1,
description: '时区偏移量'
}),
userTimespan: z.nativeEnum(AppLogTimespanEnum).meta({
example: AppLogTimespanEnum.day,
description: '用户数据时间跨度'
}),
chatTimespan: z.nativeEnum(AppLogTimespanEnum).meta({
example: AppLogTimespanEnum.day,
description: '对话数据时间跨度'
}),
appTimespan: z.nativeEnum(AppLogTimespanEnum).meta({
example: AppLogTimespanEnum.day,
description: '应用数据时间跨度'
})
});
export type getChartDataBody = z.infer<typeof GetChartDataBodySchema>;
// User Statistics Data Point (based on AppChatLogUserData)
export const UserStatsDataPointSchema = z.object({
timestamp: z.number().meta({ example: 1704067200, description: '时间戳' }),
summary: z.object({
userCount: z.number().meta({ example: 100, description: '用户总数' }),
newUserCount: z.number().meta({ example: 30, description: '新用户数' }),
retentionUserCount: z.number().meta({ example: 70, description: '留存用户数' }),
points: z.number().meta({ example: 1500, description: '积分消耗' }),
sourceCountMap: z.record(z.string(), z.number()).meta({
example: { api: 50, web: 30, mobile: 20 },
description: '各来源用户数量'
})
})
});
export type userStatsDataPoint = z.infer<typeof UserStatsDataPointSchema>;
// Chat Statistics Data Point (based on AppChatLogChatData)
export const ChatStatsDataPointSchema = z.object({
timestamp: z.number().meta({ example: 1704067200, description: '时间戳' }),
summary: z.object({
chatItemCount: z.number().meta({ example: 500, description: '对话项目总数' }),
chatCount: z.number().meta({ example: 100, description: '对话会话总数' }),
errorCount: z.number().meta({ example: 5, description: '错误次数' }),
points: z.number().meta({ example: 800, description: '积分消耗' })
})
});
export type chatStatsDataPoint = z.infer<typeof ChatStatsDataPointSchema>;
// App Statistics Data Point (based on AppChatLogAppData)
export const AppStatsDataPointSchema = z.object({
timestamp: z.number().meta({ example: 1704067200, description: '时间戳' }),
summary: z.object({
goodFeedBackCount: z.number().meta({ example: 25, description: '好评反馈数量' }),
badFeedBackCount: z.number().meta({ example: 3, description: '差评反馈数量' }),
chatCount: z.number().meta({ example: 100, description: '对话数量' }),
totalResponseTime: z.number().meta({ example: 120000, description: '总响应时间(毫秒)' })
})
});
export type appStatsDataPoint = z.infer<typeof AppStatsDataPointSchema>;
// Get Chart Data Response (based on getChartDataResponse)
export const GetChartDataResponseSchema = z.object({
userData: z.array(UserStatsDataPointSchema).meta({ description: '用户统计数据' }),
chatData: z.array(ChatStatsDataPointSchema).meta({ description: '对话统计数据' }),
appData: z.array(AppStatsDataPointSchema).meta({ description: '应用统计数据' })
});
export type getChartDataResponse = z.infer<typeof GetChartDataResponseSchema>;
// Get Total Data Query Parameters (based on getTotalDataQuery)
export const GetTotalDataQuerySchema = z.object({
appId: z.string().meta({
example: '68ad85a7463006c963799a05',
description: '应用 ID'
})
});
export type getTotalDataQuery = z.infer<typeof GetTotalDataQuerySchema>;
// Get Total Data Response (based on getTotalDataResponse)
export const GetTotalDataResponseSchema = z.object({
totalUsers: z.number().meta({
example: 1000,
description: '总用户数'
}),
totalChats: z.number().meta({
example: 5000,
description: '总对话数'
}),
totalPoints: z.number().meta({
example: 15000,
description: '总积分消耗'
})
});
export type getTotalDataResponse = z.infer<typeof GetTotalDataResponseSchema>;

View File

@ -0,0 +1,154 @@
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
}
}
}
}
}
}
};

View File

@ -0,0 +1,153 @@
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>;

View File

@ -0,0 +1,137 @@
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
}
}
}
}
}
}
};

View File

@ -1,6 +1,7 @@
import type { OpenAPIPath } from '../../type'; import type { OpenAPIPath } from '../../type';
import { ChatSettingPath } from './setting'; import { ChatSettingPath } from './setting';
import { ChatFavouriteAppPath } from './favourite/index'; import { ChatFavouriteAppPath } from './favourite/index';
import { ChatFeedbackPath } from './feedback/index';
import { z } from 'zod'; import { z } from 'zod';
import { CreatePostPresignedUrlResultSchema } from '../../../../service/common/s3/type'; import { CreatePostPresignedUrlResultSchema } from '../../../../service/common/s3/type';
import { PresignChatFileGetUrlSchema, PresignChatFilePostUrlSchema } from '../../../core/chat/api'; import { PresignChatFileGetUrlSchema, PresignChatFilePostUrlSchema } from '../../../core/chat/api';
@ -9,6 +10,7 @@ import { TagsMap } from '../../tag';
export const ChatPath: OpenAPIPath = { export const ChatPath: OpenAPIPath = {
...ChatSettingPath, ...ChatSettingPath,
...ChatFavouriteAppPath, ...ChatFavouriteAppPath,
...ChatFeedbackPath,
'/core/chat/presignChatFileGetUrl': { '/core/chat/presignChatFileGetUrl': {
post: { post: {

View File

@ -5,13 +5,13 @@ import { TagsMap } from '../../../tag';
export const ChatSettingPath: OpenAPIPath = { export const ChatSettingPath: OpenAPIPath = {
'/proApi/core/chat/setting/detail': { '/proApi/core/chat/setting/detail': {
get: { get: {
summary: '获取对话页设置', summary: '获取门户页设置',
description: description:
'获取当前团队的对话页设置,包括 slogan、对话提示、Logo、快捷应用、已选工具和精选应用标签等配置信息', '获取当前团队的门户页设置,包括 slogan、对话提示、Logo、快捷应用、已选工具和精选应用标签等配置信息',
tags: [TagsMap.chatSetting], tags: [TagsMap.chatSetting],
responses: { responses: {
200: { 200: {
description: '成功返回对话页设置信息', description: '成功返回门户页设置信息',
content: { content: {
'application/json': { 'application/json': {
schema: ChatSettingSchema schema: ChatSettingSchema
@ -23,9 +23,9 @@ export const ChatSettingPath: OpenAPIPath = {
}, },
'/proApi/core/chat/setting/update': { '/proApi/core/chat/setting/update': {
post: { post: {
summary: '更新对话页设置', summary: '更新门户页设置',
description: description:
'更新团队的对话页设置配置,包括 slogan、对话提示、Logo、快捷应用、已选工具和精选应用标签等信息', '更新团队的门户页设置配置,包括 slogan、对话提示、Logo、快捷应用、已选工具和精选应用标签等信息',
tags: [TagsMap.chatSetting], tags: [TagsMap.chatSetting],
requestBody: { requestBody: {
content: { content: {
@ -36,7 +36,7 @@ export const ChatSettingPath: OpenAPIPath = {
}, },
responses: { responses: {
200: { 200: {
description: '成功更新对话页设置', description: '成功更新门户页设置',
content: { content: {
'application/json': { 'application/json': {
schema: ChatSettingSchema schema: ChatSettingSchema

View File

@ -5,6 +5,7 @@ import { TagsMap } from './tag';
import { PluginPath } from './core/plugin'; import { PluginPath } from './core/plugin';
import { WalletPath } from './support/wallet'; import { WalletPath } from './support/wallet';
import { CustomDomainPath } from './support/customDomain'; import { CustomDomainPath } from './support/customDomain';
import { AppPath } from './core/app';
export const openAPIDocument = createDocument({ export const openAPIDocument = createDocument({
openapi: '3.1.0', openapi: '3.1.0',
@ -14,6 +15,7 @@ export const openAPIDocument = createDocument({
description: 'FastGPT API 文档' description: 'FastGPT API 文档'
}, },
paths: { paths: {
...AppPath,
...ChatPath, ...ChatPath,
...ApiKeyPath, ...ApiKeyPath,
...PluginPath, ...PluginPath,
@ -23,28 +25,28 @@ export const openAPIDocument = createDocument({
servers: [{ url: '/api' }], servers: [{ url: '/api' }],
'x-tagGroups': [ 'x-tagGroups': [
{ {
name: '对话', name: 'Agent 应用',
tags: [TagsMap.chatSetting, TagsMap.chatPage] tags: [TagsMap.appLog]
}, },
{ {
name: '插件相关', name: '对话管理',
tags: [TagsMap.chatSetting, TagsMap.chatPage, TagsMap.chatFeedback]
},
{
name: '插件系统',
tags: [TagsMap.pluginToolTag, TagsMap.pluginTeam] tags: [TagsMap.pluginToolTag, TagsMap.pluginTeam]
}, },
{ {
name: '插件-管理员', name: '支付系统',
tags: [TagsMap.pluginAdmin, TagsMap.pluginMarketplace, TagsMap.pluginToolAdmin]
},
{
name: 'ApiKey',
tags: [TagsMap.apiKey]
},
{
name: '支付',
tags: [TagsMap.walletBill, TagsMap.walletDiscountCoupon] tags: [TagsMap.walletBill, TagsMap.walletDiscountCoupon]
}, },
{ {
name: '自定义域名', name: '通用-辅助功能',
tags: [TagsMap.customDomain] tags: [TagsMap.customDomain, TagsMap.apiKey]
},
{
name: '管理员-插件管理',
tags: [TagsMap.pluginAdmin, TagsMap.pluginMarketplace, TagsMap.pluginToolAdmin]
} }
] ]
}); });

View File

@ -1,15 +1,32 @@
export const TagsMap = { export const TagsMap = {
/* Core */
// Agent - log
appLog: 'Agent 日志',
// Chat - home
chatPage: '对话页', chatPage: '对话页',
chatSetting: '对话页配置', chatSetting: '门户页配置',
pluginMarketplace: '插件市场(管理员视角)', chatFeedback: '对话反馈',
// Plugin
pluginToolTag: '工具标签', pluginToolTag: '工具标签',
pluginAdmin: '管理员插件管理',
pluginToolAdmin: '管理员系统工具管理',
pluginTeam: '团队插件管理', pluginTeam: '团队插件管理',
apiKey: 'APIKey',
/* Support */
// Wallet
walletBill: '订单', walletBill: '订单',
walletDiscountCoupon: '优惠券', walletDiscountCoupon: '优惠券',
customDomain: '自定义域名', customDomain: '自定义域名',
/* Common */
// APIKey
apiKey: 'APIKey',
/* Admin */
// Plugin
pluginMarketplace: '插件市场',
pluginAdmin: '管理员插件管理',
pluginToolAdmin: '管理员系统工具管理',
// Data
adminDashboard: '管理员仪表盘' adminDashboard: '管理员仪表盘'
}; };

View File

@ -45,7 +45,7 @@ export type TeamMemberSchema = {
updateTime?: Date; updateTime?: Date;
name: string; name: string;
role: `${TeamMemberRoleEnum}`; role: `${TeamMemberRoleEnum}`;
status: `${TeamMemberStatusEnum}`; status: TeamMemberStatusEnum;
avatar: string; avatar: string;
}; };

View File

@ -1,8 +1,9 @@
import type { LangEnum } from '../common/i18n/type'; import type { LangEnum } from '../../common/i18n/type';
import type { TeamPermission } from '../permission/user/controller'; import type { TeamPermission } from '../permission/user/controller';
import type { UserStatusEnum } from './constant'; import type { UserStatusEnum } from './constant';
import type { TeamMemberStatusEnum } from './team/constant'; import { TeamMemberStatusEnum } from './team/constant';
import type { TeamTmbItemType } from './team/type'; import type { TeamTmbItemType } from './team/type';
import z from 'zod';
export type UserModelSchema = { export type UserModelSchema = {
_id: string; _id: string;
@ -35,8 +36,13 @@ export type UserType = {
contact?: string; contact?: string;
}; };
export type SourceMemberType = { export const SourceMemberSchema = z.object({
name: string; name: z.string().meta({ example: '张三', description: '成员名称' }),
avatar: string; avatar: z
status: `${TeamMemberStatusEnum}`; .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>;

View File

@ -49,17 +49,34 @@ const addCommonMiddleware = (schema: mongoose.Schema) => {
schema.post(op, function (this: any, result: any, next) { schema.post(op, function (this: any, result: any, next) {
if (this._startTime) { if (this._startTime) {
const duration = Date.now() - this._startTime; const duration = Date.now() - this._startTime;
const warnLogData = {
collectionName: this.collection?.name, const getLogData = () => {
op: this.op, const collectionName = this.model?.collection?.name || this._model?.collection?.name;
...(this._query && { query: this._query }), const op = (() => {
...(this._update && { update: this._update }), if (this.op) return this.op;
...(this._delete && { delete: this._delete }), if (this._pipeline) {
duration 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) { if (duration > 2000) {
addLog.warn(`Slow operation ${duration}ms`, warnLogData); addLog.warn(`[Mongo Slow] Level2`, getLogData());
} else if (duration > 500) {
addLog.warn(`[Mongo Slow] Level1`, getLogData());
} }
} }
next(); next();
@ -112,7 +129,6 @@ export const getMongoModel = <T>(name: string, schema: mongoose.Schema) => {
export const getMongoLogModel = <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>; if (connectionLogMongo.models[name]) return connectionLogMongo.models[name] as Model<T>;
console.log('Load model======', name); console.log('Load model======', name);
// addCommonMiddleware(schema);
const model = connectionLogMongo.model<T>(name, schema); const model = connectionLogMongo.model<T>(name, schema);

View File

@ -2,7 +2,9 @@ import { ReadPreference } from './index';
export const readFromSecondary = { export const readFromSecondary = {
readPreference: ReadPreference.SECONDARY_PREFERRED, // primary | primaryPreferred | secondary | secondaryPreferred | nearest 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 = { export const writePrimary = {

View File

@ -217,6 +217,7 @@ export const runAgentCall = async ({
} = await createLLMResponse({ } = await createLLMResponse({
body: { body: {
...body, ...body,
max_tokens: maxTokens,
model, model,
messages: requestMessages, messages: requestMessages,
tool_choice: 'auto', tool_choice: 'auto',

View File

@ -10,7 +10,12 @@ import type {
StreamChatType, StreamChatType,
UnStreamChatType UnStreamChatType
} from '@fastgpt/global/core/ai/type'; } 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 { removeDatasetCiteText } from '@fastgpt/global/core/ai/llm/utils';
import { getAIApi } from '../config'; import { getAIApi } from '../config';
import type { OpenaiAccountType } from '@fastgpt/global/support/user/team/type'; 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 stop = body.stop ?? undefined;
const maxTokens = computedMaxToken({
model: modelData,
maxToken: body.max_tokens || undefined
});
const requestBody = { const requestBody = {
...body, ...body,
max_tokens: maxTokens,
model: modelData.model, model: modelData.model,
temperature: temperature:
typeof body.temperature === 'number' typeof body.temperature === 'number'
@ -567,7 +578,7 @@ const createChatCompletion = async ({
timeout, timeout,
options options
}: { }: {
modelData?: LLMModelItemType; modelData: LLMModelItemType;
body: ChatCompletionCreateParamsNonStreaming | ChatCompletionCreateParamsStreaming; body: ChatCompletionCreateParamsNonStreaming | ChatCompletionCreateParamsStreaming;
userKey?: OpenaiAccountType; userKey?: OpenaiAccountType;
timeout?: number; timeout?: number;
@ -587,13 +598,10 @@ const createChatCompletion = async ({
) )
> => { > => {
try { try {
// Rewrite model if (!modelData) {
const modelConstantsData = modelData || getLLMModel(body.model);
if (!modelConstantsData) {
return Promise.reject(`${body.model} not found`); return Promise.reject(`${body.model} not found`);
} }
body.model = modelConstantsData.model; body.model = modelData.model;
const formatTimeout = timeout ? timeout : 600000; const formatTimeout = timeout ? timeout : 600000;
const ai = getAIApi({ const ai = getAIApi({
@ -607,12 +615,10 @@ const createChatCompletion = async ({
const response = await ai.chat.completions.create(body, { const response = await ai.chat.completions.create(body, {
...options, ...options,
...(modelConstantsData.requestUrl ? { path: modelConstantsData.requestUrl } : {}), ...(modelData.requestUrl ? { path: modelData.requestUrl } : {}),
headers: { headers: {
...options?.headers, ...options?.headers,
...(modelConstantsData.requestAuth ...(modelData.requestAuth ? { Authorization: `Bearer ${modelData.requestAuth}` } : {})
? { Authorization: `Bearer ${modelConstantsData.requestAuth}` }
: {})
} }
}); });

View File

@ -64,6 +64,10 @@ const ChatItemSchema = new Schema({
// Field memory // Field memory
memories: Object, memories: Object,
errorMsg: String, errorMsg: String,
durationSeconds: Number,
citeCollectionIds: [String],
// Feedback
userGoodFeedback: String, userGoodFeedback: String,
userBadFeedback: String, userBadFeedback: String,
customFeedbacks: [String], customFeedbacks: [String],
@ -76,8 +80,7 @@ const ChatItemSchema = new Schema({
a: String a: String
} }
}, },
durationSeconds: Number, isFeedbackRead: Boolean,
citeCollectionIds: [String],
// @deprecated // @deprecated
[DispatchNodeResponseKeyEnum.nodeResponse]: Array [DispatchNodeResponseKeyEnum.nodeResponse]: Array
@ -91,6 +94,8 @@ const ChatItemSchema = new Schema({
close custom feedback; close custom feedback;
*/ */
ChatItemSchema.index({ appId: 1, chatId: 1, dataId: 1 }); ChatItemSchema.index({ appId: 1, chatId: 1, dataId: 1 });
// Anchor filter
ChatItemSchema.index({ appId: 1, chatId: 1, _id: -1 });
// timer, clear history // timer, clear history
ChatItemSchema.index({ teamId: 1, time: -1 }); ChatItemSchema.index({ teamId: 1, time: -1 });

View File

@ -8,6 +8,7 @@ import {
} from '@fastgpt/global/support/user/team/constant'; } from '@fastgpt/global/support/user/team/constant';
import { AppCollectionName } from '../app/schema'; import { AppCollectionName } from '../app/schema';
import { chatCollectionName } from './constants'; import { chatCollectionName } from './constants';
import { AppVersionCollectionName } from '../app/version/schema';
const ChatSchema = new Schema({ const ChatSchema = new Schema({
chatId: { chatId: {
@ -33,6 +34,10 @@ const ChatSchema = new Schema({
ref: AppCollectionName, ref: AppCollectionName,
required: true required: true
}, },
appVersionId: {
type: Schema.Types.ObjectId,
ref: AppVersionCollectionName
},
createTime: { createTime: {
type: Date, type: Date,
default: () => new Date() default: () => new Date()
@ -84,12 +89,16 @@ const ChatSchema = new Schema({
default: {} default: {}
}, },
initStatistics: Boolean // Feedback count statistics (redundant fields for performance)
// Boolean flags for efficient filtering
hasGoodFeedback: Boolean,
hasBadFeedback: Boolean,
hasUnreadGoodFeedback: Boolean,
hasUnreadBadFeedback: Boolean
}); });
try { try {
// Tmp
ChatSchema.index({ initStatistics: 1, _id: -1 });
ChatSchema.index({ appId: 1, tmbId: 1, outLinkUid: 1 }); ChatSchema.index({ appId: 1, tmbId: 1, outLinkUid: 1 });
ChatSchema.index({ chatId: 1 }); ChatSchema.index({ chatId: 1 });
@ -98,8 +107,76 @@ try {
// delete by appid; clear history; init chat; update chat; auth chat; get chat; // delete by appid; clear history; init chat; update chat; auth chat; get chat;
ChatSchema.index({ appId: 1, chatId: 1 }); ChatSchema.index({ appId: 1, chatId: 1 });
// get chat logs; /* get chat logs */
ChatSchema.index({ teamId: 1, appId: 1, sources: 1, tmbId: 1, updateTime: -1 }); // 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 // get share chat history
ChatSchema.index({ shareId: 1, outLinkUid: 1, updateTime: -1 }); ChatSchema.index({ shareId: 1, outLinkUid: 1, updateTime: -1 });

View File

@ -1,66 +1,197 @@
import type { ChatHistoryItemResType, ChatItemType } from '@fastgpt/global/core/chat/type'; import type { ChatHistoryItemResType, ChatItemType } from '@fastgpt/global/core/chat/type';
import { MongoChatItem } from './chatItemSchema'; import { MongoChatItem } from './chatItemSchema';
import { MongoChat } from './chatSchema';
import { addLog } from '../../common/system/log'; import { addLog } from '../../common/system/log';
import { DispatchNodeResponseKeyEnum } from '@fastgpt/global/core/workflow/runtime/constants'; import { DispatchNodeResponseKeyEnum } from '@fastgpt/global/core/workflow/runtime/constants';
import { ChatRoleEnum } from '@fastgpt/global/core/chat/constants'; import { ChatRoleEnum } from '@fastgpt/global/core/chat/constants';
import { MongoChatItemResponse } from './chatItemResponseSchema'; import { MongoChatItemResponse } from './chatItemResponseSchema';
import type { ClientSession } from '../../common/mongo';
import { Types } from '../../common/mongo';
import { mongoSessionRun } from '../../common/mongo/sessionRun';
import { UserError } from '@fastgpt/global/common/error/utils';
export async function getChatItems({ export async function getChatItems({
appId, appId,
chatId, chatId,
offset, field,
limit, limit,
field
offset,
initialId,
prevId,
nextId
}: { }: {
appId: string; appId: string;
chatId?: string; chatId?: string;
offset: number;
limit: number;
field: string; field: string;
}): Promise<{ histories: ChatItemType[]; total: number }> { limit: number;
offset?: number;
initialId?: string;
prevId?: string;
nextId?: string;
}): Promise<{
histories: ChatItemType[];
total: number;
hasMorePrev: boolean;
hasMoreNext: boolean;
}> {
if (!chatId) { if (!chatId) {
return { histories: [], total: 0 }; return { histories: [], total: 0, hasMorePrev: false, hasMoreNext: false };
} }
// Extend dataId // Extend dataId
field = `dataId ${field}`; field = `dataId ${field}`;
const [histories, total] = await Promise.all([ const baseCondition = { appId, chatId };
MongoChatItem.find({ appId, chatId }, field).sort({ _id: -1 }).skip(offset).limit(limit).lean(),
MongoChatItem.countDocuments({ appId, chatId }) const { histories, total, hasMorePrev, hasMoreNext } = await (async () => {
]); // Mode 1: offset pagination (original logic)
histories.reverse(); if (offset !== undefined) {
const [foundHistories, count] = await Promise.all([
MongoChatItem.find(baseCondition, field).sort({ _id: -1 }).skip(offset).limit(limit).lean(),
MongoChatItem.countDocuments(baseCondition)
]);
return {
histories: foundHistories.reverse(),
total: count,
hasMorePrev: count > limit,
hasMoreNext: offset > 0
};
}
// Mode 2: prevId - get records before the target
else if (prevId) {
const prevItem = await MongoChatItem.findOne(
{
...baseCondition,
dataId: prevId
},
{ _id: 1 }
).lean();
if (!prevItem) return Promise.reject(new UserError('Prev item not found'));
const [items, count] = await Promise.all([
MongoChatItem.find({ ...baseCondition, _id: { $lt: prevItem._id } }, field)
.sort({ _id: -1 })
.limit(limit + 1)
.lean(),
MongoChatItem.countDocuments({ ...baseCondition })
]);
return {
histories: items.slice(0, limit).reverse(),
total: count,
hasMorePrev: items.length > limit,
hasMoreNext: true
};
}
// Mode 3: nextId - get records after the target
else if (nextId) {
const nextItem = await MongoChatItem.findOne(
{
...baseCondition,
dataId: nextId
},
{ _id: 1 }
).lean();
if (!nextItem) return Promise.reject(new UserError('Next item not found'));
const [items, total] = await Promise.all([
MongoChatItem.find({ ...baseCondition, _id: { $gt: nextItem._id } }, field)
.sort({ _id: 1 })
.limit(limit + 1)
.lean(),
MongoChatItem.countDocuments({ ...baseCondition })
]);
return {
histories: items.slice(0, limit),
total,
hasMorePrev: true,
hasMoreNext: items.length > limit
};
}
// Mode 2: initialId - get records around the target
else {
if (!initialId) {
const [foundHistories, count] = await Promise.all([
MongoChatItem.find(baseCondition, field).sort({ _id: -1 }).skip(0).limit(limit).lean(),
MongoChatItem.countDocuments(baseCondition)
]);
return {
histories: foundHistories.reverse(),
total: count,
hasMorePrev: count > limit,
hasMoreNext: false
};
}
const halfLimit = Math.floor(limit / 2);
const ceilLimit = Math.ceil(limit / 2);
const targetItem = await MongoChatItem.findOne(
{ ...baseCondition, dataId: initialId },
field
).lean();
if (!targetItem) return Promise.reject(new UserError('Target item not found'));
const [prevItems, nextItems, count] = await Promise.all([
MongoChatItem.find({ ...baseCondition, _id: { $lt: targetItem._id } }, field)
.sort({ _id: -1 })
.limit(halfLimit + 1)
.lean(),
MongoChatItem.find({ ...baseCondition, _id: { $gt: targetItem._id } }, field)
.sort({ _id: 1 })
.limit(ceilLimit + 1)
.lean(),
MongoChatItem.countDocuments(baseCondition)
]);
return {
histories: [
...prevItems.slice(0, halfLimit).reverse(),
targetItem,
...nextItems.slice(0, ceilLimit)
].filter(Boolean),
total: count,
hasMorePrev: prevItems.length > halfLimit,
hasMoreNext: nextItems.length > ceilLimit
};
}
})();
// Add node responses field // Add node responses field
if (field.includes(DispatchNodeResponseKeyEnum.nodeResponse)) { if (field.includes(DispatchNodeResponseKeyEnum.nodeResponse) && histories.length > 0) {
const chatItemDataIds = histories const chatItemDataIds = histories
.filter((item) => item.obj === ChatRoleEnum.AI && !item.responseData?.length) .filter((item) => item.obj === ChatRoleEnum.AI && !item.responseData?.length)
.map((item) => item.dataId); .map((item) => item.dataId);
const chatItemResponsesMap = await MongoChatItemResponse.find( if (chatItemDataIds.length > 0) {
{ appId, chatId, chatItemDataId: { $in: chatItemDataIds } }, const chatItemResponsesMap = await MongoChatItemResponse.find(
{ chatItemDataId: 1, data: 1 } { appId, chatId, chatItemDataId: { $in: chatItemDataIds } },
) { chatItemDataId: 1, data: 1 }
.lean() )
.then((res) => { .lean()
const map = new Map<string, ChatHistoryItemResType[]>(); .then((res) => {
res.forEach((item) => { const map = new Map<string, ChatHistoryItemResType[]>();
const val = map.get(item.chatItemDataId) || []; res.forEach((item) => {
val.push(item.data); const val = map.get(item.chatItemDataId) || [];
map.set(item.chatItemDataId, val); val.push(item.data);
map.set(item.chatItemDataId, val);
});
return map;
}); });
return map;
});
histories.forEach((item) => { histories.forEach((item) => {
const val = chatItemResponsesMap.get(String(item.dataId)); const val = chatItemResponsesMap.get(String(item.dataId));
if (item.obj === ChatRoleEnum.AI && val) { if (item.obj === ChatRoleEnum.AI && val) {
item.responseData = val; item.responseData = val;
} }
}); });
}
} }
return { histories, total }; return { histories, total, hasMorePrev, hasMoreNext };
} }
export const addCustomFeedbacks = async ({ export const addCustomFeedbacks = async ({
@ -77,17 +208,183 @@ export const addCustomFeedbacks = async ({
if (!chatId || !dataId) return; if (!chatId || !dataId) return;
try { try {
await MongoChatItem.findOneAndUpdate( await mongoSessionRun(async (session) => {
{ // Add custom feedbacks to ChatItem
await MongoChatItem.updateOne(
{
appId,
chatId,
dataId
},
{
$push: { customFeedbacks: { $each: feedbacks } }
},
{ session }
);
// Update ChatLog feedback statistics
await updateChatFeedbackCount({
appId, appId,
chatId, chatId,
dataId session
}, });
{ });
$push: { customFeedbacks: { $each: feedbacks } }
}
);
} catch (error) { } catch (error) {
addLog.error('addCustomFeedbacks error', error); addLog.error('addCustomFeedbacks error', error);
throw error;
} }
}; };
/**
* Update feedback count statistics for a chat in Chat table
* This method aggregates feedback data from chatItems and updates the Chat table
*
* @param appId - Application ID
* @param chatId - Chat ID
* @param session - Optional MongoDB session for transaction support
*/
export async function updateChatFeedbackCount({
appId,
chatId,
session
}: {
appId: string;
chatId: string;
session?: ClientSession;
}): Promise<void> {
try {
// Aggregate feedback statistics from chatItems
const stats = await MongoChatItem.aggregate(
[
{
$match: {
appId: new Types.ObjectId(appId),
chatId,
obj: ChatRoleEnum.AI
}
},
{
$group: {
_id: null,
goodFeedbackCount: {
$sum: {
$cond: [{ $ifNull: ['$userGoodFeedback', false] }, 1, 0]
}
},
badFeedbackCount: {
$sum: {
$cond: [{ $ifNull: ['$userBadFeedback', false] }, 1, 0]
}
},
// Calculate unread good feedback count
unreadGoodFeedbackCount: {
$sum: {
$cond: [
{
$and: [
{ $ne: [{ $ifNull: ['$isFeedbackRead', false] }, true] },
{ $ne: [{ $ifNull: ['$userGoodFeedback', null] }, null] }
]
},
1,
0
]
}
},
// Calculate unread bad feedback count
unreadBadFeedbackCount: {
$sum: {
$cond: [
{
$and: [
{ $ne: [{ $ifNull: ['$isFeedbackRead', false] }, true] },
{ $ne: [{ $ifNull: ['$userBadFeedback', null] }, null] }
]
},
1,
0
]
}
}
}
}
],
{ session }
);
const feedbackStats = stats[0] || {
goodFeedbackCount: 0,
badFeedbackCount: 0,
unreadGoodFeedbackCount: 0,
unreadBadFeedbackCount: 0
};
// Calculate boolean flags
const hasGoodFeedback = feedbackStats.goodFeedbackCount > 0;
const hasBadFeedback = feedbackStats.badFeedbackCount > 0;
const hasUnreadGoodFeedback = feedbackStats.unreadGoodFeedbackCount > 0;
const hasUnreadBadFeedback = feedbackStats.unreadBadFeedbackCount > 0;
// Build update object - only set fields that are true, unset fields that are false
const updateObj: Record<string, any> = {};
const unsetObj: Record<string, any> = {};
if (hasGoodFeedback) {
updateObj.hasGoodFeedback = true;
} else {
unsetObj.hasGoodFeedback = '';
}
if (hasBadFeedback) {
updateObj.hasBadFeedback = true;
} else {
unsetObj.hasBadFeedback = '';
}
if (hasUnreadGoodFeedback) {
updateObj.hasUnreadGoodFeedback = true;
} else {
unsetObj.hasUnreadGoodFeedback = '';
}
if (hasUnreadBadFeedback) {
updateObj.hasUnreadBadFeedback = true;
} else {
unsetObj.hasUnreadBadFeedback = '';
}
// Build the final update query
const updateQuery: Record<string, any> = {};
if (Object.keys(updateObj).length > 0) {
updateQuery.$set = updateObj;
}
if (Object.keys(unsetObj).length > 0) {
updateQuery.$unset = unsetObj;
}
// Update Chat table with aggregated statistics and boolean flags
await MongoChat.updateOne(
{
appId,
chatId
},
updateQuery,
{
session
}
);
addLog.debug('updateChatFeedbackCount success', {
appId,
chatId,
stats: feedbackStats,
hasGoodFeedback,
hasBadFeedback,
hasUnreadGoodFeedback,
hasUnreadBadFeedback
});
} catch (error) {
addLog.error('updateChatFeedbackCount error', error);
throw error;
}
}

View File

@ -26,9 +26,10 @@ import { VariableInputEnum } from '@fastgpt/global/core/workflow/constants';
import { encryptSecretValue, anyValueDecrypt } from '../../common/secret/utils'; import { encryptSecretValue, anyValueDecrypt } from '../../common/secret/utils';
import type { SecretValueType } from '@fastgpt/global/common/secret/type'; import type { SecretValueType } from '@fastgpt/global/common/secret/type';
type Props = { export type Props = {
chatId: string; chatId: string;
appId: string; appId: string;
versionId?: string;
teamId: string; teamId: string;
tmbId: string; tmbId: string;
nodes: StoreNodeItemType[]; nodes: StoreNodeItemType[];
@ -212,6 +213,7 @@ export async function saveChat(props: Props) {
const { const {
chatId, chatId,
appId, appId,
versionId,
teamId, teamId,
tmbId, tmbId,
nodes, nodes,
@ -299,6 +301,7 @@ export async function saveChat(props: Props) {
teamId, teamId,
tmbId, tmbId,
appId, appId,
appVersionId: versionId,
chatId, chatId,
variableList, variableList,
welcomeText, welcomeText,

View File

@ -110,7 +110,7 @@ export const dispatchRunPlugin = async (props: RunPluginProps): Promise<RunPlugi
Array.isArray(val) && Array.isArray(val) &&
data[input.key] data[input.key]
) { ) {
data[input.key] = val.map((item) => item.url); data[input.key] = val.map((item) => (typeof item === 'string' ? item : item.url));
} }
return { return {

View File

@ -12,21 +12,16 @@ type PaginationResponse<T = {}> = {
list: T[]; list: T[];
}; };
type LinkedPaginationProps<T = {}> = T & { type LinkedPaginationProps<T = {}, A = any> = T & {
pageSize: number; pageSize: number;
} & RequireOnlyOne<{ anchor?: A;
initialId: string; initialId?: string;
nextId: string; nextId?: string;
prevId: string; prevId?: string;
}> & };
RequireOnlyOne<{
initialIndex: number;
nextIndex: number;
prevIndex: number;
}>;
type LinkedListResponse<T = {}> = { type LinkedListResponse<T = {}, A = any> = {
list: Array<T & { _id: string; index: number }>; list: Array<T & { id: string; anchor?: A }>;
hasMorePrev: boolean; hasMorePrev: boolean;
hasMoreNext: boolean; hasMoreNext: boolean;
}; };

View File

@ -1,8 +1,18 @@
import React, { useEffect, useMemo, useRef, useState } from 'react'; import React, { useEffect, useMemo, useRef, useState } from 'react';
import { useDisclosure, Button, ModalBody, ModalFooter, type ImageProps } from '@chakra-ui/react'; import {
import { useTranslation } from 'next-i18next'; 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 MyModal from '../components/common/MyModal';
import { useMemoizedFn } from 'ahooks'; import { useMemoizedFn } from 'ahooks';
import { useMemoEnhance } from './useMemoEnhance';
export const useConfirm = (props?: { export const useConfirm = (props?: {
title?: string; title?: string;
@ -12,10 +22,11 @@ export const useConfirm = (props?: {
type?: 'common' | 'delete'; type?: 'common' | 'delete';
hideFooter?: boolean; hideFooter?: boolean;
iconColor?: ImageProps['color']; iconColor?: ImageProps['color'];
inputConfirmText?: string;
}) => { }) => {
const { t } = useTranslation(); const { t } = useTranslation();
const map = useMemo(() => { const map = useMemoEnhance(() => {
const map = { const map = {
common: { common: {
title: t('common:action_confirm'), title: t('common:action_confirm'),
@ -38,9 +49,13 @@ export const useConfirm = (props?: {
iconColor, iconColor,
content, content,
showCancel = true, showCancel = true,
hideFooter = false hideFooter = false,
inputConfirmText: initialInputConfirmText
} = props || {}; } = props || {};
const [customContent, setCustomContent] = useState<string | React.ReactNode>(content); const [customContent, setCustomContent] = useState<string | React.ReactNode>(content);
const [customContentInputConfirmText, setCustomContentInputConfirmText] = useState<
string | undefined
>(initialInputConfirmText);
const { isOpen, onOpen, onClose } = useDisclosure(); const { isOpen, onOpen, onClose } = useDisclosure();
@ -48,11 +63,22 @@ export const useConfirm = (props?: {
const cancelCb = useRef<any>(); const cancelCb = useRef<any>();
const openConfirm = useMemoizedFn( const openConfirm = useMemoizedFn(
(confirm?: Function, cancel?: any, customContent?: string | React.ReactNode) => { ({
confirmCb.current = confirm; onConfirm,
cancelCb.current = cancel; 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; return onOpen;
} }
@ -63,22 +89,24 @@ export const useConfirm = (props?: {
closeText = t('common:Cancel'), closeText = t('common:Cancel'),
confirmText = t('common:Confirm'), confirmText = t('common:Confirm'),
isLoading, isLoading,
bg,
countDown = 0 countDown = 0
}: { }: {
closeText?: string; closeText?: string;
confirmText?: string; confirmText?: string;
isLoading?: boolean; isLoading?: boolean;
bg?: string;
countDown?: number; countDown?: number;
}) => { }) => {
const isInputDelete = !!customContentInputConfirmText;
const timer = useRef<any>(); const timer = useRef<any>();
const [countDownAmount, setCountDownAmount] = useState(countDown); const [countDownAmount, setCountDownAmount] = useState(countDown);
const [requesting, setRequesting] = useState(false); const [requesting, setRequesting] = useState(false);
const [inputValue, setInputValue] = useState('');
useEffect(() => { useEffect(() => {
if (isOpen) { if (isOpen) {
setCountDownAmount(countDown); setCountDownAmount(countDown);
setInputValue('');
timer.current = setInterval(() => { timer.current = setInterval(() => {
setCountDownAmount((val) => { setCountDownAmount((val) => {
if (val <= 0) { if (val <= 0) {
@ -94,6 +122,10 @@ export const useConfirm = (props?: {
} }
}, [isOpen]); }, [isOpen]);
const isInputDeleteConfirmValid = !isInputDelete
? true
: !!customContentInputConfirmText && inputValue.trim() === customContentInputConfirmText;
return ( return (
<MyModal <MyModal
isOpen={isOpen} isOpen={isOpen}
@ -103,7 +135,30 @@ export const useConfirm = (props?: {
maxW={['90vw', '400px']} maxW={['90vw', '400px']}
> >
<ModalBody pt={5} whiteSpace={'pre-wrap'} fontSize={'sm'}> <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> </ModalBody>
{!hideFooter && ( {!hideFooter && (
<ModalFooter> <ModalFooter>
@ -124,7 +179,7 @@ export const useConfirm = (props?: {
<Button <Button
size={'sm'} size={'sm'}
variant={map.variant} variant={map.variant}
isDisabled={countDownAmount > 0} isDisabled={countDownAmount > 0 || (isInputDelete && !isInputDeleteConfirmValid)}
ml={3} ml={3}
isLoading={isLoading || requesting} isLoading={isLoading || requesting}
px={5} px={5}

View File

@ -1,27 +1,28 @@
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 { type LinkedListResponse, type LinkedPaginationProps } from '../common/fetch/type';
import { Box, type BoxProps } from '@chakra-ui/react'; import { Box, type BoxProps } from '@chakra-ui/react';
import { useTranslation } from 'next-i18next'; import { useTranslation } from 'next-i18next';
import { useScroll, useMemoizedFn, useDebounceEffect } from 'ahooks'; import { useScroll, useMemoizedFn, useDebounceEffect } from 'ahooks';
import MyBox from '../components/common/MyBox'; import MyBox from '../components/common/MyBox';
import { useRequest2 } from './useRequest'; import { useRequest2 } from './useRequest';
import { delay } from '../../global/common/system/utils';
const threshold = 100; const threshold = 200;
export function useLinkedScroll< export function useLinkedScroll<
TParams extends LinkedPaginationProps & { isInitialLoad?: boolean }, TParams extends LinkedPaginationProps,
TData extends LinkedListResponse TData extends LinkedListResponse
>( >(
api: (data: TParams) => Promise<TData>, api: (data: TParams) => Promise<TData>,
{ {
pageSize = 15, pageSize = 10,
params = {}, params = {},
currentData currentData,
defaultScroll = 'top'
}: { }: {
pageSize?: number; pageSize?: number;
params?: Record<string, any>; params?: Record<string, any>;
currentData?: { id: string; index: number }; currentData?: { id: string; anchor?: any };
defaultScroll?: 'top' | 'bottom';
} }
) { ) {
const { t } = useTranslation(); const { t } = useTranslation();
@ -29,53 +30,61 @@ export function useLinkedScroll<
const [hasMorePrev, setHasMorePrev] = useState(true); const [hasMorePrev, setHasMorePrev] = useState(true);
const [hasMoreNext, setHasMoreNext] = useState(true); const [hasMoreNext, setHasMoreNext] = useState(true);
// 锚点,用于记录顶部和底部的数据
const anchorRef = useRef({ const anchorRef = useRef({
top: null as { _id: string; index: number } | null, top: null as TData['list'][number] | null,
bottom: null as { _id: string; index: number } | null bottom: null as TData['list'][number] | null
}); });
const containerRef = useRef<HTMLDivElement>(null); const containerRef = useRef<HTMLDivElement>(null);
const itemRefs = useRef<Map<string, HTMLElement | null>>(new Map()); const itemRefs = useRef<Map<string, HTMLElement | null>>(new Map());
const isInit = useRef(false);
const scrollToItem = async (id: string, retry = 3) => { const scrollToItem = useCallback(
const itemIndex = dataList.findIndex((item) => item._id === id); async (id?: string) => {
if (itemIndex === -1) return; if (!id) {
id = defaultScroll === 'top' ? dataList[0]?.id : dataList[dataList.length - 1]?.id;
const element = itemRefs.current.get(id);
if (!element || !containerRef.current) {
if (retry > 0) {
await delay(500);
return scrollToItem(id, retry - 1);
} }
return;
}
const elementRect = element.getBoundingClientRect(); const itemIndex = dataList.findIndex((item) => item.id === id);
const containerRect = containerRef.current.getBoundingClientRect(); if (itemIndex === -1) {
return;
}
const scrollTop = containerRef.current.scrollTop + elementRect.top - containerRect.top; const element = itemRefs.current.get(id);
if (!element || !containerRef.current) {
requestAnimationFrame(() => scrollToItem(id));
return;
}
containerRef.current.scrollTo({ const elementRect = element.getBoundingClientRect();
top: scrollTop const containerRect = containerRef.current.getBoundingClientRect();
});
}; const scrollTop = containerRef.current.scrollTop + elementRect.top - containerRect.top;
containerRef.current.scrollTo({
top: scrollTop
});
},
[dataList, defaultScroll]
);
const { runAsync: callApi, loading: isLoading } = useRequest2(api); const { runAsync: callApi, loading: isLoading } = useRequest2(api);
let scroolSign = useRef(false); let scrollSign = useRef(false);
const { runAsync: loadInitData } = useRequest2( const { runAsync: loadInitData } = useRequest2(
async ({ scrollWhenFinish, refresh } = { scrollWhenFinish: true, refresh: false }) => { 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) { if (item && !refresh) {
scrollToItem(item._id); scrollToItem(item.id);
return; return;
} }
const response = await callApi({ const response = await callApi({
initialId: currentData.id, initialId: currentData?.id,
initialIndex: currentData.index, anchor: currentData?.anchor,
pageSize, pageSize,
...params ...params
} as TParams); } as TParams);
@ -83,7 +92,7 @@ export function useLinkedScroll<
setHasMorePrev(response.hasMorePrev); setHasMorePrev(response.hasMorePrev);
setHasMoreNext(response.hasMoreNext); setHasMoreNext(response.hasMoreNext);
scroolSign.current = scrollWhenFinish; scrollSign.current = scrollWhenFinish;
setDataList(response.list); setDataList(response.list);
if (response.list.length > 0) { if (response.list.length > 0) {
@ -93,13 +102,20 @@ export function useLinkedScroll<
}, },
{ {
refreshDeps: [currentData], refreshDeps: [currentData],
onFinally() {
isInit.current = true;
},
manual: false manual: false
} }
); );
useEffect(() => { useEffect(() => {
if (scroolSign.current && currentData) { if (!isInit.current) return;
scroolSign.current = false; loadInitData({ refresh: true, scrollWhenFinish: true });
scrollToItem(currentData.id); }, [params]);
useEffect(() => {
if (scrollSign.current) {
scrollSign.current = false;
scrollToItem(currentData?.id);
} }
}, [dataList]); }, [dataList]);
@ -111,8 +127,8 @@ export function useLinkedScroll<
const prevScrollHeight = scrollRef?.current?.scrollHeight || 0; const prevScrollHeight = scrollRef?.current?.scrollHeight || 0;
const response = await callApi({ const response = await callApi({
prevId: anchorRef.current.top._id, prevId: anchorRef.current.top.id,
prevIndex: anchorRef.current.top.index, anchor: anchorRef.current.top.anchor,
pageSize, pageSize,
...params ...params
} as TParams); } as TParams);
@ -148,8 +164,8 @@ export function useLinkedScroll<
const prevScrollTop = scrollRef?.current?.scrollTop || 0; const prevScrollTop = scrollRef?.current?.scrollTop || 0;
const response = await callApi({ const response = await callApi({
nextId: anchorRef.current.bottom._id, nextId: anchorRef.current.bottom.id,
nextIndex: anchorRef.current.bottom.index, anchor: anchorRef.current.bottom.anchor,
pageSize, pageSize,
...params ...params
} as TParams); } as TParams);
@ -185,23 +201,37 @@ export function useLinkedScroll<
children: ReactNode; children: ReactNode;
ScrollContainerRef?: React.RefObject<HTMLDivElement>; ScrollContainerRef?: React.RefObject<HTMLDivElement>;
} & BoxProps) => { } & BoxProps) => {
const ref = ScrollContainerRef || containerRef; // If external ref is provided, use it; otherwise use internal ref
const scroll = useScroll(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( 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) { if (scrollTop + clientHeight >= scrollHeight - threshold) {
loadNextData(ref); loadNextData(actualContainerRef);
} }
// 滚动到顶部附近,加载更多上方数据 // 滚动到顶部附近,加载更多上方数据
if (scrollTop <= threshold) { if (scrollTop <= threshold) {
loadPrevData(ref); loadPrevData(actualContainerRef);
} }
}, },
[scroll], [scroll],
@ -209,7 +239,7 @@ export function useLinkedScroll<
); );
return ( return (
<MyBox ref={ref} h={'100%'} overflow={'auto'} isLoading={isLoading} {...props}> <MyBox ref={setRefs} h={'100%'} overflow={'auto'} isLoading={isLoading} {...props}>
{hasMorePrev && prevLoading && ( {hasMorePrev && prevLoading && (
<Box mt={2} fontSize={'xs'} color={'blackAlpha.500'} textAlign={'center'}> <Box mt={2} fontSize={'xs'} color={'blackAlpha.500'} textAlign={'center'}>
{t('common:is_requesting')} {t('common:is_requesting')}

View File

@ -69,8 +69,8 @@
"config_file_upload": "Click to Configure File Upload Rules", "config_file_upload": "Click to Configure File Upload Rules",
"config_question_guide": "Configuration guess you want to ask", "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_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_del_app_tip": "Confirm to delete this app? \nDeleting an app will delete its associated conversation records as well.",
"confirm_delete_folder_tip": "Confirm to delete this folder? All apps and corresponding conversation records under it will be deleted. Please confirm!", "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?", "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_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.", "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", "local_upload": "Local upload",
"log_chat_logs": "Dialogue log", "log_chat_logs": "Dialogue log",
"log_detail": "Log details", "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_data": "Data board",
"logs_app_result": "Application effect", "logs_app_result": "Application effect",
"logs_average_response_time": "Average run time", "logs_average_response_time": "Average run time",
"logs_average_response_time_description": "Average of total workflow 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": "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_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", "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_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_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_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_key_config": "Field Configuration",
"logs_keys_annotatedCount": "Annotated Answer Count", "logs_keys_annotatedCount": "Annotated Answer Count",
"logs_keys_chatDetails": "Conversation details", "logs_keys_chatDetails": "Conversation details",
@ -216,6 +224,7 @@
"logs_keys_source": "Source", "logs_keys_source": "Source",
"logs_keys_title": "Title", "logs_keys_title": "Title",
"logs_keys_user": "User", "logs_keys_user": "User",
"logs_keys_versionName": "Version Name",
"logs_message_total": "Total Messages", "logs_message_total": "Total Messages",
"logs_new_user_count": "New users", "logs_new_user_count": "New users",
"logs_points": "Points Consumed", "logs_points": "Points Consumed",
@ -234,6 +243,7 @@
"logs_total_points": "Accumulated points consumption", "logs_total_points": "Accumulated points consumption",
"logs_total_tips": "Cumulative indicators are not affected by time filtering", "logs_total_tips": "Cumulative indicators are not affected by time filtering",
"logs_total_users": "Cumulative number of users", "logs_total_users": "Cumulative number of users",
"logs_unread_only": "Unread Only",
"logs_user_count": "Number of users", "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_count_description": "Number of people who have a conversation with the app in unit time",
"logs_user_data": "User data", "logs_user_data": "User data",

View File

@ -2,6 +2,8 @@
"AI_input_is_empty": "The content passed to the AI node is empty", "AI_input_is_empty": "The content passed to the AI node is empty",
"Delete_all": "Clear All Lexicon", "Delete_all": "Clear All Lexicon",
"LLM_model_response_empty": "The model flow response is empty, please check whether the model flow output is normal.", "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", "ai_reasoning": "Thinking process",
"back_to_text": "Text input", "back_to_text": "Text input",
"balance_not_enough_pause": "Workflow paused due to insufficient AI points", "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", "is_chatting": "Chatting in progress... please wait until it finishes",
"items": "Items", "items": "Items",
"llm_tokens": "LLM tokens", "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", "module_runtime_and": "Total Module Runtime",
"multiple_AI_conversations": "Multiple AI Conversations", "multiple_AI_conversations": "Multiple AI Conversations",
"new_input_guide_lexicon": "New Lexicon", "new_input_guide_lexicon": "New Lexicon",

View File

@ -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.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.", "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_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_logout": "Confirm to log out?",
"confirm_move": "Move Here", "confirm_move": "Move Here",
"confirm_update": "confirm_update", "confirm_update": "confirm_update",
@ -946,11 +948,14 @@
"n_agent_amount": "{{amount}} Agent limit", "n_agent_amount": "{{amount}} Agent limit",
"n_ai_points": "{{amount}} points", "n_ai_points": "{{amount}} points",
"n_chat_records_retain": "{{amount}} Days of Chat History Retention", "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_amount": "{{amount}} Dataset limit",
"n_dataset_size": "{{amount}} Dataset Indexes", "n_dataset_size": "{{amount}} Dataset Indexes",
"n_team_audit_day": "{{amount}} days team operation log records", "n_team_audit_day": "{{amount}} days team operation log records",
"n_team_members": "{{amount}} Member", "n_team_members": "{{amount}} Member",
"n_team_qpm": "{{amount}} QPM", "n_team_qpm": "{{amount}} QPM",
"n_website_sync_max_pages": "Single knowledge base {{amount}} web pages synchronized",
"name": "name", "name": "name",
"name_is_empty": "Name Cannot Be Empty", "name_is_empty": "Name Cannot Be Empty",
"navbar.Account": "Account", "navbar.Account": "Account",
@ -985,7 +990,6 @@
"option": "Option", "option": "Option",
"page": "Page", "page": "Page",
"page_center": "Page Center", "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.amount": "Amount",
"pay.error_desc": "There was a problem when converting payment routes", "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", "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.Upgrade plan": "Upgrade Package",
"support.wallet.subscription.ai_model": "AI Language Model", "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", "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.Month": "Month",
"support.wallet.subscription.mode.Period": "Subscription Period", "support.wallet.subscription.mode.Period": "Subscription Period",
"support.wallet.subscription.mode.Year": "Year", "support.wallet.subscription.mode.Year": "Year",

View File

@ -71,8 +71,8 @@
"config_file_upload": "点击配置文件上传规则", "config_file_upload": "点击配置文件上传规则",
"config_question_guide": "配置猜你想问", "config_question_guide": "配置猜你想问",
"confirm_copy_app_tip": "系统将为您创建一个相同配置应用,但权限不会进行复制,请确认!", "confirm_copy_app_tip": "系统将为您创建一个相同配置应用,但权限不会进行复制,请确认!",
"confirm_del_app_tip": "确认删除 【{{name}}】 及其所有聊天记录?", "confirm_del_app_tip": "确认删除该应用?删除应用会将其关联的对话记录一并删除。",
"confirm_delete_folder_tip": "确认删除该文件夹?将会删除它下面所有应用及对应的聊天记录,请确认!", "confirm_delete_folder_tip": "删除该文件夹时,将会删除它下面所有应用及对应的聊天记录。",
"confirm_delete_tool": "确认删除该工具?", "confirm_delete_tool": "确认删除该工具?",
"copilot_config_message": "`当前节点配置信息: \n代码类型{{codeType}} \n当前代码 \\`\\`\\`{{codeType}} \n{{code}} \\`\\`\\` \n输入参数 {{inputs}} \n输出参数 {{outputs}}`", "copilot_config_message": "`当前节点配置信息: \n代码类型{{codeType}} \n当前代码 \\`\\`\\`{{codeType}} \n{{code}} \\`\\`\\` \n输入参数 {{inputs}} \n输出参数 {{outputs}}`",
"copilot_confirm_message": "已接收到原始配置,了解当前代码结构和输入输出参数。请说明您的优化需求。", "copilot_confirm_message": "已接收到原始配置,了解当前代码结构和输入输出参数。请说明您的优化需求。",
@ -186,11 +186,17 @@
"local_upload": "本地上传", "local_upload": "本地上传",
"log_chat_logs": "对话日志", "log_chat_logs": "对话日志",
"log_detail": "日志详情", "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_data": "数据看板",
"logs_app_result": "应用效果", "logs_app_result": "应用效果",
"logs_average_response_time": "平均运行时长(s)", "logs_average_response_time": "平均运行时长(s)",
"logs_average_response_time_description": "工作流总运行时间的平均值", "logs_average_response_time_description": "工作流总运行时间的平均值",
"logs_bad_feedback": "点踩", "logs_bad_feedback": "点踩",
"logs_bad_feedback_only": "仅看踩",
"logs_chat_count": "会话次数", "logs_chat_count": "会话次数",
"logs_chat_count_description": "该应用共新建多少个会话。 会话定义当与上条消息间隔超过15min认为是产生新会话该定义仅在此生效", "logs_chat_count_description": "该应用共新建多少个会话。 会话定义当与上条消息间隔超过15min认为是产生新会话该定义仅在此生效",
"logs_chat_data": "对话数据", "logs_chat_data": "对话数据",
@ -205,6 +211,7 @@
"logs_export_confirm_tip": "当前共有 {{total}} 条对话记录,每条对话最多可导出最新 100 条消息。确认导出?", "logs_export_confirm_tip": "当前共有 {{total}} 条对话记录,每条对话最多可导出最新 100 条消息。确认导出?",
"logs_export_title": "时间,来源,使用者,联系方式,标题,消息总数,用户赞同反馈,用户反对反馈,自定义反馈,标注答案,对话详情", "logs_export_title": "时间,来源,使用者,联系方式,标题,消息总数,用户赞同反馈,用户反对反馈,自定义反馈,标注答案,对话详情",
"logs_good_feedback": "点赞", "logs_good_feedback": "点赞",
"logs_good_feedback_only": "仅看赞",
"logs_key_config": "字段配置", "logs_key_config": "字段配置",
"logs_keys_annotatedCount": "标注答案数量", "logs_keys_annotatedCount": "标注答案数量",
"logs_keys_chatDetails": "对话详情", "logs_keys_chatDetails": "对话详情",
@ -221,6 +228,7 @@
"logs_keys_source": "来源", "logs_keys_source": "来源",
"logs_keys_title": "标题", "logs_keys_title": "标题",
"logs_keys_user": "使用者", "logs_keys_user": "使用者",
"logs_keys_versionName": "版本名",
"logs_message_total": "消息总数", "logs_message_total": "消息总数",
"logs_new_user_count": "新增用户", "logs_new_user_count": "新增用户",
"logs_points": "积分消耗", "logs_points": "积分消耗",
@ -246,6 +254,7 @@
"logs_total_points": "累计积分消耗", "logs_total_points": "累计积分消耗",
"logs_total_tips": "累计指标不受时间筛选影响", "logs_total_tips": "累计指标不受时间筛选影响",
"logs_total_users": "累计用户数", "logs_total_users": "累计用户数",
"logs_unread_only": "仅看未读",
"logs_user_callback": "用户反馈", "logs_user_callback": "用户反馈",
"logs_user_count": "用户数", "logs_user_count": "用户数",
"logs_user_count_description": "单位时间内与该应用产生对话的人数", "logs_user_count_description": "单位时间内与该应用产生对话的人数",

View File

@ -2,6 +2,8 @@
"AI_input_is_empty": "传入 AI 节点的内容为空", "AI_input_is_empty": "传入 AI 节点的内容为空",
"Delete_all": "清空词库", "Delete_all": "清空词库",
"LLM_model_response_empty": "模型流响应为空,请检查模型流输出是否正常", "LLM_model_response_empty": "模型流响应为空,请检查模型流输出是否正常",
"Next": "下一个",
"Previous": "上一个",
"ai_reasoning": "思考过程", "ai_reasoning": "思考过程",
"back_to_text": "返回输入", "back_to_text": "返回输入",
"balance_not_enough_pause": "由于 AI 积分不足,暂停运行工作流", "balance_not_enough_pause": "由于 AI 积分不足,暂停运行工作流",
@ -63,6 +65,12 @@
"is_chatting": "正在聊天中...请等待结束", "is_chatting": "正在聊天中...请等待结束",
"items": "条", "items": "条",
"llm_tokens": "LLM tokens", "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": "工作流总运行时间", "module_runtime_and": "工作流总运行时间",
"multiple_AI_conversations": "多组 AI 对话", "multiple_AI_conversations": "多组 AI 对话",
"new_input_guide_lexicon": "新词库", "new_input_guide_lexicon": "新词库",

View File

@ -102,6 +102,7 @@
"add_new": "新增", "add_new": "新增",
"add_new_param": "新增参数", "add_new_param": "新增参数",
"add_success": "添加成功", "add_success": "添加成功",
"aipoint_desc": "每次调用 AI 模型时,都会消耗一定的 AI 积分(类似于 token。点击可查看详细计算规则。",
"all_quotes": "全部引用", "all_quotes": "全部引用",
"all_result": "完整结果", "all_result": "完整结果",
"app_evaluation": "Agent 评测(Beta)", "app_evaluation": "Agent 评测(Beta)",
@ -210,10 +211,13 @@
"comfirm_leave_page": "确认离开该页面?", "comfirm_leave_page": "确认离开该页面?",
"comfirn_create": "确认创建", "comfirn_create": "确认创建",
"commercial_function_tip": "请升级商业版后使用该功能https://doc.fastgpt.cn/docs/introduction/commercial/", "commercial_function_tip": "请升级商业版后使用该功能https://doc.fastgpt.cn/docs/introduction/commercial/",
"community_support": "社区免费支持",
"comon.Continue_Adding": "继续添加", "comon.Continue_Adding": "继续添加",
"compliance.chat": "内容由第三方 AI 生成,无法确保真实准确,仅供参考", "compliance.chat": "内容由第三方 AI 生成,无法确保真实准确,仅供参考",
"compliance.dataset": "请确保您的内容严格遵守相关法律法规,避免包含任何违法或侵权的内容。请谨慎上传可能涉及敏感信息的资料。", "compliance.dataset": "请确保您的内容严格遵守相关法律法规,避免包含任何违法或侵权的内容。请谨慎上传可能涉及敏感信息的资料。",
"confirm_choice": "确认选择", "confirm_choice": "确认选择",
"confirm_input_delete_placeholder": "请输入: {{confirmText}}",
"confirm_input_delete_tip": "请输入 <bold>{{confirmText}}</bold> 确认",
"confirm_logout": "确认退出登录?", "confirm_logout": "确认退出登录?",
"confirm_move": "移动到这", "confirm_move": "移动到这",
"confirm_update": "确认更新", "confirm_update": "确认更新",
@ -946,6 +950,18 @@
"move.confirm": "确认移动", "move.confirm": "确认移动",
"move_success": "移动成功", "move_success": "移动成功",
"move_to": "移动到", "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": "名称",
"name_is_empty": "名称不能为空", "name_is_empty": "名称不能为空",
"navbar.Account": "账号", "navbar.Account": "账号",
@ -981,7 +997,6 @@
"option": "选项", "option": "选项",
"page": "页", "page": "页",
"page_center": "页面居中", "page_center": "页面居中",
"page_error": "出现未捕获的异常。\n1. 私有部署用户90%是由于模型配置不正确/模型未启用导致。。\n2. 部分系统不兼容相关API。大部分是苹果的safari 浏览器导致,可以尝试更换 chrome。\n3. 请关闭浏览器翻译功能,部分翻译导致页面崩溃。\n\n排除3后打开控制台的 console 查看具体报错信息。\n如果提示 xxx undefined 的话,就是模型配置不正确,检查:\n1. 请确保系统内每个系列模型至少有一个可用,可以在【账号-模型提供商】中检查。\n2. 请确保至少有一个知识库文件处理模型(语言模型中有一个开关),否则知识库创建会报错。\n2. 检查模型中一些“对象”参数是否异常(数组和对象),如果为空,可以尝试给个空数组或空对象。",
"pay.amount": "金额", "pay.amount": "金额",
"pay.error_desc": "转换支付途径时出现了问题", "pay.error_desc": "转换支付途径时出现了问题",
"pay.noclose": "支付完成后,请等待系统自动更新", "pay.noclose": "支付完成后,请等待系统自动更新",
@ -1047,6 +1062,7 @@
"price_over_wx_limit": "超出支付提供商限额:微信支付仅支持 6000 元以下", "price_over_wx_limit": "超出支付提供商限额:微信支付仅支持 6000 元以下",
"prompt_input_placeholder": "请输入提示词", "prompt_input_placeholder": "请输入提示词",
"psw_inconsistency": "两次密码不一致", "psw_inconsistency": "两次密码不一致",
"qpm_desc": "主要指团队每分钟请求 Agent 的最大次数,与单个 Agent 复杂度无关。其他 OpenAPI 接口也受此影响,每个接口单独计算",
"question_feedback": "工单咨询", "question_feedback": "工单咨询",
"read_course": "查看教程", "read_course": "查看教程",
"read_doc": "查看文档", "read_doc": "查看文档",
@ -1194,7 +1210,6 @@
"support.wallet.noBill": "无账单记录~", "support.wallet.noBill": "无账单记录~",
"support.wallet.no_invoice": "暂无开票记录", "support.wallet.no_invoice": "暂无开票记录",
"support.wallet.subscription.AI points": "AI 积分", "support.wallet.subscription.AI points": "AI 积分",
"aipoint_desc": "每次调用 AI 模型时,都会消耗一定的 AI 积分(类似于 token。点击可查看详细计算规则。",
"support.wallet.subscription.AI points usage": "AI 积分使用量", "support.wallet.subscription.AI points usage": "AI 积分使用量",
"support.wallet.subscription.AI points usage tip": "每次调用 AI 模型时,都会消耗一定的 AI 积分。具体的计算标准可参考上方的“计费标准”", "support.wallet.subscription.AI points usage tip": "每次调用 AI 模型时,都会消耗一定的 AI 积分。具体的计算标准可参考上方的“计费标准”",
"support.wallet.subscription.Ai points": "AI 积分计算标准", "support.wallet.subscription.Ai points": "AI 积分计算标准",
@ -1224,22 +1239,7 @@
"support.wallet.subscription.Upgrade plan": "升级套餐", "support.wallet.subscription.Upgrade plan": "升级套餐",
"support.wallet.subscription.ai_model": "AI语言模型", "support.wallet.subscription.ai_model": "AI语言模型",
"support.wallet.subscription.eval_items_count": "单次评测数据条数: {{count}} 条", "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 社区免费获取帮助和技术支持", "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.Month": "按月",
"support.wallet.subscription.mode.Period": "订阅周期", "support.wallet.subscription.mode.Period": "订阅周期",
"support.wallet.subscription.mode.Year": "按年", "support.wallet.subscription.mode.Year": "按年",
@ -1397,6 +1397,7 @@
"user_leaved": "离开", "user_leaved": "离开",
"value": "值", "value": "值",
"verification": "验证", "verification": "验证",
"worker_order_support_time": "{{amount}} 小时工单支持响应",
"xx_search_result": "{{key}} 的搜索结果", "xx_search_result": "{{key}} 的搜索结果",
"yes": "是", "yes": "是",
"yesterday": "昨天", "yesterday": "昨天",

View File

@ -69,8 +69,8 @@
"config_file_upload": "點選設定檔案上傳規則", "config_file_upload": "點選設定檔案上傳規則",
"config_question_guide": "設定猜你想問", "config_question_guide": "設定猜你想問",
"confirm_copy_app_tip": "系統將為您建立一個相同設定的應用程式,但權限不會複製,請確認!", "confirm_copy_app_tip": "系統將為您建立一個相同設定的應用程式,但權限不會複製,請確認!",
"confirm_del_app_tip": "確認刪除【{{name}}】及其所有聊天紀錄?", "confirm_del_app_tip": "確認刪除該應用?\n刪除應用會將其關聯的對話記錄一併刪除。",
"confirm_delete_folder_tip": "確認刪除這個資料夾?將會刪除它底下所有應用程式及對應的對話紀錄,請確認!", "confirm_delete_folder_tip": "刪除該文件夾時,將會刪除它下面所有應用及對應的聊天記錄。",
"confirm_delete_tool": "確認刪除該工具?", "confirm_delete_tool": "確認刪除該工具?",
"copilot_config_message": "當前節點配置信息: \n代碼類型{{codeType}} \n當前代碼 \\\\`\\\\`\\\\`{{codeType}} \n{{code}} \\\\`\\\\`\\\\` \n輸入參數 {{inputs}} \n輸出參數 {{outputs}}", "copilot_config_message": "當前節點配置信息: \n代碼類型{{codeType}} \n當前代碼 \\\\`\\\\`\\\\`{{codeType}} \n{{code}} \\\\`\\\\`\\\\` \n輸入參數 {{inputs}} \n輸出參數 {{outputs}}",
"copilot_confirm_message": "已接收到原始配置,了解當前代碼結構和輸入輸出參數。\n請說明您的優化需求。", "copilot_confirm_message": "已接收到原始配置,了解當前代碼結構和輸入輸出參數。\n請說明您的優化需求。",
@ -181,10 +181,17 @@
"local_upload": "本地上傳", "local_upload": "本地上傳",
"log_chat_logs": "對話日誌", "log_chat_logs": "對話日誌",
"log_detail": "日誌詳情", "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_data": "數據看板",
"logs_app_result": "應用效果", "logs_app_result": "應用效果",
"logs_average_response_time": "平均運行時長", "logs_average_response_time": "平均運行時長",
"logs_average_response_time_description": "工作流總運行時間的平均值", "logs_average_response_time_description": "工作流總運行時間的平均值",
"logs_bad_feedback": "差評",
"logs_bad_feedback_only": "僅差評",
"logs_chat_count": "會話次數", "logs_chat_count": "會話次數",
"logs_chat_count_description": "該應用共新建多少個會話。 \n會話定義當與上條消息間隔超過15min認為是產生新會話該定義僅在此生效", "logs_chat_count_description": "該應用共新建多少個會話。 \n會話定義當與上條消息間隔超過15min認為是產生新會話該定義僅在此生效",
"logs_chat_data": "對話數據", "logs_chat_data": "對話數據",
@ -198,7 +205,8 @@
"logs_error_rate_description": "報錯對話佔總對話數量的比例", "logs_error_rate_description": "報錯對話佔總對話數量的比例",
"logs_export_confirm_tip": "當前共有 {{total}} 條對話記錄,每條對話最多可導出最新 100 條消息。\n確認導出", "logs_export_confirm_tip": "當前共有 {{total}} 條對話記錄,每條對話最多可導出最新 100 條消息。\n確認導出",
"logs_export_title": "時間,來源,使用者,聯絡方式,標題,訊息總數,使用者贊同回饋,使用者反對回饋,自定義回饋,標註答案,對話詳細資訊", "logs_export_title": "時間,來源,使用者,聯絡方式,標題,訊息總數,使用者贊同回饋,使用者反對回饋,自定義回饋,標註答案,對話詳細資訊",
"logs_good_feedback": "點贊", "logs_good_feedback": "好評",
"logs_good_feedback_only": "僅好評",
"logs_key_config": "字段配置", "logs_key_config": "字段配置",
"logs_keys_annotatedCount": "標記答案數量", "logs_keys_annotatedCount": "標記答案數量",
"logs_keys_chatDetails": "對話詳情", "logs_keys_chatDetails": "對話詳情",
@ -214,6 +222,7 @@
"logs_keys_source": "來源", "logs_keys_source": "來源",
"logs_keys_title": "標題", "logs_keys_title": "標題",
"logs_keys_user": "使用者", "logs_keys_user": "使用者",
"logs_keys_versionName": "版本名稱",
"logs_message_total": "訊息總數", "logs_message_total": "訊息總數",
"logs_new_user_count": "新增用戶", "logs_new_user_count": "新增用戶",
"logs_points": "積分消耗", "logs_points": "積分消耗",
@ -232,6 +241,7 @@
"logs_total_points": "累計積分消耗", "logs_total_points": "累計積分消耗",
"logs_total_tips": "累計指標不受時間篩選影響", "logs_total_tips": "累計指標不受時間篩選影響",
"logs_total_users": "累計用戶數", "logs_total_users": "累計用戶數",
"logs_unread_only": "僅看未讀",
"logs_user_count": "用戶數", "logs_user_count": "用戶數",
"logs_user_count_description": "單位時間內與該應用產生對話的人數", "logs_user_count_description": "單位時間內與該應用產生對話的人數",
"logs_user_data": "用戶數據", "logs_user_data": "用戶數據",

View File

@ -2,6 +2,8 @@
"AI_input_is_empty": "傳送至 AI 節點的內容為空", "AI_input_is_empty": "傳送至 AI 節點的內容為空",
"Delete_all": "清除所有詞彙", "Delete_all": "清除所有詞彙",
"LLM_model_response_empty": "模型流程回應為空,請檢查模型流程輸出是否正常", "LLM_model_response_empty": "模型流程回應為空,請檢查模型流程輸出是否正常",
"Next": "下一個",
"Previous": "上一個",
"ai_reasoning": "思考過程", "ai_reasoning": "思考過程",
"back_to_text": "返回輸入", "back_to_text": "返回輸入",
"balance_not_enough_pause": "由於 AI 積分不足,暫停運行工作流", "balance_not_enough_pause": "由於 AI 積分不足,暫停運行工作流",
@ -63,6 +65,12 @@
"is_chatting": "對話進行中...請稍候", "is_chatting": "對話進行中...請稍候",
"items": "筆", "items": "筆",
"llm_tokens": "LLM tokens", "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": "模組執行總時間", "module_runtime_and": "模組執行總時間",
"multiple_AI_conversations": "多組 AI 對話", "multiple_AI_conversations": "多組 AI 對話",
"new_input_guide_lexicon": "新增詞彙庫", "new_input_guide_lexicon": "新增詞彙庫",

View File

@ -215,6 +215,8 @@
"compliance.chat": "內容由第三方 AI 產生,無法保證其真實性與準確性,僅供參考。", "compliance.chat": "內容由第三方 AI 產生,無法保證其真實性與準確性,僅供參考。",
"compliance.dataset": "請確保您的內容嚴格遵守相關法律法規,避免包含任何違法或侵權的內容。\n在上傳可能涉及敏感資訊的資料時請務必謹慎。", "compliance.dataset": "請確保您的內容嚴格遵守相關法律法規,避免包含任何違法或侵權的內容。\n在上傳可能涉及敏感資訊的資料時請務必謹慎。",
"confirm_choice": "確認選擇", "confirm_choice": "確認選擇",
"confirm_input_delete_placeholder": "請輸入: {{confirmText}}",
"confirm_input_delete_tip": "請輸入 <bold>{{confirmText}}</bold> 確認",
"confirm_logout": "確認退出登錄?", "confirm_logout": "確認退出登錄?",
"confirm_move": "移動至此", "confirm_move": "移動至此",
"confirm_update": "確認更新", "confirm_update": "確認更新",
@ -944,11 +946,14 @@
"n_agent_amount": "{{amount}} 個 Agent", "n_agent_amount": "{{amount}} 個 Agent",
"n_ai_points": "{{amount}} 積分", "n_ai_points": "{{amount}} 積分",
"n_chat_records_retain": "{{amount}} 天對話紀錄保留", "n_chat_records_retain": "{{amount}} 天對話紀錄保留",
"n_custom_domain_amount": "{{amount}} 個自定義域名",
"n_custom_domain_amount tip": "團隊可以配置的自定義域名數量,目前可用於接入企微智能機器人",
"n_dataset_amount": "{{amount}} 個知識庫", "n_dataset_amount": "{{amount}} 個知識庫",
"n_dataset_size": "{{amount}} 組知識庫索引", "n_dataset_size": "{{amount}} 組知識庫索引",
"n_team_audit_day": "{{amount}} 天團隊操作日誌記錄", "n_team_audit_day": "{{amount}} 天團隊操作日誌記錄",
"n_team_members": "{{amount}} 個團隊成員", "n_team_members": "{{amount}} 個團隊成員",
"n_team_qpm": "{{amount}} QPM", "n_team_qpm": "{{amount}} QPM",
"n_website_sync_max_pages": "單知識庫 {{amount}} 個網頁同步",
"name": "名稱", "name": "名稱",
"name_is_empty": "名稱不能為空", "name_is_empty": "名稱不能為空",
"navbar.Account": "帳戶", "navbar.Account": "帳戶",
@ -982,7 +987,6 @@
"option": "選項", "option": "選項",
"page": "頁", "page": "頁",
"page_center": "頁面置中", "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.amount": "金額",
"pay.error_desc": "轉換支付途徑時出現了問題", "pay.error_desc": "轉換支付途徑時出現了問題",
"pay.noclose": "支付完成後,請等待系統自動更新", "pay.noclose": "支付完成後,請等待系統自動更新",
@ -1224,9 +1228,6 @@
"support.wallet.subscription.Upgrade plan": "升級方案", "support.wallet.subscription.Upgrade plan": "升級方案",
"support.wallet.subscription.ai_model": "AI 語言模型", "support.wallet.subscription.ai_model": "AI 語言模型",
"support.wallet.subscription.function.Community support tip": "可前往 FastGPT 社群免費獲取幫助和技術支持", "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.Month": "按月",
"support.wallet.subscription.mode.Period": "訂閱週期", "support.wallet.subscription.mode.Period": "訂閱週期",
"support.wallet.subscription.mode.Year": "按年", "support.wallet.subscription.mode.Year": "按年",

View File

@ -1,11 +0,0 @@
### 常见问题
- [**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) |

View File

@ -1,24 +0,0 @@
### 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 文件。

View File

@ -1,4 +1,4 @@
import React, { useMemo } from 'react'; import React, { useEffect, useMemo } from 'react';
import { Box, Flex } from '@chakra-ui/react'; import { Box, Flex } from '@chakra-ui/react';
import { useRouter } from 'next/router'; import { useRouter } from 'next/router';
import { useLoading } from '@fastgpt/web/hooks/useLoading'; import { useLoading } from '@fastgpt/web/hooks/useLoading';
@ -70,11 +70,12 @@ const Layout = ({ children }: { children: JSX.Element }) => {
const { toast } = useToast(); const { toast } = useToast();
const { t } = useTranslation(); const { t } = useTranslation();
const { Loading } = useLoading(); const { Loading } = useLoading();
const { loading, feConfigs, llmModelList, embeddingModelList } = useSystemStore(); const { setLastRoute, loading, feConfigs, llmModelList, embeddingModelList } = useSystemStore();
const { isPc } = useSystem(); const { isPc } = useSystem();
const { userInfo, isUpdateNotification, setIsUpdateNotification } = useUserStore(); const { userInfo, isUpdateNotification, setIsUpdateNotification } = useUserStore();
const { setUserDefaultLng } = useI18nLng(); const { setUserDefaultLng } = useI18nLng();
// Auto redeem coupon
useCheckCoupon(); useCheckCoupon();
const isChatPage = useMemo( const isChatPage = useMemo(
@ -127,6 +128,11 @@ const Layout = ({ children }: { children: JSX.Element }) => {
} }
); );
// Route watch
useEffect(() => {
setLastRoute(router.pathname);
}, [router.pathname]);
return ( return (
<> <>
<Box h={'100%'} bg={'myGray.100'}> <Box h={'100%'} bg={'myGray.100'}>

View File

@ -1,5 +1,5 @@
import { useCopyData } from '@fastgpt/web/hooks/useCopyData'; import { useCopyData } from '@fastgpt/web/hooks/useCopyData';
import { Flex, type FlexProps, css, useTheme } from '@chakra-ui/react'; import { Flex, type FlexProps, Box, Button } from '@chakra-ui/react';
import { type ChatSiteItemType } from '@fastgpt/global/core/chat/type'; import { type ChatSiteItemType } from '@fastgpt/global/core/chat/type';
import MyTooltip from '@fastgpt/web/components/common/MyTooltip'; import MyTooltip from '@fastgpt/web/components/common/MyTooltip';
import React, { useMemo } from 'react'; import React, { useMemo } from 'react';
@ -11,6 +11,8 @@ import { ChatBoxContext } from '../Provider';
import { useContextSelector } from 'use-context-selector'; import { useContextSelector } from 'use-context-selector';
import MyImage from '@fastgpt/web/components/common/Image/MyImage'; import MyImage from '@fastgpt/web/components/common/Image/MyImage';
import { ChatRecordContext } from '@/web/core/chat/context/chatRecordContext'; import { ChatRecordContext } from '@/web/core/chat/context/chatRecordContext';
import { useRequest2 } from '@fastgpt/web/hooks/useRequest';
import { eventBus, EventNameEnum } from '@/web/common/utils/eventbus';
export type ChatControllerProps = { export type ChatControllerProps = {
isLastChild: boolean; isLastChild: boolean;
@ -19,10 +21,11 @@ export type ChatControllerProps = {
onRetry?: () => void; onRetry?: () => void;
onDelete?: () => void; onDelete?: () => void;
onMark?: () => void; onMark?: () => void;
onReadUserDislike?: () => void;
onCloseUserLike?: () => void;
onAddUserLike?: () => void; onAddUserLike?: () => void;
onAddUserDislike?: () => void; onAddUserDislike?: () => void;
onToggleFeedbackReadStatus?: () => void;
showFeedbackContent?: boolean;
onToggleFeedbackContent?: () => void;
}; };
const controlIconStyle = { const controlIconStyle = {
@ -41,13 +44,14 @@ const controlContainerStyle = {
const ChatController = ({ const ChatController = ({
chat, chat,
showVoiceIcon, showVoiceIcon,
onReadUserDislike,
onCloseUserLike,
onMark, onMark,
onRetry, onRetry,
onDelete, onDelete,
onAddUserDislike, onAddUserDislike,
onAddUserLike onAddUserLike,
onToggleFeedbackReadStatus,
showFeedbackContent,
onToggleFeedbackContent
}: ChatControllerProps & FlexProps) => { }: ChatControllerProps & FlexProps) => {
const { t } = useTranslation(); const { t } = useTranslation();
const { copyData } = useCopyData(); const { copyData } = useCopyData();
@ -66,178 +70,272 @@ const ChatController = ({
const chatText = useMemo(() => formatChatValue2InputType(chat.value).text || '', [chat.value]); const chatText = useMemo(() => formatChatValue2InputType(chat.value).text || '', [chat.value]);
const isLogMode = chatType === 'log';
const {
runAsync: requestOnToggleFeedbackReadStatus,
loading: isLoadingOnToggleFeedbackReadStatus
} = useRequest2(async () => onToggleFeedbackReadStatus?.(), {
manual: true,
onSuccess: () => {
eventBus.emit(EventNameEnum.refreshFeedback);
}
});
return ( return (
<Flex <>
{...controlContainerStyle} <Flex alignItems={'center'} gap={2}>
borderRadius={'sm'} <Flex
overflow={'hidden'} {...controlContainerStyle}
border={'base'} borderRadius={'sm'}
// 最后一个子元素没有border border={'base'}
css={css({ alignItems={'center'}
'& > *:last-child, & > *:last-child svg': { sx={{
borderRight: 'none', '& > :first-child svg': {
borderRadius: 'md' borderTopLeftRadius: 'sm',
} borderBottomLeftRadius: 'sm'
})} },
> '& > :last-child svg': {
<MyTooltip label={t('common:Copy')}> borderRight: 'none',
<MyIcon borderTopRightRadius: 'sm',
{...controlIconStyle} borderBottomRightRadius: 'sm'
name={'copy'} }
_hover={{ color: 'primary.600' }} }}
onClick={() => copyData(chatText)} >
/> <MyTooltip label={t('common:Copy')}>
</MyTooltip>
{!!onDelete && !isChatting && chatType !== 'log' && (
<>
{onRetry && (
<MyTooltip label={t('common:core.chat.retry')}>
<MyIcon
{...controlIconStyle}
name={'common/retryLight'}
_hover={{ color: 'green.500' }}
onClick={onRetry}
/>
</MyTooltip>
)}
<MyTooltip label={t('common:Delete')}>
<MyIcon <MyIcon
{...controlIconStyle} {...controlIconStyle}
name={'delete'} name={'copy'}
_hover={{ color: 'red.600' }} _hover={{ color: 'primary.600' }}
onClick={onDelete} onClick={() => copyData(chatText)}
/> />
</MyTooltip> </MyTooltip>
</> {!!onDelete && !isChatting && chatType !== 'log' && (
)} <>
{showVoiceIcon && {onRetry && (
hasAudio && <MyTooltip label={t('common:core.chat.retry')}>
(() => {
const isPlayingChat = chat.dataId === audioPlayingChatId;
if (isPlayingChat && audioPlaying) {
return (
<Flex alignItems={'center'}>
<MyTooltip label={t('common:core.chat.tts.Stop Speech')}>
<MyIcon <MyIcon
{...controlIconStyle} {...controlIconStyle}
borderRight={'none'} name={'common/retryLight'}
name={'core/chat/stopSpeech'} _hover={{ color: 'green.500' }}
color={'#E74694'} onClick={onRetry}
onClick={cancelAudio}
/> />
</MyTooltip> </MyTooltip>
<MyImage src="/icon/speaking.gif" w={'23px'} alt={''} borderRight={'base'} /> )}
</Flex> <MyTooltip label={t('common:Delete')}>
); <MyIcon
} {...controlIconStyle}
if (isPlayingChat && audioLoading) { name={'delete'}
return ( _hover={{ color: 'red.600' }}
<MyTooltip label={t('common:Loading')}> onClick={onDelete}
<MyIcon {...controlIconStyle} name={'common/loading'} /> />
</MyTooltip> </MyTooltip>
); </>
} )}
return ( {showVoiceIcon &&
<MyTooltip label={t('common:core.app.TTS start')}> hasAudio &&
<MyIcon (() => {
{...controlIconStyle} const isPlayingChat = chat.dataId === audioPlayingChatId;
name={'common/voiceLight'} if (isPlayingChat && audioPlaying) {
_hover={{ color: '#E74694' }} return (
onClick={async () => { <Flex alignItems={'center'}>
setAudioPlayingChatId(chat.dataId); <MyTooltip label={t('common:core.chat.tts.Stop Speech')}>
const response = await playAudioByText({ <MyIcon
buffer: chat.ttsBuffer, {...controlIconStyle}
text: chatText borderRight={'none'}
}); name={'core/chat/stopSpeech'}
color={'#E74694'}
onClick={cancelAudio}
/>
</MyTooltip>
<MyImage src="/icon/speaking.gif" w={'23px'} alt={''} borderRight={'base'} />
</Flex>
);
}
if (isPlayingChat && audioLoading) {
return (
<MyTooltip label={t('common:Loading')}>
<MyIcon {...controlIconStyle} name={'common/loading'} />
</MyTooltip>
);
}
return (
<MyTooltip label={t('common:core.app.TTS start')}>
<MyIcon
{...controlIconStyle}
name={'common/voiceLight'}
_hover={{ color: '#E74694' }}
onClick={async () => {
setAudioPlayingChatId(chat.dataId);
const response = await playAudioByText({
buffer: chat.ttsBuffer,
text: chatText
});
if (!setChatRecords || !response.buffer) return; if (!setChatRecords || !response.buffer) return;
setChatRecords((state) => setChatRecords((state) =>
state.map((item) => state.map((item) =>
item.dataId === chat.dataId item.dataId === chat.dataId
? {
...item,
ttsBuffer: response.buffer
}
: item
)
);
}}
/>
</MyTooltip>
);
})()}
{!!onMark && (
<MyTooltip label={t('common:core.chat.Mark')}>
<MyIcon
{...controlIconStyle}
name={'core/app/markLight'}
_hover={{ color: '#67c13b' }}
onClick={onMark}
/>
</MyTooltip>
)}
{chat.obj === ChatRoleEnum.AI && (
<>
{/* 日志模式下,始终展示赞/踩 */}
{isLogMode ? (
<>
{!!chat.userGoodFeedback && (
<Box position={'relative'}>
<MyIcon
{...controlIconStyle}
color={'green.500'}
name={'core/chat/feedback/goodLight'}
cursor={'not-allowed'}
/>
{!chat.isFeedbackRead && (
<Box
position={'absolute'}
top={'-2px'}
right={'-2px'}
w={'8px'}
h={'8px'}
bg={'red.500'}
borderRadius={'full'}
border={'1px solid white'}
/>
)}
</Box>
)}
{!!chat.userBadFeedback && (
<Box position={'relative'}>
<MyIcon
{...controlIconStyle}
color={'yellow.500'}
name={'core/chat/feedback/badLight'}
cursor={'not-allowed'}
/>
{!chat.isFeedbackRead && (
<Box
position={'absolute'}
top={'-2px'}
right={'-2px'}
w={'8px'}
h={'8px'}
bg={'red.500'}
borderRadius={'full'}
border={'1px solid white'}
/>
)}
</Box>
)}
</>
) : (
<>
{!!onAddUserLike && (
<MyIcon
{...controlIconStyle}
{...(!!chat.userGoodFeedback
? { ? {
...item, color: 'white',
ttsBuffer: response.buffer bg: 'green.500'
} }
: item : {
) _hover: { color: 'green.600' }
); })}
}} borderRight={!onAddUserDislike ? 'none' : 'base'}
/> borderRightRadius={!onAddUserDislike ? 'sm' : 'none'}
</MyTooltip> name={'core/chat/feedback/goodLight'}
); onClick={onAddUserLike}
})()} />
{!!onMark && ( )}
<MyTooltip label={t('common:core.chat.Mark')}> {!!onAddUserDislike && (
<MyIcon <MyIcon
{...controlIconStyle} {...controlIconStyle}
name={'core/app/markLight'} {...(!!chat.userBadFeedback
_hover={{ color: '#67c13b' }} ? {
onClick={onMark} color: 'white',
/> bg: 'yellow.500'
</MyTooltip> }
)} : {
{chat.obj === ChatRoleEnum.AI && ( _hover: { color: 'yellow.500' }
<> })}
{!!onCloseUserLike && chat.userGoodFeedback && ( borderRight={'none'}
<MyTooltip label={t('common:core.chat.feedback.Close User Like')}> borderRightRadius={'sm'}
<MyIcon name={'core/chat/feedback/badLight'}
{...controlIconStyle} onClick={onAddUserDislike}
color={'white'} />
bg={'green.500'} )}
fontWeight={'bold'} </>
name={'core/chat/feedback/goodLight'} )}
onClick={onCloseUserLike} </>
/>
</MyTooltip>
)} )}
{!!onReadUserDislike && chat.userBadFeedback && ( </Flex>
<MyTooltip label={t('common:core.chat.feedback.Read User dislike')}>
<MyIcon {onToggleFeedbackReadStatus &&
{...controlIconStyle} chat.obj === ChatRoleEnum.AI &&
color={'white'} (chat.userGoodFeedback || chat.userBadFeedback) && (
bg={'#FC9663'} <>
fontWeight={'bold'} {chat.isFeedbackRead ? (
name={'core/chat/feedback/badLight'} <Button
onClick={onReadUserDislike} variant={'unstyled'}
/> alignItems={'center'}
</MyTooltip> fontSize={'xs'}
color={'myGray.500'}
cursor={'pointer'}
_hover={{ color: 'primary.600' }}
isLoading={isLoadingOnToggleFeedbackReadStatus}
onClick={requestOnToggleFeedbackReadStatus}
>
{t('chat:log.feedback.read')}
</Button>
) : (
<Button
size={'xs'}
variant={'whitePrimaryOutline'}
fontSize={'xs'}
h={'22px'}
isLoading={isLoadingOnToggleFeedbackReadStatus}
onClick={requestOnToggleFeedbackReadStatus}
>
{t('chat:log.feedback.mark_as_read')}
</Button>
)}
{chat.userBadFeedback && onToggleFeedbackContent && !showFeedbackContent && (
<Button
size={'xs'}
variant={'grayGhost'}
fontSize={'xs'}
h={'22px'}
onClick={onToggleFeedbackContent}
color={'primary.600'}
>
{t('chat:log.feedback.show_feedback')}
</Button>
)}
</>
)} )}
{!!onAddUserLike && ( </Flex>
<MyIcon </>
{...controlIconStyle}
{...(!!chat.userGoodFeedback
? {
color: 'white',
bg: 'green.500',
fontWeight: 'bold'
}
: {
_hover: { color: 'green.600' }
})}
name={'core/chat/feedback/goodLight'}
onClick={onAddUserLike}
/>
)}
{!!onAddUserDislike && (
<MyIcon
{...controlIconStyle}
{...(!!chat.userBadFeedback
? {
color: 'white',
bg: '#FC9663',
fontWeight: 'bold',
onClick: onAddUserDislike
}
: {
_hover: { color: '#FB7C3C' },
onClick: onAddUserDislike
})}
name={'core/chat/feedback/badLight'}
/>
)}
</>
)}
</Flex>
); );
}; };

View File

@ -1,5 +1,5 @@
import { Box, type BoxProps, Card, Flex } from '@chakra-ui/react'; import { Box, type BoxProps, Card, Flex, Button } from '@chakra-ui/react';
import React, { useCallback, useEffect, useMemo, useRef } from 'react'; import React, { useMemo, useState } from 'react';
import ChatController, { type ChatControllerProps } from './ChatController'; import ChatController, { type ChatControllerProps } from './ChatController';
import ChatAvatar from './ChatAvatar'; import ChatAvatar from './ChatAvatar';
import { MessageCardStyle } from '../constants'; import { MessageCardStyle } from '../constants';
@ -37,6 +37,7 @@ import { addStatisticalDataToHistoryItem } from '@/global/core/chat/utils';
import dynamic from 'next/dynamic'; import dynamic from 'next/dynamic';
import { useMemoizedFn } from 'ahooks'; import { useMemoizedFn } from 'ahooks';
import ChatBoxDivider from '../../../Divider'; import ChatBoxDivider from '../../../Divider';
import { useMemoEnhance } from '@fastgpt/web/hooks/useMemoEnhance';
const ResponseTags = dynamic(() => import('./ResponseTags')); const ResponseTags = dynamic(() => import('./ResponseTags'));
@ -131,29 +132,34 @@ const AIContentCard = React.memo(function AIContentCard({
const ChatItem = (props: Props) => { const ChatItem = (props: Props) => {
const { type, avatar, statusBoxData, children, isLastChild, questionGuides = [], chat } = props; const { type, avatar, statusBoxData, children, isLastChild, questionGuides = [], chat } = props;
const { t } = useTranslation();
const { isPc } = useSystem(); const { isPc } = useSystem();
const styleMap: BoxProps = { const [showFeedbackContent, setShowFeedbackContent] = useState(false);
...(type === ChatRoleEnum.Human
? { const styleMap: BoxProps = useMemoEnhance(
order: 0, () => ({
borderRadius: '8px 0 8px 8px', ...(type === ChatRoleEnum.Human
justifyContent: 'flex-end', ? {
textAlign: 'right', order: 0,
bg: 'primary.100' borderRadius: '8px 0 8px 8px',
} justifyContent: 'flex-end',
: { textAlign: 'right',
order: 1, bg: 'primary.100'
borderRadius: '0 8px 8px 8px', }
justifyContent: 'flex-start', : {
textAlign: 'left', order: 1,
bg: 'myGray.50' borderRadius: '0 8px 8px 8px',
}), justifyContent: 'flex-start',
fontSize: 'mini', textAlign: 'left',
fontWeight: '400', bg: 'myGray.50'
color: 'myGray.500' }),
}; fontSize: 'mini',
const { t } = useTranslation(); fontWeight: '400',
color: 'myGray.500'
}),
[type]
);
const isChatting = useContextSelector(ChatBoxContext, (v) => v.isChatting); const isChatting = useContextSelector(ChatBoxContext, (v) => v.isChatting);
const chatType = useContextSelector(ChatBoxContext, (v) => v.chatType); const chatType = useContextSelector(ChatBoxContext, (v) => v.chatType);
@ -280,6 +286,7 @@ const ChatItem = (props: Props) => {
return ( return (
<Box <Box
data-chat-id={chat.dataId}
_hover={{ _hover={{
'& .time-label': { '& .time-label': {
display: 'block' display: 'block'
@ -304,7 +311,12 @@ const ChatItem = (props: Props) => {
}).replace('#', ':')} }).replace('#', ':')}
</Box> </Box>
)} )}
<ChatController {...props} isLastChild={isLastChild} /> <ChatController
{...props}
isLastChild={isLastChild}
showFeedbackContent={showFeedbackContent}
onToggleFeedbackContent={() => setShowFeedbackContent(!showFeedbackContent)}
/>
</Flex> </Flex>
)} )}
<ChatAvatar src={avatar} type={type} /> <ChatAvatar src={avatar} type={type} />
@ -333,6 +345,37 @@ const ChatItem = (props: Props) => {
</Flex> </Flex>
)} )}
</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 */} {/* content */}
{splitAiResponseResults.map((value, i) => ( {splitAiResponseResults.map((value, i) => (
<Box <Box

View File

@ -1,25 +0,0 @@
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);

View File

@ -3,7 +3,7 @@ import { ModalBody, Textarea, ModalFooter, Button } from '@chakra-ui/react';
import MyModal from '@fastgpt/web/components/common/MyModal'; import MyModal from '@fastgpt/web/components/common/MyModal';
import { useRequest } from '@fastgpt/web/hooks/useRequest'; import { useRequest } from '@fastgpt/web/hooks/useRequest';
import { useTranslation } from 'next-i18next'; 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 { useContextSelector } from 'use-context-selector';
import { WorkflowRuntimeContext } from '../../context/workflowRuntimeContext'; import { WorkflowRuntimeContext } from '../../context/workflowRuntimeContext';

View File

@ -1,34 +0,0 @@
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);

View File

@ -22,10 +22,11 @@ import { useSystemStore } from '@/web/common/system/useSystemStore';
import { useTranslation } from 'next-i18next'; import { useTranslation } from 'next-i18next';
import { import {
closeCustomFeedback, closeCustomFeedback,
delChatRecordById,
updateChatAdminFeedback, updateChatAdminFeedback,
updateChatUserFeedback updateChatUserFeedback,
} from '@/web/core/chat/api'; updateFeedbackReadStatus
} from '@/web/core/chat/feedback/api';
import { delChatRecordById } from '@/web/core/chat/api';
import type { AdminMarkType } from './components/SelectMarkCollection'; import type { AdminMarkType } from './components/SelectMarkCollection';
import MyTooltip from '@fastgpt/web/components/common/MyTooltip'; import MyTooltip from '@fastgpt/web/components/common/MyTooltip';
import { postQuestionGuide } from '@/web/core/ai/api'; import { postQuestionGuide } from '@/web/core/ai/api';
@ -68,9 +69,7 @@ import { formatTime2YMDHMS } from '@fastgpt/global/common/string/time';
import { TeamErrEnum } from '@fastgpt/global/common/error/code/team'; import { TeamErrEnum } from '@fastgpt/global/common/error/code/team';
const FeedbackModal = dynamic(() => import('./components/FeedbackModal')); const FeedbackModal = dynamic(() => import('./components/FeedbackModal'));
const ReadFeedbackModal = dynamic(() => import('./components/ReadFeedbackModal'));
const SelectMarkCollection = dynamic(() => import('./components/SelectMarkCollection')); const SelectMarkCollection = dynamic(() => import('./components/SelectMarkCollection'));
const Empty = dynamic(() => import('./components/Empty'));
const WelcomeBox = dynamic(() => import('./components/WelcomeBox')); const WelcomeBox = dynamic(() => import('./components/WelcomeBox'));
const VariableInputForm = dynamic(() => import('./components/VariableInputForm')); const VariableInputForm = dynamic(() => import('./components/VariableInputForm'));
const ChatHomeVariablesForm = dynamic(() => import('./components/home/ChatHomeVariablesForm')); const ChatHomeVariablesForm = dynamic(() => import('./components/home/ChatHomeVariablesForm'));
@ -90,7 +89,6 @@ type Props = OutLinkChatAuthProps &
feedbackType?: `${FeedbackTypeEnum}`; feedbackType?: `${FeedbackTypeEnum}`;
showMarkIcon?: boolean; // admin mark dataset showMarkIcon?: boolean; // admin mark dataset
showVoiceIcon?: boolean; showVoiceIcon?: boolean;
showEmptyIntro?: boolean;
active?: boolean; // can use active?: boolean; // can use
showWorkorder?: boolean; showWorkorder?: boolean;
@ -99,6 +97,7 @@ type Props = OutLinkChatAuthProps &
isNewChat?: boolean; isNewChat?: boolean;
} }
>; >;
onTriggerRefresh?: () => void;
}; };
const ChatBox = ({ const ChatBox = ({
@ -106,16 +105,16 @@ const ChatBox = ({
feedbackType = FeedbackTypeEnum.hidden, feedbackType = FeedbackTypeEnum.hidden,
showMarkIcon = false, showMarkIcon = false,
showVoiceIcon = true, showVoiceIcon = true,
showEmptyIntro = false,
active = true, active = true,
showWorkorder, showWorkorder,
onStartChat, onStartChat,
chatType chatType,
onTriggerRefresh
}: Props) => { }: Props) => {
const ScrollContainerRef = useRef<HTMLDivElement>(null); const ScrollContainerRef = useRef<HTMLDivElement>(null);
const { t } = useTranslation(); const { t } = useTranslation();
const { toast } = useToast(); const { toast } = useToast();
const { feConfigs, setNotSufficientModalType } = useSystemStore(); const { setNotSufficientModalType } = useSystemStore();
const { isPc } = useSystem(); const { isPc } = useSystem();
const TextareaDom = useRef<HTMLTextAreaElement>(null); const TextareaDom = useRef<HTMLTextAreaElement>(null);
const chatController = useRef(new AbortController()); const chatController = useRef(new AbortController());
@ -124,10 +123,6 @@ const ChatBox = ({
const [isLoading, setIsLoading] = useState(false); const [isLoading, setIsLoading] = useState(false);
const [feedbackId, setFeedbackId] = useState<string>(); const [feedbackId, setFeedbackId] = useState<string>();
const [readFeedbackData, setReadFeedbackData] = useState<{
dataId: string;
content: string;
}>();
const [adminMarkData, setAdminMarkData] = useState<AdminMarkType & { dataId: string }>(); const [adminMarkData, setAdminMarkData] = useState<AdminMarkType & { dataId: string }>();
const [questionGuides, setQuestionGuide] = useState<string[]>([]); const [questionGuides, setQuestionGuide] = useState<string[]>([]);
@ -144,6 +139,7 @@ const ChatBox = ({
const setChatRecords = useContextSelector(ChatRecordContext, (v) => v.setChatRecords); const setChatRecords = useContextSelector(ChatRecordContext, (v) => v.setChatRecords);
const isChatRecordsLoaded = useContextSelector(ChatRecordContext, (v) => v.isChatRecordsLoaded); const isChatRecordsLoaded = useContextSelector(ChatRecordContext, (v) => v.isChatRecordsLoaded);
const ScrollData = useContextSelector(ChatRecordContext, (v) => v.ScrollData); const ScrollData = useContextSelector(ChatRecordContext, (v) => v.ScrollData);
const itemRefs = useContextSelector(ChatRecordContext, (v) => v.itemRefs);
const appId = useContextSelector(WorkflowRuntimeContext, (v) => v.appId); const appId = useContextSelector(WorkflowRuntimeContext, (v) => v.appId);
const chatId = useContextSelector(WorkflowRuntimeContext, (v) => v.chatId); const chatId = useContextSelector(WorkflowRuntimeContext, (v) => v.chatId);
@ -506,6 +502,7 @@ const ChatBox = ({
requestVariables[item.key] = valueTypeFormat(val, item.valueType); requestVariables[item.key] = valueTypeFormat(val, item.valueType);
}); });
const humanChatId = getNanoid(24);
const responseChatId = getNanoid(24); const responseChatId = getNanoid(24);
// set auto audio playing // set auto audio playing
@ -517,7 +514,8 @@ const ChatBox = ({
const newChatList: ChatSiteItemType[] = [ const newChatList: ChatSiteItemType[] = [
...history, ...history,
{ {
dataId: getNanoid(24), id: humanChatId,
dataId: humanChatId,
obj: ChatRoleEnum.Human, obj: ChatRoleEnum.Human,
time: new Date(), time: new Date(),
hideInUI, hideInUI,
@ -546,6 +544,7 @@ const ChatBox = ({
status: ChatStatusEnum.finish status: ChatStatusEnum.finish
}, },
{ {
id: responseChatId,
dataId: responseChatId, dataId: responseChatId,
obj: ChatRoleEnum.AI, obj: ChatRoleEnum.AI,
value: [ value: [
@ -803,24 +802,6 @@ const ChatBox = ({
} catch (error) {} } catch (error) {}
}; };
}); });
const onCloseUserLike = useMemoizedFn((chat: ChatSiteItemType) => {
if (feedbackType !== FeedbackTypeEnum.admin) return;
return () => {
if (!chat.dataId || !chatId || !appId) return;
setChatRecords((state) =>
state.map((chatItem) =>
chatItem.dataId === chat.dataId ? { ...chatItem, userGoodFeedback: undefined } : chatItem
)
);
updateChatUserFeedback({
appId,
chatId,
dataId: chat.dataId,
userGoodFeedback: undefined,
...outLinkAuthData
});
};
});
const onAddUserDislike = useMemoizedFn((chat: ChatSiteItemType) => { const onAddUserDislike = useMemoizedFn((chat: ChatSiteItemType) => {
if ( if (
feedbackType !== FeedbackTypeEnum.user || feedbackType !== FeedbackTypeEnum.user ||
@ -850,16 +831,6 @@ const ChatBox = ({
return () => setFeedbackId(chat.dataId); return () => setFeedbackId(chat.dataId);
} }
}); });
const onReadUserDislike = useMemoizedFn((chat: ChatSiteItemType) => {
if (feedbackType !== FeedbackTypeEnum.admin || chat.obj !== ChatRoleEnum.AI) return;
return () => {
if (!chat.dataId) return;
setReadFeedbackData({
dataId: chat.dataId || '',
content: chat.userBadFeedback || ''
});
};
});
const onCloseCustomFeedback = useMemoizedFn((chat: ChatSiteItemType, i: number) => { const onCloseCustomFeedback = useMemoizedFn((chat: ChatSiteItemType, i: number) => {
return (e: React.ChangeEvent<HTMLInputElement>) => { return (e: React.ChangeEvent<HTMLInputElement>) => {
if (e.target.checked && appId && chatId && chat.dataId) { if (e.target.checked && appId && chatId && chat.dataId) {
@ -883,26 +854,37 @@ const ChatBox = ({
} }
}; };
}); });
const onToggleFeedbackReadStatus = useMemoizedFn((chat: ChatSiteItemType) => {
if (chatType !== ChatTypeEnum.log || chat.obj !== ChatRoleEnum.AI) return;
return async () => {
if (!appId || !chatId || !chat.dataId) return;
const newReadStatus = !chat.isFeedbackRead;
try {
await updateFeedbackReadStatus({
appId,
chatId,
dataId: chat.dataId,
isRead: newReadStatus
});
setChatRecords((state) =>
state.map((item) =>
item.dataId === chat.dataId
? {
...item,
isFeedbackRead: newReadStatus
}
: item
)
);
onTriggerRefresh?.();
} catch (error) {}
};
});
const showEmpty = useMemo(
() =>
chatType !== ChatTypeEnum.home &&
feConfigs?.show_emptyChat &&
showEmptyIntro &&
chatRecords.length === 0 &&
!commonVariableList?.length &&
!showExternalVariable &&
!welcomeText,
[
chatType,
feConfigs?.show_emptyChat,
showEmptyIntro,
chatRecords.length,
commonVariableList?.length,
showExternalVariable,
welcomeText
]
);
const statusBoxData = useCreation(() => { const statusBoxData = useCreation(() => {
if (!isChatting) return; if (!isChatting) return;
const chatContent = chatRecords[chatRecords.length - 1]; const chatContent = chatRecords[chatRecords.length - 1];
@ -1033,7 +1015,12 @@ const ChatBox = ({
return ( return (
<Box id={'history'}> <Box id={'history'}>
{chatRecords.map((item, index) => ( {chatRecords.map((item, index) => (
<Box key={item.dataId}> <Box
key={item.dataId}
ref={(e) => {
itemRefs.current.set(item.dataId, e);
}}
>
{/* 并且时间和上一条的time相差超过十分钟 */} {/* 并且时间和上一条的time相差超过十分钟 */}
{index !== 0 && {index !== 0 &&
item.time && item.time &&
@ -1067,9 +1054,8 @@ const ChatBox = ({
formatChatValue2InputType(chatRecords[index - 1]?.value)?.text formatChatValue2InputType(chatRecords[index - 1]?.value)?.text
), ),
onAddUserLike: onAddUserLike(item), onAddUserLike: onAddUserLike(item),
onCloseUserLike: onCloseUserLike(item),
onAddUserDislike: onAddUserDislike(item), onAddUserDislike: onAddUserDislike(item),
onReadUserDislike: onReadUserDislike(item) onToggleFeedbackReadStatus: onToggleFeedbackReadStatus(item)
}} }}
> >
{/* custom feedback */} {/* custom feedback */}
@ -1124,11 +1110,11 @@ const ChatBox = ({
questionGuides, questionGuides,
onMark, onMark,
onAddUserLike, onAddUserLike,
onCloseUserLike,
onAddUserDislike, onAddUserDislike,
onReadUserDislike, onToggleFeedbackReadStatus,
t, t,
showMarkIcon, showMarkIcon,
itemRefs,
onCloseCustomFeedback onCloseCustomFeedback
]); ]);
@ -1144,8 +1130,7 @@ const ChatBox = ({
px={[4, 0]} px={[4, 0]}
pb={6} pb={6}
> >
<Box id="chat-container" maxW={['100%', '92%']} h={'100%'} mx={'auto'}> <Box maxW={['100%', '92%']} h={'100%'} mx={'auto'}>
{showEmpty && <Empty />}
{!!welcomeText && <WelcomeBox welcomeText={welcomeText} />} {!!welcomeText && <WelcomeBox welcomeText={welcomeText} />}
{/* variable input */} {/* variable input */}
@ -1157,7 +1142,7 @@ const ChatBox = ({
</Box> </Box>
</ScrollData> </ScrollData>
); );
}, [ScrollData, showEmpty, welcomeText, chatStarted, chatForm, chatType, RecordsBox]); }, [ScrollData, welcomeText, chatStarted, chatForm, chatType, RecordsBox]);
const HomeChatRenderBox = useMemo(() => { const HomeChatRenderBox = useMemo(() => {
return ( return (
<> <>
@ -1248,31 +1233,6 @@ const ChatBox = ({
}} }}
/> />
)} )}
{/* admin read feedback modal */}
{!!readFeedbackData && (
<ReadFeedbackModal
content={readFeedbackData.content}
onClose={() => setReadFeedbackData(undefined)}
onCloseFeedback={() => {
setChatRecords((state) =>
state.map((chatItem) =>
chatItem.dataId === readFeedbackData.dataId
? { ...chatItem, userBadFeedback: undefined }
: chatItem
)
);
try {
if (!chatId || !appId) return;
updateChatUserFeedback({
appId,
chatId,
dataId: readFeedbackData.dataId
});
} catch (error) {}
setReadFeedbackData(undefined);
}}
/>
)}
{/* admin mark data */} {/* admin mark data */}
{!!adminMarkData && ( {!!adminMarkData && (
<SelectMarkCollection <SelectMarkCollection
@ -1299,23 +1259,6 @@ const ChatBox = ({
: chatItem : chatItem
) )
); );
if (readFeedbackData && chatId && appId) {
updateChatUserFeedback({
appId,
chatId,
dataId: readFeedbackData.dataId,
userBadFeedback: undefined
});
setChatRecords((state) =>
state.map((chatItem) =>
chatItem.dataId === readFeedbackData.dataId
? { ...chatItem, userBadFeedback: undefined }
: chatItem
)
);
setReadFeedbackData(undefined);
}
}} }}
/> />
)} )}

View File

@ -16,7 +16,6 @@ import { useTranslation } from 'next-i18next';
import { type ChatBoxInputFormType } from '../ChatBox/type'; import { type ChatBoxInputFormType } from '../ChatBox/type';
import { chats2GPTMessages } from '@fastgpt/global/core/chat/adapt'; import { chats2GPTMessages } from '@fastgpt/global/core/chat/adapt';
import { clientGetWorkflowToolRunUserQuery } from '@fastgpt/global/core/workflow/utils'; import { clientGetWorkflowToolRunUserQuery } from '@fastgpt/global/core/workflow/utils';
import { cloneDeep } from 'lodash';
import { ChatItemContext } from '@/web/core/chat/context/chatItemContext'; import { ChatItemContext } from '@/web/core/chat/context/chatItemContext';
import { ChatRecordContext } from '@/web/core/chat/context/chatRecordContext'; import { ChatRecordContext } from '@/web/core/chat/context/chatRecordContext';
import { type AppFileSelectConfigType } from '@fastgpt/global/core/app/type'; import { type AppFileSelectConfigType } from '@fastgpt/global/core/app/type';
@ -190,6 +189,7 @@ const PluginRunContextProvider = ({
abortRequest(); abortRequest();
const abortSignal = new AbortController(); const abortSignal = new AbortController();
chatController.current = abortSignal; chatController.current = abortSignal;
const humanChatItemId = getNanoid(24);
const responseChatItemId = getNanoid(24); const responseChatItemId = getNanoid(24);
setChatRecords([ setChatRecords([
@ -199,9 +199,12 @@ const PluginRunContextProvider = ({
variables, variables,
files: files as RuntimeUserPromptType['files'] files: files as RuntimeUserPromptType['files']
}), }),
id: humanChatItemId,
dataId: humanChatItemId,
status: 'finish' status: 'finish'
}, },
{ {
id: responseChatItemId,
dataId: responseChatItemId, dataId: responseChatItemId,
obj: ChatRoleEnum.AI, obj: ChatRoleEnum.AI,
value: [ value: [

View File

@ -220,7 +220,7 @@ const ApiKeyTable = ({ tips, appId }: { tips: string; appId?: string }) => {
label: t('common:Delete'), label: t('common:Delete'),
icon: 'delete', icon: 'delete',
type: 'danger', type: 'danger',
onClick: () => openConfirm(() => onclickRemove(_id))() onClick: () => openConfirm({ onConfirm: () => onclickRemove(_id) })()
} }
] ]
} }

View File

@ -51,11 +51,10 @@ const DefaultPermissionList = ({
value={per} value={per}
onChange={(per) => { onChange={(per) => {
if (isInheritPermission && hasParent) { if (isInheritPermission && hasParent) {
openConfirm( openConfirm({
() => onRequestChange(per), onConfirm: () => onRequestChange(per),
undefined, customContent: t('common:permission.Remove InheritPermission Confirm')
t('common:permission.Remove InheritPermission Confirm') })();
)();
} else { } else {
return onRequestChange(per); return onRequestChange(per);
} }

View File

@ -147,7 +147,7 @@ function MemberModal({
newChildClbs newChildClbs
}); });
if (isConflict && isInheritPermission) { if (isConflict && isInheritPermission) {
return openConfirmDisableInheritPer(_onConfirm)(); return openConfirmDisableInheritPer({ onConfirm: _onConfirm })();
} else { } else {
return _onConfirm(); return _onConfirm();
} }

View File

@ -23,17 +23,16 @@ const ResumeInherit = ({
cursor={'pointer'} cursor={'pointer'}
_hover={{ color: 'primary.600' }} _hover={{ color: 'primary.600' }}
onClick={() => { onClick={() => {
openCommonConfirm( openCommonConfirm({
() => onConfirm: () =>
onResume()?.then(() => { onResume()?.then(() => {
toast({ toast({
title: t('common:permission.Resume InheritPermission Success'), title: t('common:permission.Resume InheritPermission Success'),
status: 'success' status: 'success'
}); });
}), }),
undefined, customContent: t('common:permission.Resume InheritPermission Confirm')
t('common:permission.Resume InheritPermission Confirm') })();
)();
}} }}
> >
{t('common:click_to_resume')} {t('common:click_to_resume')}

View File

@ -1,15 +0,0 @@
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>;

View File

@ -1,4 +1,4 @@
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'; import type { PromotionRecordSchema } from '@fastgpt/global/support/activity/type.d';
export interface LoginSuccessResponse { export interface LoginSuccessResponse {
user: UserType; user: UserType;

View File

@ -139,9 +139,11 @@ const AccountContainer = ({
const setCurrentTab = useCallback( const setCurrentTab = useCallback(
(tab: string) => { (tab: string) => {
if (tab === TabEnum.loginout) { if (tab === TabEnum.loginout) {
openConfirm(() => { openConfirm({
setUserInfo(null); onConfirm: () => {
router.replace('/login'); setUserInfo(null);
router.replace('/login');
}
})(); })();
} else { } else {
router.replace('/account/' + tab); router.replace('/account/' + tab);

View File

@ -215,13 +215,12 @@ const ChannelTable = ({ Tab }: { Tab: React.ReactNode }) => {
icon: 'delete', icon: 'delete',
label: t('common:Delete'), label: t('common:Delete'),
onClick: () => onClick: () =>
openConfirm( openConfirm({
() => onDeleteChannel(item.id), onConfirm: () => onDeleteChannel(item.id),
undefined, customContent: t('account_model:confirm_delete_channel', {
t('account_model:confirm_delete_channel', {
name: item.name name: item.name
}) })
)() })()
} }
] ]
} }

View File

@ -188,7 +188,9 @@ function MemberTable({ Tabs }: { Tabs: React.ReactNode }) {
label: t('common:Delete'), label: t('common:Delete'),
icon: 'delete', icon: 'delete',
onClick: () => { onClick: () => {
openDeleteGroupModal(() => delDeleteGroup(group._id))(); openDeleteGroupModal({
onConfirm: () => delDeleteGroup(group._id)
})();
}, },
type: 'danger' as MenuItemType type: 'danger' as MenuItemType
} }

View File

@ -98,7 +98,8 @@ function OrgTable({ Tabs }: { Tabs: React.ReactNode }) {
type: 'delete', type: 'delete',
content: t('account_team:confirm_delete_org') 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, { const { runAsync: deleteOrgReq } = useRequest2(deleteOrg, {
onSuccess: refresh onSuccess: refresh
}); });
@ -229,13 +230,15 @@ function OrgTable({ Tabs }: { Tabs: React.ReactNode }) {
username: member.memberName username: member.memberName
}), }),
onClick: () => { onClick: () => {
openDeleteMemberFromTeamModal( openDeleteMemberFromTeamModal({
() => deleteMemberFromTeamReq(member.tmbId), onConfirm: () => deleteMemberFromTeamReq(member.tmbId),
undefined, customContent: t(
t('account_team:confirm_delete_from_team', { 'account_team:confirm_delete_from_team',
username: member.memberName {
}) username: member.memberName
)(); }
)
})();
} }
}, },
...(isSyncMember ...(isSyncMember
@ -250,8 +253,8 @@ function OrgTable({ Tabs }: { Tabs: React.ReactNode }) {
}, },
label: t('account_team:delete_from_org'), label: t('account_team:delete_from_org'),
onClick: () => onClick: () =>
openDeleteMemberFromOrgModal( openDeleteMemberFromOrgModal({
() => { onConfirm: () => {
if (currentOrg) { if (currentOrg) {
return deleteMemberReq( return deleteMemberReq(
currentOrg._id, currentOrg._id,
@ -259,11 +262,13 @@ function OrgTable({ Tabs }: { Tabs: React.ReactNode }) {
); );
} }
}, },
undefined, customContent: t(
t('account_team:confirm_delete_from_org', { 'account_team:confirm_delete_from_org',
username: member.memberName {
}) username: member.memberName
)() }
)
})()
} }
]) ])
] ]

View File

@ -1,4 +1,4 @@
import React, { useMemo } from 'react'; import React, { useMemo, useState, useCallback } from 'react';
import { Flex, Box } from '@chakra-ui/react'; import { Flex, Box } from '@chakra-ui/react';
import { useTranslation } from 'next-i18next'; import { useTranslation } from 'next-i18next';
import { HUMAN_ICON } from '@fastgpt/global/common/system/constants'; 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 { useContextSelector } from 'use-context-selector';
import ChatQuoteList from '@/pageComponents/chat/ChatQuoteList'; import ChatQuoteList from '@/pageComponents/chat/ChatQuoteList';
import { ChatTypeEnum } from '@/components/core/chat/ChatContainer/ChatBox/constants'; import { ChatTypeEnum } from '@/components/core/chat/ChatContainer/ChatBox/constants';
import { DetailLogsModalFeedbackTypeFilter } from './FeedbackTypeFilter';
const PluginRunBox = dynamic(() => import('@/components/core/chat/ChatContainer/PluginRunBox')); const PluginRunBox = dynamic(() => import('@/components/core/chat/ChatContainer/PluginRunBox'));
const ChatBox = dynamic(() => import('@/components/core/chat/ChatContainer/ChatBox')); const ChatBox = dynamic(() => import('@/components/core/chat/ChatContainer/ChatBox'));
@ -31,10 +32,24 @@ type Props = {
onClose: () => void; 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 { t } = useTranslation();
const { isPc } = useSystem(); 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 resetVariables = useContextSelector(ChatItemContext, (v) => v.resetVariables);
const setChatBoxData = useContextSelector(ChatItemContext, (v) => v.setChatBoxData); const setChatBoxData = useContextSelector(ChatItemContext, (v) => v.setChatBoxData);
const pluginRunTab = useContextSelector(ChatItemContext, (v) => v.pluginRunTab); 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 title = chat?.title;
const chatModels = chat?.app?.chatModels; const chatModels = chat?.app?.chatModels;
const isPlugin = chat?.app.type === AppTypeEnum.workflowTool; const isPlugin = chat?.app.type === AppTypeEnum.workflowTool;
@ -152,45 +176,66 @@ const DetailLogsModal = ({ appId, chatId, onClose }: Props) => {
)} )}
{/* Chat container */} {/* Chat container */}
<Flex pt={2} flex={'1 0 0'} h={0}> <Flex pt={2} flex={'1 0 0'} h={0} flexDirection={'column'}>
<Box flex={'1 0 0'} h={'100%'} overflow={'auto'}> <Flex flex={'1 0 0'} h={0}>
{isPlugin ? ( <Box flex={'1 0 0'} h={'100%'} overflow={'auto'}>
<Box px={5} py={2}> {isPlugin ? (
<PluginRunBox appId={appId} chatId={chatId} /> <Box px={5} py={2}>
</Box> <PluginRunBox appId={appId} chatId={chatId} />
) : ( </Box>
<ChatBox ) : (
isReady <ChatBox
appId={appId} isReady
chatId={chatId} appId={appId}
feedbackType={'admin'} chatId={chatId}
showMarkIcon feedbackType={'admin'}
showVoiceIcon={false} showMarkIcon
chatType={ChatTypeEnum.log} 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> </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> </Flex>
</MyBox> </MyBox>
@ -201,6 +246,8 @@ const DetailLogsModal = ({ appId, chatId, onClose }: Props) => {
const Render = (props: Props) => { const Render = (props: Props) => {
const { appId, chatId } = props; const { appId, chatId } = props;
const [feedbackRecordId, setFeedbackRecordId] = useState<string | undefined>(undefined);
const params = useMemo(() => { const params = useMemo(() => {
return { return {
chatId, chatId,
@ -210,6 +257,10 @@ const Render = (props: Props) => {
}; };
}, [appId, chatId]); }, [appId, chatId]);
const handleRecordChange = useCallback((recordId: string | undefined) => {
setFeedbackRecordId(recordId);
}, []);
return ( return (
<ChatItemContextProvider <ChatItemContextProvider
showRouteToDatasetDetail={true} showRouteToDatasetDetail={true}
@ -218,8 +269,12 @@ const Render = (props: Props) => {
// isShowFullText={true} // isShowFullText={true}
showNodeStatus showNodeStatus
> >
<ChatRecordContextProvider params={params}> <ChatRecordContextProvider params={params} feedbackRecordId={feedbackRecordId}>
<DetailLogsModal {...props} /> <DetailLogsModal
{...props}
feedbackRecordId={feedbackRecordId}
handleRecordChange={handleRecordChange}
/>
</ChatRecordContextProvider> </ChatRecordContextProvider>
</ChatItemContextProvider> </ChatItemContextProvider>
); );

View File

@ -0,0 +1,308 @@
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>
);
};

View File

@ -17,7 +17,7 @@ import { ChatSourceMap } from '@fastgpt/global/core/chat/constants';
import MultipleSelect, { import MultipleSelect, {
useMultipleSelect useMultipleSelect
} from '@fastgpt/web/components/common/MySelect/MultipleSelect'; } from '@fastgpt/web/components/common/MySelect/MultipleSelect';
import React, { useMemo, useState } from 'react'; import React, { useCallback, useMemo, useState } from 'react';
import { useTranslation } from 'next-i18next'; import { useTranslation } from 'next-i18next';
import DateRangePicker from '@fastgpt/web/components/common/DateRangePicker'; import DateRangePicker from '@fastgpt/web/components/common/DateRangePicker';
import { useScrollPagination } from '@fastgpt/web/hooks/useScrollPagination'; import { useScrollPagination } from '@fastgpt/web/hooks/useScrollPagination';
@ -40,17 +40,19 @@ import { downloadFetch } from '@/web/common/system/utils';
import { usePagination } from '@fastgpt/web/hooks/usePagination'; import { usePagination } from '@fastgpt/web/hooks/usePagination';
import { getAppChatLogs } from '@/web/core/app/api/log'; import { getAppChatLogs } from '@/web/core/app/api/log';
import QuestionTip from '@fastgpt/web/components/common/MyTooltip/QuestionTip'; import QuestionTip from '@fastgpt/web/components/common/MyTooltip/QuestionTip';
import type { AppLogsListItemType } from '@/types/app'; import type { AppLogsListItemType } from '@fastgpt/global/openapi/core/app/log/api';
import dayjs from 'dayjs'; import dayjs from 'dayjs';
import UserBox from '@fastgpt/web/components/common/UserBox'; import UserBox from '@fastgpt/web/components/common/UserBox';
import MyIcon from '@fastgpt/web/components/common/Icon'; import MyIcon from '@fastgpt/web/components/common/Icon';
import EmptyTip from '@fastgpt/web/components/common/EmptyTip'; import EmptyTip from '@fastgpt/web/components/common/EmptyTip';
import dynamic from 'next/dynamic'; import dynamic from 'next/dynamic';
import type { HeaderControlProps } from './LogChart'; import type { HeaderControlProps } from './LogChart';
import FeedbackTypeFilter from './FeedbackTypeFilter';
import { useSystemStore } from '@/web/common/system/useSystemStore'; import { useSystemStore } from '@/web/common/system/useSystemStore';
import MyBox from '@fastgpt/web/components/common/MyBox'; import MyBox from '@fastgpt/web/components/common/MyBox';
import { useContextSelector } from 'use-context-selector'; import { useContextSelector } from 'use-context-selector';
import { AppContext } from '../context'; import { AppContext } from '../context';
import { useMemoEnhance } from '@fastgpt/web/hooks/useMemoEnhance';
const DetailLogsModal = dynamic(() => import('./DetailLogsModal')); const DetailLogsModal = dynamic(() => import('./DetailLogsModal'));
@ -70,6 +72,8 @@ const LogTable = ({
const [detailLogsId, setDetailLogsId] = useState<string>(); const [detailLogsId, setDetailLogsId] = useState<string>();
const appName = useContextSelector(AppContext, (v) => v.appDetail.name); const appName = useContextSelector(AppContext, (v) => v.appDetail.name);
const [feedbackType, setFeedbackType] = useState<'all' | 'has_feedback' | 'good' | 'bad'>('all');
const [unreadOnly, setUnreadOnly] = useState<boolean>(false);
// source // source
const sourceList = useMemo( const sourceList = useMemo(
@ -142,45 +146,44 @@ const LogTable = ({
return !isEqual(teamLogKeysList, personalLogKeysList); return !isEqual(teamLogKeysList, personalLogKeysList);
}, [teamLogKeys, logKeys]); }, [teamLogKeys, logKeys]);
const { runAsync: exportLogs } = useRequest2( const { runAsync: exportLogs } = useRequest2(async () => {
async () => { const enabledKeys = logKeys.filter((item) => item.enable).map((item) => item.key);
const enabledKeys = logKeys.filter((item) => item.enable).map((item) => item.key); const headerTitle = enabledKeys.map((k) => t(AppLogKeysEnumMap[k])).join(',');
const headerTitle = enabledKeys.map((k) => t(AppLogKeysEnumMap[k])).join(','); await downloadFetch({
await downloadFetch({ url: '/api/core/app/logs/exportLogs',
url: '/api/core/app/exportChatLogs', filename: t('app:export_log_filename', { name: appName }),
filename: t('app:export_log_filename', { name: appName }), body: {
body: { appId,
appId, dateStart: dayjs(dateRange.from || new Date()).format(),
dateStart: dayjs(dateRange.from || new Date()).format(), dateEnd: dayjs(dateRange.to || new Date()).format(),
dateEnd: dayjs(dateRange.to || new Date()).format(), sources: isSelectAllSource ? undefined : chatSources,
sources: isSelectAllSource ? undefined : chatSources, tmbIds: isSelectAllTmb ? undefined : selectTmbIds,
tmbIds: isSelectAllTmb ? undefined : selectTmbIds, chatSearch,
chatSearch, title: `${headerTitle},${t('app:logs_keys_chatDetails')}`,
title: `${headerTitle},${t('app:logs_keys_chatDetails')}`, logKeys: enabledKeys,
logKeys: enabledKeys, sourcesMap: Object.fromEntries(
sourcesMap: Object.fromEntries( Object.entries(ChatSourceMap).map(([key, config]) => [
Object.entries(ChatSourceMap).map(([key, config]) => [ key,
key, {
{ label: t(config.name as any)
label: t(config.name as any) }
} ])
]) ),
) feedbackType,
} unreadOnly
}); }
}, });
{ });
refreshDeps: [chatSources] const params = useMemoEnhance(
}
);
const params = useMemo(
() => ({ () => ({
appId, appId,
dateStart: dateRange.from!, dateStart: dateRange.from!,
dateEnd: dateRange.to!, dateEnd: dateRange.to!,
sources: isSelectAllSource ? undefined : chatSources, sources: isSelectAllSource ? undefined : chatSources,
tmbIds: isSelectAllTmb ? undefined : selectTmbIds, tmbIds: isSelectAllTmb ? undefined : selectTmbIds,
chatSearch chatSearch,
feedbackType,
unreadOnly: feedbackType === 'all' ? undefined : unreadOnly
}), }),
[ [
appId, appId,
@ -190,7 +193,9 @@ const LogTable = ({
isSelectAllSource, isSelectAllSource,
selectTmbIds, selectTmbIds,
isSelectAllTmb, isSelectAllTmb,
chatSearch chatSearch,
feedbackType,
unreadOnly
] ]
); );
@ -228,7 +233,25 @@ const LogTable = ({
[AppLogKeysEnum.MESSAGE_COUNT]: ( [AppLogKeysEnum.MESSAGE_COUNT]: (
<Th key={AppLogKeysEnum.MESSAGE_COUNT}>{t('app:logs_message_total')}</Th> <Th key={AppLogKeysEnum.MESSAGE_COUNT}>{t('app:logs_message_total')}</Th>
), ),
[AppLogKeysEnum.FEEDBACK]: <Th key={AppLogKeysEnum.FEEDBACK}>{t('app:feedback_count')}</Th>, [AppLogKeysEnum.FEEDBACK]: (
<Th key={AppLogKeysEnum.FEEDBACK}>
<FeedbackTypeFilter
feedbackType={feedbackType}
setFeedbackType={setFeedbackType}
unreadOnly={unreadOnly}
setUnreadOnly={setUnreadOnly}
placement="right"
menuButtonProps={{
fontSize: '12.8px',
fontWeight: 'medium',
color: 'myGray.600',
px: 0,
_hover: {},
_active: {}
}}
/>
</Th>
),
[AppLogKeysEnum.CUSTOM_FEEDBACK]: ( [AppLogKeysEnum.CUSTOM_FEEDBACK]: (
<Th key={AppLogKeysEnum.CUSTOM_FEEDBACK}> <Th key={AppLogKeysEnum.CUSTOM_FEEDBACK}>
{t('common:core.app.feedback.Custom feedback')} {t('common:core.app.feedback.Custom feedback')}
@ -248,102 +271,104 @@ const LogTable = ({
[AppLogKeysEnum.ERROR_COUNT]: ( [AppLogKeysEnum.ERROR_COUNT]: (
<Th key={AppLogKeysEnum.ERROR_COUNT}>{t('app:logs_error_count')}</Th> <Th key={AppLogKeysEnum.ERROR_COUNT}>{t('app:logs_error_count')}</Th>
), ),
[AppLogKeysEnum.POINTS]: <Th key={AppLogKeysEnum.POINTS}>{t('app:logs_points')}</Th> [AppLogKeysEnum.POINTS]: <Th key={AppLogKeysEnum.POINTS}>{t('app:logs_points')}</Th>,
[AppLogKeysEnum.VERSION_NAME]: (
<Th key={AppLogKeysEnum.VERSION_NAME}>{t('app:logs_keys_versionName')}</Th>
)
}),
[t, feedbackType, setFeedbackType, unreadOnly, setUnreadOnly]
);
const getCellRenderMap = useCallback(
(item: AppLogsListItemType) => ({
[AppLogKeysEnum.SOURCE]: (
<Td key={AppLogKeysEnum.SOURCE}>
{/* @ts-ignore */}
{item.sourceName || t(ChatSourceMap[item.source]?.name) || item.source}
</Td>
),
[AppLogKeysEnum.CREATED_TIME]: (
<Td key={AppLogKeysEnum.CREATED_TIME}>
{dayjs(item.createTime).format('YYYY/MM/DD HH:mm')}
</Td>
),
[AppLogKeysEnum.LAST_CONVERSATION_TIME]: (
<Td key={AppLogKeysEnum.LAST_CONVERSATION_TIME}>
{dayjs(item.updateTime).format('YYYY/MM/DD HH:mm')}
</Td>
),
[AppLogKeysEnum.USER]: (
<Td key={AppLogKeysEnum.USER}>
<Box>
{!!item.outLinkUid ? (
item.outLinkUid
) : item.sourceMember ? (
<UserBox sourceMember={item.sourceMember} />
) : (
'-'
)}
</Box>
</Td>
),
[AppLogKeysEnum.REGION]: <Td key={AppLogKeysEnum.REGION}>{item.region || '-'}</Td>,
[AppLogKeysEnum.TITLE]: (
<Td key={AppLogKeysEnum.TITLE} className="textEllipsis" maxW={'250px'}>
{item.customTitle || item.title}
</Td>
),
[AppLogKeysEnum.SESSION_ID]: (
<Td key={AppLogKeysEnum.SESSION_ID} className="textEllipsis" maxW={'200px'}>
{item.chatId || '-'}
</Td>
),
[AppLogKeysEnum.MESSAGE_COUNT]: (
<Td key={AppLogKeysEnum.MESSAGE_COUNT}>{item.messageCount}</Td>
),
[AppLogKeysEnum.FEEDBACK]: (
<Td key={AppLogKeysEnum.FEEDBACK}>
<Flex gap={3} px={1}>
{!!item?.userGoodFeedbackCount && (
<Flex alignItems={'center'}>
<MyIcon mr={1} name={'core/chat/feedback/goodLight'} color={'green.500'} w={4} />
{item.userGoodFeedbackCount}
</Flex>
)}
{!!item?.userBadFeedbackCount && (
<Flex alignItems={'center'}>
<MyIcon mr={1} name={'core/chat/feedback/badLight'} color={'yellow.500'} w={4} />
{item.userBadFeedbackCount}
</Flex>
)}
{!item?.userGoodFeedbackCount && !item?.userBadFeedbackCount && <>-</>}
</Flex>
</Td>
),
[AppLogKeysEnum.CUSTOM_FEEDBACK]: (
<Td key={AppLogKeysEnum.CUSTOM_FEEDBACK}>{item.customFeedbacksCount || '-'}</Td>
),
[AppLogKeysEnum.ANNOTATED_COUNT]: (
<Td key={AppLogKeysEnum.ANNOTATED_COUNT}>{item.markCount}</Td>
),
[AppLogKeysEnum.RESPONSE_TIME]: (
<Td key={AppLogKeysEnum.RESPONSE_TIME}>
{item.averageResponseTime ? `${item.averageResponseTime.toFixed(2)}s` : '-'}
</Td>
),
[AppLogKeysEnum.ERROR_COUNT]: (
<Td key={AppLogKeysEnum.ERROR_COUNT}>{item.errorCount || '-'}</Td>
),
[AppLogKeysEnum.POINTS]: (
<Td key={AppLogKeysEnum.POINTS}>
{item.totalPoints ? `${item.totalPoints.toFixed(2)}` : '-'}
</Td>
),
[AppLogKeysEnum.VERSION_NAME]: (
<Td key={AppLogKeysEnum.VERSION_NAME}>{item.versionName || '-'}</Td>
)
}), }),
[t] [t]
); );
const getCellRenderMap = (item: AppLogsListItemType) => ({
[AppLogKeysEnum.SOURCE]: (
<Td key={AppLogKeysEnum.SOURCE}>
{/* @ts-ignore */}
{item.sourceName || t(ChatSourceMap[item.source]?.name) || item.source}
</Td>
),
[AppLogKeysEnum.CREATED_TIME]: (
<Td key={AppLogKeysEnum.CREATED_TIME}>{dayjs(item.createTime).format('YYYY/MM/DD HH:mm')}</Td>
),
[AppLogKeysEnum.LAST_CONVERSATION_TIME]: (
<Td key={AppLogKeysEnum.LAST_CONVERSATION_TIME}>
{dayjs(item.updateTime).format('YYYY/MM/DD HH:mm')}
</Td>
),
[AppLogKeysEnum.USER]: (
<Td key={AppLogKeysEnum.USER}>
<Box>
{!!item.outLinkUid ? item.outLinkUid : <UserBox sourceMember={item.sourceMember} />}
</Box>
</Td>
),
[AppLogKeysEnum.REGION]: <Td key={AppLogKeysEnum.REGION}>{item.region || '-'}</Td>,
[AppLogKeysEnum.TITLE]: (
<Td key={AppLogKeysEnum.TITLE} className="textEllipsis" maxW={'250px'}>
{item.customTitle || item.title}
</Td>
),
[AppLogKeysEnum.SESSION_ID]: (
<Td key={AppLogKeysEnum.SESSION_ID} className="textEllipsis" maxW={'200px'}>
{item.id || '-'}
</Td>
),
[AppLogKeysEnum.MESSAGE_COUNT]: <Td key={AppLogKeysEnum.MESSAGE_COUNT}>{item.messageCount}</Td>,
[AppLogKeysEnum.FEEDBACK]: (
<Td key={AppLogKeysEnum.FEEDBACK} w={'100px'}>
{!!item?.userGoodFeedbackCount && (
<Flex
mb={item?.userGoodFeedbackCount ? 1 : 0}
bg={'green.100'}
color={'green.600'}
px={3}
py={1}
alignItems={'center'}
justifyContent={'center'}
borderRadius={'md'}
fontWeight={'bold'}
>
<MyIcon mr={1} name={'core/chat/feedback/goodLight'} color={'green.600'} w={'14px'} />
{item.userGoodFeedbackCount}
</Flex>
)}
{!!item?.userBadFeedbackCount && (
<Flex
bg={'#FFF2EC'}
color={'#C96330'}
px={3}
py={1}
alignItems={'center'}
justifyContent={'center'}
borderRadius={'md'}
fontWeight={'bold'}
>
<MyIcon mr={1} name={'core/chat/feedback/badLight'} color={'#C96330'} w={'14px'} />
{item.userBadFeedbackCount}
</Flex>
)}
{!item?.userGoodFeedbackCount && !item?.userBadFeedbackCount && <>-</>}
</Td>
),
[AppLogKeysEnum.CUSTOM_FEEDBACK]: (
<Td key={AppLogKeysEnum.CUSTOM_FEEDBACK}>{item.customFeedbacksCount || '-'}</Td>
),
[AppLogKeysEnum.ANNOTATED_COUNT]: (
<Td key={AppLogKeysEnum.ANNOTATED_COUNT}>{item.markCount}</Td>
),
[AppLogKeysEnum.RESPONSE_TIME]: (
<Td key={AppLogKeysEnum.RESPONSE_TIME}>
{item.averageResponseTime ? `${item.averageResponseTime.toFixed(2)}s` : '-'}
</Td>
),
[AppLogKeysEnum.ERROR_COUNT]: (
<Td key={AppLogKeysEnum.ERROR_COUNT}>{item.errorCount || '-'}</Td>
),
[AppLogKeysEnum.POINTS]: (
<Td key={AppLogKeysEnum.POINTS}>
{item.totalPoints ? `${item.totalPoints.toFixed(2)}` : '-'}
</Td>
)
});
return ( return (
<MyBox isLoading={isLoading} display={'flex'} flexDir={'column'} h={'full'} px={px}> <MyBox isLoading={isLoading} display={'flex'} flexDir={'column'} h={'full'} px={px}>
<Flex alignItems={'center'} gap={3} flexWrap={'wrap'}> <Flex alignItems={'center'} gap={3} flexWrap={'wrap'}>
@ -492,7 +517,7 @@ const LogTable = ({
_hover={{ bg: 'myWhite.600' }} _hover={{ bg: 'myWhite.600' }}
cursor={'pointer'} cursor={'pointer'}
title={t('common:core.view_chat_detail')} title={t('common:core.view_chat_detail')}
onClick={() => setDetailLogsId(item.id)} onClick={() => setDetailLogsId(item.chatId)}
> >
{logKeys {logKeys
.filter((logKey) => logKey.enable) .filter((logKey) => logKey.enable)

View File

@ -3,11 +3,13 @@ import { Box, Button, Flex } from '@chakra-ui/react';
import MyIcon from '@fastgpt/web/components/common/Icon'; import MyIcon from '@fastgpt/web/components/common/Icon';
import { useTranslation } from 'next-i18next'; import { useTranslation } from 'next-i18next';
import React from 'react'; import React from 'react';
import type { updateLogKeysBody } from '@/pages/api/core/app/logs/updateLogKeys';
import { useRequest2 } from '@fastgpt/web/hooks/useRequest'; import { useRequest2 } from '@fastgpt/web/hooks/useRequest';
import { updateLogKeys } from '@/web/core/app/api/log'; import { updateLogKeys } from '@/web/core/app/api/log';
import type { AppLogKeysType } from '@fastgpt/global/core/app/logs/type'; 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'; import type { SetState } from 'ahooks/lib/createUseStorageState';
const SyncLogKeysPopover = ({ const SyncLogKeysPopover = ({
@ -20,7 +22,7 @@ const SyncLogKeysPopover = ({
logKeys: AppLogKeysType[]; logKeys: AppLogKeysType[];
setLogKeys: (value: SetState<AppLogKeysType[]>) => void; setLogKeys: (value: SetState<AppLogKeysType[]>) => void;
teamLogKeys: AppLogKeysType[]; teamLogKeys: AppLogKeysType[];
fetchLogKeys: () => Promise<getLogKeysResponse>; fetchLogKeys: () => Promise<getLogKeysResponseType>;
appId: string; appId: string;
}) => { }) => {
const { t } = useTranslation(); const { t } = useTranslation();

View File

@ -33,8 +33,10 @@ const WorkflowEdit = () => {
useMount(() => { useMount(() => {
if (!isV2Workflow) { if (!isV2Workflow) {
openConfirm(() => { openConfirm({
initData(JSON.parse(JSON.stringify(v1Workflow2V2((appDetail.modules || []) as any)))); onConfirm: () => {
initData(JSON.parse(JSON.stringify(v1Workflow2V2((appDetail.modules || []) as any))));
}
})(); })();
} else { } else {
initData( initData(

View File

@ -194,15 +194,17 @@ const Share = ({ appId }: { appId: string; type: PublishChannelEnum }) => {
icon: 'delete', icon: 'delete',
type: 'danger', type: 'danger',
onClick: () => onClick: () =>
openConfirm(async () => { openConfirm({
setIsLoading(true); onConfirm: async () => {
try { setIsLoading(true);
await delShareChatById(item._id); try {
refetchShareChatList(); await delShareChatById(item._id);
} catch (error) { refetchShareChatList();
console.log(error); } catch (error) {
console.log(error);
}
setIsLoading(false);
} }
setIsLoading(false);
})() })()
} }
] ]

View File

@ -105,7 +105,10 @@ const AppCard = ({
variant={'whitePrimary'} variant={'whitePrimary'}
leftIcon={<MyIcon name={'core/chat/chatLight'} w={'16px'} />} leftIcon={<MyIcon name={'core/chat/chatLight'} w={'16px'} />}
onClick={() => 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')} {t('common:core.Chat')}

View File

@ -82,7 +82,7 @@ const SimpleEdit = () => {
nodes: appDetail.modules, nodes: appDetail.modules,
chatConfig: { chatConfig: {
...appDetail.chatConfig, ...appDetail.chatConfig,
fileSelectConfig: appDetail.chatConfig.fileSelectConfig || { fileSelectConfig: appDetail.chatConfig?.fileSelectConfig || {
...defaultAppSelectFileConfig, ...defaultAppSelectFileConfig,
canSelectFile: true canSelectFile: true
} }

View File

@ -36,8 +36,13 @@ const WorkflowEdit = () => {
useMount(() => { useMount(() => {
if (!isV2Workflow) { if (!isV2Workflow) {
openConfirm(() => { openConfirm({
initData(JSON.parse(JSON.stringify(v1Workflow2V2((appDetail.modules || []) as any))), true); onConfirm: () => {
initData(
JSON.parse(JSON.stringify(v1Workflow2V2((appDetail.modules || []) as any))),
true
);
}
})(); })();
} else { } else {
initData( initData(

View File

@ -64,23 +64,25 @@ const NodeCode = ({ data, selected }: NodeProps<FlowNodeItemType>) => {
]} ]}
value={codeType?.value} value={codeType?.value}
onChange={(newLang) => { onChange={(newLang) => {
openSwitchLangConfirm(() => { openSwitchLangConfirm({
onChangeNode({ onConfirm: () => {
nodeId, onChangeNode({
type: 'updateInput', nodeId,
key: NodeInputKeyEnum.codeType, type: 'updateInput',
value: { ...codeType, value: newLang } key: NodeInputKeyEnum.codeType,
}); value: { ...codeType, value: newLang }
});
onChangeNode({ onChangeNode({
nodeId, nodeId,
type: 'updateInput', type: 'updateInput',
key: item.key, key: item.key,
value: { value: {
...item, ...item,
value: SANDBOX_CODE_TEMPLATE[newLang] value: SANDBOX_CODE_TEMPLATE[newLang]
} }
}); });
}
})(); })();
}} }}
/> />

View File

@ -22,7 +22,7 @@ import {
stringConditionList stringConditionList
} from '@fastgpt/global/core/workflow/template/system/ifElse/constant'; } from '@fastgpt/global/core/workflow/template/system/ifElse/constant';
import { useContextSelector } from 'use-context-selector'; import { useContextSelector } from 'use-context-selector';
import React, { useMemo } from 'react'; import React, { useCallback, useMemo } from 'react';
import { WorkflowBufferDataContext } from '../../../context/workflowInitContext'; import { WorkflowBufferDataContext } from '../../../context/workflowInitContext';
import MySelect from '@fastgpt/web/components/common/MySelect'; import MySelect from '@fastgpt/web/components/common/MySelect';
import MyInput from '@/components/MyInput'; import MyInput from '@/components/MyInput';
@ -452,10 +452,6 @@ const ConditionValueInput = ({
return output?.valueType; return output?.valueType;
} }
}, [globalVariables, getNodeById, variable]); }, [globalVariables, getNodeById, variable]);
const { referenceList } = useReference({
nodeId,
valueType
});
const showBooleanSelect = useMemo(() => { const showBooleanSelect = useMemo(() => {
return ( return (
@ -473,6 +469,45 @@ const ConditionValueInput = ({
); );
}, [condition, valueType]); }, [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(() => { const RenderInput = useMemo(() => {
if (showBooleanSelect) { if (showBooleanSelect) {
return ( return (

View File

@ -207,11 +207,10 @@ const AppContextProvider = ({ children }: { children: ReactNode }) => {
); );
const onDelApp = useCallback( const onDelApp = useCallback(
() => () =>
openConfirmDel( openConfirmDel({
deleteApp, onConfirm: deleteApp,
undefined, customContent: t('app:confirm_del_app_tip', { name: appDetail.name })
t('app:confirm_del_app_tip', { name: appDetail.name }) })(),
)(),
[appDetail.name, deleteApp, openConfirmDel, t] [appDetail.name, deleteApp, openConfirmDel, t]
); );

View File

@ -49,7 +49,12 @@ const CollectionReader = ({
const filterResults = useMemo(() => { const filterResults = useMemo(() => {
const res = rawSearch const res = rawSearch
.filter((item) => item.collectionId === collectionId) .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) { if (quoteId) {
setQuoteIndex(res.findIndex((item) => item.id === quoteId)); setQuoteIndex(res.findIndex((item) => item.id === quoteId));
@ -65,7 +70,7 @@ const CollectionReader = ({
if (item) { if (item) {
return { return {
id: item.id, id: item.id,
index: item.chunkIndex, anchor: item.chunkIndex,
score: item.score score: item.score
}; };
} }

View File

@ -65,8 +65,10 @@ const EditableTagItem = React.memo(function EditableTagItem({
}); });
const handleConfirmDelete = useCallback(() => { const handleConfirmDelete = useCallback(() => {
openConfirm(() => { openConfirm({
onConfirmDelete(tag); onConfirm: () => {
onConfirmDelete(tag);
}
})(); })();
}, [openConfirm, onConfirmDelete, tag]); }, [openConfirm, onConfirmDelete, tag]);

View File

@ -5,13 +5,16 @@ import { ChatSourceEnum } from '@fastgpt/global/core/chat/constants';
import type { DateRangeType } from '@fastgpt/web/components/common/DateRangePicker'; import type { DateRangeType } from '@fastgpt/web/components/common/DateRangePicker';
import { useMultipleSelect } from '@fastgpt/web/components/common/MySelect/MultipleSelect'; import { useMultipleSelect } from '@fastgpt/web/components/common/MySelect/MultipleSelect';
import { addDays } from 'date-fns'; import { addDays } from 'date-fns';
import React, { useState } from 'react'; import React, { useMemo, useState } from 'react';
import { useContextSelector } from 'use-context-selector'; import { useContextSelector } from 'use-context-selector';
type Props = { type Props = {
Header: React.FC<{ children?: React.ReactNode }>; 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 LogDetails = ({ Header }: Props) => {
const appId = useContextSelector(ChatSettingContext, (v) => v.chatSettings?.appId || ''); const appId = useContextSelector(ChatSettingContext, (v) => v.chatSettings?.appId || '');
@ -25,12 +28,11 @@ const LogDetails = ({ Header }: Props) => {
setValue: setChatSources, setValue: setChatSources,
isSelectAll: isSelectAllSource, isSelectAll: isSelectAllSource,
setIsSelectAll: setIsSelectAllSource setIsSelectAll: setIsSelectAllSource
} = useMultipleSelect<ChatSourceEnum>(Object.values(ChatSourceEnum), true); } = useMultipleSelect<ChatSourceEnum>(chatSourceValues, true);
return ( return (
<Flex gap={'13px'} flexDir="column" h={['calc(100vh - 69px)', 'full']}> <Flex gap={'13px'} flexDir="column" h={['calc(100vh - 69px)', 'full']}>
<Header /> <Header />
<LogTable <LogTable
px={[2, 0]} px={[2, 0]}
showSourceSelector={false} showSourceSelector={false}

View File

@ -172,7 +172,6 @@ const AppChatWindow = ({ myApps }: Props) => {
/> />
) : ( ) : (
<ChatBox <ChatBox
showEmptyIntro
appId={appId} appId={appId}
chatId={chatId} chatId={chatId}
isReady={!loading} isReady={!loading}

View File

@ -1,60 +0,0 @@
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;

View File

@ -46,7 +46,7 @@ const UserAvatarPopover = ({
{({ onClose }) => { {({ onClose }) => {
const onLogout = useCallback(() => { const onLogout = useCallback(() => {
onClose(); onClose();
openConfirm(handleLogout)(); openConfirm({ onConfirm: handleLogout })();
}, [onClose]); }, [onClose]);
return ( return (

View File

@ -96,7 +96,7 @@ const List = () => {
borderColor: 'primary.600' borderColor: 'primary.600'
}, },
onDrop: (dragId: string, targetId: string) => { 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, type: 'grayBg' as MenuItemType,
label: t('app:copy_one_app'), label: t('app:copy_one_app'),
onClick: () => onClick: () =>
openConfirmCopy(() => onclickCopy({ appId: app._id }))() openConfirmCopy({
onConfirm: () => onclickCopy({ appId: app._id })
})()
} }
] ]
} }
@ -411,13 +413,14 @@ const List = () => {
icon: 'delete', icon: 'delete',
label: t('common:Delete'), label: t('common:Delete'),
onClick: () => onClick: () =>
openConfirmDel( openConfirmDel({
() => onclickDelApp(app._id), onConfirm: () => onclickDelApp(app._id),
undefined, inputConfirmText: app.name,
app.type === AppTypeEnum.folder customContent:
? t('app:confirm_delete_folder_tip') app.type === AppTypeEnum.folder
: t('app:confirm_del_app_tip', { name: app.name }) ? 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 router = useRouter();
const parentId = router.query.parentId; const parentId = router.query.parentId;
const createAppType = const createAppType =
createAppTypeMap[appType as CreateAppType]?.type || appType !== 'all' && appType in createAppTypeMap
(router.pathname.includes('/agent') ? AppTypeEnum.workflow : AppTypeEnum.workflowTool); ? createAppTypeMap[appType as keyof typeof createAppTypeMap].type
: router.pathname.includes('/agent')
? AppTypeEnum.workflow
: AppTypeEnum.workflowTool;
const isToolType = ToolTypeList.includes(createAppType); const isToolType = ToolTypeList.includes(createAppType);
return ( return (
@ -571,8 +577,11 @@ const ListCreateButton = ({ appType }: { appType: AppTypeEnum | 'all' }) => {
const router = useRouter(); const router = useRouter();
const parentId = router.query.parentId; const parentId = router.query.parentId;
const createAppType = const createAppType =
createAppTypeMap[appType as CreateAppType]?.type || appType !== 'all' && appType in createAppTypeMap
(router.pathname.includes('/agent') ? AppTypeEnum.workflow : AppTypeEnum.workflowTool); ? createAppTypeMap[appType as keyof typeof createAppTypeMap].type
: router.pathname.includes('/agent')
? AppTypeEnum.workflow
: AppTypeEnum.workflowTool;
return ( return (
<MyBox <MyBox

View File

@ -1,5 +1,12 @@
import { useConfirm } from '@fastgpt/web/hooks/useConfirm'; 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 { useTranslation } from 'next-i18next';
import { createContext, useContextSelector } from 'use-context-selector'; import { createContext, useContextSelector } from 'use-context-selector';
import { DatasetTypeEnum } from '@fastgpt/global/core/dataset/constants'; import { DatasetTypeEnum } from '@fastgpt/global/core/dataset/constants';
@ -95,7 +102,7 @@ const CollectionPageContextProvider = ({ children }: { children: ReactNode }) =>
refreshDeps: [parentId, searchText, filterTags] refreshDeps: [parentId, searchText, filterTags]
}); });
const syncDataset = async () => { const syncDataset = useCallback(async () => {
if (datasetDetail.type === DatasetTypeEnum.websiteDataset) { if (datasetDetail.type === DatasetTypeEnum.websiteDataset) {
await checkTeamWebSyncLimit(); await checkTeamWebSyncLimit();
} }
@ -110,7 +117,7 @@ const CollectionPageContextProvider = ({ children }: { children: ReactNode }) =>
status: 'success', status: 'success',
title: t('dataset:collection.sync.submit') title: t('dataset:collection.sync.submit')
}); });
}; }, [datasetDetail.type, datasetId, getData, loadDatasetDetail, pageNum, t, toast]);
// dataset sync confirm // dataset sync confirm
const { openConfirm: openDatasetSyncConfirm, ConfirmModal: ConfirmDatasetSyncModal } = useConfirm( const { openConfirm: openDatasetSyncConfirm, ConfirmModal: ConfirmDatasetSyncModal } = useConfirm(
@ -141,22 +148,38 @@ const CollectionPageContextProvider = ({ children }: { children: ReactNode }) =>
} }
); );
const contextValue: CollectionPageContextType = { const contextValue: CollectionPageContextType = useMemo(
openDatasetSyncConfirm: openDatasetSyncConfirm(syncDataset), () => ({
onOpenWebsiteModal, openDatasetSyncConfirm: openDatasetSyncConfirm({ onConfirm: syncDataset }),
onOpenWebsiteModal,
searchText, searchText,
setSearchText, setSearchText,
filterTags, filterTags,
setFilterTags, setFilterTags,
collections, collections,
Pagination, Pagination,
total, total,
getData, getData,
isGetting, isGetting,
pageNum, pageNum,
pageSize pageSize
}; }),
[
Pagination,
collections,
filterTags,
getData,
isGetting,
onOpenWebsiteModal,
openDatasetSyncConfirm,
pageNum,
pageSize,
searchText,
syncDataset,
total
]
);
return ( return (
<CollectionPageContext.Provider value={contextValue}> <CollectionPageContext.Provider value={contextValue}>

View File

@ -372,8 +372,10 @@ const CollectionCard = () => {
</Flex> </Flex>
), ),
onClick: () => onClick: () =>
openSyncConfirm(() => { openSyncConfirm({
onclickStartSync(collection._id); onConfirm: () => {
onclickStartSync(collection._id);
}
})() })()
} }
] ]
@ -423,13 +425,15 @@ const CollectionCard = () => {
), ),
type: 'danger', type: 'danger',
onClick: () => onClick: () =>
openDeleteConfirm( openDeleteConfirm({
() => onDelCollection([collection._id]), onConfirm: () => onDelCollection([collection._id]),
undefined, customContent:
collection.type === DatasetCollectionTypeEnum.folder collection.type === DatasetCollectionTypeEnum.folder
? t('common:dataset.collections.Confirm to delete the folder') ? t(
: t('common:dataset.Confirm to delete the file') 'common:dataset.collections.Confirm to delete the folder'
)() )
: t('common:dataset.Confirm to delete the file')
})()
} }
] ]
} }
@ -451,16 +455,15 @@ const CollectionCard = () => {
<Button <Button
variant={'whiteBase'} variant={'whiteBase'}
onClick={() => onClick={() =>
openDeleteConfirm( openDeleteConfirm({
() => onConfirm: () =>
onDelCollection(selectedItems.map((e) => e._id)).then(() => onDelCollection(selectedItems.map((e) => e._id)).then(() =>
setSelectedItems([]) setSelectedItems([])
), ),
undefined, customContent: t('dataset:confirm_delete_collection', {
t('dataset:confirm_delete_collection', {
num: selectedItems.length num: selectedItems.length
}) })
)() })()
} }
> >
{t('dataset:batch_delete')} {t('dataset:batch_delete')}

View File

@ -195,9 +195,11 @@ const Info = ({ datasetId }: { datasetId: string }) => {
onChange={(e) => { onChange={(e) => {
const vectorModel = embeddingModelList.find((item) => item.model === e); const vectorModel = embeddingModelList.find((item) => item.model === e);
if (!vectorModel) return; if (!vectorModel) return;
return onOpenConfirmRebuild(async () => { return onOpenConfirmRebuild({
await onRebuilding(vectorModel); onConfirm: async () => {
setValue('vectorModel', vectorModel); await onRebuilding(vectorModel);
setValue('vectorModel', vectorModel);
}
})(); })();
}} }}
/> />
@ -264,16 +266,15 @@ const Info = ({ datasetId }: { datasetId: string }) => {
const autoSync = e.target.checked; const autoSync = e.target.checked;
const text = autoSync ? t('dataset:open_auto_sync') : t('dataset:close_auto_sync'); const text = autoSync ? t('dataset:open_auto_sync') : t('dataset:close_auto_sync');
onOpenConfirmSyncSchedule( onOpenConfirmSyncSchedule({
async () => { onConfirm: async () => {
return updateDataset({ return updateDataset({
id: datasetId, id: datasetId,
autoSync autoSync
}); });
}, },
undefined, customContent: text
text })();
)();
}} }}
/> />
</Flex> </Flex>

View File

@ -8,8 +8,7 @@ import { useRouter } from 'next/router';
import PermissionIconText from '@/components/support/permission/IconText'; import PermissionIconText from '@/components/support/permission/IconText';
import Avatar from '@fastgpt/web/components/common/Avatar'; import Avatar from '@fastgpt/web/components/common/Avatar';
import { useConfirm } from '@fastgpt/web/hooks/useConfirm'; import { useConfirm } from '@fastgpt/web/hooks/useConfirm';
import { useRequest, useRequest2 } from '@fastgpt/web/hooks/useRequest'; import { useRequest2 } from '@fastgpt/web/hooks/useRequest';
import { type DatasetItemType } from '@fastgpt/global/core/dataset/type';
import { useSystemStore } from '@/web/common/system/useSystemStore'; import { useSystemStore } from '@/web/common/system/useSystemStore';
import { checkTeamExportDatasetLimit } from '@/web/support/user/team/api'; import { checkTeamExportDatasetLimit } from '@/web/support/user/team/api';
import { downloadFetch } from '@/web/common/system/utils'; import { downloadFetch } from '@/web/common/system/utils';
@ -72,12 +71,13 @@ function List() {
borderColor: 'primary.600' borderColor: 'primary.600'
}, },
onDrop: (dragId: string, targetId: string) => { onDrop: (dragId: string, targetId: string) => {
openMoveConfirm(() => openMoveConfirm({
updateDataset({ onConfirm: () =>
id: dragId, updateDataset({
parentId: targetId id: dragId,
}) parentId: targetId
)(); })
})();
} }
}); });
@ -86,22 +86,27 @@ function List() {
[editPerDatasetId, myDatasets] [editPerDatasetId, myDatasets]
); );
const { mutate: exportDataset } = useRequest({ const { runAsync: exportDataset } = useRequest2(
mutationFn: async (dataset: DatasetItemType) => { async ({ _id, name }: { _id: string; name: string }) => {
setLoading(true); await checkTeamExportDatasetLimit(_id);
await checkTeamExportDatasetLimit(dataset._id);
await downloadFetch({ await downloadFetch({
url: `/api/core/dataset/exportAll?datasetId=${dataset._id}`, url: `/api/core/dataset/exportAll?datasetId=${_id}`,
filename: `${dataset.name}.csv` filename: `${name}.csv`
}); });
}, },
onSettled() { {
setLoading(false); manual: true,
}, onBefore: () => {
successToast: t('common:core.dataset.Start export'), setLoading(true);
errorToast: t('common:dataset.Export Dataset Limit Error') },
}); onFinally() {
setLoading(false);
},
successToast: t('common:core.dataset.Start export'),
errorToast: t('common:dataset.Export Dataset Limit Error')
}
);
const DeleteTipsMap = useRef({ const DeleteTipsMap = useRef({
[DatasetTypeEnum.folder]: t('common:dataset.deleteFolderTips'), [DatasetTypeEnum.folder]: t('common:dataset.deleteFolderTips'),
@ -126,18 +131,6 @@ function List() {
type: 'delete' type: 'delete'
}); });
const onClickDeleteDataset = (id: string) => {
openConfirm(
() =>
onDelDataset(id).then(() => {
refetchPaths();
loadMyDatasets();
}),
undefined,
DeleteTipsMap.current[DatasetTypeEnum.dataset]
)();
};
return ( return (
<> <>
{formatDatasets.length > 0 && ( {formatDatasets.length > 0 && (
@ -375,7 +368,17 @@ function List() {
icon: 'delete', icon: 'delete',
label: t('common:Delete'), label: t('common:Delete'),
type: 'danger' as 'danger', 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
})()
} }
] ]
} }

View File

@ -7,9 +7,19 @@ import { webPushTrack } from '@/web/common/middle/tracks/utils';
import { useTranslation } from 'next-i18next'; import { useTranslation } from 'next-i18next';
import { errorLogger } from '@/web/common/utils/errorLogger'; import { errorLogger } from '@/web/common/utils/errorLogger';
import { useMount } from 'ahooks'; 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() { function Error() {
const { t } = useTranslation(); const { t, i18n } = useTranslation();
const lang = i18n.language;
const router = useRouter(); const router = useRouter();
const { toast } = useToast(); const { toast } = useToast();
const { lastRoute, llmModelList, embeddingModelList } = useSystemStore(); const { lastRoute, llmModelList, embeddingModelList } = useSystemStore();
@ -52,7 +62,7 @@ function Error() {
}, 2000); }, 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) { export async function getServerSideProps(context: any) {

View File

@ -139,7 +139,9 @@ const CustomDomain = () => {
<Button <Button
variant="whiteDanger" variant="whiteDanger"
onClick={() => { onClick={() => {
return openConfirm(() => onDelete(customDomain.domain))(); return openConfirm({
onConfirm: () => onDelete(customDomain.domain)
})();
}} }}
> >
{t('common:Delete')} {t('common:Delete')}

View File

@ -15,7 +15,7 @@ import { useForm } from 'react-hook-form';
import { type UserUpdateParams } from '@/types/user'; import { type UserUpdateParams } from '@/types/user';
import { useToast } from '@fastgpt/web/hooks/useToast'; import { useToast } from '@fastgpt/web/hooks/useToast';
import { useUserStore } from '@/web/support/user/useUserStore'; 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 dynamic from 'next/dynamic';
import { useSystemStore } from '@/web/common/system/useSystemStore'; import { useSystemStore } from '@/web/common/system/useSystemStore';
import { useTranslation } from 'next-i18next'; import { useTranslation } from 'next-i18next';

View File

@ -0,0 +1,182 @@
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;
});

View File

@ -21,6 +21,7 @@ import {
truncateFilename truncateFilename
} from '@fastgpt/service/common/s3/utils'; } from '@fastgpt/service/common/s3/utils';
import { connectionMongo, Types } from '@fastgpt/service/common/mongo'; import { connectionMongo, Types } from '@fastgpt/service/common/mongo';
import { migrateFeedbackFlags } from './initFeedbackFlags';
// 将 GridFS 的流转换为 Buffer // 将 GridFS 的流转换为 Buffer
async function gridFSStreamToBuffer( 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}] Converted fileId: ${converted}`);
addLog.info(`[Migration ${batchId}] =======================================`); addLog.info(`[Migration ${batchId}] =======================================`);
// 重新统计每一个 chat 的反馈情况
await migrateFeedbackFlags();
return { return {
batchId, batchId,
migrationVersion, migrationVersion,

View File

@ -1,465 +0,0 @@
import { NextAPI } from '@/service/middleware/entry';
import { addLog } from '@fastgpt/service/common/system/log';
import { authCert } from '@fastgpt/service/support/permission/auth/common';
import { type NextApiRequest, type NextApiResponse } from 'next';
import { getS3DatasetSource } from '@fastgpt/service/common/s3/sources/dataset';
import {
getDownloadStream,
getGFSCollection
} from '@fastgpt/service/common/file/gridfs/controller';
import pLimit from 'p-limit';
import { MongoDatasetMigrationLog } from '@fastgpt/service/core/dataset/migration/schema';
import { randomUUID } from 'crypto';
import {
uploadImage2S3Bucket,
removeS3TTL,
truncateFilename
} from '@fastgpt/service/common/s3/utils';
import { connectionMongo, Types } from '@fastgpt/service/common/mongo';
// 将 GridFS 的流转换为 Buffer
async function gridFSStreamToBuffer(
stream: Awaited<ReturnType<typeof getDownloadStream>>
): Promise<Buffer> {
const chunks: Buffer[] = [];
stream.on('data', (chunk) => chunks.push(chunk));
await new Promise((resolve, reject) => {
stream.on('end', resolve);
stream.on('error', reject);
});
return Buffer.concat(chunks);
}
// 获取 dataset_image 的 GridFS bucket
function getDatasetImageGridBucket() {
return new connectionMongo.mongo.GridFSBucket(connectionMongo.connection.db!, {
bucketName: 'dataset_image'
});
}
// 恢复单个 collection 文件
async function recoverCollectionFile({
batchId,
migrationLog
}: {
batchId: string;
migrationLog: any;
}) {
const { sourceStorage, targetStorage, resourceId } = migrationLog;
try {
addLog.info(
`[Recovery ${batchId}] Recovering collection file. ResourceId: ${resourceId}, GridFS FileId: ${sourceStorage.fileId}, S3 Key: ${targetStorage.key}`
);
// 阶段 1: 从 GridFS 下载
const downloadStartTime = Date.now();
let buffer: Buffer;
try {
const stream = await getDownloadStream({
bucketName: 'dataset',
fileId: sourceStorage.fileId
});
buffer = await gridFSStreamToBuffer(stream);
addLog.info(
`[Recovery ${batchId}] Downloaded from GridFS. Size: ${buffer.length} bytes, Duration: ${Date.now() - downloadStartTime}ms`
);
} catch (error) {
addLog.error(
`[Recovery ${batchId}] Failed to download from GridFS for resource ${resourceId}:`,
error
);
throw error;
}
// 阶段 2: 上传到 S3使用原来的 key
const uploadStartTime = Date.now();
try {
const s3Key = targetStorage.key;
const filename = migrationLog.metadata?.fileName || 'file';
// 直接使用 bucket.putObject 上传到指定的 key
const s3Source = getS3DatasetSource();
await s3Source.client.putObject(s3Source.bucketName, s3Key, buffer, buffer.length, {
'content-type': 'application/octet-stream',
'upload-time': new Date().toISOString(),
'origin-filename': encodeURIComponent(filename)
});
addLog.info(
`[Recovery ${batchId}] Uploaded to S3. Key: ${s3Key}, Duration: ${Date.now() - uploadStartTime}ms`
);
} catch (error) {
addLog.error(
`[Recovery ${batchId}] Failed to upload to S3 for resource ${resourceId}:`,
error
);
throw error;
}
// 阶段 3: 立即移除 TTL
try {
await removeS3TTL({ key: [targetStorage.key], bucketName: 'private' });
addLog.info(`[Recovery ${batchId}] Removed TTL for ${targetStorage.key}`);
} catch (error) {
addLog.warn(`[Recovery ${batchId}] Failed to remove TTL (non-critical): ${error}`);
}
return { success: true, resourceId };
} catch (error) {
addLog.error(`[Recovery ${batchId}] Failed to recover resource ${resourceId}: ${error}`);
return { success: false, resourceId, error };
}
}
// 恢复单个 image 文件
async function recoverImageFile({ batchId, migrationLog }: { batchId: string; migrationLog: any }) {
const { sourceStorage, targetStorage, resourceId } = migrationLog;
try {
addLog.info(
`[Recovery ${batchId}] Recovering image file. ResourceId: ${resourceId}, GridFS ImageId: ${sourceStorage.fileId}, S3 Key: ${targetStorage.key}`
);
// 阶段 1: 从 GridFS 下载
const downloadStartTime = Date.now();
let buffer: Buffer;
try {
const bucket = getDatasetImageGridBucket();
const stream = bucket.openDownloadStream(new Types.ObjectId(sourceStorage.fileId));
buffer = await gridFSStreamToBuffer(stream);
addLog.info(
`[Recovery ${batchId}] Downloaded image from GridFS. Size: ${buffer.length} bytes, Duration: ${Date.now() - downloadStartTime}ms`
);
} catch (error) {
addLog.error(
`[Recovery ${batchId}] Failed to download image from GridFS for resource ${resourceId}:`,
error
);
throw error;
}
// 阶段 2: 上传到 S3使用原来的 key
const uploadStartTime = Date.now();
try {
const s3Key = targetStorage.key;
const filename = migrationLog.metadata?.fileName || 'image.png';
const truncatedFilename = truncateFilename(filename);
// 使用 uploadImage2S3Bucket 上传图片
// 注意uploadImage2S3Bucket 返回的是完整的 URL 或 key
await uploadImage2S3Bucket('private', {
base64Img: buffer.toString('base64'),
uploadKey: s3Key,
mimetype: 'image/png', // 默认值,实际会从文件判断
filename: truncatedFilename,
expiredTime: undefined // 不设置过期时间
});
addLog.info(
`[Recovery ${batchId}] Uploaded image to S3. Key: ${s3Key}, Duration: ${Date.now() - uploadStartTime}ms`
);
} catch (error) {
addLog.error(
`[Recovery ${batchId}] Failed to upload image to S3 for resource ${resourceId}:`,
error
);
throw error;
}
// 阶段 3: 立即移除 TTL
try {
await removeS3TTL({ key: [targetStorage.key], bucketName: 'private' });
addLog.info(`[Recovery ${batchId}] Removed TTL for ${targetStorage.key}`);
} catch (error) {
addLog.warn(`[Recovery ${batchId}] Failed to remove TTL (non-critical): ${error}`);
}
return { success: true, resourceId };
} catch (error) {
addLog.error(`[Recovery ${batchId}] Failed to recover image ${resourceId}: ${error}`);
return { success: false, resourceId, error };
}
}
// 批量恢复 collections
async function recoverCollectionBatch({
batchId,
offset,
limit,
concurrency,
migrationBatchId
}: {
batchId: string;
offset: number;
limit: number;
concurrency: number;
migrationBatchId?: string;
}) {
// 查找已完成的 collection 迁移记录
const query: any = {
resourceType: 'collection',
status: 'completed',
'sourceStorage.fileId': { $exists: true, $ne: null },
'targetStorage.key': { $exists: true, $ne: null }
};
if (migrationBatchId) {
query.batchId = migrationBatchId;
}
const migrationLogs = await MongoDatasetMigrationLog.find(query).skip(offset).limit(limit).lean();
if (migrationLogs.length === 0) {
return { processed: 0, succeeded: 0, failed: 0 };
}
addLog.info(`[Recovery ${batchId}] Found ${migrationLogs.length} collections to recover`);
const limitFn = pLimit(concurrency);
let succeeded = 0;
let failed = 0;
const tasks = migrationLogs.map((log) =>
limitFn(async () => {
const result = await recoverCollectionFile({ batchId, migrationLog: log });
if (result.success) {
succeeded++;
} else {
failed++;
}
})
);
await Promise.allSettled(tasks);
return {
processed: migrationLogs.length,
succeeded,
failed
};
}
// 批量恢复 images
async function recoverImageBatch({
batchId,
offset,
limit,
concurrency,
migrationBatchId
}: {
batchId: string;
offset: number;
limit: number;
concurrency: number;
migrationBatchId?: string;
}) {
// 查找已完成的 image 迁移记录
const query: any = {
resourceType: 'data_image',
status: 'completed',
'sourceStorage.fileId': { $exists: true, $ne: null },
'targetStorage.key': { $exists: true, $ne: null }
};
if (migrationBatchId) {
query.batchId = migrationBatchId;
}
const migrationLogs = await MongoDatasetMigrationLog.find(query).skip(offset).limit(limit).lean();
if (migrationLogs.length === 0) {
return { processed: 0, succeeded: 0, failed: 0 };
}
addLog.info(`[Recovery ${batchId}] Found ${migrationLogs.length} images to recover`);
const limitFn = pLimit(concurrency);
let succeeded = 0;
let failed = 0;
const tasks = migrationLogs.map((log) =>
limitFn(async () => {
const result = await recoverImageFile({ batchId, migrationLog: log });
if (result.success) {
succeeded++;
} else {
failed++;
}
})
);
await Promise.allSettled(tasks);
return {
processed: migrationLogs.length,
succeeded,
failed
};
}
async function handler(req: NextApiRequest, _res: NextApiResponse) {
await authCert({ req, authRoot: true });
// 恢复配置
const config = {
collectionBatchSize: 500,
collectionConcurrency: 10,
imageBatchSize: 500,
imageConcurrency: 5,
pauseBetweenBatches: 1000, // ms
// 可选指定要恢复的迁移批次ID如果不指定则恢复所有
migrationBatchId: req.body?.migrationBatchId as string | undefined
};
// 生成唯一的恢复批次 ID
const recoveryBatchId = `recovery_${Date.now()}_${randomUUID()}`;
addLog.info(`[Recovery ${recoveryBatchId}] Starting recovery process`);
if (config.migrationBatchId) {
addLog.info(`[Recovery ${recoveryBatchId}] Target migration batch: ${config.migrationBatchId}`);
}
// ========== 恢复 Collections ==========
addLog.info(`[Recovery ${recoveryBatchId}] Starting collection recovery...`);
// 获取总数
const collectionQuery: any = {
resourceType: 'collection',
status: 'completed',
'sourceStorage.fileId': { $exists: true, $ne: null },
'targetStorage.key': { $exists: true, $ne: null }
};
if (config.migrationBatchId) {
collectionQuery.batchId = config.migrationBatchId;
}
const totalCollections = await MongoDatasetMigrationLog.countDocuments(collectionQuery);
addLog.info(`[Recovery ${recoveryBatchId}] Total collections to recover: ${totalCollections}`);
let collectionStats = {
processed: 0,
succeeded: 0,
failed: 0
};
// 分批恢复 collections
for (let offset = 0; offset < totalCollections; offset += config.collectionBatchSize) {
const currentBatch = Math.floor(offset / config.collectionBatchSize) + 1;
const totalBatches = Math.ceil(totalCollections / config.collectionBatchSize);
addLog.info(
`[Recovery ${recoveryBatchId}] Processing collections batch ${currentBatch}/${totalBatches} (${offset}-${offset + config.collectionBatchSize})`
);
const batchStats = await recoverCollectionBatch({
batchId: recoveryBatchId,
offset,
limit: config.collectionBatchSize,
concurrency: config.collectionConcurrency,
migrationBatchId: config.migrationBatchId
});
collectionStats.processed += batchStats.processed;
collectionStats.succeeded += batchStats.succeeded;
collectionStats.failed += batchStats.failed;
addLog.info(
`[Recovery ${recoveryBatchId}] Batch ${currentBatch}/${totalBatches} completed. Batch: +${batchStats.succeeded} succeeded, +${batchStats.failed} failed. Total progress: ${collectionStats.succeeded}/${totalCollections}`
);
// 暂停一下
if (offset + config.collectionBatchSize < totalCollections) {
await new Promise((resolve) => setTimeout(resolve, config.pauseBetweenBatches));
}
}
// ========== 恢复 Images ==========
addLog.info(`[Recovery ${recoveryBatchId}] Starting image recovery...`);
const imageQuery: any = {
resourceType: 'data_image',
status: 'completed',
'sourceStorage.fileId': { $exists: true, $ne: null },
'targetStorage.key': { $exists: true, $ne: null }
};
if (config.migrationBatchId) {
imageQuery.batchId = config.migrationBatchId;
}
const totalImages = await MongoDatasetMigrationLog.countDocuments(imageQuery);
addLog.info(`[Recovery ${recoveryBatchId}] Total images to recover: ${totalImages}`);
let imageStats = {
processed: 0,
succeeded: 0,
failed: 0
};
// 分批恢复 images
for (let offset = 0; offset < totalImages; offset += config.imageBatchSize) {
const currentBatch = Math.floor(offset / config.imageBatchSize) + 1;
const totalBatches = Math.ceil(totalImages / config.imageBatchSize);
addLog.info(
`[Recovery ${recoveryBatchId}] Processing images batch ${currentBatch}/${totalBatches} (${offset}-${offset + config.imageBatchSize})`
);
const batchStats = await recoverImageBatch({
batchId: recoveryBatchId,
offset,
limit: config.imageBatchSize,
concurrency: config.imageConcurrency,
migrationBatchId: config.migrationBatchId
});
imageStats.processed += batchStats.processed;
imageStats.succeeded += batchStats.succeeded;
imageStats.failed += batchStats.failed;
addLog.info(
`[Recovery ${recoveryBatchId}] Batch ${currentBatch}/${totalBatches} completed. Batch: +${batchStats.succeeded} succeeded, +${batchStats.failed} failed. Total progress: ${imageStats.succeeded}/${totalImages}`
);
// 暂停一下
if (offset + config.imageBatchSize < totalImages) {
await new Promise((resolve) => setTimeout(resolve, config.pauseBetweenBatches));
}
}
// ========== 汇总统计 ==========
addLog.info(`[Recovery ${recoveryBatchId}] ========== Recovery Summary ==========`);
addLog.info(
`[Recovery ${recoveryBatchId}] Collections - Total: ${totalCollections}, Succeeded: ${collectionStats.succeeded}, Failed: ${collectionStats.failed}`
);
addLog.info(
`[Recovery ${recoveryBatchId}] Images - Total: ${totalImages}, Succeeded: ${imageStats.succeeded}, Failed: ${imageStats.failed}`
);
addLog.info(`[Recovery ${recoveryBatchId}] =======================================`);
return {
recoveryBatchId,
migrationBatchId: config.migrationBatchId,
summary: {
collections: {
total: totalCollections,
processed: collectionStats.processed,
succeeded: collectionStats.succeeded,
failed: collectionStats.failed
},
images: {
total: totalImages,
processed: imageStats.processed,
succeeded: imageStats.succeeded,
failed: imageStats.failed
}
}
};
}
export default NextAPI(handler);

View File

@ -71,7 +71,7 @@ export default NextAPI(handler);
const testLLMModel = async (model: LLMModelItemType, headers: Record<string, string>) => { const testLLMModel = async (model: LLMModelItemType, headers: Record<string, string>) => {
const { answerText } = await createLLMResponse({ const { answerText } = await createLLMResponse({
body: { body: {
model, model, // 传递实体 model 进去,保障底层不会去拿内存里的实体。
messages: [{ role: 'user', content: 'hi' }], messages: [{ role: 'user', content: 'hi' }],
stream: true stream: true
}, },

View File

@ -6,7 +6,6 @@ import dayjs from 'dayjs';
import { type ApiRequestProps } from '@fastgpt/service/type/next'; import { type ApiRequestProps } from '@fastgpt/service/type/next';
import { replaceRegChars } from '@fastgpt/global/common/string/tools'; import { replaceRegChars } from '@fastgpt/global/common/string/tools';
import { NextAPI } from '@/service/middleware/entry'; import { NextAPI } from '@/service/middleware/entry';
import type { GetAppChatLogsProps } from '@/global/core/api/appReq';
import { authApp } from '@fastgpt/service/support/permission/app/auth'; import { authApp } from '@fastgpt/service/support/permission/app/auth';
import { Types } from '@fastgpt/service/common/mongo'; import { Types } from '@fastgpt/service/common/mongo';
import { MongoChat } from '@fastgpt/service/core/chat/chatSchema'; 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 { getTimezoneCodeFromStr } from '@fastgpt/global/common/time/timezone';
import { getLocationFromIp } from '@fastgpt/service/common/geo'; import { getLocationFromIp } from '@fastgpt/service/common/geo';
import { getLocale } from '@fastgpt/service/common/middle/i18n'; 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) => { const formatJsonString = (data: any) => {
if (data == null) return ''; if (data == null) return '';
@ -36,13 +37,7 @@ const formatJsonString = (data: any) => {
return data; return data;
}; };
export type ExportChatLogsBody = GetAppChatLogsProps & { async function handler(req: ApiRequestProps, res: NextApiResponse) {
title: string;
sourcesMap: Record<string, { label: string }>;
logKeys: AppLogKeysEnum[];
};
async function handler(req: ApiRequestProps<ExportChatLogsBody, {}>, res: NextApiResponse) {
let { let {
appId, appId,
dateStart, dateStart,
@ -52,8 +47,10 @@ async function handler(req: ApiRequestProps<ExportChatLogsBody, {}>, res: NextAp
chatSearch, chatSearch,
title, title,
sourcesMap, sourcesMap,
logKeys = [] logKeys = [],
} = req.body; feedbackType,
unreadOnly
} = ExportChatLogsBodySchema.parse(req.body);
if (!appId) { if (!appId) {
throw new Error('缺少参数'); throw new Error('缺少参数');
@ -100,12 +97,37 @@ async function handler(req: ApiRequestProps<ExportChatLogsBody, {}>, res: NextAp
const where = { const where = {
teamId: new Types.ObjectId(teamId), teamId: new Types.ObjectId(teamId),
appId: new Types.ObjectId(appId), 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: { updateTime: {
$gte: new Date(dateStart), $gte: new Date(dateStart),
$lte: new Date(dateEnd) $lte: new Date(dateEnd)
}, },
...(sources && { source: { $in: sources } }),
...(tmbIds && { tmbId: { $in: tmbIds } }),
...(chatSearch ...(chatSearch
? { ? {
$or: [ $or: [
@ -254,6 +276,22 @@ async function handler(req: ApiRequestProps<ExportChatLogsBody, {}>, res: NextAp
as: 'chatItemResponsesData' as: 'chatItemResponsesData'
} }
}, },
{
$lookup: {
from: AppVersionCollectionName,
localField: 'appVersionId',
foreignField: '_id',
pipeline: [
{
$project: {
versionName: 1,
_id: 0
}
}
],
as: 'versionData'
}
},
{ {
$addFields: { $addFields: {
messageCount: { $ifNull: [{ $arrayElemAt: ['$chatData.messageCount', 0] }, 0] }, messageCount: { $ifNull: [{ $arrayElemAt: ['$chatData.messageCount', 0] }, 0] },
@ -341,7 +379,8 @@ async function handler(req: ApiRequestProps<ExportChatLogsBody, {}>, res: NextAp
value: '$$item.value' value: '$$item.value'
} }
} }
} },
versionName: { $ifNull: [{ $arrayElemAt: ['$versionData.versionName', 0] }, null] }
} }
}, },
{ {
@ -363,6 +402,7 @@ async function handler(req: ApiRequestProps<ExportChatLogsBody, {}>, res: NextAp
totalPoints: 1, totalPoints: 1,
outLinkUid: 1, outLinkUid: 1,
tmbId: 1, tmbId: 1,
versionName: 1,
userGoodFeedbackItems: 1, userGoodFeedbackItems: 1,
userBadFeedbackItems: 1, userBadFeedbackItems: 1,
customFeedbackItems: 1, customFeedbackItems: 1,
@ -439,6 +479,7 @@ async function handler(req: ApiRequestProps<ExportChatLogsBody, {}>, res: NextAp
[AppLogKeysEnum.ERROR_COUNT]: () => doc.errorCount || 0, [AppLogKeysEnum.ERROR_COUNT]: () => doc.errorCount || 0,
[AppLogKeysEnum.POINTS]: () => (doc.totalPoints ? Number(doc.totalPoints).toFixed(2) : 0), [AppLogKeysEnum.POINTS]: () => (doc.totalPoints ? Number(doc.totalPoints).toFixed(2) : 0),
[AppLogKeysEnum.REGION]: () => region, [AppLogKeysEnum.REGION]: () => region,
versionName: () => doc.versionName || '',
chatDetails: () => formatJsonString(doc.chatDetails || []) chatDetails: () => formatJsonString(doc.chatDetails || [])
}; };

View File

@ -1,25 +1,23 @@
import type { ApiRequestProps, ApiResponseType } from '@fastgpt/service/type/next'; import type { ApiRequestProps, ApiResponseType } from '@fastgpt/service/type/next';
import { NextAPI } from '@/service/middleware/entry'; import { NextAPI } from '@/service/middleware/entry';
import { MongoAppLogKeys } from '@fastgpt/service/core/app/logs/logkeysSchema'; 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 { authApp } from '@fastgpt/service/support/permission/app/auth';
import { AppReadChatLogPerVal } from '@fastgpt/global/support/permission/app/constant'; 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 = { export type getLogKeysQuery = {};
appId: string;
};
export type getLogKeysBody = {}; export type getLogKeysBody = {};
export type getLogKeysResponse = {
logKeys: AppLogKeysSchemaType['logKeys'];
};
async function handler( async function handler(
req: ApiRequestProps<getLogKeysBody, getLogKeysQuery>, req: ApiRequestProps,
res: ApiResponseType<any> res: ApiResponseType<any>
): Promise<getLogKeysResponse> { ): Promise<getLogKeysResponseType> {
const { appId } = req.query; const { appId } = GetLogKeysQuerySchema.parse(req.query);
const { teamId } = await authApp({ const { teamId } = await authApp({
req, req,
@ -30,7 +28,7 @@ async function handler(
const result = await MongoAppLogKeys.findOne({ teamId, appId }); const result = await MongoAppLogKeys.findOne({ teamId, appId });
return { logKeys: result?.logKeys || [] }; return GetLogKeysResponseSchema.parse({ logKeys: result?.logKeys || [] });
} }
export default NextAPI(handler); export default NextAPI(handler);

View File

@ -1,8 +1,7 @@
import type { NextApiResponse } from 'next'; import type { NextApiResponse } from 'next';
import { MongoChat } from '@fastgpt/service/core/chat/chatSchema'; import { MongoChat } from '@fastgpt/service/core/chat/chatSchema';
import { type AppLogsListItemType } from '@/types/app'; import type { PipelineStage } from '@fastgpt/service/common/mongo';
import { Types } from '@fastgpt/service/common/mongo'; import { Types } from '@fastgpt/service/common/mongo';
import type { GetAppChatLogsParams } from '@/global/core/api/appReq.d';
import { authApp } from '@fastgpt/service/support/permission/app/auth'; import { authApp } from '@fastgpt/service/support/permission/app/auth';
import { import {
ChatItemCollectionName, ChatItemCollectionName,
@ -11,7 +10,6 @@ import {
import { NextAPI } from '@/service/middleware/entry'; import { NextAPI } from '@/service/middleware/entry';
import { readFromSecondary } from '@fastgpt/service/common/mongo/utils'; import { readFromSecondary } from '@fastgpt/service/common/mongo/utils';
import { parsePaginationRequest } from '@fastgpt/service/common/api/pagination'; import { parsePaginationRequest } from '@fastgpt/service/common/api/pagination';
import { type PaginationResponse } from '@fastgpt/web/common/fetch/type';
import { addSourceMember } from '@fastgpt/service/support/user/utils'; import { addSourceMember } from '@fastgpt/service/support/user/utils';
import { replaceRegChars } from '@fastgpt/global/common/string/tools'; import { replaceRegChars } from '@fastgpt/global/common/string/tools';
import { getLocationFromIp } from '@fastgpt/service/common/geo'; import { getLocationFromIp } from '@fastgpt/service/common/geo';
@ -19,12 +17,19 @@ import { AppReadChatLogPerVal } from '@fastgpt/global/support/permission/app/con
import { CommonErrEnum } from '@fastgpt/global/common/error/code/common'; import { CommonErrEnum } from '@fastgpt/global/common/error/code/common';
import type { ApiRequestProps } from '@fastgpt/service/type/next'; import type { ApiRequestProps } from '@fastgpt/service/type/next';
import { getLocale } from '@fastgpt/service/common/middle/i18n'; import { getLocale } from '@fastgpt/service/common/middle/i18n';
import { AppVersionCollectionName } from '@fastgpt/service/core/app/version/schema';
import {
GetAppChatLogsBodySchema,
GetAppChatLogsResponseSchema,
type getAppChatLogsResponseType
} from '@fastgpt/global/openapi/core/app/log/api';
async function handler( async function handler(
req: ApiRequestProps<GetAppChatLogsParams>, req: ApiRequestProps,
_res: NextApiResponse _res: NextApiResponse
): Promise<PaginationResponse<AppLogsListItemType>> { ): Promise<getAppChatLogsResponseType> {
const { appId, dateStart, dateEnd, sources, tmbIds, chatSearch } = req.body; const { appId, dateStart, dateEnd, sources, tmbIds, chatSearch, feedbackType, unreadOnly } =
GetAppChatLogsBodySchema.parse(req.body);
const { pageSize = 20, offset } = parsePaginationRequest(req); const { pageSize = 20, offset } = parsePaginationRequest(req);
@ -45,6 +50,31 @@ async function handler(
appId: new Types.ObjectId(appId), appId: new Types.ObjectId(appId),
source: sources ? { $in: sources } : { $exists: true }, source: sources ? { $in: sources } : { $exists: true },
tmbId: tmbIds ? { $in: tmbIds.map((item) => new Types.ObjectId(item)) } : { $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: { updateTime: {
$gte: new Date(dateStart), $gte: new Date(dateStart),
$lte: new Date(dateEnd) $lte: new Date(dateEnd)
@ -60,17 +90,16 @@ async function handler(
: undefined) : undefined)
}; };
const [list, total] = await Promise.all([ // Execute both queries
const [listResult, total] = await Promise.all([
// Execute the main aggregation
MongoChat.aggregate( MongoChat.aggregate(
[ [
{ $match: where }, { $match: where },
{ { $sort: { updateTime: -1 } },
$sort: {
updateTime: -1
}
},
{ $skip: offset }, { $skip: offset },
{ $limit: pageSize }, { $limit: pageSize },
// Match chat_items for other statistics
{ {
$lookup: { $lookup: {
from: ChatItemCollectionName, from: ChatItemCollectionName,
@ -87,44 +116,6 @@ async function handler(
$group: { $group: {
_id: null, _id: null,
messageCount: { $sum: 1 }, messageCount: { $sum: 1 },
goodFeedback: {
$sum: {
$cond: [
{
$ifNull: ['$userGoodFeedback', false]
},
1,
0
]
}
},
badFeedback: {
$sum: {
$cond: [
{
$ifNull: ['$userBadFeedback', false]
},
1,
0
]
}
},
customFeedback: {
$sum: {
$cond: [{ $gt: [{ $size: { $ifNull: ['$customFeedbacks', []] } }, 0] }, 1, 0]
}
},
adminMark: {
$sum: {
$cond: [
{
$ifNull: ['$adminFeedback', false]
},
1,
0
]
}
},
totalResponseTime: { totalResponseTime: {
$sum: { $sum: {
$cond: [{ $eq: ['$obj', 'AI'] }, { $ifNull: ['$durationSeconds', 0] }, 0] $cond: [{ $eq: ['$obj', 'AI'] }, { $ifNull: ['$durationSeconds', 0] }, 0]
@ -135,7 +126,26 @@ async function handler(
$cond: [{ $eq: ['$obj', 'AI'] }, 1, 0] $cond: [{ $eq: ['$obj', 'AI'] }, 1, 0]
} }
}, },
// errorCount from chatItem responseData adminMark: {
$sum: {
$cond: [{ $ifNull: ['$adminFeedback', false] }, 1, 0]
}
},
goodFeedback: {
$sum: {
$cond: [{ $ifNull: ['$userGoodFeedback', false] }, 1, 0]
}
},
badFeedback: {
$sum: {
$cond: [{ $ifNull: ['$userBadFeedback', false] }, 1, 0]
}
},
customFeedback: {
$sum: {
$cond: [{ $gt: [{ $size: { $ifNull: ['$customFeedbacks', []] } }, 0] }, 1, 0]
}
},
errorCountFromChatItem: { errorCountFromChatItem: {
$sum: { $sum: {
$cond: [ $cond: [
@ -158,15 +168,12 @@ async function handler(
] ]
} }
}, },
// totalPoints from chatItem responseData
totalPointsFromChatItem: { totalPointsFromChatItem: {
$sum: { $sum: {
$reduce: { $reduce: {
input: { $ifNull: ['$responseData', []] }, input: { $ifNull: ['$responseData', []] },
initialValue: 0, initialValue: 0,
in: { in: { $add: ['$$value', { $ifNull: ['$$this.totalPoints', 0] }] }
$add: ['$$value', { $ifNull: ['$$this.totalPoints', 0] }]
}
} }
} }
} }
@ -176,7 +183,7 @@ async function handler(
as: 'chatItemsData' as: 'chatItemsData'
} }
}, },
// Add lookup for chatItemResponse data // Match chatItemResponses
{ {
$lookup: { $lookup: {
from: ChatItemResponseCollectionName, from: ChatItemResponseCollectionName,
@ -208,9 +215,27 @@ async function handler(
as: 'chatItemResponsesData' as: 'chatItemResponsesData'
} }
}, },
// Match app versions
{
$lookup: {
from: AppVersionCollectionName,
localField: 'appVersionId',
foreignField: '_id',
pipeline: [
{
$project: {
versionName: 1,
_id: 0 // 排除 _id 字段,只返回 versionName
}
}
],
as: 'versionData'
}
},
{ {
$addFields: { $addFields: {
messageCount: { $ifNull: [{ $arrayElemAt: ['$chatItemsData.messageCount', 0] }, 0] }, messageCount: { $ifNull: [{ $arrayElemAt: ['$chatItemsData.messageCount', 0] }, 0] },
// Use feedback counts from Chat table (redundant fields)
userGoodFeedbackCount: { userGoodFeedbackCount: {
$ifNull: [{ $arrayElemAt: ['$chatItemsData.goodFeedback', 0] }, 0] $ifNull: [{ $arrayElemAt: ['$chatItemsData.goodFeedback', 0] }, 0]
}, },
@ -235,10 +260,9 @@ async function handler(
0 0
] ]
}, },
// Merge errorCount from both sources
errorCount: { errorCount: {
$add: [ $add: [
{ $ifNull: [{ $arrayElemAt: ['$chatItemsData.errorCountFromChatItem', 0] }, 0] }, { $ifNull: [{ $arrayElemAt: ['$chatItemsData.errorCountFromChatItem', 0] }, 0] }, // 适配旧版,响应字段存在 chat_items 里
{ {
$ifNull: [ $ifNull: [
{ $arrayElemAt: ['$chatItemResponsesData.errorCountFromResponse', 0] }, { $arrayElemAt: ['$chatItemResponsesData.errorCountFromResponse', 0] },
@ -247,7 +271,6 @@ async function handler(
} }
] ]
}, },
// Merge totalPoints from both sources
totalPoints: { totalPoints: {
$add: [ $add: [
{ $ifNull: [{ $arrayElemAt: ['$chatItemsData.totalPointsFromChatItem', 0] }, 0] }, { $ifNull: [{ $arrayElemAt: ['$chatItemsData.totalPointsFromChatItem', 0] }, 0] },
@ -258,13 +281,14 @@ async function handler(
] ]
} }
] ]
} },
versionName: { $ifNull: [{ $arrayElemAt: ['$versionData.versionName', 0] }, null] }
} }
}, },
{ {
$project: { $project: {
_id: 1, _id: { $toString: '$_id' },
id: '$chatId', chatId: 1,
title: 1, title: 1,
customTitle: 1, customTitle: 1,
source: 1, source: 1,
@ -280,18 +304,26 @@ async function handler(
errorCount: 1, errorCount: 1,
totalPoints: 1, totalPoints: 1,
outLinkUid: 1, outLinkUid: 1,
tmbId: 1, tmbId: {
$cond: {
if: { $eq: ['$tmbId', null] },
then: null,
else: { $toString: '$tmbId' }
}
},
versionName: 1,
region: '$metadata.originIp' region: '$metadata.originIp'
} }
} }
], ],
{ { ...readFromSecondary }
...readFromSecondary
}
), ),
// Execute the count pipeline
MongoChat.countDocuments(where, { ...readFromSecondary }) MongoChat.countDocuments(where, { ...readFromSecondary })
]); ]);
const list = listResult;
const listWithRegion = list.map((item) => { const listWithRegion = list.map((item) => {
const ip = item.region; const ip = item.region;
const region = getLocationFromIp(ip, getLocale(req)); const region = getLocationFromIp(ip, getLocale(req));
@ -302,16 +334,17 @@ async function handler(
}; };
}); });
// 获取有 tmbId 的人员
const listWithSourceMember = await addSourceMember({ const listWithSourceMember = await addSourceMember({
list: listWithRegion list: listWithRegion
}); });
// 获取没有 tmbId 的人员
const listWithoutTmbId = listWithRegion.filter((item) => !item.tmbId); const listWithoutTmbId = listWithRegion.filter((item) => !item.tmbId);
return { return GetAppChatLogsResponseSchema.parse({
list: listWithSourceMember.concat(listWithoutTmbId), list: listWithSourceMember.concat(listWithoutTmbId),
total total
}; });
} }
export default NextAPI(handler); export default NextAPI(handler);

View File

@ -3,22 +3,11 @@ import { NextAPI } from '@/service/middleware/entry';
import { MongoAppLogKeys } from '@fastgpt/service/core/app/logs/logkeysSchema'; import { MongoAppLogKeys } from '@fastgpt/service/core/app/logs/logkeysSchema';
import { WritePermissionVal } from '@fastgpt/global/support/permission/constant'; import { WritePermissionVal } from '@fastgpt/global/support/permission/constant';
import { authApp } from '@fastgpt/service/support/permission/app/auth'; import { authApp } from '@fastgpt/service/support/permission/app/auth';
import type { AppLogKeysType } from '@fastgpt/global/core/app/logs/type'; import { UpdateLogKeysBodySchema } from '@fastgpt/global/openapi/core/app/log/api';
export type updateLogKeysQuery = {}; async function handler(req: ApiRequestProps, res: ApiResponseType<any>): Promise<{}> {
const { appId, logKeys } = UpdateLogKeysBodySchema.parse(req.body);
export type updateLogKeysBody = {
appId: string;
logKeys: AppLogKeysType[];
};
export type updateLogKeysResponse = {};
async function handler(
req: ApiRequestProps<updateLogKeysBody, updateLogKeysQuery>,
res: ApiResponseType<any>
): Promise<updateLogKeysResponse> {
const { appId, logKeys } = req.body;
const { teamId } = await authApp({ const { teamId } = await authApp({
req, req,
authToken: true, authToken: true,

View File

@ -1,49 +1,45 @@
import type { NextApiRequest, NextApiResponse } from 'next'; import type { ApiRequestProps, ApiResponseType } from '@fastgpt/service/type/next';
import { jsonRes } from '@fastgpt/service/common/response'; import { NextAPI } from '@/service/middleware/entry';
import type { AdminUpdateFeedbackParams } from '@/global/core/chat/api.d';
import { MongoChatItem } from '@fastgpt/service/core/chat/chatItemSchema'; import { MongoChatItem } from '@fastgpt/service/core/chat/chatItemSchema';
import { authChatCrud } from '@/service/support/permission/auth/chat'; import { authChatCrud } from '@/service/support/permission/auth/chat';
import {
AdminUpdateFeedbackBodySchema,
AdminUpdateFeedbackResponseSchema,
type AdminUpdateFeedbackResponseType
} from '@fastgpt/global/openapi/core/chat/feedback/api';
/* 初始化我的聊天框,需要身份验证 */ async function handler(
export default async function handler(req: NextApiRequest, res: NextApiResponse) { req: ApiRequestProps,
try { _res: ApiResponseType<any>
const { appId, chatId, dataId, datasetId, feedbackDataId, q, a } = ): Promise<AdminUpdateFeedbackResponseType> {
req.body as AdminUpdateFeedbackParams; const { appId, chatId, dataId, datasetId, feedbackDataId, q, a } =
AdminUpdateFeedbackBodySchema.parse(req.body);
if (!dataId || !datasetId || !feedbackDataId || !q) { await authChatCrud({
throw new Error('missing parameter'); req,
} authToken: true,
authApiKey: true,
appId,
chatId
});
await authChatCrud({ await MongoChatItem.updateOne(
req, {
authToken: true,
authApiKey: true,
appId, appId,
chatId chatId,
}); dataId
},
await MongoChatItem.findOneAndUpdate( {
{ adminFeedback: {
appId, datasetId,
chatId, dataId: feedbackDataId,
dataId q,
}, a
{
adminFeedback: {
datasetId,
dataId: feedbackDataId,
q,
a
}
} }
); }
);
jsonRes(res); return AdminUpdateFeedbackResponseSchema.parse({});
} catch (err) {
jsonRes(res, {
code: 500,
error: err
});
}
} }
export default NextAPI(handler);

View File

@ -1,52 +1,55 @@
import type { NextApiRequest, NextApiResponse } from 'next'; import type { ApiRequestProps, ApiResponseType } from '@fastgpt/service/type/next';
import { jsonRes } from '@fastgpt/service/common/response'; import { NextAPI } from '@/service/middleware/entry';
import { authCert } from '@fastgpt/service/support/permission/auth/common'; import { authCert } from '@fastgpt/service/support/permission/auth/common';
import type { CloseCustomFeedbackParams } from '@/global/core/chat/api.d';
import { MongoChatItem } from '@fastgpt/service/core/chat/chatItemSchema'; import { MongoChatItem } from '@fastgpt/service/core/chat/chatItemSchema';
import { authChatCrud } from '@/service/support/permission/auth/chat'; import { authChatCrud } from '@/service/support/permission/auth/chat';
import { mongoSessionRun } from '@fastgpt/service/common/mongo/sessionRun'; import { mongoSessionRun } from '@fastgpt/service/common/mongo/sessionRun';
import { updateChatFeedbackCount } from '@fastgpt/service/core/chat/controller';
import {
CloseCustomFeedbackBodySchema,
CloseCustomFeedbackResponseSchema,
type CloseCustomFeedbackResponseType
} from '@fastgpt/global/openapi/core/chat/feedback/api';
/* remove custom feedback */ async function handler(
export default async function handler(req: NextApiRequest, res: NextApiResponse) { req: ApiRequestProps,
try { _res: ApiResponseType<any>
const { appId, chatId, dataId, index } = req.body as CloseCustomFeedbackParams; ): Promise<CloseCustomFeedbackResponseType> {
const { appId, chatId, dataId, index } = CloseCustomFeedbackBodySchema.parse(req.body);
if (!dataId || !appId || !chatId) { await authChatCrud({
throw new Error('missing parameter'); req,
} authToken: true,
authApiKey: true,
appId,
chatId
});
await authCert({ req, authToken: true });
await authChatCrud({ await mongoSessionRun(async (session) => {
req, // Remove custom feedback at index
authToken: true, await MongoChatItem.findOneAndUpdate(
authApiKey: true, { appId, chatId, dataId },
{ $unset: { [`customFeedbacks.${index}`]: 1 } },
{ session }
);
// Remove null values from array
await MongoChatItem.updateOne(
{ appId, chatId, dataId },
{ $pull: { customFeedbacks: null } },
{ session }
);
// Update ChatLog feedback statistics
await updateChatFeedbackCount({
appId, appId,
chatId chatId,
session
}); });
await authCert({ req, authToken: true }); });
await mongoSessionRun(async (session) => { return CloseCustomFeedbackResponseSchema.parse({});
await MongoChatItem.findOneAndUpdate(
{ appId, chatId, dataId },
{ $unset: { [`customFeedbacks.${index}`]: 1 } },
{
session
}
);
await MongoChatItem.findOneAndUpdate(
{ appId, chatId, dataId },
{ $pull: { customFeedbacks: null } },
{
session
}
);
});
jsonRes(res);
} catch (err) {
jsonRes(res, {
code: 500,
error: err
});
}
} }
export default NextAPI(handler);

Some files were not shown because too many files have changed in this diff Show More