diff --git a/web/default/public/locales/en/translation.json b/web/default/public/locales/en/translation.json index fc54a52e..baab810b 100644 --- a/web/default/public/locales/en/translation.json +++ b/web/default/public/locales/en/translation.json @@ -34,5 +34,123 @@ "request_failed": "Request failed", "no_link": "Admin has not set up the top-up link!" } + }, + "channel": { + "title": "Channel Management", + "search": "Search channels by ID, name and key...", + "balance_notice": "OpenAI channels no longer support getting balance via key, so balance shows as 0. For supported channel types, click balance to refresh.", + "test_notice": "Channel testing only supports chat models, preferring gpt-3.5-turbo. If unavailable, uses the first model in your configured list.", + "detail_notice": "Click the detail button below to show balance and set additional test models.", + "table": { + "id": "ID", + "name": "Name", + "group": "Group", + "type": "Type", + "status": "Status", + "response_time": "Response Time", + "balance": "Balance", + "priority": "Priority", + "test_model": "Test Model", + "actions": "Actions", + "no_name": "None", + "status_enabled": "Enabled", + "status_disabled": "Disabled", + "status_auto_disabled": "Disabled", + "status_disabled_tip": "This channel is manually disabled", + "status_auto_disabled_tip": "This channel is automatically disabled", + "status_unknown": "Unknown Status", + "not_tested": "Not Tested", + "priority_tip": "Channel selection priority, higher is preferred", + "select_test_model": "Please select test model", + "click_to_update": "Click to update" + }, + "buttons": { + "test": "Test", + "delete": "Delete", + "confirm_delete": "Delete Channel", + "enable": "Enable", + "disable": "Disable", + "edit": "Edit", + "add": "Add New Channel", + "test_all": "Test All Channels", + "test_disabled": "Test Disabled Channels", + "delete_disabled": "Delete Disabled Channels", + "confirm_delete_disabled": "Confirm Delete", + "refresh": "Refresh", + "show_detail": "Details", + "hide_detail": "Hide Details" + }, + "messages": { + "test_success": "Channel ${name} test successful, model ${model}, time ${time}s, output: ${message}", + "test_all_started": "Channel testing started successfully, please refresh page to see results.", + "delete_disabled_success": "Deleted all disabled channels, total: ${count}", + "balance_update_success": "Channel ${name} balance updated successfully!", + "all_balance_updated": "All enabled channel balances have been updated!" + }, + "edit": { + "title_edit": "Update Channel Information", + "title_create": "Create New Channel", + "type": "Type", + "name": "Name", + "name_placeholder": "Please enter name", + "group": "Group", + "group_placeholder": "Please select groups that can use this channel", + "group_addition": "Please edit group multipliers in system settings to add new group:", + "models": "Models", + "models_placeholder": "Please select models supported by this channel", + "model_mapping": "Model Mapping", + "model_mapping_placeholder": "Optional, used to modify model names in request body. A JSON string where keys are request model names and values are target model names", + "system_prompt": "System Prompt", + "system_prompt_placeholder": "Optional, used to force set system prompt. Use with custom model & model mapping. First create a unique custom model name above, then map it to a natively supported model", + "base_url": "Proxy", + "base_url_placeholder": "Optional, used for API calls through proxy. Enter proxy address in format: https://domain.com", + "key": "Key", + "key_placeholder": "Please enter key", + "batch": "Batch Create", + "batch_placeholder": "Please enter keys, one per line", + "buttons": { + "cancel": "Cancel", + "submit": "Submit", + "fill_models": "Fill Related Models", + "fill_all": "Fill All Models", + "clear": "Clear All Models", + "add_custom": "Add", + "custom_placeholder": "Enter custom model name" + }, + "messages": { + "name_required": "Please enter channel name and key!", + "models_required": "Please select at least one model!", + "model_mapping_invalid": "Model mapping must be valid JSON format!", + "update_success": "Channel updated successfully!", + "create_success": "Channel created successfully!" + }, + "spark_version": "Model Version", + "spark_version_placeholder": "Please enter Spark model version from API URL, e.g.: v2.1", + "knowledge_id": "Knowledge Base ID", + "knowledge_id_placeholder": "Please enter knowledge base ID, e.g.: 123456", + "plugin_param": "Plugin Parameter", + "plugin_param_placeholder": "Please enter plugin parameter (X-DashScope-Plugin header value)", + "coze_notice": "For Coze, model name is the Bot ID. You can add prefix `bot-`, e.g.: `bot-123456`.", + "douban_notice": "For Douban, you need to go to", + "douban_notice_link": "Model Inference Page", + "douban_notice_2": "to create an inference endpoint, and use the endpoint name as model name, e.g.: `ep-20240608051426-tkxvl`.", + "aws_region_placeholder": "region, e.g.: us-west-2", + "aws_ak_placeholder": "AWS IAM Access Key", + "aws_sk_placeholder": "AWS IAM Secret Key", + "vertex_region_placeholder": "Vertex AI Region, e.g.: us-east5", + "vertex_project_id": "Vertex AI Project ID", + "vertex_project_id_placeholder": "Vertex AI Project ID", + "vertex_credentials": "Google Cloud Application Default Credentials JSON", + "vertex_credentials_placeholder": "Google Cloud Application Default Credentials JSON", + "user_id": "User ID", + "user_id_placeholder": "User ID who generated this key", + "key_prompts": { + "default": "Please enter the authentication key for this channel", + "zhipu": "Enter in format: APIKey|SecretKey", + "spark": "Enter in format: APPID|APISecret|APIKey", + "fastgpt": "Enter in format: APIKey-AppId, e.g.: fastgpt-0sp2gtvfdgyi4k30jwlgwf1i-64f335d84283f05518e9e041", + "tencent": "Enter in format: AppId|SecretId|SecretKey" + } + } } } diff --git a/web/default/public/locales/zh/translation.json b/web/default/public/locales/zh/translation.json index 51449786..8b53e005 100644 --- a/web/default/public/locales/zh/translation.json +++ b/web/default/public/locales/zh/translation.json @@ -34,5 +34,123 @@ "request_failed": "请求失败", "no_link": "超级管理员未设置充值链接!" } + }, + "channel": { + "title": "管理渠道", + "search": "搜索渠道的 ID,名称和密钥 ...", + "balance_notice": "OpenAI 渠道已经不再支持通过 key 获取余额,因此余额显示为 0。对于支持的渠道类型,请点击余额进行刷新。", + "test_notice": "渠道测试仅支持 chat 模型,优先使用 gpt-3.5-turbo,如果该模型不可用则使用你所配置的模型列表中的第一个模型。", + "detail_notice": "点击下方详情按钮可以显示余额以及设置额外的测试模型。", + "table": { + "id": "ID", + "name": "名称", + "group": "分组", + "type": "类型", + "status": "状态", + "response_time": "响应时间", + "balance": "余额", + "priority": "优先级", + "test_model": "测试模型", + "actions": "操作", + "no_name": "无", + "status_enabled": "已启用", + "status_disabled": "已禁用", + "status_auto_disabled": "已禁用", + "status_disabled_tip": "本渠道被手动禁用", + "status_auto_disabled_tip": "本渠道被程序自动禁用", + "status_unknown": "未知状态", + "not_tested": "未测试", + "priority_tip": "渠道选择优先级,越高越优先", + "select_test_model": "请选择测试模型", + "click_to_update": "点击更新" + }, + "buttons": { + "test": "测试", + "delete": "删除", + "confirm_delete": "删除渠道", + "enable": "启用", + "disable": "禁用", + "edit": "编辑", + "add": "添加新的渠道", + "test_all": "测试所有渠道", + "test_disabled": "测试禁用渠道", + "delete_disabled": "删除禁用渠道", + "confirm_delete_disabled": "确认删除", + "refresh": "刷新", + "show_detail": "详情", + "hide_detail": "隐藏详情" + }, + "messages": { + "test_success": "渠道 ${name} 测试成功,模型 ${model},耗时 ${time} 秒,模型输出:${message}", + "test_all_started": "已成功开始测试渠道,请刷新页面查看结果。", + "delete_disabled_success": "已删除所有禁用渠道,共计 ${count} 个", + "balance_update_success": "渠道 ${name} 余额更新成功!", + "all_balance_updated": "已更新完毕所有已启用渠道余额!" + }, + "edit": { + "title_edit": "更新渠道信息", + "title_create": "创建新的渠道", + "type": "类型", + "name": "名称", + "name_placeholder": "请输入名称", + "group": "分组", + "group_placeholder": "请选择可以使用该渠道的分组", + "group_addition": "请在系统设置页面编辑分组倍率以添加新的分组:", + "models": "模型", + "models_placeholder": "请选择该渠道所支持的模型", + "model_mapping": "模型重定向", + "model_mapping_placeholder": "此项可选,用于修改请求体中的模型名称,为一个 JSON 字符串,键为请求中模型名称,值为要替换的模型名称", + "system_prompt": "系统提示词", + "system_prompt_placeholder": "此项可选,用于强制设置给定的系统提示词,请配合自定义模型 & 模型重定向使用,首先创建一个唯一的自定义模型名称并在上面填入,之后将该自定义模型重定向映射到该渠道一个原生支持的模型", + "base_url": "代理", + "base_url_placeholder": "此项可选,用于通过代理站来进行 API 调用,请输入代理站地址,格式为:https://domain.com", + "key": "密钥", + "key_placeholder": "请输入密钥", + "batch": "批量创建", + "batch_placeholder": "请输入密钥,一行一个", + "buttons": { + "cancel": "取消", + "submit": "提交", + "fill_models": "填入相关模型", + "fill_all": "填入所有模型", + "clear": "清除所有模型", + "add_custom": "填入", + "custom_placeholder": "输入自定义模型名称" + }, + "messages": { + "name_required": "请填写渠道名称和渠道密钥!", + "models_required": "请至少选择一个模型!", + "model_mapping_invalid": "模型映射必须是合法的 JSON 格式!", + "update_success": "渠道更新成功!", + "create_success": "渠道创建成功!" + }, + "spark_version": "模型版本", + "spark_version_placeholder": "请输入星火大模型版本,注意是接口地址中的版本号,例如:v2.1", + "knowledge_id": "知识库 ID", + "knowledge_id_placeholder": "请输入知识库 ID,例如:123456", + "plugin_param": "插件参数", + "plugin_param_placeholder": "请输入插件参数,即 X-DashScope-Plugin 请求头的取值", + "coze_notice": "对于 Coze 而言,模型名称即 Bot ID,你可以添加一个前缀 `bot-`,例如:`bot-123456`。", + "douban_notice": "对于豆包而言,需要手动去", + "douban_notice_link": "模型推理页面", + "douban_notice_2": "创建推理接入点,以接入点名称作为模型名称,例如:`ep-20240608051426-tkxvl`。", + "aws_region_placeholder": "region,例如:us-west-2", + "aws_ak_placeholder": "AWS IAM Access Key", + "aws_sk_placeholder": "AWS IAM Secret Key", + "vertex_region_placeholder": "Vertex AI Region,例如:us-east5", + "vertex_project_id": "Vertex AI Project ID", + "vertex_project_id_placeholder": "Vertex AI Project ID", + "vertex_credentials": "Google Cloud Application Default Credentials JSON", + "vertex_credentials_placeholder": "Google Cloud Application Default Credentials JSON", + "user_id": "User ID", + "user_id_placeholder": "生成该密钥的用户 ID", + "key_prompts": { + "default": "请输入渠道对应的鉴权密钥", + "zhipu": "按照如下格式输入:APIKey|SecretKey", + "spark": "按照如下格式输入:APPID|APISecret|APIKey", + "fastgpt": "按照如下格式输入:APIKey-AppId,例如:fastgpt-0sp2gtvfdgyi4k30jwlgwf1i-64f335d84283f05518e9e041", + "tencent": "按照如下格式输入:AppId|SecretId|SecretKey" + } + } } } diff --git a/web/default/src/components/ChannelsTable.js b/web/default/src/components/ChannelsTable.js index f4ff6eb2..51cefed8 100644 --- a/web/default/src/components/ChannelsTable.js +++ b/web/default/src/components/ChannelsTable.js @@ -1,4 +1,5 @@ import React, { useEffect, useState } from 'react'; +import { useTranslation } from 'react-i18next'; import { Button, Dropdown, @@ -31,13 +32,17 @@ function renderTimestamp(timestamp) { let type2label = undefined; -function renderType(type) { +function renderType(type, t) { if (!type2label) { type2label = new Map(); for (let i = 0; i < CHANNEL_OPTIONS.length; i++) { type2label[CHANNEL_OPTIONS[i].value] = CHANNEL_OPTIONS[i]; } - type2label[0] = { value: 0, text: '未知类型', color: 'grey' }; + type2label[0] = { + value: 0, + text: t('channel.table.status_unknown'), + color: 'grey', + }; } return ( } - content='本渠道被手动禁用' + content={t('channel.table.status_disabled_tip')} basic /> ); @@ -232,29 +238,29 @@ const ChannelsTable = () => { - 已禁用 + {t('channel.table.status_auto_disabled')} } - content='本渠道被程序自动禁用' + content={t('channel.table.status_auto_disabled_tip')} basic /> ); default: return ( ); } }; - const renderResponseTime = (responseTime) => { + const renderResponseTime = (responseTime, t) => { let time = responseTime / 1000; - time = time.toFixed(2) + ' 秒'; + time = time.toFixed(2) + 's'; if (responseTime === 0) { return ( ); } else if (responseTime <= 1000) { @@ -320,9 +326,12 @@ const ChannelsTable = () => { newChannels[realIdx].test_time = Date.now() / 1000; setChannels(newChannels); showInfo( - `渠道 ${name} 测试成功,模型 ${model},耗时 ${time.toFixed( - 2 - )} 秒,模型输出:${message}` + t('channel.messages.test_success', { + name: name, + model: model, + time: time.toFixed(2), + message: message, + }) ); } else { showError(message); @@ -338,7 +347,7 @@ const ChannelsTable = () => { const res = await API.get(`/api/channel/test?scope=${scope}`); const { success, message } = res.data; if (success) { - showInfo('已成功开始测试渠道,请刷新页面查看结果。'); + showInfo(t('channel.messages.test_all_started')); } else { showError(message); } @@ -348,7 +357,9 @@ const ChannelsTable = () => { const res = await API.delete(`/api/channel/disabled`); const { success, message, data } = res.data; if (success) { - showSuccess(`已删除所有禁用渠道,共计 ${data} 个`); + showSuccess( + t('channel.messages.delete_disabled_success', { count: data }) + ); await refresh(); } else { showError(message); @@ -364,7 +375,7 @@ const ChannelsTable = () => { newChannels[realIdx].balance = balance; newChannels[realIdx].balance_updated_time = Date.now() / 1000; setChannels(newChannels); - showInfo(`渠道 ${name} 余额更新成功!`); + showInfo(t('channel.messages.balance_update_success', { name: name })); } else { showError(message); } @@ -375,7 +386,7 @@ const ChannelsTable = () => { const res = await API.get(`/api/channel/update_balance`); const { success, message } = res.data; if (success) { - showInfo('已更新完毕所有已启用渠道余额!'); + showInfo(t('channel.messages.all_balance_updated')); } else { showError(message); } @@ -413,7 +424,7 @@ const ChannelsTable = () => { icon='search' fluid iconPosition='left' - placeholder='搜索渠道的 ID,名称和密钥 ...' + placeholder={t('channel.search')} value={searchKeyword} loading={searching} onChange={handleKeywordChange} @@ -426,13 +437,11 @@ const ChannelsTable = () => { setPromptShown(promptID); }} > - OpenAI 渠道已经不再支持通过 key 获取余额,因此余额显示为 - 0。对于支持的渠道类型,请点击余额进行刷新。 + {t('channel.balance_notice')}
- 渠道测试仅支持 chat 模型,优先使用 - gpt-3.5-turbo,如果该模型不可用则使用你所配置的模型列表中的第一个模型。 + {t('channel.test_notice')}
- 点击下方详情按钮可以显示余额以及设置额外的测试模型。 + {t('channel.detail_notice')} )} @@ -444,7 +453,7 @@ const ChannelsTable = () => { sortChannel('id'); }} > - ID + {t('channel.table.id')} { sortChannel('name'); }} > - 名称 + {t('channel.table.name')} { sortChannel('group'); }} > - 分组 + {t('channel.table.group')} { sortChannel('type'); }} > - 类型 + {t('channel.table.type')} { sortChannel('status'); }} > - 状态 + {t('channel.table.status')} { sortChannel('response_time'); }} > - 响应时间 + {t('channel.table.response_time')} { }} hidden={!showDetail} > - 余额 + {t('channel.table.balance')} { sortChannel('priority'); }} > - 优先级 + {t('channel.table.priority')} - - 操作 + + {t('channel.table.actions')} @@ -519,19 +530,21 @@ const ChannelsTable = () => { return ( {channel.id} - {channel.name ? channel.name : '无'} + + {channel.name ? channel.name : t('channel.table.no_name')} + {renderGroup(channel.group)} - {renderType(channel.type)} - {renderStatus(channel.status)} + {renderType(channel.type, t)} + {renderStatus(channel.status, t)} @@ -544,10 +557,10 @@ const ChannelsTable = () => { }} style={{ cursor: 'pointer' }} > - {renderBalance(channel.type, channel.balance)} + {renderBalance(channel.type, channel.balance, t)} } - content='点击更新' + content={t('channel.table.click_to_update')} basic /> @@ -569,13 +582,13 @@ const ChannelsTable = () => { } - content='渠道选择优先级,越高越优先' + content={t('channel.table.priority_tip')} basic /> @@ -664,7 +669,7 @@ const ChannelsTable = () => { to='/channel/add' loading={loading} > - 添加新的渠道 + {t('channel.buttons.add')} - {/**/} - 删除禁用渠道 + {t('channel.buttons.delete_disabled')} } on='click' @@ -702,7 +705,7 @@ const ChannelsTable = () => { negative onClick={deleteAllDisabledChannels} > - 确认删除 + {t('channel.buttons.confirm_delete_disabled')} { } /> diff --git a/web/default/src/pages/Channel/EditChannel.js b/web/default/src/pages/Channel/EditChannel.js index 906a674d..335f0088 100644 --- a/web/default/src/pages/Channel/EditChannel.js +++ b/web/default/src/pages/Channel/EditChannel.js @@ -1,4 +1,5 @@ import React, { useEffect, useState } from 'react'; +import { useTranslation } from 'react-i18next'; import { Button, Form, @@ -26,23 +27,23 @@ const MODEL_MAPPING_EXAMPLE = { 'gpt-4-32k-0314': 'gpt-4-32k', }; -function type2secretPrompt(type) { - // inputs.type === 15 ? '按照如下格式输入:APIKey|SecretKey' : (inputs.type === 18 ? '按照如下格式输入:APPID|APISecret|APIKey' : '请输入渠道对应的鉴权密钥') +function type2secretPrompt(type, t) { switch (type) { case 15: - return '按照如下格式输入:APIKey|SecretKey'; + return t('channel.edit.key_prompts.zhipu'); case 18: - return '按照如下格式输入:APPID|APISecret|APIKey'; + return t('channel.edit.key_prompts.spark'); case 22: - return '按照如下格式输入:APIKey-AppId,例如:fastgpt-0sp2gtvfdgyi4k30jwlgwf1i-64f335d84283f05518e9e041'; + return t('channel.edit.key_prompts.fastgpt'); case 23: - return '按照如下格式输入:AppId|SecretId|SecretKey'; + return t('channel.edit.key_prompts.tencent'); default: - return '请输入渠道对应的鉴权密钥'; + return t('channel.edit.key_prompts.default'); } } const EditChannel = () => { + const { t } = useTranslation(); const params = useParams(); const navigate = useNavigate(); const channelId = params.id; @@ -194,15 +195,15 @@ const EditChannel = () => { } } if (!isEdit && (inputs.name === '' || inputs.key === '')) { - showInfo('请填写渠道名称和渠道密钥!'); + showInfo(t('channel.edit.messages.name_required')); return; } if (inputs.type !== 43 && inputs.models.length === 0) { - showInfo('请至少选择一个模型!'); + showInfo(t('channel.edit.messages.models_required')); return; } if (inputs.model_mapping !== '' && !verifyJSON(inputs.model_mapping)) { - showInfo('模型映射必须是合法的 JSON 格式!'); + showInfo(t('channel.edit.messages.model_mapping_invalid')); return; } let localInputs = { ...inputs }; @@ -230,9 +231,9 @@ const EditChannel = () => { const { success, message } = res.data; if (success) { if (isEdit) { - showSuccess('渠道更新成功!'); + showSuccess(t('channel.edit.messages.update_success')); } else { - showSuccess('渠道创建成功!'); + showSuccess(t('channel.edit.messages.create_success')); setInputs(originInputs); } } else { @@ -263,12 +264,14 @@ const EditChannel = () => { - {isEdit ? '更新渠道信息' : '创建新的渠道'} + {isEdit + ? t('channel.edit.title_edit') + : t('channel.edit.title_create')}
{ onChange={handleInputChange} /> + + + + + + + + {/* Azure OpenAI specific fields */} {inputs.type === 3 && ( <> @@ -295,9 +327,7 @@ const EditChannel = () => { { { )} + + {/* Custom base URL field */} {inputs.type === 8 && ( )} - - - - - - + {inputs.type === 18 && ( { {inputs.type === 21 && ( { {inputs.type === 17 && ( { )} {inputs.type === 34 && ( - - 对于 Coze 而言,模型名称即 Bot ID,你可以添加一个前缀 - `bot-`,例如:`bot-123456`。 - + {t('channel.edit.coze_notice')} )} {inputs.type === 40 && ( - 对于豆包而言,需要手动去{' '} + {t('channel.edit.douban_notice')} - 模型推理页面 - {' '} - 创建推理接入点,以接入点名称作为模型名称,例如:`ep-20240608051426-tkxvl`。 + {t('channel.edit.douban_notice_link')} + + {t('channel.edit.douban_notice_2')} )} {inputs.type !== 43 && ( { }); }} > - 填入相关模型 + {t('channel.edit.buttons.fill_models')} - 填入 + {t('channel.edit.buttons.add_custom')} } - placeholder='输入自定义模型名称' + placeholder={t('channel.edit.buttons.custom_placeholder')} value={customModel} onChange={(e, { value }) => { setCustomModel(value); @@ -493,12 +488,10 @@ const EditChannel = () => { <> { { label='Region' name='region' required - placeholder={'region,e.g. us-west-2'} + placeholder={t('channel.edit.aws_region_placeholder')} onChange={handleConfigChange} value={config.region} autoComplete='' @@ -540,7 +533,7 @@ const EditChannel = () => { label='AK' name='ak' required - placeholder={'AWS IAM Access Key'} + placeholder={t('channel.edit.aws_ak_placeholder')} onChange={handleConfigChange} value={config.ak} autoComplete='' @@ -549,7 +542,7 @@ const EditChannel = () => { label='SK' name='sk' required - placeholder={'AWS IAM Secret Key'} + placeholder={t('channel.edit.aws_sk_placeholder')} onChange={handleConfigChange} value={config.sk} autoComplete='' @@ -562,27 +555,25 @@ const EditChannel = () => { label='Region' name='region' required - placeholder={'Vertex AI Region.g. us-east5'} + placeholder={t('channel.edit.vertex_region_placeholder')} onChange={handleConfigChange} value={config.region} autoComplete='' /> { )} {inputs.type === 34 && ( { (batch ? ( { ) : ( ))} - {inputs.type === 37 && ( - - - - )} {inputs.type !== 33 && !isEdit && ( setBatch(!batch)} /> @@ -660,11 +636,9 @@ const EditChannel = () => { inputs.type !== 22 && ( { /> )} - +
diff --git a/web/default/src/pages/Channel/index.js b/web/default/src/pages/Channel/index.js index 73460cfa..97c417d8 100644 --- a/web/default/src/pages/Channel/index.js +++ b/web/default/src/pages/Channel/index.js @@ -1,16 +1,21 @@ import React from 'react'; import { Card } from 'semantic-ui-react'; import ChannelsTable from '../../components/ChannelsTable'; +import { useTranslation } from 'react-i18next'; -const Channel = () => ( -
- - - 管理渠道 - - - -
-); +const Channel = () => { + const { t } = useTranslation(); + + return ( +
+ + + {t('channel.title')} + + + +
+ ); +}; export default Channel;