mirror of
https://github.com/router-for-me/CLIProxyAPIPlus.git
synced 2026-03-08 06:43:41 +00:00
- Add IAM Identity Center (IDC) authentication with CLI flags (--kiro-idc-login, --kiro-idc-start-url, --kiro-idc-region) and login flow - Add ProfileArn auto-fetching in Execute/ExecuteStream for imported IDC accounts - Simplify endpoint preference with map-based alias lookup and getAuthValue helper - Redesign fingerprint as global singleton with external config and per-account deterministic generation - Add StartURL and FingerprintConfig fields to Kiro config - Add AgentContinuationID/AgentTaskType support in Kiro translators - Add comprehensive tests for executor, fingerprint, SSO OIDC, and AWS helpers - Add CLI login documentation to README
261 lines
6.3 KiB
Go
261 lines
6.3 KiB
Go
package kiro
|
||
|
||
import (
|
||
"context"
|
||
"encoding/json"
|
||
"fmt"
|
||
"io/fs"
|
||
"os"
|
||
"path/filepath"
|
||
"sort"
|
||
"strings"
|
||
"sync"
|
||
"time"
|
||
|
||
log "github.com/sirupsen/logrus"
|
||
)
|
||
|
||
// FileTokenRepository 实现 TokenRepository 接口,基于文件系统存储
|
||
type FileTokenRepository struct {
|
||
mu sync.RWMutex
|
||
baseDir string
|
||
}
|
||
|
||
// NewFileTokenRepository 创建一个新的文件 token 存储库
|
||
func NewFileTokenRepository(baseDir string) *FileTokenRepository {
|
||
return &FileTokenRepository{
|
||
baseDir: baseDir,
|
||
}
|
||
}
|
||
|
||
// SetBaseDir 设置基础目录
|
||
func (r *FileTokenRepository) SetBaseDir(dir string) {
|
||
r.mu.Lock()
|
||
r.baseDir = strings.TrimSpace(dir)
|
||
r.mu.Unlock()
|
||
}
|
||
|
||
// FindOldestUnverified 查找需要刷新的 token(按最后验证时间排序)
|
||
func (r *FileTokenRepository) FindOldestUnverified(limit int) []*Token {
|
||
r.mu.RLock()
|
||
baseDir := r.baseDir
|
||
r.mu.RUnlock()
|
||
|
||
if baseDir == "" {
|
||
log.Debug("token repository: base directory not configured")
|
||
return nil
|
||
}
|
||
|
||
var tokens []*Token
|
||
|
||
err := filepath.WalkDir(baseDir, func(path string, d fs.DirEntry, walkErr error) error {
|
||
if walkErr != nil {
|
||
return nil // 忽略错误,继续遍历
|
||
}
|
||
if d.IsDir() {
|
||
return nil
|
||
}
|
||
if !strings.HasSuffix(strings.ToLower(d.Name()), ".json") {
|
||
return nil
|
||
}
|
||
|
||
// 只处理 kiro 相关的 token 文件
|
||
if !strings.HasPrefix(d.Name(), "kiro-") {
|
||
return nil
|
||
}
|
||
|
||
token, err := r.readTokenFile(path)
|
||
if err != nil {
|
||
log.Debugf("token repository: failed to read token file %s: %v", path, err)
|
||
return nil
|
||
}
|
||
|
||
if token != nil && token.RefreshToken != "" {
|
||
// 检查 token 是否需要刷新(过期前 5 分钟)
|
||
if token.ExpiresAt.IsZero() || time.Until(token.ExpiresAt) < 5*time.Minute {
|
||
tokens = append(tokens, token)
|
||
}
|
||
}
|
||
|
||
return nil
|
||
})
|
||
|
||
if err != nil {
|
||
log.Warnf("token repository: error walking directory: %v", err)
|
||
}
|
||
|
||
// 按最后验证时间排序(最旧的优先)
|
||
sort.Slice(tokens, func(i, j int) bool {
|
||
return tokens[i].LastVerified.Before(tokens[j].LastVerified)
|
||
})
|
||
|
||
// 限制返回数量
|
||
if limit > 0 && len(tokens) > limit {
|
||
tokens = tokens[:limit]
|
||
}
|
||
|
||
return tokens
|
||
}
|
||
|
||
// UpdateToken 更新 token 并持久化到文件
|
||
func (r *FileTokenRepository) UpdateToken(token *Token) error {
|
||
if token == nil {
|
||
return fmt.Errorf("token repository: token is nil")
|
||
}
|
||
|
||
r.mu.RLock()
|
||
baseDir := r.baseDir
|
||
r.mu.RUnlock()
|
||
|
||
if baseDir == "" {
|
||
return fmt.Errorf("token repository: base directory not configured")
|
||
}
|
||
|
||
// 构建文件路径
|
||
filePath := filepath.Join(baseDir, token.ID)
|
||
if !strings.HasSuffix(filePath, ".json") {
|
||
filePath += ".json"
|
||
}
|
||
|
||
// 读取现有文件内容
|
||
existingData := make(map[string]any)
|
||
if data, err := os.ReadFile(filePath); err == nil {
|
||
_ = json.Unmarshal(data, &existingData)
|
||
}
|
||
|
||
// 更新字段
|
||
existingData["access_token"] = token.AccessToken
|
||
existingData["refresh_token"] = token.RefreshToken
|
||
existingData["last_refresh"] = time.Now().Format(time.RFC3339)
|
||
|
||
if !token.ExpiresAt.IsZero() {
|
||
existingData["expires_at"] = token.ExpiresAt.Format(time.RFC3339)
|
||
}
|
||
|
||
// 保持原有的关键字段
|
||
if token.ClientID != "" {
|
||
existingData["client_id"] = token.ClientID
|
||
}
|
||
if token.ClientSecret != "" {
|
||
existingData["client_secret"] = token.ClientSecret
|
||
}
|
||
if token.AuthMethod != "" {
|
||
existingData["auth_method"] = token.AuthMethod
|
||
}
|
||
if token.Region != "" {
|
||
existingData["region"] = token.Region
|
||
}
|
||
if token.StartURL != "" {
|
||
existingData["start_url"] = token.StartURL
|
||
}
|
||
|
||
// 序列化并写入文件
|
||
raw, err := json.MarshalIndent(existingData, "", " ")
|
||
if err != nil {
|
||
return fmt.Errorf("token repository: marshal failed: %w", err)
|
||
}
|
||
|
||
// 原子写入:先写入临时文件,再重命名
|
||
tmpPath := filePath + ".tmp"
|
||
if err := os.WriteFile(tmpPath, raw, 0o600); err != nil {
|
||
return fmt.Errorf("token repository: write temp file failed: %w", err)
|
||
}
|
||
if err := os.Rename(tmpPath, filePath); err != nil {
|
||
_ = os.Remove(tmpPath)
|
||
return fmt.Errorf("token repository: rename failed: %w", err)
|
||
}
|
||
|
||
log.Debugf("token repository: updated token %s", token.ID)
|
||
return nil
|
||
}
|
||
|
||
// readTokenFile 从文件读取 token
|
||
func (r *FileTokenRepository) readTokenFile(path string) (*Token, error) {
|
||
data, err := os.ReadFile(path)
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
|
||
var metadata map[string]any
|
||
if err := json.Unmarshal(data, &metadata); err != nil {
|
||
return nil, err
|
||
}
|
||
|
||
// 检查是否是 kiro token
|
||
tokenType, _ := metadata["type"].(string)
|
||
if tokenType != "kiro" {
|
||
return nil, nil
|
||
}
|
||
|
||
// 检查 auth_method (case-insensitive comparison to handle "IdC", "IDC", "idc", etc.)
|
||
authMethod, _ := metadata["auth_method"].(string)
|
||
authMethod = strings.ToLower(authMethod)
|
||
if authMethod != "idc" && authMethod != "builder-id" {
|
||
return nil, nil // 只处理 IDC 和 Builder ID token
|
||
}
|
||
|
||
token := &Token{
|
||
ID: filepath.Base(path),
|
||
AuthMethod: authMethod,
|
||
}
|
||
|
||
// 解析各字段
|
||
token.AccessToken, _ = metadata["access_token"].(string)
|
||
token.RefreshToken, _ = metadata["refresh_token"].(string)
|
||
token.ClientID, _ = metadata["client_id"].(string)
|
||
token.ClientSecret, _ = metadata["client_secret"].(string)
|
||
token.Region, _ = metadata["region"].(string)
|
||
token.StartURL, _ = metadata["start_url"].(string)
|
||
token.Provider, _ = metadata["provider"].(string)
|
||
|
||
// 解析时间字段
|
||
if expiresAtStr, ok := metadata["expires_at"].(string); ok && expiresAtStr != "" {
|
||
if t, err := time.Parse(time.RFC3339, expiresAtStr); err == nil {
|
||
token.ExpiresAt = t
|
||
}
|
||
}
|
||
if lastRefreshStr, ok := metadata["last_refresh"].(string); ok && lastRefreshStr != "" {
|
||
if t, err := time.Parse(time.RFC3339, lastRefreshStr); err == nil {
|
||
token.LastVerified = t
|
||
}
|
||
}
|
||
|
||
return token, nil
|
||
}
|
||
|
||
// ListKiroTokens 列出所有 Kiro token(用于调试)
|
||
func (r *FileTokenRepository) ListKiroTokens(ctx context.Context) ([]*Token, error) {
|
||
r.mu.RLock()
|
||
baseDir := r.baseDir
|
||
r.mu.RUnlock()
|
||
|
||
if baseDir == "" {
|
||
return nil, fmt.Errorf("token repository: base directory not configured")
|
||
}
|
||
|
||
var tokens []*Token
|
||
|
||
err := filepath.WalkDir(baseDir, func(path string, d fs.DirEntry, walkErr error) error {
|
||
if walkErr != nil {
|
||
return nil
|
||
}
|
||
if d.IsDir() {
|
||
return nil
|
||
}
|
||
if !strings.HasPrefix(d.Name(), "kiro-") || !strings.HasSuffix(d.Name(), ".json") {
|
||
return nil
|
||
}
|
||
|
||
token, err := r.readTokenFile(path)
|
||
if err != nil {
|
||
return nil
|
||
}
|
||
if token != nil {
|
||
tokens = append(tokens, token)
|
||
}
|
||
return nil
|
||
})
|
||
|
||
return tokens, err
|
||
}
|