diff --git a/docSite/assets/imgs/sharelinkProcess.png b/docSite/assets/imgs/sharelinkProcess.png new file mode 100644 index 000000000..b6e7076e7 Binary files /dev/null and b/docSite/assets/imgs/sharelinkProcess.png differ diff --git a/docSite/content/docs/development/openApi.md b/docSite/content/docs/development/openApi.md index 90b30e841..c2498f82e 100644 --- a/docSite/content/docs/development/openApi.md +++ b/docSite/content/docs/development/openApi.md @@ -38,7 +38,7 @@ FastGPT 的 API Key 有 2 类,一类是全局通用的 key;一类是携带 - headers.Authorization: Bearer apikey - chatId: string | undefined 。 - 为 undefined 时(不传入),不使用 FastGpt 提供的上下文功能,完全通过传入的 messages 构建上下文。 不会将你的记录存储到数据库中,你也无法在记录汇总中查阅到。 - - 为非空字符串时,意味着使用 chatId 进行对话,自动从 FastGpt 数据库取历史记录。并拼接 messages 数组最后一个内容作为完整请求。(自行确保 chatid 唯一,长度不限) + - 为非空字符串时,意味着使用 chatId 进行对话,自动从 FastGpt 数据库取历史记录。并拼接 messages 数组最后一个内容作为完整请求。(自行确保 chatId 唯一,长度不限) - messages: 与 openai gpt 接口完全一致。 - detail: 是否返回详细值(模块状态,响应的完整结果),会通过event进行区分 - variables: 变量。一个对象,效果同全局变量。 @@ -227,7 +227,7 @@ data: [{"moduleName":"KB Search","price":1.2000000000000002,"model":"Embedding-2 ![](/imgs/getKbId.png) -### 往知识库添加数据 +### 知识库添加数据 {{< tabs tabTotal="4" >}} {{< tab tabName="请求示例" >}} @@ -241,10 +241,11 @@ curl --location --request POST 'https://fastgpt.run/api/core/dataset/data/pushDa     "kbId": "64663f451ba1676dbdef0499", "mode": "index", "prompt": "qa 拆分引导词,index 模式下可以忽略", + "billId": "可选。如果有这个值,本次的数据会被聚合到一个订单中,这个值可以重复使用。可以参考 [创建训练订单] 获取该值。",     "data": [         {             "a": "test", -            "q": "1111" +            "q": "1111",         },         {             "a": "test2", @@ -370,6 +371,158 @@ curl --location --request POST 'https://fastgpt.run/api/core/dataset/searchTest' {{< /tabs >}} +## 订单 + +### 创建训练订单 + +**请求示例** + +```bash +curl --location --request POST 'https://fastgpt.run/api/common/bill/createTrainingBill' \ +--header 'Authorization: Bearer {{apikey}}' \ +--header 'Content-Type: application/json' \ +--data-raw '' +``` + +**响应结果** + +data 为 billId,可用于 api 添加数据时进行账单聚合。 + +```json +{ + "code": 200, + "statusText": "", + "message": "", + "data": "65112ab717c32018f4156361" +} +``` + +## 免登录分享链接校验(内测中) + +免登录链接配置中,增加了`凭证校验服务器`后,使用分享链接时会向服务器发起请求,校验链接是否可用,并在每次对话结束后,向服务器发送对话结果。下面以`host`来表示`凭证校验服务器`。服务器接口仅需返回是否校验成功即可,不需要返回其他数据,格式如下: + +```json +{ + "success": true, + "message": "错误提示" +} +``` + +![](/imgs/sharelinkProcess.png) + +### 分享链接中增加额外 query + +增加一个 query: authToken。例如: + +原始的链接:https://fastgpt.run/chat/share?shareId=648aaf5ae121349a16d62192 +完整链接: https://fastgpt.run/chat/share?shareId=648aaf5ae121349a16d62192&authToken=userid12345 + +发出校验请求时候,会在`body`中携带 token={{authToken}} 的参数。 + +### 初始化校验 + +**FastGPT 发出的请求** + +```bash +curl --location --request POST '{{host}}/shareAuth/init' \ +--header 'Content-Type: application/json' \ +--data-raw '{ + "token": "sintdolore" +}' +``` + +### 对话前校验 + +**FastGPT 发出的请求** + +```bash +curl --location --request POST '{{host}}/shareAuth/start' \ +--header 'Content-Type: application/json' \ +--data-raw '{ + "token": "sintdolore", + "question": "用户问题", +}' +``` + +### 对话结果上报 + +**FastGPT 发出的请求** + +```bash +curl --location --request POST '{{host}}/shareAuth/finish' \ +--header 'Content-Type: application/json' \ +--data-raw '{ + "token": "sint dolore", + "responseData": [ + { + "moduleName": "KB Search", + "price": 1.2000000000000002, + "model": "Embedding-2", + "tokens": 6, + "similarity": 0.61, + "limit": 3 + }, + { + "moduleName": "AI Chat", + "price": 454.5, + "model": "FastAI-4k", + "tokens": 303, + "question": "导演是谁", + "answer": "电影《铃芽之旅》的导演是新海诚。", + "maxToken": 2050, + "quoteList": [ + { + "kb_id": "646627f4f7b896cfd8910e38", + "id": "8099", + "q": "本作的主人公是谁?", + "a": "本作的主人公是名叫铃芽的少女。", + "source": "手动修改" + }, + { + "kb_id": "646627f4f7b896cfd8910e38", + "id": "8686", + "q": "电影《铃芽之旅》男主角是谁?", + "a": "电影《铃芽之旅》男主角是宗像草太,由松村北斗配音。", + "source": "" + }, + { + "kb_id": "646627f4f7b896cfd8910e38", + "id": "19339", + "q": "电影《铃芽之旅》的导演是谁?22", + "a": "电影《铃芽之旅》的导演是新海诚。", + "source": "手动修改" + } + ], + "completeMessages": [ + { + "obj": "System", + "value": "下面是知识库内容:\n1. [本作的主人公是谁?\n本作的主人公是名叫铃芽的少女。]\n2. [电影《铃芽之旅》男主角是谁?\n电影《铃芽之旅》男主角是宗像草太,由松村北斗配音。]\n3. [电影《铃芽之旅》的导演是谁?22\n电影《铃芽之旅》的导演是新海诚。]\n" + }, + { + "obj": "System", + "value": "1.请记住,你的身份是百度的下一代知识增强语言模型,能够完全根据知识库提供的内容回答问题。\n\n2. 你忘记了关于电影《铃芽之旅》以外的内容。" + }, + { + "obj": "System", + "value": "你仅回答关于电影《玲芽之旅》的问题,其余问题直接回复: 我不清楚。" + }, + { + "obj": "Human", + "value": "导演是谁" + }, + { + "obj": "AI", + "value": "电影《铃芽之旅》的导演是新海诚。" + } + ] + } + ] +}' +``` + +响应值与 chat 接口相同,增加了一个 token。可以重点关注`responseData`里的值,price 与实际价格的倍率为`100000`。 + +**此接口无需响应值** # 使用案例 diff --git a/projects/app/public/locales/en/common.json b/projects/app/public/locales/en/common.json index d10d5e768..f84bfc438 100644 --- a/projects/app/public/locales/en/common.json +++ b/projects/app/public/locales/en/common.json @@ -248,7 +248,10 @@ "QPM Tips": "The maximum number of queries per IP address per minute", "QPM is empty": "QPM is empty", "Response Detail": "Quote", - "Response Detail tips": "Whether detailed data such as references to be returned" + "Response Detail tips": "Whether detailed data such as references to be returned", + "token auth": "Token Auth", + "token auth Tips": "Identity verification server address. If this value is set, the server will be specified to send a request for identity verification before each session", + "token auth use cases": "Review the authentication instructions" }, "system": { "Help Document": "Document" diff --git a/projects/app/public/locales/zh/common.json b/projects/app/public/locales/zh/common.json index 35406e16f..ff1baee95 100644 --- a/projects/app/public/locales/zh/common.json +++ b/projects/app/public/locales/zh/common.json @@ -248,7 +248,10 @@ "QPM Tips": "每个 IP 每分钟最多提问多少次", "QPM is empty": "QPM 不能为空", "Response Detail": "返回详情", - "Response Detail tips": "是否需要返回详情(引用内容,调用时间等,不会返回预设提示词和完整上下文)" + "Response Detail tips": "是否需要返回详情(引用内容,调用时间等,不会返回预设提示词和完整上下文)", + "token auth": "身份验证", + "token auth Tips": "身份校验服务器地址,如填写该值,每次对话前都会想指定服务器发送一个请求,进行身份校验", + "token auth use cases": "查看身份验证使用说明" }, "system": { "Help Document": "帮助文档" diff --git a/projects/app/src/api/core/dataset/data.d.ts b/projects/app/src/api/core/dataset/data.d.ts index f869a148c..cdd8354aa 100644 --- a/projects/app/src/api/core/dataset/data.d.ts +++ b/projects/app/src/api/core/dataset/data.d.ts @@ -1,10 +1,11 @@ import { KbTypeEnum } from '@/constants/dataset'; import type { RequestPaging } from '@/types'; import { TrainingModeEnum } from '@/constants/plugin'; +import { DatasetDataItemType } from '@/types/core/dataset/data'; export type PushDataProps = { kbId: string; - data: DatasetItemType[]; + data: DatasetDataItemType[]; mode: `${TrainingModeEnum}`; prompt?: string; billId?: string; diff --git a/projects/app/src/api/support/outLink.ts b/projects/app/src/api/support/outLink.ts index c94640ba4..f550344e0 100644 --- a/projects/app/src/api/support/outLink.ts +++ b/projects/app/src/api/support/outLink.ts @@ -6,7 +6,7 @@ import type { OutLinkSchema } from '@/types/support/outLink'; /** * 初始化分享聊天 */ -export const initShareChatInfo = (data: { shareId: string }) => +export const initShareChatInfo = (data: { shareId: string; authToken?: string }) => GET(`/support/outLink/init`, data); /** diff --git a/projects/app/src/components/ChatBox/index.tsx b/projects/app/src/components/ChatBox/index.tsx index 0efe9b487..169480512 100644 --- a/projects/app/src/components/ChatBox/index.tsx +++ b/projects/app/src/components/ChatBox/index.tsx @@ -139,6 +139,7 @@ const ChatBox = ( userAvatar, variableModules, welcomeText, + active = true, onUpdateVariable, onStartChat, onDelMessage @@ -152,6 +153,7 @@ const ChatBox = ( userAvatar?: string; variableModules?: VariableItemType[]; welcomeText?: string; + active?: boolean; onUpdateVariable?: (e: Record) => void; onStartChat?: (e: StartChatFnProps) => Promise<{ responseText: string; @@ -860,7 +862,7 @@ const ChatBox = ( {/* input */} - {onStartChat && variableIsFinish ? ( + {onStartChat && variableIsFinish && active ? ( (res, { data: await pushDataToKb({ diff --git a/projects/app/src/pages/api/core/dataset/data/updateData.ts b/projects/app/src/pages/api/core/dataset/data/updateData.ts index 9b949140a..d40ebbca2 100644 --- a/projects/app/src/pages/api/core/dataset/data/updateData.ts +++ b/projects/app/src/pages/api/core/dataset/data/updateData.ts @@ -20,7 +20,7 @@ export default withNextCors(async function handler(req: NextApiRequest, res: Nex // auth user and get kb const [{ userId }, kb] = await Promise.all([ - authUser({ req }), + authUser({ req, authToken: true }), KB.findById(kbId, 'vectorModel') ]); diff --git a/projects/app/src/pages/api/core/dataset/searchTest.ts b/projects/app/src/pages/api/core/dataset/searchTest.ts index dc78b17b3..0873f9ca1 100644 --- a/projects/app/src/pages/api/core/dataset/searchTest.ts +++ b/projects/app/src/pages/api/core/dataset/searchTest.ts @@ -18,7 +18,7 @@ export default withNextCors(async function handler(req: NextApiRequest, res: Nex // 凭证校验 const [{ userId }, kb] = await Promise.all([ - authUser({ req }), + authUser({ req, authToken: true, authApiKey: true }), KB.findById(kbId, 'vectorModel') ]); diff --git a/projects/app/src/pages/api/openapi/plugin/vector.ts b/projects/app/src/pages/api/openapi/plugin/vector.ts index dfec8f640..ee903dbbf 100644 --- a/projects/app/src/pages/api/openapi/plugin/vector.ts +++ b/projects/app/src/pages/api/openapi/plugin/vector.ts @@ -17,7 +17,7 @@ type Response = { export default withNextCors(async function handler(req: NextApiRequest, res: NextApiResponse) { try { - const { userId } = await authUser({ req }); + const { userId } = await authUser({ req, authToken: true }); let { input, model } = req.query as Props; if (!Array.isArray(input)) { diff --git a/projects/app/src/pages/api/openapi/v1/chat/completions.ts b/projects/app/src/pages/api/openapi/v1/chat/completions.ts index e99ac1b27..6fe3fdd8c 100644 --- a/projects/app/src/pages/api/openapi/v1/chat/completions.ts +++ b/projects/app/src/pages/api/openapi/v1/chat/completions.ts @@ -34,7 +34,7 @@ import requestIp from 'request-ip'; import { replaceVariable } from '@/utils/common/tools/text'; import { ModuleDispatchProps } from '@/types/core/modules'; import { selectShareResponse } from '@/utils/service/core/chat'; -import { updateOutLinkUsage } from '@/service/support/outLink'; +import { pushResult2Remote, updateOutLinkUsage } from '@/service/support/outLink'; import { updateApiKeyUsage } from '@/service/support/openapi'; export type MessageItemType = ChatCompletionRequestMessage & { dataId?: string }; @@ -44,6 +44,7 @@ type FastGptWebChatProps = { }; type FastGptShareChatProps = { shareId?: string; + authToken?: string; }; export type Props = CreateChatCompletionRequest & FastGptWebChatProps & @@ -71,6 +72,7 @@ export default withNextCors(async function handler(req: NextApiRequest, res: Nex chatId, appId, shareId, + authToken, stream = false, detail = false, messages = [], @@ -111,10 +113,15 @@ export default withNextCors(async function handler(req: NextApiRequest, res: Nex if (shareId) { return authOutLinkChat({ shareId, - ip: requestIp.getClientIp(req) + ip: requestIp.getClientIp(req), + authToken, + question: + (messages[messages.length - 2]?.role === 'user' + ? messages[messages.length - 2].content + : messages[messages.length - 1]?.content) || '' }); } - return authUser({ req, authBalance: true }); + return authUser({ req, authToken: true, authApiKey: true, authBalance: true }); })(); if (!user) { @@ -260,11 +267,13 @@ export default withNextCors(async function handler(req: NextApiRequest, res: Nex response: responseData }); - !!shareId && + if (shareId) { + pushResult2Remote({ authToken, shareId, responseData }); updateOutLinkUsage({ shareId, total }); + } !!apikey && updateApiKeyUsage({ apikey, diff --git a/projects/app/src/pages/api/openapi/v1/chat/getHistory.ts b/projects/app/src/pages/api/openapi/v1/chat/getHistory.ts index 0c653f850..ca3e5d829 100644 --- a/projects/app/src/pages/api/openapi/v1/chat/getHistory.ts +++ b/projects/app/src/pages/api/openapi/v1/chat/getHistory.ts @@ -16,7 +16,7 @@ export type Response = { history: ChatItemType[] }; export default async function handler(req: NextApiRequest, res: NextApiResponse) { try { await connectToDatabase(); - const { userId } = await authUser({ req }); + const { userId } = await authUser({ req, authToken: true }); const { chatId, limit } = req.body as Props; jsonRes(res, { diff --git a/projects/app/src/pages/api/plugins/urlFetch.ts b/projects/app/src/pages/api/plugins/urlFetch.ts index 2403f3b02..ea908e04c 100644 --- a/projects/app/src/pages/api/plugins/urlFetch.ts +++ b/projects/app/src/pages/api/plugins/urlFetch.ts @@ -18,7 +18,7 @@ const fetchContent = async (req: NextApiRequest, res: NextApiResponse) => { throw new Error('urlList is empty'); } - await authUser({ req }); + await authUser({ req, authToken: true }); urlList = urlList.filter((url) => /^(http|https):\/\/[^ "]+$/.test(url)); diff --git a/projects/app/src/pages/api/support/file/delete.ts b/projects/app/src/pages/api/support/file/delete.ts index 4b7a2f536..bfac05649 100644 --- a/projects/app/src/pages/api/support/file/delete.ts +++ b/projects/app/src/pages/api/support/file/delete.ts @@ -14,7 +14,7 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse< throw new Error('fileId is empty'); } - const { userId } = await authUser({ req }); + const { userId } = await authUser({ req, authToken: true }); const gridFs = new GridFSStorage('dataset', userId); diff --git a/projects/app/src/pages/api/support/file/readUrl.ts b/projects/app/src/pages/api/support/file/readUrl.ts index 77a33d4e9..59fb7eb1d 100644 --- a/projects/app/src/pages/api/support/file/readUrl.ts +++ b/projects/app/src/pages/api/support/file/readUrl.ts @@ -16,7 +16,7 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse< throw new Error('fileId is empty'); } - const { userId } = await authUser({ req }); + const { userId } = await authUser({ req, authToken: true }); // auth file const gridFs = new GridFSStorage('dataset', userId); diff --git a/projects/app/src/pages/api/support/outLink/init.ts b/projects/app/src/pages/api/support/outLink/init.ts index 01e364099..482d926f1 100644 --- a/projects/app/src/pages/api/support/outLink/init.ts +++ b/projects/app/src/pages/api/support/outLink/init.ts @@ -5,12 +5,14 @@ import type { InitShareChatResponse } from '@/api/response/chat'; import { authApp } from '@/service/utils/auth'; import { HUMAN_ICON } from '@/constants/chat'; import { getChatModelNameList, getSpecialModule } from '@/components/ChatBox/utils'; +import { authShareChatInit } from '@/service/support/outLink/auth'; /* init share chat window */ export default async function handler(req: NextApiRequest, res: NextApiResponse) { try { - let { shareId } = req.query as { + let { shareId, authToken } = req.query as { shareId: string; + authToken?: string; }; if (!shareId) { @@ -36,7 +38,8 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse) userId: String(shareChat.userId), authOwner: false }), - User.findById(shareChat.userId, 'avatar') + User.findById(shareChat.userId, 'avatar'), + authShareChatInit(authToken, shareChat.limit?.hookUrl) ]); jsonRes(res, { diff --git a/projects/app/src/pages/app/detail/components/OutLink/Share.tsx b/projects/app/src/pages/app/detail/components/OutLink/Share.tsx index 08c940fa6..3a8dcff78 100644 --- a/projects/app/src/pages/app/detail/components/OutLink/Share.tsx +++ b/projects/app/src/pages/app/detail/components/OutLink/Share.tsx @@ -17,7 +17,8 @@ import { Menu, MenuButton, MenuList, - MenuItem + MenuItem, + Link } from '@chakra-ui/react'; import { QuestionOutlineIcon } from '@chakra-ui/icons'; import MyIcon from '@/components/Icon'; @@ -58,7 +59,7 @@ const Share = ({ appId }: { appId: string }) => { } = useQuery(['initShareChatList', appId], () => getShareChatList(appId)); return ( - + 免登录窗口 @@ -85,7 +86,7 @@ const Share = ({ appId }: { appId: string }) => { - +
@@ -96,6 +97,7 @@ const Share = ({ appId }: { appId: string }) => { + )} @@ -113,12 +115,13 @@ const Share = ({ appId }: { appId: string }) => { - + + )} @@ -267,7 +270,6 @@ function EditLinkModal({ }); const { mutate: onclickUpdate, isLoading: updating } = useRequest({ mutationFn: (e: OutLinkEditType) => { - console.log(e); return putShareChat(e); }, errorToast: '更新链接异常', @@ -338,6 +340,26 @@ function EditLinkModal({ }} /> + + + {t('outlink.token auth')} + + + + + + + + {t('outlink.token auth use cases')} + )} diff --git a/projects/app/src/pages/chat/share.tsx b/projects/app/src/pages/chat/share.tsx index e6f196994..48d53d359 100644 --- a/projects/app/src/pages/chat/share.tsx +++ b/projects/app/src/pages/chat/share.tsx @@ -21,11 +21,20 @@ import ChatHeader from './components/ChatHeader'; import ChatHistorySlider from './components/ChatHistorySlider'; import { serviceSideProps } from '@/utils/web/i18n'; -const OutLink = ({ shareId, chatId }: { shareId: string; chatId: string }) => { +const OutLink = ({ + shareId, + chatId, + authToken +}: { + shareId: string; + chatId: string; + authToken?: string; +}) => { const router = useRouter(); const { toast } = useToast(); const { isOpen: isOpenSlider, onClose: onCloseSlider, onOpen: onOpenSlider } = useDisclosure(); const { isPc } = useGlobalStore(); + const forbidRefresh = useRef(false); const ChatBoxRef = useRef(null); @@ -53,7 +62,8 @@ const OutLink = ({ shareId, chatId }: { shareId: string; chatId: string }) => { messages: prompts, variables, shareId, - chatId: completionChatId + chatId: completionChatId, + authToken }, onMessage: generatingMessage, abortSignal: controller @@ -75,10 +85,12 @@ const OutLink = ({ shareId, chatId }: { shareId: string; chatId: string }) => { }); if (completionChatId !== chatId && controller.signal.reason !== 'leave') { + forbidRefresh.current = true; router.replace({ query: { shareId, - chatId: completionChatId + chatId: completionChatId, + authToken } }); } @@ -96,11 +108,11 @@ const OutLink = ({ shareId, chatId }: { shareId: string; chatId: string }) => { return { responseText, responseData }; }, - [chatId, router, saveChatResponse, shareId] + [authToken, chatId, router, saveChatResponse, shareId] ); const loadAppInfo = useCallback( - async (shareId: string, chatId: string) => { + async (shareId: string, chatId: string, authToken?: string) => { if (!shareId) return null; const history = shareChatHistory.find((item) => item.chatId === chatId) || defaultHistory; @@ -111,7 +123,8 @@ const OutLink = ({ shareId, chatId }: { shareId: string; chatId: string }) => { const chatData = await (async () => { if (shareChatData.app.name === '') { return initShareChatInfo({ - shareId + shareId, + authToken }); } return shareChatData; @@ -142,8 +155,12 @@ const OutLink = ({ shareId, chatId }: { shareId: string; chatId: string }) => { [delManyShareChatHistoryByShareId, setShareChatData, shareChatData, shareChatHistory, toast] ); - useQuery(['init', shareId, chatId], () => { - return loadAppInfo(shareId, chatId); + useQuery(['init', shareId, chatId, authToken], () => { + if (forbidRefresh.current) { + forbidRefresh.current = false; + return null; + } + return loadAppInfo(shareId, chatId, authToken); }); return ( @@ -185,7 +202,8 @@ const OutLink = ({ shareId, chatId }: { shareId: string; chatId: string }) => { router.replace({ query: { chatId: chatId || '', - shareId + shareId, + authToken } }); if (!isPc) { @@ -197,7 +215,8 @@ const OutLink = ({ shareId, chatId }: { shareId: string; chatId: string }) => { delManyShareChatHistoryByShareId(shareId); router.replace({ query: { - shareId + shareId, + authToken } }); }} @@ -222,6 +241,7 @@ const OutLink = ({ shareId, chatId }: { shareId: string; chatId: string }) => { {/* chat box */} { export async function getServerSideProps(context: any) { const shareId = context?.query?.shareId || ''; const chatId = context?.query?.chatId || ''; + const authToken = context?.query?.authToken || ''; return { - props: { shareId, chatId, ...(await serviceSideProps(context)) } + props: { shareId, chatId, authToken, ...(await serviceSideProps(context)) } }; } diff --git a/projects/app/src/service/support/outLink/auth.ts b/projects/app/src/service/support/outLink/auth.ts index 2221a9f0d..0601acb74 100644 --- a/projects/app/src/service/support/outLink/auth.ts +++ b/projects/app/src/service/support/outLink/auth.ts @@ -3,8 +3,18 @@ import { IpLimit } from '@/service/common/ipLimit/schema'; import { authBalanceByUid, AuthUserTypeEnum } from '@/service/utils/auth'; import { OutLinkSchema } from '@/types/support/outLink'; import { OutLink } from './schema'; +import axios from 'axios'; -export async function authOutLinkChat({ shareId, ip }: { shareId: string; ip?: string | null }) { +type AuthLinkProps = { ip?: string | null; authToken?: string; question: string }; + +export async function authOutLinkChat({ + shareId, + ip, + authToken, + question +}: AuthLinkProps & { + shareId: string; +}) { // get outLink const outLink = await OutLink.findOne({ shareId @@ -18,7 +28,7 @@ export async function authOutLinkChat({ shareId, ip }: { shareId: string; ip?: s const [user] = await Promise.all([ authBalanceByUid(uid), // authBalance - ...(global.feConfigs?.isPlus ? [authOutLinkLimit({ outLink, ip })] : []) // limit auth + ...(global.feConfigs?.isPlus ? [authOutLinkLimit({ outLink, ip, authToken, question })] : []) // limit auth ]); return { @@ -32,10 +42,11 @@ export async function authOutLinkChat({ shareId, ip }: { shareId: string; ip?: s export async function authOutLinkLimit({ outLink, - ip -}: { + ip, + authToken, + question +}: AuthLinkProps & { outLink: OutLinkSchema; - ip?: string | null; }) { if (!ip || !outLink.limit) { return; @@ -49,30 +60,97 @@ export async function authOutLinkLimit({ 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 - }); + // ip limit + await (async () => { + if (!outLink.limit) { 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) {} + try { + const ipLimit = await IpLimit.findOne({ ip, eventId: outLink._id }); + + // first request + if (!ipLimit) { + return await IpLimit.create({ + eventId: outLink._id, + ip, + account: outLink.limit.QPM - 1 + }); + } + + // 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(); + } + + // over limit + if (ipLimit.account <= 0) { + return Promise.reject( + `每分钟仅能请求 ${outLink.limit.QPM} 次, ${60 - Math.round(diffTime / 1000)}s 后重试~` + ); + } + + // update limit + ipLimit.account = ipLimit.account - 1; + await ipLimit.save(); + } catch (error) {} + })(); + + // url auth. send request + await authShareStart({ authToken, tokenUrl: outLink.limit.hookUrl, question }); } + +type TokenAuthResponseType = { + success: boolean; + message?: string; +}; + +export const authShareChatInit = async (authToken?: string, tokenUrl?: string) => { + if (!tokenUrl || !global.feConfigs?.isPlus) return; + try { + const { data } = await axios({ + baseURL: tokenUrl, + url: '/shareAuth/init', + method: 'POST', + data: { + token: authToken + } + }); + if (data?.success !== true) { + return Promise.reject(data?.message || '身份校验失败'); + } + } catch (error) { + return Promise.reject('身份校验失败'); + } +}; + +export const authShareStart = async ({ + tokenUrl, + authToken, + question +}: { + authToken?: string; + question: string; + tokenUrl?: string; +}) => { + if (!tokenUrl || !global.feConfigs?.isPlus) return; + try { + const { data } = await axios({ + baseURL: tokenUrl, + url: '/shareAuth/start', + method: 'POST', + data: { + token: authToken, + question + } + }); + + if (data?.success !== true) { + return Promise.reject(data?.message || '身份校验失败'); + } + } catch (error) { + return Promise.reject('身份校验失败'); + } +}; diff --git a/projects/app/src/service/support/outLink/index.ts b/projects/app/src/service/support/outLink/index.ts index d81d265ae..23667c99c 100644 --- a/projects/app/src/service/support/outLink/index.ts +++ b/projects/app/src/service/support/outLink/index.ts @@ -1,4 +1,6 @@ import { addLog } from '@/service/utils/tools'; +import { ChatHistoryItemResType } from '@/types/chat'; +import axios from 'axios'; import { OutLink } from './schema'; export const updateOutLinkUsage = async ({ @@ -20,3 +22,31 @@ export const updateOutLinkUsage = async ({ addLog.error('update shareChat error', err); } }; + +export const pushResult2Remote = async ({ + authToken, + shareId, + responseData +}: { + authToken?: string; + shareId?: string; + responseData?: ChatHistoryItemResType[]; +}) => { + if (!shareId || !authToken) return; + try { + const outLink = await OutLink.findOne({ + shareId + }); + if (!outLink?.limit?.hookUrl) return; + + axios({ + method: 'post', + baseURL: outLink.limit.hookUrl, + url: '/shareAuth/finish', + data: { + token: authToken, + responseData + } + }); + } catch (error) {} +}; diff --git a/projects/app/src/service/support/outLink/schema.ts b/projects/app/src/service/support/outLink/schema.ts index ce8f8837a..a43781d06 100644 --- a/projects/app/src/service/support/outLink/schema.ts +++ b/projects/app/src/service/support/outLink/schema.ts @@ -48,6 +48,9 @@ const OutLinkSchema = new Schema({ credit: { type: Number, default: -1 + }, + hookUrl: { + type: String } } }); diff --git a/projects/app/src/service/utils/auth.ts b/projects/app/src/service/utils/auth.ts index 5ad52a320..257942bb6 100644 --- a/projects/app/src/service/utils/auth.ts +++ b/projects/app/src/service/utils/auth.ts @@ -12,18 +12,6 @@ export enum AuthUserTypeEnum { apikey = 'apikey' } -export const authCookieToken = async (cookie?: string, token?: string): Promise => { - // 获取 cookie - const cookies = Cookie.parse(cookie || ''); - const cookieToken = cookies.token || token; - - if (!cookieToken) { - return Promise.reject(ERROR_ENUM.unAuthorization); - } - - return await authJWT(cookieToken); -}; - /* auth balance */ export const authBalanceByUid = async (uid: string) => { const user = await User.findById( @@ -45,13 +33,27 @@ export const authUser = async ({ req, authToken = false, authRoot = false, + authApiKey = false, authBalance = false }: { req: NextApiRequest; authToken?: boolean; authRoot?: boolean; + authApiKey?: boolean; authBalance?: boolean; }) => { + const authCookieToken = async (cookie?: string, token?: string): Promise => { + // 获取 cookie + const cookies = Cookie.parse(cookie || ''); + const cookieToken = cookies.token || token; + + if (!cookieToken) { + return Promise.reject(ERROR_ENUM.unAuthorization); + } + + return await authJWT(cookieToken); + }; + // from authorization get apikey const parseAuthorization = async (authorization?: string) => { if (!authorization) { return Promise.reject(ERROR_ENUM.unAuthorization); @@ -89,6 +91,7 @@ export const authUser = async ({ appId: apiKeyAppId || authorizationAppid }; }; + // root user const parseRootKey = async (rootKey?: string, userId = '') => { if (!rootKey || !process.env.ROOT_KEY || rootKey !== process.env.ROOT_KEY) { return Promise.reject(ERROR_ENUM.unAuthorization); @@ -110,30 +113,31 @@ export const authUser = async ({ let openApiKey = apikey; let authType: `${AuthUserTypeEnum}` = AuthUserTypeEnum.token; - if (authToken) { + if (authToken && (cookie || token)) { + // user token(from fastgpt web) uid = await authCookieToken(cookie, token); authType = AuthUserTypeEnum.token; - } else if (authRoot) { + } else if (authRoot && rootkey) { + // root user uid = await parseRootKey(rootkey, userid); authType = AuthUserTypeEnum.root; - } else if (cookie || token) { - uid = await authCookieToken(cookie, token); - authType = AuthUserTypeEnum.token; - } else if (apikey) { + } else if (authApiKey && apikey) { + // apikey const parseResult = await authOpenApiKey({ apikey }); uid = parseResult.userId; authType = AuthUserTypeEnum.apikey; openApiKey = parseResult.apikey; - } else if (authorization) { + } else if (authApiKey && authorization) { + // apikey from authorization const authResponse = await parseAuthorization(authorization); uid = authResponse.uid; appId = authResponse.appId; openApiKey = authResponse.apikey; authType = AuthUserTypeEnum.apikey; - } else if (rootkey) { - uid = await parseRootKey(rootkey, userid); - authType = AuthUserTypeEnum.root; - } else { + } + + // not rootUser and no uid, reject request + if (!rootkey && !uid) { return Promise.reject(ERROR_ENUM.unAuthorization); } @@ -158,14 +162,12 @@ export const authApp = async ({ appId, userId, authUser = true, - authOwner = true, - reserveDetail = false + authOwner = true }: { appId: string; userId: string; authUser?: boolean; authOwner?: boolean; - reserveDetail?: boolean; // focus reserve detail }) => { // 获取 app 数据 const app = await App.findById(appId); diff --git a/projects/app/src/types/core/dataset/data.d.ts b/projects/app/src/types/core/dataset/data.d.ts index 7a36236a2..e8a9e2f6c 100644 --- a/projects/app/src/types/core/dataset/data.d.ts +++ b/projects/app/src/types/core/dataset/data.d.ts @@ -4,6 +4,7 @@ export type DatasetDataItemType = { source?: string; file_id?: string; }; + export type PgDataItemType = DatasetItemType & { id: string; }; diff --git a/projects/app/src/types/support/outLink.d.ts b/projects/app/src/types/support/outLink.d.ts index 4617da531..a81360395 100644 --- a/projects/app/src/types/support/outLink.d.ts +++ b/projects/app/src/types/support/outLink.d.ts @@ -14,6 +14,7 @@ export interface OutLinkSchema { expiredTime?: Date; QPM: number; credit: number; + hookUrl?: string; }; }
名称金额限制(¥) IP限流(人/分钟) 过期时间token校验最后使用时间 {item.limit && item.limit.credit > -1 ? `${item.limit.credit}元` : '无限制'} {item.limit?.QPM || '-'}{item?.limit?.QPM || '-'} - {item.limit?.expiredTime + {item?.limit?.expiredTime ? dayjs(item.limit?.expiredTime).format('YYYY/MM/DD\nHH:mm') : '-'} {item?.limit?.hookUrl ? '✔' : '✖'}{item.lastTime ? formatTimeToChatTime(item.lastTime) : '未使用'}