feat: 短信持久化会按当前 SIM 的 ICCID 隔离
This commit is contained in:
+1
-1
@@ -166,7 +166,7 @@
|
|||||||
<div class="inbox-panel">
|
<div class="inbox-panel">
|
||||||
<div class="inbox-meta">
|
<div class="inbox-meta">
|
||||||
<span id="messageCount">最近 0 条</span>
|
<span id="messageCount">最近 0 条</span>
|
||||||
<span class="inbox-meta__hint">新短信会自动出现在这里</span>
|
<span class="inbox-meta__hint" id="messageScope">新短信会自动出现在这里</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="message-list" id="messageList" aria-live="polite">
|
<div class="message-list" id="messageList" aria-live="polite">
|
||||||
<p class="empty-state empty-state--light">暂无收到的短信。</p>
|
<p class="empty-state empty-state--light">暂无收到的短信。</p>
|
||||||
|
|||||||
@@ -759,6 +759,26 @@ h3 {
|
|||||||
letter-spacing: -0.2px;
|
letter-spacing: -0.2px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.message-meta {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
margin-top: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.message-sim {
|
||||||
|
border: 1px solid var(--hairline);
|
||||||
|
border-radius: var(--r-pill);
|
||||||
|
background: var(--surface-soft);
|
||||||
|
color: var(--muted);
|
||||||
|
padding: 3px 9px;
|
||||||
|
font-family: var(--font-mono);
|
||||||
|
font-size: 11px;
|
||||||
|
line-height: 1.5;
|
||||||
|
overflow-wrap: anywhere;
|
||||||
|
}
|
||||||
|
|
||||||
.message-item__header time {
|
.message-item__header time {
|
||||||
color: var(--muted);
|
color: var(--muted);
|
||||||
font-family: var(--font-sans);
|
font-family: var(--font-sans);
|
||||||
|
|||||||
+20
-4
@@ -51,6 +51,7 @@ const elements = {
|
|||||||
smsStatus: document.querySelector('#smsStatus'),
|
smsStatus: document.querySelector('#smsStatus'),
|
||||||
messagesButton: document.querySelector('#messagesButton'),
|
messagesButton: document.querySelector('#messagesButton'),
|
||||||
messageCount: document.querySelector('#messageCount'),
|
messageCount: document.querySelector('#messageCount'),
|
||||||
|
messageScope: document.querySelector('#messageScope'),
|
||||||
messageList: document.querySelector('#messageList'),
|
messageList: document.querySelector('#messageList'),
|
||||||
atForm: document.querySelector('#atForm'),
|
atForm: document.querySelector('#atForm'),
|
||||||
atOutput: document.querySelector('#atOutput'),
|
atOutput: document.querySelector('#atOutput'),
|
||||||
@@ -311,9 +312,16 @@ function formatDateTime(value) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function renderMessages(messages) {
|
function renderMessages(messages, meta = {}) {
|
||||||
elements.messageList.replaceChildren();
|
elements.messageList.replaceChildren();
|
||||||
setText(elements.messageCount, `最近 ${messages.length} 条`);
|
const currentSimLabel = meta.currentSim?.simLabel || '当前SIM未知';
|
||||||
|
const countLabel = meta.partitionBySim ? `${currentSimLabel} 最近 ${messages.length} 条` : `全部SIM 最近 ${messages.length} 条`;
|
||||||
|
const scopeLabel = meta.partitionBySim
|
||||||
|
? (meta.currentSimKnown ? '仅显示当前SIM收到的短信' : '未识别当前SIM,收件箱已隐藏')
|
||||||
|
: 'SIM隔离未启用';
|
||||||
|
|
||||||
|
setText(elements.messageCount, countLabel);
|
||||||
|
setText(elements.messageScope, scopeLabel);
|
||||||
|
|
||||||
if (!messages.length) {
|
if (!messages.length) {
|
||||||
const empty = document.createElement('p');
|
const empty = document.createElement('p');
|
||||||
@@ -345,8 +353,16 @@ function renderMessages(messages) {
|
|||||||
text.className = 'message-text';
|
text.className = 'message-text';
|
||||||
text.textContent = message.text || '';
|
text.textContent = message.text || '';
|
||||||
|
|
||||||
|
const metaRow = document.createElement('div');
|
||||||
|
metaRow.className = 'message-meta';
|
||||||
|
|
||||||
|
const sim = document.createElement('span');
|
||||||
|
sim.className = 'message-sim';
|
||||||
|
sim.textContent = message.simLabel || '未知SIM';
|
||||||
|
|
||||||
|
metaRow.append(sim);
|
||||||
header.append(sender, status, time);
|
header.append(sender, status, time);
|
||||||
item.append(header, text);
|
item.append(header, metaRow, text);
|
||||||
fragment.append(item);
|
fragment.append(item);
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -664,7 +680,7 @@ async function refreshLogs() {
|
|||||||
|
|
||||||
async function refreshMessages() {
|
async function refreshMessages() {
|
||||||
const messages = await requestJSON('/api/sms/received?limit=50');
|
const messages = await requestJSON('/api/sms/received?limit=50');
|
||||||
renderMessages(messages.data || []);
|
renderMessages(messages.data || [], messages.meta || {});
|
||||||
}
|
}
|
||||||
|
|
||||||
async function toggleMobileData() {
|
async function toggleMobileData() {
|
||||||
|
|||||||
+14
-4
@@ -414,12 +414,18 @@ class APIServer {
|
|||||||
});
|
});
|
||||||
|
|
||||||
// 查询收到的短信
|
// 查询收到的短信
|
||||||
this.app.get('/api/sms/received', (req, res) => {
|
this.app.get('/api/sms/received', async (req, res) => {
|
||||||
try {
|
try {
|
||||||
const messages = this.smsProcessor.getReceivedMessages(req.query.limit);
|
const currentSimIdentity = await this.smsProcessor.getCurrentSimIdentity();
|
||||||
|
const result = this.smsProcessor.listReceivedMessages(req.query.limit, {
|
||||||
|
currentSimIdentity,
|
||||||
|
scope: req.query.scope
|
||||||
|
});
|
||||||
|
|
||||||
res.json({
|
res.json({
|
||||||
success: true,
|
success: true,
|
||||||
data: messages
|
data: result.messages,
|
||||||
|
meta: result.meta
|
||||||
});
|
});
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
logger.error('查询收到短信失败:', err);
|
logger.error('查询收到短信失败:', err);
|
||||||
@@ -450,12 +456,16 @@ class APIServer {
|
|||||||
// 查询模组信息
|
// 查询模组信息
|
||||||
this.app.get('/api/modem/info', async (req, res) => {
|
this.app.get('/api/modem/info', async (req, res) => {
|
||||||
try {
|
try {
|
||||||
const iccid = await this.modem.getICCID();
|
const iccid = await this.modem.getICCID({ refresh: true });
|
||||||
|
const simIdentity = this.smsProcessor.setCurrentSimFromICCID(iccid);
|
||||||
|
|
||||||
res.json({
|
res.json({
|
||||||
success: true,
|
success: true,
|
||||||
data: {
|
data: {
|
||||||
model: this.modem.modelInfo,
|
model: this.modem.modelInfo,
|
||||||
iccid,
|
iccid,
|
||||||
|
simId: simIdentity?.simId || null,
|
||||||
|
simLabel: simIdentity?.simLabel || '未知SIM',
|
||||||
ready: this.modem.ready
|
ready: this.modem.ready
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|||||||
+15
-8
@@ -13,10 +13,12 @@ class ConcatManager extends EventEmitter {
|
|||||||
/**
|
/**
|
||||||
* 查找或创建长短信槽位
|
* 查找或创建长短信槽位
|
||||||
*/
|
*/
|
||||||
findOrCreateSlot(refNumber, sender, totalParts) {
|
findOrCreateSlot(refNumber, sender, totalParts, simIdentity = null) {
|
||||||
|
const simId = simIdentity?.simId || '';
|
||||||
|
|
||||||
// 先查找是否已存在
|
// 先查找是否已存在
|
||||||
let slot = this.buffer.find(s =>
|
let slot = this.buffer.find(s =>
|
||||||
s.inUse && s.refNumber === refNumber && s.sender === sender
|
s.inUse && s.refNumber === refNumber && s.sender === sender && s.simId === simId
|
||||||
);
|
);
|
||||||
|
|
||||||
if (slot) {
|
if (slot) {
|
||||||
@@ -26,7 +28,7 @@ class ConcatManager extends EventEmitter {
|
|||||||
// 查找空闲槽位
|
// 查找空闲槽位
|
||||||
slot = this.buffer.find(s => !s.inUse);
|
slot = this.buffer.find(s => !s.inUse);
|
||||||
if (slot) {
|
if (slot) {
|
||||||
this.initSlot(slot, refNumber, sender, totalParts);
|
this.initSlot(slot, refNumber, sender, totalParts, simIdentity);
|
||||||
return slot;
|
return slot;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -44,17 +46,19 @@ class ConcatManager extends EventEmitter {
|
|||||||
logger.warn('长短信缓存已满,覆盖最老的槽位');
|
logger.warn('长短信缓存已满,覆盖最老的槽位');
|
||||||
}
|
}
|
||||||
|
|
||||||
this.initSlot(slot, refNumber, sender, totalParts);
|
this.initSlot(slot, refNumber, sender, totalParts, simIdentity);
|
||||||
return slot;
|
return slot;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 初始化槽位
|
* 初始化槽位
|
||||||
*/
|
*/
|
||||||
initSlot(slot, refNumber, sender, totalParts) {
|
initSlot(slot, refNumber, sender, totalParts, simIdentity = null) {
|
||||||
slot.inUse = true;
|
slot.inUse = true;
|
||||||
slot.refNumber = refNumber;
|
slot.refNumber = refNumber;
|
||||||
slot.sender = sender;
|
slot.sender = sender;
|
||||||
|
slot.simId = simIdentity?.simId || '';
|
||||||
|
slot.simIdentity = simIdentity;
|
||||||
slot.totalParts = totalParts;
|
slot.totalParts = totalParts;
|
||||||
slot.receivedParts = 0;
|
slot.receivedParts = 0;
|
||||||
slot.firstPartTime = Date.now();
|
slot.firstPartTime = Date.now();
|
||||||
@@ -65,10 +69,10 @@ class ConcatManager extends EventEmitter {
|
|||||||
/**
|
/**
|
||||||
* 添加短信分段
|
* 添加短信分段
|
||||||
*/
|
*/
|
||||||
addPart(refNumber, sender, partNumber, totalParts, text, timestamp) {
|
addPart(refNumber, sender, partNumber, totalParts, text, timestamp, options = {}) {
|
||||||
logger.info(`收到长短信分段 ${partNumber}/${totalParts}, 参考号: ${refNumber}`);
|
logger.info(`收到长短信分段 ${partNumber}/${totalParts}, 参考号: ${refNumber}`);
|
||||||
|
|
||||||
const slot = this.findOrCreateSlot(refNumber, sender, totalParts);
|
const slot = this.findOrCreateSlot(refNumber, sender, totalParts, options.simIdentity);
|
||||||
const partIndex = partNumber - 1; // partNumber从1开始,数组从0开始
|
const partIndex = partNumber - 1; // partNumber从1开始,数组从0开始
|
||||||
|
|
||||||
if (partIndex >= 0 && partIndex < this.maxParts) {
|
if (partIndex >= 0 && partIndex < this.maxParts) {
|
||||||
@@ -110,7 +114,8 @@ class ConcatManager extends EventEmitter {
|
|||||||
this.emit('complete', {
|
this.emit('complete', {
|
||||||
sender: slot.sender,
|
sender: slot.sender,
|
||||||
text: fullText,
|
text: fullText,
|
||||||
timestamp: slot.timestamp
|
timestamp: slot.timestamp,
|
||||||
|
simIdentity: slot.simIdentity
|
||||||
});
|
});
|
||||||
|
|
||||||
// 清空槽位
|
// 清空槽位
|
||||||
@@ -124,6 +129,8 @@ class ConcatManager extends EventEmitter {
|
|||||||
slot.inUse = false;
|
slot.inUse = false;
|
||||||
slot.parts = [];
|
slot.parts = [];
|
||||||
slot.receivedParts = 0;
|
slot.receivedParts = 0;
|
||||||
|
slot.simId = '';
|
||||||
|
slot.simIdentity = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
+3
-1
@@ -60,7 +60,9 @@ async function main() {
|
|||||||
// 监听长短信合并完成事件
|
// 监听长短信合并完成事件
|
||||||
concatManager.on('complete', async (sms) => {
|
concatManager.on('complete', async (sms) => {
|
||||||
logger.info('收到长短信合并完成事件');
|
logger.info('收到长短信合并完成事件');
|
||||||
await smsProcessor.processSmsContent(sms.sender, sms.text, sms.timestamp);
|
await smsProcessor.processSmsContent(sms.sender, sms.text, sms.timestamp, {
|
||||||
|
simIdentity: sms.simIdentity
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
// 监听模组短信事件
|
// 监听模组短信事件
|
||||||
|
|||||||
+31
-2
@@ -21,6 +21,8 @@ class ModemManager extends EventEmitter {
|
|||||||
model: '未知',
|
model: '未知',
|
||||||
version: '未知'
|
version: '未知'
|
||||||
};
|
};
|
||||||
|
this.iccid = null;
|
||||||
|
this.iccidCheckedAt = null;
|
||||||
this.mobileData = {
|
this.mobileData = {
|
||||||
cid: this.mobileDataConfig.cid,
|
cid: this.mobileDataConfig.cid,
|
||||||
desiredEnabled: false,
|
desiredEnabled: false,
|
||||||
@@ -996,6 +998,7 @@ class ModemManager extends EventEmitter {
|
|||||||
|
|
||||||
// 3. 按ML307A文档先确认SIM卡和协议栈状态
|
// 3. 按ML307A文档先确认SIM卡和协议栈状态
|
||||||
await this.waitSIMReady();
|
await this.waitSIMReady();
|
||||||
|
await this.getICCID({ refresh: true });
|
||||||
await this.ensureFullFunctionality();
|
await this.ensureFullFunctionality();
|
||||||
|
|
||||||
// 4. 启动保护:先断开应用层拨号,再等待短信所需的网络注册。
|
// 4. 启动保护:先断开应用层拨号,再等待短信所需的网络注册。
|
||||||
@@ -1308,20 +1311,46 @@ class ModemManager extends EventEmitter {
|
|||||||
/**
|
/**
|
||||||
* 查询ICCID
|
* 查询ICCID
|
||||||
*/
|
*/
|
||||||
async getICCID() {
|
async getICCID(options = {}) {
|
||||||
|
if (!options.refresh && this.iccid) {
|
||||||
|
return this.iccid;
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const resp = await this.sendATCommand('AT+CCID', 2000);
|
const resp = await this.sendATCommand('AT+CCID', 2000);
|
||||||
const match = resp.match(/\+CCID:\s*(\d+)/);
|
const match = resp.match(/\+CCID:\s*(\d+)/);
|
||||||
if (match) {
|
if (match) {
|
||||||
return match[1];
|
const previousICCID = this.iccid;
|
||||||
|
this.iccid = match[1];
|
||||||
|
this.iccidCheckedAt = new Date().toISOString();
|
||||||
|
if (previousICCID !== this.iccid) {
|
||||||
|
logger.info(`✓ 当前SIM ICCID: ${this.formatICCID(this.iccid)}`);
|
||||||
|
}
|
||||||
|
return this.iccid;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
this.iccid = null;
|
||||||
|
this.iccidCheckedAt = new Date().toISOString();
|
||||||
return null;
|
return null;
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
|
if (options.refresh) {
|
||||||
|
this.iccid = null;
|
||||||
|
this.iccidCheckedAt = new Date().toISOString();
|
||||||
|
}
|
||||||
logger.error('查询ICCID失败:', err);
|
logger.error('查询ICCID失败:', err);
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
formatICCID(iccid) {
|
||||||
|
const value = String(iccid || '');
|
||||||
|
if (!value) {
|
||||||
|
return '未知';
|
||||||
|
}
|
||||||
|
|
||||||
|
return `...${value.slice(-6)}`;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 关闭串口
|
* 关闭串口
|
||||||
*/
|
*/
|
||||||
|
|||||||
+221
-8
@@ -1,4 +1,5 @@
|
|||||||
import { parse } from 'node-pdu';
|
import { parse } from 'node-pdu';
|
||||||
|
import crypto from 'crypto';
|
||||||
import fs from 'fs';
|
import fs from 'fs';
|
||||||
import path from 'path';
|
import path from 'path';
|
||||||
import { fileURLToPath } from 'url';
|
import { fileURLToPath } from 'url';
|
||||||
@@ -17,6 +18,12 @@ class SMSProcessor {
|
|||||||
this.receivedMessageSeq = 0;
|
this.receivedMessageSeq = 0;
|
||||||
this.maxReceivedMessages = this.resolveMaxReceivedMessages();
|
this.maxReceivedMessages = this.resolveMaxReceivedMessages();
|
||||||
this.receivedMessagesFile = this.resolveReceivedMessagesFile();
|
this.receivedMessagesFile = this.resolveReceivedMessagesFile();
|
||||||
|
this.partitionMessagesBySim = this.resolvePartitionMessagesBySim();
|
||||||
|
this.allowAllSimMessages = this.resolveAllowAllSimMessages();
|
||||||
|
this.currentSimIdentity = null;
|
||||||
|
this.currentSimCheckedAt = 0;
|
||||||
|
this.currentSimLookup = null;
|
||||||
|
this.simIdentityTtlMs = 5 * 1000;
|
||||||
this.loadReceivedMessages();
|
this.loadReceivedMessages();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -40,10 +47,12 @@ class SMSProcessor {
|
|||||||
const text = parsed.data?.getText?.() || '';
|
const text = parsed.data?.getText?.() || '';
|
||||||
const part = parsed.data?.parts?.[0];
|
const part = parsed.data?.parts?.[0];
|
||||||
const header = part?.header;
|
const header = part?.header;
|
||||||
|
const simIdentity = await this.getCurrentSimIdentity();
|
||||||
|
|
||||||
logger.info('✓ PDU解析成功');
|
logger.info('✓ PDU解析成功');
|
||||||
logger.info(`发送者: ${sender}`);
|
logger.info(`发送者: ${sender}`);
|
||||||
logger.info(`时间戳: ${timestamp}`);
|
logger.info(`时间戳: ${timestamp}`);
|
||||||
|
logger.info(`收件SIM: ${simIdentity?.simLabel || '未知SIM'}`);
|
||||||
logger.info(`内容: ${text}`);
|
logger.info(`内容: ${text}`);
|
||||||
|
|
||||||
// 检查是否为长短信
|
// 检查是否为长短信
|
||||||
@@ -62,11 +71,12 @@ class SMSProcessor {
|
|||||||
partNumber,
|
partNumber,
|
||||||
totalParts,
|
totalParts,
|
||||||
text,
|
text,
|
||||||
timestamp
|
timestamp,
|
||||||
|
{ simIdentity }
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
// 普通短信,直接处理
|
// 普通短信,直接处理
|
||||||
await this.processSmsContent(sender, text, timestamp);
|
await this.processSmsContent(sender, text, timestamp, { simIdentity });
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
logger.error('处理PDU失败:', err);
|
logger.error('处理PDU失败:', err);
|
||||||
@@ -76,14 +86,19 @@ class SMSProcessor {
|
|||||||
/**
|
/**
|
||||||
* 处理短信内容并转发
|
* 处理短信内容并转发
|
||||||
*/
|
*/
|
||||||
async processSmsContent(sender, text, timestamp) {
|
async processSmsContent(sender, text, timestamp, options = {}) {
|
||||||
|
const simIdentity = options.simIdentity !== undefined
|
||||||
|
? this.normalizeSimIdentity(options.simIdentity)
|
||||||
|
: await this.getCurrentSimIdentity();
|
||||||
|
|
||||||
logger.info('=== 处理短信内容 ===');
|
logger.info('=== 处理短信内容 ===');
|
||||||
logger.info(`发送者: ${sender}`);
|
logger.info(`发送者: ${sender}`);
|
||||||
logger.info(`时间戳: ${timestamp}`);
|
logger.info(`时间戳: ${timestamp}`);
|
||||||
|
logger.info(`收件SIM: ${simIdentity?.simLabel || '未知SIM'}`);
|
||||||
logger.info(`内容: ${text}`);
|
logger.info(`内容: ${text}`);
|
||||||
logger.info('====================');
|
logger.info('====================');
|
||||||
|
|
||||||
const messageRecord = this.addReceivedMessage(sender, text, timestamp);
|
const messageRecord = this.addReceivedMessage(sender, text, timestamp, simIdentity);
|
||||||
|
|
||||||
// 推送到所有通道
|
// 推送到所有通道
|
||||||
await this.pushManager.pushToAll(sender, text, timestamp);
|
await this.pushManager.pushToAll(sender, text, timestamp);
|
||||||
@@ -102,13 +117,16 @@ class SMSProcessor {
|
|||||||
/**
|
/**
|
||||||
* 记录收到的短信,供Web管理端展示。
|
* 记录收到的短信,供Web管理端展示。
|
||||||
*/
|
*/
|
||||||
addReceivedMessage(sender, text, timestamp) {
|
addReceivedMessage(sender, text, timestamp, simIdentity = null) {
|
||||||
|
const normalizedSimIdentity = this.normalizeSimIdentity(simIdentity);
|
||||||
const message = {
|
const message = {
|
||||||
id: `${Date.now()}-${++this.receivedMessageSeq}`,
|
id: `${Date.now()}-${++this.receivedMessageSeq}`,
|
||||||
sender,
|
sender,
|
||||||
text,
|
text,
|
||||||
timestamp,
|
timestamp,
|
||||||
receivedAt: new Date().toISOString(),
|
receivedAt: new Date().toISOString(),
|
||||||
|
simId: normalizedSimIdentity?.simId || '',
|
||||||
|
simLabel: normalizedSimIdentity?.simLabel || '未知SIM',
|
||||||
status: 'received',
|
status: 'received',
|
||||||
statusText: '已接收'
|
statusText: '已接收'
|
||||||
};
|
};
|
||||||
@@ -119,7 +137,7 @@ class SMSProcessor {
|
|||||||
}
|
}
|
||||||
|
|
||||||
this.persistReceivedMessages();
|
this.persistReceivedMessages();
|
||||||
logger.info(`短信已进入Web收件箱: ${sender}`);
|
logger.info(`短信已进入Web收件箱: ${sender} (${message.simLabel})`);
|
||||||
return message;
|
return message;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -138,8 +156,53 @@ class SMSProcessor {
|
|||||||
* 获取最近收到的短信。
|
* 获取最近收到的短信。
|
||||||
*/
|
*/
|
||||||
getReceivedMessages(limit = 50) {
|
getReceivedMessages(limit = 50) {
|
||||||
const safeLimit = Math.min(Math.max(Number(limit) || 50, 1), this.maxReceivedMessages);
|
return this.receivedMessages.slice(0, this.resolveReceivedMessagesLimit(limit));
|
||||||
return this.receivedMessages.slice(0, safeLimit);
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取按SIM作用域过滤后的短信。
|
||||||
|
*/
|
||||||
|
listReceivedMessages(limit = 50, options = {}) {
|
||||||
|
const safeLimit = this.resolveReceivedMessagesLimit(limit);
|
||||||
|
const requestedScope = String(options.scope || 'current').trim().toLowerCase();
|
||||||
|
const currentSim = this.normalizeSimIdentity(options.currentSimIdentity);
|
||||||
|
const includeAllSims = requestedScope === 'all' && this.allowAllSimMessages;
|
||||||
|
|
||||||
|
let effectiveScope = includeAllSims ? 'all' : 'current';
|
||||||
|
let messages = this.receivedMessages;
|
||||||
|
|
||||||
|
if (this.partitionMessagesBySim && !includeAllSims) {
|
||||||
|
if (currentSim?.simId) {
|
||||||
|
messages = this.receivedMessages.filter(message => message.simId === currentSim.simId);
|
||||||
|
} else {
|
||||||
|
messages = [];
|
||||||
|
effectiveScope = 'unavailable';
|
||||||
|
}
|
||||||
|
} else if (!this.partitionMessagesBySim) {
|
||||||
|
effectiveScope = 'all';
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
messages: messages.slice(0, safeLimit),
|
||||||
|
meta: {
|
||||||
|
limit: safeLimit,
|
||||||
|
total: messages.length,
|
||||||
|
scope: effectiveScope,
|
||||||
|
requestedScope,
|
||||||
|
partitionBySim: this.partitionMessagesBySim,
|
||||||
|
allSimMessagesAllowed: this.allowAllSimMessages,
|
||||||
|
allSimMessagesDenied: this.partitionMessagesBySim && requestedScope === 'all' && !this.allowAllSimMessages,
|
||||||
|
currentSim,
|
||||||
|
currentSimKnown: Boolean(currentSim?.simId)
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 限制收件箱查询条数。
|
||||||
|
*/
|
||||||
|
resolveReceivedMessagesLimit(limit = 50) {
|
||||||
|
return Math.min(Math.max(Number(limit) || 50, 1), this.maxReceivedMessages);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -154,6 +217,20 @@ class SMSProcessor {
|
|||||||
return Math.min(Math.floor(configured), 5000);
|
return Math.min(Math.floor(configured), 5000);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 是否按当前SIM隔离Web收件箱。默认开启,避免换卡后读取其他SIM历史。
|
||||||
|
*/
|
||||||
|
resolvePartitionMessagesBySim() {
|
||||||
|
return this.config.inbox?.partitionBySim !== false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 是否允许显式查询全部SIM历史。默认关闭。
|
||||||
|
*/
|
||||||
|
resolveAllowAllSimMessages() {
|
||||||
|
return this.config.inbox?.allowAllSimMessages === true;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 解析收件箱持久化文件路径。
|
* 解析收件箱持久化文件路径。
|
||||||
*/
|
*/
|
||||||
@@ -232,17 +309,153 @@ class SMSProcessor {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const simIdentity = this.normalizeStoredSimIdentity(item);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
id,
|
id,
|
||||||
sender: String(item.sender || '未知号码'),
|
sender: String(item.sender || '未知号码'),
|
||||||
text: String(item.text || ''),
|
text: String(item.text || ''),
|
||||||
timestamp: String(item.timestamp || item.receivedAt || new Date().toISOString()),
|
timestamp: String(item.timestamp || item.receivedAt || new Date().toISOString()),
|
||||||
receivedAt: String(item.receivedAt || new Date().toISOString()),
|
receivedAt: String(item.receivedAt || new Date().toISOString()),
|
||||||
|
simId: simIdentity?.simId || '',
|
||||||
|
simLabel: simIdentity?.simLabel || '未知SIM',
|
||||||
status: String(item.status || 'received'),
|
status: String(item.status || 'received'),
|
||||||
statusText: String(item.statusText || '已接收')
|
statusText: String(item.statusText || '已接收')
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 从历史记录中恢复SIM身份。旧记录没有SIM字段,会归到未知SIM。
|
||||||
|
*/
|
||||||
|
normalizeStoredSimIdentity(item) {
|
||||||
|
if (item.simId || item.simLabel) {
|
||||||
|
return this.normalizeSimIdentity({
|
||||||
|
simId: item.simId,
|
||||||
|
simLabel: item.simLabel
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (item.sim && typeof item.sim === 'object') {
|
||||||
|
return this.normalizeSimIdentity(item.sim);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (item.simIccid || item.iccid) {
|
||||||
|
return this.createSimIdentityFromICCID(item.simIccid || item.iccid);
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取当前SIM身份,用于收到短信时打标和Web收件箱过滤。
|
||||||
|
*/
|
||||||
|
async getCurrentSimIdentity(options = {}) {
|
||||||
|
const refresh = options.refresh === true;
|
||||||
|
const now = Date.now();
|
||||||
|
|
||||||
|
if (!refresh && this.currentSimCheckedAt && now - this.currentSimCheckedAt < this.simIdentityTtlMs) {
|
||||||
|
return this.currentSimIdentity;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.currentSimLookup) {
|
||||||
|
return this.currentSimLookup;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!this.modem?.getICCID) {
|
||||||
|
this.currentSimIdentity = null;
|
||||||
|
this.currentSimCheckedAt = now;
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.currentSimLookup = (async () => {
|
||||||
|
try {
|
||||||
|
const iccid = await this.modem.getICCID({ refresh: true });
|
||||||
|
return this.setCurrentSimFromICCID(iccid);
|
||||||
|
} catch (err) {
|
||||||
|
logger.warn(`查询当前SIM标识失败: ${err.message}`);
|
||||||
|
this.currentSimIdentity = null;
|
||||||
|
this.currentSimCheckedAt = Date.now();
|
||||||
|
return null;
|
||||||
|
} finally {
|
||||||
|
this.currentSimLookup = null;
|
||||||
|
}
|
||||||
|
})();
|
||||||
|
|
||||||
|
return this.currentSimLookup;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 使用ICCID更新当前SIM身份缓存。
|
||||||
|
*/
|
||||||
|
setCurrentSimFromICCID(iccid) {
|
||||||
|
return this.setCurrentSimIdentity(this.createSimIdentityFromICCID(iccid));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 更新当前SIM身份缓存。
|
||||||
|
*/
|
||||||
|
setCurrentSimIdentity(identity) {
|
||||||
|
this.currentSimIdentity = this.normalizeSimIdentity(identity);
|
||||||
|
this.currentSimCheckedAt = Date.now();
|
||||||
|
return this.currentSimIdentity;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 根据ICCID生成稳定但不暴露完整卡号的SIM标识。
|
||||||
|
*/
|
||||||
|
createSimIdentityFromICCID(iccid) {
|
||||||
|
const normalized = this.normalizeICCID(iccid);
|
||||||
|
if (!normalized) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
simId: crypto.createHash('sha256').update(`iccid:${normalized}`).digest('hex').slice(0, 24),
|
||||||
|
simLabel: this.formatSimLabel(normalized)
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
normalizeICCID(iccid) {
|
||||||
|
return String(iccid || '').replace(/\D/g, '');
|
||||||
|
}
|
||||||
|
|
||||||
|
formatSimLabel(iccid) {
|
||||||
|
const value = String(iccid || '');
|
||||||
|
if (!value) {
|
||||||
|
return '未知SIM';
|
||||||
|
}
|
||||||
|
|
||||||
|
return `ICCID ...${value.slice(-6)}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
normalizeSimIdentity(identity) {
|
||||||
|
if (!identity) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeof identity === 'string') {
|
||||||
|
return this.createSimIdentityFromICCID(identity);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeof identity !== 'object' || Array.isArray(identity)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (identity.iccid || identity.simIccid) {
|
||||||
|
return this.createSimIdentityFromICCID(identity.iccid || identity.simIccid);
|
||||||
|
}
|
||||||
|
|
||||||
|
const simId = String(identity.simId || identity.id || '').trim();
|
||||||
|
if (!simId) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
simId,
|
||||||
|
simLabel: String(identity.simLabel || identity.label || '未知SIM')
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export default SMSProcessor;
|
export default SMSProcessor;
|
||||||
|
|||||||
Reference in New Issue
Block a user