perf: api key (#342)

This commit is contained in:
Archer
2023-09-23 20:28:03 +08:00
committed by GitHub
parent 814c5b3d3c
commit a4ff5a3f73
59 changed files with 1030 additions and 677 deletions

View File

@@ -1,9 +1,6 @@
{
"FeConfig": {
"show_emptyChat": true,
"show_register": false,
"show_appStore": false,
"show_userDetail": false,
"show_contact": true,
"show_git": true,
"show_doc": true,

View File

@@ -1,6 +1,6 @@
{
"name": "fastgpt",
"version": "4.4.4",
"version": "4.4.5",
"private": false,
"scripts": {
"dev": "next dev",

View File

@@ -6,15 +6,7 @@
- **反馈问卷**: 如果你遇到任何使用问题或有期望的功能,可以[填写该问卷](https://www.wjx.cn/vm/rLIw1uD.aspx#)
- **问题文档**: [先看文档,再提问](https://kjqvjse66l.feishu.cn/docx/HtrgdT0pkonP4kxGx8qcu6XDnGh)
- [点击查看商业版文档](https://fael3z0zfze.feishu.cn/docx/F155dbirfo8vDDx2WgWc6extnwf)
**价格表**
| 计费项 | 价格: 元/ 1K tokens包含上下文|
| --- | --- |
| 知识库 - 索引 | 0.002 |
| FastAI4k - 对话 | 0.015 |
| FastAI16k - 对话 | 0.03 |
| FastAI-Plus - 对话 | 0.45 |
| 文件 QA 拆分 | 0.03 |
- [计费规则](https://doc.fastgpt.run/docs/pricing/)
**其他问题**
| 交流群 | 小助手 |

View File

@@ -1,9 +1,8 @@
### Fast GPT V4.4.4
### Fast GPT V4.4.5
1. 去除 - 限定词。目前旧应用仍生效9/25 后全面去除,请及时替换
2. 新增 - 引用模板/引用提示词设置,可以 DIY 引用内容的格式,从而更好的适配场景
3. 优化 - 更好的兼容无 system role 的模型。
4. 优化 - icon 和 JS 加载逻辑。
5. [使用文档](https://doc.fastgpt.run/docs/intro/)
6. [点击查看高级编排介绍文档](https://doc.fastgpt.run/docs/workflow)
7. [点击查看商业版](https://doc.fastgpt.run/docs/commercial/)
1. 优化 - Api Key 使用。增加别名、额度限制和过期时间。自带 appId无需额外连接
2. 去除 - 限定词。目前旧应用仍生效9/25 后全面去除,请及时替换
3. 新增 - 引用模板/引用提示词设置,可以 DIY 引用内容的格式,从而更好的适配场景。[参考文档](https://doc.fastgpt.run/docs/use-cases/prompt/)
4. [使用文档](https://doc.fastgpt.run/docs/intro/)
5. [点击查看高级编排介绍文档](https://doc.fastgpt.run/docs/workflow)
6. [点击查看商业版](https://doc.fastgpt.run/docs/commercial/)

View File

@@ -104,7 +104,6 @@
},
"common": {
"Add": "Add",
"Cancel": "Cancel",
"Collect": "Collect",
"Copy": "Copy",
"Copy Successful": "Copy Successful",
@@ -119,6 +118,8 @@
"Filed is repeat": "Filed is repeated",
"Filed is repeated": "",
"Input": "Input",
"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.",
"Name is empty": "Name is empty",
"Output": "Output",
"Password inconsistency": "Password inconsistency",
@@ -228,24 +229,26 @@
"Store": "Store",
"Tools": "Tools"
},
"openapi": {
"app key tips": "These keys have the identification of the current application and can be used by external access.",
"key alias": "Alias of key, for display only",
"key tips": "You can use API keys to access certain interfaces"
},
"outlink": {
"Copy Iframe": "Copy Iframe",
"Copy Link": "Copy",
"Create Ifrme Window": "Create Iframe Link",
"Create Share Window": "Create Share Window",
"Create Link": "Create Link",
"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"
"Response Detail": "Quote",
"Response Detail tips": "Whether detailed data such as references to be returned"
},
"system": {
"Help Document": "Document"
@@ -289,6 +292,9 @@
"Update password failed": "Update password failed",
"Update password succseful": "Update password succseful",
"Usage Record": "Usage",
"apikey": {
"key": "API Keys"
},
"promotion": {
"pay": "",
"register": ""

View File

@@ -104,7 +104,6 @@
},
"common": {
"Add": "添加",
"Cancel": "取消",
"Collect": "收藏",
"Copy": "复制",
"Copy Successful": "复制成功",
@@ -119,6 +118,8 @@
"Filed is repeat": "",
"Filed is repeated": "字段重复了",
"Input": "输入",
"Max credit": "最大金额",
"Max credit tips": "该链接最大可消耗多少金额,超出后链接将被禁止使用。-1 代表无限制。",
"Name is empty": "名称不能为空",
"Output": "输出",
"Password inconsistency": "两次密码不一致",
@@ -228,24 +229,26 @@
"Store": "应用市场",
"Tools": "工具"
},
"openapi": {
"app key tips": "这些 Key 已有当前应用标识,可以直接外部接入使用。",
"key alias": "key 的别名,仅用于展示",
"key tips": "你可以使用 API 秘钥访问一些特定的接口"
},
"outlink": {
"Copy Iframe": "复制嵌入",
"Copy Link": "复制",
"Create Ifrme Window": "创建嵌入链接",
"Create Share Window": "创建免登录窗口",
"Create Link": "创建链接",
"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": "是否需要返回引用、完整上下文等详细数据"
"Response Detail": "返回引用",
"Response Detail tips": "是否需要返回引用"
},
"system": {
"Help Document": "帮助文档"
@@ -289,6 +292,9 @@
"Update password failed": "修改密码异常",
"Update password succseful": "修改密码成功",
"Usage Record": "使用记录",
"apikey": {
"key": "API 秘钥"
},
"promotion": {
"pay": "好友充值",
"register": "好友注册"

View File

@@ -1,16 +0,0 @@
import { GET, POST, DELETE } from './request';
import { UserOpenApiKey } from '@/types/openapi';
/**
* crete a api key
*/
export const createAOpenApiKey = () => POST<string>('/openapi/postKey');
/**
* get api keys
*/
export const getOpenApiKeys = () => GET<UserOpenApiKey[]>('/openapi/getKeys');
/**
* delete api by id
*/
export const delOpenApiById = (id: string) => DELETE(`/openapi/delKey?id=${id}`);

View File

@@ -0,0 +1,11 @@
import type { OpenApiSchema } from '@/types/support/openapi';
export type GetApiKeyProps = {
appId?: string;
};
export type EditApiKeyProps = {
appId?: string;
name: string;
limit: OpenApiSchema['limit'];
};

View File

@@ -0,0 +1,26 @@
import { GET, POST, DELETE } from '@/api/request';
import { EditApiKeyProps, GetApiKeyProps } from './index.d';
import type { OpenApiSchema } from '@/types/support/openapi';
/**
* crete a api key
*/
export const createAOpenApiKey = (data: EditApiKeyProps) =>
POST<string>('/support/openapi/postKey', data);
/**
* update a api key
*/
export const putOpenApiKey = (data: EditApiKeyProps & { _id: string }) =>
POST<string>('/support/openapi/putKey', data);
/**
* get api keys
*/
export const getOpenApiKeys = (params?: GetApiKeyProps) =>
GET<OpenApiSchema[]>('/support/openapi/getKeys', params);
/**
* delete api by id
*/
export const delOpenApiById = (id: string) => DELETE(`/support/openapi/delKey?id=${id}`);

View File

@@ -1,144 +0,0 @@
import React, { useState } from 'react';
import {
Box,
Button,
Flex,
ModalFooter,
ModalBody,
Table,
Thead,
Tbody,
Tr,
Th,
Td,
TableContainer,
IconButton
} from '@chakra-ui/react';
import { getOpenApiKeys, createAOpenApiKey, delOpenApiById } from '@/api/openapi';
import { useQuery, useMutation } from '@tanstack/react-query';
import { useLoading } from '@/hooks/useLoading';
import dayjs from 'dayjs';
import { AddIcon, DeleteIcon } from '@chakra-ui/icons';
import { getErrText } from '@/utils/tools';
import { useCopyData } from '@/hooks/useCopyData';
import { useToast } from '@/hooks/useToast';
import MyIcon from '../Icon';
import MyModal from '../MyModal';
const APIKeyModal = ({ onClose }: { onClose: () => void }) => {
const { Loading } = useLoading();
const { toast } = useToast();
const {
data: apiKeys = [],
isLoading: isGetting,
refetch
} = useQuery(['getOpenApiKeys'], getOpenApiKeys);
const [apiKey, setApiKey] = useState('');
const { copyData } = useCopyData();
const { mutate: onclickCreateApiKey, isLoading: isCreating } = useMutation({
mutationFn: () => createAOpenApiKey(),
onSuccess(res) {
setApiKey(res);
refetch();
},
onError(err) {
toast({
status: 'warning',
title: getErrText(err)
});
}
});
const { mutate: onclickRemove, isLoading: isDeleting } = useMutation({
mutationFn: async (id: string) => delOpenApiById(id),
onSuccess() {
refetch();
}
});
return (
<MyModal isOpen onClose={onClose} w={'600px'}>
<Box py={3} px={5}>
<Box fontWeight={'bold'} fontSize={'2xl'}>
API
</Box>
<Box fontSize={'sm'} color={'myGray.600'}>
API 使~
</Box>
</Box>
<ModalBody minH={'300px'} maxH={['70vh', '500px']} overflow={'overlay'}>
<TableContainer mt={2} position={'relative'}>
<Table>
<Thead>
<Tr>
<Th>Api Key</Th>
<Th></Th>
<Th>使</Th>
<Th />
</Tr>
</Thead>
<Tbody fontSize={'sm'}>
{apiKeys.map(({ id, apiKey, createTime, lastUsedTime }) => (
<Tr key={id}>
<Td>{apiKey}</Td>
<Td>{dayjs(createTime).format('YYYY/MM/DD HH:mm:ss')}</Td>
<Td>
{lastUsedTime
? dayjs(lastUsedTime).format('YYYY/MM/DD HH:mm:ss')
: '没有使用过'}
</Td>
<Td>
<IconButton
icon={<DeleteIcon />}
size={'xs'}
aria-label={'delete'}
variant={'base'}
colorScheme={'gray'}
onClick={() => onclickRemove(id)}
/>
</Td>
</Tr>
))}
</Tbody>
</Table>
</TableContainer>
</ModalBody>
<ModalFooter>
<Button
variant="base"
leftIcon={<AddIcon color={'myGray.600'} fontSize={'sm'} />}
onClick={() => onclickCreateApiKey()}
>
</Button>
</ModalFooter>
<Loading loading={isGetting || isCreating || isDeleting} fixed={false} />
<MyModal isOpen={!!apiKey} w={'400px'} onClose={() => setApiKey('')}>
<Box py={3} px={5}>
<Box fontWeight={'bold'} fontSize={'2xl'}>
API
</Box>
<Box fontSize={'sm'} color={'myGray.600'}>
~
</Box>
</Box>
<ModalBody>
<Flex bg={'myGray.100'} px={3} py={2} cursor={'pointer'} onClick={() => copyData(apiKey)}>
<Box flex={1}>{apiKey}</Box>
<MyIcon name={'copy'} w={'16px'}></MyIcon>
</Flex>
</ModalBody>
<ModalFooter>
<Button variant="base" onClick={() => setApiKey('')}>
</Button>
</ModalFooter>
</MyModal>
</MyModal>
);
};
export default APIKeyModal;

View File

@@ -89,10 +89,12 @@ const ResponseModal = ({
</Box>
<Box py={2} px={4} flex={'1 0 0'} overflow={'auto'}>
<Row label={t('chat.response.module name')} value={activeModule?.moduleName} />
{activeModule?.price !== undefined && (
<Row
label={t('chat.response.module price')}
value={`${formatPrice(activeModule?.price)}`}
/>
)}
<Row
label={t('chat.response.module time')}
value={`${activeModule?.runningTime || 0}s`}

View File

@@ -0,0 +1,8 @@
<?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="1695372022822"
class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="4982"
xmlns:xlink="http://www.w3.org/1999/xlink" width="64" height="64">
<path
d="M975.657143 96.914286l-48.457143-48.457143c-1.828571-1.828571-4.114286-2.628571-6.514286-2.628572s-4.685714 0.914286-6.514285 2.628572l-86.971429 86.971428a227.737143 227.737143 0 0 0-128.114286-39.2c-58.514286 0-117.028571 22.285714-161.714285 66.971429L420.914286 279.657143a9.177143 9.177143 0 0 0 0 12.914286L731.428571 603.085714c1.828571 1.828571 4.114286 2.628571 6.514286 2.628572 2.285714 0 4.685714-0.914286 6.514286-2.628572l116.457143-116.457143c78.742857-78.857143 88-200.8 27.771428-289.714285l86.971429-86.971429c3.542857-3.657143 3.542857-9.485714 0-13.028571zM588.457143 551.657143a9.177143 9.177143 0 0 0-12.914286 0L499.428571 627.771429 396.228571 524.571429l76.228572-76.228572c3.542857-3.542857 3.542857-9.371429 0-12.914286L430.857143 393.828571a9.177143 9.177143 0 0 0-12.914286 0L341.714286 470.057143l-49.142857-49.142857a8.971429 8.971429 0 0 0-6.514286-2.628572c-2.285714 0-4.685714 0.914286-6.514286 2.628572L163.2 537.371429c-78.742857 78.742857-88 200.8-27.771429 289.714285l-86.971428 86.971429a9.177143 9.177143 0 0 0 0 12.914286l48.457143 48.457142c1.828571 1.828571 4.114286 2.628571 6.514285 2.628572s4.685714-0.914286 6.514286-2.628572l86.971429-86.971428c38.514286 26.171429 83.314286 39.2 128.114285 39.2 58.514286 0 117.028571-22.285714 161.714286-66.971429l116.457143-116.457143c3.542857-3.542857 3.542857-9.371429 0-12.914285l-49.142857-49.142857 76.228571-76.228572c3.542857-3.542857 3.542857-9.371429 0-12.914286l-41.828571-41.371428z"
p-id="4983" fill="#515151"></path>
</svg>

After

Width:  |  Height:  |  Size: 1.8 KiB

View File

@@ -34,6 +34,7 @@ const iconPaths = {
kbTest: () => import('./icons/kbTest.svg'),
date: () => import('./icons/date.svg'),
apikey: () => import('./icons/apikey.svg'),
apikeyFill: () => import('./icons/fill/apikey.svg'),
save: () => import('./icons/save.svg'),
minus: () => import('./icons/minus.svg'),
chat: () => import('./icons/light/chat.svg'),

View File

@@ -1,5 +1,5 @@
import React, { useMemo } from 'react';
import { Box } from '@chakra-ui/react';
import { Box, Link } from '@chakra-ui/react';
import ReactMarkdown from 'react-markdown';
import RemarkGfm from 'remark-gfm';
import RemarkMath from 'remark-math';
@@ -10,26 +10,30 @@ import 'katex/dist/katex.min.css';
import styles from '../index.module.scss';
import Image from '../img/Image';
function Link(e: any) {
function MyLink(e: any) {
const href = e.href;
const text = String(e.children);
return (
<Box as={'li'} py={1} m={0}>
return !!href ? (
<Link href={href} target={'_blank'}>
{text}
</Link>
) : (
<Box as={'ul'}>
<Box as={'li'}>
<Box
as={'span'}
color={'blue.600'}
textDecoration={'underline'}
cursor={'pointer'}
onClick={() => {
if (href) {
return window.open(href, '_blank');
}
event.emit('guideClick', { text });
}}
>
{text}
</Box>
</Box>
</Box>
);
}
@@ -42,7 +46,7 @@ const Guide = ({ text }: { text: string }) => {
remarkPlugins={[RemarkGfm, RemarkMath]}
rehypePlugins={[RehypeKatex]}
components={{
a: Link,
a: MyLink,
img: Image
}}
>

View File

@@ -0,0 +1,362 @@
import React, { useEffect, useMemo, useState } from 'react';
import {
Box,
Button,
Flex,
ModalFooter,
ModalBody,
Table,
Thead,
Tbody,
Tr,
Th,
Td,
TableContainer,
useTheme,
Link,
Input,
MenuList,
MenuItem,
MenuButton,
Menu
} from '@chakra-ui/react';
import {
getOpenApiKeys,
createAOpenApiKey,
delOpenApiById,
putOpenApiKey
} from '@/api/support/openapi';
import type { EditApiKeyProps } from '@/api/support/openapi/index.d';
import { useQuery, useMutation } from '@tanstack/react-query';
import { useLoading } from '@/hooks/useLoading';
import dayjs from 'dayjs';
import { AddIcon, QuestionOutlineIcon } from '@chakra-ui/icons';
import { useCopyData } from '@/hooks/useCopyData';
import { feConfigs } from '@/store/static';
import { useTranslation } from 'react-i18next';
import MyIcon from '@/components/Icon';
import MyModal from '@/components/MyModal';
import { useForm } from 'react-hook-form';
import { useRequest } from '@/hooks/useRequest';
import MyTooltip from '@/components/MyTooltip';
type EditProps = EditApiKeyProps & { _id?: string };
const defaultEditData: EditProps = {
name: '',
limit: {
credit: -1
}
};
const ApiKeyTable = ({ tips, appId }: { tips: string; appId?: string }) => {
const { t } = useTranslation();
const { Loading } = useLoading();
const theme = useTheme();
const { copyData } = useCopyData();
const [baseUrl, setBaseUrl] = useState('https://fastgpt.run/api');
const [editData, setEditData] = useState<EditProps>();
const [apiKey, setApiKey] = useState('');
const { mutate: onclickRemove, isLoading: isDeleting } = useMutation({
mutationFn: async (id: string) => delOpenApiById(id),
onSuccess() {
refetch();
}
});
const {
data: apiKeys = [],
isLoading: isGetting,
refetch
} = useQuery(['getOpenApiKeys', appId], () => getOpenApiKeys({ appId }));
useEffect(() => {
setBaseUrl(`${location.origin}/api`);
}, []);
return (
<Flex flexDirection={'column'} h={'100%'} position={'relative'}>
<Box display={['block', 'flex']} py={[0, 3]} px={5} alignItems={'center'}>
<Box flex={1}>
<Flex alignItems={'flex-end'}>
<Box fontSize={['md', 'xl']} fontWeight={'bold'}>
API
</Box>
<Link
href={
feConfigs.openAPIUrl ||
'https://kjqvjse66l.feishu.cn/docx/DmLedTWtUoNGX8xui9ocdUEjnNh'
}
target={'_blank'}
ml={1}
color={'myBlue.600'}
>
</Link>
</Flex>
<Box fontSize={'sm'} color={'myGray.600'}>
{tips}
</Box>
</Box>
<Flex
mt={[2, 0]}
bg={'myWhite.600'}
py={2}
px={4}
borderRadius={'md'}
cursor={'pointer'}
userSelect={'none'}
onClick={() => copyData(baseUrl, '已复制 API 地址')}
>
<Box border={theme.borders.md} px={2} borderRadius={'md'} fontSize={'sm'}>
API地址
</Box>
<Box ml={2} color={'myGray.900'} fontSize={['sm', 'md']}>
{baseUrl}
</Box>
</Flex>
<Box mt={[2, 0]} textAlign={'right'}>
<Button
ml={3}
leftIcon={<AddIcon fontSize={'md'} />}
variant={'base'}
onClick={() =>
setEditData({
...defaultEditData,
appId
})
}
>
</Button>
</Box>
</Box>
<TableContainer mt={2} position={'relative'} minH={'300px'}>
<Table>
<Thead>
<Tr>
<Th>{t('Name')}</Th>
<Th>Api Key</Th>
<Th>()</Th>
{feConfigs?.isPlus && (
<>
<Th>()</Th>
<Th></Th>
</>
)}
<Th></Th>
<Th>使</Th>
<Th />
</Tr>
</Thead>
<Tbody fontSize={'sm'}>
{apiKeys.map(({ _id, name, usage, limit, apiKey, createTime, lastUsedTime }) => (
<Tr key={_id}>
<Td>{name}</Td>
<Td>{apiKey}</Td>
<Td>{usage}</Td>
{feConfigs?.isPlus && (
<>
<Td>{limit?.credit && limit?.credit > -1 ? `${limit?.credit}` : '无限制'}</Td>
<Td whiteSpace={'pre-wrap'}>
{limit?.expiredTime
? dayjs(limit?.expiredTime).format('YYYY/MM/DD\nHH:mm')
: '-'}
</Td>
</>
)}
<Td whiteSpace={'pre-wrap'}>{dayjs(createTime).format('YYYY/MM/DD\nHH:mm:ss')}</Td>
<Td whiteSpace={'pre-wrap'}>
{lastUsedTime ? dayjs(lastUsedTime).format('YYYY/MM/DD\nHH:mm:ss') : '没有使用过'}
</Td>
<Td>
<Menu autoSelect={false} isLazy>
<MenuButton
_hover={{ bg: 'myWhite.600 ' }}
cursor={'pointer'}
borderRadius={'md'}
>
<MyIcon name={'more'} w={'14px'} p={2} />
</MenuButton>
<MenuList color={'myGray.700'} minW={`120px !important`} zIndex={10}>
<MenuItem
onClick={() =>
setEditData({
_id,
name,
limit,
appId
})
}
py={[2, 3]}
>
<MyIcon name={'edit'} w={['14px', '16px']} />
<Box ml={[1, 2]}>{t('common.Edit')}</Box>
</MenuItem>
<MenuItem onClick={() => onclickRemove(_id)} py={[2, 3]}>
<MyIcon name={'delete'} w={['14px', '16px']} />
<Box ml={[1, 2]}>{t('common.Delete')}</Box>
</MenuItem>
</MenuList>
</Menu>
</Td>
</Tr>
))}
</Tbody>
</Table>
<Loading loading={isGetting || isDeleting} fixed={false} />
</TableContainer>
{!!editData && (
<EditKeyModal
defaultData={editData}
onClose={() => setEditData(undefined)}
onCreate={(id) => {
setApiKey(id);
refetch();
setEditData(undefined);
}}
onEdit={() => {
refetch();
setEditData(undefined);
}}
/>
)}
<MyModal isOpen={!!apiKey} w={['400px', '600px']} onClose={() => setApiKey('')}>
<Box py={3} px={5}>
<Box fontWeight={'bold'} fontSize={'2xl'}>
API
</Box>
<Box fontSize={'sm'} color={'myGray.600'}>
~
</Box>
</Box>
<ModalBody>
<Flex
bg={'myGray.100'}
px={3}
py={2}
whiteSpace={'pre-wrap'}
wordBreak={'break-all'}
cursor={'pointer'}
onClick={() => copyData(apiKey)}
>
<Box flex={1}>{apiKey}</Box>
<MyIcon ml={1} name={'copy'} w={'16px'}></MyIcon>
</Flex>
</ModalBody>
<ModalFooter>
<Button variant="base" onClick={() => setApiKey('')}>
</Button>
</ModalFooter>
</MyModal>
</Flex>
);
};
export default React.memo(ApiKeyTable);
// edit link modal
function EditKeyModal({
defaultData,
onClose,
onCreate,
onEdit
}: {
defaultData: EditProps;
onClose: () => void;
onCreate: (id: string) => void;
onEdit: () => void;
}) {
const { t } = useTranslation();
const isEdit = useMemo(() => !!defaultData._id, [defaultData]);
const {
register,
setValue,
handleSubmit: submitShareChat
} = useForm({
defaultValues: defaultData
});
const { mutate: onclickCreate, isLoading: creating } = useRequest({
mutationFn: async (e: EditProps) => createAOpenApiKey(e),
errorToast: '创建链接异常',
onSuccess: onCreate
});
const { mutate: onclickUpdate, isLoading: updating } = useRequest({
mutationFn: (e: EditProps) => {
//@ts-ignore
return putOpenApiKey(e);
},
errorToast: '更新链接异常',
onSuccess: onEdit
});
return (
<MyModal isOpen={true} title={isEdit ? t('outlink.Edit Link') : t('outlink.Create Link')}>
<ModalBody>
<Flex alignItems={'center'}>
<Box flex={'0 0 90px'}>{t('Name')}:</Box>
<Input
placeholder={t('openapi.key alias') || 'key alias'}
maxLength={20}
{...register('name', {
required: t('common.Name is empty') || 'Name is empty'
})}
/>
</Flex>
{feConfigs?.isPlus && (
<>
<Flex alignItems={'center'} mt={4}>
<Flex flex={'0 0 90px'} alignItems={'center'}>
{t('common.Max credit')}:
<MyTooltip label={t('common.Max credit tips' || '')}>
<QuestionOutlineIcon ml={1} />
</MyTooltip>
</Flex>
<Input
{...register('limit.credit', {
min: -1,
max: 1000,
valueAsNumber: true,
required: true
})}
/>
</Flex>
<Flex alignItems={'center'} mt={4}>
<Flex flex={'0 0 90px'} alignItems={'center'}>
{t('common.Expired Time')}:
</Flex>
<Input
type="datetime-local"
defaultValue={
defaultData.limit?.expiredTime
? dayjs(defaultData.limit?.expiredTime).format('YYYY-MM-DDTHH:mm')
: ''
}
onChange={(e) => {
setValue('limit.expiredTime', new Date(e.target.value));
}}
/>
</Flex>
</>
)}
</ModalBody>
<ModalFooter>
<Button variant={'base'} mr={3} onClick={onClose}>
{t('Cancel')}
</Button>
<Button
isLoading={creating || updating}
onClick={submitShareChat((data) => (isEdit ? onclickUpdate(data) : onclickCreate(data)))}
>
{t('Confirm')}
</Button>
</ModalFooter>
</MyModal>
);
}

View File

@@ -54,7 +54,8 @@ export const ChatSourceMap = {
export enum OutLinkTypeEnum {
'share' = 'share',
'iframe' = 'iframe'
'iframe' = 'iframe',
apikey = 'apikey'
}
export const HUMAN_ICON = `/icon/human.png`;

View File

@@ -0,0 +1,10 @@
import React from 'react';
import { useTranslation } from 'react-i18next';
import ApiKeyTable from '@/components/support/apikey/Table';
const ApiKey = () => {
const { t } = useTranslation();
return <ApiKeyTable tips={t('openapi.key tips')}></ApiKeyTable>;
};
export default ApiKey;

View File

@@ -210,19 +210,19 @@ const UserInfo = () => {
{t('user.Change')}
</Button>
</Flex>
{feConfigs?.show_userDetail && (
<Box mt={6} whiteSpace={'nowrap'} w={['85%', '300px']}>
<Flex alignItems={'center'}>
<Box flex={'0 0 80px'}>{t('user.Balance')}:&nbsp;</Box>
<Box flex={1}>
<strong>{userInfo?.balance.toFixed(3)}</strong>
</Box>
{feConfigs?.show_pay && (
<Button size={['sm', 'md']} ml={5} onClick={onOpenPayModal}>
{t('user.Pay')}
</Button>
)}
</Flex>
</Box>
)}
{feConfigs?.show_doc && (
<>
<Flex

View File

@@ -19,6 +19,7 @@ const Promotion = dynamic(() => import('./components/Promotion'));
const BillTable = dynamic(() => import('./components/BillTable'));
const PayRecordTable = dynamic(() => import('./components/PayRecordTable'));
const InformTable = dynamic(() => import('./components/InformTable'));
const ApiKeyTable = dynamic(() => import('./components/ApiKeyTable'));
enum TabEnum {
'info' = 'info',
@@ -26,6 +27,7 @@ enum TabEnum {
'bill' = 'bill',
'pay' = 'pay',
'inform' = 'inform',
'apikey' = 'apikey',
'loginout' = 'loginout'
}
@@ -43,13 +45,17 @@ const Account = ({ currentTab }: { currentTab: `${TabEnum}` }) => {
label: t('user.Usage Record'),
id: TabEnum.bill
},
...(feConfigs?.show_userDetail
...(feConfigs?.show_promotion
? [
{
icon: 'promotionLight',
label: t('user.Promotion Record'),
id: TabEnum.promotion
},
}
]
: []),
...(feConfigs?.show_pay
? [
{
icon: 'payRecordLight',
label: t('user.Recharge Record'),
@@ -57,6 +63,11 @@ const Account = ({ currentTab }: { currentTab: `${TabEnum}` }) => {
}
]
: []),
{
icon: 'apikey',
label: t('user.apikey.key'),
id: TabEnum.apikey
},
{
icon: 'informLight',
label: t('user.Notice'),
@@ -141,6 +152,7 @@ const Account = ({ currentTab }: { currentTab: `${TabEnum}` }) => {
{currentTab === TabEnum.bill && <BillTable />}
{currentTab === TabEnum.pay && <PayRecordTable />}
{currentTab === TabEnum.inform && <InformTable />}
{currentTab === TabEnum.apikey && <ApiKeyTable />}
</Box>
</Flex>
<ConfirmModal />

View File

@@ -41,8 +41,19 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
{
$lookup: {
from: 'chatitems',
localField: 'chatId',
foreignField: 'chatId',
let: { chat_id: '$chatId' },
pipeline: [
{
$match: {
$expr: {
$and: [
{ $eq: ['$chatId', '$$chat_id'] },
{ $eq: ['$appId', new Types.ObjectId(appId)] }
]
}
}
}
],
as: 'chatitems'
}
},

View File

@@ -6,7 +6,7 @@ import { sseResponseEventEnum } from '@/constants/chat';
import { sseResponse } from '@/service/utils/tools';
import { AppModuleItemType } from '@/types/app';
import { dispatchModules } from '../openapi/v1/chat/completions';
import { pushTaskBill } from '@/service/common/bill/push';
import { pushChatBill } from '@/service/common/bill/push';
import { BillSourceEnum } from '@/constants/user';
import { ChatItemType } from '@/types/chat';
@@ -72,7 +72,7 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
});
res.end();
pushTaskBill({
pushChatBill({
appName,
appId,
userId,

View File

@@ -45,14 +45,16 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
Chat.findOne(
{
chatId,
userId
userId,
appId
},
'title variables'
),
ChatItem.find(
{
chatId,
userId
userId,
appId
},
`dataId obj value adminFeedback userFeedback ${TaskResponseKeyEnum.responseData}`
)

View File

@@ -1,37 +0,0 @@
// Next.js API route support: https://nextjs.org/docs/api-routes/introduction
import type { NextApiRequest, NextApiResponse } from 'next';
import { jsonRes } from '@/service/response';
import { connectToDatabase, OpenApi } from '@/service/mongo';
import { authUser } from '@/service/utils/auth';
import { UserOpenApiKey } from '@/types/openapi';
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
try {
const { userId } = await authUser({ req, authToken: true });
await connectToDatabase();
const findResponse = await OpenApi.find({ userId }).sort({ _id: -1 });
// jus save four data
const apiKeys = findResponse.map<UserOpenApiKey>(
({ _id, apiKey, createTime, lastUsedTime }) => {
return {
id: _id,
apiKey: `******${apiKey.substring(apiKey.length - 4)}`,
createTime,
lastUsedTime
};
}
);
jsonRes(res, {
data: apiKeys
});
} catch (err) {
jsonRes(res, {
code: 500,
error: err
});
}
}

View File

@@ -1,6 +1,6 @@
import type { NextApiRequest, NextApiResponse } from 'next';
import { connectToDatabase } from '@/service/mongo';
import { authUser, authApp } from '@/service/utils/auth';
import { authUser, authApp, AuthUserTypeEnum } from '@/service/utils/auth';
import { sseErrRes, jsonRes } from '@/service/response';
import { addLog, withNextCors } from '@/service/utils/tools';
import { ChatRoleEnum, ChatSourceEnum, sseResponseEventEnum } from '@/constants/chat';
@@ -23,7 +23,7 @@ import { type ChatCompletionRequestMessage } from 'openai';
import { TaskResponseKeyEnum } from '@/constants/chat';
import { FlowModuleTypeEnum, initModuleType } from '@/constants/flow';
import { AppModuleItemType, RunningModuleItemType } from '@/types/app';
import { pushTaskBill } from '@/service/common/bill/push';
import { pushChatBill } from '@/service/common/bill/push';
import { BillSourceEnum } from '@/constants/user';
import { ChatHistoryItemResType } from '@/types/chat';
import { UserModelSchema } from '@/types/mongoSchema';
@@ -33,6 +33,9 @@ import { authOutLinkChat } from '@/service/support/outLink/auth';
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 { updateApiKeyUsage } from '@/service/support/openapi';
export type MessageItemType = ChatCompletionRequestMessage & { dataId?: string };
type FastGptWebChatProps = {
@@ -75,6 +78,7 @@ export default withNextCors(async function handler(req: NextApiRequest, res: Nex
} = req.body as Props;
try {
// body data check
if (!messages) {
throw new Error('Prams Error');
}
@@ -89,24 +93,35 @@ export default withNextCors(async function handler(req: NextApiRequest, res: Nex
let startTime = Date.now();
/* user auth */
let {
// @ts-ignore
responseDetail,
const {
responseDetail: shareResponseDetail,
user,
userId,
appId: authAppid,
authType
} = await (shareId
? authOutLinkChat({
authType,
apikey
} = await (async (): Promise<{
user?: UserModelSchema;
responseDetail?: boolean;
userId: string;
appId: string;
authType: `${AuthUserTypeEnum}`;
apikey?: string;
}> => {
if (shareId) {
return authOutLinkChat({
shareId,
ip: requestIp.getClientIp(req)
})
: authUser({ req, authBalance: true }));
});
}
return authUser({ req, authBalance: true });
})();
if (!user) {
throw new Error('Account is error');
}
// must have a app
appId = appId ? appId : authAppid;
if (!appId) {
throw new Error('appId is empty');
@@ -122,8 +137,9 @@ export default withNextCors(async function handler(req: NextApiRequest, res: Nex
]);
const isOwner = !shareId && userId === String(app.userId);
responseDetail = isOwner || responseDetail;
const responseDetail = isOwner || shareResponseDetail;
/* format prompts */
const prompts = history.concat(gptMessage2ChatType(messages));
if (prompts[prompts.length - 1]?.obj === 'AI') {
prompts.pop();
@@ -134,7 +150,7 @@ export default withNextCors(async function handler(req: NextApiRequest, res: Nex
throw new Error('Question is empty');
}
// 创建响应流
// set sse response headers
if (stream) {
res.setHeader('Content-Type', 'text/event-stream;charset=utf-8');
res.setHeader('Access-Control-Allow-Origin', '*');
@@ -142,7 +158,7 @@ export default withNextCors(async function handler(req: NextApiRequest, res: Nex
res.setHeader('Cache-Control', 'no-cache, no-transform');
}
/* start process */
/* start flow controller */
const { responseData, answerText } = await dispatchModules({
res,
modules: app.modules,
@@ -188,6 +204,9 @@ export default withNextCors(async function handler(req: NextApiRequest, res: Nex
addLog.info(`completions running time: ${(Date.now() - startTime) / 1000}s`);
/* select fe response field */
const feResponseData = isOwner ? responseData : selectShareResponse({ responseData });
if (stream) {
sseResponse({
res,
@@ -207,14 +226,14 @@ export default withNextCors(async function handler(req: NextApiRequest, res: Nex
sseResponse({
res,
event: sseResponseEventEnum.appStreamResponse,
data: JSON.stringify(responseData)
data: JSON.stringify(feResponseData)
});
}
res.end();
} else {
res.json({
...(detail ? { responseData } : {}),
...(detail ? { responseData: feResponseData } : {}),
id: chatId || '',
model: '',
usage: { prompt_tokens: 1, completion_tokens: 1, total_tokens: 1 },
@@ -228,7 +247,8 @@ export default withNextCors(async function handler(req: NextApiRequest, res: Nex
});
}
pushTaskBill({
// add record
const { total } = pushChatBill({
appName: app.name,
appId,
userId,
@@ -237,8 +257,18 @@ export default withNextCors(async function handler(req: NextApiRequest, res: Nex
if (shareId) return BillSourceEnum.shareLink;
return BillSourceEnum.fastgpt;
})(),
response: responseData,
shareId
response: responseData
});
!!shareId &&
updateOutLinkUsage({
shareId,
total
});
!!apikey &&
updateApiKeyUsage({
apikey,
usage: total
});
} catch (err: any) {
if (stream) {

View File

@@ -0,0 +1,25 @@
import type { NextApiRequest, NextApiResponse } from 'next';
import { jsonRes } from '@/service/response';
import { connectToDatabase, OpenApi } from '@/service/mongo';
import { authUser } from '@/service/utils/auth';
import type { GetApiKeyProps } from '@/api/support/openapi/index.d';
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
try {
const { appId } = req.query as GetApiKeyProps;
const { userId } = await authUser({ req, authToken: true });
await connectToDatabase();
const findResponse = await OpenApi.find({ userId, appId }).sort({ _id: -1 });
jsonRes(res, {
data: findResponse.map((item) => item.toObject())
});
} catch (err) {
jsonRes(res, {
code: 500,
error: err
});
}
}

View File

@@ -4,25 +4,33 @@ import { jsonRes } from '@/service/response';
import { connectToDatabase, OpenApi } from '@/service/mongo';
import { authUser } from '@/service/utils/auth';
import { customAlphabet } from 'nanoid';
const nanoid = customAlphabet('abcdefghijklmnopqrstuvwxyz1234567890', 24);
import type { EditApiKeyProps } from '@/api/support/openapi/index.d';
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
try {
const { appId, name, limit } = req.body as EditApiKeyProps;
const { userId } = await authUser({ req, authToken: true });
await connectToDatabase();
const count = await OpenApi.find({ userId }).countDocuments();
const count = await OpenApi.find({ userId, appId }).countDocuments();
if (count >= 10) {
throw new Error('最多 10 组 API 秘钥');
}
const nanoid = customAlphabet(
'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890',
Math.floor(Math.random() * 14) + 24
);
const apiKey = `${global.systemEnv?.openapiPrefix || 'fastgpt'}-${nanoid()}`;
await OpenApi.create({
userId,
apiKey
apiKey,
appId,
name,
limit
});
jsonRes(res, {

View File

@@ -0,0 +1,32 @@
import type { NextApiRequest, NextApiResponse } from 'next';
import { jsonRes } from '@/service/response';
import { connectToDatabase, OpenApi } from '@/service/mongo';
import { authUser } from '@/service/utils/auth';
import type { EditApiKeyProps } from '@/api/support/openapi/index.d';
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
try {
const { _id, name, limit } = req.body as EditApiKeyProps & { _id: string };
const { userId } = await authUser({ req, authToken: true });
await connectToDatabase();
await OpenApi.findOneAndUpdate(
{
_id,
userId
},
{
...(name && { name }),
...(limit && { limit })
}
);
jsonRes(res);
} catch (err) {
jsonRes(res, {
code: 500,
error: err
});
}
}

View File

@@ -39,9 +39,6 @@ const defaultSystemEnv: SystemEnvType = {
};
const defaultFeConfigs: FeConfigsType = {
show_emptyChat: true,
show_register: false,
show_appStore: false,
show_userDetail: false,
show_contact: true,
show_git: true,
show_doc: true,

View File

@@ -1,96 +0,0 @@
import React, { useEffect, useState } from 'react';
import { Box, Divider, Flex, useTheme, Button, Skeleton, useDisclosure } from '@chakra-ui/react';
import { useCopyData } from '@/hooks/useCopyData';
import dynamic from 'next/dynamic';
import MyIcon from '@/components/Icon';
import { useGlobalStore } from '@/store/global';
import { feConfigs } from '@/store/static';
const APIKeyModal = dynamic(() => import('@/components/APIKeyModal'), {
ssr: false
});
const API = ({ appId }: { appId: string }) => {
const theme = useTheme();
const { copyData } = useCopyData();
const [baseUrl, setBaseUrl] = useState('https://fastgpt.run/api/openapi');
const {
isOpen: isOpenAPIModal,
onOpen: onOpenAPIModal,
onClose: onCloseAPIModal
} = useDisclosure();
const [isLoaded, setIsLoaded] = useState(false);
const { isPc } = useGlobalStore();
useEffect(() => {
setBaseUrl(`${location.origin}/api/openapi`);
}, []);
return (
<Flex flexDirection={'column'} pt={[0, 5]} h={'100%'}>
<Flex px={5} alignItems={'center'}>
<Box flex={1}>
AppId:
<Box
as={'span'}
ml={2}
fontWeight={'bold'}
cursor={'pointer'}
onClick={() => copyData(appId, '已复制 AppId')}
>
{appId}
</Box>
</Box>
{isPc && (
<>
<Flex
bg={'myWhite.600'}
py={2}
px={4}
borderRadius={'md'}
cursor={'pointer'}
onClick={() => copyData(baseUrl, '已复制 API 地址')}
>
<Box border={theme.borders.md} px={2} borderRadius={'md'} fontSize={'sm'}>
API服务器
</Box>
<Box ml={2} color={'myGray.900'} fontSize={['sm', 'md']}>
{baseUrl}
</Box>
</Flex>
<Button
ml={3}
leftIcon={<MyIcon name={'apikey'} w={'16px'} color={''} />}
variant={'base'}
onClick={onOpenAPIModal}
>
API
</Button>
</>
)}
</Flex>
<Divider mt={3} />
<Box flex={'1 0 0'} h={0}>
<Skeleton h="100%" isLoaded={isLoaded} fadeDuration={2}>
<iframe
style={{
width: '100%',
height: '100%'
}}
src={
feConfigs?.openAPIUrl ||
'https://kjqvjse66l.feishu.cn/docx/DmLedTWtUoNGX8xui9ocdUEjnNh'
}
frameBorder="0"
onLoad={() => setIsLoaded(true)}
onError={() => setIsLoaded(true)}
/>
</Skeleton>
</Box>
{isOpenAPIModal && <APIKeyModal onClose={onCloseAPIModal} />}
</Flex>
);
};
export default API;

View File

@@ -73,7 +73,7 @@ const NodeCard = (props: Props) => {
{
icon: 'back',
label: t('common.Cancel'),
label: t('Cancel'),
onClick: () => {}
}
],

View File

@@ -15,7 +15,8 @@ import {
TableContainer,
useDisclosure,
Button,
IconButton
IconButton,
Text
} from '@chakra-ui/react';
import { useUserStore } from '@/store/user';
import { useQuery } from '@tanstack/react-query';
@@ -209,9 +210,18 @@ const Settings = ({ appId }: { appId: string }) => {
pt={[0, 4]}
overflow={'overlay'}
>
<Flex alignItems={'flex-end'}>
<Box fontSize={['md', 'xl']} fontWeight={'bold'}>
</Box>
<Box ml={1} color={'myGray.500'} fontSize={'sm'}>
(
<Box as={'span'} userSelect={'all'}>
{appId}
</Box>
)
</Box>
</Flex>
{/* basic info */}
<Box
border={theme.borders.base}

View File

@@ -0,0 +1,10 @@
import React, { useEffect, useState } from 'react';
import ApiKeyTable from '@/components/support/apikey/Table';
import { useTranslation } from 'react-i18next';
const API = ({ appId }: { appId: string }) => {
const { t } = useTranslation();
return <ApiKeyTable tips={t('openapi.app key tips')} appId={appId} />;
};
export default API;

View File

@@ -1,4 +1,4 @@
import React, { useMemo, useRef, useState } from 'react';
import React, { useMemo, useState } from 'react';
import {
Flex,
Box,
@@ -13,7 +13,6 @@ import {
ModalFooter,
ModalBody,
Input,
useTheme,
Switch,
Menu,
MenuButton,
@@ -42,7 +41,6 @@ import { useTranslation } from 'react-i18next';
import { useToast } from '@/hooks/useToast';
import MyTooltip from '@/components/MyTooltip';
import MyModal from '@/components/MyModal';
import MyRadio from '@/components/Radio';
import dayjs from 'dayjs';
const Share = ({ appId }: { appId: string }) => {
@@ -90,12 +88,12 @@ const Share = ({ appId }: { appId: string }) => {
<Thead>
<Tr>
<Th></Th>
<Th></Th>
<Th>()</Th>
<>
<Th></Th>
<Th>()</Th>
<Th>IP限流/</Th>
</>
<Th></Th>
<Th></Th>
<Th></Th>
<Th>使</Th>
<Th></Th>
@@ -105,7 +103,7 @@ const Share = ({ appId }: { appId: string }) => {
{shareChatList.map((item) => (
<Tr key={item._id}>
<Td>{item.name}</Td>
<Td>{formatPrice(item.total)}</Td>
<Td>{formatPrice(item.total)}</Td>
{item.limit && (
<>
<Td>{item.limit?.credit > -1 ? `${item.limit?.credit}` : '无限制'}</Td>
@@ -226,7 +224,7 @@ const Share = ({ appId }: { appId: string }) => {
};
// edit link modal
export function EditLinkModal({
function EditLinkModal({
appId,
type,
defaultData,
@@ -243,16 +241,7 @@ export function EditLinkModal({
}) {
const { t } = useTranslation();
const isEdit = useMemo(() => !!defaultData._id, [defaultData]);
const titleMap = useRef({
create: {
[OutLinkTypeEnum.share]: t('outlink.Create Share Window'),
[OutLinkTypeEnum.iframe]: t('outlink.Create Ifrme Window')
},
edit: {
[OutLinkTypeEnum.share]: t('outlink.Edit Share Window'),
[OutLinkTypeEnum.iframe]: t('outlink.Edit Ifrme Link')
}
});
const {
register,
setValue,
@@ -281,10 +270,7 @@ export function EditLinkModal({
});
return (
<MyModal
isOpen={true}
title={isEdit ? titleMap.current.edit[type] : titleMap.current.create[type]}
>
<MyModal isOpen={true} title={isEdit ? t('outlink.Edit Link') : t('outlink.Create Link')}>
<ModalBody>
<Flex alignItems={'center'}>
<Box flex={'0 0 90px'}>{t('Name')}:</Box>
@@ -315,8 +301,8 @@ export function EditLinkModal({
</Flex>
<Flex alignItems={'center'} mt={4}>
<Flex flex={'0 0 90px'} alignItems={'center'}>
{t('outlink.Max credit')}:
<MyTooltip label={t('outlink.Max credit tips' || '')}>
{t('common.Max credit')}:
<MyTooltip label={t('common.Max credit tips' || '')}>
<QuestionOutlineIcon ml={1} />
</MyTooltip>
</Flex>
@@ -372,42 +358,4 @@ export function EditLinkModal({
);
}
const OutLink = ({ appId }: { appId: string }) => {
const theme = useTheme();
const [linkType, setLinkType] = useState<`${OutLinkTypeEnum}`>(OutLinkTypeEnum.share);
return (
<Box pt={[1, 5]}>
<Box fontWeight={'bold'} fontSize={['md', 'xl']} mb={2} px={[4, 8]}>
使
</Box>
<Box pb={[5, 7]} px={[4, 8]} borderBottom={theme.borders.base}>
<MyRadio
gridTemplateColumns={['repeat(1,1fr)', 'repeat(auto-fill, minmax(0, 360px))']}
iconSize={'20px'}
list={[
{
icon: 'outlink_share',
title: '免登录窗口',
desc: '分享链接给其他用户,无需登录即可直接进行使用',
value: OutLinkTypeEnum.share
}
// {
// icon: 'outlink_iframe',
// title: '网页嵌入',
// desc: '嵌入到已有网页中,右下角会生成对话按键',
// value: OutLinkTypeEnum.iframe
// }
]}
value={linkType}
onChange={(e) => setLinkType(e as `${OutLinkTypeEnum}`)}
/>
</Box>
{linkType === OutLinkTypeEnum.share && <Share appId={appId} />}
</Box>
);
};
export default OutLink;
export default React.memo(Share);

View File

@@ -0,0 +1,56 @@
import React, { useState } from 'react';
import { Box, useTheme } from '@chakra-ui/react';
import { OutLinkTypeEnum } from '@/constants/chat';
import dynamic from 'next/dynamic';
import MyRadio from '@/components/Radio';
import Share from './Share';
const API = dynamic(() => import('./API'));
const OutLink = ({ appId }: { appId: string }) => {
const theme = useTheme();
const [linkType, setLinkType] = useState<`${OutLinkTypeEnum}`>(OutLinkTypeEnum.share);
return (
<Box pt={[1, 5]}>
<Box fontWeight={'bold'} fontSize={['md', 'xl']} mb={2} px={[4, 8]}>
使
</Box>
<Box pb={[5, 7]} px={[4, 8]} borderBottom={theme.borders.base}>
<MyRadio
gridTemplateColumns={['repeat(1,1fr)', 'repeat(auto-fill, minmax(0, 360px))']}
iconSize={'20px'}
list={[
{
icon: 'outlink_share',
title: '免登录窗口',
desc: '分享链接给其他用户,无需登录即可直接进行使用',
value: OutLinkTypeEnum.share
},
{
icon: 'apikeyFill',
title: 'API 访问',
desc: '通过 API 接入到已有系统中,或企微、飞书等',
value: OutLinkTypeEnum.apikey
}
// {
// icon: 'outlink_iframe',
// title: '网页嵌入',
// desc: '嵌入到已有网页中,右下角会生成对话按键',
// value: OutLinkTypeEnum.iframe
// }
]}
value={linkType}
onChange={(e) => setLinkType(e as `${OutLinkTypeEnum}`)}
/>
</Box>
{linkType === OutLinkTypeEnum.share && <Share appId={appId} />}
{linkType === OutLinkTypeEnum.apikey && <API appId={appId} />}
</Box>
);
};
export default OutLink;

View File

@@ -23,9 +23,6 @@ const AdEdit = dynamic(() => import('./components/AdEdit'), {
const OutLink = dynamic(() => import('./components/OutLink'), {
ssr: false
});
const API = dynamic(() => import('./components/API'), {
ssr: false
});
const Logs = dynamic(() => import('./components/Logs'), {
ssr: false
});
@@ -35,7 +32,6 @@ enum TabEnum {
'adEdit' = 'adEdit',
'outLink' = 'outLink',
'logs' = 'logs',
'API' = 'API',
'startChat' = 'startChat'
}
@@ -63,7 +59,6 @@ const AppDetail = ({ currentTab }: { currentTab: `${TabEnum}` }) => {
{ label: '简易配置', id: TabEnum.basicEdit, icon: 'overviewLight' },
{ label: '高级编排', id: TabEnum.adEdit, icon: 'settingLight' },
{ label: '外部使用', id: TabEnum.outLink, icon: 'shareLight' },
{ label: 'API访问', id: TabEnum.API, icon: 'apiLight' },
{ label: '对话日志', id: TabEnum.logs, icon: 'logsLight' },
{ label: '立即对话', id: TabEnum.startChat, icon: 'chat' }
],
@@ -179,7 +174,6 @@ const AppDetail = ({ currentTab }: { currentTab: `${TabEnum}` }) => {
{currentTab === TabEnum.adEdit && appDetail && (
<AdEdit app={appDetail} onCloseSettings={() => setCurrentTab(TabEnum.basicEdit)} />
)}
{currentTab === TabEnum.API && <API appId={appId} />}
{currentTab === TabEnum.logs && <Logs appId={appId} />}
{currentTab === TabEnum.outLink && <OutLink appId={appId} />}
</Box>

View File

@@ -72,7 +72,7 @@ const EditFolderModal = ({
</ModalBody>
<ModalFooter>
<Button mr={3} variant={'base'} onClick={onClose}>
{t('common.Cancel')}
{t('Cancel')}
</Button>
<Button isLoading={isLoading} onClick={onSave}>
{t('Confirm')}

View File

@@ -4,7 +4,7 @@ import { getModel } from '@/service/utils/data';
import { ChatHistoryItemResType } from '@/types/chat';
import { formatPrice } from '@/utils/user';
import { addLog } from '@/service/utils/tools';
import type { BillListItemType, CreateBillType } from '@/types/common/bill';
import type { CreateBillType } from '@/types/common/bill';
async function createBill(data: CreateBillType) {
try {
@@ -56,25 +56,21 @@ async function concatBill({
} catch (error) {}
}
export const pushTaskBill = async ({
export const pushChatBill = ({
appName,
appId,
userId,
source,
shareId,
response
}: {
appName: string;
appId: string;
userId: string;
source: `${BillSourceEnum}`;
shareId?: string;
response: ChatHistoryItemResType[];
}) => {
try {
const total = response.reduce((sum, item) => sum + item.price, 0);
await Promise.allSettled([
createBill({
userId,
appName,
@@ -87,45 +83,13 @@ export const pushTaskBill = async ({
model: item.model,
tokenLen: item.tokens
}))
}),
...(shareId
? [
updateShareChatBill({
shareId,
total
})
]
: [])
]);
});
addLog.info(`finish completions`, {
source,
userId,
price: formatPrice(total)
});
} catch (error) {
addLog.error(`pushTaskBill error`, error);
}
};
export const updateShareChatBill = async ({
shareId,
total
}: {
shareId: string;
total: number;
}) => {
try {
await OutLink.findOneAndUpdate(
{ shareId },
{
$inc: { total },
lastTime: new Date()
}
);
} catch (err) {
addLog.error('update shareChat error', err);
}
return { total };
};
export const pushQABill = async ({
@@ -139,7 +103,6 @@ export const pushQABill = async ({
}) => {
addLog.info('splitData generate success', { totalTokens });
try {
// 获取模型单价格, 都是用 gpt35 拆分
const unitPrice = global.qaModel.price || 3;
// 计算价格
@@ -152,9 +115,8 @@ export const pushQABill = async ({
tokens: totalTokens,
listIndex: 1
});
} catch (err) {
addLog.error('Create completions bill error', err);
}
return { total };
};
export const pushGenerateVectorBill = async ({
@@ -168,7 +130,6 @@ export const pushGenerateVectorBill = async ({
tokenLen: number;
model: string;
}) => {
try {
// 计算价格. 至少为1
const vectorModel =
global.vectorModels.find((item) => item.model === model) || global.vectorModels[0];
@@ -201,9 +162,7 @@ export const pushGenerateVectorBill = async ({
]
});
}
} catch (err) {
addLog.error('Create generateVector bill error', err);
}
return { total };
};
export const countModelPrice = ({ model, tokens }: { model: string; tokens: number }) => {

View File

@@ -39,6 +39,7 @@ export enum ERROR_ENUM {
unAuthorization = 'unAuthorization',
insufficientQuota = 'insufficientQuota',
unAuthModel = 'unAuthModel',
unAuthApiKey = 'unAuthApiKey',
unAuthKb = 'unAuthKb',
unAuthFile = 'unAuthFile'
}
@@ -80,5 +81,11 @@ export const ERROR_RESPONSE: Record<
statusText: ERROR_ENUM.unAuthFile,
message: '无权阅读该文件',
data: null
},
[ERROR_ENUM.unAuthApiKey]: {
code: 514,
statusText: ERROR_ENUM.unAuthApiKey,
message: 'Api Key 不合法',
data: null
}
};

View File

@@ -1,23 +0,0 @@
import { Schema, model, models, Model } from 'mongoose';
import { OpenApiSchema } from '@/types/mongoSchema';
const OpenApiSchema = new Schema({
userId: {
type: Schema.Types.ObjectId,
ref: 'user',
required: true
},
apiKey: {
type: String,
required: true
},
createTime: {
type: Date,
default: () => new Date()
},
lastUsedTime: {
type: Date
}
});
export const OpenApi: Model<OpenApiSchema> = models['openapi'] || model('openapi', OpenApiSchema);

View File

@@ -133,10 +133,11 @@ export * from './models/user';
export * from './common/bill/schema';
export * from './models/pay';
export * from './models/trainingData';
export * from './models/openapi';
export * from './models/promotionRecord';
export * from './models/collection';
export * from './models/kb';
export * from './models/inform';
export * from './models/image';
export * from './support/outLink/schema';
export * from './support/openapi/schema';

View File

@@ -0,0 +1,38 @@
import { ERROR_ENUM } from '@/service/errorCode';
import { updateApiKeyUsedTime } from './index';
import { OpenApi } from './schema';
export async function authOpenApiKey({ apikey }: { apikey: string }) {
if (!apikey) {
return Promise.reject(ERROR_ENUM.unAuthApiKey);
}
try {
const openApi = await OpenApi.findOne({ apiKey: apikey });
if (!openApi) {
return Promise.reject(ERROR_ENUM.unAuthApiKey);
}
const userId = String(openApi.userId);
// auth limit
if (global.feConfigs?.isPlus) {
if (openApi?.limit?.expiredTime && openApi.limit.expiredTime.getTime() < Date.now()) {
return Promise.reject(`Key ${openApi.apiKey} is expired`);
}
if (
openApi?.limit?.credit &&
openApi.limit.credit > -1 &&
openApi.usage > openApi.limit.credit
) {
return Promise.reject(`Key ${openApi.apiKey} is over usage`);
}
}
updateApiKeyUsedTime(openApi._id);
return { apikey, userId, appId: openApi.appId };
} catch (error) {
return Promise.reject(error);
}
}

View File

@@ -0,0 +1,18 @@
import { OpenApi } from './schema';
export async function updateApiKeyUsedTime(id: string) {
await OpenApi.findByIdAndUpdate(id, {
lastUsedTime: new Date()
});
}
export async function updateApiKeyUsage({ apikey, usage }: { apikey: string; usage: number }) {
await OpenApi.findOneAndUpdate(
{ apiKey: apikey },
{
$inc: {
usage
}
}
);
}

View File

@@ -0,0 +1,57 @@
import { Schema, model, models, Model } from 'mongoose';
import { OpenApiSchema } from '@/types/support/openapi';
import { PRICE_SCALE } from '@/constants/common';
import { formatPrice } from '@/utils/user';
const OpenApiSchema = new Schema(
{
userId: {
type: Schema.Types.ObjectId,
ref: 'user',
required: true
},
apiKey: {
type: String,
required: true,
get: (val: string) => `******${val.substring(val.length - 4)}`
},
createTime: {
type: Date,
default: () => new Date()
},
lastUsedTime: {
type: Date
},
appId: {
type: String,
required: false
},
name: {
type: String,
default: 'Api Key'
},
usage: {
// total usage. value from bill total
type: Number,
default: 0,
get: (val: number) => formatPrice(val)
},
limit: {
expiredTime: {
type: Date
},
credit: {
// value from user settings
type: Number,
default: -1,
set: (val: number) => val * PRICE_SCALE,
get: (val: number) => formatPrice(val)
}
}
},
{
toObject: { getters: true }
}
);
export const OpenApi: Model<OpenApiSchema> = models['openapi'] || model('openapi', OpenApiSchema);

View File

@@ -0,0 +1,22 @@
import { addLog } from '@/service/utils/tools';
import { OutLink } from './schema';
export const updateOutLinkUsage = async ({
shareId,
total
}: {
shareId: string;
total: number;
}) => {
try {
await OutLink.findOneAndUpdate(
{ shareId },
{
$inc: { total },
lastTime: new Date()
}
);
} catch (err) {
addLog.error('update shareChat error', err);
}
};

View File

@@ -1,9 +1,10 @@
import type { NextApiRequest } from 'next';
import Cookie from 'cookie';
import { App, OpenApi, User, OutLink, KB } from '../mongo';
import { App, OpenApi, User, KB } from '../mongo';
import type { AppSchema, UserModelSchema } from '@/types/mongoSchema';
import { ERROR_ENUM } from '../errorCode';
import { authJWT } from './tools';
import { authOpenApiKey } from '../support/openapi/auth';
export enum AuthUserTypeEnum {
token = 'token',
@@ -51,28 +52,6 @@ export const authUser = async ({
authRoot?: boolean;
authBalance?: boolean;
}) => {
const parseOpenApiKey = async (apiKey?: string) => {
if (!apiKey) {
return Promise.reject(ERROR_ENUM.unAuthorization);
}
try {
const openApi = await OpenApi.findOne({ apiKey });
if (!openApi) {
return Promise.reject(ERROR_ENUM.unAuthorization);
}
const userId = String(openApi.userId);
// 更新使用的时间
await OpenApi.findByIdAndUpdate(openApi._id, {
lastUsedTime: new Date()
});
return userId;
} catch (error) {
return Promise.reject(error);
}
};
const parseAuthorization = async (authorization?: string) => {
if (!authorization) {
return Promise.reject(ERROR_ENUM.unAuthorization);
@@ -84,23 +63,30 @@ export const authUser = async ({
return Promise.reject(ERROR_ENUM.unAuthorization);
}
const { apiKey, appId } = await (async () => {
const { apikey, appId: authorizationAppid = '' } = await (async () => {
const arr = auth.split('-');
if (arr.length !== 3) {
return Promise.reject(ERROR_ENUM.unAuthorization);
}
// abandon
if (arr.length === 3) {
return {
apiKey: `${arr[0]}-${arr[1]}`,
apikey: `${arr[0]}-${arr[1]}`,
appId: arr[2]
};
}
if (arr.length === 2) {
return {
apikey: auth
};
}
return Promise.reject(ERROR_ENUM.unAuthorization);
})();
// auth apiKey
const uid = await parseOpenApiKey(apiKey);
// auth apikey
const { userId, appId: apiKeyAppId = '' } = await authOpenApiKey({ apikey });
return {
uid,
appId
uid: userId,
apikey,
appId: apiKeyAppId || authorizationAppid
};
};
const parseRootKey = async (rootKey?: string, userId = '') => {
@@ -114,13 +100,14 @@ export const authUser = async ({
cookie?: string;
token?: string;
apikey?: string;
rootkey?: string;
rootkey?: string; // abandon
userid?: string;
authorization?: string;
};
let uid = '';
let appId = '';
let openApiKey = apikey;
let authType: `${AuthUserTypeEnum}` = AuthUserTypeEnum.token;
if (authToken) {
@@ -133,12 +120,15 @@ export const authUser = async ({
uid = await authCookieToken(cookie, token);
authType = AuthUserTypeEnum.token;
} else if (apikey) {
uid = await parseOpenApiKey(apikey);
const parseResult = await authOpenApiKey({ apikey });
uid = parseResult.userId;
authType = AuthUserTypeEnum.apikey;
openApiKey = parseResult.apikey;
} else if (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);
@@ -158,7 +148,8 @@ export const authUser = async ({
userId: String(uid),
appId,
authType,
user
user,
apikey: openApiKey
};
};
@@ -187,7 +178,7 @@ export const authApp = async ({
1. authOwner=true or authUser = true , just owner can use
2. authUser = false and share, anyone can use
*/
if (authOwner || (authUser && !app.share.isShare)) {
if (authOwner || authUser) {
if (userId !== String(app.userId)) return Promise.reject(ERROR_ENUM.unAuthModel);
}

View File

@@ -27,7 +27,8 @@ export async function saveChat({
const chatHistory = await Chat.findOne(
{
chatId,
userId
userId,
appId
},
'_id'
);
@@ -46,7 +47,7 @@ export async function saveChat({
if (chatHistory) {
promise.push(
Chat.updateOne(
{ chatId, userId },
{ chatId, userId, appId },
{
title: content[0].value.slice(0, 20),
updateTime: new Date()

View File

@@ -24,15 +24,17 @@ export type FeConfigsType = {
show_emptyChat?: boolean;
show_register?: boolean;
show_appStore?: boolean;
show_userDetail?: boolean;
show_contact?: boolean;
show_git?: boolean;
show_doc?: boolean;
show_pay?: boolean;
show_openai_account?: boolean;
show_promotion?: boolean;
openAPIUrl?: string;
systemTitle?: string;
authorText?: string;
googleClientVerKey?: string;
isPlus?: boolean;
oauth?: {
github?: string;
google?: string;

View File

@@ -112,14 +112,6 @@ export interface PaySchema {
status: 'SUCCESS' | 'REFUND' | 'NOTPAY' | 'CLOSED';
}
export interface OpenApiSchema {
_id: string;
userId: string;
createTime: Date;
lastUsedTime?: Date;
apiKey: string;
}
export interface PromotionRecordSchema {
_id: string;
userId: string; // 收益人

View File

@@ -1,6 +0,0 @@
export interface UserOpenApiKey {
id: string;
apiKey: string;
createTime: Date;
lastUsedTime?: Date;
}

View File

@@ -0,0 +1,14 @@
export type OpenApiSchema = {
_id: string;
userId: string;
createTime: Date;
lastUsedTime?: Date;
apiKey: string;
appId?: string;
name: string;
usage: number;
limit?: {
expiredTime?: Date;
credit?: number;
};
};

View File

@@ -0,0 +1,15 @@
import { ChatHistoryItemResType } from '@/types/chat';
export function selectShareResponse({ responseData }: { responseData: ChatHistoryItemResType[] }) {
const filedList = ['moduleType', 'moduleName', 'runningTime', 'quoteList', 'question'];
return responseData.map((item) => {
const obj: Record<string, any> = {};
for (let key in item) {
if (filedList.includes(key)) {
// @ts-ignore
obj[key] = item[key];
}
}
return obj;
});
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 196 KiB

After

Width:  |  Height:  |  Size: 436 KiB

View File

@@ -3,8 +3,8 @@
"baseUrl": ".",
"paths": {
"*": [
"../../../../../Library/Caches/hugo_cache/modules/filecache/modules/pkg/mod/github.com/gohugoio/hugo-mod-jslibs-dist/popperjs/v2@v2.21100.20000/package/dist/cjs/popper.js/*",
"../../../../../Library/Caches/hugo_cache/modules/filecache/modules/pkg/mod/github.com/twbs/bootstrap@v5.3.0+incompatible/js/*"
"../../../../../.cache/hugo_cache/modules/filecache/modules/pkg/mod/github.com/gohugoio/hugo-mod-jslibs-dist/popperjs/v2@v2.21100.20000/package/dist/cjs/popper.js/*",
"../../../../../.cache/hugo_cache/modules/filecache/modules/pkg/mod/github.com/twbs/bootstrap@v5.3.0+incompatible/js/*"
]
}
}

View File

@@ -7,11 +7,15 @@ toc: true
weight: 10
---
[OpenAI 的 API 官方计费模式](https://openai.com/pricing#language-models)为:按每次 API 请求内容和返回内容 tokens 长度来定价。每个模型具有不同的计价方式,以每 1,000 个 tokens 消耗为单位定价。其中 1,000 个 tokens 约为 750 个英文单词。平台的 tokens 数量计算算法与 OpenAI 一致,您可以随时通过「使用记录」来查看余额消耗明细的说明,来对比计算是否一致。
## Tokens 说明
[OpenAI 的 API 官方计费模式](https://openai.com/pricing#language-models)为:按每次 API 请求内容和返回内容 tokens 长度来定价。每个模型具有不同的计价方式,以每 1,000 个 tokens 消耗为单位定价。其中 1,000 个 tokens 约为 900 个英文,约 600 个中文(不是很准确,与上下长度有关,相同的词出现越多,词:Tokens 的比例越大)。平台的 tokens 数量计算算法与 OpenAI 一致,您可以随时通过「使用记录」来查看余额消耗明细的说明,来对比计算是否一致。
![](/imgs/fastgpt-price.png)
以下是详细的价格表:
## FastGPT 线上计费
目前FastGPT 线上计费也仅按 Tokens 使用数量为准。以下是详细的计费表(最新定价以线上表格为准,可在点击充值后实时获取):
{{< table "table-hover table-striped-columns" >}}
| 计费项 | 价格: 元/ 1K tokens包含上下文 |

View File

@@ -11,16 +11,14 @@ weight: 322
[Feishu OpenAI GitHub 地址](https://github.com/ConnectAI-E/Feishu-OpenAI)
由于 FastGPT 的 API 接口和 OpenAI 的规范一致,可以无需变更第三方应用即可使用 FastGPT 上编排好的应用。API 使用可参考 [这篇文章](/docs/use-cases/openai/)。编排示例,可参考 [高级编排介绍](/docs/workflow/intro)
由于 FastGPT 的 API 接口和 OpenAI 的规范一致,可以无需变更第三方应用即可使用 FastGPT 上编排好的应用。API 使用可参考 [这篇文章](/docs/use-cases/openapi/)。编排示例,可参考 [高级编排介绍](/docs/workflow/intro)
## 1. 获取 FastGPT 的 OpenAPI 秘钥
依次选择应用 -> 「API 访问」然后点击「API 密钥」来创建密钥。 [参考这篇文章](/docs/use-cases/openai/)
依次选择应用 -> 「API 访问」然后点击「API 密钥」来创建密钥。 [参考这篇文章](/docs/use-cases/openapi/)
![](/imgs/fastgpt-api.png)
利用刚复制的 API 秘钥加上 AppId 组合成一个新的秘钥,格式为:`API 秘钥-AppId`,例如:`fastgpt-z51pkjqm9nrk03a1rx2funoy-642adec15f04d67d4613efdb`
## 2. 部署飞书服务
推荐使用 Railway 一键部署
@@ -35,7 +33,7 @@ FastGPT 集成**重点参数:**
```bash
#上一步FastGPT的OpenAPI 秘钥
OPENAI_KEY=fastgpt-z51pkjqm9nrk03a1rx2funoy-642adec15f04d67d4613efdb
OPENAI_KEY=fastgpt-z51pkjqm9nrk03a1rx2funoy
#调用OpenAI的BaseUrl要换成FastGPT的
API_URL=https://fastgpt.run/api/openapi
```

View File

@@ -4,14 +4,14 @@ description: "FastGPT 对接 chatgpt-on-wechat"
icon: "chat"
draft: false
toc: true
weight: 320
weight: 312
---
# 1 分钟对接 chatgpt-on-wechat
[chatgpt-on-wechat GitHub 地址](https://github.com/zhayujie/chatgpt-on-wechat)
由于 FastGPT 的 API 接口和 OpenAI 的规范一致,可以无需变更原来的应用即可使用 FastGPT 上编排好的应用。API 使用可参考 [这篇文章](/docs/use-cases/openai/)。编排示例,可参考 [高级编排介绍](/docs/workflow/intro)
由于 FastGPT 的 API 接口和 OpenAI 的规范一致,可以无需变更原来的应用即可使用 FastGPT 上编排好的应用。API 使用可参考 [这篇文章](/docs/use-cases/openapi/)。编排示例,可参考 [高级编排介绍](/docs/workflow/intro)
## 1. 获取 OpenAPI 秘钥
@@ -23,15 +23,10 @@ weight: 320
![](/imgs/fastgpt-api.png)
## 2. 组合带应用 ID 的秘钥
利用刚复制的 API 秘钥加上 AppId 组合成一个新的秘钥,格式为:`API 秘钥-AppId`,例如:`fastgpt-z51pkjqm9nrk03a1rx2funoy-642adec15f04d67d4613efdb`
这个秘钥将会调用指定的应用。
## 3. 创建 docker-compose.yml 文件
只需要修改 `OPEN_AI_API_KEY``OPEN_AI_API_BASE` 两个环境变量即可。其中 `OPEN_AI_API_KEY` 为第二步的组合秘钥,`OPEN_AI_API_BASE` 为 FastGPT 的 OpenAPI 地址,例如:`https://fastgpt.run/api/openapi/v1`
只需要修改 `OPEN_AI_API_KEY``OPEN_AI_API_BASE` 两个环境变量即可。其中 `OPEN_AI_API_KEY` 为第一步获取的秘钥,`OPEN_AI_API_BASE` 为 FastGPT 的 OpenAPI 地址,例如:`https://fastgpt.run/api/openapi/v1`
随便找一个目录,创建一个 docker-compose.yml 文件,将下面的代码复制进去。
@@ -44,7 +39,7 @@ services:
security_opt:
- seccomp:unconfined
environment:
OPEN_AI_API_KEY: 'fastgpt-z51pkjqm9nrk03a1rx2funoy-642adec15f04d67d4613efdb'
OPEN_AI_API_KEY: 'fastgpt-z51pkjqm9nrk03a1rx2funoy'
OPEN_AI_API_BASE: 'https://fastgpt.run/api/openapi/v1'
MODEL: 'gpt-3.5-turbo'
CHANNEL_TYPE: 'wx'

View File

@@ -4,7 +4,7 @@ description: "通过与 OpenAI 兼容的 API 对接第三方应用"
icon: "model_training"
draft: false
toc: true
weight: 330
weight: 311
---
## 获取 API 秘钥
@@ -17,15 +17,16 @@ weight: 330
![](/imgs/fastgpt-api.png)
## 组合秘钥
{{% alert icon="🍅" context="success" %}}
Tips: 安全起见,你可以设置一个额度或者过期时间,放置 key 被滥用。
{{% /alert %}}
利用刚复制的 API 秘钥加上 AppId 组合成一个新的秘钥,格式为:`API 秘钥-AppId`,例如:`fastgpt-z51pkjqm9nrk03a1rx2funoy-642adec15f04d67d4613efdb`
## 替换三方应用的变量
```bash
OPENAI_API_BASE_URL: https://fastgpt.run/api/openapi (改成自己部署的域名)
OPENAI_API_KEY = 组合秘钥
OPENAI_API_KEY = 上一步获取到的秘钥
```
**[ChatGPT Next Web](https://github.com/Yidadaa/ChatGPT-Next-Web) 示例:**

View File

@@ -1,6 +1,6 @@
{
"name": "fastgpt",
"version": "3.7",
"version": "4.0",
"private": true,
"scripts": {
"prepare": "husky install",