feat: 添加 Docker 部署和短信持久化
This commit is contained in:
@@ -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,5 +1,6 @@
|
|||||||
node_modules/
|
node_modules/
|
||||||
logs/
|
logs/
|
||||||
|
data/
|
||||||
*.log
|
*.log
|
||||||
config.json
|
config.json
|
||||||
.DS_Store
|
.DS_Store
|
||||||
|
|||||||
+43
@@ -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"]
|
||||||
@@ -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
@@ -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;
|
||||||
|
|||||||
Reference in New Issue
Block a user