mirror of
https://github.com/labring/FastGPT.git
synced 2025-07-23 05:12:39 +00:00
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:
@@ -100,6 +100,13 @@ const DateRangePicker = ({
|
||||
if (date?.to === undefined) {
|
||||
date.to = date.from;
|
||||
}
|
||||
|
||||
if (date?.from) {
|
||||
date.from = new Date(date.from.setHours(0, 0, 0, 0));
|
||||
}
|
||||
if (date?.to) {
|
||||
date.to = new Date(date.to.setHours(23, 59, 59, 999));
|
||||
}
|
||||
setRange(date);
|
||||
onChange?.(date);
|
||||
}}
|
||||
|
@@ -1,4 +1,5 @@
|
||||
// @ts-nocheck
|
||||
|
||||
export const iconPaths = {
|
||||
book: () => import('./icons/book.svg'),
|
||||
change: () => import('./icons/change.svg'),
|
||||
@@ -32,8 +33,10 @@ export const iconPaths = {
|
||||
'common/customTitleLight': () => import('./icons/common/customTitleLight.svg'),
|
||||
'common/data': () => import('./icons/common/data.svg'),
|
||||
'common/dingtalkFill': () => import('./icons/common/dingtalkFill.svg'),
|
||||
'common/disable': () => import('./icons/common/disable.svg'),
|
||||
'common/downArrowFill': () => import('./icons/common/downArrowFill.svg'),
|
||||
'common/editor/resizer': () => import('./icons/common/editor/resizer.svg'),
|
||||
'common/enable': () => import('./icons/common/enable.svg'),
|
||||
'common/errorFill': () => import('./icons/common/errorFill.svg'),
|
||||
'common/file/move': () => import('./icons/common/file/move.svg'),
|
||||
'common/folderFill': () => import('./icons/common/folderFill.svg'),
|
||||
|
@@ -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 { UseFormRegister } from 'react-hook-form';
|
||||
|
||||
type Props = Omit<NumberInputProps, 'onChange'> & {
|
||||
type Props = Omit<NumberInputProps, 'onChange' | 'onBlur'> & {
|
||||
onChange?: (e?: number) => any;
|
||||
onBlur?: (e?: number) => any;
|
||||
placeholder?: string;
|
||||
register?: UseFormRegister<any>;
|
||||
name?: string;
|
||||
@@ -19,11 +20,21 @@ type Props = Omit<NumberInputProps, 'onChange'> & {
|
||||
};
|
||||
|
||||
const MyNumberInput = (props: Props) => {
|
||||
const { register, name, onChange, placeholder, bg, ...restProps } = props;
|
||||
const { register, name, onChange, onBlur, placeholder, bg, ...restProps } = props;
|
||||
|
||||
return (
|
||||
<NumberInput
|
||||
{...restProps}
|
||||
onBlur={(e) => {
|
||||
if (!onBlur) return;
|
||||
const numE = Number(e.target.value);
|
||||
if (isNaN(numE)) {
|
||||
// @ts-ignore
|
||||
onBlur('');
|
||||
} else {
|
||||
onBlur(numE);
|
||||
}
|
||||
}}
|
||||
onChange={(e) => {
|
||||
if (!onChange) return;
|
||||
const numE = Number(e);
|
||||
@@ -38,6 +49,8 @@ const MyNumberInput = (props: Props) => {
|
||||
<NumberInputField
|
||||
bg={bg}
|
||||
placeholder={placeholder}
|
||||
h={restProps.h}
|
||||
defaultValue={restProps.defaultValue}
|
||||
{...(register && name
|
||||
? register(name, {
|
||||
required: props.isRequired,
|
||||
|
@@ -98,7 +98,6 @@ const MultipleSelect = <T = any,>({
|
||||
return (
|
||||
<MenuItem
|
||||
key={i}
|
||||
{...menuItemStyles}
|
||||
{...(isSelected
|
||||
? {
|
||||
color: 'primary.600'
|
||||
@@ -114,6 +113,7 @@ const MultipleSelect = <T = any,>({
|
||||
whiteSpace={'pre-wrap'}
|
||||
fontSize={'sm'}
|
||||
gap={2}
|
||||
{...menuItemStyles}
|
||||
>
|
||||
<Checkbox isChecked={isSelected} />
|
||||
{item.icon && <MyAvatar src={item.icon} w={'1rem'} borderRadius={'0'} />}
|
||||
@@ -204,6 +204,7 @@ const MultipleSelect = <T = any,>({
|
||||
}}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
onclickItem(item.value);
|
||||
}}
|
||||
/>
|
||||
@@ -230,7 +231,6 @@ const MultipleSelect = <T = any,>({
|
||||
overflowY={'auto'}
|
||||
>
|
||||
<MenuItem
|
||||
{...menuItemStyles}
|
||||
color={isSelectAll ? 'primary.600' : 'myGray.900'}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
@@ -241,6 +241,7 @@ const MultipleSelect = <T = any,>({
|
||||
fontSize={'sm'}
|
||||
gap={2}
|
||||
mb={1}
|
||||
{...menuItemStyles}
|
||||
>
|
||||
<Checkbox isChecked={isSelectAll} />
|
||||
<Box flex={'1 0 0'}>{t('common:common.All')}</Box>
|
||||
|
@@ -4,7 +4,8 @@ import React, {
|
||||
useMemo,
|
||||
useEffect,
|
||||
useImperativeHandle,
|
||||
ForwardedRef
|
||||
ForwardedRef,
|
||||
useState
|
||||
} from 'react';
|
||||
import {
|
||||
Menu,
|
||||
@@ -15,7 +16,8 @@ import {
|
||||
MenuButton,
|
||||
Box,
|
||||
css,
|
||||
Flex
|
||||
Flex,
|
||||
Input
|
||||
} from '@chakra-ui/react';
|
||||
import type { ButtonProps, MenuItemProps } from '@chakra-ui/react';
|
||||
import MyIcon from '../Icon';
|
||||
@@ -33,8 +35,10 @@ import { useScrollPagination } from '../../../hooks/useScrollPagination';
|
||||
export type SelectProps<T = any> = ButtonProps & {
|
||||
value?: T;
|
||||
placeholder?: string;
|
||||
isSearch?: boolean;
|
||||
list: {
|
||||
alias?: string;
|
||||
icon?: string;
|
||||
label: string | React.ReactNode;
|
||||
description?: string;
|
||||
value: T;
|
||||
@@ -49,6 +53,7 @@ const MySelect = <T = any,>(
|
||||
{
|
||||
placeholder,
|
||||
value,
|
||||
isSearch = false,
|
||||
width = '100%',
|
||||
list = [],
|
||||
onchange,
|
||||
@@ -63,6 +68,7 @@ const MySelect = <T = any,>(
|
||||
const ButtonRef = useRef<HTMLButtonElement>(null);
|
||||
const MenuListRef = useRef<HTMLDivElement>(null);
|
||||
const SelectedItemRef = useRef<HTMLDivElement>(null);
|
||||
const SearchInputRef = useRef<HTMLInputElement>(null);
|
||||
|
||||
const menuItemStyles: MenuItemProps = {
|
||||
borderRadius: 'sm',
|
||||
@@ -79,6 +85,18 @@ const MySelect = <T = any,>(
|
||||
const { isOpen, onOpen, onClose } = useDisclosure();
|
||||
const selectItem = useMemo(() => list.find((item) => item.value === value), [list, value]);
|
||||
|
||||
const [search, setSearch] = useState('');
|
||||
const filterList = useMemo(() => {
|
||||
if (!isSearch || !search) {
|
||||
return list;
|
||||
}
|
||||
return list.filter((item) => {
|
||||
const text = `${item.label?.toString()}${item.alias}${item.value}`;
|
||||
const regx = new RegExp(search, 'gi');
|
||||
return regx.test(text);
|
||||
});
|
||||
}, [list, search, isSearch]);
|
||||
|
||||
useImperativeHandle(ref, () => ({
|
||||
focus() {
|
||||
onOpen();
|
||||
@@ -90,17 +108,19 @@ const MySelect = <T = any,>(
|
||||
const menu = MenuListRef.current;
|
||||
const selectedItem = SelectedItemRef.current;
|
||||
menu.scrollTop = selectedItem.offsetTop - menu.offsetTop - 100;
|
||||
|
||||
if (isSearch) {
|
||||
setSearch('');
|
||||
}
|
||||
}
|
||||
}, [isOpen]);
|
||||
}, [isSearch, isOpen]);
|
||||
|
||||
const { runAsync: onChange, loading } = useRequest2((val: T) => onchange?.(val));
|
||||
|
||||
const isSelecting = loading || isLoading;
|
||||
|
||||
const ListRender = useMemo(() => {
|
||||
return (
|
||||
<>
|
||||
{list.map((item, i) => (
|
||||
{filterList.map((item, i) => (
|
||||
<Box key={i}>
|
||||
<MenuItem
|
||||
{...menuItemStyles}
|
||||
@@ -123,7 +143,10 @@ const MySelect = <T = any,>(
|
||||
fontSize={'sm'}
|
||||
display={'block'}
|
||||
>
|
||||
<Box>{item.label}</Box>
|
||||
<Flex alignItems={'center'}>
|
||||
{item.icon && <MyIcon mr={2} name={item.icon as any} w={'1rem'} />}
|
||||
{item.label}
|
||||
</Flex>
|
||||
{item.description && (
|
||||
<Box color={'myGray.500'} fontSize={'xs'}>
|
||||
{item.description}
|
||||
@@ -135,7 +158,9 @@ const MySelect = <T = any,>(
|
||||
))}
|
||||
</>
|
||||
);
|
||||
}, [list, value]);
|
||||
}, [filterList, value]);
|
||||
|
||||
const isSelecting = loading || isLoading;
|
||||
|
||||
return (
|
||||
<Box
|
||||
@@ -176,8 +201,33 @@ const MySelect = <T = any,>(
|
||||
{...props}
|
||||
>
|
||||
<Flex alignItems={'center'}>
|
||||
{isSelecting && <MyIcon mr={2} name={'common/loading'} w={'16px'} />}
|
||||
{selectItem?.alias || selectItem?.label || placeholder}
|
||||
{isSelecting && <MyIcon mr={2} name={'common/loading'} w={'1rem'} />}
|
||||
{isSearch && isOpen ? (
|
||||
<Input
|
||||
ref={SearchInputRef}
|
||||
autoFocus
|
||||
variant={'unstyled'}
|
||||
value={search}
|
||||
onChange={(e) => setSearch(e.target.value)}
|
||||
placeholder={
|
||||
selectItem?.alias ||
|
||||
(typeof selectItem?.label === 'string' ? selectItem?.label : placeholder)
|
||||
}
|
||||
size={'sm'}
|
||||
w={'100%'}
|
||||
color={'myGray.700'}
|
||||
onBlur={() => {
|
||||
setTimeout(() => {
|
||||
SearchInputRef?.current?.focus();
|
||||
}, 0);
|
||||
}}
|
||||
/>
|
||||
) : (
|
||||
<>
|
||||
{selectItem?.icon && <MyIcon mr={2} name={selectItem.icon as any} w={'1rem'} />}
|
||||
{selectItem?.alias || selectItem?.label || placeholder}
|
||||
</>
|
||||
)}
|
||||
</Flex>
|
||||
</MenuButton>
|
||||
|
||||
|
@@ -217,7 +217,7 @@ export function useScrollPagination<
|
||||
const offset = init ? 0 : data.length;
|
||||
|
||||
setTrue();
|
||||
|
||||
console.log(offset);
|
||||
try {
|
||||
const res = await api({
|
||||
offset,
|
||||
|
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_failed": "Copy Failed, Please Copy Manually",
|
||||
"common.Create Failed": "Creation Failed",
|
||||
"common.Create New": "Create",
|
||||
"common.Create Success": "Created Successfully",
|
||||
"common.Create Time": "Creation Time",
|
||||
"common.Creating": "Creating",
|
||||
|
@@ -3,7 +3,7 @@
|
||||
"add_default_model": "添加预设模型",
|
||||
"api_key": "API 密钥",
|
||||
"bills_and_invoices": "账单与发票",
|
||||
"channel": "渠道",
|
||||
"channel": "模型渠道",
|
||||
"config_model": "模型配置",
|
||||
"confirm_logout": "确认退出登录?",
|
||||
"create_channel": "新增渠道",
|
||||
|
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_failed": "复制失败,请手动复制",
|
||||
"common.Create Failed": "创建异常",
|
||||
"common.Create New": "新建",
|
||||
"common.Create Success": "创建成功",
|
||||
"common.Create Time": "创建时间",
|
||||
"common.Creating": "创建中",
|
||||
|
@@ -3,7 +3,7 @@
|
||||
"add_default_model": "新增預設模型",
|
||||
"api_key": "API 金鑰",
|
||||
"bills_and_invoices": "帳單與發票",
|
||||
"channel": "頻道",
|
||||
"channel": "模型渠道",
|
||||
"config_model": "模型配置",
|
||||
"confirm_logout": "確認登出登入?",
|
||||
"create_channel": "新增頻道",
|
||||
|
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_failed": "複製失敗,請手動複製",
|
||||
"common.Create Failed": "建立失敗",
|
||||
"common.Create New": "建立新項目",
|
||||
"common.Create Success": "建立成功",
|
||||
"common.Create Time": "建立時間",
|
||||
"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 chat from '../i18n/zh-CN/chat.json';
|
||||
import login from '../i18n/zh-CN/login.json';
|
||||
import account_model from '../i18n/zh-CN/account_model.json';
|
||||
|
||||
export interface I18nNamespaces {
|
||||
common: typeof common;
|
||||
@@ -39,6 +40,7 @@ export interface I18nNamespaces {
|
||||
account: typeof account;
|
||||
account_team: typeof account_team;
|
||||
account_thirdParty: typeof account_thirdParty;
|
||||
account_model: typeof account_model;
|
||||
}
|
||||
|
||||
export type I18nNsType = (keyof I18nNamespaces)[];
|
||||
@@ -73,7 +75,8 @@ declare module 'i18next' {
|
||||
'account_promotion',
|
||||
'account_thirdParty',
|
||||
'account',
|
||||
'account_team'
|
||||
'account_team',
|
||||
'account_model'
|
||||
];
|
||||
resources: I18nNamespaces;
|
||||
}
|
||||
|
Reference in New Issue
Block a user