diff --git a/android-buyer/app/src/main/java/com/roubao/autopilot/MainActivity.kt b/android-buyer/app/src/main/java/com/roubao/autopilot/MainActivity.kt index ecaf95d..86f69f5 100644 --- a/android-buyer/app/src/main/java/com/roubao/autopilot/MainActivity.kt +++ b/android-buyer/app/src/main/java/com/roubao/autopilot/MainActivity.kt @@ -358,9 +358,7 @@ class MainActivity : ComponentActivity() { state = procurementState, readiness = readiness, onLogin = { input: LoginInput -> - lifecycleScope.launch { - procurementRepository.login(input) - } + procurementRepository.login(input) }, onClaim = { lifecycleScope.launch { diff --git a/android-buyer/app/src/main/java/com/roubao/autopilot/ui/screens/ProcurementScreen.kt b/android-buyer/app/src/main/java/com/roubao/autopilot/ui/screens/ProcurementScreen.kt index f90d384..636ab6a 100644 --- a/android-buyer/app/src/main/java/com/roubao/autopilot/ui/screens/ProcurementScreen.kt +++ b/android-buyer/app/src/main/java/com/roubao/autopilot/ui/screens/ProcurementScreen.kt @@ -34,6 +34,7 @@ import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableLongStateOf import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier @@ -50,12 +51,13 @@ import com.roubao.autopilot.readiness.DeviceReadinessSnapshot import com.roubao.autopilot.ui.theme.BaoziTheme import java.time.Instant import kotlinx.coroutines.delay +import kotlinx.coroutines.launch @Composable fun ProcurementScreen( state: ProcurementUiState, readiness: DeviceReadinessSnapshot, - onLogin: (LoginInput) -> Unit, + onLogin: suspend (LoginInput) -> Boolean, onClaim: () -> Unit, onStart: (ExecutionMode) -> Unit, onRelease: () -> Unit, @@ -260,9 +262,10 @@ private fun ExecutionModeSelector( @Composable private fun LoginSection( state: ProcurementUiState, - onLogin: (LoginInput) -> Unit + onLogin: suspend (LoginInput) -> Boolean ) { val colors = BaoziTheme.colors + val coroutineScope = rememberCoroutineScope() var backendUrl by remember(state.profile.backendUrl) { mutableStateOf(state.profile.backendUrl) } @@ -274,6 +277,7 @@ private fun LoginSection( mutableStateOf(state.profile.deviceId) } var deviceToken by remember { mutableStateOf("") } + var submitting by remember { mutableStateOf(false) } Column(verticalArrangement = Arrangement.spacedBy(12.dp)) { Text( @@ -329,19 +333,28 @@ private fun LoginSection( ) Button( onClick = { - onLogin( - LoginInput( + if (!submitting) { + val input = LoginInput( backendUrl = backendUrl, username = username, password = password, deviceId = deviceId, deviceToken = deviceToken ) - ) - password = "" - deviceToken = "" + submitting = true + coroutineScope.launch { + try { + if (onLogin(input)) { + password = "" + deviceToken = "" + } + } finally { + submitting = false + } + } + } }, - enabled = !state.busy, + enabled = !state.busy && !submitting, modifier = Modifier.fillMaxWidth() ) { Icon(Icons.Filled.Lock, contentDescription = null) diff --git a/docs/current-state.md b/docs/current-state.md index efeceac..83c86d6 100644 --- a/docs/current-state.md +++ b/docs/current-state.md @@ -5,7 +5,8 @@ ## 当前快照 - 日期:2026-07-27 -- 阶段:T-207 App 本地 VLM、候选、证据和结果回传完成,下一步 T-208 +- 阶段:T-207 App 本地 VLM、候选、证据和结果回传完成;T-209 登录失败保留输入完成, + 下一步 T-208 - Git:当前分支为 `main`;T-001 至 T-004、T-101 至 T-104、T-201 至 T-206 均已纳入 Git 历史;T-207 待本次提交 - 生产代码:`android-buyer/` 已接入 Roubao Android 源码 @@ -40,6 +41,8 @@ 状态、严格 HTTPS/Debug loopback 地址策略、`HttpTaskSource`、参考图校验、手动领取、 预览/start 二次确认、release、30 秒前台 heartbeat、有限离线、`SAFE_STOPPED`、 取消确认和进程恢复;后台任务不读取或覆盖本地 VLM provider +- 登录重试:采购登录失败、本地校验失败或网络失败时保留当前页面输入,只有成功才清理 + Compose 内存中的密码和设备密钥;敏感值不新增持久化或日志路径 - T-207 结果闭环:`execution_events`、受控截图、候选批次、模型出处、人工结论与 失败记录均按 execution 存储;Android 使用加密 outbox 按事件、截图、候选和终态 顺序回传,授权到期补报单独审计,所有终态固定 `order_submitted=false`。管理任务 @@ -91,6 +94,7 @@ | `docs/tasks/T-205.md` | DONE | 原子 claim、租约、execution 和取消安全确认 | | `docs/tasks/T-206.md` | DONE | App 领取、默认 30 分钟有限离线执行、恢复和安全停止 | | `docs/tasks/T-207.md` | DONE | App 本地 VLM、Key 加密、候选、事件、截图和结果回传 | +| `docs/tasks/T-209.md` | DONE | 登录失败保留采购员和设备输入以便直接重试 | | `docs/design/` | 已确认 | T-202 原型索引、4 个管理页和 7 个 Android 页面 | | `deepseek总结.txt` | 已有 | 历史讨论摘要,不是正式需求权威 | | `android-buyer/` | 已有 | Roubao `main` 固定 commit 的 Android 基线 | @@ -101,7 +105,7 @@ ## 任务摘要 -- 已完成:T-001 至 T-004、T-101 至 T-104、T-201 至 T-207。 +- 已完成:T-001 至 T-004、T-101 至 T-104、T-201 至 T-207、T-209。 - 正在进行:无。 - 下一个可领取任务:T-208 候选决策数据与人工理由闭环。 - 后置任务:T-208 候选决策数据与人工理由闭环;不得跳过 T-206/T-207 提前实现。 diff --git a/docs/tasks/T-209.md b/docs/tasks/T-209.md index 86c7fae..f109a48 100644 --- a/docs/tasks/T-209.md +++ b/docs/tasks/T-209.md @@ -4,7 +4,7 @@ title: 保留失败采购登录的已填写凭证 phase: 2 deps: - T-206 -status: TODO +status: DONE created: 2026-07-27 context_ref: d86a88e work_branch: null @@ -34,10 +34,10 @@ write_paths: ## 验收要点 -- [ ] 服务端拒绝、网络失败和本地校验失败后,五个已填写字段可直接修改并重试。 -- [ ] 成功登录后不保留登录页敏感输入,设备密钥只进入既有加密存储。 -- [ ] 密码和设备密钥不进入日志、普通 Preferences 或错误文案。 -- [ ] Android 单元测试与 Debug 构建通过。 +- [x] 服务端拒绝、网络失败和本地校验失败后,五个已填写字段可直接修改并重试。 +- [x] 成功登录后不保留登录页敏感输入,设备密钥只进入既有加密存储。 +- [x] 密码和设备密钥不进入日志、普通 Preferences 或错误文案。 +- [x] Android 单元测试与 Debug 构建通过。 ## 边界 @@ -47,3 +47,8 @@ write_paths: ## 执行记录 - 2026-07-27:任务已登记,待实现。 +- 2026-07-27:登录 UI 改为等待 Repository 布尔结果;失败时保留 Compose 内存中的 + 后台地址、账号、密码、设备 ID 和设备密钥,成功才清理敏感输入。新增本地提交锁, + 防止请求尚未开始时重复点击。 +- 2026-07-27:`./gradlew.bat :app:compileDebugKotlin :app:testDebugUnitTest + :app:assembleDebug` 通过;最新 Debug APK 已安装并冷启动至 PKG110,无 crash/ANR。