Files
kirogo/config/config.go
2026-05-11 22:31:31 +08:00

514 lines
16 KiB
Go

// Package config provides configuration management for Kiro API Proxy.
//
// This package handles persistent storage and retrieval of:
// - Account credentials and authentication tokens
// - Server settings (port, host, API keys)
// - Usage statistics and metrics
// - Thinking mode configuration for AI responses
//
// All configuration is stored in a JSON file with thread-safe access
// via read-write mutex protection.
package config
import (
"crypto/rand"
"encoding/json"
"fmt"
"os"
"runtime"
"sync"
)
// GenerateMachineId generates a UUID v4 format machine identifier.
// This ID is used to uniquely identify the proxy instance in Kiro API requests,
// helping with request tracking and rate limiting on the server side.
func GenerateMachineId() string {
bytes := make([]byte, 16)
rand.Read(bytes)
bytes[6] = (bytes[6] & 0x0f) | 0x40 // 版本 4
bytes[8] = (bytes[8] & 0x3f) | 0x80 // 变体
return fmt.Sprintf("%08x-%04x-%04x-%04x-%012x",
bytes[0:4], bytes[4:6], bytes[6:8], bytes[8:10], bytes[10:16])
}
// Account represents a Kiro API account with authentication credentials and usage statistics.
type Account struct {
// Basic identification
ID string `json:"id"` // Unique account identifier (UUID)
Email string `json:"email,omitempty"` // User email address
UserId string `json:"userId,omitempty"` // Kiro user ID
Nickname string `json:"nickname,omitempty"` // Display name for admin panel
// Authentication credentials
AccessToken string `json:"accessToken"` // OAuth access token for API calls
RefreshToken string `json:"refreshToken"` // OAuth refresh token for token renewal
ClientID string `json:"clientId,omitempty"` // OIDC client ID (for IdC auth)
ClientSecret string `json:"clientSecret,omitempty"` // OIDC client secret (for IdC auth)
AuthMethod string `json:"authMethod"` // Authentication method: "idc" (AWS IdC) or "social" (GitHub/Google)
Provider string `json:"provider,omitempty"` // Identity provider name (e.g., "BuilderId", "GitHub")
Region string `json:"region"` // AWS region for OIDC endpoints
StartUrl string `json:"startUrl,omitempty"` // AWS SSO start URL
ExpiresAt int64 `json:"expiresAt,omitempty"` // Token expiration timestamp (Unix seconds)
MachineId string `json:"machineId,omitempty"` // UUID machine identifier for request tracking
// Priority weight for load balancing (higher = more requests)
Weight int `json:"weight,omitempty"` // 0 or 1 = normal, 2+ = higher priority
// Account status
Enabled bool `json:"enabled"` // Whether account is active in the pool
BanStatus string `json:"banStatus,omitempty"` // Ban status: "ACTIVE", "BANNED", "SUSPENDED"
BanReason string `json:"banReason,omitempty"` // Reason for ban/suspension
BanTime int64 `json:"banTime,omitempty"` // Timestamp when ban was detected
// Subscription information
SubscriptionType string `json:"subscriptionType,omitempty"` // Tier: FREE, PRO, PRO_PLUS, or POWER
SubscriptionTitle string `json:"subscriptionTitle,omitempty"` // Human-readable subscription name
DaysRemaining int `json:"daysRemaining,omitempty"` // Days until subscription expires
// Usage tracking
UsageCurrent float64 `json:"usageCurrent,omitempty"` // Current period usage (credits)
UsageLimit float64 `json:"usageLimit,omitempty"` // Maximum allowed usage per period
UsagePercent float64 `json:"usagePercent,omitempty"` // Usage percentage (0.0-1.0)
NextResetDate string `json:"nextResetDate,omitempty"` // Date when usage resets (YYYY-MM-DD)
LastRefresh int64 `json:"lastRefresh,omitempty"` // Last info refresh timestamp
// Trial usage tracking
TrialUsageCurrent float64 `json:"trialUsageCurrent,omitempty"` // Trial quota current usage
TrialUsageLimit float64 `json:"trialUsageLimit,omitempty"` // Trial quota total limit
TrialUsagePercent float64 `json:"trialUsagePercent,omitempty"` // Trial quota usage percentage (0.0-1.0)
TrialStatus string `json:"trialStatus,omitempty"` // Trial status: ACTIVE, EXPIRED, NONE
TrialExpiresAt int64 `json:"trialExpiresAt,omitempty"` // Trial expiration timestamp (Unix seconds)
// Runtime statistics (updated during operation)
RequestCount int `json:"requestCount,omitempty"` // Total requests processed
ErrorCount int `json:"errorCount,omitempty"` // Total errors encountered
LastUsed int64 `json:"lastUsed,omitempty"` // Last request timestamp
TotalTokens int `json:"totalTokens,omitempty"` // Cumulative tokens processed
TotalCredits float64 `json:"totalCredits,omitempty"` // Cumulative credits consumed
}
// Config represents the global application configuration.
type Config struct {
// Server settings
Password string `json:"password"` // Admin panel password
Port int `json:"port"` // HTTP server port (default: 8080)
Host string `json:"host"` // HTTP server bind address (default: 0.0.0.0)
ApiKey string `json:"apiKey,omitempty"` // API key for client authentication
RequireApiKey bool `json:"requireApiKey"` // Whether to enforce API key validation
KiroVersion string `json:"kiroVersion,omitempty"`
SystemVersion string `json:"systemVersion,omitempty"`
NodeVersion string `json:"nodeVersion,omitempty"`
Accounts []Account `json:"accounts"` // Registered Kiro accounts
// Thinking mode configuration for extended reasoning output
ThinkingSuffix string `json:"thinkingSuffix,omitempty"` // Model suffix to trigger thinking mode (default: "-thinking")
OpenAIThinkingFormat string `json:"openaiThinkingFormat,omitempty"` // OpenAI output format: "reasoning_content", "thinking", or "think"
ClaudeThinkingFormat string `json:"claudeThinkingFormat,omitempty"` // Claude output format: "reasoning_content", "thinking", or "think"
// Endpoint configuration: "auto", "codewhisperer", or "amazonq"
PreferredEndpoint string `json:"preferredEndpoint,omitempty"`
// Proxy configuration: optional outbound proxy for Kiro API requests
// Format: "socks5://host:port", "socks5://user:pass@host:port",
// "http://host:port", "http://user:pass@host:port"
// Leave empty to connect directly.
ProxyURL string `json:"proxyURL,omitempty"`
// Global statistics (persisted across restarts)
TotalRequests int `json:"totalRequests,omitempty"` // Total API requests received
SuccessRequests int `json:"successRequests,omitempty"` // Successful requests count
FailedRequests int `json:"failedRequests,omitempty"` // Failed requests count
TotalTokens int `json:"totalTokens,omitempty"` // Total tokens processed
TotalCredits float64 `json:"totalCredits,omitempty"` // Total credits consumed
}
// AccountInfo contains account metadata retrieved from Kiro API.
// Used for updating subscription and usage information.
type AccountInfo struct {
Email string
UserId string
SubscriptionType string
SubscriptionTitle string
DaysRemaining int
UsageCurrent float64
UsageLimit float64
UsagePercent float64
NextResetDate string
LastRefresh int64
TrialUsageCurrent float64
TrialUsageLimit float64
TrialUsagePercent float64
TrialStatus string
TrialExpiresAt int64
}
// Version current version
const Version = "1.0.6"
var (
cfg *Config
cfgLock sync.RWMutex
cfgPath string
)
// Init initializes the configuration system with the specified file path.
// If the file doesn't exist, a default configuration is created.
func Init(path string) error {
cfgPath = path
return Load()
}
func Load() error {
cfgLock.Lock()
defer cfgLock.Unlock()
data, err := os.ReadFile(cfgPath)
if err != nil {
if os.IsNotExist(err) {
// Create default configuration.
// Binds to 0.0.0.0 by default for Docker/container compatibility.
cfg = &Config{
Password: "changeme",
Port: 8080,
Host: "0.0.0.0",
RequireApiKey: false,
Accounts: []Account{},
}
return Save()
}
return err
}
var c Config
if err := json.Unmarshal(data, &c); err != nil {
return err
}
cfg = &c
return nil
}
// Save persists the current configuration to the JSON file.
// Uses indented formatting for human readability.
func Save() error {
data, err := json.MarshalIndent(cfg, "", " ")
if err != nil {
return err
}
return os.WriteFile(cfgPath, data, 0600)
}
// SetPassword updates the admin password.
// Primarily used for environment variable override in containerized deployments.
func SetPassword(password string) {
cfgLock.Lock()
defer cfgLock.Unlock()
cfg.Password = password
}
func Get() *Config {
cfgLock.RLock()
defer cfgLock.RUnlock()
return cfg
}
func GetPassword() string {
cfgLock.RLock()
defer cfgLock.RUnlock()
return cfg.Password
}
func GetPort() int {
cfgLock.RLock()
defer cfgLock.RUnlock()
if cfg.Port == 0 {
return 8080
}
return cfg.Port
}
func GetHost() string {
cfgLock.RLock()
defer cfgLock.RUnlock()
if cfg.Host == "" {
return "127.0.0.1"
}
return cfg.Host
}
func GetAccounts() []Account {
cfgLock.RLock()
defer cfgLock.RUnlock()
accounts := make([]Account, len(cfg.Accounts))
copy(accounts, cfg.Accounts)
return accounts
}
func GetEnabledAccounts() []Account {
cfgLock.RLock()
defer cfgLock.RUnlock()
var accounts []Account
for _, a := range cfg.Accounts {
if a.Enabled {
accounts = append(accounts, a)
}
}
return accounts
}
func AddAccount(account Account) error {
cfgLock.Lock()
defer cfgLock.Unlock()
cfg.Accounts = append(cfg.Accounts, account)
return Save()
}
func UpdateAccount(id string, account Account) error {
cfgLock.Lock()
defer cfgLock.Unlock()
for i, a := range cfg.Accounts {
if a.ID == id {
cfg.Accounts[i] = account
return Save()
}
}
return nil
}
func DeleteAccount(id string) error {
cfgLock.Lock()
defer cfgLock.Unlock()
for i, a := range cfg.Accounts {
if a.ID == id {
cfg.Accounts = append(cfg.Accounts[:i], cfg.Accounts[i+1:]...)
return Save()
}
}
return nil
}
func UpdateAccountToken(id, accessToken, refreshToken string, expiresAt int64) error {
cfgLock.Lock()
defer cfgLock.Unlock()
for i, a := range cfg.Accounts {
if a.ID == id {
cfg.Accounts[i].AccessToken = accessToken
if refreshToken != "" {
cfg.Accounts[i].RefreshToken = refreshToken
}
cfg.Accounts[i].ExpiresAt = expiresAt
return Save()
}
}
return nil
}
func GetApiKey() string {
cfgLock.RLock()
defer cfgLock.RUnlock()
return cfg.ApiKey
}
func IsApiKeyRequired() bool {
cfgLock.RLock()
defer cfgLock.RUnlock()
return cfg.RequireApiKey
}
func UpdateSettings(apiKey string, requireApiKey bool, password string) error {
cfgLock.Lock()
defer cfgLock.Unlock()
cfg.ApiKey = apiKey
cfg.RequireApiKey = requireApiKey
if password != "" {
cfg.Password = password
}
return Save()
}
func UpdateStats(totalReq, successReq, failedReq, totalTokens int, totalCredits float64) error {
cfgLock.Lock()
defer cfgLock.Unlock()
cfg.TotalRequests = totalReq
cfg.SuccessRequests = successReq
cfg.FailedRequests = failedReq
cfg.TotalTokens = totalTokens
cfg.TotalCredits = totalCredits
return Save()
}
func GetStats() (int, int, int, int, float64) {
cfgLock.RLock()
defer cfgLock.RUnlock()
return cfg.TotalRequests, cfg.SuccessRequests, cfg.FailedRequests, cfg.TotalTokens, cfg.TotalCredits
}
func UpdateAccountStats(id string, requestCount, errorCount, totalTokens int, totalCredits float64, lastUsed int64) error {
cfgLock.Lock()
defer cfgLock.Unlock()
for i, a := range cfg.Accounts {
if a.ID == id {
cfg.Accounts[i].RequestCount = requestCount
cfg.Accounts[i].ErrorCount = errorCount
cfg.Accounts[i].TotalTokens = totalTokens
cfg.Accounts[i].TotalCredits = totalCredits
cfg.Accounts[i].LastUsed = lastUsed
return Save()
}
}
return nil
}
// UpdateAccountInfo updates an account's subscription and usage information.
// Called after refreshing account data from Kiro API.
func UpdateAccountInfo(id string, info AccountInfo) error {
cfgLock.Lock()
defer cfgLock.Unlock()
for i, a := range cfg.Accounts {
if a.ID == id {
if info.Email != "" {
cfg.Accounts[i].Email = info.Email
}
if info.UserId != "" {
cfg.Accounts[i].UserId = info.UserId
}
cfg.Accounts[i].SubscriptionType = info.SubscriptionType
cfg.Accounts[i].SubscriptionTitle = info.SubscriptionTitle
cfg.Accounts[i].DaysRemaining = info.DaysRemaining
cfg.Accounts[i].UsageCurrent = info.UsageCurrent
cfg.Accounts[i].UsageLimit = info.UsageLimit
cfg.Accounts[i].UsagePercent = info.UsagePercent
cfg.Accounts[i].NextResetDate = info.NextResetDate
cfg.Accounts[i].LastRefresh = info.LastRefresh
cfg.Accounts[i].TrialUsageCurrent = info.TrialUsageCurrent
cfg.Accounts[i].TrialUsageLimit = info.TrialUsageLimit
cfg.Accounts[i].TrialUsagePercent = info.TrialUsagePercent
cfg.Accounts[i].TrialStatus = info.TrialStatus
cfg.Accounts[i].TrialExpiresAt = info.TrialExpiresAt
return Save()
}
}
return nil
}
// ThinkingConfig holds settings for AI thinking/reasoning mode.
// When enabled, models output their reasoning process alongside the response.
type ThinkingConfig struct {
Suffix string `json:"suffix"` // Model name suffix that triggers thinking mode
OpenAIFormat string `json:"openaiFormat"` // Output format for OpenAI-compatible responses
ClaudeFormat string `json:"claudeFormat"` // Output format for Claude-compatible responses
}
// GetThinkingConfig 获取 thinking 配置
func GetThinkingConfig() ThinkingConfig {
cfgLock.RLock()
defer cfgLock.RUnlock()
suffix := cfg.ThinkingSuffix
if suffix == "" {
suffix = "-thinking"
}
openaiFormat := cfg.OpenAIThinkingFormat
if openaiFormat == "" {
openaiFormat = "reasoning_content"
}
claudeFormat := cfg.ClaudeThinkingFormat
if claudeFormat == "" {
claudeFormat = "thinking"
}
return ThinkingConfig{
Suffix: suffix,
OpenAIFormat: openaiFormat,
ClaudeFormat: claudeFormat,
}
}
// UpdateThinkingConfig 更新 thinking 配置
func UpdateThinkingConfig(suffix, openaiFormat, claudeFormat string) error {
cfgLock.Lock()
defer cfgLock.Unlock()
cfg.ThinkingSuffix = suffix
cfg.OpenAIThinkingFormat = openaiFormat
cfg.ClaudeThinkingFormat = claudeFormat
return Save()
}
// GetPreferredEndpoint 获取首选端点配置
func GetPreferredEndpoint() string {
cfgLock.RLock()
defer cfgLock.RUnlock()
if cfg.PreferredEndpoint == "" {
return "auto"
}
return cfg.PreferredEndpoint
}
// UpdatePreferredEndpoint 更新首选端点配置
func UpdatePreferredEndpoint(endpoint string) error {
cfgLock.Lock()
defer cfgLock.Unlock()
cfg.PreferredEndpoint = endpoint
return Save()
}
// GetProxyURL 获取出站代理地址
func GetProxyURL() string {
cfgLock.RLock()
defer cfgLock.RUnlock()
return cfg.ProxyURL
}
// UpdateProxySettings 更新出站代理配置
func UpdateProxySettings(proxyURL string) error {
cfgLock.Lock()
defer cfgLock.Unlock()
cfg.ProxyURL = proxyURL
return Save()
}
type KiroClientConfig struct {
KiroVersion string
SystemVersion string
NodeVersion string
}
func GetKiroClientConfig() KiroClientConfig {
cfgLock.RLock()
defer cfgLock.RUnlock()
kiroVersion := "0.11.107"
if cfg != nil && cfg.KiroVersion != "" {
kiroVersion = cfg.KiroVersion
}
systemVersion := ""
if cfg != nil {
systemVersion = cfg.SystemVersion
}
if systemVersion == "" {
systemVersion = defaultSystemVersion()
}
nodeVersion := "22.22.0"
if cfg != nil && cfg.NodeVersion != "" {
nodeVersion = cfg.NodeVersion
}
return KiroClientConfig{
KiroVersion: kiroVersion,
SystemVersion: systemVersion,
NodeVersion: nodeVersion,
}
}
func defaultSystemVersion() string {
switch runtime.GOOS {
case "windows":
return "win32#10.0.22631"
case "darwin":
return "darwin#24.6.0"
default:
return "linux#6.6.87"
}
}