EditChannelModal.jsx 102 KB

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