mirror of
https://github.com/Wei-Shaw/sub2api.git
synced 2026-10-06 14:33:10 +08:00
build: 升级 Go 1.27.0,同步 CI/Dockerfile 并适配 jsonv2 与 golangci-lint v2.13
- 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 协商用例
This commit is contained in:
@@ -33,7 +33,7 @@ jobs:
|
|||||||
cache-dependency-path: backend/go.sum
|
cache-dependency-path: backend/go.sum
|
||||||
- name: Verify Go version
|
- name: Verify Go version
|
||||||
run: |
|
run: |
|
||||||
go version | grep -q 'go1.26.6'
|
go version | grep -q 'go1.27.0'
|
||||||
- name: Unit tests
|
- name: Unit tests
|
||||||
working-directory: backend
|
working-directory: backend
|
||||||
run: make test-unit
|
run: make test-unit
|
||||||
@@ -73,10 +73,10 @@ jobs:
|
|||||||
cache-dependency-path: backend/go.sum
|
cache-dependency-path: backend/go.sum
|
||||||
- name: Verify Go version
|
- name: Verify Go version
|
||||||
run: |
|
run: |
|
||||||
go version | grep -q 'go1.26.6'
|
go version | grep -q 'go1.27.0'
|
||||||
- name: golangci-lint
|
- name: golangci-lint
|
||||||
uses: golangci/golangci-lint-action@v9
|
uses: golangci/golangci-lint-action@v9
|
||||||
with:
|
with:
|
||||||
version: v2.9
|
version: v2.13
|
||||||
args: --timeout=30m
|
args: --timeout=30m
|
||||||
working-directory: backend
|
working-directory: backend
|
||||||
|
|||||||
@@ -115,7 +115,7 @@ jobs:
|
|||||||
|
|
||||||
- name: Verify Go version
|
- name: Verify Go version
|
||||||
run: |
|
run: |
|
||||||
go version | grep -q 'go1.26.6'
|
go version | grep -q 'go1.27.0'
|
||||||
|
|
||||||
# Docker setup for GoReleaser
|
# Docker setup for GoReleaser
|
||||||
- name: Set up QEMU
|
- name: Set up QEMU
|
||||||
|
|||||||
@@ -23,7 +23,7 @@ jobs:
|
|||||||
cache-dependency-path: backend/go.sum
|
cache-dependency-path: backend/go.sum
|
||||||
- name: Verify Go version
|
- name: Verify Go version
|
||||||
run: |
|
run: |
|
||||||
go version | grep -q 'go1.26.6'
|
go version | grep -q 'go1.27.0'
|
||||||
- name: Run govulncheck
|
- name: Run govulncheck
|
||||||
working-directory: backend
|
working-directory: backend
|
||||||
run: |
|
run: |
|
||||||
|
|||||||
+2
-2
@@ -53,7 +53,7 @@ npm install -g pnpm
|
|||||||
|
|
||||||
### CI 要求
|
### 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`
|
- 前端使用 `pnpm install --frozen-lockfile`,必须提交 `pnpm-lock.yaml`
|
||||||
|
|
||||||
### 本地测试命令
|
### 本地测试命令
|
||||||
@@ -203,7 +203,7 @@ go test -tags=integration ./...
|
|||||||
**解决**:
|
**解决**:
|
||||||
```bash
|
```bash
|
||||||
cd backend
|
cd backend
|
||||||
go generate ./ent # 重新生成 ent 代码
|
go generate ./ent # 重新生成 ent 代码(json.RawMessage 字段会生成为同类型的 jsontext.Value,属预期)
|
||||||
git add ent/ # 生成的文件也要提交
|
git add ent/ # 生成的文件也要提交
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|||||||
+1
-1
@@ -8,7 +8,7 @@
|
|||||||
# =============================================================================
|
# =============================================================================
|
||||||
|
|
||||||
ARG NODE_IMAGE=node:24-alpine
|
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 ALPINE_IMAGE=alpine:3.21
|
||||||
ARG POSTGRES_IMAGE=postgres:18-alpine
|
ARG POSTGRES_IMAGE=postgres:18-alpine
|
||||||
ARG GOPROXY=https://goproxy.cn,direct
|
ARG GOPROXY=https://goproxy.cn,direct
|
||||||
|
|||||||
@@ -4,7 +4,7 @@
|
|||||||
|
|
||||||
# Sub2API
|
# Sub2API
|
||||||
|
|
||||||
[](https://golang.org/)
|
[](https://golang.org/)
|
||||||
[](https://vuejs.org/)
|
[](https://vuejs.org/)
|
||||||
[](https://www.postgresql.org/)
|
[](https://www.postgresql.org/)
|
||||||
[](https://redis.io/)
|
[](https://redis.io/)
|
||||||
@@ -194,7 +194,7 @@ Community projects that extend or integrate with Sub2API:
|
|||||||
|
|
||||||
| Component | Technology |
|
| Component | Technology |
|
||||||
|-----------|------------|
|
|-----------|------------|
|
||||||
| Backend | Go 1.26.5, Gin, Ent |
|
| Backend | Go 1.27.0, Gin, Ent |
|
||||||
| Frontend | Vue 3.4+, Vite 5+, TailwindCSS |
|
| Frontend | Vue 3.4+, Vite 5+, TailwindCSS |
|
||||||
| Database | PostgreSQL 15+ |
|
| Database | PostgreSQL 15+ |
|
||||||
| Cache/Queue | Redis 7+ |
|
| Cache/Queue | Redis 7+ |
|
||||||
|
|||||||
+2
-2
@@ -4,7 +4,7 @@
|
|||||||
|
|
||||||
# Sub2API
|
# Sub2API
|
||||||
|
|
||||||
[](https://golang.org/)
|
[](https://golang.org/)
|
||||||
[](https://vuejs.org/)
|
[](https://vuejs.org/)
|
||||||
[](https://www.postgresql.org/)
|
[](https://www.postgresql.org/)
|
||||||
[](https://redis.io/)
|
[](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 |
|
| 前端 | Vue 3.4+, Vite 5+, TailwindCSS |
|
||||||
| 数据库 | PostgreSQL 15+ |
|
| 数据库 | PostgreSQL 15+ |
|
||||||
| 缓存/队列 | Redis 7+ |
|
| 缓存/队列 | Redis 7+ |
|
||||||
|
|||||||
+2
-2
@@ -4,7 +4,7 @@
|
|||||||
|
|
||||||
# Sub2API
|
# Sub2API
|
||||||
|
|
||||||
[](https://golang.org/)
|
[](https://golang.org/)
|
||||||
[](https://vuejs.org/)
|
[](https://vuejs.org/)
|
||||||
[](https://www.postgresql.org/)
|
[](https://www.postgresql.org/)
|
||||||
[](https://redis.io/)
|
[](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 |
|
| フロントエンド | Vue 3.4+, Vite 5+, TailwindCSS |
|
||||||
| データベース | PostgreSQL 15+ |
|
| データベース | PostgreSQL 15+ |
|
||||||
| キャッシュ/キュー | Redis 7+ |
|
| キャッシュ/キュー | Redis 7+ |
|
||||||
|
|||||||
@@ -57,6 +57,10 @@ linters:
|
|||||||
- G304
|
- G304
|
||||||
- G306
|
- G306
|
||||||
- G404
|
- G404
|
||||||
|
# G703/G704 是 G304/G107 的污点分析版:网关本职就是按配置转发到上游 URL、按配置写本地文件,
|
||||||
|
# 污点来源即配置本身,规则不适用。
|
||||||
|
- G703
|
||||||
|
- G704
|
||||||
severity: high
|
severity: high
|
||||||
confidence: high
|
confidence: high
|
||||||
errcheck:
|
errcheck:
|
||||||
|
|||||||
+1
-1
@@ -1,4 +1,4 @@
|
|||||||
FROM golang:1.26.6-alpine
|
FROM golang:1.27.0-alpine
|
||||||
|
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
|
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ package ent
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
|
"encoding/json/jsontext"
|
||||||
"fmt"
|
"fmt"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
@@ -100,7 +101,7 @@ type Group struct {
|
|||||||
// 是否按上下文长度应用模型阶梯价格;默认开启以保持官方/渠道长上下文价
|
// 是否按上下文长度应用模型阶梯价格;默认开启以保持官方/渠道长上下文价
|
||||||
LongContextPricingEnabled bool `json:"long_context_pricing_enabled,omitempty"`
|
LongContextPricingEnabled bool `json:"long_context_pricing_enabled,omitempty"`
|
||||||
// 分组逐模型定价;优先级高于渠道和内置定价
|
// 分组逐模型定价;优先级高于渠道和内置定价
|
||||||
ModelPricing json.RawMessage `json:"model_pricing,omitempty"`
|
ModelPricing jsontext.Value `json:"model_pricing,omitempty"`
|
||||||
// 是否仅允许 Claude Code 客户端
|
// 是否仅允许 Claude Code 客户端
|
||||||
ClaudeCodeOnly bool `json:"claude_code_only,omitempty"`
|
ClaudeCodeOnly bool `json:"claude_code_only,omitempty"`
|
||||||
// 非 Claude Code 请求降级使用的分组 ID
|
// 非 Claude Code 请求降级使用的分组 ID
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ package ent
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"encoding/json"
|
"encoding/json/jsontext"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"time"
|
"time"
|
||||||
@@ -575,7 +575,7 @@ func (_c *GroupCreate) SetNillableLongContextPricingEnabled(v *bool) *GroupCreat
|
|||||||
}
|
}
|
||||||
|
|
||||||
// SetModelPricing sets the "model_pricing" field.
|
// 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)
|
_c.mutation.SetModelPricing(v)
|
||||||
return _c
|
return _c
|
||||||
}
|
}
|
||||||
@@ -2445,7 +2445,7 @@ func (u *GroupUpsert) UpdateLongContextPricingEnabled() *GroupUpsert {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// SetModelPricing sets the "model_pricing" field.
|
// 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)
|
u.Set(group.FieldModelPricing, v)
|
||||||
return u
|
return u
|
||||||
}
|
}
|
||||||
@@ -3615,7 +3615,7 @@ func (u *GroupUpsertOne) UpdateLongContextPricingEnabled() *GroupUpsertOne {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// SetModelPricing sets the "model_pricing" field.
|
// 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) {
|
return u.Update(func(s *GroupUpsert) {
|
||||||
s.SetModelPricing(v)
|
s.SetModelPricing(v)
|
||||||
})
|
})
|
||||||
@@ -5005,7 +5005,7 @@ func (u *GroupUpsertBulk) UpdateLongContextPricingEnabled() *GroupUpsertBulk {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// SetModelPricing sets the "model_pricing" field.
|
// 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) {
|
return u.Update(func(s *GroupUpsert) {
|
||||||
s.SetModelPricing(v)
|
s.SetModelPricing(v)
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ package ent
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"encoding/json"
|
"encoding/json/jsontext"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"time"
|
"time"
|
||||||
@@ -803,13 +803,13 @@ func (_u *GroupUpdate) SetNillableLongContextPricingEnabled(v *bool) *GroupUpdat
|
|||||||
}
|
}
|
||||||
|
|
||||||
// SetModelPricing sets the "model_pricing" field.
|
// 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)
|
_u.mutation.SetModelPricing(v)
|
||||||
return _u
|
return _u
|
||||||
}
|
}
|
||||||
|
|
||||||
// AppendModelPricing appends value to the "model_pricing" field.
|
// 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)
|
_u.mutation.AppendModelPricing(v)
|
||||||
return _u
|
return _u
|
||||||
}
|
}
|
||||||
@@ -2924,13 +2924,13 @@ func (_u *GroupUpdateOne) SetNillableLongContextPricingEnabled(v *bool) *GroupUp
|
|||||||
}
|
}
|
||||||
|
|
||||||
// SetModelPricing sets the "model_pricing" field.
|
// 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)
|
_u.mutation.SetModelPricing(v)
|
||||||
return _u
|
return _u
|
||||||
}
|
}
|
||||||
|
|
||||||
// AppendModelPricing appends value to the "model_pricing" field.
|
// 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)
|
_u.mutation.AppendModelPricing(v)
|
||||||
return _u
|
return _u
|
||||||
}
|
}
|
||||||
|
|||||||
+23
-23
@@ -4,7 +4,7 @@ package ent
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"encoding/json"
|
"encoding/json/jsontext"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"sync"
|
"sync"
|
||||||
@@ -22142,8 +22142,8 @@ type GroupMutation struct {
|
|||||||
audio_stt_price_per_hour *float64
|
audio_stt_price_per_hour *float64
|
||||||
addaudio_stt_price_per_hour *float64
|
addaudio_stt_price_per_hour *float64
|
||||||
long_context_pricing_enabled *bool
|
long_context_pricing_enabled *bool
|
||||||
model_pricing *json.RawMessage
|
model_pricing *jsontext.Value
|
||||||
appendmodel_pricing json.RawMessage
|
appendmodel_pricing jsontext.Value
|
||||||
claude_code_only *bool
|
claude_code_only *bool
|
||||||
fallback_group_id *int64
|
fallback_group_id *int64
|
||||||
addfallback_group_id *int64
|
addfallback_group_id *int64
|
||||||
@@ -24404,13 +24404,13 @@ func (m *GroupMutation) ResetLongContextPricingEnabled() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// SetModelPricing sets the "model_pricing" field.
|
// SetModelPricing sets the "model_pricing" field.
|
||||||
func (m *GroupMutation) SetModelPricing(jm json.RawMessage) {
|
func (m *GroupMutation) SetModelPricing(j jsontext.Value) {
|
||||||
m.model_pricing = &jm
|
m.model_pricing = &j
|
||||||
m.appendmodel_pricing = nil
|
m.appendmodel_pricing = nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// ModelPricing returns the value of the "model_pricing" field in the mutation.
|
// 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
|
v := m.model_pricing
|
||||||
if v == nil {
|
if v == nil {
|
||||||
return
|
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.
|
// 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.
|
// 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.
|
// 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) {
|
if !m.op.Is(OpUpdateOne) {
|
||||||
return v, errors.New("OldModelPricing is only allowed on UpdateOne operations")
|
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
|
return oldValue.ModelPricing, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// AppendModelPricing adds jm to the "model_pricing" field.
|
// AppendModelPricing adds j to the "model_pricing" field.
|
||||||
func (m *GroupMutation) AppendModelPricing(jm json.RawMessage) {
|
func (m *GroupMutation) AppendModelPricing(j jsontext.Value) {
|
||||||
m.appendmodel_pricing = append(m.appendmodel_pricing, jm...)
|
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.
|
// 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 {
|
if len(m.appendmodel_pricing) == 0 {
|
||||||
return nil, false
|
return nil, false
|
||||||
}
|
}
|
||||||
@@ -26515,7 +26515,7 @@ func (m *GroupMutation) SetField(name string, value ent.Value) error {
|
|||||||
m.SetLongContextPricingEnabled(v)
|
m.SetLongContextPricingEnabled(v)
|
||||||
return nil
|
return nil
|
||||||
case group.FieldModelPricing:
|
case group.FieldModelPricing:
|
||||||
v, ok := value.(json.RawMessage)
|
v, ok := value.(jsontext.Value)
|
||||||
if !ok {
|
if !ok {
|
||||||
return fmt.Errorf("unexpected type %T for field %s", value, name)
|
return fmt.Errorf("unexpected type %T for field %s", value, name)
|
||||||
}
|
}
|
||||||
@@ -43151,8 +43151,8 @@ type UsageCleanupTaskMutation struct {
|
|||||||
created_at *time.Time
|
created_at *time.Time
|
||||||
updated_at *time.Time
|
updated_at *time.Time
|
||||||
status *string
|
status *string
|
||||||
filters *json.RawMessage
|
filters *jsontext.Value
|
||||||
appendfilters json.RawMessage
|
appendfilters jsontext.Value
|
||||||
created_by *int64
|
created_by *int64
|
||||||
addcreated_by *int64
|
addcreated_by *int64
|
||||||
deleted_rows *int64
|
deleted_rows *int64
|
||||||
@@ -43376,13 +43376,13 @@ func (m *UsageCleanupTaskMutation) ResetStatus() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// SetFilters sets the "filters" field.
|
// SetFilters sets the "filters" field.
|
||||||
func (m *UsageCleanupTaskMutation) SetFilters(jm json.RawMessage) {
|
func (m *UsageCleanupTaskMutation) SetFilters(j jsontext.Value) {
|
||||||
m.filters = &jm
|
m.filters = &j
|
||||||
m.appendfilters = nil
|
m.appendfilters = nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// Filters returns the value of the "filters" field in the mutation.
|
// 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
|
v := m.filters
|
||||||
if v == nil {
|
if v == nil {
|
||||||
return
|
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.
|
// 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.
|
// 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.
|
// 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) {
|
if !m.op.Is(OpUpdateOne) {
|
||||||
return v, errors.New("OldFilters is only allowed on UpdateOne operations")
|
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
|
return oldValue.Filters, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// AppendFilters adds jm to the "filters" field.
|
// AppendFilters adds j to the "filters" field.
|
||||||
func (m *UsageCleanupTaskMutation) AppendFilters(jm json.RawMessage) {
|
func (m *UsageCleanupTaskMutation) AppendFilters(j jsontext.Value) {
|
||||||
m.appendfilters = append(m.appendfilters, jm...)
|
m.appendfilters = append(m.appendfilters, j...)
|
||||||
}
|
}
|
||||||
|
|
||||||
// AppendedFilters returns the list of values that were appended to the "filters" field in this mutation.
|
// 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 {
|
if len(m.appendfilters) == 0 {
|
||||||
return nil, false
|
return nil, false
|
||||||
}
|
}
|
||||||
@@ -43964,7 +43964,7 @@ func (m *UsageCleanupTaskMutation) SetField(name string, value ent.Value) error
|
|||||||
m.SetStatus(v)
|
m.SetStatus(v)
|
||||||
return nil
|
return nil
|
||||||
case usagecleanuptask.FieldFilters:
|
case usagecleanuptask.FieldFilters:
|
||||||
v, ok := value.(json.RawMessage)
|
v, ok := value.(jsontext.Value)
|
||||||
if !ok {
|
if !ok {
|
||||||
return fmt.Errorf("unexpected type %T for field %s", value, name)
|
return fmt.Errorf("unexpected type %T for field %s", value, name)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ package ent
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
|
"encoding/json/jsontext"
|
||||||
"fmt"
|
"fmt"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
@@ -25,7 +26,7 @@ type UsageCleanupTask struct {
|
|||||||
// Status holds the value of the "status" field.
|
// Status holds the value of the "status" field.
|
||||||
Status string `json:"status,omitempty"`
|
Status string `json:"status,omitempty"`
|
||||||
// Filters holds the value of the "filters" field.
|
// 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 holds the value of the "created_by" field.
|
||||||
CreatedBy int64 `json:"created_by,omitempty"`
|
CreatedBy int64 `json:"created_by,omitempty"`
|
||||||
// DeletedRows holds the value of the "deleted_rows" field.
|
// DeletedRows holds the value of the "deleted_rows" field.
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ package ent
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"encoding/json"
|
"encoding/json/jsontext"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"time"
|
"time"
|
||||||
@@ -58,7 +58,7 @@ func (_c *UsageCleanupTaskCreate) SetStatus(v string) *UsageCleanupTaskCreate {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// SetFilters sets the "filters" field.
|
// 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)
|
_c.mutation.SetFilters(v)
|
||||||
return _c
|
return _c
|
||||||
}
|
}
|
||||||
@@ -375,7 +375,7 @@ func (u *UsageCleanupTaskUpsert) UpdateStatus() *UsageCleanupTaskUpsert {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// SetFilters sets the "filters" field.
|
// 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)
|
u.Set(usagecleanuptask.FieldFilters, v)
|
||||||
return u
|
return u
|
||||||
}
|
}
|
||||||
@@ -592,7 +592,7 @@ func (u *UsageCleanupTaskUpsertOne) UpdateStatus() *UsageCleanupTaskUpsertOne {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// SetFilters sets the "filters" field.
|
// 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) {
|
return u.Update(func(s *UsageCleanupTaskUpsert) {
|
||||||
s.SetFilters(v)
|
s.SetFilters(v)
|
||||||
})
|
})
|
||||||
@@ -999,7 +999,7 @@ func (u *UsageCleanupTaskUpsertBulk) UpdateStatus() *UsageCleanupTaskUpsertBulk
|
|||||||
}
|
}
|
||||||
|
|
||||||
// SetFilters sets the "filters" field.
|
// 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) {
|
return u.Update(func(s *UsageCleanupTaskUpsert) {
|
||||||
s.SetFilters(v)
|
s.SetFilters(v)
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ package ent
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"encoding/json"
|
"encoding/json/jsontext"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"time"
|
"time"
|
||||||
@@ -51,13 +51,13 @@ func (_u *UsageCleanupTaskUpdate) SetNillableStatus(v *string) *UsageCleanupTask
|
|||||||
}
|
}
|
||||||
|
|
||||||
// SetFilters sets the "filters" field.
|
// 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)
|
_u.mutation.SetFilters(v)
|
||||||
return _u
|
return _u
|
||||||
}
|
}
|
||||||
|
|
||||||
// AppendFilters appends value to the "filters" field.
|
// 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)
|
_u.mutation.AppendFilters(v)
|
||||||
return _u
|
return _u
|
||||||
}
|
}
|
||||||
@@ -374,13 +374,13 @@ func (_u *UsageCleanupTaskUpdateOne) SetNillableStatus(v *string) *UsageCleanupT
|
|||||||
}
|
}
|
||||||
|
|
||||||
// SetFilters sets the "filters" field.
|
// 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)
|
_u.mutation.SetFilters(v)
|
||||||
return _u
|
return _u
|
||||||
}
|
}
|
||||||
|
|
||||||
// AppendFilters appends value to the "filters" field.
|
// 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)
|
_u.mutation.AppendFilters(v)
|
||||||
return _u
|
return _u
|
||||||
}
|
}
|
||||||
|
|||||||
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
module github.com/Wei-Shaw/sub2api
|
module github.com/Wei-Shaw/sub2api
|
||||||
|
|
||||||
go 1.26.6
|
go 1.27.0
|
||||||
|
|
||||||
require (
|
require (
|
||||||
entgo.io/ent v0.14.5
|
entgo.io/ent v0.14.5
|
||||||
|
|||||||
@@ -615,8 +615,10 @@ func (h *GrokOAuthHandler) ResetQuota(c *gin.Context) {
|
|||||||
response.BadRequest(c, "grok quota service is not enabled")
|
response.BadRequest(c, "grok quota service is not enabled")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
// ResetQuota 恒返回 GROK_QUOTA_RESET_UNSUPPORTED(xAI 无 OAuth 配额重置接口),err != nil 恒真为预期。
|
||||||
|
//nolint:staticcheck // SA4023
|
||||||
result, err := h.quotaService.ResetQuota(c.Request.Context(), accountID)
|
result, err := h.quotaService.ResetQuota(c.Request.Context(), accountID)
|
||||||
if err != nil {
|
if err != nil { //nolint:staticcheck // SA4023
|
||||||
response.ErrorFrom(c, err)
|
response.ErrorFrom(c, err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -441,7 +441,7 @@ func buildSettingKeyByJSONName() map[string]string {
|
|||||||
out := make(map[string]string, t.NumField())
|
out := make(map[string]string, t.NumField())
|
||||||
for i := 0; i < t.NumField(); i++ {
|
for i := 0; i < t.NumField(); i++ {
|
||||||
field := t.Field(i)
|
field := t.Field(i)
|
||||||
if field.Type.Kind() == reflect.Ptr {
|
if field.Type.Kind() == reflect.Pointer {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
name, _, _ := strings.Cut(field.Tag.Get("json"), ",")
|
name, _, _ := strings.Cut(field.Tag.Get("json"), ",")
|
||||||
|
|||||||
@@ -1144,10 +1144,10 @@ func (k oidcJWK) publicKey() (any, error) {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("decode ec y: %w", err)
|
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 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:
|
default:
|
||||||
return nil, fmt.Errorf("unsupported jwk kty: %s", k.Kty)
|
return nil, fmt.Errorf("unsupported jwk kty: %s", k.Kty)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,10 @@
|
|||||||
package repository
|
package repository
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"context"
|
||||||
|
"crypto/x509"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
"net/http/httptest"
|
||||||
"net/url"
|
"net/url"
|
||||||
"testing"
|
"testing"
|
||||||
"time"
|
"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 静默掐断会成为“死连接”:
|
// Codex/OpenAI 上游改走 HTTP/2 后,池化连接被代理/NAT 静默掐断会成为“死连接”:
|
||||||
// 两端都以为连接存活,请求落上去会挂到 TCP 重传超时(分钟级)才失败。Go 的
|
// 两端都以为连接存活,请求落上去会挂到 TCP 重传超时(分钟级)才失败。Go 的
|
||||||
// http2.Transport 默认 ReadIdleTimeout=0(不发健康 PING),无法检测这种死连接。
|
// http2.Transport 默认 ReadIdleTimeout=0(不发健康 PING),无法检测这种死连接。
|
||||||
@@ -34,7 +47,7 @@ func TestEnableOpenAIHTTP2KeepAlive_EnablesPingHealthCheck(t *testing.T) {
|
|||||||
require.Positive(t, h2.ReadIdleTimeout, "必须启用空闲 PING 探测以剔除死连接")
|
require.Positive(t, h2.ReadIdleTimeout, "必须启用空闲 PING 探测以剔除死连接")
|
||||||
require.Equal(t, openAIHTTP2ReadIdleTimeout, h2.ReadIdleTimeout)
|
require.Equal(t, openAIHTTP2ReadIdleTimeout, h2.ReadIdleTimeout)
|
||||||
require.Equal(t, openAIHTTP2PingTimeout, h2.PingTimeout, "PING 无响应必须有超时判定")
|
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 健康探测,从源头剔除死连接。
|
// openai_h2 模式构建的 Transport 必须带上 H2 PING 健康探测,从源头剔除死连接。
|
||||||
@@ -42,17 +55,45 @@ func TestBuildUpstreamTransport_OpenAIH2_EnablesPingHealthCheck(t *testing.T) {
|
|||||||
tr, err := buildUpstreamTransport(http2KeepAliveTestPoolSettings(), nil, upstreamProtocolModeOpenAIH2)
|
tr, err := buildUpstreamTransport(http2KeepAliveTestPoolSettings(), nil, upstreamProtocolModeOpenAIH2)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
require.True(t, tr.ForceAttemptHTTP2, "openai_h2 必须启用 HTTP/2")
|
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(惰性配置,
|
// 非 H2 模式(default/h1)不应因本次改动被误配置:default 走 Go 自动 H2(惰性配置,
|
||||||
// 构建时 TLSNextProto 仍为空),h1 模式显式禁用 H2。避免波及 Claude/Gemini 热路径。
|
// 构建时 Protocols/TLSNextProto 仍为空),h1 模式显式禁用 H2。避免波及 Claude/Gemini 热路径。
|
||||||
func TestBuildUpstreamTransport_NonOpenAIH2_NotEagerlyConfigured(t *testing.T) {
|
func TestBuildUpstreamTransport_NonOpenAIH2_NotEagerlyConfigured(t *testing.T) {
|
||||||
tr, err := buildUpstreamTransport(http2KeepAliveTestPoolSettings(), nil, upstreamProtocolModeDefault)
|
tr, err := buildUpstreamTransport(http2KeepAliveTestPoolSettings(), nil, upstreamProtocolModeDefault)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
require.Nil(t, tr.Protocols, "default 模式不应在构建期主动配置 http2 keepalive")
|
||||||
require.Nil(t, tr.TLSNextProto["h2"], "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 账号的真实生产路径:
|
// 死连接在经 HTTP 代理(CONNECT 隧道)时最高发,这是带 proxy 账号的真实生产路径:
|
||||||
// 显式 http2 配置须与 Transport.Proxy 同时正确生效,不能相互干扰。
|
// 显式 http2 配置须与 Transport.Proxy 同时正确生效,不能相互干扰。
|
||||||
func TestBuildUpstreamTransport_OpenAIH2_WithHTTPProxy_EnablesKeepAlive(t *testing.T) {
|
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)
|
tr, err := buildUpstreamTransport(http2KeepAliveTestPoolSettings(), proxyURL, upstreamProtocolModeOpenAIH2)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
require.True(t, tr.ForceAttemptHTTP2)
|
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 生效")
|
require.NotNil(t, tr.Proxy, "HTTP 代理仍须通过 Transport.Proxy 生效")
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -780,7 +780,7 @@ func (s usageLogScannerStub) Scan(dest ...any) error {
|
|||||||
}
|
}
|
||||||
for i := range dest {
|
for i := range dest {
|
||||||
dv := reflect.ValueOf(dest[i])
|
dv := reflect.ValueOf(dest[i])
|
||||||
if dv.Kind() != reflect.Ptr {
|
if dv.Kind() != reflect.Pointer {
|
||||||
return fmt.Errorf("dest[%d] is not pointer", i)
|
return fmt.Errorf("dest[%d] is not pointer", i)
|
||||||
}
|
}
|
||||||
dv.Elem().Set(reflect.ValueOf(s.values[i]))
|
dv.Elem().Set(reflect.ValueOf(s.values[i]))
|
||||||
|
|||||||
+1
-1
@@ -7,7 +7,7 @@
|
|||||||
# =============================================================================
|
# =============================================================================
|
||||||
|
|
||||||
ARG NODE_IMAGE=node:24-alpine
|
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 ALPINE_IMAGE=alpine:3.20
|
||||||
ARG GOPROXY=https://goproxy.cn,direct
|
ARG GOPROXY=https://goproxy.cn,direct
|
||||||
ARG GOSUMDB=sum.golang.google.cn
|
ARG GOSUMDB=sum.golang.google.cn
|
||||||
|
|||||||
Reference in New Issue
Block a user