Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
ba35c61
fix(images): pass content-moderation refusals through as 400 instead …
404QAQ Jun 23, 2026
ecbe076
fix(keys): add CLAUDE_CODE_ATTRIBUTION_HEADER=0 to Claude Code termin…
feitianbubu Jun 23, 2026
6457409
fix(gateway): enforce codex_cli_only restriction on /v1/chat/completions
feitianbubu Jun 23, 2026
dc85d07
fix(ops): prevent monitoring trend cards from growing unbounded
zichuanwangcloud-gif Jun 23, 2026
c2a1c6b
fix(auth): stop swallowing email auth identity create error via shado…
feitianbubu Jun 23, 2026
73da15d
fix(openai): fail over on chat transport errors
hsn8086 Jun 23, 2026
83a9bd9
fix(admin/usage): populate cache creation/read token breakdown in stats
feitianbubu Jun 24, 2026
9c019c0
test(admin/usage): update sqlmock rows for cache breakdown columns
feitianbubu Jun 24, 2026
3d293bc
fix: avoid default codex instructions for chat bridge
StarryKira Jun 24, 2026
e84926c
fix(token-refresh): treat refresh_token_invalidated as non-retryable
jianjianai Jun 25, 2026
bcc91e5
fix(antigravity): add project fallback for standard tier
wucm667 Jun 25, 2026
271ef43
Detect OpenAI overloaded error codes
zeyugao Jun 25, 2026
97b758e
fix(apicompat): avoid doubling tool_call arguments from single-chunk …
visa2 Jun 25, 2026
efb6cf0
fix(lint): gofmt token_refresh_service.go after refresh_token_invalid…
Wei-Shaw Jun 26, 2026
ec6bc0a
fix(ci): stabilize internal fork checks
SilkageNet Jun 26, 2026
112cbf4
fix(ci): align internal baseline with repositories
SilkageNet Jun 26, 2026
d80297c
fix(ci): restore baseline conflict indexes
SilkageNet Jun 26, 2026
36e2369
fix(ci): complete internal baseline schema contracts
SilkageNet Jun 26, 2026
e006d4d
fix(ci): align internal baseline schema
SilkageNet Jun 26, 2026
08f458c
fix(ci): tolerate seeded default groups
SilkageNet Jun 26, 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
4 changes: 2 additions & 2 deletions .github/workflows/backend-ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -35,11 +35,11 @@ jobs:
- name: Setup pnpm
uses: pnpm/action-setup@v6
with:
version: 9
version: 9.15.9
- name: Setup Node.js
uses: actions/setup-node@v6
with:
node-version: '20'
node-version-file: .nvmrc
cache: 'pnpm'
cache-dependency-path: frontend/pnpm-lock.yaml
- name: Install frontend dependencies
Expand Down
4 changes: 2 additions & 2 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -60,12 +60,12 @@ jobs:
- name: Setup pnpm
uses: pnpm/action-setup@v6
with:
version: 9
version: 9.15.9

- name: Setup Node.js
uses: actions/setup-node@v6
with:
node-version: '20'
node-version-file: .nvmrc
cache: 'pnpm'
cache-dependency-path: frontend/pnpm-lock.yaml

Expand Down
4 changes: 2 additions & 2 deletions .github/workflows/security-scan.yml
Original file line number Diff line number Diff line change
Expand Up @@ -37,11 +37,11 @@ jobs:
- name: Set up pnpm
uses: pnpm/action-setup@v6
with:
version: 9
version: 9.15.9
- name: Set up Node.js
uses: actions/setup-node@v6
with:
node-version: '20'
node-version-file: .nvmrc
cache: 'pnpm'
cache-dependency-path: frontend/pnpm-lock.yaml
- name: Install dependencies
Expand Down
11 changes: 6 additions & 5 deletions Makefile
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
.PHONY: build build-backend build-frontend build-datamanagementd test test-backend test-frontend test-frontend-critical test-datamanagementd secret-scan

FRONTEND_CRITICAL_VITEST := \
src/views/auth/__tests__/LinuxDoCallbackView.spec.ts \
src/views/auth/__tests__/WechatCallbackView.spec.ts \
src/views/user/__tests__/PaymentView.spec.ts \
src/views/user/__tests__/PaymentResultView.spec.ts \
src/components/user/profile/__tests__/ProfileInfoCard.spec.ts \
src/components/__tests__/ApiKeyCreate.spec.ts \
src/components/__tests__/Dashboard.spec.ts \
src/components/__tests__/LoginForm.spec.ts \
src/views/auth/__tests__/OidcCallbackView.spec.ts \
src/views/user/__tests__/UsageView.spec.ts \
src/views/admin/__tests__/UsersView.spec.ts \
src/views/admin/__tests__/SettingsView.spec.ts

# 一键编译前后端
Expand Down
2 changes: 1 addition & 1 deletion backend/ent/schema/user.go
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@ func (User) Fields() []ent.Field {
field.String("username").
MaxLen(100).
Default(""),
field.String("notes").
field.String("notes").
SchemaType(map[string]string{dialect.Postgres: "text"}).
Default(""),

Expand Down
8 changes: 0 additions & 8 deletions backend/internal/handler/auth_oauth_pending_flow.go
Original file line number Diff line number Diff line change
Expand Up @@ -427,14 +427,6 @@ func cloneOAuthMetadata(values map[string]any) map[string]any {
return cloned
}

func mergeOAuthMetadata(base map[string]any, overlay map[string]any) map[string]any {
merged := cloneOAuthMetadata(base)
for key, value := range overlay {
merged[key] = value
}
return merged
}

func normalizeAdoptedOAuthDisplayName(value string) string {
value = strings.TrimSpace(value)
if len([]rune(value)) > 100 {
Expand Down
47 changes: 0 additions & 47 deletions backend/internal/handler/auth_oauth_test_helpers_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,28 +2,11 @@ package handler

import (
"net/http"
"net/http/httptest"
"net/url"
"testing"

"github.com/stretchr/testify/require"
)

func buildEncodedOAuthBindUserCookie(t *testing.T, userID int64, secret string) string {
t.Helper()
value, err := buildOAuthBindUserCookieValue(userID, secret)
require.NoError(t, err)
return value
}

func encodedCookie(name, value string) *http.Cookie {
return &http.Cookie{
Name: name,
Value: encodeCookieValue(value),
Path: "/",
}
}

func findCookie(cookies []*http.Cookie, name string) *http.Cookie {
for _, cookie := range cookies {
if cookie.Name == name {
Expand All @@ -33,39 +16,9 @@ func findCookie(cookies []*http.Cookie, name string) *http.Cookie {
return nil
}

func requireCookieCleared(t *testing.T, recorder *httptest.ResponseRecorder, name string) {
t.Helper()
cookie := findCookie(recorder.Result().Cookies(), name)
require.NotNil(t, cookie)
require.Equal(t, -1, cookie.MaxAge)
}

func decodeCookieValueForTest(t *testing.T, value string) string {
t.Helper()
decoded, err := decodeCookieValue(value)
require.NoError(t, err)
return decoded
}

func assertOAuthRedirectError(t *testing.T, location string, errorCode string, errorMessage string) {
t.Helper()
values := parseOAuthRedirectFragment(t, location)
require.Equal(t, errorCode, values.Get("error"))
require.Equal(t, errorMessage, values.Get("error_message"))
}

func parseOAuthRedirectFragment(t *testing.T, location string) url.Values {
t.Helper()
require.NotEmpty(t, location)

parsed, err := url.Parse(location)
require.NoError(t, err)

rawValues := parsed.RawQuery
if rawValues == "" {
rawValues = parsed.Fragment
}
values, err := url.ParseQuery(rawValues)
require.NoError(t, err)
return values
}
40 changes: 34 additions & 6 deletions backend/internal/handler/auth_oidc_oauth.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package handler

import (
"context"
"crypto/ecdh"
"crypto/ecdsa"
"crypto/elliptic"
"crypto/rsa"
Expand Down Expand Up @@ -672,10 +673,7 @@ func (h *AuthHandler) CompleteOIDCOAuthAccountCreation(c *gin.Context) {
respondPendingOAuthBindingApplyError(c, err)
return
}
decision, err := h.ensurePendingOAuthAdoptionDecision(c, session.ID, oauthAdoptionDecisionRequest{
AdoptDisplayName: req.AdoptDisplayName,
AdoptAvatar: req.AdoptAvatar,
})
decision, err := h.ensurePendingOAuthAdoptionDecision(c, session.ID, oauthAdoptionDecisionRequest(req))
if err != nil {
response.ErrorFrom(c, err)
return
Expand Down Expand Up @@ -1113,13 +1111,21 @@ func (k oidcJWK) publicKey() (any, error) {
return &rsa.PublicKey{N: n, E: e}, nil
case "EC":
var curve elliptic.Curve
var ecdhCurve ecdh.Curve
var coordinateSize int
switch strings.TrimSpace(k.Crv) {
case "P-256":
curve = elliptic.P256()
ecdhCurve = ecdh.P256()
coordinateSize = 32
case "P-384":
curve = elliptic.P384()
ecdhCurve = ecdh.P384()
coordinateSize = 48
case "P-521":
curve = elliptic.P521()
ecdhCurve = ecdh.P521()
coordinateSize = 66
default:
return nil, fmt.Errorf("unsupported ec curve: %s", k.Crv)
}
Expand All @@ -1131,8 +1137,12 @@ func (k oidcJWK) publicKey() (any, error) {
if err != nil {
return nil, fmt.Errorf("decode ec y: %w", err)
}
if !curve.IsOnCurve(x, y) {
return nil, errors.New("ec point is not on curve")
point, err := encodeOIDCECDHPoint(x, y, coordinateSize)
if err != nil {
return nil, err
}
if _, err := ecdhCurve.NewPublicKey(point); err != nil {
return nil, fmt.Errorf("invalid ec public key: %w", err)
}
return &ecdsa.PublicKey{Curve: curve, X: x, Y: y}, nil
default:
Expand All @@ -1151,6 +1161,24 @@ func decodeBase64URLBigInt(raw string) (*big.Int, error) {
return new(big.Int).SetBytes(buf), nil
}

func encodeOIDCECDHPoint(x, y *big.Int, coordinateSize int) ([]byte, error) {
if x == nil || y == nil {
return nil, errors.New("missing ec coordinate")
}
if x.Sign() < 0 || y.Sign() < 0 {
return nil, errors.New("negative ec coordinate")
}
if len(x.Bytes()) > coordinateSize || len(y.Bytes()) > coordinateSize {
return nil, errors.New("ec coordinate is too large")
}

point := make([]byte, 1+coordinateSize*2)
point[0] = 4
x.FillBytes(point[1 : 1+coordinateSize])
y.FillBytes(point[1+coordinateSize:])
return point, nil
}

func containsString(values []string, target string) bool {
target = strings.TrimSpace(target)
for _, v := range values {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -737,6 +737,13 @@ func ChatCompletionsChunkToResponsesEvents(
copyCall.ID = generateItemID()
}
copyCall.Type = "function"
// Arguments are accumulated by the shared block below so the
// emitted delta and the stored value stay in sync. Some upstreams
// (e.g. GLM/Zhipu) pack id+name+arguments into the first tool_call
// chunk; without this reset the first chunk's arguments would be
// counted twice (once from this copy, once from the += below),
// producing a doubled, invalid JSON like {"a":1}{"a":1}.
copyCall.Function.Arguments = ""
state.ToolCalls[idx] = &copyCall
stored = &copyCall
itemID := generateItemID()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -179,6 +179,41 @@ func TestStream_ToolCallLifecycleComplete(t *testing.T) {
require.True(t, sawItemDone, "function_call output_item.done missing")
}

// TestStream_ToolCallArgumentsInFirstChunkNotDoubled guards the GLM/Zhipu shape
// where a single tool_call delta chunk carries id+name+arguments together.
// Earlier code copied the whole tool_call (including arguments) into state and
// then accumulated the same chunk's arguments again, producing a doubled,
// invalid JSON like {"cmd":"ls"}{"cmd":"ls"} that breaks Codex tool parsing
// ("trailing characters").
func TestStream_ToolCallArgumentsInFirstChunkNotDoubled(t *testing.T) {
events := collectStreamEvents(t, []string{
`{"choices":[{"index":0,"delta":{"role":"assistant"}}]}`,
`{"choices":[{"index":0,"delta":{"tool_calls":[{"index":0,"id":"call_a","type":"function","function":{"name":"exec","arguments":"{\"cmd\":\"ls\"}"}}]}}]}`,
`{"choices":[{"index":0,"delta":{},"finish_reason":"tool_calls"}]}`,
})

var argsDelta strings.Builder
var sawArgsDone, sawItemDone bool
for _, e := range events {
switch e.Type {
case "response.function_call_arguments.delta":
_, _ = argsDelta.WriteString(e.Delta)
case "response.function_call_arguments.done":
sawArgsDone = true
require.Equal(t, `{"cmd":"ls"}`, e.Arguments)
case "response.output_item.done":
if e.Item != nil && e.Item.Type == "function_call" {
sawItemDone = true
require.Equal(t, `{"cmd":"ls"}`, e.Item.Arguments)
}
}
}
require.True(t, sawArgsDone, "function_call_arguments.done missing")
require.True(t, sawItemDone, "function_call output_item.done missing")
// Accumulated deltas must equal the final arguments exactly (no duplication).
require.Equal(t, `{"cmd":"ls"}`, argsDelta.String())
}

// TestStream_SSEWireComplete drives the full stream through SSE encoding and
// asserts the function_call events carry complete fields on the wire.
func TestStream_SSEWireComplete(t *testing.T) {
Expand Down
1 change: 1 addition & 0 deletions backend/internal/repository/github_release_service_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ type testTransport struct {
func (t *testTransport) RoundTrip(req *http.Request) (*http.Response, error) {
// Rewrite the URL to point to our test server
testURL := t.testServerURL + req.URL.Path
// #nosec G704 -- testServerURL is created by httptest.Server in this test.
newReq, err := http.NewRequestWithContext(req.Context(), req.Method, testURL, req.Body)
if err != nil {
return nil, err
Expand Down
41 changes: 0 additions & 41 deletions backend/internal/repository/migrations_runner.go
Original file line number Diff line number Diff line change
Expand Up @@ -37,14 +37,6 @@ const nonTransactionalMigrationSuffix = "_notx.sql"
const schedulerOutboxPendingDedupKeyMigration = "153_scheduler_outbox_pending_dedup_key_index_notx.sql"
const schedulerOutboxPendingDedupKeyIndex = "idx_scheduler_outbox_pending_dedup_key"

type migrationChecksumCompatibilityRule struct {
fileChecksum string
acceptedDBChecksum map[string]struct{}
acceptedChecksums map[string]struct{}
}

var migrationChecksumCompatibilityRules = map[string]migrationChecksumCompatibilityRule{}

// ApplyMigrations 将嵌入的 SQL 迁移文件应用到指定的数据库。
//
// 该函数可以在每次应用启动时安全调用:
Expand Down Expand Up @@ -136,10 +128,6 @@ func applyMigrationsFS(ctx context.Context, db *sql.DB, fsys fs.FS) error {
if rowErr == nil {
// 迁移已应用,验证校验和是否匹配
if existing != checksum {
// 兼容特定历史误改场景(仅白名单规则),其余仍保持严格不可变约束。
if isMigrationChecksumCompatible(name, existing, checksum) {
continue
}
// 校验和不匹配意味着迁移文件在应用后被修改,这是危险的。
// 正确的做法是创建新的迁移文件来进行变更。
return fmt.Errorf(
Expand Down Expand Up @@ -257,35 +245,6 @@ func indexIsInvalid(ctx context.Context, db *sql.DB, indexName string) (bool, er
return invalid, err
}

func checksumSet(values ...string) map[string]struct{} {
out := make(map[string]struct{}, len(values))
for _, value := range values {
out[value] = struct{}{}
}
return out
}

func newMigrationChecksumCompatibilityRule(fileChecksum string, acceptedDBChecksums ...string) migrationChecksumCompatibilityRule {
return migrationChecksumCompatibilityRule{
fileChecksum: fileChecksum,
acceptedDBChecksum: checksumSet(acceptedDBChecksums...),
acceptedChecksums: checksumSet(append([]string{fileChecksum}, acceptedDBChecksums...)...),
}
}

func isMigrationChecksumCompatible(name, dbChecksum, fileChecksum string) bool {
rule, ok := migrationChecksumCompatibilityRules[name]
if !ok {
return false
}
_, dbOK := rule.acceptedChecksums[dbChecksum]
if !dbOK {
return false
}
_, fileOK := rule.acceptedChecksums[fileChecksum]
return fileOK
}

func validateMigrationExecutionMode(name, content string) (bool, error) {
normalizedName := strings.ToLower(strings.TrimSpace(name))
upperContent := strings.ToUpper(content)
Expand Down
16 changes: 0 additions & 16 deletions backend/internal/repository/migrations_runner_checksum_test.go

This file was deleted.

Loading