From ea6afcc08ca95c84ec5aab094cb860e275f38186 Mon Sep 17 00:00:00 2001 From: wuxu Date: Sat, 27 Jun 2026 16:50:06 +0800 Subject: [PATCH] =?UTF-8?q?docs:=20=E8=A1=A5=E5=85=85=E9=85=8D=E7=BD=AE?= =?UTF-8?q?=E8=AF=B4=E6=98=8E?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 353 ++++++++++++++++++++++++++++++++++++++++++++ config.example.json | 30 +++- 2 files changed, 377 insertions(+), 6 deletions(-) create mode 100644 README.md diff --git a/README.md b/README.md new file mode 100644 index 0000000..80a3119 --- /dev/null +++ b/README.md @@ -0,0 +1,353 @@ +# 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 卡槽识别等能力取决于模组返回的指令结果。 + +## 快速启动 + +```bash +npm install +cp config.example.json config.json +``` + +编辑 `config.json`,至少确认: + +```json +{ + "serial": { + "path": "/dev/ttyUSB0", + "baudRate": 115200, + "autoDetect": true + }, + "api": { + "port": 3000, + "webToken": "change-this-token" + } +} +``` + +启动: + +```bash +npm start +``` + +访问管理台: + +```text +http://localhost:3000/admin +``` + +第一次访问会要求输入 `api.webToken`。也可以临时通过 URL 传入: + +```text +http://localhost:3000/admin?token=change-this-token +``` + +## Docker 运行 + +先准备配置: + +```bash +cp config.example.json config.json +``` + +然后启动: + +```bash +docker compose up --build -d +``` + +查看日志: + +```bash +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`。如需修改宿主机端口: + +```bash +SMS_GATEWAY_PORT=8080 docker compose up -d +``` + +## PM2 运行 + +```bash +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 ` +- `X-Web-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。 + +如果只想给管理台显示手机号,可配置: + +```json +{ + "simCards": [ + { "slot": 0, "phoneNumber": "+8613800138000" }, + { "slot": 1, "phoneNumber": "+447700900000" } + ] +} +``` + +## HTTP API + +所有 `/api/*` 接口都需要 token。下面示例使用 `X-Web-Token`: + +```bash +TOKEN=change-this-token +BASE=http://localhost:3000 +``` + +### 状态和日志 + +```bash +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" +``` + +### 收件箱 + +```bash +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" +``` + +### 发送短信 + +```bash +curl -X POST "$BASE/api/sms/send" \ + -H "Content-Type: application/json" \ + -H "X-Web-Token: $TOKEN" \ + -d '{"phone":"13800138000","message":"测试短信"}' +``` + +### SIM 状态和切换 + +```bash +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。 + +### 移动数据 + +```bash +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 命令 + +```bash +curl -X POST "$BASE/api/modem/at" \ + -H "Content-Type: application/json" \ + -H "X-Web-Token: $TOKEN" \ + -d '{"command":"AT+CSQ"}' +``` + +### 推送通道管理 + +```bash +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"}' +``` + +## 目录结构 + +```text +. +├── 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 下先确认设备存在: + +```bash +ls -l /dev/ttyUSB* +``` + +如果是权限问题,把运行用户加入串口权限组,重新登录后生效: + +```bash +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` 里的 `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 diff --git a/config.example.json b/config.example.json index 372d0c0..06e7279 100644 --- a/config.example.json +++ b/config.example.json @@ -4,19 +4,40 @@ "baudRate": 115200, "autoDetect": true, "probeTimeout": 1200, - "_comment": "Windows使用COM1/COM2/COM3等,也可以直接写数字如3; Linux使用/dev/ttyUSB0" + "_comment": "必填。path是手动串口或自动探测优先项;autoDetect=false时必须写真实串口。Windows使用COM1/COM2/COM3等,也可以直接写数字如3;Linux使用/dev/ttyUSB0。baudRate通常为115200,probeTimeout为AT探测超时毫秒。" }, "mobileData": { "cid": 1, - "_comment": "每次启动都会强制关闭移动数据,管理台开关只影响当前运行期; ML307 cid8为IMS专用,请勿使用" + "_comment": "可选,默认cid=1。有效范围1-15,cid8为IMS专用,请勿使用。ML307每次启动都会强制断开MIPCALL应用层拨号,并保护性尝试CGACT;管理台开关只影响当前运行期。" }, + "_simCards_comment": "可选。用于给SIM1/SIM2显示手机号;slot=0是SIM1,slot=1是SIM2。也兼容旧字段名simSlots、sims、sim。", + "simCards": [ + { + "slot": 0, + "phoneNumber": "+8613800138000" + }, + { + "slot": 1, + "phoneNumber": "+447700900000" + } + ], "smtp": { + "_comment": "可选。填写server后会启用邮件通知;启用时server、port、user、pass、sendTo都需要正确填写。不需要邮件可删除smtp或清空server。", "server": "smtp.qq.com", "port": 465, "user": "your@qq.com", "pass": "your_auth_code", "sendTo": "recipient@example.com" }, + "inbox": { + "maxReceivedMessages": 200, + "receivedMessagesFile": "data/received-messages.json", + "partitionBySim": true, + "allowAllSimMessages": false, + "dualSimDefaultScope": "all", + "_comment": "全部可选。maxReceivedMessages默认200且最大5000;receivedMessagesFile默认data/received-messages.json;partitionBySim默认true;allowAllSimMessages默认false;dualSimDefaultScope可为all或current。默认显示全部短信;直出URC未携带卡槽时,新短信不区分SIM1/SIM2收件归属。" + }, + "_pushChannels_comment": "可选。enabled=false时目标字段可以为空;enabled=true时按type要求填写目标:post_json/bark/get/dingtalk/custom/feishu需要url,telegram需要url(bot token)+key1(chat id),pushplus需要key1(token),serverchan需要key1(sendkey)。customBody如果填写必须是合法JSON字符串。", "pushChannels": [ { "enabled": true, @@ -72,9 +93,6 @@ "api": { "port": 3000, "webToken": "change-this-token", - "auth": { - "username": "admin", - "password": "admin123" - } + "_comment": "必填。port是API和管理台监听端口;webToken是唯一鉴权凭据,可通过Authorization: Bearer、X-Web-Token、token查询参数或管理台登录页提交。" } }