2026-05-18 11:10:52 +08:00

189 lines
7.7 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

## Context
用户给出的 Python 实现包含两个核心函数:
- `generate_sign(secret, timestamp)`:签名字符串为 `timestamp + "\n" + secret`,把这个完整字符串作为 HMAC key消息体为空字节数组算法为 SHA-256结果做 Base64 编码。
- `push_markdown(markdown_content, webhook_id, secret)`:构造飞书 interactive markdown 卡片,加上 `timestamp``sign`10 秒超时 POST 到飞书 webhookHTTP 200 且响应 JSON `code == 0` 表示成功。
当前 Android 工程是 Java + Android Gradle Plugin 7.2.2`minSdkVersion=21``targetSdkVersion=30`。工程没有 Kotlin、Retrofit、OkHttp 或协程依赖。为了把需求做小、做稳,首版应选择 Java + OkHttp + `org.json`,不引入 Retrofit 接口层。
## Goals / Non-Goals
**Goals:**
- 先形成完整 spec不直接写代码。
- Android 侧等价实现 Python 已有飞书 webhook 推送协议。
- 推送模块独立于短信接收模块,方便单元测试和手动测试。
- 网络请求后台执行,避免阻塞主线程和短信广播处理。
- 提供清晰的本地诊断结果让用户知道是签名、配置、网络、HTTP、JSON 还是飞书业务错误。
- 推送内容包含短信原文,便于当前调试远端接收结果。
**Non-Goals:**
- 不改造服务端协议。
- 不做重试队列、离线持久化队列或 WorkManager 长任务调度。
- 不把网络发送成功作为短信接收成功的前置条件。
- 不把 webhook secret 写死到源码或提交到版本库。
- 不要求本轮编译。
## API Strategy
### 1. Android 签名算法保持 Python 等价
Python 逻辑的关键点不是常见的“secret 作为 key、timestamp 作为 message”而是
- `string_to_sign = f"{timestamp}\n{secret}"`
- HMAC key 为 `string_to_sign.encode("utf-8")`
- HMAC message 为空字节数组
- digest 为 SHA-256
- 输出 Base64 字符串
Android 侧应实现:
- `String stringToSign = timestampSeconds + "\n" + secret;`
- `Mac mac = Mac.getInstance("HmacSHA256");`
- `mac.init(new SecretKeySpec(stringToSign.getBytes(StandardCharsets.UTF_8), "HmacSHA256"));`
- `byte[] signData = mac.doFinal(new byte[0]);`
- `Base64.encodeToString(signData, Base64.NO_WRAP);`
签名单元测试必须固定 `secret``timestamp`,用 Python 参考算法生成期望值,防止迁移时误用 key/message。
### 2. 请求体保持 Python 结构
请求 JSON
```json
{
"msg_type": "interactive",
"card": {
"elements": [
{
"tag": "markdown",
"content": "..."
}
]
},
"timestamp": "秒级时间戳字符串",
"sign": "Base64签名"
}
```
Android 侧用 `JSONObject` / `JSONArray` 构造,避免手写 JSON 字符串。`timestamp` 保持字符串类型,与 Python 实现一致。请求头设置 `Content-Type: application/json; charset=utf-8`
### 3. 网络库选择 OkHttp
首版推荐 OkHttp而不是 Retrofit
- 当前需求只有一个动态 webhook URL 的 POST。
- Retrofit 更适合多接口、固定 baseUrl、声明式 API这里会引入额外抽象但收益有限。
- OkHttp 可以直接设置 10 秒 connect/read/write/call timeout并明确拿到 HTTP 状态码和响应体。
- Java 工程接入成本低,测试时可用 `MockWebServer`,也可以先只做纯 Java 请求体和响应解析测试。
如果后续服务器接口变多,再引入 Retrofit。
### 4. 线程模型
`BroadcastReceiver.onReceive` 生命周期短,不能同步执行网络请求。后续实现应采用:
- `FeishuWebhookClient.pushMarkdownAsync(...)` 提交到后台 `ExecutorService`
- `SmsReceiver` 在验证码解析成功、保存本地结果后,只触发异步推送,不等待网络结果。
- 推送结果写入 `SharedPreferences` 或轻量状态 store并通过本地广播刷新 UI。
- 若未来需要保证进程退出后仍发送,可另起 change 引入 WorkManager本轮不做。
### 5. 配置策略
首版本地配置文件:
- `webhook_id`
- `secret`
- `enabled`
- `sendFullSmsBodyForDebug`,默认 false
- 最近一次推送状态、时间、错误类型和错误摘要
飞书 webhook 配置保存到 `/sdcard/Android/data/{applicationId}/config/feishu.json`。如果 `feishu.json` 不存在app 生成默认模板 `/sdcard/Android/data/{applicationId}/config/def_config_feishu.json`,但不把模板当作生效配置。这样调试时可以直接改 `feishu.json`,不需要重新编译 Android 代码。
最近一次推送状态仍可保存在 `SharedPreferences`因为它不是动态调试配置。UI 展示时 secret 必须掩码,不在日志中打印。
### 6. 推送内容策略
验证码解析成功时,默认 markdown 内容建议包含:
- 验证码:解析出的 code。
- 来源:`system_sms_broadcast` / `sms_inbox_*`
- 发送方:掩码后的 sender。
- 时间:本地格式化时间。
- 解析策略和置信度:便于诊断。
默认包含完整短信原文,便于远端调试确认。仍不建议在 logcat 打印完整正文。
### 7. 响应解析和错误分类
成功条件:
- HTTP status 为 200。
- 响应体是 JSON。
- JSON `code` 为数字 0。
错误分类:
- `disabled`:用户未开启推送。
- `missing_config`:缺少 `webhook_id``secret`
- `sign_error`HMAC 算法或 key 初始化失败。
- `network_error`DNS、连接失败、TLS、Socket 异常等。
- `timeout`:请求超过 10 秒。
- `http_error`HTTP status 非 200。
- `invalid_json`:响应不是合法 JSON。
- `api_error`:飞书返回 `code != 0`,记录 code 和 msg。
日志策略:
- 可以记录错误类型、HTTP status、飞书 code/msg。
- 不记录 secret、sign、完整 webhook URL。
- 不默认记录完整 markdown 内容。
## Privacy Boundaries
- 默认仅上传验证码和必要诊断摘要。
- 飞书推送默认包含完整短信正文;开启推送即表示允许验证码短信离开设备。
- 不上传联系人通讯录、短信历史或设备标识。
- 本地保存 secret 时使用 `SharedPreferences` 只满足个人调试场景;如需更严格保护,应另起 change 使用 Android Keystore 加密。
- 推送到飞书意味着验证码会离开设备UI 应让用户明确知道远端推送已开启。
## Test Strategy
单元测试:
- `generateSign` 固定输入输出测试。
- 请求 JSON 构造测试,验证字段名、类型和嵌套结构。
- webhook URL 拼接测试,防止多余斜杠和空 id。
- 响应解析测试:
- `{"code":0,"msg":"success"}` 成功。
- `{"code":19021,"msg":"invalid sign"}` 返回 `api_error`
- 空响应、HTML、非法 JSON 返回 `invalid_json`
- 配置缺失测试。
手动验证:
- 在 UI 输入 webhook id 和 secret点击“测试推送”。
- 使用真实飞书机器人确认收到 markdown 卡片。
- 故意填错 secret确认 UI 显示飞书业务错误而不是短信接收失败。
- 断网或飞行模式下测试 `network_error`/`timeout`
- 收到真实验证码短信后,确认本地结果先保存,远端推送状态独立更新。
## Rollout Plan
1. 完成本 OpenSpec 并 validate。
2. 新增 OkHttp 依赖和 `INTERNET` 权限。
3. 实现签名、请求体构造、响应解析和异步发送模块。
4. 增加 JSON 配置 store 和最近推送状态。
5.`MainActivity` 增加配置、测试发送和状态展示。
6. 将验证码解析成功后的推送接入异步链路。
7. 增加聚焦单元测试,不要求本轮编译。
## Open Questions
- webhook id 和 secret 是否只用于本机调试,还是需要后续提供导入/导出配置。
- 默认推送内容是否只发验证码,还是需要包含发送方掩码和解析策略。
- 是否需要推送所有收到的字符串,还是只在验证码解析成功时推送。
- 如果网络失败,是否需要后续补发;本轮建议不做持久化补发队列。