from __future__ import annotations import json from sqlalchemy import delete, select from sqlalchemy.orm import Session, selectinload from app.db import get_session, init_db from app.db_models import ( AppSettingORM, PolicyGroupORM, ProfileORM, ProfileRuleModuleORM, ProfileSourceORM, RuleModuleORM, SourceORM, ) from app.models import AppConfig, ClientConfig, ProxyGroupConfig, RegionConfig, RuleConfig, SourceConfig from app.services.loader import load_yaml_app_config def _json_dumps(value) -> str: return json.dumps(value, ensure_ascii=False) def _json_loads(value: str | None, default): if not value: return default return json.loads(value) def import_yaml_config_to_db(config_path, *, replace_existing: bool = False) -> None: app_config = load_yaml_app_config(config_path) init_db() with get_session() as session: if replace_existing: _clear_config_tables(session) _upsert_setting(session, "public_path", app_config.public_path or "") source_rows: list[SourceORM] = [] for key, source in app_config.sources.items(): row = _get_or_create_source(session, key) row.enabled = source.enabled row.kind = source.kind row.url = source.url row.display_name = source.display_name row.headers_json = _json_dumps(source.headers) row.include_regex = source.include_regex row.exclude_regex = source.exclude_regex row.prefix = source.prefix row.suffix = source.suffix row.cache_ttl_seconds = source.cache_ttl_seconds source_rows.append(row) module_rows: list[RuleModuleORM] = [] ordered_rule_keys = list(app_config.rules.keys()) for key, rule in app_config.rules.items(): row = _get_or_create_rule_module(session, key) row.file_path = rule.file row.behavior = rule.behavior row.format = rule.format row.policy = rule.policy row.no_resolve = rule.no_resolve row.payload_json = _json_dumps(rule.payload) module_rows.append(row) session.flush() for client_key, client in app_config.clients.items(): profile = _get_or_create_profile(session, client_key) _populate_profile(profile, client) session.flush() _replace_profile_sources(session, profile.id, source_rows) _replace_profile_groups(session, profile.id, app_config) _replace_profile_rule_links(session, profile.id, module_rows, ordered_rule_keys) session.commit() def load_app_config_from_db() -> AppConfig | None: init_db() with get_session() as session: profiles = list( session.scalars( select(ProfileORM) .options( selectinload(ProfileORM.groups), selectinload(ProfileORM.rule_links).selectinload(ProfileRuleModuleORM.rule_module), ) .order_by(ProfileORM.key) ) ) sources = list(session.scalars(select(SourceORM).order_by(SourceORM.key))) if not profiles or not sources: return None public_path = _setting_value(session, "public_path") primary_profile = profiles[0] regions: dict[str, RegionConfig] = {} selector_groups: list[ProxyGroupConfig] = [] policy_groups: list[ProxyGroupConfig] = [] for group in sorted(primary_profile.groups, key=lambda item: (item.order_index, item.id)): if not group.enabled: continue if group.group_kind == "region": regions[_slugify(group.name)] = RegionConfig( name=group.name, filter=group.filter_regex or "", tolerance=group.tolerance or 50, ) continue target = selector_groups if group.group_kind == "selector" else policy_groups target.append( ProxyGroupConfig( name=group.name, type=group.type, proxies=_json_loads(group.proxies_json, []), filter=group.filter_regex, tolerance=group.tolerance, url=group.url, interval=group.interval, ) ) rules: dict[str, RuleConfig] = {} for link in sorted(primary_profile.rule_links, key=lambda item: (item.order_index, item.id)): if not link.enabled: continue module = link.rule_module rules[module.key] = RuleConfig( file=module.file_path, behavior=module.behavior, format=module.format, policy=link.policy_override or module.policy, no_resolve=module.no_resolve if link.no_resolve_override is None else link.no_resolve_override, payload=_json_loads(link.payload_override_json, _json_loads(module.payload_json, [])), ) return AppConfig( public_path=public_path or None, sources={ source.key: SourceConfig( enabled=source.enabled, kind=source.kind, url=source.url, display_name=source.display_name, headers=_json_loads(source.headers_json, {}), include_regex=source.include_regex, exclude_regex=source.exclude_regex, prefix=source.prefix, suffix=source.suffix, cache_ttl_seconds=source.cache_ttl_seconds, ) for source in sources }, rules=rules, clients={profile.key: _profile_to_client_config(profile) for profile in profiles}, regions=regions, selector_groups=selector_groups, policy_groups=policy_groups, ) def _clear_config_tables(session: Session) -> None: session.execute(delete(ProfileRuleModuleORM)) session.execute(delete(PolicyGroupORM)) session.execute(delete(ProfileSourceORM)) session.execute(delete(ProfileORM)) session.execute(delete(RuleModuleORM)) session.execute(delete(SourceORM)) session.execute(delete(AppSettingORM)) session.flush() def _upsert_setting(session: Session, key: str, value: str) -> None: row = session.scalar(select(AppSettingORM).where(AppSettingORM.key == key)) if row is None: row = AppSettingORM(key=key, value=value) session.add(row) else: row.value = value def _setting_value(session: Session, key: str) -> str | None: row = session.scalar(select(AppSettingORM).where(AppSettingORM.key == key)) if row is None: return None return row.value def _get_or_create_source(session: Session, key: str) -> SourceORM: row = session.scalar(select(SourceORM).where(SourceORM.key == key)) if row is None: row = SourceORM(key=key, kind="auto", url="") session.add(row) return row def _get_or_create_rule_module(session: Session, key: str) -> RuleModuleORM: row = session.scalar(select(RuleModuleORM).where(RuleModuleORM.key == key)) if row is None: row = RuleModuleORM(key=key, behavior="classical", format="text", policy="DIRECT") session.add(row) return row def _get_or_create_profile(session: Session, key: str) -> ProfileORM: row = session.scalar(select(ProfileORM).where(ProfileORM.key == key)) if row is None: row = ProfileORM( key=key, title=key, test_url="https://www.gstatic.com/generate_204", main_policy="πŸš€ θŠ‚η‚Ήι€‰ζ‹©", source_policy="☁️ ζœΊεœΊι€‰ζ‹©", mixed_auto_policy="♻️ θ‡ͺεŠ¨ι€‰ζ‹©", manual_policy="πŸš€ ζ‰‹εŠ¨εˆ‡ζ’", direct_policy="DIRECT", ) session.add(row) return row def _populate_profile(profile: ProfileORM, client: ClientConfig) -> None: profile.title = client.title profile.provider_interval = client.provider_interval profile.rule_interval = client.rule_interval profile.test_url = str(client.test_url) profile.test_interval = client.test_interval profile.main_policy = client.main_policy profile.source_policy = client.source_policy profile.mixed_auto_policy = client.mixed_auto_policy profile.manual_policy = client.manual_policy profile.direct_policy = client.direct_policy profile.mode = client.mode profile.allow_lan = client.allow_lan profile.ipv6 = client.ipv6 profile.mixed_port = client.mixed_port profile.socks_port = client.socks_port profile.log_level = client.log_level def _replace_profile_sources(session: Session, profile_id: int, source_rows: list[SourceORM]) -> None: session.execute(delete(ProfileSourceORM).where(ProfileSourceORM.profile_id == profile_id)) for order_index, source in enumerate(source_rows): session.add( ProfileSourceORM( profile_id=profile_id, source_id=source.id, order_index=order_index, enabled=True, ) ) def _replace_profile_groups(session: Session, profile_id: int, app_config: AppConfig) -> None: session.execute(delete(PolicyGroupORM).where(PolicyGroupORM.profile_id == profile_id)) order_index = 0 for region in app_config.regions.values(): session.add( PolicyGroupORM( profile_id=profile_id, group_kind="region", name=region.name, type="url-test", order_index=order_index, filter_regex=region.filter, tolerance=region.tolerance, enabled=True, ) ) order_index += 1 for group in app_config.selector_groups: session.add( PolicyGroupORM( profile_id=profile_id, group_kind="selector", name=group.name, type=group.type, order_index=order_index, proxies_json=_json_dumps(group.proxies), filter_regex=group.filter, tolerance=group.tolerance, url=str(group.url) if group.url else None, interval=group.interval, enabled=True, ) ) order_index += 1 for group in app_config.policy_groups: session.add( PolicyGroupORM( profile_id=profile_id, group_kind="policy", name=group.name, type=group.type, order_index=order_index, proxies_json=_json_dumps(group.proxies), filter_regex=group.filter, tolerance=group.tolerance, url=str(group.url) if group.url else None, interval=group.interval, enabled=True, ) ) order_index += 1 def _replace_profile_rule_links( session: Session, profile_id: int, module_rows: list[RuleModuleORM], ordered_rule_keys: list[str], ) -> None: session.execute(delete(ProfileRuleModuleORM).where(ProfileRuleModuleORM.profile_id == profile_id)) module_by_key = {module.key: module for module in module_rows} for order_index, key in enumerate(ordered_rule_keys): module = module_by_key[key] session.add( ProfileRuleModuleORM( profile_id=profile_id, rule_module_id=module.id, enabled=True, order_index=order_index, ) ) def _profile_to_client_config(profile: ProfileORM) -> ClientConfig: return ClientConfig( title=profile.title, provider_interval=profile.provider_interval, rule_interval=profile.rule_interval, test_url=profile.test_url, test_interval=profile.test_interval, main_policy=profile.main_policy, source_policy=profile.source_policy, mixed_auto_policy=profile.mixed_auto_policy, manual_policy=profile.manual_policy, direct_policy=profile.direct_policy, mode=profile.mode, allow_lan=profile.allow_lan, ipv6=profile.ipv6, mixed_port=profile.mixed_port, socks_port=profile.socks_port, log_level=profile.log_level, ) def _slugify(name: str) -> str: text = "".join(char.lower() if char.isalnum() else "-" for char in name).strip("-") while "--" in text: text = text.replace("--", "-") return text or "group"