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
195e227c
Commit
195e227c
authored
Jan 06, 2026
by
song
Browse files
merge: 合并 upstream/main 并保留本地图片计费功能
parents
6fa704d6
752882a0
Changes
187
Hide whitespace changes
Inline
Side-by-side
.github/workflows/release.yml
View file @
195e227c
...
...
@@ -57,19 +57,24 @@ jobs:
-
name
:
Checkout
uses
:
actions/checkout@v4
-
name
:
Setup pnpm
uses
:
pnpm/action-setup@v4
with
:
version
:
9
-
name
:
Setup Node.js
uses
:
actions/setup-node@v4
with
:
node-version
:
'
20'
cache
:
'
npm'
cache-dependency-path
:
frontend/p
ackage
-lock.
json
cache
:
'
p
npm'
cache-dependency-path
:
frontend/p
npm
-lock.
yaml
-
name
:
Install dependencies
run
:
npm
c
i
run
:
p
npm i
nstall --frozen-lockfile
working-directory
:
frontend
-
name
:
Build frontend
run
:
npm run build
run
:
p
npm run build
working-directory
:
frontend
-
name
:
Upload frontend artifact
...
...
.gitignore
View file @
195e227c
...
...
@@ -33,6 +33,7 @@ frontend/dist/
*.local
*.tsbuildinfo
vite.config.d.ts
vite.config.js.timestamp-*
# 日志
npm-debug.log*
...
...
@@ -121,3 +122,4 @@ AGENTS.md
backend/cmd/server/server
deploy/docker-compose.override.yml
.gocache/
vite.config.js
Dockerfile
View file @
195e227c
...
...
@@ -19,13 +19,16 @@ FROM ${NODE_IMAGE} AS frontend-builder
WORKDIR
/app/frontend
# Install pnpm
RUN
corepack
enable
&&
corepack prepare pnpm@latest
--activate
# Install dependencies first (better caching)
COPY
frontend/package
*
.json ./
RUN
npm
c
i
COPY
frontend/package.json
frontend/pnpm-lock.yaml
./
RUN
p
npm
i
nstall
--frozen-lockfile
# Copy frontend source and build
COPY
frontend/ ./
RUN
npm run build
RUN
p
npm run build
# -----------------------------------------------------------------------------
# Stage 2: Backend Builder
...
...
Makefile
View file @
195e227c
.PHONY
:
build build-backend build-frontend
.PHONY
:
build build-backend build-frontend
test test-backend test-frontend
# 一键编译前后端
build
:
build-backend build-frontend
...
...
@@ -10,3 +10,13 @@ build-backend:
# 编译前端(需要已安装依赖)
build-frontend
:
@
npm
--prefix
frontend run build
# 运行测试(后端 + 前端)
test
:
test-backend test-frontend
test-backend
:
@
$(MAKE)
-C
backend
test
test-frontend
:
@
npm
--prefix
frontend run lint:check
@
npm
--prefix
frontend run typecheck
README.md
View file @
195e227c
...
...
@@ -218,20 +218,23 @@ Build and run from source code for development or customization.
git clone https://github.com/Wei-Shaw/sub2api.git
cd
sub2api
# 2. Build frontend
# 2. Install pnpm (if not already installed)
npm
install
-g
pnpm
# 3. Build frontend
cd
frontend
npm
install
npm run build
p
npm
install
p
npm run build
# Output will be in ../backend/internal/web/dist/
#
3
. Build backend with embedded frontend
#
4
. Build backend with embedded frontend
cd
../backend
go build
-tags
embed
-o
sub2api ./cmd/server
#
4
. Create configuration file
#
5
. Create configuration file
cp
../deploy/config.example.yaml ./config.yaml
#
5
. Edit configuration
#
6
. Edit configuration
nano config.yaml
```
...
...
@@ -268,6 +271,24 @@ default:
rate_multiplier
:
1.0
```
Additional security-related options are available in
`config.yaml`
:
-
`cors.allowed_origins`
for CORS allowlist
-
`security.url_allowlist`
for upstream/pricing/CRS host allowlists
-
`security.url_allowlist.enabled`
to disable URL validation (use with caution)
-
`security.url_allowlist.allow_insecure_http`
to allow http URLs when validation is disabled
-
`security.response_headers.enabled`
to enable configurable response header filtering (disabled uses default allowlist)
-
`security.csp`
to control Content-Security-Policy headers
-
`billing.circuit_breaker`
to fail closed on billing errors
-
`server.trusted_proxies`
to enable X-Forwarded-For parsing
-
`turnstile.required`
to require Turnstile in release mode
If you disable URL validation or response header filtering, harden your network layer:
-
Enforce an egress allowlist for upstream domains/IPs
-
Block private/loopback/link-local ranges
-
Enforce TLS-only outbound traffic
-
Strip sensitive upstream response headers at the proxy
```
bash
# 6. Run the application
./sub2api
...
...
@@ -282,7 +303,7 @@ go run ./cmd/server
# Frontend (with hot reload)
cd
frontend
npm run dev
p
npm run dev
```
#### Code Generation
...
...
README_CN.md
View file @
195e227c
...
...
@@ -218,20 +218,23 @@ docker-compose logs -f
git clone https://github.com/Wei-Shaw/sub2api.git
cd
sub2api
# 2. 编译前端
# 2. 安装 pnpm(如果还没有安装)
npm
install
-g
pnpm
# 3. 编译前端
cd
frontend
npm
install
npm run build
p
npm
install
p
npm run build
# 构建产物输出到 ../backend/internal/web/dist/
#
3
. 编译后端(嵌入前端)
#
4
. 编译后端(嵌入前端)
cd
../backend
go build
-tags
embed
-o
sub2api ./cmd/server
#
4
. 创建配置文件
#
5
. 创建配置文件
cp
../deploy/config.example.yaml ./config.yaml
#
5
. 编辑配置
#
6
. 编辑配置
nano config.yaml
```
...
...
@@ -268,6 +271,24 @@ default:
rate_multiplier
:
1.0
```
`config.yaml`
还支持以下安全相关配置:
-
`cors.allowed_origins`
配置 CORS 白名单
-
`security.url_allowlist`
配置上游/价格数据/CRS 主机白名单
-
`security.url_allowlist.enabled`
可关闭 URL 校验(慎用)
-
`security.url_allowlist.allow_insecure_http`
关闭校验时允许 http URL
-
`security.response_headers.enabled`
可启用可配置响应头过滤(关闭时使用默认白名单)
-
`security.csp`
配置 Content-Security-Policy
-
`billing.circuit_breaker`
计费异常时 fail-closed
-
`server.trusted_proxies`
启用可信代理解析 X-Forwarded-For
-
`turnstile.required`
在 release 模式强制启用 Turnstile
如关闭 URL 校验或响应头过滤,请加强网络层防护:
-
出站访问白名单限制上游域名/IP
-
阻断私网/回环/链路本地地址
-
强制仅允许 TLS 出站
-
在反向代理层移除敏感响应头
```
bash
# 6. 运行应用
./sub2api
...
...
@@ -282,7 +303,7 @@ go run ./cmd/server
# 前端(支持热重载)
cd
frontend
npm run dev
p
npm run dev
```
#### 代码生成
...
...
backend/Makefile
View file @
195e227c
.PHONY
:
build test-unit test-integration test-e2e
.PHONY
:
build
test
test-unit test-integration test-e2e
build
:
go build
-o
bin/server ./cmd/server
test
:
go
test
./...
golangci-lint run ./...
test-unit
:
go
test
-tags
=
unit ./...
...
...
backend/cmd/server/main.go
View file @
195e227c
...
...
@@ -86,7 +86,8 @@ func main() {
func
runSetupServer
()
{
r
:=
gin
.
New
()
r
.
Use
(
middleware
.
Recovery
())
r
.
Use
(
middleware
.
CORS
())
r
.
Use
(
middleware
.
CORS
(
config
.
CORSConfig
{}))
r
.
Use
(
middleware
.
SecurityHeaders
(
config
.
CSPConfig
{
Enabled
:
true
,
Policy
:
config
.
DefaultCSPPolicy
}))
// Register setup routes
setup
.
RegisterRoutes
(
r
)
...
...
backend/cmd/server/wire_gen.go
View file @
195e227c
...
...
@@ -76,7 +76,7 @@ func initializeApplication(buildInfo handler.BuildInfo) (*Application, error) {
dashboardHandler
:=
admin
.
NewDashboardHandler
(
dashboardService
)
accountRepository
:=
repository
.
NewAccountRepository
(
client
,
db
)
proxyRepository
:=
repository
.
NewProxyRepository
(
client
,
db
)
proxyExitInfoProber
:=
repository
.
NewProxyExitInfoProber
()
proxyExitInfoProber
:=
repository
.
NewProxyExitInfoProber
(
configConfig
)
adminService
:=
service
.
NewAdminService
(
userRepository
,
groupRepository
,
accountRepository
,
proxyRepository
,
apiKeyRepository
,
redeemCodeRepository
,
billingCacheService
,
proxyExitInfoProber
)
adminUserHandler
:=
admin
.
NewUserHandler
(
adminService
)
groupHandler
:=
admin
.
NewGroupHandler
(
adminService
)
...
...
@@ -101,10 +101,10 @@ func initializeApplication(buildInfo handler.BuildInfo) (*Application, error) {
antigravityTokenProvider
:=
service
.
NewAntigravityTokenProvider
(
accountRepository
,
geminiTokenCache
,
antigravityOAuthService
)
httpUpstream
:=
repository
.
NewHTTPUpstream
(
configConfig
)
antigravityGatewayService
:=
service
.
NewAntigravityGatewayService
(
accountRepository
,
gatewayCache
,
antigravityTokenProvider
,
rateLimitService
,
httpUpstream
,
settingService
)
accountTestService
:=
service
.
NewAccountTestService
(
accountRepository
,
geminiTokenProvider
,
antigravityGatewayService
,
httpUpstream
)
accountTestService
:=
service
.
NewAccountTestService
(
accountRepository
,
geminiTokenProvider
,
antigravityGatewayService
,
httpUpstream
,
configConfig
)
concurrencyCache
:=
repository
.
ProvideConcurrencyCache
(
redisClient
,
configConfig
)
concurrencyService
:=
service
.
Provide
ConcurrencyService
(
concurrencyCache
,
accountRepository
,
configConfig
)
crsSyncService
:=
service
.
NewCRSSyncService
(
accountRepository
,
proxyRepository
,
oAuthService
,
openAIOAuthService
,
geminiOAuthService
)
concurrencyService
:=
service
.
New
ConcurrencyService
(
concurrencyCache
)
crsSyncService
:=
service
.
NewCRSSyncService
(
accountRepository
,
proxyRepository
,
oAuthService
,
openAIOAuthService
,
geminiOAuthService
,
configConfig
)
accountHandler
:=
admin
.
NewAccountHandler
(
adminService
,
oAuthService
,
openAIOAuthService
,
geminiOAuthService
,
antigravityOAuthService
,
rateLimitService
,
accountUsageService
,
accountTestService
,
concurrencyService
,
crsSyncService
)
oAuthHandler
:=
admin
.
NewOAuthHandler
(
oAuthService
)
openAIOAuthHandler
:=
admin
.
NewOpenAIOAuthHandler
(
openAIOAuthService
,
adminService
)
...
...
@@ -125,7 +125,7 @@ func initializeApplication(buildInfo handler.BuildInfo) (*Application, error) {
userAttributeService
:=
service
.
NewUserAttributeService
(
userAttributeDefinitionRepository
,
userAttributeValueRepository
)
userAttributeHandler
:=
admin
.
NewUserAttributeHandler
(
userAttributeService
)
adminHandlers
:=
handler
.
ProvideAdminHandlers
(
dashboardHandler
,
adminUserHandler
,
groupHandler
,
accountHandler
,
oAuthHandler
,
openAIOAuthHandler
,
geminiOAuthHandler
,
antigravityOAuthHandler
,
proxyHandler
,
adminRedeemHandler
,
settingHandler
,
systemHandler
,
adminSubscriptionHandler
,
adminUsageHandler
,
userAttributeHandler
)
pricingRemoteClient
:=
repository
.
NewPricingRemoteClient
()
pricingRemoteClient
:=
repository
.
NewPricingRemoteClient
(
configConfig
)
pricingService
,
err
:=
service
.
ProvidePricingService
(
configConfig
,
pricingRemoteClient
)
if
err
!=
nil
{
return
nil
,
err
...
...
@@ -136,10 +136,10 @@ func initializeApplication(buildInfo handler.BuildInfo) (*Application, error) {
timingWheelService
:=
service
.
ProvideTimingWheelService
()
deferredService
:=
service
.
ProvideDeferredService
(
accountRepository
,
timingWheelService
)
gatewayService
:=
service
.
NewGatewayService
(
accountRepository
,
groupRepository
,
usageLogRepository
,
userRepository
,
userSubscriptionRepository
,
gatewayCache
,
configConfig
,
concurrencyService
,
billingService
,
rateLimitService
,
billingCacheService
,
identityService
,
httpUpstream
,
deferredService
)
geminiMessagesCompatService
:=
service
.
NewGeminiMessagesCompatService
(
accountRepository
,
groupRepository
,
gatewayCache
,
geminiTokenProvider
,
rateLimitService
,
httpUpstream
,
antigravityGatewayService
)
gatewayHandler
:=
handler
.
NewGatewayHandler
(
gatewayService
,
geminiMessagesCompatService
,
antigravityGatewayService
,
userService
,
concurrencyService
,
billingCacheService
)
geminiMessagesCompatService
:=
service
.
NewGeminiMessagesCompatService
(
accountRepository
,
groupRepository
,
gatewayCache
,
geminiTokenProvider
,
rateLimitService
,
httpUpstream
,
antigravityGatewayService
,
configConfig
)
gatewayHandler
:=
handler
.
NewGatewayHandler
(
gatewayService
,
geminiMessagesCompatService
,
antigravityGatewayService
,
userService
,
concurrencyService
,
billingCacheService
,
configConfig
)
openAIGatewayService
:=
service
.
NewOpenAIGatewayService
(
accountRepository
,
usageLogRepository
,
userRepository
,
userSubscriptionRepository
,
gatewayCache
,
configConfig
,
concurrencyService
,
billingService
,
rateLimitService
,
billingCacheService
,
httpUpstream
,
deferredService
)
openAIGatewayHandler
:=
handler
.
NewOpenAIGatewayHandler
(
openAIGatewayService
,
concurrencyService
,
billingCacheService
)
openAIGatewayHandler
:=
handler
.
NewOpenAIGatewayHandler
(
openAIGatewayService
,
concurrencyService
,
billingCacheService
,
configConfig
)
handlerSettingHandler
:=
handler
.
ProvideSettingHandler
(
settingService
,
buildInfo
)
handlers
:=
handler
.
ProvideHandlers
(
authHandler
,
userHandler
,
apiKeyHandler
,
usageHandler
,
redeemHandler
,
subscriptionHandler
,
adminHandlers
,
gatewayHandler
,
openAIGatewayHandler
,
handlerSettingHandler
)
jwtAuthMiddleware
:=
middleware
.
NewJWTAuthMiddleware
(
authService
,
userService
)
...
...
backend/ent/account.go
View file @
195e227c
...
...
@@ -27,6 +27,8 @@ type Account struct {
DeletedAt
*
time
.
Time
`json:"deleted_at,omitempty"`
// Name holds the value of the "name" field.
Name
string
`json:"name,omitempty"`
// Notes holds the value of the "notes" field.
Notes
*
string
`json:"notes,omitempty"`
// Platform holds the value of the "platform" field.
Platform
string
`json:"platform,omitempty"`
// Type holds the value of the "type" field.
...
...
@@ -131,7 +133,7 @@ func (*Account) scanValues(columns []string) ([]any, error) {
values
[
i
]
=
new
(
sql
.
NullBool
)
case
account
.
FieldID
,
account
.
FieldProxyID
,
account
.
FieldConcurrency
,
account
.
FieldPriority
:
values
[
i
]
=
new
(
sql
.
NullInt64
)
case
account
.
FieldName
,
account
.
FieldPlatform
,
account
.
FieldType
,
account
.
FieldStatus
,
account
.
FieldErrorMessage
,
account
.
FieldSessionWindowStatus
:
case
account
.
FieldName
,
account
.
FieldNotes
,
account
.
FieldPlatform
,
account
.
FieldType
,
account
.
FieldStatus
,
account
.
FieldErrorMessage
,
account
.
FieldSessionWindowStatus
:
values
[
i
]
=
new
(
sql
.
NullString
)
case
account
.
FieldCreatedAt
,
account
.
FieldUpdatedAt
,
account
.
FieldDeletedAt
,
account
.
FieldLastUsedAt
,
account
.
FieldRateLimitedAt
,
account
.
FieldRateLimitResetAt
,
account
.
FieldOverloadUntil
,
account
.
FieldSessionWindowStart
,
account
.
FieldSessionWindowEnd
:
values
[
i
]
=
new
(
sql
.
NullTime
)
...
...
@@ -181,6 +183,13 @@ func (_m *Account) assignValues(columns []string, values []any) error {
}
else
if
value
.
Valid
{
_m
.
Name
=
value
.
String
}
case
account
.
FieldNotes
:
if
value
,
ok
:=
values
[
i
]
.
(
*
sql
.
NullString
);
!
ok
{
return
fmt
.
Errorf
(
"unexpected type %T for field notes"
,
values
[
i
])
}
else
if
value
.
Valid
{
_m
.
Notes
=
new
(
string
)
*
_m
.
Notes
=
value
.
String
}
case
account
.
FieldPlatform
:
if
value
,
ok
:=
values
[
i
]
.
(
*
sql
.
NullString
);
!
ok
{
return
fmt
.
Errorf
(
"unexpected type %T for field platform"
,
values
[
i
])
...
...
@@ -366,6 +375,11 @@ func (_m *Account) String() string {
builder
.
WriteString
(
"name="
)
builder
.
WriteString
(
_m
.
Name
)
builder
.
WriteString
(
", "
)
if
v
:=
_m
.
Notes
;
v
!=
nil
{
builder
.
WriteString
(
"notes="
)
builder
.
WriteString
(
*
v
)
}
builder
.
WriteString
(
", "
)
builder
.
WriteString
(
"platform="
)
builder
.
WriteString
(
_m
.
Platform
)
builder
.
WriteString
(
", "
)
...
...
backend/ent/account/account.go
View file @
195e227c
...
...
@@ -23,6 +23,8 @@ const (
FieldDeletedAt
=
"deleted_at"
// FieldName holds the string denoting the name field in the database.
FieldName
=
"name"
// FieldNotes holds the string denoting the notes field in the database.
FieldNotes
=
"notes"
// FieldPlatform holds the string denoting the platform field in the database.
FieldPlatform
=
"platform"
// FieldType holds the string denoting the type field in the database.
...
...
@@ -102,6 +104,7 @@ var Columns = []string{
FieldUpdatedAt
,
FieldDeletedAt
,
FieldName
,
FieldNotes
,
FieldPlatform
,
FieldType
,
FieldCredentials
,
...
...
@@ -203,6 +206,11 @@ func ByName(opts ...sql.OrderTermOption) OrderOption {
return
sql
.
OrderByField
(
FieldName
,
opts
...
)
.
ToFunc
()
}
// ByNotes orders the results by the notes field.
func
ByNotes
(
opts
...
sql
.
OrderTermOption
)
OrderOption
{
return
sql
.
OrderByField
(
FieldNotes
,
opts
...
)
.
ToFunc
()
}
// ByPlatform orders the results by the platform field.
func
ByPlatform
(
opts
...
sql
.
OrderTermOption
)
OrderOption
{
return
sql
.
OrderByField
(
FieldPlatform
,
opts
...
)
.
ToFunc
()
...
...
backend/ent/account/where.go
View file @
195e227c
...
...
@@ -75,6 +75,11 @@ func Name(v string) predicate.Account {
return
predicate
.
Account
(
sql
.
FieldEQ
(
FieldName
,
v
))
}
// Notes applies equality check predicate on the "notes" field. It's identical to NotesEQ.
func
Notes
(
v
string
)
predicate
.
Account
{
return
predicate
.
Account
(
sql
.
FieldEQ
(
FieldNotes
,
v
))
}
// Platform applies equality check predicate on the "platform" field. It's identical to PlatformEQ.
func
Platform
(
v
string
)
predicate
.
Account
{
return
predicate
.
Account
(
sql
.
FieldEQ
(
FieldPlatform
,
v
))
...
...
@@ -345,6 +350,81 @@ func NameContainsFold(v string) predicate.Account {
return
predicate
.
Account
(
sql
.
FieldContainsFold
(
FieldName
,
v
))
}
// NotesEQ applies the EQ predicate on the "notes" field.
func
NotesEQ
(
v
string
)
predicate
.
Account
{
return
predicate
.
Account
(
sql
.
FieldEQ
(
FieldNotes
,
v
))
}
// NotesNEQ applies the NEQ predicate on the "notes" field.
func
NotesNEQ
(
v
string
)
predicate
.
Account
{
return
predicate
.
Account
(
sql
.
FieldNEQ
(
FieldNotes
,
v
))
}
// NotesIn applies the In predicate on the "notes" field.
func
NotesIn
(
vs
...
string
)
predicate
.
Account
{
return
predicate
.
Account
(
sql
.
FieldIn
(
FieldNotes
,
vs
...
))
}
// NotesNotIn applies the NotIn predicate on the "notes" field.
func
NotesNotIn
(
vs
...
string
)
predicate
.
Account
{
return
predicate
.
Account
(
sql
.
FieldNotIn
(
FieldNotes
,
vs
...
))
}
// NotesGT applies the GT predicate on the "notes" field.
func
NotesGT
(
v
string
)
predicate
.
Account
{
return
predicate
.
Account
(
sql
.
FieldGT
(
FieldNotes
,
v
))
}
// NotesGTE applies the GTE predicate on the "notes" field.
func
NotesGTE
(
v
string
)
predicate
.
Account
{
return
predicate
.
Account
(
sql
.
FieldGTE
(
FieldNotes
,
v
))
}
// NotesLT applies the LT predicate on the "notes" field.
func
NotesLT
(
v
string
)
predicate
.
Account
{
return
predicate
.
Account
(
sql
.
FieldLT
(
FieldNotes
,
v
))
}
// NotesLTE applies the LTE predicate on the "notes" field.
func
NotesLTE
(
v
string
)
predicate
.
Account
{
return
predicate
.
Account
(
sql
.
FieldLTE
(
FieldNotes
,
v
))
}
// NotesContains applies the Contains predicate on the "notes" field.
func
NotesContains
(
v
string
)
predicate
.
Account
{
return
predicate
.
Account
(
sql
.
FieldContains
(
FieldNotes
,
v
))
}
// NotesHasPrefix applies the HasPrefix predicate on the "notes" field.
func
NotesHasPrefix
(
v
string
)
predicate
.
Account
{
return
predicate
.
Account
(
sql
.
FieldHasPrefix
(
FieldNotes
,
v
))
}
// NotesHasSuffix applies the HasSuffix predicate on the "notes" field.
func
NotesHasSuffix
(
v
string
)
predicate
.
Account
{
return
predicate
.
Account
(
sql
.
FieldHasSuffix
(
FieldNotes
,
v
))
}
// NotesIsNil applies the IsNil predicate on the "notes" field.
func
NotesIsNil
()
predicate
.
Account
{
return
predicate
.
Account
(
sql
.
FieldIsNull
(
FieldNotes
))
}
// NotesNotNil applies the NotNil predicate on the "notes" field.
func
NotesNotNil
()
predicate
.
Account
{
return
predicate
.
Account
(
sql
.
FieldNotNull
(
FieldNotes
))
}
// NotesEqualFold applies the EqualFold predicate on the "notes" field.
func
NotesEqualFold
(
v
string
)
predicate
.
Account
{
return
predicate
.
Account
(
sql
.
FieldEqualFold
(
FieldNotes
,
v
))
}
// NotesContainsFold applies the ContainsFold predicate on the "notes" field.
func
NotesContainsFold
(
v
string
)
predicate
.
Account
{
return
predicate
.
Account
(
sql
.
FieldContainsFold
(
FieldNotes
,
v
))
}
// PlatformEQ applies the EQ predicate on the "platform" field.
func
PlatformEQ
(
v
string
)
predicate
.
Account
{
return
predicate
.
Account
(
sql
.
FieldEQ
(
FieldPlatform
,
v
))
...
...
backend/ent/account_create.go
View file @
195e227c
...
...
@@ -73,6 +73,20 @@ func (_c *AccountCreate) SetName(v string) *AccountCreate {
return
_c
}
// SetNotes sets the "notes" field.
func
(
_c
*
AccountCreate
)
SetNotes
(
v
string
)
*
AccountCreate
{
_c
.
mutation
.
SetNotes
(
v
)
return
_c
}
// SetNillableNotes sets the "notes" field if the given value is not nil.
func
(
_c
*
AccountCreate
)
SetNillableNotes
(
v
*
string
)
*
AccountCreate
{
if
v
!=
nil
{
_c
.
SetNotes
(
*
v
)
}
return
_c
}
// SetPlatform sets the "platform" field.
func
(
_c
*
AccountCreate
)
SetPlatform
(
v
string
)
*
AccountCreate
{
_c
.
mutation
.
SetPlatform
(
v
)
...
...
@@ -501,6 +515,10 @@ func (_c *AccountCreate) createSpec() (*Account, *sqlgraph.CreateSpec) {
_spec
.
SetField
(
account
.
FieldName
,
field
.
TypeString
,
value
)
_node
.
Name
=
value
}
if
value
,
ok
:=
_c
.
mutation
.
Notes
();
ok
{
_spec
.
SetField
(
account
.
FieldNotes
,
field
.
TypeString
,
value
)
_node
.
Notes
=
&
value
}
if
value
,
ok
:=
_c
.
mutation
.
Platform
();
ok
{
_spec
.
SetField
(
account
.
FieldPlatform
,
field
.
TypeString
,
value
)
_node
.
Platform
=
value
...
...
@@ -712,6 +730,24 @@ func (u *AccountUpsert) UpdateName() *AccountUpsert {
return
u
}
// SetNotes sets the "notes" field.
func
(
u
*
AccountUpsert
)
SetNotes
(
v
string
)
*
AccountUpsert
{
u
.
Set
(
account
.
FieldNotes
,
v
)
return
u
}
// UpdateNotes sets the "notes" field to the value that was provided on create.
func
(
u
*
AccountUpsert
)
UpdateNotes
()
*
AccountUpsert
{
u
.
SetExcluded
(
account
.
FieldNotes
)
return
u
}
// ClearNotes clears the value of the "notes" field.
func
(
u
*
AccountUpsert
)
ClearNotes
()
*
AccountUpsert
{
u
.
SetNull
(
account
.
FieldNotes
)
return
u
}
// SetPlatform sets the "platform" field.
func
(
u
*
AccountUpsert
)
SetPlatform
(
v
string
)
*
AccountUpsert
{
u
.
Set
(
account
.
FieldPlatform
,
v
)
...
...
@@ -1076,6 +1112,27 @@ func (u *AccountUpsertOne) UpdateName() *AccountUpsertOne {
})
}
// SetNotes sets the "notes" field.
func
(
u
*
AccountUpsertOne
)
SetNotes
(
v
string
)
*
AccountUpsertOne
{
return
u
.
Update
(
func
(
s
*
AccountUpsert
)
{
s
.
SetNotes
(
v
)
})
}
// UpdateNotes sets the "notes" field to the value that was provided on create.
func
(
u
*
AccountUpsertOne
)
UpdateNotes
()
*
AccountUpsertOne
{
return
u
.
Update
(
func
(
s
*
AccountUpsert
)
{
s
.
UpdateNotes
()
})
}
// ClearNotes clears the value of the "notes" field.
func
(
u
*
AccountUpsertOne
)
ClearNotes
()
*
AccountUpsertOne
{
return
u
.
Update
(
func
(
s
*
AccountUpsert
)
{
s
.
ClearNotes
()
})
}
// SetPlatform sets the "platform" field.
func
(
u
*
AccountUpsertOne
)
SetPlatform
(
v
string
)
*
AccountUpsertOne
{
return
u
.
Update
(
func
(
s
*
AccountUpsert
)
{
...
...
@@ -1651,6 +1708,27 @@ func (u *AccountUpsertBulk) UpdateName() *AccountUpsertBulk {
})
}
// SetNotes sets the "notes" field.
func
(
u
*
AccountUpsertBulk
)
SetNotes
(
v
string
)
*
AccountUpsertBulk
{
return
u
.
Update
(
func
(
s
*
AccountUpsert
)
{
s
.
SetNotes
(
v
)
})
}
// UpdateNotes sets the "notes" field to the value that was provided on create.
func
(
u
*
AccountUpsertBulk
)
UpdateNotes
()
*
AccountUpsertBulk
{
return
u
.
Update
(
func
(
s
*
AccountUpsert
)
{
s
.
UpdateNotes
()
})
}
// ClearNotes clears the value of the "notes" field.
func
(
u
*
AccountUpsertBulk
)
ClearNotes
()
*
AccountUpsertBulk
{
return
u
.
Update
(
func
(
s
*
AccountUpsert
)
{
s
.
ClearNotes
()
})
}
// SetPlatform sets the "platform" field.
func
(
u
*
AccountUpsertBulk
)
SetPlatform
(
v
string
)
*
AccountUpsertBulk
{
return
u
.
Update
(
func
(
s
*
AccountUpsert
)
{
...
...
backend/ent/account_update.go
View file @
195e227c
...
...
@@ -71,6 +71,26 @@ func (_u *AccountUpdate) SetNillableName(v *string) *AccountUpdate {
return
_u
}
// SetNotes sets the "notes" field.
func
(
_u
*
AccountUpdate
)
SetNotes
(
v
string
)
*
AccountUpdate
{
_u
.
mutation
.
SetNotes
(
v
)
return
_u
}
// SetNillableNotes sets the "notes" field if the given value is not nil.
func
(
_u
*
AccountUpdate
)
SetNillableNotes
(
v
*
string
)
*
AccountUpdate
{
if
v
!=
nil
{
_u
.
SetNotes
(
*
v
)
}
return
_u
}
// ClearNotes clears the value of the "notes" field.
func
(
_u
*
AccountUpdate
)
ClearNotes
()
*
AccountUpdate
{
_u
.
mutation
.
ClearNotes
()
return
_u
}
// SetPlatform sets the "platform" field.
func
(
_u
*
AccountUpdate
)
SetPlatform
(
v
string
)
*
AccountUpdate
{
_u
.
mutation
.
SetPlatform
(
v
)
...
...
@@ -545,6 +565,12 @@ func (_u *AccountUpdate) sqlSave(ctx context.Context) (_node int, err error) {
if
value
,
ok
:=
_u
.
mutation
.
Name
();
ok
{
_spec
.
SetField
(
account
.
FieldName
,
field
.
TypeString
,
value
)
}
if
value
,
ok
:=
_u
.
mutation
.
Notes
();
ok
{
_spec
.
SetField
(
account
.
FieldNotes
,
field
.
TypeString
,
value
)
}
if
_u
.
mutation
.
NotesCleared
()
{
_spec
.
ClearField
(
account
.
FieldNotes
,
field
.
TypeString
)
}
if
value
,
ok
:=
_u
.
mutation
.
Platform
();
ok
{
_spec
.
SetField
(
account
.
FieldPlatform
,
field
.
TypeString
,
value
)
}
...
...
@@ -814,6 +840,26 @@ func (_u *AccountUpdateOne) SetNillableName(v *string) *AccountUpdateOne {
return
_u
}
// SetNotes sets the "notes" field.
func
(
_u
*
AccountUpdateOne
)
SetNotes
(
v
string
)
*
AccountUpdateOne
{
_u
.
mutation
.
SetNotes
(
v
)
return
_u
}
// SetNillableNotes sets the "notes" field if the given value is not nil.
func
(
_u
*
AccountUpdateOne
)
SetNillableNotes
(
v
*
string
)
*
AccountUpdateOne
{
if
v
!=
nil
{
_u
.
SetNotes
(
*
v
)
}
return
_u
}
// ClearNotes clears the value of the "notes" field.
func
(
_u
*
AccountUpdateOne
)
ClearNotes
()
*
AccountUpdateOne
{
_u
.
mutation
.
ClearNotes
()
return
_u
}
// SetPlatform sets the "platform" field.
func
(
_u
*
AccountUpdateOne
)
SetPlatform
(
v
string
)
*
AccountUpdateOne
{
_u
.
mutation
.
SetPlatform
(
v
)
...
...
@@ -1318,6 +1364,12 @@ func (_u *AccountUpdateOne) sqlSave(ctx context.Context) (_node *Account, err er
if
value
,
ok
:=
_u
.
mutation
.
Name
();
ok
{
_spec
.
SetField
(
account
.
FieldName
,
field
.
TypeString
,
value
)
}
if
value
,
ok
:=
_u
.
mutation
.
Notes
();
ok
{
_spec
.
SetField
(
account
.
FieldNotes
,
field
.
TypeString
,
value
)
}
if
_u
.
mutation
.
NotesCleared
()
{
_spec
.
ClearField
(
account
.
FieldNotes
,
field
.
TypeString
)
}
if
value
,
ok
:=
_u
.
mutation
.
Platform
();
ok
{
_spec
.
SetField
(
account
.
FieldPlatform
,
field
.
TypeString
,
value
)
}
...
...
backend/ent/migrate/schema.go
View file @
195e227c
...
...
@@ -70,6 +70,7 @@ var (
{
Name
:
"updated_at"
,
Type
:
field
.
TypeTime
,
SchemaType
:
map
[
string
]
string
{
"postgres"
:
"timestamptz"
}},
{
Name
:
"deleted_at"
,
Type
:
field
.
TypeTime
,
Nullable
:
true
,
SchemaType
:
map
[
string
]
string
{
"postgres"
:
"timestamptz"
}},
{
Name
:
"name"
,
Type
:
field
.
TypeString
,
Size
:
100
},
{
Name
:
"notes"
,
Type
:
field
.
TypeString
,
Nullable
:
true
,
SchemaType
:
map
[
string
]
string
{
"postgres"
:
"text"
}},
{
Name
:
"platform"
,
Type
:
field
.
TypeString
,
Size
:
50
},
{
Name
:
"type"
,
Type
:
field
.
TypeString
,
Size
:
20
},
{
Name
:
"credentials"
,
Type
:
field
.
TypeJSON
,
SchemaType
:
map
[
string
]
string
{
"postgres"
:
"jsonb"
}},
...
...
@@ -96,7 +97,7 @@ var (
ForeignKeys
:
[]
*
schema
.
ForeignKey
{
{
Symbol
:
"accounts_proxies_proxy"
,
Columns
:
[]
*
schema
.
Column
{
AccountsColumns
[
2
1
]},
Columns
:
[]
*
schema
.
Column
{
AccountsColumns
[
2
2
]},
RefColumns
:
[]
*
schema
.
Column
{
ProxiesColumns
[
0
]},
OnDelete
:
schema
.
SetNull
,
},
...
...
@@ -105,52 +106,52 @@ var (
{
Name
:
"account_platform"
,
Unique
:
false
,
Columns
:
[]
*
schema
.
Column
{
AccountsColumns
[
5
]},
Columns
:
[]
*
schema
.
Column
{
AccountsColumns
[
6
]},
},
{
Name
:
"account_type"
,
Unique
:
false
,
Columns
:
[]
*
schema
.
Column
{
AccountsColumns
[
6
]},
Columns
:
[]
*
schema
.
Column
{
AccountsColumns
[
7
]},
},
{
Name
:
"account_status"
,
Unique
:
false
,
Columns
:
[]
*
schema
.
Column
{
AccountsColumns
[
1
1
]},
Columns
:
[]
*
schema
.
Column
{
AccountsColumns
[
1
2
]},
},
{
Name
:
"account_proxy_id"
,
Unique
:
false
,
Columns
:
[]
*
schema
.
Column
{
AccountsColumns
[
2
1
]},
Columns
:
[]
*
schema
.
Column
{
AccountsColumns
[
2
2
]},
},
{
Name
:
"account_priority"
,
Unique
:
false
,
Columns
:
[]
*
schema
.
Column
{
AccountsColumns
[
1
0
]},
Columns
:
[]
*
schema
.
Column
{
AccountsColumns
[
1
1
]},
},
{
Name
:
"account_last_used_at"
,
Unique
:
false
,
Columns
:
[]
*
schema
.
Column
{
AccountsColumns
[
1
3
]},
Columns
:
[]
*
schema
.
Column
{
AccountsColumns
[
1
4
]},
},
{
Name
:
"account_schedulable"
,
Unique
:
false
,
Columns
:
[]
*
schema
.
Column
{
AccountsColumns
[
1
4
]},
Columns
:
[]
*
schema
.
Column
{
AccountsColumns
[
1
5
]},
},
{
Name
:
"account_rate_limited_at"
,
Unique
:
false
,
Columns
:
[]
*
schema
.
Column
{
AccountsColumns
[
1
5
]},
Columns
:
[]
*
schema
.
Column
{
AccountsColumns
[
1
6
]},
},
{
Name
:
"account_rate_limit_reset_at"
,
Unique
:
false
,
Columns
:
[]
*
schema
.
Column
{
AccountsColumns
[
1
6
]},
Columns
:
[]
*
schema
.
Column
{
AccountsColumns
[
1
7
]},
},
{
Name
:
"account_overload_until"
,
Unique
:
false
,
Columns
:
[]
*
schema
.
Column
{
AccountsColumns
[
1
7
]},
Columns
:
[]
*
schema
.
Column
{
AccountsColumns
[
1
8
]},
},
{
Name
:
"account_deleted_at"
,
...
...
backend/ent/mutation.go
View file @
195e227c
...
...
@@ -994,6 +994,7 @@ type AccountMutation struct {
updated_at
*
time
.
Time
deleted_at
*
time
.
Time
name
*
string
notes
*
string
platform
*
string
_type
*
string
credentials
*
map
[
string
]
interface
{}
...
...
@@ -1281,6 +1282,55 @@ func (m *AccountMutation) ResetName() {
m
.
name
=
nil
}
// SetNotes sets the "notes" field.
func
(
m
*
AccountMutation
)
SetNotes
(
s
string
)
{
m
.
notes
=
&
s
}
// Notes returns the value of the "notes" field in the mutation.
func
(
m
*
AccountMutation
)
Notes
()
(
r
string
,
exists
bool
)
{
v
:=
m
.
notes
if
v
==
nil
{
return
}
return
*
v
,
true
}
// OldNotes returns the old "notes" field's value of the Account entity.
// If the Account object wasn't provided to the builder, the object is fetched from the database.
// An error is returned if the mutation operation is not UpdateOne, or the database query fails.
func
(
m
*
AccountMutation
)
OldNotes
(
ctx
context
.
Context
)
(
v
*
string
,
err
error
)
{
if
!
m
.
op
.
Is
(
OpUpdateOne
)
{
return
v
,
errors
.
New
(
"OldNotes is only allowed on UpdateOne operations"
)
}
if
m
.
id
==
nil
||
m
.
oldValue
==
nil
{
return
v
,
errors
.
New
(
"OldNotes requires an ID field in the mutation"
)
}
oldValue
,
err
:=
m
.
oldValue
(
ctx
)
if
err
!=
nil
{
return
v
,
fmt
.
Errorf
(
"querying old value for OldNotes: %w"
,
err
)
}
return
oldValue
.
Notes
,
nil
}
// ClearNotes clears the value of the "notes" field.
func
(
m
*
AccountMutation
)
ClearNotes
()
{
m
.
notes
=
nil
m
.
clearedFields
[
account
.
FieldNotes
]
=
struct
{}{}
}
// NotesCleared returns if the "notes" field was cleared in this mutation.
func
(
m
*
AccountMutation
)
NotesCleared
()
bool
{
_
,
ok
:=
m
.
clearedFields
[
account
.
FieldNotes
]
return
ok
}
// ResetNotes resets all changes to the "notes" field.
func
(
m
*
AccountMutation
)
ResetNotes
()
{
m
.
notes
=
nil
delete
(
m
.
clearedFields
,
account
.
FieldNotes
)
}
// SetPlatform sets the "platform" field.
func
(
m
*
AccountMutation
)
SetPlatform
(
s
string
)
{
m
.
platform
=
&
s
...
...
@@ -2219,7 +2269,7 @@ func (m *AccountMutation) Type() string {
// order to get all numeric fields that were incremented/decremented, call
// AddedFields().
func
(
m
*
AccountMutation
)
Fields
()
[]
string
{
fields
:=
make
([]
string
,
0
,
2
1
)
fields
:=
make
([]
string
,
0
,
2
2
)
if
m
.
created_at
!=
nil
{
fields
=
append
(
fields
,
account
.
FieldCreatedAt
)
}
...
...
@@ -2232,6 +2282,9 @@ func (m *AccountMutation) Fields() []string {
if
m
.
name
!=
nil
{
fields
=
append
(
fields
,
account
.
FieldName
)
}
if
m
.
notes
!=
nil
{
fields
=
append
(
fields
,
account
.
FieldNotes
)
}
if
m
.
platform
!=
nil
{
fields
=
append
(
fields
,
account
.
FieldPlatform
)
}
...
...
@@ -2299,6 +2352,8 @@ func (m *AccountMutation) Field(name string) (ent.Value, bool) {
return
m
.
DeletedAt
()
case
account
.
FieldName
:
return
m
.
Name
()
case
account
.
FieldNotes
:
return
m
.
Notes
()
case
account
.
FieldPlatform
:
return
m
.
Platform
()
case
account
.
FieldType
:
...
...
@@ -2350,6 +2405,8 @@ func (m *AccountMutation) OldField(ctx context.Context, name string) (ent.Value,
return
m
.
OldDeletedAt
(
ctx
)
case
account
.
FieldName
:
return
m
.
OldName
(
ctx
)
case
account
.
FieldNotes
:
return
m
.
OldNotes
(
ctx
)
case
account
.
FieldPlatform
:
return
m
.
OldPlatform
(
ctx
)
case
account
.
FieldType
:
...
...
@@ -2421,6 +2478,13 @@ func (m *AccountMutation) SetField(name string, value ent.Value) error {
}
m
.
SetName
(
v
)
return
nil
case
account
.
FieldNotes
:
v
,
ok
:=
value
.
(
string
)
if
!
ok
{
return
fmt
.
Errorf
(
"unexpected type %T for field %s"
,
value
,
name
)
}
m
.
SetNotes
(
v
)
return
nil
case
account
.
FieldPlatform
:
v
,
ok
:=
value
.
(
string
)
if
!
ok
{
...
...
@@ -2600,6 +2664,9 @@ func (m *AccountMutation) ClearedFields() []string {
if
m
.
FieldCleared
(
account
.
FieldDeletedAt
)
{
fields
=
append
(
fields
,
account
.
FieldDeletedAt
)
}
if
m
.
FieldCleared
(
account
.
FieldNotes
)
{
fields
=
append
(
fields
,
account
.
FieldNotes
)
}
if
m
.
FieldCleared
(
account
.
FieldProxyID
)
{
fields
=
append
(
fields
,
account
.
FieldProxyID
)
}
...
...
@@ -2644,6 +2711,9 @@ func (m *AccountMutation) ClearField(name string) error {
case
account
.
FieldDeletedAt
:
m
.
ClearDeletedAt
()
return
nil
case
account
.
FieldNotes
:
m
.
ClearNotes
()
return
nil
case
account
.
FieldProxyID
:
m
.
ClearProxyID
()
return
nil
...
...
@@ -2691,6 +2761,9 @@ func (m *AccountMutation) ResetField(name string) error {
case
account
.
FieldName
:
m
.
ResetName
()
return
nil
case
account
.
FieldNotes
:
m
.
ResetNotes
()
return
nil
case
account
.
FieldPlatform
:
m
.
ResetPlatform
()
return
nil
...
...
backend/ent/runtime/runtime.go
View file @
195e227c
...
...
@@ -124,7 +124,7 @@ func init() {
}
}()
// accountDescPlatform is the schema descriptor for platform field.
accountDescPlatform
:=
accountFields
[
1
]
.
Descriptor
()
accountDescPlatform
:=
accountFields
[
2
]
.
Descriptor
()
// account.PlatformValidator is a validator for the "platform" field. It is called by the builders before save.
account
.
PlatformValidator
=
func
()
func
(
string
)
error
{
validators
:=
accountDescPlatform
.
Validators
...
...
@@ -142,7 +142,7 @@ func init() {
}
}()
// accountDescType is the schema descriptor for type field.
accountDescType
:=
accountFields
[
2
]
.
Descriptor
()
accountDescType
:=
accountFields
[
3
]
.
Descriptor
()
// account.TypeValidator is a validator for the "type" field. It is called by the builders before save.
account
.
TypeValidator
=
func
()
func
(
string
)
error
{
validators
:=
accountDescType
.
Validators
...
...
@@ -160,33 +160,33 @@ func init() {
}
}()
// accountDescCredentials is the schema descriptor for credentials field.
accountDescCredentials
:=
accountFields
[
3
]
.
Descriptor
()
accountDescCredentials
:=
accountFields
[
4
]
.
Descriptor
()
// account.DefaultCredentials holds the default value on creation for the credentials field.
account
.
DefaultCredentials
=
accountDescCredentials
.
Default
.
(
func
()
map
[
string
]
interface
{})
// accountDescExtra is the schema descriptor for extra field.
accountDescExtra
:=
accountFields
[
4
]
.
Descriptor
()
accountDescExtra
:=
accountFields
[
5
]
.
Descriptor
()
// account.DefaultExtra holds the default value on creation for the extra field.
account
.
DefaultExtra
=
accountDescExtra
.
Default
.
(
func
()
map
[
string
]
interface
{})
// accountDescConcurrency is the schema descriptor for concurrency field.
accountDescConcurrency
:=
accountFields
[
6
]
.
Descriptor
()
accountDescConcurrency
:=
accountFields
[
7
]
.
Descriptor
()
// account.DefaultConcurrency holds the default value on creation for the concurrency field.
account
.
DefaultConcurrency
=
accountDescConcurrency
.
Default
.
(
int
)
// accountDescPriority is the schema descriptor for priority field.
accountDescPriority
:=
accountFields
[
7
]
.
Descriptor
()
accountDescPriority
:=
accountFields
[
8
]
.
Descriptor
()
// account.DefaultPriority holds the default value on creation for the priority field.
account
.
DefaultPriority
=
accountDescPriority
.
Default
.
(
int
)
// accountDescStatus is the schema descriptor for status field.
accountDescStatus
:=
accountFields
[
8
]
.
Descriptor
()
accountDescStatus
:=
accountFields
[
9
]
.
Descriptor
()
// account.DefaultStatus holds the default value on creation for the status field.
account
.
DefaultStatus
=
accountDescStatus
.
Default
.
(
string
)
// account.StatusValidator is a validator for the "status" field. It is called by the builders before save.
account
.
StatusValidator
=
accountDescStatus
.
Validators
[
0
]
.
(
func
(
string
)
error
)
// accountDescSchedulable is the schema descriptor for schedulable field.
accountDescSchedulable
:=
accountFields
[
1
1
]
.
Descriptor
()
accountDescSchedulable
:=
accountFields
[
1
2
]
.
Descriptor
()
// account.DefaultSchedulable holds the default value on creation for the schedulable field.
account
.
DefaultSchedulable
=
accountDescSchedulable
.
Default
.
(
bool
)
// accountDescSessionWindowStatus is the schema descriptor for session_window_status field.
accountDescSessionWindowStatus
:=
accountFields
[
1
7
]
.
Descriptor
()
accountDescSessionWindowStatus
:=
accountFields
[
1
8
]
.
Descriptor
()
// account.SessionWindowStatusValidator is a validator for the "session_window_status" field. It is called by the builders before save.
account
.
SessionWindowStatusValidator
=
accountDescSessionWindowStatus
.
Validators
[
0
]
.
(
func
(
string
)
error
)
accountgroupFields
:=
schema
.
AccountGroup
{}
.
Fields
()
...
...
backend/ent/schema/account.go
View file @
195e227c
...
...
@@ -54,6 +54,11 @@ func (Account) Fields() []ent.Field {
field
.
String
(
"name"
)
.
MaxLen
(
100
)
.
NotEmpty
(),
// notes: 管理员备注(可为空)
field
.
String
(
"notes"
)
.
Optional
()
.
Nillable
()
.
SchemaType
(
map
[
string
]
string
{
dialect
.
Postgres
:
"text"
}),
// platform: 所属平台,如 "claude", "gemini", "openai" 等
field
.
String
(
"platform"
)
.
...
...
backend/internal/config/config.go
View file @
195e227c
...
...
@@ -2,7 +2,11 @@
package
config
import
(
"crypto/rand"
"encoding/hex"
"fmt"
"log"
"os"
"strings"
"time"
...
...
@@ -14,6 +18,8 @@ const (
RunModeSimple
=
"simple"
)
const
DefaultCSPPolicy
=
"default-src 'self'; script-src 'self' https://challenges.cloudflare.com; style-src 'self' 'unsafe-inline' https://fonts.googleapis.com; img-src 'self' data: https:; font-src 'self' data: https://fonts.gstatic.com; connect-src 'self' https:; frame-src https://challenges.cloudflare.com; frame-ancestors 'none'; base-uri 'self'; form-action 'self'"
// 连接池隔离策略常量
// 用于控制上游 HTTP 连接池的隔离粒度,影响连接复用和资源消耗
const
(
...
...
@@ -30,6 +36,10 @@ const (
type
Config
struct
{
Server
ServerConfig
`mapstructure:"server"`
CORS
CORSConfig
`mapstructure:"cors"`
Security
SecurityConfig
`mapstructure:"security"`
Billing
BillingConfig
`mapstructure:"billing"`
Turnstile
TurnstileConfig
`mapstructure:"turnstile"`
Database
DatabaseConfig
`mapstructure:"database"`
Redis
RedisConfig
`mapstructure:"redis"`
JWT
JWTConfig
`mapstructure:"jwt"`
...
...
@@ -37,6 +47,7 @@ type Config struct {
RateLimit
RateLimitConfig
`mapstructure:"rate_limit"`
Pricing
PricingConfig
`mapstructure:"pricing"`
Gateway
GatewayConfig
`mapstructure:"gateway"`
Concurrency
ConcurrencyConfig
`mapstructure:"concurrency"`
TokenRefresh
TokenRefreshConfig
`mapstructure:"token_refresh"`
RunMode
string
`mapstructure:"run_mode" yaml:"run_mode"`
Timezone
string
`mapstructure:"timezone"`
// e.g. "Asia/Shanghai", "UTC"
...
...
@@ -95,11 +106,65 @@ type PricingConfig struct {
}
type
ServerConfig
struct
{
Host
string
`mapstructure:"host"`
Port
int
`mapstructure:"port"`
Mode
string
`mapstructure:"mode"`
// debug/release
ReadHeaderTimeout
int
`mapstructure:"read_header_timeout"`
// 读取请求头超时(秒)
IdleTimeout
int
`mapstructure:"idle_timeout"`
// 空闲连接超时(秒)
Host
string
`mapstructure:"host"`
Port
int
`mapstructure:"port"`
Mode
string
`mapstructure:"mode"`
// debug/release
ReadHeaderTimeout
int
`mapstructure:"read_header_timeout"`
// 读取请求头超时(秒)
IdleTimeout
int
`mapstructure:"idle_timeout"`
// 空闲连接超时(秒)
TrustedProxies
[]
string
`mapstructure:"trusted_proxies"`
// 可信代理列表(CIDR/IP)
}
type
CORSConfig
struct
{
AllowedOrigins
[]
string
`mapstructure:"allowed_origins"`
AllowCredentials
bool
`mapstructure:"allow_credentials"`
}
type
SecurityConfig
struct
{
URLAllowlist
URLAllowlistConfig
`mapstructure:"url_allowlist"`
ResponseHeaders
ResponseHeaderConfig
`mapstructure:"response_headers"`
CSP
CSPConfig
`mapstructure:"csp"`
ProxyProbe
ProxyProbeConfig
`mapstructure:"proxy_probe"`
}
type
URLAllowlistConfig
struct
{
Enabled
bool
`mapstructure:"enabled"`
UpstreamHosts
[]
string
`mapstructure:"upstream_hosts"`
PricingHosts
[]
string
`mapstructure:"pricing_hosts"`
CRSHosts
[]
string
`mapstructure:"crs_hosts"`
AllowPrivateHosts
bool
`mapstructure:"allow_private_hosts"`
// 关闭 URL 白名单校验时,是否允许 http URL(默认只允许 https)
AllowInsecureHTTP
bool
`mapstructure:"allow_insecure_http"`
}
type
ResponseHeaderConfig
struct
{
Enabled
bool
`mapstructure:"enabled"`
AdditionalAllowed
[]
string
`mapstructure:"additional_allowed"`
ForceRemove
[]
string
`mapstructure:"force_remove"`
}
type
CSPConfig
struct
{
Enabled
bool
`mapstructure:"enabled"`
Policy
string
`mapstructure:"policy"`
}
type
ProxyProbeConfig
struct
{
InsecureSkipVerify
bool
`mapstructure:"insecure_skip_verify"`
}
type
BillingConfig
struct
{
CircuitBreaker
CircuitBreakerConfig
`mapstructure:"circuit_breaker"`
}
type
CircuitBreakerConfig
struct
{
Enabled
bool
`mapstructure:"enabled"`
FailureThreshold
int
`mapstructure:"failure_threshold"`
ResetTimeoutSeconds
int
`mapstructure:"reset_timeout_seconds"`
HalfOpenRequests
int
`mapstructure:"half_open_requests"`
}
type
ConcurrencyConfig
struct
{
// PingInterval: 并发等待期间的 SSE ping 间隔(秒)
PingInterval
int
`mapstructure:"ping_interval"`
}
// GatewayConfig API网关相关配置
...
...
@@ -134,6 +199,13 @@ type GatewayConfig struct {
// 应大于最长 LLM 请求时间,防止请求完成前槽位过期
ConcurrencySlotTTLMinutes
int
`mapstructure:"concurrency_slot_ttl_minutes"`
// StreamDataIntervalTimeout: 流数据间隔超时(秒),0表示禁用
StreamDataIntervalTimeout
int
`mapstructure:"stream_data_interval_timeout"`
// StreamKeepaliveInterval: 流式 keepalive 间隔(秒),0表示禁用
StreamKeepaliveInterval
int
`mapstructure:"stream_keepalive_interval"`
// MaxLineSize: 上游 SSE 单行最大字节数(0使用默认值)
MaxLineSize
int
`mapstructure:"max_line_size"`
// 是否记录上游错误响应体摘要(避免输出请求内容)
LogUpstreamErrorBody
bool
`mapstructure:"log_upstream_error_body"`
// 上游错误响应体记录最大字节数(超过会截断)
...
...
@@ -237,6 +309,10 @@ type JWTConfig struct {
ExpireHour
int
`mapstructure:"expire_hour"`
}
type
TurnstileConfig
struct
{
Required
bool
`mapstructure:"required"`
}
type
DefaultConfig
struct
{
AdminEmail
string
`mapstructure:"admin_email"`
AdminPassword
string
`mapstructure:"admin_password"`
...
...
@@ -263,8 +339,19 @@ func NormalizeRunMode(value string) string {
func
Load
()
(
*
Config
,
error
)
{
viper
.
SetConfigName
(
"config"
)
viper
.
SetConfigType
(
"yaml"
)
// Add config paths in priority order
// 1. DATA_DIR environment variable (highest priority)
if
dataDir
:=
os
.
Getenv
(
"DATA_DIR"
);
dataDir
!=
""
{
viper
.
AddConfigPath
(
dataDir
)
}
// 2. Docker data directory
viper
.
AddConfigPath
(
"/app/data"
)
// 3. Current directory
viper
.
AddConfigPath
(
"."
)
// 4. Config subdirectory
viper
.
AddConfigPath
(
"./config"
)
// 5. System config directory
viper
.
AddConfigPath
(
"/etc/sub2api"
)
// 环境变量支持
...
...
@@ -287,11 +374,46 @@ func Load() (*Config, error) {
}
cfg
.
RunMode
=
NormalizeRunMode
(
cfg
.
RunMode
)
cfg
.
Server
.
Mode
=
strings
.
ToLower
(
strings
.
TrimSpace
(
cfg
.
Server
.
Mode
))
if
cfg
.
Server
.
Mode
==
""
{
cfg
.
Server
.
Mode
=
"debug"
}
cfg
.
JWT
.
Secret
=
strings
.
TrimSpace
(
cfg
.
JWT
.
Secret
)
cfg
.
CORS
.
AllowedOrigins
=
normalizeStringSlice
(
cfg
.
CORS
.
AllowedOrigins
)
cfg
.
Security
.
ResponseHeaders
.
AdditionalAllowed
=
normalizeStringSlice
(
cfg
.
Security
.
ResponseHeaders
.
AdditionalAllowed
)
cfg
.
Security
.
ResponseHeaders
.
ForceRemove
=
normalizeStringSlice
(
cfg
.
Security
.
ResponseHeaders
.
ForceRemove
)
cfg
.
Security
.
CSP
.
Policy
=
strings
.
TrimSpace
(
cfg
.
Security
.
CSP
.
Policy
)
if
cfg
.
JWT
.
Secret
==
""
{
secret
,
err
:=
generateJWTSecret
(
64
)
if
err
!=
nil
{
return
nil
,
fmt
.
Errorf
(
"generate jwt secret error: %w"
,
err
)
}
cfg
.
JWT
.
Secret
=
secret
log
.
Println
(
"Warning: JWT secret auto-generated. Consider setting a fixed secret for production."
)
}
if
err
:=
cfg
.
Validate
();
err
!=
nil
{
return
nil
,
fmt
.
Errorf
(
"validate config error: %w"
,
err
)
}
if
!
cfg
.
Security
.
URLAllowlist
.
Enabled
{
log
.
Println
(
"Warning: security.url_allowlist.enabled=false; allowlist/SSRF checks disabled (minimal format validation only)."
)
}
if
!
cfg
.
Security
.
ResponseHeaders
.
Enabled
{
log
.
Println
(
"Warning: security.response_headers.enabled=false; configurable header filtering disabled (default allowlist only)."
)
}
if
cfg
.
JWT
.
Secret
!=
""
&&
isWeakJWTSecret
(
cfg
.
JWT
.
Secret
)
{
log
.
Println
(
"Warning: JWT secret appears weak; use a 32+ character random secret in production."
)
}
if
len
(
cfg
.
Security
.
ResponseHeaders
.
AdditionalAllowed
)
>
0
||
len
(
cfg
.
Security
.
ResponseHeaders
.
ForceRemove
)
>
0
{
log
.
Printf
(
"AUDIT: response header policy configured additional_allowed=%v force_remove=%v"
,
cfg
.
Security
.
ResponseHeaders
.
AdditionalAllowed
,
cfg
.
Security
.
ResponseHeaders
.
ForceRemove
,
)
}
return
&
cfg
,
nil
}
...
...
@@ -304,6 +426,45 @@ func setDefaults() {
viper
.
SetDefault
(
"server.mode"
,
"debug"
)
viper
.
SetDefault
(
"server.read_header_timeout"
,
30
)
// 30秒读取请求头
viper
.
SetDefault
(
"server.idle_timeout"
,
120
)
// 120秒空闲超时
viper
.
SetDefault
(
"server.trusted_proxies"
,
[]
string
{})
// CORS
viper
.
SetDefault
(
"cors.allowed_origins"
,
[]
string
{})
viper
.
SetDefault
(
"cors.allow_credentials"
,
true
)
// Security
viper
.
SetDefault
(
"security.url_allowlist.enabled"
,
false
)
viper
.
SetDefault
(
"security.url_allowlist.upstream_hosts"
,
[]
string
{
"api.openai.com"
,
"api.anthropic.com"
,
"api.kimi.com"
,
"open.bigmodel.cn"
,
"api.minimaxi.com"
,
"generativelanguage.googleapis.com"
,
"cloudcode-pa.googleapis.com"
,
"*.openai.azure.com"
,
})
viper
.
SetDefault
(
"security.url_allowlist.pricing_hosts"
,
[]
string
{
"raw.githubusercontent.com"
,
})
viper
.
SetDefault
(
"security.url_allowlist.crs_hosts"
,
[]
string
{})
viper
.
SetDefault
(
"security.url_allowlist.allow_private_hosts"
,
false
)
viper
.
SetDefault
(
"security.url_allowlist.allow_insecure_http"
,
false
)
viper
.
SetDefault
(
"security.response_headers.enabled"
,
false
)
viper
.
SetDefault
(
"security.response_headers.additional_allowed"
,
[]
string
{})
viper
.
SetDefault
(
"security.response_headers.force_remove"
,
[]
string
{})
viper
.
SetDefault
(
"security.csp.enabled"
,
true
)
viper
.
SetDefault
(
"security.csp.policy"
,
DefaultCSPPolicy
)
viper
.
SetDefault
(
"security.proxy_probe.insecure_skip_verify"
,
false
)
// Billing
viper
.
SetDefault
(
"billing.circuit_breaker.enabled"
,
true
)
viper
.
SetDefault
(
"billing.circuit_breaker.failure_threshold"
,
5
)
viper
.
SetDefault
(
"billing.circuit_breaker.reset_timeout_seconds"
,
30
)
viper
.
SetDefault
(
"billing.circuit_breaker.half_open_requests"
,
3
)
// Turnstile
viper
.
SetDefault
(
"turnstile.required"
,
false
)
// Database
viper
.
SetDefault
(
"database.host"
,
"localhost"
)
...
...
@@ -329,7 +490,7 @@ func setDefaults() {
viper
.
SetDefault
(
"redis.min_idle_conns"
,
10
)
// JWT
viper
.
SetDefault
(
"jwt.secret"
,
"
change-me-in-production
"
)
viper
.
SetDefault
(
"jwt.secret"
,
""
)
viper
.
SetDefault
(
"jwt.expire_hour"
,
24
)
// Default
...
...
@@ -357,7 +518,7 @@ func setDefaults() {
viper
.
SetDefault
(
"timezone"
,
"Asia/Shanghai"
)
// Gateway
viper
.
SetDefault
(
"gateway.response_header_timeout"
,
3
00
)
//
3
00秒(
5
分钟)等待上游响应头,LLM高负载时可能排队较久
viper
.
SetDefault
(
"gateway.response_header_timeout"
,
6
00
)
//
6
00秒(
10
分钟)等待上游响应头,LLM高负载时可能排队较久
viper
.
SetDefault
(
"gateway.log_upstream_error_body"
,
false
)
viper
.
SetDefault
(
"gateway.log_upstream_error_body_max_bytes"
,
2048
)
viper
.
SetDefault
(
"gateway.inject_beta_for_apikey"
,
false
)
...
...
@@ -365,19 +526,23 @@ func setDefaults() {
viper
.
SetDefault
(
"gateway.max_body_size"
,
int64
(
100
*
1024
*
1024
))
viper
.
SetDefault
(
"gateway.connection_pool_isolation"
,
ConnectionPoolIsolationAccountProxy
)
// HTTP 上游连接池配置(针对 5000+ 并发用户优化)
viper
.
SetDefault
(
"gateway.max_idle_conns"
,
240
)
// 最大空闲连接总数(HTTP/2 场景默认)
viper
.
SetDefault
(
"gateway.max_idle_conns_per_host"
,
120
)
// 每主机最大空闲连接(HTTP/2 场景默认)
viper
.
SetDefault
(
"gateway.max_conns_per_host"
,
240
)
// 每主机最大连接数(含活跃,HTTP/2 场景默认)
viper
.
SetDefault
(
"gateway.idle_conn_timeout_seconds"
,
30
0
)
// 空闲连接超时(秒)
viper
.
SetDefault
(
"gateway.max_idle_conns"
,
240
)
// 最大空闲连接总数(HTTP/2 场景默认)
viper
.
SetDefault
(
"gateway.max_idle_conns_per_host"
,
120
)
// 每主机最大空闲连接(HTTP/2 场景默认)
viper
.
SetDefault
(
"gateway.max_conns_per_host"
,
240
)
// 每主机最大连接数(含活跃,HTTP/2 场景默认)
viper
.
SetDefault
(
"gateway.idle_conn_timeout_seconds"
,
9
0
)
// 空闲连接超时(秒)
viper
.
SetDefault
(
"gateway.max_upstream_clients"
,
5000
)
viper
.
SetDefault
(
"gateway.client_idle_ttl_seconds"
,
900
)
viper
.
SetDefault
(
"gateway.concurrency_slot_ttl_minutes"
,
15
)
// 并发槽位过期时间(支持超长请求)
viper
.
SetDefault
(
"gateway.concurrency_slot_ttl_minutes"
,
30
)
// 并发槽位过期时间(支持超长请求)
viper
.
SetDefault
(
"gateway.stream_data_interval_timeout"
,
180
)
viper
.
SetDefault
(
"gateway.stream_keepalive_interval"
,
10
)
viper
.
SetDefault
(
"gateway.max_line_size"
,
10
*
1024
*
1024
)
viper
.
SetDefault
(
"gateway.scheduling.sticky_session_max_waiting"
,
3
)
viper
.
SetDefault
(
"gateway.scheduling.sticky_session_wait_timeout"
,
45
*
time
.
Second
)
viper
.
SetDefault
(
"gateway.scheduling.fallback_wait_timeout"
,
30
*
time
.
Second
)
viper
.
SetDefault
(
"gateway.scheduling.fallback_max_waiting"
,
100
)
viper
.
SetDefault
(
"gateway.scheduling.load_batch_enabled"
,
true
)
viper
.
SetDefault
(
"gateway.scheduling.slot_cleanup_interval"
,
30
*
time
.
Second
)
viper
.
SetDefault
(
"concurrency.ping_interval"
,
10
)
// TokenRefresh
viper
.
SetDefault
(
"token_refresh.enabled"
,
true
)
...
...
@@ -396,11 +561,28 @@ func setDefaults() {
}
func
(
c
*
Config
)
Validate
()
error
{
if
c
.
JWT
.
Secret
==
""
{
return
fmt
.
Errorf
(
"jwt.secret is required"
)
if
c
.
JWT
.
ExpireHour
<=
0
{
return
fmt
.
Errorf
(
"jwt.expire_hour must be positive"
)
}
if
c
.
JWT
.
ExpireHour
>
168
{
return
fmt
.
Errorf
(
"jwt.expire_hour must be <= 168 (7 days)"
)
}
if
c
.
JWT
.
Secret
==
"change-me-in-production"
&&
c
.
Server
.
Mode
==
"release"
{
return
fmt
.
Errorf
(
"jwt.secret must be changed in production"
)
if
c
.
JWT
.
ExpireHour
>
24
{
log
.
Printf
(
"Warning: jwt.expire_hour is %d hours (> 24). Consider shorter expiration for security."
,
c
.
JWT
.
ExpireHour
)
}
if
c
.
Security
.
CSP
.
Enabled
&&
strings
.
TrimSpace
(
c
.
Security
.
CSP
.
Policy
)
==
""
{
return
fmt
.
Errorf
(
"security.csp.policy is required when CSP is enabled"
)
}
if
c
.
Billing
.
CircuitBreaker
.
Enabled
{
if
c
.
Billing
.
CircuitBreaker
.
FailureThreshold
<=
0
{
return
fmt
.
Errorf
(
"billing.circuit_breaker.failure_threshold must be positive"
)
}
if
c
.
Billing
.
CircuitBreaker
.
ResetTimeoutSeconds
<=
0
{
return
fmt
.
Errorf
(
"billing.circuit_breaker.reset_timeout_seconds must be positive"
)
}
if
c
.
Billing
.
CircuitBreaker
.
HalfOpenRequests
<=
0
{
return
fmt
.
Errorf
(
"billing.circuit_breaker.half_open_requests must be positive"
)
}
}
if
c
.
Database
.
MaxOpenConns
<=
0
{
return
fmt
.
Errorf
(
"database.max_open_conns must be positive"
)
...
...
@@ -458,6 +640,9 @@ func (c *Config) Validate() error {
if
c
.
Gateway
.
IdleConnTimeoutSeconds
<=
0
{
return
fmt
.
Errorf
(
"gateway.idle_conn_timeout_seconds must be positive"
)
}
if
c
.
Gateway
.
IdleConnTimeoutSeconds
>
180
{
log
.
Printf
(
"Warning: gateway.idle_conn_timeout_seconds is %d (> 180). Consider 60-120 seconds for better connection reuse."
,
c
.
Gateway
.
IdleConnTimeoutSeconds
)
}
if
c
.
Gateway
.
MaxUpstreamClients
<=
0
{
return
fmt
.
Errorf
(
"gateway.max_upstream_clients must be positive"
)
}
...
...
@@ -467,6 +652,26 @@ func (c *Config) Validate() error {
if
c
.
Gateway
.
ConcurrencySlotTTLMinutes
<=
0
{
return
fmt
.
Errorf
(
"gateway.concurrency_slot_ttl_minutes must be positive"
)
}
if
c
.
Gateway
.
StreamDataIntervalTimeout
<
0
{
return
fmt
.
Errorf
(
"gateway.stream_data_interval_timeout must be non-negative"
)
}
if
c
.
Gateway
.
StreamDataIntervalTimeout
!=
0
&&
(
c
.
Gateway
.
StreamDataIntervalTimeout
<
30
||
c
.
Gateway
.
StreamDataIntervalTimeout
>
300
)
{
return
fmt
.
Errorf
(
"gateway.stream_data_interval_timeout must be 0 or between 30-300 seconds"
)
}
if
c
.
Gateway
.
StreamKeepaliveInterval
<
0
{
return
fmt
.
Errorf
(
"gateway.stream_keepalive_interval must be non-negative"
)
}
if
c
.
Gateway
.
StreamKeepaliveInterval
!=
0
&&
(
c
.
Gateway
.
StreamKeepaliveInterval
<
5
||
c
.
Gateway
.
StreamKeepaliveInterval
>
30
)
{
return
fmt
.
Errorf
(
"gateway.stream_keepalive_interval must be 0 or between 5-30 seconds"
)
}
if
c
.
Gateway
.
MaxLineSize
<
0
{
return
fmt
.
Errorf
(
"gateway.max_line_size must be non-negative"
)
}
if
c
.
Gateway
.
MaxLineSize
!=
0
&&
c
.
Gateway
.
MaxLineSize
<
1024
*
1024
{
return
fmt
.
Errorf
(
"gateway.max_line_size must be at least 1MB"
)
}
if
c
.
Gateway
.
Scheduling
.
StickySessionMaxWaiting
<=
0
{
return
fmt
.
Errorf
(
"gateway.scheduling.sticky_session_max_waiting must be positive"
)
}
...
...
@@ -482,9 +687,57 @@ func (c *Config) Validate() error {
if
c
.
Gateway
.
Scheduling
.
SlotCleanupInterval
<
0
{
return
fmt
.
Errorf
(
"gateway.scheduling.slot_cleanup_interval must be non-negative"
)
}
if
c
.
Concurrency
.
PingInterval
<
5
||
c
.
Concurrency
.
PingInterval
>
30
{
return
fmt
.
Errorf
(
"concurrency.ping_interval must be between 5-30 seconds"
)
}
return
nil
}
func
normalizeStringSlice
(
values
[]
string
)
[]
string
{
if
len
(
values
)
==
0
{
return
values
}
normalized
:=
make
([]
string
,
0
,
len
(
values
))
for
_
,
v
:=
range
values
{
trimmed
:=
strings
.
TrimSpace
(
v
)
if
trimmed
==
""
{
continue
}
normalized
=
append
(
normalized
,
trimmed
)
}
return
normalized
}
func
isWeakJWTSecret
(
secret
string
)
bool
{
lower
:=
strings
.
ToLower
(
strings
.
TrimSpace
(
secret
))
if
lower
==
""
{
return
true
}
weak
:=
map
[
string
]
struct
{}{
"change-me-in-production"
:
{},
"changeme"
:
{},
"secret"
:
{},
"password"
:
{},
"123456"
:
{},
"12345678"
:
{},
"admin"
:
{},
"jwt-secret"
:
{},
}
_
,
exists
:=
weak
[
lower
]
return
exists
}
func
generateJWTSecret
(
byteLength
int
)
(
string
,
error
)
{
if
byteLength
<=
0
{
byteLength
=
32
}
buf
:=
make
([]
byte
,
byteLength
)
if
_
,
err
:=
rand
.
Read
(
buf
);
err
!=
nil
{
return
""
,
err
}
return
hex
.
EncodeToString
(
buf
),
nil
}
// GetServerAddress returns the server address (host:port) from config file or environment variable.
// This is a lightweight function that can be used before full config validation,
// such as during setup wizard startup.
...
...
backend/internal/config/config_test.go
View file @
195e227c
...
...
@@ -68,3 +68,22 @@ func TestLoadSchedulingConfigFromEnv(t *testing.T) {
t
.
Fatalf
(
"StickySessionMaxWaiting = %d, want 5"
,
cfg
.
Gateway
.
Scheduling
.
StickySessionMaxWaiting
)
}
}
func
TestLoadDefaultSecurityToggles
(
t
*
testing
.
T
)
{
viper
.
Reset
()
cfg
,
err
:=
Load
()
if
err
!=
nil
{
t
.
Fatalf
(
"Load() error: %v"
,
err
)
}
if
cfg
.
Security
.
URLAllowlist
.
Enabled
{
t
.
Fatalf
(
"URLAllowlist.Enabled = true, want false"
)
}
if
cfg
.
Security
.
URLAllowlist
.
AllowInsecureHTTP
{
t
.
Fatalf
(
"URLAllowlist.AllowInsecureHTTP = true, want false"
)
}
if
cfg
.
Security
.
ResponseHeaders
.
Enabled
{
t
.
Fatalf
(
"ResponseHeaders.Enabled = true, want false"
)
}
}
Prev
1
2
3
4
5
…
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