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
aea48ae1
Commit
aea48ae1
authored
Dec 25, 2025
by
ianshaw
Browse files
feat(config): 新增 Gemini 配置项和 geminicli 核心包
- 添加 Gemini OAuth 配置结构 - 实现 geminicli 包(OAuth、Token、CodeAssist 类型) - 更新配置示例文件
parent
b3463769
Changes
7
Show whitespace changes
Inline
Side-by-side
backend/internal/config/config.go
View file @
aea48ae1
...
@@ -18,6 +18,17 @@ type Config struct {
...
@@ -18,6 +18,17 @@ type Config struct {
Gateway
GatewayConfig
`mapstructure:"gateway"`
Gateway
GatewayConfig
`mapstructure:"gateway"`
TokenRefresh
TokenRefreshConfig
`mapstructure:"token_refresh"`
TokenRefresh
TokenRefreshConfig
`mapstructure:"token_refresh"`
Timezone
string
`mapstructure:"timezone"`
// e.g. "Asia/Shanghai", "UTC"
Timezone
string
`mapstructure:"timezone"`
// e.g. "Asia/Shanghai", "UTC"
Gemini
GeminiConfig
`mapstructure:"gemini"`
}
type
GeminiConfig
struct
{
OAuth
GeminiOAuthConfig
`mapstructure:"oauth"`
}
type
GeminiOAuthConfig
struct
{
ClientID
string
`mapstructure:"client_id"`
ClientSecret
string
`mapstructure:"client_secret"`
Scopes
string
`mapstructure:"scopes"`
}
}
// TokenRefreshConfig OAuth token自动刷新配置
// TokenRefreshConfig OAuth token自动刷新配置
...
@@ -214,6 +225,11 @@ func setDefaults() {
...
@@ -214,6 +225,11 @@ func setDefaults() {
viper
.
SetDefault
(
"token_refresh.refresh_before_expiry_hours"
,
1.5
)
// 提前1.5小时刷新
viper
.
SetDefault
(
"token_refresh.refresh_before_expiry_hours"
,
1.5
)
// 提前1.5小时刷新
viper
.
SetDefault
(
"token_refresh.max_retries"
,
3
)
// 最多重试3次
viper
.
SetDefault
(
"token_refresh.max_retries"
,
3
)
// 最多重试3次
viper
.
SetDefault
(
"token_refresh.retry_backoff_seconds"
,
2
)
// 重试退避基础2秒
viper
.
SetDefault
(
"token_refresh.retry_backoff_seconds"
,
2
)
// 重试退避基础2秒
// Gemini (optional)
viper
.
SetDefault
(
"gemini.oauth.client_id"
,
""
)
viper
.
SetDefault
(
"gemini.oauth.client_secret"
,
""
)
viper
.
SetDefault
(
"gemini.oauth.scopes"
,
""
)
}
}
func
(
c
*
Config
)
Validate
()
error
{
func
(
c
*
Config
)
Validate
()
error
{
...
...
backend/internal/pkg/geminicli/codeassist_types.go
0 → 100644
View file @
aea48ae1
package
geminicli
// LoadCodeAssistRequest matches done-hub's internal Code Assist call.
type
LoadCodeAssistRequest
struct
{
Metadata
LoadCodeAssistMetadata
`json:"metadata"`
}
type
LoadCodeAssistMetadata
struct
{
IDEType
string
`json:"ideType"`
Platform
string
`json:"platform"`
PluginType
string
`json:"pluginType"`
}
type
LoadCodeAssistResponse
struct
{
CurrentTier
string
`json:"currentTier,omitempty"`
CloudAICompanionProject
string
`json:"cloudaicompanionProject,omitempty"`
AllowedTiers
[]
AllowedTier
`json:"allowedTiers,omitempty"`
}
type
AllowedTier
struct
{
ID
string
`json:"id"`
IsDefault
bool
`json:"isDefault,omitempty"`
}
type
OnboardUserRequest
struct
{
TierID
string
`json:"tierId"`
Metadata
LoadCodeAssistMetadata
`json:"metadata"`
}
type
OnboardUserResponse
struct
{
Done
bool
`json:"done"`
Response
*
OnboardUserResultData
`json:"response,omitempty"`
Name
string
`json:"name,omitempty"`
}
type
OnboardUserResultData
struct
{
CloudAICompanionProject
any
`json:"cloudaicompanionProject,omitempty"`
}
backend/internal/pkg/geminicli/constants.go
0 → 100644
View file @
aea48ae1
package
geminicli
import
"time"
const
(
AIStudioBaseURL
=
"https://generativelanguage.googleapis.com"
GeminiCliBaseURL
=
"https://cloudcode-pa.googleapis.com"
AuthorizeURL
=
"https://accounts.google.com/o/oauth2/v2/auth"
TokenURL
=
"https://oauth2.googleapis.com/token"
// DefaultScopes is the minimal scope set for GeminiCli/CodeAssist usage.
// Keep this conservative and expand only when we have a clear requirement.
DefaultScopes
=
"https://www.googleapis.com/auth/cloud-platform"
SessionTTL
=
30
*
time
.
Minute
// GeminiCLIUserAgent mimics Gemini CLI to maximize compatibility with internal endpoints.
GeminiCLIUserAgent
=
"GeminiCLI/0.1.5 (Windows; AMD64)"
)
backend/internal/pkg/geminicli/oauth.go
0 → 100644
View file @
aea48ae1
package
geminicli
import
(
"crypto/rand"
"crypto/sha256"
"encoding/base64"
"encoding/hex"
"fmt"
"net/url"
"strings"
"sync"
"time"
)
type
OAuthConfig
struct
{
ClientID
string
ClientSecret
string
Scopes
string
}
type
OAuthSession
struct
{
State
string
`json:"state"`
CodeVerifier
string
`json:"code_verifier"`
ProxyURL
string
`json:"proxy_url,omitempty"`
RedirectURI
string
`json:"redirect_uri"`
CreatedAt
time
.
Time
`json:"created_at"`
}
type
SessionStore
struct
{
mu
sync
.
RWMutex
sessions
map
[
string
]
*
OAuthSession
stopCh
chan
struct
{}
}
func
NewSessionStore
()
*
SessionStore
{
store
:=
&
SessionStore
{
sessions
:
make
(
map
[
string
]
*
OAuthSession
),
stopCh
:
make
(
chan
struct
{}),
}
go
store
.
cleanup
()
return
store
}
func
(
s
*
SessionStore
)
Set
(
sessionID
string
,
session
*
OAuthSession
)
{
s
.
mu
.
Lock
()
defer
s
.
mu
.
Unlock
()
s
.
sessions
[
sessionID
]
=
session
}
func
(
s
*
SessionStore
)
Get
(
sessionID
string
)
(
*
OAuthSession
,
bool
)
{
s
.
mu
.
RLock
()
defer
s
.
mu
.
RUnlock
()
session
,
ok
:=
s
.
sessions
[
sessionID
]
if
!
ok
{
return
nil
,
false
}
if
time
.
Since
(
session
.
CreatedAt
)
>
SessionTTL
{
return
nil
,
false
}
return
session
,
true
}
func
(
s
*
SessionStore
)
Delete
(
sessionID
string
)
{
s
.
mu
.
Lock
()
defer
s
.
mu
.
Unlock
()
delete
(
s
.
sessions
,
sessionID
)
}
func
(
s
*
SessionStore
)
Stop
()
{
select
{
case
<-
s
.
stopCh
:
return
default
:
close
(
s
.
stopCh
)
}
}
func
(
s
*
SessionStore
)
cleanup
()
{
ticker
:=
time
.
NewTicker
(
5
*
time
.
Minute
)
defer
ticker
.
Stop
()
for
{
select
{
case
<-
s
.
stopCh
:
return
case
<-
ticker
.
C
:
s
.
mu
.
Lock
()
for
id
,
session
:=
range
s
.
sessions
{
if
time
.
Since
(
session
.
CreatedAt
)
>
SessionTTL
{
delete
(
s
.
sessions
,
id
)
}
}
s
.
mu
.
Unlock
()
}
}
}
func
GenerateRandomBytes
(
n
int
)
([]
byte
,
error
)
{
b
:=
make
([]
byte
,
n
)
_
,
err
:=
rand
.
Read
(
b
)
if
err
!=
nil
{
return
nil
,
err
}
return
b
,
nil
}
func
GenerateState
()
(
string
,
error
)
{
bytes
,
err
:=
GenerateRandomBytes
(
32
)
if
err
!=
nil
{
return
""
,
err
}
return
base64URLEncode
(
bytes
),
nil
}
func
GenerateSessionID
()
(
string
,
error
)
{
bytes
,
err
:=
GenerateRandomBytes
(
16
)
if
err
!=
nil
{
return
""
,
err
}
return
hex
.
EncodeToString
(
bytes
),
nil
}
// GenerateCodeVerifier returns an RFC 7636 compatible code verifier (43+ chars).
func
GenerateCodeVerifier
()
(
string
,
error
)
{
bytes
,
err
:=
GenerateRandomBytes
(
32
)
if
err
!=
nil
{
return
""
,
err
}
return
base64URLEncode
(
bytes
),
nil
}
func
GenerateCodeChallenge
(
verifier
string
)
string
{
hash
:=
sha256
.
Sum256
([]
byte
(
verifier
))
return
base64URLEncode
(
hash
[
:
])
}
func
base64URLEncode
(
data
[]
byte
)
string
{
return
strings
.
TrimRight
(
base64
.
URLEncoding
.
EncodeToString
(
data
),
"="
)
}
func
BuildAuthorizationURL
(
cfg
OAuthConfig
,
state
,
codeChallenge
,
redirectURI
string
)
(
string
,
error
)
{
if
strings
.
TrimSpace
(
cfg
.
ClientID
)
==
""
{
return
""
,
fmt
.
Errorf
(
"gemini oauth client_id is empty"
)
}
redirectURI
=
strings
.
TrimSpace
(
redirectURI
)
if
redirectURI
==
""
{
return
""
,
fmt
.
Errorf
(
"redirect_uri is required"
)
}
scopes
:=
strings
.
TrimSpace
(
cfg
.
Scopes
)
if
scopes
==
""
{
scopes
=
DefaultScopes
}
params
:=
url
.
Values
{}
params
.
Set
(
"response_type"
,
"code"
)
params
.
Set
(
"client_id"
,
cfg
.
ClientID
)
params
.
Set
(
"redirect_uri"
,
redirectURI
)
params
.
Set
(
"scope"
,
scopes
)
params
.
Set
(
"state"
,
state
)
params
.
Set
(
"code_challenge"
,
codeChallenge
)
params
.
Set
(
"code_challenge_method"
,
"S256"
)
params
.
Set
(
"access_type"
,
"offline"
)
params
.
Set
(
"prompt"
,
"consent"
)
params
.
Set
(
"include_granted_scopes"
,
"true"
)
return
fmt
.
Sprintf
(
"%s?%s"
,
AuthorizeURL
,
params
.
Encode
()),
nil
}
backend/internal/pkg/geminicli/sanitize.go
0 → 100644
View file @
aea48ae1
package
geminicli
import
"strings"
const
maxLogBodyLen
=
2048
func
SanitizeBodyForLogs
(
body
string
)
string
{
body
=
truncateBase64InMessage
(
body
)
if
len
(
body
)
>
maxLogBodyLen
{
body
=
body
[
:
maxLogBodyLen
]
+
"...[truncated]"
}
return
body
}
func
truncateBase64InMessage
(
message
string
)
string
{
const
maxBase64Length
=
50
result
:=
message
offset
:=
0
for
{
idx
:=
strings
.
Index
(
result
[
offset
:
],
";base64,"
)
if
idx
==
-
1
{
break
}
actualIdx
:=
offset
+
idx
start
:=
actualIdx
+
len
(
";base64,"
)
end
:=
start
for
end
<
len
(
result
)
&&
isBase64Char
(
result
[
end
])
{
end
++
}
if
end
-
start
>
maxBase64Length
{
result
=
result
[
:
start
+
maxBase64Length
]
+
"...[truncated]"
+
result
[
end
:
]
offset
=
start
+
maxBase64Length
+
len
(
"...[truncated]"
)
continue
}
offset
=
end
}
return
result
}
func
isBase64Char
(
c
byte
)
bool
{
return
(
c
>=
'A'
&&
c
<=
'Z'
)
||
(
c
>=
'a'
&&
c
<=
'z'
)
||
(
c
>=
'0'
&&
c
<=
'9'
)
||
c
==
'+'
||
c
==
'/'
||
c
==
'='
}
backend/internal/pkg/geminicli/token_types.go
0 → 100644
View file @
aea48ae1
package
geminicli
type
TokenResponse
struct
{
AccessToken
string
`json:"access_token"`
RefreshToken
string
`json:"refresh_token,omitempty"`
TokenType
string
`json:"token_type"`
ExpiresIn
int64
`json:"expires_in"`
Scope
string
`json:"scope,omitempty"`
}
deploy/config.example.yaml
View file @
aea48ae1
...
@@ -87,3 +87,14 @@ pricing:
...
@@ -87,3 +87,14 @@ pricing:
update_interval_hours
:
24
update_interval_hours
:
24
# Hash check interval in minutes
# Hash check interval in minutes
hash_check_interval_minutes
:
10
hash_check_interval_minutes
:
10
# =============================================================================
# Gemini (Optional)
# =============================================================================
gemini
:
oauth
:
# Google OAuth Client ID / Secret (for GeminiCli / Code Assist internal API)
client_id
:
"
"
client_secret
:
"
"
# Optional scopes (space-separated). Leave empty to use default cloud-platform scope.
scopes
:
"
"
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