Air780E 多卡短信中枢(五):从 CRUD 后台到短信会话,再到安全公开仓库

最后更新:2026-08-04
所属系列:Air780E 多卡短信中枢实战 · 第 5 / 6 篇
文章目录 15 个章节

项目的第一版 Web 页面已经能完成 CRUD:看短信、发短信、管理 SIM、编辑规则和任务。但“功能能点”与“日常愿意用”之间还有很大距离。

短信是高频、上下文强、经常在手机上查看的内容。把它做成普通后台表格,会让回复、未读、验证码复制和跨卡查找都很别扭。后续前端迭代的核心,就是把管理后台重新组织成短信工具,同时保证桌面和移动端都能用。

功能完成后还有最后一关:仓库要公开。短信系统天然会接触号码、ICCID、IMEI、Webhook、Cookie 和消息正文,公开发布不能只看当前文件,还要检查 Git 历史和提交身份。

一、第一版为什么选择“先 CRUD,后体验”

早期页面只承担一个任务:把端到端链路跑通。

  • 登录后能看设备与 SIM;
  • 能看到收到的短信;
  • 能选择设备主动发送;
  • 能管理通知渠道、规则和保号任务。

这个阶段先用普通列表和表单是正确的。如果硬件、协议和数据模型还在变化,就提前雕琢聊天气泡、动效和移动端布局,返工成本会非常高。

但底层稳定以后必须承认:短信列表不是最终交互。按时间平铺的记录无法回答“我和这个号码之前说过什么”,也不适合边看边回复。

二、短信页从记录列表改成会话模型

最终页面拆成两层:

左侧:会话列表
  - SIM
  - 对方号码
  - 最后一条摘要
  - 时间
  - 未读数量

右侧:当前会话
  - 收发气泡
  - 日期分隔
  - 验证码高亮与复制
  - 原地回复

会话身份不能只用号码,还要包含 sim_id

(sim_id, peer)

同一个运营商号码可能同时给两张卡发消息。如果只按 peer 聚合,两张卡的验证码会混到同一会话里。

未读也遵循同一边界。打开某个会话时,只把该 SIM 与该号码的入站消息标记已读,不能把另一张卡或其他号码一起清掉。

三、未读计数看似简单,实际跨越三层

导航栏角标需要一个廉价的总数接口,会话列表又需要每个 thread 的未读数,打开会话还要立即更新状态。

完整链路包括:

  1. messages.read_at 保存读取状态;
  2. 会话聚合查询计算每组未读;
  3. 独立接口返回全局未读总数;
  4. 前端打开会话后调用 mark-read;
  5. 导航栏定期刷新,并处理页面卸载后的异步返回。

这里踩过一个 React Hooks 顺序问题:某些回调和 effect 放在条件返回之后,加载态与正常态之间切换时 Hook 数量不同。修复不是压住 lint,而是让 Hook 始终无条件声明,把条件放进 effect 或回调内部。

这也是前端状态复杂度的典型来源:数据结构很小,但它同时出现在布局、路由、异步请求和可见状态中。

四、搜索和导出不能只做“当前页”

全文搜索支持按号码和正文查找。搜索结果点击后要回到对应会话,并把那条消息放进上下文,而不只是展示一条孤立记录。

CSV 导出更容易做出一个“按钮能下载、内容却不完整”的假功能。最初的消息查询默认有分页上限,如果复用同一接口,导出的只是前若干条。

最终导出允许显式无上限查询,并保留与列表相同的过滤条件。后端的 count_messages 也必须接受相同过滤器,否则页面会出现:

结果列表是搜索后的 12 条
总数却显示数据库全部 2000 条

CSV 使用 UTF-8 BOM,保证常见表格软件打开中文时不乱码。

这类问题的经验是:

“列表、计数、导出”是同一个查询契约的三种表现,过滤逻辑不能各写一份。

五、通知规则调试器必须显示最终载荷

通知配置最难排查的不是 HTTP,而是“为什么这条短信没匹配”或“模板最后会长什么样”。

因此页面增加了规则调试器:选择一张 SIM,填写发件号码和测试正文,Server 返回:

  • 命中的渠道;
  • 实际采用的规则;
  • 规则优先级;
  • 最终标题;
  • 最终正文。

它不访问 Bark、Telegram 或飞书,但调用与真实投递相同的规则匹配和渲染函数。

如果调试器只在前端模拟,时区、卡名回退、未知占位符和渠道去重都可能与真实结果不同。一个可靠的“预览”应该是生产逻辑的无副作用入口,而不是另一套近似实现。

六、仪表盘应该展示决策信息,而不是堆卡片

仪表盘最终保留几类信息:

  • 在线设备、SIM 和未读总数;
  • 7 / 30 / 90 天短信趋势;
  • 每个模块的运营商、注册状态和存储占用;
  • RSSI / RSRP / RSRQ 信号曲线;
  • 最近短信和保号任务状态。

状态采样在 Agent 侧先去噪,Server 只保存有意义的变化。否则一分钟一个样本、多个模块长期运行,曲线会被几乎相同的数据灌满。

图表还要区分不同信号指标的量纲。RSSI 可以映射成 dBm 和信号格,但 RSRP / RSRQ 不能用同一颜色阈值硬套。UI 不是把后端字段全部画出来,而是明确每个值能回答什么问题。

七、响应式问题必须在真实视口里找

前端系统化优化后统一了:

  • 设计 Token;
  • 顶栏、抽屉和 PageHeader;
  • 加载、Toast、状态标签;
  • 卡片、表格和空状态;
  • prefers-reduced-motion 与键盘焦点;
  • 桌面和移动端布局。

但仅靠阅读 JSX 看不出真正的问题。浏览器烟测时发现,通知调试器的三个输入框加按钮在桌面宽度下也会互相挤压;短信发送区的按钮因为 helper text 导致底部错位;移动端会话页需要明确的返回按钮;宽表必须局部横向滚动,而不是把整个页面撑宽。

最终形成几个简单规则:

  1. 筛选区和表单区使用可换行布局;
  2. 内容输入允许扩展,操作按钮保持固定高度;
  3. 移动端不压缩双栏,而是切换“列表 / 会话”两个状态;
  4. 表格溢出由自己的容器承担;
  5. 所有主要操作在 390px 与桌面宽度都实际点击一遍。

一个容易误判的 MUI 构建坑

调研和早期方案曾担心某些富表格组件在 SSR / SSG 环境访问 window。最终项目使用 Vite SPA,生产构建仍然是最可靠的判据:

npm run build

不要根据组件名猜运行模式,也不要用动态 import 掩盖未复现的问题。先确认构建链真正执行了什么,再决定是否需要延迟加载。

八、浏览器验收比“看代码应该没问题”更有价值

这轮前端优化没有停在 TypeScript 编译。实际启动 Server 与 Vite 后,分别在桌面和移动视口执行:

  • 打开短信会话;
  • 标记未读;
  • 发送回复;
  • 全文搜索并跳回会话;
  • 打开仪表盘并等待图表渲染;
  • 检查所有主要页面是否横向溢出;
  • 检查输入框、按钮与 helper text 的几何位置。

有些问题 DOM 结构和类型都完全正确,只有布局后的像素位置才能暴露。对于 UI 改动,截图和实际交互不是“补充测试”,而是最直接的验收证据。

九、公开仓库前,先把它当成一次数据泄露演练

项目运行目录里可能出现:

  • Agent Token;
  • 管理员 Session Cookie;
  • SQLite 数据库与备份;
  • 真实号码、IMEI、ICCID;
  • 短信正文和日志;
  • Telegram Bot Token、Webhook、SMTP 授权码;
  • 证书和私钥。

因此公开前做了三层审计。

第一层:当前跟踪文件

检查常见 Token 形态、私钥头、个人邮箱、本机路径、私网 IP、真实设备标识和运行时文件。所有示例统一换成虚构域名、号码和标识。

.gitignore 明确排除:

.local/
*.db
*.sqlite*
*.log
cookies.txt
agent.toml
config.toml
.env*
*.pem
*.key
*.csv

忽略规则只能防止未来误提交,不能删除已经进入历史的秘密。

第二层:完整 Git 历史

只检查工作区不够。已经删除的配置、旧日志和作者邮箱仍可能存在于历史对象中。

因此扫描全部提交内容,并把历史提交身份统一为 GitHub noreply 邮箱。历史重写以后要删除本地备份引用、清理 reflog,再对远程做受控强制更新。

历史重写风险很高:协作者需要重新同步,提交哈希全部变化。所以它只适合在首次公开、协作尚未展开时做,而且要明确记录原因。

第三层:公开访问验证

仓库切为 Public 后,还要从匿名视角验证:

  • Raw README 能直接访问;
  • 仓库描述和 Topics 正确;
  • 远程提交作者不暴露私人邮箱;
  • 私密漏洞报告已启用;
  • 本地分支与远程一致。

“命令执行成功”不等于公众看到的结果正确。公开发布必须从外部再看一次。

十、致谢和许可证不能省略

README 最终明确列出参考资料和相关项目,说明具体参考了什么,也说明 air780e-hub 是独立实现、没有复制其源代码。

另一方面,仓库没有擅自选择许可证。公开可见不自动等于开源授权;在作者明确决定前,README 直接说明当前没有授予复制、修改或再分发权利。

致谢解决来源透明问题,许可证解决使用权问题,两者不能互相替代。

十一、这轮前端与发布工作的经验

  1. 先完成端到端 CRUD,再重构用户任务流;
  2. 会话身份必须包含 SIM,不能只看号码;
  3. 列表、计数、搜索和导出共享同一过滤契约;
  4. 预览必须复用真实业务逻辑;
  5. 响应式布局要在真实视口中逐页检查;
  6. UI 改动要用浏览器交互验收,不只看构建;
  7. 公开前同时审计当前文件、Git 历史和远程结果;
  8. 不确定许可证时明确保留权利,不要随便贴一个热门 License。

下一篇是整个系列的收尾:把 M0 到 M7 的过程、错误假设、测试策略和可迁移经验整理成一份适合下一次硬件系统项目直接复用的清单。