Commit 81a06718 by Archer Committed by GitHub

feat: ai proxy v1 (#3898)

* feat: ai proxy v1

* perf: ai proxy channel crud

* feat: ai proxy logs

* feat: channel test

* doc

* update lock
parent 3c382d12
......@@ -11,6 +11,7 @@ weight: 802
## 🚀 新增内容
1. 增加默认“知识库文本理解模型”配置
2. AI proxy V1版,可替换 OneAPI使用,同时提供完整模型调用日志,便于排查问题。
## ⚙️ 优化
......@@ -18,8 +19,11 @@ weight: 802
2. 集合列表数据统计方式,提高大数据量统计性能。
3. 优化数学公式,转义 Latex 格式成 Markdown 格式。
4. 解析文档图片,图片太大时,自动忽略。
5. 时间选择器,当天开始时间自动设0,结束设置设 23:59:59,避免 UI 与实际逻辑偏差。
6. 升级 mongoose 库版本依赖。
## 🐛 修复
1. 标签过滤时,子文件夹未成功过滤。
2. 暂时移除 md 阅读优化,避免链接分割错误。
\ No newline at end of file
2. 暂时移除 md 阅读优化,避免链接分割错误。
3. 离开团队时,未刷新成员列表。
\ No newline at end of file
......@@ -7,12 +7,14 @@ import { i18nT } from '../../../web/i18n/utils';
dayjs.extend(utc);
dayjs.extend(timezone);
export const formatTime2YMDHMW = (time?: Date) => dayjs(time).format('YYYY-MM-DD HH:mm:ss dddd');
export const formatTime2YMDHMS = (time?: Date) =>
export const formatTime2YMDHMW = (time?: Date | number) =>
dayjs(time).format('YYYY-MM-DD HH:mm:ss dddd');
export const formatTime2YMDHMS = (time?: Date | number) =>
time ? dayjs(time).format('YYYY-MM-DD HH:mm:ss') : '';
export const formatTime2YMDHM = (time?: Date) =>
export const formatTime2YMDHM = (time?: Date | number) =>
time ? dayjs(time).format('YYYY-MM-DD HH:mm') : '';
export const formatTime2YMD = (time?: Date) => (time ? dayjs(time).format('YYYY-MM-DD') : '');
export const formatTime2YMD = (time?: Date | number) =>
time ? dayjs(time).format('YYYY-MM-DD') : '';
export const formatTime2HM = (time: Date = new Date()) => dayjs(time).format('HH:mm');
/**
......
......@@ -54,6 +54,7 @@ export type FastGPTFeConfigsType = {
show_promotion?: boolean;
show_team_chat?: boolean;
show_compliance_copywriting?: boolean;
show_aiproxy?: boolean;
concatMd?: string;
docUrl?: string;
......
......@@ -11,14 +11,17 @@ import { i18nT } from '../../../web/i18n/utils';
import { OpenaiAccountType } from '@fastgpt/global/support/user/team/type';
import { getLLMModel } from './model';
export const openaiBaseUrl = process.env.OPENAI_BASE_URL || 'https://api.openai.com/v1';
const aiProxyBaseUrl = process.env.AIPROXY_API_ENDPOINT
? `${process.env.AIPROXY_API_ENDPOINT}/v1`
: undefined;
const openaiBaseUrl = aiProxyBaseUrl || process.env.OPENAI_BASE_URL || 'https://api.openai.com/v1';
const openaiBaseKey = process.env.AIPROXY_API_TOKEN || process.env.CHAT_API_KEY || '';
export const getAIApi = (props?: { userKey?: OpenaiAccountType; timeout?: number }) => {
const { userKey, timeout } = props || {};
const baseUrl = userKey?.baseUrl || global?.systemEnv?.oneapiUrl || openaiBaseUrl;
const apiKey = userKey?.key || global?.systemEnv?.chatApiKey || process.env.CHAT_API_KEY || '';
const apiKey = userKey?.key || global?.systemEnv?.chatApiKey || openaiBaseKey;
return new OpenAI({
baseURL: baseUrl,
apiKey,
......@@ -72,6 +75,7 @@ export const createChatCompletion = async ({
userKey,
timeout: formatTimeout
});
const response = await ai.chat.completions.create(body, {
...options,
...(modelConstantsData.requestUrl ? { path: modelConstantsData.requestUrl } : {}),
......
......@@ -15,7 +15,7 @@ import { TeamDefaultPermissionVal } from '@fastgpt/global/support/permission/use
import { MongoMemberGroupModel } from '../../permission/memberGroup/memberGroupSchema';
import { mongoSessionRun } from '../../../common/mongo/sessionRun';
import { DefaultGroupName } from '@fastgpt/global/support/user/team/group/constant';
import { getAIApi, openaiBaseUrl } from '../../../core/ai/config';
import { getAIApi } from '../../../core/ai/config';
import { createRootOrg } from '../../permission/org/controllers';
import { refreshSourceAvatar } from '../../../common/file/image/controller';
......@@ -152,7 +152,7 @@ export async function updateTeam({
// auth openai key
if (openaiAccount?.key) {
console.log('auth user openai key', openaiAccount?.key);
const baseUrl = openaiAccount?.baseUrl || openaiBaseUrl;
const baseUrl = openaiAccount?.baseUrl || 'https://api.openai.com/v1';
openaiAccount.baseUrl = baseUrl;
const ai = getAIApi({
......
......@@ -100,6 +100,13 @@ const DateRangePicker = ({
if (date?.to === undefined) {
date.to = date.from;
}
if (date?.from) {
date.from = new Date(date.from.setHours(0, 0, 0, 0));
}
if (date?.to) {
date.to = new Date(date.to.setHours(23, 59, 59, 999));
}
setRange(date);
onChange?.(date);
}}
......
// @ts-nocheck
export const iconPaths = {
book: () => import('./icons/book.svg'),
change: () => import('./icons/change.svg'),
......@@ -32,8 +33,10 @@ export const iconPaths = {
'common/customTitleLight': () => import('./icons/common/customTitleLight.svg'),
'common/data': () => import('./icons/common/data.svg'),
'common/dingtalkFill': () => import('./icons/common/dingtalkFill.svg'),
'common/disable': () => import('./icons/common/disable.svg'),
'common/downArrowFill': () => import('./icons/common/downArrowFill.svg'),
'common/editor/resizer': () => import('./icons/common/editor/resizer.svg'),
'common/enable': () => import('./icons/common/enable.svg'),
'common/errorFill': () => import('./icons/common/errorFill.svg'),
'common/file/move': () => import('./icons/common/file/move.svg'),
'common/folderFill': () => import('./icons/common/folderFill.svg'),
......
<svg t="1740494996853" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="2899" width="64" height="64"><path d="M512 953.6a441.6 441.6 0 1 1 0-883.2 441.6 441.6 0 0 1 0 883.2z m0-64a377.6 377.6 0 1 0 0-755.2 377.6 377.6 0 0 0 0 755.2z" p-id="2900"></path><path d="M182.1696 227.4304l45.2608-45.2608 614.4 614.4-45.2608 45.2608z" p-id="2901"></path></svg>
\ No newline at end of file
<svg t="1740495050372" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="4745" width="64" height="64"><path d="M510.2 959.7c-246.9-1-447-202.6-446-449.5s202.6-447 449.5-446 447 202.6 446 449.5-202.6 447-449.5 446z m3.3-833.7c-212.8-0.8-386.7 171.7-387.5 384.5S297.7 897.2 510.5 898 897.2 726.3 898 513.5 726.3 126.8 513.5 126z" p-id="4746"></path><path d="M465.8 712.3L291.1 537.6l43.7-43.7 131 131 262-262 43.7 43.7z" p-id="4747"></path></svg>
\ No newline at end of file
......@@ -10,8 +10,9 @@ import React from 'react';
import MyIcon from '../../Icon';
import { UseFormRegister } from 'react-hook-form';
type Props = Omit<NumberInputProps, 'onChange'> & {
type Props = Omit<NumberInputProps, 'onChange' | 'onBlur'> & {
onChange?: (e?: number) => any;
onBlur?: (e?: number) => any;
placeholder?: string;
register?: UseFormRegister<any>;
name?: string;
......@@ -19,11 +20,21 @@ type Props = Omit<NumberInputProps, 'onChange'> & {
};
const MyNumberInput = (props: Props) => {
const { register, name, onChange, placeholder, bg, ...restProps } = props;
const { register, name, onChange, onBlur, placeholder, bg, ...restProps } = props;
return (
<NumberInput
{...restProps}
onBlur={(e) => {
if (!onBlur) return;
const numE = Number(e.target.value);
if (isNaN(numE)) {
// @ts-ignore
onBlur('');
} else {
onBlur(numE);
}
}}
onChange={(e) => {
if (!onChange) return;
const numE = Number(e);
......@@ -38,6 +49,8 @@ const MyNumberInput = (props: Props) => {
<NumberInputField
bg={bg}
placeholder={placeholder}
h={restProps.h}
defaultValue={restProps.defaultValue}
{...(register && name
? register(name, {
required: props.isRequired,
......
......@@ -98,7 +98,6 @@ const MultipleSelect = <T = any,>({
return (
<MenuItem
key={i}
{...menuItemStyles}
{...(isSelected
? {
color: 'primary.600'
......@@ -114,6 +113,7 @@ const MultipleSelect = <T = any,>({
whiteSpace={'pre-wrap'}
fontSize={'sm'}
gap={2}
{...menuItemStyles}
>
<Checkbox isChecked={isSelected} />
{item.icon && <MyAvatar src={item.icon} w={'1rem'} borderRadius={'0'} />}
......@@ -204,6 +204,7 @@ const MultipleSelect = <T = any,>({
}}
onClick={(e) => {
e.stopPropagation();
e.preventDefault();
onclickItem(item.value);
}}
/>
......@@ -230,7 +231,6 @@ const MultipleSelect = <T = any,>({
overflowY={'auto'}
>
<MenuItem
{...menuItemStyles}
color={isSelectAll ? 'primary.600' : 'myGray.900'}
onClick={(e) => {
e.stopPropagation();
......@@ -241,6 +241,7 @@ const MultipleSelect = <T = any,>({
fontSize={'sm'}
gap={2}
mb={1}
{...menuItemStyles}
>
<Checkbox isChecked={isSelectAll} />
<Box flex={'1 0 0'}>{t('common:common.All')}</Box>
......
......@@ -4,7 +4,8 @@ import React, {
useMemo,
useEffect,
useImperativeHandle,
ForwardedRef
ForwardedRef,
useState
} from 'react';
import {
Menu,
......@@ -15,7 +16,8 @@ import {
MenuButton,
Box,
css,
Flex
Flex,
Input
} from '@chakra-ui/react';
import type { ButtonProps, MenuItemProps } from '@chakra-ui/react';
import MyIcon from '../Icon';
......@@ -33,8 +35,10 @@ import { useScrollPagination } from '../../../hooks/useScrollPagination';
export type SelectProps<T = any> = ButtonProps & {
value?: T;
placeholder?: string;
isSearch?: boolean;
list: {
alias?: string;
icon?: string;
label: string | React.ReactNode;
description?: string;
value: T;
......@@ -49,6 +53,7 @@ const MySelect = <T = any,>(
{
placeholder,
value,
isSearch = false,
width = '100%',
list = [],
onchange,
......@@ -63,6 +68,7 @@ const MySelect = <T = any,>(
const ButtonRef = useRef<HTMLButtonElement>(null);
const MenuListRef = useRef<HTMLDivElement>(null);
const SelectedItemRef = useRef<HTMLDivElement>(null);
const SearchInputRef = useRef<HTMLInputElement>(null);
const menuItemStyles: MenuItemProps = {
borderRadius: 'sm',
......@@ -79,6 +85,18 @@ const MySelect = <T = any,>(
const { isOpen, onOpen, onClose } = useDisclosure();
const selectItem = useMemo(() => list.find((item) => item.value === value), [list, value]);
const [search, setSearch] = useState('');
const filterList = useMemo(() => {
if (!isSearch || !search) {
return list;
}
return list.filter((item) => {
const text = `${item.label?.toString()}${item.alias}${item.value}`;
const regx = new RegExp(search, 'gi');
return regx.test(text);
});
}, [list, search, isSearch]);
useImperativeHandle(ref, () => ({
focus() {
onOpen();
......@@ -90,17 +108,19 @@ const MySelect = <T = any,>(
const menu = MenuListRef.current;
const selectedItem = SelectedItemRef.current;
menu.scrollTop = selectedItem.offsetTop - menu.offsetTop - 100;
if (isSearch) {
setSearch('');
}
}
}, [isOpen]);
}, [isSearch, isOpen]);
const { runAsync: onChange, loading } = useRequest2((val: T) => onchange?.(val));
const isSelecting = loading || isLoading;
const ListRender = useMemo(() => {
return (
<>
{list.map((item, i) => (
{filterList.map((item, i) => (
<Box key={i}>
<MenuItem
{...menuItemStyles}
......@@ -123,7 +143,10 @@ const MySelect = <T = any,>(
fontSize={'sm'}
display={'block'}
>
<Box>{item.label}</Box>
<Flex alignItems={'center'}>
{item.icon && <MyIcon mr={2} name={item.icon as any} w={'1rem'} />}
{item.label}
</Flex>
{item.description && (
<Box color={'myGray.500'} fontSize={'xs'}>
{item.description}
......@@ -135,7 +158,9 @@ const MySelect = <T = any,>(
))}
</>
);
}, [list, value]);
}, [filterList, value]);
const isSelecting = loading || isLoading;
return (
<Box
......@@ -176,8 +201,33 @@ const MySelect = <T = any,>(
{...props}
>
<Flex alignItems={'center'}>
{isSelecting && <MyIcon mr={2} name={'common/loading'} w={'16px'} />}
{selectItem?.alias || selectItem?.label || placeholder}
{isSelecting && <MyIcon mr={2} name={'common/loading'} w={'1rem'} />}
{isSearch && isOpen ? (
<Input
ref={SearchInputRef}
autoFocus
variant={'unstyled'}
value={search}
onChange={(e) => setSearch(e.target.value)}
placeholder={
selectItem?.alias ||
(typeof selectItem?.label === 'string' ? selectItem?.label : placeholder)
}
size={'sm'}
w={'100%'}
color={'myGray.700'}
onBlur={() => {
setTimeout(() => {
SearchInputRef?.current?.focus();
}, 0);
}}
/>
) : (
<>
{selectItem?.icon && <MyIcon mr={2} name={selectItem.icon as any} w={'1rem'} />}
{selectItem?.alias || selectItem?.label || placeholder}
</>
)}
</Flex>
</MenuButton>
......
......@@ -217,7 +217,7 @@ export function useScrollPagination<
const offset = init ? 0 : data.length;
setTrue();
console.log(offset);
try {
const res = await api({
offset,
......
{
"api_key": "API key",
"azure": "Azure",
"base_url": "Base url",
"channel_name": "Channel",
"channel_priority": "Priority",
"channel_priority_tip": "The higher the priority channel, the easier it is to be requested",
"channel_status": "state",
"channel_status_auto_disabled": "Automatically disable",
"channel_status_disabled": "Disabled",
"channel_status_enabled": "Enable",
"channel_status_unknown": "unknown",
"channel_type": "Manufacturer",
"clear_model": "Clear the model",
"copy_model_id_success": "Copyed model id",
"create_channel": "Added channels",
"default_url": "Default address",
"detail": "Detail",
"duration": "Duration",
"edit": "edit",
"edit_channel": "Channel configuration",
"enable_channel": "Enable",
"forbid_channel": "Disabled",
"key_type": "API key format:",
"log": "Call log",
"log_detail": "Log details",
"log_status": "Status",
"mapping": "Model Mapping",
"mapping_tip": "A valid Json is required. \nThe model can be mapped when sending a request to the actual address. \nFor example:\n{\n \n \"gpt-4o\": \"gpt-4o-test\"\n\n}\n\nWhen FastGPT requests the gpt-4o model, the gpt-4o-test model is sent to the actual address, instead of gpt-4o.",
"model": "Model",
"model_name": "Model name",
"model_test": "Model testing",
"model_tokens": "Input/Output tokens",
"request_at": "Request time",
"request_duration": "Request duration: {{duration}}s",
"running_test": "In testing",
"search_model": "Search for models",
"select_channel": "Select a channel name",
"select_model": "Select a model",
"select_model_placeholder": "Select the model available under this channel",
"select_provider_placeholder": "Search for manufacturers",
"selected_model_empty": "Choose at least one model",
"start_test": "Start testing {{num}} models",
"test_failed": "There are {{num}} models that report errors",
"waiting_test": "Waiting for testing"
}
......@@ -125,7 +125,6 @@
"common.Copy Successful": "Copied Successfully",
"common.Copy_failed": "Copy Failed, Please Copy Manually",
"common.Create Failed": "Creation Failed",
"common.Create New": "Create",
"common.Create Success": "Created Successfully",
"common.Create Time": "Creation Time",
"common.Creating": "Creating",
......
......@@ -3,7 +3,7 @@
"add_default_model": "添加预设模型",
"api_key": "API 密钥",
"bills_and_invoices": "账单与发票",
"channel": "渠道",
"channel": "模型渠道",
"config_model": "模型配置",
"confirm_logout": "确认退出登录?",
"create_channel": "新增渠道",
......
{
"api_key": "API 密钥",
"azure": "微软 Azure",
"base_url": "代理地址",
"channel_name": "渠道名",
"channel_priority": "优先级",
"channel_priority_tip": "优先级越高的渠道,越容易被请求到",
"channel_status": "状态",
"channel_status_auto_disabled": "自动禁用",
"channel_status_disabled": "禁用",
"channel_status_enabled": "启用",
"channel_status_unknown": "未知",
"channel_type": "厂商",
"clear_model": "清空模型",
"copy_model_id_success": "已复制模型id",
"create_channel": "新增渠道",
"default_url": "默认地址",
"detail": "详情",
"duration": "耗时",
"edit": "编辑",
"edit_channel": "渠道配置",
"enable_channel": "启用",
"forbid_channel": "禁用",
"key_type": "API key 格式: ",
"log": "调用日志",
"log_detail": "日志详情",
"log_status": "状态",
"mapping": "模型映射",
"mapping_tip": "需填写一个有效 Json。可在向实际地址发送请求时,对模型进行映射。例如:\n{\n \"gpt-4o\": \"gpt-4o-test\"\n}\n当 FastGPT 请求 gpt-4o 模型时,会向实际地址发送 gpt-4o-test 的模型,而不是 gpt-4o。",
"model": "模型",
"model_name": "模型名",
"model_test": "模型测试",
"model_tokens": "输入/输出 Tokens",
"request_at": "请求时间",
"request_duration": "请求时长: {{duration}}s",
"running_test": "测试中",
"search_model": "搜索模型",
"select_channel": "选择渠道名",
"select_model": "选择模型",
"select_model_placeholder": "选择该渠道下可用的模型",
"select_provider_placeholder": "搜索厂商",
"selected_model_empty": "至少选择一个模型",
"start_test": "开始测试{{num}}个模型",
"test_failed": "有{{num}}个模型报错",
"waiting_test": "等待测试"
}
......@@ -129,7 +129,6 @@
"common.Copy Successful": "复制成功",
"common.Copy_failed": "复制失败,请手动复制",
"common.Create Failed": "创建异常",
"common.Create New": "新建",
"common.Create Success": "创建成功",
"common.Create Time": "创建时间",
"common.Creating": "创建中",
......
......@@ -3,7 +3,7 @@
"add_default_model": "新增預設模型",
"api_key": "API 金鑰",
"bills_and_invoices": "帳單與發票",
"channel": "頻道",
"channel": "模型渠道",
"config_model": "模型配置",
"confirm_logout": "確認登出登入?",
"create_channel": "新增頻道",
......
{
"api_key": "API 密鑰",
"azure": "Azure",
"base_url": "代理地址",
"channel_name": "渠道名",
"channel_priority": "優先級",
"channel_priority_tip": "優先級越高的渠道,越容易被請求到",
"channel_status": "狀態",
"channel_status_auto_disabled": "自動禁用",
"channel_status_disabled": "禁用",
"channel_status_enabled": "啟用",
"channel_status_unknown": "未知",
"channel_type": "廠商",
"clear_model": "清空模型",
"copy_model_id_success": "已復制模型id",
"create_channel": "新增渠道",
"default_url": "默認地址",
"detail": "詳情",
"edit_channel": "渠道配置",
"enable_channel": "啟用",
"forbid_channel": "禁用",
"key_type": "API key 格式:",
"log": "調用日誌",
"log_detail": "日誌詳情",
"log_status": "狀態",
"mapping": "模型映射",
"mapping_tip": "需填寫一個有效 Json。\n可在向實際地址發送請求時,對模型進行映射。\n例如:\n{\n \n \"gpt-4o\": \"gpt-4o-test\"\n\n}\n\n當 FastGPT 請求 gpt-4o 模型時,會向實際地址發送 gpt-4o-test 的模型,而不是 gpt-4o。",
"model": "模型",
"model_name": "模型名",
"model_test": "模型測試",
"model_tokens": "輸入/輸出 Tokens",
"request_at": "請求時間",
"request_duration": "請求時長: {{duration}}s",
"running_test": "測試中",
"search_model": "搜索模型",
"select_channel": "選擇渠道名",
"select_model": "選擇模型",
"select_model_placeholder": "選擇該渠道下可用的模型",
"select_provider_placeholder": "搜索廠商",
"selected_model_empty": "至少選擇一個模型",
"start_test": "開始測試{{num}}個模型",
"test_failed": "有{{num}}個模型報錯",
"waiting_test": "等待測試"
}
......@@ -124,7 +124,6 @@
"common.Copy Successful": "複製成功",
"common.Copy_failed": "複製失敗,請手動複製",
"common.Create Failed": "建立失敗",
"common.Create New": "建立新項目",
"common.Create Success": "建立成功",
"common.Create Time": "建立時間",
"common.Creating": "建立中",
......
......@@ -18,6 +18,7 @@ import workflow from '../i18n/zh-CN/workflow.json';
import user from '../i18n/zh-CN/user.json';
import chat from '../i18n/zh-CN/chat.json';
import login from '../i18n/zh-CN/login.json';
import account_model from '../i18n/zh-CN/account_model.json';
export interface I18nNamespaces {
common: typeof common;
......@@ -39,6 +40,7 @@ export interface I18nNamespaces {
account: typeof account;
account_team: typeof account_team;
account_thirdParty: typeof account_thirdParty;
account_model: typeof account_model;
}
export type I18nNsType = (keyof I18nNamespaces)[];
......@@ -73,7 +75,8 @@ declare module 'i18next' {
'account_promotion',
'account_thirdParty',
'account',
'account_team'
'account_team',
'account_model'
];
resources: I18nNamespaces;
}
......
......@@ -206,8 +206,8 @@ importers:
specifier: ^1.6.0
version: 1.8.0
mongoose:
specifier: ^7.0.2
version: 7.8.2
specifier: ^8.10.1
version: 8.10.2(socks@2.8.3)
multer:
specifier: 1.4.5-lts.1
version: 1.4.5-lts.1
......@@ -603,7 +603,7 @@ importers:
version: 9.0.3
'@shelf/jest-mongodb':
specifier: ^4.3.2
version: 4.3.2(jest-environment-node@29.7.0)(mongodb@6.9.0(socks@2.8.3))
version: 4.3.2(jest-environment-node@29.7.0)(mongodb@6.13.1(socks@2.8.3))
'@svgr/webpack':
specifier: ^6.5.1
version: 6.5.1
......@@ -645,7 +645,7 @@ importers:
version: 14.2.3(eslint@8.56.0)(typescript@5.5.3)
mockingoose:
specifier: ^2.16.2
version: 2.16.2(mongoose@7.8.2)
version: 2.16.2(mongoose@8.10.2(socks@2.8.3))
mongodb-memory-server:
specifier: ^10.0.0
version: 10.1.0(socks@2.8.3)
......@@ -4204,9 +4204,14 @@ packages:
resolution: {integrity: sha512-ix0EwukN2EpC0SRWIj/7B5+A6uQMQy6KMREI9qQqvgpkV2frH63T0UDVd1SYedL6dNCmDBYB3QtXi4ISk9YT+g==}
engines: {node: '>=14.20.1'}
bson@6.10.3:
resolution: {integrity: sha512-MTxGsqgYTwfshYWTRdmZRC+M7FnG1b4y7RO7p2k3X24Wq0yv1m77Wsj0BzlPzd/IowgESfsruQCUToa7vbOpPQ==}
engines: {node: '>=16.20.1'}
bson@6.8.0:
resolution: {integrity: sha512-iOJg8pr7wq2tg/zSlCCHMi3hMm5JTOxLTagf3zxhcenHsFp+c6uOs6K7W5UE7A4QIJGtqh/ZovFNMP4mOPJynQ==}
engines: {node: '>=16.20.1'}
deprecated: a critical bug affecting only useBigInt64=true deserialization usage is fixed in bson@6.10.3
buffer-alloc-unsafe@1.1.0:
resolution: {integrity: sha512-TEM2iMIEQdJ2yjPJoSIsldnleVaAk1oW3DBVUykyOLsEsFmEc9kn+SFFPz+gl54KQNxlDnAwCXosOS9Okx2xAg==}
......@@ -6507,8 +6512,8 @@ packages:
jws@4.0.0:
resolution: {integrity: sha512-KDncfTmOZoOMTFG4mBlG0qUIOlc03fmzH+ru6RgYVZhPkyiy/92Owlt/8UEN+a4TXR1FQetfIpJE8ApdvdVxTg==}
kareem@2.5.1:
resolution: {integrity: sha512-7jFxRVm+jD+rkq3kY0iZDJfsO2/t4BBPeEb2qKn2lR/9KhuksYk5hxzfRYWMPV8P/x2d0kHD306YyWLzjjH+uA==}
kareem@2.6.3:
resolution: {integrity: sha512-C3iHfuGUXK2u8/ipq9LfjFfXFxAZMQJJq7vLS45r3D9Y2xQ/m4S8zaR4zMLFWh9AsNPXmcFfUDhTEO8UIC/V6Q==}
engines: {node: '>=12.0.0'}
katex@0.16.11:
......@@ -7159,6 +7164,33 @@ packages:
snappy:
optional: true
mongodb@6.13.1:
resolution: {integrity: sha512-gdq40tX8StmhP6akMp1pPoEVv+9jTYFSrga/g23JxajPAQhH39ysZrHGzQCSd9PEOnuEQEdjIWqxO7ZSwC0w7Q==}
engines: {node: '>=16.20.1'}
peerDependencies:
'@aws-sdk/credential-providers': ^3.632.0
'@mongodb-js/zstd': ^1.1.0 || ^2.0.0
gcp-metadata: ^5.2.0
kerberos: ^2.0.1
mongodb-client-encryption: '>=6.0.0 <7'
snappy: ^7.2.2
socks: ^2.7.1
peerDependenciesMeta:
'@aws-sdk/credential-providers':
optional: true
'@mongodb-js/zstd':
optional: true
gcp-metadata:
optional: true
kerberos:
optional: true
mongodb-client-encryption:
optional: true
snappy:
optional: true
socks:
optional: true
mongodb@6.9.0:
resolution: {integrity: sha512-UMopBVx1LmEUbW/QE0Hw18u583PEDVQmUmVzzBRH0o/xtE9DBRA5ZYLOjpLIa03i8FXjzvQECJcqoMvCXftTUA==}
engines: {node: '>=16.20.1'}
......@@ -7186,9 +7218,9 @@ packages:
socks:
optional: true
mongoose@7.8.2:
resolution: {integrity: sha512-/KDcZL84gg8hnmOHRRPK49WtxH3Xsph38c7YqvYPdxEB2OsDAXvwAknGxyEC0F2P3RJCqFOp+523iFCa0p3dfw==}
engines: {node: '>=14.20.1'}
mongoose@8.10.2:
resolution: {integrity: sha512-DvqfK1s/JLwP39ogXULC8ygNDdmDber5ZbxZzELYtkzl9VGJ3K5T2MCLdpTs9I9J6DnkDyIHJwt7IOyMxh/Adw==}
engines: {node: '>=16.20.1'}
mpath@0.9.0:
resolution: {integrity: sha512-ikJRQTk8hw5DEoFVxHG1Gn9T/xcjtdnOKIU1JTmGjZZlg9LST2mBLmcX3/ICIbgJydT2GOc15RnNy5mHmzfSew==}
......@@ -8348,8 +8380,8 @@ packages:
resolution: {integrity: sha512-fDW/EZ6Q9RiO8eFG8Hj+7u/oW+XrPTIChwCOM2+th2A6OblDtYYIpve9m+KvI9Z4C9qSEXlaGR6bTEYHReuglA==}
engines: {node: '>= 0.4'}
sift@16.0.1:
resolution: {integrity: sha512-Wv6BjQ5zbhW7VFefWusVP33T/EM0vYikCaQ2qR8yULbsilAT8/wQaXvuQ3ptGLpoKx+lihJE3y2UTgKDyyNHZQ==}
sift@17.1.3:
resolution: {integrity: sha512-Rtlj66/b0ICeFzYTuNvX/EF1igRbbnGSvEyT79McoZa/DeGhMyC5pWKOEsZKnpkqtSeovd5FL/bjHWC3CIIvCQ==}
siginfo@2.0.0:
resolution: {integrity: sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==}
......@@ -12469,11 +12501,11 @@ snapshots:
'@sec-ant/readable-stream@0.4.1': {}
'@shelf/jest-mongodb@4.3.2(jest-environment-node@29.7.0)(mongodb@6.9.0(socks@2.8.3))':
'@shelf/jest-mongodb@4.3.2(jest-environment-node@29.7.0)(mongodb@6.13.1(socks@2.8.3))':
dependencies:
debug: 4.3.4
jest-environment-node: 29.7.0
mongodb: 6.9.0(socks@2.8.3)
mongodb: 6.13.1(socks@2.8.3)
mongodb-memory-server: 9.2.0
transitivePeerDependencies:
- '@aws-sdk/credential-providers'
......@@ -13772,6 +13804,8 @@ snapshots:
bson@5.5.1: {}
bson@6.10.3: {}
bson@6.8.0: {}
buffer-alloc-unsafe@1.1.0: {}
......@@ -14913,7 +14947,7 @@ snapshots:
eslint: 8.56.0
eslint-import-resolver-node: 0.3.9
eslint-import-resolver-typescript: 3.6.1(@typescript-eslint/parser@6.21.0(eslint@8.56.0)(typescript@5.5.3))(eslint-import-resolver-node@0.3.9)(eslint-plugin-import@2.29.1(eslint@8.56.0))(eslint@8.56.0)
eslint-plugin-import: 2.29.1(@typescript-eslint/parser@6.21.0(eslint@8.56.0)(typescript@5.5.3))(eslint-import-resolver-typescript@3.6.1(@typescript-eslint/parser@6.21.0(eslint@8.56.0)(typescript@5.5.3))(eslint-import-resolver-node@0.3.9)(eslint-plugin-import@2.29.1(eslint@8.56.0))(eslint@8.56.0))(eslint@8.56.0)
eslint-plugin-import: 2.29.1(@typescript-eslint/parser@6.21.0(eslint@8.56.0)(typescript@5.5.3))(eslint-import-resolver-typescript@3.6.1)(eslint@8.56.0)
eslint-plugin-jsx-a11y: 6.9.0(eslint@8.56.0)
eslint-plugin-react: 7.34.4(eslint@8.56.0)
eslint-plugin-react-hooks: 4.6.2(eslint@8.56.0)
......@@ -14937,7 +14971,7 @@ snapshots:
enhanced-resolve: 5.17.0
eslint: 8.56.0
eslint-module-utils: 2.8.1(@typescript-eslint/parser@6.21.0(eslint@8.56.0)(typescript@5.5.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.6.1(@typescript-eslint/parser@6.21.0(eslint@8.56.0)(typescript@5.5.3))(eslint-import-resolver-node@0.3.9)(eslint-plugin-import@2.29.1(eslint@8.56.0))(eslint@8.56.0))(eslint@8.56.0)
eslint-plugin-import: 2.29.1(@typescript-eslint/parser@6.21.0(eslint@8.56.0)(typescript@5.5.3))(eslint-import-resolver-typescript@3.6.1(@typescript-eslint/parser@6.21.0(eslint@8.56.0)(typescript@5.5.3))(eslint-import-resolver-node@0.3.9)(eslint-plugin-import@2.29.1(eslint@8.56.0))(eslint@8.56.0))(eslint@8.56.0)
eslint-plugin-import: 2.29.1(@typescript-eslint/parser@6.21.0(eslint@8.56.0)(typescript@5.5.3))(eslint-import-resolver-typescript@3.6.1)(eslint@8.56.0)
fast-glob: 3.3.2
get-tsconfig: 4.7.5
is-core-module: 2.14.0
......@@ -14959,7 +14993,7 @@ snapshots:
transitivePeerDependencies:
- supports-color
eslint-plugin-import@2.29.1(@typescript-eslint/parser@6.21.0(eslint@8.56.0)(typescript@5.5.3))(eslint-import-resolver-typescript@3.6.1(@typescript-eslint/parser@6.21.0(eslint@8.56.0)(typescript@5.5.3))(eslint-import-resolver-node@0.3.9)(eslint-plugin-import@2.29.1(eslint@8.56.0))(eslint@8.56.0))(eslint@8.56.0):
eslint-plugin-import@2.29.1(@typescript-eslint/parser@6.21.0(eslint@8.56.0)(typescript@5.5.3))(eslint-import-resolver-typescript@3.6.1)(eslint@8.56.0):
dependencies:
array-includes: 3.1.8
array.prototype.findlastindex: 1.2.5
......@@ -16637,7 +16671,7 @@ snapshots:
jwa: 2.0.0
safe-buffer: 5.2.1
kareem@2.5.1: {}
kareem@2.6.3: {}
katex@0.16.11:
dependencies:
......@@ -17564,9 +17598,9 @@ snapshots:
dependencies:
obliterator: 2.0.4
mockingoose@2.16.2(mongoose@7.8.2):
mockingoose@2.16.2(mongoose@8.10.2(socks@2.8.3)):
dependencies:
mongoose: 7.8.2
mongoose: 8.10.2(socks@2.8.3)
monaco-editor@0.50.0: {}
......@@ -17660,6 +17694,14 @@ snapshots:
optionalDependencies:
'@mongodb-js/saslprep': 1.1.9
mongodb@6.13.1(socks@2.8.3):
dependencies:
'@mongodb-js/saslprep': 1.1.9
bson: 6.10.3
mongodb-connection-string-url: 3.0.1
optionalDependencies:
socks: 2.8.3
mongodb@6.9.0(socks@2.8.3):
dependencies:
'@mongodb-js/saslprep': 1.1.9
......@@ -17668,21 +17710,23 @@ snapshots:
optionalDependencies:
socks: 2.8.3
mongoose@7.8.2:
mongoose@8.10.2(socks@2.8.3):
dependencies:
bson: 5.5.1
kareem: 2.5.1
mongodb: 5.9.2
bson: 6.10.3
kareem: 2.6.3
mongodb: 6.13.1(socks@2.8.3)
mpath: 0.9.0
mquery: 5.0.0
ms: 2.1.3
sift: 16.0.1
sift: 17.1.3
transitivePeerDependencies:
- '@aws-sdk/credential-providers'
- '@mongodb-js/zstd'
- gcp-metadata
- kerberos
- mongodb-client-encryption
- snappy
- socks
- supports-color
mpath@0.9.0: {}
......@@ -19015,7 +19059,7 @@ snapshots:
get-intrinsic: 1.2.4
object-inspect: 1.13.2
sift@16.0.1: {}
sift@17.1.3: {}
siginfo@2.0.0: {}
......
......@@ -13,6 +13,10 @@ ROOT_KEY=fdafasd
OPENAI_BASE_URL=https://api.openai.com/v1
# OpenAI API Key
CHAT_API_KEY=sk-xxxx
# ai proxy api
AIPROXY_API_ENDPOINT=https://xxx.come
AIPROXY_API_TOKEN=xxxxx
# 强制将图片转成 base64 传递给模型
MULTIPLE_DATA_TO_BASE64=true
......
import { ModelProviderIdType } from '@fastgpt/global/core/ai/provider';
import { ChannelInfoType } from './type';
import { i18nT } from '@fastgpt/web/i18n/utils';
export enum ChannelStatusEnum {
ChannelStatusUnknown = 0,
ChannelStatusEnabled = 1,
ChannelStatusDisabled = 2,
ChannelStatusAutoDisabled = 3
}
export const ChannelStautsMap = {
[ChannelStatusEnum.ChannelStatusUnknown]: {
label: i18nT('account_model:channel_status_unknown'),
colorSchema: 'gray'
},
[ChannelStatusEnum.ChannelStatusEnabled]: {
label: i18nT('account_model:channel_status_enabled'),
colorSchema: 'green'
},
[ChannelStatusEnum.ChannelStatusDisabled]: {
label: i18nT('account_model:channel_status_disabled'),
colorSchema: 'red'
},
[ChannelStatusEnum.ChannelStatusAutoDisabled]: {
label: i18nT('account_model:channel_status_auto_disabled'),
colorSchema: 'gray'
}
};
export const defaultChannel: ChannelInfoType = {
id: 0,
status: ChannelStatusEnum.ChannelStatusEnabled,
type: 1,
created_at: 0,
models: [],
model_mapping: {},
key: '',
name: '',
base_url: '',
priority: 0
};
export const aiproxyIdMap: Record<number, { label: string; provider: ModelProviderIdType }> = {
1: {
label: 'OpenAI',
provider: 'OpenAI'
},
3: {
label: i18nT('account_model:azure'),
provider: 'OpenAI'
},
14: {
label: 'Anthropic',
provider: 'Claude'
},
12: {
label: 'Google Gemini(OpenAI)',
provider: 'Gemini'
},
24: {
label: 'Google Gemini',
provider: 'Gemini'
},
28: {
label: 'Mistral AI',
provider: 'MistralAI'
},
29: {
label: 'Groq',
provider: 'Groq'
},
17: {
label: '阿里云',
provider: 'Qwen'
},
40: {
label: '豆包',
provider: 'Doubao'
},
36: {
label: 'DeepSeek AI',
provider: 'DeepSeek'
},
13: {
label: '百度智能云 V2',
provider: 'Ernie'
},
15: {
label: '百度智能云',
provider: 'Ernie'
},
16: {
label: '智谱 AI',
provider: 'ChatGLM'
},
18: {
label: '讯飞星火',
provider: 'SparkDesk'
},
25: {
label: '月之暗面',
provider: 'Moonshot'
},
26: {
label: '百川智能',
provider: 'Baichuan'
},
27: {
label: 'MiniMax',
provider: 'MiniMax'
},
31: {
label: '零一万物',
provider: 'Yi'
},
32: {
label: '阶跃星辰',
provider: 'StepFun'
},
43: {
label: 'SiliconFlow',
provider: 'Siliconflow'
},
30: {
label: 'Ollama',
provider: 'Ollama'
}
};
import { ChannelStatusEnum } from './constants';
export type ChannelInfoType = {
model_mapping: Record<string, any>;
key: string;
name: string;
base_url: string;
models: any[];
id: number;
status: ChannelStatusEnum;
type: number;
created_at: number;
priority: number;
};
// Channel api
export type ChannelListQueryType = {
page: number;
perPage: number;
};
export type ChannelListResponseType = ChannelInfoType[];
export type CreateChannelProps = {
type: number;
model_mapping: Record<string, any>;
key?: string;
name: string;
base_url: string;
models: string[];
};
// Log
export type ChannelLogListItemType = {
token_name: string;
model: string;
request_id: string;
id: number;
channel: number;
mode: number;
created_at: number;
request_at: number;
code: number;
prompt_tokens: number;
completion_tokens: number;
endpoint: string;
content?: string;
};
import { getSystemModelList, getTestModel } from '@/web/core/ai/config';
import {
Table,
Thead,
Tbody,
Tr,
Th,
Td,
TableContainer,
Box,
Flex,
Button,
HStack,
ModalBody,
ModalFooter
} from '@chakra-ui/react';
import { getModelProvider } from '@fastgpt/global/core/ai/provider';
import { useRequest2 } from '@fastgpt/web/hooks/useRequest';
import React, { useRef, useState } from 'react';
import MyIcon from '@fastgpt/web/components/common/Icon';
import { useTranslation } from 'next-i18next';
import MyModal from '@fastgpt/web/components/common/MyModal';
import MyTag from '@fastgpt/web/components/common/Tag/index';
import QuestionTip from '@fastgpt/web/components/common/MyTooltip/QuestionTip';
import { getErrText } from '@fastgpt/global/common/error/utils';
import { batchRun } from '@fastgpt/global/common/fn/utils';
import { useToast } from '@fastgpt/web/hooks/useToast';
type ModelTestItem = {
label: React.ReactNode;
model: string;
status: 'waiting' | 'running' | 'success' | 'error';
message?: string;
duration?: number;
};
const ModelTest = ({ models, onClose }: { models: string[]; onClose: () => void }) => {
const { t } = useTranslation();
const { toast } = useToast();
const [testModelList, setTestModelList] = useState<ModelTestItem[]>([]);
const statusMap = useRef({
waiting: {
label: t('account_model:waiting_test'),
colorSchema: 'gray'
},
running: {
label: t('account_model:running_test'),
colorSchema: 'blue'
},
success: {
label: t('common:common.Success'),
colorSchema: 'green'
},
error: {
label: t('common:common.failed'),
colorSchema: 'red'
}
});
const { loading: loadingModels } = useRequest2(getSystemModelList, {
manual: false,
refreshDeps: [models],
onSuccess(res) {
const list = models
.map((model) => {
const modelData = res.find((item) => item.model === model);
if (!modelData) return null;
const provider = getModelProvider(modelData.provider);
return {
label: (
<HStack>
<MyIcon name={provider.avatar as any} w={'1rem'} />
<Box>{t(modelData.name as any)}</Box>
</HStack>
),
model: modelData.model,
status: 'waiting'
};
})
.filter(Boolean) as ModelTestItem[];
setTestModelList(list);
}
});
const { runAsync: onStartTest, loading: isTesting } = useRequest2(
async () => {
{
let errorNum = 0;
const testModel = async (model: string) => {
setTestModelList((prev) =>
prev.map((item) =>
item.model === model ? { ...item, status: 'running', message: '' } : item
)
);
const start = Date.now();
try {
await getTestModel(model);
const duration = Date.now() - start;
setTestModelList((prev) =>
prev.map((item) =>
item.model === model
? { ...item, status: 'success', duration: duration / 1000 }
: item
)
);
} catch (error) {
setTestModelList((prev) =>
prev.map((item) =>
item.model === model
? { ...item, status: 'error', message: getErrText(error) }
: item
)
);
errorNum++;
}
};
await batchRun(
testModelList.map((item) => item.model),
testModel,
5
);
if (errorNum > 0) {
toast({
status: 'warning',
title: t('account_model:test_failed', { num: errorNum })
});
}
}
},
{
refreshDeps: [testModelList]
}
);
console.log(testModelList);
return (
<MyModal
iconSrc={'core/chat/sendLight'}
isLoading={loadingModels}
title={t('account_model:model_test')}
w={'600px'}
isOpen
>
<ModalBody>
<TableContainer h={'100%'} overflowY={'auto'} fontSize={'sm'} maxH={'60vh'}>
<Table>
<Thead>
<Tr>
<Th>{t('account_model:model')}</Th>
<Th>{t('account_model:channel_status')}</Th>
</Tr>
</Thead>
<Tbody>
{testModelList.map((item) => {
const data = statusMap.current[item.status];
return (
<Tr key={item.model}>
<Td>{item.label}</Td>
<Td>
<Flex alignItems={'center'}>
<MyTag mr={1} type="borderSolid" colorSchema={data.colorSchema as any}>
{data.label}
</MyTag>
{item.message && <QuestionTip label={item.message} />}
{item.status === 'success' && item.duration && (
<Box fontSize={'sm'} color={'myGray.500'}>
{t('account_model:request_duration', {
duration: item.duration.toFixed(2)
})}
</Box>
)}
</Flex>
</Td>
</Tr>
);
})}
</Tbody>
</Table>
</TableContainer>
</ModalBody>
<ModalFooter>
<Button mr={4} variant={'whiteBase'} onClick={onClose}>
{t('common:common.Cancel')}
</Button>
<Button isLoading={isTesting} variant={'primary'} onClick={onStartTest}>
{t('account_model:start_test', { num: testModelList.length })}
</Button>
</ModalFooter>
</MyModal>
);
};
export default ModelTest;
import { deleteChannel, getChannelList, putChannel, putChannelStatus } from '@/web/core/ai/channel';
import { useRequest2 } from '@fastgpt/web/hooks/useRequest';
import React, { useState } from 'react';
import {
Table,
Thead,
Tbody,
Tr,
Th,
Td,
TableContainer,
Box,
Flex,
Button,
HStack
} from '@chakra-ui/react';
import { useTranslation } from 'next-i18next';
import MyBox from '@fastgpt/web/components/common/MyBox';
import MyIconButton from '@fastgpt/web/components/common/Icon/button';
import { useUserStore } from '@/web/support/user/useUserStore';
import { ChannelInfoType } from '@/global/aiproxy/type';
import MyTag from '@fastgpt/web/components/common/Tag/index';
import {
aiproxyIdMap,
ChannelStatusEnum,
ChannelStautsMap,
defaultChannel
} from '@/global/aiproxy/constants';
import MyMenu from '@fastgpt/web/components/common/MyMenu';
import dynamic from 'next/dynamic';
import QuestionTip from '@fastgpt/web/components/common/MyTooltip/QuestionTip';
import MyNumberInput from '@fastgpt/web/components/common/Input/NumberInput';
import { getModelProvider } from '@fastgpt/global/core/ai/provider';
import MyIcon from '@fastgpt/web/components/common/Icon';
const EditChannelModal = dynamic(() => import('./EditChannelModal'), { ssr: false });
const ModelTest = dynamic(() => import('./ModelTest'), { ssr: false });
const ChannelTable = ({ Tab }: { Tab: React.ReactNode }) => {
const { t } = useTranslation();
const { userInfo } = useUserStore();
const isRoot = userInfo?.username === 'root';
const {
data: channelList = [],
runAsync: refreshChannelList,
loading: loadingChannelList
} = useRequest2(getChannelList, {
manual: false
});
const [editChannel, setEditChannel] = useState<ChannelInfoType>();
const { runAsync: updateChannel, loading: loadingUpdateChannel } = useRequest2(putChannel, {
manual: true,
onSuccess: () => {
refreshChannelList();
}
});
const { runAsync: updateChannelStatus, loading: loadingUpdateChannelStatus } = useRequest2(
putChannelStatus,
{
onSuccess: () => {
refreshChannelList();
}
}
);
const { runAsync: onDeleteChannel, loading: loadingDeleteChannel } = useRequest2(deleteChannel, {
manual: true,
onSuccess: () => {
refreshChannelList();
}
});
const [testModels, setTestModels] = useState<string[]>();
const isLoading =
loadingChannelList ||
loadingUpdateChannel ||
loadingDeleteChannel ||
loadingUpdateChannelStatus;
return (
<>
{isRoot && (
<Flex alignItems={'center'}>
{Tab}
<Box flex={1} />
<Button variant={'whiteBase'} mr={2} onClick={() => setEditChannel(defaultChannel)}>
{t('account_model:create_channel')}
</Button>
</Flex>
)}
<MyBox flex={'1 0 0'} h={0} isLoading={isLoading}>
<TableContainer h={'100%'} overflowY={'auto'} fontSize={'sm'}>
<Table>
<Thead>
<Tr>
<Th>ID</Th>
<Th>{t('account_model:channel_name')}</Th>
<Th>{t('account_model:channel_type')}</Th>
<Th>{t('account_model:channel_status')}</Th>
<Th>
{t('account_model:channel_priority')}
<QuestionTip label={t('account_model:channel_priority_tip')} />
</Th>
<Th></Th>
</Tr>
</Thead>
<Tbody>
{channelList.map((item) => {
const providerData = aiproxyIdMap[item.type];
const provider = getModelProvider(providerData?.provider);
return (
<Tr key={item.id} _hover={{ bg: 'myGray.100' }}>
<Td>{item.id}</Td>
<Td>{item.name}</Td>
<Td>
{providerData ? (
<HStack>
<MyIcon name={provider?.avatar as any} w={'1rem'} />
<Box>{t(providerData?.label as any)}</Box>
</HStack>
) : (
'Invalid provider'
)}
</Td>
<Td>
<MyTag
colorSchema={ChannelStautsMap[item.status]?.colorSchema as any}
type="borderFill"
>
{t(ChannelStautsMap[item.status]?.label as any) ||
t('account_model:channel_status_unknown')}
</MyTag>
</Td>
<Td>
<MyNumberInput
defaultValue={item.priority || 0}
min={0}
max={100}
h={'32px'}
w={'80px'}
onBlur={(e) => {
const val = (() => {
if (!e) return 0;
return e;
})();
updateChannel({
...item,
priority: val
});
}}
/>
</Td>
<Td>
<MyMenu
menuList={[
{
label: '',
children: [
{
icon: 'core/chat/sendLight',
label: t('account_model:model_test'),
onClick: () => setTestModels(item.models)
},
...(item.status === ChannelStatusEnum.ChannelStatusEnabled
? [
{
icon: 'common/disable',
label: t('account_model:forbid_channel'),
onClick: () =>
updateChannelStatus(
item.id,
ChannelStatusEnum.ChannelStatusDisabled
)
}
]
: [
{
icon: 'common/enable',
label: t('account_model:enable_channel'),
onClick: () =>
updateChannelStatus(
item.id,
ChannelStatusEnum.ChannelStatusEnabled
)
}
]),
{
icon: 'common/settingLight',
label: t('account_model:edit'),
onClick: () => setEditChannel(item)
},
{
type: 'danger',
icon: 'delete',
label: t('common:common.Delete'),
onClick: () => onDeleteChannel(item.id)
}
]
}
]}
Button={<MyIconButton icon={'more'} />}
/>
</Td>
</Tr>
);
})}
</Tbody>
</Table>
</TableContainer>
</MyBox>
{!!editChannel && (
<EditChannelModal
defaultConfig={editChannel}
onClose={() => setEditChannel(undefined)}
onSuccess={refreshChannelList}
/>
)}
{!!testModels && <ModelTest models={testModels} onClose={() => setTestModels(undefined)} />}
</>
);
};
export default ChannelTable;
......@@ -7,13 +7,17 @@ import { useUserStore } from '@/web/support/user/useUserStore';
import FillRowTabs from '@fastgpt/web/components/common/Tabs/FillRowTabs';
import { useTranslation } from 'next-i18next';
import dynamic from 'next/dynamic';
import { useSystemStore } from '@/web/common/system/useSystemStore';
const ModelConfigTable = dynamic(() => import('@/pageComponents/account/model/ModelConfigTable'));
const ChannelTable = dynamic(() => import('@/pageComponents/account/model/Channel'));
const ChannelLog = dynamic(() => import('@/pageComponents/account/model/Log'));
type TabType = 'model' | 'config' | 'channel';
type TabType = 'model' | 'config' | 'channel' | 'channel_log';
const ModelProvider = () => {
const { t } = useTranslation();
const { feConfigs } = useSystemStore();
const [tab, setTab] = useState<TabType>('model');
......@@ -22,21 +26,29 @@ const ModelProvider = () => {
<FillRowTabs<TabType>
list={[
{ label: t('account:active_model'), value: 'model' },
{ label: t('account:config_model'), value: 'config' }
// { label: t('account:channel'), value: 'channel' }
{ label: t('account:config_model'), value: 'config' },
// @ts-ignore
...(feConfigs?.show_aiproxy
? [
{ label: t('account:channel'), value: 'channel' },
{ label: t('account_model:log'), value: 'channel_log' }
]
: [])
]}
value={tab}
py={1}
onChange={setTab}
/>
);
}, [t, tab]);
}, [feConfigs.show_aiproxy, t, tab]);
return (
<AccountContainer>
<Flex h={'100%'} flexDirection={'column'} gap={4} py={4} px={6}>
{tab === 'model' && <ValidModelTable Tab={Tab} />}
{tab === 'config' && <ModelConfigTable Tab={Tab} />}
{tab === 'channel' && <ChannelTable Tab={Tab} />}
{tab === 'channel_log' && <ChannelLog Tab={Tab} />}
</Flex>
</AccountContainer>
);
......@@ -45,7 +57,7 @@ const ModelProvider = () => {
export async function getServerSideProps(content: any) {
return {
props: {
...(await serviceSideProps(content, ['account']))
...(await serviceSideProps(content, ['account', 'account_model']))
}
};
}
......
import type { NextApiRequest, NextApiResponse } from 'next';
import { jsonRes } from '@fastgpt/service/common/response';
import { request } from 'https';
import { authSystemAdmin } from '@fastgpt/service/support/permission/user/auth';
const baseUrl = process.env.AIPROXY_API_ENDPOINT;
const token = process.env.AIPROXY_API_TOKEN;
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
try {
await authSystemAdmin({ req });
if (!baseUrl || !token) {
throw new Error('AIPROXY_API_ENDPOINT or AIPROXY_API_TOKEN is not set');
}
const { path = [], ...query } = req.query as any;
if (!path.length) {
throw new Error('url is empty');
}
const queryStr = new URLSearchParams(query).toString();
const requestPath = queryStr
? `/${path?.join('/')}?${new URLSearchParams(query).toString()}`
: `/${path?.join('/')}`;
const parsedUrl = new URL(baseUrl);
delete req.headers?.cookie;
delete req.headers?.host;
delete req.headers?.origin;
const requestResult = request({
protocol: parsedUrl.protocol,
hostname: parsedUrl.hostname,
port: parsedUrl.port,
path: requestPath,
method: req.method,
headers: {
...req.headers,
Authorization: `Bearer ${token}`
},
timeout: 30000
});
req.pipe(requestResult);
requestResult.on('response', (response) => {
Object.keys(response.headers).forEach((key) => {
// @ts-ignore
res.setHeader(key, response.headers[key]);
});
response.statusCode && res.writeHead(response.statusCode);
response.pipe(res);
});
requestResult.on('error', (e) => {
res.send(e);
res.end();
});
} catch (error) {
jsonRes(res, {
code: 500,
error
});
}
}
export const config = {
api: {
bodyParser: false
}
};
import type { ApiRequestProps, ApiResponseType } from '@fastgpt/service/type/next';
import { authSystemAdmin } from '@fastgpt/service/support/permission/user/auth';
import axios from 'axios';
import { getErrText } from '@fastgpt/global/common/error/utils';
const baseUrl = process.env.AIPROXY_API_ENDPOINT;
const token = process.env.AIPROXY_API_TOKEN;
async function handler(req: ApiRequestProps, res: ApiResponseType<any>) {
try {
await authSystemAdmin({ req });
if (!baseUrl || !token) {
return Promise.reject('AIPROXY_API_ENDPOINT or AIPROXY_API_TOKEN is not set');
}
const { data } = await axios.post(`${baseUrl}/api/channel/`, req.body, {
headers: {
Authorization: `Bearer ${token}`
}
});
res.json(data);
} catch (error) {
res.json({
success: false,
message: getErrText(error),
data: error
});
}
}
export default handler;
......@@ -60,6 +60,7 @@ const testLLMModel = async (model: LLMModelItemType) => {
const ai = getAIApi({
timeout: 10000
});
const requestBody = llmCompletionsBodyFormat(
{
model: model.model,
......
......@@ -218,7 +218,7 @@ const MyApps = () => {
size="md"
Button={
<Button variant={'primary'} leftIcon={<AddIcon />}>
<Box>{t('common:common.Create New')}</Box>
<Box>{t('common:new_create')}</Box>
</Button>
}
menuList={[
......
......@@ -147,7 +147,7 @@ const Dataset = () => {
<Button variant={'primary'} px="0">
<Flex alignItems={'center'} px={5}>
<AddIcon mr={2} />
<Box>{t('common:common.Create New')}</Box>
<Box>{t('common:new_create')}</Box>
</Flex>
</Button>
}
......
......@@ -83,7 +83,8 @@ export async function initSystemConfig() {
...fileRes?.feConfigs,
...defaultFeConfigs,
...(dbConfig.feConfigs || {}),
isPlus: !!FastGPTProUrl
isPlus: !!FastGPTProUrl,
show_aiproxy: !!process.env.AIPROXY_API_ENDPOINT
},
systemEnv: {
...fileRes.systemEnv,
......
import axios, { Method, AxiosResponse } from 'axios';
import { getWebReqUrl } from '@fastgpt/web/common/system/utils';
import {
ChannelInfoType,
ChannelListResponseType,
ChannelLogListItemType,
CreateChannelProps
} from '@/global/aiproxy/type';
import { ChannelStatusEnum } from '@/global/aiproxy/constants';
interface ResponseDataType {
success: boolean;
message: string;
data: any;
}
/**
* 请求成功,检查请求头
*/
function responseSuccess(response: AxiosResponse<ResponseDataType>) {
return response;
}
/**
* 响应数据检查
*/
function checkRes(data: ResponseDataType) {
if (data === undefined) {
console.log('error->', data, 'data is empty');
return Promise.reject('服务器异常');
} else if (!data.success) {
return Promise.reject(data);
}
return data.data;
}
/**
* 响应错误
*/
function responseError(err: any) {
console.log('error->', '请求错误', err);
const data = err?.response?.data || err;
if (!err) {
return Promise.reject({ message: '未知错误' });
}
if (typeof err === 'string') {
return Promise.reject({ message: err });
}
if (typeof data === 'string') {
return Promise.reject(data);
}
return Promise.reject(data);
}
/* 创建请求实例 */
const instance = axios.create({
timeout: 60000, // 超时时间
headers: {
'content-type': 'application/json'
}
});
/* 响应拦截 */
instance.interceptors.response.use(responseSuccess, (err) => Promise.reject(err));
function request(url: string, data: any, method: Method): any {
/* 去空 */
for (const key in data) {
if (data[key] === undefined) {
delete data[key];
}
}
return instance
.request({
baseURL: getWebReqUrl('/api/aiproxy/api'),
url,
method,
data: ['POST', 'PUT'].includes(method) ? data : undefined,
params: !['POST', 'PUT'].includes(method) ? data : undefined
})
.then((res) => checkRes(res.data))
.catch((err) => responseError(err));
}
/**
* api请求方式
* @param {String} url
* @param {Any} params
* @param {Object} config
* @returns
*/
export function GET<T = undefined>(url: string, params = {}): Promise<T> {
return request(url, params, 'GET');
}
export function POST<T = undefined>(url: string, data = {}): Promise<T> {
return request(url, data, 'POST');
}
export function PUT<T = undefined>(url: string, data = {}): Promise<T> {
return request(url, data, 'PUT');
}
export function DELETE<T = undefined>(url: string, data = {}): Promise<T> {
return request(url, data, 'DELETE');
}
// ====== API ======
export const getChannelList = () =>
GET<ChannelListResponseType>('/channels/all', {
page: 1,
perPage: 10
});
export const getChannelProviders = () =>
GET<
Record<
number,
{
defaultBaseUrl: string;
keyHelp: string;
name: string;
}
>
>('/channels/type_metas');
export const postCreateChannel = (data: CreateChannelProps) =>
POST(`/createChannel`, {
type: data.type,
name: data.name,
base_url: data.base_url,
models: data.models,
model_mapping: data.model_mapping,
key: data.key
});
export const putChannelStatus = (id: number, status: ChannelStatusEnum) =>
POST(`/channel/${id}/status`, {
status
});
export const putChannel = (data: ChannelInfoType) =>
PUT(`/channel/${data.id}`, {
type: data.type,
name: data.name,
base_url: data.base_url,
models: data.models,
model_mapping: data.model_mapping,
key: data.key,
status: data.status,
priority: data.priority
});
export const deleteChannel = (id: number) => DELETE(`/channel/${id}`);
export const getChannelLog = (params: {
channel?: string;
model_name?: string;
status?: 'all' | 'success' | 'error';
start_timestamp: number;
end_timestamp: number;
offset: number;
pageSize: number;
}) =>
GET<{
logs: ChannelLogListItemType[];
total: number;
}>(`/logs/search`, {
...params,
p: Math.floor(params.offset / params.pageSize) + 1,
per_page: params.pageSize,
offset: undefined,
pageSize: undefined
}).then((res) => {
return {
list: res.logs,
total: res.total
};
});
export const getLogDetail = (id: number) =>
GET<{
request_body: string;
response_body: string;
}>(`/logs/detail/${id}`);
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or sign in to comment