7.7 KiB
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 且响应 JSONcode == 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:
{
"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_idsecretenabledsendFullSmsBodyForDebug,默认 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
- 完成本 OpenSpec 并 validate。
- 新增 OkHttp 依赖和
INTERNET权限。 - 实现签名、请求体构造、响应解析和异步发送模块。
- 增加 JSON 配置 store 和最近推送状态。
- 在
MainActivity增加配置、测试发送和状态展示。 - 将验证码解析成功后的推送接入异步链路。
- 增加聚焦单元测试,不要求本轮编译。
Open Questions
- webhook id 和 secret 是否只用于本机调试,还是需要后续提供导入/导出配置。
- 默认推送内容是否只发验证码,还是需要包含发送方掩码和解析策略。
- 是否需要推送所有收到的字符串,还是只在验证码解析成功时推送。
- 如果网络失败,是否需要后续补发;本轮建议不做持久化补发队列。