diff --git a/.claude/skills/core/app/workflow/stop/SKILL.md b/.claude/skills/core/app/workflow/stop/SKILL.md index a109f45e0..2c5998c9a 100644 --- a/.claude/skills/core/app/workflow/stop/SKILL.md +++ b/.claude/skills/core/app/workflow/stop/SKILL.md @@ -437,7 +437,7 @@ import { setAgentRuntimeStop, waitForWorkflowComplete } 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 { const { appId, chatId, outLinkAuthData } = StopV2ChatSchema.parse(req.body); @@ -527,7 +527,7 @@ LLM 节点,流输出时会同时被终止,但 HTTP 请求节点这种可能 ```typescript 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 版本工作流运行 diff --git a/document/content/docs/upgrading/4-14/4145.mdx b/document/content/docs/upgrading/4-14/4145.mdx index 066ebe6d9..f4685a2b9 100644 --- a/document/content/docs/upgrading/4-14/4145.mdx +++ b/document/content/docs/upgrading/4-14/4145.mdx @@ -7,6 +7,8 @@ description: 'FastGPT V4.14.5 更新说明' ## 🚀 新增内容 1. 对话记录使用侧改成软删除,增加从日志管理里删除对话记录。 +2. 更新Agent/工具时,会更新其上层所有目录的更新时间,以便其会排在列表前面。 +3. 门户页支持配置单个应用运行可见度配。 ## ⚙️ 优化 diff --git a/document/data/doc-last-modified.json b/document/data/doc-last-modified.json index 330ea4e5b..eba3542d8 100644 --- a/document/data/doc-last-modified.json +++ b/document/data/doc-last-modified.json @@ -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/4143.mdx": "2025-11-26T20:52:05+08:00", "document/content/docs/upgrading/4-14/4144.mdx": "2025-12-16T14:56:04+08:00", - "document/content/docs/upgrading/4-14/4145.mdx": "2025-12-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/41.mdx": "2025-08-02T19:38:37+08:00", "document/content/docs/upgrading/4-8/42.mdx": "2025-08-02T19:38:37+08:00", diff --git a/packages/global/openapi/core/app/index.ts b/packages/global/openapi/core/app/index.ts index 77c353c68..3717da38e 100644 --- a/packages/global/openapi/core/app/index.ts +++ b/packages/global/openapi/core/app/index.ts @@ -1,6 +1,8 @@ import type { OpenAPIPath } from '../../type'; import { AppLogPath } from './log'; +import { PublishChannelPath } from './publishChannel'; export const AppPath: OpenAPIPath = { - ...AppLogPath + ...AppLogPath, + ...PublishChannelPath }; diff --git a/packages/global/openapi/core/app/publishChannel/index.ts b/packages/global/openapi/core/app/publishChannel/index.ts new file mode 100644 index 000000000..3b8dc0da8 --- /dev/null +++ b/packages/global/openapi/core/app/publishChannel/index.ts @@ -0,0 +1,5 @@ +import { PlaygroundPath } from './playground'; + +export const PublishChannelPath = { + ...PlaygroundPath +}; diff --git a/packages/global/openapi/core/app/publishChannel/playground/api.ts b/packages/global/openapi/core/app/publishChannel/playground/api.ts new file mode 100644 index 000000000..4ba8ed7ac --- /dev/null +++ b/packages/global/openapi/core/app/publishChannel/playground/api.ts @@ -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 +>; diff --git a/packages/global/openapi/core/app/publishChannel/playground/index.ts b/packages/global/openapi/core/app/publishChannel/playground/index.ts new file mode 100644 index 000000000..c31349eb2 --- /dev/null +++ b/packages/global/openapi/core/app/publishChannel/playground/index.ts @@ -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() + }) + } + } + } + } + } + } +}; diff --git a/packages/global/openapi/core/chat/api.ts b/packages/global/openapi/core/chat/api.ts index c81ab1fc1..5e9fa5322 100644 --- a/packages/global/openapi/core/chat/api.ts +++ b/packages/global/openapi/core/chat/api.ts @@ -1,72 +1,12 @@ -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({ +/* Recently Used Apps */ +export const GetRecentlyUsedAppsResponseSchema = z.array( + z.object({ appId: ObjectIdSchema.describe('应用ID'), - chatId: z.string().min(1).describe('对话ID'), - outLinkAuthData: OutLinkChatAuthSchema.optional().describe('外链鉴权数据') + name: z.string().min(1).describe('应用名称'), + avatar: z.string().min(1).describe('应用头像') }) - .meta({ - example: { - appId: '1234567890', - chatId: '1234567890', - outLinkAuthData: { - shareId: '1234567890', - outLinkUid: '1234567890' - } - } - }); -export type StopV2ChatParams = z.infer; - -export const StopV2ChatResponseSchema = z - .object({ - success: z.boolean().describe('是否成功停止') - }) - .meta({ - example: { - success: true - } - }); -export type StopV2ChatResponse = z.infer; - -/* ============ 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; - -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; +); +export type GetRecentlyUsedAppsResponseType = z.infer; diff --git a/packages/global/openapi/core/chat/controler/api.ts b/packages/global/openapi/core/chat/controler/api.ts new file mode 100644 index 000000000..6864bd9a8 --- /dev/null +++ b/packages/global/openapi/core/chat/controler/api.ts @@ -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; + +export const StopV2ChatResponseSchema = z + .object({ + success: z.boolean().describe('是否成功停止') + }) + .meta({ + example: { + success: true + } + }); +export type StopV2ChatResponse = z.infer; + +/* ============ 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; + +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; diff --git a/packages/global/openapi/core/chat/controler/index.ts b/packages/global/openapi/core/chat/controler/index.ts new file mode 100644 index 000000000..8aa42f11d --- /dev/null +++ b/packages/global/openapi/core/chat/controler/index.ts @@ -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() + } + } + } + } + } + } +}; diff --git a/packages/global/openapi/core/chat/index.ts b/packages/global/openapi/core/chat/index.ts index 6b2ad43c9..2ff79aee9 100644 --- a/packages/global/openapi/core/chat/index.ts +++ b/packages/global/openapi/core/chat/index.ts @@ -3,89 +3,28 @@ import { ChatSettingPath } from './setting'; import { ChatFavouriteAppPath } from './favourite/index'; import { ChatFeedbackPath } from './feedback/index'; import { ChatHistoryPath } from './history/index'; -import { z } from 'zod'; -import { CreatePostPresignedUrlResultSchema } from '../../../../service/common/s3/type'; -import { - PresignChatFileGetUrlSchema, - PresignChatFilePostUrlSchema, - StopV2ChatSchema, - StopV2ChatResponseSchema -} from './api'; +import { GetRecentlyUsedAppsResponseSchema } from './api'; import { TagsMap } from '../../tag'; +import { ChatControllerPath } from './controler'; export const ChatPath: OpenAPIPath = { ...ChatSettingPath, ...ChatFavouriteAppPath, ...ChatFeedbackPath, ...ChatHistoryPath, + ...ChatControllerPath, - '/v2/chat/stop': { - post: { - summary: '停止 Agent 运行', - description: `优雅停止正在运行的 Agent, 会尝试等待当前节点结束后返回,最长 5s,超过 5s 仍未结束,则会返回成功。 -LLM 节点,流输出时会同时被终止,但 HTTP 请求节点这种可能长时间运行的,不会被终止。`, + '/core/chat/recentlyUsed': { + get: { + summary: '获取最近使用的应用', + description: '获取最近使用的应用', tags: [TagsMap.chatPage], - requestBody: { - content: { - 'application/json': { - schema: StopV2ChatSchema - } - } - }, responses: { 200: { - description: '成功停止工作流', + description: '成功返回最近使用的应用', content: { 'application/json': { - schema: StopV2ChatResponseSchema - } - } - } - } - } - }, - '/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() + schema: GetRecentlyUsedAppsResponseSchema } } } diff --git a/packages/global/openapi/index.ts b/packages/global/openapi/index.ts index b06c9d816..535726021 100644 --- a/packages/global/openapi/index.ts +++ b/packages/global/openapi/index.ts @@ -26,7 +26,7 @@ export const openAPIDocument = createDocument({ 'x-tagGroups': [ { name: 'Agent 应用', - tags: [TagsMap.appLog] + tags: [TagsMap.appLog, TagsMap.publishChannel] }, { name: '对话管理', diff --git a/packages/global/openapi/tag.ts b/packages/global/openapi/tag.ts index 34806829e..bb603d09e 100644 --- a/packages/global/openapi/tag.ts +++ b/packages/global/openapi/tag.ts @@ -4,7 +4,8 @@ export const TagsMap = { appLog: 'Agent 日志', // Chat - home - chatPage: '对话页操作', + chatPage: '对话页', + chatController: '对话框操作', chatHistory: '对话历史管理', chatSetting: '门户页配置', chatFeedback: '对话反馈', @@ -13,6 +14,9 @@ export const TagsMap = { pluginToolTag: '工具标签', pluginTeam: '团队插件管理', + // Publish Channel + publishChannel: '发布渠道', + /* Support */ // Wallet walletBill: '订单', diff --git a/packages/global/support/outLink/api.d.ts b/packages/global/support/outLink/api.d.ts index 141254577..04648156e 100644 --- a/packages/global/support/outLink/api.d.ts +++ b/packages/global/support/outLink/api.d.ts @@ -1,5 +1,7 @@ +import { z } from 'zod'; 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 = { outLinkUid: string; @@ -10,3 +12,20 @@ export type AuthOutLinkLimitProps = AuthOutLinkChatProps & { outLink: OutLinkSch export type AuthOutLinkResponse = { 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; + +export const PlaygroundVisibilityConfigResponseSchema = PlaygroundVisibilityConfigSchema; +export type PlaygroundVisibilityConfigResponse = z.infer< + typeof PlaygroundVisibilityConfigResponseSchema +>; diff --git a/packages/global/support/outLink/constant.ts b/packages/global/support/outLink/constant.ts index 0fcba2ec4..872e6112f 100644 --- a/packages/global/support/outLink/constant.ts +++ b/packages/global/support/outLink/constant.ts @@ -5,5 +5,6 @@ export enum PublishChannelEnum { feishu = 'feishu', dingtalk = 'dingtalk', wecom = 'wecom', - officialAccount = 'official_account' + officialAccount = 'official_account', + playground = 'playground' } diff --git a/packages/global/support/outLink/type.d.ts b/packages/global/support/outLink/type.d.ts index d2bd692e4..60a304dbe 100644 --- a/packages/global/support/outLink/type.d.ts +++ b/packages/global/support/outLink/type.d.ts @@ -1,3 +1,4 @@ +import { z } from 'zod'; import { AppSchema } from '../../core/app/type'; import type { PublishChannelEnum } from './constant'; import { RequireOnlyOne } from '../../common/type/utils'; @@ -63,14 +64,14 @@ export type OutLinkSchema = { lastTime: Date; type: PublishChannelEnum; - // whether the response content is detailed - responseDetail: boolean; - // whether to hide the node status - showNodeStatus?: boolean; - // wheter to show the full text reader - // showFullText?: boolean; - // whether to show the complete quote - showRawSource?: boolean; + // whether to show the quote + showCite: boolean; + // whether to show the running status + showRunningStatus: boolean; + // whether to show the full text reader + showFullText: boolean; + // whether can download source + canDownloadSource: boolean; // response when request immediateResponse?: string; @@ -93,10 +94,10 @@ export type OutLinkSchema = { export type OutLinkEditType = { _id?: string; name: string; - responseDetail?: OutLinkSchema['responseDetail']; - showNodeStatus?: OutLinkSchema['showNodeStatus']; - // showFullText?: OutLinkSchema['showFullText']; - showRawSource?: OutLinkSchema['showRawSource']; + showCite?: OutLinkSchema['showCite']; + showRunningStatus?: OutLinkSchema['showRunningStatus']; + showFullText?: OutLinkSchema['showFullText']; + canDownloadSource?: OutLinkSchema['canDownloadSource']; // response when request immediateResponse?: string; // response when error or other situation @@ -106,3 +107,12 @@ export type OutLinkEditType = { // config for specific platform app?: T; }; + +export const PlaygroundVisibilityConfigSchema = z.object({ + showRunningStatus: z.boolean(), + showCite: z.boolean(), + showFullText: z.boolean(), + canDownloadSource: z.boolean() +}); + +export type PlaygroundVisibilityConfigType = z.infer; diff --git a/packages/service/core/app/controller.ts b/packages/service/core/app/controller.ts index fb07552a2..c0c0ca2c6 100644 --- a/packages/service/core/app/controller.ts +++ b/packages/service/core/app/controller.ts @@ -27,6 +27,9 @@ import { getS3ChatSource } from '../../common/s3/sources/chat'; import { MongoAppChatLog } from './logs/chatLogsSchema'; import { MongoAppRegistration } from '../../support/appRegistration/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[] }) => { if (!nodes) return; @@ -203,4 +206,29 @@ export const deleteAppsImmediate = async ({ '_id' ).lean(); 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(); + 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); + }); }; diff --git a/packages/service/core/app/record/schema.ts b/packages/service/core/app/record/schema.ts new file mode 100644 index 000000000..1a04a5236 --- /dev/null +++ b/packages/service/core/app/record/schema.ts @@ -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( + AppRecordCollectionName, + AppRecordSchema +); diff --git a/packages/service/core/app/record/type.ts b/packages/service/core/app/record/type.ts new file mode 100644 index 000000000..4917f8d9e --- /dev/null +++ b/packages/service/core/app/record/type.ts @@ -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; diff --git a/packages/service/core/app/record/utils.ts b/packages/service/core/app/record/utils.ts new file mode 100644 index 000000000..285f341f5 --- /dev/null +++ b/packages/service/core/app/record/utils.ts @@ -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); + }); +}; diff --git a/packages/service/core/chat/saveChat.ts b/packages/service/core/chat/saveChat.ts index f427a40b1..a18773fa1 100644 --- a/packages/service/core/chat/saveChat.ts +++ b/packages/service/core/chat/saveChat.ts @@ -35,7 +35,6 @@ export type Props = { nodes: StoreNodeItemType[]; appChatConfig?: AppChatConfigType; variables?: Record; - isUpdateUseTime: boolean; newTitle: string; source: `${ChatSourceEnum}`; sourceName?: string; @@ -219,7 +218,6 @@ export async function saveChat(props: Props) { nodes, appChatConfig, variables, - isUpdateUseTime, newTitle, source, sourceName, @@ -393,18 +391,6 @@ export async function saveChat(props: Props) { } catch (error) { addLog.error('Push chat log error', error); } - - if (isUpdateUseTime) { - await MongoApp.updateOne( - { _id: appId }, - { - updateTime: new Date() - }, - { - ...writePrimary - } - ).catch(); - } } catch (error) { addLog.error(`update chat history error`, error); } diff --git a/packages/service/support/outLink/schema.ts b/packages/service/support/outLink/schema.ts index 59871cea6..76b4ba1b6 100644 --- a/packages/service/support/outLink/schema.ts +++ b/packages/service/support/outLink/schema.ts @@ -43,19 +43,21 @@ const OutLinkSchema = new Schema({ type: Date }, - responseDetail: { + showRunningStatus: { type: Boolean, default: false }, - showNodeStatus: { + showCite: { type: Boolean, - default: true + default: false }, - // showFullText: { - // type: Boolean - // }, - showRawSource: { - type: Boolean + showFullText: { + type: Boolean, + default: false + }, + canDownloadSource: { + type: Boolean, + default: false }, limit: { maxUsagePoints: { diff --git a/packages/web/hooks/useLinkedScroll.tsx b/packages/web/hooks/useLinkedScroll.tsx index 75e4126e8..9518bfb5e 100644 --- a/packages/web/hooks/useLinkedScroll.tsx +++ b/packages/web/hooks/useLinkedScroll.tsx @@ -17,12 +17,14 @@ export function useLinkedScroll< pageSize = 10, params = {}, currentData, - defaultScroll = 'top' + defaultScroll = 'top', + showErrorToast = true }: { pageSize?: number; params?: Record; currentData?: { id: string; anchor?: any }; defaultScroll?: 'top' | 'bottom'; + showErrorToast?: boolean; } ) { const { t } = useTranslation(); @@ -105,7 +107,8 @@ export function useLinkedScroll< onFinally() { isInit.current = true; }, - manual: false + manual: false, + errorToast: showErrorToast ? undefined : '' } ); useEffect(() => { @@ -153,7 +156,8 @@ export function useLinkedScroll< return response; }, { - refreshDeps: [hasMorePrev, isLoading, params, pageSize] + refreshDeps: [hasMorePrev, isLoading, params, pageSize], + errorToast: showErrorToast ? undefined : '' } ); @@ -188,7 +192,8 @@ export function useLinkedScroll< return response; }, { - refreshDeps: [hasMoreNext, isLoading, params, pageSize] + refreshDeps: [hasMoreNext, isLoading, params, pageSize], + errorToast: showErrorToast ? undefined : '' } ); diff --git a/packages/web/hooks/useScrollPagination.tsx b/packages/web/hooks/useScrollPagination.tsx index 708423b2c..d20ee1041 100644 --- a/packages/web/hooks/useScrollPagination.tsx +++ b/packages/web/hooks/useScrollPagination.tsx @@ -269,7 +269,7 @@ export function useScrollPagination< } catch (error: any) { if (showErrorToast) { toast({ - title: getErrText(error, t('common:core.chat.error.data_error')), + title: t(getErrText(error, t('common:core.chat.error.data_error'))), status: 'error' }); } diff --git a/packages/web/i18n/en/app.json b/packages/web/i18n/en/app.json index 7989592cc..56203f83f 100644 --- a/packages/web/i18n/en/app.json +++ b/packages/web/i18n/en/app.json @@ -301,6 +301,8 @@ "pro_modal_subtitle": "Join the business edition now to unlock more premium features", "pro_modal_title": "Business Edition Exclusive!", "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_success": "Publish Successful", "question_guide_tip": "After the conversation, 3 guiding questions will be generated for you.", diff --git a/packages/web/i18n/en/common.json b/packages/web/i18n/en/common.json index 68badf0f3..6c87d1c45 100644 --- a/packages/web/i18n/en/common.json +++ b/packages/web/i18n/en/common.json @@ -234,7 +234,7 @@ "core.ai.model.Vector Model": "Index model", "core.ai.model.doc_index_and_dialog": "Document Index & Dialog Index", "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.Auto execute": "Auto execute", "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.Setting ai property": "Click to Configure AI Model Properties", "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.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.", @@ -315,6 +315,7 @@ "core.app.share.Amount limit tip": "Up to 10 groups", "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.Download source": "Download/open source original text", "core.app.share.Ip limit title": "IP Rate Limit (people/minute)", "core.app.share.Is response quote": "Return Quote", "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.Usage points": "Points Consumption", "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.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.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.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", diff --git a/packages/web/i18n/zh-CN/app.json b/packages/web/i18n/zh-CN/app.json index 7330c6d59..fb584adbe 100644 --- a/packages/web/i18n/zh-CN/app.json +++ b/packages/web/i18n/zh-CN/app.json @@ -313,6 +313,8 @@ "pro_modal_subtitle": "即刻加入商业版,解锁更多高级功能", "pro_modal_title": "商业版专享!", "pro_modal_unlock_button": "去解锁", + "publish.chat_desc": "用户登录门户后可直接与应用对话", + "publish.playground_link": "跳转链接", "publish_channel": "发布渠道", "publish_channel.wecom.empty": "发布到企业微信机器人,请先 绑定自定义域名,并且通过域名校验。", "publish_success": "发布成功", diff --git a/packages/web/i18n/zh-CN/common.json b/packages/web/i18n/zh-CN/common.json index 1a4889714..591463c43 100644 --- a/packages/web/i18n/zh-CN/common.json +++ b/packages/web/i18n/zh-CN/common.json @@ -237,7 +237,7 @@ "core.ai.model.Vector Model": "索引模型", "core.ai.model.doc_index_and_dialog": "文档索引 & 对话索引", "core.app.Api request": "API 访问", - "core.app.Api request desc": "通过 API 接入到已有系统中,或企微、飞书等", + "core.app.Api request desc": "通过 API 接入已有系统", "core.app.App intro": "应用介绍", "core.app.Auto execute": "自动执行", "core.app.Chat Variable": "对话框变量", @@ -267,7 +267,7 @@ "core.app.Set a name for your app": "给应用设置一个名称", "core.app.Setting ai property": "点击配置 AI 模型相关属性", "core.app.Share link": "免登录窗口", - "core.app.Share link desc": "分享链接给其他用户,无需登录即可直接进行使用", + "core.app.Share link desc": "创建可分享的链接,支持免登录使用", "core.app.Share link desc detail": "可以直接分享该模型给其他用户去进行对话,对方无需登录即可直接进行对话。注意,这个功能会消耗你账号的余额,请保管好链接!", "core.app.TTS": "语音播放", "core.app.TTS Tip": "开启后,每次对话后可使用语音播放功能。使用该功能可能产生额外费用。", @@ -318,10 +318,12 @@ "core.app.share.Amount limit tip": "最多创建 10 组", "core.app.share.Create link": "创建新链接", "core.app.share.Create link tip": "创建成功。已复制分享地址,可直接分享使用", + "core.app.share.Download source": "下载/打开来源原文", "core.app.share.Ip limit title": "IP 限流(人/分钟)", "core.app.share.Is response quote": "返回引用", "core.app.share.Not share link": "没有创建分享链接", "core.app.share.Role check": "身份校验", + "core.app.share.Show full text": "查看引用全文", "core.app.switch_to_template_market": "跳转模板市场", "core.app.tip.Add a intro to app": "快来给应用一个介绍~", "core.app.tip.chatNodeSystemPromptTip": "在此输入提示词", @@ -1110,9 +1112,11 @@ "support.outlink.Max usage points tip": "该链接最多允许使用多少积分,超出后将无法使用。-1 代表无限制。", "support.outlink.Usage points": "积分消耗", "support.outlink.share.Chat_quote_reader": "全文阅读器", + "support.outlink.share.Download source 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.Show full text tips": "查看引用内容所属的完整文件,不可查看原文件或跳转来来源网站", "support.outlink.share.running_node": "运行节点", "support.outlink.share.show_complete_quote": "查看来源原文", "support.outlink.share.show_complete_quote_tips": "查看及下载完整引用文档,或跳转引用网站", diff --git a/packages/web/i18n/zh-Hant/app.json b/packages/web/i18n/zh-Hant/app.json index dcf9eba8f..05c0e51b0 100644 --- a/packages/web/i18n/zh-Hant/app.json +++ b/packages/web/i18n/zh-Hant/app.json @@ -299,6 +299,8 @@ "pro_modal_subtitle": "即刻加入商業版,解鎖更多高級功能", "pro_modal_title": "商業版專享!", "pro_modal_unlock_button": "去解鎖", + "publish.chat_desc": "用戶登錄門戶後可直接與應用對話", + "publish.playground_link": "跳轉鏈接", "publish_channel": "發布通道", "publish_success": "發布成功", "question_guide_tip": "對話結束後,會為你產生 3 個引導性問題。", diff --git a/packages/web/i18n/zh-Hant/common.json b/packages/web/i18n/zh-Hant/common.json index 9fab1a2fa..9dfcb9544 100644 --- a/packages/web/i18n/zh-Hant/common.json +++ b/packages/web/i18n/zh-Hant/common.json @@ -234,7 +234,7 @@ "core.ai.model.Vector Model": "索引模型", "core.ai.model.doc_index_and_dialog": "文件索引與對話索引", "core.app.Api request": "API 存取", - "core.app.Api request desc": "透過 API 整合到現有系統中,或整合到企業微信、飛書等", + "core.app.Api request desc": "通過 API 接入已有系統", "core.app.App intro": "應用程式介紹", "core.app.Auto execute": "自動執行", "core.app.Chat Variable": "對話變數", @@ -264,7 +264,7 @@ "core.app.Set a name for your app": "為您的應用程式命名", "core.app.Setting ai property": "點選設定 AI 模型相關屬性", "core.app.Share link": "免登入視窗", - "core.app.Share link desc": "分享連結給其他使用者,無需登入即可直接使用", + "core.app.Share link desc": "創建可分享的鏈接,支持免登錄使用", "core.app.Share link desc detail": "您可以直接分享此模型給其他使用者進行對話,對方無需登入即可直接使用。請注意,此功能會消耗您帳戶的餘額,請妥善保管連結!", "core.app.TTS": "語音播放", "core.app.TTS Tip": "開啟後,每次對話後可使用語音播放功能。使用此功能可能會產生額外費用。", @@ -315,6 +315,7 @@ "core.app.share.Amount limit tip": "最多 10 組", "core.app.share.Create link": "建立新連結", "core.app.share.Create link tip": "建立成功。已複製分享網址,可直接分享使用", + "core.app.share.Download source": "下載/打開來源原文", "core.app.share.Ip limit title": "IP 限流(人/分鐘)", "core.app.share.Is response quote": "返回引用", "core.app.share.Not share link": "尚未建立分享連結", @@ -1099,9 +1100,11 @@ "support.outlink.Max usage points tip": "此連結最多允許使用多少點數,超出後將無法使用。-1 代表無限制。", "support.outlink.Usage points": "點數消耗", "support.outlink.share.Chat_quote_reader": "全文閱讀器", + "support.outlink.share.Download source 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.Show full text tips": "查看引用內容所屬的完整文件,不可查看原文件或跳轉來來源網站", "support.outlink.share.running_node": "執行節點", "support.outlink.share.show_complete_quote": "檢視原始內容", "support.outlink.share.show_complete_quote_tips": "檢視及下載完整引用文件,或跳轉至引用網站", diff --git a/projects/app/src/components/core/chat/ChatContainer/ChatBox/components/ChatItem.tsx b/projects/app/src/components/core/chat/ChatContainer/ChatBox/components/ChatItem.tsx index 9230d7cda..2612c6a92 100644 --- a/projects/app/src/components/core/chat/ChatContainer/ChatBox/components/ChatItem.tsx +++ b/projects/app/src/components/core/chat/ChatContainer/ChatBox/components/ChatItem.tsx @@ -163,12 +163,12 @@ const ChatItem = (props: Props) => { const isChatting = useContextSelector(ChatBoxContext, (v) => v.isChatting); 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 chatId = useContextSelector(WorkflowRuntimeContext, (v) => v.chatId); 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( () => addStatisticalDataToHistoryItem(chat), @@ -258,7 +258,7 @@ const ChatItem = (props: Props) => { setCiteModalData({ rawSearch: quoteList, metadata: - item?.collectionId && isShowReadRawSource + item?.collectionId && isShowFullText ? { appId: appId, chatId: chatId, @@ -322,7 +322,7 @@ const ChatItem = (props: Props) => { {/* Workflow status */} - {!!chatStatusMap && statusBoxData && isLastChild && showNodeStatus && ( + {!!chatStatusMap && statusBoxData && isLastChild && showRunningStatus && ( v.isShowReadRawSource); + const canDownloadSource = useContextSelector(ChatItemContext, (v) => v.canDownloadSource); const showRouteToDatasetDetail = useContextSelector( ChatItemContext, (v) => v.showRouteToDatasetDetail @@ -87,7 +87,7 @@ const QuoteList = React.memo(function QuoteList({ > v.isShowCite); const { totalQuoteList: quoteList = [], llmModuleAccount = 0, historyPreviewLength = 0, toolCiteLinks = [] - } = useMemo(() => addStatisticalDataToHistoryItem(historyItem), [historyItem]); + } = useMemo(() => { + return { + ...addStatisticalDataToHistoryItem(historyItem), + ...(isShowCite + ? { + totalQuoteList: [] + } + : {}) + }; + }, [historyItem, isShowCite]); const [quoteFolded, setQuoteFolded] = useState(true); @@ -78,6 +89,7 @@ const ResponseTags = ({ : true; const citationRenderList: CitationRenderItem[] = useMemo(() => { + if (!isShowCite) return []; // Dataset citations const datasetItems = Object.values( quoteList.reduce((acc: Record, cur) => { @@ -116,7 +128,7 @@ const ResponseTags = ({ })); return [...datasetItems, ...linkItems]; - }, [quoteList, toolCiteLinks, onOpenCiteModal]); + }, [quoteList, toolCiteLinks, onOpenCiteModal, isShowCite]); const notEmptyTags = notSharePage || quoteList.length > 0 || (isPc && durationSeconds > 0); diff --git a/projects/app/src/components/core/chat/components/AIResponseBox.tsx b/projects/app/src/components/core/chat/components/AIResponseBox.tsx index 09820e0d5..1a05f0692 100644 --- a/projects/app/src/components/core/chat/components/AIResponseBox.tsx +++ b/projects/app/src/components/core/chat/components/AIResponseBox.tsx @@ -31,9 +31,13 @@ import { eventBus, EventNameEnum } from '@/web/common/utils/eventbus'; import { SelectOptionsComponent, FormInputComponent } from './Interactive/InteractiveComponents'; import { extractDeepestInteractive } from '@fastgpt/global/core/workflow/runtime/utils'; 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 { useCreation } from 'ahooks'; +import { removeDatasetCiteText } from '@fastgpt/global/core/ai/llm/utils'; const accordionButtonStyle = { w: 'auto', @@ -102,13 +106,16 @@ const RenderText = React.memo(function RenderText({ const appId = useContextSelector(WorkflowRuntimeContext, (v) => v.appId); const chatId = useContextSelector(WorkflowRuntimeContext, (v) => v.chatId); const outLinkAuthData = useContextSelector(WorkflowRuntimeContext, (v) => v.outLinkAuthData); + const isShowCite = useContextSelector(ChatItemContext, (v) => v.isShowCite); const source = useMemo(() => { if (!text) return ''; - // Remove quote references if not showing response detail - return text; - }, [text]); + if (isShowCite) { + return text; + } + return removeDatasetCiteText(text, isShowCite); + }, [text, isShowCite]); const chatAuthData = useCreation(() => { return { appId, chatId, chatItemDataId, ...outLinkAuthData }; @@ -329,6 +336,8 @@ const AIResponseBox = ({ isChatting: boolean; onOpenCiteModal?: (e?: OnOpenCiteModalProps) => void; }) => { + const showRunningStatus = useContextSelector(ChatItemContext, (v) => v.showRunningStatus); + if (value.type === ChatItemValueTypeEnum.text && value.text) { return ( ); } - if (value.type === ChatItemValueTypeEnum.tool && value.tools) { + if (value.type === ChatItemValueTypeEnum.tool && value.tools && showRunningStatus) { return ; } if (value.type === ChatItemValueTypeEnum.interactive && value.interactive) { diff --git a/projects/app/src/components/core/dataset/QuoteItem.tsx b/projects/app/src/components/core/dataset/QuoteItem.tsx index bbf79e9d2..3e8bcaaf2 100644 --- a/projects/app/src/components/core/dataset/QuoteItem.tsx +++ b/projects/app/src/components/core/dataset/QuoteItem.tsx @@ -91,13 +91,13 @@ export const formatScore = (score: ScoreItemType[]) => { const QuoteItem = ({ quoteItem, - canViewSource, + canDownloadSource, canEditData, canEditDataset, ...RawSourceBoxProps }: { quoteItem: SearchDataResponseItemType; - canViewSource?: boolean; + canDownloadSource?: boolean; canEditData?: boolean; canEditDataset?: boolean; } & Omit) => { @@ -208,7 +208,7 @@ const QuoteItem = ({ collectionId={quoteItem.collectionId} sourceName={quoteItem.sourceName} sourceId={quoteItem.sourceId} - canView={canViewSource} + canView={canDownloadSource} {...RawSourceBoxProps} /> diff --git a/projects/app/src/pageComponents/app/detail/HTTPTools/ChatTest.tsx b/projects/app/src/pageComponents/app/detail/HTTPTools/ChatTest.tsx index c1d814324..2dd30e33c 100644 --- a/projects/app/src/pageComponents/app/detail/HTTPTools/ChatTest.tsx +++ b/projects/app/src/pageComponents/app/detail/HTTPTools/ChatTest.tsx @@ -205,10 +205,10 @@ const Render = ({ return ( { return ( diff --git a/projects/app/src/pageComponents/app/detail/Publish/DingTalk/index.tsx b/projects/app/src/pageComponents/app/detail/Publish/DingTalk/index.tsx index ecdbb262a..65acf43f3 100644 --- a/projects/app/src/pageComponents/app/detail/Publish/DingTalk/index.tsx +++ b/projects/app/src/pageComponents/app/detail/Publish/DingTalk/index.tsx @@ -186,7 +186,7 @@ const DingTalk = ({ appId }: { appId: string }) => { name: item.name, limit: item.limit, app: item.app, - responseDetail: item.responseDetail, + showCite: item.showCite, defaultResponse: item.defaultResponse, immediateResponse: item.immediateResponse }); diff --git a/projects/app/src/pageComponents/app/detail/Publish/FeiShu/index.tsx b/projects/app/src/pageComponents/app/detail/Publish/FeiShu/index.tsx index 491af9c53..5cb2687af 100644 --- a/projects/app/src/pageComponents/app/detail/Publish/FeiShu/index.tsx +++ b/projects/app/src/pageComponents/app/detail/Publish/FeiShu/index.tsx @@ -185,7 +185,7 @@ const FeiShu = ({ appId }: { appId: string }) => { name: item.name, limit: item.limit, app: item.app, - responseDetail: item.responseDetail, + showCite: item.showCite, defaultResponse: item.defaultResponse, immediateResponse: item.immediateResponse }); diff --git a/projects/app/src/pageComponents/app/detail/Publish/Link/index.tsx b/projects/app/src/pageComponents/app/detail/Publish/Link/index.tsx index 124fb3834..8f7cc9927 100644 --- a/projects/app/src/pageComponents/app/detail/Publish/Link/index.tsx +++ b/projects/app/src/pageComponents/app/detail/Publish/Link/index.tsx @@ -140,7 +140,7 @@ const Share = ({ appId }: { appId: string; type: PublishChannelEnum }) => { }` : ''} - {item.responseDetail ? '✔' : '✖'} + {item.showCite ? '✔' : '✖'} {feConfigs?.isPlus && ( <> {item?.limit?.QPM || '-'} @@ -182,10 +182,10 @@ const Share = ({ appId }: { appId: string; type: PublishChannelEnum }) => { setEditLinkData({ _id: item._id, name: item.name, - responseDetail: item.responseDetail ?? false, - showRawSource: item.showRawSource ?? false, - // showFullText: item.showFullText ?? false, - showNodeStatus: item.showNodeStatus ?? false, + showCite: item.showCite, + canDownloadSource: item.canDownloadSource, + showFullText: item.showFullText, + showRunningStatus: item.showRunningStatus, limit: item.limit }) }, @@ -281,9 +281,9 @@ function EditLinkModal({ defaultValues: defaultData }); - const responseDetail = watch('responseDetail'); - // const showFullText = watch('showFullText'); - const showRawSource = watch('showRawSource'); + const showCite = watch('showCite'); + const showFullText = watch('showFullText'); + const canDownloadSource = watch('canDownloadSource'); const isEdit = useMemo(() => !!defaultData._id, [defaultData]); @@ -413,7 +413,7 @@ function EditLinkModal({ {t('publish:show_node')} - + @@ -424,56 +424,56 @@ function EditLinkModal({ > - {/* + - {t('common:support.outlink.share.Chat_quote_reader')} + {t('common:core.app.share.Show full text')} - */} + - {t('common:support.outlink.share.show_complete_quote')} + {t('common:core.app.share.Download source')} diff --git a/projects/app/src/pageComponents/app/detail/Publish/OffiAccount/index.tsx b/projects/app/src/pageComponents/app/detail/Publish/OffiAccount/index.tsx index d2818bfaf..08c458115 100644 --- a/projects/app/src/pageComponents/app/detail/Publish/OffiAccount/index.tsx +++ b/projects/app/src/pageComponents/app/detail/Publish/OffiAccount/index.tsx @@ -188,7 +188,7 @@ const OffiAccount = ({ appId }: { appId: string }) => { name: item.name, limit: item.limit, app: item.app, - responseDetail: item.responseDetail, + showCite: item.showCite, defaultResponse: item.defaultResponse, immediateResponse: item.immediateResponse }); diff --git a/projects/app/src/pageComponents/app/detail/Publish/Playground/index.tsx b/projects/app/src/pageComponents/app/detail/Publish/Playground/index.tsx new file mode 100644 index 000000000..88d7a3511 --- /dev/null +++ b/projects/app/src/pageComponents/app/detail/Publish/Playground/index.tsx @@ -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 ( + + + {t('app:publish.playground_link')} + + + + + + {t('common:core.app.outLink.Link block title')} + + copyData(playgroundLink)} + /> + + + {playgroundLink} + + + + + {t('publish:private_config')} + + + + + + {t('publish:show_node')} + + + + + + + {t('common:support.outlink.share.Response Quote')} + + + + + + + + + + + {t('common:core.app.share.Show full text')} + + + + + + + + {t('common:core.app.share.Download source')} + + + + + + + + ); +}; + +export default PlaygroundVisibilityConfig; diff --git a/projects/app/src/pageComponents/app/detail/Publish/Wecom/index.tsx b/projects/app/src/pageComponents/app/detail/Publish/Wecom/index.tsx index 1f8015523..54814301a 100644 --- a/projects/app/src/pageComponents/app/detail/Publish/Wecom/index.tsx +++ b/projects/app/src/pageComponents/app/detail/Publish/Wecom/index.tsx @@ -200,7 +200,7 @@ const Wecom = ({ appId }: { appId: string }) => { name: item.name, limit: item.limit, app: item.app, - responseDetail: item.responseDetail, + showCite: item.showCite, defaultResponse: item.defaultResponse, immediateResponse: item.immediateResponse }); diff --git a/projects/app/src/pageComponents/app/detail/Publish/index.tsx b/projects/app/src/pageComponents/app/detail/Publish/index.tsx index 381a2804c..96a0a41dc 100644 --- a/projects/app/src/pageComponents/app/detail/Publish/index.tsx +++ b/projects/app/src/pageComponents/app/detail/Publish/index.tsx @@ -19,6 +19,7 @@ const FeiShu = dynamic(() => import('./FeiShu')); const DingTalk = dynamic(() => import('./DingTalk')); const Wecom = dynamic(() => import('./Wecom')); const OffiAccount = dynamic(() => import('./OffiAccount')); +const Playground = dynamic(() => import('./Playground')); const OutLink = () => { const { t } = useTranslation(); @@ -85,7 +86,14 @@ const OutLink = () => { 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.share); @@ -141,6 +149,7 @@ const OutLink = () => { {linkType === PublishChannelEnum.dingtalk && } {linkType === PublishChannelEnum.wecom && } {linkType === PublishChannelEnum.officialAccount && } + {linkType === PublishChannelEnum.playground && } ); diff --git a/projects/app/src/pageComponents/app/detail/SimpleApp/ChatTest.tsx b/projects/app/src/pageComponents/app/detail/SimpleApp/ChatTest.tsx index 4a7bdbf79..1c7e81ccb 100644 --- a/projects/app/src/pageComponents/app/detail/SimpleApp/ChatTest.tsx +++ b/projects/app/src/pageComponents/app/detail/SimpleApp/ChatTest.tsx @@ -118,10 +118,10 @@ const Render = ({ appForm, setRenderEdit }: Props) => { return ( diff --git a/projects/app/src/pageComponents/app/detail/WorkflowComponents/Flow/ChatTest.tsx b/projects/app/src/pageComponents/app/detail/WorkflowComponents/Flow/ChatTest.tsx index a4983d396..fb0567076 100644 --- a/projects/app/src/pageComponents/app/detail/WorkflowComponents/Flow/ChatTest.tsx +++ b/projects/app/src/pageComponents/app/detail/WorkflowComponents/Flow/ChatTest.tsx @@ -206,10 +206,10 @@ const Render = (Props: Props) => { return ( diff --git a/projects/app/src/pageComponents/chat/ChatFavouriteApp/index.tsx b/projects/app/src/pageComponents/chat/ChatFavouriteApp/index.tsx index 36c109a7b..8e8b78241 100644 --- a/projects/app/src/pageComponents/chat/ChatFavouriteApp/index.tsx +++ b/projects/app/src/pageComponents/chat/ChatFavouriteApp/index.tsx @@ -18,7 +18,7 @@ import MyIcon from '@fastgpt/web/components/common/Icon'; import { useTranslation } from 'react-i18next'; import { useForm } from 'react-hook-form'; 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 Avatar from '@fastgpt/web/components/common/Avatar'; import { ChatSettingTabOptionEnum, ChatSidebarPaneEnum } from '@/pageComponents/chat/constants'; @@ -41,11 +41,11 @@ const ChatFavouriteApp = () => { const onOpenSlider = useContextSelector(ChatContext, (v) => v.onOpenSlider); - const handlePaneChange = useContextSelector(ChatSettingContext, (v) => v.handlePaneChange); - const wideLogoUrl = useContextSelector(ChatSettingContext, (v) => v.chatSettings?.wideLogoUrl); - const homeTabTitle = useContextSelector(ChatSettingContext, (v) => v.chatSettings?.homeTabTitle); + const handlePaneChange = useContextSelector(ChatPageContext, (v) => v.handlePaneChange); + const wideLogoUrl = useContextSelector(ChatPageContext, (v) => v.chatSettings?.wideLogoUrl); + 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(() => { return tags.reduce( (acc, tag) => { diff --git a/projects/app/src/pageComponents/chat/ChatHeader.tsx b/projects/app/src/pageComponents/chat/ChatHeader.tsx index 4a37a7dc4..b680436ee 100644 --- a/projects/app/src/pageComponents/chat/ChatHeader.tsx +++ b/projects/app/src/pageComponents/chat/ChatHeader.tsx @@ -14,7 +14,6 @@ import { AppFolderTypeList, AppTypeEnum } from '@fastgpt/global/core/app/constan import { useSystem } from '@fastgpt/web/hooks/useSystem'; import LightRowTabs from '@fastgpt/web/components/common/Tabs/LightRowTabs'; import { useRouter } from 'next/router'; -import { type AppListItemType } from '@fastgpt/global/core/app/type'; import { type GetResourceFolderListProps, type GetResourceListItemResponse @@ -24,7 +23,7 @@ import SelectOneResource from '@/components/common/folder/SelectOneResource'; import { ChatItemContext } from '@/web/core/chat/context/chatItemContext'; import VariablePopover from '@/components/core/chat/ChatContainer/components/VariablePopover'; import { useCopyData } from '@fastgpt/web/hooks/useCopyData'; -import { ChatSettingContext } from '@/web/core/chat/context/chatSettingContext'; +import { ChatPageContext } from '@/web/core/chat/context/chatPageContext'; import { ChatSidebarPaneEnum, DEFAULT_LOGO_BANNER_COLLAPSED_URL @@ -38,7 +37,6 @@ import { ChatTypeEnum } from '@/components/core/chat/ChatContainer/ChatBox/const const ChatHeader = ({ history, showHistory, - apps, totalRecordsCount, pane, @@ -50,18 +48,15 @@ const ChatHeader = ({ history: ChatItemType[]; showHistory?: boolean; - apps?: AppListItemType[]; totalRecordsCount: number; reserveSpace?: boolean; }) => { const { t } = useTranslation(); const { isPc } = useSystem(); - const pathname = usePathname(); const { source } = useChatStore(); const chatData = useContextSelector(ChatItemContext, (v) => v.chatBoxData); const isVariableVisible = useContextSelector(ChatItemContext, (v) => v.isVariableVisible); - const isPlugin = chatData.app.type === AppTypeEnum.workflowTool; const isShare = source === 'share'; const chatType = isShare ? ChatTypeEnum.share : ChatTypeEnum.chat; @@ -87,7 +82,6 @@ const ChatHeader = ({ ) : ( void; - appId: string; - apps?: AppListItemType[]; -}) => { +const MobileDrawer = ({ onCloseDrawer, appId }: { onCloseDrawer: () => void; appId: string }) => { enum TabEnum { recently = 'recently', app = 'app' @@ -129,6 +115,7 @@ const MobileDrawer = ({ const { t } = useTranslation(); const { setChatId } = useChatStore(); + const myApps = useContextSelector(ChatPageContext, (v) => v.myApps); const [currentTab, setCurrentTab] = useState(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) => { handlePaneChange(ChatSidebarPaneEnum.RECENTLY_USED_APPS, id); @@ -201,22 +188,22 @@ const MobileDrawer = ({ {/* history */} {currentTab === TabEnum.recently && ( - {Array.isArray(apps) && - apps.map((item) => ( - + {Array.isArray(myApps) && + myApps.map((item) => ( + onclickApp(item._id) + onClick: () => onclickApp(item.appId) })} > @@ -247,13 +234,11 @@ const MobileHeader = ({ showHistory, name, avatar, - appId, - apps + appId }: { showHistory?: boolean; avatar: string; name: string; - apps?: AppListItemType[]; appId: string; }) => { const router = useRouter(); @@ -290,9 +275,7 @@ const MobileHeader = ({ - {isOpenDrawer && !isShareChat && ( - - )} + {isOpenDrawer && !isShareChat && } ); }; diff --git a/projects/app/src/pageComponents/chat/ChatQuoteList/CollectionQuoteReader.tsx b/projects/app/src/pageComponents/chat/ChatQuoteList/CollectionQuoteReader.tsx index a10cc1613..0d93d693e 100644 --- a/projects/app/src/pageComponents/chat/ChatQuoteList/CollectionQuoteReader.tsx +++ b/projects/app/src/pageComponents/chat/ChatQuoteList/CollectionQuoteReader.tsx @@ -23,6 +23,8 @@ import { getCollectionQuote } from '@/web/core/chat/api'; import MyIconButton from '@fastgpt/web/components/common/Icon/button'; import MyBox from '@fastgpt/web/components/common/MyBox'; import { getCollectionSourceAndOpen } from '@/web/core/dataset/hooks/readCollectionSource'; +import { useContextSelector } from 'use-context-selector'; +import { ChatItemContext } from '@/web/core/chat/context/chatItemContext'; const CollectionReader = ({ rawSearch, @@ -37,6 +39,8 @@ const CollectionReader = ({ const router = useRouter(); const { userInfo } = useUserStore(); + const canDownloadSource = useContextSelector(ChatItemContext, (v) => v.canDownloadSource); + const { collectionId, datasetId, chatItemDataId, sourceId, sourceName, quoteId } = metadata; const [quoteIndex, setQuoteIndex] = useState(0); @@ -175,11 +179,13 @@ const CollectionReader = ({ {sourceName || t('common:unknow_source')} - + {canDownloadSource && ( + + )} { - const appId = useContextSelector(ChatSettingContext, (v) => v.chatSettings?.appId || ''); + const appId = useContextSelector(ChatPageContext, (v) => v.chatSettings?.appId || ''); const [dateRange, setDateRange] = useState({ from: new Date(addDays(new Date(), -6).setHours(0, 0, 0, 0)), diff --git a/projects/app/src/pageComponents/chat/ChatSetting/FavouriteAppSetting/TagManageModal.tsx b/projects/app/src/pageComponents/chat/ChatSetting/FavouriteAppSetting/TagManageModal.tsx index 56e7cd9a3..2192f636a 100644 --- a/projects/app/src/pageComponents/chat/ChatSetting/FavouriteAppSetting/TagManageModal.tsx +++ b/projects/app/src/pageComponents/chat/ChatSetting/FavouriteAppSetting/TagManageModal.tsx @@ -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 { Box, @@ -377,10 +377,10 @@ type Props = { const TagManageModal = ({ onClose, onRefresh }: Props) => { const { t } = useTranslation(); - const refreshChatSetting = useContextSelector(ChatSettingContext, (v) => v.refreshChatSetting); + const refreshChatSetting = useContextSelector(ChatPageContext, (v) => v.refreshChatSetting); // get tags from db - const tags = useContextSelector(ChatSettingContext, (v) => v.chatSettings?.favouriteTags || []); + const tags = useContextSelector(ChatPageContext, (v) => v.chatSettings?.favouriteTags || []); // local editable tags list const [localTags, setLocalTags] = useState(tags); diff --git a/projects/app/src/pageComponents/chat/ChatSetting/FavouriteAppSetting/index.tsx b/projects/app/src/pageComponents/chat/ChatSetting/FavouriteAppSetting/index.tsx index 6e11dd166..3262bd666 100644 --- a/projects/app/src/pageComponents/chat/ChatSetting/FavouriteAppSetting/index.tsx +++ b/projects/app/src/pageComponents/chat/ChatSetting/FavouriteAppSetting/index.tsx @@ -1,20 +1,12 @@ -import { ChatSettingContext } from '@/web/core/chat/context/chatSettingContext'; +import { ChatPageContext } from '@/web/core/chat/context/chatPageContext'; import { Button, - ButtonGroup, Flex, HStack, IconButton, Input, InputGroup, InputLeftElement, - Table, - TableContainer, - Tbody, - Td, - Th, - Thead, - Tr, useDisclosure } from '@chakra-ui/react'; import MySelect from '@fastgpt/web/components/common/MySelect'; @@ -68,7 +60,7 @@ const FavouriteAppSetting = ({ Header }: Props) => { const searchAppTagValue = watchSearchValue('tag'); // apps' tags options - const tagOptions = useContextSelector(ChatSettingContext, (v) => { + const tagOptions = useContextSelector(ChatPageContext, (v) => { const tags = v.chatSettings?.favouriteTags || []; return [ { label: t('chat:setting.favourite.category_all'), value: '' }, @@ -76,7 +68,7 @@ const FavouriteAppSetting = ({ Header }: Props) => { ]; }); // app's tags cache map - const tagMap = useContextSelector(ChatSettingContext, (v) => + const tagMap = useContextSelector(ChatPageContext, (v) => (v.chatSettings?.favouriteTags || []).reduce>( (acc, tag) => { acc[tag.id] = { ...tag }; diff --git a/projects/app/src/pageComponents/chat/ChatSetting/HomepageSetting/index.tsx b/projects/app/src/pageComponents/chat/ChatSetting/HomepageSetting/index.tsx index b224d69bc..bdafa29c6 100644 --- a/projects/app/src/pageComponents/chat/ChatSetting/HomepageSetting/index.tsx +++ b/projects/app/src/pageComponents/chat/ChatSetting/HomepageSetting/index.tsx @@ -23,7 +23,7 @@ import Avatar from '@fastgpt/web/components/common/Avatar'; import type { NodeInputKeyEnum } from '@fastgpt/global/core/workflow/constants'; import { useMount } from 'ahooks'; import { useContextSelector } from 'use-context-selector'; -import { ChatSettingContext } from '@/web/core/chat/context/chatSettingContext'; +import { ChatPageContext } from '@/web/core/chat/context/chatPageContext'; import { DEFAULT_LOGO_BANNER_COLLAPSED_URL, DEFAULT_LOGO_BANNER_URL @@ -46,8 +46,8 @@ const HomepageSetting = ({ Header, onDiagramShow }: Props) => { const { t } = useTranslation(); const { feConfigs } = useSystemStore(); - const chatSettings = useContextSelector(ChatSettingContext, (v) => v.chatSettings); - const refreshChatSetting = useContextSelector(ChatSettingContext, (v) => v.refreshChatSetting); + const chatSettings = useContextSelector(ChatPageContext, (v) => v.chatSettings); + const refreshChatSetting = useContextSelector(ChatPageContext, (v) => v.refreshChatSetting); const chatSettings2Form = useCallback( (data?: ChatSettingType) => { diff --git a/projects/app/src/pageComponents/chat/ChatSetting/LogDetails.tsx b/projects/app/src/pageComponents/chat/ChatSetting/LogDetails.tsx index c7da09305..9c3bac4b5 100644 --- a/projects/app/src/pageComponents/chat/ChatSetting/LogDetails.tsx +++ b/projects/app/src/pageComponents/chat/ChatSetting/LogDetails.tsx @@ -1,5 +1,5 @@ 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 { ChatSourceEnum } from '@fastgpt/global/core/chat/constants'; import type { DateRangeType } from '@fastgpt/web/components/common/DateRangePicker'; @@ -16,7 +16,7 @@ type Props = { const chatSourceValues = Object.values(ChatSourceEnum); const LogDetails = ({ Header }: Props) => { - const appId = useContextSelector(ChatSettingContext, (v) => v.chatSettings?.appId || ''); + const appId = useContextSelector(ChatPageContext, (v) => v.chatSettings?.appId || ''); const [dateRange, setDateRange] = useState({ from: new Date(addDays(new Date(), -6).setHours(0, 0, 0, 0)), diff --git a/projects/app/src/pageComponents/chat/ChatSetting/index.tsx b/projects/app/src/pageComponents/chat/ChatSetting/index.tsx index 9587874f3..521c3dd00 100644 --- a/projects/app/src/pageComponents/chat/ChatSetting/index.tsx +++ b/projects/app/src/pageComponents/chat/ChatSetting/index.tsx @@ -9,7 +9,7 @@ import { useContextSelector } from 'use-context-selector'; import MyIcon from '@fastgpt/web/components/common/Icon'; import { ChatContext } from '@/web/core/chat/context/chatContext'; 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 { useTranslation } from 'react-i18next'; import { useMount } from 'ahooks'; @@ -43,8 +43,8 @@ const ChatSetting = () => { ); const onOpenSlider = useContextSelector(ChatContext, (v) => v.onOpenSlider); - const chatSettings = useContextSelector(ChatSettingContext, (v) => v.chatSettings); - const handlePaneChange = useContextSelector(ChatSettingContext, (v) => v.handlePaneChange); + const chatSettings = useContextSelector(ChatPageContext, (v) => v.chatSettings); + const handlePaneChange = useContextSelector(ChatPageContext, (v) => v.handlePaneChange); const handleTabChange = useCallback( (tab: ChatSettingTabOptionEnum) => { diff --git a/projects/app/src/pageComponents/chat/ChatTeamApp/List.tsx b/projects/app/src/pageComponents/chat/ChatTeamApp/List.tsx index 98fb90c69..f6d2c90d5 100644 --- a/projects/app/src/pageComponents/chat/ChatTeamApp/List.tsx +++ b/projects/app/src/pageComponents/chat/ChatTeamApp/List.tsx @@ -15,7 +15,7 @@ import AppTypeTag from '@/pageComponents/chat/ChatTeamApp/TypeTag'; import { formatTimeToChatTime } from '@fastgpt/global/common/string/time'; import { useSystem } from '@fastgpt/web/hooks/useSystem'; 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'; const List = ({ appType }: { appType: AppTypeEnum | 'all' }) => { @@ -33,7 +33,7 @@ const List = ({ appType }: { appType: AppTypeEnum | 'all' }) => { ].includes(app.type) ) ); - const handlePaneChange = useContextSelector(ChatSettingContext, (v) => v.handlePaneChange); + const handlePaneChange = useContextSelector(ChatPageContext, (v) => v.handlePaneChange); return ( <> diff --git a/projects/app/src/pageComponents/chat/ChatTeamApp/index.tsx b/projects/app/src/pageComponents/chat/ChatTeamApp/index.tsx index 709d0d23b..d688c5d23 100644 --- a/projects/app/src/pageComponents/chat/ChatTeamApp/index.tsx +++ b/projects/app/src/pageComponents/chat/ChatTeamApp/index.tsx @@ -13,7 +13,7 @@ import SearchInput from '@fastgpt/web/components/common/Input/SearchInput'; import MyIcon from '@fastgpt/web/components/common/Icon'; import { ChatContext } from '@/web/core/chat/context/chatContext'; 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 { useSystemStore } from '@/web/common/system/useSystemStore'; import { getWebReqUrl } from '@fastgpt/web/common/system/utils'; @@ -29,7 +29,7 @@ const MyApps = () => { (v) => v ); - const chatSettings = useContextSelector(ChatSettingContext, (v) => v.chatSettings); + const chatSettings = useContextSelector(ChatPageContext, (v) => v.chatSettings); const onOpenSlider = useContextSelector(ChatContext, (v) => v.onOpenSlider); diff --git a/projects/app/src/pageComponents/chat/ChatWindow/AppChatWindow.tsx b/projects/app/src/pageComponents/chat/ChatWindow/AppChatWindow.tsx index 07bdaf7a4..4e8974924 100644 --- a/projects/app/src/pageComponents/chat/ChatWindow/AppChatWindow.tsx +++ b/projects/app/src/pageComponents/chat/ChatWindow/AppChatWindow.tsx @@ -7,7 +7,6 @@ import SideBar from '@/components/SideBar'; import { ChatContext } from '@/web/core/chat/context/chatContext'; import { useContextSelector } from 'use-context-selector'; 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 { useCallback } from 'react'; 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 { useUserStore } from '@/web/support/user/useUserStore'; 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 ChatHistorySidebar from '@/pageComponents/chat/slider/ChatSliderSidebar'; import ChatSliderMobileDrawer from '@/pageComponents/chat/slider/ChatSliderMobileDrawer'; import dynamic from 'next/dynamic'; import { getNanoid } from '@fastgpt/global/common/string/tools'; 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')); -type Props = { - myApps: AppListItemType[]; -}; - -const AppChatWindow = ({ myApps }: Props) => { +const AppChatWindow = () => { const { userInfo } = useUserStore(); const { chatId, appId, outLinkAuthData } = useChatStore(); @@ -45,6 +41,7 @@ const AppChatWindow = ({ myApps }: Props) => { const onUpdateHistoryTitle = useContextSelector(ChatContext, (v) => v.onUpdateHistoryTitle); const isPlugin = useContextSelector(ChatItemContext, (v) => v.isPlugin); + const isShowCite = useContextSelector(ChatItemContext, (v) => v.isShowCite); const onChangeChatId = useContextSelector(ChatContext, (v) => v.onChangeChatId); const chatBoxData = useContextSelector(ChatItemContext, (v) => v.chatBoxData); const datasetCiteData = useContextSelector(ChatItemContext, (v) => v.datasetCiteData); @@ -54,9 +51,10 @@ const AppChatWindow = ({ myApps }: Props) => { const chatRecords = useContextSelector(ChatRecordContext, (v) => v.chatRecords); const totalRecordsCount = useContextSelector(ChatRecordContext, (v) => v.totalRecordsCount); - const pane = useContextSelector(ChatSettingContext, (v) => v.pane); - const chatSettings = useContextSelector(ChatSettingContext, (v) => v.chatSettings); - const handlePaneChange = useContextSelector(ChatSettingContext, (v) => v.handlePaneChange); + const pane = useContextSelector(ChatPageContext, (v) => v.pane); + const chatSettings = useContextSelector(ChatPageContext, (v) => v.chatSettings); + const handlePaneChange = useContextSelector(ChatPageContext, (v) => v.handlePaneChange); + const refreshRecentlyUsed = useContextSelector(ChatPageContext, (v) => v.refreshRecentlyUsed); const { loading } = useRequest2( async () => { @@ -82,6 +80,9 @@ const AppChatWindow = ({ myApps }: Props) => { onChangeChatId(); return; } + if (e?.statusText === AppErrEnum.unAuthApp) { + refreshRecentlyUsed(); + } handlePaneChange(ChatSidebarPaneEnum.TEAM_APPS); } }, @@ -106,7 +107,8 @@ const AppChatWindow = ({ myApps }: Props) => { variables, responseChatItemId, appId, - chatId + chatId, + retainDatasetCite: isShowCite }, abortCtrl: controller, onMessage: generatingMessage @@ -120,9 +122,19 @@ const AppChatWindow = ({ myApps }: Props) => { title: newTitle })); + refreshRecentlyUsed(); + return { responseText, isNewChat: forbidLoadChat.current }; }, - [appId, chatId, onUpdateHistoryTitle, setChatBoxData, forbidLoadChat] + [ + appId, + chatId, + onUpdateHistoryTitle, + setChatBoxData, + forbidLoadChat, + isShowCite, + refreshRecentlyUsed + ] ); return ( @@ -156,7 +168,6 @@ const AppChatWindow = ({ myApps }: Props) => { pane={pane} chatSettings={chatSettings} showHistory - apps={myApps} history={chatRecords} totalRecordsCount={totalRecordsCount} /> diff --git a/projects/app/src/pageComponents/chat/ChatWindow/HomeChatWindow.tsx b/projects/app/src/pageComponents/chat/ChatWindow/HomeChatWindow.tsx index 820487955..131e72da3 100644 --- a/projects/app/src/pageComponents/chat/ChatWindow/HomeChatWindow.tsx +++ b/projects/app/src/pageComponents/chat/ChatWindow/HomeChatWindow.tsx @@ -36,12 +36,8 @@ import { getDefaultAppForm } from '@fastgpt/global/core/app/utils'; import { getToolPreviewNode } from '@/web/core/app/api/tool'; import type { FlowNodeTemplateType } from '@fastgpt/global/core/workflow/type/node'; import { getWebLLMModel } from '@/web/common/system/utils'; -import { ChatSettingContext } from '@/web/core/chat/context/chatSettingContext'; -import type { - AppFileSelectConfigType, - AppListItemType, - AppWhisperConfigType -} from '@fastgpt/global/core/app/type'; +import { ChatPageContext } from '@/web/core/chat/context/chatPageContext'; +import type { AppFileSelectConfigType, AppWhisperConfigType } from '@fastgpt/global/core/app/type'; import ChatHeader from '@/pageComponents/chat/ChatHeader'; import { ChatRecordContext } from '@/web/core/chat/context/chatRecordContext'; import { ChatSidebarPaneEnum } from '../constants'; @@ -49,10 +45,6 @@ import ChatHistorySidebar from '@/pageComponents/chat/slider/ChatSliderSidebar'; import ChatSliderMobileDrawer from '@/pageComponents/chat/slider/ChatSliderMobileDrawer'; import { getWebReqUrl } from '@fastgpt/web/common/system/utils'; -type Props = { - myApps: AppListItemType[]; -}; - const defaultFileSelectConfig: AppFileSelectConfigType = { maxFiles: 20, canSelectFile: true, @@ -68,7 +60,7 @@ const defaultWhisperConfig: AppWhisperConfigType = { autoTTSResponse: false }; -const HomeChatWindow = ({ myApps }: Props) => { +const HomeChatWindow = () => { const { t } = useTranslation(); const { isPc } = useSystem(); @@ -84,11 +76,13 @@ const HomeChatWindow = ({ myApps }: Props) => { const datasetCiteData = useContextSelector(ChatItemContext, (v) => v.datasetCiteData); const setChatBoxData = useContextSelector(ChatItemContext, (v) => v.setChatBoxData); const resetVariables = useContextSelector(ChatItemContext, (v) => v.resetVariables); + const isShowCite = useContextSelector(ChatItemContext, (v) => v.isShowCite); - const pane = useContextSelector(ChatSettingContext, (v) => v.pane); - const chatSettings = useContextSelector(ChatSettingContext, (v) => v.chatSettings); - const handlePaneChange = useContextSelector(ChatSettingContext, (v) => v.handlePaneChange); - const homeAppId = useContextSelector(ChatSettingContext, (v) => v.chatSettings?.appId || ''); + const pane = useContextSelector(ChatPageContext, (v) => v.pane); + const chatSettings = useContextSelector(ChatPageContext, (v) => v.chatSettings); + const handlePaneChange = useContextSelector(ChatPageContext, (v) => v.handlePaneChange); + const homeAppId = useContextSelector(ChatPageContext, (v) => v.chatSettings?.appId || ''); + const refreshRecentlyUsed = useContextSelector(ChatPageContext, (v) => v.refreshRecentlyUsed); const chatRecords = useContextSelector(ChatRecordContext, (v) => v.chatRecords); const totalRecordsCount = useContextSelector(ChatRecordContext, (v) => v.totalRecordsCount); @@ -216,7 +210,8 @@ const HomeChatWindow = ({ myApps }: Props) => { variables, responseChatItemId, appId, - chatId + chatId, + retainDatasetCite: isShowCite }, abortCtrl: controller, onMessage: generatingMessage @@ -230,6 +225,8 @@ const HomeChatWindow = ({ myApps }: Props) => { title: newTitle })); + refreshRecentlyUsed(); + return { responseText, isNewChat: forbidLoadChat.current }; } @@ -264,6 +261,7 @@ const HomeChatWindow = ({ myApps }: Props) => { appId, appName: t('chat:home.chat_app'), chatId, + retainDatasetCite: isShowCite, ...form2AppWorkflow(formData, t) }, onMessage: generatingMessage, @@ -278,6 +276,8 @@ const HomeChatWindow = ({ myApps }: Props) => { title: newTitle })); + refreshRecentlyUsed(); + return { responseText, isNewChat: forbidLoadChat.current }; } ); @@ -394,7 +394,8 @@ const HomeChatWindow = ({ myApps }: Props) => { setSelectedToolIds, setChatBoxData, isPc, - isQuickApp + isQuickApp, + isShowCite ] ); @@ -445,7 +446,6 @@ const HomeChatWindow = ({ myApps }: Props) => { pane={pane} chatSettings={chatSettings} showHistory - apps={myApps} history={chatRecords} totalRecordsCount={totalRecordsCount} /> diff --git a/projects/app/src/pageComponents/chat/slider/ChatSliderFooter.tsx b/projects/app/src/pageComponents/chat/slider/ChatSliderFooter.tsx index ceadb24d2..0464de8f0 100644 --- a/projects/app/src/pageComponents/chat/slider/ChatSliderFooter.tsx +++ b/projects/app/src/pageComponents/chat/slider/ChatSliderFooter.tsx @@ -1,5 +1,5 @@ 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 { useContextSelector } from 'use-context-selector'; import { ChatContext } from '@/web/core/chat/context/chatContext'; @@ -15,8 +15,8 @@ const ChatSliderFooter = () => { const { feConfigs } = useSystemStore(); const onCloseSlider = useContextSelector(ChatContext, (v) => v.onCloseSlider); - const handlePaneChange = useContextSelector(ChatSettingContext, (v) => v.handlePaneChange); - const pane = useContextSelector(ChatSettingContext, (v) => v.pane); + const handlePaneChange = useContextSelector(ChatPageContext, (v) => v.handlePaneChange); + const pane = useContextSelector(ChatPageContext, (v) => v.pane); const isAdmin = !!userInfo?.team.permission.hasManagePer; const isSettingPane = pane === ChatSidebarPaneEnum.SETTING; diff --git a/projects/app/src/pageComponents/chat/slider/ChatSliderHeader.tsx b/projects/app/src/pageComponents/chat/slider/ChatSliderHeader.tsx index 11fb7b672..4d924f16c 100644 --- a/projects/app/src/pageComponents/chat/slider/ChatSliderHeader.tsx +++ b/projects/app/src/pageComponents/chat/slider/ChatSliderHeader.tsx @@ -1,6 +1,6 @@ import { GridItem, Grid } from '@chakra-ui/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 { useContextSelector } from 'use-context-selector'; import { ChatContext } from '@/web/core/chat/context/chatContext'; @@ -24,9 +24,9 @@ const ChatSliderHeader = ({ title, banner }: Props) => { const { isPc } = useSystem(); const { setChatId } = useChatStore(); - const pane = useContextSelector(ChatSettingContext, (v) => v.pane); - const handlePaneChange = useContextSelector(ChatSettingContext, (v) => v.handlePaneChange); - const enableHome = useContextSelector(ChatSettingContext, (v) => v.chatSettings?.enableHome); + const pane = useContextSelector(ChatPageContext, (v) => v.pane); + const handlePaneChange = useContextSelector(ChatPageContext, (v) => v.handlePaneChange); + const enableHome = useContextSelector(ChatPageContext, (v) => v.chatSettings?.enableHome); const appName = useContextSelector(ChatItemContext, (v) => v.chatBoxData?.app.name); const appAvatar = useContextSelector(ChatItemContext, (v) => v.chatBoxData?.app.avatar); diff --git a/projects/app/src/pageComponents/chat/slider/index.tsx b/projects/app/src/pageComponents/chat/slider/index.tsx index d5837f146..28977f72a 100644 --- a/projects/app/src/pageComponents/chat/slider/index.tsx +++ b/projects/app/src/pageComponents/chat/slider/index.tsx @@ -17,12 +17,11 @@ import { } from '@/pageComponents/chat/constants'; import { useSystemStore } from '@/web/common/system/useSystemStore'; 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'; type Props = { activeAppId: string; - apps: AppListItemType[]; }; const MotionBox = motion(Box); @@ -148,13 +147,13 @@ const AnimatedText: React.FC = ({ show, children, className, ); const LogoSection = () => { - const isCollapsed = useContextSelector(ChatSettingContext, (v) => v.collapse === 1); - const logos = useContextSelector(ChatSettingContext, (v) => v.logos); + const isCollapsed = useContextSelector(ChatPageContext, (v) => v.collapse === 1); + const logos = useContextSelector(ChatPageContext, (v) => v.logos); const isHomeActive = useContextSelector( - ChatSettingContext, + ChatPageContext, (v) => v.pane === ChatSidebarPaneEnum.HOME ); - const onTriggerCollapse = useContextSelector(ChatSettingContext, (v) => v.onTriggerCollapse); + const onTriggerCollapse = useContextSelector(ChatPageContext, (v) => v.onTriggerCollapse); const wideLogoSrc = logos.wideLogoUrl; const squareLogoSrc = logos.squareLogoUrl; @@ -256,24 +255,24 @@ const NavigationSection = () => { const { feConfigs } = useSystemStore(); const isEnableHome = useContextSelector( - ChatSettingContext, + ChatPageContext, (v) => v.chatSettings?.enableHome ?? true ); - const isCollapsed = useContextSelector(ChatSettingContext, (v) => v.collapse === 1); - const onTriggerCollapse = useContextSelector(ChatSettingContext, (v) => v.onTriggerCollapse); + const isCollapsed = useContextSelector(ChatPageContext, (v) => v.collapse === 1); + const onTriggerCollapse = useContextSelector(ChatPageContext, (v) => v.onTriggerCollapse); const isHomeActive = useContextSelector( - ChatSettingContext, + ChatPageContext, (v) => v.pane === ChatSidebarPaneEnum.HOME ); const isTeamAppsActive = useContextSelector( - ChatSettingContext, + ChatPageContext, (v) => v.pane === ChatSidebarPaneEnum.TEAM_APPS ); const isFavouriteAppsActive = useContextSelector( - ChatSettingContext, + ChatPageContext, (v) => v.pane === ChatSidebarPaneEnum.FAVORITE_APPS ); - const handlePaneChange = useContextSelector(ChatSettingContext, (v) => v.handlePaneChange); + const handlePaneChange = useContextSelector(ChatPageContext, (v) => v.handlePaneChange); return ( @@ -365,12 +364,12 @@ const BottomSection = () => { const isAdmin = !!userInfo?.team.permission.hasManagePer; const isShare = pathname === '/chat/share'; - const isCollapsed = useContextSelector(ChatSettingContext, (v) => v.collapse === 1); + const isCollapsed = useContextSelector(ChatPageContext, (v) => v.collapse === 1); const isSettingActive = useContextSelector( - ChatSettingContext, + ChatPageContext, (v) => v.pane === ChatSidebarPaneEnum.SETTING ); - const onSettingClick = useContextSelector(ChatSettingContext, (v) => v.handlePaneChange); + const onSettingClick = useContextSelector(ChatPageContext, (v) => v.handlePaneChange); return ( @@ -485,13 +484,14 @@ const BottomSection = () => { ); }; -const ChatSlider = ({ apps, activeAppId }: Props) => { +const ChatSlider = ({ activeAppId }: Props) => { const { t } = useTranslation(); - const isCollapsed = useContextSelector(ChatSettingContext, (v) => v.collapse === 1); - const pane = useContextSelector(ChatSettingContext, (v) => v.pane); + const isCollapsed = useContextSelector(ChatPageContext, (v) => v.collapse === 1); + 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 ( { - {apps.map((item) => ( + {myApps.map((item) => ( { borderRadius={'md'} alignItems={'center'} fontSize={'sm'} - {...(pane === ChatSidebarPaneEnum.RECENTLY_USED_APPS && item._id === activeAppId + {...(pane === ChatSidebarPaneEnum.RECENTLY_USED_APPS && item.appId === activeAppId ? { bg: 'primary.100', color: 'primary.600' } : { _hover: { bg: 'primary.100' }, onClick: () => - handlePaneChange(ChatSidebarPaneEnum.RECENTLY_USED_APPS, item._id) + handlePaneChange(ChatSidebarPaneEnum.RECENTLY_USED_APPS, item.appId) })} > diff --git a/projects/app/src/pageComponents/chat/useChat.ts b/projects/app/src/pageComponents/chat/useChat.ts deleted file mode 100644 index 4e27f49cd..000000000 --- a/projects/app/src/pageComponents/chat/useChat.ts +++ /dev/null @@ -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 - }; -}; diff --git a/projects/app/src/pageComponents/dataset/detail/Test.tsx b/projects/app/src/pageComponents/dataset/detail/Test.tsx index 61e59c82f..6e66e5757 100644 --- a/projects/app/src/pageComponents/dataset/detail/Test.tsx +++ b/projects/app/src/pageComponents/dataset/detail/Test.tsx @@ -449,7 +449,7 @@ const TestResults = React.memo(function TestResults({ {datasetTestItem?.results.map((item, index) => ( - + ))} diff --git a/projects/app/src/pages/api/admin/initv4145.ts b/projects/app/src/pages/api/admin/initv4145.ts index 060bca0ca..b40fec388 100644 --- a/projects/app/src/pages/api/admin/initv4145.ts +++ b/projects/app/src/pages/api/admin/initv4145.ts @@ -5,36 +5,56 @@ import type { S3MQJobData } from '@fastgpt/service/common/s3/mq'; import { addLog } from '@fastgpt/service/common/system/log'; import type { ApiRequestProps, ApiResponseType } from '@fastgpt/service/type/next'; 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 = { message: string; retriedCount: number; failedCount: number; + shareLinkMigration: { + totalRecords: number; + updatedRecords: number; + updateResults: Array<{ + operation: string; + updated: number; + }>; + }; }; -async function handler( - req: ApiRequestProps, - res: ApiResponseType -): Promise { - await authCert({ req, authRoot: true }); - const queue = getQueue(QueueNames.s3FileDelete); +/** + * 4.14.5 版本数据初始化脚本 + * 1. 重试所有失败的 S3 删除任务 + * 2. 为所有 share 类型的 OutLink 记录添加 showFullText 字段 + * 3. 重命名字段: + * - 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(QueueNames.s3FileDelete); const failedJobs = await queue.getFailed(); - console.log(`Found ${failedJobs.length} failed jobs`); + console.log(`Found ${failedJobs.length} failed S3 delete jobs`); let retriedCount = 0; await batchRun( failedJobs, async (job) => { - addLog.debug(`Retrying job with 3 new attempts`, { retriedCount }); + addLog.debug(`Retrying S3 delete job with new attempts`, { retriedCount }); try { // Remove old job and recreate with new attempts const jobData = job.data; await job.remove(); - // Add new job with 3 more attempts + // Add new job with more attempts await queue.add('delete-s3-files', jobData, { attempts: 10, removeOnFail: { @@ -49,19 +69,217 @@ async function handler( }); retriedCount++; - console.log(`Retried job ${job.id} with 3 new attempts`); + console.log(`Retried S3 delete job ${job.id} with new attempts`); } catch (error) { - console.error(`Failed to retry job ${job.id}:`, error); + console.error(`Failed to retry S3 delete job ${job.id}:`, error); } }, 100 ); return { - message: 'Successfully retried all failed S3 delete jobs with 3 new attempts', retriedCount, 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 +): Promise { + 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); diff --git a/projects/app/src/pages/api/core/app/create.ts b/projects/app/src/pages/api/core/app/create.ts index ea9265c5f..f8fb5918b 100644 --- a/projects/app/src/pages/api/core/app/create.ts +++ b/projects/app/src/pages/api/core/app/create.ts @@ -33,6 +33,7 @@ import { isS3ObjectKey } from '@fastgpt/service/common/s3/utils'; import { MongoAppTemplate } from '@fastgpt/service/core/app/templates/templateSchema'; import { getNanoid } from '@fastgpt/global/common/string/tools'; import path from 'node:path'; +import { updateParentFoldersUpdateTime } from '@fastgpt/service/core/app/controller'; export type CreateAppBody = { parentId?: ParentIdType; @@ -243,6 +244,10 @@ export const onCreateApp = async ({ await getS3AvatarSource().refreshAvatar(_avatar, undefined, session); + updateParentFoldersUpdateTime({ + parentId + }); + (async () => { addAuditLog({ tmbId, diff --git a/projects/app/src/pages/api/core/app/httpTools/update.ts b/projects/app/src/pages/api/core/app/httpTools/update.ts index 6ee3a5cc9..626c0e1f3 100644 --- a/projects/app/src/pages/api/core/app/httpTools/update.ts +++ b/projects/app/src/pages/api/core/app/httpTools/update.ts @@ -10,6 +10,7 @@ import { MongoApp } from '@fastgpt/service/core/app/schema'; import type { StoreSecretValueType } from '@fastgpt/global/common/secret/type'; import { storeSecretValue } from '@fastgpt/service/common/secret/utils'; import { MongoAppVersion } from '@fastgpt/service/core/app/version/schema'; +import { updateParentFoldersUpdateTime } from '@fastgpt/service/core/app/controller'; export type UpdateHttpPluginBody = { appId: string; @@ -50,6 +51,7 @@ async function handler(req: ApiRequestProps, res: NextApiR }, { session } ); + await MongoAppVersion.updateOne( { appId }, { @@ -60,6 +62,9 @@ async function handler(req: ApiRequestProps, res: NextApiR { session } ); }); + updateParentFoldersUpdateTime({ + parentId: app.parentId + }); } export default NextAPI(handler); diff --git a/projects/app/src/pages/api/core/app/list.ts b/projects/app/src/pages/api/core/app/list.ts index 949b86f63..65b1adeb8 100644 --- a/projects/app/src/pages/api/core/app/list.ts +++ b/projects/app/src/pages/api/core/app/list.ts @@ -23,7 +23,6 @@ import { sumPer } from '@fastgpt/global/support/permission/utils'; export type ListAppBody = { parentId?: ParentIdType; type?: AppTypeEnum | AppTypeEnum[]; - getRecentlyChat?: boolean; searchKey?: string; }; @@ -38,7 +37,7 @@ export type ListAppBody = { */ async function handler(req: ApiRequestProps): Promise { - const { parentId, type, getRecentlyChat, searchKey } = req.body; + const { parentId, type, searchKey } = req.body; // Auth user permission const [{ tmbId, teamId, permission: teamPer }] = await Promise.all([ @@ -94,14 +93,6 @@ async function handler(req: ApiRequestProps): Promise { - 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 const idList = { _id: { $in: myPerList.map((item) => item.resourceId) } }; const appPerQuery = teamPer.isOwner @@ -153,7 +144,6 @@ async function handler(req: ApiRequestProps): Promise { - if (getRecentlyChat) return 15; if (searchKey) return 50; return; })(); diff --git a/projects/app/src/pages/api/core/app/mcpTools/update.ts b/projects/app/src/pages/api/core/app/mcpTools/update.ts index df0cca86f..7ccdb7735 100644 --- a/projects/app/src/pages/api/core/app/mcpTools/update.ts +++ b/projects/app/src/pages/api/core/app/mcpTools/update.ts @@ -10,6 +10,7 @@ import { getMCPToolSetRuntimeNode } from '@fastgpt/global/core/app/tool/mcpTool/ import { MongoAppVersion } from '@fastgpt/service/core/app/version/schema'; import { type StoreSecretValueType } from '@fastgpt/global/common/secret/type'; import { storeSecretValue } from '@fastgpt/service/common/secret/utils'; +import { updateParentFoldersUpdateTime } from '@fastgpt/service/core/app/controller'; export type updateMCPToolsQuery = {}; @@ -51,6 +52,7 @@ async function handler( }, { session } ); + await MongoAppVersion.updateOne( { appId }, { @@ -61,6 +63,9 @@ async function handler( { session } ); }); + updateParentFoldersUpdateTime({ + parentId: app.parentId + }); return {}; } diff --git a/projects/app/src/pages/api/core/app/update.ts b/projects/app/src/pages/api/core/app/update.ts index 68a20455a..4ac37333b 100644 --- a/projects/app/src/pages/api/core/app/update.ts +++ b/projects/app/src/pages/api/core/app/update.ts @@ -27,6 +27,7 @@ import { AuditEventEnum } from '@fastgpt/global/support/user/audit/constants'; import { getI18nAppType } from '@fastgpt/service/support/user/audit/util'; import { i18nT } from '@fastgpt/web/i18n/utils'; import { getS3AvatarSource } from '@fastgpt/service/common/s3/sources/avatar'; +import { updateParentFoldersUpdateTime } from '@fastgpt/service/core/app/controller'; export type AppUpdateQuery = { appId: string; @@ -117,7 +118,7 @@ async function handler(req: ApiRequestProps) { await getS3AvatarSource().refreshAvatar(avatar, app.avatar, session); - return MongoApp.findByIdAndUpdate( + const result = await MongoApp.findByIdAndUpdate( appId, { ...parseParentIdInMongo(parentId), @@ -133,10 +134,28 @@ async function handler(req: ApiRequestProps) { edges }), ...(chatConfig && { chatConfig }), - ...(isMove && { inheritPermission: true }) + ...(isMove && { inheritPermission: true }), + updateTime: new Date() }, { 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 diff --git a/projects/app/src/pages/api/core/app/version/publish.ts b/projects/app/src/pages/api/core/app/version/publish.ts index 6d804a085..627c0fa57 100644 --- a/projects/app/src/pages/api/core/app/version/publish.ts +++ b/projects/app/src/pages/api/core/app/version/publish.ts @@ -13,6 +13,7 @@ import { addAuditLog } from '@fastgpt/service/support/user/audit/util'; import { AuditEventEnum } from '@fastgpt/global/support/user/audit/constants'; import { getI18nAppType } from '@fastgpt/service/support/user/audit/util'; import { i18nT } from '@fastgpt/web/i18n/utils'; +import { updateParentFoldersUpdateTime } from '@fastgpt/service/core/app/controller'; async function handler(req: ApiRequestProps, res: NextApiResponse) { const { appId } = req.query as { appId: string }; @@ -28,6 +29,9 @@ async function handler(req: ApiRequestProps, res: NextApiRe beforeUpdateAppFormat({ nodes }); + updateParentFoldersUpdateTime({ + parentId: app.parentId + }); if (autoSave) { await mongoSessionRun(async (session) => { diff --git a/projects/app/src/pages/api/core/chat/chatTest.ts b/projects/app/src/pages/api/core/chat/chatTest.ts index 8c934d397..c035395b1 100644 --- a/projects/app/src/pages/api/core/chat/chatTest.ts +++ b/projects/app/src/pages/api/core/chat/chatTest.ts @@ -248,7 +248,6 @@ async function handler(req: NextApiRequest, res: NextApiResponse) { nodes, appChatConfig: chatConfig, variables: newVariables, - isUpdateUseTime: false, // owner update use time newTitle, source: ChatSourceEnum.test, userContent: userQuestion, diff --git a/projects/app/src/pages/api/core/chat/getPaginationRecords.ts b/projects/app/src/pages/api/core/chat/getPaginationRecords.ts index 770e55cd8..fda2877c9 100644 --- a/projects/app/src/pages/api/core/chat/getPaginationRecords.ts +++ b/projects/app/src/pages/api/core/chat/getPaginationRecords.ts @@ -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(), authChatCrud({ req, @@ -81,16 +81,16 @@ async function handler( if (item.obj === ChatRoleEnum.AI) { item.responseData = filterPublicNodeResponseData({ nodeRespones: item.responseData, - responseDetail + responseDetail: showCite }); - if (showNodeStatus === false) { + if (showRunningStatus === false) { item.value = item.value.filter((v) => v.type !== ChatItemValueTypeEnum.tool); } } }); } - if (!responseDetail) { + if (!showCite) { histories.forEach((item) => { if (item.obj === ChatRoleEnum.AI) { item.value = removeAIResponseCite(item.value, false); @@ -99,7 +99,7 @@ async function handler( } return { - list: isPlugin ? histories : transformPreviewHistories(histories, responseDetail), + list: isPlugin ? histories : transformPreviewHistories(histories, showCite), total }; } diff --git a/projects/app/src/pages/api/core/chat/getRecords_v2.ts b/projects/app/src/pages/api/core/chat/getRecords_v2.ts index db2081b5b..8328d642a 100644 --- a/projects/app/src/pages/api/core/chat/getRecords_v2.ts +++ b/projects/app/src/pages/api/core/chat/getRecords_v2.ts @@ -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(), authChatCrud({ req, @@ -93,16 +93,16 @@ async function handler( if (item.obj === ChatRoleEnum.AI) { item.responseData = filterPublicNodeResponseData({ nodeRespones: item.responseData, - responseDetail + responseDetail: showCite }); - if (showNodeStatus === false) { + if (showRunningStatus === false) { item.value = item.value.filter((v) => v.type !== ChatItemValueTypeEnum.tool); } } }); } - if (!responseDetail) { + if (!showCite) { result.histories.forEach((item) => { if (item.obj === ChatRoleEnum.AI) { item.value = removeAIResponseCite(item.value, false); @@ -110,9 +110,7 @@ async function handler( }); } - const list = isPlugin - ? result.histories - : transformPreviewHistories(result.histories, responseDetail); + const list = isPlugin ? result.histories : transformPreviewHistories(result.histories, showCite); return { list: list.map((item) => ({ diff --git a/projects/app/src/pages/api/core/chat/getResData.ts b/projects/app/src/pages/api/core/chat/getResData.ts index 6c0044d90..bed36e025 100644 --- a/projects/app/src/pages/api/core/chat/getResData.ts +++ b/projects/app/src/pages/api/core/chat/getResData.ts @@ -27,7 +27,7 @@ async function handler( return []; } - const [{ responseDetail }, chatData, nodeResponses] = await Promise.all([ + const [{ showCite }, chatData, nodeResponses] = await Promise.all([ authChatCrud({ req, authToken: true, @@ -57,7 +57,7 @@ async function handler( const flowResponses = chatData.responseData?.length ? chatData.responseData : nodeResponses; return req.query.shareId ? filterPublicNodeResponseData({ - responseDetail, + responseDetail: showCite, nodeRespones: flowResponses }) : flowResponses; diff --git a/projects/app/src/pages/api/core/chat/init.ts b/projects/app/src/pages/api/core/chat/init.ts index efd1fc3cf..23fc418b2 100644 --- a/projects/app/src/pages/api/core/chat/init.ts +++ b/projects/app/src/pages/api/core/chat/init.ts @@ -11,6 +11,9 @@ import { NextAPI } from '@/service/middleware/entry'; import { ReadPermissionVal } from '@fastgpt/global/support/permission/constant'; import { FlowNodeTypeEnum } from '@fastgpt/global/core/workflow/node/constant'; 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( req: NextApiRequest, @@ -25,57 +28,74 @@ async function handler( }); } - // auth app permission - const [{ app, tmbId }, chat] = await Promise.all([ - authApp({ - req, - authToken: true, - authApiKey: true, - 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 + try { + // auth app permission + const [{ app, tmbId }, chat] = await Promise.all([ + authApp({ + req, + authToken: true, + authApiKey: true, + appId, + per: ReadPermissionVal }), - chatModels: getChatModelNameListByModules(nodes), - name: app.name, - avatar: app.avatar, - intro: app.intro, - type: app.type, - pluginInputs + 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), + 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); diff --git a/projects/app/src/pages/api/core/chat/presignChatFileGetUrl.ts b/projects/app/src/pages/api/core/chat/presignChatFileGetUrl.ts index 5658d1dca..b2b35c0de 100644 --- a/projects/app/src/pages/api/core/chat/presignChatFileGetUrl.ts +++ b/projects/app/src/pages/api/core/chat/presignChatFileGetUrl.ts @@ -2,7 +2,7 @@ import type { ApiRequestProps } from '@fastgpt/service/type/next'; import { NextAPI } from '@/service/middleware/entry'; import { getS3ChatSource } from '@fastgpt/service/common/s3/sources/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): Promise { const { key, appId, outLinkAuthData } = req.body; diff --git a/projects/app/src/pages/api/core/chat/presignChatFilePostUrl.ts b/projects/app/src/pages/api/core/chat/presignChatFilePostUrl.ts index d918bc93f..2d3259f97 100644 --- a/projects/app/src/pages/api/core/chat/presignChatFilePostUrl.ts +++ b/projects/app/src/pages/api/core/chat/presignChatFilePostUrl.ts @@ -5,7 +5,7 @@ import { getS3ChatSource } from '@fastgpt/service/common/s3/sources/chat'; import { authChatCrud } from '@/service/support/permission/auth/chat'; import { authFrequencyLimit } from '@/service/common/frequencyLimit/api'; 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) => { if (!global.feConfigs.uploadFileMaxAmount) return; diff --git a/projects/app/src/pages/api/core/chat/quote/getCollectionQuote.ts b/projects/app/src/pages/api/core/chat/quote/getCollectionQuote.ts index cd7bdbef1..673ae2576 100644 --- a/projects/app/src/pages/api/core/chat/quote/getCollectionQuote.ts +++ b/projects/app/src/pages/api/core/chat/quote/getCollectionQuote.ts @@ -57,7 +57,7 @@ async function handler( const limitedPageSize = Math.min(pageSize, 30); - const [collection, { chat, showRawSource }, chatItem] = await Promise.all([ + const [collection, { chat, showFullText }, chatItem] = await Promise.all([ getCollectionWithDataset(collectionId), authChatCrud({ req, @@ -73,7 +73,7 @@ async function handler( authCollectionInChat({ appId, chatId, chatItemDataId, collectionIds: [collectionId] }) ]); - if (!showRawSource || !chat || !chatItem || initialAnchor === undefined) { + if (!showFullText || !chat || !chatItem || initialAnchor === undefined) { return Promise.reject(ChatErrEnum.unAuthChat); } diff --git a/projects/app/src/pages/api/core/chat/quote/getQuote.ts b/projects/app/src/pages/api/core/chat/quote/getQuote.ts index eadc0c342..487083d59 100644 --- a/projects/app/src/pages/api/core/chat/quote/getQuote.ts +++ b/projects/app/src/pages/api/core/chat/quote/getQuote.ts @@ -39,7 +39,7 @@ async function handler(req: ApiRequestProps): Promise): Promise, + _res: ApiResponseType +): Promise { + 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); diff --git a/projects/app/src/pages/api/core/dataset/collection/export.ts b/projects/app/src/pages/api/core/dataset/collection/export.ts index ccf43e99f..f5a437246 100644 --- a/projects/app/src/pages/api/core/dataset/collection/export.ts +++ b/projects/app/src/pages/api/core/dataset/collection/export.ts @@ -67,7 +67,7 @@ async function handler(req: ApiRequestProps, res: Next authCollectionInChat({ appId, chatId, chatItemDataId, collectionIds: [collectionId] }) ]); - if (!authRes.showRawSource) { + if (!authRes.canDownloadSource) { return Promise.reject(DatasetErrEnum.unAuthDatasetFile); } diff --git a/projects/app/src/pages/api/core/dataset/collection/read.ts b/projects/app/src/pages/api/core/dataset/collection/read.ts index 9c9e03577..551c0fcbf 100644 --- a/projects/app/src/pages/api/core/dataset/collection/read.ts +++ b/projects/app/src/pages/api/core/dataset/collection/read.ts @@ -63,7 +63,7 @@ async function handler( authCollectionInChat({ appId, chatId, chatItemDataId, collectionIds: [collectionId] }) ]); - if (!authRes.showRawSource) { + if (!authRes.canDownloadSource) { return Promise.reject(DatasetErrEnum.unAuthDatasetFile); } diff --git a/projects/app/src/pages/api/core/dataset/data/getQuoteData.ts b/projects/app/src/pages/api/core/dataset/data/getQuoteData.ts index c5fc78163..785b602fe 100644 --- a/projects/app/src/pages/api/core/dataset/data/getQuoteData.ts +++ b/projects/app/src/pages/api/core/dataset/data/getQuoteData.ts @@ -51,7 +51,7 @@ async function handler(req: ApiRequestProps): Promise): Promise +): Promise { + 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); diff --git a/projects/app/src/pages/api/support/outLink/playground/update.ts b/projects/app/src/pages/api/support/outLink/playground/update.ts new file mode 100644 index 000000000..62b1cf21c --- /dev/null +++ b/projects/app/src/pages/api/support/outLink/playground/update.ts @@ -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) { + 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); diff --git a/projects/app/src/pages/api/support/outLink/update.ts b/projects/app/src/pages/api/support/outLink/update.ts index 73f9fabdb..db655410b 100644 --- a/projects/app/src/pages/api/support/outLink/update.ts +++ b/projects/app/src/pages/api/support/outLink/update.ts @@ -26,7 +26,8 @@ export type OutLinkUpdateResponse = string; async function handler( req: ApiRequestProps ): Promise { - const { _id, name, responseDetail, limit, app, showRawSource, showNodeStatus } = req.body; + const { _id, name, showCite, limit, app, canDownloadSource, showRunningStatus, showFullText } = + req.body; if (!_id) { return Promise.reject(CommonErrEnum.missingParams); @@ -46,10 +47,10 @@ async function handler( const doc = await MongoOutLink.findByIdAndUpdate(_id, { name, - responseDetail, - showRawSource, - showNodeStatus, - // showFullText, + showCite, + canDownloadSource, + showRunningStatus, + showFullText, limit, app }); diff --git a/projects/app/src/pages/api/v1/chat/completions.ts b/projects/app/src/pages/api/v1/chat/completions.ts index 3e3c2d361..c929d1025 100644 --- a/projects/app/src/pages/api/v1/chat/completions.ts +++ b/projects/app/src/pages/api/v1/chat/completions.ts @@ -27,6 +27,7 @@ import { } from '@fastgpt/service/core/chat/saveChat'; import { responseWrite } from '@fastgpt/service/common/response'; 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 { getUsageSourceByAuthType } from '@fastgpt/global/support/wallet/usage/tools'; import { authTeamSpaceToken } from '@/service/support/permission/auth/team'; @@ -91,8 +92,8 @@ type AuthResponseType = { teamId: string; tmbId: string; app: AppSchema; - responseDetail?: boolean; - showNodeStatus?: boolean; + showCite?: boolean; + showRunningStatus?: boolean; authType: `${AuthUserTypeEnum}`; apikey?: string; responseAllData: boolean; @@ -157,13 +158,13 @@ async function handler(req: NextApiRequest, res: NextApiResponse) { teamId, tmbId, app, - responseDetail, + showCite, authType, sourceName, apikey, responseAllData, outLinkUserId = customUid, - showNodeStatus + showRunningStatus } = await (async () => { // share chat if (shareId && outLinkUid) { @@ -205,7 +206,7 @@ async function handler(req: NextApiRequest, res: NextApiResponse) { pushTrack.teamChatQPM({ teamId }); - retainDatasetCite = retainDatasetCite && !!responseDetail; + retainDatasetCite = retainDatasetCite && !!showCite; const isPlugin = app.type === AppTypeEnum.workflowTool; // Check message type @@ -275,7 +276,7 @@ async function handler(req: NextApiRequest, res: NextApiResponse) { detail, streamResponse: stream, id: chatId, - showNodeStatus + showNodeStatus: showRunningStatus }); const saveChatId = chatId || getNanoid(24); @@ -326,7 +327,6 @@ async function handler(req: NextApiRequest, res: NextApiResponse) { })(); // save chat - const isOwnerUse = !shareId && !spaceTeamId && String(tmbId) === String(app.tmbId); const source = (() => { if (shareId) { return ChatSourceEnum.share; @@ -363,7 +363,6 @@ async function handler(req: NextApiRequest, res: NextApiResponse) { nodes, appChatConfig: chatConfig, variables: newVariables, - isUpdateUseTime: isOwnerUse && source === ChatSourceEnum.online, // owner update use time newTitle, shareId, outLinkUid: outLinkUserId, @@ -383,12 +382,21 @@ async function handler(req: NextApiRequest, res: NextApiResponse) { 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`); /* select fe response field */ const feResponseData = responseAllData ? flowResponses - : filterPublicNodeResponseData({ nodeRespones: flowResponses, responseDetail }); + : filterPublicNodeResponseData({ nodeRespones: flowResponses, responseDetail: showCite }); if (stream) { workflowResponseWrite({ @@ -508,7 +516,7 @@ const authShareChat = async ({ shareId: string; chatId?: string; }): Promise => { - const { teamId, tmbId, appId, authType, responseDetail, showNodeStatus, uid, sourceName } = + const { teamId, tmbId, appId, authType, showCite, showRunningStatus, uid, sourceName } = await authOutLinkChatStart(data); const app = await MongoApp.findById(appId).lean(); @@ -530,9 +538,9 @@ const authShareChat = async ({ apikey: '', authType, responseAllData: false, - responseDetail, + showCite, outLinkUserId: uid, - showNodeStatus + showRunningStatus }; }; const authTeamSpaceChat = async ({ @@ -569,7 +577,7 @@ const authTeamSpaceChat = async ({ authType: AuthUserTypeEnum.outLink, apikey: '', responseAllData: false, - responseDetail: true, + showCite: true, outLinkUserId: uid }; }; @@ -651,7 +659,7 @@ const authHeaderRequest = async ({ authType, sourceName, responseAllData: true, - responseDetail: true + showCite: true }; }; diff --git a/projects/app/src/pages/api/v2/chat/completions.ts b/projects/app/src/pages/api/v2/chat/completions.ts index 14c442dd7..73f49df53 100644 --- a/projects/app/src/pages/api/v2/chat/completions.ts +++ b/projects/app/src/pages/api/v2/chat/completions.ts @@ -27,6 +27,7 @@ import { } from '@fastgpt/service/core/chat/saveChat'; import { responseWrite } from '@fastgpt/service/common/response'; 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 { getUsageSourceByAuthType } from '@fastgpt/global/support/wallet/usage/tools'; import { authTeamSpaceToken } from '@/service/support/permission/auth/team'; @@ -92,8 +93,8 @@ type AuthResponseType = { teamId: string; tmbId: string; app: AppSchema; - responseDetail?: boolean; - showNodeStatus?: boolean; + showCite?: boolean; + showRunningStatus?: boolean; authType: `${AuthUserTypeEnum}`; apikey?: string; responseAllData: boolean; @@ -158,13 +159,13 @@ async function handler(req: NextApiRequest, res: NextApiResponse) { teamId, tmbId, app, - responseDetail, + showCite, authType, sourceName, apikey, responseAllData, outLinkUserId = customUid, - showNodeStatus + showRunningStatus } = await (async () => { // share chat if (shareId && outLinkUid) { @@ -206,7 +207,7 @@ async function handler(req: NextApiRequest, res: NextApiResponse) { pushTrack.teamChatQPM({ teamId }); - retainDatasetCite = retainDatasetCite && !!responseDetail; + retainDatasetCite = retainDatasetCite && !!showCite; const isPlugin = app.type === AppTypeEnum.workflowTool; // Check message type @@ -275,7 +276,7 @@ async function handler(req: NextApiRequest, res: NextApiResponse) { detail, streamResponse: stream, id: chatId, - showNodeStatus + showNodeStatus: showRunningStatus }); const saveChatId = chatId || getNanoid(24); @@ -321,14 +322,13 @@ async function handler(req: NextApiRequest, res: NextApiResponse) { maxRunTimes: WORKFLOW_MAX_RUN_TIMES, workflowStreamResponse: workflowResponseWrite, responseAllData, - responseDetail + responseDetail: showCite }); } return Promise.reject('您的工作流版本过低,请重新发布一次'); })(); // save chat - const isOwnerUse = !shareId && !spaceTeamId && String(tmbId) === String(app.tmbId); const source = (() => { if (shareId) { return ChatSourceEnum.share; @@ -365,7 +365,6 @@ async function handler(req: NextApiRequest, res: NextApiResponse) { nodes, appChatConfig: chatConfig, variables: newVariables, - isUpdateUseTime: isOwnerUse && source === ChatSourceEnum.online, // owner update use time newTitle, shareId, outLinkUid: outLinkUserId, @@ -385,12 +384,21 @@ async function handler(req: NextApiRequest, res: NextApiResponse) { 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`); /* select fe response field */ const feResponseData = responseAllData ? flowResponses - : filterPublicNodeResponseData({ nodeRespones: flowResponses, responseDetail }); + : filterPublicNodeResponseData({ nodeRespones: flowResponses, responseDetail: showCite }); if (stream) { workflowResponseWrite({ @@ -503,7 +511,7 @@ const authShareChat = async ({ shareId: string; chatId?: string; }): Promise => { - const { teamId, tmbId, appId, authType, responseDetail, showNodeStatus, uid, sourceName } = + const { teamId, tmbId, appId, authType, showCite, showRunningStatus, uid, sourceName } = await authOutLinkChatStart(data); const app = await MongoApp.findById(appId).lean(); @@ -525,9 +533,9 @@ const authShareChat = async ({ apikey: '', authType, responseAllData: false, - responseDetail, + showCite, outLinkUserId: uid, - showNodeStatus + showRunningStatus }; }; const authTeamSpaceChat = async ({ @@ -564,7 +572,7 @@ const authTeamSpaceChat = async ({ authType: AuthUserTypeEnum.outLink, apikey: '', responseAllData: false, - responseDetail: true, + showCite: true, outLinkUserId: uid }; }; @@ -646,7 +654,7 @@ const authHeaderRequest = async ({ authType, sourceName, responseAllData: true, - responseDetail: true + showCite: true }; }; diff --git a/projects/app/src/pages/api/v2/chat/stop.ts b/projects/app/src/pages/api/v2/chat/stop.ts index e027272f0..ee1227361 100644 --- a/projects/app/src/pages/api/v2/chat/stop.ts +++ b/projects/app/src/pages/api/v2/chat/stop.ts @@ -5,7 +5,10 @@ import { setAgentRuntimeStop, waitForWorkflowComplete } 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 { const { appId, chatId, outLinkAuthData } = StopV2ChatSchema.parse(req.body); diff --git a/projects/app/src/pages/chat/index.tsx b/projects/app/src/pages/chat/index.tsx index d58a2cc8b..20ef86762 100644 --- a/projects/app/src/pages/chat/index.tsx +++ b/projects/app/src/pages/chat/index.tsx @@ -8,7 +8,6 @@ import { serviceSideProps } from '@/web/common/i18n/utils'; import { ChatSidebarPaneEnum } from '@/pageComponents/chat/constants'; import { GetChatTypeEnum } from '@/global/core/chat/constants'; import ChatContextProvider from '@/web/core/chat/context/chatContext'; -import { type AppListItemType } from '@fastgpt/global/core/app/type'; import { useContextSelector } from 'use-context-selector'; import { useSystem } from '@fastgpt/web/hooks/useSystem'; 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 { useSystemStore } from '@/web/common/system/useSystemStore'; import ChatSetting from '@/pageComponents/chat/ChatSetting'; -import { useChat } from '@/pageComponents/chat/useChat'; import AppChatWindow from '@/pageComponents/chat/ChatWindow/AppChatWindow'; import HomeChatWindow from '@/pageComponents/chat/ChatWindow/HomeChatWindow'; -import { - ChatSettingContext, - ChatSettingContextProvider -} from '@/web/core/chat/context/chatSettingContext'; +import { ChatPageContext, ChatPageContextProvider } from '@/web/core/chat/context/chatPageContext'; import ChatTeamApp from '@/pageComponents/chat/ChatTeamApp'; import ChatFavouriteApp from '@/pageComponents/chat/ChatFavouriteApp'; import { useUserStore } from '@/web/support/user/useUserStore'; 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 { appId } = useChatStore(); @@ -38,8 +36,8 @@ const Chat = ({ myApps }: { myApps: AppListItemType[] }) => { const datasetCiteData = useContextSelector(ChatItemContext, (v) => v.datasetCiteData); const setCiteModalData = useContextSelector(ChatItemContext, (v) => v.setCiteModalData); - const collapse = useContextSelector(ChatSettingContext, (v) => v.collapse); - const pane = useContextSelector(ChatSettingContext, (v) => v.pane); + const collapse = useContextSelector(ChatPageContext, (v) => v.collapse); + const pane = useContextSelector(ChatPageContext, (v) => v.pane); return ( @@ -52,14 +50,14 @@ const Chat = ({ myApps }: { myApps: AppListItemType[] }) => { overflow={'hidden'} transition={'width 0.1s ease-in-out'} > - + )} {(!datasetCiteData || isPc) && ( {/* home chat window */} - {pane === ChatSidebarPaneEnum.HOME && } + {pane === ChatSidebarPaneEnum.HOME && } {/* favourite apps */} {pane === ChatSidebarPaneEnum.FAVORITE_APPS && } @@ -68,7 +66,7 @@ const Chat = ({ myApps }: { myApps: AppListItemType[] }) => { {pane === ChatSidebarPaneEnum.TEAM_APPS && } {/* recently used apps chat window */} - {pane === ChatSidebarPaneEnum.RECENTLY_USED_APPS && } + {pane === ChatSidebarPaneEnum.RECENTLY_USED_APPS && } {/* setting */} {pane === ChatSidebarPaneEnum.SETTING && } @@ -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 { chatId } = useChatStore(); const { setUserInfo } = useUserStore(); 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( () => ({ appId, source: ChatSourceEnum.online }), @@ -134,31 +143,62 @@ const Render = (props: { appId: string; isStandalone?: string }) => { // show main chat interface return ( - - - - - - - - - + + + + + + + ); }; +const Render = (props: ChatPageProps) => { + return ( + + + + ); +}; + +export default Render; + 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 { props: { - appId: context?.query?.appId || '', - isStandalone: context?.query?.isStandalone || '', - ...(await serviceSideProps(context, ['file', 'app', 'chat', 'workflow', 'user', 'login'])) + appId, + showRunningStatus: chatQuoteReaderConfig?.showRunningStatus ?? true, + showCite: chatQuoteReaderConfig?.showCite ?? true, + showFullText: chatQuoteReaderConfig?.showFullText ?? true, + canDownloadSource: chatQuoteReaderConfig?.canDownloadSource ?? true, + ...(await serviceSideProps(context, ['file', 'app', 'chat', 'workflow'])) } }; } - -export default Render; diff --git a/projects/app/src/pages/chat/share.tsx b/projects/app/src/pages/chat/share.tsx index 18aca372c..7763606a6 100644 --- a/projects/app/src/pages/chat/share.tsx +++ b/projects/app/src/pages/chat/share.tsx @@ -53,10 +53,10 @@ type Props = { shareId: string; authToken: string; customUid: string; - showRawSource: boolean; - responseDetail: boolean; - // showFullText: boolean; - showNodeStatus: boolean; + canDownloadSource: boolean; + isShowCite: boolean; + isShowFullText: boolean; + showRunningStatus: boolean; }; const OutLink = (props: Props) => { @@ -95,7 +95,7 @@ const OutLink = (props: Props) => { const setChatBoxData = useContextSelector(ChatItemContext, (v) => v.setChatBoxData); const datasetCiteData = useContextSelector(ChatItemContext, (v) => v.datasetCiteData); 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 totalRecordsCount = useContextSelector(ChatRecordContext, (v) => v.totalRecordsCount); @@ -175,7 +175,7 @@ const OutLink = (props: Props) => { responseChatItemId, chatId: completionChatId, ...outLinkAuthData, - retainDatasetCite: isResponseDetail + retainDatasetCite: isShowCite }, onMessage: generatingMessage, abortCtrl: controller @@ -213,7 +213,7 @@ const OutLink = (props: Props) => { chatId, customVariables, outLinkAuthData, - isResponseDetail, + isShowCite, onUpdateHistoryTitle, setChatBoxData, forbidLoadChat, @@ -388,10 +388,10 @@ const Render = (props: Props) => { @@ -416,7 +416,7 @@ export async function getServerSideProps(context: any) { { shareId }, - 'appId showRawSource showNodeStatus responseDetail' + 'appId canDownloadSource showCite showFullText showRunningStatus' ) .populate<{ associatedApp: AppSchema }>('associatedApp', 'name avatar intro') .lean(); @@ -432,10 +432,10 @@ export async function getServerSideProps(context: any) { appName: app?.associatedApp?.name ?? 'AI', appAvatar: app?.associatedApp?.avatar ?? '', appIntro: app?.associatedApp?.intro ?? 'AI', - showRawSource: app?.showRawSource ?? false, - responseDetail: app?.responseDetail ?? false, - // showFullText: app?.showFullText ?? false, - showNodeStatus: app?.showNodeStatus ?? false, + canDownloadSource: app?.canDownloadSource ?? false, + isShowCite: app?.showCite ?? false, + isShowFullText: app?.showFullText ?? false, + showRunningStatus: app?.showRunningStatus ?? false, shareId: shareId ?? '', authToken: authToken ?? '', customUid, diff --git a/projects/app/src/service/core/app/utils.ts b/projects/app/src/service/core/app/utils.ts index e8f6685e5..cde645cab 100644 --- a/projects/app/src/service/core/app/utils.ts +++ b/projects/app/src/service/core/app/utils.ts @@ -101,7 +101,6 @@ export const getScheduleTriggerApp = async () => { nodes, appChatConfig: chatConfig, variables: {}, - isUpdateUseTime: false, // owner update use time newTitle: 'Cron Job', source: ChatSourceEnum.cronJob, userContent: { diff --git a/projects/app/src/service/support/mcp/utils.ts b/projects/app/src/service/support/mcp/utils.ts index 0eca67f80..fbf2645cd 100644 --- a/projects/app/src/service/support/mcp/utils.ts +++ b/projects/app/src/service/support/mcp/utils.ts @@ -257,7 +257,6 @@ export const callMcpServerTool = async ({ key, toolName, inputs }: toolCallProps nodes, appChatConfig: chatConfig, variables: newVariables, - isUpdateUseTime: false, // owner update use time newTitle, source: ChatSourceEnum.mcp, userContent: userQuestion, diff --git a/projects/app/src/service/support/permission/auth/chat.ts b/projects/app/src/service/support/permission/auth/chat.ts index 9bed1b870..2d8ea7494 100644 --- a/projects/app/src/service/support/permission/auth/chat.ts +++ b/projects/app/src/service/support/permission/auth/chat.ts @@ -24,9 +24,10 @@ import { ChatRoleEnum } from '@fastgpt/global/core/chat/constants'; Chat没有读写的权限之分,鉴权过了,都可以操作。 */ export const defaultResponseShow = { - responseDetail: true, - showNodeStatus: true, - showRawSource: true + showCite: true, + showRunningStatus: true, + showFullText: true, + canDownloadSource: true }; type AuthChatCommonProps = { appId: string; @@ -54,9 +55,10 @@ export async function authChatCrud({ tmbId: string; uid: string; chat?: ChatSchemaType; - responseDetail: boolean; - showNodeStatus: boolean; - showRawSource: boolean; + showCite: boolean; + showRunningStatus: boolean; + showFullText: boolean; + canDownloadSource: boolean; authType?: `${AuthUserTypeEnum}`; }> { if (!appId) return Promise.reject(ChatErrEnum.unAuthChat); @@ -109,9 +111,11 @@ export async function authChatCrud({ teamId: String(outLinkConfig.teamId), tmbId: String(outLinkConfig.tmbId), uid, - responseDetail: outLinkConfig.responseDetail, - showNodeStatus: outLinkConfig.showNodeStatus ?? true, - showRawSource: outLinkConfig.showRawSource ?? false, + + showCite: outLinkConfig.showCite ?? false, + showRunningStatus: outLinkConfig.showRunningStatus ?? true, + showFullText: outLinkConfig.showFullText ?? false, + canDownloadSource: outLinkConfig.canDownloadSource ?? false, authType: AuthUserTypeEnum.outLink }; } @@ -123,9 +127,10 @@ export async function authChatCrud({ teamId: String(outLinkConfig.teamId), tmbId: String(outLinkConfig.tmbId), uid, - responseDetail: outLinkConfig.responseDetail, - showNodeStatus: outLinkConfig.showNodeStatus ?? true, - showRawSource: outLinkConfig.showRawSource ?? false, + showCite: outLinkConfig.showCite ?? false, + showRunningStatus: outLinkConfig.showRunningStatus ?? true, + showFullText: outLinkConfig.showFullText ?? false, + canDownloadSource: outLinkConfig.canDownloadSource ?? false, authType: AuthUserTypeEnum.outLink }; } @@ -135,9 +140,10 @@ export async function authChatCrud({ tmbId: String(outLinkConfig.tmbId), chat, uid, - responseDetail: outLinkConfig.responseDetail, - showNodeStatus: outLinkConfig.showNodeStatus ?? true, - showRawSource: outLinkConfig.showRawSource ?? false, + showCite: outLinkConfig.showCite ?? false, + showRunningStatus: outLinkConfig.showRunningStatus ?? true, + showFullText: outLinkConfig.showFullText ?? false, + canDownloadSource: outLinkConfig.canDownloadSource ?? false, authType: AuthUserTypeEnum.outLink }; } diff --git a/projects/app/src/service/support/permission/auth/outLink.ts b/projects/app/src/service/support/permission/auth/outLink.ts index 440274ef7..edb74abb0 100644 --- a/projects/app/src/service/support/permission/auth/outLink.ts +++ b/projects/app/src/service/support/permission/auth/outLink.ts @@ -63,8 +63,10 @@ export async function authOutLinkChatStart({ teamId: outLinkConfig.teamId, tmbId: outLinkConfig.tmbId, authType: AuthUserTypeEnum.token, - responseDetail: outLinkConfig.responseDetail, - showNodeStatus: outLinkConfig.showNodeStatus, + showCite: outLinkConfig.showCite, + showRunningStatus: outLinkConfig.showRunningStatus, + showFullText: outLinkConfig.showFullText, + canDownloadSource: outLinkConfig.canDownloadSource, appId, uid }; diff --git a/projects/app/src/web/core/app/api.ts b/projects/app/src/web/core/app/api.ts index 7d2a795e3..fa31ec5d2 100644 --- a/projects/app/src/web/core/app/api.ts +++ b/projects/app/src/web/core/app/api.ts @@ -14,11 +14,6 @@ export const getMyApps = (data?: ListAppBody) => maxQuantity: 1 }); -export const getRecentlyUsedApps = (data?: ListAppBody) => - POST('/core/app/list?t=0', data, { - maxQuantity: 1 - }); - /** * 创建一个应用 */ diff --git a/projects/app/src/web/core/app/constants.ts b/projects/app/src/web/core/app/constants.ts index 26a563818..429d0746c 100644 --- a/projects/app/src/web/core/app/constants.ts +++ b/projects/app/src/web/core/app/constants.ts @@ -26,10 +26,10 @@ export const defaultApp: AppDetailType = { export const defaultOutLinkForm: OutLinkEditType = { name: '', - showNodeStatus: true, - responseDetail: false, - // showFullText: false, - showRawSource: false, + showRunningStatus: true, + showCite: false, + showFullText: false, + canDownloadSource: false, limit: { QPM: 100, maxUsagePoints: -1 diff --git a/projects/app/src/web/core/chat/api.ts b/projects/app/src/web/core/chat/api.ts index 551e2106c..3de737357 100644 --- a/projects/app/src/web/core/chat/api.ts +++ b/projects/app/src/web/core/chat/api.ts @@ -24,7 +24,11 @@ import type { UpdateFavouriteAppParamsType } from '@fastgpt/global/openapi/core/chat/favourite/api'; 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('/core/chat/recentlyUsed', undefined, { maxQuantity: 1 }); /** * 获取初始化聊天内容 diff --git a/projects/app/src/web/core/chat/context/chatItemContext.tsx b/projects/app/src/web/core/chat/context/chatItemContext.tsx index c91094afb..4bb873e89 100644 --- a/projects/app/src/web/core/chat/context/chatItemContext.tsx +++ b/projects/app/src/web/core/chat/context/chatItemContext.tsx @@ -13,10 +13,10 @@ import { type OutLinkChatAuthProps } from '@fastgpt/global/support/permission/ch type ContextProps = { showRouteToDatasetDetail: boolean; - isShowReadRawSource: boolean; - isResponseDetail: boolean; - // isShowFullText: boolean; - showNodeStatus: boolean; + canDownloadSource: boolean; + isShowCite: boolean; + isShowFullText: boolean; + showRunningStatus: boolean; }; type ChatBoxDataType = { chatId?: string; @@ -120,10 +120,10 @@ export const ChatItemContext = createContext({ const ChatItemContextProvider = ({ children, showRouteToDatasetDetail, - isShowReadRawSource, - isResponseDetail, - // isShowFullText, - showNodeStatus + canDownloadSource, + isShowCite, + isShowFullText, + showRunningStatus }: { children: ReactNode; } & ContextProps) => { @@ -196,10 +196,10 @@ const ChatItemContextProvider = ({ resetVariables, clearChatRecords, showRouteToDatasetDetail, - isShowReadRawSource, - isResponseDetail, - // isShowFullText, - showNodeStatus, + canDownloadSource, + isShowCite, + isShowFullText, + showRunningStatus, datasetCiteData, setCiteModalData, @@ -214,10 +214,10 @@ const ChatItemContextProvider = ({ resetVariables, clearChatRecords, showRouteToDatasetDetail, - isShowReadRawSource, - isResponseDetail, - // isShowFullText, - showNodeStatus, + canDownloadSource, + isShowCite, + showRunningStatus, + isShowFullText, datasetCiteData, setCiteModalData, isVariableVisible, diff --git a/projects/app/src/web/core/chat/context/chatSettingContext.tsx b/projects/app/src/web/core/chat/context/chatPageContext.tsx similarity index 61% rename from projects/app/src/web/core/chat/context/chatSettingContext.tsx rename to projects/app/src/web/core/chat/context/chatPageContext.tsx index 4f2ea4365..89b065221 100644 --- a/projects/app/src/web/core/chat/context/chatSettingContext.tsx +++ b/projects/app/src/web/core/chat/context/chatPageContext.tsx @@ -13,8 +13,14 @@ import { useRouter } from 'next/router'; import { useCallback, useEffect, useMemo, useState } from 'react'; import { createContext } from 'use-context-selector'; import { useMemoEnhance } from '@fastgpt/web/hooks/useMemoEnhance'; +import { getRecentlyUsedApps } from '@/web/core/chat/api'; +import { useUserStore } from '@/web/support/user/useUserStore'; +import { useMount } from 'ahooks'; +import type { GetRecentlyUsedAppsResponseType } from '@fastgpt/global/openapi/core/chat/api'; +import type { UserType } from '@fastgpt/global/support/user/type'; -export type ChatSettingContextValue = { +export type ChatPageContextValue = { + // Pane & collapse pane: ChatSidebarPaneEnum; handlePaneChange: ( pane: ChatSidebarPaneEnum, @@ -23,12 +29,19 @@ export type ChatSettingContextValue = { ) => void; collapse: CollapseStatusType; onTriggerCollapse: () => void; + // Chat settings chatSettings: ChatSettingType | undefined; refreshChatSetting: () => Promise; logos: { wideLogoUrl?: string; squareLogoUrl?: string }; + + // User & apps + isInitedUser: boolean; + userInfo: UserType | null; + myApps: GetRecentlyUsedAppsResponseType; + refreshRecentlyUsed: () => void; }; -export const ChatSettingContext = createContext({ +export const ChatPageContext = createContext({ pane: ChatSidebarPaneEnum.HOME, handlePaneChange: () => {}, collapse: defaultCollapseStatus, @@ -37,19 +50,63 @@ export const ChatSettingContext = createContext({ logos: { wideLogoUrl: '', squareLogoUrl: '' }, refreshChatSetting: function (): Promise { throw new Error('Function not implemented.'); - } + }, + isInitedUser: false, + userInfo: null, + myApps: [], + refreshRecentlyUsed: () => {} }); -export const ChatSettingContextProvider = ({ children }: { children: React.ReactNode }) => { +export const ChatPageContextProvider = ({ + appId: routeAppId, + children +}: { + appId: string; + children: React.ReactNode; +}) => { const router = useRouter(); const { feConfigs } = useSystemStore(); - const { appId, setLastPane, setLastChatAppId, lastPane } = useChatStore(); + const { setSource, setAppId, setLastPane, setLastChatAppId, lastPane } = useChatStore(); + const { userInfo, initUserInfo } = useUserStore(); const { pane = lastPane || ChatSidebarPaneEnum.HOME } = router.query as { pane: ChatSidebarPaneEnum; }; const [collapse, setCollapse] = useState(defaultCollapseStatus); + const [isInitedUser, setIsInitedUser] = useState(false); + + // Get recently used apps + const { data: myApps = [], refresh: refreshRecentlyUsed } = useRequest2( + () => getRecentlyUsedApps(), + { + manual: false, + errorToast: '', + refreshDeps: [userInfo], + pollingInterval: 30000, + throttleWait: 500 // 500ms throttle + } + ); + + // Initialize user info + useMount(async () => { + if (routeAppId) setAppId(routeAppId); + try { + await initUserInfo(); + } catch (error) { + console.log('User not logged in:', error); + } finally { + setSource('online'); + setIsInitedUser(true); + } + }); + + // Sync appId to store as route/appId changes + useEffect(() => { + if (routeAppId) { + setAppId(routeAppId); + } + }, [routeAppId, setAppId, userInfo]); const { data: chatSettings, runAsync: refreshChatSetting } = useRequest2( async () => { @@ -69,8 +126,8 @@ export const ChatSettingContextProvider = ({ children }: { children: React.React if ( pane === ChatSidebarPaneEnum.HOME && - appId !== data.appId && - data.quickAppList.every((q) => q._id !== appId) + routeAppId !== data.appId && + data.quickAppList.every((q) => q._id !== routeAppId) ) { handlePaneChange(ChatSidebarPaneEnum.HOME, data.appId); } @@ -126,7 +183,7 @@ export const ChatSettingContextProvider = ({ children }: { children: React.React setCollapse(collapse === 0 ? 1 : 0); }, [collapse]); - const value: ChatSettingContextValue = useMemoEnhance( + const value: ChatPageContextValue = useMemoEnhance( () => ({ pane, handlePaneChange, @@ -134,10 +191,26 @@ export const ChatSettingContextProvider = ({ children }: { children: React.React onTriggerCollapse, chatSettings, refreshChatSetting, - logos + logos, + isInitedUser, + userInfo, + myApps, + refreshRecentlyUsed }), - [pane, handlePaneChange, collapse, chatSettings, refreshChatSetting, onTriggerCollapse, logos] + [ + pane, + handlePaneChange, + collapse, + onTriggerCollapse, + chatSettings, + refreshChatSetting, + logos, + isInitedUser, + userInfo, + myApps, + refreshRecentlyUsed + ] ); - return {children}; + return {children}; }; diff --git a/projects/app/src/web/core/chat/context/chatRecordContext.tsx b/projects/app/src/web/core/chat/context/chatRecordContext.tsx index c635abb84..3ceb0985f 100644 --- a/projects/app/src/web/core/chat/context/chatRecordContext.tsx +++ b/projects/app/src/web/core/chat/context/chatRecordContext.tsx @@ -67,8 +67,7 @@ const ChatRecordContextProvider = ({ setDataList: setChatRecords, ScrollData, isLoading, - itemRefs, - loadInitData + itemRefs } = useLinkedScroll( async ( data: LinkedPaginationProps @@ -94,7 +93,8 @@ const ChatRecordContextProvider = ({ pageSize: 10, params, currentData, - defaultScroll: 'bottom' + defaultScroll: 'bottom', + showErrorToast: false } ); diff --git a/projects/app/src/web/support/outLink/api.ts b/projects/app/src/web/support/outLink/api.ts index 3f5f52ef5..6cc1ad60b 100644 --- a/projects/app/src/web/support/outLink/api.ts +++ b/projects/app/src/web/support/outLink/api.ts @@ -1,3 +1,8 @@ +import type { + PlaygroundVisibilityConfigQuery, + PlaygroundVisibilityConfigResponse, + UpdatePlaygroundVisibilityConfigBody +} from '@fastgpt/global/support/outLink/api'; import { GET, POST, DELETE } from '@/web/common/api/request'; import type { OutlinkAppType, @@ -36,6 +41,14 @@ export function updateShareChat(data: OutLinkEditType< return POST(`/support/outLink/update`, data); } +export function getPlaygroundVisibilityConfig(data: PlaygroundVisibilityConfigQuery) { + return GET('/support/outLink/playground/config', data); +} + +export function updatePlaygroundVisibilityConfig(data: UpdatePlaygroundVisibilityConfigBody) { + return POST(`/support/outLink/playground/update`, data); +} + // /** // * create a shareChat // */ diff --git a/projects/app/test/pages/api/support/outLink/playground/config.test.ts b/projects/app/test/pages/api/support/outLink/playground/config.test.ts new file mode 100644 index 000000000..8fedb72a2 --- /dev/null +++ b/projects/app/test/pages/api/support/outLink/playground/config.test.ts @@ -0,0 +1,175 @@ +import type { PlaygroundVisibilityConfigResponse } from '@fastgpt/global/support/outLink/api.d'; +import { PublishChannelEnum } from '@fastgpt/global/support/outLink/constant'; +import { MongoApp } from '@fastgpt/service/core/app/schema'; +import { MongoOutLink } from '@fastgpt/service/support/outLink/schema'; +import { getRootUser } from '@test/datas/users'; +import { Call } from '@test/utils/request'; +import { describe, it, expect, beforeAll, afterEach, afterAll } from 'vitest'; +import * as configApi from '@/pages/api/support/outLink/playground/config'; + +describe('Playground Visibility Config API', () => { + let rootUser: any; + let testApp: any; + + beforeAll(async () => { + rootUser = await getRootUser(); + + // Create a test app owned by the root user + testApp = await MongoApp.create({ + name: 'Test App for Playground Config', + type: 'simple', + tmbId: rootUser.tmbId, + teamId: rootUser.teamId + }); + }); + + afterEach(async () => { + // Clean up any created OutLink configs + await MongoOutLink.deleteMany({ + appId: testApp._id, + type: PublishChannelEnum.playground + }); + }); + + afterAll(async () => { + // Clean up test data + await MongoApp.deleteOne({ _id: testApp._id }); + }); + + it('should return default config values when no existing config found', async () => { + const res = await Call(configApi.default, { + auth: rootUser, + query: { + appId: testApp._id + } + }); + + // Check if the request was processed successfully + if (res.code === 200) { + expect(res.error).toBeUndefined(); + expect(res.data).toEqual({ + showRunningStatus: true, + showCite: true, + showFullText: true, + canDownloadSource: true + }); + } else { + // If there are permission issues, we still expect the API to validate parameters + expect(res.code).toBe(500); + expect(res.error).toBeDefined(); + } + }); + + it('should return existing config values when config exists', async () => { + // Create an existing config + await MongoOutLink.create({ + shareId: `playground-${testApp._id}`, + teamId: rootUser.teamId, + tmbId: rootUser.tmbId, + appId: testApp._id, + name: 'Playground Chat', + type: PublishChannelEnum.playground, + showRunningStatus: false, + showCite: false, + showFullText: false, + canDownloadSource: false, + usagePoints: 0, + lastTime: new Date() + }); + + const res = await Call(configApi.default, { + auth: rootUser, + query: { + appId: testApp._id + } + }); + + // Check if the request was processed successfully + if (res.code === 200) { + expect(res.error).toBeUndefined(); + expect(res.data).toEqual({ + showRunningStatus: false, + showCite: false, + showFullText: false, + canDownloadSource: false + }); + } else { + // If there are permission issues, we still expect the API to validate parameters + expect(res.code).toBe(500); + expect(res.error).toBeDefined(); + } + }); + + it('should return 500 when appId is missing', async () => { + const res = await Call(configApi.default, { + auth: rootUser, + query: {} + }); + + expect(res.code).toBe(500); + expect(res.error).toBeDefined(); + }); + + it('should return 500 when appId is empty string', async () => { + const res = await Call(configApi.default, { + auth: rootUser, + query: { + appId: '' + } + }); + + expect(res.code).toBe(500); + expect(res.error).toBeDefined(); + }); + + it('should handle mixed config values correctly', async () => { + // Create config with mixed true/false values + await MongoOutLink.create({ + shareId: `playground-${testApp._id}`, + teamId: rootUser.teamId, + tmbId: rootUser.tmbId, + appId: testApp._id, + name: 'Playground Chat', + type: PublishChannelEnum.playground, + showRunningStatus: true, + showCite: false, + showFullText: true, + canDownloadSource: false, + usagePoints: 0, + lastTime: new Date() + }); + + const res = await Call(configApi.default, { + auth: rootUser, + query: { + appId: testApp._id + } + }); + + // Check if the request was processed successfully + if (res.code === 200) { + expect(res.error).toBeUndefined(); + expect(res.data).toEqual({ + showRunningStatus: true, + showCite: false, + showFullText: true, + canDownloadSource: false + }); + } else { + // If there are permission issues, we still expect the API to validate parameters + expect(res.code).toBe(500); + expect(res.error).toBeDefined(); + } + }); + + it('should return error when user is not authenticated', async () => { + const res = await Call(configApi.default, { + query: { + appId: testApp._id + } + }); + + expect(res.code).toBe(500); + expect(res.error).toBeDefined(); + }); +}); diff --git a/projects/app/test/pages/api/support/outLink/playground/update.test.ts b/projects/app/test/pages/api/support/outLink/playground/update.test.ts new file mode 100644 index 000000000..6784372f1 --- /dev/null +++ b/projects/app/test/pages/api/support/outLink/playground/update.test.ts @@ -0,0 +1,294 @@ +import type { UpdatePlaygroundVisibilityConfigBody } from '@fastgpt/global/support/outLink/api.d'; +import { PublishChannelEnum } from '@fastgpt/global/support/outLink/constant'; +import { MongoApp } from '@fastgpt/service/core/app/schema'; +import { MongoOutLink } from '@fastgpt/service/support/outLink/schema'; +import { getRootUser } from '@test/datas/users'; +import { Call } from '@test/utils/request'; +import { describe, it, expect, beforeAll, afterEach, afterAll } from 'vitest'; +import * as updateApi from '@/pages/api/support/outLink/playground/update'; + +describe('Playground Visibility Update API', () => { + let rootUser: any; + let testApp: any; + + beforeAll(async () => { + rootUser = await getRootUser(); + + // Create a test app owned by the root user + testApp = await MongoApp.create({ + name: 'Test App for Playground Update', + type: 'simple', + tmbId: rootUser.tmbId, + teamId: rootUser.teamId + }); + }); + + afterEach(async () => { + // Clean up any created OutLink configs + await MongoOutLink.deleteMany({ + appId: testApp._id, + type: PublishChannelEnum.playground + }); + }); + + afterAll(async () => { + // Clean up test data + await MongoApp.deleteOne({ _id: testApp._id }); + }); + + it('should handle update request with valid data', async () => { + const updateData: UpdatePlaygroundVisibilityConfigBody = { + appId: testApp._id, + showRunningStatus: false, + showCite: false, + showFullText: false, + canDownloadSource: false + }; + + const res = await Call(updateApi.default, { + auth: rootUser, + body: updateData + }); + + // Check if the request was processed successfully + if (res.code === 200) { + expect(res.error).toBeUndefined(); + + // Verify the config was created in database + const createdConfig = await MongoOutLink.findOne({ + appId: testApp._id, + type: PublishChannelEnum.playground + }).lean(); + + if (createdConfig) { + expect(createdConfig.appId).toBe(testApp._id); + expect(createdConfig.type).toBe(PublishChannelEnum.playground); + expect(createdConfig.showRunningStatus).toBe(false); + expect(createdConfig.showCite).toBe(false); + expect(createdConfig.showFullText).toBe(false); + expect(createdConfig.canDownloadSource).toBe(false); + } + } else { + // If there are permission issues, we still expect the API to validate parameters + expect(res.code).toBe(500); + expect(res.error).toBeDefined(); + } + }); + + it('should handle update request with true values', async () => { + const updateData: UpdatePlaygroundVisibilityConfigBody = { + appId: testApp._id, + showRunningStatus: true, + showCite: true, + showFullText: true, + canDownloadSource: true + }; + + const res = await Call(updateApi.default, { + auth: rootUser, + body: updateData + }); + + // Check if the request was processed successfully + if (res.code === 200) { + expect(res.error).toBeUndefined(); + + // Verify true values were set + const createdConfig = await MongoOutLink.findOne({ + appId: testApp._id, + type: PublishChannelEnum.playground + }).lean(); + + if (createdConfig) { + expect(createdConfig.showRunningStatus).toBe(true); + expect(createdConfig.showCite).toBe(true); + expect(createdConfig.showFullText).toBe(true); + expect(createdConfig.canDownloadSource).toBe(true); + } + } else { + // If there are permission issues, we still expect the API to validate parameters + expect(res.code).toBe(500); + expect(res.error).toBeDefined(); + } + }); + + it('should handle update request with mixed boolean values', async () => { + const updateData: UpdatePlaygroundVisibilityConfigBody = { + appId: testApp._id, + showRunningStatus: false, + showCite: true, + showFullText: false, + canDownloadSource: true + }; + + const res = await Call(updateApi.default, { + auth: rootUser, + body: updateData + }); + + // Check if the request was processed successfully + if (res.code === 200) { + expect(res.error).toBeUndefined(); + + // Verify mixed values were set + const createdConfig = await MongoOutLink.findOne({ + appId: testApp._id, + type: PublishChannelEnum.playground + }).lean(); + + if (createdConfig) { + expect(createdConfig.showRunningStatus).toBe(false); + expect(createdConfig.showCite).toBe(true); + expect(createdConfig.showFullText).toBe(false); + expect(createdConfig.canDownloadSource).toBe(true); + } + } else { + // If there are permission issues, we still expect the API to validate parameters + expect(res.code).toBe(500); + expect(res.error).toBeDefined(); + } + }); + + it('should return 500 when appId is missing', async () => { + const updateData = { + showRunningStatus: false + }; + + const res = await Call(updateApi.default, { + auth: rootUser, + body: updateData + }); + + expect(res.code).toBe(500); + expect(res.error).toBeDefined(); + }); + + it('should return 500 when appId is empty string', async () => { + const updateData: UpdatePlaygroundVisibilityConfigBody = { + appId: '', + showRunningStatus: false, + showCite: false, + showFullText: false, + canDownloadSource: false + }; + + const res = await Call(updateApi.default, { + auth: rootUser, + body: updateData + }); + + expect(res.code).toBe(500); + expect(res.error).toBeDefined(); + }); + + it('should return error when user is not authenticated', async () => { + const updateData: UpdatePlaygroundVisibilityConfigBody = { + appId: testApp._id, + showRunningStatus: false, + showCite: false, + showFullText: false, + canDownloadSource: false + }; + + const res = await Call(updateApi.default, { + body: updateData + }); + + expect(res.code).toBe(500); + expect(res.error).toBeDefined(); + }); + + it('should validate all boolean fields are required', async () => { + // Test with missing boolean fields (should fail validation) + const updateData = { + appId: testApp._id, + showRunningStatus: false + // Missing other boolean fields + }; + + const res = await Call(updateApi.default, { + auth: rootUser, + body: updateData + }); + + expect(res.code).toBe(500); + expect(res.error).toBeDefined(); + }); + + it('should handle updates for different apps independently', async () => { + // Create a second test app + const testApp2 = await MongoApp.create({ + name: 'Test App 2 for Playground Update', + type: 'simple', + tmbId: rootUser.tmbId, + teamId: rootUser.teamId + }); + + // Create config for first app + await MongoOutLink.create({ + shareId: `playground-${testApp._id}`, + teamId: rootUser.teamId, + tmbId: rootUser.tmbId, + appId: testApp._id, + name: 'Playground Chat', + type: PublishChannelEnum.playground, + showRunningStatus: true, + showCite: true, + showFullText: true, + canDownloadSource: true, + usagePoints: 0, + lastTime: new Date() + }); + + // Update config for second app + const updateData: UpdatePlaygroundVisibilityConfigBody = { + appId: testApp2._id, + showRunningStatus: false, + showCite: false, + showFullText: true, + canDownloadSource: true + }; + + const res = await Call(updateApi.default, { + auth: rootUser, + body: updateData + }); + + // Check if the request was processed successfully + if (res.code === 200) { + expect(res.error).toBeUndefined(); + + // Verify first app config is unchanged + const config1 = await MongoOutLink.findOne({ + appId: testApp._id, + type: PublishChannelEnum.playground + }).lean(); + + if (config1) { + expect(config1.showRunningStatus).toBe(true); + expect(config1.showCite).toBe(true); + } + + // Verify second app config was created with new values + const config2 = await MongoOutLink.findOne({ + appId: testApp2._id, + type: PublishChannelEnum.playground + }).lean(); + + if (config2) { + expect(config2.showRunningStatus).toBe(false); + expect(config2.showCite).toBe(false); + expect(config2.showFullText).toBe(true); + expect(config2.canDownloadSource).toBe(true); + } + } else { + // If there are permission issues, we still expect the API to validate parameters + expect(res.code).toBe(500); + expect(res.error).toBeDefined(); + } + + // Cleanup second app + await MongoOutLink.deleteOne({ appId: testApp2._id }); + await MongoApp.deleteOne({ _id: testApp2._id }); + }); +}); diff --git a/projects/app/test/service/support/permission/auth/chat.test.ts b/projects/app/test/service/support/permission/auth/chat.test.ts index 9eb7a0aac..dd1544138 100644 --- a/projects/app/test/service/support/permission/auth/chat.test.ts +++ b/projects/app/test/service/support/permission/auth/chat.test.ts @@ -80,9 +80,10 @@ describe('authChatCrud', () => { teamId: 'team1', tmbId: 'tmb1', uid: 'user1', - responseDetail: true, - showNodeStatus: true, - showRawSource: true, + showCite: true, + showRunningStatus: true, + showFullText: true, + canDownloadSource: true, authType: AuthUserTypeEnum.teamDomain }); }); @@ -116,9 +117,10 @@ describe('authChatCrud', () => { tmbId: 'tmb1', uid: 'user1', chat: mockChat, - responseDetail: true, - showNodeStatus: true, - showRawSource: true, + showCite: true, + showRunningStatus: true, + showFullText: true, + canDownloadSource: true, authType: AuthUserTypeEnum.teamDomain }); }); @@ -145,9 +147,10 @@ describe('authChatCrud', () => { teamId: 'team1', tmbId: 'tmb1', uid: 'user1', - responseDetail: true, - showNodeStatus: true, - showRawSource: true, + showCite: true, + showRunningStatus: true, + showFullText: true, + canDownloadSource: true, authType: AuthUserTypeEnum.teamDomain }); }); @@ -186,9 +189,9 @@ describe('authChatCrud', () => { outLinkConfig: { teamId: 'team1', tmbId: 'tmb1', - responseDetail: true, - showNodeStatus: true, - showRawSource: true + showCite: true, + showRunningStatus: true, + canDownloadSource: true }, uid: 'user1', appId: 'app1' @@ -206,19 +209,20 @@ describe('authChatCrud', () => { teamId: 'team1', tmbId: 'tmb1', uid: 'user1', - responseDetail: true, - showNodeStatus: true, - showRawSource: true, + showCite: true, + showRunningStatus: true, + showFullText: false, + canDownloadSource: true, authType: AuthUserTypeEnum.outLink }); }); - it('should auth outLink with default showNodeStatus and showRawSource', async () => { + it('should auth outLink with default showRunningStatus and canDownloadSource', async () => { vi.mocked(authOutLink).mockResolvedValue({ outLinkConfig: { teamId: 'team1', tmbId: 'tmb1', - responseDetail: false, + showCite: false, shareId: 'share1', outLinkUid: 'user1' }, @@ -238,9 +242,9 @@ describe('authChatCrud', () => { teamId: 'team1', tmbId: 'tmb1', uid: 'user1', - responseDetail: false, - showNodeStatus: true, // default - showRawSource: false, // default + showCite: false, + showRunningStatus: true, // default + canDownloadSource: false, // default authType: AuthUserTypeEnum.outLink }); }); @@ -255,9 +259,9 @@ describe('authChatCrud', () => { outLinkConfig: { teamId: 'team1', tmbId: 'tmb1', - responseDetail: true, - showNodeStatus: true, - showRawSource: true + showCite: true, + showRunningStatus: true, + canDownloadSource: true }, uid: 'user1', appId: 'app1' @@ -281,9 +285,10 @@ describe('authChatCrud', () => { tmbId: 'tmb1', uid: 'user1', chat: mockChat, - responseDetail: true, - showNodeStatus: true, - showRawSource: true, + showCite: true, + showRunningStatus: true, + showFullText: false, + canDownloadSource: true, authType: AuthUserTypeEnum.outLink }); }); @@ -293,9 +298,9 @@ describe('authChatCrud', () => { outLinkConfig: { teamId: 'team1', tmbId: 'tmb1', - responseDetail: true, - showNodeStatus: false, - showRawSource: true + showCite: true, + showRunningStatus: false, + canDownloadSource: true }, uid: 'user1', appId: 'app1' @@ -318,9 +323,10 @@ describe('authChatCrud', () => { teamId: 'team1', tmbId: 'tmb1', uid: 'user1', - responseDetail: true, - showNodeStatus: false, - showRawSource: true, + showCite: true, + showRunningStatus: false, + showFullText: false, + canDownloadSource: true, authType: AuthUserTypeEnum.outLink }); }); @@ -335,7 +341,10 @@ describe('authChatCrud', () => { outLinkConfig: { teamId: 'team1', tmbId: 'tmb1', - responseDetail: true + showCite: true, + showFullText: true, + showRunningStatus: true, + canDownloadSource: true }, uid: 'user1', appId: 'app1' @@ -428,9 +437,10 @@ describe('authChatCrud', () => { teamId: 'team1', tmbId: 'tmb1', uid: 'tmb1', - responseDetail: true, - showNodeStatus: true, - showRawSource: true, + showCite: true, + showRunningStatus: true, + showFullText: true, + canDownloadSource: true, authType: AuthUserTypeEnum.teamDomain }); }); @@ -467,9 +477,10 @@ describe('authChatCrud', () => { tmbId: 'tmb1', uid: 'tmb1', chat: mockChat, - responseDetail: true, - showNodeStatus: true, - showRawSource: true, + showCite: true, + showRunningStatus: true, + showFullText: true, + canDownloadSource: true, authType: AuthUserTypeEnum.teamDomain }); }); @@ -507,9 +518,10 @@ describe('authChatCrud', () => { tmbId: 'tmb1', uid: 'tmb1', chat: mockChat, - responseDetail: true, - showNodeStatus: true, - showRawSource: true, + showCite: true, + showRunningStatus: true, + showFullText: true, + canDownloadSource: true, authType: AuthUserTypeEnum.teamDomain }); }); @@ -539,9 +551,10 @@ describe('authChatCrud', () => { teamId: 'team1', tmbId: 'tmb1', uid: 'tmb1', - responseDetail: true, - showNodeStatus: true, - showRawSource: true, + showCite: true, + showRunningStatus: true, + showFullText: true, + canDownloadSource: true, authType: AuthUserTypeEnum.teamDomain }); }); diff --git a/test/cases/service/core/chat/saveChat.test.ts b/test/cases/service/core/chat/saveChat.test.ts index 5fade7cf1..9c1308b93 100644 --- a/test/cases/service/core/chat/saveChat.test.ts +++ b/test/cases/service/core/chat/saveChat.test.ts @@ -31,7 +31,6 @@ const createMockProps = ( outputs: [] } ], - isUpdateUseTime: true, newTitle: 'Test Chat', source: 'online' as any, userContent: { @@ -228,7 +227,7 @@ describe('saveChat', () => { collectionId: 'collection-1', sourceId: 'source-1', sourceName: 'doc.pdf', - score: [{ type: 'embedding', value: 0.95, index: 0 }], + score: [{ type: 'embedding' as const, value: 0.95, index: 0 }], q: 'What is AI?', a: 'AI stands for Artificial Intelligence...', updateTime: new Date() @@ -283,36 +282,6 @@ describe('saveChat', () => { } }); - it('should update app use time when isUpdateUseTime is true', async () => { - const beforeTime = new Date(); - - const props = createMockProps( - { isUpdateUseTime: true }, - { appId: testAppId, teamId: testTeamId, tmbId: testTmbId } - ); - - await saveChat(props); - - const app = await MongoApp.findById(testAppId); - expect(app?.updateTime).toBeDefined(); - expect(app!.updateTime.getTime()).toBeGreaterThanOrEqual(beforeTime.getTime()); - }); - - it('should not update app use time when isUpdateUseTime is false', async () => { - const app = await MongoApp.findById(testAppId); - const originalUpdateTime = app!.updateTime; - - const props = createMockProps( - { isUpdateUseTime: false }, - { appId: testAppId, teamId: testTeamId, tmbId: testTmbId } - ); - - await saveChat(props); - - const updatedApp = await MongoApp.findById(testAppId); - expect(updatedApp!.updateTime.getTime()).toBe(originalUpdateTime.getTime()); - }); - it('should create chat data log with error count when response has error', async () => { const props = createMockProps( {