Full Code of xishang0128/clash-meta-party for AI

master e8d5e6cb6a99 cached
237 files
3.3 MB
875.6k tokens
878 symbols
1 requests
Download .txt
Showing preview only (3,517K chars total). Download the full file or copy to clipboard to get everything.
Repository: xishang0128/clash-meta-party
Branch: master
Commit: e8d5e6cb6a99
Files: 237
Total size: 3.3 MB

Directory structure:
gitextract_1hpos0hd/

├── .editorconfig
├── .github/
│   ├── ISSUE_TEMPLATE/
│   │   ├── bug_report_zh.yml
│   │   ├── config.yml
│   │   └── feature_request_zh.yml
│   └── workflows/
│       ├── build.yml
│       └── issues.yml
├── .gitignore
├── .npmrc
├── .prettierignore
├── .prettierrc.yaml
├── .vscode/
│   ├── extensions.json
│   ├── launch.json
│   └── settings.json
├── LICENSE
├── README.md
├── aur/
│   ├── sparkle/
│   │   ├── PKGBUILD
│   │   ├── sparkle.install
│   │   └── sparkle.sh
│   ├── sparkle-bin/
│   │   ├── PKGBUILD
│   │   ├── sparkle.install
│   │   └── sparkle.sh
│   ├── sparkle-electron/
│   │   ├── PKGBUILD
│   │   ├── sparkle.desktop
│   │   ├── sparkle.install
│   │   └── sparkle.sh
│   ├── sparkle-electron-bin/
│   │   ├── PKGBUILD
│   │   ├── sparkle.desktop
│   │   ├── sparkle.install
│   │   └── sparkle.sh
│   ├── sparkle-electron-git/
│   │   ├── PKGBUILD
│   │   ├── sparkle.desktop
│   │   ├── sparkle.install
│   │   └── sparkle.sh
│   └── sparkle-git/
│       ├── PKGBUILD
│       ├── sparkle.install
│       └── sparkle.sh
├── build/
│   ├── entitlements.mac.plist
│   ├── icon.icns
│   ├── installer.nsh
│   ├── linux/
│   │   ├── postinst
│   │   └── preinst
│   └── pkg-scripts/
│       ├── postinstall
│       └── preinstall
├── changelog.md
├── electron-builder.yml
├── electron.vite.config.ts
├── eslint.config.cjs
├── package.json
├── patches/
│   └── vite-plugin-monaco-editor@1.1.0.patch
├── scripts/
│   ├── checksum.ts
│   ├── package.json
│   ├── prepare.ts
│   ├── telegram.ts
│   └── updater.ts
├── src/
│   ├── main/
│   │   ├── config/
│   │   │   ├── app.ts
│   │   │   ├── controledMihomo.ts
│   │   │   ├── index.ts
│   │   │   ├── override.ts
│   │   │   └── profile.ts
│   │   ├── core/
│   │   │   ├── factory.ts
│   │   │   ├── manager.ts
│   │   │   ├── mihomoApi.ts
│   │   │   ├── network.ts
│   │   │   ├── permission-check.ts
│   │   │   ├── permission.ts
│   │   │   ├── process-control.ts
│   │   │   ├── profile-check.ts
│   │   │   ├── profileUpdater.ts
│   │   │   ├── startup-chain.ts
│   │   │   ├── startupHook.ts
│   │   │   └── subStoreApi.ts
│   │   ├── index.ts
│   │   ├── resolve/
│   │   │   ├── autoUpdater.ts
│   │   │   ├── backup.ts
│   │   │   ├── floatingWindow.ts
│   │   │   ├── gistApi.ts
│   │   │   ├── menu.ts
│   │   │   ├── server.ts
│   │   │   ├── shortcut.ts
│   │   │   ├── theme.ts
│   │   │   ├── trafficMonitor.ts
│   │   │   └── tray.ts
│   │   ├── service/
│   │   │   ├── api.ts
│   │   │   ├── auth-store.ts
│   │   │   ├── key.ts
│   │   │   └── manager.ts
│   │   ├── sys/
│   │   │   ├── autoRun.ts
│   │   │   ├── interface.ts
│   │   │   ├── misc.ts
│   │   │   ├── ssid.ts
│   │   │   └── sysproxy.ts
│   │   └── utils/
│   │       ├── appName.ts
│   │       ├── calc.ts
│   │       ├── defaultIcon.ts
│   │       ├── devicePathResolver.ts
│   │       ├── dirs.ts
│   │       ├── elevation.ts
│   │       ├── encrypt.ts
│   │       ├── icon.ts
│   │       ├── init.ts
│   │       ├── ipc.ts
│   │       ├── log.ts
│   │       ├── merge.ts
│   │       ├── template.ts
│   │       ├── userAgent.ts
│   │       └── yaml.ts
│   ├── preload/
│   │   ├── index.d.ts
│   │   └── index.ts
│   ├── renderer/
│   │   ├── floating.html
│   │   ├── index.html
│   │   ├── src/
│   │   │   ├── App.tsx
│   │   │   ├── FloatingApp.tsx
│   │   │   ├── TrayMenuApp.tsx
│   │   │   ├── assets/
│   │   │   │   ├── floating.css
│   │   │   │   ├── hero.mjs
│   │   │   │   ├── main.css
│   │   │   │   └── traymenu.css
│   │   │   ├── components/
│   │   │   │   ├── base/
│   │   │   │   │   ├── base-confirm.tsx
│   │   │   │   │   ├── base-editor-lazy.tsx
│   │   │   │   │   ├── base-editor.tsx
│   │   │   │   │   ├── base-error-boundary.tsx
│   │   │   │   │   ├── base-list-editor.tsx
│   │   │   │   │   ├── base-page.tsx
│   │   │   │   │   ├── base-qrcode-modal.tsx
│   │   │   │   │   ├── base-setting-card.tsx
│   │   │   │   │   ├── base-setting-item.tsx
│   │   │   │   │   ├── border-switch.css
│   │   │   │   │   ├── border-swtich.tsx
│   │   │   │   │   ├── collapse-input.tsx
│   │   │   │   │   ├── interface-select.tsx
│   │   │   │   │   ├── mihomo-icon.tsx
│   │   │   │   │   └── substore-icon.tsx
│   │   │   │   ├── connections/
│   │   │   │   │   ├── connection-detail-modal.tsx
│   │   │   │   │   ├── connection-item.tsx
│   │   │   │   │   └── connection-setting-modal.tsx
│   │   │   │   ├── dns/
│   │   │   │   │   └── advanced-dns-setting.tsx
│   │   │   │   ├── logs/
│   │   │   │   │   └── log-item.tsx
│   │   │   │   ├── mihomo/
│   │   │   │   │   ├── advanced-settings.tsx
│   │   │   │   │   ├── controller-setting.tsx
│   │   │   │   │   ├── env-setting.tsx
│   │   │   │   │   ├── interface-modal.tsx
│   │   │   │   │   ├── log-setting.tsx
│   │   │   │   │   ├── permission-modal.tsx
│   │   │   │   │   ├── port-setting.tsx
│   │   │   │   │   └── service-modal.tsx
│   │   │   │   ├── override/
│   │   │   │   │   ├── edit-file-modal.tsx
│   │   │   │   │   ├── edit-info-modal.tsx
│   │   │   │   │   ├── exec-log-modal.tsx
│   │   │   │   │   └── override-item.tsx
│   │   │   │   ├── profiles/
│   │   │   │   │   ├── edit-file-modal.tsx
│   │   │   │   │   ├── edit-info-modal.tsx
│   │   │   │   │   ├── profile-item.tsx
│   │   │   │   │   └── profile-setting-modal.tsx
│   │   │   │   ├── proxies/
│   │   │   │   │   ├── proxy-item.tsx
│   │   │   │   │   └── proxy-setting-modal.tsx
│   │   │   │   ├── resources/
│   │   │   │   │   ├── geo-data.tsx
│   │   │   │   │   ├── proxy-provider.tsx
│   │   │   │   │   ├── rule-provider.tsx
│   │   │   │   │   └── viewer.tsx
│   │   │   │   ├── rules/
│   │   │   │   │   └── rule-item.tsx
│   │   │   │   ├── settings/
│   │   │   │   │   ├── actions.tsx
│   │   │   │   │   ├── advanced-settings.tsx
│   │   │   │   │   ├── appearance-confis.tsx
│   │   │   │   │   ├── css-editor-modal.tsx
│   │   │   │   │   ├── general-config.tsx
│   │   │   │   │   ├── shortcut-config.tsx
│   │   │   │   │   ├── sider-config.tsx
│   │   │   │   │   ├── substore-config.tsx
│   │   │   │   │   ├── webdav-config.tsx
│   │   │   │   │   └── webdav-restore-modal.tsx
│   │   │   │   ├── sider/
│   │   │   │   │   ├── config-viewer.tsx
│   │   │   │   │   ├── conn-card.tsx
│   │   │   │   │   ├── dns-card.tsx
│   │   │   │   │   ├── log-card.tsx
│   │   │   │   │   ├── mihomo-core-card.tsx
│   │   │   │   │   ├── outbound-mode-switcher.tsx
│   │   │   │   │   ├── override-card.tsx
│   │   │   │   │   ├── profile-card.tsx
│   │   │   │   │   ├── proxy-card.tsx
│   │   │   │   │   ├── resource-card.tsx
│   │   │   │   │   ├── rule-card.tsx
│   │   │   │   │   ├── sniff-card.tsx
│   │   │   │   │   ├── substore-card.tsx
│   │   │   │   │   ├── sysproxy-switcher.tsx
│   │   │   │   │   ├── traffic-chart.tsx
│   │   │   │   │   └── tun-switcher.tsx
│   │   │   │   ├── sysproxy/
│   │   │   │   │   ├── bypass-editor-modal.tsx
│   │   │   │   │   └── pac-editor-modal.tsx
│   │   │   │   └── updater/
│   │   │   │       ├── updater-button.tsx
│   │   │   │       └── updater-modal.tsx
│   │   │   ├── floating.tsx
│   │   │   ├── hooks/
│   │   │   │   ├── use-app-config.tsx
│   │   │   │   ├── use-card-dnd-sensors.ts
│   │   │   │   ├── use-controled-mihomo-config.tsx
│   │   │   │   ├── use-groups.tsx
│   │   │   │   ├── use-override-config.tsx
│   │   │   │   ├── use-profile-config.tsx
│   │   │   │   └── use-rules.tsx
│   │   │   ├── main.tsx
│   │   │   ├── pages/
│   │   │   │   ├── connections.tsx
│   │   │   │   ├── dns.tsx
│   │   │   │   ├── logs.tsx
│   │   │   │   ├── mihomo.tsx
│   │   │   │   ├── override.tsx
│   │   │   │   ├── profiles.tsx
│   │   │   │   ├── proxies.tsx
│   │   │   │   ├── resources.tsx
│   │   │   │   ├── rules.tsx
│   │   │   │   ├── settings.tsx
│   │   │   │   ├── sniffer.tsx
│   │   │   │   ├── substore.tsx
│   │   │   │   ├── syspeoxy.tsx
│   │   │   │   └── tun.tsx
│   │   │   ├── routes/
│   │   │   │   └── index.tsx
│   │   │   ├── traymenu.tsx
│   │   │   └── utils/
│   │   │       ├── advanced-filter.ts
│   │   │       ├── calc.ts
│   │   │       ├── connection-filter-autocomplete.ts
│   │   │       ├── debounce.ts
│   │   │       ├── delay-test.ts
│   │   │       ├── driver.ts
│   │   │       ├── env.d.ts
│   │   │       ├── hash.ts
│   │   │       ├── image.ts
│   │   │       ├── includes.ts
│   │   │       ├── init.ts
│   │   │       ├── ipc.ts
│   │   │       ├── mihomo-log-store.ts
│   │   │       └── validate.ts
│   │   └── traymenu.html
│   └── shared/
│       └── types/
│           ├── app.d.ts
│           ├── controller.d.ts
│           ├── mihomo.d.ts
│           └── types.d.ts
├── tsconfig.json
├── tsconfig.node.json
└── tsconfig.web.json

================================================
FILE CONTENTS
================================================

================================================
FILE: .editorconfig
================================================
root = true

[*]
charset = utf-8
indent_style = space
indent_size = 2
end_of_line = lf
insert_final_newline = true
trim_trailing_whitespace = true

================================================
FILE: .github/ISSUE_TEMPLATE/bug_report_zh.yml
================================================
description: 提交 sparkle 漏洞
name: 错误反馈
title: '[Bug] '
body:
  - attributes:
      description: 在提交之前,请执行并勾选以下所有选项以证明您已经阅读并理解了以下要求,否则该 issue 将被关闭。
      label: 验证步骤
      options:
        - label: 我已在标题简短的描述了我所遇到的问题
        - label: 我使用中文正确地描述了我所遇到的问题
        - label: 我已在 [Issues](https://github.com/xishang0128/sparkle/issues?q=is%3Aissue) 中寻找过我要提出的问题,但未找到相同的问题
        - label: 我已在 [常见问题](https://mihomo.party/docs/issues/common) 中寻找过我要提出的问题,并没有找到答案
        - label: 这是 GUI 程序的问题,而不是内核程序的问题
        - label: 我已经关闭所有杀毒软件/代理软件后测试过,问题依旧存在
        - label: 我已经使用最新的测试版本测试过,问题依旧存在
        - label: 我已经尝试过重启程序/电脑,问题依旧存在
        - label: 我已确认在其他设备(例如全新虚拟机)上该问题依旧存在
        - label: 这是一个明确的 bug,而不是一个功能请求或者使用问题,或者询问性质的问题
    id: ensure
    type: checkboxes
  - attributes:
      description: 请提供操作系统类型
      label: 操作系统
      multiple: true
      options:
        - MacOS
        - Windows
        - Linux
    type: dropdown
    validations:
      required: true
  - attributes:
      description: 请提供出现问题的 sparkle 版本,需要具体到小版本号,如果没有该 issues 将会被直接关闭
      label: 发生问题的 sparkle 版本
    type: input
    validations:
      required: true
  - attributes:
      description: 请安装并执行`fastfetch --logo none`或者`neofetch --off`,将输出结果粘贴在下面,如果没有该 issues 将会被直接关闭
      label: 系统信息
    type: textarea
    validations:
      required: true
  - attributes:
      description: 请提供错误的详细描述,如果没有该 issues 将会被直接关闭
      label: 描述
    type: textarea
    validations:
      required: true
  - attributes:
      description: 请提供重现错误的步骤,且可以在全新系统环境中复现,如果没有该 issues 将会被直接关闭
      label: 重现方式
    type: textarea
    validations:
      required: true


================================================
FILE: .github/ISSUE_TEMPLATE/config.yml
================================================
---
blank_issues_enabled: false
contact_links:
  - about: 提出问题前请先查看常见问题
    name: 常见问题
    url: https://mihomo.party/docs/issues/common
  - about: 提问/讨论性质的问题请勿提交 issue
    name: 交流群组
    url: https://t.me/+y7rcYjEKIiI1NzZl


================================================
FILE: .github/ISSUE_TEMPLATE/feature_request_zh.yml
================================================
description: 请求 sparkle 未实现的功能
name: 功能请求
title: '[Feature] '
body:
  - attributes:
      description: 在提交之前,请执行并勾选以下所有选项以证明您已经阅读并理解了以下要求,否则该 issue 将被关闭。
      label: 验证步骤
      options:
        - label: 我已在标题简短的描述了我所需的功能
        - label: 我已在 [Issues](https://github.com/xishang0128/sparkle/issues?q=is%3Aissue) 中寻找过,但未找到我所需的功能
        - label: 这是向 GUI 程序提出的功能请求,而不是内核程序
        - label: 我未在最新的测试版本找到我所需的功能
    id: ensure
    type: checkboxes
  - attributes:
      description: 请提供操作系统类型
      label: 操作系统
      multiple: true
      options:
        - MacOS
        - Windows
        - Linux
    type: dropdown
    validations:
      required: true
  - attributes:
      description: 请提供所需功能的详细描述
      label: 描述
    type: textarea
    validations:
      required: true


================================================
FILE: .github/workflows/build.yml
================================================
name: Build
permissions: write-all
on:
  workflow_dispatch:
    inputs:
      version:
        description: 'Tag version to release'
        required: false
  push:
    branches:
      - master
    tags:
      - v*
    paths-ignore:
      - 'README.md'
      - '.github/ISSUE_TEMPLATE/**'
      - '.github/workflows/issues.yml'

jobs:
  build:
    runs-on: ${{ matrix.os }}
    strategy:
      matrix:
        include:
          - os: windows-latest
            arch: x64
            format: 7z
          - os: windows-latest
            arch: x64
            format: nsis
          - os: windows-latest
            arch: arm64
            format: 7z
          - os: windows-latest
            arch: arm64
            format: nsis

          - os: ubuntu-latest
            arch: x64
            format: deb
          - os: ubuntu-latest
            arch: x64
            format: rpm
          - os: ubuntu-latest
            arch: x64
            format: pacman
          - os: ubuntu-latest
            arch: arm64
            format: deb
          - os: ubuntu-latest
            arch: arm64
            format: rpm
          - os: ubuntu-latest
            arch: arm64
            format: pacman
          - os: ubuntu-latest
            arch: loong64
            format: deb
          - os: ubuntu-latest
            arch: loong64
            format: rpm
          - os: ubuntu-latest
            arch: loong64
            format: pacman

          - os: macos-latest
            arch: x64
          - os: macos-latest
            arch: arm64

    name: Build ${{ matrix.os }}-${{ matrix.arch }}${{ matrix.format && '-' || '' }}${{ matrix.format || '' }}

    steps:
      - name: Checkout
        uses: actions/checkout@v6

      - name: Setup Node
        uses: actions/setup-node@v6
        with:
          node-version: 25

      - name: Setup pnpm
        run: npm install -g pnpm

      - name: Determine and Update Version
        shell: bash
        run: |
          GIT_COMMIT_HASH=$(git rev-parse --short HEAD)
          VERSION=$(jq -r '.version' package.json)

          if [ "${{ github.event.inputs.version }}" == "" ]; then
            BASE_VERSION=$(echo $VERSION | awk -F. '{print $1"."$2"."$3+1}')
            VERSION_PREFIX=${VERSION_PREFIX:-beta}
            RELEASE_VERSION="${BASE_VERSION}-${VERSION_PREFIX}-${GIT_COMMIT_HASH}"
          else
            INPUT_VERSION="${{ github.event.inputs.version }}"
            CLEAN_VERSION=$(echo "$INPUT_VERSION" | sed 's/^v//')
            if [[ ! "$CLEAN_VERSION" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]]; then
              echo "Invalid version format: $INPUT_VERSION"
              exit 1
            fi
            RELEASE_VERSION="$CLEAN_VERSION"
          fi

          jq --arg version "$RELEASE_VERSION" '.version = $version' package.json > tmp.json && mv tmp.json package.json

      - name: Keep product name lowercase on Linux
        if: matrix.os == 'ubuntu-latest'
        run: |
          sed -i "s/productName: Sparkle/productName: sparkle/" electron-builder.yml

      - name: Install Build Tools
        if: matrix.os == 'ubuntu-latest' && matrix.format == 'pacman'
        run: |
          sudo apt-get update && sudo apt-get install -y libarchive-tools

      - name: Adaptation to the loongarch64 architecture
        env:
          npm_config_arch: ${{ matrix.arch }}
          npm_config_target_arch: ${{ matrix.arch }}
        if: matrix.os == 'ubuntu-latest' && matrix.arch == 'loong64'
        shell: bash
        run: |
          export ELECTRON_MIRROR="https://github.com/darkyzhou/electron-loong64/releases/download/"
          export electron_use_remote_checksums=1
          SKIP_PREPARE=1 pnpm install electron@39.2.7 -D
          pnpm remove electron-builder
          SKIP_PREPARE=1 pnpm install @loongdotjs/electron-builder@26.8.2-loong1 -D

      - name: Install Dependencies and Prepare
        env:
          npm_config_arch: ${{ matrix.arch }}
          npm_config_target_arch: ${{ matrix.arch }}
        shell: bash
        run: |
          SKIP_PREPARE=1 pnpm install
          pnpm prepare --${{ matrix.arch }}

      - name: Build
        env:
          npm_config_arch: ${{ matrix.arch }}
          npm_config_target_arch: ${{ matrix.arch }}
        shell: bash
        run: |
          if [[ "${{ matrix.os }}" == "windows-latest" ]]; then
            pnpm build:win ${{ matrix.format }} --${{ matrix.arch }}
          elif [[ "${{ matrix.os }}" == "ubuntu-latest" ]]; then
            if [[ "${{ matrix.arch }}" == "loong64" ]]; then
              export ELECTRON_MIRROR="https://github.com/darkyzhou/electron-loong64/releases/download/"
              export electron_use_remote_checksums=1
              pnpm build:linux ${{ matrix.format }} --${{ matrix.arch }}
            else
              pnpm build:linux ${{ matrix.format }} --${{ matrix.arch }}
            fi
          else
            pnpm build:mac --${{ matrix.arch }}
          fi

      - name: Add Portable Flag
        if: matrix.os == 'windows-latest' && matrix.format == '7z'
        shell: pwsh
        run: |
          New-Item -Path "PORTABLE" -ItemType File
          Get-ChildItem dist/*portable.7z | ForEach-Object {
            7z a $_.FullName PORTABLE
          }

      - name: Create Changelog
        env:
          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
        run: |
          git log -1 --pretty=format:"%s%n%b" > changelog.md

      - name: Generate latest.yml
        shell: bash
        run: SKIP_CHANGELOG=1 pnpm updater

      - name: Upload Artifacts
        uses: actions/upload-artifact@v7
        with:
          name: ${{ matrix.os }}-${{ matrix.arch }}${{ matrix.format && '-' || '' }}${{ matrix.format || '' }}
          path: |
            dist/sparkle*
            !dist/sparkle.app.plist
            !dist/sparkle*blockmap*
            latest.yml
            changelog.md

  pre-release:
    needs: [build]
    runs-on: ubuntu-latest
    steps:
      - name: Checkout
        uses: actions/checkout@v6

      - name: Download Artifacts
        uses: actions/download-artifact@v7
        with:
          path: bin/
          merge-multiple: true

      - name: Delete current release assets
        uses: 8Mi-Tech/delete-release-assets-action@main
        with:
          github_token: ${{ secrets.GITHUB_TOKEN }}
          tag: pre-release
          deleteOnlyFromDrafts: false

      - name: Tag Repo
        uses: richardsimko/update-tag@v1
        with:
          tag_name: pre-release
        env:
          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

      - name: Publish Prerelease
        if: success()
        uses: softprops/action-gh-release@v2
        with:
          tag_name: pre-release
          body_path: bin/changelog.md
          files: |
            bin/latest.yml
            bin/dist/*
          prerelease: true

  release:
    if: startsWith(github.ref, 'refs/tags/v') || (github.event_name == 'workflow_dispatch' && github.event.inputs.version != '')
    needs: [build]
    runs-on: ubuntu-latest
    steps:
      - name: Checkout
        uses: actions/checkout@v6
        with:
          fetch-depth: '0'
          fetch-tags: 'true'

      - name: Setup Node
        uses: actions/setup-node@v6
        with:
          node-version: 25

      - name: Generate Changelog
        env:
          API_URL: ${{ secrets.API_URL }}
          API_KEY: ${{ secrets.API_KEY }}
        run: |
          TEXT=$(git log $(jq -r .version package.json)..HEAD --pretty=format:%s | grep -v '[0-9]\+\.[0-9]\+\.[0-9]\+$' | sed ':a;N;$!ba;s/\n/\\n/g')
          QUESTION="Translate into Chinese:\n"""\n$TEXT\n""""
          PAYLOAD=$(cat <<EOF
          {
            "stream": false,
            "model": "gpt-5-mini",
            "messages": [
              {
                "role": "system",
                "content": "You are a professional translation engine, please translate the text into a colloquial, professional, elegant and fluent content, without the style of machine translation. You must only translate the text content, never interpret it."
              },
              {
                "role": "user",
                "content": "$QUESTION"
              }
            ]
          }
          EOF
          )
          RESPONSE=$(curl -s -X POST "$API_URL/v1/chat/completions" \
            -H "Authorization: Bearer $API_KEY" \
            -H "Content-Type: application/json" \
            -d "$PAYLOAD" \
            --max-time 30)
          echo '# 更新日志' > changelog.md
          echo $RESPONSE | jq .choices[0].message.content -r | sed 's/\\n/\n/g' | sed 's/^/ - /g' >> changelog.md

      - name: Update Version
        run: |
          INPUT_VERSION="${{ github.event.inputs.version }}"
          CLEAN_VERSION=$(echo "$INPUT_VERSION" | sed 's/^v//')
          if [[ ! "$CLEAN_VERSION" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]]; then
            echo "Invalid version format: $INPUT_VERSION"
            exit 1
          fi
          jq --arg version "$CLEAN_VERSION" '.version = $version' package.json > tmp.json && mv tmp.json package.json

          git config --global user.email "github-actions[bot]@users.noreply.github.com"
          git config --global user.name "github-actions[bot]"
          git add package.json
          git commit -m "update version to $CLEAN_VERSION"
          git push

      - name: Install Dependencies and Updater
        run: |
          npm install -g pnpm
          SKIP_PREPARE=1 pnpm install
          pnpm updater

      - uses: actions/download-artifact@v7
        with:
          path: bin/
          merge-multiple: true

      - name: Delete Current
        uses: 8Mi-Tech/delete-release-assets-action@main
        with:
          github_token: ${{ secrets.GITHUB_TOKEN }}
          tag: ${{ github.event.inputs.version != '' && github.event.inputs.version || github.ref }}
          deleteOnlyFromDrafts: false

      - name: Publish Release
        if: success()
        uses: softprops/action-gh-release@v2
        with:
          tag_name: ${{ github.event.inputs.version != '' && github.event.inputs.version || github.ref }}
          body_path: changelog.md
          files: |
            latest.yml
            bin/dist/*


================================================
FILE: .github/workflows/issues.yml
================================================
name: Review Issues

on:
  # issues:
  #   types: [opened]
  workflow_dispatch:
  # disable issue event

jobs:
  review:
    runs-on: ubuntu-latest
    steps:
      - name: Generate Token
        uses: tibdex/github-app-token@v2
        id: generate
        with:
          app_id: ${{ secrets.BOT_APP_ID }}
          private_key: ${{ secrets.BOT_PRIVATE_KEY }}
      - name: Review Issues
        uses: mihomo-party-org/universal-assistant@v1.0.3
        with:
          github_token: ${{ steps.generate.outputs.token }}
          openai_base_url: ${{ secrets.OPENAI_BASE_URL }}
          openai_api_key: ${{ secrets.OPENAI_API_KEY }}
          openai_model: ${{ vars.OPENAI_MODEL }}
          system_prompt: ${{ vars.SYSTEM_PROMPT }}
          available_tools: ${{ vars.AVAILABLE_TOOLS }}
          user_input: |
            请审查如下 Issue:
            标题:"${{ github.event.issue.title }}"
            内容:"${{ github.event.issue.body }}"


================================================
FILE: .gitignore
================================================
node_modules
resources/files
resources/sidecar
extra
dist
out
.DS_Store
*.log*
.idea
*.ttf


================================================
FILE: .npmrc
================================================
shamefully-hoist=true
virtual-store-dir-max-length=80
public-hoist-pattern[]=*@heroui/*


================================================
FILE: .prettierignore
================================================
out
dist
pnpm-lock.yaml
LICENSE.md
tsconfig.json
tsconfig.*.json


================================================
FILE: .prettierrc.yaml
================================================
singleQuote: true
semi: false
printWidth: 100
trailingComma: none


================================================
FILE: .vscode/extensions.json
================================================
{
  "recommendations": [
    "dbaeumer.vscode-eslint",
    "esbenp.prettier-vscode",
    "huacnlee.autocorrect",
    "bradlc.vscode-tailwindcss"
  ]
}


================================================
FILE: .vscode/launch.json
================================================
{
  "version": "0.2.0",
  "configurations": [
    {
      "name": "Debug Main Process",
      "type": "node",
      "request": "launch",
      "cwd": "${workspaceRoot}",
      "runtimeExecutable": "${workspaceRoot}/node_modules/.bin/electron-vite",
      "windows": {
        "runtimeExecutable": "${workspaceRoot}/node_modules/.bin/electron-vite.cmd"
      },
      "runtimeArgs": ["--sourcemap"],
      "env": {
        "REMOTE_DEBUGGING_PORT": "9222"
      }
    },
    {
      "name": "Debug Renderer Process",
      "port": 9222,
      "request": "attach",
      "type": "chrome",
      "webRoot": "${workspaceFolder}/src/renderer",
      "timeout": 60000,
      "presentation": {
        "hidden": true
      }
    }
  ],
  "compounds": [
    {
      "name": "Debug All",
      "configurations": ["Debug Main Process", "Debug Renderer Process"],
      "presentation": {
        "order": 1
      }
    }
  ]
}


================================================
FILE: .vscode/settings.json
================================================
{
  "[typescript]": {
    "editor.defaultFormatter": "esbenp.prettier-vscode"
  },
  "[typescriptreact]": {
    "editor.defaultFormatter": "esbenp.prettier-vscode"
  },
  "[javascript]": {
    "editor.defaultFormatter": "esbenp.prettier-vscode"
  },
  "[json]": {
    "editor.defaultFormatter": "esbenp.prettier-vscode"
  },
  "[yaml]": {
    "editor.defaultFormatter": null
  },
  "[yml]": {
    "editor.defaultFormatter": null
  },
  "editor.formatOnSave": true,
  "css.lint.unknownAtRules": "ignore"
}


================================================
FILE: LICENSE
================================================
                    GNU GENERAL PUBLIC LICENSE
                       Version 3, 29 June 2007

 Copyright (C) 2007 Free Software Foundation, Inc. <https://fsf.org/>
 Everyone is permitted to copy and distribute verbatim copies
 of this license document, but changing it is not allowed.

                            Preamble

  The GNU General Public License is a free, copyleft license for
software and other kinds of works.

  The licenses for most software and other practical works are designed
to take away your freedom to share and change the works.  By contrast,
the GNU General Public License is intended to guarantee your freedom to
share and change all versions of a program--to make sure it remains free
software for all its users.  We, the Free Software Foundation, use the
GNU General Public License for most of our software; it applies also to
any other work released this way by its authors.  You can apply it to
your programs, too.

  When we speak of free software, we are referring to freedom, not
price.  Our General Public Licenses are designed to make sure that you
have the freedom to distribute copies of free software (and charge for
them if you wish), that you receive source code or can get it if you
want it, that you can change the software or use pieces of it in new
free programs, and that you know you can do these things.

  To protect your rights, we need to prevent others from denying you
these rights or asking you to surrender the rights.  Therefore, you have
certain responsibilities if you distribute copies of the software, or if
you modify it: responsibilities to respect the freedom of others.

  For example, if you distribute copies of such a program, whether
gratis or for a fee, you must pass on to the recipients the same
freedoms that you received.  You must make sure that they, too, receive
or can get the source code.  And you must show them these terms so they
know their rights.

  Developers that use the GNU GPL protect your rights with two steps:
(1) assert copyright on the software, and (2) offer you this License
giving you legal permission to copy, distribute and/or modify it.

  For the developers' and authors' protection, the GPL clearly explains
that there is no warranty for this free software.  For both users' and
authors' sake, the GPL requires that modified versions be marked as
changed, so that their problems will not be attributed erroneously to
authors of previous versions.

  Some devices are designed to deny users access to install or run
modified versions of the software inside them, although the manufacturer
can do so.  This is fundamentally incompatible with the aim of
protecting users' freedom to change the software.  The systematic
pattern of such abuse occurs in the area of products for individuals to
use, which is precisely where it is most unacceptable.  Therefore, we
have designed this version of the GPL to prohibit the practice for those
products.  If such problems arise substantially in other domains, we
stand ready to extend this provision to those domains in future versions
of the GPL, as needed to protect the freedom of users.

  Finally, every program is threatened constantly by software patents.
States should not allow patents to restrict development and use of
software on general-purpose computers, but in those that do, we wish to
avoid the special danger that patents applied to a free program could
make it effectively proprietary.  To prevent this, the GPL assures that
patents cannot be used to render the program non-free.

  The precise terms and conditions for copying, distribution and
modification follow.

                       TERMS AND CONDITIONS

  0. Definitions.

  "This License" refers to version 3 of the GNU General Public License.

  "Copyright" also means copyright-like laws that apply to other kinds of
works, such as semiconductor masks.

  "The Program" refers to any copyrightable work licensed under this
License.  Each licensee is addressed as "you".  "Licensees" and
"recipients" may be individuals or organizations.

  To "modify" a work means to copy from or adapt all or part of the work
in a fashion requiring copyright permission, other than the making of an
exact copy.  The resulting work is called a "modified version" of the
earlier work or a work "based on" the earlier work.

  A "covered work" means either the unmodified Program or a work based
on the Program.

  To "propagate" a work means to do anything with it that, without
permission, would make you directly or secondarily liable for
infringement under applicable copyright law, except executing it on a
computer or modifying a private copy.  Propagation includes copying,
distribution (with or without modification), making available to the
public, and in some countries other activities as well.

  To "convey" a work means any kind of propagation that enables other
parties to make or receive copies.  Mere interaction with a user through
a computer network, with no transfer of a copy, is not conveying.

  An interactive user interface displays "Appropriate Legal Notices"
to the extent that it includes a convenient and prominently visible
feature that (1) displays an appropriate copyright notice, and (2)
tells the user that there is no warranty for the work (except to the
extent that warranties are provided), that licensees may convey the
work under this License, and how to view a copy of this License.  If
the interface presents a list of user commands or options, such as a
menu, a prominent item in the list meets this criterion.

  1. Source Code.

  The "source code" for a work means the preferred form of the work
for making modifications to it.  "Object code" means any non-source
form of a work.

  A "Standard Interface" means an interface that either is an official
standard defined by a recognized standards body, or, in the case of
interfaces specified for a particular programming language, one that
is widely used among developers working in that language.

  The "System Libraries" of an executable work include anything, other
than the work as a whole, that (a) is included in the normal form of
packaging a Major Component, but which is not part of that Major
Component, and (b) serves only to enable use of the work with that
Major Component, or to implement a Standard Interface for which an
implementation is available to the public in source code form.  A
"Major Component", in this context, means a major essential component
(kernel, window system, and so on) of the specific operating system
(if any) on which the executable work runs, or a compiler used to
produce the work, or an object code interpreter used to run it.

  The "Corresponding Source" for a work in object code form means all
the source code needed to generate, install, and (for an executable
work) run the object code and to modify the work, including scripts to
control those activities.  However, it does not include the work's
System Libraries, or general-purpose tools or generally available free
programs which are used unmodified in performing those activities but
which are not part of the work.  For example, Corresponding Source
includes interface definition files associated with source files for
the work, and the source code for shared libraries and dynamically
linked subprograms that the work is specifically designed to require,
such as by intimate data communication or control flow between those
subprograms and other parts of the work.

  The Corresponding Source need not include anything that users
can regenerate automatically from other parts of the Corresponding
Source.

  The Corresponding Source for a work in source code form is that
same work.

  2. Basic Permissions.

  All rights granted under this License are granted for the term of
copyright on the Program, and are irrevocable provided the stated
conditions are met.  This License explicitly affirms your unlimited
permission to run the unmodified Program.  The output from running a
covered work is covered by this License only if the output, given its
content, constitutes a covered work.  This License acknowledges your
rights of fair use or other equivalent, as provided by copyright law.

  You may make, run and propagate covered works that you do not
convey, without conditions so long as your license otherwise remains
in force.  You may convey covered works to others for the sole purpose
of having them make modifications exclusively for you, or provide you
with facilities for running those works, provided that you comply with
the terms of this License in conveying all material for which you do
not control copyright.  Those thus making or running the covered works
for you must do so exclusively on your behalf, under your direction
and control, on terms that prohibit them from making any copies of
your copyrighted material outside their relationship with you.

  Conveying under any other circumstances is permitted solely under
the conditions stated below.  Sublicensing is not allowed; section 10
makes it unnecessary.

  3. Protecting Users' Legal Rights From Anti-Circumvention Law.

  No covered work shall be deemed part of an effective technological
measure under any applicable law fulfilling obligations under article
11 of the WIPO copyright treaty adopted on 20 December 1996, or
similar laws prohibiting or restricting circumvention of such
measures.

  When you convey a covered work, you waive any legal power to forbid
circumvention of technological measures to the extent such circumvention
is effected by exercising rights under this License with respect to
the covered work, and you disclaim any intention to limit operation or
modification of the work as a means of enforcing, against the work's
users, your or third parties' legal rights to forbid circumvention of
technological measures.

  4. Conveying Verbatim Copies.

  You may convey verbatim copies of the Program's source code as you
receive it, in any medium, provided that you conspicuously and
appropriately publish on each copy an appropriate copyright notice;
keep intact all notices stating that this License and any
non-permissive terms added in accord with section 7 apply to the code;
keep intact all notices of the absence of any warranty; and give all
recipients a copy of this License along with the Program.

  You may charge any price or no price for each copy that you convey,
and you may offer support or warranty protection for a fee.

  5. Conveying Modified Source Versions.

  You may convey a work based on the Program, or the modifications to
produce it from the Program, in the form of source code under the
terms of section 4, provided that you also meet all of these conditions:

    a) The work must carry prominent notices stating that you modified
    it, and giving a relevant date.

    b) The work must carry prominent notices stating that it is
    released under this License and any conditions added under section
    7.  This requirement modifies the requirement in section 4 to
    "keep intact all notices".

    c) You must license the entire work, as a whole, under this
    License to anyone who comes into possession of a copy.  This
    License will therefore apply, along with any applicable section 7
    additional terms, to the whole of the work, and all its parts,
    regardless of how they are packaged.  This License gives no
    permission to license the work in any other way, but it does not
    invalidate such permission if you have separately received it.

    d) If the work has interactive user interfaces, each must display
    Appropriate Legal Notices; however, if the Program has interactive
    interfaces that do not display Appropriate Legal Notices, your
    work need not make them do so.

  A compilation of a covered work with other separate and independent
works, which are not by their nature extensions of the covered work,
and which are not combined with it such as to form a larger program,
in or on a volume of a storage or distribution medium, is called an
"aggregate" if the compilation and its resulting copyright are not
used to limit the access or legal rights of the compilation's users
beyond what the individual works permit.  Inclusion of a covered work
in an aggregate does not cause this License to apply to the other
parts of the aggregate.

  6. Conveying Non-Source Forms.

  You may convey a covered work in object code form under the terms
of sections 4 and 5, provided that you also convey the
machine-readable Corresponding Source under the terms of this License,
in one of these ways:

    a) Convey the object code in, or embodied in, a physical product
    (including a physical distribution medium), accompanied by the
    Corresponding Source fixed on a durable physical medium
    customarily used for software interchange.

    b) Convey the object code in, or embodied in, a physical product
    (including a physical distribution medium), accompanied by a
    written offer, valid for at least three years and valid for as
    long as you offer spare parts or customer support for that product
    model, to give anyone who possesses the object code either (1) a
    copy of the Corresponding Source for all the software in the
    product that is covered by this License, on a durable physical
    medium customarily used for software interchange, for a price no
    more than your reasonable cost of physically performing this
    conveying of source, or (2) access to copy the
    Corresponding Source from a network server at no charge.

    c) Convey individual copies of the object code with a copy of the
    written offer to provide the Corresponding Source.  This
    alternative is allowed only occasionally and noncommercially, and
    only if you received the object code with such an offer, in accord
    with subsection 6b.

    d) Convey the object code by offering access from a designated
    place (gratis or for a charge), and offer equivalent access to the
    Corresponding Source in the same way through the same place at no
    further charge.  You need not require recipients to copy the
    Corresponding Source along with the object code.  If the place to
    copy the object code is a network server, the Corresponding Source
    may be on a different server (operated by you or a third party)
    that supports equivalent copying facilities, provided you maintain
    clear directions next to the object code saying where to find the
    Corresponding Source.  Regardless of what server hosts the
    Corresponding Source, you remain obligated to ensure that it is
    available for as long as needed to satisfy these requirements.

    e) Convey the object code using peer-to-peer transmission, provided
    you inform other peers where the object code and Corresponding
    Source of the work are being offered to the general public at no
    charge under subsection 6d.

  A separable portion of the object code, whose source code is excluded
from the Corresponding Source as a System Library, need not be
included in conveying the object code work.

  A "User Product" is either (1) a "consumer product", which means any
tangible personal property which is normally used for personal, family,
or household purposes, or (2) anything designed or sold for incorporation
into a dwelling.  In determining whether a product is a consumer product,
doubtful cases shall be resolved in favor of coverage.  For a particular
product received by a particular user, "normally used" refers to a
typical or common use of that class of product, regardless of the status
of the particular user or of the way in which the particular user
actually uses, or expects or is expected to use, the product.  A product
is a consumer product regardless of whether the product has substantial
commercial, industrial or non-consumer uses, unless such uses represent
the only significant mode of use of the product.

  "Installation Information" for a User Product means any methods,
procedures, authorization keys, or other information required to install
and execute modified versions of a covered work in that User Product from
a modified version of its Corresponding Source.  The information must
suffice to ensure that the continued functioning of the modified object
code is in no case prevented or interfered with solely because
modification has been made.

  If you convey an object code work under this section in, or with, or
specifically for use in, a User Product, and the conveying occurs as
part of a transaction in which the right of possession and use of the
User Product is transferred to the recipient in perpetuity or for a
fixed term (regardless of how the transaction is characterized), the
Corresponding Source conveyed under this section must be accompanied
by the Installation Information.  But this requirement does not apply
if neither you nor any third party retains the ability to install
modified object code on the User Product (for example, the work has
been installed in ROM).

  The requirement to provide Installation Information does not include a
requirement to continue to provide support service, warranty, or updates
for a work that has been modified or installed by the recipient, or for
the User Product in which it has been modified or installed.  Access to a
network may be denied when the modification itself materially and
adversely affects the operation of the network or violates the rules and
protocols for communication across the network.

  Corresponding Source conveyed, and Installation Information provided,
in accord with this section must be in a format that is publicly
documented (and with an implementation available to the public in
source code form), and must require no special password or key for
unpacking, reading or copying.

  7. Additional Terms.

  "Additional permissions" are terms that supplement the terms of this
License by making exceptions from one or more of its conditions.
Additional permissions that are applicable to the entire Program shall
be treated as though they were included in this License, to the extent
that they are valid under applicable law.  If additional permissions
apply only to part of the Program, that part may be used separately
under those permissions, but the entire Program remains governed by
this License without regard to the additional permissions.

  When you convey a copy of a covered work, you may at your option
remove any additional permissions from that copy, or from any part of
it.  (Additional permissions may be written to require their own
removal in certain cases when you modify the work.)  You may place
additional permissions on material, added by you to a covered work,
for which you have or can give appropriate copyright permission.

  Notwithstanding any other provision of this License, for material you
add to a covered work, you may (if authorized by the copyright holders of
that material) supplement the terms of this License with terms:

    a) Disclaiming warranty or limiting liability differently from the
    terms of sections 15 and 16 of this License; or

    b) Requiring preservation of specified reasonable legal notices or
    author attributions in that material or in the Appropriate Legal
    Notices displayed by works containing it; or

    c) Prohibiting misrepresentation of the origin of that material, or
    requiring that modified versions of such material be marked in
    reasonable ways as different from the original version; or

    d) Limiting the use for publicity purposes of names of licensors or
    authors of the material; or

    e) Declining to grant rights under trademark law for use of some
    trade names, trademarks, or service marks; or

    f) Requiring indemnification of licensors and authors of that
    material by anyone who conveys the material (or modified versions of
    it) with contractual assumptions of liability to the recipient, for
    any liability that these contractual assumptions directly impose on
    those licensors and authors.

  All other non-permissive additional terms are considered "further
restrictions" within the meaning of section 10.  If the Program as you
received it, or any part of it, contains a notice stating that it is
governed by this License along with a term that is a further
restriction, you may remove that term.  If a license document contains
a further restriction but permits relicensing or conveying under this
License, you may add to a covered work material governed by the terms
of that license document, provided that the further restriction does
not survive such relicensing or conveying.

  If you add terms to a covered work in accord with this section, you
must place, in the relevant source files, a statement of the
additional terms that apply to those files, or a notice indicating
where to find the applicable terms.

  Additional terms, permissive or non-permissive, may be stated in the
form of a separately written license, or stated as exceptions;
the above requirements apply either way.

  8. Termination.

  You may not propagate or modify a covered work except as expressly
provided under this License.  Any attempt otherwise to propagate or
modify it is void, and will automatically terminate your rights under
this License (including any patent licenses granted under the third
paragraph of section 11).

  However, if you cease all violation of this License, then your
license from a particular copyright holder is reinstated (a)
provisionally, unless and until the copyright holder explicitly and
finally terminates your license, and (b) permanently, if the copyright
holder fails to notify you of the violation by some reasonable means
prior to 60 days after the cessation.

  Moreover, your license from a particular copyright holder is
reinstated permanently if the copyright holder notifies you of the
violation by some reasonable means, this is the first time you have
received notice of violation of this License (for any work) from that
copyright holder, and you cure the violation prior to 30 days after
your receipt of the notice.

  Termination of your rights under this section does not terminate the
licenses of parties who have received copies or rights from you under
this License.  If your rights have been terminated and not permanently
reinstated, you do not qualify to receive new licenses for the same
material under section 10.

  9. Acceptance Not Required for Having Copies.

  You are not required to accept this License in order to receive or
run a copy of the Program.  Ancillary propagation of a covered work
occurring solely as a consequence of using peer-to-peer transmission
to receive a copy likewise does not require acceptance.  However,
nothing other than this License grants you permission to propagate or
modify any covered work.  These actions infringe copyright if you do
not accept this License.  Therefore, by modifying or propagating a
covered work, you indicate your acceptance of this License to do so.

  10. Automatic Licensing of Downstream Recipients.

  Each time you convey a covered work, the recipient automatically
receives a license from the original licensors, to run, modify and
propagate that work, subject to this License.  You are not responsible
for enforcing compliance by third parties with this License.

  An "entity transaction" is a transaction transferring control of an
organization, or substantially all assets of one, or subdividing an
organization, or merging organizations.  If propagation of a covered
work results from an entity transaction, each party to that
transaction who receives a copy of the work also receives whatever
licenses to the work the party's predecessor in interest had or could
give under the previous paragraph, plus a right to possession of the
Corresponding Source of the work from the predecessor in interest, if
the predecessor has it or can get it with reasonable efforts.

  You may not impose any further restrictions on the exercise of the
rights granted or affirmed under this License.  For example, you may
not impose a license fee, royalty, or other charge for exercise of
rights granted under this License, and you may not initiate litigation
(including a cross-claim or counterclaim in a lawsuit) alleging that
any patent claim is infringed by making, using, selling, offering for
sale, or importing the Program or any portion of it.

  11. Patents.

  A "contributor" is a copyright holder who authorizes use under this
License of the Program or a work on which the Program is based.  The
work thus licensed is called the contributor's "contributor version".

  A contributor's "essential patent claims" are all patent claims
owned or controlled by the contributor, whether already acquired or
hereafter acquired, that would be infringed by some manner, permitted
by this License, of making, using, or selling its contributor version,
but do not include claims that would be infringed only as a
consequence of further modification of the contributor version.  For
purposes of this definition, "control" includes the right to grant
patent sublicenses in a manner consistent with the requirements of
this License.

  Each contributor grants you a non-exclusive, worldwide, royalty-free
patent license under the contributor's essential patent claims, to
make, use, sell, offer for sale, import and otherwise run, modify and
propagate the contents of its contributor version.

  In the following three paragraphs, a "patent license" is any express
agreement or commitment, however denominated, not to enforce a patent
(such as an express permission to practice a patent or covenant not to
sue for patent infringement).  To "grant" such a patent license to a
party means to make such an agreement or commitment not to enforce a
patent against the party.

  If you convey a covered work, knowingly relying on a patent license,
and the Corresponding Source of the work is not available for anyone
to copy, free of charge and under the terms of this License, through a
publicly available network server or other readily accessible means,
then you must either (1) cause the Corresponding Source to be so
available, or (2) arrange to deprive yourself of the benefit of the
patent license for this particular work, or (3) arrange, in a manner
consistent with the requirements of this License, to extend the patent
license to downstream recipients.  "Knowingly relying" means you have
actual knowledge that, but for the patent license, your conveying the
covered work in a country, or your recipient's use of the covered work
in a country, would infringe one or more identifiable patents in that
country that you have reason to believe are valid.

  If, pursuant to or in connection with a single transaction or
arrangement, you convey, or propagate by procuring conveyance of, a
covered work, and grant a patent license to some of the parties
receiving the covered work authorizing them to use, propagate, modify
or convey a specific copy of the covered work, then the patent license
you grant is automatically extended to all recipients of the covered
work and works based on it.

  A patent license is "discriminatory" if it does not include within
the scope of its coverage, prohibits the exercise of, or is
conditioned on the non-exercise of one or more of the rights that are
specifically granted under this License.  You may not convey a covered
work if you are a party to an arrangement with a third party that is
in the business of distributing software, under which you make payment
to the third party based on the extent of your activity of conveying
the work, and under which the third party grants, to any of the
parties who would receive the covered work from you, a discriminatory
patent license (a) in connection with copies of the covered work
conveyed by you (or copies made from those copies), or (b) primarily
for and in connection with specific products or compilations that
contain the covered work, unless you entered into that arrangement,
or that patent license was granted, prior to 28 March 2007.

  Nothing in this License shall be construed as excluding or limiting
any implied license or other defenses to infringement that may
otherwise be available to you under applicable patent law.

  12. No Surrender of Others' Freedom.

  If conditions are imposed on you (whether by court order, agreement or
otherwise) that contradict the conditions of this License, they do not
excuse you from the conditions of this License.  If you cannot convey a
covered work so as to satisfy simultaneously your obligations under this
License and any other pertinent obligations, then as a consequence you may
not convey it at all.  For example, if you agree to terms that obligate you
to collect a royalty for further conveying from those to whom you convey
the Program, the only way you could satisfy both those terms and this
License would be to refrain entirely from conveying the Program.

  13. Use with the GNU Affero General Public License.

  Notwithstanding any other provision of this License, you have
permission to link or combine any covered work with a work licensed
under version 3 of the GNU Affero General Public License into a single
combined work, and to convey the resulting work.  The terms of this
License will continue to apply to the part which is the covered work,
but the special requirements of the GNU Affero General Public License,
section 13, concerning interaction through a network will apply to the
combination as such.

  14. Revised Versions of this License.

  The Free Software Foundation may publish revised and/or new versions of
the GNU General Public License from time to time.  Such new versions will
be similar in spirit to the present version, but may differ in detail to
address new problems or concerns.

  Each version is given a distinguishing version number.  If the
Program specifies that a certain numbered version of the GNU General
Public License "or any later version" applies to it, you have the
option of following the terms and conditions either of that numbered
version or of any later version published by the Free Software
Foundation.  If the Program does not specify a version number of the
GNU General Public License, you may choose any version ever published
by the Free Software Foundation.

  If the Program specifies that a proxy can decide which future
versions of the GNU General Public License can be used, that proxy's
public statement of acceptance of a version permanently authorizes you
to choose that version for the Program.

  Later license versions may give you additional or different
permissions.  However, no additional obligations are imposed on any
author or copyright holder as a result of your choosing to follow a
later version.

  15. Disclaimer of Warranty.

  THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY
APPLICABLE LAW.  EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT
HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY
OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO,
THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
PURPOSE.  THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM
IS WITH YOU.  SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF
ALL NECESSARY SERVICING, REPAIR OR CORRECTION.

  16. Limitation of Liability.

  IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS
THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY
GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE
USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF
DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD
PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS),
EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF
SUCH DAMAGES.

  17. Interpretation of Sections 15 and 16.

  If the disclaimer of warranty and limitation of liability provided
above cannot be given local legal effect according to their terms,
reviewing courts shall apply local law that most closely approximates
an absolute waiver of all civil liability in connection with the
Program, unless a warranty or assumption of liability accompanies a
copy of the Program in return for a fee.

                     END OF TERMS AND CONDITIONS

            How to Apply These Terms to Your New Programs

  If you develop a new program, and you want it to be of the greatest
possible use to the public, the best way to achieve this is to make it
free software which everyone can redistribute and change under these terms.

  To do so, attach the following notices to the program.  It is safest
to attach them to the start of each source file to most effectively
state the exclusion of warranty; and each file should have at least
the "copyright" line and a pointer to where the full notice is found.

    <one line to give the program's name and a brief idea of what it does.>
    Copyright (C) <year>  <name of author>

    This program is free software: you can redistribute it and/or modify
    it under the terms of the GNU General Public License as published by
    the Free Software Foundation, either version 3 of the License, or
    (at your option) any later version.

    This program is distributed in the hope that it will be useful,
    but WITHOUT ANY WARRANTY; without even the implied warranty of
    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
    GNU General Public License for more details.

    You should have received a copy of the GNU General Public License
    along with this program.  If not, see <https://www.gnu.org/licenses/>.

Also add information on how to contact you by electronic and paper mail.

  If the program does terminal interaction, make it output a short
notice like this when it starts in an interactive mode:

    <program>  Copyright (C) <year>  <name of author>
    This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
    This is free software, and you are welcome to redistribute it
    under certain conditions; type `show c' for details.

The hypothetical commands `show w' and `show c' should show the appropriate
parts of the General Public License.  Of course, your program's commands
might be different; for a GUI interface, you would use an "about box".

  You should also get your employer (if you work as a programmer) or school,
if any, to sign a "copyright disclaimer" for the program, if necessary.
For more information on this, and how to apply and follow the GNU GPL, see
<https://www.gnu.org/licenses/>.

  The GNU General Public License does not permit incorporating your program
into proprietary programs.  If your program is a subroutine library, you
may consider it more useful to permit linking proprietary applications with
the library.  If this is what you want to do, use the GNU Lesser General
Public License instead of this License.  But first, please read
<https://www.gnu.org/licenses/why-not-lgpl.html>.


================================================
FILE: README.md
================================================
# Sparkle

<h3 align="center">Another <a href="https://github.com/MetaCubeX/mihomo">Mihomo</a> GUI</h3>

<p align="center">
  <a href="https://github.com/xishang0128/sparkle/releases">
    <img src="https://img.shields.io/github/release/xishang0128/sparkle/all.svg">
  </a>
  <a href="https://t.me/+y7rcYjEKIiI1NzZl">
    <img src="https://img.shields.io/badge/Telegram-Group-blue?logo=telegram">
  </a>
</p>

## 特性

- [x] 开箱即用,无需服务模式的 Tun
- [x] 多种配色主题可选,UI 焕然一新
- [x] 支持大部分 Mihomo 常用配置修改
- [x] 内置稳定版和预览版 Mihomo 内核
- [x] 通过 WebDAV 一键备份和恢复配置
- [x] 强大的覆写功能,任意修订配置文件
- [x] 深度集成 Sub-Store,轻松管理订阅

## 开发

本项目为自用,绝大部分 pr 可能都不会合并,你可以自行 fork 修改。

### 环境要求

- **Node.js**: >= 20.0.0 (推荐使用 LTS 版本)
- **pnpm**: >= 9.0.0 (必需)
- **Git**: 最新版本

### 技术架构

Sparkle 基于 Electron + React + TypeScript 构建

#### 前端技术栈

- **React 19** - 用户界面框架
- **TypeScript** - 类型安全的 JavaScript
- **HeroUI (NextUI)** - UI 组件库
- **Tailwind CSS** - 原子化 CSS 框架
- **Monaco Editor** - 代码编辑器

#### 后端技术栈

- **Electron** - 应用主进程
- **Mihomo Core** - 代理内核
- **sysproxy-go** - 系统代理集成

### 快速开始

1. **克隆项目**

```bash
git clone https://github.com/xishang0128/sparkle.git
cd sparkle
```

2. **安装依赖**

```bash
pnpm install
```

3. **处理 Electron 安装问题**(如果遇到 pnpm dev 等命令无法成功运行)

```bash
# 如果 Electron 没有正确安装,执行以下命令
cd node_modules/electron
node install.js
cd ../..
```

4. **启动开发服务器**

```bash
pnpm dev
```

### 注意事项

windows 开发时可能会出现页面白屏,关闭 tun(虚拟网卡)即可

### 项目结构

```
sparkle/
├── src/
│   ├── main/               # Electron 主进程
│   │   ├── core/           # 内核管理
│   │   ├── config/         # 配置管理
│   │   ├── resolve/        # 解析器
│   │   ├── sys/            # 系统集成
│   │   └── utils/          # 工具函数
│   ├── renderer/           # Electron 渲染进程(前端界面)
│   │   ├── src/
│   │   │   ├── assets/     # 静态资源
│   │   │   ├── components/ # React 组件
│   │   │   ├── pages/      # 页面组件
│   │   │   ├── hooks/      # 自定义 hooks
│   │   │   ├── routes/     # 路由配置
│   │   │   └── utils/      # 前端工具
│   │   └── index.html      # 渲染进程入口 HTML
│   ├── preload/            # Electron 预加载脚本(进程间通信桥梁)
│   │   ├── index.ts        # 预加载脚本主文件
│   │   └── index.d.ts      # 预加载脚本类型定义
│   └── shared/             # 共享资源
│       └── types           # 全局类型定义
├── resources/              # 应用资源文件
├── build/                  # 构建配置
├── extra/                  # 额外资源
├── dist/                   # 构建输出目录
├── electron-builder.yml    # 打包配置
├── package.json            # 项目配置
└── README.md               # 项目说明
```

### 可用脚本

#### 开发命令

- `pnpm dev` - 启动开发服务器(前端热重载,后端需要手动重启)
- `pnpm typecheck` - TypeScript 类型检查
- `pnpm typecheck:node` - 主进程类型检查
- `pnpm typecheck:web` - 渲染进程类型检查
- `pnpm lint` - 运行代码检查
- `pnpm format` - 格式化代码

#### 构建命令

- `pnpm build:win` - 构建 Windows 版本
- `pnpm build:mac` - 构建 macOS 版本
- `pnpm build:linux` - 构建 Linux 版本

#### 其他命令

- `pnpm prepare` - 准备构建环境
- `pnpm postinstall` - 安装 Electron 依赖

### 构建发布

#### 环境准备

根据目标平台准备相应的构建环境:

**Windows 构建:**

```bash
pnpm build:win
```

**macOS 构建:**

```bash
pnpm build:mac
```

**Linux 构建:**

```bash
pnpm build:linux
```

**指定架构:**

```bash
pnpm build:win --x64/--arm64
pnpm build:mac --arm64/--x64
pnpm build:linux --x64/--arm64
```

**指定产物类型:**

```bash
pnpm build:win 7z/nsis
pnpm build:linux deb/rpm/pacman
pnpm build:mac pkg/dmg
```

**指定架构和产物类型:**

```bash
pnpm build:win 7z --x64
pnpm build:mac pkg --arm64
pnpm build:linux deb --x64
```

#### 构建产物

- **Windows**: `.exe` 安装包和 `.7z` 便携版
- **macOS**: `.pkg` 安装包
- **Linux**: `.deb`、`.rpm`、`.pkg.tar.zst(pacman)` 等格式

### 常见问题

#### 包管理器要求

本项目使用 pnpm 作为包管理器。

确保使用 pnpm 9.0.0 或更高版本:

```bash
pnpm --version
```

#### Node.js 版本要求

确保使用 Node.js 20.0.0 或更高版本:

```bash
node --version
```

#### 开发环境问题

- 确保 Node.js 版本 >= 20.0.0
- 使用 pnpm 进行依赖管理

### 贡献指南

1. Fork 本仓库
2. 创建功能分支 (`git checkout -b feature/AmazingFeature`)
3. 提交更改 (`git commit -m 'Add some AmazingFeature'`)
4. 推送分支 (`git push origin feature/AmazingFeature`)
5. 创建 Pull Request

### 开发注意事项

- 请确保代码通过 ESLint 检查
- 提交前运行 `pnpm format` 格式化代码
- 遵循现有的代码风格和命名规范
- 添加新功能时请更新相关文档
- 主进程代码修改后需要重启开发服务器
- 渲染进程代码支持热重载
- 所有命令都使用 pnpm 执行
- 修改类型定义后需要重启 TypeScript 服务
- 预加载脚本修改后需要重启应用

## Star History

<a href="https://www.star-history.com/#xishang0128/sparkle&Date">
 <picture>
   <source media="(prefers-color-scheme: dark)" srcset="https://api.star-history.com/svg?repos=xishang0128/sparkle&type=Date&theme=dark" />
   <source media="(prefers-color-scheme: light)" srcset="https://api.star-history.com/svg?repos=xishang0128/sparkle&type=Date" />
   <img alt="Star History Chart" src="https://api.star-history.com/svg?repos=xishang0128/sparkle&type=Date" />
 </picture>
</a>


================================================
FILE: aur/sparkle/PKGBUILD
================================================
pkgname=sparkle
pkgver=1.6.2
pkgrel=1
pkgdesc="Another Mihomo GUI."
arch=('x86_64' 'aarch64')
url="https://github.com/xishang0128/sparkle"
license=('GPL3')
conflicts=("$pkgname-git" "$pkgname-bin" "$pkgname-electron" "$pkgname-electron-bin" "$_pkgname-electron-git")
depends=('alsa-lib' 'gtk3' 'libnotify' 'nss' 'libxss' 'libxtst' 'xdg-utils' 'at-spi2-core' 'util-linux-libs' 'libsecret')
optdepends=('libappindicator-gtk3: Allow sparkle to extend a menu via Ayatana indicators in Unity, KDE or Systray (GTK+ 3 library).')
makedepends=('nodejs' 'pnpm' 'libxcrypt-compat')
install=$pkgname.install
source=(
    "${pkgname}-${pkgver}.tar.gz::${url}/archive/refs/tags/${pkgver}.tar.gz"
    "${pkgname}.sh"
)
sha256sums=("d2fe3633951f7e164bc2df4437decd86e880a516e318363601ea552989c0c73d"
"03eb601fe981716e90f9170eeb36a2e7938587f05a1bdaa09adadb1229c77a0a")
options=('!lto')

prepare(){
    cd $srcdir/${pkgname}-${pkgver}
    sed -i "s/productName: Sparkle/productName: sparkle/" electron-builder.yml
    pnpm install
}

build(){
    cd $srcdir/${pkgname}-${pkgver}
    pnpm build:linux deb
}

package() {
	cd $srcdir/${pkgname}-${pkgver}/dist
    bsdtar -xf sparkle-linux-${pkgver}*.deb
    bsdtar -xf data.tar.xz -C "${pkgdir}/"
    chmod +x ${pkgdir}/opt/sparkle/sparkle
    chmod +x ${pkgdir}/opt/sparkle/resources/files/sparkle-service
    chmod +sx ${pkgdir}/opt/sparkle/resources/sidecar/mihomo
    chmod +sx ${pkgdir}/opt/sparkle/resources/sidecar/mihomo-alpha
    install -Dm755 "${srcdir}/../${pkgname}.sh" "${pkgdir}/usr/bin/${pkgname}"
    sed -i '3s!/opt/sparkle/sparkle!sparkle!' "${pkgdir}/usr/share/applications/${pkgname}.desktop"

    chown -R root:root ${pkgdir}
}


================================================
FILE: aur/sparkle/sparkle.install
================================================
# Colored makepkg-like functions
note() {
    printf "${_blue}==>${_yellow} NOTE:${_bold} %s${_all_off}\n" "$1"
}

_all_off="$(tput sgr0)"
_bold="${_all_off}$(tput bold)"
_blue="${_bold}$(tput setaf 4)"
_yellow="${_bold}$(tput setaf 3)"

post_install() {
    note "Custom flags should be put directly in: ~/.config/sparkle-flags.conf"
    note "The launcher is called: 'sparkle'"
}

================================================
FILE: aur/sparkle/sparkle.sh
================================================
#!/usr/bin/bash

XDG_CONFIG_HOME=${XDG_CONFIG_HOME:-~/.config}

# Allow users to override command-line options
if [[ -f "${XDG_CONFIG_HOME}/sparkle-flags.conf" ]]; then
	mapfile -t MIHOMO_PARTY_USER_FLAGS <<<"$(grep -v '^#' "${XDG_CONFIG_HOME}/sparkle-flags.conf")"
	echo "User flags:" ${MIHOMO_PARTY_USER_FLAGS[@]}
fi

# Launch
exec /opt/sparkle/sparkle ${MIHOMO_PARTY_USER_FLAGS[@]} "$@"


================================================
FILE: aur/sparkle-bin/PKGBUILD
================================================
pkgname=sparkle-bin
_pkgname=sparkle
pkgver=1.6.2
pkgrel=1
pkgdesc="Another Mihomo GUI."
arch=('x86_64' 'aarch64')
url="https://github.com/xishang0128/sparkle"
license=('GPL3')
conflicts=("$_pkgname" "$_pkgname-git" "$_pkgname-electron" "$_pkgname-electron-bin" "$_pkgname-electron-git")
conflicts=("sparkle-git" 'sparkle')
depends=('alsa-lib' 'gtk3' 'libnotify' 'nss' 'libxss' 'libxtst' 'xdg-utils' 'at-spi2-core' 'util-linux-libs' 'libsecret')
optdepends=('libappindicator-gtk3: Allow sparkle to extend a menu via Ayatana indicators in Unity, KDE or Systray (GTK+ 3 library).')
install=$_pkgname.install
source=("${_pkgname}.sh")
source_x86_64=("${_pkgname}-${pkgver}-x86_64.deb::${url}/releases/download/${pkgver}/sparkle-linux-${pkgver}-amd64.deb")
source_aarch64=("${_pkgname}-${pkgver}-aarch64.deb::${url}/releases/download/${pkgver}/sparkle-linux-${pkgver}-arm64.deb")
sha256sums=('03eb601fe981716e90f9170eeb36a2e7938587f05a1bdaa09adadb1229c77a0a')
sha256sums_x86_64=('b8d166f1134573336aaae1866d25262284b0cbabbf393684226aca0fd8d1bd83')
sha256sums_aarch64=('8cd7398b8fc1cd70d41e386af9995cbddc1043d9018391c29f056f1435712a10')

package() {
    bsdtar -xf data.tar.xz -C "${pkgdir}/"
    chmod +x ${pkgdir}/opt/sparkle/sparkle
    chmod +x ${pkgdir}/opt/sparkle/resources/files/sparkle-service
    chmod +sx ${pkgdir}/opt/sparkle/resources/sidecar/mihomo
    chmod +sx ${pkgdir}/opt/sparkle/resources/sidecar/mihomo-alpha
    install -Dm755 "${srcdir}/${_pkgname}.sh" "${pkgdir}/usr/bin/${_pkgname}"
    sed -i '3s!/opt/sparkle/sparkle!sparkle!' "${pkgdir}/usr/share/applications/${_pkgname}.desktop"

    chown -R root:root ${pkgdir}
}


================================================
FILE: aur/sparkle-bin/sparkle.install
================================================
# Colored makepkg-like functions
note() {
    printf "${_blue}==>${_yellow} NOTE:${_bold} %s${_all_off}\n" "$1"
}

_all_off="$(tput sgr0)"
_bold="${_all_off}$(tput bold)"
_blue="${_bold}$(tput setaf 4)"
_yellow="${_bold}$(tput setaf 3)"

post_install() {
    note "Custom flags should be put directly in: ~/.config/sparkle-flags.conf"
    note "The launcher is called: 'sparkle'"
}

================================================
FILE: aur/sparkle-bin/sparkle.sh
================================================
#!/usr/bin/bash

XDG_CONFIG_HOME=${XDG_CONFIG_HOME:-~/.config}

# Allow users to override command-line options
if [[ -f "${XDG_CONFIG_HOME}/sparkle-flags.conf" ]]; then
	mapfile -t MIHOMO_PARTY_USER_FLAGS <<<"$(grep -v '^#' "${XDG_CONFIG_HOME}/sparkle-flags.conf")"
	echo "User flags:" ${MIHOMO_PARTY_USER_FLAGS[@]}
fi

# Launch
exec /opt/sparkle/sparkle ${MIHOMO_PARTY_USER_FLAGS[@]} "$@"


================================================
FILE: aur/sparkle-electron/PKGBUILD
================================================
pkgname=sparkle-electron
_pkgname=sparkle
pkgver=1.6.2
pkgrel=1
pkgdesc="Another Mihomo GUI."
arch=('x86_64' 'aarch64')
url="   "
license=('GPL3')
conflicts=("$_pkgname" "$_pkgname-git" "$_pkgname-bin" "$_pkgname-electron-bin" "$_pkgname-electron-git")
depends=('electron' 'alsa-lib' 'gtk3' 'libnotify' 'nss' 'libxss' 'libxtst' 'xdg-utils' 'at-spi2-core' 'util-linux-libs' 'libsecret')
optdepends=('libappindicator-gtk3: Allow sparkle to extend a menu via Ayatana indicators in Unity, KDE or Systray (GTK+ 3 library).')
makedepends=('nodejs' 'pnpm' 'libxcrypt-compat' 'asar')
install=$_pkgname.install
source=(
    "${_pkgname}-${pkgver}.tar.gz::${url}/archive/refs/tags/${pkgver}.tar.gz"
    "${_pkgname}.desktop"
    "${_pkgname}.sh"
)
sha256sums=("d2fe3633951f7e164bc2df4437decd86e880a516e318363601ea552989c0c73d"
"b17d85f6d862285a53a24d0f8dedd08f1f3c852ba6a901fabc487177598803cc"
"ce855656fb0682d403685244c77dd2d90ec6efb207753fb7a6ddc1e9b6aa2c49"
)
options=('!lto')

prepare(){
    cd $srcdir/${_pkgname}-${pkgver}
    sed -i "s/productName: Sparkle/productName: sparkle/" electron-builder.yml
    pnpm install
}

build(){
    cd $srcdir/${_pkgname}-${pkgver}
    pnpm build:linux deb
}

package() {
    asar extract $srcdir/${_pkgname}-${pkgver}/dist/linux-unpacked/resources/app.asar ${pkgdir}/opt/sparkle
    cp -r $srcdir/${_pkgname}-${pkgver}/extra/sidecar ${pkgdir}/opt/sparkle/resources/
    cp -r $srcdir/${_pkgname}-${pkgver}/extra/files ${pkgdir}/opt/sparkle/resources/
    chmod +x ${pkgdir}/opt/sparkle/resources/files/sparkle-service
    chmod +sx ${pkgdir}/opt/sparkle/resources/sidecar/mihomo
    chmod +sx ${pkgdir}/opt/sparkle/resources/sidecar/mihomo-alpha
    install -Dm755 "${_pkgname}.sh" "${pkgdir}/usr/bin/${_pkgname}"
    install -Dm644 "${_pkgname}.desktop" "${pkgdir}/usr/share/applications/${_pkgname}.desktop"
    install -Dm644 "${pkgdir}/opt/sparkle/resources/icon.png" "${pkgdir}/usr/share/icons/hicolor/512x512/apps/${_pkgname}.png"

    chown -R root:root ${pkgdir}
}


================================================
FILE: aur/sparkle-electron/sparkle.desktop
================================================
[Desktop Entry]
Name=Sparkle
Exec=sparkle %U
Terminal=false
Type=Application
Icon=sparkle
StartupWMClass=sparkle
MimeType=x-scheme-handler/clash;x-scheme-handler/mihomo;x-scheme-handler/sparkle;
Comment=Sparkle
Categories=Utility;


================================================
FILE: aur/sparkle-electron/sparkle.install
================================================
# Colored makepkg-like functions
note() {
    printf "${_blue}==>${_yellow} NOTE:${_bold} %s${_all_off}\n" "$1"
}

_all_off="$(tput sgr0)"
_bold="${_all_off}$(tput bold)"
_blue="${_bold}$(tput setaf 4)"
_yellow="${_bold}$(tput setaf 3)"

post_install() {
    note "Custom flags should be put directly in: ~/.config/sparkle-flags.conf"
    note "The launcher is called: 'sparkle'"
}

================================================
FILE: aur/sparkle-electron/sparkle.sh
================================================
#!/usr/bin/bash

XDG_CONFIG_HOME=${XDG_CONFIG_HOME:-~/.config}

# Allow users to override command-line options
if [[ -f "${XDG_CONFIG_HOME}/sparkle-flags.conf" ]]; then
	mapfile -t MIHOMO_PARTY_USER_FLAGS <<<"$(grep -v '^#' "${XDG_CONFIG_HOME}/sparkle-flags.conf")"
	echo "User flags:" ${MIHOMO_PARTY_USER_FLAGS[@]}
fi

# Launch
exec electron /opt/sparkle ${MIHOMO_PARTY_USER_FLAGS[@]} "$@"


================================================
FILE: aur/sparkle-electron-bin/PKGBUILD
================================================
pkgname=sparkle-electron-bin
_pkgname=sparkle
pkgver=1.6.2
pkgrel=1
pkgdesc="Another Mihomo GUI."
arch=('x86_64' 'aarch64')
url="https://github.com/xishang0128/sparkle"
license=('GPL3')
conflicts=("$_pkgname" "$_pkgname-git" "$_pkgname-bin" "$_pkgname-electron" "$_pkgname-electron-git")
depends=('electron' 'alsa-lib' 'gtk3' 'libnotify' 'nss' 'libxss' 'libxtst' 'xdg-utils' 'at-spi2-core' 'util-linux-libs' 'libsecret')
optdepends=('libappindicator-gtk3: Allow sparkle to extend a menu via Ayatana indicators in Unity, KDE or Systray (GTK+ 3 library).')
makedepends=('asar')
install=$_pkgname.install
source=("${_pkgname}.desktop" "${_pkgname}.sh")
source_x86_64=("${_pkgname}-${pkgver}-x86_64.deb::${url}/releases/download/${pkgver}/sparkle-linux-${pkgver}-amd64.deb")
source_aarch64=("${_pkgname}-${pkgver}-aarch64.deb::${url}/releases/download/${pkgver}/sparkle-linux-${pkgver}-arm64.deb")
sha256sums=(
    "b17d85f6d862285a53a24d0f8dedd08f1f3c852ba6a901fabc487177598803cc"
    "ce855656fb0682d403685244c77dd2d90ec6efb207753fb7a6ddc1e9b6aa2c49"
)
sha256sums_x86_64=("43f8b9a5818a722cdb8e5044d2a90993274860b0da96961e1a2652169539ce39")
sha256sums_aarch64=("18574fdeb01877a629aa52ac0175335ce27c83103db4fcb2f1ad69e3e42ee10f")
options=('!lto')

package() {
    bsdtar -xf data.tar.xz -C $srcdir
    asar extract $srcdir/opt/sparkle/resources/app.asar ${pkgdir}/opt/sparkle
    cp -r $srcdir/opt/sparkle/resources/sidecar ${pkgdir}/opt/sparkle/resources/
    cp -r $srcdir/opt/sparkle/resources/files ${pkgdir}/opt/sparkle/resources/
    chmod +x ${pkgdir}/opt/sparkle/resources/files/sparkle-service
    chmod +sx ${pkgdir}/opt/sparkle/resources/sidecar/mihomo
    chmod +sx ${pkgdir}/opt/sparkle/resources/sidecar/mihomo-alpha
    install -Dm755 "${srcdir}/${_pkgname}.sh" "${pkgdir}/usr/bin/${_pkgname}"
    install -Dm644 "${_pkgname}.desktop" "${pkgdir}/usr/share/applications/${_pkgname}.desktop"
    install -Dm644 "${pkgdir}/opt/sparkle/resources/icon.png" "${pkgdir}/usr/share/icons/hicolor/512x512/apps/${_pkgname}.png"

    chown -R root:root ${pkgdir}
}


================================================
FILE: aur/sparkle-electron-bin/sparkle.desktop
================================================
[Desktop Entry]
Name=Sparkle
Exec=sparkle %U
Terminal=false
Type=Application
Icon=sparkle
StartupWMClass=sparkle
MimeType=x-scheme-handler/clash;x-scheme-handler/mihomo;x-scheme-handler/sparkle;
Comment=Sparkle
Categories=Utility;


================================================
FILE: aur/sparkle-electron-bin/sparkle.install
================================================
# Colored makepkg-like functions
note() {
    printf "${_blue}==>${_yellow} NOTE:${_bold} %s${_all_off}\n" "$1"
}

_all_off="$(tput sgr0)"
_bold="${_all_off}$(tput bold)"
_blue="${_bold}$(tput setaf 4)"
_yellow="${_bold}$(tput setaf 3)"

post_install() {
    note "Custom flags should be put directly in: ~/.config/sparkle-flags.conf"
    note "The launcher is called: 'sparkle'"
}

================================================
FILE: aur/sparkle-electron-bin/sparkle.sh
================================================
#!/usr/bin/bash

XDG_CONFIG_HOME=${XDG_CONFIG_HOME:-~/.config}

# Allow users to override command-line options
if [[ -f "${XDG_CONFIG_HOME}/sparkle-flags.conf" ]]; then
	mapfile -t MIHOMO_PARTY_USER_FLAGS <<<"$(grep -v '^#' "${XDG_CONFIG_HOME}/sparkle-flags.conf")"
	echo "User flags:" ${MIHOMO_PARTY_USER_FLAGS[@]}
fi

# Launch
exec electron /opt/sparkle ${MIHOMO_PARTY_USER_FLAGS[@]} "$@"


================================================
FILE: aur/sparkle-electron-git/PKGBUILD
================================================
pkgname=sparkle-electron-git
_pkgname=${pkgname%-electron-git}
pkgver=r737.e4a7e67
pkgrel=1
pkgdesc="Another Mihomo GUI."
arch=('x86_64' 'aarch64')
url="https://github.com/xishang0128/sparkle"
license=('GPL3')
conflicts=("$_pkgname" "$_pkgname-git" "$_pkgname-bin" "$_pkgname-electron" "$_pkgname-electron-bin")
depends=('electron' 'alsa-lib' 'gtk3' 'libnotify' 'nss' 'libxss' 'libxtst' 'xdg-utils' 'at-spi2-core' 'util-linux-libs' 'libsecret')
optdepends=('libappindicator-gtk3: Allow sparkle to extend a menu via Ayatana indicators in Unity, KDE or Systray (GTK+ 3 library).')
makedepends=('nodejs' 'pnpm' 'libxcrypt-compat' 'asar')
install=$_pkgname.install
source=(
    "${_pkgname}.desktop"
    "${_pkgname}.sh"
    "git+$url.git"
)
sha256sums=(
    "b17d85f6d862285a53a24d0f8dedd08f1f3c852ba6a901fabc487177598803cc"
    "ce855656fb0682d403685244c77dd2d90ec6efb207753fb7a6ddc1e9b6aa2c49"
    "SKIP"
)
options=('!lto')

pkgver() {
    cd $srcdir/${_pkgname}
    ( set -o pipefail
        git describe --long 2>/dev/null | sed 's/\([^-]*-g\)/r\1/;s/-/./g' | tr -d 'v' ||
        printf "r%s.%s" "$(git rev-list --count HEAD)" "$(git rev-parse --short HEAD)"
    )
}

prepare(){
    cd $srcdir/${_pkgname}
    sed -i "s/productName: Sparkle/productName: sparkle/" electron-builder.yml
    pnpm install
}

build(){
    cd $srcdir/${_pkgname}
    pnpm build:linux deb
}

package() {
    asar extract $srcdir/${_pkgname}/dist/linux-unpacked/resources/app.asar ${pkgdir}/opt/sparkle
    cp -r $srcdir/${_pkgname}/extra/sidecar ${pkgdir}/opt/sparkle/resources/
    cp -r $srcdir/${_pkgname}/extra/files ${pkgdir}/opt/sparkle/resources/
    chmod +x ${pkgdir}/opt/sparkle/resources/files/sparkle-service
    chmod +sx ${pkgdir}/opt/sparkle/resources/sidecar/mihomo
    chmod +sx ${pkgdir}/opt/sparkle/resources/sidecar/mihomo-alpha
    install -Dm755 "${_pkgname}.sh" "${pkgdir}/usr/bin/${_pkgname}"
    install -Dm644 "${_pkgname}.desktop" "${pkgdir}/usr/share/applications/${_pkgname}.desktop"
    install -Dm644 "${pkgdir}/opt/sparkle/resources/icon.png" "${pkgdir}/usr/share/icons/hicolor/512x512/apps/${_pkgname}.png"

    chown -R root:root ${pkgdir}
}


================================================
FILE: aur/sparkle-electron-git/sparkle.desktop
================================================
[Desktop Entry]
Name=Sparkle
Exec=sparkle %U
Terminal=false
Type=Application
Icon=sparkle
StartupWMClass=sparkle
MimeType=x-scheme-handler/clash;x-scheme-handler/mihomo;x-scheme-handler/sparkle;
Comment=Sparkle
Categories=Utility;


================================================
FILE: aur/sparkle-electron-git/sparkle.install
================================================
# Colored makepkg-like functions
note() {
    printf "${_blue}==>${_yellow} NOTE:${_bold} %s${_all_off}\n" "$1"
}

_all_off="$(tput sgr0)"
_bold="${_all_off}$(tput bold)"
_blue="${_bold}$(tput setaf 4)"
_yellow="${_bold}$(tput setaf 3)"

post_install() {
    note "Custom flags should be put directly in: ~/.config/sparkle-flags.conf"
    note "The launcher is called: 'sparkle'"
}

================================================
FILE: aur/sparkle-electron-git/sparkle.sh
================================================
#!/usr/bin/bash

XDG_CONFIG_HOME=${XDG_CONFIG_HOME:-~/.config}

# Allow users to override command-line options
if [[ -f "${XDG_CONFIG_HOME}/sparkle-flags.conf" ]]; then
	mapfile -t MIHOMO_PARTY_USER_FLAGS <<<"$(grep -v '^#' "${XDG_CONFIG_HOME}/sparkle-flags.conf")"
	echo "User flags:" ${MIHOMO_PARTY_USER_FLAGS[@]}
fi

# Launch
exec electron /opt/sparkle ${MIHOMO_PARTY_USER_FLAGS[@]} "$@"


================================================
FILE: aur/sparkle-git/PKGBUILD
================================================
pkgname=sparkle-git
_pkgname=${pkgname%-git}
pkgver=1.6.2.r1.db8c6a0
pkgrel=1
pkgdesc="Another Mihomo GUI."
arch=('x86_64' 'aarch64')
url="https://github.com/xishang0128/sparkle"
license=('GPL3')
conflicts=("$_pkgname" "$_pkgname-bin" "$_pkgname-electron" "$_pkgname-electron-bin" "$_pkgname-electron-git")
depends=('alsa-lib' 'gtk3' 'libnotify' 'nss' 'libxss' 'libxtst' 'xdg-utils' 'at-spi2-core' 'util-linux-libs' 'libsecret')
optdepends=('libappindicator-gtk3: Allow sparkle to extend a menu via Ayatana indicators in Unity, KDE or Systray (GTK+ 3 library).')
makedepends=('nodejs' 'pnpm' 'jq' 'libxcrypt-compat')
install=$_pkgname.install
source=("${_pkgname}.sh" "git+$url.git")
sha256sums=("03eb601fe981716e90f9170eeb36a2e7938587f05a1bdaa09adadb1229c77a0a" "SKIP")
options=('!lto')

pkgver() {
    cd $srcdir/${_pkgname}
    ( set -o pipefail
        git describe --long 2>/dev/null | sed 's/\([^-]*-g\)/r\1/;s/-/./g' | tr -d 'v' ||
        printf "r%s.%s" "$(git rev-list --count HEAD)" "$(git rev-parse --short HEAD)"
    )
}

prepare(){
    cd $srcdir/${_pkgname}
    sed -i "s/productName: Sparkle/productName: sparkle/" electron-builder.yml
    pnpm install
}

build(){
    cd $srcdir/${_pkgname}
    pnpm build:linux deb
}

package() {
	cd $srcdir/${_pkgname}/dist
    bsdtar -xf sparkle-linux-$(jq '.version' $srcdir/${_pkgname}/package.json | tr -d 'v"')*.deb
    bsdtar -xf data.tar.xz -C "${pkgdir}/"
    chmod +x ${pkgdir}/opt/sparkle/sparkle
    chmod +x ${pkgdir}/opt/sparkle/resources/files/sparkle-service
    chmod +sx ${pkgdir}/opt/sparkle/resources/sidecar/mihomo
    chmod +sx ${pkgdir}/opt/sparkle/resources/sidecar/mihomo-alpha
    install -Dm755 "${srcdir}/../${_pkgname}.sh" "${pkgdir}/usr/bin/${_pkgname}"
    sed -i '3s!/opt/sparkle/sparkle!sparkle!' "${pkgdir}/usr/share/applications/${_pkgname}.desktop"

    chown -R root:root ${pkgdir}
}


================================================
FILE: aur/sparkle-git/sparkle.install
================================================
# Colored makepkg-like functions
note() {
    printf "${_blue}==>${_yellow} NOTE:${_bold} %s${_all_off}\n" "$1"
}

_all_off="$(tput sgr0)"
_bold="${_all_off}$(tput bold)"
_blue="${_bold}$(tput setaf 4)"
_yellow="${_bold}$(tput setaf 3)"

post_install() {
    note "Custom flags should be put directly in: ~/.config/sparkle-flags.conf"
    note "The launcher is called: 'sparkle'"
}

================================================
FILE: aur/sparkle-git/sparkle.sh
================================================
#!/usr/bin/bash

XDG_CONFIG_HOME=${XDG_CONFIG_HOME:-~/.config}

# Allow users to override command-line options
if [[ -f "${XDG_CONFIG_HOME}/sparkle-flags.conf" ]]; then
	mapfile -t MIHOMO_PARTY_USER_FLAGS <<<"$(grep -v '^#' "${XDG_CONFIG_HOME}/sparkle-flags.conf")"
	echo "User flags:" ${MIHOMO_PARTY_USER_FLAGS[@]}
fi

# Launch
exec /opt/sparkle/sparkle ${MIHOMO_PARTY_USER_FLAGS[@]} "$@"


================================================
FILE: build/entitlements.mac.plist
================================================
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
  <dict>
    <key>com.apple.security.cs.allow-jit</key>
    <true/>
    <key>com.apple.security.cs.allow-unsigned-executable-memory</key>
    <true/>
    <key>com.apple.security.cs.allow-dyld-environment-variables</key>
    <true/>
  </dict>
</plist>


================================================
FILE: build/installer.nsh
================================================
!ifndef BUILD_UNINSTALLER

!macro customHeader
  Var sparkleServiceWasRunning
!macroend

!macro ServiceOutputContains NEEDLE RESULT
  StrCpy ${RESULT} "false"
  StrCpy $R5 0
  StrLen $R6 $R3
  StrLen $R8 "${NEEDLE}"
  ${Do}
    StrCpy $R9 $R3 $R8 $R5
    ${If} $R9 == "${NEEDLE}"
      StrCpy ${RESULT} "true"
      ${Break}
    ${EndIf}
    IntOp $R5 $R5 + 1
  ${LoopUntil} $R5 >= $R6
!macroend

!macro QuerySparkleServiceState RESULT
  nsExec::ExecToStack '"$SYSDIR\sc.exe" query SparkleService'
  Pop $R2
  Pop $R3

  StrCpy ${RESULT} "not-installed"
  ${If} $R2 == 0
    !insertmacro ServiceOutputContains "RUNNING" $R4
    ${If} $R4 == "true"
      StrCpy ${RESULT} "running"
    ${Else}
      !insertmacro ServiceOutputContains "STOP_PENDING" $R4
      ${If} $R4 == "true"
        StrCpy ${RESULT} "stop-pending"
      ${Else}
        !insertmacro ServiceOutputContains "STOPPED" $R4
        ${If} $R4 == "true"
          StrCpy ${RESULT} "stopped"
        ${Else}
          StrCpy ${RESULT} "unknown"
        ${EndIf}
      ${EndIf}
    ${EndIf}
  ${EndIf}
!macroend

!macro WaitSparkleServiceStopped
  StrCpy $R0 0
  ${Do}
    !insertmacro QuerySparkleServiceState $R1
    ${If} $R1 == "stopped"
    ${OrIf} $R1 == "not-installed"
      ${Break}
    ${EndIf}
    Sleep 500
    IntOp $R0 $R0 + 1
  ${LoopUntil} $R0 >= 30

  !insertmacro QuerySparkleServiceState $R1
  ${If} $R1 != "stopped"
  ${AndIf} $R1 != "not-installed"
    MessageBox MB_ICONSTOP "SparkleService is still running. Please stop the service and run the installer again."
    Abort
  ${EndIf}
!macroend

!macro StopSparkleServiceIfRunning
  !insertmacro QuerySparkleServiceState $R1

  ${If} $R1 != "stopped"
  ${AndIf} $R1 != "not-installed"
    StrCpy $sparkleServiceWasRunning "true"
    DetailPrint "Stopping Sparkle service"
    nsExec::ExecToStack '"$SYSDIR\sc.exe" stop SparkleService'
    Pop $R2
    Pop $R3
    !insertmacro WaitSparkleServiceStopped
  ${EndIf}
!macroend

!macro customInit
  StrCpy $sparkleServiceWasRunning "false"
  !insertmacro StopSparkleServiceIfRunning
!macroend

!macro customInstall
  ${If} $sparkleServiceWasRunning == "true"
    StrCpy $R1 "$INSTDIR\resources\files\sparkle-service.exe"
    ${If} ${FileExists} "$R1"
      DetailPrint "Starting Sparkle service: $R1"
      nsExec::ExecToLog '"$R1" service start'
      Pop $R2
      ${If} $R2 != 0
        DetailPrint "Sparkle service start exited with code $R2"
      ${EndIf}
    ${EndIf}
  ${EndIf}
!macroend

!endif


================================================
FILE: build/linux/postinst
================================================
#!/bin/bash

if type update-alternatives 2>/dev/null >&1; then
    # Remove previous link if it doesn't use update-alternatives
    if [ -L '/usr/bin/sparkle' -a -e '/usr/bin/sparkle' -a "`readlink '/usr/bin/sparkle'`" != '/etc/alternatives/sparkle' ]; then
        rm -f '/usr/bin/sparkle'
    fi
    update-alternatives --install '/usr/bin/sparkle' 'sparkle' '/opt/sparkle/sparkle' 100 || ln -sf '/opt/sparkle/sparkle' '/usr/bin/sparkle'
else
    ln -sf '/opt/sparkle/sparkle' '/usr/bin/sparkle'
fi

sed -i 's/Name=sparkle/Name=Sparkle/' '/usr/share/applications/sparkle.desktop'

chmod 4755 '/opt/sparkle/chrome-sandbox' || true
chmod +sx /opt/sparkle/resources/sidecar/mihomo
chmod +sx /opt/sparkle/resources/sidecar/mihomo-alpha

if [ -d '/opt/sparkle/resources/files/sub-store-frontend' ]; then
    chmod -R go-w '/opt/sparkle/resources/files/sub-store-frontend' 2>/dev/null || true
fi

restore_sparkle_service_if_was_running() {
    SERVICE_NAME='SparkleService'
    STATE_FILE='/run/sparkle/service-was-running'

    if [ ! -f "$STATE_FILE" ]; then
        return
    fi
    rm -f "$STATE_FILE" 2>/dev/null || true

    if command -v systemctl >/dev/null 2>&1; then
        systemctl start "$SERVICE_NAME" >/dev/null 2>&1 || true
    elif command -v service >/dev/null 2>&1; then
        service "$SERVICE_NAME" start >/dev/null 2>&1 || true
    elif [ -x "/etc/init.d/$SERVICE_NAME" ]; then
        "/etc/init.d/$SERVICE_NAME" start >/dev/null 2>&1 || true
    fi
}

restore_sparkle_service_if_was_running

if hash update-mime-database 2>/dev/null; then
    update-mime-database /usr/share/mime || true
fi

if hash update-desktop-database 2>/dev/null; then
    update-desktop-database /usr/share/applications || true
fi


================================================
FILE: build/linux/preinst
================================================
#!/bin/bash

SERVICE_NAME='SparkleService'
STATE_DIR='/run/sparkle'
STATE_FILE="$STATE_DIR/service-was-running"
PID_FILES="/var/run/$SERVICE_NAME.pid /run/$SERVICE_NAME.pid"

ensure_state_dir() {
    mkdir -p "$STATE_DIR"
}

mark_service_running() {
    ensure_state_dir && touch "$STATE_FILE"
}

service_pid() {
    for pid_file in $PID_FILES; do
        if [ -f "$pid_file" ]; then
            pid="$(cat "$pid_file" 2>/dev/null || true)"
            if [ -n "$pid" ] && kill -0 "$pid" 2>/dev/null; then
                printf '%s\n' "$pid"
                return 0
            fi
        fi
    done
    return 1
}

service_is_running() {
    if command -v systemctl >/dev/null 2>&1 && systemctl is-active --quiet "$SERVICE_NAME"; then
        return 0
    fi

    service_pid >/dev/null 2>&1
}

stop_service() {
    if command -v systemctl >/dev/null 2>&1; then
        systemctl stop "$SERVICE_NAME" >/dev/null 2>&1 || true
    fi
    if command -v service >/dev/null 2>&1; then
        service "$SERVICE_NAME" stop >/dev/null 2>&1 || true
    fi
    if [ -x "/etc/init.d/$SERVICE_NAME" ]; then
        "/etc/init.d/$SERVICE_NAME" stop >/dev/null 2>&1 || true
    fi
}

force_stop_pid() {
    pid="$(service_pid 2>/dev/null || true)"
    if [ -n "$pid" ]; then
        kill "$pid" >/dev/null 2>&1 || true
    fi
}

wait_service_stopped() {
    i=0
    while [ "$i" -lt 30 ]; do
        if ! service_is_running; then
            return 0
        fi
        sleep 0.5
        i=$((i + 1))
    done
    return 1
}

stop_sparkle_service_if_running() {
    if ! service_is_running; then
        return
    fi

    if ! mark_service_running; then
        echo "Failed to record running Sparkle service state" >&2
        exit 1
    fi

    stop_service
    if wait_service_stopped; then
        return
    fi

    force_stop_pid
    if wait_service_stopped; then
        return
    fi

    echo "Sparkle service is still running; aborting install to avoid replacing active files" >&2
    exit 1
}

stop_sparkle_service_if_running

exit 0


================================================
FILE: build/pkg-scripts/postinstall
================================================
#!/bin/sh

APP_PATH="$2/Sparkle.app"

chown root:admin "$APP_PATH/Contents/Resources/sidecar/mihomo"
chown root:admin "$APP_PATH/Contents/Resources/sidecar/mihomo-alpha"
chmod +s "$APP_PATH/Contents/Resources/sidecar/mihomo"
chmod +s "$APP_PATH/Contents/Resources/sidecar/mihomo-alpha"

/usr/bin/codesign --force --deep --sign - \
  --preserve-metadata=identifier,entitlements,requirements,flags,runtime \
  "$APP_PATH"

restore_sparkle_service_if_was_running() {
  SERVICE_BIN="$1"
  SERVICE_STATE_FILE="/tmp/sparkle-service-was-running"

  if [ ! -f "$SERVICE_STATE_FILE" ]; then
    return
  fi
  rm -f "$SERVICE_STATE_FILE" 2>/dev/null || true

  if [ -x "$SERVICE_BIN" ]; then
    "$SERVICE_BIN" service start >/dev/null 2>&1 || true
  fi
}

restore_sparkle_service_if_was_running "$APP_PATH/Contents/Resources/files/sparkle-service"

exit 0


================================================
FILE: build/pkg-scripts/preinstall
================================================
#!/bin/sh

OLD_SYSPROXY="/Library/PrivilegedHelperTools/sparkle.helper"
SERVICE_NAME="SparkleService"
SERVICE_STATE_FILE="/tmp/sparkle-service-was-running"
APP_PATH="$2/Sparkle.app"
SERVICE_BIN="$APP_PATH/Contents/Resources/files/sparkle-service"

UNINSTALL=false

service_binary_reports_running() {
    [ -x "$SERVICE_BIN" ] || return 1
    STATUS_OUTPUT="$("$SERVICE_BIN" service status 2>&1 || true)"
    printf '%s' "$STATUS_OUTPUT" | grep -Eq '服务状态:运行中|running'
}

launch_service_reports_running() {
    launchctl list 2>/dev/null | awk -v name="$SERVICE_NAME" '$3 == name && $1 != "-" { found = 1 } END { exit found ? 0 : 1 }'
}

service_is_running() {
    service_binary_reports_running || launch_service_reports_running
}

stop_service() {
    if [ -x "$SERVICE_BIN" ]; then
        "$SERVICE_BIN" service stop >/dev/null 2>&1 || true
    else
        launchctl stop "$SERVICE_NAME" >/dev/null 2>&1 || true
    fi
}

wait_service_stopped() {
    i=0
    while [ "$i" -lt 30 ]; do
        if ! service_is_running; then
            return 0
        fi
        sleep 0.5
        i=$((i + 1))
    done
    return 1
}

stop_sparkle_service_if_running() {
    if ! service_is_running; then
        return
    fi

    if ! touch "$SERVICE_STATE_FILE"; then
        echo "Failed to record running Sparkle service state" >&2
        exit 1
    fi

    stop_service
    if wait_service_stopped; then
        return
    fi

    echo "Sparkle service is still running; aborting install to avoid replacing active files" >&2
    exit 1
}

stop_sparkle_service_if_running

if [ ! -f "$OLD_SYSPROXY" ]; then
    UNINSTALL=true
fi

if [ "$UNINSTALL" = true ]; then
    if launchctl list | grep -q "sparkle.helper"; then
        launchctl stop sparkle.helper 2>/dev/null || true
        launchctl unload /Library/LaunchDaemons/sparkle.helper.plist 2>/dev/null || true
    fi

    for file in "/Library/LaunchDaemons/sparkle.helper.plist" \
               "/Library/PrivilegedHelperTools/sparkle.helper" \
               "/tmp/sparkle.helper" \
               "/tmp/sparkle-helper.sock"; do
        if [ -e "$file" ]; then
            rm -f "$file"
        fi
    done
fi

exit 0


================================================
FILE: changelog.md
================================================
### Breaking Changes

- 1.5.0 之后 macOS 改用 pkg 安装方式,不再支持 dmg 安装方式,因此本次更新需要手动下载安装包进行安装
- electron33 已不再支持 macOS 10.15,故为 10.15 提供单独的安装包,需要的用户请自行下载安装,应用内更新时会自动检测系统版本,安装后后续可正常在应用内直接更新
- 1.5.1 之后 Windows 下 `productName` 改为 `Mihomo Party`, 更新后若出现找不到文件报错,手动以管理员权限运行 `Mihomo Party.exe` 即可
- 由于更改了应用名称,开机启动失效是正常现象,在设置中重新开关一下即可

### Features

- 添加出站接口查看
- 添加更多嗅探配置

### Bug Fixes

- null


================================================
FILE: electron-builder.yml
================================================
appId: sparkle.app
productName: Sparkle
directories:
  buildResources: build
files:
  - '!**/.vscode/*'
  - '!src/*'
  - '!aur/*'
  - '!images/*'
  - '!scripts/*'
  - '!extra/*'
  - '!tailwind.config.js'
  - '!postcss.config.js'
  - '!electron.vite.config.{js,ts,mjs,cjs}'
  - '!{.eslintignore,.eslintrc.cjs,.prettierignore,.prettierrc.yaml,dev-app-update.yml,CHANGELOG.md,README.md}'
  - '!{.env,.env.*,.npmrc,pnpm-lock.yaml}'
  - '!{tsconfig.json,tsconfig.node.json,tsconfig.web.json}'
extraResources:
  - from: './extra/'
    to: ''
protocols:
  name: 'Sparkle URI Scheme'
  schemes:
    - 'clash'
    - 'mihomo'
    - 'sparkle'
win:
  target:
    - nsis
    - 7z
  artifactName: ${name}-windows-${version}-${arch}-portable.${ext}
nsis:
  artifactName: ${name}-windows-${version}-${arch}-setup.${ext}
  uninstallDisplayName: ${productName}
  allowToChangeInstallationDirectory: true
  oneClick: false
  perMachine: true
  createDesktopShortcut: true
mac:
  target:
    - pkg
  entitlementsInherit: build/entitlements.mac.plist
  extendInfo:
    - NSCameraUsageDescription: Application requests access to the device's camera.
    - NSMicrophoneUsageDescription: Application requests access to the device's microphone.
    - NSDocumentsFolderUsageDescription: Application requests access to the user's Documents folder.
    - NSDownloadsFolderUsageDescription: Application requests access to the user's Downloads folder.
  notarize: false
  artifactName: ${name}-macos-${version}-${arch}.${ext}
pkg:
  allowAnywhere: false
  allowCurrentUserHome: false
  # background:
  #   alignment: bottomleft
  #   file: build/background.png
linux:
  desktop:
    entry:
      Name: Sparkle
      MimeType: 'x-scheme-handler/clash;x-scheme-handler/mihomo;x-scheme-handler/sparkle'
  target:
    - deb
    - rpm
    - pacman
  maintainer: xishang0128 <xishang02@gmail.com>
  category: Utility
  artifactName: ${name}-linux-${version}-${arch}.${ext}
deb:
  fpm:
    - --before-install=build/linux/preinst
  afterInstall: 'build/linux/postinst'
  depends:
    - libasound2
    - libgtk-3-0
    - libnotify4
    - libnss3
    - libxss1
    - libxtst6
    - xdg-utils
    - libatspi2.0-0
    - libuuid1
    - libsecret-1-0
  recommends:
    - libappindicator3-1
rpm:
  fpm:
    - --before-install=build/linux/preinst
  afterInstall: 'build/linux/postinst'
  depends:
    - alsa-lib
    - gtk3
    - libnotify
    - nss
    - libXScrnSaver
    - libXtst
    - xdg-utils
    - at-spi2-core
    - libuuid
    - libsecret
pacman:
  afterInstall: 'build/linux/postinst'
  fpm:
    - --before-install=build/linux/preinst
    - --pacman-compression
    - zstd
  depends:
    - alsa-lib
    - gtk3
    - libnotify
    - nss
    - libxss
    - libxtst
    - xdg-utils
    - at-spi2-core
    - util-linux-libs
    - libsecret
  artifactName: ${name}-linux-${version}-${arch}.pkg.tar.zst
npmRebuild: true
publish: []


================================================
FILE: electron.vite.config.ts
================================================
import { resolve } from 'path'
import { defineConfig } from 'electron-vite'
import react from '@vitejs/plugin-react'
// https://github.com/vdesjs/vite-plugin-monaco-editor/issues/21#issuecomment-1827562674
import monacoEditorPluginModule from 'vite-plugin-monaco-editor'
import tailwindcss from '@tailwindcss/vite'

const isObjectWithDefaultFunction = (
  module: unknown
): module is { default: typeof monacoEditorPluginModule } =>
  module != null &&
  typeof module === 'object' &&
  'default' in module &&
  typeof module.default === 'function'
const monacoEditorPlugin = isObjectWithDefaultFunction(monacoEditorPluginModule)
  ? monacoEditorPluginModule.default
  : monacoEditorPluginModule

export default defineConfig({
  main: {
    build: {
      externalizeDeps: true
    }
  },
  preload: {
    build: {
      externalizeDeps: true
    }
  },
  renderer: {
    build: {
      rollupOptions: {
        input: {
          index: resolve('src/renderer/index.html'),
          floating: resolve('src/renderer/floating.html'),
          traymenu: resolve('src/renderer/traymenu.html')
        }
      }
    },
    resolve: {
      alias: {
        '@renderer': resolve('src/renderer/src')
      }
    },
    plugins: [
      react(),
      tailwindcss(),
      monacoEditorPlugin({
        languageWorkers: ['editorWorkerService', 'typescript', 'css'],
        customDistPath: (_, out) => `${out}/monacoeditorwork`,
        customWorkers: [
          {
            label: 'yaml',
            entry: 'monaco-yaml/yaml.worker'
          }
        ]
      })
    ]
  }
})


================================================
FILE: eslint.config.cjs
================================================
const js = require('@eslint/js')
const react = require('eslint-plugin-react')
const { configs } = require('@electron-toolkit/eslint-config-ts')

module.exports = [
  {
    ignores: ['**/node_modules/**', '**/dist/**', '**/out/**', '**/extra/**']
  },

  js.configs.recommended,
  ...configs.recommended,

  {
    rules: {
      'preserve-caught-error': 'off'
    }
  },

  {
    files: ['src/renderer/src/**/*.{jsx,tsx}'],
    plugins: {
      react: react
    },
    rules: {
      ...react.configs.recommended.rules,
      ...react.configs['jsx-runtime'].rules
    },
    settings: {
      react: {
        version: '19.2.4'
      }
    },
    languageOptions: {
      ...react.configs.recommended.languageOptions
    }
  },

  {
    files: ['**/*.cjs', '**/*.mjs', '**/tailwind.config.js', '**/postcss.config.js'],
    rules: {
      '@typescript-eslint/no-require-imports': 'off'
    }
  },

  {
    files: ['**/*.{ts,tsx}'],
    rules: {
      '@typescript-eslint/no-unused-vars': 0,
      '@typescript-eslint/explicit-function-return-type': 'off',
      '@typescript-eslint/no-explicit-any': 'warn'
    }
  }
]


================================================
FILE: package.json
================================================
{
  "name": "sparkle",
  "version": "1.26.4",
  "description": "Sparkle",
  "main": "./out/main/index.js",
  "author": {
    "name": "xishang0128",
    "email": "xishang02@gmail.com"
  },
  "homepage": "https://github.com/xishang0128/sparkle",
  "scripts": {
    "format": "prettier --write .",
    "lint": "eslint . --ext .js,.jsx,.cjs,.mjs,.ts,.tsx,.cts,.mts --fix",
    "typecheck:node": "tsc --noEmit -p tsconfig.node.json --composite false",
    "typecheck:web": "tsc --noEmit -p tsconfig.web.json --composite false",
    "typecheck": "pnpm run typecheck:node && pnpm run typecheck:web",
    "prepare": "node scripts/prepare.ts",
    "updater": "node scripts/updater.ts",
    "checksum": "node scripts/checksum.ts",
    "telegram": "node scripts/telegram.ts",
    "dev": "electron-vite dev",
    "postinstall": "electron-builder install-app-deps",
    "build:win": "electron-vite build && electron-builder --publish never --win",
    "build:mac": "electron-vite build && electron-builder --publish never --mac",
    "build:linux": "electron-vite build && electron-builder --publish never --linux"
  },
  "dependencies": {
    "@electron-toolkit/preload": "^3.0.2",
    "@electron-toolkit/utils": "^4.0.0",
    "@heroui-v3/react": "npm:@heroui/react@latest",
    "@heroui-v3/styles": "npm:@heroui/styles@latest",
    "@heroui/react": "2.8.10",
    "@tailwindcss/postcss": "^4.2.2",
    "@tailwindcss/vite": "^4.2.2",
    "@types/crypto-js": "^4.2.2",
    "adm-zip": "^0.5.17",
    "axios": "^1.15.0",
    "crypto-js": "^4.2.0",
    "dayjs": "^1.11.20",
    "express": "^5.2.1",
    "file-icon": "^6.0.0",
    "file-icon-info": "^1.1.1",
    "iconv-lite": "^0.7.2",
    "is-cidr": "^6.0.3",
    "is-ip": "^5.0.1",
    "koffi": "^2.16.0",
    "qrcode.react": "^4.2.0",
    "webdav": "^5.9.0",
    "ws": "^8.20.0",
    "yaml": "^2.8.3"
  },
  "devDependencies": {
    "@dnd-kit/core": "^6.3.1",
    "@dnd-kit/sortable": "^10.0.0",
    "@dnd-kit/utilities": "^3.2.2",
    "@electron-toolkit/eslint-config-prettier": "^3.0.0",
    "@electron-toolkit/eslint-config-ts": "^3.1.0",
    "@electron-toolkit/tsconfig": "^2.0.0",
    "@eslint/eslintrc": "^3.3.5",
    "@eslint/js": "^10.0.1",
    "@types/adm-zip": "^0.5.8",
    "@types/express": "^5.0.6",
    "@types/js-yaml": "^4.0.9",
    "@types/node": "^25.6.0",
    "@types/pubsub-js": "^1.8.6",
    "@types/react": "^19.2.14",
    "@types/react-dom": "^19.2.3",
    "@types/ws": "^8.18.1",
    "@vitejs/plugin-react": "^6.0.1",
    "autoprefixer": "^10.5.0",
    "cron-validator": "^1.4.0",
    "driver.js": "^1.4.0",
    "electron": "^41.2.1",
    "electron-builder": "26.8.2",
    "electron-vite": "^5.0.0",
    "electron-window-state": "^5.0.3",
    "eslint": "^10.2.0",
    "eslint-plugin-react": "^7.37.5",
    "form-data": "^4.0.5",
    "framer-motion": "12.36.0",
    "lodash": "^4.18.1",
    "meta-json-schema": "^1.19.23",
    "monaco-yaml": "^5.4.1",
    "nanoid": "^5.1.9",
    "next-themes": "^0.4.6",
    "postcss": "^8.5.10",
    "prettier": "^3.8.3",
    "pubsub-js": "^1.9.5",
    "react": "^19.2.5",
    "react-dom": "^19.2.5",
    "react-error-boundary": "^6.1.1",
    "react-icons": "^5.6.0",
    "react-markdown": "^10.1.0",
    "react-monaco-editor": "^0.59.0",
    "react-router-dom": "^7.14.1",
    "react-virtuoso": "^4.18.5",
    "recharts": "^3.8.1",
    "swr": "^2.4.1",
    "tailwindcss": "^4.2.2",
    "tar": "^7.5.13",
    "tsx": "^4.21.0",
    "types-pac": "^1.0.3",
    "typescript": "^5.9.3",
    "vite": "npm:rolldown-vite@^7.3.1",
    "vite-plugin-monaco-editor": "1.1.0"
  },
  "pnpm": {
    "onlyBuiltDependencies": [
      "electron"
    ],
    "patchedDependencies": {
      "vite-plugin-monaco-editor@1.1.0": "patches/vite-plugin-monaco-editor@1.1.0.patch"
    },
    "overrides": {
      "vite": "npm:rolldown-vite@^7.3.1"
    }
  },
  "packageManager": "pnpm@10.15.0"
}


================================================
FILE: patches/vite-plugin-monaco-editor@1.1.0.patch
================================================
diff --git a/dist/workerMiddleware.js b/dist/workerMiddleware.js
index dd8681c56cd5106dbc6dbc6c9cd03cbada397dfc..272f75ae738fa2626541bf15e4293ebfe69e49e2 100644
--- a/dist/workerMiddleware.js
+++ b/dist/workerMiddleware.js
@@ -43,7 +43,7 @@ function workerMiddleware(middlewares, config, options) {
     const works = index_1.getWorks(options);
     // clear cacheDir
     if (fs.existsSync(exports.cacheDir)) {
-        fs.rmdirSync(exports.cacheDir, { recursive: true, force: true });
+        fs.rmSync(exports.cacheDir, { recursive: true, force: true });
     }
     for (const work of works) {
         middlewares.use(config.base + options.publicPath + '/' + getFilenameByEntry(work.entry), function (req, res, next) {


================================================
FILE: scripts/checksum.ts
================================================
import { readFileSync, readdirSync, writeFileSync } from 'fs'
import { createHash } from 'crypto'
const files = readdirSync('dist')

for (const file of files) {
  for (const ext of process.argv.slice(2)) {
    if (file.endsWith(ext)) {
      const content = readFileSync(`dist/${file}`)
      const checksum = createHash('sha256').update(content).digest('hex')
      writeFileSync(`dist/${file}.sha256`, checksum)
    }
  }
}


================================================
FILE: scripts/package.json
================================================
{
  "type": "module"
}


================================================
FILE: scripts/prepare.ts
================================================
import fs from 'fs'
import AdmZip from 'adm-zip'
import path from 'path'
import zlib from 'zlib'
import { extract } from 'tar'
import { execSync } from 'child_process'

const cwd = process.cwd()
const TEMP_DIR = path.join(cwd, 'node_modules/.temp')
let arch: string = process.arch
const platform = process.platform
if (process.argv.slice(2).length !== 0) {
  arch = process.argv.slice(2)[0].replace('--', '')
}

if (process.env.SKIP_PREPARE === '1') {
  console.log('Skipping prepare script...')
  process.exit(0)
}

function getErrorMessage(error: unknown) {
  return error instanceof Error ? error.message : String(error)
}

/* ======= mihomo alpha======= */
const MIHOMO_ALPHA_VERSION_URL =
  'https://github.com/MetaCubeX/mihomo/releases/download/Prerelease-Alpha/version.txt'
const MIHOMO_ALPHA_URL_PREFIX = `https://github.com/MetaCubeX/mihomo/releases/download/Prerelease-Alpha`
let MIHOMO_ALPHA_VERSION: string

const MIHOMO_ALPHA_MAP = {
  'win32-x64': 'mihomo-windows-amd64-v3',
  'win32-ia32': 'mihomo-windows-386',
  'win32-arm64': 'mihomo-windows-arm64',
  'darwin-x64': 'mihomo-darwin-amd64-v3',
  'darwin-arm64': 'mihomo-darwin-arm64',
  'linux-x64': 'mihomo-linux-amd64-v3',
  'linux-arm64': 'mihomo-linux-arm64',
  'linux-loong64': 'mihomo-linux-loong64-abi2'
}

// Fetch the latest alpha release version from the version.txt file
async function getLatestAlphaVersion() {
  try {
    const response = await fetch(MIHOMO_ALPHA_VERSION_URL, {
      method: 'GET'
    })
    const v = await response.text()
    MIHOMO_ALPHA_VERSION = v.trim() // Trim to remove extra whitespaces
    console.log(`Latest alpha version: ${MIHOMO_ALPHA_VERSION}`)
  } catch (error) {
    console.error('Error fetching latest alpha version:', getErrorMessage(error))
    process.exit(1)
  }
}

/* ======= mihomo release ======= */
const MIHOMO_VERSION_URL =
  'https://github.com/MetaCubeX/mihomo/releases/latest/download/version.txt'
const MIHOMO_URL_PREFIX = `https://github.com/MetaCubeX/mihomo/releases/download`
let MIHOMO_VERSION: string

const MIHOMO_MAP = {
  'win32-x64': 'mihomo-windows-amd64-v3',
  'win32-ia32': 'mihomo-windows-386',
  'win32-arm64': 'mihomo-windows-arm64',
  'darwin-x64': 'mihomo-darwin-amd64-v3',
  'darwin-arm64': 'mihomo-darwin-arm64',
  'linux-x64': 'mihomo-linux-amd64-v3',
  'linux-arm64': 'mihomo-linux-arm64',
  'linux-loong64': 'mihomo-linux-loong64-abi2'
}

// Fetch the latest release version from the version.txt file
async function getLatestReleaseVersion() {
  try {
    const response = await fetch(MIHOMO_VERSION_URL, {
      method: 'GET'
    })
    const v = await response.text()
    MIHOMO_VERSION = v.trim() // Trim to remove extra whitespaces
    console.log(`Latest release version: ${MIHOMO_VERSION}`)
  } catch (error) {
    console.error('Error fetching latest release version:', getErrorMessage(error))
    process.exit(1)
  }
}

/*
 * check available
 */
if (!MIHOMO_MAP[`${platform}-${arch}`]) {
  throw new Error(`unsupported platform "${platform}-${arch}"`)
}

if (!MIHOMO_ALPHA_MAP[`${platform}-${arch}`]) {
  throw new Error(`unsupported platform "${platform}-${arch}"`)
}

/**
 * core info
 */
function MihomoAlpha() {
  const name = MIHOMO_ALPHA_MAP[`${platform}-${arch}`]
  const isWin = platform === 'win32'
  const urlExt = isWin ? 'zip' : 'gz'
  const downloadURL = `${MIHOMO_ALPHA_URL_PREFIX}/${name}-${MIHOMO_ALPHA_VERSION}.${urlExt}`
  const exeFile = `${name}${isWin ? '.exe' : ''}`
  const zipFile = `${name}-${MIHOMO_ALPHA_VERSION}.${urlExt}`

  return {
    name: 'mihomo-alpha',
    targetFile: `mihomo-alpha${isWin ? '.exe' : ''}`,
    exeFile,
    zipFile,
    downloadURL
  }
}

function mihomo() {
  const name = MIHOMO_MAP[`${platform}-${arch}`]
  const isWin = platform === 'win32'
  const urlExt = isWin ? 'zip' : 'gz'
  const downloadURL = `${MIHOMO_URL_PREFIX}/${MIHOMO_VERSION}/${name}-${MIHOMO_VERSION}.${urlExt}`
  const exeFile = `${name}${isWin ? '.exe' : ''}`
  const zipFile = `${name}-${MIHOMO_VERSION}.${urlExt}`

  return {
    name: 'mihomo',
    targetFile: `mihomo${isWin ? '.exe' : ''}`,
    exeFile,
    zipFile,
    downloadURL
  }
}
/**
 * download sidecar and rename
 */
async function resolveSidecar(binInfo) {
  const { name, targetFile, zipFile, exeFile, downloadURL } = binInfo

  const sidecarDir = path.join(cwd, 'extra', 'sidecar')
  const sidecarPath = path.join(sidecarDir, targetFile)

  fs.mkdirSync(sidecarDir, { recursive: true })
  if (fs.existsSync(sidecarPath)) {
    fs.rmSync(sidecarPath)
  }
  const tempDir = path.join(TEMP_DIR, name)
  const tempZip = path.join(tempDir, zipFile)
  const tempExe = path.join(tempDir, exeFile)

  fs.mkdirSync(tempDir, { recursive: true })
  try {
    if (!fs.existsSync(tempZip)) {
      await downloadFile(downloadURL, tempZip)
    }

    if (zipFile.endsWith('.zip')) {
      const zip = new AdmZip(tempZip)
      zip.getEntries().forEach((entry) => {
        console.log(`[DEBUG]: "${name}" entry name`, entry.entryName)
      })
      zip.extractAllTo(tempDir, true)
      fs.renameSync(tempExe, sidecarPath)
      console.log(`[INFO]: "${name}" unzip finished`)
    } else if (zipFile.endsWith('.tgz')) {
      // tgz
      fs.mkdirSync(tempDir, { recursive: true })
      await extract({
        cwd: tempDir,
        file: tempZip
      })
      const files = fs.readdirSync(tempDir)
      console.log(`[DEBUG]: "${name}" files in tempDir:`, files)
      const extractedFile = files.find((file) => file.startsWith('虚空终端-'))
      if (extractedFile) {
        const extractedFilePath = path.join(tempDir, extractedFile)
        fs.renameSync(extractedFilePath, sidecarPath)
        console.log(`[INFO]: "${name}" file renamed to "${sidecarPath}"`)
        execSync(`chmod 755 ${sidecarPath}`)
        console.log(`[INFO]: "${name}" chmod binary finished`)
      } else {
        throw new Error(`Expected file not found in ${tempDir}`)
      }
    } else {
      // gz
      const readStream = fs.createReadStream(tempZip)
      const writeStream = fs.createWriteStream(sidecarPath)
      await new Promise<void>((resolve, reject) => {
        const onError = (error: unknown) => {
          console.error(`[ERROR]: "${name}" gz failed:`, getErrorMessage(error))
          reject(error)
        }
        readStream
          .pipe(zlib.createGunzip().on('error', onError))
          .pipe(writeStream)
          .on('finish', () => {
            console.log(`[INFO]: "${name}" gunzip finished`)
            execSync(`chmod 755 ${sidecarPath}`)
            console.log(`[INFO]: "${name}" chmod binary finished`)
            resolve()
          })
          .on('error', onError)
      })
    }
  } catch (err) {
    // 需要删除文件
    fs.rmSync(sidecarPath)
    throw err
  } finally {
    fs.rmSync(tempDir, { recursive: true })
  }
}

/**
 * download the file to the extra dir
 */
async function resolveResource(binInfo) {
  const { file, downloadURL, needExecutable = false } = binInfo

  const resDir = path.join(cwd, 'extra', 'files')
  const targetPath = path.join(resDir, file)

  if (fs.existsSync(targetPath)) {
    fs.rmSync(targetPath)
  }

  fs.mkdirSync(resDir, { recursive: true })
  await downloadFile(downloadURL, targetPath)

  if (needExecutable && platform !== 'win32') {
    execSync(`chmod 755 ${targetPath}`)
    console.log(`[INFO]: ${file} chmod finished`)
  }

  console.log(`[INFO]: ${file} finished`)
}

/**
 * download file and save to `path`
 */
async function downloadFile(url, path) {
  const response = await fetch(url, {
    method: 'GET',
    headers: { 'Content-Type': 'application/octet-stream' }
  })
  const buffer = await response.arrayBuffer()
  fs.writeFileSync(path, new Uint8Array(buffer))

  console.log(`[INFO]: download finished "${url}"`)
}

const resolveMmdb = () =>
  resolveResource({
    file: 'country.mmdb',
    downloadURL: `https://github.com/MetaCubeX/meta-rules-dat/releases/download/latest/country-lite.mmdb`
  })
const resolveMetadb = () =>
  resolveResource({
    file: 'geoip.metadb',
    downloadURL: `https://github.com/MetaCubeX/meta-rules-dat/releases/download/latest/geoip.metadb`
  })
const resolveGeosite = () =>
  resolveResource({
    file: 'geosite.dat',
    downloadURL: `https://github.com/MetaCubeX/meta-rules-dat/releases/download/latest/geosite.dat`
  })
const resolveGeoIP = () =>
  resolveResource({
    file: 'geoip.dat',
    downloadURL: `https://github.com/MetaCubeX/meta-rules-dat/releases/download/latest/geoip.dat`
  })
const resolveASN = () =>
  resolveResource({
    file: 'ASN.mmdb',
    downloadURL: `https://github.com/MetaCubeX/meta-rules-dat/releases/download/latest/GeoLite2-ASN.mmdb`
  })
const resolveEnableLoopback = () =>
  resolveResource({
    file: 'enableLoopback.exe',
    downloadURL: `https://github.com/Kuingsmile/uwp-tool/releases/download/latest/enableLoopback.exe`
  })
const resolveSparkleService = () => {
  const map = {
    'win32-x64': 'sparkle-service-windows-amd64-v3',
    'win32-ia32': 'sparkle-service-windows-386',
    'win32-arm64': 'sparkle-service-windows-arm64',
    'darwin-x64': 'sparkle-service-darwin-amd64-v3',
    'darwin-arm64': 'sparkle-service-darwin-arm64',
    'linux-x64': 'sparkle-service-linux-amd64-v3',
    'linux-arm64': 'sparkle-service-linux-arm64',
    'linux-loong64': 'sparkle-service-linux-loong64-abi2'
  }
  if (!map[`${platform}-${arch}`]) {
    throw new Error(`unsupported platform "${platform}-${arch}"`)
  }
  const base = map[`${platform}-${arch}`]
  const ext = platform == 'win32' ? '.exe' : ''

  return resolveResource({
    file: `sparkle-service${ext}`,
    downloadURL: `https://github.com/xishang0128/sparkle-service/releases/download/pre-release/${base}${ext}`,
    needExecutable: true
  })
}
const resolveRunner = () =>
  resolveResource({
    file: 'sparkle-run.exe',
    downloadURL: `https://github.com/xishang0128/sparkle-run/releases/download/${arch}/sparkle-run.exe`
  })

const resolveMonitor = async () => {
  const tempDir = path.join(TEMP_DIR, 'TrafficMonitor')
  const tempZip = path.join(tempDir, `${arch}.zip`)
  if (!fs.existsSync(tempDir)) {
    fs.mkdirSync(tempDir, { recursive: true })
  }
  await downloadFile(
    `https://github.com/xishang0128/sparkle-run/releases/download/monitor/${arch}.zip`,
    tempZip
  )
  const zip = new AdmZip(tempZip)
  const resDir = path.join(cwd, 'extra', 'files')
  const targetPath = path.join(resDir, 'TrafficMonitor')
  if (fs.existsSync(targetPath)) {
    fs.rmSync(targetPath, { recursive: true })
  }
  zip.extractAllTo(targetPath, true)

  console.log(`[INFO]: TrafficMonitor finished`)
}

const resolve7zip = () =>
  resolveResource({
    file: '7za.exe',
    downloadURL: `https://github.com/develar/7zip-bin/raw/master/win/${arch}/7za.exe`
  })
const resolveSubstore = () =>
  resolveResource({
    file: 'sub-store.bundle.js',
    downloadURL:
      'https://github.com/sub-store-org/Sub-Store/releases/latest/download/sub-store.bundle.js'
  })
const resolveSubstoreFrontend = async () => {
  const tempDir = path.join(TEMP_DIR, 'substore-frontend')
  const tempZip = path.join(tempDir, 'dist.zip')
  if (!fs.existsSync(tempDir)) {
    fs.mkdirSync(tempDir, { recursive: true })
  }
  await downloadFile(
    'https://github.com/sub-store-org/Sub-Store-Front-End/releases/latest/download/dist.zip',
    tempZip
  )
  const zip = new AdmZip(tempZip)
  const resDir = path.join(cwd, 'extra', 'files')
  const targetPath = path.join(resDir, 'sub-store-frontend')
  if (fs.existsSync(targetPath)) {
    fs.rmSync(targetPath, { recursive: true })
  }
  zip.extractAllTo(resDir, true)
  fs.renameSync(path.join(resDir, 'dist'), targetPath)

  if (platform !== 'win32') {
    try {
      const fixPermissions = (dir) => {
        const items = fs.readdirSync(dir, { withFileTypes: true })
        for (const item of items) {
          const fullPath = path.join(dir, item.name)
          if (item.isDirectory()) {
            fs.chmodSync(fullPath, 0o755)
            fixPermissions(fullPath)
          } else {
            fs.chmodSync(fullPath, 0o644)
          }
        }
      }
      fs.chmodSync(targetPath, 0o755)
      fixPermissions(targetPath)
      console.log(`[INFO]: sub-store-frontend permissions fixed`)
    } catch (error) {
      console.warn(`[WARN]: Failed to fix permissions: ${getErrorMessage(error)}`)
    }
  }

  console.log(`[INFO]: sub-store-frontend finished`)
}
const resolveFont = async () => {
  // const targetPath = path.join(cwd, 'src', 'renderer', 'src', 'assets', 'NotoColorEmoji.ttf')
  const targetPath = path.join(cwd, 'src', 'renderer', 'src', 'assets', 'twemoji.ttf')

  if (fs.existsSync(targetPath)) {
    return
  }
  await downloadFile(
    // 'https://github.com/googlefonts/noto-emoji/raw/main/fonts/NotoColorEmoji.ttf',
    'https://github.com/Sav22999/emoji/raw/refs/heads/master/font/twemoji.ttf',
    targetPath
  )

  console.log(`[INFO]: twemoji.ttf finished`)
}

type Task = {
  name: string
  func: () => Promise<void>
  retry: number
  winOnly?: boolean
  linuxOnly?: boolean
  unixOnly?: boolean
}

const tasks: Task[] = [
  {
    name: 'mihomo-alpha',
    func: () => getLatestAlphaVersion().then(() => resolveSidecar(MihomoAlpha())),
    retry: 5
  },
  {
    name: 'mihomo',
    func: () => getLatestReleaseVersion().then(() => resolveSidecar(mihomo())),
    retry: 5
  },
  { name: 'mmdb', func: resolveMmdb, retry: 5 },
  { name: 'metadb', func: resolveMetadb, retry: 5 },
  { name: 'geosite', func: resolveGeosite, retry: 5 },
  { name: 'geoip', func: resolveGeoIP, retry: 5 },
  { name: 'asn', func: resolveASN, retry: 5 },
  {
    name: 'font',
    func: resolveFont,
    retry: 5
  },
  {
    name: 'enableLoopback',
    func: resolveEnableLoopback,
    retry: 5,
    winOnly: true
  },
  {
    name: 'sparkle-service',
    func: resolveSparkleService,
    retry: 5
  },
  {
    name: 'runner',
    func: resolveRunner,
    retry: 5,
    winOnly: true
  },
  {
    name: 'monitor',
    func: resolveMonitor,
    retry: 5,
    winOnly: true
  },
  {
    name: 'substore',
    func: resolveSubstore,
    retry: 5
  },
  {
    name: 'substorefrontend',
    func: resolveSubstoreFrontend,
    retry: 5
  },
  {
    name: '7zip',
    func: resolve7zip,
    retry: 5,
    winOnly: true
  }
]

async function runTask() {
  const task = tasks.shift()
  if (!task) return
  if (task.winOnly && platform !== 'win32') return runTask()
  if (task.linuxOnly && platform !== 'linux') return runTask()
  if (task.unixOnly && platform === 'win32') return runTask()

  for (let i = 0; i < task.retry; i++) {
    try {
      await task.func()
      break
    } catch (err) {
      console.error(`[ERROR]: task::${task.name} try ${i} ==`, getErrorMessage(err))
      if (i === task.retry - 1) throw err
    }
  }
  return runTask()
}

runTask()
runTask()


================================================
FILE: scripts/telegram.ts
================================================
import axios from 'axios'
import { readFileSync } from 'fs'

const chat_id = '@MihomoPartyChannel'
const pkg = readFileSync('package.json', 'utf-8')
const changelog = readFileSync('changelog.md', 'utf-8')
const { version } = JSON.parse(pkg)
const downloadUrl = `https://github.com/mihomo-party-org/mihomo-party/releases/download/v${version}`
let content = `<b>🌟 <a href="https://github.com/mihomo-party-org/mihomo-party/releases/tag/v${version}">Mihomo Party v${version}</a> 正式发布</b>\n\n`
for (const line of changelog.split('\n')) {
  if (line.length === 0) {
    content += '\n'
  } else if (line.startsWith('### ')) {
    content += `<b>${line.replace('### ', '')}</b>\n`
  } else {
    content += `${line}\n`
  }
}

content += '\n<b>下载地址:</b>\n<b>Windows10/11:</b>\n'
content += `安装版:<a href="${downloadUrl}/mihomo-party-windows-${version}-x64-setup.exe">64 位</a> | <a href="${downloadUrl}/mihomo-party-windows-${version}-ia32-setup.exe">32 位</a> | <a href="${downloadUrl}/mihomo-party-windows-${version}-arm64-setup.exe">ARM64</a>\n`
content += `便携版:<a href="${downloadUrl}/mihomo-party-windows-${version}-x64-portable.7z">64 位</a> | <a href="${downloadUrl}/mihomo-party-windows-${version}-ia32-portable.7z">32 位</a> | <a href="${downloadUrl}/mihomo-party-windows-${version}-arm64-portable.7z">ARM64</a>\n`
content += '\n<b>Windows7/8:</b>\n'
content += `安装版:<a href="${downloadUrl}/mihomo-party-win7-${version}-x64-setup.exe">64 位</a> | <a href="${downloadUrl}/mihomo-party-win7-${version}-ia32-setup.exe">32 位</a>\n`
content += `便携版:<a href="${downloadUrl}/mihomo-party-win7-${version}-x64-portable.7z">64 位</a> | <a href="${downloadUrl}/mihomo-party-win7-${version}-ia32-portable.7z">32 位</a>\n`
content += '\n<b>macOS 11+:</b>\n'
content += `PKG:<a href="${downloadUrl}/mihomo-party-macos-${version}-x64.pkg
">Intel</a> | <a href="${downloadUrl}/mihomo-party-macos-${version}-arm64.pkg">Apple Silicon</a>\n`
content += '\n<b>macOS 10.15+:</b>\n'
content += `PKG:<a href="${downloadUrl}/mihomo-party-catalina-${version}-x64.pkg
">Intel</a> | <a href="${downloadUrl}/mihomo-party-catalina-${version}-arm64.pkg">Apple Silicon</a>\n`
content += '\n<b>Linux:</b>\n'
content += `DEB:<a href="${downloadUrl}/mihomo-party-linux-${version}-amd64.deb
">64 位</a> | <a href="${downloadUrl}/mihomo-party-linux-${version}-arm64.deb">ARM64</a>\n`
content += `RPM:<a href="${downloadUrl}/mihomo-party-linux-${version}-x86_64.rpm">64 位</a> | <a href="${downloadUrl}/mihomo-party-linux-${version}-aarch64.rpm">ARM64</a>`

await axios.post(`https://api.telegram.org/bot${process.env.TELEGRAM_BOT_TOKEN}/sendMessage`, {
  chat_id,
  text: content,
  link_preview_options: {
    is_disabled: false,
    url: 'https://github.com/mihomo-party-org/mihomo-party',
    prefer_large_media: true
  },
  parse_mode: 'HTML'
})


================================================
FILE: scripts/updater.ts
================================================
import yaml from 'yaml'
import { readFileSync, writeFileSync } from 'fs'

const pkg = readFileSync('package.json', 'utf-8')
let changelog = readFileSync('changelog.md', 'utf-8')
const { version } = JSON.parse(pkg)
const downloadUrl = `https://github.com/xishang0128/sparkle/releases/download/${version}`
const latest = {
  version,
  changelog
}

if (process.env.SKIP_CHANGELOG !== '1') {
  changelog += '\n### 下载地址:\n\n#### Windows10/11:\n\n'
  changelog += `- 安装版:[64 位](${downloadUrl}/sparkle-windows-${version}-x64-setup.exe) | [ARM64](${downloadUrl}/sparkle-windows-${version}-arm64-setup.exe)\n\n`
  changelog += '\n#### macOS 11+:\n\n'
  changelog += `- PKG:[Intel](${downloadUrl}/sparkle-macos-${version}-x64.pkg) | [Apple Silicon](${downloadUrl}/sparkle-macos-${version}-arm64.pkg)\n\n`
  changelog += '\n#### Linux:\n\n'
  changelog += `- DEB:[64 位](${downloadUrl}/sparkle-linux-${version}-amd64.deb) | [ARM64](${downloadUrl}/sparkle-linux-${version}-arm64.deb) | [loong64](${downloadUrl}/sparkle-linux-${version}-loong64.deb)\n\n`
  changelog += `- RPM:[64 位](${downloadUrl}/sparkle-linux-${version}-x86_64.rpm) | [ARM64](${downloadUrl}/sparkle-linux-${version}-aarch64.rpm) | [loong64](${downloadUrl}/sparkle-linux-${version}-loongarch64.rpm)\n\n`
  changelog += `- PACMAN:[64 位](${downloadUrl}/sparkle-linux-${version}-x64.pkg.tar.zst) | [ARM64](${downloadUrl}/sparkle-linux-${version}-aarch64.pkg.tar.zst) | [loong64](${downloadUrl}/sparkle-linux-${version}-loong64.pkg.tar.zst)`
}
writeFileSync('latest.yml', yaml.stringify(latest))
writeFileSync('changelog.md', changelog)


================================================
FILE: src/main/config/app.ts
================================================
import { readFile, writeFile, rename, copyFile, unlink } from 'fs/promises'
import { appConfigPath } from '../utils/dirs'
import { parseYaml, stringifyYaml } from '../utils/yaml'
import { deepMerge } from '../utils/merge'
import { defaultConfig } from '../utils/template'
import { readFileSync, existsSync } from 'fs'
import { encryptString, decryptString, isEncrypted } from '../utils/encrypt'

let appConfig: AppConfig
let writePromise: Promise<void> = Promise.resolve()

const ENCRYPTED_FIELDS = ['systemCorePath', 'serviceAuthKey'] as const
const PLAINTEXT_FALLBACK_FIELDS = ['systemCorePath'] as const

function isValidConfig(config: unknown): config is AppConfig {
  if (!config || typeof config !== 'object') return false
  const cfg = config as Partial<AppConfig>
  return 'sysProxy' in cfg && typeof cfg.sysProxy === 'object' && cfg.sysProxy !== null
}

async function safeWriteConfig(content: string): Promise<void> {
  const configPath = appConfigPath()
  const tmpPath = `${configPath}.tmp`
  const backupPath = `${configPath}.backup`

  try {
    await writeFile(tmpPath, content, 'utf-8')
    if (existsSync(configPath)) {
      await copyFile(configPath, backupPath)
      if (process.platform === 'win32') {
        await unlink(configPath)
      }
    }
    if (existsSync(tmpPath)) {
      await rename(tmpPath, configPath)
    }
  } catch (e) {
    if (existsSync(tmpPath)) {
      try {
        await unlink(tmpPath)
      } catch {
        // ignore
      }
    }
    throw e
  }
}

function decryptConfig(config: AppConfig): AppConfig {
  const result = { ...config }

  for (const field of ENCRYPTED_FIELDS) {
    const value = result[field]
    if (value && typeof value === 'string') {
      if (!isEncrypted(value)) {
        if ((PLAINTEXT_FALLBACK_FIELDS as readonly string[]).includes(field)) continue
        ;(result[field] as string) = ''
      } else {
        ;(result[field] as string) = decryptString(value)
      }
    }
  }

  return result
}

function encryptConfig(config: AppConfig): AppConfig {
  const result = { ...config }

  for (const field of ENCRYPTED_FIELDS) {
    const value = result[field]
    if (value && typeof value === 'string') {
      ;(result[field] as string) = encryptString(value)
    }
  }

  return result
}

export async function getAppConfig(force = false): Promise<AppConfig> {
  if (force || !appConfig) {
    try {
      const data = await readFile(appConfigPath(), 'utf-8')
      const parsed = parseYaml<AppConfig>(data)
      if (!parsed || !isValidConfig(parsed)) {
        const backup = await readFile(`${appConfigPath()}.backup`, 'utf-8')
        appConfig = decryptConfig(parseYaml<AppConfig>(backup))
      } else {
        appConfig = decryptConfig(parsed)
      }
    } catch (e) {
      appConfig = defaultConfig
    }
  }
  if (typeof appConfig !== 'object') appConfig = defaultConfig
  return appConfig
}

export async function patchAppConfig(patch: Partial<AppConfig>): Promise<void> {
  const previousPromise = writePromise
  writePromise = (async () => {
    await previousPromise
    appConfig = deepMerge(appConfig, patch)
    await safeWriteConfig(stringifyYaml(encryptConfig(appConfig)))
  })()
  await writePromise
}

export function getAppConfigSync(): AppConfig {
  try {
    const raw = readFileSync(appConfigPath(), 'utf-8')
    const data = parseYaml<AppConfig>(raw)
    if (typeof data === 'object' && data !== null) {
      return decryptConfig(data)
    }
    return defaultConfig
  } catch (e) {
    return defaultConfig
  }
}


================================================
FILE: src/main/config/controledMihomo.ts
================================================
import { controledMihomoConfigPath } from '../utils/dirs'
import { readFile, writeFile } from 'fs/promises'
import { parseYaml, stringifyYaml } from '../utils/yaml'
import { generateProfile } from '../core/factory'
import { getAppConfig } from './app'
import { defaultControledMihomoConfig } from '../utils/template'
import { deepMerge } from '../utils/merge'

let controledMihomoConfig: Partial<MihomoConfig> // mihomo.yaml

export async function getControledMihomoConfig(force = false): Promise<Partial<MihomoConfig>> {
  if (force || !controledMihomoConfig) {
    try {
      const data = await readFile(controledMihomoConfigPath(), 'utf-8')
      controledMihomoConfig = parseYaml<Partial<MihomoConfig>>(data) || defaultControledMihomoConfig
    } catch (error) {
      if ((error as NodeJS.ErrnoException).code !== 'ENOENT') {
        throw error
      }
      controledMihomoConfig = defaultControledMihomoConfig
      await writeFile(controledMihomoConfigPath(), stringifyYaml(controledMihomoConfig), 'utf-8')
    }
  }
  if (typeof controledMihomoConfig !== 'object')
    controledMihomoConfig = defaultControledMihomoConfig
  return controledMihomoConfig
}

export async function patchControledMihomoConfig(patch: Partial<MihomoConfig>): Promise<void> {
  await getControledMihomoConfig()
  const { controlDns = true, controlSniff = true } = await getAppConfig()
  if (!controlDns) {
    delete controledMihomoConfig.dns
    delete controledMihomoConfig.hosts
  } else {
    // 从不接管状态恢复
    if (controledMihomoConfig.dns?.ipv6 === undefined) {
      controledMihomoConfig.dns = defaultControledMihomoConfig.dns
    }
  }
  if (!controlSniff) {
    delete controledMihomoConfig.sniffer
  } else {
    // 从不接管状态恢复
    if (!controledMihomoConfig.sniffer) {
      controledMihomoConfig.sniffer = defaultControledMihomoConfig.sniffer
    }
  }
  if (patch.dns?.['nameserver-policy']) {
    controledMihomoConfig.dns = controledMihomoConfig.dns || {}
    controledMihomoConfig.dns['nameserver-policy'] = patch.dns['nameserver-policy']
  }
  if (patch.dns?.['proxy-server-nameserver-policy']) {
    controledMihomoConfig.dns = controledMihomoConfig.dns || {}
    controledMihomoConfig.dns['proxy-server-nameserver-policy'] =
      patch.dns['proxy-server-nameserver-policy']
  }
  if (patch.dns?.['use-hosts']) {
    controledMihomoConfig.hosts = patch.hosts
  }
  controledMihomoConfig = deepMerge(controledMihomoConfig, patch)
  await generateProfile()
  await writeFile(controledMihomoConfigPath(), stringifyYaml(controledMihomoConfig), 'utf-8')
}


================================================
FILE: src/main/config/index.ts
================================================
export { getAppConfig, patchAppConfig } from './app'
export { getControledMihomoConfig, patchControledMihomoConfig } from './controledMihomo'
export {
  getProfile,
  getCurrentProfileItem,
  getProfileItem,
  getProfileConfig,
  getFileStr,
  setFileStr,
  saveFileStrWithElevation,
  setProfileConfig,
  addProfileItem,
  removeProfileItem,
  createProfile,
  getProfileStr,
  getProfileParseStr,
  setProfileStr,
  changeCurrentProfile,
  updateProfileItem
} from './profile'
export {
  getOverrideConfig,
  setOverrideConfig,
  getOverrideItem,
  addOverrideItem,
  removeOverrideItem,
  createOverride,
  getOverride,
  setOverride,
  updateOverrideItem
} from './override'


================================================
FILE: src/main/config/override.ts
================================================
import { overrideConfigPath, overridePath } from '../utils/dirs'
import { getControledMihomoConfig } from './controledMihomo'
import { readFile, writeFile, rm } from 'fs/promises'
import { existsSync } from 'fs'
import axios, { AxiosResponse } from 'axios'
import https from 'https'
import http from 'http'
import tls from 'tls'
import { parseYaml, stringifyYaml } from '../utils/yaml'
import { getCertFingerprint } from './profile'

let overrideConfig: OverrideConfig // override.yaml

export async function getOverrideConfig(force = false): Promise<OverrideConfig> {
  if (force || !overrideConfig) {
    const data = await readFile(overrideConfigPath(), 'utf-8')
    overrideConfig = parseYaml<OverrideConfig>(data) || { items: [] }
  }
  if (typeof overrideConfig !== 'object') overrideConfig = { items: [] }
  return overrideConfig
}

export async function setOverrideConfig(config: OverrideConfig): Promise<void> {
  overrideConfig = config
  await writeFile(overrideConfigPath(), stringifyYaml(overrideConfig), 'utf-8')
}

export async function getOverrideItem(id: string | undefined): Promise<OverrideItem | undefined> {
  const { items } = await getOverrideConfig()
  return items.find((item) => item.id === id)
}

export async function updateOverrideItem(item: OverrideItem): Promise<void> {
  const config = await getOverrideConfig()
  const index = config.items.findIndex((i) => i.id === item.id)
  if (index === -1) {
    throw new Error('Override not found')
  }
  config.items[index] = item
  await setOverrideConfig(config)
}

export async function addOverrideItem(item: Partial<OverrideItem>): Promise<void> {
  const config = await getOverrideConfig()
  const newItem = await createOverride(item)
  if (await getOverrideItem(item.id)) {
    updateOverrideItem(newItem)
  } else {
    config.items.push(newItem)
  }
  await setOverrideConfig(config)
}

export async function removeOverrideItem(id: string): Promise<void> {
  const config = await getOverrideConfig()
  const item = await getOverrideItem(id)
  config.items = config.items?.filter((item) => item.id !== id)
  await setOverrideConfig(config)
  await rm(overridePath(id, item?.ext || 'js'))
}

export async function createOverride(item: Partial<OverrideItem>): Promise<OverrideItem> {
  const id = item.id || new Date().getTime().toString(16)
  const newItem = {
    id,
    name: item.name || (item.type === 'remote' ? 'Remote File' : 'Local File'),
    type: item.type,
    ext: item.ext || 'js',
    url: item.url,
    global: item.global || false,
    updated: new Date().getTime()
  } as OverrideItem
  switch (newItem.type) {
    case 'remote': {
      const { 'mixed-port': mixedPort = 7890 } = await getControledMihomoConfig()
      if (!item.url) throw new Error('Empty URL')
      let res: AxiosResponse
      try {
        const httpsAgent = new https.Agent({ rejectUnauthorized: !item.fingerprint })

        if (item.fingerprint) {
          const expected = item.fingerprint.replace(/:/g, '').toUpperCase()
          const verify = (s: tls.TLSSocket) => {
            if (getCertFingerprint(s.getPeerCertificate()) !== expected)
              s.destroy(new Error('证书指纹不匹配'))
          }

          if (mixedPort != 0) {
            const urlObj = new URL(item.url)
            const hostname = urlObj.hostname
            const port = urlObj.port || '443'
            httpsAgent.createConnection = (_, cb) => {
              const req = http.request({
                host: '127.0.0.1',
                port: mixedPort,
                method: 'CONNECT',
                path: `${hostname}:${port}`
              })

              req.on('connect', (res, sock, head) => {
                if (res.statusCode !== 200) {
                  cb?.(new Error(`代理连接失败,状态码:${res.statusCode}`), null!)
                  return
                }
                if (head.length > 0) sock.unshift(head)
                const tls$ = tls.connect(
                  { socket: sock, servername: hostname, rejectUnauthorized: false },
                  () => verify(tls$)
                )
                cb?.(null, tls$)
              })

              req.on('error', (e) => cb?.(e, null!))
              req.end()
              return null!
            }
          } else {
            const conn = httpsAgent.createConnection.bind(httpsAgent)
            httpsAgent.createConnection = (o, c) => {
              const sock = conn(o, c)
              sock?.once('secureConnect', function (this: tls.TLSSocket) {
                verify(this)
              })
              return sock
            }
          }
        }

        res = await axios.get(item.url, {
          httpsAgent,
          ...(mixedPort != 0 &&
            !item.fingerprint && {
              proxy: { protocol: 'http', host: '127.0.0.1', port: mixedPort }
            }),
          responseType: 'text'
        })
      } catch (error) {
        if (axios.isAxiosError(error)) {
          if (error.code === 'ECONNRESET' || error.code === 'ECONNABORTED') {
            throw new Error(`网络连接被重置或超时:${item.url}`)
          } else if (error.code === 'CERT_HAS_EXPIRED') {
            throw new Error(`服务器证书已过期:${item.url}`)
          } else if (error.code === 'UNABLE_TO_VERIFY_LEAF_SIGNATURE') {
            throw new Error(`无法验证服务器证书:${item.url}`)
          } else if (error.message.includes('Certificate verification failed')) {
            throw new Error(`证书验证失败:${item.url}`)
          } else {
            throw new Error(`请求失败:${error.message}`)
          }
        }
        throw error
      }

      const data = res.data
      await setOverride(id, newItem.ext, data)
      break
    }
    case 'local': {
      const data = item.file || ''
      setOverride(id, newItem.ext, data)
      break
    }
  }

  return newItem
}

export async function getOverride(id: string, ext: 'js' | 'yaml' | 'log'): Promise<string> {
  if (!existsSync(overridePath(id, ext))) {
    return ''
  }
  return await readFile(overridePath(id, ext), 'utf-8')
}

export async function setOverride(id: string, ext: 'js' | 'yaml', content: string): Promise<void> {
  await writeFile(overridePath(id, ext), content, 'utf-8')
}


================================================
FILE: src/main/config/profile.ts
================================================
import { getControledMihomoConfig } from './controledMihomo'
import { mihomoProfileWorkDir, mihomoWorkDir, profileConfigPath, profilePath } from '../utils/dirs'
import { addProfileUpdater, delProfileUpdater } from '../core/profileUpdater'
import { readFile, writeFile, rm, mkdir } from 'fs/promises'
import { restartCore } from '../core/manager'
import { getAppConfig } from './app'
import { existsSync } from 'fs'
import axios, { AxiosResponse } from 'axios'
import https from 'https'
import http from 'http'
import tls from 'tls'
import crypto from 'crypto'
import { URL } from 'url'
import { parseYaml, stringifyYaml } from '../utils/yaml'
import { defaultProfile } from '../utils/template'
import { subStorePort } from '../resolve/server'
import { dirname, isAbsolute, join, relative, resolve } from 'path'
import { deepMerge } from '../utils/merge'
import { getUserAgent } from '../utils/userAgent'
import { execWithElevation } from '../utils/elevation'

let profileConfig: ProfileConfig // profile.yaml
const FILE_PERMISSION_ELEVATION_REQUIRED = 'FILE_PERMISSION_ELEVATION_REQUIRED'

export function getCertFingerprint(cert: tls.PeerCertificate) {
  return crypto.createHash('sha256').update(cert.raw).digest('hex').toUpperCase()
}

export async function getProfileConfig(force = false): Promise<ProfileConfig> {
  if (force || !profileConfig) {
    const data = await readFile(profileConfigPath(), 'utf-8')
    profileConfig = parseYaml(data) || { items: [] }
  }
  if (typeof profileConfig !== 'object') profileConfig = { items: [] }
  return profileConfig
}

export async function setProfileConfig(config: ProfileConfig): Promise<void> {
  profileConfig = config
  await writeFile(profileConfigPath(), stringifyYaml(config), 'utf-8')
}

export async function getProfileItem(id: string | undefined): Promise<ProfileItem | undefined> {
  const { items } = await getProfileConfig()
  if (!id || id === 'default') return { id: 'default', type: 'local', name: '空白订阅' }
  return items.find((item) => item.id === id)
}

export async function changeCurrentProfile(id: string): Promise<void> {
  const config = await getProfileConfig()
  const current = config.current
  config.current = id
  await setProfileConfig(config)
  try {
    await restartCore()
  } catch (e) {
    config.current = current
    throw e
  } finally {
    await setProfileConfig(config)
  }
}

export async function updateProfileItem(item: ProfileItem): Promise<void> {
  const config = await getProfileConfig()
  const index = config.items.findIndex((i) => i.id === item.id)
  if (index === -1) {
    throw new Error('Profile not found')
  }
  config.items[index] = item
  if (!item.autoUpdate) await delProfileUpdater(item.id)
  await setProfileConfig(config)
}

export async function addProfileItem(item: Partial<ProfileItem>): Promise<void> {
  const newItem = await createProfile(item)
  const config = await getProfileConfig()
  if (await getProfileItem(newItem.id)) {
    await updateProfileItem(newItem)
  } else {
    config.items.push(newItem)
  }
  await setProfileConfig(config)

  if (!config.current) {
    await changeCurrentProfile(newItem.id)
  }
  await addProfileUpdater(newItem)
}

export async function removeProfileItem(id: string): Promise<void> {
  const config = await getProfileConfig()
  config.items = config.items?.filter((item) => item.id !== id)
  let shouldRestart = false
  if (config.current === id) {
    shouldRestart = true
    if (config.items.length > 0) {
      config.current = config.items[0].id
    } else {
      config.current = undefined
    }
  }
  await setProfileConfig(config)
  if (existsSync(profilePath(id))) {
    await rm(profilePath(id))
  }
  if (shouldRestart) {
    await restartCore()
  }
  if (existsSync(mihomoProfileWorkDir(id))) {
    await rm(mihomoProfileWorkDir(id), { recursive: true })
  }
  await delProfileUpdater(id)
}

export async function getCurrentProfileItem(): Promise<ProfileItem> {
  const { current } = await getProfileConfig()
  return (await getProfileItem(current)) || { id: 'default', type: 'local', name: '空白订阅' }
}

export async function createProfile(item: Partial<ProfileItem>): Promise<ProfileItem> {
  const id = item.id || new Date().getTime().toString(16)
  const newItem = {
    id,
    name: item.name || (item.type === 'remote' ? 'Remote File' : 'Local File'),
    type: item.type,
    url: item.url,
    fingerprint: item.fingerprint,
    ua: item.ua,
    verify: item.verify ?? false,
    autoUpdate: item.autoUpdate ?? true,
    substore: item.substore || false,
    interval: item.interval || 0,
    override: item.override || [],
    useProxy: item.useProxy || false,
    updated: new Date().getTime()
  } as ProfileItem
  switch (newItem.type) {
    case 'remote': {
      const { 'mixed-port': mixedPort = 7890 } = await getControledMihomoConfig()
      if (!item.url) throw new Error('Empty URL')
      let res: AxiosResponse
      if (newItem.substore) {
        const urlObj = new URL(`http://127.0.0.1:${subStorePort}${item.url}`)
        urlObj.searchParams.set('target', 'ClashMeta')
        urlObj.searchParams.set('noCache', 'true')
        if (newItem.useProxy && mixedPort != 0) {
          urlObj.searchParams.set('proxy', `http://127.0.0.1:${mixedPort}`)
        } else {
          urlObj.searchParams.delete('proxy')
        }
        res = await axios.get(urlObj.toString(), {
          headers: {
            'User-Agent': await getUserAgent()
          },
          responseType: 'text'
        })
      } else {
        try {
          const httpsAgent = new https.Agent({ rejectUnauthorized: !item.fingerprint })

          if (item.fingerprint) {
            const expected = item.fingerprint.replace(/:/g, '').toUpperCase()
            const verify = (s: tls.TLSSocket) => {
              if (getCertFingerprint(s.getPeerCertificate()) !== expected)
                s.destroy(new Error('证书指纹不匹配'))
            }

            if (newItem.useProxy && mixedPort != 0) {
              const urlObj = new URL(item.url)
              const hostname = urlObj.hostname
              const port = urlObj.port || '443'
              httpsAgent.createConnection = (_, cb) => {
                const req = http.request({
                  host: '127.0.0.1',
                  port: mixedPort,
                  method: 'CONNECT',
                  path: `${hostname}:${port}`
                })

                req.on('connect', (res, sock, head) => {
                  if (res.statusCode !== 200) {
                    cb?.(new Error(`代理连接失败,状态码:${res.statusCode}`), null!)
                    return
                  }
                  if (head.length > 0) sock.unshift(head)
                  const tls$ = tls.connect(
                    { socket: sock, servername: hostname, rejectUnauthorized: false },
                    () => verify(tls$)
                  )
                  cb?.(null, tls$)
                })

                req.on('error', (e) => cb?.(e, null!))
                req.end()
                return null!
              }
            } else {
              const conn = httpsAgent.createConnection.bind(httpsAgent)
              httpsAgent.createConnection = (o, c) => {
                const sock = conn(o, c)
                sock?.once('secureConnect', function (this: tls.TLSSocket) {
                  verify(this)
                })
                return sock
              }
            }
          }

          res = await axios.get(item.url, {
            httpsAgent,
            ...(newItem.useProxy &&
              mixedPort &&
              !item.fingerprint && {
                proxy: { protocol: 'http', host: '127.0.0.1', port: mixedPort }
              }),
            headers: { 'User-Agent': newItem.ua || (await getUserAgent()) },
            responseType: 'text'
          })
        } catch (error) {
          if (axios.isAxiosError(error)) {
            if (error.code === 'ECONNRESET' || error.code === 'ECONNABORTED') {
              throw new Error(`网络连接被重置或超时:${item.url}`)
            } else if (error.code === 'CERT_HAS_EXPIRED') {
              throw new Error(`服务器证书已过期:${item.url}`)
            } else if (error.code === 'UNABLE_TO_VERIFY_LEAF_SIGNATURE') {
              throw new Error(`无法验证服务器证书:${item.url}`)
            } else if (error.message.includes('Certificate verification failed')) {
              throw new Error(`证书验证失败:${item.url}`)
            } else {
              throw new Error(`请求失败:${error.message}`)
            }
          }
          throw error
        }
      }

      const data = res.data
      const headers = res.headers
      const contentDispositionKey = Object.keys(headers).find((k) =>
        k.toLowerCase().endsWith('content-disposition')
      )
      if (contentDispositionKey && newItem.name === 'Remote File') {
        newItem.name = parseFilename(headers[contentDispositionKey])
      }
      const homeKey = Object.keys(headers).find((k) =>
        k.toLowerCase().endsWith('profile-web-page-url')
      )
      if (homeKey) {
        newItem.home = headers[homeKey]
      }
      const intervalKey = Object.keys(headers).find((k) =>
        k.toLowerCase().endsWith('profile-update-interval')
      )
      if (intervalKey) {
        newItem.interval = parseInt(headers[intervalKey]) * 60
        if (newItem.interval) {
          newItem.locked = true
        }
      }
      const userinfoKey = Object.keys(headers).find((k) =>
        k.toLowerCase().endsWith('subscription-userinfo')
      )
      if (userinfoKey) {
        newItem.extra = parseSubinfo(headers[userinfoKey])
      }
      if (newItem.verify) {
        try {
          parseYaml<MihomoConfig>(data)
        } catch (error) {
          throw new Error('订阅格式错误,无法解析为有效的配置文件\n' + (error as Error).message)
        }
      }
      await setProfileStr(id, data)
      break
    }
    case 'local': {
      const data = item.file || ''
      await setProfileStr(id, data)
      break
    }
  }
  return newItem
}

export async function getProfileStr(id: string | undefined): Promise<string> {
  if (existsSync(profilePath(id || 'default'))) {
    return await readFile(profilePath(id || 'default'), 'utf-8')
  } else {
    return stringifyYaml(defaultProfile)
  }
}

export async function getProfileParseStr(id: string | undefined): Promise<string> {
  let data: string
  if (existsSync(profilePath(id || 'default'))) {
    data = await readFile(profilePath(id || 'default'), 'utf-8')
  } else {
    data = stringifyYaml(defaultProfile)
  }
  const profile = deepMerge(parseYaml<object>(data), {})
  return stringifyYaml(profile)
}

export async function setProfileStr(id: string, content: string): Promise<void> {
  const { current } = await getProfileConfig()
  await writeFile(profilePath(id), content, 'utf-8')
  if (current === id) await restartCore()
}

export async function getProfile(id: string | undefined): Promise<MihomoConfig> {
  const profile = await getProfileStr(id)
  let result = parseYaml<MihomoConfig>(profile)
  if (typeof result !== 'object') result = {} as MihomoConfig
  return result
}

// attachment;filename=xxx.yaml; filename*=UTF-8''%xx%xx%xx
function parseFilename(str: string): string {
  if (str.match(/filename\*=.*''/)) {
    const filename = decodeURIComponent(str.split(/filename\*=.*''/)[1])
    return filename
  } else {
    const filename = str.split('filename=')[1]
    return filename
  }
}

// subscription-userinfo: upload=1234; download=2234; total=1024000; expire=2218532293
function parseSubinfo(str: string): SubscriptionUserInfo {
  const parts = str.split(';')
  const obj = {} as SubscriptionUserInfo
  parts.forEach((part) => {
    const [key, value] = part.trim().split('=')
    obj[key] = parseInt(value)
  })
  return obj
}

function isAbsolutePath(path: string): boolean {
  return path.startsWith('/') || /^[a-zA-Z]:\\/.test(path)
}

function resolveEditableFilePath(
  path: string,
  current: string | undefined,
  diffWorkDir: boolean
): string {
  if (isAbsolutePath(path)) {
    return path
  }
  return join(diffWorkDir ? mihomoProfileWorkDir(current) : mihomoWorkDir(), path)
}

function isSubPath(base: string, target: string): boolean {
  const relativePath = relative(resolve(base), resolve(target))
  return relativePath === '' || (!relativePath.startsWith('..') && !isAbsolute(relativePath))
}

function isPermissionError(error: unknown): boolean {
  if (!error || typeof error !== 'object') {
    return false
  }
  const code = 'code' in error ? error.code : undefined
  return code === 'EACCES' || code === 'EPERM'
}

function shellQuote(value: string): string {
  return `'${value.replace(/'/g, `'\\''`)}'`
}

function isManagedEditableFile(target: string, current: string | undefined): boolean {
  return [mihomoWorkDir(), mihomoProfileWorkDir(current)].some((root) => isSubPath(root, target))
}

function buildPermissionRepairCommand(
  target: string,
  uid: number,
  gid: number,
  repairParent: boolean
): string {
  const parts = [`t=${shellQuote(target)}`]

  if (repairParent) {
    parts.push(`p=${shellQuote(dirname(target))}`)
    parts.push(`mkdir -p "$p"`)
    parts.push(`chown ${uid}:${gid} "$p"`)
    parts.push(`chmod u+rwx "$p"`)
  }

  parts.push(`[ ! -e "$t" ] || { chown ${uid}:${gid} "$t" && chmod u+rw "$t"; }`)
  return parts.join('; ')
}

async function repairEditableFilePermissions(
  target: string,
  current: string | undefined
): Promise<void> {
  const repairParent = process.platform !== 'win32' && isManagedEditableFile(target, current)
  if (!repairParent) {
    return
  }

  const uid = process.getuid?.()
  const gid = process.getgid?.()
  if (uid == null || gid == null) {
    return
  }

  await execWithElevation('sh', [
    '-c',
    buildPermissionRepairCommand(target, uid, gid, repairParent)
  ])
}

async function attemptWriteFile(target: string, content: string): Promise<void> {
  await mkdir(dirname(target), { recursive: true })
  await writeFile(target, content, 'utf-8')
}

async function writeEditableFile(
  target: string,
  content: string,
  current: string | undefined,
  elevate = false
): Promise<void> {
  try {
    await attemptWriteFile(target, content)
  } catch (error) {
    if (!isPermissionError(error)) {
      throw error
    }

    if (!elevate) {
      if (process.platform !== 'win32' && isManagedEditableFile(target, current)) {
        throw new Error(FILE_PERMISSION_ELEVATION_REQUIRED)
      }
      throw error
    }

    await repairEditableFilePermissions(target, current)
    await attemptWriteFile(target, content)
  }
}

export async function getFileStr(path: string): Promise<string> {
  const { diffWorkDir = false } = await getAppConfig()
  const { current } = await getProfileConfig()
  return await readFile(resolveEditableFilePath(path, current, diffWorkDir), 'utf-8')
}

export async function setFileStr(path: string, content: string): Promise<void> {
  return await saveFileStr(path, content, false)
}

export async function saveFileStrWithElevation(path: string, content: string): Promise<void> {
  return await saveFileStr(path, content, true)
}

async function saveFileStr(path: string, content: string, elevate: boolean): Promise<void> {
  const { diffWorkDir = false } = await getAppConfig()
  const { current } = await getProfileConfig()
  const target = resolveEditableFilePath(path, current, diffWorkDir)
  await writeEditableFile(target, content, current, elevate)
}


================================================
FILE: src/main/core/factory.ts
================================================
import {
  getControledMihomoConfig,
  getProfileConfig,
  getProfile,
  getProfileStr,
  getProfileItem,
  getOverride,
  getOverrideItem,
  getOverrideConfig,
  getAppConfig
} from '../config'
import {
  mihomoProfileWorkDir,
  mihomoWorkConfigPath,
  mihomoWorkDir,
  overridePath
} from '../utils/dirs'
import { parseYaml, stringifyYaml } from '../utils/yaml'
import { copyFile, mkdir, writeFile } from 'fs/promises'
import { deepMerge } from '../utils/merge'
import vm from 'vm'
import { existsSync, writeFileSync } from 'fs'
import path from 'path'

let runtimeConfigStr: string,
  rawProfileStr: string,
  currentProfileStr: string,
  overrideProfileStr: string,
  runtimeConfig: MihomoConfig

export async function generateProfile(): Promise<void> {
  const { current } = await getProfileConfig()
  const { diffWorkDir = false, controlDns = true, controlSniff = true } = await getAppConfig()
  const currentProfileConfig = await getProfile(current)
  rawProfileStr = await getProfileStr(current)
  currentProfileStr = stringifyYaml(currentProfileConfig)
  const currentProfile = await overrideProfile(current, currentProfileConfig)
  overrideProfileStr = stringifyYaml(currentProfile)
  const controledMihomoConfig = await getControledMihomoConfig()

  const configToMerge = JSON.parse(JSON.stringify(controledMihomoConfig))
  if (!controlDns) {
    delete configToMerge.dns
    delete configToMerge.hosts
  }
  if (!controlSniff) {
    delete configToMerge.sniffer
  }

  const profile = deepMerge(JSON.parse(JSON.stringify(currentProfile)), configToMerge)

  await cleanProfile(profile, controlDns, controlSniff)

  runtimeConfig = profile
  runtimeConfigStr = stringifyYaml(profile)
  if (diffWorkDir) {
    await prepareProfileWorkDir(current)
  }
  await writeFile(
    diffWorkDir ? mihomoWorkConfigPath(current) : mihomoWorkConfigPath('work'),
    runtimeConfigStr
  )
}

async function cleanProfile(
  profile: MihomoConfig,
  controlDns: boolean,
  controlSniff: boolean
): Promise<void> {
  if (!['info', 'debug'].includes(profile['log-level'])) {
    profile['log-level'] = 'info'
  }

  configureLanSettings(profile)
  cleanBooleanConfigs(profile)
  cleanNumberConfigs(profile)
  cleanStringConfigs(profile)
  cleanAuthenticationConfig(profile)
  cleanTunConfig(profile)
  cleanDnsConfig(profile, controlDns)
  cleanSnifferConfig(profile, controlSniff)
  cleanProxyConfigs(profile)
}

function cleanBooleanConfigs(profile: MihomoConfig): void {
  if (profile.ipv6 !== false) {
    delete (profile as Partial<MihomoConfig>).ipv6
  }

  const booleanConfigs = [
    'unified-delay',
    'tcp-concurrent',
    'geodata-mode',
    'geo-auto-update',
    'disable-keep-alive'
  ]

  booleanConfigs.forEach((key) => {
    if (!profile[key]) delete (profile as Partial<MihomoConfig>)[key]
  })

  if (!profile.profile) return

  const { 'store-selected': hasStoreSelected, 'store-fake-ip': hasStoreFakeIp } = profile.profile

  if (!hasStoreSelected && !hasStoreFakeIp) {
    delete (profile as Partial<MihomoConfig>).profile
  } else {
    const profileConfig = profile.profile as MihomoProfileConfig
    if (!hasStoreSelected) delete profileConfig['store-selected']
    if (!hasStoreFakeIp) delete profileConfig['store-fake-ip']
  }
}

function cleanNumberConfigs(profile: MihomoConfig): void {
  ;[
    'port',
    'socks-port',
    'redir-port',
    'tproxy-port',
    'mixed-port',
    'keep-alive-idle',
    'keep-alive-interval'
  ].forEach((key) => {
    if (profile[key] === 0) delete (profile as Partial<MihomoConfig>)[key]
  })
}

function cleanStringConfigs(profile: MihomoConfig): void {
  const partialProfile = profile as Partial<MihomoConfig>

  if (profile.mode === 'rule') delete partialProfile.mode

  const emptyStringConfigs = ['interface-name', 'secret', 'global-client-fingerprint']
  emptyStringConfigs.forEach((key) => {
    if (profile[key] === '') delete partialProfile[key]
  })

  if (profile['external-controller'] === '') {
    delete partialProfile['external-controller']
    delete partialProfile['external-ui']
    delete partialProfile['external-ui-url']
    delete partialProfile['external-controller-cors']
  } else if (profile['external-ui'] === '') {
    delete partialProfile['external-ui']
    delete partialProfile['external-ui-url']
  }
}

function configureLanSettings(profile: MihomoConfig): void {
  const partialProfile = profile as Partial<MihomoConfig>

  if (profile['allow-lan'] === false) {
    delete partialProfile['lan-allowed-ips']
    delete partialProfile['lan-disallowed-ips']
    return
  }

  if (!profile['allow-lan']) {
    delete partialProfile['allow-lan']
    delete partialProfile['lan-allowed-ips']
    delete partialProfile['lan-disallowed-ips']
    return
  }

  const allowedIps = profile['lan-allowed-ips']
  if (allowedIps?.length === 0) {
    delete partialProfile['lan-allowed-ips']
  } else if (allowedIps && !allowedIps.some((ip: string) => ip.startsWith('127.0.0.1/'))) {
    allowedIps.push('127.0.0.1/8')
  }

  if (profile['lan-disallowed-ips']?.length === 0) {
    delete partialProfile['lan-disallowed-ips']
  }
}

function cleanAuthenticationConfig(profile: MihomoConfig): void {
  if (profile.authentication?.length === 0) {
    const partialProfile = profile as Partial<MihomoConfig>
    delete partialProfile.authentication
    delete partialProfile['skip-auth-prefixes']
  }
}

function cleanTunConfig(profile: MihomoConfig): void {
  if (!profile.tun?.enable) {
    delete (profile as Partial<MihomoConfig>).tun
    return
  }

  const tunConfig = profile.tun as MihomoTunConfig

  if (tunConfig['auto-route'] !== false) {
    delete tunConfig['auto-route']
  }
  if (tunConfig['auto-detect-interface'] !== false) {
    delete tunConfig['auto-detect-interface']
  }

  const tunBooleanConfigs = ['auto-redirect', 'strict-route', 'disable-icmp-forwarding']
  tunBooleanConfigs.forEach((key) => {
    if (!tunConfig[key]) delete tunConfig[key]
  })

  if (tunConfig.device === '') {
    delete tunConfig.device
  } else if (
    process.platform === 'darwin' &&
    tunConfig.device &&
    !tunConfig.device.startsWith('utun')
  ) {
    delete tunConfig.device
  }

  if (tunConfig['dns-hijack']?.length === 0) delete tunConfig['dns-hijack']
  if (tunConfig['route-exclude-address']?.length === 0) delete tunConfig['route-exclude-address']
}

function cleanDnsConfig(profile: MihomoConfig, controlDns: boolean): void {
  if (!controlDns) return
  if (!profile.dns?.enable) {
    delete (profile as Partial<MihomoConfig>).dns
    return
  }

  const dnsConfig = profile.dns as MihomoDNSConfig
  const dnsArrayConfigs = [
    'fake-ip-range',
    'fake-ip-range6',
    'fake-ip-filter',
    'proxy-server-nameserver',
    'direct-nameserver',
    'nameserver'
  ]

  dnsArrayConfigs.forEach((key) => {
    if (dnsConfig[key]?.length === 0) delete dnsConfig[key]
  })

  if (dnsConfig['respect-rules'] === false || dnsConfig['proxy-server-nameserver']?.length === 0) {
    delete dnsConfig['respect-rules']
  }

  if (dnsConfig['nameserver-policy'] && Object.keys(dnsConfig['nameserver-policy']).length === 0) {
    delete dnsConfig['nameserver-policy']
  }
  if (
    dnsConfig['proxy-server-nameserver-policy'] &&
    Object.keys(dnsConfig['proxy-server-nameserver-policy']).length === 0
  ) {
    delete dnsConfig['proxy-server-nameserver-policy']
  }

  delete dnsConfig.fallback
  delete dnsConfig['fallback-filter']
}

function cleanSnifferConfig(profile: MihomoConfig, controlSniff: boolean): void {
  if (!controlSniff) return
  if (!profile.sniffer?.enable) {
    delete (profile as Partial<MihomoConfig>).sniffer
  }
}

function cleanProxyConfigs(profile: MihomoConfig): void {
  const partialProfile = profile as Partial<MihomoConfig>
  const arrayConfigs = ['proxies', 'proxy-groups', 'rules']
  const objectConfigs = ['proxy-providers', 'rule-providers']

  arrayConfigs.forEach((key) => {
    if (Array.isArray(profile[key]) && profile[key]?.length === 0) {
      delete partialProfile[key]
    }
  })

  objectConfigs.forEach((key) => {
    const value = profile[key]
    if (
      value === null ||
      value === undefined ||
      (value && typeof value === 'object' && Object.keys(value).length === 0)
    ) {
      delete partialProfile[key]
    }
  })
}

async function prepareProfileWorkDir(current: string | undefined): Promise<void> {
  if (!existsSync(mihomoProfileWorkDir(current))) {
    await mkdir(mihomoProfileWorkDir(current), { recursive: true })
  }
  const copy = async (file: string): Promise<void> => {
    const targetPath = path.join(mihomoProfileWorkDir(current), file)
    const sourcePath = path.join(mihomoWorkDir(), file)
    if (!existsSync(targetPath) && existsSync(sourcePath)) {
      await copyFile(sourcePath, targetPath)
    }
  }
  await Promise.all([
    copy('country.mmdb'),
    copy('geoip.metadb'),
    copy('geoip.dat'),
    copy('geosite.dat'),
    copy('ASN.mmdb')
  ])
}

async function overrideProfile(
  current: string | undefined,
  profile: MihomoConfig
): Promise<MihomoConfig> {
  const { items = [] } = (await getOverrideConfig()) || {}
  const globalOverride = items.filter((item) => item.global).map((item) => item.id)
  const { override = [] } = (await getProfileItem(current)) || {}
  for (const ov of new Set(globalOverride.concat(override))) {
    const item = await getOverrideItem(ov)
    const content = await getOverride(ov, item?.ext || 'js')
    switch (item?.ext) {
      case 'js':
        profile = await runOverrideScript(profile, content, item)
        break
      case 'yaml': {
        let patch = parseYaml<Partial<MihomoConfig>>(content)
        if (typeof patch !== 'object') patch = {}
        profile = deepMerge(profile, patch, true)
        break
      }
    }
  }
  return profile
}

async function runOverrideScript(
  profile: MihomoConfig,
  script: string,
  item: OverrideItem
): Promise<MihomoConfig> {
  const log = (type: string, data: string, flag = 'a'): void => {
    writeFileSync(overridePath(item.id, 'log'), `[${type}] ${data}\n`, {
      encoding: 'utf-8',
      flag
    })
  }
  try {
    const b64d = (str: string): string => Buffer.from(str, 'base64').toString('utf-8')
    const b64e = (data: Buffer | string): string =>
      (Buffer.isBuffer(data) ? data : Buffer.from(String(data))).toString('base64')
    const ctx = {
      console: Object.freeze({
        log: (...args: unknown[]) => log('log', args.map(format).join(' ')),
        info: (...args: unknown[]) => log('info', args.map(format).join(' ')),
        error: (...args: unknown[]) => log('error', args.map(format).join(' ')),
        debug: (...args: unknown[]) => log('debug', args.map(format).join(' '))
      }),
      fetch,
      yaml: { parse: parseYaml, stringify: stringifyYaml },
      b64d,
      b64e,
      Buffer
    }
    vm.createContext(ctx)
    log('info', '开始执行脚本', 'w')
    vm.runInContext(script, ctx)
    const promise = vm.runInContext(
      `(async () => {
        const result = main(${JSON.stringify(profile)})
        if (result instanceof Promise) return await result
        return result
      })()`,
      ctx
    )
    const newProfile = await promise
    if (typeof newProfile !== 'object') {
      throw new Error('脚本返回值必须是对象')
    }
    log('info', '脚本执行成功')
    return newProfile
  } catch (e) {
    log('exception', `脚本执行失败:${e}`)
    return profile
  }
}

function format(data: unknown): string {
  if (data instanceof Error) {
    return `${data.name}: ${data.message}\n${data.stack}`
  }
  try {
    return JSON.stringify(data)
  } catch {
    return String(data)
  }
}

export async function getRuntimeConfigStr(): Promise<string> {
  return runtimeConfigStr
}

export async function getRawProfileStr(): Promise<string> {
  return rawProfileStr
}

export async function getCurrentProfileStr(): Promise<string> {
  return currentProfileStr
}

export async function getOverrideProfileStr(): Promise<string> {
  return overrideProfileStr
}

export async function getRuntimeConfig(): Promise<MihomoConfig> {
  return runtimeConfig
}


================================================
FILE: src/main/core/manager.ts
================================================
import { ChildProcess, spawn } from 'child_process'
import { dataDir, coreLogPath, mihomoCorePath } from '../utils/dirs'
import { generateProfile, getRuntimeConfig } from './factory'
import {
  getAppConfig,
  getControledMihomoConfig,
  getProfileConfig,
  patchAppConfig,
  patchControledMihomoConfig
} from '../config'
import { app, dialog, ipcMain, Notification } from 'electron'
import {
  startMihomoTraffic,
  startMihomoConnections,
  startMihomoLogs,
  startMihomoMemory,
  stopMihomoConnections,
  stopMihomoTraffic,
  stopMihomoLogs,
  stopMihomoMemory,
  patchMihomoConfig,
  mihomoGroups
} from './mihomoApi'
import { readFile, rm, writeFile } from 'fs/promises'
import { mainWindow } from '..'
import path from 'path'
import os from 'os'
import { existsSync } from 'fs'
import { uploadRuntimeConfig } from '../resolve/gistApi'
import { startMonitor } from '../resolve/trafficMonitor'
import { floatingWindow } from '../resolve/floatingWindow'
import { getAxios } from './mihomoApi'
import {
  getCoreStatus,
  startCore as startServiceCore,
  stopCore as stopServiceCore,
  startServiceCoreEventStream,
  stopServiceCoreEventStream,
  stopServiceSysproxyEventStream,
  subscribeServiceCoreEvents,
  subscribeServiceCoreEventStream,
  setServiceUnavailableFallbackHandler,
  isServiceConnectionError,
  type ServiceCoreEvent,
  type ServiceCoreLaunchProfile
} from '../service/api'
import { appendAppLog, createLogWritable, setMihomoLogSource } from '../utils/log'
import { createCoreHookWaiter, createCoreStartupHook } from './startupHook'
import { stopChildProcess } from './process-control'
import {
  recoverDNS,
  setPublicDNS,
  startNetworkDetection as startNetworkDetectionWithCore,
  stopNetworkDetection as stopNetworkDetectionController
} from './network'
import { checkProfile } from './profile-check'
import {
  createCoreEnvironment,
  createCoreSpawnArgs,
  createProviderInitializationTracker,
  isControllerListenError,
  isControllerReadyLog,
  isTunPermissionError,
  isUpdaterFinishedLog
} from './startup-chain'
export {
  checkCorePermission,
  checkCorePermissionSync,
  manualGrantCorePermition,
  revokeCorePermission
} from './permission'
export { getDefaultDevice } from './network'

const ctlParam = process.platform === 'win32' ? '-ext-ctl-pipe' : '-ext-ctl-unix'

let child: ChildProcess
let retry = 10
let serviceCoreStreamsRestartTimer: NodeJS.Timeout | null = null
let unsubscribeServiceCoreEvents: (() => void) | null = null
let unsubscribeServiceCoreEventStream: (() => void) | null = null
let serviceCoreStreamsActive = false
let serviceCoreStreamsStarting: Promise<void> | null = null
let lastServiceCoreEventKey = ''
let serviceCoreStartupActive = false
let serviceCoreReconnectResumePromise: Promise<void> | null = null
let serviceUnavailableModeFallbackPromise: Promise<void> | null = null
const serviceConnectionRetryTimeout = 10000
const serviceConnectionRetryInterval = 500

setServiceUnavailableFallbackHandler((reason) => {
  if (!serviceUnavailableModeFallbackPromise) {
    serviceUnavailableModeFallbackPromise = fallbackUnavailableServiceModes(reason).finally(() => {
      serviceUnavailableModeFallbackPromise = null
    })
  }

  return serviceUnavailableModeFallbackPromise
})

type ServiceCoreConnectionProbe = {
  reachable: boolean
  running: boolean
  error: unknown
}

async function startMihomoApiStreams(): Promise<void> {
  await startMihomoTraffic()
  await startMihomoConnections()
  await startMihomoLogs()
  await startMihomoMemory()
  retry = 10
}

async function completeCoreInitialization(logLevel?: LogLevel): Promise<void> {
  const tasks: Promise<unknown>[] = [
    new Promise<void>((resolve) => setTimeout(resolve, 100)).then(() => {
      mainWindow?.webContents.send('groupsUpdated')
      mainWindow?.webContents.send('rulesUpdated')
    }),
    uploadRuntimeConfig()
  ]

  if (logLevel) {
    tasks.push(
      new Promise<void>((resolve) => setTimeout(resolve, 100)).then(() =>
        patchMihomoConfig({ 'log-level': logLevel })
      )
    )
  }

  await Promise.all(tasks)
  setMihomoLogSource('ws')
}

async function waitForMihomoReady(): Promise<void> {
  const maxRetries = 30
  const retryInterval = 100

  for (let i = 0; i < maxRetries; i++) {
    try {
      await mihomoGroups()
      break
    } catch (error) {
      await new Promise((resolve) => setTimeout(resolve, retryInterval))
    }
  }
}

async function waitForServiceCoreConnection(
  initialError: unknown
): Promise<ServiceCoreConnectionProbe> {
  await appendAppLog(
    `[Manager]: Service connection failed, waiting before fallback, ${initialError}\n`
  )
  const startedAt = Date.now()
  let lastError = initialError

  while (Date.now() - startedAt < serviceConnectionRetryTimeout) {
    await new Promise((resolve) => setTimeout(resolve, serviceConnectionRetryInterval))

    try {
      await getCoreStatus()
      return { reachable: true, running: true, error: lastError }
    } catch (error) {
      lastError = error
      if (!isServiceConnectionError(error)) {
        return { reachable: true, running: false, error }
      }
    }
  }

  await appendAppLog(
    `[Manager]: Service still unavailable after ${serviceConnectionRetryTimeout}ms, ${lastError}\n`
  )
  return { reachable: false, running: false, error: lastError }
}

export async function startCore(detached = false): Promise<Promise<void>[]> {
  const {
    core = 'mihomo',
    corePermissionMode = 'elevated',
    coreStartupMode = 'post-up',
    autoSetDNSMode = 'none',
    diffWorkDir = false,
    mihomoCpuPriority = 'PRIORITY_NORMAL',
    saveLogs = true,
    maxLogFileSizeMB = 20,
    disableLoopbackDetector = false,
    disableEmbedCA = false,
    disableSystemCA = false,
    disableNftables = false,
    safePaths = []
  } = await getAppConfig()
  const controlledMihomoConfig = await getControledMihomoConfig()
  const { 'log-level': logLevel, tun } = controlledMihomoConfig
  const { current } = await getProfileConfig()
  const useServiceCore = corePermissionMode === 'service' && !detached

  let corePath: string
  try {
    corePath = mihomoCorePath(core)
  } catch (error) {
    if (core === 'system') {
      await patchAppConfig({ core: 'mihomo' })
      return startCore(detached)
    }
    throw error
  }

  await generateProfile()
  await checkProfile()
  let serviceCoreRunning = false
  if (useServiceCore) {
    try {
      await getCoreStatus()
      serviceCoreRunning = true
    } catch (error) {
      if (isServiceConnectionError(error)) {
        const probe = await waitForServiceCoreConnection(error)
        if (!probe.reachable) {
          return fallbackToElevatedCore(detached, probe.error)
        }
        serviceCoreRunning = probe.running
      }
    }
  }
  if (!serviceCoreRunning) {
    await stopCore()
  }
  setMihomoLogSource('out')
  if (tun?.enable && autoSetDNSMode !== 'none') {
    try {
      await setPublicDNS()
    } catch (error) {
      await appendAppLog(`[Manager]: set dns failed, ${error}\n`)
    }
  }
  const env = createCoreEnvironment({
    disableLoopbackDetector,
    disableEmbedCA,
    disableSystemCA,
    disableNftables,
    safePaths
  })

  let initialized = false
  const coreHook =
    !useServiceCore && !detached && coreStartupMode === 'post-up'
      ? await createCoreStartupHook()
      : undefined
  const hookWaiter = coreHook ? createCoreHookWaiter(coreHook) : undefined
  if (coreHook) {
    await appendAppLog(
      `[Manager]: Core startup mode: post-up, post-up command: ${coreHook.postUpCommand}\n`
    )
  } else if (!detached) {
    await appendAppLog(`[Manager]: Core startup mode: log\n`)
  }

  const spawnArgs = createCoreSpawnArgs({
    current,
    diffWorkDir,
    ctlParam,
    coreHook
  })

  if (useServiceCore) {
    const serviceProfile: ServiceCoreLaunchProfile = {
      core_path: corePath,
      args: spawnArgs,
      safe_paths: safePaths,
      env,
      mihomo_cpu_priority: mihomoCpuPriority,
      log_path: coreLogPath(),
      save_logs: saveLogs,
      max_log_file_size_mb: maxLogFileSizeMB
    }

    await appendAppLog(`[Manager]: Core permission mode: service\n`)
    ensureServiceCoreEventHandler()
    serviceCoreStartupActive = true
    try {
      await startServiceCoreEventStream()
      if (!serviceCoreRunning) {
        await startServiceCore(serviceProfile)
      }
    } catch (error) {
      if (isServiceConnectionError(error)) {
        const probe = await waitForServiceCoreConnection(error)
        if (!probe.reachable) {
          return fallbackToElevatedCore(detached, probe.error)
        }
        await startServiceCoreEventStream()
        if (!probe.running) {
          await startServiceCore(serviceProfile)
        }
      } else {
        throw error
      }
    } finally {
      serviceCoreStartupActive = false
    }
    await ensureServiceCoreStreamsStarted()
    initialized = true
    return [completeCoreInitialization(logLevel)]
  }

  const providerTracker = createProviderInitializationTracker(await getRuntimeConfig())
  const stdout = createLogWritable('core', 'info')
  const stderr = createLogWritable('core', 'error')

  child = spawn(corePath, spawnArgs, {
    detached: detached,
    stdio: detached ? 'ignore' : undefined,
    env: env
  })
  hookWaiter?.attachProcess(child)
  if (child.pid) {
    try {
      os.setPriority(child.pid, os.constants.priority[mihomoCpuPriority])
    } catch (error) {
      await appendAppLog(`[Manager]: set core priority failed, ${error}\n`)
    }
  }
  if (detached) {
    child.unref()
    return new Promise((resolve) => {
      resolve([new Promise(() => {})])
    })
  }
  child.on('close', async (code, signal) => {
    await appendAppLog(`[Manager]: Core closed, code: ${code}, signal: ${signal}\n`)
    if (retry) {
      await appendAppLog(`[Manager]: Try Restart Core\n`)
      retry--
      await restartCore()
    } else {
      await stopCore()
    }
  })
  child.stdout?.pipe(stdout)
  child.stderr?.pipe(stderr)

  const handleCoreOutput = async (
    str: string,
    reject: (reason?: unknown) => void
  ): Promise<void> => {
    if (isControllerListenError(str)) {
      reject(`控制器监听错误:\n${str}`)
    }

    if (isUpdaterFinishedLog(str)) {
      try {
        await stopCore(true)
        const promises = await startCore()
        await Promise.all(promises)
      } catch (e) {
        dialog.showErrorBox('内核启动出错', `${e}`)
      }
    }
  }

  const waitForCoreReadyByLog = (): Promise<Promise<void>[]> => {
    let controllerReady = false

    return new Promise((resolve, reject) => {
      child.stdout?.on('data', async (data) => {
        const str = data.toString()
        await handleCoreOutput(str, reject)

        if (!controllerReady && isControllerReadyLog(str)) {
          controllerReady = true
          resolve([
            new Promise((resolve, reject) => {
              const handleProviderInitialization = async (logLine: string): Promise<void> => {
                providerTracker.track(logLine)

                if (isTunPermissionError(logLine)) {
                  patchControledMihomoConfig({ tun: { enable: false } })
                  mainWindow?.webContents.send('controledMihomoConfigUpdated')
                  ipcMain.emit('updateTrayMenu')
                  reject('虚拟网卡启动失败,前往内核设置页尝试手动授予内核权限')
                }

                if (providerTracker.isReady(logLine)) {
                  await waitForMihomoReady()
                  initialized = true
                  completeCoreInitialization(logLevel)
                    .then(() => resolve())
                    .catch(reject)
                }
              }

              child.stdout?.on('data', (data) => {
                if (!initialized) {
                  handleProviderInitialization(data.toString()).catch(reject)
                }
              })
            })
          ])
          await startMihomoApiStreams()
        }
      })
    })
  }

  const waitForCoreReadyByHook = (): Promise<Promise<void>[]> => {
    if (!hookWaiter) return waitForCoreReadyByLog()

    return new Promise((resolve, reject) => {
      child.stdout?.on('data', (data) => {
        handleCoreOutput(data.toString(), reject).catch(reject)
      })

      hookWaiter.promise
        .then(async () => {
          initialized = true
          await startMihomoApiStreams()
          resolve([completeCoreInitialization(logLevel)])
        })
        .catch(reject)
    })
  }

  return coreStartupMode === 'post-up' ? waitForCoreReadyByHook() : waitForCoreReadyByLog()
}

export async function stopCore(force = false): Promise<void> {
  try {
    if (!force) {
      await recoverDNS()
    }
  } catch (error) {
    await appendAppLog(`[Manager]: recover dns failed, ${error}\n`)
  }

  stopMihomoTraffic()
  stopMihomoConnections()
  stopMihomoLogs()
  stopMihomoMemory()
  serviceCoreStreamsActive = false
  if (serviceCoreStreamsRestartTimer) {
    clearTimeout(serviceCoreStreamsRestartTimer)
    serviceCoreStreamsRestartTimer = null
  }

  const { corePermissionMode = 'elevated' } = await getAppConfig()
  if (corePermissionMode === 'service') {
    try {
      await stopServiceCore()
    } catch (error) {
      await appendAppLog(`[Manager]: stop service core failed, ${error}\n`)
    } finally {
      stopServiceCoreEventStream()
      releaseServiceCoreEventHandler()
    }
  }

  if (child && !child.killed) {
    await stopChildProcess(child)
    child = undefined as unknown as ChildProcess
  }

  await getAxios(true).catch(() => {})

  if (existsSync(path.join(dataDir(), 'core.pid'))) {
    const pidString = await readFile(path.join(dataDir(), 'core.pid'), 'utf-8')
    const pid = parseInt(pidString.trim())
    if (!isNaN(pid)) {
      try {
        process.kill(pid, 0)
        process.kill(pid, 'SIGINT')
        await new Promise((resolve) => setTimeout(resolve, 1000))
        try {
          process.kill(pid, 0)
          process.kill(pid, 'SIGKILL')
        } catch {
          // ignore
        }
      } catch {
        // ignore
      }
    }
    await rm(path.join(dataDir(), 'core.pid')).catch(() => {})
  }
}

function ensureServiceCoreEventHandler(): void {
  if (!unsubscribeServiceCoreEvents) {
    unsubscribeServiceCoreEvents = subscribeServiceCoreEvents((event) =>
      handleServiceCoreEvent(event)
    )
  }
  if (!unsubscribeServiceCoreEventStream) {
    unsubscribeServiceCoreEventStream = subscribeServiceCoreEventStream((state) =>
      handleServiceCoreEventStreamState(state)
    )
  }
}

function releaseServiceCoreEventHandler(): void {
  if (unsubscribeServiceCoreEvents) {
    unsubscribeServiceCoreEvents()
    unsubscribeServiceCoreEvents = null
  }
  if (unsubscribeServiceCoreEventStream) {
    unsubscribeServiceCoreEventStream()
    unsubscribeServiceCoreEventStream = null
  }
}

async function handleServiceCoreEvent(event: ServiceCoreEvent): Promise<void> {
  if (isDuplicateServiceCoreEvent(event)) {
    return
  }

  await appendAppLog(
    `[Manager]: Service core event: ${event.type}${event.pid ? `, pid: ${event.pid}` : ''}${event.error ? `, error: ${event.error}` : ''}\n`
  )

  mainWindow?.webContents.send('core-status-changed', event)

  switch (event.type) {
    case 'started':
      await getAxios(true).catch(() => {})
      mainWindow?.webContents.send('core-started', event)
      mainWindow?.webContents.send('groupsUpdated')
      mainWindow?.webContents.send('rulesUpdated')
      ipcMain.emit('updateTrayMenu')
      void ensureServiceCoreStreamsStarted().catch((error) => {
        appendAppLog(`[Manager]: start service core streams failed, ${error}\n`).catch(() => {})
      })
      break
    case 'takeover':
    case 'ready':
      await getAxios(true).catch(() => {})
      mainWindow?.webContents.send('core-started', event)
      mainWindow?.webContents.send('groupsUpdated')
      mainWindow?.webContents.send('rulesUpdated')
      ipcMain.emit('updateTrayMenu')
      scheduleServiceCoreStreamsRestart()
      break
    case 'exited':
    case 'failed':
    case 'restart_failed':
      stopMihomoTraffic()
      stopMihomoConnections()
      stopMihomoLogs()
      stopMihomoMemory()
      serviceCoreStreamsActive = false
      setMihomoLogSource('out')
      mainWindow?.webContents.send('core-stopped', event)
      if (event.type === 'restart_failed') {
        mainWindow?.webContents.reload()
      }
      break
    case 'stopped':
      serviceCoreStreamsActive = false
      mainWindow?.webContents.send('core-stopped', event)
      break
  }
}

async function handleServiceCoreEventStreamState(
  state: 'connected' | 'disconnected'
): Promise<void> {
  await appendAppLog(`[Manager]: Service core event stream ${state}\n`)
  if (state !== 'connected') {
    return
  }
  if (serviceCoreStartupActive || serviceCoreReconnectResumePromise) {
    return
  }

  serviceCoreReconnectResumePromise = resumeServiceCoreAfterReconnect()
  try {
    await serviceCoreReconnectResumePromise
  } finally {
    serviceCoreReconnectResumePromise = null
  }
}

async function resumeServiceCoreAfterReconnect(): Promise<void> {
  await new Promise((resolve) => setTimeout(resolve, 500))
  if (serviceCoreStartupActive) {
    return
  }

  const { corePermissionMode = 'elevated' } = await getAppConfig()
  if (corePermissionMode !== 'service') {
    return
  }

  try {
    await getCoreStatus()
    return
  } catch (error) {
    if (isServiceConnectionError(error)) {
      return
    }
  }

  await appendAppLog(`[Manager]: Service reconnected without running core, starting core\n`)
  const promises = await startCore()
  await Promise.all(promises)
  mainWindow?.webContents.send('core-started')
}

function isDuplicateServiceCoreEvent(event: ServiceCoreEvent): boolean {
  const key =
    event.seq !== undefined
      ? `seq:${event.seq}`
      : [event.type, event.time, event.pid ?? '', event.old_pid ?? '', event.error ?? ''].join('|')
  if (key === lastServiceCoreEventKey) {
    return true
  }
  lastServiceCoreEventKey = key
  return false
}

function scheduleServiceCoreStreamsRestart(): void {
  if (serviceCoreStreamsRestartTimer) {
    clearTimeout(serviceCoreStreamsRestartTimer)
  }

  serviceCoreStreamsRestartTimer = setTimeout(() => {
    serviceCoreStreamsRestartTimer = null
    restartServiceCoreStreams().catch((error) => {
      appendAppLog(`[Manager]: restart service core streams failed, ${error}\n`).catch(() => {})
    })
  }, 300)
}

async function restartServiceCoreStreams(): Promise<void> {
  stopMihomoTraffic()
  stopMihomoConnections()
  stopMihomoLogs()
  stopMihomoMemory()
  serviceCoreStreamsActive = false
  await ensureServiceCoreStreamsStarted()
}

async function ensureServiceCoreStreamsStarted(): Promise<void> {
  if (serviceCoreStreamsRestartTimer) {
    clearTimeout(serviceCoreStreamsRestartTimer)
    serviceCoreStreamsRestartTimer = null
  }
  if (serviceCoreStreamsActive) {
    return
  }
  if (serviceCoreStreamsStarting) {
    return serviceCoreStreamsStarting
  }

  serviceCoreStreamsStarting = (async () => {
    await getAxios(true).catch(() => {})
    await startMihomoTraffic()
    await startMihomoConnections()
    await startMihomoLogs()
    await startMihomoMemory()
    setMihomoLogSource('ws')
    retry = 10
    serviceCoreStreamsActive = true
  })()

  try {
    await serviceCoreStreamsStarting
  } finally {
    serviceCoreStreamsStarting = null
  }
}

async function fallbackToElevatedCore(
  detached: boolean,
  reason: unknown
): Promise<Promise<void>[]> {
  await appendAppLog(`[Manager]: Service unavailable, fallback to elevated core, ${reason}\n`)
  stopServiceCoreEventStream()
  releaseServiceCoreEventHandler()
  await patchAppConfig({ corePermissionMode: 'elevated' })
  mainWindow?.webContents.send('appConfigUpdated')
  floatingWindow?.webContents.send('appConfigUpdated')
  return startCore(detached)
}

async function fallbackUnavailableServiceModes(reason: unknown): Promise<void> {
  const appConfig = await getAppConfig()
  const { sysProxy, corePermissionMode = 'elevated', autoSetDNSMode = 'none' } = appConfig
  const useServiceCore = corePermissionMode === 'service'
  const useServiceSysProxy = sysProxy?.settingMode === 'service'
  const useServiceDNS = autoSetDNSMode === 'service'

  if (!useServiceCore && !useServiceSysProxy && !useServiceDNS) {
    return
  }

  await appendAppLog(`[Manager]: Service unavailable, fallback service modes, ${reason}\n`)

  if (useServiceCore) {
    stopMihomoTraffic()
    stopMihomoConnections()
    stopMihomoLogs()
    stopMihomoMemory()
    serviceCoreStreamsActive = false
    if (serviceCoreStreamsRestartTimer) {
      clearTimeout(serviceCoreStreamsRestartTimer)
      serviceCoreStreamsRestartTimer = null
    }
    stopServiceCoreEventStream()
    releaseServiceCoreEventHandler()
    setMihomoLogSource('out')
  }

  if (useServiceSysProxy) {
    stopServiceSysproxyEventStream()
  }

  await patchAppConfig({
    ...(useServiceCore ? { corePermissionMode: 'elevated' as const } : {}),
    ...(useServiceSysProxy && sysProxy
      ? {
          sysProxy: {
            ...sysProxy,
            settingMode: 'exec' as const,
            guard: false,
            guardNotify: false
          }
        }
      : {}),
    ...(useServiceDNS ? { autoSetDNSMode: 'exec' as const } : {})
  })

  mainWindow?.webContents.send('appConfigUpdated')
  floatingWindow?.webContents.send('appConfigUpdated')

  try {
    if (useServiceCore) {
      const promises = await startCore()
      await Promise.all(promises)
      mainWindow?.webContents.send('core-started')
    }
    new Notification({ title: '服务不可用,已切换到非服务模式' }).show()
  } finally {
    mainWindow?.webContents.reload()
    floatingWindow?.webContents.reload()
  }
}

export async function restartCore(): Promise<void> {
  try {
    await stopCore()
    const promises = await startCore()
    await Promise.all(promises)
  } catch (e) {
    dialog.showErrorBox('内核启动出错', `${e}`)
  }
}

export async function keepCoreAlive(): Promise<void> {
  try {
    const { corePermissionMode = 'elevated' } = await getAppConfig()
    if (corePermissionMode === 'service') {
      return
    }

    await startCore(true)
    if (child && child.pid) {
      await writeFile(path.join(dataDir(), 'core.pid'), child.pid.toString())
    }
  } catch (e) {
    dialog.showErrorBox('内核启动出错', `${e}`)
  }
}

export async function quitWithoutCore(): Promise<void> {
  await keepCoreAlive()
  await startMonitor(true)
  app.exit()
}

export async function startNetworkDetection(): Promise<void> {
  await startNetworkDetectionWithCore({
    shouldStartCore: (networkDownHandled) =>
      (networkDownHandled && !child) || Boolean(child?.killed),
    startCore: async () => {
      const promises = await startCore()
      await Promise.all(promises)
    },
    stopCore
  })
}

export const stopNetworkDetection = stopNetworkDetectionController


================================================
FILE: src/main/core/mihomoApi.ts
================================================
import axios, { AxiosInstance } from 'axios'
import { getAppConfig, getControledMihomoConfig } from '../config'
import { mainWindow } from '..'
import WebSocket from 'ws'
import { tray } from '../resolve/tray'
import { calcTraffic } from '../utils/calc'
import { getRuntimeConfig } from './factory'
import { floatingWindow } from '../resolve/floatingWindow'
import { mihomoIpcPath, serviceIpcPath } from '../utils/dirs'
import { publishMihomoLog } from '../utils/log'
import { createSignedServiceAxios, getServiceAuthHeaders } from '../service/api'

let axiosIns: AxiosInstance = null!
let mihomoTrafficWs: WebSocket | null = null
let trafficRetry = 10
let trafficReconnectTimer: NodeJS.Timeout | null = null
let mihomoMemoryWs: WebSocket | null = null
let memoryRetry = 10
let memoryReconnectTimer: NodeJS.Timeout | null = null
let mihomoLogsWs: WebSocket | null = null
let logsRetry = 10
let logsReconnectTimer: NodeJS.Timeout | null = null
let mihomoConnectionsWs: WebSocket | null = null
let connectionsRetry = 10
let connectionsReconnectTimer: NodeJS.Timeout | null = null
let axiosMode: 'direct' | 'service' | null = null
const wsReconnectDelay = 1000

function isWebSocketActive(ws: WebSocket | null): boolean {
  return ws?.readyState === WebSocket.OPEN || ws?.readyState === WebSocket.CONNECTING
}

function closeWebSocket(ws: WebSocket): void {
  ws.removeAllListeners()
  ws.on('error', () => {})
  if (isWebSocketActive(ws)) {
    ws.close()
  }
}

export const getAxios = async (force: boolean = false): Promise<AxiosInstance> => {
  const { corePermissionMode = 'elevated' } = await getAppConfig()
  const nextMode = corePermissionMode === 'service' ? 'service' : 'direct'
  const currentSocketPath = nextMode === 'service' ? serviceIpcPath() : mihomoIpcPath()
  const currentBaseURL =
    nextMode === 'service' ? 'http://localhost/core/controller' : 'http://localhost'

  if (
    axiosIns &&
    (axiosIns.defaults.socketPath !== currentSocketPath ||
      axiosIns.defaults.baseURL !== currentBaseURL ||
      axiosMode !== nextMode)
  ) {
    force = true
  }

  if (axiosIns && !force) return axiosIns

  axiosMode = nextMode
  if (nextMode === 'service') {
    axiosIns = createSignedServiceAxios(currentBaseURL)
  } else {
    axiosIns = axios.create({
      baseURL: currentBaseURL,
      socketPath: currentSocketPath,
      timeout: 15000
    })

    axiosIns.interceptors.response.use(
      (response) => {
        return response.data
      },
      (error) => {
        if (error.response && error.response.data) {
          return Promise.reject(error.response.data)
        }
        return Promise.reject(error)
      }
    )
  }
  return axiosIns
}

const mihomoWs = async (path: string): Promise<WebSocket> => {
  const { corePermissionMode = 'elevated' } = await getAppConfig()
  if (corePermissionMode !== 'service') {
    return new WebSocket(`ws+unix:${mihomoIpcPath()}:${path}`)
  }

  const servicePath = `/core/controller${path}`
  return new WebSocket(`ws+unix:${serviceIpcPath()}:${servicePath}`, {
    headers: getServiceAuthHeaders('GET', servicePath)
  })
}

export async function mihomoVersion(): Promise<ControllerVersion> {
  const instance = await getAxios()
  return await instance.get('/version')
}

export const mihomoConfig = async (): Promise<ControllerConfigs> => {
  const instance = await getAxios()
  return await instance.get('/configs')
}

export const patchMihomoConfig = async (patch: Partial<ControllerConfigs>): Promise<void> => {
  const instance = await getAxios()
  return await instance.patch('/configs', patch)
}

export const mihomoCloseConnection = async (id: string): Promise<void> => {
  const instance = await getAxios()
  return await instance.delete(`/connections/${encodeURIComponent(id)}`)
}

export const mihomoGetConnections = async (): Promise<ControllerConnections> => {
  const instance = await getAxios()
  return await instance.get('/connections')
}

export const mihomoCloseConnections = async (name?: string): Promise<void> => {
  const instance = await getAxios()
  if (name) {
    const connectionsInfo = await mihomoGetConnections()
    const targetConnections =
      connectionsInfo?.connections?.filter((conn) => conn.chains && conn.chains.includes(name)) ||
      []
    for (const conn of targetConnections) {
      try {
        await mihomoCloseConnection(conn.id)
      } catch (error) {
        // ignore
      }
    }
  } else {
    return await instance.delete('/connections')
  }
}

export const mihomoRules = async (): Promise<ControllerRules> => {
  const instance = await getAxios()
  return await instance.get('/rules')
}

export const mihomoProxies = async (): Promise<ControllerProxies> => {
  const instance = await getAxios()
  return await instance.get('/proxies')
}

export const mihomoGroups = async (): Promise<ControllerMixedGroup[]> => {
  const { mode = 'rule' } = await getControledMihomoConfig()
  if (mode === 'direct') return []
  const proxies = await mihomoProxies()
  const runtime = await getRuntimeConfig()
  const groups: ControllerMixedGroup[] = []
  runtime?.['proxy-groups']?.forEach((group: { name: string; url?: string }) => {
    const { name, url } = group
    if (proxies.proxies[name] && 'all' in proxies.proxies[name] && !proxies.proxies[name].hidden) {
      const newGroup = proxies.proxies[name]
      newGroup.testUrl = url
      const newAll = newGroup.all.map((name) => proxies.proxies[name])
      groups.push({ ...newGroup, all: newAll })
    }
  })
  if (!groups.find((group) => group.name === 'GLOBAL')) {
    const newGlobal = proxies.proxies['GLOBAL'] as ControllerGroupDetail
    if (!newGlobal.hidden) {
      const newAll = newGlobal.all.map((name) => proxies.proxies[name])
      groups.push({ ...newGlobal, all: newAll })
    }
  }
  if (mode === 'global') {
    const global = groups.findIndex((group) => group.name === 'GLOBAL')
    groups.unshift(groups.splice(global, 1)[0])
  }
  return groups
}

export const mihomoProxyProviders = async (): Promise<ControllerProxyProviders> => {
  const instance = await getAxios()
  return await instance.get('/providers/proxies')
}

export const mihomoUpdateProxyProviders = async (name: string): Promise<void> => {
  const instance = await getAxios()
  return await instance.put(`/providers/proxies/${encodeURIComponent(name)}`)
}

export const mihomoRuleProviders = async (): Promise<ControllerRuleProviders> => {
  const instance = await getAxios()
  return await instance.get('/providers/rules')
}

export const mihomoUpdateRuleProviders = async (name: string): Promise<void> => {
  const instance = await getAxios()
  return await instance.put(`/providers/rules/${encodeURIComponent(name)}`)
}

export const mihomoChangeProxy = async (
  group: string,
  proxy: string
): Promise<ControllerProxiesDetail> => {
  const instance = await getAxios()
  return await instance.put(`/proxies/${encodeURIComponent(group)}`, { name: proxy })
}

export const mihomoUnfixedProxy = async (group: string): Promise<ControllerProxiesDetail> => {
  const instance = await getAxios()
  return await instance.delete(`/proxies/${encodeURIComponent(group)}`)
}

export const mihomoProxyDelay = async (
  proxy: string,
  url?: string
): Promise<ControllerProxiesDelay> => {
  const appConfig = await getAppConfig()
  const { delayTestUrl, delayTestTimeout } = appConfig
  const instance = await getAxios()
  return await instance.get(`/proxies/${encodeURIComponent(proxy)}/delay`, {
    params: {
      url: url || delayTestUrl || 'https://www.gstatic.com/generate_204',
      timeout: delayTestTimeout || 5000
    }
  })
}

export const mihomoGroupDelay = async (
  group: string,
  url?: string
): Promise<ControllerGroupDelay> => {
  const appConfig = await getAppConfig()
  const { delayTestUrl, delayTestTimeout } = appConfig
  const instance = await getAxios()
  return await instance.get(`/group/${encodeURIComponent(group)}/delay`, {
    params: {
      url: url || delayTestUrl || 'https://www.gstatic.com/generate_204',
      timeout: delayTestTimeout || 5000
    }
  })
}

export const mihomoRulesDisable = async (rules: Record<string, boolean>): Promise<void> => {
  const instance = await getAxios()
  return await instance.patch(`/rules/disable`, rules)
}

export const mihomoUpgrade = async (channel: string): Promise<void> => {
  if (process.platform === 'win32') await patchMihomoConfig({ 'log-level': 'info' })
  const instance = await getAxios()
  return await instance.post(`/upgrade?channel=${encodeURIComponent(channel)}`)
}

export const mihomoUpgradeGeo = async (): Promise<void> => {
  const instance = await getAxios()
  return await instance.post('/upgrade/geo')
}

export const mihomoUpgradeUI = async (): Promise<void> => {
  const instance = await getAxios()
  return await instance.post('/upgrade/ui')
}

export const startMihomoTraffic = async (): Promise<void> => {
  if (isWebSocketActive(mihomoTrafficWs)) return
  if (trafficReconnectTimer) {
    clearTimeout(trafficReconnectTimer)
    trafficReconnectTimer = null
  }
  await mihomoTraffic()
}

export const stopMihomoTraffic = (): void => {
  trafficRetry = 10
  if (trafficReconnectTimer) {
    clearTimeout(trafficReconnectTimer)
    trafficReconnectTimer = null
  }
  if (mihomoTrafficWs) {
    closeWebSocket(mihomoTrafficWs)
    mihomoTrafficWs = null
  }
}

const mihomoTraffic = async (): Promise<void> => {
  const ws = await mihomoWs('/traffic')
  mihomoTrafficWs = ws

  ws.onmessage = async (e): Promise<void> => {
    const data = e.data as string
    const json = JSON.parse(data) as ControllerTraffic
    trafficRetry = 10
    try {
      mainWindow?.webContents.send('mihomoTraffic', json)
      if (process.platform !== 'linux') {
        tray?.setToolTip(
          '↑' +
            `${calcTraffic(json.up)}/s`.padStart(9) +
            '\n↓' +
            `${calcTraffic(json.down)}/s`.padStart(9)
        )
      }
      floatingWindow?.webContents.send('mihomoTraffic', json)
    } catch {
      // ignore
    }
  }

  ws.onclose = (): void => {
    if (mihomoTrafficWs === ws) {
      mihomoTrafficWs = null
    }
    if (mihomoTrafficWs !== null || !trafficRetry || trafficReconnectTimer) return

    trafficRetry--
    trafficReconnectTimer = setTimeout(() => {
      trafficReconnectTimer = null
      mihomoTraffic().catch(() => {})
    }, wsReconnectDelay)
  }

  ws.onerror = (): void => {
    ws.close()
  }
}

export const startMihomoMemory = async (): Promise<void> => {
  if (isWebSocketActive(mihomoMemoryWs)) return
  if (memoryReconnectTimer) {
    clearTimeout(memoryReconnectTimer)
    memoryReconnectTimer = null
  }
  await mihomoMemory()
}

export const stopMihomoMemory = (): void => {
  memoryRetry = 10
  if (memoryReconnectTimer) {
    clearTimeout(memoryReconnectTimer)
    memoryReconnectTimer = null
  }
  if (mihomoMemoryWs) {
    closeWebSocket(mihomoMemoryWs)
    mihomoMemoryWs = null
  }
}

const mihomoMemory = async (): Promise<void> => {
  const ws = await mihomoWs('/memory')
  mihomoMemoryWs = ws

  ws.onmessage = (e): void => {
    const data = e.data as string
    memoryRetry = 10
    try {
      mainWindow?.webContents.send('mihomoMemory', JSON.parse(data) as ControllerMemory)
    } catch {
      // ignore
    }
  }

  ws.onclose = (): void => {
    if (mihomoMemoryWs === ws) {
      mihomoMemoryWs = null
    }
    if (mihomoMemoryWs !== null || !memoryRetry || memoryReconnectTimer) return

    memoryRetry--
    memoryReconnectTimer = setTimeout(() => {
      memoryReconnectTimer = null
      mihomoMemory().catch(() => {})
    }, wsReconnectDelay)
  }

  ws.onerror = (): void => {
    ws.close()
  }
}

export const startMihomoLogs = async (): Promise<void> => {
  if (isWebSocketActive(mihomoLogsWs)) return
  if (logsReconnectTimer) {
    clearTimeout(logsReconnectTimer)
    logsReconnectTimer = null
  }
  await mihomoLogs()
}

export const stopMihomoLogs = (): void => {
  logsRetry = 10
  if (logsReconnectTimer) {
    clearTimeout(logsReconnectTimer)
    logsReconnectTimer = null
  }
  if (mihomoLogsWs) {
    closeWebSocket(mihomoLogsWs)
    mihomoLogsWs = null
  }
}

export const restartMihomoLogs = async (): Promise<void> => {
  stopMihomoLogs()
  await startMihomoLogs()
}

const mihomoLogs = async (): Promise<void> => {
  const { realtimeLogLevel } = await getAppConfig()
  const { 'log-level': logLevel = 'info' } = await getControledMihomoConfig()
  const activeLogLevel = realtimeLogLevel ?? logLevel

  const ws = await mihomoWs(`/logs?level=${activeLogLevel}`)
  mihomoLogsWs = ws

  ws.onmessage = (e): void => {
    const data = e.data as string
    logsRetry = 10
    try {
      publishMihomoLog(JSON.parse(data) as ControllerLog)
    } catch {
      // ignore
    }
  }

  ws.onclose = (): void => {
    if (mihomoLogsWs === ws) {
      mihomoLogsWs = null
    }
    if (mihomoLogsWs !== null || !logsRetry || logsReconnectTimer) return

    logsRetry--
    logsReconnectTimer = setTimeout(() => {
      logsReconnectTimer = null
      mihomoLogs().catch(() => {})
    }, wsReconnectDelay)
  }

  ws.onerror = (): void => {
    ws.close()
  }
}

export const startMihomoConnections = async (): Promise<void> => {
  if (isWebSocketActive(mihomoConnectionsWs)) return
  if (connectionsReconnectTimer) {
    clearTimeout(connectionsReconnectTimer)
    connectionsReconnectTimer = null
  }
  await mihomoConnections()
}

export const stopMihomoConnections = (): void => {
  connectionsRetry = 10
  if (connectionsReconnectTimer) {
    clearTimeout(connectionsReconnectTimer)
    connectionsReconnectTimer = null
  }
  if (mihomoConnectionsWs) {
    closeWebSocket(mihomoConnectionsWs)
    mihomoConnectionsWs = null
  }
}

export const restartMihomoConnections = async (): Promise<void> => {
  stopMihomoConnections()
  await startMihomoConnections()
}

const mihomoConnections = async (): Promise<void> => {
  const { connectionInterval = 500 } = await getAppConfig()
  const ws = await mihomoWs(`/connections?interval=${connectionInterval}`)
  mihomoConnectionsWs = ws

  ws.onmessage = (e): void => {
    const data = e.data as string
    connectionsRetry = 10
    try {
      mainWindow?.webContents.send('mihomoConnections', JSON.parse(data) as ControllerConnections)
    } catch {
      // ignore
    }
  }

  ws.onclose = (): void => {
    if (mihomoConnectionsWs === ws) {
      mihomoConnectionsWs = null
    }
    if (mihomoConnectionsWs !== null || !connectionsRetry || connectionsReconnectTimer) return

    connectionsRetry--
    connectionsReconnectTimer = setTimeout(() => {
      connectionsReconnectTimer = null
      mihomoConnections().catch(() => {})
    }, wsReconnectDelay)
  }

  ws.onerror = (): void => {
    ws.close()
  }
}


================================================
FILE: src/main/core/network.ts
================================================
import { execFile } from 'child_process'
import { net } from 'electron'
import os from 'os'
import { promisify } from 'util'
import { getAppConfig, getControledMihomoConfig, patchAppConfig } from '../config'
import { setSysDns } from '../service/api'
import { triggerSysProxy } from '../sys/sysproxy'

export interface NetworkCoreController {
  shouldStartCore: (networkDownHandled: boolean) => boolean
  startCore: () => Promise<void>
  stopCore: () => Promise<void>
}

let setPublicDNSTimer: NodeJS.Timeout | null = null
let recoverDNSTimer: NodeJS.Timeout | null = null
let networkDetectionTimer: NodeJS.Timeout | null = null
let networkDownHandled = false

export async function getDefaultDevice(): Promise<string> {
  const execFilePromise = promisify(execFile)
  const { stdout: deviceOut } = await execFilePromise('route', ['-n', 'get', 'default'])
  let device = deviceOut.split('\n').find((s) => s.includes('interface:'))
  device = device?.trim().split(' ').slice(1).join(' ')
  if (!device) throw new Error('Get device failed')
  return device
}

async function getDefaultService(): Promise<string> {
  const execFilePromise = promisify(execFile)
  const device = await getDefaultDevice()
  const { stdout: order } = await execFilePromise('networksetup', ['-listnetworkserviceorder'])
  const block = order.split('\n\n').find((s) => s.includes(`Device: ${device}`))
  if (!block) throw new Error('Get networkservice failed')
  for (const line of block.split('\n')) {
    if (line.match(/^\(\d+\).*/)) {
      return line.trim().split(' ').slice(1).join(' ')
    }
  }
  throw new Error('Get service failed')
}

async function getOriginDNS(): Promise<void> {
  const execFilePromise = promisify(execFile)
  const service 
Download .txt
gitextract_1hpos0hd/

├── .editorconfig
├── .github/
│   ├── ISSUE_TEMPLATE/
│   │   ├── bug_report_zh.yml
│   │   ├── config.yml
│   │   └── feature_request_zh.yml
│   └── workflows/
│       ├── build.yml
│       └── issues.yml
├── .gitignore
├── .npmrc
├── .prettierignore
├── .prettierrc.yaml
├── .vscode/
│   ├── extensions.json
│   ├── launch.json
│   └── settings.json
├── LICENSE
├── README.md
├── aur/
│   ├── sparkle/
│   │   ├── PKGBUILD
│   │   ├── sparkle.install
│   │   └── sparkle.sh
│   ├── sparkle-bin/
│   │   ├── PKGBUILD
│   │   ├── sparkle.install
│   │   └── sparkle.sh
│   ├── sparkle-electron/
│   │   ├── PKGBUILD
│   │   ├── sparkle.desktop
│   │   ├── sparkle.install
│   │   └── sparkle.sh
│   ├── sparkle-electron-bin/
│   │   ├── PKGBUILD
│   │   ├── sparkle.desktop
│   │   ├── sparkle.install
│   │   └── sparkle.sh
│   ├── sparkle-electron-git/
│   │   ├── PKGBUILD
│   │   ├── sparkle.desktop
│   │   ├── sparkle.install
│   │   └── sparkle.sh
│   └── sparkle-git/
│       ├── PKGBUILD
│       ├── sparkle.install
│       └── sparkle.sh
├── build/
│   ├── entitlements.mac.plist
│   ├── icon.icns
│   ├── installer.nsh
│   ├── linux/
│   │   ├── postinst
│   │   └── preinst
│   └── pkg-scripts/
│       ├── postinstall
│       └── preinstall
├── changelog.md
├── electron-builder.yml
├── electron.vite.config.ts
├── eslint.config.cjs
├── package.json
├── patches/
│   └── vite-plugin-monaco-editor@1.1.0.patch
├── scripts/
│   ├── checksum.ts
│   ├── package.json
│   ├── prepare.ts
│   ├── telegram.ts
│   └── updater.ts
├── src/
│   ├── main/
│   │   ├── config/
│   │   │   ├── app.ts
│   │   │   ├── controledMihomo.ts
│   │   │   ├── index.ts
│   │   │   ├── override.ts
│   │   │   └── profile.ts
│   │   ├── core/
│   │   │   ├── factory.ts
│   │   │   ├── manager.ts
│   │   │   ├── mihomoApi.ts
│   │   │   ├── network.ts
│   │   │   ├── permission-check.ts
│   │   │   ├── permission.ts
│   │   │   ├── process-control.ts
│   │   │   ├── profile-check.ts
│   │   │   ├── profileUpdater.ts
│   │   │   ├── startup-chain.ts
│   │   │   ├── startupHook.ts
│   │   │   └── subStoreApi.ts
│   │   ├── index.ts
│   │   ├── resolve/
│   │   │   ├── autoUpdater.ts
│   │   │   ├── backup.ts
│   │   │   ├── floatingWindow.ts
│   │   │   ├── gistApi.ts
│   │   │   ├── menu.ts
│   │   │   ├── server.ts
│   │   │   ├── shortcut.ts
│   │   │   ├── theme.ts
│   │   │   ├── trafficMonitor.ts
│   │   │   └── tray.ts
│   │   ├── service/
│   │   │   ├── api.ts
│   │   │   ├── auth-store.ts
│   │   │   ├── key.ts
│   │   │   └── manager.ts
│   │   ├── sys/
│   │   │   ├── autoRun.ts
│   │   │   ├── interface.ts
│   │   │   ├── misc.ts
│   │   │   ├── ssid.ts
│   │   │   └── sysproxy.ts
│   │   └── utils/
│   │       ├── appName.ts
│   │       ├── calc.ts
│   │       ├── defaultIcon.ts
│   │       ├── devicePathResolver.ts
│   │       ├── dirs.ts
│   │       ├── elevation.ts
│   │       ├── encrypt.ts
│   │       ├── icon.ts
│   │       ├── init.ts
│   │       ├── ipc.ts
│   │       ├── log.ts
│   │       ├── merge.ts
│   │       ├── template.ts
│   │       ├── userAgent.ts
│   │       └── yaml.ts
│   ├── preload/
│   │   ├── index.d.ts
│   │   └── index.ts
│   ├── renderer/
│   │   ├── floating.html
│   │   ├── index.html
│   │   ├── src/
│   │   │   ├── App.tsx
│   │   │   ├── FloatingApp.tsx
│   │   │   ├── TrayMenuApp.tsx
│   │   │   ├── assets/
│   │   │   │   ├── floating.css
│   │   │   │   ├── hero.mjs
│   │   │   │   ├── main.css
│   │   │   │   └── traymenu.css
│   │   │   ├── components/
│   │   │   │   ├── base/
│   │   │   │   │   ├── base-confirm.tsx
│   │   │   │   │   ├── base-editor-lazy.tsx
│   │   │   │   │   ├── base-editor.tsx
│   │   │   │   │   ├── base-error-boundary.tsx
│   │   │   │   │   ├── base-list-editor.tsx
│   │   │   │   │   ├── base-page.tsx
│   │   │   │   │   ├── base-qrcode-modal.tsx
│   │   │   │   │   ├── base-setting-card.tsx
│   │   │   │   │   ├── base-setting-item.tsx
│   │   │   │   │   ├── border-switch.css
│   │   │   │   │   ├── border-swtich.tsx
│   │   │   │   │   ├── collapse-input.tsx
│   │   │   │   │   ├── interface-select.tsx
│   │   │   │   │   ├── mihomo-icon.tsx
│   │   │   │   │   └── substore-icon.tsx
│   │   │   │   ├── connections/
│   │   │   │   │   ├── connection-detail-modal.tsx
│   │   │   │   │   ├── connection-item.tsx
│   │   │   │   │   └── connection-setting-modal.tsx
│   │   │   │   ├── dns/
│   │   │   │   │   └── advanced-dns-setting.tsx
│   │   │   │   ├── logs/
│   │   │   │   │   └── log-item.tsx
│   │   │   │   ├── mihomo/
│   │   │   │   │   ├── advanced-settings.tsx
│   │   │   │   │   ├── controller-setting.tsx
│   │   │   │   │   ├── env-setting.tsx
│   │   │   │   │   ├── interface-modal.tsx
│   │   │   │   │   ├── log-setting.tsx
│   │   │   │   │   ├── permission-modal.tsx
│   │   │   │   │   ├── port-setting.tsx
│   │   │   │   │   └── service-modal.tsx
│   │   │   │   ├── override/
│   │   │   │   │   ├── edit-file-modal.tsx
│   │   │   │   │   ├── edit-info-modal.tsx
│   │   │   │   │   ├── exec-log-modal.tsx
│   │   │   │   │   └── override-item.tsx
│   │   │   │   ├── profiles/
│   │   │   │   │   ├── edit-file-modal.tsx
│   │   │   │   │   ├── edit-info-modal.tsx
│   │   │   │   │   ├── profile-item.tsx
│   │   │   │   │   └── profile-setting-modal.tsx
│   │   │   │   ├── proxies/
│   │   │   │   │   ├── proxy-item.tsx
│   │   │   │   │   └── proxy-setting-modal.tsx
│   │   │   │   ├── resources/
│   │   │   │   │   ├── geo-data.tsx
│   │   │   │   │   ├── proxy-provider.tsx
│   │   │   │   │   ├── rule-provider.tsx
│   │   │   │   │   └── viewer.tsx
│   │   │   │   ├── rules/
│   │   │   │   │   └── rule-item.tsx
│   │   │   │   ├── settings/
│   │   │   │   │   ├── actions.tsx
│   │   │   │   │   ├── advanced-settings.tsx
│   │   │   │   │   ├── appearance-confis.tsx
│   │   │   │   │   ├── css-editor-modal.tsx
│   │   │   │   │   ├── general-config.tsx
│   │   │   │   │   ├── shortcut-config.tsx
│   │   │   │   │   ├── sider-config.tsx
│   │   │   │   │   ├── substore-config.tsx
│   │   │   │   │   ├── webdav-config.tsx
│   │   │   │   │   └── webdav-restore-modal.tsx
│   │   │   │   ├── sider/
│   │   │   │   │   ├── config-viewer.tsx
│   │   │   │   │   ├── conn-card.tsx
│   │   │   │   │   ├── dns-card.tsx
│   │   │   │   │   ├── log-card.tsx
│   │   │   │   │   ├── mihomo-core-card.tsx
│   │   │   │   │   ├── outbound-mode-switcher.tsx
│   │   │   │   │   ├── override-card.tsx
│   │   │   │   │   ├── profile-card.tsx
│   │   │   │   │   ├── proxy-card.tsx
│   │   │   │   │   ├── resource-card.tsx
│   │   │   │   │   ├── rule-card.tsx
│   │   │   │   │   ├── sniff-card.tsx
│   │   │   │   │   ├── substore-card.tsx
│   │   │   │   │   ├── sysproxy-switcher.tsx
│   │   │   │   │   ├── traffic-chart.tsx
│   │   │   │   │   └── tun-switcher.tsx
│   │   │   │   ├── sysproxy/
│   │   │   │   │   ├── bypass-editor-modal.tsx
│   │   │   │   │   └── pac-editor-modal.tsx
│   │   │   │   └── updater/
│   │   │   │       ├── updater-button.tsx
│   │   │   │       └── updater-modal.tsx
│   │   │   ├── floating.tsx
│   │   │   ├── hooks/
│   │   │   │   ├── use-app-config.tsx
│   │   │   │   ├── use-card-dnd-sensors.ts
│   │   │   │   ├── use-controled-mihomo-config.tsx
│   │   │   │   ├── use-groups.tsx
│   │   │   │   ├── use-override-config.tsx
│   │   │   │   ├── use-profile-config.tsx
│   │   │   │   └── use-rules.tsx
│   │   │   ├── main.tsx
│   │   │   ├── pages/
│   │   │   │   ├── connections.tsx
│   │   │   │   ├── dns.tsx
│   │   │   │   ├── logs.tsx
│   │   │   │   ├── mihomo.tsx
│   │   │   │   ├── override.tsx
│   │   │   │   ├── profiles.tsx
│   │   │   │   ├── proxies.tsx
│   │   │   │   ├── resources.tsx
│   │   │   │   ├── rules.tsx
│   │   │   │   ├── settings.tsx
│   │   │   │   ├── sniffer.tsx
│   │   │   │   ├── substore.tsx
│   │   │   │   ├── syspeoxy.tsx
│   │   │   │   └── tun.tsx
│   │   │   ├── routes/
│   │   │   │   └── index.tsx
│   │   │   ├── traymenu.tsx
│   │   │   └── utils/
│   │   │       ├── advanced-filter.ts
│   │   │       ├── calc.ts
│   │   │       ├── connection-filter-autocomplete.ts
│   │   │       ├── debounce.ts
│   │   │       ├── delay-test.ts
│   │   │       ├── driver.ts
│   │   │       ├── env.d.ts
│   │   │       ├── hash.ts
│   │   │       ├── image.ts
│   │   │       ├── includes.ts
│   │   │       ├── init.ts
│   │   │       ├── ipc.ts
│   │   │       ├── mihomo-log-store.ts
│   │   │       └── validate.ts
│   │   └── traymenu.html
│   └── shared/
│       └── types/
│           ├── app.d.ts
│           ├── controller.d.ts
│           ├── mihomo.d.ts
│           └── types.d.ts
├── tsconfig.json
├── tsconfig.node.json
└── tsconfig.web.json
Download .txt
SYMBOL INDEX (878 symbols across 133 files)

FILE: scripts/prepare.ts
  constant TEMP_DIR (line 9) | const TEMP_DIR = path.join(cwd, 'node_modules/.temp')
  function getErrorMessage (line 21) | function getErrorMessage(error: unknown) {
  constant MIHOMO_ALPHA_VERSION_URL (line 26) | const MIHOMO_ALPHA_VERSION_URL =
  constant MIHOMO_ALPHA_URL_PREFIX (line 28) | const MIHOMO_ALPHA_URL_PREFIX = `https://github.com/MetaCubeX/mihomo/rel...
  constant MIHOMO_ALPHA_VERSION (line 29) | let MIHOMO_ALPHA_VERSION: string
  constant MIHOMO_ALPHA_MAP (line 31) | const MIHOMO_ALPHA_MAP = {
  function getLatestAlphaVersion (line 43) | async function getLatestAlphaVersion() {
  constant MIHOMO_VERSION_URL (line 58) | const MIHOMO_VERSION_URL =
  constant MIHOMO_URL_PREFIX (line 60) | const MIHOMO_URL_PREFIX = `https://github.com/MetaCubeX/mihomo/releases/...
  constant MIHOMO_VERSION (line 61) | let MIHOMO_VERSION: string
  constant MIHOMO_MAP (line 63) | const MIHOMO_MAP = {
  function getLatestReleaseVersion (line 75) | async function getLatestReleaseVersion() {
  function MihomoAlpha (line 103) | function MihomoAlpha() {
  function mihomo (line 120) | function mihomo() {
  function resolveSidecar (line 139) | async function resolveSidecar(binInfo) {
  function resolveResource (line 219) | async function resolveResource(binInfo) {
  function downloadFile (line 243) | async function downloadFile(url, path) {
  type Task (line 404) | type Task = {
  function runTask (line 475) | async function runTask() {

FILE: src/main/config/app.ts
  constant ENCRYPTED_FIELDS (line 12) | const ENCRYPTED_FIELDS = ['systemCorePath', 'serviceAuthKey'] as const
  constant PLAINTEXT_FALLBACK_FIELDS (line 13) | const PLAINTEXT_FALLBACK_FIELDS = ['systemCorePath'] as const
  function isValidConfig (line 15) | function isValidConfig(config: unknown): config is AppConfig {
  function safeWriteConfig (line 21) | async function safeWriteConfig(content: string): Promise<void> {
  function decryptConfig (line 49) | function decryptConfig(config: AppConfig): AppConfig {
  function encryptConfig (line 67) | function encryptConfig(config: AppConfig): AppConfig {
  function getAppConfig (line 80) | async function getAppConfig(force = false): Promise<AppConfig> {
  function patchAppConfig (line 99) | async function patchAppConfig(patch: Partial<AppConfig>): Promise<void> {
  function getAppConfigSync (line 109) | function getAppConfigSync(): AppConfig {

FILE: src/main/config/controledMihomo.ts
  function getControledMihomoConfig (line 11) | async function getControledMihomoConfig(force = false): Promise<Partial<...
  function patchControledMihomoConfig (line 29) | async function patchControledMihomoConfig(patch: Partial<MihomoConfig>):...

FILE: src/main/config/override.ts
  function getOverrideConfig (line 14) | async function getOverrideConfig(force = false): Promise<OverrideConfig> {
  function setOverrideConfig (line 23) | async function setOverrideConfig(config: OverrideConfig): Promise<void> {
  function getOverrideItem (line 28) | async function getOverrideItem(id: string | undefined): Promise<Override...
  function updateOverrideItem (line 33) | async function updateOverrideItem(item: OverrideItem): Promise<void> {
  function addOverrideItem (line 43) | async function addOverrideItem(item: Partial<OverrideItem>): Promise<voi...
  function removeOverrideItem (line 54) | async function removeOverrideItem(id: string): Promise<void> {
  function createOverride (line 62) | async function createOverride(item: Partial<OverrideItem>): Promise<Over...
  function getOverride (line 168) | async function getOverride(id: string, ext: 'js' | 'yaml' | 'log'): Prom...
  function setOverride (line 175) | async function setOverride(id: string, ext: 'js' | 'yaml', content: stri...

FILE: src/main/config/profile.ts
  constant FILE_PERMISSION_ELEVATION_REQUIRED (line 23) | const FILE_PERMISSION_ELEVATION_REQUIRED = 'FILE_PERMISSION_ELEVATION_RE...
  function getCertFingerprint (line 25) | function getCertFingerprint(cert: tls.PeerCertificate) {
  function getProfileConfig (line 29) | async function getProfileConfig(force = false): Promise<ProfileConfig> {
  function setProfileConfig (line 38) | async function setProfileConfig(config: ProfileConfig): Promise<void> {
  function getProfileItem (line 43) | async function getProfileItem(id: string | undefined): Promise<ProfileIt...
  function changeCurrentProfile (line 49) | async function changeCurrentProfile(id: string): Promise<void> {
  function updateProfileItem (line 64) | async function updateProfileItem(item: ProfileItem): Promise<void> {
  function addProfileItem (line 75) | async function addProfileItem(item: Partial<ProfileItem>): Promise<void> {
  function removeProfileItem (line 91) | async function removeProfileItem(id: string): Promise<void> {
  function getCurrentProfileItem (line 116) | async function getCurrentProfileItem(): Promise<ProfileItem> {
  function createProfile (line 121) | async function createProfile(item: Partial<ProfileItem>): Promise<Profil...
  function getProfileStr (line 286) | async function getProfileStr(id: string | undefined): Promise<string> {
  function getProfileParseStr (line 294) | async function getProfileParseStr(id: string | undefined): Promise<strin...
  function setProfileStr (line 305) | async function setProfileStr(id: string, content: string): Promise<void> {
  function getProfile (line 311) | async function getProfile(id: string | undefined): Promise<MihomoConfig> {
  function parseFilename (line 319) | function parseFilename(str: string): string {
  function parseSubinfo (line 330) | function parseSubinfo(str: string): SubscriptionUserInfo {
  function isAbsolutePath (line 340) | function isAbsolutePath(path: string): boolean {
  function resolveEditableFilePath (line 344) | function resolveEditableFilePath(
  function isSubPath (line 355) | function isSubPath(base: string, target: string): boolean {
  function isPermissionError (line 360) | function isPermissionError(error: unknown): boolean {
  function shellQuote (line 368) | function shellQuote(value: string): string {
  function isManagedEditableFile (line 372) | function isManagedEditableFile(target: string, current: string | undefin...
  function buildPermissionRepairCommand (line 376) | function buildPermissionRepairCommand(
  function repairEditableFilePermissions (line 395) | async function repairEditableFilePermissions(
  function attemptWriteFile (line 416) | async function attemptWriteFile(target: string, content: string): Promis...
  function writeEditableFile (line 421) | async function writeEditableFile(
  function getFileStr (line 446) | async function getFileStr(path: string): Promise<string> {
  function setFileStr (line 452) | async function setFileStr(path: string, content: string): Promise<void> {
  function saveFileStrWithElevation (line 456) | async function saveFileStrWithElevation(path: string, content: string): ...
  function saveFileStr (line 460) | async function saveFileStr(path: string, content: string, elevate: boole...

FILE: src/main/core/factory.ts
  function generateProfile (line 31) | async function generateProfile(): Promise<void> {
  function cleanProfile (line 65) | async function cleanProfile(
  function cleanBooleanConfigs (line 85) | function cleanBooleanConfigs(profile: MihomoConfig): void {
  function cleanNumberConfigs (line 115) | function cleanNumberConfigs(profile: MihomoConfig): void {
  function cleanStringConfigs (line 129) | function cleanStringConfigs(profile: MihomoConfig): void {
  function configureLanSettings (line 150) | function configureLanSettings(profile: MihomoConfig): void {
  function cleanAuthenticationConfig (line 178) | function cleanAuthenticationConfig(profile: MihomoConfig): void {
  function cleanTunConfig (line 186) | function cleanTunConfig(profile: MihomoConfig): void {
  function cleanDnsConfig (line 220) | function cleanDnsConfig(profile: MihomoConfig, controlDns: boolean): void {
  function cleanSnifferConfig (line 259) | function cleanSnifferConfig(profile: MihomoConfig, controlSniff: boolean...
  function cleanProxyConfigs (line 266) | function cleanProxyConfigs(profile: MihomoConfig): void {
  function prepareProfileWorkDir (line 289) | async function prepareProfileWorkDir(current: string | undefined): Promi...
  function overrideProfile (line 309) | async function overrideProfile(
  function runOverrideScript (line 334) | async function runOverrideScript(
  function format (line 385) | function format(data: unknown): string {
  function getRuntimeConfigStr (line 396) | async function getRuntimeConfigStr(): Promise<string> {
  function getRawProfileStr (line 400) | async function getRawProfileStr(): Promise<string> {
  function getCurrentProfileStr (line 404) | async function getCurrentProfileStr(): Promise<string> {
  function getOverrideProfileStr (line 408) | async function getOverrideProfileStr(): Promise<string> {
  function getRuntimeConfig (line 412) | async function getRuntimeConfig(): Promise<MihomoConfig> {

FILE: src/main/core/manager.ts
  type ServiceCoreConnectionProbe (line 100) | type ServiceCoreConnectionProbe = {
  function startMihomoApiStreams (line 106) | async function startMihomoApiStreams(): Promise<void> {
  function completeCoreInitialization (line 114) | async function completeCoreInitialization(logLevel?: LogLevel): Promise<...
  function waitForMihomoReady (line 135) | async function waitForMihomoReady(): Promise<void> {
  function waitForServiceCoreConnection (line 149) | async function waitForServiceCoreConnection(
  function startCore (line 178) | async function startCore(detached = false): Promise<Promise<void>[]> {
  function stopCore (line 428) | async function stopCore(force = false): Promise<void> {
  function ensureServiceCoreEventHandler (line 488) | function ensureServiceCoreEventHandler(): void {
  function releaseServiceCoreEventHandler (line 501) | function releaseServiceCoreEventHandler(): void {
  function handleServiceCoreEvent (line 512) | async function handleServiceCoreEvent(event: ServiceCoreEvent): Promise<...
  function handleServiceCoreEventStreamState (line 564) | async function handleServiceCoreEventStreamState(
  function resumeServiceCoreAfterReconnect (line 583) | async function resumeServiceCoreAfterReconnect(): Promise<void> {
  function isDuplicateServiceCoreEvent (line 609) | function isDuplicateServiceCoreEvent(event: ServiceCoreEvent): boolean {
  function scheduleServiceCoreStreamsRestart (line 621) | function scheduleServiceCoreStreamsRestart(): void {
  function restartServiceCoreStreams (line 634) | async function restartServiceCoreStreams(): Promise<void> {
  function ensureServiceCoreStreamsStarted (line 643) | async function ensureServiceCoreStreamsStarted(): Promise<void> {
  function fallbackToElevatedCore (line 673) | async function fallbackToElevatedCore(
  function fallbackUnavailableServiceModes (line 686) | async function fallbackUnavailableServiceModes(reason: unknown): Promise...
  function restartCore (line 749) | async function restartCore(): Promise<void> {
  function keepCoreAlive (line 759) | async function keepCoreAlive(): Promise<void> {
  function quitWithoutCore (line 775) | async function quitWithoutCore(): Promise<void> {
  function startNetworkDetection (line 781) | async function startNetworkDetection(): Promise<void> {

FILE: src/main/core/mihomoApi.ts
  function isWebSocketActive (line 29) | function isWebSocketActive(ws: WebSocket | null): boolean {
  function closeWebSocket (line 33) | function closeWebSocket(ws: WebSocket): void {
  function mihomoVersion (line 96) | async function mihomoVersion(): Promise<ControllerVersion> {

FILE: src/main/core/network.ts
  type NetworkCoreController (line 9) | interface NetworkCoreController {
  function getDefaultDevice (line 20) | async function getDefaultDevice(): Promise<string> {
  function getDefaultService (line 29) | async function getDefaultService(): Promise<string> {
  function getOriginDNS (line 43) | async function getOriginDNS(): Promise<void> {
  function setDNS (line 54) | async function setDNS(dns: string, mode: 'none' | 'exec' | 'service'): P...
  function setPublicDNS (line 68) | async function setPublicDNS(): Promise<void> {
  function recoverDNS (line 82) | async function recoverDNS(): Promise<void> {
  function startNetworkDetection (line 96) | async function startNetworkDetection(controller: NetworkCoreController):...
  function stopNetworkDetection (line 129) | async function stopNetworkDetection(): Promise<void> {
  function isAnyNetworkInterfaceUp (line 136) | function isAnyNetworkInterfaceUp(excludedKeywords: string[] = []): boole...

FILE: src/main/core/permission-check.ts
  function hasSetuidPermission (line 3) | function hasSetuidPermission(permissions: string): boolean {
  function checkCorePermissionPathSync (line 7) | function checkCorePermissionPathSync(corePath: string): boolean {

FILE: src/main/core/permission.ts
  type CoreName (line 7) | type CoreName = 'mihomo' | 'mihomo-alpha'
  class UserCancelledError (line 9) | class UserCancelledError extends Error {
    method constructor (line 10) | constructor(message = '用户取消操作') {
  function isUserCancelledError (line 16) | function isUserCancelledError(error: unknown): boolean {
  function manualGrantCorePermition (line 30) | async function manualGrantCorePermition(cores?: CoreName[]): Promise<voi...
  function checkCorePermissionSync (line 73) | function checkCorePermissionSync(coreName: CoreName): boolean {
  function checkCorePermission (line 77) | async function checkCorePermission(): Promise<{ mihomo: boolean; 'mihomo...
  function revokeCorePermission (line 102) | async function revokeCorePermission(cores?: CoreName[]): Promise<void> {

FILE: src/main/core/process-control.ts
  function stopChildProcess (line 4) | async function stopChildProcess(process: ChildProcess): Promise<void> {

FILE: src/main/core/profile-check.ts
  function checkProfile (line 7) | async function checkProfile(): Promise<void> {

FILE: src/main/core/profileUpdater.ts
  function calculateUpdateDelay (line 5) | function calculateUpdateDelay(item: ProfileItem): number {
  function initProfileUpdater (line 22) | async function initProfileUpdater(): Promise<void> {
  function addProfileUpdater (line 78) | async function addProfileUpdater(item: ProfileItem): Promise<void> {
  function delProfileUpdater (line 111) | async function delProfileUpdater(id: string): Promise<void> {

FILE: src/main/core/startup-chain.ts
  type RuntimeConfigProviders (line 5) | interface RuntimeConfigProviders {
  type CoreEnvironmentOptions (line 10) | interface CoreEnvironmentOptions {
  type CoreSpawnArgsOptions (line 18) | interface CoreSpawnArgsOptions {
  type ProviderInitializationTracker (line 25) | interface ProviderInitializationTracker {
  function createCoreEnvironment (line 31) | function createCoreEnvironment(
  function createCoreSpawnArgs (line 44) | function createCoreSpawnArgs(options: CoreSpawnArgsOptions): string[] {
  function createProviderInitializationTracker (line 64) | function createProviderInitializationTracker(
  function isControllerListenError (line 93) | function isControllerListenError(logLine: string): boolean {
  function isControllerReadyLog (line 100) | function isControllerReadyLog(logLine: string): boolean {
  function isTunPermissionError (line 107) | function isTunPermissionError(logLine: string): boolean {
  function isUpdaterFinishedLog (line 113) | function isUpdaterFinishedLog(logLine: string): boolean {
  function normalizeProviderName (line 117) | function normalizeProviderName(value: string): string {

FILE: src/main/core/startupHook.ts
  type CoreStartupHook (line 11) | interface CoreStartupHook {
  type CoreHookWaiter (line 19) | interface CoreHookWaiter {
  function shellQuote (line 24) | function shellQuote(value: string): string {
  function hookTouchCommand (line 28) | function hookTouchCommand(file: string): string {
  function coreHookDir (line 32) | function coreHookDir(): string {
  function createCoreStartupHook (line 39) | async function createCoreStartupHook(): Promise<CoreStartupHook> {
  function createCoreHookWaiter (line 60) | function createCoreHookWaiter(hook: CoreStartupHook): CoreHookWaiter {

FILE: src/main/core/subStoreApi.ts
  function subStoreSubs (line 5) | async function subStoreSubs(): Promise<SubStoreSub[]> {
  function subStoreCollections (line 12) | async function subStoreCollections(): Promise<SubStoreSub[]> {

FILE: src/main/index.ts
  function scheduleLightweightMode (line 42) | async function scheduleLightweightMode(): Promise<void> {
  function exitApp (line 73) | function exitApp(): void {
  function customRelaunch (line 125) | function customRelaunch(): void {
  function setNotQuitDialog (line 175) | function setNotQuitDialog(): void {
  function showWindow (line 179) | function showWindow(): number {
  function showQuitConfirmDialog (line 198) | function showQuitConfirmDialog(): Promise<boolean> {
  function handleDeepLink (line 354) | async function handleDeepLink(url: string): Promise<void> {
  function showProfileInstallConfirm (line 414) | async function showProfileInstallConfirm(url: string, name?: string | nu...
  function parseFilename (line 454) | function parseFilename(str: string): string {
  function showOverrideInstallConfirm (line 464) | async function showOverrideInstallConfirm(url: string, name?: string | n...
  function createWindow (line 491) | async function createWindow(appConfig?: AppConfig): Promise<void> {
  function triggerMainWindow (line 611) | async function triggerMainWindow(): Promise<void> {
  function showMainWindow (line 619) | async function showMainWindow(): Promise<void> {
  function closeMainWindow (line 643) | function closeMainWindow(): void {

FILE: src/main/resolve/autoUpdater.ts
  function checkUpdate (line 19) | async function checkUpdate(): Promise<AppVersion | undefined> {
  function stopServiceForPortableUpdate (line 46) | async function stopServiceForPortableUpdate(): Promise<void> {
  function downloadAndInstallUpdate (line 60) | async function downloadAndInstallUpdate(version: string): Promise<void> {
  function cancelUpdate (line 209) | async function cancelUpdate(): Promise<void> {

FILE: src/main/resolve/backup.ts
  function webdavBackup (line 16) | async function webdavBackup(): Promise<boolean> {
  function webdavRestore (line 50) | async function webdavRestore(filename: string): Promise<void> {
  function listWebdavBackups (line 68) | async function listWebdavBackups(): Promise<string[]> {
  function webdavDelete (line 85) | async function webdavDelete(filename: string): Promise<void> {

FILE: src/main/resolve/floatingWindow.ts
  function preallocateGpuResources (line 12) | async function preallocateGpuResources(): Promise<void> {
  function createFloatingWindow (line 32) | async function createFloatingWindow(): Promise<void> {
  function showFloatingWindow (line 85) | async function showFloatingWindow(): Promise<void> {
  function triggerFloatingWindow (line 93) | async function triggerFloatingWindow(): Promise<void> {
  function closeFloatingWindow (line 111) | async function closeFloatingWindow(): Promise<void> {
  function showContextMenu (line 121) | async function showContextMenu(): Promise<void> {

FILE: src/main/resolve/gistApi.ts
  type GistInfo (line 5) | interface GistInfo {
  function listGists (line 11) | async function listGists(token: string): Promise<GistInfo[]> {
  function createGist (line 31) | async function createGist(token: string, content: string): Promise<void> {
  function updateGist (line 57) | async function updateGist(token: string, id: string, content: string): P...
  function getGistUrl (line 82) | async function getGistUrl(): Promise<string> {
  function uploadRuntimeConfig (line 98) | async function uploadRuntimeConfig(): Promise<void> {

FILE: src/main/resolve/menu.ts
  function createApplicationMenu (line 7) | async function createApplicationMenu(): Promise<void> {
  function updateApplicationMenu (line 204) | async function updateApplicationMenu(): Promise<void> {

FILE: src/main/resolve/server.ts
  function findAvailablePort (line 28) | function findAvailablePort(startPort: number): Promise<number> {
  function startPacServer (line 49) | async function startPacServer(): Promise<void> {
  function stopPacServer (line 69) | async function stopPacServer(): Promise<void> {
  function startSubStoreFrontendServer (line 75) | async function startSubStoreFrontendServer(): Promise<void> {
  function stopSubStoreFrontendServer (line 89) | async function stopSubStoreFrontendServer(): Promise<void> {
  function startSubStoreBackendServer (line 95) | async function startSubStoreBackendServer(): Promise<void> {
  function stopSubStoreBackendServer (line 141) | async function stopSubStoreBackendServer(): Promise<void> {
  function downloadSubStore (line 147) | async function downloadSubStore(): Promise<void> {

FILE: src/main/resolve/shortcut.ts
  function registerShortcut (line 14) | async function registerShortcut(
  function initShortcut (line 130) | async function initShortcut(): Promise<void> {

FILE: src/main/resolve/theme.ts
  function normalizeThemeCss (line 14) | function normalizeThemeCss(css: string): string {
  function resolveThemes (line 43) | async function resolveThemes(): Promise<{ key: string; label: string }[]> {
  function fetchThemes (line 64) | async function fetchThemes(): Promise<void> {
  function importThemes (line 82) | async function importThemes(files: string[]): Promise<void> {
  function readTheme (line 92) | async function readTheme(theme: string): Promise<string> {
  function writeTheme (line 97) | async function writeTheme(theme: string, css: string): Promise<void> {
  function applyTheme (line 101) | async function applyTheme(theme: string): Promise<void> {

FILE: src/main/resolve/trafficMonitor.ts
  function startMonitor (line 10) | async function startMonitor(detached = false): Promise<void> {
  function stopMonitor (line 38) | async function stopMonitor(): Promise<void> {

FILE: src/main/resolve/tray.ts
  function formatDelayText (line 45) | function formatDelayText(delay: number): string {
  function createDarwinTrayIcon (line 54) | function createDarwinTrayIcon(): Electron.NativeImage {
  function positionCustomTrayWindow (line 60) | function positionCustomTrayWindow(win: BrowserWindow): void {
  function hideCustomTray (line 76) | function hideCustomTray(): void {
  function showCustomTray (line 82) | async function showCustomTray(): Promise<void> {
  function handleTrayClick (line 133) | async function handleTrayClick(): Promise<void> {
  function createTray (line 452) | async function createTray(): Promise<void> {
  function updateTrayMenu (line 517) | async function updateTrayMenu(): Promise<void> {
  function copyEnv (line 530) | async function copyEnv(
  function showTrayIcon (line 570) | async function showTrayIcon(): Promise<void> {
  function closeTrayIcon (line 576) | async function closeTrayIcon(): Promise<void> {
  function setDockVisible (line 588) | function setDockVisible(visible: boolean): void {

FILE: src/main/service/api.ts
  class ServiceAPIError (line 15) | class ServiceAPIError extends Error {
    method constructor (line 19) | constructor(message: string, options?: { status?: number; responseData...
  function getHeaderValue (line 27) | function getHeaderValue(config: AxiosRequestConfig, name: string): string {
  function shouldUseJsonEncoding (line 44) | function shouldUseJsonEncoding(config: AxiosRequestConfig): boolean {
  function getRequestBodyBytes (line 49) | function getRequestBodyBytes(config: AxiosRequestConfig): Buffer {
  function canonicalizeQuery (line 83) | function canonicalizeQuery(urlObj: URL): string {
  function resolveRequestUrl (line 98) | function resolveRequestUrl(instance: AxiosInstance, config: AxiosRequest...
  function buildCanonicalRequest (line 102) | function buildCanonicalRequest(
  function signServiceRequest (line 126) | function signServiceRequest(
  function attachServiceAuth (line 150) | function attachServiceAuth(instance: AxiosInstance): void {
  function setServiceUnavailableFallbackHandler (line 154) | function setServiceUnavailableFallbackHandler(
  function isServiceConnectionError (line 160) | function isServiceConnectionError(error: unknown): boolean {
  function scheduleServiceUnavailableFallback (line 174) | function scheduleServiceUnavailableFallback(reason: unknown): void {
  function runServiceUnavailableFallback (line 185) | async function runServiceUnavailableFallback(reason: unknown): Promise<v...
  function isServiceReachable (line 198) | async function isServiceReachable(): Promise<boolean> {
  function getResponseErrorMessage (line 212) | function getResponseErrorMessage(responseData: unknown, fallback: string...
  function createServiceAPIError (line 221) | function createServiceAPIError(error: unknown): unknown {
  function handleServiceAxiosError (line 246) | function handleServiceAxiosError(error: unknown): Promise<never> {
  type ServiceCoreLaunchProfile (line 359) | interface ServiceCoreLaunchProfile {
  type ServiceCoreEventType (line 370) | type ServiceCoreEventType =
  type ServiceCoreEvent (line 382) | interface ServiceCoreEvent {
  type ServiceSysproxyEventType (line 393) | type ServiceSysproxyEventType =
  type ServiceSysproxyEvent (line 402) | interface ServiceSysproxyEvent {
  type ServiceCoreEventHandler (line 426) | type ServiceCoreEventHandler = (event: ServiceCoreEvent) => void | Promi...
  type ServiceCoreEventStreamState (line 427) | type ServiceCoreEventStreamState = 'connected' | 'disconnected'
  type ServiceCoreEventStreamHandler (line 428) | type ServiceCoreEventStreamHandler = (state: ServiceCoreEventStreamState...
  type ServiceSysproxyEventHandler (line 429) | type ServiceSysproxyEventHandler = (event: ServiceSysproxyEvent) => void...
  function subscribeServiceCoreEvents (line 442) | function subscribeServiceCoreEvents(handler: ServiceCoreEventHandler): (...
  function subscribeServiceCoreEventStream (line 449) | function subscribeServiceCoreEventStream(
  function subscribeServiceSysproxyEvents (line 458) | function subscribeServiceSysproxyEvents(handler: ServiceSysproxyEventHan...
  function startServiceCoreEventStream (line 465) | async function startServiceCoreEventStream(): Promise<void> {
  function stopServiceCoreEventStream (line 523) | function stopServiceCoreEventStream(): void {
  function startServiceSysproxyEventStream (line 536) | async function startServiceSysproxyEventStream(): Promise<void> {
  function stopServiceSysproxyEventStream (line 586) | function stopServiceSysproxyEventStream(): void {
  function scheduleServiceCoreEventReconnect (line 599) | function scheduleServiceCoreEventReconnect(): void {
  function scheduleServiceSysproxyEventReconnect (line 609) | function scheduleServiceSysproxyEventReconnect(): void {
  function waitForServiceCoreEventsSocket (line 619) | async function waitForServiceCoreEventsSocket(ws: WebSocket): Promise<vo...
  function waitForServiceSysproxyEventsSocket (line 636) | async function waitForServiceSysproxyEventsSocket(ws: WebSocket): Promis...
  function dispatchServiceCoreEvent (line 653) | async function dispatchServiceCoreEvent(data: WebSocket.RawData): Promis...
  function dispatchServiceSysproxyEvent (line 663) | async function dispatchServiceSysproxyEvent(data: WebSocket.RawData): Pr...
  function dispatchServiceCoreEventStreamState (line 673) | async function dispatchServiceCoreEventStreamState(

FILE: src/main/service/auth-store.ts
  type EncryptedServiceAuthEnvelope (line 12) | interface EncryptedServiceAuthEnvelope {
  type PlainServiceAuthEnvelope (line 17) | interface PlainServiceAuthEnvelope extends ServiceAuthSecret {
  type ServiceAuthEnvelope (line 22) | type ServiceAuthEnvelope = EncryptedServiceAuthEnvelope | PlainServiceAu...
  type ServiceAuthSecret (line 24) | interface ServiceAuthSecret extends KeyPair {}
  function normalizeServiceAuthSecret (line 26) | function normalizeServiceAuthSecret(secret: {
  function usePlainServiceAuthStorage (line 51) | function usePlainServiceAuthStorage(): boolean {
  function canPersistServiceAuthSecret (line 55) | function canPersistServiceAuthSecret(): boolean {
  function loadServiceAuthSecret (line 59) | async function loadServiceAuthSecret(): Promise<ServiceAuthSecret | null> {
  function saveServiceAuthSecret (line 92) | async function saveServiceAuthSecret(secret: ServiceAuthSecret): Promise...
  function deleteServiceAuthSecret (line 130) | async function deleteServiceAuthSecret(): Promise<void> {

FILE: src/main/service/key.ts
  type KeyPair (line 3) | interface KeyPair {
  class KeyManager (line 9) | class KeyManager {
    method generateKeyPair (line 14) | generateKeyPair(): KeyPair {
    method setKeyPair (line 43) | setKeyPair(publicKey: string, privateKey: string, keyId?: string): void {
    method getKeyID (line 57) | getKeyID(): string {
    method getPublicKey (line 64) | getPublicKey(): string {
    method getPrivateKey (line 71) | getPrivateKey(): string {
    method signData (line 78) | signData(data: string): string {
    method isInitialized (line 92) | isInitialized(): boolean {
    method clear (line 102) | clear(): void {
  function computeKeyId (line 109) | function computeKeyId(publicKey: string): string {
  function generateKeyPair (line 123) | function generateKeyPair(): KeyPair {
  function signData (line 128) | function signData(privateKey: string, data: string): string {

FILE: src/main/service/manager.ts
  function parseLegacyServiceAuth (line 18) | function parseLegacyServiceAuth(value: string): ServiceAuthSecret | null {
  function clearLegacyServiceAuth (line 35) | async function clearLegacyServiceAuth(): Promise<void> {
  function loadServiceAuthFromLegacyConfig (line 41) | async function loadServiceAuthFromLegacyConfig(): Promise<ServiceAuthSec...
  function loadAvailableServiceAuth (line 64) | async function loadAvailableServiceAuth(): Promise<ServiceAuthSecret | n...
  function applyServiceAuthSecret (line 81) | function applyServiceAuthSecret(target: KeyManager, secret: ServiceAuthS...
  function currentServiceAuthSecret (line 88) | function currentServiceAuthSecret(target: KeyManager): ServiceAuthSecret {
  function ensurePersistedServiceAuth (line 96) | async function ensurePersistedServiceAuth(target: KeyManager): Promise<S...
  function initKeyManager (line 117) | async function initKeyManager(): Promise<KeyManager> {
  function getKeyManager (line 129) | function getKeyManager(): KeyManager {
  function getPublicKey (line 136) | function getPublicKey(): string {
  class UserCancelledError (line 140) | class UserCancelledError extends Error {
    method constructor (line 141) | constructor(message = '用户取消操作') {
  function isUserCancelledError (line 147) | function isUserCancelledError(error: unknown): boolean {
  type ServiceLogEntry (line 161) | interface ServiceLogEntry {
  function parseServiceLog (line 171) | function parseServiceLog(output: string): ServiceLogEntry | null {
  function serviceCommandOutput (line 196) | function serviceCommandOutput(value: unknown): string {
  function serviceCommandErrorMessage (line 206) | function serviceCommandErrorMessage(error: unknown): string {
  function getAuthorizedPrincipalArgs (line 216) | async function getAuthorizedPrincipalArgs(): Promise<string[]> {
  function exportPublicKey (line 244) | function exportPublicKey(): string {
  function getAxios (line 248) | function getAxios() {
  function waitForServiceReady (line 252) | async function waitForServiceReady(timeoutMs = 15000): Promise<void> {
  function initService (line 273) | async function initService(): Promise<void> {
  function installService (line 297) | async function installService(): Promise<void> {
  function uninstallService (line 310) | async function uninstallService(): Promise<void> {
  function startService (line 323) | async function startService(): Promise<void> {
  function stopService (line 336) | async function stopService(): Promise<void> {
  function restartService (line 349) | async function restartService(): Promise<void> {
  function serviceStatus (line 362) | async function serviceStatus(): Promise<
  function testServiceConnection (line 406) | async function testServiceConnection(): Promise<boolean> {

FILE: src/main/sys/autoRun.ts
  function checkAutoRun (line 53) | async function checkAutoRun(): Promise<boolean> {
  function enableAutoRun (line 79) | async function enableAutoRun(): Promise<void> {
  function disableAutoRun (line 124) | async function disableAutoRun(): Promise<void> {

FILE: src/main/sys/interface.ts
  function getInterfaces (line 3) | function getInterfaces(): NodeJS.Dict<NetworkInterfaceInfo[]> {

FILE: src/main/sys/misc.ts
  function getFilePath (line 19) | function getFilePath(ext: string[]): string[] | undefined {
  function readTextFile (line 27) | async function readTextFile(filePath: string): Promise<string> {
  function openFile (line 31) | function openFile(type: 'profile' | 'override', id: string, ext?: 'yaml'...
  function openUWPTool (line 40) | async function openUWPTool(): Promise<void> {
  function setupFirewall (line 46) | async function setupFirewall(): Promise<void> {
  function setNativeTheme (line 68) | function setNativeTheme(theme: 'system' | 'light' | 'dark'): void {
  function prepareElevateTaskFile (line 109) | function prepareElevateTaskFile(): string {
  function createElevateTaskSync (line 119) | function createElevateTaskSync(): void {
  function createElevateTask (line 126) | async function createElevateTask(): Promise<void> {
  function deleteElevateTask (line 138) | async function deleteElevateTask(): Promise<void> {
  function checkElevateTask (line 146) | async function checkElevateTask(): Promise<boolean> {
  function resetAppConfig (line 155) | function resetAppConfig(): void {

FILE: src/main/sys/ssid.ts
  function getCurrentSSID (line 9) | async function getCurrentSSID(): Promise<string | undefined> {
  function checkSSID (line 35) | async function checkSSID(): Promise<void> {
  function startSSIDCheck (line 58) | async function startSSIDCheck(): Promise<void> {
  function getSSIDByAirport (line 63) | async function getSSIDByAirport(): Promise<string | undefined> {
  function getSSIDByNetworksetup (line 79) | async function getSSIDByNetworksetup(): Promise<string | undefined> {
  function getSSIDByNetsh (line 93) | async function getSSIDByNetsh(): Promise<string | undefined> {
  function getSSIDByIwconfig (line 104) | async function getSSIDByIwconfig(): Promise<string | undefined> {

FILE: src/main/sys/sysproxy.ts
  function registryArgs (line 24) | function registryArgs(useRegistry: boolean): string[] {
  function triggerSysProxy (line 28) | async function triggerSysProxy(
  function setSysProxy (line 48) | async function setSysProxy(onlyActiveDevice: boolean, useRegistry = fals...
  function disableSysProxy (line 164) | async function disableSysProxy(onlyActiveDevice: boolean, useRegistry = ...
  function updateSysproxyGuardEventStream (line 182) | function updateSysproxyGuardEventStream(enabled: boolean): void {
  function handleSysproxyGuardEvent (line 201) | async function handleSysproxyGuardEvent(event: ServiceSysproxyEvent): Pr...
  function shouldNotifySysproxyGuardEvent (line 215) | async function shouldNotifySysproxyGuardEvent(event: ServiceSysproxyEven...
  function disableSysProxySync (line 230) | function disableSysProxySync(useRegistry = false): void {

FILE: src/main/utils/appName.ts
  function getAppName (line 7) | async function getAppName(appPath: string): Promise<string> {
  function getLocalizedAppName (line 43) | function getLocalizedAppName(appPath: string): string {

FILE: src/main/utils/calc.ts
  function calcTraffic (line 1) | function calcTraffic(byte: number): string {
  function formatNumString (line 21) | function formatNumString(num: number): string {

FILE: src/main/utils/devicePathResolver.ts
  type DosDeviceMapping (line 4) | type DosDeviceMapping = {
  constant DOS_DEVICE_BUFFER_CHARS (line 9) | const DOS_DEVICE_BUFFER_CHARS = 4096
  constant DRIVE_LETTERS (line 10) | const DRIVE_LETTERS = Array.from({ length: 26 }, (_, i) => `${String.fro...
  constant IS_WINDOWS (line 11) | const IS_WINDOWS = process.platform === 'win32'
  function queryDosDevice (line 20) | function queryDosDevice(deviceName: string): string | null {
  function resolveWithDosDeviceMappings (line 34) | function resolveWithDosDeviceMappings(targetPath: string): string | null {

FILE: src/main/utils/dirs.ts
  function isPortable (line 11) | function isPortable(): boolean {
  function dataDir (line 15) | function dataDir(): string {
  function taskDir (line 23) | function taskDir(): string {
  function subStoreDir (line 31) | function subStoreDir(): string {
  function exeDir (line 35) | function exeDir(): string {
  function exePath (line 39) | function exePath(): string {
  function resourcesDir (line 43) | function resourcesDir(): string {
  function resourcesFilesDir (line 55) | function resourcesFilesDir(): string {
  function themesDir (line 59) | function themesDir(): string {
  function mihomoIpcPath (line 63) | function mihomoIpcPath(): string {
  function serviceIpcPath (line 77) | function serviceIpcPath(): string {
  function mihomoCoreDir (line 84) | function mihomoCoreDir(): string {
  function mihomoCorePath (line 88) | function mihomoCorePath(core: string): string {
  function systemCorePath (line 104) | function systemCorePath(): string {
  function servicePath (line 109) | function servicePath(): string {
  function serviceAuthStorePath (line 114) | function serviceAuthStorePath(): string {
  function appConfigPath (line 118) | function appConfigPath(): string {
  function controledMihomoConfigPath (line 122) | function controledMihomoConfigPath(): string {
  function profileConfigPath (line 126) | function profileConfigPath(): string {
  function profilesDir (line 130) | function profilesDir(): string {
  function profilePath (line 134) | function profilePath(id: string): string {
  function overrideDir (line 138) | function overrideDir(): string {
  function overrideConfigPath (line 142) | function overrideConfigPath(): string {
  function overridePath (line 146) | function overridePath(id: string, ext: 'js' | 'yaml' | 'log'): string {
  function mihomoWorkDir (line 150) | function mihomoWorkDir(): string {
  function mihomoProfileWorkDir (line 154) | function mihomoProfileWorkDir(id: string | undefined): string {
  function mihomoTestDir (line 158) | function mihomoTestDir(): string {
  function mihomoWorkConfigPath (line 162) | function mihomoWorkConfigPath(id: string | undefined): string {
  function logDir (line 170) | function logDir(): string {
  function datedLogPath (line 174) | function datedLogPath(prefix?: string): string {
  function logPath (line 180) | function logPath(): string {
  function appLogPath (line 184) | function appLogPath(): string {
  function coreLogPath (line 188) | function coreLogPath(): string {
  function substoreLogPath (line 192) | function substoreLogPath(): string {
  function hasCommand (line 196) | function hasCommand(command: string): boolean {
  function findSystemMihomo (line 207) | function findSystemMihomo(): string[] {

FILE: src/main/utils/elevation.ts
  function isRunningAsAdmin (line 8) | async function isRunningAsAdmin(): Promise<boolean> {
  function shellQuote (line 23) | function shellQuote(arg: string): string {
  function appleScriptQuote (line 27) | function appleScriptQuote(value: string): string {
  function execWithElevation (line 31) | async function execWithElevation(command: string, args: string[]): Promi...

FILE: src/main/utils/encrypt.ts
  constant ENCRYPTED_PREFIX (line 3) | const ENCRYPTED_PREFIX = 'enc:'
  function isSecureStorageAvailable (line 5) | function isSecureStorageAvailable(): boolean {
  function encryptString (line 21) | function encryptString(plainText: string): string {
  function decryptString (line 40) | function decryptString(encryptedText: string): string {
  function isEncrypted (line 60) | function isEncrypted(text: string): boolean {
  function encryptStringStrict (line 65) | function encryptStringStrict(plainText: string): string {
  function decryptStringStrict (line 80) | function decryptStringStrict(encryptedText: string): string {

FILE: src/main/utils/icon.ts
  function isIOSApp (line 13) | function isIOSApp(appPath: string): boolean {
  function hasIOSAppIcon (line 23) | function hasIOSAppIcon(appPath: string): boolean {
  function hasMacOSAppIcon (line 36) | function hasMacOSAppIcon(appPath: string): boolean {
  function findBestAppPath (line 50) | function findBestAppPath(appPath: string): string | null {
  function normalizeLinuxAppId (line 85) | function normalizeLinuxAppId(value: string): string {
  function tokenizeLinuxAppId (line 92) | function tokenizeLinuxAppId(value: string): string[] {
  function getLinuxDesktopDirs (line 98) | function getLinuxDesktopDirs(): string[] {
  function collectDesktopFiles (line 108) | function collectDesktopFiles(dir: string, files: string[] = []): string[] {
  function getDesktopEntryValue (line 129) | function getDesktopEntryValue(content: string, key: string): string | nu...
  function parseShellWords (line 139) | function parseShellWords(command: string): string[] {
  function parseDesktopExecCommand (line 188) | function parseDesktopExecCommand(command: string): string {
  function isDesktopEntryDisabled (line 192) | function isDesktopEntryDisabled(content: string): boolean {
  function resolveExistingLinuxPath (line 196) | function resolveExistingLinuxPath(filePath: string): string | null {
  function resolveLinuxCommandPath (line 208) | function resolveLinuxCommandPath(command: string): string | null {
  function readLinuxScript (line 233) | function readLinuxScript(filePath: string): string | null {
  function expandLinuxScriptCommand (line 251) | function expandLinuxScriptCommand(command: string, scriptPath: string): ...
  function extractExecCommandFromScriptLine (line 258) | function extractExecCommandFromScriptLine(line: string, scriptPath: stri...
  function collectLinuxExecTargets (line 295) | function collectLinuxExecTargets(entry: string, seen = new Set<string>()...
  function getLinuxPathCandidates (line 333) | function getLinuxPathCandidates(value: string): string[] {
  function getLinuxAppIds (line 337) | function getLinuxAppIds(appPath: string, execTargets: Iterable<string> =...
  function matchLinuxAppId (line 358) | function matchLinuxAppId(entryValue: string, appIds: Set<string>): number {
  function getExecTargetMatchRank (line 392) | function getExecTargetMatchRank(entry: string | null, appExecTargets: Se...
  function getDesktopFileRank (line 411) | function getDesktopFileRank(
  function findDesktopFile (line 463) | async function findDesktopFile(appPath: string): Promise<string | null> {
  function parseIconNameFromDesktopFile (line 521) | function parseIconNameFromDesktopFile(content: string): string | null {
  function resolveIconPath (line 526) | function resolveIconPath(iconName: string): string | null {
  function getIconMimeType (line 560) | function getIconMimeType(iconPath: string): string {
  function getWindowsFileIconDataURL (line 573) | async function getWindowsFileIconDataURL(appPath: string): Promise<strin...
  function getIconDataURL (line 626) | async function getIconDataURL(appPath: string): Promise<string> {
  function getImageDataURL (line 686) | async function getImageDataURL(url: string): Promise<string> {

FILE: src/main/utils/init.ts
  function initDirs (line 45) | async function initDirs(): Promise<void> {
  function initConfig (line 67) | async function initConfig(): Promise<void> {
  function initFiles (line 93) | async function initFiles(): Promise<void> {
  function cleanup (line 116) | async function cleanup(): Promise<void> {
  function migration (line 149) | async function migration(): Promise<void> {
  function initDeeplink (line 196) | function initDeeplink(): void {
  function init (line 210) | async function init(): Promise<void> {

FILE: src/main/utils/ipc.ts
  function ipcErrorWrapper (line 137) | function ipcErrorWrapper<T>( // eslint-disable-next-line @typescript-esl...
  function patchAppConfigWithServiceSync (line 160) | async function patchAppConfigWithServiceSync(patch: Partial<AppConfig>):...
  function registerIpcMainHandlers (line 185) | function registerIpcMainHandlers(): void {

FILE: src/main/utils/log.ts
  type LogTarget (line 8) | type LogTarget = 'app' | 'core' | 'substore'
  type MihomoLogSource (line 9) | type MihomoLogSource = 'out' | 'ws'
  type LogContent (line 10) | type LogContent = string | Buffer
  type CachedControllerLog (line 11) | interface CachedControllerLog extends ControllerLog {
  function resolveLogPath (line 31) | function resolveLogPath(target: LogTarget): string {
  function shouldSaveLogs (line 46) | async function shouldSaveLogs(): Promise<boolean> {
  function getMaxLogFileSizeBytes (line 51) | async function getMaxLogFileSizeBytes(): Promise<number> {
  function getWriteStream (line 56) | function getWriteStream(target: LogTarget): WriteStream {
  function closeWriteStream (line 79) | async function closeWriteStream(target: LogTarget): Promise<void> {
  function retainTarget (line 109) | function retainTarget(target: LogTarget): void {
  function releaseTarget (line 113) | function releaseTarget(target: LogTarget): void {
  function trimCachedLogs (line 124) | function trimCachedLogs(): void {
  function isEmptyLogContent (line 129) | function isEmptyLogContent(content: LogContent): boolean {
  function getLogContentSize (line 133) | function getLogContentSize(content: LogContent): number {
  function normalizeLogContent (line 137) | function normalizeLogContent(content: string): string[] {
  function parseFileLines (line 144) | function parseFileLines(content: string): { lines: string[]; hasTrailing...
  function getLogFileSize (line 159) | async function getLogFileSize(target: LogTarget, path: string): Promise<...
  function trimLogFileToSize (line 175) | async function trimLogFileToSize(
  function enforceLogFileSizeLimit (line 208) | async function enforceLogFileSizeLimit(
  function unquoteLogfmtValue (line 223) | function unquoteLogfmtValue(value: string): string {
  function parseLogfmtLine (line 235) | function parseLogfmtLine(line: string): Record<string, string> {
  function normalizeOutLogTime (line 246) | function normalizeOutLogTime(value?: string): string | undefined {
  function normalizeOutLogLevel (line 259) | function normalizeOutLogLevel(level: string | undefined, fallbackType: L...
  function createControllerLogFromOutLine (line 275) | function createControllerLogFromOutLine(line: string, fallbackType: LogL...
  function pushCachedLog (line 293) | function pushCachedLog(log: ControllerLog): CachedControllerLog {
  function broadcastLog (line 304) | function broadcastLog(log: CachedControllerLog): void {
  function cacheAndBroadcastLog (line 312) | function cacheAndBroadcastLog(log: ControllerLog): void {
  function publishTargetLogLines (line 316) | function publishTargetLogLines(content: string, type: LogLevel): void {
  function flushCoreLogLines (line 322) | function flushCoreLogLines(lineBuffer: string, content: string, type: Lo...
  function normalizeWriteChunk (line 330) | function normalizeWriteChunk(chunk: string | Buffer): Buffer {
  function appendLog (line 334) | async function appendLog(target: LogTarget, content: LogContent): Promis...
  function setMihomoLogSource (line 364) | function setMihomoLogSource(source: MihomoLogSource): void {
  function appendAppLog (line 368) | async function appendAppLog(content: string): Promise<void> {
  function publishMihomoLog (line 372) | function publishMihomoLog(log: ControllerLog): void {
  function getCachedMihomoLogs (line 377) | function getCachedMihomoLogs(): CachedControllerLog[] {
  function clearCachedMihomoLogs (line 381) | function clearCachedMihomoLogs(): void {
  function createLogWritable (line 385) | function createLogWritable(target: LogTarget, type: LogLevel = 'info'): ...

FILE: src/main/utils/merge.ts
  function isObject (line 2) | function isObject(item: any): boolean {
  function trimWrap (line 6) | function trimWrap(str: string): string {
  function deepMerge (line 13) | function deepMerge<T extends object>(target: T, other: Partial<T>, isOve...

FILE: src/main/utils/userAgent.ts
  constant TIMEOUT_MS (line 4) | const TIMEOUT_MS = 300
  constant DEFAULT_USER_AGENT (line 5) | const DEFAULT_USER_AGENT = 'clash.meta/alpha-e89af72'
  function getUserAgent (line 7) | async function getUserAgent(): Promise<string> {

FILE: src/main/utils/yaml.ts
  function parseYaml (line 3) | function parseYaml<T = unknown>(content: string): T {
  function stringifyYaml (line 13) | function stringifyYaml(data: unknown): string {
  function addYamlTagsToProxiesShortId (line 17) | function addYamlTagsToProxiesShortId(yamlContent: string, includeNestedP...

FILE: src/preload/index.d.ts
  type Window (line 5) | interface Window {

FILE: src/renderer/src/TrayMenuApp.tsx
  type TrafficData (line 9) | interface TrafficData {

FILE: src/renderer/src/components/base/base-confirm.tsx
  type ConfirmButton (line 5) | interface ConfirmButton {
  function mapButtonVariant (line 13) | function mapButtonVariant(
  type Props (line 25) | interface Props {

FILE: src/renderer/src/components/base/base-editor-lazy.tsx
  type Language (line 8) | type Language = 'yaml' | 'javascript' | 'css' | 'json' | 'text'
  type Props (line 10) | interface Props {

FILE: src/renderer/src/components/base/base-editor.tsx
  type Language (line 11) | type Language = 'yaml' | 'javascript' | 'css' | 'json' | 'text'
  type Props (line 13) | interface Props {

FILE: src/renderer/src/components/base/base-error-boundary.tsx
  type Props (line 66) | interface Props {

FILE: src/renderer/src/components/base/base-list-editor.tsx
  type EditableListProps (line 6) | interface EditableListProps {

FILE: src/renderer/src/components/base/base-page.tsx
  type Props (line 7) | interface Props {

FILE: src/renderer/src/components/base/base-qrcode-modal.tsx
  type Props (line 5) | interface Props {

FILE: src/renderer/src/components/base/base-setting-card.tsx
  type Props (line 4) | interface Props {

FILE: src/renderer/src/components/base/base-setting-item.tsx
  type Props (line 5) | interface Props {

FILE: src/renderer/src/components/base/border-swtich.tsx
  type SiderSwitchProps (line 5) | interface SiderSwitchProps extends SwitchProps {

FILE: src/renderer/src/components/base/collapse-input.tsx
  type CollapseInputProps (line 5) | interface CollapseInputProps extends InputProps {

FILE: src/renderer/src/components/base/mihomo-icon.tsx
  function MihomoIcon (line 3) | function MihomoIcon(props: IconBaseProps): JSX.Element {

FILE: src/renderer/src/components/base/substore-icon.tsx
  function SubStoreIcon (line 3) | function SubStoreIcon(props: IconBaseProps): JSX.Element {

FILE: src/renderer/src/components/connections/connection-detail-modal.tsx
  type Props (line 18) | interface Props {
  type CopyProps (line 23) | interface CopyProps {
  type StaticRow (line 30) | interface StaticRow {
  type CopyRow (line 36) | interface CopyRow extends CopyProps {
  function buildCopyMenuItems (line 40) | function buildCopyMenuItems(value: string | string[], displayName?: stri...

FILE: src/renderer/src/components/connections/connection-item.tsx
  type Props (line 8) | interface Props {

FILE: src/renderer/src/components/connections/connection-setting-modal.tsx
  type Props (line 8) | interface Props {

FILE: src/renderer/src/components/dns/advanced-dns-setting.tsx
  type AdvancedDnsSettingProps (line 8) | interface AdvancedDnsSettingProps {

FILE: src/renderer/src/components/logs/log-item.tsx
  type Props (line 12) | interface Props extends ControllerLog {

FILE: src/renderer/src/components/mihomo/interface-modal.tsx
  type Props (line 7) | interface Props {

FILE: src/renderer/src/components/mihomo/permission-modal.tsx
  type Props (line 13) | interface Props {

FILE: src/renderer/src/components/mihomo/service-modal.tsx
  type Props (line 6) | interface Props {
  type ServiceStatusType (line 15) | type ServiceStatusType = Awaited<ReturnType<typeof serviceStatus>>
  type ConnectionStatusType (line 16) | type ConnectionStatusType = 'connected' | 'disconnected' | 'checking' | ...
  function isUserCancelledError (line 18) | function isUserCancelledError(error: unknown): boolean {
  function delay (line 23) | function delay(ms: number): Promise<void> {
  function readServiceStatus (line 27) | async function readServiceStatus(): Promise<ServiceStatusType> {

FILE: src/renderer/src/components/override/edit-file-modal.tsx
  type Props (line 9) | interface Props {

FILE: src/renderer/src/components/override/edit-info-modal.tsx
  type Props (line 8) | interface Props {

FILE: src/renderer/src/components/override/exec-log-modal.tsx
  type Props (line 7) | interface Props {

FILE: src/renderer/src/components/override/override-item.tsx
  type Props (line 23) | interface Props {
  type MenuItem (line 31) | interface MenuItem {

FILE: src/renderer/src/components/profiles/edit-file-modal.tsx
  type Props (line 10) | interface Props {

FILE: src/renderer/src/components/profiles/edit-info-modal.tsx
  type Props (line 11) | interface Props {

FILE: src/renderer/src/components/profiles/profile-item.tsx
  type Props (line 27) | interface Props {
  type MenuItem (line 38) | interface MenuItem {

FILE: src/renderer/src/components/profiles/profile-setting-modal.tsx
  type Props (line 11) | interface Props {

FILE: src/renderer/src/components/proxies/proxy-item.tsx
  type Props (line 6) | interface Props {
  function delayColor (line 28) | function delayColor(delay: number): 'primary' | 'success' | 'warning' | ...
  function delayText (line 35) | function delayText(delay: number): string {

FILE: src/renderer/src/components/proxies/proxy-setting-modal.tsx
  type Props (line 14) | interface Props {

FILE: src/renderer/src/components/resources/viewer.tsx
  type Language (line 7) | type Language = 'yaml' | 'javascript' | 'css' | 'json' | 'text'
  constant FILE_PERMISSION_ELEVATION_REQUIRED (line 8) | const FILE_PERMISSION_ELEVATION_REQUIRED = 'FILE_PERMISSION_ELEVATION_RE...
  type Props (line 10) | interface Props {
  function getDefaultLanguage (line 19) | function getDefaultLanguage(format?: string): Language {
  function getViewerContent (line 23) | function getViewerContent(fileContent: string, privderType: string, titl...

FILE: src/renderer/src/components/rules/rule-item.tsx
  type Props (line 12) | interface Props {

FILE: src/renderer/src/components/settings/css-editor-modal.tsx
  type Props (line 6) | interface Props {

FILE: src/renderer/src/components/settings/webdav-restore-modal.tsx
  type Props (line 6) | interface Props {

FILE: src/renderer/src/components/sider/config-viewer.tsx
  type Props (line 13) | interface Props {
  type CompareTarget (line 17) | type CompareTarget = 'profile' | 'raw' | 'override'

FILE: src/renderer/src/components/sider/conn-card.tsx
  type Props (line 18) | interface Props {

FILE: src/renderer/src/components/sider/dns-card.tsx
  type Props (line 12) | interface Props {

FILE: src/renderer/src/components/sider/log-card.tsx
  type Props (line 9) | interface Props {

FILE: src/renderer/src/components/sider/mihomo-core-card.tsx
  type Props (line 14) | interface Props {

FILE: src/renderer/src/components/sider/outbound-mode-switcher.tsx
  type Props (line 8) | interface Props {

FILE: src/renderer/src/components/sider/override-card.tsx
  type Props (line 9) | interface Props {

FILE: src/renderer/src/components/sider/profile-card.tsx
  type Props (line 21) | interface Props {

FILE: src/renderer/src/components/sider/proxy-card.tsx
  type Props (line 10) | interface Props {

FILE: src/renderer/src/components/sider/resource-card.tsx
  type Props (line 9) | interface Props {

FILE: src/renderer/src/components/sider/rule-card.tsx
  type Props (line 10) | interface Props {

FILE: src/renderer/src/components/sider/sniff-card.tsx
  type Props (line 12) | interface Props {

FILE: src/renderer/src/components/sider/substore-card.tsx
  type Props (line 9) | interface Props {

FILE: src/renderer/src/components/sider/sysproxy-switcher.tsx
  type Props (line 12) | interface Props {

FILE: src/renderer/src/components/sider/traffic-chart.tsx
  type TrafficChartProps (line 4) | interface TrafficChartProps {

FILE: src/renderer/src/components/sider/tun-switcher.tsx
  type Props (line 12) | interface Props {

FILE: src/renderer/src/components/sysproxy/bypass-editor-modal.tsx
  type Props (line 8) | interface Props {
  type ParsedYaml (line 14) | interface ParsedYaml {

FILE: src/renderer/src/components/sysproxy/pac-editor-modal.tsx
  type Props (line 5) | interface Props {

FILE: src/renderer/src/components/updater/updater-button.tsx
  type Props (line 7) | interface Props {

FILE: src/renderer/src/components/updater/updater-modal.tsx
  type Props (line 7) | interface Props {

FILE: src/renderer/src/hooks/use-app-config.tsx
  type AppConfigContextType (line 5) | interface AppConfigContextType {

FILE: src/renderer/src/hooks/use-card-dnd-sensors.ts
  function shouldHandleEvent (line 13) | function shouldHandleEvent(event: Event): boolean {
  class CardMouseSensor (line 20) | class CardMouseSensor extends MouseSensor {
  class CardTouchSensor (line 33) | class CardTouchSensor extends TouchSensor {
  type CardDndSensorOptions (line 46) | interface CardDndSensorOptions {
  function useCardDndSensors (line 52) | function useCardDndSensors(options: CardDndSensorOptions = {}) {

FILE: src/renderer/src/hooks/use-controled-mihomo-config.tsx
  type ControledMihomoConfigContextType (line 5) | interface ControledMihomoConfigContextType {

FILE: src/renderer/src/hooks/use-groups.tsx
  type GroupsContextType (line 5) | interface GroupsContextType {

FILE: src/renderer/src/hooks/use-override-config.tsx
  type OverrideConfigContextType (line 11) | interface OverrideConfigContextType {

FILE: src/renderer/src/hooks/use-profile-config.tsx
  type ProfileConfigContextType (line 12) | interface ProfileConfigContextType {

FILE: src/renderer/src/hooks/use-rules.tsx
  type RulesContextType (line 5) | interface RulesContextType {

FILE: src/renderer/src/pages/logs.tsx
  function isSameLogEntry (line 30) | function isSameLogEntry(left: MihomoLogEntry, right: MihomoLogEntry): bo...
  function areSameLogEntries (line 40) | function areSameLogEntries(left: MihomoLogEntry[], right: MihomoLogEntry...
  function areSameIds (line 46) | function areSameIds(left: string[], right: string[]): boolean {

FILE: src/renderer/src/pages/proxies.tsx
  type ProxyLike (line 25) | type ProxyLike = ControllerProxiesDetail | ControllerGroupDetail
  constant EMPTY_PROXIES (line 27) | const EMPTY_PROXIES: ProxyLike[] = []
  function getProxyDelay (line 29) | function getProxyDelay(proxy: ProxyLike): number {
  function compareProxyDelay (line 33) | function compareProxyDelay(a: ProxyLike, b: ProxyLike): number {

FILE: src/renderer/src/utils/advanced-filter.ts
  type FilterExpression (line 1) | type FilterExpression =
  type FilterToken (line 12) | type FilterToken =
  type CompiledAdvancedFilter (line 37) | interface CompiledAdvancedFilter<T> {
  class FilterSyntaxError (line 43) | class FilterSyntaxError extends Error {
    method constructor (line 44) | constructor(
  class FilterParser (line 53) | class FilterParser {
    method constructor (line 56) | constructor(private readonly tokens: FilterToken[]) {}
    method parse (line 58) | parse(): FilterExpression {
    method parseOr (line 68) | private parseOr(): FilterExpression {
    method parseAnd (line 84) | private parseAnd(): FilterExpression {
    method parseComparison (line 100) | private parseComparison(): FilterExpression {
    method parseUnary (line 116) | private parseUnary(): FilterExpression {
    method parsePrimary (line 128) | private parsePrimary(): FilterExpression {
    method parsePath (line 160) | private parsePath(): FilterExpression {
    method matchComparisonOperator (line 201) | private matchComparisonOperator(): '==' | '!=' | '>=' | '<=' | '>' | '...
    method current (line 231) | private current(): FilterToken {
    method match (line 235) | private match(type: FilterToken['type']): boolean {
    method expect (line 241) | private expect<T extends FilterToken['type']>(
    method consume (line 254) | private consume<T extends FilterToken['type']>(type: T): Extract<Filte...
  function parseQuotedString (line 259) | function parseQuotedString(input: string, start: number): { value: strin...
  function tokenize (line 309) | function tokenize(input: string): FilterToken[] {
  function parseFilterExpression (line 450) | function parseFilterExpression(input: string): FilterExpression {
  function getPathValue (line 454) | function getPathValue(root: unknown, segments: ReadonlyArray<string | nu...
  function isTruthy (line 469) | function isTruthy(value: unknown): boolean {
  function toNumber (line 479) | function toNumber(value: unknown): number | null {
  function stringifyValue (line 487) | function stringifyValue(value: unknown): string {
  function deepEqual (line 498) | function deepEqual(left: unknown, right: unknown): boolean {
  function compareOrderedValues (line 531) | function compareOrderedValues(left: unknown, right: unknown): number {
  function fuzzyMatch (line 541) | function fuzzyMatch(left: unknown, right: unknown): boolean {
  type ValueFn (line 551) | type ValueFn = (root: unknown) => unknown
  type Predicate (line 552) | type Predicate = (root: unknown) => boolean
  function compileValue (line 554) | function compileValue(expression: FilterExpression): ValueFn {
  function compilePredicate (line 585) | function compilePredicate(expression: FilterExpression): Predicate {
  function looksLikeAdvancedFilter (line 712) | function looksLikeAdvancedFilter(input: string): boolean {
  function formatFilterError (line 720) | function formatFilterError(error: unknown): string {
  function compileAdvancedFilter (line 728) | function compileAdvancedFilter<T>(

FILE: src/renderer/src/utils/calc.ts
  function calcTraffic (line 1) | function calcTraffic(byte: number): string {
  function formatNumString (line 21) | function formatNumString(num: number): string {
  function calcPercent (line 33) | function calcPercent(

FILE: src/renderer/src/utils/connection-filter-autocomplete.ts
  type ConnectionFilterSuggestion (line 1) | interface ConnectionFilterSuggestion {
  type ConnectionFilterCompletionSession (line 10) | interface ConnectionFilterCompletionSession {
  function isPathChar (line 76) | function isPathChar(char: string | undefined): boolean {
  function getPathTokenAtCursor (line 82) | function getPathTokenAtCursor(input: string, cursor: number) {
  function rankByPrefix (line 103) | function rankByPrefix(value: string, prefix: string): number {
  function findPathSeparatorIndex (line 114) | function findPathSeparatorIndex(value: string): number {
  function getNextPathSuggestionTarget (line 124) | function getNextPathSuggestionTarget(prefix: string, fullPath: string): ...
  function withLeadingSpace (line 141) | function withLeadingSpace(input: string, index: number, text: string) {
  function endsWithValueOperand (line 148) | function endsWithValueOperand(input: string): boolean {
  function endsWithComparisonClause (line 154) | function endsWithComparisonClause(input: string): boolean {
  function getCurrentClause (line 160) | function getCurrentClause(input: string): string {
  function hasComparisonOperator (line 204) | function hasComparisonOperator(input: string): boolean {
  function normalizeClauseForCompletion (line 208) | function normalizeClauseForCompletion(input: string): string {
  function collectPathSuggestionTargets (line 216) | function collectPathSuggestionTargets(prefix: string) {
  function getPathSuggestions (line 257) | function getPathSuggestions(
  function getConnectionFilterSuggestions (line 274) | function getConnectionFilterSuggestions(
  function getEnhancedConnectionFilterSuggestions (line 387) | function getEnhancedConnectionFilterSuggestions(
  function buildConnectionFilterSuggestionResult (line 410) | function buildConnectionFilterSuggestionResult(
  function isConnectionFilterCompletionSessionActive (line 425) | function isConnectionFilterCompletionSessionActive(

FILE: src/renderer/src/utils/debounce.ts
  function debounce (line 2) | function debounce<T extends (...args: any[]) => void>(func: T, wait: num...

FILE: src/renderer/src/utils/delay-test.ts
  constant DEFAULT_DELAY_TEST_CONCURRENCY (line 1) | const DEFAULT_DELAY_TEST_CONCURRENCY = 50
  constant MIN_DELAY_TEST_CONCURRENCY (line 2) | const MIN_DELAY_TEST_CONCURRENCY = 1
  constant MAX_DELAY_TEST_CONCURRENCY (line 3) | const MAX_DELAY_TEST_CONCURRENCY = 512
  function normalizeDelayTestConcurrency (line 5) | function normalizeDelayTestConcurrency(value?: number): number {
  function runDelayTestsWithConcurrency (line 15) | async function runDelayTestsWithConcurrency<T>(

FILE: src/renderer/src/utils/driver.ts
  type Driver (line 3) | type Driver = {
  function loadDriverModule (line 12) | async function loadDriverModule(): Promise<typeof import('driver.js')> {
  function createDriver (line 20) | async function createDriver(navigate: NavigateFunction): Promise<Driver> {
  function startTour (line 215) | async function startTour(navigate: NavigateFunction): Promise<void> {
  function getDriver (line 220) | function getDriver(): Driver | null {

FILE: src/renderer/src/utils/hash.ts
  class HashType (line 3) | class HashType {
    method constructor (line 6) | constructor(hash: string) {
    method makeHash (line 10) | static makeHash(data: string): HashType {
    method equal (line 15) | equal(hash: HashType): boolean {
    method toString (line 19) | toString(): string {
    method isValid (line 23) | isValid(): boolean {
  function getHash (line 28) | function getHash(name: string): string {

FILE: src/renderer/src/utils/image.ts
  function cropAndPadTransparent (line 1) | async function cropAndPadTransparent(

FILE: src/renderer/src/utils/includes.ts
  function includesIgnoreCase (line 1) | function includesIgnoreCase(mainStr: string = '', subStr: string = ''): ...

FILE: src/renderer/src/utils/init.ts
  function init (line 20) | async function init(): Promise<void> {

FILE: src/renderer/src/utils/ipc.ts
  function ipcErrorWrapper (line 4) | function ipcErrorWrapper(response: any): any {
  function mihomoVersion (line 12) | async function mihomoVersion(): Promise<ControllerVersion> {
  function mihomoConfig (line 16) | async function mihomoConfig(): Promise<ControllerConfigs> {
  function mihomoCloseConnection (line 20) | async function mihomoCloseConnection(id: string): Promise<void> {
  function mihomoCloseConnections (line 24) | async function mihomoCloseConnections(name?: string): Promise<void> {
  function mihomoRules (line 28) | async function mihomoRules(): Promise<ControllerRules> {
  function mihomoProxies (line 32) | async function mihomoProxies(): Promise<ControllerProxies> {
  function mihomoGroups (line 36) | async function mihomoGroups(): Promise<ControllerMixedGroup[]> {
  function mihomoProxyProviders (line 40) | async function mihomoProxyProviders(): Promise<ControllerProxyProviders> {
  function mihomoUpdateProxyProviders (line 44) | async function mihomoUpdateProxyProviders(name: string): Promise<void> {
  function mihomoRuleProviders (line 50) | async function mihomoRuleProviders(): Promise<ControllerRuleProviders> {
  function mihomoUpdateRuleProviders (line 54) | async function mihomoUpdateRuleProviders(name: string): Promise<void> {
  function mihomoChangeProxy (line 60) | async function mihomoChangeProxy(
  function mihomoUnfixedProxy (line 69) | async function mihomoUnfixedProxy(group: string): Promise<ControllerProx...
  function mihomoUpgradeGeo (line 73) | async function mihomoUpgradeGeo(): Promise<void> {
  function mihomoUpgradeUI (line 77) | async function mihomoUpgradeUI(): Promise<void> {
  function mihomoUpgrade (line 81) | async function mihomoUpgrade(channel: string): Promise<void> {
  function mihomoProxyDelay (line 85) | async function mihomoProxyDelay(
  function mihomoGroupDelay (line 92) | async function mihomoGroupDelay(group: string, url?: string): Promise<Co...
  function mihomoRulesDisable (line 96) | async function mihomoRulesDisable(rules: Record<string, boolean>): Promi...
  function patchMihomoConfig (line 100) | async function patchMihomoConfig(patch: Partial<MihomoConfig>): Promise<...
  function restartMihomoLogs (line 104) | async function restartMihomoLogs(): Promise<void> {
  function checkAutoRun (line 108) | async function checkAutoRun(): Promise<boolean> {
  function enableAutoRun (line 112) | async function enableAutoRun(): Promise<void> {
  function disableAutoRun (line 116) | async function disableAutoRun(): Promise<void> {
  function getAppConfig (line 120) | async function getAppConfig(force = false): Promise<AppConfig> {
  function getCachedMihomoLogs (line 124) | async function getCachedMihomoLogs(): Promise<
  function clearCachedMihomoLogs (line 130) | async function clearCachedMihomoLogs(): Promise<void> {
  function patchAppConfig (line 134) | async function patchAppConfig(patch: Partial<AppConfig>): Promise<void> {
  function getControledMihomoConfig (line 138) | async function getControledMihomoConfig(force = false): Promise<Partial<...
  function patchControledMihomoConfig (line 144) | async function patchControledMihomoConfig(patch: Partial<MihomoConfig>):...
  function getProfileConfig (line 150) | async function getProfileConfig(force = false): Promise<ProfileConfig> {
  function setProfileConfig (line 154) | async function setProfileConfig(config: ProfileConfig): Promise<void> {
  function getCurrentProfileItem (line 158) | async function getCurrentProfileItem(): Promise<ProfileItem> {
  function getProfileItem (line 162) | async function getProfileItem(id: string | undefined): Promise<ProfileIt...
  function changeCurrentProfile (line 166) | async function changeCurrentProfile(id: string): Promise<void> {
  function addProfileItem (line 170) | async function addProfileItem(item: Partial<ProfileItem>): Promise<void> {
  function removeProfileItem (line 174) | async function removeProfileItem(id: string): Promise<void> {
  function updateProfileItem (line 178) | async function updateProfileItem(item: ProfileItem): Promise<void> {
  function getProfileStr (line 182) | async function getProfileStr(id: string): Promise<string> {
  function getFileStr (line 186) | async function getFileStr(id: string): Promise<string> {
  function setFileStr (line 190) | async function setFileStr(id: string, str: string): Promise<void> {
  function saveFileStrWithElevation (line 194) | async function saveFileStrWithElevation(id: string, str: string): Promis...
  function setProfileStr (line 200) | async function setProfileStr(id: string, str: string): Promise<void> {
  function getOverrideConfig (line 204) | async function getOverrideConfig(force = false): Promise<OverrideConfig> {
  function setOverrideConfig (line 208) | async function setOverrideConfig(config: OverrideConfig): Promise<void> {
  function getOverrideItem (line 212) | async function getOverrideItem(id: string): Promise<OverrideItem | undef...
  function addOverrideItem (line 216) | async function addOverrideItem(item: Partial<OverrideItem>): Promise<voi...
  function removeOverrideItem (line 220) | async function removeOverrideItem(id: string): Promise<void> {
  function updateOverrideItem (line 224) | async function updateOverrideItem(item: OverrideItem): Promise<void> {
  function getOverride (line 228) | async function getOverride(id: string, ext: 'js' | 'yaml' | 'log'): Prom...
  function setOverride (line 232) | async function setOverride(id: string, ext: 'js' | 'yaml', str: string):...
  function restartCore (line 236) | async function restartCore(): Promise<void> {
  function stopCore (line 240) | async function stopCore(): Promise<void> {
  function restartMihomoConnections (line 244) | async function restartMihomoConnections(): Promise<void> {
  function startMonitor (line 248) | async function startMonitor(): Promise<void> {
  function triggerSysProxy (line 252) | async function triggerSysProxy(
  function manualGrantCorePermition (line 267) | async function manualGrantCorePermition(
  function checkCorePermission (line 275) | async function checkCorePermission(): Promise<{ mihomo: boolean; 'mihomo...
  function checkElevateTask (line 279) | async function checkElevateTask(): Promise<boolean> {
  function deleteElevateTask (line 283) | async function deleteElevateTask(): Promise<void> {
  function revokeCorePermission (line 287) | async function revokeCorePermission(cores?: ('mihomo' | 'mihomo-alpha')[...
  function serviceStatus (line 291) | async function serviceStatus(): Promise<
  function testServiceConnection (line 297) | async function testServiceConnection(): Promise<boolean> {
  function initService (line 301) | async function initService(): Promise<void> {
  function installService (line 305) | async function installService(): Promise<void> {
  function uninstallService (line 309) | async function uninstallService(): Promise<void> {
  function startService (line 313) | async function startService(): Promise<void> {
  function restartService (line 317) | async function restartService(): Promise<void> {
  function stopService (line 321) | async function stopService(): Promise<void> {
  function findSystemMihomo (line 325) | async function findSystemMihomo(): Promise<string[]> {
  function getFilePath (line 329) | async function getFilePath(ext: string[]): Promise<string[] | undefined> {
  function readTextFile (line 333) | async function readTextFile(filePath: string): Promise<string> {
  function getRuntimeConfigStr (line 337) | async function getRuntimeConfigStr(): Promise<string> {
  function getRawProfileStr (line 341) | async function getRawProfileStr(): Promise<string> {
  function getCurrentProfileStr (line 345) | async function getCurrentProfileStr(): Promise<string> {
  function getOverrideProfileStr (line 349) | async function getOverrideProfileStr(): Promise<string> {
  function getRuntimeConfig (line 353) | async function getRuntimeConfig(): Promise<MihomoConfig> {
  function checkUpdate (line 357) | async function checkUpdate(): Promise<AppVersion | undefined> {
  function downloadAndInstallUpdate (line 361) | async function downloadAndInstallUpdate(version: string): Promise<void> {
  function cancelUpdate (line 367) | async function cancelUpdate(): Promise<void> {
  function getVersion (line 371) | async function getVersion(): Promise<string> {
  function getPlatform (line 375) | async function getPlatform(): Promise<NodeJS.Platform> {
  function openUWPTool (line 379) | async function openUWPTool(): Promise<void> {
  function setupFirewall (line 383) | async function setupFirewall(): Promise<void> {
  function getInterfaces (line 387) | async function getInterfaces(): Promise<Record<string, NetworkInterfaceI...
  function webdavBackup (line 391) | async function webdavBackup(): Promise<boolean> {
  function webdavRestore (line 395) | async function webdavRestore(filename: string): Promise<void> {
  function listWebdavBackups (line 399) | async function listWebdavBackups(): Promise<string[]> {
  function webdavDelete (line 403) | async function webdavDelete(filename: string): Promise<void> {
  function setTitleBarOverlay (line 407) | async function setTitleBarOverlay(overlay: TitleBarOverlayOptions): Prom...
  function setAlwaysOnTop (line 411) | async function setAlwaysOnTop(alwaysOnTop: boolean): Promise<void> {
  function isAlwaysOnTop (line 415) | async function isAlwaysOnTop(): Promise<boolean> {
  function relaunchApp (line 419) | async function relaunchApp(): Promise<void> {
  function quitWithoutCore (line 423) | async function quitWithoutCore(): Promise<void> {
  function quitApp (line 427) | async function quitApp(): Promise<void> {
  function notDialogQuit (line 431) | async function notDialogQuit(): Promise<void> {
  function setNativeTheme (line 435) | async function setNativeTheme(theme: 'system' | 'light' | 'dark'): Promi...
  function getGistUrl (line 439) | async function getGistUrl(): Promise<string> {
  function startSubStoreFrontendServer (line 443) | async function startSubStoreFrontendServer(): Promise<void> {
  function stopSubStoreFrontendServer (line 447) | async function stopSubStoreFrontendServer(): Promise<void> {
  function startSubStoreBackendServer (line 451) | async function startSubStoreBackendServer(): Promise<void> {
  function stopSubStoreBackendServer (line 455) | async function stopSubStoreBackendServer(): Promise<void> {
  function downloadSubStore (line 458) | async function downloadSubStore(): Promise<void> {
  function subStorePort (line 462) | async function subStorePort(): Promise<number> {
  function subStoreFrontendPort (line 466) | async function subStoreFrontendPort(): Promise<number> {
  function subStoreSubs (line 470) | async function subStoreSubs(): Promise<SubStoreSub[]> {
  function subStoreCollections (line 474) | async function subStoreCollections(): Promise<SubStoreSub[]> {
  function showTrayIcon (line 478) | async function showTrayIcon(): Promise<void> {
  function closeTrayIcon (line 482) | async function closeTrayIcon(): Promise<void> {
  function setDockVisible (line 486) | async function setDockVisible(visible: boolean): Promise<void> {
  function showMainWindow (line 490) | async function showMainWindow(): Promise<void> {
  function closeMainWindow (line 494) | async function closeMainWindow(): Promise<void> {
  function triggerMainWindow (line 498) | async function triggerMainWindow(): Promise<void> {
  function showFloatingWindow (line 502) | async function showFloatingWindow(): Promise<void> {
  function closeFloatingWindow (line 506) | async function closeFloatingWindow(): Promise<void> {
  function showContextMenu (line 510) | async function showContextMenu(): Promise<void> {
  function openFile (line 514) | async function openFile(
  function openDevTools (line 522) | async function openDevTools(): Promise<void> {
  function resetAppConfig (line 526) | async function resetAppConfig(): Promise<void> {
  function createHeapSnapshot (line 530) | async function createHeapSnapshot(): Promise<void> {
  function getUserAgent (line 534) | async function getUserAgent(): Promise<string> {
  function getAppName (line 538) | async function getAppName(appPath: string): Promise<string> {
  function getImageDataURL (line 542) | async function getImageDataURL(url: string): Promise<string> {
  function getIconDataURL (line 546) | async function getIconDataURL(appPath: string): Promise<string> {
  function resolveThemes (line 550) | async function resolveThemes(): Promise<{ key: string; label: string; co...
  function fetchThemes (line 554) | async function fetchThemes(): Promise<void> {
  function importThemes (line 558) | async function importThemes(files: string[]): Promise<void> {
  function readTheme (line 562) | async function readTheme(theme: string): Promise<string> {
  function writeTheme (line 566) | async function writeTheme(theme: string, css: string): Promise<void> {
  function startNetworkDetection (line 570) | async function startNetworkDetection(): Promise<void> {
  function stopNetworkDetection (line 574) | async function stopNetworkDetection(): Promise<void> {
  function applyTheme (line 580) | async function applyTheme(theme: string): Promise<void> {
  function registerShortcut (line 596) | async function registerShortcut(
  function copyEnv (line 606) | async function copyEnv(
  function alert (line 612) | async function alert<T>(msg: T): Promise<void> {

FILE: src/renderer/src/utils/mihomo-log-store.ts
  type MihomoLogEntry (line 3) | type MihomoLogEntry = ControllerLog & { id: string; seq?: number }
  type MihomoIncomingLog (line 5) | type MihomoIncomingLog = ControllerLog & { id?: string; seq?: number }
  type MihomoLogListener (line 6) | type MihomoLogListener = (logs: MihomoLogEntry[]) => void
  function trimLogs (line 18) | function trimLogs(nextLogs: MihomoLogEntry[]): MihomoLogEntry[] {
  function notify (line 23) | function notify(): void {
  function flushPendingLogs (line 28) | function flushPendingLogs(): void {
  function scheduleFlush (line 37) | function scheduleFlush(): void {
  function createLogId (line 44) | function createLogId(log: MihomoIncomingLog): string {
  function normalizeLog (line 52) | function normalizeLog(log: MihomoIncomingLog): MihomoLogEntry {
  function isSameLog (line 62) | function isSameLog(left: MihomoLogEntry, right: MihomoLogEntry): boolean {
  function mergeLogs (line 72) | function mergeLogs(current: MihomoLogEntry[], incoming: MihomoIncomingLo...
  function initLogStore (line 108) | function initLogStore(): void {
  function getMihomoLogs (line 127) | function getMihomoLogs(): MihomoLogEntry[] {
  function subscribeMihomoLogs (line 132) | function subscribeMihomoLogs(listener: MihomoLogListener): () => void {
  function clearMihomoLogs (line 142) | function clearMihomoLogs(): void {
  function setMihomoLogMaxEntries (line 153) | function setMihomoLogMaxEntries(value: number): void {

FILE: src/renderer/src/utils/validate.ts
  type ValidationResult (line 4) | type ValidationResult = { ok: boolean; error?: string }

FILE: src/shared/types/app.d.ts
  type AppVersion (line 1) | interface AppVersion {
  type ISysProxyConfig (line 6) | interface ISysProxyConfig {
  type IHost (line 17) | interface IHost {
  type AppConfig (line 22) | interface AppConfig {
  type ProfileConfig (line 129) | interface ProfileConfig {
  type ProfileItem (line 134) | interface ProfileItem {
  type SubscriptionUserInfo (line 154) | interface SubscriptionUserInfo {
  type OverrideConfig (line 161) | interface OverrideConfig {
  type OverrideItem (line 165) | interface OverrideItem {
  type SubStoreSub (line 177) | interface SubStoreSub {

FILE: src/shared/types/controller.d.ts
  type ControllerConfigs (line 2) | interface ControllerConfigs {
  type ControllerTunDetail (line 45) | interface ControllerTunDetail {
  type ControllerConnections (line 60) | interface ControllerConnections {
  type ControllerConnectionDetail (line 68) | interface ControllerConnectionDetail {
  type ControllerLog (line 109) | interface ControllerLog {
  type ControllerMemory (line 116) | interface ControllerMemory {
  type ControllerProxies (line 122) | interface ControllerProxies {
  type ControllerProxiesHistory (line 126) | interface ControllerProxiesHistory {
  type ControllerProxiesDetail (line 131) | interface ControllerProxiesDetail {
  type ControllerGroupDetail (line 149) | interface ControllerGroupDetail {
  type ControllerMixedGroup (line 172) | interface ControllerMixedGroup extends ControllerGroupDetail {
  type ControllerProxiesDelay (line 177) | interface ControllerProxiesDelay {
  type ControllerGroupDelay (line 183) | interface ControllerGroupDelay {
  type ControllerTraffic (line 187) | interface ControllerTraffic {
  type ControllerRules (line 193) | interface ControllerRules {
  type ControllerRulesDetail (line 197) | interface ControllerRulesDetail {
  type ControllerVersion (line 213) | interface ControllerVersion {
  type ControllerProxyProviders (line 219) | interface ControllerProxyProviders {
  type ControllerProxyProviderDetail (line 223) | interface ControllerProxyProviderDetail {
  type ControllerSubscriptionUserInfoUpper (line 234) | interface ControllerSubscriptionUserInfoUpper {
  type ControllerRuleProviders (line 242) | interface ControllerRuleProviders {
  type ControllerRuleProviderDetail (line 246) | interface ControllerRuleProviderDetail {

FILE: src/shared/types/mihomo.d.ts
  type MihomoConfig (line 1) | interface MihomoConfig {
  type MihomoTunConfig (line 55) | interface MihomoTunConfig {
  type MihomoDNSConfig (line 87) | interface MihomoDNSConfig {
  type MihomoSnifferConfig (line 113) | interface MihomoSnifferConfig {
  type MihomoProfileConfig (line 136) | interface MihomoProfileConfig {
  type ProxyProviderConfig (line 141) | interface ProxyProviderConfig {

FILE: src/shared/types/types.d.ts
  type OutboundMode (line 1) | type OutboundMode = 'rule' | 'global' | 'direct'
  type LogLevel (line 2) | type LogLevel = 'info' | 'debug' | 'warning' | 'error' | 'silent'
  type SysProxyMode (line 3) | type SysProxyMode = 'auto' | 'manual'
  type CardStatus (line 4) | type CardStatus = 'col-span-2' | 'col-span-1' | 'hidden'
  type AppTheme (line 5) | type AppTheme = 'system' | 'light' | 'dark'
  type Priority (line 6) | type Priority =
  type MihomoGroupType (line 14) | type MihomoGroupType = 'Selector' | 'Fallback' | 'URLTest' | 'LoadBalanc...
  type MihomoProxyType (line 15) | type MihomoProxyType =
  type TunStack (line 44) | type TunStack = 'gvisor' | 'mixed' | 'system'
  type FindProcessMode (line 45) | type FindProcessMode = 'off' | 'strict' | 'always'
  type DnsMode (line 46) | type DnsMode = 'normal' | 'fake-ip' | 'redir-host'
  type FilterMode (line 47) | type FilterMode = 'blacklist' | 'whitelist'
  type NetworkInterfaceInfo (line 48) | type NetworkInterfaceInfo = os.NetworkInterfaceInfo
  type Fingerprints (line 49) | type Fingerprints =
Condensed preview — 237 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (3,528K chars).
[
  {
    "path": ".editorconfig",
    "chars": 146,
    "preview": "root = true\n\n[*]\ncharset = utf-8\nindent_style = space\nindent_size = 2\nend_of_line = lf\ninsert_final_newline = true\ntrim_"
  },
  {
    "path": ".github/ISSUE_TEMPLATE/bug_report_zh.yml",
    "chars": 1613,
    "preview": "description: 提交 sparkle 漏洞\nname: 错误反馈\ntitle: '[Bug] '\nbody:\n  - attributes:\n      description: 在提交之前,请执行并勾选以下所有选项以证明您已经阅"
  },
  {
    "path": ".github/ISSUE_TEMPLATE/config.yml",
    "chars": 223,
    "preview": "---\nblank_issues_enabled: false\ncontact_links:\n  - about: 提出问题前请先查看常见问题\n    name: 常见问题\n    url: https://mihomo.party/doc"
  },
  {
    "path": ".github/ISSUE_TEMPLATE/feature_request_zh.yml",
    "chars": 770,
    "preview": "description: 请求 sparkle 未实现的功能\nname: 功能请求\ntitle: '[Feature] '\nbody:\n  - attributes:\n      description: 在提交之前,请执行并勾选以下所有选"
  },
  {
    "path": ".github/workflows/build.yml",
    "chars": 10210,
    "preview": "name: Build\npermissions: write-all\non:\n  workflow_dispatch:\n    inputs:\n      version:\n        description: 'Tag version"
  },
  {
    "path": ".github/workflows/issues.yml",
    "chars": 937,
    "preview": "name: Review Issues\n\non:\n  # issues:\n  #   types: [opened]\n  workflow_dispatch:\n  # disable issue event\n\njobs:\n  review:"
  },
  {
    "path": ".gitignore",
    "chars": 91,
    "preview": "node_modules\nresources/files\nresources/sidecar\nextra\ndist\nout\n.DS_Store\n*.log*\n.idea\n*.ttf\n"
  },
  {
    "path": ".npmrc",
    "chars": 88,
    "preview": "shamefully-hoist=true\nvirtual-store-dir-max-length=80\npublic-hoist-pattern[]=*@heroui/*\n"
  },
  {
    "path": ".prettierignore",
    "chars": 65,
    "preview": "out\ndist\npnpm-lock.yaml\nLICENSE.md\ntsconfig.json\ntsconfig.*.json\n"
  },
  {
    "path": ".prettierrc.yaml",
    "chars": 66,
    "preview": "singleQuote: true\nsemi: false\nprintWidth: 100\ntrailingComma: none\n"
  },
  {
    "path": ".vscode/extensions.json",
    "chars": 151,
    "preview": "{\n  \"recommendations\": [\n    \"dbaeumer.vscode-eslint\",\n    \"esbenp.prettier-vscode\",\n    \"huacnlee.autocorrect\",\n    \"br"
  },
  {
    "path": ".vscode/launch.json",
    "chars": 915,
    "preview": "{\n  \"version\": \"0.2.0\",\n  \"configurations\": [\n    {\n      \"name\": \"Debug Main Process\",\n      \"type\": \"node\",\n      \"req"
  },
  {
    "path": ".vscode/settings.json",
    "chars": 505,
    "preview": "{\n  \"[typescript]\": {\n    \"editor.defaultFormatter\": \"esbenp.prettier-vscode\"\n  },\n  \"[typescriptreact]\": {\n    \"editor."
  },
  {
    "path": "LICENSE",
    "chars": 35149,
    "preview": "                    GNU GENERAL PUBLIC LICENSE\n                       Version 3, 29 June 2007\n\n Copyright (C) 2007 Free "
  },
  {
    "path": "README.md",
    "chars": 4574,
    "preview": "# Sparkle\n\n<h3 align=\"center\">Another <a href=\"https://github.com/MetaCubeX/mihomo\">Mihomo</a> GUI</h3>\n\n<p align=\"cente"
  },
  {
    "path": "aur/sparkle/PKGBUILD",
    "chars": 1679,
    "preview": "pkgname=sparkle\npkgver=1.6.2\npkgrel=1\npkgdesc=\"Another Mihomo GUI.\"\narch=('x86_64' 'aarch64')\nurl=\"https://github.com/xi"
  },
  {
    "path": "aur/sparkle/sparkle.install",
    "chars": 381,
    "preview": "# Colored makepkg-like functions\nnote() {\n    printf \"${_blue}==>${_yellow} NOTE:${_bold} %s${_all_off}\\n\" \"$1\"\n}\n\n_all_"
  },
  {
    "path": "aur/sparkle/sparkle.sh",
    "chars": 390,
    "preview": "#!/usr/bin/bash\n\nXDG_CONFIG_HOME=${XDG_CONFIG_HOME:-~/.config}\n\n# Allow users to override command-line options\nif [[ -f "
  },
  {
    "path": "aur/sparkle-bin/PKGBUILD",
    "chars": 1640,
    "preview": "pkgname=sparkle-bin\n_pkgname=sparkle\npkgver=1.6.2\npkgrel=1\npkgdesc=\"Another Mihomo GUI.\"\narch=('x86_64' 'aarch64')\nurl=\""
  },
  {
    "path": "aur/sparkle-bin/sparkle.install",
    "chars": 381,
    "preview": "# Colored makepkg-like functions\nnote() {\n    printf \"${_blue}==>${_yellow} NOTE:${_bold} %s${_all_off}\\n\" \"$1\"\n}\n\n_all_"
  },
  {
    "path": "aur/sparkle-bin/sparkle.sh",
    "chars": 390,
    "preview": "#!/usr/bin/bash\n\nXDG_CONFIG_HOME=${XDG_CONFIG_HOME:-~/.config}\n\n# Allow users to override command-line options\nif [[ -f "
  },
  {
    "path": "aur/sparkle-electron/PKGBUILD",
    "chars": 2006,
    "preview": "pkgname=sparkle-electron\n_pkgname=sparkle\npkgver=1.6.2\npkgrel=1\npkgdesc=\"Another Mihomo GUI.\"\narch=('x86_64' 'aarch64')\n"
  },
  {
    "path": "aur/sparkle-electron/sparkle.desktop",
    "chars": 231,
    "preview": "[Desktop Entry]\nName=Sparkle\nExec=sparkle %U\nTerminal=false\nType=Application\nIcon=sparkle\nStartupWMClass=sparkle\nMimeTyp"
  },
  {
    "path": "aur/sparkle-electron/sparkle.install",
    "chars": 381,
    "preview": "# Colored makepkg-like functions\nnote() {\n    printf \"${_blue}==>${_yellow} NOTE:${_bold} %s${_all_off}\\n\" \"$1\"\n}\n\n_all_"
  },
  {
    "path": "aur/sparkle-electron/sparkle.sh",
    "chars": 391,
    "preview": "#!/usr/bin/bash\n\nXDG_CONFIG_HOME=${XDG_CONFIG_HOME:-~/.config}\n\n# Allow users to override command-line options\nif [[ -f "
  },
  {
    "path": "aur/sparkle-electron-bin/PKGBUILD",
    "chars": 2064,
    "preview": "pkgname=sparkle-electron-bin\n_pkgname=sparkle\npkgver=1.6.2\npkgrel=1\npkgdesc=\"Another Mihomo GUI.\"\narch=('x86_64' 'aarch6"
  },
  {
    "path": "aur/sparkle-electron-bin/sparkle.desktop",
    "chars": 231,
    "preview": "[Desktop Entry]\nName=Sparkle\nExec=sparkle %U\nTerminal=false\nType=Application\nIcon=sparkle\nStartupWMClass=sparkle\nMimeTyp"
  },
  {
    "path": "aur/sparkle-electron-bin/sparkle.install",
    "chars": 381,
    "preview": "# Colored makepkg-like functions\nnote() {\n    printf \"${_blue}==>${_yellow} NOTE:${_bold} %s${_all_off}\\n\" \"$1\"\n}\n\n_all_"
  },
  {
    "path": "aur/sparkle-electron-bin/sparkle.sh",
    "chars": 391,
    "preview": "#!/usr/bin/bash\n\nXDG_CONFIG_HOME=${XDG_CONFIG_HOME:-~/.config}\n\n# Allow users to override command-line options\nif [[ -f "
  },
  {
    "path": "aur/sparkle-electron-git/PKGBUILD",
    "chars": 2155,
    "preview": "pkgname=sparkle-electron-git\n_pkgname=${pkgname%-electron-git}\npkgver=r737.e4a7e67\npkgrel=1\npkgdesc=\"Another Mihomo GUI."
  },
  {
    "path": "aur/sparkle-electron-git/sparkle.desktop",
    "chars": 231,
    "preview": "[Desktop Entry]\nName=Sparkle\nExec=sparkle %U\nTerminal=false\nType=Application\nIcon=sparkle\nStartupWMClass=sparkle\nMimeTyp"
  },
  {
    "path": "aur/sparkle-electron-git/sparkle.install",
    "chars": 381,
    "preview": "# Colored makepkg-like functions\nnote() {\n    printf \"${_blue}==>${_yellow} NOTE:${_bold} %s${_all_off}\\n\" \"$1\"\n}\n\n_all_"
  },
  {
    "path": "aur/sparkle-electron-git/sparkle.sh",
    "chars": 391,
    "preview": "#!/usr/bin/bash\n\nXDG_CONFIG_HOME=${XDG_CONFIG_HOME:-~/.config}\n\n# Allow users to override command-line options\nif [[ -f "
  },
  {
    "path": "aur/sparkle-git/PKGBUILD",
    "chars": 1873,
    "preview": "pkgname=sparkle-git\n_pkgname=${pkgname%-git}\npkgver=1.6.2.r1.db8c6a0\npkgrel=1\npkgdesc=\"Another Mihomo GUI.\"\narch=('x86_6"
  },
  {
    "path": "aur/sparkle-git/sparkle.install",
    "chars": 381,
    "preview": "# Colored makepkg-like functions\nnote() {\n    printf \"${_blue}==>${_yellow} NOTE:${_bold} %s${_all_off}\\n\" \"$1\"\n}\n\n_all_"
  },
  {
    "path": "aur/sparkle-git/sparkle.sh",
    "chars": 390,
    "preview": "#!/usr/bin/bash\n\nXDG_CONFIG_HOME=${XDG_CONFIG_HOME:-~/.config}\n\n# Allow users to override command-line options\nif [[ -f "
  },
  {
    "path": "build/entitlements.mac.plist",
    "chars": 415,
    "preview": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<!DOCTYPE plist PUBLIC \"-//Apple//DTD PLIST 1.0//EN\" \"http://www.apple.com/DTDs/P"
  },
  {
    "path": "build/installer.nsh",
    "chars": 2482,
    "preview": "!ifndef BUILD_UNINSTALLER\n\n!macro customHeader\n  Var sparkleServiceWasRunning\n!macroend\n\n!macro ServiceOutputContains NE"
  },
  {
    "path": "build/linux/postinst",
    "chars": 1729,
    "preview": "#!/bin/bash\n\nif type update-alternatives 2>/dev/null >&1; then\n    # Remove previous link if it doesn't use update-alter"
  },
  {
    "path": "build/linux/preinst",
    "chars": 2037,
    "preview": "#!/bin/bash\n\nSERVICE_NAME='SparkleService'\nSTATE_DIR='/run/sparkle'\nSTATE_FILE=\"$STATE_DIR/service-was-running\"\nPID_FILE"
  },
  {
    "path": "build/pkg-scripts/postinstall",
    "chars": 847,
    "preview": "#!/bin/sh\n\nAPP_PATH=\"$2/Sparkle.app\"\n\nchown root:admin \"$APP_PATH/Contents/Resources/sidecar/mihomo\"\nchown root:admin \"$"
  },
  {
    "path": "build/pkg-scripts/preinstall",
    "chars": 2169,
    "preview": "#!/bin/sh\n\nOLD_SYSPROXY=\"/Library/PrivilegedHelperTools/sparkle.helper\"\nSERVICE_NAME=\"SparkleService\"\nSERVICE_STATE_FILE"
  },
  {
    "path": "changelog.md",
    "chars": 378,
    "preview": "### Breaking Changes\n\n- 1.5.0 之后 macOS 改用 pkg 安装方式,不再支持 dmg 安装方式,因此本次更新需要手动下载安装包进行安装\n- electron33 已不再支持 macOS 10.15,故为 1"
  },
  {
    "path": "electron-builder.yml",
    "chars": 2890,
    "preview": "appId: sparkle.app\nproductName: Sparkle\ndirectories:\n  buildResources: build\nfiles:\n  - '!**/.vscode/*'\n  - '!src/*'\n  -"
  },
  {
    "path": "electron.vite.config.ts",
    "chars": 1575,
    "preview": "import { resolve } from 'path'\nimport { defineConfig } from 'electron-vite'\nimport react from '@vitejs/plugin-react'\n// "
  },
  {
    "path": "eslint.config.cjs",
    "chars": 1117,
    "preview": "const js = require('@eslint/js')\nconst react = require('eslint-plugin-react')\nconst { configs } = require('@electron-too"
  },
  {
    "path": "package.json",
    "chars": 3862,
    "preview": "{\n  \"name\": \"sparkle\",\n  \"version\": \"1.26.4\",\n  \"description\": \"Sparkle\",\n  \"main\": \"./out/main/index.js\",\n  \"author\": {"
  },
  {
    "path": "patches/vite-plugin-monaco-editor@1.1.0.patch",
    "chars": 733,
    "preview": "diff --git a/dist/workerMiddleware.js b/dist/workerMiddleware.js\nindex dd8681c56cd5106dbc6dbc6c9cd03cbada397dfc..272f75a"
  },
  {
    "path": "scripts/checksum.ts",
    "chars": 426,
    "preview": "import { readFileSync, readdirSync, writeFileSync } from 'fs'\nimport { createHash } from 'crypto'\nconst files = readdirS"
  },
  {
    "path": "scripts/package.json",
    "chars": 23,
    "preview": "{\n  \"type\": \"module\"\n}\n"
  },
  {
    "path": "scripts/prepare.ts",
    "chars": 14778,
    "preview": "import fs from 'fs'\nimport AdmZip from 'adm-zip'\nimport path from 'path'\nimport zlib from 'zlib'\nimport { extract } from"
  },
  {
    "path": "scripts/telegram.ts",
    "chars": 2805,
    "preview": "import axios from 'axios'\nimport { readFileSync } from 'fs'\n\nconst chat_id = '@MihomoPartyChannel'\nconst pkg = readFileS"
  },
  {
    "path": "scripts/updater.ts",
    "chars": 1589,
    "preview": "import yaml from 'yaml'\nimport { readFileSync, writeFileSync } from 'fs'\n\nconst pkg = readFileSync('package.json', 'utf-"
  },
  {
    "path": "src/main/config/app.ts",
    "chars": 3529,
    "preview": "import { readFile, writeFile, rename, copyFile, unlink } from 'fs/promises'\nimport { appConfigPath } from '../utils/dirs"
  },
  {
    "path": "src/main/config/controledMihomo.ts",
    "chars": 2553,
    "preview": "import { controledMihomoConfigPath } from '../utils/dirs'\nimport { readFile, writeFile } from 'fs/promises'\nimport { par"
  },
  {
    "path": "src/main/config/index.ts",
    "chars": 679,
    "preview": "export { getAppConfig, patchAppConfig } from './app'\nexport { getControledMihomoConfig, patchControledMihomoConfig } fro"
  },
  {
    "path": "src/main/config/override.ts",
    "chars": 6161,
    "preview": "import { overrideConfigPath, overridePath } from '../utils/dirs'\nimport { getControledMihomoConfig } from './controledMi"
  },
  {
    "path": "src/main/config/profile.ts",
    "chars": 15405,
    "preview": "import { getControledMihomoConfig } from './controledMihomo'\nimport { mihomoProfileWorkDir, mihomoWorkDir, profileConfig"
  },
  {
    "path": "src/main/core/factory.ts",
    "chars": 12050,
    "preview": "import {\n  getControledMihomoConfig,\n  getProfileConfig,\n  getProfile,\n  getProfileStr,\n  getProfileItem,\n  getOverride,"
  },
  {
    "path": "src/main/core/manager.ts",
    "chars": 22879,
    "preview": "import { ChildProcess, spawn } from 'child_process'\nimport { dataDir, coreLogPath, mihomoCorePath } from '../utils/dirs'"
  },
  {
    "path": "src/main/core/mihomoApi.ts",
    "chars": 14716,
    "preview": "import axios, { AxiosInstance } from 'axios'\nimport { getAppConfig, getControledMihomoConfig } from '../config'\nimport {"
  },
  {
    "path": "src/main/core/network.ts",
    "chars": 5141,
    "preview": "import { execFile } from 'child_process'\nimport { net } from 'electron'\nimport os from 'os'\nimport { promisify } from 'u"
  },
  {
    "path": "src/main/core/permission-check.ts",
    "chars": 521,
    "preview": "import { execFileSync } from 'child_process'\n\nexport function hasSetuidPermission(permissions: string): boolean {\n  retu"
  },
  {
    "path": "src/main/core/permission.ts",
    "chars": 4002,
    "preview": "import { execFile } from 'child_process'\nimport { promisify } from 'util'\nimport { mihomoCorePath } from '../utils/dirs'"
  },
  {
    "path": "src/main/core/process-control.ts",
    "chars": 1650,
    "preview": "import type { ChildProcess } from 'child_process'\nimport { appendAppLog } from '../utils/log'\n\nexport async function sto"
  },
  {
    "path": "src/main/core/profile-check.ts",
    "chars": 1228,
    "preview": "import { execFile } from 'child_process'\nimport path from 'path'\nimport { promisify } from 'util'\nimport { getAppConfig,"
  },
  {
    "path": "src/main/core/profileUpdater.ts",
    "chars": 2732,
    "preview": "import { addProfileItem, getCurrentProfileItem, getProfileConfig } from '../config'\n\nconst intervalPool: Record<string, "
  },
  {
    "path": "src/main/core/startup-chain.ts",
    "chars": 3725,
    "preview": "import path from 'path'\nimport type { CoreStartupHook } from './startupHook'\nimport { mihomoIpcPath, mihomoProfileWorkDi"
  },
  {
    "path": "src/main/core/startupHook.ts",
    "chars": 3210,
    "preview": "import type { ChildProcess } from 'child_process'\nimport { existsSync, watch } from 'fs'\nimport type { FSWatcher } from "
  },
  {
    "path": "src/main/core/subStoreApi.ts",
    "chars": 856,
    "preview": "import axios from 'axios'\nimport { subStorePort } from '../resolve/server'\nimport { getAppConfig } from '../config'\n\nexp"
  },
  {
    "path": "src/main/index.ts",
    "chars": 17460,
    "preview": "import { electronApp, optimizer, is } from '@electron-toolkit/utils'\nimport { registerIpcMainHandlers } from './utils/ip"
  },
  {
    "path": "src/main/resolve/autoUpdater.ts",
    "chars": 7126,
    "preview": "import axios, { AxiosRequestConfig, CancelTokenSource } from 'axios'\nimport { parseYaml } from '../utils/yaml'\nimport { "
  },
  {
    "path": "src/main/resolve/backup.ts",
    "chars": 2799,
    "preview": "import { getAppConfig } from '../config'\nimport dayjs from 'dayjs'\nimport AdmZip from 'adm-zip'\nimport {\n  appConfigPath"
  },
  {
    "path": "src/main/resolve/floatingWindow.ts",
    "chars": 3473,
    "preview": "import { is } from '@electron-toolkit/utils'\nimport { BrowserWindow, ipcMain } from 'electron'\nimport windowStateKeeper "
  },
  {
    "path": "src/main/resolve/gistApi.ts",
    "chars": 3099,
    "preview": "import axios from 'axios'\nimport { getAppConfig, getControledMihomoConfig } from '../config'\nimport { getRuntimeConfigSt"
  },
  {
    "path": "src/main/resolve/menu.ts",
    "chars": 4749,
    "preview": "import { app, Menu, shell, dialog } from 'electron'\nimport { mainWindow } from '..'\nimport { getAppConfig } from '../con"
  },
  {
    "path": "src/main/resolve/server.ts",
    "chars": 6771,
    "preview": "import { getAppConfig, getControledMihomoConfig } from '../config'\nimport { Worker } from 'worker_threads'\nimport { miho"
  },
  {
    "path": "src/main/resolve/shortcut.ts",
    "chars": 5958,
    "preview": "import { app, globalShortcut, ipcMain, Notification } from 'electron'\nimport { mainWindow, setNotQuitDialog, triggerMain"
  },
  {
    "path": "src/main/resolve/theme.ts",
    "chars": 4103,
    "preview": "import { copyFile, readdir, readFile, writeFile } from 'fs/promises'\nimport { themesDir } from '../utils/dirs'\nimport pa"
  },
  {
    "path": "src/main/resolve/trafficMonitor.ts",
    "chars": 1275,
    "preview": "import { ChildProcess, spawn } from 'child_process'\nimport { getAppConfig } from '../config'\nimport { dataDir, resources"
  },
  {
    "path": "src/main/resolve/tray.ts",
    "chars": 17514,
    "preview": "import {\n  changeCurrentProfile,\n  getAppConfig,\n  getControledMihomoConfig,\n  getProfileConfig,\n  patchAppConfig,\n  pat"
  },
  {
    "path": "src/main/service/api.ts",
    "chars": 22201,
    "preview": "import axios, { AxiosInstance, AxiosRequestConfig, InternalAxiosRequestConfig } from 'axios'\nimport crypto from 'crypto'"
  },
  {
    "path": "src/main/service/auth-store.ts",
    "chars": 3688,
    "preview": "import { existsSync } from 'fs'\nimport { mkdir, readFile, rename, unlink, writeFile } from 'fs/promises'\nimport { dirnam"
  },
  {
    "path": "src/main/service/key.ts",
    "chars": 3254,
    "preview": "import crypto from 'crypto'\n\nexport interface KeyPair {\n  keyId: string\n  publicKey: string\n  privateKey: string\n}\n\nexpo"
  },
  {
    "path": "src/main/service/manager.ts",
    "chars": 10476,
    "preview": "import { servicePath } from '../utils/dirs'\nimport { execWithElevation } from '../utils/elevation'\nimport { KeyManager, "
  },
  {
    "path": "src/main/sys/autoRun.ts",
    "chars": 4504,
    "preview": "import { exePath, homeDir, taskDir } from '../utils/dirs'\nimport { execWithElevation } from '../utils/elevation'\nimport "
  },
  {
    "path": "src/main/sys/interface.ts",
    "chars": 126,
    "preview": "import os from 'os'\n\nexport function getInterfaces(): NodeJS.Dict<NetworkInterfaceInfo[]> {\n  return os.networkInterface"
  },
  {
    "path": "src/main/sys/misc.ts",
    "chars": 5542,
    "preview": "import { exec, execFile, execSync, spawn } from 'child_process'\nimport { app, dialog, nativeTheme, shell } from 'electro"
  },
  {
    "path": "src/main/sys/ssid.ts",
    "chars": 3353,
    "preview": "import { exec } from 'child_process'\nimport { promisify } from 'util'\nimport { getAppConfig, patchControledMihomoConfig "
  },
  {
    "path": "src/main/sys/sysproxy.ts",
    "chars": 6863,
    "preview": "import { getAppConfig, getControledMihomoConfig } from '../config'\nimport { pacPort, startPacServer, stopPacServer } fro"
  },
  {
    "path": "src/main/utils/appName.ts",
    "chars": 1800,
    "preview": "import fs from 'fs'\nimport path from 'path'\nimport plist from 'plist'\nimport { findBestAppPath, isIOSApp } from './icon'"
  },
  {
    "path": "src/main/utils/calc.ts",
    "chars": 887,
    "preview": "export function calcTraffic(byte: number): string {\n  if (byte < 1024) return `${byte} B`\n  byte /= 1024\n  if (byte < 10"
  },
  {
    "path": "src/main/utils/defaultIcon.ts",
    "chars": 2374176,
    "preview": "export const windowsDefaultIcon =\n  'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAQAAAAEACAYAAABccqhmAAAAAXNSR0IArs4c6"
  },
  {
    "path": "src/main/utils/devicePathResolver.ts",
    "chars": 1659,
    "preview": "import koffi from 'koffi'\nimport path from 'path'\n\ntype DosDeviceMapping = {\n  drive: string\n  devicePath: string\n}\n\ncon"
  },
  {
    "path": "src/main/utils/dirs.ts",
    "chars": 9512,
    "preview": "import { is } from '@electron-toolkit/utils'\nimport { existsSync, mkdirSync, readdirSync } from 'fs'\nimport { app } from"
  },
  {
    "path": "src/main/utils/elevation.ts",
    "chars": 2349,
    "preview": "import { execFile } from 'child_process'\nimport { promisify } from 'util'\n\nconst execFilePromise = promisify(execFile)\n\n"
  },
  {
    "path": "src/main/utils/encrypt.ts",
    "chars": 2235,
    "preview": "import { safeStorage } from 'electron'\n\nconst ENCRYPTED_PREFIX = 'enc:'\n\nexport function isSecureStorageAvailable(): boo"
  },
  {
    "path": "src/main/utils/icon.ts",
    "chars": 18334,
    "preview": "import axios from 'axios'\nimport { getControledMihomoConfig } from '../config'\nimport fs, { existsSync } from 'fs'\nimpor"
  },
  {
    "path": "src/main/utils/init.ts",
    "chars": 6922,
    "preview": "import {\n  appConfigPath,\n  controledMihomoConfigPath,\n  dataDir,\n  logDir,\n  mihomoTestDir,\n  mihomoWorkDir,\n  override"
  },
  {
    "path": "src/main/utils/ipc.ts",
    "chars": 16468,
    "preview": "import { app, dialog, ipcMain } from 'electron'\nimport {\n  mihomoChangeProxy,\n  mihomoCloseConnections,\n  mihomoCloseCon"
  },
  {
    "path": "src/main/utils/log.ts",
    "chars": 12355,
    "preview": "import { BrowserWindow } from 'electron'\nimport { createWriteStream, type WriteStream } from 'fs'\nimport { readFile, sta"
  },
  {
    "path": "src/main/utils/merge.ts",
    "chars": 1441,
    "preview": "// eslint-disable-next-line @typescript-eslint/no-explicit-any\nfunction isObject(item: any): boolean {\n  return item && "
  },
  {
    "path": "src/main/utils/template.ts",
    "chars": 4588,
    "preview": "import os from 'os'\n\nexport const defaultConfig: AppConfig = {\n  core: 'mihomo',\n  updateChannel: 'stable',\n  silentStar"
  },
  {
    "path": "src/main/utils/userAgent.ts",
    "chars": 675,
    "preview": "import { getAppConfig } from '../config'\nimport { mihomoVersion } from '../core/mihomoApi'\n\nconst TIMEOUT_MS = 300\nconst"
  },
  {
    "path": "src/main/utils/yaml.ts",
    "chars": 2430,
    "preview": "import yaml from 'yaml'\n\nexport function parseYaml<T = unknown>(content: string): T {\n  const processedContent = addYaml"
  },
  {
    "path": "src/preload/index.d.ts",
    "chars": 229,
    "preview": "import { ElectronAPI } from '@electron-toolkit/preload'\nimport { webUtils } from 'electron'\n\ndeclare global {\n  interfac"
  },
  {
    "path": "src/preload/index.ts",
    "chars": 672,
    "preview": "import { contextBridge, webUtils } from 'electron'\nimport { electronAPI } from '@electron-toolkit/preload'\n\n// Custom AP"
  },
  {
    "path": "src/renderer/floating.html",
    "chars": 505,
    "preview": "<!doctype html>\n<html>\n  <head>\n    <meta charset=\"UTF-8\" lang=\"zh\" />\n    <title>Sparkle Floating</title>\n    <!-- http"
  },
  {
    "path": "src/renderer/index.html",
    "chars": 492,
    "preview": "<!doctype html>\n<html>\n  <head>\n    <meta charset=\"UTF-8\" lang=\"zh\" />\n    <title>Sparkle</title>\n    <!-- https://devel"
  },
  {
    "path": "src/renderer/src/App.tsx",
    "chars": 17916,
    "preview": "import { useTheme } from 'next-themes'\nimport { useEffect, useRef, useState } from 'react'\nimport { NavigateFunction, us"
  },
  {
    "path": "src/renderer/src/FloatingApp.tsx",
    "chars": 3471,
    "preview": "import { useEffect, useMemo, useState } from 'react'\nimport MihomoIcon from './components/base/mihomo-icon'\nimport { cal"
  },
  {
    "path": "src/renderer/src/TrayMenuApp.tsx",
    "chars": 8562,
    "preview": "import { useEffect, useState, useMemo } from 'react'\nimport { Button, ScrollShadow, Chip, Accordion, AccordionItem } fro"
  },
  {
    "path": "src/renderer/src/assets/floating.css",
    "chars": 505,
    "preview": "@import 'tailwindcss';\n@plugin './hero.mjs';\n@import '@heroui-v3/styles';\n@source '../../../../node_modules/@heroui/them"
  },
  {
    "path": "src/renderer/src/assets/hero.mjs",
    "chars": 63,
    "preview": "import { heroui } from '@heroui/react'\nexport default heroui()\n"
  },
  {
    "path": "src/renderer/src/assets/main.css",
    "chars": 3102,
    "preview": "@import 'tailwindcss';\n@plugin './hero.mjs';\n@import '@heroui-v3/styles';\n@source '../../../../node_modules/@heroui/them"
  },
  {
    "path": "src/renderer/src/assets/traymenu.css",
    "chars": 725,
    "preview": "@import 'tailwindcss';\n@plugin './hero.mjs';\n@import '@heroui-v3/styles';\n@source '../../../../node_modules/@heroui/them"
  },
  {
    "path": "src/renderer/src/components/base/base-confirm.tsx",
    "chars": 2937,
    "preview": "import React from 'react'\nimport { Button, Modal } from '@heroui-v3/react'\nimport { useAppConfig } from '@renderer/hooks"
  },
  {
    "path": "src/renderer/src/components/base/base-editor-lazy.tsx",
    "chars": 740,
    "preview": "import React, { Suspense } from 'react'\nimport { Spinner } from '@heroui/react'\n\nconst BaseEditorComponent = React.lazy("
  },
  {
    "path": "src/renderer/src/components/base/base-editor.tsx",
    "chars": 7272,
    "preview": "import { useRef } from 'react'\nimport * as monaco from 'monaco-editor'\nimport MonacoEditor, { MonacoDiffEditor } from 'r"
  },
  {
    "path": "src/renderer/src/components/base/base-error-boundary.tsx",
    "chars": 1780,
    "preview": "import { Button } from '@heroui/react'\nimport { JSX, ReactNode } from 'react'\nimport { ErrorBoundary, FallbackProps } fr"
  },
  {
    "path": "src/renderer/src/components/base/base-list-editor.tsx",
    "chars": 8804,
    "preview": "import React from 'react'\nimport { Button, Divider, Input, Tooltip } from '@heroui/react'\nimport { MdDeleteForever } fro"
  },
  {
    "path": "src/renderer/src/components/base/base-page.tsx",
    "chars": 2931,
    "preview": "import { Button, Divider } from '@heroui/react'\nimport { useAppConfig } from '@renderer/hooks/use-app-config'\nimport { p"
  },
  {
    "path": "src/renderer/src/components/base/base-qrcode-modal.tsx",
    "chars": 1220,
    "preview": "import React from 'react'\nimport { Modal } from '@heroui-v3/react'\nimport { QRCodeSVG } from 'qrcode.react'\n\ninterface P"
  },
  {
    "path": "src/renderer/src/components/base/base-setting-card.tsx",
    "chars": 1292,
    "preview": "import React from 'react'\nimport { Accordion, AccordionItem, Card, CardBody } from '@heroui/react'\n\ninterface Props {\n  "
  },
  {
    "path": "src/renderer/src/components/base/base-setting-item.tsx",
    "chars": 1309,
    "preview": "import { cn, Divider } from '@heroui/react'\n\nimport React from 'react'\n\ninterface Props {\n  title: React.ReactNode\n  act"
  },
  {
    "path": "src/renderer/src/components/base/border-switch.css",
    "chars": 67,
    "preview": ".border-switch {\n  input[type='checkbox'] {\n    width: 100%;\n  }\n}\n"
  },
  {
    "path": "src/renderer/src/components/base/border-swtich.tsx",
    "chars": 725,
    "preview": "import React from 'react'\nimport { cn, Switch, SwitchProps } from '@heroui/react'\nimport './border-switch.css'\n\ninterfac"
  },
  {
    "path": "src/renderer/src/components/base/collapse-input.tsx",
    "chars": 1316,
    "preview": "import React, { useRef } from 'react'\nimport { Input, InputProps } from '@heroui/react'\nimport { FaSearch } from 'react-"
  },
  {
    "path": "src/renderer/src/components/base/interface-select.tsx",
    "chars": 1013,
    "preview": "import React, { useEffect, useState } from 'react'\nimport { Select, SelectItem } from '@heroui/react'\nimport { getInterf"
  },
  {
    "path": "src/renderer/src/components/base/mihomo-icon.tsx",
    "chars": 2339,
    "preview": "import { JSX } from 'react'\nimport { GenIcon, IconBaseProps } from 'react-icons'\nfunction MihomoIcon(props: IconBaseProp"
  },
  {
    "path": "src/renderer/src/components/base/substore-icon.tsx",
    "chars": 4175,
    "preview": "import { JSX } from 'react'\nimport { GenIcon, IconBaseProps } from 'react-icons'\nfunction SubStoreIcon(props: IconBasePr"
  },
  {
    "path": "src/renderer/src/components/connections/connection-detail-modal.tsx",
    "chars": 15537,
    "preview": "import {\n  Button,\n  Description,\n  Dropdown,\n  Label,\n  Modal,\n  Separator,\n  Surface,\n  Tabs\n} from '@heroui-v3/react'"
  },
  {
    "path": "src/renderer/src/components/connections/connection-item.tsx",
    "chars": 5799,
    "preview": "import { Button, Card, CardFooter, CardHeader, Chip } from '@heroui/react'\nimport { Avatar } from '@heroui-v3/react'\nimp"
  },
  {
    "path": "src/renderer/src/components/connections/connection-setting-modal.tsx",
    "chars": 3188,
    "preview": "import { Button, Switch, Input } from '@heroui/react'\nimport { Modal } from '@heroui-v3/react'\nimport React, { useState "
  },
  {
    "path": "src/renderer/src/components/dns/advanced-dns-setting.tsx",
    "chars": 9303,
    "preview": "import React, { useState } from 'react'\nimport SettingCard from '../base/base-setting-card'\nimport SettingItem from '../"
  },
  {
    "path": "src/renderer/src/components/logs/log-item.tsx",
    "chars": 1579,
    "preview": "import { Card, CardBody, CardHeader } from '@heroui/react'\nimport React, { useEffect, useState } from 'react'\n\nconst col"
  },
  {
    "path": "src/renderer/src/components/mihomo/advanced-settings.tsx",
    "chars": 5658,
    "preview": "import { useControledMihomoConfig } from '@renderer/hooks/use-controled-mihomo-config'\nimport SettingCard from '../base/"
  },
  {
    "path": "src/renderer/src/components/mihomo/controller-setting.tsx",
    "chars": 11987,
    "preview": "import React, { useState } from 'react'\nimport SettingCard from '../base/base-setting-card'\nimport SettingItem from '../"
  },
  {
    "path": "src/renderer/src/components/mihomo/env-setting.tsx",
    "chars": 2812,
    "preview": "import React, { useState } from 'react'\nimport SettingCard from '../base/base-setting-card'\nimport SettingItem from '../"
  },
  {
    "path": "src/renderer/src/components/mihomo/interface-modal.tsx",
    "chars": 1971,
    "preview": "import { Snippet } from '@heroui/react'\nimport { Modal } from '@heroui-v3/react'\nimport React, { useEffect, useState } f"
  },
  {
    "path": "src/renderer/src/components/mihomo/log-setting.tsx",
    "chars": 5888,
    "preview": "import { useEffect, useState } from 'react'\nimport { Button, Input, Select, SelectItem, Switch, Tooltip } from '@heroui/"
  },
  {
    "path": "src/renderer/src/components/mihomo/permission-modal.tsx",
    "chars": 11976,
    "preview": "import React, { useEffect, useState } from 'react'\nimport { Button, Card, CardBody, CardHeader, Chip, Divider } from '@h"
  },
  {
    "path": "src/renderer/src/components/mihomo/port-setting.tsx",
    "chars": 10909,
    "preview": "import React, { useState } from 'react'\nimport SettingCard from '../base/base-setting-card'\nimport SettingItem from '../"
  },
  {
    "path": "src/renderer/src/components/mihomo/service-modal.tsx",
    "chars": 9326,
    "preview": "import React, { useEffect, useState, useCallback } from 'react'\nimport { Button, Spinner, Card, CardBody, Chip, Divider "
  },
  {
    "path": "src/renderer/src/components/override/edit-file-modal.tsx",
    "chars": 3605,
    "preview": "import { Button, Switch } from '@heroui/react'\nimport { Modal } from '@heroui-v3/react'\nimport React, { useEffect, useSt"
  },
  {
    "path": "src/renderer/src/components/override/edit-info-modal.tsx",
    "chars": 5417,
    "preview": "import { cn, Button, Input, Switch, Select, SelectItem } from '@heroui/react'\nimport { Label, Modal, Separator, Surface "
  },
  {
    "path": "src/renderer/src/components/override/exec-log-modal.tsx",
    "chars": 1525,
    "preview": "import { Divider } from '@heroui/react'\nimport { Modal } from '@heroui-v3/react'\nimport React, { useEffect, useState } f"
  },
  {
    "path": "src/renderer/src/components/override/override-item.tsx",
    "chars": 8589,
    "preview": "import {\n  Button,\n  Card,\n  CardBody,\n  Chip,\n  Dropdown,\n  DropdownItem,\n  DropdownMenu,\n  DropdownTrigger\n} from '@he"
  },
  {
    "path": "src/renderer/src/components/profiles/edit-file-modal.tsx",
    "chars": 4061,
    "preview": "import { Button, Switch } from '@heroui/react'\nimport { Modal } from '@heroui-v3/react'\nimport React, { useEffect, useSt"
  },
  {
    "path": "src/renderer/src/components/profiles/edit-info-modal.tsx",
    "chars": 11221,
    "preview": "import { cn, Button, Input, Switch, Tooltip } from '@heroui/react'\nimport { Dropdown, Label, Modal, Separator, Surface }"
  },
  {
    "path": "src/renderer/src/components/profiles/profile-item.tsx",
    "chars": 11583,
    "preview": "import {\n  Button,\n  Card,\n  CardBody,\n  CardFooter,\n  Chip,\n  Dropdown,\n  DropdownItem,\n  DropdownMenu,\n  DropdownTrigg"
  },
  {
    "path": "src/renderer/src/components/profiles/profile-setting-modal.tsx",
    "chars": 4880,
    "preview": "import { Button, Switch, Input, Tab, Tabs, Tooltip } from '@heroui/react'\nimport { Modal } from '@heroui-v3/react'\nimpor"
  },
  {
    "path": "src/renderer/src/components/proxies/proxy-item.tsx",
    "chars": 5316,
    "preview": "import { Button, Card, CardBody } from '@heroui/react'\nimport { mihomoUnfixedProxy } from '@renderer/utils/ipc'\nimport R"
  },
  {
    "path": "src/renderer/src/components/proxies/proxy-setting-modal.tsx",
    "chars": 8439,
    "preview": "import { Switch, Input, Select, SelectItem, Tab, Tabs } from '@heroui/react'\nimport { Modal } from '@heroui-v3/react'\nim"
  },
  {
    "path": "src/renderer/src/components/resources/geo-data.tsx",
    "chars": 5829,
    "preview": "import { Button, Input, Switch, Tab, Tabs } from '@heroui/react'\nimport SettingCard from '@renderer/components/base/base"
  },
  {
    "path": "src/renderer/src/components/resources/proxy-provider.tsx",
    "chars": 7381,
    "preview": "import {\n  mihomoProxyProviders,\n  mihomoUpdateProxyProviders,\n  getRuntimeConfig\n} from '@renderer/utils/ipc'\nimport { "
  },
  {
    "path": "src/renderer/src/components/resources/rule-provider.tsx",
    "chars": 5740,
    "preview": "import {\n  mihomoRuleProviders,\n  mihomoUpdateRuleProviders,\n  getRuntimeConfig\n} from '@renderer/utils/ipc'\nimport { ge"
  },
  {
    "path": "src/renderer/src/components/resources/viewer.tsx",
    "chars": 4219,
    "preview": "import { Button, Modal } from '@heroui-v3/react'\nimport React, { useEffect, useState } from 'react'\nimport { BaseEditor "
  },
  {
    "path": "src/renderer/src/components/rules/rule-item.tsx",
    "chars": 2577,
    "preview": "import { Card, CardBody, Switch, Chip } from '@heroui/react'\nimport React, { useEffect, useState } from 'react'\nimport {"
  },
  {
    "path": "src/renderer/src/components/settings/actions.tsx",
    "chars": 5541,
    "preview": "import { Button, Tooltip } from '@heroui/react'\nimport SettingCard from '../base/base-setting-card'\nimport SettingItem f"
  },
  {
    "path": "src/renderer/src/components/settings/advanced-settings.tsx",
    "chars": 8358,
    "preview": "import React, { useState, useEffect } from 'react'\nimport SettingCard from '../base/base-setting-card'\nimport SettingIte"
  },
  {
    "path": "src/renderer/src/components/settings/appearance-confis.tsx",
    "chars": 9722,
    "preview": "import React, { useEffect, useState, useRef } from 'react'\nimport SettingCard from '../base/base-setting-card'\nimport Se"
  },
  {
    "path": "src/renderer/src/components/settings/css-editor-modal.tsx",
    "chars": 1790,
    "preview": "import { Button, Modal } from '@heroui-v3/react'\nimport { BaseEditor } from '@renderer/components/base/base-editor-lazy'"
  },
  {
    "path": "src/renderer/src/components/settings/general-config.tsx",
    "chars": 4487,
    "preview": "import React, { useState } from 'react'\nimport SettingCard from '../base/base-setting-card'\nimport SettingItem from '../"
  },
  {
    "path": "src/renderer/src/components/settings/shortcut-config.tsx",
    "chars": 7066,
    "preview": "import { Button, Input } from '@heroui/react'\nimport SettingCard from '../base/base-setting-card'\nimport SettingItem fro"
  },
  {
    "path": "src/renderer/src/components/settings/sider-config.tsx",
    "chars": 2342,
    "preview": "import React from 'react'\nimport SettingCard from '../base/base-setting-card'\nimport SettingItem from '../base/base-sett"
  },
  {
    "path": "src/renderer/src/components/settings/substore-config.tsx",
    "chars": 9222,
    "preview": "import React, { useState, useEffect } from 'react'\nimport SettingCard from '@renderer/components/base/base-setting-card'"
  },
  {
    "path": "src/renderer/src/components/settings/webdav-config.tsx",
    "chars": 3896,
    "preview": "import React, { useState } from 'react'\nimport SettingCard from '../base/base-setting-card'\nimport SettingItem from '../"
  },
  {
    "path": "src/renderer/src/components/settings/webdav-restore-modal.tsx",
    "chars": 2783,
    "preview": "import { Button, Modal } from '@heroui-v3/react'\nimport { useAppConfig } from '@renderer/hooks/use-app-config'\nimport { "
  },
  {
    "path": "src/renderer/src/components/sider/config-viewer.tsx",
    "chars": 6133,
    "preview": "import { Label, Modal, Separator, Switch } from '@heroui-v3/react'\nimport React, { useEffect, useState, useCallback, use"
  },
  {
    "path": "src/renderer/src/components/sider/conn-card.tsx",
    "chars": 8606,
    "preview": "import { Button, Card, CardBody, CardFooter, Tooltip } from '@heroui/react'\nimport { FaCircleArrowDown, FaCircleArrowUp "
  },
  {
    "path": "src/renderer/src/components/sider/dns-card.tsx",
    "chars": 3601,
    "preview": "import { Button, Card, CardBody, CardFooter, Tooltip } from '@heroui/react'\nimport { useControledMihomoConfig } from '@r"
  },
  {
    "path": "src/renderer/src/components/sider/log-card.tsx",
    "chars": 2801,
    "preview": "import { Button, Card, CardBody, CardFooter, Tooltip } from '@heroui/react'\nimport { IoJournalOutline } from 'react-icon"
  },
  {
    "path": "src/renderer/src/components/sider/mihomo-core-card.tsx",
    "chars": 5822,
    "preview": "import { Button, Card, CardBody, CardFooter, Tooltip } from '@heroui/react'\nimport { calcTraffic } from '@renderer/utils"
  },
  {
    "path": "src/renderer/src/components/sider/outbound-mode-switcher.tsx",
    "chars": 2253,
    "preview": "import { Tabs, Tab } from '@heroui/react'\nimport { useAppConfig } from '@renderer/hooks/use-app-config'\nimport { useCont"
  },
  {
    "path": "src/renderer/src/components/sider/override-card.tsx",
    "chars": 2832,
    "preview": "import { Button, Card, CardBody, CardFooter, Tooltip } from '@heroui/react'\nimport React from 'react'\nimport { MdFormatO"
  },
  {
    "path": "src/renderer/src/components/sider/profile-card.tsx",
    "chars": 9884,
    "preview": "import { Button, Card, CardBody, CardFooter, Chip, Tooltip } from '@heroui/react'\nimport { Meter } from '@heroui-v3/reac"
  },
  {
    "path": "src/renderer/src/components/sider/proxy-card.tsx",
    "chars": 3396,
    "preview": "import { Button, Card, CardBody, CardFooter, Chip, Tooltip } from '@heroui/react'\nimport { useSortable } from '@dnd-kit/"
  },
  {
    "path": "src/renderer/src/components/sider/resource-card.tsx",
    "chars": 2847,
    "preview": "import { Button, Card, CardBody, CardFooter, Tooltip } from '@heroui/react'\nimport React from 'react'\nimport { useLocati"
  },
  {
    "path": "src/renderer/src/components/sider/rule-card.tsx",
    "chars": 3438,
    "preview": "import { Button, Card, CardBody, CardFooter, Chip, Tooltip } from '@heroui/react'\nimport { MdOutlineAltRoute } from 'rea"
  },
  {
    "path": "src/renderer/src/components/sider/sniff-card.tsx",
    "chars": 3627,
    "preview": "import { Button, Card, CardBody, CardFooter, Tooltip } from '@heroui/react'\nimport BorderSwitch from '@renderer/componen"
  },
  {
    "path": "src/renderer/src/components/sider/substore-card.tsx",
    "chars": 2915,
    "preview": "import { Button, Card, CardBody, CardFooter, Tooltip } from '@heroui/react'\nimport { useLocation, useNavigate } from 're"
  },
  {
    "path": "src/renderer/src/components/sider/sysproxy-switcher.tsx",
    "chars": 3941,
    "preview": "import { Button, Card, CardBody, CardFooter, Tooltip } from '@heroui/react'\nimport BorderSwitch from '@renderer/componen"
  },
  {
    "path": "src/renderer/src/components/sider/traffic-chart.tsx",
    "chars": 1784,
    "preview": "import React, { useMemo } from 'react'\nimport { Area, AreaChart, ResponsiveContainer } from 'recharts'\n\nexport interface"
  },
  {
    "path": "src/renderer/src/components/sider/tun-switcher.tsx",
    "chars": 3736,
    "preview": "import { Button, Card, CardBody, CardFooter, Tooltip } from '@heroui/react'\nimport { useControledMihomoConfig } from '@r"
  },
  {
    "path": "src/renderer/src/components/sysproxy/bypass-editor-modal.tsx",
    "chars": 2087,
    "preview": "/* eslint-disable react/prop-types */\nimport { useEffect, useState } from 'react'\nimport yaml from 'js-yaml'\nimport { Bu"
  },
  {
    "path": "src/renderer/src/components/sysproxy/pac-editor-modal.tsx",
    "chars": 1618,
    "preview": "import { Button, Modal } from '@heroui-v3/react'\nimport { BaseEditor } from '@renderer/components/base/base-editor-lazy'"
  },
  {
    "path": "src/renderer/src/components/updater/updater-button.tsx",
    "chars": 2118,
    "preview": "import { Button } from '@heroui/react'\nimport React, { useState, useEffect } from 'react'\nimport UpdaterModal from './up"
  },
  {
    "path": "src/renderer/src/components/updater/updater-modal.tsx",
    "chars": 4954,
    "preview": "import { Button, Label, Link, Modal, ProgressBar } from '@heroui-v3/react'\nimport ReactMarkdown from 'react-markdown'\nim"
  },
  {
    "path": "src/renderer/src/floating.tsx",
    "chars": 987,
    "preview": "import React from 'react'\nimport ReactDOM from 'react-dom/client'\nimport { ThemeProvider as NextThemesProvider } from 'n"
  },
  {
    "path": "src/renderer/src/hooks/use-app-config.tsx",
    "chars": 1428,
    "preview": "import React, { createContext, useContext, ReactNode } from 'react'\nimport useSWR from 'swr'\nimport { getAppConfig, patc"
  },
  {
    "path": "src/renderer/src/hooks/use-card-dnd-sensors.ts",
    "chars": 1668,
    "preview": "import {\n  MouseSensor,\n  TouchSensor,\n  useSensor,\n  useSensors,\n  type MouseSensorOptions,\n  type TouchSensorOptions\n}"
  },
  {
    "path": "src/renderer/src/hooks/use-controled-mihomo-config.tsx",
    "chars": 1811,
    "preview": "import React, { createContext, useContext, ReactNode } from 'react'\nimport useSWR from 'swr'\nimport { getControledMihomo"
  },
  {
    "path": "src/renderer/src/hooks/use-groups.tsx",
    "chars": 1254,
    "preview": "import React, { createContext, useContext, ReactNode } from 'react'\nimport useSWR from 'swr'\nimport { mihomoGroups } fro"
  },
  {
    "path": "src/renderer/src/hooks/use-override-config.tsx",
    "chars": 2558,
    "preview": "import React, { createContext, useContext, ReactNode, useEffect } from 'react'\nimport useSWR from 'swr'\nimport {\n  getOv"
  },
  {
    "path": "src/renderer/src/hooks/use-profile-config.tsx",
    "chars": 3101,
    "preview": "import React, { createContext, useContext, ReactNode, useEffect } from 'react'\nimport useSWR from 'swr'\nimport {\n  getPr"
  },
  {
    "path": "src/renderer/src/hooks/use-rules.tsx",
    "chars": 1221,
    "preview": "import React, { createContext, useContext, ReactNode } from 'react'\nimport useSWR from 'swr'\nimport { mihomoRules } from"
  },
  {
    "path": "src/renderer/src/main.tsx",
    "chars": 2285,
    "preview": "import React from 'react'\nimport ReactDOM from 'react-dom/client'\nimport { HashRouter } from 'react-router-dom'\nimport {"
  },
  {
    "path": "src/renderer/src/pages/connections.tsx",
    "chars": 31043,
    "preview": "import BasePage from '@renderer/components/base/base-page'\nimport { mihomoCloseConnections, mihomoCloseConnection } from"
  },
  {
    "path": "src/renderer/src/pages/dns.tsx",
    "chars": 12306,
    "preview": "import { Button, Tab, Input, Switch, Tabs, Tooltip } from '@heroui/react'\nimport BasePage from '@renderer/components/bas"
  }
]

// ... and 37 more files (download for full content)

About this extraction

This page contains the full source code of the xishang0128/clash-meta-party GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 237 files (3.3 MB), approximately 875.6k tokens, and a symbol index with 878 extracted functions, classes, methods, constants, and types. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.

Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.

Copied to clipboard!