recent app

This commit is contained in:
heheer 2025-12-19 16:26:45 +08:00
parent 284ca367aa
commit afd9e6c439
No known key found for this signature in database
GPG Key ID: 37DCB43201661540
8 changed files with 204 additions and 16 deletions

View File

@ -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<AppUsageType>(AppUsageCollectionName, AppUsageSchema);
export type AppUsageType = {
_id?: string;
tmbId: string;
teamId: string;
appId: string;
lastUsedTime: Date;
};

View File

@ -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 }
);
}
});
};

View File

@ -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],

View File

@ -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<ListAppBody>): Promise<AppListItemType[]> {
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<ListAppBody>): Promise<AppListItemTy
);
const findAppsQuery = (() => {
if (getRecentlyChat) {
return {
// get all chat app, excluding hidden apps and deleted apps
teamId,
type: { $in: [AppTypeEnum.workflow, AppTypeEnum.simple, AppTypeEnum.workflowTool] }
};
}
// Filter apps by permission, if not owner, only get apps that I have permission to access
const idList = { _id: { $in: myPerList.map((item) => item.resourceId) } };
const appPerQuery = teamPer.isOwner
@ -153,7 +144,6 @@ async function handler(req: ApiRequestProps<ListAppBody>): Promise<AppListItemTy
};
})();
const limit = (() => {
if (getRecentlyChat) return 15;
if (searchKey) return 50;
return;
})();

View File

@ -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<GetRecentlyUsedAppsResponse>
): Promise<GetRecentlyUsedAppsResponse> {
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<Awaited<ReturnType<typeof authApp>>['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);

View File

@ -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 */

View File

@ -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 */

View File

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