EditChannel.js 40 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137
  1. import React, { useEffect, useState } from 'react';
  2. import { useNavigate } from 'react-router-dom';
  3. import { useTranslation } from 'react-i18next';
  4. import {
  5. API,
  6. isMobile,
  7. showError,
  8. showInfo,
  9. showSuccess,
  10. verifyJSON,
  11. } from '../../helpers';
  12. import { CHANNEL_OPTIONS } from '../../constants';
  13. import {
  14. SideSheet,
  15. Space,
  16. Spin,
  17. Button,
  18. Input,
  19. Typography,
  20. Select,
  21. TextArea,
  22. Checkbox,
  23. Banner,
  24. Modal,
  25. ImagePreview,
  26. Card,
  27. Tag,
  28. Avatar,
  29. } from '@douyinfe/semi-ui';
  30. import { getChannelModels, copy } from '../../helpers';
  31. import {
  32. IconSave,
  33. IconClose,
  34. IconServer,
  35. IconSetting,
  36. IconCode,
  37. IconGlobe,
  38. } from '@douyinfe/semi-icons';
  39. const { Text, Title } = Typography;
  40. const MODEL_MAPPING_EXAMPLE = {
  41. 'gpt-3.5-turbo': 'gpt-3.5-turbo-0125',
  42. };
  43. const STATUS_CODE_MAPPING_EXAMPLE = {
  44. 400: '500',
  45. };
  46. const REGION_EXAMPLE = {
  47. default: 'us-central1',
  48. 'claude-3-5-sonnet-20240620': 'europe-west1',
  49. };
  50. function type2secretPrompt(type) {
  51. // inputs.type === 15 ? '按照如下格式输入:APIKey|SecretKey' : (inputs.type === 18 ? '按照如下格式输入:APPID|APISecret|APIKey' : '请输入渠道对应的鉴权密钥')
  52. switch (type) {
  53. case 15:
  54. return '按照如下格式输入:APIKey|SecretKey';
  55. case 18:
  56. return '按照如下格式输入:APPID|APISecret|APIKey';
  57. case 22:
  58. return '按照如下格式输入:APIKey-AppId,例如:fastgpt-0sp2gtvfdgyi4k30jwlgwf1i-64f335d84283f05518e9e041';
  59. case 23:
  60. return '按照如下格式输入:AppId|SecretId|SecretKey';
  61. case 33:
  62. return '按照如下格式输入:Ak|Sk|Region';
  63. case 50:
  64. return '按照如下格式输入: AccessKey|SecretKey';
  65. default:
  66. return '请输入渠道对应的鉴权密钥';
  67. }
  68. }
  69. const EditChannel = (props) => {
  70. const { t } = useTranslation();
  71. const navigate = useNavigate();
  72. const channelId = props.editingChannel.id;
  73. const isEdit = channelId !== undefined;
  74. const [loading, setLoading] = useState(isEdit);
  75. const handleCancel = () => {
  76. props.handleClose();
  77. };
  78. const originInputs = {
  79. name: '',
  80. type: 1,
  81. key: '',
  82. openai_organization: '',
  83. max_input_tokens: 0,
  84. base_url: '',
  85. other: '',
  86. model_mapping: '',
  87. status_code_mapping: '',
  88. models: [],
  89. auto_ban: 1,
  90. test_model: '',
  91. groups: ['default'],
  92. priority: 0,
  93. weight: 0,
  94. tag: '',
  95. };
  96. const [batch, setBatch] = useState(false);
  97. const [autoBan, setAutoBan] = useState(true);
  98. // const [autoBan, setAutoBan] = useState(true);
  99. const [inputs, setInputs] = useState(originInputs);
  100. const [originModelOptions, setOriginModelOptions] = useState([]);
  101. const [modelOptions, setModelOptions] = useState([]);
  102. const [groupOptions, setGroupOptions] = useState([]);
  103. const [basicModels, setBasicModels] = useState([]);
  104. const [fullModels, setFullModels] = useState([]);
  105. const [customModel, setCustomModel] = useState('');
  106. const [modalImageUrl, setModalImageUrl] = useState('');
  107. const [isModalOpenurl, setIsModalOpenurl] = useState(false);
  108. const handleInputChange = (name, value) => {
  109. if (name === 'models' && Array.isArray(value)) {
  110. value = Array.from(new Set(value.map((m) => (m || '').trim())));
  111. }
  112. if (name === 'base_url' && value.endsWith('/v1')) {
  113. Modal.confirm({
  114. title: '警告',
  115. content:
  116. '不需要在末尾加/v1,New API会自动处理,添加后可能导致请求失败,是否继续?',
  117. onOk: () => {
  118. setInputs((inputs) => ({ ...inputs, [name]: value }));
  119. },
  120. });
  121. return;
  122. }
  123. setInputs((inputs) => ({ ...inputs, [name]: value }));
  124. if (name === 'type') {
  125. let localModels = [];
  126. switch (value) {
  127. case 2:
  128. localModels = [
  129. 'mj_imagine',
  130. 'mj_variation',
  131. 'mj_reroll',
  132. 'mj_blend',
  133. 'mj_upscale',
  134. 'mj_describe',
  135. 'mj_uploads',
  136. ];
  137. break;
  138. case 5:
  139. localModels = [
  140. 'swap_face',
  141. 'mj_imagine',
  142. 'mj_variation',
  143. 'mj_reroll',
  144. 'mj_blend',
  145. 'mj_upscale',
  146. 'mj_describe',
  147. 'mj_zoom',
  148. 'mj_shorten',
  149. 'mj_modal',
  150. 'mj_inpaint',
  151. 'mj_custom_zoom',
  152. 'mj_high_variation',
  153. 'mj_low_variation',
  154. 'mj_pan',
  155. 'mj_uploads',
  156. ];
  157. break;
  158. case 36:
  159. localModels = ['suno_music', 'suno_lyrics'];
  160. break;
  161. default:
  162. localModels = getChannelModels(value);
  163. break;
  164. }
  165. if (inputs.models.length === 0) {
  166. setInputs((inputs) => ({ ...inputs, models: localModels }));
  167. }
  168. setBasicModels(localModels);
  169. }
  170. //setAutoBan
  171. };
  172. const loadChannel = async () => {
  173. setLoading(true);
  174. let res = await API.get(`/api/channel/${channelId}`);
  175. if (res === undefined) {
  176. return;
  177. }
  178. const { success, message, data } = res.data;
  179. if (success) {
  180. if (data.models === '') {
  181. data.models = [];
  182. } else {
  183. data.models = data.models.split(',');
  184. }
  185. if (data.group === '') {
  186. data.groups = [];
  187. } else {
  188. data.groups = data.group.split(',');
  189. }
  190. if (data.model_mapping !== '') {
  191. data.model_mapping = JSON.stringify(
  192. JSON.parse(data.model_mapping),
  193. null,
  194. 2,
  195. );
  196. }
  197. setInputs(data);
  198. if (data.auto_ban === 0) {
  199. setAutoBan(false);
  200. } else {
  201. setAutoBan(true);
  202. }
  203. setBasicModels(getChannelModels(data.type));
  204. // console.log(data);
  205. } else {
  206. showError(message);
  207. }
  208. setLoading(false);
  209. };
  210. const fetchUpstreamModelList = async (name) => {
  211. // if (inputs['type'] !== 1) {
  212. // showError(t('仅支持 OpenAI 接口格式'));
  213. // return;
  214. // }
  215. setLoading(true);
  216. const models = inputs['models'] || [];
  217. let err = false;
  218. if (isEdit) {
  219. // 如果是编辑模式,使用已有的channel id获取模型列表
  220. const res = await API.get('/api/channel/fetch_models/' + channelId);
  221. if (res.data && res.data?.success) {
  222. models.push(...res.data.data);
  223. } else {
  224. err = true;
  225. }
  226. } else {
  227. // 如果是新建模式,通过后端代理获取模型列表
  228. if (!inputs?.['key']) {
  229. showError(t('请填写密钥'));
  230. err = true;
  231. } else {
  232. try {
  233. const res = await API.post('/api/channel/fetch_models', {
  234. base_url: inputs['base_url'],
  235. type: inputs['type'],
  236. key: inputs['key'],
  237. });
  238. if (res.data && res.data.success) {
  239. models.push(...res.data.data);
  240. } else {
  241. err = true;
  242. }
  243. } catch (error) {
  244. console.error('Error fetching models:', error);
  245. err = true;
  246. }
  247. }
  248. }
  249. if (!err) {
  250. handleInputChange(name, Array.from(new Set(models)));
  251. showSuccess(t('获取模型列表成功'));
  252. } else {
  253. showError(t('获取模型列表失败'));
  254. }
  255. setLoading(false);
  256. };
  257. const fetchModels = async () => {
  258. try {
  259. let res = await API.get(`/api/channel/models`);
  260. const localModelOptions = res.data.data.map((model) => {
  261. const id = (model.id || '').trim();
  262. return {
  263. key: id,
  264. label: id,
  265. value: id,
  266. };
  267. });
  268. setOriginModelOptions(localModelOptions);
  269. setFullModels(res.data.data.map((model) => model.id));
  270. setBasicModels(
  271. res.data.data
  272. .filter((model) => {
  273. return model.id.startsWith('gpt-') || model.id.startsWith('text-');
  274. })
  275. .map((model) => model.id),
  276. );
  277. } catch (error) {
  278. showError(error.message);
  279. }
  280. };
  281. const fetchGroups = async () => {
  282. try {
  283. let res = await API.get(`/api/group/`);
  284. if (res === undefined) {
  285. return;
  286. }
  287. setGroupOptions(
  288. res.data.data.map((group) => ({
  289. label: group,
  290. value: group,
  291. })),
  292. );
  293. } catch (error) {
  294. showError(error.message);
  295. }
  296. };
  297. useEffect(() => {
  298. const modelMap = new Map();
  299. originModelOptions.forEach(option => {
  300. const v = (option.value || '').trim();
  301. if (!modelMap.has(v)) {
  302. modelMap.set(v, option);
  303. }
  304. });
  305. inputs.models.forEach(model => {
  306. const v = (model || '').trim();
  307. if (!modelMap.has(v)) {
  308. modelMap.set(v, {
  309. key: v,
  310. label: v,
  311. value: v,
  312. });
  313. }
  314. });
  315. setModelOptions(Array.from(modelMap.values()));
  316. }, [originModelOptions, inputs.models]);
  317. useEffect(() => {
  318. fetchModels().then();
  319. fetchGroups().then();
  320. if (isEdit) {
  321. loadChannel().then(() => { });
  322. } else {
  323. setInputs(originInputs);
  324. let localModels = getChannelModels(inputs.type);
  325. setBasicModels(localModels);
  326. setInputs((inputs) => ({ ...inputs, models: localModels }));
  327. }
  328. }, [props.editingChannel.id]);
  329. const submit = async () => {
  330. if (!isEdit && (inputs.name === '' || inputs.key === '')) {
  331. showInfo(t('请填写渠道名称和渠道密钥!'));
  332. return;
  333. }
  334. if (inputs.models.length === 0) {
  335. showInfo(t('请至少选择一个模型!'));
  336. return;
  337. }
  338. if (inputs.model_mapping !== '' && !verifyJSON(inputs.model_mapping)) {
  339. showInfo(t('模型映射必须是合法的 JSON 格式!'));
  340. return;
  341. }
  342. let localInputs = { ...inputs };
  343. if (localInputs.base_url && localInputs.base_url.endsWith('/')) {
  344. localInputs.base_url = localInputs.base_url.slice(
  345. 0,
  346. localInputs.base_url.length - 1,
  347. );
  348. }
  349. if (localInputs.type === 18 && localInputs.other === '') {
  350. localInputs.other = 'v2.1';
  351. }
  352. let res;
  353. if (!Array.isArray(localInputs.models)) {
  354. showError(t('提交失败,请勿重复提交!'));
  355. handleCancel();
  356. return;
  357. }
  358. localInputs.auto_ban = autoBan ? 1 : 0;
  359. localInputs.models = localInputs.models.join(',');
  360. localInputs.group = localInputs.groups.join(',');
  361. if (isEdit) {
  362. res = await API.put(`/api/channel/`, {
  363. ...localInputs,
  364. id: parseInt(channelId),
  365. });
  366. } else {
  367. res = await API.post(`/api/channel/`, localInputs);
  368. }
  369. const { success, message } = res.data;
  370. if (success) {
  371. if (isEdit) {
  372. showSuccess(t('渠道更新成功!'));
  373. } else {
  374. showSuccess(t('渠道创建成功!'));
  375. setInputs(originInputs);
  376. }
  377. props.refresh();
  378. props.handleClose();
  379. } else {
  380. showError(message);
  381. }
  382. };
  383. const addCustomModels = () => {
  384. if (customModel.trim() === '') return;
  385. const modelArray = customModel.split(',').map((model) => model.trim());
  386. let localModels = [...inputs.models];
  387. let localModelOptions = [...modelOptions];
  388. const addedModels = [];
  389. modelArray.forEach((model) => {
  390. if (model && !localModels.includes(model)) {
  391. localModels.push(model);
  392. localModelOptions.push({
  393. key: model,
  394. label: model,
  395. value: model,
  396. });
  397. addedModels.push(model);
  398. }
  399. });
  400. setModelOptions(localModelOptions);
  401. setCustomModel('');
  402. handleInputChange('models', localModels);
  403. if (addedModels.length > 0) {
  404. showSuccess(
  405. t('已新增 {{count}} 个模型:{{list}}', {
  406. count: addedModels.length,
  407. list: addedModels.join(', '),
  408. })
  409. );
  410. } else {
  411. showInfo(t('未发现新增模型'));
  412. }
  413. };
  414. return (
  415. <>
  416. <SideSheet
  417. placement={isEdit ? 'right' : 'left'}
  418. title={
  419. <Space>
  420. <Tag color="blue" shape="circle">{isEdit ? t('编辑') : t('新建')}</Tag>
  421. <Title heading={4} className="m-0">
  422. {isEdit ? t('更新渠道信息') : t('创建新的渠道')}
  423. </Title>
  424. </Space>
  425. }
  426. bodyStyle={{ padding: '0' }}
  427. visible={props.visible}
  428. width={isMobile() ? '100%' : 600}
  429. footer={
  430. <div className="flex justify-end bg-white">
  431. <Space>
  432. <Button
  433. theme="solid"
  434. onClick={submit}
  435. icon={<IconSave />}
  436. >
  437. {t('提交')}
  438. </Button>
  439. <Button
  440. theme="light"
  441. type="primary"
  442. onClick={handleCancel}
  443. icon={<IconClose />}
  444. >
  445. {t('取消')}
  446. </Button>
  447. </Space>
  448. </div>
  449. }
  450. closeIcon={null}
  451. onCancel={() => handleCancel()}
  452. >
  453. <Spin spinning={loading}>
  454. <div className="p-2">
  455. <Card className="!rounded-2xl shadow-sm border-0 mb-6">
  456. {/* Header: Basic Info */}
  457. <div className="flex items-center mb-2">
  458. <Avatar size="small" color="blue" className="mr-2 shadow-md">
  459. <IconServer size={16} />
  460. </Avatar>
  461. <div>
  462. <Text className="text-lg font-medium">{t('基本信息')}</Text>
  463. <div className="text-xs text-gray-600">{t('渠道的基本配置信息')}</div>
  464. </div>
  465. </div>
  466. <div className="space-y-4">
  467. <div>
  468. <Text strong className="block mb-2">{t('类型')}</Text>
  469. <Select
  470. name='type'
  471. required
  472. optionList={CHANNEL_OPTIONS}
  473. value={inputs.type}
  474. onChange={(value) => handleInputChange('type', value)}
  475. style={{ width: '100%' }}
  476. filter
  477. searchPosition='dropdown'
  478. placeholder={t('请选择渠道类型')}
  479. />
  480. </div>
  481. <div>
  482. <Text strong className="block mb-2">{t('名称')}</Text>
  483. <Input
  484. required
  485. name='name'
  486. placeholder={t('请为渠道命名')}
  487. onChange={(value) => {
  488. handleInputChange('name', value);
  489. }}
  490. value={inputs.name}
  491. autoComplete='new-password'
  492. />
  493. </div>
  494. <div>
  495. <Text strong className="block mb-2">{t('密钥')}</Text>
  496. {batch ? (
  497. <TextArea
  498. name='key'
  499. required
  500. placeholder={t('请输入密钥,一行一个')}
  501. onChange={(value) => {
  502. handleInputChange('key', value);
  503. }}
  504. value={inputs.key}
  505. autosize={{ minRows: 6, maxRows: 6 }}
  506. autoComplete='new-password'
  507. />
  508. ) : (
  509. <>
  510. {inputs.type === 41 ? (
  511. <TextArea
  512. name='key'
  513. required
  514. placeholder={
  515. '{\n' +
  516. ' "type": "service_account",\n' +
  517. ' "project_id": "abc-bcd-123-456",\n' +
  518. ' "private_key_id": "123xxxxx456",\n' +
  519. ' "private_key": "-----BEGIN PRIVATE KEY-----xxxx\n' +
  520. ' "client_email": "xxx@developer.gserviceaccount.com",\n' +
  521. ' "client_id": "111222333",\n' +
  522. ' "auth_uri": "https://accounts.google.com/o/oauth2/auth",\n' +
  523. ' "token_uri": "https://oauth2.googleapis.com/token",\n' +
  524. ' "auth_provider_x509_cert_url": "https://www.googleapis.com/oauth2/v1/certs",\n' +
  525. ' "client_x509_cert_url": "https://xxxxx.gserviceaccount.com",\n' +
  526. ' "universe_domain": "googleapis.com"\n' +
  527. '}'
  528. }
  529. onChange={(value) => {
  530. handleInputChange('key', value);
  531. }}
  532. autosize={{ minRows: 10 }}
  533. value={inputs.key}
  534. autoComplete='new-password'
  535. />
  536. ) : (
  537. <Input
  538. name='key'
  539. required
  540. placeholder={t(type2secretPrompt(inputs.type))}
  541. onChange={(value) => {
  542. handleInputChange('key', value);
  543. }}
  544. value={inputs.key}
  545. autoComplete='new-password'
  546. />
  547. )}
  548. </>
  549. )}
  550. </div>
  551. {!isEdit && (
  552. <div className="flex items-center">
  553. <Checkbox
  554. checked={batch}
  555. onChange={() => setBatch(!batch)}
  556. />
  557. <Text strong className="ml-2">{t('批量创建')}</Text>
  558. </div>
  559. )}
  560. </div>
  561. </Card>
  562. {/* API Configuration Card */}
  563. <Card className="!rounded-2xl shadow-sm border-0 mb-6">
  564. {/* Header: API Config */}
  565. <div className="flex items-center mb-2">
  566. <Avatar size="small" color="green" className="mr-2 shadow-md">
  567. <IconGlobe size={16} />
  568. </Avatar>
  569. <div>
  570. <Text className="text-lg font-medium">{t('API 配置')}</Text>
  571. <div className="text-xs text-gray-600">{t('API 地址和相关配置')}</div>
  572. </div>
  573. </div>
  574. <div className="space-y-4">
  575. {inputs.type === 40 && (
  576. <Banner
  577. type='info'
  578. description={
  579. <div>
  580. <Text strong>{t('邀请链接')}:</Text>
  581. <Text
  582. link
  583. underline
  584. className="ml-2 cursor-pointer"
  585. onClick={() => window.open('https://cloud.siliconflow.cn/i/hij0YNTZ')}
  586. >
  587. https://cloud.siliconflow.cn/i/hij0YNTZ
  588. </Text>
  589. </div>
  590. }
  591. />
  592. )}
  593. {inputs.type === 3 && (
  594. <>
  595. <Banner
  596. type='warning'
  597. description={t('2025年5月10日后添加的渠道,不需要再在部署的时候移除模型名称中的"."')}
  598. className='!rounded-lg'
  599. />
  600. <div>
  601. <Text strong className="block mb-2">AZURE_OPENAI_ENDPOINT</Text>
  602. <Input
  603. name='azure_base_url'
  604. placeholder={t('请输入 AZURE_OPENAI_ENDPOINT,例如:https://docs-test-001.openai.azure.com')}
  605. onChange={(value) => handleInputChange('base_url', value)}
  606. value={inputs.base_url}
  607. autoComplete='new-password'
  608. />
  609. </div>
  610. <div>
  611. <Text strong className="block mb-2">{t('默认 API 版本')}</Text>
  612. <Input
  613. name='azure_other'
  614. placeholder={t('请输入默认 API 版本,例如:2025-04-01-preview')}
  615. onChange={(value) => handleInputChange('other', value)}
  616. value={inputs.other}
  617. autoComplete='new-password'
  618. />
  619. </div>
  620. </>
  621. )}
  622. {inputs.type === 8 && (
  623. <>
  624. <Banner
  625. type='warning'
  626. description={t('如果你对接的是上游One API或者New API等转发项目,请使用OpenAI类型,不要使用此类型,除非你知道你在做什么。')}
  627. className='!rounded-lg'
  628. />
  629. <div>
  630. <Text strong className="block mb-2">{t('完整的 Base URL,支持变量{model}')}</Text>
  631. <Input
  632. name='base_url'
  633. placeholder={t('请输入完整的URL,例如:https://api.openai.com/v1/chat/completions')}
  634. onChange={(value) => handleInputChange('base_url', value)}
  635. value={inputs.base_url}
  636. autoComplete='new-password'
  637. />
  638. </div>
  639. </>
  640. )}
  641. {inputs.type === 37 && (
  642. <Banner
  643. type='warning'
  644. description={t('Dify渠道只适配chatflow和agent,并且agent不支持图片!')}
  645. className='!rounded-lg'
  646. />
  647. )}
  648. {inputs.type !== 3 && inputs.type !== 8 && inputs.type !== 22 && inputs.type !== 36 && inputs.type !== 45 && (
  649. <div>
  650. <Text strong className="block mb-2">{t('API地址')}</Text>
  651. <Input
  652. name='base_url'
  653. placeholder={t('此项可选,用于通过自定义API地址来进行 API 调用,末尾不要带/v1和/')}
  654. onChange={(value) => handleInputChange('base_url', value)}
  655. value={inputs.base_url}
  656. autoComplete='new-password'
  657. />
  658. <Text type="tertiary" className="mt-1 text-xs">
  659. {t('对于官方渠道,new-api已经内置地址,除非是第三方代理站点或者Azure的特殊接入地址,否则不需要填写')}
  660. </Text>
  661. </div>
  662. )}
  663. {inputs.type === 22 && (
  664. <div>
  665. <Text strong className="block mb-2">{t('私有部署地址')}</Text>
  666. <Input
  667. name='base_url'
  668. placeholder={t('请输入私有部署地址,格式为:https://fastgpt.run/api/openapi')}
  669. onChange={(value) => handleInputChange('base_url', value)}
  670. value={inputs.base_url}
  671. autoComplete='new-password'
  672. />
  673. </div>
  674. )}
  675. {inputs.type === 36 && (
  676. <div>
  677. <Text strong className="block mb-2">
  678. {t('注意非Chat API,请务必填写正确的API地址,否则可能导致无法使用')}
  679. </Text>
  680. <Input
  681. name='base_url'
  682. placeholder={t('请输入到 /suno 前的路径,通常就是域名,例如:https://api.example.com')}
  683. onChange={(value) => handleInputChange('base_url', value)}
  684. value={inputs.base_url}
  685. autoComplete='new-password'
  686. />
  687. </div>
  688. )}
  689. </div>
  690. </Card>
  691. {/* Model Configuration Card */}
  692. <Card className="!rounded-2xl shadow-sm border-0 mb-6">
  693. {/* Header: Model Config */}
  694. <div className="flex items-center mb-2">
  695. <Avatar size="small" color="purple" className="mr-2 shadow-md">
  696. <IconCode size={16} />
  697. </Avatar>
  698. <div>
  699. <Text className="text-lg font-medium">{t('模型配置')}</Text>
  700. <div className="text-xs text-gray-600">{t('模型选择和映射设置')}</div>
  701. </div>
  702. </div>
  703. <div className="space-y-4">
  704. <div>
  705. <Text strong className="block mb-2">{t('模型')}</Text>
  706. <Select
  707. placeholder={t('请选择该渠道所支持的模型')}
  708. name='models'
  709. required
  710. multiple
  711. selection
  712. filter
  713. searchPosition='dropdown'
  714. onChange={(value) => handleInputChange('models', value)}
  715. value={inputs.models}
  716. autoComplete='new-password'
  717. optionList={modelOptions}
  718. />
  719. </div>
  720. <div className="flex flex-wrap gap-2">
  721. <Button
  722. type='primary'
  723. onClick={() => handleInputChange('models', basicModels)}
  724. >
  725. {t('填入相关模型')}
  726. </Button>
  727. <Button
  728. type='secondary'
  729. onClick={() => handleInputChange('models', fullModels)}
  730. >
  731. {t('填入所有模型')}
  732. </Button>
  733. {isEdit ? (
  734. <Button
  735. type='tertiary'
  736. onClick={() => fetchUpstreamModelList('models')}
  737. >
  738. {t('获取模型列表')}
  739. </Button>
  740. ) : null}
  741. <Button
  742. type='warning'
  743. onClick={() => handleInputChange('models', [])}
  744. >
  745. {t('清除所有模型')}
  746. </Button>
  747. <Button
  748. type='tertiary'
  749. onClick={() => {
  750. if (inputs.models.length === 0) {
  751. showInfo(t('没有模型可以复制'));
  752. return;
  753. }
  754. try {
  755. copy(inputs.models.join(','));
  756. showSuccess(t('模型列表已复制到剪贴板'));
  757. } catch (error) {
  758. showError(t('复制失败'));
  759. }
  760. }}
  761. >
  762. {t('复制所有模型')}
  763. </Button>
  764. </div>
  765. {!isEdit && (
  766. <Banner
  767. type='info'
  768. description={t('创建后可在编辑渠道时获取上游模型列表')}
  769. className='!rounded-lg'
  770. />
  771. )}
  772. <div>
  773. <Input
  774. addonAfter={
  775. <Button type='primary' onClick={addCustomModels} className="!rounded-r-lg">
  776. {t('填入')}
  777. </Button>
  778. }
  779. placeholder={t('输入自定义模型名称')}
  780. value={customModel}
  781. onChange={(value) => setCustomModel(value.trim())}
  782. />
  783. </div>
  784. <div>
  785. <Text strong className="block mb-2">{t('模型重定向')}</Text>
  786. <TextArea
  787. placeholder={
  788. t('此项可选,用于修改请求体中的模型名称,为一个 JSON 字符串,键为请求中模型名称,值为要替换的模型名称,例如:') +
  789. `\n${JSON.stringify(MODEL_MAPPING_EXAMPLE, null, 2)}`
  790. }
  791. name='model_mapping'
  792. onChange={(value) => handleInputChange('model_mapping', value)}
  793. autosize
  794. value={inputs.model_mapping}
  795. autoComplete='new-password'
  796. />
  797. <Text
  798. className="!text-semi-color-primary cursor-pointer mt-1 block"
  799. onClick={() => handleInputChange('model_mapping', JSON.stringify(MODEL_MAPPING_EXAMPLE, null, 2))}
  800. >
  801. {t('填入模板')}
  802. </Text>
  803. </div>
  804. <div>
  805. <Text strong className="block mb-2">{t('默认测试模型')}</Text>
  806. <Input
  807. name='test_model'
  808. placeholder={t('不填则为模型列表第一个')}
  809. onChange={(value) => handleInputChange('test_model', value)}
  810. value={inputs.test_model}
  811. />
  812. </div>
  813. </div>
  814. </Card>
  815. {/* Advanced Settings Card */}
  816. <Card className="!rounded-2xl shadow-sm border-0 mb-6">
  817. {/* Header: Advanced Settings */}
  818. <div className="flex items-center mb-2">
  819. <Avatar size="small" color="orange" className="mr-2 shadow-md">
  820. <IconSetting size={16} />
  821. </Avatar>
  822. <div>
  823. <Text className="text-lg font-medium">{t('高级设置')}</Text>
  824. <div className="text-xs text-gray-600">{t('渠道的高级配置选项')}</div>
  825. </div>
  826. </div>
  827. <div className="space-y-4">
  828. <div>
  829. <Text strong className="block mb-2">{t('分组')}</Text>
  830. <Select
  831. placeholder={t('请选择可以使用该渠道的分组')}
  832. name='groups'
  833. required
  834. multiple
  835. selection
  836. allowAdditions
  837. additionLabel={t('请在系统设置页面编辑分组倍率以添加新的分组:')}
  838. onChange={(value) => handleInputChange('groups', value)}
  839. value={inputs.groups}
  840. autoComplete='new-password'
  841. optionList={groupOptions}
  842. />
  843. </div>
  844. {inputs.type === 18 && (
  845. <div>
  846. <Text strong className="block mb-2">{t('模型版本')}</Text>
  847. <Input
  848. name='other'
  849. placeholder={'请输入星火大模型版本,注意是接口地址中的版本号,例如:v2.1'}
  850. onChange={(value) => handleInputChange('other', value)}
  851. value={inputs.other}
  852. autoComplete='new-password'
  853. />
  854. </div>
  855. )}
  856. {inputs.type === 41 && (
  857. <div>
  858. <Text strong className="block mb-2">{t('部署地区')}</Text>
  859. <TextArea
  860. name='other'
  861. placeholder={t(
  862. '请输入部署地区,例如:us-central1\n支持使用模型映射格式\n' +
  863. '{\n' +
  864. ' "default": "us-central1",\n' +
  865. ' "claude-3-5-sonnet-20240620": "europe-west1"\n' +
  866. '}'
  867. )}
  868. autosize={{ minRows: 2 }}
  869. onChange={(value) => handleInputChange('other', value)}
  870. value={inputs.other}
  871. autoComplete='new-password'
  872. />
  873. <Text
  874. className="!text-semi-color-primary cursor-pointer mt-1 block"
  875. onClick={() => handleInputChange('other', JSON.stringify(REGION_EXAMPLE, null, 2))}
  876. >
  877. {t('填入模板')}
  878. </Text>
  879. </div>
  880. )}
  881. {inputs.type === 21 && (
  882. <div>
  883. <Text strong className="block mb-2">{t('知识库 ID')}</Text>
  884. <Input
  885. name='other'
  886. placeholder={'请输入知识库 ID,例如:123456'}
  887. onChange={(value) => handleInputChange('other', value)}
  888. value={inputs.other}
  889. autoComplete='new-password'
  890. />
  891. </div>
  892. )}
  893. {inputs.type === 39 && (
  894. <div>
  895. <Text strong className="block mb-2">Account ID</Text>
  896. <Input
  897. name='other'
  898. placeholder={'请输入Account ID,例如:d6b5da8hk1awo8nap34ube6gh'}
  899. onChange={(value) => handleInputChange('other', value)}
  900. value={inputs.other}
  901. autoComplete='new-password'
  902. />
  903. </div>
  904. )}
  905. {inputs.type === 49 && (
  906. <div>
  907. <Text strong className="block mb-2">{t('智能体ID')}</Text>
  908. <Input
  909. name='other'
  910. placeholder={'请输入智能体ID,例如:7342866812345'}
  911. onChange={(value) => handleInputChange('other', value)}
  912. value={inputs.other}
  913. autoComplete='new-password'
  914. />
  915. </div>
  916. )}
  917. <div>
  918. <Text strong className="block mb-2">{t('渠道标签')}</Text>
  919. <Input
  920. name='tag'
  921. placeholder={t('渠道标签')}
  922. onChange={(value) => handleInputChange('tag', value)}
  923. value={inputs.tag}
  924. autoComplete='new-password'
  925. />
  926. </div>
  927. <div>
  928. <Text strong className="block mb-2">{t('渠道优先级')}</Text>
  929. <Input
  930. name='priority'
  931. placeholder={t('渠道优先级')}
  932. onChange={(value) => {
  933. const number = parseInt(value);
  934. if (isNaN(number)) {
  935. handleInputChange('priority', value);
  936. } else {
  937. handleInputChange('priority', number);
  938. }
  939. }}
  940. value={inputs.priority}
  941. autoComplete='new-password'
  942. />
  943. </div>
  944. <div>
  945. <Text strong className="block mb-2">{t('渠道权重')}</Text>
  946. <Input
  947. name='weight'
  948. placeholder={t('渠道权重')}
  949. onChange={(value) => {
  950. const number = parseInt(value);
  951. if (isNaN(number)) {
  952. handleInputChange('weight', value);
  953. } else {
  954. handleInputChange('weight', number);
  955. }
  956. }}
  957. value={inputs.weight}
  958. autoComplete='new-password'
  959. />
  960. </div>
  961. <div>
  962. <Text strong className="block mb-2">{t('渠道额外设置')}</Text>
  963. <TextArea
  964. placeholder={
  965. t('此项可选,用于配置渠道特定设置,为一个 JSON 字符串,例如:') +
  966. '\n{\n "force_format": true\n}'
  967. }
  968. name='setting'
  969. onChange={(value) => handleInputChange('setting', value)}
  970. autosize
  971. value={inputs.setting}
  972. autoComplete='new-password'
  973. />
  974. <div className="flex gap-2 mt-1">
  975. <Text
  976. className="!text-semi-color-primary cursor-pointer"
  977. onClick={() => {
  978. handleInputChange(
  979. 'setting',
  980. JSON.stringify({ force_format: true }, null, 2),
  981. );
  982. }}
  983. >
  984. {t('填入模板')}
  985. </Text>
  986. <Text
  987. className="!text-semi-color-primary cursor-pointer"
  988. onClick={() => {
  989. window.open(
  990. 'https://github.com/QuantumNous/new-api/blob/main/docs/channel/other_setting.md',
  991. );
  992. }}
  993. >
  994. {t('设置说明')}
  995. </Text>
  996. </div>
  997. </div>
  998. <div>
  999. <Text strong className="block mb-2">{t('参数覆盖')}</Text>
  1000. <TextArea
  1001. placeholder={
  1002. t('此项可选,用于覆盖请求参数。不支持覆盖 stream 参数。为一个 JSON 字符串,例如:') +
  1003. '\n{\n "temperature": 0\n}'
  1004. }
  1005. name='param_override'
  1006. onChange={(value) => handleInputChange('param_override', value)}
  1007. autosize
  1008. value={inputs.param_override}
  1009. autoComplete='new-password'
  1010. />
  1011. </div>
  1012. {inputs.type === 1 && (
  1013. <div>
  1014. <Text strong className="block mb-2">{t('组织')}</Text>
  1015. <Input
  1016. name='openai_organization'
  1017. placeholder={t('请输入组织org-xxx')}
  1018. onChange={(value) => handleInputChange('openai_organization', value)}
  1019. value={inputs.openai_organization}
  1020. />
  1021. <Text type="tertiary" className="mt-1 text-xs">
  1022. {t('组织,可选,不填则为默认组织')}
  1023. </Text>
  1024. </div>
  1025. )}
  1026. <div className="flex items-center">
  1027. <Checkbox
  1028. checked={autoBan}
  1029. onChange={() => setAutoBan(!autoBan)}
  1030. />
  1031. <Text strong className="ml-2">
  1032. {t('是否自动禁用(仅当自动禁用开启时有效),关闭后不会自动禁用该渠道')}
  1033. </Text>
  1034. </div>
  1035. <div>
  1036. <Text strong className="block mb-2">
  1037. {t('状态码复写(仅影响本地判断,不修改返回到上游的状态码)')}
  1038. </Text>
  1039. <TextArea
  1040. placeholder={
  1041. t('此项可选,用于复写返回的状态码,比如将claude渠道的400错误复写为500(用于重试),请勿滥用该功能,例如:') +
  1042. '\n' +
  1043. JSON.stringify(STATUS_CODE_MAPPING_EXAMPLE, null, 2)
  1044. }
  1045. name='status_code_mapping'
  1046. onChange={(value) => handleInputChange('status_code_mapping', value)}
  1047. autosize
  1048. value={inputs.status_code_mapping}
  1049. autoComplete='new-password'
  1050. />
  1051. <Text
  1052. className="!text-semi-color-primary cursor-pointer mt-1 block"
  1053. onClick={() => {
  1054. handleInputChange(
  1055. 'status_code_mapping',
  1056. JSON.stringify(STATUS_CODE_MAPPING_EXAMPLE, null, 2),
  1057. );
  1058. }}
  1059. >
  1060. {t('填入模板')}
  1061. </Text>
  1062. </div>
  1063. </div>
  1064. </Card>
  1065. </div>
  1066. </Spin>
  1067. <ImagePreview
  1068. src={modalImageUrl}
  1069. visible={isModalOpenurl}
  1070. onVisibleChange={(visible) => setIsModalOpenurl(visible)}
  1071. />
  1072. </SideSheet>
  1073. </>
  1074. );
  1075. };
  1076. export default EditChannel;