mirror of
https://github.com/router-for-me/CLIProxyAPI.git
synced 2026-02-18 20:30:51 +08:00
feat: add Kimi authentication support and streamline device ID handling
- Introduced `RequestKimiToken` API for Kimi authentication flow. - Integrated device ID management throughout Kimi-related components. - Enhanced header management for Kimi API requests with device ID context.
This commit is contained in:
@@ -25,6 +25,7 @@ import (
|
|||||||
"github.com/router-for-me/CLIProxyAPI/v6/internal/auth/codex"
|
"github.com/router-for-me/CLIProxyAPI/v6/internal/auth/codex"
|
||||||
geminiAuth "github.com/router-for-me/CLIProxyAPI/v6/internal/auth/gemini"
|
geminiAuth "github.com/router-for-me/CLIProxyAPI/v6/internal/auth/gemini"
|
||||||
iflowauth "github.com/router-for-me/CLIProxyAPI/v6/internal/auth/iflow"
|
iflowauth "github.com/router-for-me/CLIProxyAPI/v6/internal/auth/iflow"
|
||||||
|
"github.com/router-for-me/CLIProxyAPI/v6/internal/auth/kimi"
|
||||||
"github.com/router-for-me/CLIProxyAPI/v6/internal/auth/qwen"
|
"github.com/router-for-me/CLIProxyAPI/v6/internal/auth/qwen"
|
||||||
"github.com/router-for-me/CLIProxyAPI/v6/internal/interfaces"
|
"github.com/router-for-me/CLIProxyAPI/v6/internal/interfaces"
|
||||||
"github.com/router-for-me/CLIProxyAPI/v6/internal/misc"
|
"github.com/router-for-me/CLIProxyAPI/v6/internal/misc"
|
||||||
@@ -1608,6 +1609,82 @@ func (h *Handler) RequestQwenToken(c *gin.Context) {
|
|||||||
c.JSON(200, gin.H{"status": "ok", "url": authURL, "state": state})
|
c.JSON(200, gin.H{"status": "ok", "url": authURL, "state": state})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (h *Handler) RequestKimiToken(c *gin.Context) {
|
||||||
|
ctx := context.Background()
|
||||||
|
|
||||||
|
fmt.Println("Initializing Kimi authentication...")
|
||||||
|
|
||||||
|
state := fmt.Sprintf("kmi-%d", time.Now().UnixNano())
|
||||||
|
// Initialize Kimi auth service
|
||||||
|
kimiAuth := kimi.NewKimiAuth(h.cfg)
|
||||||
|
|
||||||
|
// Generate authorization URL
|
||||||
|
deviceFlow, errStartDeviceFlow := kimiAuth.StartDeviceFlow(ctx)
|
||||||
|
if errStartDeviceFlow != nil {
|
||||||
|
log.Errorf("Failed to generate authorization URL: %v", errStartDeviceFlow)
|
||||||
|
c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to generate authorization url"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
authURL := deviceFlow.VerificationURIComplete
|
||||||
|
if authURL == "" {
|
||||||
|
authURL = deviceFlow.VerificationURI
|
||||||
|
}
|
||||||
|
|
||||||
|
RegisterOAuthSession(state, "kimi")
|
||||||
|
|
||||||
|
go func() {
|
||||||
|
fmt.Println("Waiting for authentication...")
|
||||||
|
authBundle, errWaitForAuthorization := kimiAuth.WaitForAuthorization(ctx, deviceFlow)
|
||||||
|
if errWaitForAuthorization != nil {
|
||||||
|
SetOAuthSessionError(state, "Authentication failed")
|
||||||
|
fmt.Printf("Authentication failed: %v\n", errWaitForAuthorization)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create token storage
|
||||||
|
tokenStorage := kimiAuth.CreateTokenStorage(authBundle)
|
||||||
|
|
||||||
|
metadata := map[string]any{
|
||||||
|
"type": "kimi",
|
||||||
|
"access_token": authBundle.TokenData.AccessToken,
|
||||||
|
"refresh_token": authBundle.TokenData.RefreshToken,
|
||||||
|
"token_type": authBundle.TokenData.TokenType,
|
||||||
|
"scope": authBundle.TokenData.Scope,
|
||||||
|
"timestamp": time.Now().UnixMilli(),
|
||||||
|
}
|
||||||
|
if authBundle.TokenData.ExpiresAt > 0 {
|
||||||
|
expired := time.Unix(authBundle.TokenData.ExpiresAt, 0).UTC().Format(time.RFC3339)
|
||||||
|
metadata["expired"] = expired
|
||||||
|
}
|
||||||
|
if strings.TrimSpace(authBundle.DeviceID) != "" {
|
||||||
|
metadata["device_id"] = strings.TrimSpace(authBundle.DeviceID)
|
||||||
|
}
|
||||||
|
|
||||||
|
fileName := fmt.Sprintf("kimi-%d.json", time.Now().UnixMilli())
|
||||||
|
record := &coreauth.Auth{
|
||||||
|
ID: fileName,
|
||||||
|
Provider: "kimi",
|
||||||
|
FileName: fileName,
|
||||||
|
Label: "Kimi User",
|
||||||
|
Storage: tokenStorage,
|
||||||
|
Metadata: metadata,
|
||||||
|
}
|
||||||
|
savedPath, errSave := h.saveTokenRecord(ctx, record)
|
||||||
|
if errSave != nil {
|
||||||
|
log.Errorf("Failed to save authentication tokens: %v", errSave)
|
||||||
|
SetOAuthSessionError(state, "Failed to save authentication tokens")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Printf("Authentication successful! Token saved to %s\n", savedPath)
|
||||||
|
fmt.Println("You can now use Kimi services through this CLI")
|
||||||
|
CompleteOAuthSession(state)
|
||||||
|
CompleteOAuthSessionsByProvider("kimi")
|
||||||
|
}()
|
||||||
|
|
||||||
|
c.JSON(200, gin.H{"status": "ok", "url": authURL, "state": state})
|
||||||
|
}
|
||||||
|
|
||||||
func (h *Handler) RequestIFlowToken(c *gin.Context) {
|
func (h *Handler) RequestIFlowToken(c *gin.Context) {
|
||||||
ctx := context.Background()
|
ctx := context.Background()
|
||||||
|
|
||||||
|
|||||||
@@ -623,6 +623,7 @@ func (s *Server) registerManagementRoutes() {
|
|||||||
mgmt.GET("/gemini-cli-auth-url", s.mgmt.RequestGeminiCLIToken)
|
mgmt.GET("/gemini-cli-auth-url", s.mgmt.RequestGeminiCLIToken)
|
||||||
mgmt.GET("/antigravity-auth-url", s.mgmt.RequestAntigravityToken)
|
mgmt.GET("/antigravity-auth-url", s.mgmt.RequestAntigravityToken)
|
||||||
mgmt.GET("/qwen-auth-url", s.mgmt.RequestQwenToken)
|
mgmt.GET("/qwen-auth-url", s.mgmt.RequestQwenToken)
|
||||||
|
mgmt.GET("/kimi-auth-url", s.mgmt.RequestKimiToken)
|
||||||
mgmt.GET("/iflow-auth-url", s.mgmt.RequestIFlowToken)
|
mgmt.GET("/iflow-auth-url", s.mgmt.RequestIFlowToken)
|
||||||
mgmt.POST("/iflow-auth-url", s.mgmt.RequestIFlowCookieToken)
|
mgmt.POST("/iflow-auth-url", s.mgmt.RequestIFlowCookieToken)
|
||||||
mgmt.POST("/oauth-callback", s.mgmt.PostOAuthCallback)
|
mgmt.POST("/oauth-callback", s.mgmt.PostOAuthCallback)
|
||||||
|
|||||||
@@ -10,7 +10,6 @@ import (
|
|||||||
"net/http"
|
"net/http"
|
||||||
"net/url"
|
"net/url"
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
|
||||||
"runtime"
|
"runtime"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
@@ -68,6 +67,7 @@ func (k *KimiAuth) WaitForAuthorization(ctx context.Context, deviceCode *DeviceC
|
|||||||
|
|
||||||
return &KimiAuthBundle{
|
return &KimiAuthBundle{
|
||||||
TokenData: tokenData,
|
TokenData: tokenData,
|
||||||
|
DeviceID: k.deviceClient.deviceID,
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -82,6 +82,7 @@ func (k *KimiAuth) CreateTokenStorage(bundle *KimiAuthBundle) *KimiTokenStorage
|
|||||||
RefreshToken: bundle.TokenData.RefreshToken,
|
RefreshToken: bundle.TokenData.RefreshToken,
|
||||||
TokenType: bundle.TokenData.TokenType,
|
TokenType: bundle.TokenData.TokenType,
|
||||||
Scope: bundle.TokenData.Scope,
|
Scope: bundle.TokenData.Scope,
|
||||||
|
DeviceID: strings.TrimSpace(bundle.DeviceID),
|
||||||
Expired: expired,
|
Expired: expired,
|
||||||
Type: "kimi",
|
Type: "kimi",
|
||||||
}
|
}
|
||||||
@@ -96,42 +97,29 @@ type DeviceFlowClient struct {
|
|||||||
|
|
||||||
// NewDeviceFlowClient creates a new device flow client.
|
// NewDeviceFlowClient creates a new device flow client.
|
||||||
func NewDeviceFlowClient(cfg *config.Config) *DeviceFlowClient {
|
func NewDeviceFlowClient(cfg *config.Config) *DeviceFlowClient {
|
||||||
|
return NewDeviceFlowClientWithDeviceID(cfg, "")
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewDeviceFlowClientWithDeviceID creates a new device flow client with the specified device ID.
|
||||||
|
func NewDeviceFlowClientWithDeviceID(cfg *config.Config, deviceID string) *DeviceFlowClient {
|
||||||
client := &http.Client{Timeout: 30 * time.Second}
|
client := &http.Client{Timeout: 30 * time.Second}
|
||||||
if cfg != nil {
|
if cfg != nil {
|
||||||
client = util.SetProxy(&cfg.SDKConfig, client)
|
client = util.SetProxy(&cfg.SDKConfig, client)
|
||||||
}
|
}
|
||||||
|
resolvedDeviceID := strings.TrimSpace(deviceID)
|
||||||
|
if resolvedDeviceID == "" {
|
||||||
|
resolvedDeviceID = getOrCreateDeviceID()
|
||||||
|
}
|
||||||
return &DeviceFlowClient{
|
return &DeviceFlowClient{
|
||||||
httpClient: client,
|
httpClient: client,
|
||||||
cfg: cfg,
|
cfg: cfg,
|
||||||
deviceID: getOrCreateDeviceID(),
|
deviceID: resolvedDeviceID,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// getOrCreateDeviceID returns a stable device ID.
|
// getOrCreateDeviceID returns an in-memory device ID for the current authentication flow.
|
||||||
func getOrCreateDeviceID() string {
|
func getOrCreateDeviceID() string {
|
||||||
homeDir, err := os.UserHomeDir()
|
return uuid.New().String()
|
||||||
if err != nil {
|
|
||||||
log.Warnf("kimi: could not get user home directory: %v. Using random device ID.", err)
|
|
||||||
return uuid.New().String()
|
|
||||||
}
|
|
||||||
configDir := filepath.Join(homeDir, ".cli-proxy-api")
|
|
||||||
deviceIDPath := filepath.Join(configDir, "kimi-device-id")
|
|
||||||
|
|
||||||
// Try to read existing device ID
|
|
||||||
if data, err := os.ReadFile(deviceIDPath); err == nil {
|
|
||||||
return strings.TrimSpace(string(data))
|
|
||||||
}
|
|
||||||
|
|
||||||
// Create new device ID
|
|
||||||
deviceID := uuid.New().String()
|
|
||||||
if err := os.MkdirAll(configDir, 0700); err != nil {
|
|
||||||
log.Warnf("kimi: failed to create config directory %s, cannot persist device ID: %v", configDir, err)
|
|
||||||
return deviceID
|
|
||||||
}
|
|
||||||
if err := os.WriteFile(deviceIDPath, []byte(deviceID), 0600); err != nil {
|
|
||||||
log.Warnf("kimi: failed to write device ID to %s: %v", deviceIDPath, err)
|
|
||||||
}
|
|
||||||
return deviceID
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// getDeviceModel returns a device model string.
|
// getDeviceModel returns a device model string.
|
||||||
@@ -406,4 +394,3 @@ func (c *DeviceFlowClient) RefreshToken(ctx context.Context, refreshToken string
|
|||||||
Scope: tokenResp.Scope,
|
Scope: tokenResp.Scope,
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -23,6 +23,8 @@ type KimiTokenStorage struct {
|
|||||||
TokenType string `json:"token_type"`
|
TokenType string `json:"token_type"`
|
||||||
// Scope is the OAuth2 scope granted to the token.
|
// Scope is the OAuth2 scope granted to the token.
|
||||||
Scope string `json:"scope,omitempty"`
|
Scope string `json:"scope,omitempty"`
|
||||||
|
// DeviceID is the OAuth device flow identifier used for Kimi requests.
|
||||||
|
DeviceID string `json:"device_id,omitempty"`
|
||||||
// Expired is the RFC3339 timestamp when the access token expires.
|
// Expired is the RFC3339 timestamp when the access token expires.
|
||||||
Expired string `json:"expired,omitempty"`
|
Expired string `json:"expired,omitempty"`
|
||||||
// Type indicates the authentication provider type, always "kimi" for this storage.
|
// Type indicates the authentication provider type, always "kimi" for this storage.
|
||||||
@@ -47,6 +49,8 @@ type KimiTokenData struct {
|
|||||||
type KimiAuthBundle struct {
|
type KimiAuthBundle struct {
|
||||||
// TokenData contains the OAuth token information.
|
// TokenData contains the OAuth token information.
|
||||||
TokenData *KimiTokenData
|
TokenData *KimiTokenData
|
||||||
|
// DeviceID is the device identifier used during OAuth device flow.
|
||||||
|
DeviceID string
|
||||||
}
|
}
|
||||||
|
|
||||||
// DeviceCodeResponse represents Kimi's device code response.
|
// DeviceCodeResponse represents Kimi's device code response.
|
||||||
|
|||||||
@@ -23,7 +23,6 @@ import (
|
|||||||
"github.com/tidwall/sjson"
|
"github.com/tidwall/sjson"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
// KimiExecutor is a stateless executor for Kimi API using OpenAI-compatible chat completions.
|
// KimiExecutor is a stateless executor for Kimi API using OpenAI-compatible chat completions.
|
||||||
type KimiExecutor struct {
|
type KimiExecutor struct {
|
||||||
cfg *config.Config
|
cfg *config.Config
|
||||||
@@ -88,7 +87,7 @@ func (e *KimiExecutor) Execute(ctx context.Context, auth *cliproxyauth.Auth, req
|
|||||||
return resp, fmt.Errorf("kimi executor: failed to set model in payload: %w", err)
|
return resp, fmt.Errorf("kimi executor: failed to set model in payload: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
body, err = thinking.ApplyThinking(body, req.Model, from.String(), to.String(), e.Identifier())
|
body, err = thinking.ApplyThinking(body, req.Model, from.String(), "kimi", e.Identifier())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return resp, err
|
return resp, err
|
||||||
}
|
}
|
||||||
@@ -101,7 +100,7 @@ func (e *KimiExecutor) Execute(ctx context.Context, auth *cliproxyauth.Auth, req
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return resp, err
|
return resp, err
|
||||||
}
|
}
|
||||||
applyKimiHeaders(httpReq, token, false)
|
applyKimiHeadersWithAuth(httpReq, token, false, auth)
|
||||||
var authID, authLabel, authType, authValue string
|
var authID, authLabel, authType, authValue string
|
||||||
if auth != nil {
|
if auth != nil {
|
||||||
authID = auth.ID
|
authID = auth.ID
|
||||||
@@ -179,7 +178,7 @@ func (e *KimiExecutor) ExecuteStream(ctx context.Context, auth *cliproxyauth.Aut
|
|||||||
return nil, fmt.Errorf("kimi executor: failed to set model in payload: %w", err)
|
return nil, fmt.Errorf("kimi executor: failed to set model in payload: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
body, err = thinking.ApplyThinking(body, req.Model, from.String(), to.String(), e.Identifier())
|
body, err = thinking.ApplyThinking(body, req.Model, from.String(), "kimi", e.Identifier())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@@ -196,7 +195,7 @@ func (e *KimiExecutor) ExecuteStream(ctx context.Context, auth *cliproxyauth.Aut
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
applyKimiHeaders(httpReq, token, true)
|
applyKimiHeadersWithAuth(httpReq, token, true, auth)
|
||||||
var authID, authLabel, authType, authValue string
|
var authID, authLabel, authType, authValue string
|
||||||
if auth != nil {
|
if auth != nil {
|
||||||
authID = auth.ID
|
authID = auth.ID
|
||||||
@@ -310,7 +309,7 @@ func (e *KimiExecutor) Refresh(ctx context.Context, auth *cliproxyauth.Auth) (*c
|
|||||||
return auth, nil
|
return auth, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
client := kimiauth.NewDeviceFlowClient(e.cfg)
|
client := kimiauth.NewDeviceFlowClientWithDeviceID(e.cfg, resolveKimiDeviceID(auth))
|
||||||
td, err := client.RefreshToken(ctx, refreshToken)
|
td, err := client.RefreshToken(ctx, refreshToken)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
@@ -351,6 +350,53 @@ func applyKimiHeaders(r *http.Request, token string, stream bool) {
|
|||||||
r.Header.Set("Accept", "application/json")
|
r.Header.Set("Accept", "application/json")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func resolveKimiDeviceIDFromAuth(auth *cliproxyauth.Auth) string {
|
||||||
|
if auth == nil || auth.Metadata == nil {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
deviceIDRaw, ok := auth.Metadata["device_id"]
|
||||||
|
if !ok {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
deviceID, ok := deviceIDRaw.(string)
|
||||||
|
if !ok {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
return strings.TrimSpace(deviceID)
|
||||||
|
}
|
||||||
|
|
||||||
|
func resolveKimiDeviceIDFromStorage(auth *cliproxyauth.Auth) string {
|
||||||
|
if auth == nil {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
storage, ok := auth.Storage.(*kimiauth.KimiTokenStorage)
|
||||||
|
if !ok || storage == nil {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
return strings.TrimSpace(storage.DeviceID)
|
||||||
|
}
|
||||||
|
|
||||||
|
func resolveKimiDeviceID(auth *cliproxyauth.Auth) string {
|
||||||
|
deviceID := resolveKimiDeviceIDFromAuth(auth)
|
||||||
|
if deviceID != "" {
|
||||||
|
return deviceID
|
||||||
|
}
|
||||||
|
return resolveKimiDeviceIDFromStorage(auth)
|
||||||
|
}
|
||||||
|
|
||||||
|
func applyKimiHeadersWithAuth(r *http.Request, token string, stream bool, auth *cliproxyauth.Auth) {
|
||||||
|
applyKimiHeaders(r, token, stream)
|
||||||
|
|
||||||
|
if deviceID := resolveKimiDeviceID(auth); deviceID != "" {
|
||||||
|
r.Header.Set("X-Msh-Device-Id", deviceID)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// getKimiHostname returns the machine hostname.
|
// getKimiHostname returns the machine hostname.
|
||||||
func getKimiHostname() string {
|
func getKimiHostname() string {
|
||||||
hostname, err := os.Hostname()
|
hostname, err := os.Hostname()
|
||||||
@@ -389,11 +435,6 @@ func getKimiDeviceID() string {
|
|||||||
if data, err := os.ReadFile(deviceIDPath); err == nil {
|
if data, err := os.ReadFile(deviceIDPath); err == nil {
|
||||||
return strings.TrimSpace(string(data))
|
return strings.TrimSpace(string(data))
|
||||||
}
|
}
|
||||||
// Fallback to our own device ID
|
|
||||||
ourPath := filepath.Join(homeDir, ".cli-proxy-api", "kimi-device-id")
|
|
||||||
if data, err := os.ReadFile(ourPath); err == nil {
|
|
||||||
return strings.TrimSpace(string(data))
|
|
||||||
}
|
|
||||||
return "cli-proxy-api-device"
|
return "cli-proxy-api-device"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ type ManagementTokenRequester interface {
|
|||||||
RequestCodexToken(*gin.Context)
|
RequestCodexToken(*gin.Context)
|
||||||
RequestAntigravityToken(*gin.Context)
|
RequestAntigravityToken(*gin.Context)
|
||||||
RequestQwenToken(*gin.Context)
|
RequestQwenToken(*gin.Context)
|
||||||
|
RequestKimiToken(*gin.Context)
|
||||||
RequestIFlowToken(*gin.Context)
|
RequestIFlowToken(*gin.Context)
|
||||||
RequestIFlowCookieToken(*gin.Context)
|
RequestIFlowCookieToken(*gin.Context)
|
||||||
GetAuthStatus(c *gin.Context)
|
GetAuthStatus(c *gin.Context)
|
||||||
@@ -55,6 +56,10 @@ func (m *managementTokenRequester) RequestQwenToken(c *gin.Context) {
|
|||||||
m.handler.RequestQwenToken(c)
|
m.handler.RequestQwenToken(c)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (m *managementTokenRequester) RequestKimiToken(c *gin.Context) {
|
||||||
|
m.handler.RequestKimiToken(c)
|
||||||
|
}
|
||||||
|
|
||||||
func (m *managementTokenRequester) RequestIFlowToken(c *gin.Context) {
|
func (m *managementTokenRequester) RequestIFlowToken(c *gin.Context) {
|
||||||
m.handler.RequestIFlowToken(c)
|
m.handler.RequestIFlowToken(c)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ package auth
|
|||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/router-for-me/CLIProxyAPI/v6/internal/auth/kimi"
|
"github.com/router-for-me/CLIProxyAPI/v6/internal/auth/kimi"
|
||||||
@@ -102,6 +103,9 @@ func (a KimiAuthenticator) Login(ctx context.Context, cfg *config.Config, opts *
|
|||||||
exp := time.Unix(authBundle.TokenData.ExpiresAt, 0).UTC().Format(time.RFC3339)
|
exp := time.Unix(authBundle.TokenData.ExpiresAt, 0).UTC().Format(time.RFC3339)
|
||||||
metadata["expired"] = exp
|
metadata["expired"] = exp
|
||||||
}
|
}
|
||||||
|
if strings.TrimSpace(authBundle.DeviceID) != "" {
|
||||||
|
metadata["device_id"] = strings.TrimSpace(authBundle.DeviceID)
|
||||||
|
}
|
||||||
|
|
||||||
// Generate a unique filename
|
// Generate a unique filename
|
||||||
fileName := fmt.Sprintf("kimi-%d.json", time.Now().UnixMilli())
|
fileName := fmt.Sprintf("kimi-%d.json", time.Now().UnixMilli())
|
||||||
|
|||||||
Reference in New Issue
Block a user