Compare commits

..
Author SHA1 Message Date
QiuSWandClaude Sonnet 5.5 ff8c5361cb feat(android): 状态页新增设备连接卡片 (#380)
显示当前 Wi-Fi IPv4 与无线调试开关状态(adb_wifi_enabled),读取失败显示无法读取;
端口仅给出固定查看提示。读数到文案/颜色为纯函数策略,不持久化、不上报、不写日志。

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NTDbDcwbDw1TSAcE6wfh2F
2026-10-12 15:27:26 +08:00
QiuSWandClaude Opus 5.5 23dafcf73f docs: record #372 v3 web release (#372)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NTDbDcwbDw1TSAcE6wfh2F
2026-10-10 11:58:24 +08:00
QiuSWandClaude Opus 5.5 3d5e9646f6 merge: shop column in SYB product export (#372)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NTDbDcwbDw1TSAcE6wfh2F
2026-10-10 11:52:01 +08:00
4 changed files with 180 additions and 2 deletions
@@ -74,6 +74,8 @@ class AgentStatusFragment : Fragment() {
private lateinit var mediaPermissionDetail: TextView
private lateinit var mediaPermissionAction: android.widget.Button
private lateinit var deviceText: TextView
private lateinit var deviceWifiText: TextView
private lateinit var deviceDebugText: TextView
private val mediaPermissionRequestLauncher by lazy {
registerForActivityResult(androidx.activity.result.contract.ActivityResultContracts.RequestPermission()) {
refreshStatus()
@@ -168,6 +170,20 @@ class AgentStatusFragment : Fragment() {
}
addView(mediaPermissionAction)
}))
addView(context.card(context.cardColumn().apply {
addView(context.label("设备连接", 14f, context.getColor(R.color.agent_text_muted)))
deviceWifiText = context.label("Wi-Fi IP:—", 18f, context.getColor(R.color.agent_text), true).apply {
setPadding(0, context.dp(6), 0, 0)
}
deviceDebugText = context.label("无线调试:—", 18f, context.getColor(R.color.agent_text_muted), true).apply {
setPadding(0, context.dp(6), 0, 0)
}
addView(deviceWifiText)
addView(deviceDebugText)
addView(context.label(DeviceConnectionPolicy.PORT_HINT, 14f, context.getColor(R.color.agent_text_muted)).apply {
setPadding(0, context.dp(6), 0, 0)
})
}))
}
refreshLayout = SwipeRefreshLayout(context).apply {
setColorSchemeResources(R.color.agent_primary_light)
@@ -245,6 +261,10 @@ class AgentStatusFragment : Fragment() {
private fun refreshStatus() {
if (!isAdded || view == null) return
val context = requireContext()
val deviceConnection = DeviceConnectionPolicy.display(DeviceConnectionReader.read(context))
deviceWifiText.text = deviceConnection.wifiText
deviceDebugText.text = deviceConnection.debugText
deviceDebugText.setTextColor(context.getColor(deviceConnection.debugColor))
val state = stateStore.read()
val connected = state.code in setOf("ONLINE", "BUSY", "COLLECTION_COOLDOWN")
connectionTitle.text = when (state.code) {
@@ -0,0 +1,85 @@
package cn.ilapage.goauto.agent.ui
import android.content.Context
import android.net.ConnectivityManager
import android.net.NetworkCapabilities
import android.provider.Settings
import androidx.annotation.ColorRes
import cn.ilapage.goauto.agent.R
import java.net.Inet4Address
import java.net.InetAddress
/** 无线调试开关的读取结果;读取失败为 [UNKNOWN]。 */
internal enum class WirelessDebugState { ENABLED, DISABLED, UNKNOWN }
internal data class DeviceConnectionReading(
val wifiIpv4: String?,
val wirelessDebug: WirelessDebugState,
)
internal data class DeviceConnectionDisplay(
val wifiText: String,
val debugText: String,
@ColorRes val debugColor: Int,
)
/** 原始读数到显示文案/颜色的纯函数策略,不依赖 Android 系统服务。 */
internal object DeviceConnectionPolicy {
const val PORT_HINT = "端口请在 开发者选项 → 无线调试 中查看"
const val NO_WIFI_TEXT = "未连接 Wi-Fi"
/** 仅当前网络为 Wi-Fi 时,从地址列表中取第一个非回环 IPv4;否则返回 null。 */
fun selectWifiIpv4(isWifi: Boolean, addresses: List<InetAddress>): String? {
if (!isWifi) return null
return addresses.firstOrNull { it is Inet4Address && !it.isLoopbackAddress }?.hostAddress
}
/** adb_wifi_enabled 原始值:1 开启,0 未开启,其他值或读取失败(null)为未知。 */
fun debugState(raw: Int?): WirelessDebugState = when (raw) {
1 -> WirelessDebugState.ENABLED
0 -> WirelessDebugState.DISABLED
else -> WirelessDebugState.UNKNOWN
}
fun display(reading: DeviceConnectionReading): DeviceConnectionDisplay {
val (text, color) = when (reading.wirelessDebug) {
WirelessDebugState.ENABLED -> "已开启" to R.color.agent_primary_light
WirelessDebugState.DISABLED -> "未开启" to R.color.agent_warning
WirelessDebugState.UNKNOWN -> "无法读取" to R.color.agent_text_muted
}
return DeviceConnectionDisplay(
wifiText = "Wi-Fi IP:${reading.wifiIpv4 ?: NO_WIFI_TEXT}",
debugText = "无线调试:$text",
debugColor = color,
)
}
}
/** 系统读取的薄封装;任何异常都降级为「未知」,不记录日志、不持久化。 */
internal object DeviceConnectionReader {
fun read(context: Context): DeviceConnectionReading = DeviceConnectionReading(
wifiIpv4 = readWifiIpv4(context),
wirelessDebug = DeviceConnectionPolicy.debugState(readDebugRaw(context)),
)
private fun readWifiIpv4(context: Context): String? = try {
val manager = context.getSystemService(Context.CONNECTIVITY_SERVICE) as? ConnectivityManager
val network = manager?.activeNetwork
val isWifi = network?.let { manager.getNetworkCapabilities(it) }
?.hasTransport(NetworkCapabilities.TRANSPORT_WIFI) == true
val addresses = if (isWifi && manager != null && network != null) {
manager.getLinkProperties(network)?.linkAddresses?.map { it.address }.orEmpty()
} else {
emptyList()
}
DeviceConnectionPolicy.selectWifiIpv4(isWifi, addresses)
} catch (_: Exception) {
null
}
private fun readDebugRaw(context: Context): Int? = try {
Settings.Global.getInt(context.contentResolver, "adb_wifi_enabled", 0)
} catch (_: Exception) {
null
}
}
@@ -0,0 +1,63 @@
package cn.ilapage.goauto.agent
import cn.ilapage.goauto.agent.ui.DeviceConnectionPolicy
import cn.ilapage.goauto.agent.ui.DeviceConnectionReading
import cn.ilapage.goauto.agent.ui.WirelessDebugState
import java.net.InetAddress
import org.junit.Assert.assertEquals
import org.junit.Assert.assertNull
import org.junit.Test
class DeviceConnectionPolicyTest {
private fun ip(text: String) = InetAddress.getByName(text)
@Test fun wifiWithIpv4ReturnsAddress() {
assertEquals("192.168.0.93", DeviceConnectionPolicy.selectWifiIpv4(true, listOf(ip("192.168.0.93"))))
}
@Test fun nonWifiReturnsNull() {
assertNull(DeviceConnectionPolicy.selectWifiIpv4(false, listOf(ip("10.0.0.2"))))
}
@Test fun wifiWithOnlyIpv6ReturnsNull() {
assertNull(DeviceConnectionPolicy.selectWifiIpv4(true, listOf(ip("2001:db8::1"))))
}
@Test fun wifiWithMultipleAddressesPicksIpv4() {
assertEquals(
"172.16.1.5",
DeviceConnectionPolicy.selectWifiIpv4(true, listOf(ip("2001:db8::1"), ip("172.16.1.5"))),
)
}
@Test fun loopbackIpv4IsSkipped() {
assertNull(DeviceConnectionPolicy.selectWifiIpv4(true, listOf(ip("127.0.0.1"))))
}
@Test fun debugRawValuesMapToState() {
assertEquals(WirelessDebugState.ENABLED, DeviceConnectionPolicy.debugState(1))
assertEquals(WirelessDebugState.DISABLED, DeviceConnectionPolicy.debugState(0))
assertEquals(WirelessDebugState.UNKNOWN, DeviceConnectionPolicy.debugState(2))
assertEquals(WirelessDebugState.UNKNOWN, DeviceConnectionPolicy.debugState(null))
}
@Test fun displayEnabledUsesPrimaryColor() {
val d = DeviceConnectionPolicy.display(DeviceConnectionReading("192.168.0.93", WirelessDebugState.ENABLED))
assertEquals("Wi-Fi IP:192.168.0.93", d.wifiText)
assertEquals("无线调试:已开启", d.debugText)
assertEquals(R.color.agent_primary_light, d.debugColor)
}
@Test fun displayDisabledUsesWarningColorAndNoWifiText() {
val d = DeviceConnectionPolicy.display(DeviceConnectionReading(null, WirelessDebugState.DISABLED))
assertEquals("Wi-Fi IP:未连接 Wi-Fi", d.wifiText)
assertEquals("无线调试:未开启", d.debugText)
assertEquals(R.color.agent_warning, d.debugColor)
}
@Test fun displayUnknownUsesMutedColor() {
val d = DeviceConnectionPolicy.display(DeviceConnectionReading(null, WirelessDebugState.UNKNOWN))
assertEquals("无线调试:无法读取", d.debugText)
assertEquals(R.color.agent_text_muted, d.debugColor)
}
}
+12 -2
View File
@@ -2,8 +2,8 @@
generated: true (请先修改 Gitea Wiki,禁止直接编辑本文件)
wiki_page: Deployment-and-Operations
wiki_url: https://git.ilapage.cn/OPC/goauto/wiki/Deployment-and-Operations.-
wiki_revision: b50250e0d61eff0b33f468e7cdf74139fe76bb65
synchronized_at: 2026-10-10T03:10:43Z
wiki_revision: d08a0dfbb8356f9cd7212d495e4f11beca59e293
synchronized_at: 2026-10-10T03:56:35Z
<!-- gitea-wiki-mirror:end -->
<!-- gitea-wiki-mirror:start -->
@@ -409,3 +409,13 @@ Server实现 `d84728d`、Android实现 `dbe7e98`,main 合并记录见 #378;
- 本地Android验证(cwd仓库根,JDK17及现有Android SDK):`./android/gradlew.bat -p android :app:testDebugUnitTest --tests '*HeartbeatReport*Test' --tests '*AgentTransportTimeoutTest' --tests '*SyncDiagnostic*Test' --tests '*HandledRecoveryDiagnosticTest' --tests '*AgentDiagnosticStoreMigrationTest' --tests '*AgentSyncCycleTest' :app:assembleDebug :app:compileReleaseUnitTestKotlin`。发布前还应执行#377受影响的outbox、运行占位、互斥、重购等回归;SQLite JDBC验证不等同于真机升级。
- Android兼容性测试在`android/build/diagnostics378-client-reports.json`生成纯合成HTTP捕获报告;在server目录将该绝对路径注入`GOAUTO_378_ANDROID_REPORTS`,执行`go test ./app/goauto/device -run TestAndroidGeneratedHeartbeatReports -count=1 -v`,使用真实validator/handler验证三类报告与重放。未设置路径时该项明确skip,不得据此宣称跨端通过;产物不提交Git。
- 线上迁移、发布、重启、装机仍需单独授权;本地构建/夹具不代表真机或线上30分钟心跳验收。
## #372 v3 导出店铺列的纯 Web 发布(2026-10-10)
- 用户授权合并 #372 v3 并部署线上。`feat/372-export-shop` 以 `--no-ff` 合并为 main `3d5e964`;Web 单元测试 93/93 通过,`build:prod` 通过。相对上一线上 Web(`4ad59bd`),Web 只有 v3 的导出改动。
- **main 已含 #378 的 Server 代码和迁移,本次没有部署**:线上 Server 仍是 `72a15f8` 构建的二进制。#378 的 Server 发布和迁移需另行授权。
- 发布目录 `/home/goauto/releases/20261010-3d5e964-372v3-web`,只替换 dist。goauto-server 与 config 从上一发布逐字复制(cmp/diff 一致),static/temp/var 沿用原目录,旧 js/css 哈希资源保留。index.html SHA256 `b900a40ec4691f0fb2c3d89e1e20826e7ca3205db311a33c24087767a515979a`。
- 只原子切换 current,不重启 GoAuto/Nginx;切换前后 PID 4786 不变,服务 active。
- 按内容验收:`/` 与 `/login` 返回新 index,10 项 JS/CSS 200,线上导出模块的 SYB 表头含「店铺」,health 200,验证码 code=200,未登录业务接口 401。登录后的实际导出待业务验收。
- 回滚:current 原子切回 `/home/goauto/releases/20261010-4ad59bd-372v2-web`,不需要重启。