Files
sms_forwarding/README.md
T
2026-06-27 21:56:41 +08:00

9.8 KiB
Raw Blame History

SMS Forwarding

一个运行在 Node.js 上的 4G 短信网关。程序通过串口控制 4G 模组,接收 PDU 短信,合并长短信,并把短信转发到邮件、钉钉、飞书、Telegram、PushPlus、Server 酱、Bark 或自定义 Webhook。

项目同时提供 Web 管理台和 HTTP API,可用于查看模组状态、收件箱、日志、推送通道,发送短信,切换发送短信使用的 SIM,以及执行 AT 命令。

功能

  • 通过 AT 指令和 4G 模组通信
  • 自动探测可用 AT 串口
  • PDU 模式接收短信
  • 长短信自动合并
  • 收件箱持久化到本地文件
  • 多推送通道转发短信
  • Web 管理台
  • Token 鉴权的 HTTP API
  • Web/API 发送短信
  • 双 SIM 状态识别和发送前切卡
  • 移动数据开关和启动断网保护
  • AT 命令调试入口

环境要求

  • Node.js 18 或更高版本
  • 可通过串口访问的 4G 模组
  • 已插入可收发短信的 SIM 卡
  • Linux 下通常需要访问 /dev/ttyUSB* 的权限

已主要围绕 ML307A 调试。其他 AT 指令兼容模组可以运行,但双卡、移动数据、URC 卡槽识别等能力取决于模组返回的指令结果。

快速启动

npm install
cp config.example.json config.json

编辑 config.json,至少确认:

{
  "serial": {
    "path": "/dev/ttyUSB0",
    "baudRate": 115200,
    "autoDetect": true
  },
  "api": {
    "port": 3000,
    "webToken": "change-this-token"
  }
}

启动:

npm start

访问管理台:

http://localhost:3000/admin

第一次访问会要求输入 api.webToken。也可以临时通过 URL 传入:

http://localhost:3000/admin?token=change-this-token

Docker 运行

先准备配置:

cp config.example.json config.json

然后启动:

docker compose up --build -d

查看日志:

docker compose logs -f sms-gateway

docker-compose.yml 会挂载:

  • ./config.json:/app/config.json
  • ./logs:/app/logs
  • ./data:/app/data
  • /dev:/dev
  • /sys:/sys:ro

默认端口是 3000。如需修改宿主机端口:

SMS_GATEWAY_PORT=8080 docker compose up -d

PM2 运行

npm install
cp config.example.json config.json
pm2 start ecosystem.config.js
pm2 save

配置

config.json 必须存在,否则程序会退出。完整模板见 config.example.json

必填项

配置项 说明
serial.path 串口路径。Linux 常见为 /dev/ttyUSB0Windows 可写 COM3serial.autoDetect=true 时它是优先探测项;false 时它就是实际打开的串口。
serial.baudRate 串口波特率,通常是 115200
api.port Web 管理台和 HTTP API 监听端口。
api.webToken 唯一访问凭据。项目没有默认账号密码,也没有 admin/admin123 Basic Auth。

可选项

配置项 默认值 说明
serial.autoDetect true 是否自动探测可响应 AT 的串口。
serial.probeTimeout 1200 单个串口探测超时,单位毫秒。
mobileData.cid 1 移动数据 PDP CID。有效范围 1-15,不要使用 8
simCards [] 只影响 Web 显示的 SIM 手机号或标签,不影响实际收发。slot=0 是 SIM1slot=1 是 SIM2。
smtp 未启用 配置后会额外发送邮件通知。填写 smtp.server 后需要同时正确填写 portuserpasssendTo
inbox.maxReceivedMessages 200 收件箱最多保留条数,代码上限是 5000
inbox.receivedMessagesFile data/received-messages.json 收件箱持久化文件。相对路径按项目根目录解析。
inbox.partitionBySim true 查询收件箱时是否按 SIM 信息隔离。
inbox.allowAllSimMessages false 是否允许显式查询全部 SIM 历史。
inbox.dualSimDefaultScope all 双卡场景下默认收件箱范围,可选 allcurrent
pushChannels [] 推送通道列表。通道 enabled=true 时必须填写该类型要求的目标字段。

api.webToken 支持以下传入方式:

  • Authorization: Bearer <token>
  • X-Web-Token: <token>
  • ?token=<token>
  • Web 管理台登录页

推送通道

支持的 pushChannels[].type

类型 必填字段
dingtalk urlsecret 可选
feishu urlsecret 可选
telegram url 填 Bot Tokenkey1 填 Chat ID
pushplus key1 填 PushPlus Token
serverchan key1 填 Server 酱 SendKey
bark url
post_json url
get url
custom urlcustomBody 可选

customBody 必须是合法 JSON 字符串,支持以下占位符:

  • {sender}
  • {message}
  • {timestamp}

enabled=false 的通道可以保留空字段;enabled=true 时会校验目标字段。

SIM 行为

启动初始化时程序会探测 SIM 状态。运行期间的普通状态刷新不会为了探测卡槽而持续切换 SIM。

发送短信时使用当前绑定的发送 SIM。Web 管理台的切卡入口放在“发送短信”区域,切换会同时设置模组的 SWITCHSIMBINDSIM,然后恢复短信直出配置。

短信接收使用 CNMI 直出模式。收到短信时,如果模组 URC 携带卡槽信息,会记录为 SIM1 或 SIM2;如果 URC 没有卡槽信息,收件箱会显示为未知 SIM。

如果只想给管理台显示手机号,可配置:

{
  "simCards": [
    { "slot": 0, "phoneNumber": "+8613800138000" },
    { "slot": 1, "phoneNumber": "+447700900000" }
  ]
}

HTTP API

所有 /api/* 接口都需要 token。下面示例使用 X-Web-Token

TOKEN=change-this-token
BASE=http://localhost:3000

状态和日志

curl -H "X-Web-Token: $TOKEN" "$BASE/api/status"
curl -H "X-Web-Token: $TOKEN" "$BASE/api/modem/info"
curl -H "X-Web-Token: $TOKEN" "$BASE/api/modem/signal"
curl -H "X-Web-Token: $TOKEN" "$BASE/api/logs"

收件箱

curl -H "X-Web-Token: $TOKEN" "$BASE/api/sms/received?limit=50"
curl -H "X-Web-Token: $TOKEN" "$BASE/api/sms/received?limit=50&scope=current"
curl -H "X-Web-Token: $TOKEN" "$BASE/api/sms/received?limit=50&simSlot=0"
curl -H "X-Web-Token: $TOKEN" "$BASE/api/sms/received?limit=50&simSlot=1"

发送短信

curl -X POST "$BASE/api/sms/send" \
  -H "Content-Type: application/json" \
  -H "X-Web-Token: $TOKEN" \
  -d '{"phone":"13800138000","message":"测试短信"}'

SIM 状态和切换

curl -H "X-Web-Token: $TOKEN" "$BASE/api/modem/sim"

curl -X POST "$BASE/api/modem/sim/switch" \
  -H "Content-Type: application/json" \
  -H "X-Web-Token: $TOKEN" \
  -d '{"slot":1}'

slot=0 表示 SIM1slot=1 表示 SIM2。

移动数据

curl -H "X-Web-Token: $TOKEN" "$BASE/api/modem/mobile-data"

curl -X POST "$BASE/api/modem/mobile-data" \
  -H "Content-Type: application/json" \
  -H "X-Web-Token: $TOKEN" \
  -d '{"enabled":false}'

curl -X POST "$BASE/api/modem/mobile-data/consume" \
  -H "Content-Type: application/json" \
  -H "X-Web-Token: $TOKEN" \
  -d '{"target":"8.8.8.8"}'

AT 命令

curl -X POST "$BASE/api/modem/at" \
  -H "Content-Type: application/json" \
  -H "X-Web-Token: $TOKEN" \
  -d '{"command":"AT+CSQ"}'

推送通道管理

curl -H "X-Web-Token: $TOKEN" "$BASE/api/push/channels"

curl -X PUT "$BASE/api/push/channels" \
  -H "Content-Type: application/json" \
  -H "X-Web-Token: $TOKEN" \
  -d '{"channels":[]}'

curl -X POST "$BASE/api/push/test" \
  -H "Content-Type: application/json" \
  -H "X-Web-Token: $TOKEN" \
  -d '{"enabled":true,"type":"post_json","name":"测试","url":"https://example.com/webhook"}'

目录结构

.
├── src/
│   ├── index.js      # 程序入口
│   ├── modem.js      # 串口、AT 指令、SIM 和移动数据控制
│   ├── sms.js        # PDU 解析、长短信处理后的转发、收件箱记录
│   ├── concat.js     # 长短信分段合并
│   ├── push.js       # 推送通道
│   ├── api.js        # Web 管理台和 HTTP API
│   └── logger.js     # 日志
├── public/           # Web 管理台静态文件
├── data/             # 收件箱持久化数据
├── logs/             # 运行日志
├── config.example.json
├── config.json       # 本地配置,不应提交
├── docker-compose.yml
├── Dockerfile
└── ecosystem.config.js

常见问题

串口打不开

Linux 下先确认设备存在:

ls -l /dev/ttyUSB*

如果是权限问题,把运行用户加入串口权限组,重新登录后生效:

sudo usermod -a -G dialout "$USER"

Docker 场景下确认 docker-compose.yml 已挂载 /dev,并保留了 device_cgroup_rules

自动探测不到模组

  • 确认 USB 连接和供电。
  • 确认 serial.baudRate 正确。
  • 临时关闭自动探测,把 serial.autoDetect 设为 false,并把 serial.path 写成真实 AT 串口。
  • 查看日志中每个候选串口的探测结果。

收不到短信

  • 确认 SIM 卡已插好且可以注册网络。
  • 查看 /api/status 里的 readyoperatorsignal
  • 确认日志里有 PDU模式设置完成CNMI参数设置完成
  • 部分模组不会在 URC 里带卡槽信息,这只影响收件箱显示的 SIM 归属,不代表短信没有收到。

发送短信失败

  • 先在管理台确认当前发送 SIM。
  • 确认目标号码格式正确。
  • 查看日志里的 AT 响应。
  • 如果刚切换 SIM,等待模组重新注册网络后再发。

安全说明

  • 不要提交 config.json、token、手机号、Webhook URL、SMTP 密码和日志。
  • 管理台和 API 只使用 api.webToken 鉴权。
  • POST /api/modem/at 可以执行任意 AT 命令,只应暴露在可信网络内。

License

MIT