EditChannelModal.jsx 106 KB

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