EditChannelModal.jsx 101 KB

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