feat: 添加 Docker 部署和短信持久化

This commit is contained in:
2026-06-26 16:29:19 +08:00
parent 557049b308
commit d1a87556c5
5 changed files with 200 additions and 1 deletions
+18
View File
@@ -0,0 +1,18 @@
.git
.env
.npm
.npm-cache
.vscode
.idea
node_modules
logs
data
config.json
*.log
*.tgz
*.swp
*.swo
*~
AGENTS.md
DESIGN.md
ML307A_*.md
+1
View File
@@ -1,5 +1,6 @@
node_modules/ node_modules/
logs/ logs/
data/
*.log *.log
config.json config.json
.DS_Store .DS_Store
+43
View File
@@ -0,0 +1,43 @@
FROM node:20-bookworm-slim AS deps
ENV NODE_ENV=production \
DEBIAN_FRONTEND=noninteractive \
NPM_CONFIG_AUDIT=false \
NPM_CONFIG_FUND=false \
NPM_CONFIG_UPDATE_NOTIFIER=false
WORKDIR /app
RUN apt-get update \
&& apt-get install -y --no-install-recommends python3 make g++ \
&& rm -rf /var/lib/apt/lists/*
COPY package.json package-lock.json ./
RUN npm ci --omit=dev \
&& npm cache clean --force
FROM node:20-bookworm-slim AS runtime
ENV NODE_ENV=production \
DEBIAN_FRONTEND=noninteractive \
NPM_CONFIG_AUDIT=false \
NPM_CONFIG_FUND=false \
NPM_CONFIG_UPDATE_NOTIFIER=false
WORKDIR /app
RUN apt-get update \
&& apt-get install -y --no-install-recommends udev \
&& rm -rf /var/lib/apt/lists/*
COPY --from=deps /app/node_modules ./node_modules
COPY package.json package-lock.json ./
COPY src ./src
COPY public ./public
COPY config.example.json ./
RUN mkdir -p logs data
EXPOSE 3000
CMD ["node", "src/index.js"]
+26
View File
@@ -0,0 +1,26 @@
# Before starting:
# cp config.example.json config.json
# Keep serial.autoDetect enabled in config.json so the app can probe visible serial ports.
services:
sms-gateway:
build:
context: .
dockerfile: Dockerfile
image: sms-forwarding:latest
container_name: sms-forwarding
restart: unless-stopped
init: true
environment:
NODE_ENV: production
TZ: ${TZ:-Asia/Shanghai}
ports:
- "${SMS_GATEWAY_PORT:-3000}:3000"
volumes:
- ./config.json:/app/config.json
- ./logs:/app/logs
- ./data:/app/data
- /dev:/dev
- /sys:/sys:ro
device_cgroup_rules:
- "c 188:* rwm"
- "c 166:* rwm"
+112 -1
View File
@@ -1,6 +1,12 @@
import { parse } from 'node-pdu'; import { parse } from 'node-pdu';
import fs from 'fs';
import path from 'path';
import { fileURLToPath } from 'url';
import logger from './logger.js'; import logger from './logger.js';
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
class SMSProcessor { class SMSProcessor {
constructor(config, modem, concatManager, pushManager) { constructor(config, modem, concatManager, pushManager) {
this.config = config; this.config = config;
@@ -9,7 +15,9 @@ class SMSProcessor {
this.pushManager = pushManager; this.pushManager = pushManager;
this.receivedMessages = []; this.receivedMessages = [];
this.receivedMessageSeq = 0; this.receivedMessageSeq = 0;
this.maxReceivedMessages = 200; this.maxReceivedMessages = this.resolveMaxReceivedMessages();
this.receivedMessagesFile = this.resolveReceivedMessagesFile();
this.loadReceivedMessages();
} }
/** /**
@@ -110,6 +118,7 @@ class SMSProcessor {
this.receivedMessages.length = this.maxReceivedMessages; this.receivedMessages.length = this.maxReceivedMessages;
} }
this.persistReceivedMessages();
logger.info(`短信已进入Web收件箱: ${sender}`); logger.info(`短信已进入Web收件箱: ${sender}`);
return message; return message;
} }
@@ -121,6 +130,7 @@ class SMSProcessor {
const message = this.receivedMessages.find(item => item.id === id); const message = this.receivedMessages.find(item => item.id === id);
if (message) { if (message) {
Object.assign(message, patch); Object.assign(message, patch);
this.persistReceivedMessages();
} }
} }
@@ -132,6 +142,107 @@ class SMSProcessor {
return this.receivedMessages.slice(0, safeLimit); return this.receivedMessages.slice(0, safeLimit);
} }
/**
* 解析收件箱保留条数。
*/
resolveMaxReceivedMessages() {
const configured = Number(this.config.inbox?.maxReceivedMessages);
if (!Number.isFinite(configured) || configured <= 0) {
return 200;
}
return Math.min(Math.floor(configured), 5000);
}
/**
* 解析收件箱持久化文件路径。
*/
resolveReceivedMessagesFile() {
const configuredPath = this.config.inbox?.receivedMessagesFile || 'data/received-messages.json';
if (path.isAbsolute(configuredPath)) {
return configuredPath;
}
return path.join(__dirname, '..', configuredPath);
}
/**
* 启动时加载持久化收件箱。
*/
loadReceivedMessages() {
try {
fs.mkdirSync(path.dirname(this.receivedMessagesFile), { recursive: true });
if (!fs.existsSync(this.receivedMessagesFile)) {
logger.info(`短信收件箱持久化文件不存在,将在收到短信后创建: ${this.receivedMessagesFile}`);
return;
}
const content = fs.readFileSync(this.receivedMessagesFile, 'utf8').trim();
if (!content) {
return;
}
const parsed = JSON.parse(content);
if (!Array.isArray(parsed)) {
logger.warn('短信收件箱持久化文件格式不是数组,已忽略');
return;
}
this.receivedMessages = parsed
.map(item => this.normalizeStoredMessage(item))
.filter(Boolean)
.slice(0, this.maxReceivedMessages);
logger.info(`已加载持久化短信收件箱: ${this.receivedMessages.length}`);
} catch (err) {
logger.error('加载短信收件箱持久化文件失败:', err);
this.receivedMessages = [];
}
}
/**
* 保存当前收件箱到磁盘。
*/
persistReceivedMessages() {
try {
fs.mkdirSync(path.dirname(this.receivedMessagesFile), { recursive: true });
const tmpFile = `${this.receivedMessagesFile}.${process.pid}.tmp`;
fs.writeFileSync(
tmpFile,
`${JSON.stringify(this.receivedMessages, null, 2)}\n`,
'utf8'
);
fs.renameSync(tmpFile, this.receivedMessagesFile);
} catch (err) {
logger.error('保存短信收件箱持久化文件失败:', err);
}
}
/**
* 规范化历史短信记录,避免坏数据影响Web展示。
*/
normalizeStoredMessage(item) {
if (!item || typeof item !== 'object' || Array.isArray(item)) {
return null;
}
const id = String(item.id || '').trim();
if (!id) {
return null;
}
return {
id,
sender: String(item.sender || '未知号码'),
text: String(item.text || ''),
timestamp: String(item.timestamp || item.receivedAt || new Date().toISOString()),
receivedAt: String(item.receivedAt || new Date().toISOString()),
status: String(item.status || 'received'),
statusText: String(item.statusText || '已接收')
};
}
} }
export default SMSProcessor; export default SMSProcessor;