Commit 6d438aaf by Archer Committed by GitHub

google login and power share link (#292)

parent 1aaafcf6
...@@ -9,7 +9,9 @@ ...@@ -9,7 +9,9 @@
"show_doc": true, "show_doc": true,
"systemTitle": "FastGPT", "systemTitle": "FastGPT",
"authorText": "Made by FastGPT Team.", "authorText": "Made by FastGPT Team.",
"exportLimitMinutes": 0, "limit": {
"exportLimitMinutes": 0
},
"scripts": [] "scripts": []
}, },
"SystemParams": { "SystemParams": {
......
...@@ -83,14 +83,18 @@ ...@@ -83,14 +83,18 @@
"Delete Failed": "Delete Failed", "Delete Failed": "Delete Failed",
"Delete Success": "Delete Successful", "Delete Success": "Delete Successful",
"Delete Warning": "Warning", "Delete Warning": "Warning",
"Edit": "Edit",
"Expired Time": "Expired",
"Filed is repeat": "Filed is repeated", "Filed is repeat": "Filed is repeated",
"Filed is repeated": "", "Filed is repeated": "",
"Input": "Input", "Input": "Input",
"Name is empty": "Name is empty",
"Output": "Output", "Output": "Output",
"Password inconsistency": "Password inconsistency", "Password inconsistency": "Password inconsistency",
"Rename": "Rename", "Rename": "Rename",
"Search": "Search", "Search": "Search",
"Status": "Status", "Status": "Status",
"Update Successful": "Update Successful",
"export": "" "export": ""
}, },
"dataset": { "dataset": {
...@@ -192,6 +196,25 @@ ...@@ -192,6 +196,25 @@
"Store": "Store", "Store": "Store",
"Tools": "Tools" "Tools": "Tools"
}, },
"outlink": {
"Copy Iframe": "Copy Iframe",
"Copy Link": "Copy",
"Create Ifrme Window": "Create Iframe Link",
"Create Share Window": "Create Share Window",
"Delete Link": "Delete",
"Edit Ifrme Link": "Edit Iframe Link",
"Edit Link": "Edit",
"Edit Share Window": "Edit Share Window",
"Link Name": "Link Name",
"Link is empty": "",
"Max credit": "Credit",
"Max credit tips": "What is the maximum amount of money that can be consumed by the link? If the link is exceeded, it will be banned. -1 indicates no limit.",
"QPM": "QPM",
"QPM Tips": "The maximum number of queries per IP address per minute",
"QPM is empty": "QPM is empty",
"Response Detail": "Detail",
"Response Detail tips": "Whether detailed data such as references and full context need to be returned"
},
"user": { "user": {
"Account": "Account", "Account": "Account",
"Amount of earnings": "Earnings", "Amount of earnings": "Earnings",
......
...@@ -83,14 +83,18 @@ ...@@ -83,14 +83,18 @@
"Delete Failed": "删除失败", "Delete Failed": "删除失败",
"Delete Success": "删除成功", "Delete Success": "删除成功",
"Delete Warning": "删除警告", "Delete Warning": "删除警告",
"Edit": "编辑",
"Expired Time": "过期时间",
"Filed is repeat": "", "Filed is repeat": "",
"Filed is repeated": "字段重复了", "Filed is repeated": "字段重复了",
"Input": "输入", "Input": "输入",
"Name is empty": "名称不能为空",
"Output": "输出", "Output": "输出",
"Password inconsistency": "两次密码不一致", "Password inconsistency": "两次密码不一致",
"Rename": "重命名", "Rename": "重命名",
"Search": "搜索", "Search": "搜索",
"Status": "状态", "Status": "状态",
"Update Successful": "更新成功",
"export": "" "export": ""
}, },
"dataset": { "dataset": {
...@@ -192,6 +196,25 @@ ...@@ -192,6 +196,25 @@
"Store": "应用市场", "Store": "应用市场",
"Tools": "工具" "Tools": "工具"
}, },
"outlink": {
"Copy Iframe": "复制嵌入",
"Copy Link": "复制",
"Create Ifrme Window": "创建嵌入链接",
"Create Share Window": "创建免登录窗口",
"Delete Link": "删除链接",
"Edit Ifrme Link": "更新嵌入链接",
"Edit Link": "编辑",
"Edit Share Window": "更新分享窗口",
"Link Name": "分享链接的名字",
"Link is empty": "",
"Max credit": "最大金额",
"Max credit tips": "该链接最大可消耗多少金额,超出后链接将被禁止使用。-1 代表无限制。",
"QPM": "",
"QPM Tips": "每个 IP 每分钟最多提问多少次",
"QPM is empty": "QPM 不能为空",
"Response Detail": "返回详情",
"Response Detail tips": "是否需要返回引用、完整上下文等详细数据"
},
"user": { "user": {
"Account": "账号", "Account": "账号",
"Amount of earnings": "收益(¥)", "Amount of earnings": "收益(¥)",
......
import { GET, POST, DELETE, PUT } from './request'; import { GET, POST, DELETE, PUT } from './request';
import type { ChatHistoryItemType } from '@/types/chat'; import type { ChatHistoryItemType } from '@/types/chat';
import type { InitChatResponse, InitShareChatResponse } from './response/chat'; import type { InitChatResponse } from './response/chat';
import { RequestPaging } from '../types/index'; import { RequestPaging } from '../types/index';
import type { OutLinkSchema } from '@/types/mongoSchema';
import type { ShareChatEditType } from '@/types/app';
import type { Props as UpdateHistoryProps } from '@/pages/api/chat/history/updateChatHistory'; import type { Props as UpdateHistoryProps } from '@/pages/api/chat/history/updateChatHistory';
import { AdminUpdateFeedbackParams } from './request/chat'; import { AdminUpdateFeedbackParams } from './request/chat';
...@@ -40,32 +38,6 @@ export const delChatRecordById = (data: { chatId: string; contentId: string }) = ...@@ -40,32 +38,6 @@ export const delChatRecordById = (data: { chatId: string; contentId: string }) =
export const putChatHistory = (data: UpdateHistoryProps) => export const putChatHistory = (data: UpdateHistoryProps) =>
PUT('/chat/history/updateChatHistory', data); PUT('/chat/history/updateChatHistory', data);
/**
* 初始化分享聊天
*/
export const initShareChatInfo = (data: { shareId: string }) =>
GET<InitShareChatResponse>(`/chat/shareChat/init`, data);
/**
* create a shareChat
*/
export const createShareChat = (
data: ShareChatEditType & {
appId: string;
}
) => POST<string>(`/chat/shareChat/create`, data);
/**
* get shareChat
*/
export const getShareChatList = (appId: string) =>
GET<OutLinkSchema[]>(`/chat/shareChat/list`, { appId });
/**
* delete a shareChat
*/
export const delShareChatById = (id: string) => DELETE(`/chat/shareChat/delete?id=${id}`);
export const userUpdateChatFeedback = (data: { chatItemId: string; userFeedback?: string }) => export const userUpdateChatFeedback = (data: { chatItemId: string; userFeedback?: string }) =>
POST('/chat/feedback/userUpdate', data); POST('/chat/feedback/userUpdate', data);
......
...@@ -17,7 +17,12 @@ import { ...@@ -17,7 +17,12 @@ import {
Response as SearchTestResponse Response as SearchTestResponse
} from '@/pages/api/openapi/kb/searchTest'; } from '@/pages/api/openapi/kb/searchTest';
import { Props as UpdateDataProps } from '@/pages/api/openapi/kb/updateData'; import { Props as UpdateDataProps } from '@/pages/api/openapi/kb/updateData';
import type { KbUpdateParams, CreateKbParams, GetKbDataListProps } from '../request/kb'; import type {
KbUpdateParams,
CreateKbParams,
GetKbDataListProps,
GetFileListProps
} from '../request/kb';
import { QuoteItemType } from '@/types/chat'; import { QuoteItemType } from '@/types/chat';
import { KbTypeEnum } from '@/constants/kb'; import { KbTypeEnum } from '@/constants/kb';
...@@ -38,8 +43,8 @@ export const putKbById = (data: KbUpdateParams) => PUT(`/plugins/kb/update`, dat ...@@ -38,8 +43,8 @@ export const putKbById = (data: KbUpdateParams) => PUT(`/plugins/kb/update`, dat
export const delKbById = (id: string) => DELETE(`/plugins/kb/delete?id=${id}`); export const delKbById = (id: string) => DELETE(`/plugins/kb/delete?id=${id}`);
/* kb file */ /* kb file */
export const getKbFiles = (data: { kbId: string; searchText: string }) => export const getKbFiles = (data: GetFileListProps) =>
GET<KbFileItemType[]>(`/plugins/kb/file/list`, data); POST<KbFileItemType[]>(`/plugins/kb/file/list`, data);
export const deleteKbFileById = (params: { fileId: string; kbId: string }) => export const deleteKbFileById = (params: { fileId: string; kbId: string }) =>
DELETE(`/plugins/kb/file/delFileByFileId`, params); DELETE(`/plugins/kb/file/delFileByFileId`, params);
export const getFileInfoById = (fileId: string) => export const getFileInfoById = (fileId: string) =>
......
...@@ -17,6 +17,11 @@ export type CreateKbParams = { ...@@ -17,6 +17,11 @@ export type CreateKbParams = {
type: `${KbTypeEnum}`; type: `${KbTypeEnum}`;
}; };
export type GetFileListProps = RequestPaging & {
kbId: string;
searchText: string;
};
export type GetKbDataListProps = RequestPaging & { export type GetKbDataListProps = RequestPaging & {
kbId: string; kbId: string;
searchText: string; searchText: string;
......
import axios, { Method, InternalAxiosRequestConfig, AxiosResponse } from 'axios'; import axios, { Method, InternalAxiosRequestConfig, AxiosResponse } from 'axios';
import { baseUrl } from '../../service/lib/openai';
interface ConfigType { interface ConfigType {
headers?: { [key: string]: string }; headers?: { [key: string]: string };
......
import { GET, POST, DELETE } from '../request';
import type { InitShareChatResponse } from '../response/chat';
import type { OutLinkEditType } from '@/types/support/outLink';
import type { OutLinkSchema } from '@/types/support/outLink';
/**
* 初始化分享聊天
*/
export const initShareChatInfo = (data: { shareId: string }) =>
GET<InitShareChatResponse>(`/support/outLink/init`, data);
/**
* create a shareChat
*/
export const createShareChat = (
data: OutLinkEditType & {
appId: string;
type: OutLinkSchema['type'];
}
) => POST<string>(`/support/outLink/create`, data);
export const putShareChat = (data: OutLinkEditType) =>
POST<string>(`/support/outLink/update`, data);
/**
* get shareChat
*/
export const getShareChatList = (appId: string) =>
GET<OutLinkSchema[]>(`/support/outLink/list`, { appId });
/**
* delete a shareChat
*/
export const delShareChatById = (id: string) => DELETE(`/support/outLink/delete?id=${id}`);
...@@ -5,16 +5,21 @@ import { UserAuthTypeEnum } from '@/constants/common'; ...@@ -5,16 +5,21 @@ import { UserAuthTypeEnum } from '@/constants/common';
import { UserBillType, UserType, UserUpdateParams } from '@/types/user'; import { UserBillType, UserType, UserUpdateParams } from '@/types/user';
import type { PagingData, RequestPaging } from '@/types'; import type { PagingData, RequestPaging } from '@/types';
import { informSchema, PaySchema } from '@/types/mongoSchema'; import { informSchema, PaySchema } from '@/types/mongoSchema';
import { OAuthEnum } from '@/constants/user';
export const sendAuthCode = (data: { export const sendAuthCode = (data: {
username: string; username: string;
type: `${UserAuthTypeEnum}`; type: `${UserAuthTypeEnum}`;
googleToken: string; googleToken: string;
}) => POST(`/plusApi/user/account/sendCode`, data); }) => POST(`/plusApi/user/inform/sendAuthCode`, data);
export const getTokenLogin = () => GET<UserType>('/user/account/tokenLogin'); export const getTokenLogin = () => GET<UserType>('/user/account/tokenLogin');
export const gitLogin = (params: { code: string; inviterId?: string }) => export const oauthLogin = (params: {
GET<ResLogin>('/plusApi/user/account/gitLogin', params); type: `${OAuthEnum}`;
code: string;
callbackUrl: string;
inviterId?: string;
}) => POST<ResLogin>('/plusApi/user/account/login/oauth', params);
export const postRegister = ({ export const postRegister = ({
username, username,
...@@ -27,7 +32,7 @@ export const postRegister = ({ ...@@ -27,7 +32,7 @@ export const postRegister = ({
password: string; password: string;
inviterId?: string; inviterId?: string;
}) => }) =>
POST<ResLogin>(`/plusApi/user/account/register`, { POST<ResLogin>(`/plusApi/user/account/register/emailAndPhone`, {
username, username,
code, code,
inviterId, inviterId,
...@@ -43,7 +48,7 @@ export const postFindPassword = ({ ...@@ -43,7 +48,7 @@ export const postFindPassword = ({
code: string; code: string;
password: string; password: string;
}) => }) =>
POST<ResLogin>(`/plusApi/user/account/updatePasswordByCode`, { POST<ResLogin>(`/plusApi/user/account/password/updateByCode`, {
username, username,
code, code,
password: createHashPassword(password) password: createHashPassword(password)
......
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1694437679570" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="7334" xmlns:xlink="http://www.w3.org/1999/xlink" width="64" height="64"><path d="M214.101333 512c0-32.512 5.546667-63.701333 15.36-92.928L57.173333 290.218667A491.861333 491.861333 0 0 0 4.693333 512c0 79.701333 18.858667 154.88 52.394667 221.610667l172.202667-129.066667A290.56 290.56 0 0 1 214.101333 512" fill="#FBBC05" p-id="7335"></path><path d="M516.693333 216.192c72.106667 0 137.258667 25.002667 188.458667 65.962667L854.101333 136.533333C763.349333 59.178667 646.997333 11.392 516.693333 11.392c-202.325333 0-376.234667 113.28-459.52 278.826667l172.373334 128.853333c39.68-118.016 152.832-202.88 287.146666-202.88" fill="#EA4335" p-id="7336"></path><path d="M516.693333 807.808c-134.357333 0-247.509333-84.864-287.232-202.88l-172.288 128.853333c83.242667 165.546667 257.152 278.826667 459.52 278.826667 124.842667 0 244.053333-43.392 333.568-124.757333l-163.584-123.818667c-46.122667 28.458667-104.234667 43.776-170.026666 43.776" fill="#34A853" p-id="7337"></path><path d="M1005.397333 512c0-29.568-4.693333-61.44-11.648-91.008H516.650667V614.4h274.602666c-13.696 65.962667-51.072 116.650667-104.533333 149.632l163.541333 123.818667c93.994667-85.418667 155.136-212.650667 155.136-375.850667" fill="#4285F4" p-id="7338"></path></svg>
\ No newline at end of file
...@@ -24,6 +24,7 @@ const map = { ...@@ -24,6 +24,7 @@ const map = {
out: require('./icons/out.svg').default, out: require('./icons/out.svg').default,
git: require('./icons/git.svg').default, git: require('./icons/git.svg').default,
gitFill: require('./icons/fill/git.svg').default, gitFill: require('./icons/fill/git.svg').default,
googleFill: require('./icons/fill/google.svg').default,
menu: require('./icons/menu.svg').default, menu: require('./icons/menu.svg').default,
edit: require('./icons/edit.svg').default, edit: require('./icons/edit.svg').default,
inform: require('./icons/inform.svg').default, inform: require('./icons/inform.svg').default,
......
...@@ -109,7 +109,7 @@ const Navbar = ({ unread }: { unread: number }) => { ...@@ -109,7 +109,7 @@ const Navbar = ({ unread }: { unread: number }) => {
<Avatar <Avatar
w={'36px'} w={'36px'}
h={'36px'} h={'36px'}
borderRadius={'none'} borderRadius={'50%'}
src={userInfo?.avatar} src={userInfo?.avatar}
fallbackSrc={HUMAN_ICON} fallbackSrc={HUMAN_ICON}
/> />
...@@ -157,7 +157,7 @@ const Navbar = ({ unread }: { unread: number }) => { ...@@ -157,7 +157,7 @@ const Navbar = ({ unread }: { unread: number }) => {
<Link <Link
as={NextLink} as={NextLink}
{...itemStyles} {...itemStyles}
href={`/account?type=inform`} href={`/account?currentTab=inform`}
mb={0} mb={0}
color={'#9096a5'} color={'#9096a5'}
> >
......
import type { ShareChatEditType } from '@/types/app';
import type { AppSchema } from '@/types/mongoSchema'; import type { AppSchema } from '@/types/mongoSchema';
import type { OutLinkEditType } from '@/types/support/outLink';
export const defaultApp: AppSchema = { export const defaultApp: AppSchema = {
_id: '', _id: '',
...@@ -17,6 +17,11 @@ export const defaultApp: AppSchema = { ...@@ -17,6 +17,11 @@ export const defaultApp: AppSchema = {
modules: [] modules: []
}; };
export const defaultShareChat: ShareChatEditType = { export const defaultOutLinkForm: OutLinkEditType = {
name: '' name: '',
responseDetail: false,
limit: {
QPM: 100,
credit: -1
}
}; };
export enum OAuthEnum {
github = 'github',
google = 'google'
}
export enum BillSourceEnum { export enum BillSourceEnum {
fastgpt = 'fastgpt', fastgpt = 'fastgpt',
api = 'api', api = 'api',
......
...@@ -124,10 +124,12 @@ const UserInfo = () => { ...@@ -124,10 +124,12 @@ const UserInfo = () => {
h={['44px', '54px']} h={['44px', '54px']}
borderRadius={'50%'} borderRadius={'50%'}
border={theme.borders.base} border={theme.borders.base}
overflow={'hidden'}
p={'2px'}
boxShadow={'0 0 5px rgba(0,0,0,0.1)'} boxShadow={'0 0 5px rgba(0,0,0,0.1)'}
mb={2} mb={2}
> >
<Avatar src={userInfo?.avatar} w={'100%'} h={'100%'} /> <Avatar src={userInfo?.avatar} borderRadius={'50%'} w={'100%'} h={'100%'} />
</Box> </Box>
</MyTooltip> </MyTooltip>
......
import type { NextApiRequest, NextApiResponse } from 'next'; import type { NextApiRequest, NextApiResponse } from 'next';
import { connectToDatabase } from '@/service/mongo'; import { connectToDatabase } from '@/service/mongo';
import { authUser, authApp, authShareChat, AuthUserTypeEnum } from '@/service/utils/auth'; import { authUser, authApp } from '@/service/utils/auth';
import { sseErrRes, jsonRes } from '@/service/response'; import { sseErrRes, jsonRes } from '@/service/response';
import { addLog, withNextCors } from '@/service/utils/tools'; import { addLog, withNextCors } from '@/service/utils/tools';
import { ChatRoleEnum, ChatSourceEnum, sseResponseEventEnum } from '@/constants/chat'; import { ChatRoleEnum, ChatSourceEnum, sseResponseEventEnum } from '@/constants/chat';
...@@ -29,6 +29,8 @@ import { ChatHistoryItemResType } from '@/types/chat'; ...@@ -29,6 +29,8 @@ import { ChatHistoryItemResType } from '@/types/chat';
import { UserModelSchema } from '@/types/mongoSchema'; import { UserModelSchema } from '@/types/mongoSchema';
import { SystemInputEnum } from '@/constants/app'; import { SystemInputEnum } from '@/constants/app';
import { getSystemTime } from '@/utils/user'; import { getSystemTime } from '@/utils/user';
import { authOutLinkChat } from '@/service/support/outLink/auth';
import requestIp from 'request-ip';
export type MessageItemType = ChatCompletionRequestMessage & { dataId?: string }; export type MessageItemType = ChatCompletionRequestMessage & { dataId?: string };
type FastGptWebChatProps = { type FastGptWebChatProps = {
...@@ -82,14 +84,17 @@ export default withNextCors(async function handler(req: NextApiRequest, res: Nex ...@@ -82,14 +84,17 @@ export default withNextCors(async function handler(req: NextApiRequest, res: Nex
let startTime = Date.now(); let startTime = Date.now();
/* user auth */ /* user auth */
const { let {
// @ts-ignore
responseDetail,
user, user,
userId, userId,
appId: authAppid, appId: authAppid,
authType authType
} = await (shareId } = await (shareId
? authShareChat({ ? authOutLinkChat({
shareId shareId,
ip: requestIp.getClientIp(req)
}) })
: authUser({ req, authBalance: true })); : authUser({ req, authBalance: true }));
...@@ -112,6 +117,7 @@ export default withNextCors(async function handler(req: NextApiRequest, res: Nex ...@@ -112,6 +117,7 @@ export default withNextCors(async function handler(req: NextApiRequest, res: Nex
]); ]);
const isOwner = !shareId && userId === String(app.userId); const isOwner = !shareId && userId === String(app.userId);
responseDetail = isOwner || responseDetail;
const prompts = history.concat(gptMessage2ChatType(messages)); const prompts = history.concat(gptMessage2ChatType(messages));
if (prompts[prompts.length - 1].obj === 'AI') { if (prompts[prompts.length - 1].obj === 'AI') {
...@@ -157,7 +163,7 @@ export default withNextCors(async function handler(req: NextApiRequest, res: Nex ...@@ -157,7 +163,7 @@ export default withNextCors(async function handler(req: NextApiRequest, res: Nex
appId, appId,
userId, userId,
variables, variables,
isOwner, isOwner, // owner update use time
shareId, shareId,
source: (() => { source: (() => {
if (shareId) { if (shareId) {
...@@ -197,7 +203,7 @@ export default withNextCors(async function handler(req: NextApiRequest, res: Nex ...@@ -197,7 +203,7 @@ export default withNextCors(async function handler(req: NextApiRequest, res: Nex
data: '[DONE]' data: '[DONE]'
}); });
if (isOwner && detail) { if (responseDetail && detail) {
sseResponse({ sseResponse({
res, res,
event: sseResponseEventEnum.appStreamResponse, event: sseResponseEventEnum.appStreamResponse,
......
...@@ -23,7 +23,7 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse< ...@@ -23,7 +23,7 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse<
const { userId } = await authUser({ req, authToken: true }); const { userId } = await authUser({ req, authToken: true });
const thirtyMinutesAgo = new Date( const thirtyMinutesAgo = new Date(
Date.now() - (global.feConfigs?.exportLimitMinutes || 0) * 60 * 1000 Date.now() - (global.feConfigs?.limit?.exportLimitMinutes || 0) * 60 * 1000
); );
// auth export times // auth export times
...@@ -39,7 +39,7 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse< ...@@ -39,7 +39,7 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse<
); );
if (!authTimes) { if (!authTimes) {
const minutes = `${global.feConfigs?.exportLimitMinutes || 0} 分钟`; const minutes = `${global.feConfigs?.limit?.exportLimitMinutes || 0} 分钟`;
throw new Error(`上次导出未到 ${minutes},每 ${minutes}仅可导出一次。`); throw new Error(`上次导出未到 ${minutes},每 ${minutes}仅可导出一次。`);
} }
......
...@@ -21,7 +21,7 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse< ...@@ -21,7 +21,7 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse<
const files = await bucket const files = await bucket
// 1 hours expired // 1 hours expired
.find({ .find({
uploadDate: { $lte: new Date(Date.now() - 60 * 1000) }, uploadDate: { $lte: new Date(Date.now() - 7 * 24 * 60 * 60 * 1000) },
['metadata.kbId']: kbId, ['metadata.kbId']: kbId,
['metadata.userId']: userId ['metadata.userId']: userId
}) })
......
...@@ -5,14 +5,19 @@ import { authUser } from '@/service/utils/auth'; ...@@ -5,14 +5,19 @@ import { authUser } from '@/service/utils/auth';
import { GridFSStorage } from '@/service/lib/gridfs'; import { GridFSStorage } from '@/service/lib/gridfs';
import { PgClient } from '@/service/pg'; import { PgClient } from '@/service/pg';
import { PgTrainingTableName } from '@/constants/plugin'; import { PgTrainingTableName } from '@/constants/plugin';
import { KbFileItemType } from '@/types/plugin';
import { FileStatusEnum, OtherFileId } from '@/constants/kb'; import { FileStatusEnum, OtherFileId } from '@/constants/kb';
import mongoose from 'mongoose';
export default async function handler(req: NextApiRequest, res: NextApiResponse<any>) { export default async function handler(req: NextApiRequest, res: NextApiResponse<any>) {
try { try {
await connectToDatabase(); await connectToDatabase();
let { kbId, searchText } = req.query as { kbId: string; searchText: string }; let {
pageNum = 1,
pageSize = 10,
kbId,
searchText
} = req.body as { pageNum: number; pageSize: number; kbId: string; searchText: string };
searchText = searchText.replace(/'/g, ''); searchText = searchText.replace(/'/g, '');
// 凭证校验 // 凭证校验
...@@ -21,10 +26,19 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse< ...@@ -21,10 +26,19 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse<
const gridFs = new GridFSStorage('dataset', userId); const gridFs = new GridFSStorage('dataset', userId);
const bucket = gridFs.GridFSBucket(); const bucket = gridFs.GridFSBucket();
const files = await bucket const mongoWhere = {
.find({ ['metadata.kbId']: kbId, ...(searchText && { filename: { $regex: searchText } }) }) ['metadata.kbId']: kbId,
.sort({ _id: -1 }) ...(searchText && { filename: { $regex: searchText } })
.toArray(); };
const [files, total] = await Promise.all([
bucket
.find(mongoWhere)
.sort({ _id: -1 })
.skip((pageNum - 1) * pageSize)
.limit(pageSize)
.toArray(),
mongoose.connection.db.collection('dataset.files').countDocuments(mongoWhere)
]);
async function GetOtherData() { async function GetOtherData() {
return { return {
...@@ -72,8 +86,13 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse< ...@@ -72,8 +86,13 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse<
}) })
]); ]);
jsonRes<KbFileItemType[]>(res, { jsonRes(res, {
data: data.flat().filter((item) => item.chunkLength > 0) data: {
pageNum,
pageSize,
data: data.flat().filter((item) => item.chunkLength > 0),
total
}
}); });
} catch (err) { } catch (err) {
jsonRes(res, { jsonRes(res, {
......
...@@ -2,7 +2,7 @@ import type { NextApiRequest, NextApiResponse } from 'next'; ...@@ -2,7 +2,7 @@ import type { NextApiRequest, NextApiResponse } from 'next';
import { jsonRes } from '@/service/response'; import { jsonRes } from '@/service/response';
import { connectToDatabase, OutLink } from '@/service/mongo'; import { connectToDatabase, OutLink } from '@/service/mongo';
import { authApp, authUser } from '@/service/utils/auth'; import { authApp, authUser } from '@/service/utils/auth';
import type { ShareChatEditType } from '@/types/app'; import type { OutLinkEditType } from '@/types/support/outLink';
import { customAlphabet } from 'nanoid'; import { customAlphabet } from 'nanoid';
import { OutLinkTypeEnum } from '@/constants/chat'; import { OutLinkTypeEnum } from '@/constants/chat';
const nanoid = customAlphabet('abcdefghijklmnopqrstuvwxyz1234567890', 24); const nanoid = customAlphabet('abcdefghijklmnopqrstuvwxyz1234567890', 24);
...@@ -10,8 +10,9 @@ const nanoid = customAlphabet('abcdefghijklmnopqrstuvwxyz1234567890', 24); ...@@ -10,8 +10,9 @@ const nanoid = customAlphabet('abcdefghijklmnopqrstuvwxyz1234567890', 24);
/* create a shareChat */ /* create a shareChat */
export default async function handler(req: NextApiRequest, res: NextApiResponse) { export default async function handler(req: NextApiRequest, res: NextApiResponse) {
try { try {
const { appId, name } = req.body as ShareChatEditType & { const { appId, ...props } = req.body as OutLinkEditType & {
appId: string; appId: string;
type: `${OutLinkTypeEnum}`;
}; };
await connectToDatabase(); await connectToDatabase();
...@@ -28,8 +29,7 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse) ...@@ -28,8 +29,7 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
shareId, shareId,
userId, userId,
appId, appId,
name, ...props
type: OutLinkTypeEnum.share
}); });
jsonRes(res, { jsonRes(res, {
......
...@@ -6,12 +6,12 @@ import { authUser } from '@/service/utils/auth'; ...@@ -6,12 +6,12 @@ import { authUser } from '@/service/utils/auth';
/* delete a shareChat by shareChatId */ /* delete a shareChat by shareChatId */
export default async function handler(req: NextApiRequest, res: NextApiResponse) { export default async function handler(req: NextApiRequest, res: NextApiResponse) {
try { try {
await connectToDatabase();
const { id } = req.query as { const { id } = req.query as {
id: string; id: string;
}; };
await connectToDatabase();
const { userId } = await authUser({ req, authToken: true }); const { userId } = await authUser({ req, authToken: true });
await OutLink.findOneAndRemove({ await OutLink.findOneAndRemove({
......
...@@ -6,7 +6,7 @@ import { authApp } from '@/service/utils/auth'; ...@@ -6,7 +6,7 @@ import { authApp } from '@/service/utils/auth';
import { HUMAN_ICON } from '@/constants/chat'; import { HUMAN_ICON } from '@/constants/chat';
import { getChatModelNameList, getSpecialModule } from '@/components/ChatBox/utils'; import { getChatModelNameList, getSpecialModule } from '@/components/ChatBox/utils';
/* 初始化我的聊天框,需要身份验证 */ /* init share chat window */
export default async function handler(req: NextApiRequest, res: NextApiResponse) { export default async function handler(req: NextApiRequest, res: NextApiResponse) {
try { try {
let { shareId } = req.query as { let { shareId } = req.query as {
......
...@@ -7,12 +7,12 @@ import { hashPassword } from '@/service/utils/tools'; ...@@ -7,12 +7,12 @@ import { hashPassword } from '@/service/utils/tools';
/* get shareChat list by appId */ /* get shareChat list by appId */
export default async function handler(req: NextApiRequest, res: NextApiResponse) { export default async function handler(req: NextApiRequest, res: NextApiResponse) {
try { try {
await connectToDatabase();
const { appId } = req.query as { const { appId } = req.query as {
appId: string; appId: string;
}; };
await connectToDatabase();
const { userId } = await authUser({ req, authToken: true }); const { userId } = await authUser({ req, authToken: true });
const data = await OutLink.find({ const data = await OutLink.find({
...@@ -22,15 +22,7 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse) ...@@ -22,15 +22,7 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
_id: -1 _id: -1
}); });
jsonRes(res, { jsonRes(res, { data });
data: data.map((item) => ({
_id: item._id,
shareId: item.shareId,
name: item.name,
total: item.total,
lastTime: item.lastTime
}))
});
} catch (err) { } catch (err) {
jsonRes(res, { jsonRes(res, {
code: 500, code: 500,
......
import type { NextApiRequest, NextApiResponse } from 'next';
import { jsonRes } from '@/service/response';
import { connectToDatabase, OutLink } from '@/service/mongo';
import type { OutLinkEditType } from '@/types/support/outLink';
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
try {
await connectToDatabase();
const { _id, name, responseDetail, limit } = req.body as OutLinkEditType & {};
await OutLink.findByIdAndUpdate(_id, {
name,
responseDetail,
limit
});
jsonRes(res);
} catch (err) {
jsonRes(res, {
code: 500,
error: err
});
}
}
...@@ -44,7 +44,9 @@ const defaultFeConfigs: FeConfigsType = { ...@@ -44,7 +44,9 @@ const defaultFeConfigs: FeConfigsType = {
show_doc: true, show_doc: true,
systemTitle: 'FastGPT', systemTitle: 'FastGPT',
authorText: 'Made by FastGPT Team.', authorText: 'Made by FastGPT Team.',
exportLimitMinutes: 0, limit: {
exportLimitMinutes: 0
},
scripts: [] scripts: []
}; };
const defaultChatModels = [ const defaultChatModels = [
......
import React, { useCallback, useMemo, useRef } from 'react'; import React, { useCallback, useMemo, useRef } from 'react';
import Head from 'next/head'; import Head from 'next/head';
import { useRouter } from 'next/router'; import { useRouter } from 'next/router';
import { initShareChatInfo } from '@/api/chat'; import { initShareChatInfo } from '@/api/support/outLink';
import { Box, Flex, useDisclosure, Drawer, DrawerOverlay, DrawerContent } from '@chakra-ui/react'; import { Box, Flex, useDisclosure, Drawer, DrawerOverlay, DrawerContent } from '@chakra-ui/react';
import { useToast } from '@/hooks/useToast'; import { useToast } from '@/hooks/useToast';
import { useGlobalStore } from '@/store/global'; import { useGlobalStore } from '@/store/global';
......
...@@ -65,22 +65,6 @@ const DataCard = ({ kbId }: { kbId: string }) => { ...@@ -65,22 +65,6 @@ const DataCard = ({ kbId }: { kbId: string }) => {
const [editInputData, setEditInputData] = useState<InputDataType>(); const [editInputData, setEditInputData] = useState<InputDataType>();
const { data: { qaListLen = 0, vectorListLen = 0 } = {}, refetch: refetchTrainingData } =
useQuery(['getModelSplitDataList', kbId], () => getTrainingData({ kbId, init: false }), {
onError(err) {
console.log(err);
}
});
const refetchData = useCallback(
(num = pageNum) => {
getData(num);
refetchTrainingData();
return null;
},
[getData, pageNum, refetchTrainingData]
);
// get first page data // get first page data
const getFirstData = useCallback( const getFirstData = useCallback(
debounce(() => { debounce(() => {
...@@ -90,12 +74,6 @@ const DataCard = ({ kbId }: { kbId: string }) => { ...@@ -90,12 +74,6 @@ const DataCard = ({ kbId }: { kbId: string }) => {
[] []
); );
// interval get data
useQuery(['refetchData'], () => refetchData(1), {
refetchInterval: 5000,
enabled: qaListLen > 0 || vectorListLen > 0
});
// get file info // get file info
const { data: fileInfo } = useQuery(['getFileInfo', fileId], () => getFileInfoById(fileId)); const { data: fileInfo } = useQuery(['getFileInfo', fileId], () => getFileInfoById(fileId));
const fileIcon = useMemo( const fileIcon = useMemo(
...@@ -122,7 +100,7 @@ const DataCard = ({ kbId }: { kbId: string }) => { ...@@ -122,7 +100,7 @@ const DataCard = ({ kbId }: { kbId: string }) => {
filename filename
}); });
}, },
successToast: `导出成功,下次导出需要 ${feConfigs.exportLimitMinutes} 分钟后`, successToast: `导出成功,下次导出需要 ${feConfigs?.limit?.exportLimitMinutes} 分钟后`,
errorToast: '导出异常' errorToast: '导出异常'
}); });
...@@ -136,7 +114,7 @@ const DataCard = ({ kbId }: { kbId: string }) => { ...@@ -136,7 +114,7 @@ const DataCard = ({ kbId }: { kbId: string }) => {
fontSize={['sm', 'md']} fontSize={['sm', 'md']}
alignItems={'center'} alignItems={'center'}
> >
<Image src={fileIcon} w={'16px'} mr={2} alt={''} /> <Image src={fileIcon || '/imgs/files/file.svg'} w={'16px'} mr={2} alt={''} />
{t(fileInfo?.filename || 'Filename')} {t(fileInfo?.filename || 'Filename')}
</Flex> </Flex>
<Button <Button
...@@ -172,15 +150,6 @@ const DataCard = ({ kbId }: { kbId: string }) => { ...@@ -172,15 +150,6 @@ const DataCard = ({ kbId }: { kbId: string }) => {
<Box as={'span'} fontSize={['md', 'lg']}> <Box as={'span'} fontSize={['md', 'lg']}>
{total}组 {total}组
</Box> </Box>
<Box as={'span'}>
{(qaListLen > 0 || vectorListLen > 0) && (
<>
({qaListLen > 0 ? `${qaListLen}条数据正在拆分,` : ''}
{vectorListLen > 0 ? `${vectorListLen}条数据正在生成索引,` : ''}
请耐心等待... )
</>
)}
</Box>
</Box> </Box>
<Box flex={1} mr={1} /> <Box flex={1} mr={1} />
<MyInput <MyInput
...@@ -262,7 +231,7 @@ const DataCard = ({ kbId }: { kbId: string }) => { ...@@ -262,7 +231,7 @@ const DataCard = ({ kbId }: { kbId: string }) => {
try { try {
setIsDeleting(true); setIsDeleting(true);
await delOneKbDataByDataId(item.id); await delOneKbDataByDataId(item.id);
refetchData(pageNum); getData(pageNum);
} catch (error) { } catch (error) {
toast({ toast({
title: getErrText(error), title: getErrText(error),
...@@ -297,7 +266,7 @@ const DataCard = ({ kbId }: { kbId: string }) => { ...@@ -297,7 +266,7 @@ const DataCard = ({ kbId }: { kbId: string }) => {
kbId={kbId} kbId={kbId}
defaultValues={editInputData} defaultValues={editInputData}
onClose={() => setEditInputData(undefined)} onClose={() => setEditInputData(undefined)}
onSuccess={() => refetchData()} onSuccess={() => getData(pageNum)}
/> />
)} )}
<ConfirmModal /> <ConfirmModal />
......
...@@ -11,9 +11,8 @@ import { ...@@ -11,9 +11,8 @@ import {
Tbody, Tbody,
Image Image
} from '@chakra-ui/react'; } from '@chakra-ui/react';
import { getKbFiles, deleteKbFileById } from '@/api/plugins/kb'; import { getKbFiles, deleteKbFileById, getTrainingData } from '@/api/plugins/kb';
import { useQuery } from '@tanstack/react-query'; import { useQuery } from '@tanstack/react-query';
import { useToast } from '@/hooks/useToast';
import { debounce } from 'lodash'; import { debounce } from 'lodash';
import { formatFileSize } from '@/utils/tools'; import { formatFileSize } from '@/utils/tools';
import { useConfirm } from '@/hooks/useConfirm'; import { useConfirm } from '@/hooks/useConfirm';
...@@ -26,30 +25,47 @@ import { useRequest } from '@/hooks/useRequest'; ...@@ -26,30 +25,47 @@ import { useRequest } from '@/hooks/useRequest';
import { useLoading } from '@/hooks/useLoading'; import { useLoading } from '@/hooks/useLoading';
import { FileStatusEnum } from '@/constants/kb'; import { FileStatusEnum } from '@/constants/kb';
import { useRouter } from 'next/router'; import { useRouter } from 'next/router';
import { usePagination } from '@/hooks/usePagination';
import { KbFileItemType } from '@/types/plugin';
import { useGlobalStore } from '@/store/global';
const FileCard = ({ kbId }: { kbId: string }) => { const FileCard = ({ kbId }: { kbId: string }) => {
const BoxRef = useRef<HTMLDivElement>(null); const BoxRef = useRef<HTMLDivElement>(null);
const lastSearch = useRef(''); const lastSearch = useRef('');
const router = useRouter(); const router = useRouter();
const { t } = useTranslation(); const { t } = useTranslation();
const [searchText, setSearchText] = useState('');
const { Loading } = useLoading(); const { Loading } = useLoading();
const [searchText, setSearchText] = useState('');
const { setLoading } = useGlobalStore();
const { openConfirm, ConfirmModal } = useConfirm({ const { openConfirm, ConfirmModal } = useConfirm({
content: t('kb.Confirm to delete the file') content: t('kb.Confirm to delete the file')
}); });
const { const {
data: files = [], data: files,
refetch, Pagination,
isInitialLoading total,
} = useQuery(['getFiles', kbId], () => getKbFiles({ kbId, searchText }), { isLoading,
refetchInterval: 6000, getData,
refetchOnWindowFocus: true pageNum,
pageSize
} = usePagination<KbFileItemType>({
api: getKbFiles,
pageSize: 40,
params: {
kbId,
searchText
},
onChange() {
if (BoxRef.current) {
BoxRef.current.scrollTop = 0;
}
}
}); });
const debounceRefetch = useCallback( const debounceRefetch = useCallback(
debounce(() => { debounce(() => {
refetch(); getData(1);
lastSearch.current = searchText; lastSearch.current = searchText;
}, 300), }, 300),
[] []
...@@ -68,14 +84,19 @@ const FileCard = ({ kbId }: { kbId: string }) => { ...@@ -68,14 +84,19 @@ const FileCard = ({ kbId }: { kbId: string }) => {
[files] [files]
); );
const { mutate: onDeleteFile, isLoading } = useRequest({ const { mutate: onDeleteFile } = useRequest({
mutationFn: (fileId: string) => mutationFn: (fileId: string) => {
deleteKbFileById({ setLoading(true);
return deleteKbFileById({
fileId, fileId,
kbId kbId
}), });
},
onSuccess() { onSuccess() {
refetch(); getData(pageNum);
},
onSettled() {
setLoading(false);
}, },
successToast: t('common.Delete Success'), successToast: t('common.Delete Success'),
errorToast: t('common.Delete Failed') errorToast: t('common.Delete Failed')
...@@ -92,12 +113,37 @@ const FileCard = ({ kbId }: { kbId: string }) => { ...@@ -92,12 +113,37 @@ const FileCard = ({ kbId }: { kbId: string }) => {
} }
}; };
// training data
const { data: { qaListLen = 0, vectorListLen = 0 } = {}, refetch: refetchTrainingData } =
useQuery(['getModelSplitDataList', kbId], () => getTrainingData({ kbId, init: false }), {
onError(err) {
console.log(err);
}
});
useQuery(['refetchTrainingData'], refetchTrainingData, {
refetchInterval: 8000,
enabled: qaListLen > 0 || vectorListLen > 0
});
return ( return (
<Box ref={BoxRef} position={'relative'} py={[1, 5]} h={'100%'} overflow={'overlay'}> <Box ref={BoxRef} position={'relative'} py={[1, 5]} h={'100%'} overflow={'overlay'}>
<Flex justifyContent={'space-between'} px={5}> <Flex justifyContent={'space-between'} px={5}>
<Box fontWeight={'bold'} fontSize={'lg'} mr={2}> <Box>
{t('kb.Files', { total: files.length })} <Box fontWeight={'bold'} fontSize={'lg'} mr={2}>
{t('kb.Files', { total: files.length })}
</Box>
<Box as={'span'} fontSize={'sm'}>
{(qaListLen > 0 || vectorListLen > 0) && (
<>
({qaListLen > 0 ? `${qaListLen}条数据正在拆分,` : ''}
{vectorListLen > 0 ? `${vectorListLen}条数据正在生成索引,` : ''}
请耐心等待... )
</>
)}
</Box>
</Box> </Box>
<Flex alignItems={'center'}> <Flex alignItems={'center'}>
<MyInput <MyInput
leftIcon={ leftIcon={
...@@ -112,12 +158,12 @@ const FileCard = ({ kbId }: { kbId: string }) => { ...@@ -112,12 +158,12 @@ const FileCard = ({ kbId }: { kbId: string }) => {
}} }}
onBlur={() => { onBlur={() => {
if (searchText === lastSearch.current) return; if (searchText === lastSearch.current) return;
refetch(); getData(1);
}} }}
onKeyDown={(e) => { onKeyDown={(e) => {
if (searchText === lastSearch.current) return; if (searchText === lastSearch.current) return;
if (e.key === 'Enter') { if (e.key === 'Enter') {
refetch(); getData(1);
} }
}} }}
/> />
...@@ -198,9 +244,14 @@ const FileCard = ({ kbId }: { kbId: string }) => { ...@@ -198,9 +244,14 @@ const FileCard = ({ kbId }: { kbId: string }) => {
</Tbody> </Tbody>
</Table> </Table>
</TableContainer> </TableContainer>
{total > pageSize && (
<Flex mt={2} justifyContent={'center'}>
<Pagination />
</Flex>
)}
<ConfirmModal /> <ConfirmModal />
<Loading loading={isInitialLoading || isLoading} /> <Loading loading={isLoading} />
</Box> </Box>
); );
}; };
......
...@@ -40,7 +40,11 @@ const QAImport = ({ kbId }: { kbId: string }) => { ...@@ -40,7 +40,11 @@ const QAImport = ({ kbId }: { kbId: string }) => {
// price count // price count
const price = useMemo(() => { const price = useMemo(() => {
return formatPrice(files.reduce((sum, file) => sum + file.tokens, 0) * unitPrice * 1.3); const filesToken = files.reduce((sum, file) => sum + file.tokens, 0);
const promptTokens = files.reduce((sum, file) => sum + file.chunks.length, 0) * 139;
const totalToken = (filesToken + promptTokens) * 1.8;
return formatPrice(totalToken * unitPrice);
}, [files, unitPrice]); }, [files, unitPrice]);
const { openConfirm, ConfirmModal } = useConfirm({ const { openConfirm, ConfirmModal } = useConfirm({
......
import React, { useState, Dispatch, useCallback } from 'react'; import React, { useState, Dispatch, useCallback, useRef } from 'react';
import { FormControl, Flex, Input, Button, FormErrorMessage, Box } from '@chakra-ui/react'; import { FormControl, Flex, Input, Button, FormErrorMessage, Box } from '@chakra-ui/react';
import { useForm } from 'react-hook-form'; import { useForm } from 'react-hook-form';
import { useRouter } from 'next/router'; import { useRouter } from 'next/router';
import { PageTypeEnum } from '@/constants/user'; import { OAuthEnum, PageTypeEnum } from '@/constants/user';
import { postLogin } from '@/api/user'; import { postLogin } from '@/api/user';
import type { ResLogin } from '@/api/response/user'; import type { ResLogin } from '@/api/response/user';
import { useToast } from '@/hooks/useToast'; import { useToast } from '@/hooks/useToast';
import { feConfigs } from '@/store/static'; import { feConfigs } from '@/store/static';
import { useGlobalStore } from '@/store/global'; import { useGlobalStore } from '@/store/global';
import MyIcon from '@/components/Icon'; import MyIcon from '@/components/Icon';
import { customAlphabet } from 'nanoid';
const nanoid = customAlphabet('abcdefghijklmnopqrstuvwxyz1234567890', 8);
interface Props { interface Props {
setPageType: Dispatch<`${PageTypeEnum}`>; setPageType: Dispatch<`${PageTypeEnum}`>;
...@@ -58,18 +60,29 @@ const LoginForm = ({ setPageType, loginSuccess }: Props) => { ...@@ -58,18 +60,29 @@ const LoginForm = ({ setPageType, loginSuccess }: Props) => {
[loginSuccess, toast] [loginSuccess, toast]
); );
const onclickGit = useCallback(() => { const redirectUri = `${location.origin}/login/provider`;
setLoginStore({ const state = useRef(nanoid());
provider: 'git',
lastRoute const oAuthList = [
}); ...(feConfigs?.oauth?.github
router.replace( ? [
`https://github.com/login/oauth/authorize?client_id=${ {
feConfigs?.gitLoginKey provider: OAuthEnum.github,
}&redirect_uri=${`${location.origin}/login/provider`}&scope=user:email%20read:user`, icon: 'gitFill',
'_self' redirectUrl: `https://github.com/login/oauth/authorize?client_id=${feConfigs?.oauth?.github}&redirect_uri=${redirectUri}&state=${state.current}&scope=user:email%20read:user`
); }
}, [lastRoute, setLoginStore]); ]
: []),
...(feConfigs?.oauth?.google
? [
{
provider: OAuthEnum.google,
icon: 'googleFill',
redirectUrl: `https://accounts.google.com/o/oauth2/v2/auth?client_id=${feConfigs?.oauth?.google}&redirect_uri=${redirectUri}&state=${state.current}&response_type=code&scope=https%3A%2F%2Fwww.googleapis.com%2Fauth%2Fuserinfo.profile%20https%3A%2F%2Fwww.googleapis.com%2Fauth%2Fuserinfo.email%20openid&include_granted_scopes=true`
}
]
: [])
];
return ( return (
<> <>
...@@ -138,14 +151,24 @@ const LoginForm = ({ setPageType, loginSuccess }: Props) => { ...@@ -138,14 +151,24 @@ const LoginForm = ({ setPageType, loginSuccess }: Props) => {
</Button> </Button>
{feConfigs?.show_register && ( {feConfigs?.show_register && (
<> <>
<Flex mt={10} justifyContent={'center'} alignItems={'center'}> <Flex mt={10} justifyContent={'space-around'} alignItems={'center'}>
<MyIcon {oAuthList.map((item) => (
name="gitFill" <MyIcon
w={'34px'} key={item.provider}
cursor={'pointer'} name={item.icon as any}
color={'myGray.800'} w={'34px'}
onClick={onclickGit} cursor={'pointer'}
/> color={'myGray.800'}
onClick={() => {
setLoginStore({
provider: item.provider,
lastRoute,
state: state.current
});
router.replace(item.redirectUrl, '_self');
}}
/>
))}
</Flex> </Flex>
</> </>
)} )}
......
...@@ -5,14 +5,14 @@ import { ResLogin } from '@/api/response/user'; ...@@ -5,14 +5,14 @@ import { ResLogin } from '@/api/response/user';
import { useChatStore } from '@/store/chat'; import { useChatStore } from '@/store/chat';
import { useUserStore } from '@/store/user'; import { useUserStore } from '@/store/user';
import { setToken } from '@/utils/user'; import { setToken } from '@/utils/user';
import { gitLogin } from '@/api/user'; import { oauthLogin } from '@/api/user';
import { useToast } from '@/hooks/useToast'; import { useToast } from '@/hooks/useToast';
import Loading from '@/components/Loading'; import Loading from '@/components/Loading';
import { serviceSideProps } from '@/utils/i18n'; import { serviceSideProps } from '@/utils/i18n';
import { useQuery } from '@tanstack/react-query'; import { useQuery } from '@tanstack/react-query';
import { getErrText } from '@/utils/tools'; import { getErrText } from '@/utils/tools';
const provider = ({ code }: { code: string }) => { const provider = ({ code, state }: { code: string; state: string }) => {
const { loginStore } = useGlobalStore(); const { loginStore } = useGlobalStore();
const { setLastChatId, setLastChatAppId } = useChatStore(); const { setLastChatId, setLastChatAppId } = useChatStore();
const { setUserInfo } = useUserStore(); const { setUserInfo } = useUserStore();
...@@ -36,45 +36,55 @@ const provider = ({ code }: { code: string }) => { ...@@ -36,45 +36,55 @@ const provider = ({ code }: { code: string }) => {
[setLastChatId, setLastChatAppId, setUserInfo, router, loginStore?.lastRoute] [setLastChatId, setLastChatAppId, setUserInfo, router, loginStore?.lastRoute]
); );
const authCode = useCallback(async () => { const authCode = useCallback(
if (!code) return; async (code: string) => {
if (!loginStore) { if (!loginStore) {
router.replace('/login'); router.replace('/login');
return; return;
} }
try { try {
const res = await (async () => { const res = await oauthLogin({
if (loginStore.provider === 'git') { type: loginStore?.provider,
return gitLogin({ code,
code, callbackUrl: `${location.origin}/login/provider`,
inviterId: localStorage.getItem('inviterId') || undefined inviterId: localStorage.getItem('inviterId') || undefined
});
if (!res) {
toast({
status: 'warning',
title: '登录异常'
}); });
return setTimeout(() => {
router.replace('/login');
}, 1000);
} }
return null; loginSuccess(res);
})(); } catch (error) {
if (!res) {
toast({ toast({
status: 'warning', status: 'warning',
title: '登录异常' title: getErrText(error, '登录异常')
}); });
return setTimeout(() => { setTimeout(() => {
router.replace('/login'); router.replace('/login');
}, 1000); }, 1000);
} }
loginSuccess(res); },
} catch (error) { [loginStore, loginSuccess, router, toast]
);
useQuery(['init', code], () => {
if (!code) return;
if (state !== loginStore?.state) {
toast({ toast({
status: 'warning', status: 'warning',
title: getErrText(error, '登录异常') title: '安全校验失败'
}); });
setTimeout(() => { setTimeout(() => {
router.replace('/login'); router.replace('/login');
}, 1000); }, 1000);
return;
} }
}, [code, loginStore, loginSuccess]); authCode(code);
useQuery(['init', code], () => {
authCode();
return null; return null;
}); });
...@@ -85,6 +95,7 @@ export async function getServerSideProps(content: any) { ...@@ -85,6 +95,7 @@ export async function getServerSideProps(content: any) {
return { return {
props: { props: {
code: content?.query?.code, code: content?.query?.code,
state: content?.query?.state,
...(await serviceSideProps(content)) ...(await serviceSideProps(content))
} }
}; };
......
import { Schema, model, models, Model } from 'mongoose';
import type { IpLimitSchemaType } from '@/types/common/ipLimit';
const IpLimitSchema = new Schema({
eventId: {
type: String,
required: true
},
ip: {
type: String,
required: true
},
account: {
type: Number,
default: 0
},
lastMinute: {
type: Date,
default: () => new Date()
}
});
export const IpLimit: Model<IpLimitSchemaType> =
models['ip_limit'] || model('ip_limit', IpLimitSchema);
...@@ -133,7 +133,7 @@ export * from './models/trainingData'; ...@@ -133,7 +133,7 @@ export * from './models/trainingData';
export * from './models/openapi'; export * from './models/openapi';
export * from './models/promotionRecord'; export * from './models/promotionRecord';
export * from './models/collection'; export * from './models/collection';
export * from './models/outLink';
export * from './models/kb'; export * from './models/kb';
export * from './models/inform'; export * from './models/inform';
export * from './models/image'; export * from './models/image';
export * from './support/outLink/schema';
import { PRICE_SCALE } from '@/constants/common';
import { IpLimit } from '@/service/common/ipLimit/schema';
import { authBalanceByUid, AuthUserTypeEnum } from '@/service/utils/auth';
import { OutLinkSchema } from '@/types/support/outLink';
import { OutLink } from './schema';
export async function authOutLinkChat({ shareId, ip }: { shareId: string; ip?: string | null }) {
// get outLink
const outLink = await OutLink.findOne({
shareId
});
if (!outLink) {
return Promise.reject('分享链接无效');
}
const uid = String(outLink.userId);
// authBalance
const user = await authBalanceByUid(uid);
// limit auth
await authOutLinkLimit({ outLink, ip });
return {
user,
userId: String(outLink.userId),
appId: String(outLink.appId),
authType: AuthUserTypeEnum.token,
responseDetail: outLink.responseDetail
};
}
export async function authOutLinkLimit({
outLink,
ip
}: {
outLink: OutLinkSchema;
ip?: string | null;
}) {
if (!ip || !outLink.limit) {
return;
}
if (outLink.limit.expiredTime && outLink.limit.expiredTime.getTime() < Date.now()) {
return Promise.reject('分享链接已过期');
}
if (outLink.limit.credit > -1 && outLink.total > outLink.limit.credit * PRICE_SCALE) {
return Promise.reject('链接超出使用限制');
}
const ipLimit = await IpLimit.findOne({ ip, eventId: outLink._id });
try {
if (!ipLimit) {
await IpLimit.create({
eventId: outLink._id,
ip,
account: outLink.limit.QPM - 1
});
return;
}
// over one minute
const diffTime = Date.now() - ipLimit.lastMinute.getTime();
if (diffTime >= 60 * 1000) {
ipLimit.account = outLink.limit.QPM - 1;
ipLimit.lastMinute = new Date();
return await ipLimit.save();
}
if (ipLimit.account <= 0) {
return Promise.reject(
`每分钟仅能请求 ${outLink.limit.QPM} 次, ${60 - Math.round(diffTime / 1000)}s 后重试~`
);
}
ipLimit.account = ipLimit.account - 1;
await ipLimit.save();
} catch (error) {}
}
import { Schema, model, models, Model } from 'mongoose'; import { Schema, model, models, Model } from 'mongoose';
import { OutLinkSchema as SchmaType } from '@/types/mongoSchema'; import { OutLinkSchema as SchemaType } from '@/types/support/outLink';
import { OutLinkTypeEnum } from '@/constants/chat'; import { OutLinkTypeEnum } from '@/constants/chat';
const OutLinkSchema = new Schema({ const OutLinkSchema = new Schema({
...@@ -26,12 +26,30 @@ const OutLinkSchema = new Schema({ ...@@ -26,12 +26,30 @@ const OutLinkSchema = new Schema({
required: true required: true
}, },
total: { total: {
// total amount
type: Number, type: Number,
default: 0 default: 0
}, },
lastTime: { lastTime: {
type: Date type: Date
},
responseDetail: {
type: Boolean,
default: false
},
limit: {
expiredTime: {
type: Date
},
QPM: {
type: Number,
default: 1000
},
credit: {
type: Number,
default: -1
}
} }
}); });
export const OutLink: Model<SchmaType> = models['outlinks'] || model('outlinks', OutLinkSchema); export const OutLink: Model<SchemaType> = models['outlinks'] || model('outlinks', OutLinkSchema);
...@@ -208,24 +208,3 @@ export const authKb = async ({ kbId, userId }: { kbId: string; userId: string }) ...@@ -208,24 +208,3 @@ export const authKb = async ({ kbId, userId }: { kbId: string; userId: string })
} }
return Promise.reject(ERROR_ENUM.unAuthKb); return Promise.reject(ERROR_ENUM.unAuthKb);
}; };
export const authShareChat = async ({ shareId }: { shareId: string }) => {
// get shareChat
const shareChat = await OutLink.findOne({ shareId });
if (!shareChat) {
return Promise.reject('分享链接已失效');
}
const uid = String(shareChat.userId);
// authBalance
const user = await authBalanceByUid(uid);
return {
user,
userId: String(shareChat.userId),
appId: String(shareChat.appId),
authType: AuthUserTypeEnum.token
};
};
...@@ -2,8 +2,9 @@ import { create } from 'zustand'; ...@@ -2,8 +2,9 @@ import { create } from 'zustand';
import { devtools, persist } from 'zustand/middleware'; import { devtools, persist } from 'zustand/middleware';
import { immer } from 'zustand/middleware/immer'; import { immer } from 'zustand/middleware/immer';
import axios from 'axios'; import axios from 'axios';
import { OAuthEnum } from '@/constants/user';
type LoginStoreType = { provider: 'git'; lastRoute: string }; type LoginStoreType = { provider: `${OAuthEnum}`; lastRoute: string; state: string };
type State = { type State = {
lastRoute: string; lastRoute: string;
......
...@@ -38,10 +38,6 @@ export interface ShareAppItem { ...@@ -38,10 +38,6 @@ export interface ShareAppItem {
isCollection: boolean; isCollection: boolean;
} }
export type ShareChatEditType = {
name: string;
};
/* agent */ /* agent */
/* question classify */ /* question classify */
export type ClassifyQuestionAgentItemType = { export type ClassifyQuestionAgentItemType = {
......
export type IpLimitSchemaType = {
_id: string;
eventId: string;
ip: string;
account: number;
lastMinute: Date;
};
...@@ -27,8 +27,13 @@ export type FeConfigsType = { ...@@ -27,8 +27,13 @@ export type FeConfigsType = {
authorText?: string; authorText?: string;
beianText?: string; beianText?: string;
googleClientVerKey?: string; googleClientVerKey?: string;
gitLoginKey?: string; oauth?: {
exportLimitMinutes?: number; github?: string;
google?: string;
};
limit?: {
exportLimitMinutes?: number;
};
scripts?: { [key: string]: string }[]; scripts?: { [key: string]: string }[];
}; };
export type SystemEnvType = { export type SystemEnvType = {
......
...@@ -4,7 +4,7 @@ import type { DataType } from './data'; ...@@ -4,7 +4,7 @@ import type { DataType } from './data';
import { BillSourceEnum, InformTypeEnum } from '@/constants/user'; import { BillSourceEnum, InformTypeEnum } from '@/constants/user';
import { TrainingModeEnum } from '@/constants/plugin'; import { TrainingModeEnum } from '@/constants/plugin';
import type { AppModuleItemType } from './app'; import type { AppModuleItemType } from './app';
import { ChatSourceEnum, OutLinkTypeEnum } from '@/constants/chat'; import { ChatSourceEnum } from '@/constants/chat';
import { AppTypeEnum } from '@/constants/app'; import { AppTypeEnum } from '@/constants/app';
import { KbTypeEnum } from '@/constants/kb'; import { KbTypeEnum } from '@/constants/kb';
...@@ -156,17 +156,6 @@ export interface PromotionRecordSchema { ...@@ -156,17 +156,6 @@ export interface PromotionRecordSchema {
amount: number; amount: number;
} }
export interface OutLinkSchema {
_id: string;
shareId: string;
userId: string;
appId: string;
name: string;
total: number;
lastTime: Date;
type: `${OutLinkTypeEnum}`;
}
export type kbSchema = { export type kbSchema = {
_id: string; _id: string;
userId: string; userId: string;
......
import { OutLinkTypeEnum } from '@/constants/chat';
export interface OutLinkSchema {
_id: string;
shareId: string;
userId: string;
appId: string;
name: string;
total: number;
lastTime: Date;
type: `${OutLinkTypeEnum}`;
responseDetail: boolean;
limit?: {
expiredTime?: Date;
QPM: number;
credit: number;
};
}
export type OutLinkEditType = {
_id?: string;
name: string;
responseDetail: OutLinkSchema['responseDetail'];
limit: OutLinkSchema['limit'];
};
...@@ -31,7 +31,6 @@ weight: 520 ...@@ -31,7 +31,6 @@ weight: 520
"show_git": true, // 是否展示 Git "show_git": true, // 是否展示 Git
"systemTitle": "FastGPT", // 系统的 title "systemTitle": "FastGPT", // 系统的 title
"authorText": "Made by FastGPT Team.", // 签名 "authorText": "Made by FastGPT Team.", // 签名
"gitLoginKey": "" // Git 登录凭证
}, },
... ...
... ...
...@@ -56,7 +55,6 @@ weight: 520 ...@@ -56,7 +55,6 @@ weight: 520
"show_git": true, "show_git": true,
"systemTitle": "FastGPT", "systemTitle": "FastGPT",
"authorText": "Made by FastGPT Team.", "authorText": "Made by FastGPT Team.",
"gitLoginKey": "",
"scripts": [] "scripts": []
}, },
"SystemParams": { "SystemParams": {
......
...@@ -9,7 +9,6 @@ ...@@ -9,7 +9,6 @@
"show_doc": true, "show_doc": true,
"systemTitle": "FastGPT", "systemTitle": "FastGPT",
"authorText": "Made by FastGPT Team.", "authorText": "Made by FastGPT Team.",
"gitLoginKey": "",
"scripts": [] "scripts": []
}, },
"SystemParams": { "SystemParams": {
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or sign in to comment