给自用 New API 开放用户统计

2284 字
11 分钟
给自用 New API 开放用户统计

最近自用的 New API 想开放一下 Dashboard 里的「用户统计」。

先说前提:这是自用实例,用户范围可控,不开放注册,也不是商用、多租户或者对外服务。所以我可以接受让普通登录用户看到实例内其他用户的用户名和消耗数据。如果你的 New API 是开放注册的,或者有明确的租户边界,这个改法就不合适。

New API 默认只有管理员能看「用户统计」,这个限制不是一个配置项,而是分散在前后端三处:

┌──────────┬──────────────────────────────────────────────────┐
│ 位置 │ 限制 │
├──────────┼──────────────────────────────────────────────────┤
│ 后端路由 │ /api/data/users 使用 AdminAuth │
│ 前端导航 │ users 被列入 ADMIN_ONLY_SECTIONS │
│ 页面标签 │ visibleSections 只向管理员显示 users │
└──────────┴──────────────────────────────────────────────────┘

所以只改前端没有用,接口还是会被后端拒绝;只改后端也不完整,前端入口仍然可能被藏起来。

我的做法是构建一个补丁镜像:构建时从 GitHub 拉取指定 commit 的 New API 源码,先用 grep 确认原代码还在,再用 sed 替换三处权限限制,然后重新构建前端和 Go 二进制。最终运行镜像还是基于官方 calciumion/new-api:latest,只替换里面的 /new-api

这样不用自己长期维护一份 New API 源码副本,也不会在运行容器里乱改文件。官方运行镜像本来也不带完整源码,想进容器改 router/api-router.goweb/src 这种思路基本走不通,前端也已经构建好了。

目录结构#

在原来的 docker-compose.yml 所在目录准备这些文件:

new-api/
├── docker-compose.yml
├── compose.override.yml
├── .env
└── newapi-patched/
└── Dockerfile

先创建目录:

Terminal window
mkdir -p newapi-patched

固定 New API 版本#

.env 里写入要使用的 New API commit SHA:

NEW_API_REF=填写目标commit_SHA

这里建议用完整 commit SHA,不要用 main。补丁这种东西最怕上游代码悄悄变化,今天构建和明天构建出来不是一个东西。固定 commit 至少能保证结果可复现,后续要升级时再手动改这个值。

如果现在用的是 latest,但不知道对应哪个版本,可以先从 New API 管理页面或者 /api/status 看版本,再去 GitHub 找对应 tag 或 commit。

补丁 Dockerfile#

创建 newapi-patched/Dockerfile

# syntax=docker/dockerfile:1
# ============================================================================
# 步骤1:获取指定版本源码
# ============================================================================
FROM alpine/git:latest AS source
ARG NEW_API_REF=main
RUN echo "开始获取 new-api 源码..." \
&& git init /src \
&& git -C /src remote add origin https://github.com/QuantumNous/new-api.git \
&& git -C /src fetch --depth=1 origin "${NEW_API_REF}" \
&& git -C /src checkout --detach FETCH_HEAD \
&& echo "源码获取完成: $(git -C /src rev-parse HEAD)"
WORKDIR /src
# ============================================================================
# 步骤2:替换三处权限限制
# grep 既是前置检查,也是上游代码变化后的失败保护。
# ============================================================================
RUN echo "开始修改用户统计权限..." \
&& grep -Fq \
"const ADMIN_ONLY_SECTIONS = new Set<string>(['users'])" \
web/src/features/dashboard/section-registry.tsx \
&& sed -i \
"s@const ADMIN_ONLY_SECTIONS = new Set<string>(\\['users'\\])@const ADMIN_ONLY_SECTIONS = new Set<string>()@" \
web/src/features/dashboard/section-registry.tsx \
&& grep -Fq \
"(section) => section !== 'overview' && (section !== 'users' || isAdmin)" \
web/src/features/dashboard/index.tsx \
&& sed -i \
"s@(section) => section !== 'overview' && (section !== 'users' || isAdmin)@(section) => section !== 'overview'@" \
web/src/features/dashboard/index.tsx \
&& grep -Fq \
'dataRoute.GET("/users", middleware.AdminAuth(), controller.GetQuotaDatesByUser)' \
router/api-router.go \
&& sed -i \
's@dataRoute.GET("/users", middleware.AdminAuth(), controller.GetQuotaDatesByUser)@dataRoute.GET("/users", middleware.UserAuth(), controller.GetQuotaDatesByUser)@' \
router/api-router.go \
&& grep -Fq \
"const ADMIN_ONLY_SECTIONS = new Set<string>()" \
web/src/features/dashboard/section-registry.tsx \
&& grep -Fq \
"(section) => section !== 'overview'" \
web/src/features/dashboard/index.tsx \
&& grep -Fq \
'dataRoute.GET("/users", middleware.UserAuth(), controller.GetQuotaDatesByUser)' \
router/api-router.go \
&& echo "用户统计权限修改完成"
# ============================================================================
# 步骤3:构建前端
# ============================================================================
FROM oven/bun:1 AS web-builder
WORKDIR /build/web
COPY --from=source /src/web/package.json /src/web/bun.lock ./
RUN echo "开始安装前端依赖..." \
&& bun install --frozen-lockfile \
&& echo "前端依赖安装完成"
COPY --from=source /src/web ./
COPY --from=source /src/VERSION /build/VERSION
RUN echo "开始构建前端..." \
&& DISABLE_ESLINT_PLUGIN=true \
VITE_REACT_APP_VERSION="$(cat /build/VERSION)" \
bun run build \
&& echo "前端构建完成"
# ============================================================================
# 步骤4:构建后端并嵌入前端资源
# ============================================================================
FROM golang:1.26.1-alpine AS api-builder
ENV GO111MODULE=on
ENV CGO_ENABLED=0
ENV GOEXPERIMENT=greenteagc
ARG TARGETOS
ARG TARGETARCH
ENV GOOS=${TARGETOS:-linux}
ENV GOARCH=${TARGETARCH:-amd64}
WORKDIR /build
COPY --from=source /src/go.mod /src/go.sum ./
RUN echo "开始下载 Go 依赖..." \
&& go mod download \
&& echo "Go 依赖下载完成"
COPY --from=source /src ./
COPY --from=web-builder /build/web/dist ./web/dist
RUN echo "开始构建 new-api..." \
&& go build \
-ldflags "-s -w -X 'github.com/QuantumNous/new-api/common.Version=$(cat VERSION)'" \
-o new-api \
&& echo "new-api 构建完成"
# ============================================================================
# 步骤5:保留官方运行环境,替换二进制
# ============================================================================
FROM calciumion/new-api:latest
COPY --from=api-builder /build/new-api /new-api

注意,Dockerfile 只支持 # 注释,不支持 /* ... */ 这种块注释。这个坑很低级,但一旦复制错了,报错就是 unknown instruction: /*

为什么要先 grep 再 sed#

这里的 grep 不是凑数,而是防止上游代码变了之后还继续硬改。

补丁依赖三处具体代码。如果 New API 后面改了文件路径、变量名、路由写法,旧的 sed 规则就可能不再适配。没有前置检查的话,最麻烦的情况不是构建失败,而是构建成功但行为不对。

所以每处替换前先 grep

  • 匹配到了,再执行替换;
  • 匹配不到,构建直接失败;
  • 构建失败后去核对源码,不要删除检查继续构建。

这种补丁镜像最重要的是可控,不是糊弄过去。

Compose 覆盖文件#

创建 compose.override.yml

services:
new-api:
build:
context: ./newapi-patched
dockerfile: Dockerfile
args:
NEW_API_REF: ${NEW_API_REF:?请在.env中设置NEW_API_REF}
image: local/newapi-user-stats:latest
pull_policy: build

这里的 new-api 必须和原 docker-compose.yml 里的服务名一致。如果你的服务名不是这个,覆盖文件和后面的命令都要一起改。

原 Compose 里的端口、数据库、Redis、环境变量、挂载卷和 depends_on 会继续保留。覆盖文件只负责把这个服务换成我们本地构建的镜像。

检查 Compose 配置#

先看最终配置,别急着构建:

Terminal window
docker compose \
-f docker-compose.yml \
-f compose.override.yml \
config

重点看 new-api 服务里有没有这些内容:

image: local/newapi-user-stats:latest
build:
context: .../newapi-patched

如果这里还在使用官方镜像,要么覆盖文件没生效,要么服务名写错了。

构建补丁镜像#

首次构建建议不用缓存:

Terminal window
docker compose \
-f docker-compose.yml \
-f compose.override.yml \
build --no-cache new-api

构建日志里应该能看到:

源码获取完成
用户统计权限修改完成
前端构建完成
new-api 构建完成

如果某个 grep 失败,构建会停下来。这时候不要想着把 grep 删了继续跑,先去看当前 commit 的源码是不是已经变了。

替换运行容器#

构建完成后启动服务:

Terminal window
docker compose \
-f docker-compose.yml \
-f compose.override.yml \
up -d new-api

查看状态:

Terminal window
docker compose \
-f docker-compose.yml \
-f compose.override.yml \
ps

查看日志:

Terminal window
docker compose \
-f docker-compose.yml \
-f compose.override.yml \
logs --tail=100 new-api

确认容器处于 runninghealthy 后,再去页面上看。

验证#

用普通用户账号登录,然后访问:

/dashboard/users

需要确认这些结果:

  1. Dashboard 显示「用户统计」入口。
  2. 页面能看到「用户消耗排行」和「用户消耗趋势」。
  3. 浏览器请求 /api/data/users 返回 HTTP 200。
  4. 未登录状态请求 /api/data/users 仍然被拒绝。
  5. 管理员原有功能不受影响。

后端这里改的是 UserAuth(),所以它只开放给已登录用户,不开放给匿名访客。如果匿名访客也能访问,那就不是预期行为,应该立刻回滚。

构建成功,但页面没变化#

一般从这几处查:

  1. 是否真的加载了 compose.override.yml
  2. 是否重新构建了镜像。
  3. 是否重新创建了容器。
  4. 浏览器缓存或反向代理缓存是否还在用旧资源。
  5. 如果是多实例部署,是否还有节点在跑旧镜像。

grep 校验失败#

不要跳过校验。

grep 失败通常意味着:

  • NEW_API_REF 填错;
  • 上游文件路径变了;
  • 上游代码写法变了;
  • 这套补丁不适配当前版本。

正确做法是看当前版本源码,重新确认三处权限逻辑,再调整替换规则。

升级#

后续要升级 New API 时:

  1. 在 GitHub 选择新的 tag 或 commit。
  2. 修改 .env 里的 NEW_API_REF
  3. 重新构建镜像。
  4. 重新启动服务。
  5. 重新验证普通用户、管理员和匿名访客的访问行为。

命令还是:

Terminal window
docker compose \
-f docker-compose.yml \
-f compose.override.yml \
build --no-cache new-api
docker compose \
-f docker-compose.yml \
-f compose.override.yml \
up -d new-api

数据库和挂载卷不会因为重新构建镜像而删除。

回滚#

如果改完之后服务异常,不加载覆盖文件,重新创建官方服务:

Terminal window
docker compose \
-f docker-compose.yml \
up -d --force-recreate new-api

这个操作不会删除数据库或数据卷。确认原服务恢复后,compose.override.ymlnewapi-patched/Dockerfile 可以先留着,后面要继续调也方便。

最后#

这个方案的核心就是:不长期维护源码副本,只在 Docker 构建阶段对指定 commit 做可校验的最小补丁。

好处是比较明确的:构建结果可复现,上游代码变化时会通过 grep 主动失败,运行镜像也尽量贴近官方镜像,原来的 Compose 配置、环境变量、数据卷基本不用动。

普通登录用户会看到实例内其他用户的用户名和消耗数据,相互之间可以看到使用量。对我这种自用、不开放注册、用户范围可信的环境来说可以接受;如果是生产、商用或者开放注册实例,就别这么改了。

文章分享

如果这篇文章对你有帮助,欢迎分享给更多人!

给自用 New API 开放用户统计
https://blog.qiui.net/posts/2026-07-28-new-api-user-stats/
作者
Qiui
发布于
2026-07-28
许可协议
CC BY-NC-SA 4.0

评论区

文章目录