refactor(core,shim): 统一基于 thiserror 的错误体系

- 移除 anyhow,使用 thiserror 定义强类型 Error 枚举
- 支持 #[from] 自动转换 mlua::Error,关键路径保留源码与上下文
- LuaRuntimeBuilder 采用惰性错误收集,简化链式初始化调用
- MirrorRegistry 实现规范化 FromLuaConversionError 错误转换
This commit is contained in:
2026-09-10 14:33:54 +08:00
parent 94b4c23299
commit 38dabd2ace
22 changed files with 686 additions and 482 deletions
+1
View File
@@ -14,3 +14,4 @@ serde = { workspace = true }
tracing = { workspace = true }
tracing-subscriber = { workspace = true }
tracing-appender = { workspace = true }
thiserror = "2.0.20"
+67 -1
View File
@@ -1,8 +1,21 @@
use std::path::PathBuf;
use thiserror::Error;
// 构造一个带上下文的 FromLua 转换错误,便于定位配置问题
/// 配置校验错误
pub fn validation_error(message: impl Into<String>) -> mlua::Error {
mlua::Error::RuntimeError(message.into())
}
pub fn conversion_err(
from: &'static str,
to: impl Into<String>,
msg: impl Into<String>,
) -> mlua::Error {
mlua::Error::FromLuaConversionError {
from,
to: to.into(),
message: Some(msg.into()),
}
}
/// 将底层 Lua 语法错误转换为用户友好的提示
pub fn syntax_error(err: mlua::Error) -> mlua::Error {
match &err {
@@ -22,4 +35,57 @@ pub fn syntax_error(err: mlua::Error) -> mlua::Error {
_ => {}
}
err
}
}
/// 全局统一的 `Result` 别名,默认错误类型为 [`Error`]。
pub type Result<T, E = Error> = std::result::Result<T, E>;
#[derive(Error, Debug)]
pub enum Error {
#[error("初始化 Lua 沙箱失败: {0}")]
LuaInit(#[source] mlua::Error),
#[error("构建 LuaRuntime 异常")]
BuildError,
///读取脚本文件失败
#[error("读取脚本文件失败 ({path}): {source}")]
ScriptRead {
path: PathBuf,
#[source]
source: std::io::Error,
},
#[error("脚本 {path} 不是有效的 UTF-8 文件(请将 Lua 配置文件保存为 UTF-8 编码)")]
InvalidUtf8 { path: PathBuf },
// 收敛所有来自 mlua 的错误,附带来源上下文
#[error("Lua 执行异常 ({chunk}): {source}")]
LuaRuntime {
chunk: String,
#[source]
source: mlua::Error,
},
#[error("模块 '{module_name}' 必须返回一个函数作为入口,实际返回了 {actual_type}")]
InvalidModuleEntry {
module_name: String,
actual_type: &'static str,
},
// #[error("插件/上下文挂载失败 '{name}': {source}")]
// Plugin {
// name: String,
// #[source]
// source: mlua::Error,
// },
#[error("Lua 运行时未指定上下文错误: {0}")]
LuaError(#[from] mlua::Error),
// #[error("设置 {0} 环境变量失败")]
// SetMirrorDir(String),
/// 用户自定义扩展错误,用于包装第三方插件产生的错误。
#[error("用户自定义扩展错误: {0}")]
Other(#[source] Box<dyn std::error::Error + Send + Sync>),
}
+8 -4
View File
@@ -1,6 +1,7 @@
pub mod error;
mod error;
mod layout;
mod logger;
mod plugins;
mod runtime;
mod utils;
mod validators;
@@ -9,6 +10,9 @@ pub use layout::Layout;
// pub use mirror_shim::mirror::Mirror;
pub use runtime::LuaRuntime;
pub use logger::{init_logging_from, LogLevel, Logger};
// pub use mirror_shim::utils::{lua_string_2_os_string, normalize_path_for_lua, parse_tokens};
pub use validators::validate_command_name;
pub use logger::{LogConfig, LogLevel, init_logging_from};
pub use plugins::register_basics;
pub use utils::normalize_path_for_lua;
pub use validators::validate_command_name;
// 把 Result 和 Error 提升到根命名空间,给外部带来极致便利
pub use error::{Error, Result,validation_error,conversion_err};
+65 -63
View File
@@ -1,7 +1,7 @@
use crate::Layout;
use serde::Deserialize;
use std::convert::Infallible;
use std::fs;
use std::path::PathBuf;
use std::path::{Path, PathBuf};
use std::str::FromStr;
use tracing_appender::non_blocking::{NonBlocking, WorkerGuard};
use tracing_subscriber::layer::SubscriberExt;
@@ -61,41 +61,50 @@ impl FromStr for LogLevel {
}
}
#[derive(Debug, Deserialize, Default)]
pub struct Logger {
#[serde(default)]
/// 日志配置模型(对应 mirror.ini 的纯强类型表示)
#[derive(Debug, Clone, PartialEq, Eq, Default)]
pub struct LogConfig {
pub level: LogLevel,
pub log_dir: Option<PathBuf>,
}
impl Logger {
impl LogConfig {
/// 零依赖解析 ini / key-value 文本内容
pub fn parse_ini(content: &str) -> Self {
let mut logger = Self::default();
/// 计算日志目录的实际物理路径(相对路径基于 base_dir 拼接)
pub fn resolve_log_dir(&self, base_dir: &Path) -> PathBuf {
match &self.log_dir {
Some(p) if p.is_absolute() => p.clone(),
Some(p) => base_dir.join(p),
None => base_dir.join("logs"),
}
}
}
// 协议 1: 反序列化 (INI 文本 -> LogConfig)
impl FromStr for LogConfig {
type Err = Infallible;
fn from_str(content: &str) -> Result<Self, Self::Err> {
let mut config = Self::default();
for line in content.lines() {
let line = line.trim();
// 跳过空行、注释行 (# 或 ;) 和 section 头 ([...])
if line.is_empty()
|| line.starts_with('#')
|| line.starts_with(';')
|| line.starts_with('[')
{
// 跳过空行、注释 (#, ;) 与 section 标头 ([...])
if line.is_empty() || line.starts_with(['#', ';', '[']) {
continue;
}
if let Some((key, val)) = line.split_once('=') {
let key = key.trim();
let val = val.trim().trim_matches('"').trim_matches('\'');
let val = val.trim().trim_matches(['"', '\'']);
match key {
"level" => {
if let Ok(lvl) = LogLevel::from_str(val) {
logger.level = lvl;
config.level = lvl;
}
}
"log_dir" => {
if !val.is_empty() {
logger.log_dir = Some(PathBuf::from(val));
config.log_dir = Some(PathBuf::from(val));
}
}
_ => {}
@@ -103,9 +112,24 @@ impl Logger {
}
}
logger
Ok(config)
}
}
// 协议 2: 序列化 (LogConfig -> INI 文本)
impl std::fmt::Display for LogConfig {
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
let dir_str = self
.log_dir
.as_deref()
.and_then(|p| p.to_str())
.unwrap_or("logs");
writeln!(f, "level = \"{}\"", self.level.as_str())?;
writeln!(f, "log_dir = \"{}\"", dir_str)
}
}
/// 基于 Layout 自动寻址 mirror.toml 并初始化日志系统。
/// 仅在开启详细日志(如 info/debug/trace)时激活异步文件记录并返回 `WorkerGuard`。
pub fn init_logging_from(layout: &Layout) -> Option<WorkerGuard> {
@@ -113,16 +137,11 @@ pub fn init_logging_from(layout: &Layout) -> Option<WorkerGuard> {
// let config_path = layout.base_dir.join("mirror.toml");
let config_path = layout.base_dir.join("mirror.ini");
// 读取配置文件(如不存在或解析失败,降级回退到默认设置)
let config = if config_path.exists() {
fs::read_to_string(&config_path)
.map(|s| Logger::parse_ini(&s))
// .ok()
// .and_then(|s| toml::from_str::<Logger>(&s).ok())
.unwrap_or_default()
} else {
Logger::default()
};
// 1. 读取并反序列化配置文件(缺失或异常时安全降级为 Default)
let config: LogConfig = fs::read_to_string(&config_path)
.ok()
.and_then(|s| s.parse().ok())
.unwrap_or_default();
// 1. 优先读取环境变量 MIRROR_LOG_LEVEL,解析失败或未设置则退回到 config.level
let effective_level = std::env::var("MIRROR_LOG_LEVEL")
@@ -136,47 +155,30 @@ pub fn init_logging_from(layout: &Layout) -> Option<WorkerGuard> {
// 基础终端输出层
let stderr_layer = fmt::layer().with_writer(std::io::stderr).with_target(false);
// 2. 根据强类型枚举判断是否激活落盘模式
if effective_level.is_verbose() {
let (non_blocking, guard) = create_file_appender(layout, config.log_dir);
let file_layer = fmt::layer()
// 3. 动态配置落盘文件层(仅在 verbose 级别启用)
let (file_layer, guard) = if effective_level.is_verbose() {
let (non_blocking, guard) = create_file_appender(&config.resolve_log_dir(&layout.base_dir));
let layer = fmt::layer()
.with_writer(non_blocking)
.with_ansi(false)
.with_target(false);
let _ = tracing_subscriber::registry()
.with(filter)
.with(stderr_layer)
.with(file_layer)
.try_init();
Some(guard)
(Some(layer), Some(guard))
} else {
let _ = tracing_subscriber::registry()
.with(filter)
.with(stderr_layer)
.try_init();
None
}
}
fn create_file_appender(
layout: &Layout,
log_dir_override: Option<PathBuf>,
) -> (NonBlocking, WorkerGuard) {
let output_dir = match log_dir_override {
Some(user_path) => {
if user_path.is_relative() {
layout.base_dir.join(user_path)
} else {
user_path
}
}
None => layout.base_dir.join("logs"),
(None, None)
};
if let Err(err) = fs::create_dir_all(&output_dir) {
// 4. 统一组装并初始化 Subscriber(Option<Layer> 天然实现了 Layer trait)
let _ = tracing_subscriber::registry()
.with(filter)
.with(stderr_layer)
.with(file_layer)
.try_init();
guard
}
fn create_file_appender(output_dir: &Path) -> (NonBlocking, WorkerGuard) {
if let Err(err) = fs::create_dir_all(output_dir) {
eprintln!("[WARN] 创建日志目录 {} 失败: {}", output_dir.display(), err);
}
+111
View File
@@ -0,0 +1,111 @@
use crate::error::{Error, Result};
use crate::{Layout, normalize_path_for_lua};
use mlua::{Lua, Table, Value};
use std::env;
use tracing::{debug, warn};
const MIRROR_DIR: &str = "__MIRROR_DIR__";
// 采用形式 A:直接传引用的普通函数
pub fn register_basics(lua: &Lua, layout: &Layout) -> Result<()> {
let base_dir = normalize_path_for_lua(&layout.base_dir);
register_mirror_dir(lua, &base_dir)?;
register_get_env(lua)?;
setup_require(lua, &base_dir)?;
Ok(())
}
/// 注入全局锚点变量
pub fn register_mirror_dir(lua: &Lua, base_dir: &str) -> Result<()> {
debug!("设置 {MIRROR_DIR} 环境变量");
lua.globals().set(MIRROR_DIR, base_dir)?;
// .context(format!("设置 {MIRROR_DIR} 环境变量失败"))?;
Ok(())
}
///注入 get_env 供配置读取环境变量
pub fn register_get_env(lua: &Lua) -> Result<()> {
let globals = lua.globals();
let get_env = lua.create_function(|lua, key: String| -> mlua::Result<Table> {
// 缺失变量视为空字符串,拆分后得到空表(不贡献任何路径段)
let value = env::var_os(key).unwrap_or_default();
// 空输入返回空表;否则按平台分隔符拆分(split_paths 会剥离引号包裹)
if value.is_empty() {
return lua.create_table();
}
let table = lua.create_table()?;
for (i, p) in env::split_paths(&value).enumerate() {
// 3. 跨平台提取原始字节并转为 LuaString,保证 100% 无损
#[cfg(unix)]
let lua_str = {
use std::os::unix::ffi::OsStrExt;
lua.create_string(p.as_os_str().as_bytes())?
};
#[cfg(windows)]
let lua_str = {
// Windows 路径是 UTF-16,转成字符串或保持其字节表达
let s = p.to_string_lossy();
lua.create_string(s.as_bytes())?
};
table.set(i + 1, lua_str)?;
}
Ok(table)
})?;
// .context("注册 get_env 函数失败")?;
globals.set("get_env", get_env)?;
// .context("挂载 get_env 全局函数失败")?;
Ok(())
}
/// 配置 package.path 并包装 require,拦截加载失败以提高容错性
pub fn setup_require(lua: &Lua, base_dir: &str) -> Result<()> {
let globals = lua.globals();
// 1. 安全加固并拓展 package 搜索路径
let package = globals.get::<Table>("package")?;
let _ = package.set("cpath", "");
let _ = package.set("loadlib", Value::Nil);
let commands_dir = format!("{}/commands", base_dir);
let vendor_dir = format!("{}/vendor", base_dir);
let path = package.get::<String>("path")?;
let new_path = format!(
"{};{}/?.lua;{}/?/init.lua;{}/?.lua;{}/?/init.lua;{}/?.lua;{}/?/init.lua;",
path, base_dir, base_dir, commands_dir, commands_dir, vendor_dir, vendor_dir
);
debug!("重新写入的 package 查找路径");
package.set("path", new_path)?;
// 2. 获取原生 require 并通过闭包直接持有(无需向全局表注入备份变量)
let original_require: mlua::Function = globals.get("require")?;
// .context("获取内置 require 函数失败")?;
let wrapped_require =
lua.create_function(move |_lua, module: String| -> mlua::Result<Value> {
match original_require.call::<Value>(module.as_str()) {
Ok(value) => Ok(value),
Err(e) => {
warn!(
module = %module,
error = %e,
"配置模块加载失败,已跳过该条目(可在独立配置文件中定义)"
);
Ok(Value::Nil)
}
}
})?;
// .context("创建包装版 require 函数失败")?;
// 3. 覆盖全局 require
globals.set("require", wrapped_require)?;
// .context("重载 require 函数失败")?;
Ok(())
}
+58 -166
View File
@@ -1,13 +1,34 @@
use crate::Layout;
use crate::error::syntax_error;
use crate::utils::normalize_path_for_lua;
use anyhow::{Context, Result};
use mlua::{FromLua, Lua, StdLib, Table, Value};
use crate::{Error, Result};
use mlua::{FromLua, Lua, StdLib, Value};
use std::fs;
use std::path::Path;
use std::{env, fs};
use tracing::{debug, trace, warn};
use tracing::debug;
const MIRROR_DIR: &str = "__MIRROR_DIR__";
pub struct LuaRuntimeBuilder {
lua: Result<Lua>,
}
impl LuaRuntimeBuilder {
pub fn with<F>(mut self, extension: F) -> Self
where
F: FnOnce(&Lua) -> Result<()>,
{
// 如果前面已有错误,直接短路跳过后续操作
if let Ok(ref lua) = self.lua {
if let Err(e) = extension(lua) {
self.lua = Err(e);
}
}
self
}
/// 收敛构建终点:一次性输出统一的 Result
pub fn build(self) -> Result<LuaRuntime> {
let lua = self.lua?;
Ok(LuaRuntime { lua })
}
}
/// 将 Layout 相关的公共上下文组合为一个插件项
pub struct LuaRuntime {
lua: Lua,
@@ -15,43 +36,28 @@ pub struct LuaRuntime {
impl LuaRuntime {
/// 初始化限定权限的 Lua 沙箱环境
pub fn new(layout: &Layout) -> Result<Self> {
pub fn builder() -> LuaRuntimeBuilder {
// 只加载安全的标准库,剥离 os / io 等风险模块
let lua = Lua::new_with(
StdLib::TABLE | StdLib::STRING | StdLib::MATH | StdLib::PACKAGE,
mlua::LuaOptions::default(),
)
.context("初始化 Lua 失败")?;
.map_err(Error::LuaInit);
// 统一使用 POSIX 风格路径规范化路径字符串
let base_dir = normalize_path_for_lua(&layout.base_dir);
// let tools_dir = normalize_path_for_lua(&layout.tools_dir);
// 1. 注入锚点变量 __MIRROR_DIR__(shim 安装根目录)
Self::register_mirror_dir(&lua, &base_dir)?;
// 2. 安全暴露 get_env 供配置读取环境变量
// 返回按平台路径分隔符拆分后的段数组(自动剥离引号包裹),
// 便于 PATH 等列表变量直接嵌入数组:PATH = { prefix, get_env("PATH") }
Self::register_get_env(&lua)?;
// 3. 初始化并配置安全/容错的 require 机制
Self::setup_require(&lua, &base_dir)?;
Ok(Self { lua })
LuaRuntimeBuilder { lua }
}
/// 执行指定脚本文件,直接返回完整的 Lua Table
pub fn eval_script<T: FromLua>(&self, path: impl AsRef<Path>) -> Result<T> {
let path = path.as_ref();
let bytes =
fs::read(path).with_context(|| format!("无法读取配置文件: {}", path.display()))?;
let bytes = fs::read(path).map_err(|e| Error::ScriptRead {
path: path.to_path_buf(),
source: e,
})?;
let code = String::from_utf8(bytes).with_context(|| {
format!(
"{} 不是有效的 UTF-8 文件(请将 Lua 配置文件保存为 UTF-8 编码)",
path.display()
)
let code = String::from_utf8(bytes).map_err(|_| Error::InvalidUtf8 {
path: path.to_path_buf(),
})?;
debug!("code {:?}", code);
// 使用 @<path> 格式标记 Chunk 名称,确保 Lua 报错时能精确回溯到对应的源文件名与行号。
@@ -61,150 +67,36 @@ impl LuaRuntime {
.load(&code)
.set_name(&chunk_name)
.eval::<T>()
.map_err(syntax_error)
.with_context(|| format!("执行 {} 配置文件失败: {}", chunk_name,path.display()))
}
/// 传入注册表的 handler key 与命令行原始参数,内部完成调用
pub fn call_handler(
&self,
handler_key: &mlua::RegistryKey,
args: Vec<String>,
) -> Result<()> {
let handler: mlua::Function = self.lua.registry_value(handler_key)?;
let lua_args = self.lua.create_sequence_from(args)?;
if let Err(err) = handler.call::<()>(lua_args) {
let msg = err.to_string();
// 内部消化类似 argparse 的帮助退出信号
if msg.contains("__ARGPARSE_HELP__") {
return Ok(());
}
anyhow::bail!("{msg}");
}
Ok(())
.map_err(|e| Error::LuaRuntime {
chunk: chunk_name,
source: e,
})
}
pub fn require_and_run(&self, module_name: &str, raw_args: Vec<String>) -> Result<()> {
let require: mlua::Function = self.lua.globals().get("require")?;
let handler_val: mlua::Value = require.call(module_name)?;
let handler_val: Value = require.call(module_name)?;
let func = match handler_val {
mlua::Value::Function(f) => f,
other => anyhow::bail!("模块 '{module_name}' 必须返回一个函数,实际为 {}", other.type_name()),
Value::Function(f) => f,
other => {
return Err(Error::InvalidModuleEntry {
module_name: module_name.to_string(),
actual_type: other.type_name(),
});
}
};
let lua_args = self.lua.create_sequence_from(raw_args)?;
func.call::<()>(lua_args)?;
Ok(())
}
}
impl LuaRuntime {
/// 注入全局锚点变量
fn register_mirror_dir(lua: &Lua, base_dir: &str) -> Result<()> {
lua.globals()
.set(MIRROR_DIR, base_dir)
.context(format!("设置 {MIRROR_DIR} 环境变量失败"))?;
Ok(())
}
///注入 get_env 供配置读取环境变量
fn register_get_env(lua: &Lua) -> Result<()> {
let globals = lua.globals();
let get_env = lua
.create_function(|lua, key: String| -> mlua::Result<Table> {
// 缺失变量视为空字符串,拆分后得到空表(不贡献任何路径段)
let value = env::var_os(key).unwrap_or_default();
// 空输入返回空表;否则按平台分隔符拆分(split_paths 会剥离引号包裹)
if value.is_empty() {
return lua.create_table();
}
let table = lua.create_table()?;
for (i, p) in env::split_paths(&value).enumerate() {
// 3. 跨平台提取原始字节并转为 LuaString,保证 100% 无损
#[cfg(unix)]
let lua_str = {
use std::os::unix::ffi::OsStrExt;
lua.create_string(p.as_os_str().as_bytes())?
};
#[cfg(windows)]
let lua_str = {
// Windows 路径是 UTF-16,转成字符串或保持其字节表达
let s = p.to_string_lossy();
lua.create_string(s.as_bytes())?
};
table.set(i + 1, lua_str)?;
}
Ok(table)
})
.context("注册 get_env 函数失败")?;
globals
.set("get_env", get_env)
.context("挂载 get_env 全局函数失败")?;
Ok(())
}
/// 配置 package.path 并包装 require,拦截加载失败以提高容错性
fn setup_require(lua: &Lua, base_dir: &str) -> Result<()> {
let globals = lua.globals();
// 1. 安全加固并拓展 package 搜索路径
let package = globals.get::<Table>("package")?;
let _ = package.set("cpath", "");
let _ = package.set("loadlib", Value::Nil);
let commands_dir = format!("{}/commands", base_dir);
let vendor_dir = format!("{}/vendor", base_dir);
let path = package.get::<String>("path")?;
let new_path = format!(
"{};{}/?.lua;{}/?/init.lua;{}/?.lua;{}/?/init.lua;{}/?.lua;{}/?/init.lua;",
path, base_dir, base_dir, commands_dir, commands_dir, vendor_dir, vendor_dir
);
debug!("重新写入的 package 查找路径");
package.set("path", new_path)?;
// 2. 获取原生 require 并通过闭包直接持有(无需向全局表注入备份变量)
let original_require: mlua::Function = globals
.get("require")
.context("获取内置 require 函数失败")?;
let wrapped_require = lua
.create_function(move |_lua, module: String| -> mlua::Result<Value> {
match original_require.call::<Value>(module.as_str()) {
Ok(value) => Ok(value),
Err(e) => {
warn!(
module = %module,
error = %e,
"配置模块加载失败,已跳过该条目(可在独立配置文件中定义)"
);
Ok(Value::Nil)
}
}
})
.context("创建包装版 require 函数失败")?;
// 3. 覆盖全局 require
globals
.set("require", wrapped_require)
.context("重载 require 函数失败")?;
Ok(())
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::{Layout, };
use crate::Layout;
fn test_layout() -> Layout {
let root = std::env::temp_dir().join("rshim-test-layout");
@@ -217,7 +109,7 @@ mod tests {
#[test]
fn missing_module_require_returns_nil() {
let runtime = LuaRuntime::new(&test_layout()).unwrap();
let runtime = LuaRuntime::builder().build().unwrap();
let value: Value = runtime
.lua
.load(r#"return require("rshim_test_no_such_module")"#)
@@ -228,7 +120,7 @@ mod tests {
#[test]
fn builtin_module_require_still_works() {
let runtime = LuaRuntime::new(&test_layout()).unwrap();
let runtime = LuaRuntime::builder().build().unwrap();
let value: Value = runtime
.lua
.load(r#"return pcall(require, "string")"#)
@@ -239,7 +131,7 @@ mod tests {
#[test]
fn get_env_returns_split_table() {
let runtime = LuaRuntime::new(&test_layout()).unwrap();
let runtime = LuaRuntime::builder().build().unwrap();
let value: Value = runtime
.lua
.load(r#"return get_env("PATH")"#)
@@ -266,7 +158,7 @@ mod tests {
#[test]
fn get_env_missing_returns_empty_table() {
let runtime = LuaRuntime::new(&test_layout()).unwrap();
let runtime = LuaRuntime::builder().build().unwrap();
let value: Value = runtime
.lua
.load(r#"return get_env("RSHIM_TEST_NO_SUCH_VAR_12345")"#)
@@ -298,7 +190,7 @@ mod tests {
// )
// .eval()
// .unwrap();
//
//
// let path = cfg.env.get("PATH").unwrap().to_str().unwrap();
// let prefix = std::env::temp_dir()
// .join("rshim-test-layout")
@@ -306,7 +198,7 @@ mod tests {
// .replace('\\', "/")
// + "/tools/numa;";
// assert!(path.starts_with(&prefix), "unexpected PATH: {path}");
//
//
// // 宿主 PATH 的段应被附加在配置前缀之后
// let host = std::env::var("PATH").unwrap_or_default();
// if !host.is_empty() {
@@ -320,7 +212,7 @@ mod tests {
// "missing host PATH segment: {host_first}"
// );
// }
//
//
// // 宿主 PATH 里引号包裹的畸形段(如 "D:\\...\\bin;")应被原样保留,
// // 而不是让整个配置加载失败
// if host.contains('"') {