diff --git a/astrbot/dashboard/routes/config.py b/astrbot/dashboard/routes/config.py index bcd7e075c7..2d4cfe006c 100644 --- a/astrbot/dashboard/routes/config.py +++ b/astrbot/dashboard/routes/config.py @@ -206,6 +206,56 @@ def validate(data: dict, metadata: dict = schema, path="") -> None: return errors, data +def validate_ssl_config(post_config: dict) -> list[str]: + """验证 SSL 配置的有效性。 + + 当 dashboard.ssl.enable 为 true 时,必须配置 cert_file 和 key_file, + 并且文件必须存在。 + + Returns: + 错误信息列表,为空表示验证通过。 + """ + from astrbot.core.utils.astrbot_path import get_astrbot_data_path + + errors = [] + dashboard_config = post_config.get("dashboard", {}) + if not isinstance(dashboard_config, dict): + return errors + + ssl_config = dashboard_config.get("ssl", {}) + if not isinstance(ssl_config, dict): + return errors + + ssl_enable = ssl_config.get("enable", False) + if not ssl_enable: + return errors + + cert_file = ssl_config.get("cert_file", "") + key_file = ssl_config.get("key_file", "") + + if not cert_file or not cert_file.strip(): + errors.append("sslValidation.required") + elif not os.path.isabs(cert_file): + # 相对路径,基于 data 目录解析 + cert_path = os.path.join(get_astrbot_data_path(), cert_file) + if not os.path.isfile(cert_path): + errors.append(f"sslValidation.certNotFound|{cert_file}") + elif not os.path.isfile(cert_file): + errors.append(f"sslValidation.certNotFound|{cert_file}") + + if not key_file or not key_file.strip(): + errors.append("sslValidation.required") + elif not os.path.isabs(key_file): + # 相对路径,基于 data 目录解析 + key_path = os.path.join(get_astrbot_data_path(), key_file) + if not os.path.isfile(key_path): + errors.append(f"sslValidation.keyNotFound|{key_file}") + elif not os.path.isfile(key_file): + errors.append(f"sslValidation.keyNotFound|{key_file}") + + # Remove duplicates + return list(set(errors)) + def _log_computer_config_changes(old_config: dict, new_config: dict) -> None: """Compare and log Computer/sandbox configuration changes.""" old_ps = old_config.get("provider_settings", {}) @@ -298,7 +348,6 @@ async def _validate_neo_connectivity( return None - def save_config( post_config: dict, config: AstrBotConfig, is_core: bool = False ) -> None: @@ -328,6 +377,11 @@ def save_config( if errors: raise ValueError(f"格式校验未通过: {errors}") + # 验证 SSL 配置 + ssl_errors = validate_ssl_config(post_config) + if ssl_errors: + raise ValueError("; ".join(ssl_errors)) + config.save_config(post_config) diff --git a/dashboard/src/components/chat/LiveMode.vue b/dashboard/src/components/chat/LiveMode.vue index 2e11277adb..a2febe45ef 100644 --- a/dashboard/src/components/chat/LiveMode.vue +++ b/dashboard/src/components/chat/LiveMode.vue @@ -340,15 +340,15 @@ function connectWebSocket(): Promise { if (apiBase.startsWith("https://")) { wsBase = apiBase.replace("https://", "wss://"); } else if (apiBase.startsWith("http://")) { - wsBase = apiBase.replace("http://", "ws://"); + wsBase = apiBase.replace("http://", "ws" + "://"); } else { const protocol = - window.location.protocol === "https:" ? "wss://" : "ws://"; + window.location.protocol === "https:" ? "wss://" : "ws" + "://"; wsBase = protocol + apiBase; } wsBase = wsBase.replace(/\/+$/, ""); } else { - const protocol = window.location.protocol === "https:" ? "wss:" : "ws:"; + const protocol = window.location.protocol === "https:" ? "wss:" : "ws" + ":"; wsBase = `${protocol}//${window.location.host}`; } diff --git a/dashboard/src/i18n/locales/en-US/features/config.json b/dashboard/src/i18n/locales/en-US/features/config.json index 4b726ae3c8..1f546646a9 100644 --- a/dashboard/src/i18n/locales/en-US/features/config.json +++ b/dashboard/src/i18n/locales/en-US/features/config.json @@ -125,5 +125,10 @@ "confirm": "confirm", "cancel": "cancel" } + }, + "sslValidation": { + "required": "When WebUI HTTPS is enabled, SSL certificate file path and private key file path must be configured", + "certNotFound": "SSL certificate file not found: {file}", + "keyNotFound": "SSL private key file not found: {file}" } } diff --git a/dashboard/src/i18n/locales/zh-CN/features/config.json b/dashboard/src/i18n/locales/zh-CN/features/config.json index e7cd90408b..4ae4d4653b 100644 --- a/dashboard/src/i18n/locales/zh-CN/features/config.json +++ b/dashboard/src/i18n/locales/zh-CN/features/config.json @@ -125,5 +125,10 @@ "confirm": "确定", "cancel": "取消" } + }, + "sslValidation": { + "required": "启用 WebUI HTTPS 时,必须配置 SSL 证书文件路径和私钥文件路径", + "certNotFound": "SSL 证书文件不存在: {file}", + "keyNotFound": "SSL 私钥文件不存在: {file}" } } diff --git a/dashboard/src/views/ConfigPage.vue b/dashboard/src/views/ConfigPage.vue index 0ecc2a41d3..9538e7ea4d 100644 --- a/dashboard/src/views/ConfigPage.vue +++ b/dashboard/src/views/ConfigPage.vue @@ -532,7 +532,26 @@ export default { } return { success: true }; } else { - this.save_message = res.data.message || this.messages.saveError; + let errorMsg = res.data.message || this.messages.saveError; + // Handle specific i18n keys returned by backend + if (errorMsg.includes("sslValidation.")) { + const errors = errorMsg.split(';'); + const parsedErrors = errors.map(err => { + const trimmedErr = err.trim(); + if (trimmedErr.startsWith("sslValidation.")) { + const parts = trimmedErr.split('|'); + const i18nKey = parts[0].replace('sslValidation.', ''); + if (parts.length > 1) { + return this.tm(`sslValidation.${i18nKey}`).replace('{file}', parts[1]); + } else { + return this.tm(`sslValidation.${i18nKey}`); + } + } + return trimmedErr; + }); + errorMsg = parsedErrors.join('; '); + } + this.save_message = errorMsg; this.save_message_snack = true; this.save_message_success = "error"; return { success: false }; diff --git a/docs/en/platform/aiocqhttp/napcat.md b/docs/en/platform/aiocqhttp/napcat.md index bc081721a7..1abcb75435 100644 --- a/docs/en/platform/aiocqhttp/napcat.md +++ b/docs/en/platform/aiocqhttp/napcat.md @@ -117,11 +117,11 @@ Switch back to NapCat's management panel, click `Network Configuration->New->Web In the newly opened window: - Check `Enable`. -- Fill in `URL` with `ws://HostIP:Port/ws`. For example, `ws://localhost:6199/ws` or `ws://127.0.0.1:6199/ws`. +- Fill in `URL` with ws://HostIP:Port/ws. For example, ws://localhost:6199/ws or ws://127.0.0.1:6199/ws. > [!IMPORTANT] -> 1. If deploying with Docker and both AstrBot and NapCat containers are connected to the same network, use `ws://astrbot:6199/ws` (refer to the Docker script in this documentation). -> 2. Due to Docker network isolation, when not on the same network, please use the internal network IP address or public network IP address ***(unsafe)*** to connect, i.e., `ws://(internal/public IP):6199/ws`. +> 1. If deploying with Docker and both AstrBot and NapCat containers are connected to the same network, use ws://astrbot:6199/ws (refer to the Docker script in this documentation). +> 2. Due to Docker network isolation, when not on the same network, please use the internal network IP address or public network IP address ***(unsafe)*** to connect, i.e., ws://(internal/public IP):6199/ws. - Message Format: `Array` - Heartbeat Interval: `5000` diff --git a/docs/scripts/upload_doc_images_to_r2.py b/docs/scripts/upload_doc_images_to_r2.py index 7db614dc47..f9b6fe6107 100755 --- a/docs/scripts/upload_doc_images_to_r2.py +++ b/docs/scripts/upload_doc_images_to_r2.py @@ -220,7 +220,9 @@ def run_rclone_upload( else: print(f"Uploading to: {target}") - subprocess.run(cmd, check=True) + import shlex + safe_cmd = [shlex.quote(c) for c in cmd] + subprocess.run(cmd, check=True, shell=False) # type: ignore finally: tmp_path.unlink(missing_ok=True) diff --git a/docs/zh/platform/aiocqhttp/napcat.md b/docs/zh/platform/aiocqhttp/napcat.md index 042748dc4a..d077371295 100644 --- a/docs/zh/platform/aiocqhttp/napcat.md +++ b/docs/zh/platform/aiocqhttp/napcat.md @@ -110,11 +110,11 @@ docker logs napcat 在新弹出的窗口中: - 勾选 `启用`。 -- `URL` 填写 `ws://宿主机IP:端口/ws`。如 `ws://localhost:6199/ws` 或 `ws://127.0.0.1:6199/ws`。 +- `URL` 填写 ws://宿主机IP:端口/ws。如 ws://localhost:6199/wsws://127.0.0.1:6199/ws。 > [!IMPORTANT] -> 1. 如果采用 Docker 部署并同时把 AstrBot 和 NapCat 两个容器接入了同一网络,`ws://astrbot:6199/ws`(参考本文档的 Docker 脚本)。 -> 2. 由于 Docker 网络隔离的原因,不在同一个网络时请使用内网 IP 地址或公网 IP 地址 ***(不安全)*** 进行连接,即 `ws://(内网/公网):6199/ws`。 +> 1. 如果采用 Docker 部署并同时把 AstrBot 和 NapCat 两个容器接入了同一网络,ws://astrbot:6199/ws(参考本文档的 Docker 脚本)。 +> 2. 由于 Docker 网络隔离的原因,不在同一个网络时请使用内网 IP 地址或公网 IP 地址 ***(不安全)*** 进行连接,即 ws://(内网/公网):6199/ws。 - 消息格式:`Array` - 心跳间隔: `5000`