Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
7b80a5e
修改端口
yuansirrrrr Jun 2, 2026
62961b2
feat: add ZLHub Seedance video adapter
yuansirrrrr Jun 2, 2026
7b45682
fix: build custom deployment image from source
yuansirrrrr Jun 2, 2026
5f1270c
fix: use configurable Go proxy for Docker builds
yuansirrrrr Jun 2, 2026
cafa13e
切换default的ui
yuansirrrrr Jun 3, 2026
d314987
fix_docker_compose
yuansirrrrr Jun 3, 2026
98a0bd0
feat: add official Alipay topup support
yuansirrrrr Jun 3, 2026
fdf72ef
增加支付宝官方渠道
yuansirrrrr Jun 4, 2026
6d68175
修改首页ui_v1
yuansirrrrr Jun 4, 2026
cfc97d5
延长视频代理超时时间
yuansirrrrr Jun 5, 2026
ef6e164
Add ZLHub asset audit relay
yuansirrrrr Jun 6, 2026
85368d5
Fix Codex OAuth channel proxy flow
yuansirrrrr Jun 8, 2026
8cc8e2f
Handle saved Codex OAuth channel credentials
yuansirrrrr Jun 8, 2026
0e8df73
Accept saved Codex OAuth response
yuansirrrrr Jun 8, 2026
ef5c246
Add Deeprouter user guide docs
yuansirrrrr Jun 17, 2026
bbec5a8
修改用户指南为mdx
yuansirrrrr Jun 17, 2026
0454bbe
添加文档语言切换
yuansirrrrr Jun 18, 2026
dc59654
Add ZLHub Seedance 2.0 Mini support
yuansirrrrr Jul 9, 2026
51100eb
feat(web): refresh deeprouter homepage
yuansirrrrr Jul 14, 2026
4104833
feat: add DreamBrand video channel adapter
yuansirrrrr Jul 28, 2026
746c2fb
feat: map DreamBrand Seedance model aliases
yuansirrrrr Jul 29, 2026
54b5ec7
feat: support DreamBrand Seedream generation tasks
yuansirrrrr Jul 29, 2026
38f5634
feat: align DreamBrand models with unified APIs
yuansirrrrr Jul 29, 2026
c689a63
feat: support official Seedream and Seedance requests
yuansirrrrr Jul 30, 2026
d97a5cb
fix: defer DreamBrand resolution limits upstream
yuansirrrrr Jul 30, 2026
5f2eba1
fix: extend DreamBrand image polling timeout
yuansirrrrr Jul 30, 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
17 changes: 13 additions & 4 deletions Dockerfile
Original file line number Diff line number Diff line change
@@ -1,28 +1,34 @@
FROM oven/bun:1@sha256:0733e50325078969732ebe3b15ce4c4be5082f18c4ac1a0f0ca4839c2e4e42a7 AS builder

ARG BUN_REGISTRY=https://registry.npmjs.org

WORKDIR /build/web
COPY web/package.json web/bun.lock ./
COPY web/default/package.json ./default/package.json
COPY web/classic/package.json ./classic/package.json
RUN bun install --frozen-lockfile
RUN bun install --frozen-lockfile --registry=${BUN_REGISTRY}
COPY ./web/default ./default
COPY ./VERSION /build/VERSION
RUN cd default && DISABLE_ESLINT_PLUGIN='true' VITE_REACT_APP_VERSION=$(cat /build/VERSION) bun run build

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

ARG BUN_REGISTRY=https://registry.npmjs.org

WORKDIR /build/web
COPY web/package.json web/bun.lock ./
COPY web/default/package.json ./default/package.json
COPY web/classic/package.json ./classic/package.json
RUN bun install --frozen-lockfile
RUN bun install --frozen-lockfile --registry=${BUN_REGISTRY}
COPY ./web/classic ./classic
COPY ./VERSION /build/VERSION
RUN cd classic && VITE_REACT_APP_VERSION=$(cat /build/VERSION) bun run build

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

ARG GOPROXY=https://goproxy.cn,direct
ENV GOPROXY=${GOPROXY}
ARG TARGETOS
ARG TARGETARCH
ENV GOOS=${TARGETOS:-linux} GOARCH=${TARGETARCH:-amd64}
Expand All @@ -40,8 +46,11 @@ RUN go build -ldflags "-s -w -X 'github.com/QuantumNous/new-api/common.Version=$

FROM debian:bookworm-slim@sha256:f06537653ac770703bc45b4b113475bd402f451e85223f0f2837acbf89ab020a

RUN apt-get update \
&& apt-get install -y --no-install-recommends ca-certificates tzdata libasan8 wget \
ARG DEBIAN_MIRROR=mirrors.aliyun.com

RUN sed -i "s|http://deb.debian.org|http://${DEBIAN_MIRROR}|g" /etc/apt/sources.list.d/debian.sources \
&& apt-get -o Acquire::Retries=5 -o Acquire::http::Timeout=30 update \
&& apt-get -o Acquire::Retries=5 -o Acquire::http::Timeout=30 install -y --no-install-recommends ca-certificates tzdata libasan8 wget \
&& rm -rf /var/lib/apt/lists/* \
&& update-ca-certificates

Expand Down
9 changes: 7 additions & 2 deletions Dockerfile.dev
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@
FROM golang:1.26.1-alpine AS builder

ENV GO111MODULE=on CGO_ENABLED=0
ARG GOPROXY=https://goproxy.cn,direct
ENV GOPROXY=${GOPROXY}
ARG TARGETOS
ARG TARGETARCH
ENV GOOS=${TARGETOS:-linux} GOARCH=${TARGETARCH:-amd64}
Expand All @@ -24,8 +26,11 @@ RUN go build -ldflags "-s -w -X 'github.com/QuantumNous/new-api/common.Version=$

FROM debian:bookworm-slim

RUN apt-get update \
&& apt-get install -y --no-install-recommends ca-certificates tzdata wget \
ARG DEBIAN_MIRROR=mirrors.aliyun.com

RUN sed -i "s|http://deb.debian.org|http://${DEBIAN_MIRROR}|g" /etc/apt/sources.list.d/debian.sources \
&& apt-get -o Acquire::Retries=5 -o Acquire::http::Timeout=30 update \
&& apt-get -o Acquire::Retries=5 -o Acquire::http::Timeout=30 install -y --no-install-recommends ca-certificates tzdata wget \
&& rm -rf /var/lib/apt/lists/* \
&& update-ca-certificates

Expand Down
2 changes: 2 additions & 0 deletions common/api_type.go
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,8 @@ func ChannelType2APIType(channelType int) (int, bool) {
apiType = constant.APITypeReplicate
case constant.ChannelTypeCodex:
apiType = constant.APITypeCodex
case constant.ChannelTypeDreamBrand:
apiType = constant.APITypeDreamBrand
}
if apiType == -1 {
return constant.APITypeOpenAI, false
Expand Down
1 change: 1 addition & 0 deletions constant/api_type.go
Original file line number Diff line number Diff line change
Expand Up @@ -36,5 +36,6 @@ const (
APITypeMiniMax
APITypeReplicate
APITypeCodex
APITypeDreamBrand
APITypeDummy // this one is only for count, do not add any channel after this
)
3 changes: 3 additions & 0 deletions constant/asset_audit.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
package constant

const AssetAuditModelName = "zlhub-asset-audit"
9 changes: 9 additions & 0 deletions constant/channel.go
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,9 @@ const (
ChannelTypeSora = 55
ChannelTypeReplicate = 56
ChannelTypeCodex = 57
ChannelTypeZLHubVideo = 58
ChannelTypeZLHubAsset = 59
ChannelTypeDreamBrand = 60
ChannelTypeDummy // this one is only for count, do not add any channel after this

)
Expand Down Expand Up @@ -118,6 +121,9 @@ var ChannelBaseURLs = []string{
"https://api.openai.com", //55
"https://api.replicate.com", //56
"https://chatgpt.com", //57
"https://api.zlhub.cn", //58
"https://asset.zlhub.cn", //59
"https://ai.dreambrand.studio", //60
}

var ChannelTypeNames = map[int]string{
Expand Down Expand Up @@ -175,6 +181,9 @@ var ChannelTypeNames = map[int]string{
ChannelTypeSora: "Sora",
ChannelTypeReplicate: "Replicate",
ChannelTypeCodex: "Codex",
ChannelTypeZLHubVideo: "ZLHubVideo",
ChannelTypeZLHubAsset: "ZLHubAsset",
ChannelTypeDreamBrand: "DreamBrand",
}

func GetChannelTypeName(channelType int) string {
Expand Down
212 changes: 212 additions & 0 deletions controller/asset_audit.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,212 @@
package controller

import (
"bytes"
"crypto/rand"
"encoding/hex"
"fmt"
"io"
"net/http"
"net/url"
"strings"
"time"

"github.com/QuantumNous/new-api/common"
"github.com/QuantumNous/new-api/constant"
"github.com/QuantumNous/new-api/dto"
"github.com/QuantumNous/new-api/logger"
"github.com/QuantumNous/new-api/model"
relaycommon "github.com/QuantumNous/new-api/relay/common"
"github.com/QuantumNous/new-api/relay/helper"
"github.com/QuantumNous/new-api/service"
"github.com/QuantumNous/new-api/types"

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

const assetAuditTrackIDBytes = 16

func RelayAssetAuditUploadSync(c *gin.Context) {
relayAssetAudit(c, http.MethodPost, "/api/asset/upload/sync")
}

func RelayAssetAuditUploadAsync(c *gin.Context) {
relayAssetAudit(c, http.MethodPost, "/api/asset/upload/async")
}

func RelayAssetAuditTask(c *gin.Context) {
taskID := strings.TrimSpace(c.Param("task_id"))
if taskID == "" {
c.JSON(http.StatusBadRequest, gin.H{
"code": "invalid_request",
"message": "task_id is required",
})
return
}
relayAssetAudit(c, http.MethodGet, "/api/task/"+url.PathEscape(taskID))
}

func relayAssetAudit(c *gin.Context, method string, upstreamPath string) {
if common.GetContextKeyInt(c, constant.ContextKeyChannelType) != constant.ChannelTypeZLHubAsset {
c.JSON(http.StatusBadRequest, gin.H{
"code": "invalid_channel_type",
"message": "asset audit requests require a ZLHubAsset channel",
})
return
}

info := newAssetAuditRelayInfo(c)
priceData, err := helper.ModelPriceHelperPerCall(c, info)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{
"code": "model_price_error",
"message": err.Error(),
})
return
}
info.PriceData = priceData

if !priceData.FreeModel {
if apiErr := service.PreConsumeBilling(c, priceData.Quota, info); apiErr != nil {
c.JSON(apiErr.StatusCode, gin.H{"error": apiErr.ToOpenAIError()})
return
}
}

resp, respBody, err := doAssetAuditRequest(c, method, upstreamPath)
if err != nil {
if info.Billing != nil {
info.Billing.Refund(c)
}
c.JSON(http.StatusInternalServerError, gin.H{
"code": "do_request_failed",
"message": err.Error(),
})
return
}
defer resp.Body.Close()

if resp.StatusCode >= http.StatusBadRequest {
if info.Billing != nil {
info.Billing.Refund(c)
}
service.IOCopyBytesGracefully(c, resp, respBody)
return
}

if err := service.SettleBilling(c, info, priceData.Quota); err != nil {
logger.LogError(c, fmt.Sprintf("asset audit settle billing failed: %v", err))
}
recordAssetAuditConsumeLog(c, info, priceData.Quota, upstreamPath)
Comment on lines +69 to +100

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🗄️ Data Integrity & Integration | 🟠 Major | 🏗️ Heavy lift

Preserve quota-saturation metadata before logging.

This flow never calls attachQuotaSaturation before Line 100 writes the consume log, and no clamp result is propagated into that log. Use the checked quota result and attach its saturation metadata before recording consumption.

As per coding guidelines, “Billing paths must use checked quota helpers, propagate clamp information, and call attachQuotaSaturation before writing consume or task logs so saturation is auditable.”

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@controller/asset_audit.go` around lines 69 - 100, Update the billing flow
around PreConsumeBilling and recordAssetAuditConsumeLog to use the checked quota
result, retain its clamp or saturation information, and call
attachQuotaSaturation before writing the consumption log. Ensure the resulting
metadata is propagated to recordAssetAuditConsumeLog while preserving the
existing refund and settlement behavior.

Source: Coding guidelines


service.IOCopyBytesGracefully(c, resp, respBody)
}

func newAssetAuditRelayInfo(c *gin.Context) *relaycommon.RelayInfo {
startTime := common.GetContextKeyTime(c, constant.ContextKeyRequestStartTime)
if startTime.IsZero() {
startTime = time.Now()
}
tokenGroup := common.GetContextKeyString(c, constant.ContextKeyTokenGroup)
if tokenGroup == "" {
tokenGroup = common.GetContextKeyString(c, constant.ContextKeyUserGroup)
}
info := &relaycommon.RelayInfo{
RequestId: c.GetString(common.RequestIdKey),
UserId: common.GetContextKeyInt(c, constant.ContextKeyUserId),
UsingGroup: common.GetContextKeyString(c, constant.ContextKeyUsingGroup),
UserGroup: common.GetContextKeyString(c, constant.ContextKeyUserGroup),
UserQuota: common.GetContextKeyInt(c, constant.ContextKeyUserQuota),
UserEmail: common.GetContextKeyString(c, constant.ContextKeyUserEmail),
TokenId: common.GetContextKeyInt(c, constant.ContextKeyTokenId),
TokenKey: common.GetContextKeyString(c, constant.ContextKeyTokenKey),
TokenUnlimited: common.GetContextKeyBool(c, constant.ContextKeyTokenUnlimited),
TokenGroup: tokenGroup,
OriginModelName: constant.AssetAuditModelName,
StartTime: startTime,
RelayFormat: types.RelayFormatOpenAI,
}
info.InitChannelMeta(c)
return info
}

func doAssetAuditRequest(c *gin.Context, method string, upstreamPath string) (*http.Response, []byte, error) {
var body io.Reader
if method != http.MethodGet {
storage, err := common.GetBodyStorage(c)
if err != nil {
return nil, nil, err
}
bodyBytes, err := storage.Bytes()
if err != nil {
return nil, nil, err
}
body = bytes.NewReader(bodyBytes)
}

baseURL := strings.TrimRight(common.GetContextKeyString(c, constant.ContextKeyChannelBaseUrl), "/")
if baseURL == "" {
baseURL = strings.TrimRight(constant.ChannelBaseURLs[constant.ChannelTypeZLHubAsset], "/")
}
req, err := http.NewRequestWithContext(c.Request.Context(), method, baseURL+upstreamPath, body)
if err != nil {
return nil, nil, err
}

trackID, err := newAssetAuditTrackID()
if err != nil {
return nil, nil, err
}
req.Header.Set("Content-Type", "application/json")
req.Header.Set("Accept", "application/json")
req.Header.Set("X-Access-Token", common.GetContextKeyString(c, constant.ContextKeyChannelKey))
req.Header.Set("X-Track-Id", trackID)

proxy := ""
if setting, ok := common.GetContextKeyType[dto.ChannelSettings](c, constant.ContextKeyChannelSetting); ok {
proxy = setting.Proxy
}
client, err := service.GetHttpClientWithProxy(proxy)
if err != nil {
return nil, nil, err
}
resp, err := client.Do(req)
if err != nil {
if resp != nil && resp.Body != nil {
_ = resp.Body.Close()
}
return nil, nil, err
}
respBody, err := io.ReadAll(resp.Body)
if err != nil {
_ = resp.Body.Close()
return nil, nil, err
}
resp.Body = io.NopCloser(bytes.NewReader(respBody))
return resp, respBody, nil
}

func newAssetAuditTrackID() (string, error) {
buf := make([]byte, assetAuditTrackIDBytes)
if _, err := rand.Read(buf); err != nil {
return "", err
}
return hex.EncodeToString(buf), nil
}

func recordAssetAuditConsumeLog(c *gin.Context, info *relaycommon.RelayInfo, quota int, upstreamPath string) {
model.RecordConsumeLog(c, info.UserId, model.RecordConsumeLogParams{
ChannelId: info.ChannelId,
ModelName: constant.AssetAuditModelName,
TokenName: c.GetString("token_name"),
Quota: quota,
Content: fmt.Sprintf("asset audit request: %s", upstreamPath),
TokenId: info.TokenId,
UseTimeSeconds: int(time.Since(info.StartTime).Seconds()),
Group: info.UsingGroup,
Other: map[string]interface{}{
"asset_audit": true,
"path": upstreamPath,
},
})
}
3 changes: 3 additions & 0 deletions controller/channel-test.go
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,9 @@ func testChannel(channel *model.Channel, testUserID int, testModel string, endpo
constant.ChannelTypeKling,
constant.ChannelTypeJimeng,
constant.ChannelTypeDoubaoVideo,
constant.ChannelTypeZLHubVideo,
constant.ChannelTypeZLHubAsset,
constant.ChannelTypeDreamBrand,
constant.ChannelTypeVidu,
}
if lo.Contains(unsupportedTestChannelTypes, channel.Type) {
Expand Down
2 changes: 1 addition & 1 deletion controller/misc.go
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,7 @@ func GetStatus(c *gin.Context) {
"server_address": system_setting.ServerAddress,
"turnstile_check": common.TurnstileCheckEnabled,
"turnstile_site_key": common.TurnstileSiteKey,
"docs_link": operation_setting.GetGeneralSetting().DocsLink,
"docs_link": operation_setting.NormalizeDocsLink(operation_setting.GetGeneralSetting().DocsLink),
"quota_per_unit": common.QuotaPerUnit,
// 兼容旧前端:保留 display_in_currency,同时提供新的 quota_display_type
"display_in_currency": operation_setting.IsCurrencyDisplay(),
Expand Down
7 changes: 7 additions & 0 deletions controller/model.go
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,12 @@ func init() {
OwnedBy: minimax.ChannelName,
})
}
openAIModels = append(openAIModels, dto.OpenAIModels{
Id: constant.AssetAuditModelName,
Object: "model",
Created: 1626777600,
OwnedBy: "zlhub-asset",
})
for modelName, _ := range constant.MidjourneyModel2Action {
openAIModels = append(openAIModels, dto.OpenAIModels{
Id: modelName,
Expand All @@ -105,6 +111,7 @@ func init() {
adaptor.Init(meta)
channelId2Models[i] = adaptor.GetModelList()
}
channelId2Models[constant.ChannelTypeZLHubAsset] = []string{constant.AssetAuditModelName}
openAIModels = lo.UniqBy(openAIModels, func(m dto.OpenAIModels) string {
return m.Id
})
Expand Down
14 changes: 14 additions & 0 deletions controller/payment_webhook_availability.go
Original file line number Diff line number Diff line change
Expand Up @@ -108,3 +108,17 @@ func isEpayWebhookConfigured() bool {
func isEpayWebhookEnabled() bool {
return isEpayTopUpEnabled()
}

func isAlipayTopUpEnabled() bool {
if !isPaymentComplianceConfirmed() {
return false
}
return strings.TrimSpace(setting.AlipayAppId) != "" &&
strings.TrimSpace(setting.AlipayGateway) != "" &&
strings.TrimSpace(setting.AlipayPrivateKey) != "" &&
strings.TrimSpace(setting.AlipayPublicKey) != ""
}

func isAlipayWebhookEnabled() bool {
return isAlipayTopUpEnabled()
}
Loading