# Troubleshooting 排错决策树 + FAQ

> 本文档随客户接入反馈持续更新。

## 1. 排错决策树

```mermaid
flowchart TD
    Start[客户反馈问题] --> Q1{自检页能跑通吗?}

    Q1 -- 否 --> SelfCheck[逐项确认 SDK 集成]
    Q1 -- 是 --> Q2{具体哪类问题?}

    SelfCheck --> SC1[① Manifest / Info.plist / module.json5 权限项是否齐全]
    SelfCheck --> SC2[② 自检页 URL 默认指向 beta,是否需切换?]
    SelfCheck --> SC3[③ 客户是否额外集成了 X5 / TBS 内核?]
    SelfCheck --> SC4[④ iOS SPM/xcframework 集成方式与导入写法是否正确?]

    Q2 --> A[相机/麦克风黑屏]
    Q2 --> B[Cookie 丢失/无登录态]
    Q2 --> C[白屏/页面打不开]
    Q2 --> D[qianapp 协议无回调]
    Q2 --> E[微信/支付宝拉不起来]
    Q2 --> F[PDF 链接异常]

    A --> A1{Manifest/Info.plist/module.json5 权限文案齐?}
    A1 -- 否 --> A_FIX[补全 NSCameraUsageDescription / CAMERA / RECORD_AUDIO / ohos.permission.CAMERA 等]
    A1 -- 是 --> A2{客户是否首次拒绝?}
    A2 -- 是 --> A_GUIDE[引导用户去系统设置 → 隐私 → 相机/麦克风 重新授权]

    B --> B1{客户是否手动关了第三方 Cookie?}
    B1 -- 是 --> B_FIX[SDK 强制开启,不开放关闭参数;移除自定义代码]
    B1 -- 否 --> B2[确认 Android 用 SDK 创建的 WebView,iOS 用 SDK 工厂的 Configuration]

    C --> C1{是否走 https?}
    C1 -- 否 --> C_FIX[开 usesCleartextTraffic / NSAllowsArbitraryLoadsInWebContent]
    C1 -- 是 --> C2{是否 SSL 错误?}
    C2 -- 是 --> C_SSL[SDK 永远 cancel SSL 错误,业务层弹错;联系客户域名 / 证书运营]

    D --> D1{是否拦截到了 qianapp 但未触发 onSignResult?}
    D1 -- 是 --> D_FIX[确认 SignCallback 注册时序在 startActivity 之前]
    D1 -- 否 --> D2[确认 H5 实际产出的 URL scheme 是 qianapp 而非自定义]

    E --> E1{externalSchemes 是否包含 weixin / alipays?}
    E1 -- 否 --> E_FIX[传 listOf的weixin alipays 或更多;覆盖语义不是追加]
    E1 -- 是 --> E2{Android queries 或 iOS LSApplicationQueriesSchemes 配齐?}
    E2 -- 否 --> E_QUERY[补 queries / LSApplicationQueriesSchemes]

    F --> F1{哪个端?}
    F1 -- Android --> F_AND[转 Intent.ACTION_VIEW;ActivityNotFoundException 由 onEvent DownloadFailed 上报]
    F1 -- iOS --> F_IOS{iOS 版本?}
    F_IOS -- iOS 14.5+ --> F_IOS_DL[SDK 自动下载到 tmp + 弹分享 sheet;失败自动 fallback Safari]
    F_IOS -- iOS < 14.5 --> F_IOS_OK[自动跳 Safari 下载]
    F1 -- 鸿蒙 --> F_HM[request.downloadFile 下载到缓存 + DocumentViewPicker.save 让用户选目标;失败上报 DownloadFailed]
```

## 2. 已踩坑速查(10 条)

| # | 症状 | 排查 |
|---|---|---|
| 1 | Android 11+ 微信拉不起 | Manifest `<queries>` 漏配 weixin scheme |
| 2 | iOS `UIApplication.open` 永远 false | Info.plist `LSApplicationQueriesSchemes` 漏配 |
| 3 | 人脸核身活体检测黑屏 | CAMERA / RECORD_AUDIO / NSCameraUsageDescription / ohos.permission.CAMERA 任一漏配 |
| 4 | 跨域 Cookie 丢失 | 客户自行 `setAcceptThirdPartyCookies(false)` 或换了非默认 WKProcessPool |
| 5 | iOS 下载文件时跳 Safari | iOS 14.3~14.4 设备无 WKDownloadDelegate,SDK 自动 fallback 到 Safari;升级到 iOS 14.5+ 即走原生下载 + 分享 sheet |
| 6 | Android 顶栏返回键不退出 | 客户 `OnBackPressedDispatcher` 抢了 callback,或在 SDK Activity 之外加了拦截 |
| 7 | iOS UA 注入后首页 404 | `customUserAgent` 时序竞争;SDK 已用 `loadURL` 排队,如客户绕过 SDK 直接 `webView.load` 会失序 |
| 8 | 客户 minify 后类找不到 | 客户没正确套用 SDK `consumer-rules.pro`(应该自动套用,核对 AGP 版本) |
| 9 | 鸿蒙 SPA 路由切换进度条卡死 | 1.0.4 已修复;确认 SDK 版本 ≥ 1.0.4 |
| 10 | 鸿蒙手动关闭容器不回调 EMPTY | 1.0.3 已修复;确认 SDK 版本 ≥ 1.0.3 |

## 3. FAQ

### Q1. 第三方 Cookie 是隐私问题吗?客户安全团队禁用怎么办?

**A**:本 SDK WebView 进程内 Cookie 与系统浏览器、其他 App **完全隔离**,无任何隐私互通。
关闭第三方 Cookie 会**直接破坏人脸核身**(人脸核身跨域跳第三方域名,不带 cookie 无法保持会话)。
SDK 故意不开放关闭参数,如客户安全团队仍坚持,请引导联系电子签 H5 团队评估替代方案。

### Q2. 客户安全团队不允许修改 UA 怎么办?

**A**:可在配置中设 `injectUserAgent = false`(三端均支持)。注意:
- H5 端 logger 无法识别本 SDK,排查难度增加
- 当排查时需要客户主动反馈 SDK 版本号

### Q3. SaaS 与集成版的区别?

**A**:本 SDK 是**集成版**,即 H5 集成到客户 App。SaaS 版不涉及客户 App 集成,
客户直接使用**腾讯电子签 SaaS 控制台**或**腾讯电子签小程序**完成签署。

### Q4. 微信/支付宝在 Android 11+ 偶发拉起失败?

**A**:Android 11 引入 package visibility 限制,需在 Manifest 声明 `<queries>`。
Demo 已包含完整示例,客户接入时**必须拷贝**。

### Q5. 各端 PDF/文件下载的行为是什么?

**A**:SDK 自动选择最优策略,无需配置:

- **Android**:走 `DownloadManager`(Android 10+ 无需存储权限)或转 `Intent.ACTION_VIEW` 兜底
- **iOS 14.5+**:使用 WKDownloadDelegate 下载到本地 → 弹系统分享 sheet(可保存到「文件」App / AirDrop / 分享到其他 App);下载失败自动 fallback 到 Safari
- **iOS 14.3~14.4**:直接跳 Safari 下载
- **HarmonyOS**:使用 `@ohos.request.downloadFile` 下载到缓存目录 → 弹 `DocumentViewPicker.save` 让用户选目标位置保存

客户可通过监听 SDK 下载事件了解下载状态(三端命名风格不同):Android 为 `DownloadStart` / `DownloadFailed`(无下载完成事件);iOS Swift 为 `downloadStart` / `downloadCompleted` / `downloadFailed`;HarmonyOS 为 `DownloadStart` / `DownloadCompleted` / `DownloadFailed`。

### Q6. 协议未来新增 action,老 SDK 客户怎么办?

**A**:`action` / `result` 是 String 直透,**老 SDK 不需要发版**。客户在 `when` / `switch`
的 `else` 分支会拿到原始未知字符串,可弹「请升级 App」之类提示。

### Q7. 客户工程已有 X5 / TBS,接入本 SDK 还能用吗?

**A**:**不保证兼容**。本 SDK 只适配 Android 系统 WebView 与 iOS WKWebView。
客户接入前需在电子签容器内禁用 X5(只让本 SDK 使用系统 WebView)。

### Q8. 为什么 Demo App 的 ATS 默认开放 http?

**A**:Demo 为方便打开本地网页默认开放 http(电子签网页本身走 https,不含 http 资源)。**生产环境**建议:
- iOS 删掉 `NSAllowsArbitraryLoadsInWebContent`
- Android 改 `usesCleartextTraffic="false"`

### Q9. 配置模式下能保留客户原有 client / delegate 吗?

**A**:**当前不支持** chained delegate。客户在 `Configurator.apply` 之前**不要**自行
赋值 `setWebViewClient` / `webView.navigationDelegate`,否则会被 SDK 替换。
后续将根据客户接入反馈排期支持。

### Q10. 人脸核身偶发"只能选已有视频不能录像"(`accept="video/kyc"` / `video/faceid`)

**A**:这是**人脸核身老录制模式**(早于 TRTC/WebRTC 的人脸核身链路),H5 在 WebRTC 不可用时会
降级为 `<input type="file" accept="video/kyc">`。如果 SDK 走普通文件选择器,用户**只能挑已有视频**,
活体直接失败。

**SDK 默认行为**(Android):识别 `accept` 命中 `["video/kyc", "video/faceid"]` 时,
自动改用 `MediaStore.ACTION_VIDEO_CAPTURE` 拉系统相机录像(10s 时长 + 高清)。

**客户侧需做**:
1. Manifest `<queries>` 必须包含 `<intent><action android:name="android.media.action.VIDEO_CAPTURE"/></intent>`
   (见 `integration-checklist.md` A.3,targetSdk 30+ 设备否则 resolveActivity 失败)
2. 不需要兜底:`TencentQianWebViewConfig(legacyFaceCaptureMimeTypes = emptyList())` 关闭
3. 协议演进新增 `video/livecheck` 之类:`legacyFaceCaptureMimeTypes = listOf("video/kyc", "video/faceid", "video/livecheck")` 追加

**iOS**:WKWebView 对 `<input accept>` 走系统统一文件选择器,无 capture intent 概念,无需此兜底。

**HarmonyOS**:ArkWeb 的 `onShowFileSelector` 同样无 capture intent 概念,默认走系统文件选择器。SDK 识别 `isCapture` 为 true 时改用 `@ohos.multimedia.cameraPicker.pick()` 拉系统相机录像。**鸿蒙端未提供 `legacyFaceCaptureMimeTypes` 配置项**(此兜底开关仅 Android 有)。

### Q11. 拍证件 / 自拍录像 `<input capture="user|environment">` 用户只能"挑已有文件",拉不起相机

**A**:这是 Android `WebView` 与 iOS `WKWebView` 的**核心模型差异**:
- iOS `WKWebView` 自 iOS 14 起内核**原生支持** `<input capture>`,自动拉对应镜头
- Android `WebView` 至 2026 仍**完全忽略** `capture` 属性,默认弹文件选择器

**SDK 默认行为**(Android,自 D18 起):识别 `params.isCaptureEnabled == true` 时,根据 `accept`
自动拉系统相机(`image/*` → `ACTION_IMAGE_CAPTURE`、`video/*` → `ACTION_VIDEO_CAPTURE`)。
**启动相机前自动申请 `CAMERA` 运行时权限**(Android 6+),用户授权后拉相机,拒绝则 fallback 到普通文件选择器(不阻断流程)。

**镜头偏好**(电子签自拍场景占多数,默认前置):

| 场景 | 客户配置 | SDK 行为 |
|---|---|---|
| 自拍活体 / 自拍头像(默认) | `cameraCaptureBehavior = PREFER_FRONT` | 拉系统相机 + 前置镜头 extra 暗示 |
| 拍身份证 / 营业执照 | `cameraCaptureBehavior = PREFER_BACK` | 拉系统相机 + 后置镜头 extra 暗示 |
| 让系统自己挑 | `cameraCaptureBehavior = SYSTEM_DEFAULT` | 拉系统相机,不指定镜头 |
| 完全关闭 SDK 干预(走旧行为) | `cameraCaptureBehavior = DISABLED` | 仍走普通文件选择器 |

> **镜头偏好的局限**:`CAMERA_FACING` / `LENS_FACING_FRONT` / `USE_FRONT_CAMERA` 是私有 extra,
> 不同 OEM 行为不一致(三星 / 小米 / 华为各家略有差异),SDK 全部带上做"尽力而为"暗示,但**不强保**命中。
> 即使 OEM 完全忽略,系统相机仍能拉起,只是镜头方向由系统决定。

**优先级**:`capture` 属性 > 老协议 MIME 兜底(Q10) > 默认文件选择器。
两者同时存在时(`accept="video/kyc" capture="user"`)**走 capture 分支**,不附带 10s 时长限制。

**iOS**:无需任何配置;WKWebView 内核原生处理。

**HarmonyOS**:ArkWeb 与 Android WebView 类似,默认忽略 `capture` 属性弹文件选择器。SDK 识别 `isCapture` 为 true 时用 `@ohos.multimedia.cameraPicker.pick()` 拉系统相机,并在拉起前申请 `ohos.permission.CAMERA` 运行时权限。**鸿蒙端未提供 `cameraCaptureBehavior` 配置项**(镜头偏好仅 Android 支持)。

### Q12. Android 15 (targetSdk 35) 配置模式下页面内容顶到状态栏/被导航栏遮挡

**A**：Android 15 在 `targetSdk = 35` 时强制启用 edge-to-edge，App 内容默认会绘制到状态栏与导航栏之下，
且 `setDecorFitsSystemWindows(true)` / XML `fitsSystemWindows` 均被系统忽略，**退回传统窗口模式已不可靠**。

**容器模式**（`TencentQianWebView.open`）：SDK 已内部处理（全版本统一 E2E：系统栏 / 挖孔区域自动避让，
软键盘弹出自动顶起内容），无需客户操作。

**配置 / 组件模式**（`TencentQianWebViewConfigurator.apply` / `TencentQianWebViewComponent`）：
宿主持有布局，SDK 不主动修改（避免与宿主自身 E2E 处理叠加出双 padding）。
在 `setContentView` 后对根布局调用一行，即可接入与容器模式同款的避让：

```kotlin
// Kotlin
import com.tencent.qian.webview.internal.applyEdgeToEdgeInsets

setContentView(R.layout.your_layout)
applyEdgeToEdgeInsets(findViewById(R.id.your_root))
```

```java
// Java
import com.tencent.qian.webview.internal.EdgeToEdge;

setContentView(R.layout.your_layout);
EdgeToEdge.apply(findViewById(R.id.your_root));
```

效果：系统栏 / 挖孔 inset 叠加为根布局 padding（底部取系统栏与键盘高度较大值，键盘弹出自动避让 H5 输入框）；
同时清零下发给 WebView 的系统栏 inset，H5 页面 CSS `env(safe-area-inset-*)` 归零，
避免与 SDK 避让叠加产生双重 padding（底部灰带）。

**注意事项**：

- 若宿主已有自己的 E2E / WindowInsets 处理，**不要**再调用本方法，否则会叠加出双 padding。
- E2E 下 manifest `android:windowSoftInputMode="adjustResize"` 变为 no-op（仅用于保证 compat 正常下发 IME insets），
  键盘避让已由上述 inset 逻辑接管，无需额外配置。
- 浅色界面建议配置深色状态栏文字（主题 `windowLightStatusBar` 或 `WindowInsetsControllerCompat`），
  API < 23 不支持深色状态栏文字（恒为白色），可参考 Demo `applyDemoStatusBarStyle` 垫半透明蒙层保证可读。

### Q13. iOS SPM 集成 OC 版后 `#import <TencentQianWebView/...>` 编译失败

**A**：SPM 的 C/OC target 没有 framework bundle 结构，不提供 `TencentQianWebView/` 目录前缀，因此 framework-style 导入不可用。

| 集成方式 | OC 消费者导入写法 |
|---------|----------------|
| SPM 本地包 | `@import TencentQianWebView;`（模块导入，推荐） |
| xcframework | `#import <TencentQianWebView/TencentQianWebView.h>`（framework-style） |

`@import` 通过 Clang module 系统解析，两种集成方式都可用，是最通用的写法。

### Q14. iOS SPM 集成 OC 版后本地化文案显示 key 而非中文（如 `qian_compatibility_test_title`）

**A**：SPM 的 C/OC target 不支持 `resources` 参数，`.strings` 文件不会被打包到 SPM 模块的 bundle 中。

SDK 已内置三层 fallback 机制：

```
1. bundleForClass:  → Framework 集成（.strings 在 framework 内）
2. Bundle.main      → SPM 集成 + 客户将 .strings 加入 App Copy Bundle Resources
3. hardcoded        → SPM 集成零配置兜底（内置中文默认值）
```

零配置下 SDK 自动 fallback 到 hardcoded 中文，功能正常。如需多语言支持，将 `ios-objective-c/TencentQianWebView/Sources/Resources/` 下的 `.strings` 文件拖入 App target 的 Copy Bundle Resources。

### Q15. H5 底部 fixed 弹框中的输入框被软键盘遮挡（三端键盘避让机制）

**A**：Android / 鸿蒙 / iOS 三端容器模式均已内建键盘避让，语义一致——键盘弹出时 Web 容器的
layout viewport 真正收缩，H5 `position: fixed; bottom: 0` 弹框贴在键盘上方，键盘收起后布局复原：

| 平台 | 机制 |
|------|------|
| Android | 统一 E2E + 手动 insets：根布局底 padding 取 `max(系统栏, 键盘)`，IME inset 下发 Chromium（见 Q12） |
| HarmonyOS | ArkWeb `keyboardAvoidMode(RESIZE_CONTENT)`（API 12+），Web 组件高度收缩，容器页顶栏不动 |
| iOS | 底部安全区原生避让（webView 不伸到 Home Indicator 下方，H5 `env(safe-area-inset-bottom)` 恒为 0，无双重避让）；iOS 15+ webView 底部接 `keyboardLayoutGuide.topAnchor`（系统托管、动画同步）；iOS 14.x 降级监听 `keyboardWillChangeFrameNotification` 锚定安全区上沿调整约束；键盘弹出动画窗口内冻结 contentOffset.y（抑制 WebKit 聚焦 reveal 使用过期几何造成的「掉-弹」抖动，用户手势滚动豁免）；键盘动画完成后自动补滚聚焦的流式 input / textarea 进可视区（WebKit 只在 focus 瞬间 reveal 一次且 frame 收缩后不重滚，需 SDK 补偿；iOS 15.4+ 用 `nearest` 最小滚动，输入框已完全可见时页面不再被无谓移动） |

**组件 / 配置模式**：**Android** 宿主可对根布局调用 `applyEdgeToEdgeInsets` 一行接入（见 Q12）；
**HarmonyOS** 组件模式已内建 `RESIZE_CONTENT`（避让发生在组件自身 Web 区域，不碰宿主布局），
配置模式需核对自有 `Web` 组件的 `keyboardAvoidMode`——**ArkWeb 默认值即 `RESIZE_CONTENT`**，
未显式覆盖则天然正确，仅需确认没有改为 `OFFSET` / `RESIZE_VISUAL` 等其他模式（建议显式绑定
固化语义，见 [integration-checklist C.4](./integration-checklist.md#c4-web-组件链式属性配置模式--自行适配必查)）；
**iOS** 组件 / 配置模式 SDK 不主动修改宿主布局，宿主可一行接入同款避让：
Swift `KeyboardAvoidance.apply(webView:in:)` / OC `[TQKeyboardAvoidance applyToWebView:inView:]`
（安装后不要再为该视图添加其他底部约束；安装即自动获得同款 contentOffset 冻结防抖与聚焦补滚，无需额外调用；返回句柄可 `remove()` 提前解除，默认随宿主视图释放；重复 apply 会替换旧安装，对过期句柄调 `remove()` 是安全 no-op）。

**H5 侧适配建议**（SDK 管 resize 语义，fixed 定位细节由 H5 负责）：

- meta viewport 建议加 `interactive-widget=resizes-content`（Chromium 系，键盘弹出同步收缩 layout viewport）；
- 用 `100dvh` 替代 `100vh`，避免视口高度不含键盘导致布局溢出；
- fixed 底栏需要精细定位时使用 `visualViewport` API 监听视口变化。

**已知限制（跨端内核）**：**iframe + 底部输入框**组合不保证避让正确——iframe 内 fixed 元素相对
iframe 视口定位，顶层 resize 不传导；iOS 对 iframe 内 input 的自动滚动亦有历史缺陷。H5 应避免
在 iframe 内放置底部输入框。

