11 KiB
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/ttyUSB0,Windows 可写 COM3。serial.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 是 SIM1,slot=1 是 SIM2。 |
smtp |
未启用 | 配置后会额外发送邮件通知。填写 smtp.server 后需要同时正确填写 port、user、pass、sendTo。 |
inbox.maxReceivedMessages |
200 |
收件箱最多保留条数,代码上限是 5000。 |
inbox.receivedMessagesFile |
data/received-messages.json |
收件箱持久化文件。相对路径按项目根目录解析。 |
inbox.partitionBySim |
true |
查询收件箱时是否按 SIM 信息隔离。 |
inbox.allowAllSimMessages |
false |
是否允许显式查询全部 SIM 历史。 |
inbox.dualSimDefaultScope |
all |
双卡场景下默认收件箱范围,可选 all 或 current。 |
pushChannels |
[] |
推送通道列表。通道 enabled=true 时必须填写该类型要求的目标字段。 |
api.webToken 支持以下传入方式:
Authorization: Bearer <token>X-Web-Token: <token>?token=<token>- Web 管理台登录页
推送通道
支持的 pushChannels[].type:
| 类型 | 必填字段 |
|---|---|
dingtalk |
url,secret 可选 |
feishu |
url,secret 可选 |
telegram |
url 填 Bot Token,key1 填 Chat ID |
pushplus |
key1 填 PushPlus Token |
serverchan |
key1 填 Server 酱 SendKey |
bark |
url |
post_json |
url |
get |
url |
custom |
url,customBody 可选 |
customBody 必须是合法 JSON 字符串,支持以下占位符:
{sender}{message}{timestamp}
enabled=false 的通道可以保留空字段;enabled=true 时会校验目标字段。
SIM 行为
启动初始化时程序会探测 SIM 状态。运行期间的普通状态刷新不会为了探测卡槽而持续切换 SIM。
发送短信时使用当前绑定的发送 SIM。Web 管理台的切卡入口放在“发送短信”区域,切换会同时设置模组的 SWITCHSIM 和 BINDSIM,然后恢复短信直出配置。
短信接收使用 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 表示 SIM1,slot=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 串口。 - 查看日志中每个候选串口的探测结果。
ML307A 首次上电被识别成网卡
ML307A 首次上电或恢复默认 USB 模式时,系统可能先把它识别为 RNDIS 网卡,而不是 USB 串口。此时 lsusb 能看到设备,但 /dev/ttyUSB* 里找不到可用 AT 串口。
可以用下面命令确认 USB 设备和内核识别结果:
lsusb
dmesg | grep -E 'ML307A|2ecc|3012|rndis|ttyUSB'
典型日志会包含类似内容:
usb 1-3: New USB device found, idVendor=2ecc, idProduct=3012
usb 1-3: Product: ML307A
usb 1-3: Manufacturer: CMIOT
rndis_host 1-3:1.0: rndis media connect
rndis_host 1-3:1.0 enxac0c29a39b6d: renamed from eth0
如果出现上述 RNDIS 网卡日志,并且系统没有生成 USB 串口,可以把该 USB ID 添加到 option 串口驱动:
sudo modprobe option
echo 2ecc 3012 | sudo tee /sys/bus/usb-serial/drivers/option1/new_id
执行后重新查看 /dev/ttyUSB*,再把 config.json 里的 serial.path 指向实际 AT 串口。不同内核、发行版或模组固件的枚举行为可能不完全一致;如果后续仍会被识别为网卡,可按系统发行版方式配置 udev/modprobe 规则。
收不到短信
- 确认 SIM 卡已插好且可以注册网络。
- 查看
/api/status里的ready、operator、signal。 - 确认日志里有
PDU模式设置完成和CNMI参数设置完成。 - 部分模组不会在 URC 里带卡槽信息,这只影响收件箱显示的 SIM 归属,不代表短信没有收到。
发送短信失败
- 先在管理台确认当前发送 SIM。
- 确认目标号码格式正确。
- 查看日志里的 AT 响应。
- 如果刚切换 SIM,等待模组重新注册网络后再发。
安全说明
- 不要提交
config.json、token、手机号、Webhook URL、SMTP 密码和日志。 - 管理台和 API 只使用
api.webToken鉴权。 POST /api/modem/at可以执行任意 AT 命令,只应暴露在可信网络内。
License
MIT