EditChannelModal.jsx 101 KB

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