Compare commits

..

39 Commits

Author SHA1 Message Date
QuentinHsu 8a44183873 Merge branch 'main' into perf/ui-table
# Conflicts:
#	web/default/src/features/channels/components/channels-table.tsx
2026-06-10 22:20:18 +08:00
QuentinHsu b5d13a6fee fix(provider-badge): unify provider icon spacing
- add a shared provider badge component for icon and status label layout.
- reuse it in channel type and model vendor columns so OpenAI icons align consistently.
2026-06-10 22:10:44 +08:00
QuentinHsu ac694fbc9f fix(table): prevent admin list column overflow
- widen redemption and subscription table columns so masked codes, timestamps, and localized headers fit.
- localize subscription ID headers and add Received amount translations across supported locales.
2026-06-10 21:52:14 +08:00
QuentinHsu 0a8fcb450e fix(table): align table cell content with headers
- remove extra inline padding from masked table text buttons so values start at the cell edge.
- tag status badges and offset leading badges inside table cells to match header text alignment.
2026-06-10 21:40:24 +08:00
QuentinHsu c57009ffae fix(data-table): prevent narrow column overlap
- apply stable header sizing to remaining desktop data table pages so constrained layouts scroll instead of compressing cells.
- add explicit widths for key, quota, badge, and timestamp columns that contain fixed-format content.
- constrain masked values and timestamp cells with truncation to keep content inside its assigned column.
2026-06-10 21:26:23 +08:00
QuentinHsu d58ddf2441 fix(data-table): make pinned edit column opaque
- use an opaque muted background for the active action column so sticky cells do not reveal scrolled content underneath.
2026-06-10 21:13:08 +08:00
QuentinHsu 6799f27fe5 refactor(data-table): tighten static table modes
- make StaticDataTable distinguish data-driven and children-only usage through explicit prop shapes.
- remove unsupported columns-without-data fallback after confirming no repository callers rely on it.
- default manual table modes away from unused local row models to reduce repeated table work.
2026-06-10 21:02:18 +08:00
QuentinHsu 40d0d6a82f perf(data-table): cache pinned column class resolution
- reuse the pinned column lookup while table props stay stable to reduce repeated per-render work.
- share the resolved column class handler across unified and split-header table layouts.
- localize page-number screen reader labels so pagination remains accessible in every locale.
2026-06-10 14:40:42 +08:00
QuentinHsu 9691ca06d1 fix(web): prevent user invite info overlap
- give the invite info and created-at columns explicit widths so table sizing reserves enough space.
- allow invite badges to wrap within the cell instead of spilling into adjacent columns.
2026-06-10 10:36:57 +08:00
QuentinHsu 445a87c3f3 fix(status-badge): hide status dot by default 2026-06-10 10:35:43 +08:00
QuentinHsu 823418ba36 fix(web): align model metadata icon cells
- render compact provider avatars in the metadata icon column instead of wide wordmarks.
- position icons in a fixed-size wrapper so they line up with the existing icon header alignment.
2026-06-10 10:28:52 +08:00
QuentinHsu 0cec454fc4 fix(web): set stable table utility column widths
- assign fixed widths to selection columns so shared colgroup sizing keeps checkbox cells compact.
- size id columns in redemption and user tables to keep split headers aligned with body rows.
2026-06-10 10:13:43 +08:00
QuentinHsu 7efe325dc4 fix(web): stabilize split table column sizing
- derive default colgroup widths from visible columns when split headers or header sizing are enabled.
- apply a fixed table layout with computed minimum width so header and body columns stay aligned.
- keep split-header containers from leaking horizontal overflow and avoid extra pinned-column borders.
2026-06-10 10:12:15 +08:00
QuentinHsu 9b1fc293fa refactor(data-table): organize shared table components
- group table primitives, page composition, toolbar controls, static tables, and hooks by responsibility.
- split shared view types, row rendering, header rendering, and pinned-column styling out of the main table view.
- keep the public data-table barrel stable while documenting the new ownership boundaries.
2026-06-10 09:44:23 +08:00
QuentinHsu d73f6b492f fix(web): keep pinned table columns opaque
- apply pinned column background classes after custom column classes.
- use an opaque hover background so scrolled content cannot show through fixed cells.
2026-06-10 09:24:26 +08:00
QuentinHsu 990ec72bda refactor(web): remove stale long text lint override 2026-06-10 09:18:00 +08:00
QuentinHsu 33d87e6ab1 refactor(web): hide data table view props from barrel 2026-06-10 09:15:21 +08:00
QuentinHsu f8f7716be6 refactor(web): keep static table empty row private
- stop exporting the internal StaticDataTableEmptyRow helper.
- keep the public static table API focused on the table component and column type.
2026-06-10 09:12:43 +08:00
QuentinHsu 2d978cc314 refactor(web): trim data table hook return API
- return only the TanStack table instance from useDataTable.
- keep internal state handling private because callers do not consume it directly.
2026-06-10 09:11:12 +08:00
QuentinHsu 503447103c fix(web): remove direct hast type dependency
- rely on Shiki transformer contextual typing for line nodes.
- allow frontend typecheck to pass without an undeclared hast package.
2026-06-10 09:09:35 +08:00
QuentinHsu 6df10dcebb refactor(web): extract table page pagination rendering 2026-06-10 08:42:29 +08:00
QuentinHsu 4835abfda8 refactor(web): clarify static table body rows 2026-06-10 08:40:55 +08:00
QuentinHsu d64e09bb19 refactor(web): hoist pagination size select items 2026-06-10 08:40:04 +08:00
QuentinHsu d26f277e70 refactor(web): reuse pagination state values 2026-06-10 08:39:20 +08:00
QuentinHsu f78a7973e2 refactor(web): rely on table view row defaults 2026-06-10 08:38:11 +08:00
QuentinHsu 2f6edabc97 refactor(web): reuse model ratio row state 2026-06-10 08:37:10 +08:00
QuentinHsu 9274edc409 refactor(web): simplify tiered pricing select items 2026-06-10 08:36:01 +08:00
QuentinHsu d380ed8ccd refactor(web): merge channel selector table imports 2026-06-10 08:35:01 +08:00
QuentinHsu e861caf2f0 refactor(web): merge upstream ratio table imports 2026-06-10 08:34:10 +08:00
QuentinHsu 767020d6e2 refactor(web): merge pricing table imports 2026-06-10 08:33:26 +08:00
QuentinHsu 9190895708 refactor(web): streamline pricing table rendering
- reuse translated endpoint select options between trigger data and menu items.
- precompute dynamic pricing maps per group so table cells only resolve formatted values.
- add local dynamic pricing type aliases to keep helper signatures readable.
2026-06-10 08:30:44 +08:00
QuentinHsu e6f910e329 perf(pricing): reduce dynamic pricing table render work
- reuse dynamic pricing field metadata instead of rebuilding it inside table columns.
- precompute formatted dynamic prices per tier and group to avoid repeated entry mapping for each cell.
- simplify select option construction in related dialogs while preserving the same choices.
2026-06-10 08:24:59 +08:00
QuentinHsu 895fae66ff refactor(web): simplify data table rendering internals
- split table body rendering into focused helpers for loading, empty, and row states.
- extract static table row and cell class resolution to reduce branching in the main component.
- reuse a single pagination page-size option list to avoid duplicated constants.
2026-06-09 22:14:51 +08:00
QuentinHsu 5306e640f4 perf(web): stabilize model pricing table columns
- keep model pricing columns at fixed widths so headers do not collapse in narrow layouts.
- truncate long model names and pricing summaries within their cells instead of squeezing adjacent columns.
2026-06-09 21:59:22 +08:00
QuentinHsu 8fb8cacae8 perf(web): refine table pagination controls
- show total row counts instead of redundant page range text.
- tighten visible page buttons so pagination fits constrained table widths.
- align pagination controls and tune text hierarchy for clearer scanning.
2026-06-09 21:50:42 +08:00
QuentinHsu a1f7256a05 perf(web): keep list tables fixed within page content
- make shared data table pages fill available height and scroll row data inside the table body.
- add a fixed content layout mode so selected list pages avoid page-level scrolling.
- apply the fixed table behavior to keys, logs, channels, models, users, redemptions, and subscriptions.
2026-06-09 21:22:55 +08:00
QuentinHsu 0863ddc3d9 refactor(web): unify table rendering components
- centralize static table headers, bodies, empty states, and shared class names behind the data-table package.
- migrate settings, pricing, channel, key, subscription, and model tables to the shared table APIs.
- remove data-table exports for low-level table primitives so feature code uses one supported abstraction.
2026-06-09 21:08:13 +08:00
QuentinHsu 04c0ae7aa8 refactor(web): trim data table public API
- remove unused data-table exports and dead static table helper types.
- keep internal table header, skeleton, empty state, and faceted filter helpers private to the data-table module.
- route feature imports through the data-table barrel to avoid subpath coupling.
2026-06-09 15:00:01 +08:00
QuentinHsu dc6aea065a refactor(web): centralize data table implementation
- route all TanStack table setup through a shared data-table hook to remove repeated state and row model wiring.
- move table rendering, static table wrappers, empty states, and primitive exports behind the data-table module.
- update feature tables and configuration editors to share the same table UX while preserving their existing workflows.
2026-06-09 14:52:02 +08:00
1359 changed files with 5222 additions and 7758 deletions
-92
View File
@@ -1,92 +0,0 @@
name: Docker Build
on:
push:
branches:
- main
- master
tags:
- 'v*'
workflow_dispatch:
jobs:
build-and-push:
name: Build and Push Docker Image
runs-on: act-runner-4c6g
env:
RUNNER_TOOL_CACHE: /toolcache
steps:
- name: Install Docker CLI
run: |
if ! command -v docker &> /dev/null; then
if command -v apk &> /dev/null; then
apk add --no-cache docker-cli
elif command -v apt-get &> /dev/null; then
apt-get update && apt-get install -y docker.io
else
curl -fsSL https://download.docker.com/linux/static/stable/x86_64/docker-24.0.7.tgz | tar xz -C /tmp
mv /tmp/docker/docker /usr/local/bin/
chmod +x /usr/local/bin/docker
fi
fi
docker --version
- name: Checkout repository
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Resolve tag & write VERSION
id: version
run: |
if echo "${{ github.ref }}" | grep -q "^refs/tags/"; then
TAG=${GITHUB_REF#refs/tags/}
else
SHORT_SHA=$(git rev-parse --short HEAD)
TAG="dev-${SHORT_SHA}"
fi
echo "TAG=${TAG}" >> $GITHUB_ENV
echo "${TAG}" > VERSION
echo "Building tag: ${TAG}"
cat VERSION
- name: Login to Gitea Container Registry
run: |
echo "${{ secrets.PACKAGES_TOKEN }}" | docker login git.viaeon.com -u "${{ github.actor }}" --password-stdin
- name: Build Docker image
run: |
echo "Building image with tag: ${{ env.TAG }}"
docker build \
--label "org.opencontainers.image.source=https://git.viaeon.com/admin/new-api" \
--label "org.opencontainers.image.revision=${{ github.sha }}" \
-t git.viaeon.com/admin/new-api:${{ env.TAG }} \
-t git.viaeon.com/admin/new-api:latest .
- name: Push Docker image
run: |
echo "Pushing ${{ env.TAG }}..."
docker push git.viaeon.com/admin/new-api:${{ env.TAG }}
echo "Pushing latest..."
docker push git.viaeon.com/admin/new-api:latest
- name: Cleanup Docker
if: always()
run: |
echo "Removing local images..."
docker rmi git.viaeon.com/admin/new-api:${{ env.TAG }} git.viaeon.com/admin/new-api:latest 2>/dev/null || true
echo "Pruning unused Docker resources..."
docker system prune -af --volumes 2>/dev/null || true
echo "Docker disk usage:"
docker system df
- name: Deploy via SSH
if: success()
run: |
if [ -z "${{ secrets.DEPLOY_SSH_HOST }}" ]; then
echo "DEPLOY_SSH_HOST not set, skip deploy"
exit 0
fi
apk add --no-cache sshpass 2>/dev/null || apt-get update && apt-get install -y sshpass 2>/dev/null || true
sshpass -p "${{ secrets.DEPLOY_SSH_PASS }}" ssh -o StrictHostKeyChecking=no -p ${{ secrets.DEPLOY_SSH_PORT || 22 }} ${{ secrets.DEPLOY_SSH_USER }}@${{ secrets.DEPLOY_SSH_HOST }} "cd ${{ secrets.DEPLOY_DIR || '/opt/new-api' }} && docker compose pull && docker compose up -d"
-73
View File
@@ -1,73 +0,0 @@
name: Docker Build (alpha)
on:
push:
branches:
- alpha
workflow_dispatch:
jobs:
build-and-push:
name: Build and Push Alpha Docker Image
runs-on: act-runner-4c6g
env:
RUNNER_TOOL_CACHE: /toolcache
steps:
- name: Install Docker CLI
run: |
if ! command -v docker &> /dev/null; then
if command -v apk &> /dev/null; then
apk add --no-cache docker-cli
elif command -v apt-get &> /dev/null; then
apt-get update && apt-get install -y docker.io
else
curl -fsSL https://download.docker.com/linux/static/stable/x86_64/docker-24.0.7.tgz | tar xz -C /tmp
mv /tmp/docker/docker /usr/local/bin/
chmod +x /usr/local/bin/docker
fi
fi
docker --version
- name: Checkout repository
uses: actions/checkout@v4
with:
fetch-depth: 1
- name: Determine alpha version
id: version
run: |
VERSION="alpha-$(date +'%Y%m%d')-$(git rev-parse --short HEAD)"
echo "$VERSION" > VERSION
echo "VERSION=$VERSION" >> $GITHUB_ENV
echo "Publishing version: $VERSION"
- name: Login to Gitea Container Registry
run: |
echo "${{ secrets.PACKAGES_TOKEN }}" | docker login git.viaeon.com -u "${{ github.actor }}" --password-stdin 2>&1
- name: Build Docker image
run: |
echo "Building alpha image..."
docker build \
--label "org.opencontainers.image.source=https://git.viaeon.com/admin/new-api" \
--label "org.opencontainers.image.revision=${{ github.sha }}" \
-t git.viaeon.com/admin/new-api:${{ env.VERSION }} \
-t git.viaeon.com/admin/new-api:alpha . 2>&1
- name: Push Docker image
run: |
echo "Pushing ${{ env.VERSION }}..."
docker push git.viaeon.com/admin/new-api:${{ env.VERSION }}
echo "Pushing alpha..."
docker push git.viaeon.com/admin/new-api:alpha
- name: Cleanup Docker
if: always()
run: |
echo "Removing local images..."
docker rmi git.viaeon.com/admin/new-api:${{ env.VERSION }} git.viaeon.com/admin/new-api:alpha 2>/dev/null || true
echo "Pruning unused Docker resources..."
docker system prune -af --volumes 2>/dev/null || true
echo "Docker disk usage:"
docker system df
@@ -1,73 +0,0 @@
name: Docker Build (nightly)
on:
push:
branches:
- nightly
workflow_dispatch:
jobs:
build-and-push:
name: Build and Push Nightly Docker Image
runs-on: act-runner-4c6g
env:
RUNNER_TOOL_CACHE: /toolcache
steps:
- name: Install Docker CLI
run: |
if ! command -v docker &> /dev/null; then
if command -v apk &> /dev/null; then
apk add --no-cache docker-cli
elif command -v apt-get &> /dev/null; then
apt-get update && apt-get install -y docker.io
else
curl -fsSL https://download.docker.com/linux/static/stable/x86_64/docker-24.0.7.tgz | tar xz -C /tmp
mv /tmp/docker/docker /usr/local/bin/
chmod +x /usr/local/bin/docker
fi
fi
docker --version
- name: Checkout repository
uses: actions/checkout@v4
with:
fetch-depth: 1
- name: Determine nightly version
id: version
run: |
VERSION="nightly-$(date +'%Y%m%d')-$(git rev-parse --short HEAD)"
echo "$VERSION" > VERSION
echo "VERSION=$VERSION" >> $GITHUB_ENV
echo "Publishing version: $VERSION"
- name: Login to Gitea Container Registry
run: |
echo "${{ secrets.PACKAGES_TOKEN }}" | docker login git.viaeon.com -u "${{ github.actor }}" --password-stdin 2>&1
- name: Build Docker image
run: |
echo "Building nightly image..."
docker build \
--label "org.opencontainers.image.source=https://git.viaeon.com/admin/new-api" \
--label "org.opencontainers.image.revision=${{ github.sha }}" \
-t git.viaeon.com/admin/new-api:${{ env.VERSION }} \
-t git.viaeon.com/admin/new-api:nightly . 2>&1
- name: Push Docker image
run: |
echo "Pushing ${{ env.VERSION }}..."
docker push git.viaeon.com/admin/new-api:${{ env.VERSION }}
echo "Pushing nightly..."
docker push git.viaeon.com/admin/new-api:nightly
- name: Cleanup Docker
if: always()
run: |
echo "Removing local images..."
docker rmi git.viaeon.com/admin/new-api:${{ env.VERSION }} git.viaeon.com/admin/new-api:nightly 2>/dev/null || true
echo "Pruning unused Docker resources..."
docker system prune -af --volumes 2>/dev/null || true
echo "Docker disk usage:"
docker system df
-82
View File
@@ -1,82 +0,0 @@
name: PR Check
on:
pull_request:
types: [opened, reopened]
jobs:
pr-quality:
name: PR Quality Check
runs-on: act-runner-4c6g
env:
RUNNER_TOOL_CACHE: /toolcache
steps:
- name: Checkout repository
uses: actions/checkout@v4
- name: Check PR description
env:
GITEA_TOKEN: ${{ secrets.PACKAGES_TOKEN }}
PR_NUMBER: ${{ github.event.pull_request.number }}
REPO: admin/new-api
GITEA_URL: https://git.viaeon.com
run: |
# 获取 PR 信息
PR_INFO=$(curl -s -H "Authorization: token ${GITEA_TOKEN}" \
"${GITEA_URL}/api/v1/repos/${REPO}/pulls/${PR_NUMBER}")
PR_BODY=$(echo "$PR_INFO" | jq -r '.body // empty')
PR_TITLE=$(echo "$PR_INFO" | jq -r '.title // empty')
PR_USER=$(echo "$PR_INFO" | jq -r '.user.login // empty')
FAILED=0
REASONS=""
# 检查 PR 描述是否为空
if [ -z "$PR_BODY" ] || [ "$PR_BODY" = "null" ]; then
FAILED=1
REASONS="${REASONS}- PR description is empty\n"
fi
# 检查 PR 标题是否为空
if [ -z "$PR_TITLE" ] || [ "$PR_TITLE" = "null" ]; then
FAILED=1
REASONS="${REASONS}- PR title is empty\n"
fi
# 检查是否包含纯 AI 生成标记
if echo "$PR_BODY" | grep -qi "Generated with Claude Code"; then
FAILED=1
REASONS="${REASONS}- PR appears to be purely AI-generated without meaningful human involvement\n"
fi
if [ "$FAILED" -eq 1 ]; then
echo "PR check failed:"
echo -e "$REASONS"
# 添加标签
curl -s -X POST \
"${GITEA_URL}/api/v1/repos/${REPO}/issues/${PR_NUMBER}/labels" \
-H "Authorization: token ${GITEA_TOKEN}" \
-H "Content-Type: application/json" \
-d '{"labels": ["pr-check-failed"]}'
# 添加评论
curl -s -X POST \
"${GITEA_URL}/api/v1/repos/${REPO}/issues/${PR_NUMBER}/comments" \
-H "Authorization: token ${GITEA_TOKEN}" \
-H "Content-Type: application/json" \
-d '{"body": "感谢您的提交。由于该 PR 未遵循我们的贡献模板,且被识别为缺乏人工参与的纯 AI 生成内容,我们将先予以关闭。我们更欢迎经过人工审核、验证并带有个人思考的贡献。如果您认为这其中存在误解,请回复告知。"}'
# 关闭 PR
curl -s -X PATCH \
"${GITEA_URL}/api/v1/repos/${REPO}/pulls/${PR_NUMBER}" \
-H "Authorization: token ${GITEA_TOKEN}" \
-H "Content-Type: application/json" \
-d '{"state": "closed"}'
exit 1
fi
echo "PR check passed!"
-161
View File
@@ -1,161 +0,0 @@
name: Release (Linux)
on:
push:
tags:
- 'v*'
- '!*-alpha*'
workflow_dispatch:
inputs:
tag:
description: 'Tag name to build (e.g., v0.10.8)'
required: true
type: string
jobs:
build-linux:
name: Linux Release
runs-on: act-runner-4c6g
env:
RUNNER_TOOL_CACHE: /toolcache
steps:
- name: Install dependencies
run: |
export PATH="/toolcache/bin:$PATH"
# Install Go
if ! command -v go &> /dev/null; then
curl -fsSL https://go.dev/dl/go1.25.1.linux-amd64.tar.gz | tar -C /usr/local -xzf -
echo "export PATH=\$PATH:/usr/local/go/bin" >> ~/.bashrc
export PATH=$PATH:/usr/local/go/bin
fi
go version
# Install Bun
if ! command -v bun &> /dev/null; then
curl -fsSL https://bun.sh/install | bash
export PATH="$HOME/.bun/bin:$PATH"
fi
bun --version
- name: Checkout repository
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Determine Version
run: |
if [ -n "${{ github.event.inputs.tag }}" ]; then
TAG="${{ github.event.inputs.tag }}"
else
TAG=${GITHUB_REF#refs/tags/}
fi
VERSION=$(git describe --tags 2>/dev/null || echo "$TAG")
echo "VERSION=$VERSION" >> $GITHUB_ENV
echo "Building version: $VERSION"
- name: Build Frontend (default)
env:
CI: ""
run: |
export PATH="$HOME/.bun/bin:/usr/local/go/bin:$PATH"
cd web
bun install --frozen-lockfile
cd default
DISABLE_ESLINT_PLUGIN='true' VITE_REACT_APP_VERSION=$VERSION bun run build
cd ../..
- name: Build Frontend (classic)
env:
CI: ""
run: |
export PATH="$HOME/.bun/bin:/usr/local/go/bin:$PATH"
cd web
bun install --frozen-lockfile
cd classic
VITE_REACT_APP_VERSION=$VERSION bun run build
cd ../..
- name: Build Backend (amd64)
run: |
export PATH="/usr/local/go/bin:$PATH"
go mod download
go build -ldflags "-s -w -X 'new-api/common.Version=$VERSION' -extldflags '-static'" -o new-api-$VERSION
- name: Build Backend (arm64)
run: |
export PATH="/usr/local/go/bin:$PATH"
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 'new-api/common.Version=$VERSION' -extldflags '-static'" -o new-api-arm64-$VERSION
- name: Generate checksums
run: sha256sum new-api-* > checksums-linux.txt
- name: Upload artifacts
uses: actions/upload-artifact@v4
with:
name: linux-build
path: |
new-api-*
checksums-linux.txt
release:
name: Create Gitea Release
needs: [build-linux]
runs-on: act-runner-4c6g
env:
RUNNER_TOOL_CACHE: /toolcache
steps:
- name: Checkout repository
uses: actions/checkout@v4
- name: Determine Version
run: |
if [ -n "${{ github.event.inputs.tag }}" ]; then
TAG="${{ github.event.inputs.tag }}"
else
TAG=${GITHUB_REF#refs/tags/}
fi
echo "TAG=$TAG" >> $GITHUB_ENV
- name: Download artifacts
uses: actions/download-artifact@v4
with:
path: artifacts
- name: Create Gitea Release
env:
GITEA_TOKEN: ${{ secrets.PACKAGES_TOKEN }}
run: |
# 使用 Gitea API 创建 Release
TAG="${{ env.TAG }}"
REPO="admin/new-api"
GITEA_URL="https://git.viaeon.com"
# 创建 Release
RELEASE_ID=$(curl -s -X POST \
"${GITEA_URL}/api/v1/repos/${REPO}/releases" \
-H "Authorization: token ${GITEA_TOKEN}" \
-H "Content-Type: application/json" \
-d "{
\"tag_name\": \"${TAG}\",
\"name\": \"${TAG}\",
\"body\": \"Release ${TAG}\",
\"draft\": false,
\"prerelease\": false
}" | jq -r '.id')
echo "Created release ID: ${RELEASE_ID}"
# 上传附件
find artifacts -type f | while read file; do
echo "Uploading: ${file}"
curl -s -X POST \
"${GITEA_URL}/api/v1/repos/${REPO}/releases/${RELEASE_ID}/assets" \
-H "Authorization: token ${GITEA_TOKEN}" \
-F "attachment=@${file}" \
-F "name=$(basename ${file})"
done
echo "Release ${TAG} created successfully!"
+1 -2
View File
@@ -7,10 +7,9 @@ upload
*.db
build
*.db-journal
/logs
logs
web/default/dist
web/classic/dist
web/daisy/dist
web/node_modules
web/dist
.env
-170
View File
@@ -1,170 +0,0 @@
# ModelsToken 管理平台 - 产品需求文档 (PRD)
## 1. 产品概述
ModelsToken 是一个 AI API 管理与分发平台,为开发者和企业提供统一的 AI 模型接入、密钥管理、用量计费、渠道代理等一站式服务。新前端将采用 React + DaisyUI 5 + TypeScript 构建,替换现有的 Default/Classic 双前端,同时新增本地文档管理功能。
- 目标用户:AI 应用开发者、企业运维人员、API 服务管理者
- 核心价值:简化 AI API 的管理复杂度,提供直观的操作界面和完整的文档支持
## 2. 核心功能
### 2.1 用户角色
| 角色 | 注册方式 | 核心权限 |
|------|----------|----------|
| 普通用户 | 用户名/邮箱/OAuth | 密钥管理、充值、订阅、日志查看、文档访问 |
| 管理员 | 由超级管理员指定 | 渠道管理、用户管理、兑换码、模型管理、订阅管理 |
| 超级管理员 | 系统初始化 | 全部权限 + 系统设置 |
### 2.2 功能模块
#### 公共页面(无需登录)
1. **首页**:Hero 区域、特性展示、快速入门指引
2. **登录页**:用户名/密码、OAuth 登录(GitHub/Discord/OIDC/LinuxDO/微信/Telegram/自定义)
3. **注册页**:注册表单 + Turnstile 人机验证
4. **忘记密码**:邮箱重置链接
5. **模型定价**:模型价格列表、搜索筛选
6. **关于页面**:项目信息、版本、许可证
7. **用户协议/隐私政策**
8. **初始化向导**:首次部署配置
#### 用户功能(需登录)
1. **仪表盘**:额度概览、使用趋势图、API 信息面板、公告、FAQ
2. **API 密钥管理**:创建/编辑/删除/批量操作、额度限制、模型限制、IP 限制
3. **钱包/充值**:余额查看、兑换码充值、在线支付(易支付/Stripe/Creem/Waffo)、签到
4. **订阅管理**:查看计划、购买订阅、当前订阅状态
5. **使用日志**:请求日志搜索/筛选、MJ 日志、任务日志、统计图表
6. **个人设置**:资料编辑、2FA 设置、Passkey 管理、OAuth 绑定、语言切换
7. **Playground**API 在线调试、Chat Completions 测试
8. **文档中心**(新增):本地文档管理、分类浏览、搜索、Markdown 渲染
#### 管理员功能
1. **渠道管理**:CRUD、测试、余额更新、标签管理、批量操作、多密钥、Codex OAuth、Ollama 管理
2. **用户管理**:列表/搜索/创建/编辑/升降级/启禁/额度调整
3. **兑换码管理**CRUD、批量删除无效码
4. **模型管理**:模型元数据 CRUD、上游同步、缺失模型检测
5. **供应商管理**CRUD
6. **订阅管理**:计划 CRUD、用户订阅管理
7. **部署管理**io.net 部署 CRUD、容器管理、日志
#### 超级管理员 - 系统设置
1. **站点设置**:名称/Logo/页脚/公告/首页内容/服务器地址
2. **认证设置**:注册/登录开关、OAuth 配置、Turnstile、Passkey、自定义 OAuth
3. **计费设置**:额度/倍率/支付配置/签到/分组倍率
4. **内容设置**:公告/FAQ/Uptime Kuma/聊天/绘图/Midjourney
5. **模型设置**:透传/思维模型/Gemini/Claude 配置
6. **运维设置**:重试/自动禁用/SMTP/性能监控/日志
7. **安全设置**:速率限制/敏感词/SSRF 防护/IP 过滤
### 2.3 新增功能 - 本地文档管理
| 功能 | 说明 |
|------|------|
| 文档分类 | 支持多级分类树,管理员可创建/编辑/删除分类 |
| 文档 CRUD | 管理员创建/编辑/删除文档,支持 Markdown 编辑器 |
| 文档浏览 | 用户按分类浏览文档,支持搜索 |
| 文档搜索 | 全文搜索文档标题和内容 |
| 文档版本 | 文档更新历史记录 |
| 权限控制 | 可设置文档为公开/登录可见/管理员可见 |
## 3. 核心流程
### 3.1 用户认证流程
```mermaid
flowchart TD
"访问平台" --> "已登录?"
"已登录?" -->|"是"| "仪表盘"
"已登录?" -->|"否"| "登录页"
"登录页" --> "输入凭证"
"输入凭证" --> "需要2FA?"
"需要2FA?" -->|"是"| "输入2FA码"
"需要2FA?" -->|"否"| "验证成功"
"输入2FA码" --> "验证成功"
"验证成功" --> "仪表盘"
"登录页" --> "OAuth登录"
"OAuth登录" --> "OAuth回调"
"OAuth回调" --> "已绑定账号?"
"已绑定账号?" -->|"是"| "仪表盘"
"已绑定账号?" -->|"否"| "绑定/注册"
```
### 3.2 API 调用流程
```mermaid
flowchart TD
"创建API密钥" --> "配置密钥参数"
"配置密钥参数" --> "使用密钥调用API"
"使用密钥调用API" --> "平台路由到渠道"
"平台路由到渠道" --> "返回结果"
"返回结果" --> "记录日志"
"记录日志" --> "扣除额度"
```
### 3.3 文档管理流程(新增)
```mermaid
flowchart TD
"管理员创建分类" --> "创建文档"
"创建文档" --> "Markdown编辑"
"Markdown编辑" --> "设置可见性"
"设置可见性" --> "发布文档"
"发布文档" --> "用户浏览/搜索"
```
## 4. 用户界面设计
### 4.1 设计风格
- **主色调**:深蓝 (#1e293b) + 亮蓝 (#3b82f6) 渐变,搭配 DaisyUI 的 `business` 主题
- **辅助色**:翡翠绿 (#10b981) 用于成功/在线状态,琥珀色 (#f59e0b) 用于警告
- **按钮风格**:DaisyUI 默认圆角按钮,主要操作用 `btn-primary`,危险操作用 `btn-error`
- **字体**JetBrains Mono(代码/密钥)+ Noto Sans SC(中文正文)
- **布局风格**:左侧固定导航栏 + 顶部状态栏 + 主内容区,响应式折叠
- **图标**Lucide React 图标库
- **动效**DaisyUI 内置动画 + 页面切换淡入
### 4.2 页面设计概览
| 页面 | 模块 | UI 元素 |
|------|------|---------|
| 首页 | Hero | 渐变背景、特性卡片、快速开始按钮 |
| 登录 | 表单 | 居中卡片、OAuth 按钮组、Turnstile |
| 仪表盘 | 统计卡片 | 4 列额度卡片、折线图、公告栏、API 信息 |
| 密钥管理 | 数据表 | 搜索栏、筛选器、表格、批量操作栏 |
| 渠道管理 | 数据表+表单 | 标签筛选、测试按钮、多密钥管理抽屉 |
| 系统设置 | 标签页 | 7 大分类侧边导航、表单分组、开关/输入框 |
| 文档中心 | 侧边树+内容 | 分类树导航、Markdown 渲染、搜索框、面包屑 |
| Playground | 分栏 | 左侧参数面板、右侧响应面板、模型选择器 |
### 4.3 响应式设计
- 桌面优先(1280px+
- 平板适配(768px-1279px):侧边栏折叠为抽屉
- 移动端适配(<768px):单列布局,表格改为卡片列表
### 4.4 布局结构
```
┌──────────────────────────────────────────────┐
│ 顶部导航栏 (Navbar) │
│ Logo | 搜索 | 通知 | 用户菜单 | 主题切换 │
├──────┬───────────────────────────────────────┤
│ │ │
│ 侧边 │ 主内容区 │
│ 导航 │ │
│ 栏 │ ┌─────────────────────────────────┐ │
│ │ │ 面包屑 + 页面标题 + 操作按钮 │ │
│ 仪表盘│ ├─────────────────────────────────┤ │
│ 密钥 │ │ │ │
│ 渠道 │ │ 页面内容 │ │
│ 用户 │ │ │ │
│ 日志 │ │ │ │
│ 钱包 │ └─────────────────────────────────┘ │
│ 订阅 │ │
│ 文档 │ │
│ 设置 │ │
│ │ │
└──────┴───────────────────────────────────────┘
```
-459
View File
@@ -1,459 +0,0 @@
# ModelsToken 管理平台 - 技术架构文档
## 1. 架构设计
```mermaid
flowchart TB
subgraph "前端 (React + DaisyUI 5)"
A["React 18"] --> B["React Router v6"]
B --> C["页面组件"]
C --> D["DaisyUI 5 组件"]
D --> E["Tailwind CSS 4"]
A --> F["Zustand 状态管理"]
A --> G["React Query 数据请求"]
A --> H["i18next 国际化"]
A --> I["React Markdown 渲染"]
end
subgraph "后端 (Go + Gin)"
J["Gin HTTP Server"]
J --> K["API 路由"]
J --> L["Relay 代理"]
K --> M["控制器"]
M --> N["模型层"]
N --> O["数据库 (SQLite/MySQL/PostgreSQL)"]
end
C -->|"Axios HTTP"| K
```
## 2. 技术说明
- **前端框架**React 18 + TypeScript
- **UI 库**DaisyUI 5 + Tailwind CSS 4
- **构建工具**Vite 6
- **路由**React Router v6(懒加载)
- **状态管理**Zustand(轻量级,替代 Redux
- **数据请求**TanStack React Query v5 + Axios
- **国际化**i18next + react-i18next
- **图表**Recharts
- **Markdown**react-markdown + remark-gfm + rehype-highlight
- **图标**Lucide React
- **代码高亮**highlight.js
- **表单验证**React Hook Form + Zod
- **通知**react-hot-toast
- **项目目录**`web/daisy/`
## 3. 路由定义
### 3.1 公共路由
| 路由 | 用途 |
|------|------|
| `/` | 首页 |
| `/login` | 登录 |
| `/register` | 注册 |
| `/forgot-password` | 忘记密码 |
| `/reset-password` | 密码重置确认 |
| `/setup` | 初始化向导 |
| `/pricing` | 模型定价 |
| `/about` | 关于 |
| `/user-agreement` | 用户协议 |
| `/privacy-policy` | 隐私政策 |
| `/oauth/callback/:provider` | OAuth 回调 |
### 3.2 认证后路由
| 路由 | 用途 |
|------|------|
| `/dashboard` | 仪表盘 |
| `/tokens` | API 密钥管理 |
| `/wallet` | 钱包/充值 |
| `/subscriptions` | 订阅管理 |
| `/logs` | 使用日志 |
| `/logs/midjourney` | MJ 日志 |
| `/logs/tasks` | 任务日志 |
| `/profile` | 个人设置 |
| `/playground` | Playground |
| `/docs` | 文档中心(新增) |
| `/docs/:slug` | 文档详情(新增) |
### 3.3 管理员路由
| 路由 | 用途 |
|------|------|
| `/admin/channels` | 渠道管理 |
| `/admin/users` | 用户管理 |
| `/admin/redemptions` | 兑换码管理 |
| `/admin/models` | 模型管理 |
| `/admin/vendors` | 供应商管理 |
| `/admin/deployments` | 部署管理 |
| `/admin/subscriptions` | 订阅计划管理 |
### 3.4 超级管理员路由
| 路由 | 用途 |
|------|------|
| `/settings/site` | 站点设置 |
| `/settings/auth` | 认证设置 |
| `/settings/billing` | 计费设置 |
| `/settings/content` | 内容设置 |
| `/settings/models` | 模型设置 |
| `/settings/operations` | 运维设置 |
| `/settings/security` | 安全设置 |
| `/settings/docs` | 文档管理(新增) |
## 4. API 定义
### 4.1 核心类型
```typescript
// 用户
interface User {
id: number;
username: string;
display_name: string;
email: string;
role: number; // 1=user, 10=admin, 100=root
status: number;
quota: number;
used_quota: number;
request_count: number;
group: string;
aff_code: string;
inviter_id: number;
language: string;
access_token: string;
created_time: number;
}
// 渠道
interface Channel {
id: number;
type: number;
key: string;
openai_organization?: string;
base_url: string;
models: string;
model_mapping?: string;
group: string;
groups: string[];
name: string;
priority: number;
weight: number;
status: number;
tag?: string;
setting?: string;
test_time: number;
response_time: number;
balance: number;
balance_updated_time: number;
created_time: number;
}
// 令牌
interface Token {
id: number;
user_id: number;
key: string;
status: number;
name: string;
created_time: number;
accessed_time: number;
expired_time: number;
remain_quota: number;
unlimited_quota: boolean;
used_quota: number;
models: string;
subnet: string;
group: string;
}
// 日志
interface Log {
id: number;
user_id: number;
created_at: number;
type: number;
content: string;
username: string;
token_name: string;
model_name: string;
quota: number;
prompt_tokens: number;
completion_tokens: number;
channel_id: number;
token_id: number;
group: string;
request_id: string;
ip: string;
detail: string;
}
// 订阅计划
interface SubscriptionPlan {
id: number;
name: string;
description: string;
price: number;
currency: string;
duration_days: number;
quota: number;
models: string;
enabled: boolean;
sort_order: number;
created_time: number;
}
// 文档(新增)
interface Document {
id: number;
title: string;
slug: string;
content: string; // Markdown
category_id: number;
category?: DocumentCategory;
visibility: 'public' | 'auth' | 'admin';
sort_order: number;
created_at: string;
updated_at: string;
author_id: number;
author?: User;
versions?: DocumentVersion[];
}
interface DocumentCategory {
id: number;
name: string;
slug: string;
parent_id: number | null;
children?: DocumentCategory[];
sort_order: number;
}
interface DocumentVersion {
id: number;
document_id: number;
content: string;
created_at: string;
author_id: number;
}
```
### 4.2 新增文档管理 API
| 端点 | 方法 | 权限 | 说明 |
|------|------|------|------|
| `/api/docs/categories` | GET | 公开 | 获取分类树 |
| `/api/docs/categories` | POST | Admin | 创建分类 |
| `/api/docs/categories/:id` | PUT | Admin | 更新分类 |
| `/api/docs/categories/:id` | DELETE | Admin | 删除分类 |
| `/api/docs/` | GET | 按可见性 | 文档列表(支持搜索) |
| `/api/docs/:slug` | GET | 按可见性 | 获取文档详情 |
| `/api/docs/` | POST | Admin | 创建文档 |
| `/api/docs/:id` | PUT | Admin | 更新文档 |
| `/api/docs/:id` | DELETE | Admin | 删除文档 |
| `/api/docs/:id/versions` | GET | Admin | 文档版本历史 |
## 5. 项目目录结构
```
web/daisy/
├── index.html
├── package.json
├── tsconfig.json
├── vite.config.ts
├── tailwind.config.ts
├── public/
│ └── manifest.json
└── src/
├── main.tsx # 入口
├── App.tsx # 根组件 + 路由
├── vite-env.d.ts
├── api/ # API 请求层
│ ├── client.ts # Axios 实例 + 拦截器
│ ├── auth.ts # 认证 API
│ ├── channel.ts # 渠道 API
│ ├── token.ts # 令牌 API
│ ├── user.ts # 用户 API
│ ├── log.ts # 日志 API
│ ├── subscription.ts # 订阅 API
│ ├── redemption.ts # 兑换码 API
│ ├── model.ts # 模型 API
│ ├── vendor.ts # 供应商 API
│ ├── deployment.ts # 部署 API
│ ├── option.ts # 系统设置 API
│ ├── payment.ts # 支付 API
│ └── doc.ts # 文档 API(新增)
├── stores/ # Zustand 状态
│ ├── auth.ts # 认证状态
│ └── ui.ts # UI 状态(侧边栏/主题)
├── hooks/ # 自定义 Hooks
│ ├── useAuth.ts
│ ├── usePermission.ts
│ └── useQuota.ts
├── components/ # 通用组件
│ ├── layout/
│ │ ├── AppLayout.tsx # 主布局
│ │ ├── Sidebar.tsx # 侧边导航
│ │ ├── Navbar.tsx # 顶部导航
│ │ └── Breadcrumb.tsx # 面包屑
│ ├── common/
│ │ ├── QuotaDisplay.tsx # 额度显示
│ │ ├── ModelBadge.tsx # 模型标签
│ │ ├── StatusBadge.tsx # 状态标签
│ │ ├── SearchInput.tsx # 搜索框
│ │ ├── DataTable.tsx # 数据表格
│ │ ├── ConfirmDialog.tsx # 确认对话框
│ │ └── LoadingSpinner.tsx # 加载动画
│ └── charts/
│ ├── QuotaChart.tsx # 额度趋势图
│ └── StatsChart.tsx # 统计图表
├── pages/ # 页面组件
│ ├── public/
│ │ ├── Home.tsx
│ │ ├── Login.tsx
│ │ ├── Register.tsx
│ │ ├── ForgotPassword.tsx
│ │ ├── Pricing.tsx
│ │ ├── About.tsx
│ │ └── Setup.tsx
│ ├── dashboard/
│ │ └── Dashboard.tsx
│ ├── tokens/
│ │ ├── TokenList.tsx
│ │ └── TokenForm.tsx
│ ├── channels/
│ │ ├── ChannelList.tsx
│ │ └── ChannelForm.tsx
│ ├── users/
│ │ ├── UserList.tsx
│ │ └── UserForm.tsx
│ ├── logs/
│ │ ├── LogList.tsx
│ │ ├── MidjourneyLog.tsx
│ │ └── TaskLog.tsx
│ ├── wallet/
│ │ └── Wallet.tsx
│ ├── subscriptions/
│ │ ├── PlanList.tsx
│ │ └── MySubscription.tsx
│ ├── redemptions/
│ │ └── RedemptionList.tsx
│ ├── models/
│ │ └── ModelList.tsx
│ ├── vendors/
│ │ └── VendorList.tsx
│ ├── deployments/
│ │ └── DeploymentList.tsx
│ ├── playground/
│ │ └── Playground.tsx
│ ├── profile/
│ │ └── Profile.tsx
│ ├── docs/ # 文档中心(新增)
│ │ ├── DocCenter.tsx # 文档浏览主页
│ │ ├── DocViewer.tsx # 文档阅读页
│ │ ├── DocEditor.tsx # 文档编辑页(管理员)
│ │ └── DocCategoryManager.tsx # 分类管理(管理员)
│ └── settings/
│ ├── SiteSettings.tsx
│ ├── AuthSettings.tsx
│ ├── BillingSettings.tsx
│ ├── ContentSettings.tsx
│ ├── ModelSettings.tsx
│ ├── OperationsSettings.tsx
│ ├── SecuritySettings.tsx
│ └── DocSettings.tsx # 文档设置(新增)
├── i18n/ # 国际化
│ ├── index.ts
│ └── locales/
│ ├── en.json
│ └── zh.json
├── lib/ # 工具函数
│ ├── constants.ts
│ ├── utils.ts
│ ├── quota.ts
│ └── channel-types.ts
└── types/ # TypeScript 类型
├── api.ts
├── channel.ts
├── token.ts
├── user.ts
├── log.ts
├── subscription.ts
├── doc.ts
└── option.ts
```
## 6. 数据模型(新增文档管理)
```mermaid
erDiagram
"document_categories" {
int id PK
string name
string slug UK
int parent_id FK
int sort_order
timestamp created_at
}
"documents" {
int id PK
string title
string slug UK
text content
int category_id FK
string visibility
int sort_order
int author_id FK
timestamp created_at
timestamp updated_at
}
"document_versions" {
int id PK
int document_id FK
text content
int author_id FK
timestamp created_at
}
"document_categories" ||--o{ "document_categories" : "parent"
"document_categories" ||--o{ "documents" : "has"
"documents" ||--o{ "document_versions" : "has"
```
### DDL
```sql
CREATE TABLE document_categories (
id INTEGER PRIMARY KEY AUTOINCREMENT,
name VARCHAR(100) NOT NULL,
slug VARCHAR(100) NOT NULL UNIQUE,
parent_id INTEGER REFERENCES document_categories(id) ON DELETE SET NULL,
sort_order INTEGER DEFAULT 0,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
);
CREATE TABLE documents (
id INTEGER PRIMARY KEY AUTOINCREMENT,
title VARCHAR(200) NOT NULL,
slug VARCHAR(200) NOT NULL UNIQUE,
content TEXT NOT NULL,
category_id INTEGER REFERENCES document_categories(id) ON DELETE SET NULL,
visibility VARCHAR(20) DEFAULT 'public' CHECK (visibility IN ('public', 'auth', 'admin')),
sort_order INTEGER DEFAULT 0,
author_id INTEGER NOT NULL,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP
);
CREATE TABLE document_versions (
id INTEGER PRIMARY KEY AUTOINCREMENT,
document_id INTEGER NOT NULL REFERENCES documents(id) ON DELETE CASCADE,
content TEXT NOT NULL,
author_id INTEGER NOT NULL,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
);
CREATE INDEX idx_documents_slug ON documents(slug);
CREATE INDEX idx_documents_category ON documents(category_id);
CREATE INDEX idx_documents_visibility ON documents(visibility);
CREATE INDEX idx_document_versions_doc ON document_versions(document_id);
```
+2 -2
View File
@@ -1,7 +1,7 @@
new-api Notices
new-api
Copyright (c) modelstoken and contributors.
Copyright (c) QuantumNous and contributors.
This project is licensed under the GNU Affero General Public License v3.0.
See LICENSE for the full project license terms.
@@ -19,7 +19,7 @@ Modified versions that present a user interface must also preserve a visible
link to the original project in a prominent about, legal, footer, or
attribution location:
https://git.viaeon.com/admin/new-api
https://github.com/QuantumNous/new-api
Modified versions must not misrepresent the origin of the software and must
mark their changes in accordance with AGPLv3 Section 7(c).
+1 -1
View File
@@ -14,7 +14,7 @@ import (
var StartTime = time.Now().Unix() // unit: second
var Version = "v0.0.0" // this hard coding will be replaced automatically when building, no need to manually change
var SystemName = "ModelsToken"
var SystemName = "New API"
var Footer = ""
var Logo = ""
var TopUpLink = ""
+4 -8
View File
@@ -51,21 +51,17 @@ type themeAwareFileSystem struct {
}
func (t *themeAwareFileSystem) Exists(prefix string, path string) bool {
switch GetTheme() {
case "classic":
if GetTheme() == "classic" {
return t.classicFS.Exists(prefix, path)
default:
return t.defaultFS.Exists(prefix, path)
}
return t.defaultFS.Exists(prefix, path)
}
func (t *themeAwareFileSystem) Open(name string) (http.File, error) {
switch GetTheme() {
case "classic":
if GetTheme() == "classic" {
return t.classicFS.Open(name)
default:
return t.defaultFS.Open(name)
}
return t.defaultFS.Open(name)
}
func NewThemeAwareFS(defaultFS, classicFS static.ServeFileSystem) static.ServeFileSystem {
-4
View File
@@ -206,8 +206,4 @@ var ChannelSpecialBases = map[string]ChannelSpecialBase{
ClaudeBaseURL: "https://ark.cn-beijing.volces.com/api/coding",
OpenAIBaseURL: "https://ark.cn-beijing.volces.com/api/coding/v3",
},
"tencent-coding-plan": {
ClaudeBaseURL: "https://api.lkeap.cloud.tencent.com/coding",
OpenAIBaseURL: "https://api.lkeap.cloud.tencent.com/coding/v3",
},
}
+1 -5
View File
@@ -312,11 +312,7 @@ func fetchChannelUpstreamModelIDs(channel *model.Channel) ([]string, error) {
url = fmt.Sprintf("%s/v1/models", baseURL)
}
default:
if plan, ok := constant.ChannelSpecialBases[baseURL]; ok && plan.OpenAIBaseURL != "" {
url = fmt.Sprintf("%s/models", plan.OpenAIBaseURL)
} else {
url = fmt.Sprintf("%s/v1/models", baseURL)
}
url = fmt.Sprintf("%s/v1/models", baseURL)
}
key, _, apiErr := channel.GetNextEnabledKey()
-256
View File
@@ -1,256 +0,0 @@
package controller
import (
"net/http"
"strconv"
"github.com/QuantumNous/new-api/common"
"github.com/QuantumNous/new-api/model"
"github.com/gin-gonic/gin"
)
// GetCategories 获取文档分类列表(公开)
func GetCategories(c *gin.Context) {
categories, err := model.GetDocumentCategories()
if err != nil {
common.ApiError(c, err)
return
}
common.ApiSuccess(c, categories)
}
// CreateCategory 创建文档分类(管理员)
func CreateCategory(c *gin.Context) {
var category model.DocumentCategory
if err := c.ShouldBindJSON(&category); err != nil {
common.ApiError(c, err)
return
}
if category.Name == "" {
common.ApiErrorMsg(c, "分类名称不能为空")
return
}
if category.Slug == "" {
common.ApiErrorMsg(c, "分类标识不能为空")
return
}
if err := model.CreateDocumentCategory(&category); err != nil {
common.ApiError(c, err)
return
}
common.ApiSuccess(c, &category)
}
// UpdateCategory 更新文档分类(管理员)
func UpdateCategory(c *gin.Context) {
id, err := strconv.Atoi(c.Param("id"))
if err != nil {
common.ApiError(c, err)
return
}
var category model.DocumentCategory
if err := c.ShouldBindJSON(&category); err != nil {
common.ApiError(c, err)
return
}
category.Id = id
if err := model.UpdateDocumentCategory(&category); err != nil {
common.ApiError(c, err)
return
}
common.ApiSuccess(c, &category)
}
// DeleteCategory 删除文档分类(管理员)
func DeleteCategory(c *gin.Context) {
id, err := strconv.Atoi(c.Param("id"))
if err != nil {
common.ApiError(c, err)
return
}
if err := model.DeleteDocumentCategory(id); err != nil {
common.ApiError(c, err)
return
}
common.ApiSuccess(c, nil)
}
// GetDocuments 获取文档列表(公开,根据认证状态过滤可见性)
func GetDocuments(c *gin.Context) {
keyword := c.Query("keyword")
categoryIdStr := c.Query("category_id")
var categoryId *int
if categoryIdStr != "" {
id, err := strconv.Atoi(categoryIdStr)
if err == nil {
categoryId = &id
}
}
pageInfo := common.GetPageQuery(c)
// 根据用户认证状态决定可见性过滤
visibility := c.Query("visibility")
role := c.GetInt("role")
var documents []*model.Document
var total int64
var err error
if role >= common.RoleAdminUser {
// 管理员可看所有
documents, total, err = model.GetDocuments(keyword, visibility, categoryId, pageInfo.GetStartIdx(), pageInfo.GetPageSize())
} else if role >= common.RoleCommonUser {
// 普通用户只能看 public 和 auth
if visibility == "public" || visibility == "auth" {
documents, total, err = model.GetDocuments(keyword, visibility, categoryId, pageInfo.GetStartIdx(), pageInfo.GetPageSize())
} else {
documents, total, err = model.GetDocumentsByVisibility(keyword, []string{"public", "auth"}, categoryId, pageInfo.GetStartIdx(), pageInfo.GetPageSize())
}
} else {
// 未登录用户只能看 public
documents, total, err = model.GetDocuments(keyword, "public", categoryId, pageInfo.GetStartIdx(), pageInfo.GetPageSize())
}
if err != nil {
common.ApiError(c, err)
return
}
pageInfo.SetTotal(int(total))
pageInfo.SetItems(documents)
common.ApiSuccess(c, pageInfo)
}
// GetDocument 获取单个文档(根据可见性检查权限)
func GetDocument(c *gin.Context) {
slug := c.Param("slug")
doc, err := model.GetDocumentBySlug(slug)
if err != nil {
common.ApiError(c, err)
return
}
// 检查可见性权限
role := c.GetInt("role")
switch doc.Visibility {
case "admin":
if role < common.RoleAdminUser {
c.JSON(http.StatusOK, gin.H{
"success": false,
"message": "无权访问该文档",
})
return
}
case "auth":
if role < common.RoleCommonUser {
c.JSON(http.StatusOK, gin.H{
"success": false,
"message": "请先登录后查看该文档",
})
return
}
}
common.ApiSuccess(c, doc)
}
// CreateDocument 创建文档(管理员)
func CreateDocument(c *gin.Context) {
var doc model.Document
if err := c.ShouldBindJSON(&doc); err != nil {
common.ApiError(c, err)
return
}
if doc.Title == "" {
common.ApiErrorMsg(c, "文档标题不能为空")
return
}
if doc.Slug == "" {
common.ApiErrorMsg(c, "文档标识不能为空")
return
}
if doc.Content == "" {
common.ApiErrorMsg(c, "文档内容不能为空")
return
}
if doc.Visibility == "" {
doc.Visibility = "public"
}
doc.AuthorId = c.GetInt("id")
if err := model.CreateDocument(&doc); err != nil {
common.ApiError(c, err)
return
}
common.ApiSuccess(c, &doc)
}
// UpdateDocument 更新文档(管理员,自动创建版本记录)
func UpdateDocument(c *gin.Context) {
id, err := strconv.Atoi(c.Param("id"))
if err != nil {
common.ApiError(c, err)
return
}
var doc model.Document
if err := c.ShouldBindJSON(&doc); err != nil {
common.ApiError(c, err)
return
}
doc.Id = id
// 获取旧文档内容,自动创建版本记录
oldDoc, err := model.GetDocumentById(id)
if err != nil {
common.ApiError(c, err)
return
}
version := &model.DocumentVersion{
DocumentId: oldDoc.Id,
Content: oldDoc.Content,
AuthorId: oldDoc.AuthorId,
}
if err := model.CreateDocumentVersion(version); err != nil {
common.ApiError(c, err)
return
}
if err := model.UpdateDocument(&doc); err != nil {
common.ApiError(c, err)
return
}
common.ApiSuccess(c, &doc)
}
// DeleteDocument 删除文档(管理员)
func DeleteDocument(c *gin.Context) {
id, err := strconv.Atoi(c.Param("id"))
if err != nil {
common.ApiError(c, err)
return
}
if err := model.DeleteDocument(id); err != nil {
common.ApiError(c, err)
return
}
common.ApiSuccess(c, nil)
}
// GetDocumentVersions 获取文档版本历史(管理员)
func GetDocumentVersions(c *gin.Context) {
id, err := strconv.Atoi(c.Param("id"))
if err != nil {
common.ApiError(c, err)
return
}
pageInfo := common.GetPageQuery(c)
versions, total, err := model.GetDocumentVersions(id, pageInfo.GetStartIdx(), pageInfo.GetPageSize())
if err != nil {
common.ApiError(c, err)
return
}
pageInfo.SetTotal(int(total))
pageInfo.SetItems(versions)
common.ApiSuccess(c, pageInfo)
}
+13 -1
View File
@@ -7,6 +7,7 @@ import (
"strings"
"github.com/QuantumNous/new-api/common"
"github.com/QuantumNous/new-api/i18n"
"github.com/QuantumNous/new-api/model"
"github.com/QuantumNous/new-api/setting"
"github.com/QuantumNous/new-api/setting/console_setting"
@@ -28,6 +29,10 @@ var completionRatioMetaOptionKeys = []string{
"AudioCompletionRatio",
}
func isPaymentComplianceOptionKey(key string) bool {
return strings.HasPrefix(key, "payment_setting.compliance_")
}
func isPositiveOptionValue(value string) bool {
intValue, err := strconv.Atoi(strings.TrimSpace(value))
if err == nil {
@@ -134,8 +139,15 @@ func UpdateOption(c *gin.Context) {
}
switch option.Key {
case "QuotaForInviter", "QuotaForInvitee":
// no compliance check needed
if isPositiveOptionValue(option.Value.(string)) && !operation_setting.IsPaymentComplianceConfirmed() {
common.ApiErrorI18n(c, i18n.MsgPaymentComplianceRequired)
return
}
default:
if isPaymentComplianceOptionKey(option.Key) {
common.ApiErrorMsg(c, "合规确认字段不允许通过通用设置接口修改")
return
}
}
switch option.Key {
case "GitHubOAuthEnabled":
+82
View File
@@ -0,0 +1,82 @@
package controller
import (
"fmt"
"net/http"
"strconv"
"time"
"github.com/QuantumNous/new-api/common"
"github.com/QuantumNous/new-api/i18n"
"github.com/QuantumNous/new-api/logger"
"github.com/QuantumNous/new-api/model"
"github.com/QuantumNous/new-api/setting/operation_setting"
"github.com/gin-gonic/gin"
)
type PaymentComplianceRequest struct {
Confirmed bool `json:"confirmed"`
}
func requirePaymentCompliance(c *gin.Context) bool {
if !operation_setting.IsPaymentComplianceConfirmed() {
common.ApiErrorI18n(c, i18n.MsgPaymentComplianceRequired)
return false
}
return true
}
func ConfirmPaymentCompliance(c *gin.Context) {
if c.GetBool("use_access_token") {
c.JSON(http.StatusForbidden, gin.H{
"success": false,
"message": "This operation requires dashboard session authentication. API access token is not allowed.",
})
return
}
var req PaymentComplianceRequest
if err := common.DecodeJson(c.Request.Body, &req); err != nil {
common.ApiErrorMsg(c, "参数错误")
return
}
if !req.Confirmed {
common.ApiErrorMsg(c, "请确认合规声明")
return
}
now := time.Now().Unix()
userId := c.GetInt("id")
clientIP := c.ClientIP()
updates := map[string]string{
"payment_setting.compliance_confirmed": "true",
"payment_setting.compliance_terms_version": operation_setting.CurrentComplianceTermsVersion,
"payment_setting.compliance_confirmed_at": strconv.FormatInt(now, 10),
"payment_setting.compliance_confirmed_by": strconv.Itoa(userId),
"payment_setting.compliance_confirmed_ip": clientIP,
}
for key, value := range updates {
if err := model.UpdateOption(key, value); err != nil {
common.ApiError(c, err)
return
}
}
logger.LogInfo(c.Request.Context(), fmt.Sprintf(
"payment compliance confirmed user_id=%d ip=%s terms_version=%s confirmed_at=%d",
userId,
clientIP,
operation_setting.CurrentComplianceTermsVersion,
now,
))
common.ApiSuccess(c, gin.H{
"confirmed": true,
"terms_version": operation_setting.CurrentComplianceTermsVersion,
"confirmed_at": now,
"confirmed_by": userId,
})
}
@@ -7,7 +7,14 @@ import (
"github.com/QuantumNous/new-api/setting/operation_setting"
)
func isPaymentComplianceConfirmed() bool {
return operation_setting.IsPaymentComplianceConfirmed()
}
func isStripeTopUpEnabled() bool {
if !isPaymentComplianceConfirmed() {
return false
}
return strings.TrimSpace(setting.StripeApiSecret) != "" &&
strings.TrimSpace(setting.StripeWebhookSecret) != "" &&
strings.TrimSpace(setting.StripePriceId) != ""
@@ -22,6 +29,9 @@ func isStripeWebhookEnabled() bool {
}
func isCreemTopUpEnabled() bool {
if !isPaymentComplianceConfirmed() {
return false
}
products := strings.TrimSpace(setting.CreemProducts)
return strings.TrimSpace(setting.CreemApiKey) != "" &&
products != "" &&
@@ -37,6 +47,9 @@ func isCreemWebhookEnabled() bool {
}
func isWaffoTopUpEnabled() bool {
if !isPaymentComplianceConfirmed() {
return false
}
if !setting.WaffoEnabled {
return false
}
@@ -61,6 +74,11 @@ func isWaffoWebhookEnabled() bool {
}
func isWaffoPancakeTopUpEnabled() bool {
if !isPaymentComplianceConfirmed() {
return false
}
// Presence-of-credentials = enabled. Webhook public keys ship inside
// the SDK; mode (test/prod) is read from each event.
return strings.TrimSpace(setting.WaffoPancakeMerchantID) != "" &&
strings.TrimSpace(setting.WaffoPancakePrivateKey) != "" &&
strings.TrimSpace(setting.WaffoPancakeProductID) != ""
@@ -75,6 +93,9 @@ func isWaffoPancakeWebhookEnabled() bool {
}
func isEpayTopUpEnabled() bool {
if !isPaymentComplianceConfirmed() {
return false
}
return isEpayWebhookConfigured() && len(operation_setting.PayMethods) > 0
}
@@ -8,7 +8,21 @@ import (
"github.com/stretchr/testify/require"
)
func confirmPaymentComplianceForTest(t *testing.T) {
t.Helper()
paymentSetting := operation_setting.GetPaymentSetting()
originalConfirmed := paymentSetting.ComplianceConfirmed
originalTermsVersion := paymentSetting.ComplianceTermsVersion
t.Cleanup(func() {
paymentSetting.ComplianceConfirmed = originalConfirmed
paymentSetting.ComplianceTermsVersion = originalTermsVersion
})
paymentSetting.ComplianceConfirmed = true
paymentSetting.ComplianceTermsVersion = operation_setting.CurrentComplianceTermsVersion
}
func TestStripeWebhookEnabledRequiresTopUpAndWebhookConfig(t *testing.T) {
confirmPaymentComplianceForTest(t)
originalAPISecret := setting.StripeApiSecret
originalWebhookSecret := setting.StripeWebhookSecret
originalPriceID := setting.StripePriceId
@@ -31,6 +45,7 @@ func TestStripeWebhookEnabledRequiresTopUpAndWebhookConfig(t *testing.T) {
}
func TestCreemWebhookEnabledRequiresTopUpAndWebhookConfig(t *testing.T) {
confirmPaymentComplianceForTest(t)
originalAPIKey := setting.CreemApiKey
originalProducts := setting.CreemProducts
originalWebhookSecret := setting.CreemWebhookSecret
@@ -53,6 +68,7 @@ func TestCreemWebhookEnabledRequiresTopUpAndWebhookConfig(t *testing.T) {
}
func TestWaffoWebhookEnabledRequiresTopUpAndWebhookConfig(t *testing.T) {
confirmPaymentComplianceForTest(t)
originalEnabled := setting.WaffoEnabled
originalSandbox := setting.WaffoSandbox
originalAPIKey := setting.WaffoApiKey
@@ -97,6 +113,7 @@ func TestWaffoWebhookEnabledRequiresTopUpAndWebhookConfig(t *testing.T) {
}
func TestWaffoPancakeWebhookEnabledRequiresTopUpAndWebhookConfig(t *testing.T) {
confirmPaymentComplianceForTest(t)
originalMerchantID := setting.WaffoPancakeMerchantID
originalPrivateKey := setting.WaffoPancakePrivateKey
originalProductID := setting.WaffoPancakeProductID
@@ -106,6 +123,9 @@ func TestWaffoPancakeWebhookEnabledRequiresTopUpAndWebhookConfig(t *testing.T) {
setting.WaffoPancakeProductID = originalProductID
})
// Presence of all three credentials enables the gateway. Webhook public
// keys are bundled in the SDK and there is no separate Enabled toggle —
// clear any of the three fields to disable.
setting.WaffoPancakeMerchantID = ""
setting.WaffoPancakePrivateKey = "private"
setting.WaffoPancakeProductID = "product"
@@ -123,6 +143,7 @@ func TestWaffoPancakeWebhookEnabledRequiresTopUpAndWebhookConfig(t *testing.T) {
}
func TestEpayWebhookEnabledRequiresTopUpAndWebhookConfig(t *testing.T) {
confirmPaymentComplianceForTest(t)
originalPayAddress := operation_setting.PayAddress
originalEpayID := operation_setting.EpayId
originalEpayKey := operation_setting.EpayKey
+6
View File
@@ -8,6 +8,7 @@ import (
"github.com/QuantumNous/new-api/common"
"github.com/QuantumNous/new-api/i18n"
"github.com/QuantumNous/new-api/model"
"github.com/QuantumNous/new-api/setting/operation_setting"
"github.com/gin-gonic/gin"
)
@@ -59,6 +60,11 @@ func GetRedemption(c *gin.Context) {
}
func AddRedemption(c *gin.Context) {
if !operation_setting.IsPaymentComplianceConfirmed() {
common.ApiErrorI18n(c, i18n.MsgPaymentComplianceRequired)
return
}
redemption := model.Redemption{}
err := c.ShouldBindJSON(&redemption)
if err != nil {
+30
View File
@@ -6,6 +6,7 @@ import (
"github.com/QuantumNous/new-api/common"
"github.com/QuantumNous/new-api/model"
"github.com/QuantumNous/new-api/setting/operation_setting"
"github.com/QuantumNous/new-api/setting/ratio_setting"
"github.com/gin-gonic/gin"
"gorm.io/gorm"
@@ -28,6 +29,11 @@ type SubscriptionBalancePayRequest struct {
// ---- User APIs ----
func GetSubscriptionPlans(c *gin.Context) {
if !operation_setting.IsPaymentComplianceConfirmed() {
common.ApiSuccess(c, []SubscriptionPlanDTO{})
return
}
var plans []model.SubscriptionPlan
if err := model.DB.Where("enabled = ?", true).Order("sort_order desc, id desc").Find(&plans).Error; err != nil {
common.ApiError(c, err)
@@ -92,6 +98,10 @@ func UpdateSubscriptionPreference(c *gin.Context) {
}
func SubscriptionRequestBalancePay(c *gin.Context) {
if !requirePaymentCompliance(c) {
return
}
userId := c.GetInt("id")
var req SubscriptionBalancePayRequest
if err := c.ShouldBindJSON(&req); err != nil || req.PlanId <= 0 {
@@ -129,6 +139,10 @@ type AdminUpsertSubscriptionPlanRequest struct {
}
func AdminCreateSubscriptionPlan(c *gin.Context) {
if !requirePaymentCompliance(c) {
return
}
var req AdminUpsertSubscriptionPlanRequest
if err := c.ShouldBindJSON(&req); err != nil {
common.ApiErrorMsg(c, "参数错误")
@@ -190,6 +204,10 @@ func AdminCreateSubscriptionPlan(c *gin.Context) {
}
func AdminUpdateSubscriptionPlan(c *gin.Context) {
if !requirePaymentCompliance(c) {
return
}
id, _ := strconv.Atoi(c.Param("id"))
if id <= 0 {
common.ApiErrorMsg(c, "无效的ID")
@@ -287,6 +305,10 @@ type AdminUpdateSubscriptionPlanStatusRequest struct {
}
func AdminUpdateSubscriptionPlanStatus(c *gin.Context) {
if !requirePaymentCompliance(c) {
return
}
id, _ := strconv.Atoi(c.Param("id"))
if id <= 0 {
common.ApiErrorMsg(c, "无效的ID")
@@ -311,6 +333,10 @@ type AdminBindSubscriptionRequest struct {
}
func AdminBindSubscription(c *gin.Context) {
if !requirePaymentCompliance(c) {
return
}
var req AdminBindSubscriptionRequest
if err := c.ShouldBindJSON(&req); err != nil || req.UserId <= 0 || req.PlanId <= 0 {
common.ApiErrorMsg(c, "参数错误")
@@ -350,6 +376,10 @@ type AdminCreateUserSubscriptionRequest struct {
// AdminCreateUserSubscription creates a new user subscription from a plan (no payment).
func AdminCreateUserSubscription(c *gin.Context) {
if !requirePaymentCompliance(c) {
return
}
userId, _ := strconv.Atoi(c.Param("id"))
if userId <= 0 {
common.ApiErrorMsg(c, "无效的用户ID")
+4
View File
@@ -21,6 +21,10 @@ type SubscriptionCreemPayRequest struct {
}
func SubscriptionRequestCreemPay(c *gin.Context) {
if !requirePaymentCompliance(c) {
return
}
var req SubscriptionCreemPayRequest
// Keep body for debugging consistency (like RequestCreemPay)
+4
View File
@@ -22,6 +22,10 @@ type SubscriptionEpayPayRequest struct {
}
func SubscriptionRequestEpay(c *gin.Context) {
if !requirePaymentCompliance(c) {
return
}
var req SubscriptionEpayPayRequest
if err := c.ShouldBindJSON(&req); err != nil || req.PlanId <= 0 {
common.ApiErrorMsg(c, "参数错误")
@@ -21,6 +21,10 @@ type SubscriptionStripePayRequest struct {
}
func SubscriptionRequestStripePay(c *gin.Context) {
if !requirePaymentCompliance(c) {
return
}
var req SubscriptionStripePayRequest
if err := c.ShouldBindJSON(&req); err != nil || req.PlanId <= 0 {
common.ApiErrorMsg(c, "参数错误")
@@ -21,6 +21,10 @@ type SubscriptionWaffoPancakePayRequest struct {
}
func SubscriptionRequestWaffoPancakePay(c *gin.Context) {
if !requirePaymentCompliance(c) {
return
}
var req SubscriptionWaffoPancakePayRequest
if err := c.ShouldBindJSON(&req); err != nil || req.PlanId <= 0 {
common.ApiErrorMsg(c, "参数错误")
+8 -2
View File
@@ -22,8 +22,13 @@ import (
)
func GetTopUpInfo(c *gin.Context) {
complianceConfirmed := operation_setting.IsPaymentComplianceConfirmed()
// 获取支付方式
payMethods := operation_setting.PayMethods
if !complianceConfirmed {
payMethods = []map[string]string{}
}
// 如果启用了 Stripe 支付,添加到支付方法列表
if isStripeTopUpEnabled() {
@@ -96,8 +101,9 @@ func GetTopUpInfo(c *gin.Context) {
"enable_creem_topup": isCreemTopUpEnabled(),
"enable_waffo_topup": enableWaffo,
"enable_waffo_pancake_topup": enableWaffoPancake,
"enable_redemption": true,
"payment_compliance_confirmed": true,
"enable_redemption": complianceConfirmed,
"payment_compliance_confirmed": complianceConfirmed,
"payment_compliance_terms_version": operation_setting.CurrentComplianceTermsVersion,
"waffo_pay_methods": func() interface{} {
if enableWaffo {
return setting.GetWaffoPayMethods()
+10
View File
@@ -17,6 +17,7 @@ import (
"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/constant"
@@ -343,6 +344,10 @@ type TransferAffQuotaRequest struct {
}
func TransferAffQuota(c *gin.Context) {
if !requirePaymentCompliance(c) {
return
}
id := c.GetInt("id")
user, err := model.GetUserById(id, true)
if err != nil {
@@ -1099,6 +1104,11 @@ func getTopUpLock(userID int) *topUpTryLock {
}
func TopUp(c *gin.Context) {
if !operation_setting.IsPaymentComplianceConfirmed() {
common.ApiErrorI18n(c, i18n.MsgPaymentComplianceRequired)
return
}
id := c.GetInt("id")
lock := getTopUpLock(id)
if !lock.TryLock() {
+1 -1
View File
@@ -16,7 +16,7 @@ version: '3.4' # For compatibility with older Docker versions
services:
new-api:
image: git.viaeon.com/admin/new-api:latest
image: calciumion/new-api:latest
container_name: new-api
restart: always
command: --log-dir /app/logs
+2 -2
View File
@@ -18,10 +18,10 @@
"openai",
"claude"
],
"author": "modelstoken",
"author": "QuantumNous",
"repository": {
"type": "git",
"url": "https://git.viaeon.com/admin/new-api"
"url": "https://github.com/QuantumNous/new-api"
},
"devDependencies": {
"cross-env": "^7.0.3",
+1
View File
@@ -152,6 +152,7 @@ const (
MsgPaymentWebhookNotConfig = "payment.webhook_not_configured"
MsgPaymentPriceIdNotConfig = "payment.price_id_not_configured"
MsgPaymentCreemNotConfig = "payment.creem_not_configured"
MsgPaymentComplianceRequired = "payment.compliance_required"
)
// Topup related messages
+1 -1
View File
@@ -164,7 +164,7 @@ func main() {
common.SysLog(fmt.Sprintf("panic detected: %v", err))
c.JSON(http.StatusInternalServerError, gin.H{
"error": gin.H{
"message": fmt.Sprintf("Panic detected, error: %v. Please submit a issue here: https://git.viaeon.com/admin/new-api/issues", err),
"message": fmt.Sprintf("Panic detected, error: %v. Please submit a issue here: https://github.com/Calcium-Ion/new-api", err),
"type": "new_api_panic",
},
})
-4
View File
@@ -163,10 +163,6 @@ func TryUserAuth() func(c *gin.Context) {
if id != nil {
c.Set("id", id)
}
role := session.Get("role")
if role != nil {
c.Set("role", role)
}
c.Next()
}
}
+1 -1
View File
@@ -17,7 +17,7 @@ func RelayPanicRecover() gin.HandlerFunc {
common.SysLog(fmt.Sprintf("stacktrace from panic: %s", string(debug.Stack())))
c.JSON(http.StatusInternalServerError, gin.H{
"error": gin.H{
"message": fmt.Sprintf("Panic detected, error: %v. Please submit a issue here: https://git.viaeon.com/admin/new-api/issues", err),
"message": fmt.Sprintf("Panic detected, error: %v. Please submit a issue here: https://github.com/Calcium-Ion/new-api", err),
"type": "new_api_panic",
},
})
-96
View File
@@ -1,96 +0,0 @@
package model
import (
"time"
)
type Document struct {
Id int `json:"id" gorm:"primaryKey"`
Title string `json:"title" gorm:"not null"`
Slug string `json:"slug" gorm:"type:varchar(255);uniqueIndex;not null"`
Content string `json:"content" gorm:"type:text;not null"`
CategoryId *int `json:"category_id" gorm:"index"`
Visibility string `json:"visibility" gorm:"default:'public'"` // public, auth, admin
SortOrder int `json:"sort_order" gorm:"default:0"`
AuthorId int `json:"author_id" gorm:"not null"`
CreatedAt time.Time `json:"created_at" gorm:"autoCreateTime"`
UpdatedAt time.Time `json:"updated_at" gorm:"autoUpdateTime"`
}
func GetDocuments(keyword string, visibility string, categoryId *int, startIdx int, num int) ([]*Document, int64, error) {
query := DB.Model(&Document{})
if keyword != "" {
like := "%" + keyword + "%"
query = query.Where("title LIKE ? OR content LIKE ?", like, like)
}
if visibility != "" {
query = query.Where("visibility = ?", visibility)
}
if categoryId != nil {
query = query.Where("category_id = ?", *categoryId)
}
var total int64
if err := query.Count(&total).Error; err != nil {
return nil, 0, err
}
var documents []*Document
if err := query.Order("sort_order ASC, id DESC").Offset(startIdx).Limit(num).Find(&documents).Error; err != nil {
return nil, 0, err
}
return documents, total, nil
}
func GetDocumentsByVisibility(keyword string, visibilities []string, categoryId *int, startIdx int, num int) ([]*Document, int64, error) {
query := DB.Model(&Document{})
if keyword != "" {
like := "%" + keyword + "%"
query = query.Where("title LIKE ? OR content LIKE ?", like, like)
}
if len(visibilities) > 0 {
query = query.Where("visibility IN ?", visibilities)
}
if categoryId != nil {
query = query.Where("category_id = ?", *categoryId)
}
var total int64
if err := query.Count(&total).Error; err != nil {
return nil, 0, err
}
var documents []*Document
if err := query.Order("sort_order ASC, id DESC").Offset(startIdx).Limit(num).Find(&documents).Error; err != nil {
return nil, 0, err
}
return documents, total, nil
}
func GetDocumentBySlug(slug string) (*Document, error) {
var doc Document
err := DB.Where("slug = ?", slug).First(&doc).Error
if err != nil {
return nil, err
}
return &doc, nil
}
func GetDocumentById(id int) (*Document, error) {
var doc Document
err := DB.First(&doc, id).Error
if err != nil {
return nil, err
}
return &doc, nil
}
func CreateDocument(doc *Document) error {
return DB.Create(doc).Error
}
func UpdateDocument(doc *Document) error {
return DB.Model(doc).Select("title", "slug", "content", "category_id", "visibility", "sort_order").Updates(doc).Error
}
func DeleteDocument(id int) error {
// Delete associated versions first
DB.Where("document_id = ?", id).Delete(&DocumentVersion{})
return DB.Delete(&Document{}, id).Error
}
-38
View File
@@ -1,38 +0,0 @@
package model
import (
"time"
)
type DocumentCategory struct {
Id int `json:"id" gorm:"primaryKey"`
Name string `json:"name" gorm:"not null"`
Slug string `json:"slug" gorm:"type:varchar(255);uniqueIndex;not null"`
ParentId *int `json:"parent_id" gorm:"index"`
SortOrder int `json:"sort_order" gorm:"default:0"`
CreatedAt time.Time `json:"created_at" gorm:"autoCreateTime"`
}
func GetDocumentCategories() ([]*DocumentCategory, error) {
var categories []*DocumentCategory
err := DB.Order("sort_order ASC, id ASC").Find(&categories).Error
return categories, err
}
func GetDocumentCategoryTree() ([]*DocumentCategory, error) {
var categories []*DocumentCategory
err := DB.Order("sort_order ASC, id ASC").Find(&categories).Error
return categories, err
}
func CreateDocumentCategory(category *DocumentCategory) error {
return DB.Create(category).Error
}
func UpdateDocumentCategory(category *DocumentCategory) error {
return DB.Model(category).Select("name", "slug", "parent_id", "sort_order").Updates(category).Error
}
func DeleteDocumentCategory(id int) error {
return DB.Delete(&DocumentCategory{}, id).Error
}
-30
View File
@@ -1,30 +0,0 @@
package model
import (
"time"
)
type DocumentVersion struct {
Id int `json:"id" gorm:"primaryKey"`
DocumentId int `json:"document_id" gorm:"index;not null"`
Content string `json:"content" gorm:"type:text;not null"`
AuthorId int `json:"author_id" gorm:"not null"`
CreatedAt time.Time `json:"created_at" gorm:"autoCreateTime"`
}
func GetDocumentVersions(documentId int, startIdx int, num int) ([]*DocumentVersion, int64, error) {
query := DB.Model(&DocumentVersion{}).Where("document_id = ?", documentId)
var total int64
if err := query.Count(&total).Error; err != nil {
return nil, 0, err
}
var versions []*DocumentVersion
if err := query.Order("id DESC").Offset(startIdx).Limit(num).Find(&versions).Error; err != nil {
return nil, 0, err
}
return versions, total, nil
}
func CreateDocumentVersion(version *DocumentVersion) error {
return DB.Create(version).Error
}
-6
View File
@@ -281,9 +281,6 @@ func migrateDB() error {
&CustomOAuthProvider{},
&UserOAuthBinding{},
&PerfMetric{},
&DocumentCategory{},
&Document{},
&DocumentVersion{},
)
if err != nil {
return err
@@ -333,9 +330,6 @@ func migrateDBFast() error {
{&CustomOAuthProvider{}, "CustomOAuthProvider"},
{&UserOAuthBinding{}, "UserOAuthBinding"},
{&PerfMetric{}, "PerfMetric"},
{&DocumentCategory{}, "DocumentCategory"},
{&Document{}, "Document"},
{&DocumentVersion{}, "DocumentVersion"},
}
// 动态计算migration数量,确保errChan缓冲区足够大
errChan := make(chan error, len(migrations))
+3 -2
View File
@@ -11,6 +11,7 @@ import (
"github.com/QuantumNous/new-api/common"
"github.com/QuantumNous/new-api/dto"
"github.com/QuantumNous/new-api/logger"
"github.com/QuantumNous/new-api/setting/operation_setting"
"github.com/bytedance/gopkg/util/gopool"
"gorm.io/gorm"
@@ -417,7 +418,7 @@ func (user *User) Insert(inviterId int) error {
if common.QuotaForNewUser > 0 {
RecordLog(user.Id, LogTypeSystem, fmt.Sprintf("新用户注册赠送 %s", logger.LogQuota(common.QuotaForNewUser)))
}
if inviterId != 0 {
if inviterId != 0 && operation_setting.IsPaymentComplianceConfirmed() {
if common.QuotaForInvitee > 0 {
_ = IncreaseUserQuota(user.Id, common.QuotaForInvitee, true)
RecordLog(user.Id, LogTypeSystem, fmt.Sprintf("使用邀请码赠送 %s", logger.LogQuota(common.QuotaForInvitee)))
@@ -478,7 +479,7 @@ func (user *User) FinalizeOAuthUserCreation(inviterId int) {
if common.QuotaForNewUser > 0 {
RecordLog(user.Id, LogTypeSystem, fmt.Sprintf("新用户注册赠送 %s", logger.LogQuota(common.QuotaForNewUser)))
}
if inviterId != 0 {
if inviterId != 0 && operation_setting.IsPaymentComplianceConfirmed() {
if common.QuotaForInvitee > 0 {
_ = IncreaseUserQuota(user.Id, common.QuotaForInvitee, true)
RecordLog(user.Id, LogTypeSystem, fmt.Sprintf("使用邀请码赠送 %s", logger.LogQuota(common.QuotaForInvitee)))
+1 -15
View File
@@ -164,20 +164,6 @@ func (a *Adaptor) GetRequestURL(info *relaycommon.RelayInfo) (string, error) {
url = strings.Replace(url, "{model}", info.UpstreamModelName, -1)
return url, nil
default:
// Handle coding plan special base URLs
if specialPlan, ok := constant.ChannelSpecialBases[info.ChannelBaseUrl]; ok && specialPlan.OpenAIBaseURL != "" {
if info.RelayFormat == types.RelayFormatClaude {
return fmt.Sprintf("%s/v1/messages", specialPlan.ClaudeBaseURL), nil
}
switch info.RelayMode {
case relayconstant.RelayModeEmbeddings:
return fmt.Sprintf("%s/embeddings", specialPlan.OpenAIBaseURL), nil
case relayconstant.RelayModeImagesGenerations:
return fmt.Sprintf("%s/images/generations", specialPlan.OpenAIBaseURL), nil
default:
return fmt.Sprintf("%s/chat/completions", specialPlan.OpenAIBaseURL), nil
}
}
if (info.RelayFormat == types.RelayFormatClaude || info.RelayFormat == types.RelayFormatGemini) &&
info.RelayMode != relayconstant.RelayModeResponses &&
info.RelayMode != relayconstant.RelayModeResponsesCompact {
@@ -235,7 +221,7 @@ func (a *Adaptor) SetupRequestHeader(c *gin.Context, header *http.Header, info *
header.Set("HTTP-Referer", "https://www.newapi.ai")
}
if header.Get("X-OpenRouter-Title") == "" {
header.Set("X-OpenRouter-Title", "ModelsToken")
header.Set("X-OpenRouter-Title", "New API")
}
}
return nil
+1 -24
View File
@@ -186,6 +186,7 @@ func SetApiRouter(router *gin.Engine) {
{
optionRoute.GET("/", controller.GetOptions)
optionRoute.PUT("/", controller.UpdateOption)
optionRoute.POST("/payment_compliance", controller.ConfirmPaymentCompliance)
optionRoute.GET("/channel_affinity_cache", controller.GetChannelAffinityCacheStats)
optionRoute.DELETE("/channel_affinity_cache", controller.ClearChannelAffinityCache)
optionRoute.POST("/rest_model_ratio", controller.ResetModelRatio)
@@ -346,30 +347,6 @@ func SetApiRouter(router *gin.Engine) {
taskRoute.GET("/", middleware.AdminAuth(), controller.GetAllTask)
}
// Document routes (public)
docsPublic := apiRouter.Group("/docs")
docsPublic.Use(middleware.TryUserAuth())
{
docsPublic.GET("/categories", controller.GetCategories)
docsPublic.GET("/", controller.GetDocuments)
docsPublic.GET("/:slug", controller.GetDocument)
}
// Document routes (admin) - use /admin/docs to avoid conflict with /:slug
docsAdmin := apiRouter.Group("/admin/docs")
docsAdmin.Use(middleware.AdminAuth())
{
docsAdmin.GET("/categories", controller.GetCategories)
docsAdmin.POST("/categories", controller.CreateCategory)
docsAdmin.PUT("/categories/:id", controller.UpdateCategory)
docsAdmin.DELETE("/categories/:id", controller.DeleteCategory)
docsAdmin.GET("/", controller.GetDocuments)
docsAdmin.POST("/", controller.CreateDocument)
docsAdmin.PUT("/:id", controller.UpdateDocument)
docsAdmin.DELETE("/:id", controller.DeleteDocument)
docsAdmin.GET("/:id/versions", controller.GetDocumentVersions)
}
vendorRoute := apiRouter.Group("/vendors")
vendorRoute.Use(middleware.AdminAuth())
{
+3 -4
View File
@@ -13,7 +13,7 @@ import (
"github.com/gin-gonic/gin"
)
// ThemeAssets holds the embedded frontend assets for all themes.
// ThemeAssets holds the embedded frontend assets for both themes.
type ThemeAssets struct {
DefaultBuildFS embed.FS
DefaultIndexPage []byte
@@ -37,10 +37,9 @@ func SetWebRouter(router *gin.Engine, assets ThemeAssets) {
return
}
c.Header("Cache-Control", "no-cache")
switch common.GetTheme() {
case "classic":
if common.GetTheme() == "classic" {
c.Data(http.StatusOK, "text/html; charset=utf-8", assets.ClassicIndexPage)
default:
} else {
c.Data(http.StatusOK, "text/html; charset=utf-8", assets.DefaultIndexPage)
}
})
@@ -5,8 +5,16 @@ import "github.com/QuantumNous/new-api/setting/config"
type PaymentSetting struct {
AmountOptions []int `json:"amount_options"`
AmountDiscount map[int]float64 `json:"amount_discount"` // 充值金额对应的折扣,例如 100 元 0.9 表示 100 元充值享受 9 折优惠
ComplianceConfirmed bool `json:"compliance_confirmed"`
ComplianceTermsVersion string `json:"compliance_terms_version"`
ComplianceConfirmedAt int64 `json:"compliance_confirmed_at"`
ComplianceConfirmedBy int `json:"compliance_confirmed_by"`
ComplianceConfirmedIP string `json:"compliance_confirmed_ip"`
}
const CurrentComplianceTermsVersion = "v1"
// 默认配置
var paymentSetting = PaymentSetting{
AmountOptions: []int{10, 20, 50, 100, 200, 500},
@@ -21,3 +29,8 @@ func init() {
func GetPaymentSetting() *PaymentSetting {
return &paymentSetting
}
func IsPaymentComplianceConfirmed() bool {
return paymentSetting.ComplianceConfirmed &&
paymentSetting.ComplianceTermsVersion == CurrentComplianceTermsVersion
}
+1 -1
View File
@@ -16,7 +16,7 @@
content="A unified AI model hub for aggregation & distribution. It supports cross-converting various LLMs into OpenAI-compatible, Claude-compatible, or Gemini-compatible formats. A centralized gateway for personal and enterprise model management."
/>
<meta name="generator" content="new-api" />
<title>ModelsToken</title>
<title>New API</title>
<!--umami-->
<!--Google Analytics-->
</head>
BIN
View File
Binary file not shown.

Before

Width:  |  Height:  |  Size: 52 KiB

After

Width:  |  Height:  |  Size: 15 KiB

BIN
View File
Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.9 MiB

After

Width:  |  Height:  |  Size: 9.4 KiB

+2 -2
View File
@@ -1,5 +1,5 @@
/*
Copyright (C) 2025 modelstoken
Copyright (C) 2025 QuantumNous
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as
@@ -14,7 +14,7 @@ GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
For commercial licensing, please contact admin@modelstoken.com
For commercial licensing, please contact support@quantumnous.com
*/
import React, { lazy, Suspense, useContext, useMemo } from 'react';
+2 -2
View File
@@ -1,5 +1,5 @@
/*
Copyright (C) 2025 modelstoken
Copyright (C) 2025 QuantumNous
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as
@@ -14,7 +14,7 @@ GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
For commercial licensing, please contact admin@modelstoken.com
For commercial licensing, please contact support@quantumnous.com
*/
import React, { useContext, useEffect, useMemo, useRef, useState } from 'react';
+2 -2
View File
@@ -1,5 +1,5 @@
/*
Copyright (C) 2025 modelstoken
Copyright (C) 2025 QuantumNous
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as
@@ -14,7 +14,7 @@ GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
For commercial licensing, please contact admin@modelstoken.com
For commercial licensing, please contact support@quantumnous.com
*/
import React, { useContext, useEffect, useRef } from 'react';
+2 -2
View File
@@ -1,5 +1,5 @@
/*
Copyright (C) 2025 modelstoken
Copyright (C) 2025 QuantumNous
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as
@@ -14,7 +14,7 @@ GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
For commercial licensing, please contact admin@modelstoken.com
For commercial licensing, please contact support@quantumnous.com
*/
import React, { useEffect, useState } from 'react';
+2 -2
View File
@@ -1,5 +1,5 @@
/*
Copyright (C) 2025 modelstoken
Copyright (C) 2025 QuantumNous
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as
@@ -14,7 +14,7 @@ GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
For commercial licensing, please contact admin@modelstoken.com
For commercial licensing, please contact support@quantumnous.com
*/
import React, { useEffect, useState } from 'react';
+2 -2
View File
@@ -1,5 +1,5 @@
/*
Copyright (C) 2025 modelstoken
Copyright (C) 2025 QuantumNous
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as
@@ -14,7 +14,7 @@ GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
For commercial licensing, please contact admin@modelstoken.com
For commercial licensing, please contact support@quantumnous.com
*/
import React, { useContext, useEffect, useMemo, useRef, useState } from 'react';
+2 -2
View File
@@ -1,5 +1,5 @@
/*
Copyright (C) 2025 modelstoken
Copyright (C) 2025 QuantumNous
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as
@@ -14,7 +14,7 @@ GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
For commercial licensing, please contact admin@modelstoken.com
For commercial licensing, please contact support@quantumnous.com
*/
import { API, showError, showSuccess } from '../../helpers';
import {
@@ -1,5 +1,5 @@
/*
Copyright (C) 2025 modelstoken
Copyright (C) 2025 QuantumNous
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as
@@ -14,7 +14,7 @@ GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
For commercial licensing, please contact admin@modelstoken.com
For commercial licensing, please contact support@quantumnous.com
*/
import React, { useEffect, useMemo, useState } from 'react';
@@ -1,5 +1,5 @@
/*
Copyright (C) 2025 modelstoken
Copyright (C) 2025 QuantumNous
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as
@@ -14,7 +14,7 @@ GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
For commercial licensing, please contact admin@modelstoken.com
For commercial licensing, please contact support@quantumnous.com
*/
import React, { useState } from 'react';
+2 -2
View File
@@ -1,5 +1,5 @@
/*
Copyright (C) 2025 modelstoken
Copyright (C) 2025 QuantumNous
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as
@@ -14,7 +14,7 @@ GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
For commercial licensing, please contact admin@modelstoken.com
For commercial licensing, please contact support@quantumnous.com
*/
import React from 'react';
+2 -2
View File
@@ -1,5 +1,5 @@
/*
Copyright (C) 2025 modelstoken
Copyright (C) 2025 QuantumNous
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as
@@ -14,7 +14,7 @@ GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
For commercial licensing, please contact admin@modelstoken.com
For commercial licensing, please contact support@quantumnous.com
*/
import React from 'react';
+2 -2
View File
@@ -1,5 +1,5 @@
/*
Copyright (C) 2025 modelstoken
Copyright (C) 2025 QuantumNous
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as
@@ -14,7 +14,7 @@ GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
For commercial licensing, please contact admin@modelstoken.com
For commercial licensing, please contact support@quantumnous.com
*/
import React from 'react';
@@ -1,5 +1,5 @@
/*
Copyright (C) 2025 modelstoken
Copyright (C) 2025 QuantumNous
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as
@@ -14,7 +14,7 @@ GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
For commercial licensing, please contact admin@modelstoken.com
For commercial licensing, please contact support@quantumnous.com
*/
import ReactMarkdown from 'react-markdown';
@@ -1,5 +1,5 @@
/*
Copyright (C) 2025 modelstoken
Copyright (C) 2025 QuantumNous
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as
@@ -14,7 +14,7 @@ GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
For commercial licensing, please contact admin@modelstoken.com
For commercial licensing, please contact support@quantumnous.com
*/
import React, { useCallback, useEffect, useMemo, useState } from 'react';
@@ -1,5 +1,5 @@
/*
Copyright (C) 2025 modelstoken
Copyright (C) 2025 QuantumNous
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as
@@ -14,7 +14,7 @@ GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
For commercial licensing, please contact admin@modelstoken.com
For commercial licensing, please contact support@quantumnous.com
*/
import React, { useEffect, useState } from 'react';
@@ -1,5 +1,5 @@
/*
Copyright (C) 2025 modelstoken
Copyright (C) 2025 QuantumNous
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as
@@ -14,7 +14,7 @@ GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
For commercial licensing, please contact admin@modelstoken.com
For commercial licensing, please contact support@quantumnous.com
*/
import React from 'react';
+2 -2
View File
@@ -1,5 +1,5 @@
/*
Copyright (C) 2025 modelstoken
Copyright (C) 2025 QuantumNous
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as
@@ -14,7 +14,7 @@ GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
For commercial licensing, please contact admin@modelstoken.com
For commercial licensing, please contact support@quantumnous.com
*/
import React, { useState } from 'react';
+2 -2
View File
@@ -1,5 +1,5 @@
/*
Copyright (C) 2025 modelstoken
Copyright (C) 2025 QuantumNous
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as
@@ -14,7 +14,7 @@ GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
For commercial licensing, please contact admin@modelstoken.com
For commercial licensing, please contact support@quantumnous.com
*/
import React, { useState, useEffect, useRef } from 'react';
+2 -2
View File
@@ -1,5 +1,5 @@
/*
Copyright (C) 2025 modelstoken
Copyright (C) 2025 QuantumNous
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as
@@ -14,7 +14,7 @@ GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
For commercial licensing, please contact admin@modelstoken.com
For commercial licensing, please contact support@quantumnous.com
*/
import React from 'react';
+2 -2
View File
@@ -1,5 +1,5 @@
/*
Copyright (C) 2025 modelstoken
Copyright (C) 2025 QuantumNous
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as
@@ -14,7 +14,7 @@ GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
For commercial licensing, please contact admin@modelstoken.com
For commercial licensing, please contact support@quantumnous.com
*/
import React from 'react';
+2 -2
View File
@@ -1,5 +1,5 @@
/*
Copyright (C) 2025 modelstoken
Copyright (C) 2025 QuantumNous
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as
@@ -14,7 +14,7 @@ GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
For commercial licensing, please contact admin@modelstoken.com
For commercial licensing, please contact support@quantumnous.com
*/
import React, { useState, useEffect, useCallback, useMemo } from 'react';
+2 -2
View File
@@ -1,5 +1,5 @@
/*
Copyright (C) 2025 modelstoken
Copyright (C) 2025 QuantumNous
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as
@@ -14,7 +14,7 @@ GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
For commercial licensing, please contact admin@modelstoken.com
For commercial licensing, please contact support@quantumnous.com
*/
import React from 'react';
+2 -2
View File
@@ -1,5 +1,5 @@
/*
Copyright (C) 2025 modelstoken
Copyright (C) 2025 QuantumNous
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as
@@ -14,7 +14,7 @@ GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
For commercial licensing, please contact admin@modelstoken.com
For commercial licensing, please contact support@quantumnous.com
*/
import React from 'react';
@@ -1,5 +1,5 @@
/*
Copyright (C) 2025 modelstoken
Copyright (C) 2025 QuantumNous
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as
@@ -14,7 +14,7 @@ GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
For commercial licensing, please contact admin@modelstoken.com
For commercial licensing, please contact support@quantumnous.com
*/
import React, {
@@ -1,5 +1,5 @@
/*
Copyright (C) 2025 modelstoken
Copyright (C) 2025 QuantumNous
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as
@@ -14,7 +14,7 @@ GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
For commercial licensing, please contact admin@modelstoken.com
For commercial licensing, please contact support@quantumnous.com
*/
import React, { useState, useRef, useEffect } from 'react';
@@ -1,5 +1,5 @@
/*
Copyright (C) 2025 modelstoken
Copyright (C) 2025 QuantumNous
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as
@@ -14,7 +14,7 @@ GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
For commercial licensing, please contact admin@modelstoken.com
For commercial licensing, please contact support@quantumnous.com
*/
import React from 'react';
+2 -2
View File
@@ -1,5 +1,5 @@
/*
Copyright (C) 2025 modelstoken
Copyright (C) 2025 QuantumNous
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as
@@ -14,7 +14,7 @@ GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
For commercial licensing, please contact admin@modelstoken.com
For commercial licensing, please contact support@quantumnous.com
*/
import React from 'react';
+2 -2
View File
@@ -1,5 +1,5 @@
/*
Copyright (C) 2025 modelstoken
Copyright (C) 2025 QuantumNous
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as
@@ -14,7 +14,7 @@ GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
For commercial licensing, please contact admin@modelstoken.com
For commercial licensing, please contact support@quantumnous.com
*/
import React from 'react';
+2 -2
View File
@@ -1,5 +1,5 @@
/*
Copyright (C) 2025 modelstoken
Copyright (C) 2025 QuantumNous
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as
@@ -14,7 +14,7 @@ GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
For commercial licensing, please contact admin@modelstoken.com
For commercial licensing, please contact support@quantumnous.com
*/
import React from 'react';
+2 -2
View File
@@ -1,5 +1,5 @@
/*
Copyright (C) 2025 modelstoken
Copyright (C) 2025 QuantumNous
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as
@@ -14,7 +14,7 @@ GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
For commercial licensing, please contact admin@modelstoken.com
For commercial licensing, please contact support@quantumnous.com
*/
import React from 'react';
+2 -2
View File
@@ -1,5 +1,5 @@
/*
Copyright (C) 2025 modelstoken
Copyright (C) 2025 QuantumNous
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as
@@ -14,7 +14,7 @@ GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
For commercial licensing, please contact admin@modelstoken.com
For commercial licensing, please contact support@quantumnous.com
*/
import React from 'react';
+2 -2
View File
@@ -1,5 +1,5 @@
/*
Copyright (C) 2025 modelstoken
Copyright (C) 2025 QuantumNous
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as
@@ -14,7 +14,7 @@ GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
For commercial licensing, please contact admin@modelstoken.com
For commercial licensing, please contact support@quantumnous.com
*/
import React from 'react';
+2 -2
View File
@@ -1,5 +1,5 @@
/*
Copyright (C) 2025 modelstoken
Copyright (C) 2025 QuantumNous
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as
@@ -14,7 +14,7 @@ GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
For commercial licensing, please contact admin@modelstoken.com
For commercial licensing, please contact support@quantumnous.com
*/
import React, { useContext, useEffect } from 'react';
@@ -1,5 +1,5 @@
/*
Copyright (C) 2025 modelstoken
Copyright (C) 2025 QuantumNous
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as
@@ -14,7 +14,7 @@ GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
For commercial licensing, please contact admin@modelstoken.com
For commercial licensing, please contact support@quantumnous.com
*/
import React, { useRef } from 'react';
+114 -7
View File
@@ -1,5 +1,5 @@
/*
Copyright (C) 2025 modelstoken
Copyright (C) 2025 QuantumNous
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as
@@ -14,7 +14,7 @@ GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
For commercial licensing, please contact admin@modelstoken.com
For commercial licensing, please contact support@quantumnous.com
*/
import React, { useEffect, useState, useMemo, useContext } from 'react';
@@ -56,14 +56,14 @@ const FooterBar = () => {
/>
</div>
<div className='grid grid-cols-1 sm:grid-cols-2 md:grid-cols-2 gap-8 w-full'>
<div className='grid grid-cols-1 sm:grid-cols-2 md:grid-cols-4 gap-8 w-full'>
<div className='text-left'>
<p className='!text-semi-color-text-0 font-semibold mb-5'>
{t('关于我们')}
</p>
<div className='flex flex-col gap-4'>
<a
href='https://modelstoken.com'
href='https://docs.newapi.pro/wiki/project-introduction/'
target='_blank'
rel='noopener noreferrer'
className='!text-semi-color-text-1'
@@ -71,13 +71,21 @@ const FooterBar = () => {
{t('关于项目')}
</a>
<a
href='https://modelstoken.com'
href='https://docs.newapi.pro/support/community-interaction/'
target='_blank'
rel='noopener noreferrer'
className='!text-semi-color-text-1'
>
{t('联系我们')}
</a>
<a
href='https://docs.newapi.pro/wiki/features-introduction/'
target='_blank'
rel='noopener noreferrer'
className='!text-semi-color-text-1'
>
{t('功能特性')}
</a>
</div>
</div>
@@ -87,7 +95,7 @@ const FooterBar = () => {
</p>
<div className='flex flex-col gap-4'>
<a
href='https://modelstoken.com'
href='https://docs.newapi.pro/getting-started/'
target='_blank'
rel='noopener noreferrer'
className='!text-semi-color-text-1'
@@ -95,7 +103,15 @@ const FooterBar = () => {
{t('快速开始')}
</a>
<a
href='https://modelstoken.com'
href='https://docs.newapi.pro/installation/'
target='_blank'
rel='noopener noreferrer'
className='!text-semi-color-text-1'
>
{t('安装指南')}
</a>
<a
href='https://docs.newapi.pro/api/'
target='_blank'
rel='noopener noreferrer'
className='!text-semi-color-text-1'
@@ -104,6 +120,70 @@ const FooterBar = () => {
</a>
</div>
</div>
<div className='text-left'>
<p className='!text-semi-color-text-0 font-semibold mb-5'>
{t('相关项目')}
</p>
<div className='flex flex-col gap-4'>
<a
href='https://github.com/songquanpeng/one-api'
target='_blank'
rel='noopener noreferrer'
className='!text-semi-color-text-1'
>
One API
</a>
<a
href='https://github.com/novicezk/midjourney-proxy'
target='_blank'
rel='noopener noreferrer'
className='!text-semi-color-text-1'
>
Midjourney-Proxy
</a>
<a
href='https://github.com/Calcium-Ion/new-api-key-tool'
target='_blank'
rel='noopener noreferrer'
className='!text-semi-color-text-1'
>
new-api-key-tool
</a>
</div>
</div>
<div className='text-left'>
<p className='!text-semi-color-text-0 font-semibold mb-5'>
{t('友情链接')}
</p>
<div className='flex flex-col gap-4'>
<a
href='https://github.com/Calcium-Ion/new-api-horizon'
target='_blank'
rel='noopener noreferrer'
className='!text-semi-color-text-1'
>
new-api-horizon
</a>
<a
href='https://github.com/coaidev/coai'
target='_blank'
rel='noopener noreferrer'
className='!text-semi-color-text-1'
>
CoAI
</a>
<a
href='https://www.gpt-load.com/'
target='_blank'
rel='noopener noreferrer'
className='!text-semi-color-text-1'
>
GPT-Load
</a>
</div>
</div>
</div>
</div>
)}
@@ -114,6 +194,20 @@ const FooterBar = () => {
© {currentYear} {systemName}. {t('版权所有')}
</Typography.Text>
</div>
<div className='text-sm'>
<span className='!text-semi-color-text-1'>
{t('设计与开发由')}{' '}
</span>
<a
href='https://github.com/QuantumNous/new-api'
target='_blank'
rel='noopener noreferrer'
className='!text-semi-color-primary font-medium'
>
New API
</a>
</div>
</div>
</footer>
),
@@ -133,6 +227,19 @@ const FooterBar = () => {
className='custom-footer na-cb6feafeb3990c78 text-sm !text-semi-color-text-1'
dangerouslySetInnerHTML={{ __html: footer }}
></div>
<div className='text-sm flex-shrink-0'>
<span className='!text-semi-color-text-1'>
{t('设计与开发由')}{' '}
</span>
<a
href='https://github.com/QuantumNous/new-api'
target='_blank'
rel='noopener noreferrer'
className='!text-semi-color-primary font-medium'
>
New API
</a>
</div>
</div>
</footer>
) : (
+2 -2
View File
@@ -1,5 +1,5 @@
/*
Copyright (C) 2025 modelstoken
Copyright (C) 2025 QuantumNous
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as
@@ -14,7 +14,7 @@ GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
For commercial licensing, please contact admin@modelstoken.com
For commercial licensing, please contact support@quantumnous.com
*/
import React, { useEffect, useState, useContext, useMemo } from 'react';
+2 -2
View File
@@ -1,5 +1,5 @@
/*
Copyright (C) 2025 modelstoken
Copyright (C) 2025 QuantumNous
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as
@@ -14,7 +14,7 @@ GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
For commercial licensing, please contact admin@modelstoken.com
For commercial licensing, please contact support@quantumnous.com
*/
import HeaderBar from './headerbar';
+2 -2
View File
@@ -1,5 +1,5 @@
/*
Copyright (C) 2025 modelstoken
Copyright (C) 2025 QuantumNous
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as
@@ -14,7 +14,7 @@ GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
For commercial licensing, please contact admin@modelstoken.com
For commercial licensing, please contact support@quantumnous.com
*/
import React, { useContext, useEffect } from 'react';
+2 -2
View File
@@ -1,5 +1,5 @@
/*
Copyright (C) 2025 modelstoken
Copyright (C) 2025 QuantumNous
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as
@@ -14,7 +14,7 @@ GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
For commercial licensing, please contact admin@modelstoken.com
For commercial licensing, please contact support@quantumnous.com
*/
import React, { useEffect, useMemo, useState } from 'react';
@@ -1,5 +1,5 @@
/*
Copyright (C) 2025 modelstoken
Copyright (C) 2025 QuantumNous
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as
@@ -14,7 +14,7 @@ GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
For commercial licensing, please contact admin@modelstoken.com
For commercial licensing, please contact support@quantumnous.com
*/
import React from 'react';
@@ -1,5 +1,5 @@
/*
Copyright (C) 2025 modelstoken
Copyright (C) 2025 QuantumNous
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as
@@ -14,7 +14,7 @@ GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
For commercial licensing, please contact admin@modelstoken.com
For commercial licensing, please contact support@quantumnous.com
*/
import React from 'react';
+2 -2
View File
@@ -1,5 +1,5 @@
/*
Copyright (C) 2025 modelstoken
Copyright (C) 2025 QuantumNous
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as
@@ -14,7 +14,7 @@ GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
For commercial licensing, please contact admin@modelstoken.com
For commercial licensing, please contact support@quantumnous.com
*/
import React from 'react';
@@ -1,5 +1,5 @@
/*
Copyright (C) 2025 modelstoken
Copyright (C) 2025 QuantumNous
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as
@@ -14,7 +14,7 @@ GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
For commercial licensing, please contact admin@modelstoken.com
For commercial licensing, please contact support@quantumnous.com
*/
import React from 'react';
@@ -1,5 +1,5 @@
/*
Copyright (C) 2025 modelstoken
Copyright (C) 2025 QuantumNous
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as
@@ -14,7 +14,7 @@ GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
For commercial licensing, please contact admin@modelstoken.com
For commercial licensing, please contact support@quantumnous.com
*/
import React from 'react';
+2 -2
View File
@@ -1,5 +1,5 @@
/*
Copyright (C) 2025 modelstoken
Copyright (C) 2025 QuantumNous
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as
@@ -14,7 +14,7 @@ GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
For commercial licensing, please contact admin@modelstoken.com
For commercial licensing, please contact support@quantumnous.com
*/
import React from 'react';
@@ -1,5 +1,5 @@
/*
Copyright (C) 2025 modelstoken
Copyright (C) 2025 QuantumNous
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as
@@ -14,7 +14,7 @@ GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
For commercial licensing, please contact admin@modelstoken.com
For commercial licensing, please contact support@quantumnous.com
*/
import React from 'react';
@@ -1,5 +1,5 @@
/*
Copyright (C) 2025 modelstoken
Copyright (C) 2025 QuantumNous
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as
@@ -14,7 +14,7 @@ GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
For commercial licensing, please contact admin@modelstoken.com
For commercial licensing, please contact support@quantumnous.com
*/
import React from 'react';
@@ -1,5 +1,5 @@
/*
Copyright (C) 2025 modelstoken
Copyright (C) 2025 QuantumNous
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as
@@ -14,7 +14,7 @@ GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
For commercial licensing, please contact admin@modelstoken.com
For commercial licensing, please contact support@quantumnous.com
*/
import React, { useMemo } from 'react';
+2 -2
View File
@@ -1,5 +1,5 @@
/*
Copyright (C) 2025 modelstoken
Copyright (C) 2025 QuantumNous
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as
@@ -14,7 +14,7 @@ GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
For commercial licensing, please contact admin@modelstoken.com
For commercial licensing, please contact support@quantumnous.com
*/
import React, { useRef } from 'react';
+2 -2
View File
@@ -1,5 +1,5 @@
/*
Copyright (C) 2025 modelstoken
Copyright (C) 2025 QuantumNous
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as
@@ -14,7 +14,7 @@ GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
For commercial licensing, please contact admin@modelstoken.com
For commercial licensing, please contact support@quantumnous.com
*/
import React from 'react';
@@ -1,5 +1,5 @@
/*
Copyright (C) 2025 modelstoken
Copyright (C) 2025 QuantumNous
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as
@@ -14,7 +14,7 @@ GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
For commercial licensing, please contact admin@modelstoken.com
For commercial licensing, please contact support@quantumnous.com
*/
import React from 'react';
+2 -2
View File
@@ -1,5 +1,5 @@
/*
Copyright (C) 2025 modelstoken
Copyright (C) 2025 QuantumNous
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as
@@ -14,7 +14,7 @@ GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
For commercial licensing, please contact admin@modelstoken.com
For commercial licensing, please contact support@quantumnous.com
*/
import React from 'react';
+2 -2
View File
@@ -1,5 +1,5 @@
/*
Copyright (C) 2025 modelstoken
Copyright (C) 2025 QuantumNous
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as
@@ -14,7 +14,7 @@ GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
For commercial licensing, please contact admin@modelstoken.com
For commercial licensing, please contact support@quantumnous.com
*/
import React, { useState, useMemo, useCallback } from 'react';
+2 -2
View File
@@ -1,5 +1,5 @@
/*
Copyright (C) 2025 modelstoken
Copyright (C) 2025 QuantumNous
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as
@@ -14,7 +14,7 @@ GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
For commercial licensing, please contact admin@modelstoken.com
For commercial licensing, please contact support@quantumnous.com
*/
import React, { useRef } from 'react';
@@ -1,5 +1,5 @@
/*
Copyright (C) 2025 modelstoken
Copyright (C) 2025 QuantumNous
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as
@@ -14,7 +14,7 @@ GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
For commercial licensing, please contact admin@modelstoken.com
For commercial licensing, please contact support@quantumnous.com
*/
import React, { useRef, useEffect, useCallback } from 'react';
@@ -1,5 +1,5 @@
/*
Copyright (C) 2025 modelstoken
Copyright (C) 2025 QuantumNous
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as
@@ -14,7 +14,7 @@ GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
For commercial licensing, please contact admin@modelstoken.com
For commercial licensing, please contact support@quantumnous.com
*/
import React, { useState, useEffect } from 'react';

Some files were not shown because too many files have changed in this diff Show More