Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
e1936ec
version 0.0.1
gladhu Apr 12, 2026
89e5175
add docker-compose.local-build.yml
gladhu Apr 13, 2026
d47d805
Merge branch 'QuantumNous:main' into main
gladhu Apr 26, 2026
2907f4f
Merge branch 'QuantumNous:main' into main
gladhu Apr 28, 2026
8d57705
支持 导出账单,包括 月账单 和 消费明细
gladhu May 2, 2026
a3881c7
Merge branch 'QuantumNous:main' into main
gladhu May 2, 2026
ab1ffb6
- 默认主题:processChartData 支持传入筛选起止时间生成横轴;消耗分布与模型分析图传入 modelChartTimeRan…
gladhu May 5, 2026
eefb69e
增加自定义 首页
gladhu May 10, 2026
96de828
自定义首页修改
gladhu May 10, 2026
650267e
改动说明
gladhu May 10, 2026
ca3e268
修改说明
gladhu May 10, 2026
3e07224
fix 深色模式点击跳转首页有问题,跳到浅色了
gladhu May 10, 2026
0387fc2
点击顶层菜单 文档,打开了新的网页,请修改,当文档配置的 url 为本网站的内容时,不要打开新tab
gladhu May 10, 2026
d9d6799
换 logo
gladhu May 10, 2026
d3cf812
换新logo
gladhu May 13, 2026
6533864
操练场中的参数 Temperature和Top P默认只开启Top P
gladhu May 16, 2026
bdb50dd
feature: 管理员按渠道、按用户+渠道、按时间范围查看消费金额的功能
gladhu May 16, 2026
96ea7e3
feature: 支持导出日志
gladhu May 16, 2026
e9b4d1f
feature: 支持导出日志 -- 非管理员仅导出自己可见的字段
gladhu May 16, 2026
a900706
feature: 像默认主页一样,在免费开通和查看定价前 增加 API地址
gladhu May 16, 2026
4027955
参数覆盖 delete_header 的实现
gladhu May 23, 2026
6886ac4
已重设计 Hero 区 h1 两行主标题:去掉四色渐变,主行用 --text + 字重 700,副行用单色 slate 灰(hero-ti…
gladhu May 25, 2026
070205f
default 和 classic 保持一致
gladhu May 25, 2026
aae552f
集成文档(未充分验证)
gladhu May 25, 2026
1e62338
支持 opus-4-8(todo: 验证可行性)
gladhu May 29, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
30 changes: 27 additions & 3 deletions Dockerfile
Original file line number Diff line number Diff line change
@@ -1,26 +1,46 @@
# Build parallelism: reserve RESERVE_CPU_CORES for the host (default 1). Example:
# docker build --build-arg RESERVE_CPU_CORES=2 .
# Default/classic frontend stages may run in parallel; each uses roughly
# floor((nproc - RESERVE) / 2) via GOMAXPROCS so the pair is less likely to
# saturate all CPUs. The final Go compile stage uses (nproc - RESERVE).
FROM oven/bun:1@sha256:0733e50325078969732ebe3b15ce4c4be5082f18c4ac1a0f0ca4839c2e4e42a7 AS builder
ARG RESERVE_CPU_CORES=1

WORKDIR /build
COPY web/default/package.json .
COPY web/default/bun.lock .
RUN bun install
COPY ./web/default .
COPY ./VERSION .
RUN DISABLE_ESLINT_PLUGIN='true' VITE_REACT_APP_VERSION=$(cat VERSION) bun run build
RUN RESERVE="${RESERVE_CPU_CORES:-1}"; \
TOTAL=$(nproc); \
if [ "$TOTAL" -gt "$RESERVE" ]; then AVAIL=$((TOTAL - RESERVE)); else AVAIL=1; fi; \
USE=$((AVAIL / 2)); \
if [ "$USE" -lt 1 ]; then USE=1; fi; \
export GOMAXPROCS="$USE"; \
DISABLE_ESLINT_PLUGIN='true' VITE_REACT_APP_VERSION=$(cat VERSION) bun run build

FROM oven/bun:1@sha256:0733e50325078969732ebe3b15ce4c4be5082f18c4ac1a0f0ca4839c2e4e42a7 AS builder-classic
ARG RESERVE_CPU_CORES=1

WORKDIR /build
COPY web/classic/package.json .
COPY web/classic/bun.lock .
RUN bun install
COPY ./web/classic .
COPY ./VERSION .
RUN VITE_REACT_APP_VERSION=$(cat VERSION) bun run build
RUN RESERVE="${RESERVE_CPU_CORES:-1}"; \
TOTAL=$(nproc); \
if [ "$TOTAL" -gt "$RESERVE" ]; then AVAIL=$((TOTAL - RESERVE)); else AVAIL=1; fi; \
USE=$((AVAIL / 2)); \
if [ "$USE" -lt 1 ]; then USE=1; fi; \
export GOMAXPROCS="$USE"; \
VITE_REACT_APP_VERSION=$(cat VERSION) bun run build

FROM golang:1.26.1-alpine@sha256:2389ebfa5b7f43eeafbd6be0c3700cc46690ef842ad962f6c5bd6be49ed82039 AS builder2
ENV GO111MODULE=on CGO_ENABLED=0

ARG RESERVE_CPU_CORES=1
ARG TARGETOS
ARG TARGETARCH
ENV GOOS=${TARGETOS:-linux} GOARCH=${TARGETARCH:-amd64}
Expand All @@ -34,7 +54,11 @@ RUN go mod download
COPY . .
COPY --from=builder /build/dist ./web/default/dist
COPY --from=builder-classic /build/dist ./web/classic/dist
RUN go build -ldflags "-s -w -X 'github.com/QuantumNous/new-api/common.Version=$(cat VERSION)'" -o new-api
RUN RESERVE="${RESERVE_CPU_CORES:-1}"; \
TOTAL=$(nproc); \
if [ "$TOTAL" -gt "$RESERVE" ]; then USE=$((TOTAL - RESERVE)); else USE=1; fi; \
export GOMAXPROCS="$USE"; \
go build -ldflags "-s -w -X 'github.com/QuantumNous/new-api/common.Version=$(cat VERSION)'" -o new-api

FROM debian:bookworm-slim@sha256:f06537653ac770703bc45b4b113475bd402f451e85223f0f2837acbf89ab020a

Expand Down
9 changes: 8 additions & 1 deletion Dockerfile.dev
Original file line number Diff line number Diff line change
@@ -1,9 +1,12 @@
# Backend-only build for frontend development
# Skips frontend build, uses a placeholder for //go:embed web/dist
#
# Reserve RESERVE_CPU_CORES CPUs for the host during compile (default 1).

FROM golang:1.26.1-alpine AS builder

ENV GO111MODULE=on CGO_ENABLED=0
ARG RESERVE_CPU_CORES=1
ARG TARGETOS
ARG TARGETARCH
ENV GOOS=${TARGETOS:-linux} GOARCH=${TARGETARCH:-amd64}
Expand All @@ -20,7 +23,11 @@ RUN mkdir -p web/default/dist web/classic/dist && \
echo '<!doctype html><html><head><title>dev</title></head><body>use frontend dev server</body></html>' > web/default/dist/index.html && \
echo '<!doctype html><html><head><title>dev</title></head><body>use frontend dev server</body></html>' > web/classic/dist/index.html

RUN go build -ldflags "-s -w -X 'github.com/QuantumNous/new-api/common.Version=$(cat VERSION)'" -o new-api
RUN RESERVE="${RESERVE_CPU_CORES:-1}"; \
TOTAL=$(nproc); \
if [ "$TOTAL" -gt "$RESERVE" ]; then USE=$((TOTAL - RESERVE)); else USE=1; fi; \
export GOMAXPROCS="$USE"; \
go build -ldflags "-s -w -X 'github.com/QuantumNous/new-api/common.Version=$(cat VERSION)'" -o new-api

FROM debian:bookworm-slim

Expand Down
1 change: 1 addition & 0 deletions VERSION
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
0.0.1
2 changes: 1 addition & 1 deletion common/constants.go
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ var DrawingEnabled = true
var TaskEnabled = true
var DataExportEnabled = true
var DataExportInterval = 5 // unit: minute
var DataExportDefaultTime = "hour" // unit: minute
var DataExportDefaultTime = "day"
var DefaultCollapseSidebar = false // default value of collapse sidebar

// Any options with "Secret", "Token" in its key won't be return by GetOptions
Expand Down
109 changes: 109 additions & 0 deletions controller/channel_consumption.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
package controller

import (
"errors"
"net/http"
"strconv"
"strings"
"time"

"github.com/QuantumNous/new-api/common"
"github.com/QuantumNous/new-api/model"

"github.com/gin-gonic/gin"
)

func parseChannelConsumptionTimeRange(c *gin.Context) (startTimestamp, endTimestamp int64, err error) {
startTimestamp, _ = strconv.ParseInt(c.Query("start_timestamp"), 10, 64)
endTimestamp, _ = strconv.ParseInt(c.Query("end_timestamp"), 10, 64)
if startTimestamp == 0 && endTimestamp == 0 {
now := time.Now()
loc := now.Location()
monthStart := time.Date(now.Year(), now.Month(), 1, 0, 0, 0, 0, loc)
startTimestamp = monthStart.Unix()
endTimestamp = now.Unix()
return startTimestamp, endTimestamp, nil
}
if startTimestamp == 0 || endTimestamp == 0 {
return 0, 0, errors.New("start_timestamp and end_timestamp are required")
}
if endTimestamp < startTimestamp {
return 0, 0, errors.New("end_timestamp must be greater than or equal to start_timestamp")
}
return startTimestamp, endTimestamp, nil
}

// GetChannelConsumption returns consume quota aggregated for a channel in [start_timestamp, end_timestamp].
// Optional filters: user_id or username (for per-user consumption on this channel).
func GetChannelConsumption(c *gin.Context) {
channelId, err := strconv.Atoi(c.Param("id"))
if err != nil || channelId <= 0 {
common.ApiError(c, errors.New("invalid channel id"))
return
}

channel, err := model.GetChannelById(channelId, false)
if err != nil {
common.ApiError(c, err)
return
}

startTimestamp, endTimestamp, err := parseChannelConsumptionTimeRange(c)
if err != nil {
c.JSON(http.StatusOK, gin.H{
"success": false,
"message": err.Error(),
})
return
}

userId, _ := strconv.Atoi(c.Query("user_id"))
username := strings.TrimSpace(c.Query("username"))
if userId > 0 {
if _, err := model.GetUserById(userId, false); err != nil {
common.ApiError(c, errors.New("user not found"))
return
}
username = ""
} else if username != "" {
if _, err := model.GetUserByUsername(username); err != nil {
common.ApiError(c, errors.New("user not found"))
return
}
}

stat, err := model.SumChannelConsumption(channelId, userId, username, startTimestamp, endTimestamp)
if err != nil {
common.ApiError(c, err)
return
}

resp := gin.H{
"channel_id": channel.Id,
"channel_name": channel.Name,
"start_timestamp": startTimestamp,
"end_timestamp": endTimestamp,
"quota": stat.Quota,
"request_count": stat.RequestCount,
"prompt_tokens": stat.PromptTokens,
"completion_tokens": stat.CompletionTokens,
"lifetime_used_quota": channel.UsedQuota,
}
if userId > 0 {
resp["user_id"] = userId
if user, uerr := model.GetUserById(userId, false); uerr == nil && user != nil {
resp["username"] = user.Username
}
} else if username != "" {
resp["username"] = username
if user, uerr := model.GetUserByUsername(username); uerr == nil && user != nil {
resp["user_id"] = user.Id
}
}

c.JSON(http.StatusOK, gin.H{
"success": true,
"message": "",
"data": resp,
})
}
Loading
Loading