Commit dca9bac4 by Little Write

Merge branch 'main' into feat_subscribe_sp1

parents 6a2071cb c8c207b4
...@@ -5,4 +5,5 @@ ...@@ -5,4 +5,5 @@
.gitignore .gitignore
Makefile Makefile
docs docs
.eslintcache .eslintcache
\ No newline at end of file .gocache
\ No newline at end of file
---
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.
\ No newline at end of file
---
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 @@ ...@@ -13,7 +13,3 @@
### PR 描述 ### PR 描述
**请在下方详细描述您的 PR,包括目的、实现细节等。** **请在下方详细描述您的 PR,包括目的、实现细节等。**
### **重要提示**
**所有 PR 都必须提交到 `alpha` 分支。请确保您的 PR 目标分支是 `alpha`。**
...@@ -11,19 +11,42 @@ on: ...@@ -11,19 +11,42 @@ on:
required: false required: false
jobs: jobs:
push_to_registries: build_single_arch:
name: Push Docker image to multiple registries name: Build & push (${{ matrix.arch }}) [native]
runs-on: ubuntu-latest 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: permissions:
packages: write packages: write
contents: read contents: read
steps: steps:
- name: Check out the repo - name: Check out (shallow)
uses: actions/checkout@v4 uses: actions/checkout@v4
with:
fetch-depth: 1
- name: Save version info - name: Determine alpha version
id: version
run: | 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 - name: Log in to Docker Hub
uses: docker/login-action@v3 uses: docker/login-action@v3
...@@ -31,32 +54,98 @@ jobs: ...@@ -31,32 +54,98 @@ jobs:
username: ${{ secrets.DOCKERHUB_USERNAME }} username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }} password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Log in to the Container registry - name: Log in to GHCR
uses: docker/login-action@v3 uses: docker/login-action@v3
with: with:
registry: ghcr.io registry: ghcr.io
username: ${{ github.actor }} username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }} password: ${{ secrets.GITHUB_TOKEN }}
- name: Set up Docker Buildx - name: Extract metadata (labels)
uses: docker/setup-buildx-action@v3
- name: Extract metadata (tags, labels) for Docker
id: meta id: meta
uses: docker/metadata-action@v5 uses: docker/metadata-action@v5
with: with:
images: | images: |
calciumion/new-api calciumion/new-api
ghcr.io/${{ github.repository }} ghcr.io/${{ env.GHCR_REPOSITORY }}
tags: |
type=raw,value=alpha
type=raw,value=alpha-{{date 'YYYYMMDD'}}-{{sha}}
- name: Build and push Docker images - name: Build & push single-arch (to both registries)
uses: docker/build-push-action@v5 uses: docker/build-push-action@v6
with: with:
context: . context: .
platforms: linux/amd64,linux/arm64 platforms: ${{ matrix.platform }}
push: true 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 }} 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
name: Publish Docker image (Multi Registries) name: Publish Docker image (Multi Registries, native amd64+arm64)
on: on:
push: push:
tags: tags:
- '*' - '*'
jobs: jobs:
push_to_registries: build_single_arch:
name: Push Docker image to multiple registries name: Build & push (${{ matrix.arch }}) [native]
runs-on: ubuntu-latest 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: permissions:
packages: write packages: write
contents: read contents: read
steps: steps:
- name: Check out the repo - name: Check out (shallow)
uses: actions/checkout@v4 uses: actions/checkout@v4
with:
fetch-depth: 1
- name: Save version info - name: Resolve tag & write VERSION
run: | 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 # - name: Normalize GHCR repository
uses: docker/setup-qemu-action@v3 # run: echo "GHCR_REPOSITORY=${GITHUB_REPOSITORY,,}" >> $GITHUB_ENV
- name: Set up Docker Buildx - name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3 uses: docker/setup-buildx-action@v3
...@@ -31,26 +51,88 @@ jobs: ...@@ -31,26 +51,88 @@ jobs:
username: ${{ secrets.DOCKERHUB_USERNAME }} username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }} password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Log in to the Container registry # - name: Log in to GHCR
uses: docker/login-action@v3 # uses: docker/login-action@v3
with: # with:
registry: ghcr.io # registry: ghcr.io
username: ${{ github.actor }} # username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }} # password: ${{ secrets.GITHUB_TOKEN }}
- name: Extract metadata (tags, labels) for Docker - name: Extract metadata (labels)
id: meta id: meta
uses: docker/metadata-action@v5 uses: docker/metadata-action@v5
with: with:
images: | images: |
calciumion/new-api calciumion/new-api
ghcr.io/${{ github.repository }} # ghcr.io/${{ env.GHCR_REPOSITORY }}
- name: Build and push Docker images - name: Build & push single-arch (to both registries)
uses: docker/build-push-action@v5 uses: docker/build-push-action@v6
with: with:
context: . context: .
platforms: linux/amd64,linux/arm64 platforms: ${{ matrix.platform }}
push: true push: true
tags: ${{ steps.meta.outputs.tags }} tags: |
labels: ${{ steps.meta.outputs.labels }} calciumion/new-api:${{ env.TAG }}-${{ matrix.arch }}
\ No newline at end of file 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
name: Build Electron App
on:
push:
tags:
- '*' # Triggers on version tags like v1.0.0
- '!*-*' # Ignore pre-release tags like v1.0.0-beta
- '!*-alpha*' # Ignore alpha tags like v1.0.0-alpha
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 }}
\ No newline at end of file
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 }}
\ No newline at end of file
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 }}
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 }}
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 }}"
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 }}
\ No newline at end of file
...@@ -9,6 +9,11 @@ logs ...@@ -9,6 +9,11 @@ logs
web/dist web/dist
.env .env
one-api one-api
new-api
.DS_Store .DS_Store
tiktoken_cache tiktoken_cache
.eslintcache .eslintcache
\ No newline at end of file .gocache
electron/node_modules
electron/dist
\ No newline at end of file
...@@ -9,10 +9,12 @@ COPY ./VERSION . ...@@ -9,10 +9,12 @@ COPY ./VERSION .
RUN DISABLE_ESLINT_PLUGIN='true' VITE_REACT_APP_VERSION=$(cat VERSION) bun run build RUN DISABLE_ESLINT_PLUGIN='true' VITE_REACT_APP_VERSION=$(cat VERSION) bun run build
FROM golang:alpine AS builder2 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 WORKDIR /build
...@@ -21,7 +23,7 @@ RUN go mod download ...@@ -21,7 +23,7 @@ RUN go mod download
COPY . . COPY . .
COPY --from=builder /build/dist ./web/dist 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 FROM alpine
...@@ -29,7 +31,7 @@ RUN apk upgrade --no-cache \ ...@@ -29,7 +31,7 @@ RUN apk upgrade --no-cache \
&& apk add --no-cache ca-certificates tzdata ffmpeg \ && apk add --no-cache ca-certificates tzdata ffmpeg \
&& update-ca-certificates && update-ca-certificates
COPY --from=builder2 /build/one-api / COPY --from=builder2 /build/new-api /
EXPOSE 3000 EXPOSE 3000
WORKDIR /data WORKDIR /data
ENTRYPOINT ["/one-api"] ENTRYPOINT ["/new-api"]
<p align="right"> <p align="right">
<a href="./README.md">中文</a> | <strong>English</strong> <a href="./README.md">中文</a> | <strong>English</strong> | <a href="./README.fr.md">Français</a> | <a href="./README.ja.md">日本語</a>
</p> </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"> <div align="center">
![new-api](/web/public/logo.png) ![new-api](/web/public/logo.png)
...@@ -75,7 +79,7 @@ New API offers a wide range of features, please refer to [Features Introduction] ...@@ -75,7 +79,7 @@ New API offers a wide range of features, please refer to [Features Introduction]
1. 🎨 Brand new UI interface 1. 🎨 Brand new UI interface
2. 🌍 Multi-language support 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)) 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 5. 🔄 Compatible with the original One API database
6. 💵 Support for pay-per-use model pricing 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] ...@@ -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) 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) 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) 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) 13. ⚡ Support for **OpenAI Responses** format, [API Documentation](https://docs.newapi.pro/api/openai-responses)
14. Support for entering chat interface via /chat2link route 14. ⚡ Support for **Claude Messages** format, [API Documentation](https://docs.newapi.pro/api/anthropic-chat)
15. 🧠 Support for setting reasoning effort through model name suffixes: 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 1. OpenAI o-series models
- Add `-high` suffix for high reasoning effort (e.g.: `o3-mini-high`) - 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 `-medium` suffix for medium reasoning effort (e.g.: `o3-mini-medium`)
- Add `-low` suffix for low reasoning effort (e.g.: `o3-mini-low`) - Add `-low` suffix for low reasoning effort (e.g.: `o3-mini-low`)
2. Claude thinking models 2. Claude thinking models
- Add `-thinking` suffix to enable thinking mode (e.g.: `claude-3-7-sonnet-20250219-thinking`) - Add `-thinking` suffix to enable thinking mode (e.g.: `claude-3-7-sonnet-20250219-thinking`)
16. 🔄 Thinking-to-content functionality 17. 🔄 Thinking-to-content functionality
17. 🔄 Model rate limiting for users 18. 🔄 Model rate limiting for users
18. 💰 Cache billing support, which allows billing at a set ratio when cache is hit: 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` 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 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: 3. Supported channels:
...@@ -115,7 +124,9 @@ This version supports multiple models, please refer to [API Documentation-Relay ...@@ -115,7 +124,9 @@ This version supports multiple models, please refer to [API Documentation-Relay
4. Custom channels, supporting full call address input 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) 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) 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 ## Environment Variable Configuration
...@@ -124,14 +135,12 @@ For detailed configuration instructions, please refer to [Installation Guide-Env ...@@ -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` - `GENERATE_DEFAULT_TOKEN`: Whether to generate initial tokens for newly registered users, default is `false`
- `STREAMING_TIMEOUT`: Streaming response timeout, default is 300 seconds - `STREAMING_TIMEOUT`: Streaming response timeout, default is 300 seconds
- `DIFY_DEBUG`: Whether to output workflow and node information for Dify channels, default is `true` - `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`: 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` - `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` - `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` - `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` - `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` - `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 - `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` - `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 ...@@ -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 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 ### Cache Configuration Method
1. `REDIS_CONN_STRING`: Set Redis as cache 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 ...@@ -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): For detailed API documentation, please refer to [API Documentation](https://docs.newapi.pro/api):
- [Chat API](https://docs.newapi.pro/api/openai-chat) - [Chat API (Chat Completions)](https://docs.newapi.pro/api/openai-chat)
- [Image API](https://docs.newapi.pro/api/openai-image) - [Response API (Responses)](https://docs.newapi.pro/api/openai-responses)
- [Rerank API](https://docs.newapi.pro/api/jinaai-rerank) - [Image API (Image)](https://docs.newapi.pro/api/openai-image)
- [Realtime API](https://docs.newapi.pro/api/openai-realtime) - [Rerank API (Rerank)](https://docs.newapi.pro/api/jinaai-rerank)
- [Claude Chat API (messages)](https://docs.newapi.pro/api/anthropic-chat) - [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 ## Related Projects
- [One API](https://github.com/songquanpeng/one-api): Original project - [One API](https://github.com/songquanpeng/one-api): Original project
- [Midjourney-Proxy](https://github.com/novicezk/midjourney-proxy): Midjourney interface support - [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 - [neko-api-key-tool](https://github.com/Calcium-Ion/neko-api-key-tool): Query usage quota with key
Other projects based on New API: Other projects based on New API:
- [new-api-horizon](https://github.com/Calcium-Ion/new-api-horizon): High-performance optimized version of 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 ## Help and Support
......
This diff is collapsed. Click to expand it.
This diff is collapsed. Click to expand it.
<p align="right"> <p align="right">
<strong>中文</strong> | <a href="./README.en.md">English</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> </p>
<div align="center"> <div align="center">
...@@ -75,7 +75,7 @@ New API提供了丰富的功能,详细特性请参考[特性说明](https://do ...@@ -75,7 +75,7 @@ New API提供了丰富的功能,详细特性请参考[特性说明](https://do
1. 🎨 全新的UI界面 1. 🎨 全新的UI界面
2. 🌍 多语言支持 2. 🌍 多语言支持
3. 💰 支持在线充值功能(易支付) 3. 💰 支持在线充值功能,当前支持易支付和Stripe
4. 🔍 支持用key查询使用额度(配合[neko-api-key-tool](https://github.com/Calcium-Ion/neko-api-key-tool) 4. 🔍 支持用key查询使用额度(配合[neko-api-key-tool](https://github.com/Calcium-Ion/neko-api-key-tool)
5. 🔄 兼容原版One API的数据库 5. 🔄 兼容原版One API的数据库
6. 💵 支持模型按次数收费 6. 💵 支持模型按次数收费
...@@ -85,22 +85,23 @@ New API提供了丰富的功能,详细特性请参考[特性说明](https://do ...@@ -85,22 +85,23 @@ New API提供了丰富的功能,详细特性请参考[特性说明](https://do
10. 🤖 支持更多授权登陆方式(LinuxDO,Telegram、OIDC) 10. 🤖 支持更多授权登陆方式(LinuxDO,Telegram、OIDC)
11. 🔄 支持Rerank模型(Cohere和Jina),[接口文档](https://docs.newapi.pro/api/jinaai-rerank) 11. 🔄 支持Rerank模型(Cohere和Jina),[接口文档](https://docs.newapi.pro/api/jinaai-rerank)
12. ⚡ 支持OpenAI Realtime API(包括Azure渠道),[接口文档](https://docs.newapi.pro/api/openai-realtime) 12. ⚡ 支持OpenAI Realtime API(包括Azure渠道),[接口文档](https://docs.newapi.pro/api/openai-realtime)
13. ⚡ 支持Claude Messages 格式,[接口文档](https://docs.newapi.pro/api/anthropic-chat) 13. ⚡ 支持 **OpenAI Responses** 格式,[接口文档](https://docs.newapi.pro/api/openai-responses)
14. 支持使用路由/chat2link进入聊天界面 14. ⚡ 支持 **Claude Messages** 格式,[接口文档](https://docs.newapi.pro/api/anthropic-chat)
15. 🧠 支持通过模型名称后缀设置 reasoning effort: 15. ⚡ 支持 **Google Gemini** 格式,[接口文档](https://docs.newapi.pro/api/google-gemini-chat/)
16. 🧠 支持通过模型名称后缀设置 reasoning effort:
1. OpenAI o系列模型 1. OpenAI o系列模型
- 添加后缀 `-high` 设置为 high reasoning effort (例如: `o3-mini-high`) - 添加后缀 `-high` 设置为 high reasoning effort (例如: `o3-mini-high`)
- 添加后缀 `-medium` 设置为 medium reasoning effort (例如: `o3-mini-medium`) - 添加后缀 `-medium` 设置为 medium reasoning effort (例如: `o3-mini-medium`)
- 添加后缀 `-low` 设置为 low reasoning effort (例如: `o3-mini-low`) - 添加后缀 `-low` 设置为 low reasoning effort (例如: `o3-mini-low`)
2. Claude 思考模型 2. Claude 思考模型
- 添加后缀 `-thinking` 启用思考模式 (例如: `claude-3-7-sonnet-20250219-thinking`) - 添加后缀 `-thinking` 启用思考模式 (例如: `claude-3-7-sonnet-20250219-thinking`)
16. 🔄 思考转内容功能 17. 🔄 思考转内容功能
17. 🔄 针对用户的模型限流功能 18. 🔄 针对用户的模型限流功能
18. 🔄 请求格式转换功能,支持以下三种格式转换: 19. 🔄 请求格式转换功能,支持以下三种格式转换:
1. OpenAI Chat Completions => Claude Messages 1. OpenAI Chat Completions => Claude Messages (OpenAI格式调用Claude模型)
2. Clade Messages => OpenAI Chat Completions (可用于Claude Code调用第三方模型) 2. Clade Messages => OpenAI Chat Completions (可用于Claude Code调用第三方模型)
3. OpenAI Chat Completions => Gemini Chat 3. OpenAI Chat Completions => Gemini Chat (OpenAI格式调用Gemini模型)
19. 💰 缓存计费支持,开启后可以在缓存命中时按照设定的比例计费: 20. 💰 缓存计费支持,开启后可以在缓存命中时按照设定的比例计费:
1.`系统设置-运营设置` 中设置 `提示缓存倍率` 选项 1.`系统设置-运营设置` 中设置 `提示缓存倍率` 选项
2. 在渠道中设置 `提示缓存倍率`,范围 0-1,例如设置为 0.5 表示缓存命中时按照 50% 计费 2. 在渠道中设置 `提示缓存倍率`,范围 0-1,例如设置为 0.5 表示缓存命中时按照 50% 计费
3. 支持的渠道: 3. 支持的渠道:
...@@ -119,7 +120,9 @@ New API提供了丰富的功能,详细特性请参考[特性说明](https://do ...@@ -119,7 +120,9 @@ New API提供了丰富的功能,详细特性请参考[特性说明](https://do
4. 自定义渠道,支持填入完整调用地址 4. 自定义渠道,支持填入完整调用地址
5. Rerank模型([Cohere](https://cohere.ai/)[Jina](https://jina.ai/)),[接口文档](https://docs.newapi.pro/api/jinaai-rerank) 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) 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 ...@@ -128,16 +131,14 @@ New API提供了丰富的功能,详细特性请参考[特性说明](https://do
- `GENERATE_DEFAULT_TOKEN`:是否为新注册用户生成初始令牌,默认为 `false` - `GENERATE_DEFAULT_TOKEN`:是否为新注册用户生成初始令牌,默认为 `false`
- `STREAMING_TIMEOUT`:流式回复超时时间,默认300秒 - `STREAMING_TIMEOUT`:流式回复超时时间,默认300秒
- `DIFY_DEBUG`:Dify渠道是否输出工作流和节点信息,默认 `true` - `DIFY_DEBUG`:Dify渠道是否输出工作流和节点信息,默认 `true`
- `FORCE_STREAM_OPTION`:是否覆盖客户端stream_options参数,默认 `true`
- `GET_MEDIA_TOKEN`:是否统计图片token,默认 `true` - `GET_MEDIA_TOKEN`:是否统计图片token,默认 `true`
- `GET_MEDIA_TOKEN_NOT_STREAM`:非流情况下是否统计图片token,默认 `true` - `GET_MEDIA_TOKEN_NOT_STREAM`:非流情况下是否统计图片token,默认 `true`
- `UPDATE_TASK`:是否更新异步任务(Midjourney、Suno),默认 `true` - `UPDATE_TASK`:是否更新异步任务(Midjourney、Suno),默认 `true`
- `COHERE_SAFETY_SETTING`:Cohere模型安全设置,可选值为 `NONE`, `CONTEXTUAL`, `STRICT`,默认 `NONE`
- `GEMINI_VISION_MAX_IMAGE_NUM`:Gemini模型最大图片数量,默认 `16` - `GEMINI_VISION_MAX_IMAGE_NUM`:Gemini模型最大图片数量,默认 `16`
- `MAX_FILE_DOWNLOAD_MB`: 最大文件下载大小,单位MB,默认 `20` - `MAX_FILE_DOWNLOAD_MB`: 最大文件下载大小,单位MB,默认 `20`
- `CRYPTO_SECRET`:加密密钥,用于加密数据库内容 - `CRYPTO_SECRET`:加密密钥,用于加密Redis数据库内容
- `AZURE_DEFAULT_API_VERSION`:Azure渠道默认API版本,默认 `2025-04-01-preview` - `AZURE_DEFAULT_API_VERSION`:Azure渠道默认API版本,默认 `2025-04-01-preview`
- `NOTIFICATION_LIMIT_DURATION_MINUTE`:通知限制持续时间,默认 `10`分钟 - `NOTIFICATION_LIMIT_DURATION_MINUTE`邮件等通知限制持续时间,默认 `10`分钟
- `NOTIFY_LIMIT_COUNT`:用户通知在指定持续时间内的最大数量,默认 `2` - `NOTIFY_LIMIT_COUNT`:用户通知在指定持续时间内的最大数量,默认 `2`
- `ERROR_LOG_ENABLED=true`: 是否记录并显示错误日志,默认`false` - `ERROR_LOG_ENABLED=true`: 是否记录并显示错误日志,默认`false`
...@@ -164,12 +165,18 @@ New API提供了丰富的功能,详细特性请参考[特性说明](https://do ...@@ -164,12 +165,18 @@ New API提供了丰富的功能,详细特性请参考[特性说明](https://do
#### 使用Docker Compose部署(推荐) #### 使用Docker Compose部署(推荐)
```shell ```shell
# 下载项目 # 下载项目源码
git clone https://github.com/Calcium-Ion/new-api.git git clone https://github.com/QuantumNous/new-api.git
# 进入项目目录
cd new-api cd new-api
# 按需编辑docker-compose.yml
# 启动 # 根据需要编辑 docker-compose.yml 文件
docker-compose up -d # 使用nano编辑器
nano docker-compose.yml
# 或使用vim编辑器
# vim docker-compose.yml
``` ```
#### 直接使用Docker镜像 #### 直接使用Docker镜像
...@@ -182,7 +189,7 @@ docker run --name new-api -d --restart always -p 3000:3000 -e SQL_DSN="root:1234 ...@@ -182,7 +189,7 @@ docker run --name new-api -d --restart always -p 3000:3000 -e SQL_DSN="root:1234
``` ```
## 渠道重试与缓存 ## 渠道重试与缓存
渠道重试功能已经实现,可以在`设置->运营设置->通用设置`设置重试次数,**建议开启缓存**功能。 渠道重试功能已经实现,可以在`设置->运营设置->通用设置->失败重试次数`设置重试次数,**建议开启缓存**功能。
### 缓存设置方法 ### 缓存设置方法
1. `REDIS_CONN_STRING`:设置Redis作为缓存 1. `REDIS_CONN_STRING`:设置Redis作为缓存
...@@ -192,16 +199,17 @@ docker run --name new-api -d --restart always -p 3000:3000 -e SQL_DSN="root:1234 ...@@ -192,16 +199,17 @@ docker run --name new-api -d --restart always -p 3000:3000 -e SQL_DSN="root:1234
详细接口文档请参考[接口文档](https://docs.newapi.pro/api) 详细接口文档请参考[接口文档](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) - [图像接口(Image)](https://docs.newapi.pro/api/openai-image)
- [重排序接口(Rerank)](https://docs.newapi.pro/api/jinaai-rerank) - [重排序接口(Rerank)](https://docs.newapi.pro/api/jinaai-rerank)
- [实时对话接口(Realtime)](https://docs.newapi.pro/api/openai-realtime) - [实时对话接口(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):原版项目 - [One API](https://github.com/songquanpeng/one-api):原版项目
- [Midjourney-Proxy](https://github.com/novicezk/midjourney-proxy):Midjourney接口支持 - [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查询使用额度 - [neko-api-key-tool](https://github.com/Calcium-Ion/neko-api-key-tool):用key查询使用额度
其他基于New API的项目: 其他基于New API的项目:
......
package common package common
import "one-api/constant" import "github.com/QuantumNous/new-api/constant"
func ChannelType2APIType(channelType int) (int, bool) { func ChannelType2APIType(channelType int) (int, bool) {
apiType := -1 apiType := -1
...@@ -67,6 +67,10 @@ func ChannelType2APIType(channelType int) (int, bool) { ...@@ -67,6 +67,10 @@ func ChannelType2APIType(channelType int) (int, bool) {
apiType = constant.APITypeJimeng apiType = constant.APITypeJimeng
case constant.ChannelTypeMoonshot: case constant.ChannelTypeMoonshot:
apiType = constant.APITypeMoonshot apiType = constant.APITypeMoonshot
case constant.ChannelTypeSubmodel:
apiType = constant.APITypeSubmodel
case constant.ChannelTypeMiniMax:
apiType = constant.APITypeMiniMax
} }
if apiType == -1 { if apiType == -1 {
return constant.APITypeOpenAI, false return constant.APITypeOpenAI, false
......
...@@ -19,6 +19,7 @@ var TopUpLink = "" ...@@ -19,6 +19,7 @@ var TopUpLink = ""
// var ChatLink = "" // var ChatLink = ""
// var ChatLink2 = "" // var ChatLink2 = ""
var QuotaPerUnit = 500 * 1000.0 // $0.002 / 1K tokens var QuotaPerUnit = 500 * 1000.0 // $0.002 / 1K tokens
// 保留旧变量以兼容历史逻辑,实际展示由 general_setting.quota_display_type 控制
var DisplayInCurrencyEnabled = true var DisplayInCurrencyEnabled = true
var DisplayTokenStatEnabled = true var DisplayTokenStatEnabled = true
var DrawingEnabled = true var DrawingEnabled = true
......
...@@ -4,6 +4,7 @@ import ( ...@@ -4,6 +4,7 @@ import (
"crypto/hmac" "crypto/hmac"
"crypto/sha256" "crypto/sha256"
"encoding/hex" "encoding/hex"
"golang.org/x/crypto/bcrypt" "golang.org/x/crypto/bcrypt"
) )
......
...@@ -12,4 +12,4 @@ var LogSqlType = DatabaseTypeSQLite // Default to SQLite for logging SQL queries ...@@ -12,4 +12,4 @@ var LogSqlType = DatabaseTypeSQLite // Default to SQLite for logging SQL queries
var UsingMySQL = false var UsingMySQL = false
var UsingClickHouse = false var UsingClickHouse = false
var SQLitePath = "one-api.db?_busy_timeout=30000" var SQLitePath = "one-api.db?_busy_timeout=30000"
\ No newline at end of file
...@@ -86,5 +86,8 @@ func SendEmail(subject string, receiver string, content string) error { ...@@ -86,5 +86,8 @@ func SendEmail(subject string, receiver string, content string) error {
} else { } else {
err = smtp.SendMail(addr, auth, SMTPFrom, to, mail) err = smtp.SendMail(addr, auth, SMTPFrom, to, mail)
} }
if err != nil {
SysError(fmt.Sprintf("failed to send email to %s: %v", receiver, err))
}
return err return err
} }
...@@ -2,9 +2,10 @@ package common ...@@ -2,9 +2,10 @@ package common
import ( import (
"embed" "embed"
"github.com/gin-contrib/static"
"io/fs" "io/fs"
"net/http" "net/http"
"github.com/gin-contrib/static"
) )
// Credit: https://github.com/gin-contrib/static/issues/19 // Credit: https://github.com/gin-contrib/static/issues/19
......
package common package common
import "one-api/constant" import "github.com/QuantumNous/new-api/constant"
// EndpointInfo 描述单个端点的默认请求信息 // EndpointInfo 描述单个端点的默认请求信息
// path: 上游路径 // path: 上游路径
...@@ -23,6 +23,7 @@ var defaultEndpointInfoMap = map[constant.EndpointType]EndpointInfo{ ...@@ -23,6 +23,7 @@ var defaultEndpointInfoMap = map[constant.EndpointType]EndpointInfo{
constant.EndpointTypeGemini: {Path: "/v1beta/models/{model}:generateContent", Method: "POST"}, constant.EndpointTypeGemini: {Path: "/v1beta/models/{model}:generateContent", Method: "POST"},
constant.EndpointTypeJinaRerank: {Path: "/rerank", Method: "POST"}, constant.EndpointTypeJinaRerank: {Path: "/rerank", Method: "POST"},
constant.EndpointTypeImageGeneration: {Path: "/v1/images/generations", Method: "POST"}, constant.EndpointTypeImageGeneration: {Path: "/v1/images/generations", Method: "POST"},
constant.EndpointTypeEmbeddings: {Path: "/v1/embeddings", Method: "POST"},
} }
// GetDefaultEndpointInfo 返回指定端点类型的默认信息以及是否存在 // GetDefaultEndpointInfo 返回指定端点类型的默认信息以及是否存在
......
package common package common
import "one-api/constant" import "github.com/QuantumNous/new-api/constant"
// GetEndpointTypesByChannelType 获取渠道最优先端点类型(所有的渠道都支持 OpenAI 端点) // GetEndpointTypesByChannelType 获取渠道最优先端点类型(所有的渠道都支持 OpenAI 端点)
func GetEndpointTypesByChannelType(channelType int, modelName string) []constant.EndpointType { func GetEndpointTypesByChannelType(channelType int, modelName string) []constant.EndpointType {
...@@ -26,6 +26,8 @@ func GetEndpointTypesByChannelType(channelType int, modelName string) []constant ...@@ -26,6 +26,8 @@ func GetEndpointTypesByChannelType(channelType int, modelName string) []constant
endpointTypes = []constant.EndpointType{constant.EndpointTypeGemini, constant.EndpointTypeOpenAI} endpointTypes = []constant.EndpointType{constant.EndpointTypeGemini, constant.EndpointTypeOpenAI}
case constant.ChannelTypeOpenRouter: // OpenRouter 只支持 OpenAI 端点 case constant.ChannelTypeOpenRouter: // OpenRouter 只支持 OpenAI 端点
endpointTypes = []constant.EndpointType{constant.EndpointTypeOpenAI} endpointTypes = []constant.EndpointType{constant.EndpointTypeOpenAI}
case constant.ChannelTypeSora:
endpointTypes = []constant.EndpointType{constant.EndpointTypeOpenAIVideo}
default: default:
if IsOpenAIResponseOnlyModel(modelName) { if IsOpenAIResponseOnlyModel(modelName) {
endpointTypes = []constant.EndpointType{constant.EndpointTypeOpenAIResponse} endpointTypes = []constant.EndpointType{constant.EndpointTypeOpenAIResponse}
......
...@@ -3,11 +3,13 @@ package common ...@@ -3,11 +3,13 @@ package common
import ( import (
"bytes" "bytes"
"io" "io"
"mime/multipart"
"net/http" "net/http"
"one-api/constant"
"strings" "strings"
"time" "time"
"github.com/QuantumNous/new-api/constant"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
) )
...@@ -113,3 +115,26 @@ func ApiSuccess(c *gin.Context, data any) { ...@@ -113,3 +115,26 @@ func ApiSuccess(c *gin.Context, data any) {
"data": data, "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 ...@@ -3,8 +3,9 @@ package common
import ( import (
"context" "context"
"fmt" "fmt"
"github.com/bytedance/gopkg/util/gopool"
"math" "math"
"github.com/bytedance/gopkg/util/gopool"
) )
var relayGoPool gopool.Pool var relayGoPool gopool.Pool
......
...@@ -4,11 +4,13 @@ import ( ...@@ -4,11 +4,13 @@ import (
"flag" "flag"
"fmt" "fmt"
"log" "log"
"one-api/constant"
"os" "os"
"path/filepath" "path/filepath"
"strconv" "strconv"
"strings"
"time" "time"
"github.com/QuantumNous/new-api/constant"
) )
var ( var (
...@@ -19,10 +21,10 @@ var ( ...@@ -19,10 +21,10 @@ var (
) )
func printHelp() { func printHelp() {
fmt.Println("New API " + Version + " - All in one API service for OpenAI API.") fmt.Println("NewAPI(Based OneAPI) " + Version + " - The next-generation LLM gateway and AI asset management system supports multiple languages.")
fmt.Println("Copyright (C) 2023 JustSong. All rights reserved.") fmt.Println("Original Project: OneAPI by JustSong - https://github.com/songquanpeng/one-api")
fmt.Println("GitHub: https://github.com/songquanpeng/one-api") fmt.Println("Maintainer: QuantumNous - https://github.com/QuantumNous/new-api")
fmt.Println("Usage: one-api [--port <port>] [--log-dir <log directory>] [--version] [--help]") fmt.Println("Usage: newapi [--port <port>] [--log-dir <log directory>] [--version] [--help]")
} }
func InitEnv() { func InitEnv() {
...@@ -117,4 +119,17 @@ func initConstantEnv() { ...@@ -117,4 +119,17 @@ func initConstantEnv() {
constant.GenerateDefaultToken = GetEnvOrDefaultBool("GENERATE_DEFAULT_TOKEN", false) constant.GenerateDefaultToken = GetEnvOrDefaultBool("GENERATE_DEFAULT_TOKEN", false)
// 是否启用错误日志 // 是否启用错误日志
constant.ErrorLogEnabled = GetEnvOrDefaultBool("ERROR_LOG_ENABLED", false) constant.ErrorLogEnabled = GetEnvOrDefaultBool("ERROR_LOG_ENABLED", false)
soraPatchStr := GetEnvOrDefaultString("TASK_PRICE_PATCH", "")
if soraPatchStr != "" {
var taskPricePatches []string
soraPatches := strings.Split(soraPatchStr, ",")
for _, patch := range soraPatches {
trimmedPatch := strings.TrimSpace(patch)
if trimmedPatch != "" {
taskPricePatches = append(taskPricePatches, trimmedPatch)
}
}
constant.TaskPricePatches = taskPricePatches
}
} }
...@@ -3,6 +3,7 @@ package common ...@@ -3,6 +3,7 @@ package common
import ( import (
"bytes" "bytes"
"encoding/json" "encoding/json"
"io"
) )
func Unmarshal(data []byte, v any) error { func Unmarshal(data []byte, v any) error {
...@@ -13,7 +14,7 @@ func UnmarshalJsonStr(data string, v any) error { ...@@ -13,7 +14,7 @@ func UnmarshalJsonStr(data string, v any) error {
return json.Unmarshal(StringToByteSlice(data), v) return json.Unmarshal(StringToByteSlice(data), v)
} }
func DecodeJson(reader *bytes.Reader, v any) error { func DecodeJson(reader io.Reader, v any) error {
return json.NewDecoder(reader).Decode(v) return json.NewDecoder(reader).Decode(v)
} }
......
...@@ -4,9 +4,10 @@ import ( ...@@ -4,9 +4,10 @@ import (
"context" "context"
_ "embed" _ "embed"
"fmt" "fmt"
"github.com/go-redis/redis/v8"
"one-api/common"
"sync" "sync"
"github.com/QuantumNous/new-api/common"
"github.com/go-redis/redis/v8"
) )
//go:embed lua/rate_limit.lua //go:embed lua/rate_limit.lua
......
...@@ -2,10 +2,11 @@ package common ...@@ -2,10 +2,11 @@ package common
import ( import (
"fmt" "fmt"
"github.com/shirou/gopsutil/cpu"
"os" "os"
"runtime/pprof" "runtime/pprof"
"time" "time"
"github.com/shirou/gopsutil/cpu"
) )
// Monitor 定时监控cpu使用率,超过阈值输出pprof文件 // Monitor 定时监控cpu使用率,超过阈值输出pprof文件
......
package common package common
import ( import (
"github.com/google/uuid"
"strings" "strings"
"sync" "sync"
"time" "time"
"github.com/google/uuid"
) )
type verificationValue struct { type verificationValue struct {
......
...@@ -31,6 +31,8 @@ const ( ...@@ -31,6 +31,8 @@ const (
APITypeXai APITypeXai
APITypeCoze APITypeCoze
APITypeJimeng APITypeJimeng
APITypeMoonshot // this one is only for count, do not add any channel after this APITypeMoonshot
APITypeDummy // this one is only for count, do not add any channel after this APITypeSubmodel
APITypeMiniMax
APITypeDummy // this one is only for count, do not add any channel after this
) )
...@@ -50,6 +50,9 @@ const ( ...@@ -50,6 +50,9 @@ const (
ChannelTypeKling = 50 ChannelTypeKling = 50
ChannelTypeJimeng = 51 ChannelTypeJimeng = 51
ChannelTypeVidu = 52 ChannelTypeVidu = 52
ChannelTypeSubmodel = 53
ChannelTypeDoubaoVideo = 54
ChannelTypeSora = 55
ChannelTypeDummy // this one is only for count, do not add any channel after this ChannelTypeDummy // this one is only for count, do not add any channel after this
) )
...@@ -108,4 +111,69 @@ var ChannelBaseURLs = []string{ ...@@ -108,4 +111,69 @@ var ChannelBaseURLs = []string{
"https://api.klingai.com", //50 "https://api.klingai.com", //50
"https://visual.volcengineapi.com", //51 "https://visual.volcengineapi.com", //51
"https://api.vidu.cn", //52 "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,8 @@ const ( ...@@ -9,6 +9,8 @@ const (
EndpointTypeGemini EndpointType = "gemini" EndpointTypeGemini EndpointType = "gemini"
EndpointTypeJinaRerank EndpointType = "jina-rerank" EndpointTypeJinaRerank EndpointType = "jina-rerank"
EndpointTypeImageGeneration EndpointType = "image-generation" EndpointTypeImageGeneration EndpointType = "image-generation"
EndpointTypeEmbeddings EndpointType = "embeddings"
EndpointTypeOpenAIVideo EndpointType = "openai-video"
//EndpointTypeMidjourney EndpointType = "midjourney-proxy" //EndpointTypeMidjourney EndpointType = "midjourney-proxy"
//EndpointTypeSuno EndpointType = "suno-proxy" //EndpointTypeSuno EndpointType = "suno-proxy"
//EndpointTypeKling EndpointType = "kling" //EndpointTypeKling EndpointType = "kling"
......
...@@ -13,3 +13,6 @@ var NotifyLimitCount int ...@@ -13,3 +13,6 @@ var NotifyLimitCount int
var NotificationLimitDurationMinute int var NotificationLimitDurationMinute int
var GenerateDefaultToken bool var GenerateDefaultToken bool
var ErrorLogEnabled bool var ErrorLogEnabled bool
// temporary variable for sora patch, will be removed in future
var TaskPricePatches []string
package controller package controller
import ( 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" "github.com/gin-gonic/gin"
"one-api/common"
"one-api/dto"
"one-api/model"
) )
func GetSubscription(c *gin.Context) { func GetSubscription(c *gin.Context) {
...@@ -39,8 +40,18 @@ func GetSubscription(c *gin.Context) { ...@@ -39,8 +40,18 @@ func GetSubscription(c *gin.Context) {
} }
quota := remainQuota + usedQuota quota := remainQuota + usedQuota
amount := float64(quota) amount := float64(quota)
if common.DisplayInCurrencyEnabled { // OpenAI 兼容接口中的 *_USD 字段含义保持“额度单位”对应值:
amount /= common.QuotaPerUnit // 我们将其解释为以“站点展示类型”为准:
// - 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 { if token != nil && token.UnlimitedQuota {
amount = 100000000 amount = 100000000
...@@ -80,8 +91,13 @@ func GetUsage(c *gin.Context) { ...@@ -80,8 +91,13 @@ func GetUsage(c *gin.Context) {
return return
} }
amount := float64(quota) amount := float64(quota)
if common.DisplayInCurrencyEnabled { switch operation_setting.GetQuotaDisplayType() {
amount /= common.QuotaPerUnit case operation_setting.QuotaDisplayTypeCNY:
amount = amount / common.QuotaPerUnit * operation_setting.USDExchangeRate
case operation_setting.QuotaDisplayTypeTokens:
// tokens 保持原值
default:
amount = amount / common.QuotaPerUnit
} }
usage := OpenAIUsageResponse{ usage := OpenAIUsageResponse{
Object: "list", Object: "list",
......
...@@ -6,15 +6,16 @@ import ( ...@@ -6,15 +6,16 @@ import (
"fmt" "fmt"
"io" "io"
"net/http" "net/http"
"one-api/common"
"one-api/constant"
"one-api/model"
"one-api/service"
"one-api/setting/operation_setting"
"one-api/types"
"strconv" "strconv"
"time" "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/shopspring/decimal"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
...@@ -127,6 +128,14 @@ func GetAuthHeader(token string) http.Header { ...@@ -127,6 +128,14 @@ func GetAuthHeader(token string) http.Header {
return h 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) { func GetResponseBody(method, url string, channel *model.Channel, headers http.Header) ([]byte, error) {
req, err := http.NewRequest(method, url, nil) req, err := http.NewRequest(method, url, nil)
if err != nil { if err != nil {
......
...@@ -4,13 +4,15 @@ import ( ...@@ -4,13 +4,15 @@ import (
"encoding/json" "encoding/json"
"fmt" "fmt"
"net/http" "net/http"
"one-api/common"
"one-api/constant"
"one-api/dto"
"one-api/model"
"strconv" "strconv"
"strings" "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" "github.com/gin-gonic/gin"
) )
...@@ -197,9 +199,10 @@ func FetchUpstreamModels(c *gin.Context) { ...@@ -197,9 +199,10 @@ func FetchUpstreamModels(c *gin.Context) {
// 获取响应体 - 根据渠道类型决定是否添加 AuthHeader // 获取响应体 - 根据渠道类型决定是否添加 AuthHeader
var body []byte var body []byte
key := strings.Split(channel.Key, "\n")[0] key := strings.Split(channel.Key, "\n")[0]
if channel.Type == constant.ChannelTypeGemini { switch channel.Type {
body, err = GetResponseBody("GET", url, channel, GetAuthHeader(key)) // Use AuthHeader since Gemini now forces it case constant.ChannelTypeAnthropic:
} else { body, err = GetResponseBody("GET", url, channel, GetClaudeAuthHeader(key))
default:
body, err = GetResponseBody("GET", url, channel, GetAuthHeader(key)) body, err = GetResponseBody("GET", url, channel, GetAuthHeader(key))
} }
if err != nil { if err != nil {
...@@ -383,18 +386,9 @@ func GetChannel(c *gin.Context) { ...@@ -383,18 +386,9 @@ func GetChannel(c *gin.Context) {
return return
} }
// GetChannelKey 验证2FA后获取渠道密钥 // GetChannelKey 获取渠道密钥(需要通过安全验证中间件)
// 此函数依赖 SecureVerificationRequired 中间件,确保用户已通过安全验证
func GetChannelKey(c *gin.Context) { func GetChannelKey(c *gin.Context) {
type GetChannelKeyRequest struct {
Code string `json:"code" binding:"required"`
}
var req GetChannelKeyRequest
if err := c.ShouldBindJSON(&req); err != nil {
common.ApiError(c, fmt.Errorf("参数错误: %v", err))
return
}
userId := c.GetInt("id") userId := c.GetInt("id")
channelId, err := strconv.Atoi(c.Param("id")) channelId, err := strconv.Atoi(c.Param("id"))
if err != nil { if err != nil {
...@@ -402,24 +396,6 @@ func GetChannelKey(c *gin.Context) { ...@@ -402,24 +396,6 @@ func GetChannelKey(c *gin.Context) {
return return
} }
// 获取2FA记录并验证
twoFA, err := model.GetTwoFAByUserId(userId)
if err != nil {
common.ApiError(c, fmt.Errorf("获取2FA信息失败: %v", err))
return
}
if twoFA == nil || !twoFA.IsEnabled {
common.ApiError(c, fmt.Errorf("用户未启用2FA,无法查看密钥"))
return
}
// 统一的2FA验证逻辑
if !validateTwoFactorAuth(twoFA, req.Code) {
common.ApiError(c, fmt.Errorf("验证码或备用码错误,请重试"))
return
}
// 获取渠道信息(包含密钥) // 获取渠道信息(包含密钥)
channel, err := model.GetChannelById(channelId, true) channel, err := model.GetChannelById(channelId, true)
if err != nil { if err != nil {
...@@ -435,10 +411,10 @@ func GetChannelKey(c *gin.Context) { ...@@ -435,10 +411,10 @@ func GetChannelKey(c *gin.Context) {
// 记录操作日志 // 记录操作日志
model.RecordLog(userId, model.LogTypeSystem, fmt.Sprintf("查看渠道密钥信息 (渠道ID: %d)", channelId)) model.RecordLog(userId, model.LogTypeSystem, fmt.Sprintf("查看渠道密钥信息 (渠道ID: %d)", channelId))
// 统一的成功响应格式 // 返回渠道密钥
c.JSON(http.StatusOK, gin.H{ c.JSON(http.StatusOK, gin.H{
"success": true, "success": true,
"message": "验证成功", "message": "获取成功",
"data": map[string]interface{}{ "data": map[string]interface{}{
"key": channel.Key, "key": channel.Key,
}, },
...@@ -633,6 +609,7 @@ func AddChannel(c *gin.Context) { ...@@ -633,6 +609,7 @@ func AddChannel(c *gin.Context) {
common.ApiError(c, err) common.ApiError(c, err)
return return
} }
service.ResetProxyClientCache()
c.JSON(http.StatusOK, gin.H{ c.JSON(http.StatusOK, gin.H{
"success": true, "success": true,
"message": "", "message": "",
...@@ -894,6 +871,7 @@ func UpdateChannel(c *gin.Context) { ...@@ -894,6 +871,7 @@ func UpdateChannel(c *gin.Context) {
return return
} }
model.InitChannelCache() model.InitChannelCache()
service.ResetProxyClientCache()
channel.Key = "" channel.Key = ""
clearChannelInfo(&channel.Channel) clearChannelInfo(&channel.Channel)
c.JSON(http.StatusOK, gin.H{ c.JSON(http.StatusOK, gin.H{
......
...@@ -5,8 +5,9 @@ package controller ...@@ -5,8 +5,9 @@ package controller
import ( import (
"encoding/json" "encoding/json"
"net/http" "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" "github.com/gin-gonic/gin"
) )
......
...@@ -6,11 +6,12 @@ import ( ...@@ -6,11 +6,12 @@ import (
"errors" "errors"
"fmt" "fmt"
"net/http" "net/http"
"one-api/common"
"one-api/model"
"strconv" "strconv"
"time" "time"
"github.com/QuantumNous/new-api/common"
"github.com/QuantumNous/new-api/model"
"github.com/gin-contrib/sessions" "github.com/gin-contrib/sessions"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
) )
......
...@@ -2,9 +2,10 @@ package controller ...@@ -2,9 +2,10 @@ package controller
import ( import (
"net/http" "net/http"
"one-api/model"
"one-api/setting" "github.com/QuantumNous/new-api/model"
"one-api/setting/ratio_setting" "github.com/QuantumNous/new-api/setting"
"github.com/QuantumNous/new-api/setting/ratio_setting"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
) )
......
...@@ -7,12 +7,13 @@ import ( ...@@ -7,12 +7,13 @@ import (
"fmt" "fmt"
"net/http" "net/http"
"net/url" "net/url"
"one-api/common"
"one-api/model"
"strconv" "strconv"
"strings" "strings"
"time" "time"
"github.com/QuantumNous/new-api/common"
"github.com/QuantumNous/new-api/model"
"github.com/gin-contrib/sessions" "github.com/gin-contrib/sessions"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
) )
......
...@@ -2,10 +2,11 @@ package controller ...@@ -2,10 +2,11 @@ package controller
import ( import (
"net/http" "net/http"
"one-api/common"
"one-api/model"
"strconv" "strconv"
"github.com/QuantumNous/new-api/common"
"github.com/QuantumNous/new-api/model"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
) )
......
...@@ -7,15 +7,16 @@ import ( ...@@ -7,15 +7,16 @@ import (
"fmt" "fmt"
"io" "io"
"net/http" "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" "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" "github.com/gin-gonic/gin"
) )
......
...@@ -4,16 +4,17 @@ import ( ...@@ -4,16 +4,17 @@ import (
"encoding/json" "encoding/json"
"fmt" "fmt"
"net/http" "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" "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" "github.com/gin-gonic/gin"
) )
...@@ -42,6 +43,9 @@ func GetStatus(c *gin.Context) { ...@@ -42,6 +43,9 @@ func GetStatus(c *gin.Context) {
common.OptionMapRWMutex.RLock() common.OptionMapRWMutex.RLock()
defer common.OptionMapRWMutex.RUnlock() defer common.OptionMapRWMutex.RUnlock()
passkeySetting := system_setting.GetPasskeySettings()
legalSetting := system_setting.GetLegalSettings()
data := gin.H{ data := gin.H{
"version": common.Version, "version": common.Version,
"start_time": common.StartTime, "start_time": common.StartTime,
...@@ -64,18 +68,22 @@ func GetStatus(c *gin.Context) { ...@@ -64,18 +68,22 @@ func GetStatus(c *gin.Context) {
"top_up_link": common.TopUpLink, "top_up_link": common.TopUpLink,
"docs_link": operation_setting.GetGeneralSetting().DocsLink, "docs_link": operation_setting.GetGeneralSetting().DocsLink,
"quota_per_unit": common.QuotaPerUnit, "quota_per_unit": common.QuotaPerUnit,
"display_in_currency": common.DisplayInCurrencyEnabled, // 兼容旧前端:保留 display_in_currency,同时提供新的 quota_display_type
"enable_batch_update": common.BatchUpdateEnabled, "display_in_currency": operation_setting.IsCurrencyDisplay(),
"enable_drawing": common.DrawingEnabled, "quota_display_type": operation_setting.GetQuotaDisplayType(),
"enable_task": common.TaskEnabled, "custom_currency_symbol": operation_setting.GetGeneralSetting().CustomCurrencySymbol,
"enable_data_export": common.DataExportEnabled, "custom_currency_exchange_rate": operation_setting.GetGeneralSetting().CustomCurrencyExchangeRate,
"data_export_default_time": common.DataExportDefaultTime, "enable_batch_update": common.BatchUpdateEnabled,
"default_collapse_sidebar": common.DefaultCollapseSidebar, "enable_drawing": common.DrawingEnabled,
"mj_notify_enabled": setting.MjNotifyEnabled, "enable_task": common.TaskEnabled,
"chats": setting.Chats, "enable_data_export": common.DataExportEnabled,
"demo_site_enabled": operation_setting.DemoSiteEnabled, "data_export_default_time": common.DataExportDefaultTime,
"self_use_mode_enabled": operation_setting.SelfUseModeEnabled, "default_collapse_sidebar": common.DefaultCollapseSidebar,
"default_use_auto_group": setting.DefaultUseAutoGroup, "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, "usd_exchange_rate": operation_setting.USDExchangeRate,
"price": operation_setting.Price, "price": operation_setting.Price,
...@@ -94,7 +102,16 @@ func GetStatus(c *gin.Context) { ...@@ -94,7 +102,16 @@ func GetStatus(c *gin.Context) {
"oidc_enabled": system_setting.GetOIDCSettings().Enabled, "oidc_enabled": system_setting.GetOIDCSettings().Enabled,
"oidc_client_id": system_setting.GetOIDCSettings().ClientId, "oidc_client_id": system_setting.GetOIDCSettings().ClientId,
"oidc_authorization_endpoint": system_setting.GetOIDCSettings().AuthorizationEndpoint, "oidc_authorization_endpoint": system_setting.GetOIDCSettings().AuthorizationEndpoint,
"passkey_login": passkeySetting.Enabled,
"passkey_display_name": passkeySetting.RPDisplayName,
"passkey_rp_id": passkeySetting.RPID,
"passkey_origins": passkeySetting.Origins,
"passkey_allow_insecure": passkeySetting.AllowInsecureOrigin,
"passkey_user_verification": passkeySetting.UserVerification,
"passkey_attachment": passkeySetting.AttachmentPreference,
"setup": constant.Setup, "setup": constant.Setup,
"user_agreement_enabled": legalSetting.UserAgreement != "",
"privacy_policy_enabled": legalSetting.PrivacyPolicy != "",
} }
// 根据启用状态注入可选内容 // 根据启用状态注入可选内容
...@@ -138,6 +155,24 @@ func GetAbout(c *gin.Context) { ...@@ -138,6 +155,24 @@ func GetAbout(c *gin.Context) {
return 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) { func GetMidjourney(c *gin.Context) {
common.OptionMapRWMutex.RLock() common.OptionMapRWMutex.RLock()
defer common.OptionMapRWMutex.RUnlock() defer common.OptionMapRWMutex.RUnlock()
......
...@@ -2,7 +2,8 @@ package controller ...@@ -2,7 +2,8 @@ package controller
import ( import (
"net/http" "net/http"
"one-api/model"
"github.com/QuantumNous/new-api/model"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
) )
......
...@@ -2,21 +2,22 @@ package controller ...@@ -2,21 +2,22 @@ package controller
import ( import (
"fmt" "fmt"
"github.com/gin-gonic/gin"
"github.com/samber/lo"
"net/http" "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" "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"
) )
// https://platform.openai.com/docs/api-reference/models/list // https://platform.openai.com/docs/api-reference/models/list
......
...@@ -6,9 +6,9 @@ import ( ...@@ -6,9 +6,9 @@ import (
"strconv" "strconv"
"strings" "strings"
"one-api/common" "github.com/QuantumNous/new-api/common"
"one-api/constant" "github.com/QuantumNous/new-api/constant"
"one-api/model" "github.com/QuantumNous/new-api/model"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
) )
......
...@@ -13,8 +13,8 @@ import ( ...@@ -13,8 +13,8 @@ import (
"sync" "sync"
"time" "time"
"one-api/common" "github.com/QuantumNous/new-api/common"
"one-api/model" "github.com/QuantumNous/new-api/model"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
"gorm.io/gorm" "gorm.io/gorm"
......
...@@ -6,13 +6,14 @@ import ( ...@@ -6,13 +6,14 @@ import (
"fmt" "fmt"
"net/http" "net/http"
"net/url" "net/url"
"one-api/common"
"one-api/model"
"one-api/setting/system_setting"
"strconv" "strconv"
"strings" "strings"
"time" "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-contrib/sessions"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
) )
......
...@@ -4,14 +4,15 @@ import ( ...@@ -4,14 +4,15 @@ import (
"encoding/json" "encoding/json"
"fmt" "fmt"
"net/http" "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" "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" "github.com/gin-gonic/gin"
) )
......
...@@ -3,13 +3,14 @@ package controller ...@@ -3,13 +3,14 @@ package controller
import ( import (
"errors" "errors"
"fmt" "fmt"
"one-api/common"
"one-api/constant"
"one-api/middleware"
"one-api/model"
"one-api/types"
"time" "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" "github.com/gin-gonic/gin"
) )
......
...@@ -3,8 +3,8 @@ package controller ...@@ -3,8 +3,8 @@ package controller
import ( import (
"strconv" "strconv"
"one-api/common" "github.com/QuantumNous/new-api/common"
"one-api/model" "github.com/QuantumNous/new-api/model"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
) )
......
package controller package controller
import ( import (
"one-api/model" "github.com/QuantumNous/new-api/model"
"one-api/setting" "github.com/QuantumNous/new-api/setting"
"one-api/setting/ratio_setting" "github.com/QuantumNous/new-api/setting/ratio_setting"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
) )
......
...@@ -2,7 +2,8 @@ package controller ...@@ -2,7 +2,8 @@ package controller
import ( import (
"net/http" "net/http"
"one-api/setting/ratio_setting"
"github.com/QuantumNous/new-api/setting/ratio_setting"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
) )
......
...@@ -7,14 +7,15 @@ import ( ...@@ -7,14 +7,15 @@ import (
"io" "io"
"net" "net"
"net/http" "net/http"
"one-api/logger"
"strings" "strings"
"sync" "sync"
"time" "time"
"one-api/dto" "github.com/QuantumNous/new-api/logger"
"one-api/model"
"one-api/setting/ratio_setting" "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" "github.com/gin-gonic/gin"
) )
......
...@@ -3,11 +3,12 @@ package controller ...@@ -3,11 +3,12 @@ package controller
import ( import (
"errors" "errors"
"net/http" "net/http"
"one-api/common"
"one-api/model"
"strconv" "strconv"
"unicode/utf8" "unicode/utf8"
"github.com/QuantumNous/new-api/common"
"github.com/QuantumNous/new-api/model"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
) )
......
...@@ -6,21 +6,22 @@ import ( ...@@ -6,21 +6,22 @@ import (
"io" "io"
"log" "log"
"net/http" "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" "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/bytedance/gopkg/util/gopool"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
...@@ -139,9 +140,13 @@ func Relay(c *gin.Context, relayFormat types.RelayFormat) { ...@@ -139,9 +140,13 @@ func Relay(c *gin.Context, relayFormat types.RelayFormat) {
// common.SetContextKey(c, constant.ContextKeyTokenCountMeta, meta) // common.SetContextKey(c, constant.ContextKeyTokenCountMeta, meta)
newAPIError = service.PreConsumeQuota(c, priceData.ShouldPreConsumedQuota, relayInfo) if priceData.FreeModel {
if newAPIError != nil { logger.LogInfo(c, fmt.Sprintf("模型 %s 免费,跳过预扣费", relayInfo.OriginModelName))
return } else {
newAPIError = service.PreConsumeQuota(c, priceData.QuotaToPreConsume, relayInfo)
if newAPIError != nil {
return
}
} }
defer func() { defer func() {
...@@ -224,7 +229,7 @@ func getChannel(c *gin.Context, group, originalModel string, retryCount int) (*m ...@@ -224,7 +229,7 @@ func getChannel(c *gin.Context, group, originalModel string, retryCount int) (*m
return nil, types.NewError(fmt.Errorf("获取分组 %s 下模型 %s 的可用渠道失败(retry): %s", selectGroup, originalModel, err.Error()), types.ErrorCodeGetChannelFailed, types.ErrOptionWithSkipRetry()) return nil, types.NewError(fmt.Errorf("获取分组 %s 下模型 %s 的可用渠道失败(retry): %s", selectGroup, originalModel, err.Error()), types.ErrorCodeGetChannelFailed, types.ErrOptionWithSkipRetry())
} }
if channel == nil { if channel == nil {
return nil, types.NewError(fmt.Errorf("分组 %s 下模型 %s 的可用渠道不存在(数据库一致性已被破坏,retry)", selectGroup, originalModel), types.ErrorCodeGetChannelFailed, types.ErrOptionWithSkipRetry()) return nil, types.NewError(fmt.Errorf("分组 %s 下模型 %s 的可用渠道不存在(retry)", selectGroup, originalModel), types.ErrorCodeGetChannelFailed, types.ErrOptionWithSkipRetry())
} }
newAPIError := middleware.SetupContextForSelectedChannel(c, channel, originalModel) newAPIError := middleware.SetupContextForSelectedChannel(c, channel, originalModel)
if newAPIError != nil { if newAPIError != nil {
...@@ -294,6 +299,9 @@ func processChannelError(c *gin.Context, channelError types.ChannelError, err *t ...@@ -294,6 +299,9 @@ func processChannelError(c *gin.Context, channelError types.ChannelError, err *t
userGroup := c.GetString("group") userGroup := c.GetString("group")
channelId := c.GetInt("channel_id") channelId := c.GetInt("channel_id")
other := make(map[string]interface{}) other := make(map[string]interface{})
if c.Request != nil && c.Request.URL != nil {
other["request_path"] = c.Request.URL.Path
}
other["error_type"] = err.GetErrorType() other["error_type"] = err.GetErrorType()
other["error_code"] = err.GetErrorCode() other["error_code"] = err.GetErrorCode()
other["status_code"] = err.StatusCode other["status_code"] = err.StatusCode
......
package controller
import (
"fmt"
"net/http"
"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"
)
const (
// SecureVerificationSessionKey 安全验证的 session key
SecureVerificationSessionKey = "secure_verified_at"
// SecureVerificationTimeout 验证有效期(秒)
SecureVerificationTimeout = 300 // 5分钟
)
type UniversalVerifyRequest struct {
Method string `json:"method"` // "2fa" 或 "passkey"
Code string `json:"code,omitempty"`
}
type VerificationStatusResponse struct {
Verified bool `json:"verified"`
ExpiresAt int64 `json:"expires_at,omitempty"`
}
// UniversalVerify 通用验证接口
// 支持 2FA 和 Passkey 验证,验证成功后在 session 中记录时间戳
func UniversalVerify(c *gin.Context) {
userId := c.GetInt("id")
if userId == 0 {
c.JSON(http.StatusUnauthorized, gin.H{
"success": false,
"message": "未登录",
})
return
}
var req UniversalVerifyRequest
if err := c.ShouldBindJSON(&req); err != nil {
common.ApiError(c, fmt.Errorf("参数错误: %v", err))
return
}
// 获取用户信息
user := &model.User{Id: userId}
if err := user.FillUserById(); err != nil {
common.ApiError(c, fmt.Errorf("获取用户信息失败: %v", err))
return
}
if user.Status != common.UserStatusEnabled {
common.ApiError(c, fmt.Errorf("该用户已被禁用"))
return
}
// 检查用户的验证方式
twoFA, _ := model.GetTwoFAByUserId(userId)
has2FA := twoFA != nil && twoFA.IsEnabled
passkey, passkeyErr := model.GetPasskeyByUserID(userId)
hasPasskey := passkeyErr == nil && passkey != nil
if !has2FA && !hasPasskey {
common.ApiError(c, fmt.Errorf("用户未启用2FA或Passkey"))
return
}
// 根据验证方式进行验证
var verified bool
var verifyMethod string
switch req.Method {
case "2fa":
if !has2FA {
common.ApiError(c, fmt.Errorf("用户未启用2FA"))
return
}
if req.Code == "" {
common.ApiError(c, fmt.Errorf("验证码不能为空"))
return
}
verified = validateTwoFactorAuth(twoFA, req.Code)
verifyMethod = "2FA"
case "passkey":
if !hasPasskey {
common.ApiError(c, fmt.Errorf("用户未启用Passkey"))
return
}
// Passkey 验证需要先调用 PasskeyVerifyBegin 和 PasskeyVerifyFinish
// 这里只是验证 Passkey 验证流程是否已经完成
// 实际上,前端应该先调用这两个接口,然后再调用本接口
verified = true // Passkey 验证逻辑已在 PasskeyVerifyFinish 中完成
verifyMethod = "Passkey"
default:
common.ApiError(c, fmt.Errorf("不支持的验证方式: %s", req.Method))
return
}
if !verified {
common.ApiError(c, fmt.Errorf("验证失败,请检查验证码"))
return
}
// 验证成功,在 session 中记录时间戳
session := sessions.Default(c)
now := time.Now().Unix()
session.Set(SecureVerificationSessionKey, now)
if err := session.Save(); err != nil {
common.ApiError(c, fmt.Errorf("保存验证状态失败: %v", err))
return
}
// 记录日志
model.RecordLog(userId, model.LogTypeSystem, fmt.Sprintf("通用安全验证成功 (验证方式: %s)", verifyMethod))
c.JSON(http.StatusOK, gin.H{
"success": true,
"message": "验证成功",
"data": gin.H{
"verified": true,
"expires_at": now + SecureVerificationTimeout,
},
})
}
// GetVerificationStatus 获取验证状态
func GetVerificationStatus(c *gin.Context) {
userId := c.GetInt("id")
if userId == 0 {
c.JSON(http.StatusUnauthorized, gin.H{
"success": false,
"message": "未登录",
})
return
}
session := sessions.Default(c)
verifiedAtRaw := session.Get(SecureVerificationSessionKey)
if verifiedAtRaw == nil {
c.JSON(http.StatusOK, gin.H{
"success": true,
"message": "",
"data": VerificationStatusResponse{
Verified: false,
},
})
return
}
verifiedAt, ok := verifiedAtRaw.(int64)
if !ok {
c.JSON(http.StatusOK, gin.H{
"success": true,
"message": "",
"data": VerificationStatusResponse{
Verified: false,
},
})
return
}
elapsed := time.Now().Unix() - verifiedAt
if elapsed >= SecureVerificationTimeout {
// 验证已过期
session.Delete(SecureVerificationSessionKey)
_ = session.Save()
c.JSON(http.StatusOK, gin.H{
"success": true,
"message": "",
"data": VerificationStatusResponse{
Verified: false,
},
})
return
}
c.JSON(http.StatusOK, gin.H{
"success": true,
"message": "",
"data": VerificationStatusResponse{
Verified: true,
ExpiresAt: verifiedAt + SecureVerificationTimeout,
},
})
}
// CheckSecureVerification 检查是否已通过安全验证
// 返回 true 表示验证有效,false 表示需要重新验证
func CheckSecureVerification(c *gin.Context) bool {
session := sessions.Default(c)
verifiedAtRaw := session.Get(SecureVerificationSessionKey)
if verifiedAtRaw == nil {
return false
}
verifiedAt, ok := verifiedAtRaw.(int64)
if !ok {
return false
}
elapsed := time.Now().Unix() - verifiedAt
if elapsed >= SecureVerificationTimeout {
// 验证已过期,清除 session
session.Delete(SecureVerificationSessionKey)
_ = session.Save()
return false
}
return true
}
// PasskeyVerifyAndSetSession Passkey 验证完成后设置 session
// 这是一个辅助函数,供 PasskeyVerifyFinish 调用
func PasskeyVerifyAndSetSession(c *gin.Context) {
session := sessions.Default(c)
now := time.Now().Unix()
session.Set(SecureVerificationSessionKey, now)
_ = session.Save()
}
// PasskeyVerifyForSecure 用于安全验证的 Passkey 验证流程
// 整合了 begin 和 finish 流程
func PasskeyVerifyForSecure(c *gin.Context) {
if !system_setting.GetPasskeySettings().Enabled {
c.JSON(http.StatusOK, gin.H{
"success": false,
"message": "管理员未启用 Passkey 登录",
})
return
}
userId := c.GetInt("id")
if userId == 0 {
c.JSON(http.StatusUnauthorized, gin.H{
"success": false,
"message": "未登录",
})
return
}
user := &model.User{Id: userId}
if err := user.FillUserById(); err != nil {
common.ApiError(c, fmt.Errorf("获取用户信息失败: %v", err))
return
}
if user.Status != common.UserStatusEnabled {
common.ApiError(c, fmt.Errorf("该用户已被禁用"))
return
}
credential, err := model.GetPasskeyByUserID(userId)
if err != nil {
c.JSON(http.StatusOK, gin.H{
"success": false,
"message": "该用户尚未绑定 Passkey",
})
return
}
wa, err := passkeysvc.BuildWebAuthn(c.Request)
if err != nil {
common.ApiError(c, err)
return
}
waUser := passkeysvc.NewWebAuthnUser(user, credential)
sessionData, err := passkeysvc.PopSessionData(c, passkeysvc.VerifySessionKey)
if err != nil {
common.ApiError(c, err)
return
}
_, err = wa.FinishLogin(waUser, *sessionData, c.Request)
if err != nil {
common.ApiError(c, err)
return
}
// 更新凭证的最后使用时间
now := time.Now()
credential.LastUsedAt = &now
if err := model.UpsertPasskeyCredential(credential); err != nil {
common.ApiError(c, err)
return
}
// 验证成功,设置 session
PasskeyVerifyAndSetSession(c)
// 记录日志
model.RecordLog(userId, model.LogTypeSystem, "Passkey 安全验证成功")
c.JSON(http.StatusOK, gin.H{
"success": true,
"message": "Passkey 验证成功",
"data": gin.H{
"verified": true,
"expires_at": time.Now().Unix() + SecureVerificationTimeout,
},
})
}
package controller package controller
import ( import (
"github.com/gin-gonic/gin"
"one-api/common"
"one-api/constant"
"one-api/model"
"one-api/setting/operation_setting"
"time" "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 { type Setup struct {
...@@ -178,4 +179,4 @@ func boolToString(b bool) string { ...@@ -178,4 +179,4 @@ func boolToString(b bool) string {
return "true" return "true"
} }
return "false" return "false"
} }
\ No newline at end of file
...@@ -7,16 +7,17 @@ import ( ...@@ -7,16 +7,17 @@ import (
"fmt" "fmt"
"io" "io"
"net/http" "net/http"
"one-api/common"
"one-api/constant"
"one-api/dto"
"one-api/logger"
"one-api/model"
"one-api/relay"
"sort" "sort"
"strconv" "strconv"
"time" "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/gin-gonic/gin"
"github.com/samber/lo" "github.com/samber/lo"
) )
......
...@@ -5,15 +5,17 @@ import ( ...@@ -5,15 +5,17 @@ import (
"encoding/json" "encoding/json"
"fmt" "fmt"
"io" "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" "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 { 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 ...@@ -46,6 +48,11 @@ func updateVideoTaskAll(ctx context.Context, platform constant.TaskPlatform, cha
if adaptor == nil { if adaptor == nil {
return fmt.Errorf("video adaptor not found") return fmt.Errorf("video adaptor not found")
} }
info := &relaycommon.RelayInfo{}
info.ChannelMeta = &relaycommon.ChannelMeta{
ChannelBaseUrl: cacheGetChannel.GetBaseURL(),
}
adaptor.Init(info)
for _, taskId := range taskIds { for _, taskId := range taskIds {
if err := updateVideoSingleTask(ctx, adaptor, cacheGetChannel, taskId, taskM); err != nil { 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())) logger.LogError(ctx, fmt.Sprintf("Failed to update video task %s: %s", taskId, err.Error()))
...@@ -81,26 +88,39 @@ func updateVideoSingleTask(ctx context.Context, adaptor channel.TaskAdaptor, cha ...@@ -81,26 +88,39 @@ func updateVideoSingleTask(ctx context.Context, adaptor channel.TaskAdaptor, cha
return fmt.Errorf("readAll failed for task %s: %w", taskId, err) return fmt.Errorf("readAll failed for task %s: %w", taskId, err)
} }
logger.LogDebug(ctx, fmt.Sprintf("UpdateVideoSingleTask response: %s", string(responseBody)))
taskResult := &relaycommon.TaskInfo{} taskResult := &relaycommon.TaskInfo{}
// try parse as New API response format // try parse as New API response format
var responseItems dto.TaskResponse[model.Task] var responseItems dto.TaskResponse[model.Task]
if err = json.Unmarshal(responseBody, &responseItems); err == nil && responseItems.IsSuccess() { if err = common.Unmarshal(responseBody, &responseItems); err == nil && responseItems.IsSuccess() {
logger.LogDebug(ctx, fmt.Sprintf("UpdateVideoSingleTask parsed as new api response format: %+v", responseItems))
t := responseItems.Data t := responseItems.Data
taskResult.TaskID = t.TaskID taskResult.TaskID = t.TaskID
taskResult.Status = string(t.Status) taskResult.Status = string(t.Status)
taskResult.Url = t.FailReason taskResult.Url = t.FailReason
taskResult.Progress = t.Progress taskResult.Progress = t.Progress
taskResult.Reason = t.FailReason taskResult.Reason = t.FailReason
task.Data = t.Data
} else if taskResult, err = adaptor.ParseTaskResult(responseBody); err != nil { } else if taskResult, err = adaptor.ParseTaskResult(responseBody); err != nil {
return fmt.Errorf("parseTaskResult failed for task %s: %w", taskId, err) return fmt.Errorf("parseTaskResult failed for task %s: %w", taskId, err)
} else { } else {
task.Data = redactVideoResponseBody(responseBody) task.Data = redactVideoResponseBody(responseBody)
} }
logger.LogDebug(ctx, fmt.Sprintf("UpdateVideoSingleTask taskResult: %+v", taskResult))
now := time.Now().Unix() now := time.Now().Unix()
if taskResult.Status == "" { if taskResult.Status == "" {
return fmt.Errorf("task %s status is empty", taskId) //return fmt.Errorf("task %s status is empty", taskId)
taskResult = relaycommon.FailTaskInfo("upstream returned empty status")
} }
// 记录原本的状态,防止重复退款
shouldRefund := false
quota := task.Quota
preStatus := task.Status
task.Status = model.TaskStatus(taskResult.Status) task.Status = model.TaskStatus(taskResult.Status)
switch taskResult.Status { switch taskResult.Status {
case model.TaskStatusSubmitted: case model.TaskStatusSubmitted:
...@@ -120,7 +140,98 @@ func updateVideoSingleTask(ctx context.Context, adaptor channel.TaskAdaptor, cha ...@@ -120,7 +140,98 @@ func updateVideoSingleTask(ctx context.Context, adaptor channel.TaskAdaptor, cha
if !(len(taskResult.Url) > 5 && taskResult.Url[:5] == "data:") { if !(len(taskResult.Url) > 5 && taskResult.Url[:5] == "data:") {
task.FailReason = taskResult.Url 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: case model.TaskStatusFailure:
logger.LogJson(ctx, fmt.Sprintf("Task %s failed", taskId), task)
task.Status = model.TaskStatusFailure task.Status = model.TaskStatusFailure
task.Progress = "100%" task.Progress = "100%"
if task.FinishTime == 0 { if task.FinishTime == 0 {
...@@ -128,13 +239,13 @@ func updateVideoSingleTask(ctx context.Context, adaptor channel.TaskAdaptor, cha ...@@ -128,13 +239,13 @@ func updateVideoSingleTask(ctx context.Context, adaptor channel.TaskAdaptor, cha
} }
task.FailReason = taskResult.Reason task.FailReason = taskResult.Reason
logger.LogInfo(ctx, fmt.Sprintf("Task %s failed: %s", task.TaskID, task.FailReason)) logger.LogInfo(ctx, fmt.Sprintf("Task %s failed: %s", task.TaskID, task.FailReason))
quota := task.Quota taskResult.Progress = "100%"
if quota != 0 { if quota != 0 {
if err := model.IncreaseUserQuota(task.UserId, quota, false); err != nil { if preStatus != model.TaskStatusFailure {
logger.LogError(ctx, "Failed to increase user quota: "+err.Error()) shouldRefund = true
} 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: default:
return fmt.Errorf("unknown task status %s for task %s", taskResult.Status, taskId) return fmt.Errorf("unknown task status %s for task %s", taskResult.Status, taskId)
...@@ -144,6 +255,16 @@ func updateVideoSingleTask(ctx context.Context, adaptor channel.TaskAdaptor, cha ...@@ -144,6 +255,16 @@ func updateVideoSingleTask(ctx context.Context, adaptor channel.TaskAdaptor, cha
} }
if err := task.Update(); err != nil { if err := task.Update(); err != nil {
common.SysLog("UpdateVideoTask task error: " + err.Error()) common.SysLog("UpdateVideoTask task error: " + err.Error())
shouldRefund = false
}
if shouldRefund {
// 任务失败且之前状态不是失败才退还额度,防止重复退还
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)
} }
return nil return nil
......
...@@ -6,10 +6,11 @@ import ( ...@@ -6,10 +6,11 @@ import (
"encoding/hex" "encoding/hex"
"io" "io"
"net/http" "net/http"
"one-api/common"
"one-api/model"
"sort" "sort"
"github.com/QuantumNous/new-api/common"
"github.com/QuantumNous/new-api/model"
"github.com/gin-contrib/sessions" "github.com/gin-contrib/sessions"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
) )
...@@ -65,7 +66,7 @@ func TelegramBind(c *gin.Context) { ...@@ -65,7 +66,7 @@ func TelegramBind(c *gin.Context) {
return return
} }
c.Redirect(302, "/setting") c.Redirect(302, "/console/personal")
} }
func TelegramLogin(c *gin.Context) { func TelegramLogin(c *gin.Context) {
......
...@@ -2,11 +2,12 @@ package controller ...@@ -2,11 +2,12 @@ package controller
import ( import (
"net/http" "net/http"
"one-api/common"
"one-api/model"
"strconv" "strconv"
"strings" "strings"
"github.com/QuantumNous/new-api/common"
"github.com/QuantumNous/new-api/model"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
) )
......
...@@ -4,17 +4,18 @@ import ( ...@@ -4,17 +4,18 @@ import (
"fmt" "fmt"
"log" "log"
"net/url" "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" "strconv"
"sync" "sync"
"time" "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/Calcium-Ion/go-epay/epay"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
"github.com/samber/lo" "github.com/samber/lo"
...@@ -88,8 +89,9 @@ func GetEpayClient() *epay.Client { ...@@ -88,8 +89,9 @@ func GetEpayClient() *epay.Client {
func getPayMoney(amount int64, group string) float64 { func getPayMoney(amount int64, group string) float64 {
dAmount := decimal.NewFromInt(amount) 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) dQuotaPerUnit := decimal.NewFromFloat(common.QuotaPerUnit)
dAmount = dAmount.Div(dQuotaPerUnit) dAmount = dAmount.Div(dQuotaPerUnit)
} }
...@@ -117,7 +119,7 @@ func getPayMoney(amount int64, group string) float64 { ...@@ -117,7 +119,7 @@ func getPayMoney(amount int64, group string) float64 {
func getMinTopup() int64 { func getMinTopup() int64 {
minTopup := operation_setting.MinTopUp minTopup := operation_setting.MinTopUp
if !common.DisplayInCurrencyEnabled { if operation_setting.GetQuotaDisplayType() == operation_setting.QuotaDisplayTypeTokens {
dMinTopup := decimal.NewFromInt(int64(minTopup)) dMinTopup := decimal.NewFromInt(int64(minTopup))
dQuotaPerUnit := decimal.NewFromFloat(common.QuotaPerUnit) dQuotaPerUnit := decimal.NewFromFloat(common.QuotaPerUnit)
minTopup = int(dMinTopup.Mul(dQuotaPerUnit).IntPart()) minTopup = int(dMinTopup.Mul(dQuotaPerUnit).IntPart())
...@@ -178,18 +180,19 @@ func RequestEpay(c *gin.Context) { ...@@ -178,18 +180,19 @@ func RequestEpay(c *gin.Context) {
return return
} }
amount := req.Amount amount := req.Amount
if !common.DisplayInCurrencyEnabled { if operation_setting.GetQuotaDisplayType() == operation_setting.QuotaDisplayTypeTokens {
dAmount := decimal.NewFromInt(int64(amount)) dAmount := decimal.NewFromInt(int64(amount))
dQuotaPerUnit := decimal.NewFromFloat(common.QuotaPerUnit) dQuotaPerUnit := decimal.NewFromFloat(common.QuotaPerUnit)
amount = dAmount.Div(dQuotaPerUnit).IntPart() amount = dAmount.Div(dQuotaPerUnit).IntPart()
} }
topUp := &model.TopUp{ topUp := &model.TopUp{
UserId: id, UserId: id,
Amount: amount, Amount: amount,
Money: payMoney, Money: payMoney,
TradeNo: tradeNo, TradeNo: tradeNo,
CreateTime: time.Now().Unix(), PaymentMethod: req.PaymentMethod,
Status: "pending", CreateTime: time.Now().Unix(),
Status: "pending",
} }
err = topUp.Insert() err = topUp.Insert()
if err != nil { if err != nil {
...@@ -237,8 +240,8 @@ func EpayNotify(c *gin.Context) { ...@@ -237,8 +240,8 @@ func EpayNotify(c *gin.Context) {
_, err := c.Writer.Write([]byte("fail")) _, err := c.Writer.Write([]byte("fail"))
if err != nil { if err != nil {
log.Println("易支付回调写入失败") log.Println("易支付回调写入失败")
return
} }
return
} }
verifyInfo, err := client.Verify(params) verifyInfo, err := client.Verify(params)
if err == nil && verifyInfo.VerifyStatus { if err == nil && verifyInfo.VerifyStatus {
...@@ -314,3 +317,76 @@ func RequestAmount(c *gin.Context) { ...@@ -314,3 +317,76 @@ func RequestAmount(c *gin.Context) {
} }
c.JSON(200, gin.H{"message": "success", "data": strconv.FormatFloat(payMoney, 'f', 2, 64)}) 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 ( ...@@ -5,15 +5,16 @@ import (
"io" "io"
"log" "log"
"net/http" "net/http"
"one-api/common"
"one-api/model"
"one-api/setting"
"one-api/setting/operation_setting"
"one-api/setting/system_setting"
"strconv" "strconv"
"strings" "strings"
"time" "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/gin-gonic/gin"
"github.com/stripe/stripe-go/v81" "github.com/stripe/stripe-go/v81"
"github.com/stripe/stripe-go/v81/checkout/session" "github.com/stripe/stripe-go/v81/checkout/session"
...@@ -83,12 +84,13 @@ func (*StripeAdaptor) RequestPay(c *gin.Context, req *StripePayRequest) { ...@@ -83,12 +84,13 @@ func (*StripeAdaptor) RequestPay(c *gin.Context, req *StripePayRequest) {
} }
topUp := &model.TopUp{ topUp := &model.TopUp{
UserId: id, UserId: id,
Amount: req.Amount, Amount: req.Amount,
Money: chargedMoney, Money: chargedMoney,
TradeNo: referenceId, TradeNo: referenceId,
CreateTime: time.Now().Unix(), PaymentMethod: PaymentMethodStripe,
Status: common.TopUpStatusPending, CreateTime: time.Now().Unix(),
Status: common.TopUpStatusPending,
} }
err = topUp.Insert() err = topUp.Insert()
if err != nil { if err != nil {
...@@ -225,7 +227,8 @@ func genStripeLink(referenceId string, customerId string, email string, amount i ...@@ -225,7 +227,8 @@ func genStripeLink(referenceId string, customerId string, email string, amount i
Quantity: stripe.Int64(amount), Quantity: stripe.Int64(amount),
}, },
}, },
Mode: stripe.String(string(stripe.CheckoutSessionModePayment)), Mode: stripe.String(string(stripe.CheckoutSessionModePayment)),
AllowPromotionCodes: stripe.Bool(setting.StripePromotionCodesEnabled),
} }
if "" == customerId { if "" == customerId {
...@@ -257,7 +260,7 @@ func GetChargedAmount(count float64, user model.User) float64 { ...@@ -257,7 +260,7 @@ func GetChargedAmount(count float64, user model.User) float64 {
func getStripePayMoney(amount float64, group string) float64 { func getStripePayMoney(amount float64, group string) float64 {
originalAmount := amount originalAmount := amount
if !common.DisplayInCurrencyEnabled { if operation_setting.GetQuotaDisplayType() == operation_setting.QuotaDisplayTypeTokens {
amount = amount / common.QuotaPerUnit amount = amount / common.QuotaPerUnit
} }
// Using float64 for monetary calculations is acceptable here due to the small amounts involved // Using float64 for monetary calculations is acceptable here due to the small amounts involved
...@@ -278,7 +281,7 @@ func getStripePayMoney(amount float64, group string) float64 { ...@@ -278,7 +281,7 @@ func getStripePayMoney(amount float64, group string) float64 {
func getStripeMinTopup() int64 { func getStripeMinTopup() int64 {
minTopup := setting.StripeMinTopUp minTopup := setting.StripeMinTopUp
if !common.DisplayInCurrencyEnabled { if operation_setting.GetQuotaDisplayType() == operation_setting.QuotaDisplayTypeTokens {
minTopup = minTopup * int(common.QuotaPerUnit) minTopup = minTopup * int(common.QuotaPerUnit)
} }
return int64(minTopup) return int64(minTopup)
......
...@@ -4,10 +4,11 @@ import ( ...@@ -4,10 +4,11 @@ import (
"errors" "errors"
"fmt" "fmt"
"net/http" "net/http"
"one-api/common"
"one-api/model"
"strconv" "strconv"
"github.com/QuantumNous/new-api/common"
"github.com/QuantumNous/new-api/model"
"github.com/gin-contrib/sessions" "github.com/gin-contrib/sessions"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
) )
......
...@@ -5,11 +5,12 @@ import ( ...@@ -5,11 +5,12 @@ import (
"encoding/json" "encoding/json"
"errors" "errors"
"net/http" "net/http"
"one-api/setting/console_setting"
"strconv" "strconv"
"strings" "strings"
"time" "time"
"github.com/QuantumNous/new-api/setting/console_setting"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
"golang.org/x/sync/errgroup" "golang.org/x/sync/errgroup"
) )
......
...@@ -2,10 +2,11 @@ package controller ...@@ -2,10 +2,11 @@ package controller
import ( import (
"net/http" "net/http"
"one-api/common"
"one-api/model"
"strconv" "strconv"
"github.com/QuantumNous/new-api/common"
"github.com/QuantumNous/new-api/model"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
) )
......
...@@ -5,16 +5,17 @@ import ( ...@@ -5,16 +5,17 @@ import (
"fmt" "fmt"
"net/http" "net/http"
"net/url" "net/url"
"one-api/common"
"one-api/dto"
"one-api/logger"
"one-api/model"
"one-api/setting"
"strconv" "strconv"
"strings" "strings"
"sync" "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-contrib/sessions"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
...@@ -450,6 +451,10 @@ func GetSelf(c *gin.Context) { ...@@ -450,6 +451,10 @@ func GetSelf(c *gin.Context) {
"role": user.Role, "role": user.Role,
"status": user.Status, "status": user.Status,
"email": user.Email, "email": user.Email,
"github_id": user.GitHubId,
"oidc_id": user.OidcId,
"wechat_id": user.WeChatId,
"telegram_id": user.TelegramId,
"group": user.Group, "group": user.Group,
"quota": user.Quota, "quota": user.Quota,
"used_quota": user.UsedQuota, "used_quota": user.UsedQuota,
...@@ -1098,6 +1103,9 @@ type UpdateUserSettingRequest struct { ...@@ -1098,6 +1103,9 @@ type UpdateUserSettingRequest struct {
WebhookSecret string `json:"webhook_secret,omitempty"` WebhookSecret string `json:"webhook_secret,omitempty"`
NotificationEmail string `json:"notification_email,omitempty"` NotificationEmail string `json:"notification_email,omitempty"`
BarkUrl string `json:"bark_url,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"` AcceptUnsetModelRatioModel bool `json:"accept_unset_model_ratio_model"`
RecordIpLog bool `json:"record_ip_log"` RecordIpLog bool `json:"record_ip_log"`
} }
...@@ -1113,7 +1121,7 @@ func UpdateUserSetting(c *gin.Context) { ...@@ -1113,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{ c.JSON(http.StatusOK, gin.H{
"success": false, "success": false,
"message": "无效的预警类型", "message": "无效的预警类型",
...@@ -1188,6 +1196,40 @@ func UpdateUserSetting(c *gin.Context) { ...@@ -1188,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") userId := c.GetInt("id")
user, err := model.GetUserById(userId, true) user, err := model.GetUserById(userId, true)
if err != nil { if err != nil {
...@@ -1221,6 +1263,18 @@ func UpdateUserSetting(c *gin.Context) { ...@@ -1221,6 +1263,18 @@ func UpdateUserSetting(c *gin.Context) {
settings.BarkUrl = req.BarkUrl 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) user.SetSetting(settings)
if err := user.Update(false); err != nil { if err := user.Update(false); err != nil {
......
...@@ -3,8 +3,8 @@ package controller ...@@ -3,8 +3,8 @@ package controller
import ( import (
"strconv" "strconv"
"one-api/common" "github.com/QuantumNous/new-api/common"
"one-api/model" "github.com/QuantumNous/new-api/model"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
) )
......
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 ( ...@@ -5,11 +5,12 @@ import (
"errors" "errors"
"fmt" "fmt"
"net/http" "net/http"
"one-api/common"
"one-api/model"
"strconv" "strconv"
"time" "time"
"github.com/QuantumNous/new-api/common"
"github.com/QuantumNous/new-api/model"
"github.com/gin-contrib/sessions" "github.com/gin-contrib/sessions"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
) )
......
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: services:
new-api: new-api:
...@@ -12,21 +26,25 @@ services: ...@@ -12,21 +26,25 @@ services:
- ./data:/data - ./data:/data
- ./logs:/app/logs - ./logs:/app/logs
environment: 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 - REDIS_CONN_STRING=redis://redis
- TZ=Asia/Shanghai - TZ=Asia/Shanghai
- ERROR_LOG_ENABLED=true # 是否启用错误日志记录 - ERROR_LOG_ENABLED=true # 是否启用错误日志记录 (Whether to enable error log recording)
# - STREAMING_TIMEOUT=300 # 流模式无响应超时时间,单位秒,默认120秒,如果出现空补全可以尝试改为更大值 - BATCH_UPDATE_ENABLED=true # 是否启用批量更新 (Whether to enable batch update)
# - SESSION_SECRET=random_string # 多机部署时设置,必须修改这个随机字符串!!!!!!! # - STREAMING_TIMEOUT=300 # 流模式无响应超时时间,单位秒,默认120秒,如果出现空补全可以尝试改为更大值 (Streaming timeout in seconds, default is 120s. Increase if experiencing empty completions)
# - NODE_TYPE=slave # Uncomment for slave node in multi-node deployment # - SESSION_SECRET=random_string # 多机部署时设置,必须修改这个随机字符串!! (multi-node deployment, set this to a random string!!!!!!!)
# - SYNC_FREQUENCY=60 # Uncomment if regular database syncing is needed # - 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 # - GOOGLE_ANALYTICS_ID=G-XXXXXXXXXX # Google Analytics 的测量 ID (Google Analytics Measurement ID)
# - UMAMI_WEBSITE_ID=xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx # Umami 网站 ID (Umami Website ID)
# - UMAMI_SCRIPT_URL=https://analytics.umami.is/script.js # Umami 脚本 URL,默认为官方地址 (Umami Script URL, defaults to official URL)
depends_on: depends_on:
- redis - redis
- mysql - postgres
# - mysql # Uncomment if using MySQL
healthcheck: 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 interval: 30s
timeout: 10s timeout: 10s
retries: 3 retries: 3
...@@ -36,17 +54,31 @@ services: ...@@ -36,17 +54,31 @@ services:
container_name: redis container_name: redis
restart: always restart: always
mysql: postgres:
image: mysql:8.2 image: postgres:15
container_name: mysql container_name: postgres
restart: always restart: always
environment: environment:
MYSQL_ROOT_PASSWORD: 123456 # Ensure this matches the password in SQL_DSN POSTGRES_USER: root
MYSQL_DATABASE: new-api POSTGRES_PASSWORD: 123456 # ⚠️ IMPORTANT: Change this password in production!
POSTGRES_DB: new-api
volumes: volumes:
- mysql_data:/var/lib/mysql - pg_data:/var/lib/postgresql/data
# ports: # ports:
# - "3306:3306" # If you want to access MySQL from outside Docker, uncomment # - "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: volumes:
mysql_data: pg_data:
# mysql_data:
# 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.
\ No newline at end of file
# 翻译术语表 (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.
# Русский глоссарий (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.
package dto package dto
import ( import (
"one-api/types" "encoding/json"
"github.com/QuantumNous/new-api/types"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
) )
type AudioRequest struct { type AudioRequest struct {
Model string `json:"model"` Model string `json:"model"`
Input string `json:"input"` Input string `json:"input"`
Voice string `json:"voice"` Voice string `json:"voice"`
Speed float64 `json:"speed,omitempty"` Instructions string `json:"instructions,omitempty"`
ResponseFormat string `json:"response_format,omitempty"` ResponseFormat string `json:"response_format,omitempty"`
Speed float64 `json:"speed,omitempty"`
StreamFormat string `json:"stream_format,omitempty"`
Metadata json.RawMessage `json:"metadata,omitempty"`
} }
func (r *AudioRequest) GetTokenCountMeta() *types.TokenCountMeta { func (r *AudioRequest) GetTokenCountMeta() *types.TokenCountMeta {
......
...@@ -16,7 +16,26 @@ const ( ...@@ -16,7 +16,26 @@ const (
VertexKeyTypeAPIKey VertexKeyType = "api_key" VertexKeyTypeAPIKey VertexKeyType = "api_key"
) )
type AwsKeyType string
const (
AwsKeyTypeAKSK AwsKeyType = "ak_sk" // 默认
AwsKeyTypeApiKey AwsKeyType = "api_key"
)
type ChannelOtherSettings struct { type ChannelOtherSettings struct {
AzureResponsesVersion string `json:"azure_responses_version,omitempty"` AzureResponsesVersion string `json:"azure_responses_version,omitempty"`
VertexKeyType VertexKeyType `json:"vertex_key_type,omitempty"` // "json" or "api_key" 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 透传(默认过滤以保护用户隐私)
AwsKeyType AwsKeyType `json:"aws_key_type,omitempty"`
}
func (s *ChannelOtherSettings) IsOpenRouterEnterprise() bool {
if s == nil || s.OpenRouterEnterprise == nil {
return false
}
return *s.OpenRouterEnterprise
} }
...@@ -3,10 +3,11 @@ package dto ...@@ -3,10 +3,11 @@ package dto
import ( import (
"encoding/json" "encoding/json"
"fmt" "fmt"
"one-api/common"
"one-api/types"
"strings" "strings"
"github.com/QuantumNous/new-api/common"
"github.com/QuantumNous/new-api/types"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
) )
...@@ -23,7 +24,7 @@ type ClaudeMediaMessage struct { ...@@ -23,7 +24,7 @@ type ClaudeMediaMessage struct {
StopReason *string `json:"stop_reason,omitempty"` StopReason *string `json:"stop_reason,omitempty"`
PartialJson *string `json:"partial_json,omitempty"` PartialJson *string `json:"partial_json,omitempty"`
Role string `json:"role,omitempty"` Role string `json:"role,omitempty"`
Thinking string `json:"thinking,omitempty"` Thinking *string `json:"thinking,omitempty"`
Signature string `json:"signature,omitempty"` Signature string `json:"signature,omitempty"`
Delta string `json:"delta,omitempty"` Delta string `json:"delta,omitempty"`
CacheControl json.RawMessage `json:"cache_control,omitempty"` CacheControl json.RawMessage `json:"cache_control,omitempty"`
...@@ -147,6 +148,10 @@ func (c *ClaudeMessage) SetStringContent(content string) { ...@@ -147,6 +148,10 @@ func (c *ClaudeMessage) SetStringContent(content string) {
c.Content = content c.Content = content
} }
func (c *ClaudeMessage) SetContent(content any) {
c.Content = content
}
func (c *ClaudeMessage) ParseContent() ([]ClaudeMediaMessage, error) { func (c *ClaudeMessage) ParseContent() ([]ClaudeMediaMessage, error) {
return common.Any2Type[[]ClaudeMediaMessage](c.Content) return common.Any2Type[[]ClaudeMediaMessage](c.Content)
} }
...@@ -195,11 +200,15 @@ type ClaudeRequest struct { ...@@ -195,11 +200,15 @@ type ClaudeRequest struct {
Temperature *float64 `json:"temperature,omitempty"` Temperature *float64 `json:"temperature,omitempty"`
TopP float64 `json:"top_p,omitempty"` TopP float64 `json:"top_p,omitempty"`
TopK int `json:"top_k,omitempty"` TopK int `json:"top_k,omitempty"`
//ClaudeMetadata `json:"metadata,omitempty"` Stream bool `json:"stream,omitempty"`
Stream bool `json:"stream,omitempty"` Tools any `json:"tools,omitempty"`
Tools any `json:"tools,omitempty"` ContextManagement json.RawMessage `json:"context_management,omitempty"`
ToolChoice any `json:"tool_choice,omitempty"` ToolChoice any `json:"tool_choice,omitempty"`
Thinking *Thinking `json:"thinking,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 { func (c *ClaudeRequest) GetTokenCountMeta() *types.TokenCountMeta {
......
package dto package dto
import ( import (
"one-api/types"
"strings" "strings"
"github.com/QuantumNous/new-api/types"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
) )
......
package dto package dto
import "one-api/types" import "github.com/QuantumNous/new-api/types"
type OpenAIError struct { type OpenAIError struct {
Message string `json:"message"` Message string `json:"message"`
......
...@@ -2,14 +2,17 @@ package dto ...@@ -2,14 +2,17 @@ package dto
import ( import (
"encoding/json" "encoding/json"
"github.com/gin-gonic/gin"
"one-api/common"
"one-api/logger"
"one-api/types"
"strings" "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 { type GeminiChatRequest struct {
Requests []GeminiChatRequest `json:"requests,omitempty"` // For batch requests
Contents []GeminiChatContent `json:"contents"` Contents []GeminiChatContent `json:"contents"`
SafetySettings []GeminiChatSafetySettings `json:"safetySettings,omitempty"` SafetySettings []GeminiChatSafetySettings `json:"safetySettings,omitempty"`
GenerationConfig GeminiChatGenerationConfig `json:"generationConfig,omitempty"` GenerationConfig GeminiChatGenerationConfig `json:"generationConfig,omitempty"`
...@@ -251,6 +254,7 @@ type GeminiChatTool struct { ...@@ -251,6 +254,7 @@ type GeminiChatTool struct {
GoogleSearchRetrieval any `json:"googleSearchRetrieval,omitempty"` GoogleSearchRetrieval any `json:"googleSearchRetrieval,omitempty"`
CodeExecution any `json:"codeExecution,omitempty"` CodeExecution any `json:"codeExecution,omitempty"`
FunctionDeclarations any `json:"functionDeclarations,omitempty"` FunctionDeclarations any `json:"functionDeclarations,omitempty"`
URLContext any `json:"urlContext,omitempty"`
} }
type GeminiChatGenerationConfig struct { type GeminiChatGenerationConfig struct {
...@@ -272,6 +276,7 @@ type GeminiChatGenerationConfig struct { ...@@ -272,6 +276,7 @@ type GeminiChatGenerationConfig struct {
ResponseModalities []string `json:"responseModalities,omitempty"` ResponseModalities []string `json:"responseModalities,omitempty"`
ThinkingConfig *GeminiThinkingConfig `json:"thinkingConfig,omitempty"` ThinkingConfig *GeminiThinkingConfig `json:"thinkingConfig,omitempty"`
SpeechConfig json.RawMessage `json:"speechConfig,omitempty"` // RawMessage to allow flexible speech config 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 type MediaResolution string
...@@ -290,12 +295,13 @@ type GeminiChatSafetyRating struct { ...@@ -290,12 +295,13 @@ type GeminiChatSafetyRating struct {
type GeminiChatPromptFeedback struct { type GeminiChatPromptFeedback struct {
SafetyRatings []GeminiChatSafetyRating `json:"safetyRatings"` SafetyRatings []GeminiChatSafetyRating `json:"safetyRatings"`
BlockReason *string `json:"blockReason,omitempty"`
} }
type GeminiChatResponse struct { type GeminiChatResponse struct {
Candidates []GeminiChatCandidate `json:"candidates"` Candidates []GeminiChatCandidate `json:"candidates"`
PromptFeedback GeminiChatPromptFeedback `json:"promptFeedback"` PromptFeedback *GeminiChatPromptFeedback `json:"promptFeedback,omitempty"`
UsageMetadata GeminiUsageMetadata `json:"usageMetadata"` UsageMetadata GeminiUsageMetadata `json:"usageMetadata"`
} }
type GeminiUsageMetadata struct { type GeminiUsageMetadata struct {
...@@ -325,6 +331,7 @@ type GeminiImageParameters struct { ...@@ -325,6 +331,7 @@ type GeminiImageParameters struct {
SampleCount int `json:"sampleCount,omitempty"` SampleCount int `json:"sampleCount,omitempty"`
AspectRatio string `json:"aspectRatio,omitempty"` AspectRatio string `json:"aspectRatio,omitempty"`
PersonGeneration string `json:"personGeneration,omitempty"` PersonGeneration string `json:"personGeneration,omitempty"`
ImageSize string `json:"imageSize,omitempty"`
} }
type GeminiImageResponse struct { type GeminiImageResponse struct {
......
...@@ -2,11 +2,12 @@ package dto ...@@ -2,11 +2,12 @@ package dto
import ( import (
"encoding/json" "encoding/json"
"one-api/common"
"one-api/types"
"reflect" "reflect"
"strings" "strings"
"github.com/QuantumNous/new-api/common"
"github.com/QuantumNous/new-api/types"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
) )
...@@ -74,14 +75,15 @@ func (r ImageRequest) MarshalJSON() ([]byte, error) { ...@@ -74,14 +75,15 @@ func (r ImageRequest) MarshalJSON() ([]byte, error) {
return nil, err return nil, err
} }
// 不能合并ExtraFields!!!!!!!!
// 合并 ExtraFields // 合并 ExtraFields
for k, v := range r.Extra { //for k, v := range r.Extra {
if _, exists := baseMap[k]; !exists { // if _, exists := baseMap[k]; !exists {
baseMap[k] = v // baseMap[k] = v
} // }
} //}
return json.Marshal(baseMap) return common.Marshal(baseMap)
} }
func GetJSONFieldNames(t reflect.Type) map[string]struct{} { func GetJSONFieldNames(t reflect.Type) map[string]struct{} {
......
...@@ -3,10 +3,11 @@ package dto ...@@ -3,10 +3,11 @@ package dto
import ( import (
"encoding/json" "encoding/json"
"fmt" "fmt"
"one-api/common"
"one-api/types"
"strings" "strings"
"github.com/QuantumNous/new-api/common"
"github.com/QuantumNous/new-api/types"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
) )
...@@ -57,6 +58,18 @@ type GeneralOpenAIRequest struct { ...@@ -57,6 +58,18 @@ type GeneralOpenAIRequest struct {
Dimensions int `json:"dimensions,omitempty"` Dimensions int `json:"dimensions,omitempty"`
Modalities json.RawMessage `json:"modalities,omitempty"` Modalities json.RawMessage `json:"modalities,omitempty"`
Audio json.RawMessage `json:"audio,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 // gemini
ExtraBody json.RawMessage `json:"extra_body,omitempty"` ExtraBody json.RawMessage `json:"extra_body,omitempty"`
//xai //xai
...@@ -75,6 +88,12 @@ type GeneralOpenAIRequest struct { ...@@ -75,6 +88,12 @@ type GeneralOpenAIRequest struct {
WebSearch json.RawMessage `json:"web_search,omitempty"` WebSearch json.RawMessage `json:"web_search,omitempty"`
// doubao,zhipu_v4 // doubao,zhipu_v4
THINKING json.RawMessage `json:"thinking,omitempty"` 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 { func (r *GeneralOpenAIRequest) GetTokenCountMeta() *types.TokenCountMeta {
...@@ -775,19 +794,20 @@ type OpenAIResponsesRequest struct { ...@@ -775,19 +794,20 @@ type OpenAIResponsesRequest struct {
ParallelToolCalls json.RawMessage `json:"parallel_tool_calls,omitempty"` ParallelToolCalls json.RawMessage `json:"parallel_tool_calls,omitempty"`
PreviousResponseID string `json:"previous_response_id,omitempty"` PreviousResponseID string `json:"previous_response_id,omitempty"`
Reasoning *Reasoning `json:"reasoning,omitempty"` Reasoning *Reasoning `json:"reasoning,omitempty"`
ServiceTier string `json:"service_tier,omitempty"` // 服务层级字段,用于指定 API 服务等级。允许透传可能导致实际计费高于预期,默认应过滤
Store json.RawMessage `json:"store,omitempty"` ServiceTier string `json:"service_tier,omitempty"`
PromptCacheKey json.RawMessage `json:"prompt_cache_key,omitempty"` Store json.RawMessage `json:"store,omitempty"`
Stream bool `json:"stream,omitempty"` PromptCacheKey json.RawMessage `json:"prompt_cache_key,omitempty"`
Temperature float64 `json:"temperature,omitempty"` Stream bool `json:"stream,omitempty"`
Text json.RawMessage `json:"text,omitempty"` Temperature float64 `json:"temperature,omitempty"`
ToolChoice json.RawMessage `json:"tool_choice,omitempty"` Text json.RawMessage `json:"text,omitempty"`
Tools json.RawMessage `json:"tools,omitempty"` // 需要处理的参数很少,MCP 参数太多不确定,所以用 map ToolChoice json.RawMessage `json:"tool_choice,omitempty"`
TopP float64 `json:"top_p,omitempty"` Tools json.RawMessage `json:"tools,omitempty"` // 需要处理的参数很少,MCP 参数太多不确定,所以用 map
Truncation string `json:"truncation,omitempty"` TopP float64 `json:"top_p,omitempty"`
User string `json:"user,omitempty"` Truncation string `json:"truncation,omitempty"`
MaxToolCalls uint `json:"max_tool_calls,omitempty"` User string `json:"user,omitempty"`
Prompt json.RawMessage `json:"prompt,omitempty"` MaxToolCalls uint `json:"max_tool_calls,omitempty"`
Prompt json.RawMessage `json:"prompt,omitempty"`
} }
func (r *OpenAIResponsesRequest) GetTokenCountMeta() *types.TokenCountMeta { func (r *OpenAIResponsesRequest) GetTokenCountMeta() *types.TokenCountMeta {
......
...@@ -3,7 +3,8 @@ package dto ...@@ -3,7 +3,8 @@ package dto
import ( import (
"encoding/json" "encoding/json"
"fmt" "fmt"
"one-api/types"
"github.com/QuantumNous/new-api/types"
) )
const ( const (
...@@ -233,6 +234,16 @@ type Usage struct { ...@@ -233,6 +234,16 @@ type Usage struct {
Cost any `json:"cost,omitempty"` 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 { type InputTokenDetails struct {
CachedTokens int `json:"cached_tokens"` CachedTokens int `json:"cached_tokens"`
CachedCreationTokens int `json:"-"` CachedCreationTokens int `json:"-"`
......
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:"meta_data,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"`
}
package dto package dto
import "one-api/constant" import "github.com/QuantumNous/new-api/constant"
// 这里不好动就不动了,本来想独立出来的( // 这里不好动就不动了,本来想独立出来的(
type OpenAIModels struct { type OpenAIModels struct {
......
package dto package dto
import "one-api/types" import "github.com/QuantumNous/new-api/types"
const ( const (
RealtimeEventTypeError = "error" RealtimeEventTypeError = "error"
......
package dto package dto
import ( import (
"github.com/QuantumNous/new-api/types"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
"one-api/types"
) )
type Request interface { type Request interface {
......
...@@ -2,9 +2,10 @@ package dto ...@@ -2,9 +2,10 @@ package dto
import ( import (
"fmt" "fmt"
"github.com/gin-gonic/gin"
"one-api/types"
"strings" "strings"
"github.com/QuantumNous/new-api/types"
"github.com/gin-gonic/gin"
) )
type RerankRequest struct { type RerankRequest struct {
......
...@@ -7,6 +7,9 @@ type UserSetting struct { ...@@ -7,6 +7,9 @@ type UserSetting struct {
WebhookSecret string `json:"webhook_secret,omitempty"` // WebhookSecret webhook密钥 WebhookSecret string `json:"webhook_secret,omitempty"` // WebhookSecret webhook密钥
NotificationEmail string `json:"notification_email,omitempty"` // NotificationEmail 通知邮箱地址 NotificationEmail string `json:"notification_email,omitempty"` // NotificationEmail 通知邮箱地址
BarkUrl string `json:"bark_url,omitempty"` // BarkUrl Bark推送URL 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 是否接受未设置价格的模型 AcceptUnsetRatioModel bool `json:"accept_unset_model_ratio_model,omitempty"` // AcceptUnsetRatioModel 是否接受未设置价格的模型
RecordIpLog bool `json:"record_ip_log,omitempty"` // 是否记录请求和错误日志IP RecordIpLog bool `json:"record_ip_log,omitempty"` // 是否记录请求和错误日志IP
SidebarModules string `json:"sidebar_modules,omitempty"` // SidebarModules 左侧边栏模块配置 SidebarModules string `json:"sidebar_modules,omitempty"` // SidebarModules 左侧边栏模块配置
...@@ -16,4 +19,5 @@ var ( ...@@ -16,4 +19,5 @@ var (
NotifyTypeEmail = "email" // Email 邮件 NotifyTypeEmail = "email" // Email 邮件
NotifyTypeWebhook = "webhook" // Webhook NotifyTypeWebhook = "webhook" // Webhook
NotifyTypeBark = "bark" // Bark 推送 NotifyTypeBark = "bark" // Bark 推送
NotifyTypeGotify = "gotify" // Gotify 推送
) )
# 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/`
#!/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."
\ No newline at end of file
// 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.');
}
\ No newline at end of file
<?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>
\ No newline at end of file
This diff is collapsed. Click to expand it.
This source diff could not be displayed because it is too large. You can view the blob instead.
This source diff could not be displayed because it is too large. You can view the blob instead.
This source diff could not be displayed because it is too large. You can view the blob instead.
This source diff could not be displayed because it is too large. You can view the blob instead.
This source diff could not be displayed because it is too large. You can view the blob instead.
This source diff could not be displayed because it is too large. You can view the blob instead.
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or sign in to comment