diff --git a/relay/channel/ali/adaptor.go b/relay/channel/ali/adaptor.go index 31e926d6..f30d4dc4 100644 --- a/relay/channel/ali/adaptor.go +++ b/relay/channel/ali/adaptor.go @@ -31,6 +31,8 @@ func (a *Adaptor) GetRequestURL(info *relaycommon.RelayInfo) (string, error) { switch info.RelayMode { case constant.RelayModeEmbeddings: fullRequestURL = fmt.Sprintf("%s/api/v1/services/embeddings/text-embedding/text-embedding", info.BaseUrl) + case constant.RelayModeRerank: + fullRequestURL = fmt.Sprintf("%s/api/v1/services/rerank/text-rerank/text-rerank", info.BaseUrl) case constant.RelayModeImagesGenerations: fullRequestURL = fmt.Sprintf("%s/api/v1/services/aigc/text2image/image-synthesis", info.BaseUrl) case constant.RelayModeCompletions: @@ -76,7 +78,7 @@ func (a *Adaptor) ConvertImageRequest(c *gin.Context, info *relaycommon.RelayInf } func (a *Adaptor) ConvertRerankRequest(c *gin.Context, relayMode int, request dto.RerankRequest) (any, error) { - return nil, errors.New("not implemented") + return ConvertRerankRequest(request), nil } func (a *Adaptor) ConvertEmbeddingRequest(c *gin.Context, info *relaycommon.RelayInfo, request dto.EmbeddingRequest) (any, error) { @@ -103,6 +105,8 @@ func (a *Adaptor) DoResponse(c *gin.Context, resp *http.Response, info *relaycom err, usage = aliImageHandler(c, resp, info) case constant.RelayModeEmbeddings: err, usage = aliEmbeddingHandler(c, resp) + case constant.RelayModeRerank: + err, usage = RerankHandler(c, resp, info) default: if info.IsStream { err, usage = openai.OaiStreamHandler(c, resp, info) diff --git a/relay/channel/ali/constants.go b/relay/channel/ali/constants.go index 46de5e40..df64439b 100644 --- a/relay/channel/ali/constants.go +++ b/relay/channel/ali/constants.go @@ -8,6 +8,7 @@ var ModelList = []string{ "qwq-32b", "qwen3-235b-a22b", "text-embedding-v1", + "gte-rerank-v2", } var ChannelName = "ali" diff --git a/relay/channel/ali/dto.go b/relay/channel/ali/dto.go index f51286ad..dbd18968 100644 --- a/relay/channel/ali/dto.go +++ b/relay/channel/ali/dto.go @@ -1,5 +1,7 @@ package ali +import "one-api/dto" + type AliMessage struct { Content string `json:"content"` Role string `json:"role"` @@ -97,3 +99,28 @@ type AliImageRequest struct { } `json:"parameters,omitempty"` ResponseFormat string `json:"response_format,omitempty"` } + +type AliRerankParameters struct { + TopN *int `json:"top_n,omitempty"` + ReturnDocuments *bool `json:"return_documents,omitempty"` +} + +type AliRerankInput struct { + Query string `json:"query"` + Documents []any `json:"documents"` +} + +type AliRerankRequest struct { + Model string `json:"model"` + Input AliRerankInput `json:"input"` + Parameters AliRerankParameters `json:"parameters,omitempty"` +} + +type AliRerankResponse struct { + Output struct { + Results []dto.RerankResponseResult `json:"results"` + } `json:"output"` + Usage AliUsage `json:"usage"` + RequestId string `json:"request_id"` + AliError +} diff --git a/relay/channel/ali/rerank.go b/relay/channel/ali/rerank.go new file mode 100644 index 00000000..c9ae066a --- /dev/null +++ b/relay/channel/ali/rerank.go @@ -0,0 +1,83 @@ +package ali + +import ( + "encoding/json" + "io" + "net/http" + "one-api/dto" + relaycommon "one-api/relay/common" + "one-api/service" + + "github.com/gin-gonic/gin" +) + +func ConvertRerankRequest(request dto.RerankRequest) *AliRerankRequest { + returnDocuments := request.ReturnDocuments + if returnDocuments == nil { + t := true + returnDocuments = &t + } + return &AliRerankRequest{ + Model: request.Model, + Input: AliRerankInput{ + Query: request.Query, + Documents: request.Documents, + }, + Parameters: AliRerankParameters{ + TopN: &request.TopN, + ReturnDocuments: returnDocuments, + }, + } +} + +func RerankHandler(c *gin.Context, resp *http.Response, info *relaycommon.RelayInfo) (*dto.OpenAIErrorWithStatusCode, *dto.Usage) { + responseBody, err := io.ReadAll(resp.Body) + if err != nil { + return service.OpenAIErrorWrapper(err, "read_response_body_failed", http.StatusInternalServerError), nil + } + err = resp.Body.Close() + if err != nil { + return service.OpenAIErrorWrapper(err, "close_response_body_failed", http.StatusInternalServerError), nil + } + + var aliResponse AliRerankResponse + err = json.Unmarshal(responseBody, &aliResponse) + if err != nil { + return service.OpenAIErrorWrapper(err, "unmarshal_response_body_failed", http.StatusInternalServerError), nil + } + + if aliResponse.Code != "" { + return &dto.OpenAIErrorWithStatusCode{ + Error: dto.OpenAIError{ + Message: aliResponse.Message, + Type: aliResponse.Code, + Param: aliResponse.RequestId, + Code: aliResponse.Code, + }, + StatusCode: resp.StatusCode, + }, nil + } + + usage := dto.Usage{ + PromptTokens: aliResponse.Usage.TotalTokens, + CompletionTokens: 0, + TotalTokens: aliResponse.Usage.TotalTokens, + } + rerankResponse := dto.RerankResponse{ + Results: aliResponse.Output.Results, + Usage: usage, + } + + jsonResponse, err := json.Marshal(rerankResponse) + if err != nil { + return service.OpenAIErrorWrapper(err, "marshal_response_body_failed", http.StatusInternalServerError), nil + } + c.Writer.Header().Set("Content-Type", "application/json") + c.Writer.WriteHeader(resp.StatusCode) + _, err = c.Writer.Write(jsonResponse) + if err != nil { + return service.OpenAIErrorWrapper(err, "write_response_body_failed", http.StatusInternalServerError), nil + } + + return nil, &usage +} diff --git a/web/src/components/auth/OAuth2Callback.js b/web/src/components/auth/OAuth2Callback.js index 83f9db46..6d0bbe70 100644 --- a/web/src/components/auth/OAuth2Callback.js +++ b/web/src/components/auth/OAuth2Callback.js @@ -1,15 +1,16 @@ import React, { useContext, useEffect, useState } from 'react'; -import { Spin, Typography, Space } from '@douyinfe/semi-ui'; import { useNavigate, useSearchParams } from 'react-router-dom'; +import { useTranslation } from 'react-i18next'; import { API, showError, showSuccess, updateAPI, setUserData } from '../../helpers'; import { UserContext } from '../../context/User'; +import Loading from '../common/Loading'; const OAuth2Callback = (props) => { + const { t } = useTranslation(); const [searchParams, setSearchParams] = useSearchParams(); const [userState, userDispatch] = useContext(UserContext); - const [prompt, setPrompt] = useState('处理中...'); - const [processing, setProcessing] = useState(true); + const [prompt, setPrompt] = useState(t('处理中...')); let navigate = useNavigate(); @@ -20,25 +21,25 @@ const OAuth2Callback = (props) => { const { success, message, data } = res.data; if (success) { if (message === 'bind') { - showSuccess('绑定成功!'); - navigate('/setting'); + showSuccess(t('绑定成功!')); + navigate('/console/setting'); } else { userDispatch({ type: 'login', payload: data }); localStorage.setItem('user', JSON.stringify(data)); setUserData(data); updateAPI(); - showSuccess('登录成功!'); - navigate('/token'); + showSuccess(t('登录成功!')); + navigate('/console/token'); } } else { showError(message); if (count === 0) { - setPrompt(`操作失败,重定向至登录界面中...`); - navigate('/setting'); // in case this is failed to bind GitHub + setPrompt(t('操作失败,重定向至登录界面中...')); + navigate('/console/setting'); // in case this is failed to bind GitHub return; } count++; - setPrompt(`出现错误,第 ${count} 次重试中...`); + setPrompt(t('出现错误,第 ${count} 次重试中...', { count })); await new Promise((resolve) => setTimeout(resolve, count * 2000)); await sendCode(code, state, count); } @@ -50,17 +51,7 @@ const OAuth2Callback = (props) => { sendCode(code, state, 0).then(); }, []); - return ( -
- - -
- {prompt} -
-
-
-
- ); + return ; }; export default OAuth2Callback; diff --git a/web/src/components/auth/PasswordResetConfirm.js b/web/src/components/auth/PasswordResetConfirm.js index dc69ccdc..025161ac 100644 --- a/web/src/components/auth/PasswordResetConfirm.js +++ b/web/src/components/auth/PasswordResetConfirm.js @@ -1,8 +1,8 @@ import React, { useEffect, useState } from 'react'; import { API, copy, showError, showNotice, getLogo, getSystemName } from '../../helpers'; import { useSearchParams, Link } from 'react-router-dom'; -import { Button, Card, Form, Typography } from '@douyinfe/semi-ui'; -import { IconMail, IconLock } from '@douyinfe/semi-icons'; +import { Button, Card, Form, Typography, Banner } from '@douyinfe/semi-ui'; +import { IconMail, IconLock, IconCopy } from '@douyinfe/semi-icons'; import { useTranslation } from 'react-i18next'; import Background from '/example.png'; @@ -15,13 +15,14 @@ const PasswordResetConfirm = () => { token: '', }); const { email, token } = inputs; + const isValidResetLink = email && token; const [loading, setLoading] = useState(false); const [disableButton, setDisableButton] = useState(false); const [countdown, setCountdown] = useState(30); const [newPassword, setNewPassword] = useState(''); - const [searchParams, setSearchParams] = useSearchParams(); + const [formApi, setFormApi] = useState(null); const logo = getLogo(); const systemName = getSystemName(); @@ -30,10 +31,16 @@ const PasswordResetConfirm = () => { let token = searchParams.get('token'); let email = searchParams.get('email'); setInputs({ - token, - email, + token: token || '', + email: email || '', }); - }, []); + if (formApi) { + formApi.setValues({ + email: email || '', + newPassword: newPassword || '' + }); + } + }, [searchParams, newPassword, formApi]); useEffect(() => { let countdownInterval = null; @@ -49,7 +56,10 @@ const PasswordResetConfirm = () => { }, [disableButton, countdown]); async function handleSubmit(e) { - if (!email || !token) return; + if (!email || !token) { + showError(t('无效的重置链接,请重新发起密码重置请求')); + return; + } setDisableButton(true); setLoading(true); const res = await API.post(`/api/user/reset`, { @@ -61,7 +71,7 @@ const PasswordResetConfirm = () => { let password = res.data.data; setNewPassword(password); await copy(password); - showNotice(`${t('密码已重置并已复制到剪贴板')}: ${password}`); + showNotice(`${t('密码已重置并已复制到剪贴板:')} ${password}`); } else { showError(message); } @@ -94,16 +104,28 @@ const PasswordResetConfirm = () => { {t('密码重置确认')}
-
+ {!isValidResetLink && ( + + )} + setFormApi(api)} + initValues={{ email: email || '', newPassword: newPassword || '' }} + className="space-y-4" + > } + placeholder={email ? '' : t('等待获取邮箱信息...')} /> {newPassword && ( @@ -113,14 +135,21 @@ const PasswordResetConfirm = () => { name="newPassword" size="large" className="!rounded-md" - value={newPassword} - readOnly + disabled={true} prefix={} - onClick={(e) => { - e.target.select(); - navigator.clipboard.writeText(newPassword); - showNotice(`${t('密码已复制到剪贴板')}: ${newPassword}`); - }} + suffix={ + + } /> )} @@ -133,9 +162,9 @@ const PasswordResetConfirm = () => { size="large" onClick={handleSubmit} loading={loading} - disabled={disableButton || newPassword} + disabled={disableButton || newPassword || !isValidResetLink} > - {newPassword ? t('密码重置完成') : t('提交')} + {newPassword ? t('密码重置完成') : t('确认重置密码')}
diff --git a/web/src/components/auth/PasswordResetForm.js b/web/src/components/auth/PasswordResetForm.js index 6abdfaa5..4ff7882f 100644 --- a/web/src/components/auth/PasswordResetForm.js +++ b/web/src/components/auth/PasswordResetForm.js @@ -55,7 +55,10 @@ const PasswordResetForm = () => { } async function handleSubmit(e) { - if (!email) return; + if (!email) { + showError(t('请输入邮箱地址')); + return; + } if (turnstileEnabled && turnstileToken === '') { showInfo(t('请稍后几秒重试,Turnstile 正在检查用户环境!')); return; diff --git a/web/src/components/common/Loading.js b/web/src/components/common/Loading.js index 980e9cb1..a12be053 100644 --- a/web/src/components/common/Loading.js +++ b/web/src/components/common/Loading.js @@ -14,7 +14,7 @@ const Loading = ({ prompt: name = '', size = 'large' }) => { tip={null} /> - {name ? t('加载{{name}}中...', { name }) : t('加载中...')} + {name ? t('{{name}}', { name }) : t('加载中...')} diff --git a/web/src/components/layout/Footer.js b/web/src/components/layout/Footer.js index d53ae2ed..4f44c1dc 100644 --- a/web/src/components/layout/Footer.js +++ b/web/src/components/layout/Footer.js @@ -40,36 +40,36 @@ const FooterBar = () => {

{t('关于我们')}

- {t('关于项目')} - {t('联系我们')} - {t('功能特性')} + {t('关于项目')} + {t('联系我们')} + {t('功能特性')}

{t('文档')}

- {t('快速开始')} - {t('安装指南')} - {t('API 文档')} + {t('快速开始')} + {t('安装指南')} + {t('API 文档')}

{t('相关项目')}

- One API - Midjourney-Proxy - chatnio - neko-api-key-tool + One API + Midjourney-Proxy + chatnio + neko-api-key-tool

{t('基于New API的项目')}

- new-api-horizon - {/* VoAPI */} + new-api-horizon + {/* VoAPI */}
@@ -81,14 +81,12 @@ const FooterBar = () => { © {currentYear} {systemName}. {t('版权所有')} - {isDemoSiteMode && ( -
- {t('设计与开发由')} - Douyin FE - & - QuantumNous -
- )} +
+ {t('设计与开发由')} + New API + & + One API +
), [logo, systemName, t, currentYear, isDemoSiteMode]); diff --git a/web/src/i18n/locales/en.json b/web/src/i18n/locales/en.json index 29469d14..520e30c2 100644 --- a/web/src/i18n/locales/en.json +++ b/web/src/i18n/locales/en.json @@ -265,10 +265,15 @@ "设置页脚": "Set Footer", "新版本": "New Version", "关闭": "Close", - "密码已重置并已复制到剪贴板": "Password has been reset and copied to clipboard", + "密码已重置并已复制到剪贴板:": "Password has been reset and copied to clipboard: ", + "密码已复制到剪贴板:": "Password has been copied to clipboard: ", "密码重置确认": "Password Reset Confirmation", "邮箱地址": "Email address", "提交": "Submit", + "等待获取邮箱信息...": "Waiting to get email information...", + "确认重置密码": "Confirm Password Reset", + "无效的重置链接,请重新发起密码重置请求": "Invalid reset link, please initiate a new password reset request", + "请输入邮箱地址": "Please enter the email address", "请稍后几秒重试": "Please retry in a few seconds", "正在检查用户环境": "Checking user environment", "重置邮件发送成功": "Reset mail sent successfully", @@ -1404,8 +1409,13 @@ "演示站点": "Demo Site", "页面未找到,请检查您的浏览器地址是否正确": "Page not found, please check if your browser address is correct", "New API项目仓库地址:": "New API project repository address: ", - "NewAPI © {{currentYear}} QuantumNous | 基于 One API v0.5.4 © 2023 JustSong。": "NewAPI © {{currentYear}} QuantumNous | Based on One API v0.5.4 © 2023 JustSong.", - "本项目根据MIT许可证授权,需在遵守Apache-2.0协议的前提下使用。": "This project is licensed under the MIT License and must be used in compliance with the Apache-2.0 License.", + "© {{currentYear}}": "© {{currentYear}}", + "| 基于": " | Based on ", + "MIT许可证": "MIT License", + "Apache-2.0协议": "Apache-2.0 License", + "本项目根据": "This project is licensed under the ", + "授权,需在遵守": " and must be used in compliance with the ", + "的前提下使用。": ".", "管理员暂时未设置任何关于内容": "The administrator has not set any custom About content yet", "早上好": "Good morning", "中午好": "Good afternoon", @@ -1531,6 +1541,7 @@ "关闭公告": "Close Notice", "搜索条件": "Search Conditions", "加载中...": "Loading...", + "正在跳转...": "Redirecting...", "暂无公告": "No Notice", "操练场": "Playground", "欢迎使用,请完成以下设置以开始使用系统": "Welcome to use, please complete the following settings to start using the system", diff --git a/web/src/pages/About/index.js b/web/src/pages/About/index.js index 42ac09e0..3259449e 100644 --- a/web/src/pages/About/index.js +++ b/web/src/pages/About/index.js @@ -3,7 +3,6 @@ import { API, showError } from '../../helpers'; import { marked } from 'marked'; import { Empty } from '@douyinfe/semi-ui'; import { IllustrationConstruction, IllustrationConstructionDark } from '@douyinfe/semi-illustrations'; -import { Link } from 'react-router-dom'; import { useTranslation } from 'react-i18next'; const About = () => { @@ -42,14 +41,65 @@ const About = () => {

{t('可在设置页面设置关于内容,支持 HTML & Markdown')}

{t('New API项目仓库地址:')} - + https://github.com/QuantumNous/new-api - +

- {t('NewAPI © {{currentYear}} QuantumNous | 基于 One API v0.5.4 © 2023 JustSong。', { currentYear })} + + NewAPI + {t('© {{currentYear}}', { currentYear })} + QuantumNous + {t('| 基于')} + One API v0.5.4 + © 2023 + JustSong +

- {t('本项目根据MIT许可证授权,需在遵守Apache-2.0协议的前提下使用。')} + {t('本项目根据')} + + {t('MIT许可证')} + + {t('授权,需在遵守')} + + {t('Apache-2.0协议')} + + {t('的前提下使用。')}

); diff --git a/web/src/pages/Channel/EditChannel.js b/web/src/pages/Channel/EditChannel.js index feed8beb..04dc284b 100644 --- a/web/src/pages/Channel/EditChannel.js +++ b/web/src/pages/Channel/EditChannel.js @@ -846,7 +846,7 @@ const EditChannel = (props) => { className="!rounded-lg font-mono" /> handleInputChange('model_mapping', JSON.stringify(MODEL_MAPPING_EXAMPLE, null, 2))} > {t('填入模板')} @@ -940,7 +940,7 @@ const EditChannel = (props) => { className="!rounded-lg font-mono" /> handleInputChange('other', JSON.stringify(REGION_EXAMPLE, null, 2))} > {t('填入模板')} @@ -1062,7 +1062,7 @@ const EditChannel = (props) => { />
{ handleInputChange( 'setting', @@ -1073,10 +1073,10 @@ const EditChannel = (props) => { {t('填入模板')} { window.open( - 'https://github.com/Calcium-Ion/new-api/blob/main/docs/channel/other_setting.md', + 'https://github.com/QuantumNous/new-api/blob/main/docs/channel/other_setting.md', ); }} > @@ -1146,7 +1146,7 @@ const EditChannel = (props) => { className="!rounded-lg font-mono" /> { handleInputChange( 'status_code_mapping', diff --git a/web/src/pages/Chat/index.js b/web/src/pages/Chat/index.js index bd4e19a1..22c5c1e2 100644 --- a/web/src/pages/Chat/index.js +++ b/web/src/pages/Chat/index.js @@ -1,9 +1,11 @@ -import React, { useEffect } from 'react'; +import React from 'react'; import { useTokenKeys } from '../../hooks/useTokenKeys'; -import { Banner, Layout } from '@douyinfe/semi-ui'; +import { Spin } from '@douyinfe/semi-ui'; import { useParams } from 'react-router-dom'; +import { useTranslation } from 'react-i18next'; const ChatPage = () => { + const { t } = useTranslation(); const { id } = useParams(); const { keys, serverAddress, isLoading } = useTokenKeys(id); @@ -40,12 +42,17 @@ const ChatPage = () => { allow='camera;microphone' /> ) : ( -
- - - - - +
+
+ + + {t('正在跳转...')} + +
); };