EditChannel.js 32 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996
  1. import React, { useEffect, useRef, useState } from 'react';
  2. import { useNavigate, useParams } from 'react-router-dom';
  3. import {
  4. API,
  5. isMobile,
  6. showError,
  7. showInfo,
  8. showSuccess,
  9. verifyJSON
  10. } from '../../helpers';
  11. import { CHANNEL_OPTIONS } from '../../constants';
  12. import Title from '@douyinfe/semi-ui/lib/es/typography/title';
  13. import {
  14. SideSheet,
  15. Space,
  16. Spin,
  17. Button,
  18. Tooltip,
  19. Input,
  20. Typography,
  21. Select,
  22. TextArea,
  23. Checkbox,
  24. Banner
  25. } from '@douyinfe/semi-ui';
  26. import { Divider } from 'semantic-ui-react';
  27. import { getChannelModels, loadChannelModels } from '../../components/utils.js';
  28. import axios from 'axios';
  29. const MODEL_MAPPING_EXAMPLE = {
  30. 'gpt-3.5-turbo-0301': 'gpt-3.5-turbo',
  31. 'gpt-4-0314': 'gpt-4',
  32. 'gpt-4-32k-0314': 'gpt-4-32k'
  33. };
  34. const STATUS_CODE_MAPPING_EXAMPLE = {
  35. 400: '500'
  36. };
  37. const REGION_EXAMPLE = {
  38. 'default': 'us-central1',
  39. 'claude-3-5-sonnet-20240620': 'europe-west1'
  40. };
  41. const fetchButtonTips = '1. 新建渠道时,请求通过当前浏览器发出;2. 编辑已有渠道,请求通过后端服务器发出';
  42. function type2secretPrompt(type) {
  43. // inputs.type === 15 ? '按照如下格式输入:APIKey|SecretKey' : (inputs.type === 18 ? '按照如下格式输入:APPID|APISecret|APIKey' : '请输入渠道对应的鉴权密钥')
  44. switch (type) {
  45. case 15:
  46. return '按照如下格式输入:APIKey|SecretKey';
  47. case 18:
  48. return '按照如下格式输入:APPID|APISecret|APIKey';
  49. case 22:
  50. return '按照如下格式输入:APIKey-AppId,例如:fastgpt-0sp2gtvfdgyi4k30jwlgwf1i-64f335d84283f05518e9e041';
  51. case 23:
  52. return '按照如下格式输入:AppId|SecretId|SecretKey';
  53. case 33:
  54. return '按照如下格式输入:Ak|Sk|Region';
  55. default:
  56. return '请输入渠道对应的鉴权密钥';
  57. }
  58. }
  59. const EditChannel = (props) => {
  60. const navigate = useNavigate();
  61. const channelId = props.editingChannel.id;
  62. const isEdit = channelId !== undefined;
  63. const [loading, setLoading] = useState(isEdit);
  64. const handleCancel = () => {
  65. props.handleClose();
  66. };
  67. const originInputs = {
  68. name: '',
  69. type: 1,
  70. key: '',
  71. openai_organization: '',
  72. max_input_tokens: 0,
  73. base_url: '',
  74. other: '',
  75. model_mapping: '',
  76. status_code_mapping: '',
  77. models: [],
  78. auto_ban: 1,
  79. test_model: '',
  80. groups: ['default'],
  81. priority: 0,
  82. weight: 0,
  83. tag: ''
  84. };
  85. const [batch, setBatch] = useState(false);
  86. const [autoBan, setAutoBan] = useState(true);
  87. // const [autoBan, setAutoBan] = useState(true);
  88. const [inputs, setInputs] = useState(originInputs);
  89. const [originModelOptions, setOriginModelOptions] = useState([]);
  90. const [modelOptions, setModelOptions] = useState([]);
  91. const [groupOptions, setGroupOptions] = useState([]);
  92. const [basicModels, setBasicModels] = useState([]);
  93. const [fullModels, setFullModels] = useState([]);
  94. const [customModel, setCustomModel] = useState('');
  95. const handleInputChange = (name, value) => {
  96. setInputs((inputs) => ({ ...inputs, [name]: value }));
  97. if (name === 'type') {
  98. let localModels = [];
  99. switch (value) {
  100. case 2:
  101. localModels = [
  102. 'mj_imagine',
  103. 'mj_variation',
  104. 'mj_reroll',
  105. 'mj_blend',
  106. 'mj_upscale',
  107. 'mj_describe',
  108. 'mj_uploads'
  109. ];
  110. break;
  111. case 5:
  112. localModels = [
  113. 'swap_face',
  114. 'mj_imagine',
  115. 'mj_variation',
  116. 'mj_reroll',
  117. 'mj_blend',
  118. 'mj_upscale',
  119. 'mj_describe',
  120. 'mj_zoom',
  121. 'mj_shorten',
  122. 'mj_modal',
  123. 'mj_inpaint',
  124. 'mj_custom_zoom',
  125. 'mj_high_variation',
  126. 'mj_low_variation',
  127. 'mj_pan',
  128. 'mj_uploads'
  129. ];
  130. break;
  131. case 36:
  132. localModels = [
  133. 'suno_music',
  134. 'suno_lyrics'
  135. ];
  136. break;
  137. default:
  138. localModels = getChannelModels(value);
  139. break;
  140. }
  141. if (inputs.models.length === 0) {
  142. setInputs((inputs) => ({ ...inputs, models: localModels }));
  143. }
  144. setBasicModels(localModels);
  145. }
  146. //setAutoBan
  147. };
  148. const loadChannel = async () => {
  149. setLoading(true);
  150. let res = await API.get(`/api/channel/${channelId}`);
  151. if (res === undefined) {
  152. return;
  153. }
  154. const { success, message, data } = res.data;
  155. if (success) {
  156. if (data.models === '') {
  157. data.models = [];
  158. } else {
  159. data.models = data.models.split(',');
  160. }
  161. if (data.group === '') {
  162. data.groups = [];
  163. } else {
  164. data.groups = data.group.split(',');
  165. }
  166. if (data.model_mapping !== '') {
  167. data.model_mapping = JSON.stringify(
  168. JSON.parse(data.model_mapping),
  169. null,
  170. 2
  171. );
  172. }
  173. setInputs(data);
  174. if (data.auto_ban === 0) {
  175. setAutoBan(false);
  176. } else {
  177. setAutoBan(true);
  178. }
  179. setBasicModels(getChannelModels(data.type));
  180. // console.log(data);
  181. } else {
  182. showError(message);
  183. }
  184. setLoading(false);
  185. };
  186. const fetchUpstreamModelList = async (name) => {
  187. if (inputs['type'] !== 1) {
  188. showError('仅支持 OpenAI 接口格式');
  189. return;
  190. }
  191. setLoading(true);
  192. const models = inputs['models'] || [];
  193. let err = false;
  194. if (isEdit) {
  195. const res = await API.get('/api/channel/fetch_models/' + channelId);
  196. if (res.data && res.data?.success) {
  197. models.push(...res.data.data);
  198. } else {
  199. err = true;
  200. }
  201. } else {
  202. if (!inputs?.['key']) {
  203. showError('请填写密钥');
  204. err = true;
  205. } else {
  206. try {
  207. const host = new URL((inputs['base_url'] || 'https://api.openai.com'));
  208. const url = `https://${host.hostname}/v1/models`;
  209. const key = inputs['key'];
  210. const res = await axios.get(url, {
  211. headers: {
  212. 'Authorization': `Bearer ${key}`
  213. }
  214. });
  215. if (res.data && res.data?.success) {
  216. models.push(...res.data.data.map((model) => model.id));
  217. } else {
  218. err = true;
  219. }
  220. } catch (error) {
  221. err = true;
  222. }
  223. }
  224. }
  225. if (!err) {
  226. handleInputChange(name, Array.from(new Set(models)));
  227. showSuccess('获取模型列表成功');
  228. } else {
  229. showError('获取模型列表失败');
  230. }
  231. setLoading(false);
  232. };
  233. const fetchModels = async () => {
  234. try {
  235. let res = await API.get(`/api/channel/models`);
  236. let localModelOptions = res.data.data.map((model) => ({
  237. label: model.id,
  238. value: model.id
  239. }));
  240. setOriginModelOptions(localModelOptions);
  241. setFullModels(res.data.data.map((model) => model.id));
  242. setBasicModels(
  243. res.data.data
  244. .filter((model) => {
  245. return model.id.startsWith('gpt-3') || model.id.startsWith('text-');
  246. })
  247. .map((model) => model.id)
  248. );
  249. } catch (error) {
  250. showError(error.message);
  251. }
  252. };
  253. const fetchGroups = async () => {
  254. try {
  255. let res = await API.get(`/api/group/`);
  256. if (res === undefined) {
  257. return;
  258. }
  259. setGroupOptions(
  260. res.data.data.map((group) => ({
  261. label: group,
  262. value: group
  263. }))
  264. );
  265. } catch (error) {
  266. showError(error.message);
  267. }
  268. };
  269. useEffect(() => {
  270. let localModelOptions = [...originModelOptions];
  271. inputs.models.forEach((model) => {
  272. if (!localModelOptions.find((option) => option.key === model)) {
  273. localModelOptions.push({
  274. label: model,
  275. value: model
  276. });
  277. }
  278. });
  279. setModelOptions(localModelOptions);
  280. }, [originModelOptions, inputs.models]);
  281. useEffect(() => {
  282. fetchModels().then();
  283. fetchGroups().then();
  284. if (isEdit) {
  285. loadChannel().then(() => {
  286. });
  287. } else {
  288. setInputs(originInputs);
  289. let localModels = getChannelModels(inputs.type);
  290. setBasicModels(localModels);
  291. setInputs((inputs) => ({ ...inputs, models: localModels }));
  292. }
  293. }, [props.editingChannel.id]);
  294. const submit = async () => {
  295. if (!isEdit && (inputs.name === '' || inputs.key === '')) {
  296. showInfo('请填写渠道名称和渠道密钥!');
  297. return;
  298. }
  299. if (inputs.models.length === 0) {
  300. showInfo('请至少选择一个模型!');
  301. return;
  302. }
  303. if (inputs.model_mapping !== '' && !verifyJSON(inputs.model_mapping)) {
  304. showInfo('模型映射必须是合法的 JSON 格式!');
  305. return;
  306. }
  307. let localInputs = { ...inputs };
  308. if (localInputs.base_url && localInputs.base_url.endsWith('/')) {
  309. localInputs.base_url = localInputs.base_url.slice(
  310. 0,
  311. localInputs.base_url.length - 1
  312. );
  313. }
  314. if (localInputs.type === 3 && localInputs.other === '') {
  315. localInputs.other = '2023-06-01-preview';
  316. }
  317. if (localInputs.type === 18 && localInputs.other === '') {
  318. localInputs.other = 'v2.1';
  319. }
  320. let res;
  321. if (!Array.isArray(localInputs.models)) {
  322. showError('提交失败,请勿重复提交!');
  323. handleCancel();
  324. return;
  325. }
  326. localInputs.auto_ban = autoBan ? 1 : 0;
  327. localInputs.models = localInputs.models.join(',');
  328. localInputs.group = localInputs.groups.join(',');
  329. if (isEdit) {
  330. res = await API.put(`/api/channel/`, {
  331. ...localInputs,
  332. id: parseInt(channelId)
  333. });
  334. } else {
  335. res = await API.post(`/api/channel/`, localInputs);
  336. }
  337. const { success, message } = res.data;
  338. if (success) {
  339. if (isEdit) {
  340. showSuccess('渠道更新成功!');
  341. } else {
  342. showSuccess('渠道创建成功!');
  343. setInputs(originInputs);
  344. }
  345. props.refresh();
  346. props.handleClose();
  347. } else {
  348. showError(message);
  349. }
  350. };
  351. const addCustomModels = () => {
  352. if (customModel.trim() === '') return;
  353. // 使用逗号分隔字符串,然后去除每个模型名称前后的空格
  354. const modelArray = customModel.split(',').map((model) => model.trim());
  355. let localModels = [...inputs.models];
  356. let localModelOptions = [...modelOptions];
  357. let hasError = false;
  358. modelArray.forEach((model) => {
  359. // 检查模型是否已存在,且模型名称非空
  360. if (model && !localModels.includes(model)) {
  361. localModels.push(model); // 添加到模型列表
  362. localModelOptions.push({
  363. // 添加到下拉选项
  364. key: model,
  365. text: model,
  366. value: model
  367. });
  368. } else if (model) {
  369. showError('某些模型已存在!');
  370. hasError = true;
  371. }
  372. });
  373. if (hasError) return; // 如果有错误则终止操作
  374. // 更新状态值
  375. setModelOptions(localModelOptions);
  376. setCustomModel('');
  377. handleInputChange('models', localModels);
  378. };
  379. return (
  380. <>
  381. <SideSheet
  382. maskClosable={false}
  383. placement={isEdit ? 'right' : 'left'}
  384. title={
  385. <Title level={3}>{isEdit ? '更新渠道信息' : '创建新的渠道'}</Title>
  386. }
  387. headerStyle={{ borderBottom: '1px solid var(--semi-color-border)' }}
  388. bodyStyle={{ borderBottom: '1px solid var(--semi-color-border)' }}
  389. visible={props.visible}
  390. footer={
  391. <div style={{ display: 'flex', justifyContent: 'flex-end' }}>
  392. <Space>
  393. <Button theme="solid" size={'large'} onClick={submit}>
  394. 提交
  395. </Button>
  396. <Button
  397. theme="solid"
  398. size={'large'}
  399. type={'tertiary'}
  400. onClick={handleCancel}
  401. >
  402. 取消
  403. </Button>
  404. </Space>
  405. </div>
  406. }
  407. closeIcon={null}
  408. onCancel={() => handleCancel()}
  409. width={isMobile() ? '100%' : 600}
  410. >
  411. <Spin spinning={loading}>
  412. <div style={{ marginTop: 10 }}>
  413. <Typography.Text strong>类型:</Typography.Text>
  414. </div>
  415. <Select
  416. name="type"
  417. required
  418. optionList={CHANNEL_OPTIONS}
  419. value={inputs.type}
  420. onChange={(value) => handleInputChange('type', value)}
  421. style={{ width: '50%' }}
  422. />
  423. {inputs.type === 3 && (
  424. <>
  425. <div style={{ marginTop: 10 }}>
  426. <Banner
  427. type={'warning'}
  428. description={
  429. <>
  430. 注意,<strong>模型部署名称必须和模型名称保持一致</strong>
  431. ,因为 One API 会把请求体中的 model
  432. 参数替换为你的部署名称(模型名称中的点会被剔除),
  433. <a
  434. target="_blank"
  435. href="https://github.com/songquanpeng/one-api/issues/133?notification_referrer_id=NT_kwDOAmJSYrM2NjIwMzI3NDgyOjM5OTk4MDUw#issuecomment-1571602271"
  436. >
  437. 图片演示
  438. </a>
  439. </>
  440. }
  441. ></Banner>
  442. </div>
  443. <div style={{ marginTop: 10 }}>
  444. <Typography.Text strong>
  445. AZURE_OPENAI_ENDPOINT:
  446. </Typography.Text>
  447. </div>
  448. <Input
  449. label="AZURE_OPENAI_ENDPOINT"
  450. name="azure_base_url"
  451. placeholder={
  452. '请输入 AZURE_OPENAI_ENDPOINT,例如:https://docs-test-001.openai.azure.com'
  453. }
  454. onChange={(value) => {
  455. handleInputChange('base_url', value);
  456. }}
  457. value={inputs.base_url}
  458. autoComplete="new-password"
  459. />
  460. <div style={{ marginTop: 10 }}>
  461. <Typography.Text strong>默认 API 版本:</Typography.Text>
  462. </div>
  463. <Input
  464. label="默认 API 版本"
  465. name="azure_other"
  466. placeholder={
  467. '请输入默认 API 版本,例如:2023-06-01-preview,该配置可以被实际的请求查询参数所覆盖'
  468. }
  469. onChange={(value) => {
  470. handleInputChange('other', value);
  471. }}
  472. value={inputs.other}
  473. autoComplete="new-password"
  474. />
  475. </>
  476. )}
  477. {inputs.type === 8 && (
  478. <>
  479. <div style={{ marginTop: 10 }}>
  480. <Banner
  481. type={'warning'}
  482. description={
  483. <>
  484. 如果你对接的是上游One API或者New API等转发项目,请使用OpenAI类型,不要使用此类型,除非你知道你在做什么。
  485. </>
  486. }
  487. ></Banner>
  488. </div>
  489. <div style={{ marginTop: 10 }}>
  490. <Typography.Text strong>
  491. 完整的 Base URL,支持变量{'{model}'}:
  492. </Typography.Text>
  493. </div>
  494. <Input
  495. name="base_url"
  496. placeholder={
  497. '请输入完整的URL,例如:https://api.openai.com/v1/chat/completions'
  498. }
  499. onChange={(value) => {
  500. handleInputChange('base_url', value);
  501. }}
  502. value={inputs.base_url}
  503. autoComplete="new-password"
  504. />
  505. </>
  506. )}
  507. {inputs.type !== 3 && inputs.type !== 8 && inputs.type !== 22 && inputs.type !== 36 && (
  508. <>
  509. <div style={{ marginTop: 10 }}>
  510. <Typography.Text strong>代理:</Typography.Text>
  511. </div>
  512. <Input
  513. label="代理"
  514. name="base_url"
  515. placeholder={'此项可选,用于通过代理站来进行 API 调用'}
  516. onChange={(value) => {
  517. handleInputChange('base_url', value);
  518. }}
  519. value={inputs.base_url}
  520. autoComplete="new-password"
  521. />
  522. </>
  523. )}
  524. {inputs.type === 22 && (
  525. <>
  526. <div style={{ marginTop: 10 }}>
  527. <Typography.Text strong>私有部署地址:</Typography.Text>
  528. </div>
  529. <Input
  530. name="base_url"
  531. placeholder={
  532. '请输入私有部署地址,格式为:https://fastgpt.run/api/openapi'
  533. }
  534. onChange={(value) => {
  535. handleInputChange('base_url', value);
  536. }}
  537. value={inputs.base_url}
  538. autoComplete="new-password"
  539. />
  540. </>
  541. )}
  542. {inputs.type === 36 && (
  543. <>
  544. <div style={{ marginTop: 10 }}>
  545. <Typography.Text strong>
  546. 注意非Chat API,请务必填写正确的API地址,否则可能导致无法使用
  547. </Typography.Text>
  548. </div>
  549. <Input
  550. name="base_url"
  551. placeholder={
  552. '请输入到 /suno 前的路径,通常就是域名,例如:https://api.example.com '
  553. }
  554. onChange={(value) => {
  555. handleInputChange('base_url', value);
  556. }}
  557. value={inputs.base_url}
  558. autoComplete="new-password"
  559. />
  560. </>
  561. )}
  562. <div style={{ marginTop: 10 }}>
  563. <Typography.Text strong>名称:</Typography.Text>
  564. </div>
  565. <Input
  566. required
  567. name="name"
  568. placeholder={'请为渠道命名'}
  569. onChange={(value) => {
  570. handleInputChange('name', value);
  571. }}
  572. value={inputs.name}
  573. autoComplete="new-password"
  574. />
  575. <div style={{ marginTop: 10 }}>
  576. <Typography.Text strong>分组:</Typography.Text>
  577. </div>
  578. <Select
  579. placeholder={'请选择可以使用该渠道的分组'}
  580. name="groups"
  581. required
  582. multiple
  583. selection
  584. allowAdditions
  585. additionLabel={'请在系统设置页面编辑分组倍率以添加新的分组:'}
  586. onChange={(value) => {
  587. handleInputChange('groups', value);
  588. }}
  589. value={inputs.groups}
  590. autoComplete="new-password"
  591. optionList={groupOptions}
  592. />
  593. {inputs.type === 18 && (
  594. <>
  595. <div style={{ marginTop: 10 }}>
  596. <Typography.Text strong>模型版本:</Typography.Text>
  597. </div>
  598. <Input
  599. name="other"
  600. placeholder={
  601. '请输入星火大模型版本,注意是接口地址中的版本号,例如:v2.1'
  602. }
  603. onChange={(value) => {
  604. handleInputChange('other', value);
  605. }}
  606. value={inputs.other}
  607. autoComplete="new-password"
  608. />
  609. </>
  610. )}
  611. {inputs.type === 41 && (
  612. <>
  613. <div style={{ marginTop: 10 }}>
  614. <Typography.Text strong>部署地区:</Typography.Text>
  615. </div>
  616. <TextArea
  617. name="other"
  618. placeholder={
  619. '请输入部署地区,例如:us-central1\n支持使用模型映射格式\n' +
  620. '{\n' +
  621. ' "default": "us-central1",\n' +
  622. ' "claude-3-5-sonnet-20240620": "europe-west1"\n' +
  623. '}'
  624. }
  625. autosize={{ minRows: 2 }}
  626. onChange={(value) => {
  627. handleInputChange('other', value);
  628. }}
  629. value={inputs.other}
  630. autoComplete="new-password"
  631. />
  632. <Typography.Text
  633. style={{
  634. color: 'rgba(var(--semi-blue-5), 1)',
  635. userSelect: 'none',
  636. cursor: 'pointer'
  637. }}
  638. onClick={() => {
  639. handleInputChange(
  640. 'other',
  641. JSON.stringify(REGION_EXAMPLE, null, 2)
  642. );
  643. }}
  644. >
  645. 填入模板
  646. </Typography.Text>
  647. </>
  648. )}
  649. {inputs.type === 21 && (
  650. <>
  651. <div style={{ marginTop: 10 }}>
  652. <Typography.Text strong>知识库 ID:</Typography.Text>
  653. </div>
  654. <Input
  655. label="知识库 ID"
  656. name="other"
  657. placeholder={'请输入知识库 ID,例如:123456'}
  658. onChange={(value) => {
  659. handleInputChange('other', value);
  660. }}
  661. value={inputs.other}
  662. autoComplete="new-password"
  663. />
  664. </>
  665. )}
  666. {inputs.type === 39 && (
  667. <>
  668. <div style={{ marginTop: 10 }}>
  669. <Typography.Text strong>Account ID:</Typography.Text>
  670. </div>
  671. <Input
  672. name="other"
  673. placeholder={
  674. '请输入Account ID,例如:d6b5da8hk1awo8nap34ube6gh'
  675. }
  676. onChange={(value) => {
  677. handleInputChange('other', value);
  678. }}
  679. value={inputs.other}
  680. autoComplete="new-password"
  681. />
  682. </>
  683. )}
  684. <div style={{ marginTop: 10 }}>
  685. <Typography.Text strong>模型:</Typography.Text>
  686. </div>
  687. <Select
  688. placeholder={'请选择该渠道所支持的模型'}
  689. name="models"
  690. required
  691. multiple
  692. selection
  693. onChange={(value) => {
  694. handleInputChange('models', value);
  695. }}
  696. value={inputs.models}
  697. autoComplete="new-password"
  698. optionList={modelOptions}
  699. />
  700. <div style={{ lineHeight: '40px', marginBottom: '12px' }}>
  701. <Space>
  702. <Button
  703. type="primary"
  704. onClick={() => {
  705. handleInputChange('models', basicModels);
  706. }}
  707. >
  708. 填入相关模型
  709. </Button>
  710. <Button
  711. type="secondary"
  712. onClick={() => {
  713. handleInputChange('models', fullModels);
  714. }}
  715. >
  716. 填入所有模型
  717. </Button>
  718. <Tooltip content={fetchButtonTips}>
  719. <Button
  720. type="tertiary"
  721. onClick={() => {
  722. fetchUpstreamModelList('models');
  723. }}
  724. >
  725. 获取模型列表
  726. </Button>
  727. </Tooltip>
  728. <Button
  729. type="warning"
  730. onClick={() => {
  731. handleInputChange('models', []);
  732. }}
  733. >
  734. 清除所有模型
  735. </Button>
  736. </Space>
  737. <Input
  738. addonAfter={
  739. <Button type="primary" onClick={addCustomModels}>
  740. 填入
  741. </Button>
  742. }
  743. placeholder="输入自定义模型名称"
  744. value={customModel}
  745. onChange={(value) => {
  746. setCustomModel(value.trim());
  747. }}
  748. />
  749. </div>
  750. <div style={{ marginTop: 10 }}>
  751. <Typography.Text strong>模型重定向:</Typography.Text>
  752. </div>
  753. <TextArea
  754. placeholder={`此项可选,用于修改请求体中的模型名称,为一个 JSON 字符串,键为请求中模型名称,值为要替换的模型名称,例如:\n${JSON.stringify(MODEL_MAPPING_EXAMPLE, null, 2)}`}
  755. name="model_mapping"
  756. onChange={(value) => {
  757. handleInputChange('model_mapping', value);
  758. }}
  759. autosize
  760. value={inputs.model_mapping}
  761. autoComplete="new-password"
  762. />
  763. <Typography.Text
  764. style={{
  765. color: 'rgba(var(--semi-blue-5), 1)',
  766. userSelect: 'none',
  767. cursor: 'pointer'
  768. }}
  769. onClick={() => {
  770. handleInputChange(
  771. 'model_mapping',
  772. JSON.stringify(MODEL_MAPPING_EXAMPLE, null, 2)
  773. );
  774. }}
  775. >
  776. 填入模板
  777. </Typography.Text>
  778. <div style={{ marginTop: 10 }}>
  779. <Typography.Text strong>密钥:</Typography.Text>
  780. </div>
  781. {batch ? (
  782. <TextArea
  783. label="密钥"
  784. name="key"
  785. required
  786. placeholder={'请输入密钥,一行一个'}
  787. onChange={(value) => {
  788. handleInputChange('key', value);
  789. }}
  790. value={inputs.key}
  791. style={{ minHeight: 150, fontFamily: 'JetBrains Mono, Consolas' }}
  792. autoComplete="new-password"
  793. />
  794. ) : (
  795. <>
  796. {inputs.type === 41 ? (
  797. <TextArea
  798. label="鉴权json"
  799. name="key"
  800. required
  801. placeholder={'{\n' +
  802. ' "type": "service_account",\n' +
  803. ' "project_id": "abc-bcd-123-456",\n' +
  804. ' "private_key_id": "123xxxxx456",\n' +
  805. ' "private_key": "-----BEGIN PRIVATE KEY-----xxxx\n' +
  806. ' "client_email": "xxx@developer.gserviceaccount.com",\n' +
  807. ' "client_id": "111222333",\n' +
  808. ' "auth_uri": "https://accounts.google.com/o/oauth2/auth",\n' +
  809. ' "token_uri": "https://oauth2.googleapis.com/token",\n' +
  810. ' "auth_provider_x509_cert_url": "https://www.googleapis.com/oauth2/v1/certs",\n' +
  811. ' "client_x509_cert_url": "https://xxxxx.gserviceaccount.com",\n' +
  812. ' "universe_domain": "googleapis.com"\n' +
  813. '}'}
  814. onChange={(value) => {
  815. handleInputChange('key', value);
  816. }}
  817. autosize={{ minRows: 10 }}
  818. value={inputs.key}
  819. autoComplete="new-password"
  820. />
  821. ) : (
  822. <Input
  823. label="密钥"
  824. name="key"
  825. required
  826. placeholder={type2secretPrompt(inputs.type)}
  827. onChange={(value) => {
  828. handleInputChange('key', value);
  829. }}
  830. value={inputs.key}
  831. autoComplete="new-password"
  832. />
  833. )
  834. }
  835. </>
  836. )}
  837. {!isEdit && (
  838. <div style={{ marginTop: 10, display: 'flex' }}>
  839. <Space>
  840. <Checkbox
  841. checked={batch}
  842. label="批量创建"
  843. name="batch"
  844. onChange={() => setBatch(!batch)}
  845. />
  846. <Typography.Text strong>批量创建</Typography.Text>
  847. </Space>
  848. </div>
  849. )}
  850. {inputs.type === 1 && (
  851. <>
  852. <div style={{ marginTop: 10 }}>
  853. <Typography.Text strong>组织:</Typography.Text>
  854. </div>
  855. <Input
  856. label="组织,可选,不填则为默认组织"
  857. name="openai_organization"
  858. placeholder="请输入组织org-xxx"
  859. onChange={(value) => {
  860. handleInputChange('openai_organization', value);
  861. }}
  862. value={inputs.openai_organization}
  863. />
  864. </>
  865. )}
  866. <div style={{ marginTop: 10 }}>
  867. <Typography.Text strong>默认测试模型:</Typography.Text>
  868. </div>
  869. <Input
  870. name="test_model"
  871. placeholder="不填则为模型列表第一个"
  872. onChange={(value) => {
  873. handleInputChange('test_model', value);
  874. }}
  875. value={inputs.test_model}
  876. />
  877. <div style={{ marginTop: 10, display: 'flex' }}>
  878. <Space>
  879. <Checkbox
  880. name="auto_ban"
  881. checked={autoBan}
  882. onChange={() => {
  883. setAutoBan(!autoBan);
  884. }}
  885. // onChange={handleInputChange}
  886. />
  887. <Typography.Text strong>
  888. 是否自动禁用(仅当自动禁用开启时有效),关闭后不会自动禁用该渠道:
  889. </Typography.Text>
  890. </Space>
  891. </div>
  892. <div style={{ marginTop: 10 }}>
  893. <Typography.Text strong>
  894. 状态码复写(仅影响本地判断,不修改返回到上游的状态码):
  895. </Typography.Text>
  896. </div>
  897. <TextArea
  898. placeholder={`此项可选,用于复写返回的状态码,比如将claude渠道的400错误复写为500(用于重试),请勿滥用该功能,例如:\n${JSON.stringify(STATUS_CODE_MAPPING_EXAMPLE, null, 2)}`}
  899. name="status_code_mapping"
  900. onChange={(value) => {
  901. handleInputChange('status_code_mapping', value);
  902. }}
  903. autosize
  904. value={inputs.status_code_mapping}
  905. autoComplete="new-password"
  906. />
  907. <Typography.Text
  908. style={{
  909. color: 'rgba(var(--semi-blue-5), 1)',
  910. userSelect: 'none',
  911. cursor: 'pointer'
  912. }}
  913. onClick={() => {
  914. handleInputChange(
  915. 'status_code_mapping',
  916. JSON.stringify(STATUS_CODE_MAPPING_EXAMPLE, null, 2)
  917. );
  918. }}
  919. >
  920. 填入模板
  921. </Typography.Text>
  922. <div style={{ marginTop: 10 }}>
  923. <Typography.Text strong>
  924. 渠道标签
  925. </Typography.Text>
  926. </div>
  927. <Input
  928. label="渠道标签"
  929. name="tag"
  930. placeholder={'渠道标签'}
  931. onChange={(value) => {
  932. handleInputChange('tag', value);
  933. }}
  934. value={inputs.tag}
  935. autoComplete="new-password"
  936. />
  937. <div style={{ marginTop: 10 }}>
  938. <Typography.Text strong>
  939. 渠道优先级
  940. </Typography.Text>
  941. </div>
  942. <Input
  943. label="渠道优先级"
  944. name="priority"
  945. placeholder={'渠道优先级'}
  946. onChange={(value) => {
  947. handleInputChange('priority', parseInt(value));
  948. }}
  949. value={inputs.priority}
  950. autoComplete="new-password"
  951. />
  952. <div style={{ marginTop: 10 }}>
  953. <Typography.Text strong>
  954. 渠道权重
  955. </Typography.Text>
  956. </div>
  957. <Input
  958. label="渠道权重"
  959. name="weight"
  960. placeholder={'渠道权重'}
  961. onChange={(value) => {
  962. handleInputChange('weight', parseInt(value));
  963. }}
  964. value={inputs.weight}
  965. autoComplete="new-password"
  966. />
  967. </Spin>
  968. </SideSheet>
  969. </>
  970. );
  971. };
  972. export default EditChannel;