Compare commits

..

70 Commits

Author SHA1 Message Date
Seefs
aacdc395c8 Merge pull request #2013 from seefs001/fix/ci
feat: matrix ci
2025-10-11 13:47:54 +08:00
Seefs
37cc5d245e ci 2025-10-11 13:47:14 +08:00
Seefs
2842ae52c7 Merge pull request #2010 from seefs001/fix/ci
feat: matrix ci
2025-10-11 13:12:55 +08:00
Seefs
bea8c57f1d fix 2025-10-11 13:11:46 +08:00
Seefs
e603186dfa Merge pull request #2009 from seefs001/fix/ci
feat: matrix ci
2025-10-11 13:11:09 +08:00
Seefs
a8a0da5e3e fix 2025-10-11 13:10:06 +08:00
Seefs
4e0c911a06 Merge pull request #2008 from seefs001/fix/ci
feat: matrix ci
2025-10-11 13:02:37 +08:00
Seefs
24bc24abaa feat: matrix ci 2025-10-11 13:01:50 +08:00
Seefs
c948f85ee8 Merge pull request #2007 from QuantumNous/main
alpha -> mian
2025-10-11 13:00:17 +08:00
CaIon
0ff98b1dc1 feat: update Go version in CI configuration and add release workflow for multi-platform builds 2025-10-11 12:44:09 +08:00
CaIon
5d4a0757f7 fix: ensure error message is set when it is empty in error handling #1972 2025-10-11 12:12:41 +08:00
CaIon
07b099006c feat: add logging for model details and enhance action assignment in relay tasks 2025-10-11 11:56:44 +08:00
CaIon
5fbf860020 feat: enhance multipart validation with additional fields for model, seconds, and size 2025-10-11 11:33:59 +08:00
Calcium-Ion
eab768b4a0 Merge pull request #2006 from xyfacai/feat/sora-price
feat: sora 增加参数校验与计费
2025-10-11 11:22:08 +08:00
Calcium-Ion
1031f1ddf0 Merge pull request #2004 from feitianbubu/pr/openai-sdk-kling
支持可灵使用openai sdk生成视频
2025-10-11 11:05:11 +08:00
feitianbubu
5f36e32821 feat: add openai sdk create 2025-10-11 02:44:01 +08:00
feitianbubu
11e8e4e7a6 feat: add openai sdk for kling 2025-10-11 02:43:56 +08:00
feitianbubu
35422b316d refactor: openAI video use OpenAIVideoConverter 2025-10-11 02:43:43 +08:00
Seefs
df0ae9294d Merge pull request #2002 from qixing-jk/fix/dynamic-frequency-updates
fix(channel): handle dynamic frequency updates
2025-10-11 01:01:23 +08:00
anime
57e5d67f86 fix(channel): move log statement after sleep in auto-test loop 2025-10-11 00:59:13 +08:00
anime
7351480365 fix(channel): handle dynamic frequency updates 2025-10-11 00:53:26 +08:00
anime
e19e904179 fix(channel): handle dynamic frequency updates
- replace infinite sleep loop with time.Ticker to avoid goroutine leaks
- add immediate initial test execution before ticker starts
- implement frequency change detection and ticker recreation
- ensure proper ticker cleanup when loop exits or feature disabled
2025-10-11 00:34:15 +08:00
Xyfacai
a54baf4998 feat: sora 增加参数校验与计费 2025-10-10 23:56:36 +08:00
Seefs
721357b4a4 Merge pull request #2000 from feitianbubu/pr/sora-retrieve-video-sdk
feat: support openAI sdk retrieve videos
2025-10-10 19:18:27 +08:00
feitianbubu
ff9f9fbbc9 feat: support openAI sdk retrieve videos 2025-10-10 18:59:52 +08:00
CaIon
9b551d978d feat: add informational banner to proxy settings in SystemSetting.jsx 2025-10-10 16:44:41 +08:00
Seefs
76ab8a480a Merge pull request #1401 from feitianbubu/pr/add-qwen-channel-auto-disabled
feat: add qwen channel auto disabled
2025-10-10 16:41:43 +08:00
Calcium-Ion
f091f663c2 Merge pull request #1998 from seefs001/feature/pplx-channel
feat: pplx channel
2025-10-10 16:33:27 +08:00
Seefs
e8966c7374 feat: pplx channel 2025-10-10 16:12:15 +08:00
Calcium-Ion
5a7f498629 Merge pull request #1997 from feitianbubu/pr/add-sora-fetch-task
支持Sora做为上游渠道
2025-10-10 16:10:58 +08:00
feitianbubu
4c1f138c0a fix: sora fetch task route 2025-10-10 16:01:06 +08:00
Calcium-Ion
f4d7bde20b Merge pull request #1996 from seefs001/fix/remark-ignore
fix: channel remark ignore issue
2025-10-10 15:41:30 +08:00
Calcium-Ion
0c181395b4 Merge pull request #1992 from seefs001/pr-upstream-1981
feat(web): add settings & pages of privacy policy & user agreement
2025-10-10 15:41:06 +08:00
Seefs
6897a9ffd8 fix: channel remark ignore issue 2025-10-10 15:40:00 +08:00
Calcium-Ion
77130dfb87 Merge commit from fork
feat: enhance HTTP client wit redirect handling and SSRF protection
2025-10-10 15:30:37 +08:00
Calcium-Ion
614abc3441 Merge pull request #1995 from seefs001/fix/test-channel-1993
fix: test model #1993
2025-10-10 15:26:18 +08:00
feitianbubu
2479da4986 feat: add sora video fetch task 2025-10-10 15:25:29 +08:00
CaIon
7b732ec4b7 feat: enhance HTTP client with custom redirect handling and SSRF protection 2025-10-10 15:13:41 +08:00
Seefs
0fed791ad9 fix: test model #1993 2025-10-10 15:01:24 +08:00
Seefs
7de02991a1 Merge pull request #1994 from feitianbubu/pr/fix-video-model
fix: avoid get model consuming body
2025-10-10 14:28:16 +08:00
feitianbubu
3c57cfbf71 fix: avoid get model consuming body 2025-10-10 14:19:49 +08:00
Seefs
fe9b305232 fix: legal setting 2025-10-10 13:18:26 +08:00
キュビビイ
17dafa3b03 feat: add user agreement and privacy policy to login page 2025-10-09 22:21:56 +08:00
Seefs
5f5b9425df Merge pull request #1988 from feitianbubu/pr/add-sora
新增Sora视频渠道
2025-10-09 15:37:31 +08:00
feitianbubu
b880094296 feat: add sora video proxy video content 2025-10-09 15:00:02 +08:00
feitianbubu
9c37b63f2e feat: add sora video retrieve task 2025-10-09 15:00:02 +08:00
feitianbubu
9f4a2d64a3 feat: add sora video submit task 2025-10-09 15:00:02 +08:00
CaIon
e24f13a277 fix: update axios package version to 1.12.0 2025-10-09 14:21:49 +08:00
Calcium-Ion
d67c57eaa5 Merge pull request #1986 from QuantumNous/dependabot/npm_and_yarn/electron/electron-35.7.5
chore(deps-dev): bump electron from 28.3.3 to 35.7.5 in /electron
2025-10-09 14:18:42 +08:00
CaIon
60dc910a27 fix: update jwt package import to v5 across multiple files 2025-10-09 14:17:49 +08:00
dependabot[bot]
629a534798 chore(deps-dev): bump electron from 28.3.3 to 35.7.5 in /electron
Bumps [electron](https://github.com/electron/electron) from 28.3.3 to 35.7.5.
- [Release notes](https://github.com/electron/electron/releases)
- [Changelog](https://github.com/electron/electron/blob/main/docs/breaking-changes.md)
- [Commits](https://github.com/electron/electron/compare/v28.3.3...v35.7.5)

---
updated-dependencies:
- dependency-name: electron
  dependency-version: 35.7.5
  dependency-type: direct:development
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-10-09 06:07:32 +00:00
Seefs
15a7edf6d6 Merge pull request #1982 from RedwindA/feat/zhipu_cache
fix(openai): account cached tokens for zhipu_v4 usage
2025-10-09 12:27:11 +08:00
Seefs
cdd2eb517e Merge pull request #1984 from RedwindA/feat/claude-fetchModels
feat: add GetClaudeAuthHeader function and update FetchUpstreamModels to support Anthropic channel type
2025-10-09 12:22:47 +08:00
Seefs
1a398bbc40 Merge pull request #1983 from Sh1n3zZ/feat-imagen-related
feat: gemini imagen quality value
2025-10-09 12:22:02 +08:00
RedwindA
581c51f312 feat: add GetClaudeAuthHeader function and update FetchUpstreamModels to support Anthropic channel type 2025-10-09 02:43:19 +08:00
Sh1n3zZ
8f00af181b feat: gemini imagen quality value 2025-10-09 01:16:04 +08:00
CaIon
0c417e8ec6 feat: update tab label in index.jsx for clarity on pricing settings 2025-10-08 20:56:51 +08:00
RedwindA
f930cdbb51 fix(openai): account cached tokens for
zhipu_v4 usage
2025-10-08 16:52:49 +08:00
キュビビイ
4d0a9d9494 feat: componentize User Agreement and Privacy Policy display
Extracted the User Agreement and Privacy Policy presentation into a
reusable DocumentRenderer component (web/src/components/common/DocumentRenderer).
Unified rendering logic and i18n source for these documents, removed the
legacy contentDetector utility, and updated the related pages to use the
new component. Adjusted controller/backend (controller/misc.go) and locale
files to support the new rendering approach.

This improves reuse, maintainability, and future extensibility.
2025-10-08 11:12:49 +08:00
キュビビイ
6891057647 feat(web): add settings & pages of privacy policy & user agreement 2025-10-08 10:43:47 +08:00
Seefs
a610ef48e4 Merge pull request #1977 from QuantumNous/fix/bills
❤ fix(topup): prevent nil-pointer in Epay callback; reset page on search
2025-10-07 14:15:48 +08:00
Apple\Apple
ddf5c85b81 ❤ fix(topup): prevent nil-pointer in Epay callback; reset page on search
Add early return when Epay client is missing in controller/topup.go to avoid panic
Introduce handleKeywordChange in TopupHistoryModal.jsx to reset page to 1 when keyword updates
Wire input onChange to new handler; minor UX improvement to avoid empty results on pagination mismatch
2025-10-07 14:13:14 +08:00
Calcium-Ion
68d975120d Merge pull request #1966 from QuantumNous/revert-1952-main
Revert "fix(topup): add currency symbol to amounts in RechargeCard"
2025-10-03 21:54:08 +08:00
Calcium-Ion
3473329524 Revert "fix(topup): add currency symbol to amounts in RechargeCard" 2025-10-03 21:53:55 +08:00
Calcium-Ion
5d33ec8473 Merge pull request #1952 from kyubibii/main
fix(topup): add currency symbol to amounts in RechargeCard
2025-10-03 21:39:02 +08:00
Seefs
8a01f09ef6 Merge pull request #1962 from QuantumNous/main
main -> alpha
2025-10-03 12:28:21 +08:00
キュビビイ
f44f68242e Merge branch 'main' of https://github.com/QuantumNous/new-api 2025-10-02 23:19:40 +08:00
キュビビイ
b0b6ab2ebc feat(web): add privacy policy and user agreement settings & pages
Closes #1858
2025-10-02 23:03:58 +08:00
キュビビイ
2290acc86c fix(topup): add currency symbol to amounts in RechargeCard
- What: 在充值卡显示的实付与节省金额前加入美元符号 `$`。
- Why: 满足 issue #1881,要求在金额前标注货币单位以减少歧义。
- Files: src/components/topup/RechargeCard.jsx
- Note: 这是局部修复。建议后续实现统一的 currency formatter(Intl.NumberFormat)并从后端/配置读取货币代码以支持本地化与多币种。

Closes #1881
2025-10-02 12:25:07 +08:00
feitianbubu
2488e6ab66 feat: add ali qwen channel autoDisabled 2025-07-19 21:19:46 +08:00
69 changed files with 2266 additions and 278 deletions

View File

@@ -5,4 +5,5 @@
.gitignore
Makefile
docs
.eslintcache
.eslintcache
.gocache

View File

@@ -37,7 +37,7 @@ jobs:
- name: Setup Go
uses: actions/setup-go@v5
with:
go-version: '>=1.18.0'
go-version: '>=1.25.1'
- name: Build frontend
env:

View File

@@ -1,60 +0,0 @@
name: Linux Release
permissions:
contents: write
on:
workflow_dispatch:
inputs:
name:
description: 'reason'
required: false
push:
tags:
- '*'
- '!*-alpha*'
jobs:
release:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v3
with:
fetch-depth: 0
- uses: oven-sh/setup-bun@v2
with:
bun-version: latest
- name: Build Frontend
env:
CI: ""
run: |
cd web
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
with:
go-version: '>=1.18.0'
- name: Build Backend (amd64)
run: |
go mod download
VERSION=$(git describe --tags)
go build -ldflags "-s -w -X 'one-api/common.Version=$VERSION' -extldflags '-static'" -o new-api-$VERSION
- name: Build Backend (arm64)
run: |
sudo apt-get update
DEBIAN_FRONTEND=noninteractive sudo apt-get install -y gcc-aarch64-linux-gnu
VERSION=$(git describe --tags)
CC=aarch64-linux-gnu-gcc CGO_ENABLED=1 GOOS=linux GOARCH=arm64 go build -ldflags "-s -w -X 'one-api/common.Version=$VERSION' -extldflags '-static'" -o new-api-arm64-$VERSION
- name: Release
uses: softprops/action-gh-release@v2
if: startsWith(github.ref, 'refs/tags/')
with:
files: |
new-api-*
draft: true
generate_release_notes: true
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

View File

@@ -1,52 +0,0 @@
name: macOS Release
permissions:
contents: write
on:
workflow_dispatch:
inputs:
name:
description: 'reason'
required: false
push:
tags:
- '*'
- '!*-alpha*'
jobs:
release:
runs-on: macos-latest
steps:
- name: Checkout
uses: actions/checkout@v3
with:
fetch-depth: 0
- uses: oven-sh/setup-bun@v2
with:
bun-version: latest
- name: Build Frontend
env:
CI: ""
NODE_OPTIONS: "--max-old-space-size=4096"
run: |
cd web
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
with:
go-version: '>=1.18.0'
- name: Build Backend
run: |
go mod download
VERSION=$(git describe --tags)
go build -ldflags "-X 'one-api/common.Version=$VERSION'" -o new-api-macos-$VERSION
- name: Release
uses: softprops/action-gh-release@v2
if: startsWith(github.ref, 'refs/tags/')
with:
files: new-api-macos-*
draft: true
generate_release_notes: true
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

142
.github/workflows/release.yml vendored Normal file
View File

@@ -0,0 +1,142 @@
name: Release (Linux, macOS, Windows)
permissions:
contents: write
on:
workflow_dispatch:
inputs:
name:
description: 'reason'
required: false
push:
tags:
- '*'
- '!*-alpha*'
jobs:
linux:
name: Linux Release
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v3
with:
fetch-depth: 0
- uses: oven-sh/setup-bun@v2
with:
bun-version: latest
- name: Build Frontend
env:
CI: ""
run: |
cd web
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
with:
go-version: '>=1.25.1'
- name: Build Backend (amd64)
run: |
go mod download
VERSION=$(git describe --tags)
go build -ldflags "-s -w -X 'one-api/common.Version=$VERSION' -extldflags '-static'" -o new-api-$VERSION
- name: Build Backend (arm64)
run: |
sudo apt-get update
DEBIAN_FRONTEND=noninteractive sudo apt-get install -y gcc-aarch64-linux-gnu
VERSION=$(git describe --tags)
CC=aarch64-linux-gnu-gcc CGO_ENABLED=1 GOOS=linux GOARCH=arm64 go build -ldflags "-s -w -X 'one-api/common.Version=$VERSION' -extldflags '-static'" -o new-api-arm64-$VERSION
- name: Release
uses: softprops/action-gh-release@v2
if: startsWith(github.ref, 'refs/tags/')
with:
files: |
new-api-*
draft: true
generate_release_notes: true
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
macos:
name: macOS Release
runs-on: macos-latest
steps:
- name: Checkout
uses: actions/checkout@v3
with:
fetch-depth: 0
- uses: oven-sh/setup-bun@v2
with:
bun-version: latest
- name: Build Frontend
env:
CI: ""
NODE_OPTIONS: "--max-old-space-size=4096"
run: |
cd web
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
with:
go-version: '>=1.25.1'
- name: Build Backend
run: |
go mod download
VERSION=$(git describe --tags)
go build -ldflags "-X 'one-api/common.Version=$VERSION'" -o new-api-macos-$VERSION
- name: Release
uses: softprops/action-gh-release@v2
if: startsWith(github.ref, 'refs/tags/')
with:
files: new-api-macos-*
draft: true
generate_release_notes: true
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
windows:
name: Windows Release
runs-on: windows-latest
defaults:
run:
shell: bash
steps:
- name: Checkout
uses: actions/checkout@v3
with:
fetch-depth: 0
- uses: oven-sh/setup-bun@v2
with:
bun-version: latest
- name: Build Frontend
env:
CI: ""
run: |
cd web
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
with:
go-version: '>=1.25.1'
- name: Build Backend
run: |
go mod download
VERSION=$(git describe --tags)
go build -ldflags "-s -w -X 'one-api/common.Version=$VERSION'" -o new-api-$VERSION.exe
- name: Release
uses: softprops/action-gh-release@v2
if: startsWith(github.ref, 'refs/tags/')
with:
files: new-api-*.exe
draft: true
generate_release_notes: true
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

View File

@@ -1,54 +0,0 @@
name: Windows Release
permissions:
contents: write
on:
workflow_dispatch:
inputs:
name:
description: 'reason'
required: false
push:
tags:
- '*'
- '!*-alpha*'
jobs:
release:
runs-on: windows-latest
defaults:
run:
shell: bash
steps:
- name: Checkout
uses: actions/checkout@v3
with:
fetch-depth: 0
- uses: oven-sh/setup-bun@v2
with:
bun-version: latest
- name: Build Frontend
env:
CI: ""
run: |
cd web
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
with:
go-version: '>=1.18.0'
- name: Build Backend
run: |
go mod download
VERSION=$(git describe --tags)
go build -ldflags "-s -w -X 'one-api/common.Version=$VERSION'" -o new-api-$VERSION.exe
- name: Release
uses: softprops/action-gh-release@v2
if: startsWith(github.ref, 'refs/tags/')
with:
files: new-api-*.exe
draft: true
generate_release_notes: true
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

1
.gitignore vendored
View File

@@ -13,6 +13,7 @@ new-api
.DS_Store
tiktoken_cache
.eslintcache
.gocache
electron/node_modules
electron/dist

View File

@@ -9,10 +9,12 @@ COPY ./VERSION .
RUN DISABLE_ESLINT_PLUGIN='true' VITE_REACT_APP_VERSION=$(cat VERSION) bun run build
FROM golang:alpine AS builder2
ENV GO111MODULE=on CGO_ENABLED=0
ARG TARGETOS
ARG TARGETARCH
ENV GOOS=${TARGETOS:-linux} GOARCH=${TARGETARCH:-amd64}
ENV GO111MODULE=on \
CGO_ENABLED=0 \
GOOS=linux
WORKDIR /build

View File

@@ -3,6 +3,7 @@ package common
import (
"bytes"
"io"
"mime/multipart"
"net/http"
"one-api/constant"
"strings"
@@ -113,3 +114,26 @@ func ApiSuccess(c *gin.Context, data any) {
"data": data,
})
}
func ParseMultipartFormReusable(c *gin.Context) (*multipart.Form, error) {
requestBody, err := GetRequestBody(c)
if err != nil {
return nil, err
}
contentType := c.Request.Header.Get("Content-Type")
boundary := ""
if idx := strings.Index(contentType, "boundary="); idx != -1 {
boundary = contentType[idx+9:]
}
reader := multipart.NewReader(bytes.NewReader(requestBody), boundary)
form, err := reader.ReadForm(32 << 20) // 32 MB max memory
if err != nil {
return nil, err
}
// Reset request body
c.Request.Body = io.NopCloser(bytes.NewBuffer(requestBody))
return form, nil
}

View File

@@ -52,6 +52,7 @@ const (
ChannelTypeVidu = 52
ChannelTypeSubmodel = 53
ChannelTypeDoubaoVideo = 54
ChannelTypeSora = 55
ChannelTypeDummy // this one is only for count, do not add any channel after this
)
@@ -112,6 +113,7 @@ var ChannelBaseURLs = []string{
"https://api.vidu.cn", //52
"https://llm.submodel.ai", //53
"https://ark.cn-beijing.volces.com", //54
"https://api.openai.com", //55
}
var ChannelTypeNames = map[int]string{
@@ -166,6 +168,7 @@ var ChannelTypeNames = map[int]string{
ChannelTypeVidu: "Vidu",
ChannelTypeSubmodel: "Submodel",
ChannelTypeDoubaoVideo: "DoubaoVideo",
ChannelTypeSora: "Sora",
}
func GetChannelTypeName(channelType int) string {

View File

@@ -127,6 +127,14 @@ func GetAuthHeader(token string) http.Header {
return h
}
// GetClaudeAuthHeader get claude auth header
func GetClaudeAuthHeader(token string) http.Header {
h := http.Header{}
h.Add("x-api-key", token)
h.Add("anthropic-version", "2023-06-01")
return h
}
func GetResponseBody(method, url string, channel *model.Channel, headers http.Header) ([]byte, error) {
req, err := http.NewRequest(method, url, nil)
if err != nil {

View File

@@ -59,6 +59,21 @@ func testChannel(channel *model.Channel, testModel string, endpointType string)
w := httptest.NewRecorder()
c, _ := gin.CreateTestContext(w)
testModel = strings.TrimSpace(testModel)
if testModel == "" {
if channel.TestModel != nil && *channel.TestModel != "" {
testModel = strings.TrimSpace(*channel.TestModel)
} else {
models := channel.GetModels()
if len(models) > 0 {
testModel = strings.TrimSpace(models[0])
}
if testModel == "" {
testModel = "gpt-4o-mini"
}
}
}
requestPath := "/v1/chat/completions"
// 如果指定了端点类型,使用指定的端点类型
@@ -90,18 +105,6 @@ func testChannel(channel *model.Channel, testModel string, endpointType string)
Header: make(http.Header),
}
if testModel == "" {
if channel.TestModel != nil && *channel.TestModel != "" {
testModel = *channel.TestModel
} else {
if len(channel.GetModels()) > 0 {
testModel = channel.GetModels()[0]
} else {
testModel = "gpt-4o-mini"
}
}
}
cache, err := model.GetUserCache(1)
if err != nil {
return testResult{
@@ -619,10 +622,10 @@ func AutomaticallyTestChannels() {
time.Sleep(10 * time.Minute)
continue
}
frequency := operation_setting.GetMonitorSetting().AutoTestChannelMinutes
common.SysLog(fmt.Sprintf("automatically test channels with interval %d minutes", frequency))
for {
frequency := operation_setting.GetMonitorSetting().AutoTestChannelMinutes
time.Sleep(time.Duration(frequency) * time.Minute)
common.SysLog(fmt.Sprintf("automatically test channels with interval %d minutes", frequency))
common.SysLog("automatically testing all channels")
_ = testAllChannels(false)
common.SysLog("automatically channel test finished")

View File

@@ -198,9 +198,10 @@ func FetchUpstreamModels(c *gin.Context) {
// 获取响应体 - 根据渠道类型决定是否添加 AuthHeader
var body []byte
key := strings.Split(channel.Key, "\n")[0]
if channel.Type == constant.ChannelTypeGemini {
body, err = GetResponseBody("GET", url, channel, GetAuthHeader(key)) // Use AuthHeader since Gemini now forces it
} else {
switch channel.Type {
case constant.ChannelTypeAnthropic:
body, err = GetResponseBody("GET", url, channel, GetClaudeAuthHeader(key))
default:
body, err = GetResponseBody("GET", url, channel, GetAuthHeader(key))
}
if err != nil {

View File

@@ -43,6 +43,7 @@ func GetStatus(c *gin.Context) {
defer common.OptionMapRWMutex.RUnlock()
passkeySetting := system_setting.GetPasskeySettings()
legalSetting := system_setting.GetLegalSettings()
data := gin.H{
"version": common.Version,
@@ -108,6 +109,8 @@ func GetStatus(c *gin.Context) {
"passkey_user_verification": passkeySetting.UserVerification,
"passkey_attachment": passkeySetting.AttachmentPreference,
"setup": constant.Setup,
"user_agreement_enabled": legalSetting.UserAgreement != "",
"privacy_policy_enabled": legalSetting.PrivacyPolicy != "",
}
// 根据启用状态注入可选内容
@@ -151,6 +154,24 @@ func GetAbout(c *gin.Context) {
return
}
func GetUserAgreement(c *gin.Context) {
c.JSON(http.StatusOK, gin.H{
"success": true,
"message": "",
"data": system_setting.GetLegalSettings().UserAgreement,
})
return
}
func GetPrivacyPolicy(c *gin.Context) {
c.JSON(http.StatusOK, gin.H{
"success": true,
"message": "",
"data": system_setting.GetLegalSettings().PrivacyPolicy,
})
return
}
func GetMidjourney(c *gin.Context) {
common.OptionMapRWMutex.RLock()
defer common.OptionMapRWMutex.RUnlock()

View File

@@ -47,6 +47,11 @@ func updateVideoTaskAll(ctx context.Context, platform constant.TaskPlatform, cha
if adaptor == nil {
return fmt.Errorf("video adaptor not found")
}
info := &relaycommon.RelayInfo{}
info.ChannelMeta = &relaycommon.ChannelMeta{
ChannelBaseUrl: cacheGetChannel.GetBaseURL(),
}
adaptor.Init(info)
for _, taskId := range taskIds {
if err := updateVideoSingleTask(ctx, adaptor, cacheGetChannel, taskId, taskM); err != nil {
logger.LogError(ctx, fmt.Sprintf("Failed to update video task %s: %s", taskId, err.Error()))
@@ -92,6 +97,7 @@ func updateVideoSingleTask(ctx context.Context, adaptor channel.TaskAdaptor, cha
taskResult.Url = t.FailReason
taskResult.Progress = t.Progress
taskResult.Reason = t.FailReason
task.Data = t.Data
} else if taskResult, err = adaptor.ParseTaskResult(responseBody); err != nil {
return fmt.Errorf("parseTaskResult failed for task %s: %w", taskId, err)
} else {

View File

@@ -237,8 +237,8 @@ func EpayNotify(c *gin.Context) {
_, err := c.Writer.Write([]byte("fail"))
if err != nil {
log.Println("易支付回调写入失败")
return
}
return
}
verifyInfo, err := client.Verify(params)
if err == nil && verifyInfo.VerifyStatus {

129
controller/video_proxy.go Normal file
View File

@@ -0,0 +1,129 @@
package controller
import (
"fmt"
"io"
"net/http"
"one-api/logger"
"one-api/model"
"time"
"github.com/gin-gonic/gin"
)
func VideoProxy(c *gin.Context) {
taskID := c.Param("task_id")
if taskID == "" {
c.JSON(http.StatusBadRequest, gin.H{
"error": gin.H{
"message": "task_id is required",
"type": "invalid_request_error",
},
})
return
}
task, exists, err := model.GetByOnlyTaskId(taskID)
if err != nil {
logger.LogError(c.Request.Context(), fmt.Sprintf("Failed to query task %s: %s", taskID, err.Error()))
c.JSON(http.StatusInternalServerError, gin.H{
"error": gin.H{
"message": "Failed to query task",
"type": "server_error",
},
})
return
}
if !exists || task == nil {
logger.LogError(c.Request.Context(), fmt.Sprintf("Failed to get task %s: %s", taskID, err.Error()))
c.JSON(http.StatusNotFound, gin.H{
"error": gin.H{
"message": "Task not found",
"type": "invalid_request_error",
},
})
return
}
if task.Status != model.TaskStatusSuccess {
c.JSON(http.StatusBadRequest, gin.H{
"error": gin.H{
"message": fmt.Sprintf("Task is not completed yet, current status: %s", task.Status),
"type": "invalid_request_error",
},
})
return
}
channel, err := model.CacheGetChannel(task.ChannelId)
if err != nil {
logger.LogError(c.Request.Context(), fmt.Sprintf("Failed to get channel %d: %s", task.ChannelId, err.Error()))
c.JSON(http.StatusInternalServerError, gin.H{
"error": gin.H{
"message": "Failed to retrieve channel information",
"type": "server_error",
},
})
return
}
baseURL := channel.GetBaseURL()
if baseURL == "" {
baseURL = "https://api.openai.com"
}
videoURL := fmt.Sprintf("%s/v1/videos/%s/content", baseURL, task.TaskID)
client := &http.Client{
Timeout: 60 * time.Second,
}
req, err := http.NewRequestWithContext(c.Request.Context(), http.MethodGet, videoURL, nil)
if err != nil {
logger.LogError(c.Request.Context(), fmt.Sprintf("Failed to create request for %s: %s", videoURL, err.Error()))
c.JSON(http.StatusInternalServerError, gin.H{
"error": gin.H{
"message": "Failed to create proxy request",
"type": "server_error",
},
})
return
}
req.Header.Set("Authorization", "Bearer "+channel.Key)
resp, err := client.Do(req)
if err != nil {
logger.LogError(c.Request.Context(), fmt.Sprintf("Failed to fetch video from %s: %s", videoURL, err.Error()))
c.JSON(http.StatusBadGateway, gin.H{
"error": gin.H{
"message": "Failed to fetch video content",
"type": "server_error",
},
})
return
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
logger.LogError(c.Request.Context(), fmt.Sprintf("Upstream returned status %d for %s", resp.StatusCode, videoURL))
c.JSON(http.StatusBadGateway, gin.H{
"error": gin.H{
"message": fmt.Sprintf("Upstream service returned status %d", resp.StatusCode),
"type": "server_error",
},
})
return
}
for key, values := range resp.Header {
for _, value := range values {
c.Writer.Header().Add(key, value)
}
}
c.Writer.Header().Set("Cache-Control", "public, max-age=86400") // Cache for 24 hours
c.Writer.WriteHeader(resp.StatusCode)
_, err = io.Copy(c.Writer, resp.Body)
if err != nil {
logger.LogError(c.Request.Context(), fmt.Sprintf("Failed to stream video content: %s", err.Error()))
}
}

View File

@@ -329,6 +329,7 @@ type GeminiImageParameters struct {
SampleCount int `json:"sampleCount,omitempty"`
AspectRatio string `json:"aspectRatio,omitempty"`
PersonGeneration string `json:"personGeneration,omitempty"`
ImageSize string `json:"imageSize,omitempty"`
}
type GeminiImageResponse struct {

View File

@@ -87,6 +87,12 @@ type GeneralOpenAIRequest struct {
WebSearch json.RawMessage `json:"web_search,omitempty"`
// doubao,zhipu_v4
THINKING json.RawMessage `json:"thinking,omitempty"`
// pplx Params
SearchDomainFilter json.RawMessage `json:"search_domain_filter,omitempty"`
SearchRecencyFilter string `json:"search_recency_filter,omitempty"`
ReturnImages bool `json:"return_images,omitempty"`
ReturnRelatedQuestions bool `json:"return_related_questions,omitempty"`
SearchMode string `json:"search_mode,omitempty"`
}
func (r *GeneralOpenAIRequest) GetTokenCountMeta() *types.TokenCountMeta {

View File

@@ -233,6 +233,16 @@ type Usage struct {
Cost any `json:"cost,omitempty"`
}
type OpenAIVideoResponse struct {
Id string `json:"id" example:"file-abc123"`
Object string `json:"object" example:"file"`
Bytes int64 `json:"bytes" example:"120000"`
CreatedAt int64 `json:"created_at" example:"1677610602"`
ExpiresAt int64 `json:"expires_at" example:"1677614202"`
Filename string `json:"filename" example:"mydata.jsonl"`
Purpose string `json:"purpose" example:"fine-tune"`
}
type InputTokenDetails struct {
CachedTokens int `json:"cached_tokens"`
CachedCreationTokens int `json:"-"`

View File

@@ -9,7 +9,7 @@
"version": "1.0.0",
"devDependencies": {
"cross-env": "^7.0.3",
"electron": "28.3.3",
"electron": "35.7.5",
"electron-builder": "^24.9.1"
}
},
@@ -590,13 +590,13 @@
"license": "MIT"
},
"node_modules/@types/node": {
"version": "18.19.129",
"resolved": "https://registry.npmjs.org/@types/node/-/node-18.19.129.tgz",
"integrity": "sha512-hrmi5jWt2w60ayox3iIXwpMEnfUvOLJCRtrOPbHtH15nTjvO7uhnelvrdAs0dO0/zl5DZ3ZbahiaXEVb54ca/A==",
"version": "22.18.8",
"resolved": "https://registry.npmjs.org/@types/node/-/node-22.18.8.tgz",
"integrity": "sha512-pAZSHMiagDR7cARo/cch1f3rXy0AEXwsVsVH09FcyeJVAzCnGgmYis7P3JidtTUjyadhTeSo8TgRPswstghDaw==",
"dev": true,
"license": "MIT",
"dependencies": {
"undici-types": "~5.26.4"
"undici-types": "~6.21.0"
}
},
"node_modules/@types/plist": {
@@ -824,6 +824,85 @@
"node": ">= 10.0.0"
}
},
"node_modules/archiver": {
"version": "5.3.2",
"resolved": "https://registry.npmjs.org/archiver/-/archiver-5.3.2.tgz",
"integrity": "sha512-+25nxyyznAXF7Nef3y0EbBeqmGZgeN/BxHX29Rs39djAfaFalmQ89SE6CWyDCHzGL0yt/ycBtNOmGTW0FyGWNw==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"archiver-utils": "^2.1.0",
"async": "^3.2.4",
"buffer-crc32": "^0.2.1",
"readable-stream": "^3.6.0",
"readdir-glob": "^1.1.2",
"tar-stream": "^2.2.0",
"zip-stream": "^4.1.0"
},
"engines": {
"node": ">= 10"
}
},
"node_modules/archiver-utils": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/archiver-utils/-/archiver-utils-2.1.0.tgz",
"integrity": "sha512-bEL/yUb/fNNiNTuUz979Z0Yg5L+LzLxGJz8x79lYmR54fmTIb6ob/hNQgkQnIUDWIFjZVQwl9Xs356I6BAMHfw==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"glob": "^7.1.4",
"graceful-fs": "^4.2.0",
"lazystream": "^1.0.0",
"lodash.defaults": "^4.2.0",
"lodash.difference": "^4.5.0",
"lodash.flatten": "^4.4.0",
"lodash.isplainobject": "^4.0.6",
"lodash.union": "^4.6.0",
"normalize-path": "^3.0.0",
"readable-stream": "^2.0.0"
},
"engines": {
"node": ">= 6"
}
},
"node_modules/archiver-utils/node_modules/readable-stream": {
"version": "2.3.8",
"resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz",
"integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"core-util-is": "~1.0.0",
"inherits": "~2.0.3",
"isarray": "~1.0.0",
"process-nextick-args": "~2.0.0",
"safe-buffer": "~5.1.1",
"string_decoder": "~1.1.1",
"util-deprecate": "~1.0.1"
}
},
"node_modules/archiver-utils/node_modules/safe-buffer": {
"version": "5.1.2",
"resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz",
"integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==",
"dev": true,
"license": "MIT",
"peer": true
},
"node_modules/archiver-utils/node_modules/string_decoder": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz",
"integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"safe-buffer": "~5.1.0"
}
},
"node_modules/argparse": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz",
@@ -915,6 +994,19 @@
],
"license": "MIT"
},
"node_modules/bl": {
"version": "4.1.0",
"resolved": "https://registry.npmjs.org/bl/-/bl-4.1.0.tgz",
"integrity": "sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"buffer": "^5.5.0",
"inherits": "^2.0.4",
"readable-stream": "^3.4.0"
}
},
"node_modules/bluebird": {
"version": "3.7.2",
"resolved": "https://registry.npmjs.org/bluebird/-/bluebird-3.7.2.tgz",
@@ -971,7 +1063,6 @@
}
],
"license": "MIT",
"optional": true,
"dependencies": {
"base64-js": "^1.3.1",
"ieee754": "^1.1.13"
@@ -1276,6 +1367,23 @@
"node": ">=0.10.0"
}
},
"node_modules/compress-commons": {
"version": "4.1.2",
"resolved": "https://registry.npmjs.org/compress-commons/-/compress-commons-4.1.2.tgz",
"integrity": "sha512-D3uMHtGc/fcO1Gt1/L7i1e33VOvD4A9hfQLP+6ewd+BvG/gQ84Yh4oftEhAdjSMgBgwGL+jsppT7JYNpo6MHHg==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"buffer-crc32": "^0.2.13",
"crc32-stream": "^4.0.2",
"normalize-path": "^3.0.0",
"readable-stream": "^3.6.0"
},
"engines": {
"node": ">= 10"
}
},
"node_modules/concat-map": {
"version": "0.0.1",
"resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz",
@@ -1346,8 +1454,7 @@
"resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz",
"integrity": "sha512-3lqz5YjWTYnW6dlDa5TLaTCcShfar1e40rmcJVwCBJC6mWlFuj0eCHIElmG1g5kyuJ/GD+8Wn4FFCcz4gJPfaQ==",
"dev": true,
"license": "MIT",
"optional": true
"license": "MIT"
},
"node_modules/crc": {
"version": "3.8.0",
@@ -1360,6 +1467,35 @@
"buffer": "^5.1.0"
}
},
"node_modules/crc-32": {
"version": "1.2.2",
"resolved": "https://registry.npmjs.org/crc-32/-/crc-32-1.2.2.tgz",
"integrity": "sha512-ROmzCKrTnOwybPcJApAA6WBWij23HVfGVNKqqrZpuyZOHqK2CwHSvpGuyt/UNNvaIjEd8X5IFGp4Mh+Ie1IHJQ==",
"dev": true,
"license": "Apache-2.0",
"peer": true,
"bin": {
"crc32": "bin/crc32.njs"
},
"engines": {
"node": ">=0.8"
}
},
"node_modules/crc32-stream": {
"version": "4.0.3",
"resolved": "https://registry.npmjs.org/crc32-stream/-/crc32-stream-4.0.3.tgz",
"integrity": "sha512-NT7w2JVU7DFroFdYkeq8cywxrgjPHWkdX1wjpRQXPX5Asews3tA+Ght6lddQO5Mkumffp3X7GEqku3epj2toIw==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"crc-32": "^1.2.0",
"readable-stream": "^3.4.0"
},
"engines": {
"node": ">= 10"
}
},
"node_modules/cross-env": {
"version": "7.0.3",
"resolved": "https://registry.npmjs.org/cross-env/-/cross-env-7.0.3.tgz",
@@ -1681,15 +1817,15 @@
}
},
"node_modules/electron": {
"version": "28.3.3",
"resolved": "https://registry.npmjs.org/electron/-/electron-28.3.3.tgz",
"integrity": "sha512-ObKMLSPNhomtCOBAxFS8P2DW/4umkh72ouZUlUKzXGtYuPzgr1SYhskhFWgzAsPtUzhL2CzyV2sfbHcEW4CXqw==",
"version": "35.7.5",
"resolved": "https://registry.npmjs.org/electron/-/electron-35.7.5.tgz",
"integrity": "sha512-dnL+JvLraKZl7iusXTVTGYs10TKfzUi30uEDTqsmTm0guN9V2tbOjTzyIZbh9n3ygUjgEYyo+igAwMRXIi3IPw==",
"dev": true,
"hasInstallScript": true,
"license": "MIT",
"dependencies": {
"@electron/get": "^2.0.0",
"@types/node": "^18.11.18",
"@types/node": "^22.7.7",
"extract-zip": "^2.0.1"
},
"bin": {
@@ -1726,6 +1862,61 @@
"node": ">=14.0.0"
}
},
"node_modules/electron-builder-squirrel-windows": {
"version": "24.13.3",
"resolved": "https://registry.npmjs.org/electron-builder-squirrel-windows/-/electron-builder-squirrel-windows-24.13.3.tgz",
"integrity": "sha512-oHkV0iogWfyK+ah9ZIvMDpei1m9ZRpdXcvde1wTpra2U8AFDNNpqJdnin5z+PM1GbQ5BoaKCWas2HSjtR0HwMg==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"app-builder-lib": "24.13.3",
"archiver": "^5.3.1",
"builder-util": "24.13.1",
"fs-extra": "^10.1.0"
}
},
"node_modules/electron-builder-squirrel-windows/node_modules/fs-extra": {
"version": "10.1.0",
"resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-10.1.0.tgz",
"integrity": "sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"graceful-fs": "^4.2.0",
"jsonfile": "^6.0.1",
"universalify": "^2.0.0"
},
"engines": {
"node": ">=12"
}
},
"node_modules/electron-builder-squirrel-windows/node_modules/jsonfile": {
"version": "6.2.0",
"resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.2.0.tgz",
"integrity": "sha512-FGuPw30AdOIUTRMC2OMRtQV+jkVj2cfPqSeWXv1NEAJ1qZ5zb1X6z1mFhbfOB/iy3ssJCD+3KuZ8r8C3uVFlAg==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"universalify": "^2.0.0"
},
"optionalDependencies": {
"graceful-fs": "^4.1.6"
}
},
"node_modules/electron-builder-squirrel-windows/node_modules/universalify": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz",
"integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==",
"dev": true,
"license": "MIT",
"peer": true,
"engines": {
"node": ">= 10.0.0"
}
},
"node_modules/electron-builder/node_modules/fs-extra": {
"version": "10.1.0",
"resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-10.1.0.tgz",
@@ -2033,6 +2224,14 @@
"node": ">= 6"
}
},
"node_modules/fs-constants": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/fs-constants/-/fs-constants-1.0.0.tgz",
"integrity": "sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==",
"dev": true,
"license": "MIT",
"peer": true
},
"node_modules/fs-extra": {
"version": "8.1.0",
"resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-8.1.0.tgz",
@@ -2478,8 +2677,7 @@
"url": "https://feross.org/support"
}
],
"license": "BSD-3-Clause",
"optional": true
"license": "BSD-3-Clause"
},
"node_modules/inflight": {
"version": "1.0.6",
@@ -2523,6 +2721,14 @@
"node": ">=8"
}
},
"node_modules/isarray": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz",
"integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==",
"dev": true,
"license": "MIT",
"peer": true
},
"node_modules/isbinaryfile": {
"version": "5.0.6",
"resolved": "https://registry.npmjs.org/isbinaryfile/-/isbinaryfile-5.0.6.tgz",
@@ -2652,6 +2858,56 @@
"dev": true,
"license": "MIT"
},
"node_modules/lazystream": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/lazystream/-/lazystream-1.0.1.tgz",
"integrity": "sha512-b94GiNHQNy6JNTrt5w6zNyffMrNkXZb3KTkCZJb2V1xaEGCk093vkZ2jk3tpaeP33/OiXC+WvK9AxUebnf5nbw==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"readable-stream": "^2.0.5"
},
"engines": {
"node": ">= 0.6.3"
}
},
"node_modules/lazystream/node_modules/readable-stream": {
"version": "2.3.8",
"resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz",
"integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"core-util-is": "~1.0.0",
"inherits": "~2.0.3",
"isarray": "~1.0.0",
"process-nextick-args": "~2.0.0",
"safe-buffer": "~5.1.1",
"string_decoder": "~1.1.1",
"util-deprecate": "~1.0.1"
}
},
"node_modules/lazystream/node_modules/safe-buffer": {
"version": "5.1.2",
"resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz",
"integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==",
"dev": true,
"license": "MIT",
"peer": true
},
"node_modules/lazystream/node_modules/string_decoder": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz",
"integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"safe-buffer": "~5.1.0"
}
},
"node_modules/lodash": {
"version": "4.17.21",
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz",
@@ -2659,6 +2915,46 @@
"dev": true,
"license": "MIT"
},
"node_modules/lodash.defaults": {
"version": "4.2.0",
"resolved": "https://registry.npmjs.org/lodash.defaults/-/lodash.defaults-4.2.0.tgz",
"integrity": "sha512-qjxPLHd3r5DnsdGacqOMU6pb/avJzdh9tFX2ymgoZE27BmjXrNy/y4LoaiTeAb+O3gL8AfpJGtqfX/ae2leYYQ==",
"dev": true,
"license": "MIT",
"peer": true
},
"node_modules/lodash.difference": {
"version": "4.5.0",
"resolved": "https://registry.npmjs.org/lodash.difference/-/lodash.difference-4.5.0.tgz",
"integrity": "sha512-dS2j+W26TQ7taQBGN8Lbbq04ssV3emRw4NY58WErlTO29pIqS0HmoT5aJ9+TUQ1N3G+JOZSji4eugsWwGp9yPA==",
"dev": true,
"license": "MIT",
"peer": true
},
"node_modules/lodash.flatten": {
"version": "4.4.0",
"resolved": "https://registry.npmjs.org/lodash.flatten/-/lodash.flatten-4.4.0.tgz",
"integrity": "sha512-C5N2Z3DgnnKr0LOpv/hKCgKdb7ZZwafIrsesve6lmzvZIRZRGaZ/l6Q8+2W7NaT+ZwO3fFlSCzCzrDCFdJfZ4g==",
"dev": true,
"license": "MIT",
"peer": true
},
"node_modules/lodash.isplainobject": {
"version": "4.0.6",
"resolved": "https://registry.npmjs.org/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz",
"integrity": "sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA==",
"dev": true,
"license": "MIT",
"peer": true
},
"node_modules/lodash.union": {
"version": "4.6.0",
"resolved": "https://registry.npmjs.org/lodash.union/-/lodash.union-4.6.0.tgz",
"integrity": "sha512-c4pB2CdGrGdjMKYLA+XiRDO7Y0PRQbm/Gzg8qMj+QH+pFVAoTp5sBpO0odL3FjoPCGjK96p6qsP+yQoiLoOBcw==",
"dev": true,
"license": "MIT",
"peer": true
},
"node_modules/lowercase-keys": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/lowercase-keys/-/lowercase-keys-2.0.0.tgz",
@@ -2840,6 +3136,17 @@
"license": "MIT",
"optional": true
},
"node_modules/normalize-path": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz",
"integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==",
"dev": true,
"license": "MIT",
"peer": true,
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/normalize-url": {
"version": "6.1.0",
"resolved": "https://registry.npmjs.org/normalize-url/-/normalize-url-6.1.0.tgz",
@@ -2964,6 +3271,14 @@
"node": ">=10.4.0"
}
},
"node_modules/process-nextick-args": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz",
"integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==",
"dev": true,
"license": "MIT",
"peer": true
},
"node_modules/progress": {
"version": "2.0.3",
"resolved": "https://registry.npmjs.org/progress/-/progress-2.0.3.tgz",
@@ -3040,6 +3355,33 @@
"node": ">=12.0.0"
}
},
"node_modules/readable-stream": {
"version": "3.6.2",
"resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz",
"integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"inherits": "^2.0.3",
"string_decoder": "^1.1.1",
"util-deprecate": "^1.0.1"
},
"engines": {
"node": ">= 6"
}
},
"node_modules/readdir-glob": {
"version": "1.1.3",
"resolved": "https://registry.npmjs.org/readdir-glob/-/readdir-glob-1.1.3.tgz",
"integrity": "sha512-v05I2k7xN8zXvPD9N+z/uhXPaj0sUFCe2rcWZIpBsqxfP7xXFQ0tipAd/wjj1YxWyWtUS5IDJpOG82JKt2EAVA==",
"dev": true,
"license": "Apache-2.0",
"peer": true,
"dependencies": {
"minimatch": "^5.1.0"
}
},
"node_modules/require-directory": {
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz",
@@ -3099,6 +3441,28 @@
"node": ">=8.0"
}
},
"node_modules/safe-buffer": {
"version": "5.2.1",
"resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz",
"integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==",
"dev": true,
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/feross"
},
{
"type": "patreon",
"url": "https://www.patreon.com/feross"
},
{
"type": "consulting",
"url": "https://feross.org/support"
}
],
"license": "MIT",
"peer": true
},
"node_modules/safer-buffer": {
"version": "2.1.2",
"resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz",
@@ -3287,6 +3651,17 @@
"node": ">= 6"
}
},
"node_modules/string_decoder": {
"version": "1.3.0",
"resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz",
"integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"safe-buffer": "~5.2.0"
}
},
"node_modules/string-width": {
"version": "4.2.3",
"resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz",
@@ -3389,6 +3764,24 @@
"node": ">=10"
}
},
"node_modules/tar-stream": {
"version": "2.2.0",
"resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-2.2.0.tgz",
"integrity": "sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"bl": "^4.0.3",
"end-of-stream": "^1.4.1",
"fs-constants": "^1.0.0",
"inherits": "^2.0.3",
"readable-stream": "^3.1.1"
},
"engines": {
"node": ">=6"
}
},
"node_modules/temp-file": {
"version": "3.4.0",
"resolved": "https://registry.npmjs.org/temp-file/-/temp-file-3.4.0.tgz",
@@ -3497,9 +3890,9 @@
}
},
"node_modules/undici-types": {
"version": "5.26.5",
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz",
"integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==",
"version": "6.21.0",
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz",
"integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==",
"dev": true,
"license": "MIT"
},
@@ -3530,6 +3923,14 @@
"dev": true,
"license": "(WTFPL OR MIT)"
},
"node_modules/util-deprecate": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",
"integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==",
"dev": true,
"license": "MIT",
"peer": true
},
"node_modules/verror": {
"version": "1.10.1",
"resolved": "https://registry.npmjs.org/verror/-/verror-1.10.1.tgz",
@@ -3672,6 +4073,45 @@
"buffer-crc32": "~0.2.3",
"fd-slicer": "~1.1.0"
}
},
"node_modules/zip-stream": {
"version": "4.1.1",
"resolved": "https://registry.npmjs.org/zip-stream/-/zip-stream-4.1.1.tgz",
"integrity": "sha512-9qv4rlDiopXg4E69k+vMHjNN63YFMe9sZMrdlvKnCjlCRWeCBswPPMPUfx+ipsAWq1LXHe70RcbaHdJJpS6hyQ==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"archiver-utils": "^3.0.4",
"compress-commons": "^4.1.2",
"readable-stream": "^3.6.0"
},
"engines": {
"node": ">= 10"
}
},
"node_modules/zip-stream/node_modules/archiver-utils": {
"version": "3.0.4",
"resolved": "https://registry.npmjs.org/archiver-utils/-/archiver-utils-3.0.4.tgz",
"integrity": "sha512-KVgf4XQVrTjhyWmx6cte4RxonPLR9onExufI1jhvw/MQ4BB6IsZD5gT8Lq+u/+pRkWna/6JoHpiQioaqFP5Rzw==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"glob": "^7.2.3",
"graceful-fs": "^4.2.0",
"lazystream": "^1.0.0",
"lodash.defaults": "^4.2.0",
"lodash.difference": "^4.5.0",
"lodash.flatten": "^4.4.0",
"lodash.isplainobject": "^4.0.6",
"lodash.union": "^4.6.0",
"normalize-path": "^3.0.0",
"readable-stream": "^3.6.0"
},
"engines": {
"node": ">= 10"
}
}
}
}

View File

@@ -25,7 +25,7 @@
},
"devDependencies": {
"cross-env": "^7.0.3",
"electron": "28.3.3",
"electron": "35.7.5",
"electron-builder": "^24.9.1"
},
"build": {

3
go.mod
View File

@@ -21,7 +21,7 @@ require (
github.com/go-playground/validator/v10 v10.20.0
github.com/go-redis/redis/v8 v8.11.5
github.com/go-webauthn/webauthn v0.14.0
github.com/golang-jwt/jwt v3.2.2+incompatible
github.com/golang-jwt/jwt/v5 v5.3.0
github.com/google/uuid v1.6.0
github.com/gorilla/websocket v1.5.0
github.com/jinzhu/copier v0.4.0
@@ -68,7 +68,6 @@ require (
github.com/go-sql-driver/mysql v1.7.0 // indirect
github.com/go-webauthn/x v0.1.25 // indirect
github.com/goccy/go-json v0.10.2 // indirect
github.com/golang-jwt/jwt/v5 v5.3.0 // indirect
github.com/google/go-cmp v0.6.0 // indirect
github.com/google/go-tpm v0.9.5 // indirect
github.com/gorilla/context v1.1.1 // indirect

View File

@@ -165,6 +165,38 @@ func getModelRequest(c *gin.Context) (*ModelRequest, bool, error) {
}
c.Set("platform", string(constant.TaskPlatformSuno))
c.Set("relay_mode", relayMode)
} else if strings.Contains(c.Request.URL.Path, "/v1/videos") {
//curl https://api.openai.com/v1/videos \
// -H "Authorization: Bearer $OPENAI_API_KEY" \
// -F "model=sora-2" \
// -F "prompt=A calico cat playing a piano on stage"
// -F input_reference="@image.jpg"
relayMode := relayconstant.RelayModeUnknown
if c.Request.Method == http.MethodPost {
relayMode = relayconstant.RelayModeVideoSubmit
contentType := c.Request.Header.Get("Content-Type")
if strings.HasPrefix(contentType, "multipart/form-data") {
form, err := common.ParseMultipartFormReusable(c)
if err != nil {
return nil, false, errors.New("无效的video请求, " + err.Error())
}
defer form.RemoveAll()
if form != nil {
if values, ok := form.Value["model"]; ok && len(values) > 0 {
modelRequest.Model = values[0]
}
}
} else if strings.HasPrefix(contentType, "application/json") {
err = common.UnmarshalBodyReusable(c, &modelRequest)
if err != nil {
return nil, false, errors.New("无效的video请求, " + err.Error())
}
}
} else if c.Request.Method == http.MethodGet {
relayMode = relayconstant.RelayModeVideoFetchByID
shouldSelectChannel = false
}
c.Set("relay_mode", relayMode)
} else if strings.Contains(c.Request.URL.Path, "/v1/video/generations") {
relayMode := relayconstant.RelayModeUnknown
if c.Request.Method == http.MethodPost {

View File

@@ -46,7 +46,7 @@ type Channel struct {
Setting *string `json:"setting" gorm:"type:text"` // 渠道额外设置
ParamOverride *string `json:"param_override" gorm:"type:text"`
HeaderOverride *string `json:"header_override" gorm:"type:text"`
Remark string `json:"remark,omitempty" gorm:"type:varchar(255)" validate:"max=255"`
Remark *string `json:"remark" gorm:"type:varchar(255)" validate:"max=255"`
// add after v0.8.5
ChannelInfo ChannelInfo `json:"channel_info" gorm:"type:json"`

View File

@@ -4,6 +4,7 @@ import (
"io"
"net/http"
"one-api/dto"
"one-api/model"
relaycommon "one-api/relay/common"
"one-api/types"
@@ -49,3 +50,7 @@ type TaskAdaptor interface {
ParseTaskResult(respBody []byte) (*relaycommon.TaskInfo, error)
}
type OpenAIVideoConverter interface {
ConvertToOpenAIVideo(originTask *model.Task) (*relaycommon.OpenAIVideo, error)
}

View File

@@ -67,8 +67,12 @@ func (a *Adaptor) ConvertImageRequest(c *gin.Context, info *relaycommon.RelayInf
aspectRatio = size
} else {
switch size {
case "1024x1024":
case "256x256", "512x512", "1024x1024":
aspectRatio = "1:1"
case "1536x1024":
aspectRatio = "3:2"
case "1024x1536":
aspectRatio = "2:3"
case "1024x1792":
aspectRatio = "9:16"
case "1792x1024":
@@ -91,6 +95,28 @@ func (a *Adaptor) ConvertImageRequest(c *gin.Context, info *relaycommon.RelayInf
},
}
// Set imageSize when quality parameter is specified
// Map quality parameter to imageSize (only supported by Standard and Ultra models)
// quality values: auto, high, medium, low (for gpt-image-1), hd, standard (for dall-e-3)
// imageSize values: 1K (default), 2K
// https://ai.google.dev/gemini-api/docs/imagen
// https://platform.openai.com/docs/api-reference/images/create
if request.Quality != "" {
imageSize := "1K" // default
switch request.Quality {
case "hd", "high":
imageSize = "2K"
case "2K":
imageSize = "2K"
case "standard", "medium", "low", "auto", "1K":
imageSize = "1K"
default:
// unknown quality value, default to 1K
imageSize = "1K"
}
geminiRequest.Parameters.ImageSize = imageSize
}
return geminiRequest, nil
}

View File

@@ -163,13 +163,10 @@ func OaiStreamHandler(c *gin.Context, info *relaycommon.RelayInfo, resp *http.Re
if !containStreamUsage {
usage = service.ResponseText2Usage(responseTextBuilder.String(), info.UpstreamModelName, info.PromptTokens)
usage.CompletionTokens += toolCount * 7
} else {
if info.ChannelType == constant.ChannelTypeDeepSeek {
if usage.PromptCacheHitTokens != 0 {
usage.PromptTokensDetails.CachedTokens = usage.PromptCacheHitTokens
}
}
}
applyUsagePostProcessing(info, usage, nil)
HandleFinalResponse(c, info, lastStreamData, responseId, createAt, model, systemFingerprint, usage, containStreamUsage)
return usage, nil
@@ -233,6 +230,8 @@ func OpenaiHandler(c *gin.Context, info *relaycommon.RelayInfo, resp *http.Respo
usageModified = true
}
applyUsagePostProcessing(info, &simpleResponse.Usage, responseBody)
switch info.RelayFormat {
case types.RelayFormatOpenAI:
if usageModified {
@@ -631,5 +630,60 @@ func OpenaiHandlerWithUsage(c *gin.Context, info *relaycommon.RelayInfo, resp *h
usageResp.PromptTokensDetails.ImageTokens += usageResp.InputTokensDetails.ImageTokens
usageResp.PromptTokensDetails.TextTokens += usageResp.InputTokensDetails.TextTokens
}
applyUsagePostProcessing(info, &usageResp.Usage, responseBody)
return &usageResp.Usage, nil
}
func applyUsagePostProcessing(info *relaycommon.RelayInfo, usage *dto.Usage, responseBody []byte) {
if info == nil || usage == nil {
return
}
switch info.ChannelType {
case constant.ChannelTypeDeepSeek:
if usage.PromptTokensDetails.CachedTokens == 0 && usage.PromptCacheHitTokens != 0 {
usage.PromptTokensDetails.CachedTokens = usage.PromptCacheHitTokens
}
case constant.ChannelTypeZhipu_v4:
if usage.PromptTokensDetails.CachedTokens == 0 {
if usage.InputTokensDetails != nil && usage.InputTokensDetails.CachedTokens > 0 {
usage.PromptTokensDetails.CachedTokens = usage.InputTokensDetails.CachedTokens
} else if cachedTokens, ok := extractCachedTokensFromBody(responseBody); ok {
usage.PromptTokensDetails.CachedTokens = cachedTokens
} else if usage.PromptCacheHitTokens > 0 {
usage.PromptTokensDetails.CachedTokens = usage.PromptCacheHitTokens
}
}
}
}
func extractCachedTokensFromBody(body []byte) (int, bool) {
if len(body) == 0 {
return 0, false
}
var payload struct {
Usage struct {
PromptTokensDetails struct {
CachedTokens *int `json:"cached_tokens"`
} `json:"prompt_tokens_details"`
CachedTokens *int `json:"cached_tokens"`
PromptCacheHitTokens *int `json:"prompt_cache_hit_tokens"`
} `json:"usage"`
}
if err := json.Unmarshal(body, &payload); err != nil {
return 0, false
}
if payload.Usage.PromptTokensDetails.CachedTokens != nil {
return *payload.Usage.PromptTokensDetails.CachedTokens, true
}
if payload.Usage.CachedTokens != nil {
return *payload.Usage.CachedTokens, true
}
if payload.Usage.PromptCacheHitTokens != nil {
return *payload.Usage.PromptCacheHitTokens, true
}
return 0, false
}

View File

@@ -22,10 +22,9 @@ func (a *Adaptor) ConvertGeminiRequest(*gin.Context, *relaycommon.RelayInfo, *dt
return nil, errors.New("not implemented")
}
func (a *Adaptor) ConvertClaudeRequest(*gin.Context, *relaycommon.RelayInfo, *dto.ClaudeRequest) (any, error) {
//TODO implement me
panic("implement me")
return nil, nil
func (a *Adaptor) ConvertClaudeRequest(c *gin.Context, info *relaycommon.RelayInfo, req *dto.ClaudeRequest) (any, error) {
adaptor := openai.Adaptor{}
return adaptor.ConvertClaudeRequest(c, info, req)
}
func (a *Adaptor) ConvertAudioRequest(c *gin.Context, info *relaycommon.RelayInfo, request dto.AudioRequest) (io.Reader, error) {
@@ -80,11 +79,8 @@ func (a *Adaptor) DoRequest(c *gin.Context, info *relaycommon.RelayInfo, request
}
func (a *Adaptor) DoResponse(c *gin.Context, resp *http.Response, info *relaycommon.RelayInfo) (usage any, err *types.NewAPIError) {
if info.IsStream {
usage, err = openai.OaiStreamHandler(c, info, resp)
} else {
usage, err = openai.OpenaiHandler(c, info, resp)
}
adaptor := openai.Adaptor{}
usage, err = adaptor.DoResponse(c, resp, info)
return
}

View File

@@ -2,6 +2,7 @@ package perplexity
var ModelList = []string{
"llama-3-sonar-small-32k-chat", "llama-3-sonar-small-32k-online", "llama-3-sonar-large-32k-chat", "llama-3-sonar-large-32k-online", "llama-3-8b-instruct", "llama-3-70b-instruct", "mixtral-8x7b-instruct",
"sonar", "sonar-pro", "sonar-reasoning",
}
var ChannelName = "perplexity"

View File

@@ -11,11 +11,18 @@ func requestOpenAI2Perplexity(request dto.GeneralOpenAIRequest) *dto.GeneralOpen
})
}
return &dto.GeneralOpenAIRequest{
Model: request.Model,
Stream: request.Stream,
Messages: messages,
Temperature: request.Temperature,
TopP: request.TopP,
MaxTokens: request.GetMaxTokens(),
Model: request.Model,
Stream: request.Stream,
Messages: messages,
Temperature: request.Temperature,
TopP: request.TopP,
MaxTokens: request.GetMaxTokens(),
FrequencyPenalty: request.FrequencyPenalty,
PresencePenalty: request.PresencePenalty,
SearchDomainFilter: request.SearchDomainFilter,
SearchRecencyFilter: request.SearchRecencyFilter,
ReturnImages: request.ReturnImages,
ReturnRelatedQuestions: request.ReturnRelatedQuestions,
SearchMode: request.SearchMode,
}
}

View File

@@ -7,13 +7,15 @@ import (
"io"
"net/http"
"one-api/model"
"strconv"
"strings"
"time"
"github.com/bytedance/gopkg/util/logger"
"github.com/samber/lo"
"github.com/gin-gonic/gin"
"github.com/golang-jwt/jwt"
"github.com/golang-jwt/jwt/v5"
"github.com/pkg/errors"
"one-api/constant"
@@ -303,14 +305,6 @@ func (a *TaskAdaptor) createJWTToken() (string, error) {
return a.createJWTTokenWithKey(a.apiKey)
}
//func (a *TaskAdaptor) createJWTTokenWithKey(apiKey string) (string, error) {
// parts := strings.Split(apiKey, "|")
// if len(parts) != 2 {
// return "", fmt.Errorf("invalid API key format, expected 'access_key,secret_key'")
// }
// return a.createJWTTokenWithKey(strings.TrimSpace(parts[0]), strings.TrimSpace(parts[1]))
//}
func (a *TaskAdaptor) createJWTTokenWithKey(apiKey string) (string, error) {
if isNewAPIRelay(apiKey) {
return apiKey, nil // new api relay
@@ -369,3 +363,50 @@ func (a *TaskAdaptor) ParseTaskResult(respBody []byte) (*relaycommon.TaskInfo, e
func isNewAPIRelay(apiKey string) bool {
return strings.HasPrefix(apiKey, "sk-")
}
func (a *TaskAdaptor) ConvertToOpenAIVideo(originTask *model.Task) (*relaycommon.OpenAIVideo, error) {
var klingResp responsePayload
if err := json.Unmarshal(originTask.Data, &klingResp); err != nil {
return nil, errors.Wrap(err, "unmarshal kling task data failed")
}
convertProgress := func(progress string) int {
progress = strings.TrimSuffix(progress, "%")
p, err := strconv.Atoi(progress)
if err != nil {
logger.Warnf("convert progress failed, progress: %s, err: %v", progress, err)
}
return p
}
openAIVideo := &relaycommon.OpenAIVideo{
ID: klingResp.Data.TaskId,
Object: "video",
//Model: "kling-v1", //todo save model
Status: string(originTask.Status),
CreatedAt: klingResp.Data.CreatedAt,
CompletedAt: klingResp.Data.UpdatedAt,
Metadata: make(map[string]any),
Progress: convertProgress(originTask.Progress),
}
// 处理视频 URL
if len(klingResp.Data.TaskResult.Videos) > 0 {
video := klingResp.Data.TaskResult.Videos[0]
if video.Url != "" {
openAIVideo.Metadata["url"] = video.Url
}
if video.Duration != "" {
openAIVideo.Seconds = video.Duration
}
}
if klingResp.Code != 0 && klingResp.Message != "" {
openAIVideo.Error = &relaycommon.OpenAIVideoError{
Message: klingResp.Message,
Code: fmt.Sprintf("%d", klingResp.Code),
}
}
return openAIVideo, nil
}

View File

@@ -0,0 +1,195 @@
package sora
import (
"bytes"
"encoding/json"
"fmt"
"io"
"net/http"
"one-api/common"
"one-api/dto"
"one-api/model"
"one-api/relay/channel"
relaycommon "one-api/relay/common"
"one-api/service"
"one-api/setting/system_setting"
"github.com/gin-gonic/gin"
"github.com/pkg/errors"
)
// ============================
// Request / Response structures
// ============================
type ContentItem struct {
Type string `json:"type"` // "text" or "image_url"
Text string `json:"text,omitempty"` // for text type
ImageURL *ImageURL `json:"image_url,omitempty"` // for image_url type
}
type ImageURL struct {
URL string `json:"url"`
}
type responseTask struct {
ID string `json:"id"`
TaskID string `json:"task_id,omitempty"` //兼容旧接口
Object string `json:"object"`
Model string `json:"model"`
Status string `json:"status"`
Progress int `json:"progress"`
CreatedAt int64 `json:"created_at"`
CompletedAt int64 `json:"completed_at,omitempty"`
ExpiresAt int64 `json:"expires_at,omitempty"`
Seconds string `json:"seconds,omitempty"`
Size string `json:"size,omitempty"`
RemixedFromVideoID string `json:"remixed_from_video_id,omitempty"`
Error *struct {
Message string `json:"message"`
Code string `json:"code"`
} `json:"error,omitempty"`
}
// ============================
// Adaptor implementation
// ============================
type TaskAdaptor struct {
ChannelType int
apiKey string
baseURL string
}
func (a *TaskAdaptor) Init(info *relaycommon.RelayInfo) {
a.ChannelType = info.ChannelType
a.baseURL = info.ChannelBaseUrl
a.apiKey = info.ApiKey
}
func (a *TaskAdaptor) ValidateRequestAndSetAction(c *gin.Context, info *relaycommon.RelayInfo) (taskErr *dto.TaskError) {
return relaycommon.ValidateMultipartDirect(c, info)
}
func (a *TaskAdaptor) BuildRequestURL(info *relaycommon.RelayInfo) (string, error) {
return fmt.Sprintf("%s/v1/videos", a.baseURL), nil
}
// BuildRequestHeader sets required headers.
func (a *TaskAdaptor) BuildRequestHeader(c *gin.Context, req *http.Request, info *relaycommon.RelayInfo) error {
req.Header.Set("Authorization", "Bearer "+a.apiKey)
req.Header.Set("Content-Type", c.Request.Header.Get("Content-Type"))
return nil
}
func (a *TaskAdaptor) BuildRequestBody(c *gin.Context, info *relaycommon.RelayInfo) (io.Reader, error) {
cachedBody, err := common.GetRequestBody(c)
if err != nil {
return nil, errors.Wrap(err, "get_request_body_failed")
}
return bytes.NewReader(cachedBody), nil
}
// DoRequest delegates to common helper.
func (a *TaskAdaptor) DoRequest(c *gin.Context, info *relaycommon.RelayInfo, requestBody io.Reader) (*http.Response, error) {
return channel.DoTaskApiRequest(a, c, info, requestBody)
}
// DoResponse handles upstream response, returns taskID etc.
func (a *TaskAdaptor) DoResponse(c *gin.Context, resp *http.Response, _ *relaycommon.RelayInfo) (taskID string, taskData []byte, taskErr *dto.TaskError) {
responseBody, err := io.ReadAll(resp.Body)
if err != nil {
taskErr = service.TaskErrorWrapper(err, "read_response_body_failed", http.StatusInternalServerError)
return
}
_ = resp.Body.Close()
// Parse Sora response
var dResp responseTask
if err := json.Unmarshal(responseBody, &dResp); err != nil {
taskErr = service.TaskErrorWrapper(errors.Wrapf(err, "body: %s", responseBody), "unmarshal_response_body_failed", http.StatusInternalServerError)
return
}
if dResp.ID == "" {
if dResp.TaskID == "" {
taskErr = service.TaskErrorWrapper(fmt.Errorf("task_id is empty"), "invalid_response", http.StatusInternalServerError)
return
}
dResp.ID = dResp.TaskID
dResp.TaskID = ""
}
c.JSON(http.StatusOK, dResp)
return dResp.ID, responseBody, nil
}
// FetchTask fetch task status
func (a *TaskAdaptor) FetchTask(baseUrl, key string, body map[string]any) (*http.Response, error) {
taskID, ok := body["task_id"].(string)
if !ok {
return nil, fmt.Errorf("invalid task_id")
}
uri := fmt.Sprintf("%s/v1/videos/%s", baseUrl, taskID)
req, err := http.NewRequest(http.MethodGet, uri, nil)
if err != nil {
return nil, err
}
req.Header.Set("Authorization", "Bearer "+key)
return service.GetHttpClient().Do(req)
}
func (a *TaskAdaptor) GetModelList() []string {
return ModelList
}
func (a *TaskAdaptor) GetChannelName() string {
return ChannelName
}
func (a *TaskAdaptor) ParseTaskResult(respBody []byte) (*relaycommon.TaskInfo, error) {
resTask := responseTask{}
if err := json.Unmarshal(respBody, &resTask); err != nil {
return nil, errors.Wrap(err, "unmarshal task result failed")
}
taskResult := relaycommon.TaskInfo{
Code: 0,
}
switch resTask.Status {
case "queued", "pending":
taskResult.Status = model.TaskStatusQueued
case "processing", "in_progress":
taskResult.Status = model.TaskStatusInProgress
case "completed":
taskResult.Status = model.TaskStatusSuccess
taskResult.Url = fmt.Sprintf("%s/v1/videos/%s/content", system_setting.ServerAddress, resTask.ID)
case "failed", "cancelled":
taskResult.Status = model.TaskStatusFailure
if resTask.Error != nil {
taskResult.Reason = resTask.Error.Message
} else {
taskResult.Reason = "task failed"
}
default:
}
if resTask.Progress > 0 && resTask.Progress < 100 {
taskResult.Progress = fmt.Sprintf("%d%%", resTask.Progress)
}
return &taskResult, nil
}
func (a *TaskAdaptor) ConvertToOpenAIVideo(task *model.Task) (*relaycommon.OpenAIVideo, error) {
openAIVideo := &relaycommon.OpenAIVideo{}
err := json.Unmarshal(task.Data, openAIVideo)
if err != nil {
return nil, errors.Wrap(err, "unmarshal to OpenAIVideo failed")
}
return openAIVideo, nil
}

View File

@@ -0,0 +1,8 @@
package sora
var ModelList = []string{
"sora-2",
"sora-2-pro",
}
var ChannelName = "sora"

View File

@@ -13,7 +13,7 @@ import (
"strings"
"github.com/bytedance/gopkg/cache/asynccache"
"github.com/golang-jwt/jwt"
"github.com/golang-jwt/jwt/v5"
"fmt"
"time"

View File

@@ -17,7 +17,7 @@ import (
"time"
"github.com/gin-gonic/gin"
"github.com/golang-jwt/jwt"
"github.com/golang-jwt/jwt/v5"
)
// https://open.bigmodel.cn/doc/api#chatglm_std

View File

@@ -261,6 +261,7 @@ var streamSupportedChannels = map[int]bool{
constant.ChannelTypeXai: true,
constant.ChannelTypeDeepSeek: true,
constant.ChannelTypeBaiduV2: true,
constant.ChannelTypeZhipu_v4: true,
}
func GenRelayInfoWs(c *gin.Context, ws *websocket.Conn) *RelayInfo {
@@ -549,3 +550,24 @@ func RemoveDisabledFields(jsonData []byte, channelOtherSettings dto.ChannelOther
}
return jsonDataAfter, nil
}
type OpenAIVideo struct {
ID string `json:"id"`
TaskID string `json:"task_id,omitempty"` //兼容旧接口 待废弃
Object string `json:"object"`
Model string `json:"model"`
Status string `json:"status"`
Progress int `json:"progress"`
CreatedAt int64 `json:"created_at"`
CompletedAt int64 `json:"completed_at,omitempty"`
ExpiresAt int64 `json:"expires_at,omitempty"`
Seconds string `json:"seconds,omitempty"`
Size string `json:"size,omitempty"`
RemixedFromVideoID string `json:"remixed_from_video_id,omitempty"`
Error *OpenAIVideoError `json:"error,omitempty"`
Metadata map[string]any `json:"metadata,omitempty"`
}
type OpenAIVideoError struct {
Message string `json:"message"`
Code string `json:"code"`
}

View File

@@ -6,9 +6,11 @@ import (
"one-api/common"
"one-api/constant"
"one-api/dto"
"strconv"
"strings"
"github.com/gin-gonic/gin"
"github.com/samber/lo"
)
type HasPrompt interface {
@@ -52,7 +54,7 @@ func createTaskError(err error, code string, statusCode int, localError bool) *d
}
}
func storeTaskRequest(c *gin.Context, info *RelayInfo, action string, requestObj interface{}) {
func storeTaskRequest(c *gin.Context, info *RelayInfo, action string, requestObj TaskSubmitReq) {
info.Action = action
c.Set("task_request", requestObj)
}
@@ -64,9 +66,167 @@ func validatePrompt(prompt string) *dto.TaskError {
return nil
}
func ValidateBasicTaskRequest(c *gin.Context, info *RelayInfo, action string) *dto.TaskError {
func validateMultipartTaskRequest(c *gin.Context, info *RelayInfo, action string) (TaskSubmitReq, error) {
var req TaskSubmitReq
if err := common.UnmarshalBodyReusable(c, &req); err != nil {
if _, err := c.MultipartForm(); err != nil {
return req, err
}
formData := c.Request.PostForm
req = TaskSubmitReq{
Prompt: formData.Get("prompt"),
Model: formData.Get("model"),
Mode: formData.Get("mode"),
Image: formData.Get("image"),
Size: formData.Get("size"),
Metadata: make(map[string]interface{}),
}
if durationStr := formData.Get("seconds"); durationStr != "" {
if duration, err := strconv.Atoi(durationStr); err == nil {
req.Duration = duration
}
}
if images := formData["images"]; len(images) > 0 {
req.Images = images
}
for key, values := range formData {
if len(values) > 0 && !isKnownTaskField(key) {
if intVal, err := strconv.Atoi(values[0]); err == nil {
req.Metadata[key] = intVal
} else if floatVal, err := strconv.ParseFloat(values[0], 64); err == nil {
req.Metadata[key] = floatVal
} else {
req.Metadata[key] = values[0]
}
}
}
return req, nil
}
func ValidateMultipartDirect(c *gin.Context, info *RelayInfo) *dto.TaskError {
contentType := c.GetHeader("Content-Type")
var prompt string
var model string
var seconds int
var size string
var hasInputReference bool
if strings.HasPrefix(contentType, "multipart/form-data") {
form, err := common.ParseMultipartFormReusable(c)
if err != nil {
return createTaskError(err, "invalid_multipart_form", http.StatusBadRequest, true)
}
defer form.RemoveAll()
prompts, ok := form.Value["prompt"]
if !ok || len(prompts) == 0 {
return createTaskError(fmt.Errorf("prompt field is required"), "missing_prompt", http.StatusBadRequest, true)
}
prompt = prompts[0]
if _, ok := form.Value["model"]; !ok {
return createTaskError(fmt.Errorf("model field is required"), "missing_model", http.StatusBadRequest, true)
}
model = form.Value["model"][0]
if _, ok := form.File["input_reference"]; ok {
hasInputReference = true
}
if ss, ok := form.Value["seconds"]; ok {
sInt := common.String2Int(ss[0])
if sInt > seconds {
seconds = common.String2Int(ss[0])
}
}
if sz, ok := form.Value["size"]; ok {
size = sz[0]
}
} else {
var req TaskSubmitReq
if err := common.UnmarshalBodyReusable(c, &req); err != nil {
return createTaskError(err, "invalid_json", http.StatusBadRequest, true)
}
prompt = req.Prompt
model = req.Model
seconds = req.Duration
if strings.TrimSpace(req.Model) == "" {
return createTaskError(fmt.Errorf("model field is required"), "missing_model", http.StatusBadRequest, true)
}
if req.HasImage() {
hasInputReference = true
}
}
if taskErr := validatePrompt(prompt); taskErr != nil {
return taskErr
}
action := constant.TaskActionTextGenerate
if hasInputReference {
action = constant.TaskActionGenerate
}
if strings.HasPrefix(model, "sora-2") {
if size == "" {
size = "720x1280"
}
if seconds <= 0 {
seconds = 4
}
if model == "sora-2" && !lo.Contains([]string{"720x1280", "1280x720"}, size) {
return createTaskError(fmt.Errorf("sora-2 size is invalid"), "invalid_size", http.StatusBadRequest, true)
}
if model == "sora-2-pro" && !lo.Contains([]string{"720x1280", "1280x720", "1792x1024", "1024x1792"}, size) {
return createTaskError(fmt.Errorf("sora-2 size is invalid"), "invalid_size", http.StatusBadRequest, true)
}
info.PriceData.OtherRatios = map[string]float64{
"seconds": float64(seconds),
"size": 1,
}
if lo.Contains([]string{"1792x1024", "1024x1792"}, size) {
info.PriceData.OtherRatios["size"] = 1.666667
}
}
info.Action = action
return nil
}
func isKnownTaskField(field string) bool {
knownFields := map[string]bool{
"prompt": true,
"model": true,
"mode": true,
"image": true,
"images": true,
"size": true,
"duration": true,
"input_reference": true, // Sora 特有字段
}
return knownFields[field]
}
func ValidateBasicTaskRequest(c *gin.Context, info *RelayInfo, action string) *dto.TaskError {
var err error
contentType := c.GetHeader("Content-Type")
var req TaskSubmitReq
if strings.HasPrefix(contentType, "multipart/form-data") {
req, err = validateMultipartTaskRequest(c, info, action)
if err != nil {
return createTaskError(err, "invalid_multipart_form", http.StatusBadRequest, true)
}
} else if err := common.UnmarshalBodyReusable(c, &req); err != nil {
return createTaskError(err, "invalid_request", http.StatusBadRequest, true)
}

View File

@@ -114,7 +114,7 @@ func ModelPriceHelperPerCall(c *gin.Context, info *relaycommon.RelayInfo) types.
modelPrice, success := ratio_setting.GetModelPrice(info.OriginModelName, true)
// 如果没有配置价格,则使用默认价格
if !success {
defaultPrice, ok := ratio_setting.GetDefaultModelRatioMap()[info.OriginModelName]
defaultPrice, ok := ratio_setting.GetDefaultModelPriceMap()[info.OriginModelName]
if !ok {
modelPrice = 0.1
} else {

View File

@@ -29,6 +29,7 @@ import (
taskdoubao "one-api/relay/channel/task/doubao"
taskjimeng "one-api/relay/channel/task/jimeng"
"one-api/relay/channel/task/kling"
tasksora "one-api/relay/channel/task/sora"
"one-api/relay/channel/task/suno"
taskvertex "one-api/relay/channel/task/vertex"
taskVidu "one-api/relay/channel/task/vidu"
@@ -137,6 +138,8 @@ func GetTaskAdaptor(platform constant.TaskPlatform) channel.TaskAdaptor {
return &taskVidu.TaskAdaptor{}
case constant.ChannelTypeDoubaoVideo:
return &taskdoubao.TaskAdaptor{}
case constant.ChannelTypeSora:
return &tasksora.TaskAdaptor{}
}
}
return nil

View File

@@ -11,6 +11,7 @@ import (
"one-api/constant"
"one-api/dto"
"one-api/model"
"one-api/relay/channel"
relaycommon "one-api/relay/common"
relayconstant "one-api/relay/constant"
"one-api/service"
@@ -53,7 +54,7 @@ func RelayTaskSubmit(c *gin.Context, info *relaycommon.RelayInfo) (taskErr *dto.
}
modelPrice, success := ratio_setting.GetModelPrice(modelName, true)
if !success {
defaultPrice, ok := ratio_setting.GetDefaultModelRatioMap()[modelName]
defaultPrice, ok := ratio_setting.GetDefaultModelPriceMap()[modelName]
if !ok {
modelPrice = 0.1
} else {
@@ -70,6 +71,14 @@ func RelayTaskSubmit(c *gin.Context, info *relaycommon.RelayInfo) (taskErr *dto.
} else {
ratio = modelPrice * groupRatio
}
if len(info.PriceData.OtherRatios) > 0 {
for _, ra := range info.PriceData.OtherRatios {
if 1.0 != ra {
ratio *= ra
}
}
}
println(fmt.Sprintf("model: %s, model_price: %.4f, group: %s, group_ratio: %.4f, final_ratio: %.4f", modelName, modelPrice, info.UsingGroup, groupRatio, ratio))
userQuota, err := model.GetUserQuota(info.UserId, false)
if err != nil {
taskErr = service.TaskErrorWrapper(err, "get_user_quota_failed", http.StatusInternalServerError)
@@ -138,11 +147,22 @@ func RelayTaskSubmit(c *gin.Context, info *relaycommon.RelayInfo) (taskErr *dto.
}
if quota != 0 {
tokenName := c.GetString("token_name")
gRatio := groupRatio
if hasUserGroupRatio {
gRatio = userGroupRatio
//gRatio := groupRatio
//if hasUserGroupRatio {
// gRatio = userGroupRatio
//}
logContent := fmt.Sprintf("操作 %s", info.Action)
if len(info.PriceData.OtherRatios) > 0 {
var contents []string
for key, ra := range info.PriceData.OtherRatios {
if 1.0 != ra {
contents = append(contents, fmt.Sprintf("%s: %.2f", key, ra))
}
}
if len(contents) > 0 {
logContent = fmt.Sprintf("%s, 计算参数:%s", logContent, strings.Join(contents, ", "))
}
}
logContent := fmt.Sprintf("模型固定价格 %.2f,分组倍率 %.2f,操作 %s", modelPrice, gRatio, info.Action)
other := make(map[string]interface{})
other["model_price"] = modelPrice
other["group_ratio"] = groupRatio
@@ -362,11 +382,34 @@ func videoFetchByIDRespBodyBuilder(c *gin.Context) (respBody []byte, taskResp *d
}
}()
if len(respBody) == 0 {
respBody, err = json.Marshal(dto.TaskResponse[any]{
Code: "success",
Data: TaskModel2Dto(originTask),
})
if len(respBody) != 0 {
return
}
if strings.HasPrefix(c.Request.RequestURI, "/v1/videos/") {
adaptor := GetTaskAdaptor(originTask.Platform)
if adaptor == nil {
taskResp = service.TaskErrorWrapperLocal(fmt.Errorf("invalid channel id: %d", originTask.ChannelId), "invalid_channel_id", http.StatusBadRequest)
return
}
if converter, ok := adaptor.(channel.OpenAIVideoConverter); ok {
openAIVideo, err := converter.ConvertToOpenAIVideo(originTask)
if err != nil {
taskResp = service.TaskErrorWrapper(err, "convert_to_openai_video_failed", http.StatusInternalServerError)
return
}
respBody, _ = json.Marshal(openAIVideo)
return
}
taskResp = service.TaskErrorWrapperLocal(errors.New(fmt.Sprintf("not_implemented:%s", originTask.Platform)), "not_implemented", http.StatusNotImplemented)
return
}
respBody, err = json.Marshal(dto.TaskResponse[any]{
Code: "success",
Data: TaskModel2Dto(originTask),
})
if err != nil {
taskResp = service.TaskErrorWrapper(err, "marshal_response_failed", http.StatusInternalServerError)
}
return
}

View File

@@ -20,6 +20,8 @@ func SetApiRouter(router *gin.Engine) {
apiRouter.GET("/models", middleware.UserAuth(), controller.DashboardListModels)
apiRouter.GET("/status/test", middleware.AdminAuth(), controller.TestStatus)
apiRouter.GET("/notice", controller.GetNotice)
apiRouter.GET("/user-agreement", controller.GetUserAgreement)
apiRouter.GET("/privacy-policy", controller.GetPrivacyPolicy)
apiRouter.GET("/about", controller.GetAbout)
//apiRouter.GET("/midjourney", controller.GetMidjourney)
apiRouter.GET("/home_page_content", controller.GetHomePageContent)

View File

@@ -9,11 +9,18 @@ import (
func SetVideoRouter(router *gin.Engine) {
videoV1Router := router.Group("/v1")
videoV1Router.GET("/videos/:task_id/content", controller.VideoProxy)
videoV1Router.Use(middleware.TokenAuth(), middleware.Distribute())
{
videoV1Router.POST("/video/generations", controller.RelayTask)
videoV1Router.GET("/video/generations/:task_id", controller.RelayTask)
}
// openai compatible API video routes
// docs: https://platform.openai.com/docs/api-reference/videos/create
{
videoV1Router.POST("/videos", controller.RelayTask)
videoV1Router.GET("/videos/:task_id", controller.RelayTask)
}
klingV1Router := router.Group("/kling/v1")
klingV1Router.Use(middleware.KlingRequestConvert(), middleware.TokenAuth(), middleware.Distribute())

View File

@@ -75,6 +75,8 @@ func ShouldDisableChannel(channelType int, err *types.NewAPIError) bool {
return true
case "pre_consume_token_quota_failed":
return true
case "Arrearage":
return true
}
switch oaiErr.Type {
case "insufficient_quota":

View File

@@ -45,7 +45,7 @@ func DoWorkerRequest(req *WorkerRequest) (*http.Response, error) {
return nil, fmt.Errorf("failed to marshal worker payload: %v", err)
}
return http.Post(workerUrl, "application/json", bytes.NewBuffer(workerPayload))
return GetHttpClient().Post(workerUrl, "application/json", bytes.NewBuffer(workerPayload))
}
func DoDownloadRequest(originUrl string, reason ...string) (resp *http.Response, err error) {
@@ -64,6 +64,6 @@ func DoDownloadRequest(originUrl string, reason ...string) (resp *http.Response,
}
common.SysLog(fmt.Sprintf("downloading from origin: %s, reason: %s", common.MaskSensitiveInfo(originUrl), strings.Join(reason, ", ")))
return http.Get(originUrl)
return GetHttpClient().Get(originUrl)
}
}

View File

@@ -7,6 +7,7 @@ import (
"net/http"
"net/url"
"one-api/common"
"one-api/setting/system_setting"
"sync"
"time"
@@ -19,12 +20,27 @@ var (
proxyClients = make(map[string]*http.Client)
)
func checkRedirect(req *http.Request, via []*http.Request) error {
fetchSetting := system_setting.GetFetchSetting()
urlStr := req.URL.String()
if err := common.ValidateURLWithFetchSetting(urlStr, fetchSetting.EnableSSRFProtection, fetchSetting.AllowPrivateIp, fetchSetting.DomainFilterMode, fetchSetting.IpFilterMode, fetchSetting.DomainList, fetchSetting.IpList, fetchSetting.AllowedPorts, fetchSetting.ApplyIPFilterForDomain); err != nil {
return fmt.Errorf("redirect to %s blocked: %v", urlStr, err)
}
if len(via) >= 10 {
return fmt.Errorf("stopped after 10 redirects")
}
return nil
}
func InitHttpClient() {
if common.RelayTimeout == 0 {
httpClient = &http.Client{}
httpClient = &http.Client{
CheckRedirect: checkRedirect,
}
} else {
httpClient = &http.Client{
Timeout: time.Duration(common.RelayTimeout) * time.Second,
Timeout: time.Duration(common.RelayTimeout) * time.Second,
CheckRedirect: checkRedirect,
}
}
}
@@ -69,6 +85,7 @@ func NewProxyHttpClient(proxyURL string) (*http.Client, error) {
Transport: &http.Transport{
Proxy: http.ProxyURL(parsedURL),
},
CheckRedirect: checkRedirect,
}
client.Timeout = time.Duration(common.RelayTimeout) * time.Second
proxyClientLock.Lock()
@@ -102,6 +119,7 @@ func NewProxyHttpClient(proxyURL string) (*http.Client, error) {
return dialer.Dial(network, addr)
},
},
CheckRedirect: checkRedirect,
}
client.Timeout = time.Duration(common.RelayTimeout) * time.Second
proxyClientLock.Lock()

View File

@@ -290,6 +290,8 @@ var defaultModelPrice = map[string]float64{
"mj_upscale": 0.05,
"swap_face": 0.05,
"mj_upload": 0.05,
"sora-2": 0.3,
"sora-2-pro": 0.5,
}
var defaultAudioRatio = map[string]float64{
@@ -452,6 +454,10 @@ func GetDefaultModelRatioMap() map[string]float64 {
return defaultModelRatio
}
func GetDefaultModelPriceMap() map[string]float64 {
return defaultModelPrice
}
func GetDefaultImageRatioMap() map[string]float64 {
return defaultImageRatio
}

View File

@@ -0,0 +1,21 @@
package system_setting
import "one-api/setting/config"
type LegalSettings struct {
UserAgreement string `json:"user_agreement"`
PrivacyPolicy string `json:"privacy_policy"`
}
var defaultLegalSettings = LegalSettings{
UserAgreement: "",
PrivacyPolicy: "",
}
func init() {
config.GlobalConfig.Register("legal", &defaultLegalSettings)
}
func GetLegalSettings() *LegalSettings {
return &defaultLegalSettings
}

View File

@@ -160,6 +160,9 @@ func (e *NewAPIError) ToOpenAIError() OpenAIError {
if e.errorCode != ErrorCodeCountTokenFailed {
result.Message = common.MaskSensitiveInfo(result.Message)
}
if result.Message == "" {
result.Message = string(e.errorType)
}
return result
}
@@ -186,6 +189,9 @@ func (e *NewAPIError) ToClaudeError() ClaudeError {
if e.errorCode != ErrorCodeCountTokenFailed {
result.Message = common.MaskSensitiveInfo(result.Message)
}
if result.Message == "" {
result.Message = string(e.errorType)
}
return result
}

View File

@@ -17,6 +17,7 @@ type PriceData struct {
ImageRatio float64
AudioRatio float64
AudioCompletionRatio float64
OtherRatios map[string]float64
UsePrice bool
ShouldPreConsumedQuota int
GroupRatioInfo GroupRatioInfo

View File

@@ -10,7 +10,7 @@
"@visactor/react-vchart": "~1.8.8",
"@visactor/vchart": "~1.8.8",
"@visactor/vchart-semi-theme": "~1.8.8",
"axios": "^0.27.2",
"axios": "1.12.0",
"clsx": "^2.1.1",
"country-flag-icons": "^1.5.19",
"dayjs": "^1.11.11",
@@ -687,7 +687,7 @@
"autoprefixer": ["autoprefixer@10.4.21", "", { "dependencies": { "browserslist": "^4.24.4", "caniuse-lite": "^1.0.30001702", "fraction.js": "^4.3.7", "normalize-range": "^0.1.2", "picocolors": "^1.1.1", "postcss-value-parser": "^4.2.0" }, "peerDependencies": { "postcss": "^8.1.0" }, "bin": { "autoprefixer": "bin/autoprefixer" } }, "sha512-O+A6LWV5LDHSJD3LjHYoNi4VLsj/Whi7k6zG12xTYaU4cQ8oxQGckXNX8cRHK5yOZ/ppVHe0ZBXGzSV9jXdVbQ=="],
"axios": ["axios@0.27.2", "", { "dependencies": { "follow-redirects": "^1.14.9", "form-data": "^4.0.0" } }, "sha512-t+yRIyySRTp/wua5xEr+z1q60QmLq8ABsS5O9Me1AsE5dfKqgnCFzwiCZZ/cGNd1lq4/7akDWMxdhVlucjmnOQ=="],
"axios": ["axios@1.12.0", "", { "dependencies": { "follow-redirects": "^1.15.6", "form-data": "^4.0.4", "proxy-from-env": "^1.1.0" } }, "sha512-oXTDccv8PcfjZmPGlWsPSwtOJCZ/b6W5jAMCNcfwJbCzDckwG0jrYJFaWH1yvivfCXjVzV/SPDEhMB3Q+DSurg=="],
"babel-plugin-macros": ["babel-plugin-macros@3.1.0", "", { "dependencies": { "@babel/runtime": "^7.12.5", "cosmiconfig": "^7.0.0", "resolve": "^1.19.0" } }, "sha512-Cg7TFGpIr01vOQNODXOOaGz2NpCU5gl8x1qJFbb6hbZxR7XrcE2vtbAsTAbJ7/xwJtUuJEw8K8Zr/AE0LHlesg=="],
@@ -713,6 +713,8 @@
"buffer-from": ["buffer-from@1.1.2", "", {}, "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ=="],
"call-bind-apply-helpers": ["call-bind-apply-helpers@1.0.2", "", { "dependencies": { "es-errors": "^1.3.0", "function-bind": "^1.1.2" } }, "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ=="],
"callsites": ["callsites@3.1.0", "", {}, "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ=="],
"camelcase-css": ["camelcase-css@2.0.1", "", {}, "sha512-QOSvevhslijgYwRx6Rv7zKdMF8lbRmx+uQGx2+vDc+KI/eBnsy9kit5aj23AgGu3pa4t9AgwbnXWqS+iOY+2aA=="],
@@ -895,6 +897,8 @@
"dompurify": ["dompurify@3.2.6", "", { "optionalDependencies": { "@types/trusted-types": "^2.0.7" } }, "sha512-/2GogDQlohXPZe6D6NOgQvXLPSYBqIWMnZ8zzOhn09REE4eyAzb+Hed3jhoM9OkuaJ8P6ZGTTVWQKAi8ieIzfQ=="],
"dunder-proto": ["dunder-proto@1.0.1", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.1", "es-errors": "^1.3.0", "gopd": "^1.2.0" } }, "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A=="],
"eastasianwidth": ["eastasianwidth@0.2.0", "", {}, "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA=="],
"electron-to-chromium": ["electron-to-chromium@1.5.157", "", {}, "sha512-/0ybgsQd1muo8QlnuTpKwtl0oX5YMlUGbm8xyqgDU00motRkKFFbUJySAQBWcY79rVqNLWIWa87BGVGClwAB2w=="],
@@ -907,6 +911,14 @@
"error-ex": ["error-ex@1.3.2", "", { "dependencies": { "is-arrayish": "^0.2.1" } }, "sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g=="],
"es-define-property": ["es-define-property@1.0.1", "", {}, "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g=="],
"es-errors": ["es-errors@1.3.0", "", {}, "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw=="],
"es-object-atoms": ["es-object-atoms@1.1.1", "", { "dependencies": { "es-errors": "^1.3.0" } }, "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA=="],
"es-set-tostringtag": ["es-set-tostringtag@2.1.0", "", { "dependencies": { "es-errors": "^1.3.0", "get-intrinsic": "^1.2.6", "has-tostringtag": "^1.0.2", "hasown": "^2.0.2" } }, "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA=="],
"esast-util-from-estree": ["esast-util-from-estree@2.0.0", "", { "dependencies": { "@types/estree-jsx": "^1.0.0", "devlop": "^1.0.0", "estree-util-visit": "^2.0.0", "unist-util-position-from-estree": "^2.0.0" } }, "sha512-4CyanoAudUSBAn5K13H4JhsMH6L9ZP7XbLVe/dKybkxMO7eDyLsT8UHl9TRNrU2Gr9nz+FovfSIjuXWJ81uVwQ=="],
"esast-util-from-js": ["esast-util-from-js@2.0.1", "", { "dependencies": { "@types/estree-jsx": "^1.0.0", "acorn": "^8.0.0", "esast-util-from-estree": "^2.0.0", "vfile-message": "^4.0.0" } }, "sha512-8Ja+rNJ0Lt56Pcf3TAmpBZjmx8ZcK5Ts4cAzIOjsjevg9oSXJnl6SUQ2EevU8tv3h6ZLWmoKL5H4fgWvdvfETw=="],
@@ -995,7 +1007,7 @@
"foreground-child": ["foreground-child@3.3.1", "", { "dependencies": { "cross-spawn": "^7.0.6", "signal-exit": "^4.0.1" } }, "sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw=="],
"form-data": ["form-data@4.0.1", "", { "dependencies": { "asynckit": "^0.4.0", "combined-stream": "^1.0.8", "mime-types": "^2.1.12" } }, "sha512-tzN8e4TX8+kkxGPK8D5u0FNmjPUjw3lwC9lSLxxoB/+GtsJG91CO8bSWy73APlgAZzZbXEYZJuxjkHH2w+Ezhw=="],
"form-data": ["form-data@4.0.4", "", { "dependencies": { "asynckit": "^0.4.0", "combined-stream": "^1.0.8", "es-set-tostringtag": "^2.1.0", "hasown": "^2.0.2", "mime-types": "^2.1.12" } }, "sha512-KrGhL9Q4zjj0kiUt5OO4Mr/A/jlI2jDYs5eHBpYHPcBEVSiipAvn2Ko2HnPe20rmcuuvMHNdZFp+4IlGTMF0Ow=="],
"fraction.js": ["fraction.js@4.3.7", "", {}, "sha512-ZsDfxO51wGAXREY55a7la9LScWpwv9RxIrYABrlvOFBlH/ShPnrtsXeuUIfXKKOVicNxQ+o8JTbJvjS4M89yew=="],
@@ -1019,6 +1031,10 @@
"geojson-linestring-dissolve": ["geojson-linestring-dissolve@0.0.1", "", {}, "sha512-Y8I2/Ea28R/Xeki7msBcpMvJL2TaPfaPKP8xqueJfQ9/jEhps+iOJxOR2XCBGgVb12Z6XnDb1CMbaPfLepsLaw=="],
"get-intrinsic": ["get-intrinsic@1.3.0", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.2", "es-define-property": "^1.0.1", "es-errors": "^1.3.0", "es-object-atoms": "^1.1.1", "function-bind": "^1.1.2", "get-proto": "^1.0.1", "gopd": "^1.2.0", "has-symbols": "^1.1.0", "hasown": "^2.0.2", "math-intrinsics": "^1.1.0" } }, "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ=="],
"get-proto": ["get-proto@1.0.1", "", { "dependencies": { "dunder-proto": "^1.0.1", "es-object-atoms": "^1.0.0" } }, "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g=="],
"get-stdin": ["get-stdin@6.0.0", "", {}, "sha512-jp4tHawyV7+fkkSKyvjuLZswblUtz+SQKzSWnBbii16BuZksJlU1wuBYXY75r+duh/llF1ur6oNwi+2ZzjKZ7g=="],
"get-value": ["get-value@2.0.6", "", {}, "sha512-Ln0UQDlxH1BapMu3GPtf7CuYNwRZf2gwCuPqbyG6pB8WfmFpzqcy4xtAaAMUhnNqjMKTiCPZG2oMT3YSx8U2NA=="],
@@ -1031,6 +1047,8 @@
"globals": ["globals@13.24.0", "", { "dependencies": { "type-fest": "^0.20.2" } }, "sha512-AhO5QUcj8llrbG09iWhPU2B204J1xnPeL8kQmVorSsy+Sjj1sk8gIyh6cUocGmH4L0UuhAJy+hJMRA4mgA4mFQ=="],
"gopd": ["gopd@1.2.0", "", {}, "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg=="],
"graceful-fs": ["graceful-fs@4.2.11", "", {}, "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ=="],
"graphemer": ["graphemer@1.4.0", "", {}, "sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag=="],
@@ -1039,6 +1057,10 @@
"has-flag": ["has-flag@4.0.0", "", {}, "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ=="],
"has-symbols": ["has-symbols@1.1.0", "", {}, "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ=="],
"has-tostringtag": ["has-tostringtag@1.0.2", "", { "dependencies": { "has-symbols": "^1.0.3" } }, "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw=="],
"hasown": ["hasown@2.0.2", "", { "dependencies": { "function-bind": "^1.1.2" } }, "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ=="],
"hast-util-from-dom": ["hast-util-from-dom@5.0.1", "", { "dependencies": { "@types/hast": "^3.0.0", "hastscript": "^9.0.0", "web-namespaces": "^2.0.0" } }, "sha512-N+LqofjR2zuzTjCPzyDUdSshy4Ma6li7p/c3pA78uTwzFgENbgbUrm2ugwsOdcjI1muO+o6Dgzp9p8WHtn/39Q=="],
@@ -1229,6 +1251,8 @@
"marked": ["marked@4.3.0", "", { "bin": { "marked": "bin/marked.js" } }, "sha512-PRsaiG84bK+AMvxziE/lCFss8juXjNaWzVbN5tXAm4XjeaS9NAHhop+PjQxz2A9h8Q4M/xGmzP8vqNwy6JeK0A=="],
"math-intrinsics": ["math-intrinsics@1.1.0", "", {}, "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g=="],
"mdast-util-find-and-replace": ["mdast-util-find-and-replace@3.0.2", "", { "dependencies": { "@types/mdast": "^4.0.0", "escape-string-regexp": "^5.0.0", "unist-util-is": "^6.0.0", "unist-util-visit-parents": "^6.0.0" } }, "sha512-Tmd1Vg/m3Xz43afeNxDIhWRtFZgM2VLyaf4vSTYwudTyeuTneoL3qtWMA5jeLyz/O1vDJmmV4QuScFCA2tBPwg=="],
"mdast-util-from-markdown": ["mdast-util-from-markdown@2.0.2", "", { "dependencies": { "@types/mdast": "^4.0.0", "@types/unist": "^3.0.0", "decode-named-character-reference": "^1.0.0", "devlop": "^1.0.0", "mdast-util-to-string": "^4.0.0", "micromark": "^4.0.0", "micromark-util-decode-numeric-character-reference": "^2.0.0", "micromark-util-decode-string": "^2.0.0", "micromark-util-normalize-identifier": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0", "unist-util-stringify-position": "^4.0.0" } }, "sha512-uZhTV/8NBuw0WHkPTrCqDOl0zVe1BIng5ZtHoDk49ME1qqcjYmmLmOf0gELgcRMxN4w2iuIeVso5/6QymSrgmA=="],
@@ -1491,6 +1515,8 @@
"protocol-buffers-schema": ["protocol-buffers-schema@3.6.0", "", {}, "sha512-TdDRD+/QNdrCGCE7v8340QyuXd4kIWIgapsE2+n/SaGiSSbomYl4TjHlvIoCWRpE7wFt02EpB35VVA2ImcBVqw=="],
"proxy-from-env": ["proxy-from-env@1.1.0", "", {}, "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg=="],
"punycode": ["punycode@2.3.1", "", {}, "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg=="],
"qrcode.react": ["qrcode.react@4.2.0", "", { "peerDependencies": { "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-QpgqWi8rD9DsS9EP3z7BT+5lY5SFhsqGjpgW5DY/i3mK4M9DTBNz3ErMi8BWYEfI3L0d8GIbGmcdFAS1uIRGjA=="],
@@ -1505,7 +1531,7 @@
"rc-checkbox": ["rc-checkbox@3.5.0", "", { "dependencies": { "@babel/runtime": "^7.10.1", "classnames": "^2.3.2", "rc-util": "^5.25.2" }, "peerDependencies": { "react": ">=16.9.0", "react-dom": ">=16.9.0" } }, "sha512-aOAQc3E98HteIIsSqm6Xk2FPKIER6+5vyEFMZfo73TqM+VVAIqOkHoPjgKLqSNtVLWScoaM7vY2ZrGEheI79yg=="],
"rc-collapse": ["rc-collapse@4.0.0", "", { "dependencies": { "@babel/runtime": "^7.10.1", "classnames": "2.x", "rc-motion": "^2.3.4", "rc-util": "^5.27.0" }, "peerDependencies": { "react": ">=16.9.0", "react-dom": ">=16.9.0" } }, "sha512-SwoOByE39/3oIokDs/BnkqI+ltwirZbP8HZdq1/3SkPSBi7xDdvWHTp7cpNI9ullozkR6mwTWQi6/E/9huQVrA=="],
"rc-collapse": ["rc-collapse@3.9.0", "", { "dependencies": { "@babel/runtime": "^7.10.1", "classnames": "2.x", "rc-motion": "^2.3.4", "rc-util": "^5.27.0" }, "peerDependencies": { "react": ">=16.9.0", "react-dom": ">=16.9.0" } }, "sha512-swDdz4QZ4dFTo4RAUMLL50qP0EY62N2kvmk2We5xYdRwcRn8WcYtuetCJpwpaCbUfUt5+huLpVxhvmnK+PHrkA=="],
"rc-dialog": ["rc-dialog@9.6.0", "", { "dependencies": { "@babel/runtime": "^7.10.1", "@rc-component/portal": "^1.0.0-8", "classnames": "^2.2.6", "rc-motion": "^2.3.0", "rc-util": "^5.21.0" }, "peerDependencies": { "react": ">=16.9.0", "react-dom": ">=16.9.0" } }, "sha512-ApoVi9Z8PaCQg6FsUzS8yvBEQy0ZL2PkuvAgrmohPkN3okps5WZ5WQWPc1RNuiOKaAYv8B97ACdsFU5LizzCqg=="],
@@ -1949,6 +1975,8 @@
"@lobehub/ui/lucide-react": ["lucide-react@0.484.0", "", { "peerDependencies": { "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-oZy8coK9kZzvqhSgfbGkPtTgyjpBvs3ukLgDPv14dSOZtBtboryWF5o8i3qen7QbGg7JhiJBz5mK1p8YoMZTLQ=="],
"@lobehub/ui/rc-collapse": ["rc-collapse@4.0.0", "", { "dependencies": { "@babel/runtime": "^7.10.1", "classnames": "2.x", "rc-motion": "^2.3.4", "rc-util": "^5.27.0" }, "peerDependencies": { "react": ">=16.9.0", "react-dom": ">=16.9.0" } }, "sha512-SwoOByE39/3oIokDs/BnkqI+ltwirZbP8HZdq1/3SkPSBi7xDdvWHTp7cpNI9ullozkR6mwTWQi6/E/9huQVrA=="],
"@radix-ui/react-dismissable-layer/@radix-ui/react-compose-refs": ["@radix-ui/react-compose-refs@1.0.0", "", { "dependencies": { "@babel/runtime": "^7.13.10" }, "peerDependencies": { "react": "^16.8 || ^17.0 || ^18.0" } }, "sha512-0KaSv6sx787/hK3eF53iOkiSLwAGlFMx5lotrqD2pTjB18KbybKoEIgkNZTKC60YECDQTKGTRcDBILwZVqVKvA=="],
"@radix-ui/react-popper/@floating-ui/react-dom": ["@floating-ui/react-dom@0.7.2", "", { "dependencies": { "@floating-ui/dom": "^0.5.3", "use-isomorphic-layout-effect": "^1.1.1" }, "peerDependencies": { "react": ">=16.8.0", "react-dom": ">=16.8.0" } }, "sha512-1T0sJcpHgX/u4I1OzIEhlcrvkUN8ln39nz7fMoE/2HDHrPiMFoOGR7++GYyfUmIQHkkrTinaeQsO3XWubjSvGg=="],
@@ -1965,8 +1993,6 @@
"@visactor/vrender-kits/roughjs": ["roughjs@4.5.2", "", { "dependencies": { "path-data-parser": "^0.1.0", "points-on-curve": "^0.2.0", "points-on-path": "^0.2.1" } }, "sha512-2xSlLDKdsWyFxrveYWk9YQ/Y9UfK38EAMRNkYkMqYBJvPX8abCa9PN0x3w02H8Oa6/0bcZICJU+U95VumPqseg=="],
"antd/rc-collapse": ["rc-collapse@3.9.0", "", { "dependencies": { "@babel/runtime": "^7.10.1", "classnames": "2.x", "rc-motion": "^2.3.4", "rc-util": "^5.27.0" }, "peerDependencies": { "react": ">=16.9.0", "react-dom": ">=16.9.0" } }, "sha512-swDdz4QZ4dFTo4RAUMLL50qP0EY62N2kvmk2We5xYdRwcRn8WcYtuetCJpwpaCbUfUt5+huLpVxhvmnK+PHrkA=="],
"antd/scroll-into-view-if-needed": ["scroll-into-view-if-needed@3.1.0", "", { "dependencies": { "compute-scroll-into-view": "^3.0.2" } }, "sha512-49oNpRjWRvnU8NyGVmUaYG4jtTkNonFZI86MmGRDqBphEK2EXT9gdEUoQPZhuBM8yWHxCWbobltqYO5M4XrUvQ=="],
"chokidar/glob-parent": ["glob-parent@5.1.2", "", { "dependencies": { "is-glob": "^4.0.1" } }, "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow=="],

View File

@@ -10,7 +10,7 @@
"@visactor/react-vchart": "~1.8.8",
"@visactor/vchart": "~1.8.8",
"@visactor/vchart-semi-theme": "~1.8.8",
"axios": "^0.27.2",
"axios": "1.12.0",
"clsx": "^2.1.1",
"country-flag-icons": "^1.5.19",
"dayjs": "^1.11.11",

View File

@@ -51,6 +51,8 @@ import SetupCheck from './components/layout/SetupCheck';
const Home = lazy(() => import('./pages/Home'));
const Dashboard = lazy(() => import('./pages/Dashboard'));
const About = lazy(() => import('./pages/About'));
const UserAgreement = lazy(() => import('./pages/UserAgreement'));
const PrivacyPolicy = lazy(() => import('./pages/PrivacyPolicy'));
function App() {
const location = useLocation();
@@ -301,6 +303,22 @@ function App() {
</Suspense>
}
/>
<Route
path='/user-agreement'
element={
<Suspense fallback={<Loading></Loading>} key={location.pathname}>
<UserAgreement />
</Suspense>
}
/>
<Route
path='/privacy-policy'
element={
<Suspense fallback={<Loading></Loading>} key={location.pathname}>
<PrivacyPolicy />
</Suspense>
}
/>
<Route
path='/console/chat/:id?'
element={

View File

@@ -37,7 +37,7 @@ import {
isPasskeySupported,
} from '../../helpers';
import Turnstile from 'react-turnstile';
import { Button, Card, Divider, Form, Icon, Modal } from '@douyinfe/semi-ui';
import { Button, Card, Checkbox, Divider, Form, Icon, Modal } from '@douyinfe/semi-ui';
import Title from '@douyinfe/semi-ui/lib/es/typography/title';
import Text from '@douyinfe/semi-ui/lib/es/typography/text';
import TelegramLoginButton from 'react-telegram-login';
@@ -84,6 +84,9 @@ const LoginForm = () => {
const [showTwoFA, setShowTwoFA] = useState(false);
const [passkeySupported, setPasskeySupported] = useState(false);
const [passkeyLoading, setPasskeyLoading] = useState(false);
const [agreedToTerms, setAgreedToTerms] = useState(false);
const [hasUserAgreement, setHasUserAgreement] = useState(false);
const [hasPrivacyPolicy, setHasPrivacyPolicy] = useState(false);
const logo = getLogo();
const systemName = getSystemName();
@@ -103,6 +106,10 @@ const LoginForm = () => {
setTurnstileEnabled(true);
setTurnstileSiteKey(status.turnstile_site_key);
}
// 从 status 获取用户协议和隐私政策的启用状态
setHasUserAgreement(status.user_agreement_enabled || false);
setHasPrivacyPolicy(status.privacy_policy_enabled || false);
}, [status]);
useEffect(() => {
@@ -118,6 +125,10 @@ const LoginForm = () => {
}, []);
const onWeChatLoginClicked = () => {
if ((hasUserAgreement || hasPrivacyPolicy) && !agreedToTerms) {
showInfo(t('请先阅读并同意用户协议和隐私政策'));
return;
}
setWechatLoading(true);
setShowWeChatLoginModal(true);
setWechatLoading(false);
@@ -157,6 +168,10 @@ const LoginForm = () => {
}
async function handleSubmit(e) {
if ((hasUserAgreement || hasPrivacyPolicy) && !agreedToTerms) {
showInfo(t('请先阅读并同意用户协议和隐私政策'));
return;
}
if (turnstileEnabled && turnstileToken === '') {
showInfo('请稍后几秒重试Turnstile 正在检查用户环境!');
return;
@@ -208,6 +223,10 @@ const LoginForm = () => {
// 添加Telegram登录处理函数
const onTelegramLoginClicked = async (response) => {
if ((hasUserAgreement || hasPrivacyPolicy) && !agreedToTerms) {
showInfo(t('请先阅读并同意用户协议和隐私政策'));
return;
}
const fields = [
'id',
'first_name',
@@ -244,6 +263,10 @@ const LoginForm = () => {
// 包装的GitHub登录点击处理
const handleGitHubClick = () => {
if ((hasUserAgreement || hasPrivacyPolicy) && !agreedToTerms) {
showInfo(t('请先阅读并同意用户协议和隐私政策'));
return;
}
setGithubLoading(true);
try {
onGitHubOAuthClicked(status.github_client_id);
@@ -255,6 +278,10 @@ const LoginForm = () => {
// 包装的OIDC登录点击处理
const handleOIDCClick = () => {
if ((hasUserAgreement || hasPrivacyPolicy) && !agreedToTerms) {
showInfo(t('请先阅读并同意用户协议和隐私政策'));
return;
}
setOidcLoading(true);
try {
onOIDCClicked(status.oidc_authorization_endpoint, status.oidc_client_id);
@@ -266,6 +293,10 @@ const LoginForm = () => {
// 包装的LinuxDO登录点击处理
const handleLinuxDOClick = () => {
if ((hasUserAgreement || hasPrivacyPolicy) && !agreedToTerms) {
showInfo(t('请先阅读并同意用户协议和隐私政策'));
return;
}
setLinuxdoLoading(true);
try {
onLinuxDOOAuthClicked(status.linuxdo_client_id);
@@ -283,6 +314,10 @@ const LoginForm = () => {
};
const handlePasskeyLogin = async () => {
if ((hasUserAgreement || hasPrivacyPolicy) && !agreedToTerms) {
showInfo(t('请先阅读并同意用户协议和隐私政策'));
return;
}
if (!passkeySupported) {
showInfo('当前环境无法使用 Passkey 登录');
return;
@@ -486,6 +521,44 @@ const LoginForm = () => {
</Button>
</div>
{(hasUserAgreement || hasPrivacyPolicy) && (
<div className='mt-6'>
<Checkbox
checked={agreedToTerms}
onChange={(e) => setAgreedToTerms(e.target.checked)}
>
<Text size='small' className='text-gray-600'>
{t('我已阅读并同意')}
{hasUserAgreement && (
<>
<a
href='/user-agreement'
target='_blank'
rel='noopener noreferrer'
className='text-blue-600 hover:text-blue-800 mx-1'
>
{t('用户协议')}
</a>
</>
)}
{hasUserAgreement && hasPrivacyPolicy && t('和')}
{hasPrivacyPolicy && (
<>
<a
href='/privacy-policy'
target='_blank'
rel='noopener noreferrer'
className='text-blue-600 hover:text-blue-800 mx-1'
>
{t('隐私政策')}
</a>
</>
)}
</Text>
</Checkbox>
</div>
)}
{!status.self_use_mode_enabled && (
<div className='mt-6 text-center text-sm'>
<Text>
@@ -554,6 +627,44 @@ const LoginForm = () => {
prefix={<IconLock />}
/>
{(hasUserAgreement || hasPrivacyPolicy) && (
<div className='pt-4'>
<Checkbox
checked={agreedToTerms}
onChange={(e) => setAgreedToTerms(e.target.checked)}
>
<Text size='small' className='text-gray-600'>
{t('我已阅读并同意')}
{hasUserAgreement && (
<>
<a
href='/user-agreement'
target='_blank'
rel='noopener noreferrer'
className='text-blue-600 hover:text-blue-800 mx-1'
>
{t('用户协议')}
</a>
</>
)}
{hasUserAgreement && hasPrivacyPolicy && t('和')}
{hasPrivacyPolicy && (
<>
<a
href='/privacy-policy'
target='_blank'
rel='noopener noreferrer'
className='text-blue-600 hover:text-blue-800 mx-1'
>
{t('隐私政策')}
</a>
</>
)}
</Text>
</Checkbox>
</div>
)}
<div className='space-y-2 pt-2'>
<Button
theme='solid'
@@ -562,6 +673,7 @@ const LoginForm = () => {
htmlType='submit'
onClick={handleSubmit}
loading={loginLoading}
disabled={(hasUserAgreement || hasPrivacyPolicy) && !agreedToTerms}
>
{t('继续')}
</Button>

View File

@@ -30,7 +30,7 @@ import {
setUserData,
} from '../../helpers';
import Turnstile from 'react-turnstile';
import { Button, Card, Divider, Form, Icon, Modal } from '@douyinfe/semi-ui';
import { Button, Card, Checkbox, Divider, Form, Icon, Modal } from '@douyinfe/semi-ui';
import Title from '@douyinfe/semi-ui/lib/es/typography/title';
import Text from '@douyinfe/semi-ui/lib/es/typography/text';
import {
@@ -82,6 +82,9 @@ const RegisterForm = () => {
const [wechatCodeSubmitLoading, setWechatCodeSubmitLoading] = useState(false);
const [disableButton, setDisableButton] = useState(false);
const [countdown, setCountdown] = useState(30);
const [agreedToTerms, setAgreedToTerms] = useState(false);
const [hasUserAgreement, setHasUserAgreement] = useState(false);
const [hasPrivacyPolicy, setHasPrivacyPolicy] = useState(false);
const logo = getLogo();
const systemName = getSystemName();
@@ -106,6 +109,10 @@ const RegisterForm = () => {
setTurnstileEnabled(true);
setTurnstileSiteKey(status.turnstile_site_key);
}
// 从 status 获取用户协议和隐私政策的启用状态
setHasUserAgreement(status.user_agreement_enabled || false);
setHasPrivacyPolicy(status.privacy_policy_enabled || false);
}, [status]);
useEffect(() => {
@@ -505,6 +512,44 @@ const RegisterForm = () => {
</>
)}
{(hasUserAgreement || hasPrivacyPolicy) && (
<div className='pt-4'>
<Checkbox
checked={agreedToTerms}
onChange={(e) => setAgreedToTerms(e.target.checked)}
>
<Text size='small' className='text-gray-600'>
{t('我已阅读并同意')}
{hasUserAgreement && (
<>
<a
href='/user-agreement'
target='_blank'
rel='noopener noreferrer'
className='text-blue-600 hover:text-blue-800 mx-1'
>
{t('用户协议')}
</a>
</>
)}
{hasUserAgreement && hasPrivacyPolicy && t('和')}
{hasPrivacyPolicy && (
<>
<a
href='/privacy-policy'
target='_blank'
rel='noopener noreferrer'
className='text-blue-600 hover:text-blue-800 mx-1'
>
{t('隐私政策')}
</a>
</>
)}
</Text>
</Checkbox>
</div>
)}
<div className='space-y-2 pt-2'>
<Button
theme='solid'
@@ -513,6 +558,7 @@ const RegisterForm = () => {
htmlType='submit'
onClick={handleSubmit}
loading={registerLoading}
disabled={(hasUserAgreement || hasPrivacyPolicy) && !agreedToTerms}
>
{t('注册')}
</Button>

View File

@@ -0,0 +1,243 @@
/*
Copyright (C) 2025 QuantumNous
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as
published by the Free Software Foundation, either version 3 of the
License, or (at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
For commercial licensing, please contact support@quantumnous.com
*/
import React, { useEffect, useState } from 'react';
import { API, showError } from '../../../helpers';
import { Empty, Card, Spin, Typography } from '@douyinfe/semi-ui';
const { Title } = Typography;
import {
IllustrationConstruction,
IllustrationConstructionDark,
} from '@douyinfe/semi-illustrations';
import { useTranslation } from 'react-i18next';
import MarkdownRenderer from '../markdown/MarkdownRenderer';
// 检查是否为 URL
const isUrl = (content) => {
try {
new URL(content.trim());
return true;
} catch {
return false;
}
};
// 检查是否为 HTML 内容
const isHtmlContent = (content) => {
if (!content || typeof content !== 'string') return false;
// 检查是否包含HTML标签
const htmlTagRegex = /<\/?[a-z][\s\S]*>/i;
return htmlTagRegex.test(content);
};
// 安全地渲染HTML内容
const sanitizeHtml = (html) => {
// 创建一个临时元素来解析HTML
const tempDiv = document.createElement('div');
tempDiv.innerHTML = html;
// 提取样式
const styles = Array.from(tempDiv.querySelectorAll('style'))
.map(style => style.innerHTML)
.join('\n');
// 提取body内容如果没有body标签则使用全部内容
const bodyContent = tempDiv.querySelector('body');
const content = bodyContent ? bodyContent.innerHTML : html;
return { content, styles };
};
/**
* 通用文档渲染组件
* @param {string} apiEndpoint - API 接口地址
* @param {string} title - 文档标题
* @param {string} cacheKey - 本地存储缓存键
* @param {string} emptyMessage - 空内容时的提示消息
*/
const DocumentRenderer = ({ apiEndpoint, title, cacheKey, emptyMessage }) => {
const { t } = useTranslation();
const [content, setContent] = useState('');
const [loading, setLoading] = useState(true);
const [htmlStyles, setHtmlStyles] = useState('');
const [processedHtmlContent, setProcessedHtmlContent] = useState('');
const loadContent = async () => {
// 先从缓存中获取
const cachedContent = localStorage.getItem(cacheKey) || '';
if (cachedContent) {
setContent(cachedContent);
processContent(cachedContent);
setLoading(false);
}
try {
const res = await API.get(apiEndpoint);
const { success, message, data } = res.data;
if (success && data) {
setContent(data);
processContent(data);
localStorage.setItem(cacheKey, data);
} else {
if (!cachedContent) {
showError(message || emptyMessage);
setContent('');
}
}
} catch (error) {
if (!cachedContent) {
showError(emptyMessage);
setContent('');
}
} finally {
setLoading(false);
}
};
const processContent = (rawContent) => {
if (isHtmlContent(rawContent)) {
const { content: htmlContent, styles } = sanitizeHtml(rawContent);
setProcessedHtmlContent(htmlContent);
setHtmlStyles(styles);
} else {
setProcessedHtmlContent('');
setHtmlStyles('');
}
};
useEffect(() => {
loadContent();
}, []);
// 处理HTML样式注入
useEffect(() => {
const styleId = `document-renderer-styles-${cacheKey}`;
if (htmlStyles) {
let styleEl = document.getElementById(styleId);
if (!styleEl) {
styleEl = document.createElement('style');
styleEl.id = styleId;
styleEl.type = 'text/css';
document.head.appendChild(styleEl);
}
styleEl.innerHTML = htmlStyles;
} else {
const el = document.getElementById(styleId);
if (el) el.remove();
}
return () => {
const el = document.getElementById(styleId);
if (el) el.remove();
};
}, [htmlStyles, cacheKey]);
// 显示加载状态
if (loading) {
return (
<div className='flex justify-center items-center min-h-screen'>
<Spin size='large' />
</div>
);
}
// 如果没有内容,显示空状态
if (!content || content.trim() === '') {
return (
<div className='flex justify-center items-center min-h-screen bg-gray-50'>
<Empty
title={t('管理员未设置' + title + '内容')}
image={<IllustrationConstruction style={{ width: 150, height: 150 }} />}
darkModeImage={<IllustrationConstructionDark style={{ width: 150, height: 150 }} />}
className='p-8'
/>
</div>
);
}
// 如果是 URL显示链接卡片
if (isUrl(content)) {
return (
<div className='flex justify-center items-center min-h-screen bg-gray-50 p-4'>
<Card className='max-w-md w-full'>
<div className='text-center'>
<Title heading={4} className='mb-4'>{title}</Title>
<p className='text-gray-600 mb-4'>
{t('管理员设置了外部链接,点击下方按钮访问')}
</p>
<a
href={content.trim()}
target='_blank'
rel='noopener noreferrer'
title={content.trim()}
aria-label={`${t('访问' + title)}: ${content.trim()}`}
className='inline-block px-6 py-3 bg-blue-600 text-white rounded-lg hover:bg-blue-700 transition-colors'
>
{t('访问' + title)}
</a>
</div>
</Card>
</div>
);
}
// 如果是 HTML 内容,直接渲染
if (isHtmlContent(content)) {
const { content: htmlContent, styles } = sanitizeHtml(content);
// 设置样式(如果有的话)
useEffect(() => {
if (styles && styles !== htmlStyles) {
setHtmlStyles(styles);
}
}, [content, styles, htmlStyles]);
return (
<div className='min-h-screen bg-gray-50'>
<div className='max-w-4xl mx-auto py-12 px-4 sm:px-6 lg:px-8'>
<div className='bg-white rounded-lg shadow-sm p-8'>
<Title heading={2} className='text-center mb-8'>{title}</Title>
<div
className='prose prose-lg max-w-none'
dangerouslySetInnerHTML={{ __html: htmlContent }}
/>
</div>
</div>
</div>
);
}
// 其他内容统一使用 Markdown 渲染器
return (
<div className='min-h-screen bg-gray-50'>
<div className='max-w-4xl mx-auto py-12 px-4 sm:px-6 lg:px-8'>
<div className='bg-white rounded-lg shadow-sm p-8'>
<Title heading={2} className='text-center mb-8'>{title}</Title>
<div className='prose prose-lg max-w-none'>
<MarkdownRenderer content={content} />
</div>
</div>
</div>
</div>
);
};
export default DocumentRenderer;

View File

@@ -34,10 +34,15 @@ import { useTranslation } from 'react-i18next';
import { StatusContext } from '../../context/Status';
import Text from '@douyinfe/semi-ui/lib/es/typography/text';
const LEGAL_USER_AGREEMENT_KEY = 'legal.user_agreement';
const LEGAL_PRIVACY_POLICY_KEY = 'legal.privacy_policy';
const OtherSetting = () => {
const { t } = useTranslation();
let [inputs, setInputs] = useState({
Notice: '',
[LEGAL_USER_AGREEMENT_KEY]: '',
[LEGAL_PRIVACY_POLICY_KEY]: '',
SystemName: '',
Logo: '',
Footer: '',
@@ -69,6 +74,8 @@ const OtherSetting = () => {
const [loadingInput, setLoadingInput] = useState({
Notice: false,
[LEGAL_USER_AGREEMENT_KEY]: false,
[LEGAL_PRIVACY_POLICY_KEY]: false,
SystemName: false,
Logo: false,
HomePageContent: false,
@@ -96,6 +103,50 @@ const OtherSetting = () => {
setLoadingInput((loadingInput) => ({ ...loadingInput, Notice: false }));
}
};
// 通用设置 - UserAgreement
const submitUserAgreement = async () => {
try {
setLoadingInput((loadingInput) => ({
...loadingInput,
[LEGAL_USER_AGREEMENT_KEY]: true,
}));
await updateOption(
LEGAL_USER_AGREEMENT_KEY,
inputs[LEGAL_USER_AGREEMENT_KEY],
);
showSuccess(t('用户协议已更新'));
} catch (error) {
console.error(t('用户协议更新失败'), error);
showError(t('用户协议更新失败'));
} finally {
setLoadingInput((loadingInput) => ({
...loadingInput,
[LEGAL_USER_AGREEMENT_KEY]: false,
}));
}
};
// 通用设置 - PrivacyPolicy
const submitPrivacyPolicy = async () => {
try {
setLoadingInput((loadingInput) => ({
...loadingInput,
[LEGAL_PRIVACY_POLICY_KEY]: true,
}));
await updateOption(
LEGAL_PRIVACY_POLICY_KEY,
inputs[LEGAL_PRIVACY_POLICY_KEY],
);
showSuccess(t('隐私政策已更新'));
} catch (error) {
console.error(t('隐私政策更新失败'), error);
showError(t('隐私政策更新失败'));
} finally {
setLoadingInput((loadingInput) => ({
...loadingInput,
[LEGAL_PRIVACY_POLICY_KEY]: false,
}));
}
};
// 个性化设置
const formAPIPersonalization = useRef();
// 个性化设置 - SystemName
@@ -324,6 +375,40 @@ const OtherSetting = () => {
<Button onClick={submitNotice} loading={loadingInput['Notice']}>
{t('设置公告')}
</Button>
<Form.TextArea
label={t('用户协议')}
placeholder={t(
'在此输入用户协议内容,支持 Markdown & HTML 代码',
)}
field={LEGAL_USER_AGREEMENT_KEY}
onChange={handleInputChange}
style={{ fontFamily: 'JetBrains Mono, Consolas' }}
autosize={{ minRows: 6, maxRows: 12 }}
helpText={t('填写用户协议内容后,用户注册时将被要求勾选已阅读用户协议')}
/>
<Button
onClick={submitUserAgreement}
loading={loadingInput[LEGAL_USER_AGREEMENT_KEY]}
>
{t('设置用户协议')}
</Button>
<Form.TextArea
label={t('隐私政策')}
placeholder={t(
'在此输入隐私政策内容,支持 Markdown & HTML 代码',
)}
field={LEGAL_PRIVACY_POLICY_KEY}
onChange={handleInputChange}
style={{ fontFamily: 'JetBrains Mono, Consolas' }}
autosize={{ minRows: 6, maxRows: 12 }}
helpText={t('填写隐私政策内容后,用户注册时将被要求勾选已阅读隐私政策')}
/>
<Button
onClick={submitPrivacyPolicy}
loading={loadingInput[LEGAL_PRIVACY_POLICY_KEY]}
>
{t('设置隐私政策')}
</Button>
</Form.Section>
</Card>
</Form>

View File

@@ -705,8 +705,15 @@ const SystemSetting = () => {
<Card>
<Form.Section text={t('代理设置')}>
<Banner
type='info'
description={t(
'此代理仅用于图片请求转发Webhook通知发送等AI API请求仍然由服务器直接发出可在渠道设置中单独配置代理',
)}
style={{ marginBottom: 20, marginTop: 16 }}
/>
<Text>
支持{' '}
{t('仅支持')}{' '}
<a
href='https://github.com/Calcium-Ion/new-api-worker'
target='_blank'
@@ -714,7 +721,7 @@ const SystemSetting = () => {
>
new-api-worker
</a>
{' '}{t('或其兼容new-api-worker格式的其他版本')}
</Text>
<Row
gutter={{ xs: 8, sm: 16, md: 24, lg: 24, xl: 24, xxl: 24 }}

View File

@@ -91,7 +91,7 @@ const REGION_EXAMPLE = {
// 支持并且已适配通过接口获取模型列表的渠道类型
const MODEL_FETCHABLE_TYPES = new Set([
1, 4, 14, 34, 17, 26, 24, 47, 25, 20, 23, 31, 35, 40, 42, 48, 43,
1, 4, 14, 34, 17, 26, 27, 24, 47, 25, 20, 23, 31, 35, 40, 42, 48, 43,
]);
function type2secretPrompt(type) {

View File

@@ -102,6 +102,11 @@ const TopupHistoryModal = ({ visible, onCancel, t }) => {
setPage(1);
};
const handleKeywordChange = (value) => {
setKeyword(value);
setPage(1);
};
// 管理员补单
const handleAdminComplete = async (tradeNo) => {
try {
@@ -231,7 +236,7 @@ const TopupHistoryModal = ({ visible, onCancel, t }) => {
prefix={<IconSearch />}
placeholder={t('订单号')}
value={keyword}
onChange={setKeyword}
onChange={handleKeywordChange}
showClear
/>
</div>

View File

@@ -88,6 +88,11 @@ export const CHANNEL_OPTIONS = [
color: 'purple',
label: '智谱 GLM-4V',
},
{
value: 27,
color: 'blue',
label: 'Perplexity',
},
{
value: 24,
color: 'orange',
@@ -169,6 +174,11 @@ export const CHANNEL_OPTIONS = [
color: 'blue',
label: '豆包视频',
},
{
value: 55,
color: 'green',
label: 'Sora',
},
];
export const MODEL_TABLE_PAGE_SIZE = 10;

View File

@@ -54,6 +54,7 @@ import {
FastGPT,
Kling,
Jimeng,
Perplexity,
} from '@lobehub/icons';
import {
@@ -309,6 +310,8 @@ export function getChannelIcon(channelType) {
return <Xinference.Color size={iconSize} />;
case 25: // Moonshot
return <Moonshot size={iconSize} />;
case 27: // Perplexity
return <Perplexity.Color size={iconSize} />;
case 20: // OpenRouter
return <OpenRouter size={iconSize} />;
case 19: // 360 智脑

View File

@@ -377,6 +377,12 @@ export const useLogsData = () => {
other.file_search_call_count || 0,
),
});
if (logs[i]?.content) {
expandDataLocal.push({
key: t('其他详情'),
value: logs[i].content,
});
}
}
if (logs[i].type === 2) {
let modelMapped =

View File

@@ -232,6 +232,7 @@
"邀请新用户奖励额度": "Referral bonus quota",
"新用户使用邀请码奖励额度": "New user invitation code bonus quota",
"保存额度设置": "Save Quota Settings",
"分组与模型定价设置": "Group and Model Pricing Settings",
"倍率设置": "Ratio Settings",
"模型倍率": "Model ratio",
"为一个 JSON 文本": "Is a JSON text",
@@ -244,6 +245,8 @@
"检查更新": "Check for updates",
"公告": "Announcement",
"在此输入新的公告内容,支持 Markdown & HTML 代码": "Enter the new announcement content here, supports Markdown & HTML code",
"在此输入用户协议内容,支持 Markdown & HTML 代码": "Enter user agreement content here, supports Markdown & HTML code",
"在此输入隐私政策内容,支持 Markdown & HTML 代码": "Enter privacy policy content here, supports Markdown & HTML code",
"保存公告": "Save Announcement",
"个性化设置": "Personalization Settings",
"系统名称": "System Name",
@@ -1260,6 +1263,8 @@
"仅修改展示粒度,统计精确到小时": "Only modify display granularity, statistics accurate to the hour",
"当运行通道全部测试时,超过此时间将自动禁用通道": "When running all channel tests, the channel will be automatically disabled when this time is exceeded",
"设置公告": "Set notice",
"设置用户协议": "Set user agreement",
"设置隐私政策": "Set privacy policy",
"设置 Logo": "Set Logo",
"设置首页内容": "Set home page content",
"设置关于": "Set about",
@@ -2259,5 +2264,21 @@
"补单成功": "Order completed successfully",
"补单失败": "Failed to complete order",
"确认补单": "Confirm Order Completion",
"是否将该订单标记为成功并为用户入账?": "Mark this order as successful and credit the user?"
"是否将该订单标记为成功并为用户入账?": "Mark this order as successful and credit the user?",
"用户协议": "User Agreement",
"隐私政策": "Privacy Policy",
"用户协议更新失败": "Failed to update user agreement",
"隐私政策更新失败": "Failed to update privacy policy",
"管理员未设置用户协议内容": "Administrator has not set user agreement content",
"管理员未设置隐私政策内容": "Administrator has not set privacy policy content",
"加载用户协议内容失败...": "Failed to load user agreement content...",
"加载隐私政策内容失败...": "Failed to load privacy policy content...",
"我已阅读并同意": "I have read and agree to",
"和": " and ",
"请先阅读并同意用户协议和隐私政策": "Please read and agree to the user agreement and privacy policy first",
"填写用户协议内容后,用户注册时将被要求勾选已阅读用户协议": "After filling in the user agreement content, users will be required to check that they have read the user agreement when registering",
"填写隐私政策内容后,用户注册时将被要求勾选已阅读隐私政策": "After filling in the privacy policy content, users will be required to check that they have read the privacy policy when registering",
"管理员设置了外部链接,点击下方按钮访问": "Administrator has set an external link, click the button below to access",
"访问用户协议": "Access User Agreement",
"访问隐私政策": "Access Privacy Policy"
}

View File

@@ -231,6 +231,7 @@
"邀请新用户奖励额度": "Quota de bonus de parrainage",
"新用户使用邀请码奖励额度": "Quota de bonus de code d'invitation pour nouvel utilisateur",
"保存额度设置": "Enregistrer les paramètres de quota",
"分组与模型定价设置": "Paramètres de groupe et de tarification du modèle",
"倍率设置": "Paramètres de ratio",
"模型倍率": "Ratio de modèle",
"为一个 JSON 文本": "Est un texte JSON",
@@ -2251,5 +2252,27 @@
"补单成功": "Commande complétée avec succès",
"补单失败": "Échec de la complétion de la commande",
"确认补单": "Confirmer la complétion",
"是否将该订单标记为成功并为用户入账?": "Marquer cette commande comme réussie et créditer l'utilisateur ?"
"是否将该订单标记为成功并为用户入账?": "Marquer cette commande comme réussie et créditer l'utilisateur ?",
"用户协议": "Accord utilisateur",
"隐私政策": "Politique de confidentialité",
"在此输入用户协议内容,支持 Markdown & HTML 代码": "Saisissez ici le contenu de l'accord utilisateur, prend en charge le code Markdown et HTML",
"在此输入隐私政策内容,支持 Markdown & HTML 代码": "Saisissez ici le contenu de la politique de confidentialité, prend en charge le code Markdown et HTML",
"设置用户协议": "Définir l'accord utilisateur",
"设置隐私政策": "Définir la politique de confidentialité",
"用户协议已更新": "L'accord utilisateur a été mis à jour",
"隐私政策已更新": "La politique de confidentialité a été mise à jour",
"用户协议更新失败": "Échec de la mise à jour de l'accord utilisateur",
"隐私政策更新失败": "Échec de la mise à jour de la politique de confidentialité",
"管理员未设置用户协议内容": "L'administrateur n'a pas défini le contenu de l'accord utilisateur",
"管理员未设置隐私政策内容": "L'administrateur n'a pas défini le contenu de la politique de confidentialité",
"加载用户协议内容失败...": "Échec du chargement du contenu de l'accord utilisateur...",
"加载隐私政策内容失败...": "Échec du chargement du contenu de la politique de confidentialité...",
"我已阅读并同意": "J'ai lu et j'accepte",
"和": " et ",
"请先阅读并同意用户协议和隐私政策": "Veuillez d'abord lire et accepter l'accord utilisateur et la politique de confidentialité",
"填写用户协议内容后,用户注册时将被要求勾选已阅读用户协议": "Après avoir rempli le contenu de l'accord utilisateur, les utilisateurs devront cocher qu'ils ont lu l'accord utilisateur lors de l'inscription",
"填写隐私政策内容后,用户注册时将被要求勾选已阅读隐私政策": "Après avoir rempli le contenu de la politique de confidentialité, les utilisateurs devront cocher qu'ils ont lu la politique de confidentialité lors de l'inscription",
"管理员设置了外部链接,点击下方按钮访问": "L'administrateur a défini un lien externe, cliquez sur le bouton ci-dessous pour accéder",
"访问用户协议": "Accéder à l'accord utilisateur",
"访问隐私政策": "Accéder à la politique de confidentialité"
}

View File

@@ -111,5 +111,27 @@
"补单失败": "补单失败",
"确认补单": "确认补单",
"是否将该订单标记为成功并为用户入账?": "是否将该订单标记为成功并为用户入账?",
"操作": "操作"
"操作": "操作",
"用户协议": "用户协议",
"隐私政策": "隐私政策",
"在此输入用户协议内容,支持 Markdown & HTML 代码": "在此输入用户协议内容,支持 Markdown & HTML 代码",
"在此输入隐私政策内容,支持 Markdown & HTML 代码": "在此输入隐私政策内容,支持 Markdown & HTML 代码",
"设置用户协议": "设置用户协议",
"设置隐私政策": "设置隐私政策",
"用户协议已更新": "用户协议已更新",
"隐私政策已更新": "隐私政策已更新",
"用户协议更新失败": "用户协议更新失败",
"隐私政策更新失败": "隐私政策更新失败",
"管理员未设置用户协议内容": "管理员未设置用户协议内容",
"管理员未设置隐私政策内容": "管理员未设置隐私政策内容",
"加载用户协议内容失败...": "加载用户协议内容失败...",
"加载隐私政策内容失败...": "加载隐私政策内容失败...",
"我已阅读并同意": "我已阅读并同意",
"和": "和",
"请先阅读并同意用户协议和隐私政策": "请先阅读并同意用户协议和隐私政策",
"填写用户协议内容后,用户注册时将被要求勾选已阅读用户协议": "填写用户协议内容后,用户注册时将被要求勾选已阅读用户协议",
"填写隐私政策内容后,用户注册时将被要求勾选已阅读隐私政策": "填写隐私政策内容后,用户注册时将被要求勾选已阅读隐私政策",
"管理员设置了外部链接,点击下方按钮访问": "管理员设置了外部链接,点击下方按钮访问",
"访问用户协议": "访问用户协议",
"访问隐私政策": "访问隐私政策"
}

View File

@@ -0,0 +1,37 @@
/*
Copyright (C) 2025 QuantumNous
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as
published by the Free Software Foundation, either version 3 of the
License, or (at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
For commercial licensing, please contact support@quantumnous.com
*/
import React from 'react';
import { useTranslation } from 'react-i18next';
import DocumentRenderer from '../../components/common/DocumentRenderer';
const PrivacyPolicy = () => {
const { t } = useTranslation();
return (
<DocumentRenderer
apiEndpoint="/api/privacy-policy"
title={t('隐私政策')}
cacheKey="privacy_policy"
emptyMessage={t('加载隐私政策内容失败...')}
/>
);
};
export default PrivacyPolicy;

View File

@@ -108,7 +108,7 @@ const Setting = () => {
tab: (
<span style={{ display: 'flex', alignItems: 'center', gap: '5px' }}>
<Calculator size={18} />
{t('倍率设置')}
{t('分组与模型定价设置')}
</span>
),
content: <RatioSetting />,

View File

@@ -0,0 +1,37 @@
/*
Copyright (C) 2025 QuantumNous
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as
published by the Free Software Foundation, either version 3 of the
License, or (at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
For commercial licensing, please contact support@quantumnous.com
*/
import React from 'react';
import { useTranslation } from 'react-i18next';
import DocumentRenderer from '../../components/common/DocumentRenderer';
const UserAgreement = () => {
const { t } = useTranslation();
return (
<DocumentRenderer
apiEndpoint="/api/user-agreement"
title={t('用户协议')}
cacheKey="user_agreement"
emptyMessage={t('加载用户协议内容失败...')}
/>
);
};
export default UserAgreement;