EditChannelModal.jsx 88 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279128012811282128312841285128612871288128912901291129212931294129512961297129812991300130113021303130413051306130713081309131013111312131313141315131613171318131913201321132213231324132513261327132813291330133113321333133413351336133713381339134013411342134313441345134613471348134913501351135213531354135513561357135813591360136113621363136413651366136713681369137013711372137313741375137613771378137913801381138213831384138513861387138813891390139113921393139413951396139713981399140014011402140314041405140614071408140914101411141214131414141514161417141814191420142114221423142414251426142714281429143014311432143314341435143614371438143914401441144214431444144514461447144814491450145114521453145414551456145714581459146014611462146314641465146614671468146914701471147214731474147514761477147814791480148114821483148414851486148714881489149014911492149314941495149614971498149915001501150215031504150515061507150815091510151115121513151415151516151715181519152015211522152315241525152615271528152915301531153215331534153515361537153815391540154115421543154415451546154715481549155015511552155315541555155615571558155915601561156215631564156515661567156815691570157115721573157415751576157715781579158015811582158315841585158615871588158915901591159215931594159515961597159815991600160116021603160416051606160716081609161016111612161316141615161616171618161916201621162216231624162516261627162816291630163116321633163416351636163716381639164016411642164316441645164616471648164916501651165216531654165516561657165816591660166116621663166416651666166716681669167016711672167316741675167616771678167916801681168216831684168516861687168816891690169116921693169416951696169716981699170017011702170317041705170617071708170917101711171217131714171517161717171817191720172117221723172417251726172717281729173017311732173317341735173617371738173917401741174217431744174517461747174817491750175117521753175417551756175717581759176017611762176317641765176617671768176917701771177217731774177517761777177817791780178117821783178417851786178717881789179017911792179317941795179617971798179918001801180218031804180518061807180818091810181118121813181418151816181718181819182018211822182318241825182618271828182918301831183218331834183518361837183818391840184118421843184418451846184718481849185018511852185318541855185618571858185918601861186218631864186518661867186818691870187118721873187418751876187718781879188018811882188318841885188618871888188918901891189218931894189518961897189818991900190119021903190419051906190719081909191019111912191319141915191619171918191919201921192219231924192519261927192819291930193119321933193419351936193719381939194019411942194319441945194619471948194919501951195219531954195519561957195819591960196119621963196419651966196719681969197019711972197319741975197619771978197919801981198219831984198519861987198819891990199119921993199419951996199719981999200020012002200320042005200620072008200920102011201220132014201520162017201820192020202120222023202420252026202720282029203020312032203320342035203620372038203920402041204220432044204520462047204820492050205120522053205420552056205720582059206020612062206320642065206620672068206920702071207220732074207520762077207820792080208120822083208420852086208720882089209020912092209320942095209620972098209921002101210221032104210521062107210821092110211121122113211421152116211721182119212021212122212321242125212621272128212921302131213221332134213521362137213821392140214121422143214421452146214721482149215021512152215321542155215621572158215921602161216221632164216521662167216821692170217121722173217421752176217721782179218021812182218321842185218621872188218921902191219221932194219521962197219821992200220122022203220422052206220722082209221022112212221322142215221622172218221922202221222222232224222522262227222822292230223122322233223422352236223722382239224022412242224322442245224622472248224922502251225222532254225522562257225822592260226122622263226422652266226722682269227022712272227322742275227622772278227922802281228222832284228522862287228822892290229122922293229422952296229722982299230023012302230323042305230623072308230923102311231223132314231523162317231823192320232123222323232423252326232723282329233023312332233323342335233623372338233923402341234223432344234523462347234823492350235123522353235423552356235723582359236023612362236323642365236623672368236923702371237223732374237523762377237823792380238123822383238423852386238723882389239023912392239323942395239623972398239924002401240224032404240524062407240824092410241124122413241424152416
  1. /*
  2. Copyright (C) 2025 QuantumNous
  3. This program is free software: you can redistribute it and/or modify
  4. it under the terms of the GNU Affero General Public License as
  5. published by the Free Software Foundation, either version 3 of the
  6. License, or (at your option) any later version.
  7. This program is distributed in the hope that it will be useful,
  8. but WITHOUT ANY WARRANTY; without even the implied warranty of
  9. MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  10. GNU Affero General Public License for more details.
  11. You should have received a copy of the GNU Affero General Public License
  12. along with this program. If not, see <https://www.gnu.org/licenses/>.
  13. For commercial licensing, please contact support@quantumnous.com
  14. */
  15. import React, { useEffect, useState, useRef, useMemo } from 'react';
  16. import { useTranslation } from 'react-i18next';
  17. import {
  18. API,
  19. showError,
  20. showInfo,
  21. showSuccess,
  22. verifyJSON,
  23. } from '../../../../helpers';
  24. import { useIsMobile } from '../../../../hooks/common/useIsMobile';
  25. import { CHANNEL_OPTIONS } from '../../../../constants';
  26. import {
  27. SideSheet,
  28. Space,
  29. Spin,
  30. Button,
  31. Typography,
  32. Checkbox,
  33. Banner,
  34. Modal,
  35. ImagePreview,
  36. Card,
  37. Tag,
  38. Avatar,
  39. Form,
  40. Row,
  41. Col,
  42. Highlight,
  43. Input,
  44. } from '@douyinfe/semi-ui';
  45. import {
  46. getChannelModels,
  47. copy,
  48. getChannelIcon,
  49. getModelCategories,
  50. selectFilter,
  51. } from '../../../../helpers';
  52. import ModelSelectModal from './ModelSelectModal';
  53. import JSONEditor from '../../../common/ui/JSONEditor';
  54. import TwoFactorAuthModal from '../../../common/modals/TwoFactorAuthModal';
  55. import ChannelKeyDisplay from '../../../common/ui/ChannelKeyDisplay';
  56. import {
  57. IconSave,
  58. IconClose,
  59. IconServer,
  60. IconSetting,
  61. IconCode,
  62. IconGlobe,
  63. IconBolt,
  64. } from '@douyinfe/semi-icons';
  65. const { Text, Title } = Typography;
  66. const MODEL_MAPPING_EXAMPLE = {
  67. 'gpt-3.5-turbo': 'gpt-3.5-turbo-0125',
  68. };
  69. const STATUS_CODE_MAPPING_EXAMPLE = {
  70. 400: '500',
  71. };
  72. const REGION_EXAMPLE = {
  73. default: 'global',
  74. 'gemini-1.5-pro-002': 'europe-west2',
  75. 'gemini-1.5-flash-002': 'europe-west2',
  76. 'claude-3-5-sonnet-20240620': 'europe-west1',
  77. };
  78. function type2secretPrompt(type) {
  79. // inputs.type === 15 ? '按照如下格式输入:APIKey|SecretKey' : (inputs.type === 18 ? '按照如下格式输入:APPID|APISecret|APIKey' : '请输入渠道对应的鉴权密钥')
  80. switch (type) {
  81. case 15:
  82. return '按照如下格式输入:APIKey|SecretKey';
  83. case 18:
  84. return '按照如下格式输入:APPID|APISecret|APIKey';
  85. case 22:
  86. return '按照如下格式输入:APIKey-AppId,例如:fastgpt-0sp2gtvfdgyi4k30jwlgwf1i-64f335d84283f05518e9e041';
  87. case 23:
  88. return '按照如下格式输入:AppId|SecretId|SecretKey';
  89. case 33:
  90. return '按照如下格式输入:Ak|Sk|Region';
  91. case 50:
  92. return '按照如下格式输入: AccessKey|SecretKey, 如果上游是New API,则直接输ApiKey';
  93. case 51:
  94. return '按照如下格式输入: Access Key ID|Secret Access Key';
  95. default:
  96. return '请输入渠道对应的鉴权密钥';
  97. }
  98. }
  99. const EditChannelModal = (props) => {
  100. const { t } = useTranslation();
  101. const channelId = props.editingChannel.id;
  102. const isEdit = channelId !== undefined;
  103. const [loading, setLoading] = useState(isEdit);
  104. const isMobile = useIsMobile();
  105. const handleCancel = () => {
  106. props.handleClose();
  107. };
  108. const originInputs = {
  109. name: '',
  110. type: 1,
  111. key: '',
  112. openai_organization: '',
  113. max_input_tokens: 0,
  114. base_url: '',
  115. other: '',
  116. model_mapping: '',
  117. status_code_mapping: '',
  118. models: [],
  119. auto_ban: 1,
  120. test_model: '',
  121. groups: ['default'],
  122. priority: 0,
  123. weight: 0,
  124. tag: '',
  125. multi_key_mode: 'random',
  126. // 渠道额外设置的默认值
  127. force_format: false,
  128. thinking_to_content: false,
  129. proxy: '',
  130. pass_through_body_enabled: false,
  131. system_prompt: '',
  132. system_prompt_override: false,
  133. settings: '',
  134. // 仅 Vertex: 密钥格式(存入 settings.vertex_key_type)
  135. vertex_key_type: 'json',
  136. };
  137. const [batch, setBatch] = useState(false);
  138. const [multiToSingle, setMultiToSingle] = useState(false);
  139. const [multiKeyMode, setMultiKeyMode] = useState('random');
  140. const [autoBan, setAutoBan] = useState(true);
  141. const [inputs, setInputs] = useState(originInputs);
  142. const [originModelOptions, setOriginModelOptions] = useState([]);
  143. const [modelOptions, setModelOptions] = useState([]);
  144. const [groupOptions, setGroupOptions] = useState([]);
  145. const [basicModels, setBasicModels] = useState([]);
  146. const [fullModels, setFullModels] = useState([]);
  147. const [modelGroups, setModelGroups] = useState([]);
  148. const [customModel, setCustomModel] = useState('');
  149. const [modalImageUrl, setModalImageUrl] = useState('');
  150. const [isModalOpenurl, setIsModalOpenurl] = useState(false);
  151. const [modelModalVisible, setModelModalVisible] = useState(false);
  152. const [fetchedModels, setFetchedModels] = useState([]);
  153. const formApiRef = useRef(null);
  154. const [vertexKeys, setVertexKeys] = useState([]);
  155. const [vertexFileList, setVertexFileList] = useState([]);
  156. const vertexErroredNames = useRef(new Set()); // 避免重复报错
  157. const [isMultiKeyChannel, setIsMultiKeyChannel] = useState(false);
  158. const [channelSearchValue, setChannelSearchValue] = useState('');
  159. const [useManualInput, setUseManualInput] = useState(false); // 是否使用手动输入模式
  160. const [keyMode, setKeyMode] = useState('append'); // 密钥模式:replace(覆盖)或 append(追加)
  161. // 2FA验证查看密钥相关状态
  162. const [twoFAState, setTwoFAState] = useState({
  163. showModal: false,
  164. code: '',
  165. loading: false,
  166. showKey: false,
  167. keyData: '',
  168. });
  169. // 专门的2FA验证状态(用于TwoFactorAuthModal)
  170. const [show2FAVerifyModal, setShow2FAVerifyModal] = useState(false);
  171. const [verifyCode, setVerifyCode] = useState('');
  172. const [verifyLoading, setVerifyLoading] = useState(false);
  173. // 2FA状态更新辅助函数
  174. const updateTwoFAState = (updates) => {
  175. setTwoFAState((prev) => ({ ...prev, ...updates }));
  176. };
  177. // 重置2FA状态
  178. const resetTwoFAState = () => {
  179. setTwoFAState({
  180. showModal: false,
  181. code: '',
  182. loading: false,
  183. showKey: false,
  184. keyData: '',
  185. });
  186. };
  187. // 重置2FA验证状态
  188. const reset2FAVerifyState = () => {
  189. setShow2FAVerifyModal(false);
  190. setVerifyCode('');
  191. setVerifyLoading(false);
  192. };
  193. // 渠道额外设置状态
  194. const [channelSettings, setChannelSettings] = useState({
  195. force_format: false,
  196. thinking_to_content: false,
  197. proxy: '',
  198. pass_through_body_enabled: false,
  199. system_prompt: '',
  200. });
  201. const showApiConfigCard = true; // 控制是否显示 API 配置卡片
  202. const getInitValues = () => ({ ...originInputs });
  203. // 处理渠道额外设置的更新
  204. const handleChannelSettingsChange = (key, value) => {
  205. // 更新内部状态
  206. setChannelSettings((prev) => ({ ...prev, [key]: value }));
  207. // 同步更新到表单字段
  208. if (formApiRef.current) {
  209. formApiRef.current.setValue(key, value);
  210. }
  211. // 同步更新inputs状态
  212. setInputs((prev) => ({ ...prev, [key]: value }));
  213. // 生成setting JSON并更新
  214. const newSettings = { ...channelSettings, [key]: value };
  215. const settingsJson = JSON.stringify(newSettings);
  216. handleInputChange('setting', settingsJson);
  217. };
  218. const handleChannelOtherSettingsChange = (key, value) => {
  219. // 更新内部状态
  220. setChannelSettings((prev) => ({ ...prev, [key]: value }));
  221. // 同步更新到表单字段
  222. if (formApiRef.current) {
  223. formApiRef.current.setValue(key, value);
  224. }
  225. // 同步更新inputs状态
  226. setInputs((prev) => ({ ...prev, [key]: value }));
  227. // 需要更新settings,是一个json,例如{"azure_responses_version": "preview"}
  228. let settings = {};
  229. if (inputs.settings) {
  230. try {
  231. settings = JSON.parse(inputs.settings);
  232. } catch (error) {
  233. console.error('解析设置失败:', error);
  234. }
  235. }
  236. settings[key] = value;
  237. const settingsJson = JSON.stringify(settings);
  238. handleInputChange('settings', settingsJson);
  239. };
  240. const handleInputChange = (name, value) => {
  241. if (formApiRef.current) {
  242. formApiRef.current.setValue(name, value);
  243. }
  244. if (name === 'models' && Array.isArray(value)) {
  245. value = Array.from(new Set(value.map((m) => (m || '').trim())));
  246. }
  247. if (name === 'base_url' && value.endsWith('/v1')) {
  248. Modal.confirm({
  249. title: '警告',
  250. content:
  251. '不需要在末尾加/v1,New API会自动处理,添加后可能导致请求失败,是否继续?',
  252. onOk: () => {
  253. setInputs((inputs) => ({ ...inputs, [name]: value }));
  254. },
  255. });
  256. return;
  257. }
  258. setInputs((inputs) => ({ ...inputs, [name]: value }));
  259. if (name === 'type') {
  260. let localModels = [];
  261. switch (value) {
  262. case 2:
  263. localModels = [
  264. 'mj_imagine',
  265. 'mj_variation',
  266. 'mj_reroll',
  267. 'mj_blend',
  268. 'mj_upscale',
  269. 'mj_describe',
  270. 'mj_uploads',
  271. ];
  272. break;
  273. case 5:
  274. localModels = [
  275. 'swap_face',
  276. 'mj_imagine',
  277. 'mj_video',
  278. 'mj_edits',
  279. 'mj_variation',
  280. 'mj_reroll',
  281. 'mj_blend',
  282. 'mj_upscale',
  283. 'mj_describe',
  284. 'mj_zoom',
  285. 'mj_shorten',
  286. 'mj_modal',
  287. 'mj_inpaint',
  288. 'mj_custom_zoom',
  289. 'mj_high_variation',
  290. 'mj_low_variation',
  291. 'mj_pan',
  292. 'mj_uploads',
  293. ];
  294. break;
  295. case 36:
  296. localModels = ['suno_music', 'suno_lyrics'];
  297. break;
  298. default:
  299. localModels = getChannelModels(value);
  300. break;
  301. }
  302. if (inputs.models.length === 0) {
  303. setInputs((inputs) => ({ ...inputs, models: localModels }));
  304. }
  305. setBasicModels(localModels);
  306. // 重置手动输入模式状态
  307. setUseManualInput(false);
  308. }
  309. //setAutoBan
  310. };
  311. const loadChannel = async () => {
  312. setLoading(true);
  313. let res = await API.get(`/api/channel/${channelId}`);
  314. if (res === undefined) {
  315. return;
  316. }
  317. const { success, message, data } = res.data;
  318. if (success) {
  319. if (data.models === '') {
  320. data.models = [];
  321. } else {
  322. data.models = data.models.split(',');
  323. }
  324. if (data.group === '') {
  325. data.groups = [];
  326. } else {
  327. data.groups = data.group.split(',');
  328. }
  329. if (data.model_mapping !== '') {
  330. data.model_mapping = JSON.stringify(
  331. JSON.parse(data.model_mapping),
  332. null,
  333. 2,
  334. );
  335. }
  336. const chInfo = data.channel_info || {};
  337. const isMulti = chInfo.is_multi_key === true;
  338. setIsMultiKeyChannel(isMulti);
  339. if (isMulti) {
  340. setBatch(true);
  341. setMultiToSingle(true);
  342. const modeVal = chInfo.multi_key_mode || 'random';
  343. setMultiKeyMode(modeVal);
  344. data.multi_key_mode = modeVal;
  345. } else {
  346. setBatch(false);
  347. setMultiToSingle(false);
  348. }
  349. // 解析渠道额外设置并合并到data中
  350. if (data.setting) {
  351. try {
  352. const parsedSettings = JSON.parse(data.setting);
  353. data.force_format = parsedSettings.force_format || false;
  354. data.thinking_to_content =
  355. parsedSettings.thinking_to_content || false;
  356. data.proxy = parsedSettings.proxy || '';
  357. data.pass_through_body_enabled =
  358. parsedSettings.pass_through_body_enabled || false;
  359. data.system_prompt = parsedSettings.system_prompt || '';
  360. data.system_prompt_override =
  361. parsedSettings.system_prompt_override || false;
  362. } catch (error) {
  363. console.error('解析渠道设置失败:', error);
  364. data.force_format = false;
  365. data.thinking_to_content = false;
  366. data.proxy = '';
  367. data.pass_through_body_enabled = false;
  368. data.system_prompt = '';
  369. data.system_prompt_override = false;
  370. }
  371. } else {
  372. data.force_format = false;
  373. data.thinking_to_content = false;
  374. data.proxy = '';
  375. data.pass_through_body_enabled = false;
  376. data.system_prompt = '';
  377. data.system_prompt_override = false;
  378. }
  379. if (data.settings) {
  380. try {
  381. const parsedSettings = JSON.parse(data.settings);
  382. data.azure_responses_version =
  383. parsedSettings.azure_responses_version || '';
  384. // 读取 Vertex 密钥格式
  385. data.vertex_key_type = parsedSettings.vertex_key_type || 'json';
  386. } catch (error) {
  387. console.error('解析其他设置失败:', error);
  388. data.azure_responses_version = '';
  389. data.region = '';
  390. data.vertex_key_type = 'json';
  391. }
  392. } else {
  393. // 兼容历史数据:老渠道没有 settings 时,默认按 json 展示
  394. data.vertex_key_type = 'json';
  395. }
  396. setInputs(data);
  397. if (formApiRef.current) {
  398. formApiRef.current.setValues(data);
  399. }
  400. if (data.auto_ban === 0) {
  401. setAutoBan(false);
  402. } else {
  403. setAutoBan(true);
  404. }
  405. setBasicModels(getChannelModels(data.type));
  406. // 同步更新channelSettings状态显示
  407. setChannelSettings({
  408. force_format: data.force_format,
  409. thinking_to_content: data.thinking_to_content,
  410. proxy: data.proxy,
  411. pass_through_body_enabled: data.pass_through_body_enabled,
  412. system_prompt: data.system_prompt,
  413. system_prompt_override: data.system_prompt_override || false,
  414. });
  415. // console.log(data);
  416. } else {
  417. showError(message);
  418. }
  419. setLoading(false);
  420. };
  421. const fetchUpstreamModelList = async (name) => {
  422. // if (inputs['type'] !== 1) {
  423. // showError(t('仅支持 OpenAI 接口格式'));
  424. // return;
  425. // }
  426. setLoading(true);
  427. const models = [];
  428. let err = false;
  429. if (isEdit) {
  430. // 如果是编辑模式,使用已有的 channelId 获取模型列表
  431. const res = await API.get('/api/channel/fetch_models/' + channelId, {
  432. skipErrorHandler: true,
  433. });
  434. if (res && res.data && res.data.success) {
  435. models.push(...res.data.data);
  436. } else {
  437. err = true;
  438. }
  439. } else {
  440. // 如果是新建模式,通过后端代理获取模型列表
  441. if (!inputs?.['key']) {
  442. showError(t('请填写密钥'));
  443. err = true;
  444. } else {
  445. try {
  446. const res = await API.post(
  447. '/api/channel/fetch_models',
  448. {
  449. base_url: inputs['base_url'],
  450. type: inputs['type'],
  451. key: inputs['key'],
  452. },
  453. { skipErrorHandler: true },
  454. );
  455. if (res && res.data && res.data.success) {
  456. models.push(...res.data.data);
  457. } else {
  458. err = true;
  459. }
  460. } catch (error) {
  461. console.error('Error fetching models:', error);
  462. err = true;
  463. }
  464. }
  465. }
  466. if (!err) {
  467. const uniqueModels = Array.from(new Set(models));
  468. setFetchedModels(uniqueModels);
  469. setModelModalVisible(true);
  470. } else {
  471. showError(t('获取模型列表失败'));
  472. }
  473. setLoading(false);
  474. };
  475. const fetchModels = async () => {
  476. try {
  477. let res = await API.get(`/api/channel/models`);
  478. const localModelOptions = res.data.data.map((model) => {
  479. const id = (model.id || '').trim();
  480. return {
  481. key: id,
  482. label: id,
  483. value: id,
  484. };
  485. });
  486. setOriginModelOptions(localModelOptions);
  487. setFullModels(res.data.data.map((model) => model.id));
  488. setBasicModels(
  489. res.data.data
  490. .filter((model) => {
  491. return model.id.startsWith('gpt-') || model.id.startsWith('text-');
  492. })
  493. .map((model) => model.id),
  494. );
  495. } catch (error) {
  496. showError(error.message);
  497. }
  498. };
  499. const fetchGroups = async () => {
  500. try {
  501. let res = await API.get(`/api/group/`);
  502. if (res === undefined) {
  503. return;
  504. }
  505. setGroupOptions(
  506. res.data.data.map((group) => ({
  507. label: group,
  508. value: group,
  509. })),
  510. );
  511. } catch (error) {
  512. showError(error.message);
  513. }
  514. };
  515. const fetchModelGroups = async () => {
  516. try {
  517. const res = await API.get('/api/prefill_group?type=model');
  518. if (res?.data?.success) {
  519. setModelGroups(res.data.data || []);
  520. }
  521. } catch (error) {
  522. // ignore
  523. }
  524. };
  525. // 使用TwoFactorAuthModal的验证函数
  526. const handleVerify2FA = async () => {
  527. if (!verifyCode) {
  528. showError(t('请输入验证码或备用码'));
  529. return;
  530. }
  531. setVerifyLoading(true);
  532. try {
  533. const res = await API.post(`/api/channel/${channelId}/key`, {
  534. code: verifyCode,
  535. });
  536. if (res.data.success) {
  537. // 验证成功,显示密钥
  538. updateTwoFAState({
  539. showModal: true,
  540. showKey: true,
  541. keyData: res.data.data.key,
  542. });
  543. reset2FAVerifyState();
  544. showSuccess(t('验证成功'));
  545. } else {
  546. showError(res.data.message);
  547. }
  548. } catch (error) {
  549. showError(t('获取密钥失败'));
  550. } finally {
  551. setVerifyLoading(false);
  552. }
  553. };
  554. // 显示2FA验证模态框 - 使用TwoFactorAuthModal
  555. const handleShow2FAModal = () => {
  556. setShow2FAVerifyModal(true);
  557. };
  558. useEffect(() => {
  559. const modelMap = new Map();
  560. originModelOptions.forEach((option) => {
  561. const v = (option.value || '').trim();
  562. if (!modelMap.has(v)) {
  563. modelMap.set(v, option);
  564. }
  565. });
  566. inputs.models.forEach((model) => {
  567. const v = (model || '').trim();
  568. if (!modelMap.has(v)) {
  569. modelMap.set(v, {
  570. key: v,
  571. label: v,
  572. value: v,
  573. });
  574. }
  575. });
  576. const categories = getModelCategories(t);
  577. const optionsWithIcon = Array.from(modelMap.values()).map((opt) => {
  578. const modelName = opt.value;
  579. let icon = null;
  580. for (const [key, category] of Object.entries(categories)) {
  581. if (key !== 'all' && category.filter({ model_name: modelName })) {
  582. icon = category.icon;
  583. break;
  584. }
  585. }
  586. return {
  587. ...opt,
  588. label: (
  589. <span className='flex items-center gap-1'>
  590. {icon}
  591. {modelName}
  592. </span>
  593. ),
  594. };
  595. });
  596. setModelOptions(optionsWithIcon);
  597. }, [originModelOptions, inputs.models, t]);
  598. useEffect(() => {
  599. fetchModels().then();
  600. fetchGroups().then();
  601. if (!isEdit) {
  602. setInputs(originInputs);
  603. if (formApiRef.current) {
  604. formApiRef.current.setValues(originInputs);
  605. }
  606. let localModels = getChannelModels(inputs.type);
  607. setBasicModels(localModels);
  608. setInputs((inputs) => ({ ...inputs, models: localModels }));
  609. }
  610. }, [props.editingChannel.id]);
  611. useEffect(() => {
  612. if (formApiRef.current) {
  613. formApiRef.current.setValues(inputs);
  614. }
  615. }, [inputs]);
  616. useEffect(() => {
  617. if (props.visible) {
  618. if (isEdit) {
  619. loadChannel();
  620. } else {
  621. formApiRef.current?.setValues(getInitValues());
  622. }
  623. fetchModelGroups();
  624. // 重置手动输入模式状态
  625. setUseManualInput(false);
  626. } else {
  627. // 统一的模态框关闭重置逻辑
  628. resetModalState();
  629. }
  630. }, [props.visible, channelId]);
  631. // 统一的模态框重置函数
  632. const resetModalState = () => {
  633. formApiRef.current?.reset();
  634. // 重置渠道设置状态
  635. setChannelSettings({
  636. force_format: false,
  637. thinking_to_content: false,
  638. proxy: '',
  639. pass_through_body_enabled: false,
  640. system_prompt: '',
  641. system_prompt_override: false,
  642. });
  643. // 重置密钥模式状态
  644. setKeyMode('append');
  645. // 清空表单中的key_mode字段
  646. if (formApiRef.current) {
  647. formApiRef.current.setValue('key_mode', undefined);
  648. }
  649. // 重置本地输入,避免下次打开残留上一次的 JSON 字段值
  650. setInputs(getInitValues());
  651. // 重置2FA状态
  652. resetTwoFAState();
  653. // 重置2FA验证状态
  654. reset2FAVerifyState();
  655. };
  656. const handleVertexUploadChange = ({ fileList }) => {
  657. vertexErroredNames.current.clear();
  658. (async () => {
  659. let validFiles = [];
  660. let keys = [];
  661. const errorNames = [];
  662. for (const item of fileList) {
  663. const fileObj = item.fileInstance;
  664. if (!fileObj) continue;
  665. try {
  666. const txt = await fileObj.text();
  667. keys.push(JSON.parse(txt));
  668. validFiles.push(item);
  669. } catch (err) {
  670. if (!vertexErroredNames.current.has(item.name)) {
  671. errorNames.push(item.name);
  672. vertexErroredNames.current.add(item.name);
  673. }
  674. }
  675. }
  676. // 非批量模式下只保留一个文件(最新选择的),避免重复叠加
  677. if (!batch && validFiles.length > 1) {
  678. validFiles = [validFiles[validFiles.length - 1]];
  679. keys = [keys[keys.length - 1]];
  680. }
  681. setVertexKeys(keys);
  682. setVertexFileList(validFiles);
  683. if (formApiRef.current) {
  684. formApiRef.current.setValue('vertex_files', validFiles);
  685. }
  686. setInputs((prev) => ({ ...prev, vertex_files: validFiles }));
  687. if (errorNames.length > 0) {
  688. showError(
  689. t('以下文件解析失败,已忽略:{{list}}', {
  690. list: errorNames.join(', '),
  691. }),
  692. );
  693. }
  694. })();
  695. };
  696. const submit = async () => {
  697. const formValues = formApiRef.current ? formApiRef.current.getValues() : {};
  698. let localInputs = { ...formValues };
  699. if (localInputs.type === 41) {
  700. const keyType = localInputs.vertex_key_type || 'json';
  701. if (keyType === 'api_key') {
  702. // 直接作为普通字符串密钥处理
  703. if (!isEdit && (!localInputs.key || localInputs.key.trim() === '')) {
  704. showInfo(t('请输入密钥!'));
  705. return;
  706. }
  707. } else {
  708. // JSON 服务账号密钥
  709. if (useManualInput) {
  710. if (localInputs.key && localInputs.key.trim() !== '') {
  711. try {
  712. const parsedKey = JSON.parse(localInputs.key);
  713. localInputs.key = JSON.stringify(parsedKey);
  714. } catch (err) {
  715. showError(t('密钥格式无效,请输入有效的 JSON 格式密钥'));
  716. return;
  717. }
  718. } else if (!isEdit) {
  719. showInfo(t('请输入密钥!'));
  720. return;
  721. }
  722. } else {
  723. // 文件上传模式
  724. let keys = vertexKeys;
  725. if (keys.length === 0 && vertexFileList.length > 0) {
  726. try {
  727. const parsed = await Promise.all(
  728. vertexFileList.map(async (item) => {
  729. const fileObj = item.fileInstance;
  730. if (!fileObj) return null;
  731. const txt = await fileObj.text();
  732. return JSON.parse(txt);
  733. }),
  734. );
  735. keys = parsed.filter(Boolean);
  736. } catch (err) {
  737. showError(t('解析密钥文件失败: {{msg}}', { msg: err.message }));
  738. return;
  739. }
  740. }
  741. if (keys.length === 0) {
  742. if (!isEdit) {
  743. showInfo(t('请上传密钥文件!'));
  744. return;
  745. } else {
  746. delete localInputs.key;
  747. }
  748. } else {
  749. localInputs.key = batch ? JSON.stringify(keys) : JSON.stringify(keys[0]);
  750. }
  751. }
  752. }
  753. }
  754. // 如果是编辑模式且 key 为空字符串,避免提交空值覆盖旧密钥
  755. if (isEdit && (!localInputs.key || localInputs.key.trim() === '')) {
  756. delete localInputs.key;
  757. }
  758. delete localInputs.vertex_files;
  759. if (!isEdit && (!localInputs.name || !localInputs.key)) {
  760. showInfo(t('请填写渠道名称和渠道密钥!'));
  761. return;
  762. }
  763. if (!Array.isArray(localInputs.models) || localInputs.models.length === 0) {
  764. showInfo(t('请至少选择一个模型!'));
  765. return;
  766. }
  767. if (
  768. localInputs.model_mapping &&
  769. localInputs.model_mapping !== '' &&
  770. !verifyJSON(localInputs.model_mapping)
  771. ) {
  772. showInfo(t('模型映射必须是合法的 JSON 格式!'));
  773. return;
  774. }
  775. if (localInputs.base_url && localInputs.base_url.endsWith('/')) {
  776. localInputs.base_url = localInputs.base_url.slice(
  777. 0,
  778. localInputs.base_url.length - 1,
  779. );
  780. }
  781. if (localInputs.type === 18 && localInputs.other === '') {
  782. localInputs.other = 'v2.1';
  783. }
  784. // 生成渠道额外设置JSON
  785. const channelExtraSettings = {
  786. force_format: localInputs.force_format || false,
  787. thinking_to_content: localInputs.thinking_to_content || false,
  788. proxy: localInputs.proxy || '',
  789. pass_through_body_enabled: localInputs.pass_through_body_enabled || false,
  790. system_prompt: localInputs.system_prompt || '',
  791. system_prompt_override: localInputs.system_prompt_override || false,
  792. };
  793. localInputs.setting = JSON.stringify(channelExtraSettings);
  794. // 清理不需要发送到后端的字段
  795. delete localInputs.force_format;
  796. delete localInputs.thinking_to_content;
  797. delete localInputs.proxy;
  798. delete localInputs.pass_through_body_enabled;
  799. delete localInputs.system_prompt;
  800. delete localInputs.system_prompt_override;
  801. // 顶层的 vertex_key_type 不应发送给后端
  802. delete localInputs.vertex_key_type;
  803. let res;
  804. localInputs.auto_ban = localInputs.auto_ban ? 1 : 0;
  805. localInputs.models = localInputs.models.join(',');
  806. localInputs.group = (localInputs.groups || []).join(',');
  807. let mode = 'single';
  808. if (batch) {
  809. mode = multiToSingle ? 'multi_to_single' : 'batch';
  810. }
  811. if (isEdit) {
  812. res = await API.put(`/api/channel/`, {
  813. ...localInputs,
  814. id: parseInt(channelId),
  815. key_mode: isMultiKeyChannel ? keyMode : undefined, // 只在多key模式下传递
  816. });
  817. } else {
  818. res = await API.post(`/api/channel/`, {
  819. mode: mode,
  820. multi_key_mode: mode === 'multi_to_single' ? multiKeyMode : undefined,
  821. channel: localInputs,
  822. });
  823. }
  824. const { success, message } = res.data;
  825. if (success) {
  826. if (isEdit) {
  827. showSuccess(t('渠道更新成功!'));
  828. } else {
  829. showSuccess(t('渠道创建成功!'));
  830. setInputs(originInputs);
  831. }
  832. props.refresh();
  833. props.handleClose();
  834. } else {
  835. showError(message);
  836. }
  837. };
  838. const addCustomModels = () => {
  839. if (customModel.trim() === '') return;
  840. const modelArray = customModel.split(',').map((model) => model.trim());
  841. let localModels = [...inputs.models];
  842. let localModelOptions = [...modelOptions];
  843. const addedModels = [];
  844. modelArray.forEach((model) => {
  845. if (model && !localModels.includes(model)) {
  846. localModels.push(model);
  847. localModelOptions.push({
  848. key: model,
  849. label: model,
  850. value: model,
  851. });
  852. addedModels.push(model);
  853. }
  854. });
  855. setModelOptions(localModelOptions);
  856. setCustomModel('');
  857. handleInputChange('models', localModels);
  858. if (addedModels.length > 0) {
  859. showSuccess(
  860. t('已新增 {{count}} 个模型:{{list}}', {
  861. count: addedModels.length,
  862. list: addedModels.join(', '),
  863. }),
  864. );
  865. } else {
  866. showInfo(t('未发现新增模型'));
  867. }
  868. };
  869. const batchAllowed = !isEdit || isMultiKeyChannel;
  870. const batchExtra = batchAllowed ? (
  871. <Space>
  872. {!isEdit && (
  873. <Checkbox
  874. disabled={isEdit}
  875. checked={batch}
  876. onChange={(e) => {
  877. const checked = e.target.checked;
  878. if (!checked && vertexFileList.length > 1) {
  879. Modal.confirm({
  880. title: t('切换为单密钥模式'),
  881. content: t(
  882. '将仅保留第一个密钥文件,其余文件将被移除,是否继续?',
  883. ),
  884. onOk: () => {
  885. const firstFile = vertexFileList[0];
  886. const firstKey = vertexKeys[0] ? [vertexKeys[0]] : [];
  887. setVertexFileList([firstFile]);
  888. setVertexKeys(firstKey);
  889. formApiRef.current?.setValue('vertex_files', [firstFile]);
  890. setInputs((prev) => ({ ...prev, vertex_files: [firstFile] }));
  891. setBatch(false);
  892. setMultiToSingle(false);
  893. setMultiKeyMode('random');
  894. },
  895. onCancel: () => {
  896. setBatch(true);
  897. },
  898. centered: true,
  899. });
  900. return;
  901. }
  902. setBatch(checked);
  903. if (!checked) {
  904. setMultiToSingle(false);
  905. setMultiKeyMode('random');
  906. } else {
  907. // 批量模式下禁用手动输入,并清空手动输入的内容
  908. setUseManualInput(false);
  909. if (inputs.type === 41) {
  910. // 清空手动输入的密钥内容
  911. if (formApiRef.current) {
  912. formApiRef.current.setValue('key', '');
  913. }
  914. handleInputChange('key', '');
  915. }
  916. }
  917. }}
  918. >
  919. {t('批量创建')}
  920. </Checkbox>
  921. )}
  922. {batch && (
  923. <Checkbox
  924. disabled={isEdit}
  925. checked={multiToSingle}
  926. onChange={() => {
  927. setMultiToSingle((prev) => !prev);
  928. setInputs((prev) => {
  929. const newInputs = { ...prev };
  930. if (!multiToSingle) {
  931. newInputs.multi_key_mode = multiKeyMode;
  932. } else {
  933. delete newInputs.multi_key_mode;
  934. }
  935. return newInputs;
  936. });
  937. }}
  938. >
  939. {t('密钥聚合模式')}
  940. </Checkbox>
  941. )}
  942. </Space>
  943. ) : null;
  944. const channelOptionList = useMemo(
  945. () =>
  946. CHANNEL_OPTIONS.map((opt) => ({
  947. ...opt,
  948. // 保持 label 为纯文本以支持搜索
  949. label: opt.label,
  950. })),
  951. [],
  952. );
  953. const renderChannelOption = (renderProps) => {
  954. const {
  955. disabled,
  956. selected,
  957. label,
  958. value,
  959. focused,
  960. className,
  961. style,
  962. onMouseEnter,
  963. onClick,
  964. ...rest
  965. } = renderProps;
  966. const searchWords = channelSearchValue ? [channelSearchValue] : [];
  967. // 构建样式类名
  968. const optionClassName = [
  969. 'flex items-center gap-3 px-3 py-2 transition-all duration-200 rounded-lg mx-2 my-1',
  970. focused && 'bg-blue-50 shadow-sm',
  971. selected &&
  972. 'bg-blue-100 text-blue-700 shadow-lg ring-2 ring-blue-200 ring-opacity-50',
  973. disabled && 'opacity-50 cursor-not-allowed',
  974. !disabled && 'hover:bg-gray-50 hover:shadow-md cursor-pointer',
  975. className,
  976. ]
  977. .filter(Boolean)
  978. .join(' ');
  979. return (
  980. <div
  981. style={style}
  982. className={optionClassName}
  983. onClick={() => !disabled && onClick()}
  984. onMouseEnter={(e) => onMouseEnter()}
  985. >
  986. <div className='flex items-center gap-3 w-full'>
  987. <div className='flex-shrink-0 w-5 h-5 flex items-center justify-center'>
  988. {getChannelIcon(value)}
  989. </div>
  990. <div className='flex-1 min-w-0'>
  991. <Highlight
  992. sourceString={label}
  993. searchWords={searchWords}
  994. className='text-sm font-medium truncate'
  995. />
  996. </div>
  997. {selected && (
  998. <div className='flex-shrink-0 text-blue-600'>
  999. <svg
  1000. width='16'
  1001. height='16'
  1002. viewBox='0 0 16 16'
  1003. fill='currentColor'
  1004. >
  1005. <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' />
  1006. </svg>
  1007. </div>
  1008. )}
  1009. </div>
  1010. </div>
  1011. );
  1012. };
  1013. return (
  1014. <>
  1015. <SideSheet
  1016. placement={isEdit ? 'right' : 'left'}
  1017. title={
  1018. <Space>
  1019. <Tag color='blue' shape='circle'>
  1020. {isEdit ? t('编辑') : t('新建')}
  1021. </Tag>
  1022. <Title heading={4} className='m-0'>
  1023. {isEdit ? t('更新渠道信息') : t('创建新的渠道')}
  1024. </Title>
  1025. </Space>
  1026. }
  1027. bodyStyle={{ padding: '0' }}
  1028. visible={props.visible}
  1029. width={isMobile ? '100%' : 600}
  1030. footer={
  1031. <div className='flex justify-end bg-white'>
  1032. <Space>
  1033. <Button
  1034. theme='solid'
  1035. onClick={() => formApiRef.current?.submitForm()}
  1036. icon={<IconSave />}
  1037. >
  1038. {t('提交')}
  1039. </Button>
  1040. <Button
  1041. theme='light'
  1042. type='primary'
  1043. onClick={handleCancel}
  1044. icon={<IconClose />}
  1045. >
  1046. {t('取消')}
  1047. </Button>
  1048. </Space>
  1049. </div>
  1050. }
  1051. closeIcon={null}
  1052. onCancel={() => handleCancel()}
  1053. >
  1054. <Form
  1055. key={isEdit ? 'edit' : 'new'}
  1056. initValues={originInputs}
  1057. getFormApi={(api) => (formApiRef.current = api)}
  1058. onSubmit={submit}
  1059. >
  1060. {() => (
  1061. <Spin spinning={loading}>
  1062. <div className='p-2'>
  1063. <Card className='!rounded-2xl shadow-sm border-0 mb-6'>
  1064. {/* Header: Basic Info */}
  1065. <div className='flex items-center mb-2'>
  1066. <Avatar
  1067. size='small'
  1068. color='blue'
  1069. className='mr-2 shadow-md'
  1070. >
  1071. <IconServer size={16} />
  1072. </Avatar>
  1073. <div>
  1074. <Text className='text-lg font-medium'>
  1075. {t('基本信息')}
  1076. </Text>
  1077. <div className='text-xs text-gray-600'>
  1078. {t('渠道的基本配置信息')}
  1079. </div>
  1080. </div>
  1081. </div>
  1082. <Form.Select
  1083. field='type'
  1084. label={t('类型')}
  1085. placeholder={t('请选择渠道类型')}
  1086. rules={[{ required: true, message: t('请选择渠道类型') }]}
  1087. optionList={channelOptionList}
  1088. style={{ width: '100%' }}
  1089. filter={selectFilter}
  1090. autoClearSearchValue={false}
  1091. searchPosition='dropdown'
  1092. onSearch={(value) => setChannelSearchValue(value)}
  1093. renderOptionItem={renderChannelOption}
  1094. onChange={(value) => handleInputChange('type', value)}
  1095. />
  1096. <Form.Input
  1097. field='name'
  1098. label={t('名称')}
  1099. placeholder={t('请为渠道命名')}
  1100. rules={[{ required: true, message: t('请为渠道命名') }]}
  1101. showClear
  1102. onChange={(value) => handleInputChange('name', value)}
  1103. autoComplete='new-password'
  1104. />
  1105. {inputs.type === 41 && (
  1106. <Form.Select
  1107. field='vertex_key_type'
  1108. label={t('密钥格式')}
  1109. placeholder={t('请选择密钥格式')}
  1110. optionList={[
  1111. { label: 'JSON', value: 'json' },
  1112. { label: 'API Key', value: 'api_key' },
  1113. ]}
  1114. style={{ width: '100%' }}
  1115. value={inputs.vertex_key_type || 'json'}
  1116. onChange={(value) => {
  1117. // 更新设置中的 vertex_key_type
  1118. handleChannelOtherSettingsChange('vertex_key_type', value);
  1119. // 切换为 api_key 时,关闭批量与手动/文件切换,并清理已选文件
  1120. if (value === 'api_key') {
  1121. setBatch(false);
  1122. setUseManualInput(false);
  1123. setVertexKeys([]);
  1124. setVertexFileList([]);
  1125. if (formApiRef.current) {
  1126. formApiRef.current.setValue('vertex_files', []);
  1127. }
  1128. }
  1129. }}
  1130. extraText={
  1131. inputs.vertex_key_type === 'api_key'
  1132. ? t('API Key 模式下不支持批量创建')
  1133. : t('JSON 模式支持手动输入或上传服务账号 JSON')
  1134. }
  1135. />
  1136. )}
  1137. {batch ? (
  1138. inputs.type === 41 && (inputs.vertex_key_type || 'json') === 'json' ? (
  1139. <Form.Upload
  1140. field='vertex_files'
  1141. label={t('密钥文件 (.json)')}
  1142. accept='.json'
  1143. multiple
  1144. draggable
  1145. dragIcon={<IconBolt />}
  1146. dragMainText={t('点击上传文件或拖拽文件到这里')}
  1147. dragSubText={t('仅支持 JSON 文件,支持多文件')}
  1148. style={{ marginTop: 10 }}
  1149. uploadTrigger='custom'
  1150. beforeUpload={() => false}
  1151. onChange={handleVertexUploadChange}
  1152. fileList={vertexFileList}
  1153. rules={
  1154. isEdit
  1155. ? []
  1156. : [{ required: true, message: t('请上传密钥文件') }]
  1157. }
  1158. extraText={batchExtra}
  1159. />
  1160. ) : (
  1161. <Form.TextArea
  1162. field='key'
  1163. label={t('密钥')}
  1164. placeholder={t('请输入密钥,一行一个')}
  1165. rules={
  1166. isEdit
  1167. ? []
  1168. : [{ required: true, message: t('请输入密钥') }]
  1169. }
  1170. autosize
  1171. autoComplete='new-password'
  1172. onChange={(value) => handleInputChange('key', value)}
  1173. extraText={
  1174. <div className='flex items-center gap-2'>
  1175. {isEdit &&
  1176. isMultiKeyChannel &&
  1177. keyMode === 'append' && (
  1178. <Text type='warning' size='small'>
  1179. {t(
  1180. '追加模式:新密钥将添加到现有密钥列表的末尾',
  1181. )}
  1182. </Text>
  1183. )}
  1184. {isEdit && (
  1185. <Button
  1186. size='small'
  1187. type='primary'
  1188. theme='outline'
  1189. onClick={handleShow2FAModal}
  1190. >
  1191. {t('查看密钥')}
  1192. </Button>
  1193. )}
  1194. {batchExtra}
  1195. </div>
  1196. }
  1197. showClear
  1198. />
  1199. )
  1200. ) : (
  1201. <>
  1202. {inputs.type === 41 && (inputs.vertex_key_type || 'json') === 'json' ? (
  1203. <>
  1204. {!batch && (
  1205. <div className='flex items-center justify-between mb-3'>
  1206. <Text className='text-sm font-medium'>
  1207. {t('密钥输入方式')}
  1208. </Text>
  1209. <Space>
  1210. <Button
  1211. size='small'
  1212. type={
  1213. !useManualInput ? 'primary' : 'tertiary'
  1214. }
  1215. onClick={() => {
  1216. setUseManualInput(false);
  1217. // 切换到文件上传模式时清空手动输入的密钥
  1218. if (formApiRef.current) {
  1219. formApiRef.current.setValue('key', '');
  1220. }
  1221. handleInputChange('key', '');
  1222. }}
  1223. >
  1224. {t('文件上传')}
  1225. </Button>
  1226. <Button
  1227. size='small'
  1228. type={useManualInput ? 'primary' : 'tertiary'}
  1229. onClick={() => {
  1230. setUseManualInput(true);
  1231. // 切换到手动输入模式时清空文件上传相关状态
  1232. setVertexKeys([]);
  1233. setVertexFileList([]);
  1234. if (formApiRef.current) {
  1235. formApiRef.current.setValue(
  1236. 'vertex_files',
  1237. [],
  1238. );
  1239. }
  1240. setInputs((prev) => ({
  1241. ...prev,
  1242. vertex_files: [],
  1243. }));
  1244. }}
  1245. >
  1246. {t('手动输入')}
  1247. </Button>
  1248. </Space>
  1249. </div>
  1250. )}
  1251. {batch && (
  1252. <Banner
  1253. type='info'
  1254. description={t(
  1255. '批量创建模式下仅支持文件上传,不支持手动输入',
  1256. )}
  1257. className='!rounded-lg mb-3'
  1258. />
  1259. )}
  1260. {useManualInput && !batch ? (
  1261. <Form.TextArea
  1262. field='key'
  1263. label={
  1264. isEdit
  1265. ? t('密钥(编辑模式下,保存的密钥不会显示)')
  1266. : t('密钥')
  1267. }
  1268. placeholder={t(
  1269. '请输入 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}',
  1270. )}
  1271. rules={
  1272. isEdit
  1273. ? []
  1274. : [
  1275. {
  1276. required: true,
  1277. message: t('请输入密钥'),
  1278. },
  1279. ]
  1280. }
  1281. autoComplete='new-password'
  1282. onChange={(value) =>
  1283. handleInputChange('key', value)
  1284. }
  1285. extraText={
  1286. <div className='flex items-center gap-2'>
  1287. <Text type='tertiary' size='small'>
  1288. {t('请输入完整的 JSON 格式密钥内容')}
  1289. </Text>
  1290. {isEdit &&
  1291. isMultiKeyChannel &&
  1292. keyMode === 'append' && (
  1293. <Text type='warning' size='small'>
  1294. {t(
  1295. '追加模式:新密钥将添加到现有密钥列表的末尾',
  1296. )}
  1297. </Text>
  1298. )}
  1299. {isEdit && (
  1300. <Button
  1301. size='small'
  1302. type='primary'
  1303. theme='outline'
  1304. onClick={handleShow2FAModal}
  1305. >
  1306. {t('查看密钥')}
  1307. </Button>
  1308. )}
  1309. {batchExtra}
  1310. </div>
  1311. }
  1312. autosize
  1313. showClear
  1314. />
  1315. ) : (
  1316. <Form.Upload
  1317. field='vertex_files'
  1318. label={t('密钥文件 (.json)')}
  1319. accept='.json'
  1320. draggable
  1321. dragIcon={<IconBolt />}
  1322. dragMainText={t('点击上传文件或拖拽文件到这里')}
  1323. dragSubText={t('仅支持 JSON 文件')}
  1324. style={{ marginTop: 10 }}
  1325. uploadTrigger='custom'
  1326. beforeUpload={() => false}
  1327. onChange={handleVertexUploadChange}
  1328. fileList={vertexFileList}
  1329. rules={
  1330. isEdit
  1331. ? []
  1332. : [
  1333. {
  1334. required: true,
  1335. message: t('请上传密钥文件'),
  1336. },
  1337. ]
  1338. }
  1339. extraText={batchExtra}
  1340. />
  1341. )}
  1342. </>
  1343. ) : (
  1344. <Form.Input
  1345. field='key'
  1346. label={
  1347. isEdit
  1348. ? t('密钥(编辑模式下,保存的密钥不会显示)')
  1349. : t('密钥')
  1350. }
  1351. placeholder={t(type2secretPrompt(inputs.type))}
  1352. rules={
  1353. isEdit
  1354. ? []
  1355. : [{ required: true, message: t('请输入密钥') }]
  1356. }
  1357. autoComplete='new-password'
  1358. onChange={(value) => handleInputChange('key', value)}
  1359. extraText={
  1360. <div className='flex items-center gap-2'>
  1361. {isEdit &&
  1362. isMultiKeyChannel &&
  1363. keyMode === 'append' && (
  1364. <Text type='warning' size='small'>
  1365. {t(
  1366. '追加模式:新密钥将添加到现有密钥列表的末尾',
  1367. )}
  1368. </Text>
  1369. )}
  1370. {isEdit && (
  1371. <Button
  1372. size='small'
  1373. type='primary'
  1374. theme='outline'
  1375. onClick={handleShow2FAModal}
  1376. >
  1377. {t('查看密钥')}
  1378. </Button>
  1379. )}
  1380. {batchExtra}
  1381. </div>
  1382. }
  1383. showClear
  1384. />
  1385. )}
  1386. </>
  1387. )}
  1388. {isEdit && isMultiKeyChannel && (
  1389. <Form.Select
  1390. field='key_mode'
  1391. label={t('密钥更新模式')}
  1392. placeholder={t('请选择密钥更新模式')}
  1393. optionList={[
  1394. { label: t('追加到现有密钥'), value: 'append' },
  1395. { label: t('覆盖现有密钥'), value: 'replace' },
  1396. ]}
  1397. style={{ width: '100%' }}
  1398. value={keyMode}
  1399. onChange={(value) => setKeyMode(value)}
  1400. extraText={
  1401. <Text type='tertiary' size='small'>
  1402. {keyMode === 'replace'
  1403. ? t('覆盖模式:将完全替换现有的所有密钥')
  1404. : t('追加模式:将新密钥添加到现有密钥列表末尾')}
  1405. </Text>
  1406. }
  1407. />
  1408. )}
  1409. {batch && multiToSingle && (
  1410. <>
  1411. <Form.Select
  1412. field='multi_key_mode'
  1413. label={t('密钥聚合模式')}
  1414. placeholder={t('请选择多密钥使用策略')}
  1415. optionList={[
  1416. { label: t('随机'), value: 'random' },
  1417. { label: t('轮询'), value: 'polling' },
  1418. ]}
  1419. style={{ width: '100%' }}
  1420. value={inputs.multi_key_mode || 'random'}
  1421. onChange={(value) => {
  1422. setMultiKeyMode(value);
  1423. handleInputChange('multi_key_mode', value);
  1424. }}
  1425. />
  1426. {inputs.multi_key_mode === 'polling' && (
  1427. <Banner
  1428. type='warning'
  1429. description={t(
  1430. '轮询模式必须搭配Redis和内存缓存功能使用,否则性能将大幅降低,并且无法实现轮询功能',
  1431. )}
  1432. className='!rounded-lg mt-2'
  1433. />
  1434. )}
  1435. </>
  1436. )}
  1437. {inputs.type === 18 && (
  1438. <Form.Input
  1439. field='other'
  1440. label={t('模型版本')}
  1441. placeholder={
  1442. '请输入星火大模型版本,注意是接口地址中的版本号,例如:v2.1'
  1443. }
  1444. onChange={(value) => handleInputChange('other', value)}
  1445. showClear
  1446. />
  1447. )}
  1448. {inputs.type === 41 && (
  1449. <JSONEditor
  1450. key={`region-${isEdit ? channelId : 'new'}`}
  1451. field='other'
  1452. label={t('部署地区')}
  1453. placeholder={t(
  1454. '请输入部署地区,例如:us-central1\n支持使用模型映射格式\n{\n "default": "us-central1",\n "claude-3-5-sonnet-20240620": "europe-west1"\n}',
  1455. )}
  1456. value={inputs.other || ''}
  1457. onChange={(value) => handleInputChange('other', value)}
  1458. rules={[{ required: true, message: t('请填写部署地区') }]}
  1459. template={REGION_EXAMPLE}
  1460. templateLabel={t('填入模板')}
  1461. editorType='region'
  1462. formApi={formApiRef.current}
  1463. extraText={t('设置默认地区和特定模型的专用地区')}
  1464. />
  1465. )}
  1466. {inputs.type === 21 && (
  1467. <Form.Input
  1468. field='other'
  1469. label={t('知识库 ID')}
  1470. placeholder={'请输入知识库 ID,例如:123456'}
  1471. onChange={(value) => handleInputChange('other', value)}
  1472. showClear
  1473. />
  1474. )}
  1475. {inputs.type === 39 && (
  1476. <Form.Input
  1477. field='other'
  1478. label='Account ID'
  1479. placeholder={
  1480. '请输入Account ID,例如:d6b5da8hk1awo8nap34ube6gh'
  1481. }
  1482. onChange={(value) => handleInputChange('other', value)}
  1483. showClear
  1484. />
  1485. )}
  1486. {inputs.type === 49 && (
  1487. <Form.Input
  1488. field='other'
  1489. label={t('智能体ID')}
  1490. placeholder={'请输入智能体ID,例如:7342866812345'}
  1491. onChange={(value) => handleInputChange('other', value)}
  1492. showClear
  1493. />
  1494. )}
  1495. {inputs.type === 1 && (
  1496. <Form.Input
  1497. field='openai_organization'
  1498. label={t('组织')}
  1499. placeholder={t('请输入组织org-xxx')}
  1500. showClear
  1501. helpText={t('组织,不填则为默认组织')}
  1502. onChange={(value) =>
  1503. handleInputChange('openai_organization', value)
  1504. }
  1505. />
  1506. )}
  1507. </Card>
  1508. {/* API Configuration Card */}
  1509. {showApiConfigCard && (
  1510. <Card className='!rounded-2xl shadow-sm border-0 mb-6'>
  1511. {/* Header: API Config */}
  1512. <div className='flex items-center mb-2'>
  1513. <Avatar
  1514. size='small'
  1515. color='green'
  1516. className='mr-2 shadow-md'
  1517. >
  1518. <IconGlobe size={16} />
  1519. </Avatar>
  1520. <div>
  1521. <Text className='text-lg font-medium'>
  1522. {t('API 配置')}
  1523. </Text>
  1524. <div className='text-xs text-gray-600'>
  1525. {t('API 地址和相关配置')}
  1526. </div>
  1527. </div>
  1528. </div>
  1529. {inputs.type === 40 && (
  1530. <Banner
  1531. type='info'
  1532. description={
  1533. <div>
  1534. <Text strong>{t('邀请链接')}:</Text>
  1535. <Text
  1536. link
  1537. underline
  1538. className='ml-2 cursor-pointer'
  1539. onClick={() =>
  1540. window.open(
  1541. 'https://cloud.siliconflow.cn/i/hij0YNTZ',
  1542. )
  1543. }
  1544. >
  1545. https://cloud.siliconflow.cn/i/hij0YNTZ
  1546. </Text>
  1547. </div>
  1548. }
  1549. className='!rounded-lg'
  1550. />
  1551. )}
  1552. {inputs.type === 3 && (
  1553. <>
  1554. <Banner
  1555. type='warning'
  1556. description={t(
  1557. '2025年5月10日后添加的渠道,不需要再在部署的时候移除模型名称中的"."',
  1558. )}
  1559. className='!rounded-lg'
  1560. />
  1561. <div>
  1562. <Form.Input
  1563. field='base_url'
  1564. label='AZURE_OPENAI_ENDPOINT'
  1565. placeholder={t(
  1566. '请输入 AZURE_OPENAI_ENDPOINT,例如:https://docs-test-001.openai.azure.com',
  1567. )}
  1568. onChange={(value) =>
  1569. handleInputChange('base_url', value)
  1570. }
  1571. showClear
  1572. />
  1573. </div>
  1574. <div>
  1575. <Form.Input
  1576. field='other'
  1577. label={t('默认 API 版本')}
  1578. placeholder={t(
  1579. '请输入默认 API 版本,例如:2025-04-01-preview',
  1580. )}
  1581. onChange={(value) =>
  1582. handleInputChange('other', value)
  1583. }
  1584. showClear
  1585. />
  1586. </div>
  1587. <div>
  1588. <Form.Input
  1589. field='azure_responses_version'
  1590. label={t(
  1591. '默认 Responses API 版本,为空则使用上方版本',
  1592. )}
  1593. placeholder={t('例如:preview')}
  1594. onChange={(value) =>
  1595. handleChannelOtherSettingsChange(
  1596. 'azure_responses_version',
  1597. value,
  1598. )
  1599. }
  1600. showClear
  1601. />
  1602. </div>
  1603. </>
  1604. )}
  1605. {inputs.type === 8 && (
  1606. <>
  1607. <Banner
  1608. type='warning'
  1609. description={t(
  1610. '如果你对接的是上游One API或者New API等转发项目,请使用OpenAI类型,不要使用此类型,除非你知道你在做什么。',
  1611. )}
  1612. className='!rounded-lg'
  1613. />
  1614. <div>
  1615. <Form.Input
  1616. field='base_url'
  1617. label={t('完整的 Base URL,支持变量{model}')}
  1618. placeholder={t(
  1619. '请输入完整的URL,例如:https://api.openai.com/v1/chat/completions',
  1620. )}
  1621. onChange={(value) =>
  1622. handleInputChange('base_url', value)
  1623. }
  1624. showClear
  1625. />
  1626. </div>
  1627. </>
  1628. )}
  1629. {inputs.type === 37 && (
  1630. <Banner
  1631. type='warning'
  1632. description={t(
  1633. 'Dify渠道只适配chatflow和agent,并且agent不支持图片!',
  1634. )}
  1635. className='!rounded-lg'
  1636. />
  1637. )}
  1638. {inputs.type !== 3 &&
  1639. inputs.type !== 8 &&
  1640. inputs.type !== 22 &&
  1641. inputs.type !== 36 &&
  1642. inputs.type !== 45 && (
  1643. <div>
  1644. <Form.Input
  1645. field='base_url'
  1646. label={t('API地址')}
  1647. placeholder={t(
  1648. '此项可选,用于通过自定义API地址来进行 API 调用,末尾不要带/v1和/',
  1649. )}
  1650. onChange={(value) =>
  1651. handleInputChange('base_url', value)
  1652. }
  1653. showClear
  1654. extraText={t(
  1655. '对于官方渠道,new-api已经内置地址,除非是第三方代理站点或者Azure的特殊接入地址,否则不需要填写',
  1656. )}
  1657. />
  1658. </div>
  1659. )}
  1660. {inputs.type === 22 && (
  1661. <div>
  1662. <Form.Input
  1663. field='base_url'
  1664. label={t('私有部署地址')}
  1665. placeholder={t(
  1666. '请输入私有部署地址,格式为:https://fastgpt.run/api/openapi',
  1667. )}
  1668. onChange={(value) =>
  1669. handleInputChange('base_url', value)
  1670. }
  1671. showClear
  1672. />
  1673. </div>
  1674. )}
  1675. {inputs.type === 36 && (
  1676. <div>
  1677. <Form.Input
  1678. field='base_url'
  1679. label={t(
  1680. '注意非Chat API,请务必填写正确的API地址,否则可能导致无法使用',
  1681. )}
  1682. placeholder={t(
  1683. '请输入到 /suno 前的路径,通常就是域名,例如:https://api.example.com',
  1684. )}
  1685. onChange={(value) =>
  1686. handleInputChange('base_url', value)
  1687. }
  1688. showClear
  1689. />
  1690. </div>
  1691. )}
  1692. {inputs.type === 45 && (
  1693. <div>
  1694. <Form.Select
  1695. field='base_url'
  1696. label={t('API地址')}
  1697. placeholder={t('请选择API地址')}
  1698. onChange={(value) =>
  1699. handleInputChange('base_url', value)
  1700. }
  1701. optionList={[
  1702. {
  1703. value: 'https://ark.cn-beijing.volces.com',
  1704. label: 'https://ark.cn-beijing.volces.com'
  1705. },
  1706. {
  1707. value: 'https://ark.ap-southeast.bytepluses.com',
  1708. label: 'https://ark.ap-southeast.bytepluses.com'
  1709. }
  1710. ]}
  1711. defaultValue='https://ark.cn-beijing.volces.com'
  1712. />
  1713. </div>
  1714. )}
  1715. </Card>
  1716. )}
  1717. {/* Model Configuration Card */}
  1718. <Card className='!rounded-2xl shadow-sm border-0 mb-6'>
  1719. {/* Header: Model Config */}
  1720. <div className='flex items-center mb-2'>
  1721. <Avatar
  1722. size='small'
  1723. color='purple'
  1724. className='mr-2 shadow-md'
  1725. >
  1726. <IconCode size={16} />
  1727. </Avatar>
  1728. <div>
  1729. <Text className='text-lg font-medium'>
  1730. {t('模型配置')}
  1731. </Text>
  1732. <div className='text-xs text-gray-600'>
  1733. {t('模型选择和映射设置')}
  1734. </div>
  1735. </div>
  1736. </div>
  1737. <Form.Select
  1738. field='models'
  1739. label={t('模型')}
  1740. placeholder={t('请选择该渠道所支持的模型')}
  1741. rules={[{ required: true, message: t('请选择模型') }]}
  1742. multiple
  1743. filter={selectFilter}
  1744. autoClearSearchValue={false}
  1745. searchPosition='dropdown'
  1746. optionList={modelOptions}
  1747. style={{ width: '100%' }}
  1748. onChange={(value) => handleInputChange('models', value)}
  1749. renderSelectedItem={(optionNode) => {
  1750. const modelName = String(optionNode?.value ?? '');
  1751. return {
  1752. isRenderInTag: true,
  1753. content: (
  1754. <span
  1755. className='cursor-pointer select-none'
  1756. role='button'
  1757. tabIndex={0}
  1758. title={t('点击复制模型名称')}
  1759. onClick={async (e) => {
  1760. e.stopPropagation();
  1761. const ok = await copy(modelName);
  1762. if (ok) {
  1763. showSuccess(
  1764. t('已复制:{{name}}', { name: modelName }),
  1765. );
  1766. } else {
  1767. showError(t('复制失败'));
  1768. }
  1769. }}
  1770. >
  1771. {optionNode.label || modelName}
  1772. </span>
  1773. ),
  1774. };
  1775. }}
  1776. extraText={
  1777. <Space wrap>
  1778. <Button
  1779. size='small'
  1780. type='primary'
  1781. onClick={() =>
  1782. handleInputChange('models', basicModels)
  1783. }
  1784. >
  1785. {t('填入相关模型')}
  1786. </Button>
  1787. <Button
  1788. size='small'
  1789. type='secondary'
  1790. onClick={() =>
  1791. handleInputChange('models', fullModels)
  1792. }
  1793. >
  1794. {t('填入所有模型')}
  1795. </Button>
  1796. <Button
  1797. size='small'
  1798. type='tertiary'
  1799. onClick={() => fetchUpstreamModelList('models')}
  1800. >
  1801. {t('获取模型列表')}
  1802. </Button>
  1803. <Button
  1804. size='small'
  1805. type='warning'
  1806. onClick={() => handleInputChange('models', [])}
  1807. >
  1808. {t('清除所有模型')}
  1809. </Button>
  1810. <Button
  1811. size='small'
  1812. type='tertiary'
  1813. onClick={() => {
  1814. if (inputs.models.length === 0) {
  1815. showInfo(t('没有模型可以复制'));
  1816. return;
  1817. }
  1818. try {
  1819. copy(inputs.models.join(','));
  1820. showSuccess(t('模型列表已复制到剪贴板'));
  1821. } catch (error) {
  1822. showError(t('复制失败'));
  1823. }
  1824. }}
  1825. >
  1826. {t('复制所有模型')}
  1827. </Button>
  1828. {modelGroups &&
  1829. modelGroups.length > 0 &&
  1830. modelGroups.map((group) => (
  1831. <Button
  1832. key={group.id}
  1833. size='small'
  1834. type='primary'
  1835. onClick={() => {
  1836. let items = [];
  1837. try {
  1838. if (Array.isArray(group.items)) {
  1839. items = group.items;
  1840. } else if (typeof group.items === 'string') {
  1841. const parsed = JSON.parse(
  1842. group.items || '[]',
  1843. );
  1844. if (Array.isArray(parsed)) items = parsed;
  1845. }
  1846. } catch {}
  1847. const current =
  1848. formApiRef.current?.getValue('models') ||
  1849. inputs.models ||
  1850. [];
  1851. const merged = Array.from(
  1852. new Set(
  1853. [...current, ...items]
  1854. .map((m) => (m || '').trim())
  1855. .filter(Boolean),
  1856. ),
  1857. );
  1858. handleInputChange('models', merged);
  1859. }}
  1860. >
  1861. {group.name}
  1862. </Button>
  1863. ))}
  1864. </Space>
  1865. }
  1866. />
  1867. <Form.Input
  1868. field='custom_model'
  1869. label={t('自定义模型名称')}
  1870. placeholder={t('输入自定义模型名称')}
  1871. onChange={(value) => setCustomModel(value.trim())}
  1872. value={customModel}
  1873. suffix={
  1874. <Button
  1875. size='small'
  1876. type='primary'
  1877. onClick={addCustomModels}
  1878. >
  1879. {t('填入')}
  1880. </Button>
  1881. }
  1882. />
  1883. <Form.Input
  1884. field='test_model'
  1885. label={t('默认测试模型')}
  1886. placeholder={t('不填则为模型列表第一个')}
  1887. onChange={(value) => handleInputChange('test_model', value)}
  1888. showClear
  1889. />
  1890. <JSONEditor
  1891. key={`model_mapping-${isEdit ? channelId : 'new'}`}
  1892. field='model_mapping'
  1893. label={t('模型重定向')}
  1894. placeholder={
  1895. t(
  1896. '此项可选,用于修改请求体中的模型名称,为一个 JSON 字符串,键为请求中模型名称,值为要替换的模型名称,例如:',
  1897. ) + `\n${JSON.stringify(MODEL_MAPPING_EXAMPLE, null, 2)}`
  1898. }
  1899. value={inputs.model_mapping || ''}
  1900. onChange={(value) =>
  1901. handleInputChange('model_mapping', value)
  1902. }
  1903. template={MODEL_MAPPING_EXAMPLE}
  1904. templateLabel={t('填入模板')}
  1905. editorType='keyValue'
  1906. formApi={formApiRef.current}
  1907. extraText={t('键为请求中的模型名称,值为要替换的模型名称')}
  1908. />
  1909. </Card>
  1910. {/* Advanced Settings Card */}
  1911. <Card className='!rounded-2xl shadow-sm border-0 mb-6'>
  1912. {/* Header: Advanced Settings */}
  1913. <div className='flex items-center mb-2'>
  1914. <Avatar
  1915. size='small'
  1916. color='orange'
  1917. className='mr-2 shadow-md'
  1918. >
  1919. <IconSetting size={16} />
  1920. </Avatar>
  1921. <div>
  1922. <Text className='text-lg font-medium'>
  1923. {t('高级设置')}
  1924. </Text>
  1925. <div className='text-xs text-gray-600'>
  1926. {t('渠道的高级配置选项')}
  1927. </div>
  1928. </div>
  1929. </div>
  1930. <Form.Select
  1931. field='groups'
  1932. label={t('分组')}
  1933. placeholder={t('请选择可以使用该渠道的分组')}
  1934. multiple
  1935. allowAdditions
  1936. additionLabel={t(
  1937. '请在系统设置页面编辑分组倍率以添加新的分组:',
  1938. )}
  1939. optionList={groupOptions}
  1940. style={{ width: '100%' }}
  1941. onChange={(value) => handleInputChange('groups', value)}
  1942. />
  1943. <Form.Input
  1944. field='tag'
  1945. label={t('渠道标签')}
  1946. placeholder={t('渠道标签')}
  1947. showClear
  1948. onChange={(value) => handleInputChange('tag', value)}
  1949. />
  1950. <Form.TextArea
  1951. field='remark'
  1952. label={t('备注')}
  1953. placeholder={t('请输入备注(仅管理员可见)')}
  1954. maxLength={255}
  1955. showClear
  1956. onChange={(value) => handleInputChange('remark', value)}
  1957. />
  1958. <Row gutter={12}>
  1959. <Col span={12}>
  1960. <Form.InputNumber
  1961. field='priority'
  1962. label={t('渠道优先级')}
  1963. placeholder={t('渠道优先级')}
  1964. min={0}
  1965. onNumberChange={(value) =>
  1966. handleInputChange('priority', value)
  1967. }
  1968. style={{ width: '100%' }}
  1969. />
  1970. </Col>
  1971. <Col span={12}>
  1972. <Form.InputNumber
  1973. field='weight'
  1974. label={t('渠道权重')}
  1975. placeholder={t('渠道权重')}
  1976. min={0}
  1977. onNumberChange={(value) =>
  1978. handleInputChange('weight', value)
  1979. }
  1980. style={{ width: '100%' }}
  1981. />
  1982. </Col>
  1983. </Row>
  1984. <Form.Switch
  1985. field='auto_ban'
  1986. label={t('是否自动禁用')}
  1987. checkedText={t('开')}
  1988. uncheckedText={t('关')}
  1989. onChange={(value) => setAutoBan(value)}
  1990. extraText={t(
  1991. '仅当自动禁用开启时有效,关闭后不会自动禁用该渠道',
  1992. )}
  1993. initValue={autoBan}
  1994. />
  1995. <Form.TextArea
  1996. field='param_override'
  1997. label={t('参数覆盖')}
  1998. placeholder={
  1999. t('此项可选,用于覆盖请求参数。不支持覆盖 stream 参数') +
  2000. '\n' +
  2001. t('旧格式(直接覆盖):') +
  2002. '\n{\n "temperature": 0,\n "max_tokens": 1000\n}' +
  2003. '\n\n' +
  2004. t('新格式(支持条件判断与json自定义):') +
  2005. '\n{\n "operations": [\n {\n "path": "temperature",\n "mode": "set",\n "value": 0.7,\n "conditions": [\n {\n "path": "model",\n "mode": "prefix",\n "value": "gpt"\n }\n ]\n }\n ]\n}'
  2006. }
  2007. autosize
  2008. onChange={(value) =>
  2009. handleInputChange('param_override', value)
  2010. }
  2011. extraText={
  2012. <div className='flex gap-2 flex-wrap'>
  2013. <Text
  2014. className='!text-semi-color-primary cursor-pointer'
  2015. onClick={() =>
  2016. handleInputChange(
  2017. 'param_override',
  2018. JSON.stringify({ temperature: 0 }, null, 2),
  2019. )
  2020. }
  2021. >
  2022. {t('旧格式模板')}
  2023. </Text>
  2024. <Text
  2025. className='!text-semi-color-primary cursor-pointer'
  2026. onClick={() =>
  2027. handleInputChange(
  2028. 'param_override',
  2029. JSON.stringify(
  2030. {
  2031. operations: [
  2032. {
  2033. path: 'temperature',
  2034. mode: 'set',
  2035. value: 0.7,
  2036. conditions: [
  2037. {
  2038. path: 'model',
  2039. mode: 'prefix',
  2040. value: 'gpt',
  2041. },
  2042. ],
  2043. logic: 'AND',
  2044. },
  2045. ],
  2046. },
  2047. null,
  2048. 2,
  2049. ),
  2050. )
  2051. }
  2052. >
  2053. {t('新格式模板')}
  2054. </Text>
  2055. </div>
  2056. }
  2057. showClear
  2058. />
  2059. <Form.TextArea
  2060. field='header_override'
  2061. label={t('请求头覆盖')}
  2062. placeholder={
  2063. t('此项可选,用于覆盖请求头参数') +
  2064. '\n' +
  2065. t('格式示例:') +
  2066. '\n{\n "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/139.0.0.0 Safari/537.36 Edg/139.0.0.0"\n}'
  2067. }
  2068. autosize
  2069. onChange={(value) =>
  2070. handleInputChange('header_override', value)
  2071. }
  2072. extraText={
  2073. <div className='flex gap-2 flex-wrap'>
  2074. <Text
  2075. className='!text-semi-color-primary cursor-pointer'
  2076. onClick={() =>
  2077. handleInputChange(
  2078. 'header_override',
  2079. JSON.stringify(
  2080. {
  2081. 'User-Agent':
  2082. 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/139.0.0.0 Safari/537.36 Edg/139.0.0.0',
  2083. },
  2084. null,
  2085. 2,
  2086. ),
  2087. )
  2088. }
  2089. >
  2090. {t('格式模板')}
  2091. </Text>
  2092. </div>
  2093. }
  2094. showClear
  2095. />
  2096. <JSONEditor
  2097. key={`status_code_mapping-${isEdit ? channelId : 'new'}`}
  2098. field='status_code_mapping'
  2099. label={t('状态码复写')}
  2100. placeholder={
  2101. t(
  2102. '此项可选,用于复写返回的状态码,仅影响本地判断,不修改返回到上游的状态码,比如将claude渠道的400错误复写为500(用于重试),请勿滥用该功能,例如:',
  2103. ) +
  2104. '\n' +
  2105. JSON.stringify(STATUS_CODE_MAPPING_EXAMPLE, null, 2)
  2106. }
  2107. value={inputs.status_code_mapping || ''}
  2108. onChange={(value) =>
  2109. handleInputChange('status_code_mapping', value)
  2110. }
  2111. template={STATUS_CODE_MAPPING_EXAMPLE}
  2112. templateLabel={t('填入模板')}
  2113. editorType='keyValue'
  2114. formApi={formApiRef.current}
  2115. extraText={t(
  2116. '键为原状态码,值为要复写的状态码,仅影响本地判断',
  2117. )}
  2118. />
  2119. </Card>
  2120. {/* Channel Extra Settings Card */}
  2121. <Card className='!rounded-2xl shadow-sm border-0 mb-6'>
  2122. {/* Header: Channel Extra Settings */}
  2123. <div className='flex items-center mb-2'>
  2124. <Avatar
  2125. size='small'
  2126. color='violet'
  2127. className='mr-2 shadow-md'
  2128. >
  2129. <IconBolt size={16} />
  2130. </Avatar>
  2131. <div>
  2132. <Text className='text-lg font-medium'>
  2133. {t('渠道额外设置')}
  2134. </Text>
  2135. </div>
  2136. </div>
  2137. {inputs.type === 1 && (
  2138. <Form.Switch
  2139. field='force_format'
  2140. label={t('强制格式化')}
  2141. checkedText={t('开')}
  2142. uncheckedText={t('关')}
  2143. onChange={(value) =>
  2144. handleChannelSettingsChange('force_format', value)
  2145. }
  2146. extraText={t(
  2147. '强制将响应格式化为 OpenAI 标准格式(只适用于OpenAI渠道类型)',
  2148. )}
  2149. />
  2150. )}
  2151. <Form.Switch
  2152. field='thinking_to_content'
  2153. label={t('思考内容转换')}
  2154. checkedText={t('开')}
  2155. uncheckedText={t('关')}
  2156. onChange={(value) =>
  2157. handleChannelSettingsChange('thinking_to_content', value)
  2158. }
  2159. extraText={t(
  2160. '将 reasoning_content 转换为 <think> 标签拼接到内容中',
  2161. )}
  2162. />
  2163. <Form.Switch
  2164. field='pass_through_body_enabled'
  2165. label={t('透传请求体')}
  2166. checkedText={t('开')}
  2167. uncheckedText={t('关')}
  2168. onChange={(value) =>
  2169. handleChannelSettingsChange(
  2170. 'pass_through_body_enabled',
  2171. value,
  2172. )
  2173. }
  2174. extraText={t('启用请求体透传功能')}
  2175. />
  2176. <Form.Input
  2177. field='proxy'
  2178. label={t('代理地址')}
  2179. placeholder={t('例如: socks5://user:pass@host:port')}
  2180. onChange={(value) =>
  2181. handleChannelSettingsChange('proxy', value)
  2182. }
  2183. showClear
  2184. extraText={t('用于配置网络代理,支持 socks5 协议')}
  2185. />
  2186. <Form.TextArea
  2187. field='system_prompt'
  2188. label={t('系统提示词')}
  2189. placeholder={t(
  2190. '输入系统提示词,用户的系统提示词将优先于此设置',
  2191. )}
  2192. onChange={(value) =>
  2193. handleChannelSettingsChange('system_prompt', value)
  2194. }
  2195. autosize
  2196. showClear
  2197. extraText={t(
  2198. '用户优先:如果用户在请求中指定了系统提示词,将优先使用用户的设置',
  2199. )}
  2200. />
  2201. <Form.Switch
  2202. field='system_prompt_override'
  2203. label={t('系统提示词拼接')}
  2204. checkedText={t('开')}
  2205. uncheckedText={t('关')}
  2206. onChange={(value) =>
  2207. handleChannelSettingsChange(
  2208. 'system_prompt_override',
  2209. value,
  2210. )
  2211. }
  2212. extraText={t(
  2213. '如果用户请求中包含系统提示词,则使用此设置拼接到用户的系统提示词前面',
  2214. )}
  2215. />
  2216. </Card>
  2217. </div>
  2218. </Spin>
  2219. )}
  2220. </Form>
  2221. <ImagePreview
  2222. src={modalImageUrl}
  2223. visible={isModalOpenurl}
  2224. onVisibleChange={(visible) => setIsModalOpenurl(visible)}
  2225. />
  2226. </SideSheet>
  2227. {/* 使用TwoFactorAuthModal组件进行2FA验证 */}
  2228. <TwoFactorAuthModal
  2229. visible={show2FAVerifyModal}
  2230. code={verifyCode}
  2231. loading={verifyLoading}
  2232. onCodeChange={setVerifyCode}
  2233. onVerify={handleVerify2FA}
  2234. onCancel={reset2FAVerifyState}
  2235. title={t('查看渠道密钥')}
  2236. description={t('为了保护账户安全,请验证您的两步验证码。')}
  2237. placeholder={t('请输入验证码或备用码')}
  2238. />
  2239. {/* 使用ChannelKeyDisplay组件显示密钥 */}
  2240. <Modal
  2241. title={
  2242. <div className='flex items-center'>
  2243. <div className='w-8 h-8 rounded-full bg-green-100 dark:bg-green-900 flex items-center justify-center mr-3'>
  2244. <svg
  2245. className='w-4 h-4 text-green-600 dark:text-green-400'
  2246. fill='currentColor'
  2247. viewBox='0 0 20 20'
  2248. >
  2249. <path
  2250. fillRule='evenodd'
  2251. d='M5 9V7a5 5 0 0110 0v2a2 2 0 012 2v5a2 2 0 01-2 2H5a2 2 0 01-2-2v-5a2 2 0 012-2zm8-2v2H7V7a3 3 0 016 0z'
  2252. clipRule='evenodd'
  2253. />
  2254. </svg>
  2255. </div>
  2256. {t('渠道密钥信息')}
  2257. </div>
  2258. }
  2259. visible={twoFAState.showModal && twoFAState.showKey}
  2260. onCancel={resetTwoFAState}
  2261. footer={
  2262. <Button type='primary' onClick={resetTwoFAState}>
  2263. {t('完成')}
  2264. </Button>
  2265. }
  2266. width={700}
  2267. style={{ maxWidth: '90vw' }}
  2268. >
  2269. <ChannelKeyDisplay
  2270. keyData={twoFAState.keyData}
  2271. showSuccessIcon={true}
  2272. successText={t('密钥获取成功')}
  2273. showWarning={true}
  2274. warningText={t(
  2275. '请妥善保管密钥信息,不要泄露给他人。如有安全疑虑,请及时更换密钥。',
  2276. )}
  2277. />
  2278. </Modal>
  2279. <ModelSelectModal
  2280. visible={modelModalVisible}
  2281. models={fetchedModels}
  2282. selected={inputs.models}
  2283. onConfirm={(selectedModels) => {
  2284. handleInputChange('models', selectedModels);
  2285. showSuccess(t('模型列表已更新'));
  2286. setModelModalVisible(false);
  2287. }}
  2288. onCancel={() => setModelModalVisible(false)}
  2289. />
  2290. </>
  2291. );
  2292. };
  2293. export default EditChannelModal;