# zcbot 移动 App 套壳对接

> 给 **platform 工程**:把 zcbot 控制台打包成移动 App。
> 架构:**原生壳(WebView)+ 原生登录页**。登录调 platform 自家接口换出 zcbot JWT,WebView 加载 zcbot 控制台时把 token 放 URL fragment 带入;token 失效时 zcbot 跳回 `relogin_url`,原生壳拦截后静默换新 token 重进。zcbot 前端不打包进 App,前端迭代不发版。

---

## 1. 登录链路(platform 自有接口,2026-07-21 实测可用)

```
GET  /api/login/check_captcha_required/<username>   → {require_captcha: bool}
GET  /api/login/captcha?key=<随机串>                 → 验证码图(仅 require_captcha 时)
POST /api/login/token                                → platform access_token
     form: username, password, key(必填,即使无验证码), grant_type=password [, captcha_text]
POST /api/login/external-login                       → zcbot JWT
     header: Authorization: Bearer <access_token>
```

`external-login` 返回的 `data` 字段:

```json
{"token":"<zcbot JWT>", "user_id":"<uuid>", "name":"...", "user_name":"...",
 "expires_at":"...", "ttl_seconds":604800}
```

其中 `token` / `user_id` 就是下一步进入 zcbot 所需的全部。

---

## 2. zcbot 进入契约

拿到 JWT 后,WebView 加载:

```
https://<zcbot 域名>/static/dev.html?embed=1&relogin_url=<回登录入口的地址>#token=<JWT>&user_id=<UUID>
```

- **fragment**:`token` / `user_id` 两个字段都必填;zcbot 读完立即清掉,不留在地址栏和历史。
- **`relogin_url`**:必须是带 scheme 的绝对地址。原生壳推荐自定义 scheme(如 `zcbotapp://relogin`);`javascript:` 等可执行 scheme 会被拒。
- **401 行为**:zcbot JWT 失效时页面 `location.replace(relogin_url)`。原生壳在 `shouldOverrideUrlLoading`(Android)/ `WKNavigationDelegate`(iOS)拦截该跳转 → 用缓存的 platform token 重调 `external-login` → 带新 fragment 重新 loadUrl。
- **token 生命周期**:zcbot JWT 7 天 TTL + 滑动续签(活跃期间自动续,页面内已处理)。App 每次冷启动都换一张新 JWT,实际只有 platform token 过期(约 30 天)才需要用户重输密码。
- 可选 `&task_id=<uuid>`:进入后自动定位到该 task。

**冷启动静默登录**:壳启动 → Keystore/Keychain 里有 platform token → 后台调 `external-login` → 直接 loadUrl 进控制台,用户无感;没有或已失效 → 显示原生登录页。

---

## 3. 原生壳杂活清单

| 项 | Android | iOS |
|---|---|---|
| 文件上传 | `WebChromeClient.onShowFileChooser` | 自带 |
| 文件下载 | `DownloadListener` → `DownloadManager` | `WKDownloadDelegate`(iOS 14.5+) |
| 返回键 | `canGoBack() ? goBack() : 退出确认` | 手势自带 |
| DOM storage | `setDomStorageEnabled(true)`(必须) | 自带 |
| 键盘顶起 | `windowSoftInputMode="adjustResize"` | 自带 |
| 外链 | 非 platform/zcbot 域交给系统浏览器 | 同左 |
| PDF/PPT/HTML 预览 | zcbot 前端内置渲染，无需原生 PDF 组件 | 同左 |

SSE 流式输出两端 WebView 原生支持,无需处理。iOS 上架提醒:纯套壳有 App Store 4.2(最小功能)被拒风险;企业分发 / TestFlight / 仅 Android 无此问题。

URL 白名单只处理**主 frame**导航。Android 检查 `request.isForMainFrame`,iOS 检查 `navigationAction.targetFrame?.isMainFrame`;不要拦截 zcbot 页面内部的 worker 或子 frame 请求，否则 PDF.js worker、HTML sandbox 等站内预览仍会白屏。

---

## 4. 备选:H5 登录页

不想写原生登录 UI 的话,可在 platform 域上放一个 `app.html`(登录表单 + 调自家接口,同源无 CORS),换到 JWT 后 `location.replace` 按 §2 契约跳进 zcbot,`relogin_url` 填 `app.html` 自己的地址(401 跳回来时若 platform token 还在缓存,静默重签再跳回,用户无感)。契约完全相同,只是登录层从原生换成网页。

---

## 5. 安全要点

1. **`PLATFORM_KEY` 只在 platform 后端**(`external-login` 内部用),App / 前端代码里绝不出现。
2. **不存明文账密**。"记住登录"= 把 platform token 存 Keystore / Keychain,靠 token 生命周期免登,不缓存密码重放。
3. **建议 platform 侧修**:`POST /api/login/token` 响应的 `user` 对象把 `hashed_password` 原样下发了,应在序列化时剔除。
4. WebView 域名白名单:只放行 platform 与 zcbot 两个域 + 自定义 scheme,其余交系统浏览器。
