From cbe258fd1284f1286cdc1c3843d9958a6f49dad2 Mon Sep 17 00:00:00 2001 From: feeeei Date: Sun, 23 Aug 2026 20:41:45 +0800 Subject: [PATCH] =?UTF-8?q?build:=20=E5=8D=87=E7=BA=A7=20Go=201.27.0?= =?UTF-8?q?=EF=BC=8C=E5=90=8C=E6=AD=A5=20CI/Dockerfile=20=E5=B9=B6?= =?UTF-8?q?=E9=80=82=E9=85=8D=20jsonv2=20=E4=B8=8E=20golangci-lint=20v2.13?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - go.mod 1.26.6 → 1.27.0;backend-ci/release/security-scan 的 go version 断言、 三个 Dockerfile 的 golang 镜像、README 徽章与 DEV_GUIDE 同步 - golangci-lint-action v2.9 → v2.13(v2.9 由 go1.26 构建,拒绝 go.mod 1.27 目标); 新规则按最小方式处理:排除 G703/G704 污点分析(网关按配置转发/写文件, 与既有 G304 排除策略一致)、reflect.Ptr → reflect.Pointer、 ResetQuota 恒返回错误的 SA4023 与 OIDC EC JWK 的 SA1019 加 nolint - ent 生成代码按 Go 1.27 默认 jsonv2 引擎重新生成:json.RawMessage 字段 生成为同类型别名 jsontext.Value(group.model_pricing / usage_cleanup_task.filters) - x/net v0.56 在 go1.27 下包装标准库 HTTP/2:ConfigureTransports 经 RegisterProtocol("http/2") 打开 Protocols.HTTP2 而不再写 TLSNextProto, ReadIdleTimeout/PingTimeout 建连时映射为 HTTP2Config.SendPingTimeout/PingTimeout; keepalive 测试改断言 Protocols.HTTP2(),并补真实 HTTP/2 协商用例 --- .github/workflows/backend-ci.yml | 6 +-- .github/workflows/release.yml | 2 +- .github/workflows/security-scan.yml | 2 +- DEV_GUIDE.md | 4 +- Dockerfile | 2 +- README.md | 4 +- README_CN.md | 4 +- README_JA.md | 4 +- backend/.golangci.yml | 4 ++ backend/Dockerfile | 2 +- backend/ent/group.go | 3 +- backend/ent/group_create.go | 10 ++-- backend/ent/group_update.go | 10 ++-- backend/ent/mutation.go | 46 ++++++++--------- backend/ent/usagecleanuptask.go | 3 +- backend/ent/usagecleanuptask_create.go | 10 ++-- backend/ent/usagecleanuptask_update.go | 10 ++-- backend/go.mod | 2 +- .../handler/admin/grok_oauth_handler.go | 4 +- .../handler/admin/setting_handler_update.go | 2 +- backend/internal/handler/auth_oidc_oauth.go | 4 +- .../http_upstream_http2_keepalive_test.go | 49 +++++++++++++++++-- .../usage_log_repo_request_type_test.go | 2 +- deploy/Dockerfile | 2 +- 24 files changed, 120 insertions(+), 71 deletions(-) diff --git a/.github/workflows/backend-ci.yml b/.github/workflows/backend-ci.yml index ece09c5dc1..2bac7259a9 100644 --- a/.github/workflows/backend-ci.yml +++ b/.github/workflows/backend-ci.yml @@ -33,7 +33,7 @@ jobs: cache-dependency-path: backend/go.sum - name: Verify Go version run: | - go version | grep -q 'go1.26.6' + go version | grep -q 'go1.27.0' - name: Unit tests working-directory: backend run: make test-unit @@ -73,10 +73,10 @@ jobs: cache-dependency-path: backend/go.sum - name: Verify Go version run: | - go version | grep -q 'go1.26.6' + go version | grep -q 'go1.27.0' - name: golangci-lint uses: golangci/golangci-lint-action@v9 with: - version: v2.9 + version: v2.13 args: --timeout=30m working-directory: backend diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 4ecc8a26ba..abdfc9c81d 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -115,7 +115,7 @@ jobs: - name: Verify Go version run: | - go version | grep -q 'go1.26.6' + go version | grep -q 'go1.27.0' # Docker setup for GoReleaser - name: Set up QEMU diff --git a/.github/workflows/security-scan.yml b/.github/workflows/security-scan.yml index 0e68d950ac..97357efde2 100644 --- a/.github/workflows/security-scan.yml +++ b/.github/workflows/security-scan.yml @@ -23,7 +23,7 @@ jobs: cache-dependency-path: backend/go.sum - name: Verify Go version run: | - go version | grep -q 'go1.26.6' + go version | grep -q 'go1.27.0' - name: Run govulncheck working-directory: backend run: | diff --git a/DEV_GUIDE.md b/DEV_GUIDE.md index bf5e8867d6..cb1ffabdb3 100644 --- a/DEV_GUIDE.md +++ b/DEV_GUIDE.md @@ -53,7 +53,7 @@ npm install -g pnpm ### CI 要求 -- Go 版本必须是 **1.26.6**:三个 workflow 都用 `go-version-file: backend/go.mod` 取版本,随后硬断言 `go version | grep -q 'go1.26.6'`。升级 Go 时要同时改 `backend/go.mod`、`backend-ci.yml`(两处)、`release.yml`、`security-scan.yml` 里的这句断言,**以及三个 Dockerfile 里的 Go 构建镜像**(`Dockerfile` / `deploy/Dockerfile` 的 `ARG GOLANG_IMAGE`、`backend/Dockerfile` 的 `FROM golang:`)。前者漏了 CI 会在版本校验步骤直接失败;**后者漏了 CI 不会报,而是等到有人用这些 Dockerfile 构建时才失败**(`go.mod requires go >= X (running Y; GOTOOLCHAIN=local)`)。 +- Go 版本必须是 **1.27.0**:三个 workflow 都用 `go-version-file: backend/go.mod` 取版本,随后硬断言 `go version | grep -q 'go1.27.0'`。升级 Go 时要同时改 `backend/go.mod`、`backend-ci.yml`(两处)、`release.yml`、`security-scan.yml` 里的这句断言,**以及三个 Dockerfile 里的 Go 构建镜像**(`Dockerfile` / `deploy/Dockerfile` 的 `ARG GOLANG_IMAGE`、`backend/Dockerfile` 的 `FROM golang:`)。前者漏了 CI 会在版本校验步骤直接失败;**后者漏了 CI 不会报,而是等到有人用这些 Dockerfile 构建时才失败**(`go.mod requires go >= X (running Y; GOTOOLCHAIN=local)`)。 - 前端使用 `pnpm install --frozen-lockfile`,必须提交 `pnpm-lock.yaml` ### 本地测试命令 @@ -203,7 +203,7 @@ go test -tags=integration ./... **解决**: ```bash cd backend -go generate ./ent # 重新生成 ent 代码 +go generate ./ent # 重新生成 ent 代码(json.RawMessage 字段会生成为同类型的 jsontext.Value,属预期) git add ent/ # 生成的文件也要提交 ``` diff --git a/Dockerfile b/Dockerfile index c7991c5a60..adfa4d4702 100644 --- a/Dockerfile +++ b/Dockerfile @@ -8,7 +8,7 @@ # ============================================================================= ARG NODE_IMAGE=node:24-alpine -ARG GOLANG_IMAGE=golang:1.26.6-alpine +ARG GOLANG_IMAGE=golang:1.27.0-alpine ARG ALPINE_IMAGE=alpine:3.21 ARG POSTGRES_IMAGE=postgres:18-alpine ARG GOPROXY=https://goproxy.cn,direct diff --git a/README.md b/README.md index 50ffe7e430..e5d3221c1c 100644 --- a/README.md +++ b/README.md @@ -4,7 +4,7 @@ # Sub2API -[![Go](https://img.shields.io/badge/Go-1.26.5-00ADD8.svg)](https://golang.org/) +[![Go](https://img.shields.io/badge/Go-1.27.0-00ADD8.svg)](https://golang.org/) [![Vue](https://img.shields.io/badge/Vue-3.4+-4FC08D.svg)](https://vuejs.org/) [![PostgreSQL](https://img.shields.io/badge/PostgreSQL-15+-336791.svg)](https://www.postgresql.org/) [![Redis](https://img.shields.io/badge/Redis-7+-DC382D.svg)](https://redis.io/) @@ -194,7 +194,7 @@ Community projects that extend or integrate with Sub2API: | Component | Technology | |-----------|------------| -| Backend | Go 1.26.5, Gin, Ent | +| Backend | Go 1.27.0, Gin, Ent | | Frontend | Vue 3.4+, Vite 5+, TailwindCSS | | Database | PostgreSQL 15+ | | Cache/Queue | Redis 7+ | diff --git a/README_CN.md b/README_CN.md index 034a3819f5..10329d4d8f 100644 --- a/README_CN.md +++ b/README_CN.md @@ -4,7 +4,7 @@ # Sub2API -[![Go](https://img.shields.io/badge/Go-1.26.5-00ADD8.svg)](https://golang.org/) +[![Go](https://img.shields.io/badge/Go-1.27.0-00ADD8.svg)](https://golang.org/) [![Vue](https://img.shields.io/badge/Vue-3.4+-4FC08D.svg)](https://vuejs.org/) [![PostgreSQL](https://img.shields.io/badge/PostgreSQL-15+-336791.svg)](https://www.postgresql.org/) [![Redis](https://img.shields.io/badge/Redis-7+-DC382D.svg)](https://redis.io/) @@ -196,7 +196,7 @@ Sub2API 是一个 AI API 网关平台,用于分发和管理 AI 产品订阅的 | 组件 | 技术 | |------|------| -| 后端 | Go 1.26.5, Gin, Ent | +| 后端 | Go 1.27.0, Gin, Ent | | 前端 | Vue 3.4+, Vite 5+, TailwindCSS | | 数据库 | PostgreSQL 15+ | | 缓存/队列 | Redis 7+ | diff --git a/README_JA.md b/README_JA.md index 384f9daf67..8727367980 100644 --- a/README_JA.md +++ b/README_JA.md @@ -4,7 +4,7 @@ # Sub2API -[![Go](https://img.shields.io/badge/Go-1.26.5-00ADD8.svg)](https://golang.org/) +[![Go](https://img.shields.io/badge/Go-1.27.0-00ADD8.svg)](https://golang.org/) [![Vue](https://img.shields.io/badge/Vue-3.4+-4FC08D.svg)](https://vuejs.org/) [![PostgreSQL](https://img.shields.io/badge/PostgreSQL-15+-336791.svg)](https://www.postgresql.org/) [![Redis](https://img.shields.io/badge/Redis-7+-DC382D.svg)](https://redis.io/) @@ -195,7 +195,7 @@ Sub2API を拡張・統合するコミュニティプロジェクト: | コンポーネント | 技術 | |-----------|------------| -| バックエンド | Go 1.26.5, Gin, Ent | +| バックエンド | Go 1.27.0, Gin, Ent | | フロントエンド | Vue 3.4+, Vite 5+, TailwindCSS | | データベース | PostgreSQL 15+ | | キャッシュ/キュー | Redis 7+ | diff --git a/backend/.golangci.yml b/backend/.golangci.yml index 92ba391694..a4aed11470 100644 --- a/backend/.golangci.yml +++ b/backend/.golangci.yml @@ -57,6 +57,10 @@ linters: - G304 - G306 - G404 + # G703/G704 是 G304/G107 的污点分析版:网关本职就是按配置转发到上游 URL、按配置写本地文件, + # 污点来源即配置本身,规则不适用。 + - G703 + - G704 severity: high confidence: high errcheck: diff --git a/backend/Dockerfile b/backend/Dockerfile index 1efbd3859d..398d9bdfcf 100644 --- a/backend/Dockerfile +++ b/backend/Dockerfile @@ -1,4 +1,4 @@ -FROM golang:1.26.6-alpine +FROM golang:1.27.0-alpine WORKDIR /app diff --git a/backend/ent/group.go b/backend/ent/group.go index c9795fb58f..8f30c26470 100644 --- a/backend/ent/group.go +++ b/backend/ent/group.go @@ -4,6 +4,7 @@ package ent import ( "encoding/json" + "encoding/json/jsontext" "fmt" "strings" "time" @@ -100,7 +101,7 @@ type Group struct { // 是否按上下文长度应用模型阶梯价格;默认开启以保持官方/渠道长上下文价 LongContextPricingEnabled bool `json:"long_context_pricing_enabled,omitempty"` // 分组逐模型定价;优先级高于渠道和内置定价 - ModelPricing json.RawMessage `json:"model_pricing,omitempty"` + ModelPricing jsontext.Value `json:"model_pricing,omitempty"` // 是否仅允许 Claude Code 客户端 ClaudeCodeOnly bool `json:"claude_code_only,omitempty"` // 非 Claude Code 请求降级使用的分组 ID diff --git a/backend/ent/group_create.go b/backend/ent/group_create.go index 158dca1e40..e5e92ccd3f 100644 --- a/backend/ent/group_create.go +++ b/backend/ent/group_create.go @@ -4,7 +4,7 @@ package ent import ( "context" - "encoding/json" + "encoding/json/jsontext" "errors" "fmt" "time" @@ -575,7 +575,7 @@ func (_c *GroupCreate) SetNillableLongContextPricingEnabled(v *bool) *GroupCreat } // SetModelPricing sets the "model_pricing" field. -func (_c *GroupCreate) SetModelPricing(v json.RawMessage) *GroupCreate { +func (_c *GroupCreate) SetModelPricing(v jsontext.Value) *GroupCreate { _c.mutation.SetModelPricing(v) return _c } @@ -2445,7 +2445,7 @@ func (u *GroupUpsert) UpdateLongContextPricingEnabled() *GroupUpsert { } // SetModelPricing sets the "model_pricing" field. -func (u *GroupUpsert) SetModelPricing(v json.RawMessage) *GroupUpsert { +func (u *GroupUpsert) SetModelPricing(v jsontext.Value) *GroupUpsert { u.Set(group.FieldModelPricing, v) return u } @@ -3615,7 +3615,7 @@ func (u *GroupUpsertOne) UpdateLongContextPricingEnabled() *GroupUpsertOne { } // SetModelPricing sets the "model_pricing" field. -func (u *GroupUpsertOne) SetModelPricing(v json.RawMessage) *GroupUpsertOne { +func (u *GroupUpsertOne) SetModelPricing(v jsontext.Value) *GroupUpsertOne { return u.Update(func(s *GroupUpsert) { s.SetModelPricing(v) }) @@ -5005,7 +5005,7 @@ func (u *GroupUpsertBulk) UpdateLongContextPricingEnabled() *GroupUpsertBulk { } // SetModelPricing sets the "model_pricing" field. -func (u *GroupUpsertBulk) SetModelPricing(v json.RawMessage) *GroupUpsertBulk { +func (u *GroupUpsertBulk) SetModelPricing(v jsontext.Value) *GroupUpsertBulk { return u.Update(func(s *GroupUpsert) { s.SetModelPricing(v) }) diff --git a/backend/ent/group_update.go b/backend/ent/group_update.go index beca0c71f0..41e06b4471 100644 --- a/backend/ent/group_update.go +++ b/backend/ent/group_update.go @@ -4,7 +4,7 @@ package ent import ( "context" - "encoding/json" + "encoding/json/jsontext" "errors" "fmt" "time" @@ -803,13 +803,13 @@ func (_u *GroupUpdate) SetNillableLongContextPricingEnabled(v *bool) *GroupUpdat } // SetModelPricing sets the "model_pricing" field. -func (_u *GroupUpdate) SetModelPricing(v json.RawMessage) *GroupUpdate { +func (_u *GroupUpdate) SetModelPricing(v jsontext.Value) *GroupUpdate { _u.mutation.SetModelPricing(v) return _u } // AppendModelPricing appends value to the "model_pricing" field. -func (_u *GroupUpdate) AppendModelPricing(v json.RawMessage) *GroupUpdate { +func (_u *GroupUpdate) AppendModelPricing(v jsontext.Value) *GroupUpdate { _u.mutation.AppendModelPricing(v) return _u } @@ -2924,13 +2924,13 @@ func (_u *GroupUpdateOne) SetNillableLongContextPricingEnabled(v *bool) *GroupUp } // SetModelPricing sets the "model_pricing" field. -func (_u *GroupUpdateOne) SetModelPricing(v json.RawMessage) *GroupUpdateOne { +func (_u *GroupUpdateOne) SetModelPricing(v jsontext.Value) *GroupUpdateOne { _u.mutation.SetModelPricing(v) return _u } // AppendModelPricing appends value to the "model_pricing" field. -func (_u *GroupUpdateOne) AppendModelPricing(v json.RawMessage) *GroupUpdateOne { +func (_u *GroupUpdateOne) AppendModelPricing(v jsontext.Value) *GroupUpdateOne { _u.mutation.AppendModelPricing(v) return _u } diff --git a/backend/ent/mutation.go b/backend/ent/mutation.go index ddf50a1c42..f777a0a5fb 100644 --- a/backend/ent/mutation.go +++ b/backend/ent/mutation.go @@ -4,7 +4,7 @@ package ent import ( "context" - "encoding/json" + "encoding/json/jsontext" "errors" "fmt" "sync" @@ -22142,8 +22142,8 @@ type GroupMutation struct { audio_stt_price_per_hour *float64 addaudio_stt_price_per_hour *float64 long_context_pricing_enabled *bool - model_pricing *json.RawMessage - appendmodel_pricing json.RawMessage + model_pricing *jsontext.Value + appendmodel_pricing jsontext.Value claude_code_only *bool fallback_group_id *int64 addfallback_group_id *int64 @@ -24404,13 +24404,13 @@ func (m *GroupMutation) ResetLongContextPricingEnabled() { } // SetModelPricing sets the "model_pricing" field. -func (m *GroupMutation) SetModelPricing(jm json.RawMessage) { - m.model_pricing = &jm +func (m *GroupMutation) SetModelPricing(j jsontext.Value) { + m.model_pricing = &j m.appendmodel_pricing = nil } // ModelPricing returns the value of the "model_pricing" field in the mutation. -func (m *GroupMutation) ModelPricing() (r json.RawMessage, exists bool) { +func (m *GroupMutation) ModelPricing() (r jsontext.Value, exists bool) { v := m.model_pricing if v == nil { return @@ -24421,7 +24421,7 @@ func (m *GroupMutation) ModelPricing() (r json.RawMessage, exists bool) { // OldModelPricing returns the old "model_pricing" field's value of the Group entity. // If the Group object wasn't provided to the builder, the object is fetched from the database. // An error is returned if the mutation operation is not UpdateOne, or the database query fails. -func (m *GroupMutation) OldModelPricing(ctx context.Context) (v json.RawMessage, err error) { +func (m *GroupMutation) OldModelPricing(ctx context.Context) (v jsontext.Value, err error) { if !m.op.Is(OpUpdateOne) { return v, errors.New("OldModelPricing is only allowed on UpdateOne operations") } @@ -24435,13 +24435,13 @@ func (m *GroupMutation) OldModelPricing(ctx context.Context) (v json.RawMessage, return oldValue.ModelPricing, nil } -// AppendModelPricing adds jm to the "model_pricing" field. -func (m *GroupMutation) AppendModelPricing(jm json.RawMessage) { - m.appendmodel_pricing = append(m.appendmodel_pricing, jm...) +// AppendModelPricing adds j to the "model_pricing" field. +func (m *GroupMutation) AppendModelPricing(j jsontext.Value) { + m.appendmodel_pricing = append(m.appendmodel_pricing, j...) } // AppendedModelPricing returns the list of values that were appended to the "model_pricing" field in this mutation. -func (m *GroupMutation) AppendedModelPricing() (json.RawMessage, bool) { +func (m *GroupMutation) AppendedModelPricing() (jsontext.Value, bool) { if len(m.appendmodel_pricing) == 0 { return nil, false } @@ -26515,7 +26515,7 @@ func (m *GroupMutation) SetField(name string, value ent.Value) error { m.SetLongContextPricingEnabled(v) return nil case group.FieldModelPricing: - v, ok := value.(json.RawMessage) + v, ok := value.(jsontext.Value) if !ok { return fmt.Errorf("unexpected type %T for field %s", value, name) } @@ -43151,8 +43151,8 @@ type UsageCleanupTaskMutation struct { created_at *time.Time updated_at *time.Time status *string - filters *json.RawMessage - appendfilters json.RawMessage + filters *jsontext.Value + appendfilters jsontext.Value created_by *int64 addcreated_by *int64 deleted_rows *int64 @@ -43376,13 +43376,13 @@ func (m *UsageCleanupTaskMutation) ResetStatus() { } // SetFilters sets the "filters" field. -func (m *UsageCleanupTaskMutation) SetFilters(jm json.RawMessage) { - m.filters = &jm +func (m *UsageCleanupTaskMutation) SetFilters(j jsontext.Value) { + m.filters = &j m.appendfilters = nil } // Filters returns the value of the "filters" field in the mutation. -func (m *UsageCleanupTaskMutation) Filters() (r json.RawMessage, exists bool) { +func (m *UsageCleanupTaskMutation) Filters() (r jsontext.Value, exists bool) { v := m.filters if v == nil { return @@ -43393,7 +43393,7 @@ func (m *UsageCleanupTaskMutation) Filters() (r json.RawMessage, exists bool) { // OldFilters returns the old "filters" field's value of the UsageCleanupTask entity. // If the UsageCleanupTask object wasn't provided to the builder, the object is fetched from the database. // An error is returned if the mutation operation is not UpdateOne, or the database query fails. -func (m *UsageCleanupTaskMutation) OldFilters(ctx context.Context) (v json.RawMessage, err error) { +func (m *UsageCleanupTaskMutation) OldFilters(ctx context.Context) (v jsontext.Value, err error) { if !m.op.Is(OpUpdateOne) { return v, errors.New("OldFilters is only allowed on UpdateOne operations") } @@ -43407,13 +43407,13 @@ func (m *UsageCleanupTaskMutation) OldFilters(ctx context.Context) (v json.RawMe return oldValue.Filters, nil } -// AppendFilters adds jm to the "filters" field. -func (m *UsageCleanupTaskMutation) AppendFilters(jm json.RawMessage) { - m.appendfilters = append(m.appendfilters, jm...) +// AppendFilters adds j to the "filters" field. +func (m *UsageCleanupTaskMutation) AppendFilters(j jsontext.Value) { + m.appendfilters = append(m.appendfilters, j...) } // AppendedFilters returns the list of values that were appended to the "filters" field in this mutation. -func (m *UsageCleanupTaskMutation) AppendedFilters() (json.RawMessage, bool) { +func (m *UsageCleanupTaskMutation) AppendedFilters() (jsontext.Value, bool) { if len(m.appendfilters) == 0 { return nil, false } @@ -43964,7 +43964,7 @@ func (m *UsageCleanupTaskMutation) SetField(name string, value ent.Value) error m.SetStatus(v) return nil case usagecleanuptask.FieldFilters: - v, ok := value.(json.RawMessage) + v, ok := value.(jsontext.Value) if !ok { return fmt.Errorf("unexpected type %T for field %s", value, name) } diff --git a/backend/ent/usagecleanuptask.go b/backend/ent/usagecleanuptask.go index e3a17b5aed..b4dbf0fd34 100644 --- a/backend/ent/usagecleanuptask.go +++ b/backend/ent/usagecleanuptask.go @@ -4,6 +4,7 @@ package ent import ( "encoding/json" + "encoding/json/jsontext" "fmt" "strings" "time" @@ -25,7 +26,7 @@ type UsageCleanupTask struct { // Status holds the value of the "status" field. Status string `json:"status,omitempty"` // Filters holds the value of the "filters" field. - Filters json.RawMessage `json:"filters,omitempty"` + Filters jsontext.Value `json:"filters,omitempty"` // CreatedBy holds the value of the "created_by" field. CreatedBy int64 `json:"created_by,omitempty"` // DeletedRows holds the value of the "deleted_rows" field. diff --git a/backend/ent/usagecleanuptask_create.go b/backend/ent/usagecleanuptask_create.go index 0b1dcff55e..b6dbcb3cc8 100644 --- a/backend/ent/usagecleanuptask_create.go +++ b/backend/ent/usagecleanuptask_create.go @@ -4,7 +4,7 @@ package ent import ( "context" - "encoding/json" + "encoding/json/jsontext" "errors" "fmt" "time" @@ -58,7 +58,7 @@ func (_c *UsageCleanupTaskCreate) SetStatus(v string) *UsageCleanupTaskCreate { } // SetFilters sets the "filters" field. -func (_c *UsageCleanupTaskCreate) SetFilters(v json.RawMessage) *UsageCleanupTaskCreate { +func (_c *UsageCleanupTaskCreate) SetFilters(v jsontext.Value) *UsageCleanupTaskCreate { _c.mutation.SetFilters(v) return _c } @@ -375,7 +375,7 @@ func (u *UsageCleanupTaskUpsert) UpdateStatus() *UsageCleanupTaskUpsert { } // SetFilters sets the "filters" field. -func (u *UsageCleanupTaskUpsert) SetFilters(v json.RawMessage) *UsageCleanupTaskUpsert { +func (u *UsageCleanupTaskUpsert) SetFilters(v jsontext.Value) *UsageCleanupTaskUpsert { u.Set(usagecleanuptask.FieldFilters, v) return u } @@ -592,7 +592,7 @@ func (u *UsageCleanupTaskUpsertOne) UpdateStatus() *UsageCleanupTaskUpsertOne { } // SetFilters sets the "filters" field. -func (u *UsageCleanupTaskUpsertOne) SetFilters(v json.RawMessage) *UsageCleanupTaskUpsertOne { +func (u *UsageCleanupTaskUpsertOne) SetFilters(v jsontext.Value) *UsageCleanupTaskUpsertOne { return u.Update(func(s *UsageCleanupTaskUpsert) { s.SetFilters(v) }) @@ -999,7 +999,7 @@ func (u *UsageCleanupTaskUpsertBulk) UpdateStatus() *UsageCleanupTaskUpsertBulk } // SetFilters sets the "filters" field. -func (u *UsageCleanupTaskUpsertBulk) SetFilters(v json.RawMessage) *UsageCleanupTaskUpsertBulk { +func (u *UsageCleanupTaskUpsertBulk) SetFilters(v jsontext.Value) *UsageCleanupTaskUpsertBulk { return u.Update(func(s *UsageCleanupTaskUpsert) { s.SetFilters(v) }) diff --git a/backend/ent/usagecleanuptask_update.go b/backend/ent/usagecleanuptask_update.go index 604202c679..d233258bbe 100644 --- a/backend/ent/usagecleanuptask_update.go +++ b/backend/ent/usagecleanuptask_update.go @@ -4,7 +4,7 @@ package ent import ( "context" - "encoding/json" + "encoding/json/jsontext" "errors" "fmt" "time" @@ -51,13 +51,13 @@ func (_u *UsageCleanupTaskUpdate) SetNillableStatus(v *string) *UsageCleanupTask } // SetFilters sets the "filters" field. -func (_u *UsageCleanupTaskUpdate) SetFilters(v json.RawMessage) *UsageCleanupTaskUpdate { +func (_u *UsageCleanupTaskUpdate) SetFilters(v jsontext.Value) *UsageCleanupTaskUpdate { _u.mutation.SetFilters(v) return _u } // AppendFilters appends value to the "filters" field. -func (_u *UsageCleanupTaskUpdate) AppendFilters(v json.RawMessage) *UsageCleanupTaskUpdate { +func (_u *UsageCleanupTaskUpdate) AppendFilters(v jsontext.Value) *UsageCleanupTaskUpdate { _u.mutation.AppendFilters(v) return _u } @@ -374,13 +374,13 @@ func (_u *UsageCleanupTaskUpdateOne) SetNillableStatus(v *string) *UsageCleanupT } // SetFilters sets the "filters" field. -func (_u *UsageCleanupTaskUpdateOne) SetFilters(v json.RawMessage) *UsageCleanupTaskUpdateOne { +func (_u *UsageCleanupTaskUpdateOne) SetFilters(v jsontext.Value) *UsageCleanupTaskUpdateOne { _u.mutation.SetFilters(v) return _u } // AppendFilters appends value to the "filters" field. -func (_u *UsageCleanupTaskUpdateOne) AppendFilters(v json.RawMessage) *UsageCleanupTaskUpdateOne { +func (_u *UsageCleanupTaskUpdateOne) AppendFilters(v jsontext.Value) *UsageCleanupTaskUpdateOne { _u.mutation.AppendFilters(v) return _u } diff --git a/backend/go.mod b/backend/go.mod index 2f07042ce5..b86368117f 100644 --- a/backend/go.mod +++ b/backend/go.mod @@ -1,6 +1,6 @@ module github.com/Wei-Shaw/sub2api -go 1.26.6 +go 1.27.0 require ( entgo.io/ent v0.14.5 diff --git a/backend/internal/handler/admin/grok_oauth_handler.go b/backend/internal/handler/admin/grok_oauth_handler.go index 696a24b419..fdab4473de 100644 --- a/backend/internal/handler/admin/grok_oauth_handler.go +++ b/backend/internal/handler/admin/grok_oauth_handler.go @@ -615,8 +615,10 @@ func (h *GrokOAuthHandler) ResetQuota(c *gin.Context) { response.BadRequest(c, "grok quota service is not enabled") return } + // ResetQuota 恒返回 GROK_QUOTA_RESET_UNSUPPORTED(xAI 无 OAuth 配额重置接口),err != nil 恒真为预期。 + //nolint:staticcheck // SA4023 result, err := h.quotaService.ResetQuota(c.Request.Context(), accountID) - if err != nil { + if err != nil { //nolint:staticcheck // SA4023 response.ErrorFrom(c, err) return } diff --git a/backend/internal/handler/admin/setting_handler_update.go b/backend/internal/handler/admin/setting_handler_update.go index 3775c01e11..b2c001fe73 100644 --- a/backend/internal/handler/admin/setting_handler_update.go +++ b/backend/internal/handler/admin/setting_handler_update.go @@ -441,7 +441,7 @@ func buildSettingKeyByJSONName() map[string]string { out := make(map[string]string, t.NumField()) for i := 0; i < t.NumField(); i++ { field := t.Field(i) - if field.Type.Kind() == reflect.Ptr { + if field.Type.Kind() == reflect.Pointer { continue } name, _, _ := strings.Cut(field.Tag.Get("json"), ",") diff --git a/backend/internal/handler/auth_oidc_oauth.go b/backend/internal/handler/auth_oidc_oauth.go index 0367e4cff0..13c26cf18c 100644 --- a/backend/internal/handler/auth_oidc_oauth.go +++ b/backend/internal/handler/auth_oidc_oauth.go @@ -1144,10 +1144,10 @@ func (k oidcJWK) publicKey() (any, error) { if err != nil { return nil, fmt.Errorf("decode ec y: %w", err) } - if !curve.IsOnCurve(x, y) { + if !curve.IsOnCurve(x, y) { //nolint:staticcheck // JWK 以裸坐标给出公钥;替换为 ecdsa.ParseUncompressedPublicKey 需改变点编码,待单独迁移 return nil, errors.New("ec point is not on curve") } - return &ecdsa.PublicKey{Curve: curve, X: x, Y: y}, nil + return &ecdsa.PublicKey{Curve: curve, X: x, Y: y}, nil //nolint:staticcheck // 同上 default: return nil, fmt.Errorf("unsupported jwk kty: %s", k.Kty) } diff --git a/backend/internal/repository/http_upstream_http2_keepalive_test.go b/backend/internal/repository/http_upstream_http2_keepalive_test.go index ead61da3b2..66a30dab0e 100644 --- a/backend/internal/repository/http_upstream_http2_keepalive_test.go +++ b/backend/internal/repository/http_upstream_http2_keepalive_test.go @@ -1,7 +1,10 @@ package repository import ( + "context" + "crypto/x509" "net/http" + "net/http/httptest" "net/url" "testing" "time" @@ -19,6 +22,16 @@ func http2KeepAliveTestPoolSettings() poolSettings { } } +// requireHTTP2Configured 断言 http2 已显式挂到 http.Transport 上。 +// x/net/http2 在 go1.27 && !http2legacy 下是标准库 HTTP/2 的包装:ConfigureTransports 通过 +// Transport.RegisterProtocol("http/2") 注册配置并打开 Protocols.HTTP2(TLSNextProto 不承载 h2 入口), +// ReadIdleTimeout/PingTimeout 在建连时映射为 http.HTTP2Config.SendPingTimeout/PingTimeout。 +func requireHTTP2Configured(t *testing.T, tr *http.Transport, msg string) { + t.Helper() + require.NotNil(t, tr.Protocols, msg) + require.True(t, tr.Protocols.HTTP2(), msg) +} + // Codex/OpenAI 上游改走 HTTP/2 后,池化连接被代理/NAT 静默掐断会成为“死连接”: // 两端都以为连接存活,请求落上去会挂到 TCP 重传超时(分钟级)才失败。Go 的 // http2.Transport 默认 ReadIdleTimeout=0(不发健康 PING),无法检测这种死连接。 @@ -34,7 +47,7 @@ func TestEnableOpenAIHTTP2KeepAlive_EnablesPingHealthCheck(t *testing.T) { require.Positive(t, h2.ReadIdleTimeout, "必须启用空闲 PING 探测以剔除死连接") require.Equal(t, openAIHTTP2ReadIdleTimeout, h2.ReadIdleTimeout) require.Equal(t, openAIHTTP2PingTimeout, h2.PingTimeout, "PING 无响应必须有超时判定") - require.NotNil(t, tr.TLSNextProto["h2"], "http2 必须已挂到底层 http.Transport 上") + requireHTTP2Configured(t, tr, "http2 必须已挂到底层 http.Transport 上") } // openai_h2 模式构建的 Transport 必须带上 H2 PING 健康探测,从源头剔除死连接。 @@ -42,17 +55,45 @@ func TestBuildUpstreamTransport_OpenAIH2_EnablesPingHealthCheck(t *testing.T) { tr, err := buildUpstreamTransport(http2KeepAliveTestPoolSettings(), nil, upstreamProtocolModeOpenAIH2) require.NoError(t, err) require.True(t, tr.ForceAttemptHTTP2, "openai_h2 必须启用 HTTP/2") - require.NotNil(t, tr.TLSNextProto["h2"], "openai_h2 必须显式配置 http2 以启用 ReadIdleTimeout") + requireHTTP2Configured(t, tr, "openai_h2 必须显式配置 http2 以启用 ReadIdleTimeout") } // 非 H2 模式(default/h1)不应因本次改动被误配置:default 走 Go 自动 H2(惰性配置, -// 构建时 TLSNextProto 仍为空),h1 模式显式禁用 H2。避免波及 Claude/Gemini 热路径。 +// 构建时 Protocols/TLSNextProto 仍为空),h1 模式显式禁用 H2。避免波及 Claude/Gemini 热路径。 func TestBuildUpstreamTransport_NonOpenAIH2_NotEagerlyConfigured(t *testing.T) { tr, err := buildUpstreamTransport(http2KeepAliveTestPoolSettings(), nil, upstreamProtocolModeDefault) require.NoError(t, err) + require.Nil(t, tr.Protocols, "default 模式不应在构建期主动配置 http2 keepalive") require.Nil(t, tr.TLSNextProto["h2"], "default 模式不应在构建期主动配置 http2 keepalive") } +// openai_h2 模式构建的 Transport 必须真正以 HTTP/2 与上游通信,PING 健康探测才有载体: +// 自定义 DialContext 下 Go 不会自动启用 H2,全靠 enableOpenAIHTTP2KeepAlive 的显式配置。 +func TestBuildUpstreamTransport_OpenAIH2_NegotiatesHTTP2(t *testing.T) { + srv := httptest.NewUnstartedServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { + w.WriteHeader(http.StatusOK) + })) + srv.EnableHTTP2 = true + srv.StartTLS() + defer srv.Close() + + tr, err := buildUpstreamTransport(http2KeepAliveTestPoolSettings(), nil, upstreamProtocolModeOpenAIH2) + require.NoError(t, err) + defer tr.CloseIdleConnections() + require.NotNil(t, tr.TLSClientConfig) + roots := x509.NewCertPool() + roots.AddCert(srv.Certificate()) + tr.TLSClientConfig.RootCAs = roots + + req, err := http.NewRequestWithContext(context.Background(), http.MethodGet, srv.URL, nil) + require.NoError(t, err) + resp, err := tr.RoundTrip(req) + require.NoError(t, err) + require.NoError(t, resp.Body.Close()) + require.Equal(t, http.StatusOK, resp.StatusCode) + require.Equal(t, 2, resp.ProtoMajor, "openai_h2 必须协商到 HTTP/2") +} + // 死连接在经 HTTP 代理(CONNECT 隧道)时最高发,这是带 proxy 账号的真实生产路径: // 显式 http2 配置须与 Transport.Proxy 同时正确生效,不能相互干扰。 func TestBuildUpstreamTransport_OpenAIH2_WithHTTPProxy_EnablesKeepAlive(t *testing.T) { @@ -62,6 +103,6 @@ func TestBuildUpstreamTransport_OpenAIH2_WithHTTPProxy_EnablesKeepAlive(t *testi tr, err := buildUpstreamTransport(http2KeepAliveTestPoolSettings(), proxyURL, upstreamProtocolModeOpenAIH2) require.NoError(t, err) require.True(t, tr.ForceAttemptHTTP2) - require.NotNil(t, tr.TLSNextProto["h2"], "经代理的 openai_h2 也必须启用 http2 keepalive") + requireHTTP2Configured(t, tr, "经代理的 openai_h2 也必须启用 http2 keepalive") require.NotNil(t, tr.Proxy, "HTTP 代理仍须通过 Transport.Proxy 生效") } diff --git a/backend/internal/repository/usage_log_repo_request_type_test.go b/backend/internal/repository/usage_log_repo_request_type_test.go index c69f374d41..a67c93c345 100644 --- a/backend/internal/repository/usage_log_repo_request_type_test.go +++ b/backend/internal/repository/usage_log_repo_request_type_test.go @@ -780,7 +780,7 @@ func (s usageLogScannerStub) Scan(dest ...any) error { } for i := range dest { dv := reflect.ValueOf(dest[i]) - if dv.Kind() != reflect.Ptr { + if dv.Kind() != reflect.Pointer { return fmt.Errorf("dest[%d] is not pointer", i) } dv.Elem().Set(reflect.ValueOf(s.values[i])) diff --git a/deploy/Dockerfile b/deploy/Dockerfile index 30c7a1a240..ab312c4852 100644 --- a/deploy/Dockerfile +++ b/deploy/Dockerfile @@ -7,7 +7,7 @@ # ============================================================================= ARG NODE_IMAGE=node:24-alpine -ARG GOLANG_IMAGE=golang:1.26.6-alpine +ARG GOLANG_IMAGE=golang:1.27.0-alpine ARG ALPINE_IMAGE=alpine:3.20 ARG GOPROXY=https://goproxy.cn,direct ARG GOSUMDB=sum.golang.google.cn