Jelajahi Sumber

Merge branch 'release/2.4.0'

cc12458 1 Minggu lalu
induk
melakukan
115ec1074e
35 mengubah file dengan 760 tambahan dan 120 penghapusan
  1. 6 0
      .gitmodules
  2. 2 1
      app/build.gradle.kts
  3. 6 0
      app/src/aio-CD-510100/res/values/application.xml
  4. 5 0
      app/src/aio-CD-510100/res/values/browser_proxy.xml
  5. 5 0
      app/src/aio-CD-510100/res/values/pulse.xml
  6. 6 0
      app/src/aio-ZJ-330681/res/values/application.xml
  7. 12 0
      app/src/aio-ZJ-330681/res/values/browser_proxy.xml
  8. 8 0
      app/src/aio-ZJ-330681/res/values/pulse.xml
  9. 1 1
      app/src/aio-prod/res/values/application.xml
  10. 1 8
      app/src/aio-prod/res/values/browser_proxy.xml
  11. 1 1
      app/src/aio-test/res/values/application.xml
  12. 6 6
      app/src/aio-test/res/values/browser_proxy.xml
  13. 0 64
      app/src/aio/assets/browser/bridge.js
  14. 1 3
      app/src/aio/res/values/application.xml
  15. 36 0
      app/src/main/AndroidManifest.xml
  16. 36 0
      app/src/main/java/com/hzliuzhi/applet/container/MainActivity.kt
  17. 6 4
      app/src/main/java/com/hzliuzhi/applet/container/navigation/Host.kt
  18. 25 2
      app/src/main/java/com/hzliuzhi/applet/container/navigation/Route.kt
  19. 20 8
      app/src/main/java/com/hzliuzhi/applet/container/navigation/RouteExtra.kt
  20. 246 0
      app/src/main/java/com/hzliuzhi/applet/container/ui/LauncherScreen.kt
  21. 0 1
      app/src/pda/res/values/application.xml
  22. 2 0
      core/build.gradle.kts
  23. 5 0
      core/src/main/java/com/hzliuzhi/applet/core/router/DeepLink.kt
  24. 194 0
      core/src/main/java/com/hzliuzhi/applet/core/router/route.kt
  25. 50 3
      core/src/main/java/com/hzliuzhi/applet/core/shared/SharedFlowHub.kt
  26. 32 0
      core/src/main/java/com/hzliuzhi/applet/core/store/SettingStore.kt
  27. 3 0
      gradle.properties
  28. 3 0
      gradle/libs.versions.toml
  29. 2 2
      library/browser/src/main/assets/browser/bridge.js
  30. 5 8
      library/browser/src/main/java/com/hzliuzhi/applet/browser/navigation/Route.kt
  31. 14 0
      library/device/pulse/src/main/java/com/hzliuzhi/applet/device/pulse/PulseEventHandler.kt
  32. 11 6
      library/device/pulse/src/main/java/com/hzliuzhi/applet/device/pulse/util/TaiYiUtil.kt
  33. 8 2
      library/device/scanner/src/main/java/com/hzliuzhi/applet/scanner/Scanner.kt
  34. 1 0
      module/taiyi-pulse
  35. 1 0
      module/webview-upgrade

+ 6 - 0
.gitmodules

@@ -0,0 +1,6 @@
+[submodule "module/webview-upgrade"]
+	path = module/webview-upgrade
+	url = ssh://git@121.43.162.141:10022/six.android/webview-upgrade.git
+[submodule "module/taiyi-pulse"]
+	path = module/taiyi-pulse
+	url = ssh://git@121.43.162.141:10022/six.android/taiyi-pulse.git

+ 2 - 1
app/build.gradle.kts

@@ -54,7 +54,7 @@ android {
     minSdk = 26
     targetSdk = 35
     versionCode = getVersionCode()
-    versionName = "2.2.1"
+    versionName = "2.4.0"
 
     testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
   }
@@ -134,6 +134,7 @@ dependencies {
   implementation(libs.androidx.ui.graphics)
   implementation(libs.androidx.ui.tooling.preview)
   implementation(libs.androidx.material3)
+  implementation(libs.androidx.material.icons.extended)
   testImplementation(libs.junit)
   androidTestImplementation(libs.androidx.junit)
   androidTestImplementation(libs.androidx.espresso.core)

+ 6 - 0
app/src/aio-CD-510100/res/values/application.xml

@@ -0,0 +1,6 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+  <string name="app_id">AIO</string>
+  <string name="app_name">中医健康辨识仪(区域版.CD)</string>
+  <string name="app_screen">browser/kiosk?url=https://xingxinsaas.com/aio/</string>
+</resources>

+ 5 - 0
app/src/aio-CD-510100/res/values/browser_proxy.xml

@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+  <bool name="browser_http_proxy_enabled">true</bool>
+  <string-array name="browser_proxy_pool" />
+</resources>

+ 5 - 0
app/src/aio-CD-510100/res/values/pulse.xml

@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+  <bool name="taiyi_skip_report">true</bool>
+  <integer name="taiyi_delay_disconnect">300</integer>
+</resources>

+ 6 - 0
app/src/aio-ZJ-330681/res/values/application.xml

@@ -0,0 +1,6 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+  <string name="app_id">AIO</string>
+  <string name="app_name">中医健康辨识仪(区域版.330681)</string>
+  <string name="app_screen">browser/kiosk?url=http://10.175.77.35:9048/aio/screen</string>
+</resources>

+ 12 - 0
app/src/aio-ZJ-330681/res/values/browser_proxy.xml

@@ -0,0 +1,12 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+  <bool name="browser_http_proxy_enabled">true</bool>
+  <string-array name="browser_proxy_pool">
+    <item>https://hybrid.reborn-tech.com -> http://10.175.77.35:9048/mz/hybrid</item>
+    <item>https://api.reborn-tech.com -> http://10.175.77.35:9048/mz/api</item>
+    <item>https://taiyi.oss-accelerate.aliyuncs.com -> http://10.175.77.35:9048/mz/oss</item>
+    <item>https://taiyi.oss-cn-beijing.aliyuncs.com -> http://10.175.77.35:9048/mz/oss</item>
+    <item>https://oss-accelerate.aliyuncs.com/taiyi -> http://10.175.77.35:9048/mz/oss</item>
+    <item>https://oss-cn-beijing.aliyuncs.com/taiyi -> http://10.175.77.35:9048/mz/oss</item>
+  </string-array>
+</resources>

+ 8 - 0
app/src/aio-ZJ-330681/res/values/pulse.xml

@@ -0,0 +1,8 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+  <bool name="taiyi_skip_report">true</bool>
+  <integer name="taiyi_delay_disconnect">300</integer>
+  <string name="taiyi_oss_agency_url">http://10.175.77.35:9048/liuzhi/manager2/fdhb-tablet/analysisManage/upload</string>
+  <string name="taiyi_oss_agency_method">POST</string>
+  <!-- <bool name="taiyi_oss_agency_header2query">true</bool> -->
+</resources>

+ 1 - 1
app/src/aio-prod/res/values/application.xml

@@ -2,5 +2,5 @@
 <resources>
   <string name="app_id">AIO</string>
   <string name="app_name">中医健康辨识仪</string>
-  <string name="app_screen">browser/kiosk?url=https://wx.hzliuzhi.com/aio/</string>
+  <string name="app_screen">browser/kiosk?url=https://wx2.hzliuzhi.com/aio/</string>
 </resources>

+ 1 - 8
app/src/aio-prod/res/values/browser_proxy.xml

@@ -1,12 +1,5 @@
 <?xml version="1.0" encoding="utf-8"?>
 <resources>
   <bool name="browser_http_proxy_enabled">true</bool>
-  <string-array name="browser_proxy_pool">
-    <item>https://hybrid.reborn-tech.com -> https://wx.hzliuzhi.com/mz/hybrid</item>
-    <item>https://api.reborn-tech.com -> https://wx.hzliuzhi.com/mz/api</item>
-    <item>https://taiyi.oss-accelerate.aliyuncs.com -> https://wx.hzliuzhi.com/mz/oss</item>
-    <item>https://taiyi.oss-cn-beijing.aliyuncs.com -> https://wx.hzliuzhi.com/mz/oss</item>
-    <item>https://oss-accelerate.aliyuncs.com/taiyi -> https://wx.hzliuzhi.com/mz/oss</item>
-    <item>https://oss-cn-beijing.aliyuncs.com/taiyi -> https://wx.hzliuzhi.com/mz/oss</item>
-  </string-array>
+  <string-array name="browser_proxy_pool" />
 </resources>

+ 1 - 1
app/src/aio-test/res/values/application.xml

@@ -2,5 +2,5 @@
 <resources>
   <string name="app_id">AIO</string>
   <string name="app_name">中医健康辨识仪(测试版)</string>
-  <string name="app_screen">browser/kiosk?url=https://wx.hzliuzhi.com:4433/aio/</string>
+  <string name="app_screen">browser/kiosk?url=https://test.hzliuzhi.com/aio/</string>
 </resources>

+ 6 - 6
app/src/aio-test/res/values/browser_proxy.xml

@@ -2,11 +2,11 @@
 <resources>
   <bool name="browser_http_proxy_enabled">true</bool>
   <string-array name="browser_proxy_pool">
-    <item>https://hybrid.reborn-tech.com -> http://115.236.184.102:62007/mz/hybrid</item>
-    <item>https://api.reborn-tech.com -> http://115.236.184.102:62007/mz/api</item>
-    <item>https://taiyi.oss-accelerate.aliyuncs.com -> http://115.236.184.102:62007/mz/oss</item>
-    <item>https://taiyi.oss-cn-beijing.aliyuncs.com -> http://115.236.184.102:62007/mz/oss</item>
-    <item>https://oss-accelerate.aliyuncs.com/taiyi -> http://115.236.184.102:62007/mz/oss</item>
-    <item>https://oss-cn-beijing.aliyuncs.com/taiyi -> http://115.236.184.102:62007/mz/oss</item>
+    <item>https://hybrid.reborn-tech.com -> https://wx.hzliuzhi.com/mz/hybrid</item>
+    <item>https://api.reborn-tech.com -> https://wx.hzliuzhi.com/mz/api</item>
+    <item>https://taiyi.oss-accelerate.aliyuncs.com -> https://wx.hzliuzhi.com/mz/oss</item>
+    <item>https://taiyi.oss-cn-beijing.aliyuncs.com -> https://wx.hzliuzhi.com/mz/oss</item>
+    <item>https://oss-accelerate.aliyuncs.com/taiyi -> https://wx.hzliuzhi.com/mz/oss</item>
+    <item>https://oss-cn-beijing.aliyuncs.com/taiyi -> https://wx.hzliuzhi.com/mz/oss</item>
   </string-array>
 </resources>

+ 0 - 64
app/src/aio/assets/browser/bridge.js

@@ -1,64 +0,0 @@
-class Bridge extends EventTarget {
-  #pool = new Map();
-
-  static getInstance() {
-    return Bridge._instance ?? (Bridge._instance = new Bridge());
-  }
-
-  static get Platform() {
-    return window['AndroidBridge'];
-  }
-
-  static get UUID() {
-    return crypto.randomUUID();
-  }
-
-  static pulse(userId) {
-    const { promise, ...resolvers } = Promise.withResolvers();
-    this.getInstance().#postMessage('pulse', { userId }, resolvers);
-    return promise;
-  }
-
-  static print(payload) {
-    const { promise, ...resolvers } = Promise.withResolvers();
-    this.getInstance().#postMessage('print', payload, resolvers);
-    return promise;
-  }
-
-  dispatch(message) {
-    console.log('log:bridge:js', `[dispatch] 接收消息: ${message}`);
-    try {
-      const { type, callbackId, payload }  = JSON.parse(message);
-      if (callbackId) {
-        const { resolve, reject } = this.#pool.get(callbackId) ?? {};
-        this.#pool.delete(callbackId);
-        if (payload.code === 0) resolve(payload.data)
-        else reject(payload.message)
-      } else {
-        event = new CustomEvent(type, { detail: payload });
-        super.dispatchEvent(event);
-      }
-    } catch (e) {
-      console.log('log:bridge:js', `[analysis] 解析消息错误: ${e.message}`);
-      throw e;
-    }
-  }
-
-  addEventListener(type, callback, options) {
-    super.addEventListener(type, callback, options);
-    return () => super.removeEventListener(type, callback);
-  }
-
-  #postMessage(type, payload, resolvers) {
-    const callbackId = `${type}:${Bridge.UUID}`;
-    this.#pool.set(callbackId, resolvers);
-    const message = JSON.stringify({ type, payload, callbackId })
-    Bridge.Platform.postMessage(message);
-    console.log('log:bridge:js', `[dispatch] 发送消息: ${message}`);
-  }
-}
-
-window['Bridge'] = Bridge;
-window['bridge'] = Bridge.getInstance();
-
-window.print = Bridge.print.bind(Bridge);

+ 1 - 3
app/src/aio/res/values/application.xml

@@ -2,7 +2,5 @@
 <resources>
   <string name="app_id">AIO</string>
   <string name="app_name">中医健康辨识仪(开发版)</string>
-  <string name="app_screen">browser/kiosk?url=https://wx.hzliuzhi.com:4433/aio/?sn=45dde49f100eb0cb</string>
-  <!-- <string name="app_screen">browser/kiosk?url=http://192.168.1.14:4173/aio/?sn=45dde49f100eb0cb</string> -->
-  <!-- <string name="app_screen">browser/kiosk?url=http://192.168.31.184:4173/aio/?sn=45dde49f100eb0cb</string> -->
+  <string name="app_screen">browser/kiosk?url=https://wx.hzliuzhi.com:4433/aio/</string>
 </resources>

+ 36 - 0
app/src/main/AndroidManifest.xml

@@ -21,6 +21,42 @@
 
         <category android:name="android.intent.category.LAUNCHER" />
       </intent-filter>
+
+      <intent-filter>
+        <action android:name="android.intent.action.VIEW" />
+
+        <category android:name="android.intent.category.DEFAULT" />
+        <category android:name="android.intent.category.BROWSABLE" />
+
+        <data
+            android:host="launcher"
+            android:scheme="six-applet" />
+      </intent-filter>
+
+      <intent-filter>
+        <action android:name="android.intent.action.VIEW" />
+
+        <category android:name="android.intent.category.DEFAULT" />
+        <category android:name="android.intent.category.BROWSABLE" />
+
+        <data
+            android:host="browser"
+            android:path="/kiosk"
+            android:scheme="six-applet" />
+      </intent-filter>
+
+      <intent-filter>
+        <action android:name="android.intent.action.VIEW" />
+
+        <category android:name="android.intent.category.DEFAULT" />
+        <category android:name="android.intent.category.BROWSABLE" />
+
+        <data
+            android:host="browser"
+            android:path="/web"
+            android:scheme="six-applet" />
+      </intent-filter>
+
     </activity>
 
     <meta-data

+ 36 - 0
app/src/main/java/com/hzliuzhi/applet/container/MainActivity.kt

@@ -1,12 +1,14 @@
 package com.hzliuzhi.applet.container
 
 import android.annotation.SuppressLint
+import android.content.Intent
 import android.view.KeyEvent
 import android.widget.Toast
 import androidx.activity.compose.LocalActivity
 import androidx.activity.compose.setContent
 import androidx.compose.foundation.layout.padding
 import androidx.compose.material3.Scaffold
+import androidx.compose.runtime.LaunchedEffect
 import androidx.compose.runtime.rememberCoroutineScope
 import androidx.compose.ui.Modifier
 import androidx.compose.ui.platform.LocalContext
@@ -14,6 +16,8 @@ import androidx.lifecycle.compose.LocalLifecycleOwner
 import androidx.navigation.NavHostController
 import androidx.navigation.compose.rememberNavController
 import com.hzliuzhi.applet.container.navigation.Host
+import com.hzliuzhi.applet.container.navigation.toRoute
+import com.hzliuzhi.applet.core.router.DeepLink
 import com.hzliuzhi.applet.core.shared.Message
 import com.hzliuzhi.applet.core.shared.Payload
 import com.hzliuzhi.applet.core.shared.SharedFlowHub
@@ -23,6 +27,12 @@ import com.hzliuzhi.applet.scanner.Scanner
 
 class MainActivity : AndroidActivity() {
   private var navController: NavHostController? = null
+  private var pendingDeepLink: String? = null
+
+  override fun onNewIntent(intent: Intent) {
+    super.onNewIntent(intent)
+    handleDeepLink(intent.data?.toString())
+  }
 
   @SuppressLint("RestrictedApi")
   override fun dispatchKeyEvent(event: KeyEvent): Boolean {
@@ -44,6 +54,13 @@ class MainActivity : AndroidActivity() {
         }
       }
 
+      LaunchedEffect(navController) {
+        pendingDeepLink?.takeIf { navController?.graph != null }?.also {
+          handleDeepLink(it)
+          pendingDeepLink = null
+        }
+      }
+
       val context = LocalContext.current
       val owner = LocalLifecycleOwner.current
       Scanner.getInstance(context).observe(owner) { result ->
@@ -52,6 +69,7 @@ class MainActivity : AndroidActivity() {
         } else {
           Toast.makeText(context, result.message, Toast.LENGTH_SHORT).show()
           when {
+            result.code.startsWith(DeepLink().scheme) -> runCatching { handleDeepLink(result.code).let { null } }.getOrNull()
             result.code.startsWith("six:") -> null
             else -> Payload.data(data = result).toJson()
           }
@@ -59,6 +77,8 @@ class MainActivity : AndroidActivity() {
           val event = Message(type = "scan", payload = it).toWebEvent()
           SharedFlowHub.emit(event)
         }
+        // 处理完后清空缓存
+        Scanner.getInstance(context).clear()
       }
       Scanner.getInstance(context).eventHandle(rememberCoroutineScope())
 
@@ -73,5 +93,21 @@ class MainActivity : AndroidActivity() {
         }
       }
     }
+    handleDeepLink(intent.data?.toString())
+  }
+
+  private fun handleDeepLink(link: String?) {
+    if (navController?.graph == null) {
+      pendingDeepLink = link
+    } else {
+      link?.toRoute()?.also {
+        navController?.apply {
+          navigate(it) {
+            popUpTo(graph.startDestinationId)
+            launchSingleTop = true
+          }
+        }
+      }
+    }
   }
 }

+ 6 - 4
app/src/main/java/com/hzliuzhi/applet/container/navigation/Host.kt

@@ -8,9 +8,10 @@ import androidx.compose.ui.platform.LocalContext
 import androidx.navigation.NavHostController
 import androidx.navigation.compose.NavHost
 import androidx.navigation.compose.rememberNavController
-
 import com.hzliuzhi.applet.browser.navigation.browser
 import com.hzliuzhi.applet.container.R
+import com.hzliuzhi.applet.core.router.navRoute
+import com.hzliuzhi.applet.core.store.SettingStore
 
 @Composable
 fun Host(
@@ -28,11 +29,12 @@ fun Host(
 }
 
 @Composable
-fun rememberRoute(): Any {
+fun rememberRoute(): String {
   val context = LocalContext.current.applicationContext
   return remember {
     runCatching {
-      context.resources.getString(R.string.app_screen).toRoute()
-    }.getOrNull() ?: AppRoute.Home
+      val screen = SettingStore.getInstance(context).screen ?: context.resources.getString(R.string.app_screen)
+      screen.toRoute()
+    }.getOrNull() ?: navRoute(AppRoute.Launcher())
   }
 }

+ 25 - 2
app/src/main/java/com/hzliuzhi/applet/container/navigation/Route.kt

@@ -1,22 +1,45 @@
 package com.hzliuzhi.applet.container.navigation
 
+import android.widget.Toast
 import androidx.compose.foundation.layout.fillMaxSize
 import androidx.compose.ui.Modifier
+import androidx.compose.ui.platform.LocalContext
 import androidx.navigation.NavController
 import androidx.navigation.NavGraphBuilder
-import androidx.navigation.compose.composable
 import com.hzliuzhi.applet.container.ui.HomeScreen
+import com.hzliuzhi.applet.container.ui.LauncherScreen
+import com.hzliuzhi.applet.core.router.DeepLink
+import com.hzliuzhi.applet.core.router.route
 import kotlinx.serialization.Serializable
 
 sealed class AppRoute {
   @Serializable
   data object Home : AppRoute()
+
+  @Serializable
+  data class Launcher(val text: String? = "") : AppRoute()
 }
 
 fun NavGraphBuilder.app(navController: NavController) {
-  composable<AppRoute.Home> { _ ->
+  route<AppRoute.Home> {
     HomeScreen(
       modifier = Modifier.fillMaxSize()
     )
   }
+  route<AppRoute.Launcher> { (route) ->
+    val context = LocalContext.current
+    LauncherScreen(
+      modifier = Modifier.fillMaxSize(),
+      input = route.text,
+      start = {
+        runCatching {
+          val value = if (it.startsWith(DeepLink().scheme)) it else "browser/kiosk?url=$it"
+          value.toRoute()?.also { navController.navigate(it) } ?: throw IllegalArgumentException("无效的路由")
+          value
+        }.onFailure {
+          Toast.makeText(context, "跳转失败: ${it.message}", Toast.LENGTH_SHORT).show()
+        }.getOrNull()
+      }
+    )
+  }
 }

+ 20 - 8
app/src/main/java/com/hzliuzhi/applet/container/navigation/RouteExtra.kt

@@ -1,18 +1,30 @@
 package com.hzliuzhi.applet.container.navigation
 
+import android.util.Log
 import com.hzliuzhi.applet.browser.navigation.BrowserRoute
+import com.hzliuzhi.applet.core.router.DeepLink
+import com.hzliuzhi.applet.core.router.navRoute
 
-data class DeepLink(
-  val scheme: String = "six-applet://",
-)
 
-internal fun String?.toRoute(): Any? {
+internal fun String?.toRoute(): String? {
   val value = this?.replace(DeepLink().scheme, "") ?: ""
   return when {
-    value.startsWith("home") -> AppRoute.Home
-    value.startsWith("browser/update") -> BrowserRoute.Update
-    value.startsWith("browser/web?url=") -> BrowserRoute.Web().copy(url = value.substringAfter("browser/web?url="))
-    value.startsWith("browser/kiosk?url=") -> BrowserRoute.Kiosk().copy(url = value.substringAfter("browser/kiosk?url="))
+    value.startsWith("home") -> navRoute<AppRoute.Home>()
+    value.startsWith("launcher") -> {
+      val text = value.substringAfter("launcher?text=", "")
+      navRoute(AppRoute.Launcher(text = text))
+    }
+
+    value.startsWith("browser/update") -> navRoute<BrowserRoute.Update>()
+    value.startsWith("browser/web?url=") -> {
+      val url = value.substringAfter("browser/web?url=", "")
+      navRoute(BrowserRoute.Web(url = url))
+    }
+
+    value.startsWith("browser/kiosk?url=") -> {
+      val url = value.substringAfter("browser/kiosk?url=", "")
+      navRoute(BrowserRoute.Kiosk(url = url))
+    }
     else -> null
   }
 }

+ 246 - 0
app/src/main/java/com/hzliuzhi/applet/container/ui/LauncherScreen.kt

@@ -0,0 +1,246 @@
+package com.hzliuzhi.applet.container.ui
+
+import android.content.Intent
+import android.provider.Settings
+import android.util.Patterns
+import android.widget.Toast
+import androidx.compose.foundation.clickable
+import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.Spacer
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.height
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.size
+import androidx.compose.foundation.layout.width
+import androidx.compose.foundation.text.KeyboardActions
+import androidx.compose.foundation.text.KeyboardOptions
+import androidx.compose.material.icons.Icons
+import androidx.compose.material.icons.automirrored.filled.Send
+import androidx.compose.material.icons.filled.Info
+import androidx.compose.material.icons.filled.QrCode
+import androidx.compose.material3.Card
+import androidx.compose.material3.Icon
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.material3.OutlinedTextField
+import androidx.compose.material3.Surface
+import androidx.compose.material3.Switch
+import androidx.compose.material3.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.DisposableEffect
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.setValue
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.platform.LocalContext
+import androidx.compose.ui.platform.LocalSoftwareKeyboardController
+import androidx.compose.ui.text.input.ImeAction
+import androidx.compose.ui.text.input.KeyboardType
+import androidx.compose.ui.text.style.TextDecoration
+import androidx.compose.ui.unit.dp
+import androidx.core.net.toUri
+import androidx.lifecycle.compose.LocalLifecycleOwner
+import com.hzliuzhi.applet.core.store.SettingStore
+import com.hzliuzhi.applet.scanner.ScanResult
+import com.hzliuzhi.applet.scanner.Scanner
+
+
+@Composable
+fun LauncherScreen(
+  input: String?,
+  modifier: Modifier = Modifier,
+  start: (String) -> String?,
+) {
+  val context = LocalContext.current
+  var text by remember { mutableStateOf(input ?: "") }
+  var storeEnabled by remember { mutableStateOf(true) }
+
+  Column(
+    modifier = modifier
+      .fillMaxSize()
+      .padding(24.dp),
+    verticalArrangement = Arrangement.Center
+  ) {
+    LauncherInputField(
+      value = text,
+      onValueChange = { text = it },
+      onDone = {
+        start(text)?.also {
+          SettingStore.getInstance(context).screen = if (storeEnabled) it else null
+        }
+      },
+    )
+    Spacer(modifier = Modifier.height(24.dp))
+    StorageCard(
+      enabled = storeEnabled,
+      enabledChange = { storeEnabled = it }
+    )
+  }
+}
+
+@Composable
+fun LauncherInputField(
+  value: String,
+  onValueChange: (String) -> Unit,
+  onDone: () -> Unit,
+  modifier: Modifier = Modifier,
+) {
+  val context = LocalContext.current
+
+  val scanner = Scanner.getInstance(context)
+  val owner = LocalLifecycleOwner.current
+
+  DisposableEffect(owner, scanner) {
+    val observer: (ScanResult?) -> Unit = { result -> result?.code?.also { onValueChange(it) } }
+    scanner.observe(owner, observer)
+    onDispose { scanner.removeObserver(observer) }
+  }
+
+  val isUrl = remember(value) { Patterns.WEB_URL.matcher(value).matches() }
+
+  val keyboardController = LocalSoftwareKeyboardController.current
+
+  fun done() {
+    if (isUrl) {
+      keyboardController?.hide()
+      onDone()
+    } else Toast.makeText(context, "请输入合法的网址", Toast.LENGTH_SHORT).show()
+  }
+
+
+  OutlinedTextField(
+    value = value,
+    onValueChange = onValueChange,
+    label = { Text("请输入URL") },
+    singleLine = true,
+    isError = value.isNotBlank() && !isUrl,
+    modifier = modifier.fillMaxWidth(),
+    keyboardOptions = KeyboardOptions(
+      keyboardType = KeyboardType.Uri,
+      imeAction = ImeAction.Done
+    ),
+    keyboardActions = KeyboardActions(onDone = { done() }),
+    trailingIcon = {
+      if (value.isBlank()) {
+        Icon(
+          imageVector = Icons.Filled.QrCode,
+          contentDescription = "扫码",
+          modifier = Modifier.clickable {
+            scanner.start()
+            keyboardController?.hide()
+          }
+        )
+      } else {
+        Icon(
+          imageVector = Icons.AutoMirrored.Filled.Send,
+          contentDescription = "完成",
+          modifier = Modifier.clickable { done() }
+        )
+      }
+    }
+  )
+}
+
+@Composable
+fun StorageCard(
+  enabled: Boolean,
+  enabledChange: (Boolean) -> Unit,
+) {
+  val context = LocalContext.current
+
+  val appName = runCatching {
+    val info = context.applicationInfo
+    info.labelRes.takeIf { it != 0 }?.let { context.getString(it) } ?: info.nonLocalizedLabel?.toString()
+  }.getOrNull() ?: "本App"
+
+  Card(
+    modifier = Modifier.fillMaxWidth(),
+    shape = MaterialTheme.shapes.medium,
+  ) {
+    Column(
+      modifier = Modifier.padding(start = 12.dp, end = 12.dp)
+    ) {
+      // 头部:标题+开关
+      Row(
+        modifier = Modifier.fillMaxWidth(),
+        verticalAlignment = Alignment.CenterVertically
+      ) {
+        Text(
+          text = "存储数据",
+          style = MaterialTheme.typography.titleMedium,
+          modifier = Modifier.weight(1f)
+        )
+        Switch(
+          checked = enabled,
+          onCheckedChange = enabledChange
+        )
+      }
+      if (enabled) {
+        Spacer(modifier = Modifier.height(8.dp))
+        Row(verticalAlignment = Alignment.CenterVertically) {
+          Icon(
+            imageVector = Icons.Default.Info,
+            contentDescription = null,
+            tint = MaterialTheme.colorScheme.secondary
+          )
+          Spacer(modifier = Modifier.width(8.dp))
+          Text(
+            text = "如需清除缓存,请在",
+            style = MaterialTheme.typography.bodyMedium,
+            color = MaterialTheme.colorScheme.secondary
+          )
+          Text(
+            text = "设置中操作",
+            style = MaterialTheme.typography.bodyMedium.copy(color = Color(0xFF1976D2), textDecoration = TextDecoration.Underline),
+            modifier = Modifier
+              .clickable {
+                val intent = Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS).apply {
+                  data = ("package:" + context.packageName).toUri()
+                }
+                context.startActivity(intent)
+              }
+          )
+        }
+        Spacer(modifier = Modifier.height(12.dp))
+        Text(
+          text = "清除数据:",
+          style = MaterialTheme.typography.bodyMedium,
+          color = MaterialTheme.colorScheme.primary
+        )
+        Spacer(modifier = Modifier.height(8.dp))
+        Column {
+          val steps = listOf(
+            "打开系统设置",
+            "找到应用管理",
+            "选择$appName",
+            "进入存储与缓存",
+            "点击清除全部数据(存储空间)"
+          )
+          steps.forEachIndexed { idx, step ->
+            Row(verticalAlignment = Alignment.CenterVertically) {
+              Surface(
+                shape = MaterialTheme.shapes.small,
+                color = MaterialTheme.colorScheme.primary.copy(alpha = 0.1f),
+                modifier = Modifier.size(24.dp)
+              ) {
+                Box(contentAlignment = Alignment.Center) {
+                  Text("${idx + 1}", style = MaterialTheme.typography.labelMedium)
+                }
+              }
+              Spacer(modifier = Modifier.width(8.dp))
+              Text(step, style = MaterialTheme.typography.bodySmall)
+            }
+            if (idx < steps.lastIndex) Spacer(modifier = Modifier.height(6.dp))
+          }
+        }
+        Spacer(modifier = Modifier.height(8.dp))
+      }
+    }
+  }
+}

+ 0 - 1
app/src/pda/res/values/application.xml

@@ -2,5 +2,4 @@
 <resources>
   <string name="app_id">PDA</string>
   <string name="app_name">中药处方煎配朔源管理(开发版)</string>
-  <string name="app_screen">browser/kiosk?url=http://192.168.1.14:4173/pharmacy/pda/</string>
 </resources>

+ 2 - 0
core/build.gradle.kts

@@ -37,6 +37,7 @@ dependencies {
 
   implementation(libs.androidx.core.ktx)
   implementation(libs.androidx.appcompat)
+  implementation(libs.androidx.navigation.compose)
   implementation(platform(libs.androidx.compose.bom))
   implementation(libs.androidx.ui)
   implementation(libs.androidx.ui.graphics)
@@ -46,4 +47,5 @@ dependencies {
   androidTestImplementation(libs.androidx.espresso.core)
 
   implementation(libs.gson)
+  implementation(libs.kotlin.reflect)
 }

+ 5 - 0
core/src/main/java/com/hzliuzhi/applet/core/router/DeepLink.kt

@@ -0,0 +1,5 @@
+package com.hzliuzhi.applet.core.router
+
+data class DeepLink(
+  val scheme: String = "six-applet://",
+)

+ 194 - 0
core/src/main/java/com/hzliuzhi/applet/core/router/route.kt

@@ -0,0 +1,194 @@
+package com.hzliuzhi.applet.core.router
+
+import android.os.Parcelable
+import androidx.compose.animation.AnimatedContentScope
+import androidx.compose.runtime.Composable
+import androidx.navigation.NamedNavArgument
+import androidx.navigation.NavBackStackEntry
+import androidx.navigation.NavDeepLink
+import androidx.navigation.NavGraphBuilder
+import androidx.navigation.NavHostController
+import androidx.navigation.NavType
+import androidx.navigation.compose.composable
+import androidx.navigation.navArgument
+import androidx.navigation.navDeepLink
+import java.util.concurrent.ConcurrentHashMap
+import kotlin.reflect.KProperty1
+import kotlin.reflect.KType
+import kotlin.reflect.full.createInstance
+import kotlin.reflect.full.memberProperties
+import kotlin.reflect.full.primaryConstructor
+
+
+// region 反射缓存
+val constructorCache = ConcurrentHashMap<Class<*>, kotlin.reflect.KFunction<*>>()
+val memberPropertiesCache = ConcurrentHashMap<Class<*>, Collection<KProperty1<out Any, *>>>()
+
+inline fun <reified T : Any> getPrimaryConstructor(): kotlin.reflect.KFunction<*>? {
+  val key = T::class.java
+  constructorCache[key]?.let { return it }
+  val ctor = T::class.primaryConstructor
+  if (ctor != null) {
+    constructorCache[key] = ctor
+  }
+  return ctor
+}
+
+@Suppress("UNCHECKED_CAST")
+inline fun <reified T : Any> getMemberProperties(): Collection<KProperty1<T, *>> {
+  val key = T::class.java
+  memberPropertiesCache[key]?.let { return it as Collection<KProperty1<T, *>> }
+  val props = T::class.memberProperties as Collection<KProperty1<T, *>>
+  memberPropertiesCache[key] = props
+  return props
+}
+// endregion
+
+/**
+ * 根据Kotlin类型自动推断NavType,支持基础类型。
+ * @param type Kotlin反射类型
+ * @return 对应的NavType
+ */
+fun navTypeFor(type: KType): NavType<*> = when (val classifier = type.classifier) {
+  String::class -> NavType.StringType
+  Int::class -> NavType.IntType
+  Long::class -> NavType.LongType
+  Float::class -> NavType.FloatType
+  Boolean::class -> NavType.BoolType
+  else -> NavType.StringType
+}
+
+/**
+ * 自动生成navArgument,支持部分字段有默认参数。
+ * 只为有默认值的参数设置defaultValue,必填参数不设置defaultValue。
+ * @return 包含所有参数的NamedNavArgument列表
+ */
+inline fun <reified T : Any> navArgumentsWithDefault(): List<NamedNavArgument> = T::class.run {
+  val params = getPrimaryConstructor<T>()?.parameters.orEmpty()
+  // 只尝试获取有默认值的参数的默认实例
+  val defaultInstance = try {
+    if (params.all { it.isOptional || it.isVararg }) createInstance() else null
+  } catch (e: Exception) {
+    null
+  }
+  val memberProps = getMemberProperties<T>()
+  params.map { param ->
+    val paramName = param.name!!
+    val type = navTypeFor(param.type)
+    // 只为有默认值的参数设置 defaultValue
+    val hasDefault = param.isOptional || (defaultInstance != null && memberProps.find { it.name == paramName }?.getter?.call(defaultInstance) != null)
+    val defaultValue = if (hasDefault && defaultInstance != null) {
+      memberProps.find { it.name == paramName }?.getter?.call(defaultInstance)
+    } else null
+    navArgument(paramName) {
+      this.type = type
+      defaultValue?.let { value ->
+        @Suppress("IMPLICIT_CAST_TO_ANY")
+        when (type) {
+          NavType.StringType -> value as? String
+          NavType.IntType -> value as? Int
+          NavType.LongType -> value as? Long
+          NavType.FloatType -> value as? Float
+          NavType.BoolType -> value as? Boolean
+          is NavType.EnumType<*> -> value.toString()
+          is NavType.ParcelableType<*> -> value as? Parcelable
+          else -> null
+        }?.let { this.defaultValue = it }
+      }
+    }
+  }
+}
+
+/**
+ * 自动生成路由字符串,如 user?id={id}&name={name}
+ * @return 路由字符串
+ */
+inline fun <reified T : Any> navRoute() = T::class.run {
+  val name = simpleName?.replaceFirstChar { it.lowercase() } ?: "unknown"
+  val params = getPrimaryConstructor<T>()?.parameters.orEmpty()
+  params.takeIf { it.isNotEmpty() }
+    ?.joinToString("&", prefix = "$name?") { "${it.name}={${it.name}}" }
+    ?: name
+}
+
+inline fun <reified T : Any> navRoute(instance: T): String = T::class.run {
+  val name = simpleName?.replaceFirstChar { it.lowercase() } ?: "unknown"
+  val memberProps = getMemberProperties<T>()
+  val params = memberProps.mapNotNull { prop ->
+    prop.get(instance)?.let { "${prop.name}=$it" }
+  }
+  params.takeIf { it.isNotEmpty() }
+    ?.joinToString("&", prefix = "$name?")
+    ?: name
+}
+
+/**
+ * NavBackStackEntry自动转为目标data class,支持类型和错误提示
+ * @throws IllegalArgumentException 参数解析或构造失败时抛出
+ * @return 目标类型实例
+ */
+inline fun <reified T : Any> NavBackStackEntry.toRoute(): T = T::class.run {
+  val name = simpleName?.replaceFirstChar { it.lowercase() } ?: "unknown"
+  // 处理 object 单例类型
+  T::class.objectInstance?.let { return it as T }
+  val params = getPrimaryConstructor<T>()?.parameters.orEmpty()
+  val args = params.associateWith { param ->
+    val paramName = param.name!!
+    try {
+      when (param.type.classifier) {
+        String::class -> arguments?.getString(paramName)
+        Int::class -> arguments?.getInt(paramName)
+        Long::class -> arguments?.getLong(paramName)
+        Float::class -> arguments?.getFloat(paramName)
+        Boolean::class -> arguments?.getBoolean(paramName)
+        else -> arguments?.getString(paramName)
+      }
+    } catch (e: Exception) {
+      throw IllegalArgumentException("参数[$paramName] 解析失败: ${e.message}")
+    }
+  }
+  try {
+    getPrimaryConstructor<T>()!!.callBy(args) as T
+  } catch (e: Exception) {
+    throw IllegalArgumentException("$name 构造失败: ${e.message}")
+  }
+}
+
+/**
+ * NavGraphBuilder.route扩展,自动注册路由
+ * @param content 页面内容Composable
+ * @param deepLinks 可选的DeepLink列表
+ */
+inline fun <reified T : Any> NavGraphBuilder.route(
+  deepLinks: List<NavDeepLink> = emptyList(),
+  noinline content: @Composable AnimatedContentScope.(Pair<T, NavBackStackEntry>) -> Unit,
+) = T::class.run {
+  val name = simpleName?.replaceFirstChar { it.lowercase() } ?: "unknown"
+  composable(
+    route = navRoute<T>(),
+    arguments = navArgumentsWithDefault<T>(),
+    deepLinks = deepLinks + listOf(navDeepLink { uriPattern = "${DeepLink().scheme}${navRoute<T>()}" })
+  ) {
+    runCatching { content(Pair(it.toRoute(), it)) }.onFailure {
+      val message = "$name 路由参数解析或页面构建失败: ${it.message}"
+      android.util.Log.e("log:route", message, it)
+      throw IllegalStateException(message)
+    }.getOrNull()
+  }
+}
+
+/**
+ * NavController.router扩展,自动跳转并带参数
+ * @param route 路由参数对象
+ * @throws IllegalStateException 跳转失败时抛出
+ */
+inline fun <reified T : Any> NavHostController.router(route: T) = T::class.run {
+  runCatching {
+    navigate(navRoute(route))
+  }.onFailure { it ->
+    val name = simpleName?.replaceFirstChar { it.lowercase() } ?: "unknown"
+    val message = "$name 跳转失败: ${it.message}"
+    android.util.Log.e("log:router", message, it)
+    throw IllegalStateException(message)
+  }.getOrNull()
+}

+ 50 - 3
core/src/main/java/com/hzliuzhi/applet/core/shared/SharedFlowHub.kt

@@ -1,5 +1,6 @@
 package com.hzliuzhi.applet.core.shared
 
+import com.google.gson.JsonElement
 import kotlinx.coroutines.flow.MutableSharedFlow
 import kotlinx.coroutines.flow.asSharedFlow
 
@@ -16,7 +17,7 @@ object SharedFlowHub {
     return try {
       Event(
         type = this.type,
-        payload = this.payload as? P,
+        payload = this.payload.convertTo<P>(),
         callback = this.callback as? ((C) -> Unit)
       )
     } catch (e: Exception) {
@@ -29,7 +30,53 @@ object SharedFlowHub {
     return callback as? ((C) -> Unit)
   }
 
-  inline fun <reified P> Event<*, *>.payloadAs(): P? {
-    return payload as? P
+  inline fun <reified P> Event<*, *>.payloadAs() = payload.convertTo<P>()
+
+  inline fun <reified P> Any?.convertTo(): P? {
+    return when (P::class) {
+      String::class -> when (this) {
+        is String -> this as P
+        is JsonElement -> this.asString as P
+        else -> this?.toString() as? P
+      }
+
+      Int::class -> when (this) {
+        is Int -> this as P
+        is Number -> this.toInt() as P
+        is JsonElement -> this.asInt as P
+        is String -> this.toIntOrNull() as? P
+        else -> null
+      }
+
+      Long::class -> when (this) {
+        is Long -> this as P
+        is Number -> this.toLong() as P
+        is JsonElement -> this.asLong as P
+        is String -> this.toLongOrNull() as? P
+        else -> null
+      }
+
+      Boolean::class -> when (this) {
+        is Boolean -> this as P
+        is JsonElement -> this.asBoolean as P
+        is String -> this.toBoolean() as P
+        else -> null
+      }
+
+      Double::class -> when (this) {
+        is Double -> this as P
+        is Number -> this.toDouble() as P
+        is JsonElement -> this.asDouble as P
+        is String -> this.toDoubleOrNull() as? P
+        else -> null
+      }
+
+      JsonElement::class -> when (this) {
+        is JsonElement -> this as P
+        else -> null
+      }
+
+      else -> this as? P
+    }
   }
 }

+ 32 - 0
core/src/main/java/com/hzliuzhi/applet/core/store/SettingStore.kt

@@ -0,0 +1,32 @@
+package com.hzliuzhi.applet.core.store
+
+import android.content.Context
+import android.content.SharedPreferences
+import androidx.core.content.edit
+
+class SettingStore private constructor(private val sp: SharedPreferences) {
+
+
+  var screen: String?
+    get() = sp.getString(KEY_SCREEN, null)?.takeUnless { it.isBlank() }
+    set(value) {
+      sp.edit { value?.takeUnless { it.isBlank() }?.also { putString(KEY_SCREEN, it) } ?: remove(KEY_SCREEN) }
+    }
+
+  companion object {
+    private const val KEY_SCREEN = "screen"
+
+    @Volatile
+    private var instance: SettingStore? = null
+    fun getInstance(context: Context): SettingStore {
+      return instance ?: synchronized(this) {
+        instance ?: run {
+          val sp = context.applicationContext.getSharedPreferences("app_setting", Context.MODE_PRIVATE)
+          SettingStore(sp).also { instance = it }
+        }
+      }
+    }
+  }
+
+
+}

+ 3 - 0
gradle.properties

@@ -25,6 +25,9 @@ android.nonTransitiveRClass=true
 
 project.aio=debug
 project.aio-test=aio
+project.aio-prod=aio-test
+project.aio-CD-510100=aio-prod
+project.aio-ZJ-330681=aio-prod
 project.aio-CQ=aio-test
 
 project.pda=debug

+ 3 - 0
gradle/libs.versions.toml

@@ -1,6 +1,7 @@
 [versions]
 agp = "8.9.3"
 kotlin = "2.1.21"
+kotlinReflect = "2.1.21"
 serializationJson = "1.7.3"
 coreKtx = "1.16.0"
 junit = "4.13.2"
@@ -18,7 +19,9 @@ okhttp = "4.12.0"
 webkit = "1.14.0"
 
 [libraries]
+kotlin-reflect = { module = "org.jetbrains.kotlin:kotlin-reflect", version.ref = "kotlin" }
 androidx-core-ktx = { group = "androidx.core", name = "core-ktx", version.ref = "coreKtx" }
+androidx-material-icons-extended = { module = "androidx.compose.material:material-icons-extended" }
 androidx-navigation-compose = { module = "androidx.navigation:navigation-compose", version.ref = "navigationCompose" }
 junit = { group = "junit", name = "junit", version.ref = "junit" }
 androidx-junit = { group = "androidx.test.ext", name = "junit", version.ref = "junitVersion" }

+ 2 - 2
library/browser/src/main/assets/browser/bridge.js

@@ -13,9 +13,9 @@ class Bridge extends EventTarget {
     return crypto.randomUUID();
   }
 
-  static pulse(userId) {
+  static pulse(userId, cfg = {}) {
     const { promise, ...resolvers } = Promise.withResolvers();
-    this.getInstance().#postMessage('pulse', { userId }, resolvers);
+    this.getInstance().#postMessage('pulse', { ...cfg, userId }, resolvers);
     return promise;
   }
 

+ 5 - 8
library/browser/src/main/java/com/hzliuzhi/applet/browser/navigation/Route.kt

@@ -4,35 +4,32 @@ import androidx.compose.foundation.layout.fillMaxSize
 import androidx.compose.ui.Modifier
 import androidx.navigation.NavController
 import androidx.navigation.NavGraphBuilder
-import androidx.navigation.compose.composable
-import androidx.navigation.toRoute
 import com.hzliuzhi.applet.browser.ui.KioskScreen
 import com.hzliuzhi.applet.browser.ui.UpdateScreen
 import com.hzliuzhi.applet.browser.ui.WebScreen
 import com.hzliuzhi.applet.browser.webview.WebContent
+import com.hzliuzhi.applet.core.router.route
 import kotlinx.serialization.Serializable
 
 sealed class BrowserRoute {
-
   @Serializable
   data class Web(val url: String? = "") : BrowserRoute()
 
   @Serializable
   data class Kiosk(val url: String? = "") : BrowserRoute()
+
   @Serializable
   data object Update : BrowserRoute()
 }
 
 fun NavGraphBuilder.browser(navController: NavController) {
-  composable<BrowserRoute.Web> { backStackEntry ->
-    val route = backStackEntry.toRoute<BrowserRoute.Web>()
+  route<BrowserRoute.Web> { (route) ->
     WebScreen(url = route.url ?: "localhost")
   }
-  composable<BrowserRoute.Kiosk> { backStackEntry ->
-    val route = backStackEntry.toRoute<BrowserRoute.Kiosk>()
+  route<BrowserRoute.Kiosk> { (route) ->
     KioskScreen(content = WebContent.Url(url = route.url ?: "localhost"))
   }
-  composable<BrowserRoute.Update> {
+  route<BrowserRoute.Update> {
     UpdateScreen(modifier = Modifier.fillMaxSize())
   }
 }

+ 14 - 0
library/device/pulse/src/main/java/com/hzliuzhi/applet/device/pulse/PulseEventHandler.kt

@@ -43,6 +43,12 @@ class PulseEventHandle(private val activity: Activity, scope: CoroutineScope) {
             }
           }
 
+          "${SharedFlowHub.WEBVIEW_BRIDGE_EVENT}:pulse:url:get" -> {
+            _event.cast<String, JsonElement>()?.also { event ->
+              handlePulseUrlById(event.payload, event.callback ?: {})
+            }
+          }
+
           else -> _event.callbackAs<Payload<Unit>>()?.invoke(Payload.error(message = "[pulse] 未实现 ${_event.type}"))
         }
       }
@@ -53,5 +59,13 @@ class PulseEventHandle(private val activity: Activity, scope: CoroutineScope) {
     if (pulse == null || pulse.userId.isNullOrEmpty()) Payload.error<PulseResult?>(message = "[pulse] 参数解析错误").also { callback(it) }
     else TaiYiUtil.start(activity, "six:${pulse.userId}", callback)
   }
+
+  private suspend fun handlePulseUrlById(id: String?, callback: (JsonElement) -> Unit) {
+    if (id.isNullOrBlank()) Payload.error<Unit>(message = "[pulse] 参数解析错误(measureId 为空)").toJson()?.also(callback)
+    else runCatching { TaiYiUtil.getReportUrl(id, activity) }
+      .map { Payload.data(it).toJson() }
+      .getOrElse { e -> Payload.error<Unit>(message = "[pulse] 获取报告URL失败: ${e.message}").toJson() }
+      ?.also(callback)
+  }
 }
 

+ 11 - 6
library/device/pulse/src/main/java/com/hzliuzhi/applet/device/pulse/util/TaiYiUtil.kt

@@ -2,14 +2,17 @@ package com.hzliuzhi.applet.device.pulse.util
 
 import android.app.Activity
 import android.app.Application
+import android.content.Context
 import android.content.res.Resources
 import com.hzliuzhi.applet.core.shared.Payload
 import com.hzliuzhi.applet.core.util.proxy
 import com.hzliuzhi.applet.device.pulse.PulseResult
 import com.hzliuzhi.applet.device.pulse.R
+import com.taiyi.tyusbsdk.pulse.ProxyManager
 import com.taiyi.tyusbsdk.pulse.TaiyiConfig
 import com.taiyi.tyusbsdk.pulse.TaiyiManager
 import com.taiyi.tyusbsdk.pulse.net.HttpImpl
+import com.taiyi.tyusbsdk.pulse.net.HttpUtil
 import com.taiyi.zhimai.ui.activity.MeasureMainActivity
 import kotlinx.coroutines.suspendCancellableCoroutine
 import kotlin.coroutines.resume
@@ -40,20 +43,22 @@ object TaiYiUtil {
     )
   }
 
-  fun getReportUrlSync(measureId: String) = TaiyiManager.getInstance().getUrl(measureId)
-  suspend fun getReportUrlUrl(measureId: String) {
-    suspendCancellableCoroutine { cont ->
+  private fun getReportUrlSync(measureId: String): String = TaiyiManager.getInstance().getUrl(measureId)
+  suspend fun getReportUrl(measureId: String, context: Context) = suspendCancellableCoroutine { continuation ->
+    if (HttpUtil.AuthorizationKey != "Authorization") continuation.resume(getReportUrlSync(measureId))
+    else run {
+      HttpUtil.getInstance().getRequestQueue(context.applicationContext)
       TaiyiManager.getInstance().getAsyncUrl(measureId, object : HttpImpl<String> {
         override fun showError(message: String?) {
-          cont.resumeWithException(Exception(message))
+          continuation.takeIf { it.isActive }?.resumeWithException(Exception(message))
         }
 
         override fun showResponse(url: String) {
-          cont.resume(url)
+          continuation.takeIf { it.isActive }?.resume(url)
         }
       })
     }
-  }
+  }.let { ProxyManager.getInstance().pool.entries.firstOrNull { (key, _) -> it.startsWith(key) }?.let { (key, value) -> "$value${it.removePrefix(key)}" } ?: it }
 }
 
 private fun Resources.OSSAgency(): TaiyiConfig.OSSAgency {

+ 8 - 2
library/device/scanner/src/main/java/com/hzliuzhi/applet/scanner/Scanner.kt

@@ -30,7 +30,7 @@ class Scanner private constructor(context: Context, private val impl: ScannerImp
     receiver = impl.registerReceiver(applicationContext) {
       scanTimeout?.cancel()
       if (callback == null) it?.takeIf { it.code.isNotEmpty() }.also { value = it }
-      else Payload.data(it, message = "[scan:start] 扫码开始").toJson()?.let { it1 -> callback?.invoke(it1) }
+      else Payload.data(it, message = "[scan:start] 扫码开始").toJson()?.let { it1 -> callback?.invoke(it1).also { clear() } }
 
       scanTimeout = null
       callback = null
@@ -53,7 +53,7 @@ class Scanner private constructor(context: Context, private val impl: ScannerImp
 
               event.payload
                 ?.let { ScanParams.formJson(it) }
-                ?.let { impl.start(applicationContext, it) }
+                ?.let { start(it) }
                 ?.also { seconds ->
                   scanTimeout?.cancel()
                   scanTimeout = scope.launch {
@@ -81,6 +81,12 @@ class Scanner private constructor(context: Context, private val impl: ScannerImp
       .launchIn(scope)
   }
 
+  fun start(params: ScanParams = ScanParams()) = impl.start(applicationContext, params)
+
+  fun clear() {
+    if (value != null) postValue(null)
+  }
+
   companion object {
     @Volatile
     private var instance: Scanner? = null

+ 1 - 0
module/taiyi-pulse

@@ -0,0 +1 @@
+Subproject commit f3c20821af58cc296b4b0f4396d24d03104b7206

+ 1 - 0
module/webview-upgrade

@@ -0,0 +1 @@
+Subproject commit 87000d282b40d20e68ab9f74f1a44c1cacf19831