vk-ssl-auto-deploy 0.8.5 → 0.8.7

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "vk-ssl-auto-deploy",
3
- "version": "0.8.5",
3
+ "version": "0.8.7",
4
4
  "description": "SSL证书自动部署工具 - 提供HTTP API接口,支持证书文件自动上传和部署",
5
5
  "main": "app.js",
6
6
  "scripts": {
package/routes/admin.js CHANGED
@@ -410,4 +410,95 @@ router.get('/api/cert/download/:fileName', verifyPassword, (req, res) => {
410
410
  }
411
411
  });
412
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, instructions;
474
+
475
+ if (platform === 'win32') {
476
+ // Windows 系统
477
+ command = 'powershell -ExecutionPolicy Bypass -Command "iwr -useb https://gitee.com/vk1688/vk-ssl-auto-deploy/raw/master/install.ps1 | iex"';
478
+ instructions = '请以管理员身份打开 PowerShell,然后执行以下命令:';
479
+ } else {
480
+ // Linux/Unix 系统
481
+ command = 'curl -fsSL https://gitee.com/vk1688/vk-ssl-auto-deploy/raw/master/install.sh | sudo bash';
482
+ instructions = '请在服务器终端中执行以下命令:';
483
+ }
484
+
485
+ res.json({
486
+ code: 0,
487
+ msg: '请手动在服务器上执行更新命令',
488
+ data: {
489
+ platform: platform === 'win32' ? 'Windows' : 'Linux/Unix',
490
+ instructions,
491
+ command,
492
+ note: '更新完成后程序将自动重启'
493
+ }
494
+ });
495
+ } catch (error) {
496
+ res.status(500).json({
497
+ code: 500,
498
+ msg: '获取更新命令失败: ' + error.message,
499
+ data: null
500
+ });
501
+ }
502
+ });
503
+
413
504
  module.exports = router;
package/views/admin.ejs CHANGED
@@ -363,7 +363,7 @@
363
363
  /* 操作面板按钮组 */
364
364
  .operation-buttons {
365
365
  display: grid;
366
- grid-template-columns: 1fr 1fr;
366
+ grid-template-columns: 1fr 1fr 1fr;
367
367
  gap: 15px;
368
368
  margin-top: 20px;
369
369
  }
@@ -372,6 +372,31 @@
372
372
  width: 100%;
373
373
  height: 60px;
374
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
+ }
375
400
  }
376
401
 
377
402
  /* 弹窗样式 */
@@ -569,6 +594,28 @@
569
594
  }
570
595
  }
571
596
 
597
+ @keyframes slideIn {
598
+ from {
599
+ opacity: 0;
600
+ transform: translateX(100px);
601
+ }
602
+ to {
603
+ opacity: 1;
604
+ transform: translateX(0);
605
+ }
606
+ }
607
+
608
+ @keyframes slideOut {
609
+ from {
610
+ opacity: 1;
611
+ transform: translateX(0);
612
+ }
613
+ to {
614
+ opacity: 0;
615
+ transform: translateX(100px);
616
+ }
617
+ }
618
+
572
619
  .dialog-title {
573
620
  font-size: 1.5em;
574
621
  color: #00ff88;
@@ -622,6 +669,9 @@
622
669
  <button onclick="executeUpdate()">
623
670
  更新证书
624
671
  </button>
672
+ <button id="updateBtn" onclick="checkForUpdates()">
673
+ 软件更新
674
+ </button>
625
675
  </div>
626
676
 
627
677
  <div style="margin-top: 30px; padding-top: 30px; border-top: 1px solid rgba(255, 255, 255, 0.05);">
@@ -890,6 +940,7 @@
890
940
  connectWebSocket();
891
941
  startScheduleInfoSync();
892
942
  updateCallbackApiUrl();
943
+ checkForUpdates(true); // 静默检查更新
893
944
 
894
945
  // 搜索框事件
895
946
  document.getElementById('certSearch').addEventListener('input', filterCerts);
@@ -1206,6 +1257,220 @@
1206
1257
  }
1207
1258
  }
1208
1259
 
1260
+ // 检查程序更新
1261
+ async function checkForUpdates(silent = false) {
1262
+ if (!silent) {
1263
+ addLog('→ 正在检查更新...', 'info');
1264
+ }
1265
+
1266
+ try {
1267
+ const response = await authenticatedFetch('/admin/check-update', {
1268
+ method: 'POST'
1269
+ });
1270
+ const result = await response.json();
1271
+
1272
+ if (result.code === 0) {
1273
+ const { currentVersion, remoteVersion, hasUpdate } = result.data;
1274
+
1275
+ if (hasUpdate) {
1276
+ // 显示红点提示
1277
+ const updateBtn = document.getElementById('updateBtn');
1278
+ if (updateBtn && !updateBtn.querySelector('.update-badge')) {
1279
+ const badge = document.createElement('span');
1280
+ badge.className = 'update-badge';
1281
+ updateBtn.appendChild(badge);
1282
+ }
1283
+
1284
+ if (!silent) {
1285
+ addLog(`✓ 发现新版本: ${remoteVersion} (当前版本: ${currentVersion})`, 'success');
1286
+ // 直接显示更新命令弹窗
1287
+ await performUpdate(currentVersion, remoteVersion);
1288
+ }
1289
+ } else {
1290
+ // 移除红点(如果有)
1291
+ const updateBtn = document.getElementById('updateBtn');
1292
+ if (updateBtn) {
1293
+ const badge = updateBtn.querySelector('.update-badge');
1294
+ if (badge) {
1295
+ badge.remove();
1296
+ }
1297
+ }
1298
+
1299
+ if (!silent) {
1300
+ addLog(`✓ 已是最新版本: ${currentVersion}`, 'success');
1301
+ showAlert('检查更新', `当前已是最新版本 ${currentVersion}`, 'success');
1302
+ }
1303
+ }
1304
+ } else {
1305
+ if (!silent) {
1306
+ addLog('✗ 检查更新失败: ' + result.msg, 'error');
1307
+ showAlert('检查更新失败', result.msg, 'error');
1308
+ }
1309
+ }
1310
+ } catch (error) {
1311
+ if (error.message !== '未设置密码' && error.message !== '密码验证失败') {
1312
+ if (!silent) {
1313
+ addLog('✗ 检查更新失败: ' + error.message, 'error');
1314
+ showAlert('检查更新失败', error.message, 'error');
1315
+ }
1316
+ }
1317
+ }
1318
+ }
1319
+
1320
+ // 执行更新
1321
+ async function performUpdate(currentVersion = '', remoteVersion = '') {
1322
+ addLog('→ 获取更新命令...', 'info');
1323
+
1324
+ try {
1325
+ const response = await authenticatedFetch('/admin/execute-update', {
1326
+ method: 'POST'
1327
+ });
1328
+ const result = await response.json();
1329
+
1330
+ if (result.code === 0) {
1331
+ const { platform, instructions, command, note } = result.data;
1332
+
1333
+ addLog('✓ 已获取更新命令', 'success');
1334
+
1335
+ // 显示更新命令弹窗
1336
+ showUpdateCommandDialog(instructions, command, note, currentVersion, remoteVersion);
1337
+ } else {
1338
+ addLog('✗ 获取更新命令失败: ' + result.msg, 'error');
1339
+ showAlert('获取更新命令失败', result.msg, 'error');
1340
+ }
1341
+ } catch (error) {
1342
+ if (error.message !== '未设置密码' && error.message !== '密码验证失败') {
1343
+ addLog('✗ 获取更新命令失败: ' + error.message, 'error');
1344
+ showAlert('获取更新命令失败', error.message, 'error');
1345
+ }
1346
+ }
1347
+ }
1348
+
1349
+ // 显示更新命令对话框
1350
+ function showUpdateCommandDialog(instructions, command, note, currentVersion = '', remoteVersion = '') {
1351
+ const dialog = document.getElementById('customDialog');
1352
+ const dialogTitle = document.getElementById('dialogTitleText');
1353
+ const dialogMessage = document.getElementById('dialogMessage');
1354
+ const dialogButtons = document.getElementById('dialogButtons');
1355
+ const dialogIcon = document.getElementById('dialogIcon');
1356
+
1357
+ dialogIcon.textContent = '🔄';
1358
+ dialogTitle.textContent = '手动执行更新';
1359
+
1360
+ // 版本信息HTML(如果有版本号才显示)
1361
+ let versionInfo = '';
1362
+ if (currentVersion && remoteVersion) {
1363
+ versionInfo = `
1364
+ <div style="margin-bottom: 20px; padding: 12px; background: rgba(0, 255, 136, 0.1); border-radius: 8px; border: 1px solid rgba(0, 255, 136, 0.3);">
1365
+ <div style="display: flex; justify-content: space-between; align-items: center;">
1366
+ <div>
1367
+ <span style="color: #888; font-size: 14px;">当前版本:</span>
1368
+ <span style="color: #00ff88; font-weight: bold;">${currentVersion}</span>
1369
+ </div>
1370
+ <div style="color: #00ff88; font-size: 20px;">→</div>
1371
+ <div>
1372
+ <span style="color: #888; font-size: 14px;">最新版本:</span>
1373
+ <span style="color: #00ff88; font-weight: bold;">${remoteVersion}</span>
1374
+ </div>
1375
+ </div>
1376
+ </div>
1377
+ `;
1378
+ }
1379
+
1380
+ // 创建自定义消息内容
1381
+ dialogMessage.innerHTML = `
1382
+ <div style="text-align: left; line-height: 1.8;">
1383
+ ${versionInfo}
1384
+ <p style="margin-bottom: 15px; color: #aaa;">${instructions}</p>
1385
+ <div style="position: relative; background: rgba(0, 0, 0, 0.5); border: 1px solid rgba(0, 255, 136, 0.3); border-radius: 8px; padding: 15px 15px 50px 15px; margin: 15px 0; font-family: 'Consolas', 'Monaco', monospace;">
1386
+ <pre style="margin: 0; color: #00ff88; word-wrap: break-word; white-space: pre-wrap; font-size: 13px; line-height: 1.6;">${command}</pre>
1387
+ <button onclick="copyCommand('${command.replace(/'/g, "\\'")}')" style="position: absolute; bottom: 10px; right: 10px; padding: 8px 16px; background: rgba(0, 255, 136, 0.2); border: 1px solid rgba(0, 255, 136, 0.5); border-radius: 5px; color: #00ff88; cursor: pointer; font-size: 13px; font-weight: bold; transition: all 0.2s;" onmouseover="this.style.background='rgba(0, 255, 136, 0.3)'; this.style.transform='translateY(-2px)'; this.style.boxShadow='0 4px 12px rgba(0, 255, 136, 0.3)'" onmouseout="this.style.background='rgba(0, 255, 136, 0.2)'; this.style.transform='translateY(0)'; this.style.boxShadow='none'">📋 复制命令</button>
1388
+ </div>
1389
+ <p style="margin-top: 15px; color: #888; font-size: 14px;">💡 ${note}</p>
1390
+ </div>
1391
+ `;
1392
+
1393
+ // 创建关闭按钮
1394
+ dialogButtons.innerHTML = '';
1395
+ const closeBtn = document.createElement('button');
1396
+ closeBtn.textContent = '关闭';
1397
+ closeBtn.onclick = () => {
1398
+ dialog.classList.remove('show');
1399
+ };
1400
+ dialogButtons.appendChild(closeBtn);
1401
+
1402
+ dialog.classList.add('show');
1403
+ }
1404
+
1405
+ // 复制命令到剪贴板
1406
+ function copyCommand(command) {
1407
+ // 解码转义的单引号
1408
+ const decodedCommand = command.replace(/\\'/g, "'");
1409
+
1410
+ // 使用现代 Clipboard API
1411
+ if (navigator.clipboard && window.isSecureContext) {
1412
+ navigator.clipboard.writeText(decodedCommand).then(() => {
1413
+ addLog('✓ 命令已复制到剪贴板', 'success');
1414
+ showTempMessage('命令已复制!');
1415
+ }).catch(err => {
1416
+ console.error('复制失败:', err);
1417
+ fallbackCopyCommand(decodedCommand);
1418
+ });
1419
+ } else {
1420
+ // 降级方案
1421
+ fallbackCopyCommand(decodedCommand);
1422
+ }
1423
+ }
1424
+
1425
+ // 降级复制方法
1426
+ function fallbackCopyCommand(command) {
1427
+ const textArea = document.createElement("textarea");
1428
+ textArea.value = command;
1429
+ textArea.style.position = "fixed";
1430
+ textArea.style.left = "-999999px";
1431
+ document.body.appendChild(textArea);
1432
+ textArea.select();
1433
+
1434
+ try {
1435
+ document.execCommand('copy');
1436
+ addLog('✓ 命令已复制到剪贴板', 'success');
1437
+ showTempMessage('命令已复制!');
1438
+ } catch (err) {
1439
+ console.error('复制失败:', err);
1440
+ addLog('✗ 复制失败,请手动复制', 'error');
1441
+ }
1442
+
1443
+ document.body.removeChild(textArea);
1444
+ }
1445
+
1446
+ // 显示临时提示消息
1447
+ function showTempMessage(message) {
1448
+ const tempMsg = document.createElement('div');
1449
+ tempMsg.textContent = message;
1450
+ tempMsg.style.cssText = `
1451
+ position: fixed;
1452
+ top: 20px;
1453
+ right: 20px;
1454
+ background: rgba(0, 255, 136, 0.9);
1455
+ color: #000;
1456
+ padding: 12px 24px;
1457
+ border-radius: 8px;
1458
+ font-weight: bold;
1459
+ z-index: 10001;
1460
+ animation: slideIn 0.3s ease-out;
1461
+ box-shadow: 0 4px 12px rgba(0, 255, 136, 0.3);
1462
+ `;
1463
+
1464
+ document.body.appendChild(tempMsg);
1465
+
1466
+ setTimeout(() => {
1467
+ tempMsg.style.animation = 'slideOut 0.3s ease-in';
1468
+ setTimeout(() => {
1469
+ document.body.removeChild(tempMsg);
1470
+ }, 300);
1471
+ }, 2000);
1472
+ }
1473
+
1209
1474
  // 同步定时任务信息
1210
1475
  function startScheduleInfoSync() {
1211
1476
  // 立即同步一次