// routing_profile.dart — 可配置分流档案(GET/POST /v1/me/routing)。 // // 对齐后端契约(server/internal/httpapi 分流端点): // {mode, builtin:{china_direct,lan_direct,private_via_tunnel}, rules:[...], final, // system_locked_domains:[...]}(FT-A 起响应含只读私有服务域名清单)。 // 手写 fromJson/toJson(项目无 json_serializable,仿 lib/models/me.dart)。 import 'package:flutter/foundation.dart' show listEquals; /// 分流模式。 /// 'rule' = 按规则(内置 + 自定义) | 'global' = 全局代理 | 'direct' = 全局直连。 typedef RoutingMode = String; /// 规则匹配类型。 const kRuleTypes = [ 'domain', 'domain_suffix', 'domain_keyword', 'ip_cidr', 'geoip', 'geosite', ]; /// 规则命中动作。 const kRuleActions = ['direct', 'proxy', 'reject']; /// 内置分流开关(与自定义规则叠加,内置项在服务端渲染时排在自定义规则前/后由服务端决定)。 class Builtin { const Builtin({ this.chinaDirect = true, this.lanDirect = true, this.privateViaTunnel = true, }); final bool chinaDirect; final bool lanDirect; final bool privateViaTunnel; factory Builtin.fromJson(Map m) => Builtin( chinaDirect: m['china_direct'] as bool? ?? true, lanDirect: m['lan_direct'] as bool? ?? true, privateViaTunnel: m['private_via_tunnel'] as bool? ?? true, ); Map toJson() => { 'china_direct': chinaDirect, 'lan_direct': lanDirect, 'private_via_tunnel': privateViaTunnel, }; Builtin copyWith({ bool? chinaDirect, bool? lanDirect, bool? privateViaTunnel, }) => Builtin( chinaDirect: chinaDirect ?? this.chinaDirect, lanDirect: lanDirect ?? this.lanDirect, privateViaTunnel: privateViaTunnel ?? this.privateViaTunnel, ); @override bool operator ==(Object other) => identical(this, other) || other is Builtin && runtimeType == other.runtimeType && chinaDirect == other.chinaDirect && lanDirect == other.lanDirect && privateViaTunnel == other.privateViaTunnel; @override int get hashCode => Object.hash(chinaDirect, lanDirect, privateViaTunnel); } /// copyWith 的哨兵值:用于区分「未传参(保留原值)」与「显式传 null(清空该字段)」, /// 仅 [RoutingRule.note] 需要(其它字段无「清空」语义,`??` 已够用)。 const _noteSentinel = Object(); /// 单条自定义分流规则。 /// /// **enabled 语义**(计划裁决):构造默认 `true`;[toJson] 恒输出 `enabled` 字段 /// (服务端不修正缺省值,契约靠客户端保证);[fromJson] 缺失时容错默认 `true`。 class RoutingRule { const RoutingRule({ required this.type, required this.value, required this.action, this.note, this.enabled = true, }); /// domain | domain_suffix | domain_keyword | ip_cidr | geoip | geosite final String type; final String value; /// direct | proxy | reject final String action; final String? note; final bool enabled; factory RoutingRule.fromJson(Map m) => RoutingRule( type: m['type'] as String? ?? '', value: m['value'] as String? ?? '', action: m['action'] as String? ?? 'proxy', note: m['note'] as String?, enabled: m['enabled'] as bool? ?? true, ); Map toJson() => { 'type': type, 'value': value, 'action': action, if (note != null) 'note': note, 'enabled': enabled, }; /// [note] 用哨兵:不传 → 保留原值;显式传 `null` → 清空为 null。 RoutingRule copyWith({ String? type, String? value, String? action, Object? note = _noteSentinel, bool? enabled, }) => RoutingRule( type: type ?? this.type, value: value ?? this.value, action: action ?? this.action, note: identical(note, _noteSentinel) ? this.note : note as String?, enabled: enabled ?? this.enabled, ); @override bool operator ==(Object other) => identical(this, other) || other is RoutingRule && runtimeType == other.runtimeType && type == other.type && value == other.value && action == other.action && note == other.note && enabled == other.enabled; @override int get hashCode => Object.hash(type, value, action, note, enabled); } /// 分流档案(账户级,单份)。 class RoutingProfile { const RoutingProfile({ this.mode = 'rule', this.builtin = const Builtin(), this.rules = const [], this.finalAction = 'proxy', this.systemLockedDomains = const [], }); final RoutingMode mode; final Builtin builtin; final List rules; /// 兜底动作(JSON key 为保留字 `final`,Dart 侧改名)。'proxy' | 'direct'。 final String finalAction; /// **只读**:系统强制走隧道的私有服务域名清单(服务端 `PANGOLIN_PRIVATE_SPLIT_DOMAINS`, /// FT-A 起随 GET /v1/me/routing 下发)。`toJson()` 不输出——服务端忽略,且此字段 /// 客户端不可写。 final List systemLockedDomains; factory RoutingProfile.fromJson(Map m) => RoutingProfile( mode: m['mode'] as String? ?? 'rule', builtin: m['builtin'] != null ? Builtin.fromJson(m['builtin'] as Map) : const Builtin(), rules: ((m['rules'] as List?) ?? const []) .map((e) => RoutingRule.fromJson(e as Map)) .toList(), finalAction: m['final'] as String? ?? 'proxy', systemLockedDomains: (m['system_locked_domains'] as List?)?.cast() ?? const [], ); Map toJson() => { 'mode': mode, 'builtin': builtin.toJson(), 'rules': rules.map((r) => r.toJson()).toList(), 'final': finalAction, }; /// 出厂默认档案(对齐服务端 `routing.Default()`):智能分流 + 内置全开 + 无自定义规则。 /// 不含 systemLockedDomains(那是账户相关的只读回显,由调用方按需保留;见 /// RoutingProfileNotifier.resetToDefault())。 factory RoutingProfile.defaults() => const RoutingProfile( mode: 'rule', builtin: Builtin(), rules: [], finalAction: 'proxy', ); RoutingProfile copyWith({ RoutingMode? mode, Builtin? builtin, List? rules, String? finalAction, List? systemLockedDomains, }) => RoutingProfile( mode: mode ?? this.mode, builtin: builtin ?? this.builtin, rules: rules ?? this.rules, finalAction: finalAction ?? this.finalAction, systemLockedDomains: systemLockedDomains ?? this.systemLockedDomains, ); @override bool operator ==(Object other) => identical(this, other) || other is RoutingProfile && runtimeType == other.runtimeType && mode == other.mode && builtin == other.builtin && listEquals(rules, other.rules) && finalAction == other.finalAction && listEquals(systemLockedDomains, other.systemLockedDomains); @override int get hashCode => Object.hash( mode, builtin, Object.hashAll(rules), finalAction, Object.hashAll(systemLockedDomains), ); }