上一轮只是"能编译"。这一轮在模拟器上真装了、真登录、真点了,才发现两个问题:
1. workspaces() / members() 直接按 List 解析,但服务端列表接口返回的是
{"items": [...]}。表现就是登录成功后首页红字「响应格式异常(HTTP 200)」,
而且工作区永远显示为空。新增 WorkspacePage / MemberPage 容器类型。
2. 之前 Api.kt 的请求体全是没加 @Serializable 的普通 data class(上一提交已修),
这次把成员/工作区的解析也按同样思路收口。
环境侧(写进 android/README + 新增 android/scripts/emulator.ps1):
- sdkmanager 拉不到远端索引,系统镜像是直连 dl.google.com 手动装的;
压缩包里多一层 x86_64/,要把内容提上一层,否则 AGP 找不到 system.img
- AVD 放工程内(~/.android 受限不可写),建 AVD 与启动都靠 ANDROID_USER_HOME / ANDROID_AVD_HOME
- 无头启动要 -no-window -gpu swiftshader_indirect
- 点按坐标要拿 uiautomator dump 的 bounds,别拿降采样截图猜
- adb shell input text 打不了中文(NPE),中文数据走接口或直接写库
验证(模拟器 API 35,连 fnsvr-01 的真库):
登录 → 建工作区 ShipFast → 任务列表显示 4 条中文任务(状态、截止日都对)
→ 勾选完成,条目从「进行中」消失(乐观锁生效)→ 工单页、知识页均正常渲染。
截图放进 2.Design/screens/android-{login,tasks}.png。
This commit is contained in:
Binary file not shown.
|
After Width: | Height: | Size: 78 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 93 KiB |
@@ -24,12 +24,12 @@
|
|||||||
| 任务:收件箱→待办→进行中→完成、截止时间、标签、评论、事件流、乐观锁 | ✅ 后端 + Web 已通 |
|
| 任务:收件箱→待办→进行中→完成、截止时间、标签、评论、事件流、乐观锁 | ✅ 后端 + Web 已通 |
|
||||||
| Web 列表 / 看板拖拽 / 详情侧栏编辑 / 键盘流 / 快速录入语法 | ✅ e2e 7 项断言全绿,构建通过 |
|
| Web 列表 / 看板拖拽 / 详情侧栏编辑 / 键盘流 / 快速录入语法 | ✅ e2e 7 项断言全绿,构建通过 |
|
||||||
| 1000 条任务下的首屏性能 | ✅ 实测 P50 38–55ms(目标 < 300ms) |
|
| 1000 条任务下的首屏性能 | ✅ 实测 P50 38–55ms(目标 < 300ms) |
|
||||||
| 安卓端任务列表与详情 | ⚠️ 代码已写,**本机无 JDK/Gradle,未编译验证** |
|
| 安卓端任务 / 工单 / 知识三个页面 | ✅ 已编译成 APK(9.94 MB),并在模拟器上跑通登录→建工作区→任务勾选→工单→知识 |
|
||||||
| 工单:升级不搬数据、受理→办理→提交→验收、驳回、改派、站内通知 | ✅ 后端 + Web 已通,双人 e2e 8 项全绿 |
|
| 工单:升级不搬数据、受理→办理→提交→验收、驳回、改派、站内通知 | ✅ 后端 + Web 已通,双人 e2e 8 项全绿 |
|
||||||
| 知识:工单验收自动生成草稿、核对后发布、来源可追、手记 | ✅ 后端 + Web 已通,e2e 7 项全绿 |
|
| 知识:工单验收自动生成草稿、核对后发布、来源可追、手记 | ✅ 后端 + Web 已通,e2e 7 项全绿 |
|
||||||
| 后端测试 | ✅ 65 passed,ruff 干净 |
|
| 后端测试 | ✅ 66 passed,ruff 干净 |
|
||||||
| 知识切片与语义检索(pgvector) | ⬜ 配置已就绪(`deploy/docker-compose.vector.yml`),本机 Docker 起不来,未跑过 |
|
| 知识切片与语义检索(pgvector) | ⬜ 配置已就绪(`deploy/docker-compose.vector.yml`),本机 Docker 起不来,未跑过 |
|
||||||
| 安卓端知识页 / LLM 能力 | ⬜ M3 收尾 / M4 |
|
| LLM 能力(结构化录入、问答带引用、人物画像) | ⬜ M4 |
|
||||||
|
|
||||||
## 快速开始
|
## 快速开始
|
||||||
|
|
||||||
|
|||||||
@@ -9,6 +9,18 @@ BUILD SUCCESSFUL in 38s
|
|||||||
app/build/outputs/apk/debug/app-debug.apk 9.94 MB
|
app/build/outputs/apk/debug/app-debug.apk 9.94 MB
|
||||||
```
|
```
|
||||||
|
|
||||||
|
而且**真的在模拟器上跑通了**:登录 → 建工作区 → 任务列表(中文标题、状态、截止日、勾选完成)→ 工单列表 → 知识列表,全部打到真实后端。
|
||||||
|
|
||||||
|
| 登录 | 任务列表 |
|
||||||
|
|---|---|
|
||||||
|
|  |  |
|
||||||
|
|
||||||
|
一键起模拟器 + 装包(环境变量都替你设好了):
|
||||||
|
|
||||||
|
```bash
|
||||||
|
pwsh -File android/scripts/emulator.ps1 -Install
|
||||||
|
```
|
||||||
|
|
||||||
本机从零把编译链搭起来踩过的坑都固化在仓库里了,别再走一遍:
|
本机从零把编译链搭起来踩过的坑都固化在仓库里了,别再走一遍:
|
||||||
|
|
||||||
| 现象 | 原因 | 仓库里的解法 |
|
| 现象 | 原因 | 仓库里的解法 |
|
||||||
@@ -68,8 +80,30 @@ app/src/main/java/com/dadao/task/
|
|||||||
└── KnowledgeScreen.kt 知识四态 tab、手记、按状态给动作(发布需 owner/admin)
|
└── 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 已有):需要"我指派的 / 我发起的"两栏 + 接收/开工/提交/验收按钮;
|
- 工单页还没做(Web 已有):需要"我指派的 / 我发起的"两栏 + 接收/开工/提交/验收按钮;
|
||||||
接口在 `Api.kt` 里已经齐了(`tickets` / `ticketAction` / `verifyTicket` / `promote`)。
|
接口在 `Api.kt` 里已经齐了(`tickets` / `ticketAction` / `verifyTicket` / `promote`)。
|
||||||
- 明文令牌存储(`TokenStore` 里已标 TODO)。
|
- 明文令牌存储(`TokenStore` 里已标 TODO)。
|
||||||
|
|||||||
@@ -163,6 +163,21 @@ data class TicketPage(
|
|||||||
@SerialName("has_more") val hasMore: Boolean = false,
|
@SerialName("has_more") val hasMore: Boolean = false,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
/** 工作区/成员列表的外层容器:服务端统一返回 `{"items": [...]}`。 */
|
||||||
|
@Serializable
|
||||||
|
data class WorkspacePage(
|
||||||
|
val items: List<Workspace> = emptyList(),
|
||||||
|
@SerialName("next_cursor") val nextCursor: String? = null,
|
||||||
|
@SerialName("has_more") val hasMore: Boolean = false,
|
||||||
|
)
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
data class MemberPage(
|
||||||
|
val items: List<Member> = emptyList(),
|
||||||
|
@SerialName("next_cursor") val nextCursor: String? = null,
|
||||||
|
@SerialName("has_more") val hasMore: Boolean = false,
|
||||||
|
)
|
||||||
|
|
||||||
/** 知识的附带信息,与 backend/app/api/knowledge.py 的 `_doc_out` 对齐。 */
|
/** 知识的附带信息,与 backend/app/api/knowledge.py 的 `_doc_out` 对齐。 */
|
||||||
@Serializable
|
@Serializable
|
||||||
data class KnowledgeInfo(
|
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 me(): MeResult = call(path = "/auth/me", auth = true)
|
||||||
|
|
||||||
suspend fun workspaces(): List<Workspace> = call(path = "/workspaces", auth = true)
|
/**
|
||||||
|
* 列表类接口服务端返回的是 `{"items": [...]}` 而不是裸数组
|
||||||
|
* (`backend/app/api/workspaces.py` 的 list_workspaces),
|
||||||
|
* 直接按 List 解析会得到 "响应格式异常(HTTP 200)"。
|
||||||
|
*/
|
||||||
|
suspend fun workspaces(): List<Workspace> =
|
||||||
|
call<WorkspacePage>(path = "/workspaces", auth = true).items
|
||||||
|
|
||||||
suspend fun createWorkspace(name: String): Workspace = call(
|
suspend fun createWorkspace(name: String): Workspace = call(
|
||||||
path = "/workspaces",
|
path = "/workspaces",
|
||||||
@@ -302,7 +323,7 @@ class Api(private val tokens: TokenStore) {
|
|||||||
)
|
)
|
||||||
|
|
||||||
suspend fun members(workspaceId: String): List<Member> =
|
suspend fun members(workspaceId: String): List<Member> =
|
||||||
call(path = "/workspaces/$workspaceId/members", auth = true)
|
call<MemberPage>(path = "/workspaces/$workspaceId/members", auth = true).items
|
||||||
|
|
||||||
suspend fun createInvitation(workspaceId: String): Invitation =
|
suspend fun createInvitation(workspaceId: String): Invitation =
|
||||||
call(path = "/workspaces/$workspaceId/invitations", body = "{}", auth = true)
|
call(path = "/workspaces/$workspaceId/invitations", body = "{}", auth = true)
|
||||||
|
|||||||
@@ -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 "模拟器就绪。"
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user