Compare commits
16
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ff599b8ce5 | ||
|
|
2410aa224c | ||
|
|
6c468dc10e | ||
|
|
5ad372590b | ||
|
|
a48cad170c | ||
|
|
d688dffef2 | ||
|
|
384f1be75b | ||
|
|
fce17ab148 | ||
|
|
1534c48d9e | ||
|
|
679737c737 | ||
|
|
69c49ab29b | ||
|
|
11a52add4e | ||
|
|
f63fd6bc75 | ||
|
|
038668b47e | ||
|
|
cba3973632 | ||
|
|
83118db289 |
@@ -9,6 +9,8 @@
|
|||||||
- ✅ 长短信自动合并
|
- ✅ 长短信自动合并
|
||||||
- ✅ 多通道推送(邮件/钉钉/飞书/Telegram等)
|
- ✅ 多通道推送(邮件/钉钉/飞书/Telegram等)
|
||||||
- ✅ 短信发送功能
|
- ✅ 短信发送功能
|
||||||
|
- ✅ 号码黑名单
|
||||||
|
- ✅ 管理员远程命令
|
||||||
- ✅ RESTful API接口
|
- ✅ RESTful API接口
|
||||||
- ✅ Web管理界面
|
- ✅ Web管理界面
|
||||||
|
|
||||||
@@ -61,6 +63,8 @@ cp config.example.json config.json
|
|||||||
| `mobileData.cid` | 否 | 移动数据 PDP CID,默认 `1`;有效范围 `1-15`,不要使用 `8`。 |
|
| `mobileData.cid` | 否 | 移动数据 PDP CID,默认 `1`;有效范围 `1-15`,不要使用 `8`。 |
|
||||||
| `simCards` | 否 | SIM 卡显示手机号,`slot=0` 是 SIM1,`slot=1` 是 SIM2。 |
|
| `simCards` | 否 | SIM 卡显示手机号,`slot=0` 是 SIM1,`slot=1` 是 SIM2。 |
|
||||||
| `smtp` | 否 | 邮件通知配置;填写 `smtp.server` 后启用。 |
|
| `smtp` | 否 | 邮件通知配置;填写 `smtp.server` 后启用。 |
|
||||||
|
| `adminPhone` | 否 | 管理员手机号;只有该号码发来的短信会执行管理员命令。 |
|
||||||
|
| `numberBlackList` | 否 | 黑名单号码列表,匹配后忽略短信。 |
|
||||||
| `inbox` | 否 | Web 收件箱持久化和双卡显示策略。 |
|
| `inbox` | 否 | Web 收件箱持久化和双卡显示策略。 |
|
||||||
| `pushChannels` | 否 | 推送通道列表;通道开启后才要求目标字段。 |
|
| `pushChannels` | 否 | 推送通道列表;通道开启后才要求目标字段。 |
|
||||||
|
|
||||||
@@ -95,6 +99,8 @@ cp config.example.json config.json
|
|||||||
"pass": "your_auth_code",
|
"pass": "your_auth_code",
|
||||||
"sendTo": "recipient@example.com"
|
"sendTo": "recipient@example.com"
|
||||||
},
|
},
|
||||||
|
"adminPhone": "13800138000",
|
||||||
|
"numberBlackList": ["10086", "10010"],
|
||||||
"inbox": {
|
"inbox": {
|
||||||
"maxReceivedMessages": 200,
|
"maxReceivedMessages": 200,
|
||||||
"receivedMessagesFile": "data/received-messages.json",
|
"receivedMessagesFile": "data/received-messages.json",
|
||||||
@@ -272,6 +278,21 @@ GET /api/logs
|
|||||||
- `serverchan`:`key1` 填 Server酱 SendKey。
|
- `serverchan`:`key1` 填 Server酱 SendKey。
|
||||||
- `custom`:`customBody` 如果填写,必须是合法 JSON 字符串,支持 `{sender}`、`{message}`、`{timestamp}` 占位符。
|
- `custom`:`customBody` 如果填写,必须是合法 JSON 字符串,支持 `{sender}`、`{message}`、`{timestamp}` 占位符。
|
||||||
|
|
||||||
|
## 管理员命令
|
||||||
|
|
||||||
|
通过短信发送命令(仅管理员号码):
|
||||||
|
|
||||||
|
### 远程发送短信
|
||||||
|
```
|
||||||
|
SMS:目标号码:短信内容
|
||||||
|
```
|
||||||
|
示例:`SMS:13800138000:测试消息`
|
||||||
|
|
||||||
|
### 重启系统
|
||||||
|
```
|
||||||
|
RESET
|
||||||
|
```
|
||||||
|
|
||||||
## 长短信处理
|
## 长短信处理
|
||||||
|
|
||||||
- 自动检测长短信(分段数 > 1)
|
- 自动检测长短信(分段数 > 1)
|
||||||
|
|||||||
@@ -29,6 +29,13 @@
|
|||||||
"pass": "your_auth_code",
|
"pass": "your_auth_code",
|
||||||
"sendTo": "recipient@example.com"
|
"sendTo": "recipient@example.com"
|
||||||
},
|
},
|
||||||
|
"_adminPhone_comment": "可选。配置后,只有该号码发来的短信会被当作管理员命令处理,例如SMS:号码:内容、RESET。",
|
||||||
|
"adminPhone": "13800138000",
|
||||||
|
"_numberBlackList_comment": "可选。列表中的号码短信会被忽略;支持带+86或不带+86的匹配。",
|
||||||
|
"numberBlackList": [
|
||||||
|
"10086",
|
||||||
|
"10010"
|
||||||
|
],
|
||||||
"inbox": {
|
"inbox": {
|
||||||
"maxReceivedMessages": 200,
|
"maxReceivedMessages": 200,
|
||||||
"receivedMessagesFile": "data/received-messages.json",
|
"receivedMessagesFile": "data/received-messages.json",
|
||||||
|
|||||||
+3
-5
@@ -144,6 +144,7 @@
|
|||||||
<span class="sim-slot__state" id="simSlot0State">--</span>
|
<span class="sim-slot__state" id="simSlot0State">--</span>
|
||||||
</div>
|
</div>
|
||||||
<span class="sim-slot__phone" id="simSlot0Phone">--</span>
|
<span class="sim-slot__phone" id="simSlot0Phone">--</span>
|
||||||
|
<button class="button button--secondary button--compact" type="button" data-sim-switch="0">切到SIM1</button>
|
||||||
</article>
|
</article>
|
||||||
|
|
||||||
<article class="sim-slot" id="simSlot1">
|
<article class="sim-slot" id="simSlot1">
|
||||||
@@ -152,6 +153,7 @@
|
|||||||
<span class="sim-slot__state" id="simSlot1State">--</span>
|
<span class="sim-slot__state" id="simSlot1State">--</span>
|
||||||
</div>
|
</div>
|
||||||
<span class="sim-slot__phone" id="simSlot1Phone">--</span>
|
<span class="sim-slot__phone" id="simSlot1Phone">--</span>
|
||||||
|
<button class="button button--secondary button--compact" type="button" data-sim-switch="1">切到SIM2</button>
|
||||||
</article>
|
</article>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -225,10 +227,6 @@
|
|||||||
<span>当前发送SIM</span>
|
<span>当前发送SIM</span>
|
||||||
<strong id="sendSimLabel">--</strong>
|
<strong id="sendSimLabel">--</strong>
|
||||||
</p>
|
</p>
|
||||||
<div class="send-sim-picker" aria-label="选择发送SIM">
|
|
||||||
<button class="button button--secondary button--compact" type="button" data-sim-switch="0">SIM1</button>
|
|
||||||
<button class="button button--secondary button--compact" type="button" data-sim-switch="1">SIM2</button>
|
|
||||||
</div>
|
|
||||||
<form id="smsForm" class="stack-form">
|
<form id="smsForm" class="stack-form">
|
||||||
<label class="field">
|
<label class="field">
|
||||||
<span class="field__label">目标号码</span>
|
<span class="field__label">目标号码</span>
|
||||||
@@ -421,4 +419,4 @@
|
|||||||
<script src="/assets/admin.js" type="module"></script>
|
<script src="/assets/admin.js" type="module"></script>
|
||||||
</body>
|
</body>
|
||||||
|
|
||||||
</html>
|
</html>
|
||||||
+25
-31
@@ -70,10 +70,6 @@
|
|||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
}
|
}
|
||||||
|
|
||||||
[hidden] {
|
|
||||||
display: none !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
html {
|
html {
|
||||||
scroll-behavior: smooth;
|
scroll-behavior: smooth;
|
||||||
scroll-padding-top: 80px;
|
scroll-padding-top: 80px;
|
||||||
@@ -730,13 +726,9 @@ h3 {
|
|||||||
gap: var(--space-md);
|
gap: var(--space-md);
|
||||||
}
|
}
|
||||||
|
|
||||||
.sim-slot-grid.is-single {
|
|
||||||
grid-template-columns: minmax(0, 1fr);
|
|
||||||
}
|
|
||||||
|
|
||||||
.sim-slot {
|
.sim-slot {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: minmax(0, 1fr);
|
grid-template-columns: minmax(0, 1fr) auto;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 8px 12px;
|
gap: 8px 12px;
|
||||||
border: 1px solid var(--hairline);
|
border: 1px solid var(--hairline);
|
||||||
@@ -745,14 +737,9 @@ h3 {
|
|||||||
padding: 12px 14px;
|
padding: 12px 14px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.sim-slot.is-single {
|
|
||||||
grid-template-columns: minmax(0, 1fr);
|
|
||||||
min-height: 76px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.sim-slot.is-active {
|
.sim-slot.is-active {
|
||||||
border-color: var(--surface-cream-strong);
|
border-color: rgba(93, 184, 114, 0.55);
|
||||||
background: var(--surface-soft);
|
box-shadow: inset 3px 0 0 var(--success);
|
||||||
}
|
}
|
||||||
|
|
||||||
.sim-slot__topline {
|
.sim-slot__topline {
|
||||||
@@ -782,8 +769,8 @@ h3 {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.sim-slot.is-active .sim-slot__state {
|
.sim-slot.is-active .sim-slot__state {
|
||||||
background: var(--surface-cream-strong);
|
background: rgba(93, 184, 114, 0.16);
|
||||||
color: var(--body-strong);
|
color: #2f7c44;
|
||||||
}
|
}
|
||||||
|
|
||||||
.sim-slot__phone {
|
.sim-slot__phone {
|
||||||
@@ -944,6 +931,24 @@ h3 {
|
|||||||
color: #2f7c44;
|
color: #2f7c44;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.message-item.is-admin {
|
||||||
|
border-left: 3px solid var(--accent-amber);
|
||||||
|
}
|
||||||
|
|
||||||
|
.message-item.is-admin .message-status {
|
||||||
|
background: rgba(232, 165, 90, 0.2);
|
||||||
|
color: #8a5b16;
|
||||||
|
}
|
||||||
|
|
||||||
|
.message-item.is-ignored {
|
||||||
|
border-left: 3px solid var(--error);
|
||||||
|
}
|
||||||
|
|
||||||
|
.message-item.is-ignored .message-status {
|
||||||
|
background: rgba(198, 69, 69, 0.12);
|
||||||
|
color: var(--error);
|
||||||
|
}
|
||||||
|
|
||||||
.message-text {
|
.message-text {
|
||||||
margin-top: 10px;
|
margin-top: 10px;
|
||||||
color: var(--body);
|
color: var(--body);
|
||||||
@@ -979,7 +984,7 @@ h3 {
|
|||||||
display: inline-flex;
|
display: inline-flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 8px;
|
gap: 8px;
|
||||||
margin: -4px 0 8px;
|
margin: -4px 0 var(--space-md);
|
||||||
padding: 0;
|
padding: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -997,17 +1002,6 @@ h3 {
|
|||||||
overflow-wrap: anywhere;
|
overflow-wrap: anywhere;
|
||||||
}
|
}
|
||||||
|
|
||||||
.send-sim-picker {
|
|
||||||
display: flex;
|
|
||||||
flex-wrap: wrap;
|
|
||||||
gap: 8px;
|
|
||||||
margin: 0 0 var(--space-md);
|
|
||||||
}
|
|
||||||
|
|
||||||
.send-sim-picker .button {
|
|
||||||
min-width: 96px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.field {
|
.field {
|
||||||
display: grid;
|
display: grid;
|
||||||
gap: var(--space-xs);
|
gap: var(--space-xs);
|
||||||
@@ -1616,4 +1610,4 @@ body.modal-open {
|
|||||||
.field-row--wide {
|
.field-row--wide {
|
||||||
grid-column: auto;
|
grid-column: auto;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
+11
-80
@@ -50,7 +50,6 @@ const elements = {
|
|||||||
simStatus: document.querySelector('#simStatus'),
|
simStatus: document.querySelector('#simStatus'),
|
||||||
simSlot0: document.querySelector('#simSlot0'),
|
simSlot0: document.querySelector('#simSlot0'),
|
||||||
simSlot1: document.querySelector('#simSlot1'),
|
simSlot1: document.querySelector('#simSlot1'),
|
||||||
simSlotGrid: document.querySelector('.sim-slot-grid'),
|
|
||||||
simSlot0State: document.querySelector('#simSlot0State'),
|
simSlot0State: document.querySelector('#simSlot0State'),
|
||||||
simSlot1State: document.querySelector('#simSlot1State'),
|
simSlot1State: document.querySelector('#simSlot1State'),
|
||||||
simSlot0Phone: document.querySelector('#simSlot0Phone'),
|
simSlot0Phone: document.querySelector('#simSlot0Phone'),
|
||||||
@@ -254,11 +253,6 @@ function renderSendSimLabel() {
|
|||||||
setText(elements.sendSimLabel, getActiveSimLabel());
|
setText(elements.sendSimLabel, getActiveSimLabel());
|
||||||
}
|
}
|
||||||
|
|
||||||
function setSimSwitchStatus(message, type = '') {
|
|
||||||
setStatusMessage(elements.simStatus, message);
|
|
||||||
setStatusMessage(elements.smsStatus, message, type);
|
|
||||||
}
|
|
||||||
|
|
||||||
function getSimSlot(sim = {}, slotNumber) {
|
function getSimSlot(sim = {}, slotNumber) {
|
||||||
return (sim.slots || []).find(slot => slot.slot === slotNumber) || {
|
return (sim.slots || []).find(slot => slot.slot === slotNumber) || {
|
||||||
slot: slotNumber,
|
slot: slotNumber,
|
||||||
@@ -287,18 +281,7 @@ function renderSimSlot(slot, card, stateNode, phoneNode) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
card.classList.toggle('is-active', Boolean(slot.active));
|
card.classList.toggle('is-active', Boolean(slot.active));
|
||||||
if (slot.active && slot.present === false) {
|
setText(stateNode, slot.active ? '当前使用' : '未选中');
|
||||||
setText(stateNode, '当前未就绪');
|
|
||||||
} else if (slot.active) {
|
|
||||||
setText(stateNode, '当前使用');
|
|
||||||
} else if (slot.present === true) {
|
|
||||||
setText(stateNode, '可用');
|
|
||||||
} else if (slot.present === false) {
|
|
||||||
setText(stateNode, '未检测到SIM');
|
|
||||||
} else {
|
|
||||||
setText(stateNode, '未确认');
|
|
||||||
}
|
|
||||||
|
|
||||||
const phoneText = formatSimSlotPhone(slot);
|
const phoneText = formatSimSlotPhone(slot);
|
||||||
if (phoneNode) {
|
if (phoneNode) {
|
||||||
phoneNode.textContent = phoneText;
|
phoneNode.textContent = phoneText;
|
||||||
@@ -306,81 +289,29 @@ function renderSimSlot(slot, card, stateNode, phoneNode) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function getVisibleSimSlots(sim = {}, activeSlot = null) {
|
|
||||||
const slots = sim.slots || [];
|
|
||||||
const presentSlots = slots.filter(slot => slot.present === true);
|
|
||||||
|
|
||||||
if (presentSlots.length >= 2) {
|
|
||||||
return presentSlots;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (activeSlot?.present === true) {
|
|
||||||
return [activeSlot];
|
|
||||||
}
|
|
||||||
|
|
||||||
return slots.filter(slot => slot.active || slot.present !== false);
|
|
||||||
}
|
|
||||||
|
|
||||||
function getSimModeLabel(sim = {}, activeSlot = null) {
|
|
||||||
const hardwareSwitchable = Boolean(sim.supported && sim.canSwitch);
|
|
||||||
if (!hardwareSwitchable) {
|
|
||||||
return '单卡/未检测到双卡';
|
|
||||||
}
|
|
||||||
|
|
||||||
const presentSlots = (sim.slots || []).filter(slot => slot.present === true);
|
|
||||||
if (presentSlots.length >= 2) {
|
|
||||||
return sim.modeLabel || '双卡';
|
|
||||||
}
|
|
||||||
|
|
||||||
if (activeSlot?.present === true) {
|
|
||||||
return `仅检测到 ${getSimDisplayLabel(activeSlot)}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
return '未检测到可用SIM';
|
|
||||||
}
|
|
||||||
|
|
||||||
function renderSimStatus(sim = {}) {
|
function renderSimStatus(sim = {}) {
|
||||||
state.currentSim = sim;
|
state.currentSim = sim;
|
||||||
|
|
||||||
const supported = Boolean(sim.supported && sim.canSwitch);
|
const supported = Boolean(sim.supported && sim.canSwitch);
|
||||||
const activeSlot = getActiveSimSlot(sim);
|
const activeSlot = getActiveSimSlot(sim);
|
||||||
const modeLabel = getSimModeLabel(sim, activeSlot);
|
const modeLabel = supported ? (sim.modeLabel || '双卡') : '单卡/未检测到双卡';
|
||||||
|
|
||||||
setText(elements.simMode, modeLabel);
|
setText(elements.simMode, modeLabel);
|
||||||
|
|
||||||
const visibleSlots = getVisibleSimSlots(sim, activeSlot);
|
renderSimSlot(getSimSlot(sim, 0), elements.simSlot0, elements.simSlot0State, elements.simSlot0Phone);
|
||||||
const singleSlot = visibleSlots.length === 1;
|
renderSimSlot(getSimSlot(sim, 1), elements.simSlot1, elements.simSlot1State, elements.simSlot1Phone);
|
||||||
const visibleSlotNumbers = new Set(visibleSlots.map(slot => slot.slot));
|
|
||||||
|
|
||||||
elements.simSlotGrid?.classList.toggle('is-single', singleSlot);
|
|
||||||
|
|
||||||
[
|
|
||||||
[getSimSlot(sim, 0), elements.simSlot0, elements.simSlot0State, elements.simSlot0Phone],
|
|
||||||
[getSimSlot(sim, 1), elements.simSlot1, elements.simSlot1State, elements.simSlot1Phone]
|
|
||||||
].forEach(([slot, card, stateNode, phoneNode]) => {
|
|
||||||
if (card) {
|
|
||||||
card.hidden = !visibleSlotNumbers.has(slot.slot);
|
|
||||||
card.classList.toggle('is-single', singleSlot && visibleSlotNumbers.has(slot.slot));
|
|
||||||
}
|
|
||||||
renderSimSlot(slot, card, stateNode, phoneNode);
|
|
||||||
});
|
|
||||||
|
|
||||||
elements.simSwitchButtons.forEach((button) => {
|
elements.simSwitchButtons.forEach((button) => {
|
||||||
const slot = Number.parseInt(button.dataset.simSwitch, 10);
|
const slot = Number.parseInt(button.dataset.simSwitch, 10);
|
||||||
const slotInfo = getSimSlot(sim, slot);
|
|
||||||
const isActive = activeSlot?.slot === slot;
|
const isActive = activeSlot?.slot === slot;
|
||||||
const unavailable = slotInfo.present === false;
|
button.disabled = state.simBusy || Boolean(sim.switching) || !supported || isActive;
|
||||||
button.hidden = singleSlot;
|
button.textContent = isActive ? '正在使用' : `切到SIM${slot + 1}`;
|
||||||
button.disabled = state.simBusy || Boolean(sim.switching) || !supported || isActive || unavailable;
|
|
||||||
button.textContent = isActive ? `SIM${slot + 1} 当前` : `切到SIM${slot + 1}`;
|
|
||||||
});
|
});
|
||||||
|
|
||||||
if (state.simBusy || sim.switching) {
|
if (state.simBusy || sim.switching) {
|
||||||
setSimSwitchStatus('正在切换SIM...');
|
setStatusMessage(elements.simStatus, '正在切换SIM...');
|
||||||
} else if (supported && activeSlot?.present === true) {
|
} else if (supported && activeSlot) {
|
||||||
setStatusMessage(elements.simStatus, `当前 ${getSimDisplayLabel(activeSlot)}`);
|
setStatusMessage(elements.simStatus, `当前 ${getSimDisplayLabel(activeSlot)}`, 'success');
|
||||||
} else if (supported && activeSlot?.present === false) {
|
|
||||||
setStatusMessage(elements.simStatus, `${getSimDisplayLabel(activeSlot)} 未就绪,请检查是否插卡`);
|
|
||||||
} else if (!supported) {
|
} else if (!supported) {
|
||||||
setStatusMessage(elements.simStatus, '双卡不可用');
|
setStatusMessage(elements.simStatus, '双卡不可用');
|
||||||
} else {
|
} else {
|
||||||
@@ -872,10 +803,10 @@ async function switchSim(slot) {
|
|||||||
body: JSON.stringify({ slot: slotNumber })
|
body: JSON.stringify({ slot: slotNumber })
|
||||||
});
|
});
|
||||||
renderSimStatus(result.data || {});
|
renderSimStatus(result.data || {});
|
||||||
setSimSwitchStatus(result.message || `已切换到SIM${slotNumber + 1}`, 'success');
|
setStatusMessage(elements.simStatus, result.message || `已切换到SIM${slotNumber + 1}`, 'success');
|
||||||
await refreshAll();
|
await refreshAll();
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
setSimSwitchStatus(err.message, 'error');
|
setStatusMessage(elements.simStatus, err.message, 'error');
|
||||||
await refreshSim().catch(() => {});
|
await refreshSim().catch(() => {});
|
||||||
} finally {
|
} finally {
|
||||||
state.simBusy = false;
|
state.simBusy = false;
|
||||||
|
|||||||
+17
-229
@@ -33,7 +33,6 @@ class ModemManager extends EventEmitter {
|
|||||||
switchSlot: null,
|
switchSlot: null,
|
||||||
bindSlot: null,
|
bindSlot: null,
|
||||||
switching: false,
|
switching: false,
|
||||||
probing: false,
|
|
||||||
slots: this.createSimSlots(),
|
slots: this.createSimSlots(),
|
||||||
lastCheckedAt: null,
|
lastCheckedAt: null,
|
||||||
error: '',
|
error: '',
|
||||||
@@ -64,7 +63,6 @@ class ModemManager extends EventEmitter {
|
|||||||
slot,
|
slot,
|
||||||
name: `SIM${slot + 1}`,
|
name: `SIM${slot + 1}`,
|
||||||
active: false,
|
active: false,
|
||||||
present: null,
|
|
||||||
phoneNumber,
|
phoneNumber,
|
||||||
phoneLabel: phoneNumber || `SIM${slot + 1}`,
|
phoneLabel: phoneNumber || `SIM${slot + 1}`,
|
||||||
lastCheckedAt: null
|
lastCheckedAt: null
|
||||||
@@ -540,25 +538,6 @@ class ModemManager extends EventEmitter {
|
|||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
updateSimSlotPresence(status, slotNumber, present) {
|
|
||||||
const slot = this.normalizeSimSlot(slotNumber);
|
|
||||||
if (slot === null) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
status.slots = status.slots.map(item => {
|
|
||||||
if (item.slot !== slot) {
|
|
||||||
return item;
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
...item,
|
|
||||||
present: Boolean(present),
|
|
||||||
lastCheckedAt: new Date().toISOString()
|
|
||||||
};
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
getBusinessSimSlot(status = this.sim) {
|
getBusinessSimSlot(status = this.sim) {
|
||||||
return this.normalizeSimSlot(status.bindSlot) ?? this.normalizeSimSlot(status.switchSlot);
|
return this.normalizeSimSlot(status.bindSlot) ?? this.normalizeSimSlot(status.switchSlot);
|
||||||
}
|
}
|
||||||
@@ -658,27 +637,14 @@ class ModemManager extends EventEmitter {
|
|||||||
retryDelay = 2000,
|
retryDelay = 2000,
|
||||||
settleDelay = 3000,
|
settleDelay = 3000,
|
||||||
confirmTimeout = 15000,
|
confirmTimeout = 15000,
|
||||||
confirmInterval = 1000,
|
confirmInterval = 1000
|
||||||
simReadyAttempts = 10,
|
|
||||||
simReadyDelay = 1000
|
|
||||||
} = options;
|
} = options;
|
||||||
|
|
||||||
const currentSlot = await this.querySwitchSimSlot().catch(() => this.sim.switchSlot);
|
const currentSlot = await this.querySwitchSimSlot().catch(() => this.sim.switchSlot);
|
||||||
this.sim.switchSlot = currentSlot;
|
this.sim.switchSlot = currentSlot;
|
||||||
|
|
||||||
if (currentSlot === targetSlot) {
|
if (currentSlot === targetSlot) {
|
||||||
try {
|
await this.waitSIMReady();
|
||||||
await this.waitSIMReady({
|
|
||||||
attempts: simReadyAttempts,
|
|
||||||
delay: simReadyDelay,
|
|
||||||
label: `SIM${targetSlot + 1}`
|
|
||||||
});
|
|
||||||
this.updateSimSlotPresence(this.sim, targetSlot, true);
|
|
||||||
} catch (err) {
|
|
||||||
this.updateSimSlotPresence(this.sim, targetSlot, false);
|
|
||||||
throw err;
|
|
||||||
}
|
|
||||||
|
|
||||||
return targetSlot;
|
return targetSlot;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -708,155 +674,11 @@ class ModemManager extends EventEmitter {
|
|||||||
this.sim.switchSlot = targetSlot;
|
this.sim.switchSlot = targetSlot;
|
||||||
this.markActiveSimSlot(this.sim, this.getBusinessSimSlot(this.sim) ?? targetSlot);
|
this.markActiveSimSlot(this.sim, this.getBusinessSimSlot(this.sim) ?? targetSlot);
|
||||||
await this.sleep(settleDelay);
|
await this.sleep(settleDelay);
|
||||||
|
await this.waitSIMReady();
|
||||||
try {
|
|
||||||
await this.waitSIMReady({
|
|
||||||
attempts: simReadyAttempts,
|
|
||||||
delay: simReadyDelay,
|
|
||||||
label: `SIM${targetSlot + 1}`
|
|
||||||
});
|
|
||||||
this.updateSimSlotPresence(this.sim, targetSlot, true);
|
|
||||||
} catch (err) {
|
|
||||||
this.updateSimSlotPresence(this.sim, targetSlot, false);
|
|
||||||
await this.restoreSwitchSimSlot(currentSlot, `${context}失败后回退`).catch((restoreErr) => {
|
|
||||||
logger.warn(`${context}失败后回退SIM失败: ${restoreErr.message}`);
|
|
||||||
});
|
|
||||||
throw err;
|
|
||||||
}
|
|
||||||
|
|
||||||
return targetSlot;
|
return targetSlot;
|
||||||
}
|
}
|
||||||
|
|
||||||
async restoreSwitchSimSlot(slotNumber, context = 'SIM回退', timeout = 20000) {
|
|
||||||
const slot = this.normalizeSimSlot(slotNumber);
|
|
||||||
if (slot === null) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
logger.warn(`${context}: 恢复到SIM${slot + 1}`);
|
|
||||||
await this.sendATCommand(`AT+SWITCHSIM=${slot}`, 10000).catch((err) => {
|
|
||||||
logger.warn(`${context}命令失败: ${err.message}`);
|
|
||||||
});
|
|
||||||
|
|
||||||
const restored = await this.waitForSwitchSimSlot(slot, timeout, 1000);
|
|
||||||
if (restored) {
|
|
||||||
this.sim.switchSlot = slot;
|
|
||||||
this.markActiveSimSlot(this.sim, this.getBusinessSimSlot(this.sim) ?? slot);
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
logger.warn(`${context}: 未能确认恢复到SIM${slot + 1}`);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
async restoreBindSimSlot(slotNumber, context = '业务SIM回退') {
|
|
||||||
const slot = this.normalizeSimSlot(slotNumber);
|
|
||||||
if (slot === null) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
await this.sendATWithRetry(`AT+BINDSIM=${slot}`, {
|
|
||||||
timeout: 5000,
|
|
||||||
retries: 1,
|
|
||||||
retryDelay: 500,
|
|
||||||
label: `${context}到SIM${slot + 1}`
|
|
||||||
}).catch((err) => {
|
|
||||||
logger.warn(`${context}到SIM${slot + 1}失败: ${err.message}`);
|
|
||||||
});
|
|
||||||
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
async restoreSimAfterProbe(switchSlot, bindSlot, context = 'SIM探测后恢复') {
|
|
||||||
const targetSwitchSlot = this.normalizeSimSlot(switchSlot);
|
|
||||||
const targetBindSlot = this.normalizeSimSlot(bindSlot) ?? targetSwitchSlot;
|
|
||||||
|
|
||||||
if (targetSwitchSlot !== null) {
|
|
||||||
await this.restoreSwitchSimSlot(targetSwitchSlot, context).catch((err) => {
|
|
||||||
logger.warn(`${context}切回SIM${targetSwitchSlot + 1}失败: ${err.message}`);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
if (targetBindSlot !== null) {
|
|
||||||
await this.restoreBindSimSlot(targetBindSlot, context);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async probeInactiveSimSlot(status, slotNumber, originalSlot, originalBindSlot = originalSlot) {
|
|
||||||
const targetSlot = this.normalizeSimSlot(slotNumber);
|
|
||||||
const restoreSlot = this.normalizeSimSlot(originalSlot);
|
|
||||||
const restoreBindSlot = this.normalizeSimSlot(originalBindSlot) ?? restoreSlot;
|
|
||||||
if (targetSlot === null || restoreSlot === null || targetSlot === restoreSlot) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
logger.info(`探测SIM${targetSlot + 1}是否可用`);
|
|
||||||
let switched = false;
|
|
||||||
|
|
||||||
try {
|
|
||||||
const resp = await this.sendATCommand(`AT+SWITCHSIM=${targetSlot}`, 10000);
|
|
||||||
if (!resp.includes('OK')) {
|
|
||||||
logger.warn(`探测SIM${targetSlot + 1}切换返回非OK: ${this.formatATResponse(resp)}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
switched = await this.waitForSwitchSimSlot(targetSlot, 15000, 1000);
|
|
||||||
if (!switched) {
|
|
||||||
this.updateSimSlotPresence(status, targetSlot, false);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
await this.sleep(1000);
|
|
||||||
await this.waitSIMReady({
|
|
||||||
attempts: 5,
|
|
||||||
delay: 1000,
|
|
||||||
label: `SIM${targetSlot + 1}`
|
|
||||||
});
|
|
||||||
this.updateSimSlotPresence(status, targetSlot, true);
|
|
||||||
return true;
|
|
||||||
} catch (err) {
|
|
||||||
logger.warn(`探测SIM${targetSlot + 1}失败: ${err.message}`);
|
|
||||||
this.updateSimSlotPresence(status, targetSlot, false);
|
|
||||||
return false;
|
|
||||||
} finally {
|
|
||||||
if (switched) {
|
|
||||||
await this.restoreSimAfterProbe(restoreSlot, restoreBindSlot, `探测SIM${targetSlot + 1}后恢复`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async probeSimSlotPresence(status, currentSlot) {
|
|
||||||
const originalSlot = this.normalizeSimSlot(currentSlot);
|
|
||||||
if (!status.supported || !status.canSwitch || originalSlot === null) {
|
|
||||||
return status;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (this.sim.switching || this.sim.probing) {
|
|
||||||
return status;
|
|
||||||
}
|
|
||||||
|
|
||||||
const originalBindSlot = this.normalizeSimSlot(status.bindSlot) ?? originalSlot;
|
|
||||||
|
|
||||||
this.sim.probing = true;
|
|
||||||
try {
|
|
||||||
for (const slot of status.slots) {
|
|
||||||
if (slot.slot !== originalSlot) {
|
|
||||||
await this.probeInactiveSimSlot(status, slot.slot, originalSlot, originalBindSlot);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
await this.restoreSimAfterProbe(originalSlot, originalBindSlot, 'SIM探测结束恢复');
|
|
||||||
|
|
||||||
const restoredSlot = await this.querySwitchSimSlot().catch(() => originalSlot);
|
|
||||||
status.switchSlot = this.normalizeSimSlot(restoredSlot) ?? originalSlot;
|
|
||||||
status.bindSlot = await this.queryBindSimSlot();
|
|
||||||
this.markActiveSimSlot(status, this.getBusinessSimSlot(status));
|
|
||||||
return status;
|
|
||||||
} finally {
|
|
||||||
this.sim.probing = false;
|
|
||||||
status.probing = false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async waitForSwitchSimSlot(targetSlot, timeout = 15000, interval = 1000) {
|
async waitForSwitchSimSlot(targetSlot, timeout = 15000, interval = 1000) {
|
||||||
const startedAt = Date.now();
|
const startedAt = Date.now();
|
||||||
|
|
||||||
@@ -984,27 +806,12 @@ class ModemManager extends EventEmitter {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
isSimReadyResponse(resp) {
|
|
||||||
return String(resp || '').includes('+CPIN: READY');
|
|
||||||
}
|
|
||||||
|
|
||||||
async isCurrentSimReady(timeout = 2000) {
|
|
||||||
try {
|
|
||||||
const resp = await this.sendATCommand('AT+CPIN?', timeout);
|
|
||||||
return this.isSimReadyResponse(resp);
|
|
||||||
} catch (err) {
|
|
||||||
logger.debug(`查询当前SIM就绪状态失败: ${err.message}`);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async getSimStatus(options = {}) {
|
async getSimStatus(options = {}) {
|
||||||
return await this.refreshSimStatus(options);
|
return await this.refreshSimStatus(options);
|
||||||
}
|
}
|
||||||
|
|
||||||
async refreshSimStatus(options = {}) {
|
async refreshSimStatus(options = {}) {
|
||||||
const refreshIdentity = options.refreshIdentity === true;
|
const refreshIdentity = options.refreshIdentity === true;
|
||||||
const probeSlots = options.probeSlots === true;
|
|
||||||
const status = {
|
const status = {
|
||||||
...this.sim,
|
...this.sim,
|
||||||
slots: this.sim.slots.map(slot => ({ ...slot })),
|
slots: this.sim.slots.map(slot => ({ ...slot })),
|
||||||
@@ -1032,8 +839,6 @@ class ModemManager extends EventEmitter {
|
|||||||
if (switchSlot !== null) {
|
if (switchSlot !== null) {
|
||||||
status.canSwitch = true;
|
status.canSwitch = true;
|
||||||
status.switchSlot = switchSlot;
|
status.switchSlot = switchSlot;
|
||||||
const currentSimReady = await this.isCurrentSimReady();
|
|
||||||
this.updateSimSlotPresence(status, switchSlot, currentSimReady);
|
|
||||||
} else {
|
} else {
|
||||||
status.switchSlot = null;
|
status.switchSlot = null;
|
||||||
status.error = '未能读取当前AT SIM卡槽';
|
status.error = '未能读取当前AT SIM卡槽';
|
||||||
@@ -1047,10 +852,6 @@ class ModemManager extends EventEmitter {
|
|||||||
this.updateSimSlotPhoneNumber(status, switchSlot, phoneNumber);
|
this.updateSimSlotPhoneNumber(status, switchSlot, phoneNumber);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (probeSlots && status.canSwitch && switchSlot !== null) {
|
|
||||||
await this.probeSimSlotPresence(status, switchSlot);
|
|
||||||
}
|
|
||||||
|
|
||||||
status.lastCheckedAt = new Date().toISOString();
|
status.lastCheckedAt = new Date().toISOString();
|
||||||
this.sim = status;
|
this.sim = status;
|
||||||
return this.getCachedSimStatus();
|
return this.getCachedSimStatus();
|
||||||
@@ -1095,20 +896,12 @@ class ModemManager extends EventEmitter {
|
|||||||
await this.switchSimForIdentityRead(targetSlot, {
|
await this.switchSimForIdentityRead(targetSlot, {
|
||||||
context: `切换SIM${targetSlot + 1}`,
|
context: `切换SIM${targetSlot + 1}`,
|
||||||
purpose: '用于业务绑定',
|
purpose: '用于业务绑定',
|
||||||
retries: 2,
|
retries: 4,
|
||||||
retryDelay: 1000,
|
retryDelay: 3000,
|
||||||
settleDelay: 1000,
|
settleDelay: 3000
|
||||||
confirmTimeout: 5000,
|
|
||||||
confirmInterval: 1000,
|
|
||||||
simReadyAttempts: 3,
|
|
||||||
simReadyDelay: 1000
|
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
await this.waitSIMReady({
|
await this.waitSIMReady();
|
||||||
attempts: 3,
|
|
||||||
delay: 1000,
|
|
||||||
label: `SIM${targetSlot + 1}`
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.normalizeSimSlot(before.bindSlot) !== targetSlot) {
|
if (this.normalizeSimSlot(before.bindSlot) !== targetSlot) {
|
||||||
@@ -1960,7 +1753,7 @@ class ModemManager extends EventEmitter {
|
|||||||
|
|
||||||
// 7. 按文档配置短信功能,并启用本项目需要的PDU模式
|
// 7. 按文档配置短信功能,并启用本项目需要的PDU模式
|
||||||
await this.configureSMS();
|
await this.configureSMS();
|
||||||
await this.refreshSimStatus({ refreshIdentity: true, probeSlots: true });
|
await this.refreshSimStatus({ refreshIdentity: true });
|
||||||
|
|
||||||
logger.info('模组初始化完成');
|
logger.info('模组初始化完成');
|
||||||
this.emit('ready');
|
this.emit('ready');
|
||||||
@@ -1969,29 +1762,24 @@ class ModemManager extends EventEmitter {
|
|||||||
/**
|
/**
|
||||||
* 等待SIM卡完成初始化
|
* 等待SIM卡完成初始化
|
||||||
*/
|
*/
|
||||||
async waitSIMReady(options = {}) {
|
async waitSIMReady() {
|
||||||
const attempts = options.attempts ?? 10;
|
for (let attempt = 1; attempt <= 10; attempt++) {
|
||||||
const delay = options.delay ?? 1000;
|
|
||||||
const timeout = options.timeout ?? 2000;
|
|
||||||
const label = options.label || 'SIM卡';
|
|
||||||
|
|
||||||
for (let attempt = 1; attempt <= attempts; attempt++) {
|
|
||||||
try {
|
try {
|
||||||
const resp = await this.sendATCommand('AT+CPIN?', timeout);
|
const resp = await this.sendATCommand('AT+CPIN?', 2000);
|
||||||
if (this.isSimReadyResponse(resp)) {
|
if (resp.includes('+CPIN: READY')) {
|
||||||
logger.info(`✓ ${label}已就绪`);
|
logger.info('✓ SIM卡已就绪');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
logger.warn(`${label}未就绪(${attempt}/${attempts}): ${this.formatATResponse(resp)}`);
|
logger.warn(`SIM卡未就绪(${attempt}/10): ${this.formatATResponse(resp)}`);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
logger.warn(`查询${label}状态失败(${attempt}/${attempts}): ${err.message}`);
|
logger.warn(`查询SIM卡状态失败(${attempt}/10): ${err.message}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
await this.sleep(delay);
|
await this.sleep(1000);
|
||||||
}
|
}
|
||||||
|
|
||||||
throw new Error(`${label}未就绪,可能未插卡或SIM初始化失败`);
|
throw new Error('SIM卡未就绪');
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
+122
-1
@@ -200,7 +200,7 @@ class SMSProcessor {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 处理短信内容并转发
|
* 处理短信内容(管理员命令检查和转发)
|
||||||
*/
|
*/
|
||||||
async processSmsContent(sender, text, timestamp, options = {}) {
|
async processSmsContent(sender, text, timestamp, options = {}) {
|
||||||
const simIdentity = options.simIdentity !== undefined
|
const simIdentity = options.simIdentity !== undefined
|
||||||
@@ -217,6 +217,31 @@ class SMSProcessor {
|
|||||||
|
|
||||||
const messageRecord = this.addReceivedMessage(sender, text, timestamp, simIdentity);
|
const messageRecord = this.addReceivedMessage(sender, text, timestamp, simIdentity);
|
||||||
|
|
||||||
|
// 检查是否在黑名单中
|
||||||
|
if (this.isInBlacklist(sender)) {
|
||||||
|
this.updateReceivedMessage(messageRecord.id, {
|
||||||
|
status: 'ignored',
|
||||||
|
statusText: '黑名单忽略'
|
||||||
|
});
|
||||||
|
logger.info('发送者在黑名单中,忽略该短信');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检查是否为管理员命令
|
||||||
|
if (this.isAdmin(sender)) {
|
||||||
|
logger.info('收到管理员短信,检查命令...');
|
||||||
|
const trimmedText = text.trim();
|
||||||
|
|
||||||
|
if (trimmedText.startsWith('SMS:') || trimmedText === 'RESET') {
|
||||||
|
this.updateReceivedMessage(messageRecord.id, {
|
||||||
|
status: 'admin',
|
||||||
|
statusText: '管理员命令'
|
||||||
|
});
|
||||||
|
await this.processAdminCommand(sender, trimmedText);
|
||||||
|
return; // 命令已处理,不再发送普通通知
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// 推送到所有通道
|
// 推送到所有通道
|
||||||
await this.pushManager.pushToAll(sender, text, timestamp);
|
await this.pushManager.pushToAll(sender, text, timestamp);
|
||||||
|
|
||||||
@@ -580,6 +605,102 @@ class SMSProcessor {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 检查是否在黑名单中
|
||||||
|
*/
|
||||||
|
isInBlacklist(sender) {
|
||||||
|
const blacklist = this.config.numberBlackList || [];
|
||||||
|
|
||||||
|
// 检查完整号码
|
||||||
|
if (blacklist.includes(sender)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检查去除+86后的号码
|
||||||
|
if (sender.startsWith('+86')) {
|
||||||
|
const stripped = sender.substring(3);
|
||||||
|
if (blacklist.includes(stripped)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 检查是否为管理员
|
||||||
|
*/
|
||||||
|
isAdmin(sender) {
|
||||||
|
if (!this.config.adminPhone) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
let senderStr = sender;
|
||||||
|
let adminStr = this.config.adminPhone;
|
||||||
|
|
||||||
|
// 去除+86前缀
|
||||||
|
if (senderStr.startsWith('+86')) {
|
||||||
|
senderStr = senderStr.substring(3);
|
||||||
|
}
|
||||||
|
if (adminStr.startsWith('+86')) {
|
||||||
|
adminStr = adminStr.substring(3);
|
||||||
|
}
|
||||||
|
|
||||||
|
return senderStr === adminStr;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 处理管理员命令
|
||||||
|
*/
|
||||||
|
async processAdminCommand(sender, command) {
|
||||||
|
logger.info(`处理管理员命令: ${command}`);
|
||||||
|
|
||||||
|
// 处理 SMS:号码:内容 命令
|
||||||
|
if (command.startsWith('SMS:')) {
|
||||||
|
const parts = command.split(':');
|
||||||
|
if (parts.length >= 3) {
|
||||||
|
const targetPhone = parts[1].trim();
|
||||||
|
const smsContent = parts.slice(2).join(':').trim(); // 支持内容中包含冒号
|
||||||
|
|
||||||
|
logger.info(`目标号码: ${targetPhone}`);
|
||||||
|
logger.info(`短信内容: ${smsContent}`);
|
||||||
|
|
||||||
|
const sendResult = await this.modem.sendSMS(targetPhone, smsContent);
|
||||||
|
const success = Boolean(sendResult?.success);
|
||||||
|
|
||||||
|
// 发送邮件通知结果
|
||||||
|
const subject = success ? '短信发送成功' : '短信发送失败';
|
||||||
|
const body = `管理员命令执行结果:\n命令: ${command}\n发送SIM: ${sendResult?.simLabel || '未知SIM'}\n目标号码: ${targetPhone}\n短信内容: ${smsContent}\n执行结果: ${success ? '成功' : '失败'}`;
|
||||||
|
await this.pushManager.sendEmail(subject, body);
|
||||||
|
} else {
|
||||||
|
logger.error('SMS命令格式错误');
|
||||||
|
await this.pushManager.sendEmail(
|
||||||
|
'命令执行失败',
|
||||||
|
'SMS命令格式错误,正确格式: SMS:号码:内容'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// 处理 RESET 命令
|
||||||
|
else if (command === 'RESET') {
|
||||||
|
logger.info('执行RESET命令');
|
||||||
|
|
||||||
|
// 先发送邮件通知
|
||||||
|
await this.pushManager.sendEmail(
|
||||||
|
'重启命令已执行',
|
||||||
|
'收到RESET命令,即将重启系统...'
|
||||||
|
);
|
||||||
|
|
||||||
|
// 等待邮件发送完成
|
||||||
|
await new Promise(resolve => setTimeout(resolve, 2000));
|
||||||
|
|
||||||
|
// 重启进程
|
||||||
|
logger.info('正在重启系统...');
|
||||||
|
process.exit(0); // PM2会自动重启
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
logger.warn(`未知命令: ${command}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export default SMSProcessor;
|
export default SMSProcessor;
|
||||||
|
|||||||
Reference in New Issue
Block a user