EditChannel.js 54 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253125412551256125712581259126012611262126312641265126612671268126912701271127212731274127512761277127812791280128112821283128412851286128712881289129012911292129312941295129612971298129913001301130213031304130513061307130813091310131113121313131413151316131713181319132013211322132313241325132613271328132913301331133213331334133513361337133813391340134113421343134413451346134713481349135013511352135313541355135613571358135913601361136213631364136513661367136813691370137113721373137413751376137713781379138013811382138313841385138613871388138913901391139213931394139513961397139813991400140114021403140414051406140714081409141014111412141314141415141614171418141914201421142214231424142514261427142814291430143114321433143414351436143714381439144014411442144314441445144614471448144914501451145214531454145514561457145814591460146114621463146414651466146714681469
  1. import React, { useEffect, useState, useRef, useMemo } from 'react';
  2. import { useNavigate } from 'react-router-dom';
  3. import { useTranslation } from 'react-i18next';
  4. import {
  5. API,
  6. showError,
  7. showInfo,
  8. showSuccess,
  9. verifyJSON,
  10. } from '../../helpers';
  11. import { useIsMobile } from '../../hooks/useIsMobile.js';
  12. import { CHANNEL_OPTIONS } from '../../constants';
  13. import {
  14. SideSheet,
  15. Space,
  16. Spin,
  17. Button,
  18. Typography,
  19. Checkbox,
  20. Banner,
  21. Modal,
  22. ImagePreview,
  23. Card,
  24. Tag,
  25. Avatar,
  26. Form,
  27. Row,
  28. Col,
  29. Highlight,
  30. } from '@douyinfe/semi-ui';
  31. import { getChannelModels, copy, getChannelIcon, getModelCategories } from '../../helpers';
  32. import {
  33. IconSave,
  34. IconClose,
  35. IconServer,
  36. IconSetting,
  37. IconCode,
  38. IconGlobe,
  39. IconBolt,
  40. } from '@douyinfe/semi-icons';
  41. const { Text, Title } = Typography;
  42. const MODEL_MAPPING_EXAMPLE = {
  43. 'gpt-3.5-turbo': 'gpt-3.5-turbo-0125',
  44. };
  45. const STATUS_CODE_MAPPING_EXAMPLE = {
  46. 400: '500',
  47. };
  48. const REGION_EXAMPLE = {
  49. default: 'us-central1',
  50. 'claude-3-5-sonnet-20240620': 'europe-west1',
  51. };
  52. function type2secretPrompt(type) {
  53. // inputs.type === 15 ? '按照如下格式输入:APIKey|SecretKey' : (inputs.type === 18 ? '按照如下格式输入:APPID|APISecret|APIKey' : '请输入渠道对应的鉴权密钥')
  54. switch (type) {
  55. case 15:
  56. return '按照如下格式输入:APIKey|SecretKey';
  57. case 18:
  58. return '按照如下格式输入:APPID|APISecret|APIKey';
  59. case 22:
  60. return '按照如下格式输入:APIKey-AppId,例如:fastgpt-0sp2gtvfdgyi4k30jwlgwf1i-64f335d84283f05518e9e041';
  61. case 23:
  62. return '按照如下格式输入:AppId|SecretId|SecretKey';
  63. case 33:
  64. return '按照如下格式输入:Ak|Sk|Region';
  65. case 50:
  66. return '按照如下格式输入: AccessKey|SecretKey, 如果上游是New API,则直接输ApiKey';
  67. case 51:
  68. return '按照如下格式输入: Access Key ID|Secret Access Key';
  69. default:
  70. return '请输入渠道对应的鉴权密钥';
  71. }
  72. }
  73. const EditChannel = (props) => {
  74. const { t } = useTranslation();
  75. const navigate = useNavigate();
  76. const channelId = props.editingChannel.id;
  77. const isEdit = channelId !== undefined;
  78. const [loading, setLoading] = useState(isEdit);
  79. const isMobile = useIsMobile();
  80. const handleCancel = () => {
  81. props.handleClose();
  82. };
  83. const originInputs = {
  84. name: '',
  85. type: 1,
  86. key: '',
  87. openai_organization: '',
  88. max_input_tokens: 0,
  89. base_url: '',
  90. other: '',
  91. model_mapping: '',
  92. status_code_mapping: '',
  93. models: [],
  94. auto_ban: 1,
  95. test_model: '',
  96. groups: ['default'],
  97. priority: 0,
  98. weight: 0,
  99. tag: '',
  100. multi_key_mode: 'random',
  101. };
  102. const [batch, setBatch] = useState(false);
  103. const [multiToSingle, setMultiToSingle] = useState(false);
  104. const [multiKeyMode, setMultiKeyMode] = useState('random');
  105. const [autoBan, setAutoBan] = useState(true);
  106. const [inputs, setInputs] = useState(originInputs);
  107. const [originModelOptions, setOriginModelOptions] = useState([]);
  108. const [modelOptions, setModelOptions] = useState([]);
  109. const [groupOptions, setGroupOptions] = useState([]);
  110. const [basicModels, setBasicModels] = useState([]);
  111. const [fullModels, setFullModels] = useState([]);
  112. const [customModel, setCustomModel] = useState('');
  113. const [modalImageUrl, setModalImageUrl] = useState('');
  114. const [isModalOpenurl, setIsModalOpenurl] = useState(false);
  115. const formApiRef = useRef(null);
  116. const [vertexKeys, setVertexKeys] = useState([]);
  117. const [vertexFileList, setVertexFileList] = useState([]);
  118. const vertexErroredNames = useRef(new Set()); // 避免重复报错
  119. const [isMultiKeyChannel, setIsMultiKeyChannel] = useState(false);
  120. const [channelSearchValue, setChannelSearchValue] = useState('');
  121. const [useManualInput, setUseManualInput] = useState(false); // 是否使用手动输入模式
  122. const getInitValues = () => ({ ...originInputs });
  123. const handleInputChange = (name, value) => {
  124. if (formApiRef.current) {
  125. formApiRef.current.setValue(name, value);
  126. }
  127. if (name === 'models' && Array.isArray(value)) {
  128. value = Array.from(new Set(value.map((m) => (m || '').trim())));
  129. }
  130. if (name === 'base_url' && value.endsWith('/v1')) {
  131. Modal.confirm({
  132. title: '警告',
  133. content:
  134. '不需要在末尾加/v1,New API会自动处理,添加后可能导致请求失败,是否继续?',
  135. onOk: () => {
  136. setInputs((inputs) => ({ ...inputs, [name]: value }));
  137. },
  138. });
  139. return;
  140. }
  141. setInputs((inputs) => ({ ...inputs, [name]: value }));
  142. if (name === 'type') {
  143. let localModels = [];
  144. switch (value) {
  145. case 2:
  146. localModels = [
  147. 'mj_imagine',
  148. 'mj_variation',
  149. 'mj_reroll',
  150. 'mj_blend',
  151. 'mj_upscale',
  152. 'mj_describe',
  153. 'mj_uploads',
  154. ];
  155. break;
  156. case 5:
  157. localModels = [
  158. 'swap_face',
  159. 'mj_imagine',
  160. 'mj_video',
  161. 'mj_edits',
  162. 'mj_variation',
  163. 'mj_reroll',
  164. 'mj_blend',
  165. 'mj_upscale',
  166. 'mj_describe',
  167. 'mj_zoom',
  168. 'mj_shorten',
  169. 'mj_modal',
  170. 'mj_inpaint',
  171. 'mj_custom_zoom',
  172. 'mj_high_variation',
  173. 'mj_low_variation',
  174. 'mj_pan',
  175. 'mj_uploads',
  176. ];
  177. break;
  178. case 36:
  179. localModels = ['suno_music', 'suno_lyrics'];
  180. break;
  181. default:
  182. localModels = getChannelModels(value);
  183. break;
  184. }
  185. if (inputs.models.length === 0) {
  186. setInputs((inputs) => ({ ...inputs, models: localModels }));
  187. }
  188. setBasicModels(localModels);
  189. // 重置手动输入模式状态
  190. setUseManualInput(false);
  191. }
  192. //setAutoBan
  193. };
  194. const loadChannel = async () => {
  195. setLoading(true);
  196. let res = await API.get(`/api/channel/${channelId}`);
  197. if (res === undefined) {
  198. return;
  199. }
  200. const { success, message, data } = res.data;
  201. if (success) {
  202. if (data.models === '') {
  203. data.models = [];
  204. } else {
  205. data.models = data.models.split(',');
  206. }
  207. if (data.group === '') {
  208. data.groups = [];
  209. } else {
  210. data.groups = data.group.split(',');
  211. }
  212. if (data.model_mapping !== '') {
  213. data.model_mapping = JSON.stringify(
  214. JSON.parse(data.model_mapping),
  215. null,
  216. 2,
  217. );
  218. }
  219. const chInfo = data.channel_info || {};
  220. const isMulti = chInfo.is_multi_key === true;
  221. setIsMultiKeyChannel(isMulti);
  222. if (isMulti) {
  223. setBatch(true);
  224. setMultiToSingle(true);
  225. const modeVal = chInfo.multi_key_mode || 'random';
  226. setMultiKeyMode(modeVal);
  227. data.multi_key_mode = modeVal;
  228. } else {
  229. setBatch(false);
  230. setMultiToSingle(false);
  231. }
  232. setInputs(data);
  233. if (formApiRef.current) {
  234. formApiRef.current.setValues(data);
  235. }
  236. if (data.auto_ban === 0) {
  237. setAutoBan(false);
  238. } else {
  239. setAutoBan(true);
  240. }
  241. setBasicModels(getChannelModels(data.type));
  242. // console.log(data);
  243. } else {
  244. showError(message);
  245. }
  246. setLoading(false);
  247. };
  248. const fetchUpstreamModelList = async (name) => {
  249. // if (inputs['type'] !== 1) {
  250. // showError(t('仅支持 OpenAI 接口格式'));
  251. // return;
  252. // }
  253. setLoading(true);
  254. const models = inputs['models'] || [];
  255. let err = false;
  256. if (isEdit) {
  257. // 如果是编辑模式,使用已有的 channelId 获取模型列表
  258. const res = await API.get('/api/channel/fetch_models/' + channelId, { skipErrorHandler: true });
  259. if (res && res.data && res.data.success) {
  260. models.push(...res.data.data);
  261. } else {
  262. err = true;
  263. }
  264. } else {
  265. // 如果是新建模式,通过后端代理获取模型列表
  266. if (!inputs?.['key']) {
  267. showError(t('请填写密钥'));
  268. err = true;
  269. } else {
  270. try {
  271. const res = await API.post(
  272. '/api/channel/fetch_models',
  273. {
  274. base_url: inputs['base_url'],
  275. type: inputs['type'],
  276. key: inputs['key'],
  277. },
  278. { skipErrorHandler: true },
  279. );
  280. if (res && res.data && res.data.success) {
  281. models.push(...res.data.data);
  282. } else {
  283. err = true;
  284. }
  285. } catch (error) {
  286. console.error('Error fetching models:', error);
  287. err = true;
  288. }
  289. }
  290. }
  291. if (!err) {
  292. handleInputChange(name, Array.from(new Set(models)));
  293. showSuccess(t('获取模型列表成功'));
  294. } else {
  295. showError(t('获取模型列表失败'));
  296. }
  297. setLoading(false);
  298. };
  299. const fetchModels = async () => {
  300. try {
  301. let res = await API.get(`/api/channel/models`);
  302. const localModelOptions = res.data.data.map((model) => {
  303. const id = (model.id || '').trim();
  304. return {
  305. key: id,
  306. label: id,
  307. value: id,
  308. };
  309. });
  310. setOriginModelOptions(localModelOptions);
  311. setFullModels(res.data.data.map((model) => model.id));
  312. setBasicModels(
  313. res.data.data
  314. .filter((model) => {
  315. return model.id.startsWith('gpt-') || model.id.startsWith('text-');
  316. })
  317. .map((model) => model.id),
  318. );
  319. } catch (error) {
  320. showError(error.message);
  321. }
  322. };
  323. const fetchGroups = async () => {
  324. try {
  325. let res = await API.get(`/api/group/`);
  326. if (res === undefined) {
  327. return;
  328. }
  329. setGroupOptions(
  330. res.data.data.map((group) => ({
  331. label: group,
  332. value: group,
  333. })),
  334. );
  335. } catch (error) {
  336. showError(error.message);
  337. }
  338. };
  339. useEffect(() => {
  340. const modelMap = new Map();
  341. originModelOptions.forEach((option) => {
  342. const v = (option.value || '').trim();
  343. if (!modelMap.has(v)) {
  344. modelMap.set(v, option);
  345. }
  346. });
  347. inputs.models.forEach((model) => {
  348. const v = (model || '').trim();
  349. if (!modelMap.has(v)) {
  350. modelMap.set(v, {
  351. key: v,
  352. label: v,
  353. value: v,
  354. });
  355. }
  356. });
  357. const categories = getModelCategories(t);
  358. const optionsWithIcon = Array.from(modelMap.values()).map((opt) => {
  359. const modelName = opt.value;
  360. let icon = null;
  361. for (const [key, category] of Object.entries(categories)) {
  362. if (key !== 'all' && category.filter({ model_name: modelName })) {
  363. icon = category.icon;
  364. break;
  365. }
  366. }
  367. return {
  368. ...opt,
  369. label: (
  370. <span className="flex items-center gap-1">
  371. {icon}
  372. {modelName}
  373. </span>
  374. ),
  375. };
  376. });
  377. setModelOptions(optionsWithIcon);
  378. }, [originModelOptions, inputs.models, t]);
  379. useEffect(() => {
  380. fetchModels().then();
  381. fetchGroups().then();
  382. if (!isEdit) {
  383. setInputs(originInputs);
  384. if (formApiRef.current) {
  385. formApiRef.current.setValues(originInputs);
  386. }
  387. let localModels = getChannelModels(inputs.type);
  388. setBasicModels(localModels);
  389. setInputs((inputs) => ({ ...inputs, models: localModels }));
  390. }
  391. }, [props.editingChannel.id]);
  392. useEffect(() => {
  393. if (formApiRef.current) {
  394. formApiRef.current.setValues(inputs);
  395. }
  396. }, [inputs]);
  397. useEffect(() => {
  398. if (props.visible) {
  399. if (isEdit) {
  400. loadChannel();
  401. } else {
  402. formApiRef.current?.setValues(getInitValues());
  403. }
  404. // 重置手动输入模式状态
  405. setUseManualInput(false);
  406. } else {
  407. formApiRef.current?.reset();
  408. }
  409. }, [props.visible, channelId]);
  410. const handleVertexUploadChange = ({ fileList }) => {
  411. vertexErroredNames.current.clear();
  412. (async () => {
  413. let validFiles = [];
  414. let keys = [];
  415. const errorNames = [];
  416. for (const item of fileList) {
  417. const fileObj = item.fileInstance;
  418. if (!fileObj) continue;
  419. try {
  420. const txt = await fileObj.text();
  421. keys.push(JSON.parse(txt));
  422. validFiles.push(item);
  423. } catch (err) {
  424. if (!vertexErroredNames.current.has(item.name)) {
  425. errorNames.push(item.name);
  426. vertexErroredNames.current.add(item.name);
  427. }
  428. }
  429. }
  430. // 非批量模式下只保留一个文件(最新选择的),避免重复叠加
  431. if (!batch && validFiles.length > 1) {
  432. validFiles = [validFiles[validFiles.length - 1]];
  433. keys = [keys[keys.length - 1]];
  434. }
  435. setVertexKeys(keys);
  436. setVertexFileList(validFiles);
  437. if (formApiRef.current) {
  438. formApiRef.current.setValue('vertex_files', validFiles);
  439. }
  440. setInputs((prev) => ({ ...prev, vertex_files: validFiles }));
  441. if (errorNames.length > 0) {
  442. showError(t('以下文件解析失败,已忽略:{{list}}', { list: errorNames.join(', ') }));
  443. }
  444. })();
  445. };
  446. const submit = async () => {
  447. const formValues = formApiRef.current ? formApiRef.current.getValues() : {};
  448. let localInputs = { ...formValues };
  449. if (localInputs.type === 41) {
  450. if (useManualInput) {
  451. // 手动输入模式
  452. if (localInputs.key && localInputs.key.trim() !== '') {
  453. try {
  454. // 验证 JSON 格式
  455. const parsedKey = JSON.parse(localInputs.key);
  456. // 确保是有效的密钥格式
  457. localInputs.key = JSON.stringify(parsedKey);
  458. } catch (err) {
  459. showError(t('密钥格式无效,请输入有效的 JSON 格式密钥'));
  460. return;
  461. }
  462. } else if (!isEdit) {
  463. showInfo(t('请输入密钥!'));
  464. return;
  465. }
  466. } else {
  467. // 文件上传模式
  468. let keys = vertexKeys;
  469. // 若当前未选择文件,尝试从已上传文件列表解析(异步读取)
  470. if (keys.length === 0 && vertexFileList.length > 0) {
  471. try {
  472. const parsed = await Promise.all(
  473. vertexFileList.map(async (item) => {
  474. const fileObj = item.fileInstance;
  475. if (!fileObj) return null;
  476. const txt = await fileObj.text();
  477. return JSON.parse(txt);
  478. })
  479. );
  480. keys = parsed.filter(Boolean);
  481. } catch (err) {
  482. showError(t('解析密钥文件失败: {{msg}}', { msg: err.message }));
  483. return;
  484. }
  485. }
  486. // 创建模式必须上传密钥;编辑模式可选
  487. if (keys.length === 0) {
  488. if (!isEdit) {
  489. showInfo(t('请上传密钥文件!'));
  490. return;
  491. } else {
  492. // 编辑模式且未上传新密钥,不修改 key
  493. delete localInputs.key;
  494. }
  495. } else {
  496. // 有新密钥,则覆盖
  497. if (batch) {
  498. localInputs.key = JSON.stringify(keys);
  499. } else {
  500. localInputs.key = JSON.stringify(keys[0]);
  501. }
  502. }
  503. }
  504. }
  505. // 如果是编辑模式且 key 为空字符串,避免提交空值覆盖旧密钥
  506. if (isEdit && (!localInputs.key || localInputs.key.trim() === '')) {
  507. delete localInputs.key;
  508. }
  509. delete localInputs.vertex_files;
  510. if (!isEdit && (!localInputs.name || !localInputs.key)) {
  511. showInfo(t('请填写渠道名称和渠道密钥!'));
  512. return;
  513. }
  514. if (!Array.isArray(localInputs.models) || localInputs.models.length === 0) {
  515. showInfo(t('请至少选择一个模型!'));
  516. return;
  517. }
  518. if (localInputs.model_mapping && localInputs.model_mapping !== '' && !verifyJSON(localInputs.model_mapping)) {
  519. showInfo(t('模型映射必须是合法的 JSON 格式!'));
  520. return;
  521. }
  522. if (localInputs.base_url && localInputs.base_url.endsWith('/')) {
  523. localInputs.base_url = localInputs.base_url.slice(
  524. 0,
  525. localInputs.base_url.length - 1,
  526. );
  527. }
  528. if (localInputs.type === 18 && localInputs.other === '') {
  529. localInputs.other = 'v2.1';
  530. }
  531. let res;
  532. localInputs.auto_ban = localInputs.auto_ban ? 1 : 0;
  533. localInputs.models = localInputs.models.join(',');
  534. localInputs.group = (localInputs.groups || []).join(',');
  535. let mode = 'single';
  536. if (batch) {
  537. mode = multiToSingle ? 'multi_to_single' : 'batch';
  538. }
  539. if (isEdit) {
  540. res = await API.put(`/api/channel/`, {
  541. ...localInputs,
  542. id: parseInt(channelId),
  543. });
  544. } else {
  545. res = await API.post(`/api/channel/`, {
  546. mode: mode,
  547. multi_key_mode: mode === 'multi_to_single' ? multiKeyMode : undefined,
  548. channel: localInputs,
  549. });
  550. }
  551. const { success, message } = res.data;
  552. if (success) {
  553. if (isEdit) {
  554. showSuccess(t('渠道更新成功!'));
  555. } else {
  556. showSuccess(t('渠道创建成功!'));
  557. setInputs(originInputs);
  558. }
  559. props.refresh();
  560. props.handleClose();
  561. } else {
  562. showError(message);
  563. }
  564. };
  565. const addCustomModels = () => {
  566. if (customModel.trim() === '') return;
  567. const modelArray = customModel.split(',').map((model) => model.trim());
  568. let localModels = [...inputs.models];
  569. let localModelOptions = [...modelOptions];
  570. const addedModels = [];
  571. modelArray.forEach((model) => {
  572. if (model && !localModels.includes(model)) {
  573. localModels.push(model);
  574. localModelOptions.push({
  575. key: model,
  576. label: model,
  577. value: model,
  578. });
  579. addedModels.push(model);
  580. }
  581. });
  582. setModelOptions(localModelOptions);
  583. setCustomModel('');
  584. handleInputChange('models', localModels);
  585. if (addedModels.length > 0) {
  586. showSuccess(
  587. t('已新增 {{count}} 个模型:{{list}}', {
  588. count: addedModels.length,
  589. list: addedModels.join(', '),
  590. })
  591. );
  592. } else {
  593. showInfo(t('未发现新增模型'));
  594. }
  595. };
  596. const batchAllowed = !isEdit || isMultiKeyChannel;
  597. const batchExtra = batchAllowed ? (
  598. <Space>
  599. <Checkbox
  600. disabled={isEdit}
  601. checked={batch}
  602. onChange={(e) => {
  603. const checked = e.target.checked;
  604. if (!checked && vertexFileList.length > 1) {
  605. Modal.confirm({
  606. title: t('切换为单密钥模式'),
  607. content: t('将仅保留第一个密钥文件,其余文件将被移除,是否继续?'),
  608. onOk: () => {
  609. const firstFile = vertexFileList[0];
  610. const firstKey = vertexKeys[0] ? [vertexKeys[0]] : [];
  611. setVertexFileList([firstFile]);
  612. setVertexKeys(firstKey);
  613. formApiRef.current?.setValue('vertex_files', [firstFile]);
  614. setInputs((prev) => ({ ...prev, vertex_files: [firstFile] }));
  615. setBatch(false);
  616. setMultiToSingle(false);
  617. setMultiKeyMode('random');
  618. },
  619. onCancel: () => {
  620. setBatch(true);
  621. },
  622. centered: true,
  623. });
  624. return;
  625. }
  626. setBatch(checked);
  627. if (!checked) {
  628. setMultiToSingle(false);
  629. setMultiKeyMode('random');
  630. } else {
  631. // 批量模式下禁用手动输入,并清空手动输入的内容
  632. setUseManualInput(false);
  633. if (inputs.type === 41) {
  634. // 清空手动输入的密钥内容
  635. if (formApiRef.current) {
  636. formApiRef.current.setValue('key', '');
  637. }
  638. handleInputChange('key', '');
  639. }
  640. }
  641. }}
  642. >{t('批量创建')}</Checkbox>
  643. {/*{batch && (*/}
  644. {/* <Checkbox disabled={isEdit} checked={multiToSingle} onChange={() => {*/}
  645. {/* setMultiToSingle(prev => !prev);*/}
  646. {/* setInputs(prev => {*/}
  647. {/* const newInputs = { ...prev };*/}
  648. {/* if (!multiToSingle) {*/}
  649. {/* newInputs.multi_key_mode = multiKeyMode;*/}
  650. {/* } else {*/}
  651. {/* delete newInputs.multi_key_mode;*/}
  652. {/* }*/}
  653. {/* return newInputs;*/}
  654. {/* });*/}
  655. {/* }}>{t('密钥聚合模式')}</Checkbox>*/}
  656. {/*)}*/}
  657. </Space>
  658. ) : null;
  659. const channelOptionList = useMemo(
  660. () =>
  661. CHANNEL_OPTIONS.map((opt) => ({
  662. ...opt,
  663. // 保持 label 为纯文本以支持搜索
  664. label: opt.label,
  665. })),
  666. [],
  667. );
  668. const renderChannelOption = (renderProps) => {
  669. const {
  670. disabled,
  671. selected,
  672. label,
  673. value,
  674. focused,
  675. className,
  676. style,
  677. onMouseEnter,
  678. onClick,
  679. ...rest
  680. } = renderProps;
  681. const searchWords = channelSearchValue ? [channelSearchValue] : [];
  682. // 构建样式类名
  683. const optionClassName = [
  684. 'flex items-center gap-3 px-3 py-2 transition-all duration-200 rounded-lg mx-2 my-1',
  685. focused && 'bg-blue-50 shadow-sm',
  686. selected && 'bg-blue-100 text-blue-700 shadow-lg ring-2 ring-blue-200 ring-opacity-50',
  687. disabled && 'opacity-50 cursor-not-allowed',
  688. !disabled && 'hover:bg-gray-50 hover:shadow-md cursor-pointer',
  689. className
  690. ].filter(Boolean).join(' ');
  691. return (
  692. <div
  693. style={style}
  694. className={optionClassName}
  695. onClick={() => !disabled && onClick()}
  696. onMouseEnter={e => onMouseEnter()}
  697. >
  698. <div className="flex items-center gap-3 w-full">
  699. <div className="flex-shrink-0 w-5 h-5 flex items-center justify-center">
  700. {getChannelIcon(value)}
  701. </div>
  702. <div className="flex-1 min-w-0">
  703. <Highlight
  704. sourceString={label}
  705. searchWords={searchWords}
  706. className="text-sm font-medium truncate"
  707. />
  708. </div>
  709. {selected && (
  710. <div className="flex-shrink-0 text-blue-600">
  711. <svg width="16" height="16" viewBox="0 0 16 16" fill="currentColor">
  712. <path d="M13.78 4.22a.75.75 0 010 1.06l-7.25 7.25a.75.75 0 01-1.06 0L2.22 9.28a.75.75 0 011.06-1.06L6 10.94l6.72-6.72a.75.75 0 011.06 0z"/>
  713. </svg>
  714. </div>
  715. )}
  716. </div>
  717. </div>
  718. );
  719. };
  720. return (
  721. <>
  722. <SideSheet
  723. placement={isEdit ? 'right' : 'left'}
  724. title={
  725. <Space>
  726. <Tag color="blue" shape="circle">{isEdit ? t('编辑') : t('新建')}</Tag>
  727. <Title heading={4} className="m-0">
  728. {isEdit ? t('更新渠道信息') : t('创建新的渠道')}
  729. </Title>
  730. </Space>
  731. }
  732. bodyStyle={{ padding: '0' }}
  733. visible={props.visible}
  734. width={isMobile ? '100%' : 600}
  735. footer={
  736. <div className="flex justify-end bg-white">
  737. <Space>
  738. <Button
  739. theme="solid"
  740. onClick={() => formApiRef.current?.submitForm()}
  741. icon={<IconSave />}
  742. >
  743. {t('提交')}
  744. </Button>
  745. <Button
  746. theme="light"
  747. type="primary"
  748. onClick={handleCancel}
  749. icon={<IconClose />}
  750. >
  751. {t('取消')}
  752. </Button>
  753. </Space>
  754. </div>
  755. }
  756. closeIcon={null}
  757. onCancel={() => handleCancel()}
  758. >
  759. <Form
  760. key={isEdit ? 'edit' : 'new'}
  761. initValues={originInputs}
  762. getFormApi={(api) => (formApiRef.current = api)}
  763. onSubmit={submit}
  764. >
  765. {() => (
  766. <Spin spinning={loading}>
  767. <div className="p-2">
  768. <Card className="!rounded-2xl shadow-sm border-0 mb-6">
  769. {/* Header: Basic Info */}
  770. <div className="flex items-center mb-2">
  771. <Avatar size="small" color="blue" className="mr-2 shadow-md">
  772. <IconServer size={16} />
  773. </Avatar>
  774. <div>
  775. <Text className="text-lg font-medium">{t('基本信息')}</Text>
  776. <div className="text-xs text-gray-600">{t('渠道的基本配置信息')}</div>
  777. </div>
  778. </div>
  779. <Form.Select
  780. field='type'
  781. label={t('类型')}
  782. placeholder={t('请选择渠道类型')}
  783. rules={[{ required: true, message: t('请选择渠道类型') }]}
  784. optionList={channelOptionList}
  785. style={{ width: '100%' }}
  786. filter
  787. searchPosition='dropdown'
  788. onSearch={(value) => setChannelSearchValue(value)}
  789. renderOptionItem={renderChannelOption}
  790. onChange={(value) => handleInputChange('type', value)}
  791. />
  792. <Form.Input
  793. field='name'
  794. label={t('名称')}
  795. placeholder={t('请为渠道命名')}
  796. rules={[{ required: true, message: t('请为渠道命名') }]}
  797. showClear
  798. onChange={(value) => handleInputChange('name', value)}
  799. autoComplete='new-password'
  800. />
  801. {batch ? (
  802. inputs.type === 41 ? (
  803. <Form.Upload
  804. field='vertex_files'
  805. label={t('密钥文件 (.json)')}
  806. accept='.json'
  807. multiple
  808. draggable
  809. dragIcon={<IconBolt />}
  810. dragMainText={t('点击上传文件或拖拽文件到这里')}
  811. dragSubText={t('仅支持 JSON 文件,支持多文件')}
  812. style={{ marginTop: 10 }}
  813. uploadTrigger='custom'
  814. beforeUpload={() => false}
  815. onChange={handleVertexUploadChange}
  816. fileList={vertexFileList}
  817. rules={isEdit ? [] : [{ required: true, message: t('请上传密钥文件') }]}
  818. extraText={batchExtra}
  819. />
  820. ) : (
  821. <Form.TextArea
  822. field='key'
  823. label={t('密钥')}
  824. placeholder={t('请输入密钥,一行一个')}
  825. rules={isEdit ? [] : [{ required: true, message: t('请输入密钥') }]}
  826. autosize
  827. autoComplete='new-password'
  828. onChange={(value) => handleInputChange('key', value)}
  829. extraText={batchExtra}
  830. showClear
  831. />
  832. )
  833. ) : (
  834. <>
  835. {inputs.type === 41 ? (
  836. <>
  837. {!batch && (
  838. <div className="flex items-center justify-between mb-3">
  839. <Text className="text-sm font-medium">{t('密钥输入方式')}</Text>
  840. <Space>
  841. <Button
  842. size="small"
  843. type={!useManualInput ? 'primary' : 'tertiary'}
  844. onClick={() => {
  845. setUseManualInput(false);
  846. // 切换到文件上传模式时清空手动输入的密钥
  847. if (formApiRef.current) {
  848. formApiRef.current.setValue('key', '');
  849. }
  850. handleInputChange('key', '');
  851. }}
  852. >
  853. {t('文件上传')}
  854. </Button>
  855. <Button
  856. size="small"
  857. type={useManualInput ? 'primary' : 'tertiary'}
  858. onClick={() => {
  859. setUseManualInput(true);
  860. // 切换到手动输入模式时清空文件上传相关状态
  861. setVertexKeys([]);
  862. setVertexFileList([]);
  863. if (formApiRef.current) {
  864. formApiRef.current.setValue('vertex_files', []);
  865. }
  866. setInputs((prev) => ({ ...prev, vertex_files: [] }));
  867. }}
  868. >
  869. {t('手动输入')}
  870. </Button>
  871. </Space>
  872. </div>
  873. )}
  874. {batch && (
  875. <Banner
  876. type='info'
  877. description={t('批量创建模式下仅支持文件上传,不支持手动输入')}
  878. className='!rounded-lg mb-3'
  879. />
  880. )}
  881. {useManualInput && !batch ? (
  882. <Form.TextArea
  883. field='key'
  884. label={isEdit ? t('密钥(编辑模式下,保存的密钥不会显示)') : t('密钥')}
  885. placeholder={t('请输入 JSON 格式的密钥内容,例如:\n{\n "type": "service_account",\n "project_id": "your-project-id",\n "private_key_id": "...",\n "private_key": "...",\n "client_email": "...",\n "client_id": "...",\n "auth_uri": "...",\n "token_uri": "...",\n "auth_provider_x509_cert_url": "...",\n "client_x509_cert_url": "..."\n}')}
  886. rules={isEdit ? [] : [{ required: true, message: t('请输入密钥') }]}
  887. autoComplete='new-password'
  888. onChange={(value) => handleInputChange('key', value)}
  889. extraText={
  890. <div className="flex items-center gap-2">
  891. <Text type="tertiary" size="small">
  892. {t('请输入完整的 JSON 格式密钥内容')}
  893. </Text>
  894. {batchExtra}
  895. </div>
  896. }
  897. autosize
  898. showClear
  899. />
  900. ) : (
  901. <Form.Upload
  902. field='vertex_files'
  903. label={t('密钥文件 (.json)')}
  904. accept='.json'
  905. draggable
  906. dragIcon={<IconBolt />}
  907. dragMainText={t('点击上传文件或拖拽文件到这里')}
  908. dragSubText={t('仅支持 JSON 文件')}
  909. style={{ marginTop: 10 }}
  910. uploadTrigger='custom'
  911. beforeUpload={() => false}
  912. onChange={handleVertexUploadChange}
  913. fileList={vertexFileList}
  914. rules={isEdit ? [] : [{ required: true, message: t('请上传密钥文件') }]}
  915. extraText={batchExtra}
  916. />
  917. )}
  918. </>
  919. ) : (
  920. <Form.Input
  921. field='key'
  922. label={isEdit ? t('密钥(编辑模式下,保存的密钥不会显示)') : t('密钥')}
  923. placeholder={t(type2secretPrompt(inputs.type))}
  924. rules={isEdit ? [] : [{ required: true, message: t('请输入密钥') }]}
  925. autoComplete='new-password'
  926. onChange={(value) => handleInputChange('key', value)}
  927. extraText={batchExtra}
  928. showClear
  929. />
  930. )}
  931. </>
  932. )}
  933. {batch && multiToSingle && (
  934. <>
  935. <Form.Select
  936. field='multi_key_mode'
  937. label={t('密钥聚合模式')}
  938. placeholder={t('请选择多密钥使用策略')}
  939. optionList={[
  940. { label: t('随机'), value: 'random' },
  941. { label: t('轮询'), value: 'polling' },
  942. ]}
  943. style={{ width: '100%' }}
  944. value={inputs.multi_key_mode || 'random'}
  945. onChange={(value) => {
  946. setMultiKeyMode(value);
  947. handleInputChange('multi_key_mode', value);
  948. }}
  949. />
  950. {inputs.multi_key_mode === 'polling' && (
  951. <Banner
  952. type='warning'
  953. description={t('轮询模式必须搭配Redis和内存缓存功能使用,否则性能将大幅降低,并且无法实现轮询功能')}
  954. className='!rounded-lg mt-2'
  955. />
  956. )}
  957. </>
  958. )}
  959. {inputs.type === 18 && (
  960. <Form.Input
  961. field='other'
  962. label={t('模型版本')}
  963. placeholder={'请输入星火大模型版本,注意是接口地址中的版本号,例如:v2.1'}
  964. onChange={(value) => handleInputChange('other', value)}
  965. showClear
  966. />
  967. )}
  968. {inputs.type === 41 && (
  969. <Form.TextArea
  970. field='other'
  971. label={t('部署地区')}
  972. placeholder={t(
  973. '请输入部署地区,例如:us-central1\n支持使用模型映射格式\n{\n "default": "us-central1",\n "claude-3-5-sonnet-20240620": "europe-west1"\n}'
  974. )}
  975. autosize
  976. onChange={(value) => handleInputChange('other', value)}
  977. rules={[{ required: true, message: t('请填写部署地区') }]}
  978. extraText={
  979. <Text
  980. className="!text-semi-color-primary cursor-pointer"
  981. onClick={() => handleInputChange('other', JSON.stringify(REGION_EXAMPLE, null, 2))}
  982. >
  983. {t('填入模板')}
  984. </Text>
  985. }
  986. showClear
  987. />
  988. )}
  989. {inputs.type === 21 && (
  990. <Form.Input
  991. field='other'
  992. label={t('知识库 ID')}
  993. placeholder={'请输入知识库 ID,例如:123456'}
  994. onChange={(value) => handleInputChange('other', value)}
  995. showClear
  996. />
  997. )}
  998. {inputs.type === 39 && (
  999. <Form.Input
  1000. field='other'
  1001. label='Account ID'
  1002. placeholder={'请输入Account ID,例如:d6b5da8hk1awo8nap34ube6gh'}
  1003. onChange={(value) => handleInputChange('other', value)}
  1004. showClear
  1005. />
  1006. )}
  1007. {inputs.type === 49 && (
  1008. <Form.Input
  1009. field='other'
  1010. label={t('智能体ID')}
  1011. placeholder={'请输入智能体ID,例如:7342866812345'}
  1012. onChange={(value) => handleInputChange('other', value)}
  1013. showClear
  1014. />
  1015. )}
  1016. {inputs.type === 1 && (
  1017. <Form.Input
  1018. field='openai_organization'
  1019. label={t('组织')}
  1020. placeholder={t('请输入组织org-xxx')}
  1021. showClear
  1022. helpText={t('组织,不填则为默认组织')}
  1023. onChange={(value) => handleInputChange('openai_organization', value)}
  1024. />
  1025. )}
  1026. </Card>
  1027. {/* API Configuration Card */}
  1028. <Card className="!rounded-2xl shadow-sm border-0 mb-6">
  1029. {/* Header: API Config */}
  1030. <div className="flex items-center mb-2">
  1031. <Avatar size="small" color="green" className="mr-2 shadow-md">
  1032. <IconGlobe size={16} />
  1033. </Avatar>
  1034. <div>
  1035. <Text className="text-lg font-medium">{t('API 配置')}</Text>
  1036. <div className="text-xs text-gray-600">{t('API 地址和相关配置')}</div>
  1037. </div>
  1038. </div>
  1039. {inputs.type === 40 && (
  1040. <Banner
  1041. type='info'
  1042. description={
  1043. <div>
  1044. <Text strong>{t('邀请链接')}:</Text>
  1045. <Text
  1046. link
  1047. underline
  1048. className="ml-2 cursor-pointer"
  1049. onClick={() => window.open('https://cloud.siliconflow.cn/i/hij0YNTZ')}
  1050. >
  1051. https://cloud.siliconflow.cn/i/hij0YNTZ
  1052. </Text>
  1053. </div>
  1054. }
  1055. className='!rounded-lg'
  1056. />
  1057. )}
  1058. {inputs.type === 3 && (
  1059. <>
  1060. <Banner
  1061. type='warning'
  1062. description={t('2025年5月10日后添加的渠道,不需要再在部署的时候移除模型名称中的"."')}
  1063. className='!rounded-lg'
  1064. />
  1065. <div>
  1066. <Form.Input
  1067. field='base_url'
  1068. label='AZURE_OPENAI_ENDPOINT'
  1069. placeholder={t('请输入 AZURE_OPENAI_ENDPOINT,例如:https://docs-test-001.openai.azure.com')}
  1070. onChange={(value) => handleInputChange('base_url', value)}
  1071. showClear
  1072. />
  1073. </div>
  1074. <div>
  1075. <Form.Input
  1076. field='other'
  1077. label={t('默认 API 版本')}
  1078. placeholder={t('请输入默认 API 版本,例如:2025-04-01-preview')}
  1079. onChange={(value) => handleInputChange('other', value)}
  1080. showClear
  1081. />
  1082. </div>
  1083. </>
  1084. )}
  1085. {inputs.type === 8 && (
  1086. <>
  1087. <Banner
  1088. type='warning'
  1089. description={t('如果你对接的是上游One API或者New API等转发项目,请使用OpenAI类型,不要使用此类型,除非你知道你在做什么。')}
  1090. className='!rounded-lg'
  1091. />
  1092. <div>
  1093. <Form.Input
  1094. field='base_url'
  1095. label={t('完整的 Base URL,支持变量{model}')}
  1096. placeholder={t('请输入完整的URL,例如:https://api.openai.com/v1/chat/completions')}
  1097. onChange={(value) => handleInputChange('base_url', value)}
  1098. showClear
  1099. />
  1100. </div>
  1101. </>
  1102. )}
  1103. {inputs.type === 37 && (
  1104. <Banner
  1105. type='warning'
  1106. description={t('Dify渠道只适配chatflow和agent,并且agent不支持图片!')}
  1107. className='!rounded-lg'
  1108. />
  1109. )}
  1110. {inputs.type !== 3 && inputs.type !== 8 && inputs.type !== 22 && inputs.type !== 36 && inputs.type !== 45 && (
  1111. <div>
  1112. <Form.Input
  1113. field='base_url'
  1114. label={t('API地址')}
  1115. placeholder={t('此项可选,用于通过自定义API地址来进行 API 调用,末尾不要带/v1和/')}
  1116. onChange={(value) => handleInputChange('base_url', value)}
  1117. showClear
  1118. extraText={t('对于官方渠道,new-api已经内置地址,除非是第三方代理站点或者Azure的特殊接入地址,否则不需要填写')}
  1119. />
  1120. </div>
  1121. )}
  1122. {inputs.type === 22 && (
  1123. <div>
  1124. <Form.Input
  1125. field='base_url'
  1126. label={t('私有部署地址')}
  1127. placeholder={t('请输入私有部署地址,格式为:https://fastgpt.run/api/openapi')}
  1128. onChange={(value) => handleInputChange('base_url', value)}
  1129. showClear
  1130. />
  1131. </div>
  1132. )}
  1133. {inputs.type === 36 && (
  1134. <div>
  1135. <Form.Input
  1136. field='base_url'
  1137. label={t('注意非Chat API,请务必填写正确的API地址,否则可能导致无法使用')}
  1138. placeholder={t('请输入到 /suno 前的路径,通常就是域名,例如:https://api.example.com')}
  1139. onChange={(value) => handleInputChange('base_url', value)}
  1140. showClear
  1141. />
  1142. </div>
  1143. )}
  1144. </Card>
  1145. {/* Model Configuration Card */}
  1146. <Card className="!rounded-2xl shadow-sm border-0 mb-6">
  1147. {/* Header: Model Config */}
  1148. <div className="flex items-center mb-2">
  1149. <Avatar size="small" color="purple" className="mr-2 shadow-md">
  1150. <IconCode size={16} />
  1151. </Avatar>
  1152. <div>
  1153. <Text className="text-lg font-medium">{t('模型配置')}</Text>
  1154. <div className="text-xs text-gray-600">{t('模型选择和映射设置')}</div>
  1155. </div>
  1156. </div>
  1157. <Form.Select
  1158. field='models'
  1159. label={t('模型')}
  1160. placeholder={t('请选择该渠道所支持的模型')}
  1161. rules={[{ required: true, message: t('请选择模型') }]}
  1162. multiple
  1163. filter
  1164. searchPosition='dropdown'
  1165. optionList={modelOptions}
  1166. style={{ width: '100%' }}
  1167. onChange={(value) => handleInputChange('models', value)}
  1168. extraText={(
  1169. <Space wrap>
  1170. <Button size='small' type='primary' onClick={() => handleInputChange('models', basicModels)}>
  1171. {t('填入相关模型')}
  1172. </Button>
  1173. <Button size='small' type='secondary' onClick={() => handleInputChange('models', fullModels)}>
  1174. {t('填入所有模型')}
  1175. </Button>
  1176. <Button size='small' type='tertiary' onClick={() => fetchUpstreamModelList('models')}>
  1177. {t('获取模型列表')}
  1178. </Button>
  1179. <Button size='small' type='warning' onClick={() => handleInputChange('models', [])}>
  1180. {t('清除所有模型')}
  1181. </Button>
  1182. <Button
  1183. size='small'
  1184. type='tertiary'
  1185. onClick={() => {
  1186. if (inputs.models.length === 0) {
  1187. showInfo(t('没有模型可以复制'));
  1188. return;
  1189. }
  1190. try {
  1191. copy(inputs.models.join(','));
  1192. showSuccess(t('模型列表已复制到剪贴板'));
  1193. } catch (error) {
  1194. showError(t('复制失败'));
  1195. }
  1196. }}
  1197. >
  1198. {t('复制所有模型')}
  1199. </Button>
  1200. </Space>
  1201. )}
  1202. />
  1203. <Form.Input
  1204. field='custom_model'
  1205. label={t('自定义模型名称')}
  1206. placeholder={t('输入自定义模型名称')}
  1207. onChange={(value) => setCustomModel(value.trim())}
  1208. value={customModel}
  1209. suffix={
  1210. <Button size='small' type='primary' onClick={addCustomModels}>
  1211. {t('填入')}
  1212. </Button>
  1213. }
  1214. />
  1215. <Form.Input
  1216. field='test_model'
  1217. label={t('默认测试模型')}
  1218. placeholder={t('不填则为模型列表第一个')}
  1219. onChange={(value) => handleInputChange('test_model', value)}
  1220. showClear
  1221. />
  1222. <Form.TextArea
  1223. field='model_mapping'
  1224. label={t('模型重定向')}
  1225. placeholder={
  1226. t('此项可选,用于修改请求体中的模型名称,为一个 JSON 字符串,键为请求中模型名称,值为要替换的模型名称,例如:') +
  1227. `\n${JSON.stringify(MODEL_MAPPING_EXAMPLE, null, 2)}`
  1228. }
  1229. autosize
  1230. onChange={(value) => handleInputChange('model_mapping', value)}
  1231. extraText={
  1232. <Text
  1233. className="!text-semi-color-primary cursor-pointer"
  1234. onClick={() => handleInputChange('model_mapping', JSON.stringify(MODEL_MAPPING_EXAMPLE, null, 2))}
  1235. >
  1236. {t('填入模板')}
  1237. </Text>
  1238. }
  1239. showClear
  1240. />
  1241. </Card>
  1242. {/* Advanced Settings Card */}
  1243. <Card className="!rounded-2xl shadow-sm border-0 mb-6">
  1244. {/* Header: Advanced Settings */}
  1245. <div className="flex items-center mb-2">
  1246. <Avatar size="small" color="orange" className="mr-2 shadow-md">
  1247. <IconSetting size={16} />
  1248. </Avatar>
  1249. <div>
  1250. <Text className="text-lg font-medium">{t('高级设置')}</Text>
  1251. <div className="text-xs text-gray-600">{t('渠道的高级配置选项')}</div>
  1252. </div>
  1253. </div>
  1254. <Form.Select
  1255. field='groups'
  1256. label={t('分组')}
  1257. placeholder={t('请选择可以使用该渠道的分组')}
  1258. multiple
  1259. allowAdditions
  1260. additionLabel={t('请在系统设置页面编辑分组倍率以添加新的分组:')}
  1261. optionList={groupOptions}
  1262. style={{ width: '100%' }}
  1263. onChange={(value) => handleInputChange('groups', value)}
  1264. />
  1265. <Form.Input
  1266. field='tag'
  1267. label={t('渠道标签')}
  1268. placeholder={t('渠道标签')}
  1269. showClear
  1270. onChange={(value) => handleInputChange('tag', value)}
  1271. />
  1272. <Row gutter={12}>
  1273. <Col span={12}>
  1274. <Form.InputNumber
  1275. field='priority'
  1276. label={t('渠道优先级')}
  1277. placeholder={t('渠道优先级')}
  1278. min={0}
  1279. onNumberChange={(value) => handleInputChange('priority', value)}
  1280. style={{ width: '100%' }}
  1281. />
  1282. </Col>
  1283. <Col span={12}>
  1284. <Form.InputNumber
  1285. field='weight'
  1286. label={t('渠道权重')}
  1287. placeholder={t('渠道权重')}
  1288. min={0}
  1289. onNumberChange={(value) => handleInputChange('weight', value)}
  1290. style={{ width: '100%' }}
  1291. />
  1292. </Col>
  1293. </Row>
  1294. <Form.Switch
  1295. field='auto_ban'
  1296. label={t('是否自动禁用')}
  1297. checkedText={t('开')}
  1298. uncheckedText={t('关')}
  1299. onChange={(val) => setAutoBan(val)}
  1300. extraText={t('仅当自动禁用开启时有效,关闭后不会自动禁用该渠道')}
  1301. initValue={autoBan}
  1302. />
  1303. <Form.TextArea
  1304. field='param_override'
  1305. label={t('参数覆盖')}
  1306. placeholder={
  1307. t('此项可选,用于覆盖请求参数。不支持覆盖 stream 参数。为一个 JSON 字符串,例如:') +
  1308. '\n{\n "temperature": 0\n}'
  1309. }
  1310. autosize
  1311. onChange={(value) => handleInputChange('param_override', value)}
  1312. extraText={
  1313. <Text
  1314. className="!text-semi-color-primary cursor-pointer"
  1315. onClick={() => handleInputChange('param_override', JSON.stringify({ temperature: 0 }, null, 2))}
  1316. >
  1317. {t('填入模板')}
  1318. </Text>
  1319. }
  1320. showClear
  1321. />
  1322. <Form.TextArea
  1323. field='status_code_mapping'
  1324. label={t('状态码复写')}
  1325. placeholder={
  1326. t('此项可选,用于复写返回的状态码,仅影响本地判断,不修改返回到上游的状态码,比如将claude渠道的400错误复写为500(用于重试),请勿滥用该功能,例如:') +
  1327. '\n' +
  1328. JSON.stringify(STATUS_CODE_MAPPING_EXAMPLE, null, 2)
  1329. }
  1330. autosize
  1331. onChange={(value) => handleInputChange('status_code_mapping', value)}
  1332. extraText={
  1333. <Text
  1334. className="!text-semi-color-primary cursor-pointer"
  1335. onClick={() => handleInputChange('status_code_mapping', JSON.stringify(STATUS_CODE_MAPPING_EXAMPLE, null, 2))}
  1336. >
  1337. {t('填入模板')}
  1338. </Text>
  1339. }
  1340. showClear
  1341. />
  1342. <Form.TextArea
  1343. field='setting'
  1344. label={t('渠道额外设置')}
  1345. placeholder={
  1346. t('此项可选,用于配置渠道特定设置,为一个 JSON 字符串,例如:') +
  1347. '\n{\n "force_format": true\n}'
  1348. }
  1349. autosize
  1350. onChange={(value) => handleInputChange('setting', value)}
  1351. extraText={(
  1352. <Space wrap>
  1353. <Text
  1354. className="!text-semi-color-primary cursor-pointer"
  1355. onClick={() => handleInputChange('setting', JSON.stringify({ force_format: true }, null, 2))}
  1356. >
  1357. {t('填入模板')}
  1358. </Text>
  1359. <Text
  1360. className="!text-semi-color-primary cursor-pointer"
  1361. onClick={() => window.open('https://github.com/QuantumNous/new-api/blob/main/docs/channel/other_setting.md')}
  1362. >
  1363. {t('设置说明')}
  1364. </Text>
  1365. </Space>
  1366. )}
  1367. showClear
  1368. />
  1369. </Card>
  1370. </div>
  1371. </Spin>
  1372. )}
  1373. </Form>
  1374. <ImagePreview
  1375. src={modalImageUrl}
  1376. visible={isModalOpenurl}
  1377. onVisibleChange={(visible) => setIsModalOpenurl(visible)}
  1378. />
  1379. </SideSheet>
  1380. </>
  1381. );
  1382. };
  1383. export default EditChannel;