Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
692e8d6
fix(web): restore admin unbinding for built-in providers (#6987)
zcxads666 Aug 29, 2026
ac381ac
fix(billing): 修复时间规则恒真表达式导致倍率全天生效 (#6934)
zcxads666 Aug 29, 2026
7037ac1
fix(docker): add relaykit go.mod to dev build context (#7072)
VladKabiak Aug 29, 2026
eb48396
feat(task): replace built-in task adaptors with a sandboxed JS plugin…
Calcium-Ion Aug 29, 2026
0f2a207
fix(relay): 请求参数校验错误返回 HTTP 400 (#6774)
ax2 Aug 29, 2026
98d50d5
fix(web): recheck setup status after page reload (#6968)
seefs001 Aug 29, 2026
b80d633
feat(auth): encrypt password login transport
Calcium-Ion Aug 29, 2026
8454082
feat(chat): add AQBot preset (#7079)
Licoy Aug 29, 2026
918427d
feat(auth): make password encryption opt-in #6743
Calcium-Ion Aug 29, 2026
6c22550
feat(task): resolve channel-mapped aliases and case variants for plug…
Calcium-Ion Aug 30, 2026
66031a0
fix(model): disable PostgreSQL prepared statements for pooler compati…
Calcium-Ion Aug 30, 2026
0bee5d4
fix(ali): honor image response format (#5513) (#7048)
PuppetKL Aug 30, 2026
dc4732c
feat(web): factory task plugins update only with the system
Calcium-Ion Aug 30, 2026
b5b94bc
fix(subscription): 无有效订阅时前端如实显示「仅用订阅」偏好 (#6222) (#7086)
CR-Yun Aug 30, 2026
1751f43
fix(sqlite): enable WAL + working busy timeout + _txlock=immediate to…
LinineTy Aug 30, 2026
6eb6f35
fix(model): return string from JSON column Valuers for pg simple prot…
Calcium-Ion Aug 30, 2026
b518d00
fix(relay): bound the wait for upstream response headers (fixes unbou…
txgo Aug 30, 2026
7415871
fix initialize database
Calcium-Ion Aug 30, 2026
69a41ee
fix(model): drop leftover prefill_groups unique constraints before Au…
seefs001 Aug 30, 2026
2bf0820
Revert "fix(model): drop leftover prefill_groups unique constraints b…
Calcium-Ion Aug 30, 2026
2b6f1df
fix(model): drop leftover prefill_groups unique constraints before Au…
Calcium-Ion Aug 30, 2026
19752b4
Merge upstream QuantumNous/new-api into main
chunfeng789 Aug 30, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
The table of contents is too big for display.
Diff view
Diff view
  •  
  •  
  •  
6 changes: 6 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,10 @@
# RELAY_TIMEOUT=0
# Relay HTTP 客户端空闲连接超时时间,单位秒,默认跟随 Go 标准库,设置为0表示不限制
# RELAY_IDLE_CONN_TIMEOUT=90
# 等待上游返回响应头的超时时间,单位秒,默认 1800,设置为 0 表示不限制。
# 仅约束「等待响应头」这一段;响应头返回之后的流式传输不受影响。
# 注意:非流式请求通常要等上游生成完毕才会返回响应头,因此该值需留足余量。
# RELAY_RESPONSE_HEADER_TIMEOUT=1800
# 流模式无响应超时时间,单位秒,如果出现空补全可以尝试改为更大值
# STREAMING_TIMEOUT=300

Expand All @@ -79,6 +83,8 @@

# 会话密钥
# SESSION_SECRET=random_string
# 登录密码请求体 RSA-OAEP 加密;默认关闭,且不能替代 HTTPS
# PASSWORD_LOGIN_ENCRYPTION_ENABLED=true
# false/未配置:本地 HTTP 模式,关闭 refresh/logout OriginGuard,且不得设置 TRUSTED_URL;兼容本地开发代理。
# true:启用 Secure Refresh Cookie 和严格 OriginGuard,必须同时列出全部可信 HTTPS Origin。
# SESSION_COOKIE_TRUSTED_URL 多项用英文逗号分隔;不支持通配符、路径或域名后缀匹配。
Expand Down
5 changes: 5 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,11 @@ Do NOT directly import or call `encoding/json` in business code. `json.RawMessag

**Database compatibility:** All database code MUST work with SQLite, MySQL >= 5.7.8, and PostgreSQL >= 9.6 simultaneously.

- Any change that can affect database behavior MUST be verified before the work is considered complete. This includes ORM/database-driver dependency changes, connection/DSN/protocol or prepared-statement configuration, models and GORM tags, migrations and `AutoMigrate`, constraints and indexes, `Scanner`/`Valuer`/serializer behavior, raw SQL, transactions, and row locking.
- Required database verification MUST exercise real SQLite, MySQL, and PostgreSQL instances. Unit tests, mocks, a successful build, code inspection, or testing only one dialect are not substitutes. Use at least one supported version of each engine; changes that depend on version-specific behavior must also cover the minimum supported version.
- Treat GORM core and its database dialect/driver packages as a compatible version set. Any change to one of them requires checking upstream compatibility and running the complete three-database verification matrix; do not upgrade only the core package and infer that existing drivers remain compatible.
- Schema or migration changes MUST be tested both on a fresh database and by upgrading a representative database created by the latest released version. Run startup/migration at least twice to prove idempotency, and verify that existing data, indexes, constraints, and uniqueness guarantees are preserved. Cover the separately configured log database when the affected path is shared with or used by it.
- Record the exact database versions, commands, and results in the final handoff or pull request. If any required database verification cannot be run, report the blocker explicitly and do not claim the change is database-compatible or complete.
- Prefer GORM methods (`Create`, `Find`, `Where`, `Updates`, etc.) over raw SQL.
- Let GORM handle primary key generation; do not use `AUTO_INCREMENT` or `SERIAL` directly.
- Standard `SELECT ... FOR UPDATE` row locks built with GORM query methods in `model/` MUST use `lockForUpdate(tx)`. Do not use the legacy GORM v1 pattern `tx.Set("gorm:query_option", "FOR UPDATE")`, because GORM v2 silently ignores it and no lock is acquired. Do not duplicate `clause.Locking{Strength: "UPDATE"}` at call sites; the shared helper emits `FOR UPDATE` for MySQL/PostgreSQL and skips it for SQLite, where the syntax is unsupported. Dialect-specific locking with different semantics (for example, a MySQL next-key/gap lock) may use raw SQL only behind explicit database-type branches with valid fallbacks for every supported database.
Expand Down
16 changes: 14 additions & 2 deletions CLAUDE.md
Original file line number Diff line number Diff line change
@@ -1,8 +1,20 @@
# CLAUDE.md — Project Conventions for new-api

@AGENTS.md
## MANDATORY: Read AGENTS.md with the Read tool

Do not treat `@AGENTS.md` as loaded. Claude Code does not reliably inline that import.

Before any planning, coding, reviewing, or answering a project question, you MUST call the Read tool on the repo-root file `AGENTS.md` and wait for the full contents. This is the first action of every session and every new task.

Rules:

- Do not start from memory, summaries, or this file alone.
- Do not skip the Read because a previous turn mentioned AGENTS.md.
- Do not replace the Read with a grep, glob, or partial skim.
- After reading, follow every rule in `AGENTS.md` for the rest of the work.
- If the task touches `web/`, also Read `web/AGENTS.md` before editing frontend files.

## Claude Code

- Follow the shared project instructions imported from `AGENTS.md`.
- **NEVER open a pull request against the upstream repository `https://github.com/QuantumNous/new-api`.** All PRs MUST target this fork (`chunfeng789/new-api`) only — always pass `--repo chunfeng789/new-api` to `gh pr create`, because it otherwise defaults to the upstream parent. See the **Pull requests** rules in `AGENTS.md`.
- **NEVER open a pull request against the upstream repository `https://github.com/QuantumNous/new-api`.** All PRs MUST target this fork (`chunfeng789/new-api`) only — always pass `--repo chunfeng789/new-api` to `gh pr create`, because it otherwise defaults to the upstream parent. See the **Pull requests** rules in `AGENTS.md`.
3 changes: 3 additions & 0 deletions THIRD-PARTY-LICENSES.md
Original file line number Diff line number Diff line change
Expand Up @@ -34,11 +34,13 @@ Transitive dependencies should be audited before a final external release.
| backend | production | Go | `github.com/google/uuid` | `v1.6.0` | BSD-3-Clause |
| backend | production | Go | `github.com/gorilla/websocket` | `v1.5.0` | BSD-2-Clause |
| backend | production | Go | `github.com/grafana/pyroscope-go` | `v1.2.7` | Apache-2.0 |
| backend | production | Go | `github.com/grafana/sobek` | `v0.0.0-20260708062710-267a0e055bb4` | MIT |
| backend | production | Go | `github.com/jfreymuth/oggvorbis` | `v1.0.5` | MIT |
| backend | production | Go | `github.com/jinzhu/copier` | `v0.4.0` | MIT |
| backend | production | Go | `github.com/joho/godotenv` | `v1.5.1` | MIT |
| backend | production | Go | `github.com/mewkiz/flac` | `v1.0.13` | Unlicense |
| backend | production | Go | `github.com/nicksnyder/go-i18n/v2` | `v2.6.1` | MIT |
| backend | test | Go | `github.com/openai/openai-go` | `v1.12.0` | Apache-2.0 |
| backend | production | Go | `github.com/pkg/errors` | `v0.9.1` | BSD-2-Clause |
| backend | production | Go | `github.com/pquerna/otp` | `v1.5.0` | Apache-2.0 |
| backend | production | Go | `github.com/samber/hot` | `v0.11.0` | MIT |
Expand Down Expand Up @@ -66,6 +68,7 @@ Transitive dependencies should be audited before a final external release.
| backend | production | Go | `gorm.io/gorm` | `v1.25.2` | MIT |
| backend | production | Go | `github.com/expr-lang/expr` | `v1.17.8` | MIT |
| web | production | npm | `@base-ui/react` | `1.6.0` | MIT |
| web | production | npm | `@codemirror/lang-javascript` | `6.2.5` | MIT |
| web | production | npm | `@codemirror/lang-markdown` | `6.5.1` | MIT |
| web | production | npm | `@codemirror/language` | `6.12.4` | MIT |
| web | production | npm | `@codemirror/state` | `6.7.1` | MIT |
Expand Down
5 changes: 5 additions & 0 deletions common/api_type.go
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,11 @@ func ChannelType2APIType(channelType int) (int, bool) {
apiType = constant.APITypeNewAPI
}
if apiType == -1 {
// Task plugin channels are served by the task relay and must never
// fall back to the OpenAI adaptor.
if channelType == constant.ChannelTypeTaskPlugin {
return -1, false
}
return constant.APITypeOpenAI, false
}
return apiType, true
Expand Down
14 changes: 14 additions & 0 deletions common/api_type_task_plugin_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
package common

import (
"testing"

"github.com/QuantumNous/new-api/constant"
"github.com/stretchr/testify/assert"
)

func TestTaskPluginChannelHasNoOrdinaryAPIType(t *testing.T) {
apiType, ok := ChannelType2APIType(constant.ChannelTypeTaskPlugin)
assert.Equal(t, -1, apiType)
assert.False(t, ok)
}
11 changes: 11 additions & 0 deletions common/constants.go
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,7 @@ var ItemsPerPage = 10
var MaxRecentItems = 1000

var PasswordLoginEnabled = true
var PasswordLoginEncryptionEnabled = false
var PasswordRegisterEnabled = true
var EmailVerificationEnabled = false
var GitHubOAuthEnabled = false
Expand Down Expand Up @@ -162,6 +163,16 @@ var BatchUpdateInterval int
var RelayTimeout int // unit is second

var RelayIdleConnTimeout int // unit is second

// RelayResponseHeaderTimeout limits how long the relay transport waits for the
// upstream response headers after the request has been fully written.
// 0 disables it (previous behaviour: wait forever).
//
// Note this is NOT the same as RelayTimeout (http.Client.Timeout), which covers
// the whole response read and therefore breaks legitimate long streaming calls.
// ResponseHeaderTimeout only bounds the wait for the response headers; once the
// headers arrive, streaming is unaffected.
var RelayResponseHeaderTimeout int // unit is second
var RelayMaxIdleConns int
var RelayMaxIdleConnsPerHost int

Expand Down
22 changes: 21 additions & 1 deletion common/database.go
Original file line number Diff line number Diff line change
Expand Up @@ -41,4 +41,24 @@ func UsingLogDatabase(databaseType DatabaseType) bool {
return logDatabaseType == databaseType
}

var SQLitePath = "one-api.db?_busy_timeout=30000"
// SQLitePath is the DSN for the default SQLite database. It uses WAL journal
// mode so readers are never blocked by the single writer, plus a 30s busy
// timeout for writers to queue.
//
// Two details are non-obvious and both are required for concurrent correctness:
//
// 1. The busy timeout must be passed as a `_pragma=busy_timeout(30000)` DSN
// parameter. The pure-Go driver (modernc.org/sqlite, used through
// github.com/glebarez/sqlite) silently ignores the plain `_busy_timeout=`
// form, so without this the effective timeout stays at SQLite's 5s default
// and concurrent writes surface as "database is locked" (see #6805).
//
// 2. `_txlock=immediate` (BEGIN IMMEDIATE) must be enabled. Without it, a
// transaction that first SELECTs (establishing a read snapshot) and then
// writes can hit SQLITE_BUSY_SNAPSHOT when another connection commits in
// between; the busy handler does not cover that case, so the write fails
// instantly no matter the timeout. BEGIN IMMEDIATE takes the write lock up
// front, so writers serialize through the busy timeout instead of dying on
// a stale snapshot. Autocommit SELECTs stay concurrent because WAL keeps
// readers unlocked.
var SQLitePath = "one-api.db?_pragma=busy_timeout(30000)&_pragma=journal_mode(WAL)&_txlock=immediate"
10 changes: 10 additions & 0 deletions common/init.go
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,7 @@ func InitEnv() {
DebugEnabled = os.Getenv("DEBUG") == "true"
MemoryCacheEnabled = os.Getenv("MEMORY_CACHE_ENABLED") == "true"
IsMasterNode = os.Getenv("NODE_TYPE") != "slave"
PasswordLoginEncryptionEnabled = GetEnvOrDefaultBool("PASSWORD_LOGIN_ENCRYPTION_ENABLED", false)
initNodeNameIdentity()
TLSInsecureSkipVerify = GetEnvOrDefaultBool("TLS_INSECURE_SKIP_VERIFY", false)
if TLSInsecureSkipVerify {
Expand All @@ -110,6 +111,7 @@ func InitEnv() {
BatchUpdateInterval = GetEnvOrDefault("BATCH_UPDATE_INTERVAL", 5)
RelayTimeout = GetEnvOrDefault("RELAY_TIMEOUT", 0)
RelayIdleConnTimeout = GetEnvOrDefault("RELAY_IDLE_CONN_TIMEOUT", 90)
RelayResponseHeaderTimeout = GetEnvOrDefault("RELAY_RESPONSE_HEADER_TIMEOUT", 1800)
RelayMaxIdleConns = GetEnvOrDefault("RELAY_MAX_IDLE_CONNS", 500)
RelayMaxIdleConnsPerHost = GetEnvOrDefault("RELAY_MAX_IDLE_CONNS_PER_HOST", 100)

Expand Down Expand Up @@ -187,6 +189,8 @@ func initConstantEnv() {
constant.GetMediaToken = GetEnvOrDefaultBool("GET_MEDIA_TOKEN", true)
constant.GetMediaTokenNotStream = GetEnvOrDefaultBool("GET_MEDIA_TOKEN_NOT_STREAM", false)
constant.UpdateTask = GetEnvOrDefaultBool("UPDATE_TASK", true)
constant.TaskPluginEnabled = GetEnvOrDefaultBool("TASK_PLUGIN_ENABLED", true)
constant.TaskPluginOverrideEnabled = GetEnvOrDefaultBool("TASK_PLUGIN_OVERRIDE_ENABLED", true)
constant.AzureDefaultAPIVersion = GetEnvOrDefaultString("AZURE_DEFAULT_API_VERSION", "2025-04-01-preview")
constant.NotifyLimitCount = GetEnvOrDefault("NOTIFY_LIMIT_COUNT", 2)
constant.NotificationLimitDurationMinute = GetEnvOrDefault("NOTIFICATION_LIMIT_DURATION_MINUTE", 10)
Expand All @@ -198,6 +202,12 @@ func initConstantEnv() {
constant.TaskQueryLimit = GetEnvOrDefault("TASK_QUERY_LIMIT", 1000)
// 异步任务超时时间(分钟),超过此时间未完成的任务将被标记为失败并退款。0 表示禁用。
constant.TaskTimeoutMinutes = GetEnvOrDefault("TASK_TIMEOUT_MINUTES", 1440)
// 声明式任务协议桥只观察数据库;这些值控制一次客户端观察连接,
// 不改变后台轮询或结算生命周期。
constant.TaskPluginProtocolTimeoutSeconds = GetEnvOrDefault("TASK_PLUGIN_PROTOCOL_TIMEOUT_SECONDS", 600)
constant.TaskPluginProtocolTickMilliseconds = GetEnvOrDefault("TASK_PLUGIN_PROTOCOL_TICK_MILLISECONDS", 2000)
constant.TaskPluginProtocolTickJitterMilliseconds = GetEnvOrDefault("TASK_PLUGIN_PROTOCOL_TICK_JITTER_MILLISECONDS", 500)
constant.TaskPluginProtocolHeartbeatSeconds = GetEnvOrDefault("TASK_PLUGIN_PROTOCOL_HEARTBEAT_SECONDS", 15)

soraPatchStr := GetEnvOrDefaultString("TASK_PRICE_PATCH", "")
if soraPatchStr != "" {
Expand Down
115 changes: 115 additions & 0 deletions common/password_crypto.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
package common

import (
"crypto/rand"
"crypto/rsa"
"crypto/sha256"
"crypto/x509"
"encoding/base64"
"encoding/hex"
"encoding/pem"
"errors"
"fmt"
"strings"
"sync"
)

const passwordEncryptionKeyBits = 2048

var ErrPasswordEncryptionInvalid = errors.New("password encryption payload is invalid")

var passwordEncryptionState struct {
sync.RWMutex
privateKey *rsa.PrivateKey
publicKey string
keyID string
}

// GeneratePasswordEncryptionPrivateKey creates the server key used to decrypt
// browser login passwords. The caller is responsible for persisting the PEM.
func GeneratePasswordEncryptionPrivateKey() (string, error) {
privateKey, err := rsa.GenerateKey(rand.Reader, passwordEncryptionKeyBits)
if err != nil {
return "", fmt.Errorf("generate password encryption key: %w", err)
}
privateKeyDER, err := x509.MarshalPKCS8PrivateKey(privateKey)
if err != nil {
return "", fmt.Errorf("marshal password encryption key: %w", err)
}
return string(pem.EncodeToMemory(&pem.Block{
Type: "PRIVATE KEY",
Bytes: privateKeyDER,
})), nil
}

// LoadPasswordEncryptionPrivateKey validates a persisted key before replacing
// the active in-memory key used by request handlers.
func LoadPasswordEncryptionPrivateKey(privateKeyPEM string) error {
block, rest := pem.Decode([]byte(privateKeyPEM))
if block == nil || block.Type != "PRIVATE KEY" || strings.TrimSpace(string(rest)) != "" {
return errors.New("password encryption key is not valid PKCS#8 PEM")
}
parsed, err := x509.ParsePKCS8PrivateKey(block.Bytes)
if err != nil {
return fmt.Errorf("parse password encryption key: %w", err)
}
privateKey, ok := parsed.(*rsa.PrivateKey)
if !ok {
return errors.New("password encryption key is not RSA")
}
if privateKey.N == nil || privateKey.N.BitLen() < passwordEncryptionKeyBits {
return fmt.Errorf("password encryption key must be at least %d bits", passwordEncryptionKeyBits)
}
if err := privateKey.Validate(); err != nil {
return fmt.Errorf("validate password encryption key: %w", err)
}
privateKey.Precompute()

publicKeyDER, err := x509.MarshalPKIXPublicKey(&privateKey.PublicKey)
if err != nil {
return fmt.Errorf("marshal password encryption public key: %w", err)
}
publicKeyPEM := string(pem.EncodeToMemory(&pem.Block{
Type: "PUBLIC KEY",
Bytes: publicKeyDER,
}))
keyDigest := sha256.Sum256(publicKeyDER)
keyID := hex.EncodeToString(keyDigest[:16])

passwordEncryptionState.Lock()
defer passwordEncryptionState.Unlock()
passwordEncryptionState.privateKey = privateKey
passwordEncryptionState.publicKey = publicKeyPEM
passwordEncryptionState.keyID = keyID
return nil
}

// PasswordEncryptionPublicKey returns the active key identifier and SPKI PEM
// public key exposed to browser clients.
func PasswordEncryptionPublicKey() (keyID string, publicKeyPEM string) {
passwordEncryptionState.RLock()
defer passwordEncryptionState.RUnlock()
return passwordEncryptionState.keyID, passwordEncryptionState.publicKey
}

// DecryptPassword decrypts a base64 RSA-OAEP/SHA-256 password submitted by a
// browser. All malformed inputs share one error so callers do not expose
// cryptographic details to unauthenticated clients.
func DecryptPassword(ciphertextBase64 string, keyID string) (string, error) {
passwordEncryptionState.RLock()
privateKey := passwordEncryptionState.privateKey
activeKeyID := passwordEncryptionState.keyID
passwordEncryptionState.RUnlock()
if privateKey == nil || keyID == "" || keyID != activeKeyID {
return "", ErrPasswordEncryptionInvalid
}
ciphertext, err := base64.StdEncoding.DecodeString(ciphertextBase64)
if err != nil || len(ciphertext) != privateKey.Size() {
return "", ErrPasswordEncryptionInvalid
}
plaintext, err := rsa.DecryptOAEP(sha256.New(), rand.Reader, privateKey, ciphertext, nil)
if err != nil || len(plaintext) == 0 {
return "", ErrPasswordEncryptionInvalid
}
return string(plaintext), nil
}
54 changes: 54 additions & 0 deletions common/trusted_proxies.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
package common

import (
"errors"
"fmt"
"strings"

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

var defaultTrustedProxyCIDRs = []string{
"127.0.0.0/8",
"::1",
"10.0.0.0/8",
"172.16.0.0/12",
"192.168.0.0/16",
"fc00::/7",
}

// ResolveTrustedProxies parses TRUSTED_PROXIES without applying it to an
// engine. The returned slice can be reused by the outer and plugin engines.
func ResolveTrustedProxies(raw string) (trustedProxies []string, usedDefaults bool, err error) {
raw = strings.TrimSpace(raw)
if raw == "" {
return append([]string(nil), defaultTrustedProxyCIDRs...), true, nil
}
if strings.EqualFold(raw, "none") {
return nil, false, nil
}

parts := strings.Split(raw, ",")
trustedProxies = make([]string, 0, len(parts))
for _, part := range parts {
trustedProxy := strings.TrimSpace(part)
if trustedProxy == "" {
continue
}
if strings.EqualFold(trustedProxy, "none") {
return nil, false, errors.New("TRUSTED_PROXIES=none must be used alone")
}
trustedProxies = append(trustedProxies, trustedProxy)
}
if len(trustedProxies) == 0 {
return nil, false, errors.New("TRUSTED_PROXIES does not contain an IP address or CIDR")
}
return trustedProxies, false, nil
}

func ConfigureTrustedProxies(engine *gin.Engine, trustedProxies []string) error {
if err := engine.SetTrustedProxies(trustedProxies); err != nil {
return fmt.Errorf("invalid TRUSTED_PROXIES: %w", err)
}
return nil
}
Loading
Loading