mirror of
https://github.com/QuantumNous/new-api.git
synced 2026-03-30 19:23:00 +00:00
Compare commits
195 Commits
v0.9.1.3
...
v0.9.7-pat
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
826ef2e5a6 | ||
|
|
7311c18d52 | ||
|
|
4a4238d830 | ||
|
|
9805b0f3b0 | ||
|
|
dfca9681c8 | ||
|
|
a6e6897f63 | ||
|
|
ec0633bdfb | ||
|
|
2d1534dc77 | ||
|
|
eebd7ca0f3 | ||
|
|
98e3e5ca2c | ||
|
|
e5dde67272 | ||
|
|
d2546cf9ec | ||
|
|
ede47ef014 | ||
|
|
6c7795238f | ||
|
|
0baacb2686 | ||
|
|
c5aaee9f2f | ||
|
|
1987c7e16c | ||
|
|
c13bb67360 | ||
|
|
77f8e51b56 | ||
|
|
e08f799994 | ||
|
|
cc41ac63bf | ||
|
|
4e0f4b207d | ||
|
|
8a7033e5a3 | ||
|
|
7cb60c7d83 | ||
|
|
e1c7a4f41f | ||
|
|
2d3fd5634a | ||
|
|
5cfa64838c | ||
|
|
55afd5cbdf | ||
|
|
65920983cc | ||
|
|
ced72951e4 | ||
|
|
0ff98b1dc1 | ||
|
|
5d4a0757f7 | ||
|
|
07b099006c | ||
|
|
5fbf860020 | ||
|
|
eab768b4a0 | ||
|
|
1031f1ddf0 | ||
|
|
63c01016e4 | ||
|
|
5810c05dab | ||
|
|
c29eef9b15 | ||
|
|
b472f9c5b5 | ||
|
|
a34d8f586e | ||
|
|
c7661167cf | ||
|
|
5f36e32821 | ||
|
|
11e8e4e7a6 | ||
|
|
35422b316d | ||
|
|
df0ae9294d | ||
|
|
57e5d67f86 | ||
|
|
7351480365 | ||
|
|
e19e904179 | ||
|
|
a54baf4998 | ||
|
|
721357b4a4 | ||
|
|
ff9f9fbbc9 | ||
|
|
9b551d978d | ||
|
|
76ab8a480a | ||
|
|
f091f663c2 | ||
|
|
e8966c7374 | ||
|
|
5a7f498629 | ||
|
|
4c1f138c0a | ||
|
|
f4d7bde20b | ||
|
|
0c181395b4 | ||
|
|
6897a9ffd8 | ||
|
|
77130dfb87 | ||
|
|
614abc3441 | ||
|
|
2479da4986 | ||
|
|
7b732ec4b7 | ||
|
|
0fed791ad9 | ||
|
|
7de02991a1 | ||
|
|
3c57cfbf71 | ||
|
|
fe9b305232 | ||
|
|
17dafa3b03 | ||
|
|
5f5b9425df | ||
|
|
b880094296 | ||
|
|
9c37b63f2e | ||
|
|
9f4a2d64a3 | ||
|
|
e24f13a277 | ||
|
|
d67c57eaa5 | ||
|
|
60dc910a27 | ||
|
|
629a534798 | ||
|
|
15a7edf6d6 | ||
|
|
cdd2eb517e | ||
|
|
1a398bbc40 | ||
|
|
581c51f312 | ||
|
|
8f00af181b | ||
|
|
0c417e8ec6 | ||
|
|
f930cdbb51 | ||
|
|
4d0a9d9494 | ||
|
|
6891057647 | ||
|
|
a610ef48e4 | ||
|
|
ddf5c85b81 | ||
|
|
ec590d1075 | ||
|
|
a8c9b24c7e | ||
|
|
2389dbafc5 | ||
|
|
6ef95c97cc | ||
|
|
2397ec8075 | ||
|
|
c24608730b | ||
|
|
ca9ee54fba | ||
|
|
bb0ed4dddf | ||
|
|
407da544fe | ||
|
|
98261ec9fa | ||
|
|
74f93d41f3 | ||
|
|
021892b17d | ||
|
|
9f44116260 | ||
|
|
8a56795bd8 | ||
|
|
1154077eea | ||
|
|
42861bc5fb | ||
|
|
7074ea2ed6 | ||
|
|
414be64d33 | ||
|
|
c1137027e6 | ||
|
|
ff77ba1157 | ||
|
|
3da7cebec6 | ||
|
|
7dc5f8c92d | ||
|
|
7763f11da7 | ||
|
|
7437b671ef | ||
|
|
55d19df029 | ||
|
|
731e9f4ca9 | ||
|
|
cc6fcebda1 | ||
|
|
72a12e3747 | ||
|
|
0adfcf9d27 | ||
|
|
51d71a6e1a | ||
|
|
8026e5142b | ||
|
|
9e33c83351 | ||
|
|
d2492d2af9 | ||
|
|
c9abe1d769 | ||
|
|
a8bfa7ad29 | ||
|
|
93e30703d4 | ||
|
|
b39885be1e | ||
|
|
f24feed775 | ||
|
|
6b75bc0016 | ||
|
|
937d931442 | ||
|
|
5c6e6032ef | ||
|
|
d5e01a3eab | ||
|
|
69e1542fc9 | ||
|
|
3199e2e8cd | ||
|
|
66d0764fc1 | ||
|
|
01bcbf09c6 | ||
|
|
0682a15971 | ||
|
|
19bbb7d7c7 | ||
|
|
ace855ed36 | ||
|
|
36ed41ad7a | ||
|
|
df19a8de5d | ||
|
|
0074085b13 | ||
|
|
f473d20a09 | ||
|
|
9061411ec7 | ||
|
|
6ee01d75a6 | ||
|
|
b0b275b236 | ||
|
|
81a66be721 | ||
|
|
01469aa01c | ||
|
|
0769184b9b | ||
|
|
15b21c075f | ||
|
|
4137120d69 | ||
|
|
3d1433dd70 | ||
|
|
acbfc9d3b3 | ||
|
|
4cdad47695 | ||
|
|
6a1de0ebdc | ||
|
|
92a4e88ceb | ||
|
|
e9043590a9 | ||
|
|
9e6828653b | ||
|
|
dae661bb53 | ||
|
|
649a5205c9 | ||
|
|
26a563da54 | ||
|
|
c1492be131 | ||
|
|
7ca65a5e8e | ||
|
|
b244a06ca1 | ||
|
|
c320410c84 | ||
|
|
2938246f2e | ||
|
|
0e9ad4a15f | ||
|
|
2200bb9166 | ||
|
|
d6db10b4bc | ||
|
|
85ff8b1422 | ||
|
|
1428338546 | ||
|
|
ec76b0f5e2 | ||
|
|
96b172e93b | ||
|
|
70263e96ab | ||
|
|
15db5c0062 | ||
|
|
f5a774f22c | ||
|
|
0735b0c604 | ||
|
|
0b91e45197 | ||
|
|
6bc3e62fd5 | ||
|
|
922ecef31e | ||
|
|
573b5c3e3b | ||
|
|
c4e0fc1837 | ||
|
|
aab82f22fa | ||
|
|
39a868faea | ||
|
|
7c7f9abd04 | ||
|
|
050e0221c7 | ||
|
|
a57a36a739 | ||
|
|
0046282fb8 | ||
|
|
723eefe9d8 | ||
|
|
3ed0ae83f1 | ||
|
|
f354e5de23 | ||
|
|
ec9903e640 | ||
|
|
8d92ce38ed | ||
|
|
f2e9fd7afb | ||
|
|
465830945b | ||
|
|
2488e6ab66 |
@@ -5,4 +5,5 @@
|
||||
.gitignore
|
||||
Makefile
|
||||
docs
|
||||
.eslintcache
|
||||
.eslintcache
|
||||
.gocache
|
||||
26
.github/ISSUE_TEMPLATE/bug_report_en.md
vendored
Normal file
26
.github/ISSUE_TEMPLATE/bug_report_en.md
vendored
Normal file
@@ -0,0 +1,26 @@
|
||||
---
|
||||
name: Bug Report
|
||||
about: Describe the issue you encountered with clear and detailed language
|
||||
title: ''
|
||||
labels: bug
|
||||
assignees: ''
|
||||
|
||||
---
|
||||
|
||||
**Routine Checks**
|
||||
|
||||
[//]: # (Remove the space in the box and fill with an x)
|
||||
+ [ ] I have confirmed there are no similar issues currently
|
||||
+ [ ] I have confirmed I have upgraded to the latest version
|
||||
+ [ ] I have thoroughly read the project README, especially the FAQ section
|
||||
+ [ ] I understand and am willing to follow up on this issue, assist with testing and provide feedback
|
||||
+ [ ] I understand and acknowledge the above, and understand that project maintainers have limited time and energy, **issues that do not follow the rules may be ignored or closed directly**
|
||||
|
||||
**Issue Description**
|
||||
|
||||
**Steps to Reproduce**
|
||||
|
||||
**Expected Result**
|
||||
|
||||
**Related Screenshots**
|
||||
If none, please delete this section.
|
||||
22
.github/ISSUE_TEMPLATE/feature_request_en.md
vendored
Normal file
22
.github/ISSUE_TEMPLATE/feature_request_en.md
vendored
Normal file
@@ -0,0 +1,22 @@
|
||||
---
|
||||
name: Feature Request
|
||||
about: Describe the new feature you would like to add with clear and detailed language
|
||||
title: ''
|
||||
labels: enhancement
|
||||
assignees: ''
|
||||
|
||||
---
|
||||
|
||||
**Routine Checks**
|
||||
|
||||
[//]: # (Remove the space in the box and fill with an x)
|
||||
+ [ ] I have confirmed there are no similar issues currently
|
||||
+ [ ] I have confirmed I have upgraded to the latest version
|
||||
+ [ ] I have thoroughly read the project README and confirmed the current version cannot meet my needs
|
||||
+ [ ] I understand and am willing to follow up on this issue, assist with testing and provide feedback
|
||||
+ [ ] I understand and acknowledge the above, and understand that project maintainers have limited time and energy, **issues that do not follow the rules may be ignored or closed directly**
|
||||
|
||||
**Feature Description**
|
||||
|
||||
**Use Case**
|
||||
|
||||
@@ -13,7 +13,3 @@
|
||||
### PR 描述
|
||||
|
||||
**请在下方详细描述您的 PR,包括目的、实现细节等。**
|
||||
|
||||
### **重要提示**
|
||||
|
||||
**所有 PR 都必须提交到 `alpha` 分支。请确保您的 PR 目标分支是 `alpha`。**
|
||||
|
||||
127
.github/workflows/docker-image-alpha.yml
vendored
127
.github/workflows/docker-image-alpha.yml
vendored
@@ -11,19 +11,42 @@ on:
|
||||
required: false
|
||||
|
||||
jobs:
|
||||
push_to_registries:
|
||||
name: Push Docker image to multiple registries
|
||||
runs-on: ubuntu-latest
|
||||
build_single_arch:
|
||||
name: Build & push (${{ matrix.arch }}) [native]
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
include:
|
||||
- arch: amd64
|
||||
platform: linux/amd64
|
||||
runner: ubuntu-latest
|
||||
- arch: arm64
|
||||
platform: linux/arm64
|
||||
runner: ubuntu-24.04-arm
|
||||
runs-on: ${{ matrix.runner }}
|
||||
permissions:
|
||||
packages: write
|
||||
contents: read
|
||||
steps:
|
||||
- name: Check out the repo
|
||||
- name: Check out (shallow)
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 1
|
||||
|
||||
- name: Save version info
|
||||
- name: Determine alpha version
|
||||
id: version
|
||||
run: |
|
||||
echo "alpha-$(date +'%Y%m%d')-$(git rev-parse --short HEAD)" > VERSION
|
||||
VERSION="alpha-$(date +'%Y%m%d')-$(git rev-parse --short HEAD)"
|
||||
echo "$VERSION" > VERSION
|
||||
echo "value=$VERSION" >> $GITHUB_OUTPUT
|
||||
echo "VERSION=$VERSION" >> $GITHUB_ENV
|
||||
echo "Publishing version: $VERSION for ${{ matrix.arch }}"
|
||||
|
||||
- name: Normalize GHCR repository
|
||||
run: echo "GHCR_REPOSITORY=${GITHUB_REPOSITORY,,}" >> $GITHUB_ENV
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v3
|
||||
|
||||
- name: Log in to Docker Hub
|
||||
uses: docker/login-action@v3
|
||||
@@ -31,32 +54,98 @@ jobs:
|
||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||
|
||||
- name: Log in to the Container registry
|
||||
- name: Log in to GHCR
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.actor }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v3
|
||||
|
||||
- name: Extract metadata (tags, labels) for Docker
|
||||
- name: Extract metadata (labels)
|
||||
id: meta
|
||||
uses: docker/metadata-action@v5
|
||||
with:
|
||||
images: |
|
||||
calciumion/new-api
|
||||
ghcr.io/${{ github.repository }}
|
||||
tags: |
|
||||
type=raw,value=alpha
|
||||
type=raw,value=alpha-{{date 'YYYYMMDD'}}-{{sha}}
|
||||
ghcr.io/${{ env.GHCR_REPOSITORY }}
|
||||
|
||||
- name: Build and push Docker images
|
||||
uses: docker/build-push-action@v5
|
||||
- name: Build & push single-arch (to both registries)
|
||||
uses: docker/build-push-action@v6
|
||||
with:
|
||||
context: .
|
||||
platforms: linux/amd64,linux/arm64
|
||||
platforms: ${{ matrix.platform }}
|
||||
push: true
|
||||
tags: ${{ steps.meta.outputs.tags }}
|
||||
tags: |
|
||||
calciumion/new-api:alpha-${{ matrix.arch }}
|
||||
calciumion/new-api:${{ steps.version.outputs.value }}-${{ matrix.arch }}
|
||||
ghcr.io/${{ env.GHCR_REPOSITORY }}:alpha-${{ matrix.arch }}
|
||||
ghcr.io/${{ env.GHCR_REPOSITORY }}:${{ steps.version.outputs.value }}-${{ matrix.arch }}
|
||||
labels: ${{ steps.meta.outputs.labels }}
|
||||
cache-from: type=gha
|
||||
cache-to: type=gha,mode=max
|
||||
provenance: false
|
||||
sbom: false
|
||||
|
||||
create_manifests:
|
||||
name: Create multi-arch manifests (Docker Hub + GHCR)
|
||||
needs: [build_single_arch]
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
packages: write
|
||||
contents: read
|
||||
steps:
|
||||
- name: Check out (shallow)
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 1
|
||||
|
||||
- name: Normalize GHCR repository
|
||||
run: echo "GHCR_REPOSITORY=${GITHUB_REPOSITORY,,}" >> $GITHUB_ENV
|
||||
|
||||
- name: Determine alpha version
|
||||
id: version
|
||||
run: |
|
||||
VERSION="alpha-$(date +'%Y%m%d')-$(git rev-parse --short HEAD)"
|
||||
echo "value=$VERSION" >> $GITHUB_OUTPUT
|
||||
echo "VERSION=$VERSION" >> $GITHUB_ENV
|
||||
|
||||
- name: Log in to Docker Hub
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||
|
||||
- name: Create & push manifest (Docker Hub - alpha)
|
||||
run: |
|
||||
docker buildx imagetools create \
|
||||
-t calciumion/new-api:alpha \
|
||||
calciumion/new-api:alpha-amd64 \
|
||||
calciumion/new-api:alpha-arm64
|
||||
|
||||
- name: Create & push manifest (Docker Hub - versioned alpha)
|
||||
run: |
|
||||
docker buildx imagetools create \
|
||||
-t calciumion/new-api:${VERSION} \
|
||||
calciumion/new-api:${VERSION}-amd64 \
|
||||
calciumion/new-api:${VERSION}-arm64
|
||||
|
||||
- name: Log in to GHCR
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.actor }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Create & push manifest (GHCR - alpha)
|
||||
run: |
|
||||
docker buildx imagetools create \
|
||||
-t ghcr.io/${GHCR_REPOSITORY}:alpha \
|
||||
ghcr.io/${GHCR_REPOSITORY}:alpha-amd64 \
|
||||
ghcr.io/${GHCR_REPOSITORY}:alpha-arm64
|
||||
|
||||
- name: Create & push manifest (GHCR - versioned alpha)
|
||||
run: |
|
||||
docker buildx imagetools create \
|
||||
-t ghcr.io/${GHCR_REPOSITORY}:${VERSION} \
|
||||
ghcr.io/${GHCR_REPOSITORY}:${VERSION}-amd64 \
|
||||
ghcr.io/${GHCR_REPOSITORY}:${VERSION}-arm64
|
||||
|
||||
126
.github/workflows/docker-image-arm64.yml
vendored
126
.github/workflows/docker-image-arm64.yml
vendored
@@ -1,26 +1,46 @@
|
||||
name: Publish Docker image (Multi Registries)
|
||||
name: Publish Docker image (Multi Registries, native amd64+arm64)
|
||||
|
||||
on:
|
||||
push:
|
||||
tags:
|
||||
- '*'
|
||||
|
||||
jobs:
|
||||
push_to_registries:
|
||||
name: Push Docker image to multiple registries
|
||||
runs-on: ubuntu-latest
|
||||
build_single_arch:
|
||||
name: Build & push (${{ matrix.arch }}) [native]
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
include:
|
||||
- arch: amd64
|
||||
platform: linux/amd64
|
||||
runner: ubuntu-latest
|
||||
- arch: arm64
|
||||
platform: linux/arm64
|
||||
runner: ubuntu-24.04-arm
|
||||
runs-on: ${{ matrix.runner }}
|
||||
|
||||
permissions:
|
||||
packages: write
|
||||
contents: read
|
||||
|
||||
steps:
|
||||
- name: Check out the repo
|
||||
- name: Check out (shallow)
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 1
|
||||
|
||||
- name: Save version info
|
||||
- name: Resolve tag & write VERSION
|
||||
run: |
|
||||
git describe --tags > VERSION
|
||||
git fetch --tags --force --depth=1
|
||||
TAG=${GITHUB_REF#refs/tags/}
|
||||
echo "TAG=$TAG" >> $GITHUB_ENV
|
||||
echo "$TAG" > VERSION
|
||||
echo "Building tag: $TAG for ${{ matrix.arch }}"
|
||||
|
||||
- name: Set up QEMU
|
||||
uses: docker/setup-qemu-action@v3
|
||||
|
||||
# - name: Normalize GHCR repository
|
||||
# run: echo "GHCR_REPOSITORY=${GITHUB_REPOSITORY,,}" >> $GITHUB_ENV
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v3
|
||||
@@ -31,26 +51,88 @@ jobs:
|
||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||
|
||||
- name: Log in to the Container registry
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.actor }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
# - name: Log in to GHCR
|
||||
# uses: docker/login-action@v3
|
||||
# with:
|
||||
# registry: ghcr.io
|
||||
# username: ${{ github.actor }}
|
||||
# password: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Extract metadata (tags, labels) for Docker
|
||||
- name: Extract metadata (labels)
|
||||
id: meta
|
||||
uses: docker/metadata-action@v5
|
||||
with:
|
||||
images: |
|
||||
calciumion/new-api
|
||||
ghcr.io/${{ github.repository }}
|
||||
# ghcr.io/${{ env.GHCR_REPOSITORY }}
|
||||
|
||||
- name: Build and push Docker images
|
||||
uses: docker/build-push-action@v5
|
||||
- name: Build & push single-arch (to both registries)
|
||||
uses: docker/build-push-action@v6
|
||||
with:
|
||||
context: .
|
||||
platforms: linux/amd64,linux/arm64
|
||||
platforms: ${{ matrix.platform }}
|
||||
push: true
|
||||
tags: ${{ steps.meta.outputs.tags }}
|
||||
labels: ${{ steps.meta.outputs.labels }}
|
||||
tags: |
|
||||
calciumion/new-api:${{ env.TAG }}-${{ matrix.arch }}
|
||||
calciumion/new-api:latest-${{ matrix.arch }}
|
||||
# ghcr.io/${{ env.GHCR_REPOSITORY }}:${{ env.TAG }}-${{ matrix.arch }}
|
||||
# ghcr.io/${{ env.GHCR_REPOSITORY }}:latest-${{ matrix.arch }}
|
||||
labels: ${{ steps.meta.outputs.labels }}
|
||||
cache-from: type=gha
|
||||
cache-to: type=gha,mode=max
|
||||
provenance: false
|
||||
sbom: false
|
||||
|
||||
create_manifests:
|
||||
name: Create multi-arch manifests (Docker Hub)
|
||||
needs: [build_single_arch]
|
||||
runs-on: ubuntu-latest
|
||||
if: startsWith(github.ref, 'refs/tags/')
|
||||
steps:
|
||||
- name: Extract tag
|
||||
run: echo "TAG=${GITHUB_REF#refs/tags/}" >> $GITHUB_ENV
|
||||
#
|
||||
# - name: Normalize GHCR repository
|
||||
# run: echo "GHCR_REPOSITORY=${GITHUB_REPOSITORY,,}" >> $GITHUB_ENV
|
||||
|
||||
- name: Log in to Docker Hub
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||
|
||||
- name: Create & push manifest (Docker Hub - version)
|
||||
run: |
|
||||
docker buildx imagetools create \
|
||||
-t calciumion/new-api:${TAG} \
|
||||
calciumion/new-api:${TAG}-amd64 \
|
||||
calciumion/new-api:${TAG}-arm64
|
||||
|
||||
- name: Create & push manifest (Docker Hub - latest)
|
||||
run: |
|
||||
docker buildx imagetools create \
|
||||
-t calciumion/new-api:latest \
|
||||
calciumion/new-api:latest-amd64 \
|
||||
calciumion/new-api:latest-arm64
|
||||
|
||||
# ---- GHCR ----
|
||||
# - name: Log in to GHCR
|
||||
# uses: docker/login-action@v3
|
||||
# with:
|
||||
# registry: ghcr.io
|
||||
# username: ${{ github.actor }}
|
||||
# password: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
# - name: Create & push manifest (GHCR - version)
|
||||
# run: |
|
||||
# docker buildx imagetools create \
|
||||
# -t ghcr.io/${GHCR_REPOSITORY}:${TAG} \
|
||||
# ghcr.io/${GHCR_REPOSITORY}:${TAG}-amd64 \
|
||||
# ghcr.io/${GHCR_REPOSITORY}:${TAG}-arm64
|
||||
#
|
||||
# - name: Create & push manifest (GHCR - latest)
|
||||
# run: |
|
||||
# docker buildx imagetools create \
|
||||
# -t ghcr.io/${GHCR_REPOSITORY}:latest \
|
||||
# ghcr.io/${GHCR_REPOSITORY}:latest-amd64 \
|
||||
# ghcr.io/${GHCR_REPOSITORY}:latest-arm64
|
||||
|
||||
139
.github/workflows/electron-build.yml
vendored
Normal file
139
.github/workflows/electron-build.yml
vendored
Normal file
@@ -0,0 +1,139 @@
|
||||
name: Build Electron App
|
||||
|
||||
on:
|
||||
push:
|
||||
tags:
|
||||
- '*' # Triggers on version tags like v1.0.0
|
||||
workflow_dispatch: # Allows manual triggering
|
||||
|
||||
jobs:
|
||||
build:
|
||||
strategy:
|
||||
matrix:
|
||||
# os: [macos-latest, windows-latest]
|
||||
os: [windows-latest]
|
||||
|
||||
runs-on: ${{ matrix.os }}
|
||||
defaults:
|
||||
run:
|
||||
shell: bash
|
||||
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Setup Bun
|
||||
uses: oven-sh/setup-bun@v2
|
||||
with:
|
||||
bun-version: latest
|
||||
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: '20'
|
||||
|
||||
- name: Setup Go
|
||||
uses: actions/setup-go@v5
|
||||
with:
|
||||
go-version: '>=1.25.1'
|
||||
|
||||
- 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: Build Go binary (macos/Linux)
|
||||
# if: runner.os != 'Windows'
|
||||
# run: |
|
||||
# go mod download
|
||||
# go build -ldflags "-s -w -X 'new-api/common.Version=$(git describe --tags)' -extldflags '-static'" -o new-api
|
||||
|
||||
- name: Build Go binary (Windows)
|
||||
if: runner.os == 'Windows'
|
||||
run: |
|
||||
go mod download
|
||||
go build -ldflags "-s -w -X 'new-api/common.Version=$(git describe --tags)'" -o new-api.exe
|
||||
|
||||
- name: Update Electron version
|
||||
run: |
|
||||
cd electron
|
||||
VERSION=$(git describe --tags)
|
||||
VERSION=${VERSION#v} # Remove 'v' prefix if present
|
||||
# Convert to valid semver: take first 3 components and convert rest to prerelease format
|
||||
# e.g., 0.9.3-patch.1 -> 0.9.3-patch.1
|
||||
if [[ $VERSION =~ ^([0-9]+)\.([0-9]+)\.([0-9]+)(.*)$ ]]; then
|
||||
MAJOR=${BASH_REMATCH[1]}
|
||||
MINOR=${BASH_REMATCH[2]}
|
||||
PATCH=${BASH_REMATCH[3]}
|
||||
REST=${BASH_REMATCH[4]}
|
||||
|
||||
VERSION="$MAJOR.$MINOR.$PATCH"
|
||||
|
||||
# If there's extra content, append it without adding -dev
|
||||
if [[ -n "$REST" ]]; then
|
||||
VERSION="$VERSION$REST"
|
||||
fi
|
||||
fi
|
||||
npm version $VERSION --no-git-tag-version --allow-same-version
|
||||
|
||||
- name: Install Electron dependencies
|
||||
run: |
|
||||
cd electron
|
||||
npm install
|
||||
|
||||
# - name: Build Electron app (macOS)
|
||||
# if: runner.os == 'macOS'
|
||||
# run: |
|
||||
# cd electron
|
||||
# npm run build:mac
|
||||
# env:
|
||||
# CSC_IDENTITY_AUTO_DISCOVERY: false # Skip code signing
|
||||
|
||||
- name: Build Electron app (Windows)
|
||||
if: runner.os == 'Windows'
|
||||
run: |
|
||||
cd electron
|
||||
npm run build:win
|
||||
|
||||
# - name: Upload artifacts (macOS)
|
||||
# if: runner.os == 'macOS'
|
||||
# uses: actions/upload-artifact@v4
|
||||
# with:
|
||||
# name: macos-build
|
||||
# path: |
|
||||
# electron/dist/*.dmg
|
||||
# electron/dist/*.zip
|
||||
|
||||
- name: Upload artifacts (Windows)
|
||||
if: runner.os == 'Windows'
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: windows-build
|
||||
path: |
|
||||
electron/dist/*.exe
|
||||
|
||||
release:
|
||||
needs: build
|
||||
runs-on: ubuntu-latest
|
||||
if: startsWith(github.ref, 'refs/tags/')
|
||||
permissions:
|
||||
contents: write
|
||||
|
||||
steps:
|
||||
- name: Download all artifacts
|
||||
uses: actions/download-artifact@v4
|
||||
|
||||
- name: Upload to Release
|
||||
uses: softprops/action-gh-release@v2
|
||||
with:
|
||||
files: |
|
||||
windows-build/*
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
59
.github/workflows/linux-release.yml
vendored
59
.github/workflows/linux-release.yml
vendored
@@ -1,59 +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
|
||||
go build -ldflags "-s -w -X 'one-api/common.Version=$(git describe --tags)' -extldflags '-static'" -o new-api
|
||||
|
||||
- name: Build Backend (arm64)
|
||||
run: |
|
||||
sudo apt-get update
|
||||
DEBIAN_FRONTEND=noninteractive sudo apt-get install -y gcc-aarch64-linux-gnu
|
||||
CC=aarch64-linux-gnu-gcc CGO_ENABLED=1 GOOS=linux GOARCH=arm64 go build -ldflags "-s -w -X 'one-api/common.Version=$(git describe --tags)' -extldflags '-static'" -o new-api-arm64
|
||||
|
||||
- name: Release
|
||||
uses: softprops/action-gh-release@v1
|
||||
if: startsWith(github.ref, 'refs/tags/')
|
||||
with:
|
||||
files: |
|
||||
new-api
|
||||
new-api-arm64
|
||||
draft: true
|
||||
generate_release_notes: true
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
51
.github/workflows/macos-release.yml
vendored
51
.github/workflows/macos-release.yml
vendored
@@ -1,51 +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
|
||||
go build -ldflags "-X 'one-api/common.Version=$(git describe --tags)'" -o new-api-macos
|
||||
- name: Release
|
||||
uses: softprops/action-gh-release@v1
|
||||
if: startsWith(github.ref, 'refs/tags/')
|
||||
with:
|
||||
files: new-api-macos
|
||||
draft: true
|
||||
generate_release_notes: true
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
136
.github/workflows/release.yml
vendored
Normal file
136
.github/workflows/release.yml
vendored
Normal file
@@ -0,0 +1,136 @@
|
||||
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 'new-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 'new-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-*
|
||||
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 'new-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-*
|
||||
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 'new-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
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
|
||||
91
.github/workflows/sync-to-gitee.yml
vendored
Normal file
91
.github/workflows/sync-to-gitee.yml
vendored
Normal file
@@ -0,0 +1,91 @@
|
||||
name: Sync Release to Gitee
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
tag_name:
|
||||
description: 'Release Tag to sync (e.g. v1.0.0)'
|
||||
required: true
|
||||
type: string
|
||||
|
||||
# 配置你的 Gitee 仓库信息
|
||||
env:
|
||||
GITEE_OWNER: 'QuantumNous' # 修改为你的 Gitee 用户名
|
||||
GITEE_REPO: 'new-api' # 修改为你的 Gitee 仓库名
|
||||
|
||||
jobs:
|
||||
sync-to-gitee:
|
||||
runs-on: sync
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v3
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Get Release Info
|
||||
id: release_info
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
TAG_NAME: ${{ github.event.inputs.tag_name }}
|
||||
run: |
|
||||
# 获取 release 信息
|
||||
RELEASE_INFO=$(gh release view "$TAG_NAME" --json name,body,tagName,targetCommitish)
|
||||
|
||||
RELEASE_NAME=$(echo "$RELEASE_INFO" | jq -r '.name')
|
||||
TARGET_COMMITISH=$(echo "$RELEASE_INFO" | jq -r '.targetCommitish')
|
||||
|
||||
# 使用多行字符串输出
|
||||
{
|
||||
echo "release_name=$RELEASE_NAME"
|
||||
echo "target_commitish=$TARGET_COMMITISH"
|
||||
echo "release_body<<EOF"
|
||||
echo "$RELEASE_INFO" | jq -r '.body'
|
||||
echo "EOF"
|
||||
} >> $GITHUB_OUTPUT
|
||||
|
||||
# 下载 release 的所有附件
|
||||
gh release download "$TAG_NAME" --dir ./release_assets || echo "No assets to download"
|
||||
|
||||
# 列出下载的文件
|
||||
ls -la ./release_assets/ || echo "No assets directory"
|
||||
|
||||
- name: Create Gitee Release
|
||||
id: create_release
|
||||
uses: nICEnnnnnnnLee/action-gitee-release@v2.0.0
|
||||
with:
|
||||
gitee_action: create_release
|
||||
gitee_owner: ${{ env.GITEE_OWNER }}
|
||||
gitee_repo: ${{ env.GITEE_REPO }}
|
||||
gitee_token: ${{ secrets.GITEE_TOKEN }}
|
||||
gitee_tag_name: ${{ github.event.inputs.tag_name }}
|
||||
gitee_release_name: ${{ steps.release_info.outputs.release_name }}
|
||||
gitee_release_body: ${{ steps.release_info.outputs.release_body }}
|
||||
gitee_target_commitish: ${{ steps.release_info.outputs.target_commitish }}
|
||||
|
||||
- name: Upload Assets to Gitee
|
||||
if: hashFiles('release_assets/*') != ''
|
||||
uses: nICEnnnnnnnLee/action-gitee-release@v2.0.0
|
||||
with:
|
||||
gitee_action: upload_asset
|
||||
gitee_owner: ${{ env.GITEE_OWNER }}
|
||||
gitee_repo: ${{ env.GITEE_REPO }}
|
||||
gitee_token: ${{ secrets.GITEE_TOKEN }}
|
||||
gitee_release_id: ${{ steps.create_release.outputs.release-id }}
|
||||
gitee_upload_retry_times: 3
|
||||
gitee_files: |
|
||||
release_assets/*
|
||||
|
||||
- name: Cleanup
|
||||
if: always()
|
||||
run: |
|
||||
rm -rf release_assets/
|
||||
|
||||
- name: Summary
|
||||
if: success()
|
||||
run: |
|
||||
echo "✅ Successfully synced release ${{ github.event.inputs.tag_name }} to Gitee!"
|
||||
echo "🔗 Gitee Release URL: https://gitee.com/${{ env.GITEE_OWNER }}/${{ env.GITEE_REPO }}/releases/tag/${{ github.event.inputs.tag_name }}"
|
||||
|
||||
53
.github/workflows/windows-release.yml
vendored
53
.github/workflows/windows-release.yml
vendored
@@ -1,53 +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
|
||||
go build -ldflags "-s -w -X 'one-api/common.Version=$(git describe --tags)'" -o new-api.exe
|
||||
- name: Release
|
||||
uses: softprops/action-gh-release@v1
|
||||
if: startsWith(github.ref, 'refs/tags/')
|
||||
with:
|
||||
files: new-api.exe
|
||||
draft: true
|
||||
generate_release_notes: true
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
7
.gitignore
vendored
7
.gitignore
vendored
@@ -9,6 +9,11 @@ logs
|
||||
web/dist
|
||||
.env
|
||||
one-api
|
||||
new-api
|
||||
.DS_Store
|
||||
tiktoken_cache
|
||||
.eslintcache
|
||||
.eslintcache
|
||||
.gocache
|
||||
|
||||
electron/node_modules
|
||||
electron/dist
|
||||
14
Dockerfile
14
Dockerfile
@@ -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
|
||||
|
||||
@@ -21,7 +23,7 @@ RUN go mod download
|
||||
|
||||
COPY . .
|
||||
COPY --from=builder /build/dist ./web/dist
|
||||
RUN go build -ldflags "-s -w -X 'one-api/common.Version=$(cat VERSION)'" -o one-api
|
||||
RUN go build -ldflags "-s -w -X 'github.com/QuantumNous/new-api/common.Version=$(cat VERSION)'" -o new-api
|
||||
|
||||
FROM alpine
|
||||
|
||||
@@ -29,7 +31,7 @@ RUN apk upgrade --no-cache \
|
||||
&& apk add --no-cache ca-certificates tzdata ffmpeg \
|
||||
&& update-ca-certificates
|
||||
|
||||
COPY --from=builder2 /build/one-api /
|
||||
COPY --from=builder2 /build/new-api /
|
||||
EXPOSE 3000
|
||||
WORKDIR /data
|
||||
ENTRYPOINT ["/one-api"]
|
||||
ENTRYPOINT ["/new-api"]
|
||||
|
||||
49
README.en.md
49
README.en.md
@@ -1,6 +1,10 @@
|
||||
<p align="right">
|
||||
<a href="./README.md">中文</a> | <strong>English</strong> | <a href="./README.fr.md">Français</a>
|
||||
<a href="./README.md">中文</a> | <strong>English</strong> | <a href="./README.fr.md">Français</a> | <a href="./README.ja.md">日本語</a>
|
||||
</p>
|
||||
|
||||
> [!NOTE]
|
||||
> **MT (Machine Translation)**: This document is machine translated. For the most accurate information, please refer to the [Chinese version](./README.md).
|
||||
|
||||
<div align="center">
|
||||
|
||||

|
||||
@@ -75,7 +79,7 @@ New API offers a wide range of features, please refer to [Features Introduction]
|
||||
|
||||
1. 🎨 Brand new UI interface
|
||||
2. 🌍 Multi-language support
|
||||
3. 💰 Online recharge functionality (YiPay)
|
||||
3. 💰 Online recharge functionality, currently supports EPay and Stripe
|
||||
4. 🔍 Support for querying usage quotas with keys (works with [neko-api-key-tool](https://github.com/Calcium-Ion/neko-api-key-tool))
|
||||
5. 🔄 Compatible with the original One API database
|
||||
6. 💵 Support for pay-per-use model pricing
|
||||
@@ -85,18 +89,23 @@ New API offers a wide range of features, please refer to [Features Introduction]
|
||||
10. 🤖 Support for more authorization login methods (LinuxDO, Telegram, OIDC)
|
||||
11. 🔄 Support for Rerank models (Cohere and Jina), [API Documentation](https://docs.newapi.pro/api/jinaai-rerank)
|
||||
12. ⚡ Support for OpenAI Realtime API (including Azure channels), [API Documentation](https://docs.newapi.pro/api/openai-realtime)
|
||||
13. ⚡ Support for Claude Messages format, [API Documentation](https://docs.newapi.pro/api/anthropic-chat)
|
||||
14. Support for entering chat interface via /chat2link route
|
||||
15. 🧠 Support for setting reasoning effort through model name suffixes:
|
||||
13. ⚡ Support for **OpenAI Responses** format, [API Documentation](https://docs.newapi.pro/api/openai-responses)
|
||||
14. ⚡ Support for **Claude Messages** format, [API Documentation](https://docs.newapi.pro/api/anthropic-chat)
|
||||
15. ⚡ Support for **Google Gemini** format, [API Documentation](https://docs.newapi.pro/api/google-gemini-chat/)
|
||||
16. 🧠 Support for setting reasoning effort through model name suffixes:
|
||||
1. OpenAI o-series models
|
||||
- Add `-high` suffix for high reasoning effort (e.g.: `o3-mini-high`)
|
||||
- Add `-medium` suffix for medium reasoning effort (e.g.: `o3-mini-medium`)
|
||||
- Add `-low` suffix for low reasoning effort (e.g.: `o3-mini-low`)
|
||||
2. Claude thinking models
|
||||
- Add `-thinking` suffix to enable thinking mode (e.g.: `claude-3-7-sonnet-20250219-thinking`)
|
||||
16. 🔄 Thinking-to-content functionality
|
||||
17. 🔄 Model rate limiting for users
|
||||
18. 💰 Cache billing support, which allows billing at a set ratio when cache is hit:
|
||||
17. 🔄 Thinking-to-content functionality
|
||||
18. 🔄 Model rate limiting for users
|
||||
19. 🔄 Request format conversion functionality, supporting the following three format conversions:
|
||||
1. OpenAI Chat Completions => Claude Messages
|
||||
2. Claude Messages => OpenAI Chat Completions (can be used for Claude Code to call third-party models)
|
||||
3. OpenAI Chat Completions => Gemini Chat
|
||||
20. 💰 Cache billing support, which allows billing at a set ratio when cache is hit:
|
||||
1. Set the `Prompt Cache Ratio` option in `System Settings-Operation Settings`
|
||||
2. Set `Prompt Cache Ratio` in the channel, range 0-1, e.g., setting to 0.5 means billing at 50% when cache is hit
|
||||
3. Supported channels:
|
||||
@@ -115,7 +124,9 @@ This version supports multiple models, please refer to [API Documentation-Relay
|
||||
4. Custom channels, supporting full call address input
|
||||
5. Rerank models ([Cohere](https://cohere.ai/) and [Jina](https://jina.ai/)), [API Documentation](https://docs.newapi.pro/api/jinaai-rerank)
|
||||
6. Claude Messages format, [API Documentation](https://docs.newapi.pro/api/anthropic-chat)
|
||||
7. Dify, currently only supports chatflow
|
||||
7. Google Gemini format, [API Documentation](https://docs.newapi.pro/api/google-gemini-chat/)
|
||||
8. Dify, currently only supports chatflow
|
||||
9. For more interfaces, please refer to [API Documentation](https://docs.newapi.pro/api)
|
||||
|
||||
## Environment Variable Configuration
|
||||
|
||||
@@ -124,14 +135,12 @@ For detailed configuration instructions, please refer to [Installation Guide-Env
|
||||
- `GENERATE_DEFAULT_TOKEN`: Whether to generate initial tokens for newly registered users, default is `false`
|
||||
- `STREAMING_TIMEOUT`: Streaming response timeout, default is 300 seconds
|
||||
- `DIFY_DEBUG`: Whether to output workflow and node information for Dify channels, default is `true`
|
||||
- `FORCE_STREAM_OPTION`: Whether to override client stream_options parameter, default is `true`
|
||||
- `GET_MEDIA_TOKEN`: Whether to count image tokens, default is `true`
|
||||
- `GET_MEDIA_TOKEN_NOT_STREAM`: Whether to count image tokens in non-streaming cases, default is `true`
|
||||
- `UPDATE_TASK`: Whether to update asynchronous tasks (Midjourney, Suno), default is `true`
|
||||
- `COHERE_SAFETY_SETTING`: Cohere model safety settings, options are `NONE`, `CONTEXTUAL`, `STRICT`, default is `NONE`
|
||||
- `GEMINI_VISION_MAX_IMAGE_NUM`: Maximum number of images for Gemini models, default is `16`
|
||||
- `MAX_FILE_DOWNLOAD_MB`: Maximum file download size in MB, default is `20`
|
||||
- `CRYPTO_SECRET`: Encryption key used for encrypting database content
|
||||
- `CRYPTO_SECRET`: Encryption key used for encrypting Redis database content
|
||||
- `AZURE_DEFAULT_API_VERSION`: Azure channel default API version, default is `2025-04-01-preview`
|
||||
- `NOTIFICATION_LIMIT_DURATION_MINUTE`: Notification limit duration, default is `10` minutes
|
||||
- `NOTIFY_LIMIT_COUNT`: Maximum number of user notifications within the specified duration, default is `2`
|
||||
@@ -178,7 +187,7 @@ docker run --name new-api -d --restart always -p 3000:3000 -e SQL_DSN="root:1234
|
||||
```
|
||||
|
||||
## Channel Retry and Cache
|
||||
Channel retry functionality has been implemented, you can set the number of retries in `Settings->Operation Settings->General Settings`. It is **recommended to enable caching**.
|
||||
Channel retry functionality has been implemented, you can set the number of retries in `Settings->Operation Settings->General Settings->Failure Retry Count`, **recommended to enable caching** functionality.
|
||||
|
||||
### Cache Configuration Method
|
||||
1. `REDIS_CONN_STRING`: Set Redis as cache
|
||||
@@ -188,21 +197,21 @@ Channel retry functionality has been implemented, you can set the number of retr
|
||||
|
||||
For detailed API documentation, please refer to [API Documentation](https://docs.newapi.pro/api):
|
||||
|
||||
- [Chat API](https://docs.newapi.pro/api/openai-chat)
|
||||
- [Image API](https://docs.newapi.pro/api/openai-image)
|
||||
- [Rerank API](https://docs.newapi.pro/api/jinaai-rerank)
|
||||
- [Realtime API](https://docs.newapi.pro/api/openai-realtime)
|
||||
- [Claude Chat API (messages)](https://docs.newapi.pro/api/anthropic-chat)
|
||||
- [Chat API (Chat Completions)](https://docs.newapi.pro/api/openai-chat)
|
||||
- [Response API (Responses)](https://docs.newapi.pro/api/openai-responses)
|
||||
- [Image API (Image)](https://docs.newapi.pro/api/openai-image)
|
||||
- [Rerank API (Rerank)](https://docs.newapi.pro/api/jinaai-rerank)
|
||||
- [Realtime Chat API (Realtime)](https://docs.newapi.pro/api/openai-realtime)
|
||||
- [Claude Chat API](https://docs.newapi.pro/api/anthropic-chat)
|
||||
- [Google Gemini Chat API](https://docs.newapi.pro/api/google-gemini-chat)
|
||||
|
||||
## Related Projects
|
||||
- [One API](https://github.com/songquanpeng/one-api): Original project
|
||||
- [Midjourney-Proxy](https://github.com/novicezk/midjourney-proxy): Midjourney interface support
|
||||
- [chatnio](https://github.com/Deeptrain-Community/chatnio): Next-generation AI one-stop B/C-end solution
|
||||
- [neko-api-key-tool](https://github.com/Calcium-Ion/neko-api-key-tool): Query usage quota with key
|
||||
|
||||
Other projects based on New API:
|
||||
- [new-api-horizon](https://github.com/Calcium-Ion/new-api-horizon): High-performance optimized version of New API
|
||||
- [VoAPI](https://github.com/VoAPI/VoAPI): Frontend beautified version based on New API
|
||||
|
||||
## Help and Support
|
||||
|
||||
|
||||
49
README.fr.md
49
README.fr.md
@@ -1,6 +1,10 @@
|
||||
<p align="right">
|
||||
<a href="./README.md">中文</a> | <a href="./README.en.md">English</a> | <strong>Français</strong>
|
||||
<a href="./README.md">中文</a> | <a href="./README.en.md">English</a> | <strong>Français</strong> | <a href="./README.ja.md">日本語</a>
|
||||
</p>
|
||||
|
||||
> [!NOTE]
|
||||
> **MT (Traduction Automatique)**: Ce document est traduit automatiquement. Pour les informations les plus précises, veuillez vous référer à la [version chinoise](./README.md).
|
||||
|
||||
<div align="center">
|
||||
|
||||

|
||||
@@ -75,7 +79,7 @@ New API offre un large éventail de fonctionnalités, veuillez vous référer à
|
||||
|
||||
1. 🎨 Nouvelle interface utilisateur
|
||||
2. 🌍 Prise en charge multilingue
|
||||
3. 💰 Fonctionnalité de recharge en ligne (YiPay)
|
||||
3. 💰 Fonctionnalité de recharge en ligne, prend actuellement en charge EPay et Stripe
|
||||
4. 🔍 Prise en charge de la recherche de quotas d'utilisation avec des clés (fonctionne avec [neko-api-key-tool](https://github.com/Calcium-Ion/neko-api-key-tool))
|
||||
5. 🔄 Compatible avec la base de données originale de One API
|
||||
6. 💵 Prise en charge de la tarification des modèles de paiement à l'utilisation
|
||||
@@ -85,18 +89,23 @@ New API offre un large éventail de fonctionnalités, veuillez vous référer à
|
||||
10. 🤖 Prise en charge de plus de méthodes de connexion par autorisation (LinuxDO, Telegram, OIDC)
|
||||
11. 🔄 Prise en charge des modèles Rerank (Cohere et Jina), [Documentation de l'API](https://docs.newapi.pro/api/jinaai-rerank)
|
||||
12. ⚡ Prise en charge de l'API OpenAI Realtime (y compris les canaux Azure), [Documentation de l'API](https://docs.newapi.pro/api/openai-realtime)
|
||||
13. ⚡ Prise en charge du format Claude Messages, [Documentation de l'API](https://docs.newapi.pro/api/anthropic-chat)
|
||||
14. Prise en charge de l'accès à l'interface de discussion via la route /chat2link
|
||||
15. 🧠 Prise en charge de la définition de l'effort de raisonnement via les suffixes de nom de modèle :
|
||||
13. ⚡ Prise en charge du format **OpenAI Responses**, [Documentation de l'API](https://docs.newapi.pro/api/openai-responses)
|
||||
14. ⚡ Prise en charge du format **Claude Messages**, [Documentation de l'API](https://docs.newapi.pro/api/anthropic-chat)
|
||||
15. ⚡ Prise en charge du format **Google Gemini**, [Documentation de l'API](https://docs.newapi.pro/api/google-gemini-chat/)
|
||||
16. 🧠 Prise en charge de la définition de l'effort de raisonnement via les suffixes de nom de modèle :
|
||||
1. Modèles de la série o d'OpenAI
|
||||
- Ajouter le suffixe `-high` pour un effort de raisonnement élevé (par exemple : `o3-mini-high`)
|
||||
- Ajouter le suffixe `-medium` pour un effort de raisonnement moyen (par exemple : `o3-mini-medium`)
|
||||
- Ajouter le suffixe `-low` pour un effort de raisonnement faible (par exemple : `o3-mini-low`)
|
||||
2. Modèles de pensée de Claude
|
||||
- Ajouter le suffixe `-thinking` pour activer le mode de pensée (par exemple : `claude-3-7-sonnet-20250219-thinking`)
|
||||
16. 🔄 Fonctionnalité de la pensée au contenu
|
||||
17. 🔄 Limitation du débit du modèle pour les utilisateurs
|
||||
18. 💰 Prise en charge de la facturation du cache, qui permet de facturer à un ratio défini lorsque le cache est atteint :
|
||||
17. 🔄 Fonctionnalité de la pensée au contenu
|
||||
18. 🔄 Limitation du débit du modèle pour les utilisateurs
|
||||
19. 🔄 Fonctionnalité de conversion de format de requête, prenant en charge les trois conversions de format suivantes :
|
||||
1. OpenAI Chat Completions => Claude Messages
|
||||
2. Claude Messages => OpenAI Chat Completions (peut être utilisé pour Claude Code pour appeler des modèles tiers)
|
||||
3. OpenAI Chat Completions => Gemini Chat
|
||||
20. 💰 Prise en charge de la facturation du cache, qui permet de facturer à un ratio défini lorsque le cache est atteint :
|
||||
1. Définir l'option `Ratio de cache d'invite` dans `Paramètres système->Paramètres de fonctionnement`
|
||||
2. Définir le `Ratio de cache d'invite` dans le canal, plage de 0 à 1, par exemple, le définir sur 0,5 signifie facturer à 50 % lorsque le cache est atteint
|
||||
3. Canaux pris en charge :
|
||||
@@ -115,7 +124,9 @@ Cette version prend en charge plusieurs modèles, veuillez vous référer à [Do
|
||||
4. Canaux personnalisés, prenant en charge la saisie complète de l'adresse d'appel
|
||||
5. Modèles Rerank ([Cohere](https://cohere.ai/) et [Jina](https://jina.ai/)), [Documentation de l'API](https://docs.newapi.pro/api/jinaai-rerank)
|
||||
6. Format de messages Claude, [Documentation de l'API](https://docs.newapi.pro/api/anthropic-chat)
|
||||
7. Dify, ne prend actuellement en charge que chatflow
|
||||
7. Format Google Gemini, [Documentation de l'API](https://docs.newapi.pro/api/google-gemini-chat/)
|
||||
8. Dify, ne prend actuellement en charge que chatflow
|
||||
9. Pour plus d'interfaces, veuillez vous référer à la [Documentation de l'API](https://docs.newapi.pro/api)
|
||||
|
||||
## Configuration des variables d'environnement
|
||||
|
||||
@@ -124,14 +135,12 @@ Pour des instructions de configuration détaillées, veuillez vous référer à
|
||||
- `GENERATE_DEFAULT_TOKEN` : S'il faut générer des jetons initiaux pour les utilisateurs nouvellement enregistrés, la valeur par défaut est `false`
|
||||
- `STREAMING_TIMEOUT` : Délai d'expiration de la réponse en streaming, la valeur par défaut est de 300 secondes
|
||||
- `DIFY_DEBUG` : S'il faut afficher les informations sur le flux de travail et les nœuds pour les canaux Dify, la valeur par défaut est `true`
|
||||
- `FORCE_STREAM_OPTION` : S'il faut remplacer le paramètre client stream_options, la valeur par défaut est `true`
|
||||
- `GET_MEDIA_TOKEN` : S'il faut compter les jetons d'image, la valeur par défaut est `true`
|
||||
- `GET_MEDIA_TOKEN_NOT_STREAM` : S'il faut compter les jetons d'image dans les cas sans streaming, la valeur par défaut est `true`
|
||||
- `UPDATE_TASK` : S'il faut mettre à jour les tâches asynchrones (Midjourney, Suno), la valeur par défaut est `true`
|
||||
- `COHERE_SAFETY_SETTING` : Paramètres de sécurité du modèle Cohere, les options sont `NONE`, `CONTEXTUAL`, `STRICT`, la valeur par défaut est `NONE`
|
||||
- `GEMINI_VISION_MAX_IMAGE_NUM` : Nombre maximum d'images pour les modèles Gemini, la valeur par défaut est `16`
|
||||
- `MAX_FILE_DOWNLOAD_MB` : Taille maximale de téléchargement de fichier en Mo, la valeur par défaut est `20`
|
||||
- `CRYPTO_SECRET` : Clé de chiffrement utilisée pour chiffrer le contenu de la base de données
|
||||
- `CRYPTO_SECRET` : Clé de chiffrement utilisée pour chiffrer le contenu de la base de données Redis
|
||||
- `AZURE_DEFAULT_API_VERSION` : Version de l'API par défaut du canal Azure, la valeur par défaut est `2025-04-01-preview`
|
||||
- `NOTIFICATION_LIMIT_DURATION_MINUTE` : Durée de la limite de notification, la valeur par défaut est de `10` minutes
|
||||
- `NOTIFY_LIMIT_COUNT` : Nombre maximal de notifications utilisateur dans la durée spécifiée, la valeur par défaut est `2`
|
||||
@@ -178,7 +187,7 @@ docker run --name new-api -d --restart always -p 3000:3000 -e SQL_DSN="root:1234
|
||||
```
|
||||
|
||||
## Nouvelle tentative de canal et cache
|
||||
La fonctionnalité de nouvelle tentative de canal a été implémentée, vous pouvez définir le nombre de tentatives dans `Paramètres->Paramètres de fonctionnement->Paramètres généraux`. Il est **recommandé d'activer la mise en cache**.
|
||||
La fonctionnalité de nouvelle tentative de canal a été implémentée, vous pouvez définir le nombre de tentatives dans `Paramètres->Paramètres de fonctionnement->Paramètres généraux->Nombre de tentatives en cas d'échec`, **recommandé d'activer la fonctionnalité de mise en cache**.
|
||||
|
||||
### Méthode de configuration du cache
|
||||
1. `REDIS_CONN_STRING` : Définir Redis comme cache
|
||||
@@ -188,21 +197,21 @@ La fonctionnalité de nouvelle tentative de canal a été implémentée, vous po
|
||||
|
||||
Pour une documentation détaillée de l'API, veuillez vous référer à [Documentation de l'API](https://docs.newapi.pro/api) :
|
||||
|
||||
- [API de discussion](https://docs.newapi.pro/api/openai-chat)
|
||||
- [API d'image](https://docs.newapi.pro/api/openai-image)
|
||||
- [API de rerank](https://docs.newapi.pro/api/jinaai-rerank)
|
||||
- [API en temps réel](https://docs.newapi.pro/api/openai-realtime)
|
||||
- [API de discussion Claude (messages)](https://docs.newapi.pro/api/anthropic-chat)
|
||||
- [API de discussion (Chat Completions)](https://docs.newapi.pro/api/openai-chat)
|
||||
- [API de réponse (Responses)](https://docs.newapi.pro/api/openai-responses)
|
||||
- [API d'image (Image)](https://docs.newapi.pro/api/openai-image)
|
||||
- [API de rerank (Rerank)](https://docs.newapi.pro/api/jinaai-rerank)
|
||||
- [API de discussion en temps réel (Realtime)](https://docs.newapi.pro/api/openai-realtime)
|
||||
- [API de discussion Claude](https://docs.newapi.pro/api/anthropic-chat)
|
||||
- [API de discussion Google Gemini](https://docs.newapi.pro/api/google-gemini-chat)
|
||||
|
||||
## Projets connexes
|
||||
- [One API](https://github.com/songquanpeng/one-api) : Projet original
|
||||
- [Midjourney-Proxy](https://github.com/novicezk/midjourney-proxy) : Prise en charge de l'interface Midjourney
|
||||
- [chatnio](https://github.com/Deeptrain-Community/chatnio) : Solution B/C unique d'IA de nouvelle génération
|
||||
- [neko-api-key-tool](https://github.com/Calcium-Ion/neko-api-key-tool) : Interroger le quota d'utilisation avec une clé
|
||||
|
||||
Autres projets basés sur New API :
|
||||
- [new-api-horizon](https://github.com/Calcium-Ion/new-api-horizon) : Version optimisée hautes performances de New API
|
||||
- [VoAPI](https://github.com/VoAPI/VoAPI) : Version embellie du frontend basée sur New API
|
||||
|
||||
## Aide et support
|
||||
|
||||
|
||||
226
README.ja.md
Normal file
226
README.ja.md
Normal file
@@ -0,0 +1,226 @@
|
||||
<p align="right">
|
||||
<a href="./README.md">中文</a> | <a href="./README.en.md">English</a> | <a href="./README.fr.md">Français</a> | <strong>日本語</strong>
|
||||
</p>
|
||||
|
||||
> [!NOTE]
|
||||
> **MT(機械翻訳)**: この文書は機械翻訳されています。最も正確な情報については、[中国語版](./README.md)を参照してください。
|
||||
|
||||
<div align="center">
|
||||
|
||||

|
||||
|
||||
# New API
|
||||
|
||||
🍥次世代大規模モデルゲートウェイとAI資産管理システム
|
||||
|
||||
<a href="https://trendshift.io/repositories/8227" target="_blank"><img src="https://trendshift.io/api/badge/repositories/8227" alt="Calcium-Ion%2Fnew-api | Trendshift" style="width: 250px; height: 55px;" width="250" height="55"/></a>
|
||||
|
||||
<p align="center">
|
||||
<a href="https://raw.githubusercontent.com/Calcium-Ion/new-api/main/LICENSE">
|
||||
<img src="https://img.shields.io/github/license/Calcium-Ion/new-api?color=brightgreen" alt="license">
|
||||
</a>
|
||||
<a href="https://github.com/Calcium-Ion/new-api/releases/latest">
|
||||
<img src="https://img.shields.io/github/v/release/Calcium-Ion/new-api?color=brightgreen&include_prereleases" alt="release">
|
||||
</a>
|
||||
<a href="https://github.com/users/Calcium-Ion/packages/container/package/new-api">
|
||||
<img src="https://img.shields.io/badge/docker-ghcr.io-blue" alt="docker">
|
||||
</a>
|
||||
<a href="https://hub.docker.com/r/CalciumIon/new-api">
|
||||
<img src="https://img.shields.io/badge/docker-dockerHub-blue" alt="docker">
|
||||
</a>
|
||||
<a href="https://goreportcard.com/report/github.com/Calcium-Ion/new-api">
|
||||
<img src="https://goreportcard.com/badge/github.com/Calcium-Ion/new-api" alt="GoReportCard">
|
||||
</a>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
## 📝 プロジェクト説明
|
||||
|
||||
> [!NOTE]
|
||||
> 本プロジェクトは、[One API](https://github.com/songquanpeng/one-api)をベースに二次開発されたオープンソースプロジェクトです
|
||||
|
||||
> [!IMPORTANT]
|
||||
> - 本プロジェクトは個人学習用のみであり、安定性の保証や技術サポートは提供しません。
|
||||
> - ユーザーは、OpenAIの[利用規約](https://openai.com/policies/terms-of-use)および**法律法規**を遵守する必要があり、違法な目的で使用してはいけません。
|
||||
> - [《生成式人工智能服务管理暂行办法》](http://www.cac.gov.cn/2023-07/13/c_1690898327029107.htm)の要求に従い、中国地域の公衆に未登録の生成式AI サービスを提供しないでください。
|
||||
|
||||
<h2>🤝 信頼できるパートナー</h2>
|
||||
<p id="premium-sponsors"> </p>
|
||||
<p align="center"><strong>順不同</strong></p>
|
||||
<p align="center">
|
||||
<a href="https://www.cherry-ai.com/" target=_blank><img
|
||||
src="./docs/images/cherry-studio.png" alt="Cherry Studio" height="120"
|
||||
/></a>
|
||||
<a href="https://bda.pku.edu.cn/" target=_blank><img
|
||||
src="./docs/images/pku.png" alt="北京大学" height="120"
|
||||
/></a>
|
||||
<a href="https://www.compshare.cn/?ytag=GPU_yy_gh_newapi" target=_blank><img
|
||||
src="./docs/images/ucloud.png" alt="UCloud 優刻得" height="120"
|
||||
/></a>
|
||||
<a href="https://www.aliyun.com/" target=_blank><img
|
||||
src="./docs/images/aliyun.png" alt="Alibaba Cloud" height="120"
|
||||
/></a>
|
||||
<a href="https://io.net/" target=_blank><img
|
||||
src="./docs/images/io-net.png" alt="IO.NET" height="120"
|
||||
/></a>
|
||||
</p>
|
||||
<p> </p>
|
||||
|
||||
## 📚 ドキュメント
|
||||
|
||||
詳細なドキュメントは公式Wikiをご覧ください:[https://docs.newapi.pro/](https://docs.newapi.pro/)
|
||||
|
||||
AIが生成したDeepWikiにもアクセスできます:
|
||||
[](https://deepwiki.com/QuantumNous/new-api)
|
||||
|
||||
## ✨ 主な機能
|
||||
|
||||
New APIは豊富な機能を提供しています。詳細な機能については[機能説明](https://docs.newapi.pro/wiki/features-introduction)を参照してください:
|
||||
|
||||
1. 🎨 全く新しいUIインターフェース
|
||||
2. 🌍 多言語サポート
|
||||
3. 💰 オンラインチャージ機能をサポート、現在EPayとStripeをサポート
|
||||
4. 🔍 キーによる使用量クォータの照会をサポート([neko-api-key-tool](https://github.com/Calcium-Ion/neko-api-key-tool)と連携)
|
||||
5. 🔄 オリジナルのOne APIデータベースと互換性あり
|
||||
6. 💵 モデルの従量課金をサポート
|
||||
7. ⚖️ チャネルの重み付けランダムをサポート
|
||||
8. 📈 データダッシュボード(コンソール)
|
||||
9. 🔒 トークングループ化、モデル制限
|
||||
10. 🤖 より多くの認証ログイン方法をサポート(LinuxDO、Telegram、OIDC)
|
||||
11. 🔄 Rerankモデルをサポート(CohereとJina)、[API ドキュメント](https://docs.newapi.pro/api/jinaai-rerank)
|
||||
12. ⚡ OpenAI Realtime APIをサポート(Azureチャネルを含む)、[APIドキュメント](https://docs.newapi.pro/api/openai-realtime)
|
||||
13. ⚡ **OpenAI Responses**形式をサポート、[APIドキュメント](https://docs.newapi.pro/api/openai-responses)
|
||||
14. ⚡ **Claude Messages**形式をサポート、[APIドキュメント](https://docs.newapi.pro/api/anthropic-chat)
|
||||
15. ⚡ **Google Gemini**形式をサポート、[APIドキュメント](https://docs.newapi.pro/api/google-gemini-chat/)
|
||||
16. 🧠 モデル名のサフィックスを通じてreasoning effortを設定することをサポート:
|
||||
1. OpenAI oシリーズモデル
|
||||
- `-high`サフィックスを追加してhigh reasoning effortに設定(例:`o3-mini-high`)
|
||||
- `-medium`サフィックスを追加してmedium reasoning effortに設定(例:`o3-mini-medium`)
|
||||
- `-low`サフィックスを追加してlow reasoning effortに設定(例:`o3-mini-low`)
|
||||
2. Claude思考モデル
|
||||
- `-thinking`サフィックスを追加して思考モードを有効にする(例:`claude-3-7-sonnet-20250219-thinking`)
|
||||
17. 🔄 思考からコンテンツへの機能
|
||||
18. 🔄 ユーザーに対するモデルレート制限機能
|
||||
19. 🔄 リクエストフォーマット変換機能、以下の3つのフォーマット変換をサポート:
|
||||
1. OpenAI Chat Completions => Claude Messages
|
||||
2. Claude Messages => OpenAI Chat Completions(Claude Codeがサードパーティモデルを呼び出す際に使用可能)
|
||||
3. OpenAI Chat Completions => Gemini Chat
|
||||
20. 💰 キャッシュ課金サポート、有効にするとキャッシュがヒットした際に設定された比率で課金できます:
|
||||
1. `システム設定-運営設定`で`プロンプトキャッシュ倍率`オプションを設定
|
||||
2. チャネルで`プロンプトキャッシュ倍率`を設定、範囲は0-1、例えば0.5に設定するとキャッシュがヒットした際に50%で課金
|
||||
3. サポートされているチャネル:
|
||||
- [x] OpenAI
|
||||
- [x] Azure
|
||||
- [x] DeepSeek
|
||||
- [x] Claude
|
||||
|
||||
## モデルサポート
|
||||
|
||||
このバージョンは複数のモデルをサポートしています。詳細は[APIドキュメント-中継インターフェース](https://docs.newapi.pro/api)を参照してください:
|
||||
|
||||
1. サードパーティモデル **gpts**(gpt-4-gizmo-*)
|
||||
2. サードパーティチャネル[Midjourney-Proxy(Plus)](https://github.com/novicezk/midjourney-proxy)インターフェース、[APIドキュメント](https://docs.newapi.pro/api/midjourney-proxy-image)
|
||||
3. サードパーティチャネル[Suno API](https://github.com/Suno-API/Suno-API)インターフェース、[APIドキュメント](https://docs.newapi.pro/api/suno-music)
|
||||
4. カスタムチャネル、完全な呼び出しアドレスの入力をサポート
|
||||
5. Rerankモデル([Cohere](https://cohere.ai/)と[Jina](https://jina.ai/))、[APIドキュメント](https://docs.newapi.pro/api/jinaai-rerank)
|
||||
6. Claude Messages形式、[APIドキュメント](https://docs.newapi.pro/api/anthropic-chat)
|
||||
7. Google Gemini形式、[APIドキュメント](https://docs.newapi.pro/api/google-gemini-chat/)
|
||||
8. Dify、現在はchatflowのみをサポート
|
||||
9. その他のインターフェースについては[APIドキュメント](https://docs.newapi.pro/api)を参照してください
|
||||
|
||||
## 環境変数設定
|
||||
|
||||
詳細な設定説明については[インストールガイド-環境変数設定](https://docs.newapi.pro/installation/environment-variables)を参照してください:
|
||||
|
||||
- `GENERATE_DEFAULT_TOKEN`:新規登録ユーザーに初期トークンを生成するかどうか、デフォルトは`false`
|
||||
- `STREAMING_TIMEOUT`:ストリーミング応答のタイムアウト時間、デフォルトは300秒
|
||||
- `DIFY_DEBUG`:Difyチャネルがワークフローとノード情報を出力するかどうか、デフォルトは`true`
|
||||
- `GET_MEDIA_TOKEN`:画像トークンを統計するかどうか、デフォルトは`true`
|
||||
- `GET_MEDIA_TOKEN_NOT_STREAM`:非ストリーミングの場合に画像トークンを統計するかどうか、デフォルトは`true`
|
||||
- `UPDATE_TASK`:非同期タスク(Midjourney、Suno)を更新するかどうか、デフォルトは`true`
|
||||
- `GEMINI_VISION_MAX_IMAGE_NUM`:Geminiモデルの最大画像数、デフォルトは`16`
|
||||
- `MAX_FILE_DOWNLOAD_MB`: 最大ファイルダウンロードサイズ、単位MB、デフォルトは`20`
|
||||
- `CRYPTO_SECRET`:暗号化キー、Redisデータベースの内容を暗号化するために使用
|
||||
- `AZURE_DEFAULT_API_VERSION`:Azureチャネルのデフォルトのバージョン、デフォルトは`2025-04-01-preview`
|
||||
- `NOTIFICATION_LIMIT_DURATION_MINUTE`:メールなどの通知制限の継続時間、デフォルトは`10`分
|
||||
- `NOTIFY_LIMIT_COUNT`:指定された継続時間内のユーザー通知の最大数、デフォルトは`2`
|
||||
- `ERROR_LOG_ENABLED=true`: エラーログを記録して表示するかどうか、デフォルトは`false`
|
||||
|
||||
## デプロイ
|
||||
|
||||
詳細なデプロイガイドについては[インストールガイド-デプロイ方法](https://docs.newapi.pro/installation)を参照してください:
|
||||
|
||||
> [!TIP]
|
||||
> 最新のDockerイメージ:`calciumion/new-api:latest`
|
||||
|
||||
### マルチマシンデプロイの注意事項
|
||||
- 環境変数`SESSION_SECRET`を設定する必要があります。そうしないとマルチマシンデプロイ時にログイン状態が不一致になります
|
||||
- Redisを共有する場合、`CRYPTO_SECRET`を設定する必要があります。そうしないとマルチマシンデプロイ時にRedisの内容を取得できません
|
||||
|
||||
### デプロイ要件
|
||||
- ローカルデータベース(デフォルト):SQLite(Dockerデプロイの場合は`/data`ディレクトリをマウントする必要があります)
|
||||
- リモートデータベース:MySQLバージョン >= 5.7.8、PgSQLバージョン >= 9.6
|
||||
|
||||
### デプロイ方法
|
||||
|
||||
#### 宝塔パネルのDocker機能を使用してデプロイ
|
||||
宝塔パネル(**9.2.0バージョン**以上)をインストールし、アプリケーションストアで**New-API**を見つけてインストールします。
|
||||
[画像付きチュートリアル](./docs/BT.md)
|
||||
|
||||
#### Docker Composeを使用してデプロイ(推奨)
|
||||
```shell
|
||||
# プロジェクトをダウンロード
|
||||
git clone https://github.com/Calcium-Ion/new-api.git
|
||||
cd new-api
|
||||
# 必要に応じてdocker-compose.ymlを編集
|
||||
# 起動
|
||||
docker-compose up -d
|
||||
```
|
||||
|
||||
#### Dockerイメージを直接使用
|
||||
```shell
|
||||
# SQLiteを使用
|
||||
docker run --name new-api -d --restart always -p 3000:3000 -e TZ=Asia/Shanghai -v /home/ubuntu/data/new-api:/data calciumion/new-api:latest
|
||||
|
||||
# MySQLを使用
|
||||
docker run --name new-api -d --restart always -p 3000:3000 -e SQL_DSN="root:123456@tcp(localhost:3306)/oneapi" -e TZ=Asia/Shanghai -v /home/ubuntu/data/new-api:/data calciumion/new-api:latest
|
||||
```
|
||||
|
||||
## チャネルリトライとキャッシュ
|
||||
チャネルリトライ機能はすでに実装されており、`設定->運営設定->一般設定->失敗リトライ回数`でリトライ回数を設定できます。**キャッシュ機能を有効にすることを推奨します**。
|
||||
|
||||
### キャッシュ設定方法
|
||||
1. `REDIS_CONN_STRING`:Redisをキャッシュとして設定
|
||||
2. `MEMORY_CACHE_ENABLED`:メモリキャッシュを有効にする(Redisを設定した場合は手動設定不要)
|
||||
|
||||
## APIドキュメント
|
||||
|
||||
詳細なAPIドキュメントについては[APIドキュメント](https://docs.newapi.pro/api)を参照してください:
|
||||
|
||||
- [チャットインターフェース(Chat Completions)](https://docs.newapi.pro/api/openai-chat)
|
||||
- [レスポンスインターフェース(Responses)](https://docs.newapi.pro/api/openai-responses)
|
||||
- [画像インターフェース(Image)](https://docs.newapi.pro/api/openai-image)
|
||||
- [再ランク付けインターフェース(Rerank)](https://docs.newapi.pro/api/jinaai-rerank)
|
||||
- [リアルタイム対話インターフェース(Realtime)](https://docs.newapi.pro/api/openai-realtime)
|
||||
- [Claudeチャットインターフェース](https://docs.newapi.pro/api/anthropic-chat)
|
||||
- [Google Geminiチャットインターフェース](https://docs.newapi.pro/api/google-gemini-chat)
|
||||
|
||||
## 関連プロジェクト
|
||||
- [One API](https://github.com/songquanpeng/one-api):オリジナルプロジェクト
|
||||
- [Midjourney-Proxy](https://github.com/novicezk/midjourney-proxy):Midjourneyインターフェースサポート
|
||||
- [neko-api-key-tool](https://github.com/Calcium-Ion/neko-api-key-tool):キーを使用して使用量クォータを照会
|
||||
|
||||
New APIベースのその他のプロジェクト:
|
||||
- [new-api-horizon](https://github.com/Calcium-Ion/new-api-horizon):New API高性能最適化版
|
||||
|
||||
## ヘルプサポート
|
||||
|
||||
問題がある場合は、[ヘルプサポート](https://docs.newapi.pro/support)を参照してください:
|
||||
- [コミュニティ交流](https://docs.newapi.pro/support/community-interaction)
|
||||
- [問題のフィードバック](https://docs.newapi.pro/support/feedback-issues)
|
||||
- [よくある質問](https://docs.newapi.pro/support/faq)
|
||||
|
||||
## 🌟 Star History
|
||||
|
||||
[](https://star-history.com/#Calcium-Ion/new-api&Date)
|
||||
|
||||
42
README.md
42
README.md
@@ -1,5 +1,5 @@
|
||||
<p align="right">
|
||||
<strong>中文</strong> | <a href="./README.en.md">English</a> | <a href="./README.fr.md">Français</a>
|
||||
<strong>中文</strong> | <a href="./README.en.md">English</a> | <a href="./README.fr.md">Français</a> | <a href="./README.ja.md">日本語</a>
|
||||
</p>
|
||||
<div align="center">
|
||||
|
||||
@@ -75,7 +75,7 @@ New API提供了丰富的功能,详细特性请参考[特性说明](https://do
|
||||
|
||||
1. 🎨 全新的UI界面
|
||||
2. 🌍 多语言支持
|
||||
3. 💰 支持在线充值功能(易支付)
|
||||
3. 💰 支持在线充值功能,当前支持易支付和Stripe
|
||||
4. 🔍 支持用key查询使用额度(配合[neko-api-key-tool](https://github.com/Calcium-Ion/neko-api-key-tool))
|
||||
5. 🔄 兼容原版One API的数据库
|
||||
6. 💵 支持模型按次数收费
|
||||
@@ -85,22 +85,23 @@ New API提供了丰富的功能,详细特性请参考[特性说明](https://do
|
||||
10. 🤖 支持更多授权登陆方式(LinuxDO,Telegram、OIDC)
|
||||
11. 🔄 支持Rerank模型(Cohere和Jina),[接口文档](https://docs.newapi.pro/api/jinaai-rerank)
|
||||
12. ⚡ 支持OpenAI Realtime API(包括Azure渠道),[接口文档](https://docs.newapi.pro/api/openai-realtime)
|
||||
13. ⚡ 支持Claude Messages 格式,[接口文档](https://docs.newapi.pro/api/anthropic-chat)
|
||||
14. 支持使用路由/chat2link进入聊天界面
|
||||
15. 🧠 支持通过模型名称后缀设置 reasoning effort:
|
||||
13. ⚡ 支持 **OpenAI Responses** 格式,[接口文档](https://docs.newapi.pro/api/openai-responses)
|
||||
14. ⚡ 支持 **Claude Messages** 格式,[接口文档](https://docs.newapi.pro/api/anthropic-chat)
|
||||
15. ⚡ 支持 **Google Gemini** 格式,[接口文档](https://docs.newapi.pro/api/google-gemini-chat/)
|
||||
16. 🧠 支持通过模型名称后缀设置 reasoning effort:
|
||||
1. OpenAI o系列模型
|
||||
- 添加后缀 `-high` 设置为 high reasoning effort (例如: `o3-mini-high`)
|
||||
- 添加后缀 `-medium` 设置为 medium reasoning effort (例如: `o3-mini-medium`)
|
||||
- 添加后缀 `-low` 设置为 low reasoning effort (例如: `o3-mini-low`)
|
||||
2. Claude 思考模型
|
||||
- 添加后缀 `-thinking` 启用思考模式 (例如: `claude-3-7-sonnet-20250219-thinking`)
|
||||
16. 🔄 思考转内容功能
|
||||
17. 🔄 针对用户的模型限流功能
|
||||
18. 🔄 请求格式转换功能,支持以下三种格式转换:
|
||||
1. OpenAI Chat Completions => Claude Messages
|
||||
17. 🔄 思考转内容功能
|
||||
18. 🔄 针对用户的模型限流功能
|
||||
19. 🔄 请求格式转换功能,支持以下三种格式转换:
|
||||
1. OpenAI Chat Completions => Claude Messages (OpenAI格式调用Claude模型)
|
||||
2. Clade Messages => OpenAI Chat Completions (可用于Claude Code调用第三方模型)
|
||||
3. OpenAI Chat Completions => Gemini Chat
|
||||
19. 💰 缓存计费支持,开启后可以在缓存命中时按照设定的比例计费:
|
||||
3. OpenAI Chat Completions => Gemini Chat (OpenAI格式调用Gemini模型)
|
||||
20. 💰 缓存计费支持,开启后可以在缓存命中时按照设定的比例计费:
|
||||
1. 在 `系统设置-运营设置` 中设置 `提示缓存倍率` 选项
|
||||
2. 在渠道中设置 `提示缓存倍率`,范围 0-1,例如设置为 0.5 表示缓存命中时按照 50% 计费
|
||||
3. 支持的渠道:
|
||||
@@ -119,7 +120,9 @@ New API提供了丰富的功能,详细特性请参考[特性说明](https://do
|
||||
4. 自定义渠道,支持填入完整调用地址
|
||||
5. Rerank模型([Cohere](https://cohere.ai/)和[Jina](https://jina.ai/)),[接口文档](https://docs.newapi.pro/api/jinaai-rerank)
|
||||
6. Claude Messages 格式,[接口文档](https://docs.newapi.pro/api/anthropic-chat)
|
||||
7. Dify,当前仅支持chatflow
|
||||
7. Google Gemini格式,[接口文档](https://docs.newapi.pro/api/google-gemini-chat/)
|
||||
8. Dify,当前仅支持chatflow
|
||||
9. 更多接口请参考[接口文档](https://docs.newapi.pro/api)
|
||||
|
||||
## 环境变量配置
|
||||
|
||||
@@ -128,16 +131,14 @@ New API提供了丰富的功能,详细特性请参考[特性说明](https://do
|
||||
- `GENERATE_DEFAULT_TOKEN`:是否为新注册用户生成初始令牌,默认为 `false`
|
||||
- `STREAMING_TIMEOUT`:流式回复超时时间,默认300秒
|
||||
- `DIFY_DEBUG`:Dify渠道是否输出工作流和节点信息,默认 `true`
|
||||
- `FORCE_STREAM_OPTION`:是否覆盖客户端stream_options参数,默认 `true`
|
||||
- `GET_MEDIA_TOKEN`:是否统计图片token,默认 `true`
|
||||
- `GET_MEDIA_TOKEN_NOT_STREAM`:非流情况下是否统计图片token,默认 `true`
|
||||
- `UPDATE_TASK`:是否更新异步任务(Midjourney、Suno),默认 `true`
|
||||
- `COHERE_SAFETY_SETTING`:Cohere模型安全设置,可选值为 `NONE`, `CONTEXTUAL`, `STRICT`,默认 `NONE`
|
||||
- `GEMINI_VISION_MAX_IMAGE_NUM`:Gemini模型最大图片数量,默认 `16`
|
||||
- `MAX_FILE_DOWNLOAD_MB`: 最大文件下载大小,单位MB,默认 `20`
|
||||
- `CRYPTO_SECRET`:加密密钥,用于加密数据库内容
|
||||
- `CRYPTO_SECRET`:加密密钥,用于加密Redis数据库内容
|
||||
- `AZURE_DEFAULT_API_VERSION`:Azure渠道默认API版本,默认 `2025-04-01-preview`
|
||||
- `NOTIFICATION_LIMIT_DURATION_MINUTE`:通知限制持续时间,默认 `10`分钟
|
||||
- `NOTIFICATION_LIMIT_DURATION_MINUTE`:邮件等通知限制持续时间,默认 `10`分钟
|
||||
- `NOTIFY_LIMIT_COUNT`:用户通知在指定持续时间内的最大数量,默认 `2`
|
||||
- `ERROR_LOG_ENABLED=true`: 是否记录并显示错误日志,默认`false`
|
||||
|
||||
@@ -182,7 +183,7 @@ docker run --name new-api -d --restart always -p 3000:3000 -e SQL_DSN="root:1234
|
||||
```
|
||||
|
||||
## 渠道重试与缓存
|
||||
渠道重试功能已经实现,可以在`设置->运营设置->通用设置`设置重试次数,**建议开启缓存**功能。
|
||||
渠道重试功能已经实现,可以在`设置->运营设置->通用设置->失败重试次数`设置重试次数,**建议开启缓存**功能。
|
||||
|
||||
### 缓存设置方法
|
||||
1. `REDIS_CONN_STRING`:设置Redis作为缓存
|
||||
@@ -192,16 +193,17 @@ docker run --name new-api -d --restart always -p 3000:3000 -e SQL_DSN="root:1234
|
||||
|
||||
详细接口文档请参考[接口文档](https://docs.newapi.pro/api):
|
||||
|
||||
- [聊天接口(Chat)](https://docs.newapi.pro/api/openai-chat)
|
||||
- [聊天接口(Chat Completions)](https://docs.newapi.pro/api/openai-chat)
|
||||
- [响应接口 (Responses)](https://docs.newapi.pro/api/openai-responses)
|
||||
- [图像接口(Image)](https://docs.newapi.pro/api/openai-image)
|
||||
- [重排序接口(Rerank)](https://docs.newapi.pro/api/jinaai-rerank)
|
||||
- [实时对话接口(Realtime)](https://docs.newapi.pro/api/openai-realtime)
|
||||
- [Claude聊天接口(messages)](https://docs.newapi.pro/api/anthropic-chat)
|
||||
- [Claude聊天接口](https://docs.newapi.pro/api/anthropic-chat)
|
||||
- [Google Gemini聊天接口](https://docs.newapi.pro/api/google-gemini-chat)
|
||||
|
||||
## 相关项目
|
||||
- [One API](https://github.com/songquanpeng/one-api):原版项目
|
||||
- [Midjourney-Proxy](https://github.com/novicezk/midjourney-proxy):Midjourney接口支持
|
||||
- [chatnio](https://github.com/Deeptrain-Community/chatnio):下一代AI一站式B/C端解决方案
|
||||
- [neko-api-key-tool](https://github.com/Calcium-Ion/neko-api-key-tool):用key查询使用额度
|
||||
|
||||
其他基于New API的项目:
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
package common
|
||||
|
||||
import "one-api/constant"
|
||||
import "github.com/QuantumNous/new-api/constant"
|
||||
|
||||
func ChannelType2APIType(channelType int) (int, bool) {
|
||||
apiType := -1
|
||||
|
||||
@@ -19,6 +19,7 @@ var TopUpLink = ""
|
||||
// var ChatLink = ""
|
||||
// var ChatLink2 = ""
|
||||
var QuotaPerUnit = 500 * 1000.0 // $0.002 / 1K tokens
|
||||
// 保留旧变量以兼容历史逻辑,实际展示由 general_setting.quota_display_type 控制
|
||||
var DisplayInCurrencyEnabled = true
|
||||
var DisplayTokenStatEnabled = true
|
||||
var DrawingEnabled = true
|
||||
|
||||
@@ -4,6 +4,7 @@ import (
|
||||
"crypto/hmac"
|
||||
"crypto/sha256"
|
||||
"encoding/hex"
|
||||
|
||||
"golang.org/x/crypto/bcrypt"
|
||||
)
|
||||
|
||||
|
||||
@@ -12,4 +12,4 @@ var LogSqlType = DatabaseTypeSQLite // Default to SQLite for logging SQL queries
|
||||
var UsingMySQL = false
|
||||
var UsingClickHouse = false
|
||||
|
||||
var SQLitePath = "one-api.db?_busy_timeout=30000"
|
||||
var SQLitePath = "one-api.db?_busy_timeout=30000"
|
||||
|
||||
@@ -2,9 +2,10 @@ package common
|
||||
|
||||
import (
|
||||
"embed"
|
||||
"github.com/gin-contrib/static"
|
||||
"io/fs"
|
||||
"net/http"
|
||||
|
||||
"github.com/gin-contrib/static"
|
||||
)
|
||||
|
||||
// Credit: https://github.com/gin-contrib/static/issues/19
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
package common
|
||||
|
||||
import "one-api/constant"
|
||||
import "github.com/QuantumNous/new-api/constant"
|
||||
|
||||
// EndpointInfo 描述单个端点的默认请求信息
|
||||
// path: 上游路径
|
||||
@@ -23,6 +23,7 @@ var defaultEndpointInfoMap = map[constant.EndpointType]EndpointInfo{
|
||||
constant.EndpointTypeGemini: {Path: "/v1beta/models/{model}:generateContent", Method: "POST"},
|
||||
constant.EndpointTypeJinaRerank: {Path: "/rerank", Method: "POST"},
|
||||
constant.EndpointTypeImageGeneration: {Path: "/v1/images/generations", Method: "POST"},
|
||||
constant.EndpointTypeEmbeddings: {Path: "/v1/embeddings", Method: "POST"},
|
||||
}
|
||||
|
||||
// GetDefaultEndpointInfo 返回指定端点类型的默认信息以及是否存在
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
package common
|
||||
|
||||
import "one-api/constant"
|
||||
import "github.com/QuantumNous/new-api/constant"
|
||||
|
||||
// GetEndpointTypesByChannelType 获取渠道最优先端点类型(所有的渠道都支持 OpenAI 端点)
|
||||
func GetEndpointTypesByChannelType(channelType int, modelName string) []constant.EndpointType {
|
||||
|
||||
@@ -3,11 +3,13 @@ package common
|
||||
import (
|
||||
"bytes"
|
||||
"io"
|
||||
"mime/multipart"
|
||||
"net/http"
|
||||
"one-api/constant"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/QuantumNous/new-api/constant"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
@@ -113,3 +115,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
|
||||
}
|
||||
|
||||
@@ -3,8 +3,9 @@ package common
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"github.com/bytedance/gopkg/util/gopool"
|
||||
"math"
|
||||
|
||||
"github.com/bytedance/gopkg/util/gopool"
|
||||
)
|
||||
|
||||
var relayGoPool gopool.Pool
|
||||
|
||||
@@ -4,11 +4,12 @@ import (
|
||||
"flag"
|
||||
"fmt"
|
||||
"log"
|
||||
"one-api/constant"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"github.com/QuantumNous/new-api/constant"
|
||||
)
|
||||
|
||||
var (
|
||||
@@ -19,10 +20,10 @@ var (
|
||||
)
|
||||
|
||||
func printHelp() {
|
||||
fmt.Println("New API " + Version + " - All in one API service for OpenAI API.")
|
||||
fmt.Println("Copyright (C) 2023 JustSong. All rights reserved.")
|
||||
fmt.Println("GitHub: https://github.com/songquanpeng/one-api")
|
||||
fmt.Println("Usage: one-api [--port <port>] [--log-dir <log directory>] [--version] [--help]")
|
||||
fmt.Println("NewAPI(Based OneAPI) " + Version + " - The next-generation LLM gateway and AI asset management system supports multiple languages.")
|
||||
fmt.Println("Original Project: OneAPI by JustSong - https://github.com/songquanpeng/one-api")
|
||||
fmt.Println("Maintainer: QuantumNous - https://github.com/QuantumNous/new-api")
|
||||
fmt.Println("Usage: newapi [--port <port>] [--log-dir <log directory>] [--version] [--help]")
|
||||
}
|
||||
|
||||
func InitEnv() {
|
||||
|
||||
@@ -4,9 +4,10 @@ import (
|
||||
"context"
|
||||
_ "embed"
|
||||
"fmt"
|
||||
"github.com/go-redis/redis/v8"
|
||||
"one-api/common"
|
||||
"sync"
|
||||
|
||||
"github.com/QuantumNous/new-api/common"
|
||||
"github.com/go-redis/redis/v8"
|
||||
)
|
||||
|
||||
//go:embed lua/rate_limit.lua
|
||||
|
||||
@@ -2,10 +2,11 @@ package common
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/shirou/gopsutil/cpu"
|
||||
"os"
|
||||
"runtime/pprof"
|
||||
"time"
|
||||
|
||||
"github.com/shirou/gopsutil/cpu"
|
||||
)
|
||||
|
||||
// Monitor 定时监控cpu使用率,超过阈值输出pprof文件
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
package common
|
||||
|
||||
import (
|
||||
"github.com/google/uuid"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/google/uuid"
|
||||
)
|
||||
|
||||
type verificationValue struct {
|
||||
|
||||
@@ -31,7 +31,7 @@ const (
|
||||
APITypeXai
|
||||
APITypeCoze
|
||||
APITypeJimeng
|
||||
APITypeMoonshot
|
||||
APITypeSubmodel
|
||||
APITypeDummy // this one is only for count, do not add any channel after this
|
||||
APITypeMoonshot
|
||||
APITypeSubmodel
|
||||
APITypeDummy // this one is only for count, do not add any channel after this
|
||||
)
|
||||
|
||||
@@ -51,9 +51,10 @@ const (
|
||||
ChannelTypeJimeng = 51
|
||||
ChannelTypeVidu = 52
|
||||
ChannelTypeSubmodel = 53
|
||||
ChannelTypeDoubaoVideo = 54
|
||||
ChannelTypeSora = 55
|
||||
ChannelTypeDummy // this one is only for count, do not add any channel after this
|
||||
|
||||
|
||||
)
|
||||
|
||||
var ChannelBaseURLs = []string{
|
||||
@@ -111,4 +112,68 @@ var ChannelBaseURLs = []string{
|
||||
"https://visual.volcengineapi.com", //51
|
||||
"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{
|
||||
ChannelTypeUnknown: "Unknown",
|
||||
ChannelTypeOpenAI: "OpenAI",
|
||||
ChannelTypeMidjourney: "Midjourney",
|
||||
ChannelTypeAzure: "Azure",
|
||||
ChannelTypeOllama: "Ollama",
|
||||
ChannelTypeMidjourneyPlus: "MidjourneyPlus",
|
||||
ChannelTypeOpenAIMax: "OpenAIMax",
|
||||
ChannelTypeOhMyGPT: "OhMyGPT",
|
||||
ChannelTypeCustom: "Custom",
|
||||
ChannelTypeAILS: "AILS",
|
||||
ChannelTypeAIProxy: "AIProxy",
|
||||
ChannelTypePaLM: "PaLM",
|
||||
ChannelTypeAPI2GPT: "API2GPT",
|
||||
ChannelTypeAIGC2D: "AIGC2D",
|
||||
ChannelTypeAnthropic: "Anthropic",
|
||||
ChannelTypeBaidu: "Baidu",
|
||||
ChannelTypeZhipu: "Zhipu",
|
||||
ChannelTypeAli: "Ali",
|
||||
ChannelTypeXunfei: "Xunfei",
|
||||
ChannelType360: "360",
|
||||
ChannelTypeOpenRouter: "OpenRouter",
|
||||
ChannelTypeAIProxyLibrary: "AIProxyLibrary",
|
||||
ChannelTypeFastGPT: "FastGPT",
|
||||
ChannelTypeTencent: "Tencent",
|
||||
ChannelTypeGemini: "Gemini",
|
||||
ChannelTypeMoonshot: "Moonshot",
|
||||
ChannelTypeZhipu_v4: "ZhipuV4",
|
||||
ChannelTypePerplexity: "Perplexity",
|
||||
ChannelTypeLingYiWanWu: "LingYiWanWu",
|
||||
ChannelTypeAws: "AWS",
|
||||
ChannelTypeCohere: "Cohere",
|
||||
ChannelTypeMiniMax: "MiniMax",
|
||||
ChannelTypeSunoAPI: "SunoAPI",
|
||||
ChannelTypeDify: "Dify",
|
||||
ChannelTypeJina: "Jina",
|
||||
ChannelCloudflare: "Cloudflare",
|
||||
ChannelTypeSiliconFlow: "SiliconFlow",
|
||||
ChannelTypeVertexAi: "VertexAI",
|
||||
ChannelTypeMistral: "Mistral",
|
||||
ChannelTypeDeepSeek: "DeepSeek",
|
||||
ChannelTypeMokaAI: "MokaAI",
|
||||
ChannelTypeVolcEngine: "VolcEngine",
|
||||
ChannelTypeBaiduV2: "BaiduV2",
|
||||
ChannelTypeXinference: "Xinference",
|
||||
ChannelTypeXai: "xAI",
|
||||
ChannelTypeCoze: "Coze",
|
||||
ChannelTypeKling: "Kling",
|
||||
ChannelTypeJimeng: "Jimeng",
|
||||
ChannelTypeVidu: "Vidu",
|
||||
ChannelTypeSubmodel: "Submodel",
|
||||
ChannelTypeDoubaoVideo: "DoubaoVideo",
|
||||
ChannelTypeSora: "Sora",
|
||||
}
|
||||
|
||||
func GetChannelTypeName(channelType int) string {
|
||||
if name, ok := ChannelTypeNames[channelType]; ok {
|
||||
return name
|
||||
}
|
||||
return "Unknown"
|
||||
}
|
||||
|
||||
@@ -9,6 +9,7 @@ const (
|
||||
EndpointTypeGemini EndpointType = "gemini"
|
||||
EndpointTypeJinaRerank EndpointType = "jina-rerank"
|
||||
EndpointTypeImageGeneration EndpointType = "image-generation"
|
||||
EndpointTypeEmbeddings EndpointType = "embeddings"
|
||||
//EndpointTypeMidjourney EndpointType = "midjourney-proxy"
|
||||
//EndpointTypeSuno EndpointType = "suno-proxy"
|
||||
//EndpointTypeKling EndpointType = "kling"
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
package controller
|
||||
|
||||
import (
|
||||
"github.com/QuantumNous/new-api/common"
|
||||
"github.com/QuantumNous/new-api/dto"
|
||||
"github.com/QuantumNous/new-api/model"
|
||||
"github.com/QuantumNous/new-api/setting/operation_setting"
|
||||
"github.com/gin-gonic/gin"
|
||||
"one-api/common"
|
||||
"one-api/dto"
|
||||
"one-api/model"
|
||||
)
|
||||
|
||||
func GetSubscription(c *gin.Context) {
|
||||
@@ -39,8 +40,18 @@ func GetSubscription(c *gin.Context) {
|
||||
}
|
||||
quota := remainQuota + usedQuota
|
||||
amount := float64(quota)
|
||||
if common.DisplayInCurrencyEnabled {
|
||||
amount /= common.QuotaPerUnit
|
||||
// OpenAI 兼容接口中的 *_USD 字段含义保持“额度单位”对应值:
|
||||
// 我们将其解释为以“站点展示类型”为准:
|
||||
// - USD: 直接除以 QuotaPerUnit
|
||||
// - CNY: 先转 USD 再乘汇率
|
||||
// - TOKENS: 直接使用 tokens 数量
|
||||
switch operation_setting.GetQuotaDisplayType() {
|
||||
case operation_setting.QuotaDisplayTypeCNY:
|
||||
amount = amount / common.QuotaPerUnit * operation_setting.USDExchangeRate
|
||||
case operation_setting.QuotaDisplayTypeTokens:
|
||||
// amount 保持 tokens 数值
|
||||
default:
|
||||
amount = amount / common.QuotaPerUnit
|
||||
}
|
||||
if token != nil && token.UnlimitedQuota {
|
||||
amount = 100000000
|
||||
@@ -80,8 +91,13 @@ func GetUsage(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
amount := float64(quota)
|
||||
if common.DisplayInCurrencyEnabled {
|
||||
amount /= common.QuotaPerUnit
|
||||
switch operation_setting.GetQuotaDisplayType() {
|
||||
case operation_setting.QuotaDisplayTypeCNY:
|
||||
amount = amount / common.QuotaPerUnit * operation_setting.USDExchangeRate
|
||||
case operation_setting.QuotaDisplayTypeTokens:
|
||||
// tokens 保持原值
|
||||
default:
|
||||
amount = amount / common.QuotaPerUnit
|
||||
}
|
||||
usage := OpenAIUsageResponse{
|
||||
Object: "list",
|
||||
|
||||
@@ -6,15 +6,16 @@ import (
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"one-api/common"
|
||||
"one-api/constant"
|
||||
"one-api/model"
|
||||
"one-api/service"
|
||||
"one-api/setting/operation_setting"
|
||||
"one-api/types"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"github.com/QuantumNous/new-api/common"
|
||||
"github.com/QuantumNous/new-api/constant"
|
||||
"github.com/QuantumNous/new-api/model"
|
||||
"github.com/QuantumNous/new-api/service"
|
||||
"github.com/QuantumNous/new-api/setting/operation_setting"
|
||||
"github.com/QuantumNous/new-api/types"
|
||||
|
||||
"github.com/shopspring/decimal"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
@@ -127,6 +128,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 {
|
||||
|
||||
@@ -10,24 +10,26 @@ import (
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"net/url"
|
||||
"one-api/common"
|
||||
"one-api/constant"
|
||||
"one-api/dto"
|
||||
"one-api/middleware"
|
||||
"one-api/model"
|
||||
"one-api/relay"
|
||||
relaycommon "one-api/relay/common"
|
||||
relayconstant "one-api/relay/constant"
|
||||
"one-api/relay/helper"
|
||||
"one-api/service"
|
||||
"one-api/setting/operation_setting"
|
||||
"one-api/types"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/QuantumNous/new-api/common"
|
||||
"github.com/QuantumNous/new-api/constant"
|
||||
"github.com/QuantumNous/new-api/dto"
|
||||
"github.com/QuantumNous/new-api/middleware"
|
||||
"github.com/QuantumNous/new-api/model"
|
||||
"github.com/QuantumNous/new-api/relay"
|
||||
relaycommon "github.com/QuantumNous/new-api/relay/common"
|
||||
relayconstant "github.com/QuantumNous/new-api/relay/constant"
|
||||
"github.com/QuantumNous/new-api/relay/helper"
|
||||
"github.com/QuantumNous/new-api/service"
|
||||
"github.com/QuantumNous/new-api/setting/operation_setting"
|
||||
"github.com/QuantumNous/new-api/types"
|
||||
|
||||
"github.com/bytedance/gopkg/util/gopool"
|
||||
"github.com/samber/lo"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
@@ -38,61 +40,63 @@ type testResult struct {
|
||||
newAPIError *types.NewAPIError
|
||||
}
|
||||
|
||||
func testChannel(channel *model.Channel, testModel string) testResult {
|
||||
func testChannel(channel *model.Channel, testModel string, endpointType string) testResult {
|
||||
tik := time.Now()
|
||||
if channel.Type == constant.ChannelTypeMidjourney {
|
||||
return testResult{
|
||||
localErr: errors.New("midjourney channel test is not supported"),
|
||||
newAPIError: nil,
|
||||
}
|
||||
var unsupportedTestChannelTypes = []int{
|
||||
constant.ChannelTypeMidjourney,
|
||||
constant.ChannelTypeMidjourneyPlus,
|
||||
constant.ChannelTypeSunoAPI,
|
||||
constant.ChannelTypeKling,
|
||||
constant.ChannelTypeJimeng,
|
||||
constant.ChannelTypeDoubaoVideo,
|
||||
constant.ChannelTypeVidu,
|
||||
}
|
||||
if channel.Type == constant.ChannelTypeMidjourneyPlus {
|
||||
if lo.Contains(unsupportedTestChannelTypes, channel.Type) {
|
||||
channelTypeName := constant.GetChannelTypeName(channel.Type)
|
||||
return testResult{
|
||||
localErr: errors.New("midjourney plus channel test is not supported"),
|
||||
newAPIError: nil,
|
||||
}
|
||||
}
|
||||
if channel.Type == constant.ChannelTypeSunoAPI {
|
||||
return testResult{
|
||||
localErr: errors.New("suno channel test is not supported"),
|
||||
newAPIError: nil,
|
||||
}
|
||||
}
|
||||
if channel.Type == constant.ChannelTypeKling {
|
||||
return testResult{
|
||||
localErr: errors.New("kling channel test is not supported"),
|
||||
newAPIError: nil,
|
||||
}
|
||||
}
|
||||
if channel.Type == constant.ChannelTypeJimeng {
|
||||
return testResult{
|
||||
localErr: errors.New("jimeng channel test is not supported"),
|
||||
newAPIError: nil,
|
||||
}
|
||||
}
|
||||
if channel.Type == constant.ChannelTypeVidu {
|
||||
return testResult{
|
||||
localErr: errors.New("vidu channel test is not supported"),
|
||||
newAPIError: nil,
|
||||
localErr: fmt.Errorf("%s channel test is not supported", channelTypeName),
|
||||
}
|
||||
}
|
||||
w := httptest.NewRecorder()
|
||||
c, _ := gin.CreateTestContext(w)
|
||||
|
||||
requestPath := "/v1/chat/completions"
|
||||
|
||||
// 先判断是否为 Embedding 模型
|
||||
if strings.Contains(strings.ToLower(testModel), "embedding") ||
|
||||
strings.HasPrefix(testModel, "m3e") || // m3e 系列模型
|
||||
strings.Contains(testModel, "bge-") || // bge 系列模型
|
||||
strings.Contains(testModel, "embed") ||
|
||||
channel.Type == constant.ChannelTypeMokaAI { // 其他 embedding 模型
|
||||
requestPath = "/v1/embeddings" // 修改请求路径
|
||||
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"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// VolcEngine 图像生成模型
|
||||
if channel.Type == constant.ChannelTypeVolcEngine && strings.Contains(testModel, "seedream") {
|
||||
requestPath = "/v1/images/generations"
|
||||
requestPath := "/v1/chat/completions"
|
||||
|
||||
// 如果指定了端点类型,使用指定的端点类型
|
||||
if endpointType != "" {
|
||||
if endpointInfo, ok := common.GetDefaultEndpointInfo(constant.EndpointType(endpointType)); ok {
|
||||
requestPath = endpointInfo.Path
|
||||
}
|
||||
} else {
|
||||
// 如果没有指定端点类型,使用原有的自动检测逻辑
|
||||
// 先判断是否为 Embedding 模型
|
||||
if strings.Contains(strings.ToLower(testModel), "embedding") ||
|
||||
strings.HasPrefix(testModel, "m3e") || // m3e 系列模型
|
||||
strings.Contains(testModel, "bge-") || // bge 系列模型
|
||||
strings.Contains(testModel, "embed") ||
|
||||
channel.Type == constant.ChannelTypeMokaAI { // 其他 embedding 模型
|
||||
requestPath = "/v1/embeddings" // 修改请求路径
|
||||
}
|
||||
|
||||
// VolcEngine 图像生成模型
|
||||
if channel.Type == constant.ChannelTypeVolcEngine && strings.Contains(testModel, "seedream") {
|
||||
requestPath = "/v1/images/generations"
|
||||
}
|
||||
}
|
||||
|
||||
c.Request = &http.Request{
|
||||
@@ -102,33 +106,6 @@ func testChannel(channel *model.Channel, testModel string) testResult {
|
||||
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"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 重新检查模型类型并更新请求路径
|
||||
if strings.Contains(strings.ToLower(testModel), "embedding") ||
|
||||
strings.HasPrefix(testModel, "m3e") ||
|
||||
strings.Contains(testModel, "bge-") ||
|
||||
strings.Contains(testModel, "embed") ||
|
||||
channel.Type == constant.ChannelTypeMokaAI {
|
||||
requestPath = "/v1/embeddings"
|
||||
c.Request.URL.Path = requestPath
|
||||
}
|
||||
|
||||
if channel.Type == constant.ChannelTypeVolcEngine && strings.Contains(testModel, "seedream") {
|
||||
requestPath = "/v1/images/generations"
|
||||
c.Request.URL.Path = requestPath
|
||||
}
|
||||
|
||||
cache, err := model.GetUserCache(1)
|
||||
if err != nil {
|
||||
return testResult{
|
||||
@@ -153,17 +130,54 @@ func testChannel(channel *model.Channel, testModel string) testResult {
|
||||
newAPIError: newAPIError,
|
||||
}
|
||||
}
|
||||
request := buildTestRequest(testModel)
|
||||
|
||||
// Determine relay format based on request path
|
||||
relayFormat := types.RelayFormatOpenAI
|
||||
if c.Request.URL.Path == "/v1/embeddings" {
|
||||
relayFormat = types.RelayFormatEmbedding
|
||||
}
|
||||
if c.Request.URL.Path == "/v1/images/generations" {
|
||||
relayFormat = types.RelayFormatOpenAIImage
|
||||
// Determine relay format based on endpoint type or request path
|
||||
var relayFormat types.RelayFormat
|
||||
if endpointType != "" {
|
||||
// 根据指定的端点类型设置 relayFormat
|
||||
switch constant.EndpointType(endpointType) {
|
||||
case constant.EndpointTypeOpenAI:
|
||||
relayFormat = types.RelayFormatOpenAI
|
||||
case constant.EndpointTypeOpenAIResponse:
|
||||
relayFormat = types.RelayFormatOpenAIResponses
|
||||
case constant.EndpointTypeAnthropic:
|
||||
relayFormat = types.RelayFormatClaude
|
||||
case constant.EndpointTypeGemini:
|
||||
relayFormat = types.RelayFormatGemini
|
||||
case constant.EndpointTypeJinaRerank:
|
||||
relayFormat = types.RelayFormatRerank
|
||||
case constant.EndpointTypeImageGeneration:
|
||||
relayFormat = types.RelayFormatOpenAIImage
|
||||
case constant.EndpointTypeEmbeddings:
|
||||
relayFormat = types.RelayFormatEmbedding
|
||||
default:
|
||||
relayFormat = types.RelayFormatOpenAI
|
||||
}
|
||||
} else {
|
||||
// 根据请求路径自动检测
|
||||
relayFormat = types.RelayFormatOpenAI
|
||||
if c.Request.URL.Path == "/v1/embeddings" {
|
||||
relayFormat = types.RelayFormatEmbedding
|
||||
}
|
||||
if c.Request.URL.Path == "/v1/images/generations" {
|
||||
relayFormat = types.RelayFormatOpenAIImage
|
||||
}
|
||||
if c.Request.URL.Path == "/v1/messages" {
|
||||
relayFormat = types.RelayFormatClaude
|
||||
}
|
||||
if strings.Contains(c.Request.URL.Path, "/v1beta/models") {
|
||||
relayFormat = types.RelayFormatGemini
|
||||
}
|
||||
if c.Request.URL.Path == "/v1/rerank" || c.Request.URL.Path == "/rerank" {
|
||||
relayFormat = types.RelayFormatRerank
|
||||
}
|
||||
if c.Request.URL.Path == "/v1/responses" {
|
||||
relayFormat = types.RelayFormatOpenAIResponses
|
||||
}
|
||||
}
|
||||
|
||||
request := buildTestRequest(testModel, endpointType)
|
||||
|
||||
info, err := relaycommon.GenRelayInfo(c, relayFormat, request, nil)
|
||||
|
||||
if err != nil {
|
||||
@@ -186,7 +200,8 @@ func testChannel(channel *model.Channel, testModel string) testResult {
|
||||
}
|
||||
|
||||
testModel = info.UpstreamModelName
|
||||
request.Model = testModel
|
||||
// 更新请求中的模型名称
|
||||
request.SetModelName(testModel)
|
||||
|
||||
apiType, _ := common.ChannelType2APIType(channel.Type)
|
||||
adaptor := relay.GetAdaptor(apiType)
|
||||
@@ -216,33 +231,62 @@ func testChannel(channel *model.Channel, testModel string) testResult {
|
||||
|
||||
var convertedRequest any
|
||||
// 根据 RelayMode 选择正确的转换函数
|
||||
if info.RelayMode == relayconstant.RelayModeEmbeddings {
|
||||
// 创建一个 EmbeddingRequest
|
||||
embeddingRequest := dto.EmbeddingRequest{
|
||||
Input: request.Input,
|
||||
Model: request.Model,
|
||||
}
|
||||
// 调用专门用于 Embedding 的转换函数
|
||||
convertedRequest, err = adaptor.ConvertEmbeddingRequest(c, info, embeddingRequest)
|
||||
} else if info.RelayMode == relayconstant.RelayModeImagesGenerations {
|
||||
// 创建一个 ImageRequest
|
||||
prompt := "cat"
|
||||
if request.Prompt != nil {
|
||||
if promptStr, ok := request.Prompt.(string); ok && promptStr != "" {
|
||||
prompt = promptStr
|
||||
switch info.RelayMode {
|
||||
case relayconstant.RelayModeEmbeddings:
|
||||
// Embedding 请求 - request 已经是正确的类型
|
||||
if embeddingReq, ok := request.(*dto.EmbeddingRequest); ok {
|
||||
convertedRequest, err = adaptor.ConvertEmbeddingRequest(c, info, *embeddingReq)
|
||||
} else {
|
||||
return testResult{
|
||||
context: c,
|
||||
localErr: errors.New("invalid embedding request type"),
|
||||
newAPIError: types.NewError(errors.New("invalid embedding request type"), types.ErrorCodeConvertRequestFailed),
|
||||
}
|
||||
}
|
||||
imageRequest := dto.ImageRequest{
|
||||
Prompt: prompt,
|
||||
Model: request.Model,
|
||||
N: uint(request.N),
|
||||
Size: request.Size,
|
||||
case relayconstant.RelayModeImagesGenerations:
|
||||
// 图像生成请求 - request 已经是正确的类型
|
||||
if imageReq, ok := request.(*dto.ImageRequest); ok {
|
||||
convertedRequest, err = adaptor.ConvertImageRequest(c, info, *imageReq)
|
||||
} else {
|
||||
return testResult{
|
||||
context: c,
|
||||
localErr: errors.New("invalid image request type"),
|
||||
newAPIError: types.NewError(errors.New("invalid image request type"), types.ErrorCodeConvertRequestFailed),
|
||||
}
|
||||
}
|
||||
case relayconstant.RelayModeRerank:
|
||||
// Rerank 请求 - request 已经是正确的类型
|
||||
if rerankReq, ok := request.(*dto.RerankRequest); ok {
|
||||
convertedRequest, err = adaptor.ConvertRerankRequest(c, info.RelayMode, *rerankReq)
|
||||
} else {
|
||||
return testResult{
|
||||
context: c,
|
||||
localErr: errors.New("invalid rerank request type"),
|
||||
newAPIError: types.NewError(errors.New("invalid rerank request type"), types.ErrorCodeConvertRequestFailed),
|
||||
}
|
||||
}
|
||||
case relayconstant.RelayModeResponses:
|
||||
// Response 请求 - request 已经是正确的类型
|
||||
if responseReq, ok := request.(*dto.OpenAIResponsesRequest); ok {
|
||||
convertedRequest, err = adaptor.ConvertOpenAIResponsesRequest(c, info, *responseReq)
|
||||
} else {
|
||||
return testResult{
|
||||
context: c,
|
||||
localErr: errors.New("invalid response request type"),
|
||||
newAPIError: types.NewError(errors.New("invalid response request type"), types.ErrorCodeConvertRequestFailed),
|
||||
}
|
||||
}
|
||||
default:
|
||||
// Chat/Completion 等其他请求类型
|
||||
if generalReq, ok := request.(*dto.GeneralOpenAIRequest); ok {
|
||||
convertedRequest, err = adaptor.ConvertOpenAIRequest(c, info, generalReq)
|
||||
} else {
|
||||
return testResult{
|
||||
context: c,
|
||||
localErr: errors.New("invalid general request type"),
|
||||
newAPIError: types.NewError(errors.New("invalid general request type"), types.ErrorCodeConvertRequestFailed),
|
||||
}
|
||||
}
|
||||
// 调用专门用于图像生成的转换函数
|
||||
convertedRequest, err = adaptor.ConvertImageRequest(c, info, imageRequest)
|
||||
} else {
|
||||
// 对其他所有请求类型(如 Chat),保持原有逻辑
|
||||
convertedRequest, err = adaptor.ConvertOpenAIRequest(c, info, request)
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
@@ -345,22 +389,82 @@ func testChannel(channel *model.Channel, testModel string) testResult {
|
||||
}
|
||||
}
|
||||
|
||||
func buildTestRequest(model string) *dto.GeneralOpenAIRequest {
|
||||
testRequest := &dto.GeneralOpenAIRequest{
|
||||
Model: "", // this will be set later
|
||||
Stream: false,
|
||||
func buildTestRequest(model string, endpointType string) dto.Request {
|
||||
// 根据端点类型构建不同的测试请求
|
||||
if endpointType != "" {
|
||||
switch constant.EndpointType(endpointType) {
|
||||
case constant.EndpointTypeEmbeddings:
|
||||
// 返回 EmbeddingRequest
|
||||
return &dto.EmbeddingRequest{
|
||||
Model: model,
|
||||
Input: []any{"hello world"},
|
||||
}
|
||||
case constant.EndpointTypeImageGeneration:
|
||||
// 返回 ImageRequest
|
||||
return &dto.ImageRequest{
|
||||
Model: model,
|
||||
Prompt: "a cute cat",
|
||||
N: 1,
|
||||
Size: "1024x1024",
|
||||
}
|
||||
case constant.EndpointTypeJinaRerank:
|
||||
// 返回 RerankRequest
|
||||
return &dto.RerankRequest{
|
||||
Model: model,
|
||||
Query: "What is Deep Learning?",
|
||||
Documents: []any{"Deep Learning is a subset of machine learning.", "Machine learning is a field of artificial intelligence."},
|
||||
TopN: 2,
|
||||
}
|
||||
case constant.EndpointTypeOpenAIResponse:
|
||||
// 返回 OpenAIResponsesRequest
|
||||
return &dto.OpenAIResponsesRequest{
|
||||
Model: model,
|
||||
Input: json.RawMessage("\"hi\""),
|
||||
}
|
||||
case constant.EndpointTypeAnthropic, constant.EndpointTypeGemini, constant.EndpointTypeOpenAI:
|
||||
// 返回 GeneralOpenAIRequest
|
||||
maxTokens := uint(10)
|
||||
if constant.EndpointType(endpointType) == constant.EndpointTypeGemini {
|
||||
maxTokens = 3000
|
||||
}
|
||||
return &dto.GeneralOpenAIRequest{
|
||||
Model: model,
|
||||
Stream: false,
|
||||
Messages: []dto.Message{
|
||||
{
|
||||
Role: "user",
|
||||
Content: "hi",
|
||||
},
|
||||
},
|
||||
MaxTokens: maxTokens,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 自动检测逻辑(保持原有行为)
|
||||
// 先判断是否为 Embedding 模型
|
||||
if strings.Contains(strings.ToLower(model), "embedding") || // 其他 embedding 模型
|
||||
strings.HasPrefix(model, "m3e") || // m3e 系列模型
|
||||
if strings.Contains(strings.ToLower(model), "embedding") ||
|
||||
strings.HasPrefix(model, "m3e") ||
|
||||
strings.Contains(model, "bge-") {
|
||||
testRequest.Model = model
|
||||
// Embedding 请求
|
||||
testRequest.Input = []any{"hello world"} // 修改为any,因为dto/openai_request.go 的ParseInput方法无法处理[]string类型
|
||||
return testRequest
|
||||
// 返回 EmbeddingRequest
|
||||
return &dto.EmbeddingRequest{
|
||||
Model: model,
|
||||
Input: []any{"hello world"},
|
||||
}
|
||||
}
|
||||
// 并非Embedding 模型
|
||||
|
||||
// Chat/Completion 请求 - 返回 GeneralOpenAIRequest
|
||||
testRequest := &dto.GeneralOpenAIRequest{
|
||||
Model: model,
|
||||
Stream: false,
|
||||
Messages: []dto.Message{
|
||||
{
|
||||
Role: "user",
|
||||
Content: "hi",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
if strings.HasPrefix(model, "o") {
|
||||
testRequest.MaxCompletionTokens = 10
|
||||
} else if strings.Contains(model, "thinking") {
|
||||
@@ -373,12 +477,6 @@ func buildTestRequest(model string) *dto.GeneralOpenAIRequest {
|
||||
testRequest.MaxTokens = 10
|
||||
}
|
||||
|
||||
testMessage := dto.Message{
|
||||
Role: "user",
|
||||
Content: "hi",
|
||||
}
|
||||
testRequest.Model = model
|
||||
testRequest.Messages = append(testRequest.Messages, testMessage)
|
||||
return testRequest
|
||||
}
|
||||
|
||||
@@ -402,8 +500,9 @@ func TestChannel(c *gin.Context) {
|
||||
// }
|
||||
//}()
|
||||
testModel := c.Query("model")
|
||||
endpointType := c.Query("endpoint_type")
|
||||
tik := time.Now()
|
||||
result := testChannel(channel, testModel)
|
||||
result := testChannel(channel, testModel, endpointType)
|
||||
if result.localErr != nil {
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"success": false,
|
||||
@@ -429,7 +528,6 @@ func TestChannel(c *gin.Context) {
|
||||
"message": "",
|
||||
"time": consumedTime,
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
var testAllChannelsLock sync.Mutex
|
||||
@@ -463,7 +561,7 @@ func testAllChannels(notify bool) error {
|
||||
for _, channel := range channels {
|
||||
isChannelEnabled := channel.Status == common.ChannelStatusEnabled
|
||||
tik := time.Now()
|
||||
result := testChannel(channel, "")
|
||||
result := testChannel(channel, "", "")
|
||||
tok := time.Now()
|
||||
milliseconds := tok.Sub(tik).Milliseconds()
|
||||
|
||||
@@ -477,7 +575,7 @@ func testAllChannels(notify bool) error {
|
||||
// 当错误检查通过,才检查响应时间
|
||||
if common.AutomaticDisableChannelEnabled && !shouldBanChannel {
|
||||
if milliseconds > disableThreshold {
|
||||
err := errors.New(fmt.Sprintf("响应时间 %.2fs 超过阈值 %.2fs", float64(milliseconds)/1000.0, float64(disableThreshold)/1000.0))
|
||||
err := fmt.Errorf("响应时间 %.2fs 超过阈值 %.2fs", float64(milliseconds)/1000.0, float64(disableThreshold)/1000.0)
|
||||
newAPIError = types.NewOpenAIError(err, types.ErrorCodeChannelResponseTimeExceeded, http.StatusRequestTimeout)
|
||||
shouldBanChannel = true
|
||||
}
|
||||
@@ -514,7 +612,6 @@ func TestAllChannels(c *gin.Context) {
|
||||
"success": true,
|
||||
"message": "",
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
var autoTestChannelsOnce sync.Once
|
||||
@@ -526,10 +623,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")
|
||||
|
||||
@@ -4,14 +4,15 @@ import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"one-api/common"
|
||||
"one-api/constant"
|
||||
"one-api/dto"
|
||||
"one-api/model"
|
||||
"one-api/service"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/QuantumNous/new-api/common"
|
||||
"github.com/QuantumNous/new-api/constant"
|
||||
"github.com/QuantumNous/new-api/dto"
|
||||
"github.com/QuantumNous/new-api/model"
|
||||
"github.com/QuantumNous/new-api/service"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
@@ -198,9 +199,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 {
|
||||
|
||||
@@ -5,8 +5,9 @@ package controller
|
||||
import (
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"one-api/common"
|
||||
"one-api/model"
|
||||
|
||||
"github.com/QuantumNous/new-api/common"
|
||||
"github.com/QuantumNous/new-api/model"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
@@ -6,11 +6,12 @@ import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"one-api/common"
|
||||
"one-api/model"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"github.com/QuantumNous/new-api/common"
|
||||
"github.com/QuantumNous/new-api/model"
|
||||
|
||||
"github.com/gin-contrib/sessions"
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
@@ -2,9 +2,10 @@ package controller
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"one-api/model"
|
||||
"one-api/setting"
|
||||
"one-api/setting/ratio_setting"
|
||||
|
||||
"github.com/QuantumNous/new-api/model"
|
||||
"github.com/QuantumNous/new-api/setting"
|
||||
"github.com/QuantumNous/new-api/setting/ratio_setting"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
@@ -7,12 +7,13 @@ import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"one-api/common"
|
||||
"one-api/model"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/QuantumNous/new-api/common"
|
||||
"github.com/QuantumNous/new-api/model"
|
||||
|
||||
"github.com/gin-contrib/sessions"
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
@@ -2,10 +2,11 @@ package controller
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"one-api/common"
|
||||
"one-api/model"
|
||||
"strconv"
|
||||
|
||||
"github.com/QuantumNous/new-api/common"
|
||||
"github.com/QuantumNous/new-api/model"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
|
||||
@@ -7,15 +7,16 @@ import (
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"one-api/common"
|
||||
"one-api/dto"
|
||||
"one-api/logger"
|
||||
"one-api/model"
|
||||
"one-api/service"
|
||||
"one-api/setting"
|
||||
"one-api/setting/system_setting"
|
||||
"time"
|
||||
|
||||
"github.com/QuantumNous/new-api/common"
|
||||
"github.com/QuantumNous/new-api/dto"
|
||||
"github.com/QuantumNous/new-api/logger"
|
||||
"github.com/QuantumNous/new-api/model"
|
||||
"github.com/QuantumNous/new-api/service"
|
||||
"github.com/QuantumNous/new-api/setting"
|
||||
"github.com/QuantumNous/new-api/setting/system_setting"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
|
||||
@@ -4,16 +4,17 @@ import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"one-api/common"
|
||||
"one-api/constant"
|
||||
"one-api/middleware"
|
||||
"one-api/model"
|
||||
"one-api/setting"
|
||||
"one-api/setting/console_setting"
|
||||
"one-api/setting/operation_setting"
|
||||
"one-api/setting/system_setting"
|
||||
"strings"
|
||||
|
||||
"github.com/QuantumNous/new-api/common"
|
||||
"github.com/QuantumNous/new-api/constant"
|
||||
"github.com/QuantumNous/new-api/middleware"
|
||||
"github.com/QuantumNous/new-api/model"
|
||||
"github.com/QuantumNous/new-api/setting"
|
||||
"github.com/QuantumNous/new-api/setting/console_setting"
|
||||
"github.com/QuantumNous/new-api/setting/operation_setting"
|
||||
"github.com/QuantumNous/new-api/setting/system_setting"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
@@ -43,6 +44,7 @@ func GetStatus(c *gin.Context) {
|
||||
defer common.OptionMapRWMutex.RUnlock()
|
||||
|
||||
passkeySetting := system_setting.GetPasskeySettings()
|
||||
legalSetting := system_setting.GetLegalSettings()
|
||||
|
||||
data := gin.H{
|
||||
"version": common.Version,
|
||||
@@ -66,18 +68,22 @@ func GetStatus(c *gin.Context) {
|
||||
"top_up_link": common.TopUpLink,
|
||||
"docs_link": operation_setting.GetGeneralSetting().DocsLink,
|
||||
"quota_per_unit": common.QuotaPerUnit,
|
||||
"display_in_currency": common.DisplayInCurrencyEnabled,
|
||||
"enable_batch_update": common.BatchUpdateEnabled,
|
||||
"enable_drawing": common.DrawingEnabled,
|
||||
"enable_task": common.TaskEnabled,
|
||||
"enable_data_export": common.DataExportEnabled,
|
||||
"data_export_default_time": common.DataExportDefaultTime,
|
||||
"default_collapse_sidebar": common.DefaultCollapseSidebar,
|
||||
"mj_notify_enabled": setting.MjNotifyEnabled,
|
||||
"chats": setting.Chats,
|
||||
"demo_site_enabled": operation_setting.DemoSiteEnabled,
|
||||
"self_use_mode_enabled": operation_setting.SelfUseModeEnabled,
|
||||
"default_use_auto_group": setting.DefaultUseAutoGroup,
|
||||
// 兼容旧前端:保留 display_in_currency,同时提供新的 quota_display_type
|
||||
"display_in_currency": operation_setting.IsCurrencyDisplay(),
|
||||
"quota_display_type": operation_setting.GetQuotaDisplayType(),
|
||||
"custom_currency_symbol": operation_setting.GetGeneralSetting().CustomCurrencySymbol,
|
||||
"custom_currency_exchange_rate": operation_setting.GetGeneralSetting().CustomCurrencyExchangeRate,
|
||||
"enable_batch_update": common.BatchUpdateEnabled,
|
||||
"enable_drawing": common.DrawingEnabled,
|
||||
"enable_task": common.TaskEnabled,
|
||||
"enable_data_export": common.DataExportEnabled,
|
||||
"data_export_default_time": common.DataExportDefaultTime,
|
||||
"default_collapse_sidebar": common.DefaultCollapseSidebar,
|
||||
"mj_notify_enabled": setting.MjNotifyEnabled,
|
||||
"chats": setting.Chats,
|
||||
"demo_site_enabled": operation_setting.DemoSiteEnabled,
|
||||
"self_use_mode_enabled": operation_setting.SelfUseModeEnabled,
|
||||
"default_use_auto_group": setting.DefaultUseAutoGroup,
|
||||
|
||||
"usd_exchange_rate": operation_setting.USDExchangeRate,
|
||||
"price": operation_setting.Price,
|
||||
@@ -104,6 +110,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 != "",
|
||||
}
|
||||
|
||||
// 根据启用状态注入可选内容
|
||||
@@ -147,6 +155,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()
|
||||
|
||||
@@ -2,7 +2,8 @@ package controller
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"one-api/model"
|
||||
|
||||
"github.com/QuantumNous/new-api/model"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
@@ -2,21 +2,22 @@ package controller
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"github.com/QuantumNous/new-api/common"
|
||||
"github.com/QuantumNous/new-api/constant"
|
||||
"github.com/QuantumNous/new-api/dto"
|
||||
"github.com/QuantumNous/new-api/model"
|
||||
"github.com/QuantumNous/new-api/relay"
|
||||
"github.com/QuantumNous/new-api/relay/channel/ai360"
|
||||
"github.com/QuantumNous/new-api/relay/channel/lingyiwanwu"
|
||||
"github.com/QuantumNous/new-api/relay/channel/minimax"
|
||||
"github.com/QuantumNous/new-api/relay/channel/moonshot"
|
||||
relaycommon "github.com/QuantumNous/new-api/relay/common"
|
||||
"github.com/QuantumNous/new-api/setting"
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/samber/lo"
|
||||
"net/http"
|
||||
"one-api/common"
|
||||
"one-api/constant"
|
||||
"one-api/dto"
|
||||
"one-api/model"
|
||||
"one-api/relay"
|
||||
"one-api/relay/channel/ai360"
|
||||
"one-api/relay/channel/lingyiwanwu"
|
||||
"one-api/relay/channel/minimax"
|
||||
"one-api/relay/channel/moonshot"
|
||||
relaycommon "one-api/relay/common"
|
||||
"one-api/setting"
|
||||
"time"
|
||||
)
|
||||
|
||||
// https://platform.openai.com/docs/api-reference/models/list
|
||||
|
||||
@@ -6,9 +6,9 @@ import (
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"one-api/common"
|
||||
"one-api/constant"
|
||||
"one-api/model"
|
||||
"github.com/QuantumNous/new-api/common"
|
||||
"github.com/QuantumNous/new-api/constant"
|
||||
"github.com/QuantumNous/new-api/model"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
@@ -13,8 +13,8 @@ import (
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"one-api/common"
|
||||
"one-api/model"
|
||||
"github.com/QuantumNous/new-api/common"
|
||||
"github.com/QuantumNous/new-api/model"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"gorm.io/gorm"
|
||||
|
||||
@@ -6,13 +6,14 @@ import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"one-api/common"
|
||||
"one-api/model"
|
||||
"one-api/setting/system_setting"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/QuantumNous/new-api/common"
|
||||
"github.com/QuantumNous/new-api/model"
|
||||
"github.com/QuantumNous/new-api/setting/system_setting"
|
||||
|
||||
"github.com/gin-contrib/sessions"
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
@@ -4,14 +4,15 @@ import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"one-api/common"
|
||||
"one-api/model"
|
||||
"one-api/setting"
|
||||
"one-api/setting/console_setting"
|
||||
"one-api/setting/ratio_setting"
|
||||
"one-api/setting/system_setting"
|
||||
"strings"
|
||||
|
||||
"github.com/QuantumNous/new-api/common"
|
||||
"github.com/QuantumNous/new-api/model"
|
||||
"github.com/QuantumNous/new-api/setting"
|
||||
"github.com/QuantumNous/new-api/setting/console_setting"
|
||||
"github.com/QuantumNous/new-api/setting/ratio_setting"
|
||||
"github.com/QuantumNous/new-api/setting/system_setting"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
|
||||
@@ -7,10 +7,10 @@ import (
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"one-api/common"
|
||||
"one-api/model"
|
||||
passkeysvc "one-api/service/passkey"
|
||||
"one-api/setting/system_setting"
|
||||
"github.com/QuantumNous/new-api/common"
|
||||
"github.com/QuantumNous/new-api/model"
|
||||
passkeysvc "github.com/QuantumNous/new-api/service/passkey"
|
||||
"github.com/QuantumNous/new-api/setting/system_setting"
|
||||
|
||||
"github.com/gin-contrib/sessions"
|
||||
"github.com/gin-gonic/gin"
|
||||
|
||||
@@ -3,13 +3,14 @@ package controller
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"one-api/common"
|
||||
"one-api/constant"
|
||||
"one-api/middleware"
|
||||
"one-api/model"
|
||||
"one-api/types"
|
||||
"time"
|
||||
|
||||
"github.com/QuantumNous/new-api/common"
|
||||
"github.com/QuantumNous/new-api/constant"
|
||||
"github.com/QuantumNous/new-api/middleware"
|
||||
"github.com/QuantumNous/new-api/model"
|
||||
"github.com/QuantumNous/new-api/types"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
|
||||
@@ -3,8 +3,8 @@ package controller
|
||||
import (
|
||||
"strconv"
|
||||
|
||||
"one-api/common"
|
||||
"one-api/model"
|
||||
"github.com/QuantumNous/new-api/common"
|
||||
"github.com/QuantumNous/new-api/model"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
package controller
|
||||
|
||||
import (
|
||||
"one-api/model"
|
||||
"one-api/setting"
|
||||
"one-api/setting/ratio_setting"
|
||||
"github.com/QuantumNous/new-api/model"
|
||||
"github.com/QuantumNous/new-api/setting"
|
||||
"github.com/QuantumNous/new-api/setting/ratio_setting"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
@@ -2,7 +2,8 @@ package controller
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"one-api/setting/ratio_setting"
|
||||
|
||||
"github.com/QuantumNous/new-api/setting/ratio_setting"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
@@ -7,14 +7,15 @@ import (
|
||||
"io"
|
||||
"net"
|
||||
"net/http"
|
||||
"one-api/logger"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"one-api/dto"
|
||||
"one-api/model"
|
||||
"one-api/setting/ratio_setting"
|
||||
"github.com/QuantumNous/new-api/logger"
|
||||
|
||||
"github.com/QuantumNous/new-api/dto"
|
||||
"github.com/QuantumNous/new-api/model"
|
||||
"github.com/QuantumNous/new-api/setting/ratio_setting"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
@@ -3,11 +3,12 @@ package controller
|
||||
import (
|
||||
"errors"
|
||||
"net/http"
|
||||
"one-api/common"
|
||||
"one-api/model"
|
||||
"strconv"
|
||||
"unicode/utf8"
|
||||
|
||||
"github.com/QuantumNous/new-api/common"
|
||||
"github.com/QuantumNous/new-api/model"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
|
||||
@@ -6,21 +6,22 @@ import (
|
||||
"io"
|
||||
"log"
|
||||
"net/http"
|
||||
"one-api/common"
|
||||
"one-api/constant"
|
||||
"one-api/dto"
|
||||
"one-api/logger"
|
||||
"one-api/middleware"
|
||||
"one-api/model"
|
||||
"one-api/relay"
|
||||
relaycommon "one-api/relay/common"
|
||||
relayconstant "one-api/relay/constant"
|
||||
"one-api/relay/helper"
|
||||
"one-api/service"
|
||||
"one-api/setting"
|
||||
"one-api/types"
|
||||
"strings"
|
||||
|
||||
"github.com/QuantumNous/new-api/common"
|
||||
"github.com/QuantumNous/new-api/constant"
|
||||
"github.com/QuantumNous/new-api/dto"
|
||||
"github.com/QuantumNous/new-api/logger"
|
||||
"github.com/QuantumNous/new-api/middleware"
|
||||
"github.com/QuantumNous/new-api/model"
|
||||
"github.com/QuantumNous/new-api/relay"
|
||||
relaycommon "github.com/QuantumNous/new-api/relay/common"
|
||||
relayconstant "github.com/QuantumNous/new-api/relay/constant"
|
||||
"github.com/QuantumNous/new-api/relay/helper"
|
||||
"github.com/QuantumNous/new-api/service"
|
||||
"github.com/QuantumNous/new-api/setting"
|
||||
"github.com/QuantumNous/new-api/types"
|
||||
|
||||
"github.com/bytedance/gopkg/util/gopool"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
@@ -139,9 +140,13 @@ func Relay(c *gin.Context, relayFormat types.RelayFormat) {
|
||||
|
||||
// common.SetContextKey(c, constant.ContextKeyTokenCountMeta, meta)
|
||||
|
||||
newAPIError = service.PreConsumeQuota(c, priceData.ShouldPreConsumedQuota, relayInfo)
|
||||
if newAPIError != nil {
|
||||
return
|
||||
if priceData.FreeModel {
|
||||
logger.LogInfo(c, fmt.Sprintf("模型 %s 免费,跳过预扣费", relayInfo.OriginModelName))
|
||||
} else {
|
||||
newAPIError = service.PreConsumeQuota(c, priceData.QuotaToPreConsume, relayInfo)
|
||||
if newAPIError != nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
defer func() {
|
||||
|
||||
@@ -3,12 +3,13 @@ package controller
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"one-api/common"
|
||||
"one-api/model"
|
||||
passkeysvc "one-api/service/passkey"
|
||||
"one-api/setting/system_setting"
|
||||
"time"
|
||||
|
||||
"github.com/QuantumNous/new-api/common"
|
||||
"github.com/QuantumNous/new-api/model"
|
||||
passkeysvc "github.com/QuantumNous/new-api/service/passkey"
|
||||
"github.com/QuantumNous/new-api/setting/system_setting"
|
||||
|
||||
"github.com/gin-contrib/sessions"
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
@@ -1,12 +1,13 @@
|
||||
package controller
|
||||
|
||||
import (
|
||||
"github.com/gin-gonic/gin"
|
||||
"one-api/common"
|
||||
"one-api/constant"
|
||||
"one-api/model"
|
||||
"one-api/setting/operation_setting"
|
||||
"time"
|
||||
|
||||
"github.com/QuantumNous/new-api/common"
|
||||
"github.com/QuantumNous/new-api/constant"
|
||||
"github.com/QuantumNous/new-api/model"
|
||||
"github.com/QuantumNous/new-api/setting/operation_setting"
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
type Setup struct {
|
||||
@@ -178,4 +179,4 @@ func boolToString(b bool) string {
|
||||
return "true"
|
||||
}
|
||||
return "false"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,16 +7,17 @@ import (
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"one-api/common"
|
||||
"one-api/constant"
|
||||
"one-api/dto"
|
||||
"one-api/logger"
|
||||
"one-api/model"
|
||||
"one-api/relay"
|
||||
"sort"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"github.com/QuantumNous/new-api/common"
|
||||
"github.com/QuantumNous/new-api/constant"
|
||||
"github.com/QuantumNous/new-api/dto"
|
||||
"github.com/QuantumNous/new-api/logger"
|
||||
"github.com/QuantumNous/new-api/model"
|
||||
"github.com/QuantumNous/new-api/relay"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/samber/lo"
|
||||
)
|
||||
|
||||
@@ -5,15 +5,17 @@ import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"one-api/common"
|
||||
"one-api/constant"
|
||||
"one-api/dto"
|
||||
"one-api/logger"
|
||||
"one-api/model"
|
||||
"one-api/relay"
|
||||
"one-api/relay/channel"
|
||||
relaycommon "one-api/relay/common"
|
||||
"time"
|
||||
|
||||
"github.com/QuantumNous/new-api/common"
|
||||
"github.com/QuantumNous/new-api/constant"
|
||||
"github.com/QuantumNous/new-api/dto"
|
||||
"github.com/QuantumNous/new-api/logger"
|
||||
"github.com/QuantumNous/new-api/model"
|
||||
"github.com/QuantumNous/new-api/relay"
|
||||
"github.com/QuantumNous/new-api/relay/channel"
|
||||
relaycommon "github.com/QuantumNous/new-api/relay/common"
|
||||
"github.com/QuantumNous/new-api/setting/ratio_setting"
|
||||
)
|
||||
|
||||
func UpdateVideoTaskAll(ctx context.Context, platform constant.TaskPlatform, taskChannelM map[int][]string, taskM map[string]*model.Task) error {
|
||||
@@ -46,6 +48,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()))
|
||||
@@ -91,6 +98,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 {
|
||||
@@ -120,7 +128,98 @@ func updateVideoSingleTask(ctx context.Context, adaptor channel.TaskAdaptor, cha
|
||||
if !(len(taskResult.Url) > 5 && taskResult.Url[:5] == "data:") {
|
||||
task.FailReason = taskResult.Url
|
||||
}
|
||||
|
||||
// 如果返回了 total_tokens 并且配置了模型倍率(非固定价格),则重新计费
|
||||
if taskResult.TotalTokens > 0 {
|
||||
// 获取模型名称
|
||||
var taskData map[string]interface{}
|
||||
if err := json.Unmarshal(task.Data, &taskData); err == nil {
|
||||
if modelName, ok := taskData["model"].(string); ok && modelName != "" {
|
||||
// 获取模型价格和倍率
|
||||
modelRatio, hasRatioSetting, _ := ratio_setting.GetModelRatio(modelName)
|
||||
// 只有配置了倍率(非固定价格)时才按 token 重新计费
|
||||
if hasRatioSetting && modelRatio > 0 {
|
||||
// 获取用户和组的倍率信息
|
||||
group := task.Group
|
||||
if group == "" {
|
||||
user, err := model.GetUserById(task.UserId, false)
|
||||
if err == nil {
|
||||
group = user.Group
|
||||
}
|
||||
}
|
||||
if group != "" {
|
||||
groupRatio := ratio_setting.GetGroupRatio(group)
|
||||
userGroupRatio, hasUserGroupRatio := ratio_setting.GetGroupGroupRatio(group, group)
|
||||
|
||||
var finalGroupRatio float64
|
||||
if hasUserGroupRatio {
|
||||
finalGroupRatio = userGroupRatio
|
||||
} else {
|
||||
finalGroupRatio = groupRatio
|
||||
}
|
||||
|
||||
// 计算实际应扣费额度: totalTokens * modelRatio * groupRatio
|
||||
actualQuota := int(float64(taskResult.TotalTokens) * modelRatio * finalGroupRatio)
|
||||
|
||||
// 计算差额
|
||||
preConsumedQuota := task.Quota
|
||||
quotaDelta := actualQuota - preConsumedQuota
|
||||
|
||||
if quotaDelta > 0 {
|
||||
// 需要补扣费
|
||||
logger.LogInfo(ctx, fmt.Sprintf("视频任务 %s 预扣费后补扣费:%s(实际消耗:%s,预扣费:%s,tokens:%d)",
|
||||
task.TaskID,
|
||||
logger.LogQuota(quotaDelta),
|
||||
logger.LogQuota(actualQuota),
|
||||
logger.LogQuota(preConsumedQuota),
|
||||
taskResult.TotalTokens,
|
||||
))
|
||||
if err := model.DecreaseUserQuota(task.UserId, quotaDelta); err != nil {
|
||||
logger.LogError(ctx, fmt.Sprintf("补扣费失败: %s", err.Error()))
|
||||
} else {
|
||||
model.UpdateUserUsedQuotaAndRequestCount(task.UserId, quotaDelta)
|
||||
model.UpdateChannelUsedQuota(task.ChannelId, quotaDelta)
|
||||
task.Quota = actualQuota // 更新任务记录的实际扣费额度
|
||||
|
||||
// 记录消费日志
|
||||
logContent := fmt.Sprintf("视频任务成功补扣费,模型倍率 %.2f,分组倍率 %.2f,tokens %d,预扣费 %s,实际扣费 %s,补扣费 %s",
|
||||
modelRatio, finalGroupRatio, taskResult.TotalTokens,
|
||||
logger.LogQuota(preConsumedQuota), logger.LogQuota(actualQuota), logger.LogQuota(quotaDelta))
|
||||
model.RecordLog(task.UserId, model.LogTypeSystem, logContent)
|
||||
}
|
||||
} else if quotaDelta < 0 {
|
||||
// 需要退还多扣的费用
|
||||
refundQuota := -quotaDelta
|
||||
logger.LogInfo(ctx, fmt.Sprintf("视频任务 %s 预扣费后返还:%s(实际消耗:%s,预扣费:%s,tokens:%d)",
|
||||
task.TaskID,
|
||||
logger.LogQuota(refundQuota),
|
||||
logger.LogQuota(actualQuota),
|
||||
logger.LogQuota(preConsumedQuota),
|
||||
taskResult.TotalTokens,
|
||||
))
|
||||
if err := model.IncreaseUserQuota(task.UserId, refundQuota, false); err != nil {
|
||||
logger.LogError(ctx, fmt.Sprintf("退还预扣费失败: %s", err.Error()))
|
||||
} else {
|
||||
task.Quota = actualQuota // 更新任务记录的实际扣费额度
|
||||
|
||||
// 记录退款日志
|
||||
logContent := fmt.Sprintf("视频任务成功退还多扣费用,模型倍率 %.2f,分组倍率 %.2f,tokens %d,预扣费 %s,实际扣费 %s,退还 %s",
|
||||
modelRatio, finalGroupRatio, taskResult.TotalTokens,
|
||||
logger.LogQuota(preConsumedQuota), logger.LogQuota(actualQuota), logger.LogQuota(refundQuota))
|
||||
model.RecordLog(task.UserId, model.LogTypeSystem, logContent)
|
||||
}
|
||||
} else {
|
||||
// quotaDelta == 0, 预扣费刚好准确
|
||||
logger.LogInfo(ctx, fmt.Sprintf("视频任务 %s 预扣费准确(%s,tokens:%d)",
|
||||
task.TaskID, logger.LogQuota(actualQuota), taskResult.TotalTokens))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
case model.TaskStatusFailure:
|
||||
preStatus := task.Status
|
||||
task.Status = model.TaskStatusFailure
|
||||
task.Progress = "100%"
|
||||
if task.FinishTime == 0 {
|
||||
@@ -129,12 +228,18 @@ func updateVideoSingleTask(ctx context.Context, adaptor channel.TaskAdaptor, cha
|
||||
task.FailReason = taskResult.Reason
|
||||
logger.LogInfo(ctx, fmt.Sprintf("Task %s failed: %s", task.TaskID, task.FailReason))
|
||||
quota := task.Quota
|
||||
taskResult.Progress = "100%"
|
||||
if quota != 0 {
|
||||
if err := model.IncreaseUserQuota(task.UserId, quota, false); err != nil {
|
||||
logger.LogError(ctx, "Failed to increase user quota: "+err.Error())
|
||||
if preStatus != model.TaskStatusFailure {
|
||||
// 任务失败且之前状态不是失败才退还额度,防止重复退还
|
||||
if err := model.IncreaseUserQuota(task.UserId, quota, false); err != nil {
|
||||
logger.LogWarn(ctx, "Failed to increase user quota: "+err.Error())
|
||||
}
|
||||
logContent := fmt.Sprintf("Video async task failed %s, refund %s", task.TaskID, logger.LogQuota(quota))
|
||||
model.RecordLog(task.UserId, model.LogTypeSystem, logContent)
|
||||
} else {
|
||||
logger.LogWarn(ctx, fmt.Sprintf("Task %s already in failure status, skip refund", task.TaskID))
|
||||
}
|
||||
logContent := fmt.Sprintf("Video async task failed %s, refund %s", task.TaskID, logger.LogQuota(quota))
|
||||
model.RecordLog(task.UserId, model.LogTypeSystem, logContent)
|
||||
}
|
||||
default:
|
||||
return fmt.Errorf("unknown task status %s for task %s", taskResult.Status, taskId)
|
||||
|
||||
@@ -6,10 +6,11 @@ import (
|
||||
"encoding/hex"
|
||||
"io"
|
||||
"net/http"
|
||||
"one-api/common"
|
||||
"one-api/model"
|
||||
"sort"
|
||||
|
||||
"github.com/QuantumNous/new-api/common"
|
||||
"github.com/QuantumNous/new-api/model"
|
||||
|
||||
"github.com/gin-contrib/sessions"
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
@@ -2,11 +2,12 @@ package controller
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"one-api/common"
|
||||
"one-api/model"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/QuantumNous/new-api/common"
|
||||
"github.com/QuantumNous/new-api/model"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
|
||||
@@ -4,17 +4,18 @@ import (
|
||||
"fmt"
|
||||
"log"
|
||||
"net/url"
|
||||
"one-api/common"
|
||||
"one-api/logger"
|
||||
"one-api/model"
|
||||
"one-api/service"
|
||||
"one-api/setting"
|
||||
"one-api/setting/operation_setting"
|
||||
"one-api/setting/system_setting"
|
||||
"strconv"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/QuantumNous/new-api/common"
|
||||
"github.com/QuantumNous/new-api/logger"
|
||||
"github.com/QuantumNous/new-api/model"
|
||||
"github.com/QuantumNous/new-api/service"
|
||||
"github.com/QuantumNous/new-api/setting"
|
||||
"github.com/QuantumNous/new-api/setting/operation_setting"
|
||||
"github.com/QuantumNous/new-api/setting/system_setting"
|
||||
|
||||
"github.com/Calcium-Ion/go-epay/epay"
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/samber/lo"
|
||||
@@ -86,8 +87,9 @@ func GetEpayClient() *epay.Client {
|
||||
|
||||
func getPayMoney(amount int64, group string) float64 {
|
||||
dAmount := decimal.NewFromInt(amount)
|
||||
|
||||
if !common.DisplayInCurrencyEnabled {
|
||||
// 充值金额以“展示类型”为准:
|
||||
// - USD/CNY: 前端传 amount 为金额单位;TOKENS: 前端传 tokens,需要换成 USD 金额
|
||||
if operation_setting.GetQuotaDisplayType() == operation_setting.QuotaDisplayTypeTokens {
|
||||
dQuotaPerUnit := decimal.NewFromFloat(common.QuotaPerUnit)
|
||||
dAmount = dAmount.Div(dQuotaPerUnit)
|
||||
}
|
||||
@@ -115,7 +117,7 @@ func getPayMoney(amount int64, group string) float64 {
|
||||
|
||||
func getMinTopup() int64 {
|
||||
minTopup := operation_setting.MinTopUp
|
||||
if !common.DisplayInCurrencyEnabled {
|
||||
if operation_setting.GetQuotaDisplayType() == operation_setting.QuotaDisplayTypeTokens {
|
||||
dMinTopup := decimal.NewFromInt(int64(minTopup))
|
||||
dQuotaPerUnit := decimal.NewFromFloat(common.QuotaPerUnit)
|
||||
minTopup = int(dMinTopup.Mul(dQuotaPerUnit).IntPart())
|
||||
@@ -176,18 +178,19 @@ func RequestEpay(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
amount := req.Amount
|
||||
if !common.DisplayInCurrencyEnabled {
|
||||
if operation_setting.GetQuotaDisplayType() == operation_setting.QuotaDisplayTypeTokens {
|
||||
dAmount := decimal.NewFromInt(int64(amount))
|
||||
dQuotaPerUnit := decimal.NewFromFloat(common.QuotaPerUnit)
|
||||
amount = dAmount.Div(dQuotaPerUnit).IntPart()
|
||||
}
|
||||
topUp := &model.TopUp{
|
||||
UserId: id,
|
||||
Amount: amount,
|
||||
Money: payMoney,
|
||||
TradeNo: tradeNo,
|
||||
CreateTime: time.Now().Unix(),
|
||||
Status: "pending",
|
||||
UserId: id,
|
||||
Amount: amount,
|
||||
Money: payMoney,
|
||||
TradeNo: tradeNo,
|
||||
PaymentMethod: req.PaymentMethod,
|
||||
CreateTime: time.Now().Unix(),
|
||||
Status: "pending",
|
||||
}
|
||||
err = topUp.Insert()
|
||||
if err != nil {
|
||||
@@ -235,8 +238,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 {
|
||||
@@ -312,3 +315,76 @@ func RequestAmount(c *gin.Context) {
|
||||
}
|
||||
c.JSON(200, gin.H{"message": "success", "data": strconv.FormatFloat(payMoney, 'f', 2, 64)})
|
||||
}
|
||||
|
||||
func GetUserTopUps(c *gin.Context) {
|
||||
userId := c.GetInt("id")
|
||||
pageInfo := common.GetPageQuery(c)
|
||||
keyword := c.Query("keyword")
|
||||
|
||||
var (
|
||||
topups []*model.TopUp
|
||||
total int64
|
||||
err error
|
||||
)
|
||||
if keyword != "" {
|
||||
topups, total, err = model.SearchUserTopUps(userId, keyword, pageInfo)
|
||||
} else {
|
||||
topups, total, err = model.GetUserTopUps(userId, pageInfo)
|
||||
}
|
||||
if err != nil {
|
||||
common.ApiError(c, err)
|
||||
return
|
||||
}
|
||||
|
||||
pageInfo.SetTotal(int(total))
|
||||
pageInfo.SetItems(topups)
|
||||
common.ApiSuccess(c, pageInfo)
|
||||
}
|
||||
|
||||
// GetAllTopUps 管理员获取全平台充值记录
|
||||
func GetAllTopUps(c *gin.Context) {
|
||||
pageInfo := common.GetPageQuery(c)
|
||||
keyword := c.Query("keyword")
|
||||
|
||||
var (
|
||||
topups []*model.TopUp
|
||||
total int64
|
||||
err error
|
||||
)
|
||||
if keyword != "" {
|
||||
topups, total, err = model.SearchAllTopUps(keyword, pageInfo)
|
||||
} else {
|
||||
topups, total, err = model.GetAllTopUps(pageInfo)
|
||||
}
|
||||
if err != nil {
|
||||
common.ApiError(c, err)
|
||||
return
|
||||
}
|
||||
|
||||
pageInfo.SetTotal(int(total))
|
||||
pageInfo.SetItems(topups)
|
||||
common.ApiSuccess(c, pageInfo)
|
||||
}
|
||||
|
||||
type AdminCompleteTopupRequest struct {
|
||||
TradeNo string `json:"trade_no"`
|
||||
}
|
||||
|
||||
// AdminCompleteTopUp 管理员补单接口
|
||||
func AdminCompleteTopUp(c *gin.Context) {
|
||||
var req AdminCompleteTopupRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil || req.TradeNo == "" {
|
||||
common.ApiErrorMsg(c, "参数错误")
|
||||
return
|
||||
}
|
||||
|
||||
// 订单级互斥,防止并发补单
|
||||
LockOrder(req.TradeNo)
|
||||
defer UnlockOrder(req.TradeNo)
|
||||
|
||||
if err := model.ManualCompleteTopUp(req.TradeNo); err != nil {
|
||||
common.ApiError(c, err)
|
||||
return
|
||||
}
|
||||
common.ApiSuccess(c, nil)
|
||||
}
|
||||
|
||||
@@ -5,15 +5,16 @@ import (
|
||||
"io"
|
||||
"log"
|
||||
"net/http"
|
||||
"one-api/common"
|
||||
"one-api/model"
|
||||
"one-api/setting"
|
||||
"one-api/setting/operation_setting"
|
||||
"one-api/setting/system_setting"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/QuantumNous/new-api/common"
|
||||
"github.com/QuantumNous/new-api/model"
|
||||
"github.com/QuantumNous/new-api/setting"
|
||||
"github.com/QuantumNous/new-api/setting/operation_setting"
|
||||
"github.com/QuantumNous/new-api/setting/system_setting"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/stripe/stripe-go/v81"
|
||||
"github.com/stripe/stripe-go/v81/checkout/session"
|
||||
@@ -83,12 +84,13 @@ func (*StripeAdaptor) RequestPay(c *gin.Context, req *StripePayRequest) {
|
||||
}
|
||||
|
||||
topUp := &model.TopUp{
|
||||
UserId: id,
|
||||
Amount: req.Amount,
|
||||
Money: chargedMoney,
|
||||
TradeNo: referenceId,
|
||||
CreateTime: time.Now().Unix(),
|
||||
Status: common.TopUpStatusPending,
|
||||
UserId: id,
|
||||
Amount: req.Amount,
|
||||
Money: chargedMoney,
|
||||
TradeNo: referenceId,
|
||||
PaymentMethod: PaymentMethodStripe,
|
||||
CreateTime: time.Now().Unix(),
|
||||
Status: common.TopUpStatusPending,
|
||||
}
|
||||
err = topUp.Insert()
|
||||
if err != nil {
|
||||
@@ -258,7 +260,7 @@ func GetChargedAmount(count float64, user model.User) float64 {
|
||||
|
||||
func getStripePayMoney(amount float64, group string) float64 {
|
||||
originalAmount := amount
|
||||
if !common.DisplayInCurrencyEnabled {
|
||||
if operation_setting.GetQuotaDisplayType() == operation_setting.QuotaDisplayTypeTokens {
|
||||
amount = amount / common.QuotaPerUnit
|
||||
}
|
||||
// Using float64 for monetary calculations is acceptable here due to the small amounts involved
|
||||
@@ -279,7 +281,7 @@ func getStripePayMoney(amount float64, group string) float64 {
|
||||
|
||||
func getStripeMinTopup() int64 {
|
||||
minTopup := setting.StripeMinTopUp
|
||||
if !common.DisplayInCurrencyEnabled {
|
||||
if operation_setting.GetQuotaDisplayType() == operation_setting.QuotaDisplayTypeTokens {
|
||||
minTopup = minTopup * int(common.QuotaPerUnit)
|
||||
}
|
||||
return int64(minTopup)
|
||||
|
||||
@@ -4,10 +4,11 @@ import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"one-api/common"
|
||||
"one-api/model"
|
||||
"strconv"
|
||||
|
||||
"github.com/QuantumNous/new-api/common"
|
||||
"github.com/QuantumNous/new-api/model"
|
||||
|
||||
"github.com/gin-contrib/sessions"
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
@@ -5,11 +5,12 @@ import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"net/http"
|
||||
"one-api/setting/console_setting"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/QuantumNous/new-api/setting/console_setting"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"golang.org/x/sync/errgroup"
|
||||
)
|
||||
|
||||
@@ -2,10 +2,11 @@ package controller
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"one-api/common"
|
||||
"one-api/model"
|
||||
"strconv"
|
||||
|
||||
"github.com/QuantumNous/new-api/common"
|
||||
"github.com/QuantumNous/new-api/model"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
|
||||
@@ -5,16 +5,17 @@ import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"one-api/common"
|
||||
"one-api/dto"
|
||||
"one-api/logger"
|
||||
"one-api/model"
|
||||
"one-api/setting"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
"one-api/constant"
|
||||
"github.com/QuantumNous/new-api/common"
|
||||
"github.com/QuantumNous/new-api/dto"
|
||||
"github.com/QuantumNous/new-api/logger"
|
||||
"github.com/QuantumNous/new-api/model"
|
||||
"github.com/QuantumNous/new-api/setting"
|
||||
|
||||
"github.com/QuantumNous/new-api/constant"
|
||||
|
||||
"github.com/gin-contrib/sessions"
|
||||
"github.com/gin-gonic/gin"
|
||||
@@ -1102,6 +1103,9 @@ type UpdateUserSettingRequest struct {
|
||||
WebhookSecret string `json:"webhook_secret,omitempty"`
|
||||
NotificationEmail string `json:"notification_email,omitempty"`
|
||||
BarkUrl string `json:"bark_url,omitempty"`
|
||||
GotifyUrl string `json:"gotify_url,omitempty"`
|
||||
GotifyToken string `json:"gotify_token,omitempty"`
|
||||
GotifyPriority int `json:"gotify_priority,omitempty"`
|
||||
AcceptUnsetModelRatioModel bool `json:"accept_unset_model_ratio_model"`
|
||||
RecordIpLog bool `json:"record_ip_log"`
|
||||
}
|
||||
@@ -1117,7 +1121,7 @@ func UpdateUserSetting(c *gin.Context) {
|
||||
}
|
||||
|
||||
// 验证预警类型
|
||||
if req.QuotaWarningType != dto.NotifyTypeEmail && req.QuotaWarningType != dto.NotifyTypeWebhook && req.QuotaWarningType != dto.NotifyTypeBark {
|
||||
if req.QuotaWarningType != dto.NotifyTypeEmail && req.QuotaWarningType != dto.NotifyTypeWebhook && req.QuotaWarningType != dto.NotifyTypeBark && req.QuotaWarningType != dto.NotifyTypeGotify {
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"success": false,
|
||||
"message": "无效的预警类型",
|
||||
@@ -1192,6 +1196,40 @@ func UpdateUserSetting(c *gin.Context) {
|
||||
}
|
||||
}
|
||||
|
||||
// 如果是Gotify类型,验证Gotify URL和Token
|
||||
if req.QuotaWarningType == dto.NotifyTypeGotify {
|
||||
if req.GotifyUrl == "" {
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"success": false,
|
||||
"message": "Gotify服务器地址不能为空",
|
||||
})
|
||||
return
|
||||
}
|
||||
if req.GotifyToken == "" {
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"success": false,
|
||||
"message": "Gotify令牌不能为空",
|
||||
})
|
||||
return
|
||||
}
|
||||
// 验证URL格式
|
||||
if _, err := url.ParseRequestURI(req.GotifyUrl); err != nil {
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"success": false,
|
||||
"message": "无效的Gotify服务器地址",
|
||||
})
|
||||
return
|
||||
}
|
||||
// 检查是否是HTTP或HTTPS
|
||||
if !strings.HasPrefix(req.GotifyUrl, "https://") && !strings.HasPrefix(req.GotifyUrl, "http://") {
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"success": false,
|
||||
"message": "Gotify服务器地址必须以http://或https://开头",
|
||||
})
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
userId := c.GetInt("id")
|
||||
user, err := model.GetUserById(userId, true)
|
||||
if err != nil {
|
||||
@@ -1225,6 +1263,18 @@ func UpdateUserSetting(c *gin.Context) {
|
||||
settings.BarkUrl = req.BarkUrl
|
||||
}
|
||||
|
||||
// 如果是Gotify类型,添加Gotify配置到设置中
|
||||
if req.QuotaWarningType == dto.NotifyTypeGotify {
|
||||
settings.GotifyUrl = req.GotifyUrl
|
||||
settings.GotifyToken = req.GotifyToken
|
||||
// Gotify优先级范围0-10,超出范围则使用默认值5
|
||||
if req.GotifyPriority < 0 || req.GotifyPriority > 10 {
|
||||
settings.GotifyPriority = 5
|
||||
} else {
|
||||
settings.GotifyPriority = req.GotifyPriority
|
||||
}
|
||||
}
|
||||
|
||||
// 更新用户设置
|
||||
user.SetSetting(settings)
|
||||
if err := user.Update(false); err != nil {
|
||||
|
||||
@@ -3,8 +3,8 @@ package controller
|
||||
import (
|
||||
"strconv"
|
||||
|
||||
"one-api/common"
|
||||
"one-api/model"
|
||||
"github.com/QuantumNous/new-api/common"
|
||||
"github.com/QuantumNous/new-api/model"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
130
controller/video_proxy.go
Normal file
130
controller/video_proxy.go
Normal file
@@ -0,0 +1,130 @@
|
||||
package controller
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"github.com/QuantumNous/new-api/logger"
|
||||
"github.com/QuantumNous/new-api/model"
|
||||
|
||||
"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()))
|
||||
}
|
||||
}
|
||||
@@ -5,11 +5,12 @@ import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"one-api/common"
|
||||
"one-api/model"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"github.com/QuantumNous/new-api/common"
|
||||
"github.com/QuantumNous/new-api/model"
|
||||
|
||||
"github.com/gin-contrib/sessions"
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
@@ -1,4 +1,18 @@
|
||||
version: '3.4'
|
||||
# New-API Docker Compose Configuration
|
||||
#
|
||||
# Quick Start:
|
||||
# 1. docker-compose up -d
|
||||
# 2. Access at http://localhost:3000
|
||||
#
|
||||
# Using MySQL instead of PostgreSQL:
|
||||
# 1. Comment out the postgres service and SQL_DSN line 15
|
||||
# 2. Uncomment the mysql service and SQL_DSN line 16
|
||||
# 3. Uncomment mysql in depends_on (line 28)
|
||||
# 4. Uncomment mysql_data in volumes section (line 64)
|
||||
#
|
||||
# ⚠️ IMPORTANT: Change all default passwords before deploying to production!
|
||||
|
||||
version: '3.4' # For compatibility with older Docker versions
|
||||
|
||||
services:
|
||||
new-api:
|
||||
@@ -12,21 +26,22 @@ services:
|
||||
- ./data:/data
|
||||
- ./logs:/app/logs
|
||||
environment:
|
||||
- SQL_DSN=root:123456@tcp(mysql:3306)/new-api # Point to the mysql service
|
||||
- SQL_DSN=postgresql://root:123456@postgres:5432/new-api # ⚠️ IMPORTANT: Change the password in production!
|
||||
# - SQL_DSN=root:123456@tcp(mysql:3306)/new-api # Point to the mysql service, uncomment if using MySQL
|
||||
- REDIS_CONN_STRING=redis://redis
|
||||
- TZ=Asia/Shanghai
|
||||
- ERROR_LOG_ENABLED=true # 是否启用错误日志记录
|
||||
# - STREAMING_TIMEOUT=300 # 流模式无响应超时时间,单位秒,默认120秒,如果出现空补全可以尝试改为更大值
|
||||
# - SESSION_SECRET=random_string # 多机部署时设置,必须修改这个随机字符串!!!!!!!
|
||||
# - NODE_TYPE=slave # Uncomment for slave node in multi-node deployment
|
||||
# - SYNC_FREQUENCY=60 # Uncomment if regular database syncing is needed
|
||||
# - FRONTEND_BASE_URL=https://openai.justsong.cn # Uncomment for multi-node deployment with front-end URL
|
||||
- BATCH_UPDATE_ENABLED=true # 是否启用批量更新 batch update enabled
|
||||
# - STREAMING_TIMEOUT=300 # 流模式无响应超时时间,单位秒,默认120秒,如果出现空补全可以尝试改为更大值 Streaming timeout in seconds, default is 120s. Increase if experiencing empty completions
|
||||
# - SESSION_SECRET=random_string # 多机部署时设置,必须修改这个随机字符串!! multi-node deployment, set this to a random string!!!!!!!
|
||||
# - SYNC_FREQUENCY=60 # Uncomment if regular database syncing is needed
|
||||
|
||||
depends_on:
|
||||
- redis
|
||||
- mysql
|
||||
- postgres
|
||||
# - mysql # Uncomment if using MySQL
|
||||
healthcheck:
|
||||
test: ["CMD-SHELL", "wget -q -O - http://localhost:3000/api/status | grep -o '\"success\":\\s*true' | awk -F: '{print $$2}'"]
|
||||
test: ["CMD-SHELL", "wget -q -O - http://localhost:3000/api/status | grep -o '\"success\":\\s*true' || exit 1"]
|
||||
interval: 30s
|
||||
timeout: 10s
|
||||
retries: 3
|
||||
@@ -36,17 +51,31 @@ services:
|
||||
container_name: redis
|
||||
restart: always
|
||||
|
||||
mysql:
|
||||
image: mysql:8.2
|
||||
container_name: mysql
|
||||
postgres:
|
||||
image: postgres:15
|
||||
container_name: postgres
|
||||
restart: always
|
||||
environment:
|
||||
MYSQL_ROOT_PASSWORD: 123456 # Ensure this matches the password in SQL_DSN
|
||||
MYSQL_DATABASE: new-api
|
||||
POSTGRES_USER: root
|
||||
POSTGRES_PASSWORD: 123456 # ⚠️ IMPORTANT: Change this password in production!
|
||||
POSTGRES_DB: new-api
|
||||
volumes:
|
||||
- mysql_data:/var/lib/mysql
|
||||
# ports:
|
||||
# - "3306:3306" # If you want to access MySQL from outside Docker, uncomment
|
||||
- pg_data:/var/lib/postgresql/data
|
||||
# ports:
|
||||
# - "5432:5432" # Uncomment if you need to access PostgreSQL from outside Docker
|
||||
|
||||
# mysql:
|
||||
# image: mysql:8.2
|
||||
# container_name: mysql
|
||||
# restart: always
|
||||
# environment:
|
||||
# MYSQL_ROOT_PASSWORD: 123456 # ⚠️ IMPORTANT: Change this password in production!
|
||||
# MYSQL_DATABASE: new-api
|
||||
# volumes:
|
||||
# - mysql_data:/var/lib/mysql
|
||||
# ports:
|
||||
# - "3306:3306" # Uncomment if you need to access MySQL from outside Docker
|
||||
|
||||
volumes:
|
||||
mysql_data:
|
||||
pg_data:
|
||||
# mysql_data:
|
||||
|
||||
107
docs/translation-glossary.fr.md
Normal file
107
docs/translation-glossary.fr.md
Normal file
@@ -0,0 +1,107 @@
|
||||
# Glossaire Français (French Glossary)
|
||||
|
||||
Ce document fournit des traductions standards françaises pour la terminologie clé du projet afin d'assurer la cohérence et la précision des traductions.
|
||||
|
||||
This document provides standard French translations for key project terminology to ensure consistency and accuracy in translations.
|
||||
|
||||
## Concepts de Base (Core Concepts)
|
||||
|
||||
- L'utilisation d'émojis dans les traductions est autorisée s'ils sont présents dans l'original
|
||||
- L'utilisation de termes purement techniques est autorisée s'ils sont présents dans l'original
|
||||
- L'utilisation de termes techniques en anglais est autorisée s'ils sont largement utilisés dans l'environnement technique francophone (par exemple, API)
|
||||
|
||||
| Chinois | Français | Anglais | Description |
|
||||
|---------|----------|---------|-------------|
|
||||
| 倍率 | Ratio | Ratio/Multiplier | Multiplicateur utilisé pour le calcul des prix. **Important :** Dans le contexte des calculs de prix, toujours utiliser "Ratio" plutôt que "Multiplicateur" pour assurer la cohérence terminologique |
|
||||
| 令牌 | Jeton | Token | Identifiants d'accès API ou unités de texte traitées par les modèles |
|
||||
| 渠道 | Canal | Channel | Canal d'accès aux fournisseurs d'API |
|
||||
| 分组 | Groupe | Group | Classification des utilisateurs ou des jetons |
|
||||
| 额度 | Quota | Quota | Quota de services disponible pour l'utilisateur |
|
||||
|
||||
## Modèles (Model Related)
|
||||
|
||||
| Chinois | Français | Anglais | Description |
|
||||
|---------|----------|---------|-------------|
|
||||
| 提示 | Invite | Prompt | Contenu d'entrée du modèle |
|
||||
| 补全 | Complétion | Completion | Contenu de sortie du modèle. **Important :** Ne pas utiliser "Achèvement" ou "Finalisation" - uniquement "Complétion" pour correspondre à la terminologie technique |
|
||||
| 输入 | Entrée | Input/Prompt | Contenu envoyé au modèle |
|
||||
| 输出 | Sortie | Output/Completion | Contenu retourné par le modèle |
|
||||
| 模型倍率 | Ratio du modèle | Model Ratio | Ratio de tarification pour différents modèles |
|
||||
| 补全倍率 | Ratio de complétion | Completion Ratio | Ratio de tarification supplémentaire pour la sortie |
|
||||
| 固定价格 | Prix fixe | Price per call | Prix par appel |
|
||||
| 按量计费 | Paiement à l'utilisation | Pay-as-you-go | Tarification basée sur l'utilisation |
|
||||
| 按次计费 | Paiement par appel | Pay-per-view | Prix fixe par appel |
|
||||
|
||||
## Gestion des Utilisateurs (User Management)
|
||||
|
||||
| Chinois | Français | Anglais | Description |
|
||||
|---------|----------|---------|-------------|
|
||||
| 超级管理员 | Super-administrateur | Root User | Administrateur avec les privilèges les plus élevés |
|
||||
| 管理员 | Administrateur | Admin User | Administrateur système |
|
||||
| 普通用户 | Utilisateur normal | Normal User | Utilisateur avec privilèges standards |
|
||||
|
||||
## Recharge et Échange (Recharge & Redemption)
|
||||
|
||||
| Chinois | Français | Anglais | Description |
|
||||
|---------|----------|---------|-------------|
|
||||
| 充值 | Recharge | Top Up | Ajout de quota au compte |
|
||||
| 兑换码 | Code d'échange | Redemption Code | Code qui peut être échangé contre du quota |
|
||||
|
||||
## Gestion des Canaux (Channel Management)
|
||||
|
||||
| Chinois | Français | Anglais | Description |
|
||||
|---------|----------|---------|-------------|
|
||||
| 渠道 | Canal | Channel | Canal du fournisseur d'API |
|
||||
| API密钥 | Clé API | API Key | Clé d'accès API. **Important :** Utiliser "Clé API" au lieu de "Jeton API" pour plus de précision et conformément à la terminologie technique francophone établie. Le terme "Clé" reflète mieux la fonctionnalité d'accès aux ressources, tandis que "Jeton" est plus souvent associé aux unités de texte dans le contexte du traitement des modèles linguistiques. |
|
||||
| 优先级 | Priorité | Priority | Priorité de sélection du canal |
|
||||
| 权重 | Poids | Weight | Poids d'équilibrage de charge |
|
||||
| 代理 | Proxy | Proxy | Adresse du serveur proxy |
|
||||
| 模型重定向 | Redirection de modèle | Model Mapping | Remplacement du nom du modèle dans le corps de la requête |
|
||||
| 供应商 | Fournisseur | Provider/Vendor | Fournisseur de services ou d'API |
|
||||
|
||||
## Sécurité (Security Related)
|
||||
|
||||
| Chinois | Français | Anglais | Description |
|
||||
|---------|----------|---------|-------------|
|
||||
| 两步验证 | Authentification à deux facteurs | Two-Factor Authentication | Méthode de vérification de sécurité supplémentaire pour les comptes |
|
||||
| 2FA | 2FA | Two-Factor Authentication | Abréviation de l'authentification à deux facteurs |
|
||||
|
||||
## Recommandations de Traduction (Translation Guidelines)
|
||||
|
||||
### Variantes Contextuelles de Traduction
|
||||
|
||||
**Invite/Entrée (Prompt/Input)**
|
||||
|
||||
- **Invite** : Lors de l'interaction avec les LLM, dans l'interface utilisateur, lors de la description de l'interaction avec le modèle
|
||||
- **Entrée** : Dans la tarification, la documentation technique, la description du processus de traitement des données
|
||||
- **Règle** : S'il s'agit de l'expérience utilisateur et de l'interaction avec l'IA → "Invite", s'il s'agit du processus technique ou des calculs → "Entrée"
|
||||
|
||||
**Jeton (Token)**
|
||||
|
||||
- Jeton d'accès API (API Token)
|
||||
- Unité de texte traitée par le modèle (Text Token)
|
||||
- Jeton d'accès système (Access Token)
|
||||
|
||||
**Quota (Quota)**
|
||||
|
||||
- Quota de services disponible pour l'utilisateur
|
||||
- Parfois traduit comme "Crédit"
|
||||
|
||||
### Particularités de la Langue Française
|
||||
|
||||
- **Formes plurielles** : Nécessite une implémentation correcte des formes plurielles (_one, _other)
|
||||
- **Accords grammaticaux** : Attention aux accords grammaticaux dans les termes techniques
|
||||
- **Genre grammatical** : Accord du genre des termes techniques (par exemple, "modèle" - masculin, "canal" - masculin)
|
||||
|
||||
### Termes Standardisés
|
||||
|
||||
- **Complétion (Completion)** : Contenu de sortie du modèle
|
||||
- **Ratio (Ratio)** : Multiplicateur pour le calcul des prix
|
||||
- **Code d'échange (Redemption Code)** : Utilisé au lieu de "Code d'échange" pour plus de précision
|
||||
- **Fournisseur (Provider/Vendor)** : Organisation ou service fournissant des API ou des modèles d'IA
|
||||
|
||||
---
|
||||
|
||||
**Note pour les contributeurs :** Si vous trouvez des incohérences dans les traductions de terminologie ou si vous avez de meilleures suggestions de traduction pour le français, n'hésitez pas à créer une Issue ou une Pull Request.
|
||||
|
||||
**Contribution Note for French:** If you find any inconsistencies in terminology translations or have better translation suggestions for French, please feel free to submit an Issue or Pull Request.
|
||||
86
docs/translation-glossary.md
Normal file
86
docs/translation-glossary.md
Normal file
@@ -0,0 +1,86 @@
|
||||
# 翻译术语表 (Translation Glossary)
|
||||
|
||||
本文档为翻译贡献者提供项目中关键术语的标准翻译参考,以确保翻译的一致性和准确性。
|
||||
|
||||
This document provides standard translation references for key terminology in the project to ensure consistency and accuracy for translation contributors.
|
||||
|
||||
## 核心概念 (Core Concepts)
|
||||
|
||||
| 中文 | English | 说明 | Description |
|
||||
|------|---------|------|-------------|
|
||||
| 倍率 | Ratio | 用于计算价格的乘数因子 | Multiplier factor used for price calculation |
|
||||
| 令牌 | Token | API访问凭证,也指模型处理的文本单元 | API access credentials or text units processed by models |
|
||||
| 渠道 | Channel | API服务提供商的接入通道 | Access channel for API service providers |
|
||||
| 分组 | Group | 用户或令牌的分类,影响价格倍率 | Classification of users or tokens, affecting price ratios |
|
||||
| 额度 | Quota | 用户可用的服务额度 | Available service quota for users |
|
||||
|
||||
## 模型相关 (Model Related)
|
||||
|
||||
| 中文 | English | 说明 | Description |
|
||||
|------|---------|------|-------------|
|
||||
| 提示 | Prompt | 模型输入内容 | Model input content |
|
||||
| 补全 | Completion | 模型输出内容 | Model output content |
|
||||
| 输入 | Input/Prompt | 发送给模型的内容 | Content sent to the model |
|
||||
| 输出 | Output/Completion | 模型返回的内容 | Content returned by the model |
|
||||
| 模型倍率 | Model Ratio | 不同模型的计费倍率 | Billing ratio for different models |
|
||||
| 补全倍率 | Completion Ratio | 输出内容的额外计费倍率 | Additional billing ratio for output content |
|
||||
| 固定价格 | Price per call | 按次计费的价格 | Fixed price per call |
|
||||
| 按量计费 | Pay-as-you-go | 根据使用量计费 | Billing based on usage |
|
||||
| 按次计费 | Pay-per-view | 每次调用固定价格 | Fixed price per invocation |
|
||||
|
||||
## 用户管理 (User Management)
|
||||
|
||||
| 中文 | English | 说明 | Description |
|
||||
|------|---------|------|-------------|
|
||||
| 超级管理员 | Root User | 最高权限管理员 | Administrator with highest privileges |
|
||||
| 管理员 | Admin User | 系统管理员 | System administrator |
|
||||
| 普通用户 | Normal User | 普通权限用户 | Regular user with standard privileges |
|
||||
|
||||
## 充值与兑换 (Recharge & Redemption)
|
||||
|
||||
| 中文 | English | 说明 | Description |
|
||||
|------|---------|------|-------------|
|
||||
| 充值 | Top Up | 为账户增加额度 | Add quota to account |
|
||||
| 兑换码 | Redemption Code | 可兑换额度的代码 | Code that can be redeemed for quota |
|
||||
|
||||
## 渠道管理 (Channel Management)
|
||||
|
||||
| 中文 | English | 说明 | Description |
|
||||
|------|---------|------|-------------|
|
||||
| 渠道 | Channel | API服务提供通道 | API service provider channel |
|
||||
| 密钥 | Key | API访问密钥 | API access key |
|
||||
| 优先级 | Priority | 渠道选择优先级 | Channel selection priority |
|
||||
| 权重 | Weight | 负载均衡权重 | Load balancing weight |
|
||||
| 代理 | Proxy | 代理服务器地址 | Proxy server address |
|
||||
| 模型重定向 | Model Mapping | 请求体中模型名称替换 | Model name replacement in request body |
|
||||
|
||||
## 安全相关 (Security Related)
|
||||
|
||||
| 中文 | English | 说明 | Description |
|
||||
|------|---------|------|-------------|
|
||||
| 两步验证 | Two-Factor Authentication | 为账户提供额外安全保护的验证方式 | Additional security verification method for accounts |
|
||||
| 2FA | Two-Factor Authentication | 两步验证的缩写 | Abbreviation for Two-Factor Authentication |
|
||||
|
||||
## 计费相关 (Billing Related)
|
||||
|
||||
| 中文 | English | 说明 | Description |
|
||||
|------|---------|------|-------------|
|
||||
| 倍率 | Ratio | 价格计算的乘数因子 | Multiplier factor used for price calculation |
|
||||
| 倍率 | Multiplier | 价格计算的乘数因子(同义词) | Multiplier factor used for price calculation (synonym) |
|
||||
|
||||
## 翻译注意事项 (Translation Guidelines)
|
||||
|
||||
- **提示 (Prompt)** = 模型输入内容 / Model input content
|
||||
- **补全 (Completion)** = 模型输出内容 / Model output content
|
||||
- **倍率 (Ratio)** = 价格计算的乘数因子 / Multiplier factor for price calculation
|
||||
- **额度 (Quota)** = 可用的用户服务额度,有时也翻译为 Credit / Available service quota for users, sometimes also translated as Credit
|
||||
- **Token** = 根据上下文可能指 / Depending on context, may refer to:
|
||||
- API访问令牌 (API Token)
|
||||
- 模型处理的文本单元 (Text Token)
|
||||
- 系统访问令牌 (Access Token)
|
||||
|
||||
---
|
||||
|
||||
**贡献说明**: 如发现术语翻译不一致或有更好的翻译建议,欢迎提交 Issue 或 Pull Request。
|
||||
|
||||
**Contribution Note**: If you find any inconsistencies in terminology translations or have better translation suggestions, please feel free to submit an Issue or Pull Request.
|
||||
107
docs/translation-glossary.ru.md
Normal file
107
docs/translation-glossary.ru.md
Normal file
@@ -0,0 +1,107 @@
|
||||
# Русский глоссарий (Russian Glossary)
|
||||
|
||||
Данный раздел предоставляет стандартные переводы ключевой терминологии проекта на русский язык для обеспечения согласованности и точности переводов.
|
||||
|
||||
This section provides standard Russian translations for key project terminology to ensure consistency and accuracy in translations.
|
||||
|
||||
## Основные концепции (Core Concepts)
|
||||
|
||||
- Допускается использовать символы Emoji в переводе, если они были в оригинале.
|
||||
- Допускается использование сугубо технических терминов, если они были в оригинале.
|
||||
- Допускается использование технических терминов на английском языке, если они широко используются в русскоязычной технической среде (например, API).
|
||||
|
||||
| Китайский | Русский | Английский | Описание |
|
||||
|-----------|--------|-----------|----------|
|
||||
| 倍率 | Коэффициент | Ratio/Multiplier | Множитель для расчета цены. **Важно:** В контексте расчетов цен всегда использовать "Коэффициент", а не "Множитель" для обеспечения консистентности терминологии |
|
||||
| 令牌 | Токен | Token | Учетные данные API или текстовые единицы |
|
||||
| 渠道 | Канал | Channel | Канал доступа к поставщику API |
|
||||
| 分组 | Группа | Group | Классификация пользователей или токенов |
|
||||
| 额度 | Квота | Quota | Доступная квота услуг для пользователя |
|
||||
|
||||
## Модели (Model Related)
|
||||
|
||||
| Китайский | Русский | Английский | Описание |
|
||||
|-----------|--------|-----------|----------|
|
||||
| 提示 | Промпт/Ввод | Prompt | Содержимое ввода в модель |
|
||||
| 补全 | Вывод | Completion | Содержимое вывода модели. **Важно:** Не использовать "Дополнение" или "Завершение" - только "Вывод" для соответствия технической терминологии |
|
||||
| 输入 | Ввод | Input/Prompt | Содержимое, отправляемое в модель |
|
||||
| 输出 | Вывод | Output/Completion | Содержимое, возвращаемое моделью |
|
||||
| 模型倍率 | Коэффициент модели | Model Ratio | Коэффициент тарификации для разных моделей |
|
||||
| 补全倍率 | Коэффициент вывода | Completion Ratio | Дополнительный коэффициент тарификации для вывода |
|
||||
| 固定价格 | Цена за запрос | Price per call | Цена за один вызов |
|
||||
| 按量计费 | Оплата по объему | Pay-as-you-go | Тарификация на основе использования |
|
||||
| 按次计费 | Оплата за запрос | Pay-per-view | Фиксированная цена за вызов |
|
||||
|
||||
## Управление пользователями (User Management)
|
||||
|
||||
| Китайский | Русский | Английский | Описание |
|
||||
|-----------|--------|-----------|----------|
|
||||
| 超级管理员 | Суперадминистратор | Root User | Администратор с наивысшими привилегиями |
|
||||
| 管理员 | Администратор | Admin User | Системный администратор |
|
||||
| 普通用户 | Обычный пользователь | Normal User | Пользователь со стандартными привилегиями |
|
||||
|
||||
## Пополнение и обмен (Recharge & Redemption)
|
||||
|
||||
| Китайский | Русский | Английский | Описание |
|
||||
|-----------|--------|-----------|----------|
|
||||
| 充值 | Пополнение | Top Up | Добавление квоты на аккаунт |
|
||||
| 兑换码 | Код купона | Redemption Code | Код, который можно обменять на квоту |
|
||||
|
||||
## Управление каналами (Channel Management)
|
||||
|
||||
| Китайский | Русский | Английский | Описание |
|
||||
|-----------|--------|-----------|----------|
|
||||
| 渠道 | Канал | Channel | Канал поставщика API |
|
||||
| API密钥 | API ключ | API Key | Ключ доступа к API. **Важно:** Использовать "API ключ" вместо "API токен" для большей точности и соответствия общепринятой русскоязычной технической терминологии. Термин "ключ" более точно отражает функционал доступа к ресурсам, в то время как "токен" чаще ассоциируется с текстовыми единицами в контексте обработки языковых моделей. |
|
||||
| 优先级 | Приоритет | Priority | Приоритет выбора канала |
|
||||
| 权重 | Вес | Weight | Вес балансировки нагрузки |
|
||||
| 代理 | Прокси | Proxy | Адрес прокси-сервера |
|
||||
| 模型重定向 | Перенаправление модели | Model Mapping | Замена имени модели в теле запроса |
|
||||
| 供应商 | Поставщик | Provider/Vendor | Поставщик услуг или API |
|
||||
|
||||
## Безопасность (Security Related)
|
||||
|
||||
| Китайский | Русский | Английский | Описание |
|
||||
|-----------|--------|-----------|----------|
|
||||
| 两步验证 | Двухфакторная аутентификация | Two-Factor Authentication | Дополнительный метод проверки безопасности для аккаунтов |
|
||||
| 2FA | 2FA | Two-Factor Authentication | Аббревиатура двухфакторной аутентификации |
|
||||
|
||||
## Рекомендации по переводу (Translation Guidelines)
|
||||
|
||||
### Контекстуальные варианты перевода
|
||||
|
||||
**Промпт/Ввод (Prompt/Input)**
|
||||
|
||||
- **Промпт**: При общении с LLM, в пользовательском интерфейсе, при описании взаимодействия с моделью
|
||||
- **Ввод**: При тарификации, технической документации, описании процесса обработки данных
|
||||
- **Правило**: Если речь о пользовательском опыте и взаимодействии с AI → "Промпт", если о техническом процессе или расчетах → "Ввод"
|
||||
|
||||
**Token**
|
||||
|
||||
- API токен доступа (API Token)
|
||||
- Текстовая единица, обрабатываемая моделью (Text Token)
|
||||
- Токен доступа к системе (Access Token)
|
||||
|
||||
**Квота (Quota)**
|
||||
|
||||
- Доступная квота услуг пользователя
|
||||
- Иногда переводится как "Кредит"
|
||||
|
||||
### Особенности русского языка
|
||||
|
||||
- **Множественные формы**: Требуется правильная реализация множественных форм (_one,_few, _many,_other)
|
||||
- **Падежные окончания**: Внимательное отношение к падежным окончаниям в технических терминах
|
||||
- **Грамматический род**: Согласование рода технических терминов (например, "модель" - женский род, "канал" - мужской род)
|
||||
|
||||
### Стандартизированные термины
|
||||
|
||||
- **Вывод (Completion)**: Содержимое вывода модели
|
||||
- **Коэффициент (Ratio)**: Множитель для расчета цены
|
||||
- **Код купона (Redemption Code)**: Используется вместо "Код обмена" для большей точности
|
||||
- **Поставщик (Provider/Vendor)**: Организация или сервис, предоставляющий API или AI-модели
|
||||
|
||||
---
|
||||
|
||||
**Примечание для участников:** При обнаружении несогласованности в переводах терминологии или наличии лучших предложений по переводу, не стесняйтесь создавать Issue или Pull Request.
|
||||
|
||||
**Contribution Note for Russian:** If you find any inconsistencies in terminology translations or have better translation suggestions for Russian, please feel free to submit an Issue or Pull Request.
|
||||
@@ -1,7 +1,7 @@
|
||||
package dto
|
||||
|
||||
import (
|
||||
"one-api/types"
|
||||
"github.com/QuantumNous/new-api/types"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
@@ -20,6 +20,9 @@ type ChannelOtherSettings struct {
|
||||
AzureResponsesVersion string `json:"azure_responses_version,omitempty"`
|
||||
VertexKeyType VertexKeyType `json:"vertex_key_type,omitempty"` // "json" or "api_key"
|
||||
OpenRouterEnterprise *bool `json:"openrouter_enterprise,omitempty"`
|
||||
AllowServiceTier bool `json:"allow_service_tier,omitempty"` // 是否允许 service_tier 透传(默认过滤以避免额外计费)
|
||||
DisableStore bool `json:"disable_store,omitempty"` // 是否禁用 store 透传(默认允许透传,禁用后可能导致 Codex 无法使用)
|
||||
AllowSafetyIdentifier bool `json:"allow_safety_identifier,omitempty"` // 是否允许 safety_identifier 透传(默认过滤以保护用户隐私)
|
||||
}
|
||||
|
||||
func (s *ChannelOtherSettings) IsOpenRouterEnterprise() bool {
|
||||
|
||||
@@ -3,10 +3,11 @@ package dto
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"one-api/common"
|
||||
"one-api/types"
|
||||
"strings"
|
||||
|
||||
"github.com/QuantumNous/new-api/common"
|
||||
"github.com/QuantumNous/new-api/types"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
@@ -195,12 +196,15 @@ type ClaudeRequest struct {
|
||||
Temperature *float64 `json:"temperature,omitempty"`
|
||||
TopP float64 `json:"top_p,omitempty"`
|
||||
TopK int `json:"top_k,omitempty"`
|
||||
//ClaudeMetadata `json:"metadata,omitempty"`
|
||||
Stream bool `json:"stream,omitempty"`
|
||||
Tools any `json:"tools,omitempty"`
|
||||
ContextManagement json.RawMessage `json:"context_management,omitempty"`
|
||||
ToolChoice any `json:"tool_choice,omitempty"`
|
||||
Thinking *Thinking `json:"thinking,omitempty"`
|
||||
McpServers json.RawMessage `json:"mcp_servers,omitempty"`
|
||||
Metadata json.RawMessage `json:"metadata,omitempty"`
|
||||
// 服务层级字段,用于指定 API 服务等级。允许透传可能导致实际计费高于预期,默认应过滤
|
||||
ServiceTier string `json:"service_tier,omitempty"`
|
||||
}
|
||||
|
||||
func (c *ClaudeRequest) GetTokenCountMeta() *types.TokenCountMeta {
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
package dto
|
||||
|
||||
import (
|
||||
"one-api/types"
|
||||
"strings"
|
||||
|
||||
"github.com/QuantumNous/new-api/types"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
package dto
|
||||
|
||||
import "one-api/types"
|
||||
import "github.com/QuantumNous/new-api/types"
|
||||
|
||||
type OpenAIError struct {
|
||||
Message string `json:"message"`
|
||||
|
||||
@@ -2,11 +2,13 @@ package dto
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"github.com/gin-gonic/gin"
|
||||
"one-api/common"
|
||||
"one-api/logger"
|
||||
"one-api/types"
|
||||
"strings"
|
||||
|
||||
"github.com/QuantumNous/new-api/common"
|
||||
"github.com/QuantumNous/new-api/logger"
|
||||
"github.com/QuantumNous/new-api/types"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
type GeminiChatRequest struct {
|
||||
@@ -273,6 +275,7 @@ type GeminiChatGenerationConfig struct {
|
||||
ResponseModalities []string `json:"responseModalities,omitempty"`
|
||||
ThinkingConfig *GeminiThinkingConfig `json:"thinkingConfig,omitempty"`
|
||||
SpeechConfig json.RawMessage `json:"speechConfig,omitempty"` // RawMessage to allow flexible speech config
|
||||
ImageConfig json.RawMessage `json:"imageConfig,omitempty"` // RawMessage to allow flexible image config
|
||||
}
|
||||
|
||||
type MediaResolution string
|
||||
@@ -291,12 +294,13 @@ type GeminiChatSafetyRating struct {
|
||||
|
||||
type GeminiChatPromptFeedback struct {
|
||||
SafetyRatings []GeminiChatSafetyRating `json:"safetyRatings"`
|
||||
BlockReason *string `json:"blockReason,omitempty"`
|
||||
}
|
||||
|
||||
type GeminiChatResponse struct {
|
||||
Candidates []GeminiChatCandidate `json:"candidates"`
|
||||
PromptFeedback GeminiChatPromptFeedback `json:"promptFeedback"`
|
||||
UsageMetadata GeminiUsageMetadata `json:"usageMetadata"`
|
||||
Candidates []GeminiChatCandidate `json:"candidates"`
|
||||
PromptFeedback *GeminiChatPromptFeedback `json:"promptFeedback,omitempty"`
|
||||
UsageMetadata GeminiUsageMetadata `json:"usageMetadata"`
|
||||
}
|
||||
|
||||
type GeminiUsageMetadata struct {
|
||||
@@ -326,6 +330,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 {
|
||||
|
||||
@@ -2,11 +2,12 @@ package dto
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"one-api/common"
|
||||
"one-api/types"
|
||||
"reflect"
|
||||
"strings"
|
||||
|
||||
"github.com/QuantumNous/new-api/common"
|
||||
"github.com/QuantumNous/new-api/types"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
@@ -74,14 +75,15 @@ func (r ImageRequest) MarshalJSON() ([]byte, error) {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// 不能合并ExtraFields!!!!!!!!
|
||||
// 合并 ExtraFields
|
||||
for k, v := range r.Extra {
|
||||
if _, exists := baseMap[k]; !exists {
|
||||
baseMap[k] = v
|
||||
}
|
||||
}
|
||||
//for k, v := range r.Extra {
|
||||
// if _, exists := baseMap[k]; !exists {
|
||||
// baseMap[k] = v
|
||||
// }
|
||||
//}
|
||||
|
||||
return json.Marshal(baseMap)
|
||||
return common.Marshal(baseMap)
|
||||
}
|
||||
|
||||
func GetJSONFieldNames(t reflect.Type) map[string]struct{} {
|
||||
|
||||
@@ -3,10 +3,11 @@ package dto
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"one-api/common"
|
||||
"one-api/types"
|
||||
"strings"
|
||||
|
||||
"github.com/QuantumNous/new-api/common"
|
||||
"github.com/QuantumNous/new-api/types"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
@@ -57,6 +58,18 @@ type GeneralOpenAIRequest struct {
|
||||
Dimensions int `json:"dimensions,omitempty"`
|
||||
Modalities json.RawMessage `json:"modalities,omitempty"`
|
||||
Audio json.RawMessage `json:"audio,omitempty"`
|
||||
// 安全标识符,用于帮助 OpenAI 检测可能违反使用政策的应用程序用户
|
||||
// 注意:此字段会向 OpenAI 发送用户标识信息,默认过滤以保护用户隐私
|
||||
SafetyIdentifier string `json:"safety_identifier,omitempty"`
|
||||
// Whether or not to store the output of this chat completion request for use in our model distillation or evals products.
|
||||
// 是否存储此次请求数据供 OpenAI 用于评估和优化产品
|
||||
// 注意:默认过滤此字段以保护用户隐私,但过滤后可能导致 Codex 无法正常使用
|
||||
Store json.RawMessage `json:"store,omitempty"`
|
||||
// Used by OpenAI to cache responses for similar requests to optimize your cache hit rates. Replaces the user field
|
||||
PromptCacheKey string `json:"prompt_cache_key,omitempty"`
|
||||
LogitBias json.RawMessage `json:"logit_bias,omitempty"`
|
||||
Metadata json.RawMessage `json:"metadata,omitempty"`
|
||||
Prediction json.RawMessage `json:"prediction,omitempty"`
|
||||
// gemini
|
||||
ExtraBody json.RawMessage `json:"extra_body,omitempty"`
|
||||
//xai
|
||||
@@ -75,6 +88,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 {
|
||||
@@ -775,19 +794,20 @@ type OpenAIResponsesRequest struct {
|
||||
ParallelToolCalls json.RawMessage `json:"parallel_tool_calls,omitempty"`
|
||||
PreviousResponseID string `json:"previous_response_id,omitempty"`
|
||||
Reasoning *Reasoning `json:"reasoning,omitempty"`
|
||||
ServiceTier string `json:"service_tier,omitempty"`
|
||||
Store json.RawMessage `json:"store,omitempty"`
|
||||
PromptCacheKey json.RawMessage `json:"prompt_cache_key,omitempty"`
|
||||
Stream bool `json:"stream,omitempty"`
|
||||
Temperature float64 `json:"temperature,omitempty"`
|
||||
Text json.RawMessage `json:"text,omitempty"`
|
||||
ToolChoice json.RawMessage `json:"tool_choice,omitempty"`
|
||||
Tools json.RawMessage `json:"tools,omitempty"` // 需要处理的参数很少,MCP 参数太多不确定,所以用 map
|
||||
TopP float64 `json:"top_p,omitempty"`
|
||||
Truncation string `json:"truncation,omitempty"`
|
||||
User string `json:"user,omitempty"`
|
||||
MaxToolCalls uint `json:"max_tool_calls,omitempty"`
|
||||
Prompt json.RawMessage `json:"prompt,omitempty"`
|
||||
// 服务层级字段,用于指定 API 服务等级。允许透传可能导致实际计费高于预期,默认应过滤
|
||||
ServiceTier string `json:"service_tier,omitempty"`
|
||||
Store json.RawMessage `json:"store,omitempty"`
|
||||
PromptCacheKey json.RawMessage `json:"prompt_cache_key,omitempty"`
|
||||
Stream bool `json:"stream,omitempty"`
|
||||
Temperature float64 `json:"temperature,omitempty"`
|
||||
Text json.RawMessage `json:"text,omitempty"`
|
||||
ToolChoice json.RawMessage `json:"tool_choice,omitempty"`
|
||||
Tools json.RawMessage `json:"tools,omitempty"` // 需要处理的参数很少,MCP 参数太多不确定,所以用 map
|
||||
TopP float64 `json:"top_p,omitempty"`
|
||||
Truncation string `json:"truncation,omitempty"`
|
||||
User string `json:"user,omitempty"`
|
||||
MaxToolCalls uint `json:"max_tool_calls,omitempty"`
|
||||
Prompt json.RawMessage `json:"prompt,omitempty"`
|
||||
}
|
||||
|
||||
func (r *OpenAIResponsesRequest) GetTokenCountMeta() *types.TokenCountMeta {
|
||||
|
||||
@@ -3,7 +3,8 @@ package dto
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"one-api/types"
|
||||
|
||||
"github.com/QuantumNous/new-api/types"
|
||||
)
|
||||
|
||||
const (
|
||||
@@ -233,6 +234,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:"-"`
|
||||
|
||||
52
dto/openai_video.go
Normal file
52
dto/openai_video.go
Normal file
@@ -0,0 +1,52 @@
|
||||
package dto
|
||||
|
||||
import (
|
||||
"strconv"
|
||||
"strings"
|
||||
)
|
||||
|
||||
const (
|
||||
VideoStatusUnknown = "unknown"
|
||||
VideoStatusQueued = "queued"
|
||||
VideoStatusInProgress = "in_progress"
|
||||
VideoStatusCompleted = "completed"
|
||||
VideoStatusFailed = "failed"
|
||||
)
|
||||
|
||||
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"` // Should use VideoStatus constants: VideoStatusQueued, VideoStatusInProgress, VideoStatusCompleted, VideoStatusFailed
|
||||
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"`
|
||||
}
|
||||
|
||||
func (m *OpenAIVideo) SetProgressStr(progress string) {
|
||||
progress = strings.TrimSuffix(progress, "%")
|
||||
m.Progress, _ = strconv.Atoi(progress)
|
||||
}
|
||||
func (m *OpenAIVideo) SetMetadata(k string, v any) {
|
||||
if m.Metadata == nil {
|
||||
m.Metadata = make(map[string]any)
|
||||
}
|
||||
m.Metadata[k] = v
|
||||
}
|
||||
func NewOpenAIVideo() *OpenAIVideo {
|
||||
return &OpenAIVideo{
|
||||
Object: "video",
|
||||
}
|
||||
}
|
||||
|
||||
type OpenAIVideoError struct {
|
||||
Message string `json:"message"`
|
||||
Code string `json:"code"`
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
package dto
|
||||
|
||||
import "one-api/constant"
|
||||
import "github.com/QuantumNous/new-api/constant"
|
||||
|
||||
// 这里不好动就不动了,本来想独立出来的(
|
||||
type OpenAIModels struct {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
package dto
|
||||
|
||||
import "one-api/types"
|
||||
import "github.com/QuantumNous/new-api/types"
|
||||
|
||||
const (
|
||||
RealtimeEventTypeError = "error"
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
package dto
|
||||
|
||||
import (
|
||||
"github.com/QuantumNous/new-api/types"
|
||||
"github.com/gin-gonic/gin"
|
||||
"one-api/types"
|
||||
)
|
||||
|
||||
type Request interface {
|
||||
|
||||
@@ -2,9 +2,10 @@ package dto
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/gin-gonic/gin"
|
||||
"one-api/types"
|
||||
"strings"
|
||||
|
||||
"github.com/QuantumNous/new-api/types"
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
type RerankRequest struct {
|
||||
|
||||
@@ -7,6 +7,9 @@ type UserSetting struct {
|
||||
WebhookSecret string `json:"webhook_secret,omitempty"` // WebhookSecret webhook密钥
|
||||
NotificationEmail string `json:"notification_email,omitempty"` // NotificationEmail 通知邮箱地址
|
||||
BarkUrl string `json:"bark_url,omitempty"` // BarkUrl Bark推送URL
|
||||
GotifyUrl string `json:"gotify_url,omitempty"` // GotifyUrl Gotify服务器地址
|
||||
GotifyToken string `json:"gotify_token,omitempty"` // GotifyToken Gotify应用令牌
|
||||
GotifyPriority int `json:"gotify_priority"` // GotifyPriority Gotify消息优先级
|
||||
AcceptUnsetRatioModel bool `json:"accept_unset_model_ratio_model,omitempty"` // AcceptUnsetRatioModel 是否接受未设置价格的模型
|
||||
RecordIpLog bool `json:"record_ip_log,omitempty"` // 是否记录请求和错误日志IP
|
||||
SidebarModules string `json:"sidebar_modules,omitempty"` // SidebarModules 左侧边栏模块配置
|
||||
@@ -16,4 +19,5 @@ var (
|
||||
NotifyTypeEmail = "email" // Email 邮件
|
||||
NotifyTypeWebhook = "webhook" // Webhook
|
||||
NotifyTypeBark = "bark" // Bark 推送
|
||||
NotifyTypeGotify = "gotify" // Gotify 推送
|
||||
)
|
||||
|
||||
73
electron/README.md
Normal file
73
electron/README.md
Normal file
@@ -0,0 +1,73 @@
|
||||
# New API Electron Desktop App
|
||||
|
||||
This directory contains the Electron wrapper for New API, providing a native desktop application with system tray support for Windows, macOS, and Linux.
|
||||
|
||||
## Prerequisites
|
||||
|
||||
### 1. Go Binary (Required)
|
||||
The Electron app requires the compiled Go binary to function. You have two options:
|
||||
|
||||
**Option A: Use existing binary (without Go installed)**
|
||||
```bash
|
||||
# If you have a pre-built binary (e.g., new-api-macos)
|
||||
cp ../new-api-macos ../new-api
|
||||
```
|
||||
|
||||
**Option B: Build from source (requires Go)**
|
||||
TODO
|
||||
|
||||
### 3. Electron Dependencies
|
||||
```bash
|
||||
cd electron
|
||||
npm install
|
||||
```
|
||||
|
||||
## Development
|
||||
|
||||
Run the app in development mode:
|
||||
```bash
|
||||
npm start
|
||||
```
|
||||
|
||||
This will:
|
||||
- Start the Go backend on port 3000
|
||||
- Open an Electron window with DevTools enabled
|
||||
- Create a system tray icon (menu bar on macOS)
|
||||
- Store database in `../data/new-api.db`
|
||||
|
||||
## Building for Production
|
||||
|
||||
### Quick Build
|
||||
```bash
|
||||
# Ensure Go binary exists in parent directory
|
||||
ls ../new-api # Should exist
|
||||
|
||||
# Build for current platform
|
||||
npm run build
|
||||
|
||||
# Platform-specific builds
|
||||
npm run build:mac # Creates .dmg and .zip
|
||||
npm run build:win # Creates .exe installer
|
||||
npm run build:linux # Creates .AppImage and .deb
|
||||
```
|
||||
|
||||
### Build Output
|
||||
- Built applications are in `electron/dist/`
|
||||
- macOS: `.dmg` (installer) and `.zip` (portable)
|
||||
- Windows: `.exe` (installer) and portable exe
|
||||
- Linux: `.AppImage` and `.deb`
|
||||
|
||||
## Configuration
|
||||
|
||||
### Port
|
||||
Default port is 3000. To change, edit `main.js`:
|
||||
```javascript
|
||||
const PORT = 3000; // Change to desired port
|
||||
```
|
||||
|
||||
### Database Location
|
||||
- **Development**: `../data/new-api.db` (project directory)
|
||||
- **Production**:
|
||||
- macOS: `~/Library/Application Support/New API/data/`
|
||||
- Windows: `%APPDATA%/New API/data/`
|
||||
- Linux: `~/.config/New API/data/`
|
||||
41
electron/build.sh
Executable file
41
electron/build.sh
Executable file
@@ -0,0 +1,41 @@
|
||||
#!/bin/bash
|
||||
|
||||
set -e
|
||||
|
||||
echo "Building New API Electron App..."
|
||||
|
||||
echo "Step 1: Building frontend..."
|
||||
cd ../web
|
||||
DISABLE_ESLINT_PLUGIN='true' bun run build
|
||||
cd ../electron
|
||||
|
||||
echo "Step 2: Building Go backend..."
|
||||
cd ..
|
||||
|
||||
if [[ "$OSTYPE" == "darwin"* ]]; then
|
||||
echo "Building for macOS..."
|
||||
CGO_ENABLED=1 go build -ldflags="-s -w" -o new-api
|
||||
cd electron
|
||||
npm install
|
||||
npm run build:mac
|
||||
elif [[ "$OSTYPE" == "linux-gnu"* ]]; then
|
||||
echo "Building for Linux..."
|
||||
CGO_ENABLED=1 go build -ldflags="-s -w" -o new-api
|
||||
cd electron
|
||||
npm install
|
||||
npm run build:linux
|
||||
elif [[ "$OSTYPE" == "msys" || "$OSTYPE" == "cygwin" || "$OSTYPE" == "win32" ]]; then
|
||||
echo "Building for Windows..."
|
||||
CGO_ENABLED=1 go build -ldflags="-s -w" -o new-api.exe
|
||||
cd electron
|
||||
npm install
|
||||
npm run build:win
|
||||
else
|
||||
echo "Unknown OS, building for current platform..."
|
||||
CGO_ENABLED=1 go build -ldflags="-s -w" -o new-api
|
||||
cd electron
|
||||
npm install
|
||||
npm run build
|
||||
fi
|
||||
|
||||
echo "Build complete! Check electron/dist/ for output."
|
||||
60
electron/create-tray-icon.js
Normal file
60
electron/create-tray-icon.js
Normal file
@@ -0,0 +1,60 @@
|
||||
// Create a simple tray icon for macOS
|
||||
// Run: node create-tray-icon.js
|
||||
|
||||
const fs = require('fs');
|
||||
const { createCanvas } = require('canvas');
|
||||
|
||||
function createTrayIcon() {
|
||||
// For macOS, we'll use a Template image (black and white)
|
||||
// Size should be 22x22 for Retina displays (@2x would be 44x44)
|
||||
const canvas = createCanvas(22, 22);
|
||||
const ctx = canvas.getContext('2d');
|
||||
|
||||
// Clear canvas
|
||||
ctx.clearRect(0, 0, 22, 22);
|
||||
|
||||
// Draw a simple "API" icon
|
||||
ctx.fillStyle = '#000000';
|
||||
ctx.font = 'bold 10px system-ui';
|
||||
ctx.textAlign = 'center';
|
||||
ctx.textBaseline = 'middle';
|
||||
ctx.fillText('API', 11, 11);
|
||||
|
||||
// Save as PNG
|
||||
const buffer = canvas.toBuffer('image/png');
|
||||
fs.writeFileSync('tray-icon.png', buffer);
|
||||
|
||||
// For Template images on macOS (will adapt to menu bar theme)
|
||||
fs.writeFileSync('tray-iconTemplate.png', buffer);
|
||||
fs.writeFileSync('tray-iconTemplate@2x.png', buffer);
|
||||
|
||||
console.log('Tray icon created successfully!');
|
||||
}
|
||||
|
||||
// Check if canvas is installed
|
||||
try {
|
||||
createTrayIcon();
|
||||
} catch (err) {
|
||||
console.log('Canvas module not installed.');
|
||||
console.log('For now, creating a placeholder. Install canvas with: npm install canvas');
|
||||
|
||||
// Create a minimal 1x1 transparent PNG as placeholder
|
||||
const minimalPNG = Buffer.from([
|
||||
0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A,
|
||||
0x00, 0x00, 0x00, 0x0D, 0x49, 0x48, 0x44, 0x52,
|
||||
0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x01,
|
||||
0x01, 0x03, 0x00, 0x00, 0x00, 0x25, 0xDB, 0x56,
|
||||
0xCA, 0x00, 0x00, 0x00, 0x03, 0x50, 0x4C, 0x54,
|
||||
0x45, 0x00, 0x00, 0x00, 0xA7, 0x7A, 0x3D, 0xDA,
|
||||
0x00, 0x00, 0x00, 0x01, 0x74, 0x52, 0x4E, 0x53,
|
||||
0x00, 0x40, 0xE6, 0xD8, 0x66, 0x00, 0x00, 0x00,
|
||||
0x0A, 0x49, 0x44, 0x41, 0x54, 0x08, 0x1D, 0x62,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00,
|
||||
0x00, 0x01, 0x0A, 0x2D, 0xCB, 0x59, 0x00, 0x00,
|
||||
0x00, 0x00, 0x49, 0x45, 0x4E, 0x44, 0xAE, 0x42,
|
||||
0x60, 0x82
|
||||
]);
|
||||
|
||||
fs.writeFileSync('tray-icon.png', minimalPNG);
|
||||
console.log('Created placeholder tray icon.');
|
||||
}
|
||||
18
electron/entitlements.mac.plist
Normal file
18
electron/entitlements.mac.plist
Normal file
@@ -0,0 +1,18 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>com.apple.security.cs.allow-unsigned-executable-memory</key>
|
||||
<true/>
|
||||
<key>com.apple.security.cs.allow-jit</key>
|
||||
<true/>
|
||||
<key>com.apple.security.cs.disable-library-validation</key>
|
||||
<true/>
|
||||
<key>com.apple.security.cs.allow-dyld-environment-variables</key>
|
||||
<true/>
|
||||
<key>com.apple.security.network.client</key>
|
||||
<true/>
|
||||
<key>com.apple.security.network.server</key>
|
||||
<true/>
|
||||
</dict>
|
||||
</plist>
|
||||
BIN
electron/icon.png
Normal file
BIN
electron/icon.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 30 KiB |
590
electron/main.js
Normal file
590
electron/main.js
Normal file
@@ -0,0 +1,590 @@
|
||||
const { app, BrowserWindow, dialog, Tray, Menu, shell } = require('electron');
|
||||
const { spawn } = require('child_process');
|
||||
const path = require('path');
|
||||
const http = require('http');
|
||||
const fs = require('fs');
|
||||
|
||||
let mainWindow;
|
||||
let serverProcess;
|
||||
let tray = null;
|
||||
let serverErrorLogs = [];
|
||||
const PORT = 3000;
|
||||
const DEV_FRONTEND_PORT = 5173; // Vite dev server port
|
||||
|
||||
// 保存日志到文件并打开
|
||||
function saveAndOpenErrorLog() {
|
||||
try {
|
||||
const timestamp = new Date().toISOString().replace(/[:.]/g, '-');
|
||||
const logFileName = `new-api-crash-${timestamp}.log`;
|
||||
const logDir = app.getPath('logs');
|
||||
const logFilePath = path.join(logDir, logFileName);
|
||||
|
||||
// 确保日志目录存在
|
||||
if (!fs.existsSync(logDir)) {
|
||||
fs.mkdirSync(logDir, { recursive: true });
|
||||
}
|
||||
|
||||
// 写入日志
|
||||
const logContent = `New API 崩溃日志
|
||||
生成时间: ${new Date().toLocaleString('zh-CN')}
|
||||
平台: ${process.platform}
|
||||
架构: ${process.arch}
|
||||
应用版本: ${app.getVersion()}
|
||||
|
||||
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
||||
|
||||
完整错误日志:
|
||||
|
||||
${serverErrorLogs.join('\n')}
|
||||
|
||||
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
||||
|
||||
日志文件位置: ${logFilePath}
|
||||
`;
|
||||
|
||||
fs.writeFileSync(logFilePath, logContent, 'utf8');
|
||||
|
||||
// 打开日志文件
|
||||
shell.openPath(logFilePath).then((error) => {
|
||||
if (error) {
|
||||
console.error('Failed to open log file:', error);
|
||||
// 如果打开文件失败,至少显示文件位置
|
||||
shell.showItemInFolder(logFilePath);
|
||||
}
|
||||
});
|
||||
|
||||
return logFilePath;
|
||||
} catch (err) {
|
||||
console.error('Failed to save error log:', err);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
// 分析错误日志,识别常见错误并提供解决方案
|
||||
function analyzeError(errorLogs) {
|
||||
const allLogs = errorLogs.join('\n');
|
||||
|
||||
// 检测端口占用错误
|
||||
if (allLogs.includes('failed to start HTTP server') ||
|
||||
allLogs.includes('bind: address already in use') ||
|
||||
allLogs.includes('listen tcp') && allLogs.includes('bind: address already in use')) {
|
||||
return {
|
||||
type: '端口被占用',
|
||||
title: '端口 ' + PORT + ' 被占用',
|
||||
message: '无法启动服务器,端口已被其他程序占用',
|
||||
solution: `可能的解决方案:\n\n1. 关闭占用端口 ${PORT} 的其他程序\n2. 检查是否已经运行了另一个 New API 实例\n3. 使用以下命令查找占用端口的进程:\n Mac/Linux: lsof -i :${PORT}\n Windows: netstat -ano | findstr :${PORT}\n4. 重启电脑以释放端口`
|
||||
};
|
||||
}
|
||||
|
||||
// 检测数据库错误
|
||||
if (allLogs.includes('database is locked') ||
|
||||
allLogs.includes('unable to open database')) {
|
||||
return {
|
||||
type: '数据文件被占用',
|
||||
title: '无法访问数据文件',
|
||||
message: '应用的数据文件正被其他程序占用',
|
||||
solution: '可能的解决方案:\n\n1. 检查是否已经打开了另一个 New API 窗口\n - 查看任务栏/Dock 中是否有其他 New API 图标\n - 查看系统托盘(Windows)或菜单栏(Mac)中是否有 New API 图标\n\n2. 如果刚刚关闭过应用,请等待 10 秒后再试\n\n3. 重启电脑以释放被占用的文件\n\n4. 如果问题持续,可以尝试:\n - 退出所有 New API 实例\n - 删除数据目录中的临时文件(.db-shm 和 .db-wal)\n - 重新启动应用'
|
||||
};
|
||||
}
|
||||
|
||||
// 检测权限错误
|
||||
if (allLogs.includes('permission denied') ||
|
||||
allLogs.includes('access denied')) {
|
||||
return {
|
||||
type: '权限错误',
|
||||
title: '权限不足',
|
||||
message: '程序没有足够的权限执行操作',
|
||||
solution: '可能的解决方案:\n\n1. 以管理员/root权限运行程序\n2. 检查数据目录的读写权限\n3. 检查可执行文件的权限\n4. 在 Mac 上,检查安全性与隐私设置'
|
||||
};
|
||||
}
|
||||
|
||||
// 检测网络错误
|
||||
if (allLogs.includes('network is unreachable') ||
|
||||
allLogs.includes('no such host') ||
|
||||
allLogs.includes('connection refused')) {
|
||||
return {
|
||||
type: '网络错误',
|
||||
title: '网络连接失败',
|
||||
message: '无法建立网络连接',
|
||||
solution: '可能的解决方案:\n\n1. 检查网络连接是否正常\n2. 检查防火墙设置\n3. 检查代理配置\n4. 确认目标服务器地址正确'
|
||||
};
|
||||
}
|
||||
|
||||
// 检测配置文件错误
|
||||
if (allLogs.includes('invalid configuration') ||
|
||||
allLogs.includes('failed to parse config') ||
|
||||
allLogs.includes('yaml') || allLogs.includes('json') && allLogs.includes('parse')) {
|
||||
return {
|
||||
type: '配置错误',
|
||||
title: '配置文件错误',
|
||||
message: '配置文件格式不正确或包含无效配置',
|
||||
solution: '可能的解决方案:\n\n1. 检查配置文件格式是否正确\n2. 恢复默认配置\n3. 删除配置文件让程序重新生成\n4. 查看文档了解正确的配置格式'
|
||||
};
|
||||
}
|
||||
|
||||
// 检测内存不足
|
||||
if (allLogs.includes('out of memory') ||
|
||||
allLogs.includes('cannot allocate memory')) {
|
||||
return {
|
||||
type: '内存不足',
|
||||
title: '系统内存不足',
|
||||
message: '程序运行时内存不足',
|
||||
solution: '可能的解决方案:\n\n1. 关闭其他占用内存的程序\n2. 增加系统可用内存\n3. 重启电脑释放内存\n4. 检查是否存在内存泄漏'
|
||||
};
|
||||
}
|
||||
|
||||
// 检测文件不存在错误
|
||||
if (allLogs.includes('no such file or directory') ||
|
||||
allLogs.includes('cannot find the file')) {
|
||||
return {
|
||||
type: '文件缺失',
|
||||
title: '找不到必需的文件',
|
||||
message: '缺少程序运行所需的文件',
|
||||
solution: '可能的解决方案:\n\n1. 重新安装应用程序\n2. 检查安装目录是否完整\n3. 确保所有依赖文件都存在\n4. 检查文件路径是否正确'
|
||||
};
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
function getBinaryPath() {
|
||||
const isDev = process.env.NODE_ENV === 'development';
|
||||
const platform = process.platform;
|
||||
|
||||
if (isDev) {
|
||||
const binaryName = platform === 'win32' ? 'new-api.exe' : 'new-api';
|
||||
return path.join(__dirname, '..', binaryName);
|
||||
}
|
||||
|
||||
let binaryName;
|
||||
switch (platform) {
|
||||
case 'win32':
|
||||
binaryName = 'new-api.exe';
|
||||
break;
|
||||
case 'darwin':
|
||||
binaryName = 'new-api';
|
||||
break;
|
||||
case 'linux':
|
||||
binaryName = 'new-api';
|
||||
break;
|
||||
default:
|
||||
binaryName = 'new-api';
|
||||
}
|
||||
|
||||
return path.join(process.resourcesPath, 'bin', binaryName);
|
||||
}
|
||||
|
||||
// Check if a server is available with retry logic
|
||||
function checkServerAvailability(port, maxRetries = 30, retryDelay = 1000) {
|
||||
return new Promise((resolve, reject) => {
|
||||
let currentAttempt = 0;
|
||||
|
||||
const tryConnect = () => {
|
||||
currentAttempt++;
|
||||
|
||||
if (currentAttempt % 5 === 1 && currentAttempt > 1) {
|
||||
console.log(`Attempting to connect to port ${port}... (attempt ${currentAttempt}/${maxRetries})`);
|
||||
}
|
||||
|
||||
const req = http.get({
|
||||
hostname: '127.0.0.1', // Use IPv4 explicitly instead of 'localhost' to avoid IPv6 issues
|
||||
port: port,
|
||||
timeout: 10000
|
||||
}, (res) => {
|
||||
// Server responded, connection successful
|
||||
req.destroy();
|
||||
console.log(`✓ Successfully connected to port ${port} (status: ${res.statusCode})`);
|
||||
resolve();
|
||||
});
|
||||
|
||||
req.on('error', (err) => {
|
||||
if (currentAttempt >= maxRetries) {
|
||||
reject(new Error(`Failed to connect to port ${port} after ${maxRetries} attempts: ${err.message}`));
|
||||
} else {
|
||||
setTimeout(tryConnect, retryDelay);
|
||||
}
|
||||
});
|
||||
|
||||
req.on('timeout', () => {
|
||||
req.destroy();
|
||||
if (currentAttempt >= maxRetries) {
|
||||
reject(new Error(`Connection timeout on port ${port} after ${maxRetries} attempts`));
|
||||
} else {
|
||||
setTimeout(tryConnect, retryDelay);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
tryConnect();
|
||||
});
|
||||
}
|
||||
|
||||
function startServer() {
|
||||
return new Promise((resolve, reject) => {
|
||||
const isDev = process.env.NODE_ENV === 'development';
|
||||
|
||||
const userDataPath = app.getPath('userData');
|
||||
const dataDir = path.join(userDataPath, 'data');
|
||||
|
||||
// 设置环境变量供 preload.js 使用
|
||||
process.env.ELECTRON_DATA_DIR = dataDir;
|
||||
|
||||
if (isDev) {
|
||||
// 开发模式:假设开发者手动启动了 Go 后端和前端开发服务器
|
||||
// 只需要等待前端开发服务器就绪
|
||||
console.log('Development mode: skipping server startup');
|
||||
console.log('Please make sure you have started:');
|
||||
console.log(' 1. Go backend: go run main.go (port 3000)');
|
||||
console.log(' 2. Frontend dev server: cd web && bun dev (port 5173)');
|
||||
console.log('');
|
||||
console.log('Checking if servers are running...');
|
||||
|
||||
// First check if both servers are accessible
|
||||
checkServerAvailability(DEV_FRONTEND_PORT)
|
||||
.then(() => {
|
||||
console.log('✓ Frontend dev server is accessible on port 5173');
|
||||
resolve();
|
||||
})
|
||||
.catch((err) => {
|
||||
console.error(`✗ Cannot connect to frontend dev server on port ${DEV_FRONTEND_PORT}`);
|
||||
console.error('Please make sure the frontend dev server is running:');
|
||||
console.error(' cd web && bun dev');
|
||||
reject(err);
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
// 生产模式:启动二进制服务器
|
||||
const env = { ...process.env, PORT: PORT.toString() };
|
||||
|
||||
if (!fs.existsSync(dataDir)) {
|
||||
fs.mkdirSync(dataDir, { recursive: true });
|
||||
}
|
||||
|
||||
env.SQLITE_PATH = path.join(dataDir, 'new-api.db');
|
||||
|
||||
console.log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
|
||||
console.log('📁 您的数据存储位置:');
|
||||
console.log(' ' + dataDir);
|
||||
console.log(' 💡 备份提示:复制此目录即可备份所有数据');
|
||||
console.log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
|
||||
|
||||
const binaryPath = getBinaryPath();
|
||||
const workingDir = process.resourcesPath;
|
||||
|
||||
console.log('Starting server from:', binaryPath);
|
||||
|
||||
serverProcess = spawn(binaryPath, [], {
|
||||
env,
|
||||
cwd: workingDir
|
||||
});
|
||||
|
||||
serverProcess.stdout.on('data', (data) => {
|
||||
console.log(`Server: ${data}`);
|
||||
});
|
||||
|
||||
serverProcess.stderr.on('data', (data) => {
|
||||
const errorMsg = data.toString();
|
||||
console.error(`Server Error: ${errorMsg}`);
|
||||
serverErrorLogs.push(errorMsg);
|
||||
// 只保留最近的100条错误日志
|
||||
if (serverErrorLogs.length > 100) {
|
||||
serverErrorLogs.shift();
|
||||
}
|
||||
});
|
||||
|
||||
serverProcess.on('error', (err) => {
|
||||
console.error('Failed to start server:', err);
|
||||
reject(err);
|
||||
});
|
||||
|
||||
serverProcess.on('close', (code) => {
|
||||
console.log(`Server process exited with code ${code}`);
|
||||
|
||||
// 如果退出代码不是0,说明服务器异常退出
|
||||
if (code !== 0 && code !== null) {
|
||||
const errorDetails = serverErrorLogs.length > 0
|
||||
? serverErrorLogs.slice(-20).join('\n')
|
||||
: '没有捕获到错误日志';
|
||||
|
||||
// 分析错误类型
|
||||
const knownError = analyzeError(serverErrorLogs);
|
||||
|
||||
let dialogOptions;
|
||||
if (knownError) {
|
||||
// 识别到已知错误,显示友好的错误信息和解决方案
|
||||
dialogOptions = {
|
||||
type: 'error',
|
||||
title: knownError.title,
|
||||
message: knownError.message,
|
||||
detail: `${knownError.solution}\n\n━━━━━━━━━━━━━━━━━━━━━━\n\n退出代码: ${code}\n\n错误类型: ${knownError.type}\n\n最近的错误日志:\n${errorDetails}`,
|
||||
buttons: ['退出应用', '查看完整日志'],
|
||||
defaultId: 0,
|
||||
cancelId: 0
|
||||
};
|
||||
} else {
|
||||
// 未识别的错误,显示通用错误信息
|
||||
dialogOptions = {
|
||||
type: 'error',
|
||||
title: '服务器崩溃',
|
||||
message: '服务器进程异常退出',
|
||||
detail: `退出代码: ${code}\n\n最近的错误信息:\n${errorDetails}`,
|
||||
buttons: ['退出应用', '查看完整日志'],
|
||||
defaultId: 0,
|
||||
cancelId: 0
|
||||
};
|
||||
}
|
||||
|
||||
dialog.showMessageBox(dialogOptions).then((result) => {
|
||||
if (result.response === 1) {
|
||||
// 用户选择查看详情,保存并打开日志文件
|
||||
const logPath = saveAndOpenErrorLog();
|
||||
|
||||
// 显示确认对话框
|
||||
const confirmMessage = logPath
|
||||
? `日志已保存到:\n${logPath}\n\n日志文件已在默认文本编辑器中打开。\n\n点击"退出"关闭应用程序。`
|
||||
: '日志保存失败,但已在控制台输出。\n\n点击"退出"关闭应用程序。';
|
||||
|
||||
dialog.showMessageBox({
|
||||
type: 'info',
|
||||
title: '日志已保存',
|
||||
message: confirmMessage,
|
||||
buttons: ['退出'],
|
||||
defaultId: 0
|
||||
}).then(() => {
|
||||
app.isQuitting = true;
|
||||
app.quit();
|
||||
});
|
||||
|
||||
// 同时在控制台输出
|
||||
console.log('=== 完整错误日志 ===');
|
||||
console.log(serverErrorLogs.join('\n'));
|
||||
} else {
|
||||
// 用户选择直接退出
|
||||
app.isQuitting = true;
|
||||
app.quit();
|
||||
}
|
||||
});
|
||||
} else {
|
||||
// 正常退出(code为0或null),直接关闭窗口
|
||||
if (mainWindow && !mainWindow.isDestroyed()) {
|
||||
mainWindow.close();
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
checkServerAvailability(PORT)
|
||||
.then(() => {
|
||||
console.log('✓ Backend server is accessible on port 3000');
|
||||
resolve();
|
||||
})
|
||||
.catch((err) => {
|
||||
console.error('✗ Failed to connect to backend server');
|
||||
reject(err);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function createWindow() {
|
||||
const isDev = process.env.NODE_ENV === 'development';
|
||||
const loadPort = isDev ? DEV_FRONTEND_PORT : PORT;
|
||||
|
||||
mainWindow = new BrowserWindow({
|
||||
width: 1080,
|
||||
height: 720,
|
||||
webPreferences: {
|
||||
preload: path.join(__dirname, 'preload.js'),
|
||||
nodeIntegration: false,
|
||||
contextIsolation: true
|
||||
},
|
||||
title: 'New API',
|
||||
icon: path.join(__dirname, 'icon.png')
|
||||
});
|
||||
|
||||
mainWindow.loadURL(`http://127.0.0.1:${loadPort}`);
|
||||
|
||||
console.log(`Loading from: http://127.0.0.1:${loadPort}`);
|
||||
|
||||
if (isDev) {
|
||||
mainWindow.webContents.openDevTools();
|
||||
}
|
||||
|
||||
// Close to tray instead of quitting
|
||||
mainWindow.on('close', (event) => {
|
||||
if (!app.isQuitting) {
|
||||
event.preventDefault();
|
||||
mainWindow.hide();
|
||||
if (process.platform === 'darwin') {
|
||||
app.dock.hide();
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
mainWindow.on('closed', () => {
|
||||
mainWindow = null;
|
||||
});
|
||||
}
|
||||
|
||||
function createTray() {
|
||||
// Use template icon for macOS (black with transparency, auto-adapts to theme)
|
||||
// Use colored icon for Windows
|
||||
const trayIconPath = process.platform === 'darwin'
|
||||
? path.join(__dirname, 'tray-iconTemplate.png')
|
||||
: path.join(__dirname, 'tray-icon-windows.png');
|
||||
|
||||
tray = new Tray(trayIconPath);
|
||||
|
||||
const contextMenu = Menu.buildFromTemplate([
|
||||
{
|
||||
label: 'Show New API',
|
||||
click: () => {
|
||||
if (mainWindow === null) {
|
||||
createWindow();
|
||||
} else {
|
||||
mainWindow.show();
|
||||
if (process.platform === 'darwin') {
|
||||
app.dock.show();
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
{ type: 'separator' },
|
||||
{
|
||||
label: 'Quit',
|
||||
click: () => {
|
||||
app.isQuitting = true;
|
||||
app.quit();
|
||||
}
|
||||
}
|
||||
]);
|
||||
|
||||
tray.setToolTip('New API');
|
||||
tray.setContextMenu(contextMenu);
|
||||
|
||||
// On macOS, clicking the tray icon shows the window
|
||||
tray.on('click', () => {
|
||||
if (mainWindow === null) {
|
||||
createWindow();
|
||||
} else {
|
||||
mainWindow.isVisible() ? mainWindow.hide() : mainWindow.show();
|
||||
if (mainWindow.isVisible() && process.platform === 'darwin') {
|
||||
app.dock.show();
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
app.whenReady().then(async () => {
|
||||
try {
|
||||
await startServer();
|
||||
createTray();
|
||||
createWindow();
|
||||
} catch (err) {
|
||||
console.error('Failed to start application:', err);
|
||||
|
||||
// 分析启动失败的错误
|
||||
const knownError = analyzeError(serverErrorLogs);
|
||||
|
||||
if (knownError) {
|
||||
dialog.showMessageBox({
|
||||
type: 'error',
|
||||
title: knownError.title,
|
||||
message: `启动失败: ${knownError.message}`,
|
||||
detail: `${knownError.solution}\n\n━━━━━━━━━━━━━━━━━━━━━━\n\n错误信息: ${err.message}\n\n错误类型: ${knownError.type}`,
|
||||
buttons: ['退出', '查看完整日志'],
|
||||
defaultId: 0,
|
||||
cancelId: 0
|
||||
}).then((result) => {
|
||||
if (result.response === 1) {
|
||||
// 用户选择查看日志
|
||||
const logPath = saveAndOpenErrorLog();
|
||||
|
||||
const confirmMessage = logPath
|
||||
? `日志已保存到:\n${logPath}\n\n日志文件已在默认文本编辑器中打开。\n\n点击"退出"关闭应用程序。`
|
||||
: '日志保存失败,但已在控制台输出。\n\n点击"退出"关闭应用程序。';
|
||||
|
||||
dialog.showMessageBox({
|
||||
type: 'info',
|
||||
title: '日志已保存',
|
||||
message: confirmMessage,
|
||||
buttons: ['退出'],
|
||||
defaultId: 0
|
||||
}).then(() => {
|
||||
app.quit();
|
||||
});
|
||||
|
||||
console.log('=== 完整错误日志 ===');
|
||||
console.log(serverErrorLogs.join('\n'));
|
||||
} else {
|
||||
app.quit();
|
||||
}
|
||||
});
|
||||
} else {
|
||||
dialog.showMessageBox({
|
||||
type: 'error',
|
||||
title: '启动失败',
|
||||
message: '无法启动服务器',
|
||||
detail: `错误信息: ${err.message}\n\n请检查日志获取更多信息。`,
|
||||
buttons: ['退出', '查看完整日志'],
|
||||
defaultId: 0,
|
||||
cancelId: 0
|
||||
}).then((result) => {
|
||||
if (result.response === 1) {
|
||||
// 用户选择查看日志
|
||||
const logPath = saveAndOpenErrorLog();
|
||||
|
||||
const confirmMessage = logPath
|
||||
? `日志已保存到:\n${logPath}\n\n日志文件已在默认文本编辑器中打开。\n\n点击"退出"关闭应用程序。`
|
||||
: '日志保存失败,但已在控制台输出。\n\n点击"退出"关闭应用程序。';
|
||||
|
||||
dialog.showMessageBox({
|
||||
type: 'info',
|
||||
title: '日志已保存',
|
||||
message: confirmMessage,
|
||||
buttons: ['退出'],
|
||||
defaultId: 0
|
||||
}).then(() => {
|
||||
app.quit();
|
||||
});
|
||||
|
||||
console.log('=== 完整错误日志 ===');
|
||||
console.log(serverErrorLogs.join('\n'));
|
||||
} else {
|
||||
app.quit();
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
app.on('window-all-closed', () => {
|
||||
// Don't quit when window is closed, keep running in tray
|
||||
// Only quit when explicitly choosing Quit from tray menu
|
||||
});
|
||||
|
||||
app.on('activate', () => {
|
||||
if (BrowserWindow.getAllWindows().length === 0) {
|
||||
createWindow();
|
||||
}
|
||||
});
|
||||
|
||||
app.on('before-quit', (event) => {
|
||||
if (serverProcess) {
|
||||
event.preventDefault();
|
||||
|
||||
console.log('Shutting down server...');
|
||||
serverProcess.kill('SIGTERM');
|
||||
|
||||
setTimeout(() => {
|
||||
if (serverProcess) {
|
||||
serverProcess.kill('SIGKILL');
|
||||
}
|
||||
app.exit();
|
||||
}, 5000);
|
||||
|
||||
serverProcess.on('close', () => {
|
||||
serverProcess = null;
|
||||
app.exit();
|
||||
});
|
||||
}
|
||||
});
|
||||
4117
electron/package-lock.json
generated
Normal file
4117
electron/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user