vk-ssl-auto-deploy 0.8.3 → 0.8.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,18 +1,48 @@
1
1
  # vk-ssl-auto-deploy
2
2
 
3
- SSL证书自动部署工具 - 提供HTTP API接口,支持证书文件自动上传和部署
3
+ 无忧SSL证书平台自动部署工具 - 提供HTTP API接口,支持证书文件自动上传和部署
4
+
5
+ ### 支持系统
6
+
7
+ | 系统 | 最低版本 |
8
+ |------|----------|
9
+ | Alibaba Cloud Linux | 3+ |
10
+ | CentOS | 7+ |
11
+ | Ubuntu | 20+ |
12
+ | Debian | 12+ |
13
+ | Fedora | 40+ |
14
+ | OpenSUSE | 15+ |
15
+ | Rocky Linux | 8+ |
16
+ | CentOS Stream | 9+ |
17
+ | AlmaLinux | 8+ |
18
+ | Anolis OS | 不支持 |
19
+ | Windows | 10 / 11 / Server 2016+ |
4
20
 
5
21
  ## 一键安装(推荐)
6
22
 
23
+ ### Linux 系统
24
+
7
25
  Linux 服务器执行以下命令,自动完成安装、部署和开机自启配置:
8
26
 
9
27
  ```bash
10
28
  curl -fsSL https://gitee.com/vk1688/vk-ssl-auto-deploy/raw/master/install.sh | sudo bash
11
29
  ```
12
30
 
13
- 安装完成后,访问 `http://服务器IP:6001/admin`,默认密码:`admin@123456`
31
+ ### Windows 系统
32
+
33
+ **PowerShell (推荐):**
34
+
35
+ ```powershell
36
+ iwr -useb https://gitee.com/vk1688/vk-ssl-auto-deploy/raw/master/install.ps1 | iex
37
+ ```
14
38
 
15
- > 支持 CentOS/RHEL 和 Debian/Ubuntu 系统,自动安装 Node.js 和 PM2
39
+ **CMD (命令提示符):**
40
+
41
+ ```cmd
42
+ powershell -c "iwr -useb https://gitee.com/vk1688/vk-ssl-auto-deploy/raw/master/install.ps1 | iex"
43
+ ```
44
+
45
+ 安装完成后,访问 `http://服务器IP:6001/admin`,默认密码:`admin@123456`
16
46
 
17
47
  ---
18
48
 
@@ -30,7 +60,7 @@ curl -fsSL https://rpm.nodesource.com/setup_18.x | sudo bash - && sudo yum insta
30
60
 
31
61
  #### Windows系统
32
62
 
33
- ```bash
63
+ ```powershell
34
64
  winget install --id OpenJS.NodeJS.LTS -e
35
65
  ```
36
66
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "vk-ssl-auto-deploy",
3
- "version": "0.8.3",
3
+ "version": "0.8.6",
4
4
  "description": "SSL证书自动部署工具 - 提供HTTP API接口,支持证书文件自动上传和部署",
5
5
  "main": "app.js",
6
6
  "scripts": {
@@ -1,8 +1,8 @@
1
- body {
2
- padding: 50px;
3
- font: 14px "Lucida Grande", Helvetica, Arial, sans-serif;
4
- }
5
-
6
- a {
7
- color: #00B7FF;
8
- }
1
+ body {
2
+ padding: 50px;
3
+ font: 14px "Lucida Grande", Helvetica, Arial, sans-serif;
4
+ }
5
+
6
+ a {
7
+ color: #00B7FF;
8
+ }
package/routes/admin.js CHANGED
@@ -3,6 +3,11 @@ const router = express.Router();
3
3
  const fs = require('fs');
4
4
  const path = require('path');
5
5
  const crypto = require('crypto');
6
+ const { exec } = require('child_process');
7
+ const { promisify } = require('util');
8
+ const AdmZip = require('adm-zip');
9
+
10
+ const execAsync = promisify(exec);
6
11
 
7
12
  const CONFIG_FILE = path.join(__dirname, '..', 'config.json');
8
13
 
@@ -266,4 +271,242 @@ router.get('/api/certs/local', verifyPassword, (req, res) => {
266
271
  }
267
272
  });
268
273
 
274
+ /**
275
+ * POST /api/test-nginx
276
+ * 测试 Nginx 路径是否正确
277
+ */
278
+ router.post('/api/test-nginx', verifyPassword, async (req, res) => {
279
+ try {
280
+ const { nginxPath } = req.body;
281
+
282
+ if (!nginxPath) {
283
+ return res.json({
284
+ code: 400,
285
+ msg: 'Nginx 路径不能为空',
286
+ data: null
287
+ });
288
+ }
289
+
290
+ // 检查文件是否存在
291
+ if (!fs.existsSync(nginxPath)) {
292
+ return res.json({
293
+ code: 404,
294
+ msg: 'Nginx 文件不存在',
295
+ data: null
296
+ });
297
+ }
298
+
299
+ // 执行 nginx -t 命令测试配置
300
+ try {
301
+ // 获取 nginx 所在目录作为工作目录
302
+ const nginxDir = path.dirname(nginxPath);
303
+
304
+ const { stdout, stderr } = await execAsync(`"${nginxPath}" -t`, {
305
+ timeout: 10000, // 10秒超时
306
+ cwd: nginxDir // 设置工作目录为 nginx 所在目录
307
+ });
308
+
309
+ // nginx -t 的输出通常在 stderr 中
310
+ const output = stderr || stdout;
311
+
312
+ // 检查是否包含 "syntax is ok" 和 "test is successful"
313
+ if (output.includes('syntax is ok') || output.includes('test is successful')) {
314
+ return res.json({
315
+ code: 0,
316
+ msg: 'Nginx 配置测试通过\n' + output,
317
+ data: { output }
318
+ });
319
+ } else {
320
+ return res.json({
321
+ code: 500,
322
+ msg: 'Nginx 配置测试失败\n' + output,
323
+ data: { output }
324
+ });
325
+ }
326
+ } catch (execError) {
327
+ // 执行命令失败
328
+ return res.json({
329
+ code: 500,
330
+ msg: 'Nginx 配置测试失败: ' + (execError.stderr || execError.message),
331
+ data: null
332
+ });
333
+ }
334
+ } catch (error) {
335
+ res.json({
336
+ code: 500,
337
+ msg: '测试失败: ' + error.message,
338
+ data: null
339
+ });
340
+ }
341
+ });
342
+
343
+ /**
344
+ * GET /api/cert/download/:fileName
345
+ * 下载证书(包含 cert.pem 和 cert.key 的 zip 压缩包)
346
+ */
347
+ router.get('/api/cert/download/:fileName', verifyPassword, (req, res) => {
348
+ try {
349
+ const { fileName } = req.params;
350
+ const config = JSON.parse(fs.readFileSync(CONFIG_FILE, 'utf8'));
351
+ const certDir = path.isAbsolute(config.certSaveDir) ?
352
+ config.certSaveDir :
353
+ path.join(__dirname, '..', config.certSaveDir);
354
+
355
+ // 检查目录是否存在
356
+ if (!fs.existsSync(certDir)) {
357
+ return res.status(404).json({
358
+ code: 404,
359
+ msg: '证书目录不存在',
360
+ data: null
361
+ });
362
+ }
363
+
364
+ // 构建证书文件路径
365
+ const crtFile = path.join(certDir, `${fileName}.crt`);
366
+ const keyFile = path.join(certDir, `${fileName}.key`);
367
+
368
+ // 检查文件是否存在
369
+ if (!fs.existsSync(crtFile)) {
370
+ return res.status(404).json({
371
+ code: 404,
372
+ msg: '证书文件不存在',
373
+ data: null
374
+ });
375
+ }
376
+
377
+ if (!fs.existsSync(keyFile)) {
378
+ return res.status(404).json({
379
+ code: 404,
380
+ msg: '私钥文件不存在',
381
+ data: null
382
+ });
383
+ }
384
+
385
+ // 创建 ZIP 压缩包
386
+ const zip = new AdmZip();
387
+
388
+ // 添加证书文件(重命名为 cert.pem)
389
+ zip.addLocalFile(crtFile, '', 'cert.pem');
390
+
391
+ // 添加私钥文件(重命名为 cert.key)
392
+ zip.addLocalFile(keyFile, '', 'cert.key');
393
+
394
+ // 生成 ZIP 文件
395
+ const zipBuffer = zip.toBuffer();
396
+
397
+ // 设置响应头
398
+ res.setHeader('Content-Type', 'application/zip');
399
+ res.setHeader('Content-Disposition', `attachment; filename="${fileName}.zip"`);
400
+ res.setHeader('Content-Length', zipBuffer.length);
401
+
402
+ // 发送文件
403
+ res.send(zipBuffer);
404
+ } catch (error) {
405
+ res.status(500).json({
406
+ code: 500,
407
+ msg: '下载失败: ' + error.message,
408
+ data: null
409
+ });
410
+ }
411
+ });
412
+
413
+ // 检查更新
414
+ router.post('/check-update', verifyPassword, async (req, res) => {
415
+ try {
416
+ const https = require('https');
417
+ const currentVersion = require('../package.json').version;
418
+
419
+ // 获取远程 package.json
420
+ const remoteUrl = 'https://gitee.com/vk1688/vk-ssl-auto-deploy/raw/master/package.json';
421
+
422
+ https.get(remoteUrl, (response) => {
423
+ let data = '';
424
+
425
+ response.on('data', (chunk) => {
426
+ data += chunk;
427
+ });
428
+
429
+ response.on('end', () => {
430
+ try {
431
+ const remotePackage = JSON.parse(data);
432
+ const remoteVersion = remotePackage.version;
433
+
434
+ const hasUpdate = currentVersion !== remoteVersion;
435
+
436
+ res.json({
437
+ code: 0,
438
+ msg: hasUpdate ? '发现新版本' : '已是最新版本',
439
+ data: {
440
+ currentVersion,
441
+ remoteVersion,
442
+ hasUpdate
443
+ }
444
+ });
445
+ } catch (error) {
446
+ res.status(500).json({
447
+ code: 500,
448
+ msg: '解析远程版本信息失败: ' + error.message,
449
+ data: null
450
+ });
451
+ }
452
+ });
453
+ }).on('error', (error) => {
454
+ res.status(500).json({
455
+ code: 500,
456
+ msg: '获取远程版本信息失败: ' + error.message,
457
+ data: null
458
+ });
459
+ });
460
+ } catch (error) {
461
+ res.status(500).json({
462
+ code: 500,
463
+ msg: '检查更新失败: ' + error.message,
464
+ data: null
465
+ });
466
+ }
467
+ });
468
+
469
+ // 执行更新
470
+ router.post('/execute-update', verifyPassword, async (req, res) => {
471
+ try {
472
+ const platform = process.platform;
473
+ let command;
474
+
475
+ if (platform === 'win32') {
476
+ // Windows 系统
477
+ command = 'powershell -c "iwr -useb https://gitee.com/vk1688/vk-ssl-auto-deploy/raw/master/install.ps1 | iex"';
478
+ } else {
479
+ // Linux/Unix 系统
480
+ command = 'curl -fsSL https://gitee.com/vk1688/vk-ssl-auto-deploy/raw/master/install.sh | sudo bash';
481
+ }
482
+
483
+ // 异步执行更新命令
484
+ exec(command, (error, stdout, stderr) => {
485
+ if (error) {
486
+ console.error('更新失败:', error);
487
+ return;
488
+ }
489
+ console.log('更新输出:', stdout);
490
+ if (stderr) {
491
+ console.error('更新错误:', stderr);
492
+ }
493
+ });
494
+
495
+ res.json({
496
+ code: 0,
497
+ msg: '更新命令已执行,程序将在更新完成后自动重启',
498
+ data: {
499
+ platform,
500
+ command
501
+ }
502
+ });
503
+ } catch (error) {
504
+ res.status(500).json({
505
+ code: 500,
506
+ msg: '执行更新失败: ' + error.message,
507
+ data: null
508
+ });
509
+ }
510
+ });
511
+
269
512
  module.exports = router;
package/views/admin.ejs CHANGED
@@ -101,8 +101,8 @@
101
101
  border-radius: 8px;
102
102
  color: #e0e0e0;
103
103
  font-size: 0.95em;
104
- transition: all 0.3s;
105
104
  font-family: 'Consolas', 'Monaco', monospace;
105
+ will-change: border-color, box-shadow;
106
106
  }
107
107
 
108
108
  input[type="text"]:focus,
@@ -158,8 +158,9 @@
158
158
  font-size: 1em;
159
159
  font-weight: 600;
160
160
  cursor: pointer;
161
- transition: all 0.3s;
161
+ transition: transform 0.2s, box-shadow 0.2s;
162
162
  box-shadow: 0 4px 15px rgba(0, 255, 136, 0.3);
163
+ will-change: transform, box-shadow;
163
164
  }
164
165
 
165
166
  button:hover {
@@ -362,7 +363,7 @@
362
363
  /* 操作面板按钮组 */
363
364
  .operation-buttons {
364
365
  display: grid;
365
- grid-template-columns: 1fr 1fr;
366
+ grid-template-columns: 1fr 1fr 1fr;
366
367
  gap: 15px;
367
368
  margin-top: 20px;
368
369
  }
@@ -371,6 +372,31 @@
371
372
  width: 100%;
372
373
  height: 60px;
373
374
  font-size: 1em;
375
+ position: relative;
376
+ }
377
+
378
+ /* 红点提示 */
379
+ .update-badge {
380
+ position: absolute;
381
+ top: 8px;
382
+ right: 8px;
383
+ width: 10px;
384
+ height: 10px;
385
+ background: #f44336;
386
+ border-radius: 50%;
387
+ box-shadow: 0 0 0 2px #ff0000, 0 0 10px rgba(244, 67, 54, 0.6);
388
+ animation: pulse-badge 2s infinite;
389
+ }
390
+
391
+ @keyframes pulse-badge {
392
+ 0%, 100% {
393
+ transform: scale(1);
394
+ opacity: 1;
395
+ }
396
+ 50% {
397
+ transform: scale(1.2);
398
+ opacity: 0.8;
399
+ }
374
400
  }
375
401
 
376
402
  /* 弹窗样式 */
@@ -448,7 +474,8 @@
448
474
  align-items: center;
449
475
  justify-content: center;
450
476
  border-radius: 8px;
451
- transition: all 0.3s;
477
+ transition: background-color 0.2s, color 0.2s;
478
+ will-change: background-color, color;
452
479
  }
453
480
 
454
481
  .close-btn:hover {
@@ -470,6 +497,8 @@
470
497
  border-radius: 8px;
471
498
  color: #e0e0e0;
472
499
  font-size: 0.95em;
500
+ transition: border-color 0.2s, box-shadow 0.2s;
501
+ will-change: border-color, box-shadow;
473
502
  }
474
503
 
475
504
  .search-box input:focus {
@@ -618,6 +647,9 @@
618
647
  <button onclick="executeUpdate()">
619
648
  更新证书
620
649
  </button>
650
+ <button id="updateBtn" onclick="checkForUpdates()">
651
+ 软件更新
652
+ </button>
621
653
  </div>
622
654
 
623
655
  <div style="margin-top: 30px; padding-top: 30px; border-top: 1px solid rgba(255, 255, 255, 0.05);">
@@ -660,7 +692,7 @@
660
692
 
661
693
  <!-- 本地证书列表 -->
662
694
  <div class="card full-width">
663
- <h2 class="card-title">本地证书列表</h2>
695
+ <h2 class="card-title">本地证书列表 <span id="certCount" style="color: #888; font-size: 0.8em; font-weight: normal;">(0)</span></h2>
664
696
  <div class="search-box">
665
697
  <input type="text" id="certSearch" placeholder="🔍 搜索证书域名...">
666
698
  </div>
@@ -742,7 +774,10 @@
742
774
  </div>
743
775
  <div class="form-group">
744
776
  <label for="nginxDir">Nginx路径</label>
745
- <input type="text" id="nginxDir" name="nginxDir" placeholder="请输入Nginx路径">
777
+ <div style="display: flex; gap: 10px; align-items: flex-start;">
778
+ <input type="text" id="nginxDir" name="nginxDir" placeholder="请输入Nginx路径" style="flex: 1;">
779
+ <button type="button" onclick="testNginxPath()" style="flex: none; padding: 12px 20px; min-width: auto;">测试</button>
780
+ </div>
746
781
  <div class="hint">Nginx可执行文件路径(留空则不重载)</div>
747
782
  </div>
748
783
  <div class="form-group">
@@ -883,6 +918,7 @@
883
918
  connectWebSocket();
884
919
  startScheduleInfoSync();
885
920
  updateCallbackApiUrl();
921
+ checkForUpdates(true); // 静默检查更新
886
922
 
887
923
  // 搜索框事件
888
924
  document.getElementById('certSearch').addEventListener('input', filterCerts);
@@ -943,6 +979,39 @@
943
979
  document.getElementById('configModal').classList.remove('show');
944
980
  }
945
981
 
982
+ // 测试Nginx路径
983
+ async function testNginxPath() {
984
+ const nginxPath = document.getElementById('nginxDir').value.trim();
985
+ if (!nginxPath) {
986
+ showAlert('提示', '请先输入Nginx路径', 'warning');
987
+ return;
988
+ }
989
+
990
+ addLog('→ 开始测试 Nginx 路径...', 'info');
991
+
992
+ try {
993
+ const response = await authenticatedFetch('/admin/api/test-nginx', {
994
+ method: 'POST',
995
+ headers: { 'Content-Type': 'application/json' },
996
+ body: JSON.stringify({ nginxPath })
997
+ });
998
+ const result = await response.json();
999
+
1000
+ if (result.code === 0) {
1001
+ addLog('✓ Nginx 路径测试成功', 'success');
1002
+ showAlert('测试成功', result.msg || 'Nginx 配置测试通过', 'success');
1003
+ } else {
1004
+ addLog('✗ Nginx 路径测试失败: ' + result.msg, 'error');
1005
+ showAlert('测试失败', result.msg, 'error');
1006
+ }
1007
+ } catch (error) {
1008
+ if (error.message !== '未设置密码' && error.message !== '密码验证失败') {
1009
+ addLog('✗ Nginx 路径测试失败: ' + error.message, 'error');
1010
+ showAlert('测试失败', error.message, 'error');
1011
+ }
1012
+ }
1013
+ }
1014
+
946
1015
  // 加载配置
947
1016
  async function loadConfig() {
948
1017
  try {
@@ -1028,6 +1097,9 @@
1028
1097
  function renderCerts(certs) {
1029
1098
  const container = document.getElementById('certListContainer');
1030
1099
 
1100
+ // 更新证书数量显示
1101
+ document.getElementById('certCount').textContent = `(${certs.length})`;
1102
+
1031
1103
  if (certs.length === 0) {
1032
1104
  container.innerHTML = '<div class="empty-state">暂无证书文件</div>';
1033
1105
  return;
@@ -1041,6 +1113,7 @@
1041
1113
  html += '<th>过期时间</th>';
1042
1114
  html += '<th>剩余天数</th>';
1043
1115
  html += '<th>状态</th>';
1116
+ html += '<th style="width: 100px;">操作</th>';
1044
1117
  html += '</tr></thead><tbody>';
1045
1118
 
1046
1119
  sortedCerts.forEach(cert => {
@@ -1056,6 +1129,7 @@
1056
1129
  html += `<td>${formatDate(cert.notAfter)}</td>`;
1057
1130
  html += `<td>${cert.remainingDays} 天</td>`;
1058
1131
  html += `<td><span class="status-badge ${statusClass}">${statusText}</span></td>`;
1132
+ html += `<td><button onclick="downloadCert('${escapeHtml(cert.fileName)}')" style="padding: 6px 12px; font-size: 0.85em;">下载</button></td>`;
1059
1133
  html += '</tr>';
1060
1134
  });
1061
1135
 
@@ -1063,6 +1137,59 @@
1063
1137
  container.innerHTML = html;
1064
1138
  }
1065
1139
 
1140
+ // 下载证书
1141
+ async function downloadCert(fileName) {
1142
+ try {
1143
+ addLog(`→ 开始下载证书: ${fileName}`, 'info');
1144
+
1145
+ const password = getStoredPassword();
1146
+ const response = await fetch(`/admin/api/cert/download/${encodeURIComponent(fileName)}`, {
1147
+ method: 'GET',
1148
+ headers: {
1149
+ 'X-Password': password
1150
+ }
1151
+ });
1152
+
1153
+ if (response.status === 401) {
1154
+ clearPassword();
1155
+ showPasswordModal();
1156
+ addLog('✗ 口令验证失败,请重新输入', 'error');
1157
+ return;
1158
+ }
1159
+
1160
+ if (!response.ok) {
1161
+ const result = await response.json();
1162
+ throw new Error(result.msg || '下载失败');
1163
+ }
1164
+
1165
+ // 获取文件名
1166
+ const contentDisposition = response.headers.get('Content-Disposition');
1167
+ let downloadFileName = `${fileName}.zip`;
1168
+ if (contentDisposition) {
1169
+ const match = contentDisposition.match(/filename[*]?=['"]?([^'";\n]+)['"]?/);
1170
+ if (match && match[1]) {
1171
+ downloadFileName = match[1];
1172
+ }
1173
+ }
1174
+
1175
+ // 下载文件
1176
+ const blob = await response.blob();
1177
+ const url = window.URL.createObjectURL(blob);
1178
+ const a = document.createElement('a');
1179
+ a.href = url;
1180
+ a.download = downloadFileName;
1181
+ document.body.appendChild(a);
1182
+ a.click();
1183
+ window.URL.revokeObjectURL(url);
1184
+ document.body.removeChild(a);
1185
+
1186
+ addLog(`✓ 证书下载成功: ${fileName}`, 'success');
1187
+ } catch (error) {
1188
+ addLog(`✗ 证书下载失败: ${error.message}`, 'error');
1189
+ showAlert('下载失败', error.message, 'error');
1190
+ }
1191
+ }
1192
+
1066
1193
  // 过滤证书
1067
1194
  function filterCerts() {
1068
1195
  const keyword = document.getElementById('certSearch').value.toLowerCase();
@@ -1108,6 +1235,98 @@
1108
1235
  }
1109
1236
  }
1110
1237
 
1238
+ // 检查程序更新
1239
+ async function checkForUpdates(silent = false) {
1240
+ if (!silent) {
1241
+ addLog('→ 正在检查更新...', 'info');
1242
+ }
1243
+
1244
+ try {
1245
+ const response = await authenticatedFetch('/admin/check-update', {
1246
+ method: 'POST'
1247
+ });
1248
+ const result = await response.json();
1249
+
1250
+ if (result.code === 0) {
1251
+ const { currentVersion, remoteVersion, hasUpdate } = result.data;
1252
+
1253
+ if (hasUpdate) {
1254
+ // 显示红点提示
1255
+ const updateBtn = document.getElementById('updateBtn');
1256
+ if (updateBtn && !updateBtn.querySelector('.update-badge')) {
1257
+ const badge = document.createElement('span');
1258
+ badge.className = 'update-badge';
1259
+ updateBtn.appendChild(badge);
1260
+ }
1261
+
1262
+ if (!silent) {
1263
+ addLog(`✓ 发现新版本: ${remoteVersion} (当前版本: ${currentVersion})`, 'success');
1264
+ const confirmed = await showConfirm(
1265
+ `发现新版本 ${remoteVersion}`,
1266
+ `当前版本: ${currentVersion}\n新版本: ${remoteVersion}\n\n确定要立即更新吗?更新完成后程序将自动重启。`
1267
+ );
1268
+
1269
+ if (confirmed) {
1270
+ await performUpdate();
1271
+ }
1272
+ }
1273
+ } else {
1274
+ // 移除红点(如果有)
1275
+ const updateBtn = document.getElementById('updateBtn');
1276
+ if (updateBtn) {
1277
+ const badge = updateBtn.querySelector('.update-badge');
1278
+ if (badge) {
1279
+ badge.remove();
1280
+ }
1281
+ }
1282
+
1283
+ if (!silent) {
1284
+ addLog(`✓ 已是最新版本: ${currentVersion}`, 'success');
1285
+ showAlert('检查更新', `当前已是最新版本 ${currentVersion}`, 'success');
1286
+ }
1287
+ }
1288
+ } else {
1289
+ if (!silent) {
1290
+ addLog('✗ 检查更新失败: ' + result.msg, 'error');
1291
+ showAlert('检查更新失败', result.msg, 'error');
1292
+ }
1293
+ }
1294
+ } catch (error) {
1295
+ if (error.message !== '未设置密码' && error.message !== '密码验证失败') {
1296
+ if (!silent) {
1297
+ addLog('✗ 检查更新失败: ' + error.message, 'error');
1298
+ showAlert('检查更新失败', error.message, 'error');
1299
+ }
1300
+ }
1301
+ }
1302
+ }
1303
+
1304
+ // 执行更新
1305
+ async function performUpdate() {
1306
+ addLog('→ 开始执行更新...', 'info');
1307
+
1308
+ try {
1309
+ const response = await authenticatedFetch('/admin/execute-update', {
1310
+ method: 'POST'
1311
+ });
1312
+ const result = await response.json();
1313
+
1314
+ if (result.code === 0) {
1315
+ addLog('✓ 更新命令已执行,程序将在更新完成后自动重启', 'success');
1316
+ addLog('→ 请稍等片刻,更新完成后刷新页面即可', 'info');
1317
+ showAlert('更新中', '更新命令已执行,程序将在更新完成后自动重启。请稍等片刻后刷新页面。', 'success');
1318
+ } else {
1319
+ addLog('✗ 执行更新失败: ' + result.msg, 'error');
1320
+ showAlert('执行更新失败', result.msg, 'error');
1321
+ }
1322
+ } catch (error) {
1323
+ if (error.message !== '未设置密码' && error.message !== '密码验证失败') {
1324
+ addLog('✗ 执行更新失败: ' + error.message, 'error');
1325
+ showAlert('执行更新失败', error.message, 'error');
1326
+ }
1327
+ }
1328
+ }
1329
+
1111
1330
  // 同步定时任务信息
1112
1331
  function startScheduleInfoSync() {
1113
1332
  // 立即同步一次
package/views/error.ejs CHANGED
@@ -1,3 +1,3 @@
1
- <h1><%= message %></h1>
2
- <h2><%= error.status %></h2>
3
- <pre><%= error.stack %></pre>
1
+ <h1><%= message %></h1>
2
+ <h2><%= error.status %></h2>
3
+ <pre><%= error.stack %></pre>
package/views/index.ejs CHANGED
@@ -1,10 +1,10 @@
1
- <!DOCTYPE html>
2
- <html>
3
- <head>
4
- <title><%= title %></title>
5
- <link rel='stylesheet' href='/stylesheets/style.css' />
6
- </head>
7
- <body>
8
- <h1><%= title %></h1>
9
- </body>
10
- </html>
1
+ <!DOCTYPE html>
2
+ <html>
3
+ <head>
4
+ <title><%= title %></title>
5
+ <link rel='stylesheet' href='/stylesheets/style.css' />
6
+ </head>
7
+ <body>
8
+ <h1><%= title %></h1>
9
+ </body>
10
+ </html>