Compare commits

..

30 Commits

Author SHA1 Message Date
CaIon
77d5dff0c6 chore: update CI workflows 2025-06-08 03:37:17 +08:00
CaIon
02e43ee12e fix: update import statement for vite-plugin-semi to use default import 2025-06-08 03:35:04 +08:00
CaIon
7bced6b236 chore: update CI workflow to use latest Bun version and adjust build environment variables 2025-06-08 03:28:36 +08:00
CaIon
a0844d5481 chore: update Bun version in CI workflow to 1.2.8 2025-06-08 02:57:44 +08:00
CaIon
d79b9e266e chore: update package.json to replace sse dependency and add trustedDependencies 2025-06-08 02:56:09 +08:00
CaIon
6acfe31ee9 chore: update CI workflows to use Bun for package management across all platforms 2025-06-08 02:45:18 +08:00
CaIon
2c95a7c277 chore: update CI workflows to support manual triggers and rename Docker image workflow 2025-06-08 02:42:27 +08:00
CaIon
7010450f77 chore: remove deprecated Docker image workflow for amd64 2025-06-08 02:39:53 +08:00
CaIon
c9849ecc46 Merge branch 'alpha' 2025-06-08 02:38:49 +08:00
CaIon
5b641a4ead Merge remote-tracking branch 'origin/alpha' into alpha 2025-06-08 02:34:22 +08:00
CaIon
b73af9e88f chore: update CI workflows to use Bun for package management and build process 2025-06-08 02:34:06 +08:00
Calcium-Ion
ed84f937e3 Merge pull request #1177 from QuantumNous/alpha
merge alpha to main
2025-06-08 02:26:06 +08:00
Apple\Apple
6bf8a72011 🔧 fix(auth): add copy button to disabled password input in reset confirmation
- Import IconCopy from semi-icons for copy functionality
- Replace onClick handler with suffix copy button to fix disabled input issue
- Use borderless tertiary button as input suffix for better alignment
- Update notification messages formatting (colon spacing)
- Ensure password copying works even when input field is disabled
2025-06-08 02:23:47 +08:00
Apple\Apple
d3b93196cf chore(PasswordResetConfirm): Improve password reset confirm UI and fix form data binding
- Replace error message div with Semi UI Banner component for better UX
- Add rounded corners to Banner component with !rounded-lg class
- Fix Form.Input not displaying values by implementing proper formApi usage
- Use getFormApi callback to obtain form API instance
- Replace manual value props with formApi.setValues() for dynamic updates
- Set proper initValues for form initialization
- Remove unused Input import and console.log statements
- Clean up debugging code and optimize form state management

This change enhances the visual consistency with Semi Design system
and resolves the issue where email field was not showing URL parameter values.
2025-06-08 01:44:38 +08:00
Apple\Apple
fcc4d0074f 🐛 fix(auth): resolve password reset confirmation display and functionality issues
- Fix input field display issues in password reset confirmation page
  * Replace `readOnly` with `disabled={true}` for proper field state
  * Improve URL parameter parsing and state management
  * Add proper null checks and fallback values

- Enhance user experience and error handling
  * Add validation for invalid reset links
  * Display appropriate error messages and placeholders
  * Add debug logging for troubleshooting
  * Improve button states and loading indicators

- Improve password reset form validation
  * Add proper email input validation with error messages
  * Enhance user feedback for empty email submissions

- Add missing English translations
  * Add i18n support for new UI text strings
  * Ensure proper internationalization coverage

The password reset confirmation page now correctly displays email addresses
from URL parameters and prevents user input as intended. Error handling
has been improved to provide better user guidance when reset links are
invalid or malformed.

Fixes: Password reset input fields showing empty and allowing user input
when they should display email/password and be read-only.
2025-06-08 01:08:03 +08:00
Apple\Apple
cb83a06103 🔖chore(ui): Improve Loading prompt 2025-06-08 00:33:26 +08:00
Calcium-Ion
5018945c71 Merge pull request #1171 from RedwindA/feat/ali-rerank
feat: ali rerank
2025-06-08 00:15:21 +08:00
Calcium-Ion
ce2fba7f8b Merge pull request #1173 from RedwindA/fix/ali-embedding
🐛 fix(ali): Remove hardcoding of embedding model names.
2025-06-08 00:14:55 +08:00
Calcium-Ion
2b898bc577 Merge pull request #1175 from RedwindA/fix/mistral-tool-id
🐛 fix: 适应Mistral的tool call格式要求
2025-06-08 00:14:23 +08:00
CaIon
017fa70e1a Merge remote-tracking branch 'origin/alpha' into alpha 2025-06-08 00:11:28 +08:00
CaIon
5f52148e4e chore: update bun.lockb file 2025-06-08 00:10:54 +08:00
Apple\Apple
7e9bd35ac7 ♻️ refactor(auth): replace custom loading UI with shared Loading component and add i18n support
- Replace inline loading UI in OAuth2Callback with shared Loading component
- Add internationalization support using useTranslation hook
- Translate all hardcoded Chinese strings to support multiple languages
- Remove unused processing state variable
- Maintain consistent loading experience across the application
- Support dynamic text content for retry attempts with parameter interpolation
2025-06-08 00:07:37 +08:00
RedwindA
d124ec5b1a 🐛 fix(mistral): validate and generate new IDs for tool calls and tool call IDs; Correctly handle null content for assistant messages with tool_calls. 2025-06-08 00:06:56 +08:00
Apple\Apple
6e7249cf06 🎨feat(ui): Improve Chat page UI and add i18n support
- Replace Banner with full-screen Spin component for better loading UX
- Add English translation for "正在跳转..." ("Redirecting...")
- Integrate i18next translation hook in Chat page component
- Remove unused useEffect import for cleaner code

The Chat page now shows a centered full-screen loading spinner instead of
a banner when redirecting, providing a more consistent and professional
user experience. The loading text is now properly internationalized and
will display "Redirecting..." in English and "正在跳转..." in Chinese.
2025-06-07 23:22:25 +08:00
Apple\Apple
33014e9399 🔗feat(ui): Standardize link colors and update documentation URL in EditChannel component
**Changes:**
- Unify link color styling across EditChannel.js by replacing `text-blue-500` with consistent primary color scheme
- Apply `!text-semi-color-primary hover:!text-semi-color-primary-hover transition-colors` to all template fill and documentation links
- Update documentation URL from Calcium-Ion repository to QuantumNous repository
- Add smooth hover transitions and consistent visual feedback for all clickable links

**Affected Elements:**
- Model mapping template fill link
- Deployment region template fill link
- Channel settings template fill link
- Channel settings documentation link
- Status code mapping template fill link

**Benefits:**
- Consistent visual design language across the entire application
- Improved user experience with unified link styling
- Better accessibility with clear hover states and transitions
- Correct documentation references pointing to the current project repository

**Technical Details:**
- Maintains existing functionality while improving visual consistency
- Links now match the color scheme used in About page and Footer components
- Smooth color transitions enhance user interaction feedback
2025-06-07 23:15:25 +08:00
Apple\Apple
387721e907 🔗feat(ui): Enhance About page with interactive project links and improve external link handling 2025-06-07 22:55:12 +08:00
Apple\Apple
e0cc13094f 🔗feat(ui): Enhance About page with interactive project links and improve external link handling
**Changes:**
- Replace React Router `Link` components with native `<a>` tags for external links in About and Footer components
- Add clickable links for "NewAPI", "QuantumNous", and "One API v0.5.4" in the About page
- Link "NewAPI" to the main project repository (https://github.com/QuantumNous/new-api)
- Link "QuantumNous" to the organization page (https://github.com/QuantumNous)
- Link "One API v0.5.4" to the specific release page (https://github.com/songquanpeng/one-api/releases/tag/v0.5.4)
- Apply consistent styling with primary color theme and hover effects across all links
- Add proper security attributes (`rel="noopener noreferrer"`) to all external links

**i18n Updates:**
- Refactor i18n translation keys to support the new link structure
- Split the original copyright string into smaller, reusable translation keys
- Add new translation keys: `"© {{currentYear}}"` and `"| 基于"`
- Maintain backward compatibility for existing translations

**Benefits:**
- Improved user experience with direct access to relevant project resources
- Better SEO and link accessibility
- Consistent visual styling across all external links
- Enhanced security for external link navigation
- Proper separation of concerns between internal routing and external navigation
2025-06-07 22:50:31 +08:00
RedwindA
5dc3543e41 Merge remote-tracking branch 'upstream/main' into fix/ali-embedding 2025-06-07 22:32:02 +08:00
RedwindA
f1f07cb31b 🐛 fix(ali): Remove hardcoding of embedding model names. 2025-06-07 22:28:32 +08:00
RedwindA
49e77fb3df feat: ali rerank 2025-06-07 21:29:46 +08:00
24 changed files with 377 additions and 179 deletions

View File

@@ -1,54 +0,0 @@
name: Publish Docker image (amd64)
on:
push:
tags:
- '*'
workflow_dispatch:
inputs:
name:
description: 'reason'
required: false
jobs:
push_to_registries:
name: Push Docker image to multiple registries
runs-on: ubuntu-latest
permissions:
packages: write
contents: read
steps:
- name: Check out the repo
uses: actions/checkout@v4
- name: Save version info
run: |
git describe --tags > VERSION
- name: Log in to Docker Hub
uses: docker/login-action@v3
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Log in to the Container registry
uses: docker/login-action@v3
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Extract metadata (tags, labels) for Docker
id: meta
uses: docker/metadata-action@v5
with:
images: |
calciumion/new-api
ghcr.io/${{ github.repository }}
- name: Build and push Docker images
uses: docker/build-push-action@v5
with:
context: .
push: true
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}

View File

@@ -1,14 +1,9 @@
name: Publish Docker image (arm64)
name: Publish Docker image (Multi Registries)
on:
push:
tags:
- '*'
workflow_dispatch:
inputs:
name:
description: 'reason'
required: false
jobs:
push_to_registries:
name: Push Docker image to multiple registries

View File

@@ -3,6 +3,11 @@ permissions:
contents: write
on:
workflow_dispatch:
inputs:
name:
description: 'reason'
required: false
push:
tags:
- '*'
@@ -15,16 +20,16 @@ jobs:
uses: actions/checkout@v3
with:
fetch-depth: 0
- uses: actions/setup-node@v3
- uses: oven-sh/setup-bun@v2
with:
node-version: 18
bun-version: latest
- name: Build Frontend
env:
CI: ""
run: |
cd web
npm install
REACT_APP_VERSION=$(git describe --tags) npm run build
bun install
DISABLE_ESLINT_PLUGIN='true' VITE_REACT_APP_VERSION=$(git describe --tags) bun run build
cd ..
- name: Set up Go
uses: actions/setup-go@v3

View File

@@ -3,6 +3,11 @@ permissions:
contents: write
on:
workflow_dispatch:
inputs:
name:
description: 'reason'
required: false
push:
tags:
- '*'
@@ -15,16 +20,16 @@ jobs:
uses: actions/checkout@v3
with:
fetch-depth: 0
- uses: actions/setup-node@v3
- uses: oven-sh/setup-bun@v2
with:
node-version: 18
bun-version: latest
- name: Build Frontend
env:
CI: ""
run: |
cd web
npm install
REACT_APP_VERSION=$(git describe --tags) npm run build
bun install
DISABLE_ESLINT_PLUGIN='true' VITE_REACT_APP_VERSION=$(git describe --tags) bun run build
cd ..
- name: Set up Go
uses: actions/setup-go@v3

View File

@@ -3,6 +3,11 @@ permissions:
contents: write
on:
workflow_dispatch:
inputs:
name:
description: 'reason'
required: false
push:
tags:
- '*'
@@ -18,16 +23,16 @@ jobs:
uses: actions/checkout@v3
with:
fetch-depth: 0
- uses: actions/setup-node@v3
- uses: oven-sh/setup-bun@v2
with:
node-version: 18
bun-version: latest
- name: Build Frontend
env:
CI: ""
run: |
cd web
npm install
REACT_APP_VERSION=$(git describe --tags) npm run build
bun install
DISABLE_ESLINT_PLUGIN='true' VITE_REACT_APP_VERSION=$(git describe --tags) bun run build
cd ..
- name: Set up Go
uses: actions/setup-go@v3

View File

@@ -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)

View File

@@ -8,6 +8,7 @@ var ModelList = []string{
"qwq-32b",
"qwen3-235b-a22b",
"text-embedding-v1",
"gte-rerank-v2",
}
var ChannelName = "ali"

View File

@@ -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
}

View File

@@ -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
}

View File

@@ -3,7 +3,6 @@ package ali
import (
"bufio"
"encoding/json"
"github.com/gin-gonic/gin"
"io"
"net/http"
"one-api/common"
@@ -11,6 +10,8 @@ import (
"one-api/relay/helper"
"one-api/service"
"strings"
"github.com/gin-gonic/gin"
)
// https://help.aliyun.com/document_detail/613695.html?spm=a2c4g.2399480.0.0.1adb778fAdzP9w#341800c0f8w0r
@@ -27,9 +28,6 @@ func requestOpenAI2Ali(request dto.GeneralOpenAIRequest) *dto.GeneralOpenAIReque
}
func embeddingRequestOpenAI2Ali(request dto.EmbeddingRequest) *AliEmbeddingRequest {
if request.Model == "" {
request.Model = "text-embedding-v1"
}
return &AliEmbeddingRequest{
Model: request.Model,
Input: struct {
@@ -64,7 +62,11 @@ func aliEmbeddingHandler(c *gin.Context, resp *http.Response) (*dto.OpenAIErrorW
}, nil
}
fullTextResponse := embeddingResponseAli2OpenAI(&aliResponse)
model := c.GetString("model")
if model == "" {
model = "text-embedding-v4"
}
fullTextResponse := embeddingResponseAli2OpenAI(&aliResponse, model)
jsonResponse, err := json.Marshal(fullTextResponse)
if err != nil {
return service.OpenAIErrorWrapper(err, "marshal_response_body_failed", http.StatusInternalServerError), nil
@@ -75,11 +77,11 @@ func aliEmbeddingHandler(c *gin.Context, resp *http.Response) (*dto.OpenAIErrorW
return nil, &fullTextResponse.Usage
}
func embeddingResponseAli2OpenAI(response *AliEmbeddingResponse) *dto.OpenAIEmbeddingResponse {
func embeddingResponseAli2OpenAI(response *AliEmbeddingResponse, model string) *dto.OpenAIEmbeddingResponse {
openAIEmbeddingResponse := dto.OpenAIEmbeddingResponse{
Object: "list",
Data: make([]dto.OpenAIEmbeddingResponseItem, 0, len(response.Output.Embeddings)),
Model: "text-embedding-v1",
Model: model,
Usage: dto.Usage{TotalTokens: response.Usage.TotalTokens},
}

View File

@@ -1,13 +1,55 @@
package mistral
import (
"one-api/common"
"one-api/dto"
"regexp"
)
var mistralToolCallIdRegexp = regexp.MustCompile("^[a-zA-Z0-9]{9}$")
func requestOpenAI2Mistral(request *dto.GeneralOpenAIRequest) *dto.GeneralOpenAIRequest {
messages := make([]dto.Message, 0, len(request.Messages))
idMap := make(map[string]string)
for _, message := range request.Messages {
// 1. tool_calls.id
toolCalls := message.ParseToolCalls()
if toolCalls != nil {
for i := range toolCalls {
if !mistralToolCallIdRegexp.MatchString(toolCalls[i].ID) {
if newId, ok := idMap[toolCalls[i].ID]; ok {
toolCalls[i].ID = newId
} else {
newId, err := common.GenerateRandomCharsKey(9)
if err == nil {
idMap[toolCalls[i].ID] = newId
toolCalls[i].ID = newId
}
}
}
}
message.SetToolCalls(toolCalls)
}
// 2. tool_call_id
if message.ToolCallId != "" {
if newId, ok := idMap[message.ToolCallId]; ok {
message.ToolCallId = newId
} else {
if !mistralToolCallIdRegexp.MatchString(message.ToolCallId) {
newId, err := common.GenerateRandomCharsKey(9)
if err == nil {
idMap[message.ToolCallId] = newId
message.ToolCallId = newId
}
}
}
}
mediaMessages := message.ParseContent()
if message.Role == "assistant" && message.ToolCalls != nil && string(message.Content) == "null" {
mediaMessages = []dto.MediaContent{}
}
for j, mediaMessage := range mediaMessages {
if mediaMessage.Type == dto.ContentTypeImageURL {
imageUrl := mediaMessage.GetImageMedia()

Binary file not shown.

View File

@@ -39,7 +39,7 @@
"remark-math": "^6.0.0",
"semantic-ui-offline": "^2.5.0",
"semantic-ui-react": "^2.1.3",
"sse": "https://github.com/mpetazzoni/sse.js",
"sse.js": "^2.6.0",
"unist-util-visit": "^5.0.0",
"use-debounce": "^10.0.4"
},
@@ -69,7 +69,7 @@
]
},
"devDependencies": {
"@douyinfe/semi-webpack-plugin": "^2.78.0",
"@douyinfe/vite-plugin-semi": "^2.74.0-alpha.6",
"@so1ve/prettier-config": "^3.1.0",
"@vitejs/plugin-react": "^4.2.1",
"autoprefixer": "^10.4.21",

View File

@@ -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 (
<div className="flex items-center justify-center min-h-[300px] w-full bg-white rounded-lg shadow p-6">
<Space vertical align="center">
<Spin size="large" spinning={processing}>
<div className="min-h-[200px] min-w-[200px] flex items-center justify-center">
<Typography.Text type="secondary">{prompt}</Typography.Text>
</div>
</Spin>
</Space>
</div>
);
return <Loading prompt={prompt} />;
};
export default OAuth2Callback;

View File

@@ -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 = () => {
<Title heading={3} className="text-gray-800 dark:text-gray-200">{t('密码重置确认')}</Title>
</div>
<div className="px-2 py-8">
<Form className="space-y-3">
{!isValidResetLink && (
<Banner
type="danger"
description={t('无效的重置链接,请重新发起密码重置请求')}
className="mb-4 !rounded-lg"
closeIcon={null}
/>
)}
<Form
getFormApi={(api) => setFormApi(api)}
initValues={{ email: email || '', newPassword: newPassword || '' }}
className="space-y-4"
>
<Form.Input
field="email"
label={t('邮箱')}
name="email"
size="large"
className="!rounded-md"
value={email}
readOnly
disabled={true}
prefix={<IconMail />}
placeholder={email ? '' : t('等待获取邮箱信息...')}
/>
{newPassword && (
@@ -113,14 +135,21 @@ const PasswordResetConfirm = () => {
name="newPassword"
size="large"
className="!rounded-md"
value={newPassword}
readOnly
disabled={true}
prefix={<IconLock />}
onClick={(e) => {
e.target.select();
navigator.clipboard.writeText(newPassword);
showNotice(`${t('密码已复制到剪贴板')}: ${newPassword}`);
}}
suffix={
<Button
icon={<IconCopy />}
type="tertiary"
theme="borderless"
onClick={async () => {
await copy(newPassword);
showNotice(`${t('密码已复制到剪贴板:')} ${newPassword}`);
}}
>
{t('复制')}
</Button>
}
/>
)}
@@ -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('确认重置密码')}
</Button>
</div>
</Form>

View File

@@ -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;

View File

@@ -14,7 +14,7 @@ const Loading = ({ prompt: name = '', size = 'large' }) => {
tip={null}
/>
<span className="whitespace-nowrap mt-2 text-center" style={{ color: 'var(--semi-color-primary)' }}>
{name ? t('加载{{name}}中...', { name }) : t('加载中...')}
{name ? t('{{name}}', { name }) : t('加载中...')}
</span>
</div>
</div>

View File

@@ -40,36 +40,36 @@ const FooterBar = () => {
<div className="text-left">
<p className="!text-semi-color-text-0 font-semibold mb-5">{t('关于我们')}</p>
<div className="flex flex-col gap-4">
<a href="https://docs.newapi.pro/wiki/project-introduction/" target="_blank" rel="noopener noreferrer" className="!text-semi-color-text-1 hover:!text-semi-color-primary transition-colors">{t('关于项目')}</a>
<a href="https://docs.newapi.pro/support/community-interaction/" target="_blank" rel="noopener noreferrer" className="!text-semi-color-text-1 hover:!text-semi-color-primary transition-colors">{t('联系我们')}</a>
<a href="https://docs.newapi.pro/wiki/features-introduction/" target="_blank" rel="noopener noreferrer" className="!text-semi-color-text-1 hover:!text-semi-color-primary transition-colors">{t('功能特性')}</a>
<a href="https://docs.newapi.pro/wiki/project-introduction/" target="_blank" rel="noopener noreferrer" className="!text-semi-color-text-1">{t('关于项目')}</a>
<a href="https://docs.newapi.pro/support/community-interaction/" target="_blank" rel="noopener noreferrer" className="!text-semi-color-text-1">{t('联系我们')}</a>
<a href="https://docs.newapi.pro/wiki/features-introduction/" target="_blank" rel="noopener noreferrer" className="!text-semi-color-text-1">{t('功能特性')}</a>
</div>
</div>
<div className="text-left">
<p className="!text-semi-color-text-0 font-semibold mb-5">{t('文档')}</p>
<div className="flex flex-col gap-4">
<a href="https://docs.newapi.pro/getting-started/" target="_blank" rel="noopener noreferrer" className="!text-semi-color-text-1 hover:!text-semi-color-primary transition-colors">{t('快速开始')}</a>
<a href="https://docs.newapi.pro/installation/" target="_blank" rel="noopener noreferrer" className="!text-semi-color-text-1 hover:!text-semi-color-primary transition-colors">{t('安装指南')}</a>
<a href="https://docs.newapi.pro/api/" target="_blank" rel="noopener noreferrer" className="!text-semi-color-text-1 hover:!text-semi-color-primary transition-colors">{t('API 文档')}</a>
<a href="https://docs.newapi.pro/getting-started/" target="_blank" rel="noopener noreferrer" className="!text-semi-color-text-1">{t('快速开始')}</a>
<a href="https://docs.newapi.pro/installation/" target="_blank" rel="noopener noreferrer" className="!text-semi-color-text-1">{t('安装指南')}</a>
<a href="https://docs.newapi.pro/api/" target="_blank" rel="noopener noreferrer" className="!text-semi-color-text-1">{t('API 文档')}</a>
</div>
</div>
<div className="text-left">
<p className="!text-semi-color-text-0 font-semibold mb-5">{t('相关项目')}</p>
<div className="flex flex-col gap-4">
<a href="https://github.com/songquanpeng/one-api" target="_blank" rel="noopener noreferrer" className="!text-semi-color-text-1 hover:!text-semi-color-primary transition-colors">One API</a>
<a href="https://github.com/novicezk/midjourney-proxy" target="_blank" rel="noopener noreferrer" className="!text-semi-color-text-1 hover:!text-semi-color-primary transition-colors">Midjourney-Proxy</a>
<a href="https://github.com/Deeptrain-Community/chatnio" target="_blank" rel="noopener noreferrer" className="!text-semi-color-text-1 hover:!text-semi-color-primary transition-colors">chatnio</a>
<a href="https://github.com/Calcium-Ion/neko-api-key-tool" target="_blank" rel="noopener noreferrer" className="!text-semi-color-text-1 hover:!text-semi-color-primary transition-colors">neko-api-key-tool</a>
<a href="https://github.com/songquanpeng/one-api" target="_blank" rel="noopener noreferrer" className="!text-semi-color-text-1">One API</a>
<a href="https://github.com/novicezk/midjourney-proxy" target="_blank" rel="noopener noreferrer" className="!text-semi-color-text-1">Midjourney-Proxy</a>
<a href="https://github.com/Deeptrain-Community/chatnio" target="_blank" rel="noopener noreferrer" className="!text-semi-color-text-1">chatnio</a>
<a href="https://github.com/Calcium-Ion/neko-api-key-tool" target="_blank" rel="noopener noreferrer" className="!text-semi-color-text-1">neko-api-key-tool</a>
</div>
</div>
<div className="text-left">
<p className="!text-semi-color-text-0 font-semibold mb-5">{t('基于New API的项目')}</p>
<div className="flex flex-col gap-4">
<a href="https://github.com/Calcium-Ion/new-api-horizon" target="_blank" rel="noopener noreferrer" className="!text-semi-color-text-1 hover:!text-semi-color-primary transition-colors">new-api-horizon</a>
{/* <a href="https://github.com/VoAPI/VoAPI" target="_blank" rel="noopener noreferrer" className="!text-semi-color-text-1 hover:!text-semi-color-primary transition-colors">VoAPI</a> */}
<a href="https://github.com/Calcium-Ion/new-api-horizon" target="_blank" rel="noopener noreferrer" className="!text-semi-color-text-1">new-api-horizon</a>
{/* <a href="https://github.com/VoAPI/VoAPI" target="_blank" rel="noopener noreferrer" className="!text-semi-color-text-1">VoAPI</a> */}
</div>
</div>
</div>
@@ -81,14 +81,12 @@ const FooterBar = () => {
<Typography.Text className="text-sm !text-semi-color-text-1">© {currentYear} {systemName}. {t('版权所有')}</Typography.Text>
</div>
{isDemoSiteMode && (
<div className="text-sm">
<span className="!text-semi-color-text-1">{t('设计与开发由')} </span>
<span className="!text-semi-color-primary">Douyin FE</span>
<span className="!text-semi-color-text-1"> & </span>
<a href="https://github.com/QuantumNous" target="_blank" rel="noreferrer" className="!text-semi-color-primary hover:!text-semi-color-primary-hover transition-colors">QuantumNous</a>
</div>
)}
<div className="text-sm">
<span className="!text-semi-color-text-1">{t('设计与开发由')} </span>
<a href="https://github.com/QuantumNous/new-api" target="_blank" rel="noopener noreferrer" className="!text-semi-color-primary font-medium">New API</a>
<span className="!text-semi-color-text-1"> & </span>
<a href="https://github.com/songquanpeng/one-api" target="_blank" rel="noopener noreferrer" className="!text-semi-color-primary font-medium">One API</a>
</div>
</div>
</footer>
), [logo, systemName, t, currentYear, isDemoSiteMode]);

View File

@@ -1,6 +1,6 @@
import { useCallback } from 'react';
import { useTranslation } from 'react-i18next';
import { SSE } from 'sse';
import { SSE } from 'sse.js';
import {
API_ENDPOINTS,
MESSAGE_STATUS,

View File

@@ -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",

View File

@@ -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 = () => {
<div style={{ textAlign: 'center' }}>
<p>{t('可在设置页面设置关于内容,支持 HTML & Markdown')}</p>
{t('New API项目仓库地址')}
<Link to='https://github.com/QuantumNous/new-api' target="_blank">
<a
href='https://github.com/QuantumNous/new-api'
target="_blank"
rel="noopener noreferrer"
className="!text-semi-color-primary"
>
https://github.com/QuantumNous/new-api
</Link>
</a>
<p>
{t('NewAPI © {{currentYear}} QuantumNous | 基于 One API v0.5.4 © 2023 JustSong。', { currentYear })}
<a
href="https://github.com/QuantumNous/new-api"
target="_blank"
rel="noopener noreferrer"
className="!text-semi-color-primary"
>
NewAPI
</a> {t('© {{currentYear}}', { currentYear })} <a
href="https://github.com/QuantumNous"
target="_blank"
rel="noopener noreferrer"
className="!text-semi-color-primary"
>
QuantumNous
</a> {t('| ')} <a
href="https://github.com/songquanpeng/one-api/releases/tag/v0.5.4"
target="_blank"
rel="noopener noreferrer"
className="!text-semi-color-primary"
>
One API v0.5.4
</a> © 2023 <a
href="https://github.com/songquanpeng"
target="_blank"
rel="noopener noreferrer"
className="!text-semi-color-primary"
>
JustSong
</a>
</p>
<p>
{t('本项目根据MIT许可证授权需在遵守Apache-2.0协议的前提下使用。')}
{t('本项目根据')}
<a
href="https://github.com/songquanpeng/one-api/blob/v0.5.4/LICENSE"
target="_blank"
rel="noopener noreferrer"
className="!text-semi-color-primary"
>
{t('MIT许可证')}
</a>
{t('授权,需在遵守')}
<a
href="https://github.com/QuantumNous/new-api/blob/main/LICENSE"
target="_blank"
rel="noopener noreferrer"
className="!text-semi-color-primary"
>
{t('Apache-2.0协议')}
</a>
{t('的前提下使用。')}
</p>
</div>
);

View File

@@ -846,7 +846,7 @@ const EditChannel = (props) => {
className="!rounded-lg font-mono"
/>
<Text
className="text-blue-500 cursor-pointer mt-1 block"
className="!text-semi-color-primary cursor-pointer mt-1 block"
onClick={() => handleInputChange('model_mapping', JSON.stringify(MODEL_MAPPING_EXAMPLE, null, 2))}
>
{t('填入模板')}
@@ -940,7 +940,7 @@ const EditChannel = (props) => {
className="!rounded-lg font-mono"
/>
<Text
className="text-blue-500 cursor-pointer mt-1 block"
className="!text-semi-color-primary cursor-pointer mt-1 block"
onClick={() => handleInputChange('other', JSON.stringify(REGION_EXAMPLE, null, 2))}
>
{t('填入模板')}
@@ -1062,7 +1062,7 @@ const EditChannel = (props) => {
/>
<div className="flex gap-2 mt-1">
<Text
className="text-blue-500 cursor-pointer"
className="!text-semi-color-primary cursor-pointer"
onClick={() => {
handleInputChange(
'setting',
@@ -1073,10 +1073,10 @@ const EditChannel = (props) => {
{t('填入模板')}
</Text>
<Text
className="text-blue-500 cursor-pointer"
className="!text-semi-color-primary cursor-pointer"
onClick={() => {
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"
/>
<Text
className="text-blue-500 cursor-pointer mt-1 block"
className="!text-semi-color-primary cursor-pointer mt-1 block"
onClick={() => {
handleInputChange(
'status_code_mapping',

View File

@@ -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'
/>
) : (
<div>
<Layout>
<Layout.Header>
<Banner description={'正在跳转......'} type={'warning'} />
</Layout.Header>
</Layout>
<div className="fixed inset-0 w-screen h-screen flex items-center justify-center bg-white/80 z-[1000]">
<div className="flex flex-col items-center">
<Spin
size="large"
spinning={true}
tip={null}
/>
<span className="whitespace-nowrap mt-2 text-center" style={{ color: 'var(--semi-color-primary)' }}>
{t('正在跳转...')}
</span>
</div>
</div>
);
};

View File

@@ -1,6 +1,7 @@
import react from '@vitejs/plugin-react';
import { defineConfig, transformWithEsbuild } from 'vite';
import semiWebpackPlugin from '@douyinfe/semi-webpack-plugin';
import pkg from '@douyinfe/vite-plugin-semi';
const { vitePluginSemi } = pkg;
// https://vitejs.dev/config/
export default defineConfig({
@@ -21,16 +22,9 @@ export default defineConfig({
},
},
react(),
{
name: 'semi-plugin',
apply: 'build',
configResolved() {
// Apply SemiWebpackPlugin during build
new semiWebpackPlugin({
cssLayer: true
});
}
}
vitePluginSemi({
cssLayer: true
})
],
optimizeDeps: {
force: true,