Chimera 运行时配置生成机制说明

1. 关键结论

核心(例如 chimera_client / mihomo)在启动和热重载时使用的配置不是直接从 profiles.yaml 读取的。核心真正使用的是一个运行时文件:

  • clash-config.yaml(运行时配置,位于 app_config_dir 下)

这个文件首先由后端在内存中组装,然后写入磁盘,最后通过启动参数Clash API 热重载传递给核心。

2. 配置输入(原材料)

运行时配置由四类输入构建而成:

  1. 应用设置:chimera-config.yaml

    • 结构体:IVerge
    • 加载入口:backend/tauri/src/config/chimera/mod.rsIVerge::new()
    • 用途:控制字段过滤、端口策略、TUN / 系统代理行为等。
  2. Clash Guard 覆盖模板:clash-guard-overrides.yaml

    • 结构体:IClashTemp
    • 加载入口:backend/tauri/src/config/clash/mod.rsIClashTemp::new()
    • 用途:强制覆盖关键字段(例如 modemixed-portexternal-controllersecret 等)。
  3. 配置元数据:profiles.yaml

    • 结构体:Profiles
    • 加载入口:backend/tauri/src/config/profile/profiles.rsProfiles::new()
    • 用途:记录当前激活的配置(current)和配置列表(items
  4. 具体配置内容文件:app_config_dir/profiles/*.yaml

    • 加载入口:Profiles::current_mappings()
    • 用途:提供代理、规则、DNS、TUN 等实际配置内容。

3. 启动初始化阶段

3.1 创建基础文件(如果缺失)

backend/tauri/src/utils/init/mod.rsinit_config() 会确保以下文件存在:

  • clash-guard-overrides.yaml(默认通过 IClashTemp::template() 生成)
  • chimera-config.yaml(默认通过 IVerge::template() 生成)
  • profiles.yaml(一个空 / 默认配置)

3.2 加载全局配置对象

backend/tauri/src/config/core.rsConfig::global() 会初始化:

  • Profiles::new()
  • IVerge::new()
  • IClashTemp::new()
  • IRuntime::new()

IRuntime 是内存中的运行时配置容器;其 config 字段类型为 Option<Mapping>

4. 运行时配置主组装流程

主入口:Config::generate()backend/tauri/src/config/core.rs

flowchart TD
  A["Start Config::generate"] --> B["Call enhance::enhance"]
  B --> C["Read YAML(s) of current profile"]
  C --> D["merge_profiles: merge configs"]
  D --> E["(Optional) whitelist-based field filtering"]
  E --> F["Override key fields (HANDLE_FIELDS)"]
  F --> G["Write into in-memory runtime config"]
  G --> H["Write out clash config YAML"]
  H --> I["Load at startup or hot-reload via PUT /configs"]

4.1 enhance::enhance() 做了什么

位置:backend/tauri/src/enhance/mod.rs

核心步骤:

  1. 加载 Clash Guard 配置

    • let clash_config = Config::clash().latest().0.clone()
  2. 读取当前功能开关 / 设置

    • 例如从 IVerge 读取 enable_clash_fields
  3. 加载当前激活配置的内容

    • 通过 Profiles::current_mappings()
    • 该方法会遍历 current,逐个读取 profiles/<file>.yaml,并将其转换为 Mapping
  4. (预留)执行配置链脚本

    • 调用 process_chain(...)
    • 当前实现只是占位(no-op),会直接返回原始配置
  5. 合并多个配置文件

    • 调用 merge_profiles(...)

    • 当前策略:

      • 第一份配置:完整 extend
      • 后续配置:仅将 proxies 追加到现有的 proxies
  6. 白名单字段过滤(可选,由开关控制)

    • use_whitelist_fields_filter(...)
    • enable_clash_fields = true 时,仅保留 valid + default fields 中的键
  7. 强制覆盖 Guard 字段

    • IClashTempHANDLE_FIELDS 列出的字段写回最终配置
    • 确保关键控制字段由客户端集中管理

4.2 HANDLE_FIELDS 覆盖范围

定义于 backend/tauri/src/enhance/field.rs

  • mode
  • port
  • socks-port
  • mixed-port
  • allow-lan
  • log-level
  • ipv6
  • secret
  • external-controller

这意味着,即使这些字段存在于配置文件中,其最终值也会被 clash-guard-overrides.yaml 中对应的值覆盖。

5. 写入磁盘与文件位置

入口:Config::generate_file(ConfigType::Run)backend/tauri/src/config/core.rs

  • Run 模式输出:app_config_dir()/clash-config.yaml
  • Check 模式输出:temp_dir()/clash-config-check.yaml

如果生成失败,Config::init_config() 会提供回退方案:直接将 IClashTemp 写为运行时配置。

6. 核心如何获取这份配置

6.1 启动时加载

CoreManager::run_core()Instance::try_new()backend/tauri/src/core/clash/core.rs):

  1. 调用 Config::generate_file(ConfigType::Run) 获取路径
  2. 通过 CoreInstanceBuilder.config_path(config_path) 将该路径传给核心进程

换句话说:核心在启动时会直接读取 clash-config.yaml

6.2 运行时热重载

CoreManager::update_config() 的流程:

  1. Config::generate().await? 重新组装内存中的配置
  2. check_config().await? 使用检查文件校验语法 / 可用性
  3. generate_file(Run) 重写 clash-config.yaml
  4. 调用 PUT /configs,并附带 { "path": "<absolute path>" } 请求体,以指示核心重载

相关代码:

  • backend/tauri/src/core/clash/core.rsupdate_config()
  • backend/tauri/src/core/clash/api.rsput_configs(...)

7. 会触发重建的用户操作

7.1 切换 / 修改配置选择

前端 commands.patchProfilesConfig → 后端 patch_profiles_config(...)

  1. 应用草稿:Config::profiles().draft().apply(...)
  2. 触发 CoreManager::update_config()
  3. 成功时:Config::profiles().apply() + save_file()
  4. 失败时:discard()(回滚)

7.2 修改设置(某些字段)

前端 commands.patchVergeConfig → 后端 feat::patch_verge(...)

  • 先写入一份 IVerge 草稿

  • 某些字段(例如 enable_tun_mode)可能触发:

    • Config::generate() + run_core()(重启场景)
    • update_core_config()(热更新场景)

7.3 导入第一份配置

import_profile(...) 成功后,如果当前还没有激活配置,它会自动构造 ProfilesBuilder.current = [new_uid],并复用 patch_profiles_config(...) 触发更新。

8. 关键细节与常见误解

  1. profiles.yaml 不是核心最终使用的配置

    • 它只存储配置元数据和 current 指针
  2. 配置内容文件不会原样传给核心

    • 它们只有在经过合并、过滤和 guard 覆盖之后,才会变成运行时配置
  3. external-controller 的端口可能会在启动前被修改

    • prepare_external_controller_port() 会按策略检查端口可用性,并在必要时切换端口
  4. verge_mixed_port 主要用于系统代理逻辑

    • 不会被直接写入运行时 YAML 的 mixed-port
    • 系统代理会优先使用 verge_mixed_port,否则回退到 Config::clash().get_mixed_port()
  5. get_runtime_yaml() 返回的是内存中的 IRuntime.config

    • 它通常与最近写入的 clash-config.yaml 保持一致
    • 但从根本上说,它来自内存,而不是每次都重新读取磁盘

9. 当前实现限制(以当前代码库为准)

  1. 链式脚本执行目前仍是占位实现

    • process_chain(...) 目前尚未真正改写配置
  2. 全局链处理代码仍处于注释状态

    • 目前仅存在带作用域的链式框架
  3. patch_clash_config IPC 仍然是 todo!()

    • 如果前端使用该 IPC 路径,将会失败
  4. 直接编辑配置文件不会自动触发热重载

    • save_profile_file(...) 只会写入文件;它不会调用 update_config()

10. 故障排查清单(实用顺序)

如果你怀疑“核心使用了错误的配置”,请按以下顺序检查:

  1. 确认当前激活的配置是否正确

    • 检查 profiles.yamlcurrent
  2. 确认配置源内容符合预期

    • 检查 app_config_dir/profiles/*.yaml
  3. 检查 guard 覆盖项

    • 确认 clash-guard-overrides.yaml 中的 HANDLE_FIELDS 是否覆盖了你预期的值
  4. 检查最终运行时配置

    • 检查 clash-config.yaml
    • 或调用 get_runtime_yaml() 查看内存中的版本
  5. 确认热重载是否真的发生

    • 确认 patch_profiles_config / patch_verge_config / restart_sidecar 是否已执行
    • 检查日志,确认 PUT /configs 是否成功
  6. 如果你遇到端口相关问题

    • 检查 external-controller 是否被端口策略改写