diff --git a/client/data/config.json b/client/data/config.json index 52adaf83a..5f22e1b27 100644 --- a/client/data/config.json +++ b/client/data/config.json @@ -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, diff --git a/client/package.json b/client/package.json index 267438fa8..3961cd68a 100644 --- a/client/package.json +++ b/client/package.json @@ -1,6 +1,6 @@ { "name": "fastgpt", - "version": "4.4.4", + "version": "4.4.5", "private": false, "scripts": { "dev": "next dev", diff --git a/client/public/docs/chatProblem.md b/client/public/docs/chatProblem.md index 1b5eca3ef..3135e1582 100644 --- a/client/public/docs/chatProblem.md +++ b/client/public/docs/chatProblem.md @@ -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/) **其他问题** | 交流群 | 小助手 | diff --git a/client/public/docs/versionIntro.md b/client/public/docs/versionIntro.md index a08bb7095..ee8bab486 100644 --- a/client/public/docs/versionIntro.md +++ b/client/public/docs/versionIntro.md @@ -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/) diff --git a/client/public/locales/en/common.json b/client/public/locales/en/common.json index 22049a4ab..d10d5e768 100644 --- a/client/public/locales/en/common.json +++ b/client/public/locales/en/common.json @@ -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": "" diff --git a/client/public/locales/zh/common.json b/client/public/locales/zh/common.json index 1c8ecb60f..45c60c8c5 100644 --- a/client/public/locales/zh/common.json +++ b/client/public/locales/zh/common.json @@ -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": "好友注册" diff --git a/client/src/api/openapi.ts b/client/src/api/openapi.ts deleted file mode 100644 index 83ba6ff8c..000000000 --- a/client/src/api/openapi.ts +++ /dev/null @@ -1,16 +0,0 @@ -import { GET, POST, DELETE } from './request'; -import { UserOpenApiKey } from '@/types/openapi'; -/** - * crete a api key - */ -export const createAOpenApiKey = () => POST('/openapi/postKey'); - -/** - * get api keys - */ -export const getOpenApiKeys = () => GET('/openapi/getKeys'); - -/** - * delete api by id - */ -export const delOpenApiById = (id: string) => DELETE(`/openapi/delKey?id=${id}`); diff --git a/client/src/api/support/openapi/index.d.ts b/client/src/api/support/openapi/index.d.ts new file mode 100644 index 000000000..b2d3fb66d --- /dev/null +++ b/client/src/api/support/openapi/index.d.ts @@ -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']; +}; diff --git a/client/src/api/support/openapi/index.ts b/client/src/api/support/openapi/index.ts new file mode 100644 index 000000000..8b2a17b6f --- /dev/null +++ b/client/src/api/support/openapi/index.ts @@ -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('/support/openapi/postKey', data); + +/** + * update a api key + */ +export const putOpenApiKey = (data: EditApiKeyProps & { _id: string }) => + POST('/support/openapi/putKey', data); + +/** + * get api keys + */ +export const getOpenApiKeys = (params?: GetApiKeyProps) => + GET('/support/openapi/getKeys', params); + +/** + * delete api by id + */ +export const delOpenApiById = (id: string) => DELETE(`/support/openapi/delKey?id=${id}`); diff --git a/client/src/components/APIKeyModal/index.tsx b/client/src/components/APIKeyModal/index.tsx deleted file mode 100644 index c3d1dde8d..000000000 --- a/client/src/components/APIKeyModal/index.tsx +++ /dev/null @@ -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 ( - - - - API 秘钥管理 - - - 如果你不想 API 秘钥被滥用,请勿将秘钥直接放置在前端使用~ - - - - - - - - - - - - - - {apiKeys.map(({ id, apiKey, createTime, lastUsedTime }) => ( - - - - - - - ))} - -
Api Key创建时间最后一次使用时间 -
{apiKey}{dayjs(createTime).format('YYYY/MM/DD HH:mm:ss')} - {lastUsedTime - ? dayjs(lastUsedTime).format('YYYY/MM/DD HH:mm:ss') - : '没有使用过'} - - } - size={'xs'} - aria-label={'delete'} - variant={'base'} - colorScheme={'gray'} - onClick={() => onclickRemove(id)} - /> -
-
-
- - - - - - - setApiKey('')}> - - - 新的 API 秘钥 - - - 请保管好你的秘钥,秘钥不会再次展示~ - - - - copyData(apiKey)}> - {apiKey} - - - - - - - -
- ); -}; - -export default APIKeyModal; diff --git a/client/src/components/ChatBox/WholeResponseModal.tsx b/client/src/components/ChatBox/WholeResponseModal.tsx index 6c8eb9639..79785db1b 100644 --- a/client/src/components/ChatBox/WholeResponseModal.tsx +++ b/client/src/components/ChatBox/WholeResponseModal.tsx @@ -89,10 +89,12 @@ const ResponseModal = ({ - + {activeModule?.price !== undefined && ( + + )} + + + \ No newline at end of file diff --git a/client/src/components/Icon/index.tsx b/client/src/components/Icon/index.tsx index 3e32c6c64..5cb0840de 100644 --- a/client/src/components/Icon/index.tsx +++ b/client/src/components/Icon/index.tsx @@ -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'), diff --git a/client/src/components/Markdown/chat/Guide.tsx b/client/src/components/Markdown/chat/Guide.tsx index de1447d72..c8bbde772 100644 --- a/client/src/components/Markdown/chat/Guide.tsx +++ b/client/src/components/Markdown/chat/Guide.tsx @@ -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,24 +10,28 @@ 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 ( - - { - if (href) { - return window.open(href, '_blank'); - } - event.emit('guideClick', { text }); - }} - > - {text} + + return !!href ? ( + + {text} + + ) : ( + + + { + event.emit('guideClick', { text }); + }} + > + {text} + ); @@ -42,7 +46,7 @@ const Guide = ({ text }: { text: string }) => { remarkPlugins={[RemarkGfm, RemarkMath]} rehypePlugins={[RehypeKatex]} components={{ - a: Link, + a: MyLink, img: Image }} > diff --git a/client/src/components/support/apikey/Table.tsx b/client/src/components/support/apikey/Table.tsx new file mode 100644 index 000000000..c5254fccb --- /dev/null +++ b/client/src/components/support/apikey/Table.tsx @@ -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(); + 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 ( + + + + + + API 秘钥管理 + + + 查看文档 + + + + {tips} + + + copyData(baseUrl, '已复制 API 地址')} + > + + API地址 + + + {baseUrl} + + + + + + + + + + + + + + {feConfigs?.isPlus && ( + <> + + + + )} + + + + + + + {apiKeys.map(({ _id, name, usage, limit, apiKey, createTime, lastUsedTime }) => ( + + + + + {feConfigs?.isPlus && ( + <> + + + + )} + + + + + ))} + +
{t('Name')}Api Key已用额度(¥)最大额度(¥)过期时间创建时间最后一次使用时间 +
{name}{apiKey}{usage}{limit?.credit && limit?.credit > -1 ? `${limit?.credit}` : '无限制'} + {limit?.expiredTime + ? dayjs(limit?.expiredTime).format('YYYY/MM/DD\nHH:mm') + : '-'} + {dayjs(createTime).format('YYYY/MM/DD\nHH:mm:ss')} + {lastUsedTime ? dayjs(lastUsedTime).format('YYYY/MM/DD\nHH:mm:ss') : '没有使用过'} + + + + + + + + setEditData({ + _id, + name, + limit, + appId + }) + } + py={[2, 3]} + > + + {t('common.Edit')} + + onclickRemove(_id)} py={[2, 3]}> + + {t('common.Delete')} + + + +
+ +
+ {!!editData && ( + setEditData(undefined)} + onCreate={(id) => { + setApiKey(id); + refetch(); + setEditData(undefined); + }} + onEdit={() => { + refetch(); + setEditData(undefined); + }} + /> + )} + setApiKey('')}> + + + 新的 API 秘钥 + + + 请保管好你的秘钥,秘钥不会再次展示~ + + + + copyData(apiKey)} + > + {apiKey} + + + + + + + +
+ ); +}; + +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 ( + + + + {t('Name')}: + + + {feConfigs?.isPlus && ( + <> + + + {t('common.Max credit')}: + + + + + + + + + {t('common.Expired Time')}: + + { + setValue('limit.expiredTime', new Date(e.target.value)); + }} + /> + + + )} + + + + + + + + + ); +} diff --git a/client/src/constants/chat.ts b/client/src/constants/chat.ts index 473873978..d00a24401 100644 --- a/client/src/constants/chat.ts +++ b/client/src/constants/chat.ts @@ -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`; diff --git a/client/src/pages/account/components/ApiKeyTable.tsx b/client/src/pages/account/components/ApiKeyTable.tsx new file mode 100644 index 000000000..4ca5bada8 --- /dev/null +++ b/client/src/pages/account/components/ApiKeyTable.tsx @@ -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 ; +}; + +export default ApiKey; diff --git a/client/src/pages/account/components/Info.tsx b/client/src/pages/account/components/Info.tsx index a63f3484c..2db4506de 100644 --- a/client/src/pages/account/components/Info.tsx +++ b/client/src/pages/account/components/Info.tsx @@ -210,19 +210,19 @@ const UserInfo = () => { {t('user.Change')} - {feConfigs?.show_userDetail && ( - - - {t('user.Balance')}:  - - {userInfo?.balance.toFixed(3)} 元 - + + + {t('user.Balance')}:  + + {userInfo?.balance.toFixed(3)} 元 + + {feConfigs?.show_pay && ( - - - )} + )} + + {feConfigs?.show_doc && ( <> 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 && } {currentTab === TabEnum.pay && } {currentTab === TabEnum.inform && } + {currentTab === TabEnum.apikey && }
diff --git a/client/src/pages/api/app/getChatLogs.ts b/client/src/pages/api/app/getChatLogs.ts index c9a9d4c76..80ef7cd19 100644 --- a/client/src/pages/api/app/getChatLogs.ts +++ b/client/src/pages/api/app/getChatLogs.ts @@ -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' } }, diff --git a/client/src/pages/api/chat/chatTest.ts b/client/src/pages/api/chat/chatTest.ts index b6fe741d3..3e4d3cacd 100644 --- a/client/src/pages/api/chat/chatTest.ts +++ b/client/src/pages/api/chat/chatTest.ts @@ -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, diff --git a/client/src/pages/api/chat/init.ts b/client/src/pages/api/chat/init.ts index a29aa6f7b..523ca2b18 100644 --- a/client/src/pages/api/chat/init.ts +++ b/client/src/pages/api/chat/init.ts @@ -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}` ) diff --git a/client/src/pages/api/openapi/getKeys.ts b/client/src/pages/api/openapi/getKeys.ts deleted file mode 100644 index dafb3c3e6..000000000 --- a/client/src/pages/api/openapi/getKeys.ts +++ /dev/null @@ -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( - ({ _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 - }); - } -} diff --git a/client/src/pages/api/openapi/v1/chat/completions.ts b/client/src/pages/api/openapi/v1/chat/completions.ts index 7f6ec43ee..e99ac1b27 100644 --- a/client/src/pages/api/openapi/v1/chat/completions.ts +++ b/client/src/pages/api/openapi/v1/chat/completions.ts @@ -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,9 +257,19 @@ 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) { sseErrRes(res, err); diff --git a/client/src/pages/api/openapi/delKey.ts b/client/src/pages/api/support/openapi/delKey.ts similarity index 100% rename from client/src/pages/api/openapi/delKey.ts rename to client/src/pages/api/support/openapi/delKey.ts diff --git a/client/src/pages/api/support/openapi/getKeys.ts b/client/src/pages/api/support/openapi/getKeys.ts new file mode 100644 index 000000000..bb344f08e --- /dev/null +++ b/client/src/pages/api/support/openapi/getKeys.ts @@ -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 + }); + } +} diff --git a/client/src/pages/api/openapi/postKey.ts b/client/src/pages/api/support/openapi/postKey.ts similarity index 67% rename from client/src/pages/api/openapi/postKey.ts rename to client/src/pages/api/support/openapi/postKey.ts index 1f3a9efee..e0773b3c6 100644 --- a/client/src/pages/api/openapi/postKey.ts +++ b/client/src/pages/api/support/openapi/postKey.ts @@ -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, { diff --git a/client/src/pages/api/support/openapi/putKey.ts b/client/src/pages/api/support/openapi/putKey.ts new file mode 100644 index 000000000..2de79688e --- /dev/null +++ b/client/src/pages/api/support/openapi/putKey.ts @@ -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 + }); + } +} diff --git a/client/src/pages/api/system/getInitData.ts b/client/src/pages/api/system/getInitData.ts index b947e42f9..6abe9691a 100644 --- a/client/src/pages/api/system/getInitData.ts +++ b/client/src/pages/api/system/getInitData.ts @@ -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, diff --git a/client/src/pages/app/detail/components/API.tsx b/client/src/pages/app/detail/components/API.tsx deleted file mode 100644 index 79aa84cb3..000000000 --- a/client/src/pages/app/detail/components/API.tsx +++ /dev/null @@ -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 ( - - - - AppId: - copyData(appId, '已复制 AppId')} - > - {appId} - - - {isPc && ( - <> - copyData(baseUrl, '已复制 API 地址')} - > - - API服务器 - - - {baseUrl} - - - - - )} - - - - -