Skip to content
GitLab
Menu
Projects
Groups
Snippets
Loading...
Help
Help
Support
Community forum
Keyboard shortcuts
?
Submit feedback
Sign in / Register
Toggle navigation
Menu
Open sidebar
陈曦
sub2api
Commits
c86d445c
Commit
c86d445c
authored
Jan 04, 2026
by
IanShaw027
Browse files
fix(frontend): sync with main and finalize i18n & component optimizations
parents
6c036d7b
e78c8646
Changes
186
Hide whitespace changes
Inline
Side-by-side
backend/internal/pkg/antigravity/client.go
View file @
c86d445c
// Package antigravity provides a client for the Antigravity API.
package
antigravity
package
antigravity
import
(
import
(
...
@@ -57,6 +58,29 @@ type TierInfo struct {
...
@@ -57,6 +58,29 @@ type TierInfo struct {
Description
string
`json:"description"`
// 描述
Description
string
`json:"description"`
// 描述
}
}
// UnmarshalJSON supports both legacy string tiers and object tiers.
func
(
t
*
TierInfo
)
UnmarshalJSON
(
data
[]
byte
)
error
{
data
=
bytes
.
TrimSpace
(
data
)
if
len
(
data
)
==
0
||
string
(
data
)
==
"null"
{
return
nil
}
if
data
[
0
]
==
'"'
{
var
id
string
if
err
:=
json
.
Unmarshal
(
data
,
&
id
);
err
!=
nil
{
return
err
}
t
.
ID
=
id
return
nil
}
type
alias
TierInfo
var
decoded
alias
if
err
:=
json
.
Unmarshal
(
data
,
&
decoded
);
err
!=
nil
{
return
err
}
*
t
=
TierInfo
(
decoded
)
return
nil
}
// IneligibleTier 不符合条件的层级信息
// IneligibleTier 不符合条件的层级信息
type
IneligibleTier
struct
{
type
IneligibleTier
struct
{
Tier
*
TierInfo
`json:"tier,omitempty"`
Tier
*
TierInfo
`json:"tier,omitempty"`
...
...
backend/internal/pkg/antigravity/request_transformer.go
View file @
c86d445c
...
@@ -240,10 +240,13 @@ func buildParts(content json.RawMessage, toolIDToName map[string]string, allowDu
...
@@ -240,10 +240,13 @@ func buildParts(content json.RawMessage, toolIDToName map[string]string, allowDu
ID
:
block
.
ID
,
ID
:
block
.
ID
,
},
},
}
}
// 只有 Gemini 模型使用 dummy signature
// tool_use 的 signature 处理:
// Claude 模型不设置 signature(避免验证问题)
// - Gemini 模型:使用 dummy signature(跳过 thought_signature 校验)
// - Claude 模型:透传上游返回的真实 signature(Vertex/Google 需要完整签名链路)
if
allowDummyThought
{
if
allowDummyThought
{
part
.
ThoughtSignature
=
dummyThoughtSignature
part
.
ThoughtSignature
=
dummyThoughtSignature
}
else
if
block
.
Signature
!=
""
&&
block
.
Signature
!=
dummyThoughtSignature
{
part
.
ThoughtSignature
=
block
.
Signature
}
}
parts
=
append
(
parts
,
part
)
parts
=
append
(
parts
,
part
)
...
...
backend/internal/pkg/antigravity/request_transformer_test.go
View file @
c86d445c
...
@@ -15,26 +15,26 @@ func TestBuildParts_ThinkingBlockWithoutSignature(t *testing.T) {
...
@@ -15,26 +15,26 @@ func TestBuildParts_ThinkingBlockWithoutSignature(t *testing.T) {
description
string
description
string
}{
}{
{
{
name
:
"Claude model -
ski
p thinking
block
without signature"
,
name
:
"Claude model -
dro
p thinking without signature"
,
content
:
`[
content
:
`[
{"type": "text", "text": "Hello"},
{"type": "text", "text": "Hello"},
{"type": "thinking", "thinking": "Let me think...", "signature": ""},
{"type": "thinking", "thinking": "Let me think...", "signature": ""},
{"type": "text", "text": "World"}
{"type": "text", "text": "World"}
]`
,
]`
,
allowDummyThought
:
false
,
allowDummyThought
:
false
,
expectedParts
:
2
,
//
只有两个text block
expectedParts
:
2
,
//
thinking 内容被丢弃
description
:
"Claude模型应
该跳过
无signature的thinking block"
,
description
:
"Claude模型应
丢弃
无signature的thinking block
内容
"
,
},
},
{
{
name
:
"Claude model -
keep
thinking block with signature"
,
name
:
"Claude model -
preserve
thinking block with signature"
,
content
:
`[
content
:
`[
{"type": "text", "text": "Hello"},
{"type": "text", "text": "Hello"},
{"type": "thinking", "thinking": "Let me think...", "signature": "
valid_sig
"},
{"type": "thinking", "thinking": "Let me think...", "signature": "
sig_real_123
"},
{"type": "text", "text": "World"}
{"type": "text", "text": "World"}
]`
,
]`
,
allowDummyThought
:
false
,
allowDummyThought
:
false
,
expectedParts
:
3
,
// 三个block都保留
expectedParts
:
3
,
description
:
"Claude模型应
该保留有
signature
的
thinking block"
,
description
:
"Claude模型应
透传带
signature
的
thinking block
(用于 Vertex 签名链路)
"
,
},
},
{
{
name
:
"Gemini model - use dummy signature"
,
name
:
"Gemini model - use dummy signature"
,
...
@@ -61,10 +61,64 @@ func TestBuildParts_ThinkingBlockWithoutSignature(t *testing.T) {
...
@@ -61,10 +61,64 @@ func TestBuildParts_ThinkingBlockWithoutSignature(t *testing.T) {
if
len
(
parts
)
!=
tt
.
expectedParts
{
if
len
(
parts
)
!=
tt
.
expectedParts
{
t
.
Errorf
(
"%s: got %d parts, want %d parts"
,
tt
.
description
,
len
(
parts
),
tt
.
expectedParts
)
t
.
Errorf
(
"%s: got %d parts, want %d parts"
,
tt
.
description
,
len
(
parts
),
tt
.
expectedParts
)
}
}
switch
tt
.
name
{
case
"Claude model - preserve thinking block with signature"
:
if
len
(
parts
)
!=
3
{
t
.
Fatalf
(
"expected 3 parts, got %d"
,
len
(
parts
))
}
if
!
parts
[
1
]
.
Thought
||
parts
[
1
]
.
ThoughtSignature
!=
"sig_real_123"
{
t
.
Fatalf
(
"expected thought part with signature sig_real_123, got thought=%v signature=%q"
,
parts
[
1
]
.
Thought
,
parts
[
1
]
.
ThoughtSignature
)
}
case
"Gemini model - use dummy signature"
:
if
len
(
parts
)
!=
3
{
t
.
Fatalf
(
"expected 3 parts, got %d"
,
len
(
parts
))
}
if
!
parts
[
1
]
.
Thought
||
parts
[
1
]
.
ThoughtSignature
!=
dummyThoughtSignature
{
t
.
Fatalf
(
"expected dummy thought signature, got thought=%v signature=%q"
,
parts
[
1
]
.
Thought
,
parts
[
1
]
.
ThoughtSignature
)
}
}
})
})
}
}
}
}
func
TestBuildParts_ToolUseSignatureHandling
(
t
*
testing
.
T
)
{
content
:=
`[
{"type": "tool_use", "id": "t1", "name": "Bash", "input": {"command": "ls"}, "signature": "sig_tool_abc"}
]`
t
.
Run
(
"Gemini uses dummy tool_use signature"
,
func
(
t
*
testing
.
T
)
{
toolIDToName
:=
make
(
map
[
string
]
string
)
parts
,
err
:=
buildParts
(
json
.
RawMessage
(
content
),
toolIDToName
,
true
)
if
err
!=
nil
{
t
.
Fatalf
(
"buildParts() error = %v"
,
err
)
}
if
len
(
parts
)
!=
1
||
parts
[
0
]
.
FunctionCall
==
nil
{
t
.
Fatalf
(
"expected 1 functionCall part, got %+v"
,
parts
)
}
if
parts
[
0
]
.
ThoughtSignature
!=
dummyThoughtSignature
{
t
.
Fatalf
(
"expected dummy tool signature %q, got %q"
,
dummyThoughtSignature
,
parts
[
0
]
.
ThoughtSignature
)
}
})
t
.
Run
(
"Claude model - preserve valid signature for tool_use"
,
func
(
t
*
testing
.
T
)
{
toolIDToName
:=
make
(
map
[
string
]
string
)
parts
,
err
:=
buildParts
(
json
.
RawMessage
(
content
),
toolIDToName
,
false
)
if
err
!=
nil
{
t
.
Fatalf
(
"buildParts() error = %v"
,
err
)
}
if
len
(
parts
)
!=
1
||
parts
[
0
]
.
FunctionCall
==
nil
{
t
.
Fatalf
(
"expected 1 functionCall part, got %+v"
,
parts
)
}
// Claude 模型应透传有效的 signature(Vertex/Google 需要完整签名链路)
if
parts
[
0
]
.
ThoughtSignature
!=
"sig_tool_abc"
{
t
.
Fatalf
(
"expected preserved tool signature %q, got %q"
,
"sig_tool_abc"
,
parts
[
0
]
.
ThoughtSignature
)
}
})
}
// TestBuildTools_CustomTypeTools 测试custom类型工具转换
// TestBuildTools_CustomTypeTools 测试custom类型工具转换
func
TestBuildTools_CustomTypeTools
(
t
*
testing
.
T
)
{
func
TestBuildTools_CustomTypeTools
(
t
*
testing
.
T
)
{
tests
:=
[]
struct
{
tests
:=
[]
struct
{
...
...
backend/internal/pkg/claude/constants.go
View file @
c86d445c
// Package claude provides constants and helpers for Claude API integration.
package
claude
package
claude
// Claude Code 客户端相关常量
// Claude Code 客户端相关常量
...
@@ -16,13 +17,13 @@ const DefaultBetaHeader = BetaClaudeCode + "," + BetaOAuth + "," + BetaInterleav
...
@@ -16,13 +17,13 @@ const DefaultBetaHeader = BetaClaudeCode + "," + BetaOAuth + "," + BetaInterleav
// HaikuBetaHeader Haiku 模型使用的 anthropic-beta header(不需要 claude-code beta)
// HaikuBetaHeader Haiku 模型使用的 anthropic-beta header(不需要 claude-code beta)
const
HaikuBetaHeader
=
BetaOAuth
+
","
+
BetaInterleavedThinking
const
HaikuBetaHeader
=
BetaOAuth
+
","
+
BetaInterleavedThinking
// A
pi
KeyBetaHeader API-key 账号建议使用的 anthropic-beta header(不包含 oauth)
// A
PI
KeyBetaHeader API-key 账号建议使用的 anthropic-beta header(不包含 oauth)
const
A
pi
KeyBetaHeader
=
BetaClaudeCode
+
","
+
BetaInterleavedThinking
+
","
+
BetaFineGrainedToolStreaming
const
A
PI
KeyBetaHeader
=
BetaClaudeCode
+
","
+
BetaInterleavedThinking
+
","
+
BetaFineGrainedToolStreaming
// A
pi
KeyHaikuBetaHeader Haiku 模型在 API-key 账号下使用的 anthropic-beta header(不包含 oauth / claude-code)
// A
PI
KeyHaikuBetaHeader Haiku 模型在 API-key 账号下使用的 anthropic-beta header(不包含 oauth / claude-code)
const
A
pi
KeyHaikuBetaHeader
=
BetaInterleavedThinking
const
A
PI
KeyHaikuBetaHeader
=
BetaInterleavedThinking
// Claude Code 客户端默认请求头
//
DefaultHeaders 是
Claude Code 客户端默认请求头
。
var
DefaultHeaders
=
map
[
string
]
string
{
var
DefaultHeaders
=
map
[
string
]
string
{
"User-Agent"
:
"claude-cli/2.0.62 (external, cli)"
,
"User-Agent"
:
"claude-cli/2.0.62 (external, cli)"
,
"X-Stainless-Lang"
:
"js"
,
"X-Stainless-Lang"
:
"js"
,
...
...
backend/internal/pkg/errors/types.go
View file @
c86d445c
// Package errors provides application error types and helpers.
// nolint:mnd
// nolint:mnd
package
errors
package
errors
...
...
backend/internal/pkg/gemini/models.go
View file @
c86d445c
package
gemini
// Package gemini provides minimal fallback model metadata for Gemini native endpoints.
// This package provides minimal fallback model metadata for Gemini native endpoints.
// It is used when upstream model listing is unavailable (e.g. OAuth token missing AI Studio scopes).
// It is used when upstream model listing is unavailable (e.g. OAuth token missing AI Studio scopes).
package
gemini
type
Model
struct
{
type
Model
struct
{
Name
string
`json:"name"`
Name
string
`json:"name"`
...
...
backend/internal/pkg/geminicli/codeassist_types.go
View file @
c86d445c
package
geminicli
package
geminicli
import
(
"bytes"
"encoding/json"
)
// LoadCodeAssistRequest matches done-hub's internal Code Assist call.
// LoadCodeAssistRequest matches done-hub's internal Code Assist call.
type
LoadCodeAssistRequest
struct
{
type
LoadCodeAssistRequest
struct
{
Metadata
LoadCodeAssistMetadata
`json:"metadata"`
Metadata
LoadCodeAssistMetadata
`json:"metadata"`
...
@@ -11,12 +16,51 @@ type LoadCodeAssistMetadata struct {
...
@@ -11,12 +16,51 @@ type LoadCodeAssistMetadata struct {
PluginType
string
`json:"pluginType"`
PluginType
string
`json:"pluginType"`
}
}
type
TierInfo
struct
{
ID
string
`json:"id"`
}
// UnmarshalJSON supports both legacy string tiers and object tiers.
func
(
t
*
TierInfo
)
UnmarshalJSON
(
data
[]
byte
)
error
{
data
=
bytes
.
TrimSpace
(
data
)
if
len
(
data
)
==
0
||
string
(
data
)
==
"null"
{
return
nil
}
if
data
[
0
]
==
'"'
{
var
id
string
if
err
:=
json
.
Unmarshal
(
data
,
&
id
);
err
!=
nil
{
return
err
}
t
.
ID
=
id
return
nil
}
type
alias
TierInfo
var
decoded
alias
if
err
:=
json
.
Unmarshal
(
data
,
&
decoded
);
err
!=
nil
{
return
err
}
*
t
=
TierInfo
(
decoded
)
return
nil
}
type
LoadCodeAssistResponse
struct
{
type
LoadCodeAssistResponse
struct
{
CurrentTier
string
`json:"currentTier,omitempty"`
CurrentTier
*
TierInfo
`json:"currentTier,omitempty"`
PaidTier
*
TierInfo
`json:"paidTier,omitempty"`
CloudAICompanionProject
string
`json:"cloudaicompanionProject,omitempty"`
CloudAICompanionProject
string
`json:"cloudaicompanionProject,omitempty"`
AllowedTiers
[]
AllowedTier
`json:"allowedTiers,omitempty"`
AllowedTiers
[]
AllowedTier
`json:"allowedTiers,omitempty"`
}
}
// GetTier extracts tier ID, prioritizing paidTier over currentTier
func
(
r
*
LoadCodeAssistResponse
)
GetTier
()
string
{
if
r
.
PaidTier
!=
nil
&&
r
.
PaidTier
.
ID
!=
""
{
return
r
.
PaidTier
.
ID
}
if
r
.
CurrentTier
!=
nil
{
return
r
.
CurrentTier
.
ID
}
return
""
}
type
AllowedTier
struct
{
type
AllowedTier
struct
{
ID
string
`json:"id"`
ID
string
`json:"id"`
IsDefault
bool
`json:"isDefault,omitempty"`
IsDefault
bool
`json:"isDefault,omitempty"`
...
...
backend/internal/pkg/geminicli/constants.go
View file @
c86d445c
// Package geminicli provides helpers for interacting with Gemini CLI tools.
package
geminicli
package
geminicli
import
"time"
import
"time"
...
@@ -26,6 +27,12 @@ const (
...
@@ -26,6 +27,12 @@ const (
// https://www.googleapis.com/auth/generative-language.retriever (often with cloud-platform).
// https://www.googleapis.com/auth/generative-language.retriever (often with cloud-platform).
DefaultAIStudioScopes
=
"https://www.googleapis.com/auth/cloud-platform https://www.googleapis.com/auth/generative-language.retriever"
DefaultAIStudioScopes
=
"https://www.googleapis.com/auth/cloud-platform https://www.googleapis.com/auth/generative-language.retriever"
// DefaultScopes for Google One (personal Google accounts with Gemini access)
// Only used when a custom OAuth client is configured. When using the built-in Gemini CLI client,
// Google One uses DefaultCodeAssistScopes (same as code_assist) because the built-in client
// cannot request restricted scopes like generative-language.retriever or drive.readonly.
DefaultGoogleOneScopes
=
"https://www.googleapis.com/auth/cloud-platform https://www.googleapis.com/auth/generative-language.retriever https://www.googleapis.com/auth/drive.readonly https://www.googleapis.com/auth/userinfo.email https://www.googleapis.com/auth/userinfo.profile"
// GeminiCLIRedirectURI is the redirect URI used by Gemini CLI for Code Assist OAuth.
// GeminiCLIRedirectURI is the redirect URI used by Gemini CLI for Code Assist OAuth.
GeminiCLIRedirectURI
=
"https://codeassist.google.com/authcode"
GeminiCLIRedirectURI
=
"https://codeassist.google.com/authcode"
...
...
backend/internal/pkg/geminicli/models.go
View file @
c86d445c
...
@@ -11,11 +11,12 @@ type Model struct {
...
@@ -11,11 +11,12 @@ type Model struct {
// DefaultModels is the curated Gemini model list used by the admin UI "test account" flow.
// DefaultModels is the curated Gemini model list used by the admin UI "test account" flow.
var
DefaultModels
=
[]
Model
{
var
DefaultModels
=
[]
Model
{
{
ID
:
"gemini-3-pro-preview"
,
Type
:
"model"
,
DisplayName
:
"Gemini 3 Pro Preview"
,
CreatedAt
:
""
},
{
ID
:
"gemini-2.0-flash"
,
Type
:
"model"
,
DisplayName
:
"Gemini 2.0 Flash"
,
CreatedAt
:
""
},
{
ID
:
"gemini-3-flash-preview"
,
Type
:
"model"
,
DisplayName
:
"Gemini 3 Flash Preview"
,
CreatedAt
:
""
},
{
ID
:
"gemini-2.5-pro"
,
Type
:
"model"
,
DisplayName
:
"Gemini 2.5 Pro"
,
CreatedAt
:
""
},
{
ID
:
"gemini-2.5-pro"
,
Type
:
"model"
,
DisplayName
:
"Gemini 2.5 Pro"
,
CreatedAt
:
""
},
{
ID
:
"gemini-2.5-flash"
,
Type
:
"model"
,
DisplayName
:
"Gemini 2.5 Flash"
,
CreatedAt
:
""
},
{
ID
:
"gemini-2.5-flash"
,
Type
:
"model"
,
DisplayName
:
"Gemini 2.5 Flash"
,
CreatedAt
:
""
},
{
ID
:
"gemini-3-pro-preview"
,
Type
:
"model"
,
DisplayName
:
"Gemini 3 Pro Preview"
,
CreatedAt
:
""
},
{
ID
:
"gemini-3-flash-preview"
,
Type
:
"model"
,
DisplayName
:
"Gemini 3 Flash Preview"
,
CreatedAt
:
""
},
}
}
// DefaultTestModel is the default model to preselect in test flows.
// DefaultTestModel is the default model to preselect in test flows.
const
DefaultTestModel
=
"gemini-
3-pro-preview
"
const
DefaultTestModel
=
"gemini-
2.0-flash
"
backend/internal/pkg/geminicli/oauth.go
View file @
c86d445c
...
@@ -19,13 +19,17 @@ type OAuthConfig struct {
...
@@ -19,13 +19,17 @@ type OAuthConfig struct {
}
}
type
OAuthSession
struct
{
type
OAuthSession
struct
{
State
string
`json:"state"`
State
string
`json:"state"`
CodeVerifier
string
`json:"code_verifier"`
CodeVerifier
string
`json:"code_verifier"`
ProxyURL
string
`json:"proxy_url,omitempty"`
ProxyURL
string
`json:"proxy_url,omitempty"`
RedirectURI
string
`json:"redirect_uri"`
RedirectURI
string
`json:"redirect_uri"`
ProjectID
string
`json:"project_id,omitempty"`
ProjectID
string
`json:"project_id,omitempty"`
OAuthType
string
`json:"oauth_type"`
// "code_assist" 或 "ai_studio"
// TierID is a user-selected fallback tier.
CreatedAt
time
.
Time
`json:"created_at"`
// For oauth types that support auto detection (google_one/code_assist), the server will prefer
// the detected tier and fall back to TierID when detection fails.
TierID
string
`json:"tier_id,omitempty"`
OAuthType
string
`json:"oauth_type"`
// "code_assist" 或 "ai_studio"
CreatedAt
time
.
Time
`json:"created_at"`
}
}
type
SessionStore
struct
{
type
SessionStore
struct
{
...
@@ -172,23 +176,32 @@ func EffectiveOAuthConfig(cfg OAuthConfig, oauthType string) (OAuthConfig, error
...
@@ -172,23 +176,32 @@ func EffectiveOAuthConfig(cfg OAuthConfig, oauthType string) (OAuthConfig, error
if
effective
.
Scopes
==
""
{
if
effective
.
Scopes
==
""
{
// Use different default scopes based on OAuth type
// Use different default scopes based on OAuth type
if
oauthType
==
"ai_studio"
{
switch
oauthType
{
case
"ai_studio"
:
// Built-in client can't request some AI Studio scopes (notably generative-language).
// Built-in client can't request some AI Studio scopes (notably generative-language).
if
isBuiltinClient
{
if
isBuiltinClient
{
effective
.
Scopes
=
DefaultCodeAssistScopes
effective
.
Scopes
=
DefaultCodeAssistScopes
}
else
{
}
else
{
effective
.
Scopes
=
DefaultAIStudioScopes
effective
.
Scopes
=
DefaultAIStudioScopes
}
}
}
else
{
case
"google_one"
:
// Google One uses built-in Gemini CLI client (same as code_assist)
// Built-in client can't request restricted scopes like generative-language.retriever
if
isBuiltinClient
{
effective
.
Scopes
=
DefaultCodeAssistScopes
}
else
{
effective
.
Scopes
=
DefaultGoogleOneScopes
}
default
:
// Default to Code Assist scopes
// Default to Code Assist scopes
effective
.
Scopes
=
DefaultCodeAssistScopes
effective
.
Scopes
=
DefaultCodeAssistScopes
}
}
}
else
if
oauthType
==
"ai_studio"
&&
isBuiltinClient
{
}
else
if
(
oauthType
==
"ai_studio"
||
oauthType
==
"google_one"
)
&&
isBuiltinClient
{
// If user overrides scopes while still using the built-in client, strip restricted scopes.
// If user overrides scopes while still using the built-in client, strip restricted scopes.
parts
:=
strings
.
Fields
(
effective
.
Scopes
)
parts
:=
strings
.
Fields
(
effective
.
Scopes
)
filtered
:=
make
([]
string
,
0
,
len
(
parts
))
filtered
:=
make
([]
string
,
0
,
len
(
parts
))
for
_
,
s
:=
range
parts
{
for
_
,
s
:=
range
parts
{
if
strings
.
Contains
(
s
,
"generative-language"
)
{
if
hasRestrictedScope
(
s
)
{
continue
continue
}
}
filtered
=
append
(
filtered
,
s
)
filtered
=
append
(
filtered
,
s
)
...
@@ -214,6 +227,11 @@ func EffectiveOAuthConfig(cfg OAuthConfig, oauthType string) (OAuthConfig, error
...
@@ -214,6 +227,11 @@ func EffectiveOAuthConfig(cfg OAuthConfig, oauthType string) (OAuthConfig, error
return
effective
,
nil
return
effective
,
nil
}
}
func
hasRestrictedScope
(
scope
string
)
bool
{
return
strings
.
HasPrefix
(
scope
,
"https://www.googleapis.com/auth/generative-language"
)
||
strings
.
HasPrefix
(
scope
,
"https://www.googleapis.com/auth/drive"
)
}
func
BuildAuthorizationURL
(
cfg
OAuthConfig
,
state
,
codeChallenge
,
redirectURI
,
projectID
,
oauthType
string
)
(
string
,
error
)
{
func
BuildAuthorizationURL
(
cfg
OAuthConfig
,
state
,
codeChallenge
,
redirectURI
,
projectID
,
oauthType
string
)
(
string
,
error
)
{
effectiveCfg
,
err
:=
EffectiveOAuthConfig
(
cfg
,
oauthType
)
effectiveCfg
,
err
:=
EffectiveOAuthConfig
(
cfg
,
oauthType
)
if
err
!=
nil
{
if
err
!=
nil
{
...
...
backend/internal/pkg/geminicli/oauth_test.go
0 → 100644
View file @
c86d445c
package
geminicli
import
(
"strings"
"testing"
)
func
TestEffectiveOAuthConfig_GoogleOne
(
t
*
testing
.
T
)
{
tests
:=
[]
struct
{
name
string
input
OAuthConfig
oauthType
string
wantClientID
string
wantScopes
string
wantErr
bool
}{
{
name
:
"Google One with built-in client (empty config)"
,
input
:
OAuthConfig
{},
oauthType
:
"google_one"
,
wantClientID
:
GeminiCLIOAuthClientID
,
wantScopes
:
DefaultCodeAssistScopes
,
wantErr
:
false
,
},
{
name
:
"Google One with custom client"
,
input
:
OAuthConfig
{
ClientID
:
"custom-client-id"
,
ClientSecret
:
"custom-client-secret"
,
},
oauthType
:
"google_one"
,
wantClientID
:
"custom-client-id"
,
wantScopes
:
DefaultGoogleOneScopes
,
wantErr
:
false
,
},
{
name
:
"Google One with built-in client and custom scopes (should filter restricted scopes)"
,
input
:
OAuthConfig
{
Scopes
:
"https://www.googleapis.com/auth/cloud-platform https://www.googleapis.com/auth/generative-language.retriever https://www.googleapis.com/auth/drive.readonly"
,
},
oauthType
:
"google_one"
,
wantClientID
:
GeminiCLIOAuthClientID
,
wantScopes
:
"https://www.googleapis.com/auth/cloud-platform"
,
wantErr
:
false
,
},
{
name
:
"Google One with built-in client and only restricted scopes (should fallback to default)"
,
input
:
OAuthConfig
{
Scopes
:
"https://www.googleapis.com/auth/generative-language.retriever https://www.googleapis.com/auth/drive.readonly"
,
},
oauthType
:
"google_one"
,
wantClientID
:
GeminiCLIOAuthClientID
,
wantScopes
:
DefaultCodeAssistScopes
,
wantErr
:
false
,
},
{
name
:
"Code Assist with built-in client"
,
input
:
OAuthConfig
{},
oauthType
:
"code_assist"
,
wantClientID
:
GeminiCLIOAuthClientID
,
wantScopes
:
DefaultCodeAssistScopes
,
wantErr
:
false
,
},
}
for
_
,
tt
:=
range
tests
{
t
.
Run
(
tt
.
name
,
func
(
t
*
testing
.
T
)
{
got
,
err
:=
EffectiveOAuthConfig
(
tt
.
input
,
tt
.
oauthType
)
if
(
err
!=
nil
)
!=
tt
.
wantErr
{
t
.
Errorf
(
"EffectiveOAuthConfig() error = %v, wantErr %v"
,
err
,
tt
.
wantErr
)
return
}
if
err
!=
nil
{
return
}
if
got
.
ClientID
!=
tt
.
wantClientID
{
t
.
Errorf
(
"EffectiveOAuthConfig() ClientID = %v, want %v"
,
got
.
ClientID
,
tt
.
wantClientID
)
}
if
got
.
Scopes
!=
tt
.
wantScopes
{
t
.
Errorf
(
"EffectiveOAuthConfig() Scopes = %v, want %v"
,
got
.
Scopes
,
tt
.
wantScopes
)
}
})
}
}
func
TestEffectiveOAuthConfig_ScopeFiltering
(
t
*
testing
.
T
)
{
// Test that Google One with built-in client filters out restricted scopes
cfg
,
err
:=
EffectiveOAuthConfig
(
OAuthConfig
{
Scopes
:
"https://www.googleapis.com/auth/cloud-platform https://www.googleapis.com/auth/userinfo.email https://www.googleapis.com/auth/generative-language.retriever https://www.googleapis.com/auth/drive.readonly https://www.googleapis.com/auth/userinfo.profile"
,
},
"google_one"
)
if
err
!=
nil
{
t
.
Fatalf
(
"EffectiveOAuthConfig() error = %v"
,
err
)
}
// Should only contain cloud-platform, userinfo.email, and userinfo.profile
// Should NOT contain generative-language or drive scopes
if
strings
.
Contains
(
cfg
.
Scopes
,
"generative-language"
)
{
t
.
Errorf
(
"Scopes should not contain generative-language when using built-in client, got: %v"
,
cfg
.
Scopes
)
}
if
strings
.
Contains
(
cfg
.
Scopes
,
"drive"
)
{
t
.
Errorf
(
"Scopes should not contain drive when using built-in client, got: %v"
,
cfg
.
Scopes
)
}
if
!
strings
.
Contains
(
cfg
.
Scopes
,
"cloud-platform"
)
{
t
.
Errorf
(
"Scopes should contain cloud-platform, got: %v"
,
cfg
.
Scopes
)
}
if
!
strings
.
Contains
(
cfg
.
Scopes
,
"userinfo.email"
)
{
t
.
Errorf
(
"Scopes should contain userinfo.email, got: %v"
,
cfg
.
Scopes
)
}
if
!
strings
.
Contains
(
cfg
.
Scopes
,
"userinfo.profile"
)
{
t
.
Errorf
(
"Scopes should contain userinfo.profile, got: %v"
,
cfg
.
Scopes
)
}
}
backend/internal/pkg/googleapi/status.go
View file @
c86d445c
// Package googleapi provides helpers for Google-style API responses.
package
googleapi
package
googleapi
import
"net/http"
import
"net/http"
...
...
backend/internal/pkg/oauth/oauth.go
View file @
c86d445c
// Package oauth provides helpers for OAuth flows used by this service.
package
oauth
package
oauth
import
(
import
(
...
...
backend/internal/pkg/openai/constants.go
View file @
c86d445c
// Package openai provides helpers and types for OpenAI API integration.
package
openai
package
openai
import
_
"embed"
import
_
"embed"
...
...
backend/internal/pkg/openai/oauth.go
View file @
c86d445c
...
@@ -327,7 +327,7 @@ func ParseIDToken(idToken string) (*IDTokenClaims, error) {
...
@@ -327,7 +327,7 @@ func ParseIDToken(idToken string) (*IDTokenClaims, error) {
return
&
claims
,
nil
return
&
claims
,
nil
}
}
//
Extract
UserInfo
extrac
ts user information from ID Token claims
// UserInfo
represen
ts user information
extracted
from ID Token claims
.
type
UserInfo
struct
{
type
UserInfo
struct
{
Email
string
Email
string
ChatGPTAccountID
string
ChatGPTAccountID
string
...
...
backend/internal/pkg/pagination/pagination.go
View file @
c86d445c
// Package pagination provides types and helpers for paginated responses.
package
pagination
package
pagination
// PaginationParams 分页参数
// PaginationParams 分页参数
...
...
backend/internal/pkg/response/response.go
View file @
c86d445c
// Package response provides standardized HTTP response helpers.
package
response
package
response
import
(
import
(
...
...
backend/internal/pkg/sysutil/restart.go
View file @
c86d445c
// Package sysutil provides system-level utilities for process management.
package
sysutil
package
sysutil
import
(
import
(
...
...
backend/internal/pkg/usagestats/usage_log_types.go
View file @
c86d445c
// Package usagestats provides types for usage statistics and reporting.
package
usagestats
package
usagestats
import
"time"
import
"time"
...
@@ -10,8 +11,8 @@ type DashboardStats struct {
...
@@ -10,8 +11,8 @@ type DashboardStats struct {
ActiveUsers
int64
`json:"active_users"`
// 今日有请求的用户数
ActiveUsers
int64
`json:"active_users"`
// 今日有请求的用户数
// API Key 统计
// API Key 统计
TotalA
pi
Keys
int64
`json:"total_api_keys"`
TotalA
PI
Keys
int64
`json:"total_api_keys"`
ActiveA
pi
Keys
int64
`json:"active_api_keys"`
// 状态为 active 的 API Key 数
ActiveA
PI
Keys
int64
`json:"active_api_keys"`
// 状态为 active 的 API Key 数
// 账户统计
// 账户统计
TotalAccounts
int64
`json:"total_accounts"`
TotalAccounts
int64
`json:"total_accounts"`
...
@@ -82,10 +83,10 @@ type UserUsageTrendPoint struct {
...
@@ -82,10 +83,10 @@ type UserUsageTrendPoint struct {
ActualCost
float64
`json:"actual_cost"`
// 实际扣除
ActualCost
float64
`json:"actual_cost"`
// 实际扣除
}
}
// A
pi
KeyUsageTrendPoint represents API key usage trend data point
// A
PI
KeyUsageTrendPoint represents API key usage trend data point
type
A
pi
KeyUsageTrendPoint
struct
{
type
A
PI
KeyUsageTrendPoint
struct
{
Date
string
`json:"date"`
Date
string
`json:"date"`
A
pi
KeyID
int64
`json:"api_key_id"`
A
PI
KeyID
int64
`json:"api_key_id"`
KeyName
string
`json:"key_name"`
KeyName
string
`json:"key_name"`
Requests
int64
`json:"requests"`
Requests
int64
`json:"requests"`
Tokens
int64
`json:"tokens"`
Tokens
int64
`json:"tokens"`
...
@@ -94,8 +95,8 @@ type ApiKeyUsageTrendPoint struct {
...
@@ -94,8 +95,8 @@ type ApiKeyUsageTrendPoint struct {
// UserDashboardStats 用户仪表盘统计
// UserDashboardStats 用户仪表盘统计
type
UserDashboardStats
struct
{
type
UserDashboardStats
struct
{
// API Key 统计
// API Key 统计
TotalA
pi
Keys
int64
`json:"total_api_keys"`
TotalA
PI
Keys
int64
`json:"total_api_keys"`
ActiveA
pi
Keys
int64
`json:"active_api_keys"`
ActiveA
PI
Keys
int64
`json:"active_api_keys"`
// 累计 Token 使用统计
// 累计 Token 使用统计
TotalRequests
int64
`json:"total_requests"`
TotalRequests
int64
`json:"total_requests"`
...
@@ -128,7 +129,7 @@ type UserDashboardStats struct {
...
@@ -128,7 +129,7 @@ type UserDashboardStats struct {
// UsageLogFilters represents filters for usage log queries
// UsageLogFilters represents filters for usage log queries
type
UsageLogFilters
struct
{
type
UsageLogFilters
struct
{
UserID
int64
UserID
int64
A
pi
KeyID
int64
A
PI
KeyID
int64
AccountID
int64
AccountID
int64
GroupID
int64
GroupID
int64
Model
string
Model
string
...
@@ -157,9 +158,9 @@ type BatchUserUsageStats struct {
...
@@ -157,9 +158,9 @@ type BatchUserUsageStats struct {
TotalActualCost
float64
`json:"total_actual_cost"`
TotalActualCost
float64
`json:"total_actual_cost"`
}
}
// BatchA
pi
KeyUsageStats represents usage stats for a single API key
// BatchA
PI
KeyUsageStats represents usage stats for a single API key
type
BatchA
pi
KeyUsageStats
struct
{
type
BatchA
PI
KeyUsageStats
struct
{
A
pi
KeyID
int64
`json:"api_key_id"`
A
PI
KeyID
int64
`json:"api_key_id"`
TodayActualCost
float64
`json:"today_actual_cost"`
TodayActualCost
float64
`json:"today_actual_cost"`
TotalActualCost
float64
`json:"total_actual_cost"`
TotalActualCost
float64
`json:"total_actual_cost"`
}
}
...
...
backend/internal/repository/account_repo.go
View file @
c86d445c
...
@@ -43,6 +43,11 @@ type accountRepository struct {
...
@@ -43,6 +43,11 @@ type accountRepository struct {
sql
sqlExecutor
// 原生 SQL 执行接口
sql
sqlExecutor
// 原生 SQL 执行接口
}
}
type
tempUnschedSnapshot
struct
{
until
*
time
.
Time
reason
string
}
// NewAccountRepository 创建账户仓储实例。
// NewAccountRepository 创建账户仓储实例。
// 这是对外暴露的构造函数,返回接口类型以便于依赖注入。
// 这是对外暴露的构造函数,返回接口类型以便于依赖注入。
func
NewAccountRepository
(
client
*
dbent
.
Client
,
sqlDB
*
sql
.
DB
)
service
.
AccountRepository
{
func
NewAccountRepository
(
client
*
dbent
.
Client
,
sqlDB
*
sql
.
DB
)
service
.
AccountRepository
{
...
@@ -165,6 +170,11 @@ func (r *accountRepository) GetByIDs(ctx context.Context, ids []int64) ([]*servi
...
@@ -165,6 +170,11 @@ func (r *accountRepository) GetByIDs(ctx context.Context, ids []int64) ([]*servi
accountIDs
=
append
(
accountIDs
,
acc
.
ID
)
accountIDs
=
append
(
accountIDs
,
acc
.
ID
)
}
}
tempUnschedMap
,
err
:=
r
.
loadTempUnschedStates
(
ctx
,
accountIDs
)
if
err
!=
nil
{
return
nil
,
err
}
groupsByAccount
,
groupIDsByAccount
,
accountGroupsByAccount
,
err
:=
r
.
loadAccountGroups
(
ctx
,
accountIDs
)
groupsByAccount
,
groupIDsByAccount
,
accountGroupsByAccount
,
err
:=
r
.
loadAccountGroups
(
ctx
,
accountIDs
)
if
err
!=
nil
{
if
err
!=
nil
{
return
nil
,
err
return
nil
,
err
...
@@ -191,6 +201,10 @@ func (r *accountRepository) GetByIDs(ctx context.Context, ids []int64) ([]*servi
...
@@ -191,6 +201,10 @@ func (r *accountRepository) GetByIDs(ctx context.Context, ids []int64) ([]*servi
if
ags
,
ok
:=
accountGroupsByAccount
[
entAcc
.
ID
];
ok
{
if
ags
,
ok
:=
accountGroupsByAccount
[
entAcc
.
ID
];
ok
{
out
.
AccountGroups
=
ags
out
.
AccountGroups
=
ags
}
}
if
snap
,
ok
:=
tempUnschedMap
[
entAcc
.
ID
];
ok
{
out
.
TempUnschedulableUntil
=
snap
.
until
out
.
TempUnschedulableReason
=
snap
.
reason
}
outByID
[
entAcc
.
ID
]
=
out
outByID
[
entAcc
.
ID
]
=
out
}
}
...
@@ -550,6 +564,7 @@ func (r *accountRepository) ListSchedulable(ctx context.Context) ([]service.Acco
...
@@ -550,6 +564,7 @@ func (r *accountRepository) ListSchedulable(ctx context.Context) ([]service.Acco
Where
(
Where
(
dbaccount
.
StatusEQ
(
service
.
StatusActive
),
dbaccount
.
StatusEQ
(
service
.
StatusActive
),
dbaccount
.
SchedulableEQ
(
true
),
dbaccount
.
SchedulableEQ
(
true
),
tempUnschedulablePredicate
(),
dbaccount
.
Or
(
dbaccount
.
OverloadUntilIsNil
(),
dbaccount
.
OverloadUntilLTE
(
now
)),
dbaccount
.
Or
(
dbaccount
.
OverloadUntilIsNil
(),
dbaccount
.
OverloadUntilLTE
(
now
)),
dbaccount
.
Or
(
dbaccount
.
RateLimitResetAtIsNil
(),
dbaccount
.
RateLimitResetAtLTE
(
now
)),
dbaccount
.
Or
(
dbaccount
.
RateLimitResetAtIsNil
(),
dbaccount
.
RateLimitResetAtLTE
(
now
)),
)
.
)
.
...
@@ -575,6 +590,7 @@ func (r *accountRepository) ListSchedulableByPlatform(ctx context.Context, platf
...
@@ -575,6 +590,7 @@ func (r *accountRepository) ListSchedulableByPlatform(ctx context.Context, platf
dbaccount
.
PlatformEQ
(
platform
),
dbaccount
.
PlatformEQ
(
platform
),
dbaccount
.
StatusEQ
(
service
.
StatusActive
),
dbaccount
.
StatusEQ
(
service
.
StatusActive
),
dbaccount
.
SchedulableEQ
(
true
),
dbaccount
.
SchedulableEQ
(
true
),
tempUnschedulablePredicate
(),
dbaccount
.
Or
(
dbaccount
.
OverloadUntilIsNil
(),
dbaccount
.
OverloadUntilLTE
(
now
)),
dbaccount
.
Or
(
dbaccount
.
OverloadUntilIsNil
(),
dbaccount
.
OverloadUntilLTE
(
now
)),
dbaccount
.
Or
(
dbaccount
.
RateLimitResetAtIsNil
(),
dbaccount
.
RateLimitResetAtLTE
(
now
)),
dbaccount
.
Or
(
dbaccount
.
RateLimitResetAtIsNil
(),
dbaccount
.
RateLimitResetAtLTE
(
now
)),
)
.
)
.
...
@@ -607,6 +623,7 @@ func (r *accountRepository) ListSchedulableByPlatforms(ctx context.Context, plat
...
@@ -607,6 +623,7 @@ func (r *accountRepository) ListSchedulableByPlatforms(ctx context.Context, plat
dbaccount
.
PlatformIn
(
platforms
...
),
dbaccount
.
PlatformIn
(
platforms
...
),
dbaccount
.
StatusEQ
(
service
.
StatusActive
),
dbaccount
.
StatusEQ
(
service
.
StatusActive
),
dbaccount
.
SchedulableEQ
(
true
),
dbaccount
.
SchedulableEQ
(
true
),
tempUnschedulablePredicate
(),
dbaccount
.
Or
(
dbaccount
.
OverloadUntilIsNil
(),
dbaccount
.
OverloadUntilLTE
(
now
)),
dbaccount
.
Or
(
dbaccount
.
OverloadUntilIsNil
(),
dbaccount
.
OverloadUntilLTE
(
now
)),
dbaccount
.
Or
(
dbaccount
.
RateLimitResetAtIsNil
(),
dbaccount
.
RateLimitResetAtLTE
(
now
)),
dbaccount
.
Or
(
dbaccount
.
RateLimitResetAtIsNil
(),
dbaccount
.
RateLimitResetAtLTE
(
now
)),
)
.
)
.
...
@@ -648,6 +665,31 @@ func (r *accountRepository) SetOverloaded(ctx context.Context, id int64, until t
...
@@ -648,6 +665,31 @@ func (r *accountRepository) SetOverloaded(ctx context.Context, id int64, until t
return
err
return
err
}
}
func
(
r
*
accountRepository
)
SetTempUnschedulable
(
ctx
context
.
Context
,
id
int64
,
until
time
.
Time
,
reason
string
)
error
{
_
,
err
:=
r
.
sql
.
ExecContext
(
ctx
,
`
UPDATE accounts
SET temp_unschedulable_until = $1,
temp_unschedulable_reason = $2,
updated_at = NOW()
WHERE id = $3
AND deleted_at IS NULL
AND (temp_unschedulable_until IS NULL OR temp_unschedulable_until < $1)
`
,
until
,
reason
,
id
)
return
err
}
func
(
r
*
accountRepository
)
ClearTempUnschedulable
(
ctx
context
.
Context
,
id
int64
)
error
{
_
,
err
:=
r
.
sql
.
ExecContext
(
ctx
,
`
UPDATE accounts
SET temp_unschedulable_until = NULL,
temp_unschedulable_reason = NULL,
updated_at = NOW()
WHERE id = $1
AND deleted_at IS NULL
`
,
id
)
return
err
}
func
(
r
*
accountRepository
)
ClearRateLimit
(
ctx
context
.
Context
,
id
int64
)
error
{
func
(
r
*
accountRepository
)
ClearRateLimit
(
ctx
context
.
Context
,
id
int64
)
error
{
_
,
err
:=
r
.
client
.
Account
.
Update
()
.
_
,
err
:=
r
.
client
.
Account
.
Update
()
.
Where
(
dbaccount
.
IDEQ
(
id
))
.
Where
(
dbaccount
.
IDEQ
(
id
))
.
...
@@ -808,6 +850,7 @@ func (r *accountRepository) queryAccountsByGroup(ctx context.Context, groupID in
...
@@ -808,6 +850,7 @@ func (r *accountRepository) queryAccountsByGroup(ctx context.Context, groupID in
now
:=
time
.
Now
()
now
:=
time
.
Now
()
preds
=
append
(
preds
,
preds
=
append
(
preds
,
dbaccount
.
SchedulableEQ
(
true
),
dbaccount
.
SchedulableEQ
(
true
),
tempUnschedulablePredicate
(),
dbaccount
.
Or
(
dbaccount
.
OverloadUntilIsNil
(),
dbaccount
.
OverloadUntilLTE
(
now
)),
dbaccount
.
Or
(
dbaccount
.
OverloadUntilIsNil
(),
dbaccount
.
OverloadUntilLTE
(
now
)),
dbaccount
.
Or
(
dbaccount
.
RateLimitResetAtIsNil
(),
dbaccount
.
RateLimitResetAtLTE
(
now
)),
dbaccount
.
Or
(
dbaccount
.
RateLimitResetAtIsNil
(),
dbaccount
.
RateLimitResetAtLTE
(
now
)),
)
)
...
@@ -869,6 +912,10 @@ func (r *accountRepository) accountsToService(ctx context.Context, accounts []*d
...
@@ -869,6 +912,10 @@ func (r *accountRepository) accountsToService(ctx context.Context, accounts []*d
if
err
!=
nil
{
if
err
!=
nil
{
return
nil
,
err
return
nil
,
err
}
}
tempUnschedMap
,
err
:=
r
.
loadTempUnschedStates
(
ctx
,
accountIDs
)
if
err
!=
nil
{
return
nil
,
err
}
groupsByAccount
,
groupIDsByAccount
,
accountGroupsByAccount
,
err
:=
r
.
loadAccountGroups
(
ctx
,
accountIDs
)
groupsByAccount
,
groupIDsByAccount
,
accountGroupsByAccount
,
err
:=
r
.
loadAccountGroups
(
ctx
,
accountIDs
)
if
err
!=
nil
{
if
err
!=
nil
{
return
nil
,
err
return
nil
,
err
...
@@ -894,12 +941,68 @@ func (r *accountRepository) accountsToService(ctx context.Context, accounts []*d
...
@@ -894,12 +941,68 @@ func (r *accountRepository) accountsToService(ctx context.Context, accounts []*d
if
ags
,
ok
:=
accountGroupsByAccount
[
acc
.
ID
];
ok
{
if
ags
,
ok
:=
accountGroupsByAccount
[
acc
.
ID
];
ok
{
out
.
AccountGroups
=
ags
out
.
AccountGroups
=
ags
}
}
if
snap
,
ok
:=
tempUnschedMap
[
acc
.
ID
];
ok
{
out
.
TempUnschedulableUntil
=
snap
.
until
out
.
TempUnschedulableReason
=
snap
.
reason
}
outAccounts
=
append
(
outAccounts
,
*
out
)
outAccounts
=
append
(
outAccounts
,
*
out
)
}
}
return
outAccounts
,
nil
return
outAccounts
,
nil
}
}
func
tempUnschedulablePredicate
()
dbpredicate
.
Account
{
return
dbpredicate
.
Account
(
func
(
s
*
entsql
.
Selector
)
{
col
:=
s
.
C
(
"temp_unschedulable_until"
)
s
.
Where
(
entsql
.
Or
(
entsql
.
IsNull
(
col
),
entsql
.
LTE
(
col
,
entsql
.
Expr
(
"NOW()"
)),
))
})
}
func
(
r
*
accountRepository
)
loadTempUnschedStates
(
ctx
context
.
Context
,
accountIDs
[]
int64
)
(
map
[
int64
]
tempUnschedSnapshot
,
error
)
{
out
:=
make
(
map
[
int64
]
tempUnschedSnapshot
)
if
len
(
accountIDs
)
==
0
{
return
out
,
nil
}
rows
,
err
:=
r
.
sql
.
QueryContext
(
ctx
,
`
SELECT id, temp_unschedulable_until, temp_unschedulable_reason
FROM accounts
WHERE id = ANY($1)
`
,
pq
.
Array
(
accountIDs
))
if
err
!=
nil
{
return
nil
,
err
}
defer
func
()
{
_
=
rows
.
Close
()
}()
for
rows
.
Next
()
{
var
id
int64
var
until
sql
.
NullTime
var
reason
sql
.
NullString
if
err
:=
rows
.
Scan
(
&
id
,
&
until
,
&
reason
);
err
!=
nil
{
return
nil
,
err
}
var
untilPtr
*
time
.
Time
if
until
.
Valid
{
tmp
:=
until
.
Time
untilPtr
=
&
tmp
}
if
reason
.
Valid
{
out
[
id
]
=
tempUnschedSnapshot
{
until
:
untilPtr
,
reason
:
reason
.
String
}
}
else
{
out
[
id
]
=
tempUnschedSnapshot
{
until
:
untilPtr
,
reason
:
""
}
}
}
if
err
:=
rows
.
Err
();
err
!=
nil
{
return
nil
,
err
}
return
out
,
nil
}
func
(
r
*
accountRepository
)
loadProxies
(
ctx
context
.
Context
,
proxyIDs
[]
int64
)
(
map
[
int64
]
*
service
.
Proxy
,
error
)
{
func
(
r
*
accountRepository
)
loadProxies
(
ctx
context
.
Context
,
proxyIDs
[]
int64
)
(
map
[
int64
]
*
service
.
Proxy
,
error
)
{
proxyMap
:=
make
(
map
[
int64
]
*
service
.
Proxy
)
proxyMap
:=
make
(
map
[
int64
]
*
service
.
Proxy
)
if
len
(
proxyIDs
)
==
0
{
if
len
(
proxyIDs
)
==
0
{
...
...
Prev
1
2
3
4
5
6
7
8
…
10
Next
Write
Preview
Markdown
is supported
0%
Try again
or
attach a new file
.
Attach a file
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Cancel
Please
register
or
sign in
to comment