refactor: 重构 token 管理,新增 NATS adapter 和多示例
按照 TOKEN_REFACTOR.md / FIX_CONFIG.md 方案重构核心层: - miot: Init() 优先读 storage token,新增 RefreshAuthInfo() 和刷新回调 - xiaomi: Config 改用 AuthInfo 结构体(含 UUID/UID/DataDir/OnTokenRefreshed) - xiaomi: NewClient(ctx, Config) 新签名,新增 RefreshToken(),后台自动刷新 - xiaomi/devices: 设备控制接口重构 - 新增示例 12_ac_subscribe,其余示例适配新 Config
This commit is contained in:
+3
-1
@@ -33,7 +33,9 @@ const (
|
||||
MIHOME_HTTP_API_TIMEOUT = 30
|
||||
MIHOME_MQTT_KEEPALIVE = 60
|
||||
// seconds, 3 days
|
||||
MIHOME_CERT_EXPIRE_MARGIN = 3600 * 24 * 3
|
||||
MIHOME_CERT_EXPIRE_MARGIN = 3600 * 24 * 3
|
||||
// seconds, 1 hour — token 过期前刷新(比 Python 60s 更保守)
|
||||
MIHOME_REFRESH_THRESHOLD = 3600 * 6
|
||||
NETWORK_REFRESH_INTERVAL = 30
|
||||
// seconds, 14 days
|
||||
SPEC_STD_LIB_EFFECTIVE_TIME = 3600 * 24 * 14
|
||||
|
||||
+131
-25
@@ -97,6 +97,10 @@ type MIoTClient struct {
|
||||
ctx context.Context
|
||||
cancel context.CancelFunc
|
||||
mu sync.RWMutex
|
||||
refreshMu sync.Mutex // token 刷新互斥锁
|
||||
|
||||
// === Callbacks ===
|
||||
tokenRefreshCB func(accessToken, refreshToken string)
|
||||
}
|
||||
|
||||
// NewMIoTClient creates a new MIoTClient.
|
||||
@@ -129,15 +133,41 @@ func NewMIoTClient(entryID string, entryData map[string]interface{}, uid string,
|
||||
// Init initializes the MIoTClient.
|
||||
// Aligns with Python MIoTClient.init_async.
|
||||
func (c *MIoTClient) Init() error {
|
||||
// 1. Load user config
|
||||
// 1. Load user config from storage, fallback to entryData
|
||||
accessToken, _ := c.entryData["access_token"].(string)
|
||||
refreshToken, _ := c.entryData["refresh_token"].(string)
|
||||
|
||||
if c.storage != nil {
|
||||
config := c.storage.LoadUserConfig(c.uid, c.cloudServer, nil)
|
||||
if len(config) > 0 {
|
||||
fmt.Printf("[MIoTClient] Loaded user config\n")
|
||||
authInfo := c.storage.LoadUserConfig(c.uid, c.cloudServer, nil)
|
||||
if t, ok := authInfo["access_token"].(string); ok && t != "" {
|
||||
accessToken = t
|
||||
// Also restore refresh_token and expires_ts from storage
|
||||
if rt, ok := authInfo["refresh_token"].(string); ok && rt != "" {
|
||||
refreshToken = rt
|
||||
c.entryData["refresh_token"] = rt
|
||||
}
|
||||
if et, ok := authInfo["expires_ts"].(float64); ok {
|
||||
c.entryData["expires_ts"] = et
|
||||
}
|
||||
fmt.Printf("[MIoTClient] Loaded token from storage\n")
|
||||
} else if accessToken != "" {
|
||||
// First run: write entryData token to storage
|
||||
expiresTS := float64(time.Now().Unix()) + 3600*24*30 // default 30 days
|
||||
if et, ok := c.entryData["expires_ts"].(float64); ok && et > 0 {
|
||||
expiresTS = et
|
||||
}
|
||||
c.storage.UpdateUserConfig(c.uid, c.cloudServer, map[string]interface{}{
|
||||
"access_token": accessToken,
|
||||
"refresh_token": refreshToken,
|
||||
"expires_ts": expiresTS,
|
||||
}, true)
|
||||
fmt.Printf("[MIoTClient] Saved initial token to storage\n")
|
||||
}
|
||||
_ = config
|
||||
}
|
||||
|
||||
// Keep access_token in entryData for downstream use
|
||||
c.entryData["access_token"] = accessToken
|
||||
|
||||
// 2. Load device cache
|
||||
if err := c.loadCacheDevice(); err != nil {
|
||||
fmt.Printf("[MIoTClient] Warning: failed to load device cache: %v\n", err)
|
||||
@@ -161,25 +191,21 @@ func (c *MIoTClient) Init() error {
|
||||
fmt.Printf("[MIoTClient] Cert client initialized\n")
|
||||
}
|
||||
|
||||
// 4. Initialize HTTP client (from existing code path)
|
||||
if c.http == nil && c.entryData != nil {
|
||||
accessToken, _ := c.entryData["access_token"].(string)
|
||||
if accessToken != "" {
|
||||
httpClient, err := NewMIoTHttpClient(c.cloudServer, OAUTH2_CLIENT_ID, accessToken)
|
||||
if err != nil {
|
||||
fmt.Printf("[MIoTClient] Warning: failed to create HTTP client: %v\n", err)
|
||||
} else {
|
||||
c.http = httpClient
|
||||
fmt.Printf("[MIoTClient] HTTP client created\n")
|
||||
}
|
||||
// 4. Initialize HTTP client (use token from storage if available)
|
||||
if c.http == nil && accessToken != "" {
|
||||
httpClient, err := NewMIoTHttpClient(c.cloudServer, OAUTH2_CLIENT_ID, accessToken)
|
||||
if err != nil {
|
||||
fmt.Printf("[MIoTClient] Warning: failed to create HTTP client: %v\n", err)
|
||||
} else {
|
||||
c.http = httpClient
|
||||
fmt.Printf("[MIoTClient] HTTP client created\n")
|
||||
}
|
||||
}
|
||||
|
||||
// 3.3 Initialize MipsCloudClient
|
||||
if c.mipsCloud == nil && c.entryData != nil {
|
||||
accessToken, _ := c.entryData["access_token"].(string)
|
||||
// 3.3 Initialize MipsCloudClient (use token from storage if available)
|
||||
if c.mipsCloud == nil && accessToken != "" {
|
||||
uuid, _ := c.entryData["uuid"].(string)
|
||||
if accessToken != "" && uuid != "" {
|
||||
if uuid != "" {
|
||||
broker := buildCloudBroker(c.cloudServer)
|
||||
mips := NewMipsCloudClient(broker, "ha."+uuid, OAUTH2_CLIENT_ID, accessToken)
|
||||
key := c.uid + "-" + c.cloudServer
|
||||
@@ -553,6 +579,11 @@ func (c *MIoTClient) UnsubDeviceState(did string) error {
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// SetTokenRefreshCallback sets a callback invoked when the background timer refreshes the token.
|
||||
func (c *MIoTClient) SetTokenRefreshCallback(cb func(accessToken, refreshToken string)) {
|
||||
c.tokenRefreshCB = cb
|
||||
}
|
||||
|
||||
// M2: RequestRefreshAuthInfo / RequestRefreshUserCert
|
||||
// ============================================================================
|
||||
|
||||
@@ -577,6 +608,9 @@ func (c *MIoTClient) RequestRefreshAuthInfo(delaySec int) {
|
||||
// refreshAuthInfo performs the actual token refresh logic.
|
||||
// Aligns with Python: refresh_oauth_info_async.
|
||||
func (c *MIoTClient) refreshAuthInfo() {
|
||||
c.refreshMu.Lock()
|
||||
defer c.refreshMu.Unlock()
|
||||
|
||||
if c.oauth == nil || c.storage == nil {
|
||||
return
|
||||
}
|
||||
@@ -587,13 +621,13 @@ func (c *MIoTClient) refreshAuthInfo() {
|
||||
refreshToken, _ := authInfo["refresh_token"].(string)
|
||||
expiresTS, _ := authInfo["expires_ts"].(float64)
|
||||
|
||||
// Check if refresh is needed: expires_ts - now <= MIHOME_CERT_EXPIRE_MARGIN
|
||||
// Check if refresh is needed: expires_ts - now <= MIHOME_REFRESH_THRESHOLD
|
||||
now := float64(time.Now().UnixMilli()) / 1000
|
||||
needRefresh := (expiresTS - now) <= float64(MIHOME_CERT_EXPIRE_MARGIN)
|
||||
needRefresh := (expiresTS - now) <= float64(MIHOME_REFRESH_THRESHOLD)
|
||||
|
||||
if !needRefresh && accessToken != "" {
|
||||
// Schedule next check
|
||||
nextDelay := int(expiresTS - now - float64(MIHOME_CERT_EXPIRE_MARGIN))
|
||||
nextDelay := int(expiresTS - now - float64(MIHOME_REFRESH_THRESHOLD))
|
||||
if nextDelay < 60 {
|
||||
nextDelay = 60
|
||||
}
|
||||
@@ -618,7 +652,11 @@ func (c *MIoTClient) refreshAuthInfo() {
|
||||
|
||||
newAccessToken, _ := result["access_token"].(string)
|
||||
newRefreshToken, _ := result["refresh_token"].(string)
|
||||
newExpiresTS, _ := result["expires_ts"].(float64)
|
||||
var newExpiresTS float64
|
||||
switch v := result["expires_ts"].(type) {
|
||||
case float64: newExpiresTS = v
|
||||
case int64: newExpiresTS = float64(v)
|
||||
}
|
||||
|
||||
if newAccessToken == "" {
|
||||
fmt.Printf("[MIoTClient] Token refresh returned empty access_token\n")
|
||||
@@ -645,14 +683,82 @@ func (c *MIoTClient) refreshAuthInfo() {
|
||||
|
||||
fmt.Printf("[MIoTClient] Token refreshed successfully\n")
|
||||
|
||||
// Notify callback (xiaomi layer uses this to persist to DB)
|
||||
if c.tokenRefreshCB != nil {
|
||||
c.tokenRefreshCB(newAccessToken, newRefreshToken)
|
||||
}
|
||||
|
||||
// Schedule next refresh
|
||||
nextDelay := int(newExpiresTS - now - float64(MIHOME_CERT_EXPIRE_MARGIN))
|
||||
nextDelay := int(newExpiresTS - now - float64(MIHOME_REFRESH_THRESHOLD))
|
||||
if nextDelay < 60 {
|
||||
nextDelay = 60
|
||||
}
|
||||
c.RequestRefreshAuthInfo(nextDelay)
|
||||
}
|
||||
|
||||
// RefreshAuthInfo 手动触发 token 刷新并同步返回结果(对齐 Python refresh_oauth_info_async)。
|
||||
// 返回 map 包含 access_token / refresh_token / expires_ts。内部有互斥锁,可安全并发调用。
|
||||
func (c *MIoTClient) RefreshAuthInfo() (map[string]interface{}, error) {
|
||||
c.refreshMu.Lock()
|
||||
defer c.refreshMu.Unlock()
|
||||
if c.oauth == nil || c.storage == nil {
|
||||
return nil, fmt.Errorf("miot: oauth or storage not initialized")
|
||||
}
|
||||
|
||||
authInfo := c.storage.LoadUserConfig(c.uid, c.cloudServer, nil)
|
||||
refreshToken, _ := authInfo["refresh_token"].(string)
|
||||
accessToken, _ := authInfo["access_token"].(string)
|
||||
expiresTS, _ := authInfo["expires_ts"].(float64)
|
||||
|
||||
// 未临近过期且 token 有效 → 直接返回,避免重复刷新浪费 refresh_token
|
||||
now := float64(time.Now().UnixMilli()) / 1000
|
||||
if accessToken != "" && (expiresTS-now) > float64(MIHOME_REFRESH_THRESHOLD) {
|
||||
return map[string]interface{}{
|
||||
"access_token": accessToken,
|
||||
"refresh_token": refreshToken,
|
||||
"expires_ts": expiresTS,
|
||||
}, nil
|
||||
}
|
||||
|
||||
if refreshToken == "" {
|
||||
refreshToken, _ = c.entryData["refresh_token"].(string)
|
||||
}
|
||||
if refreshToken == "" {
|
||||
return nil, fmt.Errorf("miot: no refresh token available")
|
||||
}
|
||||
|
||||
result, err := c.oauth.RefreshAccessToken(refreshToken)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("miot: refresh access token: %w", err)
|
||||
}
|
||||
|
||||
// Save new auth info
|
||||
c.entryData["access_token"] = result["access_token"]
|
||||
c.entryData["refresh_token"] = result["refresh_token"]
|
||||
if et, ok := result["expires_ts"]; ok {
|
||||
c.entryData["expires_ts"] = et
|
||||
}
|
||||
c.storage.UpdateUserConfig(c.uid, c.cloudServer, map[string]interface{}{
|
||||
"access_token": result["access_token"],
|
||||
"refresh_token": result["refresh_token"],
|
||||
"expires_ts": result["expires_ts"],
|
||||
}, true)
|
||||
|
||||
// Update live clients
|
||||
if c.http != nil {
|
||||
if ac, ok := result["access_token"].(string); ok {
|
||||
c.http.UpdateHTTPHeader(c.cloudServer, OAUTH2_CLIENT_ID, ac)
|
||||
}
|
||||
}
|
||||
if c.mipsCloud != nil {
|
||||
ac, _ := result["access_token"].(string)
|
||||
c.mipsCloud.UpdateAccessToken(ac)
|
||||
}
|
||||
|
||||
fmt.Printf("[MIoTClient] RefreshAuthInfo: token refreshed\n")
|
||||
return result, nil
|
||||
}
|
||||
|
||||
// RequestRefreshUserCert schedules a user certificate refresh after delaySec seconds.
|
||||
// Uses MIHOME_CERT_EXPIRE_MARGIN to determine when refresh is needed.
|
||||
// Aligns with Python: __request_refresh_user_cert(delay_sec).
|
||||
|
||||
Reference in New Issue
Block a user