Add share link hook (#351)

This commit is contained in:
Archer 2023-09-25 23:12:42 +08:00 committed by GitHub
parent 9136c9306a
commit 63cd379682
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
29 changed files with 430 additions and 98 deletions

Binary file not shown.

After

Width:  |  Height:  |  Size: 161 KiB

View File

@ -38,7 +38,7 @@ FastGPT 的 API Key 有 2 类,一类是全局通用的 key一类是携带
- headers.Authorization: Bearer apikey - headers.Authorization: Bearer apikey
- chatId: string | undefined 。 - chatId: string | undefined 。
- 为 undefined 时(不传入),不使用 FastGpt 提供的上下文功能,完全通过传入的 messages 构建上下文。 不会将你的记录存储到数据库中,你也无法在记录汇总中查阅到。 - 为 undefined 时(不传入),不使用 FastGpt 提供的上下文功能,完全通过传入的 messages 构建上下文。 不会将你的记录存储到数据库中,你也无法在记录汇总中查阅到。
- 为非空字符串时,意味着使用 chatId 进行对话,自动从 FastGpt 数据库取历史记录。并拼接 messages 数组最后一个内容作为完整请求。(自行确保 chatid 唯一,长度不限) - 为非空字符串时,意味着使用 chatId 进行对话,自动从 FastGpt 数据库取历史记录。并拼接 messages 数组最后一个内容作为完整请求。(自行确保 chatId 唯一,长度不限)
- messages: 与 openai gpt 接口完全一致。 - messages: 与 openai gpt 接口完全一致。
- detail: 是否返回详细值(模块状态响应的完整结果会通过event进行区分 - detail: 是否返回详细值(模块状态响应的完整结果会通过event进行区分
- variables: 变量。一个对象,效果同全局变量。 - variables: 变量。一个对象,效果同全局变量。
@ -227,7 +227,7 @@ data: [{"moduleName":"KB Search","price":1.2000000000000002,"model":"Embedding-2
![](/imgs/getKbId.png) ![](/imgs/getKbId.png)
### 知识库添加数据 ### 知识库添加数据
{{< tabs tabTotal="4" >}} {{< tabs tabTotal="4" >}}
{{< tab tabName="请求示例" >}} {{< tab tabName="请求示例" >}}
@ -241,10 +241,11 @@ curl --location --request POST 'https://fastgpt.run/api/core/dataset/data/pushDa
    "kbId": "64663f451ba1676dbdef0499",     "kbId": "64663f451ba1676dbdef0499",
"mode": "index", "mode": "index",
"prompt": "qa 拆分引导词index 模式下可以忽略", "prompt": "qa 拆分引导词index 模式下可以忽略",
"billId": "可选。如果有这个值,本次的数据会被聚合到一个订单中,这个值可以重复使用。可以参考 [创建训练订单] 获取该值。",
    "data": [     "data": [
        {         {
            "a": "test",             "a": "test",
            "q": "1111"             "q": "1111",
        },         },
        {         {
            "a": "test2",             "a": "test2",
@ -370,6 +371,158 @@ curl --location --request POST 'https://fastgpt.run/api/core/dataset/searchTest'
{{< /tabs >}} {{< /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`
**此接口无需响应值**
# 使用案例 # 使用案例

View File

@ -248,7 +248,10 @@
"QPM Tips": "The maximum number of queries per IP address per minute", "QPM Tips": "The maximum number of queries per IP address per minute",
"QPM is empty": "QPM is empty", "QPM is empty": "QPM is empty",
"Response Detail": "Quote", "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": { "system": {
"Help Document": "Document" "Help Document": "Document"

View File

@ -248,7 +248,10 @@
"QPM Tips": "每个 IP 每分钟最多提问多少次", "QPM Tips": "每个 IP 每分钟最多提问多少次",
"QPM is empty": "QPM 不能为空", "QPM is empty": "QPM 不能为空",
"Response Detail": "返回详情", "Response Detail": "返回详情",
"Response Detail tips": "是否需要返回详情(引用内容,调用时间等,不会返回预设提示词和完整上下文)" "Response Detail tips": "是否需要返回详情(引用内容,调用时间等,不会返回预设提示词和完整上下文)",
"token auth": "身份验证",
"token auth Tips": "身份校验服务器地址,如填写该值,每次对话前都会想指定服务器发送一个请求,进行身份校验",
"token auth use cases": "查看身份验证使用说明"
}, },
"system": { "system": {
"Help Document": "帮助文档" "Help Document": "帮助文档"

View File

@ -1,10 +1,11 @@
import { KbTypeEnum } from '@/constants/dataset'; import { KbTypeEnum } from '@/constants/dataset';
import type { RequestPaging } from '@/types'; import type { RequestPaging } from '@/types';
import { TrainingModeEnum } from '@/constants/plugin'; import { TrainingModeEnum } from '@/constants/plugin';
import { DatasetDataItemType } from '@/types/core/dataset/data';
export type PushDataProps = { export type PushDataProps = {
kbId: string; kbId: string;
data: DatasetItemType[]; data: DatasetDataItemType[];
mode: `${TrainingModeEnum}`; mode: `${TrainingModeEnum}`;
prompt?: string; prompt?: string;
billId?: string; billId?: string;

View File

@ -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<InitShareChatResponse>(`/support/outLink/init`, data); GET<InitShareChatResponse>(`/support/outLink/init`, data);
/** /**

View File

@ -139,6 +139,7 @@ const ChatBox = (
userAvatar, userAvatar,
variableModules, variableModules,
welcomeText, welcomeText,
active = true,
onUpdateVariable, onUpdateVariable,
onStartChat, onStartChat,
onDelMessage onDelMessage
@ -152,6 +153,7 @@ const ChatBox = (
userAvatar?: string; userAvatar?: string;
variableModules?: VariableItemType[]; variableModules?: VariableItemType[];
welcomeText?: string; welcomeText?: string;
active?: boolean;
onUpdateVariable?: (e: Record<string, any>) => void; onUpdateVariable?: (e: Record<string, any>) => void;
onStartChat?: (e: StartChatFnProps) => Promise<{ onStartChat?: (e: StartChatFnProps) => Promise<{
responseText: string; responseText: string;
@ -860,7 +862,7 @@ const ChatBox = (
</Box> </Box>
</Box> </Box>
{/* input */} {/* input */}
{onStartChat && variableIsFinish ? ( {onStartChat && variableIsFinish && active ? (
<Box m={['0 auto', '10px auto']} w={'100%'} maxW={['auto', 'min(750px, 100%)']} px={[0, 5]}> <Box m={['0 auto', '10px auto']} w={'100%'} maxW={['auto', 'min(750px, 100%)']} px={[0, 5]}>
<Box <Box
py={'18px'} py={'18px'}

View File

@ -40,7 +40,7 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
await connectToDatabase(); await connectToDatabase();
/* user auth */ /* user auth */
const { userId, user } = await authUser({ req, authBalance: true }); const { userId, user } = await authUser({ req, authToken: true, authBalance: true });
if (!user) { if (!user) {
throw new Error('user not found'); throw new Error('user not found');

View File

@ -9,7 +9,7 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
try { try {
const { name } = req.body as CreateTrainingBillType; const { name } = req.body as CreateTrainingBillType;
const { userId } = await authUser({ req, authToken: true }); const { userId } = await authUser({ req, authToken: true, authApiKey: true });
await connectToDatabase(); await connectToDatabase();

View File

@ -16,7 +16,7 @@ export default withNextCors(async function handler(req: NextApiRequest, res: Nex
} }
// 凭证校验 // 凭证校验
const { userId } = await authUser({ req }); const { userId } = await authUser({ req, authToken: true });
await PgClient.delete(PgDatasetTableName, { await PgClient.delete(PgDatasetTableName, {
where: [['user_id', userId], 'AND', ['id', dataId]] where: [['user_id', userId], 'AND', ['id', dataId]]

View File

@ -21,7 +21,7 @@ export default withNextCors(async function handler(req: NextApiRequest, res: Nex
await connectToDatabase(); await connectToDatabase();
// 凭证校验 // 凭证校验
const { userId } = await authUser({ req }); const { userId } = await authUser({ req, authToken: true });
jsonRes(res, { jsonRes(res, {
data: await getVectorAndInsertDataset({ data: await getVectorAndInsertDataset({

View File

@ -36,7 +36,7 @@ export default withNextCors(async function handler(req: NextApiRequest, res: Nex
await connectToDatabase(); await connectToDatabase();
// 凭证校验 // 凭证校验
const { userId } = await authUser({ req }); const { userId } = await authUser({ req, authToken: true, authApiKey: true });
jsonRes<PushDataResponse>(res, { jsonRes<PushDataResponse>(res, {
data: await pushDataToKb({ data: await pushDataToKb({

View File

@ -20,7 +20,7 @@ export default withNextCors(async function handler(req: NextApiRequest, res: Nex
// auth user and get kb // auth user and get kb
const [{ userId }, kb] = await Promise.all([ const [{ userId }, kb] = await Promise.all([
authUser({ req }), authUser({ req, authToken: true }),
KB.findById(kbId, 'vectorModel') KB.findById(kbId, 'vectorModel')
]); ]);

View File

@ -18,7 +18,7 @@ export default withNextCors(async function handler(req: NextApiRequest, res: Nex
// 凭证校验 // 凭证校验
const [{ userId }, kb] = await Promise.all([ const [{ userId }, kb] = await Promise.all([
authUser({ req }), authUser({ req, authToken: true, authApiKey: true }),
KB.findById(kbId, 'vectorModel') KB.findById(kbId, 'vectorModel')
]); ]);

View File

@ -17,7 +17,7 @@ type Response = {
export default withNextCors(async function handler(req: NextApiRequest, res: NextApiResponse<any>) { export default withNextCors(async function handler(req: NextApiRequest, res: NextApiResponse<any>) {
try { try {
const { userId } = await authUser({ req }); const { userId } = await authUser({ req, authToken: true });
let { input, model } = req.query as Props; let { input, model } = req.query as Props;
if (!Array.isArray(input)) { if (!Array.isArray(input)) {

View File

@ -34,7 +34,7 @@ import requestIp from 'request-ip';
import { replaceVariable } from '@/utils/common/tools/text'; import { replaceVariable } from '@/utils/common/tools/text';
import { ModuleDispatchProps } from '@/types/core/modules'; import { ModuleDispatchProps } from '@/types/core/modules';
import { selectShareResponse } from '@/utils/service/core/chat'; 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'; import { updateApiKeyUsage } from '@/service/support/openapi';
export type MessageItemType = ChatCompletionRequestMessage & { dataId?: string }; export type MessageItemType = ChatCompletionRequestMessage & { dataId?: string };
@ -44,6 +44,7 @@ type FastGptWebChatProps = {
}; };
type FastGptShareChatProps = { type FastGptShareChatProps = {
shareId?: string; shareId?: string;
authToken?: string;
}; };
export type Props = CreateChatCompletionRequest & export type Props = CreateChatCompletionRequest &
FastGptWebChatProps & FastGptWebChatProps &
@ -71,6 +72,7 @@ export default withNextCors(async function handler(req: NextApiRequest, res: Nex
chatId, chatId,
appId, appId,
shareId, shareId,
authToken,
stream = false, stream = false,
detail = false, detail = false,
messages = [], messages = [],
@ -111,10 +113,15 @@ export default withNextCors(async function handler(req: NextApiRequest, res: Nex
if (shareId) { if (shareId) {
return authOutLinkChat({ return authOutLinkChat({
shareId, 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) { if (!user) {
@ -260,11 +267,13 @@ export default withNextCors(async function handler(req: NextApiRequest, res: Nex
response: responseData response: responseData
}); });
!!shareId && if (shareId) {
pushResult2Remote({ authToken, shareId, responseData });
updateOutLinkUsage({ updateOutLinkUsage({
shareId, shareId,
total total
}); });
}
!!apikey && !!apikey &&
updateApiKeyUsage({ updateApiKeyUsage({
apikey, apikey,

View File

@ -16,7 +16,7 @@ export type Response = { history: ChatItemType[] };
export default async function handler(req: NextApiRequest, res: NextApiResponse) { export default async function handler(req: NextApiRequest, res: NextApiResponse) {
try { try {
await connectToDatabase(); await connectToDatabase();
const { userId } = await authUser({ req }); const { userId } = await authUser({ req, authToken: true });
const { chatId, limit } = req.body as Props; const { chatId, limit } = req.body as Props;
jsonRes<Response>(res, { jsonRes<Response>(res, {

View File

@ -18,7 +18,7 @@ const fetchContent = async (req: NextApiRequest, res: NextApiResponse) => {
throw new Error('urlList is empty'); throw new Error('urlList is empty');
} }
await authUser({ req }); await authUser({ req, authToken: true });
urlList = urlList.filter((url) => /^(http|https):\/\/[^ "]+$/.test(url)); urlList = urlList.filter((url) => /^(http|https):\/\/[^ "]+$/.test(url));

View File

@ -14,7 +14,7 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse<
throw new Error('fileId is empty'); throw new Error('fileId is empty');
} }
const { userId } = await authUser({ req }); const { userId } = await authUser({ req, authToken: true });
const gridFs = new GridFSStorage('dataset', userId); const gridFs = new GridFSStorage('dataset', userId);

View File

@ -16,7 +16,7 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse<
throw new Error('fileId is empty'); throw new Error('fileId is empty');
} }
const { userId } = await authUser({ req }); const { userId } = await authUser({ req, authToken: true });
// auth file // auth file
const gridFs = new GridFSStorage('dataset', userId); const gridFs = new GridFSStorage('dataset', userId);

View File

@ -5,12 +5,14 @@ import type { InitShareChatResponse } from '@/api/response/chat';
import { authApp } from '@/service/utils/auth'; 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';
import { authShareChatInit } from '@/service/support/outLink/auth';
/* init share chat window */ /* 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, authToken } = req.query as {
shareId: string; shareId: string;
authToken?: string;
}; };
if (!shareId) { if (!shareId) {
@ -36,7 +38,8 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
userId: String(shareChat.userId), userId: String(shareChat.userId),
authOwner: false authOwner: false
}), }),
User.findById(shareChat.userId, 'avatar') User.findById(shareChat.userId, 'avatar'),
authShareChatInit(authToken, shareChat.limit?.hookUrl)
]); ]);
jsonRes<InitShareChatResponse>(res, { jsonRes<InitShareChatResponse>(res, {

View File

@ -17,7 +17,8 @@ import {
Menu, Menu,
MenuButton, MenuButton,
MenuList, MenuList,
MenuItem MenuItem,
Link
} from '@chakra-ui/react'; } from '@chakra-ui/react';
import { QuestionOutlineIcon } from '@chakra-ui/icons'; import { QuestionOutlineIcon } from '@chakra-ui/icons';
import MyIcon from '@/components/Icon'; import MyIcon from '@/components/Icon';
@ -58,7 +59,7 @@ const Share = ({ appId }: { appId: string }) => {
} = useQuery(['initShareChatList', appId], () => getShareChatList(appId)); } = useQuery(['initShareChatList', appId], () => getShareChatList(appId));
return ( return (
<Box position={'relative'} pt={[3, 5, 8]} px={[5, 8]} minH={'50vh'}> <Box position={'relative'} pt={[3, 5, 8]} px={[2, 8]} minH={'50vh'}>
<Flex justifyContent={'space-between'}> <Flex justifyContent={'space-between'}>
<Box fontWeight={'bold'}> <Box fontWeight={'bold'}>
@ -85,7 +86,7 @@ const Share = ({ appId }: { appId: string }) => {
</Button> </Button>
</Flex> </Flex>
<TableContainer mt={3}> <TableContainer mt={3}>
<Table variant={'simple'} w={'100%'} overflowX={'auto'}> <Table variant={'simple'} w={'100%'} overflowX={'auto'} fontSize={'sm'}>
<Thead> <Thead>
<Tr> <Tr>
<Th></Th> <Th></Th>
@ -96,6 +97,7 @@ const Share = ({ appId }: { appId: string }) => {
<Th>()</Th> <Th>()</Th>
<Th>IP限流/</Th> <Th>IP限流/</Th>
<Th></Th> <Th></Th>
<Th>token校验</Th>
</> </>
)} )}
<Th>使</Th> <Th>使</Th>
@ -113,12 +115,13 @@ const Share = ({ appId }: { appId: string }) => {
<Td> <Td>
{item.limit && item.limit.credit > -1 ? `${item.limit.credit}` : '无限制'} {item.limit && item.limit.credit > -1 ? `${item.limit.credit}` : '无限制'}
</Td> </Td>
<Td>{item.limit?.QPM || '-'}</Td> <Td>{item?.limit?.QPM || '-'}</Td>
<Td> <Td>
{item.limit?.expiredTime {item?.limit?.expiredTime
? dayjs(item.limit?.expiredTime).format('YYYY/MM/DD\nHH:mm') ? dayjs(item.limit?.expiredTime).format('YYYY/MM/DD\nHH:mm')
: '-'} : '-'}
</Td> </Td>
<Th>{item?.limit?.hookUrl ? '✔' : '✖'}</Th>
</> </>
)} )}
<Td>{item.lastTime ? formatTimeToChatTime(item.lastTime) : '未使用'}</Td> <Td>{item.lastTime ? formatTimeToChatTime(item.lastTime) : '未使用'}</Td>
@ -267,7 +270,6 @@ function EditLinkModal({
}); });
const { mutate: onclickUpdate, isLoading: updating } = useRequest({ const { mutate: onclickUpdate, isLoading: updating } = useRequest({
mutationFn: (e: OutLinkEditType) => { mutationFn: (e: OutLinkEditType) => {
console.log(e);
return putShareChat(e); return putShareChat(e);
}, },
errorToast: '更新链接异常', errorToast: '更新链接异常',
@ -338,6 +340,26 @@ function EditLinkModal({
}} }}
/> />
</Flex> </Flex>
<Flex alignItems={'center'} mt={4}>
<Flex flex={'0 0 90px'}>
{t('outlink.token auth')}
<MyTooltip label={t('outlink.token auth Tips') || ''}>
<QuestionOutlineIcon ml={1} />
</MyTooltip>
</Flex>
<Input
placeholder={t('outlink.token auth Tips') || ''}
{...register('limit.hookUrl')}
/>
</Flex>
<Link
href="https://doc.fastgpt.run/docs/development/openapi/#分享链接中增加额外-query"
target={'_blank'}
fontSize={'sm'}
color={'myGray.500'}
>
{t('outlink.token auth use cases')}
</Link>
</> </>
)} )}

View File

@ -21,11 +21,20 @@ import ChatHeader from './components/ChatHeader';
import ChatHistorySlider from './components/ChatHistorySlider'; import ChatHistorySlider from './components/ChatHistorySlider';
import { serviceSideProps } from '@/utils/web/i18n'; 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 router = useRouter();
const { toast } = useToast(); const { toast } = useToast();
const { isOpen: isOpenSlider, onClose: onCloseSlider, onOpen: onOpenSlider } = useDisclosure(); const { isOpen: isOpenSlider, onClose: onCloseSlider, onOpen: onOpenSlider } = useDisclosure();
const { isPc } = useGlobalStore(); const { isPc } = useGlobalStore();
const forbidRefresh = useRef(false);
const ChatBoxRef = useRef<ComponentRef>(null); const ChatBoxRef = useRef<ComponentRef>(null);
@ -53,7 +62,8 @@ const OutLink = ({ shareId, chatId }: { shareId: string; chatId: string }) => {
messages: prompts, messages: prompts,
variables, variables,
shareId, shareId,
chatId: completionChatId chatId: completionChatId,
authToken
}, },
onMessage: generatingMessage, onMessage: generatingMessage,
abortSignal: controller abortSignal: controller
@ -75,10 +85,12 @@ const OutLink = ({ shareId, chatId }: { shareId: string; chatId: string }) => {
}); });
if (completionChatId !== chatId && controller.signal.reason !== 'leave') { if (completionChatId !== chatId && controller.signal.reason !== 'leave') {
forbidRefresh.current = true;
router.replace({ router.replace({
query: { query: {
shareId, shareId,
chatId: completionChatId chatId: completionChatId,
authToken
} }
}); });
} }
@ -96,11 +108,11 @@ const OutLink = ({ shareId, chatId }: { shareId: string; chatId: string }) => {
return { responseText, responseData }; return { responseText, responseData };
}, },
[chatId, router, saveChatResponse, shareId] [authToken, chatId, router, saveChatResponse, shareId]
); );
const loadAppInfo = useCallback( const loadAppInfo = useCallback(
async (shareId: string, chatId: string) => { async (shareId: string, chatId: string, authToken?: string) => {
if (!shareId) return null; if (!shareId) return null;
const history = shareChatHistory.find((item) => item.chatId === chatId) || defaultHistory; 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 () => { const chatData = await (async () => {
if (shareChatData.app.name === '') { if (shareChatData.app.name === '') {
return initShareChatInfo({ return initShareChatInfo({
shareId shareId,
authToken
}); });
} }
return shareChatData; return shareChatData;
@ -142,8 +155,12 @@ const OutLink = ({ shareId, chatId }: { shareId: string; chatId: string }) => {
[delManyShareChatHistoryByShareId, setShareChatData, shareChatData, shareChatHistory, toast] [delManyShareChatHistoryByShareId, setShareChatData, shareChatData, shareChatHistory, toast]
); );
useQuery(['init', shareId, chatId], () => { useQuery(['init', shareId, chatId, authToken], () => {
return loadAppInfo(shareId, chatId); if (forbidRefresh.current) {
forbidRefresh.current = false;
return null;
}
return loadAppInfo(shareId, chatId, authToken);
}); });
return ( return (
@ -185,7 +202,8 @@ const OutLink = ({ shareId, chatId }: { shareId: string; chatId: string }) => {
router.replace({ router.replace({
query: { query: {
chatId: chatId || '', chatId: chatId || '',
shareId shareId,
authToken
} }
}); });
if (!isPc) { if (!isPc) {
@ -197,7 +215,8 @@ const OutLink = ({ shareId, chatId }: { shareId: string; chatId: string }) => {
delManyShareChatHistoryByShareId(shareId); delManyShareChatHistoryByShareId(shareId);
router.replace({ router.replace({
query: { query: {
shareId shareId,
authToken
} }
}); });
}} }}
@ -222,6 +241,7 @@ const OutLink = ({ shareId, chatId }: { shareId: string; chatId: string }) => {
{/* chat box */} {/* chat box */}
<Box flex={1}> <Box flex={1}>
<ChatBox <ChatBox
active={!!shareChatData.app.name}
ref={ChatBoxRef} ref={ChatBoxRef}
appAvatar={shareChatData.app.avatar} appAvatar={shareChatData.app.avatar}
userAvatar={shareChatData.userAvatar} userAvatar={shareChatData.userAvatar}
@ -252,9 +272,10 @@ const OutLink = ({ shareId, chatId }: { shareId: string; chatId: string }) => {
export async function getServerSideProps(context: any) { export async function getServerSideProps(context: any) {
const shareId = context?.query?.shareId || ''; const shareId = context?.query?.shareId || '';
const chatId = context?.query?.chatId || ''; const chatId = context?.query?.chatId || '';
const authToken = context?.query?.authToken || '';
return { return {
props: { shareId, chatId, ...(await serviceSideProps(context)) } props: { shareId, chatId, authToken, ...(await serviceSideProps(context)) }
}; };
} }

View File

@ -3,8 +3,18 @@ import { IpLimit } from '@/service/common/ipLimit/schema';
import { authBalanceByUid, AuthUserTypeEnum } from '@/service/utils/auth'; import { authBalanceByUid, AuthUserTypeEnum } from '@/service/utils/auth';
import { OutLinkSchema } from '@/types/support/outLink'; import { OutLinkSchema } from '@/types/support/outLink';
import { OutLink } from './schema'; 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 // get outLink
const outLink = await OutLink.findOne({ const outLink = await OutLink.findOne({
shareId shareId
@ -18,7 +28,7 @@ export async function authOutLinkChat({ shareId, ip }: { shareId: string; ip?: s
const [user] = await Promise.all([ const [user] = await Promise.all([
authBalanceByUid(uid), // authBalance authBalanceByUid(uid), // authBalance
...(global.feConfigs?.isPlus ? [authOutLinkLimit({ outLink, ip })] : []) // limit auth ...(global.feConfigs?.isPlus ? [authOutLinkLimit({ outLink, ip, authToken, question })] : []) // limit auth
]); ]);
return { return {
@ -32,10 +42,11 @@ export async function authOutLinkChat({ shareId, ip }: { shareId: string; ip?: s
export async function authOutLinkLimit({ export async function authOutLinkLimit({
outLink, outLink,
ip ip,
}: { authToken,
question
}: AuthLinkProps & {
outLink: OutLinkSchema; outLink: OutLinkSchema;
ip?: string | null;
}) { }) {
if (!ip || !outLink.limit) { if (!ip || !outLink.limit) {
return; return;
@ -49,30 +60,97 @@ export async function authOutLinkLimit({
return Promise.reject('链接超出使用限制'); return Promise.reject('链接超出使用限制');
} }
const ipLimit = await IpLimit.findOne({ ip, eventId: outLink._id }); // ip limit
await (async () => {
try { if (!outLink.limit) {
if (!ipLimit) {
await IpLimit.create({
eventId: outLink._id,
ip,
account: outLink.limit.QPM - 1
});
return; return;
} }
// over one minute try {
const diffTime = Date.now() - ipLimit.lastMinute.getTime(); const ipLimit = await IpLimit.findOne({ ip, eventId: outLink._id });
if (diffTime >= 60 * 1000) {
ipLimit.account = outLink.limit.QPM - 1; // first request
ipLimit.lastMinute = new Date(); if (!ipLimit) {
return await ipLimit.save(); return await IpLimit.create({
} eventId: outLink._id,
if (ipLimit.account <= 0) { ip,
return Promise.reject( account: outLink.limit.QPM - 1
`每分钟仅能请求 ${outLink.limit.QPM} 次, ${60 - Math.round(diffTime / 1000)}s 后重试~` });
); }
}
ipLimit.account = ipLimit.account - 1; // over one minute
await ipLimit.save(); const diffTime = Date.now() - ipLimit.lastMinute.getTime();
} catch (error) {} 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<TokenAuthResponseType>({
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<TokenAuthResponseType>({
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('身份校验失败');
}
};

View File

@ -1,4 +1,6 @@
import { addLog } from '@/service/utils/tools'; import { addLog } from '@/service/utils/tools';
import { ChatHistoryItemResType } from '@/types/chat';
import axios from 'axios';
import { OutLink } from './schema'; import { OutLink } from './schema';
export const updateOutLinkUsage = async ({ export const updateOutLinkUsage = async ({
@ -20,3 +22,31 @@ export const updateOutLinkUsage = async ({
addLog.error('update shareChat error', err); 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) {}
};

View File

@ -48,6 +48,9 @@ const OutLinkSchema = new Schema({
credit: { credit: {
type: Number, type: Number,
default: -1 default: -1
},
hookUrl: {
type: String
} }
} }
}); });

View File

@ -12,18 +12,6 @@ export enum AuthUserTypeEnum {
apikey = 'apikey' apikey = 'apikey'
} }
export const authCookieToken = async (cookie?: string, token?: string): Promise<string> => {
// 获取 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 */ /* auth balance */
export const authBalanceByUid = async (uid: string) => { export const authBalanceByUid = async (uid: string) => {
const user = await User.findById<UserModelSchema>( const user = await User.findById<UserModelSchema>(
@ -45,13 +33,27 @@ export const authUser = async ({
req, req,
authToken = false, authToken = false,
authRoot = false, authRoot = false,
authApiKey = false,
authBalance = false authBalance = false
}: { }: {
req: NextApiRequest; req: NextApiRequest;
authToken?: boolean; authToken?: boolean;
authRoot?: boolean; authRoot?: boolean;
authApiKey?: boolean;
authBalance?: boolean; authBalance?: boolean;
}) => { }) => {
const authCookieToken = async (cookie?: string, token?: string): Promise<string> => {
// 获取 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) => { const parseAuthorization = async (authorization?: string) => {
if (!authorization) { if (!authorization) {
return Promise.reject(ERROR_ENUM.unAuthorization); return Promise.reject(ERROR_ENUM.unAuthorization);
@ -89,6 +91,7 @@ export const authUser = async ({
appId: apiKeyAppId || authorizationAppid appId: apiKeyAppId || authorizationAppid
}; };
}; };
// root user
const parseRootKey = async (rootKey?: string, userId = '') => { const parseRootKey = async (rootKey?: string, userId = '') => {
if (!rootKey || !process.env.ROOT_KEY || rootKey !== process.env.ROOT_KEY) { if (!rootKey || !process.env.ROOT_KEY || rootKey !== process.env.ROOT_KEY) {
return Promise.reject(ERROR_ENUM.unAuthorization); return Promise.reject(ERROR_ENUM.unAuthorization);
@ -110,30 +113,31 @@ export const authUser = async ({
let openApiKey = apikey; let openApiKey = apikey;
let authType: `${AuthUserTypeEnum}` = AuthUserTypeEnum.token; let authType: `${AuthUserTypeEnum}` = AuthUserTypeEnum.token;
if (authToken) { if (authToken && (cookie || token)) {
// user token(from fastgpt web)
uid = await authCookieToken(cookie, token); uid = await authCookieToken(cookie, token);
authType = AuthUserTypeEnum.token; authType = AuthUserTypeEnum.token;
} else if (authRoot) { } else if (authRoot && rootkey) {
// root user
uid = await parseRootKey(rootkey, userid); uid = await parseRootKey(rootkey, userid);
authType = AuthUserTypeEnum.root; authType = AuthUserTypeEnum.root;
} else if (cookie || token) { } else if (authApiKey && apikey) {
uid = await authCookieToken(cookie, token); // apikey
authType = AuthUserTypeEnum.token;
} else if (apikey) {
const parseResult = await authOpenApiKey({ apikey }); const parseResult = await authOpenApiKey({ apikey });
uid = parseResult.userId; uid = parseResult.userId;
authType = AuthUserTypeEnum.apikey; authType = AuthUserTypeEnum.apikey;
openApiKey = parseResult.apikey; openApiKey = parseResult.apikey;
} else if (authorization) { } else if (authApiKey && authorization) {
// apikey from authorization
const authResponse = await parseAuthorization(authorization); const authResponse = await parseAuthorization(authorization);
uid = authResponse.uid; uid = authResponse.uid;
appId = authResponse.appId; appId = authResponse.appId;
openApiKey = authResponse.apikey; openApiKey = authResponse.apikey;
authType = AuthUserTypeEnum.apikey; authType = AuthUserTypeEnum.apikey;
} else if (rootkey) { }
uid = await parseRootKey(rootkey, userid);
authType = AuthUserTypeEnum.root; // not rootUser and no uid, reject request
} else { if (!rootkey && !uid) {
return Promise.reject(ERROR_ENUM.unAuthorization); return Promise.reject(ERROR_ENUM.unAuthorization);
} }
@ -158,14 +162,12 @@ export const authApp = async ({
appId, appId,
userId, userId,
authUser = true, authUser = true,
authOwner = true, authOwner = true
reserveDetail = false
}: { }: {
appId: string; appId: string;
userId: string; userId: string;
authUser?: boolean; authUser?: boolean;
authOwner?: boolean; authOwner?: boolean;
reserveDetail?: boolean; // focus reserve detail
}) => { }) => {
// 获取 app 数据 // 获取 app 数据
const app = await App.findById<AppSchema>(appId); const app = await App.findById<AppSchema>(appId);

View File

@ -4,6 +4,7 @@ export type DatasetDataItemType = {
source?: string; source?: string;
file_id?: string; file_id?: string;
}; };
export type PgDataItemType = DatasetItemType & { export type PgDataItemType = DatasetItemType & {
id: string; id: string;
}; };

View File

@ -14,6 +14,7 @@ export interface OutLinkSchema {
expiredTime?: Date; expiredTime?: Date;
QPM: number; QPM: number;
credit: number; credit: number;
hookUrl?: string;
}; };
} }