Skip to content

第 6 章:Profile 生命周期——从订阅到当前配置

Profile 不是一个 URL,也不是一个 YAML 文件。它是一组元数据、内容文件、增强引用、更新时间和节点选择记录。本章追踪创建、下载、更新、激活、删除与选择恢复,说明为什么每一步都要区分“索引”和“内容”。

一、PrfItem 是 Profile 的目录项

PrfItem 主要字段包括:

  • uid:稳定标识;
  • itype:remote/local/merge/script/rules/proxies/groups;
  • namedesc
  • file:实际内容文件;
  • url:远程订阅来源;
  • updatedextra:更新时间和流量信息;
  • option:更新代理策略、自动更新、增强引用;
  • selected:代理组的节点选择记录。

索引与内容分离的好处:

  • 更新远程内容无需重写整个 profiles.yaml;
  • Merge/Script 可以被多个 Profile 引用;
  • 删除时能精确判断文件是否仍被使用;
  • 大文件不会让配置索引越来越重。

二、七种 item 其实是三类角色

角色类型说明
基础配置remote、local提供完整 Mihomo 配置
Mapping 覆盖merge、script深合并或执行 JS 变换
Sequence 覆盖rules、proxies、groupsprepend / append / delete

remote/local 能成为 current;其余类型作为增强素材被引用。启动时系统还会确保全局 MergeScript 默认项存在。

三、创建本地 Profile

PrfItem::from_local() 的工作比生成 uid 多:

  1. 合并传入 option;
  2. 若没有 merge/script 引用,创建默认增强项;
  3. 为内容生成独立文件名;
  4. 校验或写入 YAML;
  5. 返回完整索引项。

IProfiles::append_item() 再负责把 item 加入索引并安全保存。

这里把“构造一个合法 item”和“把 item 放入集合”分开,便于 remote/local 共用集合逻辑。

四、远程订阅下载的三条网络路径

Profile 更新按顺序尝试:

text
直接请求
  ↓ 失败
通过当前 Clash/Mihomo 代理请求
  ↓ 失败
通过操作系统代理请求
  ↓ 失败
通知用户最终错误

每次尝试通过 PrfOption 切换 self_proxywith_proxy,而不是复制三套下载实现。

为什么先直连?当前订阅可能正是为了修复代理,依赖旧核心可能形成自举问题。为什么仍要有后两种路径?某些订阅服务器只能从代理网络访问,或用户的组织网络要求系统代理。

错误日志会对 URL 和敏感内容做 mask,避免订阅凭据进入日志。

五、自动更新与手动更新语义不同

should_update_profile() 会检查:

  • 是否 remote;
  • 是否有 URL;
  • allow_auto_update
  • 调用者是否要求忽略自动更新限制。

自动更新遇到 Busy/Skipped 可以记录后等待下一轮;手动触发则需要把失败明确反馈给用户。update_config_with_force(is_manual) 正是把这两种用户预期传到配置刷新层。

六、激活 Profile 是一次核心配置替换

切换当前 Profile 的逻辑不是只改 current

text
记录新的 current uid(draft)

enhance() 读取其基础 YAML 与增强引用

生成并校验 runtime

reload 或 restart Mihomo

提交 profiles/runtime

发 profile-changed / refresh events

恢复该 Profile 的节点选择

任何一步失败,当前 Profile 不应被正式切换。

七、为什么要保存代理组选择

Mihomo reload/restart 后,selector group 可能回到配置默认节点。为了保持用户体验,Profile 保存 { group, node } 记录。

前端选节点后做三件事:

  1. 通过 Mihomo API 切换组;
  2. 通知托盘同步;
  3. command record_selected_node(groupName, node) 更新 Profile。

command 只传一个变化,不传整份 selection list。后端在最新 Profile 上 merge,可避免两次快速选择形成 lost update。

八、恢复选择不是简单循环 PUT

核心刚启动时,代理组和 provider 可能尚未全部加载。restore_selected_nodes() 因此包含一个收敛过程:

  1. 读取保存的选择记录;
  2. 获取 Mihomo 当前 proxies 快照;
  3. 判断组是否存在、是否可选、节点是否属于该组;
  4. 已经选中则标记 settled;
  5. 未加载完整则等待第二次快照;
  6. 节点确认不存在时选择有效 fallback 或移除无效记录;
  7. 先执行仍合法的激活,再提交修复后的记录。

8.1 为什么要两次快照

第一次不存在,可能只是核心/provider 尚未就绪;第二次仍不存在,才更有把握判定记录陈旧。一次快照就删除会在慢启动时丢失用户选择。

8.2 supersede generation

恢复任务运行期间可能又切换 Profile。全局 generation 使旧任务发现自己已被 supersede 后主动退出,避免把 A Profile 的选择写进 B Profile。

九、删除与孤儿文件清理

删除 item 不能直接删 item.file

  • 该文件可能仍被另一索引项引用;
  • 全局 Merge/Script 是保护项;
  • profiles.yaml 加载失败时,索引不可信,不能据此清空目录。

cleanup_orphaned_files() 先建立所有 active file 集合和 protected global files,再删除符合 Profile 文件命名规则、但不在引用集合中的文件。

项目专门测试了“profiles 索引无法读取时,不删除现有 Profile 文件”。这是失败时保护用户数据的典型原则:不确定时宁可留下孤儿,也不要猜测删除。

十、结构化编辑器和原始编辑器

前端 Profile 页同时支持:

  • Monaco 原始 YAML/JS;
  • rules/proxies/groups 结构化编辑器;
  • Merge 与 Script 关联;
  • 拖拽排序;
  • 二维码/URI 导入。

这些编辑最终仍落入相同的 PrfItem 内容文件与 command 门面。多种 UI 不应创造多套后端数据模型。

十一、Profile 模型的设计启示

  1. 索引与大内容分离,让集合操作和内容更新互不放大。
  2. 网络策略参数化,同一下载器按 direct/self/system proxy 重试。
  3. 当前项切换是事务,不是赋值
  4. 恢复外部系统状态要允许收敛时间,不要把第一次缺失当最终事实。
  5. 清理的证据门槛应高于创建,索引损坏时优先保护数据。

本章源码索引

  • src-tauri/src/config/prfitem.rs::PrfItem:Profile 目录项与构造器
  • src-tauri/src/config/prfitem.rs::PrfOption:增强与下载选项
  • src-tauri/src/config/profiles.rs::IProfiles:集合、当前项、CRUD 与清理
  • src-tauri/src/config/profiles.rs::record_selected_node:增量保存选择
  • src-tauri/src/config/profiles.rs::restore_selected_nodes:选择恢复与修复
  • src-tauri/src/feat/profile.rs::update_profile:订阅更新与三段重试
  • src-tauri/src/cmd/profile.rs:Profile commands
  • src/pages/profiles.tsxsrc/components/profile/:前端管理与编辑

原创文档 CC BY-SA 4.0 · 站点代码 MIT