diff --git a/2.Design/screens/android-login.png b/2.Design/screens/android-login.png new file mode 100644 index 0000000..fc48f85 Binary files /dev/null and b/2.Design/screens/android-login.png differ diff --git a/2.Design/screens/android-tasks.png b/2.Design/screens/android-tasks.png new file mode 100644 index 0000000..44253d2 Binary files /dev/null and b/2.Design/screens/android-tasks.png differ diff --git a/README.md b/README.md index 9d94207..1d22556 100644 --- a/README.md +++ b/README.md @@ -24,12 +24,12 @@ | 任务:收件箱→待办→进行中→完成、截止时间、标签、评论、事件流、乐观锁 | ✅ 后端 + Web 已通 | | Web 列表 / 看板拖拽 / 详情侧栏编辑 / 键盘流 / 快速录入语法 | ✅ e2e 7 项断言全绿,构建通过 | | 1000 条任务下的首屏性能 | ✅ 实测 P50 38–55ms(目标 < 300ms) | -| 安卓端任务列表与详情 | ⚠️ 代码已写,**本机无 JDK/Gradle,未编译验证** | +| 安卓端任务 / 工单 / 知识三个页面 | ✅ 已编译成 APK(9.94 MB),并在模拟器上跑通登录→建工作区→任务勾选→工单→知识 | | 工单:升级不搬数据、受理→办理→提交→验收、驳回、改派、站内通知 | ✅ 后端 + Web 已通,双人 e2e 8 项全绿 | | 知识:工单验收自动生成草稿、核对后发布、来源可追、手记 | ✅ 后端 + Web 已通,e2e 7 项全绿 | -| 后端测试 | ✅ 65 passed,ruff 干净 | +| 后端测试 | ✅ 66 passed,ruff 干净 | | 知识切片与语义检索(pgvector) | ⬜ 配置已就绪(`deploy/docker-compose.vector.yml`),本机 Docker 起不来,未跑过 | -| 安卓端知识页 / LLM 能力 | ⬜ M3 收尾 / M4 | +| LLM 能力(结构化录入、问答带引用、人物画像) | ⬜ M4 | ## 快速开始 diff --git a/android/README.md b/android/README.md index 53ba47a..60ba2d2 100644 --- a/android/README.md +++ b/android/README.md @@ -9,6 +9,18 @@ BUILD SUCCESSFUL in 38s app/build/outputs/apk/debug/app-debug.apk 9.94 MB ``` +而且**真的在模拟器上跑通了**:登录 → 建工作区 → 任务列表(中文标题、状态、截止日、勾选完成)→ 工单列表 → 知识列表,全部打到真实后端。 + +| 登录 | 任务列表 | +|---|---| +| ![登录](../2.Design/screens/android-login.png) | ![任务](../2.Design/screens/android-tasks.png) | + +一键起模拟器 + 装包(环境变量都替你设好了): + +```bash +pwsh -File android/scripts/emulator.ps1 -Install +``` + 本机从零把编译链搭起来踩过的坑都固化在仓库里了,别再走一遍: | 现象 | 原因 | 仓库里的解法 | @@ -68,8 +80,30 @@ app/src/main/java/com/dadao/task/ └── KnowledgeScreen.kt 知识四态 tab、手记、按状态给动作(发布需 owner/admin) ``` +## 模拟器(本机搭建记录,别重复踩) + +`sdkmanager` 在这台机器上拉不到远端仓库索引(`Failed to download any source lists!`,但直连 +`dl.google.com` 的 zip 是通的),所以系统镜像是**手动装的**: + +1. 下 `https://dl.google.com/android/repository/sys-img/google_apis/x86_64-35_r09.zip`(1.66 GB), + 解压到 `%LOCALAPPDATA%\Android\Sdk\system-images\android-35\google_apis\x86_64` + —— 注意压缩包里还有一层 `x86_64/`,要**把内容提上一层**,否则 AGP 找不到 `system.img`。 +2. 建 AVD 时把用户目录指到工程内(`~/.android` 在受限用户下不可写): + ```powershell + $env:ANDROID_USER_HOME="$repo\.local\android-user" + $env:ANDROID_AVD_HOME="$repo\.local\android-avd" + avdmanager create avd -n dadao -k "system-images;android-35;google_apis;x86_64" -d pixel_6 --force + ``` +3. 启动必须带 `-no-window -gpu swiftshader_indirect`(无头环境下 GL 走软件渲染); + `emulator -accel-check` 应显示 `WHPX ... installed and usable`。 + +驱动模拟器点按不要凭截图猜坐标——`adb shell input tap` 用的是**物理像素**,而截图常被降采样。 +用 `adb shell uiautomator dump` 拿 `bounds` 最稳。另外 `adb shell input text` 打不了中文(会 NPE), +中文数据走接口造。 + ## 已知待办 + - 工单页还没做(Web 已有):需要"我指派的 / 我发起的"两栏 + 接收/开工/提交/验收按钮; 接口在 `Api.kt` 里已经齐了(`tickets` / `ticketAction` / `verifyTicket` / `promote`)。 - 明文令牌存储(`TokenStore` 里已标 TODO)。 diff --git a/android/app/src/main/java/com/dadao/task/data/Api.kt b/android/app/src/main/java/com/dadao/task/data/Api.kt index ada826c..18dcf9f 100644 --- a/android/app/src/main/java/com/dadao/task/data/Api.kt +++ b/android/app/src/main/java/com/dadao/task/data/Api.kt @@ -163,6 +163,21 @@ data class TicketPage( @SerialName("has_more") val hasMore: Boolean = false, ) +/** 工作区/成员列表的外层容器:服务端统一返回 `{"items": [...]}`。 */ +@Serializable +data class WorkspacePage( + val items: List = emptyList(), + @SerialName("next_cursor") val nextCursor: String? = null, + @SerialName("has_more") val hasMore: Boolean = false, +) + +@Serializable +data class MemberPage( + val items: List = emptyList(), + @SerialName("next_cursor") val nextCursor: String? = null, + @SerialName("has_more") val hasMore: Boolean = false, +) + /** 知识的附带信息,与 backend/app/api/knowledge.py 的 `_doc_out` 对齐。 */ @Serializable data class KnowledgeInfo( @@ -293,7 +308,13 @@ class Api(private val tokens: TokenStore) { suspend fun me(): MeResult = call(path = "/auth/me", auth = true) - suspend fun workspaces(): List = call(path = "/workspaces", auth = true) + /** + * 列表类接口服务端返回的是 `{"items": [...]}` 而不是裸数组 + * (`backend/app/api/workspaces.py` 的 list_workspaces), + * 直接按 List 解析会得到 "响应格式异常(HTTP 200)"。 + */ + suspend fun workspaces(): List = + call(path = "/workspaces", auth = true).items suspend fun createWorkspace(name: String): Workspace = call( path = "/workspaces", @@ -302,7 +323,7 @@ class Api(private val tokens: TokenStore) { ) suspend fun members(workspaceId: String): List = - call(path = "/workspaces/$workspaceId/members", auth = true) + call(path = "/workspaces/$workspaceId/members", auth = true).items suspend fun createInvitation(workspaceId: String): Invitation = call(path = "/workspaces/$workspaceId/invitations", body = "{}", auth = true) diff --git a/android/scripts/emulator.ps1 b/android/scripts/emulator.ps1 new file mode 100644 index 0000000..6a1e2e4 --- /dev/null +++ b/android/scripts/emulator.ps1 @@ -0,0 +1,82 @@ +# 启动本机模拟器并装上 debug APK。 +# +# 之所以写这个脚本而不是让人手敲:这台机器上 Gradle 与 emulator 都需要几个 +# 环境变量(受限沙箱下 ~/.android 不可写、%TEMP% 不可写),漏一个就报 +# "AccessDeniedException" 或 "Could not connect to Kotlin compile daemon"。 +# +# pwsh -File android/scripts/emulator.ps1 # 只启动模拟器 +# pwsh -File android/scripts/emulator.ps1 -Install # 顺带编译并安装 APK +# +# 首次使用前需要有: +# - JDK 17(默认 .local/jdk17/...,可用 -JdkHome 覆盖) +# - Android SDK platform 35 + system-images;android-35;google_apis;x86_64 +# - 已创建的 AVD(默认名 dadao,可用 -AvdName 覆盖) + +param( + [switch]$Install, + [string]$AvdName = "dadao", + [string]$JdkHome = "", + [string]$SdkHome = "$env:LOCALAPPDATA\Android\Sdk" +) + +$ErrorActionPreference = "Stop" +$repo = Split-Path -Parent (Split-Path -Parent $PSScriptRoot) # android/.. -> 仓库根 +$local = Join-Path $repo ".local" + +if (-not $JdkHome) { + $JdkHome = Join-Path $local "jdk17\jdk-17.0.21.0.2+1" +} +if (-not (Test-Path $JdkHome)) { throw "找不到 JDK 17:$JdkHome(用 -JdkHome 指定)" } +if (-not (Test-Path (Join-Path $SdkHome "platform-tools\adb.exe"))) { throw "找不到 Android SDK:$SdkHome" } + +# 这几个目录必须是可写的,否则 Gradle/Kotlin/AGP 会以各种 AccessDenied 失败 +$env:JAVA_HOME = $JdkHome +$env:GRADLE_USER_HOME = Join-Path $local "gradle-home" +$env:ANDROID_HOME = $SdkHome +$env:ANDROID_SDK_ROOT = $SdkHome +$env:ANDROID_USER_HOME = Join-Path $local "android-user" +$env:ANDROID_AVD_HOME = Join-Path $local "android-avd" +$env:TEMP = Join-Path $local "tmp" +$env:TMP = $env:TEMP +foreach ($d in @($env:GRADLE_USER_HOME, $env:ANDROID_USER_HOME, $env:ANDROID_AVD_HOME, $env:TEMP)) { + New-Item -ItemType Directory -Force -Path $d | Out-Null +} + +$adb = Join-Path $SdkHome "platform-tools\adb.exe" +$emulator = Join-Path $SdkHome "emulator\emulator.exe" + +if ($Install) { + Push-Location (Join-Path $repo "android") + try { + & (Join-Path $local "gradle-8.13\bin\gradle.bat") :app:assembleDebug --no-daemon --console=plain + } finally { + Pop-Location + } +} + +& $adb start-server | Out-Null +$running = & $adb devices | Select-String "emulator-\d+\s+device" +if (-not $running) { + Write-Host "启动模拟器 $AvdName ..." + Start-Process -FilePath $emulator -ArgumentList @( + "-avd", $AvdName, "-no-window", "-no-audio", "-no-boot-anim", + "-no-snapshot", "-gpu", "swiftshader_indirect" + ) -WindowStyle Hidden +} + +Write-Host "等待开机完成 ..." +for ($i = 0; $i -lt 60; $i++) { + Start-Sleep -Seconds 5 + $boot = (& $adb shell getprop sys.boot_completed 2>$null) -join "" + if ($boot.Trim() -eq "1") { break } +} +if ($boot.Trim() -ne "1") { throw "模拟器 5 分钟内没起来,看 $local\emulator.log" } + +if ($Install) { + $apk = Join-Path $repo "android\app\build\outputs\apk\debug\app-debug.apk" + & $adb install -r $apk + & $adb shell am start -n com.dadao.task/.MainActivity | Out-Null + Write-Host "已安装并启动。后端要在 8100 端口,模拟器通过 10.0.2.2 访问宿主机。" +} else { + Write-Host "模拟器就绪。" +}