mirror of
https://github.com/labring/FastGPT.git
synced 2025-07-23 05:12:39 +00:00
perf: api key (#342)
This commit is contained in:
@@ -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,
|
||||
|
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "fastgpt",
|
||||
"version": "4.4.4",
|
||||
"version": "4.4.5",
|
||||
"private": false,
|
||||
"scripts": {
|
||||
"dev": "next dev",
|
||||
|
@@ -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/)
|
||||
|
||||
**其他问题**
|
||||
| 交流群 | 小助手 |
|
||||
|
@@ -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/)
|
||||
|
@@ -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": ""
|
||||
|
@@ -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": "好友注册"
|
||||
|
@@ -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}`);
|
11
client/src/api/support/openapi/index.d.ts
vendored
Normal file
11
client/src/api/support/openapi/index.d.ts
vendored
Normal 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'];
|
||||
};
|
26
client/src/api/support/openapi/index.ts
Normal file
26
client/src/api/support/openapi/index.ts
Normal 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}`);
|
@@ -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;
|
@@ -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`}
|
||||
|
8
client/src/components/Icon/icons/fill/apikey.svg
Normal file
8
client/src/components/Icon/icons/fill/apikey.svg
Normal 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 |
@@ -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'),
|
||||
|
@@ -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
|
||||
}}
|
||||
>
|
||||
|
362
client/src/components/support/apikey/Table.tsx
Normal file
362
client/src/components/support/apikey/Table.tsx
Normal 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>
|
||||
);
|
||||
}
|
@@ -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`;
|
||||
|
10
client/src/pages/account/components/ApiKeyTable.tsx
Normal file
10
client/src/pages/account/components/ApiKeyTable.tsx
Normal 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;
|
@@ -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')}: </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
|
||||
|
@@ -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 />
|
||||
|
@@ -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'
|
||||
}
|
||||
},
|
||||
|
@@ -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,
|
||||
|
@@ -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}`
|
||||
)
|
||||
|
@@ -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
|
||||
});
|
||||
}
|
||||
}
|
@@ -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) {
|
||||
|
25
client/src/pages/api/support/openapi/getKeys.ts
Normal file
25
client/src/pages/api/support/openapi/getKeys.ts
Normal 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
|
||||
});
|
||||
}
|
||||
}
|
@@ -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, {
|
32
client/src/pages/api/support/openapi/putKey.ts
Normal file
32
client/src/pages/api/support/openapi/putKey.ts
Normal 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
|
||||
});
|
||||
}
|
||||
}
|
@@ -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,
|
||||
|
@@ -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;
|
@@ -73,7 +73,7 @@ const NodeCard = (props: Props) => {
|
||||
|
||||
{
|
||||
icon: 'back',
|
||||
label: t('common.Cancel'),
|
||||
label: t('Cancel'),
|
||||
onClick: () => {}
|
||||
}
|
||||
],
|
||||
|
@@ -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}
|
||||
|
10
client/src/pages/app/detail/components/OutLink/API.tsx
Normal file
10
client/src/pages/app/detail/components/OutLink/API.tsx
Normal 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;
|
@@ -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);
|
56
client/src/pages/app/detail/components/OutLink/index.tsx
Normal file
56
client/src/pages/app/detail/components/OutLink/index.tsx
Normal 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;
|
@@ -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>
|
||||
|
@@ -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')}
|
||||
|
@@ -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 }) => {
|
||||
|
@@ -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
|
||||
}
|
||||
};
|
||||
|
@@ -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);
|
@@ -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';
|
||||
|
38
client/src/service/support/openapi/auth.ts
Normal file
38
client/src/service/support/openapi/auth.ts
Normal 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);
|
||||
}
|
||||
}
|
18
client/src/service/support/openapi/index.ts
Normal file
18
client/src/service/support/openapi/index.ts
Normal 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
|
||||
}
|
||||
}
|
||||
);
|
||||
}
|
57
client/src/service/support/openapi/schema.ts
Normal file
57
client/src/service/support/openapi/schema.ts
Normal 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);
|
22
client/src/service/support/outLink/index.ts
Normal file
22
client/src/service/support/outLink/index.ts
Normal 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);
|
||||
}
|
||||
};
|
@@ -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);
|
||||
}
|
||||
|
||||
|
@@ -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()
|
||||
|
4
client/src/types/index.d.ts
vendored
4
client/src/types/index.d.ts
vendored
@@ -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;
|
||||
|
8
client/src/types/mongoSchema.d.ts
vendored
8
client/src/types/mongoSchema.d.ts
vendored
@@ -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; // 收益人
|
||||
|
6
client/src/types/openapi.d.ts
vendored
6
client/src/types/openapi.d.ts
vendored
@@ -1,6 +0,0 @@
|
||||
export interface UserOpenApiKey {
|
||||
id: string;
|
||||
apiKey: string;
|
||||
createTime: Date;
|
||||
lastUsedTime?: Date;
|
||||
}
|
14
client/src/types/support/openapi.ts
Normal file
14
client/src/types/support/openapi.ts
Normal 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;
|
||||
};
|
||||
};
|
15
client/src/utils/service/core/chat/index.ts
Normal file
15
client/src/utils/service/core/chat/index.ts
Normal 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 |
@@ -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/*"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
@@ -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 一致,您可以随时通过「使用记录」来查看余额消耗明细的说明,来对比计算是否一致。
|
||||
|
||||

|
||||
|
||||
以下是详细的价格表:
|
||||
|
||||
## FastGPT 线上计费
|
||||
|
||||
目前,FastGPT 线上计费也仅按 Tokens 使用数量为准。以下是详细的计费表(最新定价以线上表格为准,可在点击充值后实时获取):
|
||||
|
||||
{{< table "table-hover table-striped-columns" >}}
|
||||
| 计费项 | 价格: 元/ 1K tokens(包含上下文) |
|
||||
|
@@ -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/)
|
||||
|
||||

|
||||
|
||||
利用刚复制的 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
|
||||
```
|
||||
|
@@ -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
|
||||
|
||||

|
||||
|
||||
## 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'
|
||||
|
@@ -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
|
||||
|
||||

|
||||
|
||||
## 组合秘钥
|
||||
{{% 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) 示例:**
|
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "fastgpt",
|
||||
"version": "3.7",
|
||||
"version": "4.0",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"prepare": "husky install",
|
||||
|
Reference in New Issue
Block a user