diff --git a/README.md b/README.md new file mode 100644 index 0000000..2820efd --- /dev/null +++ b/README.md @@ -0,0 +1,325 @@ +# Node.js 4G SMS Gateway + +基于Node.js的4G短信网关,通过串口与4G模块通信,接收短信并转发到多种推送渠道。 + +## 功能特性 + +- ✅ 4G模块AT指令交互 +- ✅ 自动接收短信(PDU模式) +- ✅ 长短信自动合并 +- ✅ 多通道推送(邮件/钉钉/飞书/Telegram等) +- ✅ 短信发送功能 +- ✅ 号码黑名单 +- ✅ 管理员远程命令 +- ✅ RESTful API接口 +- ✅ Web管理界面 + +## 环境要求 + +- Node.js >= 18.x +- 4G模块通过USB转串口连接到服务器 +- 支持的4G模块:ML307R-DC、SIM7600等AT指令兼容模块 + +## 安装 + +```bash +npm install +``` + +## 配置 + +复制配置模板: +```bash +cp config.example.json config.json +``` + +`config.json` 必须存在,否则程序会直接退出。最小可运行配置如下: + +```json +{ + "serial": { + "path": "/dev/ttyUSB0", + "baudRate": 115200, + "autoDetect": true, + "probeTimeout": 1200 + }, + "api": { + "port": 3000, + "webToken": "change-this-token" + } +} +``` + +完整配置可参考 `config.example.json`。主要配置项如下: + +| 配置项 | 是否必填 | 说明 | +|--------|----------|------| +| `serial.path` | 是 | 串口路径;Linux 常用 `/dev/ttyUSB0`,Windows 使用 `COM3` 等。`autoDetect=true` 时作为优先探测项,`autoDetect=false` 时作为实际串口。 | +| `serial.baudRate` | 是 | 串口波特率,通常为 `115200`。 | +| `serial.autoDetect` | 否 | 是否自动探测可响应 `AT` 的串口,默认按非 `false` 处理。 | +| `serial.probeTimeout` | 否 | 自动探测单个串口的超时时间,单位毫秒,默认 `1200`。 | +| `api.port` | 是 | API 和 Web 管理台监听端口。 | +| `api.webToken` | 是 | 唯一鉴权凭据;已取消 `admin/admin123` Basic Auth。 | +| `mobileData.cid` | 否 | 移动数据 PDP CID,默认 `1`;有效范围 `1-15`,不要使用 `8`。 | +| `simCards` | 否 | SIM 卡显示手机号,`slot=0` 是 SIM1,`slot=1` 是 SIM2。 | +| `smtp` | 否 | 邮件通知配置;填写 `smtp.server` 后启用。 | +| `adminPhone` | 否 | 管理员手机号;只有该号码发来的短信会执行管理员命令。 | +| `numberBlackList` | 否 | 黑名单号码列表,匹配后忽略短信。 | +| `inbox` | 否 | Web 收件箱持久化和双卡显示策略。 | +| `pushChannels` | 否 | 推送通道列表;通道开启后才要求目标字段。 | + +`api.webToken` 可以通过以下方式传入: + +- `Authorization: Bearer ` +- `X-Web-Token: ` +- `?token=` +- Web 管理台 token 登录页 + +### 完整配置示例 + +```json +{ + "serial": { + "path": "/dev/ttyUSB0", + "baudRate": 115200, + "autoDetect": true, + "probeTimeout": 1200 + }, + "mobileData": { + "cid": 1 + }, + "simCards": [ + { "slot": 0, "phoneNumber": "+8613800138000" }, + { "slot": 1, "phoneNumber": "+447700900000" } + ], + "smtp": { + "server": "smtp.qq.com", + "port": 465, + "user": "your@qq.com", + "pass": "your_auth_code", + "sendTo": "recipient@example.com" + }, + "adminPhone": "13800138000", + "numberBlackList": ["10086", "10010"], + "inbox": { + "maxReceivedMessages": 200, + "receivedMessagesFile": "data/received-messages.json", + "partitionBySim": true, + "allowAllSimMessages": false, + "dualSimDefaultScope": "all" + }, + "pushChannels": [ + { + "enabled": true, + "type": "dingtalk", + "name": "钉钉通知", + "url": "https://oapi.dingtalk.com/robot/send?access_token=xxx", + "secret": "SECxxx" + } + ], + "api": { + "port": 3000, + "webToken": "change-this-token" + } +} +``` + +### 可选项细节 + +- `smtp`:不需要邮件时可以删除整个对象,或清空 `server`。启用邮件时需要正确填写 `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` 或 `current`;`current` 表示双卡时默认只看当前 SIM。 +- `simCards`:只影响管理台显示,不影响短信收发。代码也兼容旧字段名 `simSlots`、`sims`、`sim`。 + +## 运行 + +### 开发模式 +```bash +npm run dev +``` + +### 生产模式 +```bash +npm start +``` + +### 使用PM2(推荐) +```bash +pm2 start ecosystem.config.js +pm2 save +pm2 startup +``` + +## API接口 + +### 查询状态 +```bash +GET /api/status +``` + +### 发送短信 +```bash +POST /api/sms/send +Content-Type: application/json + +{ + "phone": "13800138000", + "message": "测试短信" +} +``` + +### 查询收到的短信 +```bash +GET /api/sms/received?limit=50 +``` + +双卡双待模式下,默认返回全部 SIM 历史;显式传 `scope=current` 时才按当前卡槽过滤。短信记录会保留 `simSlot`、`simLabel` 与 `simSource`,用于判断收件卡槽。 +也可以显式按卡槽查询: + +```bash +GET /api/sms/received?limit=50&simSlot=0 # SIM1 +GET /api/sms/received?limit=50&simSlot=1 # SIM2 +``` + +直出模式下,新短信主要通过模组 `+CMT` URC 进入收件箱;如果 URC 未携带卡槽信息,会显示为未知 SIM。 + +### 查询信号强度 +```bash +GET /api/modem/signal +``` + +### 查询模组信息 +```bash +GET /api/modem/info +``` + +### 查询与切换SIM +```bash +GET /api/modem/sim +POST /api/modem/sim/switch +Content-Type: application/json + +{ + "slot": 1 +} +``` + +当模组返回 `AT+DUALSIM?`、`AT+SWITCHSIM?` 与 `AT+BINDSIM?` 能力时,管理台会显示 SIM1/SIM2。短信接收使用 `CNMI` 直出模式,不为了接收短信切换 `SWITCHSIM`。如果模组上报的 URC 未携带卡槽信息,新短信会记录为未知 SIM。 + +管理台切卡会同时设置 `AT+SWITCHSIM` 与 `AT+BINDSIM`,随后恢复短信直出配置。发送短信前会提示当前使用的 SIM。 + +如果希望管理台显示手机号,可在 `config.json` 中配置: + +```json +{ + "simCards": [ + { "slot": 0, "phoneNumber": "+8613800138000" }, + { "slot": 1, "phoneNumber": "+447700900000" } + ] +} +``` + +### 发送AT命令 +```bash +POST /api/modem/at +Content-Type: application/json + +{ + "command": "AT+CSQ" +} +``` + +### 获取日志 +```bash +GET /api/logs +``` + +所有API均需要有效 token。可以通过 `Authorization: Bearer `、`X-Web-Token` 请求头或 `token` 查询参数传入。 + +## 项目结构 + +``` +. +├── src/ +│ ├── index.js # 入口文件 +│ ├── modem.js # 4G模块通信 +│ ├── sms.js # 短信处理 +│ ├── push.js # 推送通道 +│ ├── concat.js # 长短信合并 +│ ├── api.js # REST API +│ └── logger.js # 日志系统 +├── config.json # 配置文件 +├── package.json +└── README.md +``` + +## 推送通道类型 + +| 类型 | type值 | 配置项 | +|------|--------|--------| +| 钉钉机器人 | `dingtalk` | url, secret(可选) | +| 飞书机器人 | `feishu` | url, secret(可选) | +| Telegram Bot | `telegram` | url(bot token), key1(chat_id) | +| PushPlus | `pushplus` | key1(token) | +| Server酱 | `serverchan` | key1(sendkey) | +| Bark | `bark` | url(bark服务器) | +| POST JSON | `post_json` | url | +| GET请求 | `get` | url | +| 自定义模板 | `custom` | url, customBody | + +`pushChannels` 中 `enabled: false` 的通道可以保留空字段;`enabled: true` 时必须配置对应目标: + +- `post_json`、`bark`、`get`、`dingtalk`、`custom`、`feishu`:必须有 `url`。 +- `telegram`:`url` 填 Bot Token,`key1` 填 Chat ID。 +- `pushplus`:`key1` 填 PushPlus Token。 +- `serverchan`:`key1` 填 Server酱 SendKey。 +- `custom`:`customBody` 如果填写,必须是合法 JSON 字符串,支持 `{sender}`、`{message}`、`{timestamp}` 占位符。 + +## 管理员命令 + +通过短信发送命令(仅管理员号码): + +### 远程发送短信 +``` +SMS:目标号码:短信内容 +``` +示例:`SMS:13800138000:测试消息` + +### 重启系统 +``` +RESET +``` + +## 长短信处理 + +- 自动检测长短信(分段数 > 1) +- 使用参考号和发送者号码标识同一条长短信 +- 收齐所有分段后自动合并 +- 30秒超时保护(未收齐也转发) + +## 故障排查 + +### 串口打不开 +```bash +# Linux检查串口设备 +ls -l /dev/ttyUSB* +# 添加用户到dialout组 +sudo usermod -a -G dialout $USER +``` + +### 模块不响应 +- 检查串口路径和波特率 +- 检查USB连接 +- 检查SIM卡是否插好 + +### 收不到短信 +- 检查AT+CEREG?网络注册状态 +- 检查AT+CNMI配置是否正确 +- 检查SIM卡余额和服务状态 + +## 许可证 + +MIT diff --git a/config.example.json b/config.example.json index 4f8063b..5d7c174 100644 --- a/config.example.json +++ b/config.example.json @@ -4,24 +4,47 @@ "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" }, + "_adminPhone_comment": "可选。配置后,只有该号码发来的短信会被当作管理员命令处理,例如SMS:号码:内容、RESET。", "adminPhone": "13800138000", + "_numberBlackList_comment": "可选。列表中的号码短信会被忽略;支持带+86或不带+86的匹配。", "numberBlackList": [ "10086", "10010" ], + "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, @@ -77,9 +100,6 @@ "api": { "port": 3000, "webToken": "change-this-token", - "auth": { - "username": "admin", - "password": "admin123" - } + "_comment": "必填。port是API和管理台监听端口;webToken是唯一鉴权凭据,可通过Authorization: Bearer、X-Web-Token、token查询参数或管理台登录页提交。" } }