用一台闲置 Android 手机搭建私有短信网关

121 阅读13分钟

原文:How I Built an SMS Gateway with a $20 Android Phone Content was rephrased for compliance with licensing restrictions

为什么要自建短信网关

商业短信平台(国外的 Twilio、国内的阿里云短信/腾讯云短信)按条计费,通常 0.03–0.05 元/条。对于正式的 C 端业务(验证码、营销通知),这些平台是必选项——它们有运营商直连通道、模板审核、到达率保障。

但有一类场景,你只是想给自己或团队内部发几条通知,不需要那么"正规":

  • 服务器挂了,给自己发条短信告警
  • 定时任务跑完了,短信通知一下结果
  • 智能家居传感器异常,短信提醒自己
  • IoT 设备在偏远地区没有稳定网络,但有手机信号,用短信做数据回传
  • 开发阶段调试短信流程,不想花钱接正规通道

这些场景的共同特点是:发送量小、接收者是自己人、不需要模板审核。

方案很简单:找一台闲置的 Android 手机,装上开源应用 SMS Gateway for Android,它会把手机变成一个带 REST API 的短信网关。短信走手机 SIM 卡的套餐,成本几乎为零。

工作原理

整个方案的本质是把 Android 手机的短信收发能力包装成 HTTP API:

发送短信(出站): 你的服务器 → HTTP POST → SMS Gateway App → Android 短信 API → 运营商网络 → 对方手机

接收短信(入站): 对方手机 → 运营商网络 → Android 收到短信 → SMS Gateway App 拦截 → HTTP Webhook → 你的服务器

SMS Gateway 做了两件事:

  1. 把 Android 的短信能力暴露为 REST API
  2. 通过 Webhook 把收到的短信实时推送给你的后端

适用场景与边界

适合用的场景

场景说明
运维告警服务宕机、磁盘满、证书过期,给自己发短信,比邮件通知更及时
定时任务通知数据备份完成、报表生成完毕、爬虫跑完了
智能家居/IoT温湿度异常、门窗传感器触发、摄像头离线
开发测试调试短信相关业务逻辑,不想花钱买正规短信通道
内网私密通知本地模式下消息完全不经过第三方,数据不出局域网
网络降级备份主网络断了,用短信作为备用通知通道

不适合的场景

场景原因
给用户发验证码到达率无保障,不合规,应使用阿里云/腾讯云短信
营销群发运营商会封号,违反相关法规
大量发送(日均 50+ 条)运营商风控会触发,可能被限制或封号
面向 C 端用户的任何通知应走正规短信平台,有模板审核和到达率保障

一句话总结:给自己人发少量通知——用它;给外部用户发业务短信——用正规平台。

前置条件

  • 一台 Android 手机(5.0+),插有 SIM 卡(国内任意运营商均可)
  • 一个后端应用(本文以 Next.js 15 + App Router 为例,任何后端框架都行)
  • Node.js 18+
  • 使用云模式时需要 ngrok 做内网穿透

安装 SMS Gateway

  1. 国内无法访问 Google Play,直接从 GitHub Releases 下载 APK 安装
  2. 打开应用,授予短信权限
  3. 主界面上有 Local Server 和 Cloud Server 两个开关

image.png

应用支持两种模式——本地模式和云模式,下面分别介绍。

本地服务器模式

本地模式直接在手机上运行一个 HTTP 服务器,后端通过局域网与之通信。不依赖云服务,不经过第三方服务器,是最简单也最私密的方案。对于国内用户,推荐优先使用本地模式,避免数据经过境外服务器。

配置步骤

image.png

  1. 打开 "Local Server" 开关
  2. 进入 Settings > Local Server 配置:
    • 端口:1024–65535(默认 8080)
    • 用户名:至少 3 个字符
    • 密码:至少 8 个字符
  3. 点击 "Offline",状态变为 "Online"
  4. 记下显示的本地 IP 地址(例如 192.168.1.50

验证服务是否正常:

# 健康检查
curl http://192.168.1.50:8080/health

# Swagger 文档
open http://192.168.1.50:8080/docs

发送第一条短信

curl -X POST http://192.168.1.50:8080/message \
  -u "admin:yourpassword" \
  -H "Content-Type: application/json" \
  -d '{
    "textMessage": { "text": "服务器磁盘使用率超过90%,请及时处理" },
    "phoneNumbers": ["+8613800138000"]
  }'

手机会用自己的号码发送短信,费用按你的手机套餐计算。

注册 Webhook 接收入站短信

curl -X POST http://192.168.1.50:8080/webhooks \
  -u "admin:yourpassword" \
  -H "Content-Type: application/json" \
  -d '{
    "id": "my-webhook",
    "url": "http://192.168.1.100:4000/api/sms/webhook",
    "event": "sms:received"
  }'

192.168.1.100 替换成你开发机的局域网 IP。两台设备需要在同一个 WiFi 网络下。

本地模式注意事项

  • AP 隔离:很多路由器(尤其是 Mesh 网络和办公 WiFi)会阻止设备间通信。如果连不上手机,检查路由器设置中的 "AP 隔离" 或 "客户端隔离" 并关闭它。
  • 电池优化:Android 会为了省电杀掉后台服务。在手机设置中为 SMS Gateway 关闭电池优化。dontkillmyapp.com 有各品牌手机的具体操作指南,国产手机(小米、华为、OPPO 等)的后台管理尤其激进,务必仔细设置。
  • 保持充电:手机应该一直插着充电器,这是它唯一的工作。

云服务器模式

image.png

云模式设置更简单,且不受局域网限制。手机连接到 SMS Gateway 的云中继服务(api.sms-gate.app),后端也通过同一个云 API 通信。

国内用户注意:云模式的中继服务器在境外,短信内容会经过第三方服务器。如果你在意数据隐私,建议使用本地模式。另外,国内网络访问境外 API 可能不稳定,需要考虑连通性问题。

启用步骤

  1. 在应用中打开 "Cloud Server" 开关
  2. 点击 "Offline",自动连接并注册
  3. 系统会自动生成用户名和密码(在 Cloud Server 区域可见)
  4. 记下这些凭据,API 调用时需要用到

云模式采用混合推送架构:Firebase Cloud Messaging 作为主通道,Server-Sent Events 作为备用,15 分钟轮询作为最后兜底。

通过云 API 发送短信

curl -X POST https://api.sms-gate.app/3rdparty/v1/messages \
  -u "YOUR_USERNAME:YOUR_PASSWORD" \
  -H "Content-Type: application/json" \
  -d '{
    "textMessage": { "text": "来自云端的问候!" },
    "phoneNumbers": ["+8613800138000"]
  }'

注册 Webhook(云模式)

云模式下 Webhook URL 必须是 HTTPS。本地开发时可以用 ngrok:

# 启动 ngrok 隧道
ngrok http 4000
# 输出: https://abc123.ngrok.app

# 注册 Webhook
curl -X POST https://api.sms-gate.app/3rdparty/v1/webhooks \
  -u "YOUR_USERNAME:YOUR_PASSWORD" \
  -H "Content-Type: application/json" \
  -d '{
    "url": "https://abc123.ngrok.app/api/sms/webhook",
    "event": "sms:received"
  }'

管理 Webhook

# 列出所有 Webhook
curl -u "YOUR_USERNAME:YOUR_PASSWORD" \
  https://api.sms-gate.app/3rdparty/v1/webhooks

# 删除 Webhook
curl -X DELETE -u "YOUR_USERNAME:YOUR_PASSWORD" \
  https://api.sms-gate.app/3rdparty/v1/webhooks/WEBHOOK_ID

代码实现——Next.js 集成

核心思路是做一个 Provider 抽象层,切换短信通道时不需要改动业务逻辑。将来如果要换成阿里云短信,只需新增一个 Provider 实现即可。

Provider 接口

// src/lib/sms/provider.ts

export interface InboundSms {
  from: string;
  body: string;
  receivedAt?: Date;
}

export interface SmsProvider {
  send(to: string, body: string): Promise<string>;
  parseWebhook(req: Request): Promise<InboundSms | null>;
  webhookResponse(replyText?: string): Response;
}

export async function getSmsProvider(): Promise<SmsProvider> {
  const provider = process.env.SMS_PROVIDER || "sms-gate";

  switch (provider) {
    case "sms-gate": {
      const { SmsGateProvider } = await import("./sms-gate");
      return new SmsGateProvider();
    }
    case "console": {
      const { ConsoleProvider } = await import("./console");
      return new ConsoleProvider();
    }
    // 将来可以扩展:
    // case "aliyun": { ... }
    // case "tencent": { ... }
    default:
      throw new Error(`Unknown SMS provider: ${provider}`);
  }
}

SMS Gate Provider

同时处理本地和云 API 的差异:

// src/lib/sms/sms-gate.ts

import type { SmsProvider, InboundSms } from "./provider";

const SMSGATE_URL = process.env.SMSGATE_URL || "http://localhost:8080";
const SMSGATE_USER = process.env.SMSGATE_USER || "";
const SMSGATE_PASSWORD = process.env.SMSGATE_PASSWORD || "";

export class SmsGateProvider implements SmsProvider {
  private headers(): Record<string, string> {
    const auth = Buffer.from(
      `${SMSGATE_USER}:${SMSGATE_PASSWORD}`
    ).toString("base64");
    return {
      "Content-Type": "application/json",
      Authorization: `Basic ${auth}`,
    };
  }

  async send(to: string, body: string): Promise<string> {
    const isCloud = SMSGATE_URL.includes("api.sms-gate.app");
    const endpoint = isCloud
      ? `${SMSGATE_URL}/3rdparty/v1/messages`
      : `${SMSGATE_URL}/api/3rdparty/v1/message`;
    const payload = isCloud
      ? { textMessage: { text: body }, phoneNumbers: [to] }
      : { phoneNumbers: [to], message: body };

    const res = await fetch(endpoint, {
      method: "POST",
      headers: this.headers(),
      body: JSON.stringify(payload),
    });

    if (!res.ok) {
      const err = await res.text();
      throw new Error(`SMS Gate send failed: ${res.status} ${err}`);
    }

    const data = await res.json();
    return data.id || "sent";
  }

  async parseWebhook(req: Request): Promise<InboundSms | null> {
    try {
      const body = await req.json();

      if (body.event !== "sms:received" || !body.payload) {
        return null;
      }

      const { phoneNumber, message, receivedAt } = body.payload;
      if (!phoneNumber || !message) return null;

      return {
        from: phoneNumber,
        body: message,
        receivedAt: receivedAt ? new Date(receivedAt) : new Date(),
      };
    } catch {
      return null;
    }
  }

  webhookResponse(): Response {
    return new Response(JSON.stringify({ ok: true }), {
      headers: { "Content-Type": "application/json" },
    });
  }
}

Webhook 路由

一个运维告警场景的 Webhook 处理器示例:

// src/app/api/sms/webhook/route.ts

import { NextRequest } from "next/server";
import { getSmsProvider } from "@/lib/sms/provider";

export async function POST(req: NextRequest) {
  const provider = await getSmsProvider();
  const sms = await provider.parseWebhook(req);

  if (!sms) {
    return new Response("Bad request", { status: 400 });
  }

  const { from, body } = sms;

  // 示例:通过短信回复查询服务器状态
  if (body.trim().toLowerCase() === "status") {
    await provider.send(from, "CPU: 23% | 内存: 67% | 磁盘: 45% | 全部正常");
    return provider.webhookResponse();
  }

  console.log(`[SMS from ${from}]: ${body}`);
  await provider.send(from, "收到,已记录。");
  return provider.webhookResponse();
}

Console Provider(用于测试)

本地开发时不需要手机的替代方案:

// src/lib/sms/console.ts

import type { SmsProvider, InboundSms } from "./provider";

export class ConsoleProvider implements SmsProvider {
  async send(to: string, body: string): Promise<string> {
    console.log(`[SMS -> ${to}] ${body}`);
    return `console-${Date.now()}`;
  }

  async parseWebhook(req: Request): Promise<InboundSms | null> {
    const data = await req.json();
    return {
      from: data.from || "+8613800138000",
      body: data.body || "",
      receivedAt: new Date(),
    };
  }

  webhookResponse(): Response {
    return new Response(JSON.stringify({ ok: true }), {
      headers: { "Content-Type": "application/json" },
    });
  }
}

环境变量

# .env

# Provider: "sms-gate" | "console"
SMS_PROVIDER=sms-gate

# 本地模式(推荐国内用户使用)
SMSGATE_URL=http://192.168.1.50:8080
SMSGATE_USER=admin
SMSGATE_PASSWORD=yourpassword

# 云模式(境外中继,国内网络可能不稳定)
# SMSGATE_URL=https://api.sms-gate.app
# SMSGATE_USER=auto-generated-username
# SMSGATE_PASSWORD=auto-generated-password

实战示例:服务器告警通知

这是一个最典型的使用场景——当服务器出现异常时,通过短信网关发送告警:

// src/lib/alert.ts

import { getSmsProvider } from "@/lib/sms/provider";

const ADMIN_PHONES = (process.env.ADMIN_PHONES || "").split(",").filter(Boolean);

export async function sendAlert(message: string) {
  if (ADMIN_PHONES.length === 0) {
    console.warn("未配置告警手机号,跳过短信通知");
    return;
  }

  const provider = await getSmsProvider();

  for (const phone of ADMIN_PHONES) {
    try {
      await provider.send(phone.trim(), `[告警] ${message}`);
    } catch (err) {
      console.error(`发送告警短信失败 (${phone}):`, err);
    }
  }
}

// 使用示例:
// await sendAlert("数据库连接池耗尽,当前活跃连接数: 100/100");
// await sendAlert("SSL 证书将在 3 天后过期");
// await sendAlert("磁盘使用率 95%,请及时清理");
# .env 中配置告警接收人
ADMIN_PHONES=+8613800138000,+8613900139000

Webhook 载荷参考

当有人给你的 Android 手机发短信时,SMS Gateway 会向你的 Webhook URL 发送 POST 请求:

{
  "id": "Ey6ECgOkVVFjz3CL48B8C",
  "webhookId": "LreFUt-Z3sSq0JufY9uWB",
  "deviceId": "your-device-id",
  "event": "sms:received",
  "payload": {
    "messageId": "abc123",
    "message": "你好!",
    "sender": "+8613800138000",
    "recipient": "+8613900139000",
    "simNumber": 1,
    "receivedAt": "2026-04-01T12:41:59.000+00:00"
  }
}

可用事件

事件说明
sms:received收到入站短信
sms:sent出站短信已发送
sms:delivered出站短信已确认送达
sms:failed出站短信发送失败
system:ping心跳——设备仍在线

Webhook 安全

SMS Gateway 使用 HMAC-SHA256 对 Webhook 载荷进行签名,包含两个 Header:

  • X-Signature:十六进制编码的 HMAC-SHA256 签名
  • X-Timestamp:用于签名的 Unix 时间戳
import crypto from "crypto";

function verifyWebhook(
  signingKey: string,
  payload: string,
  timestamp: string,
  signature: string
): boolean {
  const expected = crypto
    .createHmac("sha256", signingKey)
    .update(payload + timestamp)
    .digest("hex");
  return crypto.timingSafeEqual(
    Buffer.from(expected, "hex"),
    Buffer.from(signature, "hex")
  );
}

重试机制

如果你的服务器在 30 秒内没有返回 2xx 响应,SMS Gateway 会以指数退避策略重试——从 10 秒开始,每次翻倍,最多重试 14 次(约 2 天)。默认行为已经很合理,不需要额外配置。

测试完整流程

1. 启动开发服务器

npm run dev
# Next.js 运行在 http://localhost:4000

2. 暴露服务(云模式才需要)

ngrok http 4000
# https://abc123.ngrok.app -> http://localhost:4000

3. 注册 Webhook

# 本地模式
curl -X POST http://192.168.1.50:8080/webhooks \
  -u "admin:yourpassword" \
  -H "Content-Type: application/json" \
  -d '{
    "id": "alert-webhook",
    "url": "http://192.168.1.100:4000/api/sms/webhook",
    "event": "sms:received"
  }'

4. 发一条短信试试

用另一台手机给你的 Android 手机发短信,你会看到:

  1. SMS Gateway 收到短信
  2. Webhook 触发,请求发到你的服务器
  3. 服务器处理请求
  4. 通过 API 发送回复短信
  5. 发送者的手机收到回复

不用手机也能测试

# 用 console provider 模拟
SMS_PROVIDER=console npm run dev

curl -X POST http://localhost:4000/api/sms/webhook \
  -H "Content-Type: application/json" \
  -d '{"from": "+8613800138000", "body": "status"}'

生产环境部署

手机设置

  • 专用设备:用一台闲置的 Android 手机,配一张最便宜的套餐 SIM 卡。它的一生就是插着充电器连着 WiFi。
  • 关闭电池优化:为 SMS Gateway 关闭电池优化。国产手机(小米、华为、OPPO、vivo)的后台管理特别激进,需要在"电池"和"自启动管理"两个地方都设置。去 dontkillmyapp.com 查你的设备型号。
  • 开机自启:在 SMS Gateway 应用设置中启用 "start on boot"。
  • 监控:注册一个 system:ping Webhook,设备离线时发出告警(可以用另一个通道,比如邮件或企业微信机器人)。

本地模式 vs 云模式

本地模式云模式
延迟更低(直连)略高(经过中继)
网络需要同一网络随处可用
隐私消息不离开你的网络消息经过境外服务器
可靠性取决于你的网络FCM/SSE 冗余(国内可能受限)
国内推荐度⭐⭐⭐⭐⭐⭐⭐

国内用户强烈建议使用本地模式。 如果你的服务器不在本地局域网,可以通过 VPN 或内网穿透(frp、ngrok)打通网络,仍然比走境外云中继更可控。

成本对比(国内视角)

方案单条成本每月费用(500 条)适用场景
阿里云/腾讯云短信~0.04 元/条~20 元C 端业务、验证码、正式通知
手机网关 + 低价套餐~0 元/条8–19 元(套餐费)内部告警、个人通知、开发测试

国内使用的注意事项

  1. 发送频率控制:不要短时间内大量发送,建议日均不超过 30–50 条,避免触发运营商风控
  2. 内容合规:即使是给自己发,也不要发送违规内容,运营商有内容审查机制
  3. 号码选择:建议用一张专门的副卡,不要用主力号码,万一被限制不影响日常使用
  4. 双通道备份:重要告警建议同时配置企业微信/钉钉机器人作为备用通道,短信网关作为最后兜底
  5. APK 安装:国内无法访问 Google Play,从 GitHub Releases 下载 APK 即可

更多玩法:短信网关还能做什么

前面的例子集中在运维告警和定时通知,但"手机短信能力 + HTTP API"这个组合的想象空间远不止于此。

双向交互:短信 ChatOps

短信不只是单向通知,入站短信 + Webhook 让它变成一个双向交互通道:

  • 短信执行命令:发"重启 nginx"触发服务器操作,发"查余额"返回账户信息。相当于一个不依赖任何 App 的极简 ChatOps。
  • 短信审批流:关键操作(数据库删除、生产部署)需要管理员回复"确认"才执行。短信作为带外确认通道,比邮件审批更即时。
  • 短信触发 CI/CD:紧急情况下(只有手机没有电脑),发一条"deploy prod v2.3.1"触发生产部署。

认证与安全

  • 自建 2FA / OTP:给内部系统(VPN、后台管理面板)做短信验证码,不用接商业短信平台。适合小团队内部工具。
  • 登录告警:有人登录服务器或后台时,短信通知 IP 和时间,异常登录第一时间知道。
  • 蜜罐触发通知:服务器上部署蜜罐文件或端口,被访问立刻短信告警,作为入侵检测的最后一道通知。

IoT 与边缘场景

  • 远程控制:在没有稳定网络的偏远地区(农场、工地、山区基站),通过短信发指令控制设备开关、调整参数。手机信号覆盖远比 WiFi 广。
  • 短信数据采集:传感器数据通过短信回传到网关手机,再由 Webhook 写入数据库。适合低频数据上报(每小时一次温湿度之类的)。
  • 车辆/资产追踪:在车上放一台带 GPS 的手机,定时短信上报位置,或者发短信查询当前位置。

网络降级与灾备

  • 断网应急通道:主网络挂了,短信作为唯一的通信通道。通过短信触发服务器重启、切换 DNS、启动备用线路。
  • 灾难恢复协调:团队成员在网络中断时通过短信网关互相通信、协调恢复操作。

自动化与工作流

  • 表单/工单通知:内部工单系统有新工单时短信通知值班人员,比 App 推送更不容易被忽略。
  • 定时提醒:吃药、会议、账单到期,短信提醒比 App 通知更可靠。
  • 完全离线的通知系统:本地模式下消息不经过任何第三方服务器,对数据主权有要求的场景(医疗、金融内部系统)是一个优势。

这些玩法的共同模式是:短信作为一个不依赖互联网的可编程通信通道。网络可能断、App 可能被杀、邮件可能被忽略,但短信几乎总能送达。

不止短信:旧手机还能做什么

短信网关利用的是手机的 SIM 卡和短信能力,但一台旧 Android 手机的硬件资源远不止于此。同样的思路——把硬件能力包装成可编程的 API——可以套到手机的其他硬件上。

摄像头 → 安防监控

IP Webcam 把手机摄像头变成 RTSP/HTTP 视频流,直接对接 Home Assistant 或 NVR。配合运动检测,触发时通过短信网关发告警——一台手机同时干两件事。

GPS → 追踪定位

Traccar Client 是开源的 GPS 追踪方案,客户端和服务端都开源。把旧手机放在车里或贵重物品里,定时上报 GPS 坐标到你的服务器。比买专门的 GPS 追踪器便宜,而且手机有 SIM 卡可以走数据网络上报。

麦克风 → 环境监测

噪音监测(检测分贝超标告警,适合机房、工厂)、婴儿监控(检测到哭声推送通知)、甚至配合语音识别做本地语音控制。

屏幕 → 信息仪表盘

全屏显示 Grafana 面板、Home Assistant 界面、天气、日历,或者做小店铺的数字标牌、会议室门口的预约状态。用 Fully Kiosk Browser 锁定成单一用途的显示终端。

蓝牙 → BLE 网关

采集周围的蓝牙温湿度传感器(比如小米的那些)数据,转发到 MQTT 或 HTTP 后端。也可以做蓝牙 Beacon 检测,实现简易的人员到离感知。

WiFi → 网络探针

定时 ping 关键服务和设备,断了就通过短信网关告警。一台手机同时当网络探针和告警通道。

NFC → 门禁打卡

旧手机固定在门口,刷 NFC 标签记录出入,或者做资产盘点——扫 NFC 标签登记设备信息。

计算能力 → 轻量级服务器

通过 Termux 装 Python、Node.js,跑定时脚本、小型爬虫。挂个 SD 卡跑 aria2 做下载节点。

组合才是最有价值的

一台旧手机同时跑短信网关 + 摄像头监控 + 环境传感 + 网络探针,就是一个全能的"哨兵节点"——检测到异常用摄像头拍一张照,同时短信通知你。所有这些能力都可以用同一套"硬件能力 → HTTP API"的架构模式来统一管理。

供电与续航:绕不开的现实问题

旧手机再利用,供电是必须面对的问题。不同场景差异很大。

固定场景:插电就行

短信网关、信息仪表盘、BLE 网关、NAS 下载节点这些,手机放在家里或办公室,一直插着充电器就行。需要注意的是长期满电充电会加速电池老化膨胀:

  • 部分手机支持"充电保护",限制充电到 80%(三星、索尼等有此功能)
  • 没有此功能的手机,可以用智能插座定时断电,做简易的充放循环
  • 定期检查电池是否鼓包,发现膨胀立即停用

移动场景:需要取舍

GPS 追踪(放车里)、户外摄像头、偏远地区 IoT 节点,没有固定电源,供电方案:

方案续航适用场景
车载 USB 充电器行驶时无限,熄火后 1–2 天车辆追踪
太阳能板(5–10W)+ 充电宝持续供电(晴天)户外监控、农场、工地
大容量充电宝(20000mAh)3–5 天(低功耗模式)临时部署、短期监测

软件省电策略

硬件供电之外,软件层面的优化同样关键:

  • 关屏幕:屏幕是最耗电的硬件,不需要显示的场景保持息屏
  • 降低采集频率:GPS 不需要秒级上报,每 5 分钟一次够用
  • 按需唤醒:平时休眠,收到短信指令或定时器触发时才工作
  • 关闭不用的硬件:不用蓝牙就关蓝牙,不用 WiFi 就走移动数据
  • 自动化省电TaskerMacroDroid 可以设置规则,比如电量低于 20% 自动降低上报频率

电池安全:认真对待

旧手机长期充电或在高温环境运行,电池膨胀甚至起火的风险是真实存在的:

  • 放在通风的地方,避免阳光直射和密闭空间
  • 车内场景夏天温度能到 60–70°C,锂电池的安全上限一般是 45°C,这个要认真对待
  • 如果手机支持,拆掉电池直接用 USB 供电(部分手机可以)
  • 定期检查电池是否鼓包,发现异常立即停用
  • 不要在床上、沙发上等易燃物旁边长期充电

总结

这个方案的起点是一个简单的需求:用闲置手机搭一个私有短信网关。但往深了想,它代表的是一种更通用的思路——把旧手机的硬件能力(短信、摄像头、GPS、蓝牙、麦克风、屏幕)包装成可编程的 API,让它变成一个低成本的边缘计算节点。

短信网关是其中最实用的一个切入点。短信的独特优势在于不依赖互联网——手机没网也能收短信,App 通知可能被关掉但短信不会。当你的服务器凌晨三点挂了,一条短信比一封邮件更可能把你叫醒。

对于正式的业务短信,老老实实用阿里云或腾讯云短信服务。对于给自己和团队发告警通知,一台闲置手机就够了。

这类工具已经形成了一个小品类,httpSMStextbee 也做类似的事。SMS Gateway for Android 的优势在于本地模式好用、文档扎实、维护活跃(2026 年 3 月发布了 v1.56.0)。代码里的 Provider 抽象层也留好了扩展口——将来如果需要接入阿里云短信,加一个 AliyunProvider 就行。

而那台闲置手机,远不止能发短信。

相关链接