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

7.7 KiB
Raw Blame History

Context

用户给出的 Python 实现包含两个核心函数:

  • generate_sign(secret, timestamp):签名字符串为 timestamp + "\n" + secret,把这个完整字符串作为 HMAC key消息体为空字节数组算法为 SHA-256结果做 Base64 编码。
  • push_markdown(markdown_content, webhook_id, secret):构造飞书 interactive markdown 卡片,加上 timestampsign10 秒超时 POST 到飞书 webhookHTTP 200 且响应 JSON code == 0 表示成功。

当前 Android 工程是 Java + Android Gradle Plugin 7.2.2minSdkVersion=21targetSdkVersion=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);

签名单元测试必须固定 secrettimestamp,用 Python 参考算法生成期望值,防止迁移时误用 key/message。

2. 请求体保持 Python 结构

请求 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_idsecret
  • sign_errorHMAC 算法或 key 初始化失败。
  • network_errorDNS、连接失败、TLS、Socket 异常等。
  • timeout:请求超过 10 秒。
  • http_errorHTTP 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 是否只用于本机调试,还是需要后续提供导入/导出配置。
  • 默认推送内容是否只发验证码,还是需要包含发送方掩码和解析策略。
  • 是否需要推送所有收到的字符串,还是只在验证码解析成功时推送。
  • 如果网络失败,是否需要后续补发;本轮建议不做持久化补发队列。