EditChannelModal.jsx 92 KB

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