第 6 章:Profile 生命周期——从订阅到当前配置
Profile 不是一个 URL,也不是一个 YAML 文件。它是一组元数据、内容文件、增强引用、更新时间和节点选择记录。本章追踪创建、下载、更新、激活、删除与选择恢复,说明为什么每一步都要区分“索引”和“内容”。
一、PrfItem 是 Profile 的目录项
PrfItem 主要字段包括:
uid:稳定标识;itype:remote/local/merge/script/rules/proxies/groups;name、desc;file:实际内容文件;url:远程订阅来源;updated、extra:更新时间和流量信息;option:更新代理策略、自动更新、增强引用;selected:代理组的节点选择记录。
索引与内容分离的好处:
- 更新远程内容无需重写整个 profiles.yaml;
- Merge/Script 可以被多个 Profile 引用;
- 删除时能精确判断文件是否仍被使用;
- 大文件不会让配置索引越来越重。
二、七种 item 其实是三类角色
| 角色 | 类型 | 说明 |
|---|---|---|
| 基础配置 | remote、local | 提供完整 Mihomo 配置 |
| Mapping 覆盖 | merge、script | 深合并或执行 JS 变换 |
| Sequence 覆盖 | rules、proxies、groups | prepend / append / delete |
remote/local 能成为 current;其余类型作为增强素材被引用。启动时系统还会确保全局 Merge 和 Script 默认项存在。
三、创建本地 Profile
PrfItem::from_local() 的工作比生成 uid 多:
- 合并传入 option;
- 若没有 merge/script 引用,创建默认增强项;
- 为内容生成独立文件名;
- 校验或写入 YAML;
- 返回完整索引项。
IProfiles::append_item() 再负责把 item 加入索引并安全保存。
这里把“构造一个合法 item”和“把 item 放入集合”分开,便于 remote/local 共用集合逻辑。
四、远程订阅下载的三条网络路径
Profile 更新按顺序尝试:
直接请求
↓ 失败
通过当前 Clash/Mihomo 代理请求
↓ 失败
通过操作系统代理请求
↓ 失败
通知用户最终错误每次尝试通过 PrfOption 切换 self_proxy 和 with_proxy,而不是复制三套下载实现。
为什么先直连?当前订阅可能正是为了修复代理,依赖旧核心可能形成自举问题。为什么仍要有后两种路径?某些订阅服务器只能从代理网络访问,或用户的组织网络要求系统代理。
错误日志会对 URL 和敏感内容做 mask,避免订阅凭据进入日志。
五、自动更新与手动更新语义不同
should_update_profile() 会检查:
- 是否 remote;
- 是否有 URL;
allow_auto_update;- 调用者是否要求忽略自动更新限制。
自动更新遇到 Busy/Skipped 可以记录后等待下一轮;手动触发则需要把失败明确反馈给用户。update_config_with_force(is_manual) 正是把这两种用户预期传到配置刷新层。
六、激活 Profile 是一次核心配置替换
切换当前 Profile 的逻辑不是只改 current:
记录新的 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 } 记录。
前端选节点后做三件事:
- 通过 Mihomo API 切换组;
- 通知托盘同步;
- command
record_selected_node(groupName, node)更新 Profile。
command 只传一个变化,不传整份 selection list。后端在最新 Profile 上 merge,可避免两次快速选择形成 lost update。
八、恢复选择不是简单循环 PUT
核心刚启动时,代理组和 provider 可能尚未全部加载。restore_selected_nodes() 因此包含一个收敛过程:
- 读取保存的选择记录;
- 获取 Mihomo 当前 proxies 快照;
- 判断组是否存在、是否可选、节点是否属于该组;
- 已经选中则标记 settled;
- 未加载完整则等待第二次快照;
- 节点确认不存在时选择有效 fallback 或移除无效记录;
- 先执行仍合法的激活,再提交修复后的记录。
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 模型的设计启示
- 索引与大内容分离,让集合操作和内容更新互不放大。
- 网络策略参数化,同一下载器按 direct/self/system proxy 重试。
- 当前项切换是事务,不是赋值。
- 恢复外部系统状态要允许收敛时间,不要把第一次缺失当最终事实。
- 清理的证据门槛应高于创建,索引损坏时优先保护数据。
本章源码索引
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 commandssrc/pages/profiles.tsx、src/components/profile/:前端管理与编辑