## 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 到飞书 webhook,HTTP 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 是否只用于本机调试,还是需要后续提供导入/导出配置。 - 默认推送内容是否只发验证码,还是需要包含发送方掩码和解析策略。 - 是否需要推送所有收到的字符串,还是只在验证码解析成功时推送。 - 如果网络失败,是否需要后续补发;本轮建议不做持久化补发队列。