Переглянути джерело

feat:接入日志,调整计时为广播方案

lilu 1 місяць тому
батько
коміт
8cdbb2b8a7
79 змінених файлів з 4396 додано та 2263 видалено
  1. 2 0
      android/app/build.gradle.kts
  2. BIN
      android/app/libs/libxray.aar
  3. 26 30
      android/app/src/main/AndroidManifest.xml
  4. 5 0
      android/app/src/main/kotlin/app/xixi/nomo/Actions.kt
  5. 52 17
      android/app/src/main/kotlin/app/xixi/nomo/App.kt
  6. 232 0
      android/app/src/main/kotlin/app/xixi/nomo/AppLogger.kt
  7. 8 0
      android/app/src/main/kotlin/app/xixi/nomo/Broadcasts.kt
  8. 14 18
      android/app/src/main/kotlin/app/xixi/nomo/CoreApi.g.kt
  9. 89 55
      android/app/src/main/kotlin/app/xixi/nomo/CoreApiImpl.kt
  10. 148 0
      android/app/src/main/kotlin/app/xixi/nomo/CryptoUtils.kt
  11. 10 0
      android/app/src/main/kotlin/app/xixi/nomo/ErrorCode.kt
  12. 55 0
      android/app/src/main/kotlin/app/xixi/nomo/LifecycleVpnService.kt
  13. 5 5
      android/app/src/main/kotlin/app/xixi/nomo/MainActivity.kt
  14. 468 0
      android/app/src/main/kotlin/app/xixi/nomo/NetworkReporter.kt
  15. 41 0
      android/app/src/main/kotlin/app/xixi/nomo/NotificationUtils.kt
  16. 7 0
      android/app/src/main/kotlin/app/xixi/nomo/ServiceStatus.kt
  17. 1 1
      android/app/src/main/kotlin/app/xixi/nomo/TProxyService.kt
  18. 121 262
      android/app/src/main/kotlin/app/xixi/nomo/XRayApi.kt
  19. 492 331
      android/app/src/main/kotlin/app/xixi/nomo/XRayService.kt
  20. 17 1
      android/app/src/main/kotlin/app/xixi/nomo/XrayConfig.kt
  21. BIN
      assets/images/test.png
  22. 4 0
      assets/vectors/vip.svg
  23. 3 0
      devtools_options.yaml
  24. 14 16
      ios/Runner/CoreApi.g.swift
  25. 62 4
      lib/app/api/base/base_api.dart
  26. 5 11
      lib/app/api/core/api_core.dart
  27. 6 0
      lib/app/api/core/api_core_paths.dart
  28. 0 11
      lib/app/api/file/api_file.dart
  29. 0 11
      lib/app/api/log/api_log.dart
  30. 5 11
      lib/app/api/router/api_router.dart
  31. 5 0
      lib/app/constants/api_domains.dart
  32. 1 0
      lib/app/constants/assets.dart
  33. 12 5
      lib/app/constants/enums.dart
  34. 9 49
      lib/app/constants/errors.dart
  35. 424 23
      lib/app/controllers/api_controller.dart
  36. 170 52
      lib/app/controllers/core_controller.dart
  37. 0 8
      lib/app/data/models/fingerprint.dart
  38. 13 8
      lib/app/data/models/launch/user.dart
  39. 43 3
      lib/app/data/models/launch/user.freezed.dart
  40. 4 0
      lib/app/data/models/launch/user.g.dart
  41. 40 1
      lib/app/data/models/vpn_message.dart
  42. 0 66
      lib/app/data/sp/ix_sp.dart
  43. 5 36
      lib/app/modules/account/controllers/account_controller.dart
  44. 58 163
      lib/app/modules/account/views/account_view.dart
  45. 0 5
      lib/app/modules/home/controllers/home_controller.dart
  46. 20 4
      lib/app/modules/home/views/home_view.dart
  47. 6 25
      lib/app/modules/setting/controllers/setting_controller.dart
  48. 38 38
      lib/app/modules/setting/views/setting_view.dart
  49. 1 1
      lib/app/modules/subscription/controllers/subscription_controller.dart
  50. 10 0
      lib/config/translations/ar_AR/ar_ar_translation.dart
  51. 10 0
      lib/config/translations/de_DE/de_de_translation.dart
  52. 10 0
      lib/config/translations/en_US/en_us_translation.dart
  53. 10 0
      lib/config/translations/es_ES/es_es_translation.dart
  54. 10 0
      lib/config/translations/fa_IR/fa_ir_translation.dart
  55. 10 0
      lib/config/translations/fr_FR/fr_fr_translation.dart
  56. 10 0
      lib/config/translations/hi_IN/hi_in_translation.dart
  57. 10 0
      lib/config/translations/id_ID/id_id_translation.dart
  58. 10 0
      lib/config/translations/ja_JP/ja_jp_translation.dart
  59. 10 0
      lib/config/translations/ko_KR/ko_kr_translation.dart
  60. 10 0
      lib/config/translations/my_MM/my_mm_translation.dart
  61. 10 0
      lib/config/translations/pt_BR/pt_br_translation.dart
  62. 10 0
      lib/config/translations/ru_RU/ru_ru_translation.dart
  63. 13 0
      lib/config/translations/strings_enum.dart
  64. 10 0
      lib/config/translations/th_TH/th_th_translation.dart
  65. 10 0
      lib/config/translations/tk_TM/tk_tm_translation.dart
  66. 10 0
      lib/config/translations/tl_PH/tl_ph_translation.dart
  67. 10 0
      lib/config/translations/tr_TR/tr_tr_translation.dart
  68. 10 0
      lib/config/translations/vi_VN/vi_vn_translation.dart
  69. 10 0
      lib/config/translations/zh_TW/zh_tw_translation.dart
  70. 2 25
      lib/pigeons/core_api.g.dart
  71. 577 0
      lib/utils/api_statistics.dart
  72. 8 12
      lib/utils/boost_logger.dart
  73. 3 0
      lib/utils/boost_report_manager.dart
  74. 144 336
      lib/utils/developer/ix_developer_tools.dart
  75. 399 343
      lib/utils/developer/simple_api_card.dart
  76. 278 251
      lib/utils/developer/simple_log_card.dart
  77. 28 24
      lib/utils/ntp_time_service.dart
  78. 12 1
      pigeons/core_api.dart
  79. 1 0
      pubspec.yaml

+ 2 - 0
android/app/build.gradle.kts

@@ -124,4 +124,6 @@ dependencies {
     // Core Libraries
     implementation(fileTree(mapOf("dir" to "libs", "include" to listOf("*.aar", "*.jar"))))
     implementation("com.google.code.gson:gson:2.13.2")
+    implementation("androidx.lifecycle:lifecycle-service:2.8.4")
+    implementation("commons-io:commons-io:2.6")
 }

BIN
android/app/libs/libxray.aar


+ 26 - 30
android/app/src/main/AndroidManifest.xml

@@ -1,28 +1,24 @@
 <manifest xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:tools="http://schemas.android.com/tools">
-    <uses-permission
-        android:name="android.permission.ACCESS_NETWORK_STATE"/>
-    <uses-permission
-        android:name="android.permission.CHANGE_NETWORK_STATE"/>
-    <uses-permission
-        android:name="android.permission.INTERNET"/>
-    <uses-permission
-        android:name="android.permission.FOREGROUND_SERVICE"/>
-    <uses-permission
-        android:name="android.permission.FOREGROUND_SERVICE_SPECIAL_USE"/>
+
+    <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
+    <uses-permission android:name="android.permission.CHANGE_NETWORK_STATE" />
+    <uses-permission android:name="android.permission.INTERNET" />
+    <uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
+    <uses-permission android:name="android.permission.FOREGROUND_SERVICE_SPECIAL_USE" />
     <!-- <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" /> -->
-    <uses-permission
-        android:name="android.permission.POST_NOTIFICATIONS"/>
-    <uses-permission
-        android:name="android:activate_vpn"/>
+    <uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
+    <uses-permission android:name="android:activate_vpn" />
     <uses-permission
         android:name="android.permission.PACKAGE_USAGE_STATS"
-        tools:ignore="ProtectedPermissions"/>
+        tools:ignore="ProtectedPermissions" />
 
     <!-- 仅在 Android 10 及以下版本需要存储权限 -->
-    <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"
+    <uses-permission
+        android:name="android.permission.WRITE_EXTERNAL_STORAGE"
         android:maxSdkVersion="29" />
-    <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"
+    <uses-permission
+        android:name="android.permission.READ_EXTERNAL_STORAGE"
         android:maxSdkVersion="29" />
 
     <queries>
@@ -71,12 +67,13 @@
                  to determine the Window background behind the Flutter UI. -->
             <meta-data
                 android:name="io.flutter.embedding.android.NormalTheme"
-                android:resource="@style/NormalTheme"/>
+                android:resource="@style/NormalTheme" />
             <intent-filter>
-                <action android:name="android.intent.action.MAIN"/>
-                <category android:name="android.intent.category.LAUNCHER"/>
+                <action android:name="android.intent.action.MAIN" />
+                <category android:name="android.intent.category.LAUNCHER" />
             </intent-filter>
         </activity>
+
         <service
             android:name=".XRayService"
             android:directBootAware="true"
@@ -84,21 +81,22 @@
             android:foregroundServiceType="specialUse"
             android:label="@string/app_name"
             android:permission="android.permission.BIND_VPN_SERVICE"
-            android:process=":nomo_vpn_service"
-            tools:ignore="ForegroundServicePermission">
+            android:process=":vpn_service">
             <intent-filter>
-                <action
-                    android:name="android.net.VpnService"/>
+                <action android:name="android.net.VpnService" />
             </intent-filter>
             <meta-data
                 android:name="android.net.VpnService.SUPPORTS_ALWAYS_ON"
-                android:value="false"/>
+                android:value="false" />
+            <property
+                android:name="android.app.PROPERTY_SPECIAL_USE_FGS_SUBTYPE"
+                android:value="vpn" />
         </service>
         <!-- Don't delete the meta-data below.
              This is used by the Flutter tool to generate GeneratedPluginRegistrant.java -->
         <meta-data
             android:name="flutterEmbedding"
-            android:value="2"/>
+            android:value="2" />
         <meta-data
             android:name="channel"
             android:value="${CHANNEL}" />
@@ -110,10 +108,8 @@
          In particular, this is used by the Flutter engine in io.flutter.plugin.text.ProcessTextPlugin. -->
     <queries>
         <intent>
-            <action
-                android:name="android.intent.action.PROCESS_TEXT"/>
-            <data
-                android:mimeType="text/plain"/>
+            <action android:name="android.intent.action.PROCESS_TEXT" />
+            <data android:mimeType="text/plain" />
         </intent>
     </queries>
 </manifest>

+ 5 - 0
android/app/src/main/kotlin/app/xixi/nomo/Actions.kt

@@ -0,0 +1,5 @@
+package app.xixi.nomo
+
+const val START_ACTION = "start"
+const val STOP_ACTION = "stop"
+

+ 52 - 17
android/app/src/main/kotlin/app/xixi/nomo/App.kt

@@ -2,17 +2,26 @@ package app.xixi.nomo
 
 import android.app.Application
 import android.content.Context
+import android.content.Intent
 import android.os.Process
+import android.util.Log
+import androidx.lifecycle.DefaultLifecycleObserver
+import androidx.lifecycle.LifecycleOwner
+import androidx.lifecycle.ProcessLifecycleOwner
 import io.flutter.FlutterInjector
 import io.flutter.embedding.engine.FlutterEngine
 import io.flutter.embedding.engine.FlutterEngineCache
-import android.util.Log
+
 class App : Application() {
     
     companion object {
         private const val TAG = "App"
         private const val ENGINE_ID = "main_engine"
         
+        // 应用前后台状态广播
+
+        const val EXTRA_IS_FOREGROUND = "is_foreground"
+        
         // 获取缓存的Flutter引擎
         fun getFlutterEngine(): FlutterEngine? {
             return FlutterEngineCache.getInstance().get(ENGINE_ID)
@@ -39,21 +48,47 @@ class App : Application() {
         }
     }
     
+    // 应用生命周期观察者
+    private val appLifecycleObserver = object : DefaultLifecycleObserver {
+        override fun onStart(owner: LifecycleOwner) {
+            // 应用进入前台
+            Log.d(TAG, "应用进入前台")
+            sendForegroundBroadcast(true)
+        }
+        
+        override fun onStop(owner: LifecycleOwner) {
+            // 应用进入后台
+            Log.d(TAG, "应用进入后台")
+            sendForegroundBroadcast(false)
+        }
+    }
+    
+    private fun sendForegroundBroadcast(isForeground: Boolean) {
+        val intent = Intent(APP_FOREGROUND_BROADCAST).apply {
+            setPackage(packageName)
+            putExtra(EXTRA_IS_FOREGROUND, isForeground)
+        }
+        sendBroadcast(intent)
+    }
+    
     override fun onCreate() {
         super.onCreate()
         
-        // 检查是否为主进程
-        if (isMainProcess(this)) {
-            Log.d(TAG, "主进程启动,开始初始化...")
+        // 注册应用前后台状态监听
+        ProcessLifecycleOwner.get().lifecycle.addObserver(appLifecycleObserver)
+        
+        // // 检查是否为主进程
+        // if (isMainProcess(this)) {
+        //     Log.d(TAG, "主进程启动,开始初始化...")
             
-            // 在主进程中初始化Flutter引擎
-            initializeFlutterEngine()
+        //     // 在主进程中初始化Flutter引擎
+        //     initializeFlutterEngine()
             
-            // 其他应用初始化逻辑
-            initializeApp()
-        } else {
-            Log.d(TAG, "非主进程启动,跳过Flutter引擎初始化")
-        }
+        //     // 其他应用初始化逻辑
+        //     initializeApp()
+        // } else {
+        //     Log.d(TAG, "非主进程启动,跳过Flutter引擎初始化")
+        // }
     }
     
     private fun initializeFlutterEngine() {
@@ -76,7 +111,7 @@ class App : Application() {
             // }
             
             // 缓存引擎以便后续使用
-//            FlutterEngineCache.getInstance().put(ENGINE_ID, flutterEngine)
+           FlutterEngineCache.getInstance().put(ENGINE_ID, flutterEngine)
             
             Log.d(TAG, "Flutter引擎在主进程中初始化完成")
             
@@ -97,9 +132,9 @@ class App : Application() {
         }
     }
     
-    override fun attachBaseContext(base: Context?) {
-        super.attachBaseContext(base)
-        // 确保Flutter注入器已初始化(在所有进程中)
-        FlutterInjector.instance()
-    }
+//    override fun attachBaseContext(base: Context?) {
+//        super.attachBaseContext(base)
+//        // 确保Flutter注入器已初始化(在所有进程中)
+//        FlutterInjector.instance()
+//    }
 }

+ 232 - 0
android/app/src/main/kotlin/app/xixi/nomo/AppLogger.kt

@@ -0,0 +1,232 @@
+package app.xixi.nomo
+
+import android.annotation.SuppressLint
+import android.content.Context
+import android.os.SystemClock
+import org.json.JSONObject
+import java.io.File
+import java.util.concurrent.locks.ReentrantLock
+import kotlin.concurrent.withLock
+
+/**
+ * 应用日志记录器 - Kotlin版本
+ * 优化点:
+ * 1. 优化文件读写操作,使用缓冲区
+ * 2. 减少锁持有时间
+ * 3. 优化JSON操作
+ * 4. 改进异常处理和资源管理
+ */
+class AppLogger private constructor(context: Context) {
+    
+    private val context: Context = context.applicationContext
+    
+    // 全局文件锁,防止多个方法同时读写文件
+    private val fileLock = ReentrantLock()
+    
+    companion object {
+        private const val LOG_FOLDER = "boost_logs"
+        private const val CURRENT_APP_LOG_FILE = "app.json"
+        private const val BUFFER_SIZE = 8192
+
+        @SuppressLint("StaticFieldLeak")
+        @Volatile
+        private var instance: AppLogger? = null
+        
+        fun getInstance(context: Context): AppLogger {
+            return instance ?: synchronized(this) {
+                instance ?: AppLogger(context).also { instance = it }
+            }
+        }
+    }
+    
+    /**
+     * 获取app.json文件路径
+     */
+    private fun getAppLogFile(): File {
+        val logDir = File(context.filesDir, LOG_FOLDER)
+        if (!logDir.exists()) {
+            logDir.mkdirs()
+        }
+        return File(logDir, CURRENT_APP_LOG_FILE)
+    }
+    
+    /**
+     * 读取app.json文件内容 - 优化版本
+     * 使用缓冲区提高读取效率
+     */
+    private fun readAppJsonFile(): JSONObject {
+        val appLogFile = getAppLogFile()
+        if (!appLogFile.exists()) {
+            throw RuntimeException("app.json file does not exist")
+        }
+        
+        val content = appLogFile.bufferedReader(bufferSize = BUFFER_SIZE).use { it.readText() }
+        
+        if (content.isEmpty()) {
+            throw RuntimeException("app.json file is empty")
+        }
+        
+        return JSONObject(content)
+    }
+    
+    /**
+     * 写入app.json文件内容 - 优化版本
+     */
+    private fun writeAppJsonFile(appData: JSONObject) {
+        val appLogFile = getAppLogFile()
+        appLogFile.bufferedWriter().use { writer ->
+            writer.write(appData.toString(2))
+            writer.flush()
+        }
+    }
+    
+    /**
+     * 安全地更新app.json文件 - 优化版本
+     * 减少锁持有时间
+     */
+    private inline fun safeUpdateAppJson(operationName: String, crossinline update: (JSONObject) -> Unit) {
+        fileLock.withLock {
+            try {
+                val appData = readAppJsonFile()
+                
+                val sessionInfo = appData.optJSONObject("sessionInfo")
+                if (sessionInfo == null) {
+                    VLog.w(TAG, "$operationName sessionInfo not found in app.json")
+                    return
+                }
+                
+                // 执行具体的更新操作
+                try {
+                    update(sessionInfo)
+                } catch (e: Exception) {
+                    VLog.e(TAG, "$operationName Error in update callback: ${e.message}")
+                    return
+                }
+                
+                // 保存更新后的文件
+                writeAppJsonFile(appData)
+                VLog.i(TAG, "$operationName successfully updated app.json")
+                
+            } catch (e: Exception) {
+                VLog.e(TAG, "$operationName Error updating app.json: ${e.message}")
+            }
+        }
+    }
+    
+    /**
+     * 安全地更新JSONObject - 优化版本
+     */
+    private fun safePut(jsonObject: JSONObject?, key: String, value: Any?, operationName: String) {
+        if (jsonObject == null) {
+            VLog.e(TAG, "$operationName JSONObject is null, cannot put key: $key")
+            return
+        }
+        
+        try {
+            jsonObject.put(key, value)
+        } catch (e: Exception) {
+            VLog.e(TAG, "$operationName Failed to put key '$key' with value '$value': ${e.message}")
+        }
+    }
+    
+    /**
+     * 更新app.json文件中的boostStopTime、boostDuration
+     * 通过 app.json 中的 boostStartTime 和 elapsedRealtime 计算
+     * boostStopTime = SystemClock.elapsedRealtime() - elapsedRealtime + boostStartTime
+     */
+    fun updateAppJsonStopTime() {
+        safeUpdateAppJson("updateAppJsonStopTime") { sessionInfo ->
+            val boostStartTime = sessionInfo.optLong("boostStartTime", 0)
+            val boostSuccessTime = sessionInfo.optLong("boostSuccessTime", 0)
+            val savedElapsedRealtime = sessionInfo.optLong("elapsedRealtime", 0)
+            
+            if (boostStartTime == 0L) {
+                throw RuntimeException("boostStartTime not found or invalid in app.json")
+            }
+            
+            // 计算 boostStopTime
+            var boostStopTime = if (savedElapsedRealtime != 0L) {
+                // 使用保存的 elapsedRealtime 计算精确的停止时间
+                SystemClock.elapsedRealtime() - savedElapsedRealtime + boostStartTime
+            } else {
+                // 降级使用系统时间
+                System.currentTimeMillis()
+            }
+            
+            // 计算 boostDuration
+            val boostDuration: Long
+            if (boostSuccessTime != 0L) {
+                if (boostStopTime < boostSuccessTime) {
+                    boostStopTime = boostSuccessTime
+                }
+                boostDuration = (boostStopTime - boostSuccessTime) / 1000
+            } else {
+                if (boostStopTime < boostStartTime) {
+                    boostStopTime = boostStartTime
+                }
+                boostDuration = (boostStopTime - boostStartTime) / 1000
+            }
+            
+            safePut(sessionInfo, "boostStopTime", boostStopTime, "updateAppJsonStopTime")
+            safePut(sessionInfo, "boostDuration", boostDuration, "updateAppJsonStopTime")
+        }
+    }
+    
+    /**
+     * 更新app.json文件中的success、errorCode、boostSuccessTime
+     * 通过 app.json 中的 boostStartTime 和 elapsedRealtime 计算
+     * boostSuccessTime = SystemClock.elapsedRealtime() - elapsedRealtime + boostStartTime
+     */
+    fun updateAppJsonStatusInfo(success: Boolean, errorCode: Long) {
+        safeUpdateAppJson("updateAppJsonStatusInfo") { sessionInfo ->
+            safePut(sessionInfo, "success", success, "updateAppJsonStatusInfo")
+            safePut(sessionInfo, "errorCode", errorCode, "updateAppJsonStatusInfo")
+            
+            val boostStartTime = sessionInfo.optLong("boostStartTime", 0)
+            val savedElapsedRealtime = sessionInfo.optLong("elapsedRealtime", 0)
+            
+            if (boostStartTime == 0L) {
+                throw RuntimeException("boostStartTime not found or invalid in app.json")
+            }
+            
+            // 计算 boostSuccessTime
+            var boostSuccessTime = if (savedElapsedRealtime != 0L) {
+                // 使用保存的 elapsedRealtime 计算精确的成功时间
+                SystemClock.elapsedRealtime() - savedElapsedRealtime + boostStartTime
+            } else {
+                // 降级使用系统时间
+                System.currentTimeMillis()
+            }
+            
+            if (success) {
+                // 校验时间合理性:不能早于开始时间,也不能晚于开始时间超过3分钟
+                if (boostSuccessTime < boostStartTime || boostSuccessTime - 180000 > boostStartTime) {
+                    boostSuccessTime = boostStartTime
+                }
+                safePut(sessionInfo, "boostSuccessTime", boostSuccessTime, "updateAppJsonStatusInfo")
+                safePut(sessionInfo, "boostStopTime", boostSuccessTime, "updateAppJsonStatusInfo")
+                safePut(sessionInfo, "boostDuration", 0, "updateAppJsonStatusInfo")
+            }
+        }
+    }
+    
+    /**
+     * 更新app.json文件中的boostDownloadDuration - 优化版本
+     */
+    fun updateAppJsonDownloadDuration(downloadDuration: Long) {
+        safeUpdateAppJson("updateAppJsonDownloadDuration") { sessionInfo ->
+            safePut(sessionInfo, "boostDownloadDuration", downloadDuration, "updateAppJsonDownloadDuration")
+        }
+    }
+    
+    /**
+     * 更新app.json文件中的errorCode - 优化版本
+     */
+    fun updateAppJsonCode(code: Int) {
+        safeUpdateAppJson("updateAppJsonCode") { sessionInfo ->
+            safePut(sessionInfo, "errorCode", code, "updateAppJsonCode")
+        }
+    }
+}
+
+private const val TAG = "AppLogger"

+ 8 - 0
android/app/src/main/kotlin/app/xixi/nomo/Broadcasts.kt

@@ -0,0 +1,8 @@
+package app.xixi.nomo
+
+const val STATUS_BROADCAST = "app.xixi.nomo.STATUS"
+const val TIMER_BROADCAST = "app.xixi.nomo.TIMER"
+
+const val APP_FOREGROUND_BROADCAST = "app.xixi.nomo.APP_FOREGROUND"
+
+const val BOOST_RESULT_BROADCAST = "app.xixi.nomo.BOOST_RESULT"

+ 14 - 18
android/app/src/main/kotlin/app/xixi/nomo/CoreApi.g.kt

@@ -62,14 +62,13 @@ val CoreApiPigeonMethodCodec = StandardMethodCodec(CoreApiPigeonCodec())
 interface CoreApi {
   fun getApps(callback: (Result<String?>) -> Unit)
   fun getSystemLocale(): String?
-  fun connect(sessionId: String, socksPort: Long, tunnelConfig: String, configJson: String): Boolean?
+  fun connect(sessionId: String, socksPort: Long, tunnelConfig: String, configJson: String, remainTime: Long, isCountdown: Boolean, allowVpnApps: List<String>, disallowVpnApps: List<String>, accessToken: String, aesKey: String, aesIv: String, locationId: Long, locationCode: String, baseUrls: List<String>, params: String, peekTimeInterval: Long): Boolean?
   fun disconnect(): Boolean?
   fun getRemoteIp(): String?
   fun getAdvertisingId(): String?
   fun moveTaskToBack(): Boolean?
   fun isConnected(): Boolean?
   fun getSimInfo(): String?
-  fun reconnect(): Boolean?
   fun getChannel(): String?
 
   companion object {
@@ -123,8 +122,20 @@ interface CoreApi {
             val socksPortArg = args[1] as Long
             val tunnelConfigArg = args[2] as String
             val configJsonArg = args[3] as String
+            val remainTimeArg = args[4] as Long
+            val isCountdownArg = args[5] as Boolean
+            val allowVpnAppsArg = args[6] as List<String>
+            val disallowVpnAppsArg = args[7] as List<String>
+            val accessTokenArg = args[8] as String
+            val aesKeyArg = args[9] as String
+            val aesIvArg = args[10] as String
+            val locationIdArg = args[11] as Long
+            val locationCodeArg = args[12] as String
+            val baseUrlsArg = args[13] as List<String>
+            val paramsArg = args[14] as String
+            val peekTimeIntervalArg = args[15] as Long
             val wrapped: List<Any?> = try {
-              listOf(api.connect(sessionIdArg, socksPortArg, tunnelConfigArg, configJsonArg))
+              listOf(api.connect(sessionIdArg, socksPortArg, tunnelConfigArg, configJsonArg, remainTimeArg, isCountdownArg, allowVpnAppsArg, disallowVpnAppsArg, accessTokenArg, aesKeyArg, aesIvArg, locationIdArg, locationCodeArg, baseUrlsArg, paramsArg, peekTimeIntervalArg))
             } catch (exception: Throwable) {
               CoreApiPigeonUtils.wrapError(exception)
             }
@@ -224,21 +235,6 @@ interface CoreApi {
           channel.setMessageHandler(null)
         }
       }
-      run {
-        val channel = BasicMessageChannel<Any?>(binaryMessenger, "dev.flutter.pigeon.app.xixi.nomo.CoreApi.reconnect$separatedMessageChannelSuffix", codec)
-        if (api != null) {
-          channel.setMessageHandler { _, reply ->
-            val wrapped: List<Any?> = try {
-              listOf(api.reconnect())
-            } catch (exception: Throwable) {
-              CoreApiPigeonUtils.wrapError(exception)
-            }
-            reply.reply(wrapped)
-          }
-        } else {
-          channel.setMessageHandler(null)
-        }
-      }
       run {
         val channel = BasicMessageChannel<Any?>(binaryMessenger, "dev.flutter.pigeon.app.xixi.nomo.CoreApi.getChannel$separatedMessageChannelSuffix", codec)
         if (api != null) {

+ 89 - 55
android/app/src/main/kotlin/app/xixi/nomo/CoreApiImpl.kt

@@ -18,7 +18,7 @@ import android.telephony.TelephonyManager
 import android.util.Base64
 import android.util.Log
 import androidx.core.graphics.createBitmap
-import app.xixi.nomo.XRayApi.Companion.VPN_STATE_PERMISSION_DENIED
+import app.xixi.nomo.XRayApi.Companion.VPN_STATE_ERROR
 import app.xixi.nomo.XRayApi.OnVpnServiceEvent
 import com.google.gson.Gson
 import com.google.gson.JsonArray
@@ -40,6 +40,7 @@ import java.util.Map
 data class VpnStatusMessage(
     val type: String,
     val status: Long,
+    val code: Long,
     val message: String
 )
 
@@ -51,6 +52,14 @@ data class TimerUpdateMessage(
     val isPaused: Boolean
 )
 
+data class BoostResultMessage(
+    val type: String,
+    val param: String,
+    val success: Boolean,
+    val locationCode: String,
+    val nodeId: String
+)
+
 class CoreApiImpl(private val activity: Activity) : CoreApi {
     companion object {
         private const val TAG = "CoreApiImpl"
@@ -63,10 +72,7 @@ class CoreApiImpl(private val activity: Activity) : CoreApi {
     // xray服务
     private var xrayApi: XRayApi? = null
 
-    private var sessionId: String = ""
-    private var socksPort: Long = 10808
-    private var tunnelConfig: String = ""
-    private var configJson: String = ""
+    private var xrayConfig: XrayConfig? = null
 
     
     // 事件流处理器实现
@@ -89,10 +95,11 @@ class CoreApiImpl(private val activity: Activity) : CoreApi {
     }
 
     // 通知 Flutter VPN状态变化
-    private fun notifyVpnStatusChange(status: Long, message: String) {
+    private fun notifyVpnStatusChange(status: Long, code: Long, message: String) {
         val vpnMessage = VpnStatusMessage(
             type = "vpn_status",
             status = status,
+            code = code,
             message = message
         )
         notifyFlutter(vpnMessage)
@@ -110,13 +117,25 @@ class CoreApiImpl(private val activity: Activity) : CoreApi {
         notifyFlutter(timerMessage)
     }
 
+    // 通知 Flutter 上报结果
+    private fun notifyBoostResult(param: String, success: Boolean, locationCode: String, nodeId: String) {
+        val boostResultMessage = BoostResultMessage(
+            type = "boost_result",
+            param = param,
+            success = success,
+            locationCode = locationCode,
+            nodeId = nodeId,
+        )
+        notifyFlutter(boostResultMessage)
+    }
+
     // 通用通知方法
     private fun notifyFlutter(message: Any) {
         eventSink?.let { sink ->
             try {
                 val json = Gson().toJson(message)
                 sink.success(json)
-                Log.d(TAG, "已通知 Flutter: $json")
+//                Log.d(TAG, "已通知 Flutter: $json")
             } catch (e: Exception) {
                 Log.e(TAG, "通知 Flutter 失败", e)
             }
@@ -129,6 +148,12 @@ class CoreApiImpl(private val activity: Activity) : CoreApi {
     fun initXrayApi() {
         VLog.i(TAG, "CoreApiImpl.initXrayApi() 创建 XRayApi 实例")
         xrayApi = XRayApi()
+        xrayApi?.init(activity)
+    }
+
+    fun unInitXrayApi() {
+        xrayApi?.unInit()
+        xrayApi = null
     }
 
     /**
@@ -136,27 +161,27 @@ class CoreApiImpl(private val activity: Activity) : CoreApi {
      */
     fun setVpnServiceEventListener() {
         xrayApi?.setVpnServiceEventListener(object : OnVpnServiceEvent {
-            override fun onVpnStatusChange(status: Long, message: String) {
-                VLog.i(TAG, "接收到VPN状态变化: status=$status, message=$message")
+            override fun onVpnStatusChange(status: Long, code: Long, message: String) {
+                VLog.i(TAG, "接收到VPN状态变化: status=$status, code=$code, message=$message")
                 // 同时发送详细的状态信息
-                notifyVpnStatusChange(status, message)
+                notifyVpnStatusChange(status, code, message)
             }
             
             override fun onTimerUpdate(currentTime: Long, mode: Int, isRunning: Boolean, isPaused: Boolean) {
-                VLog.i(TAG, "接收到计时更新: time=$currentTime, mode=$mode, running=$isRunning, paused=$isPaused")
                 notifyTimerUpdate(currentTime, mode, isRunning, isPaused)
             }
-        })
-    }
 
-    fun xrayInit() {
-        VLog.i(TAG, "CoreApiImpl.xrayInit() 被调用")
-        xrayApi?.init(activity)
-        VLog.i(TAG, "CoreApiImpl.xrayInit() 调用完成")
-    }
+            override fun onBoostResult(
+                param: String,
+                success: Boolean,
+                locationCode: String,
+                nodeId: String
+            ) {
+                VLog.i(TAG, "收到加速结果: nodeId=$nodeId, locationCode=$locationCode, success=$success, param=$param")
+                notifyBoostResult(param, success, locationCode, nodeId)
+            }
 
-    fun xrayUnInit() {
-        xrayApi?.uninit()
+        })
     }
 
     override fun getSystemLocale(): String? {
@@ -235,11 +260,43 @@ class CoreApiImpl(private val activity: Activity) : CoreApi {
         return bos.toByteArray()
     }
 
-    override fun connect(sessionId: String, socksPort: Long, tunnelConfig: String, configJson: String): Boolean? {
-        this.sessionId = sessionId
-        this.socksPort = socksPort
-        this.tunnelConfig = tunnelConfig
-        this.configJson = configJson
+    override fun connect(
+        sessionId: String,
+        socksPort: Long,
+        tunnelConfig: String,
+        configJson: String,
+        remainTime: Long,
+        isCountdown: Boolean,
+        allowVpnApps: List<String>,
+        disallowVpnApps: List<String>,
+        accessToken: String,
+        aesKey: String,
+        aesIv: String,
+        locationId: Long,
+        locationCode: String,
+        baseUrls: List<String>,
+        params: String,
+        peekTimeInterval: Long
+    ): Boolean? {
+        this.xrayConfig = XrayConfig(
+            sessionId = sessionId,
+            socksPort = socksPort.toInt(),
+            tunnelConfig = tunnelConfig,
+            configJson = configJson,
+            remainTime = remainTime,
+            isCountdown = isCountdown,
+            allowVpnApps = allowVpnApps as ArrayList<String>,
+            disallowVpnApps = disallowVpnApps as ArrayList<String>,
+            peekTimeInterval = peekTimeInterval.toInt(),
+            accessToken = accessToken,
+            aesKey = aesKey,
+            aesIv = aesIv,
+            locationId = locationId.toInt(),
+            locationCode = locationCode,
+            baseUrls = baseUrls as ArrayList<String>,
+            params = params,
+        )
+
         Log.i(TAG, "Starting V2Ray with permission check")
         // 检查VPN权限
         val intent = VpnService.prepare(activity)
@@ -261,7 +318,6 @@ class CoreApiImpl(private val activity: Activity) : CoreApi {
         // 停止V2Ray服务
         VLog.i(TAG, "调用 xrayApi.stopXray()")
         xrayApi?.stopXray()
-        xrayUnInit()
         VLog.i(TAG, "V2Ray 服务停止完成")
 
         return true
@@ -337,19 +393,10 @@ class CoreApiImpl(private val activity: Activity) : CoreApi {
         }
     }
 
-    override fun reconnect(): Boolean? {
-        xrayInit()
-        return true
-    }
-
     fun startV2RayService() {
         VLog.i(TAG, "============ 开始启动 V2Ray 服务 ============")
         
-        // 1. 初始化 XRay (启动和绑定服务)
-        VLog.i(TAG, "步骤 1: 调用 xrayInit() 初始化服务")
-        xrayInit()
-        
-        // 2. 启动 XRay (发送启动消息)
+        // 1. 启动 XRay (发送启动消息)
         // 注意:startXray() 内部已经处理了等待服务连接的逻辑
         // 如果服务还在连接中 (XRAY_SVR_CONNECT_SERVICE),会注册回调等待
         // 如果服务已连接 (XRAY_SVR_SERVICE_WORKING),会直接发送启动消息
@@ -363,16 +410,8 @@ class CoreApiImpl(private val activity: Activity) : CoreApi {
     // 启动V2Ray服务
     private fun startXray() {
         VLog.i(TAG, "CoreApiImpl.startXray() 被调用,开始准备配置")
-        
-        val config = XrayConfig()
-        config.sessionId = sessionId
-        config.socksPort = socksPort.toInt()
-        config.tunnelConfig = tunnelConfig
-        
-        VLog.i(TAG, "sessionId: ${config.sessionId}")
-        VLog.i(TAG, "socksPort: ${config.socksPort}")
-        VLog.i(TAG, "tunnelConfig: ${config.tunnelConfig}")
-        VLog.i(TAG, "configJson: $configJson")
+
+        VLog.i(TAG, "XrayConfig: ${xrayConfig?.toJson()}")
 
         var geoPath = ""
         if (File(activity.filesDir, "geo/geoip.dat").exists() && File(activity.filesDir, "geo/geosite.dat").exists()) {
@@ -381,20 +420,15 @@ class CoreApiImpl(private val activity: Activity) : CoreApi {
         VLog.i(TAG, "geoDir: $geoPath")
         val startOptions = Map.of<String?, Any?>(
             "geoPath", geoPath,
-            "nodesConfig", configJson
+            "nodesConfig", xrayConfig?.configJson ?: ""
         )
-        config.startOptions = Gson().toJson(startOptions)
+        xrayConfig?.startOptions = Gson().toJson(startOptions)
         
         VLog.i(TAG, "配置准备完成,调用 xrayApi.startXray()")
-        val result = xrayApi?.startXray(config)
+        val result = xrayConfig?.let { xrayApi?.startXray(it) }
         VLog.i(TAG, "xrayApi.startXray() 返回结果: $result")
     }
 
-    // 解绑XRay服务
-    fun unbindXrayService() {
-        xrayApi?.unbindXrayService()
-    }
-
     fun safeStringToJsonArray(jsonString: String?): JsonArray? {
         if (jsonString.isNullOrBlank()) return null
 
@@ -431,7 +465,7 @@ class CoreApiImpl(private val activity: Activity) : CoreApi {
                 startV2RayService()
             } else {
                 Log.w(TAG, "VPN permission denied")
-                notifyVpnStatusChange(VPN_STATE_PERMISSION_DENIED, "")
+                notifyVpnStatusChange(VPN_STATE_ERROR, ERROR_PERMISSION_DENIED, "VPN permission denied")
             }
         }
     }

+ 148 - 0
android/app/src/main/kotlin/app/xixi/nomo/CryptoUtils.kt

@@ -0,0 +1,148 @@
+package app.xixi.nomo
+
+import android.util.Base64
+import java.io.ByteArrayInputStream
+import java.io.ByteArrayOutputStream
+import java.nio.charset.StandardCharsets
+import java.util.zip.GZIPInputStream
+import java.util.zip.GZIPOutputStream
+import javax.crypto.Cipher
+import javax.crypto.spec.IvParameterSpec
+import javax.crypto.spec.SecretKeySpec
+
+/**
+ * 加密工具类 - Kotlin 优化版本
+ */
+object CryptoUtils {
+    private const val TAG = "CryptoUtils"
+    private const val AES_ALGORITHM = "AES/CTR/NoPadding"
+    private const val AES_KEY_ALGORITHM = "AES"
+    private const val BUFFER_SIZE = 8192
+    private const val GZIP_BUFFER_SIZE = 1024
+
+    private fun validateParams(key: String, iv: String) {
+        require(key.isNotEmpty()) { "Key cannot be null or empty" }
+        require(iv.isNotEmpty()) { "IV cannot be null or empty" }
+    }
+
+    private fun encryptBytes(data: ByteArray, key: String, iv: String): ByteArray {
+        require(data.isNotEmpty()) { "Data cannot be null or empty" }
+        validateParams(key, iv)
+
+        return try {
+            val keyBytes = Base64.decode(key, Base64.DEFAULT)
+            val ivBytes = Base64.decode(iv, Base64.DEFAULT)
+
+            require(keyBytes.size in listOf(16, 24, 32)) { "Invalid key length: ${keyBytes.size}" }
+            require(ivBytes.size == 16) { "Invalid IV length: ${ivBytes.size}" }
+
+            val secretKey = SecretKeySpec(keyBytes, AES_KEY_ALGORITHM)
+            val ivSpec = IvParameterSpec(ivBytes)
+
+            val cipher = Cipher.getInstance(AES_ALGORITHM)
+            cipher.init(Cipher.ENCRYPT_MODE, secretKey, ivSpec)
+
+            cipher.doFinal(data)
+        } catch (e: IllegalArgumentException) {
+            throw e
+        } catch (e: Exception) {
+            VLog.e(TAG, "Encryption failed: ${e.message}")
+            throw RuntimeException("Encryption failed", e)
+        }
+    }
+
+    private fun decryptBytes(encryptedData: ByteArray, key: String, iv: String): ByteArray {
+        require(encryptedData.isNotEmpty()) { "Encrypted data cannot be null or empty" }
+        validateParams(key, iv)
+
+        return try {
+            val keyBytes = Base64.decode(key, Base64.DEFAULT)
+            val ivBytes = Base64.decode(iv, Base64.DEFAULT)
+
+            val secretKey = SecretKeySpec(keyBytes, AES_KEY_ALGORITHM)
+            val ivSpec = IvParameterSpec(ivBytes)
+
+            val cipher = Cipher.getInstance(AES_ALGORITHM)
+            cipher.init(Cipher.DECRYPT_MODE, secretKey, ivSpec)
+
+            cipher.doFinal(encryptedData)
+        } catch (e: IllegalArgumentException) {
+            throw e
+        } catch (e: Exception) {
+            VLog.e(TAG, "Decryption failed: ${e.message}")
+            throw RuntimeException("Decryption failed", e)
+        }
+    }
+
+    private fun compressBytes(data: ByteArray?): ByteArray? {
+        if (data == null || data.isEmpty()) {
+            return data
+        }
+
+        return try {
+            ByteArrayOutputStream(data.size).use { byteStream ->
+                GZIPOutputStream(byteStream).use { gzipStream ->
+                    gzipStream.write(data)
+                    gzipStream.finish()
+                }
+                byteStream.toByteArray()
+            }
+        } catch (e: Exception) {
+            VLog.e(TAG, "Compression failed: ${e.message}")
+            throw RuntimeException("Compression failed", e)
+        }
+    }
+
+    private fun decompressBytes(compressedData: ByteArray?): ByteArray? {
+        if (compressedData == null || compressedData.isEmpty()) {
+            return compressedData
+        }
+
+        return try {
+            ByteArrayInputStream(compressedData).use { byteStream ->
+                GZIPInputStream(byteStream, GZIP_BUFFER_SIZE).use { gzipStream ->
+                    ByteArrayOutputStream().use { outputStream ->
+                        gzipStream.copyTo(outputStream, BUFFER_SIZE)
+                        outputStream.toByteArray()
+                    }
+                }
+            }
+        } catch (e: Exception) {
+            VLog.e(TAG, "Decompression failed: ${e.message}")
+            throw RuntimeException("Decompression failed", e)
+        }
+    }
+
+    fun compressAndEncrypt(text: String, key: String, iv: String): ByteArray {
+        require(text.isNotEmpty()) { "Text cannot be null or empty" }
+        validateParams(key, iv)
+
+        return try {
+            val textBytes = text.toByteArray(StandardCharsets.UTF_8)
+            val compressedBytes = compressBytes(textBytes)!!
+            encryptBytes(compressedBytes, key, iv)
+        } catch (e: IllegalArgumentException) {
+            throw e
+        } catch (e: Exception) {
+            VLog.e(TAG, "Compress and encrypt failed: ${e.message}")
+            throw RuntimeException("Compress and encrypt failed", e)
+        }
+    }
+
+    fun decryptAndDecompress(encryptedData: ByteArray, key: String, iv: String): String {
+        require(encryptedData.isNotEmpty()) { "Encrypted data cannot be null or empty" }
+        validateParams(key, iv)
+
+        return try {
+            val decryptedBytes = decryptBytes(encryptedData, key, iv)
+            val decompressedBytes = decompressBytes(decryptedBytes)!!
+            String(decompressedBytes, StandardCharsets.UTF_8)
+        } catch (e: IllegalArgumentException) {
+            throw e
+        } catch (e: Exception) {
+            VLog.e(TAG, "Decrypt and decompress failed: ${e.message}")
+            throw RuntimeException("Decrypt and decompress failed", e)
+        }
+    }
+}
+

+ 10 - 0
android/app/src/main/kotlin/app/xixi/nomo/ErrorCode.kt

@@ -0,0 +1,10 @@
+package app.xixi.nomo
+
+const val ERROR_INIT = 1100L // vpn初始化失败
+const val ERROR_KILL = 1101L // 服务异常kill
+const val ERROR_REVOKE = 1102L // 系统强杀
+const val ERROR_SERVICE_EMPTY = 1103L // 服务器节点返回空
+const val ERROR_ROUTER = 1104L // 调度失败
+const val ERROR_PERMISSION_DENIED = 1105L // 拒绝权限
+
+const val ERROR_REMAIN_TIME = 1106L // 没有可用时间

+ 55 - 0
android/app/src/main/kotlin/app/xixi/nomo/LifecycleVpnService.kt

@@ -0,0 +1,55 @@
+package app.xixi.nomo
+
+import android.content.Intent
+import android.net.VpnService
+import android.os.IBinder
+import androidx.annotation.CallSuper
+import androidx.lifecycle.Lifecycle
+import androidx.lifecycle.LifecycleOwner
+import androidx.lifecycle.ServiceLifecycleDispatcher
+
+/**
+ * Based on [androidx.lifecycle.LifecycleService]
+ */
+open class LifecycleVpnService : VpnService(), LifecycleOwner {
+    @Suppress("LeakingThis")
+    private val dispatcher = ServiceLifecycleDispatcher(this)
+
+    @CallSuper
+    override fun onCreate() {
+        dispatcher.onServicePreSuperOnCreate()
+        super.onCreate()
+    }
+
+    @CallSuper
+    override fun onBind(intent: Intent): IBinder? {
+        dispatcher.onServicePreSuperOnBind()
+        return super.onBind(intent)
+    }
+
+    @Deprecated("Deprecated in Java")
+    @CallSuper
+    override fun onStart(intent: Intent?, startId: Int) {
+        dispatcher.onServicePreSuperOnStart()
+        @Suppress("DEPRECATION")
+        super.onStart(intent, startId)
+    }
+
+    // this method is added only to annotate it with @CallSuper.
+    // In usual Service, super.onStartCommand is no-op, but in LifecycleService
+    // it results in dispatcher.onServicePreSuperOnStart() call, because
+    // super.onStartCommand calls onStart().
+    @CallSuper
+    override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
+        return super.onStartCommand(intent, flags, startId)
+    }
+
+    @CallSuper
+    override fun onDestroy() {
+        dispatcher.onServicePreSuperOnDestroy()
+        super.onDestroy()
+    }
+
+    override val lifecycle: Lifecycle
+        get() = dispatcher.lifecycle
+}

+ 5 - 5
android/app/src/main/kotlin/app/xixi/nomo/MainActivity.kt

@@ -27,11 +27,6 @@ class MainActivity: FlutterActivity() {
 //        }
 //        return engine
 //    }
-
-    override fun detachFromFlutterEngine() {
-        super.detachFromFlutterEngine()
-        coreApiImpl.unbindXrayService()
-    }
     
     override fun onCreate(savedInstanceState: android.os.Bundle?) {
         super.onCreate(savedInstanceState)
@@ -111,4 +106,9 @@ class MainActivity: FlutterActivity() {
         super.onActivityResult(requestCode, resultCode, data)
         coreApiImpl.onActivityResult(requestCode, resultCode, data)
     }
+
+    override fun onDestroy() {
+        coreApiImpl.unInitXrayApi()
+        super.onDestroy()
+    }
 }

+ 468 - 0
android/app/src/main/kotlin/app/xixi/nomo/NetworkReporter.kt

@@ -0,0 +1,468 @@
+package win.fkey.netboost.service
+
+import app.xixi.nomo.CryptoUtils
+import app.xixi.nomo.VLog
+import app.xixi.nomo.XrayConfig
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.launch
+import kotlinx.coroutines.withContext
+import org.apache.commons.io.IOUtils
+import org.json.JSONArray
+import org.json.JSONObject
+import java.io.IOException
+import java.net.HttpURLConnection
+import java.net.URL
+import java.util.UUID
+import java.util.concurrent.atomic.AtomicInteger
+
+/**
+ * 网络请求上报类 - Kotlin 优化版本
+ * 使用协程处理异步请求
+ */
+class NetworkReporter private constructor() {
+
+    companion object {
+        private const val TAG = "NetworkReporter"
+        private const val CONNECT_TIMEOUT = 15000
+        private const val READ_TIMEOUT = 15000
+
+        @Volatile
+        private var instance: NetworkReporter? = null
+
+        fun getInstance(): NetworkReporter {
+            return instance ?: synchronized(this) {
+                instance ?: NetworkReporter().also { instance = it }
+            }
+        }
+    }
+
+    private var baseUrls: List<String> = emptyList()
+    private var token: String? = null
+    private var cachedJsonObject: JSONObject? = null
+
+    // 加密相关参数
+    private var aesKey: String? = null
+    private var aesIv: String? = null
+    private var enableEncryption = false
+
+    // 选择节点数据
+    private var locationId: Int = 0
+    private var locationCode: String? = null
+
+    private var connectedNodeId = ""
+
+    // 动态参数
+    private val peekLogIndex = AtomicInteger(0)
+
+    // 不同模块的items参数配置
+    private val itemsConfigs = HashMap<String, ItemsParamConfig>()
+
+
+    fun getLocationCode() : String? {
+        return locationCode
+    }
+
+    fun getConnectedNodeId() : String? {
+        return connectedNodeId
+    }
+
+    data class ItemsParamConfig(
+        val module: String,
+        var category: String = "nomo",
+        var level: String = "info",
+        val defaultFields: MutableMap<String, Any> = HashMap(),
+        var useDynamicFields: Boolean = false
+    ) {
+        fun addDefaultField(key: String, value: Any): ItemsParamConfig {
+            defaultFields[key] = value
+            return this
+        }
+    }
+
+    interface ReportCallback {
+        fun onSuccess(response: String)
+        fun onFailure(error: String)
+    }
+
+    init {
+        resetPeekLogIndex()
+    }
+
+    fun initialize(xrayConfig: XrayConfig) {
+        this.baseUrls = xrayConfig.baseUrls
+        this.token = xrayConfig.accessToken
+        resetPeekLogIndex()
+        VLog.i(TAG, "NetworkReporter initialized with ${this.baseUrls.size} base URLs")
+        setEncryptionParams(xrayConfig.aesKey, xrayConfig.aesIv)
+        setCachedParams(xrayConfig.params)
+        setLocationParams(xrayConfig.locationId, xrayConfig.locationCode)
+    }
+
+    fun setEncryptionParams(aesKey: String?, aesIv: String?) {
+        this.aesKey = aesKey
+        this.aesIv = aesIv
+        this.enableEncryption = !aesKey.isNullOrEmpty() && !aesIv.isNullOrEmpty()
+        if (this.enableEncryption) {
+            VLog.i(TAG, "Encryption enabled")
+        }
+    }
+
+    fun setLocationParams(locationId: Int, locationCode: String?) {
+        this.locationId = locationId
+        this.locationCode = locationCode
+    }
+
+    fun disableEncryption() {
+        this.enableEncryption = false
+        VLog.i(TAG, "Encryption disabled")
+    }
+
+    fun setCachedParams(jsonParams: String?) {
+        if (jsonParams.isNullOrEmpty()) {
+            this.cachedJsonObject = null
+            return
+        }
+        try {
+            this.cachedJsonObject = JSONObject(jsonParams)
+        } catch (e: Exception) {
+            VLog.e(TAG, "Failed to parse cached JSON parameters: ${e.message}")
+            this.cachedJsonObject = null
+        }
+    }
+
+    private fun configureItemsParams(module: String, config: ItemsParamConfig) {
+        synchronized(itemsConfigs) {
+            itemsConfigs[module] = config
+        }
+        VLog.i(TAG, "Configured items params for module: $module")
+    }
+
+    fun removeItemsConfig(module: String) {
+        synchronized(itemsConfigs) {
+            itemsConfigs.remove(module)
+        }
+        VLog.i(TAG, "Removed items config for module: $module")
+    }
+
+    private fun getItemsConfig(module: String): ItemsParamConfig {
+        return synchronized(itemsConfigs) {
+            itemsConfigs[module]
+        } ?: ItemsParamConfig(module = "default")
+    }
+
+    fun addPeekLogParams(module: String, stats: String?, boostSessionId: String?) {
+        try {
+            val config = ItemsParamConfig(module).apply {
+                useDynamicFields = true
+                addDefaultField("locationId", locationId)
+                addDefaultField("locationCode", locationCode ?: "")
+                addDefaultField("boostSessionId", boostSessionId ?: "")
+            }
+
+            if (!stats.isNullOrEmpty()) {
+                parseStatsJson(config, stats)
+            }
+            configureItemsParams(module, config)
+            VLog.i(TAG, "Successfully parsed realtimeSpeed JSON")
+        } catch (e: Exception) {
+            VLog.e(TAG, "addPeekLogParams Failed to parse realtimeSpeed JSON: ${e.message}")
+        }
+    }
+
+    fun addBoostResultParams(
+        module: String, stats: String?, boostSessionId: String?,
+        success: Boolean, code: Long
+    ) {
+        try {
+            val config = ItemsParamConfig(module).apply {
+                addDefaultField("success", success)
+                addDefaultField("code", code)
+                addDefaultField("locationId", locationId)
+                addDefaultField("locationCode", locationCode ?: "")
+                addDefaultField("boostSessionId", boostSessionId ?: "")
+            }
+
+            if (!stats.isNullOrEmpty()) {
+                parseStatsJson(config, stats)
+                parseConnectionHistory(config, stats)
+            }
+            configureItemsParams(module, config)
+            VLog.i(TAG, "Successfully parsed realtimeSpeed JSON")
+        } catch (e: Exception) {
+            VLog.e(TAG, "addBoostResultParams Failed to parse realtimeSpeed JSON: ${e.message}")
+        }
+    }
+
+    private fun parseStatsJson(config: ItemsParamConfig, stats: String) {
+        val jsonObject = JSONObject(stats)
+        if (jsonObject.has("maxSpeed")) {
+            val maxSpeedsObj = jsonObject.getJSONObject("maxSpeed")
+            val maxSpeeds = HashMap<String, Int>()
+            val keys = maxSpeedsObj.keys()
+            while (keys.hasNext()) {
+                val key = keys.next()
+                maxSpeeds[key] = maxSpeedsObj.getInt(key)
+            }
+            config.addDefaultField("maxSpeeds", maxSpeeds)
+        }
+    }
+
+    private fun parseConnectionHistory(config: ItemsParamConfig, realtimeSpeed: String) {
+        val jsonObject = JSONObject(realtimeSpeed)
+        if (jsonObject.has("connectionHistory")) {
+            val connectionHistoryArray = jsonObject.getJSONArray("connectionHistory")
+            val connectionHistory = ArrayList<Map<String, Any>>()
+            for (i in 0 until connectionHistoryArray.length()) {
+                val connectionObj = connectionHistoryArray.getJSONObject(i)
+                val connection = HashMap<String, Any>()
+                val keys = connectionObj.keys()
+                while (keys.hasNext()) {
+                    val key = keys.next()
+                    connection[key] = connectionObj.get(key)
+                }
+                connectionHistory.add(connection)
+            }
+            if(connectionHistory.isNotEmpty()) {
+                connectedNodeId = try {
+                    connectionHistory[connectionHistory.size - 1]["nodeId"].toString()
+                } catch (e: Exception) {
+                    ""
+                }
+            }
+            config.addDefaultField("connectionHistory", connectionHistory)
+        }
+    }
+
+    private fun resetPeekLogIndex() {
+        peekLogIndex.set(0)
+    }
+
+    fun report(endpoint: String, module: String, jsonParams: String?, callback: ReportCallback?) {
+        if (baseUrls.isEmpty()) {
+            callback?.onFailure("No base URLs configured")
+            return
+        }
+
+        // 使用 IO 协程执行请求
+        CoroutineScope(Dispatchers.IO).launch {
+            val jsonObject = parseJsonParams(jsonParams, callback) ?: return@launch
+
+            var lastException: Exception? = null
+            var success = false
+
+            for ((i, baseUrl) in baseUrls.withIndex()) {
+                val fullUrl = baseUrl + endpoint
+                VLog.i(TAG, "Trying to report to: $fullUrl (attempt ${i + 1})")
+
+                try {
+                    val response = makeRequest(fullUrl, module, jsonObject)
+                    VLog.i(TAG, "Report successful: module -> $module, url -> $fullUrl")
+                    withContext(Dispatchers.Main) {
+                        callback?.onSuccess(response)
+                    }
+                    success = true
+                    break
+                } catch (e: Exception) {
+                    lastException = e
+                    VLog.e(TAG, "Report failed: $fullUrl, error: ${e.message}")
+                    if (i < baseUrls.size - 1) {
+                        VLog.i(TAG, "Trying next URL...")
+                    }
+                }
+            }
+
+            if (!success) {
+                val errorMsg = "All baseURLs failed to report: ${lastException?.message}"
+                VLog.e(TAG, errorMsg)
+                withContext(Dispatchers.Main) {
+                    callback?.onFailure(errorMsg)
+                }
+            }
+        }
+    }
+
+    private fun parseJsonParams(jsonParams: String?, callback: ReportCallback?): JSONObject? {
+        if (jsonParams != null) {
+            return try {
+                JSONObject(jsonParams)
+            } catch (e: Exception) {
+                VLog.e(TAG, "Failed to parse JSON parameters: ${e.message}")
+                callback?.onFailure("Invalid JSON parameters")
+                null
+            }
+        } else if (cachedJsonObject != null) {
+            return cachedJsonObject
+        } else {
+            callback?.onFailure("No JSON parameters provided")
+            return null
+        }
+    }
+
+    fun report(endpoint: String, jsonParams: String?, callback: ReportCallback?) {
+        report(endpoint, "FK_PeekLog", jsonParams, callback)
+    }
+
+    fun buildRequestParams(module: String): String? {
+        return try {
+            val jsonObject = JSONObject()
+            val finalJsonParams = getItemsParam(jsonObject, module)
+            VLog.i(TAG, "Built request parameters")
+            finalJsonParams
+        } catch (e: Exception) {
+            VLog.e(TAG, "Failed to build request parameters: ${e.message}")
+            null
+        }
+    }
+
+    private fun makeRequest(urlString: String, module: String, jsonObject: JSONObject): String {
+        var connection: HttpURLConnection? = null
+        try {
+            val url = URL(urlString)
+            connection = url.openConnection() as HttpURLConnection
+
+            connection.requestMethod = "POST"
+            connection.setRequestProperty("Content-Type", "application/json")
+            connection.setRequestProperty("X-NL-Product-Code", "fkey")
+            connection.setRequestProperty("X-NL-Content-Encoding", "gzip")
+
+            token?.takeIf { it.isNotEmpty() }?.let {
+                connection.setRequestProperty("Authorization", it)
+            }
+
+            connection.connectTimeout = CONNECT_TIMEOUT
+            connection.readTimeout = READ_TIMEOUT
+            connection.doOutput = true
+            connection.doInput = true
+
+            val finalJsonParams = insertItemsParam(jsonObject, module)
+
+            val requestBody = if (enableEncryption && !aesKey.isNullOrEmpty() && !aesIv.isNullOrEmpty()) {
+                try {
+                    CryptoUtils.compressAndEncrypt(finalJsonParams, aesKey!!, aesIv!!)
+                } catch (e: Exception) {
+                    VLog.e(TAG, "Failed to encrypt request: ${e.message}")
+                    throw IOException("Encryption failed", e)
+                }
+            } else {
+                finalJsonParams.toByteArray(Charsets.UTF_8)
+            }
+
+            connection.outputStream.use { os ->
+                os.write(requestBody)
+                os.flush()
+            }
+
+            val responseCode = connection.responseCode
+            VLog.i(TAG, "Response code: $responseCode")
+
+            val inputStream = if (responseCode in 200..299) connection.inputStream else connection.errorStream
+                ?: throw IOException("Response stream is null")
+
+            val inputByte = inputStream.use { IOUtils.toByteArray(it) }
+
+            val finalResponse = if (enableEncryption && !aesKey.isNullOrEmpty() && !aesIv.isNullOrEmpty()) {
+                try {
+                    CryptoUtils.decryptAndDecompress(inputByte, aesKey!!, aesIv!!)
+                } catch (e: Exception) {
+                    VLog.e(TAG, "Failed to decrypt response: ${e.message}")
+                    throw IOException("Decryption failed", e)
+                }
+            } else {
+                String(inputByte, Charsets.UTF_8)
+            }
+
+            if (responseCode in 200..299) {
+                return finalResponse
+            } else {
+                throw IOException("HTTP error: $responseCode, response: $finalResponse")
+            }
+
+        } finally {
+            connection?.disconnect()
+        }
+    }
+
+    private fun insertItemsParam(jsonObject: JSONObject, module: String): String {
+        return try {
+            val item = createItemObject(module)
+            val items = JSONArray().apply { put(item) }
+            jsonObject.put("items", items)
+            jsonObject.toString()
+        } catch (e: Exception) {
+            VLog.e(TAG, "Failed to insert items parameter: ${e.message}")
+            jsonObject.toString()
+        }
+    }
+
+    private fun getItemsParam(jsonObject: JSONObject, module: String): String {
+        return try {
+            val item = createItemObject(module)
+            val items = JSONArray().apply { put(item) }
+            items.toString()
+        } catch (e: Exception) {
+            VLog.e(TAG, "Failed to get items parameter: ${e.message}")
+            jsonObject.toString()
+        }
+    }
+
+    private fun createItemObject(module: String): JSONObject {
+        val config = getItemsConfig(module)
+
+        val currentIndex = if (config.useDynamicFields) peekLogIndex.incrementAndGet() else 0
+        val dynamicId = UUID.randomUUID().toString()
+        val time = System.currentTimeMillis()
+
+        val item = JSONObject().apply {
+            put("id", dynamicId)
+            put("time", time)
+            put("level", config.level)
+            put("module", config.module)
+            put("category", config.category)
+        }
+
+        val fields = JSONObject()
+        addDefaultFields(fields, config.defaultFields)
+
+        if (config.useDynamicFields) {
+            fields.put("peekLogIndex", currentIndex)
+        }
+        fields.put("generatedTime", time)
+
+        item.put("fields", fields)
+        return item
+    }
+
+    private fun addDefaultFields(fields: JSONObject, defaultFields: Map<String, Any>) {
+        defaultFields.forEach { (key, value) ->
+            when (value) {
+                is Map<*, *> -> {
+                    val jsonValue = JSONObject()
+                    (value as Map<String, Any>).forEach { (k, v) ->
+                        jsonValue.put(k, v)
+                    }
+                    fields.put(key, jsonValue)
+                }
+                is List<*> -> {
+                    val jsonArray = JSONArray()
+                    (value as List<Any>).forEach { item ->
+                        if (item is Map<*, *>) {
+                            val jsonItem = JSONObject()
+                            (item as Map<String, Any>).forEach { (k, v) ->
+                                jsonItem.put(k, v)
+                            }
+                            jsonArray.put(jsonItem)
+                        } else {
+                            jsonArray.put(item)
+                        }
+                    }
+                    fields.put(key, jsonArray)
+                }
+                else -> {
+                    fields.put(key, value)
+                }
+            }
+        }
+    }
+}
+

+ 41 - 0
android/app/src/main/kotlin/app/xixi/nomo/NotificationUtils.kt

@@ -0,0 +1,41 @@
+package app.xixi.nomo
+
+import android.app.Notification
+import android.app.NotificationChannel
+import android.app.NotificationManager
+import android.app.PendingIntent
+import android.content.Context
+import android.content.Intent
+import android.os.Build
+import androidx.annotation.StringRes
+import androidx.core.app.NotificationCompat
+
+fun registerNotificationChannel(context: Context, id: String, name: String) {
+    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
+        val manager = context.getSystemService(NotificationManager::class.java) ?: return
+
+        val channel = NotificationChannel(
+            id,
+            name,
+            NotificationManager.IMPORTANCE_DEFAULT
+        )
+        channel.enableLights(false)
+        channel.enableVibration(false)
+        channel.setShowBadge(false)
+
+        manager.createNotificationChannel(channel)
+    }
+}
+
+fun createConnectionNotification(
+    context: Context,
+    channelId: String,
+    title: String,
+    content: String,
+): Notification =
+    NotificationCompat.Builder(context, channelId)
+        .setSmallIcon(R.drawable.ic_xvpn)
+        .setSilent(true)
+        .setContentTitle(title)
+        .setContentText(content)
+        .build()

+ 7 - 0
android/app/src/main/kotlin/app/xixi/nomo/ServiceStatus.kt

@@ -0,0 +1,7 @@
+package app.xixi.nomo
+
+enum class ServiceStatus {
+    Disconnected,
+    Connected,
+    Failed,
+}

+ 1 - 1
android/app/src/main/kotlin/app/xixi/nomo/TProxyService.kt

@@ -40,7 +40,7 @@ internal class TProxyService {
         val configFileName = "hev-socks5-tunnel.yaml"
         val cfgFile = File(configFileDir, configFileName)
         try {
-            FileOutputStream(cfgFile, true).use { fos ->
+            FileOutputStream(cfgFile, false).use { fos ->
                 fos.write(configStr.toByteArray())
             }
         } catch (e: Exception) {

+ 121 - 262
android/app/src/main/kotlin/app/xixi/nomo/XRayApi.kt

@@ -1,338 +1,197 @@
 package app.xixi.nomo
 
 import android.app.ActivityManager
-import android.content.ComponentName
+import android.content.BroadcastReceiver
 import android.content.Context
 import android.content.Intent
-import android.content.ServiceConnection
+import android.content.IntentFilter
 import android.os.Build
 import android.os.Bundle
-import android.os.DeadObjectException
 import android.os.Handler
-import android.os.IBinder
 import android.os.Looper
-import android.os.Message
-import android.os.Messenger
-import java.util.concurrent.locks.ReentrantLock
-
+import androidx.core.content.ContextCompat
+import com.google.gson.Gson
 
+/**
+ * XRay VPN API - 简化版
+ * 使用广播机制与 XRayCoreService 通信
+ */
 class XRayApi {
-    private val lock = ReentrantLock()
-    private val mReplyMsgHandler = Messenger(ReplyMsgHandler(Looper.getMainLooper()))
     private var context: Context? = null
-    private var mService: Messenger? = null
-    private var vpnState = VPN_STATE_IDLE
-    private var xraySvrState = XRAY_SVR_IDLE
-    private var isInited = false
-    private var firstInit = true
-    private var serviceEvent: OnXrayServiceEvent? = null
     private var vpnServiceEvent: OnVpnServiceEvent? = null
-
-    private inner class ReplyMsgHandler(looper: Looper) : Handler(looper) {
-        override fun handleMessage(msg: Message) {
-            when (msg.what) {
-                XRayService.XRAY_MSG_REPLY_STATUS -> {
-                    val status = msg.data?.getLong("status") ?: -1L
-                    val message = msg.data?.getString("message") ?: ""
-                    VLog.i(TAG, "接收到VPN状态消息: status=$status, message=$message")
-                    vpnServiceEvent?.onVpnStatusChange(status, message)
-                    if(status == VPN_STATE_ERROR) {
-                        stopXray()
-                        uninit()
-                    }
-                }
-                XRayService.XRAY_MSG_REPLY_TIMER_UPDATE -> {
-                    val currentTime = msg.data?.getLong("currentTime") ?: 0L
-                    val mode = msg.data?.getInt("mode") ?: 0
-                    val isRunning = msg.data?.getBoolean("isRunning") ?: false
-                    val isPaused = msg.data?.getBoolean("isPaused") ?: false
-                    VLog.i(TAG, "接收到计时更新消息: time=$currentTime, mode=$mode, running=$isRunning, paused=$isPaused")
-                    vpnServiceEvent?.onTimerUpdate(currentTime, mode, isRunning, isPaused)
-                }
-                XRayService.XRAY_MSG_REPLY_STOP -> {
-                    VLog.i(TAG, "接收到停止消息")
-                    uninit()
-                }
-                else -> {
-                    VLog.w(TAG, "未知的消息类型: ${msg.what}")
-                }
-            }
-        }
-    }
-
-    private fun interface OnXrayServiceEvent {
-        fun onXrayServiceStarted()
-    }
+    private var isRegistered = false
 
     interface OnVpnServiceEvent {
-        fun onVpnStatusChange(status: Long, message: String)
+        fun onVpnStatusChange(status: Long, code: Long, message: String)
         fun onTimerUpdate(currentTime: Long, mode: Int, isRunning: Boolean, isPaused: Boolean)
+        fun onBoostResult(param: String, success: Boolean, locationCode: String, nodeId: String)
     }
 
-    private val mConnection = object : ServiceConnection {
-        override fun onServiceConnected(className: ComponentName, service: IBinder) {
-            VLog.i(TAG, "xray service connected 服务连接成功")
-            mService = Messenger(service)
-            lock.lock()
-            try {
-                xraySvrState = XRAY_SVR_SERVICE_WORKING
-                VLog.i(TAG, "服务状态更新为 XRAY_SVR_SERVICE_WORKING")
-            } finally {
-                lock.unlock()
-            }
-            // 如果有等待执行的事件,现在执行
-            serviceEvent?.let {
-                VLog.i(TAG, "执行等待的服务事件")
-                it.onXrayServiceStarted()
-                serviceEvent = null
-            }
-            regReplyMessenger();
-        }
-
-        override fun onServiceDisconnected(className: ComponentName) {
-            VLog.i(TAG, "xray service disconnected 服务断开连接")
-            lock.lock()
-            try {
-                mService = null
-                vpnState = VPN_STATE_IDLE
-                xraySvrState = XRAY_SVR_IDLE
-                vpnServiceEvent?.onVpnStatusChange(VPN_STATE_SERVICE_DISCONNECTED, "xray service disconnected")
-            } finally {
-                lock.unlock()
-                uninit()
+    // 状态广播接收器
+    private val statusReceiver = object : BroadcastReceiver() {
+        override fun onReceive(context: Context?, intent: Intent?) {
+            intent ?: return
+            when (intent.action) {
+                STATUS_BROADCAST -> {
+                    val status = intent.getLongExtra("status", -1L)
+                    val code = intent.getLongExtra("code", 0L)
+                    val message = intent.getStringExtra("message") ?: ""
+                    VLog.i(TAG, "接收到VPN状态广播: status=$status, code=$code, message=$message")
+                    vpnServiceEvent?.onVpnStatusChange(status, code, message)
+                }
+                TIMER_BROADCAST -> {
+                    val currentTime = intent.getLongExtra("currentTime", 0L)
+                    val mode = intent.getIntExtra("mode", 0)
+                    val isRunning = intent.getBooleanExtra("isRunning", false)
+                    val isPaused = intent.getBooleanExtra("isPaused", false)
+                    vpnServiceEvent?.onTimerUpdate(currentTime, mode, isRunning, isPaused)
+                }
+                BOOST_RESULT_BROADCAST -> {
+                    val param = intent.getStringExtra("param") ?: ""
+                    val success = intent.getBooleanExtra("success", false)
+                    val locationCode = intent.getStringExtra("locationCode") ?: ""
+                    val nodeId = intent.getStringExtra("nodeId") ?: ""
+                    VLog.i(TAG, "接收到VPN加速结果广播: nodeId=$nodeId, locationCode=$locationCode, success=$success, param=$param,")
+                    vpnServiceEvent?.onBoostResult(param, success, locationCode, nodeId)
+                }
             }
-        }
-    }
 
-    private fun regReplyMessenger() {
-        val msg = Message.obtain(null, XRayService.XRAY_MSG_REG_MESSENGER, 0, 0)
-        try {
-            msg.replyTo = mReplyMsgHandler
-            mService?.send(msg)
-            VLog.i(TAG, "注册replyTo消息已发送到 XRayService")
-        } catch (e: DeadObjectException) {
-            VLog.e(TAG, "发送注册replyTo消息失败:DeadObjectException", e)
-        } catch (e: Exception) {
-            VLog.e(TAG, "发送注册replyTo消息失败", e)
         }
     }
 
-
+    /**
+     * 初始化 - 注册广播接收器
+     */
     fun init(context: Context) {
-        VLog.i(TAG, "XRayApi init 开始")
-        if (isInited) {
-            VLog.i(TAG, "XRayApi 已经初始化,跳过")
+        if (isRegistered) {
+            VLog.i(TAG, "已经初始化,跳过")
             return
         }
+        VLog.i(TAG, "XRayApi init")
         this.context = context
-        isInited = true
 
-        lock.lock()
-        try {
-            if (xraySvrState != XRAY_SVR_IDLE) {
-                VLog.i(TAG, "XRay 服务状态不是 IDLE,当前状态: $xraySvrState")
-                return
-            }
-            doStartVpnService(null)
-            VLog.i(TAG, "XRayApi init 完成,开始启动和绑定服务")
-        } finally {
-            lock.unlock()
-        }
+        // 注册广播接收器
+        ContextCompat.registerReceiver(
+            context,
+            statusReceiver,
+            IntentFilter().apply {
+                addAction(STATUS_BROADCAST)
+                addAction(TIMER_BROADCAST)
+                addAction(BOOST_RESULT_BROADCAST)
+            },
+            ContextCompat.RECEIVER_NOT_EXPORTED
+        )
+        isRegistered = true
+        VLog.i(TAG, "广播接收器已注册")
     }
 
-    fun uninit() {
-        if (!isInited) return
-        VLog.i(TAG, "uninit 开始清理服务")
-        
-        try {
-            context?.unbindService(mConnection)
-            VLog.i(TAG, "Service 已解绑")
-        } catch (e: Exception) {
-            VLog.e(TAG, "解绑 Service 失败", e)
-        }
-        
-        // 重要:停止 Service(因为启动时用了 startService)
+    /**
+     * 反初始化 - 注销广播接收器
+     */
+    fun unInit() {
+        if (!isRegistered) return
+        VLog.i(TAG, "XRayApi uninit")
+
         try {
-            val intent = Intent(context, XRayService::class.java)
-            context?.stopService(intent)
-            VLog.i(TAG, "stopService 已调用")
+            context?.unregisterReceiver(statusReceiver)
+            VLog.i(TAG, "广播接收器已注销")
         } catch (e: Exception) {
-            VLog.e(TAG, "停止 Service 失败", e)
+            VLog.e(TAG, "注销广播接收器失败", e)
         }
-        
-        lock.lock()
-        try {
-            xraySvrState = XRAY_SVR_IDLE
-        } finally {
-            lock.unlock()
-        }
-        isInited = false
-        firstInit = true
-        VLog.i(TAG, "uninit 完成")
-    }
-
-    private fun doStartVpnService(callback: OnXrayServiceEvent?) {
-        context?.let { ctx ->
-            VLog.i(TAG, "doStartVpnService 开始")
-            doStartService(ctx)
-            val intent = Intent(ctx, XRayService::class.java)
-            VLog.i(TAG, "准备绑定 xray service")
-            if (!ctx.bindService(intent, mConnection, Context.BIND_AUTO_CREATE)) {
-                VLog.e(TAG, "绑定 xray service 失败")
-                return
-            }
-            xraySvrState = XRAY_SVR_CONNECT_SERVICE
-            serviceEvent = callback
-            VLog.i(TAG, "xray service 绑定请求已发送,状态更新为 XRAY_SVR_CONNECT_SERVICE")
-        }
-    }
 
-    private fun doStartService(context: Context) {
-        if (isServiceRunning(context)) {
-            VLog.i(TAG, "XRayService 已经在运行")
-            return
-        }
-        val intent = Intent(context, XRayService::class.java)
-        VLog.i(TAG, "启动 XRayService")
-        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
-            context.startForegroundService(intent)
-        } else {
-            context.startService(intent)
-        }
-        VLog.i(TAG, "XRayService 启动命令已发送")
+        isRegistered = false
+        context = null
     }
 
+    /**
+     * 启动 VPN
+     */
     fun startXray(config: XrayConfig): Boolean {
-        lock.lock()
-        return try {
-            VLog.i(TAG, "startXray 开始,当前 xraySvrState = $xraySvrState")
-            when (xraySvrState) {
-                XRAY_SVR_IDLE -> {
-                    VLog.w(TAG, "服务状态为 IDLE,需要先启动服务")
-                    doStartVpnService(OnXrayServiceEvent {
-                        VLog.i(TAG, "服务启动完成,开始 startXray")
-                        doStartXray(config)
-                    })
-                    lock.unlock()
-                    true
-                }
-                XRAY_SVR_CONNECT_SERVICE, XRAY_SVR_QUERY_STATE -> {
-                    VLog.i(TAG, "服务正在连接中,注册回调等待连接完成")
-                    serviceEvent = OnXrayServiceEvent {
-                        VLog.i(TAG, "服务连接完成,现在执行 startXray")
-                        doStartXray(config)
-                    }
-                    lock.unlock()
-                    true
-                }
-                XRAY_SVR_SERVICE_WORKING -> {
-                    lock.unlock()
-                    VLog.i(TAG, "服务已就绪,直接启动 xray")
-                    doStartXray(config)
-                }
-                else -> {
-                    VLog.e(TAG, "未知的服务状态: $xraySvrState")
-                    lock.unlock()
-                    false
-                }
-            }
-        } catch (e: Exception) {
-            VLog.e(TAG, "startXray 异常", e)
-            lock.unlock()
-            false
+        val ctx = context ?: run {
+            VLog.e(TAG, "Context 为空,无法启动")
+            return false
         }
-    }
 
-    fun doStartXray(config: XrayConfig): Boolean {
-        VLog.i(TAG, "doStartXray 开始执行")
-        VLog.i(TAG, "配置信息 - socksPort: ${config.socksPort}, sessionId: ${config.sessionId}")
-        
-        val msg = Message.obtain(null, XRayService.XRAY_MSG_START, 0, 0)
-        val bundle = Bundle().apply {
-            putInt("socksPort", config.socksPort)
-            putString("sessionId", config.sessionId)
-            putString("tunnelConfig", config.tunnelConfig)
-            putString("startOptions", config.startOptions)
-            putStringArrayList("allowVpnApps", config.allowVpnApps)
-            putStringArrayList("disallowVpnApps", config.disallowVpnApps)
+        VLog.i(TAG, "startXray - socksPort: ${config.socksPort}, sessionId: ${config.sessionId}")
+
+        val configJson = Gson().toJson(config)
+        val intent = Intent(ctx, XRayService::class.java).apply {
+            action = START_ACTION
+            putExtras(Bundle().apply {
+                putString("config", configJson)
+            })
         }
-        msg.data = bundle
+
         return try {
-            msg.replyTo = mReplyMsgHandler
-            mService?.send(msg)
-            VLog.i(TAG, "启动消息已发送到 XRayService")
+            if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
+                ctx.startForegroundService(intent)
+            } else {
+                ctx.startService(intent)
+            }
+            VLog.i(TAG, "启动服务命令已发送")
             true
-        } catch (e: DeadObjectException) {
-            VLog.e(TAG, "发送启动消息失败:DeadObjectException", e)
-            false
         } catch (e: Exception) {
-            VLog.e(TAG, "发送启动消息失败", e)
+            VLog.e(TAG, "启动服务失败", e)
             false
         }
     }
 
+    /**
+     * 停止 VPN
+     */
     fun stopXray() {
-        VLog.i(TAG, "stopXray 被调用")
-        lock.lock()
-        try {
-            if (xraySvrState != XRAY_SVR_SERVICE_WORKING) {
-                // 服务未启动,点击了关闭
-                vpnServiceEvent?.onVpnStatusChange(VPN_STATE_IDLE, "")
-                VLog.w(TAG, "xray 服务未在运行,当前状态: $xraySvrState")
-                return
-            }
-        } finally {
-            lock.unlock()
+        val ctx = context ?: run {
+            VLog.e(TAG, "Context 为空,无法停止")
+            return
         }
-        val msg = Message.obtain(null, XRayService.XRAY_MSG_STOP, 0, 0)
-        try {
-            mService?.send(msg)
-            VLog.i(TAG, "停止消息已发送到 XRayService")
-        } catch (e: Exception) {
-            VLog.e(TAG, "发送停止消息失败", e)
+
+        VLog.i(TAG, "stopXray")
+
+        val intent = Intent(ctx, XRayService::class.java).apply {
+            action = STOP_ACTION
         }
-    }
 
-    // 只解绑服务,不停止服务
-    fun unbindXrayService() {
-        VLog.i(TAG, "unbindXrayService 被调用")
-        lock.lock()
         try {
-            context?.unbindService(mConnection)
-        } finally {
-            lock.unlock()
+            // 发送停止命令,让 Service 内部优雅清理资源
+            ctx.startService(intent)
+            VLog.i(TAG, "停止命令已发送,等待服务内部清理")
+        } catch (e: Exception) {
+            VLog.e(TAG, "停止服务失败", e)
         }
     }
 
+    /**
+     * 设置事件监听器
+     */
     fun setVpnServiceEventListener(listener: OnVpnServiceEvent?) {
         vpnServiceEvent = listener
     }
 
     companion object {
-        private const val TAG = "ixvpn"
-        const val XRAY_SVR_IDLE = 0
-        const val XRAY_SVR_CONNECT_SERVICE = 1
-        const val XRAY_SVR_QUERY_STATE = 2
-        const val XRAY_SVR_SERVICE_WORKING = 7
+        private const val TAG = "XRayApi"
+        private const val STOP_TIMEOUT_MS = 500L  // 停止超时时间
 
+        // VPN 状态常量
         const val VPN_STATE_IDLE = 0L
         const val VPN_STATE_CONNECTING = 1L
         const val VPN_STATE_CONNECTED = 2L
         const val VPN_STATE_ERROR = 3L
-        const val VPN_STATE_SERVICE_DISCONNECTED = 1000L
-        const val VPN_STATE_PERMISSION_DENIED = 1001L
-
 
+        /**
+         * 检查 VPN 服务是否正在运行
+         */
+        fun isServiceRunning(context: Context): Boolean {
+            val activityManager =
+                context.getSystemService(Context.ACTIVITY_SERVICE) as ActivityManager
 
-        fun isServiceRunning(mContext: Context): Boolean {
-            val activityManager = mContext.getSystemService(Context.ACTIVITY_SERVICE) as ActivityManager
+            @Suppress("DEPRECATION")
             val serviceList = activityManager.getRunningServices(300)
 
             if (serviceList.isEmpty()) {
                 return false
             }
 
-            return serviceList.any { it.service.className == "app.xixi.nomo.XRayService" }
+            return serviceList.any { it.service.className == XRayService::class.java.name }
         }
     }
 }

+ 492 - 331
android/app/src/main/kotlin/app/xixi/nomo/XRayService.kt

@@ -1,129 +1,214 @@
 package app.xixi.nomo
 
 import android.app.Notification
-import android.app.NotificationChannel
 import android.app.NotificationManager
+import android.content.BroadcastReceiver
+import android.content.Context
 import android.content.Intent
-import android.content.pm.ServiceInfo.FOREGROUND_SERVICE_TYPE_SPECIAL_USE
-import android.net.VpnService
+import android.content.IntentFilter
+import android.content.pm.ServiceInfo
 import android.os.Build
 import android.os.Bundle
-import android.os.DeadObjectException
-import android.os.Handler
-import android.os.IBinder
-import android.os.Looper
-import android.os.Message
-import android.os.Messenger
 import android.os.ParcelFileDescriptor
+import android.os.PowerManager
 import android.os.SystemClock
-import androidx.core.app.NotificationCompat
+import androidx.core.content.ContextCompat
+import androidx.lifecycle.lifecycleScope
+import app.xixi.nomo.App.Companion.EXTRA_IS_FOREGROUND
 import app.xixi.nomo.XRayApi.Companion.VPN_STATE_CONNECTED
 import app.xixi.nomo.XRayApi.Companion.VPN_STATE_ERROR
+import app.xixi.nomo.XRayApi.Companion.VPN_STATE_IDLE
+import com.google.gson.Gson
+import com.tekartik.sqflite.Constant
 import go.Seq
 import ixvpn_mobile.Ixvpn_mobile
 import ixvpn_mobile.ProxyConnectorHandler
+import kotlinx.coroutines.Job
+import kotlinx.coroutines.delay
+import kotlinx.coroutines.isActive
+import kotlinx.coroutines.launch
+import kotlinx.coroutines.sync.Mutex
+import kotlinx.coroutines.sync.withLock
+import org.json.JSONObject
+import win.fkey.netboost.service.NetworkReporter
 
-class XRayService : VpnService() {
 
-    private val mMessenger = Messenger(IncomingHandler())
+class XRayService : LifecycleVpnService() {
     private val tunnelService = TProxyService()
-    private var tunFileDescriptor: ParcelFileDescriptor? = null
-    private var replyMessenger: Messenger? = null
+    private var tunFd: ParcelFileDescriptor? = null
+    private val mutex = Mutex()
 
-    // 计时功能相关变量
-    private val timerHandler = Handler(Looper.getMainLooper())
-    private var isTimerRunning = false
-    private var isTimerPaused = false
-    private var timerMode = 0 // 0: 普通计时, 1: 倒计时
+    private var vpnStatus = VPN_STATE_IDLE
 
-    // 使用 SystemClock.elapsedRealtime() 确保时间准确性(包括休眠时间)
+    // 计时功能相关变量
+    private var timerJob: Job? = null
+    private var timerMode = TIMER_MODE_NORMAL // 0: 普通计时, 1: 倒计时
     private var timerBaseRealtime = 0L // 计时开始的真实时间(elapsedRealtime)
     private var timerInitialTime = 0L // 初始时间(对于倒计时是总时长,对于正常计时是0或已用时间)
     private var timerPausedElapsed = 0L // 暂停时已经过的时间
+    private var isTimerPaused = false
 
-    // 计时器Runnable
-    private val timerRunnable = object : Runnable {
-        override fun run() {
-            if (isTimerRunning && !isTimerPaused) {
-                // 计算从开始到现在经过的真实时间
-                val elapsedTime = SystemClock.elapsedRealtime() - timerBaseRealtime
+    private var xrayConfig: XrayConfig? = null
 
-                val currentTime = if (timerMode == 0) {
-                    // 普通计时:初始时间 + 经过时间
-                    timerInitialTime + elapsedTime
-                } else {
-                    // 倒计时:初始时间 - 经过时间
-                    timerInitialTime - elapsedTime
-                }
+    private var remainTime = 0L
 
-                // 检查倒计时是否结束
-                if (timerMode == 1 && currentTime <= 0) {
-                    VLog.i(TAG, "倒计时结束 - 关闭VPN (elapsed: ${elapsedTime}ms)")
-                    dealStopMsg()
-                    notifyStop()
-                    return
-                }
+    // 屏幕状态跟踪
+    private var isScreenOn = true
+    private var screenReceiver: BroadcastReceiver? = null
 
-                // 更新通知
-                updateNotification(currentTime)
+    // 应用前后台状态跟踪
+    private var isAppForeground = true
+    private var foregroundReceiver: BroadcastReceiver? = null
 
-                // 发送计时更新
-                sendTimerUpdate(currentTime)
+    companion object {
+        private val TAG: String = XRayService::class.java.simpleName
+
+        private const val FOREGROUND_SERVICE_ID: Int = 1
+        private const val NOTIFICATION_CHANNEL_ID: String = "NOMO"
+
+        // 计时模式常量
+        const val TIMER_MODE_NORMAL = 0  // 普通计时(正计时)
+        const val TIMER_MODE_COUNTDOWN = 1  // 倒计时
+
+        private var serviceStatus: ServiceStatus = ServiceStatus.Disconnected
+    }
+
+    override fun onCreate() {
+        super.onCreate()
+        // 初始化文件日志系统
+        VLog.init(applicationContext)
+        VLog.i(TAG, "XRayService onCreate")
+
+        Seq.setContext(applicationContext)
+        Ixvpn_mobile.initProxyConnector()
+        registerNotificationChannel(
+            this,
+            NOTIFICATION_CHANNEL_ID,
+            "VPN",
+        )
+        // 注册屏幕状态监听
+        registerScreenReceiver()
+        // 注册应用前后台状态监听
+        registerForegroundReceiver()
+    }
+
+    /**
+     * 注册屏幕状态监听器
+     */
+    private fun registerScreenReceiver() {
+        screenReceiver = object : BroadcastReceiver() {
+            override fun onReceive(context: Context, intent: Intent) {
+                when (intent.action) {
+                    Intent.ACTION_SCREEN_ON -> {
+                        isScreenOn = true
+                        VLog.d(TAG, "屏幕亮起")
+                    }
 
-                // 继续下一秒
-                timerHandler.postDelayed(this, 1000L)
+                    Intent.ACTION_SCREEN_OFF -> {
+                        isScreenOn = false
+                        VLog.d(TAG, "屏幕熄灭")
+                    }
+                }
             }
         }
+
+        val filter = IntentFilter().apply {
+            addAction(Intent.ACTION_SCREEN_ON)
+            addAction(Intent.ACTION_SCREEN_OFF)
+        }
+        registerReceiver(screenReceiver, filter)
+
+        // 获取初始屏幕状态
+        val powerManager = getSystemService(POWER_SERVICE) as PowerManager
+        isScreenOn = powerManager.isInteractive
     }
 
-    private val vpnHandler = object : ProxyConnectorHandler {
-        override fun proxyStatusChange(l: Long, msg: String) {
-            VLog.i(TAG, "status:$l msg:$msg")
-            // 发送状态消息到XRayApi
-            sendStatusMessage(l, msg)
+    /**
+     * 注销屏幕状态监听器
+     */
+    private fun unregisterScreenReceiver() {
+        screenReceiver?.let {
+            try {
+                unregisterReceiver(it)
+            } catch (e: Exception) {
+                VLog.e(TAG, "注销屏幕监听器失败", e)
+            }
+            screenReceiver = null
         }
     }
 
-    private inner class IncomingHandler : Handler() {
-        override fun handleMessage(msg: Message) {
-            handleRemoteMessage(msg)
+    /**
+     * 注册应用前后台状态监听器
+     */
+    private fun registerForegroundReceiver() {
+        foregroundReceiver = object : BroadcastReceiver() {
+            override fun onReceive(context: Context, intent: Intent) {
+                if (intent.action == APP_FOREGROUND_BROADCAST) {
+                    isAppForeground = intent.getBooleanExtra(EXTRA_IS_FOREGROUND, true)
+                    VLog.d(TAG, "应用前后台状态变化: ${if (isAppForeground) "前台" else "后台"}")
+                }
+            }
         }
+
+        val filter = IntentFilter(APP_FOREGROUND_BROADCAST)
+        ContextCompat.registerReceiver(
+            this,
+            foregroundReceiver,
+            filter,
+            ContextCompat.RECEIVER_NOT_EXPORTED
+        )
     }
 
-    override fun onBind(intent: Intent): IBinder? {
-        // 首先检查是否是 VPN 服务的绑定请求
-        if (SERVICE_INTERFACE == intent.action) {
-            return super.onBind(intent)
+    /**
+     * 注销应用前后台状态监听器
+     */
+    private fun unregisterForegroundReceiver() {
+        foregroundReceiver?.let {
+            try {
+                unregisterReceiver(it)
+            } catch (e: Exception) {
+                VLog.e(TAG, "注销前后台监听器失败", e)
+            }
+            foregroundReceiver = null
         }
-        // 其他绑定请求返回 Messenger
-        return mMessenger.binder
     }
 
-    override fun onCreate() {
-        super.onCreate()
-        // 初始化文件日志系统
-        VLog.init(applicationContext)
-        VLog.i(TAG, "XRayService onCreate")
+    override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
+        super.onStartCommand(intent, flags, startId)
+        return when (val action = intent?.action) {
+            START_ACTION -> {
+                lifecycleScope.launch { intent.extras?.let { start(it) } }
+                START_STICKY
+            }
 
-        Seq.setContext(applicationContext)
-        Ixvpn_mobile.initProxyConnector()
-        createNotificationChannel()
+            STOP_ACTION -> {
+                VLog.i(TAG, "onStartCommand")
+                lifecycleScope.launch {
+                    stop()
+                }
+                START_NOT_STICKY
+            }
+
+            else -> {
+                VLog.i(TAG, "Unknown action: $action")
+                START_NOT_STICKY
+            }
+        }
     }
 
     override fun onRevoke() {
-        super.onRevoke()
         VLog.i(TAG, "onRevoke")
-        // 停止计时
-        sendStatusMessage(VPN_STATE_ERROR, "onRevoke")
-    }
-
-    override fun onTaskRemoved(rootIntent: Intent?) {
-        super.onTaskRemoved(rootIntent)
-        VLog.i(TAG, "onTaskRemoved")
+        lifecycleScope.launch { stop() }
+        sendStatusMessage(VPN_STATE_ERROR, ERROR_REVOKE, "System cleanup")
     }
 
     override fun onDestroy() {
-        VLog.i(TAG, "onDestroy")
+        VLog.i(TAG, "Service onDestroy")
+        // 确保资源被清理
+        // 注销屏幕状态监听
+        unregisterScreenReceiver()
+        // 注销前后台监听
+        unregisterForegroundReceiver()
         // 停止计时
         stopTimer()
         // 先停止前台服务
@@ -133,10 +218,6 @@ class XRayService : VpnService() {
             VLog.e(TAG, "stopForeground 失败", e)
         }
 
-        // 清理Messenger引用
-        replyMessenger = null
-        VLog.i(TAG, "replyMessenger 已清理")
-
         // 清理资源
         try {
             Ixvpn_mobile.freeProxyConnector()
@@ -146,97 +227,208 @@ class XRayService : VpnService() {
 
         // 关闭日志系统
         VLog.close()
-
         super.onDestroy()
     }
 
-    override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
-        createNotification()
-        return START_STICKY
-    }
+    private suspend fun start(bundle: Bundle) {
+        VLog.i(TAG, "Starting")
 
-    private fun handleRemoteMessage(msg: Message) {
-        // 只在有replyTo时才记录Messenger对象
-        if (msg.replyTo != null) {
-            replyMessenger = msg.replyTo
-            VLog.i(TAG, "记录replyTo Messenger: ${msg.what}")
-        } else {
-            VLog.w(TAG, "消息没有replyTo,无法记录Messenger")
+        if (serviceStatus == ServiceStatus.Connected) {
+            VLog.i(TAG, "VPN already connected")
+            return
         }
 
-        when (msg.what) {
-            XRAY_MSG_START -> dealStartMsg(msg)
-            XRAY_MSG_STOP -> dealStopMsg()
+        val configJson = bundle.getString("config")
+        xrayConfig = Gson().fromJson(configJson, XrayConfig::class.java)
+
+        if(xrayConfig == null) {
+            updateStatus(ServiceStatus.Failed)
+            sendStatusMessage(VPN_STATE_ERROR, ERROR_INIT, "参数异常")
+            stopSelfService()
+            return
+        }
+
+        remainTime = xrayConfig!!.remainTime * 1000L
+
+        try {
+            mutex.withLock {
+                startTun2Socks()
+            }
+            updateStatus(ServiceStatus.Connected)
+            startForeground()
+        } catch (e: Exception) {
+            VLog.e(TAG, "Failed to start VPN", e)
+            updateStatus(ServiceStatus.Failed)
+            stop()
+            sendStatusMessage(VPN_STATE_ERROR, ERROR_INIT, "Failed to startTun2Socks")
         }
     }
 
-    private fun dealStartMsg(msg: Message) {
-        VLog.i(TAG, "deal start xray msg")
-        val bundle = msg.data
-        val socksPort = bundle.getInt("socksPort")
-        val startOptions = bundle.getString("startOptions")
-        val sessionId = bundle.getString("sessionId")
-        val tunnelConfig = bundle.getString("tunnelConfig")
-        val allowVpnApps = bundle.getStringArrayList("allowVpnApps") ?: arrayListOf()
-        val disallowVpnApps = bundle.getStringArrayList("disallowVpnApps") ?: arrayListOf()
-
-        VLog.i(TAG, "socksPort: $socksPort, sessionId: $sessionId")
+    private fun startTun2Socks() {
+        VLog.i(TAG, "socksPort: ${xrayConfig?.socksPort}, sessionId: ${xrayConfig?.sessionId}")
         VLog.i(
             TAG,
-            "allowVpnApps count: ${allowVpnApps.size}, disallowVpnApps count: ${disallowVpnApps.size}"
+            "allowVpnApps count: ${xrayConfig?.allowVpnApps?.size}, disallowVpnApps count: ${xrayConfig?.disallowVpnApps?.size}"
         )
+        val builder = Builder()
+        val ipAddress = "192.168.34.2"
+        builder.setSession("ix_vpn")
+            .setMtu(1500)
+            .addAddress(ipAddress, 24)
+            .addRoute("0.0.0.0", 0)
+            .addDnsServer("8.8.8.8")
+
+        if (xrayConfig?.allowVpnApps!!.isNotEmpty()) {
+            xrayConfig?.allowVpnApps!!.forEach { app ->
+                builder.addAllowedApplication(app)
+            }
+            VLog.i(TAG, "允许的应用: ${xrayConfig?.allowVpnApps?.joinToString(", ")}")
+        } else {
+            xrayConfig?.disallowVpnApps!!.forEach { app ->
+                builder.addDisallowedApplication(app)
+            }
+            builder.addDisallowedApplication(packageName)
+            VLog.i(TAG, "禁止的应用: ${xrayConfig?.disallowVpnApps?.joinToString(", ")}")
+        }
 
+        // 初始化网络请求参数
+        NetworkReporter.getInstance().initialize(xrayConfig!!)
+
+        tunFd = builder.establish()
+        tunFd?.let { tfd ->
+            VLog.i(TAG, "VPN tunnel established, fd: ${tfd.fd}")
+            tunnelService.startTunnel(this, xrayConfig!!.socksPort, xrayConfig!!.tunnelConfig, tfd.fd)
+            Ixvpn_mobile.proxyConnectorStart(xrayConfig!!.sessionId, xrayConfig!!.startOptions, vpnHandler)
+            VLog.i(TAG, "XRay proxy started successfully")
+        } ?: run {
+            VLog.e(TAG, "Failed to establish VPN tunnel")
+            updateStatus(ServiceStatus.Failed)
+            // 通知 Flutter 层启动失败
+            sendStatusMessage(VPN_STATE_ERROR, ERROR_INIT, "Failed to establish VPN tunnel")
+            stopSelfService()
+        }
+    }
+
+    private val vpnHandler = ProxyConnectorHandler { status, params ->
+        VLog.i(TAG, "status:$status params:$params")
+        if (vpnStatus == status) {
+            return@ProxyConnectorHandler
+        }
+        vpnStatus = status
+
+        var code = 0L
+        var message = ""
         try {
-            val builder = Builder()
-            val ipAddress = "192.168.34.2"
-            builder.setSession("ixvpn")
-                .setMtu(1500)
-                .addAddress(ipAddress, 24)
-                .addRoute("0.0.0.0", 0)
-                .addDnsServer("8.8.8.8")
-
-            if (allowVpnApps.isNotEmpty()) {
-                allowVpnApps.forEach { app ->
-                    builder.addAllowedApplication(app)
-                }
-                VLog.i(TAG, "允许的应用: ${allowVpnApps.joinToString(", ")}")
-            } else {
-                disallowVpnApps.forEach { app ->
-                    builder.addDisallowedApplication(app)
-                }
-                builder.addDisallowedApplication(packageName)
-                VLog.i(TAG, "禁止的应用: ${disallowVpnApps.joinToString(", ")}")
-            }
+            val json = JSONObject(params)
+            code = json.optLong("code", 0)
+            message = json.optString("message", "")
+        } catch (e: Exception) {
+            VLog.e(TAG, "解析 params JSON 失败", e)
+        }
 
-            tunFileDescriptor = builder.establish()
-            tunFileDescriptor?.let { tfd ->
-                VLog.i(TAG, "VPN tunnel established, fd: ${tfd.fd}")
-                tunnelService.startTunnel(this, socksPort, tunnelConfig, tfd.fd)
-                Ixvpn_mobile.proxyConnectorStart(sessionId, startOptions, vpnHandler)
-                VLog.i(TAG, "XRay proxy started successfully")
-            } ?: run {
-                VLog.e(TAG, "Failed to establish VPN tunnel")
-                // 通知 Flutter 层启动失败
-                sendStatusMessage(VPN_STATE_ERROR, "Failed to establish VPN tunnel")
+        // VPN 连接成功后启动计时
+        if (status == VPN_STATE_CONNECTED) {
+            val mode = if (xrayConfig!!.isCountdown) TIMER_MODE_COUNTDOWN else TIMER_MODE_NORMAL
+            val time = if (xrayConfig!!.isCountdown) remainTime else 0L
+            startTimer(mode, time)
+            uploadBoostResult(true, code)
+            AppLogger.getInstance(this).updateAppJsonStatusInfo(true, code)
+            dealStat()
+        } else if (status == VPN_STATE_ERROR) {
+            AppLogger.getInstance(this).updateAppJsonStatusInfo(false, code)
+            uploadBoostResult(false, code)
+            VLog.i(TAG, "vpnHandler")
+            lifecycleScope.launch { stop() }
+        }
+        // 发送状态消息到XRayApi
+
+        sendStatusMessage(status, code, message)
+    }
+
+    private fun uploadBoostResult(success: Boolean, code: Long) {
+        try {
+            // 更新 实时流量
+            val queryTypes = byteArrayOf(1)
+            val stats: String? = Ixvpn_mobile.proxyConnectorQueryStats(queryTypes)
+            val session: String = xrayConfig!!.sessionId
+            NetworkReporter.getInstance().addBoostResultParams(
+                "NM_BoostResult",
+                stats,
+                session,
+                success,
+                code
+            )
+            val param = NetworkReporter.getInstance()
+                .buildRequestParams("NM_BoostResult")
+            val intent = Intent(
+                BOOST_RESULT_BROADCAST
+            ).apply {
+                setPackage(packageName)
+                putExtra("param", param)
+                putExtra("success", success)
+                putExtra("locationCode",  NetworkReporter.getInstance().getLocationCode())
+                putExtra("nodeId", NetworkReporter.getInstance().getConnectedNodeId())
             }
-        } catch (e: Exception) {
-            VLog.e(TAG, "启动 XRay 失败", e)
-            // 通知 Flutter 层启动失败
-            sendStatusMessage(VPN_STATE_ERROR, "启动 XRay 失败: ${e.message}")
+            sendBroadcast(intent)
+        } catch (e: java.lang.Exception) {
+            VLog.e(TAG, "uploadBoostResult error: ${e.message}")
         }
     }
 
-    private fun dealStopMsg() {
-        VLog.i(TAG, "deal stop xray msg")
-        doStop()
+    private fun sendStatusMessage(status: Long, code: Long, message: String) {
+        VLog.i(TAG, "sendStatusMessage status:$status code:$code message:$message")
+        val intent = Intent(STATUS_BROADCAST).apply {
+            setPackage(packageName)  // 必须设置包名,否则 RECEIVER_NOT_EXPORTED 接收不到
+            putExtra("status", status)
+            putExtra("code", code)
+            putExtra("message", message)
+        }
+        sendBroadcast(intent)
     }
 
-    private fun doStop() {
-        VLog.i(TAG, "开始停止 XRay 服务")
+    private fun startForeground() {
+        val notification: Notification = createNotification()
+        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) {
+            startForeground(
+                FOREGROUND_SERVICE_ID,
+                notification,
+                ServiceInfo.FOREGROUND_SERVICE_TYPE_SPECIAL_USE,
+            )
+        } else {
+            startForeground(FOREGROUND_SERVICE_ID, notification)
+        }
+    }
+
+    private suspend fun stop() {
+        VLog.i(TAG, "Stopping")
+
+        mutex.withLock {
+            stopTun2Socks()
+        }
+        stopSelfService()
+    }
+
+    private fun stopSelfService() {
+        VLog.i(TAG, "stop self service")
+        updateStatus(ServiceStatus.Disconnected)
+        stopSelf()
+    }
 
+    private fun stopTun2Socks() {
         // 停止计时
         stopTimer()
 
+        tunFd?.let { tfd ->
+            try {
+                tfd.close()
+                VLog.i(TAG, "TUN file descriptor closed")
+            } catch (e: Exception) {
+                VLog.e(TAG, "关闭 TUN file descriptor 失败", e)
+            }
+            tunFd = null
+        }
+        VLog.i(TAG, "xray stopped")
+
         try {
             tunnelService.stopTunnel()
             VLog.i(TAG, "Tunnel stopped")
@@ -250,243 +442,212 @@ class XRayService : VpnService() {
         } catch (e: Exception) {
             VLog.e(TAG, "停止 proxy connector 失败", e)
         }
-
-        tunFileDescriptor?.let { tfd ->
-            try {
-                tfd.close()
-                VLog.i(TAG, "TUN file descriptor closed")
-            } catch (e: Exception) {
-                VLog.e(TAG, "关闭 TUN file descriptor 失败", e)
-            }
-            tunFileDescriptor = null
-        }
-        VLog.i(TAG, "xray stopped")
     }
 
-    private fun notifyStop() {
-        replyMessenger?.let { messenger ->
-            try {
-                val msg = Message.obtain().apply {
-                    what = XRAY_MSG_REPLY_STOP
-                }
-                messenger.send(msg)
-                VLog.i(TAG, "停止消息已通过Messenger发送")
-            } catch (e: DeadObjectException) {
-                VLog.w(TAG, "Messenger已失效,清理引用", e)
-                replyMessenger = null
-            } catch (e: Exception) {
-                VLog.e(TAG, "发送Messenger停止消息失败", e)
-            }
-        } ?: run {
-            VLog.w(TAG, "replyMessenger为空,无法发送停止消息")
-        }
-    }
 
-    private fun sendStatusMessage(status: Long, message: String) {
-        try {
-            if (status == VPN_STATE_CONNECTED) {
-                // 启动计时(普通计时模式)
-                startTimer(0, 0)
-//                startTimer(1, 10000L)
-            } else if (status == VPN_STATE_ERROR) {
-                VLog.i(TAG, "VPN 连接失败,status:$status")
-            }
-            replyMessenger?.let { messenger ->
-                try {
-                    val msg = Message.obtain().apply {
-                        what = XRAY_MSG_REPLY_STATUS
-                        // 使用Bundle传递消息,避免跨进程序列化问题
-                        data = Bundle().apply {
-                            putLong("status", status)
-                            putString("message", message)
-                        }
-                    }
-                    messenger.send(msg)
-                    VLog.i(TAG, "状态消息已通过Messenger发送: status=$status, message=$message")
-                } catch (e: DeadObjectException) {
-                    VLog.w(TAG, "Messenger已失效,清理引用", e)
-                    replyMessenger = null
-                } catch (e: Exception) {
-                    VLog.e(TAG, "发送Messenger状态消息失败", e)
-                }
-            } ?: run {
-                VLog.w(TAG, "replyMessenger为空,无法发送状态消息")
-            }
-        } catch (e: Exception) {
-            VLog.e(TAG, "发送状态消息失败", e)
-        }
+    private fun updateStatus(newStatus: ServiceStatus) {
+        VLog.d(TAG, "VPN status changed from $serviceStatus to $newStatus")
+        serviceStatus = newStatus
     }
 
-    // 计时相关方法
+    private fun createNotification(): Notification =
+        createConnectionNotification(
+            this,
+            NOTIFICATION_CHANNEL_ID,
+            "NOMO VPN",
+            "Service is running",
+        )
+
+// ==================== 计时功能 ====================
+
+    /**
+     * 启动计时
+     * @param mode 计时模式: TIMER_MODE_NORMAL(0)=普通计时, TIMER_MODE_COUNTDOWN(1)=倒计时
+     * @param initialTime 初始时间(毫秒),普通计时为0,倒计时为总时长
+     */
     private fun startTimer(mode: Int, initialTime: Long) {
-        if (isTimerRunning)
+        if (timerJob?.isActive == true) {
+            VLog.w(TAG, "计时器已在运行")
             return
+        }
+
         VLog.i(TAG, "启动计时: mode=$mode, initialTime=$initialTime")
         timerMode = mode
         timerInitialTime = initialTime
         timerBaseRealtime = SystemClock.elapsedRealtime()
         timerPausedElapsed = 0L
-
-        isTimerRunning = true
         isTimerPaused = false
 
-        // 开始计时循环
-        timerHandler.post(timerRunnable)
+        timerJob = lifecycleScope.launch {
+            while (isActive && !isTimerPaused) {
+                // 计算从开始到现在经过的真实时间
+                val elapsedTime = SystemClock.elapsedRealtime() - timerBaseRealtime
+
+                val currentTime = if (timerMode == TIMER_MODE_NORMAL) {
+                    // 普通计时:初始时间 + 经过时间
+                    timerInitialTime + elapsedTime
+                } else {
+                    // 倒计时:初始时间 - 经过时间
+                    timerInitialTime - elapsedTime
+                }
+
+                // 检查倒计时是否结束
+                if (timerMode == TIMER_MODE_COUNTDOWN && currentTime <= 0) {
+                    VLog.i(TAG, "倒计时结束 - 关闭VPN (elapsed: ${elapsedTime}ms)")
+                    sendTimerUpdate(0L)
+                    stop()
+                    sendStatusMessage(VPN_STATE_ERROR, ERROR_REMAIN_TIME, "No available time")
+                    return@launch
+                }
+                if (timerMode == TIMER_MODE_NORMAL && currentTime >= remainTime) {
+                    VLog.i(TAG, "可用时间已结束 - 关闭VPN (elapsed: ${elapsedTime}ms)")
+                    sendTimerUpdate(0L)
+                    stop()
+                    sendStatusMessage(VPN_STATE_ERROR, ERROR_REMAIN_TIME, "No available time")
+                    return@launch
+                }
+
+                // 更新通知
+                updateTimerNotification(currentTime)
+
+                // 发送计时更新广播
+                sendTimerUpdate(currentTime)
+
+                // 等待1秒
+                delay(1000L)
+            }
+        }
     }
 
+    /**
+     * 停止计时
+     */
     private fun stopTimer() {
-        if (!isTimerRunning)
-            return
-        VLog.i(TAG, "停止计时")
-        isTimerRunning = false
+        if (timerJob != null) {
+            VLog.i(TAG, "停止计时")
+        }
+        timerJob?.cancel()
+        timerJob = null
         isTimerPaused = false
-        timerHandler.removeCallbacks(timerRunnable)
+        timerPausedElapsed = 0L
     }
 
+    /**
+     * 暂停计时
+     */
     private fun pauseTimer() {
-        if (isTimerRunning && !isTimerPaused) {
-            // 记录暂停时已经过的时间
+        if (timerJob?.isActive == true && !isTimerPaused) {
             val elapsedTime = SystemClock.elapsedRealtime() - timerBaseRealtime
             timerPausedElapsed = elapsedTime
 
             VLog.i(TAG, "暂停计时 (已用: ${elapsedTime}ms)")
             isTimerPaused = true
-            timerHandler.removeCallbacks(timerRunnable)
+            timerJob?.cancel()
+            timerJob = null
         }
     }
 
+    /**
+     * 恢复计时
+     */
     private fun resumeTimer() {
-        if (isTimerRunning && isTimerPaused) {
+        if (isTimerPaused) {
             VLog.i(TAG, "恢复计时 (之前已用: ${timerPausedElapsed}ms)")
 
             // 恢复时重新设置基准时间,但要减去之前已经过的时间
             timerBaseRealtime = SystemClock.elapsedRealtime() - timerPausedElapsed
-
             isTimerPaused = false
-            timerHandler.post(timerRunnable)
-        }
-    }
 
-    private fun sendTimerUpdate(currentTime: Long) {
-        try {
-            replyMessenger?.let { messenger ->
-                try {
-                    val msg = Message.obtain().apply {
-                        what = XRAY_MSG_REPLY_TIMER_UPDATE
-                        data = Bundle().apply {
-                            putLong("currentTime", currentTime)
-                            putInt("mode", timerMode)
-                            putBoolean("isRunning", isTimerRunning)
-                            putBoolean("isPaused", isTimerPaused)
-                        }
+            timerJob = lifecycleScope.launch {
+                while (isActive && !isTimerPaused) {
+                    val elapsedTime = SystemClock.elapsedRealtime() - timerBaseRealtime
+
+                    val currentTime = if (timerMode == TIMER_MODE_NORMAL) {
+                        timerInitialTime + elapsedTime
+                    } else {
+                        timerInitialTime - elapsedTime
+                    }
+
+                    if (timerMode == TIMER_MODE_COUNTDOWN && currentTime <= 0) {
+                        VLog.i(TAG, "倒计时结束 - 关闭VPN")
+                        sendTimerUpdate(0L)
+                        stop()
+                        return@launch
                     }
-                    messenger.send(msg)
-                    VLog.i(TAG, "计时更新消息已发送: time=$currentTime, mode=$timerMode")
-                } catch (e: DeadObjectException) {
-                    VLog.w(TAG, "Messenger已失效,清理引用", e)
-                    replyMessenger = null
-                } catch (e: Exception) {
-                    VLog.e(TAG, "发送计时更新消息失败", e)
+
+                    updateTimerNotification(currentTime)
+                    sendTimerUpdate(currentTime)
+                    delay(1000L)
                 }
             }
-        } catch (e: Exception) {
-            VLog.e(TAG, "发送计时更新失败", e)
         }
     }
 
-    private fun updateNotification(currentTime: Long) {
+    /**
+     * 发送计时更新广播
+     * 屏幕关闭时不发送,节省资源
+     */
+    private fun sendTimerUpdate(currentTime: Long) {
+        // 屏幕关闭或应用在后台时不发送广播,节省资源
+        if (!isScreenOn || !isAppForeground) {
+            return
+        }
+
+        val intent = Intent(TIMER_BROADCAST).apply {
+            setPackage(packageName)  // 必须设置包名,否则 RECEIVER_NOT_EXPORTED 接收不到
+            putExtra("currentTime", currentTime)
+            putExtra("mode", timerMode)
+            putExtra("isRunning", timerJob?.isActive == true)
+            putExtra("isPaused", isTimerPaused)
+        }
+        sendBroadcast(intent)
+    }
+
+    /**
+     * 更新通知显示计时
+     */
+    private fun updateTimerNotification(currentTime: Long) {
         val timeText = formatTime(currentTime)
-        val modeText = if (timerMode == 0) "计时中" else "倒计时"
+        val modeText = if (timerMode == TIMER_MODE_NORMAL) "计时中" else "倒计时"
         val statusText = if (isTimerPaused) "已暂停" else "运行中"
 
-        val notification: Notification = NotificationCompat.Builder(this, CHANNEL_ID)
-            .setContentTitle("NoMo Service $modeText")
-            .setContentText("$timeText - $statusText")
-            .setSmallIcon(R.drawable.ic_xvpn)
-            .setPriority(NotificationCompat.PRIORITY_LOW)
-            .setOngoing(true)
-            .setShowWhen(false)
-            .setOnlyAlertOnce(true)
-            .build()
+        val notification = createConnectionNotification(
+            this,
+            NOTIFICATION_CHANNEL_ID,
+            "NOMO VPN $modeText",
+            "$timeText - $statusText",
+        )
 
         val notificationManager = getSystemService(NotificationManager::class.java)
-        notificationManager?.notify(1, notification)
+        notificationManager?.notify(FOREGROUND_SERVICE_ID, notification)
+    }
+
+    /**
+     * 查询日志状态
+     * QueryStatsConnectionHistory = 1 查询连接历史
+     * QueryStatsBandwidth         = 2 带宽
+     * QueryStatsMaxSpeed          = 3 最大速度
+     */
+    private fun dealStat() {
+        val queryTypes = byteArrayOf(1, 2, 3)
+        val stats: String? = Ixvpn_mobile.proxyConnectorQueryStats(queryTypes)
+        VLog.i(Constant.TAG, "stats = $stats")
     }
 
+    /**
+     * 格式化时间显示
+     */
     private fun formatTime(timeMs: Long): String {
-        val totalSeconds = Math.abs(timeMs) / 1000
-        val days = totalSeconds / 86400 // 86400 = 24 * 3600
+        val totalSeconds = kotlin.math.abs(timeMs) / 1000
+        val days = totalSeconds / 86400
         val hours = (totalSeconds % 86400) / 3600
         val minutes = (totalSeconds % 3600) / 60
         val seconds = totalSeconds % 60
 
-        return if (days > 0) {
-            String.format("%d 天 %02d:%02d:%02d", days, hours, minutes, seconds)
-        } else if (hours > 0) {
-            String.format("%02d:%02d:%02d", hours, minutes, seconds)
-        } else {
-            String.format("00:%02d:%02d", minutes, seconds)
-        }
-    }
-
-    private fun createNotificationChannel() {
-        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
-            val channelName = "NoMo Service Notifications"
-            val channelDescription = "NoMo Service is running in the background"
-            val importance = NotificationManager.IMPORTANCE_LOW
-
-            val channel = NotificationChannel(CHANNEL_ID, channelName, importance)
-            channel.setShowBadge(false)
-            channel.description = channelDescription
-
-            val notificationManager =
-                getSystemService(NotificationManager::class.java)
-            notificationManager?.createNotificationChannel(channel)
+        return when {
+            days > 0 -> String.format("%d 天 %02d:%02d:%02d", days, hours, minutes, seconds)
+            hours > 0 -> String.format("%02d:%02d:%02d", hours, minutes, seconds)
+            else -> String.format("00:%02d:%02d", minutes, seconds)
         }
     }
-
-    private fun createNotification() {
-        VLog.i(TAG, "开始创建通知")
-        try {
-            val notification: Notification = NotificationCompat.Builder(this, CHANNEL_ID)
-                .setContentTitle("NoMo Service Running")
-                //.setContentText("NoMo connection is active.")
-                .setSmallIcon(R.drawable.ic_xvpn)
-                .setPriority(NotificationCompat.PRIORITY_MIN)
-                .setOngoing(true)
-                .setShowWhen(false)
-                .setOnlyAlertOnce(true)
-                .build()
-
-            VLog.i(TAG, "通知创建成功,准备启动前台服务")
-
-            if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) {
-                try {
-                    startForeground(1, notification, FOREGROUND_SERVICE_TYPE_SPECIAL_USE)
-                    VLog.i(TAG, "前台服务已启动 (Android 14+)")
-                } catch (e: SecurityException) {
-                    VLog.w(TAG, "使用 SPECIAL_USE 类型失败,降级为普通前台服务", e)
-                    startForeground(1, notification)
-                    VLog.i(TAG, "前台服务已启动 (降级模式)")
-                }
-            } else {
-                startForeground(1, notification)
-                VLog.i(TAG, "前台服务已启动")
-            }
-        } catch (e: Exception) {
-            VLog.e(TAG, "创建通知失败", e)
-        }
-    }
-
-    companion object {
-        const val XRAY_MSG_START = 1
-        const val XRAY_MSG_STOP = 2
-        const val XRAY_MSG_REG_MESSENGER = 3
-        const val XRAY_MSG_REPLY_STATUS = 101
-        const val XRAY_MSG_REPLY_TIMER_UPDATE = 102
-        const val XRAY_MSG_REPLY_STOP = 103
-        private const val TAG = "ixvpn"
-
-        private const val CHANNEL_ID: String = "nomo_channel_id"
-    }
-}
+}

+ 17 - 1
android/app/src/main/kotlin/app/xixi/nomo/XrayConfig.kt

@@ -7,10 +7,26 @@ data class XrayConfig(
     var socksPort: Int = 0,
     var tunnelConfig: String = "",
     var startOptions: String = "",
+    var configJson: String = "",
+    var remainTime: Long = 0L,
+    var isCountdown: Boolean = false,
     var allowVpnApps: ArrayList<String> = ArrayList(),
-    var disallowVpnApps: ArrayList<String> = ArrayList()
+    var disallowVpnApps: ArrayList<String> = ArrayList(),
+    var peekTimeInterval: Int = 0,
+    var accessToken: String = "",
+    var aesKey: String = "",
+    var aesIv: String = "",
+    var locationId: Int = 0,
+    var locationCode: String = "",
+    var baseUrls: ArrayList<String> = ArrayList(),
+    var params: String = "",
+    
 ) {
     fun getProxyStartOptions(): String {
         return startOptions
     }
+
+    fun toJson(): String {
+        return Gson().toJson(this)
+    }
 }

BIN
assets/images/test.png


+ 4 - 0
assets/vectors/vip.svg

@@ -0,0 +1,4 @@
+<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
+<path d="M3.74996 16.6663L1.66663 7.08301L5.83329 9.16634L9.99996 3.33301L14.1666 9.16634L18.3333 7.08301L16.25 16.6663H3.74996Z" stroke="#F5D89F" style="stroke:#F5D89F;stroke:color(display-p3 0.9608 0.8471 0.6235);stroke-opacity:1;" stroke-width="1.25" stroke-linejoin="round"/>
+<path d="M10 13.7503C10.9205 13.7503 11.6667 13.0041 11.6667 12.0837C11.6667 11.1632 10.9205 10.417 10 10.417C9.07958 10.417 8.33337 11.1632 8.33337 12.0837C8.33337 13.0041 9.07958 13.7503 10 13.7503Z" stroke="#F5D89F" style="stroke:#F5D89F;stroke:color(display-p3 0.9608 0.8471 0.6235);stroke-opacity:1;" stroke-width="1.25" stroke-linejoin="round"/>
+</svg>

+ 3 - 0
devtools_options.yaml

@@ -0,0 +1,3 @@
+description: This file stores settings for Dart & Flutter DevTools.
+documentation: https://docs.flutter.dev/tools/devtools/extensions#configure-extension-enablement-states
+extensions:

+ 14 - 16
ios/Runner/CoreApi.g.swift

@@ -92,14 +92,13 @@ var coreApiPigeonMethodCodec = FlutterStandardMethodCodec(readerWriter: CoreApiP
 protocol CoreApi {
   func getApps(completion: @escaping (Result<String?, Error>) -> Void)
   func getSystemLocale() throws -> String?
-  func connect(sessionId: String, socksPort: Int64, tunnelConfig: String, configJson: String) throws -> Bool?
+  func connect(sessionId: String, socksPort: Int64, tunnelConfig: String, configJson: String, remainTime: Int64, isCountdown: Bool, allowVpnApps: [String], disallowVpnApps: [String], accessToken: String, aesKey: String, aesIv: String, locationId: Int64, locationCode: String, baseUrls: [String], params: String, peekTimeInterval: Int64) throws -> Bool?
   func disconnect() throws -> Bool?
   func getRemoteIp() throws -> String?
   func getAdvertisingId() throws -> String?
   func moveTaskToBack() throws -> Bool?
   func isConnected() throws -> Bool?
   func getSimInfo() throws -> String?
-  func reconnect() throws -> Bool?
   func getChannel() throws -> String?
 }
 
@@ -145,8 +144,20 @@ class CoreApiSetup {
         let socksPortArg = args[1] as! Int64
         let tunnelConfigArg = args[2] as! String
         let configJsonArg = args[3] as! String
+        let remainTimeArg = args[4] as! Int64
+        let isCountdownArg = args[5] as! Bool
+        let allowVpnAppsArg = args[6] as! [String]
+        let disallowVpnAppsArg = args[7] as! [String]
+        let accessTokenArg = args[8] as! String
+        let aesKeyArg = args[9] as! String
+        let aesIvArg = args[10] as! String
+        let locationIdArg = args[11] as! Int64
+        let locationCodeArg = args[12] as! String
+        let baseUrlsArg = args[13] as! [String]
+        let paramsArg = args[14] as! String
+        let peekTimeIntervalArg = args[15] as! Int64
         do {
-          let result = try api.connect(sessionId: sessionIdArg, socksPort: socksPortArg, tunnelConfig: tunnelConfigArg, configJson: configJsonArg)
+          let result = try api.connect(sessionId: sessionIdArg, socksPort: socksPortArg, tunnelConfig: tunnelConfigArg, configJson: configJsonArg, remainTime: remainTimeArg, isCountdown: isCountdownArg, allowVpnApps: allowVpnAppsArg, disallowVpnApps: disallowVpnAppsArg, accessToken: accessTokenArg, aesKey: aesKeyArg, aesIv: aesIvArg, locationId: locationIdArg, locationCode: locationCodeArg, baseUrls: baseUrlsArg, params: paramsArg, peekTimeInterval: peekTimeIntervalArg)
           reply(wrapResult(result))
         } catch {
           reply(wrapError(error))
@@ -233,19 +244,6 @@ class CoreApiSetup {
     } else {
       getSimInfoChannel.setMessageHandler(nil)
     }
-    let reconnectChannel = FlutterBasicMessageChannel(name: "dev.flutter.pigeon.app.xixi.nomo.CoreApi.reconnect\(channelSuffix)", binaryMessenger: binaryMessenger, codec: codec)
-    if let api = api {
-      reconnectChannel.setMessageHandler { _, reply in
-        do {
-          let result = try api.reconnect()
-          reply(wrapResult(result))
-        } catch {
-          reply(wrapError(error))
-        }
-      }
-    } else {
-      reconnectChannel.setMessageHandler(nil)
-    }
     let getChannelChannel = FlutterBasicMessageChannel(name: "dev.flutter.pigeon.app.xixi.nomo.CoreApi.getChannel\(channelSuffix)", binaryMessenger: binaryMessenger, codec: codec)
     if let api = api {
       getChannelChannel.setMessageHandler { _, reply in

+ 62 - 4
lib/app/api/base/base_api.dart

@@ -4,6 +4,7 @@ import 'package:dio/dio.dart';
 import 'package:dio/io.dart';
 import 'package:get/get.dart' as getx;
 import '../../../config/translations/strings_enum.dart';
+import '../../../utils/api_statistics.dart';
 import '../../../utils/log/logger.dart';
 import '../../components/country_restricted_overlay.dart';
 import '../../data/models/api_result.dart';
@@ -292,6 +293,8 @@ abstract class BaseApi {
   }) async {
     final retryState = _retryStates[domainType]!;
     log('BaseApi', 'Request: $method ${dio.options.baseUrl}$path');
+    // 记录请求开始时间
+    final requestStartTime = DateTime.now().millisecondsSinceEpoch;
     try {
       // 重置重试状态
       if (!retryState.isRetrying) {
@@ -359,9 +362,19 @@ abstract class BaseApi {
       }
       // 重置重试状态
       retryState.reset();
-
+      // 计算响应耗时
+      final responseTime =
+          DateTime.now().millisecondsSinceEpoch - requestStartTime;
       // 先处理特殊状态码,不进行解密
       if (response.statusCode == 204) {
+        ApiStatistics.instance.addFailure(
+          domain: _baseUrl,
+          path: path,
+          apiRequestTime: requestStartTime,
+          apiResponseTime: responseTime,
+          errorCode: response.statusCode ?? 204,
+          errorMessage: Strings.regionRestricted.tr,
+        );
         getx.Get.offAll(
           () => const CountryRestrictedOverlay(),
           transition: getx.Transition.fadeIn,
@@ -376,11 +389,42 @@ abstract class BaseApi {
 
       // 正常状态码才进行解密
       if (response.statusCode == 200) {
-        return getApiResult(decrypt(response.data));
+        final result = getApiResult(decrypt(response.data));
+        // 记录统计
+        if (result.success) {
+          ApiStatistics.instance.addSuccess(
+            domain: _baseUrl,
+            path: path,
+            apiRequestTime: requestStartTime,
+            apiResponseTime: responseTime,
+            errorCode: response.statusCode ?? 200,
+          );
+        } else {
+          ApiStatistics.instance.addFailure(
+            domain: _baseUrl,
+            path: path,
+            apiRequestTime: requestStartTime,
+            apiResponseTime: responseTime,
+            errorCode: int.tryParse(result.errorCode ?? '0') ?? 0,
+            errorMessage: result.errorMessage ?? '',
+          );
+        }
+        return result;
       }
-
       // 其他状态码返回原始数据
-      return getApiResult(response.data);
+      final result = getApiResult(response.data);
+
+      // 记录失败统计
+      ApiStatistics.instance.addFailure(
+        domain: _baseUrl,
+        path: path,
+        apiRequestTime: requestStartTime,
+        apiResponseTime: responseTime,
+        errorCode: response.statusCode ?? 0,
+        errorMessage: result.errorMessage ?? 'Unknown error',
+      );
+
+      return result;
     } on DioException catch (e) {
       // 检查是否应该重试
       if (_shouldRetryError(e, domainType)) {
@@ -405,6 +449,20 @@ abstract class BaseApi {
         );
       }
 
+      // 计算响应耗时
+      final responseTime =
+          DateTime.now().millisecondsSinceEpoch - requestStartTime;
+
+      // 记录失败统计
+      ApiStatistics.instance.addFailure(
+        domain: _baseUrl,
+        path: path,
+        apiRequestTime: requestStartTime,
+        apiResponseTime: responseTime,
+        errorCode: e.response?.statusCode ?? -1,
+        errorMessage: e.message ?? e.type.name,
+      );
+
       // 重置重试状态
       retryState.reset();
       rethrow;

+ 5 - 11
lib/app/api/core/api_core.dart

@@ -12,7 +12,6 @@ import '../../constants/errors.dart';
 import '../../constants/keys.dart';
 import '../../data/models/api_exception.dart';
 import '../../data/models/api_result.dart';
-import '../../data/models/disconnect_domain.dart';
 import '../../data/models/launch/user.dart';
 import '../../routes/app_pages.dart';
 import '../base/base_api.dart';
@@ -63,16 +62,6 @@ class ApiCore extends BaseApi {
           return handler.next(options);
         },
         onError: (DioException error, handler) async {
-          IXSP.addDisconnectDomain(
-            DisconnectDomain(
-              domain: error.requestOptions.baseUrl,
-              status: error.response?.statusCode ?? -1,
-              msg: error.message ?? '',
-              startTime: DateTime.now().millisecondsSinceEpoch,
-              endTime: DateTime.now().millisecondsSinceEpoch,
-              count: 1,
-            ),
-          );
           if (error.response?.statusCode == Errors.eRegionNotAvailable) {
             // 403 eRegionNotAvailable 当前区域不可用
             // 这里可以发送事件通知UI层处理
@@ -262,6 +251,11 @@ class ApiCore extends BaseApi {
     return post(ApiCorePaths.launch, data: data);
   }
 
+  /// 刷新全量数据
+  Future<ApiResult> refreshLaunch(dynamic data) async {
+    return post(ApiCorePaths.refreshLaunch, data: data);
+  }
+
   /// 注册
   Future<ApiResult> register(dynamic data) async {
     return post(ApiCorePaths.register, data: data);

+ 6 - 0
lib/app/api/core/api_core_paths.dart

@@ -8,6 +8,9 @@ class ApiCorePaths {
   /// 获取全量数据
   static const String launch = '$_ver/app/launch';
 
+  /// 刷新全量数据
+  static const String refreshLaunch = '$_ver/app/refreshLaunch';
+
   /// 刷新token
   static const String refreshToken = '$_ver/user/refreshToken';
 
@@ -87,4 +90,7 @@ class ApiCorePaths {
 
   /// 恢复购买
   static const String restore = '$_ver/pay/restore';
+
+  /// 连接成功
+  static const String connected = '$_ver/router/connected';
 }

+ 0 - 11
lib/app/api/file/api_file.dart

@@ -9,7 +9,6 @@ import '../../../utils/log/logger.dart';
 import '../../constants/keys.dart';
 import '../../data/models/api_exception.dart';
 import '../../data/models/api_result.dart';
-import '../../data/models/disconnect_domain.dart';
 import '../../data/sp/ix_sp.dart';
 import '../base/base_api.dart';
 
@@ -57,16 +56,6 @@ class ApiFile extends BaseApi {
           return handler.next(options);
         },
         onError: (DioException error, handler) async {
-          IXSP.addDisconnectDomain(
-            DisconnectDomain(
-              domain: error.requestOptions.baseUrl,
-              status: error.response?.statusCode ?? -1,
-              msg: error.message ?? '',
-              startTime: DateTime.now().millisecondsSinceEpoch,
-              endTime: DateTime.now().millisecondsSinceEpoch,
-              count: 1,
-            ),
-          );
           return handler.next(error);
         },
       ),

+ 0 - 11
lib/app/api/log/api_log.dart

@@ -9,7 +9,6 @@ import '../../../utils/log/logger.dart';
 import '../../constants/keys.dart';
 import '../../data/models/api_exception.dart';
 import '../../data/models/api_result.dart';
-import '../../data/models/disconnect_domain.dart';
 import '../../data/sp/ix_sp.dart';
 import '../base/base_api.dart';
 
@@ -56,16 +55,6 @@ class ApiLog extends BaseApi {
           return handler.next(options);
         },
         onError: (DioException error, handler) async {
-          IXSP.addDisconnectDomain(
-            DisconnectDomain(
-              domain: error.requestOptions.baseUrl,
-              status: error.response?.statusCode ?? -1,
-              msg: error.message ?? '',
-              startTime: DateTime.now().millisecondsSinceEpoch,
-              endTime: DateTime.now().millisecondsSinceEpoch,
-              count: 1,
-            ),
-          );
           return handler.next(error);
         },
       ),

+ 5 - 11
lib/app/api/router/api_router.dart

@@ -9,7 +9,6 @@ import '../../../utils/log/logger.dart';
 import '../../constants/keys.dart';
 import '../../data/models/api_exception.dart';
 import '../../data/models/api_result.dart';
-import '../../data/models/disconnect_domain.dart';
 import '../../data/sp/ix_sp.dart';
 import '../base/base_api.dart';
 
@@ -56,16 +55,6 @@ class ApiRouter extends BaseApi {
           return handler.next(options);
         },
         onError: (DioException error, handler) async {
-          IXSP.addDisconnectDomain(
-            DisconnectDomain(
-              domain: error.requestOptions.baseUrl,
-              status: error.response?.statusCode ?? -1,
-              msg: error.message ?? '',
-              startTime: DateTime.now().millisecondsSinceEpoch,
-              endTime: DateTime.now().millisecondsSinceEpoch,
-              count: 1,
-            ),
-          );
           return handler.next(error);
         },
       ),
@@ -132,4 +121,9 @@ class ApiRouter extends BaseApi {
       cancelToken: cancelToken,
     );
   }
+
+  /// 连接成功
+  Future<ApiResult> connected(dynamic data, {CancelToken? cancelToken}) async {
+    return post(ApiCorePaths.connected, data: data, cancelToken: cancelToken);
+  }
 }

+ 5 - 0
lib/app/constants/api_domains.dart

@@ -595,4 +595,9 @@ class ApiDomains {
     await _saveApiUrlsIndex();
     return false;
   }
+
+  // 获取所有logUrl
+  List<String> getAllLogUrls() {
+    return _logUrls;
+  }
 }

+ 1 - 0
lib/app/constants/assets.dart

@@ -61,6 +61,7 @@ class Assets {
   static const String connectionNetworkError = 'assets/images/network.png';
 
   static const String premium = 'assets/images/premium.png';
+  static const String test = 'assets/images/test.png';
   static const String free = 'assets/images/free.png';
 
   // 评价

+ 12 - 5
lib/app/constants/enums.dart

@@ -61,6 +61,16 @@ enum FirebaseEvent {
   restore,
 }
 
+enum LogModule {
+  NM_Metrics, //游戏指标
+  NM_Log, // core es 日志
+  NM_BoostResult, // 加速结果
+  NM_PeekLog, // 探针日志
+  NM_ApiLaunchLog, // launch接口报错
+  NM_ApiRouterLog, // router接口报错
+  NM_ApiOtherLog, // 其他接口报错
+}
+
 enum ConnectionState {
   disconnected, // 默认断开状态
   connecting, // 连接中状态
@@ -70,8 +80,7 @@ enum ConnectionState {
 
 enum MemberLevel {
   guest(1, 'Guest'),
-  normal(2, 'Normal'),
-  vip(3, 'VIP');
+  normal(2, 'Normal');
 
   final int level;
   final String label;
@@ -92,9 +101,7 @@ enum VpnStatus {
   idle(0, '空闲状态'), // 空闲状态
   connecting(1, '连接中状态'), // 连接中状态
   connected(2, '连接成功状态'), // 连接成功状态
-  error(3, '连接错误状态'), // 连接错误状态
-  serviceDisconnected(1000, '服务异常断开连接'), // 服务异常断开连接
-  permissionDenied(1001, '权限拒绝'); // 权限拒绝
+  error(3, '连接错误状态'); // 连接错误状态
 
   final int value;
   final String label;

+ 9 - 49
lib/app/constants/errors.dart

@@ -20,55 +20,15 @@ class Errors {
   // token实效
   static const int eTokenExpired = 401;
 
-  // VPN 错误代码(500-599)
-  static const int eMethodCall = 400;
-  static const int eVpnUserAuth = 500; //用户未授权
-  static const int eVpnUserStatus = 501; //用户被禁用或者是需续费
-  static const int eVpnMaxDevice = 502; //设备数到上限
-  static const int eVpnConnectServer = 503; //连接服务器失败
-  static const int eVpnRedisReadError = 504; //Redis读取错误,无法获得用户信息
-  static const int eVpnIpRegion = 505; //IP地区不一致
-  static const int eVpnUserExpire = 506; //用户过期
-  static const int eVpnUserLevelError = 507; //会员级别不一致
-  static const int eVpnServerOverload = 508; //服务器负载上限
-  static const int eVpnNoServer = 509; //没有服务器
-  static const int eVpnInvalidDeviceId = 510; //用户账号下不存在该设备Id
-  static const int eVpnTrialTimeLimited = 511; //试用时长上限
-  static const int eVpnTellRetry = 512; //重试错误
-  static const int eVpnBadParam = 513; //参数错误
-  static const int eVpnConnectRouter = 514; //连接router超时
-  static const int eVpnInit = 515; //初始化失败
-
   // 自定义错误码
-  static const int eVpnServerKilled = 1111; // 服务器异常kill
-  static const int eVpnAutoStop = 1112; // 开启vpn时切换后台自动关闭
-  static const int eVpnNotPermission = 1113; // vpn没有授权
-
-  // VPN 错误代码文字描述
-  static const Map<int, String> vpnErrorTexts = {
-    eMethodCall: '接口方法调用失败',
-    eVpnUserAuth: '用户未授权',
-    eVpnUserStatus: '用户被禁用或者是需续费',
-    eVpnMaxDevice: '设备数到上限',
-    eVpnConnectServer: '连接服务器失败',
-    eVpnRedisReadError: 'Redis读取错误无法获得用户信息',
-    eVpnIpRegion: 'IP地区不一致',
-    eVpnUserExpire: '用户过期',
-    eVpnUserLevelError: '会员级别不一致',
-    eVpnServerOverload: '服务器负载上限',
-    eVpnNoServer: '没有服务器',
-    eVpnInvalidDeviceId: '用户账号下不存在该设备Id',
-    eVpnTrialTimeLimited: '试用时长上限',
-    eVpnTellRetry: '重试错误',
-    eVpnBadParam: '参数错误',
-    eVpnConnectRouter: '连接route失败',
-    eVpnInit: '初始化失败',
-    eVpnServerKilled: '服务器异常kill',
-  };
+  static const int ERROR_NODE_TIMEOUT = 500; // 节点连接超时
+  static const int ERROR_NO_NODE = 501; // 没有节点
 
-  // Subscribe 错误代码
-  static const String eQueryProduct = '600';
-  static const String ePurchaseSubscription = '601';
-  static const String eRestoreSubscription = '602';
-  static const String eVerifyPurchase = '604';
+  static const int ERROR_INIT = 1100; // vpn初始化失败
+  static const int ERROR_KILL = 1101; // 服务异常kill
+  static const int ERROR_REVOKE = 1102; // 系统强杀
+  static const int ERROR_SERVICE_EMPTY = 1103; // 服务器节点返回空
+  static const int ERROR_ROUTER = 1104; // 调度失败
+  static const int ERROR_PERMISSION_DENIED = 1105; // 拒绝权限
+  static const int ERROR_REMAIN_TIME = 1106; // 没有可用时间
 }

+ 424 - 23
lib/app/controllers/api_controller.dart

@@ -1,24 +1,30 @@
+import 'dart:async';
 import 'dart:convert';
 import 'dart:io';
 
 import 'package:device_info_plus/device_info_plus.dart';
 import 'package:dio/dio.dart';
+import 'package:flutter/material.dart';
 
 import 'package:get/get.dart';
 import 'package:nomo/app/api/router/api_router.dart';
 import 'package:nomo/app/data/sp/ix_sp.dart';
 import 'package:package_info_plus/package_info_plus.dart';
+import 'package:path_provider/path_provider.dart';
 import 'package:play_install_referrer/play_install_referrer.dart';
 
 import '../../config/translations/localization_service.dart';
 import '../../config/translations/strings_enum.dart';
 import '../../pigeons/core_api.g.dart';
+import '../../utils/api_statistics.dart';
 import '../../utils/device_manager.dart';
 import '../../utils/geo_downloader.dart';
 import '../../utils/log/logger.dart';
 import '../../utils/network_helper.dart';
 
+import '../../utils/ntp_time_service.dart';
 import '../api/core/api_core.dart';
+import '../api/log/api_log.dart';
 import '../components/country_restricted_overlay.dart';
 import '../components/ix_snackbar.dart';
 import '../constants/api_domains.dart';
@@ -34,7 +40,7 @@ import '../data/models/launch/groups.dart';
 import '../data/models/launch/launch.dart';
 import '../dialog/all_dialog.dart';
 
-class ApiController extends GetxService {
+class ApiController extends GetxService with WidgetsBindingObserver {
   final TAG = 'ApiController';
 
   // 记录是否已经显示禁用弹窗
@@ -45,6 +51,26 @@ class ApiController extends GetxService {
   bool get isGuest => _isGuest.value;
   set isGuest(bool value) => _isGuest.value = value;
 
+  //是否是会员
+  final _isPremium = false.obs;
+  bool get isPremium => _isPremium.value;
+  set isPremium(bool value) => _isPremium.value = value;
+
+  //用户等级
+  final _userLevel = 1.obs;
+  int get userLevel => _userLevel.value;
+  set userLevel(int value) => _userLevel.value = value;
+
+  // 过期时间文本
+  final _expireTimeText = ''.obs;
+  String get expireTimeText => _expireTimeText.value;
+  set expireTimeText(String value) => _expireTimeText.value = value;
+
+  // 有效期文本
+  final _validTermText = ''.obs;
+  String get validTermText => _validTermText.value;
+  set validTermText(String value) => _validTermText.value = value;
+
   //全部节点列表
   final _nodesList = <LocationList>[].obs;
   List<LocationList> get nodesList => _nodesList.value;
@@ -53,6 +79,47 @@ class ApiController extends GetxService {
   //初始化fingerprint
   Fingerprint fp = Fingerprint.empty();
 
+  // 全局剩余时间倒计时(秒)
+  final _remainTimeSeconds = 0.obs;
+  int get remainTimeSeconds => _remainTimeSeconds.value;
+  set remainTimeSeconds(int value) => _remainTimeSeconds.value = value;
+
+  // 格式化后的剩余时间字符串
+  String get remainTimeFormatted => _formatRemainTime(_remainTimeSeconds.value);
+
+  // 倒计时定时器
+  Timer? _remainTimeTimer;
+
+  // 是否在后台
+  bool isBackground = false;
+
+  @override
+  void onInit() {
+    super.onInit();
+    WidgetsBinding.instance.addObserver(this);
+  }
+
+  @override
+  void onClose() {
+    WidgetsBinding.instance.removeObserver(this);
+    super.onClose();
+  }
+
+  @override
+  void didChangeAppLifecycleState(AppLifecycleState state) {
+    log("App state: $state");
+    if (state == AppLifecycleState.paused) {
+      isBackground = true;
+      ApiStatistics.instance.onAppPaused();
+    } else if (state == AppLifecycleState.resumed) {
+      if (isBackground) {
+        isBackground = false;
+        asyncHandleLaunch(isRefreshLaunch: true);
+        ApiStatistics.instance.onAppResumed();
+      }
+    }
+  }
+
   Future<Fingerprint> initFingerprint() async {
     // 读取app发布渠道
     if (Platform.isIOS) {
@@ -152,7 +219,13 @@ class ApiController extends GetxService {
       if (launch != null) {
         // 初始化用户状态
         isGuest = launch.userConfig?.memberLevel == MemberLevel.guest.level;
+        userLevel = launch.userConfig?.userLevel ?? 1;
+        isPremium = userLevel == 3 || userLevel == 9999;
+        expireTimeText = _getExpireTimeText();
+        validTermText = _getValidTermText();
+        updateRemainTime(launch.userConfig?.remainTime ?? 0);
 
+        NtpTimeService().initLaunchInitialTime();
         // 设置路由和节点
         nodesList = launch.groups?.normal?.list ?? [];
 
@@ -186,13 +259,6 @@ class ApiController extends GetxService {
       try {
         ApiCore().setbaseUrl(ApiDomains.instance.getApiUrl());
         final request = fp.toJson();
-        if (IXSP.getDisconnectDomains().isNotEmpty) {
-          final disconnectDomainList = IXSP
-              .getDisconnectDomains()
-              .map((e) => e.toJson())
-              .toList();
-          request['disconnectDomainList'] = disconnectDomainList;
-        }
         final result = await ApiCore().launch(request);
 
         if (!result.success) {
@@ -206,9 +272,6 @@ class ApiController extends GetxService {
               ? FirebaseEvent.launchCacheSuccess
               : FirebaseEvent.launchSuccess,
         );
-        if (IXSP.getDisconnectDomains().isNotEmpty) {
-          IXSP.clearDisconnectDomains();
-        }
         // 重置禁用状态
         IXSP.setLastIsRegionDisabled(false);
         IXSP.setLastIsUserDisabled(false);
@@ -265,9 +328,80 @@ class ApiController extends GetxService {
     }
   }
 
-  Future<void> asyncHandleLaunch() async {
+  Future<Launch> refreshLaunch() async {
+    while (true) {
+      try {
+        ApiCore().setbaseUrl(ApiDomains.instance.getApiUrl());
+        final request = fp.toJson();
+        final result = await ApiCore().refreshLaunch(request);
+
+        if (!result.success) {
+          throw Failure(
+            code: result.errorCode ?? '',
+            message: result.errorMessage ?? '',
+          );
+        }
+        // 重置禁用状态
+        IXSP.setLastIsRegionDisabled(false);
+        IXSP.setLastIsUserDisabled(false);
+
+        final launchData = Launch.fromJson(result.data);
+
+        // 设置扩展数据
+        fp.exData = launchData.exData;
+
+        // 更新URL列表
+        await ApiDomains.instance.updateFromLaunch(launchData);
+
+        // 保存Launch数据
+        await IXSP.saveLaunch(launchData);
+
+        // 初始化Launch
+        await initData(launchData);
+
+        return launchData;
+      } on ApiException catch (e) {
+        final url = await ApiDomains.instance.getNextApiUrl();
+        log(TAG, 'refresh launch request failed for URL $url: $e');
+        if (url.isEmpty) {
+          rethrow;
+        }
+        ApiCore().setbaseUrl(url);
+      } on Failure catch (_) {
+        rethrow;
+      } on DioException catch (e) {
+        if (e.response?.statusCode == Errors.eRegionNotAvailable ||
+            e.response?.statusCode == Errors.eUserDisabled ||
+            e.response?.statusCode == Errors.eTokenExpired) {
+          rethrow;
+        } else {
+          if (await NetworkHelper.instance.isNetworkAvailable()) {
+            final url = await ApiDomains.instance.getNextApiUrl();
+            log(TAG, 'refresh launch request failed for URL $url: $e');
+            if (url.isEmpty) {
+              rethrow;
+            }
+            ApiCore().setbaseUrl(url);
+          } else {
+            rethrow;
+          }
+        }
+      } catch (e) {
+        final url = await ApiDomains.instance.getNextApiUrl();
+        log(TAG, 'refresh launch request failed for URL $url: $e');
+        if (url.isEmpty) {
+          rethrow;
+        }
+        ApiCore().setbaseUrl(url);
+      }
+    }
+  }
+
+  Future<void> asyncHandleLaunch({bool isRefreshLaunch = false}) async {
     try {
-      final data = await launch(isCache: true);
+      final data = isRefreshLaunch
+          ? await refreshLaunch()
+          : await launch(isCache: true);
       final isVpnRunning = await CoreApi().isConnected() ?? false;
       if (!isVpnRunning) {
         await checkUpdate();
@@ -388,13 +522,6 @@ class ApiController extends GetxService {
       try {
         ApiRouter().setbaseUrl(ApiDomains.instance.getRouterUrl());
         final request = fp.toJson();
-        if (IXSP.getDisconnectDomains().isNotEmpty) {
-          final disconnectDomainList = IXSP
-              .getDisconnectDomains()
-              .map((e) => e.toJson())
-              .toList();
-          request['disconnectDomainList'] = disconnectDomainList;
-        }
         request['locationId'] = locationId;
         request['locationCode'] = locationCode;
         final result = await ApiRouter().getDispatchInfo(
@@ -408,9 +535,6 @@ class ApiController extends GetxService {
             message: result.errorMessage ?? '',
           );
         }
-        if (IXSP.getDisconnectDomains().isNotEmpty) {
-          IXSP.clearDisconnectDomains();
-        }
         // 重置禁用状态
         IXSP.setLastIsRegionDisabled(false);
         IXSP.setLastIsUserDisabled(false);
@@ -587,6 +711,8 @@ class ApiController extends GetxService {
 
       // 保存 Launch 数据
       await IXSP.saveLaunch(launchData);
+
+      await initData(launchData);
       return launchData;
     } catch (e) {
       rethrow;
@@ -610,6 +736,8 @@ class ApiController extends GetxService {
 
       // 保存 Launch 数据
       await IXSP.saveLaunch(launchData);
+
+      await initData(launchData);
       return launchData;
     } catch (e) {
       rethrow;
@@ -715,6 +843,8 @@ class ApiController extends GetxService {
 
       // 保存 Launch 数据
       await IXSP.saveLaunch(launchData);
+      // 初始化Launch
+      await initData(launchData);
       return launchData;
     } catch (e) {
       rethrow;
@@ -738,9 +868,280 @@ class ApiController extends GetxService {
 
       // 保存 Launch 数据
       await IXSP.saveLaunch(launchData);
+      // 初始化Launch
+      await initData(launchData);
       return launchData;
     } catch (e) {
       rethrow;
     }
   }
+
+  Future<void> connected(Map<String, dynamic> params) async {
+    try {
+      final request = fp.toJson();
+      request.addAll(params);
+      final result = await ApiRouter().connected(request);
+      if (!result.success) {
+        throw Failure(
+          code: result.errorCode ?? '',
+          message: result.errorMessage ?? '',
+        );
+      }
+    } catch (e) {
+      rethrow;
+    }
+  }
+
+  Future<String> uploadLogs(List<dynamic> items, {bool isCache = false}) async {
+    await updateFingerprintData();
+    Map<String, dynamic> request = fp.toJson();
+    request['items'] = items;
+    while (true) {
+      try {
+        ApiLog().setbaseUrl(ApiDomains.instance.getLogUrl());
+        final result = await ApiLog().uploadLogs(request);
+        if (!result.success) {
+          throw Failure(
+            code: result.errorCode ?? '',
+            message: result.errorMessage ?? '',
+          );
+        }
+        return result.errorMessage ?? '';
+      } on ApiException catch (_) {
+        rethrow;
+      } on Failure catch (_) {
+        rethrow;
+      } on DioException catch (e) {
+        if (e.response?.statusCode == Errors.eRegionNotAvailable ||
+            e.response?.statusCode == Errors.eUserDisabled ||
+            e.response?.statusCode == Errors.eTokenExpired) {
+          if (isCache) {
+            await _cacheLogRequest(items);
+          }
+          rethrow;
+        } else {
+          if (await NetworkHelper.instance.isNetworkAvailable()) {
+            final url = await ApiDomains.instance.getNextLogUrl();
+            if (url.isEmpty) {
+              if (isCache) {
+                await _cacheLogRequest(items);
+              }
+              rethrow;
+            }
+            log(
+              TAG,
+              'uploadLogs request failed for URL ${ApiLog().baseUrl}: $e',
+            );
+            ApiLog().setbaseUrl(url);
+          } else {
+            if (isCache) {
+              await _cacheLogRequest(items);
+            }
+            rethrow;
+          }
+        }
+      } catch (e) {
+        final url = await ApiDomains.instance.getNextLogUrl();
+        if (url.isEmpty) {
+          if (isCache) {
+            await _cacheLogRequest(items);
+          }
+          rethrow;
+        }
+        log(TAG, 'uploadLogs request failed for URL ${ApiLog().baseUrl}: $e');
+        ApiLog().setbaseUrl(url);
+      }
+    }
+  }
+
+  // 缓存日志请求数据
+  Future<void> _cacheLogRequest(dynamic items) async {
+    try {
+      final dir = await getApplicationDocumentsDirectory();
+      final cacheDir = Directory('${dir.path}/log_cache');
+      if (!await cacheDir.exists()) {
+        await cacheDir.create(recursive: true);
+      }
+
+      // 生成唯一的文件名
+      final timestamp = DateTime.now().millisecondsSinceEpoch;
+      final fileName = 'log_$timestamp.json';
+      final file = File('${cacheDir.path}/$fileName');
+
+      // 将请求数据写入文件
+      await file.writeAsString(jsonEncode(items));
+
+      // 清理缓存目录
+      await _cleanupCacheDirectory(cacheDir);
+    } catch (e) {
+      log(TAG, 'Failed to cache log request: $e');
+    }
+  }
+
+  // 清理缓存目录
+  Future<void> _cleanupCacheDirectory(Directory cacheDir) async {
+    try {
+      const maxFiles = 10; // 最多保留10个文件
+      const maxCacheSize = 5 * 1024 * 1024; // 5MB
+      final files = await cacheDir.list().toList();
+
+      // 按修改时间排序
+      files.sort(
+        (a, b) => a.statSync().modified.compareTo(b.statSync().modified),
+      );
+
+      // 计算当前缓存大小
+      int totalSize = 0;
+      for (var file in files) {
+        totalSize += file.statSync().size;
+      }
+
+      // 如果超过文件数量或大小限制,删除最旧的文件
+      while ((files.length > maxFiles || totalSize > maxCacheSize) &&
+          files.isNotEmpty) {
+        final oldestFile = files.removeAt(0);
+        totalSize -= oldestFile.statSync().size;
+        await oldestFile.delete();
+      }
+    } catch (e) {
+      log(TAG, 'Failed to cleanup cache directory: $e');
+    }
+  }
+
+  Future<void> uploadApiStatisticsLog(
+    List<Map<String, dynamic>> logs, {
+    LogModule module = LogModule.NM_ApiLaunchLog,
+  }) async {
+    if (isNeedUploadLogs(module)) {
+      await uploadLogs(logs);
+    }
+  }
+
+  // 判断是否需要上传日志
+  bool isNeedUploadLogs(LogModule module) {
+    final launch = IXSP.getLaunch();
+    if (launch == null) {
+      return false;
+    }
+    if (launch.appConfig?.disabledLogModules?.contains(module.name) ?? false) {
+      return false;
+    }
+    return true;
+  }
+
+  /// 获取订阅周期类型文本
+  /// subscribeType: 1Day 2Week 3Month 4Year
+  String _getSubscribeTypeText() {
+    final user = IXSP.getUser();
+    final planInfo = user?.planInfo;
+
+    // 仅当 isSubscribe=true 时有效
+    if (planInfo?.isSubscribe != true) {
+      return planInfo?.subTitle ?? '';
+    }
+
+    switch (planInfo?.subscribeType) {
+      case 1:
+        return 'Day';
+      case 2:
+        return 'Week';
+      case 3:
+        return 'Month';
+      case 4:
+        return 'Year';
+      default:
+        return '';
+    }
+  }
+
+  /// 获取过期时间文本
+  String _getExpireTimeText() {
+    final user = IXSP.getUser();
+    final expireTime = user?.expireTime;
+
+    if (expireTime == null || expireTime == 0) {
+      return '';
+    }
+
+    // 时间戳转日期(秒级时间戳)
+    final date = DateTime.fromMillisecondsSinceEpoch(expireTime * 1000);
+    final formatted =
+        "${date.year.toString().padLeft(4, '0')}-"
+        "${date.month.toString().padLeft(2, '0')}-"
+        "${date.day.toString().padLeft(2, '0')}";
+    return formatted;
+  }
+
+  /// 获取有效期显示文本
+  String _getValidTermText() {
+    final subscribeType = _getSubscribeTypeText();
+    final expireTime = _getExpireTimeText();
+
+    if (subscribeType.isNotEmpty && expireTime.isNotEmpty) {
+      return '$subscribeType / $expireTime';
+    } else if (expireTime.isNotEmpty) {
+      return expireTime;
+    }
+    return '';
+  }
+
+  /// 启动剩余时间倒计时
+  /// [seconds] 剩余时间(秒),例如:3600 表示 1 小时
+  void startRemainTimeCountdown(int seconds) {
+    // 取消之前的定时器
+    _remainTimeTimer?.cancel();
+
+    // 设置初始剩余时间
+    _remainTimeSeconds.value = seconds;
+
+    // 启动每秒倒计时
+    _remainTimeTimer = Timer.periodic(const Duration(seconds: 1), (timer) {
+      if (_remainTimeSeconds.value > 0) {
+        _remainTimeSeconds.value--;
+      } else {
+        // 倒计时结束
+        timer.cancel();
+        _remainTimeTimer = null;
+        _onRemainTimeExpired();
+      }
+    });
+  }
+
+  /// 停止剩余时间倒计时
+  void stopRemainTimeCountdown() {
+    _remainTimeTimer?.cancel();
+    _remainTimeTimer = null;
+  }
+
+  /// 更新剩余时间(从服务器获取新的时间后调用)
+  void updateRemainTime(int seconds) {
+    stopRemainTimeCountdown();
+    if (seconds > 0) {
+      startRemainTimeCountdown(seconds);
+    } else {
+      _remainTimeSeconds.value = 0;
+    }
+  }
+
+  /// 剩余时间到期处理
+  void _onRemainTimeExpired() {
+    log(TAG, 'VIP剩余时间已到期');
+    // 可以在这里添加到期后的处理逻辑,例如:
+    // - 显示续费提示
+    // - 断开VPN连接
+    // - 刷新用户信息
+  }
+
+  /// 格式化剩余时间显示
+  String _formatRemainTime(int totalSeconds) {
+    if (totalSeconds <= 0) {
+      return '00:00:00';
+    }
+
+    final hours = totalSeconds ~/ 3600;
+    final minutes = (totalSeconds % 3600) ~/ 60;
+    final seconds = totalSeconds % 60;
+
+    return '${hours.toString().padLeft(2, '0')}:${minutes.toString().padLeft(2, '0')}:${seconds.toString().padLeft(2, '0')}';
+  }
 }

+ 170 - 52
lib/app/controllers/core_controller.dart

@@ -5,6 +5,8 @@ import 'dart:io';
 import 'package:device_info_plus/device_info_plus.dart';
 import 'package:dio/dio.dart';
 import 'package:get/get.dart';
+import 'package:nomo/app/constants/api_domains.dart';
+import 'package:nomo/app/constants/keys.dart';
 import 'package:package_info_plus/package_info_plus.dart';
 import 'package:uuid/uuid.dart';
 
@@ -16,6 +18,7 @@ import '../../utils/log/logger.dart';
 import '../../utils/network_helper.dart';
 import '../components/ix_snackbar.dart';
 import '../constants/enums.dart';
+import '../constants/errors.dart';
 import '../data/models/api_exception.dart';
 import '../data/models/failure.dart';
 import '../data/models/vpn_message.dart';
@@ -67,7 +70,6 @@ class CoreController extends GetxService {
     CoreApi().isConnected().then((value) {
       if (value == true) {
         state = ConnectionState.connected;
-        CoreApi().reconnect();
       } else {
         state = ConnectionState.disconnected;
       }
@@ -91,6 +93,7 @@ class CoreController extends GetxService {
       CoreApi().disconnect();
       // 延迟300ms
       Future.delayed(const Duration(milliseconds: 300), () {
+        log(TAG, 'selectLocationConnect disconnected = $state');
         state = ConnectionState.connecting;
         getDispatchInfo();
       });
@@ -124,13 +127,47 @@ class CoreController extends GetxService {
       if (_cancelToken == currentToken) {
         _cancelToken = null;
       }
+      final remainTime = launch.userConfig?.remainTime ?? 0;
+
+      if (remainTime <= 0) {
+        _onVpnError(Errors.ERROR_REMAIN_TIME, Strings.remainTimeEnded.tr);
+        return;
+      }
 
       if (state == ConnectionState.connecting) {
         final sessionId = Uuid().v4();
         final socksPort = launch.nodesConfig!.socketPort!;
         final tunnelConfig = launch.nodesConfig!.tunnelConfig!;
         final configJson = jsonEncode(launch.nodesConfig!);
-        CoreApi().connect(sessionId, socksPort, tunnelConfig, configJson);
+
+        // 根据分流隧道设置获取 allowVpnApps 和 disallowVpnApps
+        final splitTunnelingApps = _getSplitTunnelingApps();
+        final allowVpnApps = splitTunnelingApps['allowVpnApps']!;
+        final disallowVpnApps = splitTunnelingApps['disallowVpnApps']!;
+        final accessToken = launch.userConfig?.accessToken ?? '';
+        final peekTimeInterval = launch.appConfig?.peekTimeInterval ?? 600;
+        final aesKey = Keys.aesKey;
+        final aesIv = Keys.aesIv;
+        final baseUrls = ApiDomains.instance.getAllLogUrls();
+        final params = jsonEncode(_apiController.fp);
+        CoreApi().connect(
+          sessionId,
+          socksPort,
+          tunnelConfig,
+          configJson,
+          remainTime,
+          false,
+          allowVpnApps,
+          disallowVpnApps,
+          accessToken,
+          aesKey,
+          aesIv,
+          locationId,
+          locationCode,
+          baseUrls,
+          params,
+          peekTimeInterval,
+        );
       }
     } on DioException catch (e, s) {
       // 只有当前 token 没有被替换时才清空
@@ -163,6 +200,62 @@ class CoreController extends GetxService {
     }
   }
 
+  /// 根据分流隧道设置获取 allowVpnApps 和 disallowVpnApps
+  /// 直接从 IXSP 读取,参考 SplittunnelingController 的逻辑
+  Map<String, List<String>> _getSplitTunnelingApps() {
+    List<String> allowVpnApps = [];
+    List<String> disallowVpnApps = [];
+
+    try {
+      // 读取选中的模式
+      final modeString = IXSP.getString('splittunneling_selected_mode');
+      if (modeString == null) {
+        log(TAG, '分流隧道未设置模式');
+        return {
+          'allowVpnApps': allowVpnApps,
+          'disallowVpnApps': disallowVpnApps,
+        };
+      }
+
+      // 判断模式类型
+      final isExcludeMode = modeString.contains('exclude');
+      final isIncludeMode = modeString.contains('include');
+
+      if (!isExcludeMode && !isIncludeMode) {
+        log(TAG, '分流隧道模式为 none');
+        return {
+          'allowVpnApps': allowVpnApps,
+          'disallowVpnApps': disallowVpnApps,
+        };
+      }
+
+      // 根据模式获取对应的应用列表
+      final key = isExcludeMode
+          ? 'splittunneling_exclude_selected_apps'
+          : 'splittunneling_include_selected_apps';
+
+      final selectedAppsJson = IXSP.getString(key);
+      if (selectedAppsJson != null) {
+        final selectedPackageNames =
+            (jsonDecode(selectedAppsJson) as List<dynamic>).cast<String>();
+
+        if (isIncludeMode) {
+          // include 模式:只有选中的应用走 VPN
+          allowVpnApps = selectedPackageNames;
+          log(TAG, '分流隧道 include 模式,允许的应用: $allowVpnApps');
+        } else {
+          // exclude 模式:选中的应用不走 VPN
+          disallowVpnApps = selectedPackageNames;
+          log(TAG, '分流隧道 exclude 模式,排除的应用: $disallowVpnApps');
+        }
+      }
+    } catch (e) {
+      log(TAG, '获取分流隧道设置失败: $e');
+    }
+
+    return {'allowVpnApps': allowVpnApps, 'disallowVpnApps': disallowVpnApps};
+  }
+
   /// 开始监听来自 Android 的事件
   void _startListeningToEvents() {
     _eventSubscription = onEventChange().listen(
@@ -186,6 +279,9 @@ class CoreController extends GetxService {
         case 'timer_update':
           _handleTimerUpdate(TimerUpdateMessage.fromJson(json));
           break;
+        case 'boost_result':
+          _handleBoostResult(BoostResultMessage.fromJson(json));
+          break;
         default:
           log(TAG, '未知消息类型: $type');
       }
@@ -198,7 +294,7 @@ class CoreController extends GetxService {
     final vpnError = VpnStatus.fromValue(message.status);
     log(
       TAG,
-      'VPN状态变化: ${vpnError.label}, status=${message.status}, message=${message.message}',
+      'VPN状态变化: ${vpnError.label}, status=${message.status}, code=${message.code}, message=${message.message}',
     );
     // 根据状态码处理不同的VPN状态
     switch (vpnError) {
@@ -216,24 +312,16 @@ class CoreController extends GetxService {
         break;
       case VpnStatus.error:
         // error
-        _onVpnError(message.message);
-        break;
-      case VpnStatus.serviceDisconnected:
-        // service disconnected
-        _onVpnServiceDisconnected();
-        break;
-      case VpnStatus.permissionDenied:
-        // permission denied
-        _onVpnPermissionDenied();
+        _onVpnError(message.code, message.message);
         break;
     }
   }
 
   void _handleTimerUpdate(TimerUpdateMessage message) {
-    log(
-      TAG,
-      '计时更新: time=${message.currentTime}, mode=${message.mode}, running=${message.isRunning}, paused=${message.isPaused}',
-    );
+    // log(
+    //   TAG,
+    //   '计时更新: time=${message.currentTime}, mode=${message.mode}, running=${message.isRunning}, paused=${message.isPaused}',
+    // );
 
     timer = _formatTime(message.currentTime);
 
@@ -249,14 +337,33 @@ class CoreController extends GetxService {
     }
   }
 
+  void _handleBoostResult(BoostResultMessage message) async {
+    log(TAG, '加速结果: ${message.toString()}');
+    if (message.success) {
+      try {
+        await _apiController.connected({
+          'locationCode': message.locationCode,
+          'instanceId': message.nodeId,
+        });
+      } catch (e) {
+        log('handleRouterConnected error: $e');
+      }
+    }
+
+    try {
+      final json = jsonDecode(message.param);
+      await _apiController.uploadLogs(json);
+    } catch (e) {
+      log('handleBoostResult error: $e');
+    }
+  }
+
   // VPN状态处理方法
   void _onVpnDisconnected() {
     log(TAG, 'VPN已断开连接');
     // 更新UI状态
-    state = ConnectionState.disconnected;
-    timer = "00:00:00";
-    HapticFeedbackManager.connectionDisconnected();
-    FeedbackBottomSheet.show();
+    _uninitState();
+    // FeedbackBottomSheet.show();
   }
 
   void _onVpnConnecting() {
@@ -272,34 +379,52 @@ class CoreController extends GetxService {
     HapticFeedbackManager.connectionSuccess();
   }
 
-  void _onVpnError(String message) {
-    log(TAG, 'VPN连接错误');
+  void _onVpnError(int code, String message) {
+    log(TAG, 'VPN连接错误: code=$code, message=$message');
     // 显示错误信息
-    state = ConnectionState.disconnected;
-    timer = "00:00:00";
-    HapticFeedbackManager.connectionDisconnected();
-    ErrorDialog.show(
-      message: message == 'null' ? Strings.vpnConnectionError.tr : message,
-    );
+    _uninitState();
+    showErrorDialog(code, message);
   }
 
-  void _onVpnServiceDisconnected() {
-    log(TAG, 'VPN服务异常断开连接');
-    // 显示错误信息
-    state = ConnectionState.disconnected;
-    timer = "00:00:00";
-    HapticFeedbackManager.connectionDisconnected();
-    // 可以显示错误提示
-    ErrorDialog.show(message: Strings.vpnServiceDisconnected.tr);
+  void showErrorDialog(int code, String message) {
+    var errorMessage = message;
+    switch (code) {
+      case Errors.ERROR_NODE_TIMEOUT:
+        errorMessage = Strings.vpnConnectionTimeoutError.tr;
+        break;
+      case Errors.ERROR_NO_NODE:
+        errorMessage = Strings.vpnNoNodeError.tr;
+        break;
+      case Errors.ERROR_INIT:
+        errorMessage = Strings.vpnInitError.tr;
+        break;
+      case Errors.ERROR_KILL:
+        errorMessage = Strings.vpnKillError.tr;
+        break;
+      case Errors.ERROR_REVOKE:
+        errorMessage = Strings.vpnRevokeError.tr;
+        break;
+      case Errors.ERROR_SERVICE_EMPTY:
+        errorMessage = Strings.vpnServiceEmptyError.tr;
+        break;
+      case Errors.ERROR_ROUTER:
+        errorMessage = Strings.vpnRouterError.tr;
+        break;
+      case Errors.ERROR_PERMISSION_DENIED:
+        errorMessage = Strings.vpnPermissionDeniedError.tr;
+        break;
+    }
+    if (errorMessage.isNotEmpty) {
+      ErrorDialog.show(message: errorMessage);
+    }
   }
 
-  void _onVpnPermissionDenied() {
-    log(TAG, 'VPN权限拒绝');
-    // 显示权限拒绝状态
-    state = ConnectionState.disconnected;
-    HapticFeedbackManager.connectionDisconnected();
-    // 可以显示错误提示
-    ErrorDialog.show(message: '权限拒绝');
+  void _uninitState() {
+    if (state != ConnectionState.disconnected) {
+      state = ConnectionState.disconnected;
+      timer = "00:00:00";
+      HapticFeedbackManager.connectionDisconnected();
+    }
   }
 
   // 计时器状态处理方法
@@ -324,18 +449,11 @@ class CoreController extends GetxService {
   // 格式化时间显示
   String _formatTime(int timeMs) {
     final totalSeconds = (timeMs / 1000).abs().round();
-    final days = totalSeconds ~/ 86400; // 86400 = 24 * 3600
-    final hours = (totalSeconds % 86400) ~/ 3600;
+    final hours = totalSeconds ~/ 3600;
     final minutes = (totalSeconds % 3600) ~/ 60;
     final seconds = totalSeconds % 60;
 
-    if (days > 0) {
-      return '$days days ${hours.toString().padLeft(2, '0')}:${minutes.toString().padLeft(2, '0')}:${seconds.toString().padLeft(2, '0')}';
-    } else if (hours > 0) {
-      return '${hours.toString().padLeft(2, '0')}:${minutes.toString().padLeft(2, '0')}:${seconds.toString().padLeft(2, '0')}';
-    } else {
-      return '00:${minutes.toString().padLeft(2, '0')}:${seconds.toString().padLeft(2, '0')}';
-    }
+    return '${hours.toString().padLeft(2, '0')}:${minutes.toString().padLeft(2, '0')}:${seconds.toString().padLeft(2, '0')}';
   }
 
   void handleSnackBarError(dynamic error, StackTrace stackTrace) {

+ 0 - 8
lib/app/data/models/fingerprint.dart

@@ -1,5 +1,4 @@
 import '../../constants/configs.dart';
-import 'disconnect_domain.dart';
 
 /// 设备指纹信息
 ///
@@ -105,9 +104,6 @@ class Fingerprint {
   /// 是否是开发者模式
   int adb;
 
-  /// 连接不上网络的域名
-  List<DisconnectDomain> disconnectDomainList;
-
   /// 扩展数据
   dynamic exData;
 
@@ -148,7 +144,6 @@ class Fingerprint {
     this.isVpn = false,
     this.isConnectedVpn = false,
     this.adb = 2,
-    this.disconnectDomainList = const [],
     this.exData,
   });
 
@@ -184,7 +179,6 @@ class Fingerprint {
       isVpn: false,
       isConnectedVpn: false,
       adb: 2,
-      disconnectDomainList: const [],
       exData: null,
     );
   }
@@ -228,7 +222,6 @@ class Fingerprint {
       isVpn: json['isVpn'] ?? false,
       isConnectedVpn: json['isConnectedVpn'] ?? false,
       adb: json['adb'] ?? 2,
-      disconnectDomainList: json['disconnectDomainList'] ?? const [],
       exData: json['exData'],
     );
   }
@@ -271,7 +264,6 @@ class Fingerprint {
       'isVpn': isVpn,
       'isConnectedVpn': isConnectedVpn,
       'adb': adb,
-      'disconnectDomainList': disconnectDomainList,
       'exData': exData,
     };
   }

+ 13 - 8
lib/app/data/models/launch/user.dart

@@ -1,5 +1,7 @@
 import 'package:freezed_annotation/freezed_annotation.dart';
 import 'package:flutter/foundation.dart';
+
+import '../channelplan/channel_plan_list.dart';
 part 'user.freezed.dart';
 part 'user.g.dart';
 
@@ -15,14 +17,17 @@ class User with _$User {
     String? accountPassword,
     int? createTime, // API 返回 int 类型的时间戳
     bool? geographyEea,
-    int? memberLevel, // 会员等级 1 游客 2 普通用户 3 会员
-    int? userLevel,
-    int? expireTime,
-    int? remainTime,
-    bool? isExpired,
-    bool? isTestUser,
-    bool? isSubscribeUser,
-    Account? account, // API 返回对象类型
+    int? memberLevel, // 1设备用户 2注册用户
+    int? userLevel, // 1试用 2免费 3会员 9999内部
+    int? expireTime, // VIP套餐到期时间
+    int? remainTime, // VIP套餐剩余时间
+    bool? isExpired, // VIP套餐是否过期
+    bool? isTestUser, // 是否是测试用户
+    bool? isSubscribeUser, // 是否是连续订阅用户
+    Account? account,
+    //isSubscribeUser=false 时返回最后一次购买套餐信息
+    //isSubscribeUser=true 时返回当前订阅套餐信息
+    ChannelPlan? planInfo, // userLevel=3时生效
     bool? activated, // 是否激活
   }) = _User;
 

+ 43 - 3
lib/app/data/models/launch/user.freezed.dart

@@ -40,6 +40,7 @@ mixin _$User {
   bool? get isTestUser => throw _privateConstructorUsedError;
   bool? get isSubscribeUser => throw _privateConstructorUsedError;
   Account? get account => throw _privateConstructorUsedError; // API 返回对象类型
+  ChannelPlan? get planInfo => throw _privateConstructorUsedError;
   bool? get activated => throw _privateConstructorUsedError;
 
   /// Serializes this User to a JSON map.
@@ -74,10 +75,12 @@ abstract class $UserCopyWith<$Res> {
     bool? isTestUser,
     bool? isSubscribeUser,
     Account? account,
+    ChannelPlan? planInfo,
     bool? activated,
   });
 
   $AccountCopyWith<$Res>? get account;
+  $ChannelPlanCopyWith<$Res>? get planInfo;
 }
 
 /// @nodoc
@@ -112,6 +115,7 @@ class _$UserCopyWithImpl<$Res, $Val extends User>
     Object? isTestUser = freezed,
     Object? isSubscribeUser = freezed,
     Object? account = freezed,
+    Object? planInfo = freezed,
     Object? activated = freezed,
   }) {
     return _then(
@@ -184,6 +188,10 @@ class _$UserCopyWithImpl<$Res, $Val extends User>
                 ? _value.account
                 : account // ignore: cast_nullable_to_non_nullable
                       as Account?,
+            planInfo: freezed == planInfo
+                ? _value.planInfo
+                : planInfo // ignore: cast_nullable_to_non_nullable
+                      as ChannelPlan?,
             activated: freezed == activated
                 ? _value.activated
                 : activated // ignore: cast_nullable_to_non_nullable
@@ -206,6 +214,20 @@ class _$UserCopyWithImpl<$Res, $Val extends User>
       return _then(_value.copyWith(account: value) as $Val);
     });
   }
+
+  /// Create a copy of User
+  /// with the given fields replaced by the non-null parameter values.
+  @override
+  @pragma('vm:prefer-inline')
+  $ChannelPlanCopyWith<$Res>? get planInfo {
+    if (_value.planInfo == null) {
+      return null;
+    }
+
+    return $ChannelPlanCopyWith<$Res>(_value.planInfo!, (value) {
+      return _then(_value.copyWith(planInfo: value) as $Val);
+    });
+  }
 }
 
 /// @nodoc
@@ -234,11 +256,14 @@ abstract class _$$UserImplCopyWith<$Res> implements $UserCopyWith<$Res> {
     bool? isTestUser,
     bool? isSubscribeUser,
     Account? account,
+    ChannelPlan? planInfo,
     bool? activated,
   });
 
   @override
   $AccountCopyWith<$Res>? get account;
+  @override
+  $ChannelPlanCopyWith<$Res>? get planInfo;
 }
 
 /// @nodoc
@@ -270,6 +295,7 @@ class __$$UserImplCopyWithImpl<$Res>
     Object? isTestUser = freezed,
     Object? isSubscribeUser = freezed,
     Object? account = freezed,
+    Object? planInfo = freezed,
     Object? activated = freezed,
   }) {
     return _then(
@@ -342,6 +368,10 @@ class __$$UserImplCopyWithImpl<$Res>
             ? _value.account
             : account // ignore: cast_nullable_to_non_nullable
                   as Account?,
+        planInfo: freezed == planInfo
+            ? _value.planInfo
+            : planInfo // ignore: cast_nullable_to_non_nullable
+                  as ChannelPlan?,
         activated: freezed == activated
             ? _value.activated
             : activated // ignore: cast_nullable_to_non_nullable
@@ -372,6 +402,7 @@ class _$UserImpl with DiagnosticableTreeMixin implements _User {
     this.isTestUser,
     this.isSubscribeUser,
     this.account,
+    this.planInfo,
     this.activated,
   });
 
@@ -416,11 +447,13 @@ class _$UserImpl with DiagnosticableTreeMixin implements _User {
   final Account? account;
   // API 返回对象类型
   @override
+  final ChannelPlan? planInfo;
+  @override
   final bool? activated;
 
   @override
   String toString({DiagnosticLevel minLevel = DiagnosticLevel.info}) {
-    return 'User(country: $country, countryName: $countryName, userIp: $userIp, accessToken: $accessToken, refreshToken: $refreshToken, accountKey: $accountKey, accountPassword: $accountPassword, createTime: $createTime, geographyEea: $geographyEea, memberLevel: $memberLevel, userLevel: $userLevel, expireTime: $expireTime, remainTime: $remainTime, isExpired: $isExpired, isTestUser: $isTestUser, isSubscribeUser: $isSubscribeUser, account: $account, activated: $activated)';
+    return 'User(country: $country, countryName: $countryName, userIp: $userIp, accessToken: $accessToken, refreshToken: $refreshToken, accountKey: $accountKey, accountPassword: $accountPassword, createTime: $createTime, geographyEea: $geographyEea, memberLevel: $memberLevel, userLevel: $userLevel, expireTime: $expireTime, remainTime: $remainTime, isExpired: $isExpired, isTestUser: $isTestUser, isSubscribeUser: $isSubscribeUser, account: $account, planInfo: $planInfo, activated: $activated)';
   }
 
   @override
@@ -445,6 +478,7 @@ class _$UserImpl with DiagnosticableTreeMixin implements _User {
       ..add(DiagnosticsProperty('isTestUser', isTestUser))
       ..add(DiagnosticsProperty('isSubscribeUser', isSubscribeUser))
       ..add(DiagnosticsProperty('account', account))
+      ..add(DiagnosticsProperty('planInfo', planInfo))
       ..add(DiagnosticsProperty('activated', activated));
   }
 
@@ -484,13 +518,15 @@ class _$UserImpl with DiagnosticableTreeMixin implements _User {
             (identical(other.isSubscribeUser, isSubscribeUser) ||
                 other.isSubscribeUser == isSubscribeUser) &&
             (identical(other.account, account) || other.account == account) &&
+            (identical(other.planInfo, planInfo) ||
+                other.planInfo == planInfo) &&
             (identical(other.activated, activated) ||
                 other.activated == activated));
   }
 
   @JsonKey(includeFromJson: false, includeToJson: false)
   @override
-  int get hashCode => Object.hash(
+  int get hashCode => Object.hashAll([
     runtimeType,
     country,
     countryName,
@@ -509,8 +545,9 @@ class _$UserImpl with DiagnosticableTreeMixin implements _User {
     isTestUser,
     isSubscribeUser,
     account,
+    planInfo,
     activated,
-  );
+  ]);
 
   /// Create a copy of User
   /// with the given fields replaced by the non-null parameter values.
@@ -545,6 +582,7 @@ abstract class _User implements User {
     final bool? isTestUser,
     final bool? isSubscribeUser,
     final Account? account,
+    final ChannelPlan? planInfo,
     final bool? activated,
   }) = _$UserImpl;
 
@@ -585,6 +623,8 @@ abstract class _User implements User {
   @override
   Account? get account; // API 返回对象类型
   @override
+  ChannelPlan? get planInfo;
+  @override
   bool? get activated;
 
   /// Create a copy of User

+ 4 - 0
lib/app/data/models/launch/user.g.dart

@@ -26,6 +26,9 @@ _$UserImpl _$$UserImplFromJson(Map<String, dynamic> json) => _$UserImpl(
   account: json['account'] == null
       ? null
       : Account.fromJson(json['account'] as Map<String, dynamic>),
+  planInfo: json['planInfo'] == null
+      ? null
+      : ChannelPlan.fromJson(json['planInfo'] as Map<String, dynamic>),
   activated: json['activated'] as bool?,
 );
 
@@ -48,6 +51,7 @@ Map<String, dynamic> _$$UserImplToJson(_$UserImpl instance) =>
       'isTestUser': instance.isTestUser,
       'isSubscribeUser': instance.isSubscribeUser,
       'account': instance.account,
+      'planInfo': instance.planInfo,
       'activated': instance.activated,
     };
 

+ 40 - 1
lib/app/data/models/vpn_message.dart

@@ -4,11 +4,13 @@ import 'dart:convert';
 class VpnStatusMessage {
   final String type;
   final int status;
+  final int code;
   final String message;
 
   VpnStatusMessage({
     required this.type,
     required this.status,
+    required this.code,
     required this.message,
   });
 
@@ -16,12 +18,13 @@ class VpnStatusMessage {
     return VpnStatusMessage(
       type: json['type'] ?? '',
       status: json['status'] ?? 0,
+      code: json['code'] ?? 0,
       message: json['message'] ?? '',
     );
   }
 
   Map<String, dynamic> toJson() {
-    return {'type': type, 'status': status, 'message': message};
+    return {'type': type, 'status': status, 'code': code, 'message': message};
   }
 }
 
@@ -61,6 +64,42 @@ class TimerUpdateMessage {
   }
 }
 
+class BoostResultMessage {
+  final String type;
+  final String param;
+  final bool success;
+  final String locationCode;
+  final String nodeId;
+
+  BoostResultMessage({
+    required this.type,
+    required this.param,
+    required this.success,
+    required this.locationCode,
+    required this.nodeId,
+  });
+
+  factory BoostResultMessage.fromJson(Map<String, dynamic> json) {
+    return BoostResultMessage(
+      type: json['type'] ?? '',
+      param: json['param'] ?? '',
+      success: json['success'] ?? false,
+      locationCode: json['locationCode'] ?? '',
+      nodeId: json['nodeId'] ?? '',
+    );
+  }
+
+  Map<String, dynamic> toJson() {
+    return {
+      'type': type,
+      'param': param,
+      'success': success,
+      'locationCode': locationCode,
+      'nodeId': nodeId,
+    };
+  }
+}
+
 class TimerNotificationMessage {
   final String type;
   final String message;

+ 0 - 66
lib/app/data/sp/ix_sp.dart

@@ -7,7 +7,6 @@ import '../../../config/translations/localization_service.dart';
 import '../../../utils/crypto.dart';
 import '../../../utils/log/logger.dart';
 import '../../constants/keys.dart';
-import '../models/disconnect_domain.dart';
 import '../models/launch/app_config.dart';
 import '../models/launch/groups.dart';
 import '../models/launch/launch.dart';
@@ -33,8 +32,6 @@ class IXSP {
   static const String _ignoreVersionKey = 'ignore_version';
   static const String _isNewInstallKey = 'is_new_install';
   static const String _launchDataKey = 'launch_data';
-  //记录网络报错的域名信息
-  static const String _disconnectDomainsKey = 'disconnect_domains';
   //记录最后一次是否是地区禁用
   static const String _isRegionDisabledKey = 'is_region_disabled';
   //记录最后一次是否是用户禁用
@@ -124,69 +121,6 @@ class IXSP {
   static bool getIsNewInstall() =>
       _sharedPreferences.getBool(_isNewInstallKey) ?? true;
 
-  /// 保存 DisconnectDomain 列表,合并 domain+status 相同的数据
-  static Future<void> addDisconnectDomain(DisconnectDomain newItem) async {
-    try {
-      final prefs = _sharedPreferences;
-      // 读取已存在的数据
-      final oldJson = prefs.getString(_disconnectDomainsKey);
-      List<DisconnectDomain> allList = [];
-      if (oldJson != null && oldJson.isNotEmpty) {
-        final List<dynamic> oldList = jsonDecode(oldJson);
-        allList = oldList.map((e) => DisconnectDomain.fromJson(e)).toList();
-      }
-      // 合并新旧数据
-      allList.add(newItem);
-      // 按 domain+status 分组合并
-      final Map<String, DisconnectDomain> merged = {};
-      for (var item in allList) {
-        final key = '${item.domain}_${item.status}';
-        if (!merged.containsKey(key)) {
-          merged[key] = DisconnectDomain(
-            domain: item.domain,
-            status: item.status,
-            msg: item.msg,
-            startTime: item.startTime,
-            endTime: item.endTime,
-            count: item.count,
-          );
-        } else {
-          final exist = merged[key]!;
-          exist.startTime = exist.startTime < item.startTime
-              ? exist.startTime
-              : item.startTime;
-          exist.endTime = exist.endTime > item.endTime
-              ? exist.endTime
-              : item.endTime;
-          exist.count += item.count;
-          // msg 以最新的为准
-          exist.msg = item.msg;
-        }
-      }
-      // 保存合并后的数据
-      final mergedList = merged.values.toList();
-      await prefs.setString(
-        _disconnectDomainsKey,
-        jsonEncode(mergedList.map((e) => e.toJson()).toList()),
-      );
-    } catch (e) {
-      log('IVSP', 'addDisconnectDomain error: $e');
-    }
-  }
-
-  /// 读取 DisconnectDomain 列表
-  static List<DisconnectDomain> getDisconnectDomains() {
-    final jsonStr = _sharedPreferences.getString(_disconnectDomainsKey);
-    if (jsonStr == null || jsonStr.isEmpty) return [];
-    final List<dynamic> list = jsonDecode(jsonStr);
-    return list.map((e) => DisconnectDomain.fromJson(e)).toList();
-  }
-
-  /// 清空 DisconnectDomain 列表
-  static Future<void> clearDisconnectDomains() async {
-    await _sharedPreferences.remove(_disconnectDomainsKey);
-  }
-
   // 记录最后一次是否是用户禁用
   static Future<void> setLastIsUserDisabled(bool isUserDisabled) async {
     await _sharedPreferences.setBool(_isUserDisabledKey, isUserDisabled);

+ 5 - 36
lib/app/modules/account/controllers/account_controller.dart

@@ -2,59 +2,28 @@ import 'package:get/get.dart';
 
 import '../../../../config/translations/strings_enum.dart';
 import '../../../../utils/device_manager.dart';
-import '../../../constants/enums.dart';
 import '../../../controllers/api_controller.dart';
-import '../../../data/sp/ix_sp.dart';
 import '../../../dialog/loading/loading_dialog.dart';
 import '../../../routes/app_pages.dart';
 
 class AccountController extends GetxController {
-  final _apiController = Get.find<ApiController>();
-
-  // 是否是会员(true: Premium, false: Free)
-  final _isPremium = false.obs;
-  bool get isPremium => _isPremium.value;
-  set isPremium(bool value) => _isPremium.value = value;
-
-  //是否是游客
-  final _isGuest = false.obs;
-  bool get isGuest => _isGuest.value;
-  set isGuest(bool value) => _isGuest.value = value;
+  final apiController = Get.find<ApiController>();
 
   // UID
   String uid = '';
 
-  // Free 用户剩余时间
-  final freeTime = '01:60:59 / Days';
-
-  // Premium 用户有效期
-  final validTerm = 'Year / 2026-12-12';
-
-  // 设备授权数量
-  final deviceCount = 1;
-  final maxDeviceCount = 4;
-
-  /// 切换会员状态(用于测试)
-  void togglePremium() {
-    isPremium = !isPremium;
+  void toSubscription() {
+    Get.toNamed(Routes.SUBSCRIPTION);
   }
 
   @override
   void onInit() {
     super.onInit();
-    initIsGuest();
     uid = DeviceManager.getCacheDeviceId().length > 12
         ? '${DeviceManager.getCacheDeviceId().substring(0, 6)}***${DeviceManager.getCacheDeviceId().substring(DeviceManager.getCacheDeviceId().length - 6)}'
         : DeviceManager.getCacheDeviceId();
   }
 
-  void initIsGuest() {
-    final user = IXSP.getUser();
-    if (user != null) {
-      isGuest = user.memberLevel == MemberLevel.guest.level;
-    }
-  }
-
   // 处理退出登录
   Future<void> handleLogout() async {
     await LoadingDialog.show(
@@ -63,7 +32,7 @@ class AccountController extends GetxController {
       successText: Strings.logoutSuccessful.tr,
       onRequest: () async {
         // 执行你的异步请求
-        await _apiController.logout();
+        await apiController.logout();
       },
       onSuccess: () {
         // 成功后的操作
@@ -80,7 +49,7 @@ class AccountController extends GetxController {
       successText: Strings.deleteAccountSuccessful.tr,
       onRequest: () async {
         // 执行你的异步请求
-        await _apiController.deleteAccount();
+        await apiController.deleteAccount();
       },
       onSuccess: () {
         // 成功后的操作

+ 58 - 163
lib/app/modules/account/views/account_view.dart

@@ -29,7 +29,6 @@ class AccountView extends BaseView<AccountController> {
   @override
   Widget buildContent(BuildContext context) {
     return Obx(() {
-      final isPremium = controller.isPremium;
       return SingleChildScrollView(
         padding: EdgeInsets.symmetric(horizontal: 14.w, vertical: 10.w),
         child: Column(
@@ -39,13 +38,13 @@ class AccountView extends BaseView<AccountController> {
             // _buildAccountCard(isPremium),
             _buildAccountSection(),
             // Security Section
-            _buildSectionHeader(Strings.securitySection.tr),
-            _buildSecuritySection(),
+            if (!controller.apiController.isGuest)
+              _buildSectionHeader(Strings.securitySection.tr),
+            if (!controller.apiController.isGuest) _buildSecuritySection(),
 
             20.verticalSpaceFromWidth,
-
             // 底部按钮
-            _buildBottomButtons(isPremium),
+            _buildBottomButtons(),
 
             20.verticalSpaceFromWidth,
           ],
@@ -70,10 +69,6 @@ class AccountView extends BaseView<AccountController> {
           // UID 条目
           _buildUIDItem(),
           _buildDivider(),
-
-          // Time/Term 条目
-          if (isPremium) _buildValidTermItem() else _buildFreeTimeItem(),
-          _buildDivider(),
           // Premium 功能列表
           _buildPremiumFeatures(isPremium),
         ],
@@ -168,92 +163,6 @@ class AccountView extends BaseView<AccountController> {
     );
   }
 
-  /// Free Time 条目
-  Widget _buildFreeTimeItem() {
-    return Container(
-      height: 56.w,
-      padding: EdgeInsets.symmetric(horizontal: 16.w),
-      child: Row(
-        children: [
-          // 图标
-          Container(
-            width: 30.w,
-            height: 30.w,
-            decoration: BoxDecoration(
-              color: Get.reactiveTheme.shadowColor,
-              borderRadius: BorderRadius.circular(8.r),
-            ),
-            child: Icon(IconFont.icon30, size: 20.w, color: Colors.white),
-          ),
-          10.horizontalSpace,
-          // 标题
-          Expanded(
-            child: Text(
-              Strings.freeTime.tr,
-              style: TextStyle(
-                fontSize: 14.sp,
-                color: Get.reactiveTheme.textTheme.bodyLarge!.color,
-                fontWeight: FontWeight.w400,
-              ),
-            ),
-          ),
-          // 时间
-          Text(
-            controller.freeTime,
-            style: TextStyle(
-              fontSize: 13.sp,
-              color: DarkThemeColors.validTermColor,
-              fontWeight: FontWeight.w400,
-            ),
-          ),
-        ],
-      ),
-    );
-  }
-
-  /// Valid Term 条目
-  Widget _buildValidTermItem() {
-    return Container(
-      height: 56.w,
-      padding: EdgeInsets.symmetric(horizontal: 16.w),
-      child: Row(
-        children: [
-          // 图标
-          Container(
-            width: 30.w,
-            height: 30.w,
-            decoration: BoxDecoration(
-              color: Get.reactiveTheme.shadowColor,
-              borderRadius: BorderRadius.circular(8.r),
-            ),
-            child: Icon(IconFont.icon30, size: 20.w, color: Colors.white),
-          ),
-          10.horizontalSpace,
-          // 标题
-          Expanded(
-            child: Text(
-              Strings.validTerm.tr,
-              style: TextStyle(
-                fontSize: 14.sp,
-                color: Get.reactiveTheme.textTheme.bodyLarge!.color,
-                fontWeight: FontWeight.w400,
-              ),
-            ),
-          ),
-          // 有效期
-          Text(
-            controller.validTerm,
-            style: TextStyle(
-              fontSize: 13.sp,
-              color: Get.reactiveTheme.primaryColor,
-              fontWeight: FontWeight.w400,
-            ),
-          ),
-        ],
-      ),
-    );
-  }
-
   Widget _buildPremiumFeatures(bool isPremium) {
     return Padding(
       padding: EdgeInsets.symmetric(horizontal: 14.w),
@@ -307,58 +216,43 @@ class AccountView extends BaseView<AccountController> {
   }
 
   /// 底部按钮
-  Widget _buildBottomButtons(bool isPremium) {
-    if (isPremium) {
-      return Column(
-        children: [
-          SubmitButton(
-            text: Strings.changeSubscription.tr,
-            bgColor: Get.reactiveTheme.highlightColor,
-            textColor: DarkThemeColors.subscriptionColor,
-            onPressed: () {
-              // TODO: 修改订阅
-            },
-          ),
-          20.verticalSpaceFromWidth,
-          // Device Authorization 按钮
-          _buildSecondaryButton(
-            text:
-                '${Strings.deviceAuthorization.tr} (${controller.deviceCount}/${controller.maxDeviceCount})',
-            icon: IconFont.icon11,
-            onTap: () {
-              // TODO: 设备授权
-            },
-          ),
-          10.verticalSpaceFromWidth,
-          // 提示文字
-          Text(
-            Strings.youCanAuthorizeOtherDevices.trParams({
-              'current': controller.deviceCount.toString(),
-              'max': controller.maxDeviceCount.toString(),
-            }),
-            textAlign: TextAlign.center,
-            style: TextStyle(
-              fontSize: 12.sp,
-              color: Get.reactiveTheme.hintColor,
-              height: 1.5,
+  Widget _buildBottomButtons() {
+    return Column(
+      children: [
+        if (controller.apiController.isPremium) ...[
+          if (controller.apiController.isGuest) ...[
+            SubmitButton(
+              text: Strings.changeSubscription.tr,
+              bgColor: Get.reactiveTheme.highlightColor,
+              textColor: DarkThemeColors.subscriptionColor,
+              onPressed: () {
+                controller.toSubscription();
+              },
             ),
-          ),
-        ],
-      );
-    } else {
-      return Column(
-        children: [
+          ] else ...[
+            _buildSecondaryButton(
+              text: Strings.changeSubscription.tr,
+              icon: IconFont.icon23,
+              onTap: () {
+                controller.toSubscription();
+              },
+            ),
+          ],
+        ] else ...[
           // Upgrade to Premium 按钮
-          SubmitButton(
+          _buildSecondaryButton(
             text: Strings.upgradeToPremium.tr,
-            bgColor: Get.reactiveTheme.highlightColor,
-            textColor: DarkThemeColors.subscriptionColor,
-            onPressed: () {
-              // TODO: 修改订阅
+            icon: IconFont.icon23,
+            onTap: () {
+              controller.toSubscription();
             },
           ),
+        ],
+
+        // 绑定邮箱 按钮
+        if (controller.apiController.isGuest &&
+            controller.apiController.isPremium) ...[
           20.verticalSpaceFromWidth,
-          // Activate Pre Code 按钮
           _buildSecondaryButton(
             text: Strings.bindEmailMemberBenefits.tr,
             icon: IconFont.icon23,
@@ -378,8 +272,8 @@ class AccountView extends BaseView<AccountController> {
             ),
           ),
         ],
-      );
-    }
+      ],
+    );
   }
 
   /// 次要按钮(黑色边框)
@@ -438,8 +332,6 @@ class AccountView extends BaseView<AccountController> {
   /// Account 分组
   Widget _buildAccountSection() {
     return Obx(() {
-      final isPremium = controller.isPremium;
-      final isGuest = controller.isGuest;
       return Container(
         decoration: BoxDecoration(
           color: Get.reactiveTheme.highlightColor,
@@ -447,22 +339,25 @@ class AccountView extends BaseView<AccountController> {
         ),
         child: Column(
           children: [
-            if (!isGuest)
-              _buildSettingItem(
-                icon: IconFont.icon29,
-                iconColor: Get.reactiveTheme.shadowColor,
-                title: Strings.account.tr,
-                trailing: IXImage(
-                  source: isPremium ? Assets.premium : Assets.free,
-                  width: isPremium ? 92.w : 64.w,
-                  height: 28.w,
-                  sourceType: ImageSourceType.asset,
-                ),
-                onTap: () {
-                  Get.toNamed(Routes.ACCOUNT);
-                },
+            _buildSettingItem(
+              icon: IconFont.icon29,
+              iconColor: Get.reactiveTheme.shadowColor,
+              title: Strings.account.tr,
+              trailing: IXImage(
+                source: controller.apiController.userLevel == 3
+                    ? Assets.premium
+                    : controller.apiController.userLevel == 9999
+                    ? Assets.test
+                    : Assets.free,
+                width: controller.apiController.userLevel == 3 ? 92.w : 64.w,
+                height: 28.w,
+                sourceType: ImageSourceType.asset,
               ),
-            if (!isGuest) _buildDivider(),
+              onTap: () {
+                Get.toNamed(Routes.ACCOUNT);
+              },
+            ),
+            _buildDivider(),
             _buildSettingItem(
               icon: IconFont.icon14,
               iconColor: Get.reactiveTheme.shadowColor,
@@ -492,7 +387,7 @@ class AccountView extends BaseView<AccountController> {
             ),
             _buildDivider(),
             // 根据用户类型显示不同的时间信息
-            if (isPremium) ...[
+            if (controller.apiController.isPremium) ...[
               // _buildSettingItem(
               //   icon: IconFont.icon23,
               //   iconColor: Get.reactiveTheme.shadowColor,
@@ -526,7 +421,7 @@ class AccountView extends BaseView<AccountController> {
                 iconColor: Get.reactiveTheme.shadowColor,
                 title: Strings.validTerm.tr,
                 trailing: Text(
-                  'Year / 2026-12-12',
+                  controller.apiController.validTermText,
                   style: TextStyle(
                     fontSize: 13.sp,
                     color: Get.reactiveTheme.primaryColor,
@@ -543,7 +438,7 @@ class AccountView extends BaseView<AccountController> {
                 iconColor: Get.reactiveTheme.shadowColor,
                 title: Strings.freeTime.tr,
                 trailing: Text(
-                  '01:60:59 / Days',
+                  '${controller.apiController.remainTimeFormatted} / Days',
                   style: TextStyle(
                     fontSize: 14.sp,
                     color: const Color(0xFFFFCC00),

+ 0 - 5
lib/app/modules/home/controllers/home_controller.dart

@@ -57,11 +57,6 @@ class HomeController extends BaseController {
   List<Locations> get recentLocations =>
       _recentLocations.where((loc) => loc.id != selectedLocation?.id).toList();
 
-  // 是否是会员
-  final _isPremium = false.obs;
-  bool get isPremium => _isPremium.value;
-  set isPremium(bool value) => _isPremium.value = value;
-
   @override
   void onInit() {
     super.onInit();

+ 20 - 4
lib/app/modules/home/views/home_view.dart

@@ -41,10 +41,14 @@ class HomeView extends BaseView<HomeController> {
                       () => ClickOpacity(
                         onTap: () => Get.toNamed(Routes.SUBSCRIPTION),
                         child: IXImage(
-                          source: controller.isPremium
+                          source: controller.apiController.userLevel == 3
                               ? Assets.premium
+                              : controller.apiController.userLevel == 9999
+                              ? Assets.test
                               : Assets.free,
-                          width: controller.isPremium ? 92.w : 64.w,
+                          width: controller.apiController.userLevel == 3
+                              ? 92.w
+                              : 64.w,
                           height: 28.w,
                           sourceType: ImageSourceType.asset,
                         ),
@@ -110,7 +114,12 @@ class HomeView extends BaseView<HomeController> {
                             ),
                           ),
                           Text(
-                            Strings.activeTime.tr,
+                            controller.apiController.isGuest &&
+                                    !controller.apiController.isPremium &&
+                                    controller.apiController.remainTimeSeconds >
+                                        0
+                                ? Strings.remainTime.tr
+                                : Strings.activeTime.tr,
                             style: TextStyle(
                               fontSize: 18.sp,
                               height: 1.3,
@@ -120,7 +129,14 @@ class HomeView extends BaseView<HomeController> {
                           2.verticalSpaceFromWidth,
                           Obx(
                             () => Text(
-                              controller.coreController.timer,
+                              controller.apiController.isGuest &&
+                                      !controller.apiController.isPremium &&
+                                      controller
+                                              .apiController
+                                              .remainTimeSeconds >
+                                          0
+                                  ? controller.apiController.remainTimeFormatted
+                                  : controller.coreController.timer,
                               style: TextStyle(
                                 fontSize: 28.sp,
                                 height: 1.2,

+ 6 - 25
lib/app/modules/setting/controllers/setting_controller.dart

@@ -2,13 +2,11 @@ import 'package:get/get.dart';
 
 import '../../../../config/translations/strings_enum.dart';
 import '../../../../utils/awesome_notifications_helper.dart';
-import '../../../constants/enums.dart';
 import '../../../controllers/api_controller.dart';
-import '../../../data/sp/ix_sp.dart';
 import '../../../dialog/loading/loading_dialog.dart';
 
 class SettingController extends GetxController {
-  final _apiController = Get.find<ApiController>();
+  final apiController = Get.find<ApiController>();
 
   // 自动重连开关
   final _autoReconnect = true.obs;
@@ -19,37 +17,20 @@ class SettingController extends GetxController {
   bool get pushNotifications => _pushNotifications.value;
   set pushNotifications(bool value) => _pushNotifications.value = value;
 
-  final _isPremium = false.obs;
-  bool get isPremium => _isPremium.value;
-  set isPremium(bool value) => _isPremium.value = value;
-
-  //是否是游客
-  final _isGuest = false.obs;
-  bool get isGuest => _isGuest.value;
-  set isGuest(bool value) => _isGuest.value = value;
-
   @override
   void onInit() {
     super.onInit();
-    initPushNotifications();
-    initIsGuest();
-  }
-
-  void initIsGuest() {
-    final user = IXSP.getUser();
-    if (user != null) {
-      isGuest = user.memberLevel == MemberLevel.guest.level;
-    }
+    _initPushNotifications();
   }
 
-  Future<void> initPushNotifications() async {
+  Future<void> _initPushNotifications() async {
     pushNotifications =
         await AwesomeNotificationsHelper.checkNotificationPermission();
   }
 
   Future<void> showNotificationConfigPage() async {
     await AwesomeNotificationsHelper.showNotificationConfigPage();
-    initPushNotifications();
+    _initPushNotifications();
   }
 
   // 处理退出登录
@@ -60,7 +41,7 @@ class SettingController extends GetxController {
       successText: Strings.logoutSuccessful.tr,
       onRequest: () async {
         // 执行你的异步请求
-        await _apiController.logout();
+        await apiController.logout();
       },
       onSuccess: () {
         // 成功后的操作
@@ -77,7 +58,7 @@ class SettingController extends GetxController {
       successText: Strings.deleteAccountSuccessful.tr,
       onRequest: () async {
         // 执行你的异步请求
-        await _apiController.deleteAccount();
+        await apiController.deleteAccount();
       },
       onSuccess: () {
         // 成功后的操作

+ 38 - 38
lib/app/modules/setting/views/setting_view.dart

@@ -78,9 +78,7 @@ class SettingView extends BaseView<SettingController> {
   Widget _buildLoginSection() {
     return SliverToBoxAdapter(
       child: Obx(() {
-        final isPremium = controller.isPremium;
-        final isGuest = controller.isGuest;
-        if (!isGuest) {
+        if (!controller.apiController.isGuest) {
           return SizedBox.shrink();
         }
         return Container(
@@ -93,21 +91,10 @@ class SettingView extends BaseView<SettingController> {
             icon: IconFont.icon37,
             iconColor: Get.reactiveTheme.shadowColor,
             title: Strings.login.tr,
-            trailing: Row(
-              children: [
-                IXImage(
-                  source: isPremium ? Assets.premium : Assets.free,
-                  width: isPremium ? 92.w : 64.w,
-                  height: 28.w,
-                  sourceType: ImageSourceType.asset,
-                ),
-                4.horizontalSpace,
-                Icon(
-                  IconFont.icon02,
-                  size: 20.w,
-                  color: Get.reactiveTheme.hintColor,
-                ),
-              ],
+            trailing: Icon(
+              IconFont.icon02,
+              size: 20.w,
+              color: Get.reactiveTheme.hintColor,
             ),
 
             onTap: () {
@@ -123,8 +110,6 @@ class SettingView extends BaseView<SettingController> {
   Widget _buildAccountSection() {
     return SliverToBoxAdapter(
       child: Obx(() {
-        final isPremium = controller.isPremium;
-        final isGuest = controller.isGuest;
         return Container(
           margin: EdgeInsets.symmetric(horizontal: 14.w),
           decoration: BoxDecoration(
@@ -133,22 +118,37 @@ class SettingView extends BaseView<SettingController> {
           ),
           child: Column(
             children: [
-              if (!isGuest)
-                _buildSettingItem(
-                  icon: IconFont.icon29,
-                  iconColor: Get.reactiveTheme.shadowColor,
-                  title: Strings.account.tr,
-                  trailing: IXImage(
-                    source: isPremium ? Assets.premium : Assets.free,
-                    width: isPremium ? 92.w : 64.w,
-                    height: 28.w,
-                    sourceType: ImageSourceType.asset,
-                  ),
-                  onTap: () {
-                    Get.toNamed(Routes.ACCOUNT);
-                  },
+              _buildSettingItem(
+                icon: IconFont.icon29,
+                iconColor: Get.reactiveTheme.shadowColor,
+                title: Strings.account.tr,
+                trailing: Row(
+                  children: [
+                    IXImage(
+                      source: controller.apiController.userLevel == 3
+                          ? Assets.premium
+                          : controller.apiController.userLevel == 9999
+                          ? Assets.test
+                          : Assets.free,
+                      width: controller.apiController.userLevel == 3
+                          ? 92.w
+                          : 64.w,
+                      height: 28.w,
+                      sourceType: ImageSourceType.asset,
+                    ),
+                    4.horizontalSpace,
+                    Icon(
+                      IconFont.icon02,
+                      size: 20.w,
+                      color: Get.reactiveTheme.hintColor,
+                    ),
+                  ],
                 ),
-              if (!isGuest) _buildDivider(),
+                onTap: () {
+                  Get.toNamed(Routes.ACCOUNT);
+                },
+              ),
+              _buildDivider(),
               _buildSettingItem(
                 icon: IconFont.icon14,
                 iconColor: Get.reactiveTheme.shadowColor,
@@ -178,7 +178,7 @@ class SettingView extends BaseView<SettingController> {
               ),
               _buildDivider(),
               // 根据用户类型显示不同的时间信息
-              if (isPremium) ...[
+              if (controller.apiController.isPremium) ...[
                 // _buildSettingItem(
                 //   icon: IconFont.icon23,
                 //   iconColor: Get.reactiveTheme.shadowColor,
@@ -212,7 +212,7 @@ class SettingView extends BaseView<SettingController> {
                   iconColor: Get.reactiveTheme.shadowColor,
                   title: Strings.validTerm.tr,
                   trailing: Text(
-                    'Year / 2026-12-12',
+                    controller.apiController.validTermText,
                     style: TextStyle(
                       fontSize: 13.sp,
                       color: Get.reactiveTheme.primaryColor,
@@ -229,7 +229,7 @@ class SettingView extends BaseView<SettingController> {
                   iconColor: Get.reactiveTheme.shadowColor,
                   title: Strings.freeTime.tr,
                   trailing: Text(
-                    '01:60:59 / Days',
+                    '${controller.apiController.remainTimeFormatted} / Days',
                     style: TextStyle(
                       fontSize: 14.sp,
                       color: const Color(0xFFFFCC00),

+ 1 - 1
lib/app/modules/subscription/controllers/subscription_controller.dart

@@ -146,7 +146,7 @@ class SubscriptionController extends GetxController {
       }
 
       // 加载对应的内购产品
-      await _loadProductsFromPlans();
+      // await _loadProductsFromPlans();
     } catch (e) {
       log(TAG, '获取套餐列表失败: $e');
       IXSnackBar.showIXSnackBar(title: Strings.error.tr, message: '获取套餐列表失败');

+ 10 - 0
lib/config/translations/ar_AR/ar_ar_translation.dart

@@ -383,4 +383,14 @@ final Map<String, String> arAR = {
   Strings.associatedInterestsDesc:
       'يرجى ملاحظة قواعد الاشتراك التالية عند تسجيل الدخول:\n1. الحساب المجاني: سيرث الحساب الاشتراك الحالي على هذا الجهاز.\n2. حساب العضوية: سيتحول التطبيق إلى خطة ذلك الحساب الحالية.\nملاحظة: ستظل عملية الشراء على هذا الجهاز صالحة.',
   Strings.notNow: 'ليس الآن',
+
+  // VPN Error Messages
+  Strings.vpnConnectionTimeoutError: 'انتهت مهلة الاتصال بالعقدة',
+  Strings.vpnNoNodeError: 'لا توجد عقد متاحة',
+  Strings.vpnInitError: 'فشل في تهيئة خدمة VPN',
+  Strings.vpnKillError: 'تم إنهاء خدمة VPN',
+  Strings.vpnRevokeError: 'تم قطع اتصال VPN، يرجى إعادة الاتصال',
+  Strings.vpnServiceEmptyError: 'خدمة VPN غير متوفرة',
+  Strings.vpnRouterError: 'خطأ في تكوين توجيه VPN',
+  Strings.vpnPermissionDeniedError: 'تم رفض إذن VPN',
 };

+ 10 - 0
lib/config/translations/de_DE/de_de_translation.dart

@@ -388,4 +388,14 @@ const Map<String, String> deDE = {
   Strings.associatedInterestsDesc:
       'Bitte beachten Sie die folgenden Abonnementregeln nach der Anmeldung:\n1. Kostenloses Konto: Das Konto übernimmt das aktuelle Abonnement auf diesem Gerät.\n2. Mitgliedskonto: Die App wechselt zum bestehenden Plan dieses Kontos.\nHinweis: Der Kauf auf diesem Gerät bleibt gültig.',
   Strings.notNow: 'Nicht jetzt',
+
+  // VPN Error Messages
+  Strings.vpnConnectionTimeoutError: 'Zeitüberschreitung bei Knotenverbindung',
+  Strings.vpnNoNodeError: 'Keine verfügbaren Knoten',
+  Strings.vpnInitError: 'VPN-Dienst konnte nicht initialisiert werden',
+  Strings.vpnKillError: 'VPN-Dienst wurde beendet',
+  Strings.vpnRevokeError: 'VPN-Verbindung getrennt, bitte erneut verbinden',
+  Strings.vpnServiceEmptyError: 'VPN-Dienst nicht verfügbar',
+  Strings.vpnRouterError: 'VPN-Routing-Konfigurationsfehler',
+  Strings.vpnPermissionDeniedError: 'VPN-Berechtigung verweigert',
 };

+ 10 - 0
lib/config/translations/en_US/en_us_translation.dart

@@ -399,4 +399,14 @@ Map<String, String> enUs = {
   Strings.associatedInterestsDesc:
       'Please note the following subscription rules upon login:\n1. Free Account: The account will inherit the current subscription on this device.\n2. Member Account: The app will switch to that account\'s existing plan.\nNote: The purchase on this device will remain valid.',
   Strings.notNow: 'Not now',
+
+  // VPN Error Messages
+  Strings.vpnConnectionTimeoutError: 'Connection to node timed out',
+  Strings.vpnNoNodeError: 'No available nodes found',
+  Strings.vpnInitError: 'Failed to initialize VPN service',
+  Strings.vpnKillError: 'VPN service was terminated',
+  Strings.vpnRevokeError: 'VPN connection disconnected, please reconnect',
+  Strings.vpnServiceEmptyError: 'VPN service is unavailable',
+  Strings.vpnRouterError: 'VPN routing configuration error',
+  Strings.vpnPermissionDeniedError: 'VPN permission denied',
 };

+ 10 - 0
lib/config/translations/es_ES/es_es_translation.dart

@@ -393,4 +393,14 @@ const Map<String, String> esEs = {
   Strings.associatedInterestsDesc:
       'Ten en cuenta las siguientes reglas de suscripción al iniciar sesión:\n1. Cuenta gratuita: La cuenta heredará la suscripción actual en este dispositivo.\n2. Cuenta de miembro: La app cambiará al plan existente de esa cuenta.\nNota: La compra en este dispositivo seguirá siendo válida.',
   Strings.notNow: 'Ahora no',
+
+  // VPN Error Messages
+  Strings.vpnConnectionTimeoutError: 'Tiempo de conexión al nodo agotado',
+  Strings.vpnNoNodeError: 'No hay nodos disponibles',
+  Strings.vpnInitError: 'Error al inicializar el servicio VPN',
+  Strings.vpnKillError: 'El servicio VPN fue terminado',
+  Strings.vpnRevokeError: 'Conexión VPN desconectada, por favor reconecte',
+  Strings.vpnServiceEmptyError: 'Servicio VPN no disponible',
+  Strings.vpnRouterError: 'Error de configuración de enrutamiento VPN',
+  Strings.vpnPermissionDeniedError: 'Permiso de VPN denegado',
 };

+ 10 - 0
lib/config/translations/fa_IR/fa_ir_translation.dart

@@ -389,4 +389,14 @@ const Map<String, String> faIR = {
   Strings.associatedInterestsDesc:
       'لطفاً به قوانین اشتراک زیر پس از ورود توجه کنید:\n1. حساب رایگان: حساب اشتراک فعلی این دستگاه را به ارث می‌برد.\n2. حساب عضویت: برنامه به طرح موجود آن حساب تغییر می‌کند.\nتوجه: خرید در این دستگاه معتبر باقی می‌ماند.',
   Strings.notNow: 'الان نه',
+
+  // VPN Error Messages
+  Strings.vpnConnectionTimeoutError: 'زمان اتصال به گره به پایان رسید',
+  Strings.vpnNoNodeError: 'هیچ گره‌ای در دسترس نیست',
+  Strings.vpnInitError: 'راه‌اندازی سرویس VPN ناموفق بود',
+  Strings.vpnKillError: 'سرویس VPN متوقف شد',
+  Strings.vpnRevokeError: 'اتصال VPN قطع شد، لطفاً دوباره متصل شوید',
+  Strings.vpnServiceEmptyError: 'سرویس VPN در دسترس نیست',
+  Strings.vpnRouterError: 'خطای پیکربندی مسیریابی VPN',
+  Strings.vpnPermissionDeniedError: 'مجوز VPN رد شد',
 };

+ 10 - 0
lib/config/translations/fr_FR/fr_fr_translation.dart

@@ -394,4 +394,14 @@ const Map<String, String> frFR = {
   Strings.associatedInterestsDesc:
       'Veuillez noter les règles d\'abonnement suivantes lors de la connexion :\n1. Compte gratuit : Le compte héritera de l\'abonnement actuel sur cet appareil.\n2. Compte membre : L\'application passera au forfait existant de ce compte.\nRemarque : L\'achat sur cet appareil restera valide.',
   Strings.notNow: 'Pas maintenant',
+
+  // VPN Error Messages
+  Strings.vpnConnectionTimeoutError: 'Délai de connexion au nœud dépassé',
+  Strings.vpnNoNodeError: 'Aucun nœud disponible',
+  Strings.vpnInitError: 'Échec de l\'initialisation du service VPN',
+  Strings.vpnKillError: 'Le service VPN a été arrêté',
+  Strings.vpnRevokeError: 'Connexion VPN interrompue, veuillez vous reconnecter',
+  Strings.vpnServiceEmptyError: 'Service VPN indisponible',
+  Strings.vpnRouterError: 'Erreur de configuration du routage VPN',
+  Strings.vpnPermissionDeniedError: 'Autorisation VPN refusée',
 };

+ 10 - 0
lib/config/translations/hi_IN/hi_in_translation.dart

@@ -302,5 +302,15 @@ Map<String, String> hiIN = {
   Strings.associatedInterestsDesc:
       'कृपया लॉगिन के बाद निम्नलिखित सदस्यता नियमों पर ध्यान दें:\n1. मुफ्त खाता: खाता इस डिवाइस पर वर्तमान सदस्यता को प्राप्त करेगा।\n2. सदस्य खाता: ऐप उस खाते की मौजूदा योजना पर स्विच हो जाएगा।\nनोट: इस डिवाइस पर खरीदारी वैध रहेगी।',
   Strings.notNow: 'अभी नहीं',
+
+  // VPN Error Messages
+  Strings.vpnConnectionTimeoutError: 'नोड कनेक्शन समय समाप्त',
+  Strings.vpnNoNodeError: 'कोई उपलब्ध नोड नहीं',
+  Strings.vpnInitError: 'VPN सेवा प्रारंभ करने में विफल',
+  Strings.vpnKillError: 'VPN सेवा समाप्त कर दी गई',
+  Strings.vpnRevokeError: 'VPN कनेक्शन डिस्कनेक्ट हो गया, कृपया पुनः कनेक्ट करें',
+  Strings.vpnServiceEmptyError: 'VPN सेवा उपलब्ध नहीं है',
+  Strings.vpnRouterError: 'VPN रूटिंग कॉन्फ़िगरेशन त्रुटि',
+  Strings.vpnPermissionDeniedError: 'VPN अनुमति अस्वीकृत',
 };
 

+ 10 - 0
lib/config/translations/id_ID/id_id_translation.dart

@@ -302,5 +302,15 @@ Map<String, String> idID = {
   Strings.associatedInterestsDesc:
       'Harap perhatikan aturan langganan berikut saat login:\n1. Akun Gratis: Akun akan mewarisi langganan saat ini di perangkat ini.\n2. Akun Anggota: Aplikasi akan beralih ke paket yang ada di akun tersebut.\nCatatan: Pembelian di perangkat ini akan tetap berlaku.',
   Strings.notNow: 'Nanti saja',
+
+  // VPN Error Messages
+  Strings.vpnConnectionTimeoutError: 'Koneksi ke node habis waktu',
+  Strings.vpnNoNodeError: 'Tidak ada node yang tersedia',
+  Strings.vpnInitError: 'Gagal menginisialisasi layanan VPN',
+  Strings.vpnKillError: 'Layanan VPN dihentikan',
+  Strings.vpnRevokeError: 'Koneksi VPN terputus, silakan sambungkan kembali',
+  Strings.vpnServiceEmptyError: 'Layanan VPN tidak tersedia',
+  Strings.vpnRouterError: 'Kesalahan konfigurasi routing VPN',
+  Strings.vpnPermissionDeniedError: 'Izin VPN ditolak',
 };
 

+ 10 - 0
lib/config/translations/ja_JP/ja_jp_translation.dart

@@ -368,4 +368,14 @@ const Map<String, String> jaJP = {
   Strings.associatedInterestsDesc:
       'ログイン後の以下のサブスクリプションルールにご注意ください:\n1. 無料アカウント:アカウントはこのデバイスの現在のサブスクリプションを継承します。\n2. 会員アカウント:アプリはそのアカウントの既存プランに切り替わります。\n注意:このデバイスでの購入は有効なままです。',
   Strings.notNow: '後で',
+
+  // VPN Error Messages
+  Strings.vpnConnectionTimeoutError: 'ノード接続がタイムアウトしました',
+  Strings.vpnNoNodeError: '利用可能なノードがありません',
+  Strings.vpnInitError: 'VPNサービスの初期化に失敗しました',
+  Strings.vpnKillError: 'VPNサービスが終了しました',
+  Strings.vpnRevokeError: 'VPN接続が切断されました、再接続してください',
+  Strings.vpnServiceEmptyError: 'VPNサービスが利用できません',
+  Strings.vpnRouterError: 'VPNルーティング設定エラー',
+  Strings.vpnPermissionDeniedError: 'VPN権限が拒否されました',
 };

+ 10 - 0
lib/config/translations/ko_KR/ko_kr_translation.dart

@@ -361,4 +361,14 @@ const Map<String, String> koKR = {
   Strings.associatedInterestsDesc:
       '로그인 후 다음 구독 규칙을 참고하세요:\n1. 무료 계정: 계정이 이 기기의 현재 구독을 상속합니다.\n2. 회원 계정: 앱이 해당 계정의 기존 요금제로 전환됩니다.\n참고: 이 기기에서의 구매는 유효합니다.',
   Strings.notNow: '나중에',
+
+  // VPN Error Messages
+  Strings.vpnConnectionTimeoutError: '노드 연결 시간 초과',
+  Strings.vpnNoNodeError: '사용 가능한 노드가 없습니다',
+  Strings.vpnInitError: 'VPN 서비스 초기화 실패',
+  Strings.vpnKillError: 'VPN 서비스가 종료되었습니다',
+  Strings.vpnRevokeError: 'VPN 연결이 끊어졌습니다, 다시 연결해 주세요',
+  Strings.vpnServiceEmptyError: 'VPN 서비스를 사용할 수 없습니다',
+  Strings.vpnRouterError: 'VPN 라우팅 구성 오류',
+  Strings.vpnPermissionDeniedError: 'VPN 권한이 거부되었습니다',
 };

+ 10 - 0
lib/config/translations/my_MM/my_mm_translation.dart

@@ -397,4 +397,14 @@ const Map<String, String> myMM = {
   Strings.associatedInterestsDesc:
       'ဝင်ရောက်ပြီးနောက် အောက်ပါစာရင်းသွင်းစည်းမျဉ်းများကို သတိပြုပါ:\n1. အခမဲ့အကောင့်: အကောင့်သည် ဤစက်ပေါ်ရှိ လက်ရှိစာရင်းသွင်းမှုကို အမွေဆက်ခံမည်။\n2. အသင်းဝင်အကောင့်: အက်ပ်သည် ထိုအကောင့်၏ လက်ရှိအစီအစဉ်သို့ ပြောင်းလဲမည်။\nမှတ်ချက်: ဤစက်ပေါ်ရှိ ဝယ်ယူမှုသည် တရားဝင်ဆက်လက်ရှိနေမည်။',
   Strings.notNow: 'အခုမဟုတ်သေးပါ',
+
+  // VPN Error Messages
+  Strings.vpnConnectionTimeoutError: 'Node ချိတ်ဆက်မှု အချိန်ကုန်သွားပါပြီ',
+  Strings.vpnNoNodeError: 'ရရှိနိုင်သော node မရှိပါ',
+  Strings.vpnInitError: 'VPN ဝန်ဆောင်မှု စတင်ခြင်း မအောင်မြင်ပါ',
+  Strings.vpnKillError: 'VPN ဝန်ဆောင်မှု ရပ်တန့်သွားပါပြီ',
+  Strings.vpnRevokeError: 'VPN ချိတ်ဆက်မှု ပြတ်တောက်သွားပါပြီ၊ ကျေးဇူးပြု၍ ပြန်လည်ချိတ်ဆက်ပါ',
+  Strings.vpnServiceEmptyError: 'VPN ဝန်ဆောင်မှု မရရှိနိုင်ပါ',
+  Strings.vpnRouterError: 'VPN routing စီစဉ်မှု အမှား',
+  Strings.vpnPermissionDeniedError: 'VPN ခွင့်ပြုချက် ငြင်းပယ်ခံရပါပြီ',
 };

+ 10 - 0
lib/config/translations/pt_BR/pt_br_translation.dart

@@ -432,5 +432,15 @@ Map<String, String> ptBR = {
   Strings.associatedInterestsDesc:
       'Por favor, observe as seguintes regras de assinatura ao fazer login:\n1. Conta Gratuita: A conta herdará a assinatura atual neste dispositivo.\n2. Conta de Membro: O app mudará para o plano existente dessa conta.\nNota: A compra neste dispositivo permanecerá válida.',
   Strings.notNow: 'Agora não',
+
+  // VPN Error Messages
+  Strings.vpnConnectionTimeoutError: 'Tempo de conexão com o nó esgotado',
+  Strings.vpnNoNodeError: 'Nenhum nó disponível',
+  Strings.vpnInitError: 'Falha ao inicializar o serviço VPN',
+  Strings.vpnKillError: 'O serviço VPN foi encerrado',
+  Strings.vpnRevokeError: 'Conexão VPN desconectada, por favor reconecte',
+  Strings.vpnServiceEmptyError: 'Serviço VPN indisponível',
+  Strings.vpnRouterError: 'Erro de configuração de roteamento VPN',
+  Strings.vpnPermissionDeniedError: 'Permissão de VPN negada',
 };
 

+ 10 - 0
lib/config/translations/ru_RU/ru_ru_translation.dart

@@ -393,4 +393,14 @@ const Map<String, String> ruRU = {
   Strings.associatedInterestsDesc:
       'Обратите внимание на следующие правила подписки при входе:\n1. Бесплатный аккаунт: Аккаунт унаследует текущую подписку на этом устройстве.\n2. Аккаунт участника: Приложение переключится на существующий план этого аккаунта.\nПримечание: Покупка на этом устройстве останется действительной.',
   Strings.notNow: 'Не сейчас',
+
+  // VPN Error Messages
+  Strings.vpnConnectionTimeoutError: 'Время подключения к узлу истекло',
+  Strings.vpnNoNodeError: 'Нет доступных узлов',
+  Strings.vpnInitError: 'Не удалось инициализировать VPN-сервис',
+  Strings.vpnKillError: 'VPN-сервис был завершён',
+  Strings.vpnRevokeError: 'VPN-соединение прервано, пожалуйста, переподключитесь',
+  Strings.vpnServiceEmptyError: 'VPN-сервис недоступен',
+  Strings.vpnRouterError: 'Ошибка конфигурации маршрутизации VPN',
+  Strings.vpnPermissionDeniedError: 'Разрешение VPN отклонено',
 };

+ 13 - 0
lib/config/translations/strings_enum.dart

@@ -275,6 +275,16 @@ class Strings {
   static const String vpnConnectionError = 'VPN connection error';
   static const String vpnServiceDisconnected =
       'VPN service disconnected unexpectedly';
+  static const String vpnConnectionTimeoutError = 'Node connection timeout';
+  static const String vpnNoNodeError = 'No available nodes';
+  static const String vpnInitError = 'VPN initialization failed';
+  static const String vpnKillError = 'VPN service killed';
+  static const String vpnRevokeError =
+      'VPN connection disconnected, please reconnect';
+  static const String vpnServiceEmptyError = 'VPN service empty';
+  static const String vpnRouterError = 'VPN router error';
+  static const String vpnPermissionDeniedError = 'VPN permission denied';
+
   static const String failedCaptureScreenshot = 'Failed to capture screenshot';
   static const String imageSavedToAlbum =
       'The image has been saved to your local album';
@@ -440,4 +450,7 @@ class Strings {
   static const String associatedInterestsDesc =
       'Please note the following subscription rules upon login:\n1. Free Account: The account will inherit the current subscription on this device.\n2. Member Account: The app will switch to that account\'s existing plan.\nNote: The purchase on this device will remain valid.';
   static const String notNow = 'Not now';
+
+  static const String remainTime = 'Remain time';
+  static const String remainTimeEnded = 'Your available time has ended';
 }

+ 10 - 0
lib/config/translations/th_TH/th_th_translation.dart

@@ -302,5 +302,15 @@ Map<String, String> thTH = {
   Strings.associatedInterestsDesc:
       'โปรดทราบกฎการสมัครสมาชิกต่อไปนี้เมื่อเข้าสู่ระบบ:\n1. บัญชีฟรี: บัญชีจะรับช่วงการสมัครสมาชิกปัจจุบันบนอุปกรณ์นี้\n2. บัญชีสมาชิก: แอปจะเปลี่ยนไปใช้แผนที่มีอยู่ของบัญชีนั้น\nหมายเหตุ: การซื้อบนอุปกรณ์นี้จะยังคงใช้ได้',
   Strings.notNow: 'ไม่ใช่ตอนนี้',
+
+  // VPN Error Messages
+  Strings.vpnConnectionTimeoutError: 'การเชื่อมต่อโหนดหมดเวลา',
+  Strings.vpnNoNodeError: 'ไม่มีโหนดที่ใช้ได้',
+  Strings.vpnInitError: 'การเริ่มต้นบริการ VPN ล้มเหลว',
+  Strings.vpnKillError: 'บริการ VPN ถูกยุติ',
+  Strings.vpnRevokeError: 'การเชื่อมต่อ VPN ถูกตัด กรุณาเชื่อมต่อใหม่',
+  Strings.vpnServiceEmptyError: 'บริการ VPN ไม่พร้อมใช้งาน',
+  Strings.vpnRouterError: 'ข้อผิดพลาดการกำหนดค่าเส้นทาง VPN',
+  Strings.vpnPermissionDeniedError: 'สิทธิ์ VPN ถูกปฏิเสธ',
 };
 

+ 10 - 0
lib/config/translations/tk_TM/tk_tm_translation.dart

@@ -429,4 +429,14 @@ Map<String, String> tkTM = {
   Strings.associatedInterestsDesc:
       'Giriş edeňizde aşakdaky abuna ýazgy düzgünlerini belläň:\n1. Mugt hasap: Hasap bu enjamda häzirki abuna ýazgyny miras alar.\n2. Agza hasap: Programma şol hasabyň bar bolan meýilnamasyna geçer.\nBellik: Bu enjamda satyn alyş güýjini saklar.',
   Strings.notNow: 'Häzir däl',
+
+  // VPN Error Messages
+  Strings.vpnConnectionTimeoutError: 'Düwün birikmesi wagty gutardy',
+  Strings.vpnNoNodeError: 'Elýeterli düwün ýok',
+  Strings.vpnInitError: 'VPN hyzmatyny başlamak şowsuz',
+  Strings.vpnKillError: 'VPN hyzmaty ýatyryldy',
+  Strings.vpnRevokeError: 'VPN birikme kesildi, täzeden birikdiriň',
+  Strings.vpnServiceEmptyError: 'VPN hyzmaty elýeterli däl',
+  Strings.vpnRouterError: 'VPN marşrutlaşdyryş sazlaýyş ýalňyşlygy',
+  Strings.vpnPermissionDeniedError: 'VPN rugsady ret edildi',
 };

+ 10 - 0
lib/config/translations/tl_PH/tl_ph_translation.dart

@@ -302,5 +302,15 @@ Map<String, String> tlPH = {
   Strings.associatedInterestsDesc:
       'Pakitandaan ang mga sumusunod na patakaran sa subscription sa pag-login:\n1. Libreng Account: Mamanahin ng account ang kasalukuyang subscription sa device na ito.\n2. Member Account: Lilipat ang app sa umiiral na plano ng account na iyon.\nTandaan: Mananatiling valid ang pagbili sa device na ito.',
   Strings.notNow: 'Hindi ngayon',
+
+  // VPN Error Messages
+  Strings.vpnConnectionTimeoutError: 'Nag-timeout ang koneksyon sa node',
+  Strings.vpnNoNodeError: 'Walang available na node',
+  Strings.vpnInitError: 'Nabigo ang pagsisimula ng serbisyo ng VPN',
+  Strings.vpnKillError: 'Winakasan ang serbisyo ng VPN',
+  Strings.vpnRevokeError: 'Nadiskonekta ang koneksyon ng VPN, mangyaring muling kumonekta',
+  Strings.vpnServiceEmptyError: 'Hindi available ang serbisyo ng VPN',
+  Strings.vpnRouterError: 'Error sa configuration ng VPN routing',
+  Strings.vpnPermissionDeniedError: 'Tinanggihan ang pahintulot sa VPN',
 };
 

+ 10 - 0
lib/config/translations/tr_TR/tr_tr_translation.dart

@@ -302,5 +302,15 @@ Map<String, String> trTR = {
   Strings.associatedInterestsDesc:
       'Giriş yaptıktan sonra lütfen aşağıdaki abonelik kurallarını dikkate alın:\n1. Ücretsiz Hesap: Hesap, bu cihazdaki mevcut aboneliği devralacaktır.\n2. Üye Hesap: Uygulama, o hesabın mevcut planına geçecektir.\nNot: Bu cihazdaki satın alma geçerli kalacaktır.',
   Strings.notNow: 'Şimdi değil',
+
+  // VPN Error Messages
+  Strings.vpnConnectionTimeoutError: 'Düğüm bağlantısı zaman aşımına uğradı',
+  Strings.vpnNoNodeError: 'Kullanılabilir düğüm yok',
+  Strings.vpnInitError: 'VPN hizmeti başlatılamadı',
+  Strings.vpnKillError: 'VPN hizmeti sonlandırıldı',
+  Strings.vpnRevokeError: 'VPN bağlantısı kesildi, lütfen yeniden bağlanın',
+  Strings.vpnServiceEmptyError: 'VPN hizmeti kullanılamıyor',
+  Strings.vpnRouterError: 'VPN yönlendirme yapılandırma hatası',
+  Strings.vpnPermissionDeniedError: 'VPN izni reddedildi',
 };
 

+ 10 - 0
lib/config/translations/vi_VN/vi_vn_translation.dart

@@ -343,4 +343,14 @@ Map<String, String> viVN = {
   Strings.associatedInterestsDesc:
       'Vui lòng lưu ý các quy tắc đăng ký sau khi đăng nhập:\n1. Tài khoản Miễn phí: Tài khoản sẽ kế thừa gói đăng ký hiện tại trên thiết bị này.\n2. Tài khoản Thành viên: Ứng dụng sẽ chuyển sang gói hiện có của tài khoản đó.\nLưu ý: Giao dịch mua trên thiết bị này sẽ vẫn có hiệu lực.',
   Strings.notNow: 'Để sau',
+
+  // VPN Error Messages
+  Strings.vpnConnectionTimeoutError: 'Kết nối đến nút đã hết thời gian',
+  Strings.vpnNoNodeError: 'Không có nút khả dụng',
+  Strings.vpnInitError: 'Khởi tạo dịch vụ VPN thất bại',
+  Strings.vpnKillError: 'Dịch vụ VPN đã bị dừng',
+  Strings.vpnRevokeError: 'Kết nối VPN đã ngắt, vui lòng kết nối lại',
+  Strings.vpnServiceEmptyError: 'Dịch vụ VPN không khả dụng',
+  Strings.vpnRouterError: 'Lỗi cấu hình định tuyến VPN',
+  Strings.vpnPermissionDeniedError: 'Quyền VPN bị từ chối',
 };

+ 10 - 0
lib/config/translations/zh_TW/zh_tw_translation.dart

@@ -394,4 +394,14 @@ Map<String, String> zhTW = {
   Strings.associatedInterestsDesc:
       '請注意登入後的以下訂閱規則:\n1. 免費帳號:帳號將繼承此設備上的當前訂閱。\n2. 會員帳號:應用將切換到該帳號的現有方案。\n注意:此設備上的購買將保持有效。',
   Strings.notNow: '暫不',
+
+  // VPN Error Messages
+  Strings.vpnConnectionTimeoutError: '節點連線逾時',
+  Strings.vpnNoNodeError: '無可用節點',
+  Strings.vpnInitError: 'VPN 服務初始化失敗',
+  Strings.vpnKillError: 'VPN 服務已被終止',
+  Strings.vpnRevokeError: 'VPN 連線已斷開,請重新連接',
+  Strings.vpnServiceEmptyError: 'VPN 服務不可用',
+  Strings.vpnRouterError: 'VPN 路由配置錯誤',
+  Strings.vpnPermissionDeniedError: 'VPN 權限被拒絕',
 };

+ 2 - 25
lib/pigeons/core_api.g.dart

@@ -98,14 +98,14 @@ class CoreApi {
     }
   }
 
-  Future<bool?> connect(String sessionId, int socksPort, String tunnelConfig, String configJson) async {
+  Future<bool?> connect(String sessionId, int socksPort, String tunnelConfig, String configJson, int remainTime, bool isCountdown, List<String> allowVpnApps, List<String> disallowVpnApps, String accessToken, String aesKey, String aesIv, int locationId, String locationCode, List<String> baseUrls, String params, int peekTimeInterval) async {
     final String pigeonVar_channelName = 'dev.flutter.pigeon.app.xixi.nomo.CoreApi.connect$pigeonVar_messageChannelSuffix';
     final BasicMessageChannel<Object?> pigeonVar_channel = BasicMessageChannel<Object?>(
       pigeonVar_channelName,
       pigeonChannelCodec,
       binaryMessenger: pigeonVar_binaryMessenger,
     );
-    final Future<Object?> pigeonVar_sendFuture = pigeonVar_channel.send(<Object?>[sessionId, socksPort, tunnelConfig, configJson]);
+    final Future<Object?> pigeonVar_sendFuture = pigeonVar_channel.send(<Object?>[sessionId, socksPort, tunnelConfig, configJson, remainTime, isCountdown, allowVpnApps, disallowVpnApps, accessToken, aesKey, aesIv, locationId, locationCode, baseUrls, params, peekTimeInterval]);
     final List<Object?>? pigeonVar_replyList =
         await pigeonVar_sendFuture as List<Object?>?;
     if (pigeonVar_replyList == null) {
@@ -259,29 +259,6 @@ class CoreApi {
     }
   }
 
-  Future<bool?> reconnect() async {
-    final String pigeonVar_channelName = 'dev.flutter.pigeon.app.xixi.nomo.CoreApi.reconnect$pigeonVar_messageChannelSuffix';
-    final BasicMessageChannel<Object?> pigeonVar_channel = BasicMessageChannel<Object?>(
-      pigeonVar_channelName,
-      pigeonChannelCodec,
-      binaryMessenger: pigeonVar_binaryMessenger,
-    );
-    final Future<Object?> pigeonVar_sendFuture = pigeonVar_channel.send(null);
-    final List<Object?>? pigeonVar_replyList =
-        await pigeonVar_sendFuture as List<Object?>?;
-    if (pigeonVar_replyList == null) {
-      throw _createConnectionError(pigeonVar_channelName);
-    } else if (pigeonVar_replyList.length > 1) {
-      throw PlatformException(
-        code: pigeonVar_replyList[0]! as String,
-        message: pigeonVar_replyList[1] as String?,
-        details: pigeonVar_replyList[2],
-      );
-    } else {
-      return (pigeonVar_replyList[0] as bool?);
-    }
-  }
-
   Future<String?> getChannel() async {
     final String pigeonVar_channelName = 'dev.flutter.pigeon.app.xixi.nomo.CoreApi.getChannel$pigeonVar_messageChannelSuffix';
     final BasicMessageChannel<Object?> pigeonVar_channel = BasicMessageChannel<Object?>(

+ 577 - 0
lib/utils/api_statistics.dart

@@ -0,0 +1,577 @@
+import 'dart:async';
+import 'dart:collection';
+import 'dart:convert';
+import 'dart:io';
+import 'package:get/get.dart';
+import 'package:path_provider/path_provider.dart';
+import 'package:uuid/uuid.dart';
+
+import '../app/api/core/api_core_paths.dart';
+import '../app/constants/configs.dart';
+import '../app/constants/enums.dart';
+import '../app/controllers/api_controller.dart';
+import '../app/data/sp/ix_sp.dart';
+import 'log/logger.dart';
+
+/// API请求统计记录
+class ApiStatRecord {
+  final String id;
+  final String domain;
+  final String path;
+  final bool success;
+  final int errorCode;
+  final String errorMessage;
+  final int apiRequestTime;
+  final int apiResponseTime;
+
+  ApiStatRecord({
+    required this.id,
+    required this.domain,
+    required this.path,
+    required this.success,
+    required this.errorCode,
+    required this.errorMessage,
+    required this.apiRequestTime,
+    required this.apiResponseTime,
+  });
+
+  /// 从JSON创建
+  factory ApiStatRecord.fromJson(Map<String, dynamic> json) {
+    return ApiStatRecord(
+      id: json['id'] ?? '',
+      domain: json['domain'] ?? '',
+      path: json['path'] ?? '',
+      success: json['success'] ?? false,
+      errorCode: json['errorCode'] ?? 0,
+      errorMessage: json['errorMessage'] ?? '',
+      apiRequestTime: json['apiRequestTime'] ?? 0,
+      apiResponseTime: json['apiResponseTime'] ?? 0,
+    );
+  }
+
+  /// 转换为JSON
+  Map<String, dynamic> toJson() {
+    return {
+      'id': id,
+      'domain': domain,
+      'path': path,
+      'success': success,
+      'errorCode': errorCode,
+      'errorMessage': errorMessage,
+      'apiRequestTime': apiRequestTime,
+      'apiResponseTime': apiResponseTime,
+    };
+  }
+
+  @override
+  String toString() {
+    return jsonEncode(toJson());
+  }
+}
+
+/// API统计管理器
+///
+/// 功能:
+/// - 记录 API 请求的成功/失败统计
+/// - 最大保存 100 条记录(FIFO)
+/// - 支持文件持久化存储
+/// - 批量写入优化(每 10 条)
+class ApiStatistics {
+  // 单例模式
+  static final ApiStatistics _instance = ApiStatistics._internal();
+  factory ApiStatistics() => _instance;
+  ApiStatistics._internal();
+
+  static ApiStatistics get instance => _instance;
+
+  static const String TAG = 'ApiStatistics';
+
+  // 最大保存记录数
+  static const int maxRecords = 100;
+
+  // 批量写入阈值
+  static const int _batchWriteThreshold = 10;
+
+  // 文件名
+  static const String _fileName = 'api_statistics.json';
+
+  // 使用Queue来保持FIFO顺序
+  final Queue<ApiStatRecord> _records = Queue<ApiStatRecord>();
+
+  // UUID生成器
+  final Uuid _uuid = const Uuid();
+
+  // 操作锁,防止在遍历时修改
+  bool _isOperating = false;
+
+  // 是否已初始化
+  bool _isInitialized = false;
+
+  // 未保存的记录数
+  int _unsavedCount = 0;
+
+  // 是否正在写入文件
+  bool _isWriting = false;
+
+  // 文件路径缓存
+  String? _filePath;
+
+  // 禁用的日志模块缓存
+  Set<String> _disabledModules = {};
+
+  /// 初始化(需要在 App 启动时调用)
+  Future<void> initialize() async {
+    if (_isInitialized) return;
+
+    try {
+      // 获取文件路径
+      _filePath = await _getFilePath();
+
+      // 从文件加载数据
+      await _loadFromFile();
+
+      _isInitialized = true;
+      log(TAG, 'ApiStatistics initialized, loaded ${_records.length} records');
+    } catch (e) {
+      log(TAG, 'ApiStatistics initialize error: $e');
+    }
+  }
+
+  /// 更新禁用模块列表(在 launch 数据更新后调用)
+  void updateDisabledModules() {
+    try {
+      final launch = IXSP.getLaunch();
+      _disabledModules = (launch?.appConfig?.disabledLogModules ?? []).toSet();
+      log(TAG, 'ApiStatistics disabled modules updated: $_disabledModules');
+    } catch (e) {
+      log(TAG, 'ApiStatistics updateDisabledModules error: $e');
+    }
+  }
+
+  /// 检查模块是否被禁用
+  bool _isModuleDisabled(String path) {
+    var moduleName = LogModule.NM_ApiOtherLog.name;
+    if (path == ApiCorePaths.launch) {
+      moduleName = LogModule.NM_ApiLaunchLog.name;
+    } else if (path == ApiCorePaths.getDispatchInfo) {
+      moduleName = LogModule.NM_ApiRouterLog.name;
+    }
+    return _disabledModules.contains(moduleName);
+  }
+
+  /// 获取文件路径
+  Future<String> _getFilePath() async {
+    if (_filePath != null) return _filePath!;
+    final directory = await getApplicationDocumentsDirectory();
+    _filePath = '${directory.path}/$_fileName';
+    return _filePath!;
+  }
+
+  /// 从文件加载数据
+  Future<void> _loadFromFile() async {
+    try {
+      final filePath = await _getFilePath();
+      final file = File(filePath);
+
+      if (!await file.exists()) {
+        log(TAG, 'ApiStatistics file not exists, skip loading');
+        return;
+      }
+
+      final content = await file.readAsString();
+      if (content.isEmpty) return;
+
+      final List<dynamic> jsonList = jsonDecode(content);
+      _records.clear();
+
+      for (final json in jsonList) {
+        if (json is Map<String, dynamic>) {
+          _records.addLast(ApiStatRecord.fromJson(json));
+        }
+      }
+
+      // 确保不超过最大数量
+      while (_records.length > maxRecords) {
+        _records.removeFirst();
+      }
+
+      log(TAG, 'ApiStatistics loaded ${_records.length} records from file');
+    } catch (e) {
+      log(TAG, 'ApiStatistics load error: $e');
+    }
+  }
+
+  /// 保存到文件
+  Future<void> _saveToFile() async {
+    if (_isWriting) return;
+    if (_unsavedCount == 0) return;
+
+    _isWriting = true;
+    try {
+      // 更新禁用模块列表
+      updateDisabledModules();
+
+      final filePath = await _getFilePath();
+      final file = File(filePath);
+
+      // 过滤掉被禁用模块的记录
+      _filterDisabledRecords();
+
+      // 获取记录副本
+      final records = getRecordsJson();
+
+      // 写入文件
+      await file.writeAsString(jsonEncode(records));
+
+      _unsavedCount = 0;
+      log(TAG, 'ApiStatistics saved ${records.length} records to file');
+    } catch (e) {
+      log(TAG, 'ApiStatistics save error: $e');
+    } finally {
+      _isWriting = false;
+    }
+  }
+
+  /// 过滤掉被禁用模块的记录
+  void _filterDisabledRecords() {
+    if (_disabledModules.isEmpty) return;
+
+    final toRemove = <ApiStatRecord>[];
+    for (final record in _records) {
+      if (_isModuleDisabled(record.path)) {
+        toRemove.add(record);
+      }
+    }
+
+    for (final record in toRemove) {
+      _records.remove(record);
+    }
+
+    if (toRemove.isNotEmpty) {
+      log(
+        TAG,
+        'ApiStatistics filtered out ${toRemove.length} disabled module records',
+      );
+    }
+  }
+
+  /// 检查是否需要保存
+  void _checkAndSave() {
+    if (_unsavedCount >= _batchWriteThreshold) {
+      _saveToFile();
+    }
+  }
+
+  /// 添加一条成功记录
+  void addSuccess({
+    required String domain,
+    required String path,
+    required int apiRequestTime,
+    required int apiResponseTime,
+    int errorCode = 200,
+  }) {
+    _addRecord(
+      domain: domain,
+      path: path,
+      success: true,
+      errorCode: errorCode,
+      errorMessage: '',
+      apiRequestTime: apiRequestTime,
+      apiResponseTime: apiResponseTime,
+    );
+  }
+
+  /// 添加一条失败记录
+  void addFailure({
+    required String domain,
+    required String path,
+    required int apiRequestTime,
+    required int apiResponseTime,
+    required int errorCode,
+    required String errorMessage,
+  }) {
+    _addRecord(
+      domain: domain,
+      path: path,
+      success: false,
+      errorCode: errorCode,
+      errorMessage: errorMessage,
+      apiRequestTime: apiRequestTime,
+      apiResponseTime: apiResponseTime,
+    );
+  }
+
+  /// 内部添加记录方法(同步,线程安全)
+  void _addRecord({
+    required String domain,
+    required String path,
+    required bool success,
+    required int errorCode,
+    required String errorMessage,
+    required int apiRequestTime,
+    required int apiResponseTime,
+  }) {
+    // 如果正在进行其他操作,延迟添加
+    if (_isOperating) {
+      scheduleMicrotask(
+        () => _addRecord(
+          domain: domain,
+          path: path,
+          success: success,
+          errorCode: errorCode,
+          errorMessage: errorMessage,
+          apiRequestTime: apiRequestTime,
+          apiResponseTime: apiResponseTime,
+        ),
+      );
+      return;
+    }
+
+    final record = ApiStatRecord(
+      id: _uuid.v4(),
+      domain: domain,
+      path: path,
+      success: success,
+      errorCode: errorCode,
+      errorMessage: errorMessage,
+      apiRequestTime: apiRequestTime,
+      apiResponseTime: apiResponseTime,
+    );
+
+    _records.addLast(record);
+    _unsavedCount++;
+
+    // 超出最大数量时,移除最早的记录
+    while (_records.length > maxRecords) {
+      _records.removeFirst();
+    }
+
+    // 检查是否需要保存
+    _checkAndSave();
+  }
+
+  /// 获取所有记录(返回副本,避免并发修改问题)
+  List<ApiStatRecord> getRecords() {
+    _isOperating = true;
+    try {
+      return List<ApiStatRecord>.from(_records);
+    } finally {
+      _isOperating = false;
+    }
+  }
+
+  /// 获取所有记录的JSON列表(返回副本)
+  List<Map<String, dynamic>> getRecordsJson() {
+    _isOperating = true;
+    try {
+      return _records.map((r) => r.toJson()).toList();
+    } finally {
+      _isOperating = false;
+    }
+  }
+
+  /// 获取所有记录的JSON字符串
+  String getRecordsJsonString({bool pretty = false}) {
+    final json = getRecordsJson();
+    if (pretty) {
+      return const JsonEncoder.withIndent('  ').convert(json);
+    }
+    return jsonEncode(json);
+  }
+
+  /// 获取成功记录数
+  int get successCount {
+    final records = getRecords();
+    return records.where((r) => r.success).length;
+  }
+
+  /// 获取失败记录数
+  int get failureCount {
+    final records = getRecords();
+    return records.where((r) => !r.success).length;
+  }
+
+  /// 获取总记录数
+  int get totalCount => _records.length;
+
+  /// 获取成功率
+  double get successRate {
+    final total = totalCount;
+    if (total == 0) return 0.0;
+    return successCount / total;
+  }
+
+  /// 获取平均响应时间(毫秒)
+  double get averageResponseTime {
+    final records = getRecords();
+    if (records.isEmpty) return 0.0;
+    final totalTime = records.fold<int>(0, (sum, r) => sum + r.apiResponseTime);
+    return totalTime / records.length;
+  }
+
+  /// 清空所有记录
+  Future<void> clear() async {
+    _isOperating = true;
+    try {
+      _records.clear();
+      _unsavedCount = 0;
+      // 清空文件
+      await _saveToFile();
+    } finally {
+      _isOperating = false;
+    }
+  }
+
+  /// 移除已上传的记录,保留上传过程中新增的记录
+  Future<void> _removeUploadedRecords(
+    List<ApiStatRecord> uploadedRecords,
+  ) async {
+    _isOperating = true;
+    try {
+      // 获取已上传记录的 ID 集合
+      final uploadedIds = uploadedRecords.map((r) => r.id).toSet();
+      // 移除已上传的记录
+      _records.removeWhere((r) => uploadedIds.contains(r.id));
+      _unsavedCount = _records.length;
+      // 保存剩余记录到文件(包括上传过程中新增的记录)
+      await _forceSaveToFile();
+    } finally {
+      _isOperating = false;
+    }
+  }
+
+  /// 强制保存到文件(忽略 _unsavedCount 检查)
+  Future<void> _forceSaveToFile() async {
+    if (_isWriting) return;
+
+    _isWriting = true;
+    try {
+      final filePath = await _getFilePath();
+      final file = File(filePath);
+      final records = _records.map((r) => r.toJson()).toList();
+      await file.writeAsString(jsonEncode(records));
+      _unsavedCount = 0;
+      log(TAG, 'ApiStatistics force saved ${records.length} records to file');
+    } catch (e) {
+      log(TAG, 'ApiStatistics force save error: $e');
+    } finally {
+      _isWriting = false;
+    }
+  }
+
+  /// 获取最近N条记录(返回副本)
+  List<ApiStatRecord> getRecentRecords(int count) {
+    final records = getRecords();
+    if (count >= records.length) {
+      return records;
+    }
+    return records.sublist(records.length - count);
+  }
+
+  /// 获取统计摘要
+  Map<String, dynamic> getSummary() {
+    final records = getRecords();
+    final total = records.length;
+    final success = records.where((r) => r.success).length;
+    final failure = total - success;
+    final rate = total > 0 ? success / total : 0.0;
+    final avgTime = total > 0
+        ? records.fold<int>(0, (sum, r) => sum + r.apiResponseTime) / total
+        : 0.0;
+
+    return {
+      'totalCount': total,
+      'successCount': success,
+      'failureCount': failure,
+      'successRate': '${(rate * 100).toStringAsFixed(2)}%',
+      'averageResponseTime': '${avgTime.toStringAsFixed(2)}ms',
+    };
+  }
+
+  /// 强制保存到文件(App 进入后台或退出时调用)
+  Future<void> flush() async {
+    await _saveToFile();
+  }
+
+  /// 释放资源(App 退出时调用)
+  Future<void> dispose() async {
+    await _saveToFile();
+    _isInitialized = false;
+  }
+
+  /// App 进入后台时调用
+  Future<void> onAppPaused() async {
+    await _saveToFile();
+  }
+
+  /// App 进入前台时调用
+  Future<void> onAppResumed() async {
+    await _loadFromFile();
+    await _uploadAndClear();
+  }
+
+  /// 上传统计数据并清空
+  Future<void> _uploadAndClear() async {
+    try {
+      // 先更新禁用模块列表
+      updateDisabledModules();
+
+      final records = getRecords();
+      if (records.isEmpty) return;
+
+      // 检查模块是否禁用
+      final isLaunchDisabled = _disabledModules.contains(
+        LogModule.NM_ApiLaunchLog.name,
+      );
+      final isRouterDisabled = _disabledModules.contains(
+        LogModule.NM_ApiRouterLog.name,
+      );
+
+      // 过滤掉禁用模块的记录
+      final enabledRecords = records.where((record) {
+        if (record.path == ApiCorePaths.launch) {
+          return !isLaunchDisabled;
+        } else {
+          return !isRouterDisabled;
+        }
+      }).toList();
+
+      // 合并上传(一次接口调用)
+      if (enabledRecords.isNotEmpty) {
+        final apiController = Get.find<ApiController>();
+        final logs = _formatLogsForUpload(enabledRecords);
+        await apiController.uploadApiStatisticsLog(logs);
+      }
+
+      // 移除已上传的记录,保留上传过程中新增的记录(如上传接口本身的记录)
+      await _removeUploadedRecords(records);
+      log(
+        TAG,
+        'ApiStatistics uploaded ${enabledRecords.length}/${records.length} records '
+        '(launch disabled: $isLaunchDisabled, router disabled: $isRouterDisabled)',
+      );
+    } catch (e) {
+      log(TAG, 'ApiStatistics upload error: $e');
+    }
+  }
+
+  /// 格式化日志数据用于上传
+  List<Map<String, dynamic>> _formatLogsForUpload(List<ApiStatRecord> records) {
+    return records.map((record) {
+      var moduleName = LogModule.NM_ApiOtherLog.name;
+      if (record.path == ApiCorePaths.launch) {
+        moduleName = LogModule.NM_ApiLaunchLog.name;
+      } else if (record.path == ApiCorePaths.getDispatchInfo) {
+        moduleName = LogModule.NM_ApiRouterLog.name;
+      }
+      return {
+        'id': record.id,
+        'time': record.apiRequestTime,
+        'level': LogLevel.info.name,
+        'module': moduleName,
+        'category': Configs.productCode,
+        'fields': record.toJson(),
+      };
+    }).toList();
+  }
+}

+ 8 - 12
lib/utils/boost_logger.dart

@@ -21,7 +21,14 @@ class BoostLogger {
 
   /// 获取日志目录
   Future<Directory> _getLogDirectory() async {
-    if (Platform.isIOS) {
+    if (Platform.isAndroid) {
+      final appDir = await getApplicationSupportDirectory();
+      final logDir = Directory('${appDir.path}/$LOG_FOLDER');
+      if (!await logDir.exists()) {
+        await logDir.create(recursive: true);
+      }
+      return logDir;
+    } else {
       try {
         // iOS 使用 App Group 目录
         final Directory? appGroupDirectory =
@@ -62,17 +69,6 @@ class BoostLogger {
         }
         return logDir;
       }
-    } else {
-      // Android 使用外部存储目录
-      final appDir = await getExternalStorageDirectory();
-      if (appDir == null) {
-        throw Exception('Failed to get external storage directory');
-      }
-      final logDir = Directory('${appDir.path}/$LOG_FOLDER');
-      if (!await logDir.exists()) {
-        await logDir.create(recursive: true);
-      }
-      return logDir;
     }
   }
 

+ 3 - 0
lib/utils/boost_report_manager.dart

@@ -1,3 +1,5 @@
+import 'package:system_clock/system_clock.dart';
+
 import 'boost_logger.dart';
 import 'log/logger.dart';
 import 'ntp_time_service.dart';
@@ -51,6 +53,7 @@ class BoostReportManager {
         'boostStartTime': _getCurrentTimestamp(),
         'boostStopTime': 0,
         'boostDuration': 0,
+        'elapsedRealtime': SystemClock.elapsedRealtime().inMilliseconds,
         'userDeviceInfo': {
           'deviceModel': deviceInfo['deviceModel'] ?? '',
           'osVersion': deviceInfo['osVersion'] ?? '',

+ 144 - 336
lib/utils/developer/ix_developer_tools.dart

@@ -424,91 +424,49 @@ class _SimpleDeveloperToolsScreenState extends State<SimpleDeveloperToolsScreen>
         title: Text(
           '开发者工具',
           style: TextStyle(
-            fontWeight: FontWeight.bold,
-            fontSize: 18,
+            fontWeight: FontWeight.w600,
+            fontSize: 17,
             color: Get.reactiveTheme.textTheme.bodyLarge!.color,
           ),
         ),
         bottom: PreferredSize(
-          preferredSize: const Size.fromHeight(50),
+          preferredSize: const Size.fromHeight(44),
           child: Container(
-            margin: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
+            margin: const EdgeInsets.symmetric(horizontal: 16, vertical: 6),
             decoration: BoxDecoration(
               color: Colors.grey[100],
-              borderRadius: BorderRadius.circular(25),
-              boxShadow: [
-                BoxShadow(
-                  color: Colors.grey[300]!.withValues(alpha: 0.5),
-                  blurRadius: 4,
-                  offset: const Offset(0, 2),
-                ),
-              ],
+              borderRadius: BorderRadius.circular(8),
             ),
             child: TabBar(
               controller: _tabController,
               indicator: BoxDecoration(
-                gradient: LinearGradient(
-                  colors: [Colors.blue[500]!, Colors.purple[500]!],
-                ),
-                borderRadius: BorderRadius.circular(25),
-                boxShadow: [
-                  BoxShadow(
-                    color: Colors.blue[200]!.withValues(alpha: 0.5),
-                    blurRadius: 6,
-                    offset: const Offset(0, 2),
-                  ),
-                ],
+                color: Colors.black87,
+                borderRadius: BorderRadius.circular(6),
               ),
               indicatorSize: TabBarIndicatorSize.tab,
               dividerColor: Colors.transparent,
               labelColor: Colors.white,
               unselectedLabelColor: Colors.grey[600],
               labelStyle: const TextStyle(
-                fontWeight: FontWeight.bold,
-                fontSize: 14,
+                fontWeight: FontWeight.w500,
+                fontSize: 13,
               ),
               unselectedLabelStyle: const TextStyle(
-                fontWeight: FontWeight.w500,
-                fontSize: 14,
+                fontWeight: FontWeight.w400,
+                fontSize: 13,
               ),
+              indicatorPadding: const EdgeInsets.all(3),
               tabs: const [
-                Tab(
-                  child: Row(
-                    mainAxisAlignment: MainAxisAlignment.center,
-                    children: [
-                      Icon(Icons.terminal, size: 18),
-                      SizedBox(width: 6),
-                      Text('控制台日志'),
-                    ],
-                  ),
-                ),
-                Tab(
-                  child: Row(
-                    mainAxisAlignment: MainAxisAlignment.center,
-                    children: [
-                      Icon(Icons.network_check, size: 18),
-                      SizedBox(width: 6),
-                      Text('API监控'),
-                    ],
-                  ),
-                ),
+                Tab(text: '日志'),
+                Tab(text: 'API'),
               ],
             ),
           ),
         ),
       ),
-      body: Container(
-        decoration: const BoxDecoration(
-          gradient: LinearGradient(
-            begin: Alignment.topCenter,
-            end: Alignment.bottomCenter,
-            colors: [Colors.grey, Colors.blue],
-          ),
-        ),
-        child: TabBarView(
-          controller: _tabController,
-          children: const [SimpleConsoleLogTab(), SimpleApiMonitorTab()],
-        ),
+      body: TabBarView(
+        controller: _tabController,
+        children: const [SimpleConsoleLogTab(), SimpleApiMonitorTab()],
       ),
     );
   }
@@ -528,17 +486,10 @@ class SimpleDeveloperToolsDialog extends StatelessWidget {
         height: Get.height * 0.9,
         decoration: BoxDecoration(
           color: Colors.white,
-          borderRadius: BorderRadius.circular(20),
-          boxShadow: [
-            BoxShadow(
-              color: Colors.black.withValues(alpha: 0.1),
-              blurRadius: 20,
-              offset: const Offset(0, 10),
-            ),
-          ],
+          borderRadius: BorderRadius.circular(12),
         ),
         child: ClipRRect(
-          borderRadius: BorderRadius.circular(20),
+          borderRadius: BorderRadius.circular(12),
           child: const SimpleDeveloperToolsScreen(),
         ),
       ),
@@ -591,85 +542,51 @@ class _SimpleConsoleLogTabState extends State<SimpleConsoleLogTab> {
       children: [
         // 搜索和操作栏
         Container(
-          margin: const EdgeInsets.all(16),
-          padding: const EdgeInsets.all(16),
-          decoration: BoxDecoration(
-            color: Colors.white,
-            borderRadius: BorderRadius.circular(16),
-            boxShadow: [
-              BoxShadow(
-                color: Colors.grey[200]!.withValues(alpha: 0.8),
-                blurRadius: 10,
-                offset: const Offset(0, 4),
-              ),
-            ],
-          ),
-          child: Column(
+          padding: const EdgeInsets.all(12),
+          child: Row(
             children: [
               // 搜索框
-              Container(
-                decoration: BoxDecoration(
-                  color: Colors.grey[50],
-                  borderRadius: BorderRadius.circular(12),
-                  border: Border.all(color: Colors.blue[200]!, width: 1),
-                ),
-                child: TextField(
-                  controller: _searchController,
-                  decoration: InputDecoration(
-                    hintText: '搜索日志内容...',
-                    hintStyle: TextStyle(color: Colors.grey[500]),
-                    prefixIcon: Icon(Icons.search, color: Colors.blue[600]),
-                    border: InputBorder.none,
-                    contentPadding: const EdgeInsets.symmetric(
-                      horizontal: 16,
-                      vertical: 12,
-                    ),
+              Expanded(
+                child: Container(
+                  height: 36,
+                  decoration: BoxDecoration(
+                    color: Colors.grey[100],
+                    borderRadius: BorderRadius.circular(8),
                   ),
-                  onChanged: (_) => _updateLogs(),
-                ),
-              ),
-              const SizedBox(height: 12),
-              // 操作按钮
-              Row(
-                children: [
-                  Expanded(
-                    child: ElevatedButton.icon(
-                      icon: const Icon(Icons.refresh, size: 18),
-                      label: const Text('刷新'),
-                      style: ElevatedButton.styleFrom(
-                        backgroundColor: Colors.blue[500],
-                        foregroundColor: Colors.white,
-                        elevation: 2,
-                        padding: const EdgeInsets.symmetric(vertical: 12),
-                        shape: RoundedRectangleBorder(
-                          borderRadius: BorderRadius.circular(10),
-                        ),
+                  child: TextField(
+                    controller: _searchController,
+                    style: const TextStyle(fontSize: 13),
+                    decoration: InputDecoration(
+                      hintText: '搜索...',
+                      hintStyle: TextStyle(
+                        color: Colors.grey[400],
+                        fontSize: 13,
                       ),
-                      onPressed: _updateLogs,
-                    ),
-                  ),
-                  const SizedBox(width: 12),
-                  Expanded(
-                    child: ElevatedButton.icon(
-                      icon: const Icon(Icons.clear_all, size: 18),
-                      label: const Text('清除'),
-                      style: ElevatedButton.styleFrom(
-                        backgroundColor: Colors.red[500],
-                        foregroundColor: Colors.white,
-                        elevation: 2,
-                        padding: const EdgeInsets.symmetric(vertical: 12),
-                        shape: RoundedRectangleBorder(
-                          borderRadius: BorderRadius.circular(10),
-                        ),
+                      prefixIcon: Icon(
+                        Icons.search,
+                        color: Colors.grey[400],
+                        size: 18,
+                      ),
+                      border: InputBorder.none,
+                      isDense: true,
+                      contentPadding: const EdgeInsets.symmetric(
+                        vertical: 8,
+                        horizontal: 0,
                       ),
-                      onPressed: () {
-                        IXDeveloperTools.clearLogs();
-                        _updateLogs();
-                      },
                     ),
+                    onChanged: (_) => _updateLogs(),
                   ),
-                ],
+                ),
               ),
+              const SizedBox(width: 8),
+              // 刷新按钮
+              _buildIconButton(Icons.refresh, _updateLogs),
+              const SizedBox(width: 6),
+              // 清除按钮
+              _buildIconButton(Icons.delete_outline, () {
+                IXDeveloperTools.clearLogs();
+                _updateLogs();
+              }),
             ],
           ),
         ),
@@ -677,63 +594,51 @@ class _SimpleConsoleLogTabState extends State<SimpleConsoleLogTab> {
         Expanded(
           child: _filteredLogs.isEmpty
               ? Center(
-                  child: Container(
-                    padding: const EdgeInsets.all(32),
-                    child: Column(
-                      mainAxisSize: MainAxisSize.min,
-                      children: [
-                        Container(
-                          padding: const EdgeInsets.all(20),
-                          decoration: BoxDecoration(
-                            gradient: LinearGradient(
-                              colors: [Colors.blue[100]!, Colors.purple[100]!],
-                            ),
-                            shape: BoxShape.circle,
-                          ),
-                          child: Icon(
-                            Icons.terminal,
-                            size: 48,
-                            color: Colors.blue[600],
-                          ),
-                        ),
-                        const SizedBox(height: 16),
-                        Text(
-                          '暂无日志',
-                          style: TextStyle(
-                            fontSize: 18,
-                            fontWeight: FontWeight.bold,
-                            color: Colors.grey[600],
-                          ),
-                        ),
-                        const SizedBox(height: 8),
-                        Text(
-                          '开始使用应用后,日志将显示在这里',
-                          style: TextStyle(
-                            fontSize: 14,
-                            color: Colors.grey[500],
-                          ),
-                        ),
-                      ],
-                    ),
+                  child: Column(
+                    mainAxisSize: MainAxisSize.min,
+                    children: [
+                      Icon(
+                        Icons.inbox_outlined,
+                        size: 48,
+                        color: Colors.grey[300],
+                      ),
+                      const SizedBox(height: 12),
+                      Text(
+                        '暂无日志',
+                        style: TextStyle(fontSize: 14, color: Colors.grey[400]),
+                      ),
+                    ],
                   ),
                 )
-              : RefreshIndicator(
-                  onRefresh: () async {
-                    _updateLogs();
+              : ListView.separated(
+                  padding: const EdgeInsets.symmetric(horizontal: 12),
+                  itemCount: _filteredLogs.length,
+                  separatorBuilder: (_, __) => const SizedBox(height: 1),
+                  itemBuilder: (context, index) {
+                    final log = _filteredLogs[index];
+                    return SimpleLogCard(log: log);
                   },
-                  child: ListView.builder(
-                    physics: const AlwaysScrollableScrollPhysics(),
-                    itemCount: _filteredLogs.length,
-                    itemBuilder: (context, index) {
-                      final log = _filteredLogs[index];
-                      return SimpleLogCard(log: log);
-                    },
-                  ),
                 ),
         ),
       ],
     );
   }
+
+  Widget _buildIconButton(IconData icon, VoidCallback onTap) {
+    return InkWell(
+      onTap: onTap,
+      borderRadius: BorderRadius.circular(8),
+      child: Container(
+        width: 36,
+        height: 36,
+        decoration: BoxDecoration(
+          color: Colors.grey[100],
+          borderRadius: BorderRadius.circular(8),
+        ),
+        child: Icon(icon, size: 18, color: Colors.grey[600]),
+      ),
+    );
+  }
 }
 
 /// 简化版API监控标签页
@@ -753,108 +658,23 @@ class _SimpleApiMonitorTabState extends State<SimpleApiMonitorTab> {
       children: [
         // 操作栏
         Container(
-          margin: const EdgeInsets.all(16),
-          padding: const EdgeInsets.all(16),
-          decoration: BoxDecoration(
-            color: Colors.white,
-            borderRadius: BorderRadius.circular(16),
-            boxShadow: [
-              BoxShadow(
-                color: Colors.grey[200]!.withValues(alpha: 0.8),
-                blurRadius: 10,
-                offset: const Offset(0, 4),
-              ),
-            ],
-          ),
-          child: Column(
+          padding: const EdgeInsets.all(12),
+          child: Row(
             children: [
               // 统计信息
-              Container(
-                width: double.infinity,
-                padding: const EdgeInsets.all(12),
-                decoration: BoxDecoration(
-                  gradient: LinearGradient(
-                    colors: [Colors.blue[50]!, Colors.purple[50]!],
-                  ),
-                  borderRadius: BorderRadius.circular(12),
-                  border: Border.all(color: Colors.blue[200]!, width: 1),
-                ),
-                child: Row(
-                  children: [
-                    Icon(Icons.analytics, color: Colors.blue[600], size: 20),
-                    const SizedBox(width: 8),
-                    Text(
-                      '共 ${requests.length} 个请求',
-                      style: TextStyle(
-                        fontSize: 16,
-                        fontWeight: FontWeight.bold,
-                        color: Colors.blue[700],
-                      ),
-                    ),
-                    const Spacer(),
-                    Container(
-                      padding: const EdgeInsets.symmetric(
-                        horizontal: 8,
-                        vertical: 4,
-                      ),
-                      decoration: BoxDecoration(
-                        color: Colors.blue[100],
-                        borderRadius: BorderRadius.circular(8),
-                      ),
-                      child: Text(
-                        '实时监控',
-                        style: TextStyle(
-                          fontSize: 12,
-                          color: Colors.blue[700],
-                          fontWeight: FontWeight.w500,
-                        ),
-                      ),
-                    ),
-                  ],
-                ),
-              ),
-              const SizedBox(height: 12),
-              // 操作按钮
-              Row(
-                children: [
-                  Expanded(
-                    child: ElevatedButton.icon(
-                      icon: const Icon(Icons.refresh, size: 18),
-                      label: const Text('刷新'),
-                      style: ElevatedButton.styleFrom(
-                        backgroundColor: Colors.green[500],
-                        foregroundColor: Colors.white,
-                        elevation: 2,
-                        padding: const EdgeInsets.symmetric(vertical: 12),
-                        shape: RoundedRectangleBorder(
-                          borderRadius: BorderRadius.circular(10),
-                        ),
-                      ),
-                      onPressed: () => setState(() {}),
-                    ),
-                  ),
-                  const SizedBox(width: 12),
-                  Expanded(
-                    child: ElevatedButton.icon(
-                      icon: const Icon(Icons.clear_all, size: 18),
-                      label: const Text('清除'),
-                      style: ElevatedButton.styleFrom(
-                        backgroundColor: Colors.orange[500],
-                        foregroundColor: Colors.white,
-                        elevation: 2,
-                        padding: const EdgeInsets.symmetric(vertical: 12),
-                        shape: RoundedRectangleBorder(
-                          borderRadius: BorderRadius.circular(10),
-                        ),
-                      ),
-                      onPressed: () {
-                        IXDeveloperTools.clearApiRequests();
-                        setState(() {});
-                      },
-                    ),
-                  ),
-                ],
+              Text(
+                '${requests.length} 个请求',
+                style: TextStyle(fontSize: 13, color: Colors.grey[600]),
               ),
+              const Spacer(),
+              // 刷新按钮
+              _buildIconButton(Icons.refresh, () => setState(() {})),
+              const SizedBox(width: 6),
+              // 清除按钮
+              _buildIconButton(Icons.delete_outline, () {
+                IXDeveloperTools.clearApiRequests();
+                setState(() {});
+              }),
             ],
           ),
         ),
@@ -862,61 +682,49 @@ class _SimpleApiMonitorTabState extends State<SimpleApiMonitorTab> {
         Expanded(
           child: requests.isEmpty
               ? Center(
-                  child: Container(
-                    padding: const EdgeInsets.all(32),
-                    child: Column(
-                      mainAxisSize: MainAxisSize.min,
-                      children: [
-                        Container(
-                          padding: const EdgeInsets.all(20),
-                          decoration: BoxDecoration(
-                            gradient: LinearGradient(
-                              colors: [Colors.green[100]!, Colors.blue[100]!],
-                            ),
-                            shape: BoxShape.circle,
-                          ),
-                          child: Icon(
-                            Icons.network_check,
-                            size: 48,
-                            color: Colors.green[600],
-                          ),
-                        ),
-                        const SizedBox(height: 16),
-                        Text(
-                          '暂无API请求',
-                          style: TextStyle(
-                            fontSize: 18,
-                            fontWeight: FontWeight.bold,
-                            color: Colors.grey[600],
-                          ),
-                        ),
-                        const SizedBox(height: 8),
-                        Text(
-                          '开始网络请求后,API信息将显示在这里',
-                          style: TextStyle(
-                            fontSize: 14,
-                            color: Colors.grey[500],
-                          ),
-                        ),
-                      ],
-                    ),
+                  child: Column(
+                    mainAxisSize: MainAxisSize.min,
+                    children: [
+                      Icon(
+                        Icons.cloud_off_outlined,
+                        size: 48,
+                        color: Colors.grey[300],
+                      ),
+                      const SizedBox(height: 12),
+                      Text(
+                        '暂无请求',
+                        style: TextStyle(fontSize: 14, color: Colors.grey[400]),
+                      ),
+                    ],
                   ),
                 )
-              : RefreshIndicator(
-                  onRefresh: () async {
-                    setState(() {});
+              : ListView.separated(
+                  padding: const EdgeInsets.symmetric(horizontal: 12),
+                  itemCount: requests.length,
+                  separatorBuilder: (_, __) => const SizedBox(height: 1),
+                  itemBuilder: (context, index) {
+                    final request = requests[index];
+                    return SimpleApiCard(request: request);
                   },
-                  child: ListView.builder(
-                    physics: const AlwaysScrollableScrollPhysics(),
-                    itemCount: requests.length,
-                    itemBuilder: (context, index) {
-                      final request = requests[index];
-                      return SimpleApiCard(request: request);
-                    },
-                  ),
                 ),
         ),
       ],
     );
   }
+
+  Widget _buildIconButton(IconData icon, VoidCallback onTap) {
+    return InkWell(
+      onTap: onTap,
+      borderRadius: BorderRadius.circular(8),
+      child: Container(
+        width: 36,
+        height: 36,
+        decoration: BoxDecoration(
+          color: Colors.grey[100],
+          borderRadius: BorderRadius.circular(8),
+        ),
+        child: Icon(icon, size: 18, color: Colors.grey[600]),
+      ),
+    );
+  }
 }

+ 399 - 343
lib/utils/developer/simple_api_card.dart

@@ -4,366 +4,442 @@ import 'package:flutter/services.dart';
 import 'package:get/get.dart';
 import 'ix_developer_tools.dart';
 
-/// 轻量级API请求卡片
-class SimpleApiCard extends StatefulWidget {
+/// 轻量级API请求卡片 - 纵向排列,点击弹窗显示详情
+class SimpleApiCard extends StatelessWidget {
   final ApiRequestInfo request;
 
   const SimpleApiCard({super.key, required this.request});
 
-  @override
-  State<SimpleApiCard> createState() => _SimpleApiCardState();
-}
+  /// 格式化请求时间
+  String get _formattedTime {
+    final t = request.timestamp;
+    return '${t.hour.toString().padLeft(2, '0')}:'
+        '${t.minute.toString().padLeft(2, '0')}:'
+        '${t.second.toString().padLeft(2, '0')}';
+  }
 
-class _SimpleApiCardState extends State<SimpleApiCard>
-    with SingleTickerProviderStateMixin {
-  bool _isExpanded = false;
-  late AnimationController _animationController;
-  late Animation<double> _expandAnimation;
-  late Animation<double> _iconRotation;
+  /// 获取完整URL
+  String get _fullUrl {
+    return request.url;
+  }
 
   @override
-  void initState() {
-    super.initState();
-    _animationController = AnimationController(
-      duration: const Duration(milliseconds: 300),
-      vsync: this,
+  Widget build(BuildContext context) {
+    return InkWell(
+      onTap: () => _showDetailDialog(context),
+      borderRadius: BorderRadius.circular(10),
+      child: Container(
+        margin: const EdgeInsets.symmetric(horizontal: 4, vertical: 4),
+        padding: const EdgeInsets.all(12),
+        decoration: BoxDecoration(
+          color: Colors.white,
+          borderRadius: BorderRadius.circular(10),
+          border: Border.all(color: Colors.grey[200]!, width: 1),
+          boxShadow: [
+            BoxShadow(
+              color: Colors.black.withValues(alpha: 0.03),
+              blurRadius: 4,
+              offset: const Offset(0, 1),
+            ),
+          ],
+        ),
+        child: Column(
+          crossAxisAlignment: CrossAxisAlignment.start,
+          children: [
+            // 第一行:方法、状态码、耗时、时间
+            Row(
+              children: [
+                // 状态指示点
+                Container(
+                  width: 8,
+                  height: 8,
+                  decoration: BoxDecoration(
+                    color: _getStatusColor(),
+                    shape: BoxShape.circle,
+                  ),
+                ),
+                const SizedBox(width: 8),
+                // 方法标签
+                Container(
+                  padding: const EdgeInsets.symmetric(
+                    horizontal: 8,
+                    vertical: 3,
+                  ),
+                  decoration: BoxDecoration(
+                    color: _getMethodColor().withValues(alpha: 0.1),
+                    borderRadius: BorderRadius.circular(4),
+                  ),
+                  child: Text(
+                    request.method,
+                    style: TextStyle(
+                      fontSize: 11,
+                      fontWeight: FontWeight.w600,
+                      color: _getMethodColor(),
+                    ),
+                  ),
+                ),
+                const SizedBox(width: 8),
+                // 状态码
+                if (request.statusCode != null)
+                  Container(
+                    padding: const EdgeInsets.symmetric(
+                      horizontal: 6,
+                      vertical: 3,
+                    ),
+                    decoration: BoxDecoration(
+                      color: _getStatusColor().withValues(alpha: 0.1),
+                      borderRadius: BorderRadius.circular(4),
+                    ),
+                    child: Text(
+                      '${request.statusCode}',
+                      style: TextStyle(
+                        fontSize: 11,
+                        fontWeight: FontWeight.w600,
+                        color: _getStatusColor(),
+                      ),
+                    ),
+                  ),
+                const Spacer(),
+                // 耗时
+                if (request.duration != null)
+                  Text(
+                    '${request.duration!.inMilliseconds}ms',
+                    style: TextStyle(fontSize: 10, color: Colors.grey[500]),
+                  ),
+                const SizedBox(width: 8),
+                // 时间
+                Text(
+                  _formattedTime,
+                  style: TextStyle(
+                    fontSize: 10,
+                    color: Colors.grey[400],
+                    fontFamily: 'monospace',
+                  ),
+                ),
+              ],
+            ),
+            const SizedBox(height: 8),
+            // 第二行:完整URL
+            Text(
+              _fullUrl,
+              style: TextStyle(fontSize: 12, color: Colors.grey[700]),
+              maxLines: 2,
+              overflow: TextOverflow.ellipsis,
+            ),
+          ],
+        ),
+      ),
     );
-    _expandAnimation = CurvedAnimation(
-      parent: _animationController,
-      curve: Curves.easeInOut,
+  }
+
+  void _showDetailDialog(BuildContext context) {
+    showDialog(
+      context: context,
+      builder: (context) => _ApiDetailDialog(request: request),
     );
-    _iconRotation =
-        Tween<double>(begin: 0.0, end: 0.5).animate(_expandAnimation);
   }
 
-  @override
-  void dispose() {
-    _animationController.dispose();
-    super.dispose();
+  Color _getStatusColor() {
+    if (request.error != null) return Colors.red;
+    if (request.statusCode == null) return Colors.orange;
+    if (request.statusCode! >= 200 && request.statusCode! < 300) {
+      return Colors.green;
+    }
+    return Colors.red;
+  }
+
+  Color _getMethodColor() {
+    switch (request.method.toUpperCase()) {
+      case 'GET':
+        return Colors.blue;
+      case 'POST':
+        return Colors.teal;
+      case 'PUT':
+        return Colors.orange;
+      case 'DELETE':
+        return Colors.red;
+      case 'PATCH':
+        return Colors.purple;
+      default:
+        return Colors.grey;
+    }
   }
+}
+
+/// API详情弹窗
+class _ApiDetailDialog extends StatelessWidget {
+  final ApiRequestInfo request;
+
+  const _ApiDetailDialog({required this.request});
 
   @override
   Widget build(BuildContext context) {
-    return Container(
-      margin: const EdgeInsets.symmetric(horizontal: 12, vertical: 4),
-      decoration: BoxDecoration(
-        borderRadius: BorderRadius.circular(12),
-        gradient: const LinearGradient(
-          begin: Alignment.topLeft,
-          end: Alignment.bottomRight,
-          colors: [
-            Colors.white,
-            Colors.blue,
-          ],
-        ),
-        border: Border.all(color: Colors.blue[200]!, width: 1),
-        boxShadow: [
-          BoxShadow(
-            color: Colors.blue[100]!.withValues(alpha: 0.3),
-            blurRadius: 8,
-            offset: const Offset(0, 3),
-          ),
-        ],
-      ),
-      child: Column(
-        children: [
-          // 主要内容区域
-          InkWell(
-            onTap: _toggleExpansion,
-            borderRadius: const BorderRadius.vertical(top: Radius.circular(12)),
-            child: Padding(
+    return Dialog(
+      backgroundColor: Colors.grey[50],
+      shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(16)),
+      insetPadding: const EdgeInsets.symmetric(horizontal: 16, vertical: 24),
+      child: Container(
+        width: Get.width * 0.92,
+        constraints: BoxConstraints(maxHeight: Get.height * 0.85),
+        child: Column(
+          mainAxisSize: MainAxisSize.min,
+          crossAxisAlignment: CrossAxisAlignment.start,
+          children: [
+            // 头部
+            Container(
               padding: const EdgeInsets.all(16),
+              decoration: BoxDecoration(
+                color: Colors.white,
+                borderRadius: const BorderRadius.vertical(
+                  top: Radius.circular(16),
+                ),
+                boxShadow: [
+                  BoxShadow(
+                    color: Colors.black.withValues(alpha: 0.05),
+                    blurRadius: 4,
+                    offset: const Offset(0, 2),
+                  ),
+                ],
+              ),
               child: Column(
                 crossAxisAlignment: CrossAxisAlignment.start,
                 children: [
+                  // 第一行:方法、状态码、关闭按钮
                   Row(
                     children: [
-                      // 状态指示器
-                      Container(
-                        padding: const EdgeInsets.all(6),
-                        decoration: BoxDecoration(
-                          gradient: LinearGradient(
-                            colors: widget.request.statusText == 'SUCCESS'
-                                ? [Colors.green[500]!, Colors.teal[600]!]
-                                : [Colors.red[500]!, Colors.pink[600]!],
-                          ),
-                          shape: BoxShape.circle,
-                          boxShadow: [
-                            BoxShadow(
-                              color:
-                                  widget.request.statusColor.withValues(alpha: 0.4),
-                              blurRadius: 4,
-                              offset: const Offset(0, 2),
-                            ),
-                          ],
-                        ),
-                        child: Icon(
-                          widget.request.statusText == 'SUCCESS'
-                              ? Icons.check
-                              : Icons.close,
-                          color: Colors.white,
-                          size: 14,
-                        ),
-                      ),
-                      const SizedBox(width: 12),
-                      // HTTP方法
+                      // 方法标签
                       Container(
                         padding: const EdgeInsets.symmetric(
-                            horizontal: 10, vertical: 4),
+                          horizontal: 10,
+                          vertical: 5,
+                        ),
                         decoration: BoxDecoration(
-                          gradient: LinearGradient(
-                            colors: [
-                              _getMethodColor(),
-                              _getMethodColor().withValues(alpha: 0.8)
-                            ],
+                          color: _getMethodColor().withValues(alpha: 0.1),
+                          borderRadius: BorderRadius.circular(6),
+                          border: Border.all(
+                            color: _getMethodColor().withValues(alpha: 0.3),
                           ),
-                          borderRadius: BorderRadius.circular(10),
-                          boxShadow: [
-                            BoxShadow(
-                              color: _getMethodColor().withValues(alpha: 0.3),
-                              blurRadius: 3,
-                              offset: const Offset(0, 1),
-                            ),
-                          ],
                         ),
                         child: Text(
-                          widget.request.method,
-                          style: const TextStyle(
-                            color: Colors.white,
-                            fontSize: 11,
-                            fontWeight: FontWeight.bold,
+                          request.method,
+                          style: TextStyle(
+                            fontSize: 13,
+                            fontWeight: FontWeight.w700,
+                            color: _getMethodColor(),
                           ),
                         ),
                       ),
-                      const SizedBox(width: 8),
-                      // 状态标签
-                      Container(
-                        padding: const EdgeInsets.symmetric(
-                            horizontal: 8, vertical: 3),
-                        decoration: BoxDecoration(
-                          gradient: LinearGradient(
-                            colors: widget.request.statusText == 'SUCCESS'
-                                ? [Colors.lightGreen[400]!, Colors.green[500]!]
-                                : [Colors.orange[400]!, Colors.red[500]!],
+                      const SizedBox(width: 10),
+                      // 状态码
+                      if (request.statusCode != null)
+                        Container(
+                          padding: const EdgeInsets.symmetric(
+                            horizontal: 8,
+                            vertical: 4,
+                          ),
+                          decoration: BoxDecoration(
+                            color: _getStatusColor().withValues(alpha: 0.1),
+                            borderRadius: BorderRadius.circular(6),
+                          ),
+                          child: Text(
+                            '${request.statusCode}',
+                            style: TextStyle(
+                              fontSize: 13,
+                              fontWeight: FontWeight.w600,
+                              color: _getStatusColor(),
+                            ),
                           ),
-                          borderRadius: BorderRadius.circular(8),
                         ),
-                        child: Text(
-                          widget.request.statusText,
-                          style: const TextStyle(
-                            color: Colors.white,
-                            fontSize: 10,
-                            fontWeight: FontWeight.bold,
+                      const SizedBox(width: 10),
+                      // 耗时
+                      if (request.duration != null)
+                        Container(
+                          padding: const EdgeInsets.symmetric(
+                            horizontal: 8,
+                            vertical: 4,
+                          ),
+                          decoration: BoxDecoration(
+                            color: Colors.grey[100],
+                            borderRadius: BorderRadius.circular(6),
+                          ),
+                          child: Text(
+                            '${request.duration!.inMilliseconds}ms',
+                            style: TextStyle(
+                              fontSize: 12,
+                              fontWeight: FontWeight.w500,
+                              color: Colors.grey[700],
+                            ),
                           ),
                         ),
-                      ),
                       const Spacer(),
-                      // 状态码和响应时间
-                      Row(
-                        mainAxisSize: MainAxisSize.min,
-                        children: [
-                          if (widget.request.statusCode != null)
-                            Container(
-                              padding: const EdgeInsets.symmetric(
-                                  horizontal: 6, vertical: 2),
-                              decoration: BoxDecoration(
-                                color: Colors.indigo[600],
-                                borderRadius: BorderRadius.circular(6),
-                              ),
-                              child: Text(
-                                '${widget.request.statusCode}',
-                                style: const TextStyle(
-                                  fontSize: 10,
-                                  color: Colors.white,
-                                  fontWeight: FontWeight.bold,
-                                ),
-                              ),
-                            ),
-                          if (widget.request.duration != null) ...[
-                            const SizedBox(width: 6),
-                            Container(
-                              padding: const EdgeInsets.symmetric(
-                                  horizontal: 6, vertical: 2),
-                              decoration: BoxDecoration(
-                                gradient: LinearGradient(
-                                  colors: [
-                                    Colors.amber[500]!,
-                                    Colors.orange[600]!
-                                  ],
-                                ),
-                                borderRadius: BorderRadius.circular(6),
-                              ),
-                              child: Text(
-                                '${widget.request.duration!.inMilliseconds}ms',
-                                style: const TextStyle(
-                                  fontSize: 10,
-                                  color: Colors.white,
-                                  fontWeight: FontWeight.bold,
-                                ),
-                              ),
-                            ),
-                          ],
-                          const SizedBox(width: 8),
-                          // 展开图标
-                          AnimatedBuilder(
-                            animation: _iconRotation,
-                            builder: (context, child) {
-                              return Transform.rotate(
-                                angle: _iconRotation.value * 3.14159,
-                                child: Container(
-                                  padding: const EdgeInsets.all(4),
-                                  decoration: BoxDecoration(
-                                    color: Colors.blue[100],
-                                    shape: BoxShape.circle,
-                                  ),
-                                  child: Icon(
-                                    Icons.keyboard_arrow_down,
-                                    color: Colors.blue[700],
-                                    size: 16,
-                                  ),
-                                ),
-                              );
-                            },
+                      // 关闭按钮
+                      InkWell(
+                        onTap: () => Navigator.pop(context),
+                        borderRadius: BorderRadius.circular(8),
+                        child: Container(
+                          padding: const EdgeInsets.all(6),
+                          decoration: BoxDecoration(
+                            color: Colors.grey[100],
+                            borderRadius: BorderRadius.circular(8),
+                          ),
+                          child: Icon(
+                            Icons.close,
+                            size: 18,
+                            color: Colors.grey[600],
                           ),
-                        ],
+                        ),
                       ),
                     ],
                   ),
-                  const SizedBox(height: 8),
-                  // URL显示
-                  Container(
-                    width: double.infinity,
-                    padding: const EdgeInsets.all(12),
-                    decoration: BoxDecoration(
-                      color: Colors.grey[100],
-                      borderRadius: BorderRadius.circular(8),
-                      border: Border.all(color: Colors.grey[300]!, width: 1),
-                    ),
-                    child: Text(
-                      widget.request.url,
-                      style: const TextStyle(
-                        fontSize: 12,
-                        color: Colors.black87,
-                        fontFamily: 'monospace',
-                        fontWeight: FontWeight.w500,
-                      ),
-                    ),
-                  ),
                 ],
               ),
             ),
-          ),
-          // 展开的详细内容
-          SizeTransition(
-            sizeFactor: _expandAnimation,
-            child: Container(
-              width: double.infinity,
-              decoration: BoxDecoration(
-                gradient: LinearGradient(
-                  begin: Alignment.topCenter,
-                  end: Alignment.bottomCenter,
-                  colors: [Colors.indigo[50]!, Colors.blue[50]!],
-                ),
-                borderRadius: const BorderRadius.only(
-                  bottomLeft: Radius.circular(12),
-                  bottomRight: Radius.circular(12),
-                ),
-              ),
-              child: Padding(
+            // 内容
+            Flexible(
+              child: SingleChildScrollView(
                 padding: const EdgeInsets.all(16),
                 child: Column(
                   crossAxisAlignment: CrossAxisAlignment.start,
                   children: [
-                    _buildDataSection('Headers', widget.request.headers),
-                    if (widget.request.requestData != null)
-                      _buildDataSection('Request', widget.request.requestData),
-                    if (widget.request.responseData != null)
-                      _buildDataSection(
-                          'Response', widget.request.responseData),
-                    if (widget.request.error != null)
-                      _buildDataSection('Error', widget.request.error),
+                    _buildSection('URL', request.url, icon: Icons.link),
+                    if (request.headers != null)
+                      _buildSection(
+                        'Headers',
+                        _formatJson(request.headers),
+                        icon: Icons.description_outlined,
+                      ),
+                    if (request.requestData != null)
+                      _buildSection(
+                        'Request',
+                        _formatJson(request.requestData),
+                        icon: Icons.upload_outlined,
+                        color: Colors.blue,
+                      ),
+                    if (request.responseData != null)
+                      _buildSection(
+                        'Response',
+                        _formatJson(request.responseData),
+                        icon: Icons.download_outlined,
+                        color: Colors.green,
+                      ),
+                    if (request.error != null)
+                      _buildSection(
+                        'Error',
+                        request.error!,
+                        icon: Icons.error_outline,
+                        isError: true,
+                      ),
                   ],
                 ),
               ),
             ),
-          ),
-        ],
+          ],
+        ),
       ),
     );
   }
 
-  Widget _buildDataSection(String title, dynamic data) {
-    final jsonString = _formatAsJson(data);
+  Widget _buildSection(
+    String title,
+    dynamic content, {
+    IconData? icon,
+    Color? color,
+    bool isError = false,
+  }) {
+    final text = content is String ? content : content.toString();
+    final sectionColor = isError ? Colors.red : (color ?? Colors.grey[700]!);
 
-    return Padding(
-      padding: const EdgeInsets.only(bottom: 16),
+    return Container(
+      margin: const EdgeInsets.only(bottom: 16),
+      decoration: BoxDecoration(
+        color: Colors.white,
+        borderRadius: BorderRadius.circular(12),
+        border: Border.all(
+          color: isError
+              ? Colors.red.withValues(alpha: 0.3)
+              : Colors.grey[200]!,
+        ),
+        boxShadow: [
+          BoxShadow(
+            color: Colors.black.withValues(alpha: 0.02),
+            blurRadius: 4,
+            offset: const Offset(0, 1),
+          ),
+        ],
+      ),
       child: Column(
         crossAxisAlignment: CrossAxisAlignment.start,
         children: [
-          Row(
-            children: [
-              Container(
-                padding:
-                    const EdgeInsets.symmetric(horizontal: 10, vertical: 4),
-                decoration: BoxDecoration(
-                  gradient: LinearGradient(
-                    colors: [Colors.deepPurple[500]!, Colors.purple[600]!],
-                  ),
-                  borderRadius: BorderRadius.circular(8),
-                  boxShadow: [
-                    BoxShadow(
-                      color: Colors.purple[200]!.withValues(alpha: 0.5),
-                      blurRadius: 3,
-                      offset: const Offset(0, 1),
-                    ),
-                  ],
-                ),
-                child: Text(
+          // 标题栏
+          Container(
+            padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 10),
+            decoration: BoxDecoration(
+              color: isError
+                  ? Colors.red.withValues(alpha: 0.05)
+                  : Colors.grey[50],
+              borderRadius: const BorderRadius.vertical(
+                top: Radius.circular(11),
+              ),
+            ),
+            child: Row(
+              children: [
+                if (icon != null) ...[
+                  Icon(icon, size: 16, color: sectionColor),
+                  const SizedBox(width: 6),
+                ],
+                Text(
                   title,
-                  style: const TextStyle(
-                    fontWeight: FontWeight.bold,
-                    color: Colors.white,
-                    fontSize: 12,
+                  style: TextStyle(
+                    fontSize: 13,
+                    fontWeight: FontWeight.w600,
+                    color: sectionColor,
                   ),
                 ),
-              ),
-              const Spacer(),
-              ElevatedButton.icon(
-                icon: const Icon(Icons.copy, size: 14),
-                label: const Text('复制'),
-                style: ElevatedButton.styleFrom(
-                  backgroundColor: Colors.teal[600],
-                  foregroundColor: Colors.white,
-                  elevation: 3,
-                  padding:
-                      const EdgeInsets.symmetric(horizontal: 10, vertical: 4),
-                  shape: RoundedRectangleBorder(
-                    borderRadius: BorderRadius.circular(8),
+                const Spacer(),
+                InkWell(
+                  onTap: () => _copyToClipboard(text, title),
+                  borderRadius: BorderRadius.circular(6),
+                  child: Container(
+                    padding: const EdgeInsets.symmetric(
+                      horizontal: 10,
+                      vertical: 5,
+                    ),
+                    decoration: BoxDecoration(
+                      color: Colors.grey[100],
+                      borderRadius: BorderRadius.circular(6),
+                    ),
+                    child: Row(
+                      mainAxisSize: MainAxisSize.min,
+                      children: [
+                        Icon(Icons.copy, size: 12, color: Colors.grey[600]),
+                        const SizedBox(width: 4),
+                        Text(
+                          '复制',
+                          style: TextStyle(
+                            fontSize: 11,
+                            fontWeight: FontWeight.w500,
+                            color: Colors.grey[600],
+                          ),
+                        ),
+                      ],
+                    ),
                   ),
                 ),
-                onPressed: () => _copyToClipboard(jsonString),
-              ),
-            ],
-          ),
-          const SizedBox(height: 8),
-          Container(
-            width: double.infinity,
-            padding: const EdgeInsets.all(16),
-            decoration: BoxDecoration(
-              color: Colors.grey[50],
-              borderRadius: BorderRadius.circular(8),
-              border: Border.all(color: Colors.blue[200]!, width: 1),
+              ],
             ),
-            child: SingleChildScrollView(
-              scrollDirection: Axis.horizontal,
-              child: SelectableText(
-                jsonString,
-                style: const TextStyle(
-                  fontFamily: 'monospace',
-                  fontSize: 12,
-                  color: Colors.black87,
-                  height: 1.4,
-                ),
+          ),
+          // 分隔线
+          Divider(height: 1, color: Colors.grey[200]),
+          // 内容区域 - 不限制高度
+          Padding(
+            padding: const EdgeInsets.all(14),
+            child: SelectableText(
+              text,
+              style: TextStyle(
+                fontFamily: 'monospace',
+                fontSize: 12,
+                color: isError ? Colors.red[700] : Colors.black87,
+                height: 1.5,
               ),
             ),
           ),
@@ -372,84 +448,64 @@ class _SimpleApiCardState extends State<SimpleApiCard>
     );
   }
 
-  void _toggleExpansion() {
-    setState(() {
-      _isExpanded = !_isExpanded;
-      if (_isExpanded) {
-        _animationController.forward();
-      } else {
-        _animationController.reverse();
-      }
-    });
-  }
-
-  String _formatAsJson(dynamic data) {
+  String _formatJson(dynamic data) {
     try {
       if (data == null) return 'null';
-
-      // 如果已经是字符串,检查是否是JSON格式
       if (data is String) {
         try {
           final decoded = jsonDecode(data);
           return const JsonEncoder.withIndent('  ').convert(decoded);
-        } catch (e) {
-          // 不是JSON格式,直接返回字符串
+        } catch (_) {
           return data;
         }
       }
-      // 对象转JSON,不限制大小
       return const JsonEncoder.withIndent('  ').convert(data);
     } catch (e) {
-      if (data.runtimeType.toString() == 'FormData') {
-        if (data.fields is List<MapEntry<String, String>>) {
-          final fields = data.fields as List<MapEntry<String, String>>;
-          final jsonMap = {
-            ...Map.fromEntries(fields.map((e) => MapEntry(e.key, e.value))),
-          };
-          return const JsonEncoder.withIndent('  ').convert(jsonMap);
-        }
-        return data.fields.toString();
-      }
-      // JSON序列化失败,返回字符串形式
-      return '${data.toString()}\n\n(JSON序列化失败: $e)';
+      return data.toString();
     }
   }
 
-  void _copyToClipboard(String text) {
+  void _copyToClipboard(String text, String type) {
     Clipboard.setData(ClipboardData(text: text));
     Get.snackbar(
-      '🎉 复制成功',
-      '内容已复制到剪贴板',
+      '已复制',
+      '$type 内容已复制到剪贴板',
       duration: const Duration(seconds: 1),
       snackPosition: SnackPosition.bottom,
-      backgroundColor: Colors.green[500],
+      backgroundColor: Colors.grey[800],
       colorText: Colors.white,
       borderRadius: 10,
       margin: const EdgeInsets.all(16),
-      boxShadows: [
-        BoxShadow(
-          color: Colors.green[200]!.withValues(alpha: 0.5),
-          blurRadius: 6,
-          offset: const Offset(0, 3),
-        ),
-      ],
+      icon: const Padding(
+        padding: EdgeInsets.only(left: 12),
+        child: Icon(Icons.check_circle, color: Colors.white, size: 20),
+      ),
     );
   }
 
+  Color _getStatusColor() {
+    if (request.error != null) return Colors.red;
+    if (request.statusCode == null) return Colors.orange;
+    if (request.statusCode! >= 200 && request.statusCode! < 300) {
+      return Colors.green;
+    }
+    return Colors.red;
+  }
+
   Color _getMethodColor() {
-    switch (widget.request.method.toUpperCase()) {
+    switch (request.method.toUpperCase()) {
       case 'GET':
-        return Colors.blue[600]!;
+        return Colors.blue;
       case 'POST':
-        return Colors.green[600]!;
+        return Colors.teal;
       case 'PUT':
-        return Colors.orange[600]!;
+        return Colors.orange;
       case 'DELETE':
-        return Colors.red[600]!;
+        return Colors.red;
       case 'PATCH':
-        return Colors.purple[600]!;
+        return Colors.purple;
       default:
-        return Colors.grey[600]!;
+        return Colors.grey;
     }
   }
 }

+ 278 - 251
lib/utils/developer/simple_log_card.dart

@@ -3,309 +3,336 @@ import 'package:flutter/services.dart';
 import 'package:get/get.dart';
 import 'ix_developer_tools.dart';
 
-/// 轻量级日志条目组件
-class SimpleLogCard extends StatefulWidget {
+/// 轻量级日志条目组件 - 纵向排列,点击弹窗显示完整内容
+class SimpleLogCard extends StatelessWidget {
   final ConsoleLogEntry log;
 
   const SimpleLogCard({super.key, required this.log});
 
   @override
-  State<SimpleLogCard> createState() => _SimpleLogCardState();
-}
+  Widget build(BuildContext context) {
+    return InkWell(
+      onTap: () => _showDetailDialog(context),
+      borderRadius: BorderRadius.circular(10),
+      child: Container(
+        margin: const EdgeInsets.symmetric(horizontal: 4, vertical: 4),
+        padding: const EdgeInsets.all(12),
+        decoration: BoxDecoration(
+          color: Colors.white,
+          borderRadius: BorderRadius.circular(10),
+          border: Border.all(color: Colors.grey[200]!, width: 1),
+          boxShadow: [
+            BoxShadow(
+              color: Colors.black.withValues(alpha: 0.03),
+              blurRadius: 4,
+              offset: const Offset(0, 1),
+            ),
+          ],
+        ),
+        child: Column(
+          crossAxisAlignment: CrossAxisAlignment.start,
+          children: [
+            // 第一行:时间、Tag
+            Row(
+              children: [
+                // 状态指示点
+                Container(
+                  width: 8,
+                  height: 8,
+                  decoration: BoxDecoration(
+                    color: _getTagColor(),
+                    shape: BoxShape.circle,
+                  ),
+                ),
+                const SizedBox(width: 8),
+                // Tag标签
+                Container(
+                  padding: const EdgeInsets.symmetric(
+                    horizontal: 8,
+                    vertical: 3,
+                  ),
+                  decoration: BoxDecoration(
+                    color: _getTagColor().withValues(alpha: 0.1),
+                    borderRadius: BorderRadius.circular(4),
+                  ),
+                  child: Text(
+                    log.tag,
+                    style: TextStyle(
+                      fontSize: 11,
+                      color: _getTagColor(),
+                      fontWeight: FontWeight.w600,
+                    ),
+                  ),
+                ),
+                const Spacer(),
+                // 时间
+                Text(
+                  log.formattedTime,
+                  style: TextStyle(
+                    fontFamily: 'monospace',
+                    fontSize: 10,
+                    color: Colors.grey[400],
+                  ),
+                ),
+              ],
+            ),
+            const SizedBox(height: 8),
+            // 第二行:日志内容(完整显示,最多3行)
+            Text(
+              log.message,
+              style: const TextStyle(
+                fontSize: 12,
+                color: Colors.black87,
+                height: 1.4,
+              ),
+              maxLines: 3,
+              overflow: TextOverflow.ellipsis,
+            ),
+          ],
+        ),
+      ),
+    );
+  }
 
-class _SimpleLogCardState extends State<SimpleLogCard>
-    with SingleTickerProviderStateMixin {
-  bool _isExpanded = false;
-  late AnimationController _animationController;
-  late Animation<double> _expandAnimation;
-  late Animation<double> _iconRotation;
+  Color _getTagColor() {
+    final tag = log.tag.toLowerCase();
+    if (tag.contains('error') || tag.contains('err')) return Colors.red;
+    if (tag.contains('warn')) return Colors.orange;
+    if (tag.contains('info')) return Colors.blue;
+    if (tag.contains('debug')) return Colors.purple;
+    if (tag.contains('api') || tag.contains('http')) return Colors.teal;
+    if (tag.contains('vpn') || tag.contains('xray')) return Colors.indigo;
+    return Colors.blueGrey;
+  }
 
-  @override
-  void initState() {
-    super.initState();
-    _animationController = AnimationController(
-      duration: const Duration(milliseconds: 250),
-      vsync: this,
-    );
-    _expandAnimation = CurvedAnimation(
-      parent: _animationController,
-      curve: Curves.easeInOut,
+  void _showDetailDialog(BuildContext context) {
+    showDialog(
+      context: context,
+      builder: (context) => _LogDetailDialog(log: log),
     );
-    _iconRotation =
-        Tween<double>(begin: 0.0, end: 0.5).animate(_expandAnimation);
   }
+}
 
-  @override
-  void dispose() {
-    _animationController.dispose();
-    super.dispose();
-  }
+/// 日志详情弹窗 - 纵向排列
+class _LogDetailDialog extends StatelessWidget {
+  final ConsoleLogEntry log;
+
+  const _LogDetailDialog({required this.log});
 
   @override
   Widget build(BuildContext context) {
-    final fullMessage = '${widget.log.tag}: ${widget.log.message}';
-
-    return Container(
-      margin: const EdgeInsets.symmetric(horizontal: 12, vertical: 3),
-      decoration: BoxDecoration(
-        borderRadius: BorderRadius.circular(12),
-        gradient: const LinearGradient(
-          begin: Alignment.topLeft,
-          end: Alignment.bottomRight,
-          colors: [
-            Colors.white,
-            Colors.blue,
-          ],
-        ),
-        border: Border.all(color: Colors.blue[200]!, width: 1),
-        boxShadow: [
-          BoxShadow(
-            color: Colors.blue[100]!.withValues(alpha: 0.3),
-            blurRadius: 6,
-            offset: const Offset(0, 2),
-          ),
-        ],
-      ),
-      child: Column(
-        children: [
-          // 主要内容区域
-          InkWell(
-            onTap: _toggleExpansion,
-            borderRadius: const BorderRadius.vertical(top: Radius.circular(12)),
-            child: Padding(
+    return Dialog(
+      backgroundColor: Colors.grey[50],
+      shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(16)),
+      insetPadding: const EdgeInsets.symmetric(horizontal: 16, vertical: 24),
+      child: Container(
+        width: Get.width * 0.92,
+        constraints: BoxConstraints(maxHeight: Get.height * 0.75),
+        child: Column(
+          mainAxisSize: MainAxisSize.min,
+          crossAxisAlignment: CrossAxisAlignment.start,
+          children: [
+            // 头部
+            Container(
               padding: const EdgeInsets.all(16),
-              child: Row(
+              decoration: BoxDecoration(
+                color: Colors.white,
+                borderRadius: const BorderRadius.vertical(
+                  top: Radius.circular(16),
+                ),
+                boxShadow: [
+                  BoxShadow(
+                    color: Colors.black.withValues(alpha: 0.05),
+                    blurRadius: 4,
+                    offset: const Offset(0, 2),
+                  ),
+                ],
+              ),
+              child: Column(
+                crossAxisAlignment: CrossAxisAlignment.start,
                 children: [
-                  // 状态指示器
-                  Container(
-                    width: 10,
-                    height: 10,
-                    decoration: BoxDecoration(
-                      gradient: LinearGradient(
-                        colors: [Colors.cyan[400]!, Colors.teal[500]!],
-                      ),
-                      shape: BoxShape.circle,
-                      boxShadow: [
-                        BoxShadow(
-                          color: Colors.cyan[300]!.withValues(alpha: 0.5),
-                          blurRadius: 4,
-                          offset: const Offset(0, 1),
+                  // 第一行:Tag、时间、操作按钮
+                  Row(
+                    children: [
+                      // Tag
+                      Container(
+                        padding: const EdgeInsets.symmetric(
+                          horizontal: 10,
+                          vertical: 5,
                         ),
-                      ],
-                    ),
-                  ),
-                  const SizedBox(width: 12),
-                  // 时间标签
-                  Container(
-                    padding:
-                        const EdgeInsets.symmetric(horizontal: 10, vertical: 4),
-                    decoration: BoxDecoration(
-                      gradient: LinearGradient(
-                        colors: [Colors.purple[500]!, Colors.pink[500]!],
-                      ),
-                      borderRadius: BorderRadius.circular(10),
-                      boxShadow: [
-                        BoxShadow(
-                          color: Colors.purple[200]!.withValues(alpha: 0.5),
-                          blurRadius: 3,
-                          offset: const Offset(0, 1),
+                        decoration: BoxDecoration(
+                          color: _getTagColor().withValues(alpha: 0.1),
+                          borderRadius: BorderRadius.circular(6),
+                          border: Border.all(
+                            color: _getTagColor().withValues(alpha: 0.3),
+                          ),
+                        ),
+                        child: Text(
+                          log.tag,
+                          style: TextStyle(
+                            fontSize: 13,
+                            fontWeight: FontWeight.w600,
+                            color: _getTagColor(),
+                          ),
                         ),
-                      ],
-                    ),
-                    child: Text(
-                      widget.log.formattedTime,
-                      style: const TextStyle(
-                        fontFamily: 'monospace',
-                        fontSize: 10,
-                        color: Colors.white,
-                        fontWeight: FontWeight.bold,
                       ),
-                    ),
-                  ),
-                  const SizedBox(width: 12),
-                  // 日志内容
-                  Expanded(
-                    child: Text(
-                      fullMessage,
-                      style: const TextStyle(
-                        fontFamily: 'monospace',
-                        fontSize: 13,
-                        color: Colors.black87,
-                        fontWeight: FontWeight.w500,
+                      const SizedBox(width: 10),
+                      // 时间
+                      Container(
+                        padding: const EdgeInsets.symmetric(
+                          horizontal: 8,
+                          vertical: 4,
+                        ),
+                        decoration: BoxDecoration(
+                          color: Colors.grey[100],
+                          borderRadius: BorderRadius.circular(6),
+                        ),
+                        child: Text(
+                          log.formattedTime,
+                          style: TextStyle(
+                            fontSize: 12,
+                            color: Colors.grey[700],
+                            fontFamily: 'monospace',
+                            fontWeight: FontWeight.w500,
+                          ),
+                        ),
                       ),
-                      maxLines: 2,
-                      overflow: TextOverflow.ellipsis,
-                    ),
-                  ),
-                  // 展开/收起图标
-                  AnimatedBuilder(
-                    animation: _iconRotation,
-                    builder: (context, child) {
-                      return Transform.rotate(
-                        angle: _iconRotation.value * 3.14159,
+                      const Spacer(),
+                      // 复制按钮
+                      InkWell(
+                        onTap: () =>
+                            _copyToClipboard('${log.tag}: ${log.message}'),
+                        borderRadius: BorderRadius.circular(8),
                         child: Container(
-                          padding: const EdgeInsets.all(4),
-                          decoration: BoxDecoration(
-                            color: Colors.blue[100],
-                            shape: BoxShape.circle,
+                          padding: const EdgeInsets.symmetric(
+                            horizontal: 10,
+                            vertical: 6,
                           ),
-                          child: Icon(
-                            Icons.keyboard_arrow_down,
-                            color: Colors.blue[700],
-                            size: 16,
+                          decoration: BoxDecoration(
+                            color: Colors.grey[100],
+                            borderRadius: BorderRadius.circular(8),
                           ),
-                        ),
-                      );
-                    },
-                  ),
-                ],
-              ),
-            ),
-          ),
-          // 展开的详细内容
-          SizeTransition(
-            sizeFactor: _expandAnimation,
-            child: Container(
-              width: double.infinity,
-              decoration: BoxDecoration(
-                gradient: LinearGradient(
-                  begin: Alignment.topCenter,
-                  end: Alignment.bottomCenter,
-                  colors: [Colors.blue[50]!, Colors.indigo[50]!],
-                ),
-                borderRadius: const BorderRadius.only(
-                  bottomLeft: Radius.circular(12),
-                  bottomRight: Radius.circular(12),
-                ),
-              ),
-              child: Column(
-                children: [
-                  // 分割线
-                  Container(
-                    height: 2,
-                    margin: const EdgeInsets.symmetric(horizontal: 16),
-                    decoration: BoxDecoration(
-                      gradient: LinearGradient(
-                        colors: [Colors.blue[300]!, Colors.purple[300]!],
-                      ),
-                      borderRadius: BorderRadius.circular(1),
-                    ),
-                  ),
-                  // 详细内容
-                  Padding(
-                    padding: const EdgeInsets.all(16),
-                    child: Column(
-                      crossAxisAlignment: CrossAxisAlignment.start,
-                      children: [
-                        Row(
-                          children: [
-                            Container(
-                              padding: const EdgeInsets.symmetric(
-                                  horizontal: 10, vertical: 4),
-                              decoration: BoxDecoration(
-                                gradient: LinearGradient(
-                                  colors: [
-                                    Colors.orange[500]!,
-                                    Colors.deepOrange[600]!
-                                  ],
-                                ),
-                                borderRadius: BorderRadius.circular(8),
-                                boxShadow: [
-                                  BoxShadow(
-                                    color: Colors.orange[200]!.withValues(alpha: 0.5),
-                                    blurRadius: 3,
-                                    offset: const Offset(0, 1),
-                                  ),
-                                ],
+                          child: Row(
+                            mainAxisSize: MainAxisSize.min,
+                            children: [
+                              Icon(
+                                Icons.copy,
+                                size: 14,
+                                color: Colors.grey[600],
                               ),
-                              child: const Text(
-                                '完整内容',
+                              const SizedBox(width: 4),
+                              Text(
+                                '复制',
                                 style: TextStyle(
-                                  fontWeight: FontWeight.bold,
-                                  color: Colors.white,
-                                  fontSize: 11,
-                                ),
-                              ),
-                            ),
-                            const Spacer(),
-                            ElevatedButton.icon(
-                              icon: const Icon(Icons.copy, size: 14),
-                              label: const Text('复制'),
-                              style: ElevatedButton.styleFrom(
-                                backgroundColor: Colors.teal[600],
-                                foregroundColor: Colors.white,
-                                elevation: 3,
-                                padding: const EdgeInsets.symmetric(
-                                    horizontal: 12, vertical: 6),
-                                shape: RoundedRectangleBorder(
-                                  borderRadius: BorderRadius.circular(10),
+                                  fontSize: 12,
+                                  fontWeight: FontWeight.w500,
+                                  color: Colors.grey[600],
                                 ),
                               ),
-                              onPressed: () => _copyToClipboard(fullMessage),
-                            ),
-                          ],
+                            ],
+                          ),
                         ),
-                        const SizedBox(height: 12),
-                        Container(
-                          width: double.infinity,
-                          padding: const EdgeInsets.all(16),
+                      ),
+                      const SizedBox(width: 8),
+                      // 关闭按钮
+                      InkWell(
+                        onTap: () => Navigator.pop(context),
+                        borderRadius: BorderRadius.circular(8),
+                        child: Container(
+                          padding: const EdgeInsets.all(6),
                           decoration: BoxDecoration(
-                            color: Colors.grey[50],
+                            color: Colors.grey[100],
                             borderRadius: BorderRadius.circular(8),
-                            border:
-                                Border.all(color: Colors.blue[200]!, width: 1),
                           ),
-                          child: SelectableText(
-                            fullMessage,
-                            style: const TextStyle(
-                              fontFamily: 'monospace',
-                              fontSize: 13,
-                              color: Colors.black87,
-                              height: 1.4,
-                            ),
+                          child: Icon(
+                            Icons.close,
+                            size: 18,
+                            color: Colors.grey[600],
                           ),
                         ),
-                      ],
+                      ),
+                    ],
+                  ),
+                ],
+              ),
+            ),
+            // 内容标题
+            Padding(
+              padding: const EdgeInsets.fromLTRB(16, 16, 16, 8),
+              child: Row(
+                children: [
+                  Icon(
+                    Icons.article_outlined,
+                    size: 16,
+                    color: Colors.grey[600],
+                  ),
+                  const SizedBox(width: 6),
+                  Text(
+                    '日志内容',
+                    style: TextStyle(
+                      fontSize: 13,
+                      fontWeight: FontWeight.w600,
+                      color: Colors.grey[700],
                     ),
                   ),
                 ],
               ),
             ),
-          ),
-        ],
+            // 内容
+            Flexible(
+              child: Container(
+                margin: const EdgeInsets.fromLTRB(16, 0, 16, 16),
+                decoration: BoxDecoration(
+                  color: Colors.white,
+                  borderRadius: BorderRadius.circular(12),
+                  border: Border.all(color: Colors.grey[200]!),
+                ),
+                child: SingleChildScrollView(
+                  padding: const EdgeInsets.all(16),
+                  child: SelectableText(
+                    log.message,
+                    style: const TextStyle(
+                      fontFamily: 'monospace',
+                      fontSize: 13,
+                      color: Colors.black87,
+                      height: 1.6,
+                    ),
+                  ),
+                ),
+              ),
+            ),
+          ],
+        ),
       ),
     );
   }
 
-  void _toggleExpansion() {
-    setState(() {
-      _isExpanded = !_isExpanded;
-      if (_isExpanded) {
-        _animationController.forward();
-      } else {
-        _animationController.reverse();
-      }
-    });
+  Color _getTagColor() {
+    final tag = log.tag.toLowerCase();
+    if (tag.contains('error') || tag.contains('err')) return Colors.red;
+    if (tag.contains('warn')) return Colors.orange;
+    if (tag.contains('info')) return Colors.blue;
+    if (tag.contains('debug')) return Colors.purple;
+    if (tag.contains('api') || tag.contains('http')) return Colors.teal;
+    if (tag.contains('vpn') || tag.contains('xray')) return Colors.indigo;
+    return Colors.blueGrey;
   }
 
   void _copyToClipboard(String text) {
     Clipboard.setData(ClipboardData(text: text));
     Get.snackbar(
-      '🎉 复制成功',
+      '已复制',
       '日志内容已复制到剪贴板',
       duration: const Duration(seconds: 1),
       snackPosition: SnackPosition.bottom,
-      backgroundColor: Colors.green[500],
+      backgroundColor: Colors.grey[800],
       colorText: Colors.white,
       borderRadius: 10,
       margin: const EdgeInsets.all(16),
-      boxShadows: [
-        BoxShadow(
-          color: Colors.green[200]!.withValues(alpha: 0.5),
-          blurRadius: 6,
-          offset: const Offset(0, 3),
-        ),
-      ],
+      icon: const Padding(
+        padding: EdgeInsets.only(left: 12),
+        child: Icon(Icons.check_circle, color: Colors.white, size: 20),
+      ),
     );
   }
 }

+ 28 - 24
lib/utils/ntp_time_service.dart

@@ -51,8 +51,15 @@ class NtpTimeService {
   Duration? _elapsedRealtimeInitial;
   String? _currentServer;
 
+  DateTime? _launchInitialTime;
+  Duration? _launchElapsedRealtimeInitial;
+
   bool get isInitialized =>
       _ntpInitialTime != null && _elapsedRealtimeInitial != null;
+
+  bool get isLaunchInitialized =>
+      _launchInitialTime != null && _launchElapsedRealtimeInitial != null;
+
   String? get currentServer => _currentServer;
 
   /// 初始化:从多个NTP服务器获取时间,并记录系统启动时间
@@ -89,7 +96,6 @@ class NtpTimeService {
           continue;
         }
       }
-
       log(TAG, 'All NTP server connection failed, using system time');
       return false;
     } catch (e) {
@@ -98,18 +104,31 @@ class NtpTimeService {
     }
   }
 
+  void initLaunchInitialTime() {
+    final appConfig = IXSP.getAppConfig();
+    if (appConfig != null && appConfig.serverTime != null) {
+      _launchInitialTime = DateTime.fromMillisecondsSinceEpoch(
+        appConfig.serverTime!,
+      );
+      _launchElapsedRealtimeInitial = SystemClock.elapsedRealtime();
+    }
+  }
+
   /// 获取当前"网络时间",通过系统运行时间 + NTP 差值计算
   DateTime getCurrentTime() {
-    if (!isInitialized) {
-      final appConfig = IXSP.getAppConfig();
-      if (appConfig != null && appConfig.serverTime != null) {
-        return DateTime.fromMillisecondsSinceEpoch(appConfig.serverTime!);
+    if (isInitialized) {
+      final currentElapsedRealtime = SystemClock.elapsedRealtime();
+      final elapsed = currentElapsedRealtime - _elapsedRealtimeInitial!;
+      return _ntpInitialTime!.add(elapsed);
+    } else {
+      if (isLaunchInitialized) {
+        final currentElapsedRealtime = SystemClock.elapsedRealtime();
+        final elapsed = currentElapsedRealtime - _launchElapsedRealtimeInitial!;
+        return _launchInitialTime!.add(elapsed);
+      } else {
+        return DateTime.now();
       }
-      return DateTime.now();
     }
-    final currentElapsedRealtime = SystemClock.elapsedRealtime();
-    final elapsed = currentElapsedRealtime - _elapsedRealtimeInitial!;
-    return _ntpInitialTime!.add(elapsed);
   }
 
   /// 获取当前时间戳(毫秒)
@@ -118,21 +137,6 @@ class NtpTimeService {
     return currentTime.millisecondsSinceEpoch;
   }
 
-  int getNtpInitialTime() {
-    if (!isInitialized) {
-      return getCurrentTimestamp();
-    }
-    return _ntpInitialTime?.millisecondsSinceEpoch ?? getCurrentTimestamp();
-  }
-
-  int getElapsedRealtimeInitial() {
-    if (!isInitialized) {
-      return SystemClock.elapsedRealtime().inMilliseconds;
-    }
-    return _elapsedRealtimeInitial?.inMilliseconds ??
-        SystemClock.elapsedRealtime().inMilliseconds;
-  }
-
   /// 同步一次 NTP,重新校准
   Future<bool> resync() async {
     try {

+ 12 - 1
pigeons/core_api.dart

@@ -18,6 +18,18 @@ abstract class CoreApi {
     int socksPort,
     String tunnelConfig,
     String configJson,
+    int remainTime,
+    bool isCountdown,
+    List<String> allowVpnApps,
+    List<String> disallowVpnApps,
+    String accessToken,
+    String aesKey,
+    String aesIv,
+    int locationId,
+    String locationCode,
+    List<String> baseUrls,
+    String params,
+    int peekTimeInterval,
   );
   bool? disconnect();
   String? getRemoteIp();
@@ -25,7 +37,6 @@ abstract class CoreApi {
   bool? moveTaskToBack();
   bool? isConnected();
   String? getSimInfo();
-  bool? reconnect();
   String? getChannel();
 }
 

+ 1 - 0
pubspec.yaml

@@ -85,6 +85,7 @@ dependencies:
   pull_to_refresh_flutter3: ^2.0.2 # 下拉刷新
   awesome_notifications: ^0.10.1 # 通知
   flutter_app_group_directory: ^1.1.0 # App Group目录
+  # hive_ce: ^2.17.0 # 本地缓存
 
 dev_dependencies:
   flutter_test: