确认删除
@@ -481,12 +673,23 @@
var enabledCheckbox = document.getElementById('clicd-fw-enabled');
var defaultActionSelect = document.getElementById('clicd-fw-default-action');
var statusText = document.getElementById('clicd-fw-status-text');
+ var saveSettingsButton = document.getElementById('clicd-fw-save-settings');
+ var saveHint = document.getElementById('clicd-fw-save-hint');
+ var addModal = document.getElementById('clicd-fw-add-modal');
+ var addClose = document.getElementById('clicd-fw-add-close');
+ var addCancel = document.getElementById('clicd-fw-add-cancel');
+ var addConfirm = document.getElementById('clicd-fw-add-confirm');
+ var ruleModalTitle = document.getElementById('clicd-fw-rule-modal-title');
var deleteModal = document.getElementById('clicd-fw-delete-modal');
var deleteText = document.getElementById('clicd-fw-delete-text');
var deleteCancel = document.getElementById('clicd-fw-delete-cancel');
var deleteConfirm = document.getElementById('clicd-fw-delete-confirm');
var pendingDeleteRule = null;
var currentRules = [];
+ var ruleModalMode = 'add';
+ var editingRuleIndex = null;
+ var settingsDirty = false;
+ var busy = false;
function showMessage(type, text) {
message.className = 'clicd-fw-message' + (type === 'error' ? ' error' : '');
@@ -507,8 +710,31 @@
return panel.getAttribute('data-service-id') || '';
}
- function setBusy(busy) {
- panel.querySelectorAll('button, input, select').forEach(function (el) { el.disabled = !!busy; });
+ function updateSaveState() {
+ if (saveSettingsButton) {
+ saveSettingsButton.disabled = busy || !settingsDirty;
+ }
+ if (saveHint) {
+ saveHint.textContent = settingsDirty ? '有未保存更改,点击保存设置后生效' : '当前设置已保存';
+ }
+ }
+
+ function setBusy(value) {
+ busy = !!value;
+ panel.querySelectorAll('button, input, select').forEach(function (el) { el.disabled = busy; });
+ updateSaveState();
+ }
+
+ function markDirty(text) {
+ settingsDirty = true;
+ updateStatusText();
+ updateSaveState();
+ showMessage('success', text || '设置已修改,点击保存设置后生效');
+ }
+
+ function clearDirty() {
+ settingsDirty = false;
+ updateSaveState();
}
function escapeHtml(value) {
@@ -532,7 +758,7 @@
rulesContainer.innerHTML = '
暂无防火墙规则
';
return;
}
- rulesContainer.innerHTML = currentRules.map(function (rule, idx) {
+ var rows = currentRules.map(function (rule, idx) {
var dir = (rule.direction || 'in').toLowerCase();
var proto = (rule.protocol || 'tcp').toLowerCase();
var action = (rule.action || 'ACCEPT').toUpperCase();
@@ -540,110 +766,98 @@
var port = escapeHtml(portDisplay(rule.port));
var srcIp = escapeHtml(sourceIpDisplay(rule.source_ip));
var desc = escapeHtml(rule.description || '');
- var ruleId = escapeHtml(rule.id || '');
var enabled = rule.enabled !== false;
var enabledChecked = enabled ? 'checked' : '';
var dirClass = dir === 'in' ? 'clicd-fw-direction-in' : 'clicd-fw-direction-out';
var actionClass = 'clicd-fw-action-' + action;
- return '
' +
- '' +
- '
' +
- '' + (desc || '无说明') + '' +
- '
' +
- '
' +
- '
' +
- '
' +
- '
' +
- '
' +
- '
' +
- '
' +
- '
' +
- '
' +
- '' +
- '' +
- '
' +
- '
' +
- '
';
+ return '
' +
+ ' | ' +
+ '' + networkLabel(network) + ' | ' +
+ '' + (dir === 'in' ? '入站' : '出站') + ' | ' +
+ '' + escapeHtml(proto.toUpperCase()) + ' | ' +
+ '' + port + ' | ' +
+ '' + srcIp + ' | ' +
+ '' + (action === 'ACCEPT' ? '放行' : '拒绝') + ' | ' +
+ '' + (desc || '-') + ' | ' +
+ '' +
+ '' +
+ '' +
+ ' | ' +
+ '
';
}).join('');
+ rulesContainer.innerHTML = '
' +
+ '' +
+ '| 状态 | ' +
+ '网络 | ' +
+ '方向 | ' +
+ '协议 | ' +
+ '端口 | ' +
+ '来源/目标 IP | ' +
+ '动作 | ' +
+ '描述 | ' +
+ '操作 | ' +
+ '
' + rows + '
';
+
rulesContainer.querySelectorAll('.clicd-fw-rule-enabled').forEach(function (toggle, idx) {
toggle.addEventListener('change', function () {
var rule = currentRules[idx];
if (!rule) return;
rule.enabled = toggle.checked;
- saveFirewall();
+ markDirty('规则开关已修改,点击保存设置后生效');
});
});
}
- function getEditField(item, name) {
- return item.querySelector('[data-field="' + name + '"]');
+ function setRuleField(id, value) {
+ var field = document.getElementById(id);
+ if (field) {
+ field.value = value == null ? '' : value;
+ }
}
- function updateRuleFromFields(item, idx) {
- currentRules[idx].network = getEditField(item, 'network') ? getEditField(item, 'network').value : 'ipv4';
- currentRules[idx].direction = getEditField(item, 'direction') ? getEditField(item, 'direction').value : 'in';
- currentRules[idx].protocol = getEditField(item, 'protocol') ? getEditField(item, 'protocol').value : 'tcp';
- currentRules[idx].port = getEditField(item, 'port') ? getEditField(item, 'port').value : '';
- currentRules[idx].source_ip = getEditField(item, 'source_ip') ? getEditField(item, 'source_ip').value : '';
- currentRules[idx].action = getEditField(item, 'action') ? getEditField(item, 'action').value : 'ACCEPT';
- currentRules[idx].description = getEditField(item, 'description') ? getEditField(item, 'description').value : '';
+ function getRuleField(id, fallback) {
+ var field = document.getElementById(id);
+ if (!field) return fallback || '';
+ return field.value;
}
- function getAddRulePayload() {
+ function getRuleFormPayload(baseRule) {
+ var base = baseRule || {};
return {
- id: '',
- network: document.getElementById('clicd-fw-add-network').value,
- direction: document.getElementById('clicd-fw-add-direction').value,
- protocol: document.getElementById('clicd-fw-add-protocol').value,
- port: document.getElementById('clicd-fw-add-port').value,
- source_ip: document.getElementById('clicd-fw-add-source-ip').value,
- action: document.getElementById('clicd-fw-add-action').value,
- description: document.getElementById('clicd-fw-add-desc').value,
- enabled: true
+ id: base.id || '',
+ network: getRuleField('clicd-fw-add-network', 'ipv4'),
+ direction: getRuleField('clicd-fw-add-direction', 'in'),
+ protocol: getRuleField('clicd-fw-add-protocol', 'tcp'),
+ port: getRuleField('clicd-fw-add-port', ''),
+ source_ip: getRuleField('clicd-fw-add-source-ip', ''),
+ action: getRuleField('clicd-fw-add-action', 'DROP'),
+ description: getRuleField('clicd-fw-add-desc', ''),
+ enabled: baseRule ? base.enabled !== false : true
};
}
function clearAddForm() {
- document.getElementById('clicd-fw-add-network').value = 'ipv4';
- document.getElementById('clicd-fw-add-port').value = '';
- document.getElementById('clicd-fw-add-source-ip').value = '';
- document.getElementById('clicd-fw-add-action').value = 'ACCEPT';
- document.getElementById('clicd-fw-add-desc').value = '';
+ setRuleField('clicd-fw-add-network', 'ipv4');
+ setRuleField('clicd-fw-add-direction', 'in');
+ setRuleField('clicd-fw-add-protocol', 'tcp');
+ setRuleField('clicd-fw-add-port', '');
+ setRuleField('clicd-fw-add-source-ip', '');
+ setRuleField('clicd-fw-add-action', 'DROP');
+ setRuleField('clicd-fw-add-desc', '');
+ }
+
+ function fillRuleForm(rule) {
+ var item = rule || {};
+ setRuleField('clicd-fw-add-network', item.network || 'ipv4');
+ setRuleField('clicd-fw-add-direction', item.direction || 'in');
+ setRuleField('clicd-fw-add-protocol', item.protocol || 'tcp');
+ setRuleField('clicd-fw-add-port', item.port || '');
+ setRuleField('clicd-fw-add-source-ip', item.source_ip || '');
+ setRuleField('clicd-fw-add-action', item.action || 'DROP');
+ setRuleField('clicd-fw-add-desc', item.description || '');
}
- // ── FIX: 发 JSON body,走 firewallajax 路由 ──
function saveFirewall() {
var enabled = enabledCheckbox.checked;
var defaultAction = defaultActionSelect.value;
@@ -661,22 +875,22 @@
};
});
+ var body = new URLSearchParams();
+ body.set('id', serviceId());
+ body.set('func', 'firewallUpdate');
+ body.set('enabled', enabled ? 'true' : 'false');
+ body.set('default_action', defaultAction);
+ body.set('rules', JSON.stringify(rules));
+
setBusy(true);
fetch(endpoint(), {
method: 'POST',
headers: {
- 'Content-Type': 'application/json; charset=UTF-8',
+ 'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8',
'Authorization': 'JWT {$Think.get.jwt}'
},
credentials: 'same-origin',
- body: JSON.stringify({
- id: serviceId(),
- func: 'firewallajax',
- action: 'update',
- enabled: enabled,
- default_action: defaultAction,
- rules: rules
- })
+ body: body.toString()
})
.then(function (res) { return res.text(); })
.then(function (text) {
@@ -685,10 +899,17 @@
showDebug((data.data && data.data.debug) || data.debug || data);
if (data.status === 200 || data.status === 'success') {
showMessage('success', data.msg || '防火墙设置已更新');
+ if (data.data && Object.prototype.hasOwnProperty.call(data.data, 'enabled')) {
+ enabledCheckbox.checked = data.data.enabled === true || data.data.enabled === 'true' || data.data.enabled === 1;
+ }
+ if (data.data && data.data.default_action && defaultActionSelect) {
+ defaultActionSelect.value = data.data.default_action;
+ }
if (data.data && Array.isArray(data.data.rules)) {
currentRules = data.data.rules;
renderRules(currentRules);
}
+ clearDirty();
updateStatusText();
} else {
showMessage('error', data.msg || '更新失败');
@@ -703,21 +924,20 @@
});
}
- // ── FIX: 发 JSON body,走 firewallajax 路由 ──
function loadFirewall() {
+ var body = new URLSearchParams();
+ body.set('id', serviceId());
+ body.set('func', 'firewallList');
+
setBusy(true);
fetch(endpoint(), {
method: 'POST',
headers: {
- 'Content-Type': 'application/json; charset=UTF-8',
+ 'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8',
'Authorization': 'JWT {$Think.get.jwt}'
},
credentials: 'same-origin',
- body: JSON.stringify({
- id: serviceId(),
- func: 'firewallajax',
- action: 'list'
- })
+ body: body.toString()
})
.then(function (res) { return res.text(); })
.then(function (text) {
@@ -732,6 +952,7 @@
}
currentRules = Array.isArray(data.data.rules) ? data.data.rules : [];
renderRules(currentRules);
+ clearDirty();
updateStatusText();
}
} else {
@@ -751,9 +972,9 @@
function updateStatusText() {
if (enabledCheckbox.checked) {
- statusText.textContent = '已启用 - 默认' + (defaultActionSelect.value === 'DROP' ? '拒绝' : '放行') + '未匹配流量';
+ statusText.textContent = '已启用,未匹配规则的流量将被' + (defaultActionSelect.value === 'DROP' ? '拒绝' : '放行');
} else {
- statusText.textContent = '已禁用 - 所有流量不受限制';
+ statusText.textContent = '未启用时不接管该容器流量';
}
}
@@ -774,27 +995,51 @@
}
}
+ function openRuleModal(mode, idx) {
+ ruleModalMode = mode === 'edit' ? 'edit' : 'add';
+ editingRuleIndex = ruleModalMode === 'edit' ? idx : null;
+ if (ruleModalTitle) {
+ ruleModalTitle.textContent = ruleModalMode === 'edit' ? '编辑规则' : '添加规则';
+ }
+ if (ruleModalMode === 'edit' && currentRules[idx]) {
+ fillRuleForm(currentRules[idx]);
+ } else {
+ clearAddForm();
+ }
+ if (addModal) {
+ addModal.style.display = 'flex';
+ }
+ var portInput = document.getElementById('clicd-fw-add-port');
+ if (portInput) {
+ setTimeout(function () { portInput.focus(); }, 0);
+ }
+ }
+
+ function closeAddModal() {
+ ruleModalMode = 'add';
+ editingRuleIndex = null;
+ if (addModal) {
+ addModal.style.display = 'none';
+ }
+ }
+
panel.addEventListener('click', function (event) {
var button = event.target.closest('[data-clicd-fw-action]');
if (!button) return;
var action = button.getAttribute('data-clicd-fw-action');
- if (action === 'add-rule') {
- currentRules.push(getAddRulePayload());
- renderRules(currentRules);
- clearAddForm();
- saveFirewall();
+ if (action === 'open-add-rule') {
+ openRuleModal('add');
return;
}
- var item = button.closest('.clicd-fw-rule');
+ var item = button.closest('[data-rule-index]');
if (!item) return;
var idx = parseInt(item.getAttribute('data-rule-index'), 10);
if (isNaN(idx) || !currentRules[idx]) return;
- if (action === 'update-rule') {
- updateRuleFromFields(item, idx);
- saveFirewall();
+ if (action === 'edit-rule') {
+ openRuleModal('edit', idx);
return;
}
@@ -807,14 +1052,46 @@
enabledCheckbox.addEventListener('change', function () {
updateStatusText();
- saveFirewall();
+ markDirty('防火墙开关已修改,点击保存设置后生效');
});
defaultActionSelect.addEventListener('change', function () {
updateStatusText();
- saveFirewall();
+ markDirty('默认动作已修改,点击保存设置后生效');
});
+ if (saveSettingsButton) {
+ saveSettingsButton.addEventListener('click', saveFirewall);
+ }
+
+ if (addClose) {
+ addClose.addEventListener('click', closeAddModal);
+ }
+ if (addCancel) {
+ addCancel.addEventListener('click', closeAddModal);
+ }
+ if (addModal) {
+ addModal.addEventListener('click', function (event) {
+ if (event.target === addModal) closeAddModal();
+ });
+ }
+ if (addConfirm) {
+ addConfirm.addEventListener('click', function () {
+ if (ruleModalMode === 'edit' && editingRuleIndex !== null && currentRules[editingRuleIndex]) {
+ currentRules[editingRuleIndex] = getRuleFormPayload(currentRules[editingRuleIndex]);
+ renderRules(currentRules);
+ closeAddModal();
+ markDirty('规则已更新,点击保存设置后生效');
+ return;
+ }
+ currentRules.push(getRuleFormPayload(null));
+ renderRules(currentRules);
+ closeAddModal();
+ clearAddForm();
+ markDirty('规则已添加,点击保存设置后生效');
+ });
+ }
+
if (deleteCancel) {
deleteCancel.addEventListener('click', closeDeleteModal);
}
@@ -831,11 +1108,12 @@
if (idx >= 0 && idx < currentRules.length) {
currentRules.splice(idx, 1);
renderRules(currentRules);
- saveFirewall();
+ markDirty('规则已删除,点击保存设置后生效');
}
});
}
+ updateSaveState();
loadFirewall();
})();
-
\ No newline at end of file
+
From 2ed42992ed131b1419df6a04c27993a047c30385 Mon Sep 17 00:00:00 2001
From: Meng Meng
Date: Sat, 13 Jun 2026 21:04:10 +0800
Subject: [PATCH 3/4] Potential fix for code scanning alert no. 29: DOM text
reinterpreted as HTML
Co-authored-by: Copilot Autofix powered by AI <62310815+github-advanced-security[bot]@users.noreply.github.com>
---
Mofang/templates/firewall.html | 3 ++-
1 file changed, 2 insertions(+), 1 deletion(-)
diff --git a/Mofang/templates/firewall.html b/Mofang/templates/firewall.html
index df836ba..1375ade 100644
--- a/Mofang/templates/firewall.html
+++ b/Mofang/templates/firewall.html
@@ -761,7 +761,8 @@
var rows = currentRules.map(function (rule, idx) {
var dir = (rule.direction || 'in').toLowerCase();
var proto = (rule.protocol || 'tcp').toLowerCase();
- var action = (rule.action || 'ACCEPT').toUpperCase();
+ var rawAction = (rule.action || 'ACCEPT').toUpperCase();
+ var action = rawAction === 'ACCEPT' ? 'ACCEPT' : 'DROP';
var network = (rule.network || 'ipv4').toLowerCase();
var port = escapeHtml(portDisplay(rule.port));
var srcIp = escapeHtml(sourceIpDisplay(rule.source_ip));
From a54e03b9242674ce8bf89521a7b15fb612ca16fa Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?=E6=AC=A2?= <2711452309@qq.com>
Date: Sun, 14 Jun 2026 03:15:02 +0800
Subject: [PATCH 4/4] =?UTF-8?q?=E6=9B=B4=E6=96=B0=E4=B8=80=E9=94=AE?=
=?UTF-8?q?=E5=AE=89=E8=A3=85certbot=E8=84=9A=E6=9C=AC+=E5=A2=9E=E5=8A=A0?=
=?UTF-8?q?=E6=9B=B4=E5=AE=8C=E6=95=B4=E7=9A=84webssh=E9=80=BB=E8=BE=91?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
Mofang/clicd.php | 14 ++++++-
Mofang/handlers/webssh.php | 19 ++++++++-
install-certbot.sh | 80 ++++++++++++++++++++++++++++++++++++++
3 files changed, 109 insertions(+), 4 deletions(-)
create mode 100644 install-certbot.sh
diff --git a/Mofang/clicd.php b/Mofang/clicd.php
index db09a36..353f912 100644
--- a/Mofang/clicd.php
+++ b/Mofang/clicd.php
@@ -40,7 +40,7 @@ function clicd_MetaData()
'DisplayName' => 'CLICD 对接模块 by 欢-Huan and ChatGPT 5.5 and DeepSeek V4',
'APIVersion' => '1.1',
'HelpDoc' => 'https://github.com/MengMengCode/CLICD',
- 'version' => '1.0.10',
+ 'version' => '1.0.11',
];
}
@@ -365,7 +365,10 @@ function clicd_webssh_url($params, $ticket, $containerName)
$host = parse_url($baseUrl, PHP_URL_HOST);
$port = parse_url($baseUrl, PHP_URL_PORT);
$wsBase = $scheme . '://' . $host . ($port ? ':' . $port : '');
- $wsUrl = $wsBase . '/api/ssh?container=' . rawurlencode((string)$containerName);
+ $wsUrl = $wsBase
+ . '/api/ssh?container=' . rawurlencode((string)$containerName)
+ . '&container_name=' . rawurlencode((string)$containerName)
+ . '&ticket=' . rawurlencode((string)$ticket);
$siteScheme = (!empty($_SERVER['HTTPS']) && $_SERVER['HTTPS'] !== 'off') ? 'https' : 'http';
$siteHost = $_SERVER['HTTP_HOST'] ?? '';
@@ -374,6 +377,7 @@ function clicd_webssh_url($params, $ticket, $containerName)
return $handler
. '?ws=' . rawurlencode($wsUrl)
. '&protocol=' . rawurlencode('clicd-ticket.' . (string)$ticket)
+ . '&ticket=' . rawurlencode((string)$ticket)
. '&container=' . rawurlencode((string)$containerName);
}
@@ -1419,7 +1423,13 @@ function clicd_ClientButton($params)
function clicd_webssh($params)
{
+ $container = [];
$containerName = clicd_container_name($params);
+ clicd_container_api_id($params, $container);
+ if (!empty($container['name'])) {
+ $containerName = (string)$container['name'];
+ }
+
$res = clicd_request($params, '/api/v1/ssh-ticket', ['container_name' => $containerName], 'POST', 30);
if (!clicd_success($res)) {
return ['status' => 'error', 'msg' => clicd_message($res, 'WebSSH ticket create failed')];
diff --git a/Mofang/handlers/webssh.php b/Mofang/handlers/webssh.php
index 3be35c7..510308a 100644
--- a/Mofang/handlers/webssh.php
+++ b/Mofang/handlers/webssh.php
@@ -2,8 +2,13 @@
$ws = isset($_GET['ws']) ? (string)$_GET['ws'] : (isset($_GET['amp;ws']) ? (string)$_GET['amp;ws'] : '');
$protocol = isset($_GET['protocol']) ? (string)$_GET['protocol'] : (isset($_GET['amp;protocol']) ? (string)$_GET['amp;protocol'] : '');
$container = isset($_GET['container']) ? (string)$_GET['container'] : (isset($_GET['amp;container']) ? (string)$_GET['amp;container'] : '');
+$ticket = isset($_GET['ticket']) ? (string)$_GET['ticket'] : (isset($_GET['amp;ticket']) ? (string)$_GET['amp;ticket'] : '');
-if ($ws === '' || $protocol === '') {
+if ($protocol === '' && $ticket !== '') {
+ $protocol = 'clicd-ticket.' . $ticket;
+}
+
+if ($ws === '') {
http_response_code(400);
header('Content-Type: text/plain; charset=utf-8');
echo "Missing WebSSH parameters\n";
@@ -64,6 +69,7 @@ if ($ws === '' || $protocol === '') {
(function(){
var wsUrl = ;
var protocol = ;
+ var ticket = ;
var term = document.getElementById('term');
var state = document.getElementById('state');
var modeSelect = document.getElementById('send-mode');
@@ -189,8 +195,17 @@ if ($ws === '' || $protocol === '') {
iostat.textContent = 'S' + sentCount + ' R' + recvCount + ' ' + stateText;
}
+ function websocketProtocolValue(value) {
+ value = String(value || '');
+ return /^[!#$%&'*+\-.^_`|~0-9A-Za-z]+$/.test(value) ? value : '';
+ }
+
try {
- socket = new WebSocket(wsUrl, protocol);
+ var protocolValue = websocketProtocolValue(protocol);
+ if (!protocolValue && ticket) {
+ append('[WebSSH] 票据已通过 URL 参数传递,当前浏览器不会发送子协议。\n');
+ }
+ socket = protocolValue ? new WebSocket(wsUrl, protocolValue) : new WebSocket(wsUrl);
socket.binaryType = 'arraybuffer';
} catch (e) {
setState('err', '\nWebSocket 创建失败:' + e.message + '\n');
diff --git a/install-certbot.sh b/install-certbot.sh
new file mode 100644
index 0000000..71c6a73
--- /dev/null
+++ b/install-certbot.sh
@@ -0,0 +1,80 @@
+#!/usr/bin/env bash
+set -e
+
+echo "=============================="
+echo " Certbot (Snap) Auto Installer"
+echo "=============================="
+
+# 检测系统
+if [ -f /etc/os-release ]; then
+ . /etc/os-release
+ OS=$ID
+ VER=$VERSION_ID
+else
+ echo "无法识别系统版本"
+ exit 1
+fi
+
+echo "检测到系统: $OS"
+
+install_snap_debian() {
+ apt update -y
+ apt install -y snapd
+ systemctl enable --now snapd.socket || true
+
+ # 修复 snap 路径
+ ln -sf /var/lib/snapd/snap /snap
+
+ # 安装 certbot
+ snap install --classic certbot
+
+ # 软链
+ ln -sf /snap/bin/certbot /usr/bin/certbot
+}
+
+install_snap_rhel() {
+ # 启用 EPEL(部分系统需要)
+ if command -v dnf >/dev/null 2>&1; then
+ dnf install -y epel-release || true
+ dnf install -y snapd
+ systemctl enable --now snapd.socket || true
+ else
+ yum install -y epel-release || true
+ yum install -y snapd
+ systemctl enable --now snapd.socket || true
+ fi
+
+ # snap 经典路径
+ ln -sf /var/lib/snapd/snap /snap
+
+ # 安装 certbot
+ snap install --classic certbot
+
+ # 软链
+ ln -sf /snap/bin/certbot /usr/bin/certbot
+}
+
+case "$OS" in
+ ubuntu|debian)
+ install_snap_debian
+ ;;
+ centos|rhel|almalinux|rocky)
+ install_snap_rhel
+ ;;
+ fedora)
+ dnf install -y snapd
+ systemctl enable --now snapd.socket || true
+ ln -sf /var/lib/snapd/snap /snap
+ snap install --classic certbot
+ ln -sf /snap/bin/certbot /usr/bin/certbot
+ ;;
+ *)
+ echo "不支持的系统: $OS"
+ exit 1
+ ;;
+esac
+
+echo "=============================="
+echo "安装完成!验证版本:"
+certbot --version || true
+echo "=============================="
\ No newline at end of file