diff --git a/packages/service/core/app/usage/schema.ts b/packages/service/core/app/usage/schema.ts new file mode 100644 index 000000000..10ffca1e2 --- /dev/null +++ b/packages/service/core/app/usage/schema.ts @@ -0,0 +1,49 @@ +import { + TeamCollectionName, + TeamMemberCollectionName +} from '@fastgpt/global/support/user/team/constant'; +import { getMongoModel, Schema } from '../../../common/mongo'; + +export const AppUsageCollectionName = 'app_usages'; + +const AppUsageSchema = 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: 'apps', + required: true + }, + lastUsedTime: { + type: Date, + default: () => new Date() + } + }, + { + minimize: false, + timestamps: false + } +); + +AppUsageSchema.index({ tmbId: 1, lastUsedTime: -1 }); // 查询用户最近使用的应用 +AppUsageSchema.index({ tmbId: 1, appId: 1 }, { unique: true }); // 防止重复记录 +AppUsageSchema.index({ teamId: 1, appId: 1 }); // 用于清理权限失效的记录 + +export const MongoAppUsage = getMongoModel(AppUsageCollectionName, AppUsageSchema); + +export type AppUsageType = { + _id?: string; + tmbId: string; + teamId: string; + appId: string; + lastUsedTime: Date; +}; diff --git a/packages/service/core/app/usage/utils.ts b/packages/service/core/app/usage/utils.ts new file mode 100644 index 000000000..49dd87805 --- /dev/null +++ b/packages/service/core/app/usage/utils.ts @@ -0,0 +1,52 @@ +import { mongoSessionRun } from '../../../common/mongo/sessionRun'; +import { MongoAppUsage } from './schema'; + +export const recordAppUsage = async ({ + appId, + tmbId, + teamId +}: { + appId: string; + tmbId: string; + teamId: string; +}) => { + await mongoSessionRun(async (session) => { + await MongoAppUsage.findOneAndUpdate( + { tmbId, appId }, + { + $set: { + teamId, + lastUsedTime: new Date() + } + }, + { + upsert: true, + new: true, + session + } + ); + + // 保留最新的50条记录,删除超出限制的旧记录 + const threshold = await MongoAppUsage.findOne( + { tmbId }, + { lastUsedTime: 1 }, + { + session, + sort: { lastUsedTime: -1 }, + skip: 49, + lean: true + } + ); + + if (threshold) { + await MongoAppUsage.deleteMany( + { + tmbId, + _id: { $ne: threshold._id }, + lastUsedTime: { $lte: threshold.lastUsedTime } + }, + { session } + ); + } + }); +}; diff --git a/projects/app/src/pageComponents/chat/useChat.ts b/projects/app/src/pageComponents/chat/useChat.ts index 4e27f49cd..c470f498a 100644 --- a/projects/app/src/pageComponents/chat/useChat.ts +++ b/projects/app/src/pageComponents/chat/useChat.ts @@ -12,7 +12,7 @@ export const useChat = (appId: string) => { const [isInitedUser, setIsInitedUser] = useState(false); // get app list - const { data: myApps = [] } = useRequest2(() => getRecentlyUsedApps({ getRecentlyChat: true }), { + const { data: myApps = [] } = useRequest2(() => getRecentlyUsedApps(), { manual: false, errorToast: '', refreshDeps: [userInfo], 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/recentlyUsed.ts b/projects/app/src/pages/api/core/app/recentlyUsed.ts new file mode 100644 index 000000000..214911997 --- /dev/null +++ b/projects/app/src/pages/api/core/app/recentlyUsed.ts @@ -0,0 +1,82 @@ +import type { ApiRequestProps, ApiResponseType } from '@fastgpt/service/type/next'; +import { NextAPI } from '@/service/middleware/entry'; +import { authUserPer } from '@fastgpt/service/support/permission/user/auth'; +import { ReadPermissionVal } from '@fastgpt/global/support/permission/constant'; +import { MongoAppUsage } from '@fastgpt/service/core/app/usage/schema'; +import { addSourceMember } from '@fastgpt/service/support/user/utils'; +import { authApp } from '@fastgpt/service/support/permission/app/auth'; +import type { AppListItemType } from '@fastgpt/global/core/app/type'; + +export type GetRecentlyUsedAppsResponse = AppListItemType[]; + +async function handler( + req: ApiRequestProps<{}, {}>, + _res: ApiResponseType +): Promise { + const { tmbId, teamId } = await authUserPer({ + req, + authToken: true, + authApiKey: true + }); + + const recentUsages = await MongoAppUsage.find( + { tmbId }, + { appId: 1 }, + { sort: { lastUsedTime: -1 }, limit: 20 } + ).lean(); + + if (!recentUsages.length) return []; + + const appIds = recentUsages.map((usage) => usage.appId); + + // 并发检查权限 + const results = await Promise.allSettled( + appIds.map((appId) => + authApp({ req, authToken: true, authApiKey: true, appId, per: ReadPermissionVal }) + .then(({ app }) => ({ appId, app })) + .catch(() => ({ appId, app: null })) + ) + ); + + const validApps: { + appId: string; + app: NonNullable>['app']>; + }[] = []; + const invalidAppIds: string[] = []; + + for (const result of results) { + if (result.status === 'fulfilled') { + const { appId, app } = result.value; + if (app) { + validApps.push({ appId, app }); + } else { + invalidAppIds.push(appId); + } + } + } + + // 异步清理无效记录 + if (invalidAppIds.length) { + MongoAppUsage.deleteMany({ tmbId, teamId, appId: { $in: invalidAppIds } }).catch((err) => + console.error('Failed to clean invalid app usage records:', err) + ); + } + + return addSourceMember({ + list: validApps.map(({ app }) => ({ + _id: app._id, + parentId: app.parentId, + tmbId: app.tmbId, + name: app.name, + avatar: app.avatar, + intro: app.intro, + type: app.type, + updateTime: app.updateTime, + pluginData: app.pluginData, + permission: app.permission, + inheritPermission: app.inheritPermission + })) + }); +} + +export default NextAPI(handler); diff --git a/projects/app/src/pages/api/v1/chat/completions.ts b/projects/app/src/pages/api/v1/chat/completions.ts index 2df434716..64680ed55 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/usage/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'; @@ -381,6 +382,14 @@ async function handler(req: NextApiRequest, res: NextApiResponse) { await saveChat(params); } + setImmediate(async () => { + await recordAppUsage({ + appId: String(app._id), + tmbId: String(tmbId), + teamId: String(teamId) + }); + }); + addLog.info(`completions running time: ${(Date.now() - startTime) / 1000}s`); /* select fe response field */ diff --git a/projects/app/src/pages/api/v2/chat/completions.ts b/projects/app/src/pages/api/v2/chat/completions.ts index b1d14e471..c251a500e 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/usage/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'; @@ -383,6 +384,14 @@ async function handler(req: NextApiRequest, res: NextApiResponse) { await saveChat(params); } + setImmediate(async () => { + await recordAppUsage({ + appId: String(app._id), + tmbId: String(tmbId), + teamId: String(teamId) + }); + }); + addLog.info(`completions running time: ${(Date.now() - startTime) / 1000}s`); /* select fe response field */ diff --git a/projects/app/src/web/core/app/api.ts b/projects/app/src/web/core/app/api.ts index 7d2a795e3..cae04e228 100644 --- a/projects/app/src/web/core/app/api.ts +++ b/projects/app/src/web/core/app/api.ts @@ -14,10 +14,7 @@ export const getMyApps = (data?: ListAppBody) => maxQuantity: 1 }); -export const getRecentlyUsedApps = (data?: ListAppBody) => - POST('/core/app/list?t=0', data, { - maxQuantity: 1 - }); +export const getRecentlyUsedApps = () => GET('/core/app/recentlyUsed'); /** * 创建一个应用