feat: ai proxy v1 (#3898)
* feat: ai proxy v1 * perf: ai proxy channel crud * feat: ai proxy logs * feat: channel test * doc * update lock
This commit is contained in:
parent
3c382d1240
commit
81a06718d8
@ -11,6 +11,7 @@ weight: 802
|
|||||||
## 🚀 新增内容
|
## 🚀 新增内容
|
||||||
|
|
||||||
1. 增加默认“知识库文本理解模型”配置
|
1. 增加默认“知识库文本理解模型”配置
|
||||||
|
2. AI proxy V1版,可替换 OneAPI使用,同时提供完整模型调用日志,便于排查问题。
|
||||||
|
|
||||||
## ⚙️ 优化
|
## ⚙️ 优化
|
||||||
|
|
||||||
@ -18,8 +19,11 @@ weight: 802
|
|||||||
2. 集合列表数据统计方式,提高大数据量统计性能。
|
2. 集合列表数据统计方式,提高大数据量统计性能。
|
||||||
3. 优化数学公式,转义 Latex 格式成 Markdown 格式。
|
3. 优化数学公式,转义 Latex 格式成 Markdown 格式。
|
||||||
4. 解析文档图片,图片太大时,自动忽略。
|
4. 解析文档图片,图片太大时,自动忽略。
|
||||||
|
5. 时间选择器,当天开始时间自动设0,结束设置设 23:59:59,避免 UI 与实际逻辑偏差。
|
||||||
|
6. 升级 mongoose 库版本依赖。
|
||||||
|
|
||||||
## 🐛 修复
|
## 🐛 修复
|
||||||
|
|
||||||
1. 标签过滤时,子文件夹未成功过滤。
|
1. 标签过滤时,子文件夹未成功过滤。
|
||||||
2. 暂时移除 md 阅读优化,避免链接分割错误。
|
2. 暂时移除 md 阅读优化,避免链接分割错误。
|
||||||
|
3. 离开团队时,未刷新成员列表。
|
||||||
@ -7,12 +7,14 @@ import { i18nT } from '../../../web/i18n/utils';
|
|||||||
dayjs.extend(utc);
|
dayjs.extend(utc);
|
||||||
dayjs.extend(timezone);
|
dayjs.extend(timezone);
|
||||||
|
|
||||||
export const formatTime2YMDHMW = (time?: Date) => dayjs(time).format('YYYY-MM-DD HH:mm:ss dddd');
|
export const formatTime2YMDHMW = (time?: Date | number) =>
|
||||||
export const formatTime2YMDHMS = (time?: Date) =>
|
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') : '';
|
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') : '';
|
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');
|
export const formatTime2HM = (time: Date = new Date()) => dayjs(time).format('HH:mm');
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@ -54,6 +54,7 @@ export type FastGPTFeConfigsType = {
|
|||||||
show_promotion?: boolean;
|
show_promotion?: boolean;
|
||||||
show_team_chat?: boolean;
|
show_team_chat?: boolean;
|
||||||
show_compliance_copywriting?: boolean;
|
show_compliance_copywriting?: boolean;
|
||||||
|
show_aiproxy?: boolean;
|
||||||
concatMd?: string;
|
concatMd?: string;
|
||||||
|
|
||||||
docUrl?: string;
|
docUrl?: string;
|
||||||
|
|||||||
@ -11,14 +11,17 @@ import { i18nT } from '../../../web/i18n/utils';
|
|||||||
import { OpenaiAccountType } from '@fastgpt/global/support/user/team/type';
|
import { OpenaiAccountType } from '@fastgpt/global/support/user/team/type';
|
||||||
import { getLLMModel } from './model';
|
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 }) => {
|
export const getAIApi = (props?: { userKey?: OpenaiAccountType; timeout?: number }) => {
|
||||||
const { userKey, timeout } = props || {};
|
const { userKey, timeout } = props || {};
|
||||||
|
|
||||||
const baseUrl = userKey?.baseUrl || global?.systemEnv?.oneapiUrl || openaiBaseUrl;
|
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({
|
return new OpenAI({
|
||||||
baseURL: baseUrl,
|
baseURL: baseUrl,
|
||||||
apiKey,
|
apiKey,
|
||||||
@ -72,6 +75,7 @@ export const createChatCompletion = async ({
|
|||||||
userKey,
|
userKey,
|
||||||
timeout: formatTimeout
|
timeout: formatTimeout
|
||||||
});
|
});
|
||||||
|
|
||||||
const response = await ai.chat.completions.create(body, {
|
const response = await ai.chat.completions.create(body, {
|
||||||
...options,
|
...options,
|
||||||
...(modelConstantsData.requestUrl ? { path: modelConstantsData.requestUrl } : {}),
|
...(modelConstantsData.requestUrl ? { path: modelConstantsData.requestUrl } : {}),
|
||||||
|
|||||||
@ -15,7 +15,7 @@ import { TeamDefaultPermissionVal } from '@fastgpt/global/support/permission/use
|
|||||||
import { MongoMemberGroupModel } from '../../permission/memberGroup/memberGroupSchema';
|
import { MongoMemberGroupModel } from '../../permission/memberGroup/memberGroupSchema';
|
||||||
import { mongoSessionRun } from '../../../common/mongo/sessionRun';
|
import { mongoSessionRun } from '../../../common/mongo/sessionRun';
|
||||||
import { DefaultGroupName } from '@fastgpt/global/support/user/team/group/constant';
|
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 { createRootOrg } from '../../permission/org/controllers';
|
||||||
import { refreshSourceAvatar } from '../../../common/file/image/controller';
|
import { refreshSourceAvatar } from '../../../common/file/image/controller';
|
||||||
|
|
||||||
@ -152,7 +152,7 @@ export async function updateTeam({
|
|||||||
// auth openai key
|
// auth openai key
|
||||||
if (openaiAccount?.key) {
|
if (openaiAccount?.key) {
|
||||||
console.log('auth user openai key', 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;
|
openaiAccount.baseUrl = baseUrl;
|
||||||
|
|
||||||
const ai = getAIApi({
|
const ai = getAIApi({
|
||||||
|
|||||||
@ -100,6 +100,13 @@ const DateRangePicker = ({
|
|||||||
if (date?.to === undefined) {
|
if (date?.to === undefined) {
|
||||||
date.to = date.from;
|
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);
|
setRange(date);
|
||||||
onChange?.(date);
|
onChange?.(date);
|
||||||
}}
|
}}
|
||||||
|
|||||||
@ -1,4 +1,5 @@
|
|||||||
// @ts-nocheck
|
// @ts-nocheck
|
||||||
|
|
||||||
export const iconPaths = {
|
export const iconPaths = {
|
||||||
book: () => import('./icons/book.svg'),
|
book: () => import('./icons/book.svg'),
|
||||||
change: () => import('./icons/change.svg'),
|
change: () => import('./icons/change.svg'),
|
||||||
@ -32,8 +33,10 @@ export const iconPaths = {
|
|||||||
'common/customTitleLight': () => import('./icons/common/customTitleLight.svg'),
|
'common/customTitleLight': () => import('./icons/common/customTitleLight.svg'),
|
||||||
'common/data': () => import('./icons/common/data.svg'),
|
'common/data': () => import('./icons/common/data.svg'),
|
||||||
'common/dingtalkFill': () => import('./icons/common/dingtalkFill.svg'),
|
'common/dingtalkFill': () => import('./icons/common/dingtalkFill.svg'),
|
||||||
|
'common/disable': () => import('./icons/common/disable.svg'),
|
||||||
'common/downArrowFill': () => import('./icons/common/downArrowFill.svg'),
|
'common/downArrowFill': () => import('./icons/common/downArrowFill.svg'),
|
||||||
'common/editor/resizer': () => import('./icons/common/editor/resizer.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/errorFill': () => import('./icons/common/errorFill.svg'),
|
||||||
'common/file/move': () => import('./icons/common/file/move.svg'),
|
'common/file/move': () => import('./icons/common/file/move.svg'),
|
||||||
'common/folderFill': () => import('./icons/common/folderFill.svg'),
|
'common/folderFill': () => import('./icons/common/folderFill.svg'),
|
||||||
|
|||||||
@ -0,0 +1 @@
|
|||||||
|
<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>
|
||||||
|
After Width: | Height: | Size: 397 B |
@ -0,0 +1 @@
|
|||||||
|
<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>
|
||||||
|
After Width: | Height: | Size: 486 B |
@ -10,8 +10,9 @@ import React from 'react';
|
|||||||
import MyIcon from '../../Icon';
|
import MyIcon from '../../Icon';
|
||||||
import { UseFormRegister } from 'react-hook-form';
|
import { UseFormRegister } from 'react-hook-form';
|
||||||
|
|
||||||
type Props = Omit<NumberInputProps, 'onChange'> & {
|
type Props = Omit<NumberInputProps, 'onChange' | 'onBlur'> & {
|
||||||
onChange?: (e?: number) => any;
|
onChange?: (e?: number) => any;
|
||||||
|
onBlur?: (e?: number) => any;
|
||||||
placeholder?: string;
|
placeholder?: string;
|
||||||
register?: UseFormRegister<any>;
|
register?: UseFormRegister<any>;
|
||||||
name?: string;
|
name?: string;
|
||||||
@ -19,11 +20,21 @@ type Props = Omit<NumberInputProps, 'onChange'> & {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const MyNumberInput = (props: Props) => {
|
const MyNumberInput = (props: Props) => {
|
||||||
const { register, name, onChange, placeholder, bg, ...restProps } = props;
|
const { register, name, onChange, onBlur, placeholder, bg, ...restProps } = props;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<NumberInput
|
<NumberInput
|
||||||
{...restProps}
|
{...restProps}
|
||||||
|
onBlur={(e) => {
|
||||||
|
if (!onBlur) return;
|
||||||
|
const numE = Number(e.target.value);
|
||||||
|
if (isNaN(numE)) {
|
||||||
|
// @ts-ignore
|
||||||
|
onBlur('');
|
||||||
|
} else {
|
||||||
|
onBlur(numE);
|
||||||
|
}
|
||||||
|
}}
|
||||||
onChange={(e) => {
|
onChange={(e) => {
|
||||||
if (!onChange) return;
|
if (!onChange) return;
|
||||||
const numE = Number(e);
|
const numE = Number(e);
|
||||||
@ -38,6 +49,8 @@ const MyNumberInput = (props: Props) => {
|
|||||||
<NumberInputField
|
<NumberInputField
|
||||||
bg={bg}
|
bg={bg}
|
||||||
placeholder={placeholder}
|
placeholder={placeholder}
|
||||||
|
h={restProps.h}
|
||||||
|
defaultValue={restProps.defaultValue}
|
||||||
{...(register && name
|
{...(register && name
|
||||||
? register(name, {
|
? register(name, {
|
||||||
required: props.isRequired,
|
required: props.isRequired,
|
||||||
|
|||||||
@ -98,7 +98,6 @@ const MultipleSelect = <T = any,>({
|
|||||||
return (
|
return (
|
||||||
<MenuItem
|
<MenuItem
|
||||||
key={i}
|
key={i}
|
||||||
{...menuItemStyles}
|
|
||||||
{...(isSelected
|
{...(isSelected
|
||||||
? {
|
? {
|
||||||
color: 'primary.600'
|
color: 'primary.600'
|
||||||
@ -114,6 +113,7 @@ const MultipleSelect = <T = any,>({
|
|||||||
whiteSpace={'pre-wrap'}
|
whiteSpace={'pre-wrap'}
|
||||||
fontSize={'sm'}
|
fontSize={'sm'}
|
||||||
gap={2}
|
gap={2}
|
||||||
|
{...menuItemStyles}
|
||||||
>
|
>
|
||||||
<Checkbox isChecked={isSelected} />
|
<Checkbox isChecked={isSelected} />
|
||||||
{item.icon && <MyAvatar src={item.icon} w={'1rem'} borderRadius={'0'} />}
|
{item.icon && <MyAvatar src={item.icon} w={'1rem'} borderRadius={'0'} />}
|
||||||
@ -204,6 +204,7 @@ const MultipleSelect = <T = any,>({
|
|||||||
}}
|
}}
|
||||||
onClick={(e) => {
|
onClick={(e) => {
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
|
e.preventDefault();
|
||||||
onclickItem(item.value);
|
onclickItem(item.value);
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
@ -230,7 +231,6 @@ const MultipleSelect = <T = any,>({
|
|||||||
overflowY={'auto'}
|
overflowY={'auto'}
|
||||||
>
|
>
|
||||||
<MenuItem
|
<MenuItem
|
||||||
{...menuItemStyles}
|
|
||||||
color={isSelectAll ? 'primary.600' : 'myGray.900'}
|
color={isSelectAll ? 'primary.600' : 'myGray.900'}
|
||||||
onClick={(e) => {
|
onClick={(e) => {
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
@ -241,6 +241,7 @@ const MultipleSelect = <T = any,>({
|
|||||||
fontSize={'sm'}
|
fontSize={'sm'}
|
||||||
gap={2}
|
gap={2}
|
||||||
mb={1}
|
mb={1}
|
||||||
|
{...menuItemStyles}
|
||||||
>
|
>
|
||||||
<Checkbox isChecked={isSelectAll} />
|
<Checkbox isChecked={isSelectAll} />
|
||||||
<Box flex={'1 0 0'}>{t('common:common.All')}</Box>
|
<Box flex={'1 0 0'}>{t('common:common.All')}</Box>
|
||||||
|
|||||||
@ -4,7 +4,8 @@ import React, {
|
|||||||
useMemo,
|
useMemo,
|
||||||
useEffect,
|
useEffect,
|
||||||
useImperativeHandle,
|
useImperativeHandle,
|
||||||
ForwardedRef
|
ForwardedRef,
|
||||||
|
useState
|
||||||
} from 'react';
|
} from 'react';
|
||||||
import {
|
import {
|
||||||
Menu,
|
Menu,
|
||||||
@ -15,7 +16,8 @@ import {
|
|||||||
MenuButton,
|
MenuButton,
|
||||||
Box,
|
Box,
|
||||||
css,
|
css,
|
||||||
Flex
|
Flex,
|
||||||
|
Input
|
||||||
} from '@chakra-ui/react';
|
} from '@chakra-ui/react';
|
||||||
import type { ButtonProps, MenuItemProps } from '@chakra-ui/react';
|
import type { ButtonProps, MenuItemProps } from '@chakra-ui/react';
|
||||||
import MyIcon from '../Icon';
|
import MyIcon from '../Icon';
|
||||||
@ -33,8 +35,10 @@ import { useScrollPagination } from '../../../hooks/useScrollPagination';
|
|||||||
export type SelectProps<T = any> = ButtonProps & {
|
export type SelectProps<T = any> = ButtonProps & {
|
||||||
value?: T;
|
value?: T;
|
||||||
placeholder?: string;
|
placeholder?: string;
|
||||||
|
isSearch?: boolean;
|
||||||
list: {
|
list: {
|
||||||
alias?: string;
|
alias?: string;
|
||||||
|
icon?: string;
|
||||||
label: string | React.ReactNode;
|
label: string | React.ReactNode;
|
||||||
description?: string;
|
description?: string;
|
||||||
value: T;
|
value: T;
|
||||||
@ -49,6 +53,7 @@ const MySelect = <T = any,>(
|
|||||||
{
|
{
|
||||||
placeholder,
|
placeholder,
|
||||||
value,
|
value,
|
||||||
|
isSearch = false,
|
||||||
width = '100%',
|
width = '100%',
|
||||||
list = [],
|
list = [],
|
||||||
onchange,
|
onchange,
|
||||||
@ -63,6 +68,7 @@ const MySelect = <T = any,>(
|
|||||||
const ButtonRef = useRef<HTMLButtonElement>(null);
|
const ButtonRef = useRef<HTMLButtonElement>(null);
|
||||||
const MenuListRef = useRef<HTMLDivElement>(null);
|
const MenuListRef = useRef<HTMLDivElement>(null);
|
||||||
const SelectedItemRef = useRef<HTMLDivElement>(null);
|
const SelectedItemRef = useRef<HTMLDivElement>(null);
|
||||||
|
const SearchInputRef = useRef<HTMLInputElement>(null);
|
||||||
|
|
||||||
const menuItemStyles: MenuItemProps = {
|
const menuItemStyles: MenuItemProps = {
|
||||||
borderRadius: 'sm',
|
borderRadius: 'sm',
|
||||||
@ -79,6 +85,18 @@ const MySelect = <T = any,>(
|
|||||||
const { isOpen, onOpen, onClose } = useDisclosure();
|
const { isOpen, onOpen, onClose } = useDisclosure();
|
||||||
const selectItem = useMemo(() => list.find((item) => item.value === value), [list, value]);
|
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, () => ({
|
useImperativeHandle(ref, () => ({
|
||||||
focus() {
|
focus() {
|
||||||
onOpen();
|
onOpen();
|
||||||
@ -90,17 +108,19 @@ const MySelect = <T = any,>(
|
|||||||
const menu = MenuListRef.current;
|
const menu = MenuListRef.current;
|
||||||
const selectedItem = SelectedItemRef.current;
|
const selectedItem = SelectedItemRef.current;
|
||||||
menu.scrollTop = selectedItem.offsetTop - menu.offsetTop - 100;
|
menu.scrollTop = selectedItem.offsetTop - menu.offsetTop - 100;
|
||||||
|
|
||||||
|
if (isSearch) {
|
||||||
|
setSearch('');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}, [isOpen]);
|
}, [isSearch, isOpen]);
|
||||||
|
|
||||||
const { runAsync: onChange, loading } = useRequest2((val: T) => onchange?.(val));
|
const { runAsync: onChange, loading } = useRequest2((val: T) => onchange?.(val));
|
||||||
|
|
||||||
const isSelecting = loading || isLoading;
|
|
||||||
|
|
||||||
const ListRender = useMemo(() => {
|
const ListRender = useMemo(() => {
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{list.map((item, i) => (
|
{filterList.map((item, i) => (
|
||||||
<Box key={i}>
|
<Box key={i}>
|
||||||
<MenuItem
|
<MenuItem
|
||||||
{...menuItemStyles}
|
{...menuItemStyles}
|
||||||
@ -123,7 +143,10 @@ const MySelect = <T = any,>(
|
|||||||
fontSize={'sm'}
|
fontSize={'sm'}
|
||||||
display={'block'}
|
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 && (
|
{item.description && (
|
||||||
<Box color={'myGray.500'} fontSize={'xs'}>
|
<Box color={'myGray.500'} fontSize={'xs'}>
|
||||||
{item.description}
|
{item.description}
|
||||||
@ -135,7 +158,9 @@ const MySelect = <T = any,>(
|
|||||||
))}
|
))}
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}, [list, value]);
|
}, [filterList, value]);
|
||||||
|
|
||||||
|
const isSelecting = loading || isLoading;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box
|
<Box
|
||||||
@ -176,8 +201,33 @@ const MySelect = <T = any,>(
|
|||||||
{...props}
|
{...props}
|
||||||
>
|
>
|
||||||
<Flex alignItems={'center'}>
|
<Flex alignItems={'center'}>
|
||||||
{isSelecting && <MyIcon mr={2} name={'common/loading'} w={'16px'} />}
|
{isSelecting && <MyIcon mr={2} name={'common/loading'} w={'1rem'} />}
|
||||||
{selectItem?.alias || selectItem?.label || placeholder}
|
{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>
|
</Flex>
|
||||||
</MenuButton>
|
</MenuButton>
|
||||||
|
|
||||||
|
|||||||
@ -217,7 +217,7 @@ export function useScrollPagination<
|
|||||||
const offset = init ? 0 : data.length;
|
const offset = init ? 0 : data.length;
|
||||||
|
|
||||||
setTrue();
|
setTrue();
|
||||||
|
console.log(offset);
|
||||||
try {
|
try {
|
||||||
const res = await api({
|
const res = await api({
|
||||||
offset,
|
offset,
|
||||||
|
|||||||
46
packages/web/i18n/en/account_model.json
Normal file
46
packages/web/i18n/en/account_model.json
Normal file
@ -0,0 +1,46 @@
|
|||||||
|
{
|
||||||
|
"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 Successful": "Copied Successfully",
|
||||||
"common.Copy_failed": "Copy Failed, Please Copy Manually",
|
"common.Copy_failed": "Copy Failed, Please Copy Manually",
|
||||||
"common.Create Failed": "Creation Failed",
|
"common.Create Failed": "Creation Failed",
|
||||||
"common.Create New": "Create",
|
|
||||||
"common.Create Success": "Created Successfully",
|
"common.Create Success": "Created Successfully",
|
||||||
"common.Create Time": "Creation Time",
|
"common.Create Time": "Creation Time",
|
||||||
"common.Creating": "Creating",
|
"common.Creating": "Creating",
|
||||||
|
|||||||
@ -3,7 +3,7 @@
|
|||||||
"add_default_model": "添加预设模型",
|
"add_default_model": "添加预设模型",
|
||||||
"api_key": "API 密钥",
|
"api_key": "API 密钥",
|
||||||
"bills_and_invoices": "账单与发票",
|
"bills_and_invoices": "账单与发票",
|
||||||
"channel": "渠道",
|
"channel": "模型渠道",
|
||||||
"config_model": "模型配置",
|
"config_model": "模型配置",
|
||||||
"confirm_logout": "确认退出登录?",
|
"confirm_logout": "确认退出登录?",
|
||||||
"create_channel": "新增渠道",
|
"create_channel": "新增渠道",
|
||||||
|
|||||||
46
packages/web/i18n/zh-CN/account_model.json
Normal file
46
packages/web/i18n/zh-CN/account_model.json
Normal file
@ -0,0 +1,46 @@
|
|||||||
|
{
|
||||||
|
"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 Successful": "复制成功",
|
||||||
"common.Copy_failed": "复制失败,请手动复制",
|
"common.Copy_failed": "复制失败,请手动复制",
|
||||||
"common.Create Failed": "创建异常",
|
"common.Create Failed": "创建异常",
|
||||||
"common.Create New": "新建",
|
|
||||||
"common.Create Success": "创建成功",
|
"common.Create Success": "创建成功",
|
||||||
"common.Create Time": "创建时间",
|
"common.Create Time": "创建时间",
|
||||||
"common.Creating": "创建中",
|
"common.Creating": "创建中",
|
||||||
|
|||||||
@ -3,7 +3,7 @@
|
|||||||
"add_default_model": "新增預設模型",
|
"add_default_model": "新增預設模型",
|
||||||
"api_key": "API 金鑰",
|
"api_key": "API 金鑰",
|
||||||
"bills_and_invoices": "帳單與發票",
|
"bills_and_invoices": "帳單與發票",
|
||||||
"channel": "頻道",
|
"channel": "模型渠道",
|
||||||
"config_model": "模型配置",
|
"config_model": "模型配置",
|
||||||
"confirm_logout": "確認登出登入?",
|
"confirm_logout": "確認登出登入?",
|
||||||
"create_channel": "新增頻道",
|
"create_channel": "新增頻道",
|
||||||
|
|||||||
44
packages/web/i18n/zh-Hant/account_model.json
Normal file
44
packages/web/i18n/zh-Hant/account_model.json
Normal file
@ -0,0 +1,44 @@
|
|||||||
|
{
|
||||||
|
"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 Successful": "複製成功",
|
||||||
"common.Copy_failed": "複製失敗,請手動複製",
|
"common.Copy_failed": "複製失敗,請手動複製",
|
||||||
"common.Create Failed": "建立失敗",
|
"common.Create Failed": "建立失敗",
|
||||||
"common.Create New": "建立新項目",
|
|
||||||
"common.Create Success": "建立成功",
|
"common.Create Success": "建立成功",
|
||||||
"common.Create Time": "建立時間",
|
"common.Create Time": "建立時間",
|
||||||
"common.Creating": "建立中",
|
"common.Creating": "建立中",
|
||||||
|
|||||||
5
packages/web/types/i18next.d.ts
vendored
5
packages/web/types/i18next.d.ts
vendored
@ -18,6 +18,7 @@ import workflow from '../i18n/zh-CN/workflow.json';
|
|||||||
import user from '../i18n/zh-CN/user.json';
|
import user from '../i18n/zh-CN/user.json';
|
||||||
import chat from '../i18n/zh-CN/chat.json';
|
import chat from '../i18n/zh-CN/chat.json';
|
||||||
import login from '../i18n/zh-CN/login.json';
|
import login from '../i18n/zh-CN/login.json';
|
||||||
|
import account_model from '../i18n/zh-CN/account_model.json';
|
||||||
|
|
||||||
export interface I18nNamespaces {
|
export interface I18nNamespaces {
|
||||||
common: typeof common;
|
common: typeof common;
|
||||||
@ -39,6 +40,7 @@ export interface I18nNamespaces {
|
|||||||
account: typeof account;
|
account: typeof account;
|
||||||
account_team: typeof account_team;
|
account_team: typeof account_team;
|
||||||
account_thirdParty: typeof account_thirdParty;
|
account_thirdParty: typeof account_thirdParty;
|
||||||
|
account_model: typeof account_model;
|
||||||
}
|
}
|
||||||
|
|
||||||
export type I18nNsType = (keyof I18nNamespaces)[];
|
export type I18nNsType = (keyof I18nNamespaces)[];
|
||||||
@ -73,7 +75,8 @@ declare module 'i18next' {
|
|||||||
'account_promotion',
|
'account_promotion',
|
||||||
'account_thirdParty',
|
'account_thirdParty',
|
||||||
'account',
|
'account',
|
||||||
'account_team'
|
'account_team',
|
||||||
|
'account_model'
|
||||||
];
|
];
|
||||||
resources: I18nNamespaces;
|
resources: I18nNamespaces;
|
||||||
}
|
}
|
||||||
|
|||||||
94
pnpm-lock.yaml
generated
94
pnpm-lock.yaml
generated
@ -206,8 +206,8 @@ importers:
|
|||||||
specifier: ^1.6.0
|
specifier: ^1.6.0
|
||||||
version: 1.8.0
|
version: 1.8.0
|
||||||
mongoose:
|
mongoose:
|
||||||
specifier: ^7.0.2
|
specifier: ^8.10.1
|
||||||
version: 7.8.2
|
version: 8.10.2(socks@2.8.3)
|
||||||
multer:
|
multer:
|
||||||
specifier: 1.4.5-lts.1
|
specifier: 1.4.5-lts.1
|
||||||
version: 1.4.5-lts.1
|
version: 1.4.5-lts.1
|
||||||
@ -603,7 +603,7 @@ importers:
|
|||||||
version: 9.0.3
|
version: 9.0.3
|
||||||
'@shelf/jest-mongodb':
|
'@shelf/jest-mongodb':
|
||||||
specifier: ^4.3.2
|
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':
|
'@svgr/webpack':
|
||||||
specifier: ^6.5.1
|
specifier: ^6.5.1
|
||||||
version: 6.5.1
|
version: 6.5.1
|
||||||
@ -645,7 +645,7 @@ importers:
|
|||||||
version: 14.2.3(eslint@8.56.0)(typescript@5.5.3)
|
version: 14.2.3(eslint@8.56.0)(typescript@5.5.3)
|
||||||
mockingoose:
|
mockingoose:
|
||||||
specifier: ^2.16.2
|
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:
|
mongodb-memory-server:
|
||||||
specifier: ^10.0.0
|
specifier: ^10.0.0
|
||||||
version: 10.1.0(socks@2.8.3)
|
version: 10.1.0(socks@2.8.3)
|
||||||
@ -4204,9 +4204,14 @@ packages:
|
|||||||
resolution: {integrity: sha512-ix0EwukN2EpC0SRWIj/7B5+A6uQMQy6KMREI9qQqvgpkV2frH63T0UDVd1SYedL6dNCmDBYB3QtXi4ISk9YT+g==}
|
resolution: {integrity: sha512-ix0EwukN2EpC0SRWIj/7B5+A6uQMQy6KMREI9qQqvgpkV2frH63T0UDVd1SYedL6dNCmDBYB3QtXi4ISk9YT+g==}
|
||||||
engines: {node: '>=14.20.1'}
|
engines: {node: '>=14.20.1'}
|
||||||
|
|
||||||
|
bson@6.10.3:
|
||||||
|
resolution: {integrity: sha512-MTxGsqgYTwfshYWTRdmZRC+M7FnG1b4y7RO7p2k3X24Wq0yv1m77Wsj0BzlPzd/IowgESfsruQCUToa7vbOpPQ==}
|
||||||
|
engines: {node: '>=16.20.1'}
|
||||||
|
|
||||||
bson@6.8.0:
|
bson@6.8.0:
|
||||||
resolution: {integrity: sha512-iOJg8pr7wq2tg/zSlCCHMi3hMm5JTOxLTagf3zxhcenHsFp+c6uOs6K7W5UE7A4QIJGtqh/ZovFNMP4mOPJynQ==}
|
resolution: {integrity: sha512-iOJg8pr7wq2tg/zSlCCHMi3hMm5JTOxLTagf3zxhcenHsFp+c6uOs6K7W5UE7A4QIJGtqh/ZovFNMP4mOPJynQ==}
|
||||||
engines: {node: '>=16.20.1'}
|
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:
|
buffer-alloc-unsafe@1.1.0:
|
||||||
resolution: {integrity: sha512-TEM2iMIEQdJ2yjPJoSIsldnleVaAk1oW3DBVUykyOLsEsFmEc9kn+SFFPz+gl54KQNxlDnAwCXosOS9Okx2xAg==}
|
resolution: {integrity: sha512-TEM2iMIEQdJ2yjPJoSIsldnleVaAk1oW3DBVUykyOLsEsFmEc9kn+SFFPz+gl54KQNxlDnAwCXosOS9Okx2xAg==}
|
||||||
@ -6507,8 +6512,8 @@ packages:
|
|||||||
jws@4.0.0:
|
jws@4.0.0:
|
||||||
resolution: {integrity: sha512-KDncfTmOZoOMTFG4mBlG0qUIOlc03fmzH+ru6RgYVZhPkyiy/92Owlt/8UEN+a4TXR1FQetfIpJE8ApdvdVxTg==}
|
resolution: {integrity: sha512-KDncfTmOZoOMTFG4mBlG0qUIOlc03fmzH+ru6RgYVZhPkyiy/92Owlt/8UEN+a4TXR1FQetfIpJE8ApdvdVxTg==}
|
||||||
|
|
||||||
kareem@2.5.1:
|
kareem@2.6.3:
|
||||||
resolution: {integrity: sha512-7jFxRVm+jD+rkq3kY0iZDJfsO2/t4BBPeEb2qKn2lR/9KhuksYk5hxzfRYWMPV8P/x2d0kHD306YyWLzjjH+uA==}
|
resolution: {integrity: sha512-C3iHfuGUXK2u8/ipq9LfjFfXFxAZMQJJq7vLS45r3D9Y2xQ/m4S8zaR4zMLFWh9AsNPXmcFfUDhTEO8UIC/V6Q==}
|
||||||
engines: {node: '>=12.0.0'}
|
engines: {node: '>=12.0.0'}
|
||||||
|
|
||||||
katex@0.16.11:
|
katex@0.16.11:
|
||||||
@ -7159,6 +7164,33 @@ packages:
|
|||||||
snappy:
|
snappy:
|
||||||
optional: true
|
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:
|
mongodb@6.9.0:
|
||||||
resolution: {integrity: sha512-UMopBVx1LmEUbW/QE0Hw18u583PEDVQmUmVzzBRH0o/xtE9DBRA5ZYLOjpLIa03i8FXjzvQECJcqoMvCXftTUA==}
|
resolution: {integrity: sha512-UMopBVx1LmEUbW/QE0Hw18u583PEDVQmUmVzzBRH0o/xtE9DBRA5ZYLOjpLIa03i8FXjzvQECJcqoMvCXftTUA==}
|
||||||
engines: {node: '>=16.20.1'}
|
engines: {node: '>=16.20.1'}
|
||||||
@ -7186,9 +7218,9 @@ packages:
|
|||||||
socks:
|
socks:
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
mongoose@7.8.2:
|
mongoose@8.10.2:
|
||||||
resolution: {integrity: sha512-/KDcZL84gg8hnmOHRRPK49WtxH3Xsph38c7YqvYPdxEB2OsDAXvwAknGxyEC0F2P3RJCqFOp+523iFCa0p3dfw==}
|
resolution: {integrity: sha512-DvqfK1s/JLwP39ogXULC8ygNDdmDber5ZbxZzELYtkzl9VGJ3K5T2MCLdpTs9I9J6DnkDyIHJwt7IOyMxh/Adw==}
|
||||||
engines: {node: '>=14.20.1'}
|
engines: {node: '>=16.20.1'}
|
||||||
|
|
||||||
mpath@0.9.0:
|
mpath@0.9.0:
|
||||||
resolution: {integrity: sha512-ikJRQTk8hw5DEoFVxHG1Gn9T/xcjtdnOKIU1JTmGjZZlg9LST2mBLmcX3/ICIbgJydT2GOc15RnNy5mHmzfSew==}
|
resolution: {integrity: sha512-ikJRQTk8hw5DEoFVxHG1Gn9T/xcjtdnOKIU1JTmGjZZlg9LST2mBLmcX3/ICIbgJydT2GOc15RnNy5mHmzfSew==}
|
||||||
@ -8348,8 +8380,8 @@ packages:
|
|||||||
resolution: {integrity: sha512-fDW/EZ6Q9RiO8eFG8Hj+7u/oW+XrPTIChwCOM2+th2A6OblDtYYIpve9m+KvI9Z4C9qSEXlaGR6bTEYHReuglA==}
|
resolution: {integrity: sha512-fDW/EZ6Q9RiO8eFG8Hj+7u/oW+XrPTIChwCOM2+th2A6OblDtYYIpve9m+KvI9Z4C9qSEXlaGR6bTEYHReuglA==}
|
||||||
engines: {node: '>= 0.4'}
|
engines: {node: '>= 0.4'}
|
||||||
|
|
||||||
sift@16.0.1:
|
sift@17.1.3:
|
||||||
resolution: {integrity: sha512-Wv6BjQ5zbhW7VFefWusVP33T/EM0vYikCaQ2qR8yULbsilAT8/wQaXvuQ3ptGLpoKx+lihJE3y2UTgKDyyNHZQ==}
|
resolution: {integrity: sha512-Rtlj66/b0ICeFzYTuNvX/EF1igRbbnGSvEyT79McoZa/DeGhMyC5pWKOEsZKnpkqtSeovd5FL/bjHWC3CIIvCQ==}
|
||||||
|
|
||||||
siginfo@2.0.0:
|
siginfo@2.0.0:
|
||||||
resolution: {integrity: sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==}
|
resolution: {integrity: sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==}
|
||||||
@ -12469,11 +12501,11 @@ snapshots:
|
|||||||
|
|
||||||
'@sec-ant/readable-stream@0.4.1': {}
|
'@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:
|
dependencies:
|
||||||
debug: 4.3.4
|
debug: 4.3.4
|
||||||
jest-environment-node: 29.7.0
|
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
|
mongodb-memory-server: 9.2.0
|
||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
- '@aws-sdk/credential-providers'
|
- '@aws-sdk/credential-providers'
|
||||||
@ -13772,6 +13804,8 @@ snapshots:
|
|||||||
|
|
||||||
bson@5.5.1: {}
|
bson@5.5.1: {}
|
||||||
|
|
||||||
|
bson@6.10.3: {}
|
||||||
|
|
||||||
bson@6.8.0: {}
|
bson@6.8.0: {}
|
||||||
|
|
||||||
buffer-alloc-unsafe@1.1.0: {}
|
buffer-alloc-unsafe@1.1.0: {}
|
||||||
@ -14913,7 +14947,7 @@ snapshots:
|
|||||||
eslint: 8.56.0
|
eslint: 8.56.0
|
||||||
eslint-import-resolver-node: 0.3.9
|
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-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-jsx-a11y: 6.9.0(eslint@8.56.0)
|
||||||
eslint-plugin-react: 7.34.4(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)
|
eslint-plugin-react-hooks: 4.6.2(eslint@8.56.0)
|
||||||
@ -14937,7 +14971,7 @@ snapshots:
|
|||||||
enhanced-resolve: 5.17.0
|
enhanced-resolve: 5.17.0
|
||||||
eslint: 8.56.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-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
|
fast-glob: 3.3.2
|
||||||
get-tsconfig: 4.7.5
|
get-tsconfig: 4.7.5
|
||||||
is-core-module: 2.14.0
|
is-core-module: 2.14.0
|
||||||
@ -14959,7 +14993,7 @@ snapshots:
|
|||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
- supports-color
|
- 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:
|
dependencies:
|
||||||
array-includes: 3.1.8
|
array-includes: 3.1.8
|
||||||
array.prototype.findlastindex: 1.2.5
|
array.prototype.findlastindex: 1.2.5
|
||||||
@ -16637,7 +16671,7 @@ snapshots:
|
|||||||
jwa: 2.0.0
|
jwa: 2.0.0
|
||||||
safe-buffer: 5.2.1
|
safe-buffer: 5.2.1
|
||||||
|
|
||||||
kareem@2.5.1: {}
|
kareem@2.6.3: {}
|
||||||
|
|
||||||
katex@0.16.11:
|
katex@0.16.11:
|
||||||
dependencies:
|
dependencies:
|
||||||
@ -17564,9 +17598,9 @@ snapshots:
|
|||||||
dependencies:
|
dependencies:
|
||||||
obliterator: 2.0.4
|
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:
|
dependencies:
|
||||||
mongoose: 7.8.2
|
mongoose: 8.10.2(socks@2.8.3)
|
||||||
|
|
||||||
monaco-editor@0.50.0: {}
|
monaco-editor@0.50.0: {}
|
||||||
|
|
||||||
@ -17660,6 +17694,14 @@ snapshots:
|
|||||||
optionalDependencies:
|
optionalDependencies:
|
||||||
'@mongodb-js/saslprep': 1.1.9
|
'@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):
|
mongodb@6.9.0(socks@2.8.3):
|
||||||
dependencies:
|
dependencies:
|
||||||
'@mongodb-js/saslprep': 1.1.9
|
'@mongodb-js/saslprep': 1.1.9
|
||||||
@ -17668,21 +17710,23 @@ snapshots:
|
|||||||
optionalDependencies:
|
optionalDependencies:
|
||||||
socks: 2.8.3
|
socks: 2.8.3
|
||||||
|
|
||||||
mongoose@7.8.2:
|
mongoose@8.10.2(socks@2.8.3):
|
||||||
dependencies:
|
dependencies:
|
||||||
bson: 5.5.1
|
bson: 6.10.3
|
||||||
kareem: 2.5.1
|
kareem: 2.6.3
|
||||||
mongodb: 5.9.2
|
mongodb: 6.13.1(socks@2.8.3)
|
||||||
mpath: 0.9.0
|
mpath: 0.9.0
|
||||||
mquery: 5.0.0
|
mquery: 5.0.0
|
||||||
ms: 2.1.3
|
ms: 2.1.3
|
||||||
sift: 16.0.1
|
sift: 17.1.3
|
||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
- '@aws-sdk/credential-providers'
|
- '@aws-sdk/credential-providers'
|
||||||
- '@mongodb-js/zstd'
|
- '@mongodb-js/zstd'
|
||||||
|
- gcp-metadata
|
||||||
- kerberos
|
- kerberos
|
||||||
- mongodb-client-encryption
|
- mongodb-client-encryption
|
||||||
- snappy
|
- snappy
|
||||||
|
- socks
|
||||||
- supports-color
|
- supports-color
|
||||||
|
|
||||||
mpath@0.9.0: {}
|
mpath@0.9.0: {}
|
||||||
@ -19015,7 +19059,7 @@ snapshots:
|
|||||||
get-intrinsic: 1.2.4
|
get-intrinsic: 1.2.4
|
||||||
object-inspect: 1.13.2
|
object-inspect: 1.13.2
|
||||||
|
|
||||||
sift@16.0.1: {}
|
sift@17.1.3: {}
|
||||||
|
|
||||||
siginfo@2.0.0: {}
|
siginfo@2.0.0: {}
|
||||||
|
|
||||||
|
|||||||
@ -13,6 +13,10 @@ ROOT_KEY=fdafasd
|
|||||||
OPENAI_BASE_URL=https://api.openai.com/v1
|
OPENAI_BASE_URL=https://api.openai.com/v1
|
||||||
# OpenAI API Key
|
# OpenAI API Key
|
||||||
CHAT_API_KEY=sk-xxxx
|
CHAT_API_KEY=sk-xxxx
|
||||||
|
# ai proxy api
|
||||||
|
AIPROXY_API_ENDPOINT=https://xxx.come
|
||||||
|
AIPROXY_API_TOKEN=xxxxx
|
||||||
|
|
||||||
# 强制将图片转成 base64 传递给模型
|
# 强制将图片转成 base64 传递给模型
|
||||||
MULTIPLE_DATA_TO_BASE64=true
|
MULTIPLE_DATA_TO_BASE64=true
|
||||||
|
|
||||||
|
|||||||
128
projects/app/src/global/aiproxy/constants.ts
Normal file
128
projects/app/src/global/aiproxy/constants.ts
Normal file
@ -0,0 +1,128 @@
|
|||||||
|
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'
|
||||||
|
}
|
||||||
|
};
|
||||||
47
projects/app/src/global/aiproxy/type.d.ts
vendored
Normal file
47
projects/app/src/global/aiproxy/type.d.ts
vendored
Normal file
@ -0,0 +1,47 @@
|
|||||||
|
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;
|
||||||
|
};
|
||||||
722
projects/app/src/pageComponents/account/model/AddModelBox.tsx
Normal file
722
projects/app/src/pageComponents/account/model/AddModelBox.tsx
Normal file
@ -0,0 +1,722 @@
|
|||||||
|
import {
|
||||||
|
Box,
|
||||||
|
Flex,
|
||||||
|
HStack,
|
||||||
|
Table,
|
||||||
|
TableContainer,
|
||||||
|
Tbody,
|
||||||
|
Td,
|
||||||
|
Th,
|
||||||
|
Thead,
|
||||||
|
Tr,
|
||||||
|
Switch,
|
||||||
|
ModalBody,
|
||||||
|
Input,
|
||||||
|
ModalFooter,
|
||||||
|
Button,
|
||||||
|
ButtonProps
|
||||||
|
} from '@chakra-ui/react';
|
||||||
|
import { useTranslation } from 'next-i18next';
|
||||||
|
import React, { useMemo, useRef, useState } from 'react';
|
||||||
|
import {
|
||||||
|
ModelProviderList,
|
||||||
|
ModelProviderIdType,
|
||||||
|
getModelProvider
|
||||||
|
} from '@fastgpt/global/core/ai/provider';
|
||||||
|
import MySelect from '@fastgpt/web/components/common/MySelect';
|
||||||
|
import { ModelTypeEnum } from '@fastgpt/global/core/ai/model';
|
||||||
|
import Avatar from '@fastgpt/web/components/common/Avatar';
|
||||||
|
import { useRequest2 } from '@fastgpt/web/hooks/useRequest';
|
||||||
|
import { getSystemModelDefaultConfig, putSystemModel } from '@/web/core/ai/config';
|
||||||
|
import { SystemModelItemType } from '@fastgpt/service/core/ai/type';
|
||||||
|
import { useForm } from 'react-hook-form';
|
||||||
|
import MyNumberInput from '@fastgpt/web/components/common/Input/NumberInput';
|
||||||
|
import MyTextarea from '@/components/common/Textarea/MyTextarea';
|
||||||
|
import JsonEditor from '@fastgpt/web/components/common/Textarea/JsonEditor';
|
||||||
|
import MyMenu from '@fastgpt/web/components/common/MyMenu';
|
||||||
|
import { useSystemStore } from '@/web/common/system/useSystemStore';
|
||||||
|
import QuestionTip from '@fastgpt/web/components/common/MyTooltip/QuestionTip';
|
||||||
|
import { Prompt_CQJson, Prompt_ExtractJson } from '@fastgpt/global/core/ai/prompt/agent';
|
||||||
|
import MyModal from '@fastgpt/web/components/common/MyModal';
|
||||||
|
|
||||||
|
export const AddModelButton = ({
|
||||||
|
onCreate,
|
||||||
|
...props
|
||||||
|
}: { onCreate: (type: ModelTypeEnum) => void } & ButtonProps) => {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<MyMenu
|
||||||
|
trigger="hover"
|
||||||
|
size="sm"
|
||||||
|
Button={<Button {...props}>{t('account:create_model')}</Button>}
|
||||||
|
menuList={[
|
||||||
|
{
|
||||||
|
children: [
|
||||||
|
{
|
||||||
|
label: t('common:model.type.chat'),
|
||||||
|
onClick: () => onCreate(ModelTypeEnum.llm)
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: t('common:model.type.embedding'),
|
||||||
|
onClick: () => onCreate(ModelTypeEnum.embedding)
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: t('common:model.type.tts'),
|
||||||
|
onClick: () => onCreate(ModelTypeEnum.tts)
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: t('common:model.type.stt'),
|
||||||
|
onClick: () => onCreate(ModelTypeEnum.stt)
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: t('common:model.type.reRank'),
|
||||||
|
onClick: () => onCreate(ModelTypeEnum.rerank)
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const InputStyles = {
|
||||||
|
maxW: '300px',
|
||||||
|
bg: 'myGray.50',
|
||||||
|
w: '100%',
|
||||||
|
rows: 3
|
||||||
|
};
|
||||||
|
export const ModelEditModal = ({
|
||||||
|
modelData,
|
||||||
|
onSuccess,
|
||||||
|
onClose
|
||||||
|
}: {
|
||||||
|
modelData: SystemModelItemType;
|
||||||
|
onSuccess: () => void;
|
||||||
|
onClose: () => void;
|
||||||
|
}) => {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
const { feConfigs } = useSystemStore();
|
||||||
|
|
||||||
|
const { register, getValues, setValue, handleSubmit, watch, reset } =
|
||||||
|
useForm<SystemModelItemType>({
|
||||||
|
defaultValues: modelData
|
||||||
|
});
|
||||||
|
|
||||||
|
const isCustom = !!modelData.isCustom;
|
||||||
|
const isLLMModel = modelData?.type === ModelTypeEnum.llm;
|
||||||
|
const isEmbeddingModel = modelData?.type === ModelTypeEnum.embedding;
|
||||||
|
const isTTSModel = modelData?.type === ModelTypeEnum.tts;
|
||||||
|
const isSTTModel = modelData?.type === ModelTypeEnum.stt;
|
||||||
|
const isRerankModel = modelData?.type === ModelTypeEnum.rerank;
|
||||||
|
|
||||||
|
const provider = watch('provider');
|
||||||
|
const providerData = useMemo(() => getModelProvider(provider), [provider]);
|
||||||
|
|
||||||
|
const providerList = useRef<{ label: any; value: ModelProviderIdType }[]>(
|
||||||
|
ModelProviderList.map((item) => ({
|
||||||
|
label: (
|
||||||
|
<HStack>
|
||||||
|
<Avatar src={item.avatar} w={'1rem'} />
|
||||||
|
<Box>{t(item.name as any)}</Box>
|
||||||
|
</HStack>
|
||||||
|
),
|
||||||
|
value: item.id
|
||||||
|
}))
|
||||||
|
);
|
||||||
|
|
||||||
|
const priceUnit = useMemo(() => {
|
||||||
|
if (isLLMModel || isEmbeddingModel) return '/ 1k Tokens';
|
||||||
|
if (isTTSModel) return `/ 1k ${t('common:unit.character')}`;
|
||||||
|
if (isSTTModel) return `/ 60 ${t('common:unit.seconds')}`;
|
||||||
|
return '';
|
||||||
|
}, [isLLMModel, isEmbeddingModel, isTTSModel, t, isSTTModel]);
|
||||||
|
|
||||||
|
const { runAsync: updateModel, loading: updatingModel } = useRequest2(
|
||||||
|
async (data: SystemModelItemType) => {
|
||||||
|
return putSystemModel({
|
||||||
|
model: data.model,
|
||||||
|
metadata: data
|
||||||
|
}).then(onSuccess);
|
||||||
|
},
|
||||||
|
{
|
||||||
|
onSuccess: () => {
|
||||||
|
onClose();
|
||||||
|
},
|
||||||
|
successToast: t('common:common.Success')
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
const [key, setKey] = useState(0);
|
||||||
|
const { runAsync: loadDefaultConfig, loading: loadingDefaultConfig } = useRequest2(
|
||||||
|
getSystemModelDefaultConfig,
|
||||||
|
{
|
||||||
|
onSuccess(res) {
|
||||||
|
reset({
|
||||||
|
...getValues(),
|
||||||
|
...res
|
||||||
|
});
|
||||||
|
setTimeout(() => {
|
||||||
|
setKey((prev) => prev + 1);
|
||||||
|
}, 0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<MyModal
|
||||||
|
iconSrc={'modal/edit'}
|
||||||
|
title={t('account:model.edit_model')}
|
||||||
|
isOpen
|
||||||
|
onClose={onClose}
|
||||||
|
maxW={['90vw', '80vw']}
|
||||||
|
w={'100%'}
|
||||||
|
h={'100%'}
|
||||||
|
>
|
||||||
|
<ModalBody>
|
||||||
|
<Flex gap={4} key={key}>
|
||||||
|
<TableContainer flex={'1'}>
|
||||||
|
<Table>
|
||||||
|
<Thead>
|
||||||
|
<Tr color={'myGray.600'}>
|
||||||
|
<Th fontSize={'xs'}>{t('account:model.param_name')}</Th>
|
||||||
|
<Th fontSize={'xs'}></Th>
|
||||||
|
</Tr>
|
||||||
|
</Thead>
|
||||||
|
<Tbody>
|
||||||
|
<Tr>
|
||||||
|
<Td>
|
||||||
|
<HStack spacing={1}>
|
||||||
|
<Box>{t('account:model.model_id')}</Box>
|
||||||
|
<QuestionTip label={t('account:model.model_id_tip')} />
|
||||||
|
</HStack>
|
||||||
|
</Td>
|
||||||
|
<Td textAlign={'right'}>
|
||||||
|
{isCustom ? (
|
||||||
|
<Input {...register('model', { required: true })} {...InputStyles} />
|
||||||
|
) : (
|
||||||
|
modelData?.model
|
||||||
|
)}
|
||||||
|
</Td>
|
||||||
|
</Tr>
|
||||||
|
<Tr>
|
||||||
|
<Td>{t('common:model.provider')}</Td>
|
||||||
|
<Td textAlign={'right'}>
|
||||||
|
<MySelect
|
||||||
|
value={provider}
|
||||||
|
onchange={(value) => setValue('provider', value)}
|
||||||
|
list={providerList.current}
|
||||||
|
{...InputStyles}
|
||||||
|
/>
|
||||||
|
</Td>
|
||||||
|
</Tr>
|
||||||
|
<Tr>
|
||||||
|
<Td>
|
||||||
|
<HStack spacing={1}>
|
||||||
|
<Box>{t('account:model.alias')}</Box>
|
||||||
|
<QuestionTip label={t('account:model.alias_tip')} />
|
||||||
|
</HStack>
|
||||||
|
</Td>
|
||||||
|
<Td textAlign={'right'}>
|
||||||
|
<Input {...register('name', { required: true })} {...InputStyles} />
|
||||||
|
</Td>
|
||||||
|
</Tr>
|
||||||
|
{priceUnit && feConfigs?.isPlus && (
|
||||||
|
<>
|
||||||
|
<Tr>
|
||||||
|
<Td>
|
||||||
|
<HStack spacing={1}>
|
||||||
|
<Box>{t('account:model.charsPointsPrice')}</Box>
|
||||||
|
<QuestionTip label={t('account:model.charsPointsPrice_tip')} />
|
||||||
|
</HStack>
|
||||||
|
</Td>
|
||||||
|
<Td>
|
||||||
|
<Flex justify="flex-end">
|
||||||
|
<HStack w={'100%'} maxW={'300px'}>
|
||||||
|
<MyNumberInput
|
||||||
|
flex={'1 0 0'}
|
||||||
|
register={register}
|
||||||
|
name={'charsPointsPrice'}
|
||||||
|
step={0.01}
|
||||||
|
/>
|
||||||
|
<Box fontSize={'sm'}>{priceUnit}</Box>
|
||||||
|
</HStack>
|
||||||
|
</Flex>
|
||||||
|
</Td>
|
||||||
|
</Tr>
|
||||||
|
{isLLMModel && (
|
||||||
|
<>
|
||||||
|
<Tr>
|
||||||
|
<Td>
|
||||||
|
<HStack spacing={1}>
|
||||||
|
<Box>{t('account:model.input_price')}</Box>
|
||||||
|
<QuestionTip label={t('account:model.input_price_tip')} />
|
||||||
|
</HStack>
|
||||||
|
</Td>
|
||||||
|
<Td>
|
||||||
|
<Flex justify="flex-end">
|
||||||
|
<HStack w={'100%'} maxW={'300px'}>
|
||||||
|
<MyNumberInput
|
||||||
|
flex={'1 0 0'}
|
||||||
|
register={register}
|
||||||
|
name={'inputPrice'}
|
||||||
|
step={0.01}
|
||||||
|
/>
|
||||||
|
<Box fontSize={'sm'}>{priceUnit}</Box>
|
||||||
|
</HStack>
|
||||||
|
</Flex>
|
||||||
|
</Td>
|
||||||
|
</Tr>
|
||||||
|
<Tr>
|
||||||
|
<Td>
|
||||||
|
<HStack spacing={1}>
|
||||||
|
<Box>{t('account:model.output_price')}</Box>
|
||||||
|
<QuestionTip label={t('account:model.output_price_tip')} />
|
||||||
|
</HStack>
|
||||||
|
</Td>
|
||||||
|
<Td>
|
||||||
|
<Flex justify="flex-end">
|
||||||
|
<HStack w={'100%'} maxW={'300px'}>
|
||||||
|
<MyNumberInput
|
||||||
|
flex={'1 0 0'}
|
||||||
|
register={register}
|
||||||
|
name={'outputPrice'}
|
||||||
|
step={0.01}
|
||||||
|
/>
|
||||||
|
<Box fontSize={'sm'}>{priceUnit}</Box>
|
||||||
|
</HStack>
|
||||||
|
</Flex>
|
||||||
|
</Td>
|
||||||
|
</Tr>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
{isLLMModel && (
|
||||||
|
<>
|
||||||
|
<Tr>
|
||||||
|
<Td>{t('common:core.ai.Max context')}</Td>
|
||||||
|
<Td textAlign={'right'}>
|
||||||
|
<Flex justifyContent={'flex-end'}>
|
||||||
|
<MyNumberInput
|
||||||
|
register={register}
|
||||||
|
isRequired
|
||||||
|
name="maxContext"
|
||||||
|
{...InputStyles}
|
||||||
|
/>
|
||||||
|
</Flex>
|
||||||
|
</Td>
|
||||||
|
</Tr>
|
||||||
|
<Tr>
|
||||||
|
<Td>{t('account:model.max_quote')}</Td>
|
||||||
|
<Td textAlign={'right'}>
|
||||||
|
<Flex justifyContent={'flex-end'}>
|
||||||
|
<MyNumberInput
|
||||||
|
register={register}
|
||||||
|
isRequired
|
||||||
|
name="quoteMaxToken"
|
||||||
|
{...InputStyles}
|
||||||
|
/>
|
||||||
|
</Flex>
|
||||||
|
</Td>
|
||||||
|
</Tr>
|
||||||
|
<Tr>
|
||||||
|
<Td>{t('common:core.chat.response.module maxToken')}</Td>
|
||||||
|
<Td textAlign={'right'}>
|
||||||
|
<Flex justifyContent={'flex-end'}>
|
||||||
|
<MyNumberInput register={register} name="maxResponse" {...InputStyles} />
|
||||||
|
</Flex>
|
||||||
|
</Td>
|
||||||
|
</Tr>
|
||||||
|
<Tr>
|
||||||
|
<Td>{t('account:model.max_temperature')}</Td>
|
||||||
|
<Td textAlign={'right'}>
|
||||||
|
<Flex justifyContent={'flex-end'}>
|
||||||
|
<MyNumberInput
|
||||||
|
register={register}
|
||||||
|
name="maxTemperature"
|
||||||
|
step={0.1}
|
||||||
|
{...InputStyles}
|
||||||
|
/>
|
||||||
|
</Flex>
|
||||||
|
</Td>
|
||||||
|
</Tr>
|
||||||
|
<Tr>
|
||||||
|
<Td>
|
||||||
|
<HStack spacing={1}>
|
||||||
|
<Box>{t('account:model.show_top_p')}</Box>
|
||||||
|
</HStack>
|
||||||
|
</Td>
|
||||||
|
<Td textAlign={'right'}>
|
||||||
|
<Flex justifyContent={'flex-end'}>
|
||||||
|
<Switch {...register('showTopP')} />
|
||||||
|
</Flex>
|
||||||
|
</Td>
|
||||||
|
</Tr>
|
||||||
|
<Tr>
|
||||||
|
<Td>
|
||||||
|
<HStack spacing={1}>
|
||||||
|
<Box>{t('account:model.show_stop_sign')}</Box>
|
||||||
|
</HStack>
|
||||||
|
</Td>
|
||||||
|
<Td textAlign={'right'}>
|
||||||
|
<Flex justifyContent={'flex-end'}>
|
||||||
|
<Switch {...register('showStopSign')} />
|
||||||
|
</Flex>
|
||||||
|
</Td>
|
||||||
|
</Tr>
|
||||||
|
<Tr>
|
||||||
|
<Td>{t('account:model.response_format')}</Td>
|
||||||
|
<Td textAlign={'right'}>
|
||||||
|
<JsonEditor
|
||||||
|
value={JSON.stringify(getValues('responseFormatList'), null, 2)}
|
||||||
|
resize
|
||||||
|
onChange={(e) => {
|
||||||
|
if (!e) {
|
||||||
|
setValue('responseFormatList', []);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
setValue('responseFormatList', JSON.parse(e));
|
||||||
|
} catch (error) {
|
||||||
|
console.error(error);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
{...InputStyles}
|
||||||
|
/>
|
||||||
|
</Td>
|
||||||
|
</Tr>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
{isEmbeddingModel && (
|
||||||
|
<>
|
||||||
|
<Tr>
|
||||||
|
<Td>
|
||||||
|
<HStack spacing={1}>
|
||||||
|
<Box>{t('account:model.normalization')}</Box>
|
||||||
|
<QuestionTip label={t('account:model.normalization_tip')} />
|
||||||
|
</HStack>
|
||||||
|
</Td>
|
||||||
|
<Td textAlign={'right'}>
|
||||||
|
<Flex justifyContent={'flex-end'}>
|
||||||
|
<Switch {...register('normalization')} />
|
||||||
|
</Flex>
|
||||||
|
</Td>
|
||||||
|
</Tr>
|
||||||
|
<Tr>
|
||||||
|
<Td>
|
||||||
|
<HStack spacing={1}>
|
||||||
|
<Box>{t('account:model.default_token')}</Box>
|
||||||
|
<QuestionTip label={t('account:model.default_token_tip')} />
|
||||||
|
</HStack>
|
||||||
|
</Td>
|
||||||
|
<Td textAlign={'right'}>
|
||||||
|
<Flex justifyContent={'flex-end'}>
|
||||||
|
<MyNumberInput
|
||||||
|
register={register}
|
||||||
|
isRequired
|
||||||
|
name="defaultToken"
|
||||||
|
{...InputStyles}
|
||||||
|
/>
|
||||||
|
</Flex>
|
||||||
|
</Td>
|
||||||
|
</Tr>
|
||||||
|
<Tr>
|
||||||
|
<Td>{t('common:core.ai.Max context')}</Td>
|
||||||
|
<Td textAlign={'right'}>
|
||||||
|
<Flex justifyContent={'flex-end'}>
|
||||||
|
<MyNumberInput
|
||||||
|
register={register}
|
||||||
|
isRequired
|
||||||
|
name="maxToken"
|
||||||
|
{...InputStyles}
|
||||||
|
/>
|
||||||
|
</Flex>
|
||||||
|
</Td>
|
||||||
|
</Tr>
|
||||||
|
<Tr>
|
||||||
|
<Td>
|
||||||
|
<HStack spacing={1}>
|
||||||
|
<Box>{t('account:model.defaultConfig')}</Box>
|
||||||
|
<QuestionTip label={t('account:model.defaultConfig_tip')} />
|
||||||
|
</HStack>
|
||||||
|
</Td>
|
||||||
|
<Td textAlign={'right'}>
|
||||||
|
<Flex justifyContent={'flex-end'}>
|
||||||
|
<JsonEditor
|
||||||
|
value={JSON.stringify(getValues('defaultConfig'), null, 2)}
|
||||||
|
onChange={(e) => {
|
||||||
|
if (!e) {
|
||||||
|
setValue('defaultConfig', {});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
setValue('defaultConfig', JSON.parse(e));
|
||||||
|
} catch (error) {
|
||||||
|
console.error(error);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
{...InputStyles}
|
||||||
|
/>
|
||||||
|
</Flex>
|
||||||
|
</Td>
|
||||||
|
</Tr>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
{isTTSModel && (
|
||||||
|
<>
|
||||||
|
<Tr>
|
||||||
|
<Td>
|
||||||
|
<HStack spacing={1}>
|
||||||
|
<Box>{t('account:model.voices')}</Box>
|
||||||
|
<QuestionTip label={t('account:model.voices_tip')} />
|
||||||
|
</HStack>
|
||||||
|
</Td>
|
||||||
|
<Td textAlign={'right'}>
|
||||||
|
<Flex justifyContent={'flex-end'}>
|
||||||
|
<JsonEditor
|
||||||
|
value={JSON.stringify(getValues('voices'), null, 2)}
|
||||||
|
onChange={(e) => {
|
||||||
|
try {
|
||||||
|
setValue('voices', JSON.parse(e));
|
||||||
|
} catch (error) {
|
||||||
|
console.error(error);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
{...InputStyles}
|
||||||
|
/>
|
||||||
|
</Flex>
|
||||||
|
</Td>
|
||||||
|
</Tr>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
<Tr>
|
||||||
|
<Td>
|
||||||
|
<HStack spacing={1}>
|
||||||
|
<Box>{t('account:model.request_url')}</Box>
|
||||||
|
<QuestionTip label={t('account:model.request_url_tip')} />
|
||||||
|
</HStack>
|
||||||
|
</Td>
|
||||||
|
<Td textAlign={'right'}>
|
||||||
|
<Input {...register('requestUrl')} {...InputStyles} />
|
||||||
|
</Td>
|
||||||
|
</Tr>
|
||||||
|
<Tr>
|
||||||
|
<Td>
|
||||||
|
<HStack spacing={1}>
|
||||||
|
<Box>{t('account:model.request_auth')}</Box>
|
||||||
|
<QuestionTip label={t('account:model.request_auth_tip')} />
|
||||||
|
</HStack>
|
||||||
|
</Td>
|
||||||
|
<Td textAlign={'right'}>
|
||||||
|
<Input {...register('requestAuth')} {...InputStyles} />
|
||||||
|
</Td>
|
||||||
|
</Tr>
|
||||||
|
</Tbody>
|
||||||
|
</Table>
|
||||||
|
</TableContainer>
|
||||||
|
{isLLMModel && (
|
||||||
|
<TableContainer flex={'1'}>
|
||||||
|
<Table>
|
||||||
|
<Thead>
|
||||||
|
<Tr color={'myGray.600'}>
|
||||||
|
<Th fontSize={'xs'}>{t('account:model.param_name')}</Th>
|
||||||
|
<Th fontSize={'xs'}></Th>
|
||||||
|
</Tr>
|
||||||
|
</Thead>
|
||||||
|
<Tbody>
|
||||||
|
<Tr>
|
||||||
|
<Td>
|
||||||
|
<HStack spacing={1}>
|
||||||
|
<Box>{t('account:model.tool_choice')}</Box>
|
||||||
|
<QuestionTip label={t('account:model.tool_choice_tip')} />
|
||||||
|
</HStack>
|
||||||
|
</Td>
|
||||||
|
<Td textAlign={'right'}>
|
||||||
|
<Flex justifyContent={'flex-end'}>
|
||||||
|
<Switch {...register('toolChoice')} />
|
||||||
|
</Flex>
|
||||||
|
</Td>
|
||||||
|
</Tr>
|
||||||
|
<Tr>
|
||||||
|
<Td>
|
||||||
|
<HStack spacing={1}>
|
||||||
|
<Box>{t('account:model.function_call')}</Box>
|
||||||
|
<QuestionTip label={t('account:model.function_call_tip')} />
|
||||||
|
</HStack>
|
||||||
|
</Td>
|
||||||
|
<Td textAlign={'right'}>
|
||||||
|
<Flex justifyContent={'flex-end'}>
|
||||||
|
<Switch {...register('functionCall')} />
|
||||||
|
</Flex>
|
||||||
|
</Td>
|
||||||
|
</Tr>
|
||||||
|
<Tr>
|
||||||
|
<Td>
|
||||||
|
<HStack spacing={1}>
|
||||||
|
<Box>{t('account:model.vision')}</Box>
|
||||||
|
<QuestionTip label={t('account:model.vision_tip')} />
|
||||||
|
</HStack>
|
||||||
|
</Td>
|
||||||
|
<Td textAlign={'right'}>
|
||||||
|
<Flex justifyContent={'flex-end'}>
|
||||||
|
<Switch {...register('vision')} />
|
||||||
|
</Flex>
|
||||||
|
</Td>
|
||||||
|
</Tr>
|
||||||
|
<Tr>
|
||||||
|
<Td>
|
||||||
|
<HStack spacing={1}>
|
||||||
|
<Box>{t('account:model.reasoning')}</Box>
|
||||||
|
<QuestionTip label={t('account:model.reasoning_tip')} />
|
||||||
|
</HStack>
|
||||||
|
</Td>
|
||||||
|
<Td textAlign={'right'}>
|
||||||
|
<Flex justifyContent={'flex-end'}>
|
||||||
|
<Switch {...register('reasoning')} />
|
||||||
|
</Flex>
|
||||||
|
</Td>
|
||||||
|
</Tr>
|
||||||
|
{feConfigs?.isPlus && (
|
||||||
|
<Tr>
|
||||||
|
<Td>
|
||||||
|
<HStack spacing={1}>
|
||||||
|
<Box>{t('account:model.censor')}</Box>
|
||||||
|
<QuestionTip label={t('account:model.censor_tip')} />
|
||||||
|
</HStack>
|
||||||
|
</Td>
|
||||||
|
<Td textAlign={'right'}>
|
||||||
|
<Flex justifyContent={'flex-end'}>
|
||||||
|
<Switch {...register('censor')} />
|
||||||
|
</Flex>
|
||||||
|
</Td>
|
||||||
|
</Tr>
|
||||||
|
)}
|
||||||
|
<Tr>
|
||||||
|
<Td>{t('account:model.dataset_process')}</Td>
|
||||||
|
<Td textAlign={'right'}>
|
||||||
|
<Flex justifyContent={'flex-end'}>
|
||||||
|
<Switch {...register('datasetProcess')} />
|
||||||
|
</Flex>
|
||||||
|
</Td>
|
||||||
|
</Tr>
|
||||||
|
<Tr>
|
||||||
|
<Td>{t('account:model.used_in_classify')}</Td>
|
||||||
|
<Td textAlign={'right'}>
|
||||||
|
<Flex justifyContent={'flex-end'}>
|
||||||
|
<Switch {...register('usedInClassify')} />
|
||||||
|
</Flex>
|
||||||
|
</Td>
|
||||||
|
</Tr>
|
||||||
|
<Tr>
|
||||||
|
<Td>{t('account:model.used_in_extract_fields')}</Td>
|
||||||
|
<Td textAlign={'right'}>
|
||||||
|
<Flex justifyContent={'flex-end'}>
|
||||||
|
<Switch {...register('usedInExtractFields')} />
|
||||||
|
</Flex>
|
||||||
|
</Td>
|
||||||
|
</Tr>
|
||||||
|
<Tr>
|
||||||
|
<Td>{t('account:model.used_in_tool_call')}</Td>
|
||||||
|
<Td textAlign={'right'}>
|
||||||
|
<Flex justifyContent={'flex-end'}>
|
||||||
|
<Switch {...register('usedInToolCall')} />
|
||||||
|
</Flex>
|
||||||
|
</Td>
|
||||||
|
</Tr>
|
||||||
|
<Tr>
|
||||||
|
<Td>
|
||||||
|
<HStack spacing={1}>
|
||||||
|
<Box>{t('account:model.default_system_chat_prompt')}</Box>
|
||||||
|
<QuestionTip label={t('account:model.default_system_chat_prompt_tip')} />
|
||||||
|
</HStack>
|
||||||
|
</Td>
|
||||||
|
<Td textAlign={'right'}>
|
||||||
|
<MyTextarea {...register('defaultSystemChatPrompt')} {...InputStyles} />
|
||||||
|
</Td>
|
||||||
|
</Tr>
|
||||||
|
<Tr>
|
||||||
|
<Td>
|
||||||
|
<HStack spacing={1}>
|
||||||
|
<Box>{t('account:model.custom_cq_prompt')}</Box>
|
||||||
|
<QuestionTip
|
||||||
|
label={t('account:model.custom_cq_prompt_tip', { prompt: Prompt_CQJson })}
|
||||||
|
/>
|
||||||
|
</HStack>
|
||||||
|
</Td>
|
||||||
|
<Td textAlign={'right'}>
|
||||||
|
<MyTextarea {...register('customCQPrompt')} {...InputStyles} />
|
||||||
|
</Td>
|
||||||
|
</Tr>
|
||||||
|
<Tr>
|
||||||
|
<Td>
|
||||||
|
<HStack spacing={1}>
|
||||||
|
<Box>{t('account:model.custom_extract_prompt')}</Box>
|
||||||
|
<QuestionTip
|
||||||
|
label={t('account:model.custom_extract_prompt_tip', {
|
||||||
|
prompt: Prompt_ExtractJson
|
||||||
|
})}
|
||||||
|
/>
|
||||||
|
</HStack>
|
||||||
|
</Td>
|
||||||
|
<Td textAlign={'right'}>
|
||||||
|
<MyTextarea {...register('customExtractPrompt')} {...InputStyles} />
|
||||||
|
</Td>
|
||||||
|
</Tr>
|
||||||
|
<Tr>
|
||||||
|
<Td>
|
||||||
|
<HStack spacing={1}>
|
||||||
|
<Box>{t('account:model.default_config')}</Box>
|
||||||
|
<QuestionTip label={t('account:model.default_config_tip')} />
|
||||||
|
</HStack>
|
||||||
|
</Td>
|
||||||
|
<Td textAlign={'right'}>
|
||||||
|
<JsonEditor
|
||||||
|
value={JSON.stringify(getValues('defaultConfig'), null, 2)}
|
||||||
|
resize
|
||||||
|
onChange={(e) => {
|
||||||
|
if (!e) {
|
||||||
|
setValue('defaultConfig', {});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
setValue('defaultConfig', JSON.parse(e));
|
||||||
|
} catch (error) {
|
||||||
|
console.error(error);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
{...InputStyles}
|
||||||
|
/>
|
||||||
|
</Td>
|
||||||
|
</Tr>
|
||||||
|
</Tbody>
|
||||||
|
</Table>
|
||||||
|
</TableContainer>
|
||||||
|
)}
|
||||||
|
</Flex>
|
||||||
|
</ModalBody>
|
||||||
|
<ModalFooter>
|
||||||
|
{!modelData.isCustom && (
|
||||||
|
<Button
|
||||||
|
isLoading={loadingDefaultConfig}
|
||||||
|
variant={'whiteBase'}
|
||||||
|
mr={4}
|
||||||
|
onClick={() => loadDefaultConfig(modelData.model)}
|
||||||
|
>
|
||||||
|
{t('account:reset_default')}
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
<Button variant={'whiteBase'} mr={4} onClick={onClose}>
|
||||||
|
{t('common:common.Cancel')}
|
||||||
|
</Button>
|
||||||
|
<Button isLoading={updatingModel} onClick={handleSubmit(updateModel)}>
|
||||||
|
{t('common:common.Confirm')}
|
||||||
|
</Button>
|
||||||
|
</ModalFooter>
|
||||||
|
</MyModal>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default function Dom() {
|
||||||
|
return <></>;
|
||||||
|
}
|
||||||
@ -0,0 +1,499 @@
|
|||||||
|
import { aiproxyIdMap } from '@/global/aiproxy/constants';
|
||||||
|
import { ChannelInfoType } from '@/global/aiproxy/type';
|
||||||
|
import {
|
||||||
|
Box,
|
||||||
|
BoxProps,
|
||||||
|
Button,
|
||||||
|
Flex,
|
||||||
|
Input,
|
||||||
|
MenuItemProps,
|
||||||
|
ModalBody,
|
||||||
|
ModalFooter,
|
||||||
|
useDisclosure,
|
||||||
|
Menu,
|
||||||
|
MenuButton,
|
||||||
|
MenuList,
|
||||||
|
MenuItem,
|
||||||
|
HStack,
|
||||||
|
useOutsideClick
|
||||||
|
} from '@chakra-ui/react';
|
||||||
|
import FormLabel from '@fastgpt/web/components/common/MyBox/FormLabel';
|
||||||
|
import MyModal from '@fastgpt/web/components/common/MyModal';
|
||||||
|
import MySelect from '@fastgpt/web/components/common/MySelect';
|
||||||
|
import { useTranslation } from 'next-i18next';
|
||||||
|
import React, { useCallback, useMemo, useRef, useState } from 'react';
|
||||||
|
import { useForm } from 'react-hook-form';
|
||||||
|
import { AddModelButton } from '../AddModelBox';
|
||||||
|
import dynamic from 'next/dynamic';
|
||||||
|
import { SystemModelItemType } from '@fastgpt/service/core/ai/type';
|
||||||
|
import { ModelTypeEnum } from '@fastgpt/global/core/ai/model';
|
||||||
|
import { useSystemStore } from '@/web/common/system/useSystemStore';
|
||||||
|
import { getSystemModelList } from '@/web/core/ai/config';
|
||||||
|
import { useRequest2 } from '@fastgpt/web/hooks/useRequest';
|
||||||
|
import { getModelProvider } from '@fastgpt/global/core/ai/provider';
|
||||||
|
import MyIcon from '@fastgpt/web/components/common/Icon';
|
||||||
|
import MyAvatar from '@fastgpt/web/components/common/Avatar';
|
||||||
|
import MyTag from '@fastgpt/web/components/common/Tag/index';
|
||||||
|
import { useCopyData } from '@fastgpt/web/hooks/useCopyData';
|
||||||
|
import QuestionTip from '@fastgpt/web/components/common/MyTooltip/QuestionTip';
|
||||||
|
import JsonEditor from '@fastgpt/web/components/common/Textarea/JsonEditor';
|
||||||
|
import { getChannelProviders, postCreateChannel, putChannel } from '@/web/core/ai/channel';
|
||||||
|
import CopyBox from '@fastgpt/web/components/common/String/CopyBox';
|
||||||
|
|
||||||
|
const ModelEditModal = dynamic(() => import('../AddModelBox').then((mod) => mod.ModelEditModal));
|
||||||
|
|
||||||
|
const LabelStyles: BoxProps = {
|
||||||
|
fontSize: 'sm',
|
||||||
|
color: 'myGray.900',
|
||||||
|
flex: '0 0 70px'
|
||||||
|
};
|
||||||
|
const EditChannelModal = ({
|
||||||
|
defaultConfig,
|
||||||
|
onClose,
|
||||||
|
onSuccess
|
||||||
|
}: {
|
||||||
|
defaultConfig: ChannelInfoType;
|
||||||
|
onClose: () => void;
|
||||||
|
onSuccess: () => void;
|
||||||
|
}) => {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
const { defaultModels } = useSystemStore();
|
||||||
|
const isEdit = defaultConfig.id !== 0;
|
||||||
|
|
||||||
|
const { register, handleSubmit, watch, setValue } = useForm({
|
||||||
|
defaultValues: defaultConfig
|
||||||
|
});
|
||||||
|
|
||||||
|
const providerType = watch('type');
|
||||||
|
const { data: providerList = [] } = useRequest2(
|
||||||
|
() =>
|
||||||
|
getChannelProviders().then((res) => {
|
||||||
|
return Object.entries(res)
|
||||||
|
.map(([key, value]) => {
|
||||||
|
const mapData = aiproxyIdMap[key as any] ?? {
|
||||||
|
label: value.name,
|
||||||
|
provider: 'Other'
|
||||||
|
};
|
||||||
|
const provider = getModelProvider(mapData.provider);
|
||||||
|
return {
|
||||||
|
order: provider.order,
|
||||||
|
defaultBaseUrl: value.defaultBaseUrl,
|
||||||
|
keyHelp: value.keyHelp,
|
||||||
|
icon: provider.avatar,
|
||||||
|
label: t(mapData.label as any),
|
||||||
|
value: Number(key)
|
||||||
|
};
|
||||||
|
})
|
||||||
|
.sort((a, b) => a.order - b.order);
|
||||||
|
}),
|
||||||
|
{
|
||||||
|
manual: false
|
||||||
|
}
|
||||||
|
);
|
||||||
|
const selectedProvider = useMemo(() => {
|
||||||
|
const res = providerList.find((item) => item.value === providerType);
|
||||||
|
return res;
|
||||||
|
}, [providerList, providerType]);
|
||||||
|
|
||||||
|
const [editModelData, setEditModelData] = useState<SystemModelItemType>();
|
||||||
|
const onCreateModel = (type: ModelTypeEnum) => {
|
||||||
|
const defaultModel = defaultModels[type];
|
||||||
|
|
||||||
|
setEditModelData({
|
||||||
|
...defaultModel,
|
||||||
|
model: '',
|
||||||
|
name: '',
|
||||||
|
charsPointsPrice: 0,
|
||||||
|
inputPrice: undefined,
|
||||||
|
outputPrice: undefined,
|
||||||
|
|
||||||
|
isCustom: true,
|
||||||
|
isActive: true,
|
||||||
|
// @ts-ignore
|
||||||
|
type
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const models = watch('models');
|
||||||
|
const {
|
||||||
|
data: systemModelList = [],
|
||||||
|
runAsync: refreshSystemModelList,
|
||||||
|
loading: loadingModels
|
||||||
|
} = useRequest2(getSystemModelList, {
|
||||||
|
manual: false
|
||||||
|
});
|
||||||
|
const modelList = useMemo(() => {
|
||||||
|
const currentProvider = aiproxyIdMap[providerType]?.provider;
|
||||||
|
return systemModelList
|
||||||
|
.map((item) => {
|
||||||
|
const provider = getModelProvider(item.provider);
|
||||||
|
|
||||||
|
return {
|
||||||
|
provider: item.provider,
|
||||||
|
icon: provider.avatar,
|
||||||
|
label: item.model,
|
||||||
|
value: item.model
|
||||||
|
};
|
||||||
|
})
|
||||||
|
.sort((a, b) => {
|
||||||
|
// sort by provider, same provider first
|
||||||
|
if (a.provider === currentProvider && b.provider !== currentProvider) return -1;
|
||||||
|
if (a.provider !== currentProvider && b.provider === currentProvider) return 1;
|
||||||
|
return 0;
|
||||||
|
});
|
||||||
|
}, [providerType, systemModelList]);
|
||||||
|
|
||||||
|
const modelMapping = watch('model_mapping');
|
||||||
|
|
||||||
|
const { runAsync: onSubmit, loading: loadingCreate } = useRequest2(
|
||||||
|
(data: ChannelInfoType) => {
|
||||||
|
if (data.models.length === 0) {
|
||||||
|
return Promise.reject(t('account_model:selected_model_empty'));
|
||||||
|
}
|
||||||
|
return isEdit ? putChannel(data) : postCreateChannel(data);
|
||||||
|
},
|
||||||
|
{
|
||||||
|
onSuccess() {
|
||||||
|
onSuccess();
|
||||||
|
onClose();
|
||||||
|
},
|
||||||
|
successToast: isEdit ? t('common:common.Update Success') : t('common:common.Create Success'),
|
||||||
|
manual: true
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
const isLoading = loadingModels || loadingCreate;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<MyModal
|
||||||
|
isLoading={isLoading}
|
||||||
|
iconSrc={'modal/setting'}
|
||||||
|
title={t('account_model:edit_channel')}
|
||||||
|
onClose={onClose}
|
||||||
|
w={'100%'}
|
||||||
|
maxW={['90vw', '800px']}
|
||||||
|
>
|
||||||
|
<ModalBody>
|
||||||
|
{/* Chnnel name */}
|
||||||
|
<Box>
|
||||||
|
<FormLabel required {...LabelStyles}>
|
||||||
|
{t('account_model:channel_name')}
|
||||||
|
</FormLabel>
|
||||||
|
<Input mt={1} {...register('name', { required: true })} />
|
||||||
|
</Box>
|
||||||
|
{/* Provider */}
|
||||||
|
<Box alignItems={'center'} mt={4}>
|
||||||
|
<FormLabel required {...LabelStyles}>
|
||||||
|
{t('account_model:channel_type')}
|
||||||
|
</FormLabel>
|
||||||
|
<Box mt={1}>
|
||||||
|
<MySelect
|
||||||
|
list={providerList}
|
||||||
|
placeholder={t('account_model:select_provider_placeholder')}
|
||||||
|
value={providerType}
|
||||||
|
isSearch
|
||||||
|
onchange={(val) => {
|
||||||
|
setValue('type', val);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</Box>
|
||||||
|
</Box>
|
||||||
|
{/* Model */}
|
||||||
|
<Box mt={4}>
|
||||||
|
<Flex alignItems={'center'}>
|
||||||
|
<FormLabel required flex={'1 0 0'}>
|
||||||
|
{t('account_model:model')}({models.length})
|
||||||
|
</FormLabel>
|
||||||
|
|
||||||
|
<AddModelButton onCreate={onCreateModel} size={'sm'} variant={'outline'} />
|
||||||
|
<Button ml={2} size={'sm'} variant={'outline'} onClick={() => setValue('models', [])}>
|
||||||
|
{t('account_model:clear_model')}
|
||||||
|
</Button>
|
||||||
|
</Flex>
|
||||||
|
<Box mt={2}>
|
||||||
|
<MultipleSelect
|
||||||
|
value={models}
|
||||||
|
list={modelList}
|
||||||
|
onSelect={(val) => {
|
||||||
|
setValue('models', val);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</Box>
|
||||||
|
</Box>
|
||||||
|
{/* Mapping */}
|
||||||
|
<Box mt={4}>
|
||||||
|
<HStack>
|
||||||
|
<FormLabel>{t('account_model:mapping')}</FormLabel>
|
||||||
|
<QuestionTip label={t('account_model:mapping_tip')} />
|
||||||
|
</HStack>
|
||||||
|
<Box mt={2}>
|
||||||
|
<JsonEditor
|
||||||
|
value={JSON.stringify(modelMapping, null, 2)}
|
||||||
|
onChange={(val) => {
|
||||||
|
if (!val) {
|
||||||
|
setValue('model_mapping', {});
|
||||||
|
} else {
|
||||||
|
try {
|
||||||
|
setValue('model_mapping', JSON.parse(val));
|
||||||
|
} catch (error) {}
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</Box>
|
||||||
|
</Box>
|
||||||
|
{/* url and key */}
|
||||||
|
<Box mt={4}>
|
||||||
|
<Flex alignItems={'center'}>
|
||||||
|
<FormLabel>{t('account_model:base_url')}</FormLabel>
|
||||||
|
{selectedProvider && (
|
||||||
|
<Flex alignItems={'center'} fontSize={'xs'}>
|
||||||
|
<Box>{'('}</Box>
|
||||||
|
<Box mr={1}>{t('account_model:default_url')}:</Box>
|
||||||
|
<CopyBox value={selectedProvider?.defaultBaseUrl || ''}>
|
||||||
|
{selectedProvider?.defaultBaseUrl || ''}
|
||||||
|
</CopyBox>
|
||||||
|
<Box>{')'}</Box>
|
||||||
|
</Flex>
|
||||||
|
)}
|
||||||
|
</Flex>
|
||||||
|
<Input
|
||||||
|
mt={1}
|
||||||
|
{...register('base_url')}
|
||||||
|
placeholder={selectedProvider?.defaultBaseUrl || 'https://api.openai.com/v1'}
|
||||||
|
/>
|
||||||
|
</Box>
|
||||||
|
<Box mt={4}>
|
||||||
|
<Flex alignItems={'center'}>
|
||||||
|
<FormLabel>{t('account_model:api_key')}</FormLabel>
|
||||||
|
{selectedProvider?.keyHelp && (
|
||||||
|
<Flex alignItems={'center'} fontSize={'xs'}>
|
||||||
|
<Box>{'('}</Box>
|
||||||
|
<Box mr={1}>{t('account_model:key_type')}</Box>
|
||||||
|
<Box>{selectedProvider.keyHelp}</Box>
|
||||||
|
<Box>{')'}</Box>
|
||||||
|
</Flex>
|
||||||
|
)}
|
||||||
|
</Flex>
|
||||||
|
<Input
|
||||||
|
mt={1}
|
||||||
|
{...register('key')}
|
||||||
|
placeholder={selectedProvider?.keyHelp || 'sk-1234567890'}
|
||||||
|
/>
|
||||||
|
</Box>
|
||||||
|
</ModalBody>
|
||||||
|
<ModalFooter>
|
||||||
|
<Button variant={'outline'} onClick={onClose} mr={4}>
|
||||||
|
{t('common:common.Cancel')}
|
||||||
|
</Button>
|
||||||
|
<Button variant={'primary'} onClick={handleSubmit(onSubmit)}>
|
||||||
|
{isEdit ? t('common:common.Update') : t('common:new_create')}
|
||||||
|
</Button>
|
||||||
|
</ModalFooter>
|
||||||
|
</MyModal>
|
||||||
|
{!!editModelData && (
|
||||||
|
<ModelEditModal
|
||||||
|
modelData={editModelData}
|
||||||
|
onSuccess={refreshSystemModelList}
|
||||||
|
onClose={() => setEditModelData(undefined)}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
export default EditChannelModal;
|
||||||
|
|
||||||
|
type SelectProps = {
|
||||||
|
list: {
|
||||||
|
icon?: string;
|
||||||
|
label: string;
|
||||||
|
value: string;
|
||||||
|
}[];
|
||||||
|
value: string[];
|
||||||
|
onSelect: (val: string[]) => void;
|
||||||
|
};
|
||||||
|
const menuItemStyles: MenuItemProps = {
|
||||||
|
borderRadius: 'sm',
|
||||||
|
py: 2,
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
_hover: {
|
||||||
|
backgroundColor: 'myGray.100'
|
||||||
|
},
|
||||||
|
_notLast: {
|
||||||
|
mb: 0.5
|
||||||
|
}
|
||||||
|
};
|
||||||
|
const MultipleSelect = ({ value = [], list = [], onSelect }: SelectProps) => {
|
||||||
|
const ref = useRef<HTMLDivElement>(null);
|
||||||
|
const BoxRef = useRef<HTMLDivElement>(null);
|
||||||
|
|
||||||
|
const { t } = useTranslation();
|
||||||
|
const { isOpen, onOpen, onClose } = useDisclosure();
|
||||||
|
const { copyData } = useCopyData();
|
||||||
|
|
||||||
|
const onclickItem = useCallback(
|
||||||
|
(val: string) => {
|
||||||
|
if (value.includes(val)) {
|
||||||
|
onSelect(value.filter((i) => i !== val));
|
||||||
|
} else {
|
||||||
|
onSelect([...value, val]);
|
||||||
|
BoxRef.current?.scrollTo({
|
||||||
|
top: BoxRef.current.scrollHeight
|
||||||
|
});
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[value, onSelect]
|
||||||
|
);
|
||||||
|
|
||||||
|
const [search, setSearch] = useState('');
|
||||||
|
|
||||||
|
const filterUnSelected = useMemo(() => {
|
||||||
|
return list
|
||||||
|
.filter((item) => !value.includes(item.value))
|
||||||
|
.filter((item) => {
|
||||||
|
if (!search) return true;
|
||||||
|
const regx = new RegExp(search, 'i');
|
||||||
|
return regx.test(item.label);
|
||||||
|
});
|
||||||
|
}, [list, value, search]);
|
||||||
|
|
||||||
|
useOutsideClick({
|
||||||
|
ref,
|
||||||
|
handler: () => {
|
||||||
|
onClose();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Box ref={ref}>
|
||||||
|
<Menu autoSelect={false} isOpen={isOpen} strategy={'fixed'} matchWidth closeOnSelect={false}>
|
||||||
|
<Box
|
||||||
|
position={'relative'}
|
||||||
|
py={2}
|
||||||
|
borderRadius={'md'}
|
||||||
|
border={'base'}
|
||||||
|
userSelect={'none'}
|
||||||
|
cursor={'pointer'}
|
||||||
|
_active={{
|
||||||
|
transform: 'none'
|
||||||
|
}}
|
||||||
|
_hover={{
|
||||||
|
borderColor: 'primary.300'
|
||||||
|
}}
|
||||||
|
{...(isOpen
|
||||||
|
? {
|
||||||
|
boxShadow: '0px 0px 4px #A8DBFF',
|
||||||
|
borderColor: 'primary.500',
|
||||||
|
onClick: onClose
|
||||||
|
}
|
||||||
|
: {
|
||||||
|
onClick: () => {
|
||||||
|
onOpen();
|
||||||
|
setSearch('');
|
||||||
|
}
|
||||||
|
})}
|
||||||
|
>
|
||||||
|
<MenuButton zIndex={0} position={'absolute'} bottom={0} left={0} right={0} top={0} />
|
||||||
|
<Flex
|
||||||
|
ref={BoxRef}
|
||||||
|
position={'relative'}
|
||||||
|
alignItems={value.length === 0 ? 'center' : 'flex-start'}
|
||||||
|
gap={2}
|
||||||
|
px={2}
|
||||||
|
pb={0}
|
||||||
|
overflowY={'auto'}
|
||||||
|
maxH={'200px'}
|
||||||
|
>
|
||||||
|
{value.length === 0 ? (
|
||||||
|
<Box flex={'1 0 0'} color={'myGray.500'} fontSize={'xs'}>
|
||||||
|
{t('account_model:select_model_placeholder')}
|
||||||
|
</Box>
|
||||||
|
) : (
|
||||||
|
<Flex flex={'1 0 0'} alignItems={'center'} gap={2} flexWrap={'wrap'}>
|
||||||
|
{value.map((item) => (
|
||||||
|
<MyTag
|
||||||
|
key={item}
|
||||||
|
type="borderSolid"
|
||||||
|
colorSchema="gray"
|
||||||
|
bg={'myGray.150'}
|
||||||
|
color={'myGray.900'}
|
||||||
|
_hover={{
|
||||||
|
bg: 'myGray.250'
|
||||||
|
}}
|
||||||
|
onClick={(e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
copyData(item, t('account_model:copy_model_id_success'));
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Box>{item}</Box>
|
||||||
|
<MyIcon
|
||||||
|
ml={0.5}
|
||||||
|
name={'common/closeLight'}
|
||||||
|
w={'14px'}
|
||||||
|
h={'14px'}
|
||||||
|
_hover={{
|
||||||
|
color: 'red.600'
|
||||||
|
}}
|
||||||
|
onClick={(e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
onclickItem(item);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</MyTag>
|
||||||
|
))}
|
||||||
|
{isOpen && (
|
||||||
|
<Input
|
||||||
|
key={'search'}
|
||||||
|
variant={'unstyled'}
|
||||||
|
w={'150px'}
|
||||||
|
h={'24px'}
|
||||||
|
autoFocus
|
||||||
|
value={search}
|
||||||
|
onChange={(e) => setSearch(e.target.value)}
|
||||||
|
placeholder={t('account_model:search_model')}
|
||||||
|
onClick={(e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</Flex>
|
||||||
|
)}
|
||||||
|
<MyIcon name={'core/chat/chevronDown'} color={'myGray.600'} w={4} h={4} />
|
||||||
|
</Flex>
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
<MenuList
|
||||||
|
px={'6px'}
|
||||||
|
py={'6px'}
|
||||||
|
border={'1px solid #fff'}
|
||||||
|
boxShadow={
|
||||||
|
'0px 4px 10px 0px rgba(19, 51, 107, 0.10), 0px 0px 1px 0px rgba(19, 51, 107, 0.10);'
|
||||||
|
}
|
||||||
|
zIndex={99}
|
||||||
|
maxH={'40vh'}
|
||||||
|
overflowY={'auto'}
|
||||||
|
>
|
||||||
|
{filterUnSelected.map((item, i) => {
|
||||||
|
return (
|
||||||
|
<MenuItem
|
||||||
|
key={i}
|
||||||
|
color={'myGray.900'}
|
||||||
|
onClick={(e) => {
|
||||||
|
onclickItem(item.value);
|
||||||
|
}}
|
||||||
|
whiteSpace={'pre-wrap'}
|
||||||
|
fontSize={'sm'}
|
||||||
|
gap={2}
|
||||||
|
{...menuItemStyles}
|
||||||
|
>
|
||||||
|
{item.icon && <MyAvatar src={item.icon} w={'1rem'} borderRadius={'0'} />}
|
||||||
|
<Box flex={'1 0 0'}>{item.label}</Box>
|
||||||
|
</MenuItem>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</MenuList>
|
||||||
|
</Menu>
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
};
|
||||||
@ -0,0 +1,196 @@
|
|||||||
|
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;
|
||||||
230
projects/app/src/pageComponents/account/model/Channel/index.tsx
Normal file
230
projects/app/src/pageComponents/account/model/Channel/index.tsx
Normal file
@ -0,0 +1,230 @@
|
|||||||
|
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;
|
||||||
406
projects/app/src/pageComponents/account/model/Log/index.tsx
Normal file
406
projects/app/src/pageComponents/account/model/Log/index.tsx
Normal file
@ -0,0 +1,406 @@
|
|||||||
|
import { getChannelList, getChannelLog, getLogDetail } from '@/web/core/ai/channel';
|
||||||
|
import { getSystemModelList } from '@/web/core/ai/config';
|
||||||
|
import { useUserStore } from '@/web/support/user/useUserStore';
|
||||||
|
import {
|
||||||
|
Table,
|
||||||
|
Thead,
|
||||||
|
Tbody,
|
||||||
|
Tr,
|
||||||
|
Th,
|
||||||
|
Td,
|
||||||
|
TableContainer,
|
||||||
|
Box,
|
||||||
|
Flex,
|
||||||
|
Button,
|
||||||
|
HStack,
|
||||||
|
ModalBody,
|
||||||
|
Grid,
|
||||||
|
GridItem,
|
||||||
|
BoxProps
|
||||||
|
} from '@chakra-ui/react';
|
||||||
|
import { getModelProvider } from '@fastgpt/global/core/ai/provider';
|
||||||
|
import DateRangePicker, { DateRangeType } from '@fastgpt/web/components/common/DateRangePicker';
|
||||||
|
import MyBox from '@fastgpt/web/components/common/MyBox';
|
||||||
|
import FormLabel from '@fastgpt/web/components/common/MyBox/FormLabel';
|
||||||
|
import MySelect from '@fastgpt/web/components/common/MySelect';
|
||||||
|
import { useRequest2 } from '@fastgpt/web/hooks/useRequest';
|
||||||
|
import { useScrollPagination } from '@fastgpt/web/hooks/useScrollPagination';
|
||||||
|
import { addDays } from 'date-fns';
|
||||||
|
import { useTranslation } from 'next-i18next';
|
||||||
|
import React, { useCallback, useMemo, useState } from 'react';
|
||||||
|
import MyIcon from '@fastgpt/web/components/common/Icon';
|
||||||
|
import { formatTime2YMDHMS } from '@fastgpt/global/common/string/time';
|
||||||
|
import MyModal from '@fastgpt/web/components/common/MyModal';
|
||||||
|
import QuestionTip from '@fastgpt/web/components/common/MyTooltip/QuestionTip';
|
||||||
|
|
||||||
|
type LogDetailType = {
|
||||||
|
id: number;
|
||||||
|
request_id: string;
|
||||||
|
channelName: string | number;
|
||||||
|
model: React.JSX.Element;
|
||||||
|
duration: number;
|
||||||
|
request_at: string;
|
||||||
|
code: number;
|
||||||
|
prompt_tokens: number;
|
||||||
|
completion_tokens: number;
|
||||||
|
endpoint: string;
|
||||||
|
|
||||||
|
content?: string;
|
||||||
|
request_body?: string;
|
||||||
|
response_body?: string;
|
||||||
|
};
|
||||||
|
const ChannelLog = ({ Tab }: { Tab: React.ReactNode }) => {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
const { userInfo } = useUserStore();
|
||||||
|
|
||||||
|
const isRoot = userInfo?.username === 'root';
|
||||||
|
const [filterProps, setFilterProps] = useState<{
|
||||||
|
channelId?: string;
|
||||||
|
model?: string;
|
||||||
|
code_type: 'all' | 'success' | 'error';
|
||||||
|
dateRange: DateRangeType;
|
||||||
|
}>({
|
||||||
|
code_type: 'all',
|
||||||
|
dateRange: {
|
||||||
|
from: (() => {
|
||||||
|
const today = addDays(new Date(), -1);
|
||||||
|
today.setHours(0, 0, 0, 0);
|
||||||
|
return today;
|
||||||
|
})(),
|
||||||
|
to: (() => {
|
||||||
|
const today = new Date();
|
||||||
|
today.setHours(23, 59, 59, 999);
|
||||||
|
return today;
|
||||||
|
})()
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const { data: channelList = [] } = useRequest2(
|
||||||
|
async () => {
|
||||||
|
const res = await getChannelList().then((res) =>
|
||||||
|
res.map((item) => ({
|
||||||
|
label: item.name,
|
||||||
|
value: `${item.id}`
|
||||||
|
}))
|
||||||
|
);
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
label: t('common:common.All'),
|
||||||
|
value: ''
|
||||||
|
},
|
||||||
|
...res
|
||||||
|
];
|
||||||
|
},
|
||||||
|
{
|
||||||
|
manual: false
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
const { data: systemModelList = [] } = useRequest2(getSystemModelList, {
|
||||||
|
manual: false
|
||||||
|
});
|
||||||
|
const modelList = useMemo(() => {
|
||||||
|
const res = systemModelList
|
||||||
|
.map((item) => {
|
||||||
|
const provider = getModelProvider(item.provider);
|
||||||
|
|
||||||
|
return {
|
||||||
|
order: provider.order,
|
||||||
|
icon: provider.avatar,
|
||||||
|
label: item.model,
|
||||||
|
value: item.model
|
||||||
|
};
|
||||||
|
})
|
||||||
|
.sort((a, b) => a.order - b.order);
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
label: t('common:common.All'),
|
||||||
|
value: ''
|
||||||
|
},
|
||||||
|
...res
|
||||||
|
];
|
||||||
|
}, [systemModelList]);
|
||||||
|
|
||||||
|
const { data, isLoading, ScrollData } = useScrollPagination(getChannelLog, {
|
||||||
|
pageSize: 20,
|
||||||
|
refreshDeps: [filterProps],
|
||||||
|
params: {
|
||||||
|
channel: filterProps.channelId,
|
||||||
|
model_name: filterProps.model,
|
||||||
|
code_type: filterProps.code_type,
|
||||||
|
start_timestamp: filterProps.dateRange.from?.getTime() || 0,
|
||||||
|
end_timestamp: filterProps.dateRange.to?.getTime() || 0
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const formatData = useMemo<LogDetailType[]>(() => {
|
||||||
|
return data.map((item) => {
|
||||||
|
const duration = item.created_at - item.request_at;
|
||||||
|
const durationSecond = duration / 1000;
|
||||||
|
|
||||||
|
const channelName = channelList.find((channel) => channel.value === `${item.channel}`)?.label;
|
||||||
|
|
||||||
|
const model = systemModelList.find((model) => model.model === item.model);
|
||||||
|
const provider = getModelProvider(model?.provider);
|
||||||
|
|
||||||
|
return {
|
||||||
|
id: item.id,
|
||||||
|
channelName: channelName || item.channel,
|
||||||
|
model: (
|
||||||
|
<HStack>
|
||||||
|
<MyIcon name={provider?.avatar as any} w={'1rem'} />
|
||||||
|
<Box>{model?.model}</Box>
|
||||||
|
</HStack>
|
||||||
|
),
|
||||||
|
duration: durationSecond,
|
||||||
|
request_at: formatTime2YMDHMS(item.request_at),
|
||||||
|
code: item.code,
|
||||||
|
prompt_tokens: item.prompt_tokens,
|
||||||
|
completion_tokens: item.completion_tokens,
|
||||||
|
request_id: item.request_id,
|
||||||
|
endpoint: item.endpoint,
|
||||||
|
content: item.content
|
||||||
|
};
|
||||||
|
});
|
||||||
|
}, [data]);
|
||||||
|
|
||||||
|
const [logDetail, setLogDetail] = useState<LogDetailType>();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{isRoot && (
|
||||||
|
<Flex alignItems={'center'}>
|
||||||
|
{Tab}
|
||||||
|
<Box flex={1} />
|
||||||
|
</Flex>
|
||||||
|
)}
|
||||||
|
<HStack spacing={4}>
|
||||||
|
<HStack>
|
||||||
|
<FormLabel>{t('common:user.Time')}</FormLabel>
|
||||||
|
<Box>
|
||||||
|
<DateRangePicker
|
||||||
|
defaultDate={filterProps.dateRange}
|
||||||
|
dateRange={filterProps.dateRange}
|
||||||
|
position="bottom"
|
||||||
|
onSuccess={(e) => setFilterProps({ ...filterProps, dateRange: e })}
|
||||||
|
/>
|
||||||
|
</Box>
|
||||||
|
</HStack>
|
||||||
|
<HStack flex={'0 0 200px'}>
|
||||||
|
<FormLabel>{t('account_model:channel_name')}</FormLabel>
|
||||||
|
<Box flex={'1 0 0'}>
|
||||||
|
<MySelect<string>
|
||||||
|
bg={'myGray.50'}
|
||||||
|
isSearch
|
||||||
|
list={channelList}
|
||||||
|
placeholder={t('account_model:select_channel')}
|
||||||
|
value={filterProps.channelId}
|
||||||
|
onchange={(val) => setFilterProps({ ...filterProps, channelId: val })}
|
||||||
|
/>
|
||||||
|
</Box>
|
||||||
|
</HStack>
|
||||||
|
<HStack flex={'0 0 200px'}>
|
||||||
|
<FormLabel>{t('account_model:model_name')}</FormLabel>
|
||||||
|
<Box flex={'1 0 0'}>
|
||||||
|
<MySelect<string>
|
||||||
|
bg={'myGray.50'}
|
||||||
|
isSearch
|
||||||
|
list={modelList}
|
||||||
|
placeholder={t('account_model:select_model')}
|
||||||
|
value={filterProps.model}
|
||||||
|
onchange={(val) => setFilterProps({ ...filterProps, model: val })}
|
||||||
|
/>
|
||||||
|
</Box>
|
||||||
|
</HStack>
|
||||||
|
<HStack flex={'0 0 200px'}>
|
||||||
|
<FormLabel>{t('account_model:log_status')}</FormLabel>
|
||||||
|
<Box flex={'1 0 0'}>
|
||||||
|
<MySelect<'all' | 'success' | 'error'>
|
||||||
|
bg={'myGray.50'}
|
||||||
|
list={[
|
||||||
|
{ label: t('common:common.All'), value: 'all' },
|
||||||
|
{ label: t('common:common.Success'), value: 'success' },
|
||||||
|
{ label: t('common:common.failed'), value: 'error' }
|
||||||
|
]}
|
||||||
|
value={filterProps.code_type}
|
||||||
|
onchange={(val) => setFilterProps({ ...filterProps, code_type: val })}
|
||||||
|
/>
|
||||||
|
</Box>
|
||||||
|
</HStack>
|
||||||
|
</HStack>
|
||||||
|
<MyBox flex={'1 0 0'} h={0} isLoading={isLoading}>
|
||||||
|
<ScrollData h={'100%'}>
|
||||||
|
<TableContainer fontSize={'sm'}>
|
||||||
|
<Table>
|
||||||
|
<Thead>
|
||||||
|
<Tr>
|
||||||
|
<Th>{t('account_model:channel_name')}</Th>
|
||||||
|
<Th>{t('account_model:model')}</Th>
|
||||||
|
<Th>{t('account_model:model_tokens')}</Th>
|
||||||
|
<Th>{t('account_model:duration')}</Th>
|
||||||
|
<Th>{t('account_model:channel_status')}</Th>
|
||||||
|
<Th>{t('account_model:request_at')}</Th>
|
||||||
|
<Th></Th>
|
||||||
|
</Tr>
|
||||||
|
</Thead>
|
||||||
|
<Tbody>
|
||||||
|
{formatData.map((item) => (
|
||||||
|
<Tr key={item.id}>
|
||||||
|
<Td>{item.channelName}</Td>
|
||||||
|
<Td>{item.model}</Td>
|
||||||
|
<Td>
|
||||||
|
{item.prompt_tokens} / {item.completion_tokens}
|
||||||
|
</Td>
|
||||||
|
<Td color={item.duration > 10 ? 'red.600' : ''}>{item.duration.toFixed(2)}s</Td>
|
||||||
|
<Td color={item.code === 200 ? 'green.600' : 'red.600'}>
|
||||||
|
{item.code}
|
||||||
|
{item.content && <QuestionTip label={item.content} />}
|
||||||
|
</Td>
|
||||||
|
<Td>{item.request_at}</Td>
|
||||||
|
<Td>
|
||||||
|
<Button
|
||||||
|
leftIcon={<MyIcon name={'menu'} w={'1rem'} />}
|
||||||
|
size={'sm'}
|
||||||
|
variant={'outline'}
|
||||||
|
onClick={() => setLogDetail(item)}
|
||||||
|
>
|
||||||
|
{t('account_model:detail')}
|
||||||
|
</Button>
|
||||||
|
</Td>
|
||||||
|
</Tr>
|
||||||
|
))}
|
||||||
|
</Tbody>
|
||||||
|
</Table>
|
||||||
|
</TableContainer>
|
||||||
|
</ScrollData>
|
||||||
|
</MyBox>
|
||||||
|
|
||||||
|
{!!logDetail && <LogDetail data={logDetail} onClose={() => setLogDetail(undefined)} />}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default ChannelLog;
|
||||||
|
|
||||||
|
const LogDetail = ({ data, onClose }: { data: LogDetailType; onClose: () => void }) => {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
const { data: detailData } = useRequest2(
|
||||||
|
async () => {
|
||||||
|
if (data.code === 200) return data;
|
||||||
|
const res = await getLogDetail(data.id);
|
||||||
|
return {
|
||||||
|
...res,
|
||||||
|
...data
|
||||||
|
};
|
||||||
|
},
|
||||||
|
{
|
||||||
|
manual: false
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
const Title = useCallback(({ children, ...props }: { children: React.ReactNode } & BoxProps) => {
|
||||||
|
return (
|
||||||
|
<Box
|
||||||
|
bg={'myGray.50'}
|
||||||
|
color="myGray.900 "
|
||||||
|
borderRight={'base'}
|
||||||
|
p={3}
|
||||||
|
flex={'0 0 100px'}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
}, []);
|
||||||
|
const Container = useCallback(
|
||||||
|
({ children, ...props }: { children: React.ReactNode } & BoxProps) => {
|
||||||
|
return (
|
||||||
|
<Box p={3} flex={1} {...props}>
|
||||||
|
{children}
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
[]
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<MyModal
|
||||||
|
isOpen
|
||||||
|
iconSrc="support/bill/payRecordLight"
|
||||||
|
title={t('account_model:log_detail')}
|
||||||
|
onClose={onClose}
|
||||||
|
maxW={['90vw', '800px']}
|
||||||
|
w={'100%'}
|
||||||
|
>
|
||||||
|
{detailData && (
|
||||||
|
<ModalBody>
|
||||||
|
{/* 基本信息表格 */}
|
||||||
|
<Grid
|
||||||
|
templateColumns="repeat(2, 1fr)"
|
||||||
|
gap={0}
|
||||||
|
borderWidth="1px"
|
||||||
|
borderRadius="md"
|
||||||
|
fontSize={'sm'}
|
||||||
|
overflow={'hidden'}
|
||||||
|
>
|
||||||
|
{/* 第一行 */}
|
||||||
|
<GridItem display={'flex'} borderBottomWidth="1px" borderRightWidth="1px">
|
||||||
|
<Title>RequestID</Title>
|
||||||
|
<Container>{detailData?.request_id}</Container>
|
||||||
|
</GridItem>
|
||||||
|
<GridItem display={'flex'} borderBottomWidth="1px" borderRightWidth="1px">
|
||||||
|
<Title>{t('account_model:channel_status')}</Title>
|
||||||
|
<Container color={detailData.code === 200 ? 'green.600' : 'red.600'}>
|
||||||
|
{detailData?.code}
|
||||||
|
</Container>
|
||||||
|
</GridItem>
|
||||||
|
<GridItem display={'flex'} borderBottomWidth="1px" borderRightWidth="1px">
|
||||||
|
<Title>Endpoint</Title>
|
||||||
|
<Container>{detailData?.endpoint}</Container>
|
||||||
|
</GridItem>
|
||||||
|
<GridItem display={'flex'} borderBottomWidth="1px" borderRightWidth="1px">
|
||||||
|
<Title>{t('account_model:channel_name')}</Title>
|
||||||
|
<Container>{detailData?.channelName}</Container>
|
||||||
|
</GridItem>
|
||||||
|
<GridItem display={'flex'} borderBottomWidth="1px" borderRightWidth="1px">
|
||||||
|
<Title>{t('account_model:request_at')}</Title>
|
||||||
|
<Container>{detailData?.request_at}</Container>
|
||||||
|
</GridItem>
|
||||||
|
<GridItem display={'flex'} borderBottomWidth="1px" borderRightWidth="1px">
|
||||||
|
<Title>{t('account_model:duration')}</Title>
|
||||||
|
<Container>{detailData?.duration.toFixed(2)}s</Container>
|
||||||
|
</GridItem>
|
||||||
|
<GridItem display={'flex'} borderBottomWidth="1px" borderRightWidth="1px">
|
||||||
|
<Title>{t('account_model:model')}</Title>
|
||||||
|
<Container>{detailData?.model}</Container>
|
||||||
|
</GridItem>
|
||||||
|
<GridItem display={'flex'} borderBottomWidth="1px" borderRightWidth="1px">
|
||||||
|
<Title flex={'0 0 150px'}>{t('account_model:model_tokens')}</Title>
|
||||||
|
<Container>
|
||||||
|
{detailData?.prompt_tokens} / {detailData?.completion_tokens}
|
||||||
|
</Container>
|
||||||
|
</GridItem>
|
||||||
|
{detailData?.content && (
|
||||||
|
<GridItem display={'flex'} borderBottomWidth="1px" borderRightWidth="1px" colSpan={2}>
|
||||||
|
<Title>Content</Title>
|
||||||
|
<Container>{detailData?.content}</Container>
|
||||||
|
</GridItem>
|
||||||
|
)}
|
||||||
|
{detailData?.request_body && (
|
||||||
|
<GridItem display={'flex'} borderBottomWidth="1px" borderRightWidth="1px" colSpan={2}>
|
||||||
|
<Title>Request Body</Title>
|
||||||
|
<Container userSelect={'all'}>{detailData?.request_body}</Container>
|
||||||
|
</GridItem>
|
||||||
|
)}
|
||||||
|
{detailData?.response_body && (
|
||||||
|
<GridItem display={'flex'} borderBottomWidth="1px" borderRightWidth="1px" colSpan={2}>
|
||||||
|
<Title>Response Body</Title>
|
||||||
|
<Container>{detailData?.response_body}</Container>
|
||||||
|
</GridItem>
|
||||||
|
)}
|
||||||
|
</Grid>
|
||||||
|
</ModalBody>
|
||||||
|
)}
|
||||||
|
</MyModal>
|
||||||
|
);
|
||||||
|
};
|
||||||
@ -33,7 +33,6 @@ import { useRequest2 } from '@fastgpt/web/hooks/useRequest';
|
|||||||
import {
|
import {
|
||||||
deleteSystemModel,
|
deleteSystemModel,
|
||||||
getModelConfigJson,
|
getModelConfigJson,
|
||||||
getSystemModelDefaultConfig,
|
|
||||||
getSystemModelDetail,
|
getSystemModelDetail,
|
||||||
getSystemModelList,
|
getSystemModelList,
|
||||||
getTestModel,
|
getTestModel,
|
||||||
@ -44,24 +43,20 @@ import MyBox from '@fastgpt/web/components/common/MyBox';
|
|||||||
import { SystemModelItemType } from '@fastgpt/service/core/ai/type';
|
import { SystemModelItemType } from '@fastgpt/service/core/ai/type';
|
||||||
import { useConfirm } from '@fastgpt/web/hooks/useConfirm';
|
import { useConfirm } from '@fastgpt/web/hooks/useConfirm';
|
||||||
import MyIconButton from '@fastgpt/web/components/common/Icon/button';
|
import MyIconButton from '@fastgpt/web/components/common/Icon/button';
|
||||||
import { useForm } from 'react-hook-form';
|
|
||||||
import MyNumberInput from '@fastgpt/web/components/common/Input/NumberInput';
|
|
||||||
import MyTextarea from '@/components/common/Textarea/MyTextarea';
|
|
||||||
import JsonEditor from '@fastgpt/web/components/common/Textarea/JsonEditor';
|
import JsonEditor from '@fastgpt/web/components/common/Textarea/JsonEditor';
|
||||||
import { clientInitData } from '@/web/common/system/staticData';
|
import { clientInitData } from '@/web/common/system/staticData';
|
||||||
import { useUserStore } from '@/web/support/user/useUserStore';
|
import { useUserStore } from '@/web/support/user/useUserStore';
|
||||||
import MyMenu from '@fastgpt/web/components/common/MyMenu';
|
|
||||||
import { useSystemStore } from '@/web/common/system/useSystemStore';
|
import { useSystemStore } from '@/web/common/system/useSystemStore';
|
||||||
import QuestionTip from '@fastgpt/web/components/common/MyTooltip/QuestionTip';
|
import QuestionTip from '@fastgpt/web/components/common/MyTooltip/QuestionTip';
|
||||||
import { putUpdateWithJson } from '@/web/core/ai/config';
|
import { putUpdateWithJson } from '@/web/core/ai/config';
|
||||||
import CopyBox from '@fastgpt/web/components/common/String/CopyBox';
|
import CopyBox from '@fastgpt/web/components/common/String/CopyBox';
|
||||||
import MyIcon from '@fastgpt/web/components/common/Icon';
|
import MyIcon from '@fastgpt/web/components/common/Icon';
|
||||||
import AIModelSelector from '@/components/Select/AIModelSelector';
|
import AIModelSelector from '@/components/Select/AIModelSelector';
|
||||||
import { useRefresh } from '../../../../../../packages/web/hooks/useRefresh';
|
|
||||||
import { Prompt_CQJson, Prompt_ExtractJson } from '@fastgpt/global/core/ai/prompt/agent';
|
|
||||||
import MyDivider from '@fastgpt/web/components/common/MyDivider';
|
import MyDivider from '@fastgpt/web/components/common/MyDivider';
|
||||||
|
import { AddModelButton } from './AddModelBox';
|
||||||
|
|
||||||
const MyModal = dynamic(() => import('@fastgpt/web/components/common/MyModal'));
|
const MyModal = dynamic(() => import('@fastgpt/web/components/common/MyModal'));
|
||||||
|
const ModelEditModal = dynamic(() => import('./AddModelBox').then((mod) => mod.ModelEditModal));
|
||||||
|
|
||||||
const ModelTable = ({ Tab }: { Tab: React.ReactNode }) => {
|
const ModelTable = ({ Tab }: { Tab: React.ReactNode }) => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
@ -271,6 +266,7 @@ const ModelTable = ({ Tab }: { Tab: React.ReactNode }) => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
const onCreateModel = (type: ModelTypeEnum) => {
|
const onCreateModel = (type: ModelTypeEnum) => {
|
||||||
const defaultModel = defaultModels[type];
|
const defaultModel = defaultModels[type];
|
||||||
|
|
||||||
@ -316,37 +312,7 @@ const ModelTable = ({ Tab }: { Tab: React.ReactNode }) => {
|
|||||||
<Button variant={'whiteBase'} mr={2} onClick={onOpenJsonConfig}>
|
<Button variant={'whiteBase'} mr={2} onClick={onOpenJsonConfig}>
|
||||||
{t('account:model.json_config')}
|
{t('account:model.json_config')}
|
||||||
</Button>
|
</Button>
|
||||||
<MyMenu
|
<AddModelButton onCreate={onCreateModel} />
|
||||||
trigger="hover"
|
|
||||||
size="sm"
|
|
||||||
Button={<Button>{t('account:create_model')}</Button>}
|
|
||||||
menuList={[
|
|
||||||
{
|
|
||||||
children: [
|
|
||||||
{
|
|
||||||
label: t('common:model.type.chat'),
|
|
||||||
onClick: () => onCreateModel(ModelTypeEnum.llm)
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: t('common:model.type.embedding'),
|
|
||||||
onClick: () => onCreateModel(ModelTypeEnum.embedding)
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: t('common:model.type.tts'),
|
|
||||||
onClick: () => onCreateModel(ModelTypeEnum.tts)
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: t('common:model.type.stt'),
|
|
||||||
onClick: () => onCreateModel(ModelTypeEnum.stt)
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: t('common:model.type.reRank'),
|
|
||||||
onClick: () => onCreateModel(ModelTypeEnum.rerank)
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
]}
|
|
||||||
/>
|
|
||||||
</Flex>
|
</Flex>
|
||||||
)}
|
)}
|
||||||
<MyBox flex={'1 0 0'} isLoading={isLoading}>
|
<MyBox flex={'1 0 0'} isLoading={isLoading}>
|
||||||
@ -512,650 +478,6 @@ const ModelTable = ({ Tab }: { Tab: React.ReactNode }) => {
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
const InputStyles = {
|
|
||||||
maxW: '300px',
|
|
||||||
bg: 'myGray.50',
|
|
||||||
w: '100%',
|
|
||||||
rows: 3
|
|
||||||
};
|
|
||||||
const ModelEditModal = ({
|
|
||||||
modelData,
|
|
||||||
onSuccess,
|
|
||||||
onClose
|
|
||||||
}: {
|
|
||||||
modelData: SystemModelItemType;
|
|
||||||
onSuccess: () => void;
|
|
||||||
onClose: () => void;
|
|
||||||
}) => {
|
|
||||||
const { t } = useTranslation();
|
|
||||||
const { feConfigs } = useSystemStore();
|
|
||||||
|
|
||||||
const { register, getValues, setValue, handleSubmit, watch, reset } =
|
|
||||||
useForm<SystemModelItemType>({
|
|
||||||
defaultValues: modelData
|
|
||||||
});
|
|
||||||
|
|
||||||
const isCustom = !!modelData.isCustom;
|
|
||||||
const isLLMModel = modelData?.type === ModelTypeEnum.llm;
|
|
||||||
const isEmbeddingModel = modelData?.type === ModelTypeEnum.embedding;
|
|
||||||
const isTTSModel = modelData?.type === ModelTypeEnum.tts;
|
|
||||||
const isSTTModel = modelData?.type === ModelTypeEnum.stt;
|
|
||||||
const isRerankModel = modelData?.type === ModelTypeEnum.rerank;
|
|
||||||
|
|
||||||
const provider = watch('provider');
|
|
||||||
const providerData = useMemo(() => getModelProvider(provider), [provider]);
|
|
||||||
|
|
||||||
const providerList = useRef<{ label: any; value: ModelProviderIdType }[]>(
|
|
||||||
ModelProviderList.map((item) => ({
|
|
||||||
label: (
|
|
||||||
<HStack>
|
|
||||||
<Avatar src={item.avatar} w={'1rem'} />
|
|
||||||
<Box>{t(item.name as any)}</Box>
|
|
||||||
</HStack>
|
|
||||||
),
|
|
||||||
value: item.id
|
|
||||||
}))
|
|
||||||
);
|
|
||||||
|
|
||||||
const priceUnit = useMemo(() => {
|
|
||||||
if (isLLMModel || isEmbeddingModel) return '/ 1k Tokens';
|
|
||||||
if (isTTSModel) return `/ 1k ${t('common:unit.character')}`;
|
|
||||||
if (isSTTModel) return `/ 60 ${t('common:unit.seconds')}`;
|
|
||||||
return '';
|
|
||||||
return '';
|
|
||||||
}, [isLLMModel, isEmbeddingModel, isTTSModel, t, isSTTModel]);
|
|
||||||
|
|
||||||
const { runAsync: updateModel, loading: updatingModel } = useRequest2(
|
|
||||||
async (data: SystemModelItemType) => {
|
|
||||||
return putSystemModel({
|
|
||||||
model: data.model,
|
|
||||||
metadata: data
|
|
||||||
}).then(onSuccess);
|
|
||||||
},
|
|
||||||
{
|
|
||||||
onSuccess: () => {
|
|
||||||
onClose();
|
|
||||||
},
|
|
||||||
successToast: t('common:common.Success')
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
const [key, setKey] = useState(0);
|
|
||||||
const { runAsync: loadDefaultConfig, loading: loadingDefaultConfig } = useRequest2(
|
|
||||||
getSystemModelDefaultConfig,
|
|
||||||
{
|
|
||||||
onSuccess(res) {
|
|
||||||
reset({
|
|
||||||
...getValues(),
|
|
||||||
...res
|
|
||||||
});
|
|
||||||
setTimeout(() => {
|
|
||||||
setKey((prev) => prev + 1);
|
|
||||||
}, 0);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<MyModal
|
|
||||||
iconSrc={'modal/edit'}
|
|
||||||
title={t('account:model.edit_model')}
|
|
||||||
isOpen
|
|
||||||
onClose={onClose}
|
|
||||||
maxW={['90vw', '80vw']}
|
|
||||||
w={'100%'}
|
|
||||||
h={'100%'}
|
|
||||||
>
|
|
||||||
<ModalBody>
|
|
||||||
<Flex gap={4} key={key}>
|
|
||||||
<TableContainer flex={'1'}>
|
|
||||||
<Table>
|
|
||||||
<Thead>
|
|
||||||
<Tr color={'myGray.600'}>
|
|
||||||
<Th fontSize={'xs'}>{t('account:model.param_name')}</Th>
|
|
||||||
<Th fontSize={'xs'}></Th>
|
|
||||||
</Tr>
|
|
||||||
</Thead>
|
|
||||||
<Tbody>
|
|
||||||
<Tr>
|
|
||||||
<Td>
|
|
||||||
<HStack spacing={1}>
|
|
||||||
<Box>{t('account:model.model_id')}</Box>
|
|
||||||
<QuestionTip label={t('account:model.model_id_tip')} />
|
|
||||||
</HStack>
|
|
||||||
</Td>
|
|
||||||
<Td textAlign={'right'}>
|
|
||||||
{isCustom ? (
|
|
||||||
<Input {...register('model', { required: true })} {...InputStyles} />
|
|
||||||
) : (
|
|
||||||
modelData?.model
|
|
||||||
)}
|
|
||||||
</Td>
|
|
||||||
</Tr>
|
|
||||||
<Tr>
|
|
||||||
<Td>{t('common:model.provider')}</Td>
|
|
||||||
<Td textAlign={'right'}>
|
|
||||||
<MySelect
|
|
||||||
value={provider}
|
|
||||||
onchange={(value) => setValue('provider', value)}
|
|
||||||
list={providerList.current}
|
|
||||||
{...InputStyles}
|
|
||||||
/>
|
|
||||||
</Td>
|
|
||||||
</Tr>
|
|
||||||
<Tr>
|
|
||||||
<Td>
|
|
||||||
<HStack spacing={1}>
|
|
||||||
<Box>{t('account:model.alias')}</Box>
|
|
||||||
<QuestionTip label={t('account:model.alias_tip')} />
|
|
||||||
</HStack>
|
|
||||||
</Td>
|
|
||||||
<Td textAlign={'right'}>
|
|
||||||
<Input {...register('name', { required: true })} {...InputStyles} />
|
|
||||||
</Td>
|
|
||||||
</Tr>
|
|
||||||
{priceUnit && feConfigs?.isPlus && (
|
|
||||||
<>
|
|
||||||
<Tr>
|
|
||||||
<Td>
|
|
||||||
<HStack spacing={1}>
|
|
||||||
<Box>{t('account:model.charsPointsPrice')}</Box>
|
|
||||||
<QuestionTip label={t('account:model.charsPointsPrice_tip')} />
|
|
||||||
</HStack>
|
|
||||||
</Td>
|
|
||||||
<Td>
|
|
||||||
<Flex justify="flex-end">
|
|
||||||
<HStack w={'100%'} maxW={'300px'}>
|
|
||||||
<MyNumberInput
|
|
||||||
flex={'1 0 0'}
|
|
||||||
register={register}
|
|
||||||
name={'charsPointsPrice'}
|
|
||||||
step={0.01}
|
|
||||||
/>
|
|
||||||
<Box fontSize={'sm'}>{priceUnit}</Box>
|
|
||||||
</HStack>
|
|
||||||
</Flex>
|
|
||||||
</Td>
|
|
||||||
</Tr>
|
|
||||||
{isLLMModel && (
|
|
||||||
<>
|
|
||||||
<Tr>
|
|
||||||
<Td>
|
|
||||||
<HStack spacing={1}>
|
|
||||||
<Box>{t('account:model.input_price')}</Box>
|
|
||||||
<QuestionTip label={t('account:model.input_price_tip')} />
|
|
||||||
</HStack>
|
|
||||||
</Td>
|
|
||||||
<Td>
|
|
||||||
<Flex justify="flex-end">
|
|
||||||
<HStack w={'100%'} maxW={'300px'}>
|
|
||||||
<MyNumberInput
|
|
||||||
flex={'1 0 0'}
|
|
||||||
register={register}
|
|
||||||
name={'inputPrice'}
|
|
||||||
step={0.01}
|
|
||||||
/>
|
|
||||||
<Box fontSize={'sm'}>{priceUnit}</Box>
|
|
||||||
</HStack>
|
|
||||||
</Flex>
|
|
||||||
</Td>
|
|
||||||
</Tr>
|
|
||||||
<Tr>
|
|
||||||
<Td>
|
|
||||||
<HStack spacing={1}>
|
|
||||||
<Box>{t('account:model.output_price')}</Box>
|
|
||||||
<QuestionTip label={t('account:model.output_price_tip')} />
|
|
||||||
</HStack>
|
|
||||||
</Td>
|
|
||||||
<Td>
|
|
||||||
<Flex justify="flex-end">
|
|
||||||
<HStack w={'100%'} maxW={'300px'}>
|
|
||||||
<MyNumberInput
|
|
||||||
flex={'1 0 0'}
|
|
||||||
register={register}
|
|
||||||
name={'outputPrice'}
|
|
||||||
step={0.01}
|
|
||||||
/>
|
|
||||||
<Box fontSize={'sm'}>{priceUnit}</Box>
|
|
||||||
</HStack>
|
|
||||||
</Flex>
|
|
||||||
</Td>
|
|
||||||
</Tr>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
{isLLMModel && (
|
|
||||||
<>
|
|
||||||
<Tr>
|
|
||||||
<Td>{t('common:core.ai.Max context')}</Td>
|
|
||||||
<Td textAlign={'right'}>
|
|
||||||
<Flex justifyContent={'flex-end'}>
|
|
||||||
<MyNumberInput
|
|
||||||
register={register}
|
|
||||||
isRequired
|
|
||||||
name="maxContext"
|
|
||||||
{...InputStyles}
|
|
||||||
/>
|
|
||||||
</Flex>
|
|
||||||
</Td>
|
|
||||||
</Tr>
|
|
||||||
<Tr>
|
|
||||||
<Td>{t('account:model.max_quote')}</Td>
|
|
||||||
<Td textAlign={'right'}>
|
|
||||||
<Flex justifyContent={'flex-end'}>
|
|
||||||
<MyNumberInput
|
|
||||||
register={register}
|
|
||||||
isRequired
|
|
||||||
name="quoteMaxToken"
|
|
||||||
{...InputStyles}
|
|
||||||
/>
|
|
||||||
</Flex>
|
|
||||||
</Td>
|
|
||||||
</Tr>
|
|
||||||
<Tr>
|
|
||||||
<Td>{t('common:core.chat.response.module maxToken')}</Td>
|
|
||||||
<Td textAlign={'right'}>
|
|
||||||
<Flex justifyContent={'flex-end'}>
|
|
||||||
<MyNumberInput
|
|
||||||
register={register}
|
|
||||||
isRequired
|
|
||||||
name="maxResponse"
|
|
||||||
{...InputStyles}
|
|
||||||
/>
|
|
||||||
</Flex>
|
|
||||||
</Td>
|
|
||||||
</Tr>
|
|
||||||
<Tr>
|
|
||||||
<Td>{t('account:model.max_temperature')}</Td>
|
|
||||||
<Td textAlign={'right'}>
|
|
||||||
<Flex justifyContent={'flex-end'}>
|
|
||||||
<MyNumberInput
|
|
||||||
register={register}
|
|
||||||
isRequired
|
|
||||||
name="maxTemperature"
|
|
||||||
step={0.1}
|
|
||||||
{...InputStyles}
|
|
||||||
/>
|
|
||||||
</Flex>
|
|
||||||
</Td>
|
|
||||||
</Tr>
|
|
||||||
<Tr>
|
|
||||||
<Td>
|
|
||||||
<HStack spacing={1}>
|
|
||||||
<Box>{t('account:model.show_top_p')}</Box>
|
|
||||||
</HStack>
|
|
||||||
</Td>
|
|
||||||
<Td textAlign={'right'}>
|
|
||||||
<Flex justifyContent={'flex-end'}>
|
|
||||||
<Switch {...register('showTopP')} />
|
|
||||||
</Flex>
|
|
||||||
</Td>
|
|
||||||
</Tr>
|
|
||||||
<Tr>
|
|
||||||
<Td>
|
|
||||||
<HStack spacing={1}>
|
|
||||||
<Box>{t('account:model.show_stop_sign')}</Box>
|
|
||||||
</HStack>
|
|
||||||
</Td>
|
|
||||||
<Td textAlign={'right'}>
|
|
||||||
<Flex justifyContent={'flex-end'}>
|
|
||||||
<Switch {...register('showStopSign')} />
|
|
||||||
</Flex>
|
|
||||||
</Td>
|
|
||||||
</Tr>
|
|
||||||
<Tr>
|
|
||||||
<Td>{t('account:model.response_format')}</Td>
|
|
||||||
<Td textAlign={'right'}>
|
|
||||||
<JsonEditor
|
|
||||||
value={JSON.stringify(getValues('responseFormatList'), null, 2)}
|
|
||||||
resize
|
|
||||||
onChange={(e) => {
|
|
||||||
if (!e) {
|
|
||||||
setValue('responseFormatList', []);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
try {
|
|
||||||
setValue('responseFormatList', JSON.parse(e));
|
|
||||||
} catch (error) {
|
|
||||||
console.error(error);
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
{...InputStyles}
|
|
||||||
/>
|
|
||||||
</Td>
|
|
||||||
</Tr>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
{isEmbeddingModel && (
|
|
||||||
<>
|
|
||||||
<Tr>
|
|
||||||
<Td>
|
|
||||||
<HStack spacing={1}>
|
|
||||||
<Box>{t('account:model.normalization')}</Box>
|
|
||||||
<QuestionTip label={t('account:model.normalization_tip')} />
|
|
||||||
</HStack>
|
|
||||||
</Td>
|
|
||||||
<Td textAlign={'right'}>
|
|
||||||
<Flex justifyContent={'flex-end'}>
|
|
||||||
<Switch {...register('normalization')} />
|
|
||||||
</Flex>
|
|
||||||
</Td>
|
|
||||||
</Tr>
|
|
||||||
<Tr>
|
|
||||||
<Td>
|
|
||||||
<HStack spacing={1}>
|
|
||||||
<Box>{t('account:model.default_token')}</Box>
|
|
||||||
<QuestionTip label={t('account:model.default_token_tip')} />
|
|
||||||
</HStack>
|
|
||||||
</Td>
|
|
||||||
<Td textAlign={'right'}>
|
|
||||||
<Flex justifyContent={'flex-end'}>
|
|
||||||
<MyNumberInput
|
|
||||||
register={register}
|
|
||||||
isRequired
|
|
||||||
name="defaultToken"
|
|
||||||
{...InputStyles}
|
|
||||||
/>
|
|
||||||
</Flex>
|
|
||||||
</Td>
|
|
||||||
</Tr>
|
|
||||||
<Tr>
|
|
||||||
<Td>{t('common:core.ai.Max context')}</Td>
|
|
||||||
<Td textAlign={'right'}>
|
|
||||||
<Flex justifyContent={'flex-end'}>
|
|
||||||
<MyNumberInput
|
|
||||||
register={register}
|
|
||||||
isRequired
|
|
||||||
name="maxToken"
|
|
||||||
{...InputStyles}
|
|
||||||
/>
|
|
||||||
</Flex>
|
|
||||||
</Td>
|
|
||||||
</Tr>
|
|
||||||
<Tr>
|
|
||||||
<Td>
|
|
||||||
<HStack spacing={1}>
|
|
||||||
<Box>{t('account:model.defaultConfig')}</Box>
|
|
||||||
<QuestionTip label={t('account:model.defaultConfig_tip')} />
|
|
||||||
</HStack>
|
|
||||||
</Td>
|
|
||||||
<Td textAlign={'right'}>
|
|
||||||
<Flex justifyContent={'flex-end'}>
|
|
||||||
<JsonEditor
|
|
||||||
value={JSON.stringify(getValues('defaultConfig'), null, 2)}
|
|
||||||
onChange={(e) => {
|
|
||||||
if (!e) {
|
|
||||||
setValue('defaultConfig', {});
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
try {
|
|
||||||
setValue('defaultConfig', JSON.parse(e));
|
|
||||||
} catch (error) {
|
|
||||||
console.error(error);
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
{...InputStyles}
|
|
||||||
/>
|
|
||||||
</Flex>
|
|
||||||
</Td>
|
|
||||||
</Tr>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
{isTTSModel && (
|
|
||||||
<>
|
|
||||||
<Tr>
|
|
||||||
<Td>
|
|
||||||
<HStack spacing={1}>
|
|
||||||
<Box>{t('account:model.voices')}</Box>
|
|
||||||
<QuestionTip label={t('account:model.voices_tip')} />
|
|
||||||
</HStack>
|
|
||||||
</Td>
|
|
||||||
<Td textAlign={'right'}>
|
|
||||||
<Flex justifyContent={'flex-end'}>
|
|
||||||
<JsonEditor
|
|
||||||
value={JSON.stringify(getValues('voices'), null, 2)}
|
|
||||||
onChange={(e) => {
|
|
||||||
try {
|
|
||||||
setValue('voices', JSON.parse(e));
|
|
||||||
} catch (error) {
|
|
||||||
console.error(error);
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
{...InputStyles}
|
|
||||||
/>
|
|
||||||
</Flex>
|
|
||||||
</Td>
|
|
||||||
</Tr>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
<Tr>
|
|
||||||
<Td>
|
|
||||||
<HStack spacing={1}>
|
|
||||||
<Box>{t('account:model.request_url')}</Box>
|
|
||||||
<QuestionTip label={t('account:model.request_url_tip')} />
|
|
||||||
</HStack>
|
|
||||||
</Td>
|
|
||||||
<Td textAlign={'right'}>
|
|
||||||
<Input {...register('requestUrl')} {...InputStyles} />
|
|
||||||
</Td>
|
|
||||||
</Tr>
|
|
||||||
<Tr>
|
|
||||||
<Td>
|
|
||||||
<HStack spacing={1}>
|
|
||||||
<Box>{t('account:model.request_auth')}</Box>
|
|
||||||
<QuestionTip label={t('account:model.request_auth_tip')} />
|
|
||||||
</HStack>
|
|
||||||
</Td>
|
|
||||||
<Td textAlign={'right'}>
|
|
||||||
<Input {...register('requestAuth')} {...InputStyles} />
|
|
||||||
</Td>
|
|
||||||
</Tr>
|
|
||||||
</Tbody>
|
|
||||||
</Table>
|
|
||||||
</TableContainer>
|
|
||||||
{isLLMModel && (
|
|
||||||
<TableContainer flex={'1'}>
|
|
||||||
<Table>
|
|
||||||
<Thead>
|
|
||||||
<Tr color={'myGray.600'}>
|
|
||||||
<Th fontSize={'xs'}>{t('account:model.param_name')}</Th>
|
|
||||||
<Th fontSize={'xs'}></Th>
|
|
||||||
</Tr>
|
|
||||||
</Thead>
|
|
||||||
<Tbody>
|
|
||||||
<Tr>
|
|
||||||
<Td>
|
|
||||||
<HStack spacing={1}>
|
|
||||||
<Box>{t('account:model.tool_choice')}</Box>
|
|
||||||
<QuestionTip label={t('account:model.tool_choice_tip')} />
|
|
||||||
</HStack>
|
|
||||||
</Td>
|
|
||||||
<Td textAlign={'right'}>
|
|
||||||
<Flex justifyContent={'flex-end'}>
|
|
||||||
<Switch {...register('toolChoice')} />
|
|
||||||
</Flex>
|
|
||||||
</Td>
|
|
||||||
</Tr>
|
|
||||||
<Tr>
|
|
||||||
<Td>
|
|
||||||
<HStack spacing={1}>
|
|
||||||
<Box>{t('account:model.function_call')}</Box>
|
|
||||||
<QuestionTip label={t('account:model.function_call_tip')} />
|
|
||||||
</HStack>
|
|
||||||
</Td>
|
|
||||||
<Td textAlign={'right'}>
|
|
||||||
<Flex justifyContent={'flex-end'}>
|
|
||||||
<Switch {...register('functionCall')} />
|
|
||||||
</Flex>
|
|
||||||
</Td>
|
|
||||||
</Tr>
|
|
||||||
<Tr>
|
|
||||||
<Td>
|
|
||||||
<HStack spacing={1}>
|
|
||||||
<Box>{t('account:model.vision')}</Box>
|
|
||||||
<QuestionTip label={t('account:model.vision_tip')} />
|
|
||||||
</HStack>
|
|
||||||
</Td>
|
|
||||||
<Td textAlign={'right'}>
|
|
||||||
<Flex justifyContent={'flex-end'}>
|
|
||||||
<Switch {...register('vision')} />
|
|
||||||
</Flex>
|
|
||||||
</Td>
|
|
||||||
</Tr>
|
|
||||||
<Tr>
|
|
||||||
<Td>
|
|
||||||
<HStack spacing={1}>
|
|
||||||
<Box>{t('account:model.reasoning')}</Box>
|
|
||||||
<QuestionTip label={t('account:model.reasoning_tip')} />
|
|
||||||
</HStack>
|
|
||||||
</Td>
|
|
||||||
<Td textAlign={'right'}>
|
|
||||||
<Flex justifyContent={'flex-end'}>
|
|
||||||
<Switch {...register('reasoning')} />
|
|
||||||
</Flex>
|
|
||||||
</Td>
|
|
||||||
</Tr>
|
|
||||||
{feConfigs?.isPlus && (
|
|
||||||
<Tr>
|
|
||||||
<Td>
|
|
||||||
<HStack spacing={1}>
|
|
||||||
<Box>{t('account:model.censor')}</Box>
|
|
||||||
<QuestionTip label={t('account:model.censor_tip')} />
|
|
||||||
</HStack>
|
|
||||||
</Td>
|
|
||||||
<Td textAlign={'right'}>
|
|
||||||
<Flex justifyContent={'flex-end'}>
|
|
||||||
<Switch {...register('censor')} />
|
|
||||||
</Flex>
|
|
||||||
</Td>
|
|
||||||
</Tr>
|
|
||||||
)}
|
|
||||||
<Tr>
|
|
||||||
<Td>{t('account:model.dataset_process')}</Td>
|
|
||||||
<Td textAlign={'right'}>
|
|
||||||
<Flex justifyContent={'flex-end'}>
|
|
||||||
<Switch {...register('datasetProcess')} />
|
|
||||||
</Flex>
|
|
||||||
</Td>
|
|
||||||
</Tr>
|
|
||||||
<Tr>
|
|
||||||
<Td>{t('account:model.used_in_classify')}</Td>
|
|
||||||
<Td textAlign={'right'}>
|
|
||||||
<Flex justifyContent={'flex-end'}>
|
|
||||||
<Switch {...register('usedInClassify')} />
|
|
||||||
</Flex>
|
|
||||||
</Td>
|
|
||||||
</Tr>
|
|
||||||
<Tr>
|
|
||||||
<Td>{t('account:model.used_in_extract_fields')}</Td>
|
|
||||||
<Td textAlign={'right'}>
|
|
||||||
<Flex justifyContent={'flex-end'}>
|
|
||||||
<Switch {...register('usedInExtractFields')} />
|
|
||||||
</Flex>
|
|
||||||
</Td>
|
|
||||||
</Tr>
|
|
||||||
<Tr>
|
|
||||||
<Td>{t('account:model.used_in_tool_call')}</Td>
|
|
||||||
<Td textAlign={'right'}>
|
|
||||||
<Flex justifyContent={'flex-end'}>
|
|
||||||
<Switch {...register('usedInToolCall')} />
|
|
||||||
</Flex>
|
|
||||||
</Td>
|
|
||||||
</Tr>
|
|
||||||
<Tr>
|
|
||||||
<Td>
|
|
||||||
<HStack spacing={1}>
|
|
||||||
<Box>{t('account:model.default_system_chat_prompt')}</Box>
|
|
||||||
<QuestionTip label={t('account:model.default_system_chat_prompt_tip')} />
|
|
||||||
</HStack>
|
|
||||||
</Td>
|
|
||||||
<Td textAlign={'right'}>
|
|
||||||
<MyTextarea {...register('defaultSystemChatPrompt')} {...InputStyles} />
|
|
||||||
</Td>
|
|
||||||
</Tr>
|
|
||||||
<Tr>
|
|
||||||
<Td>
|
|
||||||
<HStack spacing={1}>
|
|
||||||
<Box>{t('account:model.custom_cq_prompt')}</Box>
|
|
||||||
<QuestionTip
|
|
||||||
label={t('account:model.custom_cq_prompt_tip', { prompt: Prompt_CQJson })}
|
|
||||||
/>
|
|
||||||
</HStack>
|
|
||||||
</Td>
|
|
||||||
<Td textAlign={'right'}>
|
|
||||||
<MyTextarea {...register('customCQPrompt')} {...InputStyles} />
|
|
||||||
</Td>
|
|
||||||
</Tr>
|
|
||||||
<Tr>
|
|
||||||
<Td>
|
|
||||||
<HStack spacing={1}>
|
|
||||||
<Box>{t('account:model.custom_extract_prompt')}</Box>
|
|
||||||
<QuestionTip
|
|
||||||
label={t('account:model.custom_extract_prompt_tip', {
|
|
||||||
prompt: Prompt_ExtractJson
|
|
||||||
})}
|
|
||||||
/>
|
|
||||||
</HStack>
|
|
||||||
</Td>
|
|
||||||
<Td textAlign={'right'}>
|
|
||||||
<MyTextarea {...register('customExtractPrompt')} {...InputStyles} />
|
|
||||||
</Td>
|
|
||||||
</Tr>
|
|
||||||
<Tr>
|
|
||||||
<Td>
|
|
||||||
<HStack spacing={1}>
|
|
||||||
<Box>{t('account:model.default_config')}</Box>
|
|
||||||
<QuestionTip label={t('account:model.default_config_tip')} />
|
|
||||||
</HStack>
|
|
||||||
</Td>
|
|
||||||
<Td textAlign={'right'}>
|
|
||||||
<JsonEditor
|
|
||||||
value={JSON.stringify(getValues('defaultConfig'), null, 2)}
|
|
||||||
resize
|
|
||||||
onChange={(e) => {
|
|
||||||
if (!e) {
|
|
||||||
setValue('defaultConfig', {});
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
try {
|
|
||||||
setValue('defaultConfig', JSON.parse(e));
|
|
||||||
} catch (error) {
|
|
||||||
console.error(error);
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
{...InputStyles}
|
|
||||||
/>
|
|
||||||
</Td>
|
|
||||||
</Tr>
|
|
||||||
</Tbody>
|
|
||||||
</Table>
|
|
||||||
</TableContainer>
|
|
||||||
)}
|
|
||||||
</Flex>
|
|
||||||
</ModalBody>
|
|
||||||
<ModalFooter>
|
|
||||||
{!modelData.isCustom && (
|
|
||||||
<Button
|
|
||||||
isLoading={loadingDefaultConfig}
|
|
||||||
variant={'whiteBase'}
|
|
||||||
mr={4}
|
|
||||||
onClick={() => loadDefaultConfig(modelData.model)}
|
|
||||||
>
|
|
||||||
{t('account:reset_default')}
|
|
||||||
</Button>
|
|
||||||
)}
|
|
||||||
<Button variant={'whiteBase'} mr={4} onClick={onClose}>
|
|
||||||
{t('common:common.Cancel')}
|
|
||||||
</Button>
|
|
||||||
<Button isLoading={updatingModel} onClick={handleSubmit(updateModel)}>
|
|
||||||
{t('common:common.Confirm')}
|
|
||||||
</Button>
|
|
||||||
</ModalFooter>
|
|
||||||
</MyModal>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
const JsonConfigModal = ({
|
const JsonConfigModal = ({
|
||||||
onClose,
|
onClose,
|
||||||
onSuccess
|
onSuccess
|
||||||
|
|||||||
@ -7,13 +7,17 @@ import { useUserStore } from '@/web/support/user/useUserStore';
|
|||||||
import FillRowTabs from '@fastgpt/web/components/common/Tabs/FillRowTabs';
|
import FillRowTabs from '@fastgpt/web/components/common/Tabs/FillRowTabs';
|
||||||
import { useTranslation } from 'next-i18next';
|
import { useTranslation } from 'next-i18next';
|
||||||
import dynamic from 'next/dynamic';
|
import dynamic from 'next/dynamic';
|
||||||
|
import { useSystemStore } from '@/web/common/system/useSystemStore';
|
||||||
|
|
||||||
const ModelConfigTable = dynamic(() => import('@/pageComponents/account/model/ModelConfigTable'));
|
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 ModelProvider = () => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
const { feConfigs } = useSystemStore();
|
||||||
|
|
||||||
const [tab, setTab] = useState<TabType>('model');
|
const [tab, setTab] = useState<TabType>('model');
|
||||||
|
|
||||||
@ -22,21 +26,29 @@ const ModelProvider = () => {
|
|||||||
<FillRowTabs<TabType>
|
<FillRowTabs<TabType>
|
||||||
list={[
|
list={[
|
||||||
{ label: t('account:active_model'), value: 'model' },
|
{ label: t('account:active_model'), value: 'model' },
|
||||||
{ label: t('account:config_model'), value: 'config' }
|
{ label: t('account:config_model'), value: 'config' },
|
||||||
// { label: t('account:channel'), value: 'channel' }
|
// @ts-ignore
|
||||||
|
...(feConfigs?.show_aiproxy
|
||||||
|
? [
|
||||||
|
{ label: t('account:channel'), value: 'channel' },
|
||||||
|
{ label: t('account_model:log'), value: 'channel_log' }
|
||||||
|
]
|
||||||
|
: [])
|
||||||
]}
|
]}
|
||||||
value={tab}
|
value={tab}
|
||||||
py={1}
|
py={1}
|
||||||
onChange={setTab}
|
onChange={setTab}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}, [t, tab]);
|
}, [feConfigs.show_aiproxy, t, tab]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<AccountContainer>
|
<AccountContainer>
|
||||||
<Flex h={'100%'} flexDirection={'column'} gap={4} py={4} px={6}>
|
<Flex h={'100%'} flexDirection={'column'} gap={4} py={4} px={6}>
|
||||||
{tab === 'model' && <ValidModelTable Tab={Tab} />}
|
{tab === 'model' && <ValidModelTable Tab={Tab} />}
|
||||||
{tab === 'config' && <ModelConfigTable Tab={Tab} />}
|
{tab === 'config' && <ModelConfigTable Tab={Tab} />}
|
||||||
|
{tab === 'channel' && <ChannelTable Tab={Tab} />}
|
||||||
|
{tab === 'channel_log' && <ChannelLog Tab={Tab} />}
|
||||||
</Flex>
|
</Flex>
|
||||||
</AccountContainer>
|
</AccountContainer>
|
||||||
);
|
);
|
||||||
@ -45,7 +57,7 @@ const ModelProvider = () => {
|
|||||||
export async function getServerSideProps(content: any) {
|
export async function getServerSideProps(content: any) {
|
||||||
return {
|
return {
|
||||||
props: {
|
props: {
|
||||||
...(await serviceSideProps(content, ['account']))
|
...(await serviceSideProps(content, ['account', 'account_model']))
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
72
projects/app/src/pages/api/aiproxy/[...path].ts
Normal file
72
projects/app/src/pages/api/aiproxy/[...path].ts
Normal file
@ -0,0 +1,72 @@
|
|||||||
|
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
|
||||||
|
}
|
||||||
|
};
|
||||||
33
projects/app/src/pages/api/aiproxy/api/createChannel.ts
Normal file
33
projects/app/src/pages/api/aiproxy/api/createChannel.ts
Normal file
@ -0,0 +1,33 @@
|
|||||||
|
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({
|
const ai = getAIApi({
|
||||||
timeout: 10000
|
timeout: 10000
|
||||||
});
|
});
|
||||||
|
|
||||||
const requestBody = llmCompletionsBodyFormat(
|
const requestBody = llmCompletionsBodyFormat(
|
||||||
{
|
{
|
||||||
model: model.model,
|
model: model.model,
|
||||||
|
|||||||
@ -218,7 +218,7 @@ const MyApps = () => {
|
|||||||
size="md"
|
size="md"
|
||||||
Button={
|
Button={
|
||||||
<Button variant={'primary'} leftIcon={<AddIcon />}>
|
<Button variant={'primary'} leftIcon={<AddIcon />}>
|
||||||
<Box>{t('common:common.Create New')}</Box>
|
<Box>{t('common:new_create')}</Box>
|
||||||
</Button>
|
</Button>
|
||||||
}
|
}
|
||||||
menuList={[
|
menuList={[
|
||||||
|
|||||||
@ -147,7 +147,7 @@ const Dataset = () => {
|
|||||||
<Button variant={'primary'} px="0">
|
<Button variant={'primary'} px="0">
|
||||||
<Flex alignItems={'center'} px={5}>
|
<Flex alignItems={'center'} px={5}>
|
||||||
<AddIcon mr={2} />
|
<AddIcon mr={2} />
|
||||||
<Box>{t('common:common.Create New')}</Box>
|
<Box>{t('common:new_create')}</Box>
|
||||||
</Flex>
|
</Flex>
|
||||||
</Button>
|
</Button>
|
||||||
}
|
}
|
||||||
|
|||||||
@ -83,7 +83,8 @@ export async function initSystemConfig() {
|
|||||||
...fileRes?.feConfigs,
|
...fileRes?.feConfigs,
|
||||||
...defaultFeConfigs,
|
...defaultFeConfigs,
|
||||||
...(dbConfig.feConfigs || {}),
|
...(dbConfig.feConfigs || {}),
|
||||||
isPlus: !!FastGPTProUrl
|
isPlus: !!FastGPTProUrl,
|
||||||
|
show_aiproxy: !!process.env.AIPROXY_API_ENDPOINT
|
||||||
},
|
},
|
||||||
systemEnv: {
|
systemEnv: {
|
||||||
...fileRes.systemEnv,
|
...fileRes.systemEnv,
|
||||||
|
|||||||
183
projects/app/src/web/core/ai/channel.ts
Normal file
183
projects/app/src/web/core/ai/channel.ts
Normal file
@ -0,0 +1,183 @@
|
|||||||
|
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}`);
|
||||||
Loading…
x
Reference in New Issue
Block a user