diff --git a/public/admin.html b/public/admin.html index 93f7fa5..7650c80 100644 --- a/public/admin.html +++ b/public/admin.html @@ -166,7 +166,7 @@
最近 0 条 - 新短信会自动出现在这里 + 新短信会自动出现在这里

暂无收到的短信。

diff --git a/public/assets/admin.css b/public/assets/admin.css index 27a977e..183e606 100644 --- a/public/assets/admin.css +++ b/public/assets/admin.css @@ -759,6 +759,26 @@ h3 { 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 { color: var(--muted); font-family: var(--font-sans); diff --git a/public/assets/admin.js b/public/assets/admin.js index 4d16df9..15650fa 100644 --- a/public/assets/admin.js +++ b/public/assets/admin.js @@ -51,6 +51,7 @@ const elements = { smsStatus: document.querySelector('#smsStatus'), messagesButton: document.querySelector('#messagesButton'), messageCount: document.querySelector('#messageCount'), + messageScope: document.querySelector('#messageScope'), messageList: document.querySelector('#messageList'), atForm: document.querySelector('#atForm'), atOutput: document.querySelector('#atOutput'), @@ -311,9 +312,16 @@ function formatDateTime(value) { }); } -function renderMessages(messages) { +function renderMessages(messages, meta = {}) { 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) { const empty = document.createElement('p'); @@ -345,8 +353,16 @@ function renderMessages(messages) { text.className = '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); - item.append(header, text); + item.append(header, metaRow, text); fragment.append(item); }); @@ -664,7 +680,7 @@ async function refreshLogs() { async function refreshMessages() { const messages = await requestJSON('/api/sms/received?limit=50'); - renderMessages(messages.data || []); + renderMessages(messages.data || [], messages.meta || {}); } async function toggleMobileData() { diff --git a/src/api.js b/src/api.js index fda9e89..ba6e024 100644 --- a/src/api.js +++ b/src/api.js @@ -414,12 +414,18 @@ class APIServer { }); // 查询收到的短信 - this.app.get('/api/sms/received', (req, res) => { + this.app.get('/api/sms/received', async (req, res) => { 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({ success: true, - data: messages + data: result.messages, + meta: result.meta }); } catch (err) { logger.error('查询收到短信失败:', err); @@ -450,12 +456,16 @@ class APIServer { // 查询模组信息 this.app.get('/api/modem/info', async (req, res) => { try { - const iccid = await this.modem.getICCID(); + const iccid = await this.modem.getICCID({ refresh: true }); + const simIdentity = this.smsProcessor.setCurrentSimFromICCID(iccid); + res.json({ success: true, data: { model: this.modem.modelInfo, iccid, + simId: simIdentity?.simId || null, + simLabel: simIdentity?.simLabel || '未知SIM', ready: this.modem.ready } }); diff --git a/src/concat.js b/src/concat.js index 64e3f5d..42dff96 100644 --- a/src/concat.js +++ b/src/concat.js @@ -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 => - s.inUse && s.refNumber === refNumber && s.sender === sender + s.inUse && s.refNumber === refNumber && s.sender === sender && s.simId === simId ); if (slot) { @@ -26,7 +28,7 @@ class ConcatManager extends EventEmitter { // 查找空闲槽位 slot = this.buffer.find(s => !s.inUse); if (slot) { - this.initSlot(slot, refNumber, sender, totalParts); + this.initSlot(slot, refNumber, sender, totalParts, simIdentity); return slot; } @@ -44,17 +46,19 @@ class ConcatManager extends EventEmitter { logger.warn('长短信缓存已满,覆盖最老的槽位'); } - this.initSlot(slot, refNumber, sender, totalParts); + this.initSlot(slot, refNumber, sender, totalParts, simIdentity); return slot; } /** * 初始化槽位 */ - initSlot(slot, refNumber, sender, totalParts) { + initSlot(slot, refNumber, sender, totalParts, simIdentity = null) { slot.inUse = true; slot.refNumber = refNumber; slot.sender = sender; + slot.simId = simIdentity?.simId || ''; + slot.simIdentity = simIdentity; slot.totalParts = totalParts; slot.receivedParts = 0; 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}`); - const slot = this.findOrCreateSlot(refNumber, sender, totalParts); + const slot = this.findOrCreateSlot(refNumber, sender, totalParts, options.simIdentity); const partIndex = partNumber - 1; // partNumber从1开始,数组从0开始 if (partIndex >= 0 && partIndex < this.maxParts) { @@ -110,7 +114,8 @@ class ConcatManager extends EventEmitter { this.emit('complete', { sender: slot.sender, text: fullText, - timestamp: slot.timestamp + timestamp: slot.timestamp, + simIdentity: slot.simIdentity }); // 清空槽位 @@ -124,6 +129,8 @@ class ConcatManager extends EventEmitter { slot.inUse = false; slot.parts = []; slot.receivedParts = 0; + slot.simId = ''; + slot.simIdentity = null; } /** diff --git a/src/index.js b/src/index.js index f445d98..fed6422 100644 --- a/src/index.js +++ b/src/index.js @@ -60,7 +60,9 @@ async function main() { // 监听长短信合并完成事件 concatManager.on('complete', async (sms) => { logger.info('收到长短信合并完成事件'); - await smsProcessor.processSmsContent(sms.sender, sms.text, sms.timestamp); + await smsProcessor.processSmsContent(sms.sender, sms.text, sms.timestamp, { + simIdentity: sms.simIdentity + }); }); // 监听模组短信事件 diff --git a/src/modem.js b/src/modem.js index 0d1475f..2fdb34d 100644 --- a/src/modem.js +++ b/src/modem.js @@ -21,6 +21,8 @@ class ModemManager extends EventEmitter { model: '未知', version: '未知' }; + this.iccid = null; + this.iccidCheckedAt = null; this.mobileData = { cid: this.mobileDataConfig.cid, desiredEnabled: false, @@ -996,6 +998,7 @@ class ModemManager extends EventEmitter { // 3. 按ML307A文档先确认SIM卡和协议栈状态 await this.waitSIMReady(); + await this.getICCID({ refresh: true }); await this.ensureFullFunctionality(); // 4. 启动保护:先断开应用层拨号,再等待短信所需的网络注册。 @@ -1308,20 +1311,46 @@ class ModemManager extends EventEmitter { /** * 查询ICCID */ - async getICCID() { + async getICCID(options = {}) { + if (!options.refresh && this.iccid) { + return this.iccid; + } + try { const resp = await this.sendATCommand('AT+CCID', 2000); const match = resp.match(/\+CCID:\s*(\d+)/); 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; } catch (err) { + if (options.refresh) { + this.iccid = null; + this.iccidCheckedAt = new Date().toISOString(); + } logger.error('查询ICCID失败:', err); return null; } } + formatICCID(iccid) { + const value = String(iccid || ''); + if (!value) { + return '未知'; + } + + return `...${value.slice(-6)}`; + } + /** * 关闭串口 */ diff --git a/src/sms.js b/src/sms.js index 69506f3..251aff4 100644 --- a/src/sms.js +++ b/src/sms.js @@ -1,4 +1,5 @@ import { parse } from 'node-pdu'; +import crypto from 'crypto'; import fs from 'fs'; import path from 'path'; import { fileURLToPath } from 'url'; @@ -17,6 +18,12 @@ class SMSProcessor { this.receivedMessageSeq = 0; this.maxReceivedMessages = this.resolveMaxReceivedMessages(); 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(); } @@ -40,10 +47,12 @@ class SMSProcessor { const text = parsed.data?.getText?.() || ''; const part = parsed.data?.parts?.[0]; const header = part?.header; + const simIdentity = await this.getCurrentSimIdentity(); logger.info('✓ PDU解析成功'); logger.info(`发送者: ${sender}`); logger.info(`时间戳: ${timestamp}`); + logger.info(`收件SIM: ${simIdentity?.simLabel || '未知SIM'}`); logger.info(`内容: ${text}`); // 检查是否为长短信 @@ -62,11 +71,12 @@ class SMSProcessor { partNumber, totalParts, text, - timestamp + timestamp, + { simIdentity } ); } else { // 普通短信,直接处理 - await this.processSmsContent(sender, text, timestamp); + await this.processSmsContent(sender, text, timestamp, { simIdentity }); } } catch (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(`发送者: ${sender}`); logger.info(`时间戳: ${timestamp}`); + logger.info(`收件SIM: ${simIdentity?.simLabel || '未知SIM'}`); logger.info(`内容: ${text}`); logger.info('===================='); - const messageRecord = this.addReceivedMessage(sender, text, timestamp); + const messageRecord = this.addReceivedMessage(sender, text, timestamp, simIdentity); // 推送到所有通道 await this.pushManager.pushToAll(sender, text, timestamp); @@ -102,13 +117,16 @@ class SMSProcessor { /** * 记录收到的短信,供Web管理端展示。 */ - addReceivedMessage(sender, text, timestamp) { + addReceivedMessage(sender, text, timestamp, simIdentity = null) { + const normalizedSimIdentity = this.normalizeSimIdentity(simIdentity); const message = { id: `${Date.now()}-${++this.receivedMessageSeq}`, sender, text, timestamp, receivedAt: new Date().toISOString(), + simId: normalizedSimIdentity?.simId || '', + simLabel: normalizedSimIdentity?.simLabel || '未知SIM', status: 'received', statusText: '已接收' }; @@ -119,7 +137,7 @@ class SMSProcessor { } this.persistReceivedMessages(); - logger.info(`短信已进入Web收件箱: ${sender}`); + logger.info(`短信已进入Web收件箱: ${sender} (${message.simLabel})`); return message; } @@ -138,8 +156,53 @@ class SMSProcessor { * 获取最近收到的短信。 */ getReceivedMessages(limit = 50) { - const safeLimit = Math.min(Math.max(Number(limit) || 50, 1), this.maxReceivedMessages); - return this.receivedMessages.slice(0, safeLimit); + return this.receivedMessages.slice(0, this.resolveReceivedMessagesLimit(limit)); + } + + /** + * 获取按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); } + /** + * 是否按当前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; } + const simIdentity = this.normalizeStoredSimIdentity(item); + 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()), + simId: simIdentity?.simId || '', + simLabel: simIdentity?.simLabel || '未知SIM', status: String(item.status || 'received'), 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;