optimize app update time (#6127)

* feat: add chat visibility controls and improve quote reader permissions (#6102)

* feat: add chat visibility controls and improve quote reader permissions

* fix test

* zod

* fix

* test & openapi

* frontend filter

* update name

* fix

* fix

* rename variables

* fix

* test

* fix build

* fix

* fix

---------

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

* app update time

* recent app

* fix

* type

* fix

* context

* perf: update app usingtime code

* fix: ts

* update parent

* doc

* perf: code per

* unauth refresh

---------

Co-authored-by: archer <545436317@qq.com>
This commit is contained in:
heheer 2025-12-24 14:28:42 +08:00 committed by GitHub
parent ab743b9358
commit f175a1a30c
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
108 changed files with 2274 additions and 757 deletions

View File

@ -437,7 +437,7 @@ import {
setAgentRuntimeStop, setAgentRuntimeStop,
waitForWorkflowComplete waitForWorkflowComplete
} from '@fastgpt/service/core/workflow/dispatch/workflowStatus'; } from '@fastgpt/service/core/workflow/dispatch/workflowStatus';
import { StopV2ChatSchema, type StopV2ChatResponse } from '@fastgpt/global/openapi/core/chat/api'; import { StopV2ChatSchema, type StopV2ChatResponse } from '@fastgpt/global/openapi/core/chat/controler/api';
async function handler(req: NextApiRequest, res: NextApiResponse): Promise<StopV2ChatResponse> { async function handler(req: NextApiRequest, res: NextApiResponse): Promise<StopV2ChatResponse> {
const { appId, chatId, outLinkAuthData } = StopV2ChatSchema.parse(req.body); const { appId, chatId, outLinkAuthData } = StopV2ChatSchema.parse(req.body);
@ -527,7 +527,7 @@ LLM 节点,流输出时会同时被终止,但 HTTP 请求节点这种可能
```typescript ```typescript
import { POST } from '@/web/common/api/request'; import { POST } from '@/web/common/api/request';
import type { StopV2ChatParams, StopV2ChatResponse } from '@fastgpt/global/openapi/core/chat/api'; import type { StopV2ChatParams, StopV2ChatResponse } from '@fastgpt/global/openapi/core/chat/controler/api';
/** /**
* 停止 v2 版本工作流运行 * 停止 v2 版本工作流运行

View File

@ -7,6 +7,8 @@ description: 'FastGPT V4.14.5 更新说明'
## 🚀 新增内容 ## 🚀 新增内容
1. 对话记录使用侧改成软删除,增加从日志管理里删除对话记录。 1. 对话记录使用侧改成软删除,增加从日志管理里删除对话记录。
2. 更新Agent/工具时,会更新其上层所有目录的更新时间,以便其会排在列表前面。
3. 门户页支持配置单个应用运行可见度配。
## ⚙️ 优化 ## ⚙️ 优化

View File

@ -120,7 +120,7 @@
"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-16T14:56:04+08:00", "document/content/docs/upgrading/4-14/4144.mdx": "2025-12-16T14:56:04+08:00",
"document/content/docs/upgrading/4-14/4145.mdx": "2025-12-21T19:15:10+08:00", "document/content/docs/upgrading/4-14/4145.mdx": "2025-12-21T23:28:19+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,6 +1,8 @@
import type { OpenAPIPath } from '../../type'; import type { OpenAPIPath } from '../../type';
import { AppLogPath } from './log'; import { AppLogPath } from './log';
import { PublishChannelPath } from './publishChannel';
export const AppPath: OpenAPIPath = { export const AppPath: OpenAPIPath = {
...AppLogPath ...AppLogPath,
...PublishChannelPath
}; };

View File

@ -0,0 +1,5 @@
import { PlaygroundPath } from './playground';
export const PublishChannelPath = {
...PlaygroundPath
};

View File

@ -0,0 +1,52 @@
import { z } from 'zod';
import { ObjectIdSchema } from '../../../../../common/type/mongo';
// Playground Visibility Config Fields
const PlaygroundVisibilityConfigFieldsSchema = z.object({
showRunningStatus: z.boolean().meta({
example: true,
description: '是否显示运行状态'
}),
showCite: z.boolean().meta({
example: true,
description: '是否显示引用'
}),
showFullText: z.boolean().meta({
example: true,
description: '是否显示全文'
}),
canDownloadSource: z.boolean().meta({
example: true,
description: '是否可下载来源'
})
});
// Get Playground Visibility Config Parameters
export const GetPlaygroundVisibilityConfigParamsSchema = z.object({
appId: ObjectIdSchema.meta({
example: '68ad85a7463006c963799a05',
description: '应用 ID'
})
});
export type GetPlaygroundVisibilityConfigParamsType = z.infer<
typeof GetPlaygroundVisibilityConfigParamsSchema
>;
// Playground Visibility Config Response
export const PlaygroundVisibilityConfigResponseSchema = PlaygroundVisibilityConfigFieldsSchema;
export type PlaygroundVisibilityConfigResponseType = z.infer<
typeof PlaygroundVisibilityConfigResponseSchema
>;
// Update Playground Visibility Config Parameters
export const UpdatePlaygroundVisibilityConfigParamsSchema = z
.object({
appId: ObjectIdSchema.meta({
example: '68ad85a7463006c963799a05',
description: '应用 ID'
})
})
.extend(PlaygroundVisibilityConfigFieldsSchema.shape);
export type UpdatePlaygroundVisibilityConfigParamsType = z.infer<
typeof UpdatePlaygroundVisibilityConfigParamsSchema
>;

View File

@ -0,0 +1,109 @@
import { z } from 'zod';
import type { OpenAPIPath } from '../../../../type';
import {
GetPlaygroundVisibilityConfigParamsSchema,
PlaygroundVisibilityConfigResponseSchema,
UpdatePlaygroundVisibilityConfigParamsSchema
} from './api';
import { TagsMap } from '../../../../tag';
export const PlaygroundPath: OpenAPIPath = {
'/api/support/outLink/playground/config': {
get: {
summary: '获取门户配置',
description:
'获取指定应用的门户聊天界面的可见性配置,包括节点状态、响应详情、全文显示和原始来源显示的设置',
tags: [TagsMap.publishChannel],
requestParams: {
query: GetPlaygroundVisibilityConfigParamsSchema
},
responses: {
200: {
description: '成功返回门户配置',
content: {
'application/json': {
schema: PlaygroundVisibilityConfigResponseSchema
}
}
},
400: {
description: '请求参数错误',
content: {
'application/json': {
schema: z.object({
code: z.literal(500),
statusText: z.literal('Invalid Params'),
message: z.string(),
data: z.null()
})
}
}
},
401: {
description: '用户未授权',
content: {
'application/json': {
schema: z.object({
code: z.literal(401),
statusText: z.literal('unAuthorization'),
message: z.string(),
data: z.null()
})
}
}
}
}
}
},
'/api/support/outLink/playground/update': {
post: {
summary: '更新门户配置',
description:
'更新指定应用的门户聊天界面的可见性配置,包括节点状态、响应详情、全文显示和原始来源显示的设置。如果配置不存在则创建新配置',
tags: [TagsMap.publishChannel],
requestBody: {
content: {
'application/json': {
schema: UpdatePlaygroundVisibilityConfigParamsSchema
}
}
},
responses: {
200: {
description: '成功更新门户配置',
content: {
'application/json': {
schema: z.null()
}
}
},
400: {
description: '请求参数错误',
content: {
'application/json': {
schema: z.object({
code: z.literal(500),
statusText: z.literal('Invalid Params'),
message: z.string(),
data: z.null()
})
}
}
},
401: {
description: '用户未授权',
content: {
'application/json': {
schema: z.object({
code: z.literal(401),
statusText: z.literal('unAuthorization'),
message: z.string(),
data: z.null()
})
}
}
}
}
}
}
};

View File

@ -1,72 +1,12 @@
import { OutLinkChatAuthSchema } from '../../../support/permission/chat/type';
import { ObjectIdSchema } from '../../../common/type/mongo'; import { ObjectIdSchema } from '../../../common/type/mongo';
import z from 'zod'; import z from 'zod';
/* ============ v2/chat/stop ============ */ /* Recently Used Apps */
export const StopV2ChatSchema = z export const GetRecentlyUsedAppsResponseSchema = z.array(
.object({ z.object({
appId: ObjectIdSchema.describe('应用ID'), appId: ObjectIdSchema.describe('应用ID'),
chatId: z.string().min(1).describe('对话ID'), name: z.string().min(1).describe('应用名称'),
outLinkAuthData: OutLinkChatAuthSchema.optional().describe('外链鉴权数据') avatar: z.string().min(1).describe('应用头像')
}) })
.meta({ );
example: { export type GetRecentlyUsedAppsResponseType = z.infer<typeof GetRecentlyUsedAppsResponseSchema>;
appId: '1234567890',
chatId: '1234567890',
outLinkAuthData: {
shareId: '1234567890',
outLinkUid: '1234567890'
}
}
});
export type StopV2ChatParams = z.infer<typeof StopV2ChatSchema>;
export const StopV2ChatResponseSchema = z
.object({
success: z.boolean().describe('是否成功停止')
})
.meta({
example: {
success: true
}
});
export type StopV2ChatResponse = z.infer<typeof StopV2ChatResponseSchema>;
/* ============ chat file ============ */
export const PresignChatFileGetUrlSchema = z
.object({
key: z.string().min(1).describe('文件key'),
appId: ObjectIdSchema.describe('应用ID'),
outLinkAuthData: OutLinkChatAuthSchema.optional().describe('外链鉴权数据')
})
.meta({
example: {
key: '1234567890',
appId: '1234567890',
outLinkAuthData: {
shareId: '1234567890',
outLinkUid: '1234567890'
}
}
});
export type PresignChatFileGetUrlParams = z.infer<typeof PresignChatFileGetUrlSchema>;
export const PresignChatFilePostUrlSchema = z
.object({
filename: z.string().min(1).describe('文件名'),
appId: ObjectIdSchema.describe('应用ID'),
chatId: z.string().min(1).describe('对话ID'),
outLinkAuthData: OutLinkChatAuthSchema.optional().describe('外链鉴权数据')
})
.meta({
example: {
filename: '1234567890',
appId: '1234567890',
chatId: '1234567890',
outLinkAuthData: {
shareId: '1234567890',
outLinkUid: '1234567890'
}
}
});
export type PresignChatFilePostUrlParams = z.infer<typeof PresignChatFilePostUrlSchema>;

View File

@ -0,0 +1,72 @@
import { OutLinkChatAuthSchema } from '../../../../support/permission/chat/type';
import { ObjectIdSchema } from '../../../../common/type/mongo';
import z from 'zod';
/* ============ v2/chat/stop ============ */
export const StopV2ChatSchema = z
.object({
appId: ObjectIdSchema.describe('应用ID'),
chatId: z.string().min(1).describe('对话ID'),
outLinkAuthData: OutLinkChatAuthSchema.optional().describe('外链鉴权数据')
})
.meta({
example: {
appId: '1234567890',
chatId: '1234567890',
outLinkAuthData: {
shareId: '1234567890',
outLinkUid: '1234567890'
}
}
});
export type StopV2ChatParams = z.infer<typeof StopV2ChatSchema>;
export const StopV2ChatResponseSchema = z
.object({
success: z.boolean().describe('是否成功停止')
})
.meta({
example: {
success: true
}
});
export type StopV2ChatResponse = z.infer<typeof StopV2ChatResponseSchema>;
/* ============ chat file ============ */
export const PresignChatFileGetUrlSchema = z
.object({
key: z.string().min(1).describe('文件key'),
appId: ObjectIdSchema.describe('应用ID'),
outLinkAuthData: OutLinkChatAuthSchema.optional().describe('外链鉴权数据')
})
.meta({
example: {
key: '1234567890',
appId: '1234567890',
outLinkAuthData: {
shareId: '1234567890',
outLinkUid: '1234567890'
}
}
});
export type PresignChatFileGetUrlParams = z.infer<typeof PresignChatFileGetUrlSchema>;
export const PresignChatFilePostUrlSchema = z
.object({
filename: z.string().min(1).describe('文件名'),
appId: ObjectIdSchema.describe('应用ID'),
chatId: z.string().min(1).describe('对话ID'),
outLinkAuthData: OutLinkChatAuthSchema.optional().describe('外链鉴权数据')
})
.meta({
example: {
filename: '1234567890',
appId: '1234567890',
chatId: '1234567890',
outLinkAuthData: {
shareId: '1234567890',
outLinkUid: '1234567890'
}
}
});
export type PresignChatFilePostUrlParams = z.infer<typeof PresignChatFilePostUrlSchema>;

View File

@ -0,0 +1,86 @@
import type { OpenAPIPath } from '../../../type';
import { TagsMap } from '../../../tag';
import {
StopV2ChatSchema,
StopV2ChatResponseSchema,
PresignChatFilePostUrlSchema,
PresignChatFileGetUrlSchema
} from './api';
import { CreatePostPresignedUrlResultSchema } from '../../../../../service/common/s3/type';
import { z } from 'zod';
export const ChatControllerPath: OpenAPIPath = {
'/v2/chat/stop': {
post: {
summary: '停止 Agent 运行',
description: `优雅停止正在运行的 Agent, 会尝试等待当前节点结束后返回,最长 5s超过 5s 仍未结束,则会返回成功。
LLM HTTP `,
tags: [TagsMap.chatController],
requestBody: {
content: {
'application/json': {
schema: StopV2ChatSchema
}
}
},
responses: {
200: {
description: '成功停止工作流',
content: {
'application/json': {
schema: StopV2ChatResponseSchema
}
}
}
}
}
},
'/core/chat/presignChatFilePostUrl': {
post: {
summary: '获取文件上传 URL',
description: '获取文件上传 URL',
tags: [TagsMap.chatController],
requestBody: {
content: {
'application/json': {
schema: PresignChatFilePostUrlSchema
}
}
},
responses: {
200: {
description: '成功上传对话文件预签名 URL',
content: {
'application/json': {
schema: CreatePostPresignedUrlResultSchema
}
}
}
}
}
},
'/core/chat/presignChatFileGetUrl': {
post: {
summary: '获取文件预览地址',
description: '获取文件预览地址',
tags: [TagsMap.chatController],
requestBody: {
content: {
'application/json': {
schema: PresignChatFileGetUrlSchema
}
}
},
responses: {
200: {
description: '成功获取对话文件预签名 URL',
content: {
'application/json': {
schema: z.string()
}
}
}
}
}
}
};

View File

@ -3,89 +3,28 @@ import { ChatSettingPath } from './setting';
import { ChatFavouriteAppPath } from './favourite/index'; import { ChatFavouriteAppPath } from './favourite/index';
import { ChatFeedbackPath } from './feedback/index'; import { ChatFeedbackPath } from './feedback/index';
import { ChatHistoryPath } from './history/index'; import { ChatHistoryPath } from './history/index';
import { z } from 'zod'; import { GetRecentlyUsedAppsResponseSchema } from './api';
import { CreatePostPresignedUrlResultSchema } from '../../../../service/common/s3/type';
import {
PresignChatFileGetUrlSchema,
PresignChatFilePostUrlSchema,
StopV2ChatSchema,
StopV2ChatResponseSchema
} from './api';
import { TagsMap } from '../../tag'; import { TagsMap } from '../../tag';
import { ChatControllerPath } from './controler';
export const ChatPath: OpenAPIPath = { export const ChatPath: OpenAPIPath = {
...ChatSettingPath, ...ChatSettingPath,
...ChatFavouriteAppPath, ...ChatFavouriteAppPath,
...ChatFeedbackPath, ...ChatFeedbackPath,
...ChatHistoryPath, ...ChatHistoryPath,
...ChatControllerPath,
'/v2/chat/stop': { '/core/chat/recentlyUsed': {
post: { get: {
summary: '停止 Agent 运行', summary: '获取最近使用的应用',
description: `优雅停止正在运行的 Agent, 会尝试等待当前节点结束后返回,最长 5s超过 5s 仍未结束,则会返回成功。 description: '获取最近使用的应用',
LLM HTTP `,
tags: [TagsMap.chatPage], tags: [TagsMap.chatPage],
requestBody: {
content: {
'application/json': {
schema: StopV2ChatSchema
}
}
},
responses: { responses: {
200: { 200: {
description: '成功停止工作流', description: '成功返回最近使用的应用',
content: { content: {
'application/json': { 'application/json': {
schema: StopV2ChatResponseSchema schema: GetRecentlyUsedAppsResponseSchema
}
}
}
}
}
},
'/core/chat/presignChatFilePostUrl': {
post: {
summary: '获取文件上传 URL',
description: '获取文件上传 URL',
tags: [TagsMap.chatPage],
requestBody: {
content: {
'application/json': {
schema: PresignChatFilePostUrlSchema
}
}
},
responses: {
200: {
description: '成功上传对话文件预签名 URL',
content: {
'application/json': {
schema: CreatePostPresignedUrlResultSchema
}
}
}
}
}
},
'/core/chat/presignChatFileGetUrl': {
post: {
summary: '获取文件预览地址',
description: '获取文件预览地址',
tags: [TagsMap.chatPage],
requestBody: {
content: {
'application/json': {
schema: PresignChatFileGetUrlSchema
}
}
},
responses: {
200: {
description: '成功获取对话文件预签名 URL',
content: {
'application/json': {
schema: z.string()
} }
} }
} }

View File

@ -26,7 +26,7 @@ export const openAPIDocument = createDocument({
'x-tagGroups': [ 'x-tagGroups': [
{ {
name: 'Agent 应用', name: 'Agent 应用',
tags: [TagsMap.appLog] tags: [TagsMap.appLog, TagsMap.publishChannel]
}, },
{ {
name: '对话管理', name: '对话管理',

View File

@ -4,7 +4,8 @@ export const TagsMap = {
appLog: 'Agent 日志', appLog: 'Agent 日志',
// Chat - home // Chat - home
chatPage: '对话页操作', chatPage: '对话页',
chatController: '对话框操作',
chatHistory: '对话历史管理', chatHistory: '对话历史管理',
chatSetting: '门户页配置', chatSetting: '门户页配置',
chatFeedback: '对话反馈', chatFeedback: '对话反馈',
@ -13,6 +14,9 @@ export const TagsMap = {
pluginToolTag: '工具标签', pluginToolTag: '工具标签',
pluginTeam: '团队插件管理', pluginTeam: '团队插件管理',
// Publish Channel
publishChannel: '发布渠道',
/* Support */ /* Support */
// Wallet // Wallet
walletBill: '订单', walletBill: '订单',

View File

@ -1,5 +1,7 @@
import { z } from 'zod';
import type { HistoryItemType } from '../../core/chat/type.d'; import type { HistoryItemType } from '../../core/chat/type.d';
import type { OutLinkSchema } from './type.d'; import type { OutLinkSchema, PlaygroundVisibilityConfigType } from './type.d';
import { PlaygroundVisibilityConfigSchema } from './type.d';
export type AuthOutLinkInitProps = { export type AuthOutLinkInitProps = {
outLinkUid: string; outLinkUid: string;
@ -10,3 +12,20 @@ export type AuthOutLinkLimitProps = AuthOutLinkChatProps & { outLink: OutLinkSch
export type AuthOutLinkResponse = { export type AuthOutLinkResponse = {
uid: string; uid: string;
}; };
export const UpdatePlaygroundVisibilityConfigBodySchema = PlaygroundVisibilityConfigSchema.extend({
appId: z.string().min(1, 'App ID is required')
});
export type UpdatePlaygroundVisibilityConfigBody = z.infer<
typeof UpdatePlaygroundVisibilityConfigBodySchema
>;
export const PlaygroundVisibilityConfigQuerySchema = z.object({
appId: z.string().min(1, 'App ID is required')
});
export type PlaygroundVisibilityConfigQuery = z.infer<typeof PlaygroundVisibilityConfigQuerySchema>;
export const PlaygroundVisibilityConfigResponseSchema = PlaygroundVisibilityConfigSchema;
export type PlaygroundVisibilityConfigResponse = z.infer<
typeof PlaygroundVisibilityConfigResponseSchema
>;

View File

@ -5,5 +5,6 @@ export enum PublishChannelEnum {
feishu = 'feishu', feishu = 'feishu',
dingtalk = 'dingtalk', dingtalk = 'dingtalk',
wecom = 'wecom', wecom = 'wecom',
officialAccount = 'official_account' officialAccount = 'official_account',
playground = 'playground'
} }

View File

@ -1,3 +1,4 @@
import { z } from 'zod';
import { AppSchema } from '../../core/app/type'; import { AppSchema } from '../../core/app/type';
import type { PublishChannelEnum } from './constant'; import type { PublishChannelEnum } from './constant';
import { RequireOnlyOne } from '../../common/type/utils'; import { RequireOnlyOne } from '../../common/type/utils';
@ -63,14 +64,14 @@ export type OutLinkSchema<T extends OutlinkAppType = undefined> = {
lastTime: Date; lastTime: Date;
type: PublishChannelEnum; type: PublishChannelEnum;
// whether the response content is detailed // whether to show the quote
responseDetail: boolean; showCite: boolean;
// whether to hide the node status // whether to show the running status
showNodeStatus?: boolean; showRunningStatus: boolean;
// wheter to show the full text reader // whether to show the full text reader
// showFullText?: boolean; showFullText: boolean;
// whether to show the complete quote // whether can download source
showRawSource?: boolean; canDownloadSource: boolean;
// response when request // response when request
immediateResponse?: string; immediateResponse?: string;
@ -93,10 +94,10 @@ export type OutLinkSchema<T extends OutlinkAppType = undefined> = {
export type OutLinkEditType<T = undefined> = { export type OutLinkEditType<T = undefined> = {
_id?: string; _id?: string;
name: string; name: string;
responseDetail?: OutLinkSchema<T>['responseDetail']; showCite?: OutLinkSchema<T>['showCite'];
showNodeStatus?: OutLinkSchema<T>['showNodeStatus']; showRunningStatus?: OutLinkSchema<T>['showRunningStatus'];
// showFullText?: OutLinkSchema<T>['showFullText']; showFullText?: OutLinkSchema<T>['showFullText'];
showRawSource?: OutLinkSchema<T>['showRawSource']; canDownloadSource?: OutLinkSchema<T>['canDownloadSource'];
// response when request // response when request
immediateResponse?: string; immediateResponse?: string;
// response when error or other situation // response when error or other situation
@ -106,3 +107,12 @@ export type OutLinkEditType<T = undefined> = {
// config for specific platform // config for specific platform
app?: T; app?: T;
}; };
export const PlaygroundVisibilityConfigSchema = z.object({
showRunningStatus: z.boolean(),
showCite: z.boolean(),
showFullText: z.boolean(),
canDownloadSource: z.boolean()
});
export type PlaygroundVisibilityConfigType = z.infer<typeof PlaygroundVisibilityConfigSchema>;

View File

@ -27,6 +27,9 @@ import { getS3ChatSource } from '../../common/s3/sources/chat';
import { MongoAppChatLog } from './logs/chatLogsSchema'; import { MongoAppChatLog } from './logs/chatLogsSchema';
import { MongoAppRegistration } from '../../support/appRegistration/schema'; import { MongoAppRegistration } from '../../support/appRegistration/schema';
import { MongoMcpKey } from '../../support/mcp/schema'; import { MongoMcpKey } from '../../support/mcp/schema';
import { MongoAppRecord } from './record/schema';
import { mongoSessionRun } from '../../common/mongo/sessionRun';
import { addLog } from '../../common/system/log';
export const beforeUpdateAppFormat = ({ nodes }: { nodes?: StoreNodeItemType[] }) => { export const beforeUpdateAppFormat = ({ nodes }: { nodes?: StoreNodeItemType[] }) => {
if (!nodes) return; if (!nodes) return;
@ -203,4 +206,29 @@ export const deleteAppsImmediate = async ({
'_id' '_id'
).lean(); ).lean();
await Promise.all(evalJobs.map((evalJob) => removeEvaluationJob(evalJob._id))); await Promise.all(evalJobs.map((evalJob) => removeEvaluationJob(evalJob._id)));
// Remove app record
await MongoAppRecord.deleteMany({ teamId, appId: { $in: appIds } });
};
export const updateParentFoldersUpdateTime = ({ parentId }: { parentId?: string | null }) => {
mongoSessionRun(async (session) => {
const existsId = new Set<string>();
while (true) {
if (!parentId || existsId.has(parentId)) return;
existsId.add(parentId);
const parentApp = await MongoApp.findById(parentId, 'parentId updateTime');
if (!parentApp) return;
parentApp.updateTime = new Date();
await parentApp.save({ session });
// 递归更新上层
parentId = parentApp.parentId;
}
}).catch((err) => {
addLog.error('updateParentFoldersUpdateTime error', err);
});
}; };

View File

@ -0,0 +1,45 @@
import {
TeamCollectionName,
TeamMemberCollectionName
} from '@fastgpt/global/support/user/team/constant';
import { getMongoModel, Schema } from '../../../common/mongo';
import { AppCollectionName } from '../schema';
import type { AppRecordType } from './type';
export const AppRecordCollectionName = 'app_records';
const AppRecordSchema = new Schema(
{
tmbId: {
type: Schema.Types.ObjectId,
ref: TeamMemberCollectionName,
required: true
},
teamId: {
type: Schema.Types.ObjectId,
ref: TeamCollectionName,
required: true
},
appId: {
type: Schema.Types.ObjectId,
ref: AppCollectionName,
required: true
},
lastUsedTime: {
type: Date,
default: () => new Date()
}
},
{
timestamps: false
}
);
AppRecordSchema.index({ tmbId: 1, lastUsedTime: -1 }); // 查询用户最近使用的应用
AppRecordSchema.index({ tmbId: 1, appId: 1 }, { unique: true }); // 防止重复记录
AppRecordSchema.index({ teamId: 1, appId: 1 }); // 用于清理权限失效的记录
export const MongoAppRecord = getMongoModel<AppRecordType>(
AppRecordCollectionName,
AppRecordSchema
);

View File

@ -0,0 +1,12 @@
import { z } from 'zod';
export const AppRecordSchemaZod = z.object({
_id: z.string(),
tmbId: z.string(),
teamId: z.string(),
appId: z.string(),
lastUsedTime: z.date()
});
// TypeScript types inferred from Zod schemas
export type AppRecordType = z.infer<typeof AppRecordSchemaZod>;

View File

@ -0,0 +1,27 @@
import { MongoAppRecord } from './schema';
import { addLog } from '../../../common/system/log';
export const recordAppUsage = async ({
appId,
tmbId,
teamId
}: {
appId: string;
tmbId: string;
teamId: string;
}) => {
await MongoAppRecord.updateOne(
{ tmbId, appId },
{
$set: {
teamId,
lastUsedTime: new Date()
}
},
{
upsert: true
}
).catch((error) => {
addLog.error('recordAppUsage error', error);
});
};

View File

@ -35,7 +35,6 @@ export type Props = {
nodes: StoreNodeItemType[]; nodes: StoreNodeItemType[];
appChatConfig?: AppChatConfigType; appChatConfig?: AppChatConfigType;
variables?: Record<string, any>; variables?: Record<string, any>;
isUpdateUseTime: boolean;
newTitle: string; newTitle: string;
source: `${ChatSourceEnum}`; source: `${ChatSourceEnum}`;
sourceName?: string; sourceName?: string;
@ -219,7 +218,6 @@ export async function saveChat(props: Props) {
nodes, nodes,
appChatConfig, appChatConfig,
variables, variables,
isUpdateUseTime,
newTitle, newTitle,
source, source,
sourceName, sourceName,
@ -393,18 +391,6 @@ export async function saveChat(props: Props) {
} catch (error) { } catch (error) {
addLog.error('Push chat log error', error); addLog.error('Push chat log error', error);
} }
if (isUpdateUseTime) {
await MongoApp.updateOne(
{ _id: appId },
{
updateTime: new Date()
},
{
...writePrimary
}
).catch();
}
} catch (error) { } catch (error) {
addLog.error(`update chat history error`, error); addLog.error(`update chat history error`, error);
} }

View File

@ -43,19 +43,21 @@ const OutLinkSchema = new Schema({
type: Date type: Date
}, },
responseDetail: { showRunningStatus: {
type: Boolean, type: Boolean,
default: false default: false
}, },
showNodeStatus: { showCite: {
type: Boolean, type: Boolean,
default: true default: false
}, },
// showFullText: { showFullText: {
// type: Boolean type: Boolean,
// }, default: false
showRawSource: { },
type: Boolean canDownloadSource: {
type: Boolean,
default: false
}, },
limit: { limit: {
maxUsagePoints: { maxUsagePoints: {

View File

@ -17,12 +17,14 @@ export function useLinkedScroll<
pageSize = 10, pageSize = 10,
params = {}, params = {},
currentData, currentData,
defaultScroll = 'top' defaultScroll = 'top',
showErrorToast = true
}: { }: {
pageSize?: number; pageSize?: number;
params?: Record<string, any>; params?: Record<string, any>;
currentData?: { id: string; anchor?: any }; currentData?: { id: string; anchor?: any };
defaultScroll?: 'top' | 'bottom'; defaultScroll?: 'top' | 'bottom';
showErrorToast?: boolean;
} }
) { ) {
const { t } = useTranslation(); const { t } = useTranslation();
@ -105,7 +107,8 @@ export function useLinkedScroll<
onFinally() { onFinally() {
isInit.current = true; isInit.current = true;
}, },
manual: false manual: false,
errorToast: showErrorToast ? undefined : ''
} }
); );
useEffect(() => { useEffect(() => {
@ -153,7 +156,8 @@ export function useLinkedScroll<
return response; return response;
}, },
{ {
refreshDeps: [hasMorePrev, isLoading, params, pageSize] refreshDeps: [hasMorePrev, isLoading, params, pageSize],
errorToast: showErrorToast ? undefined : ''
} }
); );
@ -188,7 +192,8 @@ export function useLinkedScroll<
return response; return response;
}, },
{ {
refreshDeps: [hasMoreNext, isLoading, params, pageSize] refreshDeps: [hasMoreNext, isLoading, params, pageSize],
errorToast: showErrorToast ? undefined : ''
} }
); );

View File

@ -269,7 +269,7 @@ export function useScrollPagination<
} catch (error: any) { } catch (error: any) {
if (showErrorToast) { if (showErrorToast) {
toast({ toast({
title: getErrText(error, t('common:core.chat.error.data_error')), title: t(getErrText(error, t('common:core.chat.error.data_error'))),
status: 'error' status: 'error'
}); });
} }

View File

@ -301,6 +301,8 @@
"pro_modal_subtitle": "Join the business edition now to unlock more premium features", "pro_modal_subtitle": "Join the business edition now to unlock more premium features",
"pro_modal_title": "Business Edition Exclusive!", "pro_modal_title": "Business Edition Exclusive!",
"pro_modal_unlock_button": "Unlock Now", "pro_modal_unlock_button": "Unlock Now",
"publish.chat_desc": "After logging into the portal, users can talk directly to the application",
"publish.playground_link": "Redirect Link",
"publish_channel": "Publish", "publish_channel": "Publish",
"publish_success": "Publish Successful", "publish_success": "Publish Successful",
"question_guide_tip": "After the conversation, 3 guiding questions will be generated for you.", "question_guide_tip": "After the conversation, 3 guiding questions will be generated for you.",

View File

@ -234,7 +234,7 @@
"core.ai.model.Vector Model": "Index model", "core.ai.model.Vector Model": "Index model",
"core.ai.model.doc_index_and_dialog": "Document Index & Dialog Index", "core.ai.model.doc_index_and_dialog": "Document Index & Dialog Index",
"core.app.Api request": "API Request", "core.app.Api request": "API Request",
"core.app.Api request desc": "Integrate into existing systems through API, or WeChat Work, Feishu, etc.", "core.app.Api request desc": "Connect to existing systems via API",
"core.app.App intro": "App Introduction", "core.app.App intro": "App Introduction",
"core.app.Auto execute": "Auto execute", "core.app.Auto execute": "Auto execute",
"core.app.Chat Variable": "Chat Variable", "core.app.Chat Variable": "Chat Variable",
@ -264,7 +264,7 @@
"core.app.Set a name for your app": "Set a Name for Your App", "core.app.Set a name for your app": "Set a Name for Your App",
"core.app.Setting ai property": "Click to Configure AI Model Properties", "core.app.Setting ai property": "Click to Configure AI Model Properties",
"core.app.Share link": "Login-Free Window", "core.app.Share link": "Login-Free Window",
"core.app.Share link desc": "Share the link with other users, they can use it directly without logging in", "core.app.Share link desc": "Create shareable links and support login-free use",
"core.app.Share link desc detail": "You can directly share this model with other users for conversation, they can use it directly without logging in. Note, this feature will consume your account balance, please keep the link safe!", "core.app.Share link desc detail": "You can directly share this model with other users for conversation, they can use it directly without logging in. Note, this feature will consume your account balance, please keep the link safe!",
"core.app.TTS": "Voice Playback", "core.app.TTS": "Voice Playback",
"core.app.TTS Tip": "After enabling, you can use the voice playback function after each conversation. Using this feature may incur additional costs.", "core.app.TTS Tip": "After enabling, you can use the voice playback function after each conversation. Using this feature may incur additional costs.",
@ -315,6 +315,7 @@
"core.app.share.Amount limit tip": "Up to 10 groups", "core.app.share.Amount limit tip": "Up to 10 groups",
"core.app.share.Create link": "Create New Link", "core.app.share.Create link": "Create New Link",
"core.app.share.Create link tip": "Creation successful. The share address has been copied and can be shared directly.", "core.app.share.Create link tip": "Creation successful. The share address has been copied and can be shared directly.",
"core.app.share.Download source": "Download/open source original text",
"core.app.share.Ip limit title": "IP Rate Limit (people/minute)", "core.app.share.Ip limit title": "IP Rate Limit (people/minute)",
"core.app.share.Is response quote": "Return Quote", "core.app.share.Is response quote": "Return Quote",
"core.app.share.Not share link": "No Share Link Created", "core.app.share.Not share link": "No Share Link Created",
@ -1102,9 +1103,11 @@
"support.outlink.Max usage points tip": "The maximum number of points allowed for this link. It cannot be used after exceeding the limit. -1 means unlimited.", "support.outlink.Max usage points tip": "The maximum number of points allowed for this link. It cannot be used after exceeding the limit. -1 means unlimited.",
"support.outlink.Usage points": "Points Consumption", "support.outlink.Usage points": "Points Consumption",
"support.outlink.share.Chat_quote_reader": "Full text reader", "support.outlink.share.Chat_quote_reader": "Full text reader",
"support.outlink.share.Download source tips": "Download the original file of the knowledge base, or jump to the source website",
"support.outlink.share.Full_text tips": "Allows reading of the complete dataset from which the referenced fragment is derived", "support.outlink.share.Full_text tips": "Allows reading of the complete dataset from which the referenced fragment is derived",
"support.outlink.share.Response Quote": "Return Quote", "support.outlink.share.Response Quote": "View quoted snippets",
"support.outlink.share.Response Quote tips": "Return quoted content in the share link, but do not allow users to download the original document", "support.outlink.share.Response Quote tips": "Return quoted content in the share link, but do not allow users to download the original document",
"support.outlink.share.Show full text tips": "View the complete file to which the quoted content belongs. You cannot view the original file or jump to the source website.",
"support.outlink.share.running_node": "Running node", "support.outlink.share.running_node": "Running node",
"support.outlink.share.show_complete_quote": "View original source", "support.outlink.share.show_complete_quote": "View original source",
"support.outlink.share.show_complete_quote_tips": "View and download the complete citation document, or jump to the citation website", "support.outlink.share.show_complete_quote_tips": "View and download the complete citation document, or jump to the citation website",

View File

@ -313,6 +313,8 @@
"pro_modal_subtitle": "即刻加入商业版,解锁更多高级功能", "pro_modal_subtitle": "即刻加入商业版,解锁更多高级功能",
"pro_modal_title": "商业版专享!", "pro_modal_title": "商业版专享!",
"pro_modal_unlock_button": "去解锁", "pro_modal_unlock_button": "去解锁",
"publish.chat_desc": "用户登录门户后可直接与应用对话",
"publish.playground_link": "跳转链接",
"publish_channel": "发布渠道", "publish_channel": "发布渠道",
"publish_channel.wecom.empty": "发布到企业微信机器人,请先 <a>绑定自定义域名</a>,并且通过域名校验。", "publish_channel.wecom.empty": "发布到企业微信机器人,请先 <a>绑定自定义域名</a>,并且通过域名校验。",
"publish_success": "发布成功", "publish_success": "发布成功",

View File

@ -237,7 +237,7 @@
"core.ai.model.Vector Model": "索引模型", "core.ai.model.Vector Model": "索引模型",
"core.ai.model.doc_index_and_dialog": "文档索引 & 对话索引", "core.ai.model.doc_index_and_dialog": "文档索引 & 对话索引",
"core.app.Api request": "API 访问", "core.app.Api request": "API 访问",
"core.app.Api request desc": "通过 API 接入已有系统中,或企微、飞书等", "core.app.Api request desc": "通过 API 接入已有系统",
"core.app.App intro": "应用介绍", "core.app.App intro": "应用介绍",
"core.app.Auto execute": "自动执行", "core.app.Auto execute": "自动执行",
"core.app.Chat Variable": "对话框变量", "core.app.Chat Variable": "对话框变量",
@ -267,7 +267,7 @@
"core.app.Set a name for your app": "给应用设置一个名称", "core.app.Set a name for your app": "给应用设置一个名称",
"core.app.Setting ai property": "点击配置 AI 模型相关属性", "core.app.Setting ai property": "点击配置 AI 模型相关属性",
"core.app.Share link": "免登录窗口", "core.app.Share link": "免登录窗口",
"core.app.Share link desc": "分享链接给其他用户,无需登录即可直接进行使用", "core.app.Share link desc": "创建可分享的链接,支持免登录使用",
"core.app.Share link desc detail": "可以直接分享该模型给其他用户去进行对话,对方无需登录即可直接进行对话。注意,这个功能会消耗你账号的余额,请保管好链接!", "core.app.Share link desc detail": "可以直接分享该模型给其他用户去进行对话,对方无需登录即可直接进行对话。注意,这个功能会消耗你账号的余额,请保管好链接!",
"core.app.TTS": "语音播放", "core.app.TTS": "语音播放",
"core.app.TTS Tip": "开启后,每次对话后可使用语音播放功能。使用该功能可能产生额外费用。", "core.app.TTS Tip": "开启后,每次对话后可使用语音播放功能。使用该功能可能产生额外费用。",
@ -318,10 +318,12 @@
"core.app.share.Amount limit tip": "最多创建 10 组", "core.app.share.Amount limit tip": "最多创建 10 组",
"core.app.share.Create link": "创建新链接", "core.app.share.Create link": "创建新链接",
"core.app.share.Create link tip": "创建成功。已复制分享地址,可直接分享使用", "core.app.share.Create link tip": "创建成功。已复制分享地址,可直接分享使用",
"core.app.share.Download source": "下载/打开来源原文",
"core.app.share.Ip limit title": "IP 限流(人/分钟)", "core.app.share.Ip limit title": "IP 限流(人/分钟)",
"core.app.share.Is response quote": "返回引用", "core.app.share.Is response quote": "返回引用",
"core.app.share.Not share link": "没有创建分享链接", "core.app.share.Not share link": "没有创建分享链接",
"core.app.share.Role check": "身份校验", "core.app.share.Role check": "身份校验",
"core.app.share.Show full text": "查看引用全文",
"core.app.switch_to_template_market": "跳转模板市场", "core.app.switch_to_template_market": "跳转模板市场",
"core.app.tip.Add a intro to app": "快来给应用一个介绍~", "core.app.tip.Add a intro to app": "快来给应用一个介绍~",
"core.app.tip.chatNodeSystemPromptTip": "在此输入提示词", "core.app.tip.chatNodeSystemPromptTip": "在此输入提示词",
@ -1110,9 +1112,11 @@
"support.outlink.Max usage points tip": "该链接最多允许使用多少积分,超出后将无法使用。-1 代表无限制。", "support.outlink.Max usage points tip": "该链接最多允许使用多少积分,超出后将无法使用。-1 代表无限制。",
"support.outlink.Usage points": "积分消耗", "support.outlink.Usage points": "积分消耗",
"support.outlink.share.Chat_quote_reader": "全文阅读器", "support.outlink.share.Chat_quote_reader": "全文阅读器",
"support.outlink.share.Download source tips": "下载知识库原文件,或跳转来源网站",
"support.outlink.share.Full_text tips": "允许阅读该引用片段来源的完整数据集", "support.outlink.share.Full_text tips": "允许阅读该引用片段来源的完整数据集",
"support.outlink.share.Response Quote": "引用内容", "support.outlink.share.Response Quote": "查看引用片段",
"support.outlink.share.Response Quote tips": "查看知识库搜索的引用内容,不可查看完整引用文档或跳转引用网站", "support.outlink.share.Response Quote tips": "查看知识库搜索的引用内容,不可查看完整引用文档或跳转引用网站",
"support.outlink.share.Show full text tips": "查看引用内容所属的完整文件,不可查看原文件或跳转来来源网站",
"support.outlink.share.running_node": "运行节点", "support.outlink.share.running_node": "运行节点",
"support.outlink.share.show_complete_quote": "查看来源原文", "support.outlink.share.show_complete_quote": "查看来源原文",
"support.outlink.share.show_complete_quote_tips": "查看及下载完整引用文档,或跳转引用网站", "support.outlink.share.show_complete_quote_tips": "查看及下载完整引用文档,或跳转引用网站",

View File

@ -299,6 +299,8 @@
"pro_modal_subtitle": "即刻加入商業版,解鎖更多高級功能", "pro_modal_subtitle": "即刻加入商業版,解鎖更多高級功能",
"pro_modal_title": "商業版專享!", "pro_modal_title": "商業版專享!",
"pro_modal_unlock_button": "去解鎖", "pro_modal_unlock_button": "去解鎖",
"publish.chat_desc": "用戶登錄門戶後可直接與應用對話",
"publish.playground_link": "跳轉鏈接",
"publish_channel": "發布通道", "publish_channel": "發布通道",
"publish_success": "發布成功", "publish_success": "發布成功",
"question_guide_tip": "對話結束後,會為你產生 3 個引導性問題。", "question_guide_tip": "對話結束後,會為你產生 3 個引導性問題。",

View File

@ -234,7 +234,7 @@
"core.ai.model.Vector Model": "索引模型", "core.ai.model.Vector Model": "索引模型",
"core.ai.model.doc_index_and_dialog": "文件索引與對話索引", "core.ai.model.doc_index_and_dialog": "文件索引與對話索引",
"core.app.Api request": "API 存取", "core.app.Api request": "API 存取",
"core.app.Api request desc": "透過 API 整合到現有系統中,或整合到企業微信、飛書等", "core.app.Api request desc": "通過 API 接入已有系統",
"core.app.App intro": "應用程式介紹", "core.app.App intro": "應用程式介紹",
"core.app.Auto execute": "自動執行", "core.app.Auto execute": "自動執行",
"core.app.Chat Variable": "對話變數", "core.app.Chat Variable": "對話變數",
@ -264,7 +264,7 @@
"core.app.Set a name for your app": "為您的應用程式命名", "core.app.Set a name for your app": "為您的應用程式命名",
"core.app.Setting ai property": "點選設定 AI 模型相關屬性", "core.app.Setting ai property": "點選設定 AI 模型相關屬性",
"core.app.Share link": "免登入視窗", "core.app.Share link": "免登入視窗",
"core.app.Share link desc": "分享連結給其他使用者,無需登入即可直接使用", "core.app.Share link desc": "創建可分享的鏈接,支持免登錄使用",
"core.app.Share link desc detail": "您可以直接分享此模型給其他使用者進行對話,對方無需登入即可直接使用。請注意,此功能會消耗您帳戶的餘額,請妥善保管連結!", "core.app.Share link desc detail": "您可以直接分享此模型給其他使用者進行對話,對方無需登入即可直接使用。請注意,此功能會消耗您帳戶的餘額,請妥善保管連結!",
"core.app.TTS": "語音播放", "core.app.TTS": "語音播放",
"core.app.TTS Tip": "開啟後,每次對話後可使用語音播放功能。使用此功能可能會產生額外費用。", "core.app.TTS Tip": "開啟後,每次對話後可使用語音播放功能。使用此功能可能會產生額外費用。",
@ -315,6 +315,7 @@
"core.app.share.Amount limit tip": "最多 10 組", "core.app.share.Amount limit tip": "最多 10 組",
"core.app.share.Create link": "建立新連結", "core.app.share.Create link": "建立新連結",
"core.app.share.Create link tip": "建立成功。已複製分享網址,可直接分享使用", "core.app.share.Create link tip": "建立成功。已複製分享網址,可直接分享使用",
"core.app.share.Download source": "下載/打開來源原文",
"core.app.share.Ip limit title": "IP 限流(人/分鐘)", "core.app.share.Ip limit title": "IP 限流(人/分鐘)",
"core.app.share.Is response quote": "返回引用", "core.app.share.Is response quote": "返回引用",
"core.app.share.Not share link": "尚未建立分享連結", "core.app.share.Not share link": "尚未建立分享連結",
@ -1099,9 +1100,11 @@
"support.outlink.Max usage points tip": "此連結最多允許使用多少點數,超出後將無法使用。-1 代表無限制。", "support.outlink.Max usage points tip": "此連結最多允許使用多少點數,超出後將無法使用。-1 代表無限制。",
"support.outlink.Usage points": "點數消耗", "support.outlink.Usage points": "點數消耗",
"support.outlink.share.Chat_quote_reader": "全文閱讀器", "support.outlink.share.Chat_quote_reader": "全文閱讀器",
"support.outlink.share.Download source tips": "下載知識庫原文件,或跳轉來源網站",
"support.outlink.share.Full_text tips": "允許閱讀該引用片段來源的完整資料集", "support.outlink.share.Full_text tips": "允許閱讀該引用片段來源的完整資料集",
"support.outlink.share.Response Quote": "回傳引用", "support.outlink.share.Response Quote": "查看引用片段",
"support.outlink.share.Response Quote tips": "在分享連結中回傳引用內容,但不允許使用者下載原始文件", "support.outlink.share.Response Quote tips": "在分享連結中回傳引用內容,但不允許使用者下載原始文件",
"support.outlink.share.Show full text tips": "查看引用內容所屬的完整文件,不可查看原文件或跳轉來來源網站",
"support.outlink.share.running_node": "執行節點", "support.outlink.share.running_node": "執行節點",
"support.outlink.share.show_complete_quote": "檢視原始內容", "support.outlink.share.show_complete_quote": "檢視原始內容",
"support.outlink.share.show_complete_quote_tips": "檢視及下載完整引用文件,或跳轉至引用網站", "support.outlink.share.show_complete_quote_tips": "檢視及下載完整引用文件,或跳轉至引用網站",

View File

@ -163,12 +163,12 @@ const ChatItem = (props: Props) => {
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);
const showNodeStatus = useContextSelector(ChatItemContext, (v) => v.showNodeStatus); const showRunningStatus = useContextSelector(ChatItemContext, (v) => v.showRunningStatus);
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);
const outLinkAuthData = useContextSelector(WorkflowRuntimeContext, (v) => v.outLinkAuthData); const outLinkAuthData = useContextSelector(WorkflowRuntimeContext, (v) => v.outLinkAuthData);
const isShowReadRawSource = useContextSelector(ChatItemContext, (v) => v.isShowReadRawSource); const isShowFullText = useContextSelector(ChatItemContext, (v) => v.isShowFullText);
const { totalQuoteList: quoteList = [] } = useMemo( const { totalQuoteList: quoteList = [] } = useMemo(
() => addStatisticalDataToHistoryItem(chat), () => addStatisticalDataToHistoryItem(chat),
@ -258,7 +258,7 @@ const ChatItem = (props: Props) => {
setCiteModalData({ setCiteModalData({
rawSearch: quoteList, rawSearch: quoteList,
metadata: metadata:
item?.collectionId && isShowReadRawSource item?.collectionId && isShowFullText
? { ? {
appId: appId, appId: appId,
chatId: chatId, chatId: chatId,
@ -322,7 +322,7 @@ const ChatItem = (props: Props) => {
<ChatAvatar src={avatar} type={type} /> <ChatAvatar src={avatar} type={type} />
{/* Workflow status */} {/* Workflow status */}
{!!chatStatusMap && statusBoxData && isLastChild && showNodeStatus && ( {!!chatStatusMap && statusBoxData && isLastChild && showRunningStatus && (
<Flex <Flex
alignItems={'center'} alignItems={'center'}
px={3} px={3}

View File

@ -26,7 +26,7 @@ const QuoteList = React.memo(function QuoteList({
chatId: v.chatId, chatId: v.chatId,
...(v.outLinkAuthData || {}) ...(v.outLinkAuthData || {})
})); }));
const showRawSource = useContextSelector(ChatItemContext, (v) => v.isShowReadRawSource); const canDownloadSource = useContextSelector(ChatItemContext, (v) => v.canDownloadSource);
const showRouteToDatasetDetail = useContextSelector( const showRouteToDatasetDetail = useContextSelector(
ChatItemContext, ChatItemContext,
(v) => v.showRouteToDatasetDetail (v) => v.showRouteToDatasetDetail
@ -87,7 +87,7 @@ const QuoteList = React.memo(function QuoteList({
> >
<QuoteItem <QuoteItem
quoteItem={item} quoteItem={item}
canViewSource={showRawSource} canDownloadSource={canDownloadSource}
canEditData={showRouteToDatasetDetail} canEditData={showRouteToDatasetDetail}
canEditDataset={showRouteToDatasetDetail} canEditDataset={showRouteToDatasetDetail}
{...RawSourceBoxProps} {...RawSourceBoxProps}

View File

@ -14,6 +14,7 @@ import { addStatisticalDataToHistoryItem } from '@/global/core/chat/utils';
import { useSize } from 'ahooks'; import { useSize } from 'ahooks';
import { useContextSelector } from 'use-context-selector'; import { useContextSelector } from 'use-context-selector';
import { ChatBoxContext } from '../Provider'; import { ChatBoxContext } from '../Provider';
import { ChatItemContext } from '@/web/core/chat/context/chatItemContext';
export type CitationRenderItem = { export type CitationRenderItem = {
type: 'dataset' | 'link'; type: 'dataset' | 'link';
@ -48,12 +49,22 @@ const ResponseTags = ({
const chatTime = historyItem.time || new Date(); const chatTime = historyItem.time || new Date();
const durationSeconds = historyItem.durationSeconds || 0; const durationSeconds = historyItem.durationSeconds || 0;
const isShowCite = useContextSelector(ChatItemContext, (v) => v.isShowCite);
const { const {
totalQuoteList: quoteList = [], totalQuoteList: quoteList = [],
llmModuleAccount = 0, llmModuleAccount = 0,
historyPreviewLength = 0, historyPreviewLength = 0,
toolCiteLinks = [] toolCiteLinks = []
} = useMemo(() => addStatisticalDataToHistoryItem(historyItem), [historyItem]); } = useMemo(() => {
return {
...addStatisticalDataToHistoryItem(historyItem),
...(isShowCite
? {
totalQuoteList: []
}
: {})
};
}, [historyItem, isShowCite]);
const [quoteFolded, setQuoteFolded] = useState<boolean>(true); const [quoteFolded, setQuoteFolded] = useState<boolean>(true);
@ -78,6 +89,7 @@ const ResponseTags = ({
: true; : true;
const citationRenderList: CitationRenderItem[] = useMemo(() => { const citationRenderList: CitationRenderItem[] = useMemo(() => {
if (!isShowCite) return [];
// Dataset citations // Dataset citations
const datasetItems = Object.values( const datasetItems = Object.values(
quoteList.reduce((acc: Record<string, SearchDataResponseItemType[]>, cur) => { quoteList.reduce((acc: Record<string, SearchDataResponseItemType[]>, cur) => {
@ -116,7 +128,7 @@ const ResponseTags = ({
})); }));
return [...datasetItems, ...linkItems]; return [...datasetItems, ...linkItems];
}, [quoteList, toolCiteLinks, onOpenCiteModal]); }, [quoteList, toolCiteLinks, onOpenCiteModal, isShowCite]);
const notEmptyTags = notSharePage || quoteList.length > 0 || (isPc && durationSeconds > 0); const notEmptyTags = notSharePage || quoteList.length > 0 || (isPc && durationSeconds > 0);

View File

@ -31,9 +31,13 @@ import { eventBus, EventNameEnum } from '@/web/common/utils/eventbus';
import { SelectOptionsComponent, FormInputComponent } from './Interactive/InteractiveComponents'; import { SelectOptionsComponent, FormInputComponent } from './Interactive/InteractiveComponents';
import { extractDeepestInteractive } from '@fastgpt/global/core/workflow/runtime/utils'; import { extractDeepestInteractive } from '@fastgpt/global/core/workflow/runtime/utils';
import { useContextSelector } from 'use-context-selector'; import { useContextSelector } from 'use-context-selector';
import { type OnOpenCiteModalProps } from '@/web/core/chat/context/chatItemContext'; import {
type OnOpenCiteModalProps,
ChatItemContext
} from '@/web/core/chat/context/chatItemContext';
import { WorkflowRuntimeContext } from '../ChatContainer/context/workflowRuntimeContext'; import { WorkflowRuntimeContext } from '../ChatContainer/context/workflowRuntimeContext';
import { useCreation } from 'ahooks'; import { useCreation } from 'ahooks';
import { removeDatasetCiteText } from '@fastgpt/global/core/ai/llm/utils';
const accordionButtonStyle = { const accordionButtonStyle = {
w: 'auto', w: 'auto',
@ -102,13 +106,16 @@ const RenderText = React.memo(function RenderText({
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);
const outLinkAuthData = useContextSelector(WorkflowRuntimeContext, (v) => v.outLinkAuthData); const outLinkAuthData = useContextSelector(WorkflowRuntimeContext, (v) => v.outLinkAuthData);
const isShowCite = useContextSelector(ChatItemContext, (v) => v.isShowCite);
const source = useMemo(() => { const source = useMemo(() => {
if (!text) return ''; if (!text) return '';
// Remove quote references if not showing response detail if (isShowCite) {
return text; return text;
}, [text]); }
return removeDatasetCiteText(text, isShowCite);
}, [text, isShowCite]);
const chatAuthData = useCreation(() => { const chatAuthData = useCreation(() => {
return { appId, chatId, chatItemDataId, ...outLinkAuthData }; return { appId, chatId, chatItemDataId, ...outLinkAuthData };
@ -329,6 +336,8 @@ const AIResponseBox = ({
isChatting: boolean; isChatting: boolean;
onOpenCiteModal?: (e?: OnOpenCiteModalProps) => void; onOpenCiteModal?: (e?: OnOpenCiteModalProps) => void;
}) => { }) => {
const showRunningStatus = useContextSelector(ChatItemContext, (v) => v.showRunningStatus);
if (value.type === ChatItemValueTypeEnum.text && value.text) { if (value.type === ChatItemValueTypeEnum.text && value.text) {
return ( return (
<RenderText <RenderText
@ -348,7 +357,7 @@ const AIResponseBox = ({
/> />
); );
} }
if (value.type === ChatItemValueTypeEnum.tool && value.tools) { if (value.type === ChatItemValueTypeEnum.tool && value.tools && showRunningStatus) {
return <RenderTool showAnimation={isChatting} tools={value.tools} />; return <RenderTool showAnimation={isChatting} tools={value.tools} />;
} }
if (value.type === ChatItemValueTypeEnum.interactive && value.interactive) { if (value.type === ChatItemValueTypeEnum.interactive && value.interactive) {

View File

@ -91,13 +91,13 @@ export const formatScore = (score: ScoreItemType[]) => {
const QuoteItem = ({ const QuoteItem = ({
quoteItem, quoteItem,
canViewSource, canDownloadSource,
canEditData, canEditData,
canEditDataset, canEditDataset,
...RawSourceBoxProps ...RawSourceBoxProps
}: { }: {
quoteItem: SearchDataResponseItemType; quoteItem: SearchDataResponseItemType;
canViewSource?: boolean; canDownloadSource?: boolean;
canEditData?: boolean; canEditData?: boolean;
canEditDataset?: boolean; canEditDataset?: boolean;
} & Omit<readCollectionSourceBody, 'collectionId'>) => { } & Omit<readCollectionSourceBody, 'collectionId'>) => {
@ -208,7 +208,7 @@ const QuoteItem = ({
collectionId={quoteItem.collectionId} collectionId={quoteItem.collectionId}
sourceName={quoteItem.sourceName} sourceName={quoteItem.sourceName}
sourceId={quoteItem.sourceId} sourceId={quoteItem.sourceId}
canView={canViewSource} canView={canDownloadSource}
{...RawSourceBoxProps} {...RawSourceBoxProps}
/> />
<Box flex={1} /> <Box flex={1} />

View File

@ -205,10 +205,10 @@ const Render = ({
return ( return (
<ChatItemContextProvider <ChatItemContextProvider
showRouteToDatasetDetail={true} showRouteToDatasetDetail={true}
isShowReadRawSource={true} canDownloadSource={true}
isResponseDetail={true} isShowCite={true}
// isShowFullText={true} isShowFullText={true}
showNodeStatus showRunningStatus={true}
> >
<ChatRecordContextProvider params={chatRecordProviderParams}> <ChatRecordContextProvider params={chatRecordProviderParams}>
<ChatTest <ChatTest

View File

@ -264,10 +264,10 @@ const Render = (props: Props) => {
return ( return (
<ChatItemContextProvider <ChatItemContextProvider
showRouteToDatasetDetail={true} showRouteToDatasetDetail={true}
isShowReadRawSource={true} canDownloadSource={true}
isResponseDetail={true} isShowCite={true}
// isShowFullText={true} isShowFullText={true}
showNodeStatus showRunningStatus={true}
> >
<ChatRecordContextProvider params={params} feedbackRecordId={feedbackRecordId}> <ChatRecordContextProvider params={params} feedbackRecordId={feedbackRecordId}>
<DetailLogsModal <DetailLogsModal

View File

@ -182,10 +182,10 @@ const Render = ({
return ( return (
<ChatItemContextProvider <ChatItemContextProvider
showRouteToDatasetDetail={true} showRouteToDatasetDetail={true}
isShowReadRawSource={true} canDownloadSource={true}
isResponseDetail={true} isShowCite={true}
// isShowFullText={true} isShowFullText={true}
showNodeStatus showRunningStatus={true}
> >
<ChatRecordContextProvider params={chatRecordProviderParams}> <ChatRecordContextProvider params={chatRecordProviderParams}>
<ChatTest currentTool={currentTool} url={url} headerSecret={headerSecret} /> <ChatTest currentTool={currentTool} url={url} headerSecret={headerSecret} />

View File

@ -186,7 +186,7 @@ const DingTalk = ({ appId }: { appId: string }) => {
name: item.name, name: item.name,
limit: item.limit, limit: item.limit,
app: item.app, app: item.app,
responseDetail: item.responseDetail, showCite: item.showCite,
defaultResponse: item.defaultResponse, defaultResponse: item.defaultResponse,
immediateResponse: item.immediateResponse immediateResponse: item.immediateResponse
}); });

View File

@ -185,7 +185,7 @@ const FeiShu = ({ appId }: { appId: string }) => {
name: item.name, name: item.name,
limit: item.limit, limit: item.limit,
app: item.app, app: item.app,
responseDetail: item.responseDetail, showCite: item.showCite,
defaultResponse: item.defaultResponse, defaultResponse: item.defaultResponse,
immediateResponse: item.immediateResponse immediateResponse: item.immediateResponse
}); });

View File

@ -140,7 +140,7 @@ const Share = ({ appId }: { appId: string; type: PublishChannelEnum }) => {
}` }`
: ''} : ''}
</Td> </Td>
<Td>{item.responseDetail ? '✔' : '✖'}</Td> <Td>{item.showCite ? '✔' : '✖'}</Td>
{feConfigs?.isPlus && ( {feConfigs?.isPlus && (
<> <>
<Td>{item?.limit?.QPM || '-'}</Td> <Td>{item?.limit?.QPM || '-'}</Td>
@ -182,10 +182,10 @@ const Share = ({ appId }: { appId: string; type: PublishChannelEnum }) => {
setEditLinkData({ setEditLinkData({
_id: item._id, _id: item._id,
name: item.name, name: item.name,
responseDetail: item.responseDetail ?? false, showCite: item.showCite,
showRawSource: item.showRawSource ?? false, canDownloadSource: item.canDownloadSource,
// showFullText: item.showFullText ?? false, showFullText: item.showFullText,
showNodeStatus: item.showNodeStatus ?? false, showRunningStatus: item.showRunningStatus,
limit: item.limit limit: item.limit
}) })
}, },
@ -281,9 +281,9 @@ function EditLinkModal({
defaultValues: defaultData defaultValues: defaultData
}); });
const responseDetail = watch('responseDetail'); const showCite = watch('showCite');
// const showFullText = watch('showFullText'); const showFullText = watch('showFullText');
const showRawSource = watch('showRawSource'); const canDownloadSource = watch('canDownloadSource');
const isEdit = useMemo(() => !!defaultData._id, [defaultData]); const isEdit = useMemo(() => !!defaultData._id, [defaultData]);
@ -413,7 +413,7 @@ function EditLinkModal({
</Box> </Box>
<Flex alignItems={'center'} mt={4} justify={'space-between'} height={'36px'}> <Flex alignItems={'center'} mt={4} justify={'space-between'} height={'36px'}>
<FormLabel>{t('publish:show_node')}</FormLabel> <FormLabel>{t('publish:show_node')}</FormLabel>
<Switch {...register('showNodeStatus')} /> <Switch {...register('showRunningStatus')} />
</Flex> </Flex>
<Flex alignItems={'center'} mt={4} justify={'space-between'} height={'36px'}> <Flex alignItems={'center'} mt={4} justify={'space-between'} height={'36px'}>
<Flex alignItems={'center'}> <Flex alignItems={'center'}>
@ -424,56 +424,56 @@ function EditLinkModal({
></QuestionTip> ></QuestionTip>
</Flex> </Flex>
<Switch <Switch
{...register('responseDetail', { {...register('showCite', {
onChange(e) { onChange(e) {
if (!e.target.checked) { if (!e.target.checked) {
// setValue('showFullText', false); setValue('showFullText', false);
setValue('showRawSource', false); setValue('canDownloadSource', false);
} }
} }
})} })}
isChecked={responseDetail} isChecked={showCite}
/> />
</Flex> </Flex>
{/* <Flex alignItems={'center'} mt={4} justify={'space-between'} height={'36px'}> <Flex alignItems={'center'} mt={4} justify={'space-between'} height={'36px'}>
<Flex alignItems={'center'}> <Flex alignItems={'center'}>
<FormLabel>{t('common:support.outlink.share.Chat_quote_reader')}</FormLabel> <FormLabel>{t('common:core.app.share.Show full text')}</FormLabel>
<QuestionTip <QuestionTip
ml={1} ml={1}
label={t('common:support.outlink.share.Full_text tips')} label={t('common:support.outlink.share.Show full text tips')}
></QuestionTip> ></QuestionTip>
</Flex> </Flex>
<Switch <Switch
{...register('showFullText', { {...register('showFullText', {
onChange(e) { onChange(e) {
if (e.target.checked) { if (!e.target.checked) {
setValue('responseDetail', true); setValue('canDownloadSource', false);
} else { } else {
setValue('showRawSource', false); setValue('showCite', true);
} }
} }
})} })}
isChecked={showFullText} isChecked={showFullText}
/> />
</Flex> */} </Flex>
<Flex alignItems={'center'} mt={4} justify={'space-between'} height={'36px'}> <Flex alignItems={'center'} mt={4} justify={'space-between'} height={'36px'}>
<Flex alignItems={'center'}> <Flex alignItems={'center'}>
<FormLabel>{t('common:support.outlink.share.show_complete_quote')}</FormLabel> <FormLabel>{t('common:core.app.share.Download source')}</FormLabel>
<QuestionTip <QuestionTip
ml={1} ml={1}
label={t('common:support.outlink.share.show_complete_quote_tips')} label={t('common:support.outlink.share.Download source tips')}
></QuestionTip> ></QuestionTip>
</Flex> </Flex>
<Switch <Switch
{...register('showRawSource', { {...register('canDownloadSource', {
onChange(e) { onChange(e) {
if (e.target.checked) { if (e.target.checked) {
setValue('responseDetail', true); setValue('showFullText', true);
// setValue('showFullText', true); setValue('showCite', true);
} }
} }
})} })}
isChecked={showRawSource} isChecked={canDownloadSource}
/> />
</Flex> </Flex>
</Box> </Box>

View File

@ -188,7 +188,7 @@ const OffiAccount = ({ appId }: { appId: string }) => {
name: item.name, name: item.name,
limit: item.limit, limit: item.limit,
app: item.app, app: item.app,
responseDetail: item.responseDetail, showCite: item.showCite,
defaultResponse: item.defaultResponse, defaultResponse: item.defaultResponse,
immediateResponse: item.immediateResponse immediateResponse: item.immediateResponse
}); });

View File

@ -0,0 +1,186 @@
import React, { useMemo } from 'react';
import { Box, Flex, Grid, Switch } from '@chakra-ui/react';
import { useTranslation } from 'next-i18next';
import { useForm } from 'react-hook-form';
import FormLabel from '@fastgpt/web/components/common/MyBox/FormLabel';
import QuestionTip from '@fastgpt/web/components/common/MyTooltip/QuestionTip';
import { useRequest2 } from '@fastgpt/web/hooks/useRequest';
import {
getPlaygroundVisibilityConfig,
updatePlaygroundVisibilityConfig
} from '@/web/support/outLink/api';
import type { PlaygroundVisibilityConfigType } from '@fastgpt/global/support/outLink/type';
import MyIcon from '@fastgpt/web/components/common/Icon';
import { useCopyData } from '@fastgpt/web/hooks/useCopyData';
import { ChatSidebarPaneEnum } from '@/pageComponents/chat/constants';
const defaultPlaygroundVisibilityForm: PlaygroundVisibilityConfigType = {
showRunningStatus: true,
showCite: true,
showFullText: true,
canDownloadSource: true
};
const PlaygroundVisibilityConfig = ({ appId }: { appId: string }) => {
const { t } = useTranslation();
const { copyData } = useCopyData();
const { register, watch, setValue, reset } = useForm({
defaultValues: defaultPlaygroundVisibilityForm
});
const showCite = watch('showCite');
const showFullText = watch('showFullText');
const canDownloadSource = watch('canDownloadSource');
const playgroundLink = useMemo(() => {
if (typeof window !== 'undefined') {
return `${window.location.origin}/chat?appId=${appId}&pane=${ChatSidebarPaneEnum.RECENTLY_USED_APPS}`;
}
return '';
}, [appId]);
useRequest2(() => getPlaygroundVisibilityConfig({ appId }), {
onSuccess: (data) => {
reset({
showRunningStatus: data.showRunningStatus,
showCite: data.showCite,
showFullText: data.showFullText,
canDownloadSource: data.canDownloadSource
});
},
manual: false
});
const { runAsync: saveConfig } = useRequest2(
async (data: PlaygroundVisibilityConfigType) => {
return await updatePlaygroundVisibilityConfig({
appId,
...data
});
},
{
successToast: t('common:save_success')
}
);
const autoSave = async () => {
const values = watch();
await saveConfig(values);
};
return (
<Flex flexDirection="column" h="100%">
<Box fontSize={'sm'} fontWeight={'medium'} color={'myGray.900'} mb={3}>
{t('app:publish.playground_link')}
</Box>
<Box borderRadius={'md'} bg={'myGray.100'} overflow={'hidden'} fontSize={'sm'} mb={6}>
<Flex
p={3}
bg={'myWhite.500'}
border="base"
borderTopLeftRadius={'md'}
borderTopRightRadius={'md'}
alignItems={'center'}
>
<Box flex={1} fontSize={'xs'} color={'myGray.600'}>
{t('common:core.app.outLink.Link block title')}
</Box>
<MyIcon
name={'copy'}
w={'16px'}
color={'myGray.600'}
cursor={'pointer'}
_hover={{ color: 'primary.500' }}
onClick={() => copyData(playgroundLink)}
/>
</Flex>
<Box whiteSpace={'nowrap'} p={3} overflowX={'auto'}>
{playgroundLink}
</Box>
</Box>
<Box fontSize={'sm'} fontWeight={'medium'} color={'myGray.900'}>
{t('publish:private_config')}
</Box>
<Grid templateColumns={'1fr 1fr'} gap={4} mt={4}>
<Flex alignItems={'center'}>
<FormLabel fontSize={'12px'} flex={'0 0 127px'}>
{t('publish:show_node')}
</FormLabel>
<Switch
{...register('showRunningStatus', {
onChange: autoSave
})}
/>
</Flex>
<Flex>
<Flex alignItems={'center'} flex={'0 0 158px'}>
<FormLabel fontSize={'12px'}>
{t('common:support.outlink.share.Response Quote')}
</FormLabel>
<QuestionTip ml={1} label={t('common:support.outlink.share.Response Quote tips')} />
</Flex>
<Switch
{...register('showCite', {
onChange(e) {
if (!e.target.checked) {
setValue('showFullText', false);
setValue('canDownloadSource', false);
}
autoSave();
}
})}
isChecked={showCite}
/>
</Flex>
</Grid>
<Grid templateColumns={'1fr 1fr'} gap={4} mt={4}>
<Flex>
<Flex alignItems={'center'} flex={'0 0 127px'}>
<FormLabel fontSize={'12px'}>{t('common:core.app.share.Show full text')}</FormLabel>
<QuestionTip ml={1} label={t('common:support.outlink.share.Show full text tips')} />
</Flex>
<Switch
{...register('showFullText', {
onChange(e) {
if (!e.target.checked) {
setValue('canDownloadSource', false);
} else {
setValue('showCite', true);
}
autoSave();
}
})}
isChecked={showFullText}
/>
</Flex>
<Flex>
<Flex alignItems={'center'} flex={'0 0 158px'}>
<FormLabel fontSize={'12px'} fontWeight={'medium'}>
{t('common:core.app.share.Download source')}
</FormLabel>
<QuestionTip ml={1} label={t('common:support.outlink.share.Download source tips')} />
</Flex>
<Switch
{...register('canDownloadSource', {
onChange(e) {
if (e.target.checked) {
setValue('showFullText', true);
setValue('showCite', true);
}
autoSave();
}
})}
isChecked={canDownloadSource}
/>
</Flex>
</Grid>
</Flex>
);
};
export default PlaygroundVisibilityConfig;

View File

@ -200,7 +200,7 @@ const Wecom = ({ appId }: { appId: string }) => {
name: item.name, name: item.name,
limit: item.limit, limit: item.limit,
app: item.app, app: item.app,
responseDetail: item.responseDetail, showCite: item.showCite,
defaultResponse: item.defaultResponse, defaultResponse: item.defaultResponse,
immediateResponse: item.immediateResponse immediateResponse: item.immediateResponse
}); });

View File

@ -19,6 +19,7 @@ const FeiShu = dynamic(() => import('./FeiShu'));
const DingTalk = dynamic(() => import('./DingTalk')); const DingTalk = dynamic(() => import('./DingTalk'));
const Wecom = dynamic(() => import('./Wecom')); const Wecom = dynamic(() => import('./Wecom'));
const OffiAccount = dynamic(() => import('./OffiAccount')); const OffiAccount = dynamic(() => import('./OffiAccount'));
const Playground = dynamic(() => import('./Playground'));
const OutLink = () => { const OutLink = () => {
const { t } = useTranslation(); const { t } = useTranslation();
@ -85,7 +86,14 @@ const OutLink = () => {
isProFn: true isProFn: true
} }
] ]
: []) : []),
{
icon: 'core/chat/sidebar/home',
title: t('common:navbar.Chat'),
desc: t('app:publish.chat_desc'),
value: PublishChannelEnum.playground,
isProFn: false
}
]); ]);
const [linkType, setLinkType] = useState<PublishChannelEnum>(PublishChannelEnum.share); const [linkType, setLinkType] = useState<PublishChannelEnum>(PublishChannelEnum.share);
@ -141,6 +149,7 @@ const OutLink = () => {
{linkType === PublishChannelEnum.dingtalk && <DingTalk appId={appId} />} {linkType === PublishChannelEnum.dingtalk && <DingTalk appId={appId} />}
{linkType === PublishChannelEnum.wecom && <Wecom appId={appId} />} {linkType === PublishChannelEnum.wecom && <Wecom appId={appId} />}
{linkType === PublishChannelEnum.officialAccount && <OffiAccount appId={appId} />} {linkType === PublishChannelEnum.officialAccount && <OffiAccount appId={appId} />}
{linkType === PublishChannelEnum.playground && <Playground appId={appId} />}
</Flex> </Flex>
</Box> </Box>
); );

View File

@ -118,10 +118,10 @@ const Render = ({ appForm, setRenderEdit }: Props) => {
return ( return (
<ChatItemContextProvider <ChatItemContextProvider
showRouteToDatasetDetail={true} showRouteToDatasetDetail={true}
isShowReadRawSource={true} canDownloadSource={true}
isResponseDetail={true} isShowCite={true}
// isShowFullText={true} isShowFullText={true}
showNodeStatus showRunningStatus={true}
> >
<ChatRecordContextProvider params={chatRecordProviderParams}> <ChatRecordContextProvider params={chatRecordProviderParams}>
<ChatTest appForm={appForm} setRenderEdit={setRenderEdit} /> <ChatTest appForm={appForm} setRenderEdit={setRenderEdit} />

View File

@ -206,10 +206,10 @@ const Render = (Props: Props) => {
return ( return (
<ChatItemContextProvider <ChatItemContextProvider
showRouteToDatasetDetail={true} showRouteToDatasetDetail={true}
isShowReadRawSource={true} canDownloadSource={true}
isResponseDetail={true} isShowCite={true}
// isShowFullText={true} isShowFullText={true}
showNodeStatus showRunningStatus={true}
> >
<ChatRecordContextProvider params={chatRecordProviderParams}> <ChatRecordContextProvider params={chatRecordProviderParams}>
<ChatTest {...Props} chatId={chatId} /> <ChatTest {...Props} chatId={chatId} />

View File

@ -18,7 +18,7 @@ import MyIcon from '@fastgpt/web/components/common/Icon';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { useForm } from 'react-hook-form'; import { useForm } from 'react-hook-form';
import { useContextSelector } from 'use-context-selector'; import { useContextSelector } from 'use-context-selector';
import { ChatSettingContext } from '@/web/core/chat/context/chatSettingContext'; import { ChatPageContext } from '@/web/core/chat/context/chatPageContext';
import { useMemo } from 'react'; import { useMemo } from 'react';
import Avatar from '@fastgpt/web/components/common/Avatar'; import Avatar from '@fastgpt/web/components/common/Avatar';
import { ChatSettingTabOptionEnum, ChatSidebarPaneEnum } from '@/pageComponents/chat/constants'; import { ChatSettingTabOptionEnum, ChatSidebarPaneEnum } from '@/pageComponents/chat/constants';
@ -41,11 +41,11 @@ const ChatFavouriteApp = () => {
const onOpenSlider = useContextSelector(ChatContext, (v) => v.onOpenSlider); const onOpenSlider = useContextSelector(ChatContext, (v) => v.onOpenSlider);
const handlePaneChange = useContextSelector(ChatSettingContext, (v) => v.handlePaneChange); const handlePaneChange = useContextSelector(ChatPageContext, (v) => v.handlePaneChange);
const wideLogoUrl = useContextSelector(ChatSettingContext, (v) => v.chatSettings?.wideLogoUrl); const wideLogoUrl = useContextSelector(ChatPageContext, (v) => v.chatSettings?.wideLogoUrl);
const homeTabTitle = useContextSelector(ChatSettingContext, (v) => v.chatSettings?.homeTabTitle); const homeTabTitle = useContextSelector(ChatPageContext, (v) => v.chatSettings?.homeTabTitle);
const tags = useContextSelector(ChatSettingContext, (v) => v.chatSettings?.favouriteTags || []); const tags = useContextSelector(ChatPageContext, (v) => v.chatSettings?.favouriteTags || []);
const tagCache = useMemo(() => { const tagCache = useMemo(() => {
return tags.reduce( return tags.reduce(
(acc, tag) => { (acc, tag) => {

View File

@ -14,7 +14,6 @@ import { AppFolderTypeList, AppTypeEnum } from '@fastgpt/global/core/app/constan
import { useSystem } from '@fastgpt/web/hooks/useSystem'; import { useSystem } from '@fastgpt/web/hooks/useSystem';
import LightRowTabs from '@fastgpt/web/components/common/Tabs/LightRowTabs'; import LightRowTabs from '@fastgpt/web/components/common/Tabs/LightRowTabs';
import { useRouter } from 'next/router'; import { useRouter } from 'next/router';
import { type AppListItemType } from '@fastgpt/global/core/app/type';
import { import {
type GetResourceFolderListProps, type GetResourceFolderListProps,
type GetResourceListItemResponse type GetResourceListItemResponse
@ -24,7 +23,7 @@ import SelectOneResource from '@/components/common/folder/SelectOneResource';
import { ChatItemContext } from '@/web/core/chat/context/chatItemContext'; import { ChatItemContext } from '@/web/core/chat/context/chatItemContext';
import VariablePopover from '@/components/core/chat/ChatContainer/components/VariablePopover'; import VariablePopover from '@/components/core/chat/ChatContainer/components/VariablePopover';
import { useCopyData } from '@fastgpt/web/hooks/useCopyData'; import { useCopyData } from '@fastgpt/web/hooks/useCopyData';
import { ChatSettingContext } from '@/web/core/chat/context/chatSettingContext'; import { ChatPageContext } from '@/web/core/chat/context/chatPageContext';
import { import {
ChatSidebarPaneEnum, ChatSidebarPaneEnum,
DEFAULT_LOGO_BANNER_COLLAPSED_URL DEFAULT_LOGO_BANNER_COLLAPSED_URL
@ -38,7 +37,6 @@ import { ChatTypeEnum } from '@/components/core/chat/ChatContainer/ChatBox/const
const ChatHeader = ({ const ChatHeader = ({
history, history,
showHistory, showHistory,
apps,
totalRecordsCount, totalRecordsCount,
pane, pane,
@ -50,18 +48,15 @@ const ChatHeader = ({
history: ChatItemType[]; history: ChatItemType[];
showHistory?: boolean; showHistory?: boolean;
apps?: AppListItemType[];
totalRecordsCount: number; totalRecordsCount: number;
reserveSpace?: boolean; reserveSpace?: boolean;
}) => { }) => {
const { t } = useTranslation(); const { t } = useTranslation();
const { isPc } = useSystem(); const { isPc } = useSystem();
const pathname = usePathname();
const { source } = useChatStore(); const { source } = useChatStore();
const chatData = useContextSelector(ChatItemContext, (v) => v.chatBoxData); const chatData = useContextSelector(ChatItemContext, (v) => v.chatBoxData);
const isVariableVisible = useContextSelector(ChatItemContext, (v) => v.isVariableVisible); const isVariableVisible = useContextSelector(ChatItemContext, (v) => v.isVariableVisible);
const isPlugin = chatData.app.type === AppTypeEnum.workflowTool; const isPlugin = chatData.app.type === AppTypeEnum.workflowTool;
const isShare = source === 'share'; const isShare = source === 'share';
const chatType = isShare ? ChatTypeEnum.share : ChatTypeEnum.chat; const chatType = isShare ? ChatTypeEnum.share : ChatTypeEnum.chat;
@ -87,7 +82,6 @@ const ChatHeader = ({
</> </>
) : ( ) : (
<MobileHeader <MobileHeader
apps={apps}
appId={chatData.appId} appId={chatData.appId}
name={ name={
pane === ChatSidebarPaneEnum.HOME && !isShare pane === ChatSidebarPaneEnum.HOME && !isShare
@ -113,15 +107,7 @@ const ChatHeader = ({
); );
}; };
const MobileDrawer = ({ const MobileDrawer = ({ onCloseDrawer, appId }: { onCloseDrawer: () => void; appId: string }) => {
onCloseDrawer,
appId,
apps
}: {
onCloseDrawer: () => void;
appId: string;
apps?: AppListItemType[];
}) => {
enum TabEnum { enum TabEnum {
recently = 'recently', recently = 'recently',
app = 'app' app = 'app'
@ -129,6 +115,7 @@ const MobileDrawer = ({
const { t } = useTranslation(); const { t } = useTranslation();
const { setChatId } = useChatStore(); const { setChatId } = useChatStore();
const myApps = useContextSelector(ChatPageContext, (v) => v.myApps);
const [currentTab, setCurrentTab] = useState<TabEnum>(TabEnum.recently); const [currentTab, setCurrentTab] = useState<TabEnum>(TabEnum.recently);
@ -143,7 +130,7 @@ const MobileDrawer = ({
); );
}, []); }, []);
const handlePaneChange = useContextSelector(ChatSettingContext, (v) => v.handlePaneChange); const handlePaneChange = useContextSelector(ChatPageContext, (v) => v.handlePaneChange);
const onclickApp = (id: string) => { const onclickApp = (id: string) => {
handlePaneChange(ChatSidebarPaneEnum.RECENTLY_USED_APPS, id); handlePaneChange(ChatSidebarPaneEnum.RECENTLY_USED_APPS, id);
@ -201,22 +188,22 @@ const MobileDrawer = ({
{/* history */} {/* history */}
{currentTab === TabEnum.recently && ( {currentTab === TabEnum.recently && (
<Box px={3} overflow={'auto'} h={'100%'}> <Box px={3} overflow={'auto'} h={'100%'}>
{Array.isArray(apps) && {Array.isArray(myApps) &&
apps.map((item) => ( myApps.map((item) => (
<Flex justify={'center'} key={item._id}> <Flex justify={'center'} key={item.appId}>
<Flex <Flex
py={2.5} py={2.5}
px={2} px={2}
width={'100%'} width={'100%'}
borderRadius={'md'} borderRadius={'md'}
alignItems={'center'} alignItems={'center'}
{...(item._id === appId {...(item.appId === appId
? { ? {
backgroundColor: 'primary.50 !important', backgroundColor: 'primary.50 !important',
color: 'primary.600' color: 'primary.600'
} }
: { : {
onClick: () => onclickApp(item._id) onClick: () => onclickApp(item.appId)
})} })}
> >
<Avatar src={item.avatar} w={'24px'} borderRadius={'sm'} /> <Avatar src={item.avatar} w={'24px'} borderRadius={'sm'} />
@ -247,13 +234,11 @@ const MobileHeader = ({
showHistory, showHistory,
name, name,
avatar, avatar,
appId, appId
apps
}: { }: {
showHistory?: boolean; showHistory?: boolean;
avatar: string; avatar: string;
name: string; name: string;
apps?: AppListItemType[];
appId: string; appId: string;
}) => { }) => {
const router = useRouter(); const router = useRouter();
@ -290,9 +275,7 @@ const MobileHeader = ({
</Flex> </Flex>
</Flex> </Flex>
{isOpenDrawer && !isShareChat && ( {isOpenDrawer && !isShareChat && <MobileDrawer appId={appId} onCloseDrawer={onCloseDrawer} />}
<MobileDrawer apps={apps} appId={appId} onCloseDrawer={onCloseDrawer} />
)}
</> </>
); );
}; };

View File

@ -23,6 +23,8 @@ import { getCollectionQuote } from '@/web/core/chat/api';
import MyIconButton from '@fastgpt/web/components/common/Icon/button'; import MyIconButton from '@fastgpt/web/components/common/Icon/button';
import MyBox from '@fastgpt/web/components/common/MyBox'; import MyBox from '@fastgpt/web/components/common/MyBox';
import { getCollectionSourceAndOpen } from '@/web/core/dataset/hooks/readCollectionSource'; import { getCollectionSourceAndOpen } from '@/web/core/dataset/hooks/readCollectionSource';
import { useContextSelector } from 'use-context-selector';
import { ChatItemContext } from '@/web/core/chat/context/chatItemContext';
const CollectionReader = ({ const CollectionReader = ({
rawSearch, rawSearch,
@ -37,6 +39,8 @@ const CollectionReader = ({
const router = useRouter(); const router = useRouter();
const { userInfo } = useUserStore(); const { userInfo } = useUserStore();
const canDownloadSource = useContextSelector(ChatItemContext, (v) => v.canDownloadSource);
const { collectionId, datasetId, chatItemDataId, sourceId, sourceName, quoteId } = metadata; const { collectionId, datasetId, chatItemDataId, sourceId, sourceName, quoteId } = metadata;
const [quoteIndex, setQuoteIndex] = useState(0); const [quoteIndex, setQuoteIndex] = useState(0);
@ -175,11 +179,13 @@ const CollectionReader = ({
{sourceName || t('common:unknow_source')} {sourceName || t('common:unknow_source')}
</Box> </Box>
<Box ml={3}> <Box ml={3}>
<DownloadButton {canDownloadSource && (
canAccessRawData={true} <DownloadButton
onDownload={handleDownload} canAccessRawData={true}
onRead={handleRead} onDownload={handleDownload}
/> onRead={handleRead}
/>
)}
</Box> </Box>
</Flex> </Flex>
<MyIconButton <MyIconButton

View File

@ -1,5 +1,5 @@
import LogChart from '@/pageComponents/app/detail/Logs/LogChart'; import LogChart from '@/pageComponents/app/detail/Logs/LogChart';
import { ChatSettingContext } from '@/web/core/chat/context/chatSettingContext'; import { ChatPageContext } from '@/web/core/chat/context/chatPageContext';
import { Flex } from '@chakra-ui/react'; import { Flex } from '@chakra-ui/react';
import { ChatSourceEnum } from '@fastgpt/global/core/chat/constants'; 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';
@ -13,7 +13,7 @@ type Props = {
}; };
const LogDetails = ({ Header }: Props) => { const LogDetails = ({ Header }: Props) => {
const appId = useContextSelector(ChatSettingContext, (v) => v.chatSettings?.appId || ''); const appId = useContextSelector(ChatPageContext, (v) => v.chatSettings?.appId || '');
const [dateRange, setDateRange] = useState<DateRangeType>({ const [dateRange, setDateRange] = useState<DateRangeType>({
from: new Date(addDays(new Date(), -6).setHours(0, 0, 0, 0)), from: new Date(addDays(new Date(), -6).setHours(0, 0, 0, 0)),

View File

@ -1,4 +1,4 @@
import { ChatSettingContext } from '@/web/core/chat/context/chatSettingContext'; import { ChatPageContext } from '@/web/core/chat/context/chatPageContext';
import { AddIcon } from '@chakra-ui/icons'; import { AddIcon } from '@chakra-ui/icons';
import { import {
Box, Box,
@ -377,10 +377,10 @@ type Props = {
const TagManageModal = ({ onClose, onRefresh }: Props) => { const TagManageModal = ({ onClose, onRefresh }: Props) => {
const { t } = useTranslation(); const { t } = useTranslation();
const refreshChatSetting = useContextSelector(ChatSettingContext, (v) => v.refreshChatSetting); const refreshChatSetting = useContextSelector(ChatPageContext, (v) => v.refreshChatSetting);
// get tags from db // get tags from db
const tags = useContextSelector(ChatSettingContext, (v) => v.chatSettings?.favouriteTags || []); const tags = useContextSelector(ChatPageContext, (v) => v.chatSettings?.favouriteTags || []);
// local editable tags list // local editable tags list
const [localTags, setLocalTags] = useState<ChatFavouriteTagType[]>(tags); const [localTags, setLocalTags] = useState<ChatFavouriteTagType[]>(tags);

View File

@ -1,20 +1,12 @@
import { ChatSettingContext } from '@/web/core/chat/context/chatSettingContext'; import { ChatPageContext } from '@/web/core/chat/context/chatPageContext';
import { import {
Button, Button,
ButtonGroup,
Flex, Flex,
HStack, HStack,
IconButton, IconButton,
Input, Input,
InputGroup, InputGroup,
InputLeftElement, InputLeftElement,
Table,
TableContainer,
Tbody,
Td,
Th,
Thead,
Tr,
useDisclosure useDisclosure
} from '@chakra-ui/react'; } from '@chakra-ui/react';
import MySelect from '@fastgpt/web/components/common/MySelect'; import MySelect from '@fastgpt/web/components/common/MySelect';
@ -68,7 +60,7 @@ const FavouriteAppSetting = ({ Header }: Props) => {
const searchAppTagValue = watchSearchValue('tag'); const searchAppTagValue = watchSearchValue('tag');
// apps' tags options // apps' tags options
const tagOptions = useContextSelector(ChatSettingContext, (v) => { const tagOptions = useContextSelector(ChatPageContext, (v) => {
const tags = v.chatSettings?.favouriteTags || []; const tags = v.chatSettings?.favouriteTags || [];
return [ return [
{ label: t('chat:setting.favourite.category_all'), value: '' }, { label: t('chat:setting.favourite.category_all'), value: '' },
@ -76,7 +68,7 @@ const FavouriteAppSetting = ({ Header }: Props) => {
]; ];
}); });
// app's tags cache map // app's tags cache map
const tagMap = useContextSelector(ChatSettingContext, (v) => const tagMap = useContextSelector(ChatPageContext, (v) =>
(v.chatSettings?.favouriteTags || []).reduce<Record<string, ChatFavouriteTagType>>( (v.chatSettings?.favouriteTags || []).reduce<Record<string, ChatFavouriteTagType>>(
(acc, tag) => { (acc, tag) => {
acc[tag.id] = { ...tag }; acc[tag.id] = { ...tag };

View File

@ -23,7 +23,7 @@ import Avatar from '@fastgpt/web/components/common/Avatar';
import type { NodeInputKeyEnum } from '@fastgpt/global/core/workflow/constants'; import type { NodeInputKeyEnum } from '@fastgpt/global/core/workflow/constants';
import { useMount } from 'ahooks'; import { useMount } from 'ahooks';
import { useContextSelector } from 'use-context-selector'; import { useContextSelector } from 'use-context-selector';
import { ChatSettingContext } from '@/web/core/chat/context/chatSettingContext'; import { ChatPageContext } from '@/web/core/chat/context/chatPageContext';
import { import {
DEFAULT_LOGO_BANNER_COLLAPSED_URL, DEFAULT_LOGO_BANNER_COLLAPSED_URL,
DEFAULT_LOGO_BANNER_URL DEFAULT_LOGO_BANNER_URL
@ -46,8 +46,8 @@ const HomepageSetting = ({ Header, onDiagramShow }: Props) => {
const { t } = useTranslation(); const { t } = useTranslation();
const { feConfigs } = useSystemStore(); const { feConfigs } = useSystemStore();
const chatSettings = useContextSelector(ChatSettingContext, (v) => v.chatSettings); const chatSettings = useContextSelector(ChatPageContext, (v) => v.chatSettings);
const refreshChatSetting = useContextSelector(ChatSettingContext, (v) => v.refreshChatSetting); const refreshChatSetting = useContextSelector(ChatPageContext, (v) => v.refreshChatSetting);
const chatSettings2Form = useCallback( const chatSettings2Form = useCallback(
(data?: ChatSettingType) => { (data?: ChatSettingType) => {

View File

@ -1,5 +1,5 @@
import LogTable from '@/pageComponents/app/detail/Logs/LogTable'; import LogTable from '@/pageComponents/app/detail/Logs/LogTable';
import { ChatSettingContext } from '@/web/core/chat/context/chatSettingContext'; import { ChatPageContext } from '@/web/core/chat/context/chatPageContext';
import { Flex } from '@chakra-ui/react'; import { Flex } from '@chakra-ui/react';
import { ChatSourceEnum } from '@fastgpt/global/core/chat/constants'; 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';
@ -16,7 +16,7 @@ type Props = {
const chatSourceValues = Object.values(ChatSourceEnum); const chatSourceValues = Object.values(ChatSourceEnum);
const LogDetails = ({ Header }: Props) => { const LogDetails = ({ Header }: Props) => {
const appId = useContextSelector(ChatSettingContext, (v) => v.chatSettings?.appId || ''); const appId = useContextSelector(ChatPageContext, (v) => v.chatSettings?.appId || '');
const [dateRange, setDateRange] = useState<DateRangeType>({ const [dateRange, setDateRange] = useState<DateRangeType>({
from: new Date(addDays(new Date(), -6).setHours(0, 0, 0, 0)), from: new Date(addDays(new Date(), -6).setHours(0, 0, 0, 0)),

View File

@ -9,7 +9,7 @@ import { useContextSelector } from 'use-context-selector';
import MyIcon from '@fastgpt/web/components/common/Icon'; import MyIcon from '@fastgpt/web/components/common/Icon';
import { ChatContext } from '@/web/core/chat/context/chatContext'; import { ChatContext } from '@/web/core/chat/context/chatContext';
import NextHead from '@/components/common/NextHead'; import NextHead from '@/components/common/NextHead';
import { ChatSettingContext } from '@/web/core/chat/context/chatSettingContext'; import { ChatPageContext } from '@/web/core/chat/context/chatPageContext';
import ChatSliderMobileDrawer from '@/pageComponents/chat/slider/ChatSliderMobileDrawer'; import ChatSliderMobileDrawer from '@/pageComponents/chat/slider/ChatSliderMobileDrawer';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { useMount } from 'ahooks'; import { useMount } from 'ahooks';
@ -43,8 +43,8 @@ const ChatSetting = () => {
); );
const onOpenSlider = useContextSelector(ChatContext, (v) => v.onOpenSlider); const onOpenSlider = useContextSelector(ChatContext, (v) => v.onOpenSlider);
const chatSettings = useContextSelector(ChatSettingContext, (v) => v.chatSettings); const chatSettings = useContextSelector(ChatPageContext, (v) => v.chatSettings);
const handlePaneChange = useContextSelector(ChatSettingContext, (v) => v.handlePaneChange); const handlePaneChange = useContextSelector(ChatPageContext, (v) => v.handlePaneChange);
const handleTabChange = useCallback( const handleTabChange = useCallback(
(tab: ChatSettingTabOptionEnum) => { (tab: ChatSettingTabOptionEnum) => {

View File

@ -15,7 +15,7 @@ import AppTypeTag from '@/pageComponents/chat/ChatTeamApp/TypeTag';
import { formatTimeToChatTime } from '@fastgpt/global/common/string/time'; import { formatTimeToChatTime } from '@fastgpt/global/common/string/time';
import { useSystem } from '@fastgpt/web/hooks/useSystem'; import { useSystem } from '@fastgpt/web/hooks/useSystem';
import UserBox from '@fastgpt/web/components/common/UserBox'; import UserBox from '@fastgpt/web/components/common/UserBox';
import { ChatSettingContext } from '@/web/core/chat/context/chatSettingContext'; import { ChatPageContext } from '@/web/core/chat/context/chatPageContext';
import { ChatSidebarPaneEnum } from '@/pageComponents/chat/constants'; import { ChatSidebarPaneEnum } from '@/pageComponents/chat/constants';
const List = ({ appType }: { appType: AppTypeEnum | 'all' }) => { const List = ({ appType }: { appType: AppTypeEnum | 'all' }) => {
@ -33,7 +33,7 @@ const List = ({ appType }: { appType: AppTypeEnum | 'all' }) => {
].includes(app.type) ].includes(app.type)
) )
); );
const handlePaneChange = useContextSelector(ChatSettingContext, (v) => v.handlePaneChange); const handlePaneChange = useContextSelector(ChatPageContext, (v) => v.handlePaneChange);
return ( return (
<> <>

View File

@ -13,7 +13,7 @@ import SearchInput from '@fastgpt/web/components/common/Input/SearchInput';
import MyIcon from '@fastgpt/web/components/common/Icon'; import MyIcon from '@fastgpt/web/components/common/Icon';
import { ChatContext } from '@/web/core/chat/context/chatContext'; import { ChatContext } from '@/web/core/chat/context/chatContext';
import NextHead from '@/components/common/NextHead'; import NextHead from '@/components/common/NextHead';
import { ChatSettingContext } from '@/web/core/chat/context/chatSettingContext'; import { ChatPageContext } from '@/web/core/chat/context/chatPageContext';
import ChatSliderMobileDrawer from '@/pageComponents/chat/slider/ChatSliderMobileDrawer'; import ChatSliderMobileDrawer from '@/pageComponents/chat/slider/ChatSliderMobileDrawer';
import { useSystemStore } from '@/web/common/system/useSystemStore'; import { useSystemStore } from '@/web/common/system/useSystemStore';
import { getWebReqUrl } from '@fastgpt/web/common/system/utils'; import { getWebReqUrl } from '@fastgpt/web/common/system/utils';
@ -29,7 +29,7 @@ const MyApps = () => {
(v) => v (v) => v
); );
const chatSettings = useContextSelector(ChatSettingContext, (v) => v.chatSettings); const chatSettings = useContextSelector(ChatPageContext, (v) => v.chatSettings);
const onOpenSlider = useContextSelector(ChatContext, (v) => v.onOpenSlider); const onOpenSlider = useContextSelector(ChatContext, (v) => v.onOpenSlider);

View File

@ -7,7 +7,6 @@ import SideBar from '@/components/SideBar';
import { ChatContext } from '@/web/core/chat/context/chatContext'; import { ChatContext } from '@/web/core/chat/context/chatContext';
import { useContextSelector } from 'use-context-selector'; import { useContextSelector } from 'use-context-selector';
import { ChatItemContext } from '@/web/core/chat/context/chatItemContext'; import { ChatItemContext } from '@/web/core/chat/context/chatItemContext';
import { type AppListItemType } from '@fastgpt/global/core/app/type';
import { ChatTypeEnum } from '@/components/core/chat/ChatContainer/ChatBox/constants'; import { ChatTypeEnum } from '@/components/core/chat/ChatContainer/ChatBox/constants';
import { useCallback } from 'react'; import { useCallback } from 'react';
import type { StartChatFnProps } from '@/components/core/chat/ChatContainer/type'; import type { StartChatFnProps } from '@/components/core/chat/ChatContainer/type';
@ -20,21 +19,18 @@ import { useRequest2 } from '@fastgpt/web/hooks/useRequest';
import { getInitChatInfo } from '@/web/core/chat/api'; import { getInitChatInfo } from '@/web/core/chat/api';
import { useUserStore } from '@/web/support/user/useUserStore'; import { useUserStore } from '@/web/support/user/useUserStore';
import NextHead from '@/components/common/NextHead'; import NextHead from '@/components/common/NextHead';
import { ChatSettingContext } from '@/web/core/chat/context/chatSettingContext'; import { ChatPageContext } from '@/web/core/chat/context/chatPageContext';
import { ChatSidebarPaneEnum } from '../constants'; import { ChatSidebarPaneEnum } from '../constants';
import ChatHistorySidebar from '@/pageComponents/chat/slider/ChatSliderSidebar'; import ChatHistorySidebar from '@/pageComponents/chat/slider/ChatSliderSidebar';
import ChatSliderMobileDrawer from '@/pageComponents/chat/slider/ChatSliderMobileDrawer'; import ChatSliderMobileDrawer from '@/pageComponents/chat/slider/ChatSliderMobileDrawer';
import dynamic from 'next/dynamic'; import dynamic from 'next/dynamic';
import { getNanoid } from '@fastgpt/global/common/string/tools'; import { getNanoid } from '@fastgpt/global/common/string/tools';
import { ChatErrEnum } from '@fastgpt/global/common/error/code/chat'; import { ChatErrEnum } from '@fastgpt/global/common/error/code/chat';
import { AppErrEnum } from '@fastgpt/global/common/error/code/app';
const CustomPluginRunBox = dynamic(() => import('@/pageComponents/chat/CustomPluginRunBox')); const CustomPluginRunBox = dynamic(() => import('@/pageComponents/chat/CustomPluginRunBox'));
type Props = { const AppChatWindow = () => {
myApps: AppListItemType[];
};
const AppChatWindow = ({ myApps }: Props) => {
const { userInfo } = useUserStore(); const { userInfo } = useUserStore();
const { chatId, appId, outLinkAuthData } = useChatStore(); const { chatId, appId, outLinkAuthData } = useChatStore();
@ -45,6 +41,7 @@ const AppChatWindow = ({ myApps }: Props) => {
const onUpdateHistoryTitle = useContextSelector(ChatContext, (v) => v.onUpdateHistoryTitle); const onUpdateHistoryTitle = useContextSelector(ChatContext, (v) => v.onUpdateHistoryTitle);
const isPlugin = useContextSelector(ChatItemContext, (v) => v.isPlugin); const isPlugin = useContextSelector(ChatItemContext, (v) => v.isPlugin);
const isShowCite = useContextSelector(ChatItemContext, (v) => v.isShowCite);
const onChangeChatId = useContextSelector(ChatContext, (v) => v.onChangeChatId); const onChangeChatId = useContextSelector(ChatContext, (v) => v.onChangeChatId);
const chatBoxData = useContextSelector(ChatItemContext, (v) => v.chatBoxData); const chatBoxData = useContextSelector(ChatItemContext, (v) => v.chatBoxData);
const datasetCiteData = useContextSelector(ChatItemContext, (v) => v.datasetCiteData); const datasetCiteData = useContextSelector(ChatItemContext, (v) => v.datasetCiteData);
@ -54,9 +51,10 @@ const AppChatWindow = ({ myApps }: Props) => {
const chatRecords = useContextSelector(ChatRecordContext, (v) => v.chatRecords); const chatRecords = useContextSelector(ChatRecordContext, (v) => v.chatRecords);
const totalRecordsCount = useContextSelector(ChatRecordContext, (v) => v.totalRecordsCount); const totalRecordsCount = useContextSelector(ChatRecordContext, (v) => v.totalRecordsCount);
const pane = useContextSelector(ChatSettingContext, (v) => v.pane); const pane = useContextSelector(ChatPageContext, (v) => v.pane);
const chatSettings = useContextSelector(ChatSettingContext, (v) => v.chatSettings); const chatSettings = useContextSelector(ChatPageContext, (v) => v.chatSettings);
const handlePaneChange = useContextSelector(ChatSettingContext, (v) => v.handlePaneChange); const handlePaneChange = useContextSelector(ChatPageContext, (v) => v.handlePaneChange);
const refreshRecentlyUsed = useContextSelector(ChatPageContext, (v) => v.refreshRecentlyUsed);
const { loading } = useRequest2( const { loading } = useRequest2(
async () => { async () => {
@ -82,6 +80,9 @@ const AppChatWindow = ({ myApps }: Props) => {
onChangeChatId(); onChangeChatId();
return; return;
} }
if (e?.statusText === AppErrEnum.unAuthApp) {
refreshRecentlyUsed();
}
handlePaneChange(ChatSidebarPaneEnum.TEAM_APPS); handlePaneChange(ChatSidebarPaneEnum.TEAM_APPS);
} }
}, },
@ -106,7 +107,8 @@ const AppChatWindow = ({ myApps }: Props) => {
variables, variables,
responseChatItemId, responseChatItemId,
appId, appId,
chatId chatId,
retainDatasetCite: isShowCite
}, },
abortCtrl: controller, abortCtrl: controller,
onMessage: generatingMessage onMessage: generatingMessage
@ -120,9 +122,19 @@ const AppChatWindow = ({ myApps }: Props) => {
title: newTitle title: newTitle
})); }));
refreshRecentlyUsed();
return { responseText, isNewChat: forbidLoadChat.current }; return { responseText, isNewChat: forbidLoadChat.current };
}, },
[appId, chatId, onUpdateHistoryTitle, setChatBoxData, forbidLoadChat] [
appId,
chatId,
onUpdateHistoryTitle,
setChatBoxData,
forbidLoadChat,
isShowCite,
refreshRecentlyUsed
]
); );
return ( return (
@ -156,7 +168,6 @@ const AppChatWindow = ({ myApps }: Props) => {
pane={pane} pane={pane}
chatSettings={chatSettings} chatSettings={chatSettings}
showHistory showHistory
apps={myApps}
history={chatRecords} history={chatRecords}
totalRecordsCount={totalRecordsCount} totalRecordsCount={totalRecordsCount}
/> />

View File

@ -36,12 +36,8 @@ import { getDefaultAppForm } from '@fastgpt/global/core/app/utils';
import { getToolPreviewNode } from '@/web/core/app/api/tool'; import { getToolPreviewNode } from '@/web/core/app/api/tool';
import type { FlowNodeTemplateType } from '@fastgpt/global/core/workflow/type/node'; import type { FlowNodeTemplateType } from '@fastgpt/global/core/workflow/type/node';
import { getWebLLMModel } from '@/web/common/system/utils'; import { getWebLLMModel } from '@/web/common/system/utils';
import { ChatSettingContext } from '@/web/core/chat/context/chatSettingContext'; import { ChatPageContext } from '@/web/core/chat/context/chatPageContext';
import type { import type { AppFileSelectConfigType, AppWhisperConfigType } from '@fastgpt/global/core/app/type';
AppFileSelectConfigType,
AppListItemType,
AppWhisperConfigType
} from '@fastgpt/global/core/app/type';
import ChatHeader from '@/pageComponents/chat/ChatHeader'; import ChatHeader from '@/pageComponents/chat/ChatHeader';
import { ChatRecordContext } from '@/web/core/chat/context/chatRecordContext'; import { ChatRecordContext } from '@/web/core/chat/context/chatRecordContext';
import { ChatSidebarPaneEnum } from '../constants'; import { ChatSidebarPaneEnum } from '../constants';
@ -49,10 +45,6 @@ import ChatHistorySidebar from '@/pageComponents/chat/slider/ChatSliderSidebar';
import ChatSliderMobileDrawer from '@/pageComponents/chat/slider/ChatSliderMobileDrawer'; import ChatSliderMobileDrawer from '@/pageComponents/chat/slider/ChatSliderMobileDrawer';
import { getWebReqUrl } from '@fastgpt/web/common/system/utils'; import { getWebReqUrl } from '@fastgpt/web/common/system/utils';
type Props = {
myApps: AppListItemType[];
};
const defaultFileSelectConfig: AppFileSelectConfigType = { const defaultFileSelectConfig: AppFileSelectConfigType = {
maxFiles: 20, maxFiles: 20,
canSelectFile: true, canSelectFile: true,
@ -68,7 +60,7 @@ const defaultWhisperConfig: AppWhisperConfigType = {
autoTTSResponse: false autoTTSResponse: false
}; };
const HomeChatWindow = ({ myApps }: Props) => { const HomeChatWindow = () => {
const { t } = useTranslation(); const { t } = useTranslation();
const { isPc } = useSystem(); const { isPc } = useSystem();
@ -84,11 +76,13 @@ const HomeChatWindow = ({ myApps }: Props) => {
const datasetCiteData = useContextSelector(ChatItemContext, (v) => v.datasetCiteData); const datasetCiteData = useContextSelector(ChatItemContext, (v) => v.datasetCiteData);
const setChatBoxData = useContextSelector(ChatItemContext, (v) => v.setChatBoxData); const setChatBoxData = useContextSelector(ChatItemContext, (v) => v.setChatBoxData);
const resetVariables = useContextSelector(ChatItemContext, (v) => v.resetVariables); const resetVariables = useContextSelector(ChatItemContext, (v) => v.resetVariables);
const isShowCite = useContextSelector(ChatItemContext, (v) => v.isShowCite);
const pane = useContextSelector(ChatSettingContext, (v) => v.pane); const pane = useContextSelector(ChatPageContext, (v) => v.pane);
const chatSettings = useContextSelector(ChatSettingContext, (v) => v.chatSettings); const chatSettings = useContextSelector(ChatPageContext, (v) => v.chatSettings);
const handlePaneChange = useContextSelector(ChatSettingContext, (v) => v.handlePaneChange); const handlePaneChange = useContextSelector(ChatPageContext, (v) => v.handlePaneChange);
const homeAppId = useContextSelector(ChatSettingContext, (v) => v.chatSettings?.appId || ''); const homeAppId = useContextSelector(ChatPageContext, (v) => v.chatSettings?.appId || '');
const refreshRecentlyUsed = useContextSelector(ChatPageContext, (v) => v.refreshRecentlyUsed);
const chatRecords = useContextSelector(ChatRecordContext, (v) => v.chatRecords); const chatRecords = useContextSelector(ChatRecordContext, (v) => v.chatRecords);
const totalRecordsCount = useContextSelector(ChatRecordContext, (v) => v.totalRecordsCount); const totalRecordsCount = useContextSelector(ChatRecordContext, (v) => v.totalRecordsCount);
@ -216,7 +210,8 @@ const HomeChatWindow = ({ myApps }: Props) => {
variables, variables,
responseChatItemId, responseChatItemId,
appId, appId,
chatId chatId,
retainDatasetCite: isShowCite
}, },
abortCtrl: controller, abortCtrl: controller,
onMessage: generatingMessage onMessage: generatingMessage
@ -230,6 +225,8 @@ const HomeChatWindow = ({ myApps }: Props) => {
title: newTitle title: newTitle
})); }));
refreshRecentlyUsed();
return { responseText, isNewChat: forbidLoadChat.current }; return { responseText, isNewChat: forbidLoadChat.current };
} }
@ -264,6 +261,7 @@ const HomeChatWindow = ({ myApps }: Props) => {
appId, appId,
appName: t('chat:home.chat_app'), appName: t('chat:home.chat_app'),
chatId, chatId,
retainDatasetCite: isShowCite,
...form2AppWorkflow(formData, t) ...form2AppWorkflow(formData, t)
}, },
onMessage: generatingMessage, onMessage: generatingMessage,
@ -278,6 +276,8 @@ const HomeChatWindow = ({ myApps }: Props) => {
title: newTitle title: newTitle
})); }));
refreshRecentlyUsed();
return { responseText, isNewChat: forbidLoadChat.current }; return { responseText, isNewChat: forbidLoadChat.current };
} }
); );
@ -394,7 +394,8 @@ const HomeChatWindow = ({ myApps }: Props) => {
setSelectedToolIds, setSelectedToolIds,
setChatBoxData, setChatBoxData,
isPc, isPc,
isQuickApp isQuickApp,
isShowCite
] ]
); );
@ -445,7 +446,6 @@ const HomeChatWindow = ({ myApps }: Props) => {
pane={pane} pane={pane}
chatSettings={chatSettings} chatSettings={chatSettings}
showHistory showHistory
apps={myApps}
history={chatRecords} history={chatRecords}
totalRecordsCount={totalRecordsCount} totalRecordsCount={totalRecordsCount}
/> />

View File

@ -1,5 +1,5 @@
import React from 'react'; import React from 'react';
import { ChatSettingContext } from '@/web/core/chat/context/chatSettingContext'; import { ChatPageContext } from '@/web/core/chat/context/chatPageContext';
import { ChatSidebarPaneEnum } from '@/pageComponents/chat/constants'; import { ChatSidebarPaneEnum } from '@/pageComponents/chat/constants';
import { useContextSelector } from 'use-context-selector'; import { useContextSelector } from 'use-context-selector';
import { ChatContext } from '@/web/core/chat/context/chatContext'; import { ChatContext } from '@/web/core/chat/context/chatContext';
@ -15,8 +15,8 @@ const ChatSliderFooter = () => {
const { feConfigs } = useSystemStore(); const { feConfigs } = useSystemStore();
const onCloseSlider = useContextSelector(ChatContext, (v) => v.onCloseSlider); const onCloseSlider = useContextSelector(ChatContext, (v) => v.onCloseSlider);
const handlePaneChange = useContextSelector(ChatSettingContext, (v) => v.handlePaneChange); const handlePaneChange = useContextSelector(ChatPageContext, (v) => v.handlePaneChange);
const pane = useContextSelector(ChatSettingContext, (v) => v.pane); const pane = useContextSelector(ChatPageContext, (v) => v.pane);
const isAdmin = !!userInfo?.team.permission.hasManagePer; const isAdmin = !!userInfo?.team.permission.hasManagePer;
const isSettingPane = pane === ChatSidebarPaneEnum.SETTING; const isSettingPane = pane === ChatSidebarPaneEnum.SETTING;

View File

@ -1,6 +1,6 @@
import { GridItem, Grid } from '@chakra-ui/react'; import { GridItem, Grid } from '@chakra-ui/react';
import React from 'react'; import React from 'react';
import { ChatSettingContext } from '@/web/core/chat/context/chatSettingContext'; import { ChatPageContext } from '@/web/core/chat/context/chatPageContext';
import { ChatSidebarPaneEnum } from '@/pageComponents/chat/constants'; import { ChatSidebarPaneEnum } from '@/pageComponents/chat/constants';
import { useContextSelector } from 'use-context-selector'; import { useContextSelector } from 'use-context-selector';
import { ChatContext } from '@/web/core/chat/context/chatContext'; import { ChatContext } from '@/web/core/chat/context/chatContext';
@ -24,9 +24,9 @@ const ChatSliderHeader = ({ title, banner }: Props) => {
const { isPc } = useSystem(); const { isPc } = useSystem();
const { setChatId } = useChatStore(); const { setChatId } = useChatStore();
const pane = useContextSelector(ChatSettingContext, (v) => v.pane); const pane = useContextSelector(ChatPageContext, (v) => v.pane);
const handlePaneChange = useContextSelector(ChatSettingContext, (v) => v.handlePaneChange); const handlePaneChange = useContextSelector(ChatPageContext, (v) => v.handlePaneChange);
const enableHome = useContextSelector(ChatSettingContext, (v) => v.chatSettings?.enableHome); const enableHome = useContextSelector(ChatPageContext, (v) => v.chatSettings?.enableHome);
const appName = useContextSelector(ChatItemContext, (v) => v.chatBoxData?.app.name); const appName = useContextSelector(ChatItemContext, (v) => v.chatBoxData?.app.name);
const appAvatar = useContextSelector(ChatItemContext, (v) => v.chatBoxData?.app.avatar); const appAvatar = useContextSelector(ChatItemContext, (v) => v.chatBoxData?.app.avatar);

View File

@ -17,12 +17,11 @@ import {
} from '@/pageComponents/chat/constants'; } from '@/pageComponents/chat/constants';
import { useSystemStore } from '@/web/common/system/useSystemStore'; import { useSystemStore } from '@/web/common/system/useSystemStore';
import { useContextSelector } from 'use-context-selector'; import { useContextSelector } from 'use-context-selector';
import { ChatSettingContext } from '@/web/core/chat/context/chatSettingContext'; import { ChatPageContext } from '@/web/core/chat/context/chatPageContext';
import { usePathname } from 'next/navigation'; import { usePathname } from 'next/navigation';
type Props = { type Props = {
activeAppId: string; activeAppId: string;
apps: AppListItemType[];
}; };
const MotionBox = motion(Box); const MotionBox = motion(Box);
@ -148,13 +147,13 @@ const AnimatedText: React.FC<AnimatedTextProps> = ({ show, children, className,
); );
const LogoSection = () => { const LogoSection = () => {
const isCollapsed = useContextSelector(ChatSettingContext, (v) => v.collapse === 1); const isCollapsed = useContextSelector(ChatPageContext, (v) => v.collapse === 1);
const logos = useContextSelector(ChatSettingContext, (v) => v.logos); const logos = useContextSelector(ChatPageContext, (v) => v.logos);
const isHomeActive = useContextSelector( const isHomeActive = useContextSelector(
ChatSettingContext, ChatPageContext,
(v) => v.pane === ChatSidebarPaneEnum.HOME (v) => v.pane === ChatSidebarPaneEnum.HOME
); );
const onTriggerCollapse = useContextSelector(ChatSettingContext, (v) => v.onTriggerCollapse); const onTriggerCollapse = useContextSelector(ChatPageContext, (v) => v.onTriggerCollapse);
const wideLogoSrc = logos.wideLogoUrl; const wideLogoSrc = logos.wideLogoUrl;
const squareLogoSrc = logos.squareLogoUrl; const squareLogoSrc = logos.squareLogoUrl;
@ -256,24 +255,24 @@ const NavigationSection = () => {
const { feConfigs } = useSystemStore(); const { feConfigs } = useSystemStore();
const isEnableHome = useContextSelector( const isEnableHome = useContextSelector(
ChatSettingContext, ChatPageContext,
(v) => v.chatSettings?.enableHome ?? true (v) => v.chatSettings?.enableHome ?? true
); );
const isCollapsed = useContextSelector(ChatSettingContext, (v) => v.collapse === 1); const isCollapsed = useContextSelector(ChatPageContext, (v) => v.collapse === 1);
const onTriggerCollapse = useContextSelector(ChatSettingContext, (v) => v.onTriggerCollapse); const onTriggerCollapse = useContextSelector(ChatPageContext, (v) => v.onTriggerCollapse);
const isHomeActive = useContextSelector( const isHomeActive = useContextSelector(
ChatSettingContext, ChatPageContext,
(v) => v.pane === ChatSidebarPaneEnum.HOME (v) => v.pane === ChatSidebarPaneEnum.HOME
); );
const isTeamAppsActive = useContextSelector( const isTeamAppsActive = useContextSelector(
ChatSettingContext, ChatPageContext,
(v) => v.pane === ChatSidebarPaneEnum.TEAM_APPS (v) => v.pane === ChatSidebarPaneEnum.TEAM_APPS
); );
const isFavouriteAppsActive = useContextSelector( const isFavouriteAppsActive = useContextSelector(
ChatSettingContext, ChatPageContext,
(v) => v.pane === ChatSidebarPaneEnum.FAVORITE_APPS (v) => v.pane === ChatSidebarPaneEnum.FAVORITE_APPS
); );
const handlePaneChange = useContextSelector(ChatSettingContext, (v) => v.handlePaneChange); const handlePaneChange = useContextSelector(ChatPageContext, (v) => v.handlePaneChange);
return ( return (
<Flex mt={4} flexDirection={'column'} gap={1} px={4}> <Flex mt={4} flexDirection={'column'} gap={1} px={4}>
@ -365,12 +364,12 @@ const BottomSection = () => {
const isAdmin = !!userInfo?.team.permission.hasManagePer; const isAdmin = !!userInfo?.team.permission.hasManagePer;
const isShare = pathname === '/chat/share'; const isShare = pathname === '/chat/share';
const isCollapsed = useContextSelector(ChatSettingContext, (v) => v.collapse === 1); const isCollapsed = useContextSelector(ChatPageContext, (v) => v.collapse === 1);
const isSettingActive = useContextSelector( const isSettingActive = useContextSelector(
ChatSettingContext, ChatPageContext,
(v) => v.pane === ChatSidebarPaneEnum.SETTING (v) => v.pane === ChatSidebarPaneEnum.SETTING
); );
const onSettingClick = useContextSelector(ChatSettingContext, (v) => v.handlePaneChange); const onSettingClick = useContextSelector(ChatPageContext, (v) => v.handlePaneChange);
return ( return (
<MotionBox mt={'auto'} px={3} py={4} layout={false}> <MotionBox mt={'auto'} px={3} py={4} layout={false}>
@ -485,13 +484,14 @@ const BottomSection = () => {
); );
}; };
const ChatSlider = ({ apps, activeAppId }: Props) => { const ChatSlider = ({ activeAppId }: Props) => {
const { t } = useTranslation(); const { t } = useTranslation();
const isCollapsed = useContextSelector(ChatSettingContext, (v) => v.collapse === 1); const isCollapsed = useContextSelector(ChatPageContext, (v) => v.collapse === 1);
const pane = useContextSelector(ChatSettingContext, (v) => v.pane); const pane = useContextSelector(ChatPageContext, (v) => v.pane);
const myApps = useContextSelector(ChatPageContext, (v) => v.myApps);
const handlePaneChange = useContextSelector(ChatSettingContext, (v) => v.handlePaneChange); const handlePaneChange = useContextSelector(ChatPageContext, (v) => v.handlePaneChange);
return ( return (
<MotionFlex <MotionFlex
@ -531,9 +531,9 @@ const ChatSlider = ({ apps, activeAppId }: Props) => {
</HStack> </HStack>
<MyBox flex={'1 0 0'} h={0} overflow={'overlay'} px={4} position={'relative'}> <MyBox flex={'1 0 0'} h={0} overflow={'overlay'} px={4} position={'relative'}>
{apps.map((item) => ( {myApps.map((item) => (
<Flex <Flex
key={item._id} key={item.appId}
py={2} py={2}
px={2} px={2}
mb={3} mb={3}
@ -541,12 +541,12 @@ const ChatSlider = ({ apps, activeAppId }: Props) => {
borderRadius={'md'} borderRadius={'md'}
alignItems={'center'} alignItems={'center'}
fontSize={'sm'} fontSize={'sm'}
{...(pane === ChatSidebarPaneEnum.RECENTLY_USED_APPS && item._id === activeAppId {...(pane === ChatSidebarPaneEnum.RECENTLY_USED_APPS && item.appId === activeAppId
? { bg: 'primary.100', color: 'primary.600' } ? { bg: 'primary.100', color: 'primary.600' }
: { : {
_hover: { bg: 'primary.100' }, _hover: { bg: 'primary.100' },
onClick: () => onClick: () =>
handlePaneChange(ChatSidebarPaneEnum.RECENTLY_USED_APPS, item._id) handlePaneChange(ChatSidebarPaneEnum.RECENTLY_USED_APPS, item.appId)
})} })}
> >
<Avatar src={item.avatar} w={'1.5rem'} borderRadius={'md'} /> <Avatar src={item.avatar} w={'1.5rem'} borderRadius={'md'} />

View File

@ -1,48 +0,0 @@
import { getRecentlyUsedApps } from '@/web/core/app/api';
import { useChatStore } from '@/web/core/chat/context/useChatStore';
import { useUserStore } from '@/web/support/user/useUserStore';
import { useRequest2 } from '@fastgpt/web/hooks/useRequest';
import { useMount } from 'ahooks';
import { useState, useEffect } from 'react';
export const useChat = (appId: string) => {
const { setSource, setAppId } = useChatStore();
const { userInfo, initUserInfo } = useUserStore();
const [isInitedUser, setIsInitedUser] = useState(false);
// get app list
const { data: myApps = [] } = useRequest2(() => getRecentlyUsedApps({ getRecentlyChat: true }), {
manual: false,
errorToast: '',
refreshDeps: [userInfo],
pollingInterval: 30000
});
// initialize user info
useMount(async () => {
// ensure store has current appId before setting source (avoids fallback to lastChatAppId)
if (appId) setAppId(appId);
try {
await initUserInfo();
} catch (error) {
console.log('User not logged in:', error);
} finally {
setSource('online');
setIsInitedUser(true);
}
});
// sync appId to store as soon as route/appId changes
useEffect(() => {
if (appId) {
setAppId(appId);
}
}, [appId, setAppId, userInfo]);
return {
isInitedUser,
userInfo,
myApps
};
};

View File

@ -449,7 +449,7 @@ const TestResults = React.memo(function TestResults({
<Box mt={1} gap={4}> <Box mt={1} gap={4}>
{datasetTestItem?.results.map((item, index) => ( {datasetTestItem?.results.map((item, index) => (
<Box key={item.id} p={3} borderRadius={'lg'} bg={'myGray.100'} _notLast={{ mb: 2 }}> <Box key={item.id} p={3} borderRadius={'lg'} bg={'myGray.100'} _notLast={{ mb: 2 }}>
<QuoteItem quoteItem={item} canViewSource canEditData /> <QuoteItem quoteItem={item} canDownloadSource canEditData />
</Box> </Box>
))} ))}
</Box> </Box>

View File

@ -5,36 +5,56 @@ import type { S3MQJobData } from '@fastgpt/service/common/s3/mq';
import { addLog } from '@fastgpt/service/common/system/log'; import { addLog } from '@fastgpt/service/common/system/log';
import type { ApiRequestProps, ApiResponseType } from '@fastgpt/service/type/next'; import type { ApiRequestProps, ApiResponseType } from '@fastgpt/service/type/next';
import { authCert } from '@fastgpt/service/support/permission/auth/common'; import { authCert } from '@fastgpt/service/support/permission/auth/common';
import { PublishChannelEnum } from '@fastgpt/global/support/outLink/constant';
import { connectionMongo } from '@fastgpt/service/common/mongo';
export type ResponseType = { export type ResponseType = {
message: string; message: string;
retriedCount: number; retriedCount: number;
failedCount: number; failedCount: number;
shareLinkMigration: {
totalRecords: number;
updatedRecords: number;
updateResults: Array<{
operation: string;
updated: number;
}>;
};
}; };
async function handler( /**
req: ApiRequestProps, * 4.14.5
res: ApiResponseType<ResponseType> * 1. S3
): Promise<ResponseType> { * 2. share OutLink showFullText
await authCert({ req, authRoot: true }); * 3.
const queue = getQueue<S3MQJobData>(QueueNames.s3FileDelete); * - showNodeStatus -> showRunningStatus
* - responseDetail -> showCite
* - showRawSource -> canDownloadSource
*/
// Get all failed jobs and retry them /**
* 功能1: 重试所有失败的 S3
*/
async function retryFailedS3DeleteJobs(): Promise<{
retriedCount: number;
failedCount: number;
}> {
const queue = getQueue<S3MQJobData>(QueueNames.s3FileDelete);
const failedJobs = await queue.getFailed(); const failedJobs = await queue.getFailed();
console.log(`Found ${failedJobs.length} failed jobs`); console.log(`Found ${failedJobs.length} failed S3 delete jobs`);
let retriedCount = 0; let retriedCount = 0;
await batchRun( await batchRun(
failedJobs, failedJobs,
async (job) => { async (job) => {
addLog.debug(`Retrying job with 3 new attempts`, { retriedCount }); addLog.debug(`Retrying S3 delete job with new attempts`, { retriedCount });
try { try {
// Remove old job and recreate with new attempts // Remove old job and recreate with new attempts
const jobData = job.data; const jobData = job.data;
await job.remove(); await job.remove();
// Add new job with 3 more attempts // Add new job with more attempts
await queue.add('delete-s3-files', jobData, { await queue.add('delete-s3-files', jobData, {
attempts: 10, attempts: 10,
removeOnFail: { removeOnFail: {
@ -49,19 +69,217 @@ async function handler(
}); });
retriedCount++; retriedCount++;
console.log(`Retried job ${job.id} with 3 new attempts`); console.log(`Retried S3 delete job ${job.id} with new attempts`);
} catch (error) { } catch (error) {
console.error(`Failed to retry job ${job.id}:`, error); console.error(`Failed to retry S3 delete job ${job.id}:`, error);
} }
}, },
100 100
); );
return { return {
message: 'Successfully retried all failed S3 delete jobs with 3 new attempts',
retriedCount, retriedCount,
failedCount: failedJobs.length failedCount: failedJobs.length
}; };
} }
/**
* 功能2和3: 处理 OutLink
* - showFullText
* -
*/
async function migrateOutLinkData(): Promise<{
totalRecords: number;
updatedRecords: number;
updateResults: Array<{
operation: string;
updated: number;
}>;
}> {
let totalUpdated = 0;
const updateResults: Array<{
operation: string;
updated: number;
}> = [];
// 获取 MongoDB 原生集合,绕过 Mongoose 的严格模式
const db = connectionMongo.connection.db;
if (!db) {
throw new Error('Database connection not established');
}
const outLinkCollection = db.collection('outlinks');
// 1. 为所有 share 类型的记录添加 showFullText 字段
const shareLinks = await outLinkCollection
.find({
type: PublishChannelEnum.share,
showFullText: { $exists: false } // 只查找没有 showFullText 字段的记录
})
.toArray();
if (shareLinks.length > 0) {
// 批量更新添加 showFullText 字段
const showFullTextOps = shareLinks.map((link: any) => ({
updateOne: {
filter: { _id: link._id },
update: { $set: { showFullText: link.showRawSource ?? true } }
}
}));
const showFullTextResult = await outLinkCollection.bulkWrite(showFullTextOps);
totalUpdated += showFullTextResult.modifiedCount;
updateResults.push({
operation: 'Add showFullText field',
updated: showFullTextResult.modifiedCount
});
console.log(`Added showFullText field to ${showFullTextResult.modifiedCount} share links`);
}
// 2. 重命名字段showNodeStatus -> showRunningStatus
const showNodeStatusLinks = await outLinkCollection
.find({
showNodeStatus: { $exists: true },
showRunningStatus: { $exists: false }
})
.toArray();
if (showNodeStatusLinks.length > 0) {
const renameNodeStatusOps = showNodeStatusLinks.map((link: any) => ({
updateOne: {
filter: { _id: link._id },
update: [
{
$set: { showRunningStatus: '$showNodeStatus' }
},
{
$unset: 'showNodeStatus'
}
]
}
}));
const renameNodeStatusResult = await outLinkCollection.bulkWrite(renameNodeStatusOps);
totalUpdated += renameNodeStatusResult.modifiedCount;
updateResults.push({
operation: 'Rename showNodeStatus to showRunningStatus',
updated: renameNodeStatusResult.modifiedCount
});
console.log(
`Renamed showNodeStatus to showRunningStatus for ${renameNodeStatusResult.modifiedCount} links`
);
}
// 3. 重命名字段responseDetail -> showCite
const responseDetailLinks = await outLinkCollection
.find({
responseDetail: { $exists: true },
showCite: { $exists: false }
})
.toArray();
if (responseDetailLinks.length > 0) {
const renameResponseDetailOps = responseDetailLinks.map((link: any) => ({
updateOne: {
filter: { _id: link._id },
update: [
{
$set: { showCite: '$responseDetail' }
},
{
$unset: 'responseDetail'
}
]
}
}));
const renameResponseDetailResult = await outLinkCollection.bulkWrite(renameResponseDetailOps);
totalUpdated += renameResponseDetailResult.modifiedCount;
updateResults.push({
operation: 'Rename responseDetail to showCite',
updated: renameResponseDetailResult.modifiedCount
});
console.log(
`Renamed responseDetail to showCite for ${renameResponseDetailResult.modifiedCount} links`
);
}
// 4. 重命名字段showRawSource -> canDownloadSource
const showRawSourceLinks = await outLinkCollection
.find({
showRawSource: { $exists: true },
canDownloadSource: { $exists: false }
})
.toArray();
if (showRawSourceLinks.length > 0) {
const renameRawSourceOps = showRawSourceLinks.map((link: any) => ({
updateOne: {
filter: { _id: link._id },
update: [
{
$set: { canDownloadSource: '$showRawSource' }
},
{
$unset: 'showRawSource'
}
]
}
}));
const renameRawSourceResult = await outLinkCollection.bulkWrite(renameRawSourceOps);
totalUpdated += renameRawSourceResult.modifiedCount;
updateResults.push({
operation: 'Rename showRawSource to canDownloadSource',
updated: renameRawSourceResult.modifiedCount
});
console.log(
`Renamed showRawSource to canDownloadSource for ${renameRawSourceResult.modifiedCount} links`
);
}
return {
totalRecords: totalUpdated,
updatedRecords: totalUpdated,
updateResults
};
}
/**
*
*/
async function handler(
req: ApiRequestProps,
_res: ApiResponseType<ResponseType>
): Promise<ResponseType> {
await authCert({ req, authRoot: true });
// 执行功能1: 重试 S3 删除任务
const s3JobResult = await retryFailedS3DeleteJobs();
// 执行功能2&3: OutLink 数据迁移
let shareLinkMigration = {
totalRecords: 0,
updatedRecords: 0,
updateResults: [] as Array<{ operation: string; updated: number }>
};
try {
shareLinkMigration = await migrateOutLinkData();
} catch (error) {
console.error('Failed to migrate outLink data:', error);
// 即使迁移失败,也继续返回 S3 任务处理的结果
}
return {
message: `Completed v4.14.5 initialization: S3 job retries and outLink migration`,
retriedCount: s3JobResult.retriedCount,
failedCount: s3JobResult.failedCount,
shareLinkMigration
};
}
export default NextAPI(handler); export default NextAPI(handler);

View File

@ -33,6 +33,7 @@ import { isS3ObjectKey } from '@fastgpt/service/common/s3/utils';
import { MongoAppTemplate } from '@fastgpt/service/core/app/templates/templateSchema'; import { MongoAppTemplate } from '@fastgpt/service/core/app/templates/templateSchema';
import { getNanoid } from '@fastgpt/global/common/string/tools'; import { getNanoid } from '@fastgpt/global/common/string/tools';
import path from 'node:path'; import path from 'node:path';
import { updateParentFoldersUpdateTime } from '@fastgpt/service/core/app/controller';
export type CreateAppBody = { export type CreateAppBody = {
parentId?: ParentIdType; parentId?: ParentIdType;
@ -243,6 +244,10 @@ export const onCreateApp = async ({
await getS3AvatarSource().refreshAvatar(_avatar, undefined, session); await getS3AvatarSource().refreshAvatar(_avatar, undefined, session);
updateParentFoldersUpdateTime({
parentId
});
(async () => { (async () => {
addAuditLog({ addAuditLog({
tmbId, tmbId,

View File

@ -10,6 +10,7 @@ import { MongoApp } from '@fastgpt/service/core/app/schema';
import type { StoreSecretValueType } from '@fastgpt/global/common/secret/type'; import type { StoreSecretValueType } from '@fastgpt/global/common/secret/type';
import { storeSecretValue } from '@fastgpt/service/common/secret/utils'; import { storeSecretValue } from '@fastgpt/service/common/secret/utils';
import { MongoAppVersion } from '@fastgpt/service/core/app/version/schema'; import { MongoAppVersion } from '@fastgpt/service/core/app/version/schema';
import { updateParentFoldersUpdateTime } from '@fastgpt/service/core/app/controller';
export type UpdateHttpPluginBody = { export type UpdateHttpPluginBody = {
appId: string; appId: string;
@ -50,6 +51,7 @@ async function handler(req: ApiRequestProps<UpdateHttpPluginBody>, res: NextApiR
}, },
{ session } { session }
); );
await MongoAppVersion.updateOne( await MongoAppVersion.updateOne(
{ appId }, { appId },
{ {
@ -60,6 +62,9 @@ async function handler(req: ApiRequestProps<UpdateHttpPluginBody>, res: NextApiR
{ session } { session }
); );
}); });
updateParentFoldersUpdateTime({
parentId: app.parentId
});
} }
export default NextAPI(handler); export default NextAPI(handler);

View File

@ -23,7 +23,6 @@ import { sumPer } from '@fastgpt/global/support/permission/utils';
export type ListAppBody = { export type ListAppBody = {
parentId?: ParentIdType; parentId?: ParentIdType;
type?: AppTypeEnum | AppTypeEnum[]; type?: AppTypeEnum | AppTypeEnum[];
getRecentlyChat?: boolean;
searchKey?: string; searchKey?: string;
}; };
@ -38,7 +37,7 @@ export type ListAppBody = {
*/ */
async function handler(req: ApiRequestProps<ListAppBody>): Promise<AppListItemType[]> { async function handler(req: ApiRequestProps<ListAppBody>): Promise<AppListItemType[]> {
const { parentId, type, getRecentlyChat, searchKey } = req.body; const { parentId, type, searchKey } = req.body;
// Auth user permission // Auth user permission
const [{ tmbId, teamId, permission: teamPer }] = await Promise.all([ const [{ tmbId, teamId, permission: teamPer }] = await Promise.all([
@ -94,14 +93,6 @@ async function handler(req: ApiRequestProps<ListAppBody>): Promise<AppListItemTy
); );
const findAppsQuery = (() => { const findAppsQuery = (() => {
if (getRecentlyChat) {
return {
// get all chat app, excluding hidden apps and deleted apps
teamId,
type: { $in: [AppTypeEnum.workflow, AppTypeEnum.simple, AppTypeEnum.workflowTool] }
};
}
// Filter apps by permission, if not owner, only get apps that I have permission to access // Filter apps by permission, if not owner, only get apps that I have permission to access
const idList = { _id: { $in: myPerList.map((item) => item.resourceId) } }; const idList = { _id: { $in: myPerList.map((item) => item.resourceId) } };
const appPerQuery = teamPer.isOwner const appPerQuery = teamPer.isOwner
@ -153,7 +144,6 @@ async function handler(req: ApiRequestProps<ListAppBody>): Promise<AppListItemTy
}; };
})(); })();
const limit = (() => { const limit = (() => {
if (getRecentlyChat) return 15;
if (searchKey) return 50; if (searchKey) return 50;
return; return;
})(); })();

View File

@ -10,6 +10,7 @@ import { getMCPToolSetRuntimeNode } from '@fastgpt/global/core/app/tool/mcpTool/
import { MongoAppVersion } from '@fastgpt/service/core/app/version/schema'; import { MongoAppVersion } from '@fastgpt/service/core/app/version/schema';
import { type StoreSecretValueType } from '@fastgpt/global/common/secret/type'; import { type StoreSecretValueType } from '@fastgpt/global/common/secret/type';
import { storeSecretValue } from '@fastgpt/service/common/secret/utils'; import { storeSecretValue } from '@fastgpt/service/common/secret/utils';
import { updateParentFoldersUpdateTime } from '@fastgpt/service/core/app/controller';
export type updateMCPToolsQuery = {}; export type updateMCPToolsQuery = {};
@ -51,6 +52,7 @@ async function handler(
}, },
{ session } { session }
); );
await MongoAppVersion.updateOne( await MongoAppVersion.updateOne(
{ appId }, { appId },
{ {
@ -61,6 +63,9 @@ async function handler(
{ session } { session }
); );
}); });
updateParentFoldersUpdateTime({
parentId: app.parentId
});
return {}; return {};
} }

View File

@ -27,6 +27,7 @@ import { AuditEventEnum } from '@fastgpt/global/support/user/audit/constants';
import { getI18nAppType } from '@fastgpt/service/support/user/audit/util'; import { getI18nAppType } from '@fastgpt/service/support/user/audit/util';
import { i18nT } from '@fastgpt/web/i18n/utils'; import { i18nT } from '@fastgpt/web/i18n/utils';
import { getS3AvatarSource } from '@fastgpt/service/common/s3/sources/avatar'; import { getS3AvatarSource } from '@fastgpt/service/common/s3/sources/avatar';
import { updateParentFoldersUpdateTime } from '@fastgpt/service/core/app/controller';
export type AppUpdateQuery = { export type AppUpdateQuery = {
appId: string; appId: string;
@ -117,7 +118,7 @@ async function handler(req: ApiRequestProps<AppUpdateBody, AppUpdateQuery>) {
await getS3AvatarSource().refreshAvatar(avatar, app.avatar, session); await getS3AvatarSource().refreshAvatar(avatar, app.avatar, session);
return MongoApp.findByIdAndUpdate( const result = await MongoApp.findByIdAndUpdate(
appId, appId,
{ {
...parseParentIdInMongo(parentId), ...parseParentIdInMongo(parentId),
@ -133,10 +134,28 @@ async function handler(req: ApiRequestProps<AppUpdateBody, AppUpdateQuery>) {
edges edges
}), }),
...(chatConfig && { chatConfig }), ...(chatConfig && { chatConfig }),
...(isMove && { inheritPermission: true }) ...(isMove && { inheritPermission: true }),
updateTime: new Date()
}, },
{ session } { session }
); );
if (isMove) {
// Update both old and new parent folders
updateParentFoldersUpdateTime({
parentId: app.parentId
});
updateParentFoldersUpdateTime({
parentId
});
} else {
// Update current parent folder
updateParentFoldersUpdateTime({
parentId: parentId || app.parentId
});
}
return result;
}; };
// Move // Move

View File

@ -13,6 +13,7 @@ import { addAuditLog } from '@fastgpt/service/support/user/audit/util';
import { AuditEventEnum } from '@fastgpt/global/support/user/audit/constants'; import { AuditEventEnum } from '@fastgpt/global/support/user/audit/constants';
import { getI18nAppType } from '@fastgpt/service/support/user/audit/util'; import { getI18nAppType } from '@fastgpt/service/support/user/audit/util';
import { i18nT } from '@fastgpt/web/i18n/utils'; import { i18nT } from '@fastgpt/web/i18n/utils';
import { updateParentFoldersUpdateTime } from '@fastgpt/service/core/app/controller';
async function handler(req: ApiRequestProps<PostPublishAppProps>, res: NextApiResponse<any>) { async function handler(req: ApiRequestProps<PostPublishAppProps>, res: NextApiResponse<any>) {
const { appId } = req.query as { appId: string }; const { appId } = req.query as { appId: string };
@ -28,6 +29,9 @@ async function handler(req: ApiRequestProps<PostPublishAppProps>, res: NextApiRe
beforeUpdateAppFormat({ beforeUpdateAppFormat({
nodes nodes
}); });
updateParentFoldersUpdateTime({
parentId: app.parentId
});
if (autoSave) { if (autoSave) {
await mongoSessionRun(async (session) => { await mongoSessionRun(async (session) => {

View File

@ -248,7 +248,6 @@ async function handler(req: NextApiRequest, res: NextApiResponse) {
nodes, nodes,
appChatConfig: chatConfig, appChatConfig: chatConfig,
variables: newVariables, variables: newVariables,
isUpdateUseTime: false, // owner update use time
newTitle, newTitle,
source: ChatSourceEnum.test, source: ChatSourceEnum.test,
userContent: userQuestion, userContent: userQuestion,

View File

@ -40,7 +40,7 @@ async function handler(
}; };
} }
const [app, { responseDetail, showNodeStatus, authType }] = await Promise.all([ const [app, { showCite, showRunningStatus, authType }] = await Promise.all([
MongoApp.findById(appId, 'type').lean(), MongoApp.findById(appId, 'type').lean(),
authChatCrud({ authChatCrud({
req, req,
@ -81,16 +81,16 @@ async function handler(
if (item.obj === ChatRoleEnum.AI) { if (item.obj === ChatRoleEnum.AI) {
item.responseData = filterPublicNodeResponseData({ item.responseData = filterPublicNodeResponseData({
nodeRespones: item.responseData, nodeRespones: item.responseData,
responseDetail responseDetail: showCite
}); });
if (showNodeStatus === false) { if (showRunningStatus === false) {
item.value = item.value.filter((v) => v.type !== ChatItemValueTypeEnum.tool); item.value = item.value.filter((v) => v.type !== ChatItemValueTypeEnum.tool);
} }
} }
}); });
} }
if (!responseDetail) { if (!showCite) {
histories.forEach((item) => { histories.forEach((item) => {
if (item.obj === ChatRoleEnum.AI) { if (item.obj === ChatRoleEnum.AI) {
item.value = removeAIResponseCite(item.value, false); item.value = removeAIResponseCite(item.value, false);
@ -99,7 +99,7 @@ async function handler(
} }
return { return {
list: isPlugin ? histories : transformPreviewHistories(histories, responseDetail), list: isPlugin ? histories : transformPreviewHistories(histories, showCite),
total total
}; };
} }

View File

@ -50,7 +50,7 @@ async function handler(
}; };
} }
const [app, { responseDetail, showNodeStatus, authType }] = await Promise.all([ const [app, { showCite, showRunningStatus, authType }] = await Promise.all([
MongoApp.findById(appId, 'type').lean(), MongoApp.findById(appId, 'type').lean(),
authChatCrud({ authChatCrud({
req, req,
@ -93,16 +93,16 @@ async function handler(
if (item.obj === ChatRoleEnum.AI) { if (item.obj === ChatRoleEnum.AI) {
item.responseData = filterPublicNodeResponseData({ item.responseData = filterPublicNodeResponseData({
nodeRespones: item.responseData, nodeRespones: item.responseData,
responseDetail responseDetail: showCite
}); });
if (showNodeStatus === false) { if (showRunningStatus === false) {
item.value = item.value.filter((v) => v.type !== ChatItemValueTypeEnum.tool); item.value = item.value.filter((v) => v.type !== ChatItemValueTypeEnum.tool);
} }
} }
}); });
} }
if (!responseDetail) { if (!showCite) {
result.histories.forEach((item) => { result.histories.forEach((item) => {
if (item.obj === ChatRoleEnum.AI) { if (item.obj === ChatRoleEnum.AI) {
item.value = removeAIResponseCite(item.value, false); item.value = removeAIResponseCite(item.value, false);
@ -110,9 +110,7 @@ async function handler(
}); });
} }
const list = isPlugin const list = isPlugin ? result.histories : transformPreviewHistories(result.histories, showCite);
? result.histories
: transformPreviewHistories(result.histories, responseDetail);
return { return {
list: list.map((item) => ({ list: list.map((item) => ({

View File

@ -27,7 +27,7 @@ async function handler(
return []; return [];
} }
const [{ responseDetail }, chatData, nodeResponses] = await Promise.all([ const [{ showCite }, chatData, nodeResponses] = await Promise.all([
authChatCrud({ authChatCrud({
req, req,
authToken: true, authToken: true,
@ -57,7 +57,7 @@ async function handler(
const flowResponses = chatData.responseData?.length ? chatData.responseData : nodeResponses; const flowResponses = chatData.responseData?.length ? chatData.responseData : nodeResponses;
return req.query.shareId return req.query.shareId
? filterPublicNodeResponseData({ ? filterPublicNodeResponseData({
responseDetail, responseDetail: showCite,
nodeRespones: flowResponses nodeRespones: flowResponses
}) })
: flowResponses; : flowResponses;

View File

@ -11,6 +11,9 @@ import { NextAPI } from '@/service/middleware/entry';
import { ReadPermissionVal } from '@fastgpt/global/support/permission/constant'; import { ReadPermissionVal } from '@fastgpt/global/support/permission/constant';
import { FlowNodeTypeEnum } from '@fastgpt/global/core/workflow/node/constant'; import { FlowNodeTypeEnum } from '@fastgpt/global/core/workflow/node/constant';
import { presignVariablesFileUrls } from '@fastgpt/service/core/chat/utils'; import { presignVariablesFileUrls } from '@fastgpt/service/core/chat/utils';
import { MongoAppRecord } from '@fastgpt/service/core/app/record/schema';
import { AppErrEnum } from '@fastgpt/global/common/error/code/app';
import { authCert } from '@fastgpt/service/support/permission/auth/common';
async function handler( async function handler(
req: NextApiRequest, req: NextApiRequest,
@ -25,57 +28,74 @@ async function handler(
}); });
} }
// auth app permission try {
const [{ app, tmbId }, chat] = await Promise.all([ // auth app permission
authApp({ const [{ app, tmbId }, chat] = await Promise.all([
req, authApp({
authToken: true, req,
authApiKey: true, authToken: true,
appId, authApiKey: true,
per: ReadPermissionVal appId,
}), per: ReadPermissionVal
chatId ? MongoChat.findOne({ appId, chatId }) : undefined
]);
// auth chat permission
if (chat && !app.permission.hasReadChatLogPer && String(tmbId) !== String(chat?.tmbId)) {
return Promise.reject(ChatErrEnum.unAuthChat);
}
// get app and history
const { nodes, chatConfig } = await getAppLatestVersion(app._id, app);
const pluginInputs =
chat?.pluginInputs ??
nodes?.find((node) => node.flowNodeType === FlowNodeTypeEnum.pluginInput)?.inputs ??
[];
const variables = await presignVariablesFileUrls({
variables: chat?.variables,
variableConfig: chat?.variableList
});
return {
chatId,
appId,
title: chat?.title,
userAvatar: undefined,
variables,
app: {
chatConfig: getAppChatConfig({
chatConfig,
systemConfigNode: getGuideModule(nodes),
storeVariables: chat?.variableList,
storeWelcomeText: chat?.welcomeText,
isPublicFetch: false
}), }),
chatModels: getChatModelNameListByModules(nodes), chatId ? MongoChat.findOne({ appId, chatId }) : undefined
name: app.name, ]);
avatar: app.avatar,
intro: app.intro, // auth chat permission
type: app.type, if (chat && !app.permission.hasReadChatLogPer && String(tmbId) !== String(chat?.tmbId)) {
pluginInputs return Promise.reject(ChatErrEnum.unAuthChat);
} }
};
// get app and history
const { nodes, chatConfig } = await getAppLatestVersion(app._id, app);
const pluginInputs =
chat?.pluginInputs ??
nodes?.find((node) => node.flowNodeType === FlowNodeTypeEnum.pluginInput)?.inputs ??
[];
const variables = await presignVariablesFileUrls({
variables: chat?.variables,
variableConfig: chat?.variableList
});
return {
chatId,
appId,
title: chat?.title,
userAvatar: undefined,
variables,
app: {
chatConfig: getAppChatConfig({
chatConfig,
systemConfigNode: getGuideModule(nodes),
storeVariables: chat?.variableList,
storeWelcomeText: chat?.welcomeText,
isPublicFetch: false
}),
chatModels: getChatModelNameListByModules(nodes),
name: app.name,
avatar: app.avatar,
intro: app.intro,
type: app.type,
pluginInputs
}
};
} catch (error: any) {
if (error === AppErrEnum.unAuthApp && appId) {
const { tmbId } = await authCert({
req,
authToken: true,
authApiKey: true
});
await MongoAppRecord.deleteOne({
tmbId,
appId
});
}
return Promise.reject(error);
}
} }
export default NextAPI(handler); export default NextAPI(handler);

View File

@ -2,7 +2,7 @@ import type { ApiRequestProps } from '@fastgpt/service/type/next';
import { NextAPI } from '@/service/middleware/entry'; import { NextAPI } from '@/service/middleware/entry';
import { getS3ChatSource } from '@fastgpt/service/common/s3/sources/chat'; import { getS3ChatSource } from '@fastgpt/service/common/s3/sources/chat';
import { authChatCrud } from '@/service/support/permission/auth/chat'; import { authChatCrud } from '@/service/support/permission/auth/chat';
import type { PresignChatFileGetUrlParams } from '@fastgpt/global/openapi/core/chat/api'; import type { PresignChatFileGetUrlParams } from '@fastgpt/global/openapi/core/chat/controler/api';
async function handler(req: ApiRequestProps<PresignChatFileGetUrlParams>): Promise<string> { async function handler(req: ApiRequestProps<PresignChatFileGetUrlParams>): Promise<string> {
const { key, appId, outLinkAuthData } = req.body; const { key, appId, outLinkAuthData } = req.body;

View File

@ -5,7 +5,7 @@ import { getS3ChatSource } from '@fastgpt/service/common/s3/sources/chat';
import { authChatCrud } from '@/service/support/permission/auth/chat'; import { authChatCrud } from '@/service/support/permission/auth/chat';
import { authFrequencyLimit } from '@/service/common/frequencyLimit/api'; import { authFrequencyLimit } from '@/service/common/frequencyLimit/api';
import { addSeconds } from 'date-fns'; import { addSeconds } from 'date-fns';
import type { PresignChatFilePostUrlParams } from '@fastgpt/global/openapi/core/chat/api'; import type { PresignChatFilePostUrlParams } from '@fastgpt/global/openapi/core/chat/controler/api';
const authUploadLimit = (tmbId: string) => { const authUploadLimit = (tmbId: string) => {
if (!global.feConfigs.uploadFileMaxAmount) return; if (!global.feConfigs.uploadFileMaxAmount) return;

View File

@ -57,7 +57,7 @@ async function handler(
const limitedPageSize = Math.min(pageSize, 30); const limitedPageSize = Math.min(pageSize, 30);
const [collection, { chat, showRawSource }, chatItem] = await Promise.all([ const [collection, { chat, showFullText }, chatItem] = await Promise.all([
getCollectionWithDataset(collectionId), getCollectionWithDataset(collectionId),
authChatCrud({ authChatCrud({
req, req,
@ -73,7 +73,7 @@ async function handler(
authCollectionInChat({ appId, chatId, chatItemDataId, collectionIds: [collectionId] }) authCollectionInChat({ appId, chatId, chatItemDataId, collectionIds: [collectionId] })
]); ]);
if (!showRawSource || !chat || !chatItem || initialAnchor === undefined) { if (!showFullText || !chat || !chatItem || initialAnchor === undefined) {
return Promise.reject(ChatErrEnum.unAuthChat); return Promise.reject(ChatErrEnum.unAuthChat);
} }

View File

@ -39,7 +39,7 @@ async function handler(req: ApiRequestProps<GetQuoteProps>): Promise<GetQuotesRe
datasetDataIdList datasetDataIdList
} = req.body; } = req.body;
const [{ chat, responseDetail }, chatItem] = await Promise.all([ const [{ chat, showCite }, chatItem] = await Promise.all([
authChatCrud({ authChatCrud({
req, req,
authToken: true, authToken: true,
@ -53,7 +53,7 @@ async function handler(req: ApiRequestProps<GetQuoteProps>): Promise<GetQuotesRe
MongoChatItem.findOne({ appId, chatId, dataId: chatItemDataId }, 'time').lean(), MongoChatItem.findOne({ appId, chatId, dataId: chatItemDataId }, 'time').lean(),
authCollectionInChat({ appId, chatId, chatItemDataId, collectionIds: collectionIdList }) authCollectionInChat({ appId, chatId, chatItemDataId, collectionIds: collectionIdList })
]); ]);
if (!chat || !chatItem || !responseDetail) return Promise.reject(ChatErrEnum.unAuthChat); if (!chat || !chatItem || !showCite) return Promise.reject(ChatErrEnum.unAuthChat);
const list = await MongoDatasetData.find( const list = await MongoDatasetData.find(
{ _id: { $in: datasetDataIdList }, collectionId: { $in: collectionIdList } }, { _id: { $in: datasetDataIdList }, collectionId: { $in: collectionIdList } },

View File

@ -0,0 +1,43 @@
import type { ApiRequestProps, ApiResponseType } from '@fastgpt/service/type/next';
import { NextAPI } from '@/service/middleware/entry';
import { authUserPer } from '@fastgpt/service/support/permission/user/auth';
import { MongoAppRecord } from '@fastgpt/service/core/app/record/schema';
import { MongoApp } from '@fastgpt/service/core/app/schema';
import type { GetRecentlyUsedAppsResponseType } from '@fastgpt/global/openapi/core/chat/api';
async function handler(
req: ApiRequestProps<{}, {}>,
_res: ApiResponseType
): Promise<GetRecentlyUsedAppsResponseType> {
const { tmbId } = await authUserPer({
req,
authToken: true,
authApiKey: true
});
const recentRecords = await MongoAppRecord.find(
{ tmbId },
{ appId: 1 },
{ sort: { lastUsedTime: -1 }, limit: 20 }
).lean();
if (!recentRecords.length) return [];
const apps = await MongoApp.find(
{ _id: { $in: recentRecords.map((record) => record.appId) } },
'_id name avatar'
).lean();
const appMap = new Map(apps.map((app) => [String(app._id), app]));
return recentRecords
.map((record) => appMap.get(String(record.appId)))
.filter((app) => app != null)
.map((app) => ({
appId: String(app._id),
name: app.name,
avatar: app.avatar
}));
}
export default NextAPI(handler);

View File

@ -67,7 +67,7 @@ async function handler(req: ApiRequestProps<ExportCollectionBody, {}>, res: Next
authCollectionInChat({ appId, chatId, chatItemDataId, collectionIds: [collectionId] }) authCollectionInChat({ appId, chatId, chatItemDataId, collectionIds: [collectionId] })
]); ]);
if (!authRes.showRawSource) { if (!authRes.canDownloadSource) {
return Promise.reject(DatasetErrEnum.unAuthDatasetFile); return Promise.reject(DatasetErrEnum.unAuthDatasetFile);
} }

View File

@ -63,7 +63,7 @@ async function handler(
authCollectionInChat({ appId, chatId, chatItemDataId, collectionIds: [collectionId] }) authCollectionInChat({ appId, chatId, chatItemDataId, collectionIds: [collectionId] })
]); ]);
if (!authRes.showRawSource) { if (!authRes.canDownloadSource) {
return Promise.reject(DatasetErrEnum.unAuthDatasetFile); return Promise.reject(DatasetErrEnum.unAuthDatasetFile);
} }

View File

@ -51,7 +51,7 @@ async function handler(req: ApiRequestProps<GetQuoteDataProps>): Promise<GetQuot
return Promise.reject(i18nT('common:data_not_found')); return Promise.reject(i18nT('common:data_not_found'));
} }
const [collection, { responseDetail }] = await Promise.all([ const [collection, { showCite }] = await Promise.all([
MongoDatasetCollection.findById(datasetData.collectionId).lean(), MongoDatasetCollection.findById(datasetData.collectionId).lean(),
authChatCrud({ authChatCrud({
req, req,
@ -73,7 +73,7 @@ async function handler(req: ApiRequestProps<GetQuoteDataProps>): Promise<GetQuot
if (!collection) { if (!collection) {
return Promise.reject('Can not find the collection'); return Promise.reject('Can not find the collection');
} }
if (!responseDetail) { if (!showCite) {
return Promise.reject(ChatErrEnum.unAuthChat); return Promise.reject(ChatErrEnum.unAuthChat);
} }

View File

@ -0,0 +1,42 @@
import { MongoOutLink } from '@fastgpt/service/support/outLink/schema';
import { authApp } from '@fastgpt/service/support/permission/app/auth';
import { PublishChannelEnum } from '@fastgpt/global/support/outLink/constant';
import { WritePermissionVal } from '@fastgpt/global/support/permission/constant';
import type { ApiRequestProps } from '@fastgpt/service/type/next';
import { NextAPI } from '@/service/middleware/entry';
import {
type PlaygroundVisibilityConfigQuery,
type PlaygroundVisibilityConfigResponse,
PlaygroundVisibilityConfigQuerySchema,
PlaygroundVisibilityConfigResponseSchema
} from '@fastgpt/global/support/outLink/api.d';
async function handler(
req: ApiRequestProps<{}, PlaygroundVisibilityConfigQuery>
): Promise<PlaygroundVisibilityConfigResponse> {
const { appId } = PlaygroundVisibilityConfigQuerySchema.parse(req.query);
await authApp({
req,
authToken: true,
appId,
per: WritePermissionVal
});
const existingConfig = await MongoOutLink.findOne(
{
appId,
type: PublishChannelEnum.playground
},
'showRunningStatus showCite showFullText canDownloadSource'
).lean();
return PlaygroundVisibilityConfigResponseSchema.parse({
showRunningStatus: existingConfig?.showRunningStatus ?? true,
showCite: existingConfig?.showCite ?? true,
showFullText: existingConfig?.showFullText ?? true,
canDownloadSource: existingConfig?.canDownloadSource ?? true
});
}
export default NextAPI(handler);

View File

@ -0,0 +1,45 @@
import { MongoOutLink } from '@fastgpt/service/support/outLink/schema';
import { authApp } from '@fastgpt/service/support/permission/app/auth';
import { PublishChannelEnum } from '@fastgpt/global/support/outLink/constant';
import { WritePermissionVal } from '@fastgpt/global/support/permission/constant';
import type { ApiRequestProps } from '@fastgpt/service/type/next';
import { NextAPI } from '@/service/middleware/entry';
import {
type UpdatePlaygroundVisibilityConfigBody,
UpdatePlaygroundVisibilityConfigBodySchema
} from '@fastgpt/global/support/outLink/api.d';
async function handler(req: ApiRequestProps<UpdatePlaygroundVisibilityConfigBody, {}>) {
const { appId, showRunningStatus, showCite, showFullText, canDownloadSource } =
UpdatePlaygroundVisibilityConfigBodySchema.parse(req.body);
const { teamId, tmbId } = await authApp({
req,
authToken: true,
appId,
per: WritePermissionVal
});
await MongoOutLink.updateOne(
{ appId, type: PublishChannelEnum.playground },
{
$setOnInsert: {
shareId: `playground-${appId}`,
teamId,
tmbId,
name: 'Playground Chat'
},
$set: {
appId,
type: PublishChannelEnum.playground,
showRunningStatus: showRunningStatus,
showCite: showCite,
showFullText: showFullText,
canDownloadSource: canDownloadSource
}
},
{ upsert: true }
);
}
export default NextAPI(handler);

View File

@ -26,7 +26,8 @@ export type OutLinkUpdateResponse = string;
async function handler( async function handler(
req: ApiRequestProps<OutLinkUpdateBody, OutLinkUpdateQuery> req: ApiRequestProps<OutLinkUpdateBody, OutLinkUpdateQuery>
): Promise<OutLinkUpdateResponse> { ): Promise<OutLinkUpdateResponse> {
const { _id, name, responseDetail, limit, app, showRawSource, showNodeStatus } = req.body; const { _id, name, showCite, limit, app, canDownloadSource, showRunningStatus, showFullText } =
req.body;
if (!_id) { if (!_id) {
return Promise.reject(CommonErrEnum.missingParams); return Promise.reject(CommonErrEnum.missingParams);
@ -46,10 +47,10 @@ async function handler(
const doc = await MongoOutLink.findByIdAndUpdate(_id, { const doc = await MongoOutLink.findByIdAndUpdate(_id, {
name, name,
responseDetail, showCite,
showRawSource, canDownloadSource,
showNodeStatus, showRunningStatus,
// showFullText, showFullText,
limit, limit,
app app
}); });

View File

@ -27,6 +27,7 @@ import {
} from '@fastgpt/service/core/chat/saveChat'; } from '@fastgpt/service/core/chat/saveChat';
import { responseWrite } from '@fastgpt/service/common/response'; import { responseWrite } from '@fastgpt/service/common/response';
import { authOutLinkChatStart } from '@/service/support/permission/auth/outLink'; import { authOutLinkChatStart } from '@/service/support/permission/auth/outLink';
import { recordAppUsage } from '@fastgpt/service/core/app/record/utils';
import { pushResult2Remote, addOutLinkUsage } from '@fastgpt/service/support/outLink/tools'; import { pushResult2Remote, addOutLinkUsage } from '@fastgpt/service/support/outLink/tools';
import { getUsageSourceByAuthType } from '@fastgpt/global/support/wallet/usage/tools'; import { getUsageSourceByAuthType } from '@fastgpt/global/support/wallet/usage/tools';
import { authTeamSpaceToken } from '@/service/support/permission/auth/team'; import { authTeamSpaceToken } from '@/service/support/permission/auth/team';
@ -91,8 +92,8 @@ type AuthResponseType = {
teamId: string; teamId: string;
tmbId: string; tmbId: string;
app: AppSchema; app: AppSchema;
responseDetail?: boolean; showCite?: boolean;
showNodeStatus?: boolean; showRunningStatus?: boolean;
authType: `${AuthUserTypeEnum}`; authType: `${AuthUserTypeEnum}`;
apikey?: string; apikey?: string;
responseAllData: boolean; responseAllData: boolean;
@ -157,13 +158,13 @@ async function handler(req: NextApiRequest, res: NextApiResponse) {
teamId, teamId,
tmbId, tmbId,
app, app,
responseDetail, showCite,
authType, authType,
sourceName, sourceName,
apikey, apikey,
responseAllData, responseAllData,
outLinkUserId = customUid, outLinkUserId = customUid,
showNodeStatus showRunningStatus
} = await (async () => { } = await (async () => {
// share chat // share chat
if (shareId && outLinkUid) { if (shareId && outLinkUid) {
@ -205,7 +206,7 @@ async function handler(req: NextApiRequest, res: NextApiResponse) {
pushTrack.teamChatQPM({ teamId }); pushTrack.teamChatQPM({ teamId });
retainDatasetCite = retainDatasetCite && !!responseDetail; retainDatasetCite = retainDatasetCite && !!showCite;
const isPlugin = app.type === AppTypeEnum.workflowTool; const isPlugin = app.type === AppTypeEnum.workflowTool;
// Check message type // Check message type
@ -275,7 +276,7 @@ async function handler(req: NextApiRequest, res: NextApiResponse) {
detail, detail,
streamResponse: stream, streamResponse: stream,
id: chatId, id: chatId,
showNodeStatus showNodeStatus: showRunningStatus
}); });
const saveChatId = chatId || getNanoid(24); const saveChatId = chatId || getNanoid(24);
@ -326,7 +327,6 @@ async function handler(req: NextApiRequest, res: NextApiResponse) {
})(); })();
// save chat // save chat
const isOwnerUse = !shareId && !spaceTeamId && String(tmbId) === String(app.tmbId);
const source = (() => { const source = (() => {
if (shareId) { if (shareId) {
return ChatSourceEnum.share; return ChatSourceEnum.share;
@ -363,7 +363,6 @@ async function handler(req: NextApiRequest, res: NextApiResponse) {
nodes, nodes,
appChatConfig: chatConfig, appChatConfig: chatConfig,
variables: newVariables, variables: newVariables,
isUpdateUseTime: isOwnerUse && source === ChatSourceEnum.online, // owner update use time
newTitle, newTitle,
shareId, shareId,
outLinkUid: outLinkUserId, outLinkUid: outLinkUserId,
@ -383,12 +382,21 @@ async function handler(req: NextApiRequest, res: NextApiResponse) {
await saveChat(params); await saveChat(params);
} }
const isOwnerUse = !shareId && !spaceTeamId && String(tmbId) === String(app.tmbId);
if (isOwnerUse && source === ChatSourceEnum.online) {
await recordAppUsage({
appId: app._id,
tmbId,
teamId
});
}
addLog.info(`completions running time: ${(Date.now() - startTime) / 1000}s`); addLog.info(`completions running time: ${(Date.now() - startTime) / 1000}s`);
/* select fe response field */ /* select fe response field */
const feResponseData = responseAllData const feResponseData = responseAllData
? flowResponses ? flowResponses
: filterPublicNodeResponseData({ nodeRespones: flowResponses, responseDetail }); : filterPublicNodeResponseData({ nodeRespones: flowResponses, responseDetail: showCite });
if (stream) { if (stream) {
workflowResponseWrite({ workflowResponseWrite({
@ -508,7 +516,7 @@ const authShareChat = async ({
shareId: string; shareId: string;
chatId?: string; chatId?: string;
}): Promise<AuthResponseType> => { }): Promise<AuthResponseType> => {
const { teamId, tmbId, appId, authType, responseDetail, showNodeStatus, uid, sourceName } = const { teamId, tmbId, appId, authType, showCite, showRunningStatus, uid, sourceName } =
await authOutLinkChatStart(data); await authOutLinkChatStart(data);
const app = await MongoApp.findById(appId).lean(); const app = await MongoApp.findById(appId).lean();
@ -530,9 +538,9 @@ const authShareChat = async ({
apikey: '', apikey: '',
authType, authType,
responseAllData: false, responseAllData: false,
responseDetail, showCite,
outLinkUserId: uid, outLinkUserId: uid,
showNodeStatus showRunningStatus
}; };
}; };
const authTeamSpaceChat = async ({ const authTeamSpaceChat = async ({
@ -569,7 +577,7 @@ const authTeamSpaceChat = async ({
authType: AuthUserTypeEnum.outLink, authType: AuthUserTypeEnum.outLink,
apikey: '', apikey: '',
responseAllData: false, responseAllData: false,
responseDetail: true, showCite: true,
outLinkUserId: uid outLinkUserId: uid
}; };
}; };
@ -651,7 +659,7 @@ const authHeaderRequest = async ({
authType, authType,
sourceName, sourceName,
responseAllData: true, responseAllData: true,
responseDetail: true showCite: true
}; };
}; };

View File

@ -27,6 +27,7 @@ import {
} from '@fastgpt/service/core/chat/saveChat'; } from '@fastgpt/service/core/chat/saveChat';
import { responseWrite } from '@fastgpt/service/common/response'; import { responseWrite } from '@fastgpt/service/common/response';
import { authOutLinkChatStart } from '@/service/support/permission/auth/outLink'; import { authOutLinkChatStart } from '@/service/support/permission/auth/outLink';
import { recordAppUsage } from '@fastgpt/service/core/app/record/utils';
import { pushResult2Remote, addOutLinkUsage } from '@fastgpt/service/support/outLink/tools'; import { pushResult2Remote, addOutLinkUsage } from '@fastgpt/service/support/outLink/tools';
import { getUsageSourceByAuthType } from '@fastgpt/global/support/wallet/usage/tools'; import { getUsageSourceByAuthType } from '@fastgpt/global/support/wallet/usage/tools';
import { authTeamSpaceToken } from '@/service/support/permission/auth/team'; import { authTeamSpaceToken } from '@/service/support/permission/auth/team';
@ -92,8 +93,8 @@ type AuthResponseType = {
teamId: string; teamId: string;
tmbId: string; tmbId: string;
app: AppSchema; app: AppSchema;
responseDetail?: boolean; showCite?: boolean;
showNodeStatus?: boolean; showRunningStatus?: boolean;
authType: `${AuthUserTypeEnum}`; authType: `${AuthUserTypeEnum}`;
apikey?: string; apikey?: string;
responseAllData: boolean; responseAllData: boolean;
@ -158,13 +159,13 @@ async function handler(req: NextApiRequest, res: NextApiResponse) {
teamId, teamId,
tmbId, tmbId,
app, app,
responseDetail, showCite,
authType, authType,
sourceName, sourceName,
apikey, apikey,
responseAllData, responseAllData,
outLinkUserId = customUid, outLinkUserId = customUid,
showNodeStatus showRunningStatus
} = await (async () => { } = await (async () => {
// share chat // share chat
if (shareId && outLinkUid) { if (shareId && outLinkUid) {
@ -206,7 +207,7 @@ async function handler(req: NextApiRequest, res: NextApiResponse) {
pushTrack.teamChatQPM({ teamId }); pushTrack.teamChatQPM({ teamId });
retainDatasetCite = retainDatasetCite && !!responseDetail; retainDatasetCite = retainDatasetCite && !!showCite;
const isPlugin = app.type === AppTypeEnum.workflowTool; const isPlugin = app.type === AppTypeEnum.workflowTool;
// Check message type // Check message type
@ -275,7 +276,7 @@ async function handler(req: NextApiRequest, res: NextApiResponse) {
detail, detail,
streamResponse: stream, streamResponse: stream,
id: chatId, id: chatId,
showNodeStatus showNodeStatus: showRunningStatus
}); });
const saveChatId = chatId || getNanoid(24); const saveChatId = chatId || getNanoid(24);
@ -321,14 +322,13 @@ async function handler(req: NextApiRequest, res: NextApiResponse) {
maxRunTimes: WORKFLOW_MAX_RUN_TIMES, maxRunTimes: WORKFLOW_MAX_RUN_TIMES,
workflowStreamResponse: workflowResponseWrite, workflowStreamResponse: workflowResponseWrite,
responseAllData, responseAllData,
responseDetail responseDetail: showCite
}); });
} }
return Promise.reject('您的工作流版本过低,请重新发布一次'); return Promise.reject('您的工作流版本过低,请重新发布一次');
})(); })();
// save chat // save chat
const isOwnerUse = !shareId && !spaceTeamId && String(tmbId) === String(app.tmbId);
const source = (() => { const source = (() => {
if (shareId) { if (shareId) {
return ChatSourceEnum.share; return ChatSourceEnum.share;
@ -365,7 +365,6 @@ async function handler(req: NextApiRequest, res: NextApiResponse) {
nodes, nodes,
appChatConfig: chatConfig, appChatConfig: chatConfig,
variables: newVariables, variables: newVariables,
isUpdateUseTime: isOwnerUse && source === ChatSourceEnum.online, // owner update use time
newTitle, newTitle,
shareId, shareId,
outLinkUid: outLinkUserId, outLinkUid: outLinkUserId,
@ -385,12 +384,21 @@ async function handler(req: NextApiRequest, res: NextApiResponse) {
await saveChat(params); await saveChat(params);
} }
const isOwnerUse = !shareId && !spaceTeamId && String(tmbId) === String(app.tmbId);
if (isOwnerUse && source === ChatSourceEnum.online) {
await recordAppUsage({
appId: app._id,
tmbId,
teamId
});
}
addLog.info(`completions running time: ${(Date.now() - startTime) / 1000}s`); addLog.info(`completions running time: ${(Date.now() - startTime) / 1000}s`);
/* select fe response field */ /* select fe response field */
const feResponseData = responseAllData const feResponseData = responseAllData
? flowResponses ? flowResponses
: filterPublicNodeResponseData({ nodeRespones: flowResponses, responseDetail }); : filterPublicNodeResponseData({ nodeRespones: flowResponses, responseDetail: showCite });
if (stream) { if (stream) {
workflowResponseWrite({ workflowResponseWrite({
@ -503,7 +511,7 @@ const authShareChat = async ({
shareId: string; shareId: string;
chatId?: string; chatId?: string;
}): Promise<AuthResponseType> => { }): Promise<AuthResponseType> => {
const { teamId, tmbId, appId, authType, responseDetail, showNodeStatus, uid, sourceName } = const { teamId, tmbId, appId, authType, showCite, showRunningStatus, uid, sourceName } =
await authOutLinkChatStart(data); await authOutLinkChatStart(data);
const app = await MongoApp.findById(appId).lean(); const app = await MongoApp.findById(appId).lean();
@ -525,9 +533,9 @@ const authShareChat = async ({
apikey: '', apikey: '',
authType, authType,
responseAllData: false, responseAllData: false,
responseDetail, showCite,
outLinkUserId: uid, outLinkUserId: uid,
showNodeStatus showRunningStatus
}; };
}; };
const authTeamSpaceChat = async ({ const authTeamSpaceChat = async ({
@ -564,7 +572,7 @@ const authTeamSpaceChat = async ({
authType: AuthUserTypeEnum.outLink, authType: AuthUserTypeEnum.outLink,
apikey: '', apikey: '',
responseAllData: false, responseAllData: false,
responseDetail: true, showCite: true,
outLinkUserId: uid outLinkUserId: uid
}; };
}; };
@ -646,7 +654,7 @@ const authHeaderRequest = async ({
authType, authType,
sourceName, sourceName,
responseAllData: true, responseAllData: true,
responseDetail: true showCite: true
}; };
}; };

View File

@ -5,7 +5,10 @@ import {
setAgentRuntimeStop, setAgentRuntimeStop,
waitForWorkflowComplete waitForWorkflowComplete
} from '@fastgpt/service/core/workflow/dispatch/workflowStatus'; } from '@fastgpt/service/core/workflow/dispatch/workflowStatus';
import { StopV2ChatSchema, type StopV2ChatResponse } from '@fastgpt/global/openapi/core/chat/api'; import {
StopV2ChatSchema,
type StopV2ChatResponse
} from '@fastgpt/global/openapi/core/chat/controler/api';
async function handler(req: NextApiRequest, res: NextApiResponse): Promise<StopV2ChatResponse> { async function handler(req: NextApiRequest, res: NextApiResponse): Promise<StopV2ChatResponse> {
const { appId, chatId, outLinkAuthData } = StopV2ChatSchema.parse(req.body); const { appId, chatId, outLinkAuthData } = StopV2ChatSchema.parse(req.body);

View File

@ -8,7 +8,6 @@ import { serviceSideProps } from '@/web/common/i18n/utils';
import { ChatSidebarPaneEnum } from '@/pageComponents/chat/constants'; import { ChatSidebarPaneEnum } from '@/pageComponents/chat/constants';
import { GetChatTypeEnum } from '@/global/core/chat/constants'; import { GetChatTypeEnum } from '@/global/core/chat/constants';
import ChatContextProvider from '@/web/core/chat/context/chatContext'; import ChatContextProvider from '@/web/core/chat/context/chatContext';
import { type AppListItemType } from '@fastgpt/global/core/app/type';
import { useContextSelector } from 'use-context-selector'; import { useContextSelector } from 'use-context-selector';
import { useSystem } from '@fastgpt/web/hooks/useSystem'; import { useSystem } from '@fastgpt/web/hooks/useSystem';
import { ChatSourceEnum } from '@fastgpt/global/core/chat/constants'; import { ChatSourceEnum } from '@fastgpt/global/core/chat/constants';
@ -18,19 +17,18 @@ import ChatQuoteList from '@/pageComponents/chat/ChatQuoteList';
import LoginModal from '@/pageComponents/login/LoginModal'; import LoginModal from '@/pageComponents/login/LoginModal';
import { useSystemStore } from '@/web/common/system/useSystemStore'; import { useSystemStore } from '@/web/common/system/useSystemStore';
import ChatSetting from '@/pageComponents/chat/ChatSetting'; import ChatSetting from '@/pageComponents/chat/ChatSetting';
import { useChat } from '@/pageComponents/chat/useChat';
import AppChatWindow from '@/pageComponents/chat/ChatWindow/AppChatWindow'; import AppChatWindow from '@/pageComponents/chat/ChatWindow/AppChatWindow';
import HomeChatWindow from '@/pageComponents/chat/ChatWindow/HomeChatWindow'; import HomeChatWindow from '@/pageComponents/chat/ChatWindow/HomeChatWindow';
import { import { ChatPageContext, ChatPageContextProvider } from '@/web/core/chat/context/chatPageContext';
ChatSettingContext,
ChatSettingContextProvider
} from '@/web/core/chat/context/chatSettingContext';
import ChatTeamApp from '@/pageComponents/chat/ChatTeamApp'; import ChatTeamApp from '@/pageComponents/chat/ChatTeamApp';
import ChatFavouriteApp from '@/pageComponents/chat/ChatFavouriteApp'; import ChatFavouriteApp from '@/pageComponents/chat/ChatFavouriteApp';
import { useUserStore } from '@/web/support/user/useUserStore'; import { useUserStore } from '@/web/support/user/useUserStore';
import type { LoginSuccessResponse } from '@/global/support/api/userRes'; import type { LoginSuccessResponse } from '@/global/support/api/userRes';
import { MongoOutLink } from '@fastgpt/service/support/outLink/schema';
import { addLog } from '@fastgpt/service/common/system/log';
import { PublishChannelEnum } from '@fastgpt/global/support/outLink/constant';
const Chat = ({ myApps }: { myApps: AppListItemType[] }) => { const Chat = () => {
const { isPc } = useSystem(); const { isPc } = useSystem();
const { appId } = useChatStore(); const { appId } = useChatStore();
@ -38,8 +36,8 @@ const Chat = ({ myApps }: { myApps: AppListItemType[] }) => {
const datasetCiteData = useContextSelector(ChatItemContext, (v) => v.datasetCiteData); const datasetCiteData = useContextSelector(ChatItemContext, (v) => v.datasetCiteData);
const setCiteModalData = useContextSelector(ChatItemContext, (v) => v.setCiteModalData); const setCiteModalData = useContextSelector(ChatItemContext, (v) => v.setCiteModalData);
const collapse = useContextSelector(ChatSettingContext, (v) => v.collapse); const collapse = useContextSelector(ChatPageContext, (v) => v.collapse);
const pane = useContextSelector(ChatSettingContext, (v) => v.pane); const pane = useContextSelector(ChatPageContext, (v) => v.pane);
return ( return (
<Flex h={'100%'}> <Flex h={'100%'}>
@ -52,14 +50,14 @@ const Chat = ({ myApps }: { myApps: AppListItemType[] }) => {
overflow={'hidden'} overflow={'hidden'}
transition={'width 0.1s ease-in-out'} transition={'width 0.1s ease-in-out'}
> >
<ChatSlider apps={myApps} activeAppId={appId} /> <ChatSlider activeAppId={appId} />
</Box> </Box>
)} )}
{(!datasetCiteData || isPc) && ( {(!datasetCiteData || isPc) && (
<PageContainer flex="1 0 0" w={0} position="relative"> <PageContainer flex="1 0 0" w={0} position="relative">
{/* home chat window */} {/* home chat window */}
{pane === ChatSidebarPaneEnum.HOME && <HomeChatWindow myApps={myApps} />} {pane === ChatSidebarPaneEnum.HOME && <HomeChatWindow />}
{/* favourite apps */} {/* favourite apps */}
{pane === ChatSidebarPaneEnum.FAVORITE_APPS && <ChatFavouriteApp />} {pane === ChatSidebarPaneEnum.FAVORITE_APPS && <ChatFavouriteApp />}
@ -68,7 +66,7 @@ const Chat = ({ myApps }: { myApps: AppListItemType[] }) => {
{pane === ChatSidebarPaneEnum.TEAM_APPS && <ChatTeamApp />} {pane === ChatSidebarPaneEnum.TEAM_APPS && <ChatTeamApp />}
{/* recently used apps chat window */} {/* recently used apps chat window */}
{pane === ChatSidebarPaneEnum.RECENTLY_USED_APPS && <AppChatWindow myApps={myApps} />} {pane === ChatSidebarPaneEnum.RECENTLY_USED_APPS && <AppChatWindow />}
{/* setting */} {/* setting */}
{pane === ChatSidebarPaneEnum.SETTING && <ChatSetting />} {pane === ChatSidebarPaneEnum.SETTING && <ChatSetting />}
@ -88,12 +86,23 @@ const Chat = ({ myApps }: { myApps: AppListItemType[] }) => {
); );
}; };
const Render = (props: { appId: string; isStandalone?: string }) => { type ChatPageProps = {
appId: string;
isStandalone?: string;
showRunningStatus: boolean;
showCite: boolean;
showFullText: boolean;
canDownloadSource: boolean;
};
const ChatContent = (props: ChatPageProps) => {
const { appId, isStandalone } = props; const { appId, isStandalone } = props;
const { chatId } = useChatStore(); const { chatId } = useChatStore();
const { setUserInfo } = useUserStore(); const { setUserInfo } = useUserStore();
const { feConfigs } = useSystemStore(); const { feConfigs } = useSystemStore();
const { isInitedUser, userInfo, myApps } = useChat(appId);
const isInitedUser = useContextSelector(ChatPageContext, (v) => v.isInitedUser);
const userInfo = useContextSelector(ChatPageContext, (v) => v.userInfo);
const chatHistoryProviderParams = useMemo( const chatHistoryProviderParams = useMemo(
() => ({ appId, source: ChatSourceEnum.online }), () => ({ appId, source: ChatSourceEnum.online }),
@ -134,31 +143,62 @@ const Render = (props: { appId: string; isStandalone?: string }) => {
// show main chat interface // show main chat interface
return ( return (
<ChatSettingContextProvider> <ChatContextProvider params={chatHistoryProviderParams}>
<ChatContextProvider params={chatHistoryProviderParams}> <ChatItemContextProvider
<ChatItemContextProvider showRouteToDatasetDetail={isStandalone !== '1'}
showRouteToDatasetDetail={isStandalone !== '1'} showRunningStatus={props.showRunningStatus}
isShowReadRawSource={true} canDownloadSource={props.canDownloadSource}
isResponseDetail={true} isShowCite={props.showCite}
showNodeStatus isShowFullText={props.showFullText}
> >
<ChatRecordContextProvider params={chatRecordProviderParams}> <ChatRecordContextProvider params={chatRecordProviderParams}>
<Chat myApps={myApps} /> <Chat />
</ChatRecordContextProvider> </ChatRecordContextProvider>
</ChatItemContextProvider> </ChatItemContextProvider>
</ChatContextProvider> </ChatContextProvider>
</ChatSettingContextProvider>
); );
}; };
const Render = (props: ChatPageProps) => {
return (
<ChatPageContextProvider appId={props.appId}>
<ChatContent {...props} />
</ChatPageContextProvider>
);
};
export default Render;
export async function getServerSideProps(context: any) { export async function getServerSideProps(context: any) {
const appId = context?.query?.appId || '';
const chatQuoteReaderConfig = await (async () => {
try {
if (!appId) return null;
const config = await MongoOutLink.findOne(
{
appId,
type: PublishChannelEnum.playground
},
'showRunningStatus showCite showFullText canDownloadSource'
).lean();
return config;
} catch (error) {
addLog.error('getServerSideProps', error);
return null;
}
})();
return { return {
props: { props: {
appId: context?.query?.appId || '', appId,
isStandalone: context?.query?.isStandalone || '', showRunningStatus: chatQuoteReaderConfig?.showRunningStatus ?? true,
...(await serviceSideProps(context, ['file', 'app', 'chat', 'workflow', 'user', 'login'])) showCite: chatQuoteReaderConfig?.showCite ?? true,
showFullText: chatQuoteReaderConfig?.showFullText ?? true,
canDownloadSource: chatQuoteReaderConfig?.canDownloadSource ?? true,
...(await serviceSideProps(context, ['file', 'app', 'chat', 'workflow']))
} }
}; };
} }
export default Render;

View File

@ -53,10 +53,10 @@ type Props = {
shareId: string; shareId: string;
authToken: string; authToken: string;
customUid: string; customUid: string;
showRawSource: boolean; canDownloadSource: boolean;
responseDetail: boolean; isShowCite: boolean;
// showFullText: boolean; isShowFullText: boolean;
showNodeStatus: boolean; showRunningStatus: boolean;
}; };
const OutLink = (props: Props) => { const OutLink = (props: Props) => {
@ -95,7 +95,7 @@ const OutLink = (props: Props) => {
const setChatBoxData = useContextSelector(ChatItemContext, (v) => v.setChatBoxData); const setChatBoxData = useContextSelector(ChatItemContext, (v) => v.setChatBoxData);
const datasetCiteData = useContextSelector(ChatItemContext, (v) => v.datasetCiteData); const datasetCiteData = useContextSelector(ChatItemContext, (v) => v.datasetCiteData);
const setCiteModalData = useContextSelector(ChatItemContext, (v) => v.setCiteModalData); const setCiteModalData = useContextSelector(ChatItemContext, (v) => v.setCiteModalData);
const isResponseDetail = useContextSelector(ChatItemContext, (v) => v.isResponseDetail); const isShowCite = useContextSelector(ChatItemContext, (v) => v.isShowCite);
const chatRecords = useContextSelector(ChatRecordContext, (v) => v.chatRecords); const chatRecords = useContextSelector(ChatRecordContext, (v) => v.chatRecords);
const totalRecordsCount = useContextSelector(ChatRecordContext, (v) => v.totalRecordsCount); const totalRecordsCount = useContextSelector(ChatRecordContext, (v) => v.totalRecordsCount);
@ -175,7 +175,7 @@ const OutLink = (props: Props) => {
responseChatItemId, responseChatItemId,
chatId: completionChatId, chatId: completionChatId,
...outLinkAuthData, ...outLinkAuthData,
retainDatasetCite: isResponseDetail retainDatasetCite: isShowCite
}, },
onMessage: generatingMessage, onMessage: generatingMessage,
abortCtrl: controller abortCtrl: controller
@ -213,7 +213,7 @@ const OutLink = (props: Props) => {
chatId, chatId,
customVariables, customVariables,
outLinkAuthData, outLinkAuthData,
isResponseDetail, isShowCite,
onUpdateHistoryTitle, onUpdateHistoryTitle,
setChatBoxData, setChatBoxData,
forbidLoadChat, forbidLoadChat,
@ -388,10 +388,10 @@ const Render = (props: Props) => {
<ChatContextProvider params={chatHistoryProviderParams}> <ChatContextProvider params={chatHistoryProviderParams}>
<ChatItemContextProvider <ChatItemContextProvider
showRouteToDatasetDetail={false} showRouteToDatasetDetail={false}
isShowReadRawSource={props.showRawSource} canDownloadSource={props.canDownloadSource}
isResponseDetail={props.responseDetail} isShowCite={props.isShowCite}
// isShowFullText={props.showFullText} isShowFullText={props.isShowFullText}
showNodeStatus={props.showNodeStatus} showRunningStatus={props.showRunningStatus}
> >
<ChatRecordContextProvider params={chatRecordProviderParams}> <ChatRecordContextProvider params={chatRecordProviderParams}>
<OutLink {...props} /> <OutLink {...props} />
@ -416,7 +416,7 @@ export async function getServerSideProps(context: any) {
{ {
shareId shareId
}, },
'appId showRawSource showNodeStatus responseDetail' 'appId canDownloadSource showCite showFullText showRunningStatus'
) )
.populate<{ associatedApp: AppSchema }>('associatedApp', 'name avatar intro') .populate<{ associatedApp: AppSchema }>('associatedApp', 'name avatar intro')
.lean(); .lean();
@ -432,10 +432,10 @@ export async function getServerSideProps(context: any) {
appName: app?.associatedApp?.name ?? 'AI', appName: app?.associatedApp?.name ?? 'AI',
appAvatar: app?.associatedApp?.avatar ?? '', appAvatar: app?.associatedApp?.avatar ?? '',
appIntro: app?.associatedApp?.intro ?? 'AI', appIntro: app?.associatedApp?.intro ?? 'AI',
showRawSource: app?.showRawSource ?? false, canDownloadSource: app?.canDownloadSource ?? false,
responseDetail: app?.responseDetail ?? false, isShowCite: app?.showCite ?? false,
// showFullText: app?.showFullText ?? false, isShowFullText: app?.showFullText ?? false,
showNodeStatus: app?.showNodeStatus ?? false, showRunningStatus: app?.showRunningStatus ?? false,
shareId: shareId ?? '', shareId: shareId ?? '',
authToken: authToken ?? '', authToken: authToken ?? '',
customUid, customUid,

View File

@ -101,7 +101,6 @@ export const getScheduleTriggerApp = async () => {
nodes, nodes,
appChatConfig: chatConfig, appChatConfig: chatConfig,
variables: {}, variables: {},
isUpdateUseTime: false, // owner update use time
newTitle: 'Cron Job', newTitle: 'Cron Job',
source: ChatSourceEnum.cronJob, source: ChatSourceEnum.cronJob,
userContent: { userContent: {

View File

@ -257,7 +257,6 @@ export const callMcpServerTool = async ({ key, toolName, inputs }: toolCallProps
nodes, nodes,
appChatConfig: chatConfig, appChatConfig: chatConfig,
variables: newVariables, variables: newVariables,
isUpdateUseTime: false, // owner update use time
newTitle, newTitle,
source: ChatSourceEnum.mcp, source: ChatSourceEnum.mcp,
userContent: userQuestion, userContent: userQuestion,

View File

@ -24,9 +24,10 @@ import { ChatRoleEnum } from '@fastgpt/global/core/chat/constants';
Chat没有读写的权限之分 Chat没有读写的权限之分
*/ */
export const defaultResponseShow = { export const defaultResponseShow = {
responseDetail: true, showCite: true,
showNodeStatus: true, showRunningStatus: true,
showRawSource: true showFullText: true,
canDownloadSource: true
}; };
type AuthChatCommonProps = { type AuthChatCommonProps = {
appId: string; appId: string;
@ -54,9 +55,10 @@ export async function authChatCrud({
tmbId: string; tmbId: string;
uid: string; uid: string;
chat?: ChatSchemaType; chat?: ChatSchemaType;
responseDetail: boolean; showCite: boolean;
showNodeStatus: boolean; showRunningStatus: boolean;
showRawSource: boolean; showFullText: boolean;
canDownloadSource: boolean;
authType?: `${AuthUserTypeEnum}`; authType?: `${AuthUserTypeEnum}`;
}> { }> {
if (!appId) return Promise.reject(ChatErrEnum.unAuthChat); if (!appId) return Promise.reject(ChatErrEnum.unAuthChat);
@ -109,9 +111,11 @@ export async function authChatCrud({
teamId: String(outLinkConfig.teamId), teamId: String(outLinkConfig.teamId),
tmbId: String(outLinkConfig.tmbId), tmbId: String(outLinkConfig.tmbId),
uid, uid,
responseDetail: outLinkConfig.responseDetail,
showNodeStatus: outLinkConfig.showNodeStatus ?? true, showCite: outLinkConfig.showCite ?? false,
showRawSource: outLinkConfig.showRawSource ?? false, showRunningStatus: outLinkConfig.showRunningStatus ?? true,
showFullText: outLinkConfig.showFullText ?? false,
canDownloadSource: outLinkConfig.canDownloadSource ?? false,
authType: AuthUserTypeEnum.outLink authType: AuthUserTypeEnum.outLink
}; };
} }
@ -123,9 +127,10 @@ export async function authChatCrud({
teamId: String(outLinkConfig.teamId), teamId: String(outLinkConfig.teamId),
tmbId: String(outLinkConfig.tmbId), tmbId: String(outLinkConfig.tmbId),
uid, uid,
responseDetail: outLinkConfig.responseDetail, showCite: outLinkConfig.showCite ?? false,
showNodeStatus: outLinkConfig.showNodeStatus ?? true, showRunningStatus: outLinkConfig.showRunningStatus ?? true,
showRawSource: outLinkConfig.showRawSource ?? false, showFullText: outLinkConfig.showFullText ?? false,
canDownloadSource: outLinkConfig.canDownloadSource ?? false,
authType: AuthUserTypeEnum.outLink authType: AuthUserTypeEnum.outLink
}; };
} }
@ -135,9 +140,10 @@ export async function authChatCrud({
tmbId: String(outLinkConfig.tmbId), tmbId: String(outLinkConfig.tmbId),
chat, chat,
uid, uid,
responseDetail: outLinkConfig.responseDetail, showCite: outLinkConfig.showCite ?? false,
showNodeStatus: outLinkConfig.showNodeStatus ?? true, showRunningStatus: outLinkConfig.showRunningStatus ?? true,
showRawSource: outLinkConfig.showRawSource ?? false, showFullText: outLinkConfig.showFullText ?? false,
canDownloadSource: outLinkConfig.canDownloadSource ?? false,
authType: AuthUserTypeEnum.outLink authType: AuthUserTypeEnum.outLink
}; };
} }

View File

@ -63,8 +63,10 @@ export async function authOutLinkChatStart({
teamId: outLinkConfig.teamId, teamId: outLinkConfig.teamId,
tmbId: outLinkConfig.tmbId, tmbId: outLinkConfig.tmbId,
authType: AuthUserTypeEnum.token, authType: AuthUserTypeEnum.token,
responseDetail: outLinkConfig.responseDetail, showCite: outLinkConfig.showCite,
showNodeStatus: outLinkConfig.showNodeStatus, showRunningStatus: outLinkConfig.showRunningStatus,
showFullText: outLinkConfig.showFullText,
canDownloadSource: outLinkConfig.canDownloadSource,
appId, appId,
uid uid
}; };

View File

@ -14,11 +14,6 @@ export const getMyApps = (data?: ListAppBody) =>
maxQuantity: 1 maxQuantity: 1
}); });
export const getRecentlyUsedApps = (data?: ListAppBody) =>
POST<AppListItemType[]>('/core/app/list?t=0', data, {
maxQuantity: 1
});
/** /**
* *
*/ */

View File

@ -26,10 +26,10 @@ export const defaultApp: AppDetailType = {
export const defaultOutLinkForm: OutLinkEditType = { export const defaultOutLinkForm: OutLinkEditType = {
name: '', name: '',
showNodeStatus: true, showRunningStatus: true,
responseDetail: false, showCite: false,
// showFullText: false, showFullText: false,
showRawSource: false, canDownloadSource: false,
limit: { limit: {
QPM: 100, QPM: 100,
maxUsagePoints: -1 maxUsagePoints: -1

View File

@ -24,7 +24,11 @@ import type {
UpdateFavouriteAppParamsType UpdateFavouriteAppParamsType
} from '@fastgpt/global/openapi/core/chat/favourite/api'; } from '@fastgpt/global/openapi/core/chat/favourite/api';
import type { ChatFavouriteAppType } from '@fastgpt/global/core/chat/favouriteApp/type'; import type { ChatFavouriteAppType } from '@fastgpt/global/core/chat/favouriteApp/type';
import type { StopV2ChatParams } from '@fastgpt/global/openapi/core/chat/api'; import type { StopV2ChatParams } from '@fastgpt/global/openapi/core/chat/controler/api';
import type { GetRecentlyUsedAppsResponseType } from '@fastgpt/global/openapi/core/chat/api';
export const getRecentlyUsedApps = () =>
GET<GetRecentlyUsedAppsResponseType>('/core/chat/recentlyUsed', undefined, { maxQuantity: 1 });
/** /**
* *

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