feat: 添加推送通道管理
This commit is contained in:
+90
-1
@@ -20,7 +20,10 @@
|
|||||||
<a href="#console">AT 控制台</a>
|
<a href="#console">AT 控制台</a>
|
||||||
<a href="#logs">日志</a>
|
<a href="#logs">日志</a>
|
||||||
</nav>
|
</nav>
|
||||||
<button class="button button--secondary" id="refreshButton" type="button">刷新状态</button>
|
<div class="top-nav__actions">
|
||||||
|
<button class="button button--secondary" id="pushSettingsButton" type="button">推送渠道</button>
|
||||||
|
<button class="button button--secondary" id="refreshButton" type="button">刷新状态</button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
@@ -165,6 +168,92 @@
|
|||||||
</section>
|
</section>
|
||||||
</main>
|
</main>
|
||||||
|
|
||||||
|
<div class="modal" id="pushModal" aria-hidden="true">
|
||||||
|
<div class="modal__backdrop" data-close-push-modal></div>
|
||||||
|
<section class="modal__dialog" role="dialog" aria-modal="true" aria-labelledby="pushModalTitle">
|
||||||
|
<div class="modal__header">
|
||||||
|
<div>
|
||||||
|
<span class="badge badge--soft">push</span>
|
||||||
|
<h2 id="pushModalTitle">推送渠道设置</h2>
|
||||||
|
</div>
|
||||||
|
<button class="icon-button" id="pushModalClose" type="button" aria-label="关闭推送渠道设置">×</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="push-settings">
|
||||||
|
<aside class="channel-list-panel" aria-label="推送渠道列表">
|
||||||
|
<div class="panel-header panel-header--light">
|
||||||
|
<span>channels</span>
|
||||||
|
<button class="button button--secondary button--compact" id="pushAddButton" type="button">新增</button>
|
||||||
|
</div>
|
||||||
|
<div class="channel-list" id="pushChannelList"></div>
|
||||||
|
</aside>
|
||||||
|
|
||||||
|
<form class="channel-form" id="pushChannelForm">
|
||||||
|
<fieldset class="form-fieldset" id="pushFormFields" disabled>
|
||||||
|
<div class="channel-form__grid">
|
||||||
|
<label class="switch-row">
|
||||||
|
<input type="checkbox" name="enabled">
|
||||||
|
<span>启用通道</span>
|
||||||
|
</label>
|
||||||
|
|
||||||
|
<label>
|
||||||
|
<span>通道名称</span>
|
||||||
|
<input class="text-input" type="text" name="name" maxlength="80" placeholder="钉钉通知">
|
||||||
|
</label>
|
||||||
|
|
||||||
|
<label>
|
||||||
|
<span>推送类型</span>
|
||||||
|
<select class="text-input select-input" name="type">
|
||||||
|
<option value="dingtalk">钉钉机器人</option>
|
||||||
|
<option value="feishu">飞书机器人</option>
|
||||||
|
<option value="telegram">Telegram Bot</option>
|
||||||
|
<option value="pushplus">PushPlus</option>
|
||||||
|
<option value="serverchan">Server酱</option>
|
||||||
|
<option value="post_json">POST JSON</option>
|
||||||
|
<option value="bark">Bark</option>
|
||||||
|
<option value="get">GET</option>
|
||||||
|
<option value="custom">自定义 POST</option>
|
||||||
|
</select>
|
||||||
|
</label>
|
||||||
|
|
||||||
|
<label class="field-row field-row--wide" data-push-field="url">
|
||||||
|
<span id="pushUrlLabel">Webhook URL</span>
|
||||||
|
<input class="text-input" type="text" name="url" autocomplete="off">
|
||||||
|
</label>
|
||||||
|
|
||||||
|
<label class="field-row field-row--wide" data-push-field="secret">
|
||||||
|
<span id="pushSecretLabel">加签密钥</span>
|
||||||
|
<input class="text-input" type="password" name="secret" autocomplete="new-password">
|
||||||
|
</label>
|
||||||
|
|
||||||
|
<label class="field-row" data-push-field="key1">
|
||||||
|
<span id="pushKey1Label">Key1</span>
|
||||||
|
<input class="text-input" type="text" name="key1" autocomplete="off">
|
||||||
|
</label>
|
||||||
|
|
||||||
|
<label class="field-row" data-push-field="key2">
|
||||||
|
<span id="pushKey2Label">Key2</span>
|
||||||
|
<input class="text-input" type="text" name="key2" autocomplete="off">
|
||||||
|
</label>
|
||||||
|
|
||||||
|
<label class="field-row field-row--wide" data-push-field="customBody">
|
||||||
|
<span>自定义请求体 JSON</span>
|
||||||
|
<textarea class="text-input text-area text-area--code" name="customBody" rows="7" spellcheck="false"></textarea>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
</fieldset>
|
||||||
|
|
||||||
|
<div class="form-actions channel-form__actions">
|
||||||
|
<button class="button button--primary" id="pushSaveButton" type="submit">保存设置</button>
|
||||||
|
<button class="button button--secondary" id="pushTestButton" type="button">发送测试</button>
|
||||||
|
<button class="button button--danger" id="pushDeleteButton" type="button">删除通道</button>
|
||||||
|
<p class="form-status" id="pushStatus" role="status"></p>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
</div>
|
||||||
|
|
||||||
<footer class="footer">
|
<footer class="footer">
|
||||||
<div class="container footer__inner">
|
<div class="container footer__inner">
|
||||||
<span class="brand brand--footer"><span class="brand__mark" aria-hidden="true">✣</span> SMS Gateway</span>
|
<span class="brand brand--footer"><span class="brand__mark" aria-hidden="true">✣</span> SMS Gateway</span>
|
||||||
|
|||||||
+248
-5
@@ -45,7 +45,8 @@ body {
|
|||||||
|
|
||||||
button,
|
button,
|
||||||
input,
|
input,
|
||||||
textarea {
|
textarea,
|
||||||
|
select {
|
||||||
font: inherit;
|
font: inherit;
|
||||||
letter-spacing: 0;
|
letter-spacing: 0;
|
||||||
}
|
}
|
||||||
@@ -106,13 +107,19 @@ a {
|
|||||||
|
|
||||||
.nav-links a:focus-visible,
|
.nav-links a:focus-visible,
|
||||||
.button:focus-visible,
|
.button:focus-visible,
|
||||||
.text-input:focus-visible {
|
.icon-button:focus-visible,
|
||||||
|
.channel-list__item:focus-visible,
|
||||||
|
.text-input:focus-visible,
|
||||||
|
.select-input:focus-visible {
|
||||||
outline: 3px solid rgba(204, 120, 92, 0.18);
|
outline: 3px solid rgba(204, 120, 92, 0.18);
|
||||||
outline-offset: 2px;
|
outline-offset: 2px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.top-nav .button {
|
.top-nav__actions {
|
||||||
margin-left: auto;
|
margin-left: auto;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 10px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.button {
|
.button {
|
||||||
@@ -154,6 +161,33 @@ a {
|
|||||||
color: var(--on-dark);
|
color: var(--on-dark);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.button--danger {
|
||||||
|
border: 1px solid rgba(198, 69, 69, 0.28);
|
||||||
|
background: rgba(198, 69, 69, 0.1);
|
||||||
|
color: var(--error);
|
||||||
|
}
|
||||||
|
|
||||||
|
.button--compact {
|
||||||
|
min-height: 32px;
|
||||||
|
padding: 8px 12px;
|
||||||
|
font-size: 13px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.icon-button {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
width: 36px;
|
||||||
|
height: 36px;
|
||||||
|
border: 1px solid var(--hairline);
|
||||||
|
border-radius: 9999px;
|
||||||
|
background: var(--canvas);
|
||||||
|
color: var(--ink);
|
||||||
|
font-size: 22px;
|
||||||
|
line-height: 1;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
.hero-band {
|
.hero-band {
|
||||||
padding: 72px 0 96px;
|
padding: 72px 0 96px;
|
||||||
background: var(--canvas);
|
background: var(--canvas);
|
||||||
@@ -480,11 +514,22 @@ h3 {
|
|||||||
outline: 3px solid rgba(204, 120, 92, 0.15);
|
outline: 3px solid rgba(204, 120, 92, 0.15);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.select-input {
|
||||||
|
appearance: auto;
|
||||||
|
}
|
||||||
|
|
||||||
.text-area {
|
.text-area {
|
||||||
min-height: 144px;
|
min-height: 144px;
|
||||||
resize: vertical;
|
resize: vertical;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.text-area--code {
|
||||||
|
min-height: 160px;
|
||||||
|
font-family: "JetBrains Mono", ui-monospace, SFMono-Regular, Menlo, monospace;
|
||||||
|
font-size: 14px;
|
||||||
|
line-height: 1.6;
|
||||||
|
}
|
||||||
|
|
||||||
.form-actions {
|
.form-actions {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
@@ -621,9 +666,178 @@ h3 {
|
|||||||
color: var(--on-dark);
|
color: var(--on-dark);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
body.modal-open {
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal {
|
||||||
|
position: fixed;
|
||||||
|
inset: 0;
|
||||||
|
z-index: 50;
|
||||||
|
display: none;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
padding: 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal.is-open {
|
||||||
|
display: flex;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal__backdrop {
|
||||||
|
position: absolute;
|
||||||
|
inset: 0;
|
||||||
|
background: rgba(20, 20, 19, 0.48);
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal__dialog {
|
||||||
|
position: relative;
|
||||||
|
width: min(1040px, 100%);
|
||||||
|
max-height: min(86vh, 860px);
|
||||||
|
overflow: auto;
|
||||||
|
border-radius: 12px;
|
||||||
|
background: var(--canvas);
|
||||||
|
box-shadow: 0 24px 80px rgba(20, 20, 19, 0.24);
|
||||||
|
padding: 32px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal__header {
|
||||||
|
display: flex;
|
||||||
|
align-items: start;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: 24px;
|
||||||
|
border-bottom: 1px solid var(--hairline);
|
||||||
|
padding-bottom: 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal__header h2 {
|
||||||
|
margin-top: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.push-settings {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 280px minmax(0, 1fr);
|
||||||
|
gap: 24px;
|
||||||
|
margin-top: 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.channel-list-panel {
|
||||||
|
border-radius: 12px;
|
||||||
|
background: var(--surface-card);
|
||||||
|
padding: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.panel-header--light {
|
||||||
|
color: var(--muted);
|
||||||
|
}
|
||||||
|
|
||||||
|
.channel-list {
|
||||||
|
display: grid;
|
||||||
|
gap: 10px;
|
||||||
|
max-height: 520px;
|
||||||
|
overflow: auto;
|
||||||
|
margin-top: 18px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.channel-list__item {
|
||||||
|
width: 100%;
|
||||||
|
border: 1px solid var(--hairline);
|
||||||
|
border-radius: 8px;
|
||||||
|
background: var(--canvas);
|
||||||
|
color: var(--body);
|
||||||
|
padding: 14px;
|
||||||
|
text-align: left;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.channel-list__item.is-active {
|
||||||
|
border-color: var(--surface-dark);
|
||||||
|
background: var(--surface-dark);
|
||||||
|
color: var(--on-dark);
|
||||||
|
}
|
||||||
|
|
||||||
|
.channel-list__name {
|
||||||
|
display: block;
|
||||||
|
overflow-wrap: anywhere;
|
||||||
|
color: inherit;
|
||||||
|
font-size: 15px;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.channel-list__meta {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: 12px;
|
||||||
|
margin-top: 8px;
|
||||||
|
color: var(--muted);
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.channel-list__item.is-active .channel-list__meta {
|
||||||
|
color: var(--on-dark-soft);
|
||||||
|
}
|
||||||
|
|
||||||
|
.channel-form {
|
||||||
|
border-radius: 12px;
|
||||||
|
background: var(--surface-card);
|
||||||
|
padding: 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-fieldset {
|
||||||
|
margin: 0;
|
||||||
|
border: 0;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-fieldset:disabled {
|
||||||
|
opacity: 0.62;
|
||||||
|
}
|
||||||
|
|
||||||
|
.channel-form__grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||||
|
gap: 18px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.channel-form label {
|
||||||
|
display: grid;
|
||||||
|
gap: 8px;
|
||||||
|
color: var(--body-strong);
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.switch-row {
|
||||||
|
grid-column: 1 / -1;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 10px;
|
||||||
|
width: max-content;
|
||||||
|
}
|
||||||
|
|
||||||
|
.switch-row input {
|
||||||
|
width: 18px;
|
||||||
|
min-height: 18px;
|
||||||
|
accent-color: var(--primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.field-row--wide {
|
||||||
|
grid-column: 1 / -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.channel-form__actions {
|
||||||
|
margin-top: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.is-hidden {
|
||||||
|
display: none !important;
|
||||||
|
}
|
||||||
|
|
||||||
@media (max-width: 1024px) {
|
@media (max-width: 1024px) {
|
||||||
.hero-grid,
|
.hero-grid,
|
||||||
.workbench-grid {
|
.workbench-grid,
|
||||||
|
.push-settings {
|
||||||
grid-template-columns: 1fr;
|
grid-template-columns: 1fr;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -656,7 +870,7 @@ h3 {
|
|||||||
padding-bottom: 4px;
|
padding-bottom: 4px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.top-nav .button {
|
.top-nav__actions {
|
||||||
margin-left: auto;
|
margin-left: auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -720,4 +934,33 @@ h3 {
|
|||||||
grid-template-columns: 1fr;
|
grid-template-columns: 1fr;
|
||||||
gap: 4px;
|
gap: 4px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.modal {
|
||||||
|
align-items: stretch;
|
||||||
|
padding: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal__dialog {
|
||||||
|
max-height: calc(100vh - 24px);
|
||||||
|
padding: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal__header {
|
||||||
|
gap: 16px;
|
||||||
|
padding-bottom: 18px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.channel-form,
|
||||||
|
.channel-list-panel {
|
||||||
|
padding: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.channel-form__grid {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
}
|
||||||
|
|
||||||
|
.switch-row,
|
||||||
|
.field-row--wide {
|
||||||
|
grid-column: auto;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+387
-1
@@ -1,8 +1,26 @@
|
|||||||
const state = {
|
const state = {
|
||||||
busy: false
|
busy: false,
|
||||||
|
pushChannels: [],
|
||||||
|
selectedPushChannel: -1,
|
||||||
|
pushLoaded: false
|
||||||
};
|
};
|
||||||
|
|
||||||
const elements = {
|
const elements = {
|
||||||
|
pushSettingsButton: document.querySelector('#pushSettingsButton'),
|
||||||
|
pushModal: document.querySelector('#pushModal'),
|
||||||
|
pushModalClose: document.querySelector('#pushModalClose'),
|
||||||
|
pushAddButton: document.querySelector('#pushAddButton'),
|
||||||
|
pushDeleteButton: document.querySelector('#pushDeleteButton'),
|
||||||
|
pushSaveButton: document.querySelector('#pushSaveButton'),
|
||||||
|
pushTestButton: document.querySelector('#pushTestButton'),
|
||||||
|
pushChannelList: document.querySelector('#pushChannelList'),
|
||||||
|
pushChannelForm: document.querySelector('#pushChannelForm'),
|
||||||
|
pushFormFields: document.querySelector('#pushFormFields'),
|
||||||
|
pushStatus: document.querySelector('#pushStatus'),
|
||||||
|
pushUrlLabel: document.querySelector('#pushUrlLabel'),
|
||||||
|
pushSecretLabel: document.querySelector('#pushSecretLabel'),
|
||||||
|
pushKey1Label: document.querySelector('#pushKey1Label'),
|
||||||
|
pushKey2Label: document.querySelector('#pushKey2Label'),
|
||||||
refreshButton: document.querySelector('#refreshButton'),
|
refreshButton: document.querySelector('#refreshButton'),
|
||||||
logsButton: document.querySelector('#logsButton'),
|
logsButton: document.querySelector('#logsButton'),
|
||||||
readyBadge: document.querySelector('#readyBadge'),
|
readyBadge: document.querySelector('#readyBadge'),
|
||||||
@@ -29,6 +47,60 @@ const elements = {
|
|||||||
logWindow: document.querySelector('#logWindow')
|
logWindow: document.querySelector('#logWindow')
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const PUSH_TYPE_OPTIONS = {
|
||||||
|
dingtalk: '钉钉机器人',
|
||||||
|
feishu: '飞书机器人',
|
||||||
|
telegram: 'Telegram Bot',
|
||||||
|
pushplus: 'PushPlus',
|
||||||
|
serverchan: 'Server酱',
|
||||||
|
post_json: 'POST JSON',
|
||||||
|
bark: 'Bark',
|
||||||
|
get: 'GET',
|
||||||
|
custom: '自定义 POST'
|
||||||
|
};
|
||||||
|
|
||||||
|
const PUSH_TYPE_META = {
|
||||||
|
dingtalk: {
|
||||||
|
urlLabel: '钉钉 Webhook URL',
|
||||||
|
secretLabel: '加签密钥',
|
||||||
|
show: ['url', 'secret']
|
||||||
|
},
|
||||||
|
feishu: {
|
||||||
|
urlLabel: '飞书 Webhook URL',
|
||||||
|
secretLabel: '加签密钥',
|
||||||
|
show: ['url', 'secret']
|
||||||
|
},
|
||||||
|
telegram: {
|
||||||
|
urlLabel: 'Bot Token',
|
||||||
|
key1Label: 'Chat ID',
|
||||||
|
show: ['url', 'key1']
|
||||||
|
},
|
||||||
|
pushplus: {
|
||||||
|
key1Label: 'PushPlus Token',
|
||||||
|
show: ['key1']
|
||||||
|
},
|
||||||
|
serverchan: {
|
||||||
|
key1Label: 'Server酱 SendKey',
|
||||||
|
show: ['key1']
|
||||||
|
},
|
||||||
|
post_json: {
|
||||||
|
urlLabel: 'POST URL',
|
||||||
|
show: ['url']
|
||||||
|
},
|
||||||
|
bark: {
|
||||||
|
urlLabel: 'Bark 推送 URL',
|
||||||
|
show: ['url']
|
||||||
|
},
|
||||||
|
get: {
|
||||||
|
urlLabel: 'GET URL',
|
||||||
|
show: ['url']
|
||||||
|
},
|
||||||
|
custom: {
|
||||||
|
urlLabel: 'POST URL',
|
||||||
|
show: ['url', 'customBody']
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
function setText(node, value) {
|
function setText(node, value) {
|
||||||
node.textContent = value || '--';
|
node.textContent = value || '--';
|
||||||
}
|
}
|
||||||
@@ -207,6 +279,303 @@ function renderMessages(messages) {
|
|||||||
elements.messageList.append(fragment);
|
elements.messageList.append(fragment);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function getPushTypeLabel(type) {
|
||||||
|
return PUSH_TYPE_OPTIONS[type] || type || '推送通道';
|
||||||
|
}
|
||||||
|
|
||||||
|
function createDefaultPushChannel(type = 'dingtalk') {
|
||||||
|
return {
|
||||||
|
enabled: true,
|
||||||
|
type,
|
||||||
|
name: `${getPushTypeLabel(type)}通知`,
|
||||||
|
url: '',
|
||||||
|
secret: '',
|
||||||
|
key1: '',
|
||||||
|
key2: '',
|
||||||
|
customBody: type === 'custom' ? '{"from":"{sender}","message":"{message}","timestamp":"{timestamp}"}' : ''
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function normalizePushChannel(channel = {}) {
|
||||||
|
const fallback = createDefaultPushChannel(channel.type || 'dingtalk');
|
||||||
|
return {
|
||||||
|
...fallback,
|
||||||
|
enabled: Boolean(channel.enabled),
|
||||||
|
type: String(channel.type || fallback.type),
|
||||||
|
name: String(channel.name || fallback.name),
|
||||||
|
url: String(channel.url || ''),
|
||||||
|
secret: String(channel.secret || ''),
|
||||||
|
key1: String(channel.key1 || ''),
|
||||||
|
key2: String(channel.key2 || ''),
|
||||||
|
customBody: String(channel.customBody || '')
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function setPushStatus(message, type = '') {
|
||||||
|
setStatusMessage(elements.pushStatus, message, type);
|
||||||
|
}
|
||||||
|
|
||||||
|
function getPushControls() {
|
||||||
|
return elements.pushChannelForm.elements;
|
||||||
|
}
|
||||||
|
|
||||||
|
function readPushChannelForm() {
|
||||||
|
const controls = getPushControls();
|
||||||
|
return {
|
||||||
|
enabled: controls.enabled.checked,
|
||||||
|
type: String(controls.type.value || 'dingtalk'),
|
||||||
|
name: String(controls.name.value || '').trim(),
|
||||||
|
url: String(controls.url.value || '').trim(),
|
||||||
|
secret: String(controls.secret.value || '').trim(),
|
||||||
|
key1: String(controls.key1.value || '').trim(),
|
||||||
|
key2: String(controls.key2.value || '').trim(),
|
||||||
|
customBody: String(controls.customBody.value || '').trim()
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function writePushChannelForm(channel) {
|
||||||
|
const controls = getPushControls();
|
||||||
|
controls.enabled.checked = Boolean(channel.enabled);
|
||||||
|
controls.type.value = channel.type;
|
||||||
|
controls.name.value = channel.name;
|
||||||
|
controls.url.value = channel.url;
|
||||||
|
controls.secret.value = channel.secret;
|
||||||
|
controls.key1.value = channel.key1;
|
||||||
|
controls.key2.value = channel.key2;
|
||||||
|
controls.customBody.value = channel.customBody;
|
||||||
|
updatePushFieldVisibility(channel.type);
|
||||||
|
}
|
||||||
|
|
||||||
|
function clearPushChannelForm() {
|
||||||
|
writePushChannelForm(createDefaultPushChannel());
|
||||||
|
}
|
||||||
|
|
||||||
|
function setPushFieldVisible(name, visible) {
|
||||||
|
const row = elements.pushChannelForm.querySelector(`[data-push-field="${name}"]`);
|
||||||
|
if (row) {
|
||||||
|
row.classList.toggle('is-hidden', !visible);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function updatePushFieldVisibility(type) {
|
||||||
|
const meta = PUSH_TYPE_META[type] || PUSH_TYPE_META.dingtalk;
|
||||||
|
const visibleFields = new Set(meta.show || []);
|
||||||
|
|
||||||
|
elements.pushUrlLabel.textContent = meta.urlLabel || 'Webhook URL';
|
||||||
|
elements.pushSecretLabel.textContent = meta.secretLabel || '密钥';
|
||||||
|
elements.pushKey1Label.textContent = meta.key1Label || 'Key1';
|
||||||
|
elements.pushKey2Label.textContent = meta.key2Label || 'Key2';
|
||||||
|
|
||||||
|
['url', 'secret', 'key1', 'key2', 'customBody'].forEach((name) => {
|
||||||
|
setPushFieldVisible(name, visibleFields.has(name));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function syncCurrentPushChannel() {
|
||||||
|
const index = state.selectedPushChannel;
|
||||||
|
if (index < 0 || !state.pushChannels[index] || elements.pushFormFields.disabled) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
state.pushChannels[index] = normalizePushChannel(readPushChannelForm());
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderPushChannelList() {
|
||||||
|
elements.pushChannelList.replaceChildren();
|
||||||
|
|
||||||
|
if (!state.pushChannels.length) {
|
||||||
|
const empty = document.createElement('p');
|
||||||
|
empty.className = 'empty-state empty-state--light';
|
||||||
|
empty.textContent = '暂无推送通道。';
|
||||||
|
elements.pushChannelList.append(empty);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const fragment = document.createDocumentFragment();
|
||||||
|
state.pushChannels.forEach((channel, index) => {
|
||||||
|
const item = document.createElement('button');
|
||||||
|
item.type = 'button';
|
||||||
|
item.className = `channel-list__item${index === state.selectedPushChannel ? ' is-active' : ''}`;
|
||||||
|
|
||||||
|
const name = document.createElement('span');
|
||||||
|
name.className = 'channel-list__name';
|
||||||
|
name.textContent = channel.name || getPushTypeLabel(channel.type);
|
||||||
|
|
||||||
|
const meta = document.createElement('span');
|
||||||
|
meta.className = 'channel-list__meta';
|
||||||
|
|
||||||
|
const type = document.createElement('span');
|
||||||
|
type.textContent = getPushTypeLabel(channel.type);
|
||||||
|
|
||||||
|
const status = document.createElement('span');
|
||||||
|
status.textContent = channel.enabled ? '启用' : '停用';
|
||||||
|
|
||||||
|
meta.append(type, status);
|
||||||
|
item.append(name, meta);
|
||||||
|
item.addEventListener('click', () => {
|
||||||
|
selectPushChannel(index);
|
||||||
|
});
|
||||||
|
fragment.append(item);
|
||||||
|
});
|
||||||
|
|
||||||
|
elements.pushChannelList.append(fragment);
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderPushChannelForm() {
|
||||||
|
const channel = state.pushChannels[state.selectedPushChannel];
|
||||||
|
const hasChannel = Boolean(channel);
|
||||||
|
|
||||||
|
elements.pushFormFields.disabled = !hasChannel;
|
||||||
|
elements.pushTestButton.disabled = !hasChannel;
|
||||||
|
elements.pushDeleteButton.disabled = !hasChannel;
|
||||||
|
elements.pushSaveButton.disabled = !state.pushLoaded;
|
||||||
|
|
||||||
|
if (!hasChannel) {
|
||||||
|
clearPushChannelForm();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
writePushChannelForm(channel);
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderPushSettings() {
|
||||||
|
renderPushChannelList();
|
||||||
|
renderPushChannelForm();
|
||||||
|
}
|
||||||
|
|
||||||
|
function selectPushChannel(index) {
|
||||||
|
syncCurrentPushChannel();
|
||||||
|
state.selectedPushChannel = index;
|
||||||
|
renderPushSettings();
|
||||||
|
setPushStatus('');
|
||||||
|
}
|
||||||
|
|
||||||
|
function addPushChannel() {
|
||||||
|
syncCurrentPushChannel();
|
||||||
|
state.pushChannels.push(createDefaultPushChannel());
|
||||||
|
state.selectedPushChannel = state.pushChannels.length - 1;
|
||||||
|
renderPushSettings();
|
||||||
|
setPushStatus('');
|
||||||
|
}
|
||||||
|
|
||||||
|
function deletePushChannel() {
|
||||||
|
if (state.selectedPushChannel < 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
state.pushChannels.splice(state.selectedPushChannel, 1);
|
||||||
|
state.selectedPushChannel = Math.min(state.selectedPushChannel, state.pushChannels.length - 1);
|
||||||
|
renderPushSettings();
|
||||||
|
setPushStatus('');
|
||||||
|
}
|
||||||
|
|
||||||
|
async function loadPushChannels() {
|
||||||
|
elements.pushAddButton.disabled = true;
|
||||||
|
elements.pushSaveButton.disabled = true;
|
||||||
|
elements.pushTestButton.disabled = true;
|
||||||
|
elements.pushDeleteButton.disabled = true;
|
||||||
|
setPushStatus('加载中...');
|
||||||
|
|
||||||
|
try {
|
||||||
|
const result = await requestJSON('/api/push/channels');
|
||||||
|
state.pushChannels = (result.data || []).map(normalizePushChannel);
|
||||||
|
state.selectedPushChannel = state.pushChannels.length ? 0 : -1;
|
||||||
|
state.pushLoaded = true;
|
||||||
|
renderPushSettings();
|
||||||
|
setPushStatus('');
|
||||||
|
} catch (err) {
|
||||||
|
setPushStatus(err.message, 'error');
|
||||||
|
} finally {
|
||||||
|
elements.pushAddButton.disabled = false;
|
||||||
|
elements.pushSaveButton.disabled = !state.pushLoaded;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function openPushModal() {
|
||||||
|
elements.pushModal.classList.add('is-open');
|
||||||
|
elements.pushModal.setAttribute('aria-hidden', 'false');
|
||||||
|
document.body.classList.add('modal-open');
|
||||||
|
|
||||||
|
if (!state.pushLoaded) {
|
||||||
|
loadPushChannels();
|
||||||
|
} else {
|
||||||
|
renderPushSettings();
|
||||||
|
setPushStatus('');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function closePushModal() {
|
||||||
|
syncCurrentPushChannel();
|
||||||
|
elements.pushModal.classList.remove('is-open');
|
||||||
|
elements.pushModal.setAttribute('aria-hidden', 'true');
|
||||||
|
document.body.classList.remove('modal-open');
|
||||||
|
}
|
||||||
|
|
||||||
|
async function savePushChannels(event) {
|
||||||
|
event.preventDefault();
|
||||||
|
syncCurrentPushChannel();
|
||||||
|
|
||||||
|
elements.pushSaveButton.disabled = true;
|
||||||
|
setPushStatus('保存中...');
|
||||||
|
|
||||||
|
try {
|
||||||
|
const result = await requestJSON('/api/push/channels', {
|
||||||
|
method: 'PUT',
|
||||||
|
body: JSON.stringify({ channels: state.pushChannels })
|
||||||
|
});
|
||||||
|
state.pushChannels = (result.data || []).map(normalizePushChannel);
|
||||||
|
if (state.selectedPushChannel >= state.pushChannels.length) {
|
||||||
|
state.selectedPushChannel = state.pushChannels.length - 1;
|
||||||
|
}
|
||||||
|
renderPushSettings();
|
||||||
|
setPushStatus(result.message || '推送通道配置已保存。', 'success');
|
||||||
|
await refreshLogs();
|
||||||
|
} catch (err) {
|
||||||
|
setPushStatus(err.message, 'error');
|
||||||
|
} finally {
|
||||||
|
elements.pushSaveButton.disabled = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function testPushChannel() {
|
||||||
|
syncCurrentPushChannel();
|
||||||
|
|
||||||
|
const channel = state.pushChannels[state.selectedPushChannel];
|
||||||
|
if (!channel) {
|
||||||
|
setPushStatus('请先新增一个推送通道。', 'error');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
elements.pushTestButton.disabled = true;
|
||||||
|
setPushStatus('测试发送中...');
|
||||||
|
|
||||||
|
try {
|
||||||
|
const result = await requestJSON('/api/push/test', {
|
||||||
|
method: 'POST',
|
||||||
|
body: JSON.stringify({ channel })
|
||||||
|
});
|
||||||
|
setPushStatus(result.message || '测试推送已发送。', 'success');
|
||||||
|
await refreshLogs();
|
||||||
|
} catch (err) {
|
||||||
|
setPushStatus(err.message, 'error');
|
||||||
|
} finally {
|
||||||
|
elements.pushTestButton.disabled = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function handlePushTypeChange() {
|
||||||
|
const controls = getPushControls();
|
||||||
|
const type = controls.type.value;
|
||||||
|
|
||||||
|
updatePushFieldVisibility(type);
|
||||||
|
if (!controls.name.value.trim()) {
|
||||||
|
controls.name.value = `${getPushTypeLabel(type)}通知`;
|
||||||
|
}
|
||||||
|
if (type === 'custom' && !controls.customBody.value.trim()) {
|
||||||
|
controls.customBody.value = createDefaultPushChannel('custom').customBody;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async function refreshLogs() {
|
async function refreshLogs() {
|
||||||
const logs = await requestJSON('/api/logs');
|
const logs = await requestJSON('/api/logs');
|
||||||
renderLogs(logs.data || []);
|
renderLogs(logs.data || []);
|
||||||
@@ -304,11 +673,28 @@ async function runATCommand(event) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
elements.pushSettingsButton.addEventListener('click', openPushModal);
|
||||||
|
elements.pushModalClose.addEventListener('click', closePushModal);
|
||||||
|
elements.pushModal.addEventListener('click', (event) => {
|
||||||
|
if (event.target.matches('[data-close-push-modal]')) {
|
||||||
|
closePushModal();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
elements.pushAddButton.addEventListener('click', addPushChannel);
|
||||||
|
elements.pushDeleteButton.addEventListener('click', deletePushChannel);
|
||||||
|
elements.pushChannelForm.addEventListener('submit', savePushChannels);
|
||||||
|
elements.pushTestButton.addEventListener('click', testPushChannel);
|
||||||
|
elements.pushChannelForm.elements.type.addEventListener('change', handlePushTypeChange);
|
||||||
elements.refreshButton.addEventListener('click', refreshAll);
|
elements.refreshButton.addEventListener('click', refreshAll);
|
||||||
elements.logsButton.addEventListener('click', refreshLogs);
|
elements.logsButton.addEventListener('click', refreshLogs);
|
||||||
elements.messagesButton.addEventListener('click', refreshMessages);
|
elements.messagesButton.addEventListener('click', refreshMessages);
|
||||||
elements.smsForm.addEventListener('submit', sendSMS);
|
elements.smsForm.addEventListener('submit', sendSMS);
|
||||||
elements.atForm.addEventListener('submit', runATCommand);
|
elements.atForm.addEventListener('submit', runATCommand);
|
||||||
|
document.addEventListener('keydown', (event) => {
|
||||||
|
if (event.key === 'Escape' && elements.pushModal.classList.contains('is-open')) {
|
||||||
|
closePushModal();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
refreshAll();
|
refreshAll();
|
||||||
setInterval(() => {
|
setInterval(() => {
|
||||||
|
|||||||
+238
-1
@@ -1,3 +1,4 @@
|
|||||||
|
import fs from 'fs';
|
||||||
import express from 'express';
|
import express from 'express';
|
||||||
import crypto from 'crypto';
|
import crypto from 'crypto';
|
||||||
import path from 'path';
|
import path from 'path';
|
||||||
@@ -7,13 +8,39 @@ import logger from './logger.js';
|
|||||||
const __filename = fileURLToPath(import.meta.url);
|
const __filename = fileURLToPath(import.meta.url);
|
||||||
const __dirname = path.dirname(__filename);
|
const __dirname = path.dirname(__filename);
|
||||||
|
|
||||||
|
const PUSH_CHANNEL_TYPES = new Set([
|
||||||
|
'post_json',
|
||||||
|
'bark',
|
||||||
|
'get',
|
||||||
|
'dingtalk',
|
||||||
|
'pushplus',
|
||||||
|
'serverchan',
|
||||||
|
'custom',
|
||||||
|
'feishu',
|
||||||
|
'telegram'
|
||||||
|
]);
|
||||||
|
|
||||||
|
const PUSH_CHANNEL_TYPE_LABELS = {
|
||||||
|
post_json: 'POST JSON',
|
||||||
|
bark: 'Bark',
|
||||||
|
get: 'GET',
|
||||||
|
dingtalk: '钉钉',
|
||||||
|
pushplus: 'PushPlus',
|
||||||
|
serverchan: 'Server酱',
|
||||||
|
custom: '自定义',
|
||||||
|
feishu: '飞书',
|
||||||
|
telegram: 'Telegram'
|
||||||
|
};
|
||||||
|
|
||||||
class APIServer {
|
class APIServer {
|
||||||
constructor(config, modem, smsProcessor) {
|
constructor(config, modem, smsProcessor, pushManager) {
|
||||||
this.config = config;
|
this.config = config;
|
||||||
this.modem = modem;
|
this.modem = modem;
|
||||||
this.smsProcessor = smsProcessor;
|
this.smsProcessor = smsProcessor;
|
||||||
|
this.pushManager = pushManager;
|
||||||
this.app = express();
|
this.app = express();
|
||||||
this.publicDir = path.join(__dirname, '../public');
|
this.publicDir = path.join(__dirname, '../public');
|
||||||
|
this.configPath = path.join(__dirname, '../config.json');
|
||||||
|
|
||||||
this.setupMiddleware();
|
this.setupMiddleware();
|
||||||
this.setupRoutes();
|
this.setupRoutes();
|
||||||
@@ -194,6 +221,135 @@ class APIServer {
|
|||||||
</html>`);
|
</html>`);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async readConfigFile() {
|
||||||
|
const configContent = await fs.promises.readFile(this.configPath, 'utf8');
|
||||||
|
return JSON.parse(configContent);
|
||||||
|
}
|
||||||
|
|
||||||
|
async savePushChannels(channels) {
|
||||||
|
const diskConfig = await this.readConfigFile();
|
||||||
|
diskConfig.pushChannels = channels;
|
||||||
|
|
||||||
|
await fs.promises.writeFile(
|
||||||
|
this.configPath,
|
||||||
|
`${JSON.stringify(diskConfig, null, 2)}\n`,
|
||||||
|
'utf8'
|
||||||
|
);
|
||||||
|
|
||||||
|
this.config.pushChannels = channels;
|
||||||
|
if (this.pushManager?.config) {
|
||||||
|
this.pushManager.config.pushChannels = channels;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
getPushChannelsFromConfig(config = this.config) {
|
||||||
|
return Array.isArray(config.pushChannels) ? config.pushChannels : [];
|
||||||
|
}
|
||||||
|
|
||||||
|
normalizePushChannels(channels) {
|
||||||
|
if (!Array.isArray(channels)) {
|
||||||
|
throw new Error('pushChannels 必须是数组');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (channels.length > 30) {
|
||||||
|
throw new Error('推送通道数量不能超过 30 个');
|
||||||
|
}
|
||||||
|
|
||||||
|
return channels.map((channel, index) => this.normalizePushChannel(channel, index));
|
||||||
|
}
|
||||||
|
|
||||||
|
normalizePushChannel(channel, index = 0) {
|
||||||
|
if (!channel || typeof channel !== 'object' || Array.isArray(channel)) {
|
||||||
|
throw new Error(`第 ${index + 1} 个推送通道格式不正确`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const type = this.normalizeString(channel.type, '推送类型', 32);
|
||||||
|
if (!PUSH_CHANNEL_TYPES.has(type)) {
|
||||||
|
throw new Error(`第 ${index + 1} 个推送通道类型不支持`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const typeLabel = PUSH_CHANNEL_TYPE_LABELS[type] || type;
|
||||||
|
const fallbackName = `${typeLabel}通知`;
|
||||||
|
const name = this.normalizeString(channel.name || fallbackName, '通道名称', 80);
|
||||||
|
|
||||||
|
return {
|
||||||
|
enabled: Boolean(channel.enabled),
|
||||||
|
type,
|
||||||
|
name: name || fallbackName,
|
||||||
|
url: this.normalizeString(channel.url, 'URL', 2048),
|
||||||
|
secret: this.normalizeString(channel.secret, '密钥', 2048),
|
||||||
|
key1: this.normalizeString(channel.key1, 'Key1', 2048),
|
||||||
|
key2: this.normalizeString(channel.key2, 'Key2', 2048),
|
||||||
|
customBody: this.normalizeString(channel.customBody, '自定义请求体', 10000)
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
normalizeString(value, fieldName, maxLength) {
|
||||||
|
const text = String(value ?? '').trim();
|
||||||
|
if (text.length > maxLength) {
|
||||||
|
throw new Error(`${fieldName}长度不能超过 ${maxLength} 个字符`);
|
||||||
|
}
|
||||||
|
return text;
|
||||||
|
}
|
||||||
|
|
||||||
|
validatePushChannels(channels) {
|
||||||
|
channels.forEach((channel, index) => {
|
||||||
|
this.validatePushChannel(channel, { index });
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
validatePushChannel(channel, options = {}) {
|
||||||
|
const index = options.index ?? 0;
|
||||||
|
const requireDestination = Boolean(options.requireDestination || channel.enabled);
|
||||||
|
const prefix = `第 ${index + 1} 个推送通道`;
|
||||||
|
const urlTypes = new Set(['post_json', 'bark', 'get', 'dingtalk', 'custom', 'feishu']);
|
||||||
|
|
||||||
|
if (urlTypes.has(channel.type)) {
|
||||||
|
if (requireDestination && !channel.url) {
|
||||||
|
throw new Error(`${prefix}缺少 URL`);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (channel.url && !this.isHttpUrl(channel.url)) {
|
||||||
|
throw new Error(`${prefix}的 URL 格式不正确`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (channel.type === 'telegram') {
|
||||||
|
if (requireDestination && !channel.url) {
|
||||||
|
throw new Error(`${prefix}缺少 Bot Token`);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (requireDestination && !channel.key1) {
|
||||||
|
throw new Error(`${prefix}缺少 Chat ID`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (channel.type === 'pushplus' && requireDestination && !channel.key1) {
|
||||||
|
throw new Error(`${prefix}缺少 PushPlus Token`);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (channel.type === 'serverchan' && requireDestination && !channel.key1) {
|
||||||
|
throw new Error(`${prefix}缺少 Server酱 SendKey`);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (channel.type === 'custom' && channel.customBody) {
|
||||||
|
try {
|
||||||
|
JSON.parse(channel.customBody);
|
||||||
|
} catch (err) {
|
||||||
|
throw new Error(`${prefix}的自定义请求体不是有效 JSON`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
isHttpUrl(value) {
|
||||||
|
try {
|
||||||
|
const url = new URL(value);
|
||||||
|
return url.protocol === 'http:' || url.protocol === 'https:';
|
||||||
|
} catch (err) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
setupRoutes() {
|
setupRoutes() {
|
||||||
this.app.get('/', (req, res) => {
|
this.app.get('/', (req, res) => {
|
||||||
res.redirect('/admin');
|
res.redirect('/admin');
|
||||||
@@ -357,6 +513,87 @@ class APIServer {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// 查询推送通道配置
|
||||||
|
this.app.get('/api/push/channels', async (req, res) => {
|
||||||
|
try {
|
||||||
|
const diskConfig = await this.readConfigFile();
|
||||||
|
const channels = this.normalizePushChannels(this.getPushChannelsFromConfig(diskConfig));
|
||||||
|
|
||||||
|
res.json({
|
||||||
|
success: true,
|
||||||
|
data: channels
|
||||||
|
});
|
||||||
|
} catch (err) {
|
||||||
|
logger.error('读取推送通道配置失败:', err);
|
||||||
|
res.status(500).json({
|
||||||
|
success: false,
|
||||||
|
error: err.message
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// 保存推送通道配置
|
||||||
|
this.app.put('/api/push/channels', async (req, res) => {
|
||||||
|
try {
|
||||||
|
const rawChannels = Array.isArray(req.body) ? req.body : req.body?.channels;
|
||||||
|
const channels = this.normalizePushChannels(rawChannels);
|
||||||
|
this.validatePushChannels(channels);
|
||||||
|
await this.savePushChannels(channels);
|
||||||
|
|
||||||
|
res.json({
|
||||||
|
success: true,
|
||||||
|
message: '推送通道配置已保存',
|
||||||
|
data: channels
|
||||||
|
});
|
||||||
|
} catch (err) {
|
||||||
|
logger.error('保存推送通道配置失败:', err);
|
||||||
|
res.status(400).json({
|
||||||
|
success: false,
|
||||||
|
error: err.message
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// 测试推送通道
|
||||||
|
this.app.post('/api/push/test', async (req, res) => {
|
||||||
|
try {
|
||||||
|
if (!this.pushManager) {
|
||||||
|
return res.status(500).json({
|
||||||
|
success: false,
|
||||||
|
error: '推送管理器未初始化'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const channel = this.normalizePushChannel(req.body?.channel || req.body);
|
||||||
|
this.validatePushChannel(channel, { requireDestination: true });
|
||||||
|
|
||||||
|
const success = await this.pushManager.pushToChannel(
|
||||||
|
{ ...channel, enabled: true },
|
||||||
|
'测试号码',
|
||||||
|
'这是一条短信网关推送测试消息',
|
||||||
|
new Date().toISOString()
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!success) {
|
||||||
|
return res.status(502).json({
|
||||||
|
success: false,
|
||||||
|
error: '测试推送发送失败,请查看运行日志'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
res.json({
|
||||||
|
success: true,
|
||||||
|
message: '测试推送已发送'
|
||||||
|
});
|
||||||
|
} catch (err) {
|
||||||
|
logger.error('测试推送失败:', err);
|
||||||
|
res.status(400).json({
|
||||||
|
success: false,
|
||||||
|
error: err.message
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
// 健康检查
|
// 健康检查
|
||||||
this.app.get('/health', (req, res) => {
|
this.app.get('/health', (req, res) => {
|
||||||
res.json({
|
res.json({
|
||||||
|
|||||||
+1
-1
@@ -99,7 +99,7 @@ async function main() {
|
|||||||
concatManager.startTimeoutChecker();
|
concatManager.startTimeoutChecker();
|
||||||
|
|
||||||
// 启动API服务器
|
// 启动API服务器
|
||||||
const apiServer = new APIServer(config, modem, smsProcessor);
|
const apiServer = new APIServer(config, modem, smsProcessor, pushManager);
|
||||||
apiServer.start();
|
apiServer.start();
|
||||||
|
|
||||||
logger.info('========================================');
|
logger.info('========================================');
|
||||||
|
|||||||
Reference in New Issue
Block a user