[
  {
    "path": ".github/ISSUE_TEMPLATE/1_BUG_REPORT.md",
    "content": "---\nname: 问题上报\nabout: 如果你在使用过程中发现问题，请使用此模板。\nlabels: Bug\n---\n\n<!-- 如果搜索过但未找到，请将 `[ ]` 替换为 `[x]` -->\n\n- [ ] 你是否在现有 [Issue列表](/docmirror/dev-sidecar/issues) 中搜索过相同问题，但未找到？\n\n### Ⅰ. 请说明操作系统及DS的版本号：\n\n1. 操作系统：?\n2. DS版本号：? <!-- 如：`1.8.6-node17` -->\n\n### Ⅱ. 问题描述：\n\n### Ⅲ. 期望的结果：\n\n### Ⅳ. 如何复现问题？\n\n1. xxx\n2. xxx\n3. xxx\n\n### Ⅴ. 请提供相关的错误日志，尽可能的详细：（日志文件在 `${user.home}/.dev-sidecar/logs/` 目录下）\n\n<details>\n<summary>点击查看日志</summary>\n\n```log\n\n```\n</details>\n\n### Ⅵ. 有必要时，请提供 `${user.home}/.dev-sidecar/running.json` 文件内容：\n\n<!-- 请将 'running.json' 文件的内容粘贴在这里，方便我们排查问题是否由配置错误导致。 -->\n\n<details>\n<summary>点击查看运行参数</summary>\n\n```json\n\n```\n</details>\n"
  },
  {
    "path": ".github/ISSUE_TEMPLATE/2_STYLE_ISSUE.md",
    "content": "---\nname: 样式问题\nabout: 如果你发现了一些页面样式问题，请使用此模板。\nlabels: Style Issue\n---\n\n<!-- 如果搜索过但未找到，请将 `[ ]` 替换为 `[x]` -->\n\n- [ ] 你是否在现有 [Issue列表](/docmirror/dev-sidecar/issues) 中搜索过相同问题，但未找到？\n\n### Ⅰ. 请说明操作系统及DS的版本号：\n\n1. 操作系统：?\n2. DS版本号：? <!-- 如：`1.8.6-node17` -->\n\n### Ⅱ. 样式问题描述：\n\n### Ⅲ. 样式问题截图：\n"
  },
  {
    "path": ".github/ISSUE_TEMPLATE/3_CONFIG_ISSUES.md",
    "content": "---\nname: 配置问题\nabout: 如果你不知道如何配置DS来访问某个网站，请使用这个模板。\nlabels: Config Issue\n---\n\n### Ⅰ. 你对哪个功能的配置不了解？\n\n<!-- 请选择一个或多个选项，将前面的 `[ ]` 修改为 `[x]` 即可。 -->\n\n- [ ] 拦截设置：\n  - [ ] redirect\n  - [ ] proxy\n  - [ ] sni\n  - [ ] success\n  - [ ] abort\n  - [ ] cache\n  - [ ] options\n  - [ ] script\n  - [ ] requestReplace\n  - [ ] responseReplace\n- [ ] DNS设置和IP测速\n- [ ] 系统代理\n- [ ] 远程配置\n- [ ] 应用：\n  - [ ] NPM加速\n  - [ ] Git代理\n  - [ ] PIP加速\n  - [ ] 增强功能\n\n### Ⅱ. 请详细描述你的问题：\n\n### Ⅲ. 有必要时，请提供 `${user.home}/.dev-sidecar/running.json` 文件内容：\n\n<!-- 请将 'running.json' 文件的内容粘贴在这里，方便我们排查问题是否由配置错误导致。 -->\n\n<details>\n<summary>点击查看运行参数</summary>\n\n```json\n\n```\n</details>\n"
  },
  {
    "path": ".github/ISSUE_TEMPLATE/4_FEATURE_REQUEST.md",
    "content": "---\nname: 提新需求\nabout: 如果你想提出一个新需求，请使用此模板。\nlabels: Feature Request\n---\n\n### Ⅰ. 请描述你想要的新功能：\n\n<!-- 请简单描述你希望的新功能，例如：\"在某某页面，添加一个按钮，点击按钮时，弹出一个某某对话框，用于xxx。\" -->\n\n### Ⅱ. 请描述你心目中新功能的样子：\n\n<!-- 可以讲讲你对新功能的看法，可以解释更多关于该功能的输入和输出的信息，或贴上你设想的界面设计。 -->\n\n### Ⅲ. 你希望该新功能修复哪个issue？\n\n<!-- 请将相关issue的编号填写在下面，格式如：#123 -->\n"
  },
  {
    "path": ".github/ISSUE_TEMPLATE/5_OTHERS.md",
    "content": "---\nname: 其他问题\nabout: 如果不是以上问题，请使用此模板。\n---\n\n### 请详细描述你的问题、需求或建议：\n"
  },
  {
    "path": ".github/PULL_REQUEST_TEMPLATE.md",
    "content": "### Ⅰ. 描述此PR的作用：\n\n### Ⅱ. 此PR修复了哪个issue吗？\n\n<!-- 如果是的话，请在下一行写上 \"fixes #xxx\"，比如：fixes #97 -->\n\n### Ⅲ. 界面变化截屏\n\n<!-- 如果存在界面上的变化，请截屏展示出来 -->\n"
  },
  {
    "path": ".github/workflows/build-and-release.yml",
    "content": "name: Build And Release\n\non:\n  push:\n    branches:\n      - release*\n\njobs:\n  # job 1\n  build-and-upload:\n    runs-on: ${{ matrix.os }}-latest\n    env:\n      ELECTRON_CACHE: ${{ github.workspace }}/.cache/electron\n      ELECTRON_BUILDER_CACHE: ${{ github.workspace }}/.cache/electron-builder\n    strategy:\n      fail-fast: false\n      matrix:\n        os:\n          - windows\n          - ubuntu\n          - macos\n        node:\n          - 22\n    steps:\n      - name: Checkout\n        uses: actions/checkout@v4.1.7\n\n      - name: Setup pnpm\n        uses: pnpm/action-setup@v4\n\n      - name: 'Setup Node.js \"${{ matrix.node }}.x\" environment'\n        uses: actions/setup-node@v4\n        with:\n          node-version: ${{ matrix.node }}\n          registry-url: https://npm.pkg.github.com/\n          cache: pnpm\n\n      - name: Setup Python environment (Mac) Because of electron-builder install-app-deps requires Python setup tools\n        if: matrix.os == 'macos'\n        uses: actions/setup-python@v5\n        with:\n          python-version: '3.10'\n\n      - name: Get package info\n        id: package-info\n        uses: luizfelipelaviola/get-package-info@v1\n        with:\n          path: ./packages/mitmproxy\n\n      - name: Print\n        run: |\n          echo \"version = ${{ steps.package-info.outputs.version }}\";\n          echo \"github.ref_type = ${{ github.ref_type }}\";\n          echo \"github.ref = ${{ github.ref }}\";\n          echo \"github.ref_name = ${{ github.ref_name }}\";\n\n      - name: 'npm -v | pnpm -v | python --version'\n        run: |\n          echo \"======================================================================\";\n          echo \"npm -v\";\n          echo \"--------------------\";\n          npm -v;\n\n          echo \"======================================================================\";\n          echo \"pnpm -v\";\n          echo \"--------------------\";\n          pnpm -v;\n\n          echo \"======================================================================\";\n          echo \"python --version\";\n          echo \"--------------------\";\n          python --version;\n\n      - name: Setup electron cahce\n        uses: actions/cache@v4\n        with:\n          path: ${{ github.workspace }}/.cache/electron\n          key: ${{ runner.os }}-electron-cache-${{ hashFiles('**/pnpm-lock.yaml') }}\n          restore-keys: |\n            ${{ runner.os }}-electron-cache-\n\n      - name: Setup electron-builder cahce\n        uses: actions/cache@v4\n        with:\n          path: ${{ github.workspace }}/.cache/electron-builder\n          key: ${{ runner.os }}-electron-builder-cache-${{ hashFiles('**/pnpm-lock.yaml') }}\n          restore-keys: |\n            ${{ runner.os }}-electron-builder-cache-\n\n      - name: \"'pnpm install' Because we need to install optional dependencies\"\n        run: |\n          echo \"======================================================================\";\n          dir || ls -lah;\n\n          echo \"======================================================================\";\n          echo \"pnpm install\";\n          echo \"--------------------\";\n          pnpm install;\n\n      - name: 'test packages/core'\n        run: |\n          cd packages/core;\n          pnpm run test;\n\n      - name: 'test packages/mitmproxy'\n        run: |\n          cd packages/mitmproxy;\n          pnpm run test;\n\n      - name: 'npm run electron:build'\n        run: |\n          echo \"======================================================================\";\n          echo \"cd packages/gui\";\n          echo \"--------------------\";\n          cd packages/gui;\n          dir || ls -lah;\n\n          echo \"======================================================================\";\n          echo \"npm run electron:build\";\n          echo \"--------------------\";\n          npm run electron:build;\n\n      - name: 'Print dir \"packages/gui/dist_electron/\"'\n        run: |\n          echo \"======================================================================\";\n          echo \"cd packages/gui/dist_electron\";\n          echo \"--------------------\";\n          cd packages/gui/dist_electron;\n          dir || ls -lah;\n\n      # Rename artifacts\n      - name: 'Rename artifacts - Windows'\n        if: ${{ matrix.os == 'windows' }}\n        run: |\n          cd packages/gui/dist_electron;\n          ren DevSidecar-${{ steps.package-info.outputs.version }}-x64.exe   DevSidecar-${{ steps.package-info.outputs.version }}-windows-x64.exe;\n          ren DevSidecar-${{ steps.package-info.outputs.version }}-ia32.exe  DevSidecar-${{ steps.package-info.outputs.version }}-windows-ia32.exe;\n          ren DevSidecar-${{ steps.package-info.outputs.version }}-arm64.exe DevSidecar-${{ steps.package-info.outputs.version }}-windows-arm64.exe;\n          ren DevSidecar-${{ steps.package-info.outputs.version }}.exe       DevSidecar-${{ steps.package-info.outputs.version }}-windows-universal.exe;\n          dir;\n      - name: 'Rename artifacts - Linux'\n        if: ${{ matrix.os == 'ubuntu' }}\n        run: |\n          cd packages/gui/dist_electron;\n          mv DevSidecar-${{ steps.package-info.outputs.version }}-amd64.deb       DevSidecar-${{ steps.package-info.outputs.version }}-linux-amd64.deb;\n          mv DevSidecar-${{ steps.package-info.outputs.version }}-x86_64.AppImage DevSidecar-${{ steps.package-info.outputs.version }}-linux-x86_64.AppImage;\n          mv DevSidecar-${{ steps.package-info.outputs.version }}-x64.tar.gz      DevSidecar-${{ steps.package-info.outputs.version }}-linux-x64.tar.gz;\n          #-------------------------------------------------------------------------------------------------------------------------\n          mv DevSidecar-${{ steps.package-info.outputs.version }}-arm64.deb       DevSidecar-${{ steps.package-info.outputs.version }}-linux-arm64.deb;\n          mv DevSidecar-${{ steps.package-info.outputs.version }}-arm64.AppImage  DevSidecar-${{ steps.package-info.outputs.version }}-linux-arm64.AppImage;\n          mv DevSidecar-${{ steps.package-info.outputs.version }}-arm64.tar.gz    DevSidecar-${{ steps.package-info.outputs.version }}-linux-arm64.tar.gz;\n          #-------------------------------------------------------------------------------------------------------------------------\n          mv DevSidecar-${{ steps.package-info.outputs.version }}-armv7l.deb      DevSidecar-${{ steps.package-info.outputs.version }}-linux-armv7l.deb;\n          mv DevSidecar-${{ steps.package-info.outputs.version }}-armv7l.AppImage DevSidecar-${{ steps.package-info.outputs.version }}-linux-armv7l.AppImage;\n          mv DevSidecar-${{ steps.package-info.outputs.version }}-armv7l.tar.gz   DevSidecar-${{ steps.package-info.outputs.version }}-linux-armv7l.tar.gz;\n          ls -lah;\n      - name: 'Rename artifacts - macOS'\n        if: ${{ matrix.os == 'macos' }}\n        run: |\n          cd packages/gui/dist_electron;\n          mv DevSidecar-${{ steps.package-info.outputs.version }}-x64.dmg        DevSidecar-${{ steps.package-info.outputs.version }}-macos-x64.dmg;\n          mv DevSidecar-${{ steps.package-info.outputs.version }}-arm64.dmg      DevSidecar-${{ steps.package-info.outputs.version }}-macos-arm64.dmg;\n          mv DevSidecar-${{ steps.package-info.outputs.version }}-universal.dmg  DevSidecar-${{ steps.package-info.outputs.version }}-macos-universal.dmg;\n          ls -lah;\n\n      #region Upload artifacts - Windows\n      - name: 'Upload DevSidecar-${{ steps.package-info.outputs.version }}-windows-x64.exe'\n        uses: actions/upload-artifact@v4.4.0\n        if: ${{ matrix.os == 'windows' }}\n        with:\n          path: packages/gui/dist_electron/DevSidecar-${{ steps.package-info.outputs.version }}-windows-x64.exe\n          name: 'DevSidecar-${{ steps.package-info.outputs.version }}-windows-x64.exe'\n          if-no-files-found: error\n      - name: 'Upload DevSidecar-${{ steps.package-info.outputs.version }}-windows-ia32.exe'\n        uses: actions/upload-artifact@v4.4.0\n        if: ${{ matrix.os == 'windows' }}\n        with:\n          path: packages/gui/dist_electron/DevSidecar-${{ steps.package-info.outputs.version }}-windows-ia32.exe\n          name: 'DevSidecar-${{ steps.package-info.outputs.version }}-windows-ia32.exe'\n          if-no-files-found: error\n      - name: 'Upload DevSidecar-${{ steps.package-info.outputs.version }}-windows-arm64.exe'\n        uses: actions/upload-artifact@v4.4.0\n        if: ${{ matrix.os == 'windows' }}\n        with:\n          path: packages/gui/dist_electron/DevSidecar-${{ steps.package-info.outputs.version }}-windows-arm64.exe\n          name: 'DevSidecar-${{ steps.package-info.outputs.version }}-windows-arm64.exe'\n          if-no-files-found: error\n      - name: 'Upload DevSidecar-${{ steps.package-info.outputs.version }}-windows-universal.exe'\n        uses: actions/upload-artifact@v4.4.0\n        if: ${{ matrix.os == 'windows' }}\n        with:\n          path: packages/gui/dist_electron/DevSidecar-${{ steps.package-info.outputs.version }}-windows-universal.exe\n          name: 'DevSidecar-${{ steps.package-info.outputs.version }}-windows-universal.exe'\n          if-no-files-found: error\n      #endregion Upload artifacts - Windows\n\n      #region Upload artifacts - Linux\n      - name: 'Upload DevSidecar-${{ steps.package-info.outputs.version }}-linux-amd64.deb'\n        uses: actions/upload-artifact@v4.4.0\n        if: ${{ matrix.os == 'ubuntu' }}\n        with:\n          path: packages/gui/dist_electron/DevSidecar-${{ steps.package-info.outputs.version }}-linux-amd64.deb\n          name: 'DevSidecar-${{ steps.package-info.outputs.version }}-linux-amd64.deb'\n          if-no-files-found: error\n      - name: 'Upload DevSidecar-${{ steps.package-info.outputs.version }}-linux-x86_64.AppImage'\n        uses: actions/upload-artifact@v4.4.0\n        if: ${{ matrix.os == 'ubuntu' }}\n        with:\n          path: packages/gui/dist_electron/DevSidecar-${{ steps.package-info.outputs.version }}-linux-x86_64.AppImage\n          name: 'DevSidecar-${{ steps.package-info.outputs.version }}-linux-x86_64.AppImage'\n          if-no-files-found: error\n      - name: 'Upload DevSidecar-${{ steps.package-info.outputs.version }}-linux-x64.tar.gz'\n        uses: actions/upload-artifact@v4.4.0\n        if: ${{ matrix.os == 'ubuntu' }}\n        with:\n          path: packages/gui/dist_electron/DevSidecar-${{ steps.package-info.outputs.version }}-linux-x64.tar.gz\n          name: 'DevSidecar-${{ steps.package-info.outputs.version }}-linux-x64.tar.gz'\n          if-no-files-found: error\n      #-------------------------------------------------------------------------------------------------------------------------\n      - name: 'Upload DevSidecar-${{ steps.package-info.outputs.version }}-linux-arm64.deb'\n        uses: actions/upload-artifact@v4.4.0\n        if: ${{ matrix.os == 'ubuntu' }}\n        with:\n          path: packages/gui/dist_electron/DevSidecar-${{ steps.package-info.outputs.version }}-linux-arm64.deb\n          name: 'DevSidecar-${{ steps.package-info.outputs.version }}-linux-arm64.deb'\n          if-no-files-found: error\n      - name: 'Upload DevSidecar-${{ steps.package-info.outputs.version }}-linux-arm64.AppImage'\n        uses: actions/upload-artifact@v4.4.0\n        if: ${{ matrix.os == 'ubuntu' }}\n        with:\n          path: packages/gui/dist_electron/DevSidecar-${{ steps.package-info.outputs.version }}-linux-arm64.AppImage\n          name: 'DevSidecar-${{ steps.package-info.outputs.version }}-linux-arm64.AppImage'\n          if-no-files-found: error\n      - name: 'Upload DevSidecar-${{ steps.package-info.outputs.version }}-linux-arm64.tar.gz'\n        uses: actions/upload-artifact@v4.4.0\n        if: ${{ matrix.os == 'ubuntu' }}\n        with:\n          path: packages/gui/dist_electron/DevSidecar-${{ steps.package-info.outputs.version }}-linux-arm64.tar.gz\n          name: 'DevSidecar-${{ steps.package-info.outputs.version }}-linux-arm64.tar.gz'\n          if-no-files-found: error\n      #-------------------------------------------------------------------------------------------------------------------------\n      - name: 'Upload DevSidecar-${{ steps.package-info.outputs.version }}-linux-armv7l.deb'\n        uses: actions/upload-artifact@v4.4.0\n        if: ${{ matrix.os == 'ubuntu' }}\n        with:\n          path: packages/gui/dist_electron/DevSidecar-${{ steps.package-info.outputs.version }}-linux-armv7l.deb\n          name: 'DevSidecar-${{ steps.package-info.outputs.version }}-linux-armv7l.deb'\n          if-no-files-found: error\n      - name: 'Upload DevSidecar-${{ steps.package-info.outputs.version }}-linux-armv7l.AppImage'\n        uses: actions/upload-artifact@v4.4.0\n        if: ${{ matrix.os == 'ubuntu' }}\n        with:\n          path: packages/gui/dist_electron/DevSidecar-${{ steps.package-info.outputs.version }}-linux-armv7l.AppImage\n          name: 'DevSidecar-${{ steps.package-info.outputs.version }}-linux-armv7l.AppImage'\n          if-no-files-found: error\n      - name: 'Upload DevSidecar-${{ steps.package-info.outputs.version }}-linux-armv7l.tar.gz'\n        uses: actions/upload-artifact@v4.4.0\n        if: ${{ matrix.os == 'ubuntu' }}\n        with:\n          path: packages/gui/dist_electron/DevSidecar-${{ steps.package-info.outputs.version }}-linux-armv7l.tar.gz\n          name: 'DevSidecar-${{ steps.package-info.outputs.version }}-linux-armv7l.tar.gz'\n          if-no-files-found: error\n      #endregion Upload artifacts - Linux\n\n      # Upload artifacts - macOS\n      - name: 'Upload DevSidecar-${{ steps.package-info.outputs.version }}-macos-x64.dmg'\n        uses: actions/upload-artifact@v4.4.0\n        if: ${{ matrix.os == 'macos' }}\n        with:\n          path: packages/gui/dist_electron/DevSidecar-${{ steps.package-info.outputs.version }}-macos-x64.dmg\n          name: 'DevSidecar-${{ steps.package-info.outputs.version }}-macos-x64.dmg'\n          if-no-files-found: error\n      - name: 'Upload DevSidecar-${{ steps.package-info.outputs.version }}-macos-arm64.dmg'\n        uses: actions/upload-artifact@v4.4.0\n        if: ${{ matrix.os == 'macos' }}\n        with:\n          path: packages/gui/dist_electron/DevSidecar-${{ steps.package-info.outputs.version }}-macos-arm64.dmg\n          name: 'DevSidecar-${{ steps.package-info.outputs.version }}-macos-arm64.dmg'\n          if-no-files-found: error\n      - name: 'Upload DevSidecar-${{ steps.package-info.outputs.version }}-macos-universal.dmg'\n        uses: actions/upload-artifact@v4.4.0\n        if: ${{ matrix.os == 'macos' }}\n        with:\n          path: packages/gui/dist_electron/DevSidecar-${{ steps.package-info.outputs.version }}-macos-universal.dmg\n          name: 'DevSidecar-${{ steps.package-info.outputs.version }}-macos-universal.dmg'\n          if-no-files-found: error\n\n\n  # job 2\n  download-and-release:\n    runs-on: ubuntu-latest\n    needs:\n      - build-and-upload\n    steps:\n      - name: Checkout\n        uses: actions/checkout@v4.1.7\n\n      - name: Get package info\n        id: package-info\n        uses: luizfelipelaviola/get-package-info@v1\n        with:\n          path: ./packages/mitmproxy\n\n      - name: 'Make \"release\" dir'\n        run: mkdir release\n\n      # Download artifacts\n      - name: 'Download DevSidecar-${{ steps.package-info.outputs.version }}-windows-x64.exe'\n        uses: actions/download-artifact@v4.1.8\n        with:\n          name: 'DevSidecar-${{ steps.package-info.outputs.version }}-windows-x64.exe'\n          path: release\n      - name: 'Download DevSidecar-${{ steps.package-info.outputs.version }}-windows-ia32.exe'\n        uses: actions/download-artifact@v4.1.8\n        with:\n          name: 'DevSidecar-${{ steps.package-info.outputs.version }}-windows-ia32.exe'\n          path: release\n      - name: 'Download DevSidecar-${{ steps.package-info.outputs.version }}-windows-arm64.exe'\n        uses: actions/download-artifact@v4.1.8\n        with:\n          name: 'DevSidecar-${{ steps.package-info.outputs.version }}-windows-arm64.exe'\n          path: release\n      - name: 'Download DevSidecar-${{ steps.package-info.outputs.version }}-windows-universal.exe'\n        uses: actions/download-artifact@v4.1.8\n        with:\n          name: 'DevSidecar-${{ steps.package-info.outputs.version }}-windows-universal.exe'\n          path: release\n\n      - name: 'Download DevSidecar-${{ steps.package-info.outputs.version }}-linux-amd64.deb'\n        uses: actions/download-artifact@v4.1.8\n        with:\n          name: 'DevSidecar-${{ steps.package-info.outputs.version }}-linux-amd64.deb'\n          path: release\n      - name: 'Download DevSidecar-${{ steps.package-info.outputs.version }}-linux-x86_64.AppImage'\n        uses: actions/download-artifact@v4.1.8\n        with:\n          name: 'DevSidecar-${{ steps.package-info.outputs.version }}-linux-x86_64.AppImage'\n          path: release\n      - name: 'Download DevSidecar-${{ steps.package-info.outputs.version }}-linux-x64.tar.gz'\n        uses: actions/download-artifact@v4.1.8\n        with:\n          name: 'DevSidecar-${{ steps.package-info.outputs.version }}-linux-x64.tar.gz'\n          path: release\n      #-------------------------------------------------------------------------------------------------------------------------\n      - name: 'Download DevSidecar-${{ steps.package-info.outputs.version }}-linux-arm64.deb'\n        uses: actions/download-artifact@v4.1.8\n        with:\n          name: 'DevSidecar-${{ steps.package-info.outputs.version }}-linux-arm64.deb'\n          path: release\n      - name: 'Download DevSidecar-${{ steps.package-info.outputs.version }}-linux-arm64.AppImage'\n        uses: actions/download-artifact@v4.1.8\n        with:\n          name: 'DevSidecar-${{ steps.package-info.outputs.version }}-linux-arm64.AppImage'\n          path: release\n      - name: 'Download DevSidecar-${{ steps.package-info.outputs.version }}-linux-arm64.tar.gz'\n        uses: actions/download-artifact@v4.1.8\n        with:\n          name: 'DevSidecar-${{ steps.package-info.outputs.version }}-linux-arm64.tar.gz'\n          path: release\n      #-------------------------------------------------------------------------------------------------------------------------\n      - name: 'Download DevSidecar-${{ steps.package-info.outputs.version }}-linux-armv7l.deb'\n        uses: actions/download-artifact@v4.1.8\n        with:\n          name: 'DevSidecar-${{ steps.package-info.outputs.version }}-linux-armv7l.deb'\n          path: release\n      - name: 'Download DevSidecar-${{ steps.package-info.outputs.version }}-linux-armv7l.AppImage'\n        uses: actions/download-artifact@v4.1.8\n        with:\n          name: 'DevSidecar-${{ steps.package-info.outputs.version }}-linux-armv7l.AppImage'\n          path: release\n      - name: 'Download DevSidecar-${{ steps.package-info.outputs.version }}-linux-armv7l.tar.gz'\n        uses: actions/download-artifact@v4.1.8\n        with:\n          name: 'DevSidecar-${{ steps.package-info.outputs.version }}-linux-armv7l.tar.gz'\n          path: release\n\n      - name: 'Download DevSidecar-${{ steps.package-info.outputs.version }}-macos-x64.dmg'\n        uses: actions/download-artifact@v4.1.8\n        with:\n          name: 'DevSidecar-${{ steps.package-info.outputs.version }}-macos-x64.dmg'\n          path: release\n      - name: 'Download DevSidecar-${{ steps.package-info.outputs.version }}-macos-arm64.dmg'\n        uses: actions/download-artifact@v4.1.8\n        with:\n          name: 'DevSidecar-${{ steps.package-info.outputs.version }}-macos-arm64.dmg'\n          path: release\n      - name: 'Download DevSidecar-${{ steps.package-info.outputs.version }}-macos-universal.dmg'\n        uses: actions/download-artifact@v4.1.8\n        with:\n          name: 'DevSidecar-${{ steps.package-info.outputs.version }}-macos-universal.dmg'\n          path: release\n\n      - name: 'Print files from \"release\" dir'\n        run: |\n          ls -lah release;\n\n      - name: Create a draft release\n        uses: wangliang181230/github-action-ghr@master\n        env:\n          GITHUB_TOKEN: ${{ github.token }}\n          GHR_PATH: release/\n          GHR_TITLE: ${{ github.ref_name }}\n          GHR_REPLACE: true\n          GHR_DRAFT: true\n"
  },
  {
    "path": ".github/workflows/npm-run-electron.yml",
    "content": "name: npm run electron\n\non:\n  push:\n    branches:\n      - run*\n      - test*\n      - release*\n    paths-ignore:\n      - '_script/**'\n      - 'doc/**'\n      - '**/*.md'\n      - '**/.gitignore'\n      - '**/LICENSE'\n\njobs:\n  npm-run-electron:\n    runs-on: ${{ matrix.os }}-latest\n    env:\n      ELECTRON_CACHE: ${{ github.workspace }}/.cache/electron\n      ELECTRON_BUILDER_CACHE: ${{ github.workspace }}/.cache/electron-builder\n    strategy:\n      fail-fast: false\n      matrix:\n        os:\n          - windows\n          - ubuntu\n          - macos\n        node:\n          - 22\n    steps:\n      - name: Checkout\n        uses: actions/checkout@v4.1.7\n\n      - name: Setup pnpm\n        uses: pnpm/action-setup@v4\n\n      - name: 'Setup Node.js \"${{ matrix.node }}.x\" environment'\n        uses: actions/setup-node@v4\n        with:\n          node-version: ${{ matrix.node }}\n          registry-url: https://npm.pkg.github.com/\n          cache: pnpm\n\n      - name: Setup Python environment (Mac) Because of electron-builder install-app-deps requires Python setup tools\n        if: matrix.os == 'macos'\n        uses: actions/setup-python@v5\n        with:\n          python-version: '3.10'\n\n      - name: Print\n        run: |\n          echo \"github.ref_type = ${{ github.ref_type }}\";\n          echo \"github.ref = ${{ github.ref }}\";\n          echo \"github.ref_name = ${{ github.ref_name }}\";\n\n      - name: 'npm -v | pnpm -v | python --version'\n        run: |\n          echo \"======================================================================\";\n          echo \"npm -v\";\n          echo \"--------------------\";\n          npm -v;\n\n          echo \"======================================================================\";\n          echo \"pnpm -v\";\n          echo \"--------------------\";\n          pnpm -v;\n\n          echo \"======================================================================\";\n          echo \"python --version\";\n          echo \"--------------------\";\n          python --version;\n\n      - name: Setup electron cahce\n        uses: actions/cache@v4\n        with:\n          path: ${{ github.workspace }}/.cache/electron\n          key: ${{ runner.os }}-electron-cache-${{ hashFiles('**/pnpm-lock.yaml') }}\n          restore-keys: |\n            ${{ runner.os }}-electron-cache-\n\n      - name: Setup electron-builder cahce\n        uses: actions/cache@v4\n        with:\n          path: ${{ github.workspace }}/.cache/electron-builder\n          key: ${{ runner.os }}-electron-builder-cache-${{ hashFiles('**/pnpm-lock.yaml') }}\n          restore-keys: |\n            ${{ runner.os }}-electron-builder-cache-\n\n      - name: pnpm install\n        run: |\n          echo \"======================================================================\";\n          dir || ls -lah;\n\n          echo \"======================================================================\";\n          echo \"pnpm install\";\n          echo \"--------------------\";\n          pnpm install;\n\n      - name: npm run electron\n        run: |\n          echo \"======================================================================\";\n          echo \"cd packages/gui\";\n          echo \"--------------------\";\n          cd packages/gui;\n          dir || ls -lah;\n\n          echo \"======================================================================\";\n          echo \"npm run electron\";\n          echo \"--------------------\";\n          npm run electron;\n"
  },
  {
    "path": ".github/workflows/test-and-upload.yml",
    "content": "name: Test And Upload\n\non:\n  push:\n    branches:\n      - master\n      - 1.x\n      - develop\n      - test*\n    paths-ignore:\n      - '_script/**'\n      - 'doc/**'\n      - '**/*.md'\n      - '**/.gitignore'\n      - '**/LICENSE'\n  pull_request:\n    branches:\n      - master\n      - develop\n      - 1.x\n    paths-ignore:\n      - '_script/**'\n      - 'doc/**'\n      - '**/*.md'\n      - '**/.gitignore'\n      - '**/LICENSE'\n\njobs:\n  test-and-upload:\n    runs-on: ${{ matrix.os }}-latest\n    env:\n      ELECTRON_CACHE: ${{ github.workspace }}/.cache/electron\n      ELECTRON_BUILDER_CACHE: ${{ github.workspace }}/.cache/electron-builder\n    strategy:\n      fail-fast: false\n      matrix:\n        os:\n          - windows\n          - ubuntu\n          - macos\n        node:\n          - 22\n    steps:\n      - name: Checkout\n        uses: actions/checkout@v4.1.7\n\n      - name: Setup pnpm\n        uses: pnpm/action-setup@v4\n\n      - name: 'Setup Node.js \"${{ matrix.node }}.x\" environment'\n        uses: actions/setup-node@v4\n        with:\n          node-version: ${{ matrix.node }}\n          registry-url: https://npm.pkg.github.com/\n          cache: pnpm\n\n      - name: Setup Python environment (Mac) Because of electron-builder install-app-deps requires Python setup tools\n        if: matrix.os == 'macos'\n        uses: actions/setup-python@v5\n        with:\n          python-version: '3.10'\n\n      - name: Get package info\n        id: package-info\n        uses: luizfelipelaviola/get-package-info@v1\n        with:\n          path: ./packages/mitmproxy\n\n      - name: Print\n        run: |\n          echo \"version = ${{ steps.package-info.outputs.version }}\";\n          echo \"github.ref_type = ${{ github.ref_type }}\";\n          echo \"github.ref = ${{ github.ref }}\";\n          echo \"github.ref_name = ${{ github.ref_name }}\";\n\n      - name: 'npm -v | pnpm -v | python --version'\n        run: |\n          echo \"======================================================================\";\n          echo \"npm -v\";\n          echo \"--------------------\";\n          npm -v;\n\n          echo \"======================================================================\";\n          echo \"pnpm -v\";\n          echo \"--------------------\";\n          pnpm -v;\n\n          echo \"======================================================================\";\n          echo \"python --version\";\n          echo \"--------------------\";\n          python --version;\n\n      - name: Setup electron cahce\n        uses: actions/cache@v4\n        with:\n          path: ${{ github.workspace }}/.cache/electron\n          key: ${{ runner.os }}-electron-cache-${{ hashFiles('**/pnpm-lock.yaml') }}\n          restore-keys: |\n            ${{ runner.os }}-electron-cache-\n\n      - name: Setup electron-builder cahce\n        uses: actions/cache@v4\n        with:\n          path: ${{ github.workspace }}/.cache/electron-builder\n          key: ${{ runner.os }}-electron-builder-cache-${{ hashFiles('**/pnpm-lock.yaml') }}\n          restore-keys: |\n            ${{ runner.os }}-electron-builder-cache-\n\n      - name: \"'pnpm install' Because we need to install optional dependencies\"\n        run: |\n          echo \"======================================================================\";\n          dir || ls -lah;\n\n          echo \"======================================================================\";\n          echo \"pnpm install\";\n          echo \"--------------------\";\n          pnpm install;\n\n      - name: 'test packages/core'\n        run: |\n          cd packages/core;\n          pnpm run test;\n\n      - name: 'test packages/mitmproxy'\n        run: |\n          cd packages/mitmproxy;\n          pnpm run test;\n\n      - name: 'npm run electron:build'\n        run: |\n          echo \"======================================================================\";\n          echo \"cd packages/gui\";\n          echo \"--------------------\";\n          cd packages/gui;\n          dir || ls -lah;\n\n          echo \"======================================================================\";\n          echo \"npm run electron:build\";\n          echo \"--------------------\";\n          npm run electron:build;\n\n      - name: 'Print dir \"packages/gui/dist_electron/\"'\n        run: |\n          echo \"======================================================================\";\n          echo \"cd packages/gui/dist_electron\";\n          echo \"--------------------\";\n          cd packages/gui/dist_electron;\n          dir || ls -lah;\n\n      # Rename artifacts\n      - name: 'Rename artifacts - Windows'\n        if: ${{ matrix.os == 'windows' }}\n        run: |\n          cd packages/gui/dist_electron;\n          ren DevSidecar-${{ steps.package-info.outputs.version }}-x64.exe   DevSidecar-${{ steps.package-info.outputs.version }}-windows-x64.exe;\n          ren DevSidecar-${{ steps.package-info.outputs.version }}-ia32.exe  DevSidecar-${{ steps.package-info.outputs.version }}-windows-ia32.exe;\n          ren DevSidecar-${{ steps.package-info.outputs.version }}-arm64.exe DevSidecar-${{ steps.package-info.outputs.version }}-windows-arm64.exe;\n          ren DevSidecar-${{ steps.package-info.outputs.version }}.exe       DevSidecar-${{ steps.package-info.outputs.version }}-windows-universal.exe;\n          dir;\n      - name: 'Rename artifacts - Linux'\n        if: ${{ matrix.os == 'ubuntu' }}\n        run: |\n          cd packages/gui/dist_electron;\n          mv DevSidecar-${{ steps.package-info.outputs.version }}-amd64.deb       DevSidecar-${{ steps.package-info.outputs.version }}-linux-amd64.deb;\n          mv DevSidecar-${{ steps.package-info.outputs.version }}-x86_64.AppImage DevSidecar-${{ steps.package-info.outputs.version }}-linux-x86_64.AppImage;\n          mv DevSidecar-${{ steps.package-info.outputs.version }}-x64.tar.gz      DevSidecar-${{ steps.package-info.outputs.version }}-linux-x64.tar.gz;\n          #-------------------------------------------------------------------------------------------------------------------------\n          mv DevSidecar-${{ steps.package-info.outputs.version }}-arm64.deb       DevSidecar-${{ steps.package-info.outputs.version }}-linux-arm64.deb;\n          mv DevSidecar-${{ steps.package-info.outputs.version }}-arm64.AppImage  DevSidecar-${{ steps.package-info.outputs.version }}-linux-arm64.AppImage;\n          mv DevSidecar-${{ steps.package-info.outputs.version }}-arm64.tar.gz    DevSidecar-${{ steps.package-info.outputs.version }}-linux-arm64.tar.gz;\n          #-------------------------------------------------------------------------------------------------------------------------\n          mv DevSidecar-${{ steps.package-info.outputs.version }}-armv7l.deb      DevSidecar-${{ steps.package-info.outputs.version }}-linux-armv7l.deb;\n          mv DevSidecar-${{ steps.package-info.outputs.version }}-armv7l.AppImage DevSidecar-${{ steps.package-info.outputs.version }}-linux-armv7l.AppImage;\n          mv DevSidecar-${{ steps.package-info.outputs.version }}-armv7l.tar.gz   DevSidecar-${{ steps.package-info.outputs.version }}-linux-armv7l.tar.gz;\n          ls -lah;\n      - name: 'Rename artifacts - macOS'\n        if: ${{ matrix.os == 'macos' }}\n        run: |\n          cd packages/gui/dist_electron;\n          mv DevSidecar-${{ steps.package-info.outputs.version }}-x64.dmg        DevSidecar-${{ steps.package-info.outputs.version }}-macos-x64.dmg;\n          mv DevSidecar-${{ steps.package-info.outputs.version }}-arm64.dmg      DevSidecar-${{ steps.package-info.outputs.version }}-macos-arm64.dmg;\n          mv DevSidecar-${{ steps.package-info.outputs.version }}-universal.dmg  DevSidecar-${{ steps.package-info.outputs.version }}-macos-universal.dmg;\n          ls -lah;\n\n      #region Upload artifacts - Windows\n      - name: 'Upload DevSidecar-${{ steps.package-info.outputs.version }}-windows-x64.exe'\n        uses: actions/upload-artifact@v4.4.0\n        if: ${{ matrix.os == 'windows' }}\n        with:\n          path: packages/gui/dist_electron/DevSidecar-${{ steps.package-info.outputs.version }}-windows-x64.exe\n          name: 'DevSidecar-${{ steps.package-info.outputs.version }}-windows-x64.exe'\n          if-no-files-found: error\n      - name: 'Upload DevSidecar-${{ steps.package-info.outputs.version }}-windows-ia32.exe'\n        uses: actions/upload-artifact@v4.4.0\n        if: ${{ matrix.os == 'windows' }}\n        with:\n          path: packages/gui/dist_electron/DevSidecar-${{ steps.package-info.outputs.version }}-windows-ia32.exe\n          name: 'DevSidecar-${{ steps.package-info.outputs.version }}-windows-ia32.exe'\n          if-no-files-found: error\n      - name: 'Upload DevSidecar-${{ steps.package-info.outputs.version }}-windows-arm64.exe'\n        uses: actions/upload-artifact@v4.4.0\n        if: ${{ matrix.os == 'windows' }}\n        with:\n          path: packages/gui/dist_electron/DevSidecar-${{ steps.package-info.outputs.version }}-windows-arm64.exe\n          name: 'DevSidecar-${{ steps.package-info.outputs.version }}-windows-arm64.exe'\n          if-no-files-found: error\n      - name: 'Upload DevSidecar-${{ steps.package-info.outputs.version }}-windows-universal.exe'\n        uses: actions/upload-artifact@v4.4.0\n        if: ${{ matrix.os == 'windows' }}\n        with:\n          path: packages/gui/dist_electron/DevSidecar-${{ steps.package-info.outputs.version }}-windows-universal.exe\n          name: 'DevSidecar-${{ steps.package-info.outputs.version }}-windows-universal.exe'\n          if-no-files-found: error\n      #endregion Upload artifacts - Windows\n\n      #region Upload artifacts - Linux\n      - name: 'Upload DevSidecar-${{ steps.package-info.outputs.version }}-linux-amd64.deb'\n        uses: actions/upload-artifact@v4.4.0\n        if: ${{ matrix.os == 'ubuntu' }}\n        with:\n          path: packages/gui/dist_electron/DevSidecar-${{ steps.package-info.outputs.version }}-linux-amd64.deb\n          name: 'DevSidecar-${{ steps.package-info.outputs.version }}-linux-amd64.deb'\n          if-no-files-found: error\n      - name: 'Upload DevSidecar-${{ steps.package-info.outputs.version }}-linux-x86_64.AppImage'\n        uses: actions/upload-artifact@v4.4.0\n        if: ${{ matrix.os == 'ubuntu' }}\n        with:\n          path: packages/gui/dist_electron/DevSidecar-${{ steps.package-info.outputs.version }}-linux-x86_64.AppImage\n          name: 'DevSidecar-${{ steps.package-info.outputs.version }}-linux-x86_64.AppImage'\n          if-no-files-found: error\n      - name: 'Upload DevSidecar-${{ steps.package-info.outputs.version }}-linux-x64.tar.gz'\n        uses: actions/upload-artifact@v4.4.0\n        if: ${{ matrix.os == 'ubuntu' }}\n        with:\n          path: packages/gui/dist_electron/DevSidecar-${{ steps.package-info.outputs.version }}-linux-x64.tar.gz\n          name: 'DevSidecar-${{ steps.package-info.outputs.version }}-linux-x64.tar.gz'\n          if-no-files-found: error\n      #-------------------------------------------------------------------------------------------------------------------------\n      - name: 'Upload DevSidecar-${{ steps.package-info.outputs.version }}-linux-arm64.deb'\n        uses: actions/upload-artifact@v4.4.0\n        if: ${{ matrix.os == 'ubuntu' }}\n        with:\n          path: packages/gui/dist_electron/DevSidecar-${{ steps.package-info.outputs.version }}-linux-arm64.deb\n          name: 'DevSidecar-${{ steps.package-info.outputs.version }}-linux-arm64.deb'\n          if-no-files-found: error\n      - name: 'Upload DevSidecar-${{ steps.package-info.outputs.version }}-linux-arm64.AppImage'\n        uses: actions/upload-artifact@v4.4.0\n        if: ${{ matrix.os == 'ubuntu' }}\n        with:\n          path: packages/gui/dist_electron/DevSidecar-${{ steps.package-info.outputs.version }}-linux-arm64.AppImage\n          name: 'DevSidecar-${{ steps.package-info.outputs.version }}-linux-arm64.AppImage'\n          if-no-files-found: error\n      - name: 'Upload DevSidecar-${{ steps.package-info.outputs.version }}-linux-arm64.tar.gz'\n        uses: actions/upload-artifact@v4.4.0\n        if: ${{ matrix.os == 'ubuntu' }}\n        with:\n          path: packages/gui/dist_electron/DevSidecar-${{ steps.package-info.outputs.version }}-linux-arm64.tar.gz\n          name: 'DevSidecar-${{ steps.package-info.outputs.version }}-linux-arm64.tar.gz'\n          if-no-files-found: error\n      #-------------------------------------------------------------------------------------------------------------------------\n      - name: 'Upload DevSidecar-${{ steps.package-info.outputs.version }}-linux-armv7l.deb'\n        uses: actions/upload-artifact@v4.4.0\n        if: ${{ matrix.os == 'ubuntu' }}\n        with:\n          path: packages/gui/dist_electron/DevSidecar-${{ steps.package-info.outputs.version }}-linux-armv7l.deb\n          name: 'DevSidecar-${{ steps.package-info.outputs.version }}-linux-armv7l.deb'\n          if-no-files-found: error\n      - name: 'Upload DevSidecar-${{ steps.package-info.outputs.version }}-linux-armv7l.AppImage'\n        uses: actions/upload-artifact@v4.4.0\n        if: ${{ matrix.os == 'ubuntu' }}\n        with:\n          path: packages/gui/dist_electron/DevSidecar-${{ steps.package-info.outputs.version }}-linux-armv7l.AppImage\n          name: 'DevSidecar-${{ steps.package-info.outputs.version }}-linux-armv7l.AppImage'\n          if-no-files-found: error\n      - name: 'Upload DevSidecar-${{ steps.package-info.outputs.version }}-linux-armv7l.tar.gz'\n        uses: actions/upload-artifact@v4.4.0\n        if: ${{ matrix.os == 'ubuntu' }}\n        with:\n          path: packages/gui/dist_electron/DevSidecar-${{ steps.package-info.outputs.version }}-linux-armv7l.tar.gz\n          name: 'DevSidecar-${{ steps.package-info.outputs.version }}-linux-armv7l.tar.gz'\n          if-no-files-found: error\n      #endregion Upload artifacts - Linux\n\n      # Upload artifacts - macOS\n      - name: 'Upload DevSidecar-${{ steps.package-info.outputs.version }}-macos-x64.dmg'\n        uses: actions/upload-artifact@v4.4.0\n        if: ${{ matrix.os == 'macos' }}\n        with:\n          path: packages/gui/dist_electron/DevSidecar-${{ steps.package-info.outputs.version }}-macos-x64.dmg\n          name: 'DevSidecar-${{ steps.package-info.outputs.version }}-macos-x64.dmg'\n          if-no-files-found: error\n      - name: 'Upload DevSidecar-${{ steps.package-info.outputs.version }}-macos-arm64.dmg'\n        uses: actions/upload-artifact@v4.4.0\n        if: ${{ matrix.os == 'macos' }}\n        with:\n          path: packages/gui/dist_electron/DevSidecar-${{ steps.package-info.outputs.version }}-macos-arm64.dmg\n          name: 'DevSidecar-${{ steps.package-info.outputs.version }}-macos-arm64.dmg'\n          if-no-files-found: error\n      - name: 'Upload DevSidecar-${{ steps.package-info.outputs.version }}-macos-universal.dmg'\n        uses: actions/upload-artifact@v4.4.0\n        if: ${{ matrix.os == 'macos' }}\n        with:\n          path: packages/gui/dist_electron/DevSidecar-${{ steps.package-info.outputs.version }}-macos-universal.dmg\n          name: 'DevSidecar-${{ steps.package-info.outputs.version }}-macos-universal.dmg'\n          if-no-files-found: error\n"
  },
  {
    "path": ".gitignore",
    "content": "# IntelliJ project files\n.idea\n*.iml\n\n# vscode settings files\n.vscode\n\n# Mac\n.DS_Store\n\n# Node files\nnode_modules/\n*.lock\npackage-lock.json\n\n# Other files\nout\ngen\n*.log\n*.lnk\n"
  },
  {
    "path": ".npmrc",
    "content": "shamefully-hoist=true\n"
  },
  {
    "path": "LICENSE",
    "content": "Mozilla Public License Version 2.0\n==================================\n\n1. Definitions\n--------------\n\n1.1. \"Contributor\"\n    means each individual or legal entity that creates, contributes to\n    the creation of, or owns Covered Software.\n\n1.2. \"Contributor Version\"\n    means the combination of the Contributions of others (if any) used\n    by a Contributor and that particular Contributor's Contribution.\n\n1.3. \"Contribution\"\n    means Covered Software of a particular Contributor.\n\n1.4. \"Covered Software\"\n    means Source Code Form to which the initial Contributor has attached\n    the notice in Exhibit A, the Executable Form of such Source Code\n    Form, and Modifications of such Source Code Form, in each case\n    including portions thereof.\n\n1.5. \"Incompatible With Secondary Licenses\"\n    means\n\n    (a) that the initial Contributor has attached the notice described\n        in Exhibit B to the Covered Software; or\n\n    (b) that the Covered Software was made available under the terms of\n        version 1.1 or earlier of the License, but not also under the\n        terms of a Secondary License.\n\n1.6. \"Executable Form\"\n    means any form of the work other than Source Code Form.\n\n1.7. \"Larger Work\"\n    means a work that combines Covered Software with other material, in\n    a separate file or files, that is not Covered Software.\n\n1.8. \"License\"\n    means this document.\n\n1.9. \"Licensable\"\n    means having the right to grant, to the maximum extent possible,\n    whether at the time of the initial grant or subsequently, any and\n    all of the rights conveyed by this License.\n\n1.10. \"Modifications\"\n    means any of the following:\n\n    (a) any file in Source Code Form that results from an addition to,\n        deletion from, or modification of the contents of Covered\n        Software; or\n\n    (b) any new file in Source Code Form that contains any Covered\n        Software.\n\n1.11. \"Patent Claims\" of a Contributor\n    means any patent claim(s), including without limitation, method,\n    process, and apparatus claims, in any patent Licensable by such\n    Contributor that would be infringed, but for the grant of the\n    License, by the making, using, selling, offering for sale, having\n    made, import, or transfer of either its Contributions or its\n    Contributor Version.\n\n1.12. \"Secondary License\"\n    means either the GNU General Public License, Version 2.0, the GNU\n    Lesser General Public License, Version 2.1, the GNU Affero General\n    Public License, Version 3.0, or any later versions of those\n    licenses.\n\n1.13. \"Source Code Form\"\n    means the form of the work preferred for making modifications.\n\n1.14. \"You\" (or \"Your\")\n    means an individual or a legal entity exercising rights under this\n    License. For legal entities, \"You\" includes any entity that\n    controls, is controlled by, or is under common control with You. For\n    purposes of this definition, \"control\" means (a) the power, direct\n    or indirect, to cause the direction or management of such entity,\n    whether by contract or otherwise, or (b) ownership of more than\n    fifty percent (50%) of the outstanding shares or beneficial\n    ownership of such entity.\n\n2. License Grants and Conditions\n--------------------------------\n\n2.1. Grants\n\nEach Contributor hereby grants You a world-wide, royalty-free,\nnon-exclusive license:\n\n(a) under intellectual property rights (other than patent or trademark)\n    Licensable by such Contributor to use, reproduce, make available,\n    modify, display, perform, distribute, and otherwise exploit its\n    Contributions, either on an unmodified basis, with Modifications, or\n    as part of a Larger Work; and\n\n(b) under Patent Claims of such Contributor to make, use, sell, offer\n    for sale, have made, import, and otherwise transfer either its\n    Contributions or its Contributor Version.\n\n2.2. Effective Date\n\nThe licenses granted in Section 2.1 with respect to any Contribution\nbecome effective for each Contribution on the date the Contributor first\ndistributes such Contribution.\n\n2.3. Limitations on Grant Scope\n\nThe licenses granted in this Section 2 are the only rights granted under\nthis License. No additional rights or licenses will be implied from the\ndistribution or licensing of Covered Software under this License.\nNotwithstanding Section 2.1(b) above, no patent license is granted by a\nContributor:\n\n(a) for any code that a Contributor has removed from Covered Software;\n    or\n\n(b) for infringements caused by: (i) Your and any other third party's\n    modifications of Covered Software, or (ii) the combination of its\n    Contributions with other software (except as part of its Contributor\n    Version); or\n\n(c) under Patent Claims infringed by Covered Software in the absence of\n    its Contributions.\n\nThis License does not grant any rights in the trademarks, service marks,\nor logos of any Contributor (except as may be necessary to comply with\nthe notice requirements in Section 3.4).\n\n2.4. Subsequent Licenses\n\nNo Contributor makes additional grants as a result of Your choice to\ndistribute the Covered Software under a subsequent version of this\nLicense (see Section 10.2) or under the terms of a Secondary License (if\npermitted under the terms of Section 3.3).\n\n2.5. Representation\n\nEach Contributor represents that the Contributor believes its\nContributions are its original creation(s) or it has sufficient rights\nto grant the rights to its Contributions conveyed by this License.\n\n2.6. Fair Use\n\nThis License is not intended to limit any rights You have under\napplicable copyright doctrines of fair use, fair dealing, or other\nequivalents.\n\n2.7. Conditions\n\nSections 3.1, 3.2, 3.3, and 3.4 are conditions of the licenses granted\nin Section 2.1.\n\n3. Responsibilities\n-------------------\n\n3.1. Distribution of Source Form\n\nAll distribution of Covered Software in Source Code Form, including any\nModifications that You create or to which You contribute, must be under\nthe terms of this License. You must inform recipients that the Source\nCode Form of the Covered Software is governed by the terms of this\nLicense, and how they can obtain a copy of this License. You may not\nattempt to alter or restrict the recipients' rights in the Source Code\nForm.\n\n3.2. Distribution of Executable Form\n\nIf You distribute Covered Software in Executable Form then:\n\n(a) such Covered Software must also be made available in Source Code\n    Form, as described in Section 3.1, and You must inform recipients of\n    the Executable Form how they can obtain a copy of such Source Code\n    Form by reasonable means in a timely manner, at a charge no more\n    than the cost of distribution to the recipient; and\n\n(b) You may distribute such Executable Form under the terms of this\n    License, or sublicense it under different terms, provided that the\n    license for the Executable Form does not attempt to limit or alter\n    the recipients' rights in the Source Code Form under this License.\n\n3.3. Distribution of a Larger Work\n\nYou may create and distribute a Larger Work under terms of Your choice,\nprovided that You also comply with the requirements of this License for\nthe Covered Software. If the Larger Work is a combination of Covered\nSoftware with a work governed by one or more Secondary Licenses, and the\nCovered Software is not Incompatible With Secondary Licenses, this\nLicense permits You to additionally distribute such Covered Software\nunder the terms of such Secondary License(s), so that the recipient of\nthe Larger Work may, at their option, further distribute the Covered\nSoftware under the terms of either this License or such Secondary\nLicense(s).\n\n3.4. Notices\n\nYou may not remove or alter the substance of any license notices\n(including copyright notices, patent notices, disclaimers of warranty,\nor limitations of liability) contained within the Source Code Form of\nthe Covered Software, except that You may alter any license notices to\nthe extent required to remedy known factual inaccuracies.\n\n3.5. Application of Additional Terms\n\nYou may choose to offer, and to charge a fee for, warranty, support,\nindemnity or liability obligations to one or more recipients of Covered\nSoftware. However, You may do so only on Your own behalf, and not on\nbehalf of any Contributor. You must make it absolutely clear that any\nsuch warranty, support, indemnity, or liability obligation is offered by\nYou alone, and You hereby agree to indemnify every Contributor for any\nliability incurred by such Contributor as a result of warranty, support,\nindemnity or liability terms You offer. You may include additional\ndisclaimers of warranty and limitations of liability specific to any\njurisdiction.\n\n4. Inability to Comply Due to Statute or Regulation\n---------------------------------------------------\n\nIf it is impossible for You to comply with any of the terms of this\nLicense with respect to some or all of the Covered Software due to\nstatute, judicial order, or regulation then You must: (a) comply with\nthe terms of this License to the maximum extent possible; and (b)\ndescribe the limitations and the code they affect. Such description must\nbe placed in a text file included with all distributions of the Covered\nSoftware under this License. Except to the extent prohibited by statute\nor regulation, such description must be sufficiently detailed for a\nrecipient of ordinary skill to be able to understand it.\n\n5. Termination\n--------------\n\n5.1. The rights granted under this License will terminate automatically\nif You fail to comply with any of its terms. However, if You become\ncompliant, then the rights granted under this License from a particular\nContributor are reinstated (a) provisionally, unless and until such\nContributor explicitly and finally terminates Your grants, and (b) on an\nongoing basis, if such Contributor fails to notify You of the\nnon-compliance by some reasonable means prior to 60 days after You have\ncome back into compliance. Moreover, Your grants from a particular\nContributor are reinstated on an ongoing basis if such Contributor\nnotifies You of the non-compliance by some reasonable means, this is the\nfirst time You have received notice of non-compliance with this License\nfrom such Contributor, and You become compliant prior to 30 days after\nYour receipt of the notice.\n\n5.2. If You initiate litigation against any entity by asserting a patent\ninfringement claim (excluding declaratory judgment actions,\ncounter-claims, and cross-claims) alleging that a Contributor Version\ndirectly or indirectly infringes any patent, then the rights granted to\nYou by any and all Contributors for the Covered Software under Section\n2.1 of this License shall terminate.\n\n5.3. In the event of termination under Sections 5.1 or 5.2 above, all\nend user license agreements (excluding distributors and resellers) which\nhave been validly granted by You or Your distributors under this License\nprior to termination shall survive termination.\n\n************************************************************************\n*                                                                      *\n*  6. Disclaimer of Warranty                                           *\n*  -------------------------                                           *\n*                                                                      *\n*  Covered Software is provided under this License on an \"as is\"       *\n*  basis, without warranty of any kind, either expressed, implied, or  *\n*  statutory, including, without limitation, warranties that the       *\n*  Covered Software is free of defects, merchantable, fit for a        *\n*  particular purpose or non-infringing. The entire risk as to the     *\n*  quality and performance of the Covered Software is with You.        *\n*  Should any Covered Software prove defective in any respect, You     *\n*  (not any Contributor) assume the cost of any necessary servicing,   *\n*  repair, or correction. This disclaimer of warranty constitutes an   *\n*  essential part of this License. No use of any Covered Software is   *\n*  authorized under this License except under this disclaimer.         *\n*                                                                      *\n************************************************************************\n\n************************************************************************\n*                                                                      *\n*  7. Limitation of Liability                                          *\n*  --------------------------                                          *\n*                                                                      *\n*  Under no circumstances and under no legal theory, whether tort      *\n*  (including negligence), contract, or otherwise, shall any           *\n*  Contributor, or anyone who distributes Covered Software as          *\n*  permitted above, be liable to You for any direct, indirect,         *\n*  special, incidental, or consequential damages of any character      *\n*  including, without limitation, damages for lost profits, loss of    *\n*  goodwill, work stoppage, computer failure or malfunction, or any    *\n*  and all other commercial damages or losses, even if such party      *\n*  shall have been informed of the possibility of such damages. This   *\n*  limitation of liability shall not apply to liability for death or   *\n*  personal injury resulting from such party's negligence to the       *\n*  extent applicable law prohibits such limitation. Some               *\n*  jurisdictions do not allow the exclusion or limitation of           *\n*  incidental or consequential damages, so this exclusion and          *\n*  limitation may not apply to You.                                    *\n*                                                                      *\n************************************************************************\n\n8. Litigation\n-------------\n\nAny litigation relating to this License may be brought only in the\ncourts of a jurisdiction where the defendant maintains its principal\nplace of business and such litigation shall be governed by laws of that\njurisdiction, without reference to its conflict-of-law provisions.\nNothing in this Section shall prevent a party's ability to bring\ncross-claims or counter-claims.\n\n9. Miscellaneous\n----------------\n\nThis License represents the complete agreement concerning the subject\nmatter hereof. If any provision of this License is held to be\nunenforceable, such provision shall be reformed only to the extent\nnecessary to make it enforceable. Any law or regulation which provides\nthat the language of a contract shall be construed against the drafter\nshall not be used to construe this License against a Contributor.\n\n10. Versions of the License\n---------------------------\n\n10.1. New Versions\n\nMozilla Foundation is the license steward. Except as provided in Section\n10.3, no one other than the license steward has the right to modify or\npublish new versions of this License. Each version will be given a\ndistinguishing version number.\n\n10.2. Effect of New Versions\n\nYou may distribute the Covered Software under the terms of the version\nof the License under which You originally received the Covered Software,\nor under the terms of any subsequent version published by the license\nsteward.\n\n10.3. Modified Versions\n\nIf you create software not governed by this License, and you want to\ncreate a new license for such software, you may create and use a\nmodified version of this License if you rename the license and remove\nany references to the name of the license steward (except to note that\nsuch modified license differs from this License).\n\n10.4. Distributing Source Code Form that is Incompatible With Secondary\nLicenses\n\nIf You choose to distribute Source Code Form that is Incompatible With\nSecondary Licenses under the terms of this version of the License, the\nnotice described in Exhibit B of this License must be attached.\n\nExhibit A - Source Code Form License Notice\n-------------------------------------------\n\n  This Source Code Form is subject to the terms of the Mozilla Public\n  License, v. 2.0. If a copy of the MPL was not distributed with this\n  file, You can obtain one at http://mozilla.org/MPL/2.0/.\n\nIf it is not possible or desirable to put the notice in a particular\nfile, then You may include the notice in a location (such as a LICENSE\nfile in a relevant directory) where a recipient would be likely to look\nfor such a notice.\n\nYou may add additional accurate notices of copyright ownership.\n\nExhibit B - \"Incompatible With Secondary Licenses\" Notice\n---------------------------------------------------------\n\n  This Source Code Form is \"Incompatible With Secondary Licenses\", as\n  defined by the Mozilla Public License, v. 2.0.\n"
  },
  {
    "path": "README.md",
    "content": "# dev-sidecar\n\n开发者边车，命名取自service-mesh的service-sidecar，意为为开发者打辅助的边车工具（以下简称ds）\n通过本地代理的方式将https请求代理到一些国内的加速通道上\n\n<a href='https://github.com/docmirror/dev-sidecar'><img alt=\"GitHub stars\" src=\"https://img.shields.io/github/stars/docmirror/dev-sidecar?logo=github\"></a>\n\n[![Star History Chart](https://api.star-history.com/svg?repos=docmirror/dev-sidecar&type=date&legend=top-left)](https://www.star-history.com/#docmirror/dev-sidecar&type=date&legend=top-left)\n\n> Gitee上的同步项目已被封禁，请认准本项目唯一官方仓库地址[https://github.com/docmirror/dev-sidecar](https://github.com/docmirror/dev-sidecar) 【狗头保命】\n>\n> 我将继续奋战在开源一线，为社区贡献更多更好的开源项目。\n> \n> 感兴趣的可以关注我的主页 [【github】](https://github.com/greper) [【gitee】](https://gitee.com/greper)\n\n## 打个广告\n\n> [https://github.com/certd/certd](https://github.com/certd/certd)\n> \n> 我的开源证书管理工具项目，全自动申请和部署证书，有需求的可以去试试，帮忙点个star\n\n## 重要提醒\n\n> ------------------------------重要提醒1---------------------------------\n>\n> 注意：由于electron无法监听windows的关机事件，开着ds情况下直接重启电脑，会导致无法上网，你可以手动启动ds即可恢复网络，你也可以将ds设置为开机自启。\n>\n> 关于此问题的更多讨论请前往：[https://github.com/docmirror/dev-sidecar/issues/109](https://github.com/docmirror/dev-sidecar/issues/109)\n>\n> 注：此问题已在 `1.8.9` 版本中得到解决。\n\n> ------------------------------重要提醒2---------------------------------\n>\n> 注意：本应用启动会自动修改系统代理，所以会与其他代理软件有冲突，一起使用时请谨慎使用。\n> \n> 与Watt Toolkit（原Steam++）共用时，请以hosts模式启动Watt Toolkit\n> \n> 与TUN网卡模式运行的游戏加速器可以共用\n> \n> 本应用主要目的在于直连访问github，如果你已经有飞机了，那建议还是不要用这个自行车（ds）了\n\n## 一、 特性\n\n### 1.1、 dns优选（解决\\*\\*\\*污染问题）\n\n- 根据网络状况智能解析最佳域名ip地址，获取最佳网络速度\n- 解决一些网站和库无法访问或访问速度慢的问题\n- 建议遇到打开比较慢的国外网站，可以优先尝试将该域名添加到dns设置中（注意：被\\*\\*\\*封杀的无效）\n\n### 1.2、 请求拦截\n\n- 拦截打不开的网站，代理到加速镜像站点上去。\n- 可配置多个镜像站作为备份\n- 具备测速机制，当访问失败或超时之后，自动切换到备用站点，使得目标服务高可用\n\n### 1.3、 github加速\n\n- github 直连加速 (通过修改sni实现，感谢 [fastGithub](https://github.com/dotnetcore/FastGithub) 提供的思路)\n- release、source、zip下载加速\n- clone 加速\n- 头像加速\n- 解决readme中图片引用无法加载的问题\n- gist.github.com 加速\n- 解决git push 偶尔失败需要输入账号密码的问题（fatal: TaskCanceledException encountered / fatal: HttpRequestException encountered）\n- raw/blame加速\n\n> 以上部分功能通过 `X.I.U` 的油猴脚本实现， 以下是仓库和脚本下载链接，大家可以去支持一下。\n>\n> - [https://github.com/XIU2/UserScript](https://github.com/XIU2/UserScript)\n> \n> - [https://greasyfork.org/scripts/412245](https://greasyfork.org/scripts/412245)\n>\n> 由于此脚本在ds中是打包在本地的，更新会不及时，你可以直接通过浏览器安装油猴插件使用此脚本，从而获得最新更新（ds本地的可以通过 `加速服务->基本设置->启用脚本` 进行关闭）。\n\n### 1.4、 Stack Overflow 加速\n\n- 将ajax.google.com代理到加速CDN上\n- recaptcha 图片验证码加速\n\n### 1.5、 npm加速\n\n- 支持开启npm代理\n- 官方与淘宝npm registry一键切换\n- 某些npm install的时候，并且使用cnpm也无法安装时，可以尝试开启npm代理再试\n\n**_安全警告_**：\n\n- 请勿使用来源不明的服务/远程配置地址，有隐私和账号泄露风险\n- 本应用及服务/默认远程配置端承诺不收集任何信息。介意者请使用安全模式。\n\n## 二、快速开始\n\n支持windows、Mac、Linux(Ubuntu)\n\n### 2.1、DevSidecar桌面应用\n\n#### 1）下载安装包\n\n- release下载\n  [Github Release](https://github.com/docmirror/dev-sidecar/releases)\n\n> Windows: 请选择DevSidecar-x.x.x-windows-universal.exe \n> \n> Mac: 请选择DevSidecar-x.x.x-macos-universal.dmg \n> \n> Debian系及其他支持deb安装包的Linux: 请选择DevSidecar-x.x.x-linux-[架构].deb \n> \n> 其他Linux: 请选择DevSidecar-x.x.x-linux-[架构].AppImage (未做测试，不保证能用)\n\n> linux安装说明请参考 [linux安装文档](./doc/linux.md)\n\n> 注意：由于没有买应用证书，所以应用在下载安装时会有“未知发行者”等安全提示，选择保留即可。\n\n#### 2）安装后打开\n\n界面应大致如下图所示：\n> 注意：mac版安装需要在“系统偏好设置->安全性与隐私->通用”中解锁并允许应用安装\n\n![](./doc/index.png)\n\n#### 3）安装根证书\n\n第一次打开会提示安装证书，根据提示操作即可\n\n更多有关根证书的说明，请参考 [为什么要安装根证书?](./doc/caroot.md)\n\n> 根证书是本地随机生成的，所以不用担心根证书的安全问题（本应用不收集任何用户信息）\n> \n> 你也可以在加速服务设置中自定义根证书（PEM格式的证书与私钥）\n\n> 火狐浏览器需要[手动安装证书](#3火狐浏览器火狐浏览器不走系统的根证书需要在选项中添加根证书)\n\n#### 4）开始加速吧\n\n去试试打开github、huggingface、docker hub吧\n\n### 2.2、开启前 vs 开启后\n\n|          | 开启前                         | 开启后                                           |\n| -------- | ------------------------------ | ----------------------------------------------- |\n| 头像     | ![](./doc/avatar2.png)         | ![](./doc/avatar1.png)                          |\n| clone    | ![](./doc/clone-before.png)    | ![](./doc/clone.png)                            |\n| zip 下载 | ![](./doc/download-before.png) | ![](./doc/download.png)秒下的，实在截不到速度的图 |\n\n## 三、模式说明\n\n### 3.1、安全模式\n\n- 此模式：关闭拦截、关闭增强、不使用远程配置、开启dns优选、开启测速\n- 最安全，无需安装证书，可以在浏览器地址栏左侧查看域名证书\n- 功能也最弱，只有特性1，相当于查询github的国外ip，手动改hosts一个意思。\n- github的可访问性不稳定，取决于IP测速，如果有绿色ip存在，就 `有可能` 可以直连访问。\n  ![](./doc/speed.png)\n\n### 3.2、默认模式\n\n- 此模式：开启拦截、关闭增强、使用远程配置、开启dns优选、开启测速\n- 需要安装证书，通过修改sni直连访问github\n- 功能上包含特性1/2/3/4。\n\n## 四、 最佳实践\n\n- 把dev-sidecar一直开着就行了\n- 建议遇到打开比较慢的国外网站，可以尝试将该域名添加到dns设置中（注意：被\\*\\*\\*封杀的无效）\n\n### 其他加速\n\n#### 1）git clone 加速\n\n- 方式1：快捷复制：\n\n  > 开启脚本支持，然后在复制clone链接下方，即可复制到加速链接\n\n- 方式2：\n\n  > 1. 使用方式：用实际的名称替换 `{}` 的内容，即可加速clone [https://hub.fastgit.org/{username}/{reponame}.git](https://hub.fastgit.org/%7Busername%7D/%7Breponame%7D.git)\n  > 2. clone 出来的 remote \"origin\" 为fastgit的地址，需要手动改回来\n  > 3. 你也可以直接使用他们的clone加速工具 [fgit-go](https://github.com/FastGitORG/fgit-go)\n\n#### 2）`github.com` 的镜像网站（注意：部分镜像网站不能登录）\n\n> 1. [hub.fastgit.org](https://hub.fastgit.org/) （2024/11/18：这个好像失效了？）\n> 2. [github.com.cnpmjs.org](https://github.com.cnpmjs.org/) 这个很容易超限（2024/11/18：这个好像失效了？）\n> 3. [dgithub.xyz](https://dgithub.xyz/)\n\n## 五、api\n\n### 5.1、拦截配置\n\n没有配置域名的不会拦截，其他根据配置进行拦截处理。\n\n在【加速服务-拦截设置】中配置，格式如下：（更多内容参见[wiki](https://github.com/docmirror/dev-sidecar/wiki/%E5%8A%A0%E9%80%9F%E6%9C%8D%E5%8A%A1%E4%BD%BF%E7%94%A8%E8%AF%B4%E6%98%8E)）\n\n```jsonc\n{\n  // 要拦截的域名\n  'github.com': {\n    // 需要拦截url的正则表达式\n    '/.*/.*/releases/download/': {\n      // 拦截类型\n      // redirect: url,          // 临时重定向（url会变，一些下载资源可以通过此方式配置）\n      // proxy: url,             // 代理（url不会变，没有跨域问题）\n      // abort: true,            // 取消请求（适用于被***封锁的资源，找不到替代，直接取消请求，快速失败，节省时间）\n      // success: true,          // 直接返回成功请求（某些请求不想发出去，可以伪装成功返回）\n      // cacheDays: 1,           // GET请求的使用缓存，单位：天（常用于一些静态资源）\n      // options: true,          // OPTIONS请求直接返回成功请求（该功能存在一定风险，请谨慎使用）\n      // optionsMaxAge: 2592000, // OPTIONS请求缓存时间，默认：2592000（一个月）\n      redirect: 'download.fastgit.org'\n    },\n    '.*': {\n      proxy: 'github.com',\n      sni: 'baidu.com' // 修改sni，规避***握手拦截\n    }\n  },\n  'ajax.googleapis.com': {\n    '.*': {\n      proxy: 'ajax.loli.net', // 代理请求，url不会变\n      backup: ['ajax.proxy.ustclug.org'], // 备份，当前代理请求失败后，将会切换到备用地址\n      test: 'ajax.googleapis.com/ajax/libs/jquery/1.12.4/jquery.min.js',\n      replace: '/(.*)/xxx'// 当加速地址的链接和原链接不是完全相同时，可以通过正则表达式replace，此时proxy通过$1$2来重组url， proxy:'ajax.loli.net/xxx/$1'\n    }\n  },\n  'clients*.google.com': {\n    '.*': {\n      abort: true // 取消请求，被***封锁的资源，找不到替代，直接取消请求，快速失败，节省时间\n    }\n  }\n}\n```\n\n### 5.2、DNS优选配置\n\n某些域名解析出来的ip会无法访问，（比如api.github.com会被解析到新加坡的ip上，新加坡的服务器在上午挺好，到了晚上就卡死，基本不可用）\n\n通过从dns上获取ip列表，切换不同的ip进行尝试，最终会挑选到一个最快的ip（该功能需要事先配置好所用DNS），更多说明参见[wiki](https://github.com/docmirror/dev-sidecar/wiki/%E5%8A%A0%E9%80%9F%E6%9C%8D%E5%8A%A1%E4%BD%BF%E7%94%A8%E8%AF%B4%E6%98%8E)\n\n```json\n{\n  \"dns\": {\n    \"mapping\": {\n      \"api.github.com\": \"cloudflare\", // \"解决push的时候需要输入密码的问题\",\n      \"gist.github.com\": \"cloudflare\", // 解决gist无法访问的问题\n      \"*.githubusercontent.com\": \"cloudflare\" // 解决github头像经常下载不到的问题\n    }\n  }\n}\n```\n\n注意：暂时只支持IPv4的解析\n\n## 六、问题排查\n\n### 6.1、dev-sidecar的前两个开关没有处于打开状态\n\n1. 尝试将开关按钮手动打开\n2. 请尝试右键dev-sidecar图标，点退出。再重新打开\n3. 如果还不行，请将日志发送给作者\n\n如果是mac系统，可能是下面的原因\n\n#### 1）Mac系统使用时，首页的系统代理开关无法打开\n\n出现这个问题可能是没有开启系统代理命令的执行权限\n\n```\nnetworksetup -setwebproxy 'WiFi' 127.0.0.1 31181\n#看是否有如下错误提示\n** Error: Command requires admin privileges.\n```\n\n如果有上面的错误提示，请尝试如下方法：\n\n> 取消访问偏好设置需要管理员密码\n> \n> 系统偏好设置—>安全性与隐私—> 通用—> 高级—> 访问系统范围的偏好设置需要输入管理员密码（取消勾选）\n\n### 6.2、没有加速效果\n\n1. 本应用默认仅开启https加速，一般足够覆盖需求。\n    如果你访问的是仅支持http协议的网站，请手动在【系统代理】中打开【代理HTTP请求】\n2. 检查浏览器是否装了什么插件，与ds有冲突\n3. 检查是否安装了其他代理软件，与ds有冲突\n4. 请确认浏览器的代理设置为使用IE代理/或者使用系统代理状态\n5. 可以尝试换个浏览器试试\n6. 请确认网络代理设置处于勾选状态\n   正常情况下ds在“系统代理”开关打开时，会自动设置系统代理。\n\n### 6.3、浏览器打开提示证书不受信任\n\n![](./doc/crt-error.png)\n\n一般是证书安装位置不对，重新安装根证书后，重启浏览器\n\n#### 1）windows: 请确认证书已正确安装在“本地计算机-将所有的证书都放入下列存储：受信任的根证书颁发机构”下\n\n#### 2）mac: 请确认证书已经被安装并已经设置信任\n\n#### 3）火狐浏览器：火狐浏览器不走系统的根证书，需要在选项中添加根证书\n\n1. 火狐浏览器->选项->隐私与安全->证书->查看证书\n![](./doc/figures/Firefox/1.png)\n2. 证书颁发机构->导入\n3. 选择证书文件 `C:\\Users(用户)\\Administrator(你的账号)\\.dev-sidecar\\dev-sidecar.ca.crt`（Mac或linux为 `~/.dev-sidecar` 目录）\n![](./doc/figures/Firefox/2.png)\n4. 勾选信任由此证书颁发机构来标识网站，确定即可\n![](./doc/figures/Firefox/3.png)\n\n### 6.4、打开github显示连接超时\n\n```html\nDevSidecar Warning: Error: www.github.com:443, 代理请求超时\n```\n\n1. 检查测速界面github.com是否有ip ，如果没有ip，则可能是由于你的网络提供商封锁了dns服务商的ip（试试能否ping通：1.1.1.1 / 9.9.9.9 ）\n2. 如果是安全模式，则是因为不稳定导致的，等一会再刷新试试\n3. 如果是增强模式，则是由于访问人数过多，正常现象\n\n### 6.5、查看日志是否有报错\n\n如果还是不行，请在下方加官方QQ群或提issue，附上服务日志（server.log）以便进行分析\n\n日志打开方式：加速服务->右边日志按钮->打开日志文件夹\n\n![](./doc/log.png)\n\n### 6.6、某些原本可以打开的网站打不开了\n\n1. 可以尝试关闭pac\n2. 可以将域名加入白名单\n\n### 6.7、应用意外关闭导致没有网络了\n\n应用开启后会自动修改系统代理设置，正常退出会自动关闭系统代理\n当应用意外关闭时，可能会因为没有将系统代理恢复，从而导致完全无法上网。\n\n对于此问题有如下几种解决方案可供选择：\n\n1. 重新打开应用即可（右键应用托盘图标可完全退出，将会正常关闭系统代理设置）\n2. 如果应用被卸载了，此时需要[手动关闭系统代理设置](./doc/recover.md)\n3. 如果你是因为开着ds的情况下重启电脑导致无法上网，你可以设置ds为开机自启\n\n### 6.8、卸载应用后上不了网，git请求不了\n\n如果你在卸载应用前，没有正常退出app，就有可能无法上网。请按如下步骤操作恢复您的网络：\n\n1、关闭系统代理设置，参见：[手动关闭系统代理设置](./doc/recover.md)\n2、执行下面的命令关闭git的代理设置（如果你开启过 `Git.exe代理` 的开关）\n\n```shell\ngit config --global --unset http.proxy\ngit config --global --unset https.proxy\ngit config --global --unset http.sslVerify\n```\n\n3、执行下面的命令关闭npm的代理设置（如果你开启过npm加速的开关）\n\n```shell\nnpm config delete proxy\nnpm config delete https-proxy\n```\n\n### 6.9、其他问题\n\n请查阅[wiki](https://github.com/docmirror/dev-sidecar/wiki)\n\n也可以查阅[有文档tag的issue](https://github.com/docmirror/dev-sidecar/issues?q=is%3Aissue%20label%3ADocumentation)，它们被开发者认证为相当于文档级别的参考issue。\n\n## 七、在其他程序使用\n\n- [java程序使用](./doc/other.md#Java程序使用)\n\n## 八、贡献代码\n\n### 8.1、准备环境\n\n#### 1）安装 `nodejs`\n\n推荐安装 nodejs `22.x.x` 的版本，其他版本未做测试\n\n#### 2）安装 `pnpm`\n\n运行如下命令即可安装所需依赖：\n\n```shell\nnpm install -g pnpm --registry=https://registry.npmmirror.com\n\n```\n\n### 8.2、开发调试模式启动\n\n运行如下命令即可开发模式启动\n\n```shell\n# 拉取代码\ngit clone https://github.com/docmirror/dev-sidecar\n\ncd dev-sidecar\n\n# 注意不要使用 `npm install` 来安装依赖，因为 `pnpm` 会自动安装依赖\npnpm install\n\n# 运行DevSidecar\ncd packages/gui\nnpm run electron\n\n```\n\n> 如果electron依赖包下载不动，可以开启ds的npm加速\n\n### 8.3、打包成可执行文件\n\n```shell\n# 先执行上面的步骤，然后运行如下命令打包成可执行文件\nnpm run electron:build\n```\n\n### 8.4、提交pr\n\n如果你想将你的修改贡献出来，请提交pr\n\n## 九、联系作者\n\n欢迎bug反馈，需求建议，技术交流等\n\n加官方QQ群（请备注dev-sidecar，或简称DS）\n\n- QQ 1群：390691483，人数：499 / 500（满）\n- QQ 2群：667666069，人数：500 / 500（满）\n- QQ 3群：419807815，人数：493 / 500（满）\n- QQ 4群：[438148299](http://qm.qq.com/cgi-bin/qm/qr?_wv=1027&k=i_NCBB5f_Bkm2JsEV1tLs2TkQ79UlCID&authKey=nMsVJbJ6P%2FGNO7Q6vsVUadXRKnULUURwR8zvUZJnP3IgzhHYPhYdcBCHvoOh8vYr&noverify=0&group_code=438148299)，人数：700 / 1000\n- QQ 5群：[767622917](http://qm.qq.com/cgi-bin/qm/qr?_wv=1027&k=nAWi_Rxj7mM4Unp5LMiatmUWhGimtbcB&authKey=aswmlWGjbt3GIWXtvjB2GJqqAKuv7hWjk6UBs3MTb%2Biyvr%2Fsbb1kA9CjF6sK7Hgg&noverify=0&group_code=767622917)，人数：200 / 200（new）\n\n## 十、求star\n\n我的其他项目求star\n\n- [fast-crud](https://github.com/fast-crud/fast-crud) : 开发crud快如闪电\n- [certd](https://github.com/certd/certd) : 让你的证书永不过期\n- [trident-sync](https://github.com/handsfree-work/trident-sync) : 二次开发项目同步升级工具\n\n## 十一、感谢\n\n本项目使用lerna包管理工具\n\n[![lerna](https://img.shields.io/badge/maintained%20with-lerna-cc00ff.svg)](https://lerna.js.org/)\n\n本项目参考如下开源项目\n\n- [node-mitmproxy](https://github.com/wuchangming/node-mitmproxy)\n- [ReplaceGoogleCDN](https://github.com/justjavac/ReplaceGoogleCDN)\n\n特别感谢\n\n- [github增强油猴脚本](https://greasyfork.org/zh-CN/scripts/412245-github-%E5%A2%9E%E5%BC%BA-%E9%AB%98%E9%80%9F%E4%B8%8B%E8%BD%BD) 本项目部分加速功能完全复制该脚本。\n- [中国域名白名单](https://github.com/pluwen/china-domain-allowlist)，本项目的系统代理排除域名功能中，使用了该白名单。\n\n本项目部分加速资源由如下组织提供\n\n- [FastGit UK](https://fastgit.org/)\n"
  },
  {
    "path": "_script/0、updateDependencies.bat",
    "content": "node -v\n\n# 安装ncu\n# npm install -g npm-check-updates\n\ncd ../packages/core\nncu -u\n\n# cd ../packages/gui\n# ncu -u\n\n# cd ../packages/mitmproxy\n# ncu -u\n"
  },
  {
    "path": "_script/1、setupEnv.bat",
    "content": "node -v\n\ncd ../\nnpm install -g pnpm --registry=https://registry.npmmirror.com\n"
  },
  {
    "path": "_script/2、installProject.bat",
    "content": "node -v\n\ncd ../\nchcp 65001\npnpm install\n"
  },
  {
    "path": "_script/3、buildAndRun.bat",
    "content": "node -v\n\ncd ../packages/gui\nchcp 65001\nnpm run electron\n"
  },
  {
    "path": "_script/4.1、runTestCore.bat",
    "content": "node -v\n\ncd ../packages/core\npnpm run test\n"
  },
  {
    "path": "_script/4.2、runTestMitmproxy.bat",
    "content": "node -v\n\ncd ../packages/mitmproxy\npnpm run test\n"
  },
  {
    "path": "_script/5、generateSetupFile.bat",
    "content": "node -v\n\ncd ../packages/gui\n\nif not exist \"dist_electron\" mkdir \"dist_electron\"\nstart dist_electron\n\nnpm run electron:build\n"
  },
  {
    "path": "doc/caroot.md",
    "content": "# 关于信任根证书的说明\n\n## 一、为什么要信任根证书。\n\n要回答这个问题需要先掌握下面两个知识点\n\n### 知识点1：什么是根证书\n\n[百度百科-什么是根证书](https://baike.baidu.com/item/%E6%A0%B9%E8%AF%81%E4%B9%A6/9874620?fr=aladdin)\n\n当访问目标网站是https协议时，服务器会发送一个由根证书签发的网站ssl证书给浏览器，让浏览器用这个ssl证书给数据加密。\n浏览器需要先验证这个证书的真伪，之后才会使用证书加密。\n证书的真伪是通过验证证书的签发机构的证书是否可信，一直追溯到最初始的签发机构的证书（根证书）。\n浏览器只需信任根证书，间接的就信任了这条证书链下签发的所有证书。\n\nwindows、mac、linux或者浏览器他们都内置了市面上可信的大型证书颁发机构的根证书。\n\n### 知识点2：中间人攻击\n\n本应用的实现原理如下图：\n\n![](./flow.jpg)\n\n> 简单来说就是DevSidecar在本地启动了一个代理服务器帮你访问目标网站。\n> 实际上就是 [中间人攻击](https://baike.baidu.com/item/%E4%B8%AD%E9%97%B4%E4%BA%BA%E6%94%BB%E5%87%BB/1739730?fr=aladdin) 的原理，只是本应用没有用它来干坏事，而是帮助开发者加速目标网站的访问。\n\n### 现在可以回答为什么要信任根证书\n\n当目标网站不需要加速拦截时，直接走TCP转发，不需要中间人攻击，没有安全风险，在此不多做讨论。\n\n当目标网站需要拦截时（例如github），就需要通过中间人攻击修改请求或者请求其他替代网站，从而达到加速的目的。\n\n例如加速github就需要修改如下几处\n\n1. 直连访问github需要修改tls握手时的sni域名，规避\\*\\*\\*的sni阻断问题。\n2. asserts.github.com等静态资源拦截替换成fastgit.org的镜像地址\n\nDevSidecar在第一次启动时会在本地随机生成一份根证书，当有用户访问github时，就用这份根证书来签发一份假的叫github.com的证书。\n如果浏览器事先信任了这份根证书，那么就可以正常访问DevSidecar返回的网页内容了。\n\n## 二、信任根证书有安全风险吗\n\n1. 根证书是DevSidecar第一次启动时本地随机生成的，除了你这台电脑没人知道这份根证书的内容。\n2. 代理请求目标网站时会校验目标网站的证书（除非关闭了`代理校验ssl`）。\n\n> 两段链路都是安全的，所以信任根证书没有问题。\n> 但如果应用本身来源不明，或者`拦截配置`里的替代网站作恶，则有安全风险。\n\n> 对于应用来源风险：\n> 请勿从未知网站下载DevSidecar应用，认准官方版本发布地址\n> [Github Release](https://github.com/docmirror/dev-sidecar/releases)\n>\n> 或者从源码自行编译安装\n\n> 对于拦截配置里的替代网站风险：\n>\n> 1. 尽量缩小替代配置的范围\n> 2. 不使用来源不明的镜像地址，尽量使用知名度较高的镜像地址\n> 3. 你甚至可以将其他拦截配置全部删除，只保留github相关配置\n"
  },
  {
    "path": "doc/linux.md",
    "content": "# Linux 支持\n\n`Linux`使用说明，目前仅官方支持`Ubuntu x86_64 GNOME桌面版（原版）`，其他`Linux`未测试\n\n> 注意：需要开启 [sudo 免密支持](https://www.jianshu.com/p/5d02428f313d)，否则请自行安装证书\n\n## 一、安装\n\n### 1.1. Ubuntu / Debian或其衍生版（未测试）\n\n- 下载`DevSidecar-x.x.x.deb`\n- 使用 root 执行命令安装 `dpkg -i DevSidecar-x.x.x.deb`\n- 去应用列表里面找到 dev-sidecar 应用，打开即可\n\n### 1.2. 其他基于glibc的Linux系统（未测试）\n\n- 下载 `DevSidecar-x.x.x.AppImage`\n- 设置可执行权限 `chmod +x DevSidecar-x.x.x.AppImage`\n- 双击运行\n\n### 1.3. 特殊的Linux系统（如Alpine和Chimera Linux）\n\n> 此处默认用户有较专业的Linux知识，故不详细描述，请参考并自行试验\n- 创建Debian（最方便且省空间）容器，可使用distrobox（推荐），接下来以此为例说明\n- 下载deb包并在容器内安装\n- 穿透系统设置：\n    在容器内 `/usr/bin/gsettings` 文件写入：\n\n    ```bash\n    #!/bin/sh\n    distrobox-host-exec gsettings \"$@\"\n    ```\n    并设置可执行权限\n\n    简化版命令（请在容器内执行）:\n    ```\n    echo -e '#!/bin/sh\\ndistrobox-host-exec gsettings \"$@\"' >/usr/bin/gsettings\n    ```\n- 使用命令启动应用，使用“自动安装证书”功能，回到终端，找到输出里含有 `sudo` 的两句命令，复制到主系统执行，如失败（或使用其他证书系统），请自行安装证书，可参考 [议题 #204](https://github.com/docmirror/dev-sidecar/issues/204)\n\n### 1.4. 版本选择\n\n不同CPU架构，选择对应的版本，如果安装失败，请下载 `universal` 版本\n\n\n## 二、证书安装\n\n默认模式和增强模式需要系统信任CA证书。\n由于Linux上火狐和Chrome都不走系统证书，所以除了安装系统证书之外，还需要给浏览器安装证书\n\n### 2.1. 系统证书安装\n\n根据弹出的提示：\n\n- 点击首页右上角“安装根证书”按钮\n- 点击“点此去安装”\n- 提示安装成功即可\n\n### 2.2. 火狐浏览器安装证书\n\n- 火狐浏览器->选项->隐私与安全->证书->查看证书\n- 证书颁发机构->导入\n- 选择证书文件在 `~/.dev-sidecar` 目录下\n- 勾选信任由此证书颁发机构来标识网站，确定即可\n\n### 2.3. Chrome浏览器安装证书\n\n证书文件目录为 `~/.dev-sidecar`\n\n![](../packages/gui/public/setup-linux.png)\n"
  },
  {
    "path": "doc/other.md",
    "content": "# 其他程序使用\n\n## Java程序使用\n\n> 由 [Enaium](https://github.com/Enaium) 提供，未做验证，可供参考\n\n1. 先通过keytool安装证书：\n\n   ```shell\n   keytool -import -alias dev-sidecar -keystore \"jdk路径\\security\\cacerts\" -file 用户目录\\.dev-sidecar\\dev-sidecar.ca.crt\n   ```\n   默认密码为 `changeit`\n\n2. 启动时还需要设置参数，例：\n\n   ```shell\n   java -Dhttp.proxyHost=localhost -Dhttp.proxyPort=31181 -Dhttps.proxyHost=localhost -Dhttps.proxyPort=31181 -jar xxxx.jar\n   ```\n\n3. Gradle还需在`用户目录/.gradle/gradle.properties`创建配置文件：\n\n    ```properties\n    systemProp.http.proxyHost=localhost\n    systemProp.http.proxyPort=31181\n    systemProp.https.proxyHost=localhost\n    systemProp.https.proxyPort=31181\n    ```\n"
  },
  {
    "path": "doc/recover.md",
    "content": "# 卸载与恢复网络\n\n由于应用启动后会自动设置系统代理，正常退出时会关闭系统代理。\n当应用意外关闭，或者未正常退出后被卸载，此时会因为系统代理没有恢复从而导致完全上不了网。\n目前electron在windows系统上无法监听系统重启事件。更多相关资料 [electron issues](https://github.com/electron/electron/pull/24261)\n\n## 恢复代理设置\n\n### 1、windows 代理关闭\n\n如何打开查看windows代理设置：\n\n- win10: 开始->设置->网络和Internet->最下方代理\n- win7: 开始->控制面板->网络和Internet->网络和共享中心->左下角Internet选项->连接选项卡->局域网设置\n\n![windows](./proxy.png)\n\n### 2、mac 代理关闭\n\n网络->网卡->代理->去掉http和https的两个勾\n\n![](./mac-proxy.png)\n\n### 3、Linux（Ubuntu）\n\n网络->代理->选择禁用\n"
  },
  {
    "path": "doc/wiki/Home.md",
    "content": "> **给作者打个广告：**<br>\n> [https://github.com/certd/certd](https://github.com/certd/certd) 我的开源证书管理工具项目，全自动申请和部署证书，有需求的可以去试试，帮忙点个star\n\n> 注：Wiki还在完善中，敬请期待更多内容。<br>\n> 说明：以下文档均以最新版本进行编写，请下载最新版DS后，再参考以下文档使用！<br>\n\n# 一、下载安装：\n\n访问 https://github.com/docmirror/dev-sidecar/releases 页面，下载对应操作系统的安装程序进行安装。\n\n如安装有问题，请查看 [各平台安装说明](https://github.com/docmirror/dev-sidecar/wiki/%E5%90%84%E5%B9%B3%E5%8F%B0%E5%AE%89%E8%A3%85%E8%AF%B4%E6%98%8E)\n\n# 二、功能使用说明：\n\n1. [`加速服务`使用说明](https://github.com/docmirror/dev-sidecar/wiki/%E5%8A%A0%E9%80%9F%E6%9C%8D%E5%8A%A1%E4%BD%BF%E7%94%A8%E8%AF%B4%E6%98%8E)\n2. 系统代理使用说明：\n3. 通用功能使用说明：\n    1. 开机自启动：\n    2. 远程配置：\n    3. 主题设置：\n    4. 窗口设置：\n    5. 检查更新：\n4. 应用使用说明：\n    1. NPM加速：\n    2. Git.exe加速：\n    3. PIP加速：\n    4. 彩蛋（功能增强）：\n5. 帮助中心\n6. 反馈问题\n\n# 三、解决问题：\n\n1. [解决Github访问不了或速度很慢的问题](https://github.com/docmirror/dev-sidecar/wiki/%E8%A7%A3%E5%86%B3Github%E8%AE%BF%E9%97%AE%E4%B8%8D%E4%BA%86%E6%88%96%E9%80%9F%E5%BA%A6%E5%BE%88%E6%85%A2%E7%9A%84%E9%97%AE%E9%A2%98)\n2. [Linux安装证书失败的避坑](https://github.com/docmirror/dev-sidecar/issues/238)\n3. [解决Linux（deb）系统下无法安装根证书的问题](https://github.com/docmirror/dev-sidecar/issues/135)\n4. [在Arch/Fedora下的证书安装](https://github.com/docmirror/dev-sidecar/issues/204)\n5. [Mac安装：`无法打开“dev-sidecar”，因为无法验证开发者。` 的解决方案](https://github.com/docmirror/dev-sidecar/issues/147)\n6. [在 WSL 中的使用方法](https://github.com/docmirror/dev-sidecar/issues/73)\n\n[> 点击前往Issue区查找更多帮助信息](https://github.com/docmirror/dev-sidecar/issues)\n\n# 四、DevSidecar技术交流群\n\n- QQ 1群：[390691483](http://qm.qq.com/cgi-bin/qm/qr?_wv=1027&k=hIG_VClE1CU2gHuLSSTaazMlo6M760iL&authKey=5VUMMwzH5FeabLDbZNZJbqmZk1gfmB%2B%2FlotO%2Brszz%2BW3E8xwKD2hTg2%2FV2LJEKL7&noverify=0&group_code=390691483)，人数：496 / 500\n- QQ 2群：[667666069](http://qm.qq.com/cgi-bin/qm/qr?_wv=1027&k=n4nksr4sji93vZtD5e8YEHRT6qbh6VyQ&authKey=XKBZnzmoiJrAFyOT4V%2BCrgX5c13ds59b84g%2FVRhXAIQd%2FlAiilsuwDRGWJct%2B570&noverify=0&group_code=667666069)，人数：488 / 500\n- QQ 3群：[419807815](http://qm.qq.com/cgi-bin/qm/qr?_wv=1027&k=zRkm0eHUhRmWWJA5O35C7BOKPZ4_gmrz&authKey=X9JHezR1BOalcEmvV6If04TN%2BIbzjAayBDaOSiuOg1SPpPguA7RqoLSHVEeo7A4e&noverify=0&group_code=419807815)，人数：494 / 500\n- QQ 4群：[438148299](http://qm.qq.com/cgi-bin/qm/qr?_wv=1027&k=i_NCBB5f_Bkm2JsEV1tLs2TkQ79UlCID&authKey=nMsVJbJ6P%2FGNO7Q6vsVUadXRKnULUURwR8zvUZJnP3IgzhHYPhYdcBCHvoOh8vYr&noverify=0&group_code=438148299)，人数：295 / 1000\n- QQ 5群：[767622917](http://qm.qq.com/cgi-bin/qm/qr?_wv=1027&k=nAWi_Rxj7mM4Unp5LMiatmUWhGimtbcB&authKey=aswmlWGjbt3GIWXtvjB2GJqqAKuv7hWjk6UBs3MTb%2Biyvr%2Fsbb1kA9CjF6sK7Hgg&noverify=0&group_code=767622917)，人数：068 / 200（new）\n\n# 五、版本更新日志\n\nhttps://github.com/docmirror/dev-sidecar/releases\n"
  },
  {
    "path": "doc/wiki/加速服务使用说明.md",
    "content": "# 1. 加速服务:\n\n1. 什么是 `加速服务`？\n\n   - `加速服务` 即 `代理服务`，它通过中间人攻击的方式，将网络请求拦截下来，并经过DNS加速、修改、重定向、代理等一系列的功能，达到加速访问、或访问原本无法访问的站点等目的。<br>\n2. 如何启动加速服务：<br>\n    - 点击首页的【代理服务】右侧的开关按钮，即可启动/关闭加速服务。<br>\n    - 点击首页的【系统代理】右侧的开关按钮，即可将dev-sidecar设置/不设置为系统默认代理。（系统只能有一个默认代理，在将dev-sidecar与其他网络辅助软件共用时请谨慎开启本开关）<br>\n    - 点击首页的【NPM加速】和【Git.exe代理】右侧的开关按钮，即可启动/关闭dev-sidecar为对应软件提供的加速服务。如果你的电脑上并未安装NPM或Git，则这两个按钮将不可用，这是正常情况。\n\n\n# 2. 根证书使用说明：\n\n1. 什么是根证书：TODO\n2. [为什么需要安装根证书这么高风险性的步骤](https://github.com/docmirror/dev-sidecar/blob/master/doc/caroot.md)\n3. 如何安装根证书：参见dev-sidecar【首页】的【安装根证书】按钮（注意Firefox浏览器还需要一次手动导入根证书）\n\n# 3. 模式：\n\n1. 安全模式：TODO\n2. 默认模式：TODO\n3. 增强模式（彩蛋）：TODO\n\n# 4. 拦截功能使用和配置说明：\n\n## 4.1. 拦截器类型：\n\n### 1）请求拦截器：\n| 请求拦截器名称     | 拦截器配置名    | 请求拦截优先级 | 作用 |\n| ----------------- | -------------- | ------------- | --------- |\n| OPTIONS请求拦截器  | options        | 101           | 直接响应200，不发送该OPTIONS请求 |\n| 快速成功拦截器     | success        | 102           | 直接响应200，不发送该请求 |\n| 快速失败拦截器     | abort          | 103           | 直接响应403，不发送该请求 |\n| 缓存请求拦截器     | cacheXxx       | 104           | 如果缓存还生效，直接响应304，不发送该请求<br>如果缓存已过期或无缓存，则发送请求<br>注：只对GET请求生效！ |\n| 重定向拦截器       | redirect       | 105           | 重定向到指定地址，直接响应302，不发送该请求 |\n| 请求篡改拦截器     | requestReplace | 111           | 篡改请求头，达到想要的目的 |\n| 代理拦截器         | proxy          | 121           | 将请求转发到指定地址 |\n| SNI拦截器         | sni            | 122           | 设置 `servername`，用于避开GFW |\n\n### 2）响应拦截器：\n| 响应拦截器名称    | 拦截器配置名     | 响应拦截优先级 | 作用 |\n| ---------------- | --------------- | ------------- | --------- |\n| OPTIONS响应拦截器 | options         | 201           | 设置跨域所需的响应头，避免被浏览器的跨域策略阻拦 |\n| 缓存响应拦截器    | cacheXxx        | 202           | 设置缓存所需的响应头，使浏览器缓存当前请求<br>注：只对GET请求生效！ |\n| 响应篡改拦截器    | responseReplace | 203           | 篡改响应头，避免被浏览器的安全策略阻拦 |\n| 脚本拦截器        | script          | 211           | 注入JavaScript脚本到页面中，如：Github油猴脚本 |\n\n## 4.2. 拦截配置说明书：\n\nTODO：内容待完善\n\n# 5. 域名白名单：\n\n选择哪些域名不会被dev-sidecar处理。\n\n**注意：** 该设置与【系统代理-自定义排除域名】的区别在于：\n\n1. 前者只是被dev-sidecar自身忽略，后者则是写入系统设置、不会被（任何的）系统代理处理，在手动修改系统代理设置时务必小心后者可能残留的作用！\n2. 在条目较多时，前者的性能不如后者，可能产生明显延迟。<br>\n\n在config.json的 `proxy.excludeIpList:object` 中设置，**该字段**格式如下：<br>\n> 注意：这里点号用来作为JSON object嵌套关系的缩写，冒号指明该条目的类型（主要用来区分object和list），并没有哪一个Object的key为 `proxy.excludeIpList`。为避免歧义，配置中object和list的key总不应包含点号。下同）\n\n```json\n{\n  \"proxy\": {\n    \"excludeIpList\": {\n      \"example1.com\": true,\n      \"example2.com\": false,\n      \"example3.com\": null,\n      \"example4.com\": {\n        \"desc1\": \"域名对应字段设置为false时会被处理，null会移除现有设置（多用于远程配置）\",\n        \"desc2\": \"其他情况下就和设置true一样，不会被处理。因而你可以像这样插入注释\",\n        \"desc3\": \"同样的技巧可以用在其他本应设置一个bool值的地方\",\n        \"desc4\": \"原则上来说config.json不支持//形式的注释，但下文为了方便阅读，还是这么写了\"\n      }\n    }\n  }\n}\n```\n\n# 6. DNS服务管理：\n\n用来配置在dev-sidecar中需要的指定DNS，出于保密和可靠起见建议使用DoH和DoT。<br>\n在 `server.dns.provider:object` 中设置，**其中的每个条目** 格式如下：\n\n## 6.1. 配置 `DNS-over-HTTPS`（简称DoH）：\n> 注：并非被所有DNS支持，但是保证只要能使用就一定匿名且可靠的DNS服务。\n\n```json\n\"cloudflare\": {\n  \"type\": \"https\", // 如果server上以\"https://\"开头指明了协议，就不需要写type了\n  \"server\": \"https://1.1.1.1/dns-query\",\n  \"cacheSize\": 1000\n}\n```\n\n## 6.2. 配置 `DNS-over-TLS`（简称DoT）：\n> 并非被所有DNS支持，但是保证只要能使用就一定匿名且可靠的DNS服务。\n\n```json\n\"cloudflareTLS\": {\n  \"type\": \"tls\", // 如果server上以\"tls://\"开头指明了协议，就不需要写type了\n  \"server\": \"1.1.1.1\",\n  \"port\": 853, // 不配置时，默认端口为：853\n  \"servername\": \"cloudflare-dns.com\", // 需要伪造成的SNI\n  //\"sni\": \"cloudflare-dns.com\", // SNI缩写配置\n  \"cacheSize\": 1000\n}\n```\n\n## 6.3. 配置 `TCP` 的DNS服务：\n> 并非被所有DNS支持，该方法既不保密也不可靠\n\n```json\n\"googleTCP\": {\n  \"type\": \"tcp\", // 如果server上以\"tcp://\"开头指明了协议，就不需要写type了\n  \"server\": \"8.8.8.8\",\n  \"port\": 53, // 不配置时，默认端口为：53\n  \"cacheSize\": 1000\n}\n```\n\n## 6.4. 配置 `UDP` 的DNS服务：\n> 所有DNS服务器均支持UDP方式，但该方法既不保密也不可靠\n\n```json\n\"google\": {\n  \"type\": \"udp\", // 如果server上以\"udp://\"开头指明了协议，就不需要写type了\n  \"server\": \"8.8.8.8\",\n  \"port\": 53, // 不配置时，默认端口为：53\n  \"cacheSize\": 1000\n}\n```\n\n# 7. DNS设置：\n\n选择哪些域名需要使用指定的DNS（需要先在【DNS服务管理】中设置）获取IP。<br>\n在config.json中的 `server.dns.mapping:key-value` 中设置，**其中的每个条目**格式如下：\n\n```json\n\"*.example.com\": \"your-dns-name\"\n```\n\n# 8. IP预设置：\n\n为一些DNS无法获取的域名手动设置ip，起到类似于hosts的作用（仅在dev-sidecar开启时生效）。<br>\n在config.json中的 `server.preSetIpList:object` 中设置，**其中的每个条目**格式如下：\n\n```json\n{\n  \"example.com\": {\n    \"1.1.1.1\": true, // 如果有多个IP，可以继续添加\n    \"1.0.0.1\": false, // 指定为false时，不使用该IP\n    \"2.2.2.2\": {\n      \"desc\": \"这样可以合法的在配置中插入注释。上面使用的//注释方式在文件中是不允许的\"\n    }\n  }\n}\n```\n\n# 9. IP测速：\n\n用来对从指定的DNS与IP预设置中获取到的IP测试TCP延迟，也可以用来测试DoH和DoT服务器的可用性，后者操作如下：先在【DNS服务管理】中配置好需要测试的DNS设置，然后在【IP测速】里添加一个没有设置【IP预设置】的辅助域名，并选择使用需检测的DNS进行解析。<br>\n对于DoH/DoT而言，由于答案不能被篡改和窃听，所以辅助域名要么获得真实IP（说明可用）要么没有收到答案（说明不可用）。该方法不适用于常规TCP/UDP的DNS，因为它们没有加密，即使收到答案也可能被篡改而不可用）。<br>\n在config.json中的 `server.dns.speedTest:object`中设置，**该条目** 格式如下：\n\n```json\n\"speedTest\": {\n  \"hostnameList\": [\n    \"example1.com\",\n    \"example2.com\"\n  ],\n  \"dnsProviders\": [\n    \"your-DNS-name-used-in-test1\",\n    \"your-DNS-name-used-in-test2\"\n  ]\n}\n```\n"
  },
  {
    "path": "doc/wiki/各平台安装说明.md",
    "content": "|平台|安装说明 |\n|---|---|\n| 【Windows】 | 下载后提示无法验证发行者时，选择保留即可 <br/>注意：开着ds重启电脑会导致无法上网，你可以再次打开ds，然后右键小图标退出ds即可。[更多说明](https://github.com/docmirror/dev-sidecar/issues/109)|\n|  【Mac】 |安装时提示无法验证开发者时，请先取消<br/>然后去系统偏好设置->安全与隐私->下方已阻止使用DevSidecar<br/>选择仍要打开 |\n|  【Ubuntu】 | [安装说明](https://github.com/docmirror/dev-sidecar/blob/master/doc/linux.md)|\n|【其他Linux】|  |"
  },
  {
    "path": "doc/wiki/解决Github访问不了或速度很慢的问题.md",
    "content": "> 注：请使用 `v2.0.0-RC2` 及以上版本，下载地址：https://github.com/docmirror/dev-sidecar/releases\n\n目前，Github通过预设置的IP来访问的，选取测速排在前的IP。\n\n可是，虽然IP测速延迟很低，但是依然会存在不同地区访问部分预设IP不通或很慢的问题。\n\n如果碰到此问题，可以通过将预设IP设置为 `false` 来禁用访问慢的IP，以此达到切换IP的目的，如下图：\n如果访问还慢，再将测速排在第1的IP再禁用掉，以此循环，将访问慢的IP都禁掉，直到选取到的IP访问Github速度很快为止。\n\n> 假如：测速排第1的IP为 `20.27.177.113`，则将其配置为 `false`，或者删除该IP\n![输入图片说明](https://foruda.gitee.com/images/1737713514504282222/96a679f9_1895865.png \"屏幕截图\")\n"
  },
  {
    "path": "eslint.config.js",
    "content": "import antfu from '@antfu/eslint-config'\n\nexport default antfu(\n  {\n    vue: {\n      vueVersion: 2,\n    },\n    rules: {\n      'style/brace-style': ['error', '1tbs'],\n      'style/space-before-function-paren': ['error', 'always'],\n      'import/newline-after-import': 'off',\n      'import/first': 'off',\n      'perfectionist/sort-imports': 'off',\n      'node/prefer-global/buffer': 'off',\n      'node/prefer-global/process': 'off',\n      'no-console': 'off',\n    },\n    ignores: [\n      '**/build/*',\n      '**/dist_electron',\n    ],\n    formatters: {\n      css: true,\n      html: true,\n      markdown: 'prettier',\n    },\n  },\n)\n"
  },
  {
    "path": "package.json",
    "content": "{\n  \"name\": \"dev-sidecar-parent\",\n  \"type\": \"module\",\n  \"private\": false,\n  \"packageManager\": \"pnpm@9.13.2\",\n  \"author\": \"Greper\",\n  \"license\": \"MPL-2.0\",\n  \"scripts\": {\n    \"lint\": \"eslint .\",\n    \"lint:fix\": \"eslint . --fix\"\n  },\n  \"devDependencies\": {\n    \"@antfu/eslint-config\": \"^3.9.1\",\n    \"eslint\": \"^9.15.0\",\n    \"eslint-plugin-format\": \"^0.1.2\"\n  },\n  \"pnpm\": {\n    \"supportedArchitectures\": {\n      \"os\": [\"current\"],\n      \"cpu\": [\"x64\", \"arm64\", \"ia32\"]\n    }\n  }\n}\n"
  },
  {
    "path": "packages/cli/LICENSE",
    "content": "Mozilla Public License Version 2.0\n==================================\n\n1. Definitions\n--------------\n\n1.1. \"Contributor\"\n    means each individual or legal entity that creates, contributes to\n    the creation of, or owns Covered Software.\n\n1.2. \"Contributor Version\"\n    means the combination of the Contributions of others (if any) used\n    by a Contributor and that particular Contributor's Contribution.\n\n1.3. \"Contribution\"\n    means Covered Software of a particular Contributor.\n\n1.4. \"Covered Software\"\n    means Source Code Form to which the initial Contributor has attached\n    the notice in Exhibit A, the Executable Form of such Source Code\n    Form, and Modifications of such Source Code Form, in each case\n    including portions thereof.\n\n1.5. \"Incompatible With Secondary Licenses\"\n    means\n\n    (a) that the initial Contributor has attached the notice described\n        in Exhibit B to the Covered Software; or\n\n    (b) that the Covered Software was made available under the terms of\n        version 1.1 or earlier of the License, but not also under the\n        terms of a Secondary License.\n\n1.6. \"Executable Form\"\n    means any form of the work other than Source Code Form.\n\n1.7. \"Larger Work\"\n    means a work that combines Covered Software with other material, in\n    a separate file or files, that is not Covered Software.\n\n1.8. \"License\"\n    means this document.\n\n1.9. \"Licensable\"\n    means having the right to grant, to the maximum extent possible,\n    whether at the time of the initial grant or subsequently, any and\n    all of the rights conveyed by this License.\n\n1.10. \"Modifications\"\n    means any of the following:\n\n    (a) any file in Source Code Form that results from an addition to,\n        deletion from, or modification of the contents of Covered\n        Software; or\n\n    (b) any new file in Source Code Form that contains any Covered\n        Software.\n\n1.11. \"Patent Claims\" of a Contributor\n    means any patent claim(s), including without limitation, method,\n    process, and apparatus claims, in any patent Licensable by such\n    Contributor that would be infringed, but for the grant of the\n    License, by the making, using, selling, offering for sale, having\n    made, import, or transfer of either its Contributions or its\n    Contributor Version.\n\n1.12. \"Secondary License\"\n    means either the GNU General Public License, Version 2.0, the GNU\n    Lesser General Public License, Version 2.1, the GNU Affero General\n    Public License, Version 3.0, or any later versions of those\n    licenses.\n\n1.13. \"Source Code Form\"\n    means the form of the work preferred for making modifications.\n\n1.14. \"You\" (or \"Your\")\n    means an individual or a legal entity exercising rights under this\n    License. For legal entities, \"You\" includes any entity that\n    controls, is controlled by, or is under common control with You. For\n    purposes of this definition, \"control\" means (a) the power, direct\n    or indirect, to cause the direction or management of such entity,\n    whether by contract or otherwise, or (b) ownership of more than\n    fifty percent (50%) of the outstanding shares or beneficial\n    ownership of such entity.\n\n2. License Grants and Conditions\n--------------------------------\n\n2.1. Grants\n\nEach Contributor hereby grants You a world-wide, royalty-free,\nnon-exclusive license:\n\n(a) under intellectual property rights (other than patent or trademark)\n    Licensable by such Contributor to use, reproduce, make available,\n    modify, display, perform, distribute, and otherwise exploit its\n    Contributions, either on an unmodified basis, with Modifications, or\n    as part of a Larger Work; and\n\n(b) under Patent Claims of such Contributor to make, use, sell, offer\n    for sale, have made, import, and otherwise transfer either its\n    Contributions or its Contributor Version.\n\n2.2. Effective Date\n\nThe licenses granted in Section 2.1 with respect to any Contribution\nbecome effective for each Contribution on the date the Contributor first\ndistributes such Contribution.\n\n2.3. Limitations on Grant Scope\n\nThe licenses granted in this Section 2 are the only rights granted under\nthis License. No additional rights or licenses will be implied from the\ndistribution or licensing of Covered Software under this License.\nNotwithstanding Section 2.1(b) above, no patent license is granted by a\nContributor:\n\n(a) for any code that a Contributor has removed from Covered Software;\n    or\n\n(b) for infringements caused by: (i) Your and any other third party's\n    modifications of Covered Software, or (ii) the combination of its\n    Contributions with other software (except as part of its Contributor\n    Version); or\n\n(c) under Patent Claims infringed by Covered Software in the absence of\n    its Contributions.\n\nThis License does not grant any rights in the trademarks, service marks,\nor logos of any Contributor (except as may be necessary to comply with\nthe notice requirements in Section 3.4).\n\n2.4. Subsequent Licenses\n\nNo Contributor makes additional grants as a result of Your choice to\ndistribute the Covered Software under a subsequent version of this\nLicense (see Section 10.2) or under the terms of a Secondary License (if\npermitted under the terms of Section 3.3).\n\n2.5. Representation\n\nEach Contributor represents that the Contributor believes its\nContributions are its original creation(s) or it has sufficient rights\nto grant the rights to its Contributions conveyed by this License.\n\n2.6. Fair Use\n\nThis License is not intended to limit any rights You have under\napplicable copyright doctrines of fair use, fair dealing, or other\nequivalents.\n\n2.7. Conditions\n\nSections 3.1, 3.2, 3.3, and 3.4 are conditions of the licenses granted\nin Section 2.1.\n\n3. Responsibilities\n-------------------\n\n3.1. Distribution of Source Form\n\nAll distribution of Covered Software in Source Code Form, including any\nModifications that You create or to which You contribute, must be under\nthe terms of this License. You must inform recipients that the Source\nCode Form of the Covered Software is governed by the terms of this\nLicense, and how they can obtain a copy of this License. You may not\nattempt to alter or restrict the recipients' rights in the Source Code\nForm.\n\n3.2. Distribution of Executable Form\n\nIf You distribute Covered Software in Executable Form then:\n\n(a) such Covered Software must also be made available in Source Code\n    Form, as described in Section 3.1, and You must inform recipients of\n    the Executable Form how they can obtain a copy of such Source Code\n    Form by reasonable means in a timely manner, at a charge no more\n    than the cost of distribution to the recipient; and\n\n(b) You may distribute such Executable Form under the terms of this\n    License, or sublicense it under different terms, provided that the\n    license for the Executable Form does not attempt to limit or alter\n    the recipients' rights in the Source Code Form under this License.\n\n3.3. Distribution of a Larger Work\n\nYou may create and distribute a Larger Work under terms of Your choice,\nprovided that You also comply with the requirements of this License for\nthe Covered Software. If the Larger Work is a combination of Covered\nSoftware with a work governed by one or more Secondary Licenses, and the\nCovered Software is not Incompatible With Secondary Licenses, this\nLicense permits You to additionally distribute such Covered Software\nunder the terms of such Secondary License(s), so that the recipient of\nthe Larger Work may, at their option, further distribute the Covered\nSoftware under the terms of either this License or such Secondary\nLicense(s).\n\n3.4. Notices\n\nYou may not remove or alter the substance of any license notices\n(including copyright notices, patent notices, disclaimers of warranty,\nor limitations of liability) contained within the Source Code Form of\nthe Covered Software, except that You may alter any license notices to\nthe extent required to remedy known factual inaccuracies.\n\n3.5. Application of Additional Terms\n\nYou may choose to offer, and to charge a fee for, warranty, support,\nindemnity or liability obligations to one or more recipients of Covered\nSoftware. However, You may do so only on Your own behalf, and not on\nbehalf of any Contributor. You must make it absolutely clear that any\nsuch warranty, support, indemnity, or liability obligation is offered by\nYou alone, and You hereby agree to indemnify every Contributor for any\nliability incurred by such Contributor as a result of warranty, support,\nindemnity or liability terms You offer. You may include additional\ndisclaimers of warranty and limitations of liability specific to any\njurisdiction.\n\n4. Inability to Comply Due to Statute or Regulation\n---------------------------------------------------\n\nIf it is impossible for You to comply with any of the terms of this\nLicense with respect to some or all of the Covered Software due to\nstatute, judicial order, or regulation then You must: (a) comply with\nthe terms of this License to the maximum extent possible; and (b)\ndescribe the limitations and the code they affect. Such description must\nbe placed in a text file included with all distributions of the Covered\nSoftware under this License. Except to the extent prohibited by statute\nor regulation, such description must be sufficiently detailed for a\nrecipient of ordinary skill to be able to understand it.\n\n5. Termination\n--------------\n\n5.1. The rights granted under this License will terminate automatically\nif You fail to comply with any of its terms. However, if You become\ncompliant, then the rights granted under this License from a particular\nContributor are reinstated (a) provisionally, unless and until such\nContributor explicitly and finally terminates Your grants, and (b) on an\nongoing basis, if such Contributor fails to notify You of the\nnon-compliance by some reasonable means prior to 60 days after You have\ncome back into compliance. Moreover, Your grants from a particular\nContributor are reinstated on an ongoing basis if such Contributor\nnotifies You of the non-compliance by some reasonable means, this is the\nfirst time You have received notice of non-compliance with this License\nfrom such Contributor, and You become compliant prior to 30 days after\nYour receipt of the notice.\n\n5.2. If You initiate litigation against any entity by asserting a patent\ninfringement claim (excluding declaratory judgment actions,\ncounter-claims, and cross-claims) alleging that a Contributor Version\ndirectly or indirectly infringes any patent, then the rights granted to\nYou by any and all Contributors for the Covered Software under Section\n2.1 of this License shall terminate.\n\n5.3. In the event of termination under Sections 5.1 or 5.2 above, all\nend user license agreements (excluding distributors and resellers) which\nhave been validly granted by You or Your distributors under this License\nprior to termination shall survive termination.\n\n************************************************************************\n*                                                                      *\n*  6. Disclaimer of Warranty                                           *\n*  -------------------------                                           *\n*                                                                      *\n*  Covered Software is provided under this License on an \"as is\"       *\n*  basis, without warranty of any kind, either expressed, implied, or  *\n*  statutory, including, without limitation, warranties that the       *\n*  Covered Software is free of defects, merchantable, fit for a        *\n*  particular purpose or non-infringing. The entire risk as to the     *\n*  quality and performance of the Covered Software is with You.        *\n*  Should any Covered Software prove defective in any respect, You     *\n*  (not any Contributor) assume the cost of any necessary servicing,   *\n*  repair, or correction. This disclaimer of warranty constitutes an   *\n*  essential part of this License. No use of any Covered Software is   *\n*  authorized under this License except under this disclaimer.         *\n*                                                                      *\n************************************************************************\n\n************************************************************************\n*                                                                      *\n*  7. Limitation of Liability                                          *\n*  --------------------------                                          *\n*                                                                      *\n*  Under no circumstances and under no legal theory, whether tort      *\n*  (including negligence), contract, or otherwise, shall any           *\n*  Contributor, or anyone who distributes Covered Software as          *\n*  permitted above, be liable to You for any direct, indirect,         *\n*  special, incidental, or consequential damages of any character      *\n*  including, without limitation, damages for lost profits, loss of    *\n*  goodwill, work stoppage, computer failure or malfunction, or any    *\n*  and all other commercial damages or losses, even if such party      *\n*  shall have been informed of the possibility of such damages. This   *\n*  limitation of liability shall not apply to liability for death or   *\n*  personal injury resulting from such party's negligence to the       *\n*  extent applicable law prohibits such limitation. Some               *\n*  jurisdictions do not allow the exclusion or limitation of           *\n*  incidental or consequential damages, so this exclusion and          *\n*  limitation may not apply to You.                                    *\n*                                                                      *\n************************************************************************\n\n8. Litigation\n-------------\n\nAny litigation relating to this License may be brought only in the\ncourts of a jurisdiction where the defendant maintains its principal\nplace of business and such litigation shall be governed by laws of that\njurisdiction, without reference to its conflict-of-law provisions.\nNothing in this Section shall prevent a party's ability to bring\ncross-claims or counter-claims.\n\n9. Miscellaneous\n----------------\n\nThis License represents the complete agreement concerning the subject\nmatter hereof. If any provision of this License is held to be\nunenforceable, such provision shall be reformed only to the extent\nnecessary to make it enforceable. Any law or regulation which provides\nthat the language of a contract shall be construed against the drafter\nshall not be used to construe this License against a Contributor.\n\n10. Versions of the License\n---------------------------\n\n10.1. New Versions\n\nMozilla Foundation is the license steward. Except as provided in Section\n10.3, no one other than the license steward has the right to modify or\npublish new versions of this License. Each version will be given a\ndistinguishing version number.\n\n10.2. Effect of New Versions\n\nYou may distribute the Covered Software under the terms of the version\nof the License under which You originally received the Covered Software,\nor under the terms of any subsequent version published by the license\nsteward.\n\n10.3. Modified Versions\n\nIf you create software not governed by this License, and you want to\ncreate a new license for such software, you may create and use a\nmodified version of this License if you rename the license and remove\nany references to the name of the license steward (except to note that\nsuch modified license differs from this License).\n\n10.4. Distributing Source Code Form that is Incompatible With Secondary\nLicenses\n\nIf You choose to distribute Source Code Form that is Incompatible With\nSecondary Licenses under the terms of this version of the License, the\nnotice described in Exhibit B of this License must be attached.\n\nExhibit A - Source Code Form License Notice\n-------------------------------------------\n\n  This Source Code Form is subject to the terms of the Mozilla Public\n  License, v. 2.0. If a copy of the MPL was not distributed with this\n  file, You can obtain one at http://mozilla.org/MPL/2.0/.\n\nIf it is not possible or desirable to put the notice in a particular\nfile, then You may include the notice in a location (such as a LICENSE\nfile in a relevant directory) where a recipient would be likely to look\nfor such a notice.\n\nYou may add additional accurate notices of copyright ownership.\n\nExhibit B - \"Incompatible With Secondary Licenses\" Notice\n---------------------------------------------------------\n\n  This Source Code Form is \"Incompatible With Secondary Licenses\", as\n  defined by the Mozilla Public License, v. 2.0.\n"
  },
  {
    "path": "packages/cli/cli.js",
    "content": "#!/usr/bin/env node\n\nrequire('./src')\n"
  },
  {
    "path": "packages/cli/package.json",
    "content": "{\n  \"name\": \"@docmirror/dev-sidecar-cli\",\n  \"version\": \"2.0.1\",\n  \"private\": false,\n  \"description\": \"给开发者的加速代理工具\",\n  \"author\": \"docmirror.cn\",\n  \"license\": \"MPL-2.0\",\n  \"keywords\": [\n    \"dev-sidecar\",\n    \"github加速\",\n    \"google加速\",\n    \"代理\"\n  ],\n  \"main\": \"src/index.js\",\n  \"bin\": \"./cli.js\",\n  \"scripts\": {\n    \"start\": \"node ./src\"\n  },\n  \"dependencies\": {\n    \"@docmirror/dev-sidecar\": \"workspace:*\",\n    \"@docmirror/mitmproxy\": \"workspace:*\"\n  }\n}\n"
  },
  {
    "path": "packages/cli/src/banner.txt",
    "content": "    ____                 _____ _     __\n   / __ \\___ _   __     / ___/(_)___/ /__  _________ ______\n  / / / / _ \\ | / /_____\\__ \\/ / __  / _ \\/ ___/ __ `/ ___/\n / /_/ /  __/ |/ /_____/__/ / / /_/ /  __/ /__/ /_/ / /\n/_____/\\___/|___/     /____/_/\\__,_/\\___/\\___/\\__,_/_/\n\n\n==================== 开发者边车 ====================\n"
  },
  {
    "path": "packages/cli/src/index.js",
    "content": "const fs = require('node:fs')\nconst DevSidecar = require('@docmirror/dev-sidecar')\nconst jsonApi = require('@docmirror/mitmproxy/src/json')\n\n// 启动服务\nconst mitmproxyPath = './mitmproxy'\nasync function startup () {\n  const banner = fs.readFileSync('./banner.txt')\n  console.log(banner.toString())\n\n  const configPath = './user_config.json5'\n  if (fs.existsSync(configPath)) {\n    const file = fs.readFileSync(configPath)\n    let userConfig\n    try {\n      userConfig = jsonApi.parse(file.toString())\n      console.info(`读取和解析 user_config.json5 成功:${configPath}`)\n    } catch (e) {\n      console.error(`读取或解析 user_config.json5 失败: ${configPath}, error:`, e)\n      userConfig = {}\n    }\n    DevSidecar.api.config.set(userConfig)\n  }\n\n  await DevSidecar.api.startup({ mitmproxyPath })\n  console.log('dev-sidecar 已启动')\n}\n\nasync function onClose () {\n  console.log('on sigint ')\n  await DevSidecar.api.shutdown()\n  console.log('on closed ')\n  process.exit(0)\n}\nprocess.on('SIGINT', onClose)\n\nstartup()\n"
  },
  {
    "path": "packages/cli/src/mitmproxy.js",
    "content": "const fs = require('node:fs')\nconst path = require('node:path')\nconst server = require('@docmirror/mitmproxy')\nconst jsonApi = require('@docmirror/mitmproxy/src/json')\nconst log = require('@docmirror/mitmproxy/src/utils/util.log.server') // 当前脚本是在 server 的进程中执行的，所以使用 mitmproxy 中的logger\n\nconst home = process.env.USER_HOME || process.env.HOME || 'C:/Users/Administrator/'\n\nlet configPath\nif (process.argv && process.argv.length > 3) {\n  configPath = process.argv[2]\n} else {\n  configPath = path.join(home, '.dev-sidecar/running.json')\n}\n\nconst configJson = fs.readFileSync(configPath)\nlog.info('读取 running.json by cli 成功:', configPath)\nlet config\ntry {\n  config = jsonApi.parse(configJson.toString())\n} catch (e) {\n  log.error(`running.json 文件内容格式不正确，文件路径：${configPath}，文件内容: ${configJson.toString()}, error:`, e)\n  config = {}\n}\n// const scriptDir = '../../gui/extra/scripts/'\n// config.setting.script.defaultDir = path.join(__dirname, scriptDir)\n// const pacFilePath = '../../gui/extra/pac/pac.txt'\n// config.plugin.overwall.pac.customPacFilePath = path.join(__dirname, pacFilePath)\nconfig.setting.rootDir = path.join(__dirname, '../../gui/')\nlog.info(`start mitmproxy by cli, configPath: ${configPath}`)\nserver.start(config)\n"
  },
  {
    "path": "packages/cli/src/user_config.json5",
    "content": "{\n  \"app\": {\n    \"autoStart\": {\n      \"enabled\": true\n    },\n    \"mode\": \"default\"\n  },\n  \"plugin\": {\n    \"node\": {\n      \"setting\": {\n        \"yarnRegistry\": \"null\"\n      }\n    },\n    \"git\": {\n      \"enabled\": true\n    },\n    \"overwall\": {\n      \"enabled\": false,\n      \"targets\": {\n        \"*gagedigital.com\": true,\n        \"*yonsz.net\": true,\n        \"*bootstrapcdn.com\": true,\n        \"*cloudflare.com\": true,\n        \"help.yonsz.net\": true\n      }\n    }\n  },\n  \"server\": {\n    \"intercepts\": {\n      \"dev-sidecar.docmirror.cn\": {\n        \".*\": {\n          \"proxy\": \"dev-sidecar-preview.docmirror.cn\"\n        }\n      },\n      \"test1111.gagedigital.com\": {\n        \".*\": {\n          \"proxy\": \"test1.gagedigital.com\"\n        }\n      }\n    }\n  }\n}\n"
  },
  {
    "path": "packages/core/LICENSE",
    "content": "Mozilla Public License Version 2.0\n==================================\n\n1. Definitions\n--------------\n\n1.1. \"Contributor\"\n    means each individual or legal entity that creates, contributes to\n    the creation of, or owns Covered Software.\n\n1.2. \"Contributor Version\"\n    means the combination of the Contributions of others (if any) used\n    by a Contributor and that particular Contributor's Contribution.\n\n1.3. \"Contribution\"\n    means Covered Software of a particular Contributor.\n\n1.4. \"Covered Software\"\n    means Source Code Form to which the initial Contributor has attached\n    the notice in Exhibit A, the Executable Form of such Source Code\n    Form, and Modifications of such Source Code Form, in each case\n    including portions thereof.\n\n1.5. \"Incompatible With Secondary Licenses\"\n    means\n\n    (a) that the initial Contributor has attached the notice described\n        in Exhibit B to the Covered Software; or\n\n    (b) that the Covered Software was made available under the terms of\n        version 1.1 or earlier of the License, but not also under the\n        terms of a Secondary License.\n\n1.6. \"Executable Form\"\n    means any form of the work other than Source Code Form.\n\n1.7. \"Larger Work\"\n    means a work that combines Covered Software with other material, in\n    a separate file or files, that is not Covered Software.\n\n1.8. \"License\"\n    means this document.\n\n1.9. \"Licensable\"\n    means having the right to grant, to the maximum extent possible,\n    whether at the time of the initial grant or subsequently, any and\n    all of the rights conveyed by this License.\n\n1.10. \"Modifications\"\n    means any of the following:\n\n    (a) any file in Source Code Form that results from an addition to,\n        deletion from, or modification of the contents of Covered\n        Software; or\n\n    (b) any new file in Source Code Form that contains any Covered\n        Software.\n\n1.11. \"Patent Claims\" of a Contributor\n    means any patent claim(s), including without limitation, method,\n    process, and apparatus claims, in any patent Licensable by such\n    Contributor that would be infringed, but for the grant of the\n    License, by the making, using, selling, offering for sale, having\n    made, import, or transfer of either its Contributions or its\n    Contributor Version.\n\n1.12. \"Secondary License\"\n    means either the GNU General Public License, Version 2.0, the GNU\n    Lesser General Public License, Version 2.1, the GNU Affero General\n    Public License, Version 3.0, or any later versions of those\n    licenses.\n\n1.13. \"Source Code Form\"\n    means the form of the work preferred for making modifications.\n\n1.14. \"You\" (or \"Your\")\n    means an individual or a legal entity exercising rights under this\n    License. For legal entities, \"You\" includes any entity that\n    controls, is controlled by, or is under common control with You. For\n    purposes of this definition, \"control\" means (a) the power, direct\n    or indirect, to cause the direction or management of such entity,\n    whether by contract or otherwise, or (b) ownership of more than\n    fifty percent (50%) of the outstanding shares or beneficial\n    ownership of such entity.\n\n2. License Grants and Conditions\n--------------------------------\n\n2.1. Grants\n\nEach Contributor hereby grants You a world-wide, royalty-free,\nnon-exclusive license:\n\n(a) under intellectual property rights (other than patent or trademark)\n    Licensable by such Contributor to use, reproduce, make available,\n    modify, display, perform, distribute, and otherwise exploit its\n    Contributions, either on an unmodified basis, with Modifications, or\n    as part of a Larger Work; and\n\n(b) under Patent Claims of such Contributor to make, use, sell, offer\n    for sale, have made, import, and otherwise transfer either its\n    Contributions or its Contributor Version.\n\n2.2. Effective Date\n\nThe licenses granted in Section 2.1 with respect to any Contribution\nbecome effective for each Contribution on the date the Contributor first\ndistributes such Contribution.\n\n2.3. Limitations on Grant Scope\n\nThe licenses granted in this Section 2 are the only rights granted under\nthis License. No additional rights or licenses will be implied from the\ndistribution or licensing of Covered Software under this License.\nNotwithstanding Section 2.1(b) above, no patent license is granted by a\nContributor:\n\n(a) for any code that a Contributor has removed from Covered Software;\n    or\n\n(b) for infringements caused by: (i) Your and any other third party's\n    modifications of Covered Software, or (ii) the combination of its\n    Contributions with other software (except as part of its Contributor\n    Version); or\n\n(c) under Patent Claims infringed by Covered Software in the absence of\n    its Contributions.\n\nThis License does not grant any rights in the trademarks, service marks,\nor logos of any Contributor (except as may be necessary to comply with\nthe notice requirements in Section 3.4).\n\n2.4. Subsequent Licenses\n\nNo Contributor makes additional grants as a result of Your choice to\ndistribute the Covered Software under a subsequent version of this\nLicense (see Section 10.2) or under the terms of a Secondary License (if\npermitted under the terms of Section 3.3).\n\n2.5. Representation\n\nEach Contributor represents that the Contributor believes its\nContributions are its original creation(s) or it has sufficient rights\nto grant the rights to its Contributions conveyed by this License.\n\n2.6. Fair Use\n\nThis License is not intended to limit any rights You have under\napplicable copyright doctrines of fair use, fair dealing, or other\nequivalents.\n\n2.7. Conditions\n\nSections 3.1, 3.2, 3.3, and 3.4 are conditions of the licenses granted\nin Section 2.1.\n\n3. Responsibilities\n-------------------\n\n3.1. Distribution of Source Form\n\nAll distribution of Covered Software in Source Code Form, including any\nModifications that You create or to which You contribute, must be under\nthe terms of this License. You must inform recipients that the Source\nCode Form of the Covered Software is governed by the terms of this\nLicense, and how they can obtain a copy of this License. You may not\nattempt to alter or restrict the recipients' rights in the Source Code\nForm.\n\n3.2. Distribution of Executable Form\n\nIf You distribute Covered Software in Executable Form then:\n\n(a) such Covered Software must also be made available in Source Code\n    Form, as described in Section 3.1, and You must inform recipients of\n    the Executable Form how they can obtain a copy of such Source Code\n    Form by reasonable means in a timely manner, at a charge no more\n    than the cost of distribution to the recipient; and\n\n(b) You may distribute such Executable Form under the terms of this\n    License, or sublicense it under different terms, provided that the\n    license for the Executable Form does not attempt to limit or alter\n    the recipients' rights in the Source Code Form under this License.\n\n3.3. Distribution of a Larger Work\n\nYou may create and distribute a Larger Work under terms of Your choice,\nprovided that You also comply with the requirements of this License for\nthe Covered Software. If the Larger Work is a combination of Covered\nSoftware with a work governed by one or more Secondary Licenses, and the\nCovered Software is not Incompatible With Secondary Licenses, this\nLicense permits You to additionally distribute such Covered Software\nunder the terms of such Secondary License(s), so that the recipient of\nthe Larger Work may, at their option, further distribute the Covered\nSoftware under the terms of either this License or such Secondary\nLicense(s).\n\n3.4. Notices\n\nYou may not remove or alter the substance of any license notices\n(including copyright notices, patent notices, disclaimers of warranty,\nor limitations of liability) contained within the Source Code Form of\nthe Covered Software, except that You may alter any license notices to\nthe extent required to remedy known factual inaccuracies.\n\n3.5. Application of Additional Terms\n\nYou may choose to offer, and to charge a fee for, warranty, support,\nindemnity or liability obligations to one or more recipients of Covered\nSoftware. However, You may do so only on Your own behalf, and not on\nbehalf of any Contributor. You must make it absolutely clear that any\nsuch warranty, support, indemnity, or liability obligation is offered by\nYou alone, and You hereby agree to indemnify every Contributor for any\nliability incurred by such Contributor as a result of warranty, support,\nindemnity or liability terms You offer. You may include additional\ndisclaimers of warranty and limitations of liability specific to any\njurisdiction.\n\n4. Inability to Comply Due to Statute or Regulation\n---------------------------------------------------\n\nIf it is impossible for You to comply with any of the terms of this\nLicense with respect to some or all of the Covered Software due to\nstatute, judicial order, or regulation then You must: (a) comply with\nthe terms of this License to the maximum extent possible; and (b)\ndescribe the limitations and the code they affect. Such description must\nbe placed in a text file included with all distributions of the Covered\nSoftware under this License. Except to the extent prohibited by statute\nor regulation, such description must be sufficiently detailed for a\nrecipient of ordinary skill to be able to understand it.\n\n5. Termination\n--------------\n\n5.1. The rights granted under this License will terminate automatically\nif You fail to comply with any of its terms. However, if You become\ncompliant, then the rights granted under this License from a particular\nContributor are reinstated (a) provisionally, unless and until such\nContributor explicitly and finally terminates Your grants, and (b) on an\nongoing basis, if such Contributor fails to notify You of the\nnon-compliance by some reasonable means prior to 60 days after You have\ncome back into compliance. Moreover, Your grants from a particular\nContributor are reinstated on an ongoing basis if such Contributor\nnotifies You of the non-compliance by some reasonable means, this is the\nfirst time You have received notice of non-compliance with this License\nfrom such Contributor, and You become compliant prior to 30 days after\nYour receipt of the notice.\n\n5.2. If You initiate litigation against any entity by asserting a patent\ninfringement claim (excluding declaratory judgment actions,\ncounter-claims, and cross-claims) alleging that a Contributor Version\ndirectly or indirectly infringes any patent, then the rights granted to\nYou by any and all Contributors for the Covered Software under Section\n2.1 of this License shall terminate.\n\n5.3. In the event of termination under Sections 5.1 or 5.2 above, all\nend user license agreements (excluding distributors and resellers) which\nhave been validly granted by You or Your distributors under this License\nprior to termination shall survive termination.\n\n************************************************************************\n*                                                                      *\n*  6. Disclaimer of Warranty                                           *\n*  -------------------------                                           *\n*                                                                      *\n*  Covered Software is provided under this License on an \"as is\"       *\n*  basis, without warranty of any kind, either expressed, implied, or  *\n*  statutory, including, without limitation, warranties that the       *\n*  Covered Software is free of defects, merchantable, fit for a        *\n*  particular purpose or non-infringing. The entire risk as to the     *\n*  quality and performance of the Covered Software is with You.        *\n*  Should any Covered Software prove defective in any respect, You     *\n*  (not any Contributor) assume the cost of any necessary servicing,   *\n*  repair, or correction. This disclaimer of warranty constitutes an   *\n*  essential part of this License. No use of any Covered Software is   *\n*  authorized under this License except under this disclaimer.         *\n*                                                                      *\n************************************************************************\n\n************************************************************************\n*                                                                      *\n*  7. Limitation of Liability                                          *\n*  --------------------------                                          *\n*                                                                      *\n*  Under no circumstances and under no legal theory, whether tort      *\n*  (including negligence), contract, or otherwise, shall any           *\n*  Contributor, or anyone who distributes Covered Software as          *\n*  permitted above, be liable to You for any direct, indirect,         *\n*  special, incidental, or consequential damages of any character      *\n*  including, without limitation, damages for lost profits, loss of    *\n*  goodwill, work stoppage, computer failure or malfunction, or any    *\n*  and all other commercial damages or losses, even if such party      *\n*  shall have been informed of the possibility of such damages. This   *\n*  limitation of liability shall not apply to liability for death or   *\n*  personal injury resulting from such party's negligence to the       *\n*  extent applicable law prohibits such limitation. Some               *\n*  jurisdictions do not allow the exclusion or limitation of           *\n*  incidental or consequential damages, so this exclusion and          *\n*  limitation may not apply to You.                                    *\n*                                                                      *\n************************************************************************\n\n8. Litigation\n-------------\n\nAny litigation relating to this License may be brought only in the\ncourts of a jurisdiction where the defendant maintains its principal\nplace of business and such litigation shall be governed by laws of that\njurisdiction, without reference to its conflict-of-law provisions.\nNothing in this Section shall prevent a party's ability to bring\ncross-claims or counter-claims.\n\n9. Miscellaneous\n----------------\n\nThis License represents the complete agreement concerning the subject\nmatter hereof. If any provision of this License is held to be\nunenforceable, such provision shall be reformed only to the extent\nnecessary to make it enforceable. Any law or regulation which provides\nthat the language of a contract shall be construed against the drafter\nshall not be used to construe this License against a Contributor.\n\n10. Versions of the License\n---------------------------\n\n10.1. New Versions\n\nMozilla Foundation is the license steward. Except as provided in Section\n10.3, no one other than the license steward has the right to modify or\npublish new versions of this License. Each version will be given a\ndistinguishing version number.\n\n10.2. Effect of New Versions\n\nYou may distribute the Covered Software under the terms of the version\nof the License under which You originally received the Covered Software,\nor under the terms of any subsequent version published by the license\nsteward.\n\n10.3. Modified Versions\n\nIf you create software not governed by this License, and you want to\ncreate a new license for such software, you may create and use a\nmodified version of this License if you rename the license and remove\nany references to the name of the license steward (except to note that\nsuch modified license differs from this License).\n\n10.4. Distributing Source Code Form that is Incompatible With Secondary\nLicenses\n\nIf You choose to distribute Source Code Form that is Incompatible With\nSecondary Licenses under the terms of this version of the License, the\nnotice described in Exhibit B of this License must be attached.\n\nExhibit A - Source Code Form License Notice\n-------------------------------------------\n\n  This Source Code Form is subject to the terms of the Mozilla Public\n  License, v. 2.0. If a copy of the MPL was not distributed with this\n  file, You can obtain one at http://mozilla.org/MPL/2.0/.\n\nIf it is not possible or desirable to put the notice in a particular\nfile, then You may include the notice in a location (such as a LICENSE\nfile in a relevant directory) where a recipient would be likely to look\nfor such a notice.\n\nYou may add additional accurate notices of copyright ownership.\n\nExhibit B - \"Incompatible With Secondary Licenses\" Notice\n---------------------------------------------------------\n\n  This Source Code Form is \"Incompatible With Secondary Licenses\", as\n  defined by the Mozilla Public License, v. 2.0.\n"
  },
  {
    "path": "packages/core/index.js",
    "content": "module.exports = require('./src')\n\n// TODO  这是一个解谜游戏 ↓ ↓ ↓ ↓ ↓ ↓ ，如果你破解了它，请不要公开，好好用它来学习和查资料吧（特别注意：为了你的人身安全，请不要用它来查看和发表不当言论，你懂得）。\n/**\n \\u0061\\u0048\\u0052\\u0030\\u0063\\u0044\\u006f\\u0076\\u004c\\u0032\\u0052\\u006c\\u0064\\u0069\\u0031\\u007a\\u0061\\u0057\\u0052\\u006c\\u0059\\u0032\\u0046\\u0079\\u004c\\u006d\\u0052\\u0076\\u0059\\u0032\\u0031\\u0070\\u0063\\u006e\\u004a\\u0076\\u0063\\u0069\\u0035\\u006a\\u0062\\u0069\\u0039\\u0035\\u0062\\u0033\\u0056\\u006d\\u0061\\u0057\\u0035\\u006b\\u0061\\u0058\\u0051\\u0076\\u0061\\u0057\\u0035\\u006b\\u005a\\u0058\\u0067\\u0075\\u0061\\u0048\\u0052\\u0074\\u0062\\u0041\\u003d\\u003d\n */\n// 这个项目里有一点点解谜提示： https://github.com/fast-crud/fast-crud  （打开拉到最下面）\n"
  },
  {
    "path": "packages/core/package.json",
    "content": "{\n  \"name\": \"@docmirror/dev-sidecar\",\n  \"version\": \"2.0.1\",\n  \"private\": false,\n  \"description\": \"给开发者的加速代理工具\",\n  \"author\": \"docmirror.cn\",\n  \"license\": \"MPL-2.0\",\n  \"keywords\": [\n    \"dev-sidecar\",\n    \"github加速\",\n    \"google加速\",\n    \"代理\"\n  ],\n  \"main\": \"src/index.js\",\n  \"scripts\": {\n    \"test\": \"mocha\"\n  },\n  \"dependencies\": {\n    \"@starknt/sysproxy\": \"^0.0.3\",\n    \"@vscode/sudo-prompt\": \"^9.3.1\",\n    \"fix-path\": \"^3.0.0\",\n    \"iconv-lite\": \"^0.6.3\",\n    \"lodash\": \"^4.17.21\",\n    \"log4js\": \"^6.9.1\",\n    \"node-powershell\": \"^4.0.0\",\n    \"spawn-sync\": \"^2.0.0\",\n    \"winreg\": \"^1.2.5\"\n  },\n  \"devDependencies\": {\n    \"chai\": \"^4.3.4\",\n    \"mocha\": \"^8.2.1\"\n  }\n}\n"
  },
  {
    "path": "packages/core/src/config/index.js",
    "content": "const path = require('node:path')\nconst configLoader = require('./local-config-loader')\n\nfunction getRootCaCertPath () {\n  return path.join(configLoader.getUserBasePath(), '/dev-sidecar.ca.crt')\n}\n\nfunction getRootCaKeyPath () {\n  return path.join(configLoader.getUserBasePath(), '/dev-sidecar.ca.key.pem')\n}\n\nconst defaultConfig = {\n  app: {\n    mode: 'default',\n    autoStart: {\n      enabled: false,\n    },\n    remoteConfig: {\n      enabled: true,\n      // 共享远程配置地址\n      url: 'https://gitee.com/wangliang181230/dev-sidecar/raw/docmirror2.x/packages/core/src/config/remote_config.json',\n      // 个人远程配置地址\n      personalUrl: '',\n    },\n    startShowWindow: true, // 启动时是否打开窗口：true=打开窗口, false=隐藏窗口\n    needCheckHideWindow: true, // 是否需要在隐藏窗口时做检查\n    showHideShortcut: 'Alt + S', // 显示/隐藏窗口快捷键\n    windowSize: { width: 900, height: 750 }, // 启动时，窗口的尺寸\n    theme: 'dark', // 主题：light=亮色, dark=暗色\n    autoChecked: true, // 是否自动检查更新\n    skipPreRelease: true, // 是否忽略预发布版本\n    dock: {\n      hideWhenWinClose: false,\n    },\n    closeStrategy: 0,\n    showShutdownTip: true,\n\n    // 日志相关配置\n    logFileSavePath: path.join(configLoader.getUserBasePath(), '/logs'), // 日志文件保存路径\n    keepLogFileCount: 15, // 保留日志文件数\n    maxLogFileSize: 1, // 最大日志文件大小\n    maxLogFileSizeUnit: 'GB', // 最大日志文件大小单位\n  },\n  server: {\n    enabled: true,\n    host: '127.0.0.1',\n    port: 31181,\n    setting: {\n      NODE_TLS_REJECT_UNAUTHORIZED: true,\n      verifySsl: true,\n      script: {\n        enabled: true,\n        defaultDir: './extra/scripts/',\n      },\n      userBasePath: configLoader.getUserBasePath(),\n      rootCaFile: {\n        certPath: getRootCaCertPath(),\n        keyPath: getRootCaKeyPath(),\n      },\n\n      // 默认超时时间配置\n      defaultTimeout: 20000, // 请求超时时间\n      defaultKeepAliveTimeout: 30000, // 连接超时时间\n\n      // 指定域名超时时间配置\n      timeoutMapping: {\n        'github.com': {\n          timeout: 20000,\n          keepAliveTimeout: 30000,\n        },\n      },\n\n      // 慢速IP延迟时间：测速超过该值时，则视为延迟高，显示为橙色\n      lowSpeedDelay: 200,\n    },\n    compatible: {\n      // **** 自定义兼容配置 **** //\n      // connect阶段所需的兼容性配置\n      connect: {\n        // 参考配置（无path）\n        // 'xxx.xxx.xxx.xxx:443': {\n        //   ssl: false\n        // }\n      },\n      // request阶段所需的兼容性配置\n      request: {\n        // 参考配置（配置方式同 `拦截配置`）\n        // 'xxx.xxx.xxx.xxx:443': {\n        //   '.*': {\n        //     rejectUnauthorized: false\n        //   }\n        // }\n      },\n    },\n    intercept: {\n      enabled: true,\n    },\n    intercepts: {\n      'github.com': {\n        '.*': {\n          sni: 'baidu.com',\n        },\n        '^(/[\\\\w-.]+){2,}/?(\\\\?.*)?$': {\n          // 篡改猴插件地址，以下是高速镜像地址\n          tampermonkeyScript: 'https://gitee.com/wangliang181230/dev-sidecar/raw/scripts/tampermonkey.js',\n          // Github油猴脚本地址，以下是高速镜像地址\n          script: 'https://gitee.com/wangliang181230/dev-sidecar/raw/scripts/GithubEnhanced-High-Speed-Download.user.js',\n          remark: '注：上面所使用的脚本地址，为高速镜像地址。',\n          desc: '油猴脚本：高速下载 Git Clone/SSH、Release、Raw、Code(ZIP) 等文件 (公益加速)、项目列表单文件快捷下载、添加 git clone 命令',\n        },\n        // 以下三项暂时先注释掉，因为已经有油猴脚本提供高速下载地址了。\n        // '/.*/.*/releases/download/': {\n        //   redirect: 'gh.api.99988866.xyz/https://github.com',\n        //   desc: 'release文件加速下载跳转地址'\n        // },\n        // '/.*/.*/archive/': {\n        //   redirect: 'gh.api.99988866.xyz/https://github.com'\n        // },\n        // 以下代理地址不支持该类资源的代理，暂时注释掉\n        // '/.*/.*/blame/': {\n        //   redirect: 'gh.api.99988866.xyz/https://github.com'\n        // },\n        '/fluidicon.png': {\n          cacheDays: 365,\n          desc: 'Github那只猫的图片，缓存1年',\n        },\n        '^(/[^/]+){2}/pull/\\\\d+/open_with_menu.*$': {\n          cacheDays: 7,\n          desc: 'PR详情页：标题右边那个Code按钮的HTML代码请求地址，感觉上应该可以缓存。暂时先设置为缓存7天',\n        },\n        '^((/[^/]+){2,})/raw((/[^/]+)+\\\\.(jpg|jpeg|png|gif))(\\\\?.*)?$': {\n          // eslint-disable-next-line no-template-curly-in-string\n          proxy: 'https://raw.githubusercontent.com${m[1]}${m[3]}',\n          sni: 'baidu.com',\n          cacheDays: 7,\n          desc: '仓库内图片，重定向改为代理，并缓存7天。',\n        },\n        '^((/[^/]+){2,})/raw((/[^/]+)+\\\\.js)(\\\\?.*)?$': {\n          // eslint-disable-next-line no-template-curly-in-string\n          proxy: 'https://raw.githubusercontent.com${m[1]}${m[3]}',\n          sni: 'baidu.com',\n          responseReplace: { headers: { 'content-type': 'application/javascript; charset=utf-8' } },\n          desc: '仓库内脚本，重定向改为代理，并设置响应头Content-Type。作用：方便script拦截器直接使用，避免引起跨域问题和脚本内容限制问题。',\n        },\n      },\n      'github.githubassets.com': {\n        '.*': {\n          sni: 'baidu.com',\n        },\n      },\n      'camo.githubusercontent.com': {\n        '^[a-zA-Z0-9/]+(\\\\?.*)?$': {\n          cacheDays: 365,\n          desc: '图片，缓存1年',\n        },\n      },\n      'collector.github.com': {\n        '.*': {\n          sni: 'baidu.com',\n        },\n      },\n      'customer-stories-feed.github.com': {\n        '.*': { proxy: 'customer-stories-feed.fastgit.org' },\n      },\n      'user-images.githubusercontent.com': {\n        '^/.*\\\\.png(\\\\?.*)?$': {\n          cacheDays: 365,\n          desc: '用户在PR或issue等内容中上传的图片，缓存1年。注：每张图片都有唯一的ID，不会重复，可以安心缓存',\n        },\n      },\n      'private-user-images.githubusercontent.com': {\n        '^/.*\\\\.png(\\\\?.*)?$': {\n          cacheDays: 30,\n          cacheHours: null,\n          desc: '用户在PR或issue等内容中上传的图片，缓存30天',\n        },\n      },\n      'avatars.githubusercontent.com': {\n        '^/u/\\\\d+(\\\\?.*)?$': {\n          cacheDays: 365,\n          desc: '用户头像，缓存1年',\n        },\n      },\n      'api.github.com': {\n        '^/_private/browser/stats$': {\n          success: true,\n          desc: 'github的访问速度分析上传，没有必要，直接返回成功',\n        },\n        '.*': {\n          sni: 'baidu.com',\n        },\n      },\n      '*.docker.com': {\n        '.*': {\n          sni: 'baidu.com',\n        },\n      },\n      'login.docker.com': {\n        '/favicon.ico': {\n          proxy: 'hub.docker.com',\n          sni: 'baidu.com',\n          desc: '登录页面的ico，采用hub.docker.com的',\n        },\n      },\n      // google cdn\n      'www.google.com': {\n        '/recaptcha/.*': { proxy: 'www.recaptcha.net' },\n        // '.*': {\n        //   proxy: 'gg.docmirror.top/_yxorp',\n        //   desc: '呀，被你发现了，偷偷的用，别声张'\n        // }\n      },\n      'www.gstatic.com': {\n        '/recaptcha/.*': { proxy: 'www.recaptcha.net' },\n      },\n      'ajax.googleapis.com': {\n        '.*': {\n          proxy: 'ajax.lug.ustc.edu.cn',\n          backup: ['gapis.geekzu.org'],\n          test: 'ajax.googleapis.com/ajax/libs/jquery/1.12.4/jquery.min.js',\n        },\n      },\n      'fonts.googleapis.com': {\n        '.*': {\n          proxy: 'fonts.loli.net',\n          test: 'https://fonts.googleapis.com/css?family=Oswald',\n        },\n      },\n      'themes.googleapis.com': {\n        '.*': {\n          proxy: 'themes.loli.net',\n          backup: ['themes.proxy.ustclug.org'],\n        },\n      },\n      'themes.googleusercontent.com': {\n        '.*': { proxy: 'google-themes.proxy.ustclug.org' },\n      },\n      // 'fonts.gstatic.com': {\n      //   '.*': {\n      //     proxy: 'gstatic.loli.net',\n      //     backup: ['fonts-gstatic.proxy.ustclug.org']\n      //   }\n      // },\n      'clients*.google.com': { '.*': { abort: false, desc: '设置abort：true可以快速失败，节省时间' } },\n      'www.googleapis.com': { '.*': { abort: false, desc: '设置abort：true可以快速失败，节省时间' } },\n      'lh*.googleusercontent.com': { '.*': { abort: false, desc: '设置abort：true可以快速失败，节省时间' } },\n      // mapbox-node-binary.s3.amazonaws.com/sqlite3/v5.0.0/napi-v3-win32-x64.tar.gz\n      '*.s3.1amazonaws1.com': {\n        '/sqlite3/.*': {\n          redirect: 'npm.taobao.org/mirrors',\n        },\n      },\n      // 'packages.elastic.co': { '.*': { proxy: 'elastic.proxy.ustclug.org' } },\n      // 'ppa.launchpad.net': { '.*': { proxy: 'launchpad.proxy.ustclug.org' } },\n      // 'archive.cloudera.com': { '.*': { regexp: '/cdh5/.*', proxy: 'cloudera.proxy.ustclug.org' } },\n      // 'downloads.lede-project.org': { '.*': { proxy: 'lede.proxy.ustclug.org' } },\n      // 'downloads.openwrt.org': { '.*': { proxy: 'openwrt.proxy.ustclug.org' } },\n      // 'secure.gravatar.com': { '.*': { proxy: 'gravatar.proxy.ustclug.org' } },\n      '*.carbonads.com': {\n        '/carbon.*': {\n          abort: true,\n          desc: '广告拦截',\n        },\n      },\n      '*.buysellads.com': {\n        '/ads/.*': {\n          abort: true,\n          desc: '广告拦截',\n        },\n      },\n    },\n    // 预设置IP列表\n    preSetIpList: {\n      'github.com': {\n        '4.237.22.38': true,\n        '20.26.156.215': true,\n        '20.27.177.113': true,\n        '20.87.245.0': true,\n        '20.200.245.247': true,\n        '20.201.28.151': true,\n        '20.205.243.166': true,\n        '140.82.113.3': true,\n        '140.82.114.4': true,\n        '140.82.116.3': true,\n        '140.82.116.4': true,\n        '140.82.121.3': true,\n        '140.82.121.4': true,\n      },\n      'api.github.com': {\n        '20.26.156.210': true,\n        '20.27.177.116': true,\n        '20.87.245.6': true,\n        '20.200.245.245': true,\n        '20.201.28.148': true,\n        '20.205.243.168': true,\n        '20.248.137.49': true,\n        '140.82.112.5': true,\n        '140.82.113.6': true,\n        '140.82.116.6': true,\n        '140.82.121.6': true,\n      },\n      'codeload.github.com': {\n        '20.26.156.216': true,\n        '20.27.177.114': true,\n        '20.87.245.7': true,\n        '20.200.245.246': true,\n        '20.201.28.149': true,\n        '20.205.243.165': true,\n        '20.248.137.55': true,\n        '140.82.113.9': true,\n        '140.82.114.10': true,\n        '140.82.116.10': true,\n        '140.82.121.9': true,\n      },\n      '*.githubusercontent.com': {\n        '146.75.92.133': true,\n        '199.232.88.133': true,\n        '199.232.144.133': true,\n      },\n      'viewscreen.githubusercontent.com': {\n        '140.82.112.21': true,\n        '140.82.112.22': true,\n        '140.82.113.21': true,\n        '140.82.113.22': true,\n        '140.82.114.21': true,\n        '140.82.114.22': true,\n      },\n      'github.io': {\n        '185.199.108.153': true,\n        '185.199.109.153': true,\n        '185.199.110.153': true,\n        '185.199.111.153': true,\n      },\n      '*.githubassets.com': {\n        '185.199.108.154': true,\n        '185.199.109.154': true,\n        '185.199.110.154': true,\n        '185.199.111.154': true,\n      },\n      '^(analytics|ghcc)\\\\.githubassets\\\\.com$': {\n        '185.199.108.153': true,\n        '185.199.110.153': true,\n        '185.199.109.153': true,\n        '185.199.111.153': true,\n      },\n      '*.pixiv.net': {\n        // 以下为 `cdn-origin.pixiv.net` 域名的IP\n        '210.140.139.154': true,\n        '210.140.139.157': true,\n        '210.140.139.160': true,\n      },\n      'hub.docker.com': {\n        '44.221.37.199': true,\n        '52.44.227.212': true,\n        '54.156.140.159': true,\n      },\n      'sessions-bugsnag.docker.com': {\n        '44.221.37.199': true,\n        '52.44.227.212': true,\n        '54.156.140.159': true,\n      },\n    },\n    whiteList: {\n      '*.cn': true,\n      'cn.*': true,\n      '*china*': true,\n      '*.dingtalk.com': true,\n      '*.apple.com': true,\n      '*.microsoft.com': true,\n      '*.alipay.com': true,\n      '*.qq.com': true,\n      '*.baidu.com': true,\n      '192.168.*': true,\n    },\n    dns: {\n      providers: {\n        aliyun: {\n          type: 'https',\n          server: 'https://dns.alidns.com/dns-query',\n          cacheSize: 1000,\n        },\n        cloudflare: {\n          type: 'https',\n          server: 'https://1.1.1.1/dns-query',\n          cacheSize: 1000,\n        },\n        quad9: {\n          type: 'https',\n          server: 'https://9.9.9.9/dns-query',\n          cacheSize: 1000,\n        },\n        safe360: {\n          type: 'https',\n          server: 'https://doh.360.cn/dns-query',\n          cacheSize: 1000,\n          forSNI: true,\n        },\n        rubyfish: {\n          type: 'https',\n          server: 'https://rubyfish.cn/dns-query',\n          cacheSize: 1000,\n        },\n      },\n      mapping: {\n        '*.github.com': 'quad9',\n        '*github*.com': 'quad9',\n        '*.github.io': 'quad9',\n        '*.docker.com': 'quad9',\n        '*.stackoverflow.com': 'quad9',\n        '*.electronjs.org': 'quad9',\n        '*.amazonaws.com': 'quad9',\n        '*.yarnpkg.com': 'quad9',\n        '*.cloudfront.net': 'quad9',\n        '*.cloudflare.com': 'quad9',\n        'img.shields.io': 'quad9',\n        '*.vuepress.vuejs.org': 'quad9',\n        '*.gh.docmirror.top': 'quad9',\n        '*.v2ex.com': 'quad9',\n        '*.pypi.org': 'quad9',\n        '*.jetbrains.com': 'quad9',\n        '*.azureedge.net': 'quad9',\n      },\n      speedTest: {\n        enabled: true,\n        interval: 300000,\n        hostnameList: ['github.com'],\n        dnsProviders: ['cloudflare', 'safe360', 'rubyfish'],\n      },\n    },\n  },\n  proxy: {},\n  plugin: {},\n  help: {\n    dataList: [\n      {\n        title: '查看DevSidecar的说明文档（Wiki）',\n        url: 'https://github.com/docmirror/dev-sidecar/wiki',\n      },\n      {\n        title: '为了展示更多帮助信息，请启用 “远程配置” 功能！！！',\n      },\n    ],\n  },\n}\n\n// 从本地文件中加载配置\ndefaultConfig.configFromFiles = configLoader.getConfigFromFiles(configLoader.getUserConfig(), defaultConfig)\n\nmodule.exports = defaultConfig\n"
  },
  {
    "path": "packages/core/src/config/local-config-loader.js",
    "content": "const fs = require('node:fs')\nconst path = require('node:path')\nconst lodash = require('lodash')\nconst jsonApi = require('@docmirror/mitmproxy/src/json')\nconst mergeApi = require('../merge')\nconst logOrConsole = require('../utils/util.log-or-console')\n\nfunction getUserBasePath (autoCreate = true) {\n  const userHome = process.env.USERPROFILE || process.env.HOME || '/'\n  const dir = path.resolve(userHome, './.dev-sidecar')\n\n  // 自动创建目录\n  if (autoCreate && !fs.existsSync(dir)) {\n    fs.mkdirSync(dir)\n  }\n\n  return dir\n}\n\nfunction loadConfigFromFile (configFilePath) {\n  if (configFilePath == null) {\n    logOrConsole.error('配置文件地址为空')\n    return {}\n  }\n\n  if (!fs.existsSync(configFilePath)) {\n    logOrConsole.info('配置文件不存在:', configFilePath)\n    return {} // 文件不存在，返回空配置\n  }\n\n  // 读取配置文件\n  let configStr\n  try {\n    configStr = fs.readFileSync(configFilePath)\n  } catch (e) {\n    logOrConsole.error('读取配置文件失败:', configFilePath, ', error:', e)\n    return {}\n  }\n\n  // 解析配置文件\n  try {\n    const config = jsonApi.parse(configStr)\n    logOrConsole.info('读取配置文件成功:', configFilePath)\n    return config\n  } catch (e) {\n    logOrConsole.error(`解析配置文件失败，文件内容格式不正确，文件路径: ${configFilePath}，文件内容：${configStr}，error:`, e)\n    return {}\n  }\n}\n\nfunction getUserConfigPath () {\n  const dir = getUserBasePath()\n\n  // 兼容1.7.3及以下版本的配置文件处理逻辑\n  const newFilePath = path.join(dir, '/config.json')\n  const oldFilePath = path.join(dir, '/config.json5')\n  if (!fs.existsSync(newFilePath) && fs.existsSync(oldFilePath)) {\n    return oldFilePath // 如果新文件不存在，但旧文件存在，则返回旧文件路径\n  }\n\n  return newFilePath\n}\n\nfunction getUserConfig () {\n  const configFilePath = getUserConfigPath()\n  return loadConfigFromFile(configFilePath)\n}\n\nfunction getRemoteConfigPath (suffix = '') {\n  const dir = getUserBasePath()\n  return path.join(dir, `/remote_config${suffix}.json5`)\n}\n\nfunction getRemoteConfig (suffix = '') {\n  const remoteConfigFilePath = getRemoteConfigPath(suffix)\n  return loadConfigFromFile(remoteConfigFilePath)\n}\n\nfunction getAutomaticCompatibleConfigPath () {\n  const dir = getUserBasePath()\n  return path.join(dir, '/automaticCompatibleConfig.json')\n}\n\n/**\n * 从文件读取配置\n *\n * @param userConfig 用户配置\n * @param defaultConfig 默认配置\n */\nfunction getConfigFromFiles (userConfig, defaultConfig) {\n  const merged = userConfig != null ? lodash.cloneDeep(userConfig) : {}\n\n  const personalRemoteConfig = getRemoteConfig('_personal')\n  const shareRemoteConfig = getRemoteConfig()\n\n  mergeApi.doMerge(merged, personalRemoteConfig) // 先合并一次个人远程配置，使配置顺序在前\n  mergeApi.doMerge(merged, shareRemoteConfig) // 先合并一次共享远程配置，使配置顺序在前\n  mergeApi.doMerge(merged, defaultConfig) // 合并默认配置，顺序排在最后\n  mergeApi.doMerge(merged, shareRemoteConfig) // 再合并一次共享远程配置，使配置生效\n  mergeApi.doMerge(merged, personalRemoteConfig) // 再合并一次个人远程配置，使配置生效\n\n  if (userConfig != null) {\n    mergeApi.doMerge(merged, userConfig) // 再合并一次用户配置，使用户配置重新生效\n  }\n\n  // 删除为null及[delete]的项\n  mergeApi.deleteNullItems(merged)\n\n  logOrConsole.info('加载及合并远程配置完成')\n  return merged\n}\n\nmodule.exports = {\n  getUserBasePath,\n\n  loadConfigFromFile,\n\n  getUserConfigPath,\n  getUserConfig,\n\n  getRemoteConfigPath,\n  getRemoteConfig,\n\n  getAutomaticCompatibleConfigPath,\n\n  getConfigFromFiles,\n}\n"
  },
  {
    "path": "packages/core/src/config/remote_config.json5",
    "content": "{\n  \"server\": {\n    \"compatible\": {\n      \"connect\": {\n        \"218.18.106.132:443\": {\n          \"ssl\": true\n        }\n      },\n      \"request\": {\n        \"218.18.106.132:443\": {\n          \"rejectUnauthorized\": false\n        }\n      }\n    },\n    \"intercepts\": {\n      \"github.com\": {\n        \"^(/[\\\\w-.]+){2,}/?(\\\\?.*)?$\": {\n          \"tampermonkeyScript\": \"https://gitee.com/wangliang181230/dev-sidecar/raw/scripts/tampermonkey.js\",\n          \"script\": \"https://gitee.com/wangliang181230/dev-sidecar/raw/scripts/GithubEnhanced-High-Speed-Download.user.js\"\n        },\n        \"^(/[^/]+){2}/releases/download/.*$\": {\n          \"redirect\": \"ghp.ci/https://github.com\",\n          \"desc\": \"release文件加速下载重定向地址\"\n        },\n        \"^(/[^/]+){2}/archive/.*\\\\.(zip|tar.gz)$\": {\n          \"redirect\": \"ghp.ci/https://github.com\",\n          \"desc\": \"release源代码加速下载重定向地址\"\n        },\n        \"^((/[^/]+){2,})/raw((/[^/]+)+\\\\.(jpg|jpeg|png|gif))(\\\\?.*)?$\": {\n          \"sni\": \"baidu.com\" // proxy拦截器不会使用 .* 中的sni配置，故补充此配置\n        },\n        \"^((/[^/]+){2,})/raw((/[^/]+)+\\\\.js)(\\\\?.*)?$\": {\n          \"sni\": \"baidu.com\" // proxy拦截器不会使用 .* 中的sni配置，故补充此配置\n        }\n      },\n      \"api.github.com\": {\n        \".*\": {\n          \"sni\": \"baidu.com\"\n        }\n      },\n      \"github.githubassets.com\": {\n        \".*\": {\n          \"sni\": \"baidu.com\"\n        }\n      },\n      \"avatars.githubusercontent.com\": {\n        \".*\": {\n          \"sni\": \"baidu.com\"\n        }\n      },\n      \"camo.githubusercontent.com\": {\n        \".*\": {\n          \"sni\": \"baidu.com\"\n        }\n      },\n      \"collector.github.com\": {\n        \".*\": {\n          \"sni\": \"baidu.com\"\n        }\n      },\n      \"www.gstatic.com\": {\n        \"/recaptcha/.*\": {\n          \"proxy\": \"www.recaptcha.net\"\n        }\n      }\n    },\n    \"preSetIpList\": {\n      \"github.com\": [\n        \"4.237.22.38\",\n        \"20.26.156.215\",\n        \"20.27.177.113\",\n        \"20.87.245.0\",\n        \"20.200.245.247\",\n        \"20.201.28.151\",\n        \"20.205.243.166\",\n        \"140.82.113.3\",\n        \"140.82.114.4\",\n        \"140.82.116.3\",\n        \"140.82.116.4\",\n        \"140.82.121.3\",\n        \"140.82.121.4\"\n      ],\n      \"hub.docker.com\": null // 1.8.2版本中，该域名的预设IP有问题，现在远程配置中删除\n    },\n    \"dns\": {\n      \"mapping\": {\n        \"*.jetbrains.com\": \"quad9\",\n        \"*.azureedge.net\": \"quad9\",\n        \"*.stackoverflow.com\": \"quad9\"\n      },\n      \"speedTest\": {\n        \"interval\": 300000\n      }\n    },\n    \"whiteList\": {\n      \"*.icloud.com\": true,\n      \"*.lenovo.net\": true\n    }\n  },\n  \"proxy\": {\n    \"remoteDomesticDomainAllowListFileUrl\": \"https://raw.kkgithub.com/pluwen/china-domain-allowlist/main/allow-list.sorl\",\n    \"excludeIpList\": {\n      // Github文件上传所使用的域名，被DS代理会导致文件上传经常失败，从系统代理中排除掉\n      \"objects-origin.githubusercontent.com\": true,\n      // Github通过Actions上传的文件，下载时所需的域名，从系统代理中排除掉，否则下载会失败\n      \"*.windows.net\": true,\n      // Github下载release文件的高速镜像地址\n      \"*.ghproxy.net\": true,\n      \"*.ghp.ci\": true,\n      \"*.kkgithub.com\": true,\n\n      // Github建站域名\n      \"*.github.io\": true,\n\n      // bilibili相关\n      \"*.bilicomic.com\": true,\n\n      // 中国移动云盘登录API\n      \"[2049:8c54:813:10c::140]\": true,\n      \"[2409:8a0c:a442:ff40:a51f:4b9c:8b41:25ea]\": true,\n      \"[2606:2800:147:120f:30c:1ba0:fc6:265a]\": true,\n      // 移动云盘相关\n      \"*.cmicapm.com\": true,\n\n      // cloudflare：排除以下域名，cloudflare的人机校验会更快，成功率更高。\n      \"*.cloudflare.com\": true,\n      \"*.cloudflare-cn.com\": true,\n\n      // VS相关\n      \"*.microsoftonline.com\": true, // 此域名不排除的话，部分功能将出现异常\n      \"*.msecnd.net\": true,\n      \"*.msedge.net\": true,\n\n      // 卡巴斯基升级域名\n      \"*kaspersky*.com\": true,\n      \"*.upd.kaspersky.com\": true,\n\n      // sandbox沙盒域名\n      \"*.sandboxie-plus.com\": true,\n\n      // 无忧论坛\n      \"*.wuyou.net\": true,\n\n      // python建图包域名（浏览器）\n      \"*.pyecharts.org\": true,\n\n      // 教育网站\n      \"*.bcloudlink.com\": true,\n\n      // 奇迹秀（资源）\n      \"*.qijishow.com\": true,\n\n      // Z-Library\n      \"*.z-lib.fo\": true,\n\n      // Finalshell（Linux学习网）\n      \"*.finalshell.com\": true,\n\n      // MineBBS（我的世界中文论坛）\n      \"*.minebbs.com\": true,\n\n      // 我的世界插件网\n      \"*.spigotmc.org\": true,\n\n      // bd测试\n      \"*.virustotal.com\": true,\n\n      // 未知\n      \"*.youdemai.com\": true,\n      \"*.casualthink.com\": true,\n      \"44.239.165.12\": true,\n      \"3.164.110.117\": true\n    }\n  },\n  \"plugin\": {\n    \"overwall\": {\n      \"targets\": {\n        \"*.github.com\": true,\n        \"*github*.com\": true,\n        \"*.nodejs.org\": true,\n        \"*.npmjs.com\": true,\n        \"*.wikimedia.org\": true,\n        \"*.v2ex.com\": true,\n        \"*.azureedge.net\": true,\n        \"*.cloudfront.net\": true,\n        \"*.bing.com\": true,\n        \"*.discourse-cdn.com\": true,\n        \"*.gravatar.com\": true,\n        \"*.docker.com\": true,\n        \"*.vueuse.org\": true,\n        \"*.elastic.co\": true,\n        \"*.optimizely.com\": true,\n        \"*.stackpathcdn.com\": true,\n        \"*.fastly.net\": true,\n        \"*.cloudflare.com\": true,\n        \"*.233v2.com\": true,\n        \"*.v2fly.org\": true,\n        \"*.telegram.org\": true,\n        \"*.amazon.com\": true,\n        \"*.googleapis.com\": true,\n        \"*.google-analytics.com\": true,\n        \"*.cloudflareinsights.com\": true,\n        \"*.intlify.dev\": true,\n        \"*.segment.io\": true,\n        \"*.shields.io\": true,\n        \"*.jsdelivr.net\": true,\n        \"*.z-library.sk\": true,\n        \"*.zlibrary*.se\": true,\n\n        // 维基百科\n        \"*.wikipedia-on-ipfs.org\": true,\n\n        // ChatGPT\n        \"*.oaiusercontent.com\": true, // 在ChatGPT中生成文件并下载所需的域名\n\n        // Pixiv相关\n        \"*.pixiv.org\": true,\n        \"*.fanbox.cc\": true,\n        \"*.onesignal.com\": true // pixiv站点，会加载该域名下的js脚本\n      },\n      \"pac\": {\n        \"pacFileUpdateUrl\": \"https://raw.kkgithub.com/gfwlist/gfwlist/master/gfwlist.txt\"\n      }\n    }\n  }\n}\n"
  },
  {
    "path": "packages/core/src/config-api.js",
    "content": "const fs = require('node:fs')\nconst jsonApi = require('@docmirror/mitmproxy/src/json')\nconst lodash = require('lodash')\nconst request = require('request')\nconst defConfig = require('./config/index.js')\nconst mergeApi = require('./merge.js')\nconst Shell = require('./shell')\nconst log = require('./utils/util.log.core')\nconst configLoader = require('./config/local-config-loader')\n\nlet configTarget = lodash.cloneDeep(defConfig)\n\nfunction get () {\n  return configTarget\n}\n\nlet timer\nconst configApi = {\n  async startAutoDownloadRemoteConfig () {\n    if (timer != null) {\n      clearInterval(timer)\n    }\n    const download = async () => {\n      try {\n        await configApi.downloadRemoteConfig()\n        configApi.reload()\n      } catch (e) {\n        log.error('定时下载远程配置并重载配置失败', e)\n      }\n    }\n    await download()\n    timer = setInterval(download, 24 * 60 * 60 * 1000) // 1天\n  },\n  async downloadRemoteConfig () {\n    if (get().app.remoteConfig.enabled !== true) {\n      // 删除保存的远程配置文件\n      configApi.deleteRemoteConfigFile()\n      configApi.deleteRemoteConfigFile('_personal')\n      return\n    }\n\n    const remoteConfig = get().app.remoteConfig\n    await configApi.doDownloadRemoteConfig(remoteConfig.url)\n    await configApi.doDownloadRemoteConfig(remoteConfig.personalUrl, '_personal')\n  },\n  doDownloadRemoteConfig (remoteConfigUrl, suffix = '') {\n    if (!remoteConfigUrl) {\n      // 删除保存的远程配置文件\n      configApi.deleteRemoteConfigFile(suffix)\n      return\n    }\n\n    return new Promise((resolve, reject) => {\n      log.info('开始下载远程配置:', remoteConfigUrl)\n\n      const headers = {\n        'Cache-Control': 'no-cache', // 禁止使用缓存\n        'Pragma': 'no-cache', // 禁止使用缓存\n      }\n      if (remoteConfigUrl.startsWith('https://raw.githubusercontent.com/')) {\n        headers['Server-Name'] = 'baidu.com'\n      }\n      request(remoteConfigUrl, { headers }, (error, response, body) => {\n        if (error) {\n          log.error(`下载远程配置失败: ${remoteConfigUrl}, error:`, error, ', response:', response, ', body:', body)\n          reject(error)\n          return\n        }\n        if (response && response.statusCode === 200) {\n          if (body == null || body.length < 2) {\n            log.warn('下载远程配置成功，但内容为空:', remoteConfigUrl)\n            resolve()\n            return\n          } else {\n            log.info('下载远程配置成功:', remoteConfigUrl)\n          }\n\n          // 尝试解析远程配置，如果解析失败，则不保存它\n          let remoteConfig\n          try {\n            remoteConfig = jsonApi.parse(body)\n          } catch {\n            log.error(`远程配置内容格式不正确, url: ${remoteConfigUrl}, body: ${body}`)\n            remoteConfig = null\n          }\n\n          if (remoteConfig != null) {\n            const remoteSavePath = configLoader.getRemoteConfigPath(suffix)\n            try {\n              fs.writeFileSync(remoteSavePath, body)\n              log.info('保存远程配置文件成功:', remoteSavePath)\n            } catch (e) {\n              log.error('保存远程配置文件失败:', remoteSavePath, ', error:', e)\n              reject(new Error(`保存远程配置文件失败: ${e.message}`))\n              return\n            }\n          } else {\n            log.warn('远程配置对象为空:', remoteConfigUrl)\n          }\n\n          resolve()\n        } else {\n          log.error(`下载远程配置失败: ${remoteConfigUrl}, response:`, response, ', body:', body)\n\n          let message\n          if (response) {\n            message = `下载远程配置失败: ${remoteConfigUrl}, message: ${response.message}, code: ${response.statusCode}`\n          } else {\n            message = `下载远程配置失败: response: ${response}`\n          }\n          reject(new Error(message))\n        }\n      })\n    })\n  },\n  deleteRemoteConfigFile (suffix = '') {\n    const remoteSavePath = configLoader.getRemoteConfigPath(suffix)\n    if (fs.existsSync(remoteSavePath)) {\n      fs.unlinkSync(remoteSavePath)\n      log.info('删除远程配置文件成功:', remoteSavePath)\n    }\n  },\n  readRemoteConfigStr (suffix = '') {\n    try {\n      const path = configLoader.getRemoteConfigPath(suffix)\n      if (fs.existsSync(path)) {\n        const file = fs.readFileSync(path)\n        log.info('读取远程配置文件内容成功:', path)\n        return file.toString()\n      } else {\n        log.info('远程配置文件不存在:', path)\n      }\n    } catch (e) {\n      log.error('读取远程配置文件内容失败:', e)\n    }\n\n    return '{}'\n  },\n  /**\n   * 保存自定义的 config\n   * @param newConfig\n   */\n  save (newConfig) {\n    // 对比默认config的异同\n    const defConfig = configApi.cloneDefault()\n\n    // 如果开启了远程配置，则读取远程配置，合并到默认配置中\n    if (get().app.remoteConfig.enabled === true) {\n      if (get().app.remoteConfig.url) {\n        mergeApi.doMerge(defConfig, configLoader.getRemoteConfig())\n      }\n      if (get().app.remoteConfig.personalUrl) {\n        mergeApi.doMerge(defConfig, configLoader.getRemoteConfig('_personal'))\n      }\n    }\n\n    // 计算新配置与默认配置（启用远程配置时，含远程配置）的差异\n    const diffConfig = mergeApi.doDiff(defConfig, newConfig)\n\n    // 将差异作为用户配置保存到 config.json 中\n    const configPath = configLoader.getUserConfigPath()\n    try {\n      fs.writeFileSync(configPath, jsonApi.stringify(diffConfig))\n      log.info('保存 config.json 自定义配置文件成功:', configPath)\n    } catch (e) {\n      log.error('保存 config.json 自定义配置文件失败:', configPath, ', error:', e)\n      throw e\n    }\n\n    // 重载配置\n    const allConfig = configApi.set(diffConfig)\n\n    return {\n      diffConfig,\n      allConfig,\n    }\n  },\n  doMerge: mergeApi.doMerge,\n  doDiff: mergeApi.doDiff,\n  /**\n   * 读取 config.json 后，合并配置\n   */\n  reload () {\n    const userConfig = configLoader.getUserConfig()\n    return configApi.set(userConfig) || {}\n  },\n  update (partConfig) {\n    const newConfig = lodash.merge(configApi.get(), partConfig)\n    configApi.save(newConfig)\n  },\n  get,\n  set (newConfig) {\n    if (newConfig == null) {\n      log.warn('newConfig 为空，不做任何操作')\n      return configTarget\n    }\n    return configApi.load(newConfig)\n  },\n  load (newConfig) {\n    const config = configLoader.getConfigFromFiles(newConfig, defConfig)\n    configTarget = config\n    return config\n  },\n  cloneDefault () {\n    return lodash.cloneDeep(defConfig)\n  },\n  addDefault (key, defValue) {\n    lodash.set(defConfig, key, defValue)\n  },\n  // 移除用户配置，用于恢复出厂设置功能\n  async removeUserConfig () {\n    const configPath = configLoader.getUserConfigPath()\n    if (fs.existsSync(configPath)) {\n      // 读取 config.json 文件内容\n      const fileOriginalStr = fs.readFileSync(configPath).toString()\n\n      // 判断文件内容是否为空或空配置\n      const fileStr = fileOriginalStr.replace(/\\s/g, '')\n      if (fileStr.length < 5) {\n        try {\n          fs.writeFileSync(configPath, '{}')\n        } catch (e) {\n          log.warn('简化用户配置文件失败:', configPath, ', error:', e)\n        }\n        return false // config.json 内容为空，或为空json\n      }\n\n      // 备份用户自定义配置文件\n      const bakConfigPath = `${configPath}.${Date.now()}.bak.json`\n      try {\n        fs.writeFileSync(bakConfigPath, fileOriginalStr)\n        log.info('备份用户配置文件成功:', bakConfigPath)\n      } catch (e) {\n        log.error('备份用户配置文件失败:', bakConfigPath, ', error:', e)\n        throw e\n      }\n      // 原配置文件内容设为空\n      try {\n        fs.writeFileSync(configPath, '{}')\n      } catch (e) {\n        log.error('初始化用户配置文件失败:', configPath, ', error:', e)\n        throw e\n      }\n\n      // 重新加载配置\n      configApi.load(null)\n\n      return true // 删除并重新加载配置成功\n    } else {\n      return false // config.json 文件不存在\n    }\n  },\n  resetDefault (key) {\n    if (key) {\n      let value = lodash.get(defConfig, key)\n      value = lodash.cloneDeep(value)\n      lodash.set(configTarget, key, value)\n    } else {\n      configTarget = lodash.cloneDeep(defConfig)\n    }\n    return configTarget\n  },\n  async getVariables (type) {\n    const method = type === 'npm' ? Shell.getNpmEnv : Shell.getSystemEnv\n    const currentMap = await method()\n    const list = []\n    const map = configTarget.variables[type]\n    for (const key in map) {\n      const exists = currentMap[key] != null\n      list.push({\n        key,\n        value: map[key],\n        exists,\n      })\n    }\n    return list\n  },\n  async setVariables (type) {\n    const list = await configApi.getVariables(type)\n    const noSetList = list.filter((item) => {\n      return !item.exists\n    })\n    if (list.length > 0) {\n      const context = {\n        root_ca_cert_path: configApi.get().server.setting.rootCaFile.certPath,\n      }\n      for (const item of noSetList) {\n        if (item.value.includes('${')) {\n          for (const key in context) {\n            item.value = item.value.replcace(new RegExp(`\\${${key}}`, 'g'), context[key])\n          }\n        }\n      }\n      const method = type === 'npm' ? Shell.setNpmEnv : Shell.setSystemEnv\n      return method({ list: noSetList })\n    }\n  },\n}\n\nmodule.exports = configApi\n"
  },
  {
    "path": "packages/core/src/event.js",
    "content": "const listener = {}\nlet index = 1\nfunction register (channel, handle, order = 10) {\n  let handles = listener[channel]\n  if (handles == null) {\n    handles = listener[channel] = []\n  }\n  handles.push({ id: index, handle, order })\n  handles.sort((a, b) => {\n    return a.order - b.order\n  })\n  return index++\n}\nfunction fire (channel, event) {\n  const handles = listener[channel]\n  if (handles == null) {\n    return\n  }\n  for (const item of handles) {\n    item.handle(event)\n  }\n}\n\nfunction unregister (id) {\n  for (const key in listener) {\n    const handlers = listener[key]\n    for (let i = 0; i < handlers.length; i++) {\n      const handle = handlers[i]\n      if (handle.id === id) {\n        handlers.splice(i)\n        return\n      }\n    }\n  }\n}\nconst EventHub = {\n  register,\n  fire,\n  unregister,\n}\nmodule.exports = EventHub\n"
  },
  {
    "path": "packages/core/src/expose.js",
    "content": "const lodash = require('lodash')\nconst config = require('./config-api')\nconst event = require('./event')\nconst modules = require('./modules')\nconst shell = require('./shell')\nconst status = require('./status')\nconst log = require('./utils/util.log.core')\n\nconst context = {\n  config,\n  shell,\n  status,\n  event,\n  log,\n}\n\nfunction setupPlugin (key, plugin, context, config) {\n  const pluginConfig = plugin.config\n  const PluginClass = plugin.plugin\n  const pluginStatus = plugin.status\n  const api = PluginClass(context)\n  config.addDefault(key, pluginConfig)\n  if (pluginStatus) {\n    lodash.set(status, key, pluginStatus)\n  }\n  return api\n}\n\nconst proxy = setupPlugin('proxy', modules.proxy, context, config)\nconst plugin = {}\nfor (const key in modules.plugin) {\n  const target = modules.plugin[key]\n  const api = setupPlugin(`plugin.${key}`, target, context, config)\n  plugin[key] = api\n}\nconfig.resetDefault()\nconst server = modules.server\nconst serverStart = server.start\n\nfunction newServerStart ({ mitmproxyPath }) {\n  return serverStart({ mitmproxyPath, plugins: plugin })\n}\nserver.start = newServerStart\nasync function startup ({ mitmproxyPath }) {\n  const conf = config.get()\n  if (conf.server.enabled) {\n    try {\n      await server.start({ mitmproxyPath })\n    } catch (err) {\n      log.error('代理服务启动失败：', err)\n    }\n  }\n  if (conf.proxy.enabled) {\n    try {\n      await proxy.start()\n    } catch (err) {\n      log.error('开启系统代理失败：', err)\n    }\n  }\n  try {\n    const plugins = []\n    for (const key in plugin) {\n      if (conf.plugin[key].enabled) {\n        const start = async () => {\n          try {\n            await plugin[key].start()\n            log.info(`插件【${key}】已启动`)\n          } catch (err) {\n            log.error(`插件【${key}】启动失败:`, err)\n          }\n        }\n        plugins.push(start())\n      }\n    }\n    if (plugins && plugins.length > 0) {\n      await Promise.all(plugins)\n    }\n  } catch (err) {\n    log.error('开启插件失败：', err)\n  }\n}\n\nasync function shutdown () {\n  try {\n    const plugins = []\n    for (const key in plugin) {\n      if (status.plugin[key] && status.plugin[key].enabled && plugin[key].close) {\n        const close = async () => {\n          try {\n            await plugin[key].close()\n            log.info(`插件【${key}】已关闭`)\n          } catch (err) {\n            log.error(`插件【${key}】关闭失败:`, err)\n          }\n        }\n        plugins.push(close())\n      }\n    }\n    if (plugins.length > 0) {\n      await Promise.all(plugins)\n    }\n  } catch (error) {\n    log.error('插件关闭失败:', error)\n  }\n\n  if (status.proxy.enabled) {\n    try {\n      await proxy.close()\n      log.info('系统代理已关闭')\n    } catch (err) {\n      log.error('系统代理关闭失败:', err)\n    }\n  }\n  if (status.server.enabled) {\n    try {\n      await server.close()\n      log.info('代理服务已关闭')\n    } catch (err) {\n      log.error('代理服务关闭失败:', err)\n    }\n  }\n}\n\nconst api = {\n  startup,\n  shutdown,\n  status: {\n    get () {\n      return status\n    },\n  },\n  config,\n  event,\n  shell,\n  server,\n  proxy,\n  plugin,\n  log,\n}\nmodule.exports = {\n  status,\n  api,\n}\n"
  },
  {
    "path": "packages/core/src/index.js",
    "content": "const expose = require('./expose.js')\nconst log = require('./utils/util.log.core')\n// process.env.NODE_TLS_REJECT_UNAUTHORIZED = '0'\n\n// 避免异常崩溃\nprocess.on('uncaughtException', (err) => {\n  log.error('Process Uncaught Exception:', err)\n})\n\nprocess.on('unhandledRejection', (reason, p) => {\n  log.error('Process Unhandled Rejection at: Promise:', p, ', reason:', reason)\n  // application specific logging, throwing an error, or other logic here\n})\n\nmodule.exports = expose\n"
  },
  {
    "path": "packages/core/src/merge.js",
    "content": "const lodash = require('lodash')\n\n/**\n * 找出 newObj 相对于 oldObj 有差异的部分\n *\n * @param oldObj\n * @param newObj\n * @returns {{}|*}\n */\nfunction doDiff (oldObj, newObj) {\n  if (newObj == null) {\n    return oldObj\n  }\n\n  // 临时的对象，用于找出被删除的数据\n  const tempObj = { ...oldObj }\n  // 删除空项，使差异对象更干净一些，体现出用户自定义内容\n  deleteNullItems(tempObj)\n\n  // 保存差异的对象\n  const diffObj = {}\n\n  // 读取新对象，并解析\n  for (const key in newObj) {\n    const newValue = newObj[key]\n    const oldValue = oldObj[key]\n\n    // 新值不为空，旧值为空时，直接取新值\n    if (newValue != null && oldValue == null) {\n      diffObj[key] = newValue\n      continue\n    }\n    // 新旧值相等时，忽略\n    if (lodash.isEqual(newValue, oldValue)) {\n      delete tempObj[key]\n      continue\n    }\n    // 新的值为数组时，直接取新值\n    if (lodash.isArray(newValue)) {\n      diffObj[key] = newValue\n      delete tempObj[key]\n      continue\n    }\n\n    // 新的值为对象时，递归合并\n    if (lodash.isObject(newValue)) {\n      diffObj[key] = doDiff(oldValue, newValue)\n      delete tempObj[key]\n      continue\n    }\n\n    // 基础类型，直接覆盖\n    delete tempObj[key]\n    diffObj[key] = newValue\n  }\n\n  // tempObj 里面剩下的是被删掉的数据\n  lodash.forEach(tempObj, (oldValue, key) => {\n    // 将被删除的属性设置为null，目的是为了merge时，将被删掉的对象设置为null，达到删除的目的\n    diffObj[key] = null\n  })\n\n  return diffObj\n}\n\nfunction deleteNullItems (target) {\n  lodash.forEach(target, (item, key) => {\n    if (item == null || item === '[delete]') {\n      delete target[key]\n    }\n    if (lodash.isObject(item)) {\n      deleteNullItems(item)\n    }\n  })\n}\n\nmodule.exports = {\n  doMerge (oldObj, newObj) {\n    return lodash.mergeWith(oldObj, newObj, (objValue, srcValue) => {\n      if (lodash.isArray(objValue)) {\n        return srcValue\n      }\n    })\n  },\n  doDiff,\n  deleteNullItems,\n}\n"
  },
  {
    "path": "packages/core/src/modules/index.js",
    "content": "module.exports = {\n  server: require('./server'),\n  proxy: require('./proxy'),\n  plugin: require('./plugin'),\n}\n"
  },
  {
    "path": "packages/core/src/modules/plugin/git/config.js",
    "content": "module.exports = {\n  name: 'Git.exe代理',\n  enabled: false,\n  tip: '如果你没有安装git命令行则不需要启动它',\n  setting: {\n    sslVerify: true, // Git.exe 是否关闭sslVerify，true=关闭 false=开启\n    noProxyUrls: {\n      'https://gitee.com': true, // 码云\n      'https://e.coding.net': true, // Coding（腾讯云）\n      'https://codeup.aliyun.com': true, // 云效 Codeup (阿里云)\n    },\n  },\n}\n"
  },
  {
    "path": "packages/core/src/modules/plugin/git/index.js",
    "content": "const pluginConfig = require('./config')\n\nconst Plugin = function (context) {\n  const { config, shell, event, log } = context\n  const pluginApi = {\n    async start () {\n      const ip = '127.0.0.1'\n      const port = config.get().server.port\n      await pluginApi.setProxy(ip, port)\n      return { ip, port }\n    },\n\n    async close () {\n      return pluginApi.unsetProxy()\n    },\n\n    async restart () {\n      await pluginApi.close()\n      await pluginApi.start()\n    },\n\n    isEnabled () {\n      return config.get().plugin.git.enabled\n    },\n\n    async save (newConfig) {\n\n    },\n\n    async setProxy (ip, port) {\n      const cmds = [\n        `git config --global http.proxy  http://${ip}:${port - 1} `,\n        `git config --global https.proxy http://${ip}:${port} `,\n      ]\n\n      if (config.get().plugin.git.setting.sslVerify === true) {\n        cmds.push('git config --global http.sslVerify false ')\n      }\n\n      if (config.get().plugin.git.setting.noProxyUrls != null) {\n        for (const url in config.get().plugin.git.setting.noProxyUrls) {\n          cmds.push(`git config --global http.\"${url}\".proxy \"\" `)\n        }\n      }\n\n      const ret = await shell.exec(cmds, { type: 'cmd' })\n      event.fire('status', { key: 'plugin.git.enabled', value: true })\n      log.info('开启【Git】代理成功')\n\n      return ret\n    },\n\n    // 当手动修改过 `~/.gitconfig` 时，`unset` 可能会执行失败，所以除了第一条命令外，其他命令都添加了try-catch，防止关闭Git代理失败\n    async unsetProxy () {\n      const ret = await shell.exec(['git config --global --unset http.proxy '], { type: 'cmd' })\n\n      try {\n        await shell.exec(['git config --global --unset https.proxy '], { type: 'cmd' })\n      } catch {\n      }\n\n      if (config.get().plugin.git.setting.sslVerify === true) {\n        try {\n          await shell.exec(['git config --global --unset http.sslVerify '], { type: 'cmd' })\n        } catch {\n        }\n      }\n\n      if (config.get().plugin.git.setting.noProxyUrls != null) {\n        for (const url in config.get().plugin.git.setting.noProxyUrls) {\n          try {\n            await shell.exec([`git config --global --unset http.\"${url}\".proxy `], { type: 'cmd' })\n          } catch {\n          }\n        }\n      }\n      event.fire('status', { key: 'plugin.git.enabled', value: false })\n      log.info('关闭【Git】代理成功')\n      return ret\n    },\n  }\n  return pluginApi\n}\n\nmodule.exports = {\n  key: 'git',\n  config: pluginConfig,\n  status: {\n    enabled: false,\n  },\n  plugin: Plugin,\n}\n"
  },
  {
    "path": "packages/core/src/modules/plugin/index.js",
    "content": "module.exports = {\n  node: require('./node'),\n  git: require('./git'),\n  pip: require('./pip'),\n  overwall: require('./overwall'),\n}\n"
  },
  {
    "path": "packages/core/src/modules/plugin/node/config.js",
    "content": "module.exports = {\n  name: 'NPM加速',\n  enabled: false,\n  tip: '如果你没有安装nodejs则不需要启动它',\n  startup: {\n    variables: true,\n  },\n  setting: {\n    'command': 'npm',\n    'strict-ssl': true,\n    'cafile': false,\n    'NODE_EXTRA_CA_CERTS': false,\n    'NODE_TLS_REJECT_UNAUTHORIZED': false,\n    'yarnRegistry': 'default',\n    'registry': 'https://registry.npmjs.org', // 可以选择切换官方或者淘宝镜像\n  },\n  variables: {\n    phantomjs_cdnurl: 'https://npmmirror.com/mirrors/phantomjs',\n    chromedriver_cdnurl: 'https://npmmirror.com/mirrors/chromedriver',\n    sass_binary_site: 'https://npmmirror.com/mirrors/node-sass',\n    ELECTRON_MIRROR: 'https://npmmirror.com/mirrors/electron/',\n    NVM_NODEJS_ORG_MIRROR: 'https://npmmirror.com/mirrors/node',\n    CHROMEDRIVER_CDNURL: 'https://npmmirror.com/mirrors/chromedriver',\n    OPERADRIVER: 'https://npmmirror.com/mirrors/operadriver',\n    ELECTRON_BUILDER_BINARIES_MIRROR: 'https://npmmirror.com/mirrors/electron-builder-binaries/',\n    PYTHON_MIRROR: 'https://npmmirror.com/mirrors/python',\n  },\n}\n"
  },
  {
    "path": "packages/core/src/modules/plugin/node/index.js",
    "content": "const jsonApi = require('@docmirror/mitmproxy/src/json')\nconst nodeConfig = require('./config')\n\nconst NodePlugin = function (context) {\n  const { config, shell, event, log } = context\n  const nodeApi = {\n    async start () {\n      try {\n        await nodeApi.setVariables()\n      } catch (err) {\n        log.warn('set variables error:', err)\n      }\n\n      const ip = '127.0.0.1'\n      const port = config.get().server.port\n      await nodeApi.setProxy(ip, port)\n      return { ip, port }\n    },\n\n    async close () {\n      return nodeApi.unsetProxy()\n    },\n\n    async restart () {\n      await nodeApi.close()\n      await nodeApi.start()\n    },\n\n    async save (newConfig) {\n      nodeApi.setVariables()\n    },\n    async getNpmEnv () {\n      const command = config.get().plugin.node.setting.command || 'npm'\n\n      const ret = await shell.exec([`${command} config list --json`], { type: 'cmd' })\n      if (ret != null) {\n        const json = ret.substring(ret.indexOf('{'))\n        return jsonApi.parse(json)\n      }\n      return {}\n    },\n\n    async setNpmEnv (list) {\n      const command = config.get().plugin.node.setting.command || 'npm'\n\n      const cmds = []\n      for (const item of list) {\n        if (item.value != null && item.value.length > 0 && item.value !== 'default' && item.value !== 'null') {\n          cmds.push(`${command} config set ${item.key}  ${item.value}`)\n        } else {\n          cmds.push(`${command} config delete ${item.key}`)\n        }\n      }\n      return await shell.exec(cmds, { type: 'cmd' })\n    },\n\n    async unsetNpmEnv (list) {\n      const command = config.get().plugin.node.setting.command || 'npm'\n\n      const cmds = []\n      for (const item of list) {\n        cmds.push(`${command} config delete ${item} `)\n      }\n      return await shell.exec(cmds, { type: 'cmd' })\n    },\n\n    async setYarnEnv (list) {\n      const cmds = []\n      log.debug('yarn set:', JSON.stringify(list))\n      for (const item of list) {\n        if (item.value != null && item.value.length > 0 && item.value !== 'default' && item.value !== 'null') {\n          cmds.push(`yarn config set ${item.key}  ${item.value}`)\n        } else {\n          cmds.push(`yarn config delete ${item.key}`)\n        }\n      }\n      return await shell.exec(cmds, { type: 'cmd' })\n    },\n\n    async unsetYarnEnv (list) {\n      const cmds = []\n      for (const item of list) {\n        cmds.push(`yarn config delete ${item} `)\n      }\n      return await shell.exec(cmds, { type: 'cmd' })\n    },\n\n    async getVariables () {\n      const currentMap = await nodeApi.getNpmEnv()\n      const list = []\n      const map = config.get().plugin.node.variables\n      for (const key in map) {\n        const exists = currentMap[key] != null\n        list.push({\n          key,\n          value: map[key],\n          oldValue: currentMap[key],\n          exists,\n          hadSet: currentMap[key] === map[key],\n        })\n      }\n      return list\n    },\n\n    async setVariables () {\n      const list = await nodeApi.getVariables()\n      const noSetList = list.filter((item) => {\n        return !item.exists\n      })\n      if (noSetList.length > 0) {\n        return nodeApi.setNpmEnv(noSetList)\n      }\n    },\n\n    async setRegistry ({ registry, type }) {\n      if (type === 'npm') {\n        await nodeApi.setNpmEnv([{ key: 'registry', value: registry }])\n      } else {\n        await nodeApi.setYarnEnv([{ key: 'registry', value: registry }])\n      }\n      return true\n    },\n\n    async setProxy (ip, port) {\n      const command = config.get().plugin.node.setting.command || 'npm'\n\n      const cmds = [\n        `${command} config set proxy=http://${ip}:${port - 1}`,\n        `${command} config set https-proxy=http://${ip}:${port}`,\n      ]\n\n      const env = []\n\n      /**\n       *  'strict-ssl': false,\n       cafile: true,\n       NODE_EXTRA_CA_CERTS: true,\n       NODE_TLS_REJECT_UNAUTHORIZED: false\n       */\n      const nodeConfig = config.get().plugin.node\n      const rootCaCertFile = config.get().server.setting.rootCaFile.certPath\n      if (nodeConfig.setting['strict-ssl']) {\n        cmds.push(`${command} config set strict-ssl false`)\n      }\n      if (nodeConfig.setting.cafile) {\n        cmds.push(`${command} config set cafile \"${rootCaCertFile}\"`)\n      }\n\n      if (nodeConfig.setting.NODE_EXTRA_CA_CERTS) {\n        cmds.push(`${command} config set NODE_EXTRA_CA_CERTS \"${rootCaCertFile}\"`)\n        env.push({ key: 'NODE_EXTRA_CA_CERTS', value: rootCaCertFile })\n      }\n\n      if (nodeConfig.setting.NODE_TLS_REJECT_UNAUTHORIZED) {\n        cmds.push(`${command} config set NODE_TLS_REJECT_UNAUTHORIZED 0`)\n        env.push({ key: 'NODE_TLS_REJECT_UNAUTHORIZED', value: '0' })\n      }\n\n      const ret = await shell.exec(cmds, { type: 'cmd' })\n      if (env.length > 0) {\n        await shell.setSystemEnv({ list: env })\n      }\n      event.fire('status', { key: 'plugin.node.enabled', value: true })\n      log.info('开启【NPM】代理成功')\n\n      return ret\n    },\n\n    async unsetProxy () {\n      const command = config.get().plugin.node.setting.command || 'npm'\n\n      const cmds = [\n        `${command} config  delete proxy`,\n        `${command} config  delete https-proxy`,\n        `${command} config  delete NODE_EXTRA_CA_CERTS`,\n        `${command} config  delete strict-ssl`,\n      ]\n      const ret = await shell.exec(cmds, { type: 'cmd' })\n      event.fire('status', { key: 'plugin.node.enabled', value: false })\n      log.info('关闭【NPM】代理成功')\n      return ret\n    },\n  }\n  return nodeApi\n}\n\nmodule.exports = {\n  key: 'node',\n  config: nodeConfig,\n  status: {\n    enabled: false,\n  },\n  plugin: NodePlugin,\n}\n"
  },
  {
    "path": "packages/core/src/modules/plugin/overwall/config.js",
    "content": "module.exports = {\n  name: '梯子',\n  enabled: false, // 默认关闭梯子\n  server: {},\n  serverDefault: {\n    'ow-prod.docmirror.top': {\n      port: 443,\n      path: 'X2dvX292ZXJfd2FsbF8',\n      password: 'dev_sidecar_is_666',\n    },\n  },\n  targets: {\n    '*.github.com': true,\n    '*github*.com': true,\n    '*.wikimedia.org': true,\n    '*.v2ex.com': true,\n    '*.azureedge.net': true,\n    '*.cloudfront.net': true,\n    '*.bing.com': true,\n    '*.discourse-cdn.com': true,\n    '*.gravatar.com': true,\n    '*.docker.com': true,\n    '*.vueuse.org': true,\n    '*.elastic.co': true,\n    '*.optimizely.com': true,\n    '*.stackpathcdn.com': true,\n    '*.fastly.net': true,\n    '*.cloudflare.com': true,\n    '*.233v2.com': true,\n    '*.v2fly.org': true,\n    '*.telegram.org': true,\n    '*.amazon.com': true,\n    '*.googleapis.com': true,\n    '*.google-analytics.com': true,\n    '*.cloudflareinsights.com': true,\n    '*.intlify.dev': true,\n    '*.segment.io': true,\n    '*.shields.io': true,\n    '*.jsdelivr.net': true,\n  },\n  pac: {\n    enabled: true,\n    autoUpdate: true,\n    pacFileUpdateUrl: 'https://raw.githubusercontent.com/gfwlist/gfwlist/master/gfwlist.txt',\n    pacFileAbsolutePath: null, // 自定义 pac.txt 文件位置，可以是本地文件路径\n    pacFilePath: './extra/pac/pac.txt', // 内置 pac.txt 文件路径\n  },\n}\n"
  },
  {
    "path": "packages/core/src/modules/plugin/overwall/index.js",
    "content": "const pluginConfig = require('./config')\n\nconst Plugin = function (context) {\n  const { config, shell, event, log } = context\n  const api = {\n    async start () {\n      // event.fire('status', { key: 'plugin.overwall.enabled', value: true })\n    },\n\n    async close () {\n      // event.fire('status', { key: 'plugin.overwall.enabled', value: false })\n    },\n\n    async restart () {\n      await api.close()\n      await api.start()\n    },\n\n    async  overrideRunningConfig_bak (serverConfig) {\n      const conf = config.get().plugin.overwall\n      if (!conf || !conf.enabled || !conf.targets) {\n        return\n      }\n      const server = conf.server\n      let i = 0\n      let main\n      const backup = []\n      for (const key in server) {\n        if (i === 0) {\n          main = key\n        } else {\n          backup.push(key)\n        }\n        i++\n      }\n      for (const key in conf.targets) {\n        serverConfig.intercepts[key] = {\n          '.*': {\n            proxy: `${main}/\\${host}`,\n            backup,\n          },\n        }\n      }\n    },\n  }\n  return api\n}\n\nmodule.exports = {\n  key: 'overwall',\n  config: pluginConfig,\n  plugin: Plugin,\n}\n"
  },
  {
    "path": "packages/core/src/modules/plugin/pip/config.js",
    "content": "module.exports = {\n  name: 'PIP加速',\n  statusOff: true,\n  enabled: null, // 没有开关\n  tip: '如果你没有安装pip则不需要启动它',\n  startup: {\n  },\n  setting: {\n    command: 'pip',\n    trustedHost: 'pypi.org',\n    registry: 'https://pypi.org/simple/', // 可以选择切换官方或者淘宝镜像\n  },\n}\n"
  },
  {
    "path": "packages/core/src/modules/plugin/pip/index.js",
    "content": "const pipConfig = require('./config')\n\nconst PipPlugin = function (context) {\n  const { config, shell, event, log } = context\n  const api = {\n    async start () {\n      await api.setRegistry({ registry: config.get().plugin.pip.setting.registry })\n      await api.setTrustedHost(config.get().plugin.pip.setting.trustedHost)\n    },\n\n    async close () {\n    },\n\n    async restart () {\n      await api.close()\n      await api.start()\n    },\n\n    async save (newConfig) {\n      await api.setVariables()\n    },\n    async getPipEnv () {\n      const command = config.get().plugin.pip.setting.command\n      let ret = await shell.exec([`${command} config list`], { type: 'cmd' })\n      if (ret != null) {\n        ret = ret.trim()\n        const lines = ret.split('\\n')\n        const vars = {}\n        for (const line of lines) {\n          if (!line.startsWith('global')) {\n            continue\n          }\n          const key = line.substring(0, line.indexOf('='))\n          let value = line.substring(line.indexOf('=') + 1)\n          if (value.startsWith('\\'')) {\n            value = value.startsWith(1, value.length - 1)\n          }\n          vars[key] = value\n        }\n        return vars\n      }\n      return {}\n    },\n\n    async setPipEnv (list) {\n      const command = config.get().plugin.pip.setting.command\n      const cmds = []\n      for (const item of list) {\n        if (item.value != null) {\n          cmds.push(`${command} config set global.${item.key}  ${item.value}`)\n        } else {\n          cmds.push(`${command} config unset  global.${item.key}`)\n        }\n      }\n      return await shell.exec(cmds, { type: 'cmd' })\n    },\n\n    async unsetPipEnv (list) {\n      const command = config.get().plugin.pip.setting.command\n      const cmds = []\n      for (const item of list) {\n        cmds.push(`${command} config unset  global.${item} `)\n      }\n      return await shell.exec(cmds, { type: 'cmd' })\n    },\n\n    async setRegistry ({ registry }) {\n      await api.setPipEnv([{ key: 'index-url', value: registry }])\n      return true\n    },\n\n    async setTrustedHost (host) {\n      await api.setPipEnv([{ key: 'trusted-host', value: host }])\n      return true\n    },\n\n    async setProxy (ip, port) {\n\n    },\n\n    async unsetProxy () {\n\n    },\n  }\n  return api\n}\n\nmodule.exports = {\n  key: 'pip',\n  config: pipConfig,\n  status: {\n    enabled: false,\n  },\n  plugin: PipPlugin,\n}\n"
  },
  {
    "path": "packages/core/src/modules/proxy/index.js",
    "content": "const ProxyPlugin = function (context) {\n  const { config, event, shell, log } = context\n  const api = {\n    async start () {\n      return api.setProxy()\n    },\n\n    async close () {\n      return api.unsetProxy()\n    },\n\n    async restart () {\n      await api.close()\n      await api.start()\n    },\n\n    async setProxy () {\n      const ip = '127.0.0.1'\n      const port = config.get().server.port\n      const setEnv = config.get().proxy.setEnv\n      await shell.setSystemProxy({ ip, port, setEnv })\n      log.info(`开启系统代理成功：${ip}:${port}`)\n      event.fire('status', { key: 'proxy.enabled', value: true })\n      return { ip, port }\n    },\n\n    async unsetProxy (setEnv) {\n      if (setEnv) {\n        setEnv = config.get().proxy.setEnv\n      }\n      try {\n        await shell.setSystemProxy({ setEnv })\n        event.fire('status', { key: 'proxy.enabled', value: false })\n        log.info('关闭系统代理成功')\n        return true\n      } catch (err) {\n        log.error('关闭系统代理失败:', err)\n        return false\n      }\n    },\n\n    async setEnableLoopback () {\n      await shell.enableLoopback()\n      log.info('打开EnableLoopback成功')\n      return true\n    },\n  }\n  return api\n}\nmodule.exports = {\n  key: 'proxy',\n  config: {\n    enabled: true,\n    name: '系统代理',\n    use: 'local',\n    other: [],\n    proxyHttp: false, // false=只代理HTTPS请求   true=同时代理HTTP和HTTPS请求\n    setEnv: false,\n\n    // 排除国内域名 所需配置\n    excludeDomesticDomainAllowList: true, // 是否排除国内域名，默认：需要排除\n    autoUpdateDomesticDomainAllowList: true, // 是否自动更新国内域名\n    remoteDomesticDomainAllowListFileUrl: 'https://raw.githubusercontent.com/pluwen/china-domain-allowlist/refs/heads/main/allow-list.sorl',\n    domesticDomainAllowListFileAbsolutePath: null, // 自定义 domestic-domain-allowlist.txt 文件位置，可以是本地文件路径\n    domesticDomainAllowListFilePath: './extra/proxy/domestic-domain-allowlist.txt', // 内置国内域名文件\n\n    // 自定义系统代理排除列表\n    excludeIpList: {\n      // region 常用国内可访问域名\n\n      // 中国大陆\n      '*.cn': true,\n      'cn.*': true,\n      '*china*': true,\n\n      // Github加速源：以下加速源代理后反而出现问题，从系统代理中排除掉\n      '*.kkgithub.com': true,\n      '*.ghproxy.*': true,\n\n      // Github ssh\n      'ssh.github.com': true,\n\n      // DeepL\n      'www.deepl.com': true,\n\n      // CSDN\n      '*.csdn.net': true,\n\n      // 360 so\n      '*.so.com': true,\n\n      // 百度\n      '*.baidu.com': true,\n      '*.baiducontent.com': true,\n      '*.bdimg.com': true,\n      '*.bdstatic.com': true,\n      '*.bdydns.com': true,\n\n      // 腾讯\n      '*.tencent.com': true,\n      '*.qq.com': true,\n      '*.weixin.com': true,\n      '*.weixinbridge.com': true,\n      '*.wechat.com': true,\n      '*.idqqimg.com': true,\n      '*.gtimg.com': true,\n      '*.qpic.com': true,\n      '*.qlogo.com': true,\n      '*.myapp.com': true,\n\n      // 阿里\n      '*.aliyun.com': true,\n      '*.alipay.com': true,\n      '*.taobao.com': true,\n      '*.tmall.com': true,\n      '*.alipayobjects.com': true,\n      '*.dingtalk.com': true,\n      '*.mmstat.com': true,\n      '*.alicdn.com': true,\n      '*.hdslb.com': true,\n\n      // Gitee\n      'gitee.com': true,\n      '*.gitee.com': true,\n      '*.gitee.io': true,\n      '*.giteeusercontent.com': true,\n\n      // Mozilla Firefox\n      '*.mozilla.org': true,\n      '*.mozilla.com': true,\n      '*.mozilla.net': true,\n      '*.firefox.com': true,\n      '*.firefox.org': true,\n      '*.mozillademos.org': true,\n      '*.mozillians.org': true,\n      '*.mozillians.net': true,\n      '*.mozillians.com': true,\n\n      // OSS\n      '*.sonatype.org': true,\n      // Maven镜像\n      '*.maven.org': true,\n      // Maven Repository\n      '*.mvnrepository.com': true,\n\n      // 苹果\n      '*.apple.com': true,\n      '*.icloud.com': true,\n\n      // 微软\n      '*.microsoft.com': true,\n      '*.windows.com': true,\n      '*.office.com': true,\n      '*.office.net': true,\n      '*.live.com': true,\n      '*.msn.com': true,\n\n      // WPS\n      '*.wps.com': true,\n      '*.wps.net': true,\n      '*.ksord.com': true,\n\n      // 奇虎\n      '*.qihoo.com': true,\n      '*.qihucdn.com': true,\n      // 360\n      '*.360.com': true,\n      '*.360safe.com': true,\n      '*.360buyimg.com': true,\n      '*.360buy.com': true,\n\n      // 京东\n      '*.jd.com': true,\n      '*.jcloud.com': true,\n      '*.jcloudcs.com': true,\n      '*.jcloudcache.com': true,\n      '*.jcloudcdn.com': true,\n      '*.jcloudlb.com': true,\n\n      // 哔哩哔哩\n      '*.bilibili.com': true,\n      '*.bilivideo.com': true,\n      '*.biliapi.net': true,\n\n      // 移动\n      '*.10086.com': true,\n      '*.10086cloud.com': true,\n\n      // 移动：139邮箱\n      '*.139.com': true,\n\n      // 迅雷\n      '*.xunlei.com': true,\n\n      // 网站ICP备案查询\n      '*.icpapi.com': true,\n\n      // Navicat\n      '*.navicat.com': true,\n\n      // Github文件上传所使用的域名，被DS代理会导致文件上传经常失败，从系统代理中排除掉\n      'objects-origin.githubusercontent.com': true,\n\n      // cloudflare：排除以下域名，cloudflare的人机校验会更快，成功率更高。\n      'challenges.cloudflare.com': true,\n\n      // endregion\n\n      // 本地地址，无需代理\n      'localhost': true,\n      'localhost.*': true, // 部分VPN会在host中添加这种格式的域名指向127.0.0.1，所以也排除掉\n      '127.*.*.*': true,\n      'test.*': true, // 本地开发时，测试用的虚拟域名格式，无需代理\n\n      // 服务器端常用地址，无需代理\n      '10.*.*.*': true,\n      '172.16.*.*': true,\n      '172.17.*.*': true,\n      '172.18.*.*': true,\n      '172.19.*.*': true,\n      '172.20.*.*': true,\n      '172.21.*.*': true,\n      '172.22.*.*': true,\n      '172.23.*.*': true,\n      '172.24.*.*': true,\n      '172.25.*.*': true,\n      '172.26.*.*': true,\n      '172.27.*.*': true,\n      '172.28.*.*': true,\n      '172.29.*.*': true,\n      '172.30.*.*': true,\n      '172.31.*.*': true,\n\n      // 局域网地址，无需代理\n      '192.168.*.*': true,\n    },\n  },\n  status: {\n    enabled: false,\n    proxyTarget: '',\n  },\n  plugin: ProxyPlugin,\n}\n"
  },
  {
    "path": "packages/core/src/modules/server/index.js",
    "content": "const fork = require('node:child_process').fork\nconst fs = require('node:fs')\nconst path = require('node:path')\nconst lodash = require('lodash')\nconst config = require('../../config-api')\nconst event = require('../../event')\nconst status = require('../../status')\nconst jsonApi = require('@docmirror/mitmproxy/src/json')\nconst log = require('../../utils/util.log.core')\n\nlet server = null\nfunction fireStatus (status) {\n  event.fire('status', { key: 'server.enabled', value: status })\n}\nfunction sleep (time) {\n  return new Promise((resolve) => {\n    setTimeout(() => {\n      resolve()\n    }, time)\n  })\n}\nconst serverApi = {\n  async startup () {\n    if (config.get().server.startup) {\n      return this.start(config.get().server)\n    }\n  },\n  async shutdown () {\n    if (status.server) {\n      return this.close()\n    }\n  },\n  async start ({ mitmproxyPath, plugins }) {\n    const allConfig = config.get()\n    const serverConfig = lodash.cloneDeep(allConfig.server)\n\n    const intercepts = serverConfig.intercepts\n    const dnsMapping = serverConfig.dns.mapping\n\n    if (allConfig.plugin) {\n      lodash.each(allConfig.plugin, (value) => {\n        const plugin = value\n        if (!plugin.enabled) {\n          return\n        }\n        if (plugin.intercepts) {\n          lodash.merge(intercepts, plugin.intercepts)\n        }\n        if (plugin.dns) {\n          lodash.merge(dnsMapping, plugin.dns)\n        }\n      })\n    }\n\n    if (allConfig.app) {\n      serverConfig.app = allConfig.app\n    }\n\n    if (serverConfig.intercept.enabled === false) {\n      // 如果设置为关闭拦截\n      serverConfig.intercepts = {}\n    }\n\n    for (const key in plugins) {\n      const plugin = plugins[key]\n      if (plugin.overrideRunningConfig) {\n        plugin.overrideRunningConfig(serverConfig)\n      }\n    }\n    serverConfig.plugin = allConfig.plugin\n\n    if (allConfig.proxy && allConfig.proxy.enabled) {\n      serverConfig.proxy = allConfig.proxy\n    }\n\n    // fireStatus('ing') // 启动中\n    const basePath = serverConfig.setting.userBasePath\n    const runningConfigPath = path.join(basePath, '/running.json')\n    try {\n      fs.writeFileSync(runningConfigPath, jsonApi.stringify(serverConfig))\n      log.info('保存 running.json 运行时配置文件成功:', runningConfigPath)\n    } catch (e) {\n      log.error('保存 running.json 运行时配置文件失败:', runningConfigPath, ', error:', e)\n      throw e\n    }\n    const serverProcess = fork(mitmproxyPath, [runningConfigPath])\n    server = {\n      id: serverProcess.pid,\n      process: serverProcess,\n      close () {\n        serverProcess.send({ type: 'action', event: { key: 'close' } })\n      },\n    }\n    serverProcess.on('beforeExit', (code) => {\n      log.warn('server process beforeExit, code:', code)\n    })\n    serverProcess.on('SIGPIPE', (code, signal) => {\n      log.warn(`server process SIGPIPE, code: ${code}, signal:`, signal)\n    })\n    serverProcess.on('exit', (code, signal) => {\n      log.warn(`server process exit, code: ${code}, signal:`, signal)\n    })\n    serverProcess.on('uncaughtException', (err, origin) => {\n      log.error('server process uncaughtException:', err)\n    })\n    serverProcess.on('message', (msg) => {\n      log.debug('收到子进程消息:', JSON.stringify(msg))\n      if (msg.type === 'status') {\n        fireStatus(msg.event)\n      } else if (msg.type === 'error') {\n        let code = ''\n        if (msg.event.code) {\n          code = msg.event.code\n        }\n        fireStatus(false) // 启动失败\n        event.fire('error', { key: 'server', value: code, error: msg.event, message: msg.message })\n      } else if (msg.type === 'speed') {\n        event.fire('speed', msg.event)\n      }\n    })\n    return { port: serverConfig.port }\n  },\n  async kill () {\n    if (server) {\n      server.process.kill('SIGINT')\n      await sleep(1000)\n    }\n    fireStatus(false)\n  },\n  async close () {\n    return await serverApi.kill()\n  },\n  async close1 () {\n    return new Promise((resolve, reject) => {\n      if (server) {\n        // fireStatus('ing')// 关闭中\n        server.close((err) => {\n          if (err) {\n            log.warn('close error:', err)\n            if (err.code === 'ERR_SERVER_NOT_RUNNING') {\n              log.info('代理服务关闭成功')\n              resolve()\n              return\n            }\n            log.warn('代理服务关闭失败:', err)\n            reject(err)\n          } else {\n            log.info('代理服务关闭成功')\n            resolve()\n          }\n        })\n      } else {\n        log.info('server is null')\n        resolve()\n      }\n    })\n  },\n  async restart ({ mitmproxyPath }) {\n    await serverApi.kill()\n    await serverApi.start({ mitmproxyPath })\n  },\n  getServer () {\n    return server\n  },\n  getSpeedTestList () {\n    if (server) {\n      server.process.send({ type: 'speed', event: { key: 'getList' } })\n    }\n  },\n  reSpeedTest () {\n    if (server) {\n      server.process.send({ type: 'speed', event: { key: 'reTest' } })\n    }\n  },\n}\nmodule.exports = serverApi\n"
  },
  {
    "path": "packages/core/src/shell/index.js",
    "content": "const enableLoopback = require('./scripts/enable-loopback')\nconst extraPath = require('./scripts/extra-path')\nconst getNpmEnv = require('./scripts/get-npm-env')\nconst getSystemEnv = require('./scripts/get-system-env')\nconst killByPort = require('./scripts/kill-by-port')\nconst setNpmEnv = require('./scripts/set-npm-env')\nconst setSystemEnv = require('./scripts/set-system-env')\nconst setSystemProxy = require('./scripts/set-system-proxy')\nconst setupCa = require('./scripts/setup-ca')\nconst shell = require('./shell')\n\nmodule.exports = {\n  killByPort,\n  setupCa,\n  getSystemEnv,\n  setSystemEnv,\n  getNpmEnv,\n  setNpmEnv,\n  setSystemProxy,\n  enableLoopback,\n  extraPath,\n  async exec (cmds, args) {\n    return shell.getSystemShell().exec(cmds, args)\n  },\n  getSystemPlatform: shell.getSystemPlatform,\n}\n"
  },
  {
    "path": "packages/core/src/shell/scripts/enable-loopback.js",
    "content": "/**\n */\nconst Shell = require('../shell')\nconst extraPath = require('./extra-path')\nconst sudoPrompt = require('@vscode/sudo-prompt')\nconst log = require('../../utils/util.log.core')\nconst execute = Shell.execute\n\nconst executor = {\n  windows (exec) {\n    const loopbackPath = extraPath.getEnableLoopbackPath()\n    const sudoCommand = [`\"${loopbackPath}\"`]\n\n    const options = {\n      name: 'EnableLoopback',\n    }\n    return new Promise((resolve, reject) => {\n      sudoPrompt.exec(\n        sudoCommand.join(' '),\n        options,\n        (error, _, stderr) => {\n          if (stderr) {\n            log.error(`[sudo-prompt] 发生错误: ${stderr}`)\n          }\n\n          if (error) {\n            reject(error)\n          } else {\n            resolve(undefined)\n          }\n        },\n      )\n    })\n  },\n  async linux (exec, { port }) {\n    throw new Error('不支持此操作')\n  },\n  async mac (exec, { port }) {\n    throw new Error('不支持此操作')\n  },\n}\n\nmodule.exports = async function (args) {\n  return execute(executor, args)\n}\n"
  },
  {
    "path": "packages/core/src/shell/scripts/extra-path/index.js",
    "content": "const path = require('node:path')\nconst log = require('../../../utils/util.log.core')\n\nfunction getExtraPath () {\n  let extraPath = process.env.DS_EXTRA_PATH\n  log.info('extraPath:', extraPath)\n  if (!extraPath) {\n    extraPath = __dirname\n  }\n  return extraPath\n}\n\nfunction getProxyExePath () {\n  const extraPath = getExtraPath()\n  return path.join(extraPath, 'sysproxy.exe')\n}\n\nfunction getEnableLoopbackPath () {\n  const extraPath = getExtraPath()\n  return path.join(extraPath, 'EnableLoopback.exe')\n}\n\nmodule.exports = {\n  getProxyExePath,\n  getEnableLoopbackPath,\n}\n"
  },
  {
    "path": "packages/core/src/shell/scripts/get-npm-env.js",
    "content": "/**\n * 获取环境变量\n */\nconst jsonApi = require('@docmirror/mitmproxy/src/json')\nconst Shell = require('../shell')\n\nconst execute = Shell.execute\n\nconst executor = {\n  async windows (exec) {\n    const ret = await exec(['npm config list --json'], { type: 'cmd' })\n    if (ret != null) {\n      const json = ret.substring(ret.indexOf('{'))\n      return jsonApi.parse(json)\n    }\n    return {}\n  },\n  async linux (exec, { port }) {\n    throw new Error('暂未实现此功能')\n  },\n  async mac (exec, { port }) {\n    throw new Error('暂未实现此功能')\n  },\n}\n\nmodule.exports = async function (args) {\n  return execute(executor, args)\n}\n"
  },
  {
    "path": "packages/core/src/shell/scripts/get-system-env.js",
    "content": "/**\n * 获取环境变量\n */\nconst Shell = require('../shell')\n\nconst execute = Shell.execute\n\nconst executor = {\n  async windows (exec) {\n    const ret = await exec(['set'], { type: 'cmd' })\n    const map = {}\n    if (ret != null) {\n      const lines = ret.split('\\r\\n')\n      for (const item of lines) {\n        const kv = item.split('=')\n        if (kv.length > 1) {\n          map[kv[0].trim()] = kv[1].trim()\n        }\n      }\n    }\n    return map\n  },\n  async linux (exec, { port }) {\n    throw new Error('暂未实现此功能')\n  },\n  async mac (exec, { port }) {\n    throw new Error('暂未实现此功能')\n  },\n}\n\nmodule.exports = async function (args) {\n  return execute(executor, args)\n}\n"
  },
  {
    "path": "packages/core/src/shell/scripts/kill-by-port.js",
    "content": "const Shell = require('../shell')\n\nconst execute = Shell.execute\n\nconst executor = {\n  async windows (exec, { port }) {\n    const cmds = [`for /f \"tokens=5\" %a in ('netstat -aon ^| find \":${port}\" ^| find \"LISTENING\"') do (taskkill /f /pid %a & exit /B) `]\n    await exec(cmds, { type: 'cmd' })\n    return true\n  },\n  async linux (exec, { port }) {\n    await exec(`kill \\`lsof -i:${port} |grep 'dev-sidecar\\\\|electron\\\\|@docmirro' |awk '{print $2}'\\``)\n    return true\n  },\n  async mac (exec, { port }) {\n    await exec(`kill \\`lsof -i:${port} |grep 'dev-side\\\\|Elect' |awk '{print $2}'\\``)\n    return true\n  },\n}\n\nmodule.exports = async function (args) {\n  return execute(executor, args)\n}\n"
  },
  {
    "path": "packages/core/src/shell/scripts/set-npm-env.js",
    "content": "/**\n * 设置环境变量\n */\nconst Shell = require('../shell')\n\nconst execute = Shell.execute\n\nconst executor = {\n  async windows (exec, { list }) {\n    const cmds = []\n    for (const item of list) {\n      cmds.push(`npm config set ${item.key}  ${item.value}`)\n    }\n    return await exec(cmds, { type: 'cmd' })\n  },\n  async linux (exec, { port }) {\n    throw new Error('暂未实现此功能')\n  },\n  async mac (exec, { port }) {\n    throw new Error('暂未实现此功能')\n  },\n}\n\nmodule.exports = async function (args) {\n  return execute(executor, args)\n}\n"
  },
  {
    "path": "packages/core/src/shell/scripts/set-system-env.js",
    "content": "/**\n * 设置环境变量\n */\nconst Shell = require('../shell')\n\nconst execute = Shell.execute\n\nconst executor = {\n  async windows (exec, { list }) {\n    const cmds = []\n    for (const item of list) {\n      // [Environment]::SetEnvironmentVariable('FOO', 'bar', 'Machine')\n      cmds.push(`[Environment]::SetEnvironmentVariable('${item.key}', '${item.value}', 'Machine')`)\n    }\n    const ret = await exec(cmds, { type: 'ps' })\n\n    const cmds2 = []\n    for (const item of list) {\n      // [Environment]::SetEnvironmentVariable('FOO', 'bar', 'Machine')\n      cmds2.push(`set ${item.key}=\"\"`)\n    }\n    await exec(cmds2, { type: 'cmd' })\n    return ret\n  },\n  async linux (exec, { port }) {\n    throw new Error('暂未实现此功能')\n  },\n  async mac (exec, { port }) {\n    throw new Error('暂未实现此功能')\n  },\n}\n\nmodule.exports = async function (args) {\n  return execute(executor, args)\n}\n"
  },
  {
    "path": "packages/core/src/shell/scripts/set-system-proxy/index.js",
    "content": "/**\n * 获取环境变量\n */\nconst fs = require('node:fs')\nconst path = require('node:path')\nconst request = require('request')\nconst Registry = require('winreg')\nconst log = require('../../../utils/util.log.core')\nconst Shell = require('../../shell')\nconst extraPath = require('../extra-path')\nconst dateUtil = require('../../../utils/util.date')\n\nconst execute = Shell.execute\nconst execFile = Shell.execFile\n\nlet config = null\nfunction loadConfig () {\n  if (config == null) {\n    config = require('../../../config-api.js')\n  }\n}\n\nfunction getDomesticDomainAllowListTmpFilePath () {\n  return path.join(config.get().server.setting.userBasePath, '/domestic-domain-allowlist.txt')\n}\n\nasync function downloadDomesticDomainAllowListAsync () {\n  loadConfig()\n\n  const remoteFileUrl = config.get().proxy.remoteDomesticDomainAllowListFileUrl\n  log.info('开始下载远程 domestic-domain-allowlist.txt 文件:', remoteFileUrl)\n  request(remoteFileUrl, (error, response, body) => {\n    if (error) {\n      log.error(`下载远程 domestic-domain-allowlist.txt 文件失败: ${remoteFileUrl}, error:`, error, ', response:', response, ', body:', body)\n      return\n    }\n    if (response && response.statusCode === 200) {\n      if (body == null || body.length < 100) {\n        log.warn('下载远程 domestic-domain-allowlist.txt 文件成功，但内容为空或内容太短，判断为无效的 domestic-domain-allowlist.txt 文件:', remoteFileUrl, ', body:', body)\n        return\n      } else {\n        log.info('下载远程 domestic-domain-allowlist.txt 文件成功:', remoteFileUrl)\n      }\n\n      let fileTxt = body\n      try {\n        if (!fileTxt.includes('*.')) {\n          fileTxt = Buffer.from(fileTxt, 'base64').toString('utf8')\n          // log.debug('解析 base64 后的 domestic-domain-allowlist:', fileTxt)\n        }\n      } catch {\n        if (!fileTxt.includes('*.')) {\n          log.error(`远程 domestic-domain-allowlist.txt 文件内容即不是base64格式，也不是要求的格式，url: ${remoteFileUrl}，body: ${body}`)\n          return\n        }\n      }\n\n      // 保存到本地\n      saveDomesticDomainAllowListFile(fileTxt)\n    } else {\n      log.error(`下载远程 domestic-domain-allowlist.txt 文件失败: ${remoteFileUrl}, response:`, response, ', body:', body)\n    }\n  })\n}\n\nfunction loadLastModifiedTimeFromTxt (fileTxt) {\n  const matched = fileTxt.match(/(?<=; Update Date: )[^\\r\\n]+/g)\n  if (matched && matched.length > 0) {\n    try {\n      return new Date(matched[0])\n    } catch {\n      return null\n    }\n  }\n}\n\n// 保存 国内域名白名单 内容到 `~/domestic-domain-allowlist.txt` 文件中\nfunction saveDomesticDomainAllowListFile (fileTxt) {\n  const filePath = getDomesticDomainAllowListTmpFilePath()\n  try {\n    fs.writeFileSync(filePath, fileTxt.replaceAll(/\\r\\n?/g, '\\n'))\n    log.info('保存 domestic-domain-allowlist.txt 文件成功:', filePath)\n  } catch (e) {\n    log.error('保存 domestic-domain-allowlist.txt 文件失败:', filePath, ', error:', e)\n    return\n  }\n\n  // 尝试解析和修改 domestic-domain-allowlist.txt 文件时间\n  const lastModifiedTime = loadLastModifiedTimeFromTxt(fileTxt)\n  if (lastModifiedTime) {\n    fs.stat(filePath, (err, _stats) => {\n      if (err) {\n        log.error('修改 domestic-domain-allowlist.txt 文件时间失败:', err)\n        return\n      }\n\n      // 修改文件的访问时间和修改时间为当前时间\n      fs.utimes(filePath, lastModifiedTime, lastModifiedTime, (utimesErr) => {\n        if (utimesErr) {\n          log.error('修改 domestic-domain-allowlist.txt 文件时间失败:', utimesErr)\n        } else {\n          log.info(`'${filePath}' 文件的修改时间已更新为其最近更新时间 '${dateUtil.format(lastModifiedTime, false)}'`)\n        }\n      })\n    })\n  }\n}\n\nfunction getDomesticDomainAllowList () {\n  loadConfig()\n\n  if (!config.get().proxy.excludeDomesticDomainAllowList) {\n    return null\n  }\n\n  // 判断是否需要自动更新国内域名\n  let fileAbsolutePath = config.get().proxy.domesticDomainAllowListFileAbsolutePath\n  if (!fileAbsolutePath && config.get().proxy.autoUpdateDomesticDomainAllowList) {\n    // 异步下载，下载成功后，下次系统代理生效\n    downloadDomesticDomainAllowListAsync().then()\n  }\n\n  // 加载本地文件\n  if (!fileAbsolutePath) {\n    const tmpFilePath = getDomesticDomainAllowListTmpFilePath()\n    if (fs.existsSync(tmpFilePath)) {\n      // 如果临时文件已存在，则使用临时文件\n      fileAbsolutePath = tmpFilePath\n      log.info('读取已下载的 domestic-domain-allowlist.txt 文件:', fileAbsolutePath)\n    } else {\n      // 如果临时文件不存在，则使用内置文件\n      log.info('__dirname:', __dirname)\n      fileAbsolutePath = path.join(__dirname, '../', config.get().proxy.domesticDomainAllowListFilePath)\n      log.info('读取内置的 domestic-domain-allowlist.txt 文件:', fileAbsolutePath)\n    }\n  } else {\n    log.info('读取自定义路径的 domestic-domain-allowlist.txt 文件:', fileAbsolutePath)\n  }\n\n  try {\n    return fs.readFileSync(fileAbsolutePath).toString()\n  } catch (e) {\n    log.error(`读取 domestic-domain-allowlist.txt 文件失败: ${fileAbsolutePath}, error:`, e)\n    return null\n  }\n}\n\nfunction getProxyExcludeIpStr (split) {\n  const proxyExcludeIpConfig = config.get().proxy.excludeIpList\n\n  let excludeIpStr = ''\n  for (const ip in proxyExcludeIpConfig) {\n    if (proxyExcludeIpConfig[ip] === true) {\n      excludeIpStr += ip + split\n    }\n  }\n\n  // 排除国内域名\n  // log.debug('系统代理排除域名（excludeIpStr）:', excludeIpStr)\n  if (config.get().proxy.excludeDomesticDomainAllowList) {\n    try {\n      const domesticDomainAllowList = getDomesticDomainAllowList()\n      if (domesticDomainAllowList) {\n        const domesticDomainList = (`\\n${domesticDomainAllowList}`).replaceAll(/[\\r\\n]+/g, '\\n').match(/(?<=\\n)(?:[\\w\\-.*]+|\\[[\\w:]+\\])(?=\\n)/g)\n        if (domesticDomainList && domesticDomainList.length > 0) {\n          for (const domesticDomain of domesticDomainList) {\n            if (proxyExcludeIpConfig[domesticDomain] !== false) {\n              excludeIpStr += domesticDomain + split\n            } else {\n              log.info('系统代理排除列表拼接国内域名时，跳过域名，系统代理将继续代理它:', domesticDomain)\n            }\n          }\n          log.info('系统代理排除列表拼接国内域名成功')\n        } else {\n          log.info('国内域名为空，不进行系统代理排除列表拼接国内域名')\n        }\n      }\n    } catch (e) {\n      log.error('系统代理排除列表拼接国内域名失败:', e)\n    }\n  }\n\n  return excludeIpStr\n}\n\nconst executor = {\n  async windows (exec, params = {}) {\n    const { ip, port, setEnv } = params\n    if (ip != null) { // 设置代理\n      // 延迟加载config\n      loadConfig()\n\n      log.info('开始设置windows系统代理:', ip, port, setEnv)\n\n      // https\n      let proxyAddr = `https=http://${ip}:${port}`\n      // http\n      if (config.get().proxy.proxyHttp) {\n        proxyAddr = `http=http://${ip}:${port - 1};${proxyAddr}`\n      }\n\n      // 读取排除域名\n      const excludeIpStr = getProxyExcludeIpStr(';')\n      // 设置代理，同时设置排除域名\n      try {\n        require('@starknt/sysproxy').triggerManualProxyByUrl(true, proxyAddr, excludeIpStr, true)\n        log.info(`设置windows系统代理成功: ${proxyAddr} ......(省略排除IP列表)`)\n      } catch (e1) {\n        log.warn('设置windows系统代理失败：执行 `@starknt/sysproxy` 失败，现尝试通过执行 `sysproxy.exe global ...` 来设置系统代理！\\r\\n捕获的异常:', e1)\n\n        const proxyPath = extraPath.getProxyExePath()\n        const execFun = 'global'\n        try {\n          await execFile(proxyPath, [execFun, proxyAddr, excludeIpStr])\n          log.info(`设置windows系统代理成功，执行的命令：${proxyPath} ${execFun} ${proxyAddr} ......(省略排除IP列表)`)\n        } catch (e2) {\n          log.error(`设置windows系统代理失败，执行的命令：${proxyPath} ${execFun} ${proxyAddr} ......(省略排除IP列表), error:`, e2)\n          throw e1 // 将上面的异常抛出\n        }\n      }\n\n      if (setEnv) {\n        // 设置全局代理所需的环境变量\n        try {\n          await exec(`echo '设置环境变量 HTTPS_PROXY${config.get().proxy.proxyHttp ? '、HTTP_PROXY' : ''}'`)\n\n          log.info(`开启系统代理的同时设置环境变量：HTTPS_PROXY = \"http://${ip}:${port}/\"`)\n          await exec(`setx HTTPS_PROXY \"http://${ip}:${port}/\"`)\n\n          if (config.get().proxy.proxyHttp) {\n            log.info(`开启系统代理的同时设置环境变量：HTTP_PROXY = \"http://${ip}:${port - 1}/\"`)\n            await exec(`setx HTTP_PROXY \"http://${ip}:${port - 1}/\"`)\n          }\n\n          //  await addClearScriptIni()\n        } catch (e) {\n          log.error('设置环境变量 HTTPS_PROXY、HTTP_PROXY 失败:', e)\n        }\n      }\n\n      return true\n    } else { // 关闭代理\n      try {\n        log.info('开始关闭windows系统代理')\n        require('@starknt/sysproxy').triggerManualProxy(false, '', 0, '')\n        log.info('关闭windows系统代理成功')\n      } catch (e1) {\n        log.error('关闭windows系统代理失败：执行 `@starknt/sysproxy` 失败，现尝试通过执行 `sysproxy.exe set 1` 来关闭系统代理！\\r\\n捕获的异常:', e1)\n\n        try {\n          const proxyPath = extraPath.getProxyExePath()\n          await execFile(proxyPath, ['set', '1'])\n          log.info('关闭windows系统代理成功，执行的命令：sysproxy.exe set 1')\n        } catch (e2) {\n          log.error('关闭windows系统代理失败，执行的命令：sysproxy.exe set 1, error:', e2)\n          throw e1 // 将上面的异常抛出\n        }\n      }\n\n      try {\n        await exec('echo \\'删除环境变量 HTTPS_PROXY、HTTP_PROXY\\'')\n        const regKey = new Registry({ // new operator is optional\n          hive: Registry.HKCU, // open registry hive HKEY_CURRENT_USER\n          key: '\\\\Environment', // key containing autostart programs\n        })\n        regKey.get('HTTPS_PROXY', (err) => {\n          if (!err) {\n            regKey.remove('HTTPS_PROXY', async (err) => {\n              log.warn('删除环境变量 HTTPS_PROXY 失败:', err)\n              await exec('setx DS_REFRESH \"1\"')\n            })\n          }\n        })\n        regKey.get('HTTP_PROXY', (err) => {\n          if (!err) {\n            regKey.remove('HTTP_PROXY', async (err) => {\n              log.warn('删除环境变量 HTTP_PROXY 失败:', err)\n            })\n          }\n        })\n      } catch (e) {\n        log.error('删除环境变量 HTTPS_PROXY、HTTP_PROXY 失败:', e)\n      }\n\n      return true\n    }\n  },\n  async linux (exec, params = {}) {\n    const { ip, port } = params\n    if (ip != null) { // 设置代理\n      // 延迟加载config\n      loadConfig()\n\n      // https\n      const setProxyCmd = [\n        'gsettings set org.gnome.system.proxy mode manual',\n        `gsettings set org.gnome.system.proxy.https host ${ip}`,\n        `gsettings set org.gnome.system.proxy.https port ${port}`,\n      ]\n      // http\n      if (config.get().proxy.proxyHttp) {\n        setProxyCmd.push(`gsettings set org.gnome.system.proxy.http host ${ip}`)\n        setProxyCmd.push(`gsettings set org.gnome.system.proxy.http port ${port - 1}`)\n      } else {\n        setProxyCmd.push('gsettings set org.gnome.system.proxy.http host \\'\\'')\n        setProxyCmd.push('gsettings set org.gnome.system.proxy.http port 0')\n      }\n\n      // 设置排除域名（ignore-hosts）\n      const excludeIpStr = getProxyExcludeIpStr('\\', \\'')\n      setProxyCmd.push(`gsettings set org.gnome.system.proxy ignore-hosts \"['${excludeIpStr}']\"`)\n\n      await exec(setProxyCmd)\n    } else { // 关闭代理\n      const setProxyCmd = [\n        'gsettings set org.gnome.system.proxy mode none',\n      ]\n      await exec(setProxyCmd)\n    }\n  },\n  async mac (exec, params = {}) {\n    // exec = _exec\n    let wifiAdaptor = await exec('sh -c \"networksetup -listnetworkserviceorder | grep `route -n get 0.0.0.0 | grep \\'interface\\' | cut -d \\':\\' -f2` -B 1 | head -n 1 \"')\n    wifiAdaptor = wifiAdaptor.trim()\n    wifiAdaptor = wifiAdaptor.substring(wifiAdaptor.indexOf(' ')).trim()\n    const { ip, port } = params\n    if (ip != null) { // 设置代理\n      // 延迟加载config\n      loadConfig()\n\n      // https\n      await exec(`networksetup -setsecurewebproxy \"${wifiAdaptor}\" ${ip} ${port}`)\n      // http\n      if (config.get().proxy.proxyHttp) {\n        await exec(`networksetup -setwebproxy \"${wifiAdaptor}\" ${ip} ${port - 1}`)\n      } else {\n        await exec(`networksetup -setwebproxystate \"${wifiAdaptor}\" off`)\n      }\n\n      // 设置排除域名\n      const excludeIpStr = getProxyExcludeIpStr('\" \"')\n      await exec(`networksetup -setproxybypassdomains \"${wifiAdaptor}\" \"${excludeIpStr}\"`)\n\n      // const setEnv = `cat <<ENDOF >>  ~/.zshrc\n      // export http_proxy=\"http://${ip}:${port}\"\n      // export https_proxy=\"http://${ip}:${port}\"\n      // ENDOF\n      // source ~/.zshrc\n      // `\n      // await exec(setEnv)\n    } else { // 关闭代理\n      // https\n      await exec(`networksetup -setsecurewebproxystate \"${wifiAdaptor}\" off`)\n      // http\n      await exec(`networksetup -setwebproxystate \"${wifiAdaptor}\" off`)\n\n      // const removeEnv = `\n      // sed -ie '/export http_proxy/d' ~/.zshrc\n      // sed -ie '/export https_proxy/d' ~/.zshrc\n      // source ~/.zshrc\n      // `\n      // await exec(removeEnv)\n    }\n  },\n}\n\nmodule.exports = async function (args) {\n  return execute(executor, args)\n}\n"
  },
  {
    "path": "packages/core/src/shell/scripts/set-system-proxy/refresh-internet.js",
    "content": "const script = `\n$signature = @'\n[DllImport(\"wininet.dll\", SetLastError = true, CharSet=CharSet.Auto)]\npublic static extern bool InternetSetOption(IntPtr hInternet, int dwOption, IntPtr lpBuffer, int dwBufferLength);\n'@\n\n$INTERNET_OPTION_SETTINGS_CHANGED   = 39\n$INTERNET_OPTION_REFRESH            = 37\n$type = Add-Type -MemberDefinition $signature -Name wininet -Namespace pinvoke -PassThru\n$a = $type::InternetSetOption(0, $INTERNET_OPTION_SETTINGS_CHANGED, 0, 0)\n$b = $type::InternetSetOption(0, $INTERNET_OPTION_REFRESH, 0, 0)\n$a -and $b\n`\nmodule.exports = script\n"
  },
  {
    "path": "packages/core/src/shell/scripts/setup-ca.js",
    "content": "const Shell = require('../shell')\n\nconst execute = Shell.execute\n\nconst executor = {\n  async windows (exec, { certPath }) {\n    const cmds = [`start \"\" \"${certPath}\"`]\n    await exec(cmds, { type: 'cmd' })\n    return true\n  },\n  async linux (exec, { certPath }) {\n    const cmds = [`sudo cp ${certPath} /usr/local/share/ca-certificates`, 'sudo update-ca-certificates ']\n    await exec(cmds)\n    return true\n  },\n  async mac (exec, { certPath }) {\n    const cmds = [`open \"${certPath}\"`]\n    await exec(cmds, { type: 'cmd' })\n    return true\n  },\n}\n\nmodule.exports = async function (args) {\n  return execute(executor, args)\n}\n"
  },
  {
    "path": "packages/core/src/shell/shell.js",
    "content": "const childProcess = require('node:child_process')\nconst os = require('node:os')\nconst fixPath = require('fix-path')\nconst PowerShell = require('node-powershell')\nconst log = require('../utils/util.log.core')\n\nfixPath()\n\nclass SystemShell {\n  static async exec (cmds, args) {\n    throw new Error('You have to implement the method exec!')\n  }\n}\n\nclass LinuxSystemShell extends SystemShell {\n  static async exec (cmds) {\n    if (typeof cmds === 'string') {\n      cmds = [cmds]\n    }\n    for (const cmd of cmds) {\n      await childExec(cmd, { shell: '/bin/bash' })\n    }\n  }\n}\n\nclass DarwinSystemShell extends SystemShell {\n  static async exec (cmds) {\n    if (typeof cmds === 'string') {\n      cmds = [cmds]\n    }\n    let ret\n    for (const cmd of cmds) {\n      ret = await childExec(cmd)\n    }\n    return ret\n  }\n}\n\nclass WindowsSystemShell extends SystemShell {\n  static async exec (cmds, args = { }) {\n    let { type } = args\n    type = type || 'ps'\n    if (typeof cmds === 'string') {\n      cmds = [cmds]\n    }\n    if (type === 'ps') {\n      const ps = new PowerShell({\n        executionPolicy: 'Bypass',\n        noProfile: true,\n      })\n\n      for (const cmd of cmds) {\n        ps.addCommand(cmd)\n      }\n\n      try {\n        return await ps.invoke()\n      } finally {\n        ps.dispose()\n      }\n    } else {\n      let compose = 'chcp 65001' // 'chcp 65001  '\n      for (const cmd of cmds) {\n        compose += ` && ${cmd}`\n      }\n      // compose += '&& exit'\n      return await childExec(compose, args)\n    }\n  }\n}\n\nfunction childExec (composeCmds, options = {}) {\n  return new Promise((resolve, reject) => {\n    log.info('shell:', composeCmds)\n    childProcess.exec(composeCmds, options, (error, stdout, stderr) => {\n      if (error) {\n        if (options.printErrorLog !== false) {\n          log.error('cmd 命令执行错误：\\n===>\\ncommands:', composeCmds, '\\n   error:', error, '\\n<===')\n        }\n        reject(new Error(stderr))\n      } else {\n        // log.info('cmd 命令完成：', stdout)\n        resolve(stdout.replace('Active code page: 65001\\r\\n', ''))\n      }\n      // log.info('关闭 cmd')\n      // ps.kill('SIGINT')\n    })\n  })\n}\n\nfunction getSystemShell () {\n  switch (getSystemPlatform(true)) {\n    case 'mac':\n      return DarwinSystemShell\n    case 'linux':\n      return LinuxSystemShell\n    case 'windows':\n      return WindowsSystemShell\n    default:\n      throw new Error(`UNKNOWN OS TYPE ${os.platform()}`)\n  }\n}\n\nfunction getSystemPlatform (throwIfUnknown = false) {\n  switch (os.platform()) {\n    case 'darwin':\n      return 'mac'\n    case 'linux':\n      return 'linux'\n    case 'win32':\n      return 'windows'\n    case 'win64':\n      return 'windows'\n    default:\n      log.error(`UNKNOWN OS TYPE: ${os.platform()}`)\n      if (throwIfUnknown) {\n        throw new Error(`UNKNOWN OS TYPE '${os.platform()}'`)\n      } else {\n        return 'unknown-os'\n      }\n  }\n}\n\nasync function execute (executor, args) {\n  return executor[getSystemPlatform(true)](getSystemShell().exec, args)\n}\n\nasync function execFile (file, args, options) {\n  return new Promise((resolve, reject) => {\n    try {\n      childProcess.execFile(file, args, options, (err, stdout) => {\n        if (err) {\n          log.error('文件执行出错：', file, err)\n          reject(err)\n          return\n        }\n        log.debug('文件执行成功：', file)\n        resolve(stdout)\n      })\n    } catch (e) {\n      log.error('文件执行出错：', file, e)\n      reject(e)\n    }\n  })\n}\n\nmodule.exports = {\n  getSystemShell,\n  getSystemPlatform,\n  execute,\n  execFile,\n}\n"
  },
  {
    "path": "packages/core/src/status.js",
    "content": "const lodash = require('lodash')\nconst event = require('./event')\nconst log = require('./utils/util.log.core')\n\nconst status = {\n  server: { enabled: false },\n  proxy: {},\n  plugin: {},\n}\n\nevent.register('status', (event) => {\n  lodash.set(status, event.key, event.value)\n  log.info('status changed:', event)\n}, -999)\n\nmodule.exports = status\n"
  },
  {
    "path": "packages/core/src/utils/util.date.js",
    "content": "module.exports = {\n\n  format (date, needMill = true) {\n    if (date == null) {\n      return 'null'\n    }\n\n    const year = date.getFullYear() // 获取年份\n    const month = (date.getMonth() + 1).toString().padStart(2, '0') // 获取月份（注意月份从 0 开始计数）\n    const day = date.getDate().toString().padStart(2, '0') // 获取天数\n    const hours = date.getHours().toString().padStart(2, '0') // 获取小时\n    const minutes = date.getMinutes().toString().padStart(2, '0') // 获取分钟\n    const seconds = date.getSeconds().toString().padStart(2, '0') // 获取秒数\n    const milliseconds = needMill ? `.${date.getMilliseconds().toString().padStart(3, '0')}` : '' // 获取毫秒\n\n    return `${year}-${month}-${day} ${hours}:${minutes}:${seconds}${milliseconds}`\n  },\n\n  now (needMill = true) {\n    return this.format(new Date(), needMill)\n  },\n\n}\n"
  },
  {
    "path": "packages/core/src/utils/util.log-or-console.js",
    "content": "const dateUtil = require('./util.date')\n\nlet log = console\n\n// 将console中的日志缓存起来，当setLogger时，将控制台的日志写入日志文件\nlet backupLogs = []\n\nfunction backup (fun, args) {\n  if (backupLogs === null) {\n    return\n  }\n\n  try {\n    backupLogs.push({\n      fun,\n      args,\n      time: dateUtil.format(new Date()),\n    })\n\n    // 最多缓存 100 条\n    if (backupLogs.length > 100) {\n      backupLogs = backupLogs.slice(1)\n    }\n  } catch {\n  }\n}\n\nfunction printBackups () {\n  if (backupLogs === null || log === console) {\n    return\n  }\n\n  try {\n    const backups = backupLogs\n    backupLogs = null // 先置空历史消息对象，再记录日志\n\n    for (const item of backups) {\n      log[item.fun](...[`[${item.time}] console -`, ...item.args])\n    }\n  } catch {\n  }\n}\n\nfunction _doLog (fun, args) {\n  if (log === console) {\n    log[fun](...[`[${fun.toUpperCase()}]`, ...args])\n    backup(fun, args) // 控制台日志备份起来\n  } else {\n    log[fun](...args)\n  }\n}\n\nmodule.exports = {\n  setLogger (logger) {\n    if (logger == null) {\n      log.error('logger 不能为空')\n      return\n    }\n\n    if (logger === log) {\n      return\n    }\n\n    log = logger\n\n    if (log !== console) {\n      try {\n        if (backupLogs && backupLogs.length > 0) {\n          log.info('[util.log-or-console.js] 日志系统已初始化完成，现开始将历史控制台信息记录到日志文件中：')\n          printBackups()\n        }\n      } catch {\n      }\n    }\n  },\n\n  debug (...args) {\n    _doLog('debug', args)\n  },\n  info (...args) {\n    _doLog('info', args)\n  },\n  warn (...args) {\n    _doLog('warn', args)\n  },\n  error (...args) {\n    _doLog('error', args)\n  },\n}\n"
  },
  {
    "path": "packages/core/src/utils/util.log.core.js",
    "content": "const loggerFactory = require('./util.logger')\n\nconst logger = loggerFactory.getLogger('core')\n\nmodule.exports = logger\n"
  },
  {
    "path": "packages/core/src/utils/util.logger.js",
    "content": "const path = require('node:path')\nconst log4js = require('log4js')\nconst logOrConsole = require('./util.log-or-console')\nconst defaultConfig = require('../config/index.js')\nconst configFromFiles = defaultConfig.configFromFiles\n\n// 日志级别\nconst level = process.env.NODE_ENV === 'development' ? 'debug' : 'info'\n\nfunction getDefaultConfigBasePath () {\n  if (configFromFiles.app.logFileSavePath) {\n    let logFileSavePath = configFromFiles.app.logFileSavePath\n    if (logFileSavePath.endsWith('/') || logFileSavePath.endsWith('\\\\')) {\n      logFileSavePath = logFileSavePath.slice(0, -1)\n    }\n    // eslint-disable-next-line no-template-curly-in-string\n    return logFileSavePath.replace('${userBasePath}', configFromFiles.server.setting.userBasePath)\n  } else {\n    return path.join(configFromFiles.server.setting.userBasePath, '/logs')\n  }\n}\n\n// 日志文件目录\nconst basePath = getDefaultConfigBasePath()\n\n// 通用日志配置\nconst appenderConfig = {\n  type: 'file',\n  pattern: 'yyyy-MM-dd',\n  compress: true, // 压缩日志文件\n  keepFileExt: true, // 保留日志文件扩展名为 .log\n  backups: Math.ceil(configFromFiles.app.keepLogFileCount) || defaultConfig.app.keepLogFileCount, // 保留日志文件数\n  maxLogSize: Math.ceil((configFromFiles.app.maxLogFileSize || defaultConfig.app.maxLogFileSize) * 1024 * 1024 * (configFromFiles.app.maxLogFileSizeUnit === 'GB' ? 1024 : 1)), // 目前单位只有GB和MB\n}\n\nlet log = null\n\n// 设置一组日志配置\nfunction log4jsConfigure (categories) {\n  if (log != null) {\n    log.error('当前进程已经设置过日志配置，无法再设置更多日志配置:', categories)\n    return\n  }\n\n  const config = {\n    appenders: {\n      std: { type: 'stdout' },\n    },\n    categories: {\n      default: { appenders: ['std'], level },\n    },\n  }\n\n  for (const category of categories) {\n    config.appenders[category] = { ...appenderConfig, filename: path.join(basePath, `/${category}.log`) }\n    config.categories[category] = { appenders: [category, 'std'], level }\n  }\n\n  log4js.configure(config)\n\n  // 拿第一个日志类型来logger并设置到log变量中\n  log = log4js.getLogger(categories[0])\n  logOrConsole.setLogger(log)\n\n  log.info(`设置日志配置完成，进程ID: ${process.pid}，categories：[${categories}]，config:`, JSON.stringify(config))\n}\n\nmodule.exports = {\n  getLogger (category) {\n    if (!category) {\n      if (log) {\n        log.error('未指定日志类型，无法配置并获取日志对象！！！')\n      }\n      throw new Error('未指定日志类型，无法配置并获取日志对象！！！')\n    }\n\n    if (category === 'core' || category === 'gui') {\n      // core 和 gui 的日志配置，因为它们在同一进程中，所以一起配置，且只能配置一次\n      if (log == null) {\n        log4jsConfigure(['core', 'gui'])\n      }\n\n      return log4js.getLogger(category)\n    } else {\n      if (log == null) {\n        log4jsConfigure([category])\n      } else if (category !== log.category) {\n        log.error(`当前进程已经设置过日志配置，无法再设置 \"${category}\" 的配置，先临时返回 \"${log.category}\" 的 log 进行日志记录。如果与其他类型的日志在同一进程中写入，请参照 core 和 gui 一起配置`)\n      }\n\n      return log\n    }\n  },\n}\n"
  },
  {
    "path": "packages/core/src/utils/util.version.js",
    "content": "function parseVersion (version) {\n  const matched = version.match(/^v?(\\d{1,2}(?:\\.\\d{1,2})*)(.*)$/)\n  return {\n    versions: matched[1].split('.'), // 版本号数组\n    pre: matched[2], // 预发布版本号\n  }\n}\n\n/**\n * 比较版本号\n *\n * @param onlineVersion  线上版本号\n * @param currentVersion 当前版本号\n * @param log            日志对象\n * @returns {number} 比较线上版本号是否为更新的版本，大于0=是|0=相等|小于0=否|-999=出现异常，比较结果未知\n */\nexport function isNewVersion (onlineVersion, currentVersion, log = null) {\n  if (onlineVersion === currentVersion) {\n    return 0\n  }\n\n  try {\n    const onlineVersionObj = parseVersion(onlineVersion)\n    const curVersionObj = parseVersion(currentVersion)\n\n    const { versions: versions1 } = onlineVersionObj\n    const { versions: versions2 } = curVersionObj\n\n    if (versions1.length !== versions2.length) {\n      // 短的数组补0\n      if (versions1.length < versions2.length) {\n        for (let i = versions1.length; i < versions2.length; i++) {\n          versions1.push('0')\n        }\n      } else if (versions1.length > versions2.length) {\n        for (let i = versions2.length; i < versions1.length; i++) {\n          versions2.push('0')\n        }\n      }\n    }\n\n    // 版本数组比对\n    for (let i = 0; i < versions1.length; i++) {\n      if (versions1[i] > versions2[i]) {\n        return i + 1 // 为新版本，需要更新\n      } else if (versions1[i] < versions2[i]) {\n        return -(i + 1) // 为旧版本，无需更新\n      }\n    }\n\n    // 版本号相同，继续比对预发布版本号\n    if (onlineVersionObj.pre && curVersionObj.pre) {\n      // 都为预发布版本时，直接比较预发布版本号字符串的大小\n      if (onlineVersionObj.pre > curVersionObj.pre) {\n        return 101\n      } else if (onlineVersionObj.pre < curVersionObj.pre) {\n        return -101\n      }\n    } else if (!onlineVersionObj.pre && curVersionObj.pre) {\n      // 线上为正式版本，当前版本为预发布版本，需要更新\n      return 102\n    } else if (onlineVersionObj.pre && !curVersionObj.pre) {\n      // 线上为预发布版本，当前版本为正式版本，无需更新\n      return -102\n    }\n\n    return 0 // 相同版本，无需更新\n  } catch (e) {\n    (log || console).error(`比对版本失败，当前版本号：${currentVersion}，线上版本号：${onlineVersion}, error:`, e)\n    return -999 // 比对异常\n  }\n}\n"
  },
  {
    "path": "packages/core/test/configTest.js",
    "content": "// const config = require('../src/config-api')\n//\n// config.set({\n//   server: {\n//     intercepts: {\n//       'github1.githubassets.com': {\n//         '.*': {\n//           redirect: 'assets.fastgit.org',\n//           test: 'https://github.githubassets.com/favicons/favicon.svg',\n//           desc: '静态资源加速'\n//         }\n//       },\n//       'github.githubassets.com': null\n//     }\n//   }\n// })\n//\n// console.log(config.get())\n//\n// config.reload()\n"
  },
  {
    "path": "packages/core/test/httpsVerifyTest.js",
    "content": "// const https = require('node:https')\n//\n// process.env.NODE_TLS_REJECT_UNAUTHORIZED = '1'\n//\n// function request () {\n//   return new Promise((resolve, reject) => {\n//     const options = {\n//       hostname: 'test1.gagedigital.com',\n//       port: 443,\n//       path: '/ssltest.php',\n//       method: 'GET',\n//       rejectUnauthorized: true,\n//     }\n//     console.log('ssl test: gagedigital')\n//     const req = https.request(options, (res) => {\n//       console.log('statusCode:', res.statusCode)\n//       console.log('headers:', res.headers)\n//\n//       res.on('data', (d) => {\n//         process.stdout.write(d)\n//         resolve()\n//       })\n//     })\n//\n//     req.on('error', (e) => {\n//       console.error(e)\n//       reject(e)\n//     })\n//     req.end()\n//   })\n// }\n// // eslint-disable-next-line no-undef\n// describe('ssl.verify', () => {\n//   // eslint-disable-next-line no-undef\n//   it('regex.test.js', async () => {\n//     // https.request('https://test1.gagedigital.com/ssltest.php')\n//     await request()\n//\n//     // expect(ret).be.ok\n//   })\n// })\n"
  },
  {
    "path": "packages/core/test/macProxyTest.js",
    "content": "const assert = require('node:assert')\n\n// const childProcess = require('child_process')\n// const util = require('util')\n// const exec = util.promisify(childProcess.exec)\n//\n// async function test () {\n//   const wifiAdaptor = (await exec('sh -c \"networksetup -listnetworkserviceorder | grep `route -n get 0.0.0.0 | grep \\'interface\\' | cut -d \\':\\' -f2` -B 1 | head -n 1 | cut -d \\' \\' -f2\"')).stdout.trim()\n//\n//   await exec(`networksetup -setwebproxystate '${wifiAdaptor}' off`)\n//   return await exec(`networksetup -setsecurewebproxystate '${wifiAdaptor}' off`)\n// }\n// test().then((ret) => {\n//   console.log('haha', ret)\n// })\nlet wifiAdaptor = '(151) test'\nwifiAdaptor = wifiAdaptor.substring(wifiAdaptor.indexOf(' ')).trim()\nconsole.log(wifiAdaptor)\nassert.strictEqual(wifiAdaptor, 'test')\n"
  },
  {
    "path": "packages/core/test/mergeTest.js",
    "content": "const assert = require('node:assert')\nconst lodash = require('lodash')\nconst mergeApi = require('../src/merge.js')\n\n// 默认配置\nconst defConfig = {\n  a: {\n    aa: { value: 1 },\n    bb: { value: 2 },\n  },\n  b: { c: 2 },\n  c: 1,\n  d: [1, 2, 3],\n  e: {\n    aa: 2,\n    ee: 5,\n  },\n  f: {\n    x: 1,\n  },\n  g: [1, 2],\n  h: null,\n  i: null,\n}\n\n// 自定义配置\nconst customConfig = {\n  a: {\n    bb: { value: 2 },\n    cc: { value: 3 },\n  },\n  b: { c: 2 },\n  c: null,\n  d: [1, 2, 3, 4],\n  e: {\n    aa: 2,\n    ee: 5,\n    ff: 6,\n  },\n  f: {},\n  g: [1, 2],\n  h: null,\n}\n\n// doDiff\nconst doDiffResult = mergeApi.doDiff(defConfig, customConfig)\nconsole.log('doDiffResult:', JSON.stringify(doDiffResult, null, 2))\nconsole.log('\\r')\n// 校验doDiff结果\nconst doDiffExpect = {\n  a: {\n    aa: null,\n    cc: { value: 3 },\n  },\n  c: null,\n  d: [1, 2, 3, 4],\n  e: {\n    ff: 6,\n  },\n  f: {\n    x: null,\n  },\n}\nconsole.log('check diff result:', lodash.isEqual(doDiffResult, doDiffExpect))\nconsole.log('\\r')\n\n// doMerge\nconst doMergeResult = mergeApi.doMerge(defConfig, doDiffResult)\n// delete null item\nmergeApi.deleteNullItems(doMergeResult)\nconsole.log('running:', JSON.stringify(doMergeResult, null, 2))\n// 校验doMerge结果\nconst doMergeExpect = {\n  a: {\n    bb: { value: 2 },\n    cc: { value: 3 },\n  },\n  b: { c: 2 },\n  d: [1, 2, 3, 4],\n  e: {\n    aa: 2,\n    ee: 5,\n    ff: 6,\n  },\n  f: {},\n  g: [1, 2],\n}\n\nconst result = lodash.isEqual(doMergeResult, doMergeExpect)\nconsole.log('check merge result:', result)\nconsole.log('\\r')\nassert.strictEqual(result, true)\n"
  },
  {
    "path": "packages/core/test/regex.test.js",
    "content": "const assert = require('node:assert')\nconst expect = require('chai').expect\n// eslint-disable-next-line no-undef\ndescribe('test', () => {\n  // eslint-disable-next-line no-undef\n  it('regexp', () => {\n    const test = '^/[^/]+/[^/]+(?:/releases(?:/.*)?)?$'\n    const reg = new RegExp(test)\n\n    const ret = reg.test('/docmirror/dev-sidecar/releases/tag')\n    console.log(ret)\n    assert.strictEqual(ret, true)\n\n    expect(ret).be.ok\n  })\n})\n"
  },
  {
    "path": "packages/core/test/requestTest.js",
    "content": "const HttpsAgent = require('@docmirror/mitmproxy/src/lib/proxy/common/ProxyHttpsAgent')\nconst request = require('request')\n\nconst options = {\n  url: 'https://raw.githubusercontent.com/docmirror/dev-sidecar/refs/heads/master/packages/core/src/config/remote_config.json5',\n  // url: 'https://gitee.com/wangliang181230/dev-sidecar/raw/docmirror2.x/packages/core/src/config/remote_config.json',\n  servername: 'baidu.com',\n  agent: new HttpsAgent({\n    keepAlive: true,\n    timeout: 20000,\n    keepAliveTimeout: 30000,\n    rejectUnauthorized: false,\n  }),\n}\nif (options.agent.options) {\n  options.agent.options.rejectUnauthorized = false\n  console.info('options.agent.options.rejectUnauthorized = false')\n}\n\nrequest(options, (error, response, body) => {\n  console.info('error:', error, '\\n---------------------------------------------------------------------------\\n'\n  + 'response:', response, '\\n---------------------------------------------------------------------------\\n'\n  + 'body:', body)\n})\n"
  },
  {
    "path": "packages/core/test/versionTest.js",
    "content": "const assert = require('node:assert')\nconst { isNewVersion } = require('../src/utils/util.version.js')\n\nfunction testIsNewVersion (onlineVersion, currentVersion, expected) {\n  const ret = isNewVersion(onlineVersion, currentVersion)\n  console.log(ret >= 0 ? ` ${ret}` : `${ret}`)\n  assert.strictEqual(ret, expected)\n}\n\ntestIsNewVersion('2.0.0', '2.0.0', 0)\n\ntestIsNewVersion('2.0.0', '1.0.0', 1)\ntestIsNewVersion('1.0.0', '2.0.0', -1)\n\ntestIsNewVersion('2.1.0', '2.0.0', 2)\ntestIsNewVersion('2.0.0', '2.1.0', -2)\n\ntestIsNewVersion('2.0.1', '2.0.0', 3)\ntestIsNewVersion('2.0.0', '2.0.1', -3)\n\ntestIsNewVersion('2.0.0.1', '2.0.0', 4)\ntestIsNewVersion('2.0.0', '2.0.0.1', -4)\n\ntestIsNewVersion('2.0.0.9.1', '2.0.0.9', 5)\ntestIsNewVersion('2.0.0.9', '2.0.0.9.1', -5)\n\ntestIsNewVersion('2.0.0-RC2', '2.0.0-RC1', 101)\ntestIsNewVersion('2.0.0-RC1', '2.0.0-RC2', -101)\n\ntestIsNewVersion('2.0.0', '2.0.0-RC1', 102)\ntestIsNewVersion('2.0.0-RC1', '2.0.0', -102)\n\ntestIsNewVersion('2.0.0.0', '2.0.0', 0)\n\ntestIsNewVersion('x', 'v', -999)\n"
  },
  {
    "path": "packages/gui/.editorconfig",
    "content": "[*.{js,jsx,ts,tsx,vue}]\nindent_style = space\nindent_size = 2\ntrim_trailing_whitespace = true\ninsert_final_newline = true\n"
  },
  {
    "path": "packages/gui/.gitignore",
    "content": ".DS_Store\nnode_modules\n/dist\n\n\n# local env files\n.env.local\n.env.*.local\n\n# Log files\nnpm-debug.log*\nyarn-debug.log*\nyarn-error.log*\npnpm-debug.log*\n\n# Editor directories and files\n.idea\n.vscode\n*.suo\n*.ntvs*\n*.njsproj\n*.sln\n*.sw?\n*.lock\n*.log\n#Electron-builder output\n/dist_electron\n/config\n"
  },
  {
    "path": "packages/gui/LICENSE",
    "content": "Mozilla Public License Version 2.0\n==================================\n\n1. Definitions\n--------------\n\n1.1. \"Contributor\"\n    means each individual or legal entity that creates, contributes to\n    the creation of, or owns Covered Software.\n\n1.2. \"Contributor Version\"\n    means the combination of the Contributions of others (if any) used\n    by a Contributor and that particular Contributor's Contribution.\n\n1.3. \"Contribution\"\n    means Covered Software of a particular Contributor.\n\n1.4. \"Covered Software\"\n    means Source Code Form to which the initial Contributor has attached\n    the notice in Exhibit A, the Executable Form of such Source Code\n    Form, and Modifications of such Source Code Form, in each case\n    including portions thereof.\n\n1.5. \"Incompatible With Secondary Licenses\"\n    means\n\n    (a) that the initial Contributor has attached the notice described\n        in Exhibit B to the Covered Software; or\n\n    (b) that the Covered Software was made available under the terms of\n        version 1.1 or earlier of the License, but not also under the\n        terms of a Secondary License.\n\n1.6. \"Executable Form\"\n    means any form of the work other than Source Code Form.\n\n1.7. \"Larger Work\"\n    means a work that combines Covered Software with other material, in\n    a separate file or files, that is not Covered Software.\n\n1.8. \"License\"\n    means this document.\n\n1.9. \"Licensable\"\n    means having the right to grant, to the maximum extent possible,\n    whether at the time of the initial grant or subsequently, any and\n    all of the rights conveyed by this License.\n\n1.10. \"Modifications\"\n    means any of the following:\n\n    (a) any file in Source Code Form that results from an addition to,\n        deletion from, or modification of the contents of Covered\n        Software; or\n\n    (b) any new file in Source Code Form that contains any Covered\n        Software.\n\n1.11. \"Patent Claims\" of a Contributor\n    means any patent claim(s), including without limitation, method,\n    process, and apparatus claims, in any patent Licensable by such\n    Contributor that would be infringed, but for the grant of the\n    License, by the making, using, selling, offering for sale, having\n    made, import, or transfer of either its Contributions or its\n    Contributor Version.\n\n1.12. \"Secondary License\"\n    means either the GNU General Public License, Version 2.0, the GNU\n    Lesser General Public License, Version 2.1, the GNU Affero General\n    Public License, Version 3.0, or any later versions of those\n    licenses.\n\n1.13. \"Source Code Form\"\n    means the form of the work preferred for making modifications.\n\n1.14. \"You\" (or \"Your\")\n    means an individual or a legal entity exercising rights under this\n    License. For legal entities, \"You\" includes any entity that\n    controls, is controlled by, or is under common control with You. For\n    purposes of this definition, \"control\" means (a) the power, direct\n    or indirect, to cause the direction or management of such entity,\n    whether by contract or otherwise, or (b) ownership of more than\n    fifty percent (50%) of the outstanding shares or beneficial\n    ownership of such entity.\n\n2. License Grants and Conditions\n--------------------------------\n\n2.1. Grants\n\nEach Contributor hereby grants You a world-wide, royalty-free,\nnon-exclusive license:\n\n(a) under intellectual property rights (other than patent or trademark)\n    Licensable by such Contributor to use, reproduce, make available,\n    modify, display, perform, distribute, and otherwise exploit its\n    Contributions, either on an unmodified basis, with Modifications, or\n    as part of a Larger Work; and\n\n(b) under Patent Claims of such Contributor to make, use, sell, offer\n    for sale, have made, import, and otherwise transfer either its\n    Contributions or its Contributor Version.\n\n2.2. Effective Date\n\nThe licenses granted in Section 2.1 with respect to any Contribution\nbecome effective for each Contribution on the date the Contributor first\ndistributes such Contribution.\n\n2.3. Limitations on Grant Scope\n\nThe licenses granted in this Section 2 are the only rights granted under\nthis License. No additional rights or licenses will be implied from the\ndistribution or licensing of Covered Software under this License.\nNotwithstanding Section 2.1(b) above, no patent license is granted by a\nContributor:\n\n(a) for any code that a Contributor has removed from Covered Software;\n    or\n\n(b) for infringements caused by: (i) Your and any other third party's\n    modifications of Covered Software, or (ii) the combination of its\n    Contributions with other software (except as part of its Contributor\n    Version); or\n\n(c) under Patent Claims infringed by Covered Software in the absence of\n    its Contributions.\n\nThis License does not grant any rights in the trademarks, service marks,\nor logos of any Contributor (except as may be necessary to comply with\nthe notice requirements in Section 3.4).\n\n2.4. Subsequent Licenses\n\nNo Contributor makes additional grants as a result of Your choice to\ndistribute the Covered Software under a subsequent version of this\nLicense (see Section 10.2) or under the terms of a Secondary License (if\npermitted under the terms of Section 3.3).\n\n2.5. Representation\n\nEach Contributor represents that the Contributor believes its\nContributions are its original creation(s) or it has sufficient rights\nto grant the rights to its Contributions conveyed by this License.\n\n2.6. Fair Use\n\nThis License is not intended to limit any rights You have under\napplicable copyright doctrines of fair use, fair dealing, or other\nequivalents.\n\n2.7. Conditions\n\nSections 3.1, 3.2, 3.3, and 3.4 are conditions of the licenses granted\nin Section 2.1.\n\n3. Responsibilities\n-------------------\n\n3.1. Distribution of Source Form\n\nAll distribution of Covered Software in Source Code Form, including any\nModifications that You create or to which You contribute, must be under\nthe terms of this License. You must inform recipients that the Source\nCode Form of the Covered Software is governed by the terms of this\nLicense, and how they can obtain a copy of this License. You may not\nattempt to alter or restrict the recipients' rights in the Source Code\nForm.\n\n3.2. Distribution of Executable Form\n\nIf You distribute Covered Software in Executable Form then:\n\n(a) such Covered Software must also be made available in Source Code\n    Form, as described in Section 3.1, and You must inform recipients of\n    the Executable Form how they can obtain a copy of such Source Code\n    Form by reasonable means in a timely manner, at a charge no more\n    than the cost of distribution to the recipient; and\n\n(b) You may distribute such Executable Form under the terms of this\n    License, or sublicense it under different terms, provided that the\n    license for the Executable Form does not attempt to limit or alter\n    the recipients' rights in the Source Code Form under this License.\n\n3.3. Distribution of a Larger Work\n\nYou may create and distribute a Larger Work under terms of Your choice,\nprovided that You also comply with the requirements of this License for\nthe Covered Software. If the Larger Work is a combination of Covered\nSoftware with a work governed by one or more Secondary Licenses, and the\nCovered Software is not Incompatible With Secondary Licenses, this\nLicense permits You to additionally distribute such Covered Software\nunder the terms of such Secondary License(s), so that the recipient of\nthe Larger Work may, at their option, further distribute the Covered\nSoftware under the terms of either this License or such Secondary\nLicense(s).\n\n3.4. Notices\n\nYou may not remove or alter the substance of any license notices\n(including copyright notices, patent notices, disclaimers of warranty,\nor limitations of liability) contained within the Source Code Form of\nthe Covered Software, except that You may alter any license notices to\nthe extent required to remedy known factual inaccuracies.\n\n3.5. Application of Additional Terms\n\nYou may choose to offer, and to charge a fee for, warranty, support,\nindemnity or liability obligations to one or more recipients of Covered\nSoftware. However, You may do so only on Your own behalf, and not on\nbehalf of any Contributor. You must make it absolutely clear that any\nsuch warranty, support, indemnity, or liability obligation is offered by\nYou alone, and You hereby agree to indemnify every Contributor for any\nliability incurred by such Contributor as a result of warranty, support,\nindemnity or liability terms You offer. You may include additional\ndisclaimers of warranty and limitations of liability specific to any\njurisdiction.\n\n4. Inability to Comply Due to Statute or Regulation\n---------------------------------------------------\n\nIf it is impossible for You to comply with any of the terms of this\nLicense with respect to some or all of the Covered Software due to\nstatute, judicial order, or regulation then You must: (a) comply with\nthe terms of this License to the maximum extent possible; and (b)\ndescribe the limitations and the code they affect. Such description must\nbe placed in a text file included with all distributions of the Covered\nSoftware under this License. Except to the extent prohibited by statute\nor regulation, such description must be sufficiently detailed for a\nrecipient of ordinary skill to be able to understand it.\n\n5. Termination\n--------------\n\n5.1. The rights granted under this License will terminate automatically\nif You fail to comply with any of its terms. However, if You become\ncompliant, then the rights granted under this License from a particular\nContributor are reinstated (a) provisionally, unless and until such\nContributor explicitly and finally terminates Your grants, and (b) on an\nongoing basis, if such Contributor fails to notify You of the\nnon-compliance by some reasonable means prior to 60 days after You have\ncome back into compliance. Moreover, Your grants from a particular\nContributor are reinstated on an ongoing basis if such Contributor\nnotifies You of the non-compliance by some reasonable means, this is the\nfirst time You have received notice of non-compliance with this License\nfrom such Contributor, and You become compliant prior to 30 days after\nYour receipt of the notice.\n\n5.2. If You initiate litigation against any entity by asserting a patent\ninfringement claim (excluding declaratory judgment actions,\ncounter-claims, and cross-claims) alleging that a Contributor Version\ndirectly or indirectly infringes any patent, then the rights granted to\nYou by any and all Contributors for the Covered Software under Section\n2.1 of this License shall terminate.\n\n5.3. In the event of termination under Sections 5.1 or 5.2 above, all\nend user license agreements (excluding distributors and resellers) which\nhave been validly granted by You or Your distributors under this License\nprior to termination shall survive termination.\n\n************************************************************************\n*                                                                      *\n*  6. Disclaimer of Warranty                                           *\n*  -------------------------                                           *\n*                                                                      *\n*  Covered Software is provided under this License on an \"as is\"       *\n*  basis, without warranty of any kind, either expressed, implied, or  *\n*  statutory, including, without limitation, warranties that the       *\n*  Covered Software is free of defects, merchantable, fit for a        *\n*  particular purpose or non-infringing. The entire risk as to the     *\n*  quality and performance of the Covered Software is with You.        *\n*  Should any Covered Software prove defective in any respect, You     *\n*  (not any Contributor) assume the cost of any necessary servicing,   *\n*  repair, or correction. This disclaimer of warranty constitutes an   *\n*  essential part of this License. No use of any Covered Software is   *\n*  authorized under this License except under this disclaimer.         *\n*                                                                      *\n************************************************************************\n\n************************************************************************\n*                                                                      *\n*  7. Limitation of Liability                                          *\n*  --------------------------                                          *\n*                                                                      *\n*  Under no circumstances and under no legal theory, whether tort      *\n*  (including negligence), contract, or otherwise, shall any           *\n*  Contributor, or anyone who distributes Covered Software as          *\n*  permitted above, be liable to You for any direct, indirect,         *\n*  special, incidental, or consequential damages of any character      *\n*  including, without limitation, damages for lost profits, loss of    *\n*  goodwill, work stoppage, computer failure or malfunction, or any    *\n*  and all other commercial damages or losses, even if such party      *\n*  shall have been informed of the possibility of such damages. This   *\n*  limitation of liability shall not apply to liability for death or   *\n*  personal injury resulting from such party's negligence to the       *\n*  extent applicable law prohibits such limitation. Some               *\n*  jurisdictions do not allow the exclusion or limitation of           *\n*  incidental or consequential damages, so this exclusion and          *\n*  limitation may not apply to You.                                    *\n*                                                                      *\n************************************************************************\n\n8. Litigation\n-------------\n\nAny litigation relating to this License may be brought only in the\ncourts of a jurisdiction where the defendant maintains its principal\nplace of business and such litigation shall be governed by laws of that\njurisdiction, without reference to its conflict-of-law provisions.\nNothing in this Section shall prevent a party's ability to bring\ncross-claims or counter-claims.\n\n9. Miscellaneous\n----------------\n\nThis License represents the complete agreement concerning the subject\nmatter hereof. If any provision of this License is held to be\nunenforceable, such provision shall be reformed only to the extent\nnecessary to make it enforceable. Any law or regulation which provides\nthat the language of a contract shall be construed against the drafter\nshall not be used to construe this License against a Contributor.\n\n10. Versions of the License\n---------------------------\n\n10.1. New Versions\n\nMozilla Foundation is the license steward. Except as provided in Section\n10.3, no one other than the license steward has the right to modify or\npublish new versions of this License. Each version will be given a\ndistinguishing version number.\n\n10.2. Effect of New Versions\n\nYou may distribute the Covered Software under the terms of the version\nof the License under which You originally received the Covered Software,\nor under the terms of any subsequent version published by the license\nsteward.\n\n10.3. Modified Versions\n\nIf you create software not governed by this License, and you want to\ncreate a new license for such software, you may create and use a\nmodified version of this License if you rename the license and remove\nany references to the name of the license steward (except to note that\nsuch modified license differs from this License).\n\n10.4. Distributing Source Code Form that is Incompatible With Secondary\nLicenses\n\nIf You choose to distribute Source Code Form that is Incompatible With\nSecondary Licenses under the terms of this version of the License, the\nnotice described in Exhibit B of this License must be attached.\n\nExhibit A - Source Code Form License Notice\n-------------------------------------------\n\n  This Source Code Form is subject to the terms of the Mozilla Public\n  License, v. 2.0. If a copy of the MPL was not distributed with this\n  file, You can obtain one at http://mozilla.org/MPL/2.0/.\n\nIf it is not possible or desirable to put the notice in a particular\nfile, then You may include the notice in a location (such as a LICENSE\nfile in a relevant directory) where a recipient would be likely to look\nfor such a notice.\n\nYou may add additional accurate notices of copyright ownership.\n\nExhibit B - \"Incompatible With Secondary Licenses\" Notice\n---------------------------------------------------------\n\n  This Source Code Form is \"Incompatible With Secondary Licenses\", as\n  defined by the Mozilla Public License, v. 2.0.\n"
  },
  {
    "path": "packages/gui/README.md",
    "content": "# dev-sidecar-gui\n\n## Project setup\n\n```\nyarn install\n```\n\n### Compiles and hot-reloads for development\n\n```\nyarn serve\n```\n\n### Compiles and minifies for production\n\n```\nyarn build\n```\n\n### Lints and fixes files\n\n```\nyarn lint\n```\n\n### Customize configuration\n\nSee [Configuration Reference](https://cli.vuejs.org/config/).\n"
  },
  {
    "path": "packages/gui/babel.config.js",
    "content": "module.exports = {\n  presets: [\n    '@vue/babel-preset-jsx',\n  ],\n}\n"
  },
  {
    "path": "packages/gui/extra/pac/pac.txt",
    "content": "[AutoProxy 0.2.9]\n! Checksum: BZUefB22itmhAjqqdpvRkA\n! Expires: 6h\n! Title: GFWList4LL\n! GFWList with EVERYTHING included\n! Last Modified: Sun, 12 Jan 2025 11:56:36 -0500\n!\n! HomePage: https://github.com/gfwlist/gfwlist\n! License: https://www.gnu.org/licenses/old-licenses/lgpl-2.1.txt\n!\n! GFWList is unlikely to fully comprise the real\n! rules being deployed inside GFW system. We try\n! our best to keep the list up to date. Please\n! contact us regarding URL submission / removal,\n! or suggestion / enhancement at issue tracker:\n! https://github.com/gfwlist/gfwlist/issues/.\n\n!---------403/451/503/520 & URL Redirects---------\n!--ehentai\n|http://85.17.73.31/\n!--||adorama.com\n||afreecatv.com\n||agnesb.fr\n||akiba-web.com\n||altrec.com\n||angela-merkel.de\n||angola.org\n||anthropic.com\n||apartmentratings.com\n||apartments.com\n||arena.taipei\n||asianspiss.com\n||assimp.org\n||athenaeizou.com\n||azubu.tv\n||bankmobilevibe.com\n||banorte.com\n||beeg.com\n||global.bing.com\n||booktopia.com.au\n||boysmaster.com\n||bynet.co.il\n||byrut.org\n||carfax.com\n.casinobellini.com\n||casinobellini.com\n||centauro.com.br\n||chobit.cc\n||ciciai.com\n||claude.ai\n||clearsurance.com\n||cnbeta.com.tw\n||images.comico.tw\n||static.comico.tw\n||counter.social\n||costco.com\n||coze.com\n||crossfire.co.kr\n||crunchyroll.com\n||d2pass.com\n||darpa.mil\n||dawangidc.com\n||deezer.com\n||desipro.de\n||dingchin.com.tw\n||discord.com\n||discord.gg\n||discordapp.com\n||discordapp.net\n||dish.com\n|http://img.dlsite.jp/\n||dm530.net\nshare.dmhy.org\n||dmhy.org\n||dmm.co.jp\n|http://www.dmm.com/netgame\n||dnvod.tv\n||dubox.com\n||dvdpac.com\n||eesti.ee\n||esurance.com\n.expekt.com\n||expekt.com\n.extmatrix.com\n||extmatrix.com\n||fakku.net\n||fastpic.ru\n||filesor.com\n||financetwitter.com\n||flipboard.com\n||flitto.com\n||fnac.be\n||fnac.com\n||funkyimg.com\n||fxnetworks.com\n||g-area.org\n||gettyimages.com\n||getuploader.com\n||ghidra-sre.org\n!--|https://github.com/programthink/zhao\n!--|https://raw.githubusercontent.com/programthink/zhao\n||glass8.eu\n||glype.com\n||go141.com\n||guo.media\n||hautelook.com\n||hautelookcdn.com\n||wego.here.com\n||gamer-cds.cdn.hinet.net\n||gamer2-cds.cdn.hinet.net\n||hmoegirl.com\n||hmvdigital.ca\n||hmvdigital.com\n||homedepot.com\n||hoovers.com\n||hulu.com\n||huluim.com\n|http://secure.hustler.com\n|http://hustlercash.com\n|http://www.hustlercash.com\n||hybrid-analysis.com\n||cdn*.i-scmp.com\n||ilbe.com\n||ilovelongtoes.com\n|http://imgmega.com/*.gif.html\n|http://imgmega.com/*.jpg.html\n|http://imgmega.com/*.jpeg.html\n|http://imgmega.com/*.png.html\n||imlive.com\n||tw.iqiyi.com\n||javhub.net\n||javhuge.com\n.javlibrary.com\n||javlibrary.com\n||jcpenney.com\n||jims.net\n||tv.jtbc.joins.com\n||jukujo-club.com\n||juliepost.com\n||kawaiikawaii.jp\n||kendatire.com\n||khatrimaza.org\n||kkbox.com\n||leisurepro.com\n||lifemiles.com\n||longtoes.com\n||lovetvshow.com\n|http://www.m-sport.co.uk\n||macgamestore.com\n||madonna-av.com\n||mandiant.com\n||mangafox.com\n||mangafox.me\n||manta.com\n||matome-plus.com\n||matome-plus.net\n||mattwilcox.net\n||metarthunter.com\n||mfxmedia.com\n||miraheze.org\n||mojim.com\n||kb.monitorware.com\n||monster.com\n||moodyz.com\n||moonbingo.com\n||mos.ru\n||msha.gov\n||muzu.tv\n||mvg.jp\n.mybet.com\n||mybet.com\n||mypikpak.com\n||nationwide.com\n|http://www.nbc.com/live\n||neo-miracle.com\n||netflix.com\n||netflix.net\n||nflximg.com\n||nflximg.net\n||nflxext.com\n||nflxso.net\n||nflxvideo.net\n||nic.gov\n|http://mo.nightlife141.com\n||purpose.nike.com\n||noxinfluencer.com\n@@||cn.noxinfluencer.com\n||nordstrom.com\n||nordstromimage.com\n||nordstromrack.com\n||nottinghampost.com\n||npsboost.com\n||ntdtv.cz\n||s1.nudezz.com\n||nusatrip.com\n||nuuvem.com\n||olehdtv.com\n||omni7.jp\n||onapp.com\n!--We are confused as well\n||ontrac.com\n@@|http://blog.ontrac.com\n||openai.com\n||pandora.com\n.pandora.tv\n||parkansky.com\n||phmsociety.org\n|http://*.pimg.tw/\n||podcast.co\n||pure18.com\n||pytorch.org\n||qq.co.za\n||r18.com\n|http://radiko.jp\n||ramcity.com.au\n||rateyourmusic.com\n||rd.com\n||rdio.com\n|https://riseup.net\n||sadistic-v.com\n||isc.sans.edu\n|http://cdn*.search.xxx/\n||shiksha.com\n||slacker.com\n||sm-miracle.com\n||softnology.biz\n||soylentnews.org\n||spotify.com\n||spreadshirt.es\n||springboardplatform.com\n||sprite.org\n@@|http://store.sprite.org\n||superokayama.com\n||superpages.com\n||swagbucks.com\n||switch1.jp\n||tapanwap.com\n||gsp.target.com\n||login.target.com\n!--@@||intl.target.com\n||rcam.target.com\n||technews.tw\n||terabox.com\n||thinkgeek.com\n||thebodyshop-usa.com\n||tma.co.jp\n||tracfone.com\n||tryheart.jp\n||turntable.fm\n||twerkingbutt.com\n||ulop.net\n||uukanshu.com\n||vegasred.com\n||vevo.com\n||vip-enterprise.com\n|http://viu.tv/ch/\n|http://viu.tv/encore/\n||vmpsoft.com\n|http://ecsm.vs.com/\n||wanz-factory.com\n||ssl.webpack.de\n||wheretowatch.com\n||wingamestore.com\n||wizcrafts.net\n||wowhead.com\n||vod.wwe.com\n||xfinity.com\n||xiaomi.eu\n||youwin.com\n||ytn.co.kr\n||zamimg.com\n||zattoo.com\n||zim.vn\n||zozotown.com\n\n!##############General List Start###############\n!-------------------Pure IP---------------------\n14.102.250.18\n14.102.250.19\n50.7.31.230:8898\n174.142.105.153\n69.65.19.160\n\n!----------------------IDN----------------------\n||xn--4gq171p.com\n||xn--czq75pvv1aj5c.org\n||xn--i2ru8q2qg.com\n||xn--oiq.cc\n||xn--p8j9a0d9c9a.xn--q9jyb4c\n||xn--9pr62r24a.com\n\n!-----------------DNS Poisoning-----------------\n!---Amazon---\n!-||cdn-images.mailchimp.com\n||abebooks.com\n|https://*.s3.amazonaws.com\n||s3-ap-southeast-2.amazonaws.com\n\n||43110.cf\n||9cache.com\n||9gag.com\n||agro.hk\n||share.america.gov\n||apkmirror.com\n||arte.tv\n||artstation.com\n||bangdream.space\n||behance.net\n||bird.so\n||bitterwinter.org\n||bnn.co\n||businessinsider.com\n||boomssr.com\n||bwgyhw.com\n||castbox.fm\n||chinatimes.com\n||clyp.it\n||cmcn.org\n||cmx.im\n||dailyview.tw\n||daum.net\n||depositphotos.com\n||disconnect.me\n||documentingreality.com\n||doubibackup.com\n||doubmirror.cf\n||encyclopedia.com\n||fangeqiang.com\n||fanqiangdang.com\n||feedly.com\n||feedx.net\n||flyzy2005.com\n||foreignpolicy.com\n||free-ss.site\n||freehongkong.org\n||blog.fuckgfw233.org\n||g0v.social\n||globalvoices.org\n||glorystar.me\n||goregrish.com\n||guangnianvpn.com\n||hanime.tv\n||hbo.com\n||spaces.hightail.com\n||hkgalden.com\n||hkgolden.com\n||hudson.org\n||ipfs.io\n||japantimes.co.jp\n||jiji.com\n||jintian.net\n||jinx.com\n||joinmastodon.org\n||liangzhichuanmei.com\n||lighti.me\n||lightyearvpn.com\n||lihkg.com\n||line-scdn.net\n||i.lithium.com\n||cloud.mail.ru\n||cdn-images.mailchimp.com\n||mastodon.cloud\n||mastodon.host\n||mastodon.social\n||mastodon.xyz\n||matters.news\n||me.me\n||metart.com\n||mohu.club\n||mohu.ml\n||motiyun.com\n||msa-it.org\n||goo.ne.jp\n||go.nesnode.com\n||international-news.newsmagazine.asia\n||nikkei.com\n||nitter.cc\n||nitter.net\n||niu.moe\n||nofile.io\n||now.com\n||openvpn.org\n||onejav.com\n||paste.ee\n||my.pcloud.com\n||picacomic.com\n||pincong.rocks\n||pixiv.net\n||potato.im\n||premproxy.com\n||prism-break.org\n||proton.me\n||protonvpn.com\n||api.pureapk.com\n||quora.com\n||quoracdn.net\n||qz.com\n||cdn.seatguru.com\n||secure.raxcdn.com\n||redd.it\n||reddit.com\n.redditlist.com\n|http://redditlist.com\n||redditmedia.com\n||redditstatic.com\n!--defunct\n||rixcloud.com\n||rixcloud.us\n||rsdlmonitor.com\n||shadowsocks.be\n||shadowsocks9.com\n||tn1.shemalez.com\n||tn2.shemalez.com\n||tn3.shemalez.com\n||static.shemalez.com\n||six-degrees.io\n||softfamous.com\n||softsmirror.cf\n||sosreader.com\n||sspanel.net\n||sulian.me\n||supchina.com\n||teddysun.com\n||textnow.me\n||tineye.com\n||top10vpn.com\n||tubepornclassic.com\n||uku.im\n||unseen.is\n||cn.uptodown.com\n||uraban.me\n||vrsmash.com\n||vultryhw.com\n||scache.vzw.com\n||scache1.vzw.com\n||scache2.vzw.com\n||ss7.vzw.com\n||ssr.tools\n||steemit.com\n||taiwanjustice.net\n||tinc-vpn.org\n||u15.info\n||washingtonpost.com\n||wenzhao.ca\n||whatsonweibo.com\n||wire.com\n||blog.workflow.is\n||xm.com\n||xuehua.us\n||yes-news.com\n||yigeni.com\n||you-get.org\n||zzcloud.me\n\n!---Digital Currency Exchange(CRYPTO)---\n||aex.com\n||allcoin.com\n||adcex.com\n||bcex.ca\n||bibox.com\n||big.one\n||bigone.com\n||binance.com\n||bit-z.com\n||bitz.ai\n||bitbay.net\n||bitcoinworld.com\n||bitfinex.com\n||bithumb.com\n||bitinka.com.ar\n||bitmex.com\n||bnbstatic.com\n||btc98.com\n||btcbank.bank\n||btctrade.im\n||bybit.com\n||c2cx.com\n||chaoex.com\n||cobinhood.com\n||coin2co.in\n||coinbene.com\n.coinegg.com\n||coinegg.com\n||coinex.com\n!--|https://www.coinexchange.io/\n||coingecko.com\n||coingi.com\n||coinmarketcap.com\n||coinrail.co.kr\n||cointiger.com\n||cointobe.com\n||coinut.com\n||discoins.com\n||dragonex.io\n||ebtcbank.com\n||etherdelta.com\n||ethermine.org\n||etherscan.io\n||exmo.com\n||exrates.me\n||exx.com\n||f2pool.com\n||fatbtc.com\n||ftx.com\n||gate.io\n||gatecoin.com\n||hbg.com\n||hitbtc.com\n||hotcoin.com\n||huobi.co\n||huobi.com\n||huobi.me\n!--||huobi.li\n||huobi.pro\n||huobi.sc\n||huobipro.com\n||bx.in.th\n||jex.com\n||kex.com\n||kraken.com\n||kspcoin.com\n||kucoin.com\n||lbank.info\n||liquiditytp.com\n||livecoin.net\n||localbitcoins.com\n||mercatox.com\n||oanda.com\n||obyte.org\n||oex.com\n||okex.com\n||okx.com\n||opensea.io\n||otcbtc.com\n||paxful.com\n||poolin.com\n||rightbtc.com\n||solv.finance\n||topbtc.com\n||tronscan.org\n||xbtce.com\n||yobit.net\n||zb.com\n\n!----------------Frauds & Scams-----------------\n!!---Content Farm(fake 500 error)---\n||read01.com\n||kknews.cc\n\nchina-mmm.jp.net\n.lsxszzg.com\n.china-mmm.net\n||china-mmm.net\nchina-mmm.sa.com\n\n!---------------------Groups--------------------\n!!---Afraid FreeDNS---\n.allowed.org\n.now.im\n\n!!---Amazon---\n||amazon.co.jp\n.amazon.com/Dalai-Lama\namazon.com/Prisoner-State-Secret-Journal-Premier\ns3-ap-northeast-1.amazonaws.com\n\n!!---AOL---\n||aolchannels.aol.com\nvideo.aol.ca/video-detail\nvideo.aol.co.uk/video-detail\nvideo.aol.com\n||video.aol.com\n||search.aol.com\nwww.aolnews.com\n\n!!---AvMoo---\n.avmo.pw\n!--|http://avmo.pw\n.avmoo.com\n|http://avmoo.com\n.avmoo.net\n|http://avmoo.net\n||avmoo.pw\n.javmoo.xyz\n|http://javmoo.xyz\n.javtag.com\n|http://javtag.com\n.javzoo.com\n|http://javzoo.com\n.tellme.pw\n\n!!---BBC---\n!--.bbc.co.uk/blogs\n!--.bbc.co.uk/chinese\n!--.bbc.co.uk/news/world-asia-china\n!--.bbc.co.uk/tv\n!--.bbc.co.uk/zhongwen\n!--.bbc.com/ukchina\n!--.bbc.com/zhongwen\n!--.bbc.com%2Fzhongwen\n!--news.bbc.co.uk/onthisday*newsid_2496000/2496277\n!--newsforums.bbc.co.uk\n.bbc.com\n||bbc.com\n.bbc.co.uk\n||bbc.co.uk\n||bbci.co.uk\n.bbcchinese.com\n||bbcchinese.com\n|http://bbc.in\n\n!!---Bloomberg---\n.bloomberg.cn\n||bloomberg.cn\n.bloomberg.com\n||bloomberg.com\nbloomberg.de\n||bloomberg.de\n||bloombergview.com\n.businessweek.com\n\n!!---ChangeIP---\n.1dumb.com\n.25u.com\n.2waky.com\n.3-a.net\n.4dq.com\n.4mydomain.com\n.4pu.com\n.acmetoy.com\n.almostmy.com\n.americanunfinished.com\n.authorizeddns.net\n.authorizeddns.org\n.authorizeddns.us\n.bigmoney.biz\n.changeip.name\n.changeip.net\n.changeip.org\n.cleansite.biz\n.cleansite.info\n.cleansite.us\n.compress.to\n.ddns.info\n.ddns.me.uk\n.ddns.mobi\n.ddns.ms\n.ddns.name\n.ddns.us\n.dhcp.biz\n.dns-dns.com\n.dns-stuff.com\n.dns04.com\n.dns05.com\n.dns1.us\n.dns2.us\n.dnset.com\n.dnsrd.com\n.dsmtp.com\n.dumb1.com\n.dynamic-dns.net\n.dynamicdns.biz\n.dynamicdns.co.uk\n.dynamicdns.me.uk\n.dynamicdns.org.uk\n.dyndns.pro\n.dynssl.com\n.edns.biz\n.epac.to\n.esmtp.biz\n.ezua.com\n.faqserv.com\n.fartit.com\n.freeddns.com\n.freetcp.com\n.freewww.biz\n.freewww.info\n.ftp1.biz\n.ftpserver.biz\n.gettrials.com\n.got-game.org\n.gr8domain.biz\n.gr8name.biz\n.https443.net\n.https443.org\n.ikwb.com\n.instanthq.com\n.iownyour.biz\n.iownyour.org\n.isasecret.com\n.itemdb.com\n.itsaol.com\n.jetos.com\n.jkub.com\n.jungleheart.com\n.justdied.com\n.lflink.com\n.lflinkup.com\n.lflinkup.net\n.lflinkup.org\n.longmusic.com\n.mefound.com\n.moneyhome.biz\n.mrbasic.com\n.mrbonus.com\n.mrface.com\n.mrslove.com\n.my03.com\n.mydad.info\n.myddns.com\n.myftp.info\n.myftp.name\n.mylftv.com\n.mymom.info\n.mynetav.net\n.mynetav.org\n.mynumber.org\n.mypicture.info\n.mypop3.net\n.mypop3.org\n.mysecondarydns.com\n.mywww.biz\n.myz.info\n.ninth.biz\n.ns01.biz\n.ns01.info\n.ns01.us\n.ns02.biz\n.ns02.info\n.ns02.us\n.ns1.name\n.ns2.name\n.ns3.name\n.ocry.com\n.onedumb.com\n.onmypc.biz\n.onmypc.info\n.onmypc.net\n.onmypc.org\n.onmypc.us\n.organiccrap.com\n.otzo.com\n.ourhobby.com\n.pcanywhere.net\n.port25.biz\n.proxydns.com\n.qhigh.com\n.qpoe.com\n.rebatesrule.net\n.sellclassics.com\n.sendsmtp.com\n.serveuser.com\n.serveusers.com\n.sexidude.com\n.sexxxy.biz\n.sixth.biz\n.squirly.info\n.ssl443.org\n.toh.info\n.toythieves.com\n.trickip.net\n.trickip.org\n.vizvaz.com\n.wha.la\n.wikaba.com\n.www1.biz\n.wwwhost.biz\n@@|http://xx.wwwhost.biz\n.x24hr.com\n.xxuz.com\n.xxxy.biz\n.xxxy.info\n.ygto.com\n.youdontcare.com\n.yourtrap.com\n.zyns.com\n.zzux.com\n\n!!--Cloudflare--\n!--||pages.dev\n\n!!---CloudFront---\nd1b183sg0nvnuh.cloudfront.net\n|https://d1b183sg0nvnuh.cloudfront.net\nd1c37gjwa26taa.cloudfront.net\n|https://d1c37gjwa26taa.cloudfront.net\nd3c33hcgiwev3.cloudfront.net\n|https://d3c33hcgiwev3.cloudfront.net\n||d3rhr7kgmtrq1v.cloudfront.net\n\n!!---DtDNS---\n!###https://www.dtdns.com/dtsite/faq\n.3d-game.com\n.4irc.com\n.b0ne.com\n.chatnook.com\n.darktech.org\n.deaftone.com\n.dtdns.net\n.effers.com\n.etowns.net\n.etowns.org\n.flnet.org\n.gotgeeks.com\n.scieron.com\n.slyip.com\n.slyip.net\n.suroot.com\n\n!!---DynDNS---\n!###https://help.dyn.com/list-of-dyn-dns-pro-remote-access-domain-names/\n.blogdns.org\n.dyndns.org\n.dyndns-ip.com\n.dyndns-pics.com\n.from-sd.com\n.from-pr.com\n.is-a-hunter.com\n\n!!---Dynu---\n.dynu.com\n||dynu.com\n.dynu.net\n.freeddns.org\n\n!!---Facebook---\n||accountkit.com\ncdninstagram.com\n||cdninstagram.com\n||f8.com\n||facebook.br\n.facebook.com\n||facebook.com\n!--/^https?:\\/\\/[^\\/]+facebook\\.com/\n@@||v6.facebook.com\n||facebook.de\n||facebook.design\n||connect.facebook.net\n||facebook.hu\n||facebook.in\n||facebook.nl\n||facebook.se\n||facebookmail.com\n||fb.com\n||fb.me\n||fb.watch\n||fbcdn.net\n||fbsbx.com\n||fbaddins.com\n||fbworkmail.com\n.instagram.com\n||instagram.com\n||m.me\n||messenger.com\n||meta.com\n||oculus.com\n||oculuscdn.com\n||rocksdb.org\n@@||ip6.static.sl-reverse.com\n||parse.com\n||thefacebook.com\n||threads.net\n||whatsapp.com\n||whatsapp.net\n\n!!---Fandom---\n||auntology.fandom.com\n||hongkong.fandom.com\n\n!!---FTChinese---\n.ftchinese.com\n||ftchinese.com\n!--.ftchinese.com/channel/video\n!--.ftchinese.com/premium/001081066\n!--.ftchinese.com/story/00102753\n!--.ftchinese.com/story/001026616\n!--.ftchinese.com/story/001026749\n!--.ftchinese.com/story/001026807\n!--.ftchinese.com/story/001026808\n!--.ftchinese.com/story/001026834\n!--.ftchinese.com/story/001026880\n!--.ftchinese.com/story/001027429\n!--.ftchinese.com/story/001030341\n!--.ftchinese.com/story/001030502\n!--.ftchinese.com/story/001030803\n!--.ftchinese.com/story/001031317\n!--.ftchinese.com/story/001032617\n!--.ftchinese.com/story/001032636\n!--.ftchinese.com/story/001032692\n!--.ftchinese.com/story/001032762\n!--.ftchinese.com/story/001033138\n!--.ftchinese.com/story/001034917\n!--.ftchinese.com/story/001034926\n!--.ftchinese.com/story/001034927\n!--.ftchinese.com/story/001034928\n!--.ftchinese.com/story/001034952\n!--.ftchinese.com/story/001035890\n!--.ftchinese.com/story/001035972\n!--.ftchinese.com/story/001035993\n!--.ftchinese.com/story/001036417\n!--.ftchinese.com/story/001037090\n!--.ftchinese.com/story/001037091\n!--.ftchinese.com/story/001038178\n!--.ftchinese.com/story/001038199\n!--.ftchinese.com/story/001038220\n!--.ftchinese.com/story/001038819\n!--.ftchinese.com/story/001038862\n!--.ftchinese.com/story/001039067\n!--.ftchinese.com/story/001039178\n!--.ftchinese.com/story/001039211\n!--.ftchinese.com/story/001039271\n!--.ftchinese.com/story/001039295\n!--.ftchinese.com/story/001039369\n!--.ftchinese.com/story/001039482\n!--.ftchinese.com/story/001039534\n!--.ftchinese.com/story/001039555\n!--.ftchinese.com/story/001039576\n!--.ftchinese.com/story/001039712\n!--.ftchinese.com/story/001039779\n!--.ftchinese.com/story/001039809\n!--.ftchinese.com/story/001040134\n!--.ftchinese.com/story/001040835\n!--.ftchinese.com/story/001040890\n!--.ftchinese.com/story/001040918\n!--.ftchinese.com/story/001040992\n!--.ftchinese.com/story/001041209\n!--.ftchinese.com/story/001042100\n!--.ftchinese.com/story/001042252\n!--.ftchinese.com/story/001042272\n!--.ftchinese.com/story/001042280\n!--.ftchinese.com/story/001043029\n!--.ftchinese.com/story/001043066\n!--.ftchinese.com/story/001043096\n!--.ftchinese.com/story/001043124\n!--.ftchinese.com/story/001043152\n!--.ftchinese.com/story/001043189\n!--.ftchinese.com/story/001043428\n!--.ftchinese.com/story/001043439\n!--.ftchinese.com/story/001043534\n!--.ftchinese.com/story/001043675\n!--.ftchinese.com/story/001043680\n!--.ftchinese.com/story/001043702\n!--.ftchinese.com/story/001043849\n!--.ftchinese.com/story/001044099\n!--.ftchinese.com/story/001044776\n!--.ftchinese.com/story/001044871\n!--.ftchinese.com/story/001044897\n!--.ftchinese.com/story/001045114\n!--.ftchinese.com/story/001045139\n!--.ftchinese.com/story/001045186\n!--.ftchinese.com/story/001045755\n!--.ftchinese.com/story/001046087\n!--.ftchinese.com/story/001046105\n!--.ftchinese.com/story/001046118\n!--.ftchinese.com/story/001046132\n!--.ftchinese.com/story/001046517\n!--.ftchinese.com/story/001046822\n!--.ftchinese.com/story/001046866\n!--.ftchinese.com/story/001046942\n!--.ftchinese.com/story/001047180\n!--.ftchinese.com/story/001047206\n!--.ftchinese.com/story/001047304\n!--.ftchinese.com/story/001047317\n!--.ftchinese.com/story/001047345\n!--.ftchinese.com/story/001047358\n!--.ftchinese.com/story/001047375\n!--.ftchinese.com/story/001047381\n!--.ftchinese.com/story/001047413\n!--.ftchinese.com/story/001047456\n!--.ftchinese.com/story/001047491\n!--.ftchinese.com/story/001047545\n!--.ftchinese.com/story/001047558\n!--.ftchinese.com/story/001047568\n!--.ftchinese.com/story/001047627\n!--.ftchinese.com/story/001048293\n!--.ftchinese.com/story/001048343\n!--.ftchinese.com/story/001048710\n!--.ftchinese.com/story/001049289\n!--.ftchinese.com/story/001049360\n!--.ftchinese.com/story/001049896\n!--.ftchinese.com/story/001050152\n!--.ftchinese.com/story/001051027\n!--.ftchinese.com/story/001051161\n!--.ftchinese.com/story/001051372\n!--.ftchinese.com/story/001051479\n!--.ftchinese.com/story/001052138\n!--.ftchinese.com/story/001052161\n!--.ftchinese.com/story/001052525\n!--.ftchinese.com/story/001052549\n!--.ftchinese.com/story/001052701\n!--.ftchinese.com/story/001052965\n!--.ftchinese.com/story/001053149\n!--.ftchinese.com/story/001053150\n!--.ftchinese.com/story/001053200\n!--.ftchinese.com/story/001053425\n!--.ftchinese.com/story/001053496\n!--.ftchinese.com/story/001053526\n!--.ftchinese.com/story/001053557\n!--.ftchinese.com/story/001053906\n!--.ftchinese.com/story/001054049\n!--.ftchinese.com/story/001054103\n!--.ftchinese.com/story/001054109\n!--.ftchinese.com/story/001054119\n!--.ftchinese.com/story/001054123\n!--.ftchinese.com/story/001054139\n!--.ftchinese.com/story/001054166\n!--.ftchinese.com/story/001054168\n!--.ftchinese.com/story/001054190\n!--.ftchinese.com/story/001054437\n!--.ftchinese.com/story/001054526\n!--.ftchinese.com/story/001054607\n!--.ftchinese.com/story/001054644\n!--.ftchinese.com/story/001054786\n!--.ftchinese.com/story/001054843\n!--.ftchinese.com/story/001054925\n!--.ftchinese.com/story/001054940\n!--.ftchinese.com/story/001055051\n!--.ftchinese.com/story/001055063\n!--.ftchinese.com/story/001055069\n!--.ftchinese.com/story/001055136\n!--.ftchinese.com/story/001055170\n!--.ftchinese.com/story/001055202\n!--.ftchinese.com/story/001055242\n!--.ftchinese.com/story/001055263\n!--.ftchinese.com/story/001055274\n!--.ftchinese.com/story/001055299\n!--.ftchinese.com/story/001055480\n!--.ftchinese.com/story/001055551\n!--.ftchinese.com/story/001055559\n!--.ftchinese.com/story/001055566\n!--.ftchinese.com/story/001055840\n!--.ftchinese.com/story/001056099\n!--.ftchinese.com/story/001056108\n!--.ftchinese.com/story/001056131\n!--.ftchinese.com/story/001056375\n!--.ftchinese.com/story/001056491\n!--.ftchinese.com/story/001056529\n!--.ftchinese.com/story/001056534\n!--.ftchinese.com/story/001056538\n!--.ftchinese.com/story/001056541\n!--.ftchinese.com/story/001056554\n!--.ftchinese.com/story/001056557\n!--.ftchinese.com/story/001056560\n!--.ftchinese.com/story/001056567\n!--.ftchinese.com/story/001056574\n!--.ftchinese.com/story/001056588\n!--.ftchinese.com/story/001056594\n!--.ftchinese.com/story/001056596\n!--.ftchinese.com/story/001056684\n!--.ftchinese.com/story/001056832\n!--.ftchinese.com/story/001056833\n!--.ftchinese.com/story/001056851\n!--.ftchinese.com/story/001056874\n!--.ftchinese.com/story/001056896\n!--.ftchinese.com/story/001056927\n!--.ftchinese.com/story/001057011\n!--.ftchinese.com/story/001057018\n!--.ftchinese.com/story/001057044\n!--.ftchinese.com/story/001057162\n!--.ftchinese.com/story/001057500\n!--.ftchinese.com/story/001057504\n!--.ftchinese.com/story/001057509\n!--.ftchinese.com/story/001057518\n!--.ftchinese.com/story/001057532\n!--.ftchinese.com/story/001057533\n!--.ftchinese.com/story/001057556\n!--.ftchinese.com/story/001057580\n!--.ftchinese.com/story/001057638\n!--.ftchinese.com/story/001057644\n!--.ftchinese.com/story/001057817\n!--.ftchinese.com/story/001057875\n!--.ftchinese.com/story/001058009\n!--.ftchinese.com/story/001058056\n!--.ftchinese.com/story/001058224\n!--.ftchinese.com/story/001058257\n!--.ftchinese.com/story/001058295\n!--.ftchinese.com/story/001058328\n!--.ftchinese.com/story/001058339\n!--.ftchinese.com/story/001058344\n!--.ftchinese.com/story/001058352\n!--.ftchinese.com/story/001058413\n!--.ftchinese.com/story/001058421\n!--.ftchinese.com/story/001058440\n!--.ftchinese.com/story/001058458\n!--.ftchinese.com/story/001058468\n!--.ftchinese.com/story/001058561\n!--.ftchinese.com/story/001058566\n!--.ftchinese.com/story/001058567\n!--.ftchinese.com/story/001058585\n!--.ftchinese.com/story/001058628\n!--.ftchinese.com/story/001058656\n!--.ftchinese.com/story/001058665\n!--.ftchinese.com/story/001058678\n!--.ftchinese.com/story/001058691\n!--.ftchinese.com/story/001058721\n!--.ftchinese.com/story/001058728\n!--.ftchinese.com/story/001059464\n!--.ftchinese.com/story/001059484\n!--.ftchinese.com/story/001059537\n!--.ftchinese.com/story/001059538\n!--.ftchinese.com/story/001059551\n!--.ftchinese.com/story/001059818\n!--.ftchinese.com/story/001059914\n!--.ftchinese.com/story/001059920\n!--.ftchinese.com/story/001059957\n!--.ftchinese.com/story/001060088\n!--.ftchinese.com/story/001060156\n!--.ftchinese.com/story/001060157\n!--.ftchinese.com/story/001060160\n!--.ftchinese.com/story/001060181\n!--.ftchinese.com/story/001060185\n!--.ftchinese.com/story/001060493\n!--.ftchinese.com/story/001060495\n!--.ftchinese.com/story/001060590\n!--.ftchinese.com/story/001060846\n!--.ftchinese.com/story/001060847\n!--.ftchinese.com/story/001060875\n!--.ftchinese.com/story/001060921\n!--.ftchinese.com/story/001060946\n!--.ftchinese.com/story/001061120\n!--.ftchinese.com/story/001061474\n!--.ftchinese.com/story/001061524\n!--.ftchinese.com/story/001061642\n!--.ftchinese.com/story/001062017\n!--.ftchinese.com/story/001062020\n!--.ftchinese.com/story/001062028\n!--.ftchinese.com/story/001062092\n!--.ftchinese.com/story/001062096\n!--.ftchinese.com/story/001062147\n!--.ftchinese.com/story/001062176\n!--.ftchinese.com/story/001062188\n!--.ftchinese.com/story/001062254\n!--.ftchinese.com/story/001062374\n!--.ftchinese.com/story/001062482\n!--.ftchinese.com/story/001062496\n!--.ftchinese.com/story/001062501\n!--.ftchinese.com/story/001062508\n!--.ftchinese.com/story/001062519\n!--.ftchinese.com/story/001062554\n!--.ftchinese.com/story/001062741\n!--.ftchinese.com/story/001062794\n!--.ftchinese.com/story/001063160\n!--.ftchinese.com/story/001063359\n!--.ftchinese.com/story/001063512\n!--.ftchinese.com/story/001063668\n!--.ftchinese.com/story/001063692\n!--.ftchinese.com/story/001063763\n!--.ftchinese.com/story/001063764\n!--.ftchinese.com/story/001063826\n!--.ftchinese.com/story/001064127\n!--.ftchinese.com/story/001064312\n!--.ftchinese.com/story/001064705\n!--.ftchinese.com/story/001064807\n!--.ftchinese.com/story/001065120\n!--.ftchinese.com/story/001065168\n!--.ftchinese.com/story/001065249\n!--.ftchinese.com/story/001065287\n!--.ftchinese.com/story/001065335\n!--.ftchinese.com/story/001065337\n!--.ftchinese.com/story/001065541\n!--.ftchinese.com/story/001065715\n!--.ftchinese.com/story/001065735\n!--.ftchinese.com/story/001065756\n!--.ftchinese.com/story/001065802\n!--.ftchinese.com/story/001066112\n!--.ftchinese.com/story/001066136\n!--.ftchinese.com/story/001066140\n!--.ftchinese.com/story/001066465\n!--.ftchinese.com/story/001066881\n!--.ftchinese.com/story/001066950\n!--.ftchinese.com/story/001066959\n!--.ftchinese.com/story/001067435\n!--www.ftchinese.com/story/001067479\n!--.ftchinese.com/story/001067528\n!--.ftchinese.com/story/001067545\n!--.ftchinese.com/story/001067572\n!--.ftchinese.com/story/001067648\n!--.ftchinese.com/story/001067650\n!--.ftchinese.com/story/001067680\n!--.ftchinese.com/story/001067692\n!--.ftchinese.com/story/001067871\n!--.ftchinese.com/story/001067923\n!--.ftchinese.com/story/001068062\n!--.ftchinese.com/story/001068248\n!--.ftchinese.com/story/001068278\n!--.ftchinese.com/story/001068379\n!--.ftchinese.com/story/001068483\n!--.ftchinese.com/story/001068506\n!--.ftchinese.com/story/001068547\n!--.ftchinese.com/story/001068616\n!--.ftchinese.com/story/001068622\n!--.ftchinese.com/story/001068707\n!--.ftchinese.com/story/001069146\n!--.ftchinese.com/story/001069373\n!--.ftchinese.com/story/001069516\n!--.ftchinese.com/story/001069517\n!--.ftchinese.com/story/001069687\n!--.ftchinese.com/story/001069741\n!--.ftchinese.com/story/001069861\n!--.ftchinese.com/story/001069952\n!--.ftchinese.com/story/001070053\n!--.ftchinese.com/story/001070177\n!--.ftchinese.com/story/001070307\n!--.ftchinese.com/story/001070809\n!--.ftchinese.com/story/001070990\n!--.ftchinese.com/story/001071042\n!--.ftchinese.com/story/001071044\n!--.ftchinese.com/story/001071106\n!--.ftchinese.com/story/001071166\n!--.ftchinese.com/story/001071181\n!--ftchinese.com/story/001071200\n!--.ftchinese.com/story/001071208\n!--.ftchinese.com/story/001071238\n!--.ftchinese.com/story/001071683\n!--.ftchinese.com/story/001072271\n!--.ftchinese.com/story/001072348\n!--.ftchinese.com/story/001072677\n!--.ftchinese.com/story/001072726\n!--.ftchinese.com/story/001072794\n!--.ftchinese.com/story/001072853\n!--.ftchinese.com/story/001072895\n!--.ftchinese.com/story/001072993\n!--.ftchinese.com/story/001073043\n!--.ftchinese.com/story/001073103\n!--.ftchinese.com/story/001073157\n!--.ftchinese.com/story/001073216\n!--.ftchinese.com/story/001073246\n!--.ftchinese.com/story/001073305\n!--.ftchinese.com/story/001073307\n!--.ftchinese.com/story/001073408\n!--.ftchinese.com/story/001073537\n!--.ftchinese.com/story/001073672\n!--.ftchinese.com/story/001073849\n!--.ftchinese.com/story/001073906\n!--.ftchinese.com/story/001074089\n!--.ftchinese.com/story/001074110\n!--.ftchinese.com/story/001074128\n!--.ftchinese.com/story/001074157\n!--.ftchinese.com/story/001074246\n!--.ftchinese.com/story/001074307\n!--.ftchinese.com/story/001074347\n!--.ftchinese.com/story/001074423\n!--.ftchinese.com/story/001074454\n!--.ftchinese.com/story/001074467\n!--.ftchinese.com/story/001074493\n!--.ftchinese.com/story/001074550\n!--.ftchinese.com/story/001074562\n!--.ftchinese.com/story/001074653\n!--.ftchinese.com/story/001074693\n!--.ftchinese.com/story/001074699\n!--.ftchinese.com/story/001074712\n!--.ftchinese.com/story/001074713\n!--.ftchinese.com/story/001074768\n!--.ftchinese.com/story/001074782\n!--.ftchinese.com/story/001074794\n!--.ftchinese.com/story/001074822\n!--.ftchinese.com/story/001074874\n!--.ftchinese.com/story/001074891\n!--.ftchinese.com/story/001074918\n!--.ftchinese.com/story/001075081\n!--.ftchinese.com/story/001075134\n!--.ftchinese.com/story/001075142\n!--.ftchinese.com/story/001075216\n!--.ftchinese.com/story/001075230\n!--.ftchinese.com/story/001075238\n!--.ftchinese.com/story/001075262\n!--.ftchinese.com/story/001075269\n!--.ftchinese.com/story/001075491\n!--.ftchinese.com/story/001075500\n!--.ftchinese.com/story/001075650\n!--.ftchinese.com/story/001075678\n!--.ftchinese.com/story/001075703\n!--.ftchinese.com/story/001075739\n!--.ftchinese.com/story/001076066\n!--.ftchinese.com/story/001076142\n!--.ftchinese.com/story/001076459\n!--.ftchinese.com/story/001076470\n!--.ftchinese.com/story/001076538\n!--.ftchinese.com/story/001076573\n!--.ftchinese.com/story/001076901\n!--.ftchinese.com/story/001077067\n!--.ftchinese.com/story/001077084\n!--.ftchinese.com/story/001077235\n!--.ftchinese.com/story/001077344\n!--.ftchinese.com/story/001077390\n!--.ftchinese.com/story/001077392\n!--.ftchinese.com/story/001077465\n!--.ftchinese.com/story/001077468\n!--.ftchinese.com/story/001077492\n!--.ftchinese.com/story/001077745\n!--.ftchinese.com/story/001077768\n!--.ftchinese.com/story/001077804\n!--.ftchinese.com/story/001077852\n!--.ftchinese.com/story/001078646\n!--.ftchinese.com/story/001078928\n!--.ftchinese.com/story/001078967\n!--.ftchinese.com/story/001079559\n!--.ftchinese.com/story/001079641\n!--.ftchinese.com/story/001079909\n!--.ftchinese.com/story/001079934\n!--.ftchinese.com/story/001079992\n!--.ftchinese.com/story/001080054\n!--.ftchinese.com/story/001080109\n!--.ftchinese.com/story/001080169\n!--.ftchinese.com/story/001080226\n!--.ftchinese.com/story/001080429\n!--.ftchinese.com/story/001080471\n!--.ftchinese.com/story/001080550\n!--.ftchinese.com/story/001080581\n!--.ftchinese.com/story/001080647\n!--.ftchinese.com/story/001080778\n!--.ftchinese.com/story/001080892\n!--.ftchinese.com/story/001080915\n!--.ftchinese.com/story/001080935\n!--.ftchinese.com/story/001081059\n!--.ftchinese.com/story/001081127\n!--.ftchinese.com/tag/%E5%8D%81%E5%85%AB%E5%B1%8A%E4%B8%89%E4%B8%AD%E5%85%A8%E4%BC%9A\n!--.ftchinese.com/tag/%E6%B8%A9%E5%AE%B6%E5%AE%9D\n!--.ftchinese.com/tag/%E8%96%84%E7%86%99%E6%9D%A5\n!--.ftchinese.com/video/1437\n!--.ftchinese.com/video/1882\n!--.ftchinese.com/video/2446\n!--.ftchinese.com/video/2601\n!--.ftchinese.com/comments\n\n!!---Google---\n!###https://www.google.com/supported_domains###\n!...GFWList doesn't intend to support typosquatting...\n||1e100.net\n||466453.com\n||abc.xyz\n||about.google\n||admob.com\n||adsense.com\n||advertisercommunity.com\n||agoogleaday.com\n||ai.google\n||ampproject.org\n@@|https://www.ampproject.org\n@@|https://cdn.ampproject.org\n||android.com\n||androidify.com\n||androidtv.com\n||api.ai\n.appspot.com\n||appspot.com\n||autodraw.com\n||blog.google\n||blogblog.com\nblogspot.com\n/^https?:\\/\\/[^\\/]+blogspot\\.(.*)/\n.blogspot.hk\n.blogspot.jp\n.blogspot.tw\n||business.page\n!--||capitalg.com\n||certificate-transparency.org\n||chrome.com\n||chromecast.com\n||chromeenterprise.google\n||chromeexperiments.com\n||chromercise.com\n||chromestatus.com\n||chromium.org\n||cloudfunctions.net\n||com.google\n||crbug.com\n||creativelab5.com\n||crisisresponse.google\n||crrev.com\n||data-vocabulary.org\n||debug.com\n||deepmind.com\n||deja.com\n||design.google\n||digisfera.com\n||dns.google\n||hub.docker.com\n||docs.new\n||domains.google\n||duck.com\n||environment.google\n||feedburner.com\n||firebaseio.com\n||g.co\n||gcr.io\n||get.app\n||get.dev\n||get.how\n||get.page\n||getmdl.io\n||getoutline.org\n||ggpht.com\n||gmail.com\n||gmodules.com\n||godoc.org\n||golang.org\n||goo.gl\n||goo.gle\n.google.ae\n.google.as\n.google.am\n.google.at\n.google.az\n.google.ba\n.google.be\n.google.bg\n.google.ca\n.google.cd\n.google.ci\n.google.co.id\n.google.co.jp\n.google.co.kr\n.google.co.ma\n.google.co.uk\n.google.com\n.google.de\n||google.dev\n.google.dj\n.google.dk\n.google.es\n.google.fi\n.google.fm\n.google.fr\n.google.gg\n.google.gl\n.google.gr\n.google.ie\n.google.is\n.google.it\n.google.jo\n.google.kz\n.google.lv\n.google.mn\n.google.ms\n.google.nl\n.google.nu\n.google.no\n.google.ro\n.google.ru\n.google.rw\n.google.sc\n.google.sh\n.google.sk\n.google.sm\n.google.sn\n.google.tk\n.google.tm\n.google.to\n.google.tt\n.google.vu\n.google.ws\n/^https?:\\/\\/([^\\/]+\\.)*google\\.(ac|ad|ae|af|ai|al|am|as|at|az|ba|be|bf|bg|bi|bj|bs|bt|by|ca|cat|cd|cf|cg|ch|ci|cl|cm|co.ao|co.bw|co.ck|co.cr|co.id|co.il|co.in|co.jp|co.ke|co.kr|co.ls|co.ma|com|com.af|com.ag|com.ai|com.ar|com.au|com.bd|com.bh|com.bn|com.bo|com.br|com.bz|com.co|com.cu|com.cy|com.do|com.ec|com.eg|com.et|com.fj|com.gh|com.gi|com.gt|com.hk|com.jm|com.kh|com.kw|com.lb|com.ly|com.mm|com.mt|com.mx|com.my|com.na|com.nf|com.ng|com.ni|com.np|com.om|com.pa|com.pe|com.pg|com.ph|com.pk|com.pr|com.py|com.qa|com.sa|com.sb|com.sg|com.sl|com.sv|com.tj|com.tr|com.tw|com.ua|com.uy|com.vc|com.vn|co.mz|co.nz|co.th|co.tz|co.ug|co.uk|co.uz|co.ve|co.vi|co.za|co.zm|co.zw|cv|cz|de|dj|dk|dm|dz|ee|es|eu|fi|fm|fr|ga|ge|gg|gl|gm|gp|gr|gy|hk|hn|hr|ht|hu|ie|im|iq|is|it|it.ao|je|jo|kg|ki|kz|la|li|lk|lt|lu|lv|md|me|mg|mk|ml|mn|ms|mu|mv|mw|mx|ne|nl|no|nr|nu|org|pl|pn|ps|pt|ro|rs|ru|rw|sc|se|sh|si|sk|sm|sn|so|sr|st|td|tg|tk|tl|tm|tn|to|tt|us|vg|vn|vu|ws)\\/.*/\n!--||google-analytics.com\n!--||googleadservices.com\n||googleapis.cn\n||googleapis.com\n||googleapps.com\n||googleartproject.com\n||googleblog.com\n||googlebot.com\n!--||googlecapital.com\n||googlechinawebmaster.com\n||googlecode.com\n||googlecommerce.com\n||googledomains.com\n||googlearth.com\n||googleearth.com\n||googledrive.com\n||googlefiber.net\n||googlegroups.com\n||googlehosted.com\n||googleideas.com\n||googleinsidesearch.com\n||googlelabs.com\n||googlemail.com\n||googlemashups.com\n||googlepagecreator.com\n||googleplay.com\n||googleplus.com\n||googlescholar.comUSA\n||googlesource.com\n!--||googlesyndication.com\n!--||googletagmanager.com\n!--||googletagservices.com\n||googleusercontent.com\n.googlevideo.com\n||googlevideo.com\n||googleweblight.com\n||googlezip.net\n||groups.google.cn\n||grow.google\n||gstatic.com\n!--||gv.com\n||gvt0.com\n||gvt1.com\n@@||redirector.gvt1.com\n||gvt3.com\n||gwtproject.org\n||html5rocks.com\n||iam.soy\n||igoogle.com\n||itasoftware.com\n||lers.google\n||like.com\n||madewithcode.com\n||material.io\n||nic.google\n||on2.com\n||opensource.google\n||panoramio.com\n||passwords.google\n||picasaweb.com\n||pki.goog\n||plus.codes\n||polymer-project.org\n||pride.google\n||questvisual.com\n||admin.recaptcha.net\n||api.recaptcha.net\n||api-secure.recaptcha.net\n||api-verify.recaptcha.net\n||redhotlabs.com\n||registry.google\n||research.google\n||safety.google\n||savethedate.foo\n||schema.org\n||shattered.io\n|http://sipml5.org/\n||sheets.new\n||slides.new\n||snapseed.com\n||stories.google\n||sustainability.google\n||synergyse.com\n||teachparentstech.org\n||tensorflow.org\n||tfhub.dev\n||thinkwithgoogle.com\n||tiltbrush.com\n||translate.goog\n||tv.google\n||urchin.com\n!--||www.google\n||waveprotocol.org\n||waymo.com\n||web.dev\n||webmproject.org\n||webpkgcache.com\n||webrtc.org\n||whatbrowser.org\n||whats.new\n||widevine.com\n||withgoogle.com\n||withyoutube.com\n||x.company\n||xn--ngstr-lra8j.com\n||youtu.be\n.youtube.com\n||youtube.com\n||youtube-nocookie.com\n||youtubeeducation.com\n||youtubegaming.com\n||youtubekids.com\n||yt.be\n||ytimg.com\n||zynamics.com\n\n!!---KickASS---\n!--OFFICIAL URL list at: https://kastatus.com\n\n!!---NaughtyAmerica---\n||naughtyamerica.com\n\n!!---NYTimes---\n!--||d1f1eryiqyjs0r.cloudfront.net\n!--||d3lar09xbwlsge.cloudfront.net\n!--||d3q1qj9jzsu8nw.cloudfront.net\n!--||dc8xl0ndzn2cb.cloudfront.net\n!--||a1.nyt.com\n!--||int.nyt.com\n!--||s1.nyt.com\nstatic01.nyt.com\n!--||static01.nyt.com\n!--||typeface.nyt.com\n||nyt.com\nnytchina.com\nnytcn.me\n||nytcn.me\n||nytco.com\n|http://nyti.ms/\n.nytimes.com\n||nytimes.com\n||nytimg.com\nuserapi.nytlog.com\ncn.nytstyle.com\n||nytstyle.com\n\n!!---Steam---\n.steamcommunity.com\n||steamcommunity.com\n!--steamcommunity.com/profiles/76561198062771609\n!--steamcommunity.com/groups/LibetTibet\n!--steamcommunity.com/groups/zhonggong\n!--steamcommunity.com/id/CJT_Jackton\n||store.steampowered.com\n\n!!---Telegram---\n!!!---Domain---\n||cdn-telegram.org\n||comments.app\n||graph.org\n||quiz.directory\n||t.me\n||updates.tdesktop.com\n||telegram.dog\n||telegram.me\n||telegram.org\n||telegram.space\n||telegram-cdn.org\n||telegramdownload.com\n||telegra.ph\n||telesco.pe\n!!!---IP---\n\n!!---Tiktok---\n||tiktok.com\n||tiktokv.com\n||tiktokv.us\n||tiktokcdn-us.com\n\n!!---Twitch---\n||jtvnw.net\n||ttvnw.net\n||twitch.tv\n||twitchcdn.net\n\n!!---Twitter/X---\n||periscope.tv\n.pscp.tv\n||pscp.tv\n.t.co\n||t.co\n.tweetdeck.com\n||tweetdeck.com\n||twimg.com\n.twitpic.com\n||twitpic.com\n.twitter.com\n||twitter.com\n||twitter.jp\n||vine.co\n||x.com\n\n!!---Taiwan---\n||gov.taipei\n.gov.tw\n|https://aiss.anws.gov.tw\n||archives.gov.tw\n||tacc.cwb.gov.tw\n||data.gov.tw\n||epa.gov.tw\n||fa.gov.tw\n||fda.gov.tw\n||hpa.gov.tw\n||immigration.gov.tw\n||itaiwan.gov.tw\n||li.taipei\n||mjib.gov.tw\n||moeaic.gov.tw\n||mofa.gov.tw\n||mol.gov.tw\n||mvdis.gov.tw\n||nat.gov.tw\n||nhi.gov.tw\n||npa.gov.tw\n||nsc.gov.tw\n||ntbk.gov.tw\n||ntbna.gov.tw\n||ntbt.gov.tw\n||ntsna.gov.tw\n||pcc.gov.tw\n||stat.gov.tw\n||taipei.gov.tw\n||taiwanjobs.gov.tw\n||thb.gov.tw\n||tipo.gov.tw\n||wda.gov.tw\n\n||teco-hk.org\n||teco-mo.org\n\n@@||aftygh.gov.tw\n@@||aide.gov.tw\n@@||tpde.aide.gov.tw\n@@||arte.gov.tw\n@@||chukuang.gov.tw\n@@||cwb.gov.tw\n@@||cycab.gov.tw\n@@||dbnsa.gov.tw\n@@||df.gov.tw\n@@||eastcoast-nsa.gov.tw\n@@||erv-nsa.gov.tw\n@@||grb.gov.tw\n@@||gysd.nyc.gov.tw\n@@||hchcc.gov.tw\n@@||hsinchu-cc.gov.tw\n@@||iner.gov.tw\n@@||klsio.gov.tw\n@@||kmseh.gov.tw\n@@||lungtanhr.gov.tw\n@@||maolin-nsa.gov.tw\n@@||matsu-news.gov.tw\n@@||matsu-nsa.gov.tw\n@@||matsucc.gov.tw\n@@||moe.gov.tw\n@@||nankan.gov.tw\n@@||ncree.gov.tw\n@@||necoast-nsa.gov.tw\n@@||siraya-nsa.gov.tw\n@@||cromotc.nat.gov.tw\n@@||tax.nat.gov.tw\n@@||necoast-nsa.gov.tw\n@@||ner.gov.tw\n@@||nmmba.gov.tw\n@@||nmp.gov.tw\n@@||nmvttc.gov.tw\n@@||northguan-nsa.gov.tw\n||npm.gov.tw\n@@||nstm.gov.tw\n@@||ntdmh.gov.tw\n@@||ntl.gov.tw\n@@||ntsec.gov.tw\n@@||ntuh.gov.tw\n@@||nvri.gov.tw\n@@||penghu-nsa.gov.tw\n@@||post.gov.tw\n@@||siraya-nsa.gov.tw\n@@||stdtime.gov.tw\n@@||sunmoonlake.gov.tw\n@@||taitung-house.gov.tw\n@@||taoyuan.gov.tw\n@@||tphcc.gov.tw\n@@||trimt-nsa.gov.tw\n@@||vghtpe.gov.tw\n@@||vghks.gov.tw\n@@||vghtc.gov.tw\n@@||wanfang.gov.tw\n@@||yatsen.gov.tw\n@@||yda.gov.tw\n\n!--@@||4pppc.gov.tw\n!--@@||921.gov.tw\n!--@@||dmtip.gov.tw\n!--@@||etraining.gov.tw\n!--@@||gsn-cert.nat.gov.tw\n!--@@||nici.nat.gov.tw\n!--@@||hcc.gov.tw\n!--@@||hengchuen.gov.tw\n!--@@||khcc.gov.tw\n!--@@||khms.gov.tw\n!--@@||kk.gov.tw\n!--@@||klccab.gov.tw\n!--@@||klra.gov.tw\n!--@@||nmh.gov.tw\n!--@@||nmtl.gov.tw\n!--@@||pabp.gov.tw\n!--@@||pet.gov.tw\n!--@@||tchb.gov.tw\n!--@@||tcsac.gov.tw\n!--@@||tncsec.gov.tw\n||kinmen.org.tw\n\n!!---USA---\n|http://www.americorps.gov\n||jpl.nasa.gov\n||pds.nasa.gov\n||solarsystem.nasa.gov\niipdigital.usembassy.gov\n||usfk.mil\n||usmc.mil\n|http://tarr.uspto.gov/\n||tsdr.uspto.gov\n\n!!---V2EX---\n||v2ex.com\n!--.v2ex.com\n!--Included in above rule: dns.v2ex.com\n!--@@|http://v2ex.com\n!--@@|http://cdn.v2ex.com\n!--@@|http://cn.v2ex.com\n!--@@|http://hk.v2ex.com\n!--@@|http://i.v2ex.com\n!--@@|http://lax.v2ex.com\n!--@@|http://neue.v2ex.com\n!--@@|http://pagespeed.v2ex.com\n!--@@|http://static.v2ex.com\n!--@@|http://workspace.v2ex.com\n!--@@|http://www.v2ex.com\n\n!!---VOA---\ncn.voa.mobi\ntw.voa.mobi\n||voacambodia.com\n.voachineseblog.com\n||voachineseblog.com\n.voacantonese.com\n||voacantonese.com\nvoachinese.com\n||voachinese.com\nvoagd.com\n||voaindonesia.com\n.voanews.com\n||voanews.com\nvoatibetan.com\n||voatibetan.com\n.voatibetanenglish.com\n||voatibetanenglish.com\n\n!!---Wikia---\n||zh.ecdm.wikia.com\n||evchk.wikia.com\nfq.wikia.com\nzh.pttpedia.wikia.com/wiki/%E7%BF%92%E5%8C%85%E5%AD%90%E4%B9%8B%E4%BA%82\ncn.uncyclopedia.wikia.com\nzh.uncyclopedia.wikia.com\n\n!-------------Wikipedia Related-------------\n!!Emergency need only(IP/Port block usage)!!\n!------0------\n!--||mediawiki.org\n!--@@||m.mediawiki.org\n!------1------\n!--||wikidata.org\n!--@@||m.wikidata.org\n!------2------\n||wikimedia.org\n!--@@||lists.wikimedia.org\n!--@@||m.wikimedia.org\n!--@@||phabricator.wikimedia.org\n!--@@||upload.wikimedia.org\n!--@@||wikitech.wikimedia.org\n!------3------\n!--||wikibooks.org\n!--@@||m.wikibooks.org\n!------4------\n!--||wikiversity.org\n!--@@||m.wikiversity.org\n!------5------\n!--||wikisource.org\n!--@@||m.wikisource.org\n|http://zh.wikisource.org\n!------6------\n||zh.wikiquote.org\n!--@@||m.wikiquote.org\n!------7------\n!--||wikinews.org\n!--@@||m.wikinews.org\n||zh.wikinews.org\n!------8------\n!--||wikivoyage.org\n!--@@||m.wikivoyage.org\n!--|http://zh.wikivoyage.org\n!------9------\n!--||wiktionary.org\n!--@@||m.wiktionary.org\n!--|http://zh.wiktionary.org\n!-----10------\n!--||wikimediafoundation.org\n!--@@||m.wikimediafoundation.org\n!----Main-----\n!!--||en.wikipedia.org\n!--||wikipedia.org\n||ja.wikipedia.org\n!!--zh.wikipedia.org\n!--||zh.wikipedia.org\n!!--||ug.m.wikipedia.org\n!!--zh.m.wikipedia.org\n!!--|https://zh.m.wikipedia.org\n!--@@||m.wikipedia.org\n!!--|https://zh.wikipedia.org\n!--Other Languages of Wikipedia\n!!--wuu.wikipedia.org\n!!--|https://wuu.wikipedia.org\n!!--zh-yue.wikipedia.org\n!!--|https://zh-yue.wikipedia.org\n!!! Starting with !! are previous rules replaced by:\n||wikipedia.org\n\n!!---Yahoo---\n||data.flurry.com\n||page.bid.yahoo.com\n||tw.bid.yahoo.com\n||auctions.yahoo.co.jp\n||blogs.yahoo.co.jp\n||search.yahoo.co.jp\n||buy.yahoo.com.tw\n||hk.yahoo.com\n||hk.knowledge.yahoo.com\n||tw.money.yahoo.com\n||hk.myblog.yahoo.com\nnews.yahoo.com/china-blocks-bbc\n||hk.news.yahoo.com\nhk.rd.yahoo.com\nhk.search.yahoo.com/search\nhk.video.news.yahoo.com/video\nmeme.yahoo.com\n!--tw.yahoo.com\ntw.answers.yahoo.com\n|https://tw.answers.yahoo.com\n||tw.knowledge.yahoo.com\n||tw.mall.yahoo.com\ntw.yahoo.com\n||tw.mobi.yahoo.com\ntw.myblog.yahoo.com\n||tw.news.yahoo.com\npulse.yahoo.com\n||search.yahoo.com\nupcoming.yahoo.com\nvideo.yahoo.com\n||yahoo.com.hk\n||duckduckgo-owned-server.yahoo.net\n\n!------------------Numerics---------------------\n||000webhost.com\n.030buy.com\n.0rz.tw\n|http://0rz.tw\n1-apple.com.tw\n||1-apple.com.tw\n.10.tt\n.100ke.org\n.1000giri.net\n||1000giri.net\n||10beasts.net\n.10conditionsoflove.com\n||10musume.com\n123rf.com\n.12bet.com\n||12bet.com\n.12vpn.com\n.12vpn.net\n||12vpn.com\n||12vpn.net\n||1337x.to\n.138.com\n141hongkong.com/forum\n||141jj.com\n.141tube.com\n||1688.com.au\n.173ng.com\n||173ng.com\n.177pic.info\n.17t17p.com\n||18board.com\n||18board.info\n18onlygirls.com\n.18p2p.com\n.18virginsex.com\n.1949er.org\nzhao.1984.city\n||zhao.1984.city\n1984bbs.com\n||1984bbs.com\n!--||1984blog.com\n.1984bbs.org\n||1984bbs.org\n.1991way.com\n||1991way.com\n.1998cdp.org\n.1bao.org\n|http://1bao.org\n.1eew.com\n.1mobile.com\n|http://*.1mobile.tw\n||1point3acres.com\n||1pondo.tv\n.2-hand.info\n.2000fun.com/bbs\n||2008xianzhang.info\n||2017.hk\n||2021hkcharter.com\n||2047.name\n21andy.com/blog\n21sextury.com\n.228.net.tw\n||233abc.com\n||24hrs.ca\n24smile.org\n2lipstube.com\n.2shared.com\n30boxes.com\n.315lz.com\n||32red.com\n||36rain.com\n.3a5a.com\n3arabtv.com\n.3boys2girls.com\n.3proxy.ru\n.3ren.ca\n.3tui.net\n||404museum.com\n||4bluestones.biz\n.4chan.com\n!--||4chan.org\n.4everproxy.com\n||4everproxy.com\n||4rbtv.com\n||4shared.com\ntaiwannation.50webs.com\n||51.ca\n||51jav.org\n.51luoben.com\n||51luoben.com\n||5278.cc\n.5299.tv\n5aimiku.com\n5i01.com\n.5isotoi5.org\n.5maodang.com\n||63i.com\n.64museum.org\n64tianwang.com\n64wiki.com\n.66.ca\n666kb.com\n||6do.news\n.6park.com\n||6park.com\n||6parkbbs.com\n||6parker.com\n||6parknews.com\n||7capture.com\n.7cow.com\n!--||7-zip.org\n.8-d.com\n|http://8-d.com\n85cc.net\n.85cc.us\n|http://85cc.us\n|http://85st.com\n.881903.com/page/zh-tw/\n||881903.com\n.888.com\n.888poker.com\n89.64.charter.constitutionalism.solutions\n89-64.org\n||89-64.org\n||8964museum.com\n.8news.com.tw\n.8z1.net\n||8z1.net\n.9001700.com\n|http://908taiwan.org/\n||91porn.com\n||91porny.com\n||91vps.club\n.92ccav.com\n.991.com\n|http://991.com\n.99btgc01.com\n||99btgc01.com\n.99cn.info\n|http://99cn.info\n||9bis.com\n||9bis.net\n||9news.com.au\n\n!--------------------AA-------------------------\n.tibet.a.se\n|http://tibet.a.se\n||a-normal-day.com\na5.com.ru\n|http://aamacau.com\n!--|http://cdn*.abc.com/\n.abc.com\n.abc.net.au\n||abc.net.au\n.abchinese.com\nabclite.net\n|https://www.abclite.net\n.ablwang.com\n.aboluowang.com\n||aboluowang.com\n||about.me\n.aboutgfw.com\n.abs.edu\n||acast.com\n.accim.org\n.aceros-de-hispania.com\n.acevpn.com\n||acevpn.com\n.acg18.me\n|http://acg18.me\n||acgbox.org\n||acgkj.com\n||acgnx.se\n.acmedia365.com\n.acnw.com.au\nactfortibet.org\nactimes.com.au\nactivpn.com\n||activpn.com\n||aculo.us\n||addictedtocoffee.de\n||addyoutube.com\n.adelaidebbs.com/bbs\n.adpl.org.hk\n|http://adpl.org.hk\n.adult-sex-games.com\n||adult-sex-games.com\nadultfriendfinder.com\nadultkeep.net/peepshow/members/main.htm\n||advanscene.com\n||advertfan.com\n.ae.org\n||aei.org\n||aenhancers.com\n||af.mil\n.afantibbs.com\n|http://afantibbs.com\n||afr.com\n.ai-kan.net\n||ai-kan.net\nai-wen.net\n.aiph.net\n||aiph.net\n.airasia.com\n||airconsole.com\n|http://download.aircrack-ng.org\n.airvpn.org\n||airvpn.org\n.aisex.com\n||ait.org.tw\naiweiwei.com\n.aiweiweiblog.com\n||aiweiweiblog.com\n||www.ajsands.com\n\n!!---Akamai---\na248.e.akamai.net\n||a248.e.akamai.net\n\nrfalive1.akacast.akamaistream.net\nvoa-11.akacast.akamaistream.net\n\n!!--403\n||abematv.akamaized.net\n||linear-abematv.akamaized.net\n||vod-abematv.akamaized.net\n\n|https://fbcdn*.akamaihd.net/\n!--||fbexternal-a.akamaihd.net\n!--||fbstatic-a.akamaihd.net\n!--|https://igcdn*.akamaihd.net\nrthklive2-lh.akamaihd.net\n\n.akademiye.org/ug\n|http://akademiye.org/ug\n||akiba-online.com\n||akow.org\n.al-islam.com\n||al-qimmah.net\n||alabout.com\n.alanhou.com\n|http://alanhou.com\n.alarab.qa\n||alasbarricadas.org\nalexlur.org\n||alforattv.net\n.alhayat.com\n.alicejapan.co.jp\naliengu.com\n||alive.bar\n||alkasir.com\n||all4mom.org\n||allconnected.co\n.alldrawnsex.com\n||alldrawnsex.com\n.allervpn.com\n||allfinegirls.com\n.allgirlmassage.com\nallgirlsallowed.org\n.allgravure.com\nalliance.org.hk\n.allinfa.com\n||allinfa.com\n.alljackpotscasino.com\n||allmovie.com\n||almasdarnews.com\n.alphaporno.com\n||alternate-tools.com\nalternativeto.net/software\nalvinalexander.com\nalwaysdata.com\n||alwaysdata.com\n||alwaysdata.net\n.alwaysvpn.com\n||alwaysvpn.com\n||am730.com.hk\nameblo.jp\n||ameblo.jp\nwww1.american.edu/ted/ice/tibet\n||americangreencard.com\n||amiblockedornot.com\n.amigobbs.net\n.amitabhafoundation.us\n|http://amitabhafoundation.us\n.amnesty.org\n||amnesty.org\n||amnesty.org.hk\n.amnesty.tw\n.amnestyusa.org\n||amnestyusa.org\n.amnyemachen.org\n.amoiist.com\n.amtb-taipei.org\nandroidplus.co/apk\n.andygod.com\n|http://andygod.com\nannatam.com/chinese\n||anchor.fm\n||anchorfree.com\n!--GHS\n||ancsconf.org\n||andfaraway.net\n||android-x86.org\nangelfire.com/hi/hayashi\n||angularjs.org\nanimecrazy.net\naniscartujo.com\n||aniscartujo.com\n||anobii.com\n||anonfiles.com\n.anonymitynetwork.com\n.anonymizer.com\n.anonymouse.org\n||anonymouse.org\nanontext.com\n.anpopo.com\n.answering-islam.org\n|http://www.antd.org\n||anthonycalzadilla.com\n.anti1984.com\nantichristendom.com\n.antiwave.net\n|http://antiwave.net\n.anyporn.com\n.anysex.com\n|http://anysex.com\n.ao3.org\n||ao3.org\n||aobo.com.au\n.aofriend.com\n|http://aofriend.com\n.aofriend.com.au\n.aojiao.org\n||aomiwang.com\nvideo.ap.org\n||apat1989.org\n.apetube.com\n||apiary.io\n.apigee.com\n||apigee.com\n||apk.support\n||apk-dl.com\n||apkcombo.com\n.apkmonk.com/app\n||apkmonk.com\n||apkplz.com\n||apkpure.com\n||apkpure.net\n.aplusvpn.com\n!--||appannie.com\n||appbrain.com\n.appdownloader.net/Android\n.appledaily.com\n||appledaily.com\nappledaily.com.hk\n||appledaily.com.hk\nappledaily.com.tw\n||appledaily.com.tw\n.appshopper.com\n|http://appshopper.com\n||appsocks.net\n||appsto.re\n.aptoide.com\n||aptoide.com\n||archives.gov\n.archive.fo\n||archive.fo\n.archive.is\n||archive.is\n.archive.li\n||archive.li\n||archive.md\n||archive.org\n||archive.ph\narchive.today\n|https://archive.today\n||archiveofourown.com\n||archiveofourown.org\n.arctosia.com\n|http://arctosia.com\n||areca-backup.org\n.arethusa.su\n||arethusa.su\n||arlingtoncemetery.mil\n||army.mil\n.art4tibet1998.org\nartofpeacefoundation.org\nartsy.net\n||asacp.org\nasdfg.jp/dabr\nasg.to\n.asia-gaming.com\n.asiaharvest.org\n||asiaharvest.org\n||asianage.com\n||asianews.it\n|http://japanfirst.asianfreeforum.com/\n||asiansexdiary.com\n||asianwomensfilm.de\n||asiaone.com\n.asiatgp.com\n.asiatoday.us\n||askstudent.com\n.askynz.net\n||askynz.net\n||aspi.org.au\n||aspistrategist.org.au\n||assembla.com\n||astrill.com\n||atc.org.au\n.atchinese.com\n|http://atchinese.com\natgfw.org\n.atlaspost.com\n||atlaspost.com\n||atdmt.com\n.atlanta168.com\n||atlanta168.com\n.atnext.com\n||atnext.com\n||audacy.com\nice.audionow.com\n.av.com\n||av.movie\n.av-e-body.com\navaaz.org\n||avaaz.org\n!--||avast.com\n.avbody.tv\n.avcity.tv\n.avcool.com\n.avdb.in\n||avdb.in\n.avdb.tv\n||avdb.tv\n.avfantasy.com\n||avg.com\n.avgle.com\n||avgle.com\n||avidemux.org\n||avoision.com\n.avyahoo.com\n||axios.com\n||axureformac.com\n.azerbaycan.tv\nazerimix.com\n||azirevpn.com\n!--boxun.azurewebsites.net doesn't exist.\nboxun*.azurewebsites.net\n||boxun*.azurewebsites.net\n\n!--------------------BB-------------------------\n||b-ok.cc\nforum.baby-kingdom.com\n||babylonbee.com\nbabynet.com.hk\nbackchina.com\n||backchina.com\n.backpackers.com.tw/forum\nbacktotiananmen.com\n||bad.news\n.badiucao.com\n||badiucao.com\n.badjojo.com\nbadoo.com\n|http://*2.bahamut.com.tw\n||baidu.jp\n.baijie.org\n||baijie.org\n||bailandaily.com\n||baixing.me\n||baizhi.org\n||bakgeekhome.tk\n.banana-vpn.com\n||banana-vpn.com\n||band.us\n||bandcamp.com\n.bandwagonhost.com\n||bandwagonhost.com\n.bangbrosnetwork.com\n.bangchen.net\n|http://bangchen.net\n||bangkokpost.com\n||bangyoulater.com\nbannedbook.org\n||bannedbook.org\n.bannednews.org\n.baramangaonline.com\n|http://baramangaonline.com\n.barenakedislam.com\n||barnabu.co.uk\n||barton.de\n.bastillepost.com\n||bastillepost.com\nbayvoice.net\n||bayvoice.net\ndajusha.baywords.com\n||bbchat.tv\n||bb-chat.tv\n.bbg.gov\n.bbkz.com/forum\n.bbnradio.org\nbbs-tw.com\n.bbsdigest.com/thread\n||bbsfeed.com\nbbsland.com\n.bbsmo.com\n.bbsone.com\nbbtoystore.com\n.bcast.co.nz\n.bcc.com.tw/board\n.bcchinese.net\n.bcmorning.com\nbdsmvideos.net\n.beaconevents.com\n.bebo.com\n||bebo.com\n.beevpn.com\n||beevpn.com\n.behindkink.com\n||beijing1989.com\n||beijing2022.art\nbeijingspring.com\n||beijingspring.com\n.beijingzx.org\n|http://beijingzx.org\n.belamionline.com\n.bell.wiki\n|http://bell.wiki\nbemywife.cc\nberic.me\n||berlinerbericht.de\n.berlintwitterwall.com\n||berlintwitterwall.com\n.berm.co.nz\n.bestforchina.org\n||bestforchina.org\n.bestgore.com\n.bestpornstardb.com\n||bestvpn.com\n.bestvpnanalysis.com\n.bestvpnserver.com\n.bestvpnservice.com\n.bestvpnusa.com\n||bet365.com\n.betfair.com\n||betternet.co\n.bettervpn.com\n||bettervpn.com\n.bettween.com\n||bettween.com\n||betvictor.com\n.bewww.net\n.beyondfirewall.com\n||bfnn.org\n||bfsh.hk\n.bgvpn.com\n||bgvpn.com\n.bianlei.com\n@@||bianlei.com\nbiantailajiao.com\nbiantailajiao.in\n.biblesforamerica.org\n|http://biblesforamerica.org\n.bic2011.org\n||biedian.me\nbigfools.com\n||bigjapanesesex.com\n.bignews.org\n||bignews.org\n.bigsound.org\n||bild.de\n.biliworld.com\n|http://biliworld.com\n|http://billypan.com/wiki\n.binux.me\nai.binwang.me/couplet\n.bit.do\n|http://bit.do\n.bit.ly\n|http://bit.ly\n!--||bitbucket.org\n||bitchute.com\n||bitcointalk.org\n.bitshare.com\n||bitshare.com\nbitsnoop.com\n.bitvise.com\n||bitvise.com\nbizhat.com\n||bl-doujinsouko.com\n.bjnewlife.org\n.bjs.org\nbjzc.org\n||bjzc.org\n.blacklogic.com\n.blackvpn.com\n||blackvpn.com\nblewpass.com\ntor.blingblingsquad.net\n.blinkx.com\n||blinkx.com\nblinw.com\n.blip.tv\n||blip.tv/\n||blockcast.it\n.blockcn.com\n||blockcn.com\n||blockedbyhk.com\n||blockless.com\n||blog.de\n.blog.jp\n|http://blog.jp\n@@||jpush.cn\n.blogcatalog.com\n||blogcatalog.com\n||blogcity.me\n.blogger.com\n||blogger.com\nblogimg.jp\n||blog.kangye.org\n.bloglines.com\n||bloglines.com\n||bloglovin.com\nrconversation.blogs.com\nblogtd.net\n.blogtd.org\n|http://blogtd.org\n||bloodshed.net\n!--403\n||assets.bwbx.io\n\n||bloomfortune.com\nblueangellive.com\n||blubrry.com\n.bmfinn.com\n.bnews.co\n||bnews.co\n||bnext.com.tw\n||bnrmetal.com\nboardreader.com/thread\n||boardreader.com\n.bod.asia\n||bod.asia\n.bodog88.com\n.bolehvpn.net\n||bolehvpn.net\nbonbonme.com\n.bonbonsex.com\n.bonfoundation.org\n.bongacams.com\n||boobstagram.com\n||book.com.tw\n||bookdepository.com\nbookepub.com\n||books.com.tw\n||borgenmagazine.com\n||botanwang.com\n.bot.nu\n.bowenpress.com\n||bowenpress.com\n||app.box.com\ndl.box.net\n||dl.box.net\n.boxpn.com\n||boxpn.com\nboxun.com\n||boxun.com\n.boxun.tv\n||boxun.tv\nboxunblog.com\n||boxunblog.com\n.boxunclub.com\nboyangu.com\n.boyfriendtv.com\n.boysfood.com\n||br.st\n.brainyquote.com/quotes/authors/d/dalai_lama\n||brandonhutchinson.com\n||braumeister.org\n||brave.com\n.bravotube.net\n||bravotube.net\n.brazzers.com\n||brazzers.com\n||breached.to\n.break.com\n||break.com\nbreakgfw.com\n||breakgfw.com\nbreaking911.com\n.breakingtweets.com\n||breakingtweets.com\n||breakwall.net\nbriian.com/6511/freegate\n.briefdream.com/%E7%B4%A0%E6%A3%BA\n||brill.com\nbrizzly.com\n||brizzly.com\n||brkmd.com\nbroadbook.com\n.broadpressinc.com\n||broadpressinc.com\nbbs.brockbbs.com\n||brookings.edu\nbrucewang.net\n.brutaltgp.com\n||brutaltgp.com\n||bsky.app\n||bsky.social\n||bt95.com\n.btaia.com\n.btbtav.com\n||btdig.com\n||btdigg.org\n.btku.me\n||btku.me\n||btku.org\n.btspread.com\n.btsynckeys.com\n.budaedu.org\n||budaedu.org\n.buddhanet.com.tw/zfrop/tibet\n||buffered.com\n||bullguard.com\n.bullog.org\n||bullog.org\n.bullogger.com\n||bullogger.com\n||bumingbai.net\n||bunbunhk.com\n.busayari.com\n|http://busayari.com\n||business-humanrights.org\n.businessinsider.com/bing-could-be-censoring-search-results-2014\n.businessinsider.com/china-banks-preparing-for-debt-implosion-2014\n.businessinsider.com/hong-kong-activists-defy-police-tear-gas-as-protests-continue-overnight-2014\n.businessinsider.com/internet-outages-reported-in-north-korea-2014\n.businessinsider.com/iphone-6-is-approved-for-sale-in-china-2014\n.businessinsider.com/nfl-announcers-surface-tablets-2014\n.businessinsider.com/panama-papers\n.businessinsider.com/umbrella-man-hong-kong-2014\n|http://www.businessinsider.com.au/*\n.businesstoday.com.tw\n||businesstoday.com.tw\n.busu.org/news\n|http://busu.org/news\nbusytrade.com\n.buugaa.com\n.buzzhand.com\n.buzzhand.net\n.buzzorange.com\n||buzzorange.com\n||buzzsprout.com\n||bvpn.com\n||bwh1.net\nbwsj.hk\n||bx.tl\n||bypasscensorship.org\n\n!--------------------CC-------------------------\n||c-span.org\n.c-spanvideo.org\n||c-spanvideo.org\n||c-est-simple.com\n.c100tibet.org\n||cableav.tv\n||cablegatesearch.net\n.cachinese.com\n.cacnw.com\n|http://cacnw.com\n.cactusvpn.com\n||cactusvpn.com\n.cafepress.com\n.cahr.org.tw\n.caijinglengyan.com\n||caijinglengyan.com\n.calameo.com/books\n||calendarz.com\n.calgarychinese.ca\n.calgarychinese.com\n.calgarychinese.net\n|http://blog.calibre-ebook.com\nfalun.caltech.edu\n.its.caltech.edu/~falun/\n.cam4.com\n.cam4.jp\n.cam4.sg\n.camfrog.com\n||camfrog.com\n||campaignforuyghurs.org\n||cams.com\n.cams.org.sg\ncanadameet.com\n.canalporno.com\n|http://bbs.cantonese.asia/\n!--http://www.cantonese.asia/action-bbs.html\n.canyu.org\n||canyu.org\n.cao.im\n.caobian.info\n||caobian.info\ncaochangqing.com\n||caochangqing.com\n.cap.org.hk\n||cap.org.hk\n.carabinasypistolas.com\ncardinalkungfoundation.org\n||posts.careerengine.us\ncarmotorshow.com\n||carrd.co\nss.carryzhou.com\n.cartoonmovement.com\n||cartoonmovement.com\n.casadeltibetbcn.org\n.casatibet.org.mx\n|http://casatibet.org.mx\n.cari.com.my\n||cari.com.my\n||caribbeancom.com\n.casinoking.com\n.casinoriva.com\n||catch22.net\n.catchgod.com\n|http://catchgod.com\n||catfightpayperview.xxx\n.catholic.org.hk\n||catholic.org.hk\ncatholic.org.tw\n||catholic.org.tw\n.cathvoice.org.tw\n||cato.org\n||cattt.com\n.cbc.ca\n||cbc.ca\n.cbsnews.com/video\n.cbtc.org.hk\n||southpark.cc.com\n!-.ccc.de\n!-||ccc.de\n||cccat.cc\n||cccat.co\n.ccdtr.org\n||ccdtr.org\n.cchere.com\n||cchere.com\n.ccim.org\n.cclife.ca\ncclife.org\n||cclife.org\ncclifefl.org\n||cclifefl.org\n.ccthere.com\n||ccthere.com\n||ccthere.net\n.cctmweb.net\n.cctongbao.com/article/2078732\nccue.ca\nccue.com\n.ccvoice.ca\n.ccw.org.tw\n.cgdepot.org\n|http://cgdepot.org\n||cdbook.org\n.cdcparty.com\n.cdef.org\n||cdef.org\n||cdig.info\ncdjp.org\n||cdjp.org\n!--.cdn-apple.com\n!--||cdn-apple.com\n.cdnews.com.tw\ncdp1989.org\ncdp1998.org\n||cdp1998.org\ncdp2006.org\n||cdp2006.org\n.cdpa.url.tw\n||cdpeu.org\n||cdpuk.co.uk\n||cdpusa.org\n||cdpweb.org\n||cdpweb.org\n||cdpwu.org\n||cdw.com\n||cecc.gov\n||cellulo.info\n||cenews.eu\n||centerforhumanreprod.com\n||centralnation.com\n.centurys.net\n|http://centurys.net\n.cfhks.org.hk\n.cfos.de\n||cfr.org\n.cftfc.com\n.cgst.edu\n.change.org\n||change.org\n.changp.com\n||changp.com\n.changsa.net\n|http://changsa.net\n||channelnewsasia.com\n.chapm25.com\n||chatgpt.com\n.chaturbate.com\n||chaturbate.com\n.chuang-yen.org\n||checkgfw.com\nchengmingmag.com\n.chenguangcheng.com\n||chenguangcheng.com\n.chenpokong.com\n||chenpokong.com\n.chenpokong.net\n|http://chenpokong.net\n||chenpokongvip.com\n||cherrysave.com\n.chhongbi.org\nchicagoncmtv.com\n|http://chicagoncmtv.com\n.china-week.com\nchina101.com\n||china101.com\n||china18.org\n||china21.com\nchina21.org\n||china21.org\n.china5000.us\nchinaaffairs.org\n||chinaaffairs.org\n||chinaaid.me\nchinaaid.us\nchinaaid.org\nchinaaid.net\n||chinaaid.net\nchinacomments.org\n||chinacomments.org\n.chinachange.org\n||chinachange.org\nchinachannel.hk\n||chinachannel.hk\n.chinacitynews.be\n.chinadialogue.net\n.chinadigitaltimes.net\n||chinadigitaltimes.net\n.chinaelections.org\n||chinaelections.org\n.chinaeweekly.com\n||chinaeweekly.com\n||chinafile.com\n||chinafreepress.org\n.chinagate.com\nchinageeks.org\nchinagfw.org\n||chinagfw.org\n.chinagonet.com\n.chinagreenparty.org\n||chinagreenparty.org\n.chinahorizon.org\n||chinahorizon.org\n.chinahush.com\n.chinainperspective.com\n||chinainterimgov.org\nchinalaborwatch.org\nchinalawtranslate.com\n.chinapost.com.tw/taiwan/national/national-news\nchinaxchina.com/howto\nchinalawandpolicy.com\n.chinamule.com\n||chinamule.com\nchinamz.org\n.chinanewscenter.com\n|https://chinanewscenter.com\n.chinapress.com.my\n||chinapress.com.my\n.china-review.com.ua\n|http://china-review.com.ua\n.chinarightsia.org\nchinasmile.net/forums\nchinasocialdemocraticparty.com\n||chinasocialdemocraticparty.com\nchinasoul.org\n||chinasoul.org\n.chinasucks.net\n||chinatopsex.com\n.chinatown.com.au\nchinatweeps.com\nchinaway.org\n.chinaworker.info\n||chinaworker.info\nchinayouth.org.hk\nchinayuanmin.org\n||chinayuanmin.org\n.chinese-hermit.net\nchinese-leaders.org\nchinese-memorial.org\n.chinesedaily.com\n||chinesedailynews.com\n.chinesedemocracy.com\n||chinesedemocracy.com\n||chinesegay.org\n.chinesen.de\n||chinesen.de\n.chinesenews.net.au/\n.chinesepen.org\n||chineseradioseattle.com\n.chinesetalks.net/ch\n||chineseupress.com\n.chingcheong.com\n||chingcheong.com\n.chinman.net\n|http://chinman.net\nchithu.org\n||cnnews.chosun.com\n.chrdnet.com\n|http://chrdnet.com\n.christianfreedom.org\n||christianfreedom.org\nchristianstudy.com\n||christianstudy.com\nchristusrex.org/www1/sdc\n.chubold.com\nchubun.com\n||christiantimes.org.hk\n.chrlawyers.hk\n||chrlawyers.hk\n.churchinhongkong.org/b5/index.php\n|http://churchinhongkong.org/b5/index.php\n.chushigangdrug.ch\n.cienen.com\n.cineastentreff.de\n.cipfg.org\n||circlethebayfortibet.org\n||cirosantilli.com\n.citizencn.com\n||citizencn.com\n||citizenlab.ca\n||citizenlab.org\n||citizenscommission.hk\n.citizenlab.org\ncitizensradio.org\n.city365.ca\n|http://city365.ca\ncity9x.com\n||citypopulation.de\n.citytalk.tw/event\n.civicparty.hk\n||civicparty.hk\n.civildisobediencemovement.org\ncivilhrfront.org\n||civilhrfront.org\n.civiliangunner.com\n.civilmedia.tw\n||civilmedia.tw\npsiphon.civisec.org\n||civitai.com\n.ck101.com\n||ck101.com\n.clarionproject.org/news/islamic-state-isis-isil-propaganda\n||classicalguitarblog.net\n.clb.org.hk\nclearharmony.net\nclearwisdom.net\n||clinica-tibet.ru\n.clipfish.de\ncloakpoint.com\n||app.cloudcone.com\n||cloudflare-ipfs.com\n||club1069.com\n||clubhouseapi.com\n||cmegroup.com\n||cmi.org.tw\n|http://www.cmoinc.org\ncmp.hku.hk\nhkupop.hku.hk\n||cmule.com\n||cmule.org\n||cms.gov\n|http://vpn.cmu.edu\n|http://vpn.sv.cmu.edu\n.cn6.eu\n.cna.com.tw\n||cna.com.tw\n.cnabc.com\n.cnd.org\n||cnd.org\ndownload.cnet.com\n.cnex.org.cn\n.cnineu.com\nwiki.cnitter.com\n.cnn.com/video\n.cnpolitics.org\n||cnpolitics.org\n.cn-proxy.com\n|http://cn-proxy.com\n.cnproxy.com\nblog.cnyes.com\nnews.cnyes.com\n||coat.co.jp\n.cochina.co\n||cochina.co\n||cochina.org\n.code1984.com/64\n|http://goagent.codeplex.com\n||codeshare.io\n||codeskulptor.org\n||conoha.jp\n|http://tosh.comedycentral.com\ncomefromchina.com\n||comefromchina.com\n.comic-mega.me\ncommandarms.com\n||commentshk.com\n.communistcrimes.org\n||communistcrimes.org\n||communitychoicecu.com\n||comparitech.com\n||compileheart.com\n||conoha.jp\n.contactmagazine.net\n.convio.net\n.coobay.com\n||cool18.com\n.coolaler.com\n||coolaler.com\ncoolder.com\n||coolder.com\n||coolloud.org.tw\n.coolncute.com\n||coolstuffinc.com\ncorumcollege.com\n.cos-moe.com\n|http://cos-moe.com\n.cosplayjav.pl\n|http://cosplayjav.pl\n.cotweet.com\n||cotweet.com\n.coursehero.com\n||coursehero.com\ncpj.org\n||cpj.org\n.cq99.us\n|http://cq99.us\ncrackle.com\n||crackle.com\n.crazys.cc\n.crazyshit.com\n||crazyshit.com\n||crchina.org\ncrd-net.org\ncreaders.net\n||creaders.net\n.creadersnet.com\n||cristyli.com\n||croxyproxy.com\n.crocotube.com\n|http://crocotube.com\n.crossthewall.net\n||crossthewall.net\n.crossvpn.net\n||crossvpn.net\n||crucial.com\n||blog.cryptographyengineering.com\ncsdparty.com\n||csdparty.com\n||csis.org\n||csmonitor.com\n||csuchen.de\n.csw.org.uk\n.ct.org.tw\n||ct.org.tw\n.ctao.org\n.ctfriend.net\n.ctitv.com.tw\n||ctowc.org\n.cts.com.tw\n||cts.com.tw\n||ctwant.com\n|http://library.usc.cuhk.edu.hk/\n|http://mjlsh.usc.cuhk.edu.hk/\n.cuhkacs.org/~benng\n.cuihua.org\n||cuihua.org\n.cuiweiping.net\n||cuiweiping.net\n||culture.tw\n.cumlouder.com\n||cumlouder.com\n||curvefish.com\n||cusp.hk\n.cusu.hk\n||cusu.hk\n.cutscenes.net\n||cutscenes.net\n.cw.com.tw\n||cw.com.tw\n|http://forum.cyberctm.com\ncyberghostvpn.com\n||cyberghostvpn.com\n||cynscribe.com\ncytode.us\n||ifan.cz.cc\n||mike.cz.cc\n||nic.cz.cc\n\n!--------------------DD-------------------------\n.d-fukyu.com\n|http://d-fukyu.com\ncl.d0z.net\n.d100.net\n||d100.net\n.d2bay.com\n|http://d2bay.com\n.dabr.co.uk\n||dabr.co.uk\ndabr.eu\ndabr.mobi\n||dabr.mobi\n||dabr.me\ndadazim.com\n||dadazim.com\n.dadi360.com\n.dafabet.com\ndafagood.com\ndafahao.com\n.dafoh.org\n.daftporn.com\n.dagelijksestandaard.nl\n.daidostup.ru\n|http://daidostup.ru\n.dailidaili.com\n||dailidaili.com\n||dailymail.co.uk\n.dailymotion.com\n||dailymotion.com\n||dailysabah.com\ndaiphapinfo.net\n.dajiyuan.com\n||dajiyuan.de\ndajiyuan.eu\ndalailama.com\n.dalailama.mn\n|http://dalailama.mn\n.dalailama.ru\n||dalailama.ru\ndalailama80.org\n.dalailama-archives.org\n.dalailamacenter.org\n|http://dalailamacenter.org\ndalailamafellows.org\n.dalailamafilm.com\n.dalailamafoundation.org\n.dalailamahindi.com\n.dalailamainaustralia.org\n.dalailamajapanese.com\n.dalailamaprotesters.info\n.dalailamaquotes.org\n.dalailamatrust.org\n.dalailamavisit.org.nz\n.dalailamaworld.com\n||dalailamaworld.com\ndalianmeng.org\n||dalianmeng.org\n.daliulian.org\n||daliulian.org\n.danke4china.net\n||danke4china.net\ndaolan.net\ndarktoy.net\n||darrenliuwei.com\n||dastrassi.org\n||daum.net\n.david-kilgour.com\n|http://david-kilgour.com\ndaxa.cn\n||daxa.cn\ncn.dayabook.com\n.daylife.com/topic/dalai_lama\n||db.tt\n||dbgjd.com\n||dcard.tw\ndcmilitary.com\n||ddc.com.tw\n.ddhw.info\n||de-sci.org\n.de-sci.org\n||deadhouse.org\n||deadline.com\n||deepai.org\n||decodet.co\n\n!--Origin:cdn-i30$_\n!--Exception: Homepage access without rst\n!--Keyword is $_\n.definebabe.com\n\n||delcamp.net\ndelicious.com/GFWbookmark\n.democrats.org\n||democrats.org\n.demosisto.hk\n||demosisto.hk\n||desc.se\n||dessci.com\n.destroy-china.jp\n||deutsche-welle.de\n||deviantart.com\n||deviantart.net\n||devio.us\n||devpn.com\n||devv.ai\n||dfas.mil\ndfn.org\ndharmakara.net\n.dharamsalanet.com\n.diaoyuislands.org\n||diaoyuislands.org\n.difangwenge.org\n|http://digiland.tw/\n||digitalnomadsproject.org\n.diigo.com\n||diigo.com\n||dilber.se\n||furl.net\n.dipity.com\n||directcreative.com\n!--||discogs.com\n!--@@||cdn.discogs.com\n.discuss.com.hk\n||discuss.com.hk\n.discuss4u.com\ndisp.cc\n.disqus.com\n||disqus.com\n.dit-inc.us\n||dit-inc.us\n.dizhidizhi.com\n||dizhuzhishang.com\ndjangosnippets.org\n.djorz.com\n||djorz.com\n||dl-laby.jp\n||dlive.tv\n||dlsite.com\n||dlyoutube.com\n||dmc.nico\n||dmcdn.net\n.dnscrypt.org\n||dnscrypt.org\n||dns2go.com\n||dnssec.net\ndoctorvoice.org\n\n!--DogFartNetwork\n.dogfartnetwork.com/tour\ngloryhole.com\n\n.dojin.com\n.dok-forum.net\n||dolc.de\n||dolf.org.hk\n||dollf.com\n.domain.club.tw\n.domaintoday.com.au\nchinese.donga.com\ndongtaiwang.com\n||dongtaiwang.com\n.dongtaiwang.net\n||dongtaiwang.net\n.dongyangjing.com\n|http://danbooru.donmai.us\n.dontfilter.us\n||dontmovetochina.com\n.dorjeshugden.com\n.dotplane.com\n||dotplane.com\n||dotsub.com\n.dotvpn.com\n||dotvpn.com\n.doub.io\n||doub.io\n||doublethinklab.org\n||dougscripts.com\n||douhokanko.net\n||doujincafe.com\ndowei.org\n|https://bartender.dowjones.com\ndphk.org\ndpp.org.tw\n||dpp.org.tw\n||dpr.info\n||dragonsprings.org\n!--||draw.io\n.dreamamateurs.com\n.drepung.org\n||drgan.net\n.drmingxia.org\n|http://drmingxia.org\n||dropbooks.tv\n||dropbox.com\n||api.dropboxapi.com\n||notify.dropboxapi.com\n||dropboxusercontent.com\ndrsunacademy.com\n.drtuber.com\n.dscn.info\n|http://dscn.info\n.dstk.dk\n|http://dstk.dk\n||dtiblog.com\n||dtic.mil\n.dtwang.org\n.duanzhihu.com\n.duckdns.org\n|http://duckdns.org\n.duckduckgo.com\n||duckduckgo.com\n.duckload.com/download\n||duckmylife.com\n.duga.jp\n|http://duga.jp\n.duihua.org\n||duihua.org\n||duihuahrjournal.org\n.dunyabulteni.net\n.duoweitimes.com\n||duoweitimes.com\nduping.net\n||duplicati.com\ndupola.com\ndupola.net\n.dushi.ca\n||duyaoss.com\n||dvorak.org\n.dw.com\n||dw.com\n||dw.de\n.dw-world.com\n||dw-world.com\n.dw-world.de\n|http://dw-world.de\nwww.dwheeler.com\ndwnews.com\n||dwnews.com\ndwnews.net\n||dwnews.net\nxys.dxiong.com\n||dynawebinc.com\n||dysfz.cc\n.dzze.com\n\n!--------------------EE-------------------------\n||e-classical.com.tw\n||e-gold.com\n.e-gold.com\n.e-hentai.org\n||e-hentai.org\n.e-hentaidb.com\n|http://e-hentaidb.com\ne-info.org.tw\n.e-traderland.net/board\n.e-zone.com.hk/discuz\n|http://e-zone.com.hk/discuz\n.e123.hk\n||e123.hk\n.earlytibet.com\n|http://earlytibet.com\n.earthcam.com\n.earthvpn.com\n||earthvpn.com\neastern-ark.com\n.easternlightning.org\n.eastturkestan.com\n|http://www.eastturkistan.net/\n.eastturkistan-gov.org\n.eastturkistancc.org\n.eastturkistangovernmentinexile.us\n||eastturkistangovernmentinexile.us\n.easyca.ca\n.easypic.com\n||fnc.ebc.net.tw\n||news.ebc.net.tw\n.ebony-beauty.com\nebookbrowse.com\nebookee.com\n||ecfa.org.tw\nushuarencity.echainhost.com\n||ecimg.tw\necministry.net\n.economist.com\nbbs.ecstart.com\nedgecastcdn.net\n||edgecastcdn.net\n/twimg\\.edgesuite\\.net\\/\\/?appledaily/\nedicypages.com\n.edmontonchina.cn\n.edmontonservice.com\nedoors.com\n.edubridge.com\n||edubridge.com\n.edupro.org\n||eevpn.com\nefcc.org.hk\n.efukt.com\n|http://efukt.com\n||eic-av.com\n||eireinikotaerukai.com\n.eisbb.com\n.eksisozluk.com\n||eksisozluk.com\nelectionsmeter.com\n||elgoog.im\n.ellawine.org\n.elpais.com\n||elpais.com\n.eltondisney.com\n.emaga.com/info/3407\nemilylau.org.hk\n.emanna.com/chineseTraditional\nbitc.bme.emory.edu/~lzhou/blogs\n.empfil.com\n.emule-ed2k.com\n|http://emule-ed2k.com\n.emulefans.com\n|http://emulefans.com\n.emuparadise.me\n.enanyang.my\n!--.enanyang.my/news/20170502/%E7%BE%8E%E5%9B%BD%E4%B9%8B%E9%9F%B3%E5%A4%A7%E5%9C%B0%E9%9C%87%E3%80%8A%E8%8B%B9%E6%9E%9C%E3%80%8B%E7%8B%AC%E5%AE%B6\n||encrypt.me\n||enewstree.com\n.enfal.de\n||chinese.engadget.com\n||engagedaily.org\nenglishforeveryone.org\n||englishfromengland.co.uk\nenglishpen.org\n.enlighten.org.tw\n||entermap.com\n||app.evozi.com\n.episcopalchurch.org\n.epochhk.com\n||epochhk.com\nepochtimes-bg.com\n||epochtimes-bg.com\nepochtimes-romania.com\n||epochtimes-romania.com\nepochtimes.co.il\n||epochtimes.co.il\nepochtimes.co.kr\n||epochtimes.co.kr\nepochtimes.com\n||epochtimes.com\n.epochtimes.cz\n||epochtimes.de\n||epochtimes.fr\n||epochtimes.ie\n||epochtimes.it\n||epochtimes.jp\n||epochtimes.ru\n||epochtimes.se\n||epochtimestr.com\n.epochweek.com\n||epochweek.com\n||epochweekly.com\n.eporner.com\n.equinenow.com\nerabaru.net\n.eracom.com.tw\n.eraysoft.com.tr\n.erepublik.com\n.erights.net\n||erights.net\n.erktv.com\n|http://erktv.com\n||ernestmandel.org\n||erodaizensyu.com\n||erodoujinlog.com\n||erodoujinworld.com\n||eromanga-kingdom.com\n||eromangadouzin.com\n.eromon.net\n|http://eromon.net\n.eroprofile.com\n.eroticsaloon.net\n.eslite.com\n||eslite.com\n!--.eslite.com/product\n!--.eslite.com/Search_BW.aspx?q\nwiki.esu.im/%E8%9B%A4%E8%9B%A4%E8%AF%AD%E5%BD%95\n||esu.dog\n.etaa.org.au\n.etadult.com\netaiwannews.com\n||etizer.org\n||etokki.com\n||etsy.com\n!--.ettoday.net\n.ettoday.net/news/20151216/614081\netvonline.hk\n.eu.org\n||eu.org\n.eucasino.com\n.eulam.com\n.eurekavpt.com\n||eurekavpt.com\n.euronews.com\n||euronews.com\neeas.europa.eu/delegations/china/press_corner/all_news/news/2015/20150716_zh\neeas.europa.eu/statements-eeas/2015/151022\n||apps.evozi.com\n||evschool.net\n||exblog.jp\n||blog.exblog.co.jp\n@@||www.exblog.jp\n.exchristian.hk\n||exchristian.hk\n|http://blog.excite.co.jp\n||exhentai.org\n||exmormon.org\n||expatshield.com\n.expecthim.com\n||expecthim.com\nexperts-univers.com\n||exploader.net\n.expressvpn.com\n||expressvpn.com\n.extremetube.com\neyevio.jp\n||eyevio.jp\n.eyny.com\n||eyny.com\n.ezpc.tk/category/soft\n.ezpeer.com\n\n!--------------------FF-------------------------\n||facebookquotes4u.com\n.faceless.me\n||faceless.me\n|http://facesoftibetanselfimmolators.info\n||facesofnyfw.com\n||factpedia.org\n.faith100.org\n|http://faith100.org\n\n!--Enhancement:\n!--http://faithfuleye.com.detail.website/\n!--http://faithfuleye.com.ipaddress.com/\n.faithfuleye.com\n\n||faiththedog.info\n.fakku.net\n||fallenark.com\n.falsefire.com\n||falsefire.com\nfalun-co.org\nfalunart.org\n||falunasia.info\n|http://falunau.org\n.falunaz.net\nfalundafa.org\nfalundafa-dc.org\n||falundafa-florida.org\n||falundafa-nc.org\n||falundafa-pa.net\n||falundafa-sacramento.org\nfalun-ny.net\n||falundafaindia.org\nfalundafamuseum.org\n.falungong.club\n.falungong.de\nfalungong.org.uk\n||falunhr.org\nfaluninfo.de\nfaluninfo.net\n.falunpilipinas.net\n||falunworld.net\nfamilyfed.org\n.fangeming.com\n||fanglizhi.info\n||fangong.org\nfangongheike.com\n||fanhaolou.com\n.fanqiang.tk\nfanqianghou.com\n||fanqianghou.com\n.fanqiangzhe.com\n||fanqiangzhe.com\n||fantv.hk\nfapdu.com\nfaproxy.com\n!--.farxian.com\n.fawanghuihui.org\nfanqiangyakexi.net\nfail.hk\n||famunion.com\n.fan-qiang.com\n.fangbinxing.com\n||fangbinxing.com\nfangeming.com\n.fangmincn.org\n||fangmincn.org\n.fanhaodang.com\n||fanqiang.network\n||fanswong.com\n.fanyue.info\n.farwestchina.com\n\n!--Fastly\nen.favotter.net\n!--||rnw.global.ssl.fastly.net\n!--|https://*global.ssl.fastly.net/\nnytimes.map.fastly.net\n||nytimes.map.fastly.net\n||fast.wistia.com\n\n||fastestvpn.com\n||fastssh.com\n||faststone.org\nfavstar.fm\n||favstar.fm\nfaydao.com/weblog\n||faz.net\n.fc2.com\n.fc2china.com\n.fc2cn.com\n||fc2cn.com\nfc2blog.net\n|http://uygur.fc2web.com/\nvideo.fdbox.com\n.fdc64.de\n.fdc64.org\n.fdc89.jp\n||fourface.nodesnoop.com\n!--feedbooks.mobi\n||feeder.co\n||feelssh.com\nfeer.com\n.feifeiss.com\n|http://feitianacademy.org\n.feitian-california.org\n||feixiaohao.com\n||feministteacher.com\n.fengzhenghu.com\n||fengzhenghu.com\n.fengzhenghu.net\n||fengzhenghu.net\n.fevernet.com\n|http://ff.im\nfffff.at\nfflick.com\n.ffvpn.com\nfgmtv.net\n.fgmtv.org\n.fhreports.net\n|http://fhreports.net\n.figprayer.com\n||figprayer.com\n.fileflyer.com\n||fileflyer.com\n|http://feeds.fileforum.com\n.files2me.com\n.fileserve.com/file\nfillthesquare.org\nfilmingfortibet.org\n.filthdump.com\n.finchvpn.com\n||finchvpn.com\n!--findbook.tw\nfindmespot.com\n||findyoutube.com\n||findyoutube.net\n.fingerdaily.com\nfinler.net\n.firearmsworld.net\n|http://firearmsworld.net\n||relay.firefox.com\n.fireofliberty.org\n||fireofliberty.org\n.firetweet.io\n||firetweet.io\n||firstpost.com\n||firstrade.com\n||fish.audio\n!--||flagfox.net\n.flagsonline.it\nfleshbot.com\n.fleursdeslettres.com\n|http://fleursdeslettres.com\n||flgg.us\n||flgjustice.org\n\n!--||farm6.staticflickr.com\n!--.flickr.com/photos/46231077@N06\n!--.flickr.com/groups/aiweiwei\n!--.flickr.com/photos/digitalboy100\n!--.flickr.com/photos/fzhenghu\n!--.flickr.com/photos/lonelyfox\n!--flickr.com/photos/vanvan/529925157\n!--.flickr.com/photos/winterkanal\n!--.flickr.com/photos/zola\n||flickr.com\n||staticflickr.com\n\nflickrhivemind.net\n.flickriver.com\n.fling.com\n||flipkart.com\n||flog.tw\n.flyvpn.com\n||flyvpn.com\n|http://cn.fmnnow.com\nfofldfradio.org\nblog.foolsmountain.com\n.forum4hk.com\nfangong.forums-free.com\npioneer-worker.forums-free.com\n!--foursquare.com\n!--|http://4sq.com\n|https://ss*.4sqi.net\nvideo.foxbusiness.com\n|http://foxgay.com\n||fringenetwork.com\n||flecheinthepeche.fr\n.fochk.org\n||fochk.org\n||focustaiwan.tw\n.focusvpn.com\n||fofg.org\n.fofg-europe.net\n.fooooo.com\n||fooooo.com\n||foreignaffairs.com\n.fotile.me\n||fourthinternational.org\n||foxdie.us\n||foxsub.com\nfoxtang.com\n.fpmt.org\n|http://fpmt.org\n.fpmt.tw\n.fpmt-osel.org\n||fpmtmexico.org\nfqok.org\n||fqrouter.com\n||franklc.com\n.freakshare.com\n|http://freakshare.com\n||free4u.com.ar\nfree-gate.org\n.free-hada-now.org\nfree-proxy.cz\n.free.fr/adsl\nkineox.free.fr\ntibetlibre.free.fr\n||freealim.com\nwhitebear.freebearblog.org\n||freebrowser.org\n.freechal.com\n.freedomchina.info\n||freedomchina.info\n.freedomhouse.org\n||freedomhouse.org\n.freedomsherald.org\n||freedomsherald.org\n.freefq.com\n.freefuckvids.com\n.freegao.com\n||freegao.com\nfreeilhamtohti.org\n||freekazakhs.org\n.freekwonpyong.org\n||saveliuxiaobo.com\n.freelotto.com\n||freelotto.com\nfreeman2.com\n.freeopenvpn.com\nfreemoren.com\nfreemorenews.com\nfreemuse.org/archives/789\nfreenet-china.org\nfreenewscn.com\ncn.freeones.com\n.freeoz.org/bbs\n||freeoz.org\n||freessh.us\nfree4u.com.ar\n.free-ssh.com\n||free-ssh.com\n||freebeacon.com\n.freechina.news\n||freechinaforum.org\n||freechinaweibo.com\n.freedomcollection.org/interviews/rebiya_kadeer\n.freeforums.org\n||freenetproject.org\n.freeoz.org\n.freetibet.net\n||freetibet.org\n.freetibetanheroes.org\n|http://freetibetanheroes.org\n||freetribe.me\n.freeviewmovies.com\n.freevpn.me\n|http://freevpn.me\n||freewallpaper4.me\n.freewebs.com\n.freewechat.com\n||freewechat.com\nfreeweibo.com\n||freeweibo.com\n.freexinwen.com\n.freeyoutubeproxy.net\n||freeyoutubeproxy.net\nfriendfeed.com\nfriendfeed-media.com/e99a4ebe2fb4c1985c2a58775eb4422961aa5a2e\nfriends-of-tibet.org\n.friendsoftibet.org\n||friendsoftibet.org\nfreechina.net\n|http://www.zensur.freerk.com/\nfreevpn.nl\nfreeyellow.com\nhk.frienddy.com/hk\n|http://adult.friendfinder.com/\n.fring.com\n||fring.com\n.fromchinatousa.net\n||frommel.net\n.frontlinedefenders.org\n||frontlinedefenders.org\n.frootvpn.com\n||frootvpn.com\n||fscked.org\n.fsurf.com\n.ftv.com.tw\n||ftv.com.tw\n||ftvnews.com.tw\nfucd.com\n.fuckcnnic.net\n||fuckcnnic.net\nfuckgfw.org\n.fulione.com\n|https://fulione.com\n||fullerconsideration.com\nfulue.com\n.funf.tw\nfunp.com\n.fuq.com\n.furhhdl.org\n||furinkan.com\n.futurechinaforum.org\n||futuremessage.org\n.fux.com\n.fuyin.net\n.fuyindiantai.org\n.fuyu.org.tw\n||fw.cm\n.fxcm-chinese.com\n||fxcm-chinese.com\nfzh999.com\nfzh999.net\nfzlm.com\n\n!--------------------GG-------------------------\n.g6hentai.com\n|http://g6hentai.com\n||g-queen.com\n||gab.com\n||gabocorp.com\n.gaeproxy.com\n.gaforum.org\n.gagaoolala.com\n||gagaoolala.com\n.galaxymacau.com\n||galenwu.com\n.galstars.net\n||game735.com\ngamebase.com.tw\ngamejolt.com\n|http://wiki.gamerp.jp\n||gamer.com.tw\n.gamer.com.tw\n.gamez.com.tw\n||gamez.com.tw\n.gamousa.com\n.gaoming.net\n||gaoming.net\nganges.com\n||ganjing.com\n||ganjingworld.com\n.gaopi.net\n|http://gaopi.net\n.gaozhisheng.org\n.gaozhisheng.net\ngardennetworks.com\n||gardennetworks.org\n!--IP of Garden Network\n72.52.81.22\n||gartlive.com\n||gate-project.com\n||gather.com\n.gatherproxy.com\ngati.org.tw\n.gaybubble.com\n.gaycn.net\n.gayhub.com\n||gaymap.cc\n.gaymenring.com\n.gaytube.com\n!--||gaytube.com\n||images-gaytube.com\n.gaywatch.com\n|http://gaywatch.com\n.gazotube.com\n||gazotube.com\n||gcc.org.hk\n||gclooney.com\n||gclubs.com\n||gcmasia.com\n.gcpnews.com\n|http://gcpnews.com\n.gdbt.net/forum\ngdzf.org\n||geek-art.net\ngeekerhome.com/2010/03/xixiang-project-cross-gfw\n||geekheart.info\n.gekikame.com\n|http://gekikame.com\n.gelbooru.com\n|http://gelbooru.com\n||generated.photos\n||genius.com\n!--||genuitec.com\n.geocities.co.jp\n.geocities.com/SiliconValley/Circuit/5683/download.html\nhk.geocities.com\ngeocities.jp\n||geph.io\n.gerefoundation.org\n||getastrill.com\n.getchu.com\n.getcloak.com\n||getcloak.com\n||getfoxyproxy.org\n.getfreedur.com\n||getgom.com\n.geti2p.net\n||geti2p.net\ngetiton.com\n.getjetso.com/forum\n.getlantern.org\n||getlantern.org\n||getmalus.com\n.getsocialscope.com\n||getsync.com\n||gettr.com\ngfbv.de\n.gfgold.com.hk\n.gfsale.com\n||gfsale.com\ngfw.org.ua\n.gfw.press\n||gfw.press\n||gfw.report\n.ggssl.com\n||ggssl.com\n!--||ghost.org\n.ghostpath.com\n||ghostpath.com\n||ghut.org\n.giantessnight.com\n|http://giantessnight.com\n.gifree.com\n||giga-web.jp\ntw.gigacircle.com\n|http://cn.giganews.com/\ngigporno.ru\n||girlbanker.com\n.git.io\n||git.io\n|http://softwaredownload.gitbooks.io\n||raw.githack.com\n\n!---GitHub---\n||github.blog\n||github.com\n!--github.com/getlantern\n!--|https://gist.github.com\n!--http://cthlo.github.io/hktv\n!--hahaxixi.github.io\n!--|https://hahaxixi.github.io\n!--||haoel.github.io\n!--|http://onionhacker.github.io\n!--||rg3.github.io\n!--||sikaozhe1997.github.io\n!--||sodatea.github.io\n!--||terminus2049.github.io\n!--||toutyrater.github.io\n!--wsgzao.github.io\n!--|https://wsgzao.github.io\n.github.io\n||github.io\n||githubusercontent.com\n||githubassets.com\n\n.gizlen.net\n||gizlen.net\n.gjczz.com\n||gjczz.com\nglobaljihad.net\nglobalmediaoutreach.com\nglobalmuseumoncommunism.org\n||globalrescue.net\n.globaltm.org\n.globalvoicesonline.org\n||globalvoicesonline.org\n||globalvpn.net\n.glock.com\ngluckman.com/DalaiLama\n||gmgard.com\n||gmhz.org\n|http://www.gmiddle.com\n|http://www.gmiddle.net\n.gmll.org\n||suche.gmx.net\n||gnci.org.hk\n||gnews.org\ngo-pki.com\n||goagent.biz\n||goagentplus.com\ngobet.cc\n||godaddy.com\ngodfootsteps.org\n||godfootsteps.org\ngodns.work\ngodsdirectcontact.co.uk\n.godsdirectcontact.org\ngodsdirectcontact.org.tw\n.godsimmediatecontact.com\n||gofundme.com\n.gogotunnel.com\n||gohappy.com.tw\n.gokbayrak.com\n.goldbet.com\n||goldbetsports.com\n||golden-ages.org\n||goldeneyevault.com\n.goldenfrog.com\n||goldenfrog.com\n.goldjizz.com\n|http://goldjizz.com\n.goldstep.net\n||goldwave.com\ngongmeng.info\ngongm.in\ngongminliliang.com\n.gongwt.com\n|http://gongwt.com\nblog.goo.ne.jp/duck-tail_2009\n.gooday.xyz\n||gooday.xyz\n||goodhope.school\n.goodreads.com\n||goodreads.com\n.goodreaders.com\n||goodreaders.com\n.goodtv.com.tw\n.goodtv.tv\n||goofind.com\n.googlesile.com\n.gopetition.com\n||gopetition.com\n.goproxing.net\n||goreforum.com\n.gotrusted.com\n||gotrusted.com\n||gotw.ca\n||grammaly.com\ngrandtrial.org\n.graphis.ne.jp\n||graphis.ne.jp\n||graphql.org\n||gravatar.com\ngreatfirewall.biz\n||greatfirewallofchina.net\n.greatfirewallofchina.org\n||greatfirewallofchina.org\n||greenfieldbookstore.com.hk\n.greenparty.org.tw\n||greenpeace.org\n.greenreadings.com/forum\ngreat-firewall.com\ngreat-roc.org\ngreatroc.org\ngreatzhonghua.org\n.greenpeace.com.tw\n.greenvpn.net\n||greenvpn.net\n.greenvpn.org\n||grindr.com\n||ground.news\n||grotty-monday.com\ngs-discuss.com\n||gsearch.media\n||gtricks.com\nguancha.org\nguaneryu.com\n.guardster.com\n.gun-world.net\ngunsandammo.com\n||gutteruncensored.com\n||gvm.com.tw\n||gwins.org\n.gzm.tv\n||gzone-anime.info\n\n!-------------GHS-----\n!-||feeds.cbsnews.com\n!-||www.chinesealbumart.com\n||clementine-player.org\n!-||clemesha.org\n!-||www.cloudgirlfriend.com\n!-||cocoawithlove.com\n!-||blog.controlspace.org\n!-D\n!-||www.dailygyan.com\n!-||dailytodo.org\n!-||blog.danmarner.com\n!-||github.danmarner.com\n!-||design-seeds.com\n!-||designers-artists.com\n!-||mail.diyang.org\n!-||blog.doughellmann.com\n!-||downforeveryoneorjustme.com\n!-||droidsecurity.com\n!-||www.dropmocks.com\n!-||dumblittleman.com\n!-E\nechofon.com\n!-||echofon.com\n!-||epc-jav.com\n!-||everdark.info\n!-||evhead.com\n!-F\n!-||facilelogin.com\n!-||*.fatduck.org\n!-||blog.fdcn.org\n!-||fftogo.com\n!-||flightsimtalk.com\n!-||mclee.foolme.net\n!-||www.frienddeck.com\n!-||fringespoilers.com\n!-||fringetelevision.com\n!-||funpea.com\n!-G\n!-||blog.gatein.org\n!-||feeds.gawker.com\n!-||geektang.com\n!-||geohot.us\n!-||getaround.com\n!-||gmer.net\n!-||www.gmote.org\n!-||blog.go2web20.net\n!-||google-melange.com\n!-||fame.gonzolabs.org\n!-||govecn.org\n!-||gqueues.com\n!-||graphycalc.com\n!-||blog.growlforwindows.com\n!-H\n!-||hcm.com.tw\n!-||blog.headius.com\n!-||hogbaysoftware.com\n!-||blog.hotot.org\n!-||feeds.howstuffworks.com\n!-||huhaitai.com\n!-||blog.humanrightsfirst.org\n!-I\n!-||site.icu-project.org\n!-||igorware.com\n!-||ihas1337code.com\n!-||inknouveau.com\n!-||inote.tw\n!-||ironhelmet.com\n!-||iwfwcf.com\n!-J\n!-||blog.jangmt.com\n!-||blog.jayfields.com\n!-||blog.joint.net\n!-||blog.jsquaredjavascript.com\n!-||blog.jtbworld.com\n!-K\n!-||kathyschwalbe.com\n!-||tomatovpn.keithmoyer.com\n!-||www.keithmoyer.com\n!-||kendalvandyke.com\n!-||blog.kengao.tw\n!-||log.keso.cn\n!-||www.khanacademy.org\n||www.klip.me\n!-||usbloadergx.koureio.net\n!-||blog.kowalczyk.info\n!-L\n!-||labyrinth2.com\n!-||larsgeorge.com\n!-||blog.lastpass.com\n!-||docs.latexlab.org\n!-||leanessays.com\n!-||blog.lidaobing.info\n!-||log.lightory.net\n!-||feeds.limi.net\n!-||www.liteapplications.com\n!-||blog.liukangxu.info\n!-||twitter.liukangxu.info\n!-||oasisnewsroom.live4ever.us\n!-||www.lockergnome.com\n!-||locql.com\n@@||site.locql.com\n!-||feeds.loiclemeur.com\n!-||blog.louisgray.com\n!-M\n!-||madebysofa.com\n!-||mademoisellerobot.com\n!-||masamixes.com\n!-||www.metamuse.net\n!-||blog.metasploit.com\n!-||milazi.com\n!-||www.miniweather.com\n!-||twitter.missiu.com\n!-||plurktop-button.mmdays.com\n!-||feeds.mobileread.com\n!-||www.modernizr.com\n!-||www.modk.it\n!-||mytwishirt.com\n!-N\n!-||blog.netflix.com\n!-||blog.nihilogic.dk\n!-||ntlk.org\n!-||nvquan.org\n!-||nogoodatcoding.com\n!-||blog.notdot.net\n!-||www.notify.io\n!-O\n!-||blog.obvious.com\n!-||onebigfluke.com\n!-||overstimulate.com\n!-P\n!-||pcgeekblog.com\n!-||feeds.pdfchm.net\n!-||feeds.people.com\n!-||blog.persistent.info\n!-||chrome.plantsvszombies.com\n!-||portablesoft.org.ru\n!-||prasannatech.net\n!-||talk.news.pts.org.tw\n!-||python-excel.org\n!-Q\n!-R\n!-||r-chart.com\n!-||rameshsubramanian.org\n!-||rapid.pk\n!-||blog.renanse.com\n!-||robertmao.com\n!-||www.romeo-foxtrot.com\n!-S\n!-||salmiyuck.com\n!-||samsal.com\n!-||blog.seeminglee.com\n!-||blog.sflow.com\n!-||blog.sigfpe.com\n!-||simpletext.ws\n!-||www.skulpt.org\n!-||rss.slashdot.org\n!-||snippetsapp.com\n!-||w.sns.ly\n!-||www.socialnmobile.com\n!-||www.socialwhois.com\n!-||spiritjb.org\n!-||ssbook.com\n!-||sshforwarding.com\n!-||stationeria.com\n||stephaniered.com\n!-||sunjidong.net\n!-||syniumsoftware.com\n@@||download.syniumsoftware.com\n!-T\n!-||tagxedo.com\n!-||blog.tatoeba.org\n!-||www.techfob.com\n!-||teachparentstech.org\n!-||the8pen.com\n!-||theiphonewiki.com\n!-||blog.thesilentnumber.me\n!-||thesponty.com\n!-||theultralinx.com\n!-||blog.think-async.com\n!-||tornadoweb.org\n!-||transparentuptime.com\n!-||triangulationblog.com\n!-||blog.tsunanet.net\n!-||en.tuxero.com\n!-||twazzup.com\n!-||tweetswell.com\n!-||twibes.com\n!-||art.twgg.org\n!-||twivert.com\n!-U\n|http://ub0.cc\n!-||jonny.ubuntu-tw.net\n!-||blog.umonkey.net\n!-V\n!-||tp.vbap.com.au\n!-||www.virtuousrom.com\n!-||blog.visibotech.com\n!-W\n!-||waveprotocol.org\n!-||www.wavesandbox.com\n!-||webfee.org.ru\n!-||blog.webmproject.org\n!-||webupd8.org\n!-||www.whatbrowser.org\n!-||www.wheredoyougo.net\n!-||willhains.com\n!-||feeds.wired.com\n!-||wisemapping.org\nwozy.in\n!-||wozy.in/\n!-||blog.wundercounter.com\n!-X\n!-||xdelta.org\n!-||xiaogaozi.org\n!-||xilou.us\n!-||xzy.org.ru\n!-Y\n!-||yooper.be\n!-||tsong.yunxi.net\n!-Z\n\ngospelherald.com\n||gospelherald.com\n|http://hk.gradconnection.com/\n||grangorz.org\ngreatfire.org\n||greatfire.org\ngreatfirewallofchina.org\n||greatroc.tw\n.gts-vpn.com\n|http://gts-vpn.com\n||gtv.org\n||gtv1.org\n.gu-chu-sum.org\n|http://gu-chu-sum.org\n.guaguass.com\n|http://guaguass.com\n.guaguass.org\n|http://guaguass.org\n.guangming.com.my\nguishan.org\n||guishan.org\n.gumroad.com\n||gumroad.com\n||gunsamerica.com\nguruonline.hk\n|http://gvlib.com\n.gyalwarinpoche.com\n.gyatsostudio.com\n\n!--------------------HH-------------------------\n.h528.com\n.h5dm.com\n.h5galgame.me\n||h-china.org\n.h-moe.com\n|http://h-moe.com\nh1n1china.org\n.hacken.cc/bbs\n.hacker.org\n||hackmd.io\n||hackthatphone.net\nhahlo.com\n||haijiao.com\n||hakkatv.org.tw\n.handcraftedsoftware.org\n|http://bbs.hanminzu.org/\n.hanunyi.com\n.hao.news/news\n|http://ae.hao123.com\n|http://ar.hao123.com\n|http://br.hao123.com\n|http://en.hao123.com\n|http://id.hao123.com\n|http://jp.hao123.com\n|http://ma.hao123.com\n|http://mx.hao123.com\n|http://sa.hao123.com\n|http://th.hao123.com\n|http://tw.hao123.com\n|http://vn.hao123.com\n|http://hk.hao123img.com\n|http://ld.hao123img.com\n||happy-vpn.com\n.haproxy.org\n||hardsextube.com\n.harunyahya.com\n|http://harunyahya.com\nbbs.hasi.wang\nhave8.com\n@@||haygo.com\n.hclips.com\n||hdlt.me\n||hdtvb.net\n.hdzog.com\n|http://hdzog.com\n||ordns.he.net\n||heartyit.com\n.heavy-r.com\n.hec.su\n|http://hec.su\n.hecaitou.net\n||hecaitou.net\n.hechaji.com\n||hechaji.com\n||heeact.edu.tw\n.hegre-art.com\n|http://hegre-art.com\n||cdn.helixstudios.net\n||helplinfen.com\n||helpuyghursnow.org\n||helloandroid.com\n||helloqueer.com\n.helloss.pw\nhellotxt.com\n||hellotxt.com\n.hentai.to\n.hellouk.org/forum/lofiversion\n.helpeachpeople.com\n||helpeachpeople.com\n||helpster.de\n.helpzhuling.org\nhentaitube.tv\n.hentaivideoworld.com\n\n!###########--Heroku--##########\n!--||getcloudapp.com\n!--||cl.ly\n!--@@||f.cl.ly\n!--EC2 DNS Poisoned\n||id.heroku.com\n||herokuapp.com\n\n||heqinglian.net\n||heritage.org\n||heungkongdiscuss.com\n.hexieshe.com\n||hexieshe.com\n||hexieshe.xyz\n!--Google employee within Google IP\n||hexxeh.net\n||heyuedi.com\napp.heywire.com\n.heyzo.com\n.hgseav.com\n.hhdcb3office.org\n.hhthesakyatrizin.org\nhi-on.org.tw\n||hiccears.com\nhidden-advent.org\n||hidden-advent.org\nhidecloud.com/blog/2008/07/29/fuck-beijing-olympics.html\n||hide.me\n.hidein.net\n.hideipvpn.com\n||hideipvpn.com\n.hideman.net\n||hideman.net\nhideme.nl\n||hidemy.name\n.hidemyass.com\n||hidemyass.com\nhidemycomp.com\n||hidemycomp.com\n.hihiforum.com\n.hihistory.net\n||hihistory.net\n.higfw.com\nhighpeakspureearth.com\n||highrockmedia.com\n||hiitch.com\n||hikinggfw.org\n.hilive.tv\n.himalayan-foundation.org\n||himalayan-foundation.org\nhimalayanglacier.com\n.himemix.com\n||himemix.com\n.himemix.net\ntimes.hinet.net\n.hitomi.la\n|http://hitomi.la\n.hiwifi.com\n@@||hiwifi.com\nhizbuttahrir.org\nhizb-ut-tahrir.info\nhizb-ut-tahrir.org\n.hjclub.info\n.hk-pub.com/forum\n|http://hk-pub.com\n.hk01.com\n||hk01.com\n.hk32168.com\n||hk32168.com\n||hkacg.com\n||hkacg.net\n.hkatvnews.com\nhkbc.net\n.hkbf.org\n.hkbookcity.com\n||hkbookcity.com\n||hkchronicles.com\n.hkchurch.org\nhkci.org.hk\n.hkcmi.edu\n||hkcnews.com\n||hkcoc.com\n||hkctu.org.hk\nhkday.net\n.hkdailynews.com.hk/china.php\n||hkdc.us\nhkdf.org\n.hkej.com\n.hkepc.com/forum/viewthread.php?tid=1153322\n||hket.com\n||hkfaa.com\nhkfreezone.com\nhkfront.org\nm.hkgalden.com\n|https://m.hkgalden.com\n.hkgreenradio.org/home\n||hkgpao.com\n.hkheadline.com*blog\n.hkheadline.com/instantnews\nhkhkhk.com\nhkhrc.org.hk\nhkhrm.org.hk\n||hkip.org.uk\n1989report.hkja.org.hk\nhkjc.com\n.hkjp.org\n.hklft.com\n.hklts.org.hk\n||hklts.org.hk\n||hkmap.live\n||hkopentv.com\n||hkpeanut.com\nhkptu.org\n.hkreporter.com\n||hkreporter.com\n|http://hkupop.hku.hk/\n.hkusu.net\n||hkusu.net\n.hkvwet.com\n.hkwcc.org.hk\n||hkzone.org\n.hmonghot.com\n|http://hmonghot.com\n.hmv.co.jp/\nhnjhj.com\n||hnjhj.com\n.hnntube.com\n||hojemacau.com.mo\n||hola.com\n||hola.org\nholymountaincn.com\nholyspiritspeaks.org\n||holyspiritspeaks.org\n||derekhsu.homeip.net\n.homeperversion.com\n|http://homeservershow.com\n|http://old.honeynet.org/scans/scan31/sub/doug_eric/spam_translation.html\n.hongkongfp.com\n||hongkongfp.com\nhongmeimei.com\n||hongzhi.li\n||honven.xyz\n.hootsuite.com\n||hootsuite.com\n||hoover.org\n.hopedialogue.org\n|http://hopedialogue.org\n.hopto.org\n.hornygamer.com\n.hornytrip.com\n|http://hornytrip.com\n||horrorporn.com\n||hostloc.com\n||hotair.com\n.hotav.tv\n.hotels.cn\nhotfrog.com.tw\nhotgoo.com\n.hotpornshow.com\nhotpot.hk\n.hotshame.com\n||hotspotshield.com\n||hottg.com\n.hotvpn.com\n||hotvpn.com\n||hougaige.com\n||howtoforge.com\n||hoxx.com\n||hpjav.com\n.hqcdp.org\n||hqcdp.org\n||hqjapanesesex.com\nhqmovies.com\n.hrcir.com\n.hrcchina.org\n.hrea.org\n.hrichina.org\n||hrichina.org\n||hrntt.org\n.hrtsea.com\n.hrw.org\n||hrw.org\nhrweb.org\n||hsex.men\n||hsjp.net\n||hsselite.com\n||hst.net.tw\n.hstern.net\n.hstt.net\n.htkou.net\n||htkou.net\n.hua-yue.net\n.huaglad.com\n||huaglad.com\n.huanghuagang.org\n||huanghuagang.org\n.huangyiyu.com\n.huaren.us\n||huaren.us\n.huaren4us.com\n.huashangnews.com\n|http://huashangnews.com\nbbs.huasing.org\nhuaxia-news.com\nhuaxiabao.org\nhuaxin.ph\n||huayuworld.org\n||huffingtonpost.com\n||huffpost.com\n||huggingface.co\n||hugoroy.eu\n||huhaitai.com\n||huhamhire.com\n.huhangfei.com\n||huhangfei.com\nhuiyi.in\n.hulkshare.com\n||humanparty.me\n||humanrightspressawards.org\n||hung-ya.com\n||hungerstrikeforaids.org\n||huping.net\nhurgokbayrak.com\n.hurriyet.com.tr\n.hut2.ru\n||hutianyi.net\nhutong9.net\nhuyandex.com\n.hwadzan.tw\n||hwayue.org.tw\n||hwinfo.com\n||hxwk.org\nhxwq.org\n||hyperrate.com\nebook.hyread.com.tw\n||ebook.hyread.com.tw\n\n!--------------------II-------------------------\n||i1.hk\n||i2p2.de\n||i2runner.com\n||i818hk.com\n.i-cable.com\n.i-part.com.tw\n.iamtopone.com\niask.ca\n||iask.ca\niask.bz\n||iask.bz\n.iav19.com\n||iavian.net\nibiblio.org/pub/packages/ccic\n||ibit.am\n.iblist.com\n||iblogserv-f.net\nibros.org\n|http://cn.ibtimes.com\n.ibvpn.com\n||ibvpn.com\nicams.com\n||icedrive.net\n.icij.org\n||icij.org\n||icl-fi.org\n.icoco.com\n||icoco.com\n\n!--38.103.165.50\n||furbo.org\n!--||iconfactory.com\n||warbler.iconfactory.net\n\n||iconpaper.org\n!-- Google Pages\n||icu-project.org\nw.idaiwan.com/forum\nidemocracy.asia\n.identi.ca\n||identi.ca\n||idiomconnection.com\n|http://www.idlcoyote.com\n.idouga.com\n.idreamx.com\nforum.idsam.com\n.idv.tw\n.ieasy5.com\n|http://ieasy5.com\n.ied2k.net\n.ienergy1.com\n||iepl.us\n||ift.tt\nifanqiang.com\n.ifcss.org\n||ifcss.org\nifjc.org\n.ift.tt\n|http://ift.tt\n||ifreewares.com\n||igcd.net\n.igfw.net\n||igfw.net\n.igfw.tech\n||igfw.tech\n.igmg.de\n||ignitedetroit.net\n.igotmail.com.tw\n||igvita.com\n||ihakka.net\n.ihao.org/dz5\n||iicns.com\n.ikstar.com\n||ilhamtohtiinstitute.org\n||illusionfactory.com\n||ilove80.be\n||im.tv\n@@||myvlog.im.tv\n||im88.tw\n||imgchili.net\n.imageab.com\n.imagefap.com\n||imagefap.com\n||imageflea.com\n||imageglass.org\n||imageshack.us\n||imagevenue.com\n||imagezilla.net\n.imb.org\n|http://imb.org\n\n!--IMDB\n|http://www.imdb.com/name/nm0482730\n.imdb.com/title/tt0819354\n.imdb.com/title/tt1540068\n.imdb.com/title/tt4908644\n\n.img.ly\n||img.ly\n||imgasd.com\n.imgur.com\n||imgur.com\n.imkev.com\n||imkev.com\n.imlive.com\n.immoral.jp\nimpact.org.au\nimpp.mn\n|http://tech2.in.com/video/\nin99.org\nin-disguise.com\n.incapdns.net\n.incloak.com\n||incloak.com\n||incredibox.fr\n||independent.co.uk\n||indiablooms.com\n||indiandefensenews.in\n||indianarrative.com\n||timesofindia.indiatimes.com\n.indiemerch.com\n||indiemerch.com\n||info-graf.fr\nwebsite.informer.com\n||inherit.live\n||initiativesforchina.org\n||inkbunny.net\n||inkui.com\n||inmediahk.net\n||inmediahk.net\n||innermongolia.org\n||inoreader.com\n||inote.tw\n||insecam.org\n|http://insecam.org\n||inside.com.tw\n||insidevoa.com\n||institut-tibetain.org\n||interactivebrokers.com\n||internet.org\ninternetdefenseleague.org\n||internetfreedom.org\n!--||interpol.int\n||internetpopculture.com\n.inthenameofconfuciusmovie.com\n||inthenameofconfuciusmovie.com\ninxian.com\n||inxian.com\nipalter.com\n!--||ipcf.org.tw\n||ipfire.org\n||iphone4hongkong.com\n||iphonehacks.com\n||iphonetaiwan.org\n||iphonix.fr\n||ipicture.ru\n.ipjetable.net\n||ipjetable.net\n.ipobar.com/read.php?\nipoock.com/img\n.iportal.me\n|http://iportal.me\n||ippotv.com\n.ipredator.se\n||ipredator.se\n.iptv.com.tw\n||iptvbin.com\n||ipvanish.com\niredmail.org\nchinese.irib.ir\n||ironbigfools.compython.net\n||ironpython.net\n.ironsocket.com\n||ironsocket.com\n.is.gd\n.islahhaber.net\n.islam.org.hk\n|http://islam.org.hk\n.islamawareness.net/Asia/China\n.islamhouse.com\n||islamhouse.com\n.islamicity.com\n.islamicpluralism.org\n.islamtoday.net\n.isaacmao.com\n||isaacmao.com\n||isgreat.org\n||ismaelan.com\n.ismalltits.com\n||ismprofessional.net\nisohunt.com\n||israbox.com\n.issuu.com\n||issuu.com\n.istars.co.nz\noversea.istarshine.com\n||oversea.istarshine.com\nblog.istef.info/2007/10/21/myentunnel\n.istiqlalhewer.com\n.istockphoto.com\nisunaffairs.com\nisuntv.com\n||isupportuyghurs.org\nitaboo.info\n||itaboo.info\n||italiatibet.org\n||itemfix.com\nithelp.ithome.com.tw\n||itshidden.com\n.itsky.it\n.itweet.net\n|http://itweet.net\n.iu45.com\n.iuhrdf.org\n||iuhrdf.org\n.iuksky.com\n.ivacy.com\n||ivacy.com\n.iverycd.com\n||ivonblog.com\n.ivpn.net\n||ivpn.net\n||iwara.tv\n||ixquick.com\n.ixxx.com\n.iyouport.com\n||iyouport.com\n||iyouport.org\n.izaobao.us\n||gmozomg.izihost.org\n.izles.net\n.izlesem.org\n\n!--------------------JJ-------------------------\n||j.mp\n||jable.tv\n||blog.jackjia.com\njamaat.org\n||jamestown.org\n||jamyangnorbu.com\n||jan.ai\n.jandyx.com\n||janwongphoto.com\n||japan-whores.com\n.jav.com\n.jav101.com\n.jav2be.com\n||jav2be.com\n.jav68.tv\n.javakiba.org\n|http://javakiba.org\n.javbus.com\n||javbus.com\n||javfor.me\n.javhd.com\n.javhip.com\n.javmobile.net\n|http://javmobile.net\n.javmoo.com\n.javseen.com\n|http://javseen.com\njbtalks.cc\njbtalks.com\njbtalks.my\n.jdwsy.com\njeanyim.com\n||jfqu36.club\n||jfqu37.xyz\n||jgoodies.com\n.jiangweiping.com\n||jiangweiping.com\n||jiaoyou8.com\n||jichangtj.com\n.jiehua.cz\n||hk.jiepang.com\n||tw.jiepang.com\njieshibaobao.com\n.jigglegifs.com\n56cun04.jigsy.com\njigong1024.com\ndaodu14.jigsy.com\nspecxinzl.jigsy.com\nwlcnew.jigsy.com\n.jihadology.net\n|http://jihadology.net\njinbushe.org\n||jinbushe.org\n.jingsim.org\nzhao.jinhai.de\njingpin.org\n||jingpin.org\njinpianwang.com\n.jinroukong.com\nac.jiruan.net\n||jitouch.com\n.jizzthis.com\njjgirls.com\n.jkb.cc\n|http://jkb.cc\njkforum.net\n||jma.go.jp\nresearch.jmsc.hku.hk/social\nweiboscope.jmsc.hku.hk\n.jmscult.com\n|http://jmscult.com\n||joachims.org\n||jobso.tv\n.sunwinism.joinbbs.net\n||joinclubhouse.com\n||jornaldacidadeonline.com.br\n.journalchretien.net\n||journalofdemocracy.org\n.joymiihub.com\n.joyourself.com\njpopforum.net\n||jsdelivr.net\n||fiddle.jshell.net\n.jubushoushen.com\n||jubushoushen.com\n!--Doamin parking\n.juhuaren.com\n||juliereyc.com\n||junauza.com\n.june4commemoration.org\n.junefourth-20.net\n||junefourth-20.net\n||bbs.junglobal.net\n.juoaa.com\n|http://juoaa.com\njustfreevpn.com\n||justhost.ru\n.justicefortenzin.org\njustpaste.it\n||justmysocks1.net\njusttristan.com\njuyuange.org\njuziyue.com\n||juziyue.com\n||jwmusic.org\n@@||music.jwmusic.org\n||cdn.jwplayer.com\n.jyxf.net\n\n!--------------------KK-------------------------\n||k-doujin.net\n||ka-wai.com\n||kadokawa.co.jp\n.kagyu.org\n||kagyu.org.za\n.kagyumonlam.org\n.kagyunews.com.hk\n.kagyuoffice.org\n||kagyuoffice.org\n||kagyuoffice.org.tw\n.kaiyuan.de\n.kakao.com\n||kakao.com\n.kalachakralugano.org\n.kankan.today\n.kannewyork.com\n||kannewyork.com\n.kanshifang.com\n||kanshifang.com\n||kantie.org\nkanzhongguo.com\nkanzhongguo.eu\n.kaotic.com\n||kaotic.com\n||karayou.com\nkarkhung.com\n.karmapa.org\n.karmapa-teachings.org\n||kawase.com\n.kba-tx.org\n.kcoolonline.com\n.kebrum.com\n||kebrum.com\n.kechara.com\n.keepandshare.com/visit/visit_page.php?i=688154\n!--||keepvid.com\n.keezmovies.com\n.kendincos.net\n.kenengba.com\n||kenengba.com\n||keontech.net\n.kepard.com\n||kepard.com\nwiki.keso.cn/Home\n||keycdn.com\n.khabdha.org\n.khmusic.com.tw\n||kichiku-doujinko.com\n.kik.com\n||kik.com\nbbs.kimy.com.tw\n.kindleren.com\n|http://kindleren.com\n|http://www.kindleren.com\n.kingdomsalvation.org\n||kingdomsalvation.org\nkinghost.com\n!--.kingstone.com.tw/book/\n||kingstone.com.tw\n.kink.com\n.kinokuniya.com\n||kinokuniya.com\nkillwall.com\n||killwall.com\n||kinmen.travel\n.kir.jp\n.kissbbao.cn\n|http://kiwi.kz\n||kk-whys.co.jp\n!--||kmt.org.tw\n.kmuh.org.tw\n.knowledgerush.com/kr/encyclopedia\n||knowyourmeme.com\n.kobo.com\n||kobo.com\n.kobobooks.com\n||kobobooks.com\n||kodingen.com\n@@||www.kodingen.com\n||kompozer.net\n.konachan.com\n||konachan.com\n.kone.com\n||koolsolutions.com\n.koornk.com\n||koornk.com\n||koranmandarin.com\n.korenan2.com\n||kqes.net\n|http://gojet.krtco.com.tw\n.ksdl.org\n.ksnews.com.tw\n||ktzhk.com\n.kui.name/event\n||kukuku.uk\nkun.im\n.kurashsultan.com\n||kurtmunger.com\nkusocity.com\n||kwcg.ca\n||kwok7.com\n.kwongwah.com.my\n||kwongwah.com.my\n.kxsw.life\n||kxsw.life\n.kyofun.com\nkyohk.net\n||kyoyue.com\n.kyzyhello.com\n||kyzyhello.com\n.kzeng.info\n||kzeng.info\n\n!--------------------LL-------------------------\nla-forum.org\nladbrokes.com\n||labiennale.org\n.lagranepoca.com\n||lagranepoca.com\n||lala.im\n.lalulalu.com\n.lama.com.tw\n||lama.com.tw\n.lamayeshe.com\n|http://lamayeshe.com\n|http://www.lamenhu.com\n.lamnia.co.uk\n||lamnia.co.uk\nlamrim.com\n||landofhope.tv\n.lanterncn.cn\n|http://lanterncn.cn\n.lantosfoundation.org\n.laod.cn\n|http://laod.cn\nlaogai.org\n||laogai.org\n||laogairesearch.org\nlaomiu.com\n.laoyang.info\n|http://laoyang.info\n||laptoplockdown.com\n.laqingdan.net\n||laqingdan.net\n||larsgeorge.com\n.lastcombat.com\n|http://lastcombat.com\n||lastfm.es\nlatelinenews.com\n||lausan.hk\n||le-vpn.com\n.leafyvpn.net\n||leafyvpn.net\n||ledger.com\nleeao.com.cn/bbs/forum.php\n!--||leecheukyan.org\nlefora.com\n||left21.hk\n.legalporno.com\n.legsjapan.com\n|http://leirentv.ca\nleisurecafe.ca\n||lematin.ch\n.lemonde.fr\n||lenwhite.com\n||leorockwell.com\nlerosua.org\n||lerosua.org\nblog.lester850.info\n||lesoir.be\n.letou.com\nletscorp.net\n||letscorp.net\n||ocsp.int-x3.letsencrypt.org\n||ss.levyhsu.com\n!69.16.175.42\n||cdn.assets.lfpcontent.com\n.lhakar.org\n|http://lhakar.org\n.lhasocialwork.org\n.liangyou.net\n||liangyou.net\n.lianyue.net\n||liaowangxizang.net\n.liaowangxizang.net\n||liberal.org.hk\n||libertysculpturepark.com\n||libertytimes.com.tw\nblogs.libraryinformationtechnology.com/jxyz\n||libredd.it\n||lighten.org.tw\n||lightnovel.cn\nlimiao.net\nlinkuswell.com\nabitno.linpie.com/use-ipv6-to-fuck-gfw\n||line.me\n||line-apps.com\n.linglingfa.com\n||lingvodics.com\n.link-o-rama.com\n|http://link-o-rama.com\n||linkedin.com\n.linkideo.com\n||api.linksalpha.com\n||apidocs.linksalpha.com\n||www.linksalpha.com\n||help.linksalpha.com\n||linux.org.hk\nlinuxtoy.org/archives/installing-west-chamber-on-ubuntu\n.lionsroar.com\n.lipuman.com\n||liquidvpn.com\n||greatfire.us7.list-manage.com\n||listennotes.com\n||listentoyoutube.com\nlistorious.com\n.liu-xiaobo.org\n||liudejun.com\n.liuhanyu.com\n.liujianshu.com\n||liujianshu.com\n.liuxiaobo.net\n||liuxiaobo.net\nliuxiaotong.com\n||liuxiaotong.com\n.livedoor.jp\n.liveleak.com\n||liveleak.com\n||livemint.com\nlivestream.com\n||livestream.com\n||livingonline.us\n||livingstream.com\n||livevideo.com\n.livevideo.com\n.liwangyang.com\nlizhizhuangbi.com\nlkcn.net\n||chat.lmsys.org\n||lncn.org\n.load.to\n.lobsangwangyal.com\n.localdomain.ws\n||localdomain.ws\nlocalpresshk.com\n||lockestek.com\nlogbot.net\n||logiqx.com\nsecure.logmein.com\n||secure.logmein.com\n||logos.com.hk\n.londonchinese.ca\n.longhair.hk\nlongmusic.com\n||longtermly.net\n||lookpic.com\n.looktoronto.com\n|http://looktoronto.com\n.lotsawahouse.org/tibetan-masters/fourteenth-dalai-lama\n.lotuslight.org.hk\n.lotuslight.org.tw\nhkreporter.loved.hk\n!--403?\n||lpsg.com\n||lrfz.com\n.lrip.org\n||lrip.org\n.lsd.org.hk\n||lsd.org.hk\nlsforum.net\n.lsm.org\n||lsm.org\n.lsmchinese.org\n||lsmchinese.org\n.lsmkorean.org\n||lsmkorean.org\n.lsmradio.com/rad_archives\n.lsmwebcast.com\n.ltn.com.tw\n||ltn.com.tw\n||luckydesigner.space\n.luke54.com\n.luke54.org\n.lupm.org\n||lupm.org\n||lushstories.com\nluxebc.com\nlvhai.org\n||lvhai.org\n||lvv2.com\n.lyfhk.net\n|http://lyfhk.net\n||lzjscript.com\n.lzmtnews.org\n||lzmtnews.org\n\n!--------------------MM-------------------------\nhttp://*.m-team.cc\n!--m-team.cc/forum\n.macrovpn.com\nmacts.com.tw\n||mad-ar.ch\n||madrau.com\n||madthumbs.com\n||magic-net.info\nmahabodhi.org\nmy.mail.ru\n.maiplus.com\n|http://maiplus.com\n.maizhong.org\nmakkahnewspaper.com\n.mamingzhe.com\nmanicur4ik.ru\n||manyvoices.news\n.maplew.com\n|http://maplew.com\n||marc.info\nmarguerite.su\n||martincartoons.com\nmaskedip.com\n.maiio.net\n.mail-archive.com\n.malaysiakini.com\n||makemymood.com\n.manchukuo.net\n.maniash.com\n|http://maniash.com\n.mansion.com\n.mansionpoker.com\n!--||marines.mil\n!--markmail.org*message\n||martau.com\n|http://blog.martinoei.com\n.martsangkagyuofficial.org\n|http://martsangkagyuofficial.org\nmaruta.be/forget\n.marxist.com\n||marxist.net\n.marxists.org/chinese\n!--||mashable.com\n||matainja.com\n||mathable.io\n||mathiew-badimon.com\n||matrix.org\n||matsushimakaede.com\n||matters.town\n||maturejp.com\nmayimayi.com\n.maxing.jp\n.mcaf.ee\n|http://mcaf.ee\n||mcadforums.com\nmcfog.com\nmcreasite.com\n.md-t.org\n||md-t.org\n||meansys.com\n.media.org.hk\n.mediachinese.com\n||mediachinese.com\n.mediafire.com/?\n.mediafire.com/download\n.mediafreakcity.com\n||mediafreakcity.com\n.medium.com\n||medium.com\n.meetav.com\n||meetup.com\nmefeedia.com\njihadintel.meforum.org\n||mega.co.nz\n||mega.io\n||mega.nz\n||megaproxy.com\n||megarotic.com\nmegavideo.com\n||megurineluka.com\n||meizhong.blog\n||meizhong.report\n.meltoday.com\n.memehk.com\n||memehk.com\nmemorybbs.com\n.memri.org\n.memrijttm.org\n||mercdn.net\n.mercyprophet.org\n||mercyprophet.org\n||mergersandinquisitions.org\n.meridian-trust.org\n||meridian-trust.org\n.meripet.biz\n||meripet.biz\n.meripet.com\n||meripet.com\n||merit-times.com.tw\nmeshrep.com\n.mesotw.com/bbs\nmetacafe.com/watch\n||metafilter.com\n||meteorshowersonline.com\n||metro.taipei\n.metrohk.com.hk/?cmd=detail&categoryID=2\n||metrolife.ca\n.metroradio.com.hk\n|http://metroradio.com.hk\n||mewe.com\nmeyou.jp\n.meyul.com\n||mgoon.com\n||mgstage.com\n||mh4u.org\nmhradio.org\n|http://michaelanti.com\n||michaelmarketl.com\n|http://bbs.mikocon.com\n.microvpn.com\n|http://microvpn.com\nmiddle-way.net\n.mihk.hk/forum\n.mihr.com\nmihua.org\n!--IP\n||mikesoltys.com\n.milph.net\n|http://milph.net\n.milsurps.com\nmimiai.net\n.mimivip.com\n.mimivv.com\n.mindrolling.org\n|http://mindrolling.org\n||mingdemedia.org\n.minghui.or.kr\n|http://minghui.or.kr\nminghui.org\n||minghui.org\nminghui-a.org\nminghui-b.org\nminghui-school.org\n.mingjinglishi.com\n||mingjinglishi.com\nmingjingnews.com\n||mingjingtimes.com\n.mingpao.com\n||mingpao.com\n.mingpaocanada.com\n.mingpaomonthly.com\n|http://mingpaomonthly.com\nmingpaonews.com\n.mingpaony.com\n.mingpaosf.com\n.mingpaotor.com\n.mingpaovan.com\n.mingshengbao.com\n.minhhue.net\n.miniforum.org\n.ministrybooks.org\n.minzhuhua.net\n||minzhuhua.net\nminzhuzhanxian.com\nminzhuzhongguo.org\n||miroguide.com\nmirrorbooks.com\n||mirrormedia.mg\n.mist.vip\n||thecenter.mit.edu\n||scratch.mit.edu\n.mitao.com.tw\n.mitbbs.com\n||mitbbs.com\nmitbbsau.com\n.mixero.com\n||mixero.com\n||mixi.jp\nmixpod.com\n.mixx.com\n||mixx.com\n||mizzmona.com\n.mk5000.com\n.mlcool.com\n||mlzs.work\n.mm-cg.com\n||mmaaxx.com\n.mmmca.com\nmnewstv.com\n||mobatek.net\n.mobile01.com\n||mobile01.com\n||mobileways.de\n.mobypicture.com\n|http://moby.to\n||mod.io\n||modernchinastudies.org\n||moeerolibrary.com\nwiki.moegirl.org\n.mofaxiehui.com\n.mofos.com\n||mog.com\n||mohu.rocks\nmolihua.org\n||momoshop.com.tw\n||mondex.org\n||money-link.com.tw\n|http://www.monlamit.org\n||moon.fm\n.moonbbs.com\n||moonbbs.com\n||moptt.tw\n||monica.im\n||monitorchina.org\n||monocloud.me\nbbs.morbell.com\n||morningsun.org\n||moroneta.com\n.motherless.com\n|http://motherless.com\nmotor4ik.ru\n.mousebreaker.com\n!--||movabletype.com\n.movements.org\n||movements.org\n||moviefap.com\n||www.moztw.org\n.mp3buscador.com\n||mpettis.com\n.mpfinance.com\n||mpfinance.com\n.mpinews.com\n||mpinews.com\nmponline.hk\n.mqxd.org\n|http://mqxd.org\nmrtweet.com\n||mrtweet.com\nnews.hk.msn.com\nnews.msn.com.tw\nmsguancha.com\n.mswe1.org\n|http://mswe1.org\n||mthruf.com\n||mubi.com\nmuchosucko.com\n||multiply.com\nmultiproxy.org\nmultiupload.com\n.mullvad.net\n||mullvad.net\n.mummysgold.com\n.murmur.tw\n|http://murmur.tw\n.musicade.net\n.muslimvideo.com\n||muzi.com\n||muzi.net\n||mx981.com\n.my-formosa.com\n.my-proxy.com\n.my-private-network.co.uk\n||my-private-network.co.uk\nforum.my903.com\n.myactimes.com/actimes\n||myanniu.com\n.myaudiocast.com\n||myaudiocast.com\n.myav.com.tw/bbs\n.mybbs.us\n.myca168.com\n.mycanadanow.com\n||bbs.mychat.to\n||mychinamyhome.com\n.mychinamyhome.com\n.mychinanet.com\n.mychinanews.com\n||mychinanews.com\n.mychinese.news\n||mycnnews.com\n||mykomica.org\nmycould.com/discuz\n.myeasytv.com\n||myeclipseide.com\n.myforum.com.hk\n||myforum.com.hk\n||myforum.com.uk\n.myfreecams.com\n.myfreepaysite.com\n.myfreshnet.com\n.myiphide.com\n||myiphide.com\nforum.mymaji.com\nmymediarom.com/files/box\n||mymoe.moe\n||mymusic.net.tw\n||myparagliding.com\n||mypopescu.com\nmyradio.hk/podcast\n.myreadingmanga.info\nmysinablog.com\n.myspace.com\n!--.blogs.myspace.com\n!--||blogs.myspace.com\n!--vids.myspace.com/index.cfm?fuseaction=vids.\n!--viewmorepics.myspace.com\n||myspacecdn.com\n.mytalkbox.com\n.mytizi.com\n\n!--------------------NN-------------------------\n||naacoalition.org\nold.nabble.com\n||naitik.net\n.nakido.com\n||nakido.com\n.nakuz.com/bbs\n||nalandabodhi.org\n||nalandawest.org\n.namgyal.org\nnamgyalmonastery.org\n||namsisi.com\n.nanyang.com\n||nanyang.com\n.nanyangpost.com\n||nanyangpost.com\n.nanzao.com\n!--.nanzao.com/sc/china/20223\n!--.nanzao.com/sc/hk-macau-tw\n.naol.ca\n.naol.cc\nuighur.narod.ru\n.nat.moe\n||nat.moe\ncyberghost.natado.com\n||national-lottery.co.uk\n||nationalawakening.org\n||nationalinterest.org\nnews.nationalgeographic.com/news/2014/06/140603-tiananmen-square\n||nationalreview.com\n.nationsonline.org/oneworld/tibet\n||line.naver.jp\n||navyfamily.navy.mil\n||navyreserve.navy.mil\n||nko.navy.mil\n||usno.navy.mil\nnaweeklytimes.com\n||nbcnews.com\n.nbtvpn.com\n|http://nbtvpn.com\nnccwatch.org.tw\n.nch.com.tw\n.ncn.org\n||nchrd.org\n||ncn.org\n||etools.ncol.com\n.nde.de\n||ndi.org\n.ndr.de\n.ned.org\n||nekoslovakia.net\n||neowin.net\n||nepusoku.com\n||net-fits.pro\n||netalert.me\n!--bbsnew.netbig.com\nbbs.netbig.com\n.netbirds.com\nnetcolony.com\nbolin.netfirms.com\n||netflav.com\n||netme.cc\n||netsarang.com\nnetsneak.com\n.network54.com\nnetworkedblogs.com\n.networktunnel.net\nneverforget8964.org\nnew-3lunch.net\n.new-akiba.com\n.new96.ca\n.newcenturymc.com\n|http://newcenturymc.com\nnewcenturynews.com\n||newchen.com\n.newchen.com\n.newgrounds.com\n||newhighlandvision.com\nnewipnow.com\n.newlandmagazine.com.au\n||newmitbbs.com\n.newnews.ca\nnews100.com.tw\nnewschinacomment.org\n.newscn.org\n||newscn.org\nnewspeak.cc/story\n.newsancai.com\n||newsancai.com\n.newsdetox.ca\n.newsdh.com\n||newsmax.com\n||newstamago.com\n||newstapa.org\n||newstatesman.com\nnewstarnet.com\n||newsweek.com\n.newtaiwan.com.tw\nnewtalk.tw\n||newtalk.tw\n||newyorker.com\nnewyorktimes.com\n||nexon.com\n.next11.co.jp\n||nextdigital.com.hk\n.nextmag.com.tw\n\n!--hk*.nextmedia.com\n!--tw*.nextmedia.com\n!--static*.nextmedia.com\n.nextmedia.com\n\n||nexton-net.jp\n||nexttv.com.tw\n.nfjtyd.com\n||co.ng.mil\n||nga.mil\nngensis.com\n||ngodupdongchung.com\n.nhentai.net\n|http://nhentai.net\n.nhk-ondemand.jp\n.nicovideo.jp/watch\n||nicovideo.jp\n||nighost.org\nav.nightlife141.com\nninecommentaries.com\n.ninjacloak.com\n||ninjaproxy.ninja\nnintendium.com\ntaiwanyes.ning.com\nusmgtcg.ning.com/forum\n||niusnews.com\n||njactb.org\nnjuice.com\n||njuice.com\n||nlfreevpn.com\n||nmsl.website\n||nnews.eu\n\n!--no-ip.com#NOIP\n.ddns.net/\n.gooddns.info\n||gotdns.ch\n.maildns.xyz\n.no-ip.org\n.opendn.xyz\n.servehttp.com\nsytes.net\n.whodns.xyz\n.zapto.org\n|http://dynupdate.no-ip.com/\n\n||nobel.se\n!--.nobelprize.org\n!--|http://nobelprize.org\nnobelprize.org/nobel_prizes/peace/laureates/1989\nnobelprize.org/nobel_prizes/peace/laureates/2010\nnobodycanstop.us\n||nobodycanstop.us\n||nokogiri.org\n||nokola.com\nnoodlevpn.com\n.norbulingka.org\nnordvpn.com\n||nordvpn.com\n||notepad-plus-plus.org\n||novelasia.com\n.news.now.com\n|http://news.now.com\n!--|http://news.now.com/home*\nnews.now.com%2Fhome\n||nownews.com\n.nowtorrents.com\n.noypf.com\n||noypf.com\n||npa.go.jp\n.npnt.me\n|http://npnt.me\n.nps.gov\n.nradio.me\n|http://nradio.me\n.nrk.no\n||nrk.no\n.ntd.tv\n||ntd.tv\n.ntdtv.com\n||ntdtv.com\n||ntdtv.com.tw\n.ntdtv.co.kr\nntdtv.ca\nntdtv.org\nntdtv.ru\nntdtvla.com\n.ntrfun.com\n||cbs.ntu.edu.tw\n||media.nu.nl\n.nubiles.net\n||nuexpo.com\n.nukistream.com\n||nurgo-software.com\n||nutaku.net\n||nutsvpn.work\n.nuvid.com\n||nvdst.com\nnuzcom.com\n.nvquan.org\n.nvtongzhisheng.org\n|http://nvtongzhisheng.org\n.nwtca.org\n|http://nyaa.eu\n||nyaa.si\n||nybooks.com\n.nydus.ca\nnylon-angel.com\nnylonstockingsonline.com\n||nypost.com\n!--nysingtao.com\n.nzchinese.com\n||nzchinese.net.nz\n\n!--------------------OO-------------------------\n||oann.com\nobservechina.net\n.obutu.com\nocaspro.com\noccupytiananmen.com\noclp.hk\n.ocreampies.com\n||october-review.org\n||odysee.com\noffbeatchina.com\n||officeoftibet.com\n|http://ofile.org\n||ogaoga.org\ntwtr2src.ogaoga.org\n.ogate.org\n||ogate.org\nwww2.ohchr.org/english/bodies/cat/docs/ngos/II_China_41.pdf\n||ohmyrss.com\n.oikos.com.tw/v4\n.oiktv.com\noizoblog.com\n.ok.ru\n||ok.ru\n.okayfreedom.com\n||okayfreedom.com\n||okk.tw\n|http://filmy.olabloga.pl/player\nold-cat.net\n||olevod.com\n||olumpo.com\n.olympicwatch.org\n||omct.org\nomgili.com\n||omnitalk.com\n||omnitalk.org\n||omny.fm\ncling.omy.sg\nforum.omy.sg\nnews.omy.sg\nshowbiz.omy.sg\n||on.cc\n||onedrive.live.com\n||onion.city\n||onion.ly\n.onlinecha.com\n||onlineyoutube.com\n||onlygayvideo.com\n.onlytweets.com\n|http://onlytweets.com\nonmoon.net\nonmoon.com\n.onthehunt.com\n|http://onthehunt.com\n.oopsforum.com\nopen.com.hk\nopenallweb.com\nopendemocracy.net\n||opendemocracy.net\n.openervpn.in\nopenid.net\n||openid.net\n.openleaks.org\n||openleaks.org\n||openstreetmap.org\n||opentech.fund\nopenvpn.net\n||openvpn.net\n||openwebster.com\n.openwrt.org.cn\n@@||openwrt.org.cn\nmy.opera.com/dahema\n||demo.opera-mini.net\n.opus-gaming.com\n|http://opus-gaming.com\nwww.orchidbbs.com\n.organcare.org.tw\norganharvestinvestigation.net\n.orgasm.com\n.orgfree.com\n||oricon.co.jp\n||orient-doll.com\norientaldaily.com.my\n||orientaldaily.com.my\n!--orientaldaily.on.cc\n||orn.jp\nt.orzdream.com\n||t.orzdream.com\ntui.orzdream.com\n||orzistic.org\n||osfoora.com\n.otnd.org\n||otnd.org\n||otto.de\n||ourdearamy.com\noursogo.com\n.oursteps.com.au\n||oursteps.com.au\n.oursweb.net\n||ourtv.hk\nxinqimeng.over-blog.com\n||overcast.fm\n||overdaily.org\n||overplay.net\nshare.ovi.com/media\n||ovpn.com\n|http://owl.li\n|http://ht.ly\n|http://htl.li\n|http://mash.to\nwww.owind.com\n||owltail.com\n||oxfordscholarship.com\n|http://www.oxid.it\noyax.com\noyghan.com/wps\n.ozchinese.com/bbs\n||ow.ly\nbbs.ozchinese.com\n.ozvoice.org\n||ozvoice.org\n.ozxw.com\n.ozyoyo.com\n\n!--------------------PP-------------------------\n||pachosting.com\n.pacificpoker.com\n.packetix.net\n||pacopacomama.com\n.padmanet.com\n||page.link\npage2rss.com\n||pagodabox.com\n.palacemoon.com\nforum.palmislife.com\n||eriversoft.com\n.paldengyal.com\npaljorpublications.com\n.paltalk.com\n!--||pangci.net\n||pandapow.co\n.pandapow.net\n.pandavpn-jp.com\n||pandavpn-jp.com\n||pandavpnpro.com\n.panluan.net\n||panluan.net\n||pao-pao.net\npaper.li\npaperb.us\n.paradisehill.cc\n.paradisepoker.com\n||parler.com\n||parsevideo.com\n.partycasino.com\n.partypoker.com\n.passion.com\n||passion.com\n.passiontimes.hk\npastebin.com\n.pastie.org\n||pastie.org\n||blog.pathtosharepoint.com\n||patreon.com\n||pawoo.net\npbs.org/wgbh/pages/frontline/tankman\npbs.org/wgbh/pages/frontline/tibet\nvideo.pbs.org\n\n!--Pbwiki\npbwiki.com\n||pbworks.com\n||developers.box.net\n||wiki.oauth.net\n||wiki.phonegap.com\n||wiki.jqueryui.com\n\n||pbxes.com\n||pbxes.org\npcdvd.com.tw\n||pcgamestorrents.com\n.pchome.com.tw\n||pcij.org\n.pcstore.com.tw\n||pct.org.tw\npdetails.com\n||pdproxy.com\n||peace.ca\npeacefire.org\npeacehall.com\n||peacehall.com\n|http://pearlher.org\n.peeasian.com\n||peing.net\n.pekingduck.org\n||pekingduck.org\n.pemulihan.or.id\n|http://pemulihan.or.id\n||pen.io\npenchinese.com\n||penchinese.net\n.penchinese.net\n||blog.pentalogic.net\n.penthouse.com\n||pentoy.hk\n.peoplebookcafe.com\n.peoplenews.tw\n||peoplenews.tw\n.peopo.org\n||peopo.org\n.percy.in\n.perfectgirls.net\n||perfect-privacy.com\n||perplexity.ai\n.persecutionblog.com\n.persiankitty.com\nphapluan.org\n.phayul.com\n||phayul.com\nphilborges.com\n||phncdn.com\n||photodharma.net\n||photofocus.com\n||phuquocservices.com\n||picacomiccn.com\n.picidae.net\n||img*.picturedip.com\npicturesocial.com\n||pin-cong.com\n.pin6.com\n||pin6.com\n.ping.fm\n||ping.fm\n||pinimg.com\n.pinkrod.com\n||pinoy-n.com\n||pinterest.at\n||pinterest.ca\n||pinterest.co.kr\n||pinterest.co.uk\n.pinterest.com\n||pinterest.com\n||pinterest.com.mx\n||pinterest.de\n||pinterest.dk\n||pinterest.fr\n||pinterest.jp\n||pinterest.nl\n||pinterest.se\n.pipii.tv\n.piposay.com\npiraattilahti.org\n.piring.com\n||pixeldrain.com\n||pixelqi.com\n||css.pixnet.in\n||pixnet.net\n.pixnet.net\n.pk.com\n||placemix.com\n!--.planetsuzy.org\n|http://pictures.playboy.com\n||playboy.com\n.playboyplus.com\n||playboyplus.com\n||player.fm\n.playno1.com\n||playno1.com\n||playpcesor.com\nplays.com.tw\n||plexvpn.pro\n||m.plixi.com\nplm.org.hk\nplunder.com\n.plurk.com\n||plurk.com\n.plus28.com\n.plusbb.com\n.pmatehunter.com\n||pmatehunter.com\n.pmates.com\n||po2b.com\npobieramy.top\n!--||pocoo.org\n||podbean.com\n||podictionary.com\n||poe.com\n.pokerstars.com\n||pokerstars.com\n||pokerstars.net\n||zh.pokerstrategy.com\n||politicalchina.org\n||politicalconsultation.org\n.politiscales.net\n||poloniex.com\n||polymerhk.com\n.popo.tw\n!--||popularpages.net\n||popvote.hk\n||popxi.click\n.popyard.com\n||popyard.org\n.porn.com\n.porn2.com\n.porn5.com\n.pornbase.org\n.pornerbros.com\n||pornhd.com\n.pornhost.com\n.pornhub.com\n||pornhub.com\n.pornhubdeutsch.net\n|http://pornhubdeutsch.net\n||pornmm.net\n.pornoxo.com\n.pornrapidshare.com\n||pornrapidshare.com\n.pornsharing.com\n|http://pornsharing.com\n.pornsocket.com\n.pornstarclub.com\n||pornstarclub.com\n.porntube.com\n.porntubenews.com\n.porntvblog.com\n||porntvblog.com\n.pornvisit.com\n.portablevpn.nl\n||poskotanews.com\n.post01.com\n.post76.com\n||post76.com\n.post852.com\n||post852.com\npostadult.com\n.postimg.org\n||potvpn.com\n||pourquoi.tw\n||powercx.com\n.powerphoto.org\n||www.powerpointninja.com\n||presidentlee.tw\n||cdn.printfriendly.com\n.pritunl.com\nprovpnaccounts.com\n||provpnaccounts.com\n.proxfree.com\n||proxfree.com\nproxyanonimo.es\n.proxynetwork.org.uk\n||proxynetwork.org.uk\n||pts.org.tw\n.pttvan.org\npubu.com.tw\npuffinbrowser.com\npureinsight.org\n.pushchinawall.com\n.putty.org\n||putty.org\n\n!-------------Posterous-----\n||calebelston.com\n||blog.fizzik.com\n||nf.id.au\n||sogrady.me\n||vatn.org\n||ventureswell.com\n||whereiswerner.com\n\n.power.com\n||power.com\npowerapple.com\n||powerapple.com\n||abc.pp.ru\nheix.pp.ru\n||prayforchina.net\n||premeforwindows7.com\n||presentationzen.com\n||prestige-av.com\n.prisoneralert.com\n||pritunl.com\n||privacybox.de\n.private.com/home\n||privateinternetaccess.com\nprivatepaste.com\n||privatepaste.com\nprivatetunnel.com\n||privatetunnel.com\n||privatevpn.com\n||privoxy.org\n||procopytips.com\n||project-syndicate.org\n||proton.me\nprovideocoalition.com\n||prosiben.de\nproxifier.com\n||proxomitron.info\n.proxpn.com\n||proxpn.com\n.proxylist.org.uk\n||proxylist.org.uk\n.proxypy.net\n||proxypy.net\nproxyroad.com\n.proxytunnel.net\n!--403 maybe\n||proyectoclubes.com\nprozz.net\npsblog.name\n||psblog.name\n||pshvpn.com\n||psiphon.ca\n.psiphon3.com\n||psiphon3.com\n.psiphontoday.com\n||pstatic.net\n||pt.im\n.ptt.cc\n||ptt.cc\n||pttgame.com\n.puffstore.com\n.puuko.com\n||pullfolio.com\n.punyu.com/puny\n||pureconcepts.net\n||pureinsight.org\n||purepdf.com\n||purevpn.com\n.purplelotus.org\n.pursuestar.com\n||pursuestar.com\n||nitter.pussthecat.org\n.pussyspace.com\n.putihome.org\n.putlocker.com/file\npwned.com\n||pximg.net\npython.com\n.python.com.tw\n||python.com.tw\npythonhackers.com/p\nss.pythonic.life\n\n!--------------------QQ-------------------------\n.qanote.com\n||qanote.com\n||qbittorrent.org\n||qgirl.com.tw\n||qianbai.tw\n||qiandao.today\n||qiangwaikan.com\n.qi-gong.me\n||qi-gong.me\n!--#921\n||qiangyou.org\n.qidian.ca\n.qienkuen.org\n||qienkuen.org\n||qiwen.lu\nqixianglu.cn\nbbs.qmzdd.com\n.qkshare.com\nqoos.com\n||qoos.com\n||efksoft.com\n||qstatus.com\n||qtweeter.com\n||qtrac.eu\n.quannengshen.org\n||quannengshen.org\nquantumbooter.net\n||quitccp.net\n.quitccp.net\n||quitccp.org\n.quitccp.org\n.quora.com/Chinas-Future\n.quran.com\n|http://quran.com\n.quranexplorer.com\nqusi8.net\n.qvodzy.org\nnemesis2.qx.net/pages/MyEnTunnel\nqxbbs.org\n\n!--------------------RR-------------------------\n||r0.ru\n||radio-canada.ca\n||radio-en-ligne.fr\n||rael.org\nradicalparty.org\n||radio.garden\n||radioaustralia.net.au\n.radiohilight.net\n||radiohilight.net\n||radioline.co\nopml.radiotime.com\n||radiovaticana.org\n||radiovncr.com\n||raggedbanner.com\n||raidcall.com.tw\n.raidtalk.com.tw\n.rainbowplan.org/bbs\n|https://raindrop.io/\n.raizoji.or.jp\n|http://raizoji.or.jp\nrangwang.biz\nrangzen.net\nrangzen.org\n|http://blog.ranxiang.com/\nranyunfei.com\n||ranyunfei.com\n.rapbull.net\n!--|http://rapidgator.net/\n||rapidmoviez.com\nrapidvpn.com\n||rapidvpn.com\n||rarbgprx.org\n.raremovie.cc\n|http://raremovie.cc\n.raremovie.net\n|http://raremovie.net\n||rationalwiki.org\n||rawgit.com\n||rawgithub.com\n!--.rayfme.com/bbs\n||razyboard.com\nrcinet.ca\n.read100.com\n.readingtimes.com.tw\n||readingtimes.com.tw\n||readmoo.com\n.readydown.com\n|http://readydown.com\n.realcourage.org\n.realitykings.com\n||realitykings.com\n.realraptalk.com\n.realsexpass.com\n||reason.com\n.recordhistory.org\n.recovery.org.tw\n|http://online.recoveryversion.org\n||recoveryversion.com.tw\n||red-lang.org\nredballoonsolidarity.org\n||redbubble.com\n.redchinacn.net\n|http://redchinacn.net\nredchinacn.org\nredtube.com\nreferer.us\n||referer.us\n||reflectivecode.com\nrelaxbbs.com\n.relay.com.tw\n.releaseinternational.org\n||religionnews.com\nreligioustolerance.org\nrenminbao.com\n||renminbao.com\n.renyurenquan.org\n||renyurenquan.org\n|http://certificate.revocationcheck.com\nsubacme.rerouted.org\n||resilio.com\n.reuters.com\n||reuters.com\n||reutersmedia.net\n.revleft.com\n||resistchina.org\nretweetist.com\n||retweetrank.com\n!--connectedchina.reuters.com\n!--|http://www.reuters.com/news/video\nrevver.com\n.rfa.org\n||rfa.org\n.rfachina.com\n.rfamobile.org\nrfaweb.org\n||rferl.org\n.rfi.fr\n||rfi.fr\n||rfi.my\n!--.rhcloud.com\n!--Edgecast\n|http://vds.rightster.com/\n.rigpa.org\n.rileyguide.com\n||riku.me\n.ritouki.jp\n||ritter.vg\n.rlwlw.com\n||rlwlw.com\n||rmbl.ws\n.rmjdw.com\n.rmjdw132.info\n.roadshow.hk\n.roboforex.com\n||robustnessiskey.com\n!--||roc-taiwan.org\n||rocket-inc.net\n|http://www2.rocketbbs.com/11/bbs.cgi?id=5mus\n|http://www2.rocketbbs.com/11/bbs.cgi?id=freemgl\n!--||rocmp.org\n||rojo.com\n||ronjoneswriter.com\n||rolfoundation.org\n||rolia.net\n||rolsociety.org\n.roodo.com\n.rosechina.net\n.rotten.com\n||rou.video\n.rsf.org\n||rsf.org\n.rsf-chinese.org\n||rsf-chinese.org\n.rsgamen.org\n||rsshub.app\n||phosphation13.rssing.com\n.rssmeme.com\n||rssmeme.com\n||rtalabel.org\n.rthk.hk\n||rthk.hk\n.rthk.org.hk\n||rthk.org.hk\n.rti.org.tw\n||rti.org.tw\n||rti.tw\n.rtycminnesota.org\n.ruanyifeng.com/blog*some_ways_to_break_the_great_firewall\nrukor.org\n||rule34.xxx\n||rumble.com\n.runbtx.com\n.rushbee.com\n||rusvpn.com\n.ruten.com.tw\n||ruten.com.tw\n||rutracker.net\nrutube.ru\n.ruyiseek.com\n.rxhj.net\n|http://rxhj.net\n\n!--------------------SS-------------------------\n.s1s1s1.com\n||s-cute.com\n.s-dragon.org\n||s1heng.com\n|http://www.s4miniarchive.com\n||s8forum.com\ncdn1.lp.saboom.com\n||sacks.com\nsacom.hk\n||sacom.hk\n||sadpanda.us\n||safechat.com\n||safeguarddefenders.com\n.safervpn.com\n||safervpn.com\n.saintyculture.com\n|http://saintyculture.com\n.saiq.me\n||saiq.me\n||sakuralive.com\n.sakya.org\n.salvation.org.hk\n||salvation.org.hk\n.samair.ru/proxy/type-01\n.sambhota.org\n||cn.sandscotaicentral.com\n||sankakucomplex.com\n||sankei.com\n.sanmin.com.tw\nsapikachu.net\nsavemedia.com\n||savethesounds.info\n.savetibet.de\n||savetibet.de\nsavetibet.fr\nsavetibet.nl\n.savetibet.org\n||savetibet.org\nsavetibet.ru\n.savetibetstore.org\n||savetibetstore.org\n||saveuighur.org\nsavevid.com\n||say2.info\n.sbme.me\n|http://sbme.me\n.sbs.com.au/yourlanguage\n.scasino.com\n|http://www.sciencemag.org/content/344/6187/953\n.sciencenets.com\n.scmp.com\n||scmp.com\n.scmpchinese.com\n||scramble.io\n.scribd.com\n||scribd.com\n||scriptspot.com\n||search.com\n.searchtruth.com\n||searx.me\n||seattlefdc.com\n.secretchina.com\n||secretchina.com\n||secretgarden.no\n.secretsline.biz\n||secretsline.biz\n||secureservercdn.net\n||securetunnel.com\nsecurityinabox.org\n|https://securityinabox.org\n.securitykiss.com\n||securitykiss.com\n||seed4.me\nnews.seehua.com\nseesmic.com\n||seevpn.com\n||seezone.net\nsejie.com\n.sendspace.com\n||sensortower.com\n|http://tweets.seraph.me/\nsesawe.net\n||sesawe.net\n.sesawe.org\n||sethwklein.net\n.setn.com\n.settv.com.tw\nforum.setty.com.tw\n.sevenload.com\n||sevenload.com\n.sex.com\n||sex.com\n.sex-11.com\n||sex3.com\n||sex8.cc\n.sexandsubmission.com\n.sexbot.com\n.sexhu.com\n.sexhuang.com\nsexinsex.net\n||sexinsex.net\n.sextvx.com\n\n!--IP of SexInSex\n67.220.91.15\n67.220.91.18\n67.220.91.23\n\n|http://*.sf.net\n.sfileydy.com\n||sfshibao.com\n.sftindia.org\n.sftuk.org\n||sftuk.org\n||shadeyouvpn.com\nshadow.ma\n.shadowsky.xyz\n.shadowsocks.asia\n||www.shadowsocks.com\n.shadowsocks.com\n||shadowsocks.com.hk\n.shadowsocks.org\n||shadowsocks.org\n||shadowsocks-r.com\n|http://cn.shafaqna.com\n||shahit.biz\n.shambalapost.com\n.shambhalasun.com\n.shangfang.org\n||shangfang.org\nshapeservices.com\n.sharebee.com\n||sharecool.org\n!--||sharkdolphin.com\nsharpdaily.com.hk\n||sharpdaily.com.hk\n.sharpdaily.hk\n.sharpdaily.tw\n.shat-tibet.com\nsheikyermami.com\n.shellfire.de\n||shellfire.de\n.shenshou.org\nshenyun.com\nshenyunperformingarts.org\n||shenyunperformingarts.org\n||shenyunshop.com\nshenzhoufilm.com\n||shenzhoufilm.com\n||shenzhouzhengdao.org\n||sherabgyaltsen.com\n.shiatv.net\n.shicheng.org\nshinychan.com\nshipcamouflage.com\n.shireyishunjian.com\n.shitaotv.org\n||shixiao.org\n||shizhao.org\nshizhao.org\nshkspr.mobi/dabr\n||shodanhq.com\n||shooshtime.com\n.shop2000.com.tw\n||shopee.tw\n.shopping.com\n.showhaotu.com\n.showtime.jp\n||showwe.tw\n.shutterstock.com\n||shutterstock.com\nch.shvoong.com\n.shwchurch.org\n||shwchurch.org\n.shwchurch3.com\n|http://shwchurch3.com\n.siddharthasintent.org\n||sidelinesnews.com\n.sidelinessportseatery.com\n||signal.org\n.sijihuisuo.club\n.sijihuisuo.com\n.silkbook.com\n||simbolostwitter.com\nsimplecd.org\n||simplecd.org\n@@||simplecd.me\nsimpleproductivityblog.com\nbbs.sina.com/\nbbs.sina.com%2F\nblog.sina.com.tw\ndailynews.sina.com/\ndailynews.sina.com%2F\nforum.sina.com.hk\nhome.sina.com\n||magazines.sina.com.tw\nnews.sina.com.hk\nnews.sina.com.tw\nnews.sinchew.com.my\n.sinchew.com.my/node/\n.sinchew.com.my/taxonomy/term\n.singaporepools.com.sg\n||singaporepools.com.sg\n.singfortibet.com\n.singpao.com.hk\nsingtao.com\n||singtao.com\nnews.singtao.ca\n.singtaousa.com\n||singtaousa.com\n!--||cdp.sinica.edu.tw\nsino-monthly.com\n||sinoca.com\n||sinocast.com\nsinocism.com\nsinomontreal.ca\n.sinonet.ca\n.sinopitt.info\n.sinoants.com\n||sinoants.com\n||sinoinsider.com\n.sinoquebec.com\n.sierrafriendsoftibet.org\nsis.xxx\n||sis001.com\nsis001.us\n.site2unblock.com\n||site90.net\n.sitebro.tw\n||sitekreator.com\n||siteks.uk.to\n||sitemaps.org\n.sjrt.org\n|http://sjrt.org\n||sjum.cn\n||sketchappsources.com\n||skimtube.com\n||lab.skk.moe\n||skybet.com\n|http://users.skynet.be/reves/tibethome.html\n.skyking.com.tw\nbbs.skykiwi.com\n|http://www.skype.com/intl/\n|http://www.skype.com/zh-Hant\n||skyvegas.com\n.xskywalker.com\n||xskywalker.com\n||skyxvpn.com\nm.slandr.net\n.slaytizle.com\n.sleazydream.com\n||sleazyfork.org\n||slheng.com\n||slideshare.net\nforum.slime.com.tw\n.slinkset.com\n||slickvpn.com\n.slutload.com\n||smartdnsproxy.com\n.smarthide.com\n||app.smartmailcloud.com\nsmchbooks.com\n.smh.com.au/world/death-of-chinese-playboy-leaves-fresh-scratches-in-party-paintwork-20120903-25a8v\nsmhric.org\n.smith.edu/dalailama\n.smyxy.org\n!--TODO-no-homepage\n||snapchat.com\n.snaptu.com\n||snaptu.com\n||sndcdn.com\nsneakme.net\nsnowlionpub.com\nhome.so-net.net.tw/yisa_tsai\n||soc.mil\n||socialblade.com\n.socks-proxy.net\n||socks-proxy.net\n.sockscap64.com\n||sockslist.net\n.socrec.org\n|http://socrec.org\n.sod.co.jp\n.softether.org\n||softether.org\n.softether-download.com\n||softether-download.com\n||cdn.softlayer.net\n||sogclub.com\nsohcradio.com\n||sohcradio.com\n.sokmil.com\n||sorting-algorithms.com\n.sostibet.org\n.soumo.info\n||soup.io\n@@||static.soup.io\n.sobees.com\n||sobees.com\nsocialwhale.com\n.softether.co.jp\n||softwarebychuck.com\nblog.sogoo.org\nsoh.tw\n||soh.tw\nsohfrance.org\n||sohfrance.org\nchinese.soifind.com\nsokamonline.com\n||solana.com\n.solidaritetibet.org\n.solidfiles.com\n||somee.com\n.songjianjun.com\n||songjianjun.com\n.sonicbbs.cc\n.sonidodelaesperanza.org\n.sopcast.com\n.sopcast.org\n||nakedsecurity.sophos.com\n.sorazone.net\n||sos.org\nbbs.sou-tong.org\n.soubory.com\n|http://soubory.com\n.soul-plus.net\n.soulcaliburhentai.net\n||soulcaliburhentai.net\n||soundcloud.com\n!--|https://soundcloud.com/punkgod\n.soundofhope.kr\nsoundofhope.org\n||soundofhope.org\n||soupofmedia.com\n!--.sourceforge.net\n!-|http://sourceforge.net\n|http://sourceforge.net/p*/shadowsocksgui/\n.sourcewadio.com\n||south-plus.org\nsouthnews.com.tw\nsowers.org.hk\n||wlx.sowiki.net\n||spankbang.com\n.spankingtube.com\n.spankwire.com\n||spb.com\n||speakerdeck.com\n||speedify.com\nspem.at\n||spencertipping.com\n||spendee.com\n||spicevpn.com\n.spideroak.com\n||spideroak.com\n.spike.com\n.spotflux.com\n||spotflux.com\n||spreaker.com\n.spring4u.info\n||spring4u.info\n||springwood.me\n||sproutcore.com\n||sproxy.info\n||squirrelvpn.com\n||srocket.us\n.ss-link.com\n||ss-link.com\n.ssglobal.co/wp\n|http://ssglobal.co\n.ssglobal.me\n||ssh91.com\n.sspro.ml\n|http://sspro.ml\n.ssrshare.com\n||ssrshare.com\n||sss.camp\n!--|http://cdn.sstatic.net/\n||sstm.moe\n||sstmlt.moe\nsstmlt.net\n||sstmlt.net\n|http://stackoverflow.com/users/895245\n.stage64.hk\n||stage64.hk\n||standupfortibet.org\n||standwithhk.org\nstanford.edu/group/falun\nusinfo.state.gov\n||statueofdemocracy.org\n.starfishfx.com\n.starp2p.com\n||starp2p.com\n.startpage.com\n||startpage.com\n.startuplivingchina.com\n|http://startuplivingchina.com\n||static-economist.com\n||stboy.net\n||stc.com.sa\n||steel-storm.com\n.steganos.com\n||steganos.com\n.steganos.net\n.stepchina.com\n!--||stepmania.com\nny.stgloballink.com\nhd.stheadline.com/news/realtime\nsthoo.com\n||sthoo.com\n.stickam.com\nstickeraction.com/sesawe\n.stileproject.com\n.sto.cc\n.stoporganharvesting.org\n||storagenewsletter.com\n.storm.mg\n||storm.mg\n.stoptibetcrisis.net\n||stoptibetcrisis.net\n||storify.com\n||storj.io\n.stormmediagroup.com\n||stoweboyd.com\n||straitstimes.com\nstranabg.com\n||straplessdildo.com\n||streamable.com\n||streamate.com\n||streamingthe.net\nstreema.com/tv/NTDTV_Chinese\ncn.streetvoice.com/article\ncn.streetvoice.com/diary\ncn2.streetvoice.com\ntw.streetvoice.com\n.strikingly.com\n||strongvpn.com\n.strongwindpress.com\n.student.tw/db\n||studentsforafreetibet.org\n||stumbleupon.com\nstupidvideos.com\n||substack.com\n.successfn.com\npanamapapers.sueddeutsche.de\n.sugarsync.com\n||sugarsync.com\n.sugobbs.com\n||sugumiru18.com\n||suissl.com\nsummify.com\n.sumrando.com\n||sumrando.com\nsun1911.com\n||sundayguardianlive.com\n.sunporno.com\n||sunmedia.ca\n||sunporno.com\n.sunskyforum.com\n.sunta.com.tw\n.sunvpn.net\n.suoluo.org\n.superfreevpn.com\n.supervpn.net\n||supervpn.net\n.superzooi.com\n|http://superzooi.com\n.suppig.net\n.suprememastertv.com\n|http://suprememastertv.com\n.surfeasy.com\n||surfeasy.com\n.surfeasy.com.au\n|http://surfeasy.com.au\n||surfshark.com\n||surrenderat20.net\n.svsfx.com\n.swissinfo.ch\n||swissinfo.ch\n.swissvpn.net\n||swissvpn.net\nswitchvpn.net\n||switchvpn.net\n.sydneytoday.com\n||sydneytoday.com\n.sylfoundation.org\n||sylfoundation.org\n||syncback.com\nsysresccd.org\n.sytes.net\nblog.syx86.com/2009/09/puff\nblog.syx86.cn/2009/09/puff\n.szbbs.net\n.szetowah.org.hk\n\n!--------------------TT-------------------------\n||t-g.com\n.t35.com\n.t66y.com\n||t66y.com\n||esg.t91y.com\n.taa-usa.org\n|http://taa-usa.org\n.taaze.tw\n||taaze.tw\n|http://www.tablesgenerator.com/\ntabtter.jp\n.tacem.org\n.taconet.com.tw\n||taedp.org.tw\n.tafm.org\n.tagwa.org.au\ntagwalk.com\n||tagwalk.com\ntahr.org.tw\n.taipeisociety.org\n||taipeisociety.org\n||taipeitimes.com\n||taisounds.com\n.taiwanbible.com\n.taiwancon.com\n.taiwandaily.net\n||taiwandaily.net\n.taiwandc.org\n!--||taiwanembassy.org\n||taiwanhot.net\n.taiwanjustice.com\ntaiwankiss.com\ntaiwannation.com\ntaiwannation.com.tw\n||taiwanncf.org.tw\n||taiwannews.com.tw\n|http://www.taiwanonline.cc/\n!--||taiwantoday.tw\ntaiwantp.net\n||taiwantt.org.tw\ntaiwanus.net\ntaiwanyes.com\ntaiwan-sex.com\n.talk853.com\n.talkboxapp.com\n||talkboxapp.com\n.talkcc.com\n||talkcc.com\n.talkonly.net\n||talkonly.net\n||tamiaode.tk\n||tanc.org\ntangben.com\n.tangren.us\n.taoism.net\n|http://taoism.net\n.taolun.info\n||taolun.info\n.tapatalk.com\n||tapatalk.com\nblog.taragana.com\n.tascn.com.au\n||taup.net\n|http://www.taup.org.tw\n.taweet.com\n||taweet.com\n.tbcollege.org\n||tbcollege.org\n.tbi.org.hk\n.tbicn.org\n.tbjyt.org\n||tbpic.info\n.tbrc.org\ntbs-rainbow.org\n.tbsec.org\n||tbsec.org\ntbskkinabalu.page.tl\n.tbsmalaysia.org\n.tbsn.org\n||tbsn.org\n.tbsseattle.org\n.tbssqh.org\n|http://tbssqh.org\ntbswd.org\n.tbtemple.org.uk\n.tbthouston.org\n.tccwonline.org\n.tcewf.org\ntchrd.org\ntcnynj.org\n||tcpspeed.co\n.tcpspeed.com\n||tcpspeed.com\n.tcsofbc.org\n.tcsovi.org\n.tdm.com.mo\nteamamericany.com\n||techspot.com\n!--OVH\n||techviz.net\n||teck.in\n.teeniefuck.net\nteensinasia.com\n||tehrantimes.com\n.telecomspace.com\n||telegraph.co.uk\n.tenacy.com\n||tenor.com\n||tenzinpalmo.com\n.tew.org\n||tew.org\n||tfiflve.com\n.thaicn.com\n||theatlantic.com\n||theatrum-belli.com\n||cn.theaustralian.com.au\ntheblemish.com\n||thebcomplex.com\n||theblaze.com\n.thebobs.com\n||thebobs.com\n.thechinabeat.org\n||thechinacollection.org\n|http://www.thechinastory.org/yearbooks/yearbook-2012/\n||theconversation.com\n.thedalailamamovie.com\n|http://thedalailamamovie.com\n||thediplomat.com\n||thedw.us\n||theepochtimes.com\n!--||thefreeland.club\nthefrontier.hk/tf\n||theguardian.com\n||thegay.com\n|http://thegioitinhoc.vn/\n.thegly.com\n.thehots.info\nthehousenews.com\n||thehun.net\n.theinitium.com\n||theinitium.com\n||themoviedb.org\n.thenewslens.com\n||thenewslens.com\n.thepiratebay.org\n||thepiratebay.org\n!--||thepiratebay.se\n.theporndude.com\n||theporndude.com\n||theportalwiki.com\n||theprint.in\n||threadreaderapp.com\nthereallove.kr\ntherock.net.nz\n||thesaturdaypaper.com.au\n||thestandnews.com\nthetibetcenter.org\nthetibetconnection.org\n.thetibetmuseum.org\n.thetibetpost.com\n||thetibetpost.com\n!--Tor\n||thetinhat.com\nthetrotskymovie.com\n||thetvdb.com\nthevivekspot.com\n||thewgo.org\n.theync.com\n|http://theync.com\n.thinkingtaiwan.com\n||thinkingtaiwan.com\n.thisav.com\n|http://thisav.com\n.thlib.org\n||thomasbernhard.org\n.thongdreams.com\nthreatchaos.com\n||throughnightsfire.com\n.thumbzilla.com\n||thywords.com\n.thywords.com.tw\ntiananmenmother.org\n.tiananmenduizhi.com\n||tiananmenduizhi.com\n||tiananmenuniv.com\n||tiananmenuniv.net\n||tiandixing.org\n.tianhuayuan.com\n.tianlawoffice.com\n||tianti.io\ntiantibooks.org\n||tiantibooks.org\ntianyantong.org.cn\n.tianzhu.org\n.tibet.at\ntibet.ca\n.tibet.com\n||tibet.com\ntibet.fr\n.tibet.net\n||tibet.net\n||tibet.nu\n.tibet.org\n||tibet.org\n.tibet.sk\n||tibet.org.tw\n||tibet.to\n.tibet-envoy.eu\n||tibet-envoy.eu\n.tibet-foundation.org\n.tibet-house-trust.co.uk\n||tibet-initiative.de\n.tibet-munich.de\n.tibet3rdpole.org\n|http://tibet3rdpole.org\ntibetaction.net\n||tibetaction.net\n.tibetaid.org\ntibetalk.com\n.tibetan.fr\ntibetan-alliance.org\n.tibetanarts.org\n.tibetanbuddhistinstitute.org\n||tibetanbuddhistinstitute.org\n||tibetancommunity.org\n||tibetanentrepreneurs.org\n||tibetanhealth.org\n.tibetanjournal.com\n.tibetanlanguage.org\n.tibetanliberation.org\n||tibetanliberation.org\n.tibetcollection.com\n.tibetanaidproject.org\n.tibetancommunityuk.net\n|http://tibetancommunityuk.net\ntibetanculture.org\ntibetanfeministcollective.org\n.tibetanpaintings.com\n.tibetanphotoproject.com\n.tibetanpoliticalreview.org\n.tibetanreview.net\n|http://tibetansports.org\n.tibetanwomen.org\n|http://tibetanwomen.org\n.tibetanyouth.org\n.tibetanyouthcongress.org\n||tibetanyouthcongress.org\n.tibetcharity.dk\ntibetcharity.in\n.tibetchild.org\n.tibetcity.com\n||tibetcorps.org\n||tibetexpress.net\n||tibetfocus.com\n||tibetfund.org\n.tibetgermany.com\n||tibetgermany.de\n.tibethaus.com\n.tibetheritagefund.org\n||tibethouse.jp\n||tibethouse.org\n||tibethouse.us\n.tibetinfonet.net\n.tibetjustice.org\n.tibetkomite.dk\n||tibetmuseum.org\n||tibetnetwork.org\n||tibetoffice.ch\ntibetoffice.eu\n||tibetoffice.org\n||tibetonline.com\n||tibetoffice.com.au\n||tibetonline.tv\n||tibetoralhistory.org\n||tibetpolicy.eu\n||tibetrelieffund.co.uk\n||tibetsites.com\n||tibetsociety.com\n||tibetsun.com\n||tibetsupportgroup.org\n||tibetswiss.ch\n||tibettelegraph.com\n||tibettimes.net\n||tibettruth.com\n||tibetwrites.org\n.ticket.com.tw\n.tigervpn.com\n||tigervpn.com\n.timdir.com\n|http://timdir.com\n.time.com\n|http://time.com\n!--.time.com/time/time100/leaders/profile/rebel\n!--.time.com/time/specials/packages/article/0,28804\n!--.time.com/time/magazine\n||timesnownews.com\n.timsah.com\n||timtales.com\n||blog.tiney.com\ntintuc101.com\n.tiny.cc\n|http://tiny.cc\ntinychat.com\n||tinypaste.com\n||tipas.net\n.tistory.com\n||tkcs-collins.com\n.tmagazine.com\n||tmagazine.com\n.tmdfish.com\n|http://tmi.me\n.tmpp.org\n|http://tmpp.org\n.tnaflix.com\n||tnaflix.com\n.tngrnow.com\n.tngrnow.net\n.tnp.org\n|http://tnp.org\n.to-porno.com\n||to-porno.com\ntogetter.com\n.tokyo-247.com\n.tokyo-hot.com\n||tokyo-porn-tube.com\n||tokyocn.com\ntw.tomonews.net\n.tongil.or.kr\n.tono-oka.jp\ntonyyan.net\n.toodoc.com\ntoonel.net\ntop81.ws\n.topnews.in\n.toppornsites.com\n|http://toppornsites.com\n.torguard.net\n||torguard.net\n||top.tv\n.topshareware.com\n.topsy.com\n||topsy.com\n||toptip.ca\ntora.to\n.torcn.com\n||torlock.com\n.torproject.org\n||torproject.org\n||torrentkitty.tv\ntorrentprivacy.com\n||torrentprivacy.com\n|http://torrentproject.se\n||torrenty.org\n||torrentz.eu\n||tortoisesvn.net\n||torvpn.com\n||totalvpn.com\n.toutiaoabc.com\ntowngain.com\ntoypark.in\ntoytractorshow.com\n.tparents.org\n.tpi.org.tw\n||tpi.org.tw\n||tradingview.com\n||transparency.org\n||treemall.com.tw\ntrendsmap.com\n||trendsmap.com\n.trialofccp.org\n||trialofccp.org\n.trimondi.de/SDLE\n.trouw.nl\n||trouw.nl\n.trt.net.tr\n||trt.net.tr\ntrtc.com.tw\n.truebuddha-md.org\n||truebuddha-md.org\ntrulyergonomic.com\n.truth101.co.tv\n||truth101.co.tv\n.truthontour.org\n||truthontour.org\n||truthsocial.com\n.truveo.com\n.tsctv.net\n.tsemtulku.com\ntsquare.tv\n.tsu.org.tw\ntsunagarumon.com\n!--|http://www.tsuru-bird.net/\n.tsctv.net\n||tt1069.com\n.tttan.com\n||tttan.com\n||ttv.com.tw\ntu8964.com\n.tubaholic.com\n.tube.com\ntube8.com\n||tube8.com\n.tube911.com\n||tube911.com\n.tubecup.com\n.tubegals.com\n.tubeislam.com\n|http://tubeislam.com\n.tubestack.com\n||tubewolf.com\n.tuibeitu.net\ntuidang.net\n.tuidang.org\n||tuidang.org\n.tuidang.se\nbbs.tuitui.info\n.tumutanzi.com\n|http://tumutanzi.com\n||tumview.com\n.tunein.com\n|http://tunein.com\n||tunnelbear.com\n||tunnelblick.net\n.tunnelr.com\n||tunnelr.com\n||tunsafe.com\ntuitwit.com\n.turansam.org\n.turbobit.net\n||turbobit.net\n.turbohide.com\n||turbohide.com\n||turkistantimes.com\n.tushycash.com\n|http://tushycash.com\n||app.tutanota.com\n.tuvpn.com\n||tuvpn.com\n|http://tuzaijidi.com\n|http://*.tuzaijidi.com\n.tw01.org\n|http://tw01.org\n\n!---Tumblr---\n.tumblr.com\n||tumblr.com\n!--@@||assets.tumblr.com\n!--@@||data.tumblr.com\n!--@@||media.tumblr.com\n!--@@||static.tumblr.com\n!--@@||www.tumblr.com\n||lecloud.net\n|http://cosmic.monar.ch\n||slutmoonbeam.com\n|http://blog.soylent.com\n\n.tv.com\n|http://tv.com\ntvants.com\nforum.tvb.com\nnews.tvb.com/list/world\nnews.tvb.com/local\nnews.tvbs.com.tw\n.tvboxnow.com\n|http://tvboxnow.com/\ntvider.com\n.tvmost.com.hk\n.tvplayvideos.com\n||tvunetworks.com\n.tw-blog.com\n|https://tw-blog.com\n.tw-npo.org\n.twaitter.com\ntwapperkeeper.com\n||twapperkeeper.com\n||twaud.io\n.twaud.io\n.twavi.com\n.twbbs.net.tw\ntwbbs.org\ntwbbs.tw\n||twblogger.com\ntweepmag.com\n.tweepml.org\n||tweepml.org\n.tweetbackup.com\n||tweetbackup.com\ntweetboard.com\n||tweetboard.com\n.tweetboner.biz\n||tweetboner.biz\n.tweetcs.com\n|http://tweetcs.com\n|http://deck.ly\n!-- Operation discontinued\n!--||tweete.net\n!--m.tweete.net\n||mtw.tl\n||tweetedtimes.com\n!-- Operation discontinued\n!--tweetmeme.com\n||tweetmylast.fm\ntweetphoto.com\n||tweetphoto.com\n||tweetrans.com\ntweetree.com\n||tweetree.com\n.tweettunnel.com\n||tweettunnel.com\n||tweetwally.com\ntweetymail.com\n||twelve.today\n.tweez.net\n|http://tweez.net\n||twftp.org\n||twgreatdaily.com\ntwibase.com\n.twibble.de\n||twibble.de\ntwibbon.com\n||twibs.com\n.twicountry.org\n|http://twicountry.org\ntwicsy.com\n.twiends.com\n|http://twiends.com\n.twifan.com\n|http://twifan.com\ntwiffo.com\n||twiffo.com\n.twilightsex.com\ntwilog.org\ntwimbow.com\n||twindexx.com\ntwipple.jp\n||twipple.jp\n||twip.me\ntwishort.com\n||twishort.com\ntwistar.cc\n||twister.net.co\n||twisterio.com\ntwisternow.com\ntwistory.net\ntwitbrowser.net\n||twitcause.com\n||twitgether.com\n||twiggit.org\ntwitgoo.com\ntwitiq.com\n||twitiq.com\n.twitlonger.com\n||twitlonger.com\n|http://tl.gd/\ntwitmania.com\ntwitoaster.com\n||twitoaster.com\n||twitonmsn.com\n!--Same IP\n.twit2d.com\n||twit2d.com\n.twitstat.com\n||twitstat.com\n||firstfivefollowers.com\n||retweeteffect.com\n||tweeplike.me\n||tweepguide.com\n||turbotwitter.com\n.twitvid.com\n||twitvid.com\n|http://twt.tl\ntwittbot.net\n||ads-twitter.com\n||twttr.com\n||twitter4j.org\n.twittercounter.com\n||twittercounter.com\ntwitterfeed.com\n.twittergadget.com\n||twittergadget.com\n.twitterkr.com\n||twitterkr.com\n||twittermail.com\n||twitterrific.com\ntwittertim.es\n||twittertim.es\ntwitthat.com\n||twitturk.com\n.twitturly.com\n||twitturly.com\n.twitzap.com\ntwiyia.com\n||twstar.net\n.twtkr.com\n|http://twtkr.com\n.twnorth.org.tw\n||twreporter.org\ntwskype.com\ntwtrland.com\ntwurl.nl\n.twyac.org\n||twyac.org\n.txxx.com\n.tycool.com\n||tycool.com\n\n!--typepad\n||typepad.com\n@@||www.typepad.com\n@@||static.typepad.com\n||blog.expofutures.com\n||legaltech.law.com\n||blogs.tampabay.com\n||contests.twilio.com\n!-lawprofessors.typepad.com/china_law_prof\n||typora.io\n\n!--------------------UU-------------------------\n.u9un.com\n||u9un.com\n.ubddns.org\n|http://ubddns.org\n||uberproxy.net\n.uc-japan.org\n||uc-japan.org\n.srcf.ucam.org/salon/\n|http://china.ucanews.com/\n||ucdc1998.org\n|http://hum*.uchicago.edu/faculty/ywang/history\n||uderzo.it\n.udn.com\n||udn.com\n||udn.com.tw\nudnbkk.com/bbs\n||uforadio.com.tw\nufreevpn.com\n.ugo.com\n!--ghs\n||uhdwallpapers.org\n||uhrp.org\n.uighur.nl\n||uighur.nl\nuighurbiz.net\n.ulike.net\nukcdp.co.uk\nukliferadio.co.uk\n||ukliferadio.co.uk\nultravpn.fr\n||ultravpn.fr\nultraxs.com\numich.edu/~falun\n||unblock.cn.com\n.unblocker.yt\nunblock-us.com\n||unblock-us.com\n.unblockdmm.com\n|http://unblockdmm.com\n||unblocksit.es\nuncyclomedia.org\n.uncyclopedia.hk/wiki\n|http://uncyclopedia.hk\n!--uncyclopedia.info\n|http://uncyclopedia.tw\nunderwoodammo.com\n||underwoodammo.com\n||unholyknight.com\n.uni.cc\n||cldr.unicode.org\n.unification.net\n.unification.org.tw\n||unirule.cloud\n.unitedsocialpress.com\n.unix100.com\n||unknownspace.org\n.unodedos.com\nunpo.org\n||unstable.icu\n.untraceable.us\n|http://untraceable.us\n||uocn.org\ntor.updatestar.com\n||upghsbc.com\n.upholdjustice.org\n.upload4u.info\nuploaded.net/file\n|http://uploaded.net/file\n|http://uploaded.to/file\n.uploadstation.com/file\n.upmedia.mg\n||upmedia.mg\n.upornia.com\n|http://upornia.com\n||uproxy.org\n||uptodown.com\n.upwill.org\nur7s.com\n||urbandictionary.com\n||urbansurvival.com\nmyshare.url.com.tw/\n||urlborg.com\n||urlparser.com\nus.to\n||usacn.com\n.usaip.eu\n||usaip.eu\n||uscnpm.org\n||uscardforum.com\n||usma.edu\n.usocctn.com\n||ustibetcommittee.org\n.ustream.tv\n||ustream.tv\nusus.cc\n.utopianpal.com\n||utopianpal.com\n.uu-gg.com\n.uvwxyz.xyz\n||uvwxyz.xyz\n.uwants.com\n||uwants.com\n.uwants.net\nuyghur.co.uk\n|http://uyghur-j.org\n||uyghuraa.org\n||uyghuramerican.org\n||uyghurbiz.org\n||uyghurcanadian.ca\n||uyghurcongress.org\n||uyghurpen.org\n||uyghurpress.com\n||uyghurstudies.org\n||uyghurtribunal.com\nuygur.org\n|http://uymaarip.com/\n\n!--------------------VV-------------------------\n||v2fly.org\n.v2ray.com\n||v2ray.com\n||v2raycn.com\n||v2raytech.com\n||valeursactuelles.com\n.van001.com\n.van698.com\n.vanemu.cn\n.vanilla-jp.com\n.vanpeople.com\nvansky.com\n||vaticannews.va\n||vcf-online.org\n||vcfbuilder.org\n.vegasred.com\n.velkaepocha.sk\n.venbbs.com\n.venchina.com\n.venetianmacao.com\n||venetianmacao.com\nveoh.com\n||vercel.app\nmysite.verizon.net\nvermonttibet.org\n.versavpn.com\n||versavpn.com\n||verybs.com\n.vft.com.tw\n.viber.com\n||viber.com\n.vica.info\n.victimsofcommunism.org\n||victimsofcommunism.org\n||vid.me\n||vidble.com\nvideobam.com\n||videobam.com\n.videodetective.com\n.videomega.tv\n||videomega.tv\n.videomo.com\nvideopediaworld.com\n.videopress.com\n.vidinfo.org/video\nvietdaikynguyen.com\n.vijayatemple.org\n||vilavpn.com\nvimeo.com\n||vimeo.com\n||vimperator.org\n||vincnd.com\n||vinniev.com\n|http://www.lib.virginia.edu/area-studies/Tibet/tibet.html\n.virtualrealporn.com\n||virtualrealporn.com\nvisibletweets.com\n|http://ny.visiontimes.com\n.vital247.org\n||viu.com\n.vivahentai4u.net\n||vivaldi.com\n.vivatube.com\n.vivthomas.com\n||vivthomas.com\n.vjav.com\n||vjav.com\n.vjmedia.com.hk\n.vllcs.org\n|http://vllcs.org\n||vmixcore.com\n||vnet.link\n.vocativ.com\nvocn.tv\n||vocus.cc\n||voicettank.org\n.vot.org\n||vot.org\n.vovo2000.com\n|http://vovo2000.com\n.voxer.com\n||voxer.com\n.voy.com\n||vpn.ac\n.vpn4all.com\n||vpn4all.com\n.vpnaccount.org\n|http://vpnaccount.org\n.vpnaccounts.com\n||vpnaccounts.com\n.vpncomparison.org\n.vpncup.com\n||vpncup.com\nvpnbook.com\n.vpncoupons.com\n|http://vpncoupons.com\n.vpndada.com\n||vpndada.com\n.vpnfan.com\nvpnfire.com\n.vpnfires.biz\n.vpnforgame.net\n||vpnforgame.net\n||vpngate.jp\n.vpngate.net\n||vpngate.net\n.vpngratis.net\nvpnhq.com\n||vpnhub.com\n.vpnmaster.com\n||vpnmaster.com\n.vpnmentor.com\n||vpnmentor.com\n.vpninja.net\n||vpninja.net\n.vpnintouch.com\n||vpnintouch.net\nvpnjack.com\n||vpnjack.com\n.vpnpick.com\n||vpnpick.com\n||vpnpop.com\n||vpnpronet.com\n.vpnreactor.com\n||vpnreactor.com\n||vpnreviewz.com\n.vpnsecure.me\n||vpnsecure.me\n.vpnshazam.com\n||vpnshazam.com\n.vpnshieldapp.com\n||vpnshieldapp.com\n.vpnsp.com\n.vpntraffic.com\n.vpntunnel.com\n||vpntunnel.com\n.vpnuk.info\n||vpnuk.info\n||vpnunlimitedapp.com\n.vpnvip.com\n||vpnvip.com\n.vpnworldwide.com\n.vporn.com\n||vporn.com\n.vpser.net\n@@||vpser.net\nvraiesagesse.net\n||vrchat.com\n.vrmtr.com\n||vtunnel.com\n||vuku.cc\n\n!--------------------WW-------------------------\nlists.w3.org/archives/public\n||w3schools.com\n||waffle1999.com\n.wahas.com\n.waigaobu.com\nwaikeung.org/php_wind\n.wailaike.net\n||wainao.me\n.waiwaier.com\n|http://waiwaier.com\n||wallmama.com\nwallornot.org\n||wallpapercasa.com\n.wallproxy.com\n@@||wallproxy.com.cn\n||wallsttv.com\n||waltermartin.com\n||waltermartin.org\n||www.wan-press.org\n||wanderinghorse.net\n||wangafu.net\n||wangjinbo.org\n.wangjinbo.org\nwanglixiong.com\n.wango.org\n||wango.org\nwangruoshui.net\nwww.wangruowang.org\n||want-daily.com\nwapedia.mobi/zhsimp\n||warroom.org\n||waselpro.com\n.watchinese.com\n||watchout.tw\n.wattpad.com\n||wattpad.com\n.makzhou.warehouse333.com\nwasheng.net\n.watch8x.com\n||watchmygf.net\n||wav.tv\n||wd.bible\n.wdf5.com\n||wealth.com.tw\n.wearehairy.com\n.wearn.com\n||wearn.com\n|http://hkcoc.weather.com.hk\n||hudatoriq.web.id\n||web2project.net\nwebbang.net\n.webevader.org\n.webfreer.com\nweblagu.com\n.webjb.org\n.webrush.net\nwebs-tv.net\n.websitepulse.com/help/testtools.china-test\n|http://www.websnapr.com\n.webwarper.net\n|http://webwarper.net\nwebworkerdaily.com\n||wechatlawsuit.com\n.weekmag.info\n||wefightcensorship.org\n.wefong.com\nweiboleak.com\n.weihuo.org\n||weijingsheng.org\n.weiming.info\n||weiming.info\nweiquanwang.org\n|http://weisuo.ws\n.welovecock.com\n||welt.de\n.wemigrate.org\n|http://wemigrate.org\nwengewang.com\n||wengewang.org\n.wenhui.ch\n|http://trans.wenweipo.com/gb/\n.wenxuecity.com\n||wenxuecity.com\n.wenyunchao.com\n||wenyunchao.com\n.westca.com\n||westca.com\n||westernwolves.com\n.westkit.net\n||westpoint.edu\n.westernshugdensociety.org\nwetpussygames.com\n.wetplace.com\nwexiaobo.org\n||wexiaobo.org\nwezhiyong.org\n||wezone.net\n.wforum.com\n||wforum.com/\n.whatblocked.com\n||whatblocked.com\n.wheatseeds.org\n||wheelockslatin.com\n.whippedass.com\n!--|http://who.is/\n.whoer.net\n||whoer.net\nwhotalking.com\nwhylover.com\n||whyx.org\n||wikileaks.ch\n||wikileaks.com\n||wikileaks.de\n||wikileaks.eu\n||wikileaks.lu\n.wikileaks.org\n||wikileaks.org\n||wikileaks.pl\n.wikileaks-forum.com\nwildammo.com\n.williamhill.com\n||collateralmurder.com\n||collateralmurder.org\nwikilivres.info/wiki/%E9%9B%B6%E5%85%AB%E5%AE%AA%E7%AB%A0\n||wikimapia.org\n.wikiwand.com\n||wikiwand.com\n||wikiwiki.jp\n||casino.williamhill.com\n||sports.williamhill.com\n||vegas.williamhill.com\n||willw.net\n||windowsphoneme.com\n.windscribe.com\n||windscribe.com\n||community.windy.com\n||wingy.site\n.winning11.com\nwinwhispers.info\n||wionews.com\n||wiredbytes.com\n||wiredpen.com\n||wireguard.com\n!--||wireshark.org\n.wisdompubs.org\n.wisevid.com\n||wisevid.com\n||whispersystems.org\n.witnessleeteaching.com\n.witopia.net\n.wjbk.org\n||wjbk.org\n||wmflabs.org\n||wn.com\n.wnacg.com\n.wnacg.org\n.wo.tc\n||woeser.com\n.wokar.org\n||wokar.org\nwolfax.com\n||wolfax.com\n||wombo.ai\n||woolyss.com\nwoopie.jp\n||woopie.jp\nwoopie.tv\n||woopie.tv\n||workatruna.com\n.workerdemo.org.hk\n.workerempowerment.org\n||workers.dev\n||workersthebig.net\n.worldcat.org\nworldjournal.com\n.worldvpn.net\n||worldvpn.net\n\n||videopress.com\n.wordpress.com\n|http://*.wordpress.com\n||chenshan20042005.wordpress.com\n||chinaview.wordpress.com\n||cnbbnews.wordpress.com\n||freedominfonetweb.wordpress.com\n||hka8964.wordpress.com\n||hkanews.wordpress.com\n||hqsbnet.wordpress.com\n||hqsbonline.wordpress.com\n||investigating.wordpress.com\n||jobnewera.wordpress.com\n||matthewdgreen.wordpress.com\n||minghuiyw.wordpress.com\n||wo3ttt.wordpress.com\n||sujiatun.wordpress.com\n||xijie.wordpress.com\n||wp.com\n\n!-||wormsculptor.com\n.wow.com\n.wow-life.net\n||wowlegacy.ml\n||wowporn.com\n||wowgirls.com\n.wowrk.com\nwoxinghuiguo.com\n.woyaolian.org\n|http://woyaolian.org\n.wpoforum.com\n||wpoforum.com\n.wqyd.org\n||wqyd.org\nwrchina.org\nwretch.cc\n||writesonic.com\n.wsj.com\n||wsj.com\n.wsj.net\n||wsj.net\n.wsjhk.com\n.wtbn.org\n.wtfpeople.com\nwuerkaixi.com\n||wufafangwen.com\n||wufi.org.tw\n||wuguoguang.com\nwujie.net\nwujieliulan.com\n||wujieliulan.com\nwukangrui.net\n||wuw.red\n||wuyanblog.com\n.wwitv.com\n||wwitv.com\nwzyboy.im/post/160\n\n!--------------------XX-------------------------\n||x.co\n.x-berry.com\n||x-berry.com\n||x-art.com\n||x-wall.org\nx1949x.com\nx365x.com\nxanga.com\n||xbabe.com\n.xbookcn.com\n||xbookcn.com\n||xcafe.in\n||xcity.jp\n.xcritic.com\n|http://cdn*.xda-developers.com\n.xerotica.com\ndestiny.xfiles.to/ubbthreads\n.xfm.pp.ru\n.xgmyd.com\n||xgmyd.com\nxhamster.com\n||xhamster.com\n.xianba.net\n.xianchawang.net\n.xianjian.tw\n|http://xianjian.tw\n.xianqiao.net\n.xiaobaiwu.com\n.xiaochuncnjp.com\n.xiaod.in\n.xiaohexie.com\n||xiaolan.me\n||xiaoma.org\n||xiaohexie.com\n||xiaxiaoqiang.net\nxiezhua.com\n.xihua.es\nforum.xinbao.de/forum\n.xing.com\n|http://xing.com\n||xinjiangpolicefiles.org\n.xinmiao.com.hk\n||xinmiao.com.hk\nxinsheng.net\nxinshijue.com\nxinhuanet.org\n|http://xinyubbs.net\n.xiongpian.com\n.xiuren.org\n||xixicui.icu\nxizang-zhiye.org\nxjp.cc\n||xjp.cc\n||xjtravelguide.com\nxlfmtalk.com\n||xlfmwz.info\n||xml-training-guide.com\nxmovies.com\n||xnxx.com\n!--||xnxx-cdn.com\nxpdo.net\n||xpud.org\n.xrentdvd.com\n.xskywalker.net\n||xtube.com\nblog.xuite.net\nvlog.xuite.net\nxuzhiyong.net\n||xuchao.org\nxuchao.net\n||xuchao.net\nxvideo.cc\n.xvideos.com\n||xvideos.com\n||xvideos-cdn.com\n||xvideos.es\n||xvbelink.com\n||xvinlink.com\n.xkiwi.tk/\n||xsden.info\n.xxbbx.com\n.xxlmovies.com\n||xxx.com\n.xxx.xxx\n|http://xxx.xxx\n.xxxfuckmom.com\n||xxxx.com.au\n.xxxymovies.com\n|http://xxxymovies.com\nxys.org\nxysblogs.org\nxyy69.com\nxyy69.info\n\n!--------------------YY-------------------------\n||y2mate.com\n||yadi.sk\n||yakbutterblues.com\n||yam.com\n||yam.org.tw\n||yande.re\n||disk.yandex.com\n||disk.yandex.ru\n.yanghengjun.com\nyangjianli.com\n.yasni.co.uk\n||yasni.co.uk\n!--||yasukuni.or.jp\n.yayabay.com/forum\n||news.ycombinator.com\n.ydy.com\n.yeahteentube.com\n||yeahteentube.com\n||yecl.net\n||yeelou.com\n||yeeyi.com\nyegle.net\n||yegle.net\n.yes.xxx\n||yes123.com.tw\n||yesasia.com\n||yesasia.com.hk\n.yes-news.com\n|http://yes-news.com\n.yespornplease.com\n||yespornplease.com\n|http://yeyeclub.com\n!--yfrog.com\n||yhcw.net\n.yibada.com\n.yibaochina.com\n.yidio.com\n||yidio.com\n||yigeni.com\nyilubbs.com\n||s.yimg.com\n||xa.yimg.com\n.yingsuoss.com\n.yipub.com\n||yipub.com\nyinlei.org/mt\n.yizhihongxing.com\n||yizhihongxing.com\n.yobt.com\n.yobt.tv\n||yobt.tv\n.yogichen.org\n||yogichen.org\n.yolasite.com\n.yomiuri.co.jp\nyong.hu\n.yorkbbs.ca\n||you.com\n||youxu.info\n.youjizz.com\n||youjizz.com\n.youmaker.com\n||youmaker.com\n.youngpornvideos.com\nyoungspiration.hk\n.youpai.org\n||youpai.org\n.your-freedom.net\n||yourepeat.com\n.yourprivatevpn.com\n||yourprivatevpn.com\n.yousendit.com\n||yousendit.com\n||youthforfreechina.org\n.youthnetradio.org/tmit/forum\nblog.youthwant.com.tw\nme.youthwant.com.tw\nshare.youthwant.com.tw\ntopic.youthwant.com.tw\n.youporn.com\n||youporn.com\n.youporngay.com\n||youporngay.com\n.yourlisten.com\n||yourlisten.com\n.yourlust.com\n||yourlust.com\nyoushun12.com\n.youtubecn.com\nyouversion.com\n||youversion.com\nytht.net\nyuanming.net\n.yuanzhengtang.org\n.yulghun.com\n||yulghun.com\n||yunchao.net\n.yuvutu.com\n||yvesgeleyn.com\n.ywpw.com/forums/history/post/A0/p0/html/227\nyx51.net\n.yyii.org\n||yyii.org\n||yyjlymb.xyz\n||yysub.net\n.yzzk.com\n||yzzk.com\n\n!--------------------ZZ-------------------------\n||z-lib.io\n||z-lib.org\nzacebook.com\n.zalmos.com\n||zalmos.com\n||zannel.com\n.zaobao.com\n||zaobao.com\n||zaobao.com.sg\n.zaozon.com\n||zdnet.com.tw\n.zello.com\n||zello.com\n.zengjinyan.org\n.zenmate.com\n||zenmate.com\n||zenmate.com.ru\n||zerohedge.com\n||zeronet.io\n||zeutch.com\n!--www.zfreet.com/post/usejump-browns.html\n.zfreet.com\n.zgsddh.com\nzgzcjj.net\n.zhanbin.net\n||zhanbin.net\n.zhangboli.net\n||zhangtianliang.com\n||zhanlve.org\nzhenghui.org\n.zhengjian.org\n||zhengjian.org\nzhengwunet.org\nzhenlibu.info\n||zhenlibu.info\n.zhenlibu1984.com\n||zhenlibu1984.com\n|http://zhenxiang.biz\n.zhinengluyou.com\nzhongguo.ca\n|http://zhongguorenquan.org\nzhongguotese.net\n||zhongguotese.net\n||zhongmeng.org\n.zhoushuguang.com\n||zhreader.com\n.zhuangbi.me\n||zhuangbi.me\n.zhuanxing.cn\n||zhuatieba.com\nzhuichaguoji.org\n||zhuichaguoji.org\n||zi.media\n|http://book.zi5.me\n.ziddu.com/download\n||zillionk.com\n.zinio.com\n||zinio.com\n.ziporn.com\n.zippyshare.com\n.zkaip.com\n||zkaip.com\nrealforum.zkiz.com\n!--||zlib.net\n||zmw.cn\n.zodgame.us\nzomobo.net\n.zonaeuropa.com\n||zonaeuropa.com\n||zonghexinwen.com\n.zonghexinwen.net\n||zoogvpn.com\n||zootool.com\n.zoozle.net\n||zophar.net\nwriter.zoho.com\n||zorrovpn.com\n||zpn.im\n||zspeeder.me\n.zsrhao.com\n.zuo.la\n||zuo.la\n||zuobiao.me\n.zuola.com\n||zuola.com\n||zvereff.com\n||zyxel.com\n.zynaima.com\nzyzc9.com\n.zzcartoon.com\n!##############General List End#################\n\n!###########Supplemental List Start#############\n!-----------------URL Keywords------------------\n64memo\naHR0cHM6Ly95ZWNsLm5ldA\nfreenet\n.google.*/falun\nphobos.apple.com*/video\nq=freedom\nq%3Dfreedom\nremembering_tiananmen_20_years\nsearch*safeweb\nq=triangle\nq%3DTriangle\nultrareach\nultrasurf\n!#############Supplemental List End#############\n\n!################Whitelist Start################\n@@||aliyun.com\n@@||baidu.com\n!--@@||bing.com\n@@||chinaso.com\n@@||chinaz.com\n@@|http://nrch.culture.tw/\n\n!---Some are powered by GuXiang (BGP), please comment off if\n!---you encounter connectivity issues.\n@@||adservice.google.com\n!--ISP cache works sometimes, verified at drpeng + gehua.\n@@||dl.google.com\n!--@@||kh.google.com\n!--@@||khm.google.com\n!--@@||khm0.google.com\n!--@@||khm1.google.com\n!--@@||khm2.google.com\n!--@@||khm3.google.com\n!--@@||khmdb.google.com\n@@||tools.google.com\n@@||clientservices.googleapis.com\n@@||fonts.googleapis.com\n!--@@||khm.googleapis.com\n!--@@||khm0.googleapis.com\n!--@@||khm1.googleapis.com\n!--@@||khm2.googleapis.com\n!--@@||khm3.googleapis.com\n!--@@||khmdb.googleapis.com\n@@||storage.googleapis.com\n!--@@||translate.googleapis.com\n@@||update.googleapis.com\n@@||safebrowsing.googleapis.com\n@@||cn.gravatar.com\n!--@@||connectivitycheck.gstatic.com\n!--@@||csi.gstatic.com\n!--@@||fonts.gstatic.com\n!--@@||ssl.gstatic.com\n@@||haosou.com\n@@||ip.cn\n@@||jike.com\n@@|http://translate.google.cn\n@@|http://www.google.cn/maps\n@@||http2.golang.org\n@@||gov.cn\n@@||ocsp.pki.goog\n@@||qq.com\n@@||sina.cn\n@@||sina.com.cn\n@@||sogou.com\n@@||so.com\n@@||soso.com\n@@||uluai.com.cn\n@@||weibo.com\n@@||yahoo.cn\n@@||youdao.com\n@@||zhongsou.com\n@@|http://ime.baidu.jp\n!################Whitelist End##################\n!---------------------EOF-----------------------\n"
  },
  {
    "path": "packages/gui/extra/proxy/domestic-domain-allowlist.txt",
    "content": "[SwitchyOmega Conditions]\n; Require: SwitchyOmega >= 2.3.2\n; Update Date: 2024/12/01\n; Author: Pluwen\n; Usage: https://github.com/FelisCatus/SwitchyOmega/wiki/RuleListUsage\n\n; IP 地址段\n10.*.*.*\n100.64.*.*\n127.*.*.*\n172.16.*.*\n192.168.*.*\n\n; cn 域名都不走代理\n*.cn\n\n; 其他域名\n*.00cdn.com\n*.0daydown.com\n*.0o0.ooo\n*.10010.com\n*.10086cloud.com\n*.114la.com\n*.114yygh.com\n*.115.com\n*.123pan.com\n*.126.com\n*.126.net\n*.127.net\n*.139.com\n*.163.com\n*.163yun.com\n*.1688.com\n*.17173.com\n*.178.com\n*.17ce.com\n*.17font.com\n*.17k.com\n*.199it.com\n*.1ptba.com\n*.1qimg.com\n*.1qmsg.com\n*.1tpic.com\n*.1year.cc\n*.1years.cc\n*.21cn.com\n*.21tb.com\n*.2345.com\n*.2cto.com\n*.3322.cc\n*.3366.com\n*.33ss.tech\n*.360.com\n*.360buy.com\n*.360buyimg.com\n*.360doc.com\n*.360in.com\n*.360safe.com\n*.36kr.com\n*.39.net\n*.3dmgame.com\n*.4399.com\n*.51.la\n*.51.net\n*.5173.com\n*.5173cdn.com\n*.51cto.com\n*.51job.com\n*.51ym.me\n*.52audio.com\n*.52yuwan.com\n*.56.com\n*.58.com\n*.58pic.com\n*.591mogu.com\n*.616pic.com\n*.699pic.com\n*.71.am\n*.7k7k.com\n*.8686c.com\n*.86ps.com\n*.91.com\n*.91118.com\n*.91mjw.com\n*.99.com\n*.a9vg.com\n*.aaplimg.com\n*.abchina.com\n*.accuweather.com\n*.acfun.tv\n*.acg.rip\n*.acg.tv\n*.acggate.net\n*.acgvideo.com\n*.acs.org\n*.aday01.com\n*.adf.ly\n*.agora.io\n*.aicdn.com\n*.aicheren.com\n*.aicoinstorge.com\n*.aipai.com\n*.air-matters.com\n*.air-matters.io\n*.airbnb.com\n*.aiwebcom.com\n*.aixifan.com\n*.aizhan.com\n*.akadns.net\n*.akamaihd.net\n*.akamaized.net\n*.akarin.me\n*.akarin.top\n*.aldwx.com\n*.aliapp.org\n*.alibaba-inc.com\n*.alibaba.com\n*.alibabacloud.com\n*.alibabausercontent.com\n*.alicdn.com\n*.alicloudccp.com\n*.alikunlun.com\n*.alikunlun.net\n*.alimama.com\n*.alipan.com\n*.alipay.com\n*.alipayobjects.com\n*.alisports.com\n*.aliued.com\n*.aliyun.com\n*.aliyuncs.com\n*.aliyundrive.com\n*.aliyunpds.com\n*.allhistory.com\n*.alltuu.com\n*.amap.com\n*.amd.com\n*.ancda.com\n*.animebytes.tv\n*.anjuke.com\n*.anquan.org\n*.ant.design\n*.antfin-inc.com\n*.antfin.com\n*.antpcdn.com\n*.anw.red\n*.anyway.fm\n*.anzhi.com\n*.appclub.in\n*.appgame.com\n*.appinn.com\n*.appinn.net\n*.apple-cloudkit.com\n*.apple.co\n*.apple.com\n*.appletuan.com\n*.appstore.com\n*.aps.org\n*.archlinux.org\n*.archlinuxcn.org\n*.areyoucereal.com\n*.arubanetworks.com\n*.atomicstryker.net\n*.augix.me\n*.autonavi.com\n*.awesome-hd.me\n*.axhub.im\n*.axshare.com\n*.axure.org\n*.axureux.com\n*.b612.net\n*.babybus.com\n*.baidu.com\n*.baidubcr.com\n*.baiducontent.com\n*.baidupan.com\n*.baidupcs.com\n*.baidustatic.com\n*.baiduwp.com\n*.baiduyundns.com\n*.baiduyundns.net\n*.baimiaoapp.com\n*.bankcomm.com\n*.baomihua.com\n*.baomitu.com\n*.baozoumanhua.com\n*.battle.net\n*.bbtree.com\n*.bcebos.com\n*.bcedns.com\n*.bcedns.net\n*.bcy.net\n*.bdatu.com\n*.bdimg.com\n*.bdstatic.com\n*.bdydns.com\n*.bdydns.net\n*.behe.com\n*.beianbeian.com\n*.beisen.com\n*.beitaichufang.com\n*.bejson.com\n*.bendibao.com\n*.bible.com\n*.biliapi.com\n*.biliapi.net\n*.bilibili.com\n*.bilibili.tv\n*.bilicomic.com\n*.biligame.com\n*.biligame.net\n*.bilivideo.com\n*.bitbucket.org\n*.blackyau.cc\n*.blizzard.com\n*.blogchina.com\n*.blogjava.net\n*.bluedoc.io\n*.booking.com\n*.bootcss.com\n*.bqtalk.com\n*.broadcasthe.net\n*.bstatic.com\n*.bt0.com\n*.btdx8.com\n*.btsync.org\n*.btyingshi.com\n*.bumimi.com\n*.bybbs.org\n*.bytecdntp.com\n*.ca001.com\n*.cachemoment.com\n*.cailianpress.com\n*.caiyunapp.com\n*.camera360.com\n*.ccb.com\n*.ccgslb.com\n*.ccgslb.net\n*.cckefu.net\n*.cckefu3.com\n*.cctv.com\n*.cctvpic.com\n*.cdn-apple.com\n*.cdn.hockeyapp.net\n*.cdnbee.com\n*.cdndm.com\n*.cdndm5.com\n*.cdnjs.com\n*.cdnst.net\n*.cdntip.com\n*.cdog.me\n*.ceair.com\n*.cebbank.com\n*.cee.network\n*.chainnews.com\n*.chaoxing.com\n*.chdbits.co\n*.china.com\n*.chinanetcenter.com\n*.chinaso.com\n*.chinassl.net\n*.chinaunix.net\n*.chinauos.com\n*.chinaz.com\n*.chiphell.com\n*.chongdiantou.com\n*.chuangzaoshi.com\n*.chuimg.com\n*.chunyu.mobi\n*.chunyuanfood.com\n*.ciligod.com\n*.citicbank.com\n*.classix-unlimited.co.uk\n*.cli.im\n*.clouddn.com\n*.cloudinary.com\n*.cloudxns.net\n*.cmbchina.com\n*.cmbimg.com\n*.cn-ki.net\n*.cn.engadget.com\n*.cnbeta.com\n*.cnbetacdn.com\n*.cnblogs.com\n*.cnki.net\n*.cnsageo.com\n*.cnzz.com\n*.cnzz.net\n*.code4app.com\n*.coding.io\n*.coding.me\n*.coding.net\n*.coloros.com\n*.comicat.org\n*.coolapk.com\n*.coolpad.com\n*.cootekservice.com\n*.cowtransfer.com\n*.cqvip.com\n*.csair.com\n*.csdn.net\n*.css-js.com\n*.css.net\n*.css.network\n*.ct10000.com\n*.ctrip.com\n*.cupfox.app\n*.d7vg.com\n*.damengxiang.me\n*.dandanplay.com\n*.dangdang.com\n*.daocloud.io\n*.datagrand.com\n*.dbankcdn.com\n*.ddos.cc\n*.ddrk.me\n*.deepin.com\n*.deepin.org\n*.deepinos.org\n*.deliwenku.com\n*.dfcfw.com\n*.dgtle.com\n*.dianping.com\n*.didialift.com\n*.didiglobal.com\n*.dilidili.com\n*.dilidili.wang\n*.dingtalk.com\n*.dingtalkapps.com\n*.diybeta.com\n*.diyvm.com\n*.dji.com\n*.dji.net\n*.dm5.com\n*.dmzj.com\n*.dns.com\n*.dnspao.com\n*.doc88.com\n*.docer.com\n*.docin.com\n*.docschina.org\n*.dopa.com\n*.douban.*\n*.douban.com\n*.douban.fm\n*.doubanio.com\n*.doubleclick.net\n*.douyin.com\n*.douyu.com\n*.douyutv.com\n*.doyoo.net\n*.doyoudo.com\n*.dpfile.com\n*.draw.io\n*.drivergenius.com\n*.dsxys.com\n*.duanwenxue.com\n*.duguletian.com\n*.duokan.com\n*.duoshao.app\n*.duoshao.net\n*.duoshuo.com\n*.duowan.com\n*.dwstatic.com\n*.dxycdn.com\n*.dygod.net\n*.dytt8.net\n*.easou.com\n*.eastmoney.com\n*.ecitic.com\n*.edifier.com\n*.eebbk.com\n*.eeboard.com\n*.eggjs.org\n*.ele.me\n*.elemecdn.com\n*.elong.com\n*.elsevier.com\n*.empornium.me\n*.enkj.com\n*.epicgames.com\n*.epubw.com\n*.erp321.com\n*.etao.com\n*.eudic.net\n*.ewei.com\n*.fang.com\n*.fatetypo.xyz\n*.feiliao.com\n*.feishucdn.com\n*.feng.com\n*.fengkongcloud.com\n*.fengniao.com\n*.ffalcon.com\n*.figma.cool\n*.figmachina.com\n*.figmacn.com\n*.fiio.com\n*.fir.im\n*.firefox.com\n*.fj12379.com\n*.fjdzyz.com\n*.fjgdwl.com\n*.fjhxbank.com\n*.fliggy.com\n*.flomoapp.com\n*.flow.ci\n*.flyertea.com\n*.fnnas.com\n*.fontke.com\n*.foundertype.com\n*.foxirj.com\n*.frdic.com\n*.freebuf.com\n*.freeziti.com\n*.fromgeek.com\n*.futu5.com\n*.futunn.com\n*.fydeos.com\n*.fzzfgjj.com\n*.g-cores.com\n*.galstars.net\n*.gamersky.com\n*.gandi.net\n*.ganji.com\n*.gank.io\n*.gazellegames.net\n*.gcores.com\n*.geetest.com\n*.geilicdn.com\n*.getfedora.org\n*.getpricetag.com\n*.getui.com\n*.gfan.com\n*.gifshow.com\n*.gitee.com\n*.gitee.io\n*.godic.net\n*.golaravel.com\n*.goofish.com\n*.googletagmanager.com\n*.gratisography.com\n*.growingio.com\n*.gtimg.com\n*.guazi.com\n*.guokr.com\n*.gwdang.com\n*.h-ui.net\n*.h2os.com\n*.hacpai.com\n*.haitum.com\n*.halyul.cc\n*.hao123.com\n*.haosou.com\n*.happyeo.com\n*.harmonyos.com\n*.hasee.com\n*.hdb.com\n*.hdbits.org\n*.hdchina.org\n*.hddolby.com\n*.hdfans.org\n*.hdhome.org\n*.hdsky.me\n*.hdslb.com\n*.hdslb.net\n*.hejie.me\n*.heweather.com\n*.hexun.com\n*.hexunimg.com\n*.hicloud.com\n*.hihonor.com\n*.hikvision.com\n*.hitv.com\n*.hiwifi.com\n*.homestyler.com\n*.hommk.com\n*.hongxiu.com\n*.honor.com\n*.hostbuf.com\n*.hostker.com\n*.hotmail.com\n*.houxu.app\n*.huaban.com\n*.huabanimg.com\n*.huanmusic.com\n*.huanqiu.com\n*.huawei.com\n*.huaweicloud.com\n*.huiji.wiki\n*.huijistatic.com\n*.huijiwiki.com\n*.hujiang.com\n*.huomao.com\n*.hupu.com\n*.huxiu.com\n*.huxiucdn.com\n*.huya.com\n*.hxcdn.net\n*.hxjyb.com\n*.hy233.tv\n*.i-meto.com\n*.iapps.im\n*.iaweg.com\n*.iaxure.com\n*.ibm.com\n*.ibruce.info\n*.ibucm.com\n*.icetorrent.org\n*.iciba.com\n*.icloud-content.com\n*.icloud.com\n*.idqqimg.com\n*.ieee.org\n*.iesdouyin.com\n*.ifanr.com\n*.ifanr.in\n*.ifdream.net\n*.ifeng.com\n*.ifengimg.com\n*.ifigma.design\n*.igamecj.com\n*.iguoguo.net\n*.iguxuan.com\n*.iina.io\n*.ijinshan.com\n*.iknoworld.net\n*.iknowwhatyoudownload.com\n*.im9.com\n*.imiku.me\n*.imooc.com\n*.imququ.com\n*.indienova.com\n*.infinitynewtab.com\n*.infoq.com\n*.installbi.me\n*.intercomcdn.com\n*.ip-api.com\n*.ip-cdn.com\n*.ip.la\n*.ip.sb\n*.ip138.com\n*.ipip.net\n*.iplaysoft.com\n*.ipv6-test.com\n*.iqihang.com\n*.iqing.in\n*.iqiyi.com\n*.iqiyipic.com\n*.irs01.com\n*.isharepc.com\n*.it168.com\n*.iteye.com\n*.ithome.com\n*.itjuzi.com\n*.jandan.net\n*.java.com\n*.javaeye.com\n*.jb51.net\n*.jcodecraeer.com\n*.jd.com\n*.jd.hk\n*.jdkindle.com\n*.jdpay.com\n*.jetbrains.com\n*.jfdaily.com\n*.jfrft.com\n*.jhdec.com\n*.jianguoyun.com\n*.jianshu.*\n*.jianshu.com\n*.jianshu.io\n*.jianshuapi.com\n*.jiathis.com\n*.jidian.im\n*.jiemian.com\n*.jikexueyuan.com\n*.jikipedia.com\n*.jinshuju.net\n*.jisuanke.com\n*.jomodns.com\n*.joyneop.xyz\n*.joyyang.com\n*.jpopsuki.eu\n*.jqhtml.com\n*.js.design\n*.jsdelivr.com\n*.juejin.im\n*.juji.tv\n*.kaiyanapp.com\n*.kan300.com\n*.kankan.com\n*.kanzhun.com\n*.kaspersky-labs.com\n*.kcdnvip.com\n*.ke.com\n*.keepcdn.com\n*.keepfrds.com\n*.kekenet.com\n*.kele5240.com\n*.kf5.com\n*.kingsoft.com\n*.kkmh.com\n*.kmf.com\n*.knewone.com\n*.knownsec.com\n*.ksosoft.com\n*.ksyun.com\n*.ksyungslb.com\n*.ku6.com\n*.kuaidi100.com\n*.kuaishou.com\n*.kuaizhan.com\n*.kugou.com\n*.kujiale.com\n*.kunlunaq.com\n*.kunlunar.com\n*.kunlunca.com\n*.kunluncan.com\n*.kunlunea.com\n*.kunlungem.com\n*.kunlungr.com\n*.kunlunhuf.com\n*.kunlunle.com\n*.kunlunli.com\n*.kunlunno.com\n*.kunlunpi.com\n*.kunlunra.com\n*.kunlunsa.com\n*.kunlunsc.com\n*.kunlunsl.com\n*.kunlunso.com\n*.kunlunta.com\n*.kunlunvi.com\n*.kunlunwe.com\n*.kyoceraconnect.com\n*.lackar.com\n*.lagou.com\n*.lanhuapp.com\n*.lanjinger.com\n*.lany.me\n*.lanyus.com\n*.lanzous.com\n*.lanzoux.com\n*.laravel-china.org\n*.layui.com\n*.lbesec.com\n*.le.com\n*.lecloud.com\n*.leetcode-cn.com\n*.lemicp.com\n*.lenovo.net\n*.lenovomobile.com\n*.letv.com\n*.letvimg.com\n*.lianjia.com\n*.liantu.com\n*.liaoxuefeng.com\n*.licdn.com\n*.liepin.com\n*.lifan.ooo\n*.likefont.com\n*.lilithgames.com\n*.linuxidc.com\n*.livechina.com\n*.liyin.date\n*.lizhi.fm\n*.lizhi.io\n*.lkkdesign.com\n*.lncld.net\n*.locoy.com\n*.locvps.com\n*.lofter.com\n*.loj.ac\n*.loli.net\n*.lolinet.com\n*.longzhu.com\n*.lucifr.com\n*.ludashi.com\n*.luogu.org\n*.luojilab.com\n*.luoo.net\n*.lvmama.com\n*.lwl12.com\n*.ly.com\n*.lyjsws.com\n*.m-team.cc\n*.macpaw.com\n*.macrr.com\n*.macw.com\n*.macwk.com\n*.madsrevolution.net\n*.magi.com\n*.mail4geek.com\n*.manmanbuy.com\n*.maoyan.com\n*.maoyun.tv\n*.masadora.net\n*.mastergo.com\n*.maxfox.me\n*.mcbbs.net\n*.mdnice.com\n*.mdui.org\n*.me.com\n*.mediav.com\n*.megvii.com\n*.meican.com\n*.meiin.com\n*.meijutw.com\n*.meipai.com\n*.meiqia.com\n*.meitu.com\n*.meituan.com\n*.meituan.net\n*.meitudata.com\n*.meitustat.com\n*.meixincdn.com\n*.meizu.com\n*.mengniang.org\n*.mgtv.com\n*.mi-img.com\n*.mi.com\n*.miaopai.com\n*.microbit.org\n*.midifan.com\n*.mikanani.me\n*.minapp.com\n*.mindstore.io\n*.mingdao.com\n*.miui.com\n*.miwifi.com\n*.mls-cdn.com\n*.mmstat.com\n*.mmtrix.com\n*.mob.com\n*.mobike.com\n*.moe.im\n*.moe123.net\n*.moegirl.org\n*.moetransit.com\n*.mojidoc.com\n*.moke.com\n*.mokeedev.com\n*.momentcdn.com\n*.momoyu.cc\n*.moonvy.com\n*.morethan.tv\n*.mozilla.org\n*.mp4ba.cc\n*.msftconnecttest.com\n*.mtyun.com\n*.mu6.me\n*.mubu.com\n*.muchong.com\n*.mukewang.com\n*.mumayi.com\n*.muscache.com\n*.mxhichina.com\n*.myanonamouse.net\n*.myapp.com\n*.mydrivers.com\n*.myip.la\n*.myqcloud.com\n*.myzaker.com\n*.mzstatic.com\n*.naixue.com\n*.nanyangpt.com\n*.nature.com\n*.ncore.cc\n*.nekonazo.com\n*.netease.com\n*.netease.im\n*.netseer.com\n*.netspeedtestmaster.com\n*.newsmth.net\n*.ngacn.cc\n*.nim-lang-cn.org\n*.nipic.com\n*.nlark.com\n*.nobook.com\n*.nocode.com\n*.now.sh\n*.nowcoder.com\n*.nowcoder.net\n*.ntp.org\n*.nuomi.com\n*.nvidia.com\n*.nyato.com\n*.obsapp.com\n*.oekaki.so\n*.office.net\n*.office365.com\n*.okii.com\n*.omico.me\n*.onekbit.com\n*.oneplus.com\n*.oneplusbbs.com\n*.onlinedown.net\n*.open-open.com\n*.open.cd\n*.oppo.com\n*.ops.moe\n*.oracle.com\n*.oray.com\n*.oray.net\n*.orayimg.com\n*.oschina.io\n*.oschina.net\n*.ourbits.club\n*.ourdvs.com\n*.ourdvsss.com\n*.oursketch.com\n*.outlook.com\n*.pag.art\n*.paipai.com\n*.panda.tv\n*.panduoduo.net\n*.paperpass.com\n*.passthepopcorn.me\n*.pc6.com\n*.pcbeta.com\n*.pdcicons.ml\n*.pdim.gs\n*.pengyou.com\n*.pexels.com\n*.pgyer.com\n*.phonegap100.com\n*.phpcomposer.com\n*.piaoquantv.com\n*.pingan.com\n*.pingwest.com\n*.planetmeican.com\n*.plex.tv\n*.polyfill.io\n*.pomotodo.com\n*.ppgame.com\n*.pplink.link\n*.ppsimg.com\n*.pptv.com\n*.privatehd.to\n*.processon.com\n*.psbc.com\n*.psnine.com\n*.pstatp.com\n*.pterclub.com\n*.pythonclub.org\n*.qbox.me\n*.qcc.com\n*.qcloud.com\n*.qcloudcdn.com\n*.qcwgg.com\n*.qdaily.com\n*.qdan.me\n*.qdmm.com\n*.qeeyou.com\n*.qhimg.com\n*.qhmsg.com\n*.qhres.com\n*.qianxin.com\n*.qichacha.com\n*.qidian.com\n*.qihucdn.com\n*.qimiaomh.com\n*.qingmang.me\n*.qingting.fm\n*.qiniu.com\n*.qiniucdn.com\n*.qiniudn.com\n*.qiniudns.com\n*.qiniup.com\n*.qiniuts.com\n*.qiuziti.com\n*.qiyi.com\n*.qiyipic.com\n*.qiyukf.com\n*.qnssl.com\n*.qq.com\n*.qqmail.com\n*.qqurl.com\n*.qqzzz.net\n*.quanmingjiexi.com\n*.qudong.com\n*.qunar.com\n*.qweather.com\n*.qyer.com\n*.qyerstatic.com\n*.rapoo.com\n*.rarbg.to\n*.raychase.net\n*.realme.com\n*.redacted.ch\n*.renren.com\n*.renrenche.com\n*.renrendoc.com\n*.researchgate.net\n*.rework.tools\n*.rkecloud.com\n*.rkidc.net\n*.rlcdn.com\n*.rom.mk\n*.ronghub.com\n*.rr.tv\n*.rrfmn.com\n*.rrimg.com\n*.rsc.org\n*.ruanmei.com\n*.ruanyifeng.com\n*.ruby-china.org\n*.ruguoapp.com\n*.runoob.com\n*.s-reader.com\n*.sandai.net\n*.sankuai.com\n*.sarm.net\n*.sb.sb\n*.sc115.com\n*.sciencedirect.com\n*.sciencemag.org\n*.scofd.com\n*.scomper.me\n*.sdbeta.com\n*.sdo.com\n*.seafile.com\n*.seele.tech\n*.segmentfault.com\n*.sekorm.com\n*.servicewechat.com\n*.sf-express.com\n*.shejijia.com\n*.shidianguji.com\n*.shikezhi.com\n*.shimo.im\n*.shiyanlou.com\n*.shssp.org\n*.shxibank.com\n*.shyywz.com\n*.sigmaaldrich.com\n*.sigujiexi.com\n*.sina.com\n*.sinaapp.com\n*.since1989.org\n*.siweiearth.com\n*.sketchchina.com\n*.slack.com\n*.sm.ms\n*.smart2pay.com\n*.smartgslb.com\n*.smartisan.com\n*.smzdm.com\n*.snapdrop.net\n*.snssdk.com\n*.snwx.com\n*.so.com\n*.sobot.com\n*.sogo.com\n*.sogou.com\n*.sogoucdn.com\n*.sohu-inc.com\n*.sohu.com\n*.sohucs.com\n*.soku.com\n*.solidot.org\n*.songshuhui.net\n*.soso.com\n*.soufun.com\n*.sourcegcdn.com\n*.speedtest.net\n*.springer.com\n*.springerlink.com\n*.springleaf-biomax.com\n*.springsunday.net\n*.sspai.com\n*.stargame.com\n*.staticdn.net\n*.staticfile.org\n*.steamcn.com\n*.steamcontent.com\n*.subhd.tv\n*.sui.com\n*.suning.com\n*.surface.wiki\n*.sznews.com\n*.t.tt\n*.taichi.graphics\n*.taihe.com\n*.takungpao.com\n*.talkingdata.com\n*.tangdou.com\n*.tangdouddn.com\n*.tanx.com\n*.taobao.com\n*.taobao.org\n*.taobaocdn.com\n*.tapdb.net\n*.tapimg.com\n*.taptap.com\n*.tbcache.com\n*.tcdn.qq.com\n*.tcl.com\n*.teambition.com\n*.teamviewer.com\n*.tencent-cloud.com\n*.tencent-cloud.net\n*.tencent.com\n*.tencentmind.com\n*.tengshiauto.com\n*.tenpay.com\n*.tenxcloud.com\n*.test-ipv6.com\n*.tgbus.com\n*.thefuture.top\n*.thomsonreuters.com\n*.tianyancha.com\n*.tietuku.com\n*.tigerlust.com\n*.tingyun.com\n*.tinyservices.net\n*.tinywow.com\n*.tjupt.org\n*.tmall.com\n*.tmall.hk\n*.todesk.com\n*.tool.lu\n*.tophub.today\n*.totheglory.im\n*.toushibao.com\n*.toutiao.com\n*.toutiao.io\n*.toutiaoimg.com\n*.tower.im\n*.trontv.com\n*.truevue.org\n*.ttt.tt\n*.tuchong.com\n*.tudou.com\n*.tuicool.com\n*.tuna.moe\n*.tuniu.com\n*.typeisbeautiful.com\n*.u9u9.com\n*.ubuntukylin.com\n*.ucweb.com\n*.ucxinwen.com\n*.udache.com\n*.udacity.com\n*.uedna.com\n*.uigreat.com\n*.uisdc.com\n*.uisheji.com\n*.umeng.com\n*.umengcloud.com\n*.umetrip.com\n*.undraw.co\n*.uning.com\n*.upai.com\n*.upaiyun.com\n*.upyun.com\n*.ustclug.org\n*.uuu.moe\n*.uxengine.net\n*.v-56.com\n*.vamaker.com\n*.vaptcha.net\n*.veryzhun.com\n*.vhall.com\n*.vhallyun.com\n*.videojj.com\n*.viosey.com\n*.vip.com\n*.visualhunt.com\n*.visualstudio.com\n*.vite.org\n*.vjudge.net\n*.vmall.com\n*.vmware.com\n*.voidcn.com\n*.vostic.net\n*.vpgame.com\n*.vpgcdn.com\n*.vpsmm.com\n*.vss.im\n*.vzan.com\n*.wacai.com\n*.waerfa.com\n*.walklake.com\n*.wallhaven.cc\n*.wandoujia.com\n*.wangsu.com\n*.wanmei.com\n*.weather.com\n*.web.guoweishu.net\n*.webfont.com\n*.webofknowledge.com\n*.wechat.com\n*.weibo.com\n*.weibocdn.com\n*.weico.cc\n*.weidian.com\n*.weidown.com\n*.weidunewtab.com\n*.weiosx.com\n*.weixinbridge.com\n*.weiyun.com\n*.westlakemuseum.com\n*.whatismyip.com\n*.wht.im\n*.wiley.com\n*.windows.com\n*.windowsupdate.com\n*.wisenjoy.com\n*.wjx.top\n*.wodemo.com\n*.wolai.com\n*.woozooo.com\n*.woshipm.com\n*.woyoo.com\n*.wps.com\n*.wscdns.com\n*.wulihub.com\n*.wxb.com\n*.xbongbong.com\n*.xclient.info\n*.xdccpro.com\n*.xf9168.com\n*.xiachufang.com\n*.xiami.com\n*.xiami.net\n*.xiaoe-tech.com\n*.xiaoe-tools.com\n*.xiaohongshu.com\n*.xiaoka.tv\n*.xiaomark.com\n*.xiaomi.com\n*.xiaomi.net\n*.xiaomicp.com\n*.xiaomiyoupin.com\n*.xiaotu.io\n*.xiazaiziti.com\n*.ximalaya.com\n*.xinhuanet.com\n*.xiniu.com\n*.xinquji.com\n*.xitu.io\n*.xiya.vip\n*.xldns.net\n*.xmac.app\n*.xmcdn.com\n*.xnpic.com\n*.xpcha.com\n*.xuanfengge.com\n*.xueqiu.com\n*.xuetangx.com\n*.xujc.com\n*.xunlei.com\n*.xunyou.com\n*.xx1t.com\n*.xxsy.net\n*.xycdn.com\n*.xywy.com\n*.yamibo.com\n*.yangkeduo.com\n*.yangwangauto.com\n*.yaohuo.me\n*.yd-jxt.com\n*.ydstatic.com\n*.yecdn.com\n*.yesky.com\n*.yeyfree.com\n*.yfscdn.net\n*.yfsvdn.net\n*.yhd.com\n*.yi2.net\n*.yiche.com\n*.yihaodianimg.com\n*.yinxiang.com\n*.yinyuetai.com\n*.yizhibo.com\n*.ykimg.com\n*.ylmf.net\n*.youdao.com\n*.youku.com\n*.youlebe.com\n*.youzan.com\n*.yunjiasu-cdn.net\n*.yunpian.com\n*.yunshipei.com\n*.yuque.com\n*.yuwantech.com\n*.yxt.com\n*.yy.com\n*.z-bank.com\n*.zaih.com\n*.zanata.org\n*.zanmeishi.com\n*.zdic.net\n*.zealer.com\n*.zh.moegirl.org\n*.zhan.com\n*.zhangxinxu.com\n*.zhangyao.name\n*.zhangzishi.cc\n*.zhanqi.tv\n*.zhaopin.com\n*.zhihu.com\n*.zhihuishu.com\n*.zhimap.com\n*.zhimg.com\n*.zhipin.com\n*.zhiye.com\n*.zhiziyun.com\n*.zhongguose.com\n*.zhuihd.com\n*.zhujike.com\n*.zijieapi.com\n*.zimuzu.tv\n*.zku.net\n*.znyj365.com\n*.zto.com\n"
  },
  {
    "path": "packages/gui/extra/scripts/github.script",
    "content": "// ==UserScript==\n// @name         Github 增强 - 高速下载\n// @name:zh-CN   Github 增强 - 高速下载\n// @name:zh-TW   Github 增強 - 高速下載\n// @name:en      Github Enhancement - High Speed Download\n// @version      2.5.21\n// @author       X.I.U\n// @description        高速下载 Git Clone/SSH、Release、Raw、Code(ZIP) 等文件 (公益加速)、项目列表单文件快捷下载 (☁)、添加 git clone 命令\n// @description:zh-CN  高速下载 Git Clone/SSH、Release、Raw、Code(ZIP) 等文件 (公益加速)、项目列表单文件快捷下载 (☁)\n// @description:zh-TW  高速下載 Git Clone/SSH、Release、Raw、Code(ZIP) 等文件 (公益加速)、項目列表單文件快捷下載 (☁)\n// @description:en     High-speed download of Git Clone/SSH, Release, Raw, Code(ZIP) and other files (Based on public welfare), project list file quick download (☁)\n// @match        *://github.com/*\n// @match        *://hub.incept.pw/*\n// @match        *://hub.nuaa.cf/*\n// @match        *://hub.yzuu.cf/*\n// @match        *://hub.scholar.rr.nu/*\n// @match        *://dgithub.xyz/*\n// @match        *://kkgithub.com/*\n// @icon         data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAMAAABEpIrGAAAABGdBTUEAALGPC/xhBQAAAAFzUkdCAK7OHOkAAACEUExURUxpcRgWFhsYGBgWFhcWFh8WFhoYGBgWFiUlJRcVFRkWFhgVFRgWFhgVFRsWFhgWFigeHhkWFv////////////r6+h4eHv///xcVFfLx8SMhIUNCQpSTk/r6+jY0NCknJ97e3ru7u+fn51BOTsPCwqGgoISDg6empmpoaK2srNDQ0FhXV3eXcCcAAAAXdFJOUwCBIZXMGP70BuRH2Ze/LpIMUunHkpQR34sfygAAAVpJREFUOMt1U+magjAMDAVb5BDU3W25b9T1/d9vaYpQKDs/rF9nSNJkArDA9ezQZ8wPbc8FE6eAiQUsOO1o19JolFibKCdHGHC0IJezOMD5snx/yE+KOYYr42fPSufSZyazqDoseTPw4lGJNOu6LBXVUPBG3lqYAOv/5ZwnNUfUifzBt8gkgfgINmjxOpgqUA147QWNaocLniqq3QsSVbQHNp45N/BAwoYQz9oUJEiE4GMGfoBSMj5gjeWRIMMqleD/CAzUHFqTLyjOA5zjNnwa4UCEZ2YK3khEcBXHjVBtEFeIZ6+NxYbPqWp1DLKV42t6Ujn2ydyiPi9nX0TTNAkVVZ/gozsl6FbrktkwaVvL2TRK0C8Ca7Hck7f5OBT6FFbLATkL2ugV0tm0RLM9fedDvhWstl8Wp9AFDjFX7yOY/lJrv8AkYuz7fuP8dv9izCYH+x3/LBnj9fYPBTpJDNzX+7cAAAAASUVORK5CYII=\n// @grant        GM_registerMenuCommand\n// @grant        GM_unregisterMenuCommand\n// @grant        GM_openInTab\n// @grant        GM_getValue\n// @grant        GM_setValue\n// @grant        GM_notification\n// @grant        window.onurlchange\n// @sandbox      JavaScript\n// @license      GPL-3.0 License\n// @run-at       document-end\n// @namespace    https://greasyfork.org/scripts/412245\n// @supportURL   https://github.com/XIU2/UserScript\n// @homepageURL  https://github.com/XIU2/UserScript\n// ==/UserScript==\n\n(function() {\n\t'use strict';\n\tvar backColor = '#ffffff', fontColor = '#888888', menu_rawFast = GM_getValue('xiu2_menu_raw_fast'), menu_rawFast_ID, menu_rawDownLink_ID, menu_gitClone_ID, menu_feedBack_ID;\n\tconst download_url_us = [\n\t\t//['https://gh.h233.eu.org/https://github.com', '美国', '[美国 Cloudflare CDN] - 该公益加速源由 [@X.I.U/XIU2] 提供'],\n\t\t//['https://gh.api.99988866.xyz/https://github.com', '美国', '[美国 Cloudflare CDN] - 该公益加速源由 [hunshcn/gh-proxy] 提供'], // 官方演示站用的人太多了\n\t\t['https://gh.ddlc.top/https://github.com', '美国', '[美国 Cloudflare CDN] - 该公益加速源由 [@mtr-static-official] 提供'],\n\t\t//['https://gh2.yanqishui.work/https://github.com', '美国', '[美国 Cloudflare CDN] - 该公益加速源由 [@HongjieCN] 提供'], // 解析错误\n\t\t['https://dl.ghpig.top/https://github.com', '美国', '[美国 Cloudflare CDN] - 该公益加速源由 [feizhuqwq.com] 提供'],\n\t\t//['https://gh.flyinbug.top/gh/https://github.com', '美国', '[美国 Cloudflare CDN] - 该公益加速源由 [Mintimate] 提供'], // 错误\n\t\t['https://slink.ltd/https://github.com', '美国', '[美国 Cloudflare CDN] - 该公益加速源由 [知了小站] 提供'],\n\t\t//['https://git.xfj0.cn/https://github.com', '美国', '[美国 Cloudflare CDN] - 该公益加速源由 [佚名] 提供'], // 无解析\n\t\t['https://gh.con.sh/https://github.com', '美国', '[美国 Cloudflare CDN] - 该公益加速源由 [佚名] 提供'],\n\t\t//['https://ghps.cc/https://github.com', '美国', '[美国 Cloudflare CDN] - 该公益加速源由 [佚名] 提供'], // 提示 blocked\n\t\t//['https://gh-proxy.com/https://github.com', '美国', '[美国 Cloudflare CDN] - 该公益加速源由 [佚名] 提供'], // 502\n\t\t['https://cors.isteed.cc/github.com', '美国', '[美国 Cloudflare CDN] - 该公益加速源由 [@Lufs\\'s] 提供'],\n\t\t['https://hub.gitmirror.com/https://github.com', '美国', '[美国 Cloudflare CDN] - 该公益加速源由 [GitMirror] 提供'],\n\t\t['https://sciproxy.com/github.com', '美国', '[美国 Cloudflare CDN] - 该公益加速源由 [sciproxy.com] 提供'],\n\t\t['https://ghproxy.cc/https://github.com', '美国', '[美国 洛杉矶] - 该公益加速源由 [@yionchiii lau] 提供'],\n\t\t['https://cf.ghproxy.cc/https://github.com', '美国', '[美国 Cloudflare CDN] - 该公益加速源由 [@yionchiii lau] 提供'],\n\t\t['https://gh.jiasu.in/https://github.com', '美国', '[美国 Cloudflare CDN] - 该公益加速源由 [@0-RTT] 提供'],\n\t\t['https://dgithub.xyz', '美国', '[美国 西雅图] - 该公益加速源由 [dgithub.xyz] 提供'],\n\t\t//['https://download.fgit.cf', '美国', '[美国 洛杉矶] - 该公益加速源由 [FastGit 群组成员] 提供'], // 被投诉挂了\n\t\t['https://download.nuaa.cf', '美国', '[美国 洛杉矶] - 该公益加速源由 [FastGit 群组成员] 提供'],\n\t\t['https://download.scholar.rr.nu', '美国', '[美国 纽约] - 该公益加速源由 [FastGit 群组成员] 提供'],\n\t\t//['https://download.njuu.cf', '美国', '[美国 纽约] - 该公益加速源由 [FastGit 群组成员] 提供'], // 域名挂了\n\t\t['https://download.yzuu.cf', '美国', '[美国 纽约] - 该公益加速源由 [FastGit 群组成员] 提供']\n\t];\n\n\tconst download_url = [\n\t\t//['https://download.fastgit.org', '德国', '[德国] - 该公益加速源由 [FastGit] 提供&#10;&#10;提示：希望大家尽量多使用前面的美国节点（每次随机 4 个来负载均衡），&#10;避免流量都集中到亚洲公益节点，减少成本压力，公益才能更持久~', 'https://archive.fastgit.org'], // 证书过期\n\t\t['https://mirror.ghproxy.com/https://github.com', '韩国', '[日本、韩国、德国等]（CDN 不固定） - 该公益加速源由 [ghproxy] 提供&#10;&#10;提示：希望大家尽量多使用前面的美国节点（每次随机 负载均衡），&#10;避免流量都集中到亚洲公益节点，减少成本压力，公益才能更持久~'],\n\t\t['https://ghproxy.net/https://github.com', '日本', '[日本 大阪] - 该公益加速源由 [ghproxy] 提供&#10;&#10;提示：希望大家尽量多使用前面的美国节点（每次随机 负载均衡），&#10;避免流量都集中到亚洲公益节点，减少成本压力，公益才能更持久~'],\n\t\t['https://kkgithub.com', '香港', '[中国香港、日本、新加坡等] - 该公益加速源由 [help.kkgithub.com] 提供&#10;&#10;提示：希望大家尽量多使用前面的美国节点（每次随机 4 个来负载均衡），&#10;避免流量都集中到亚洲公益节点，减少成本压力，公益才能更持久~'],\n\t\t//['https://download.incept.pw', '香港', '[中国香港] - 该公益加速源由 [FastGit 群组成员] 提供&#10;&#10;提示：希望大家尽量多使用前面的美国节点（每次随机 4 个来负载均衡），&#10;避免流量都集中到亚洲公益节点，减少成本压力，公益才能更持久~'] // ERR_SSL_PROTOCOL_ERROR\n\t];\n\n\tconst clone_url = [\n\t\t['https://gitclone.com', '国内', '[中国 国内] - 该公益加速源由 [GitClone] 提供&#10;&#10; - 缓存：有&#10; - 首次比较慢，缓存后较快'],\n\t\t['https://kkgithub.com', '香港', '[中国香港、日本、新加坡等] - 该公益加速源由 [help.kkgithub.com] 提供&#10;&#10; - 缓存：无（或时间很短）'],\n\t\t['https://hub.incept.pw', '香港', '[中国香港、美国] - 该公益加速源由 [FastGit 群组成员] 提供'],\n\t\t['https://mirror.ghproxy.com/https://github.com', '韩国', '[日本、韩国、德国等]（CDN 不固定） - 该公益加速源由 [ghproxy] 提供&#10;&#10; - 缓存：无（或时间很短）'],\n\t\t//['https://gh-proxy.com/https://github.com', '韩国', '[韩国] - 该公益加速源由 [ghproxy] 提供&#10;&#10; - 缓存：无（或时间很短）'],\n\t\t['https://githubfast.com', '韩国', '[韩国] - 该公益加速源由 [Github Fast] 提供&#10;&#10; - 缓存：无（或时间很短）'],\n\t\t['https://ghproxy.net/https://github.com', '日本', '[日本 大阪] - 该公益加速源由 [ghproxy] 提供&#10;&#10; - 缓存：无（或时间很短）'],\n\t\t['https://github.moeyy.xyz/https://github.com', '新加坡', '[新加坡、中国香港、日本等]（CDN 不固定） - 该公益加速源由 [Moeyy] 提供&#10;&#10; - 缓存：无（或时间很短）'],\n\t\t//['https://slink.ltd/https://github.com', '美国', '[美国 Cloudflare CDN] - 该公益加速源由 [知了小站] 提供'] // 暂无必要\n\t\t//['https://hub.gitmirror.com/https://github.com', '美国', '[美国 Cloudflare CDN] - 该公益加速源由 [GitMirror] 提供'], // 暂无必要\n\t\t//['https://sciproxy.com/github.com', '美国', '[美国 Cloudflare CDN] - 该公益加速源由 [sciproxy.com] 提供'], // 暂无必要\n\t\t//['https://ghproxy.cc/https://github.com', '美国', '[美国 洛杉矶] - 该公益加速源由 [@yionchiii lau] 提供'], // 暂无必要\n\t\t//['https://cf.ghproxy.cc/https://github.com', '美国', '[美国 Cloudflare CDN] - 该公益加速源由 [@yionchiii lau] 提供'], // 暂无必要\n\t\t//['https://gh.jiasu.in/https://github.com', '美国', '[美国 Cloudflare CDN] - 该公益加速源由 [@0-RTT] 提供'], // 暂无必要\n\t\t//['https://dgithub.xyz', '美国', '[美国 西雅图] - 该公益加速源由 [dgithub.xyz] 提供'], // 暂无必要\n\t\t//['https://hub.fgit.cf', '美国', '[美国 洛杉矶] - 该公益加速源由 [FastGit 群组成员] 提供'], // 被投诉挂了\n\t\t//['https://hub.nuaa.cf', '美国', '[美国 洛杉矶] - 该公益加速源由 [FastGit 群组成员] 提供'], // 暂无必要\n\t\t//['https://hub.scholar.rr.nu', '美国', '[美国 纽约] - 该公益加速源由 [FastGit 群组成员] 提供'], // 暂无必要\n\t\t//['https://hub.njuu.cf', '美国', '[美国 纽约] - 该公益加速源由 [FastGit 群组成员] 提供'], // 域名挂了\n\t\t//['https://hub.yzuu.cf', '美国', '[美国 纽约] - 该公益加速源由 [FastGit 群组成员] 提供'], // 暂无必要\n\t\t//['https://hub.0z.gs', '美国', '[美国 Cloudflare CDN]'], // 域名无解析\n\t\t//['https://hub.shutcm.cf', '美国', '[美国 Cloudflare CDN]'] // 连接超时\n\t];\n\n\tconst clone_ssh_url = [\n\t\t['ssh://git@ssh.github.com:443/', 'Github 原生', '[日本、新加坡等] - Github 官方提供的 443 端口的 SSH（依然是 SSH 协议），适用于限制访问 22 端口的网络环境'],\n\t\t['git@ssh.fastgit.org:', '香港', '[中国 香港] - 该公益加速源由 [FastGit] 提供']\n\t\t//['git@git.zhlh6.cn:', '美国', '[美国 洛杉矶]'] // 挂了\n\t];\n\n\tconst raw_url = [\n\t\t['https://raw.githubusercontent.com', 'Github 原生', '[日本 东京]'],\n\t\t['https://raw.kkgithub.com', '香港', '[中国香港、日本、新加坡等] - 该公益加速源由 [help.kkgithub.com] 提供&#10;&#10; - 缓存：无（或时间很短）'],\n\t\t['https://mirror.ghproxy.com/https://raw.githubusercontent.com', '韩国', '[日本、韩国、德国等]（CDN 不固定） - 该公益加速源由 [ghproxy] 提供&#10;&#10; - 缓存：无（或时间很短）'],\n\t\t//['https://gh-proxy.com/https://raw.githubusercontent.com', '韩国 2', '[韩国] - 该公益加速源由 [ghproxy] 提供&#10;&#10; - 缓存：无（或时间很短）'],\n\t\t['https://ghproxy.net/https://raw.githubusercontent.com', '日本 1', '[日本 大阪] - 该公益加速源由 [ghproxy] 提供&#10;&#10; - 缓存：无（或时间很短）'],\n\t\t['https://fastly.jsdelivr.net/gh', '日本 2', '[日本 东京] - 该公益加速源由 [JSDelivr CDN] 提供&#10;&#10; - 缓存：有&#10; - 不支持大小超过 50 MB 的文件&#10; - 不支持版本号格式的分支名（如 v1.2.3）'],\n\t\t['https://fastraw.ixnic.net', '日本 3', '[日本 大阪] - 该公益加速源由 [FastGit 群组成员] 提供&#10;&#10; - 缓存：无（或时间很短）'],\n\t\t//['https://gcore.jsdelivr.net/gh', '美国', '[美国 Cloudflare CDN] - 该公益加速源由 [JSDelivr CDN] 提供&#10;&#10; - 缓存：有&#10; - 不支持大小超过 50 MB 的文件&#10; - 不支持版本号格式的分支名（如 v1.2.3）'], // 变成 美国 Cloudflare CDN 了\n\t\t['https://cdn.jsdelivr.us/gh', '其他 1', '[韩国、美国、马来西亚、罗马尼亚等]（CDN 不固定） - 该公益加速源由 [@ayao] 提供&#10;&#10; - 缓存：有'],\n\t\t//['https://jsdelivr.b-cdn.net/gh', '其他 2', '[中国香港、台湾、日本、新加坡等]（CDN 不固定） - 该公益加速源由 [@rttwyjz] 提供&#10;&#10; - 缓存：有'],\n\t\t['https://github.moeyy.xyz/https://raw.githubusercontent.com', '其他 3', '[新加坡、中国香港、日本等]（CDN 不固定）&#10;&#10; - 缓存：无（或时间很短）'],\n\t\t['https://raw.cachefly.998111.xyz', '其他 4', '[新加坡、日本、印度等]（Anycast CDN 不固定） - 该公益加速源由 [@XxXxXxXxXxXxXxXxXxXxXxXxXxXxXxXxXxXxX0] 提供&#10;&#10; - 缓存：有（约 12 小时）'],\n\t\t//['https://raw.incept.pw', '香港', '[中国香港、美国] - 该公益加速源由 [FastGit 群组成员] 提供&#10;&#10; - 缓存：无（或时间很短）'], // ERR_SSL_PROTOCOL_ERROR\n\t\t//['https://ghproxy.cc/https://raw.githubusercontent.com', '美国', '[美国 洛杉矶] - 该公益加速源由 [@yionchiii lau] 提供'], // 暂无必要\n\t\t//['https://cf.ghproxy.cc/https://raw.githubusercontent.com', '美国', '[美国 Cloudflare CDN] - 该公益加速源由 [@yionchiii lau] 提供'], // 暂无必要\n\t\t//['https://gh.jiasu.in/https://raw.githubusercontent.com', '美国', '[美国 Cloudflare CDN] - 该公益加速源由 [@0-RTT] 提供'], // 暂无必要\n\t\t//['https://dgithub.xyz', '美国', '[美国 西雅图] - 该公益加速源由 [dgithub.xyz] 提供'], // 暂无必要\n\t\t//['https://raw.fgit.cf', '美国', '[美国 洛杉矶] - 该公益加速源由 [FastGit 群组成员] 提供&#10;&#10; - 缓存：无（或时间很短）'], // 被投诉挂了\n\t\t//['https://raw.nuaa.cf', '美国', '[美国 洛杉矶] - 该公益加速源由 [FastGit 群组成员] 提供'], // 暂无必要\n\t\t//['https://raw.scholar.rr.nu', '美国', '[美国 纽约] - 该公益加速源由 [FastGit 群组成员] 提供'], // 暂无必要\n\t\t//['https://raw.njuu.cf', '美国', '[美国 纽约] - 该公益加速源由 [FastGit 群组成员] 提供&#10;&#10; - 缓存：无（或时间很短）'], // 域名挂了\n\t\t//['https://raw.yzuu.cf', '美国', '[美国 纽约] - 该公益加速源由 [FastGit 群组成员] 提供&#10;&#10; - 缓存：无（或时间很短）'], // 暂无必要\n\t\t//['https://raw.gitmirror.com', '美国', '[美国 Cloudflare CDN] - 该公益加速源由 [GitMirror] 提供&#10;&#10; - 缓存：有'], // 暂无必要\n\t\t//['https://cdn.54188.cf/gh', '美国', '[美国 Cloudflare CDN] - 该公益加速源由 [PencilNavigator] 提供&#10;&#10; - 缓存：有'], // 暂无必要\n\t\t//['https://raw.fastgit.org', '德国', '[德国] - 该公益加速源由 [FastGit] 提供&#10;&#10; - 缓存：无（或时间很短）'], // 挂了\n\t\t//['https://git.yumenaka.net/https://raw.githubusercontent.com', '美国', '[美国 圣何塞]&#10;&#10; - 缓存：无（或时间很短）'], // 连接超时\n\t];\n\n\tconst svg = [\n\t\t'<svg class=\"octicon octicon-cloud-download\" aria-hidden=\"true\" height=\"16\" version=\"1.1\" viewBox=\"0 0 16 16\" width=\"16\"><path d=\"M9 12h2l-3 3-3-3h2V7h2v5zm3-8c0-.44-.91-3-4.5-3C5.08 1 3 2.92 3 5 1.02 5 0 6.52 0 8c0 1.53 1 3 3 3h3V9.7H3C1.38 9.7 1.3 8.28 1.3 8c0-.17.05-1.7 1.7-1.7h1.3V5c0-1.39 1.56-2.7 3.2-2.7 2.55 0 3.13 1.55 3.2 1.8v1.2H12c.81 0 2.7.22 2.7 2.2 0 2.09-2.25 2.2-2.7 2.2h-2V11h2c2.08 0 4-1.16 4-3.5C16 5.06 14.08 4 12 4z\"></path></svg>'\n\t], style = ['padding:0 6px; margin-right: -1px; border-radius: 2px; background-color: var(--XIU2-back-Color); border-color: rgba(27, 31, 35, 0.1); font-size: 11px; color: var(--XIU2-font-Color);'];\n\n\tif (menu_rawFast == null){menu_rawFast = 1; GM_setValue('xiu2_menu_raw_fast', 1)};\n\tif (GM_getValue('menu_rawDownLink') == null){GM_setValue('menu_rawDownLink', true)};\n\tif (GM_getValue('menu_gitClone') == null){GM_setValue('menu_gitClone', true)};\n\tregisterMenuCommand();\n\t// 注册脚本菜单\n\tfunction registerMenuCommand() {\n\t\t// 如果反馈菜单ID不是 null，则删除所有脚本菜单\n\t\tif (menu_feedBack_ID) {GM_unregisterMenuCommand(menu_rawFast_ID); GM_unregisterMenuCommand(menu_rawDownLink_ID); GM_unregisterMenuCommand(menu_gitClone_ID); GM_unregisterMenuCommand(menu_feedBack_ID); menu_rawFast = GM_getValue('xiu2_menu_raw_fast');}\n\t\t// 避免在减少 raw 数组后，用户储存的数据大于数组而报错\n\t\tif (menu_rawFast > raw_url.length - 1) menu_rawFast = 0\n\t\tmenu_rawDownLink_ID = GM_registerMenuCommand(`${GM_getValue('menu_rawDownLink')?'✅':'❌'} 项目列表单文件快捷下载 (☁)`, function(){if (GM_getValue('menu_rawDownLink') == true) {GM_setValue('menu_rawDownLink', false); GM_notification({text: `已关闭「项目列表单文件快捷下载 (☁)」功能\\n（点击刷新网页后生效）`, timeout: 3500, onclick: function(){location.reload();}});} else {GM_setValue('menu_rawDownLink', true); GM_notification({text: `已开启「项目列表单文件快捷下载 (☁)」功能\\n（点击刷新网页后生效）`, timeout: 3500, onclick: function(){location.reload();}});}registerMenuCommand();}, {title: \"点击开关「项目列表单文件快捷下载 (☁)」功能\"});\n\t\tif (GM_getValue('menu_rawDownLink')) menu_rawFast_ID = GM_registerMenuCommand(`&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;${['0️⃣','1️⃣','2️⃣','3️⃣','4️⃣','5️⃣','6️⃣','7️⃣','8️⃣','9️⃣','🔟'][menu_rawFast]} [ ${raw_url[menu_rawFast][1]} ] 加速源 (☁) - 点击切换`, menu_toggle_raw_fast, {title: \"点击切换「项目列表单文件快捷下载 (☁)」功能的加速源\"});\n\t\tmenu_gitClone_ID = GM_registerMenuCommand(`${GM_getValue('menu_gitClone')?'✅':'❌'} 添加 git clone 命令`, function(){if (GM_getValue('menu_gitClone') == true) {GM_setValue('menu_gitClone', false); GM_notification({text: `已关闭「添加 git clone 命令」功能`, timeout: 3500});} else {GM_setValue('menu_gitClone', true); GM_notification({text: `已开启「添加 git clone 命令」功能`, timeout: 3500});}registerMenuCommand();}, {title: \"点击开关「添加 git clone 命令」功能\"});\n\t\tmenu_feedBack_ID = GM_registerMenuCommand('💬 反馈问题 & 功能建议', function () {GM_openInTab('https://github.com/XIU2/UserScript', {active: true,insert: true,setParent: true});GM_openInTab('https://greasyfork.org/zh-CN/scripts/412245/feedback', {active: true,insert: true,setParent: true});}, {title: \"点击前往反馈问题或提出建议\"});\n\t}\n\n\t// 切换加速源\n\tfunction menu_toggle_raw_fast() {\n\t\t// 如果当前加速源位置大于等于加速源总数，则改为第一个加速源，反之递增下一个加速源\n\t\tif (menu_rawFast >= raw_url.length - 1) {menu_rawFast = 0;} else {menu_rawFast += 1;}\n\t\tGM_setValue('xiu2_menu_raw_fast', menu_rawFast);\n\t\tdelRawDownLink(); // 删除旧加速源\n\t\taddRawDownLink(); // 添加新加速源\n\t\tGM_notification({text: \"已切换加速源为：\" + raw_url[menu_rawFast][1], timeout: 3000}); // 提示消息\n\t\tregisterMenuCommand(); // 重新注册脚本菜单\n\t};\n\n\tcolorMode(); // 适配白天/夜间主题模式\n\tsetTimeout(addRawFile, 1000); // Raw 加速\n\tsetTimeout(addRawDownLink, 2000); // Raw 单文件快捷下载（☁），延迟 2 秒执行，避免被 pjax 刷掉\n\n\t// Tampermonkey v4.11 版本添加的 onurlchange 事件 grant，可以监控 pjax 等网页的 URL 变化\n\tif (window.onurlchange === undefined) addUrlChangeEvent();\n\twindow.addEventListener('urlchange', function() {\n\t\tcolorMode(); // 适配白天/夜间主题模式\n\t\tif (location.pathname.indexOf('/releases')) addRelease(); // Release 加速\n\t\tsetTimeout(addRawFile, 1000); // Raw 加速\n\t\tsetTimeout(addRawDownLink, 2000); // Raw 单文件快捷下载（☁），延迟 2 秒执行，避免被 pjax 刷掉\n\t\tsetTimeout(addRawDownLink_, 1000); // 在浏览器返回/前进时重新添加 Raw 下载链接（☁）鼠标事件\n\t});\n\n\n\t// Github Git Clone/SSH、Release、Download ZIP 改版为动态加载文件列表，因此需要监控网页元素变化\n\tconst callback = (mutationsList, observer) => {\n\t\tif (location.pathname.indexOf('/releases') > -1) { // Release\n\t\t\tfor (const mutation of mutationsList) {\n\t\t\t\tfor (const target of mutation.addedNodes) {\n\t\t\t\t\tif (target.nodeType !== 1) return\n\t\t\t\t\tif (target.tagName === 'DIV' && target.dataset.viewComponent === 'true' && target.classList[0] === 'Box') addRelease();\n\t\t\t\t}\n\t\t\t}\n\t\t} else if (document.querySelector('#repository-container-header:not([hidden])')) { // 项目首页\n\t\t\tfor (const mutation of mutationsList) {\n\t\t\t\tfor (const target of mutation.addedNodes) {\n\t\t\t\t\tif (target.nodeType !== 1) return\n\t\t\t\t\tif (target.tagName === 'DIV' && target.parentElement.id === '__primerPortalRoot__') {\n\t\t\t\t\t\taddDownloadZIP(target);\n\t\t\t\t\t\tif (addGitClone(target) === false) return;\n\t\t\t\t\t\tif (addGitCloneSSH(target) === false) return;\n\t\t\t\t\t} else if (target.tagName === 'DIV' && target.className.indexOf('Box-sc-') !== -1) {\n\t\t\t\t\t\tif (target.querySelector('input[value^=\"https:\"]')) {\n\t\t\t\t\t\t\taddGitCloneClear('.XIU2-GCS');\n\t\t\t\t\t\t\tif (addGitClone(target) === false) return;\n\t\t\t\t\t\t} else if (target.querySelector('input[value^=\"git@\"]')) {\n\t\t\t\t\t\t\taddGitCloneClear('.XIU2-GC');\n\t\t\t\t\t\t\tif (addGitCloneSSH(target) === false) return;\n\t\t\t\t\t\t} else if (target.querySelector('input[value^=\"gh \"]')) {\n\t\t\t\t\t\t\taddGitCloneClear('.XIU2-GC, .XIU2-GCS');\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t};\n\tconst observer = new MutationObserver(callback);\n\tobserver.observe(document, { childList: true, subtree: true });\n\n\n\t// download_url 随机 4 个美国加速源\n\tfunction get_New_download_url() {\n\t\t//return download_url_us.concat(download_url) // 全输出调试用\n\t\tlet shuffled = download_url_us.slice(0), i = download_url_us.length, min = i - 4, temp, index;\n\t\twhile (i-- > min) {index = Math.floor((i + 1) * Math.random()); temp = shuffled[index]; shuffled[index] = shuffled[i]; shuffled[i] = temp;}\n\t\treturn shuffled.slice(min).concat(download_url); // 随机洗牌 download_url_us 数组并取前 4 个，然后将其合并至 download_url 数组\n\t}\n\n\t// Release\n\tfunction addRelease() {\n\t\tlet html = document.querySelectorAll('.Box-footer'); if (html.length == 0 || location.pathname.indexOf('/releases') == -1) return\n\t\tlet divDisplay = 'margin-left: -90px;', new_download_url = get_New_download_url();\n\t\tif (document.documentElement.clientWidth > 755) {divDisplay = 'margin-top: -3px;margin-left: 8px;display: inherit;';}; // 调整小屏幕时的样式\n\t\tfor (const current of html) {\n\t\t\tif (current.querySelector('.XIU2-RS')) continue\n\t\t\tcurrent.querySelectorAll('li.Box-row a').forEach(function (_this) {\n\t\t\t\tlet href = _this.href.split(location.host),\n\t\t\t\t\turl = '', _html = `<div class=\"XIU2-RS\" style=\"${divDisplay}\">`;\n\n\t\t\t\tfor (let i=0;i<new_download_url.length;i++) {\n\t\t\t\t\tif (new_download_url[i][3] !== undefined && url.indexOf('/archive/') !== -1) {\n\t\t\t\t\t\turl = new_download_url[i][3] + href[1]\n\t\t\t\t\t} else {\n\t\t\t\t\t\turl = new_download_url[i][0] + href[1]\n\t\t\t\t\t}\n\t\t\t\t\t_html += `<a style=\"${style[0]}\" class=\"btn\" href=\"${url}\" target=\"_blank\" title=\"${new_download_url[i][2]}\" rel=\"noreferrer noopener nofollow\">${new_download_url[i][1]}</a>`\n\t\t\t\t}\n\t\t\t\t_this.parentElement.nextElementSibling.insertAdjacentHTML('beforeend', _html + '</div>');\n\t\t\t});\n\t\t}\n\t}\n\n\n\t// Download ZIP\n\tfunction addDownloadZIP(target) {\n\t\tlet html = target.querySelector('ul[class^=List__ListBox-sc-] ul[class^=List__ListBox-sc-]>li:last-child');if (!html) return\n\t\tlet href_script = document.querySelector('react-partial[partial-name=repos-overview]>script[data-target=\"react-partial.embeddedData\"]'),\n\t\t\thref_slice = href_script.textContent.slice(href_script.textContent.indexOf('\"zipballUrl\":\"')+14),\n\t\t\thref = href_slice.slice(0, href_slice.indexOf('\"')),\n\t\t\turl = '', _html = '', new_download_url = get_New_download_url();\n\n\t\t// 克隆原 Download ZIP 元素，并定位 <a> <span> 标签\n\t\tlet html_clone = html.cloneNode(true),\n\t\t\thtml_clone_a = html_clone.querySelector('a[href$=\".zip\"]'),\n\t\t\thtml_clone_span = html_clone.querySelector('span[id]');\n\n\t\tfor (let i=0;i<new_download_url.length;i++) {\n\t\t\tif (new_download_url[i][3] === '') continue\n\n\t\t\tif (new_download_url[i][3] !== undefined) {\n\t\t\t\turl = new_download_url[i][3] + href\n\t\t\t} else {\n\t\t\t\turl = new_download_url[i][0] + href\n\t\t\t}\n\t\t\thtml_clone_a.href = url\n\t\t\thtml_clone_a.setAttribute('title', new_download_url[i][2].replaceAll('&#10;','\\n'))\n\t\t\thtml_clone_span.textContent = 'Download ZIP ' + new_download_url[i][1]\n\t\t\t_html += html_clone.outerHTML\n\t\t}\n\t\thtml.insertAdjacentHTML('afterend', _html);\n\t}\n\n\t// Git Clone 切换清理\n\tfunction addGitCloneClear(css) {\n\t\tdocument.querySelectorAll(css).forEach((e)=>{e.remove()})\n\t}\n\n\t// Git Clone\n\tfunction addGitClone(target) {\n\t\tlet html = target.querySelector('input[value^=\"https:\"]');if (!html) return\n\t\tif (!html.nextElementSibling) return false;\n\t\tlet href_split = html.value.split(location.host)[1],\n\t\t\thtml_parent = '<div style=\"margin-top: 4px;\" class=\"XIU2-GC ' + html.parentElement.className + '\">',\n\t\t\turl = '', _html = '', _gitClone = '';\n\t\thtml.nextElementSibling.hidden = true; // 隐藏右侧复制按钮（考虑到能直接点击复制，就不再重复实现复制按钮事件了）\n\t\tif (GM_getValue('menu_gitClone')) {_gitClone='git clone '; html.value = _gitClone + html.value; html.setAttribute('value', html.value);}\n\t\t// 克隆原 Git Clone 元素\n\t\tlet html_clone = html.cloneNode(true);\n\t\tfor (let i=0;i<clone_url.length;i++) {\n\t\t\tif (clone_url[i][0] === 'https://gitclone.com') {\n\t\t\t\turl = clone_url[i][0] + '/github.com' + href_split\n\t\t\t} else {\n\t\t\t\turl = clone_url[i][0] + href_split\n\t\t\t}\n\t\t\thtml_clone.title = `${url}\\n\\n${clone_url[i][2].replaceAll('&#10;','\\n')}\\n\\n提示：点击文字可直接复制`\n\t\t\thtml_clone.setAttribute('value', _gitClone + url)\n\t\t\t_html += html_parent + html_clone.outerHTML + '</div>'\n\t\t}\n\t\thtml.parentElement.insertAdjacentHTML('afterend', _html);\n\t}\n\n\n\t// Git Clone SSH\n\tfunction addGitCloneSSH(target) {\n\t\tlet html = target.querySelector('input[value^=\"git@\"]');if (!html) return\n\t\tif (!html.nextElementSibling) return false;\n\t\tlet href_split = html.value.split(':')[1],\n\t\t\thtml_parent = '<div style=\"margin-top: 4px;\" class=\"XIU2-GCS ' + html.parentElement.className + '\">',\n\t\t\turl = '', _html = '', _gitClone = '';\n\t\thtml.nextElementSibling.hidden = true; // 隐藏右侧复制按钮（考虑到能直接点击复制，就不再重复实现复制按钮事件了）\n\t\tif (GM_getValue('menu_gitClone')) {_gitClone='git clone '; html.value = _gitClone + html.value; html.setAttribute('value', html.value);}\n\t\t// 克隆原 Git Clone SSH 元素\n\t\tlet html_clone = html.cloneNode(true);\n\t\tfor (let i=0;i<clone_ssh_url.length;i++) {\n\t\t\turl = clone_ssh_url[i][0] + href_split\n\t\t\thtml_clone.title = `${url}\\n\\n${clone_ssh_url[i][2].replaceAll('&#10;','\\n')}\\n\\n提示：点击文字可直接复制`\n\t\t\thtml_clone.setAttribute('value', _gitClone + url)\n\t\t\t_html += html_parent + html_clone.outerHTML + '</div>'\n\t\t}\n\t\thtml.parentElement.insertAdjacentHTML('afterend', _html);\n\t}\n\n\n\t// Raw\n\tfunction addRawFile() {\n\t\tlet html = document.querySelector('a[data-testid=\"raw-button\"]');if (!html) return\n\t\tlet href = location.href.replace(`https://${location.host}`,''),\n\t\t\thref2 = href.replace('/blob/','/'),\n\t\t\turl = '', _html = '';\n\n\t\tfor (let i=1;i<raw_url.length;i++) {\n\t\t\tif ((raw_url[i][0].indexOf('/gh') + 3 === raw_url[i][0].length) && raw_url[i][0].indexOf('cdn.staticaly.com') === -1) {\n\t\t\t\turl = raw_url[i][0] + href.replace('/blob/','@');\n\t\t\t} else {\n\t\t\t\turl = raw_url[i][0] + href2;\n\t\t\t}\n\t\t\t_html += `<a href=\"${url}\" title=\"${raw_url[i][2]}\" target=\"_blank\" role=\"button\" rel=\"noreferrer noopener nofollow\" data-size=\"small\" class=\"${html.className} XIU2-RF\">${raw_url[i][1].replace(/ \\d/,'')}</a>`\n\t\t}\n\t\tif (document.querySelector('.XIU2-RF')) document.querySelectorAll('.XIU2-RF').forEach((e)=>{e.remove()})\n\t\thtml.insertAdjacentHTML('afterend', _html);\n\t}\n\n\n\t// Raw 单文件快捷下载（☁）\n\tfunction addRawDownLink() {\n\t\tif (!GM_getValue('menu_rawDownLink')) return\n\t\t// 如果不是项目文件页面，就返回，如果网页有 Raw 下载链接（☁）就返回\n\t\tlet files = document.querySelectorAll('div.Box-row svg.octicon.octicon-file, .react-directory-filename-column>svg.color-fg-muted');if(files.length === 0) return;if (location.pathname.indexOf('/tags') > -1) return\n\t\tlet files1 = document.querySelectorAll('a.fileDownLink');if(files1.length > 0) return;\n\n\t\t// 鼠标指向则显示\n\t\tvar mouseOverHandler = function(evt) {\n\t\t\tlet elem = evt.currentTarget,\n\t\t\t\taElm_new = elem.querySelectorAll('.fileDownLink'),\n\t\t\t\taElm_now = elem.querySelectorAll('svg.octicon.octicon-file, svg.color-fg-muted');\n\t\t\taElm_new.forEach(el=>{el.style.cssText = 'display: inline'});\n\t\t\taElm_now.forEach(el=>{el.style.cssText = 'display: none'});\n\t\t};\n\n\t\t// 鼠标离开则隐藏\n\t\tvar mouseOutHandler = function(evt) {\n\t\t\tlet elem = evt.currentTarget,\n\t\t\t\taElm_new = elem.querySelectorAll('.fileDownLink'),\n\t\t\t\taElm_now = elem.querySelectorAll('svg.octicon.octicon-file, svg.color-fg-muted');\n\t\t\taElm_new.forEach(el=>{el.style.cssText = 'display: none'});\n\t\t\taElm_now.forEach(el=>{el.style.cssText = 'display: inline'});\n\t\t};\n\n\t\t// 循环添加\n\t\tfiles.forEach(function(fileElm) {\n\t\t\tlet trElm = fileElm.parentNode.parentNode,\n\t\t\t\tcntElm_a = trElm.querySelector('[role=\"rowheader\"] > .css-truncate.css-truncate-target.d-block.width-fit > a, .react-directory-truncate>a'),\n\t\t\t\tName = cntElm_a.innerText,\n\t\t\t\thref = cntElm_a.getAttribute('href'),\n\t\t\t\thref2 = href.replace('/blob/','/'), url = '';\n\t\t\tif ((raw_url[menu_rawFast][0].indexOf('/gh') + 3 === raw_url[menu_rawFast][0].length) && raw_url[menu_rawFast][0].indexOf('cdn.staticaly.com') === -1) {\n\t\t\t\turl = raw_url[menu_rawFast][0] + href.replace('/blob/','@');\n\t\t\t} else {\n\t\t\t\turl = raw_url[menu_rawFast][0] + href2;\n\t\t\t}\n\n\t\t\tfileElm.insertAdjacentHTML('afterend', `<a href=\"${url}?DS_DOWNLOAD\" download=\"${Name}\" target=\"_blank\" rel=\"noreferrer noopener nofollow\" class=\"fileDownLink\" style=\"display: none;\" title=\"「${raw_url[menu_rawFast][1]}」&#10;&#10;左键点击下载文件（注意：鼠标点击 [☁] 图标进行下载，而不是文件名！）&#10;&#10;${raw_url[menu_rawFast][2]}&#10;&#10;提示：点击页面右侧飘浮着的 TamperMonkey 扩展图标中的菜单「 [${raw_url[menu_rawFast][1]}] 加速源 (☁) 」即可切换。\">${svg[0]}</a>`);\n\t\t\t// 绑定鼠标事件\n\t\t\ttrElm.onmouseover = mouseOverHandler;\n\t\t\ttrElm.onmouseout = mouseOutHandler;\n\t\t});\n\t}\n\n\n\t// 移除 Raw 单文件快捷下载（☁）\n\tfunction delRawDownLink() {\n\t\tif (!GM_getValue('menu_rawDownLink')) return\n\t\tlet aElm = document.querySelectorAll('.fileDownLink');if(aElm.length === 0) return;\n\t\taElm.forEach(function(fileElm) {fileElm.remove();})\n\t}\n\n\n\t// 在浏览器返回/前进时重新添加 Raw 单文件快捷下载（☁）鼠标事件\n\tfunction addRawDownLink_() {\n\t\tif (!GM_getValue('menu_rawDownLink')) return\n\t\t// 如果不是项目文件页面，就返回，如果网页没有 Raw 下载链接（☁）就返回\n\t\tlet files = document.querySelectorAll('div.Box-row svg.octicon.octicon-file, .react-directory-filename-column>svg.color-fg-muted');if(files.length === 0) return;\n\t\tlet files1 = document.querySelectorAll('a.fileDownLink');if(files1.length === 0) return;\n\n\t\t// 鼠标指向则显示\n\t\tvar mouseOverHandler = function(evt) {\n\t\t\tlet elem = evt.currentTarget,\n\t\t\t\taElm_new = elem.querySelectorAll('.fileDownLink'),\n\t\t\t\taElm_now = elem.querySelectorAll('svg.octicon.octicon-file, svg.color-fg-muted');\n\t\t\taElm_new.forEach(el=>{el.style.cssText = 'display: inline'});\n\t\t\taElm_now.forEach(el=>{el.style.cssText = 'display: none'});\n\t\t};\n\n\t\t// 鼠标离开则隐藏\n\t\tvar mouseOutHandler = function(evt) {\n\t\t\tlet elem = evt.currentTarget,\n\t\t\t\taElm_new = elem.querySelectorAll('.fileDownLink'),\n\t\t\t\taElm_now = elem.querySelectorAll('svg.octicon.octicon-file, svg.color-fg-muted');\n\t\t\taElm_new.forEach(el=>{el.style.cssText = 'display: none'});\n\t\t\taElm_now.forEach(el=>{el.style.cssText = 'display: inline'});\n\t\t};\n\t\t// 循环添加\n\t\tfiles.forEach(function(fileElm) {\n\t\t\tlet trElm = fileElm.parentNode.parentNode;\n\t\t\t// 绑定鼠标事件\n\t\t\ttrElm.onmouseover = mouseOverHandler;\n\t\t\ttrElm.onmouseout = mouseOutHandler;\n\t\t});\n\t}\n\n\n\t// 适配白天/夜间主题模式\n\tfunction colorMode() {\n\t\tlet style_Add;\n\t\tif (document.getElementById('XIU2-Github')) {style_Add = document.getElementById('XIU2-Github')} else {style_Add = document.createElement('style'); style_Add.id = 'XIU2-Github'; style_Add.type = 'text/css';}\n\t\tbackColor = '#ffffff'; fontColor = '#888888';\n\n\t\tif (document.lastElementChild.dataset.colorMode === 'dark') { // 如果是夜间模式\n\t\t\tif (document.lastElementChild.dataset.darkTheme === 'dark_dimmed') {\n\t\t\t\tbackColor = '#272e37'; fontColor = '#768390';\n\t\t\t} else {\n\t\t\t\tbackColor = '#161a21'; fontColor = '#97a0aa';\n\t\t\t}\n\t\t} else if (document.lastElementChild.dataset.colorMode === 'auto') { // 如果是自动模式\n\t\t\tif (window.matchMedia('(prefers-color-scheme: dark)').matches || document.lastElementChild.dataset.lightTheme.indexOf('dark') > -1) { // 如果浏览器是夜间模式 或 白天模式是 dark 的情况\n\t\t\t\tif (document.lastElementChild.dataset.darkTheme === 'dark_dimmed') {\n\t\t\t\t\tbackColor = '#272e37'; fontColor = '#768390';\n\t\t\t\t} else if (document.lastElementChild.dataset.darkTheme.indexOf('light') == -1) { // 排除夜间模式是 light 的情况\n\t\t\t\t\tbackColor = '#161a21'; fontColor = '#97a0aa';\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\tdocument.lastElementChild.appendChild(style_Add).textContent = `.XIU2-RS a {--XIU2-back-Color: ${backColor}; --XIU2-font-Color: ${fontColor};}`;\n\t}\n\n\n\t// 自定义 urlchange 事件（用来监听 URL 变化），针对非 Tampermonkey 油猴管理器\n\tfunction addUrlChangeEvent() {\n\t\thistory.pushState = ( f => function pushState(){\n\t\t\tvar ret = f.apply(this, arguments);\n\t\t\twindow.dispatchEvent(new Event('pushstate'));\n\t\t\twindow.dispatchEvent(new Event('urlchange'));\n\t\t\treturn ret;\n\t\t})(history.pushState);\n\n\t\thistory.replaceState = ( f => function replaceState(){\n\t\t\tvar ret = f.apply(this, arguments);\n\t\t\twindow.dispatchEvent(new Event('replacestate'));\n\t\t\twindow.dispatchEvent(new Event('urlchange'));\n\t\t\treturn ret;\n\t\t})(history.replaceState);\n\n\t\twindow.addEventListener('popstate',()=>{ // 点击浏览器的前进/后退按钮时触发 urlchange 事件\n\t\t\twindow.dispatchEvent(new Event('urlchange'))\n\t\t});\n\t}\n})();\n"
  },
  {
    "path": "packages/gui/extra/scripts/google.js",
    "content": "// ==UserScript==\n// @name         google增强\n// @version      1.2.4\n// @author       Greper\n// @description  去除ping链接\n// @match        https://www.google.com/*/*\n// @icon         https://www.google.com/favicon.ico\n// @license      GPL-3.0 License\n// @run-at       document-end\n// @namespace\n// ==/UserScript==\n\n(function () {\n  console.log('google script  loaded')\n  const aList = document.getElementsByTagName('a')\n  for (let i = 0; i <= aList.length; i++) {\n    console.log(aList[i].href)\n    aList[i].ping = undefined\n  }\n})()\n"
  },
  {
    "path": "packages/gui/extra/scripts/tampermonkey.script",
    "content": "/**\n * 篡改猴（Tampermonkey）| 油猴（Greasemonkey）浏览器脚本扩展\n *\n * @version        0.1.4\n * @since          2024-04-24 17:06\n * @author         王良\n * @authorHomePage https://wangliang1024.cn\n * @remark         当前脚本为仿照的版本，并非篡改猴插件的源码，仅供学习参考。\n * @description    篡改猴 (Tampermonkey) 是拥有 超过 1000 万用户 的最流行的浏览器扩展之一。 它适用于 Chrome、Microsoft Edge、Safari、Opera Next 和 Firefox。\n *                 有些人也会把篡改猴(Tampermonkey)称作油猴(Greasemonkey)，尽管后者只是一款仅适用于 Firefox 浏览器的浏览器扩展程序。\n *                 它允许用户自定义并增强您最喜爱的网页的功能。用户脚本是小型 JavaScript 程序，可用于向网页添加新功能或修改现有功能。使用 篡改猴，您可以轻松在任何网站上创建、管理和运行这些用户脚本。\n *                 例如，使用 篡改猴，您可以向网页添加一个新按钮，可以快速在社交媒体上分享链接，或自动填写带有个人信息的表格。在数字化时代，这特别有用，因为网页常常被用作访问广泛的服务和应用程序的用户界面。\n *                 此外，篡改猴 使您轻松找到并安装其他用户创建的用户脚本。这意味着您可以快速轻松地访问为您喜爱的网页定制的广泛库，而无需花费数小时编写自己的代码。\n *                 无论您是希望为您的站点添加新功能的 Web 开发人员，还是只是希望 改善在线体验的普通用户，篡改猴 都是您的工具箱中的一个很好的工具。\n * @homepageUrl    https://www.tampermonkey.net\n */\n'use strict';\n(function () {\n\tconst version = \"0.1.4\";\n\tconst PRE = \"DS-Tampermonkey:\"; // 前缀\n\tconst MENU_ID_PRE = PRE + \"menu-\";\n\tconst icon = \"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAADAAAAAwEAQAAACtm+1PAAAABGdBTUEAALGPC/xhBQAAAAFzUkdCAK7OHOkAAAAgY0hSTQAAeiYAAICEAAD6AAAAgOgAAHUwAADqYAAAOpgAABdwnLpRPAAAAAJiS0dEAACqjSMyAAAACXBIWXMAAABIAAAASABGyWs+AAAL+UlEQVRo3tWaeXRUdZbHP7+qkI1ABIQJryoJEAkYaAJECBEEGhAIgiw2DAfBAUEE9MjixtjSKNotyICI4CggckCddsSWRZYASqvIIltQo0ADQdaEfctGSH3nj9/L0ggIkjPY95yqd+q9V/fe77339373e6vgX1zMtS4+2CRuzo+fDOl4IbfDY4H2DR7P3xjWMXAS8AKhV/lSDhC4htIwIOgq1y6BJwJCU/NWeb7PmBEem9a0wZ/fjX7/ob1DbgjA042jeq8YNtGcm/vQhqIkc8C8BYwBugHRYO4AfK6ztwHhZbRdAgqAY0Cwe+0MUAmo7DpfbFXAUaACEAH6EfgBWAlMBj0N3n2Kifx8fkqXPmM16ZWsj34RQPfHW7b7Ln5h14L0qNEmF8wa4EMww4BBwO/cVxVXQSzQyDoAwCmgEDjkRjsCOO7eH+m+ijNwAtgCOu1+5wdgB5AGehF4AtQKFA4hfbNeq/dpj54rntxU+6oAUucnZ+4cvDb24udhxlQHkwPmWzCfAqPAdASSgKZALOAB8lynzpeC+kU542aj2PolIBvYCnwPWgn8F2ggqDaoks1O8IA8JdT+/U/LVpeCKAHw7JdRvT8K2Z5SEIga7ekGxg8m2I3+h8AIMN2Be4B/c6NYFbsWKrnnKlwnAICTbpkVuZkoAHJtRvgYNBt4DNQcFAH6BgKfQGi/rNf63NVkwysf23IqWU4r908eUrA9qqMZCaYumBgwZ8HE2WhwAsxPQJQLOxg4dwMOX0sCQD6QBewHXQQzF7t+agLVLDhTE/I7Ro1e8fHkBjDgI9wiYGjleJ1e8OAQcxxMHpiKYM6DOQRkgFkF5mNguFs+ES6A8hKP62wd4A9gpoBZCXwDZjeYLDBeMGH2eGr4g0Me/Uu8SgBsWT7oYFFLc8BsAbMPWARmLTAPzNtA+3J09nolGcwsYCmY1e4r3fpVVGQObJk/6CC4a6Dec5u75cy+a4lZAp7XwbS09W+WAm1ugfNlZTOoLSgYtAECQ0HdoOKMLffv6tNsqQGIy8tNK5gc1tHzdzCvgycROOlG33uLAQBMAIVAYB3oKQi0hNBZeav2ZIZ3MgD+YxLHwPMteJ4Dc7d9fPIft9pzV74EJYDWQOAJCNwBpMAhjzFBAIoFTyGwDlvvtbl6q3ArpKnrU8D1LwwCOfaSB7CbUR0wrYB6QDTXvyn9f0gEUB+IAxMPph4lpe0peb8d+yirjI2+51Z7fZnEAzWAi4DARJQFgOt8ODYbxah/S1LJ9S/BftSpywEUS/GZ87fa4yv4FcBmAC4rIYALoMP2SB621f0tyUlK2w3ARNpjKbU4DJwF9mF7nJ3Yvue3IAHXn8PYDJTZmyyAO7HE4htsNxgNrAXa3mrPXdmI7XQPWu4gbMNHpeISKgDtA/5he3H9D2gBMBnYe4udzwTNxwb0K2ADtpz22Mt2I9sBZiOWynmwZfQB6Elsds5cQXEFoDWYRKAZkIJlYNcjF4E1rnM7sFk/XOZ6dWxnGopldqNAfwJ5gRXYdXpbGQAUArtBH9iNQt+6IOq6yi64qLdi6aEHSyODQJlgHgFqgRkKPIjdDK/EtncCfwO95epfC9qELZFdWHJTD0twggE/lgt0Bx4GkoFlwBNAw7IAjgGrgSO2WTLtQR2xpP1zYCkozY3KfjeC9cH0AMbacjN/diMWBQwC0xlIxNLFjaB04A17jw5jWdd/A31AS7HszmApa0MwDwG9XBAAna1P6gtMB2LKAtgBLALWgb52P2cBM0EfAgdsO8s2Sp8GB+zRTLftt9bZVsQsB1MdNARogt18QkFBbsksBDUCloDO2PLUBrfGC90sNLCVQD83q52xu3APoLGbsbvLAvjJdfwkmA6ge62zmgC0A02C0FZH61Wo/NXSwN93j/TGFY7iKadn0YKmEfmfNn05sMc71FRzo5hnCbh5Abt7XrJAtQuYB/orqIJ9kngWFM0Kq7p1Fp02nQ85e2J6Ye8K09QwrlGR03ZR7taau0wyaB6YUW7J3g0aCyoAllsABsAZI5k8MIuAVECgiUBvqBy57U/+/uOc9/ulBWoUFT12eVmPuD1mzNf/O2r5mbTH+xbtqzDe+MB4wKQBA9yIvgNqbw3rMHiHFr5YpcOMvya3nXL07e8On71cp+Sd2WF423sOtX7pyIVZKZ1YCmacLVvmgpJAOXBkvrErzRkk+RpKvvck30rJN1CKbqxJLd4av/zEXu94rkN6fpGYUefA7lzfD1J0uBS9V4rpKsXcL0VvlKKN5NshxUXuzu31XWLG9eg8X+gdn7Lw+Qf8ay4d9A2UfFsl3weSL1Zyequ0V3CSJOeY5Ksl+YIl3zqldNj+ZNL1GCkrwx5w+t8xdl9f3zLJ/5EUXUuKbij5P5B8f5PqpO/OfXSr0/9G9baIGzTJH6sUXxXJd5fkHJactmUBJEjOF5KzR3JGS4mb36l9o0ZKMtE/MSPWc/EF3wzJ30DyJ0u+uVKs5+ILve67vshfSRrXebO/80fJ2S05cyXnzrIAkiWns+R0k2qHnh4xNify+K81BJD41NT6zhbJ11/yDZOcdVKjFVPr34zOVzZEHq8z8vQI51HJSZWcOJWOVbiIXc5VIHz/7MMTK56tfjPGfv/AtC7e3UWzNBQ0DLyzi2a1/n5al5vR+Z8pZ6tXSp59mJPuiaNlLjqhkvO25CRKqUEtsm7GULHET9iU5HSVnJ5S3ZWbbng9XUl6V26R5bSSnOGSE1w2AwHgAgS9pzbvPp0+rTyMEbvpPEHYjnHxlj7loXL+7vRpwTvUhndKz5USmskQ1klNo17Jn1gexkKmnJhOEWDAk52dWB46w6LyJyr1RGTZc/9EKQMdysOMlcIHKkyjMhAJ6h9abgQ1aHrl6T8D4HGZV+5mT9WZg/2Ly8OQmR3/OhWwVPVS3B/KQ+fsbf7F+eNCFgB4WpYBENIobxW1QQWMe29O6xY3ayinuXfmxUP3dOMiEASXftd2yDGvd+bN6p2T0GqlljGOJhDSLm9VCYCg7IwZ+IDmcKb5oEkSvW7GUI9xnTx5y2ruYiQwBPK+rDHnoS+6PHIzOiV6nev8cD2aAfEQdCJjRgmAME9aU7YDSXA+tMOU7kXtdv5aQydjvOMPLnrpCCFg+oJ5GIiDzLUvvHT8OvuqK8n9Ge12nj9x70haAeshOHV5egmAxOR3o70TFUMeBBpBxt45jQe/GPnZrzGUmvF88rmkphPM8zb6DLat9fk6TSd0cZ5P/jU6H5kZ+dkP++c0DrQHvOD9UDF3jZj/z8FI3LUgzWnhbmjtpfht6yOe2Vot9kYMtdv3ZJL/a03yNZB8CyV/TclfR/KtlnxNJH+6JrX77MaaxGc2V4uNL1wf4YRJzhuS01JquG7u9J/dOOLO28/VSTs61WkjOW9KTjMpbsS+fve93S7hl9bEmOzI402i5r3sm64UX5jke1Xy15D8IZK/tuT3u616fck3RymNd817ecxz1+63JHp1ndEuIW7kvn5OqOSMcX0yR6c+nF7tyk+1rtuSM2vNzQ04vSRnquQMsRFM6PB5RkpSv+w3sksfsdnvhY7tYlpkNYh5tXuttadHOMNsm+vrK/nSJf89kj9W8reWfEWSL1/yDZKcfMl5Vqq1+PSIhJ9e7d45p0XWkezQscV6Z67wL05Z3i+7/oDVY/yxkjNScmbayNf6KjfQLT85s6zPP5sddItKzszYtOiT/HujRtMP+yvhbCAfzB5eCvcFThX9JXfnpXMVcy+9ab6gCpbI/xHwgXkOO1kYAOZ2SkaUeh9LUydiJ3/PAJnWg6C2ahN0R064d0Z4/dwfPVVViXHchx3XFADzIOzHrNca1O3Rc8nma/zQXSyj766xYvWrE/9xNn/g5KKB5gDDgRDsiOVyWYqljanYHyISwDRyQQTc7xViJx/bXG78FZCOHQ50u0xfDnawnAmsB+9Cxdy2aN7THbuPrTul3rHUy81f888egyvFzdn67KCDuUvaP1aUkrg9f2RYRzW77KYooKJr2IudQlTFznN87j2nsO3vaezgOBc7vj/Fz4ZmZj2Ezs1b5T20o0nIv69Kar5m/vh3pl/9zx7/8vJ/39rPvCQFiBIAAAAldEVYdGRhdGU6Y3JlYXRlADIwMTYtMDEtMjlUMjM6NDM6MjcrMDE6MDDuWSV6AAAAJXRFWHRkYXRlOm1vZGlmeQAyMDE2LTAxLTI5VDIzOjQzOjI3KzAxOjAwnwSdxgAAAFl0RVh0c3ZnOmJhc2UtdXJpAGZpbGU6Ly8vaG9tZS9qYW5iL1Byb2pla3RlL3RhbXBlcm1vbmtleS9yZWwvaW1hZ2VzL2luY2x1ZGVzL3RhbXBlcm1vbmtleS5zdmf3en/XAAAAAElFTkSuQmCC\";\n\n\tconst context = {\n\t\tinitialized: false, // 是否已经初始化\n\t\tdefaultPluginOptions: {}, // 默认插件选项\n\t\tpluginOptions: {}, // 插件选项\n\t\tstyleElement: null, // 插件样式元素\n\t\tpluginElement: null, // 插件元素\n\t\tarrowElement: null, // 箭头元素\n\t\tmenusElement: null, // 菜单列表元素\n\t\tuserMenusElement: null, // 用户菜单列表元素\n\t\tmenus: {}, // 菜单集合\n\t\tmenuIndex: 0, // 菜单索引，用于生成menuCmdId\n\t\tlastNotification: null // 最后一次通知\n\t\t/* 最后一次通知的对象结构如下：\n\t\t{\n\t\t\tobj: null, // 通知对象，类型：Notification\n\t\t\toptions: null, // 通知选项\n\t\t\ttimeout: null // 通知定时器\n\t\t}\n\t\t*/\n\t};\n\n\n\t// 创建插件API\n\tconst api = {};\n\n\t// 监听页面关闭事件，用于关闭最后一个通知\n\twindow.addEventListener('beforeunload', function(event) {\n\t\tapi.closeLastNotification();\n\t});\n\n\n\t//region DS自定义的API start\n\n\t// 获取上下文\n\tapi.getContext = () => context;\n\n\t// 创建插件样式\n\tapi.createPluginStyle = (options) => {\n\t\toptions = options || {};\n\n\t\t// 创建一个新的<style>元素\n\t\tconst styleElement = document.createElement('style');\n\t\tstyleElement.id = PRE + \"plugin-style\";\n\t\t// 设置<style>元素的type属性\n\t\tstyleElement.type = 'text/css';\n\n\t\t// 设置<style>元素的内容\n\t\tlet cssContent = `\n.___ds-tampermonkey___{\n\tposition: fixed;\n\tright: 10px;\n\ttop: 30%;\n\tz-index: 9999;\n\twidth: 36px;\n\theight: 36px;\n\tborder-radius: 8px;\n\tuser-select: none;         /* Standard syntax */  \n\t-webkit-user-select: none; /* Safari */  \n\t-ms-user-select: none;     /* IE 10+/Edge */  \n\tbackground-color: #DDD;\n\tbackground-repeat: no-repeat;\n\tbackground-size: cover;\n\tbackground-image: url(\"${icon}\");\n}\n.___ds-tampermonkey-hide___{\n\twidth: 0;\n}\n.___ds-menus___{\n\tdisplay: none;\n\tposition: absolute;\n\tright: 36px;\n\ttop: 0;\n\tz-index: 10000;\n\tmin-width: 200px;\n\tmin-height: 35px;\n\tborder-radius: 8px;\n\tbackground-color: #323231;\n\tborder: 1px solid #52525E;\n\toverflow: hidden;\n}\n.___ds-tampermonkey___:hover:not(.___ds-tampermonkey-hide___) .___ds-menus___{\n\tdisplay: block;\n}\n.___ds-tampermonkey-hide___ .___ds-menus___{\n\tdisplay: none;\n}\n.___ds-menu___{\n\theight: 35px;\n\tline-height: 35px;\n\tpadding: 0 10px;\n\twhite-space: nowrap;\n\tcolor: #FFF;\n\tcursor: pointer;\n\tmargin-left: 26px;\n}\n.___ds-menu___:hover{\n\tbackground-color: #855F16;\n}\n.___ds-menu0___{\n\tmargin-left: 0;\n\tfont-size: 16px;\n\tfont-weight: bold;\n}\n.___ds-menu0___ img{\n\twidth: 23px;\n\theight: 23px;\n\tvertical-align: middle;\n\tmargin: 0 8px 3px 8px;\n}\n.___ds-arrow___{\n\twidth: 0;\n\theight: 0;\n\tposition: absolute;\n\ttop: 11px;\n\tleft: 36px;\n\tcursor: pointer;\n\tborder-top: 7px solid transparent;\n\tborder-bottom: 7px solid transparent;\n\tborder-left: 10px solid #665c5c;\n\tdisplay: none;\n}\n.___ds-tampermonkey___:hover .___ds-arrow___{\n\tdisplay: block;\n}\n.___ds-tampermonkey-hide___ .___ds-arrow___{\n\tborder-top: 7px solid transparent;\n\tborder-bottom: 7px solid transparent;\n\tborder-right: 10px solid #665c5c;\n\tborder-left: 0;\n\tleft: 0;\n\tdisplay: block;\n}\n`;\n\t\t// 如果有自定义样式，则添加到 CSS 内容中\n\t\tif (options.style) {\n\t\t\tcssContent += options.style;\n\t\t}\n\n\t\t// 添加 CSS 内容到<style>元素中\n\t\tif (styleElement.styleSheet) {\n\t\t\t// 兼容 IE\n\t\t\tstyleElement.styleSheet.cssText = cssContent;\n\t\t} else {\n\t\t\tstyleElement.appendChild(document.createTextNode(cssContent));\n\t\t}\n\n\t\t// 将<style>元素添加到<head>中\n\t\tdocument.head.append(styleElement);\n\n\t\t// 将<style>元素保存在上下文中\n\t\tcontext.styleElement = styleElement;\n\t};\n\n\t// 创建插件div\n\tapi.createPluginDiv = (options) => {\n\t\toptions = {\n\t\t\t...{ name: \"未知名的脚本\" },\n\t\t\t...options\n\t\t}\n\n\t\t// 创建插件div\n\t\tcontext.pluginElement = document.createElement('div');\n\t\tcontext.pluginElement.id = PRE + \"plugin\";\n\t\tcontext.pluginElement.title = \"油猴插件\" + (options.name ? \"：\" + options.name : \"\");\n\t\tcontext.pluginElement.className = \"___ds-tampermonkey___\";\n\t\tif (api.GM_getValue(\"ds_hide\")) {\n\t\t\tcontext.pluginElement.classList.add(\"___ds-tampermonkey-hide___\");\n\t\t}\n\n\t\t// 创建菜单列表div\n\t\tcontext.menusElement = document.createElement('div');\n\t\tcontext.menusElement.id = PRE + \"menus\";\n\t\tcontext.menusElement.className = \"___ds-menus___\";\n\t\tif (options.width > 0) {\n\t\t\tcontext.menusElement.style['min-width'] = options.width + \"px\";\n\t\t}\n\t\t// 将菜单列表div添加到插件div中\n\t\tcontext.pluginElement.append(context.menusElement);\n\n\t\t// 创建开关菜单\n\t\tconst enabled = api.GM_getValue(\"ds_enabled\", true)\n\t\tconst switchMenuElement = document.createElement('div');\n\t\tconst icon = (options.icon ? `<img alt=\"icon\" src=\"${options.icon}\"/>` : \" \");\n\t\tswitchMenuElement.id = PRE + \"menu-0\";\n\t\tswitchMenuElement.className = \"___ds-menu___ ___ds-menu0___\";\n\t\tswitchMenuElement.innerHTML = (enabled ? \"✅\" : \"❌\") + icon + options.name;\n\t\tswitchMenuElement.title = `点击${enabled ? \"关闭\" : \"开启\"}此脚本功能`;\n\t\tswitchMenuElement.onclick = function () {\n\t\t\tlet enabled = api.GM_getValue(\"ds_enabled\", true)\n\t\t\tif (enabled) {\n\t\t\t\tapi.hideUserMenus();\n\t\t\t\tenabled = false;\n\t\t\t} else {\n\t\t\t\tapi.showUserMenus();\n\t\t\t\tenabled = true;\n\t\t\t}\n\t\t\tswitchMenuElement.innerHTML = (enabled ? \"✅\" : \"❌\") + icon + options.name;\n\t\t\tswitchMenuElement.title = `点击${enabled ? \"关闭\" : \"开启\"}此脚本功能`;\n\t\t\tapi.GM_setValue(\"ds_enabled\", enabled)\n\t\t\tapi.GM_notification({\n\t\t\t\ttitle: \"脚本状态变更通知\",\n\t\t\t\ttext: `已${enabled ? \"开启\" : \"关闭\"} 「${options.name}」 功能\\n（点击刷新网页后生效）`,\n\t\t\t\ttimeout: 3500,\n\t\t\t\tonclick: () => location.reload()\n\t\t\t});\n\t\t};\n\t\t// 将开关菜单添加到菜单列表div中\n\t\tcontext.menusElement.append(switchMenuElement);\n\n\t\t// 创建用户菜单列表div\n\t\tcontext.userMenusElement = document.createElement('div');\n\t\tcontext.userMenusElement.id = PRE + \"user-menus\";\n\t\tcontext.userMenusElement.className = \"___ds-user-menus___\";\n\t\t// 将用户菜单div添加到菜单div中\n\t\tcontext.menusElement.append(context.userMenusElement);\n\n\t\t// 获取body元素\n\t\tconst body = document.getElementsByTagName('body')[0];\n\t\t// 将插件div添加到body中\n\t\tbody.prepend(context.pluginElement);\n\t}\n\n\t// 创建箭头\n\tapi.createArrow = (options) => {\n\t\t// 创建箭头元素\n\t\tcontext.arrowElement = document.createElement('div');\n\t\tcontext.arrowElement.id = PRE + \"arrow\";\n\t\tcontext.arrowElement.className = \"___ds-arrow___\";\n\t\t// 初始化title\n\t\tapi.initArrowTitle();\n\t\t// 绑定点击事件\n\t\tcontext.arrowElement.onclick = () => {\n\t\t\tif (__ds_global__.getContext().pluginElement.classList.contains(\"___ds-tampermonkey-hide___\")) {\n\t\t\t\tapi.showPlugin();\n\t\t\t\tapi.initArrowTitle(false);\n\t\t\t} else {\n\t\t\t\tapi.hidePlugin();\n\t\t\t\tapi.initArrowTitle(true);\n\t\t\t}\n\t\t}\n\t\t// 将箭头元素添加到插件div中\n\t\tcontext.pluginElement.append(context.arrowElement);\n\t}\n\n\tapi.initArrowTitle = (isHidden) => {\n\t\tif (isHidden == null) {\n\t\t\tisHidden = context.pluginElement.classList.contains(\"___ds-tampermonkey-hide___\");\n\t\t}\n\n\t\tif (isHidden) {\n\t\t\tcontext.arrowElement.title = \"点击展示「油猴插件」的操作界面\";\n\t\t} else {\n\t\t\tcontext.arrowElement.title = \"点击隐藏「油猴插件」的操作界面\";\n\t\t}\n\t}\n\n\t// 隐藏插件\n\tapi.hidePlugin = () => {\n\t\tif (context.pluginElement) {\n\t\t\tcontext.pluginElement.classList.add(\"___ds-tampermonkey-hide___\");\n\t\t}\n\t\tapi.GM_setValue(\"ds_hide\", true);\n\t}\n\n\t// 显示插件\n\tapi.showPlugin = () => {\n\t\tif (context.pluginElement) {\n\t\t\tcontext.pluginElement.classList.remove(\"___ds-tampermonkey-hide___\");\n\t\t}\n\t\tapi.GM_setValue(\"ds_hide\", false);\n\t}\n\n\t// 显示用户菜单列表\n\tapi.showUserMenus = () => {\n\t\tif (context.userMenusElement) {\n\t\t\tcontext.userMenusElement.style.display = \"block\";\n\t\t}\n\t}\n\n\t// 隐藏用户菜单列表\n\tapi.hideUserMenus = () => {\n\t\tif (context.userMenusElement) {\n\t\t\tcontext.userMenusElement.style.display = \"none\";\n\t\t}\n\t}\n\n\t// 初始化篡改猴操作界面\n\tapi.DS_init = (options) => {\n\t\ttry {\n\t\t\t// 如果已经初始化过，则直接返回\n\t\t\tif (context.initialized) return;\n\n\t\t\t// 合并默认参数\n\t\t\toptions = {\n\t\t\t\t...context.defaultPluginOptions,\n\t\t\t\t...options\n\t\t\t};\n\n\t\t\t// 创建样式元素\n\t\t\tapi.createPluginStyle(options);\n\t\t\t// 创建插件div\n\t\t\tapi.createPluginDiv(options);\n\t\t\t// 创建箭头\n\t\t\tapi.createArrow(options);\n\t\t\t// 保存参数\n\t\t\tcontext.pluginOptions = options;\n\n\t\t\t// 初始化完成\n\t\t\tcontext.initialized = true;\n\n\t\t\tconsole.log(`ds_tampermonkey_${version}: initialization completed（篡改猴插件初始化完成，篡改猴图标已显示在页面右侧，鼠标移到上面可展示功能列表！）`)\n\t\t} catch (e) {\n\t\t\tconsole.error(`ds_tampermonkey_${version}: initialization failed（篡改猴插件初始化失败）:`, e);\n\t\t}\n\t};\n\n\t// 关闭上一个通知\n\tapi.closeLastNotification = () => {\n\t\tlet lastNotification = context.lastNotification;\n\t\tif (lastNotification) {\n\t\t\tcontext.lastNotification = null;\n\t\t\tlastNotification.timeout && clearTimeout(lastNotification.timeout);\n\t\t\ttry {\n\t\t\t\tlastNotification.obj && lastNotification.obj.close();\n\t\t\t} catch (e) {\n\t\t\t\tconsole.error(`ds_tampermonkey_${version}: GM_notification: 关闭上一个通知失败:`, e);\n\t\t\t}\n\t\t}\n\t};\n\n\t//endregion DS自定义的API end\n\n\n\t//region 篡改猴标准API，由DS自定义实现 start\n\n\t// 注册菜单\n\tapi.GM_registerMenuCommand = (name, callback, options_or_accessKey) => {\n\t\tconst options = typeof options_or_accessKey === \"string\" ? { accessKey: options_or_accessKey } : options_or_accessKey;\n\n\t\t// 生成菜单ID\n\t\tlet menuCmdId;\n\t\tif (options.id) {\n\t\t\tif (typeof options.id !== \"string\") {\n\t\t\t\toptions.id = options.id.toString();\n\t\t\t}\n\n\t\t\tmenuCmdId = (options.id.indexOf(MENU_ID_PRE) === 0 ? '' : MENU_ID_PRE) + options.id;\n\n\t\t\t// 如果是数字ID，为了避免与自增ID索引冲突，将数字ID赋值给自增ID索引\n\t\t\tif (options.id.match(\"^\\\\d+$\")) {\n\t\t\t\tconst numberId = parseInt(options.id);\n\t\t\t\tif (numberId > context.menuIndex) {\n\t\t\t\t\tcontext.menuIndex = numberId;\n\t\t\t\t}\n\t\t\t}\n\t\t} else {\n\t\t\tmenuCmdId = MENU_ID_PRE + (++context.menuIndex);\n\t\t}\n\n\t\t// 创建菜单元素\n\t\tconst menuElement = document.createElement('div');\n\t\tmenuElement.id = menuCmdId;\n\t\tmenuElement.className = \"___ds-menu___\";\n\t\tmenuElement.innerHTML = name;\n\t\tif (options.title) {\n\t\t\tmenuElement.title = typeof options.title === \"function\" ? options.title() : options.title;\n\t\t}\n\t\tif (callback) {\n\t\t\tmenuElement.onclick = callback;\n\t\t}\n\t\tif (options.accessKey) {\n\t\t\t// TODO: 快捷键功能待开发，篡改猴官方文档：https://www.tampermonkey.net/documentation.php#api:GM_registerMenuCommand\n\t\t}\n\n\t\t// 将菜单元素添加到菜单列表div中\n\t\tcontext.userMenusElement.append(menuElement);\n\n\t\t// 将菜单添加到菜单集合中\n\t\tcontext.menus[menuCmdId] = {\n\t\t\tname: name,\n\t\t\tcallback: callback,\n\t\t\toptions: options,\n\t\t\telement: menuElement\n\t\t};\n\n\t\t// 返回菜单ID\n\t\treturn menuCmdId;\n\t};\n\n\t// 删除菜单\n\tapi.GM_unregisterMenuCommand = (menuCmdId) => {\n\t\tif (menuCmdId == null) {\n\t\t\treturn;\n\t\t}\n\n\t\tif (typeof menuCmdId !== \"string\") {\n\t\t\tmenuCmdId = menuCmdId.toString();\n\t\t}\n\n\t\tif (menuCmdId.indexOf(MENU_ID_PRE) !== 0) {\n\t\t\tmenuCmdId = MENU_ID_PRE + menuCmdId;\n\t\t}\n\n\t\tconst menu = context.menus[menuCmdId];\n\t\tif (menu) {\n\t\t\tmenu.element.remove();\n\t\t\tdelete context.menus[menuCmdId];\n\t\t} else {\n\t\t\tconst menuElement = document.getElementById(menuCmdId)\n\t\t\tif (menuElement) {\n\t\t\t\tmenuElement.remove();\n\t\t\t}\n\t\t}\n\t};\n\n\t// 打开新标签\n\tapi.GM_openInTab = (url, options_or_loadInBackground) => {\n\t\t// const options = typeof options_or_loadInBackground === \"boolean\"\n\t\t// \t? { loadInBackground: options_or_loadInBackground }\n\t\t// \t: (options_or_loadInBackground || {});\n\n\t\twindow.open(url)\n\t};\n\n\t// 获取配置\n\tapi.GM_getValue = (key, defaultValue) => {\n\t\tkey = PRE + key;\n\t\tconst valueStr = localStorage.getItem(key);\n\t\tif (valueStr == null || valueStr === '') {\n\t\t\treturn defaultValue;\n\t\t}\n\t\ttry {\n\t\t\treturn JSON.parse(valueStr).v;\n\t\t} catch (e) {\n\t\t}\n\t\treturn valueStr;\n\t};\n\n\t// 设置配置\n\tapi.GM_setValue = (key, value) => {\n\t\tkey = PRE + key;\n\t\tlocalStorage.setItem(key, JSON.stringify({ v: value }));\n\t};\n\n\t// 删除设置\n\tapi.GM_deleteValue = (key) => {\n\t\tkey = PRE + key;\n\t\tlocalStorage.removeItem(key);\n\t};\n\n\t// 通知\n\tapi.GM_notification = (details_or_text, ondone_or_title, image, onclick) => {\n\t\t// param1\n\t\tlet options = typeof details_or_text === \"string\" ? { text: details_or_text } : details_or_text;\n\t\tif (typeof options !== \"object\") {\n\t\t\tconsole.error(`ds_tampermonkey_${version}: GM_notification: 无效的参数值：details_or_text = ` + details_or_text);\n\t\t\treturn;\n\t\t}\n\t\t// param2\n\t\tif (typeof ondone_or_title === \"string\") {\n\t\t\toptions.title = ondone_or_title;\n\t\t} else if (typeof ondone_or_title === \"function\") {\n\t\t\toptions.ondone = ondone_or_title;\n\t\t} else if (ondone_or_title != null) {\n\t\t\tconsole.warn(`ds_tampermonkey_${version}: GM_notification: 无效的参数值：ondone_or_title = ` + ondone_or_title);\n\t\t}\n\t\t// param3\n\t\tif (typeof image === \"string\") {\n\t\t\toptions.image = image;\n\t\t} else if (image != null) {\n\t\t\tconsole.warn(`ds_tampermonkey_${version}: GM_notification: 无效的参数值：image = ` + image);\n\t\t}\n\t\t// param4\n\t\tif (typeof onclick === \"function\") {\n\t\t\toptions.onclick = onclick;\n\t\t} else if (onclick != null) {\n\t\t\tconsole.warn(`ds_tampermonkey_${version}: GM_notification: 无效的参数值：onclick = ` + onclick);\n\t\t}\n\n\t\t// 显示通知方法\n\t\tconst showNotification = () => {\n\t\t\t// 先关闭上一个通知\n\t\t\tapi.closeLastNotification();\n\n\t\t\t// 获取标题和文本\n\t\t\tlet text = options.text;\n\t\t\tlet title = options.title;\n\t\t\tif (title == null) {\n\t\t\t\ttitle = text;\n\t\t\t\ttext = null;\n\t\t\t} else {\n\t\t\t\tdelete options.title;\n\t\t\t}\n\t\t\tdelete options.text;\n\n\n\t\t\t// 创建通知属性\n\t\t\tconst notificationOptions = {\n\t\t\t\t...options,\n\t\t\t\ticon: options.image || options.icon || (context.pluginOptions ? context.pluginOptions.icon : null) || icon\n\t\t\t};\n\t\t\tif (text) notificationOptions.body = text;\n\t\t\t// 创建通知\n\t\t\tconst notification = new Notification(title, notificationOptions);\n\t\t\t// 将通知对象保存到context中\n\t\t\tconst lastNotification = {\n\t\t\t\tobj: notification,\n\t\t\t\toptions: options,\n\t\t\t\ttimeout: null\n\t\t\t}\n\t\t\tcontext.lastNotification = lastNotification;\n\t\t\t// 设置点击通知事件\n\t\t\tif (options.onclick) {\n\t\t\t\tnotification.onclick = () => options.onclick();\n\t\t\t}\n\t\t\t// 设置通知关闭事件\n\t\t\tif (typeof options.ondone === \"function\" || typeof options.onclose === \"function\") {\n\t\t\t\tnotification.onclose = () => {\n\t\t\t\t\t// 执行回调方法\n\t\t\t\t\tif (typeof options.ondone === \"function\") {\n\t\t\t\t\t\ttry {\n\t\t\t\t\t\t\toptions.ondone();\n\t\t\t\t\t\t} catch (e) {\n\t\t\t\t\t\t\tconsole.error(`ds_tampermonkey_${version}: GM_notification: ondone回调函数执行失败：`, e);\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t\t// 执行关闭方法\n\t\t\t\t\tif (typeof options.onclose === \"function\") {\n\t\t\t\t\t\ttry {\n\t\t\t\t\t\t\toptions.onclose();\n\t\t\t\t\t\t} catch (e) {\n\t\t\t\t\t\t\tconsole.error(`ds_tampermonkey_${version}: GM_notification: onclose关闭函数执行失败：`, e);\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t\t// 设置定时关闭\n\t\t\tif (options.timeout) {\n\t\t\t\tlastNotification.timeout = setTimeout(() => {\n\t\t\t\t\tcontext.lastNotification = null;\n\t\t\t\t\tnotification.close();\n\t\t\t\t}, options.timeout);\n\t\t\t}\n\t\t\treturn notification;\n\t\t};\n\t\t// 当不支持Notification API，则使用alert显示通知\n\t\tconst showAlert = () => {\n\t\t\tlet text = options.text;\n\t\t\tif (options.title) {\n\t\t\t\ttext = options.title + \": \" + text;\n\t\t\t}\n\n\t\t\talert(text);\n\t\t\tif (options.ondone) options.ondone(); // 回调\n\t\t};\n\n\t\t// 检查浏览器是否支持Notification API\n\t\tif (!(\"Notification\" in window)) {\n\t\t\tshowAlert(); // 不支持，直接使用alert显示通知\n\t\t}\n\t\t// 检查用户是否已授予权限\n\t\telse if (Notification.permission === \"granted\") {\n\t\t\t// 如果用户已授予权限，我们可以显示通知\n\t\t\tshowNotification();\n\t\t}\n\t\t// 否则，先请求权限\n\t\telse if (Notification.permission !== 'denied') {\n\t\t\tNotification.requestPermission(function (permission) {\n\t\t\t\tif (permission === \"granted\") {\n\t\t\t\t\tshowNotification(); // 用户接受权限，我们可以显示通知\n\t\t\t\t} else {\n\t\t\t\t\tshowAlert(); // 用户驳回了权限，直接使用alert显示通知\n\t\t\t\t}\n\t\t\t});\n\t\t}\n\t};\n\n\t//endregion 篡改猴标准API，由DS自定义实现 end\n\n\n\t// 设置API\n\twindow.__ds_global__ = api;\n\n\t// 模块化支持\n\tif (typeof module !== 'undefined') {\n\t\tmodule.exports = api;\n\t}\n\n\tconsole.log(`ds_tampermonkey_${version}: completed`)\n})();"
  },
  {
    "path": "packages/gui/package.json",
    "content": "{\n  \"name\": \"@docmirror/dev-sidecar-gui\",\n  \"version\": \"2.0.1\",\n  \"private\": false,\n  \"author\": {\n    \"email\": \"xiaojunnuo@qq.com\",\n    \"name\": \"Greper\"\n  },\n  \"license\": \"MPL-2.0\",\n  \"homepage\": \"https://github.com/docmirror/dev-sidecar\",\n  \"scripts\": {\n    \"serve\": \"vue-cli-service serve\",\n    \"lint\": \"vue-cli-service lint\",\n    \"electron:build\": \"vue-cli-service electron:build\",\n    \"electron\": \"vue-cli-service electron:serve\",\n    \"postinstall\": \"electron-builder install-app-deps\",\n    \"postuninstall\": \"electron-builder install-app-deps\",\n    \"electron:icons\": \"electron-icon-builder --input=./public/logo/win.png --output=build --flatten\",\n    \"electron:icons-mac\": \"electron-icon-builder --input=./public/logo/mac.png --output=build --flatten\",\n    \"electron:icons-black\": \"electron-icon-builder --input=./public/logo/win-black.png --output=build/black --flatten\"\n  },\n  \"dependencies\": {\n    \"@docmirror/dev-sidecar\": \"workspace:*\",\n    \"@docmirror/mitmproxy\": \"workspace:*\",\n    \"@starknt/shutdown-handler-napi\": \"^0.0.3\",\n    \"@starknt/sysproxy\": \"^0.0.3\",\n    \"@vscode/sudo-prompt\": \"^9.3.1\",\n    \"adm-zip\": \"^0.5.16\",\n    \"ant-design-vue\": \"^1.7.8\",\n    \"electron-baidu-tongji\": \"^1.0.5\",\n    \"electron-updater\": \"^6.3.9\",\n    \"json5\": \"^2.2.3\",\n    \"lodash\": \"^4.17.21\",\n    \"request-progress\": \"^3.0.0\",\n    \"sass\": \"^1.81.0\",\n    \"sass-loader\": \"^16.0.3\",\n    \"search-bar-vue2\": \"^1.0.0\",\n    \"vue\": \"^2.7.16\",\n    \"vue-json-editor-fix-cn\": \"^1.4.3\",\n    \"vue-router\": \"^3.6.5\"\n  },\n  \"devDependencies\": {\n    \"@babel/plugin-syntax-jsx\": \"^7.25.9\",\n    \"@vue/babel-helper-vue-jsx-merge-props\": \"^1.4.0\",\n    \"@vue/babel-preset-jsx\": \"^1.4.0\",\n    \"@vue/cli-plugin-babel\": \"^5.0.8\",\n    \"@vue/cli-service\": \"^5.0.8\",\n    \"electron\": \"^19.1.9\",\n    \"electron-builder\": \"^25.1.8\",\n    \"electron-icon-builder\": \"^2.0.1\",\n    \"json5-loader\": \"^4.0.1\",\n    \"vue-cli-plugin-electron-builder\": \"^3.0.0-alpha.4\"\n  },\n  \"browserslist\": [\n    \"> 1%\",\n    \"last 2 versions\",\n    \"not dead\"\n  ]\n}\n"
  },
  {
    "path": "packages/gui/pkg/after-all-artifact-build.js",
    "content": "const fs = require('node:fs')\nconst path = require('node:path')\nconst pkg = require('../package.json')\n\nfunction appendIntro (context, systemType, latest) {\n  const version = pkg.version\n  const partUpdateFile = `update-${systemType}-${version}.zip`\n\n  const partUpdateUrl = context.configuration.publish.url + partUpdateFile\n\n  const latestFilePath = path.join(context.outDir, latest)\n  fs.appendFile(latestFilePath, `partPackage: ${partUpdateUrl}\npartMiniVersion: 1.7.0\nreleaseNotes:\n  - 升级日志\n  - https://download.fastgit.org/docmirror/dev-sidecar/releases/download/v${version}/DevSidecar-${version}.exe\n`, (err) => {\n    if (err) {\n      console.log('修改latest 失败')\n    }\n  })\n}\nexports.default = async function (context) {\n  console.log('after-all-artifact-build')\n  appendIntro(context, 'mac', 'latest-mac.yml')\n  appendIntro(context, 'win', 'latest.yml')\n  appendIntro(context, 'linux', 'latest-linux.yml')\n}\n"
  },
  {
    "path": "packages/gui/pkg/after-pack.js",
    "content": "const fs = require('node:fs')\nconst path = require('node:path')\nconst AdmZip = require('adm-zip')\nconst pkg = require('../package.json')\n\nfunction writeAppUpdateYmlForLinux () {\n  const publishUrl = process.env.VUE_APP_PUBLISH_URL\n  const publishProvider = process.env.VUE_APP_PUBLISH_PROVIDER\n  // provider: generic\n  // url: 'http://dev-sidecar.docmirror.cn/update/preview/'\n  // updaterCacheDirName: '@docmirrordev-sidecar-gui-updater'\n  const fileContent = `provider: ${publishProvider}\nurl: '${publishUrl}'\nupdaterCacheDirName: '@docmirrordev-sidecar-gui-updater'\n`\n  console.log('write linux app-update.yml,updateUrl:', publishUrl)\n  const filePath = path.resolve('./dist_electron/linux-unpacked/resources/app-update.yml')\n  fs.writeFileSync(filePath, fileContent)\n}\nexports.default = async function (context) {\n  let targetPath\n  let systemType\n  if (context.packager.platform.nodeName === 'darwin') {\n    targetPath = path.join(context.appOutDir, `${context.packager.appInfo.productName}.app/Contents/Resources`)\n    systemType = 'mac'\n  } else if (context.packager.platform.nodeName === 'linux') {\n    targetPath = path.join(context.appOutDir, './resources')\n    systemType = 'linux'\n    writeAppUpdateYmlForLinux()\n  } else {\n    targetPath = path.join(context.appOutDir, './resources')\n    systemType = 'win'\n  }\n  const zip = new AdmZip()\n  zip.addLocalFolder(targetPath)\n  const partUpdateFile = `update-${systemType}-${pkg.version}.zip`\n  zip.writeZip(path.join(context.outDir, partUpdateFile))\n}\n"
  },
  {
    "path": "packages/gui/public/index.html",
    "content": "<!DOCTYPE html>\n<html lang=\"en\" style=\"height: 100%\">\n  <head>\n    <meta charset=\"utf-8\" />\n    <meta http-equiv=\"X-UA-Compatible\" content=\"IE=edge\" />\n    <meta name=\"viewport\" content=\"width=device-width,initial-scale=1.0\" />\n    <link rel=\"icon\" href=\"<%= BASE_URL %>favicon.ico\" />\n    <title><%= htmlWebpackPlugin.options.title %></title>\n    <script type=\"application/javascript\">\n      window.config = {}\n    </script>\n  </head>\n  <body style=\"height: 100%\">\n    <div id=\"app\" style=\"height: 100%\">\n      <div style=\"display: flex; align-items: center; justify-content: center; height: 100%; width: 100%\">\n        <img src=\"loading-spin.svg\" />\n      </div>\n    </div>\n    <!-- built files will be auto injected -->\n  </body>\n</html>\n"
  },
  {
    "path": "packages/gui/src/background/powerMonitor.js",
    "content": "import { acquireShutdownBlock, insertWndProcHook, releaseShutdownBlock, removeWndProcHook, setMainWindowHandle } from '@starknt/shutdown-handler-napi'\nimport { powerMonitor as _powerMonitor } from 'electron'\n\nclass PowerMonitor {\n  constructor () {\n    this.setup = false\n    this._listeners = []\n    this._shutdownCallback = null\n  }\n\n  /**\n   * @param {import('electron').BrowserWindow} window\n   */\n  setupMainWindow (window) {\n    if (!this.setup) {\n      setMainWindowHandle(window.getNativeWindowHandle())\n      this.setup = true\n    }\n  }\n\n  addListener (event, listener) {\n    return this.on(event, listener)\n  }\n\n  removeListener (event, listener) {\n    return this.off(event, listener)\n  }\n\n  removeAllListeners (event) {\n    if (event === 'shutdown' && process.platform === 'win32') {\n      this._listeners = []\n      if (this._shutdownCallback) {\n        removeWndProcHook()\n        releaseShutdownBlock()\n        this._shutdownCallback = null\n      }\n    } else {\n      return _powerMonitor.removeAllListeners(event)\n    }\n  }\n\n  on (event, listener) {\n    if (event === 'shutdown' && process.platform === 'win32') {\n      if (!this._shutdownCallback) {\n        this._shutdownCallback = async () => {\n          await Promise.all(this._listeners.map(fn => fn()))\n          releaseShutdownBlock()\n        }\n        insertWndProcHook(this._shutdownCallback)\n        acquireShutdownBlock('正在停止 DevSidecar 代理')\n      }\n      this._listeners.push(listener)\n    } else {\n      return _powerMonitor.on(event, listener)\n    }\n  }\n\n  off (event, listener) {\n    if (event === 'shutdown' && process.platform === 'win32') {\n      this._listeners = this._listeners.filter(fn => fn !== listener)\n    } else {\n      return _powerMonitor.off(event, listener)\n    }\n  }\n\n  once (event, listener) {\n    if (event === 'shutdown' && process.platform === 'win32') {\n      return this.on(event, listener)\n    } else {\n      return _powerMonitor.once(event, listener)\n    }\n  }\n\n  emit (event, ...args) {\n    return _powerMonitor.emit(event, ...args)\n  }\n\n  eventNames () {\n    return _powerMonitor.eventNames()\n  }\n\n  getMaxListeners () {\n    return _powerMonitor.getMaxListeners()\n  }\n\n  listeners (event) {\n    return _powerMonitor.listeners(event)\n  }\n\n  rawListeners (event) {\n    return _powerMonitor.rawListeners(event)\n  }\n\n  listenerCount (event, listener) {\n    return _powerMonitor.listenerCount(event, listener)\n  }\n\n  /**\n   * @returns {boolean}\n   */\n  get onBatteryPower () {\n    return _powerMonitor.onBatteryPower\n  }\n\n  /**\n   * @param {number} idleThreshold\n   * @returns {'active'|'idle'|'locked'|'unknown'}\n   */\n  getSystemIdleState (idleThreshold) {\n    return _powerMonitor.getSystemIdleState(idleThreshold)\n  }\n\n  /**\n   * @returns {number}\n   */\n  getSystemIdleTime () {\n    return _powerMonitor.getSystemIdleTime()\n  }\n\n  /**\n   * @returns {'unknown'|'nominal'|'fair'|'serious'|'critical'}\n   */\n  getCurrentThermalState () {\n    return _powerMonitor.getCurrentThermalState()\n  }\n\n  /**\n   * @returns {boolean}\n   */\n  isOnBatteryPower () {\n    return _powerMonitor.isOnBatteryPower()\n  }\n}\n\nexport const powerMonitor = new PowerMonitor()\n"
  },
  {
    "path": "packages/gui/src/background.js",
    "content": "'use strict'\n/* global __static */\nimport path from 'node:path'\nimport DevSidecar from '@docmirror/dev-sidecar'\nimport { app, BrowserWindow, dialog, globalShortcut, ipcMain, Menu, nativeImage, nativeTheme, powerMonitor, protocol, Tray } from 'electron'\nimport minimist from 'minimist'\nimport { createProtocol } from 'vue-cli-plugin-electron-builder/lib'\nimport backend from './bridge/backend'\nimport jsonApi from '@docmirror/mitmproxy/src/json'\nimport log from './utils/util.log.gui'\n\nlog.info(`background.js start, platform is ${process.platform}`)\n\nconst isWindows = process.platform === 'win32'\nconst isLinux = process.platform === 'linux'\nconst isMac = process.platform === 'darwin'\n\nconst isDevelopment = process.env.NODE_ENV !== 'production'\n\n// 避免其他系统出现异常，只有 Windows 使用 './background/powerMonitor'\nlet _powerMonitor = powerMonitor\nif (isWindows) {\n  try {\n    _powerMonitor = require('./background/powerMonitor').powerMonitor\n  } catch (e) {\n    log.error(`加载 './background/powerMonitor' 失败，现捕获异常并使用默认的 powerMonitor。\\r\\n目前，启动着DS重启电脑时，将无法正常关闭系统代理，届时请自行关闭系统代理！\\r\\n捕获的异常信息:`, e)\n  }\n}\n\n// Keep a global reference of the window object, if you don't, the window will\n// be closed automatically when the JavaScript object is garbage collected.\nlet win\nlet winIsHidden = false\n\nlet tray // 防止被内存清理\nlet forceClose = false\n\ntry {\n  DevSidecar.api.config.reload()\n} catch (e) {\n  log.error('配置加载失败:', e)\n}\n\nlet hideDockWhenWinClose = DevSidecar.api.config.get().app.dock.hideWhenWinClose || false\n// Scheme must be registered before the app is ready\nprotocol.registerSchemesAsPrivileged([\n  { scheme: 'app', privileges: { secure: true, standard: true } },\n])\n\nfunction openDevTools () {\n  try {\n    log.debug('尝试打开 `开发者工具`')\n    win.webContents.openDevTools()\n    log.debug('打开 `开发者工具` 成功')\n  } catch (e) {\n    log.error('打开 `开发者工具` 失败:', e)\n  }\n}\n\nfunction closeDevTools () {\n  try {\n    log.debug('尝试关闭 `开发者工具`')\n    win.webContents.closeDevTools()\n    log.debug('关闭 `开发者工具` 成功')\n  } catch (e) {\n    log.error('关闭 `开发者工具` 失败:', e)\n  }\n}\n\nfunction switchDevTools () {\n  if (!win || !win.webContents) {\n    return\n  }\n  if (win.webContents.isDevToolsOpened()) {\n    closeDevTools()\n  } else {\n    openDevTools()\n  }\n}\n\n// 隐藏主窗口，并创建托盘，绑定关闭事件\nfunction setTray () {\n  // const topMenu = Menu.buildFromTemplate({})\n  // Menu.setApplicationMenu(topMenu)\n  // 用一个 Tray 来表示一个图标,这个图标处于正在运行的系统的通知区\n  // 通常被添加到一个 context menu 上.\n  // 系统托盘右键菜单\n  const trayMenuTemplate = [\n    {\n      // 系统托盘图标目录\n      label: 'DevTools (F12)',\n      click: switchDevTools,\n    },\n    {\n      // 系统托盘图标目录\n      label: '退出',\n      click: () => {\n        log.info('force quit')\n        forceClose = true\n        quit('系统托盘图标-退出')\n      },\n    },\n  ]\n  // 设置系统托盘图标\n  const iconRootPath = path.join(__dirname, '../extra/icons/tray')\n  let iconPath = path.join(iconRootPath, 'icon.png')\n  const iconWhitePath = path.join(iconRootPath, 'icon-white.png')\n  const iconBlackPath = path.join(iconRootPath, 'icon-black.png')\n  if (isMac) {\n    iconPath = nativeTheme.shouldUseDarkColors ? iconWhitePath : iconBlackPath\n  }\n\n  const trayIcon = nativeImage.createFromPath(iconPath)\n  const appTray = new Tray(trayIcon)\n\n  // 当桌面主题更新时\n  if (isMac) {\n    nativeTheme.on('updated', () => {\n      log.info('i am changed')\n      if (nativeTheme.shouldUseDarkColors) {\n        log.info('i am dark.')\n        tray.setImage(iconWhitePath)\n      } else {\n        log.info('i am light.')\n        tray.setImage(iconBlackPath)\n        // tray.setPressedImage(iconWhitePath)\n      }\n    })\n  }\n\n  // 图标的上下文菜单\n  const contextMenu = Menu.buildFromTemplate(trayMenuTemplate)\n\n  // 设置托盘悬浮提示\n  appTray.setToolTip('DevSidecar-开发者边车辅助工具')\n  // 单击托盘小图标显示应用\n  appTray.on('click', () => {\n    // 显示主程序\n    showWin()\n  })\n\n  appTray.on('right-click', () => {\n    setTimeout(() => {\n      appTray.popUpContextMenu(contextMenu)\n    }, 200)\n  })\n\n  return appTray\n}\n\nfunction checkHideWin () {\n  const config = DevSidecar.api.config.get()\n\n  // 配置为false时，不需要校验\n  if (!config.app.needCheckHideWindow) {\n    return true\n  }\n\n  // 如果是linux，且没有设置快捷键，则提示先设置快捷键\n  if (isLinux && !hasShortcut(config.app.showHideShortcut)) {\n    dialog.showMessageBox({\n      type: 'info',\n      title: '提示：请先设置快捷键',\n      message: '由于大部分 Linux 系统没有系统托盘，所以需使用快捷键呼出窗口。\\n但您还未设置快捷键，请先到 “设置” 页面中设置好快捷键，再关闭窗口。',\n      buttons: ['确定'],\n    })\n    return false\n  }\n\n  return true\n}\n\nfunction hideWin (reason = '', needCheck = false) {\n  if (win) {\n    if (needCheck && !checkHideWin()) {\n      return\n    }\n\n    win.hide()\n    if (isMac && hideDockWhenWinClose) {\n      app.dock.hide()\n    }\n    winIsHidden = true\n  } else {\n    log.warn(`win is null, do not hide win, reason: ${reason}`)\n  }\n}\n\nfunction showWin () {\n  if (win) {\n    win.show()\n  } else {\n    log.warn('win is null, do not show win')\n  }\n  if (app.dock) {\n    app.dock.show()\n  }\n  winIsHidden = false\n}\n\nfunction changeAppConfig (config) {\n  if (config.hideDockWhenWinClose != null) {\n    hideDockWhenWinClose = config.hideDockWhenWinClose\n  }\n}\n\nfunction createWindow (startHideWindow, autoQuitIfError = true) {\n  // Create the browser window.\n  const windowSize = DevSidecar.api.config.get().app.windowSize || {}\n\n  try {\n    win = new BrowserWindow({\n      width: windowSize.width || 900,\n      height: windowSize.height || 750,\n      title: 'DevSidecar',\n      webPreferences: {\n        enableRemoteModule: true,\n        contextIsolation: false,\n        nativeWindowOpen: true, // ADD THIS\n        // Use pluginOptions.nodeIntegration, leave this alone\n        // See nklayman.github.io/vue-cli-plugin-electron-builder/guide/security.html#node-integration for more info\n        nodeIntegration: true, // process.env.ELECTRON_NODE_INTEGRATION\n      },\n      show: !startHideWindow,\n      icon: path.join(__static, 'icon.png'),\n    })\n  } catch (e) {\n    log.error('创建窗口失败:', e)\n    dialog.showErrorBox('错误', `创建窗口失败: ${e.message}`)\n    if (autoQuitIfError) {\n      quit('创建窗口失败')\n    }\n    return false\n  }\n  winIsHidden = !!startHideWindow\n\n  Menu.setApplicationMenu(null)\n  win.setMenu(null)\n\n  // !!IMPORTANT\n  if (isWindows && typeof _powerMonitor.setupMainWindow === 'function') {\n    _powerMonitor.setupMainWindow(win)\n  }\n\n  if (process.env.WEBPACK_DEV_SERVER_URL) {\n    // Load the url of the dev server if in development mode\n    win.loadURL(process.env.WEBPACK_DEV_SERVER_URL)\n    if (!process.env.IS_TEST) {\n      setTimeout(openDevTools, 2000)\n    }\n  } else {\n    createProtocol('app')\n    // Load the index.html when not in development\n    win.loadURL('app://./index.html')\n  }\n\n  if (startHideWindow) {\n    hideWin('startHideWindow')\n  }\n\n  win.on('closed', async (...args) => {\n    log.info('win closed:', ...args)\n    win = null\n    tray = null\n  })\n\n  ipcMain.on('close', async (event, message) => {\n    if (message.value === 1) {\n      quit('ipc receive \"close\"')\n    } else {\n      hideWin('ipc receive \"close\"', true)\n    }\n  })\n\n  win.on('close', (e, ...args) => {\n    log.info('win close:', e, ...args)\n    if (forceClose) {\n      return\n    }\n    e.preventDefault()\n    const config = DevSidecar.api.config.get()\n    const closeStrategy = config.app.closeStrategy\n    if (closeStrategy === 1) {\n      // 直接退出\n      quit('win close')\n    } else if (closeStrategy === 2) {\n      // 隐藏窗口\n      hideWin('win close', true)\n    } else {\n      // 弹窗提示，选择关闭策略\n      win.webContents.send('close.showTip', { closeStrategy, showHideShortcut: config.app.showHideShortcut })\n    }\n  })\n\n  win.on('session-end', async (e, ...args) => {\n    log.info('win session-end:', e, ...args)\n    await quit('win session-end')\n  })\n\n  const shortcut = (event, input) => {\n    if (input.key === 'F12' && input.type === 'keyUp' && !input.control && !input.shift && !input.alt && !input.meta) {\n      // 按 F12，打开/关闭 开发者工具\n      event.preventDefault()\n      switchDevTools()\n    } else if (input.key === 'F5' && input.type === 'keyUp' && !input.control && !input.shift && !input.alt && !input.meta) {\n      // 按 F5，刷新页面\n      event.preventDefault()\n      win.webContents.reload()\n    } else {\n      // 全文检索框（SearchBar）相关快捷键\n      if ((input.key === 'F' || input.key === 'f') && input.type === 'keyDown' && input.control && !input.shift && !input.alt && !input.meta) {\n        // 按 Ctrl + F，显示或隐藏全文检索框（SearchBar）\n        event.preventDefault()\n        win.webContents.send('search-bar', { key: 'show-hide' })\n      } else if (input.key === 'Escape' && input.type === 'keyUp' && !input.control && !input.shift && !input.alt && !input.meta) {\n        // 按 ESC，隐藏全文检索框（SearchBar）\n        event.preventDefault()\n        win.webContents.send('search-bar', { key: 'hide' })\n      } else if (input.key === 'F3' && input.type === 'keyDown' && !input.control && !input.shift && !input.alt && !input.meta) {\n        // 按 F3，全文检索框（SearchBar）定位到下一个\n        event.preventDefault()\n        win.webContents.send('search-bar', { key: 'next' })\n      } else if (input.key === 'F3' && input.type === 'keyDown' && !input.control && input.shift && !input.alt && !input.meta) {\n        // 按 Shift + F3，全文检索框（SearchBar）定位到上一个\n        event.preventDefault()\n        win.webContents.send('search-bar', { key: 'previous' })\n      }\n    }\n  }\n\n  // 监听键盘事件\n  win.webContents.on('before-input-event', (event, input) => {\n    win.webContents.executeJavaScript('config')\n      .then((value) => {\n        console.info('window.config:', value, ', key:', input.key)\n        if (!value || (value.disableBeforeInputEvent !== true && value.disableBeforeInputEvent !== 'true')) {\n          shortcut(event, input)\n        }\n      })\n      .catch(() => {\n        shortcut(event, input)\n      })\n  })\n\n  // 监听渲染进程发送过来的消息\n  win.webContents.on('ipc-message', (event, channel, message, ...args) => {\n    console.info('win ipc-message:', event, channel, message, ...args)\n\n    // 记录日志\n    if (channel && channel.startsWith('[ERROR]')) {\n      log.error('win ipc-message:', channel.substring(7), message, ...args)\n    } else {\n      log.info('win ipc-message:', channel, message, ...args)\n    }\n\n    if (channel === 'change-showHideShortcut') {\n      registerShowHideShortcut(message)\n    }\n  })\n\n  return true\n}\n\nasync function beforeQuit () {\n  log.info('before quit')\n  return DevSidecar.api.shutdown()\n}\nasync function quit (reason) {\n  log.info('app quit:', reason)\n\n  if (tray) {\n    tray.displayBalloon({ title: '正在关闭', content: '关闭中,请稍候。。。' })\n  }\n  await beforeQuit()\n  forceClose = true\n  app.quit()\n}\n\nfunction hasShortcut (showHideShortcut) {\n  return showHideShortcut && showHideShortcut.length > 1\n}\n\nfunction registerShowHideShortcut (showHideShortcut) {\n  globalShortcut.unregisterAll()\n  if (hasShortcut(showHideShortcut)) {\n    try {\n      const registerSuccess = globalShortcut.register(DevSidecar.api.config.get().app.showHideShortcut, () => {\n        if (winIsHidden) {\n          showWin()\n        } else {\n          if (!win.isFocused()) {\n            win.focus() // 如果窗口打开着，但没有获取焦点，则获取焦点，而不是hide\n          } else {\n            hideWin('shortcut')\n          }\n        }\n      })\n\n      if (registerSuccess) {\n        log.info('注册快捷键成功:', DevSidecar.api.config.get().app.showHideShortcut)\n      } else {\n        log.error('注册快捷键失败:', DevSidecar.api.config.get().app.showHideShortcut)\n      }\n    } catch (e) {\n      log.error('注册快捷键异常:', DevSidecar.api.config.get().app.showHideShortcut, ', error:', e)\n    }\n  }\n}\n\nfunction initApp () {\n  if (isMac) {\n    app.whenReady().then(() => {\n      app.dock.setIcon(path.join(__dirname, '../extra/icons/512x512-2.png'))\n    })\n  }\n\n  // 全局监听快捷键，用于 显示/隐藏 窗口\n  app.whenReady().then(async () => {\n    registerShowHideShortcut(DevSidecar.api.config.get().app.showHideShortcut)\n  })\n}\n\n// -------------执行开始---------------\ntry {\n  app.disableHardwareAcceleration() // 禁用gpu\n\n  // 开启后是否默认隐藏window\n  let startHideWindow = !DevSidecar.api.config.get().app.startShowWindow\n  if (app.getLoginItemSettings().wasOpenedAsHidden) {\n    startHideWindow = true\n  } else if (process.argv) {\n    const args = minimist(process.argv)\n    log.info('start args:', args)\n\n    // 通过启动参数，判断是否隐藏窗口\n    const hideWindowArg = `${args.hideWindow}`\n    if (hideWindowArg === 'true' || hideWindowArg === '1') {\n      startHideWindow = true\n    } else if (hideWindowArg === 'false' || hideWindowArg === '0') {\n      startHideWindow = false\n    }\n  }\n  log.info('startHideWindow = ', startHideWindow, ', app.getLoginItemSettings() = ', jsonApi.stringify2(app.getLoginItemSettings()))\n\n  // 禁止双开\n  const isFirstInstance = app.requestSingleInstanceLock()\n  if (!isFirstInstance) {\n    log.info('app quit: is second instance（禁止双开）')\n    setTimeout(() => {\n      app.quit()\n    }, 1000)\n  } else {\n    app.on('before-quit', async () => {\n      log.info('before-quit')\n      if (process.platform === 'darwin') {\n        quit('before quit')\n      }\n    })\n    app.on('will-quit', () => {\n      log.info('应用关闭，注销所有快捷键')\n      globalShortcut.unregisterAll()\n    })\n    app.on('second-instance', (event, commandLine) => {\n      log.info('new app started, command:', commandLine)\n      if (win) {\n        showWin()\n        win.focus()\n      }\n    })\n\n    // Quit when all windows are closed.\n    app.on('window-all-closed', () => {\n      log.info('window-all-closed')\n      // On macOS it is common for applications and their menu bar\n      // to stay active until the user quits explicitly with Cmd + Q\n      if (process.platform !== 'darwin') {\n        quit('window-all-closed')\n      }\n    })\n\n    app.on('activate', () => {\n      // On macOS it's common to re-create a window in the app when the\n      // dock icon is clicked and there are no other windows open.\n      if (win == null) {\n        createWindow(false, false)\n      } else {\n        showWin()\n      }\n    })\n\n    // initApp()\n\n    // This method will be called when Electron has finished\n    // initialization and is ready to create browser windows.\n    // Some APIs can only be used after this event occurs.\n    app.on('ready', async () => {\n      // if (isDevelopment && !process.env.IS_TEST) {\n      //   // Install Vue Devtools\n      //   try {\n      //     await installExtension(VUEJS_DEVTOOLS)\n      //   } catch (e) {\n      //     log.error('Vue Devtools failed to install:', e.toString())\n      //   }\n      // }\n\n      try {\n        if (!createWindow(startHideWindow)) {\n          return // 创建窗口失败，应用将关闭\n        }\n      } catch (err) {\n        log.error('createWindow error:', err)\n      }\n\n      try {\n        const context = { win, app, beforeQuit, quit, ipcMain, dialog, log, api: DevSidecar.api, changeAppConfig }\n        backend.install(context) // 模块安装\n      } catch (err) {\n        log.error('install modules error:', err)\n      }\n\n      try {\n        // 最小化到托盘\n        tray = setTray()\n      } catch (err) {\n        log.error('setTray error:', err)\n      }\n\n      _powerMonitor.on('shutdown', async (e) => {\n        if (e) {\n          e.preventDefault()\n        }\n        log.info('系统关机，恢复代理设置')\n        await quit('系统关机')\n      })\n    })\n  }\n\n  initApp()\n\n  // Exit cleanly on request from parent process in development mode.\n  if (isDevelopment) {\n    if (process.platform === 'win32') {\n      process.on('message', (data) => {\n        if (data === 'graceful-exit') {\n          quit('graceful-exit')\n        }\n      })\n    } else {\n      process.on('SIGINT', () => {\n        quit('SIGINT')\n      })\n    }\n  }\n  // 系统关机和重启时的操作\n  process.on('exit', () => {\n    quit('进程结束，退出app')\n  })\n\n  log.info('background.js finished')\n} catch (e) {\n  log.error('应用启动过程中，出现未知异常：', e)\n}\n"
  },
  {
    "path": "packages/gui/src/bridge/api/backend.js",
    "content": "import fs from 'node:fs'\nimport path from 'node:path'\nimport DevSidecar from '@docmirror/dev-sidecar'\nimport { ipcMain } from 'electron'\nimport lodash from 'lodash'\n\nconst jsonApi = require('@docmirror/mitmproxy/src/json')\nconst pk = require('../../../package.json')\nconst configFromFiles = require('@docmirror/dev-sidecar/src/config/index.js').configFromFiles\nconst log = require('../../utils/util.log.gui')\nconst dateUtil = require('@docmirror/dev-sidecar/src/utils/util.date')\n\nconst mitmproxyPath = path.join(__dirname, 'mitmproxy.js')\nprocess.env.DS_EXTRA_PATH = path.join(__dirname, '../extra/')\n\nconst getDefaultConfigBasePath = function () {\n  return DevSidecar.api.config.get().server.setting.userBasePath\n}\n\nconst localApi = {\n  /**\n   * 返回所有api列表，供vue来ipc调用\n   * @returns {[]} api列表\n   */\n  getApiList () {\n    const core = lodash.cloneDeep(DevSidecar.api)\n    const local = lodash.cloneDeep(localApi)\n    lodash.merge(core, local)\n    const list = []\n    _deepFindFunction(list, core, '')\n    // log.info('api list:', list)\n    return list\n  },\n  info: {\n    get () {\n      return {\n        version: pk.version,\n      }\n    },\n    getConfigDir () {\n      return getDefaultConfigBasePath()\n    },\n    getLogDir () {\n      return configFromFiles.app.logFileSavePath || path.join(getDefaultConfigBasePath(), '/logs/')\n    },\n    getSystemPlatform (throwIfUnknown = false) {\n      return DevSidecar.api.shell.getSystemPlatform(throwIfUnknown)\n    },\n  },\n  /**\n   * 软件设置\n   */\n  setting: {\n    load () {\n      const settingPath = _getSettingsPath()\n      let setting = {}\n      if (fs.existsSync(settingPath)) {\n        const file = fs.readFileSync(settingPath)\n        try {\n          setting = jsonApi.parse(file.toString())\n          log.info('读取 setting.json 成功:', settingPath)\n        } catch (e) {\n          log.error('读取 setting.json 失败:', settingPath, ', error:', e)\n        }\n        if (setting == null) {\n          setting = {}\n        }\n      }\n      if (setting.overwall == null) {\n        setting.overwall = false\n      }\n\n      if (setting.installTime == null) {\n        // 设置安装时间\n        setting.installTime = dateUtil.now()\n\n        // 初始化 rootCa.setuped\n        if (setting.rootCa == null) {\n          setting.rootCa = {\n            setuped: false,\n            desc: '根证书未安装',\n          }\n        }\n\n        // 保存 setting.json\n        localApi.setting.save(setting)\n      }\n      return setting\n    },\n    save (setting = {}) {\n      const settingPath = _getSettingsPath()\n      try {\n        fs.writeFileSync(settingPath, jsonApi.stringify(setting))\n        log.info('保存 setting.json 配置文件成功:', settingPath)\n      } catch (e) {\n        log.error('保存 setting.json 配置文件失败:', settingPath, ', error:', e)\n      }\n    },\n  },\n  /**\n   * 启动所有\n   * @returns {Promise<void>}\n   */\n  startup () {\n    return DevSidecar.api.startup({ mitmproxyPath })\n  },\n  server: {\n    /**\n     * 启动代理服务\n     * @returns {Promise<{port: *}>}\n     */\n    start () {\n      return DevSidecar.api.server.start({ mitmproxyPath })\n    },\n    /**\n     * 重启代理服务\n     * @returns {Promise<void>}\n     */\n    restart () {\n      return DevSidecar.api.server.restart({ mitmproxyPath })\n    },\n  },\n}\n\nfunction _deepFindFunction (list, parent, parentKey) {\n  for (const key in parent) {\n    const item = parent[key]\n    if (item instanceof Function) {\n      list.push(parentKey + key)\n    } else if (item instanceof Object) {\n      _deepFindFunction(list, item, `${parentKey + key}.`)\n    }\n  }\n}\n\nfunction _getSettingsPath () {\n  const dir = getDefaultConfigBasePath()\n  if (!fs.existsSync(dir)) {\n    fs.mkdirSync(dir)\n  } else {\n    // 兼容1.7.3及以下版本的配置文件处理逻辑\n    const newFilePath = path.join(dir, '/setting.json')\n    const oldFilePath = path.join(dir, '/setting.json5')\n    if (!fs.existsSync(newFilePath) && fs.existsSync(oldFilePath)) {\n      return oldFilePath // 如果新文件不存在，且旧文件存在，则返回旧文件路径\n    }\n    return newFilePath\n  }\n  return path.join(dir, '/setting.json')\n}\n\nfunction invoke (api, param) {\n  let target = lodash.get(localApi, api)\n  if (target == null) {\n    target = lodash.get(DevSidecar.api, api)\n  }\n  if (target == null) {\n    log.info('找不到此接口方法：', api)\n  }\n  const ret = target(param)\n  // log.info('api:', api, 'ret:', ret)\n  return ret\n}\n\nasync function doStart () {\n  // 开启自动下载远程配置\n  await DevSidecar.api.config.startAutoDownloadRemoteConfig()\n  // 启动所有\n  localApi.startup()\n}\n\nexport default {\n  install ({ win }) {\n    // 接收view的方法调用\n    ipcMain.handle('apiInvoke', async (event, args) => {\n      const api = args[0]\n      let param\n      if (args.length >= 2) {\n        param = args[1]\n      }\n      return invoke(api, param)\n    })\n    // 注册从core里来的事件，并转发给view\n    DevSidecar.api.event.register('status', (event) => {\n      log.info('bridge on status, event:', event)\n      if (win) {\n        win.webContents.send('status', { ...event })\n      }\n    })\n    DevSidecar.api.event.register('error', (event) => {\n      log.error('bridge on error, event:', event)\n      if (win) {\n        win.webContents.send('error.core', event)\n      }\n    })\n    DevSidecar.api.event.register('speed', (event) => {\n      if (win) {\n        win.webContents.send('speed', event)\n      }\n    })\n\n    // 合并用户配置\n    DevSidecar.api.config.reload()\n    doStart()\n  },\n  devSidecar: DevSidecar,\n  invoke,\n}\n"
  },
  {
    "path": "packages/gui/src/bridge/api/open-enable-loopback.js",
    "content": "/* global __static */\nimport DevSidecar from '@docmirror/dev-sidecar'\nimport sudoPrompt from '@vscode/sudo-prompt'\nimport { join } from 'node:path'\nimport log from '../../utils/util.log.gui'\n\nexport default {\n  open () {\n    const options = {\n      name: 'EnableLoopback',\n      icns: process.platform === 'darwin' ? join(__static, 'icon.icns') : undefined,\n      env: { PARAM: 'VALUE' },\n    }\n    const exeFile = DevSidecar.api.shell.extraPath.getEnableLoopbackPath()\n    const sudoCommand = [`\"${exeFile}\"`]\n\n    return new Promise((resolve, reject) => {\n      sudoPrompt.exec(\n        sudoCommand.join(' '),\n        options,\n        (error, _, stderr) => {\n          if (stderr) {\n            log.error(`[sudo-prompt] 发生错误: ${stderr}`)\n          }\n\n          if (error) {\n            reject(error)\n          } else {\n            resolve(undefined)\n          }\n        },\n      )\n    })\n  },\n}\n"
  },
  {
    "path": "packages/gui/src/bridge/auto-start/backend.js",
    "content": "import DevSidecar from '@docmirror/dev-sidecar'\n\nasync function setAutoStartForLinux (app, enable = true) {\n  const path = app.getPath('exe')\n  if (enable) {\n    const cmd = `\nmkdir -p ~/.config/autostart/\ncat >> ~/.config/autostart/dev-sidecar.desktop <<EOF\n[Desktop Entry]\nType=Application\nExec=${path}\nHidden=false\nNoDisplay=false\nX-GNOME-Autostart-enabled=true\nName[en_US]=DevSidecar\nName=DevSidecar\nComment[en_US]=\nComment=\nEOF\n`\n    await DevSidecar.api.shell.exec(cmd)\n  } else {\n    const removeStart = 'sudo rm ~/.config/autostart/dev-sidecar.desktop -rf'\n    await DevSidecar.api.shell.exec(removeStart)\n  }\n}\nexport default {\n  install (context) {\n    const { ipcMain, app } = context\n\n    // 定义事件，渲染进程中直接使用\n\n    // 开启 开机自启动\n    ipcMain.on('auto-start', async (event, message) => {\n      console.log('auto start', message)\n      const isLinux = DevSidecar.api.shell.getSystemPlatform() === 'linux'\n      if (message.value) {\n        if (isLinux) {\n          await setAutoStartForLinux(app, true)\n        } else {\n          app.setLoginItemSettings({\n            openAtLogin: true,\n            openAsHidden: true,\n            args: [\n              '--hideWindow',\n              '\"true\"',\n            ],\n          })\n        }\n\n        event.sender.send('auto-start', { key: 'enabled', value: true })\n      } else {\n        if (isLinux) {\n          await setAutoStartForLinux(app, false)\n        } else {\n          app.setLoginItemSettings({\n            openAtLogin: false,\n            openAsHidden: false,\n            args: [],\n          })\n        }\n\n        event.sender.send('auto-start', { key: 'enabled', value: false })\n      }\n    })\n  },\n}\n"
  },
  {
    "path": "packages/gui/src/bridge/auto-start/front.js",
    "content": "function install (app, api) {\n  api.ipc.on('auto-start', (event, message) => {\n    if (message.value === true) {\n      app.$message.info('已添加开机自启')\n    } else {\n      app.$message.info('已取消开机自启')\n    }\n  })\n  api.autoStart = {\n    async enabled (value) {\n      api.ipc.send('auto-start', { key: 'enabled', value })\n    },\n  }\n}\n\nexport default {\n  install,\n}\n"
  },
  {
    "path": "packages/gui/src/bridge/backend.js",
    "content": "import api from './api/backend'\nimport autoStart from './auto-start/backend'\nimport fileSelector from './file-selector/backend'\nimport tongji from './tongji/backend'\nimport update from './update/backend'\nimport log from '../utils/util.log.gui'\n\nconst modules = {\n  api, // 核心接口模块\n  fileSelector, // 文件选择模块\n  tongji, // 统计模块\n  update, // 自动更新\n  autoStart,\n}\nexport default {\n  install (context) {\n    for (const module in modules) {\n      log.info('install module:', module)\n      modules[module].install(context)\n    }\n  },\n  ...modules,\n}\n"
  },
  {
    "path": "packages/gui/src/bridge/error/front.js",
    "content": "let latestConfirmTime = null\n\nfunction install (app, api) {\n  api.ipc.on('error.core', (event, message) => {\n    console.error('view on error', message)\n    const key = message.key\n    if (key === 'server') {\n      handleServerStartError(message, message.error, app, api)\n    }\n  })\n  api.ipc.on('error', (event, message) => {\n    console.error('error', event, message)\n  })\n}\n\nfunction handleServerStartError (message, err, app, api) {\n  if (message.value === 'EADDRINUSE') {\n    // 避免重复弹窗\n    const now = Date.now()\n    if (latestConfirmTime != null && now - latestConfirmTime < 1000) {\n      return\n    }\n    latestConfirmTime = now\n\n    app.$confirm({\n      title: '端口被占用，代理服务启动失败',\n      content: '是否要杀掉占用进程？您也可以点击取消，然后前往加速服务->基本设置中修改代理端口',\n      onOk () {\n        api.config.get().then((config) => {\n          console.log('config:', config)\n          api.shell.killByPort({ port: config.server.port }).then((ret) => {\n            app.$message.info('杀掉进程成功，请重试开启代理服务')\n          })\n        })\n      },\n      onCancel () {\n        console.log('Cancel')\n      },\n    })\n  } else {\n    app.$message.error(`加速服务启动失败：${message.message}`)\n  }\n}\n\nexport default {\n  install,\n}\n"
  },
  {
    "path": "packages/gui/src/bridge/file-selector/backend.js",
    "content": "export default {\n  install (context) {\n    const { ipcMain, dialog, log } = context\n    ipcMain.on('file-selector', (event, message) => {\n      if (message.key === 'open') {\n        /**\n         * @type {Electron.OpenDialogOptions}\n         */\n        const options = message.options || {}\n        if (options.properties == null || options.properties.length === 0) {\n          options.properties = ['openFile']\n        }\n\n        dialog.showOpenDialog(options).then((result) => {\n          if (result.canceled) {\n            event.sender.send('file-selector', { key: 'canceled' })\n          } else {\n            event.sender.send('file-selector', { key: 'selected', value: result.filePaths })\n          }\n        }).catch((err) => {\n          log.error('选择文件失败:', err)\n          event.sender.send('file-selector', { key: 'error', error: err })\n        })\n      }\n    })\n  },\n}\n"
  },
  {
    "path": "packages/gui/src/bridge/file-selector/front.js",
    "content": "function install (app, api) {\n  api.fileSelector = {\n\n    /**\n     * 打开文件选择框\n     *\n     * 支持传参方式：\n     * 1. open(String defaultPath)\n     * 2. open(String defaultPath, String properties)\n     * 3. open(null, String properties)\n     * 4. open(String defaultPath, Object options)\n     * 5. open(Object options)\n     *\n     * @param value\n     * @param {Electron.OpenDialogOptions} options\n     * @returns {Promise<unknown>} promise\n     */\n    open (value = null, options = null) {\n      if (options == null && value && typeof value !== 'string') {\n        options = { ...value }\n        value = null\n      } else {\n        if (typeof options === 'string') {\n          if (options === 'dir') {\n            options = 'openDirectory'\n          } else if (options === 'file') {\n            options = 'openFile'\n          }\n\n          options = { properties: [options] } // options 为字符串时，视为 properties 属性的值\n        } else {\n          options = options || {}\n        }\n      }\n\n      // 如果没有 defaultPath，则使用 value 作为 defaultPath\n      if (!options.defaultPath && value && typeof value === 'string') {\n        options.defaultPath = value\n      }\n\n      return new Promise((resolve, reject) => {\n        api.ipc.send('file-selector', { key: 'open', options })\n        api.ipc.on('file-selector', (event, message) => {\n          console.log('selector', message)\n          if (message.key === 'selected') {\n            resolve(message.value)\n          } else if (message.key === 'canceled') {\n            resolve('') // 没有选择文件\n          } else if (message.key === 'error') {\n            reject(message.error)\n          } else {\n            reject(new Error('未知的响应'))\n          }\n          api.ipc.on('file-selector', () => {})\n        })\n      })\n    },\n  }\n}\n\nexport default {\n  install,\n}\n"
  },
  {
    "path": "packages/gui/src/bridge/front.js",
    "content": "// import api from './api/front'\nimport autoStart from './auto-start/front'\nimport error from './error/front'\nimport fileSelector from './file-selector/front'\nimport onClose from './on-close/front'\nimport tongji from './tongji/front'\nimport update from './update/front'\n\nconst modules = {\n  // api, // 核心接口模块\n  error,\n  fileSelector, // 文件选择模块\n  tongji, // 统计模块\n  update, // 自动更新\n  autoStart,\n  onClose,\n}\nexport default {\n  install (app, api, router) {\n    for (const module in modules) {\n      modules[module].install(app, api, router)\n    }\n  },\n  ...modules,\n}\n"
  },
  {
    "path": "packages/gui/src/bridge/mitmproxy.js",
    "content": "const fs = require('node:fs')\nconst path = require('node:path')\nconst server = require('@docmirror/mitmproxy')\nconst jsonApi = require('@docmirror/mitmproxy/src/json')\nconst log = require('@docmirror/mitmproxy/src/utils/util.log.server') // 当前脚本是在 server 的进程中执行的，所以使用 mitmproxy 中的logger\n\nconst configPath = process.argv[2]\nconst configJson = fs.readFileSync(configPath)\nlog.info('读取 running.json by gui bridge 成功:', configPath)\nlet config\ntry {\n  config = jsonApi.parse(configJson.toString())\n} catch (e) {\n  log.error(`running.json 文件内容格式不正确，文件路径：${configPath}，文件内容: ${configJson.toString()}, error:`, e)\n  config = {}\n}\n// const scriptDir = '../extra/scripts/'\n// config.setting.script.defaultDir = path.join(__dirname, scriptDir)\n// const pacFilePath = '../extra/pac/pac.txt'\n// config.plugin.overwall.pac.customPacFilePath = path.join(__dirname, pacFilePath)\nconfig.setting.rootDir = path.join(__dirname, '../')\nlog.info(`start mitmproxy by gui bridge, configPath: ${configPath}`)\nserver.start(config)\n"
  },
  {
    "path": "packages/gui/src/bridge/on-close/front.js",
    "content": "let closeType = 2\nlet doSave = false\n\nfunction install (app, api) {\n  api.ipc.on('close.showTip', (event, message) => {\n    console.info('ipc channel: \"close.showTip\", event:', event, ', message:', message)\n    function onRadioChange (event) {\n      closeType = event.target.value\n    }\n    function onCheckChange (event) {\n      doSave = event.target.checked\n    }\n    app.$confirm({\n      title: '关闭策略',\n      content: (h) => (\n        <div>\n          <div style=\"margin-top:10px\">\n            <a-radio-group vOn:change={onRadioChange} defaultValue={closeType}>\n              <a-radio value={1}>直接关闭</a-radio>\n              <a-radio value={2}>最小化到系统托盘</a-radio>\n            </a-radio-group>\n          </div>\n          <div style=\"margin-top:10px\">\n            <a-checkbox vOn:change={onCheckChange} defaultChecked={doSave}>\n              记住本次选择，不再提示\n            </a-checkbox>\n          </div>\n          <div style=\"margin-top:20px\">\n            提示：打开窗口的快捷键为\n            <code>{message.showHideShortcut || '无'}</code>\n          </div>\n        </div>\n      ),\n      async onOk () {\n        console.log('OK. closeType=', closeType, ', doSave:', doSave)\n        if (doSave) {\n          await api.config.update({ app: { closeStrategy: closeType } })\n        }\n        api.ipc.send('close', { key: 'selected', value: closeType })\n      },\n      onCancel () {\n        console.log('Cancel. closeType=', closeType)\n      },\n    })\n  })\n}\n\nexport default {\n  install,\n}\n"
  },
  {
    "path": "packages/gui/src/bridge/tongji/backend.js",
    "content": "/**\n * first step\n * @param {*} ipcMain\n */\nfunction ebtMain (ipcMain) {\n  const isDevelopment = process.env.NODE_ENV !== 'production'\n  const request = require('request')\n  /* istanbul ignore else */\n  if (!(ipcMain && ipcMain.on)) {\n    throw new TypeError('require ipcMain')\n  }\n\n  // step 2\n  ipcMain.on('electron-baidu-tongji-message', (event, arg) => {\n    // electron 生产模式下是直接请求文件系统，没有 http 地址\n    // 前台拿不到 hm.js 的内容\n    request({\n      url: `https://hm.baidu.com/hm.js?${arg}`,\n      method: 'GET',\n      headers: {\n        Referer: 'https://hm.baidu.com/',\n      },\n    }, (err, response, body) => {\n      if (err) {\n        console.error('百度统计请求出错', err)\n        return\n      }\n      const rource = '(h.c.b.su=h.c.b.u||document.location.href),h.c.b.u=f.protocol+\"//\"+document.location.host+'\n      /* istanbul ignore else */\n      if (body && body.includes(rource)) {\n        // step 3\n        let text = body\n\n        /* istanbul ignore else */\n        if (!isDevelopment) {\n          // 百度统计可能改规则了，不统计 file:// 开始的请求\n          // 这里强制替换为 https\n          const target = '(h.c.b.su=h.c.b.u||\"https://\"+c.dm[0]+a[1]),h.c.b.u=\"https://\"+c.dm[0]+'\n          const target2 = '\"https://\"+c.dm[0]+window.location.pathname+window.location.hash'\n          text = body.replace(rource, target).replace(/window.location.href/g, target2)\n        }\n        console.log('baidu tonji: ret')\n        event.sender.send('electron-baidu-tongji-reply', { text, isDevelopment })\n      }\n    })\n  })\n}\n\nexport default {\n  install (context) {\n    ebtMain(context.ipcMain)\n  },\n}\n"
  },
  {
    "path": "packages/gui/src/bridge/tongji/front.js",
    "content": "/**\n * second step\n * @param {*} ipcRenderer\n * @param {*} siteId\n * @param {*} router\n */\nfunction ebtRenderer (ipcRenderer, siteId, router) {\n  /* istanbul ignore else */\n  if (!(ipcRenderer && ipcRenderer.on && ipcRenderer.send)) {\n    throw new TypeError('require ipcRenderer')\n  }\n\n  /* istanbul ignore else */\n  if (!(siteId && typeof siteId === 'string')) {\n    throw new TypeError('require siteId')\n  }\n\n  // step 4\n  ipcRenderer.on('electron-baidu-tongji-reply', (_, { text, isDevelopment }) => {\n    console.log('electron-baidu-tongji-reply')\n    /* istanbul ignore else */\n    if (isDevelopment) {\n      document.body.classList.add('electron-baidu-tongji_dev')\n    }\n\n    window._hmt = window._hmt || []\n\n    const hm = document.createElement('script')\n    hm.text = text\n\n    const head = document.getElementsByTagName('head')[0]\n    head.appendChild(hm)\n\n    // Vue单页应用时，监听router的每次变化\n    // 把虚拟的url地址赋给百度统计的API接口\n\n    /* istanbul ignore else */\n    if (router && router.beforeEach) {\n      router.beforeEach((to, _, next) => {\n        /* istanbul ignore else */\n        if (to.path) {\n          window._hmt.push(['_trackPageview', `/#${to.fullPath}`])\n          console.log('baidu trace', to.fullPath)\n        }\n\n        next()\n      })\n    }\n  })\n\n  // step 1\n  ipcRenderer.send('electron-baidu-tongji-message', siteId)\n}\n\nexport default {\n  install (app, api, router) {\n    const BAIDU_SITE_ID = 'f2d170ce560aef0005b689f28697f852'\n    // 百度统计\n    const { ipcRenderer } = require('electron')\n    ebtRenderer(ipcRenderer, BAIDU_SITE_ID, router)\n  },\n  ebtRenderer,\n}\n"
  },
  {
    "path": "packages/gui/src/bridge/update/backend.js",
    "content": "import fs from 'node:fs'\nimport path from 'node:path'\nimport DevSidecar from '@docmirror/dev-sidecar'\nimport AdmZip from 'adm-zip'\nimport { ipcMain } from 'electron'\nimport { autoUpdater } from 'electron-updater'\nimport request from 'request'\nimport progress from 'request-progress'\nimport pkg from '../../../package.json'\nimport appPathUtil from '../../utils/util.apppath'\nimport log from '../../utils/util.log.gui'\nimport { isNewVersion } from '@docmirror/dev-sidecar/src/utils/util.version'\n\nconst isMac = process.platform === 'darwin'\nconst isLinux = process.platform === 'linux'\n\nconst curVersion = pkg.version\nconst isPreRelease = curVersion.includes('-')\n\nfunction downloadFile (uri, filePath, onProgress, onSuccess, onError) {\n  log.info('download url', uri)\n  progress(request(uri), {\n    // throttle: 2000,                    // Throttle the progress event to 2000ms, defaults to 1000ms\n    // delay: 1000,                       // Only start to emit after 1000ms delay, defaults to 0ms\n    // lengthHeader: 'x-transfer-length'  // Length header to use, defaults to content-length\n  })\n    .on('progress', (state) => {\n      onProgress(state.percent * 100)\n      log.log('progress', state.percent)\n    })\n    .on('error', (err) => {\n      // Do something with err\n      log.error('下载升级包失败:', err)\n      onError(err)\n    })\n    .on('end', () => {\n      // Do something after request finishes\n      onSuccess()\n    })\n    .pipe(fs.createWriteStream(filePath))\n}\n\n/**\n * 检测更新，在你想要检查更新的时候执行，renderer事件触发后的操作自行编写\n */\nfunction updateHandle (app, api, win, beforeQuit, quit, log) {\n  // // 更新前，删除本地安装包 ↓\n  // const updaterCacheDirName = 'dev-sidecar-updater'\n  // const updatePendingPath = path.join(autoUpdater.app.baseCachePath, updaterCacheDirName, 'pending')\n  // fs.emptyDir(updatePendingPath)\n  // // 更新前，删除本地安装包 ↑\n  const message = {\n    error: '更新失败',\n    checking: '检查更新中',\n    updateAva: '发现新版本',\n    updateNotAva: '当前为最新版本，无需更新',\n  }\n  // 本地开发环境，改变app-update.yml地址\n  if (process.env.NODE_ENV === 'development') {\n    // const publishUrl = process.env.VUE_APP_PUBLISH_URL\n    // autoUpdater.setFeedURL({\n    //   provider: 'generic',\n    //   url: publishUrl\n    // })\n    if (isMac) {\n      autoUpdater.updateConfigPath = path.join(__dirname, 'mac/dev-sidecar.app/Contents/Resources/app-update.yml')\n    } else if (isLinux) {\n      autoUpdater.updateConfigPath = path.join(__dirname, 'linux-unpacked/resources/app-update.yml')\n    } else {\n      autoUpdater.updateConfigPath = path.join(__dirname, 'win-unpacked/resources/app-update.yml')\n    }\n  }\n\n  log.info('auto updater', autoUpdater.getFeedURL())\n  autoUpdater.autoDownload = false\n\n  let partPackagePath = null\n\n  // 检查更新\n  const releasesApiUrl = 'https://api.github.com/repos/docmirror/dev-sidecar/releases'\n  async function checkForUpdatesFromGitHub () {\n    request(releasesApiUrl, { headers: { 'User-Agent': `DS/${curVersion}`, 'Server-Name': 'baidu.com' } }, (error, response, body) => {\n      try {\n        if (error) {\n          log.error('检查更新失败:', error)\n          const errorMsg = `检查更新失败：${error}`\n          win.webContents.send('update', { key: 'error', action: 'checkForUpdate', error: errorMsg })\n          return\n        }\n        if (response && response.statusCode === 200) {\n          if (body == null || body.length < 2) {\n            log.warn('检查更新失败，github API返回数据为空:', body)\n            win.webContents.send('update', { key: 'error', action: 'checkForUpdate', error: '检查更新失败，github 返回数据为空' })\n            return\n          }\n\n          // 尝试解析API响应内容\n          let data\n          try {\n            data = JSON.parse(body)\n          } catch {\n            log.error('检查更新失败，github API返回数据格式不正确:', body)\n            win.webContents.send('update', { key: 'error', action: 'checkForUpdate', error: '检查更新失败，github API返回数据格式不正确' })\n            return\n          }\n\n          if (typeof data !== 'object' || data.length === undefined) {\n            log.error('检查更新失败，github API返回数据不是数组:', body)\n            win.webContents.send('update', { key: 'error', action: 'checkForUpdate', error: '检查更新失败，github API返回数据不是数组' })\n            return\n          }\n\n          log.debug('github api返回的release数据：', JSON.stringify(data, null, '\\t'))\n\n          // 检查更新\n          for (let i = 0; i < data.length; i++) {\n            const versionData = data[i]\n\n            // log.debug('版本数据：', versionData)\n\n            if (!versionData.assets || versionData.assets.length === 0) {\n              log.info('跳过空版本，即未上传过安装包：', versionData.name)\n              continue // 跳过空版本，即未上传过安装包\n            }\n            if (!versionData.name.match(/^v?\\d+(\\.\\d+)*(-.+)?$/g)) {\n              log.info('跳过即 “不是正式，又不是预发布” 的版本:', versionData.name)\n              continue // 跳过即 “不是正式，又不是预发布” 的版本\n            }\n            if (!isPreRelease && DevSidecar.api.config.get().app.skipPreRelease && (versionData.name.includes('-') || versionData.prerelease)) {\n              log.info('跳过预发布版本:', versionData.name)\n              continue // 跳过预发布版本\n            }\n\n            log.info('最近正式版本：', versionData.name)\n\n            // 获取版本号\n            let onlineVersion = versionData.name\n            if (onlineVersion.indexOf('v') === 0) {\n              onlineVersion = onlineVersion.substring(1)\n            }\n\n            // 比对版本号，是否为新版本\n            const isNew = isNewVersion(onlineVersion, curVersion, log)\n            log.info(`版本比对结果：isNewVersion('${onlineVersion}', '${curVersion}') = ${isNew}`)\n            if (isNew > 0) {\n              log.info(`检查更新：发现新版本 '${onlineVersion}'，当前版本号为 '${curVersion}'`)\n              win.webContents.send('update', {\n                key: 'available',\n                value: {\n                  version: onlineVersion,\n                  releaseNotes: versionData.body\n                    ? (versionData.body.replace(/\\r\\n/g, '\\n').replace(/https:\\/\\/github.com\\/docmirror\\/dev-sidecar/g, '').replace(/(?<=(^|\\n))[ \\t]*(?:#[ #]*)?#\\s*/g, '') || '无')\n                    : '无',\n                },\n              })\n            } else {\n              log.info(`检查更新：没有新版本，最近发布的版本号为 '${onlineVersion}'，而当前版本号为 '${curVersion}'`)\n              win.webContents.send('update', { key: 'notAvailable' })\n            }\n\n            return // 只检查最近一个版本\n          }\n\n          log.info('检查更新-没有正式版本数据')\n          win.webContents.send('update', { key: 'notAvailable' })\n        } else {\n          log.error('检查更新失败, status:', response.statusCode, ', body:', body)\n\n          let bodyObj\n          try {\n            bodyObj = JSON.parse(body)\n          } catch {\n            bodyObj = null\n          }\n\n          let message\n          if (response) {\n            message = `检查更新失败: ${bodyObj && bodyObj.message ? bodyObj.message : response.message}, code: ${response.statusCode}`\n          } else {\n            message = `检查更新失败: ${bodyObj && bodyObj.message ? bodyObj.message : body}`\n          }\n          win.webContents.send('update', { key: 'error', action: 'checkForUpdate', error: message })\n        }\n      } catch (e) {\n        log.error('检查更新失败:', e)\n        win.webContents.send('update', { key: 'error', action: 'checkForUpdate', error: `检查更新失败:${e.message}` })\n      }\n    })\n  }\n\n  // 下载升级包\n  function downloadPart (app, value) {\n    const appPath = appPathUtil.getAppRootPath(app)\n    const fileDir = path.join(appPath, 'update')\n    log.info('download dir:', fileDir)\n    try {\n      fs.accessSync(fileDir, fs.constants.F_OK)\n    } catch {\n      fs.mkdirSync(fileDir)\n    }\n    const filePath = path.join(fileDir, `${value.version}.zip`)\n\n    downloadFile(value.partPackage, filePath, (data) => {\n      win.webContents.send('update', { key: 'progress', value: Number.parseInt(data) })\n    }, () => {\n      // 文件下载完成\n      win.webContents.send('update', { key: 'progress', value: 100 })\n      log.info('升级包下载成功：', filePath)\n      partPackagePath = filePath\n      win.webContents.send('update', {\n        key: 'downloaded',\n        value,\n      })\n    }, (error) => {\n      sendUpdateMessage({ key: 'error', value: error, error })\n    })\n  }\n\n  async function updatePart (app, api, value, partPackagePath) {\n    const appPath = appPathUtil.getAppRootPath(app)\n    const platform = api.shell.getSystemPlatform()\n    let target = path.join(appPath, 'resources')\n    if (platform === 'mac') {\n      target = path.join(appPath, 'Resources')\n    }\n    const length = fs.statSync(partPackagePath)\n    log.info('安装包大小:', length)\n\n    log.info('开始解压缩，安装升级包:', partPackagePath, target)\n\n    try {\n      await beforeQuit()\n      app.relaunch()\n      // 解压缩\n      const zip = new AdmZip(partPackagePath)\n      zip.extractAllTo(target, true)\n      log.info('安装完成，重启app')\n    } finally {\n      app.exit(0)\n    }\n  }\n\n  autoUpdater.on('error', (error) => {\n    log.warn('autoUpdater error:', error)\n    sendUpdateMessage({ key: 'error', value: error, error })\n    // dialog.showErrorBox('Error: ', error == null ? 'unknown' : (error.stack || error).toString())\n  })\n  autoUpdater.on('checking-for-update', () => {\n    log.info('autoUpdater checking-for-update')\n    sendUpdateMessage({ key: 'checking', value: message.checking })\n  })\n  autoUpdater.on('update-available', (info) => {\n    log.info('autoUpdater update-available')\n    sendUpdateMessage({ key: 'available', value: info })\n  })\n  autoUpdater.on('update-not-available', () => {\n    log.info('autoUpdater update-not-available')\n    sendUpdateMessage({ key: 'notAvailable', value: message.updateNotAva })\n  })\n  // 更新下载进度\n  autoUpdater.on('download-progress', (progressObj) => {\n    log.info('autoUpdater download-progress')\n    win.webContents.send('update', { key: 'progress', value: Number.parseInt(progressObj.percent) })\n  })\n  // 更新完成，重启应用\n  autoUpdater.on('update-downloaded', (info) => {\n    log.info('download complete, version:', info.version)\n    win.webContents.send('update', {\n      key: 'downloaded',\n      value: info,\n    })\n  })\n\n  ipcMain.on('update', (e, arg) => {\n    if (arg.key === 'doUpdateNow') {\n      if (partPackagePath) {\n        updatePart(app, api, arg.value, partPackagePath)\n        return\n      }\n      // some code here to handle event\n      beforeQuit().then(() => {\n        autoUpdater.quitAndInstall()\n        if (app) {\n          setTimeout(() => {\n            app.exit()\n          }, 1000)\n        }\n      })\n    } else if (arg.key === 'checkForUpdate') {\n      // 执行自动更新检查\n      log.info('autoUpdater checkForUpdates:', arg.fromUser)\n\n      // 调用 github API，获取release数据，来检查更新\n      // autoUpdater.checkForUpdates()\n      checkForUpdatesFromGitHub()\n    } else if (arg.key === 'downloadUpdate') {\n      // 下载新版本\n      log.info('autoUpdater downloadUpdate')\n      autoUpdater.downloadUpdate()\n    } else if (arg.key === 'downloadPart') {\n      // 下载增量更新版本\n      log.info('autoUpdater downloadPart')\n      downloadPart(app, arg.value)\n    }\n  })\n  // 通过main进程发送事件给renderer进程，提示更新信息\n  function sendUpdateMessage (message) {\n    log.info('autoUpdater sendUpdateMessage')\n    win.webContents.send('update', message)\n  }\n\n  log.info('auto update inited')\n  return autoUpdater\n}\n\nexport default {\n  install (context) {\n    const { app, api, win, beforeQuit, quit, log } = context\n    if (process.env.NODE_ENV === 'development') {\n      Object.defineProperty(app, 'isPackaged', {\n        get () {\n          return true\n        },\n      })\n    }\n    updateHandle(app, api, win, beforeQuit, quit, log)\n  },\n}\n"
  },
  {
    "path": "packages/gui/src/bridge/update/front.js",
    "content": "function install (app, api) {\n  const updateParams = app.$global.update = { fromUser: false, autoDownload: false, progress: 0, checking: false, downloading: false, newVersion: false, isFullUpdate: true }\n  api.ipc.on('update', (event, message) => {\n    console.log('on message', event, message)\n    handleUpdateMessage(message, app)\n  })\n\n  api.update = {\n    checkForUpdate (fromUser) {\n      if (fromUser != null) {\n        updateParams.fromUser = fromUser\n      }\n      updateParams.checking = true\n      api.ipc.send('update', { key: 'checkForUpdate', fromUser })\n    },\n    downloadUpdate () {\n      api.ipc.send('update', { key: 'downloadUpdate' })\n    },\n    downloadPart (value) {\n      // 增量更新\n      api.ipc.send('update', { key: 'downloadPart', value })\n    },\n    doUpdateNow () {\n      api.ipc.send('update', { key: 'doUpdateNow' })\n    },\n  }\n\n  function handleUpdateMessage (message) {\n    const type = message.key\n    if (type === 'available') {\n      updateParams.checking = false\n      updateParams.newVersionData = message.value\n      foundNewVersion(message.value)\n    } else if (type === 'notAvailable') {\n      updateParams.checking = false\n      noNewVersion()\n    } else if (type === 'downloaded') {\n      // 更新包已下载完成，让用户确认是否更新\n      updateParams.downloading = false\n      console.log('updateParams', updateParams)\n      newUpdateIsReady(message.value)\n    } else if (type === 'progress') {\n      progressUpdate(message.value)\n    } else if (type === 'error') {\n      updateParams.checking = false\n      updateParams.downloading = false\n      if (message.action === 'checkForUpdate' && updateParams.newVersionData) {\n        // 如果检查更新报错了，但刚才成功拿到过一次数据，就拿之前的数据\n        foundNewVersion(updateParams.newVersionData)\n      } else {\n        if (updateParams.fromUser === false && message.action === 'checkForUpdate') {\n          return // 不是手动检查更新，不提示错误信息，避免打扰\n        }\n        const error = message.error\n        app.$message.error((error == null ? '未知错误' : (error.stack || error).toString()))\n      }\n    }\n  }\n\n  function noNewVersion () {\n    updateParams.newVersion = false\n    if (updateParams.fromUser) {\n      app.$message.info('当前已经是最新版本')\n    }\n  }\n\n  function progressUpdate (value) {\n    updateParams.progress = value\n  }\n\n  function openGithubUrl () {\n    api.ipc.openExternal('https://github.com/docmirror/dev-sidecar/releases')\n  }\n\n  function goManualUpdate (value) {\n    updateParams.newVersion = false\n    app.$confirm({\n      title: '暂不提供自动升级',\n      cancelText: '取消',\n      okText: '打开链接',\n      width: 420,\n      content: (h) => {\n        return (\n          <div>\n            <div>\n              请前往\n              <a onClick={openGithubUrl}>github项目release页面</a>\n              下载新版本手动安装\n            </div>\n            <div><a onClick={openGithubUrl}>https://github.com/docmirror/dev-sidecar/releases</a></div>\n          </div>\n        )\n      },\n      onOk () {\n        openGithubUrl()\n      },\n    })\n  }\n\n  // /**\n  //  * 是否小版本升级\n  //  * @param value\n  //  */\n  // async function isSupportPartUpdate (value) {\n  //   const info = await api.info.get()\n  //   console.log('升级版本:', value.version)\n  //   console.log('增量更新最小版本:', value.partMiniVersion)\n  //   console.log('当前版本:', info.version)\n  //   if (!value.partPackage) {\n  //     return false\n  //   }\n  //   return !!(value.partMiniVersion && value.partMiniVersion < info.version)\n  // }\n\n  async function downloadNewVersion (value) {\n    // 暂时取消自动更新功能\n    goManualUpdate(value)\n\n    // const platform = await api.shell.getSystemPlatform()\n    // console.log(`download new version: ${JSON.stringify(value)}, platform: ${platform}`)\n    // if (platform === 'linux') {\n    //   goManualUpdate(value)\n    //   return\n    // }\n    // const partUpdate = await isSupportPartUpdate(value)\n    // if (partUpdate) {\n    //   // 有增量更新\n    //   api.update.downloadPart(value)\n    // } else {\n    //   if (platform === 'mac') {\n    //     goManualUpdate(value)\n    //     return\n    //   }\n    //   updateParams.downloading = true\n    //   api.update.downloadUpdate()\n    // }\n  }\n  function foundNewVersion (value) {\n    updateParams.newVersion = true\n\n    if (updateParams.autoDownload !== false) {\n      app.$message.info('发现新版本，正在下载中...')\n\n      downloadNewVersion(value)\n      return\n    }\n    console.log(value)\n    app.$confirm({\n      title: `发现新版本：v${value.version}`,\n      cancelText: '暂不升级',\n      okText: '升级',\n      width: 700,\n      content: (h) => {\n        if (value.releaseNotes) {\n          const notes = []\n          if (typeof value.releaseNotes === 'string') {\n            const releaseNotes = value.releaseNotes.replace(/\\r\\n/g, '\\n')\n            return (\n              <div>\n                <div>\n                  发布公告：\n                  <a onClick={openGithubUrl}>https://github.com/docmirror/dev-sidecar/releases</a>\n                </div>\n                <hr />\n                <pre style=\"max-height:350px;font-family:auto\">\n                  {releaseNotes}\n                </pre>\n              </div>\n            )\n          } else {\n            for (const note of value.releaseNotes) {\n              notes.push(<li>{note}</li>)\n            }\n            return (\n              <div>\n                <div>\n                  发布公告：\n                  <a onClick={openGithubUrl}>https://github.com/docmirror/dev-sidecar/releases</a>\n                </div>\n                <div>更新内容：</div>\n                <ol>{notes}</ol>\n              </div>\n            )\n          }\n        }\n      },\n      onOk () {\n        console.log('OK')\n        downloadNewVersion(value)\n      },\n      onCancel () {\n        console.log('Cancel')\n      },\n    })\n  }\n\n  function newUpdateIsReady (value) {\n    updateParams.downloading = false\n    console.log(value)\n    app.$confirm({\n      title: `新版本(v${value.version})已准备好，是否立即升级?`,\n      cancelText: '暂不升级',\n      okText: '立即升级',\n      width: 700,\n      content: (h) => {\n        if (value.releaseNotes) {\n          const notes = []\n          if (typeof value.releaseNotes === 'string') {\n            const releaseNotes = value.releaseNotes.replace(/\\r\\n/g, '\\n')\n            return (\n              <div>\n                <div>\n                  发布公告：\n                  <a onClick={openGithubUrl}>https://github.com/docmirror/dev-sidecar/releases</a>\n                </div>\n                <hr />\n                <pre style=\"max-height:350px;font-family:auto\">\n                  {releaseNotes}\n                </pre>\n              </div>\n            )\n          } else {\n            for (const note of value.releaseNotes) {\n              notes.push(<li>{note}</li>)\n            }\n            return (\n              <div>\n                <div>\n                  发布公告：\n                  <a onClick={openGithubUrl}>https://github.com/docmirror/dev-sidecar/releases</a>\n                </div>\n                <div>更新内容：</div>\n                <ol>{notes}</ol>\n              </div>\n            )\n          }\n        }\n      },\n      onOk () {\n        api.update.doUpdateNow()\n      },\n    })\n  }\n}\n\nexport default {\n  install,\n}\n"
  },
  {
    "path": "packages/gui/src/main.js",
    "content": "import antd from 'ant-design-vue'\nimport Vue from 'vue'\nimport VueRouter from 'vue-router'\nimport SearchBar from 'search-bar-vue2'\nimport { ipcRenderer } from 'electron'\nimport view from './view'\nimport App from './view/App.vue'\nimport DsContainer from './view/components/container'\nimport routes from './view/router'\nimport 'ant-design-vue/dist/antd.css'\nimport './view/style/index.scss'\nimport './view/style/theme/dark.scss' // 暗色主题\n\ntry {\n  window.onerror = (message, source, lineno, colno, error) => {\n    ipcRenderer.send(`[ERROR] JavaScript脚本异常：Error in ${source} at line ${lineno}: ${message}`, error)\n  }\n} catch (e) {\n  console.error('监听 window.onerror 出现异常:', e)\n}\n\ntry {\n  console.info('main.js start')\n  ipcRenderer.send('main.js start')\n\n  Vue.config.productionTip = false\n  Vue.use(antd)\n  Vue.use(VueRouter)\n  Vue.use(SearchBar)\n  Vue.component(DsContainer)\n  // 3. 创建 router 实例，然后传 `routes` 配置\n  // 你还可以传别的配置参数, 不过先这么简单着吧。\n  const router = new VueRouter({\n    routes, // (缩写) 相当于 routes: routes\n  })\n  const app = new Vue({\n    router,\n    render: h => h(App),\n  })\n  view.initApi(app).then(async (api) => {\n    // 初始化status\n    try {\n      await view.initPre(Vue, api)\n      app.$mount('#app')\n      view.initModules(app, router)\n    } catch (e) {\n      console.error('view初始化出现未知异常：', e)\n      ipcRenderer.send('view初始化出现未知异常：', e)\n    }\n  })\n\n  // fix vue-router NavigationDuplicated\n  const VueRouterPush = VueRouter.prototype.push\n  VueRouter.prototype.push = function push (location) {\n    return VueRouterPush.call(this, location).catch(err => err)\n  }\n  const VueRouterReplace = VueRouter.prototype.replace\n  VueRouter.prototype.replace = function replace (location) {\n    return VueRouterReplace.call(this, location).catch(err => err)\n  }\n\n  console.info('main.js finished')\n  ipcRenderer.send('main.js finished')\n} catch (e) {\n  console.error('页面加载出现未知异常：', e)\n  ipcRenderer.send('[ERROR] 页面加载出现未知异常：', e)\n}\n"
  },
  {
    "path": "packages/gui/src/utils/util.apppath.js",
    "content": "import os from 'node:os'\nimport path from 'node:path'\nimport log from './util.log.gui'\n\nfunction getSystemPlatform (throwIfUnknown = false) {\n  switch (os.platform()) {\n    case 'darwin':\n      return 'mac'\n    case 'linux':\n      return 'linux'\n    case 'win32':\n      return 'windows'\n    case 'win64':\n      return 'windows'\n    default:\n      log.error(`UNKNOWN OS TYPE: ${os.platform()}`)\n      if (throwIfUnknown) {\n        throw new Error(`UNKNOWN OS TYPE ${os.platform()}`)\n      } else {\n        return 'unknown-os'\n      }\n  }\n}\n\nexport default {\n  getAppRootPath (app) {\n    const exePath = app.getPath('exe')\n    if (getSystemPlatform() === 'mac') {\n      return path.join(exePath, '../../')\n    }\n    return path.join(exePath, '../')\n  },\n}\n"
  },
  {
    "path": "packages/gui/src/utils/util.log.gui.js",
    "content": "const loggerFactory = require('@docmirror/dev-sidecar/src/utils/util.logger')\n\nconst logger = loggerFactory.getLogger('gui')\n\nmodule.exports = logger\n"
  },
  {
    "path": "packages/gui/src/view/App.vue",
    "content": "<script>\nimport { ipcRenderer } from 'electron'\nimport createMenus from '@/view/router/menu'\nimport zhCN from 'ant-design-vue/lib/locale-provider/zh_CN'\nimport { colorTheme } from './composables/theme'\n\nexport default {\n  name: 'App',\n  data () {\n    return {\n      locale: zhCN,\n      info: {},\n      menus: undefined,\n      config: undefined,\n      hideSearchBar: true,\n      searchBarIsFocused: false,\n      searchBarInputKeyupTimeout: null,\n    }\n  },\n  computed: {\n    themeClass () {\n      return `theme-${colorTheme.value}`\n    },\n    theme () {\n      return colorTheme.value\n    },\n  },\n  mounted () {\n    let theme = this.config.app.theme\n    if (this.config.app.theme === 'system') {\n      theme = window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light'\n    }\n\n    colorTheme.value = theme\n  },\n  created () {\n    this.menus = createMenus(this)\n    this.config = this.$global.config\n    this.$api.info.get().then((ret) => {\n      this.info = ret\n    })\n\n    ipcRenderer.on('search-bar', (_, message) => {\n      if (window.config.disableSearchBar) {\n        this.hideSearchBar = true\n        return\n      }\n\n      // 如果不是显示/隐藏操作，并且还未显示检索框，先按显示操作处理\n      if (!message.key.includes('hide') && this.hideSearchBar) {\n        message = { key: 'show-hide' }\n      }\n\n      try {\n        if (message.key === 'show-hide') { // 显示/隐藏\n          const hide = message.hideSearchBar != null ? message.hideSearchBar : !this.hideSearchBar\n\n          // 如果为隐藏操作，但SearchBar未隐藏且未获取焦点，则获取焦点\n          if (hide && !this.hideSearchBar && !this.searchBarIsFocused) {\n            this.doSearchBarInputFocus()\n            return\n          }\n\n          this.hideSearchBar = hide\n\n          // 显示后，获取输入框焦点\n          if (!this.hideSearchBar) {\n            this.doSearchBarInputFocus()\n          } else {\n            this.searchBarIsFocused = false\n          }\n        } else if (message.key === 'hide') { // 隐藏\n          this.hideSearchBar = true\n          this.searchBarIsFocused = false\n        } else if (message.key === 'next') { // 下一项\n          this.$refs.searchBar.next()\n        } else if (message.key === 'previous') { // 上一项\n          this.$refs.searchBar.previous()\n        }\n      } catch (e) {\n        console.error('操作SearchBar出现异常：', e)\n      }\n\n      const input = this.getSearchBarInput()\n      if (input) {\n        input.addEventListener('focus', this.onSearchBarInputFocus)\n        input.addEventListener('blur', this.onSearchBarInputBlur)\n        input.addEventListener('keydown', this.onSearchBarInputKeydown)\n        input.addEventListener('keyup', this.onSearchBarInputKeyup)\n      }\n    })\n  },\n  methods: {\n    getSearchBarInput () {\n      return this.$refs.searchBar.$el.querySelector('input[type=text]')\n    },\n    onSearchBarInputFocus () {\n      this.searchBarIsFocused = true\n    },\n    onSearchBarInputBlur () {\n      this.searchBarIsFocused = false\n    },\n    onSearchBarInputKeydown () {\n      clearTimeout(this.searchBarInputKeyupTimeout)\n    },\n    onSearchBarInputKeyup (e) {\n      if (!this.$refs.searchBar || e.key === 'Enter' || e.key === 'F3') {\n        return\n      }\n      clearTimeout(this.searchBarInputKeyupTimeout)\n      this.searchBarInputKeyupTimeout = setTimeout(() => {\n        // 连续调用以下两个方法，为了获取检索结果中的第一项\n        this.$refs.searchBar.next()\n        this.$refs.searchBar.previous()\n      }, 150)\n    },\n    doSearchBarInputFocus () {\n      setTimeout(() => {\n        const input = this.getSearchBarInput()\n        if (input) {\n          input.focus()\n        }\n      }, 100)\n    },\n    titleClick (item) {\n      console.log('title click:', item)\n    },\n    menuClick (item) {\n      console.log('menu click:', item)\n      window.config.disableSearchBar = false\n      this.$router.replace(item.path)\n    },\n    async openExternal (url) {\n      await this.$api.ipc.openExternal(url)\n    },\n  },\n}\n</script>\n\n<template>\n  <a-config-provider :locale=\"locale\">\n    <div class=\"ds_layout\" :class=\"themeClass\">\n      <SearchBar ref=\"searchBar\"\n                 :root=\"'#document'\"\n                 :highlightClass=\"'search-bar-highlight'\"\n                 :selectedClass=\"'selected-highlight'\"\n                 :hiden.sync=\"hideSearchBar\"\n                 style=\"inset:auto auto 53px 210px; background-color:#ddd\"\n      />\n      <a-layout>\n        <a-layout-sider :theme=\"theme\" style=\"overflow-y: auto\">\n          <div class=\"logo\" />\n          <div class=\"aside\">\n            <a-menu\n              mode=\"inline\"\n              :default-selected-keys=\"[$route.fullPath]\"\n              :default-open-keys=\"['/plugin']\"\n            >\n              <template v-for=\"(item) of menus\">\n                <a-sub-menu v-if=\"item.children && item.children.length > 0\" :key=\"item.path\" @titleClick=\"titleClick(item)\">\n                  <span slot=\"title\"><a-icon :type=\"item.icon ? item.icon : 'file'\" /><span>{{ item.title }}</span></span>\n                  <a-menu-item v-for=\"(sub) of item.children\" :key=\"sub.path\" @click=\"menuClick(sub)\">\n                    <a-icon :type=\"sub.icon ? sub.icon : 'file'\" /> {{ sub.title }}\n                  </a-menu-item>\n                </a-sub-menu>\n                <a-menu-item v-else :key=\"item.path\" @click=\"menuClick(item)\">\n                  <a-icon :type=\"item.icon ? item.icon : 'file'\" />\n                  <span class=\"nav-text\">{{ item.title }}</span>\n                </a-menu-item>\n              </template>\n            </a-menu>\n          </div>\n        </a-layout-sider>\n        <a-layout>\n          <!-- <a-layout-header>Header</a-layout-header> -->\n          <a-layout-content>\n            <router-view id=\"document\" />\n          </a-layout-content>\n          <a-layout-footer>\n            <div class=\"footer\">\n              ©2020-2025 docmirror.cn by <a @click=\"openExternal('https://github.com/greper')\">Greper</a>, <a @click=\"openExternal('https://github.com/wangliang181230')\">WangLiang</a>  <span>{{ info.version }}</span>\n            </div>\n          </a-layout-footer>\n        </a-layout>\n      </a-layout>\n    </div>\n  </a-config-provider>\n</template>\n\n<style lang=\"scss\">\nbody {\n  height: 100%;\n}\n.ds_layout {\n  font-family: Avenir, Helvetica, Arial, sans-serif;\n  -webkit-font-smoothing: antialiased;\n  -moz-osx-font-smoothing: grayscale;\n  color: #2c3e50;\n  height: 100%;\n  .ant-layout-has-sider {\n    border: 1px solid #eee;\n  }\n  .ant-layout-sider-children {\n    border-right: 1px solid #eee;\n  }\n  .ant-layout {\n    height: 100%;\n  }\n  .logo {\n    padding: 5px;\n    border-bottom: #eee solid 1px;\n    height: 60px;\n    background-image: url('../../public/logo/logo-lang.svg');\n    background-size: auto 50px;\n    background-repeat: no-repeat;\n    background-position: 5px center;\n  }\n  .ant-layout-footer {\n    padding: 10px;\n    text-align: center;\n    border-top: #d6d4d4 solid 1px;\n  }\n  .ant-menu-inline,\n  .ant-menu-vertical,\n  .ant-menu-vertical-left {\n    border: 0;\n  }\n}\n.search-bar-highlight {\n  background-color: #ef0fff;\n  color: #fdfdfd;\n\n  &.selected-highlight {\n    background-color: #17a450;\n  }\n}\n</style>\n"
  },
  {
    "path": "packages/gui/src/view/api.js",
    "content": "import { ipcRenderer, shell } from 'electron'\nimport lodash from 'lodash'\nimport path from 'node:path'\n\nlet inited = false\nlet apiObj = null\nexport function apiInit (app) {\n  const invoke = (api, args) => {\n    return ipcRenderer.invoke('apiInvoke', [api, args]).catch((e) => {\n      app.$notification.error({\n        message: 'Api invoke error',\n        description: e.message,\n      })\n    })\n  }\n  const send = (channel, message) => {\n    console.log('ipcRenderer.send, channel=', channel, ', message=', message)\n    return ipcRenderer.send(channel, message)\n  }\n\n  apiObj = {\n    ipc: {\n      on (channel, callback) {\n        ipcRenderer.on(channel, callback)\n      },\n      removeAllListeners (channel) {\n        ipcRenderer.removeAllListeners(channel)\n      },\n      invoke,\n      postMessage (channel, ...args) {\n        ipcRenderer.postMessage(channel, ...args)\n      },\n      send,\n      async openExternal (href) {\n        await shell.openExternal(href)\n      },\n      openPath (file) {\n        shell.openPath(path.resolve(file))\n      },\n    },\n  }\n\n  const bindApi = (api, param1) => {\n    lodash.set(apiObj, api, (param2) => {\n      return invoke(api, param2 || param1)\n    })\n  }\n\n  if (!inited) {\n    return invoke('getApiList').then((list) => {\n      inited = true\n      for (const item of list) {\n        bindApi(item)\n      }\n      console.log('api inited:', apiObj)\n      return apiObj\n    })\n  }\n\n  return new Promise((resolve) => {\n    resolve(apiObj)\n  })\n}\nexport function useApi () {\n  return apiObj\n}\n"
  },
  {
    "path": "packages/gui/src/view/components/container.vue",
    "content": "<script>\nexport default {\n  name: 'DsContainer',\n}\n</script>\n\n<template>\n  <div class=\"ds-container\">\n    <div class=\"body-wrapper\">\n      <div v-if=\"$slots.header\" class=\"container-header\">\n        <span><slot name=\"header\" /></span>\n        <span style=\"color:#999\"><slot name=\"header-right\" /></span>\n      </div>\n      <div class=\"container-body\">\n        <slot />\n      </div>\n      <div class=\"container-footer\">\n        <slot name=\"footer\" />\n      </div>\n    </div>\n  </div>\n</template>\n\n<style lang=\"scss\">\n.ds-container {\n  height: 100%;\n  background-color: #fff;\n  display: flex;\n  position: relative;\n\n  .body-wrapper {\n    position: absolute;\n    top: 0;\n    right: 0;\n    bottom: 0;\n    left: 0;\n    display: flex;\n    flex-direction: column;\n    overflow: hidden;\n  }\n\n  .container-header {\n    padding: 15px;\n    border-bottom: 1px solid #eee;\n    background-color: #fff;\n    height: 60px;\n    display: flex;\n    align-items: center;\n    justify-content: space-between;\n  }\n  .container-body {\n    flex: 1;\n    height: 0;\n    overflow: auto;\n    position: relative;\n    padding: 15px;\n  }\n}\n</style>\n"
  },
  {
    "path": "packages/gui/src/view/components/mock-input.vue",
    "content": "<!--\n  组件：模拟输入框（当前为简易版本，只添加了value属性）\n  作用：全文检索（SearchBar）组件，无法检索 `<a-input/>` 的内容，所以使用 `<span contenteditable=\"true\"></span>` 代替。\n-->\n<script>\nexport default {\n  name: 'MockInput',\n  props: {\n    value: {\n      type: String,\n      default: '',\n      required: false,\n    },\n  },\n  methods: {\n    onKeydown (event) {\n      // 不允许输入换行符\n      if (event.key === 'Enter' || event.keyCode === 13) {\n        event.preventDefault()\n      }\n    },\n    onBlur () {\n      if (this.$refs.input.textContent !== this.value) {\n        this.$emit('input', this.$refs.input.textContent)\n      }\n    },\n  },\n}\n</script>\n\n<template>\n  <span ref=\"input\" class=\"ant-input\" contenteditable=\"true\" spellcheck=\"false\" :title=\"value\" @blur=\"onBlur\" @keydown=\"onKeydown\" v-html=\"value\" />\n</template>\n"
  },
  {
    "path": "packages/gui/src/view/components/setup-ca.vue",
    "content": "<script>\nexport default {\n  name: 'SetupCa',\n  components: {\n\n  },\n  props: {\n    title: {\n      type: String,\n      default: '安装根证书',\n    },\n    visible: {\n      type: Boolean,\n    },\n  },\n  data () {\n    return {\n      systemPlatform: '',\n    }\n  },\n  computed: {\n    setupImage () {\n      if (this.systemPlatform === 'mac') {\n        return '/setup-mac.png'\n      } else if (this.systemPlatform === 'linux') {\n        return '/setup-linux.png'\n      } else {\n        return '/setup.png'\n      }\n    },\n  },\n  async created () {\n    this.systemPlatform = await this.$api.info.getSystemPlatform()\n  },\n  methods: {\n    async openExternal (url) {\n      await this.$api.ipc.openExternal(url)\n    },\n    afterVisibleChange (val) {\n    },\n    showDrawer () {\n      this.$emit('update:visible', true)\n    },\n    onClose () {\n      this.$emit('update:visible', false)\n    },\n    async doSetup () {\n      this.$emit('setup')\n      if (this.systemPlatform === 'linux') {\n        this.$message.success('根证书已成功安装到系统证书库（注意：浏览器仍然需要手动安装）')\n      }\n    },\n  },\n}\n</script>\n\n<template>\n  <a-drawer\n    placement=\"right\"\n    :closable=\"false\"\n    :visible=\"visible\"\n    :after-visible-change=\"afterVisibleChange\"\n    width=\"660px\"\n    height=\"100%\"\n    :slots=\"{ title: 'title' }\"\n    wrap-class-name=\"json-wrapper\"\n    @close=\"onClose\"\n  >\n    <template slot=\"title\">\n      {{ title }}\n      <a-button type=\"primary\" style=\"float:right\" @click=\"doSetup()\">\n        点此去安装\n      </a-button>\n      <a-button style=\"float:right;margin-right:10px;\" @click=\"openExternal('https://github.com/docmirror/dev-sidecar/blob/master/doc/caroot.md')\">\n        为什么要安装证书？\n      </a-button>\n    </template>\n    <div>\n      <b>本应用在非“安全模式”下必须安装和信任CA根证书</b>，该证书是应用启动时本地随机生成的<br>\n\n      <template v-if=\"systemPlatform === 'mac'\">\n        1、点击右上角“点此去安装按钮”，打开钥匙串，<b style=\"color:red\">选择”系统“</b><br>\n        2、然后按如下图步骤将随机生成的根证书设置为始终信任<br>\n        3、可能需要重新启动应用和浏览器才能生效<br>\n        4、注意：如果出现无法导入提示时，先点一下钥匙串的左边切换到<b style=\"color:red\">“系统”栏</b>，然后再重新安装证书即可<br>\n      </template>\n      <template v-else-if=\"systemPlatform === 'linux'\">\n        1、点击右上角“点此去安装按钮”,将自动安装到系统证书库中<br>\n        2、<b color=\"red\">火狐、chrome等浏览器不走系统证书</b>，需要手动安装(下图以chrome为例安装根证书)<br>\n      </template>\n      <template v-else>\n        1、点击右上角“点此去安装按钮”，打开证书<br>\n        2、然后按如下图步骤将根证书添加到<b style=\"color:red\">信任的根证书颁发机构</b>\n      </template>\n    </div>\n    <img width=\"100%\" :src=\"setupImage\">\n  </a-drawer>\n</template>\n"
  },
  {
    "path": "packages/gui/src/view/components/tree-node.vue",
    "content": "<script>\nexport default {\n  name: 'TreeNode',\n  props: {\n    treeData: Array,\n  },\n  methods: {\n    async openExternal (url) {\n      await this.$api.ipc.openExternal(url)\n    },\n  },\n}\n</script>\n\n<template>\n  <ul>\n    <li v-for=\"node in treeData\" :key=\"node.title\">\n      <div v-if=\"node.url && (node.url.startsWith('http://') || node.url.startsWith('https://'))\" :class=\"node.rowClass\" :style=\"node.rowStyle\">\n        <a :title=\"node.tip || node.title\" :class=\"node.labelClass\" :style=\"node.labelStyle\" @click=\"openExternal(node.url)\">{{ node.title }}</a>\n      </div>\n      <div v-else :class=\"node.rowClass\" :style=\"node.rowStyle\">\n        <label :title=\"node.tip || node.title\" :class=\"node.labelClass\" :style=\"node.labelStyle\">{{ node.title }}</label>\n      </div>\n      <tree-node v-if=\"node.children && node.children.length > 0\" :tree-data=\"node.children\" class=\"child-node\" />\n    </li>\n  </ul>\n</template>\n"
  },
  {
    "path": "packages/gui/src/view/composables/theme.js",
    "content": "import { ref } from 'vue'\n\nexport const colorTheme = ref('dark')\n"
  },
  {
    "path": "packages/gui/src/view/index.js",
    "content": "import modules from '../bridge/front'\nimport { apiInit, useApi } from './api'\nimport status from './status'\n\nexport default {\n  initApi: apiInit,\n  async initPre (Vue, api) {\n    Vue.prototype.$api = api\n    const setting = await api.setting.load()\n    Vue.prototype.$global = {\n      setting,\n      config: await api.config.get(),\n    }\n    await status.install(api)\n  },\n  initModules (app, router) {\n    const api = useApi()\n    modules.install(app, api, router)\n  },\n}\n"
  },
  {
    "path": "packages/gui/src/view/mixins/plugin.js",
    "content": "import lodash from 'lodash'\nimport DsContainer from '../components/container'\n\nexport default {\n  components: {\n    DsContainer,\n  },\n  data () {\n    return {\n      key: undefined,\n      config: undefined,\n      status: {},\n      labelCol: { span: 5 },\n      wrapperCol: { span: 19 },\n      resetDefaultLoading: false,\n      applyLoading: false,\n      systemPlatform: '',\n    }\n  },\n  created () {\n    this.init()\n  },\n  mounted () {\n  },\n  methods: {\n    getKey () {\n      if (this.key) {\n        return this.key\n      }\n      throw new Error('请设置key')\n    },\n    async init () {\n      this.status = this.$status\n      await this.reloadConfig()\n      this.printConfig('Init, ')\n      this.systemPlatform = await this.$api.info.getSystemPlatform()\n\n      if (this.ready) {\n        return this.ready(this.config)\n      }\n    },\n    async apply () {\n      if (this.applyLoading === true) {\n        return // 防重复提交\n      }\n      this.applyLoading = true\n      try {\n        await this.applyBefore()\n        await this.saveConfig()\n        await this.applyAfter()\n      } finally {\n        this.applyLoading = false\n      }\n    },\n    async applyBefore () {\n\n    },\n    async applyAfter () {\n\n    },\n    resetDefault () {\n      const key = this.getKey()\n      this.$confirm({\n        title: '提示',\n        content: '确定要恢复默认设置吗？',\n        cancelText: '取消',\n        okText: '确定',\n        onOk: async () => {\n          this.resetDefaultLoading = true\n          try {\n            this.config = await this.$api.config.resetDefault(key)\n            if (this.ready) {\n              await this.ready(this.config)\n            }\n            await this.apply()\n          } finally {\n            this.resetDefaultLoading = false\n          }\n        },\n        onCancel () {},\n      })\n    },\n    saveConfig () {\n      return this.$api.config.save(this.config).then((ret) => {\n        this.$message.success('设置已保存')\n        this.setConfig(ret.allConfig)\n        this.printConfig('After saveConfig(), ')\n        return ret\n      })\n    },\n    getConfig (key) {\n      const value = lodash.get(this.config, key)\n      if (value == null) {\n        return {}\n      }\n      return value\n    },\n    setConfig (newConfig) {\n      this.$set(this, 'config', newConfig)\n    },\n    printConfig (prefix = '') {\n      console.log(`${prefix}${this.key} page config:`, this.config, this.systemPlatform)\n    },\n    getStatus (key) {\n      const value = lodash.get(this.status, key)\n      if (value == null) {\n        return {}\n      }\n      return value\n    },\n    async reloadConfig () {\n      const config = await this.$api.config.reload()\n      this.setConfig(config)\n    },\n    async reloadConfigAndRestart () {\n      if (this.$api.plugin.git.isEnabled()) {\n        await this.$api.plugin.git.close()\n      }\n      await this.reloadConfig()\n      this.printConfig('After reloadConfigAndRestart(), ')\n      if (this.status.server.enabled || this.status.proxy.enabled) {\n        await this.$api.proxy.restart()\n        await this.$api.server.restart()\n        if (this.$api.plugin.git.isEnabled()) {\n          await this.$api.plugin.git.start()\n        }\n        this.$message.success('代理服务和系统代理重启成功')\n      } else {\n        this.$message.info('代理服务和系统代理未启动，无需重启')\n      }\n    },\n    isWindows () {\n      return this.systemPlatform === 'windows'\n    },\n    isMac () {\n      return this.systemPlatform === 'mac'\n    },\n    isLinux () {\n      return this.systemPlatform === 'linux'\n    },\n    async openLog () {\n      const dir = await this.$api.info.getLogDir()\n      this.$api.ipc.openPath(dir)\n    },\n    async focusFirst (ref) {\n      if (ref && ref.length != null) {\n        setTimeout(() => {\n          if (ref.length > 0) {\n            try {\n              ref[0].$el.querySelector('.ant-input').focus()\n            } catch (e) {\n              console.error('获取输入框焦点失败：', e)\n            }\n          }\n        }, 100)\n      }\n    },\n    handleHostname (hostname) {\n      if (this.isNotHostname(hostname)) {\n        return ''\n      }\n\n      // 移除所有空白符\n      return hostname.replaceAll(/\\s+/g, '')\n    },\n    isNotHostname (hostname) {\n      // 暂时只判断数字\n      return !hostname || /^[\\d\\s]+$/.test(hostname)\n    },\n  },\n}\n"
  },
  {
    "path": "packages/gui/src/view/pages/help.vue",
    "content": "<script>\nimport Plugin from '../mixins/plugin'\nimport TreeNode from '../components/tree-node'\n\nexport default {\n  name: 'Help',\n  components: {\n    TreeNode,\n  },\n  mixins: [Plugin],\n  data () {\n    return {\n      key: 'help',\n    }\n  },\n  methods: {\n    async openExternal (url) {\n      await this.$api.ipc.openExternal(url)\n    },\n  },\n}\n</script>\n\n<template>\n  <ds-container>\n    <template slot=\"header\">\n      帮助中心\n    </template>\n    <template slot=\"header-right\">\n      <a-button class=\"mr10\" @click=\"openExternal('https://github.com/docmirror/dev-sidecar/issues/new/choose')\">反馈问题</a-button>\n      <a-button class=\"mr10\" icon=\"profile\" @click=\"openLog()\">查看日志</a-button>\n    </template>\n\n    <div v-if=\"config\" class=\"help-list\">\n      <TreeNode :tree-data=\"config.help.dataList\" />\n    </div>\n  </ds-container>\n</template>\n"
  },
  {
    "path": "packages/gui/src/view/pages/index.vue",
    "content": "<script>\nimport lodash from 'lodash'\nimport DsContainer from '../components/container'\nimport SetupCa from '../components/setup-ca'\n\nexport default {\n  name: 'Index',\n  components: {\n    DsContainer,\n    SetupCa,\n  },\n  data () {\n    return {\n      status: undefined,\n      startup: {\n        loading: false,\n        type: () => {\n          return (this.status.server && this.status.server.enabled) ? 'primary' : 'default'\n        },\n        doClick: () => {\n          if (this.status.server.enabled) {\n            this.apiCall(this.startup, this.$api.shutdown)\n          } else {\n            this.apiCall(this.startup, this.$api.startup)\n          }\n        },\n      },\n      info: {},\n      newVersionDownloading: false,\n      setting: {},\n      server: {\n        key: '代理服务',\n        loading: false,\n        doClick: (checked) => {\n          this.onServerClick(checked)\n        },\n      },\n      switchBtns: undefined,\n      config: undefined,\n      setupCa: {\n        visible: false,\n      },\n      update: { checking: false, downloading: false, progress: 0, newVersion: false },\n    }\n  },\n  computed: {\n    _rootCaSetuped () {\n      if (this.setting.rootCa) {\n        return this.setting.rootCa.setuped === true\n      }\n      return false\n    },\n  },\n  async created () {\n    await this.doCheckRootCa()\n    await this.reloadConfig()\n    this.$set(this, 'status', this.$status)\n    this.switchBtns = this.createSwitchBtns()\n    this.$set(this, 'update', this.$global.update)\n    if (!this.update.autoChecked && this.config.app.autoChecked) {\n      this.update.autoChecked = true // 应用启动时，执行一次\n      this.doCheckUpdate(false)\n    }\n    this.$api.info.get().then((ret) => {\n      this.info = ret\n    })\n  },\n  mounted () {\n  },\n  methods: {\n    async modeChange (event) {\n      const mode = this.config.app.mode\n      if (mode === 'safe') {\n        this.config.server.intercept.enabled = false\n        this.config.server.dns.speedTest.enabled = true\n        this.config.plugin.overwall.enabled = false\n      } else if (mode === 'default') {\n        this.config.server.intercept.enabled = true\n        this.config.server.dns.speedTest.enabled = true\n        this.config.plugin.overwall.enabled = false\n      } else if (mode === 'ow') {\n        console.log('event', event)\n        if (!this.setting.overwall) {\n          this.wantOW()\n          return\n        }\n        this.config.server.intercept.enabled = true\n        this.config.server.dns.speedTest.enabled = true\n        this.config.plugin.overwall.enabled = true\n      }\n      this.$api.config.save(this.config).then(() => {\n        this.$message.success('设置已保存')\n      })\n      if (this.status.server.enabled) {\n        return this.$api.server.restart()\n      }\n    },\n    wantOW () {\n      this.$success({\n        title: '彩蛋（增强模式）',\n        content: (\n          <div>\n            我把它藏在了源码里，感兴趣的话可以找一找它（线索提示 // TODO）\n          </div>\n        ),\n      })\n    },\n    async doCheckRootCa () {\n      const setting = await this.$api.setting.load()\n      console.log('setting', setting)\n      this.setting = setting || {}\n      if (this.setting.rootCa && (this.setting.rootCa.setuped || this.setting.rootCa.noTip)) {\n        return\n      }\n      this.$confirm({\n        title: '第一次使用，请先安装CA根证书',\n        content: '本应用正常使用，必须安装和信任CA根证书',\n        cancelText: '下次安装',\n        okText: '去安装',\n        onOk: () => {\n          this.openSetupCa()\n        },\n        onCancel: () => {\n          this.setting.rootCa = this.setting.rootCa || {}\n          // const rootCa = this.setting.rootCa\n          // rootCa.noTip = true\n          // this.$api.setting.save(this.setting)\n        },\n      })\n    },\n    openSetupCa () {\n      this.setupCa.visible = true\n    },\n    getDateTimeStr () {\n      const date = new Date() // 创建一个表示当前日期和时间的 Date 对象\n      const year = date.getFullYear() // 获取年份\n      const month = String(date.getMonth() + 1).padStart(2, '0') // 获取月份（注意月份从 0 开始计数）\n      const day = String(date.getDate()).padStart(2, '0') // 获取天数\n      const hours = String(date.getHours()).padStart(2, '0') // 获取小时\n      const minutes = String(date.getMinutes()).padStart(2, '0') // 获取分钟\n      const seconds = String(date.getSeconds()).padStart(2, '0') // 获取秒数\n      const milliseconds = String(date.getMilliseconds()).padStart(3, '0') // 获取毫秒\n      return `${year}-${month}-${day} ${hours}:${minutes}:${seconds}.${milliseconds}`\n    },\n    async handleCaSetuped () {\n      console.log('this.config.server.setting.rootCaFile.certPath', this.config.server.setting.rootCaFile.certPath)\n      await this.$api.shell.setupCa({ certPath: this.config.server.setting.rootCaFile.certPath })\n      this.setting.rootCa = this.setting.rootCa || {}\n      const rootCa = this.setting.rootCa\n\n      // 根证书已安装\n      rootCa.setuped = true\n      // 保存安装时间\n      rootCa.setupTime = this.getDateTimeStr()\n      // 保存安装描述\n      rootCa.desc = '根证书已安装'\n      // 删除noTip数据\n      // delete rootCa.noTip\n\n      this.$set(this, 'setting', this.setting)\n      this.$api.setting.save(this.setting)\n    },\n    reloadConfig () {\n      return this.$api.config.reload().then((ret) => {\n        this.config = ret\n        return ret\n      })\n    },\n    createSwitchBtns () {\n      console.log('api,', this.$api)\n      const btns = {}\n      const status = this.status\n      btns.server = this.createSwitchBtn('server', '代理服务', this.$api.server, status)\n      btns.proxy = this.createSwitchBtn('proxy', '系统代理', this.$api.proxy, status)\n      lodash.forEach(status.plugin, (item, key) => {\n        if (this.config.plugin[key].statusOff) {\n          return\n        }\n        btns[key] = this.createSwitchBtn(key, this.config.plugin[key].name, this.$api.plugin[key], status.plugin, this.config.plugin[key].tip)\n      })\n      return btns\n    },\n    createSwitchBtn (key, label, apiTarget, statusParent, tip) {\n      return {\n        loading: false,\n        key,\n        label,\n        tip,\n        status: () => {\n          return statusParent[key].enabled\n        },\n        doClick: (checked) => {\n          this.onSwitchClick(this.switchBtns[key], apiTarget.start, apiTarget.close, checked)\n        },\n      }\n    },\n    async apiCall (btn, api, param) {\n      btn.loading = true\n      try {\n        const ret = await api(param)\n        console.log('this status', this.status)\n        return ret\n      } catch (err) {\n        btn.loading = false // 有时候记录日志会卡死，先设置为false\n        console.log('api invoke error:', err)\n      } finally {\n        btn.loading = false\n      }\n    },\n\n    onSwitchClick (btn, openApi, closeApi, checked) {\n      if (checked) {\n        return this.apiCall(btn, openApi)\n      } else {\n        return this.apiCall(btn, closeApi)\n      }\n    },\n    onServerClick (checked) {\n      return this.onSwitchClick(this.server, this.$api.server.start, this.$api.server.close, checked)\n    },\n    start (checked) {\n      this.apiCall(this.startup, this.$api.startup)\n    },\n    openSettings () {\n      this.setting.visible = true\n    },\n    onConfigChanged (newConfig) {\n      console.log('config changed', newConfig)\n      this.reloadConfig().then(() => {\n        if (this.status.server) {\n          return this.$api.server.restart()\n        }\n      })\n    },\n    goDonate () {\n      this.$message.info('感谢支持')\n    },\n    doCheckUpdate (fromUser) {\n      this.$api.update.checkForUpdate(fromUser)\n    },\n    async openExternal (url) {\n      await this.$api.ipc.openExternal(url)\n    },\n    onShutdownTipClose (e) {\n      this.$confirm({\n        title: '是否永久关闭该提示',\n        okText: '我已知晓，不再提示',\n        cancelText: '下次还显示',\n        onOk: () => {\n          this.$api.config.update({ app: { showShutdownTip: false } })\n        },\n      })\n    },\n  },\n}\n</script>\n\n<template>\n  <DsContainer class=\"page_index\">\n    <template slot=\"header\">\n      给开发者的辅助工具\n    </template>\n    <template slot=\"header-right\">\n      <a-button style=\"margin-right:10px\" @click=\"openSetupCa\">\n        <a-badge :count=\"_rootCaSetuped ? 0 : 1\" dot>安装根证书</a-badge>\n      </a-button>\n\n      <a-button\n        style=\"margin-right:10px\" :loading=\"update.downloading || update.checking\" :title=\"`当前版本:${info.version}`\"\n        @click=\"doCheckUpdate(true)\"\n      >\n        <a-badge :count=\"update.newVersion ? 1 : 0\" dot>\n          <span v-if=\"update.downloading\">{{ update.progress }}%</span>{{ update.downloading ? '新版本下载中' : (`检查更新${update.checking ? '中' : ''}`) }}\n        </a-badge>\n      </a-button>\n    </template>\n\n    <div class=\"box\">\n      <a-alert v-if=\"config && config.app.showShutdownTip\" message=\"本应用开启后会修改系统代理，直接重启电脑可能会无法上网，您可以再次启动本应用即可恢复。如您需要卸载，在卸载前请务必完全退出本应用再进行卸载\" banner closable @close=\"onShutdownTipClose\" />\n      <div v-if=\"config && config.app\" class=\"mode-bar\" style=\"margin:20px;\">\n        <a-radio-group v-model=\"config.app.mode\" button-style=\"solid\" @change=\"modeChange\">\n          <a-tooltip placement=\"topLeft\" title=\"启用测速，关闭拦截，关闭增强（不稳定，不需要安装证书，最安全）\">\n            <a-radio-button value=\"safe\">\n              安全模式\n            </a-radio-button>\n          </a-tooltip>\n          <a-tooltip placement=\"topLeft\" title=\"启用测速，启用拦截，关闭增强（需要安装证书）\">\n            <a-radio-button value=\"default\">\n              默认模式\n            </a-radio-button>\n          </a-tooltip>\n          <a-tooltip v-if=\"setting.overwall\" placement=\"topLeft\" title=\"一个简单的梯子（敏感原因，默认隐藏，更多信息请点击左侧增强功能菜单）\">\n            <a-radio-button value=\"ow\">\n              增强模式\n            </a-radio-button>\n          </a-tooltip>\n          <a-tooltip v-else placement=\"topLeft\" title=\"这个页面有个彩蛋\">\n            <a-radio-button :disabled=\"true\" value=\"ow\">\n              彩蛋\n            </a-radio-button>\n          </a-tooltip>\n        </a-radio-group>\n      </div>\n\n      <div\n        v-if=\"status\"\n        style=\"margin-top:20px;display: flex; align-items:center;justify-content:space-around;flex-direction: row\"\n      >\n        <div style=\"text-align: center\">\n          <div class=\"big_button\">\n            <a-button shape=\"circle\" :type=\"startup.type()\" :loading=\"startup.loading\" @click=\"startup.doClick\">\n              <img v-if=\"!startup.loading && !status.server.enabled\" width=\"50\" src=\"/logo/logo-simple.svg\">\n              <img v-if=\"!startup.loading && status.server.enabled\" width=\"50\" src=\"/logo/logo-fff.svg\">\n            </a-button>\n            <div class=\"mt10\">\n              {{ status.server.enabled ? '已开启' : '已关闭' }}\n            </div>\n          </div>\n        </div>\n        <div :span=\"12\">\n          <a-form style=\"margin-top:20px\" :label-col=\"{ span: 15 }\" :wrapper-col=\"{ span: 9 }\">\n            <a-form-item v-for=\"(item, key) in switchBtns\" :key=\"key\" :label=\"item.label\">\n              <a-tooltip placement=\"topLeft\">\n                <a-switch\n                  style=\"margin-left:10px\" :loading=\"item.loading\" :checked=\"item.status()\" default-checked\n                  @change=\"item.doClick\"\n                >\n                  <a-icon slot=\"checkedChildren\" type=\"check\" />\n                  <a-icon slot=\"unCheckedChildren\" type=\"close\" />\n                </a-switch>\n              </a-tooltip>\n            </a-form-item>\n          </a-form>\n        </div>\n      </div>\n    </div>\n\n    <SetupCa title=\"安装证书\" :visible.sync=\"setupCa.visible\" @setup=\"handleCaSetuped\" />\n    <div slot=\"footer\">\n      <div v-if=\"!setting.overwall\" class=\"star\">\n        <div class=\"donate\">\n          <a-tooltip placement=\"topLeft\" title=\"彩蛋，点我\">\n            <span style=\"display: block;width:100px;height:50px;\" @click=\"wantOW()\" />\n          </a-tooltip>\n        </div>\n        <div class=\"right\" />\n      </div>\n      <div v-if=\"setting.development == null || !setting.development\" class=\"star\">\n        <div class=\"donate\" />\n        <div class=\"right\">\n          <div>\n            如果它解决了你的问题，请不要吝啬你的star哟！点这里\n            <a-icon style=\"margin-right:10px;\" type=\"arrow-right\" theme=\"outlined\" />\n          </div>\n          <a @click=\"openExternal('https://github.com/docmirror/dev-sidecar')\"><img\n            alt=\"GitHub stars\"\n            src=\"https://img.shields.io/github/stars/docmirror/dev-sidecar?logo=github\"\n          ></a>\n        </div>\n      </div>\n    </div>\n  </DsContainer>\n</template>\n\n<style lang=\"scss\">\n.page_index {\n  .mode-bar {\n    margin: 30px;\n    text-align: center;\n  }\n\n  .star {\n    display: flex;\n    flex-direction: row;\n    align-items: center;\n    justify-content: space-between;\n    padding: 10px;\n    .donate {\n      cursor: pointer;\n    }\n\n    .right {\n      display: flex;\n      flex-direction: row;\n      justify-content: flex-end;\n    }\n\n    > * {\n      margin-right: 10px;\n    }\n\n    a {\n      height: 21px;\n\n      img {\n        height: 21px;\n      }\n    }\n  }\n}\n\n.payQrcode {\n  padding: 10px;\n  display: flex;\n  justify-content: space-evenly;\n}\n\n.big_button > button {\n  width: 100px;\n  height: 100px;\n  border-radius: 100px;\n}\n\n.big_button > button i {\n  size: 40px;\n}\n\ndiv.ant-form-item {\n  margin-bottom: 9px;\n}\n</style>\n"
  },
  {
    "path": "packages/gui/src/view/pages/plugin/git.vue",
    "content": "<script>\nimport Plugin from '../../mixins/plugin'\nimport MockInput from '@/view/components/mock-input.vue'\n\nexport default {\n  name: 'Git',\n  components: { MockInput },\n  mixins: [Plugin],\n  data () {\n    return {\n      key: 'plugin.git',\n      labelCol: { span: 4 },\n      wrapperCol: { span: 20 },\n      noProxyUrls: [],\n      needRestart: false,\n    }\n  },\n  created () {\n    console.log('status:', this.status)\n  },\n  mounted () {\n  },\n  methods: {\n    ready () {\n      this.initNoProxyUrls()\n    },\n    async applyBefore () {\n      if (this.status.plugin.git.enabled) {\n        await this.$api.plugin.git.close()\n        this.needRestart = true\n      } else {\n        this.needRestart = false\n      }\n      this.submitNoProxyUrls()\n    },\n    async applyAfter () {\n      if (this.needRestart) {\n        await this.$api.plugin.git.start()\n      }\n    },\n    initNoProxyUrls () {\n      this.noProxyUrls = []\n      for (const key in this.config.plugin.git.setting.noProxyUrls) {\n        const value = this.config.plugin.git.setting.noProxyUrls[key]\n        this.noProxyUrls.push({\n          key,\n          value,\n        })\n      }\n    },\n    addNoProxyUrl () {\n      this.noProxyUrls.unshift({ key: '' })\n      this.focusFirst(this.$refs.noProxyUrls)\n    },\n    delNoProxyUrl (item, index) {\n      this.noProxyUrls.splice(index, 1)\n    },\n    submitNoProxyUrls () {\n      const noProxyUrls = {}\n      for (const item of this.noProxyUrls) {\n        if (item.key) {\n          const hostname = this.handleHostname(item.key)\n          if (hostname) {\n            noProxyUrls[hostname] = true\n          }\n        }\n      }\n      this.config.plugin.git.setting.noProxyUrls = noProxyUrls\n    },\n  },\n}\n</script>\n\n<template>\n  <ds-container>\n    <template slot=\"header\">\n      Git.exe代理设置\n    </template>\n    <template slot=\"header-right\">\n      仅针对git命令行的代理设置，github网站的访问无需设置\n    </template>\n\n    <div v-if=\"config\">\n      <a-form layout=\"horizontal\">\n        <a-form-item label=\"启用Git代理\" :label-col=\"labelCol\" :wrapper-col=\"wrapperCol\">\n          <a-checkbox v-model=\"config.plugin.git.enabled\">\n            随应用启动\n          </a-checkbox>\n          <a-tag v-if=\"status.plugin.git.enabled\" color=\"green\">\n            当前已启动\n          </a-tag>\n          <a-tag v-else color=\"red\">\n            当前未启动\n          </a-tag>\n        </a-form-item>\n        <a-form-item label=\"SSL校验\" :label-col=\"labelCol\" :wrapper-col=\"wrapperCol\">\n          <a-checkbox v-model=\"config.plugin.git.setting.sslVerify\">\n            关闭sslVerify\n          </a-checkbox>\n          安装Git时未选择使用系统证书管理服务时必须关闭\n        </a-form-item>\n        <a-form-item label=\"排除仓库地址\" :label-col=\"labelCol\" :wrapper-col=\"wrapperCol\">\n          <div>\n            <a-row :gutter=\"10\">\n              <a-col :span=\"22\">\n                <span><code>Git.exe</code>将不代理以下仓库；可以是根地址、组织/机构地址、完整地址</span>\n              </a-col>\n              <a-col :span=\"2\">\n                <a-button type=\"primary\" icon=\"plus\" @click=\"addNoProxyUrl()\" />\n              </a-col>\n            </a-row>\n            <a-row v-for=\"(item, index) of noProxyUrls\" ref=\"noProxyUrls\" :key=\"index\" :gutter=\"10\">\n              <a-col :span=\"22\">\n                <MockInput v-model=\"item.key\" class=\"mt-2\" />\n              </a-col>\n              <a-col :span=\"2\">\n                <a-button type=\"danger\" icon=\"minus\" @click=\"delNoProxyUrl(item, index)\" />\n              </a-col>\n            </a-row>\n          </div>\n        </a-form-item>\n      </a-form>\n    </div>\n    <template slot=\"footer\">\n      <div class=\"footer-bar\">\n        <a-button :loading=\"resetDefaultLoading\" class=\"mr10\" icon=\"sync\" @click=\"resetDefault()\">\n          恢复默认\n        </a-button>\n        <a-button :loading=\"applyLoading\" icon=\"check\" type=\"primary\" @click=\"apply()\">\n          应用\n        </a-button>\n      </div>\n    </template>\n  </ds-container>\n</template>\n"
  },
  {
    "path": "packages/gui/src/view/pages/plugin/node.vue",
    "content": "<script>\nimport Plugin from '../../mixins/plugin'\n\nexport default {\n  name: 'Node',\n  mixins: [Plugin],\n  data () {\n    return {\n      key: 'plugin.node',\n      labelCol: { span: 4 },\n      wrapperCol: { span: 20 },\n      npmVariables: undefined,\n      registry: false,\n    }\n  },\n  created () {\n    console.log('status:', this.status)\n  },\n  mounted () {\n  },\n  methods: {\n    ready () {\n      return this.$api.plugin.node.getVariables().then((ret) => {\n        console.log('variables', ret)\n        this.npmVariables = ret\n      })\n    },\n    async onSwitchRegistry (event) {\n      await this.setRegistry({ registry: event.target.value, type: 'npm' })\n      this.$message.success('切换成功')\n    },\n    async onSwitchYarnRegistry (event) {\n      const registry = event.target.value\n      console.log('registry', registry)\n      await this.setRegistry({ registry, type: 'yarn' })\n      this.$message.success('切换成功')\n    },\n    async setRegistry ({ registry, type }) {\n      this.apply()\n      console.log('type', type)\n      await this.$api.plugin.node.setRegistry({ registry, type })\n    },\n    setNpmVariableAll () {\n      this.saveConfig().then(() => {\n        this.$api.plugin.node.setVariables()\n      })\n    },\n  },\n}\n</script>\n\n<template>\n  <ds-container>\n    <template slot=\"header\">\n      NPM加速\n    </template>\n    <template slot=\"header-right\">\n      由于nodejs不走系统证书，所以npm加速不是很好用，可以用淘宝registry\n    </template>\n\n    <div v-if=\"config\">\n      <a-form layout=\"horizontal\">\n        <a-form-item label=\"启用NPM代理\" :label-col=\"labelCol\" :wrapper-col=\"wrapperCol\">\n          <a-checkbox v-model=\"config.plugin.node.enabled\">\n            随应用启动\n          </a-checkbox>\n          <a-tag v-if=\"status.plugin.node.enabled\" color=\"green\">\n            当前已启动\n          </a-tag>\n          <a-tag v-else color=\"red\">\n            当前未启动\n          </a-tag>\n        </a-form-item>\n        <a-form-item label=\"npm命令名\" :label-col=\"labelCol\" :wrapper-col=\"wrapperCol\">\n          <a-input v-model=\"config.plugin.node.setting.command\" spellcheck=\"false\" />\n          <div class=\"form-help\">\n            如果你的npm命令改成了其他名字，或者想设置绿色版npm程序路径，可在此处修改\n          </div>\n        </a-form-item>\n        <a-form-item label=\"SSL校验\" :label-col=\"labelCol\" :wrapper-col=\"wrapperCol\">\n          <a-checkbox v-model=\"config.plugin.node.setting['strict-ssl']\">\n            关闭strict-ssl\n          </a-checkbox>\n          npm代理启用后必须关闭\n        </a-form-item>\n        <a-form-item label=\"npm仓库镜像\" :label-col=\"labelCol\" :wrapper-col=\"wrapperCol\">\n          <a-radio-group\n            v-model=\"config.plugin.node.setting.registry\" default-value=\"https://registry.npmjs.org\"\n            button-style=\"solid\" @change=\"onSwitchRegistry\"\n          >\n            <a-radio-button value=\"https://registry.npmjs.org\" title=\"https://registry.npmjs.org\">\n              npmjs原生\n            </a-radio-button>\n            <a-radio-button value=\"https://registry.npmmirror.com\" title=\"https://registry.npmmirror.com\">\n              taobao镜像\n            </a-radio-button>\n          </a-radio-group>\n          <div class=\"form-help\">\n            设置后立即生效，即使关闭 ds 也会继续保持\n          </div>\n        </a-form-item>\n        <a-form-item label=\"yarn仓库镜像\" :label-col=\"labelCol\" :wrapper-col=\"wrapperCol\">\n          <a-radio-group v-model=\"config.plugin.node.setting.yarnRegistry\" default-value=\"null\" button-style=\"solid\" @change=\"onSwitchYarnRegistry\">\n            <a-radio-button value=\"default\" title=\"https://registry.yarnpkg.com\">\n              yarn原生\n            </a-radio-button>\n            <a-radio-button value=\"https://registry.npmmirror.com\" title=\"https://registry.npmmirror.com\">\n              taobao镜像\n            </a-radio-button>\n          </a-radio-group>\n          <div class=\"form-help\">\n            设置后立即生效，即使关闭 ds 也会继续保持\n          </div>\n        </a-form-item>\n        <a-form-item label=\"镜像变量设置\" :label-col=\"labelCol\" :wrapper-col=\"wrapperCol\">\n          <a-checkbox v-model=\"config.plugin.node.startup.variables\">\n            自动设置，启动npm加速开关时将会设置如下环境变量\n          </a-checkbox>\n          <div class=\"form-help\">\n            某些库需要自己设置镜像变量，才能下载，比如：<code>electron</code>\n          </div>\n          <a-row v-for=\"(item, index) of npmVariables\" :key=\"index\" :gutter=\"10\" style=\"margin-top: 2px\">\n            <a-col :span=\"10\">\n              <a-input v-model=\"item.key\" :title=\"item.key\" read-only spellcheck=\"false\" />\n            </a-col>\n            <a-col :span=\"13\">\n              <a-input v-model=\"item.value\" :title=\"item.value\" read-only spellcheck=\"false\" />\n            </a-col>\n            <a-col :span=\"1\">\n              <a-icon v-if=\"item.exists && item.hadSet\" title=\"已设置\" style=\"color:green\" type=\"check\" />\n              <a-icon v-else title=\"还未设置\" style=\"color:red\" type=\"exclamation-circle\" />\n            </a-col>\n          </a-row>\n        </a-form-item>\n      </a-form>\n    </div>\n    <template slot=\"footer\">\n      <div class=\"footer-bar\">\n        <a-button :loading=\"resetDefaultLoading\" class=\"mr10\" icon=\"sync\" @click=\"resetDefault()\">\n          恢复默认\n        </a-button>\n        <a-button :loading=\"applyLoading\" icon=\"check\" type=\"primary\" @click=\"apply()\">\n          应用\n        </a-button>\n      </div>\n    </template>\n  </ds-container>\n</template>\n"
  },
  {
    "path": "packages/gui/src/view/pages/plugin/overwall.vue",
    "content": "<script>\nimport Plugin from '../../mixins/plugin'\nimport MockInput from '@/view/components/mock-input.vue'\n\nexport default {\n  name: 'Overwall',\n  components: { MockInput },\n  mixins: [Plugin],\n  data () {\n    return {\n      key: 'plugin.overwall',\n      labelCol: { span: 4 },\n      wrapperCol: { span: 20 },\n      targets: undefined,\n      servers: undefined,\n      overwallOptions: [\n        {\n          label: '启用',\n          value: 'true',\n        },\n        {\n          label: '禁用',\n          value: 'false',\n        },\n      ],\n    }\n  },\n  created () {\n    console.log('status:', this.status)\n  },\n  mounted () {\n  },\n  methods: {\n    async openExternal (url) {\n      await this.$api.ipc.openExternal(url)\n    },\n    async applyAfter () {\n      if (this.status.server.enabled) {\n        return this.$api.server.restart()\n      }\n    },\n    ready () {\n      this.initTarget()\n      this.initServer()\n    },\n    async applyBefore () {\n      this.submitTarget()\n      this.submitServer()\n    },\n    initTarget () {\n      this.targets = []\n      const targetsMap = this.config.plugin.overwall.targets\n      for (const key in targetsMap) {\n        const value = targetsMap[key]\n        this.targets.push({\n          key: key || '',\n          value: value === true ? 'true' : 'false',\n        })\n      }\n    },\n    addTarget () {\n      this.targets.unshift({ key: '', value: 'true' })\n      this.focusFirst(this.$refs.targets)\n    },\n    deleteTarget (item, index) {\n      this.targets.splice(index, 1)\n    },\n    submitTarget () {\n      const map = {}\n      for (const item of this.targets) {\n        if (item.key) {\n          const hostname = this.handleHostname(item.key)\n          if (hostname) {\n            map[hostname] = (item.value === 'true')\n          }\n        }\n      }\n      this.config.plugin.overwall.targets = map\n    },\n\n    initServer () {\n      this.servers = []\n      const targetsMap = this.config.plugin.overwall.server\n      for (const key in targetsMap) {\n        const value = targetsMap[key]\n        this.servers.push({\n          key,\n          value,\n        })\n      }\n      if (this.servers.length === 0) {\n        this.addServer(false)\n      }\n    },\n    deleteServer (item, index) {\n      this.servers.splice(index, 1)\n    },\n    addServer (needFocus = true) {\n      this.servers.unshift({ key: '', value: { type: 'path' } })\n      if (needFocus) {\n        this.focusFirst(this.$refs.servers)\n      }\n    },\n    submitServer () {\n      const map = {}\n      for (const item of this.servers) {\n        if (item.key) {\n          const hostname = this.handleHostname(item.key)\n          if (hostname) {\n            map[hostname] = item.value\n          }\n        }\n      }\n      this.config.plugin.overwall.server = map\n    },\n  },\n}\n</script>\n\n<template>\n  <ds-container>\n    <template slot=\"header\">\n      梯子\n    </template>\n    <template slot=\"header-right\">\n      <a-button type=\"primary\" @click=\"openExternal('https://github.com/docmirror/dev-sidecar-doc/blob/main/ow.md')\">原理说明</a-button>\n    </template>\n\n    <div v-if=\"config\">\n      <a-form layout=\"horizontal\">\n        <a-form-item label=\"梯子\" :label-col=\"labelCol\" :wrapper-col=\"wrapperCol\">\n          <a-checkbox v-model=\"config.plugin.overwall.enabled\">\n            启用\n          </a-checkbox>\n          <div class=\"form-help\">\n            这是什么功能？你懂的！偷偷的用，别声张。<code><i>注：请不要看视频，流量挺小的！</i></code><br>\n            建议参照右上角的<code>原理说明</code>，自建二层代理服务端，并在此页下方配置<code>代理服务端</code>。<br>\n            声明：此功能仅供技术学习与探讨！\n          </div>\n        </a-form-item>\n        <hr>\n        <a-form-item label=\"PAC\" :label-col=\"labelCol\" :wrapper-col=\"wrapperCol\">\n          <a-checkbox v-model=\"config.plugin.overwall.pac.enabled\">\n            启用PAC\n          </a-checkbox>\n          <div class=\"form-help\">\n            PAC内收录了常见的被封杀的域名<br>当里面某些域名你不想被拦截时，你可以配置这些域名为<code>禁用</code>，也可以关闭PAC\n          </div>\n        </a-form-item>\n        <a-form-item label=\"自动更新PAC\" :label-col=\"labelCol\" :wrapper-col=\"wrapperCol\">\n          <a-checkbox v-model=\"config.plugin.overwall.pac.autoUpdate\">\n            是否自动更新PAC\n          </a-checkbox>\n          <div class=\"form-help\">\n            开启自动更新后，启动代理服务时，将会异步从下面的远程地址下载PAC文件到本地。<br>\n            注：只要下载成功后，即使关闭自动更新功能，也会优先读取最近下载的文件！\n          </div>\n        </a-form-item>\n        <a-form-item label=\"远程PAC文件\" :label-col=\"labelCol\" :wrapper-col=\"wrapperCol\">\n          <a-input v-model=\"config.plugin.overwall.pac.pacFileUpdateUrl\" :title=\"config.plugin.overwall.pac.pacFileUpdateUrl\" spellcheck=\"false\" />\n          <div class=\"form-help\">\n            远程PAC文件内容可以是<code>base64</code>编码格式，也可以是未经过编码的\n          </div>\n        </a-form-item>\n        <hr>\n        <a-form-item label=\"自定义域名\" :label-col=\"labelCol\" :wrapper-col=\"wrapperCol\" class=\"fine-tuning2\">\n          <div>\n            <a-row :gutter=\"10\" style=\"\">\n              <a-col :span=\"22\">\n                <span>PAC没有拦截到的域名，可以在此处定义；配置为<code>禁用</code>时，将不使用梯子</span>\n              </a-col>\n              <a-col :span=\"2\">\n                <a-button type=\"primary\" icon=\"plus\" @click=\"addTarget()\" />\n              </a-col>\n            </a-row>\n            <a-row v-for=\"(item, index) of targets\" ref=\"targets\" :key=\"index\" :gutter=\"10\">\n              <a-col :span=\"18\">\n                <MockInput v-model=\"item.key\" class=\"mt-2\" />\n              </a-col>\n              <a-col :span=\"4\">\n                <a-select v-model=\"item.value\" style=\"width:100%\">\n                  <a-select-option v-for=\"(item2) of overwallOptions\" :key=\"item2.value\" :value=\"item2.value\">\n                    {{ item2.label }}\n                  </a-select-option>\n                </a-select>\n              </a-col>\n              <a-col :span=\"2\">\n                <a-button type=\"danger\" icon=\"minus\" @click=\"deleteTarget(item, index)\" />\n              </a-col>\n            </a-row>\n          </div>\n        </a-form-item>\n        <a-form-item label=\"代理服务端\" :label-col=\"labelCol\" :wrapper-col=\"wrapperCol\">\n          <div>\n            <a-row :gutter=\"10\" style=\"\">\n              <a-col :span=\"22\">\n                <span>Nginx二层代理服务端配置</span>\n              </a-col>\n              <a-col :span=\"2\">\n                <a-button type=\"primary\" icon=\"plus\" @click=\"addServer()\" />\n              </a-col>\n            </a-row>\n            <a-row v-for=\"(item, index) of servers\" ref=\"servers\" :key=\"index\" :gutter=\"10\">\n              <a-col :span=\"6\">\n                <a-input v-model=\"item.key\" :title=\"item.key\" addon-before=\"域名\" placeholder=\"yourdomain.com\" spellcheck=\"false\" />\n              </a-col>\n              <a-col :span=\"5\">\n                <a-input v-model=\"item.value.port\" :title=\"item.value.port\" addon-before=\"端口\" placeholder=\"443\" spellcheck=\"false\" />\n              </a-col>\n              <a-col :span=\"6\">\n                <a-input v-model=\"item.value.path\" :title=\"item.value.path\" addon-before=\"路径\" placeholder=\"xxxxxx\" spellcheck=\"false\" />\n              </a-col>\n              <a-col :span=\"5\">\n                <a-input v-model=\"item.value.password\" addon-before=\"密码\" type=\"password\" placeholder=\"password\" spellcheck=\"false\" />\n              </a-col>\n              <a-col :span=\"2\">\n                <a-button type=\"danger\" icon=\"minus\" @click=\"deleteServer(item, index)\" />\n              </a-col>\n            </a-row>\n            <div class=\"form-help\">\n              您可以在此处配置自己的代理服务器地址。<br>\n              警告：请勿使用来源不明的服务器地址，有安全风险！\n            </div>\n          </div>\n        </a-form-item>\n      </a-form>\n    </div>\n    <template slot=\"footer\">\n      <div class=\"footer-bar\">\n        <a-button :loading=\"resetDefaultLoading\" class=\"mr10\" icon=\"sync\" @click=\"resetDefault()\">\n          恢复默认\n        </a-button>\n        <a-button :loading=\"applyLoading\" icon=\"check\" type=\"primary\" @click=\"apply()\">\n          应用\n        </a-button>\n      </div>\n    </template>\n  </ds-container>\n</template>\n\n<style lang=\"scss\">\n/*样式微调*/\n.fine-tuning2 .ant-btn-danger {\n  margin-top: 2px !important;\n}\n.ant-input-group-addon:first-child {\n  padding: 0 5px !important;\n}\n</style>\n"
  },
  {
    "path": "packages/gui/src/view/pages/plugin/pip.vue",
    "content": "<script>\nimport Plugin from '../../mixins/plugin'\n\nexport default {\n  name: 'Pip',\n  mixins: [Plugin],\n  data () {\n    return {\n      key: 'plugin.pip',\n      labelCol: { span: 4 },\n      wrapperCol: { span: 20 },\n      npmVariables: undefined,\n      registry: false,\n      trustedHostList: [],\n    }\n  },\n  created () {\n    console.log('status:', this.status)\n  },\n  mounted () {\n  },\n  methods: {\n    ready () {\n    },\n    async applyBefore () {\n      this.config.plugin.pip.setting.trustedHost = this.config.plugin.pip.setting.trustedHost.replaceAll(/[,，。+\\s]+/g, ' ').trim()\n    },\n    async applyAfter () {\n      await this.$api.plugin.pip.start()\n      await this.$api.proxy.restart()\n    },\n    async onSwitchRegistry (event) {\n      await this.setRegistry({ registry: event.target.value })\n      this.$message.success('切换成功')\n    },\n    async setRegistry ({ registry }) {\n      this.config.plugin.pip.setting.registry = registry\n      const domain = registry.substring(registry.indexOf('//') + 2, registry.indexOf('/', 8))\n      this.config.plugin.pip.setting.trustedHost = domain\n      await this.apply()\n    },\n  },\n}\n</script>\n\n<template>\n  <ds-container>\n    <template slot=\"header\">\n      PIP加速\n    </template>\n\n    <div v-if=\"config\">\n      <a-form layout=\"horizontal\">\n        <!--        <a-form-item label=\"启用PIP加速\" :label-col=\"labelCol\" :wrapper-col=\"wrapperCol\"> -->\n        <!--          <a-checkbox v-model=\"config.plugin.pip.enabled\"> -->\n        <!--            随应用启动 -->\n        <!--          </a-checkbox> -->\n        <!--          <a-tag v-if=\"status.plugin.pip.enabled\" color=\"green\"> -->\n        <!--            当前已启动 -->\n        <!--          </a-tag> -->\n        <!--          <a-tag v-else color=\"red\"> -->\n        <!--            当前未启动 -->\n        <!--          </a-tag> -->\n        <!--        </a-form-item> -->\n        <a-form-item label=\"pip命令名\" :label-col=\"labelCol\" :wrapper-col=\"wrapperCol\">\n          <a-input v-model=\"config.plugin.pip.setting.command\" spellcheck=\"false\" />\n          <div class=\"form-help\">\n            如果你的<code>pip</code>命令改成了其他名字（如<code>pip3</code>），或想设置绿色版<code>pip</code>程序路径，可在此处修改\n          </div>\n        </a-form-item>\n        <a-form-item label=\"仓库镜像\" :label-col=\"labelCol\" :wrapper-col=\"wrapperCol\">\n          <a-radio-group\n            v-model=\"config.plugin.pip.setting.registry\" default-value=\"https://pypi.org/simple/\"\n            button-style=\"solid\" @change=\"onSwitchRegistry\"\n          >\n            <a-radio-button value=\"https://pypi.org/simple/\" title=\"https://pypi.org/simple/\">\n              原生\n            </a-radio-button>\n            <a-radio-button value=\"https://mirrors.aliyun.com/pypi/simple/\" title=\"https://mirrors.aliyun.com/pypi/simple/\">\n              aliyun镜像\n            </a-radio-button>\n            <a-radio-button value=\"https://mirrors.bfsu.edu.cn/pypi/web/simple/\" title=\"https://mirrors.bfsu.edu.cn/pypi/web/simple/\">\n              北京外国语大学镜像\n            </a-radio-button>\n            <a-radio-button value=\"https://mirror.nju.edu.cn/pypi/web/simple/\" title=\"https://mirror.nju.edu.cn/pypi/web/simple/\">\n              南京大学镜像\n            </a-radio-button>\n            <a-radio-button value=\"https://pypi.tuna.tsinghua.edu.cn/simple/\" title=\"https://pypi.tuna.tsinghua.edu.cn/simple/\">\n              清华大学镜像\n            </a-radio-button>\n            <a-radio-button value=\"https://mirror.baidu.com/pypi/simple/\" title=\"https://mirror.baidu.com/pypi/simple/\">\n              百度镜像\n            </a-radio-button>\n            <a-radio-button value=\"https://pypi.mirrors.ustc.edu.cn/simple/\" title=\"https://pypi.mirrors.ustc.edu.cn/simple/\">\n              中科大镜像\n            </a-radio-button>\n            <a-radio-button value=\"http://pypi.douban.com/simple/\" title=\"http://pypi.douban.com/simple/\">\n              豆瓣镜像\n            </a-radio-button>\n            <a-radio-button value=\"http://mirrors.sohu.com/Python/\" title=\"http://mirrors.sohu.com/Python/\">\n              搜狐镜像\n            </a-radio-button>\n            <a-radio-button value=\"https://pypi.hustunique.com/\" title=\"https://pypi.hustunique.com/\">\n              华中科大镜像\n            </a-radio-button>\n            <a-radio-button value=\"http://pypi.sdutlinux.org/\" title=\"http://pypi.sdutlinux.org/\">\n              山东理工大学镜像\n            </a-radio-button>\n          </a-radio-group>\n          <div class=\"form-help\">\n            设置后立即生效，即使关闭 ds 也会继续保持\n          </div>\n        </a-form-item>\n        <a-form-item label=\"信任仓库域名\" :label-col=\"labelCol\" :wrapper-col=\"wrapperCol\">\n          <a-input v-model=\"config.plugin.pip.setting.trustedHost\" spellcheck=\"false\" />\n          <div class=\"form-help\">\n            使用以上域名安装包时，不会进行SSL证书验证，多个域名用空格隔开<br>\n            注意：切换仓库镜像同时会修改<code>pip.ini</code>中的<code>trusted-host</code>配置，即使关闭 ds 也会继续保持\n          </div>\n        </a-form-item>\n      </a-form>\n    </div>\n    <template slot=\"footer\">\n      <div class=\"footer-bar\">\n        <a-button :loading=\"resetDefaultLoading\" class=\"mr10\" icon=\"sync\" @click=\"resetDefault()\">\n          恢复默认\n        </a-button>\n        <a-button :loading=\"applyLoading\" icon=\"check\" type=\"primary\" @click=\"apply()\">\n          应用\n        </a-button>\n      </div>\n    </template>\n  </ds-container>\n</template>\n"
  },
  {
    "path": "packages/gui/src/view/pages/proxy.vue",
    "content": "<script>\nimport Plugin from '../mixins/plugin'\nimport MockInput from '@/view/components/mock-input.vue'\n\nexport default {\n  name: 'Proxy',\n  components: { MockInput },\n  mixins: [Plugin],\n  data () {\n    return {\n      key: 'proxy',\n      loopbackVisible: false,\n      excludeIpList: [],\n      excludeIpOptions: [\n        {\n          label: '排除',\n          value: 'true',\n        },\n        {\n          label: '不排除',\n          value: 'false',\n        },\n      ],\n    }\n  },\n  async created () {\n  },\n  mounted () {\n  },\n  methods: {\n    async openExternal (url) {\n      await this.$api.ipc.openExternal(url)\n    },\n    ready () {\n      this.initExcludeIpList()\n    },\n    async applyBefore () {\n      this.submitExcludeIpList()\n    },\n    async applyAfter () {\n      await this.$api.proxy.restart()\n    },\n    async openEnableLoopback () {\n      try {\n        await this.$api.proxy.setEnableLoopback()\n      } catch (e) {\n        this.$message.error(`打开失败：${e.message}`)\n      }\n    },\n    getProxyConfig () {\n      return this.config.proxy\n    },\n    initExcludeIpList () {\n      this.excludeIpList = []\n      for (const key in this.config.proxy.excludeIpList) {\n        const value = this.config.proxy.excludeIpList[key]\n        this.excludeIpList.push({\n          key: key || '',\n          value: value === true ? 'true' : 'false',\n        })\n      }\n    },\n    addExcludeIp () {\n      this.excludeIpList.unshift({ key: '', value: 'true' })\n      this.focusFirst(this.$refs.excludeIpList)\n    },\n    delExcludeIp (item, index) {\n      this.excludeIpList.splice(index, 1)\n    },\n    submitExcludeIpList () {\n      const excludeIpList = {}\n      for (const item of this.excludeIpList) {\n        if (item.key) {\n          const hostname = this.handleHostname(item.key)\n          if (hostname) {\n            excludeIpList[hostname] = (item.value === 'true')\n          }\n        }\n      }\n      this.config.proxy.excludeIpList = excludeIpList\n    },\n  },\n}\n</script>\n\n<template>\n  <ds-container>\n    <template slot=\"header\">\n      系统代理设置\n    </template>\n\n    <div v-if=\"config\">\n      <a-form-item label=\"启用系统代理\" :label-col=\"labelCol\" :wrapper-col=\"wrapperCol\">\n        <a-checkbox v-model=\"config.proxy.enabled\">\n          随应用启动\n        </a-checkbox>\n        <a-tag v-if=\"status.proxy.enabled\" color=\"green\">\n          当前已启动\n        </a-tag>\n        <a-tag v-else color=\"red\">\n          当前未启动\n        </a-tag>\n        <div class=\"form-help\">\n          <a @click=\"openExternal('https://github.com/docmirror/dev-sidecar/blob/master/doc/recover.md')\">卸载与恢复网络说明</a>\n        </div>\n      </a-form-item>\n      <a-form-item label=\"代理HTTP请求\" :label-col=\"labelCol\" :wrapper-col=\"wrapperCol\">\n        <a-checkbox v-model=\"config.proxy.proxyHttp\">\n          是否代理HTTP请求\n        </a-checkbox>\n        <div class=\"form-help\">\n          勾选时，同时代理<code>HTTP</code>和<code>HTTPS</code>请求；不勾选时，只代理<code>HTTPS</code>请求<br>\n          提示：仅为了加速访问<code>Github网站</code>的用户，建议不勾选。\n        </div>\n      </a-form-item>\n\n      <!-- 以下两个功能仅windows支持，mac和linux暂不支持 -->\n      <a-form-item v-if=\"isWindows()\" label=\"设置环境变量\" :label-col=\"labelCol\" :wrapper-col=\"wrapperCol\">\n        <a-checkbox v-model=\"config.proxy.setEnv\">\n          是否同时修改<code>HTTPS_PROXY</code>环境变量（不好用，不建议勾选）\n        </a-checkbox>\n        <div class=\"form-help\">\n          当发现某些应用并没有走加速通道或加速报错时，可尝试勾选此选项，并重新开启系统代理开关<br>\n          注意：当前已打开的命令行并不会实时生效，需要重新打开一个新的命令行窗口\n        </div>\n      </a-form-item>\n      <a-form-item v-if=\"isWindows()\" label=\"设置Loopback\" :label-col=\"labelCol\" :wrapper-col=\"wrapperCol\">\n        <a-button @click=\"loopbackVisible = true\">\n          去设置\n        </a-button>\n        <div class=\"form-help\">\n          解决<code>OneNote</code>、<code>MicrosoftStore</code>、<code>Outlook</code>等<code>UWP应用</code>开启代理后无法访问网络的问题\n        </div>\n      </a-form-item>\n\n      <hr>\n      <a-form-item label=\"排除国内域名\" :label-col=\"labelCol\" :wrapper-col=\"wrapperCol\">\n        <a-checkbox v-model=\"config.proxy.excludeDomesticDomainAllowList\">\n          是否排除国内域名白名单\n        </a-checkbox>\n        <div class=\"form-help\">\n          国内域名白名单内收录了国内可直接访问的域名，这些域名将不被代理<br>当里面某些域名你希望代理时，你可以配置这些域名为<code>不排除</code>，也可以关闭国内域名白名单\n        </div>\n      </a-form-item>\n      <a-form-item label=\"自动更新国内域名\" :label-col=\"labelCol\" :wrapper-col=\"wrapperCol\">\n        <a-checkbox v-model=\"config.proxy.autoUpdateDomesticDomainAllowList\">\n          是否自动更新国内域名白名单\n        </a-checkbox>\n        <div class=\"form-help\">\n          开启自动更新并启动系统代理时，将会异步从下面的远程地址下载国内域名白名单文件到本地。<br>\n          注：只要下载成功后，即使关闭自动更新功能，也会优先读取最近下载的文件！\n        </div>\n      </a-form-item>\n      <a-form-item label=\"远程国内域名地址\" :label-col=\"labelCol\" :wrapper-col=\"wrapperCol\">\n        <a-input v-model=\"config.proxy.remoteDomesticDomainAllowListFileUrl\" :title=\"config.proxy.remoteDomesticDomainAllowListFileUrl\" spellcheck=\"false\" />\n        <div class=\"form-help\">\n          远程国内域名白名单文件内容可以是<code>base64</code>编码格式，也可以是未经过编码的\n        </div>\n      </a-form-item>\n      <hr>\n      <a-form-item label=\"自定义排除域名\" :label-col=\"labelCol\" :wrapper-col=\"wrapperCol\">\n        <a-row :gutter=\"10\">\n          <a-col :span=\"22\">\n            <span>国内域名不包含的域名，可以在此处定义；配置为 <code>不排除</code>时，将被代理</span>\n          </a-col>\n          <a-col :span=\"2\">\n            <a-button type=\"primary\" icon=\"plus\" @click=\"addExcludeIp()\" />\n          </a-col>\n        </a-row>\n        <a-row v-for=\"(item, index) of excludeIpList\" ref=\"excludeIpList\" :key=\"index\" :gutter=\"10\" class=\"fine-tuning\">\n          <a-col :span=\"17\">\n            <MockInput v-model=\"item.key\" class=\"mt-1\" />\n          </a-col>\n          <a-col :span=\"5\">\n            <a-select v-model=\"item.value\" style=\"width:100%\">\n              <a-select-option v-for=\"(item2) of excludeIpOptions\" :key=\"item2.value\" :value=\"item2.value\">\n                {{ item2.label }}\n              </a-select-option>\n            </a-select>\n          </a-col>\n          <a-col :span=\"2\">\n            <a-button type=\"danger\" icon=\"minus\" @click=\"delExcludeIp(item, index)\" />\n          </a-col>\n        </a-row>\n      </a-form-item>\n    </div>\n    <template slot=\"footer\">\n      <div class=\"footer-bar\">\n        <a-button :loading=\"resetDefaultLoading\" class=\"mr10\" icon=\"sync\" @click=\"resetDefault()\">\n          恢复默认\n        </a-button>\n        <a-button :loading=\"applyLoading\" icon=\"check\" type=\"primary\" @click=\"apply()\">\n          应用\n        </a-button>\n      </div>\n    </template>\n\n    <a-drawer\n      placement=\"right\"\n      :closable=\"false\"\n      :visible.sync=\"loopbackVisible\"\n      width=\"660px\"\n      height=\"100%\"\n      :slots=\"{ title: 'title' }\"\n      wrap-class-name=\"json-wrapper\"\n      @close=\"loopbackVisible = false\"\n    >\n      <template slot=\"title\">\n        设置Loopback\n        <a-button style=\"float:right;margin-right:10px;\" @click=\"openEnableLoopback()\">\n          打开EnableLoopback\n        </a-button>\n      </template>\n      <div>\n        <div>1、此设置用于解决OneNote、MicrosoftStore、Outlook等UWP应用无法访问网络的问题。</div>\n        <div>2、点击右上方按钮，打开EnableLoopback，然后按下图所示操作即可</div>\n        <img style=\"margin-top:20px;border:1px solid #eee\" width=\"80%\" src=\"loopback.png\">\n      </div>\n    </a-drawer>\n  </ds-container>\n</template>\n\n<style lang=\"scss\">\n/*样式微调*/\n.fine-tuning .ant-btn-danger {\n  margin-top: 3px !important;\n}\n</style>\n"
  },
  {
    "path": "packages/gui/src/view/pages/server.vue",
    "content": "<script>\nimport _ from 'lodash'\nimport VueJsonEditor from 'vue-json-editor-fix-cn'\nimport Plugin from '../mixins/plugin'\nimport MockInput from '@/view/components/mock-input.vue'\n\nexport default {\n  name: 'Server',\n  components: {\n    VueJsonEditor,\n    MockInput,\n  },\n  mixins: [Plugin],\n  data () {\n    return {\n      key: 'server',\n      activeTabKey: '1',\n      dnsMappings: [],\n      speedTestList: [],\n      whiteList: [],\n      whiteListOptions: [\n        {\n          label: '不代理',\n          value: 'true',\n        },\n        {\n          label: '代理',\n          value: 'false',\n        },\n      ],\n    }\n  },\n  computed: {\n    speedDnsOptions () {\n      const options = []\n      if (!this.config || !this.config.server || !this.config.server.dns || !this.config.server.dns.providers) {\n        return options\n      }\n      _.forEach(this.config.server.dns.providers, (dnsConfig, key) => {\n        options.push({\n          value: key,\n          label: key,\n        })\n      })\n      return options\n    },\n  },\n  created () {\n  },\n  mounted () {\n    this.registerSpeedTestEvent()\n  },\n  methods: {\n    async onCrtSelect () {\n      const value = await this.$api.fileSelector.open(this.config.server.setting.rootCaFile.certPath, 'file')\n      if (value != null && value.length > 0) {\n        this.config.server.setting.rootCaFile.certPath = value[0]\n      }\n    },\n    async onKeySelect () {\n      const value = await this.$api.fileSelector.open(this.config.server.setting.rootCaFile.keyPath, 'file')\n      if (value != null && value.length > 0) {\n        this.config.server.setting.rootCaFile.keyPath = value[0]\n      }\n    },\n    ready () {\n      this.initDnsMapping()\n      this.initWhiteList()\n      if (this.config.server.dns.speedTest.dnsProviders) {\n        this.speedDns = this.config.server.dns.speedTest.dnsProviders\n      }\n    },\n    async applyBefore () {\n      this.submitDnsMappings()\n      this.submitWhiteList()\n      this.delEmptySpeedHostname()\n    },\n    async applyAfter () {\n      if (this.status.server.enabled) {\n        return this.$api.server.restart()\n      }\n    },\n    // dnsMapping\n    initDnsMapping () {\n      this.dnsMappings = []\n      for (const key in this.config.server.dns.mapping) {\n        const value = this.config.server.dns.mapping[key]\n        this.dnsMappings.push({\n          key,\n          value,\n        })\n      }\n    },\n    submitDnsMappings () {\n      const dnsMapping = {}\n      for (const item of this.dnsMappings) {\n        if (item.key) {\n          const hostname = this.handleHostname(item.key)\n          if (hostname) {\n            dnsMapping[hostname] = item.value\n          }\n        }\n      }\n      this.config.server.dns.mapping = dnsMapping\n    },\n    deleteDnsMapping (item, index) {\n      this.dnsMappings.splice(index, 1)\n    },\n    addDnsMapping () {\n      this.dnsMappings.unshift({ key: '', value: 'quad9' })\n      this.focusFirst(this.$refs.dnsMappings)\n    },\n\n    // whiteList\n    initWhiteList () {\n      this.whiteList = []\n      for (const key in this.config.server.whiteList) {\n        const value = this.config.server.whiteList[key]\n        this.whiteList.push({\n          key: key || '',\n          value: value === true ? 'true' : 'false',\n        })\n      }\n    },\n    addWhiteList () {\n      this.whiteList.unshift({ key: '', value: 'true' })\n      this.focusFirst(this.$refs.whiteList)\n    },\n    deleteWhiteList (item, index) {\n      this.whiteList.splice(index, 1)\n    },\n    submitWhiteList () {\n      const whiteList = {}\n      for (const item of this.whiteList) {\n        if (item.key) {\n          const hostname = this.handleHostname(item.key)\n          if (hostname) {\n            whiteList[hostname] = (item.value === 'true')\n          }\n        }\n      }\n      this.config.server.whiteList = whiteList\n    },\n    getSpeedTestConfig () {\n      return this.config.server.dns.speedTest\n    },\n    addSpeedHostname () {\n      this.getSpeedTestConfig().hostnameList.unshift('')\n      this.focusFirst(this.$refs.hostnameList)\n    },\n    delSpeedHostname (item, index) {\n      this.getSpeedTestConfig().hostnameList.splice(index, 1)\n    },\n    delEmptySpeedHostname () {\n      for (let i = this.getSpeedTestConfig().hostnameList.length - 1; i >= 0; i--) {\n        const hostname = this.handleHostname(this.getSpeedTestConfig().hostnameList[i])\n        if (!hostname) {\n          this.getSpeedTestConfig().hostnameList.splice(i, 1)\n        }\n      }\n    },\n    reSpeedTest () {\n      this.$api.server.reSpeedTest()\n    },\n    registerSpeedTestEvent () {\n      const listener = async (event, message) => {\n        console.log('get speed event', event, message)\n        if (message.key === 'getList') {\n          this.speedTestList = message.value\n        }\n      }\n      this.$api.ipc.on('speed', listener)\n      const interval = this.startSpeedRefreshInterval()\n      this.reloadAllSpeedTester()\n\n      this.$once('hook:beforeDestroy', () => {\n        clearInterval(interval)\n        this.$api.ipc.removeAllListeners('speed')\n      })\n    },\n    async reloadAllSpeedTester () {\n      this.$api.server.getSpeedTestList()\n    },\n    startSpeedRefreshInterval () {\n      return setInterval(() => {\n        this.reloadAllSpeedTester()\n      }, 5000)\n    },\n    async handleTabChange (key) {\n      this.activeTabKey = key\n      if (key !== '2' && key !== '3' && key !== '5' && key !== '6' && key !== '7') {\n        // 没有 JsonEditor，启用SearchBar\n        window.config.disableSearchBar = false\n      } else {\n        // 有 JsonEditor，禁用SearchBar\n        window.config.disableSearchBar = true\n      }\n    },\n  },\n}\n</script>\n\n<template>\n  <ds-container>\n    <template slot=\"header\">\n      加速服务设置\n    </template>\n\n    <div style=\"height: 100%\" class=\"json-wrapper\">\n      <a-tabs\n        v-if=\"config\"\n        :default-active-key=\"activeTabKey\"\n        tab-position=\"left\"\n        :style=\"{ height: '100%' }\"\n        @change=\"handleTabChange\"\n      >\n        <a-tab-pane key=\"1\" tab=\"基本设置\">\n          <div v-if=\"activeTabKey === '1'\" style=\"padding-right:10px\">\n            <a-form-item label=\"代理服务:\" :label-col=\"labelCol\" :wrapper-col=\"wrapperCol\">\n              <a-checkbox v-model=\"config.server.enabled\">\n                随应用启动\n              </a-checkbox>\n              <a-tag v-if=\"status.server.enabled\" color=\"green\">\n                当前已启动\n              </a-tag>\n              <a-tag v-else color=\"red\">\n                当前未启动\n              </a-tag>\n            </a-form-item>\n            <a-form-item label=\"绑定IP\" :label-col=\"labelCol\" :wrapper-col=\"wrapperCol\">\n              <a-input v-model=\"config.server.host\" spellcheck=\"false\" />\n              <div class=\"form-help\">\n                你可以设置为<code>0.0.0.0</code>，让其他电脑可以使用此代理服务\n              </div>\n            </a-form-item>\n            <a-form-item label=\"代理端口\" :label-col=\"labelCol\" :wrapper-col=\"wrapperCol\">\n              <a-input-number v-model=\"config.server.port\" :min=\"0\" :max=\"65535\" :precision=\"0\" spellcheck=\"false\" />\n              <div class=\"form-help\">\n                修改后需要重启应用\n              </div>\n            </a-form-item>\n            <hr>\n            <a-form-item label=\"全局校验SSL\" :label-col=\"labelCol\" :wrapper-col=\"wrapperCol\">\n              <a-checkbox v-model=\"config.server.setting.NODE_TLS_REJECT_UNAUTHORIZED\">\n                NODE_TLS_REJECT_UNAUTHORIZED\n              </a-checkbox>\n              <div class=\"form-help\">\n                高风险操作，没有特殊情况请勿关闭\n              </div>\n            </a-form-item>\n            <a-form-item label=\"代理校验SSL\" :label-col=\"labelCol\" :wrapper-col=\"wrapperCol\">\n              <a-checkbox v-model=\"config.server.setting.verifySsl\">\n                校验加速目标网站的ssl证书\n              </a-checkbox>\n              <div class=\"form-help\">\n                如果目标网站证书有问题，但你想强行访问，可以临时关闭此项\n              </div>\n            </a-form-item>\n            <a-form-item label=\"根证书\" :label-col=\"labelCol\" :wrapper-col=\"wrapperCol\">\n              <a-input-search\n                v-model=\"config.server.setting.rootCaFile.certPath\" addon-before=\"Cert\" enter-button=\"选择\"\n                :title=\"config.server.setting.rootCaFile.certPath\" spellcheck=\"false\"\n                @search=\"onCrtSelect\"\n              />\n              <a-input-search\n                v-model=\"config.server.setting.rootCaFile.keyPath\" addon-before=\"Key\" enter-button=\"选择\"\n                :title=\"config.server.setting.rootCaFile.keyPath\" spellcheck=\"false\"\n                @search=\"onKeySelect\"\n              />\n            </a-form-item>\n            <hr>\n            <a-form-item label=\"启用拦截\" :label-col=\"labelCol\" :wrapper-col=\"wrapperCol\">\n              <a-checkbox v-model=\"config.server.intercept.enabled\">\n                启用拦截\n              </a-checkbox>\n              <div class=\"form-help\">\n                关闭拦截，且关闭增强功能时，就不需要安装根证书，退化为安全模式\n              </div>\n            </a-form-item>\n            <a-form-item label=\"启用脚本\" :label-col=\"labelCol\" :wrapper-col=\"wrapperCol\">\n              <a-checkbox v-model=\"config.server.setting.script.enabled\">\n                允许插入并运行脚本\n              </a-checkbox>\n              <div class=\"form-help\">\n                关闭后，<code>Github油猴脚本</code>也将关闭\n              </div>\n            </a-form-item>\n          </div>\n        </a-tab-pane>\n        <a-tab-pane key=\"2\" tab=\"拦截设置\">\n          <div v-if=\"activeTabKey === '2'\" style=\"height:100%\">\n            <VueJsonEditor\n              v-model=\"config.server.intercepts\" style=\"height:100%\" mode=\"code\"\n              :show-btns=\"false\" :expanded-on-start=\"true\"\n            />\n          </div>\n        </a-tab-pane>\n        <a-tab-pane key=\"3\" tab=\"超时时间设置\">\n          <div v-if=\"activeTabKey === '3'\" style=\"height:100%;display:flex;flex-direction:column\">\n            <a-form-item label=\"默认超时时间\" :label-col=\"labelCol\" :wrapper-col=\"wrapperCol\">\n              请求：<a-input-number v-model=\"config.server.setting.defaultTimeout\" :step=\"1000\" :min=\"1000\" :precision=\"0\" spellcheck=\"false\" /> ms，对应<code>timeout</code>配置<br>\n              连接：<a-input-number v-model=\"config.server.setting.defaultKeepAliveTimeout\" :step=\"1000\" :min=\"1000\" :precision=\"0\" spellcheck=\"false\" /> ms，对应<code>keepAliveTimeout</code>配置\n            </a-form-item>\n            <hr style=\"margin-bottom:15px\">\n            <div>这里指定域名的超时时间：<span class=\"form-help\">（域名配置可使用通配符或正则）</span></div>\n            <VueJsonEditor\n              v-model=\"config.server.setting.timeoutMapping\" style=\"flex-grow:1;min-height:300px;margin-top:10px\" mode=\"code\"\n              :show-btns=\"false\" :expanded-on-start=\"true\"\n            />\n          </div>\n        </a-tab-pane>\n        <a-tab-pane key=\"4\" tab=\"域名白名单\">\n          <div v-if=\"activeTabKey === '4'\">\n            <a-row style=\"margin-top:10px\">\n              <a-col span=\"21\">\n                <div>配置为<code>不代理</code>的域名不会通过代理</div>\n              </a-col>\n              <a-col span=\"3\">\n                <a-button style=\"margin-left:8px\" type=\"primary\" icon=\"plus\" @click=\"addWhiteList()\" />\n              </a-col>\n            </a-row>\n            <a-row v-for=\"(item, index) of whiteList\" ref=\"whiteList\" :key=\"index\" :gutter=\"10\" style=\"margin-top: 5px\">\n              <a-col :span=\"16\">\n                <MockInput v-model=\"item.key\" />\n              </a-col>\n              <a-col :span=\"5\">\n                <a-select v-model=\"item.value\" style=\"width:100%\">\n                  <a-select-option v-for=\"(item2) of whiteListOptions\" :key=\"item2.value\" :value=\"item2.value\">\n                    {{ item2.label }}\n                  </a-select-option>\n                </a-select>\n              </a-col>\n              <a-col :span=\"3\">\n                <a-button type=\"danger\" icon=\"minus\" @click=\"deleteWhiteList(item, index)\" />\n              </a-col>\n            </a-row>\n          </div>\n        </a-tab-pane>\n        <a-tab-pane key=\"5\" tab=\"自动兼容程序\">\n          <div v-if=\"activeTabKey === '5'\" style=\"height:100%;display:flex;flex-direction:column\">\n            <div>\n              说明：<code>自动兼容程序</code>会自动根据错误信息进行兼容性调整，并将兼容设置保存在 <code>~/.dev-sidecar/automaticCompatibleConfig.json</code> 文件中。但并不是所有的兼容设置都是正确的，所以需要通过以下配置来覆盖错误的兼容设置。\n            </div>\n            <VueJsonEditor\n              v-model=\"config.server.compatible\" style=\"flex-grow:1;min-height:300px;margin-top:10px;\" mode=\"code\"\n              :show-btns=\"false\" :expanded-on-start=\"true\"\n            />\n          </div>\n        </a-tab-pane>\n        <a-tab-pane key=\"6\" tab=\"IP预设置\">\n          <div v-if=\"activeTabKey === '6'\" style=\"height:100%;display:flex;flex-direction:column\">\n            <div>\n              提示：<code>IP预设置</code>功能，优先级高于 <code>DNS设置</code>\n              <span class=\"form-help\">（域名配置可使用通配符或正则）</span>\n            </div>\n            <VueJsonEditor\n              v-model=\"config.server.preSetIpList\" style=\"flex-grow:1;min-height:300px;margin-top:10px;\" mode=\"code\"\n              :show-btns=\"false\" :expanded-on-start=\"true\"\n            />\n          </div>\n        </a-tab-pane>\n        <a-tab-pane key=\"7\" tab=\"DNS服务管理\">\n          <div v-if=\"activeTabKey === '7'\" style=\"height:100%\">\n            <VueJsonEditor\n              v-model=\"config.server.dns.providers\" style=\"height:100%\" mode=\"code\"\n              :show-btns=\"false\" :expanded-on-start=\"true\"\n            />\n          </div>\n        </a-tab-pane>\n        <a-tab-pane key=\"8\" tab=\"DNS设置\">\n          <div v-if=\"activeTabKey === '8'\">\n            <a-row style=\"margin-top:10px\">\n              <a-col span=\"21\">\n                <div>这里配置哪些域名需要通过国外DNS服务器获取IP进行访问</div>\n              </a-col>\n              <a-col span=\"3\">\n                <a-button style=\"margin-left:8px\" type=\"primary\" icon=\"plus\" @click=\"addDnsMapping()\" />\n              </a-col>\n            </a-row>\n            <a-row v-for=\"(item, index) of dnsMappings\" ref=\"dnsMappings\" :key=\"index\" :gutter=\"10\" style=\"margin-top: 5px\">\n              <a-col :span=\"15\">\n                <MockInput v-model=\"item.key\" />\n              </a-col>\n              <a-col :span=\"6\">\n                <a-select v-model=\"item.value\" :disabled=\"item.value === false\" style=\"width: 100%\">\n                  <a-select-option v-for=\"(item) of speedDnsOptions\" :key=\"item.value\" :value=\"item.value\">\n                    {{ item.value }}\n                  </a-select-option>\n                </a-select>\n              </a-col>\n              <a-col :span=\"3\">\n                <a-button type=\"danger\" icon=\"minus\" @click=\"deleteDnsMapping(item, index)\" />\n              </a-col>\n            </a-row>\n          </div>\n        </a-tab-pane>\n        <a-tab-pane key=\"9\" tab=\"IP测速\">\n          <div v-if=\"activeTabKey === '9'\" class=\"ip-tester\" style=\"padding-right: 10px\">\n            <a-alert type=\"info\" message=\"对从DNS获取到的IP进行测速，使用速度最快的IP进行访问（注意：对使用了增强功能的域名没啥用）\" />\n            <a-form-item label=\"开启DNS测速\" :label-col=\"labelCol\" :wrapper-col=\"wrapperCol\">\n              <a-checkbox v-model=\"getSpeedTestConfig().enabled\">\n                启用\n              </a-checkbox>\n            </a-form-item>\n            <a-form-item label=\"自动测试间隔\" :label-col=\"labelCol\" :wrapper-col=\"wrapperCol\">\n              <a-input-number v-model=\"getSpeedTestConfig().interval\" :step=\"1000\" :min=\"1\" :precision=\"0\" spellcheck=\"false\" /> ms\n            </a-form-item>\n            <!-- <a-form-item label=\"慢速IP阈值\" :label-col=\"labelCol\" :wrapper-col=\"wrapperCol\">\n              <a-input-number v-model=\"config.server.setting.lowSpeedDelay\" :step=\"10\" :min=\"100\" :precision=\"0\" spellcheck=\"false\" /> ms\n            </a-form-item> -->\n            <div>使用以下DNS获取IP进行测速</div>\n            <a-row style=\"margin-top:10px\">\n              <a-col span=\"24\">\n                <a-checkbox-group\n                  v-model=\"getSpeedTestConfig().dnsProviders\"\n                  :options=\"speedDnsOptions\"\n                />\n              </a-col>\n            </a-row>\n            <a-row :gutter=\"10\" class=\"mt20\">\n              <a-col :span=\"21\">\n                以下域名在启动后立即进行测速，其他域名在第一次访问时才测速\n              </a-col>\n              <a-col :span=\"2\">\n                <a-button style=\"margin-left:10px\" type=\"primary\" icon=\"plus\" @click=\"addSpeedHostname()\" />\n              </a-col>\n            </a-row>\n            <a-row v-for=\"(item, index) of getSpeedTestConfig().hostnameList\" ref=\"hostnameList\" :key=\"index\" :gutter=\"10\" style=\"margin-top: 5px\">\n              <a-col :span=\"21\">\n                <MockInput v-model=\"getSpeedTestConfig().hostnameList[index]\" />\n              </a-col>\n              <a-col :span=\"2\">\n                <a-button style=\"margin-left:10px\" type=\"danger\" icon=\"minus\" @click=\"delSpeedHostname(item, index)\" />\n              </a-col>\n            </a-row>\n\n            <a-divider />\n            <a-row :gutter=\"10\" class=\"mt10\">\n              <a-col span=\"24\">\n                <a-button type=\"primary\" icon=\"plus\" @click=\"reSpeedTest()\">\n                  立即重新测速\n                </a-button>\n                <a-button class=\"ml10\" type=\"primary\" icon=\"reload\" @click=\"reloadAllSpeedTester()\">\n                  刷新\n                </a-button>\n              </a-col>\n            </a-row>\n\n            <a-row :gutter=\"20\">\n              <a-col v-for=\"(item, key) of speedTestList\" :key=\"key\" span=\"12\">\n                <a-card size=\"small\" class=\"mt10\" :title=\"key\">\n                  <a slot=\"extra\" href=\"javascript:void(0)\" :title=\"key\" style=\"cursor:default\">\n                    <a-icon v-if=\"item.alive.length > 0\" type=\"check\" />\n                    <a-icon v-else type=\"info-circle\" />\n                  </a>\n                  <a-tag\n                    v-for=\"(element, index) of item.backupList\" :key=\"index\" style=\"margin:2px;\"\n                    :title=\"element.title || `测速中：${element.host}`\" :color=\"element.time ? (element.time > config.server.setting.lowSpeedDelay ? 'orange' : 'green') : (element.title ? 'red' : '')\"\n                  >\n                    {{ element.host }} {{ element.time ? `${element.time}ms` : (element.title ? '' : '测速中') }} {{ element.dns }}\n                  </a-tag>\n                </a-card>\n              </a-col>\n            </a-row>\n          </div>\n        </a-tab-pane>\n      </a-tabs>\n    </div>\n    <template slot=\"footer\">\n      <div class=\"footer-bar\">\n        <a-button :loading=\"resetDefaultLoading\" class=\"mr10\" icon=\"sync\" @click=\"resetDefault()\">\n          恢复默认\n        </a-button>\n        <a-button :loading=\"applyLoading\" icon=\"check\" type=\"primary\" @click=\"apply()\">\n          应用\n        </a-button>\n      </div>\n    </template>\n  </ds-container>\n</template>\n\n<style lang=\"scss\">\n.json-wrapper {\n  .ant-drawer-wrapper-body {\n    display: flex;\n    flex-direction: column;\n\n    .ant-drawer-body {\n      flex: 1;\n      height: 0;\n    }\n  }\n\n  .jsoneditor-vue {\n    height: 100%;\n  }\n\n  .ant-tabs {\n    height: 100%;\n  }\n\n  .ant-tabs-content {\n    height: 100%;\n  }\n\n  .ant-tabs-tabpane-active {\n    height: 100%;\n    overflow-y: auto;\n    overflow-x: hidden;\n  }\n  .ant-input-group-addon:first-child {\n    width: 45px;\n  }\n}\n</style>\n"
  },
  {
    "path": "packages/gui/src/view/pages/setting.vue",
    "content": "<script>\nimport { ipcRenderer } from 'electron'\nimport Plugin from '../mixins/plugin'\nimport { colorTheme } from '../composables/theme'\n\nexport default {\n  name: 'Setting',\n  mixins: [Plugin],\n  data () {\n    return {\n      key: 'app',\n      removeUserConfigLoading: false,\n      reloadLoading: false,\n      urlBackup: null,\n      personalUrlBackup: null,\n      maxLogFileSizeStep: 1, // 单位不同，值不同：GB=1，MB=100\n      maxLogFileSizeUnit: [\n        {\n          label: 'GB',\n          value: 'GB',\n        },\n        {\n          label: 'MB',\n          value: 'MB',\n        },\n      ],\n    }\n  },\n  methods: {\n    ready (config) {\n      this.urlBackup = config.app.remoteConfig.url\n      this.personalUrlBackup = config.app.remoteConfig.personalUrl\n    },\n    getEventKey (event) {\n      // 忽略以下键\n      switch (event.key) {\n        case 'Control':\n        case 'Alt':\n        case 'Shift':\n        case 'Meta': // Window键\n        case 'Escape':\n        case 'Backspace':\n        case 'Tab':\n        case 'CapsLock':\n        case 'NumLock':\n        case 'Enter':\n        case 'ArrowUp':\n        case 'ArrowDown':\n        case 'ArrowLeft':\n        case 'ArrowRight':\n          return ''\n      }\n\n      switch (event.code) {\n        // F1 ~ F12\n        case 'F1': return 'F1'\n        case 'F2': return 'F2'\n        case 'F3': return 'F3'\n        case 'F4': return 'F4'\n        case 'F5': return 'F5'\n        case 'F6': return 'F6'\n        case 'F7': return 'F7'\n        case 'F8': return 'F8'\n        case 'F9': return 'F9'\n        case 'F10': return 'F10'\n        case 'F11': return 'F11'\n        case 'F12': return 'F12'\n\n        // 0 ~ 9\n        case 'Digit0': return '0'\n        case 'Digit1': return '1'\n        case 'Digit2': return '2'\n        case 'Digit3': return '3'\n        case 'Digit4': return '4'\n        case 'Digit5': return '5'\n        case 'Digit6': return '6'\n        case 'Digit7': return '7'\n        case 'Digit8': return '8'\n        case 'Digit9': return '9'\n\n        case 'Backquote': return '`'\n        case 'Minus': return '-'\n        case 'Equal': return '='\n        case 'Space': return 'Space'\n\n        case 'BracketLeft': return '['\n        case 'BracketRight': return ']'\n        case 'Backslash': return '\\\\'\n        case 'Semicolon': return ';'\n        case 'Quote': return '\\''\n        case 'Comma': return ','\n        case 'Period': return '.'\n        case 'Slash': return '/'\n\n        case 'Insert': return 'Insert'\n        case 'Delete': return 'Delete'\n        case 'Home': return 'Home'\n        case 'End': return 'End'\n        case 'PageUp': return 'PageUp'\n        case 'PageDown': return 'PageDown'\n\n        // 小键盘\n        case 'Numpad1': return 'Num1'\n        case 'Numpad2': return 'Num2'\n        case 'Numpad3': return 'Num3'\n        case 'Numpad4': return 'Num4'\n        case 'Numpad5': return 'Num5'\n        case 'Numpad6': return 'Num6'\n        case 'Numpad7': return 'Num7'\n        case 'Numpad8': return 'Num8'\n        case 'Numpad9': return 'Num9'\n        case 'Numpad0': return 'Num0'\n\n        // 不支持监听以下几个键，返回空\n        case 'NumpadDivide': // return 'Num/'\n        case 'NumpadMultiply': // return 'Num*'\n        case 'NumpadDecimal': // return 'Num.'\n        case 'NumpadSubtract': // return 'Num-'\n        case 'NumpadAdd': // return 'Num+'\n          return ''\n      }\n\n      // 字母\n      if (event.code.startsWith('Key') && event.code.length === 4) {\n        return event.key.toUpperCase()\n      }\n\n      console.error(`未能识别的按键：key=${event.key}, code=${event.code}, keyCode=${event.keyCode}`)\n      return ''\n    },\n    async disableBeforeInputEvent () {\n      clearTimeout(window.enableBeforeInputEventTimeout)\n      window.config.disableBeforeInputEvent = true\n      window.enableBeforeInputEventTimeout = setTimeout(() => {\n        window.config.disableBeforeInputEvent = false\n      }, 2000)\n    },\n    shortcutChange () {\n      this.config.app.showHideShortcut = '无'\n    },\n    shortcutKeyUp (event) {\n      event.preventDefault()\n      this.disableBeforeInputEvent()\n    },\n    shortcutKeyDown (event) {\n      event.preventDefault()\n      this.disableBeforeInputEvent()\n\n      // console.info(`code=${event.code}, key=${event.key}, keyCode=${event.keyCode}`)\n      if (event.type !== 'keydown') {\n        return\n      }\n\n      const key = this.getEventKey(event)\n      if (!key) {\n        this.config.app.showHideShortcut = '无'\n        return\n      }\n\n      // 判断 Ctrl、Alt、Shift、Window 按钮是否已按下，如果已按下，则拼接键值\n      let shortcut = event.ctrlKey ? 'Ctrl + ' : ''\n      if (event.altKey) {\n        shortcut += 'Alt + '\n      }\n      if (event.shiftKey) {\n        shortcut += 'Shift + '\n      }\n      if (event.metaKey) {\n        shortcut += 'Meta + '\n      }\n\n      // 如果以上按钮都没有按下，并且当前键不是F1、F2、F4、F6~F11时，则直接返回（注：F5已经是刷新页面快捷键、F12已经是打开DevTools的快捷键了）\n      if (shortcut === '' && !key.match(/^F([1246-9]|1[01])$/g)) {\n        this.config.app.showHideShortcut = '无'\n        return\n      }\n\n      // 拼接键值\n      shortcut += key\n\n      if (shortcut === 'Ctrl + F' || shortcut === 'Shift + F3') {\n        shortcut = '无' // 如果是其他已被占用快捷键，则设置为 '无'\n      }\n\n      this.config.app.showHideShortcut = shortcut\n    },\n    async applyBefore () {\n      if (!this.config.app.showHideShortcut) {\n        this.config.app.showHideShortcut = '无'\n      }\n    },\n    async applyAfter () {\n      let reloadLazy = 10\n\n      let theme = this.config.app.theme\n      if (theme === 'system') {\n        theme = window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light'\n      }\n      colorTheme.value = theme\n\n      // 判断远程配置地址是否变更过，如果是则重载远程配置并重启服务\n      if (this.config.app.remoteConfig.url !== this.urlBackup || this.config.app.remoteConfig.personalUrl !== this.personalUrlBackup) {\n        await this.$api.config.downloadRemoteConfig()\n        await this.reloadConfigAndRestart()\n        reloadLazy = 300\n        setTimeout(() => window.location.reload(), reloadLazy)\n      }\n\n      // 变更 “打开窗口快捷键”\n      ipcRenderer.send('change-showHideShortcut', this.config.app.showHideShortcut)\n    },\n    async openExternal (url) {\n      await this.$api.ipc.openExternal(url)\n    },\n    onAutoStartChange () {\n      this.$api.autoStart.enabled(this.config.app.autoStart.enabled)\n      this.saveConfig()\n    },\n    async onRemoteConfigEnabledChange () {\n      await this.saveConfig()\n      if (this.config.app.remoteConfig.enabled === true) {\n        this.reloadLoading = true\n        try {\n          this.$message.info('开始下载远程配置')\n          await this.$api.config.downloadRemoteConfig()\n          this.$message.info('下载远程配置成功，开始重启代理服务和系统代理')\n          await this.reloadConfigAndRestart()\n        } finally {\n          this.reloadLoading = false\n        }\n      } else {\n        this.$message.info('远程配置已关闭，开始重启代理服务和系统代理')\n        await this.reloadConfigAndRestart()\n      }\n    },\n    async reloadRemoteConfig () {\n      if (this.config.app.remoteConfig.enabled === false) {\n        return\n      }\n\n      this.reloadLoading = true\n      try {\n        const remoteConfig = {}\n\n        await this.$api.config.readRemoteConfigStr().then((ret) => {\n          remoteConfig.old1 = ret\n        })\n        await this.$api.config.readRemoteConfigStr('_personal').then((ret) => {\n          remoteConfig.old2 = ret\n        })\n        await this.$api.config.downloadRemoteConfig()\n        await this.$api.config.readRemoteConfigStr().then((ret) => {\n          remoteConfig.new1 = ret\n        })\n        await this.$api.config.readRemoteConfigStr('_personal').then((ret) => {\n          remoteConfig.new2 = ret\n        })\n\n        if (remoteConfig.old1 === remoteConfig.new1 && remoteConfig.old2 === remoteConfig.new2) {\n          this.$message.info('远程配置没有变化，不做任何处理。')\n          this.$message.warn('如果您确实修改了远程配置，请稍等片刻再重试！')\n        } else {\n          this.$message.success('获取到了最新的远程配置，开始重启代理服务和系统代理')\n          await this.reloadConfigAndRestart()\n        }\n      } finally {\n        this.reloadLoading = false\n      }\n    },\n    async restoreFactorySettings () {\n      this.$confirm({\n        title: '确定要恢复出厂设置吗？',\n        width: 610,\n        content: h => (\n          <div class=\"restore-factory-settings\">\n            <hr />\n            <p>\n              <h3>操作警告：</h3>\n              <div>\n                该功能将备份您的所有页面的个性化配置，并重载\n                <span>默认配置</span>\n                及\n                <span>远程配置</span>\n                ，请谨慎操作！！！\n              </div>\n            </p>\n            <hr />\n            <p>\n              <h3>找回个性化配置的方法：</h3>\n              <div>\n                1. 找到备份文件，路径：\n                <span>~/.dev-sidecar/config.json.时间戳.bak.json</span>\n                <br />\n                2. 将该备份文件重命名为\n                <span>config.json</span>\n                ，再重启软件即可恢复个性化配置。\n              </div>\n            </p>\n          </div>\n        ),\n        cancelText: '取消',\n        okText: '确定',\n        onOk: async () => {\n          this.removeUserConfigLoading = true\n          try {\n            const result = await this.$api.config.removeUserConfig()\n            if (result) {\n              this.config = await this.$api.config.get()\n              this.$message.success('恢复出厂设置成功，开始重启代理服务和系统代理')\n              await this.reloadConfigAndRestart()\n            } else {\n              this.$message.info('已是出厂设置，无需恢复')\n            }\n          } finally {\n            this.removeUserConfigLoading = false\n          }\n        },\n        onCancel () {},\n      })\n    },\n    async onMaxLogFileSizeUnitChange (value) {\n      if (value === 'MB') {\n        this.config.app.maxLogFileSize = Math.ceil((this.config.app.maxLogFileSize || 1024) * 1024) // 转为整数\n        this.maxLogFileSizeStep = 100\n      } else {\n        this.config.app.maxLogFileSize = ((this.config.app.maxLogFileSize || 1024) / 1024).toFixed(2) - 0 // 最多保留2位小数\n        this.maxLogFileSizeStep = 1\n      }\n      this.$refs.maxLogFileSize.focus()\n    },\n    async onLogFileSavePathSelect () {\n      const value = await this.$api.fileSelector.open(this.config.app.logFileSavePath, 'dir')\n      if (value != null && value.length > 0) {\n        this.config.app.logFileSavePath = value[0]\n      }\n    },\n  },\n}\n</script>\n\n<template>\n  <ds-container>\n    <template slot=\"header\">\n      设置\n    </template>\n    <template slot=\"header-right\">\n      <a-button class=\"mr10\" icon=\"profile\" @click=\"openLog()\">查看日志</a-button>\n    </template>\n\n    <div v-if=\"config\">\n      <a-form-item label=\"开机自启\" :label-col=\"labelCol\" :wrapper-col=\"wrapperCol\">\n        <a-checkbox v-model=\"config.app.autoStart.enabled\" @change=\"onAutoStartChange\">\n          本应用开机自启\n        </a-checkbox>\n      </a-form-item>\n      <a-form-item v-if=\"systemPlatform === 'mac'\" label=\"隐藏Dock图标\" :label-col=\"labelCol\" :wrapper-col=\"wrapperCol\">\n        <a-checkbox v-model=\"config.app.dock.hideWhenWinClose\">\n          关闭窗口时隐藏Dock图标(仅限Mac)\n        </a-checkbox>\n        <div class=\"form-help\">\n          修改后需要重启应用\n        </div>\n      </a-form-item>\n      <hr>\n      <a-form-item label=\"远程配置\" :label-col=\"labelCol\" :wrapper-col=\"wrapperCol\">\n        <a-checkbox v-model=\"config.app.remoteConfig.enabled\" @change=\"onRemoteConfigEnabledChange\">\n          启用远程配置\n        </a-checkbox>\n        <div class=\"form-help\">\n          应用启动时会向下面的地址请求配置补丁，获得最新的优化后的github访问体验。<br>\n          如果您觉得远程配置有安全风险，请关闭此功能，或删除共享远程配置，仅使用个人远程配置。<br>\n          配置优先级：本地修改配置  >  个人远程配置  >  共享远程配置 > 默认配置\n        </div>\n      </a-form-item>\n      <a-form-item label=\"共享远程配置地址\" :label-col=\"labelCol\" :wrapper-col=\"wrapperCol\">\n        <a-input v-model=\"config.app.remoteConfig.url\" :title=\"config.app.remoteConfig.url\" spellcheck=\"false\" />\n      </a-form-item>\n      <a-form-item label=\"个人远程配置地址\" :label-col=\"labelCol\" :wrapper-col=\"wrapperCol\">\n        <a-input v-model=\"config.app.remoteConfig.personalUrl\" :title=\"config.app.remoteConfig.personalUrl\" spellcheck=\"false\" />\n      </a-form-item>\n      <a-form-item label=\"重载远程配置\" :label-col=\"labelCol\" :wrapper-col=\"wrapperCol\">\n        <a-button :disabled=\"config.app.remoteConfig.enabled === false\" :loading=\"reloadLoading\" icon=\"sync\" @click=\"reloadRemoteConfig()\">\n          重载远程配置\n        </a-button>\n        <div class=\"form-help\">\n          注意，部分远程配置文件所在站点，修改内容后可能需要等待一段时间才能生效。<br>\n          如果重载远程配置后发现下载的还是修改前的内容，请稍等片刻再重试。\n        </div>\n      </a-form-item>\n      <hr>\n      <a-form-item label=\"主题设置\" :label-col=\"labelCol\" :wrapper-col=\"wrapperCol\">\n        <a-radio-group v-model=\"config.app.theme\" default-value=\"light\" button-style=\"solid\">\n          <a-radio-button value=\"light\" title=\"light\">\n            亮色\n          </a-radio-button>\n          <a-radio-button value=\"dark\" title=\"dark\">\n            暗色\n          </a-radio-button>\n          <a-radio-button value=\"system\" title=\"system\">\n            跟随系统\n          </a-radio-button>\n        </a-radio-group>\n      </a-form-item>\n      <a-form-item label=\"首页提示\" :label-col=\"labelCol\" :wrapper-col=\"wrapperCol\">\n        <a-radio-group v-model=\"config.app.showShutdownTip\" default-value=\"true\" button-style=\"solid\">\n          <a-radio-button :value=\"true\">\n            显示\n          </a-radio-button>\n          <a-radio-button :value=\"false\">\n            隐藏\n          </a-radio-button>\n        </a-radio-group>\n        <div class=\"form-help\">\n          是否显示首页的警告提示\n        </div>\n      </a-form-item>\n      <a-form-item v-if=\"!isLinux()\" label=\"关闭策略\" :label-col=\"labelCol\" :wrapper-col=\"wrapperCol\">\n        <a-radio-group v-model=\"config.app.closeStrategy\" default-value=\"0\" button-style=\"solid\">\n          <a-radio-button :value=\"0\">\n            弹出提示\n          </a-radio-button>\n          <a-radio-button :value=\"1\">\n            直接退出\n          </a-radio-button>\n          <a-radio-button :value=\"2\">\n            最小化到系统托盘\n          </a-radio-button>\n        </a-radio-group>\n        <div class=\"form-help\">\n          点击窗口右上角关闭按钮的效果\n        </div>\n      </a-form-item>\n      <hr>\n      <a-form-item label=\"打开窗口快捷键\" :label-col=\"labelCol\" :wrapper-col=\"wrapperCol\">\n        <a-input v-model=\"config.app.showHideShortcut\" spellcheck=\"false\" @change=\"shortcutChange\" @keydown=\"shortcutKeyDown\" @keyup=\"shortcutKeyUp\" />\n        <div class=\"form-help\">\n          部分快捷键已被占用：<code>F5</code>、<code>F12</code>、<code>Ctrl+F</code>、<code>F3</code>、<code>Shift+F3</code>\n        </div>\n      </a-form-item>\n      <a-form-item label=\"启动时窗口状态\" :label-col=\"labelCol\" :wrapper-col=\"wrapperCol\">\n        <a-radio-group v-model=\"config.app.startShowWindow\" default-value=\"true\" button-style=\"solid\">\n          <a-radio-button :value=\"true\">\n            打开窗口\n          </a-radio-button>\n          <a-radio-button :value=\"false\">\n            隐藏窗口\n          </a-radio-button>\n        </a-radio-group>\n        <div class=\"form-help\">\n          启动软件时，是否打开窗口。提示：如果设置为隐藏窗口，可点击系统托盘小图标打开窗口。\n        </div>\n      </a-form-item>\n      <a-form-item label=\"启动时窗口大小\" :label-col=\"labelCol\" :wrapper-col=\"wrapperCol\">\n        <a-input-number v-model=\"config.app.windowSize.width\" :step=\"50\" :min=\"600\" :max=\"2400\" :precision=\"0\" spellcheck=\"false\" />&nbsp;×\n        <a-input-number v-model=\"config.app.windowSize.height\" :step=\"50\" :min=\"500\" :max=\"2000\" :precision=\"0\" spellcheck=\"false\" />\n      </a-form-item>\n      <hr>\n      <a-form-item label=\"自动检查更新\" :label-col=\"labelCol\" :wrapper-col=\"wrapperCol\">\n        <a-radio-group v-model=\"config.app.autoChecked\" default-value=\"light\" button-style=\"solid\">\n          <a-radio-button :value=\"true\">\n            开启\n          </a-radio-button>\n          <a-radio-button :value=\"false\">\n            关闭\n          </a-radio-button>\n        </a-radio-group>\n        <div class=\"form-help\">\n          开启自动检查更新后，每次应用启动时会检查一次更新，如有新版本，则会弹出提示。\n        </div>\n      </a-form-item>\n      <a-form-item label=\"忽略预发布版本\" :label-col=\"labelCol\" :wrapper-col=\"wrapperCol\">\n        <a-radio-group v-model=\"config.app.skipPreRelease\" default-value=\"light\" button-style=\"solid\">\n          <a-radio-button :value=\"true\">\n            忽略\n          </a-radio-button>\n          <a-radio-button :value=\"false\">\n            不忽略\n          </a-radio-button>\n        </a-radio-group>\n        <div class=\"form-help\">\n          预发布版本号为带有 “<code>-</code>” 的版本。注：该配置只对当前版本为正式版本时有效。\n        </div>\n      </a-form-item>\n      <hr>\n      <a-form-item label=\"日志文件保存目录\" :label-col=\"labelCol\" :wrapper-col=\"wrapperCol\">\n        <a-input-search\n          v-model=\"config.app.logFileSavePath\" enter-button=\"选择\"\n          :title=\"config.app.logFileSavePath\" spellcheck=\"false\"\n          @search=\"onLogFileSavePathSelect\"\n        />\n        <div class=\"form-help\">\n          修改后，重启DS才生效！<br>\n          注意：原目录中的文件不会自动转移到新的目录，请自行转移或删除。\n        </div>\n      </a-form-item>\n      <a-form-item label=\"最大日志文件大小\" :label-col=\"labelCol\" :wrapper-col=\"wrapperCol\">\n        <a-input-number ref=\"maxLogFileSize\" v-model=\"config.app.maxLogFileSize\" :step=\"maxLogFileSizeStep\" :min=\"0\" spellcheck=\"false\" />\n        <a-select v-model=\"config.app.maxLogFileSizeUnit\" class=\"ml5\" @change=\"onMaxLogFileSizeUnitChange\">\n          <a-select-option v-for=\"(item) of maxLogFileSizeUnit\" :key=\"item.value\" :value=\"item.value\">\n            {{ item.label }}\n          </a-select-option>\n        </a-select>\n        <div class=\"form-help\">\n          修改后，重启DS才生效！<br>\n          单个日志文件的大小限制，达到限制时会执行备份和清理程序；配置为0时，表示不限制大小。\n        </div>\n      </a-form-item>\n      <a-form-item label=\"保留日志文件数\" :label-col=\"labelCol\" :wrapper-col=\"wrapperCol\">\n        <a-input-number v-model=\"config.app.keepLogFileCount\" :step=\"1\" :min=\"0\" :precision=\"0\" spellcheck=\"false\" />\n        <div class=\"form-help\">\n          修改后，重启DS才生效，<code>隔天</code>或<code>达到日志文件大小限制</code>时，才会触发清理程序！\n        </div>\n      </a-form-item>\n    </div>\n    <template slot=\"footer\">\n      <div class=\"footer-bar\">\n        <a-button :loading=\"removeUserConfigLoading\" class=\"mr10\" icon=\"sync\" @click=\"restoreFactorySettings()\">\n          恢复出厂设置\n        </a-button>\n        <a-button :loading=\"resetDefaultLoading\" class=\"mr10\" icon=\"sync\" @click=\"resetDefault()\">\n          恢复默认\n        </a-button>\n        <a-button :loading=\"applyLoading\" icon=\"check\" type=\"primary\" @click=\"apply()\">\n          应用\n        </a-button>\n      </div>\n    </template>\n  </ds-container>\n</template>\n"
  },
  {
    "path": "packages/gui/src/view/router/index.js",
    "content": "import Index from '../pages/index'\nimport Git from '../pages/plugin/git'\nimport Node from '../pages/plugin/node'\nimport Overwall from '../pages/plugin/overwall'\nimport Pip from '../pages/plugin/pip'\nimport Proxy from '../pages/proxy'\nimport Server from '../pages/server'\nimport Setting from '../pages/setting'\nimport Help from '../pages/help'\n\nconst routes = [\n  { path: '/', redirect: '/index' },\n  { path: '/index', component: Index },\n  { path: '/server', component: Server },\n  { path: '/proxy', component: Proxy },\n  { path: '/setting', component: Setting },\n  { path: '/help', component: Help },\n  { path: '/plugin/node', component: Node },\n  { path: '/plugin/git', component: Git },\n  { path: '/plugin/pip', component: Pip },\n  { path: '/plugin/overwall', component: Overwall },\n]\n\nexport default routes\n"
  },
  {
    "path": "packages/gui/src/view/router/menu.js",
    "content": "export default function createMenus (app) {\n  const plugins = [\n    { title: 'NPM加速', path: '/plugin/node', icon: 'like' },\n    { title: 'Git.exe代理', path: '/plugin/git', icon: 'github' },\n    { title: 'PIP加速', path: '/plugin/pip', icon: 'bulb' },\n  ]\n  const menus = [\n    { title: '首页', path: '/index', icon: 'home' },\n    { title: '加速服务', path: '/server', icon: 'thunderbolt' },\n    { title: '系统代理', path: '/proxy', icon: 'deployment-unit' },\n    { title: '设置', path: '/setting', icon: 'setting' },\n    { title: '帮助中心', path: '/help', icon: 'star' },\n    {\n      title: '应用',\n      path: '/plugin',\n      icon: 'api',\n      children: plugins,\n    },\n  ]\n  if (app.$global && app.$global.setting && app.$global.setting.overwall) {\n    plugins.push({ title: '增强功能', path: '/plugin/overwall', icon: 'global' })\n  }\n  return menus\n}\n"
  },
  {
    "path": "packages/gui/src/view/status.js",
    "content": "import lodash from 'lodash'\nimport Vue from 'vue'\n\nconst status = {\n  server: {\n    enabled: false,\n  },\n  proxy: {\n    enabled: false,\n  },\n  plugin: {\n    node: {},\n  },\n}\nasync function install (api) {\n  api.ipc.on('status', (event, message) => {\n    console.log('view on status', event, message)\n    const value = message.value\n    const key = message.key\n    lodash.set(status, key, value)\n  })\n  const basicStatus = await api.status.get()\n  lodash.merge(status, basicStatus)\n  Vue.prototype.$status = status\n  return status\n}\nexport default {\n  install,\n  status,\n}\n"
  },
  {
    "path": "packages/gui/src/view/style/index.scss",
    "content": ".footer-bar {\n  padding: 10px;\n  text-align: right;\n  border-top: #eee 1px solid;\n}\n\n.flex-l-r {\n  align-content: center;\n  display: flex;\n  justify-content: flex-end;\n  align-items: center;\n\n  & > a {\n    align-content: center;\n    display: flex;\n    align-items: center;\n  }\n}\n\nspan.ant-input {\n  white-space: nowrap;\n  overflow: hidden;\n  vertical-align: middle;\n}\n\n.mr10 {\n  margin-right: 10px;\n}\n\n.mt-1 {\n  margin-top: -1px;\n}\n.mt-2 {\n  margin-top: -2px;\n}\n.mt10 {\n  margin-top: 10px;\n}\n.mt20 {\n  margin-top: 20px;\n}\n\n.ml5 {\n  margin-left: 5px;\n}\n.ml10 {\n  margin-left: 10px;\n}\n\nol {\n  margin-block-start: 0em;\n  margin-block-end: 0em;\n  padding-inline-start: 20px;\n}\n\n.form-help {\n  font-size: 12px;\n  line-height: 15px;\n  color: #a1a1a1;\n\n  i {\n    font-family: 'Microsoft YaHei', serif;\n    font-style: normal;\n    font-weight: bold;\n  }\n\n  code {\n    padding: 0 0.4em;\n  }\n}\n\ncode {\n  font-size: 85%;\n  font-style: normal;\n  border-radius: 6px;\n  color: #888;\n  background-color: #f1f1f1;\n  margin-left: 0.2em;\n  margin-right: 0.2em;\n  padding: 0.2em 0.4em;\n  white-space: break-spaces;\n}\n\n.ace_search_form .ace_searchbtn {\n  width: auto;\n  min-width: 27px;\n}\n\n.ant-radio-button-wrapper {\n  margin-bottom: 3px;\n}\n\n.ant-form-item-control {\n  line-height: 37px;\n}\n\nhr {\n  border-width: 2px 0 0 0;\n  border-style: solid;\n  border-color: #eee;\n  width: 100%;\n}\n\n.ant-modal-content {\n  background-color: #fbfbfb;\n}\n\n.restore-factory-settings {\n  div {\n    padding-left: 1em;\n  }\n  span {\n    display: inline-block;\n    background-color: #eee;\n    padding: 2px 5px;\n    margin: 0 5px 5px 5px;\n  }\n}\n\n.help-list {\n  ul {\n    padding-left: 10px;\n\n    li {\n      list-style: none;\n      line-height: 30px;\n\n      div {\n        white-space: nowrap;\n        overflow: hidden;\n        text-overflow: ellipsis;\n        font-size: 14px;\n      }\n\n      a {\n        color: #3990e0;\n      }\n      a:hover {\n        text-decoration: underline;\n        color: #2c9be5;\n      }\n    }\n\n    // 嵌套列表\n    ul {\n      padding-left: 20px;\n    }\n  }\n\n  ul:first-child li:first-child div:first-child.title1 {\n    margin-top: 0;\n  }\n\n  .title1 {\n    font-size: 18px;\n    font-weight: bold;\n    border-bottom: 1px solid #eee;\n    margin-top: 12px;\n    margin-bottom: 5px;\n    padding-bottom: 5px;\n    padding-left: 5px;\n  }\n\n  .title2 {\n    font-size: 16px;\n    font-weight: bold;\n    margin-top: 10px;\n  }\n\n  .console {\n    font-family: Consolas, arial, serif;\n  }\n}\n"
  },
  {
    "path": "packages/gui/src/view/style/theme/dark.scss",
    "content": "/* 暗色主题 */\n$dark-logo: url('../../../../public/logo/logo-lang-light.svg');\n$dark-bg: #1e1f22; //背景\n$dark-bg-highlight: #333; //高亮块：背景\n$dark-text: #ddd; //字体颜色\n$dark-bd: #333; //边框和分隔线\n$dark-btn: #444; //按钮：边框和背景颜色\n$dark-input: #777; //输入框：背景色\n.theme-dark {\n  hr {\n    border-color: $dark-bd;\n  }\n\n  /* 背景色和字体颜色 */\n  .ds_layout,\n  .ant-layout,\n  .ds-container,\n  .ds-container .container-header,\n  .ant-layout-footer {\n    background-color: $dark-bg;\n    color: $dark-text;\n  }\n  div,\n  span,\n  label {\n    color: $dark-text;\n  }\n  .form-help {\n    color: #a1a1a1;\n  }\n  code {\n    color: #bbb;\n    background-color: #333;\n  }\n\n  /* 高亮块：背景色和字体颜色 */\n  /* 警告类型 */\n  .ant-alert-warning {\n    background-color: $dark-bg-highlight;\n    border-color: $dark-bg-highlight;\n    color: $dark-text;\n    /* 关闭图标颜色 */\n    .ant-alert-close-icon .anticon-close {\n      color: $dark-text;\n    }\n  }\n  /* 消息类型 */\n  .ant-alert-info {\n    background-color: $dark-bg-highlight;\n    border-color: $dark-bg-highlight;\n    color: $dark-text;\n  }\n\n  /* 边框和分隔线 */\n  .ant-layout-has-sider,\n  .ant-layout-sider-children,\n  .ds-container .container-header,\n  .logo,\n  .footer-bar,\n  .ant-layout-footer,\n  .ant-tabs .ant-tabs-left-bar,\n  .ant-tabs .ant-tabs-left-content {\n    border-color: $dark-bd;\n  }\n  .ant-radio-button-wrapper:not(:first-child)::before {\n    background-color: #666;\n  }\n  .ant-divider {\n    background-color: $dark-bd;\n  }\n\n  .help-list .title1 {\n    border-bottom-color: $dark-bd;\n  }\n\n  /* 左侧 */\n  /** 背景色 **/\n  .ant-layout-sider {\n    background-color: $dark-bg;\n  }\n  /** Logo **/\n  .logo {\n    background-image: $dark-logo; /* logo使用亮色的 */\n  }\n  /** 菜单 **/\n  .ant-menu {\n    background-color: $dark-bg;\n    color: $dark-text;\n  }\n  /* 菜单选中时，或鼠标移到菜单上时的样式 */\n  .ant-menu-item:hover,\n  .ant-menu-submenu .ant-menu-submenu-title:hover,\n  .ant-menu:not(.ant-menu-horizontal) .ant-menu-item-selected {\n    background-color: $dark-bg-highlight;\n    color: #1890ff;\n    span {\n      color: #1890ff;\n    }\n  }\n\n  /* 输入框、下拉框 */\n  .ant-input,\n  .ant-input-number-input,\n  .ant-input-number,\n  .ant-select-selection,\n  .ant-input-group-addon {\n    background-color: $dark-input;\n    border-color: #aaa;\n    color: $dark-text;\n    &:hover,\n    &:focus {\n      border-color: #eee;\n    }\n\n    &:focus {\n      box-shadow: 0 0 0 2px rgb(255 255 255 / 35%);\n    }\n  }\n\n  /* 选中的下拉框 */\n  .ant-select-open,\n  .ant-select-focused {\n    .ant-select-selection {\n      box-shadow: 0 0 0 2px rgb(255 255 255 / 35%);\n    }\n  }\n\n  /* 卡片消息：IP测速 */\n  .ant-card {\n    background-color: $dark-input;\n    border-color: $dark-input;\n    .ant-card-head {\n      border-bottom-color: #929292;\n    }\n  }\n\n  /* 标签：未启用 */\n  .ant-tag-red {\n    background-color: #4f4749;\n    border-color: #4f4749;\n    color: #bf8285;\n  }\n  /* 标签：已启用 */\n  .ant-tag-green {\n    background-color: #505f5f;\n    border-color: #505f5f;\n    color: #90cb9f;\n  }\n  /* 标签：警告 */\n  .ant-tag-orange {\n    background-color: #5a5750;\n    border-color: #5a5750;\n    color: #cfa572;\n  }\n  /* 标签：未知 */\n  .ant-tag:not(.ant-tag-red, .ant-tag-green, .ant-tag-orange) {\n    background-color: #5a5a5a;\n    border-color: #5a5a5a;\n    color: #ccc;\n  }\n\n  /* 按钮 */\n  .ant-btn:not(.ant-btn-danger, .ant-btn-primary) {\n    background-color: $dark-btn;\n    border-color: $dark-btn;\n    color: $dark-text;\n    &:hover {\n      opacity: 0.8;\n    }\n  }\n\n  /* 单选框：开关式 */\n  .ant-switch:not(.ant-switch-checked) {\n    background-color: $dark-btn;\n    border-color: $dark-btn;\n    &:hover {\n      opacity: 0.8;\n    }\n  }\n  /* 单选框：按钮式 */\n  .ant-radio-button-wrapper {\n    background-color: $dark-btn;\n    border-color: $dark-btn;\n    color: $dark-text;\n    &:hover {\n      opacity: 0.8;\n    }\n  }\n\n  /* JSON编辑器：应用于拦截设置 */\n  .jsoneditor-vue {\n    /*整个编辑框：背景色和边框*/\n    div.jsoneditor {\n      background-color: $dark-bg-highlight;\n      border: none;\n    }\n    /* 头部菜单栏：边框  */\n    div.jsoneditor-menu {\n      background-color: $dark-bg-highlight;\n      border-color: $dark-bg-highlight;\n    }\n    /* 内容区域左边：行号 */\n    .ace_gutter {\n      background-color: #444;\n      .ace_gutter-cell {\n        color: #aaa;\n      }\n    }\n    /* 内容区域右边：JSON内容 */\n    .ace_scroller {\n      background-color: #555;\n    }\n    /* key的颜色 */\n    .ace_variable,\n    .ace_text-layer {\n      color: #eee;\n    }\n    /* 字符串值的颜色 */\n    .ace_string,\n    .ace_cjk {\n      color: #a6eaa6;\n    }\n    .ace_constant {\n      /* 数字的颜色 */\n      &.ace_numeric {\n        color: #ec9999;\n      }\n      /* 布尔值的颜色 */\n      &.ace_language {\n        color: #f4c995;\n      }\n    }\n    /* 当前行高亮样式 */\n    .ace_gutter-active-line,\n    .ace_marker-layer .ace_active-line {\n      background-color: #838774;\n    }\n    /* 选中行高亮样式 */\n    .ace-jsoneditor {\n      .ace_marker-layer .ace_selection {\n        background-color: #8b2929; /* 同时应用于当前选中的搜索结果项的背景色，建议与搜索结果边框颜色保持一致 */\n      }\n\n      /* 光标颜色 */\n      .ace_cursor {\n        border-left-color: #ddd;\n      }\n    }\n    /* 搜索框 */\n    .ace_button,\n    button,\n    .ace_search_field {\n      color: #000;\n    }\n    /* 搜索结果 */\n    .ace-jsoneditor .ace_marker-layer .ace_selected-word {\n      border-color: #8b2929;\n    }\n  }\n\n  .search-bar {\n    div {\n      color: #000;\n    }\n    span {\n      color: #000;\n    }\n  }\n}\n"
  },
  {
    "path": "packages/gui/vue.config.js",
    "content": "const path = require('node:path')\nconst { defineConfig } = require('@vue/cli-service')\nconst webpack = require('webpack')\n\nconst publishUrl = process.env.VUE_APP_PUBLISH_URL\nconst publishProvider = process.env.VUE_APP_PUBLISH_PROVIDER\nconsole.log('Publish url:', publishUrl)\n\nmodule.exports = defineConfig({\n  pages: {\n    index: {\n      entry: 'src/main.js',\n      title: 'DevSidecar-给开发者的边车辅助工具',\n    },\n  },\n  lintOnSave: false,\n  configureWebpack: {\n    plugins: [\n      new webpack.DefinePlugin({ 'global.GENTLY': true }),\n    ],\n    module: {\n      rules: [\n        {\n          test: /\\.json5$/i,\n          loader: 'json5-loader',\n          options: {\n            esModule: false,\n          },\n          type: 'javascript/auto',\n        },\n      ],\n    },\n  },\n  pluginOptions: {\n    electronBuilder: {\n      mainProcessFile: './src/background.js',\n      // Ref: https://github.com/nklayman/vue-cli-plugin-electron-builder/issues/1891\n      customFileProtocol: './',\n      externals: [\n        '@starknt/sysproxy',\n        '@starknt/sysproxy-win32-ia32-msvc',\n        '@starknt/sysproxy-win32-x64-msvc',\n        '@starknt/sysproxy-win32-arm64-msvc',\n        '@starknt/sysproxy-linux-x64-gnu',\n        '@starknt/sysproxy-linux-arm64-gnu',\n        '@starknt/sysproxy-darwin-x64',\n        '@starknt/sysproxy-darwin-arm64',\n        '@starknt/shutdown-handler-napi',\n        '@starknt/shutdown-handler-napi-win32-ia32-msvc',\n        '@starknt/shutdown-handler-napi-win32-x64-msvc',\n        '@starknt/shutdown-handler-napi-win32-arm64-msvc',\n        '@starknt/shutdown-handler-napi-linux-x64-gnu',\n        '@starknt/shutdown-handler-napi-linux-arm64-gnu',\n        '@starknt/shutdown-handler-napi-darwin-x64',\n        '@starknt/shutdown-handler-napi-darwin-arm64',\n      ],\n      nodeIntegration: true,\n      // Provide an array of files that, when changed, will recompile the main process and restart Electron\n      // Your main process file will be added by default\n      mainProcessWatch: ['src/bridge', 'src/*.js', 'node_modules/dev-sidecar/src'],\n      builderOptions: {\n        afterPack: './pkg/after-pack.js',\n        afterAllArtifactBuild: './pkg/after-all-artifact-build.js',\n        // artifactBuildCompleted: './pkg/artifact-build-completed.js',\n        // builderOptions: {\n        //   publish: ['github']// 此处写入github 就好，不用添加其他内容\n        // },\n        extraResources: [\n          {\n            from: 'extra',\n            to: 'extra',\n          },\n        ],\n        appId: 'dev-sidecar',\n        productName: 'dev-sidecar',\n        // eslint-disable-next-line no-template-curly-in-string\n        artifactName: 'DevSidecar-${version}-${arch}.${ext}',\n        copyright: 'Copyright © 2020-2025 Greper, WangLiang',\n        nsis: {\n          oneClick: false,\n          perMachine: true,\n          allowElevation: true,\n          allowToChangeInstallationDirectory: true,\n        },\n        win: {\n          icon: 'build/icons/',\n          target: [\n            {\n              target: 'nsis',\n              arch: ['x64', 'ia32', 'arm64'],\n            },\n          ],\n          // requestedExecutionLevel: 'highestAvailable', // 加了这个无法开机自启\n        },\n        linux: {\n          icon: 'build/mac/',\n          target: [\n            {\n              target: 'deb',\n              arch: ['x64', 'arm64', 'armv7l'],\n            },\n            {\n              target: 'AppImage',\n              arch: ['x64', 'arm64', 'armv7l'],\n            },\n            {\n              target: 'tar.gz',\n              arch: ['x64', 'arm64', 'armv7l'],\n            },\n          ],\n          category: 'System',\n        },\n        mac: {\n          icon: './build/mac/icon.icns',\n          target: {\n            target: 'dmg',\n            arch: ['x64', 'arm64', 'universal'],\n          },\n          category: 'public.app-category.developer-tools',\n        },\n        publish: {\n          provider: publishProvider,\n          url: publishUrl,\n          // url: 'http://dev-sidecar.docmirror.cn/update/preview/',\n        },\n      },\n      chainWebpackMainProcess (config) {\n        config.entry('mitmproxy').add(path.join(__dirname, 'src/bridge/mitmproxy.js'))\n      },\n    },\n  },\n})\n"
  },
  {
    "path": "packages/mitmproxy/LICENSE",
    "content": "Mozilla Public License Version 2.0\n==================================\n\n1. Definitions\n--------------\n\n1.1. \"Contributor\"\n    means each individual or legal entity that creates, contributes to\n    the creation of, or owns Covered Software.\n\n1.2. \"Contributor Version\"\n    means the combination of the Contributions of others (if any) used\n    by a Contributor and that particular Contributor's Contribution.\n\n1.3. \"Contribution\"\n    means Covered Software of a particular Contributor.\n\n1.4. \"Covered Software\"\n    means Source Code Form to which the initial Contributor has attached\n    the notice in Exhibit A, the Executable Form of such Source Code\n    Form, and Modifications of such Source Code Form, in each case\n    including portions thereof.\n\n1.5. \"Incompatible With Secondary Licenses\"\n    means\n\n    (a) that the initial Contributor has attached the notice described\n        in Exhibit B to the Covered Software; or\n\n    (b) that the Covered Software was made available under the terms of\n        version 1.1 or earlier of the License, but not also under the\n        terms of a Secondary License.\n\n1.6. \"Executable Form\"\n    means any form of the work other than Source Code Form.\n\n1.7. \"Larger Work\"\n    means a work that combines Covered Software with other material, in\n    a separate file or files, that is not Covered Software.\n\n1.8. \"License\"\n    means this document.\n\n1.9. \"Licensable\"\n    means having the right to grant, to the maximum extent possible,\n    whether at the time of the initial grant or subsequently, any and\n    all of the rights conveyed by this License.\n\n1.10. \"Modifications\"\n    means any of the following:\n\n    (a) any file in Source Code Form that results from an addition to,\n        deletion from, or modification of the contents of Covered\n        Software; or\n\n    (b) any new file in Source Code Form that contains any Covered\n        Software.\n\n1.11. \"Patent Claims\" of a Contributor\n    means any patent claim(s), including without limitation, method,\n    process, and apparatus claims, in any patent Licensable by such\n    Contributor that would be infringed, but for the grant of the\n    License, by the making, using, selling, offering for sale, having\n    made, import, or transfer of either its Contributions or its\n    Contributor Version.\n\n1.12. \"Secondary License\"\n    means either the GNU General Public License, Version 2.0, the GNU\n    Lesser General Public License, Version 2.1, the GNU Affero General\n    Public License, Version 3.0, or any later versions of those\n    licenses.\n\n1.13. \"Source Code Form\"\n    means the form of the work preferred for making modifications.\n\n1.14. \"You\" (or \"Your\")\n    means an individual or a legal entity exercising rights under this\n    License. For legal entities, \"You\" includes any entity that\n    controls, is controlled by, or is under common control with You. For\n    purposes of this definition, \"control\" means (a) the power, direct\n    or indirect, to cause the direction or management of such entity,\n    whether by contract or otherwise, or (b) ownership of more than\n    fifty percent (50%) of the outstanding shares or beneficial\n    ownership of such entity.\n\n2. License Grants and Conditions\n--------------------------------\n\n2.1. Grants\n\nEach Contributor hereby grants You a world-wide, royalty-free,\nnon-exclusive license:\n\n(a) under intellectual property rights (other than patent or trademark)\n    Licensable by such Contributor to use, reproduce, make available,\n    modify, display, perform, distribute, and otherwise exploit its\n    Contributions, either on an unmodified basis, with Modifications, or\n    as part of a Larger Work; and\n\n(b) under Patent Claims of such Contributor to make, use, sell, offer\n    for sale, have made, import, and otherwise transfer either its\n    Contributions or its Contributor Version.\n\n2.2. Effective Date\n\nThe licenses granted in Section 2.1 with respect to any Contribution\nbecome effective for each Contribution on the date the Contributor first\ndistributes such Contribution.\n\n2.3. Limitations on Grant Scope\n\nThe licenses granted in this Section 2 are the only rights granted under\nthis License. No additional rights or licenses will be implied from the\ndistribution or licensing of Covered Software under this License.\nNotwithstanding Section 2.1(b) above, no patent license is granted by a\nContributor:\n\n(a) for any code that a Contributor has removed from Covered Software;\n    or\n\n(b) for infringements caused by: (i) Your and any other third party's\n    modifications of Covered Software, or (ii) the combination of its\n    Contributions with other software (except as part of its Contributor\n    Version); or\n\n(c) under Patent Claims infringed by Covered Software in the absence of\n    its Contributions.\n\nThis License does not grant any rights in the trademarks, service marks,\nor logos of any Contributor (except as may be necessary to comply with\nthe notice requirements in Section 3.4).\n\n2.4. Subsequent Licenses\n\nNo Contributor makes additional grants as a result of Your choice to\ndistribute the Covered Software under a subsequent version of this\nLicense (see Section 10.2) or under the terms of a Secondary License (if\npermitted under the terms of Section 3.3).\n\n2.5. Representation\n\nEach Contributor represents that the Contributor believes its\nContributions are its original creation(s) or it has sufficient rights\nto grant the rights to its Contributions conveyed by this License.\n\n2.6. Fair Use\n\nThis License is not intended to limit any rights You have under\napplicable copyright doctrines of fair use, fair dealing, or other\nequivalents.\n\n2.7. Conditions\n\nSections 3.1, 3.2, 3.3, and 3.4 are conditions of the licenses granted\nin Section 2.1.\n\n3. Responsibilities\n-------------------\n\n3.1. Distribution of Source Form\n\nAll distribution of Covered Software in Source Code Form, including any\nModifications that You create or to which You contribute, must be under\nthe terms of this License. You must inform recipients that the Source\nCode Form of the Covered Software is governed by the terms of this\nLicense, and how they can obtain a copy of this License. You may not\nattempt to alter or restrict the recipients' rights in the Source Code\nForm.\n\n3.2. Distribution of Executable Form\n\nIf You distribute Covered Software in Executable Form then:\n\n(a) such Covered Software must also be made available in Source Code\n    Form, as described in Section 3.1, and You must inform recipients of\n    the Executable Form how they can obtain a copy of such Source Code\n    Form by reasonable means in a timely manner, at a charge no more\n    than the cost of distribution to the recipient; and\n\n(b) You may distribute such Executable Form under the terms of this\n    License, or sublicense it under different terms, provided that the\n    license for the Executable Form does not attempt to limit or alter\n    the recipients' rights in the Source Code Form under this License.\n\n3.3. Distribution of a Larger Work\n\nYou may create and distribute a Larger Work under terms of Your choice,\nprovided that You also comply with the requirements of this License for\nthe Covered Software. If the Larger Work is a combination of Covered\nSoftware with a work governed by one or more Secondary Licenses, and the\nCovered Software is not Incompatible With Secondary Licenses, this\nLicense permits You to additionally distribute such Covered Software\nunder the terms of such Secondary License(s), so that the recipient of\nthe Larger Work may, at their option, further distribute the Covered\nSoftware under the terms of either this License or such Secondary\nLicense(s).\n\n3.4. Notices\n\nYou may not remove or alter the substance of any license notices\n(including copyright notices, patent notices, disclaimers of warranty,\nor limitations of liability) contained within the Source Code Form of\nthe Covered Software, except that You may alter any license notices to\nthe extent required to remedy known factual inaccuracies.\n\n3.5. Application of Additional Terms\n\nYou may choose to offer, and to charge a fee for, warranty, support,\nindemnity or liability obligations to one or more recipients of Covered\nSoftware. However, You may do so only on Your own behalf, and not on\nbehalf of any Contributor. You must make it absolutely clear that any\nsuch warranty, support, indemnity, or liability obligation is offered by\nYou alone, and You hereby agree to indemnify every Contributor for any\nliability incurred by such Contributor as a result of warranty, support,\nindemnity or liability terms You offer. You may include additional\ndisclaimers of warranty and limitations of liability specific to any\njurisdiction.\n\n4. Inability to Comply Due to Statute or Regulation\n---------------------------------------------------\n\nIf it is impossible for You to comply with any of the terms of this\nLicense with respect to some or all of the Covered Software due to\nstatute, judicial order, or regulation then You must: (a) comply with\nthe terms of this License to the maximum extent possible; and (b)\ndescribe the limitations and the code they affect. Such description must\nbe placed in a text file included with all distributions of the Covered\nSoftware under this License. Except to the extent prohibited by statute\nor regulation, such description must be sufficiently detailed for a\nrecipient of ordinary skill to be able to understand it.\n\n5. Termination\n--------------\n\n5.1. The rights granted under this License will terminate automatically\nif You fail to comply with any of its terms. However, if You become\ncompliant, then the rights granted under this License from a particular\nContributor are reinstated (a) provisionally, unless and until such\nContributor explicitly and finally terminates Your grants, and (b) on an\nongoing basis, if such Contributor fails to notify You of the\nnon-compliance by some reasonable means prior to 60 days after You have\ncome back into compliance. Moreover, Your grants from a particular\nContributor are reinstated on an ongoing basis if such Contributor\nnotifies You of the non-compliance by some reasonable means, this is the\nfirst time You have received notice of non-compliance with this License\nfrom such Contributor, and You become compliant prior to 30 days after\nYour receipt of the notice.\n\n5.2. If You initiate litigation against any entity by asserting a patent\ninfringement claim (excluding declaratory judgment actions,\ncounter-claims, and cross-claims) alleging that a Contributor Version\ndirectly or indirectly infringes any patent, then the rights granted to\nYou by any and all Contributors for the Covered Software under Section\n2.1 of this License shall terminate.\n\n5.3. In the event of termination under Sections 5.1 or 5.2 above, all\nend user license agreements (excluding distributors and resellers) which\nhave been validly granted by You or Your distributors under this License\nprior to termination shall survive termination.\n\n************************************************************************\n*                                                                      *\n*  6. Disclaimer of Warranty                                           *\n*  -------------------------                                           *\n*                                                                      *\n*  Covered Software is provided under this License on an \"as is\"       *\n*  basis, without warranty of any kind, either expressed, implied, or  *\n*  statutory, including, without limitation, warranties that the       *\n*  Covered Software is free of defects, merchantable, fit for a        *\n*  particular purpose or non-infringing. The entire risk as to the     *\n*  quality and performance of the Covered Software is with You.        *\n*  Should any Covered Software prove defective in any respect, You     *\n*  (not any Contributor) assume the cost of any necessary servicing,   *\n*  repair, or correction. This disclaimer of warranty constitutes an   *\n*  essential part of this License. No use of any Covered Software is   *\n*  authorized under this License except under this disclaimer.         *\n*                                                                      *\n************************************************************************\n\n************************************************************************\n*                                                                      *\n*  7. Limitation of Liability                                          *\n*  --------------------------                                          *\n*                                                                      *\n*  Under no circumstances and under no legal theory, whether tort      *\n*  (including negligence), contract, or otherwise, shall any           *\n*  Contributor, or anyone who distributes Covered Software as          *\n*  permitted above, be liable to You for any direct, indirect,         *\n*  special, incidental, or consequential damages of any character      *\n*  including, without limitation, damages for lost profits, loss of    *\n*  goodwill, work stoppage, computer failure or malfunction, or any    *\n*  and all other commercial damages or losses, even if such party      *\n*  shall have been informed of the possibility of such damages. This   *\n*  limitation of liability shall not apply to liability for death or   *\n*  personal injury resulting from such party's negligence to the       *\n*  extent applicable law prohibits such limitation. Some               *\n*  jurisdictions do not allow the exclusion or limitation of           *\n*  incidental or consequential damages, so this exclusion and          *\n*  limitation may not apply to You.                                    *\n*                                                                      *\n************************************************************************\n\n8. Litigation\n-------------\n\nAny litigation relating to this License may be brought only in the\ncourts of a jurisdiction where the defendant maintains its principal\nplace of business and such litigation shall be governed by laws of that\njurisdiction, without reference to its conflict-of-law provisions.\nNothing in this Section shall prevent a party's ability to bring\ncross-claims or counter-claims.\n\n9. Miscellaneous\n----------------\n\nThis License represents the complete agreement concerning the subject\nmatter hereof. If any provision of this License is held to be\nunenforceable, such provision shall be reformed only to the extent\nnecessary to make it enforceable. Any law or regulation which provides\nthat the language of a contract shall be construed against the drafter\nshall not be used to construe this License against a Contributor.\n\n10. Versions of the License\n---------------------------\n\n10.1. New Versions\n\nMozilla Foundation is the license steward. Except as provided in Section\n10.3, no one other than the license steward has the right to modify or\npublish new versions of this License. Each version will be given a\ndistinguishing version number.\n\n10.2. Effect of New Versions\n\nYou may distribute the Covered Software under the terms of the version\nof the License under which You originally received the Covered Software,\nor under the terms of any subsequent version published by the license\nsteward.\n\n10.3. Modified Versions\n\nIf you create software not governed by this License, and you want to\ncreate a new license for such software, you may create and use a\nmodified version of this License if you rename the license and remove\nany references to the name of the license steward (except to note that\nsuch modified license differs from this License).\n\n10.4. Distributing Source Code Form that is Incompatible With Secondary\nLicenses\n\nIf You choose to distribute Source Code Form that is Incompatible With\nSecondary Licenses under the terms of this version of the License, the\nnotice described in Exhibit B of this License must be attached.\n\nExhibit A - Source Code Form License Notice\n-------------------------------------------\n\n  This Source Code Form is subject to the terms of the Mozilla Public\n  License, v. 2.0. If a copy of the MPL was not distributed with this\n  file, You can obtain one at http://mozilla.org/MPL/2.0/.\n\nIf it is not possible or desirable to put the notice in a particular\nfile, then You may include the notice in a location (such as a LICENSE\nfile in a relevant directory) where a recipient would be likely to look\nfor such a notice.\n\nYou may add additional accurate notices of copyright ownership.\n\nExhibit B - \"Incompatible With Secondary Licenses\" Notice\n---------------------------------------------------------\n\n  This Source Code Form is \"Incompatible With Secondary Licenses\", as\n  defined by the Mozilla Public License, v. 2.0.\n"
  },
  {
    "path": "packages/mitmproxy/index.js",
    "content": "module.exports = require('./src')\n"
  },
  {
    "path": "packages/mitmproxy/package.json",
    "content": "{\n  \"name\": \"@docmirror/mitmproxy\",\n  \"version\": \"2.0.1\",\n  \"private\": false,\n  \"description\": \"\",\n  \"author\": \"docmirror.cn\",\n  \"license\": \"MPL-2.0\",\n  \"keywords\": [\n    \"dev-sidecar\"\n  ],\n  \"main\": \"src/index.js\",\n  \"scripts\": {\n    \"test\": \"mocha\"\n  },\n  \"dependencies\": {\n    \"@docmirror/dev-sidecar\": \"workspace:*\",\n    \"agentkeepalive\": \"^4.5.0\",\n    \"axios\": \"^1.7.7\",\n    \"baidu-aip-sdk\": \"^4.16.16\",\n    \"dns-over-http\": \"^0.2.0\",\n    \"is-browser\": \"^2.1.0\",\n    \"json5\": \"^2.2.3\",\n    \"lodash\": \"^4.17.21\",\n    \"lru-cache\": \"^7.15.0\",\n    \"node-forge\": \"^1.3.1\",\n    \"stream-throttle\": \"^0.1.3\",\n    \"through2\": \"^4.0.2\",\n    \"tunnel-agent\": \"^0.6.0\"\n  }\n}\n"
  },
  {
    "path": "packages/mitmproxy/src/index.js",
    "content": "const mitmproxy = require('./lib/proxy')\nconst proxyConfig = require('./lib/proxy/common/config')\nconst speedTest = require('./lib/speed/index.js')\nconst ProxyOptions = require('./options')\nconst log = require('./utils/util.log.server')\nconst { fireError, fireStatus } = require('./utils/util.process')\n\nlet servers = []\n\nconst api = {\n  async start (config) {\n    const proxyOptions = ProxyOptions(config)\n    const setting = config.setting\n    if (setting) {\n      if (setting.userBasePath) {\n        proxyConfig.setDefaultCABasePath(setting.userBasePath)\n      }\n    }\n\n    if (proxyOptions.setting && proxyOptions.setting.NODE_TLS_REJECT_UNAUTHORIZED === false) {\n      process.env.NODE_TLS_REJECT_UNAUTHORIZED = '0'\n    } else {\n      process.env.NODE_TLS_REJECT_UNAUTHORIZED = '1'\n    }\n    // log.info('启动代理服务时的配置:', JSON.stringify(proxyOptions, null, '\\t'))\n    const newServers = mitmproxy.createProxy(proxyOptions, (server, port, host, ssl) => {\n      fireStatus(true)\n      log.info(`代理服务已启动：${host}:${port}, ssl: ${ssl}`)\n    })\n    for (const newServer of newServers) {\n      newServer.on('close', () => {\n        log.info('server will closed ')\n        if (servers.includes(newServer)) {\n          servers = servers.filter(item => item !== newServer)\n          if (servers.length === 0) {\n            fireStatus(false)\n          }\n        }\n      })\n      newServer.on('error', (e) => {\n        log.error('server error', e)\n        // newServer = null\n        fireError(e)\n      })\n    }\n    servers = newServers\n\n    registerProcessListener()\n  },\n  async close () {\n    return new Promise((resolve, reject) => {\n      if (servers && servers.length > 0) {\n        for (const server of servers) {\n          server.close((err) => {\n            if (err && err.code !== 'ERR_SERVER_NOT_RUNNING') {\n              if (err.code === 'ERR_SERVER_NOT_RUNNING') {\n                log.info('代理服务未运行，无需关闭')\n                resolve()\n              } else {\n                log.error('代理服务关闭失败:', err)\n                reject(err)\n              }\n              return\n            }\n\n            log.info('代理服务关闭成功')\n            resolve()\n          })\n        }\n        servers = []\n      } else {\n        log.info('server is null, no need to close.')\n        fireStatus(false)\n        resolve()\n      }\n    })\n  },\n}\n\nfunction registerProcessListener () {\n  process.on('message', (msg) => {\n    log.info('child get msg:', JSON.stringify(msg))\n    if (msg.type === 'action') {\n      api[msg.event.key](msg.event.params)\n    } else if (msg.type === 'speed') {\n      speedTest.action(msg.event)\n    }\n  })\n\n  process.on('SIGINT', () => {\n    log.info('on sigint : closed ')\n    process.exit(0)\n  })\n\n  // 避免异常崩溃\n  process.on('uncaughtException', (err) => {\n    if (err.code === 'ECONNABORTED') {\n      //  log.error(err.errno)\n      return\n    }\n    log.error('Process uncaughtException:', err)\n  })\n\n  process.on('unhandledRejection', (err, p) => {\n    log.info('Process unhandledRejection at: Promise', p, 'err:', err)\n    // application specific logging, throwing an error, or other logic here\n  })\n  process.on('uncaughtExceptionMonitor', (err, origin) => {\n    log.info('Process uncaughtExceptionMonitor:', err, origin)\n  })\n  process.on('exit', (code, signal) => {\n    log.info('代理服务进程被关闭:', code, signal)\n  })\n  process.on('beforeExit', (code, signal) => {\n    log.info('Process beforeExit event with code: ', code, signal)\n  })\n  process.on('SIGPIPE', (code, signal) => {\n    log.warn('sub Process SIGPIPE', code, signal)\n  })\n}\n\nmodule.exports = {\n  ...api,\n  config: proxyConfig,\n  log,\n  speedTest,\n}\n"
  },
  {
    "path": "packages/mitmproxy/src/json.js",
    "content": "const logOrConsole = require('@docmirror/dev-sidecar/src/utils/util.log-or-console')\nlet JSON5 = require('json5')\nif (JSON5.default) {\n  JSON5 = JSON5.default\n}\n\nmodule.exports = {\n  parse (str, defaultValue) {\n    if (str == null || str.length < 2) {\n      return defaultValue || {}\n    }\n\n    str = str.toString()\n\n    if (defaultValue != null) {\n      try {\n        return JSON5.parse(str)\n      } catch (e) {\n        logOrConsole.error(`JSON5解析失败: ${e.message}，JSON内容:\\r\\n`, str)\n        return defaultValue\n      }\n    } else {\n      return JSON5.parse(str)\n    }\n  },\n  stringify (obj) {\n    return JSON.stringify(obj, null, '\\t')\n  },\n\n  // 仅用于记录日志时使用\n  stringify2 (obj) {\n    try {\n      return JSON.stringify(obj)\n    } catch {\n      try {\n        return JSON5.stringify(obj)\n      } catch {\n        return obj\n      }\n    }\n  },\n}\n"
  },
  {
    "path": "packages/mitmproxy/src/lib/choice/RequestCounter.js",
    "content": "const { ChoiceCache } = require('./index')\n\nmodule.exports = new ChoiceCache()\n"
  },
  {
    "path": "packages/mitmproxy/src/lib/choice/index.js",
    "content": "const LRUCache = require('lru-cache')\nconst log = require('../../utils/util.log.server')\n\nconst cacheSize = 1024\n\nclass ChoiceCache {\n  constructor () {\n    this.cache = new LRUCache({\n      maxSize: cacheSize,\n      sizeCalculation: () => {\n        return 1\n      },\n    })\n  }\n\n  get (key) {\n    return this.cache.get(key)\n  }\n\n  getOrCreate (key, backupList) {\n    log.info('get counter:', key)\n    let item = this.cache.get(key)\n    if (item == null) {\n      item = new DynamicChoice(key)\n      item.setBackupList(backupList)\n      this.cache.set(key, item)\n    }\n    return item\n  }\n}\n\nclass DynamicChoice {\n  constructor (key) {\n    this.key = key\n    this.countMap = {} /* ip -> count { value, total, error, keepErrorCount, successRate }  */\n    this.value = null // 当前使用的host\n    this.backupList = [] // 备选host列表\n    this.createTime = new Date()\n  }\n\n  doRank () {\n    // 将count里面根据成功率排序\n    const countList = []\n    for (const key in this.countMap) {\n      countList.push(this.countMap[key])\n    }\n\n    // 将countList根据成功率排序\n    countList.sort((a, b) => {\n      return b.successRate - a.successRate\n    })\n\n    log.info('Do rank:', JSON.stringify(countList))\n\n    const newBackupList = countList.map(item => item.value)\n    this.setBackupList(newBackupList)\n  }\n\n  /**\n   * 设置新的backup列表\n   * @param newBackupList 新的backupList\n   */\n  setBackupList (newBackupList) {\n    this.backupList = newBackupList\n    let defaultTotal = newBackupList.length\n    for (const ip of newBackupList) {\n      if (!this.countMap[ip]) {\n        this.countMap[ip] = { value: ip, total: defaultTotal, error: 0, keepErrorCount: 0, successRate: 0.5 }\n        defaultTotal--\n      }\n    }\n    this.value = newBackupList.shift()\n    this.doCount(this.value, false)\n  }\n\n  countStart (value) {\n    this.doCount(value, false)\n  }\n\n  /**\n   * 换下一个\n   * @param count 计数器\n   */\n  changeNext (count) {\n    log.info('切换backup', count, this.backupList)\n    count.keepErrorCount = 0 // 清空连续失败\n    count.total = 0\n    count.error = 0\n\n    const valueBackup = this.value\n    if (this.backupList.length > 0) {\n      this.value = this.backupList.shift()\n      log.info(`切换backup完成: ${this.key}, ip: ${valueBackup} ➜ ${this.value}, this:`, this)\n    } else {\n      this.value = null\n      log.info(`切换backup完成: ${this.key}, backupList为空了，设置this.value: from '${valueBackup}' to null. this:`, this)\n    }\n  }\n\n  /**\n   * 记录使用次数或错误次数\n   * @param ip\n   * @param isError\n   */\n  doCount (ip, isError) {\n    let count = this.countMap[ip]\n    if (count == null) {\n      count = this.countMap[ip] = { value: ip, total: 5, error: 0, keepErrorCount: 0, successRate: 1 }\n    }\n\n    if (isError) {\n      // 失败次数+1，累计连续失败次数+1\n      count.error++\n      count.keepErrorCount++\n    } else {\n      // 总次数+1\n      count.total++\n    }\n    // 计算成功率\n    count.successRate = 1.0 - (count.error / count.total)\n    if (isError && this.value === ip) {\n      // 连续错误3次，切换下一个\n      if (count.keepErrorCount >= 3) {\n        this.changeNext(count)\n      }\n      // 成功率小于40%,切换下一个\n      if (count.successRate < 0.4) {\n        this.changeNext(count)\n      }\n    }\n  }\n}\n\nmodule.exports = {\n  DynamicChoice,\n  ChoiceCache,\n}\n"
  },
  {
    "path": "packages/mitmproxy/src/lib/dns/base.js",
    "content": "const LRUCache = require('lru-cache')\nconst log = require('../../utils/util.log.server')\nconst matchUtil = require('../../utils/util.match')\nconst { DynamicChoice } = require('../choice/index')\n\nfunction mapToList (ipMap) {\n  const ipList = []\n  for (const key in ipMap) {\n    const value = ipMap[key]\n    if (value && value !== 'false' && value !== '0') { // 配置为 ture 时才生效\n      ipList.push(key)\n    }\n  }\n  return ipList\n}\n\nconst defaultCacheSize = 1024\n\nclass IpCache extends DynamicChoice {\n  constructor (hostname) {\n    super(hostname)\n    this.lookupCount = 0\n  }\n\n  /**\n   * 设置新的ipList\n   *\n   * @param newBackupList\n   */\n  setBackupList (newBackupList) {\n    super.setBackupList(newBackupList)\n    this.lookupCount++\n  }\n}\n\nmodule.exports = class BaseDNS {\n  constructor (dnsName, dnsType, cacheSize, preSetIpList) {\n    this.dnsName = dnsName\n    this.dnsType = dnsType\n    this.preSetIpList = preSetIpList\n    this.cache = new LRUCache({\n      maxSize: (cacheSize > 0 ? cacheSize : defaultCacheSize),\n      sizeCalculation: () => {\n        return 1\n      },\n    })\n  }\n\n  count (hostname, ip, isError = true) {\n    const ipCache = this.cache.get(hostname)\n    if (ipCache) {\n      ipCache.doCount(ip, isError)\n    }\n  }\n\n  async lookup (hostname, ipChecker) {\n    try {\n      let ipCache = this.cache.get(hostname)\n      if (ipCache) {\n        const ip = ipCache.value\n        if (ip != null) {\n          if (ipChecker && ipChecker(ip)) {\n            ipCache.doCount(ip, false)\n            return ip\n          } else {\n            return hostname\n          }\n        }\n      } else {\n        ipCache = new IpCache(hostname)\n        this.cache.set(hostname, ipCache)\n      }\n\n      const t = Date.now()\n      let ipList = await this._lookupWithPreSetIpList(hostname)\n      if (ipList == null) {\n        // 没有获取到ipv4地址\n        ipList = []\n      }\n      ipList.push(hostname) // 把原域名加入到统计里去\n\n      ipCache.setBackupList(ipList)\n\n      const ip = ipCache.value\n      log.info(`[DNS-over-${this.dnsType} '${this.dnsName}'] ${hostname} ➜ ${ip} (${Date.now() - t} ms), ipList: ${JSON.stringify(ipList)}, ipCache:`, JSON.stringify(ipCache))\n\n      if (ipChecker) {\n        if (ip != null && ip !== hostname && ipChecker(ip)) {\n          return ip\n        }\n\n        for (const ip of ipList) {\n          if (ip !== hostname && ipChecker(ip)) {\n            return ip\n          }\n        }\n      }\n\n      return ip != null ? ip : hostname\n    } catch (error) {\n      log.error(`[DNS-over-${this.dnsType} '${this.dnsName}'] cannot resolve hostname ${hostname}, error:`, error)\n      return hostname\n    }\n  }\n\n  async _lookupWithPreSetIpList (hostname) {\n    if (this.preSetIpList) {\n      // 获取当前域名的预设IP列表\n      let hostnamePreSetIpList = matchUtil.matchHostname(this.preSetIpList, hostname, `matched preSetIpList(${this.dnsName})`)\n      if (hostnamePreSetIpList && (hostnamePreSetIpList.length > 0 || hostnamePreSetIpList.length === undefined)) {\n        if (hostnamePreSetIpList.length > 0) {\n          hostnamePreSetIpList = hostnamePreSetIpList.slice() // 复制一份列表数据，避免配置数据被覆盖\n        } else {\n          hostnamePreSetIpList = mapToList(hostnamePreSetIpList)\n        }\n\n        if (hostnamePreSetIpList.length > 0) {\n          hostnamePreSetIpList.isPreSet = true\n          log.info(`[DNS-over-PreSet '${this.dnsName}'] 获取到该域名的预设IP列表： ${hostname} - ${JSON.stringify(hostnamePreSetIpList)}`)\n          return hostnamePreSetIpList\n        }\n      }\n    }\n\n    return await this._lookup(hostname)\n  }\n\n  async _lookup (hostname) {\n    const start = Date.now()\n\n    let response\n    try {\n      // 执行DNS查询\n      log.debug(`[DNS-over-${this.dnsType} '${this.dnsName}'] query start: ${hostname}`)\n      response = await this._doDnsQuery(hostname, 'A', start)\n    } catch {\n      // 异常日志在 _doDnsQuery已经打印过，这里就不再打印了\n      return []\n    }\n\n    try {\n      const cost = Date.now() - start\n      log.debug(`[DNS-over-${this.dnsType} '${this.dnsName}'] query end: ${hostname}, cost: ${cost} ms, response:`, response)\n\n      if (response == null || response.answers == null || response.answers.length == null || response.answers.length === 0) {\n        log.warn(`[DNS-over-${this.dnsType} '${this.dnsName}'] 没有该域名的IP地址: ${hostname}, cost: ${cost} ms, response:`, response)\n        return []\n      }\n\n      const ret = response.answers.filter(item => item.type === 'A').map(item => item.data)\n      if (ret.length === 0) {\n        log.info(`[DNS-over-${this.dnsType} '${this.dnsName}'] 没有该域名的IP地址: ${hostname}, cost: ${cost} ms`)\n      } else {\n        log.info(`[DNS-over-${this.dnsType} '${this.dnsName}'] 获取到该域名的IP地址： ${hostname} - ${JSON.stringify(ret)}, cost: ${cost} ms`)\n      }\n\n      return ret\n    } catch (e) {\n      log.error(`[DNS-over-${this.dnsType} '${this.dnsName}'] 解读响应失败，response:`, response, ', error:', e)\n      return []\n    }\n  }\n\n  _doDnsQuery (hostname, type = 'A', start) {\n    if (start == null) {\n      start = Date.now()\n    }\n\n    return new Promise((resolve, reject) => {\n      // 设置超时任务\n      let isOver = false\n      const timeout = 8000\n      const timeoutId = setTimeout(() => {\n        if (!isOver) {\n          log.error(`[DNS-over-${this.dnsType} '${this.dnsName}'] DNS查询超时, hostname: ${hostname}, sni: ${this.dnsServerName || '无'}, type: ${type}${this.dnsServer ? `, dnsServer: ${this.dnsServer}` : ''}${this.dnsServerPort ? `:${this.dnsServerPort}` : ''}, cost: ${Date.now() - start} ms`)\n          reject(new Error('DNS查询超时'))\n        }\n      }, timeout)\n\n      try {\n        this._dnsQueryPromise(hostname, type)\n          .then((response) => {\n            isOver = true\n            clearTimeout(timeoutId)\n            resolve(response)\n          })\n          .catch((e) => {\n            isOver = true\n            clearTimeout(timeoutId)\n            if (e.message === 'DNS查询超时') {\n              log.error(`[DNS-over-${this.dnsType} '${this.dnsName}'] DNS查询超时. hostname: ${hostname}, sni: ${this.dnsServerName || '无'}, type: ${type}${this.dnsServer ? `, dnsServer: ${this.dnsServer}` : ''}${this.dnsServerPort ? `:${this.dnsServerPort}` : ''}, cost: ${Date.now() - start} ms`)\n            } else {\n              log.error(`[DNS-over-${this.dnsType} '${this.dnsName}'] DNS查询错误, hostname: ${hostname}, sni: ${this.dnsServerName || '无'}, type: ${type}${this.dnsServer ? `, dnsServer: ${this.dnsServer}` : ''}${this.dnsServerPort ? `:${this.dnsServerPort}` : ''}, cost: ${Date.now() - start} ms, error:`, e)\n            }\n            reject(e)\n          })\n      } catch (e) {\n        isOver = true\n        clearTimeout(timeoutId)\n        log.error(`[DNS-over-${this.dnsType} '${this.dnsName}'] DNS查询异常, hostname: ${hostname}, type: ${type}${this.dnsServer ? `, dnsServer: ${this.dnsServer}` : ''}${this.dnsServerPort ? `:${this.dnsServerPort}` : ''}, cost: ${Date.now() - start} ms, error:`, e)\n        reject(e)\n      }\n    })\n  }\n}\n"
  },
  {
    "path": "packages/mitmproxy/src/lib/dns/https.js",
    "content": "const { promisify } = require('node:util')\nconst doh = require('dns-over-http')\nconst BaseDNS = require('./base')\nconst HttpsAgent = require('../proxy/common/ProxyHttpsAgent')\nconst Agent = require('../proxy/common/ProxyHttpAgent')\n\nconst dohQueryAsync = promisify(doh.query)\n\nfunction createAgent (dnsServer) {\n  return new (dnsServer.startsWith('https:') ? HttpsAgent : Agent)({\n    keepAlive: true,\n    timeout: 4000,\n  })\n}\n\nmodule.exports = class DNSOverHTTPS extends BaseDNS {\n  constructor (dnsName, cacheSize, preSetIpList, dnsServer, dnsServerName) {\n    super(dnsName, 'HTTPS', cacheSize, preSetIpList)\n    this.dnsServer = dnsServer.replace(/\\s+/, '')\n    this.dnsServerName = dnsServerName\n  }\n\n  _dnsQueryPromise (hostname, type = 'A') {\n    // 请求参数\n    const options = {\n      url: this.dnsServer,\n      agent: createAgent(this.dnsServer),\n    }\n    if (this.dnsServerName) {\n      // 设置SNI\n      options.servername = this.dnsServerName\n      options.rejectUnauthorized = false\n    }\n\n    // DNS查询参数\n    const questions = [\n      {\n        type,\n        name: hostname,\n      },\n    ]\n\n    return dohQueryAsync(options, questions)\n  }\n}\n"
  },
  {
    "path": "packages/mitmproxy/src/lib/dns/index.js",
    "content": "const matchUtil = require('../../utils/util.match')\nconst log = require('../../utils/util.log.server')\nconst DNSOverPreSetIpList = require('./preset.js')\nconst DNSOverHTTPS = require('./https.js')\nconst DNSOverTLS = require('./tls.js')\nconst DNSOverTCP = require('./tcp.js')\nconst DNSOverUDP = require('./udp.js')\n\nmodule.exports = {\n  initDNS (dnsProviders, preSetIpList) {\n    const dnsMap = {}\n\n    // 创建普通的DNS\n    for (const provider in dnsProviders) {\n      const conf = dnsProviders[provider]\n\n      // 获取DNS服务器\n      let server = conf.server || conf.host\n      if (server != null) {\n        server = server.replace(/\\s+/, '')\n      }\n      if (!server) {\n        continue\n      }\n\n      // 获取DNS类型\n      let type = conf.type\n      if (type == null) {\n        if (server.startsWith('https://') || server.startsWith('http://')) {\n          type = 'https'\n        } else if (server.startsWith('tls://') || server.startsWith('dot://')) {\n          type = 'tls'\n        } else if (server.startsWith('tcp://')) {\n          type = 'tcp'\n        } else if (server.includes('://') && !server.startsWith('udp://')) {\n          throw new Error(`Unknown type DNS: ${server}, provider: ${provider}`)\n        } else {\n          type = 'udp'\n        }\n      } else {\n        type = type.replace(/\\s+/, '').toLowerCase()\n      }\n\n      // 创建DNS对象\n      if (type === 'https' || type === 'doh' || type === 'dns-over-https') {\n        if (!server.includes('/')) {\n          server = `https://${server}/dns-query`\n        }\n\n        // 基于 https\n        dnsMap[provider] = new DNSOverHTTPS(provider, conf.cacheSize, preSetIpList, server, conf.sni || conf.servername)\n      } else {\n        // 获取DNS端口\n        let port = conf.port\n\n        // 处理带协议的DNS服务地址\n        if (server.includes('://')) {\n          server = server.split('://')[1]\n        }\n        // 处理带端口的DNS服务地址\n        if (port == null && server.includes(':')) {\n          [server, port] = server.split(':')\n        }\n\n        if (type === 'tls' || type === 'dot' || type === 'dns-over-tls') {\n          // 基于 tls\n          dnsMap[provider] = new DNSOverTLS(provider, conf.cacheSize, preSetIpList, server, port, conf.sni || conf.servername)\n        } else if (type === 'tcp') {\n          // 基于 tcp\n          dnsMap[provider] = new DNSOverTCP(provider, conf.cacheSize, preSetIpList, server, port)\n        } else {\n          // 基于 udp\n          dnsMap[provider] = new DNSOverUDP(provider, conf.cacheSize, preSetIpList, server, port)\n        }\n      }\n\n      if (conf.forSNI || conf.forSni) {\n        dnsMap.ForSNI = dnsMap[provider]\n      }\n    }\n\n    // 创建预设IP的DNS\n    dnsMap.PreSet = new DNSOverPreSetIpList(preSetIpList)\n    if (dnsMap.ForSNI == null) {\n      dnsMap.ForSNI = dnsMap.PreSet\n    }\n\n    log.info(`设置SNI默认使用的DNS为 '${dnsMap.ForSNI.dnsName}'（注：当某个域名配置了SNI但未配置DNS时，将默认使用该DNS）`)\n\n    return dnsMap\n  },\n  hasDnsLookup (dnsConfig, hostname) {\n    // 先匹配 预设IP配置\n    const hostnamePreSetIpList = matchUtil.matchHostname(dnsConfig.preSetIpList, hostname, 'matched preSetIpList(hasDnsLookup)')\n    if (hostnamePreSetIpList) {\n      return dnsConfig.dnsMap.PreSet\n    }\n\n    // 再匹配 DNS映射配置\n    const providerName = matchUtil.matchHostname(dnsConfig.mapping, hostname, 'get dns providerName')\n\n    // 由于DNS中的usa已重命名为cloudflare，所以做以下处理，为了向下兼容\n    if (providerName === 'usa' && dnsConfig.dnsMap.usa == null && dnsConfig.dnsMap.cloudflare != null) {\n      return dnsConfig.dnsMap.cloudflare\n    }\n\n    if (providerName) {\n      return dnsConfig.dnsMap[providerName]\n    }\n  },\n}\n"
  },
  {
    "path": "packages/mitmproxy/src/lib/dns/preset.js",
    "content": "const BaseDNS = require('./base')\n\nmodule.exports = class DNSOverPreSetIpList extends BaseDNS {\n  constructor (preSetIpList) {\n    super('PreSet', 'PreSet', null, preSetIpList)\n  }\n\n  async _lookup (_hostname) {\n    return []\n  }\n}\n"
  },
  {
    "path": "packages/mitmproxy/src/lib/dns/tcp.js",
    "content": "const net = require('node:net')\nconst { Buffer } = require('node:buffer')\nconst dnsPacket = require('dns-packet')\nconst randi = require('random-int')\nconst BaseDNS = require('./base')\n\nconst defaultPort = 53 // TCP类型的DNS服务默认端口号\n\nmodule.exports = class DNSOverTCP extends BaseDNS {\n  constructor (dnsName, cacheSize, preSetIpList, dnsServer, dnsServerPort) {\n    super(dnsName, 'TCP', cacheSize, preSetIpList)\n    this.dnsServer = dnsServer.replace(/\\s+/, '')\n    this.dnsServerPort = Number.parseInt(dnsServerPort) || defaultPort\n  }\n\n  _dnsQueryPromise (hostname, type = 'A') {\n    return new Promise((resolve, reject) => {\n      // 构造 DNS 查询报文\n      const packet = dnsPacket.encode({\n        flags: dnsPacket.RECURSION_DESIRED,\n        type: 'query',\n        id: randi(0x0, 0xFFFF),\n        questions: [{\n          type,\n          name: hostname,\n        }],\n      })\n\n      // --- TCP 查询 ---\n      const tcpClient = net.createConnection({\n        host: this.dnsServer,\n        port: this.dnsServerPort,\n      }, () => {\n        // TCP DNS 报文前需添加 2 字节长度头\n        const lengthBuffer = Buffer.alloc(2)\n        lengthBuffer.writeUInt16BE(packet.length)\n        tcpClient.write(Buffer.concat([lengthBuffer, packet]))\n      })\n\n      tcpClient.once('data', (data) => {\n        const length = data.readUInt16BE(0)\n        const response = dnsPacket.decode(data.subarray(2, 2 + length))\n        resolve(response)\n        tcpClient.end()\n      })\n\n      tcpClient.once('error', (err) => {\n        reject(err)\n        tcpClient.end()\n      })\n    })\n  }\n}\n"
  },
  {
    "path": "packages/mitmproxy/src/lib/dns/tls.js",
    "content": "const dnstls = require('./util/dns-over-tls')\nconst BaseDNS = require('./base')\n\nconst defaultPort = 853\n\nmodule.exports = class DNSOverTLS extends BaseDNS {\n  constructor (dnsName, cacheSize, preSetIpList, dnsServer, dnsServerPort, dnsServerName) {\n    super(dnsName, 'TLS', cacheSize, preSetIpList)\n    this.dnsServer = dnsServer.replace(/\\s+/, '')\n    this.dnsServerPort = Number.parseInt(dnsServerPort) || defaultPort\n    this.dnsServerName = dnsServerName\n  }\n\n  _dnsQueryPromise (hostname, type = 'A') {\n    const options = {\n      host: this.dnsServer,\n      port: this.dnsServerPort,\n      servername: this.dnsServerName || this.dnsServer,\n      rejectUnauthorized: !this.dnsServerName,\n\n      name: hostname,\n      klass: 'IN',\n      type,\n\n      timeout: 4000,\n    }\n\n    return dnstls.query(options)\n  }\n}\n"
  },
  {
    "path": "packages/mitmproxy/src/lib/dns/udp.js",
    "content": "const dgram = require('node:dgram')\nconst dnsPacket = require('dns-packet')\nconst randi = require('random-int')\nconst BaseDNS = require('./base')\n\nconst defaultPort = 53 // UDP类型的DNS服务默认端口号\n\nmodule.exports = class DNSOverUDP extends BaseDNS {\n  constructor (dnsName, cacheSize, preSetIpList, dnsServer, dnsServerPort) {\n    super(dnsName, 'UDP', cacheSize, preSetIpList)\n    this.dnsServer = dnsServer.replace(/\\s+/, '')\n    this.dnsServerPort = Number.parseInt(dnsServerPort) || defaultPort\n\n    this.isIPv6 = dnsServer.includes(':') && dnsServer.includes('[') && dnsServer.includes(']')\n    this.socketType = this.isIPv6 ? 'udp6' : 'udp4'\n  }\n\n  _dnsQueryPromise (hostname, type = 'A') {\n    return new Promise((resolve, reject) => {\n      let isOver = false\n      const timeout = 5000\n      let timeoutId = null\n\n      // 构造 DNS 查询报文\n      const packet = dnsPacket.encode({\n        flags: dnsPacket.RECURSION_DESIRED,\n        type: 'query',\n        id: randi(0x0, 0xFFFF),\n        questions: [{\n          type,\n          name: hostname,\n        }],\n      })\n\n      // 创建客户端\n      const udpClient = dgram.createSocket(this.socketType, (msg, _rinfo) => {\n        isOver = true\n        clearTimeout(timeoutId)\n\n        const response = dnsPacket.decode(msg)\n        resolve(response)\n        udpClient.close()\n      })\n\n      // 发送 UDP 查询\n      udpClient.send(packet, 0, packet.length, this.dnsServerPort, this.dnsServer, (err, _bytes) => {\n        if (err) {\n          isOver = true\n          clearTimeout(timeoutId)\n          reject(err)\n          udpClient.close()\n        }\n      })\n\n      // 设置超时任务\n      timeoutId = setTimeout(() => {\n        if (!isOver) {\n          reject(new Error('DNS查询超时'))\n          udpClient.close()\n        }\n      }, timeout)\n    })\n  }\n}\n"
  },
  {
    "path": "packages/mitmproxy/src/lib/dns/util/dns-over-tls.js",
    "content": "/**\n * 由于组件 `dns-over-tls@0.0.9` 不支持 `rejectUnauthorized` 和 `timeout` 两个参数，所以将源码复制过来，并简化了代码。\n */\nconst dnsPacket = require('dns-packet')\nconst tls_1 = require('node:tls')\nconst randi = require('random-int')\n\nconst TWO_BYTES = 2\n\nfunction getDnsQuery ({ type, name, klass, id }) {\n  return {\n    id,\n    type: 'query',\n    flags: dnsPacket.RECURSION_DESIRED,\n    questions: [{ class: klass, name, type }],\n  }\n}\n\nfunction query ({ host, servername, type, name, klass, port, rejectUnauthorized, timeout }) {\n  return new Promise((resolve, reject) => {\n    if (!host || !servername || !name) {\n      throw new Error('At least host, servername and name must be set.')\n    }\n\n    let response = Buffer.alloc(0)\n    let packetLength = 0\n    const dnsQuery = getDnsQuery({ id: randi(0x0, 0xFFFF), type, name, klass })\n    const dnsQueryBuf = dnsPacket.streamEncode(dnsQuery)\n    const socket = tls_1.connect({ host, port, servername, rejectUnauthorized, timeout })\n\n    // 超时处理\n    let isFinished = false\n    let interval\n    if (timeout > 0) {\n      interval = setInterval(() => {\n        if (!isFinished) {\n          socket.destroy((...args) => {\n            console.info('socket destory callback args:', args)\n          })\n\n          reject(new Error('DNS查询超时'))\n        }\n      }, timeout)\n    }\n\n    socket.on('secureConnect', () => socket.write(dnsQueryBuf))\n    socket.on('data', (data) => {\n      if (timeout) {\n        isFinished = true\n        clearInterval(interval)\n      }\n\n      if (response.length === 0) {\n        packetLength = data.readUInt16BE(0)\n        if (packetLength < 12) {\n          reject(new Error('Below DNS minimum packet length (DNS Header is 12 bytes)'))\n        }\n        response = Buffer.from(data)\n      } else {\n        response = Buffer.concat([response, data])\n      }\n\n      if (response.length === packetLength + TWO_BYTES) {\n        socket.destroy()\n        resolve(dnsPacket.streamDecode(response))\n      } else {\n        reject(new Error('响应长度不正确'))\n      }\n    })\n    socket.on('error', (err) => {\n      if (timeout) {\n        isFinished = true\n        clearInterval(interval)\n      }\n      reject(err)\n    })\n  })\n}\n\nexports.query = query\nexports.default = { query }\n"
  },
  {
    "path": "packages/mitmproxy/src/lib/interceptor/impl/req/OPTIONS.js",
    "content": "const defaultAllowHeaders = '*'\nconst defaultAllowMethods = 'GET,POST,PUT,DELETE,HEAD,OPTIONS,PATCH' // CONNECT、TRACE被认为是不安全的请求，通常不建议允许跨域\n\nfunction readConfig (config, defaultConfig) {\n  if (config) {\n    if (Object.isArray(config)) {\n      config = config.join(',')\n    }\n  } else {\n    config = defaultConfig\n  }\n  return config\n}\n\nmodule.exports = {\n  name: 'options',\n  priority: 101,\n  requestIntercept (context, interceptOpt, req, res, ssl, next) {\n    const { rOptions, log } = context\n\n    // 不是 OPTIONS 请求，或请求头中不含 origin 时，跳过当前拦截器\n    if (rOptions.method !== 'OPTIONS' || rOptions.headers.origin == null) {\n      return\n    }\n\n    // 从请求头中获取跨域相关信息；如果不存在，则从配置中获取的值；如果还不存在，则使用默认值\n    const allowHeaders = rOptions.headers['access-control-request-headers'] || readConfig(interceptOpt.optionsAllowHeaders, defaultAllowHeaders)\n    const allowMethods = rOptions.headers['access-control-request-method'] || readConfig(interceptOpt.optionsAllowMethods, defaultAllowMethods)\n\n    const headers = {\n      // 允许跨域\n      'DS-Interceptor': 'options',\n      'Access-Control-Allow-Origin': rOptions.headers.origin,\n      'Access-Control-Allow-Headers': allowHeaders,\n      'Access-Control-Allow-Methods': allowMethods,\n      'Access-Control-Max-Age': interceptOpt.optionsMaxAge > 0 ? interceptOpt.optionsMaxAge : 2592000, // 默认有效一个月\n      'Date': new Date().toUTCString(),\n    }\n\n    // 判断是否允许\n    if (interceptOpt.optionsCredentials !== false && interceptOpt.optionsCredentials !== 'false') {\n      headers['Access-Control-Allow-Credentials'] = 'true'\n    }\n\n    res.writeHead(200, headers)\n    res.end()\n\n    log.info('options intercept:', (rOptions.original || rOptions).url)\n    return true // true代表请求结束\n  },\n  is (interceptOpt) {\n    return !!interceptOpt.options\n  },\n}\n"
  },
  {
    "path": "packages/mitmproxy/src/lib/interceptor/impl/req/abort.js",
    "content": "module.exports = {\n  name: 'abort',\n  priority: 103,\n  requestIntercept (context, interceptOpt, req, res, ssl, next) {\n    const { rOptions, log } = context\n\n    if (interceptOpt.abort === true || interceptOpt.abort === 'true') {\n      const headers = {\n        'Content-Type': 'text/plain; charset=utf-8',\n        'DS-Interceptor': 'abort',\n      }\n\n      // headers.Access-Control-Allow-*：避免跨域问题\n      if (rOptions.headers.origin) {\n        headers['Access-Control-Allow-Credentials'] = 'true'\n        headers['Access-Control-Allow-Origin'] = rOptions.headers.origin\n      }\n\n      res.writeHead(403, headers)\n      res.write(\n        'DevSidecar 403: Request abort.\\n\\n'\n        + '  This request is matched by abort intercept.\\n\\n'\n        + '  因配置abort拦截器，本请求直接返回403禁止访问。',\n      )\n      res.end()\n\n      const url = `${rOptions.method} ➜ ${rOptions.protocol}//${rOptions.hostname}:${rOptions.port}${req.url}`\n      log.info('abort intercept:', url)\n      return true // true代表请求结束\n    } else {\n      const response = interceptOpt.abort\n\n      // status\n      const status = response.status || 403\n      response.status = status\n\n      // body\n      const body = response.html || response.json || response.script || response.css || response.text || response.body\n        || `DevSidecar ${status}: Request abort.\\n\\n`\n        + '  This request is matched by abort intercept.\\n\\n'\n        + `  因配置abort拦截器，本请求直接返回${status}禁止访问。`\n\n      // headers\n      const headers = response.headers || {}\n      response.headers = headers\n      headers['DS-Interceptor'] = 'abort'\n      // headers.Content-Type\n      if (status !== 204) {\n        // （1）如果没有Content-Type，根据response的内容自动设置\n        if (!headers['Content-Type']) {\n          if (response.html != null) {\n            headers['Content-Type'] = 'text/html'\n          } else if (response.json != null) {\n            headers['Content-Type'] = 'application/json'\n          } else if (response.script != null) {\n            headers['Content-Type'] = 'application/javascript'\n          } else if (response.css != null) {\n            headers['Content-Type'] = 'text/css'\n          } else {\n            headers['Content-Type'] = 'text/plain'\n          }\n        }\n        // （2）如果Content-Type没有charset，自动设置为utf-8\n        if (headers['Content-Type'] != null && !headers['Content-Type'].includes('charset')) {\n          headers['Content-Type'] += '; charset=utf-8'\n        }\n      }\n      // headers.Access-Control-Allow-*：避免跨域问题\n      if (rOptions.headers.origin && !headers['Access-Control-Allow-Origin']) {\n        headers['Access-Control-Allow-Credentials'] = 'true'\n        headers['Access-Control-Allow-Origin'] = rOptions.headers.origin\n      }\n\n      res.writeHead(status, headers)\n      if (status !== 204) {\n        res.write(body)\n      }\n      res.end()\n\n      const url = `${rOptions.method} ➜ ${rOptions.protocol}//${rOptions.hostname}:${rOptions.port}${req.url}`\n      log.info('abort intercept:', url, ', response:', JSON.stringify(response))\n      return true // true代表请求结束\n    }\n  },\n  is (interceptOpt) {\n    return !!interceptOpt.abort\n  },\n}\n"
  },
  {
    "path": "packages/mitmproxy/src/lib/interceptor/impl/req/baiduOcr.js",
    "content": "function getTomorrow () {\n  const now = new Date()\n  const tomorrow = new Date(now)\n\n  // 设置日期为明天\n  tomorrow.setDate(now.getDate() + 1)\n  // 重置时间为凌晨 0 点 0 分 0 秒\n  tomorrow.setHours(0, 0, 0, 0)\n\n  return tomorrow.getTime()\n}\n// function getNextMonth () {\n//   const now = new Date()\n//   const currentYear = now.getFullYear()\n//   const currentMonth = now.getMonth()\n//\n//   // 如果当前月份是12月，年份增加1，并且月份设为0（1月）\n//   const nextMonth = (currentMonth + 1) % 12\n//   const nextYear = nextMonth === 0 ? currentYear + 1 : currentYear\n//\n//   return new Date(nextYear, nextMonth, 1, 0, 0, 0, 0).getTime()\n// }\n\nconst AipOcrClient = require('baidu-aip-sdk').ocr\n\nconst AipOcrClientMap = {}\nconst apis = [\n  'accurateBasic', // 调用通用文字识别（高精度版）\n  'accurate', // 调用通用文字识别（含位置高精度版）\n  'handwriting', // 手写文字识别\n]\nconst limitMap = {}\n\nfunction createBaiduOcrClient (config) {\n  const key = config.id\n  if (AipOcrClientMap[key]) {\n    return AipOcrClientMap[key]\n  }\n  const client = new AipOcrClient(config.id, config.ak, config.sk)\n  AipOcrClientMap[key] = client\n  return client\n}\n\nlet count = 0\n\nfunction getConfig (interceptOpt, tryCount, log) {\n  tryCount = tryCount || 1\n\n  let config\n  if (typeof (interceptOpt.baiduOcr) && interceptOpt.baiduOcr.length > 0) {\n    config = interceptOpt.baiduOcr[count++ % interceptOpt.baiduOcr.length]\n\n    if (tryCount < interceptOpt.baiduOcr.length) {\n      if (!config || !config.id || !config.ak || !config.sk) {\n        return getConfig(interceptOpt, tryCount + 1, log) // 递归找到有效的配置\n      }\n    }\n\n    // 避免count值过大，造成问题\n    if (count >= 100000) {\n      count = 0\n    }\n  } else {\n    config = interceptOpt.baiduOcr\n    tryCount = null // 将tryCount设置为null代表只有一个配置\n  }\n\n  if (!config || !config.id || !config.ak || !config.sk) {\n    return null // 没有配置或配置错误，直接返回null\n  }\n\n  // 获取当前配置可用的API\n  for (let i = 0; i < apis.length; i++) {\n    const api = apis[i]\n    if (!checkIsLimitConfig(config.id, api)) {\n      config.api = api\n      break\n    }\n    log.warn(`百度云账号 ${config.id} 的接口 ${api} 已超出限额`)\n  }\n\n  // 如果当前配置的所有API均不可用，则返回null\n  if (config.api == null) {\n    if (tryCount == null) {\n      return null // 只配置了一个账号，没有更多账号可以选择了，直接返回null\n    } else {\n      if (tryCount < interceptOpt.baiduOcr.length) {\n        // 递归找到有效的配置\n        return getConfig(interceptOpt, tryCount + 1, log)\n      } else {\n        return null\n      }\n    }\n  }\n\n  return config\n}\n\nfunction limitConfig (id, api) {\n  const key = `${id}_${api}`\n  limitMap[key] = getTomorrow()\n  // limitMap[key] = Date.now() + 5000 // 测试用，5秒后解禁\n}\n\nfunction checkIsLimitConfig (id, api) {\n  const key = `${id}_${api}`\n  const limitTime = limitMap[key]\n  return limitTime && limitTime > Date.now()\n}\n\nmodule.exports = {\n  name: 'baiduOcr',\n  priority: 131,\n  requestIntercept (context, interceptOpt, req, res, ssl, next) {\n    const { rOptions, log } = context\n\n    const headers = {\n      'Content-Type': 'application/json; charset=utf-8',\n      'Access-Control-Allow-Origin': '*',\n    }\n\n    // 获取配置\n    const config = getConfig(interceptOpt, null, log)\n    if (!config) {\n      res.writeHead(200, headers)\n      res.write('{\"error_code\": 99917, \"error_msg\": \"dev-sidecar中，未配置百度云账号，或所有百度云账号的免费额度都已用完！！！\"}')\n      res.end()\n      return true\n    }\n    if (!config.id || !config.ak || !config.sk) {\n      res.writeHead(200, headers)\n      res.write('{\"error_code\": 999500, \"error_msg\": \"dev-sidecar中，baiduOcr的 id 或 ak 或 sk 配置为空\"}')\n      res.end()\n      return true\n    }\n\n    headers['DS-Interceptor'] = `baiduOcr: id=${config.id}, api=${config.api || apis[0]}, account=${config.account}`\n\n    // 获取图片的base64编码\n    let imageBase64 = rOptions.path.substring(rOptions.path.indexOf('?') + 1)\n    if (!imageBase64) {\n      res.writeHead(200, headers)\n      res.write('{\"error_code\": 999400, \"error_msg\": \"图片Base64参数为空\"}')\n      res.end()\n      return true\n    }\n    imageBase64 = decodeURIComponent(imageBase64)\n\n    // 调用百度云 “文字识别” 相关接口，根据 `config.api` 调用不同的接口\n    const client = createBaiduOcrClient(config)\n    const options = {\n      recognize_granularity: 'big',\n      detect_direction: 'false',\n      paragraph: 'false',\n      probability: 'false',\n      ...(config.options || {}),\n    }\n    log.info('发起百度ocr请求', req.hostname)\n    client[config.api || apis[0]](imageBase64, options).then((result) => {\n      if (result.error_code != null) {\n        log.error('baiduOcr error:', result)\n        if (result.error_code === 17) {\n          // 当前百度云账号，达到当日调用次数上限\n          limitConfig(config.id, config.api)\n          log.error(`当前百度云账号的接口 ${config.api}，已达到当日调用次数上限，暂时禁用它，明天会自动放开:`, config)\n        }\n      } else {\n        log.info('baiduOcr success:', result)\n      }\n\n      res.writeHead(200, headers)\n      res.write(JSON.stringify(result)) // 格式如：{\"words_result\":[{\"words\":\"6525\"}],\"words_result_num\":1,\"log_id\":1818877093747960000}\n      res.end()\n      if (next) {\n        next() // 异步执行完继续next\n      }\n    }).catch((err) => {\n      log.error('baiduOcr error:', err)\n      res.writeHead(200, headers)\n      res.write(`{\"error_code\": 999500, \"error_msg\": \"${err}\"}`) // 格式如：{\"words_result\":[{\"words\":\"6525\"}],\"words_result_num\":1,\"log_id\":1818877093747960000}\n      res.end()\n      if (next) {\n        next() // 异步执行完继续next\n      }\n    })\n\n    log.info('proxy baiduOcr: hostname:', req.hostname)\n\n    return 'no-next'\n  },\n  is (interceptOpt) {\n    return !!interceptOpt.baiduOcr\n  },\n}\n"
  },
  {
    "path": "packages/mitmproxy/src/lib/interceptor/impl/req/cacheRequest.js",
    "content": "function getMaxAge (interceptOpt) {\n  // 秒\n  if (interceptOpt.cacheSeconds > 0 || interceptOpt.cacheMaxAge > 0 || interceptOpt.cache > 0) {\n    return interceptOpt.cacheSeconds || interceptOpt.cacheMaxAge || interceptOpt.cache\n  }\n  // 分钟\n  if (interceptOpt.cacheMinutes > 0) {\n    return interceptOpt.cacheMinutes * 60 // 60：1分钟\n  }\n  // 小时\n  if (interceptOpt.cacheHours > 0) {\n    return interceptOpt.cacheHours * 3600 // 60 * 60 一小时\n  }\n  // 天\n  if (interceptOpt.cacheDays > 0) {\n    return interceptOpt.cacheDays * 86400 // 60 * 60 * 24 一天\n  }\n  // 星期\n  if (interceptOpt.cacheWeeks > 0) {\n    return interceptOpt.cacheWeeks * 604800 // 60 * 60 * 24 * 7 一周\n  }\n  // 月\n  if (interceptOpt.cacheMonths > 0) {\n    return interceptOpt.cacheMonths * 2592000 // 60 * 60 * 24 * 30 一个月\n  }\n  // 年\n  if (interceptOpt.cacheYears > 0) {\n    return interceptOpt.cacheYears * 31536000 // 60 * 60 * 24 * 365 一年\n  }\n\n  return null\n}\n\n// 获取 lastModifiedTime 的方法\nfunction getLastModifiedTimeFromIfModifiedSince (rOptions, log) {\n  // 获取 If-Modified-Since 和 If-None-Match 用于判断是否命中缓存\n  const lastModified = rOptions.headers['if-modified-since']\n  if (lastModified == null || lastModified.length === 0) {\n    return null // 没有lastModified，返回null\n  }\n\n  try {\n    // 尝试解析 lastModified，并获取time\n    return new Date(lastModified).getTime()\n  } catch (e) {\n    // 为数字时，直接返回\n    if (/\\\\d+/.test(lastModified)) {\n      return lastModified - 0\n    }\n\n    log.warn(`cache intercept: 解析 if-modified-since 失败: '${lastModified}', error:`, e)\n  }\n\n  return null\n}\n\nmodule.exports = {\n  name: 'cacheRequest',\n  priority: 104,\n  requestIntercept (context, interceptOpt, req, res, ssl, next) {\n    const { rOptions, log } = context\n\n    if (rOptions.method !== 'GET') {\n      return // 非GET请求，不拦截\n    }\n\n    // 获取 Cache-Control 用于判断是否禁用缓存\n    const cacheControl = rOptions.headers['cache-control']\n    if (cacheControl && (cacheControl.includes('no-cache') || cacheControl.includes('no-store'))) {\n      return // 当前请求指定要禁用缓存，跳过当前拦截器\n    }\n    // 获取 Pragma 用于判断是否禁用缓存\n    const pragma = rOptions.headers.pragma\n    if (pragma && (pragma.includes('no-cache') || pragma.includes('no-store'))) {\n      return // 当前请求指定要禁用缓存，跳过当前拦截器\n    }\n\n    // 最近编辑时间\n    const lastModifiedTime = getLastModifiedTimeFromIfModifiedSince(rOptions, log)\n    if (lastModifiedTime == null) {\n      return // 没有 lastModified，不拦截\n    }\n\n    // 获取maxAge配置\n    const maxAge = getMaxAge(interceptOpt)\n    // 判断缓存是否已过期\n    const passTime = Date.now() - lastModifiedTime\n    if (passTime > maxAge * 1000) {\n      return // 缓存已过期，不拦截\n    }\n\n    // 缓存未过期，直接拦截请求并响应304\n    res.writeHead(304, {\n      'DS-Interceptor': `cache: ${maxAge}`,\n    })\n    res.end()\n\n    const url = `${rOptions.method} ➜ ${rOptions.protocol}//${rOptions.hostname}:${rOptions.port}${req.url}`\n    log.info('cache intercept:', url)\n    return true\n  },\n  is (interceptOpt) {\n    const maxAge = getMaxAge(interceptOpt)\n    return maxAge != null && maxAge > 0\n  },\n  getMaxAge,\n}\n"
  },
  {
    "path": "packages/mitmproxy/src/lib/interceptor/impl/req/proxy.js",
    "content": "const url = require('node:url')\nconst lodash = require('lodash')\n\nfunction replacePlaceholder0 (url, matched, pre) {\n  if (matched) {\n    for (let i = 0; i < matched.length; i++) {\n      url = url.replace(`\\${${pre}[${i}]}`, matched[i] || '')\n    }\n    if (matched.groups) {\n      for (const key in matched.groups) {\n        url = url.replace(`\\${${key}}`, matched.groups[key] || '')\n      }\n    }\n  }\n  return url\n}\n\n// 替换占位符\nfunction replacePlaceholder (url, rOptions, pathMatched, hostnameMatched) {\n  if (url.includes('${')) {\n    // eslint-disable-next-line no-template-curly-in-string\n    url = url.replace('${host}', rOptions.hostname)\n\n    if (url.includes('${')) {\n      url = replacePlaceholder0(url, pathMatched, 'p')\n      url = replacePlaceholder0(url, hostnameMatched, 'h')\n    }\n\n    // 移除多余的占位符\n    if (url.includes('${')) {\n      url = url.replace(/\\$\\{[^}]+\\}/g, '')\n    }\n  }\n\n  return url\n}\n\nfunction buildTargetUrl (rOptions, urlConf, interceptOpt, matched, hostnameMatched) {\n  let targetUrl\n  if (interceptOpt && interceptOpt.replace) {\n    const regexp = new RegExp(interceptOpt.replace)\n    targetUrl = rOptions.path.replace(regexp, urlConf)\n  } else if (urlConf.indexOf('http:') === 0 || urlConf.indexOf('https:') === 0) {\n    targetUrl = urlConf\n  } else {\n    let uri = rOptions.path\n    if (uri.indexOf('http:') === 0 || uri.indexOf('https:') === 0) {\n      // eslint-disable-next-line node/no-deprecated-api\n      const URL = url.parse(uri)\n      uri = URL.path\n    }\n    targetUrl = urlConf + uri\n  }\n\n  // 替换占位符\n  targetUrl = replacePlaceholder(targetUrl, rOptions, matched, hostnameMatched)\n\n  // 拼接协议\n  targetUrl = targetUrl.indexOf('http:') === 0 || targetUrl.indexOf('https:') === 0 ? targetUrl : `${rOptions.protocol}//${targetUrl}`\n\n  return targetUrl\n}\n\nfunction doProxy (proxyConf, rOptions, req, interceptOpt, matched, hostnameMatched) {\n  // 获取代理目标地址\n  const proxyTarget = buildTargetUrl(rOptions, proxyConf, interceptOpt, matched, hostnameMatched)\n\n  // 替换rOptions的属性\n  // eslint-disable-next-line node/no-deprecated-api\n  const URL = url.parse(proxyTarget)\n  rOptions.origional = lodash.cloneDeep(rOptions) // 备份原始请求参数\n  delete rOptions.origional.agent\n  delete rOptions.origional.headers\n  rOptions.protocol = URL.protocol\n  rOptions.hostname = URL.host\n  rOptions.host = URL.host\n  rOptions.headers.host = URL.host\n  rOptions.path = URL.path\n  if (URL.port == null) {\n    rOptions.port = rOptions.protocol === 'https:' ? 443 : 80\n  }\n\n  return proxyTarget\n}\n\nmodule.exports = {\n  name: 'proxy',\n  priority: 121,\n  replacePlaceholder,\n  buildTargetUrl,\n  doProxy,\n  requestIntercept (context, interceptOpt, req, res, ssl, next, matched, hostnameMatched) {\n    const { rOptions, log, RequestCounter } = context\n\n    const originHostname = rOptions.hostname\n\n    let proxyConf = interceptOpt.proxy\n    if (RequestCounter && interceptOpt.backup && interceptOpt.backup.length > 0) {\n      // 优选逻辑\n      const backupList = [proxyConf]\n      for (const bk of interceptOpt.backup) {\n        backupList.push(bk)\n      }\n      const key = `${rOptions.hostname}/${interceptOpt.key}`\n      const count = RequestCounter.getOrCreate(key, backupList)\n      if (count.value == null) {\n        count.doRank()\n      }\n      if (count.value == null) {\n        log.error('`count.value` is null, the count:', count)\n      } else {\n        count.doCount(count.value)\n        proxyConf = count.value\n        context.requestCount = {\n          key,\n          value: count.value,\n          count,\n        }\n      }\n    }\n\n    // 替换 rOptions 中的地址，并返回代理目标地址\n    const proxyTarget = doProxy(proxyConf, rOptions, req, interceptOpt, matched, hostnameMatched)\n\n    if (context.requestCount) {\n      log.info('proxy choice:', JSON.stringify(context.requestCount))\n    }\n\n    if (interceptOpt.sni) {\n      let unVerifySsl = rOptions.agent.options.rejectUnauthorized === false\n\n      rOptions.servername = interceptOpt.sni\n      if (rOptions.agent.options.rejectUnauthorized && rOptions.agent.unVerifySslAgent) {\n        // rOptions.agent.options.rejectUnauthorized = false // 不能直接在agent上进行修改属性值，因为它采用了单例模式，所有请求共用这个对象的\n        rOptions.agent = rOptions.agent.unVerifySslAgent\n        unVerifySsl = true\n      }\n\n      const unVerifySslStr = unVerifySsl ? ', unVerifySsl' : ''\n      res.setHeader('DS-Interceptor', `proxy: ${proxyTarget}, sni: ${interceptOpt.sni}${unVerifySslStr}`)\n      log.info(`proxy intercept: hostname: ${originHostname}, target: ${proxyTarget}, sni replace servername: ${rOptions.servername}${unVerifySslStr}`)\n    } else if (interceptOpt.unVerifySsl === true) {\n      if (rOptions.agent.options.rejectUnauthorized && rOptions.agent.unVerifySslAgent) {\n        rOptions.agent = rOptions.agent.unVerifySslAgent\n        res.setHeader('DS-Interceptor', `proxy: ${proxyTarget}, unVerifySsl`)\n        log.info(`proxy intercept: hostname: ${originHostname}, target: ${proxyTarget}, unVerifySsl`)\n      } else {\n        res.setHeader('DS-Interceptor', `proxy: ${proxyTarget}, already unVerifySsl`)\n        log.info(`proxy intercept: hostname: ${originHostname}, target: ${proxyTarget}, already unVerifySsl`)\n      }\n    } else {\n      res.setHeader('DS-Interceptor', `proxy: ${proxyTarget}`)\n      log.info(`proxy intercept: hostname: ${originHostname}, target：${proxyTarget}`)\n    }\n\n    return true\n  },\n  is (interceptOpt) {\n    return !!interceptOpt.proxy\n  },\n}\n"
  },
  {
    "path": "packages/mitmproxy/src/lib/interceptor/impl/req/redirect.js",
    "content": "const proxyApi = require('./proxy')\n\nmodule.exports = {\n  name: 'redirect',\n  priority: 105,\n  requestIntercept (context, interceptOpt, req, res, ssl, next, matched, hostnameMatched) {\n    const { rOptions, log } = context\n\n    // 获取重定向目标地址\n    const redirect = proxyApi.buildTargetUrl(rOptions, interceptOpt.redirect, interceptOpt, matched, hostnameMatched)\n\n    const headers = {\n      'Location': redirect,\n      'DS-Interceptor': 'redirect',\n    }\n\n    // headers.Access-Control-Allow-*：避免跨域问题\n    if (rOptions.headers.origin) {\n      headers['Access-Control-Allow-Credentials'] = 'true'\n      headers['Access-Control-Allow-Origin'] = rOptions.headers.origin\n    }\n\n    res.writeHead(302, headers)\n    res.end()\n\n    const url = `${rOptions.method} ➜ ${rOptions.protocol}//${rOptions.hostname}:${rOptions.port}${req.url}`\n    log.info(`redirect intercept: ${url} ➜ ${redirect}`)\n    return true // true代表请求结束\n  },\n  is (interceptOpt) {\n    return interceptOpt.redirect // 如果配置中有redirect，那么这个配置是需要redirect拦截的\n  },\n}\n"
  },
  {
    "path": "packages/mitmproxy/src/lib/interceptor/impl/req/requestReplace.js",
    "content": "const REMOVE = '[remove]'\n\nfunction replaceRequestHeaders (rOptions, headers, log) {\n  for (const key in headers) {\n    let value = headers[key]\n    if (value === REMOVE) {\n      value = null\n    }\n\n    if (value) {\n      log.debug(`[DS-RequestReplace-Interceptor] replace '${key}': '${rOptions.headers[key.toLowerCase()]}' -> '${value}'`)\n      rOptions.headers[key.toLowerCase()] = value\n    } else if (rOptions.headers[key.toLowerCase()]) {\n      log.debug(`[DS-RequestReplace-Interceptor] remove '${key}': '${rOptions.headers[key.toLowerCase()]}'`)\n      delete rOptions.headers[key.toLowerCase()]\n    }\n  }\n\n  log.debug(`[DS-RequestReplace-Interceptor] 最终headers: \\r\\n${JSON.stringify(rOptions.headers, null, '\\t')}`)\n}\n\nmodule.exports = {\n  name: 'requestReplace',\n  priority: 111,\n  requestIntercept (context, interceptOpt, req, res, ssl, next) {\n    const { rOptions, log } = context\n\n    const requestReplaceConfig = interceptOpt.requestReplace\n\n    let actions = ''\n\n    // 替换请求头\n    if (requestReplaceConfig.headers) {\n      replaceRequestHeaders(rOptions, requestReplaceConfig.headers, log)\n      actions += `${actions ? ',' : ''}headers`\n    }\n\n    // 替换下载文件请求的请求地址（此功能主要是为了方便拦截配置）\n    // 注：要转换为下载请求，需要 responseReplace 拦截器的配合使用。\n    if (requestReplaceConfig.doDownload && rOptions.path.match(/DS_DOWNLOAD/i)) {\n      rOptions.doDownload = true\n      rOptions.path = rOptions.path.replace(/[?&/]?DS_DOWNLOAD(=[^?&/]+)?$/gi, '')\n      actions += `${actions ? ',' : ''}path:remove-DS_DOWNLOAD`\n    }\n\n    res.setHeader('DS-RequestReplace-Interceptor', actions)\n\n    const url = `${rOptions.method} ➜ ${rOptions.protocol}//${rOptions.hostname}:${rOptions.port}${req.url}`\n    log.info('requestReplace intercept:', url)\n  },\n  is (interceptOpt) {\n    return !!interceptOpt.requestReplace\n  },\n}\n"
  },
  {
    "path": "packages/mitmproxy/src/lib/interceptor/impl/req/sni.js",
    "content": "module.exports = {\n  name: 'sni',\n  priority: 123,\n  requestIntercept (context, interceptOpt, req, res, ssl, next) {\n    const { rOptions, log } = context\n\n    let unVerifySsl = rOptions.agent.options.rejectUnauthorized === false\n\n    rOptions.servername = interceptOpt.sni\n    if (rOptions.agent.options.rejectUnauthorized && rOptions.agent.unVerifySslAgent) {\n      // rOptions.agent.options.rejectUnauthorized = false // 不能直接在agent上进行修改属性值，因为它采用了单例模式，所有请求共用这个对象的\n      rOptions.agent = rOptions.agent.unVerifySslAgent\n      unVerifySsl = true\n    }\n\n    const unVerifySslStr = unVerifySsl ? ', unVerifySsl' : ''\n    res.setHeader('DS-Interceptor', `sni: ${interceptOpt.sni}${unVerifySslStr}`)\n\n    log.info(`sni intercept: sni replace servername: ${rOptions.hostname} ➜ ${rOptions.servername}${unVerifySslStr}`)\n    return true\n  },\n  is (interceptOpt) {\n    return !!interceptOpt.sni && !interceptOpt.proxy // proxy生效时，sni不需要生效，因为proxy中也会使用sni覆盖 rOptions.servername\n  },\n}\n"
  },
  {
    "path": "packages/mitmproxy/src/lib/interceptor/impl/req/success.js",
    "content": "module.exports = {\n  name: 'success',\n  priority: 102,\n  requestIntercept (context, interceptOpt, req, res, ssl, next) {\n    const { rOptions, log } = context\n\n    if (interceptOpt.success === true || interceptOpt.success === 'true') {\n      const headers = {\n        'Content-Type': 'text/plain; charset=utf-8',\n        'DS-Interceptor': 'success',\n      }\n\n      // headers.Access-Control-Allow-*：避免跨域问题\n      if (rOptions.headers.origin) {\n        headers['Access-Control-Allow-Credentials'] = 'true'\n        headers['Access-Control-Allow-Origin'] = rOptions.headers.origin\n      }\n\n      res.writeHead(200, headers)\n      res.write(\n        'DevSidecar 200: Request success.\\n\\n'\n        + '  This request is matched by success intercept.\\n\\n'\n        + '  因配置success拦截器，本请求直接返回200成功。',\n      )\n      res.end()\n\n      const url = `${rOptions.method} ➜ ${rOptions.protocol}//${rOptions.hostname}:${rOptions.port}${req.url}`\n      log.info('success intercept:', url)\n      return true // true代表请求结束\n    } else {\n      const response = interceptOpt.success\n\n      // status\n      const status = response.status || 200\n      response.status = status\n\n      // body\n      const body = response.html || response.json || response.script || response.css || response.text || response.body\n        || `DevSidecar ${status}: Request success.\\n\\n`\n        + '  This request is matched by success intercept.\\n\\n'\n        + `  因配置success拦截器，本请求直接返回${status}成功。`\n\n      // headers\n      const headers = response.headers || {}\n      response.headers = headers\n      headers['DS-Interceptor'] = 'success'\n      // headers.Content-Type\n      if (status !== 204) {\n        // （1）如果没有Content-Type，根据response的内容自动设置\n        if (!headers['Content-Type']) {\n          if (response.html != null) {\n            headers['Content-Type'] = 'text/html'\n          } else if (response.json != null) {\n            headers['Content-Type'] = 'application/json'\n          } else if (response.script != null) {\n            headers['Content-Type'] = 'application/javascript'\n          } else if (response.css != null) {\n            headers['Content-Type'] = 'text/css'\n          } else {\n            headers['Content-Type'] = 'text/plain'\n          }\n        }\n        // （2）如果Content-Type没有charset，自动设置为utf-8\n        if (headers['Content-Type'] != null && !headers['Content-Type'].includes('charset')) {\n          headers['Content-Type'] += '; charset=utf-8'\n        }\n      }\n      // headers.Access-Control-Allow-*：避免跨域问题\n      if (rOptions.headers.origin && !headers['Access-Control-Allow-Origin']) {\n        headers['Access-Control-Allow-Credentials'] = 'true'\n        headers['Access-Control-Allow-Origin'] = rOptions.headers.origin\n      }\n\n      res.writeHead(status, headers)\n      if (status !== 204) {\n        res.write(body)\n      }\n      res.end()\n\n      const url = `${rOptions.method} ➜ ${rOptions.protocol}//${rOptions.hostname}:${rOptions.port}${req.url}`\n      log.info('success intercept:', url, ', response:', JSON.stringify(response))\n      return true // true代表请求结束\n    }\n  },\n  is (interceptOpt) {\n    return !!interceptOpt.success\n  },\n}\n"
  },
  {
    "path": "packages/mitmproxy/src/lib/interceptor/impl/req/unVerifySsl.js",
    "content": "module.exports = {\n  name: 'unVerifySsl',\n  priority: 124,\n  requestIntercept (context, interceptOpt, req, res, ssl, next) {\n    const { rOptions, log } = context\n\n    if (rOptions.agent.options.rejectUnauthorized && rOptions.agent.unVerifySslAgent) {\n      rOptions.agent = rOptions.agent.unVerifySslAgent\n      log.info(`unVerifySsl intercept: ${rOptions.hostname}, unVerifySsl`)\n      res.setHeader('DS-Interceptor', 'unVerifySsl')\n    } else {\n      log.info(`unVerifySsl intercept: ${rOptions.hostname}, already unVerifySsl`)\n      res.setHeader('DS-Interceptor', 'already unVerifySsl')\n    }\n\n    return true\n  },\n  is (interceptOpt) {\n    return interceptOpt.unVerifySsl === true || interceptOpt.ssl === false\n  },\n}\n"
  },
  {
    "path": "packages/mitmproxy/src/lib/interceptor/impl/res/AfterOPTIONSHeaders.js",
    "content": "const responseReplaceApi = require('./responseReplace')\n\nmodule.exports = {\n  name: 'AfterOPTIONSHeaders',\n  desc: '开启了options.js功能时，正常请求时，会需要增加响应头 `Access-Control-Allow-Origin: xxx`',\n  priority: 201,\n  responseIntercept (context, interceptOpt, req, res, proxyReq, proxyRes, ssl, next) {\n    const { rOptions, log } = context\n\n    if (rOptions.method === 'OPTIONS') {\n      return\n    }\n\n    const headers = {\n      'Access-Control-Allow-Credentials': 'true',\n      'Access-Control-Allow-Origin': '*',\n      'Cross-Origin-Resource-Policy': interceptOpt.optionsCrossPolicy || 'cross-origin',\n    }\n\n    // 替换响应头\n    if (responseReplaceApi.replaceResponseHeaders({ ...headers }, res, proxyRes)) {\n      log.info('AfterOPTIONSHeaders intercept:', JSON.stringify(headers))\n      res.setHeader('DS-AfterOPTIONSHeaders-Interceptor', '1')\n    } else {\n      res.setHeader('DS-AfterOPTIONSHeaders-Interceptor', '0')\n    }\n  },\n  is (interceptOpt) {\n    return !!interceptOpt.options\n  },\n}\n"
  },
  {
    "path": "packages/mitmproxy/src/lib/interceptor/impl/res/cacheResponse.js",
    "content": "const cacheReq = require('../req/cacheRequest')\n\nmodule.exports = {\n  name: 'cacheResponse',\n  priority: 202,\n  responseIntercept (context, interceptOpt, req, res, proxyReq, proxyRes, ssl, next) {\n    const { rOptions, log } = context\n\n    // 只有GET请求\n    if (rOptions.method !== 'GET') {\n      return\n    }\n\n    // 判断当前响应码是否不使用缓存\n    if (interceptOpt.cacheExcludeStatusCodeList && interceptOpt.cacheExcludeStatusCodeList[`${proxyRes.statusCode}`]) {\n      return\n    }\n\n    // 响应码为 200~303 时才进行缓存（可通过以下两个参数调整范围）\n    let minStatusCode = interceptOpt.cacheMinStatusCode || 200\n    let maxStatusCode = interceptOpt.cacheMaxStatusCode || 303\n    if (minStatusCode > maxStatusCode) {\n      const temp = minStatusCode\n      minStatusCode = maxStatusCode\n      maxStatusCode = temp\n    }\n    if (proxyRes.statusCode < minStatusCode || proxyRes.statusCode > maxStatusCode) {\n      // res.setHeader('DS-Cache-Response-Interceptor', `skip: 'method' or 'status' not match`)\n      return\n    }\n\n    // 获取maxAge配置\n    let maxAge = cacheReq.getMaxAge(interceptOpt)\n    // public 或 private\n    const cacheControlType = `${interceptOpt.cacheControlType || 'public'}, `\n    // immutable属性\n    const cacheImmutable = interceptOpt.cacheImmutable !== false && interceptOpt.cacheImmutable !== 'false' ? ', immutable' : ''\n\n    // 获取原响应头中的cache-control、last-modified、expires\n    const originalHeaders = {\n      cacheControl: null,\n      lastModified: null,\n      expires: null,\n      etag: null,\n    }\n    for (let i = 0; i < proxyRes.rawHeaders.length; i += 2) {\n      // 尝试修改rawHeaders中的cache-control、last-modified、expires\n      if (proxyRes.rawHeaders[i].toLowerCase() === 'cache-control') {\n        originalHeaders.cacheControl = { value: proxyRes.rawHeaders[i + 1], valueIndex: i + 1 }\n      } else if (proxyRes.rawHeaders[i].toLowerCase() === 'last-modified') {\n        originalHeaders.lastModified = { value: proxyRes.rawHeaders[i + 1], valueIndex: i + 1 }\n      } else if (proxyRes.rawHeaders[i].toLowerCase() === 'expires') {\n        originalHeaders.expires = { value: proxyRes.rawHeaders[i + 1], valueIndex: i + 1 }\n      } else if (proxyRes.rawHeaders[i].toLowerCase() === 'etag') {\n        originalHeaders.etag = { value: proxyRes.rawHeaders[i + 1], valueIndex: i + 1 }\n      }\n\n      // 如果已经设置了cache-control、last-modified、expires，则直接break\n      if (originalHeaders.cacheControl && originalHeaders.lastModified && originalHeaders.expires && originalHeaders.etag) {\n        break\n      }\n    }\n\n    // 判断原max-age是否大于新max-age\n    if (originalHeaders.cacheControl) {\n      const maxAgeMatch = originalHeaders.cacheControl.value.match(/max-age=(\\d+)/i)\n      if (maxAgeMatch && Number.parseInt(maxAgeMatch[1]) > maxAge) {\n        if (interceptOpt.cacheImmutable !== false && !originalHeaders.cacheControl.value.includes('immutable')) {\n          maxAge = Number.parseInt(maxAgeMatch[1])\n        } else {\n          const url = `${rOptions.method} ➜ ${rOptions.protocol}//${rOptions.hostname}:${rOptions.port}${req.url}`\n          res.setHeader('DS-Cache-Response-Interceptor', `skip: ${maxAgeMatch[1]} > ${maxAge}`)\n          log.info(`cache response intercept: skip: ${maxAgeMatch[1]} > ${maxAge}, url: ${url}`)\n          return\n        }\n      }\n    }\n\n    // 替换用的头信息\n    const now = new Date()\n    const replaceHeaders = {\n      cacheControl: `${cacheControlType}max-age=${maxAge + 1}${cacheImmutable}`,\n      lastModified: now.toUTCString(),\n      expires: new Date(now.getTime() + maxAge * 1000).toUTCString(),\n    }\n    // 开始替换\n    // 替换cache-control\n    if (originalHeaders.cacheControl) {\n      proxyRes.rawHeaders[originalHeaders.cacheControl.valueIndex] = replaceHeaders.cacheControl\n    } else {\n      res.setHeader('Cache-Control', replaceHeaders.cacheControl)\n    }\n    // 替换last-modified\n    if (originalHeaders.lastModified) {\n      proxyRes.rawHeaders[originalHeaders.lastModified.valueIndex] = replaceHeaders.lastModified\n    } else {\n      res.setHeader('Last-Modified', replaceHeaders.lastModified)\n    }\n    // 替换expires\n    if (originalHeaders.expires) {\n      proxyRes.rawHeaders[originalHeaders.expires.valueIndex] = replaceHeaders.expires\n    } else {\n      res.setHeader('Expires', replaceHeaders.expires)\n    }\n\n    res.setHeader('DS-Cache-Response-Interceptor', maxAge)\n  },\n  is (interceptOpt) {\n    const maxAge = cacheReq.getMaxAge(interceptOpt)\n    return maxAge != null && maxAge > 0\n  },\n}\n"
  },
  {
    "path": "packages/mitmproxy/src/lib/interceptor/impl/res/responseReplace.js",
    "content": "const lodash = require('lodash')\nconst cacheReq = require('../req/cacheRequest')\n\nconst REMOVE = '[remove]'\n\n// 替换响应头\nfunction replaceResponseHeaders (newHeaders, res, proxyRes) {\n  if (!newHeaders || lodash.isEmpty(newHeaders)) {\n    return null\n  }\n\n  // 响应头Key统一转小写\n  for (const headerKey in newHeaders) {\n    if (headerKey === headerKey.toLowerCase()) {\n      continue\n    }\n\n    const value = newHeaders[headerKey]\n    delete newHeaders[headerKey]\n    newHeaders[headerKey.toLowerCase()] = value\n  }\n\n  // 原先响应头\n  const preHeaders = {}\n\n  // 替换响应头\n  const needDeleteKeys = []\n  for (let i = 0; i < proxyRes.rawHeaders.length; i += 2) {\n    const headerKey = proxyRes.rawHeaders[i].toLowerCase()\n\n    const newHeaderValue = newHeaders[headerKey]\n    if (newHeaderValue) {\n      if (newHeaderValue !== proxyRes.rawHeaders[i + 1]) {\n        preHeaders[headerKey] = proxyRes.rawHeaders[i + 1] // 先保存原先响应头\n        if (newHeaderValue === REMOVE) { // 由于拦截配置中不允许配置null，会被删，所以配置一个 \"[remove]\"，当作删除响应头的意思\n          proxyRes.rawHeaders[i + 1] = ''\n        } else {\n          proxyRes.rawHeaders[i + 1] = newHeaderValue\n        }\n      }\n      needDeleteKeys.push(headerKey)\n    }\n  }\n  // 处理删除响应头\n  for (const headerKey of needDeleteKeys) {\n    delete newHeaders[headerKey]\n  }\n  // 新增响应头\n  for (const headerKey in newHeaders) {\n    const headerValue = newHeaders[headerKey]\n    if (headerValue == null || headerValue === REMOVE) {\n      continue\n    }\n\n    res.setHeader(headerKey, newHeaders[headerKey])\n    preHeaders[headerKey] = null // 标记原先响应头为null\n  }\n\n  if (lodash.isEmpty(preHeaders)) {\n    return null\n  }\n  // 返回原先响应头\n  return preHeaders\n}\n\nmodule.exports = {\n  name: 'responseReplace',\n  priority: 203,\n  replaceResponseHeaders,\n  responseIntercept (context, interceptOpt, req, res, proxyReq, proxyRes, ssl, next) {\n    const { rOptions, log } = context\n\n    if (proxyRes.statusCode !== 200) {\n      return\n    }\n\n    const responseReplaceConfig = interceptOpt.responseReplace\n\n    let actions = ''\n\n    const replaceHeaders = responseReplaceConfig.headers || {}\n\n    // 处理文件下载请求\n    if (responseReplaceConfig.doDownload || rOptions.doDownload) {\n      const filename = (rOptions.path.match('^.*/([^/?]+)/?(\\\\?.*)?$') || [])[1] || 'UNKNOWN_FILENAME'\n      // 设置文件下载响应头\n      replaceHeaders['content-disposition'] = `attachment; filename=\"${encodeURIComponent(filename)}\"`\n      // 设置文件类型\n      if (replaceHeaders['content-type'] == null) {\n        replaceHeaders['content-type'] = 'application/octet-stream'\n      }\n      // 如果未手动配置需要缓存，则不允许使用缓存\n      const maxAge = cacheReq.getMaxAge(interceptOpt)\n      if (maxAge == null || maxAge <= 0) {\n        replaceHeaders['cache-control'] = REMOVE\n        replaceHeaders['last-modified'] = REMOVE\n        replaceHeaders.expires = REMOVE\n      }\n\n      actions += `${actions ? ',' : ''}download:${filename}`\n    }\n\n    // 替换响应头\n    if (replaceResponseHeaders(replaceHeaders, res, proxyRes)) {\n      actions += `${actions ? ',' : ''}headers`\n    }\n\n    if (actions) {\n      res.setHeader('DS-ResponseReplace-Interceptor', actions)\n      log.info(`response intercept: ${actions}`)\n    }\n  },\n  is (interceptOpt) {\n    return !!interceptOpt.responseReplace\n  },\n}\n"
  },
  {
    "path": "packages/mitmproxy/src/lib/interceptor/impl/res/script.js",
    "content": "const monkey = require('../../../monkey')\n// const CryptoJs = require('crypto-js')\nconst lodash = require('lodash')\nconst log = require('../../../../utils/util.log.server')\n\nconst SCRIPT_URL_PRE = '/____ds_script____/' // 内置脚本的请求地址前缀\nconst SCRIPT_PROXY_URL_PRE = '/____ds_script_proxy____/' // 绝对地址脚本的伪脚本地址前缀\nconst REMOVE = '[remove]' // 标记需要移除的头信息\n\nfunction getScript (key, script) {\n  const scriptUrl = SCRIPT_URL_PRE + key\n  // const hash = CryptoJs.SHA256(script).toString(CryptoJs.enc.Base64)\n  // return `<script crossorigin=\"anonymous\" defer=\"defer\" type=\"application/javascript\" src=\"${scriptUrl}\" integrity=\"sha256-${hash}\"></script>`\n  return `<script crossorigin=\"anonymous\" defer=\"defer\" type=\"application/javascript\" src=\"${scriptUrl}\"></script>`\n}\nfunction getScriptByUrlOrPath (scriptUrlOrPath) {\n  return `<script crossorigin=\"anonymous\" defer=\"defer\" type=\"application/javascript\" src=\"${scriptUrlOrPath}\"></script>`\n}\n\nmodule.exports = {\n  name: 'script',\n  priority: 211,\n  responseIntercept (context, interceptOpt, req, res, proxyReq, proxyRes, ssl, next) {\n    const { rOptions, log, setting } = context\n\n    // github特殊处理\n    if (rOptions.hostname === 'github.com' && rOptions.headers['turbo-frame'] === 'repo-content-turbo-frame') {\n      return\n    }\n\n    // 如果没有响应头 'content-type'，或其值不是 'text/html'，则不处理\n    if (!proxyRes.headers['content-type'] || !proxyRes.headers['content-type'].includes('text/html')) {\n      res.setHeader('DS-Script-Interceptor', 'Not text/html')\n      return\n    }\n\n    let keys = interceptOpt.script\n    if (typeof keys === 'string') {\n      keys = [keys]\n    }\n    try {\n      // 内置脚本列表\n      const scripts = monkey.get(setting.script.dirAbsolutePath)\n\n      let tags = ''\n      for (const key of keys) {\n        if (key === 'global' || key === 'tampermonkey') {\n          continue\n        }\n\n        let scriptTag\n\n        if (key.includes('/')) {\n          scriptTag = getScriptByUrlOrPath(key) // 1.绝对地址或相对地址（注意：当目标站点限制跨域脚本时，可使用相对地址，再结合proxy拦截器进行代理，可规避掉限制跨域脚本问题。）\n        } else {\n          const script = scripts[key]\n          if (script == null) {\n            continue\n          }\n          scriptTag = getScript(key, script.script) // 2.DS内置脚本\n        }\n\n        tags += `\\r\\n\\t${scriptTag}`\n      }\n\n      // 如果脚本为空，则不插入\n      if (tags === '') {\n        return\n      }\n\n      // 插入油猴脚本浏览器扩展\n      if (typeof interceptOpt.tampermonkeyScript === 'string') {\n        tags = `\\r\\n\\t${getScriptByUrlOrPath(interceptOpt.tampermonkeyScript)}${tags}`\n      } else {\n        tags = `\\r\\n\\t${getScript('tampermonkey', scripts.tampermonkey.script)}${tags}`\n      }\n\n      res.setHeader('DS-Script-Interceptor', 'true')\n      log.info(`script response intercept: insert script ${rOptions.protocol}//${rOptions.hostname}:${rOptions.port}${rOptions.path}`, ', head:', tags)\n      return {\n        head: `${tags}\\r\\n`,\n      }\n    } catch (err) {\n      try {\n        res.setHeader('DS-Script-Interceptor', 'error')\n      } catch (e) {\n        // ignore\n      }\n      log.error('load monkey script error', err)\n    }\n  },\n  is (interceptOpt) {\n    return interceptOpt.script\n  },\n  // 处理拦截配置：自动生成script拦截器所需的辅助配置，降低使用`script拦截器`配置绝对地址和相对地址时的门槛\n  handleScriptInterceptConfig (intercepts) {\n    // 为了简化 script 拦截器配置脚本绝对地址，这里特殊处理一下\n    for (const hostnamePattern in intercepts) {\n      const hostnameConfig = intercepts[hostnamePattern]\n\n      const scriptProxy = {}\n      const handleScriptUrl = (scriptUrl, name, replaceScriptUrlFun) => {\n        if (scriptUrl.indexOf('https:') === 0 || scriptUrl.indexOf('http:') === 0) {\n          // 绝对地址\n          const scriptKey = `${SCRIPT_PROXY_URL_PRE + scriptUrl.replace('.js', '').replace(/[\\W_]+/g, '_')}.js` // 伪脚本地址：移除 script 中可能存在的特殊字符，并转为相对地址\n          scriptProxy[scriptKey] = scriptUrl\n          log.info(`替换${name}配置值：'${scriptUrl}' -> '${scriptKey}'`)\n          if (typeof replaceScriptUrlFun === 'function') {\n            replaceScriptUrlFun(scriptKey)\n          }\n        } else if (scriptUrl.indexOf('/') === 0) {\n          // 相对地址\n          scriptProxy[scriptUrl] = scriptUrl\n        }\n      }\n\n      for (const pathPattern in hostnameConfig) {\n        const pathConfig = hostnameConfig[pathPattern]\n        // 处理 script 配置\n        if (typeof pathConfig.script === 'object' && pathConfig.script.length > 0) {\n          for (let i = 0; i < pathConfig.script.length; i++) {\n            const scriptUrl = pathConfig.script[i]\n            handleScriptUrl(scriptUrl, 'script', (scriptKey) => {\n              pathConfig.script[i] = scriptKey\n            })\n          }\n        } else if (typeof pathConfig.script === 'string') {\n          handleScriptUrl(pathConfig.script, 'script', (scriptKey) => {\n            pathConfig.script = scriptKey\n          })\n        }\n\n        // 处理 tampermonkeyScript 配置\n        if (typeof pathConfig.tampermonkeyScript === 'string') {\n          handleScriptUrl(pathConfig.tampermonkeyScript, 'tampermonkey', (scriptKey) => {\n            pathConfig.tampermonkeyScript = scriptKey\n          })\n        }\n      }\n\n      // 自动创建脚本\n      if (!lodash.isEmpty(scriptProxy)) {\n        for (const scriptKey in scriptProxy) {\n          if (scriptKey.indexOf(SCRIPT_PROXY_URL_PRE) === 0) {\n            // 绝对地址：新增代理配置\n            const scriptUrl = scriptProxy[scriptKey]\n\n            const pathPattern = `^${scriptKey.replace(/\\./g, '\\\\.')}$`\n            if (hostnameConfig[pathPattern]) {\n              continue // 配置已经存在，按自定义配置优先\n            }\n            hostnameConfig[pathPattern] = {\n              proxy: scriptUrl,\n              // 移除部分请求头，避免触发目标站点的拦截策略\n              requestReplace: {\n                headers: {\n                  host: REMOVE,\n                  referer: REMOVE,\n                  cookie: REMOVE,\n                },\n              },\n              // 替换和移除部分响应头，避免触发目标站点的阻止脚本加载策略\n              responseReplace: {\n                headers: {\n                  'content-type': 'application/javascript; charset=utf-8',\n                  'set-cookie': REMOVE,\n                  'server': REMOVE,\n                },\n              },\n              cacheDays: 7,\n              desc: '为伪脚本文件设置代理地址，并设置响应头 `content-type: \\'application/javascript; charset=utf-8\\'`，同时缓存7天。',\n            }\n\n            const obj = {}\n            obj[pathPattern] = hostnameConfig[pathPattern]\n            log.debug(`域名 '${hostnamePattern}' 拦截配置中，新增伪脚本地址的代理配置:`, JSON.stringify(obj, null, '\\t'))\n          } else {\n            // 相对地址：新增响应头Content-Type替换配置\n            if (hostnameConfig[scriptKey]) {\n              continue // 配置已经存在，按自定义配置优先\n            }\n\n            hostnameConfig[scriptKey] = {\n              responseReplace: {\n                headers: {\n                  'content-type': 'application/javascript; charset=utf-8',\n                },\n              },\n              cacheDays: 7,\n              desc: '为脚本设置响应头 `content-type: \\'application/javascript; charset=utf-8\\'`，同时缓存7天。',\n            }\n\n            const obj = {}\n            obj[scriptKey] = hostnameConfig[scriptKey]\n            log.info(`域名 '${hostnamePattern}' 拦截配置中，新增目标脚本地址的响应头替换配置:`, JSON.stringify(obj, null, '\\t'))\n          }\n        }\n      }\n    }\n  },\n}\n"
  },
  {
    "path": "packages/mitmproxy/src/lib/interceptor/index.js",
    "content": "// request interceptor impls\nconst OPTIONS = require('./impl/req/OPTIONS.js')\nconst success = require('./impl/req/success')\nconst abort = require('./impl/req/abort')\nconst cacheRequest = require('./impl/req/cacheRequest')\nconst redirect = require('./impl/req/redirect')\n\nconst requestReplace = require('./impl/req/requestReplace')\n\nconst proxy = require('./impl/req/proxy')\nconst sni = require('./impl/req/sni')\nconst unVerifySsl = require('./impl/req/unVerifySsl')\n\nconst baiduOcr = require('./impl/req/baiduOcr')\n\n// response interceptor impls\nconst AfterOPTIONSHeaders = require('./impl/res/AfterOPTIONSHeaders')\nconst cacheResponse = require('./impl/res/cacheResponse')\nconst responseReplace = require('./impl/res/responseReplace')\n\nconst script = require('./impl/res/script')\n\nmodule.exports = [\n  // request interceptor impls\n  OPTIONS, success, abort, cacheRequest, redirect,\n  requestReplace,\n  proxy, sni, unVerifySsl,\n  baiduOcr,\n\n  // response interceptor impls\n  AfterOPTIONSHeaders, cacheResponse, responseReplace,\n  script,\n]\n"
  },
  {
    "path": "packages/mitmproxy/src/lib/monkey/index.js",
    "content": "const fs = require('node:fs')\nconst path = require('node:path')\nconst log = require('../../utils/util.log.server')\n\nlet scripts\n\nfunction buildScript (sc, content, scriptName) {\n  const scriptKey = `ds_${scriptName}${sc.version ? (`_${sc.version}`) : ''}:`\n\n  // 代码1：监听事件\n  const runAt = sc['run-at'] || 'document-end'\n  let eventStr\n  if (runAt === 'document-end') {\n    eventStr = 'document.addEventListener(\"DOMContentLoaded\"'\n  } else {\n    eventStr = 'window.addEventListener(\"load\"'\n  }\n\n  // 代码2：初始化\n  const options = {\n    name: sc.name,\n    version: sc.version,\n    icon: sc.icon,\n  }\n  const initStr = `\nconst DS_init = (window.__ds_global__ || {})['DS_init']\nif (typeof DS_init === 'function') {\n\\tconsole.log(\"${scriptKey} do DS_init\")\n\\tDS_init(${JSON.stringify(options)});\n} else {\n\\tconsole.log(\"${scriptKey} has no DS_init\")\n}`\n\n  // 代码3：判断是否启用了脚本\n  const checkEnabledStr = `\nif (!((window.__ds_global__ || {}).GM_getValue || (() => true))(\"ds_enabled\", true)) {\n\\tconsole.log(\"${scriptKey} tampermonkey disabled\")\n\\treturn\n}`\n\n  // 代码4：`GM_xxx` 方法读取\n  let grantStr = ''\n  for (const item of sc.grant) {\n    if (grantStr.length > 0) {\n      grantStr += '\\r\\n'\n    }\n\n    if (item.indexOf('.') > 0) {\n      grantStr += `${item} = (window.__ds_global__ || {})['${item}'];`\n    } else {\n      grantStr += `const ${item} = (window.__ds_global__ || {})['${item}'] || (() => {});`\n    }\n  }\n\n  // 拼接脚本\n  return `${eventStr}, () => {${\n    initStr}\\r\\n${\n    checkEnabledStr}\\r\\n\\r\\n${\n    grantStr ? (`${grantStr}\\r\\n\\r\\n`) : ''\n  }${content\n  }\\r\\nconsole.log(\"${scriptKey} completed\")`\n  + `\\r\\n})`\n  + `\\r\\nconsole.log(\"${scriptKey} loaded\")`\n}\n\nfunction loadScript (content, scriptName) {\n  // @grant        GM_registerMenuCommand\n  // @grant        GM_unregisterMenuCommand\n  // @grant        GM_openInTab\n  // @grant        GM_getValue\n  // @grant        GM_setValue\n  // @grant        GM_notification\n  const annoFlag = '// ==/UserScript=='\n  const arr = content.split(annoFlag)\n  const start = 0\n\n  const confStr = arr[start]\n  const confItemArr = confStr.split('\\n')\n  const sc = {\n    grant: [],\n    match: [],\n    script: '',\n  }\n  for (const string of confItemArr) {\n    const reg = new RegExp('.*@(\\\\S+)\\\\s(.+)')\n    const ret = string.match(reg)\n    if (ret) {\n      const key = ret[1].trim()\n      const value = ret[2].trim()\n      if (key === 'grant') {\n        sc.grant.push(value)\n      } else if (key === 'match') {\n        sc.match.push(value)\n      } else {\n        sc[key] = value\n      }\n    }\n  }\n  const script = arr[start + 1].trim()\n\n  sc.script = buildScript(sc, script, scriptName)\n  return sc\n}\n\nfunction readFile (rootDir, script) {\n  log.info('read script, script root location:', path.resolve('./'))\n  const location = path.join(rootDir, `./${script}`)\n  log.info('read script, the script location:', location)\n  return fs.readFileSync(location).toString()\n}\n\nconst api = {\n  get (rootDir) {\n    if (scripts == null) {\n      return api.load(rootDir)\n    }\n    return scripts\n  },\n  load (rootDir) {\n    scripts = {}\n    scripts.github = loadScript(readFile(rootDir, 'github.script'), 'github')\n    scripts.google = loadScript(readFile(rootDir, 'google.js'), 'google')\n    // scripts.jquery = { script: readFile(rootDir, 'jquery.min.js') }\n    scripts.tampermonkey = { script: readFile(rootDir, 'tampermonkey.script') }\n    return scripts\n  },\n  loadScript,\n}\n\nmodule.exports = api\n"
  },
  {
    "path": "packages/mitmproxy/src/lib/proxy/common/ProxyHttpAgent.js",
    "content": "const AgentOrigin = require('agentkeepalive')\n\nmodule.exports = class Agent extends AgentOrigin {\n  // Hacky\n  getName (option) {\n    let name = AgentOrigin.prototype.getName.call(this, option)\n    name += ':'\n    if (option.customSocketId) {\n      name += option.customSocketId\n    }\n    return name\n  }\n}\n"
  },
  {
    "path": "packages/mitmproxy/src/lib/proxy/common/ProxyHttpsAgent.js",
    "content": "const HttpsAgentOrigin = require('agentkeepalive').HttpsAgent\n\nmodule.exports = class HttpsAgent extends HttpsAgentOrigin {\n  // Hacky\n  getName (option) {\n    let name = HttpsAgentOrigin.prototype.getName.call(this, option)\n    name += ':'\n    if (option.customSocketId) {\n      name += option.customSocketId\n    }\n    return name\n  }\n}\n"
  },
  {
    "path": "packages/mitmproxy/src/lib/proxy/common/config.js",
    "content": "const path = require('node:path')\n\nconst config = exports\n\nconfig.defaultHost = '127.0.0.1'\nconfig.defaultPort = 31181\nconfig.defaultMaxLength = 100\n\nconfig.caCertFileName = 'dev-sidecar.ca.crt'\nconfig.caKeyFileName = 'dev-sidecar.ca.key.pem'\nconfig.caName = 'DevSidecar - This certificate is generated locally'\nconfig.caBasePath = buildDefaultCABasePath()\n\nconfig.getDefaultCABasePath = function () {\n  return config.caBasePath\n}\nconfig.setDefaultCABasePath = function (path) {\n  config.caBasePath = path\n}\nfunction buildDefaultCABasePath () {\n  const userHome = process.env.USERPROFILE || process.env.HOME || '/'\n  return path.resolve(userHome, './.dev-sidecar')\n}\n\nconfig.getDefaultCACertPath = function () {\n  return path.resolve(config.getDefaultCABasePath(), config.caCertFileName)\n}\n\nconfig.getDefaultCAKeyPath = function () {\n  return path.resolve(config.getDefaultCABasePath(), config.caKeyFileName)\n}\n"
  },
  {
    "path": "packages/mitmproxy/src/lib/proxy/common/util.js",
    "content": "const url = require('node:url')\nconst tunnelAgent = require('tunnel-agent')\nconst log = require('../../../utils/util.log.server')\nconst matchUtil = require('../../../utils/util.match')\nconst Agent = require('./ProxyHttpAgent')\nconst HttpsAgent = require('./ProxyHttpsAgent')\n\nconst util = exports\n\nconst httpsAgentCache = {}\nconst httpAgentCache = {}\n\nlet socketId = 0\n\nlet httpsOverHttpAgent, httpOverHttpsAgent, httpsOverHttpsAgent\n\nfunction getTimeoutConfig (hostname, serverSetting) {\n  const timeoutMapping = serverSetting.timeoutMapping\n\n  const timeoutConfig = matchUtil.matchHostname(timeoutMapping, hostname, 'get timeoutConfig') || {}\n\n  return {\n    timeout: timeoutConfig.timeout || serverSetting.defaultTimeout || 20000,\n    keepAliveTimeout: timeoutConfig.keepAliveTimeout || serverSetting.defaultKeepAliveTimeout || 30000,\n  }\n}\n\nfunction createHttpsAgent (timeoutConfig, verifySsl) {\n  const key = `${timeoutConfig.timeout}-${timeoutConfig.keepAliveTimeout}`\n  if (!httpsAgentCache[key]) {\n    verifySsl = !!verifySsl\n\n    // 证书回调函数\n    const checkServerIdentity = (host, cert) => {\n      log.info(`checkServerIdentity: ${host}, CN: ${cert.subject.CN}, C: ${cert.subject.C || cert.issuer.C}, ST: ${cert.subject.ST || cert.issuer.ST}, bits: ${cert.bits}`)\n    }\n\n    const agent = new HttpsAgent({\n      keepAlive: true,\n      timeout: timeoutConfig.timeout,\n      keepAliveTimeout: timeoutConfig.keepAliveTimeout,\n      checkServerIdentity,\n      rejectUnauthorized: verifySsl,\n    })\n\n    agent.unVerifySslAgent = new HttpsAgent({\n      keepAlive: true,\n      timeout: timeoutConfig.timeout,\n      keepAliveTimeout: timeoutConfig.keepAliveTimeout,\n      checkServerIdentity,\n      rejectUnauthorized: false,\n    })\n\n    httpsAgentCache[key] = agent\n    log.info('创建 HttpsAgent 成功, timeoutConfig:', timeoutConfig, ', verifySsl:', verifySsl)\n  }\n  return httpsAgentCache[key]\n}\n\nfunction createHttpAgent (timeoutConfig) {\n  const key = `${timeoutConfig.timeout}-${timeoutConfig.keepAliveTimeout}`\n  if (!httpAgentCache[key]) {\n    httpAgentCache[key] = new Agent({\n      keepAlive: true,\n      timeout: timeoutConfig.timeout,\n      keepAliveTimeout: timeoutConfig.keepAliveTimeout,\n    })\n    log.info('创建 HttpAgent 成功, timeoutConfig:', timeoutConfig)\n  }\n  return httpAgentCache[key]\n}\n\nfunction createAgent (protocol, timeoutConfig, verifySsl) {\n  return protocol === 'https:'\n    ? createHttpsAgent(timeoutConfig, verifySsl)\n    : createHttpAgent(timeoutConfig)\n}\n\nutil.parseHostnameAndPort = (host, defaultPort) => {\n  let arr = host.match(/^(\\[[^\\]]+\\])(?::(\\d+))?$/) // 尝试解析IPv6\n  if (arr) {\n    arr = arr.slice(1)\n    if (arr[1]) {\n      arr[1] = Number.parseInt(arr[1], 10)\n    }\n  } else {\n    arr = host.split(':')\n    if (arr.length > 1) {\n      arr[1] = Number.parseInt(arr[1], 10)\n    }\n  }\n\n  if (defaultPort > 0 && (arr.length === 1 || arr[1] === undefined)) {\n    arr[1] = defaultPort\n  } else if (arr.length === 2 && arr[1] === undefined) {\n    arr.pop()\n  }\n\n  return arr\n}\n\nutil.getOptionsFromRequest = (req, ssl, externalProxy = null, serverSetting, compatibleConfig = null) => {\n  // eslint-disable-next-line node/no-deprecated-api\n  const urlObject = url.parse(req.url)\n  const defaultPort = ssl ? 443 : 80\n  const protocol = ssl ? 'https:' : 'http:'\n  const headers = Object.assign({}, req.headers)\n  let externalProxyUrl = null\n\n  if (externalProxy) {\n    if (typeof externalProxy === 'string') {\n      externalProxyUrl = externalProxy\n    } else if (typeof externalProxy === 'function') {\n      try {\n        externalProxyUrl = externalProxy(req, ssl)\n      } catch (e) {\n        log.error('externalProxy error:', e)\n      }\n    }\n  }\n\n  // 解析host和port\n  const arr = util.parseHostnameAndPort(req.headers.host)\n  const hostname = arr[0]\n  const port = arr[1] || defaultPort\n\n  delete headers['proxy-connection']\n  let agent\n  if (!externalProxyUrl) {\n    // keepAlive\n    if (headers.connection !== 'close') {\n      const timeoutConfig = getTimeoutConfig(hostname, serverSetting)\n      // log.info(`get timeoutConfig '${hostname}':`, timeoutConfig)\n      agent = createAgent(protocol, timeoutConfig, serverSetting.verifySsl)\n      headers.connection = 'keep-alive'\n    } else {\n      agent = false\n    }\n  } else {\n    agent = util.getTunnelAgent(protocol === 'https:', externalProxyUrl)\n  }\n\n  // 初始化options\n  const options = {\n    protocol,\n    method: req.method,\n    url: req.url,\n    hostname,\n    port,\n    path: urlObject.path,\n    headers: req.headers,\n    agent,\n    compatibleConfig,\n  }\n\n  // eslint-disable-next-line node/no-deprecated-api\n  if (protocol === 'http:' && externalProxyUrl && (url.parse(externalProxyUrl)).protocol === 'http:') {\n    // eslint-disable-next-line node/no-deprecated-api\n    const externalURL = url.parse(externalProxyUrl)\n    options.hostname = externalURL.hostname\n    options.port = externalURL.port\n    // support non-transparent proxy\n    options.path = `http://${urlObject.host}${urlObject.path}`\n  }\n\n  // mark a socketId for Agent to bind socket for NTLM\n  if (req.socket.customSocketId) {\n    options.customSocketId = req.socket.customSocketId\n  } else if (headers.authorization) {\n    options.customSocketId = req.socket.customSocketId = socketId++\n  }\n\n  return options\n}\n\nutil.getTunnelAgent = (requestIsSSL, externalProxyUrl) => {\n  // eslint-disable-next-line node/no-deprecated-api\n  const urlObject = url.parse(externalProxyUrl)\n  const protocol = urlObject.protocol || 'http:'\n  let port = urlObject.port\n  if (!port) {\n    port = protocol === 'http:' ? 80 : 443\n  }\n  const hostname = urlObject.hostname || 'localhost'\n\n  if (requestIsSSL) {\n    if (protocol === 'http:') {\n      if (!httpsOverHttpAgent) {\n        httpsOverHttpAgent = tunnelAgent.httpsOverHttp({\n          proxy: {\n            host: hostname,\n            port,\n          },\n        })\n      }\n      return httpsOverHttpAgent\n    } else {\n      if (!httpsOverHttpsAgent) {\n        httpsOverHttpsAgent = tunnelAgent.httpsOverHttps({\n          proxy: {\n            host: hostname,\n            port,\n          },\n        })\n      }\n      return httpsOverHttpsAgent\n    }\n  } else {\n    if (protocol === 'http:') {\n      // if (!httpOverHttpAgent) {\n      //     httpOverHttpAgent = tunnelAgent.httpOverHttp({\n      //         proxy: {\n      //             host: hostname,\n      //             port: port\n      //         }\n      //     })\n      // }\n      return false\n    } else {\n      if (!httpOverHttpsAgent) {\n        httpOverHttpsAgent = tunnelAgent.httpOverHttps({\n          proxy: {\n            host: hostname,\n            port,\n          },\n        })\n      }\n      return httpOverHttpsAgent\n    }\n  }\n}\n"
  },
  {
    "path": "packages/mitmproxy/src/lib/proxy/compatible/compatible.js",
    "content": "/**\n * 自动兼容程序自适应生成配置\n * 此脚本会针对各种兼容性问题，为对应域名生成相应的兼容性配置，并将自适应配置写入到 `~/.dev-sidecar/automaticCompatibleConfig.json` 文件中。\n * 当然，也有可能会生成错误的配置，导致无法兼容，这时候可以通过 `config.server.compatible` 配置项，来覆盖这里生成的配置，达到主动适配的效果。\n *\n * @author WangLiang\n */\nconst fs = require('node:fs')\nconst jsonApi = require('../../../json')\nconst log = require('../../../utils/util.log.server')\nconst matchUtil = require('../../../utils/util.match')\nconst configLoader = require('@docmirror/dev-sidecar/src/config/local-config-loader')\n\nconst defaultConfig = {\n  // connect阶段所需的兼容性配置\n  connect: {\n    // 参考配置\n    // 'xxx.xxx.xxx.xxx:443': {\n    //   ssl: false\n    // }\n  },\n  // request阶段所需的兼容性配置\n  request: {\n    // 参考配置\n    // 'xxx.xxx.xxx.xxx:443': {\n    //   rejectUnauthorized: false\n    // }\n  },\n}\n\nconst config = _loadFromFile(defaultConfig)\n\nfunction _getConnectConfig (hostname, port) {\n  const connectConfig = config.connect[`${hostname}:${port}`]\n  log.info(`getConnectConfig: ${hostname}:${port}, ${jsonApi.stringify2(connectConfig)}`)\n  return connectConfig\n}\nfunction _getRequestConfig (hostname, port) {\n  const requestConfig = config.request[`${hostname}:${port}`]\n  log.info(`getRequestConfig: ${hostname}:${port}, ${jsonApi.stringify2(requestConfig)}`)\n  return requestConfig\n}\n\n// region 本地配置文件所需函数\n\nfunction _loadFromFile (defaultConfig) {\n  const configPath = configLoader.getAutomaticCompatibleConfigPath()\n  let config\n  if (!fs.existsSync(configPath)) {\n    config = defaultConfig\n    log.info(`本地未保存过 ${configPath} 文件，使用默认配置`)\n  } else {\n    const file = fs.readFileSync(configPath)\n    log.info('读取 automaticCompatibleConfig.json 成功:', configPath)\n    const fileStr = file.toString()\n    try {\n      config = jsonApi.parse(fileStr)\n      if (config.connect == null) {\n        config.connect = defaultConfig.connect\n      }\n      if (config.request == null) {\n        config.request = defaultConfig.request\n      }\n    } catch (e) {\n      log.error('解析 automaticCompatibleConfig.json 成功:', configPath, ', error:', e)\n      return defaultConfig\n    }\n  }\n\n  return config\n}\n\nfunction _saveConfigToFile () {\n  const filePath = configLoader.getAutomaticCompatibleConfigPath()\n  try {\n    fs.writeFileSync(filePath, jsonApi.stringify(config))\n    log.info('保存 automaticCompatibleConfig.json 成功:', filePath)\n  } catch (e) {\n    log.error('保存 automaticCompatibleConfig.json 失败:', filePath, ', error:', e)\n  }\n}\n\n// endregion\n\nmodule.exports = {\n  /**\n   * 获取 connect 阶段所需的兼容性配置\n   *\n   * @param hostname 域名\n   * @param port     端口\n   * @param manualCompatibleConfig 手动兼容性配置\n   * @returns connect阶段所需的兼容性配置\n   */\n  getConnectCompatibleConfig (hostname, port, manualCompatibleConfig = null) {\n    let connectCompatibleConfig = manualCompatibleConfig == null ? null : matchUtil.matchHostname(manualCompatibleConfig.connect, `${hostname}:${port}`, 'getConnectCompatibleConfig')\n    if (connectCompatibleConfig == null) {\n      connectCompatibleConfig = _getConnectConfig(hostname, port)\n    }\n    return connectCompatibleConfig\n  },\n\n  setConnectSsl (hostname, port, ssl, autoSave = true) {\n    const connectCompatibleConfig = this.getConnectCompatibleConfig(hostname, port)\n    if (connectCompatibleConfig) {\n      connectCompatibleConfig.ssl = ssl\n    } else {\n      config.connect[`${hostname}:${port}`] = { ssl }\n    }\n\n    // 配置保存到文件\n    if (autoSave) {\n      _saveConfigToFile()\n    }\n\n    log.info(`【自动兼容程序】${hostname}:${port}: 设置 connect.ssl = ${ssl}`)\n  },\n\n  // --------------------------------------------------------------------------------------------------------------------------\n\n  /**\n   * 获取 request 阶段所需的兼容性配置\n   *\n   * @param rOptions\n   * @param manualCompatibleConfig\n   */\n  getRequestCompatibleConfig (rOptions, manualCompatibleConfig = null) {\n    let requestCompatibleConfig = manualCompatibleConfig == null ? null : matchUtil.matchHostname(manualCompatibleConfig.request, `${rOptions.hostname}:${rOptions.port}`, 'getRequestCompatibleConfig')\n    if (requestCompatibleConfig == null) {\n      requestCompatibleConfig = _getRequestConfig(rOptions.hostname, rOptions.port)\n    }\n    return requestCompatibleConfig\n  },\n\n  setRequestRejectUnauthorized (rOptions, rejectUnauthorized, autoSave = true) {\n    const requestCompatibleConfig = this.getRequestCompatibleConfig(rOptions.hostname, rOptions.port)\n    if (requestCompatibleConfig) {\n      requestCompatibleConfig.rejectUnauthorized = rejectUnauthorized\n    } else {\n      config.request[`${rOptions.hostname}:${rOptions.port}`] = { rejectUnauthorized }\n    }\n\n    // 配置保存到文件\n    if (autoSave) {\n      _saveConfigToFile()\n    }\n\n    log.info(`【自动兼容程序】${rOptions.hostname}:${rOptions.port}: 设置 request.rejectUnauthorized = ${rejectUnauthorized}`)\n  },\n}\n"
  },
  {
    "path": "packages/mitmproxy/src/lib/proxy/index.js",
    "content": "// require('babel-polyfill')\nmodule.exports = require('./mitmproxy')\n"
  },
  {
    "path": "packages/mitmproxy/src/lib/proxy/middleware/InsertScriptMiddleware.js",
    "content": "const zlib = require('node:zlib')\nconst through = require('through2')\nconst log = require('../../../utils/util.log.server')\n\n// 编解码器\nconst codecMap = {\n  gzip: {\n    createCompressor: () => zlib.createGzip(),\n    createDecompressor: () => zlib.createGunzip(),\n  },\n  deflate: {\n    createCompressor: () => zlib.createDeflate(),\n    createDecompressor: () => zlib.createInflate(),\n  },\n  br: {\n    createCompressor: () => zlib.createBrotliCompress(),\n    createDecompressor: () => zlib.createBrotliDecompress(),\n  },\n}\nconst supportedEncodings = Object.keys(codecMap)\nconst supportedEncodingsStr = supportedEncodings.join(', ')\n\nconst httpUtil = {\n  // 获取响应内容编码\n  getContentEncoding (res) {\n    const encoding = res.headers['content-encoding']\n    if (encoding) {\n      return encoding.toLowerCase()\n    }\n    return null\n  },\n  // 获取编解码器\n  getCodec (encoding) {\n    return codecMap[encoding]\n  },\n  // 获取支持的编解码器名称字符串\n  supportedEncodingsStr () {\n    return supportedEncodingsStr\n  },\n  // 是否HTML代码\n  isHtml (res) {\n    const contentType = res.headers['content-type']\n    return (typeof contentType !== 'undefined') && /text\\/html|application\\/xhtml\\+xml/.test(contentType)\n  },\n}\nconst HEAD = Buffer.from('</head>')\nconst HEAD_UP = Buffer.from('</HEAD>')\nconst BODY = Buffer.from('</body>')\nconst BODY_UP = Buffer.from('</BODY>')\n\nfunction chunkByteReplace (_this, chunk, enc, callback, append) {\n  if (append) {\n    if (append.head) {\n      const ret = injectScriptIntoHtml([HEAD, HEAD_UP], chunk, append.head)\n      if (ret != null) {\n        chunk = ret\n      }\n    }\n    if (append.body) {\n      const ret = injectScriptIntoHtml([BODY, BODY_UP], chunk, append.body)\n      if (ret != null) {\n        chunk = ret\n      }\n    }\n  }\n  _this.push(chunk)\n  callback()\n}\n\nfunction injectScriptIntoHtml (tags, chunk, script) {\n  for (const tag of tags) {\n    const index = chunk.indexOf(tag)\n    if (index < 0) {\n      continue\n    }\n    const scriptBuf = Buffer.from(script)\n    const chunkNew = Buffer.alloc(chunk.length + scriptBuf.length)\n    chunk.copy(chunkNew, 0, 0, index)\n    scriptBuf.copy(chunkNew, index, 0)\n    chunk.copy(chunkNew, index + scriptBuf.length, index)\n    return chunkNew\n  }\n  return null\n}\n\nfunction handleResponseHeaders (res, proxyRes) {\n  Object.keys(proxyRes.headers).forEach((key) => {\n    if (proxyRes.headers[key] !== undefined) {\n      // let newkey = key.replace(/^[a-z]|-[a-z]/g, (match) => {\n      //   return match.toUpperCase()\n      // })\n      const newkey = key\n      if (key === 'content-length') {\n        // do nothing\n        return\n      }\n      if (key === 'content-security-policy') {\n        // content-security-policy\n        let policy = proxyRes.headers[key]\n        const reg = /script-src ([^:]*);/i\n        const matched = policy.match(reg)\n        if (matched) {\n          if (!matched[1].includes('self')) {\n            policy = policy.replace('script-src', 'script-src \\'self\\' ')\n          }\n        }\n        res.setHeader(newkey, policy)\n        return\n      }\n\n      res.setHeader(newkey, proxyRes.headers[key])\n    }\n  })\n\n  res.writeHead(proxyRes.statusCode)\n}\n\nconst contextPath = '/____ds_script____/'\nconst monkey = require('../../monkey')\n\nmodule.exports = {\n  requestIntercept (context, req, res, ssl, next) {\n    const { rOptions, log, setting } = context\n    if (rOptions.path.indexOf(contextPath) !== 0) {\n      return\n    }\n    const urlPath = rOptions.path\n    let filename = urlPath.replace(contextPath, '')\n\n    // 重命名过，向下兼容\n    if (filename === 'global') {\n      filename = 'tampermonkey'\n    }\n\n    const script = monkey.get(setting.script.defaultDir)[filename]\n    // log.info(`urlPath: ${urlPath}, fileName: ${filename}, script: ${script}`)\n\n    log.info('ds_script, filename:', filename, ', `script != null` =', script != null)\n    const now = new Date()\n    res.writeHead(200, {\n      'DS-Middleware': 'ds_script',\n      'Content-Type': 'application/javascript; charset=utf-8',\n      'Cache-Control': 'public, max-age=86401, immutable', // 缓存1天\n      'Last-Modified': now.toUTCString(),\n      'Expires': new Date(now.getTime() + 86400000).toUTCString(), // 缓存1天\n      'Date': new Date().toUTCString(),\n    })\n    res.write(script.script)\n    res.end()\n    return true\n  },\n  responseInterceptor (req, res, proxyReq, proxyRes, ssl, next, append) {\n    if (append == null || (!append.head && !append.body)) {\n      next()\n      return\n    }\n\n    const isHtml = httpUtil.isHtml(proxyRes)\n    const contentLengthIsZero = (() => {\n      return proxyRes.headers['content-length'] === 0\n    })()\n    if (!isHtml || contentLengthIsZero) {\n      next()\n      return\n    }\n\n    // 先处理头信息\n    handleResponseHeaders(res, proxyRes)\n\n    // 获取响应内容编码\n    const encoding = httpUtil.getContentEncoding(proxyRes)\n    if (encoding) {\n      // 获取编解码器\n      const codec = httpUtil.getCodec(encoding)\n      if (codec) {\n        proxyRes\n          .pipe(codec.createDecompressor()) // 解码\n          .pipe(through(function (chunk, enc, callback) {\n            // 插入head和body\n            chunkByteReplace(this, chunk, enc, callback, append)\n          }))\n          .pipe(codec.createCompressor()) // 编码\n          .pipe(res)\n      } else {\n        log.error(`InsertScriptMiddleware.responseInterceptor(): 暂不支持编码方式 ${encoding}, 目前支持:`, httpUtil.supportedEncodingsStr())\n      }\n    } else {\n      proxyRes\n        .pipe(through(function (chunk, enc, callback) {\n          chunkByteReplace(this, chunk, enc, callback, append)\n        }))\n        .pipe(res)\n    }\n\n    next()\n  },\n  httpUtil,\n  handleResponseHeaders,\n}\n"
  },
  {
    "path": "packages/mitmproxy/src/lib/proxy/middleware/overwall.js",
    "content": "const { Buffer } = require('node:buffer')\nconst fs = require('node:fs')\nconst path = require('node:path')\nconst url = require('node:url')\nconst lodash = require('lodash')\nconst request = require('request')\nconst log = require('../../../utils/util.log.server')\nconst matchUtil = require('../../../utils/util.match')\nconst pac = require('./source/pac')\nconst dateUtil = require('@docmirror/dev-sidecar/src/utils/util.date')\n\nlet pacClient = null\n\nfunction matched (hostname, overWallTargetMap) {\n  // 匹配配置文件\n  const ret1 = matchUtil.matchHostname(overWallTargetMap, hostname, 'matched overwall')\n  if (ret1) {\n    return 'in config'\n  } else if (ret1 === false || ret1 === 'false') {\n    log.debug(`域名 ${hostname} 的overwall配置为 false，跳过增强功能，即使它在 pac.txt 里`)\n    return null\n  }\n\n  // 匹配 pac.txt\n  if (pacClient == null) {\n    return null\n  }\n  const ret = pacClient.FindProxyForURL(`https://${hostname}`, hostname)\n  if (ret && ret.indexOf('PROXY ') === 0) {\n    log.info(`matchHostname: matched overwall: '${hostname}' -> '${ret}' in pac.txt`)\n    return 'in pac.txt'\n  } else {\n    log.debug(`matchHostname: matched overwall: Not-Matched '${hostname}' -> '${ret}' in pac.txt`)\n    return null\n  }\n}\n\nfunction getUserBasePath () {\n  const userHome = process.env.USERPROFILE || process.env.HOME || '/'\n  return path.resolve(userHome, './.dev-sidecar')\n}\n\n// 下载的 pac.txt 文件保存路径\nfunction getTmpPacFilePath () {\n  return path.join(getUserBasePath(), '/pac.txt')\n}\n\nfunction loadPacLastModifiedTime (pacTxt) {\n  const matched = pacTxt.match(/(?<=! Last Modified: )[^\\r\\n]+/g)\n  if (matched && matched.length > 0) {\n    try {\n      return new Date(matched[0])\n    } catch {\n      return null\n    }\n  }\n}\n\n// 保存 pac 内容到 `~/pac.txt` 文件中\nfunction savePacFile (pacTxt) {\n  const pacFilePath = getTmpPacFilePath()\n  try {\n    fs.writeFileSync(pacFilePath, pacTxt)\n    log.info('保存 pac.txt 文件成功:', pacFilePath)\n  } catch (e) {\n    log.error('保存 pac.txt 文件失败:', pacFilePath, ', error:', e)\n    return\n  }\n\n  // 尝试解析和修改 pac.txt 文件时间\n  const lastModifiedTime = loadPacLastModifiedTime(pacTxt)\n  if (lastModifiedTime) {\n    fs.stat(pacFilePath, (err, _stats) => {\n      if (err) {\n        log.error('修改 pac.txt 文件时间失败:', err)\n        return\n      }\n\n      // 修改文件的访问时间和修改时间为当前时间\n      fs.utimes(pacFilePath, lastModifiedTime, lastModifiedTime, (utimesErr) => {\n        if (utimesErr) {\n          log.error('修改 pac.txt 文件时间失败:', utimesErr)\n        } else {\n          log.info(`'${pacFilePath}' 文件的修改时间已更新为其最近更新时间 '${dateUtil.format(lastModifiedTime, false)}'`)\n        }\n      })\n    })\n  }\n}\n\n// 异步下载 pac.txt ，避免影响代理服务的启动速度\nasync function downloadPacAsync (pacConfig) {\n  const remotePacFileUrl = pacConfig.pacFileUpdateUrl\n  log.info('开始下载远程 pac.txt 文件:', remotePacFileUrl)\n  request(remotePacFileUrl, (error, response, body) => {\n    if (error) {\n      log.error(`下载远程 pac.txt 文件失败: ${remotePacFileUrl}, error:`, error, ', response:', response, ', body:', body)\n      return\n    }\n    if (response && response.statusCode === 200) {\n      if (body == null || body.length < 100) {\n        log.warn('下载远程 pac.txt 文件成功，但内容为空或内容太短，判断为无效的 pax.txt 文件:', remotePacFileUrl, ', body:', body)\n        return\n      } else {\n        log.info('下载远程 pac.txt 文件成功:', remotePacFileUrl)\n      }\n\n      // 尝试解析Base64（注：https://gitlab.com/gfwlist/gfwlist/raw/master/gfwlist.txt 下载下来的是Base64格式）\n      let pacTxt = body\n      if (!pacTxt.includes('!---------------------EOF')) {\n        try {\n          pacTxt = Buffer.from(pacTxt, 'base64').toString('utf8')\n          // log.debug('解析 base64 后的 pax:', pacTxt)\n        } catch {\n          log.error(`远程 pac.txt 文件内容即不是base64格式，也不是要求的格式，url: ${remotePacFileUrl}，body: ${body}`)\n          return\n        }\n      }\n\n      // 保存到本地\n      savePacFile(pacTxt)\n    } else {\n      log.error(`下载远程 pac.txt 文件失败: ${remotePacFileUrl}, response:`, response, ', body:', body)\n    }\n  })\n}\n\nfunction createOverwallMiddleware (overWallConfig) {\n  if (!overWallConfig || overWallConfig.enabled !== true) {\n    return null\n  }\n  if (overWallConfig.pac && overWallConfig.pac.enabled) {\n    // 初始化pac\n    pacClient = pac.createPacClient(overWallConfig.pac.pacFileAbsolutePath)\n  }\n\n  let server = overWallConfig.server\n  let keys = Object.keys(server)\n  if (keys.length === 0) {\n    server = overWallConfig.serverDefault\n    keys = Object.keys(server)\n  }\n  if (keys.length === 0) {\n    return null\n  }\n  const overWallTargetMap = matchUtil.domainMapRegexply(overWallConfig.targets)\n  return {\n    sslConnectInterceptor: (req, _cltSocket, _head) => {\n      const hostname = req.url.split(':')[0]\n      return matched(hostname, overWallTargetMap)\n    },\n    requestIntercept (context, req, res, _ssl, _next) {\n      const { rOptions, log, RequestCounter } = context\n      if (rOptions.protocol === 'http:') {\n        return\n      }\n      const hostname = rOptions.hostname\n      const matchedResult = matched(hostname, overWallTargetMap)\n      if (matchedResult == null || matchedResult === false || matchedResult === 'false') {\n        return\n      }\n      const cacheKey = '__over_wall_proxy__'\n      let proxyServer = keys[0]\n      if (RequestCounter && keys.length > 1) {\n        const count = RequestCounter.getOrCreate(cacheKey, keys)\n        if (count.value == null) {\n          count.doRank()\n        }\n        if (count.value == null) {\n          log.error('`count.value` is null, the count:', count)\n        } else {\n          count.doCount(count.value)\n          proxyServer = count.value\n          context.requestCount = {\n            key: cacheKey,\n            value: count.value,\n            count,\n          }\n        }\n      }\n\n      const domain = proxyServer\n      const port = server[domain].port\n      const path = server[domain].path\n      const password = server[domain].password\n      const proxyTarget = `${domain}/${path}/${hostname}${req.url}`\n\n      // const backup = interceptOpt.backup\n      const proxy = proxyTarget.indexOf('http:') === 0 || proxyTarget.indexOf('https:') === 0 ? proxyTarget : (`${rOptions.protocol}//${proxyTarget}`)\n      // eslint-disable-next-line node/no-deprecated-api\n      const URL = url.parse(proxy)\n      rOptions.origional = lodash.cloneDeep(rOptions) // 备份原始请求参数\n      delete rOptions.origional.agent\n      delete rOptions.origional.headers\n      rOptions.protocol = URL.protocol\n      rOptions.hostname = URL.host\n      rOptions.host = URL.host\n      rOptions.headers.host = URL.host\n      if (password) {\n        rOptions.headers.dspassword = password\n      }\n      rOptions.path = URL.path\n      if (URL.port == null) {\n        rOptions.port = port || (rOptions.protocol === 'https:' ? 443 : 80)\n      }\n      log.info('OverWall:', rOptions.hostname, '➜', proxyTarget)\n      if (context.requestCount) {\n        log.debug('OverWall choice:', JSON.stringify(context.requestCount))\n      }\n\n      res.setHeader('DS-Overwall', matchedResult)\n\n      return true\n    },\n  }\n}\n\nmodule.exports = {\n  getTmpPacFilePath,\n  downloadPacAsync,\n  createOverwallMiddleware,\n}\n"
  },
  {
    "path": "packages/mitmproxy/src/lib/proxy/middleware/source/pac.js",
    "content": "const fs = require('node:fs')\nconst path = require('node:path')\nconst log = require('../../../../utils/util.log.server')\n\nfunction createPacClient (pacFilePath) {\n  const __PROXY__ = 'PROXY 127.0.0.1:1080;'\n\n  function readFile (location) {\n    try {\n      log.info('pac root dir:', path.resolve('./'))\n      log.info('pac location:', location)\n      const filePath = path.resolve(location)\n      log.info('read pac path:', filePath)\n      return fs.readFileSync(location).toString()\n    } catch (e) {\n      log.error('读取pac失败:', e)\n      return ''\n    }\n  }\n\n  const getRules = function (pacFilePath) {\n    let text = readFile(pacFilePath)\n    if (!text.includes('!---------------------EOF')) {\n      text = Buffer.from(text, 'base64').toString()\n    }\n    const rules = []\n    const arr = text.split('\\n')\n    for (const line of arr) {\n      const row = line.trim()\n      if (row === '' || row.indexOf('!') === 0 || row.indexOf('[') === 0) {\n        continue\n      }\n      rules.push(row)\n    }\n    return rules\n  }\n  const __RULES__ = getRules(pacFilePath)\n\n  /* eslint-disable */\n  // Was generated by gfwlist2pac in precise mode\n  // https://github.com/clowwindy/gfwlist2pac\n\n  // 2019-10-06: More 'javascript' way to interaction with main program\n  // 2019-02-08: Updated to support shadowsocks-windows user rules.\n\n  const proxy = __PROXY__\n  const rules = []\n\n  // convert to abp grammar\n  for (let i = 0; i < __RULES__.length; i++) {\n    let s = __RULES__[i]\n    if (s.substring(0, 2) === \"||\") s += \"^\"\n    rules.push(s)\n  }\n\n  /*\n  * This file is part of Adblock Plus <http://adblockplus.org/>,\n  * Copyright (C) 2006-2014 Eyeo GmbH\n  *\n  * Adblock Plus is free software: you can redistribute it and/or modify\n  * it under the terms of the GNU General Public License version 3 as\n  * published by the Free Software Foundation.\n  *\n  * Adblock Plus is distributed in the hope that it will be useful,\n  * but WITHOUT ANY WARRANTY; without even the implied warranty of\n  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n  * GNU General Public License for more details.\n  *\n  * You should have received a copy of the GNU General Public License\n  * along with Adblock Plus.  If not, see <http://www.gnu.org/licenses/>.\n  */\n\n  function createDict () {\n    const result = {}\n    result.__proto__ = null\n    return result\n  }\n\n  function getOwnPropertyDescriptor (obj, key) {\n    if (obj.hasOwnProperty(key)) {\n      return obj[key]\n    }\n    return null\n  }\n\n  function extend (subClass, superClass, definition) {\n    if (Object.__proto__) {\n      definition.__proto__ = superClass.prototype\n      subClass.prototype = definition\n    } else {\n      const tmpClass = function () {}\n      tmpClass.prototype = superClass.prototype\n      subClass.prototype = new tmpClass()\n      subClass.prototype.constructor = superClass\n      for (const key in definition) {\n        if (definition.hasOwnProperty(key)) {\n          subClass.prototype[key] = definition[key]\n        }\n      }\n    }\n  }\n\n  function Filter (text) {\n    this.text = text\n    this.subscriptions = []\n  }\n\n  Filter.prototype = {\n    text: null,\n    subscriptions: null,\n    toString: function () {\n      return this.text\n    }\n  }\n  Filter.knownFilters = createDict()\n  Filter.elemhideRegExp = /^([^\\/\\*\\|\\@\"!]*?)#(\\@)?(?:([\\w\\-]+|\\*)((?:\\([\\w\\-]+(?:[$^*]?=[^\\(\\)\"]*)?\\))*)|#([^{}]+))$/\n  Filter.regexpRegExp = /^(@@)?\\/.*\\/(?:\\$~?[\\w\\-]+(?:=[^,\\s]+)?(?:,~?[\\w\\-]+(?:=[^,\\s]+)?)*)?$/\n  Filter.optionsRegExp = /\\$(~?[\\w\\-]+(?:=[^,\\s]+)?(?:,~?[\\w\\-]+(?:=[^,\\s]+)?)*)$/\n  Filter.fromText = function (text) {\n    if (text in Filter.knownFilters) {\n      return Filter.knownFilters[text]\n    }\n    let ret\n    if (text.charAt(0) === \"!\") {\n      ret = new CommentFilter(text)\n    } else {\n      ret = RegExpFilter.fromText(text)\n    }\n    Filter.knownFilters[ret.text] = ret\n    return ret\n  }\n\n  function InvalidFilter (text, reason) {\n    Filter.call(this, text)\n    this.reason = reason\n  }\n\n  extend(InvalidFilter, Filter, {\n    reason: null\n  })\n\n  function CommentFilter (text) {\n    Filter.call(this, text)\n  }\n\n  extend(CommentFilter, Filter, {})\n\n  function ActiveFilter (text, domains) {\n    Filter.call(this, text)\n    this.domainSource = domains\n  }\n\n  extend(ActiveFilter, Filter, {\n    domainSource: null,\n    domainSeparator: null,\n    ignoreTrailingDot: true,\n    domainSourceIsUpperCase: false,\n    getDomains: function () {\n      const prop = getOwnPropertyDescriptor(this, \"domains\")\n      if (prop) {\n        return prop\n      }\n      let domains = null\n      if (this.domainSource) {\n        let source = this.domainSource\n        if (!this.domainSourceIsUpperCase) {\n          source = source.toUpperCase()\n        }\n        const list = source.split(this.domainSeparator)\n        if (list.length === 1 && (list[0]).charAt(0) !== \"~\") {\n          domains = createDict()\n          domains[\"\"] = false\n          if (this.ignoreTrailingDot) {\n            list[0] = list[0].replace(/\\.+$/, \"\")\n          }\n          domains[list[0]] = true\n        } else {\n          let hasIncludes = false\n          for (let i = 0; i < list.length; i++) {\n            let domain = list[i]\n            if (this.ignoreTrailingDot) {\n              domain = domain.replace(/\\.+$/, \"\")\n            }\n            if (domain === \"\") {\n              continue\n            }\n            let include\n            if (domain.charAt(0) === \"~\") {\n              include = false\n              domain = domain.substr(1)\n            } else {\n              include = true\n              hasIncludes = true\n            }\n            if (!domains) {\n              domains = createDict()\n            }\n            domains[domain] = include\n          }\n          domains[\"\"] = !hasIncludes\n        }\n        this.domainSource = null\n      }\n      return this.domains\n    },\n    siteKeys: null,\n    isActiveOnDomain: function (docDomain, siteKey) {\n      if (this.getSiteKeys() && (!siteKey || this.getSiteKeys().indexOf(siteKey.toUpperCase()) < 0)) {\n        return false\n      }\n      if (!this.getDomains()) {\n        return true\n      }\n      if (!docDomain) {\n        return this.getDomains()[\"\"]\n      }\n      if (this.ignoreTrailingDot) {\n        docDomain = docDomain.replace(/\\.+$/, \"\")\n      }\n      docDomain = docDomain.toUpperCase()\n      while (true) {\n        if (docDomain in this.getDomains()) {\n          return this.domains[docDomain]\n        }\n        const nextDot = docDomain.indexOf(\".\")\n        if (nextDot < 0) {\n          break\n        }\n        docDomain = docDomain.substr(nextDot + 1)\n      }\n      return this.domains[\"\"]\n    }/*,\n    isActiveOnlyOnDomain: function (docDomain) {\n      if (!docDomain || !this.getDomains() || this.getDomains()[\"\"]) {\n        return false\n      }\n      if (this.ignoreTrailingDot) {\n        docDomain = docDomain.replace(/\\.+$/, \"\")\n      }\n      docDomain = docDomain.toUpperCase()\n      for (const domain in this.getDomains()) {\n        if (this.domains[domain] && domain != docDomain && (domain.length <= docDomain.length || domain.indexOf(\".\" + docDomain) != domain.length - docDomain.length - 1)) {\n          return false\n        }\n      }\n      return true\n    }*/\n  })\n\n  function RegExpFilter (text, regexpSource, contentType, matchCase, domains, thirdParty, siteKeys) {\n    ActiveFilter.call(this, text, domains, siteKeys)\n    if (contentType != null) {\n      this.contentType = contentType\n    }\n    if (matchCase) {\n      this.matchCase = matchCase\n    }\n    if (thirdParty != null) {\n      this.thirdParty = thirdParty\n    }\n    if (siteKeys != null) {\n      this.siteKeySource = siteKeys\n    }\n    if (regexpSource.length >= 2 && regexpSource.charAt(0) === \"/\" && regexpSource.charAt(regexpSource.length - 1) === \"/\") {\n      this.regexp = new RegExp(regexpSource.substr(1, regexpSource.length - 2), this.matchCase ? \"\" : \"i\")\n    } else {\n      this.regexpSource = regexpSource\n    }\n  }\n\n  extend(RegExpFilter, ActiveFilter, {\n    domainSourceIsUpperCase: true,\n    length: 1,\n    domainSeparator: \"|\",\n    regexpSource: null,\n    getRegexp: function () {\n      const prop = getOwnPropertyDescriptor(this, \"regexp\")\n      if (prop) {\n        return prop\n      }\n      const source = this.regexpSource.replace(/\\*+/g, \"*\").replace(/\\^\\|$/, \"^\").replace(/\\W/g, \"\\\\$&\").replace(/\\\\\\*/g, \".*\").replace(/\\\\\\^/g, \"(?:[\\\\x00-\\\\x24\\\\x26-\\\\x2C\\\\x2F\\\\x3A-\\\\x40\\\\x5B-\\\\x5E\\\\x60\\\\x7B-\\\\x7F]|$)\").replace(/^\\\\\\|\\\\\\|/, \"^[\\\\w\\\\-]+:\\\\/+(?!\\\\/)(?:[^\\\\/]+\\\\.)?\").replace(/^\\\\\\|/, \"^\").replace(/\\\\\\|$/, \"$\").replace(/^(\\.\\*)/, \"\").replace(/(\\.\\*)$/, \"\")\n      const regexp = new RegExp(source, this.matchCase ? \"\" : \"i\")\n      this.regexp = regexp\n      return regexp\n    },\n    contentType: 2147483647,\n    matchCase: false,\n    thirdParty: null,\n    siteKeySource: null,\n    getSiteKeys: function () {\n      const prop = getOwnPropertyDescriptor(this, \"siteKeys\")\n      if (prop) {\n        return prop\n      }\n      let siteKeys = null\n      if (this.siteKeySource) {\n        siteKeys = this.siteKeySource.split(\"|\")\n        this.siteKeySource = null\n      }\n      this.siteKeys = siteKeys\n      return this.siteKeys\n    },\n    matches: function (location, contentType, docDomain, thirdParty, siteKey) {\n      return !!(this.getRegexp().test(location) && this.isActiveOnDomain(docDomain, siteKey))\n    }\n  })\n  RegExpFilter.prototype[\"0\"] = \"#this\"\n  RegExpFilter.fromText = function (text) {\n    let blocking = true\n    const origText = text\n    if (text.indexOf(\"@@\") === 0) {\n      blocking = false\n      text = text.substr(2)\n    }\n    let contentType = null\n    let matchCase = null\n    let domains = null\n    let siteKeys = null\n    let thirdParty = null\n    let collapse = null\n    let options\n    const match = text.indexOf(\"$\") >= 0 ? Filter.optionsRegExp.exec(text) : null\n    if (match) {\n      options = match[1].toUpperCase().split(\",\")\n      text = match.input.substr(0, match.index)\n      for (let _loopIndex6 = 0; _loopIndex6 < options.length; ++_loopIndex6) {\n        let option = options[_loopIndex6]\n        let value = null\n        const separatorIndex = option.indexOf(\"=\")\n        if (separatorIndex >= 0) {\n          value = option.substr(separatorIndex + 1)\n          option = option.substr(0, separatorIndex)\n        }\n        option = option.replace(/-/, \"_\")\n        if (option in RegExpFilter.typeMap) {\n          if (contentType == null) {\n            contentType = 0\n          }\n          contentType |= RegExpFilter.typeMap[option]\n        } else if (option.charAt(0) === \"~\" && option.substr(1) in RegExpFilter.typeMap) {\n          if (contentType == null) {\n            contentType = RegExpFilter.prototype.contentType\n          }\n          contentType &= ~RegExpFilter.typeMap[option.substr(1)]\n        } else if (option === \"MATCH_CASE\") {\n          matchCase = true\n        } else if (option === \"~MATCH_CASE\") {\n          matchCase = false\n        } else if (option === \"DOMAIN\" && typeof value != \"undefined\") {\n          domains = value\n        } else if (option === \"THIRD_PARTY\") {\n          thirdParty = true\n        } else if (option === \"~THIRD_PARTY\") {\n          thirdParty = false\n        } else if (option === \"COLLAPSE\") {\n          collapse = true\n        } else if (option === \"~COLLAPSE\") {\n          collapse = false\n        } else if (option === \"SITEKEY\" && typeof value != \"undefined\") {\n          siteKeys = value\n        } else {\n          return new InvalidFilter(origText, \"Unknown option \" + option.toLowerCase())\n        }\n      }\n    }\n    if (!blocking && (contentType == null || contentType & RegExpFilter.typeMap.DOCUMENT) && (!options || options.indexOf(\"DOCUMENT\") < 0) && !/^\\|?[\\w\\-]+:/.test(text)) {\n      if (contentType == null) {\n        contentType = RegExpFilter.prototype.contentType\n      }\n      contentType &= ~RegExpFilter.typeMap.DOCUMENT\n    }\n    try {\n      if (blocking) {\n        return new BlockingFilter(origText, text, contentType, matchCase, domains, thirdParty, siteKeys, collapse)\n      } else {\n        return new WhitelistFilter(origText, text, contentType, matchCase, domains, thirdParty, siteKeys)\n      }\n    } catch (e) {\n      return new InvalidFilter(origText, e)\n    }\n  }\n  RegExpFilter.typeMap = {\n    OTHER: 1,\n    SCRIPT: 2,\n    IMAGE: 4,\n    STYLESHEET: 8,\n    OBJECT: 16,\n    SUBDOCUMENT: 32,\n    DOCUMENT: 64,\n    XBL: 1,\n    PING: 1,\n    XMLHTTPREQUEST: 2048,\n    OBJECT_SUBREQUEST: 4096,\n    DTD: 1,\n    MEDIA: 16384,\n    FONT: 32768,\n    BACKGROUND: 4,\n    POPUP: 268435456,\n    ELEMHIDE: 1073741824\n  }\n  RegExpFilter.prototype.contentType &= ~(RegExpFilter.typeMap.ELEMHIDE | RegExpFilter.typeMap.POPUP)\n\n  function BlockingFilter (text, regexpSource, contentType, matchCase, domains, thirdParty, siteKeys, collapse) {\n    RegExpFilter.call(this, text, regexpSource, contentType, matchCase, domains, thirdParty, siteKeys)\n    this.collapse = collapse\n  }\n\n  extend(BlockingFilter, RegExpFilter, {\n    collapse: null\n  })\n\n  function WhitelistFilter (text, regexpSource, contentType, matchCase, domains, thirdParty, siteKeys) {\n    RegExpFilter.call(this, text, regexpSource, contentType, matchCase, domains, thirdParty, siteKeys)\n  }\n\n  extend(WhitelistFilter, RegExpFilter, {})\n\n  function Matcher () {\n    this.clear()\n  }\n\n  Matcher.prototype = {\n    filterByKeyword: null,\n    keywordByFilter: null,\n    clear: function () {\n      this.filterByKeyword = createDict()\n      this.keywordByFilter = createDict()\n    },\n    add: function (filter) {\n      if (filter.text in this.keywordByFilter) {\n        return\n      }\n      const keyword = this.findKeyword(filter)\n      const oldEntry = this.filterByKeyword[keyword]\n      if (typeof oldEntry == \"undefined\") {\n        this.filterByKeyword[keyword] = filter\n      } else if (oldEntry.length === 1) {\n        this.filterByKeyword[keyword] = [oldEntry, filter]\n      } else {\n        oldEntry.push(filter)\n      }\n      this.keywordByFilter[filter.text] = keyword\n    },\n    remove: function (filter) {\n      if (!(filter.text in this.keywordByFilter)) {\n        return\n      }\n      const keyword = this.keywordByFilter[filter.text]\n      const list = this.filterByKeyword[keyword]\n      if (list.length <= 1) {\n        delete this.filterByKeyword[keyword]\n      } else {\n        const index = list.indexOf(filter)\n        if (index >= 0) {\n          list.splice(index, 1)\n          if (list.length === 1) {\n            this.filterByKeyword[keyword] = list[0]\n          }\n        }\n      }\n      delete this.keywordByFilter[filter.text]\n    },\n    findKeyword: function (filter) {\n      let result = \"\"\n      let text = filter.text\n      if (Filter.regexpRegExp.test(text)) {\n        return result\n      }\n      const match = Filter.optionsRegExp.exec(text)\n      if (match) {\n        text = match.input.substr(0, match.index)\n      }\n      if (text.substr(0, 2) === \"@@\") {\n        text = text.substr(2)\n      }\n      const candidates = text.toLowerCase().match(/[^a-z0-9%*][a-z0-9%]{3,}(?=[^a-z0-9%*])/g)\n      if (!candidates) {\n        return result\n      }\n      const hash = this.filterByKeyword\n      let resultCount = 16777215\n      let resultLength = 0\n      for (let i = 0, l = candidates.length; i < l; i++) {\n        const candidate = candidates[i].substr(1)\n        const count = candidate in hash ? hash[candidate].length : 0\n        if (count < resultCount || count === resultCount && candidate.length > resultLength) {\n          result = candidate\n          resultCount = count\n          resultLength = candidate.length\n        }\n      }\n      return result\n    },\n    hasFilter: function (filter) {\n      return filter.text in this.keywordByFilter\n    },\n    getKeywordForFilter: function (filter) {\n      if (filter.text in this.keywordByFilter) {\n        return this.keywordByFilter[filter.text]\n      } else {\n        return null\n      }\n    },\n    _checkEntryMatch: function (keyword, location, contentType, docDomain, thirdParty, siteKey) {\n      const list = this.filterByKeyword[keyword]\n      for (let i = 0; i < list.length; i++) {\n        let filter = list[i]\n        if (filter === \"#this\") {\n          filter = list\n        }\n        if (filter.matches(location, contentType, docDomain, thirdParty, siteKey)) {\n          return filter\n        }\n      }\n      return null\n    }/*,\n    matchesAny: function (location, contentType, docDomain, thirdParty, siteKey) {\n      let candidates = location.toLowerCase().match(/[a-z0-9%]{3,}/g)\n      if (candidates === null) {\n        candidates = []\n      }\n      candidates.push(\"\")\n      for (let i = 0, l = candidates.length; i < l; i++) {\n        const substr = candidates[i]\n        if (substr in this.filterByKeyword) {\n          const result = this._checkEntryMatch(substr, location, contentType, docDomain, thirdParty, siteKey)\n          if (result) {\n            return result\n          }\n        }\n      }\n      return null\n    }*/\n  }\n\n  function CombinedMatcher () {\n    this.blacklist = new Matcher()\n    this.whitelist = new Matcher()\n    this.resultCache = createDict()\n  }\n\n  CombinedMatcher.maxCacheEntries = 1000\n  CombinedMatcher.prototype = {\n    blacklist: null,\n    whitelist: null,\n    resultCache: null,\n    cacheEntries: 0,\n    clear: function () {\n      this.blacklist.clear()\n      this.whitelist.clear()\n      this.resultCache = createDict()\n      this.cacheEntries = 0\n    },\n    add: function (filter) {\n      if (filter instanceof WhitelistFilter) {\n        this.whitelist.add(filter)\n      } else {\n        this.blacklist.add(filter)\n      }\n      if (this.cacheEntries > 0) {\n        this.resultCache = createDict()\n        this.cacheEntries = 0\n      }\n    },\n    remove: function (filter) {\n      if (filter instanceof WhitelistFilter) {\n        this.whitelist.remove(filter)\n      } else {\n        this.blacklist.remove(filter)\n      }\n      if (this.cacheEntries > 0) {\n        this.resultCache = createDict()\n        this.cacheEntries = 0\n      }\n    },\n    findKeyword: function (filter) {\n      if (filter instanceof WhitelistFilter) {\n        return this.whitelist.findKeyword(filter)\n      } else {\n        return this.blacklist.findKeyword(filter)\n      }\n    },\n    hasFilter: function (filter) {\n      if (filter instanceof WhitelistFilter) {\n        return this.whitelist.hasFilter(filter)\n      } else {\n        return this.blacklist.hasFilter(filter)\n      }\n    },\n    getKeywordForFilter: function (filter) {\n      if (filter instanceof WhitelistFilter) {\n        return this.whitelist.getKeywordForFilter(filter)\n      } else {\n        return this.blacklist.getKeywordForFilter(filter)\n      }\n    },\n    /*isSlowFilter: function (filter) {\n      const matcher = filter instanceof WhitelistFilter ? this.whitelist : this.blacklist\n      if (matcher.hasFilter(filter)) {\n        return !matcher.getKeywordForFilter(filter)\n      } else {\n        return !matcher.findKeyword(filter)\n      }\n    },*/\n    matchesAnyInternal: function (location, contentType, docDomain, thirdParty, siteKey) {\n      let candidates = location.toLowerCase().match(/[a-z0-9%]{3,}/g)\n      if (candidates === null) {\n        candidates = []\n      }\n      candidates.push(\"\")\n      let blacklistHit = null\n      for (let i = 0, l = candidates.length; i < l; i++) {\n        const substr = candidates[i]\n        if (substr in this.whitelist.filterByKeyword) {\n          const result = this.whitelist._checkEntryMatch(substr, location, contentType, docDomain, thirdParty, siteKey)\n          if (result) {\n            return result\n          }\n        }\n        if (substr in this.blacklist.filterByKeyword && blacklistHit === null) {\n          blacklistHit = this.blacklist._checkEntryMatch(substr, location, contentType, docDomain, thirdParty, siteKey)\n        }\n      }\n      return blacklistHit\n    },\n    matchesAny: function (location, docDomain) {\n      const key = location + \" \" + docDomain + \" \"\n      if (key in this.resultCache) {\n        return this.resultCache[key]\n      }\n      const result = this.matchesAnyInternal(location, 0, docDomain, null, null)\n      if (this.cacheEntries >= CombinedMatcher.maxCacheEntries) {\n        this.resultCache = createDict()\n        this.cacheEntries = 0\n      }\n      this.resultCache[key] = result\n      this.cacheEntries++\n      return result\n    }\n  }\n\n  const userrulesMatcher = new CombinedMatcher()\n  const defaultMatcher = new CombinedMatcher()\n\n  const direct = 'DIRECT;'\n\n  for (let i = 0; i < rules.length; i++) {\n    defaultMatcher.add(Filter.fromText(rules[i]))\n  }\n\n  function FindProxyForURL (url, host) {\n    let matchedResult = userrulesMatcher.matchesAny(url, host)\n    if (matchedResult instanceof BlockingFilter) {\n      return proxy\n    } else if (matchedResult instanceof WhitelistFilter) {\n      return direct\n    }\n\n    // Hack for Geosite, it provides a whitelist...\n    matchedResult = defaultMatcher.matchesAny(url, host)\n    if (matchedResult instanceof BlockingFilter) {\n      return proxy\n    } else if (matchedResult instanceof WhitelistFilter) {\n      return direct\n    }\n\n    return direct\n  }\n\n\n  return {\n    FindProxyForURL,\n    proxyUrl: __PROXY__\n  }\n}\n\nmodule.exports = {\n  createPacClient\n}\n"
  },
  {
    "path": "packages/mitmproxy/src/lib/proxy/mitmproxy/createConnectHandler.js",
    "content": "const net = require('node:net')\nconst url = require('node:url')\nconst jsonApi = require('../../../json')\nconst log = require('../../../utils/util.log.server')\nconst DnsUtil = require('../../dns')\nconst dnsLookup = require('./dnsLookup')\n\nconst localIP = '127.0.0.1'\n\nfunction isSslConnect (sslConnectInterceptors, req, cltSocket, head) {\n  for (const intercept of sslConnectInterceptors) {\n    const ret = intercept(req, cltSocket, head)\n    log.debug('当前拦截器返回结果：', ret, `, url: ${req.url}, intercept:`, intercept)\n    if (ret == null) {\n      continue\n    }\n    return !(ret === false || ret === 'false')\n  }\n  return false\n}\n\n// create connectHandler function\nmodule.exports = function createConnectHandler (sslConnectInterceptor, middlewares, fakeServerCenter, dnsConfig, compatibleConfig) {\n  // return\n  const sslConnectInterceptors = []\n  sslConnectInterceptors.push(sslConnectInterceptor)\n  for (const middleware of middlewares) {\n    if (middleware.sslConnectInterceptor) {\n      sslConnectInterceptors.push(middleware.sslConnectInterceptor)\n    }\n  }\n\n  return function connectHandler (req, cltSocket, head, ssl) {\n    // eslint-disable-next-line node/no-deprecated-api\n    let { hostname, port } = url.parse(`${ssl ? 'https' : 'http'}://${req.url}`)\n    port = Number.parseInt(port)\n\n    if (isSslConnect(sslConnectInterceptors, req, cltSocket, head)) {\n      // 需要拦截，代替目标服务器，让客户端连接DS在本地启动的代理服务\n      fakeServerCenter.getServerPromise(hostname, port, ssl, compatibleConfig).then((serverObj) => {\n        log.info(`----- fakeServer connect: ${localIP}:${serverObj.port} ➜ ${req.url} -----`)\n        connect(req, cltSocket, head, localIP, serverObj.port, null, false, hostname)\n      }, (e) => {\n        log.error(`----- fakeServer getServerPromise error: ${hostname}:${port}, error:`, e)\n      }).catch((e) => {\n        log.error(`----- fakeServer getServerPromise error: ${hostname}:${port}, error:`, e)\n      })\n    } else {\n      log.info(`不拦截请求，直连目标服务器: ${hostname}:${port}, headers:`, jsonApi.stringify2(req.headers))\n      connect(req, cltSocket, head, hostname, port, dnsConfig, true)\n    }\n  }\n}\n\nfunction connect (req, cltSocket, head, hostname, port, dnsConfig = null, isDirect = false, target = null) {\n  // tunneling https\n  // log.info('connect:', hostname, port)\n  const start = Date.now()\n  const isDnsIntercept = {}\n  const hostport = `${hostname}:${port}`\n\n  // 用于记录日志\n  const connectInfo = isDirect ? hostport : `fakeServer: ${hostport}, target: ${target}`\n\n  try {\n    // 客户端的连接事件监听\n    cltSocket.on('timeout', (e) => {\n      log.error(`cltSocket timeout: ${connectInfo}, errorMsg: ${e.message}`)\n    })\n    cltSocket.on('error', (e) => {\n      log.error(`cltSocket error:   ${connectInfo}, errorMsg: ${e.message}`)\n    })\n    // 开发过程中，如有需要可以将此参数临时改为true，打印所有事件的日志\n    const printDebugLog = process.env.NODE_ENV === 'development' && false\n    if (printDebugLog) {\n      cltSocket.on('close', (hadError) => {\n        log.debug('【cltSocket close】', hadError)\n      })\n      cltSocket.on('connect', () => {\n        log.debug('【cltSocket connect】')\n      })\n      cltSocket.on('connectionAttempt', (ip, port, family) => {\n        log.debug(`【cltSocket connectionAttempt】${ip}:${port}: ${connectInfo}, family:`, family)\n      })\n      cltSocket.on('connectionAttemptFailed', (ip, port, family) => {\n        log.debug(`【cltSocket connectionAttemptFailed】${ip}:${port}: ${connectInfo}, family:`, family)\n      })\n      cltSocket.on('connectionAttemptTimeout', (ip, port, family) => {\n        log.debug(`【cltSocket connectionAttemptTimeout】${ip}:${port}: ${connectInfo}, family:`, family)\n      })\n      cltSocket.on('data', (_data) => {\n        log.debug(`【cltSocket data】${connectInfo}`)\n      })\n      cltSocket.on('drain', () => {\n        log.debug(`【cltSocket drain】${connectInfo}`)\n      })\n      cltSocket.on('end', () => {\n        log.debug(`【cltSocket end】${connectInfo}`)\n      })\n      // cltSocket.on('lookup', (err, address, family, host) => {\n      // })\n      cltSocket.on('ready', () => {\n        log.debug(`【cltSocket ready】${connectInfo}`)\n      })\n    }\n\n    // ---------------------------------------------------------------------------------------------------\n\n    const options = {\n      port,\n      host: hostname,\n      connectTimeout: 10000,\n    }\n    if (dnsConfig && dnsConfig.dnsMap) {\n      const dns = DnsUtil.hasDnsLookup(dnsConfig, hostname)\n      if (dns) {\n        options.lookup = dnsLookup.createLookupFunc(null, dns, 'connect', hostport, port, isDnsIntercept)\n      }\n    }\n    // 代理连接事件监听\n    const proxySocket = net.connect(options, () => {\n      if (!isDirect) {\n        log.info(`Proxy connect start: ${hostport}`)\n      } else {\n        log.debug('Direct connect start:', hostport)\n      }\n\n      cltSocket.write('HTTP/1.1 200 Connection Established\\r\\n'\n        + 'Proxy-agent: dev-sidecar\\r\\n'\n        + '\\r\\n')\n      proxySocket.write(head)\n      proxySocket.pipe(cltSocket)\n\n      cltSocket.pipe(proxySocket)\n    })\n    proxySocket.on('timeout', () => {\n      const cost = Date.now() - start\n      const errorMsg = `${isDirect ? '直连' : '代理连接'}超时: ${hostport}, cost: ${cost} ms`\n      log.error(errorMsg)\n\n      cltSocket.destroy()\n\n      if (isDnsIntercept && isDnsIntercept.dns && isDnsIntercept.ip !== isDnsIntercept.hostname) {\n        const { dns, ip, hostname } = isDnsIntercept\n        dns.count(hostname, ip, true)\n        log.error(`记录ip失败次数，用于优选ip！ hostname: ${hostname}, ip: ${ip}, reason: ${errorMsg}, dns: ${dns.dnsName}`)\n      }\n    })\n    proxySocket.on('error', (e) => {\n      // 连接失败，可能被GFW拦截，或者服务端拥挤\n      const cost = Date.now() - start\n      const errorMsg = `${isDirect ? '直连' : '代理连接'}失败: ${hostport}, cost: ${cost} ms, errorMsg: ${e.message}`\n      log.error(`${errorMsg}\\r\\n`, e)\n\n      cltSocket.destroy()\n\n      if (isDnsIntercept && isDnsIntercept.dns && isDnsIntercept.ip !== isDnsIntercept.hostname) {\n        const { dns, ip, hostname } = isDnsIntercept\n        dns.count(hostname, ip, true)\n        log.error(`记录ip失败次数，用于优选ip！ hostname: ${hostname}, ip: ${ip}, reason: ${errorMsg}, dns: ${dns.dnsName}`)\n      }\n    })\n\n    if (printDebugLog) {\n      proxySocket.on('close', (hadError) => {\n        log.debug('【proxySocket close】', hadError)\n      })\n      proxySocket.on('connect', () => {\n        log.debug('【proxySocket connect】')\n      })\n      proxySocket.on('connectionAttempt', (ip, port, family) => {\n        log.debug(`【proxySocket connectionAttempt】${ip}:${port}, family:`, family)\n      })\n      proxySocket.on('connectionAttemptFailed', (ip, port, family) => {\n        log.debug(`【proxySocket connectionAttemptFailed】${ip}:${port}, family:`, family)\n      })\n      proxySocket.on('connectionAttemptTimeout', (ip, port, family) => {\n        log.debug(`【proxySocket connectionAttemptTimeout】${ip}:${port}, family:`, family)\n      })\n      proxySocket.on('data', (_data) => {\n        log.debug('【proxySocket data】')\n      })\n      proxySocket.on('drain', () => {\n        log.debug('【proxySocket drain】')\n      })\n      proxySocket.on('end', () => {\n        log.debug('【proxySocket end】')\n      })\n      // proxySocket.on('lookup', (err, address, family, host) => {\n      // })\n      proxySocket.on('ready', () => {\n        log.debug('【proxySocket ready】')\n      })\n    }\n\n    return proxySocket\n  } catch (e) {\n    log.error(`${isDirect ? '直连' : '代理连接'}错误: ${hostport}, error:`, e)\n  }\n}\n"
  },
  {
    "path": "packages/mitmproxy/src/lib/proxy/mitmproxy/createFakeServerCenter.js",
    "content": "const fs = require('node:fs')\nconst forge = require('node-forge')\nconst log = require('../../../utils/util.log.server')\nconst FakeServersCenter = require('../tls/FakeServersCenter')\n\nmodule.exports = function createFakeServerCenter ({\n  maxLength,\n  caCertPath,\n  caKeyPath,\n  requestHandler,\n  upgradeHandler,\n  getCertSocketTimeout,\n}) {\n  let caCert\n  let caKey\n  try {\n    fs.accessSync(caCertPath, fs.F_OK)\n    fs.accessSync(caKeyPath, fs.F_OK)\n    const caCertPem = fs.readFileSync(caCertPath)\n    const caKeyPem = fs.readFileSync(caKeyPath)\n    caCert = forge.pki.certificateFromPem(caCertPem)\n    caKey = forge.pki.privateKeyFromPem(caKeyPem)\n  } catch (e) {\n    log.error('Can not find `CA certificate` or `CA key`:', e)\n    process.exit(1)\n  }\n\n  return new FakeServersCenter({\n    caCert,\n    caKey,\n    maxLength,\n    requestHandler,\n    upgradeHandler,\n    getCertSocketTimeout,\n  })\n}\n"
  },
  {
    "path": "packages/mitmproxy/src/lib/proxy/mitmproxy/createRequestHandler.js",
    "content": "const http = require('node:http')\nconst https = require('node:https')\nconst jsonApi = require('../../../json')\nconst log = require('../../../utils/util.log.server')\nconst RequestCounter = require('../../choice/RequestCounter')\nconst commonUtil = require('../common/util')\n// const upgradeHeader = /(^|,)\\s*upgrade\\s*($|,)/i\nconst DnsUtil = require('../../dns')\nconst compatible = require('../compatible/compatible')\nconst InsertScriptMiddleware = require('../middleware/InsertScriptMiddleware')\nconst dnsLookup = require('./dnsLookup')\n\nconst MAX_SLOW_TIME = 8000 // 超过此时间 则认为太慢了\n\n// create requestHandler function\nmodule.exports = function createRequestHandler (createIntercepts, middlewares, externalProxy, dnsConfig, setting, compatibleConfig) {\n  // return\n  return function requestHandler (req, res, ssl) {\n    let proxyReq\n\n    const rOptions = commonUtil.getOptionsFromRequest(req, ssl, externalProxy, setting, compatibleConfig)\n    let url = `${rOptions.method} ➜ ${rOptions.protocol}//${rOptions.hostname}:${rOptions.port}${rOptions.path}`\n\n    if (rOptions.headers.connection === 'close') {\n      req.socket.setKeepAlive(false)\n    } else if (rOptions.customSocketId != null) { // for NTLM\n      req.socket.setKeepAlive(true, 60 * 60 * 1000)\n    } else {\n      req.socket.setKeepAlive(true, 30000)\n    }\n    const context = {\n      rOptions,\n      log,\n      RequestCounter,\n      setting,\n    }\n    let interceptors = createIntercepts(context)\n    if (interceptors == null) {\n      interceptors = []\n    }\n    const reqIncpts = interceptors.filter((item) => {\n      return item.requestIntercept != null\n    })\n    const resIncpts = interceptors.filter((item) => {\n      return item.responseIntercept != null\n    })\n\n    const requestInterceptorPromise = () => {\n      return new Promise((resolve, reject) => {\n        const next = () => {\n          resolve()\n        }\n        try {\n          if (setting.script.enabled) {\n            reqIncpts.unshift(InsertScriptMiddleware)\n          }\n          for (const middleware of middlewares) {\n            reqIncpts.push(middleware)\n          }\n          if (reqIncpts && reqIncpts.length > 0) {\n            for (const reqIncpt of reqIncpts) {\n              if (!reqIncpt.requestIntercept) {\n                continue\n              }\n              const goNext = reqIncpt.requestIntercept(context, req, res, ssl, next)\n              if (goNext) {\n                if (goNext !== 'no-next') {\n                  next()\n                }\n                return\n              }\n            }\n            next()\n          } else {\n            next()\n          }\n        } catch (e) {\n          reject(e)\n        }\n      })\n    }\n\n    function countSlow (isDnsIntercept, reason) {\n      if (isDnsIntercept && isDnsIntercept.dns && isDnsIntercept.ip !== isDnsIntercept.hostname) {\n        const { dns, ip, hostname } = isDnsIntercept\n        dns.count(hostname, ip, true)\n        log.error(`记录ip失败次数，用于优选ip！ hostname: ${hostname}, ip: ${ip}, reason: ${reason}, dns: ${dns.dnsName}`)\n      }\n      const counter = context.requestCount\n      if (counter != null) {\n        counter.count.doCount(counter.value, true)\n        log.error(`记录Proxy请求失败次数，用于切换备选域名！ hostname: ${counter.value}, reason: ${reason}, counter.count:`, counter.count)\n      }\n    }\n\n    const proxyRequestPromise = async () => {\n      rOptions.host = rOptions.hostname || rOptions.host || 'localhost'\n      return new Promise((resolve, reject) => {\n        // use the binded socket for NTLM\n        if (rOptions.agent && rOptions.customSocketId != null && rOptions.agent.getName) {\n          const socketName = rOptions.agent.getName(rOptions)\n          const bindingSocket = rOptions.agent.sockets[socketName]\n          if (bindingSocket && bindingSocket.length > 0) {\n            bindingSocket[0].once('free', onFree)\n            return\n          }\n        }\n        onFree()\n\n        function onFree () {\n          url = `${rOptions.method} ➜ ${rOptions.protocol}//${rOptions.hostname}:${rOptions.port}${rOptions.path}`\n          const start = Date.now()\n          log.info('发起代理请求:', url, (rOptions.servername ? `, sni: ${rOptions.servername}` : ''), ', headers:', jsonApi.stringify2(rOptions.headers))\n\n          const isDnsIntercept = {}\n          if (dnsConfig && dnsConfig.dnsMap) {\n            let dns = DnsUtil.hasDnsLookup(dnsConfig, rOptions.hostname)\n            if (!dns && rOptions.servername) {\n              dns = dnsConfig.dnsMap.ForSNI\n              if (dns) {\n                log.info(`域名 ${rOptions.hostname} 在dns中未配置，但使用了 sni: ${rOptions.servername}, 必须使用dns，现默认使用 '${dns.dnsName}' DNS.`)\n              } else {\n                log.warn(`域名 ${rOptions.hostname} 在dns中未配置，但使用了 sni: ${rOptions.servername}，且DNS服务管理中，也未指定SNI默认使用的DNS。`)\n              }\n            }\n            if (dns) {\n              rOptions.lookup = dnsLookup.createLookupFunc(res, dns, 'request url', url, rOptions.port, isDnsIntercept)\n              log.debug(`域名 ${rOptions.hostname} DNS: ${dns.dnsName}`)\n              res.setHeader('DS-DNS', dns.dnsName)\n            } else {\n              log.info(`域名 ${rOptions.hostname} 在DNS中未配置`)\n            }\n          } else {\n            log.info(`域名 ${rOptions.hostname} DNS配置不存在`)\n          }\n\n          // rOptions.sigalgs = 'RSA-PSS+SHA256:RSA-PSS+SHA512:ECDSA+SHA256'\n          // rOptions.agent.options.sigalgs = rOptions.sigalgs\n          // rOptions.ciphers = 'TLS_CHACHA20_POLY1305_SHA256:TLS_AES_128_GCM_SHA256:TLS_AES_256_GCM_SHA384:DHE-RSA-AES128-SHA256:DHE-RSA-AES256-SHA384:DHE-RSA-AES256-SHA256:ECDHE-RSA-AES256-SHA256:HIGH'\n          // rOptions.agent.options.ciphers = rOptions.ciphers\n          // log.debug('rOptions:', rOptions.hostname + rOptions.path, '\\r\\n', rOptions)\n          // log.debug('agent:', rOptions.agent)\n          // log.debug('agent.options:', rOptions.agent.options)\n          res.setHeader('DS-Proxy-Request', rOptions.hostname)\n\n          // 自动兼容程序：2\n          if (rOptions.agent) {\n            const compatibleConfig = compatible.getRequestCompatibleConfig(rOptions, rOptions.compatibleConfig)\n            if (compatibleConfig && compatibleConfig.rejectUnauthorized != null && rOptions.agent.options.rejectUnauthorized !== compatibleConfig.rejectUnauthorized) {\n              if (compatibleConfig.rejectUnauthorized === false && rOptions.agent.unVerifySslAgent) {\n                log.info(`【自动兼容程序】${rOptions.hostname}:${rOptions.port}: 设置 'rOptions.agent.options.rejectUnauthorized = ${compatibleConfig.rejectUnauthorized}'`)\n                rOptions.agent = rOptions.agent.unVerifySslAgent\n                res.setHeader('DS-Compatible', 'unVerifySsl')\n              }\n            }\n          }\n\n          proxyReq = (rOptions.protocol === 'https:' ? https : http).request(rOptions, (proxyRes) => {\n            const cost = Date.now() - start\n            if (rOptions.protocol === 'https:') {\n              log.info(`代理请求返回: 【${proxyRes.statusCode}】${url}, cost: ${cost} ms`)\n            } else {\n              log.info(`请求返回: 【${proxyRes.statusCode}】${url}, cost: ${cost} ms`)\n            }\n            // log.info('request:', proxyReq, proxyReq.socket)\n\n            if (cost > MAX_SLOW_TIME) {\n              countSlow(isDnsIntercept, `代理请求成功但太慢, cost: ${cost} ms > ${MAX_SLOW_TIME} ms`)\n            }\n\n            resolve(proxyRes)\n          })\n\n          // 代理请求的事件监听\n          proxyReq.on('timeout', () => {\n            const cost = Date.now() - start\n            const errorMsg = `代理请求超时: ${url}, cost: ${cost} ms`\n            log.error(errorMsg, ', rOptions:', jsonApi.stringify2(rOptions))\n            countSlow(isDnsIntercept, `代理请求超时, cost: ${cost} ms`)\n            proxyReq.end()\n            proxyReq.destroy()\n            const error = new Error(errorMsg)\n            error.status = 408\n            reject(error)\n          })\n          proxyReq.on('error', (e) => {\n            const cost = Date.now() - start\n            log.error(`代理请求错误: ${url}, cost: ${cost} ms, error:`, e, ', rOptions:', jsonApi.stringify2(rOptions))\n            countSlow(isDnsIntercept, `代理请求错误: ${e.message}`)\n            reject(e)\n\n            // 自动兼容程序：2\n            if (e.code === 'DEPTH_ZERO_SELF_SIGNED_CERT') {\n              compatible.setRequestRejectUnauthorized(rOptions, false)\n            }\n          })\n          proxyReq.on('aborted', () => {\n            const cost = Date.now() - start\n            const errorMsg = `代理请求被取消: ${url}, cost: ${cost} ms`\n            log.error(errorMsg, ', rOptions:', jsonApi.stringify2(rOptions))\n\n            if (cost > MAX_SLOW_TIME) {\n              countSlow(isDnsIntercept, `代理请求被取消，且请求太慢, cost: ${cost} ms > ${MAX_SLOW_TIME} ms`)\n            }\n\n            if (res.writableEnded) {\n              return\n            }\n            reject(new Error(errorMsg))\n          })\n\n          // 原始请求的事件监听\n          req.on('aborted', () => {\n            const cost = Date.now() - start\n            const errorMsg = `请求被取消: ${url}, cost: ${cost} ms`\n            log.error(errorMsg, ', rOptions:', jsonApi.stringify2(rOptions))\n            proxyReq.abort()\n            if (res.writableEnded) {\n              return\n            }\n            reject(new Error(errorMsg))\n          })\n          req.on('error', (e, req, res) => {\n            const cost = Date.now() - start\n            log.error(`请求错误: ${url}, cost: ${cost} ms, error:`, e, ', rOptions:', jsonApi.stringify2(rOptions))\n            reject(e)\n          })\n          req.on('timeout', () => {\n            const cost = Date.now() - start\n            const errorMsg = `请求超时: ${url}, cost: ${cost} ms`\n            log.error(errorMsg, ', rOptions:', jsonApi.stringify2(rOptions))\n            reject(new Error(errorMsg))\n          })\n          req.pipe(proxyReq)\n        }\n      })\n    }\n\n    // workflow control\n    (async () => {\n      await requestInterceptorPromise()\n\n      if (res.writableEnded) {\n        // log.info('res is writableEnded, return false')\n        return false\n      }\n\n      const proxyRes = await proxyRequestPromise()\n\n      // proxyRes.on('data', (chunk) => {\n      //   // log.info('BODY: ')\n      // })\n      proxyRes.on('error', (error) => {\n        countSlow(null, `error: ${error.message}`)\n        log.error(`proxy res error: ${url}, error:`, error)\n      })\n\n      const responseInterceptorPromise = new Promise((resolve, reject) => {\n        const next = () => {\n          resolve()\n        }\n        for (const middleware of middlewares) {\n          if (middleware.responseInterceptor) {\n            middleware.responseInterceptor(req, res, proxyReq, proxyRes, ssl, next)\n          }\n        }\n        if (!setting.script.enabled) {\n          next()\n          return\n        }\n        try {\n          if (resIncpts && resIncpts.length > 0) {\n            let head = ''\n            let body = ''\n            for (const resIncpt of resIncpts) {\n              const append = resIncpt.responseIntercept(context, req, res, proxyReq, proxyRes, ssl, next)\n              // 判断是否已经关闭\n              if (res.writableEnded) {\n                next()\n                return\n              }\n              if (append) {\n                if (append.head) {\n                  head += append.head\n                }\n                if (append.body) {\n                  body += append.body\n                }\n              } else if (append === false) {\n                break // 返回false表示终止拦截器，跳出循环\n              }\n            }\n            InsertScriptMiddleware.responseInterceptor(req, res, proxyReq, proxyRes, ssl, next, {\n              head,\n              body,\n            })\n          } else {\n            next()\n          }\n        } catch (e) {\n          reject(e)\n        }\n      })\n\n      await responseInterceptorPromise\n\n      if (!res.headersSent) { // prevent duplicate set headers\n        Object.keys(proxyRes.headers).forEach((key) => {\n          if (proxyRes.headers[key] !== undefined) {\n            // https://github.com/nodejitsu/node-http-proxy/issues/362\n            if (/^www-authenticate$/i.test(key)) {\n              if (proxyRes.headers[key]) {\n                proxyRes.headers[key] = proxyRes.headers[key] && proxyRes.headers[key].split(',')\n              }\n              key = 'www-authenticate'\n            }\n            res.setHeader(key, proxyRes.headers[key])\n          }\n        })\n\n        if (proxyRes.statusCode >= 400) {\n          countSlow(null, `Status return: ${proxyRes.statusCode}`)\n        }\n        res.writeHead(proxyRes.statusCode)\n        proxyRes.pipe(res)\n      }\n    })().catch((e) => {\n      if (!res.writableEnded) {\n        try {\n          const status = e.status || 500\n          res.writeHead(status, { 'Content-Type': 'text/html;charset=UTF8' })\n          res.write(`DevSidecar Error:<br/>\n目标网站请求错误：【${e.code}】 ${e.message}<br/>\n目标地址：${rOptions.protocol}//${rOptions.hostname}:${rOptions.port}${rOptions.path}`,\n          )\n        } catch (e) {\n          // do nothing\n        }\n\n        try {\n          res.end()\n        } catch (e) {\n          // do nothing\n        }\n\n        // region 忽略部分已经打印过ERROR日志的错误\n        if (e.message) {\n          const ignoreErrors = [\n            '代理请求错误: ',\n            '代理请求超时: ',\n            '代理请求被取消: ',\n          ]\n          for (const ignoreError of ignoreErrors) {\n            if (e.message.startsWith(ignoreError)) {\n              return\n            }\n          }\n        }\n        // endregion\n\n        log.error(`Request error: ${url}, error:`, e)\n      }\n    })\n  }\n}\n"
  },
  {
    "path": "packages/mitmproxy/src/lib/proxy/mitmproxy/createUpgradeHandler.js",
    "content": "const http = require('node:http')\nconst https = require('node:https')\nconst log = require('../../../utils/util.log.server')\nconst util = require('../common/util')\n\n// copy from node-http-proxy.  ^_^\n\n// create connectHandler function\nmodule.exports = function createUpgradeHandler (serverSetting) {\n  // return\n  return function upgradeHandler (req, cltSocket, head, ssl) {\n    const clientOptions = util.getOptionsFromRequest(req, ssl, null, serverSetting)\n    const proxyReq = (ssl ? https : http).request(clientOptions)\n    proxyReq.on('error', (e) => {\n      log.error('upgradeHandler error:', e)\n    })\n    proxyReq.on('response', (res) => {\n      // if upgrade event isn't going to happen, close the socket\n      if (!res.upgrade) {\n        cltSocket.end()\n      }\n    })\n\n    proxyReq.on('upgrade', (proxyRes, proxySocket, proxyHead) => {\n      proxySocket.on('error', (e) => {\n        log.error('upgrade error:', e)\n      })\n\n      cltSocket.on('error', (e) => {\n        log.error('upgrade socket error:', e)\n        proxySocket.end()\n      })\n\n      proxySocket.setTimeout(0)\n      proxySocket.setNoDelay(true)\n\n      proxySocket.setKeepAlive(true, 0)\n\n      if (proxyHead && proxyHead.length) {\n        proxySocket.unshift(proxyHead)\n      }\n\n      cltSocket.write(\n        `${Object.keys(proxyRes.headers).reduce((head, key) => {\n          const value = proxyRes.headers[key]\n\n          if (!Array.isArray(value)) {\n            head.push(`${key}: ${value}`)\n            return head\n          }\n\n          for (let i = 0; i < value.length; i++) {\n            head.push(`${key}: ${value[i]}`)\n          }\n          return head\n        }, ['HTTP/1.1 101 Switching Protocols']).join('\\r\\n')}\\r\\n\\r\\n`,\n      )\n\n      proxySocket.pipe(cltSocket).pipe(proxySocket)\n    })\n    proxyReq.end()\n  }\n}\n"
  },
  {
    "path": "packages/mitmproxy/src/lib/proxy/mitmproxy/dnsLookup.js",
    "content": "const defaultDns = require('node:dns')\nconst log = require('../../../utils/util.log.server')\nconst speedTest = require('../../speed')\n\nfunction createIpChecker (tester) {\n  if (!tester || tester.backupList == null || tester.backupList.length === 0) {\n    return null\n  }\n\n  return (ip) => {\n    for (let i = 0; i < tester.backupList.length; i++) {\n      const item = tester.backupList[i]\n      if (item.host === ip) {\n        if (item.time > 0) {\n          return true // IP测速成功\n        }\n        if (item.status === 'failed') {\n          return false // IP测速失败\n        }\n        break\n      }\n    }\n\n    return true // IP测速未知\n  }\n}\n\nmodule.exports = {\n  createLookupFunc (res, dns, action, target, port, isDnsIntercept) {\n    target = target ? (`, target: ${target}`) : ''\n\n    return (hostname, options, callback) => {\n      const tester = speedTest.getSpeedTester(hostname, port)\n      if (tester) {\n        const aliveIpObj = tester.pickFastAliveIpObj()\n        if (aliveIpObj) {\n          log.info(`----- ${action}: ${hostname}, use alive ip from dns '${aliveIpObj.dns}': ${aliveIpObj.host}${target} -----`)\n          if (res) {\n            res.setHeader('DS-DNS-Lookup', `IpTester: ${aliveIpObj.host} ${aliveIpObj.dns === '预设IP' ? 'PreSet' : aliveIpObj.dns}`)\n          }\n          callback(null, aliveIpObj.host, 4)\n          return\n        } else {\n          log.info(`----- ${action}: ${hostname}, no alive ip${target}, tester: { \"ready\": ${tester.ready}, \"backupList\": ${JSON.stringify(tester.backupList)} }`)\n        }\n      }\n\n      const ipChecker = createIpChecker(tester)\n\n      dns.lookup(hostname, ipChecker).then((ip) => {\n        if (isDnsIntercept) {\n          isDnsIntercept.dns = dns\n          isDnsIntercept.hostname = hostname\n          isDnsIntercept.ip = ip\n        }\n\n        if (ip !== hostname) {\n          log.info(`----- ${action}: ${hostname}, use ip from dns '${dns.dnsName}': ${ip}${target} -----`)\n          if (res) {\n            res.setHeader('DS-DNS-Lookup', `DNS: ${ip} ${dns.dnsName === '预设IP' ? 'PreSet' : dns.dnsName}`)\n          }\n          callback(null, ip, 4)\n        } else {\n          // 使用默认dns\n          log.info(`----- ${action}: ${hostname}, use default DNS: ${hostname}${target}, options:`, options, ', dns:', dns)\n          defaultDns.lookup(hostname, options, callback)\n        }\n      })\n    }\n  },\n}\n"
  },
  {
    "path": "packages/mitmproxy/src/lib/proxy/mitmproxy/index.js",
    "content": "const http = require('node:http')\nconst log = require('../../../utils/util.log.server')\nconst speedTest = require('../../speed/index.js')\nconst config = require('../common/config')\nconst tlsUtils = require('../tls/tlsUtils')\nconst createConnectHandler = require('./createConnectHandler')\nconst createFakeServerCenter = require('./createFakeServerCenter')\nconst createRequestHandler = require('./createRequestHandler')\nconst createUpgradeHandler = require('./createUpgradeHandler')\n\nmodule.exports = {\n  createProxy ({\n    host = config.defaultHost,\n    port = config.defaultPort,\n    maxLength = config.defaultMaxLength,\n    caCertPath,\n    caKeyPath,\n    sslConnectInterceptor,\n    createIntercepts,\n    getCertSocketTimeout = 1000,\n    middlewares = [],\n    externalProxy,\n    dnsConfig,\n    setting,\n    compatibleConfig,\n  }, callback) {\n    // Don't reject unauthorized\n    // process.env.NODE_TLS_REJECT_UNAUTHORIZED = '0'\n    log.info(`CA Cert read in: ${caCertPath}`)\n    log.info(`CA private key read in: ${caKeyPath}`)\n    if (!caCertPath) {\n      caCertPath = config.getDefaultCACertPath()\n    }\n    if (!caKeyPath) {\n      caKeyPath = config.getDefaultCAKeyPath()\n    }\n    const rs = this.createCA({ caCertPath, caKeyPath })\n    if (rs.create) {\n      log.info(`CA Cert saved in: ${caCertPath}`)\n      log.info(`CA private key saved in: ${caKeyPath}`)\n    }\n\n    port = ~~port\n    const speedTestConfig = dnsConfig.speedTest\n    const dnsMap = dnsConfig.dnsMap\n    if (speedTestConfig) {\n      const dnsProviders = speedTestConfig.dnsProviders\n      const map = {}\n      for (const dnsProvider of dnsProviders) {\n        if (dnsMap[dnsProvider]) {\n          map[dnsProvider] = dnsMap[dnsProvider]\n        }\n      }\n      speedTest.initSpeedTest({ ...speedTestConfig, dnsMap: map })\n    }\n\n    const requestHandler = createRequestHandler(\n      createIntercepts,\n      middlewares,\n      externalProxy,\n      dnsConfig,\n      setting,\n      compatibleConfig,\n    )\n\n    const upgradeHandler = createUpgradeHandler(setting)\n\n    const fakeServersCenter = createFakeServerCenter({\n      maxLength,\n      caCertPath,\n      caKeyPath,\n      requestHandler,\n      upgradeHandler,\n      getCertSocketTimeout,\n    })\n\n    const connectHandler = createConnectHandler(\n      sslConnectInterceptor,\n      middlewares,\n      fakeServersCenter,\n      dnsConfig,\n      compatibleConfig,\n    )\n\n    // 创建监听方法，用于监听 http 和 https 两个端口\n    const printDebugLog = process.env.NODE_ENV === 'development' && false // 开发过程中，如有需要可以将此参数临时改为true，打印所有事件的日志\n    const serverListen = (server, ssl, port, host) => {\n      server.listen(port, host, () => {\n        log.info(`dev-sidecar启动 ${ssl ? 'https' : 'http'} 端口: ${host}:${port}`)\n        server.on('request', (req, res) => {\n          if (printDebugLog) {\n            log.debug(`【server request, ssl: ${ssl}】\\r\\n----- req -----\\r\\n`, req, '\\r\\n----- res -----\\r\\n', res)\n          }\n          requestHandler(req, res, ssl)\n        })\n        // tunneling for https\n        server.on('connect', (req, cltSocket, head) => {\n          if (printDebugLog) {\n            log.debug(`【server connect, ssl: ${ssl}】\\r\\n----- req -----\\r\\n`, req, '\\r\\n----- cltSocket -----\\r\\n', cltSocket, '\\r\\n----- head -----\\r\\n', head)\n          }\n          connectHandler(req, cltSocket, head, ssl)\n        })\n        // TODO: handler WebSocket\n        server.on('upgrade', (req, cltSocket, head) => {\n          if (printDebugLog) {\n            log.debug(`【server upgrade, ssl: ${ssl}】\\r\\n----- req -----\\r\\n`, req)\n          } else {\n            log.info(`【server upgrade, ssl: ${ssl}】`, req.url)\n          }\n          upgradeHandler(req, cltSocket, head, ssl)\n        })\n        server.on('error', (err) => {\n          log.error(`【server error, ssl: ${ssl}】\\r\\n----- error -----\\r\\n`, err)\n        })\n        server.on('clientError', (err, cltSocket) => {\n          // log.error(`【server clientError, ssl: ${ssl}】\\r\\n----- error -----\\r\\n`, err, '\\r\\n----- cltSocket -----\\r\\n', cltSocket)\n          log.error(`【server clientError, ssl: ${ssl}】socket.localPort = ${cltSocket.localPort}\\r\\n`, err)\n          cltSocket.end('HTTP/1.1 400 Bad Request\\r\\n\\r\\n')\n        })\n\n        // 其他事件：仅记录debug日志\n        if (printDebugLog) {\n          server.on('close', () => {\n            log.debug(`【server close, ssl: ${ssl}】no arguments...`)\n          })\n          server.on('connection', (cltSocket) => {\n            log.debug(`【server connection, ssl: ${ssl}】\\r\\n----- cltSocket -----\\r\\n`, cltSocket)\n          })\n          server.on('listening', () => {\n            log.debug(`【server listening, ssl: ${ssl}】no arguments...`)\n          })\n          server.on('checkContinue', (req, res) => {\n            log.debug(`【server checkContinue, ssl: ${ssl}】\\r\\n----- req -----\\r\\n`, req, '\\r\\n----- res -----\\r\\n', res)\n          })\n          server.on('checkExpectation', (req, res) => {\n            log.debug(`【server checkExpectation, ssl: ${ssl}】\\r\\n----- req -----\\r\\n`, req, '\\r\\n----- res -----\\r\\n', res)\n          })\n          server.on('dropRequest', (req, cltSocket) => {\n            log.debug(`【server checkExpectation, ssl: ${ssl}】\\r\\n----- req -----\\r\\n`, req, '\\r\\n----- cltSocket -----\\r\\n', cltSocket)\n          })\n        }\n\n        if (callback) {\n          callback(server, port, host, ssl)\n        }\n      })\n    }\n\n    const httpsServer = new http.Server()\n    const httpServer = new http.Server()\n\n    // `http端口` 比 `https端口` 要小1\n    const httpsPort = port\n    const httpPort = port - 1\n    serverListen(httpsServer, true, httpsPort, host)\n    serverListen(httpServer, false, httpPort, host)\n\n    return [httpsServer, httpServer]\n  },\n  createCA (caPaths) {\n    return tlsUtils.initCA(caPaths)\n  },\n}\n"
  },
  {
    "path": "packages/mitmproxy/src/lib/proxy/tls/CertAndKeyContainer.js",
    "content": "const tlsUtils = require('./tlsUtils')\n// const https = require('https')\nconst log = require('../../../utils/util.log.server')\n\nmodule.exports = class CertAndKeyContainer {\n  constructor ({\n    maxLength = 1000,\n    // getCertSocketTimeout = 2 * 1000,\n    caCert,\n    caKey,\n  }) {\n    this.queue = []\n    this.maxLength = maxLength\n    // this.getCertSocketTimeout = getCertSocketTimeout\n    this.caCert = caCert\n    this.caKey = caKey\n  }\n\n  addCertPromise (certPromiseObj) {\n    if (this.queue.length >= this.maxLength) {\n      const delCertObj = this.queue.shift()\n      log.info(`超过最大证书数量${this.maxLength}，删除旧证书。delCertObj:`, delCertObj)\n    }\n    this.queue.push(certPromiseObj)\n    return certPromiseObj\n  }\n\n  getCertPromise (hostname, port, dnsName, mappingHostNames) {\n    for (let i = 0; i < this.queue.length; i++) {\n      const _certPromiseObj = this.queue[i]\n      const mappingHostNames = _certPromiseObj.mappingHostNames\n      for (let j = 0; j < mappingHostNames.length; j++) {\n        const DNSName = mappingHostNames[j]\n        if (DNSName === dnsName || tlsUtils.isMappingHostName(DNSName, hostname)) {\n          this.reRankCert(i)\n          log.info(`Load fakeCertPromise from cache, hostname: ${hostname}:${port}, certPromiseObj: {\"mappingHostNames\":${JSON.stringify(_certPromiseObj.mappingHostNames)}}`)\n          return _certPromiseObj.promise\n        }\n      }\n    }\n\n    const certPromiseObj = {\n      mappingHostNames,\n    }\n\n    const promise = new Promise((resolve, _reject) => {\n      log.info(`【CreateFakeCertificate】dnsName: ${dnsName}, hostname: ${hostname}:${port}`)\n\n      const certObj = tlsUtils.createFakeCertificateByDomain(this.caKey, this.caCert, dnsName, mappingHostNames)\n      resolve(certObj)\n    })\n\n    certPromiseObj.promise = promise\n    this.addCertPromise(certPromiseObj)\n\n    return promise\n  }\n\n  reRankCert (index) {\n    // index ==> queue foot\n    this.queue.push((this.queue.splice(index, 1))[0])\n  }\n}\n"
  },
  {
    "path": "packages/mitmproxy/src/lib/proxy/tls/FakeServersCenter.js",
    "content": "const http = require('node:http')\nconst https = require('node:https')\nconst tls = require('node:tls')\nconst forge = require('node-forge')\nconst CertAndKeyContainer = require('./CertAndKeyContainer')\nconst tlsUtils = require('./tlsUtils')\nconst log = require('../../../utils/util.log.server')\nconst compatible = require('../compatible/compatible')\n\nconst pki = forge.pki\n\n// 获取DNS名称\nfunction getDnsName (hostname) {\n  if (!hostname.includes('.')) {\n    return hostname // 可能是IPv6地址，直接返回\n  }\n\n  // 判断是否为IP\n  if (hostname.match(/\\b(25[0-5]|2[0-4]\\d|1\\d{2}|[1-9]\\d|\\d)(\\.(25[0-5]|2[0-4]\\d|1\\d{2}|[1-9]\\d|\\d)\\b){3}/g)) {\n    return hostname // 为IP，直接返回\n  }\n\n  // 判断是否是一级域名\n  if (hostname.indexOf('.') === hostname.lastIndexOf('.')) {\n    return `*.${hostname}`\n  }\n\n  // 获取域名\n  return `*${hostname.substring(hostname.indexOf('.'))}`\n}\n\nmodule.exports = class FakeServersCenter {\n  constructor ({ maxLength = 256, requestHandler, upgradeHandler, caCert, caKey, getCertSocketTimeout }) {\n    this.queue = []\n    this.maxLength = maxLength\n    this.requestHandler = requestHandler\n    this.upgradeHandler = upgradeHandler\n    this.certAndKeyContainer = new CertAndKeyContainer({\n      getCertSocketTimeout,\n      caCert,\n      caKey,\n    })\n  }\n\n  addServerPromise (serverPromiseObj) {\n    if (this.queue.length >= this.maxLength) {\n      const delServerObj = this.queue.shift()\n      try {\n        log.info(`超过最大服务数量${this.maxLength}，删除旧服务。delServerObj:`, delServerObj)\n        delServerObj.serverObj.server.close()\n      } catch (e) {\n        log.error('`delServerObj.serverObj.server.close()` error:', e)\n      }\n    }\n    this.queue.push(serverPromiseObj)\n    return serverPromiseObj\n  }\n\n  getServerPromise (hostname, port, ssl, manualCompatibleConfig) {\n    if (port === 443 || port === 80) {\n      ssl = port === 443\n    } else if (ssl) {\n      // 自动兼容程序：1\n      const compatibleConfig = compatible.getConnectCompatibleConfig(hostname, port, manualCompatibleConfig)\n      if (compatibleConfig && compatibleConfig.ssl != null) {\n        ssl = compatibleConfig.ssl\n      }\n    }\n\n    log.info(`getServerPromise, hostname: ${hostname}:${port}, ssl: ${ssl}, protocol: ${ssl ? 'https' : 'http'}`)\n\n    for (let i = 0; i < this.queue.length; i++) {\n      const serverPromiseObj = this.queue[i]\n      if (serverPromiseObj.port === port && serverPromiseObj.ssl === ssl) {\n        const mappingHostNames = serverPromiseObj.mappingHostNames\n        for (let j = 0; j < mappingHostNames.length; j++) {\n          const DNSName = mappingHostNames[j]\n          if (tlsUtils.isMappingHostName(DNSName, hostname)) {\n            this.reRankServer(i)\n            log.info(`Load fakeServerPromise from cache, hostname: ${hostname}:${port}, ssl: ${ssl}, serverPromiseObj: {\"ssl\":${serverPromiseObj.ssl},\"port\":${serverPromiseObj.port},\"mappingHostNames\":${JSON.stringify(serverPromiseObj.mappingHostNames)}}`)\n            return serverPromiseObj.promise\n          }\n        }\n      }\n    }\n\n    const dnsName = getDnsName(hostname)\n    const mappingHostNames = [dnsName]\n    if (dnsName.startsWith('*.')) {\n      mappingHostNames.push(dnsName.replace('*.', ''))\n    }\n\n    const serverPromiseObj = {\n      port,\n      ssl,\n      mappingHostNames,\n    }\n\n    const promise = new Promise((resolve, _reject) => {\n      (async () => {\n        let fakeServer\n        let cert\n        let key\n\n        log.info(`【CreateFakeServer】hostname: ${hostname}:${port}, ssl: ${ssl}, protocol: ${ssl ? 'https' : 'http'}`)\n\n        if (ssl) {\n          const certObj = await this.certAndKeyContainer.getCertPromise(hostname, port, dnsName, mappingHostNames)\n          cert = certObj.cert\n          key = certObj.key\n          const certPem = pki.certificateToPem(cert)\n          const keyPem = pki.privateKeyToPem(key)\n          fakeServer = new https.Server({\n            key: keyPem,\n            cert: certPem,\n            SNICallback: (hostname, done) => {\n              (async () => {\n                log.info(`fakeServer SNICallback: ${hostname}:${port}`)\n                done(null, tls.createSecureContext({\n                  key: pki.privateKeyToPem(certObj.key),\n                  cert: pki.certificateToPem(certObj.cert),\n                }))\n              })()\n            },\n          })\n        } else {\n          fakeServer = new http.Server()\n        }\n        const serverObj = {\n          cert,\n          key,\n          server: fakeServer,\n          port: 0, // if port === 0 ,should listen server's `listening` event.\n        }\n        serverPromiseObj.serverObj = serverObj\n\n        const printDebugLog = process.env.NODE_ENV === 'development' && false // 开发过程中，如有需要可以将此参数临时改为true，打印所有事件的日志\n        fakeServer.listen(0, () => {\n          const address = fakeServer.address()\n          serverObj.port = address.port\n        })\n        fakeServer.on('request', (req, res) => {\n          if (printDebugLog) {\n            log.debug(`【fakeServer request - ${hostname}:${port}】\\r\\n----- req -----\\r\\n`, req, '\\r\\n----- res -----\\r\\n', res)\n          }\n          this.requestHandler(req, res, ssl)\n        })\n        fakeServer.on('listening', () => {\n          if (printDebugLog) {\n            log.debug(`【fakeServer listening - ${hostname}:${port}】no arguments...`)\n          }\n          resolve(serverObj)\n        })\n        fakeServer.on('upgrade', (req, socket, head) => {\n          if (printDebugLog) {\n            log.debug(`【fakeServer upgrade - ${hostname}:${port}】\\r\\n----- req -----\\r\\n`, req, '\\r\\n----- socket -----\\r\\n', socket, '\\r\\n----- head -----\\r\\n', head)\n          } else {\n            log.info(`【fakeServer upgrade - ${hostname}:${port}】`, req.url)\n          }\n          this.upgradeHandler(req, socket, head, ssl)\n        })\n\n        // 三个 error 事件\n        fakeServer.on('error', (e) => {\n          log.error(`【fakeServer error - ${hostname}:${port}】\\r\\n----- error -----\\r\\n`, e)\n        })\n        fakeServer.on('clientError', (err, _socket) => {\n          // log.error(`【fakeServer clientError - ${hostname}:${port}】\\r\\n----- error -----\\r\\n`, err, '\\r\\n----- socket -----\\r\\n', socket)\n          log.error(`【fakeServer clientError - ${hostname}:${port}】\\r\\n`, err)\n\n          // 自动兼容程序：1\n          if (port !== 443 && port !== 80) {\n            if (ssl === true && err.code.indexOf('ERR_SSL_') === 0) {\n              compatible.setConnectSsl(hostname, port, false)\n              log.error(`自动兼容程序：SSL异常，现设置为禁用ssl: ${hostname}:${port}, ssl = false`)\n            } else if (ssl === false && err.code === 'HPE_INVALID_METHOD') {\n              compatible.setConnectSsl(hostname, port, true)\n              log.error(`自动兼容程序：${err.code}，现设置为启用ssl: ${hostname}:${port}, ssl = true`)\n            }\n          }\n        })\n        if (ssl) {\n          fakeServer.on('tlsClientError', (err, _tlsSocket) => {\n            if (err.code === 'ECONNRESET' || err.code === 'ETIMEDOUT') {\n              return // 在tlsClientError事件中，以上异常不记录日志\n            }\n            // log.error(`【fakeServer tlsClientError - ${hostname}:${port}】\\r\\n----- error -----\\r\\n`, err, '\\r\\n----- tlsSocket -----\\r\\n', tlsSocket)\n            log.error(`【fakeServer tlsClientError - ${hostname}:${port}】\\r\\n`, err)\n          })\n        }\n\n        // 其他监听事件，只打印debug日志\n        if (printDebugLog) {\n          if (ssl) {\n            fakeServer.on('keylog', (line, tlsSocket) => {\n              log.debug(`【fakeServer keylog - ${hostname}:${port}】\\r\\n----- line -----\\r\\n`, line, '\\r\\n----- tlsSocket -----\\r\\n', tlsSocket)\n            })\n            // fakeServer.on('newSession', (sessionId, sessionData, callback) => {\n            //   log.debug(`【fakeServer newSession - ${hostname}:${port}】\\r\\n----- sessionId -----\\r\\n`, sessionId, '\\r\\n----- sessionData -----\\r\\n', sessionData, '\\r\\n----- callback -----\\r\\n', callback)\n            // })\n            // fakeServer.on('OCSPRequest', (certificate, issuer, callback) => {\n            //   log.debug(`【fakeServer OCSPRequest - ${hostname}:${port}】\\r\\n----- certificate -----\\r\\n`, certificate, '\\r\\n----- issuer -----\\r\\n', issuer, '\\r\\n----- callback -----\\r\\n', callback)\n            // })\n            // fakeServer.on('resumeSession', (sessionId, callback) => {\n            //   log.debug(`【fakeServer resumeSession - ${hostname}:${port}】\\r\\n----- sessionId -----\\r\\n`, sessionId, '\\r\\n----- callback -----\\r\\n', callback)\n            // })\n            fakeServer.on('secureConnection', (tlsSocket) => {\n              log.debug(`【fakeServer secureConnection - ${hostname}:${port}】\\r\\n----- tlsSocket -----\\r\\n`, tlsSocket)\n            })\n          }\n          fakeServer.on('close', () => {\n            log.debug(`【fakeServer close - ${hostname}:${port}】no arguments...`)\n          })\n          fakeServer.on('connection', (socket) => {\n            log.debug(`【fakeServer connection - ${hostname}:${port}】\\r\\n----- socket -----\\r\\n`, socket)\n          })\n          fakeServer.on('checkContinue', (req, res) => {\n            log.debug(`【fakeServer checkContinue - ${hostname}:${port}】\\r\\n----- req -----\\r\\n`, req, '\\r\\n----- res -----\\r\\n', res)\n          })\n          fakeServer.on('checkExpectation', (req, res) => {\n            log.debug(`【fakeServer checkExpectation - ${hostname}:${port}】\\r\\n----- req -----\\r\\n`, req, '\\r\\n----- res -----\\r\\n', res)\n          })\n          fakeServer.on('connect', (req, socket, head) => {\n            log.debug(`【fakeServer resumeSession - ${hostname}:${port}】\\r\\n----- req -----\\r\\n`, req, '\\r\\n----- socket -----\\r\\n', socket, '\\r\\n----- head -----\\r\\n', head)\n          })\n        }\n      })()\n    })\n\n    serverPromiseObj.promise = promise\n    this.addServerPromise(serverPromiseObj)\n\n    return promise\n  }\n\n  reRankServer (index) {\n    // index ==> queue foot\n    this.queue.push((this.queue.splice(index, 1))[0])\n  }\n}\n"
  },
  {
    "path": "packages/mitmproxy/src/lib/proxy/tls/sniUtil.js",
    "content": "module.exports = function extractSNI (data) {\n  /*\n    From https://tools.ietf.org/html/rfc5246:\n    enum {\n        hello_request(0), client_hello(1), server_hello(2),\n        certificate(11), server_key_exchange (12),\n        certificate_request(13), server_hello_done(14),\n        certificate_verify(15), client_key_exchange(16),\n        finished(20)\n        (255)\n    } HandshakeType;\n    struct {\n        HandshakeType msg_type;\n        uint24 length;\n        select (HandshakeType) {\n            case hello_request:       HelloRequest;\n            case client_hello:        ClientHello;\n            case server_hello:        ServerHello;\n            case certificate:         Certificate;\n            case server_key_exchange: ServerKeyExchange;\n            case certificate_request: CertificateRequest;\n            case server_hello_done:   ServerHelloDone;\n            case certificate_verify:  CertificateVerify;\n            case client_key_exchange: ClientKeyExchange;\n            case finished:            Finished;\n        } body;\n    } Handshake;\n    struct {\n        uint8 major;\n        uint8 minor;\n    } ProtocolVersion;\n    struct {\n        uint32 gmt_unix_time;\n        opaque random_bytes[28];\n    } Random;\n    opaque SessionID<0..32>;\n    uint8 CipherSuite[2];\n    enum { null(0), (255) } CompressionMethod;\n    struct {\n        ProtocolVersion client_version;\n        Random random;\n        SessionID session_id;\n        CipherSuite cipher_suites<2..2^16-2>;\n        CompressionMethod compression_methods<1..2^8-1>;\n        select (extensions_present) {\n            case false:\n                struct {};\n            case true:\n                Extension extensions<0..2^16-1>;\n        };\n    } ClientHello;\n    */\n\n  let end = data.length\n\n  // skip the record header\n  let pos = 5\n\n  // skip HandshakeType (you should already have verified this)\n  pos += 1\n\n  // skip handshake length\n  pos += 3\n\n  // skip protocol version (you should already have verified this)\n  pos += 2\n\n  // skip Random\n  pos += 32\n\n  // skip SessionID\n  if (pos > end - 1) {\n    return null\n  }\n  const sessionIdLength = data[pos]\n  pos += 1 + sessionIdLength\n\n  // skip CipherSuite\n  if (pos > end - 2) {\n    return null\n  }\n  const cipherSuiteLength = data[pos] << 8 | data[pos + 1]\n  pos += 2 + cipherSuiteLength\n\n  // skip CompressionMethod\n  if (pos > end - 1) {\n    return null\n  }\n  const compressionMethodLength = data[pos]\n  pos += 1 + compressionMethodLength\n\n  // verify extensions exist\n  if (pos > end - 2) {\n    return null\n  }\n  const extensionsLength = data[pos] << 8 | data[pos + 1]\n  pos += 2\n\n  // verify the extensions fit\n  const extensionsEnd = pos + extensionsLength\n  if (extensionsEnd > end) {\n    return null\n  }\n  end = extensionsEnd\n\n  /*\n    From https://tools.ietf.org/html/rfc5246\n     and http://tools.ietf.org/html/rfc6066:\n    struct {\n        ExtensionType extension_type;\n        opaque extension_data<0..2^16-1>;\n    } Extension;\n    enum {\n        signature_algorithms(13), (65535)\n    } ExtensionType;\n    enum {\n        server_name(0), max_fragment_length(1),\n        client_certificate_url(2), trusted_ca_keys(3),\n        truncated_hmac(4), status_request(5), (65535)\n    } ExtensionType;\n    struct {\n        NameType name_type;\n        select (name_type) {\n            case host_name: HostName;\n        } name;\n    } ServerName;\n    enum {\n        host_name(0), (255)\n    } NameType;\n    opaque HostName<1..2^16-1>;\n    struct {\n        ServerName server_name_list<1..2^16-1>\n    } ServerNameList;\n    */\n\n  while (pos <= end - 4) {\n    const extensionType = data[pos] << 8 | data[pos + 1]\n    const extensionSize = data[pos + 2] << 8 | data[pos + 3]\n    pos += 4\n    if (extensionType === 0) { // ExtensionType was server_name(0)\n      // read ServerNameList length\n      if (pos > end - 2) {\n        return null\n      }\n      const nameListLength = data[pos] << 8 | data[pos + 1]\n      pos += 2\n\n      // verify we have enough bytes and loop over SeverNameList\n      let n = pos\n      pos += nameListLength\n      if (pos > end) {\n        return null\n      }\n      while (n < pos - 3) {\n        const nameType = data[n]\n        const nameLength = data[n + 1] << 8 | data[n + 2]\n        n += 3\n\n        // check if NameType is host_name(0)\n        if (nameType === 0) {\n          // verify we have enough bytes\n          if (n > end - nameLength) {\n            return null\n          }\n\n          // decode as ascii and return\n\n          const sniName = data.toString('ascii', n, n + nameLength)\n          return {\n            sniName,\n            start: n,\n            end: n + nameLength,\n            length: nameLength,\n          }\n        } else {\n          n += nameLength\n        }\n      }\n    } else { // ExtensionType was something we are not interested in\n      pos += extensionSize\n    }\n  }\n\n  return null\n}\n"
  },
  {
    "path": "packages/mitmproxy/src/lib/proxy/tls/tlsUtils.js",
    "content": "const fs = require('node:fs')\nconst path = require('node:path')\nconst _ = require('lodash')\nconst forge = require('node-forge')\nconst log = require('../../../utils/util.log.server')\nconst config = require('../common/config')\n// const colors = require('colors')\n\nconst utils = exports\nconst pki = forge.pki\n\n// const os = require('os')\n// let username = 'dev-sidecar'\n// try {\n//   const user = os.userInfo()\n//   username = user.username\n// } catch (e) {\n//   log.info('get userinfo error', e)\n// }\n\nutils.createCA = function (CN) {\n  const keys = pki.rsa.generateKeyPair(2048)\n  const cert = pki.createCertificate()\n  cert.publicKey = keys.publicKey\n  cert.serialNumber = `${Date.now()}`\n  cert.validity.notBefore = new Date(Date.now() - (60 * 60 * 1000))\n  cert.validity.notAfter = new Date()\n  cert.validity.notAfter.setFullYear(cert.validity.notAfter.getFullYear() + 20)\n  const attrs = [{\n    name: 'commonName',\n    value: CN,\n  }, {\n    name: 'countryName',\n    value: 'CN',\n  }, {\n    shortName: 'ST',\n    value: 'GuangDong',\n  }, {\n    name: 'localityName',\n    value: 'ShenZhen',\n  }, {\n    name: 'organizationName',\n    value: 'dev-sidecar',\n  }, {\n    shortName: 'OU',\n    value: 'https://github.com/docmirror/dev-sidecar',\n  }]\n  cert.setSubject(attrs)\n  cert.setIssuer(attrs)\n  cert.setExtensions([{\n    name: 'basicConstraints',\n    critical: true,\n    cA: true,\n  }, {\n    name: 'keyUsage',\n    critical: true,\n    keyCertSign: true,\n  }, {\n    name: 'subjectKeyIdentifier',\n  }])\n\n  // self-sign certificate\n  cert.sign(keys.privateKey, forge.md.sha256.create())\n\n  return {\n    key: keys.privateKey,\n    cert,\n  }\n}\n\nutils.covertNodeCertToForgeCert = function (originCertificate) {\n  const obj = forge.asn1.fromDer(originCertificate.raw.toString('binary'))\n  return forge.pki.certificateFromAsn1(obj)\n}\n\nutils.createFakeCertificateByDomain = function (caKey, caCert, domain, mappingHostNames) {\n  // 作用域名\n  const altNames = []\n  mappingHostNames.forEach((mappingHostName) => {\n    altNames.push({\n      type: 2, // 1=电子邮箱、2=DNS名称\n      value: mappingHostName,\n    })\n  })\n\n  const keys = pki.rsa.generateKeyPair(2048)\n  const cert = pki.createCertificate()\n  cert.publicKey = keys.publicKey\n\n  cert.serialNumber = `${Date.now()}`\n  cert.validity.notBefore = new Date()\n  cert.validity.notBefore.setFullYear(cert.validity.notBefore.getFullYear() - 1)\n  cert.validity.notAfter = new Date()\n  cert.validity.notAfter.setFullYear(cert.validity.notAfter.getFullYear() + 1)\n  const attrs = [{\n    name: 'commonName',\n    value: domain,\n  }, {\n    name: 'countryName',\n    value: 'CN',\n  }, {\n    shortName: 'ST',\n    value: 'GuangDong',\n  }, {\n    name: 'localityName',\n    value: 'ShenZhen',\n  }, {\n    name: 'organizationName',\n    value: 'dev-sidecar',\n  }, {\n    shortName: 'OU',\n    value: 'https://github.com/docmirror/dev-sidecar',\n  }]\n\n  cert.setIssuer(caCert.subject.attributes)\n  cert.setSubject(attrs)\n\n  cert.setExtensions([{\n    name: 'basicConstraints',\n    critical: true,\n    cA: false,\n  },\n  // {\n  //   name: 'keyUsage',\n  //   critical: true,\n  //   digitalSignature: true,\n  //   contentCommitment: true,\n  //   keyEncipherment: true,\n  //   dataEncipherment: true,\n  //   keyAgreement: true,\n  //   keyCertSign: true,\n  //   cRLSign: true,\n  //   encipherOnly: true,\n  //   decipherOnly: true\n  // },\n  {\n    name: 'subjectAltName',\n    altNames,\n  }, {\n    name: 'subjectKeyIdentifier',\n  }, {\n    name: 'extKeyUsage',\n    serverAuth: true,\n    clientAuth: true,\n    codeSigning: true,\n    emailProtection: true,\n    timeStamping: true,\n  }, {\n    name: 'authorityKeyIdentifier',\n  }])\n  cert.sign(caKey, forge.md.sha256.create())\n\n  return {\n    key: keys.privateKey,\n    cert,\n  }\n}\n\nutils.createFakeCertificateByCA = function (caKey, caCert, originCertificate) {\n  const certificate = utils.covertNodeCertToForgeCert(originCertificate)\n\n  const keys = pki.rsa.generateKeyPair(2048)\n  const cert = pki.createCertificate()\n  cert.publicKey = keys.publicKey\n\n  cert.serialNumber = certificate.serialNumber\n  cert.validity.notBefore = new Date()\n  cert.validity.notBefore.setFullYear(cert.validity.notBefore.getFullYear() - 1)\n  cert.validity.notAfter = new Date()\n  cert.validity.notAfter.setFullYear(cert.validity.notAfter.getFullYear() + 1)\n\n  cert.setSubject(certificate.subject.attributes)\n  cert.setIssuer(caCert.subject.attributes)\n\n  certificate.subjectaltname && (cert.subjectaltname = certificate.subjectaltname)\n\n  const subjectAltName = _.find(certificate.extensions, { name: 'subjectAltName' })\n  cert.setExtensions([{\n    name: 'basicConstraints',\n    critical: true,\n    cA: false,\n  }, {\n    name: 'keyUsage',\n    critical: true,\n    digitalSignature: true,\n    contentCommitment: true,\n    keyEncipherment: true,\n    dataEncipherment: true,\n    keyAgreement: true,\n    keyCertSign: true,\n    cRLSign: true,\n    encipherOnly: true,\n    decipherOnly: true,\n  }, {\n    name: 'subjectAltName',\n    altNames: subjectAltName.altNames,\n  }, {\n    name: 'subjectKeyIdentifier',\n  }, {\n    name: 'extKeyUsage',\n    serverAuth: true,\n    clientAuth: true,\n    codeSigning: true,\n    emailProtection: true,\n    timeStamping: true,\n  }, {\n    name: 'authorityKeyIdentifier',\n  }])\n  cert.sign(caKey, forge.md.sha256.create())\n\n  return {\n    key: keys.privateKey,\n    cert,\n  }\n}\n\nutils.isBrowserRequest = function (userAgent) {\n  return /Mozilla/i.test(userAgent)\n}\n//\n//  /^[^.]+\\.a\\.com$/.test('c.a.com')\n//\nutils.isMappingHostName = function (DNSName, hostname) {\n  if (DNSName === hostname) {\n    return true\n  }\n\n  let reg = DNSName.replace(/\\./g, '\\\\.').replace(/\\*/g, '[^.]+')\n  reg = `^${reg}$`\n  return (new RegExp(reg)).test(hostname)\n}\n\nutils.getMappingHostNamesFromCert = function (cert) {\n  let mappingHostNames = []\n  mappingHostNames.push(cert.subject.getField('CN') ? cert.subject.getField('CN').value : '')\n  const altNames = cert.getExtension('subjectAltName') ? cert.getExtension('subjectAltName').altNames : []\n  mappingHostNames = mappingHostNames.concat(_.map(altNames, 'value'))\n  return mappingHostNames\n}\n\n// sync\nutils.initCA = function ({ caCertPath, caKeyPath }) {\n  try {\n    fs.accessSync(caCertPath, fs.F_OK)\n    fs.accessSync(caKeyPath, fs.F_OK)\n\n    // has exist\n    return {\n      caCertPath,\n      caKeyPath,\n      create: false,\n    }\n  } catch (e0) {\n    log.info('证书文件不存在，重新生成:', e0)\n\n    try {\n      const caObj = utils.createCA(config.caName)\n\n      const caCert = caObj.cert\n      const cakey = caObj.key\n\n      const certPem = pki.certificateToPem(caCert)\n      const keyPem = pki.privateKeyToPem(cakey)\n      fs.mkdirSync(path.dirname(caCertPath), { recursive: true })\n      fs.writeFileSync(caCertPath, certPem)\n      fs.writeFileSync(caKeyPath, keyPem)\n      log.info('生成证书文件成功，共2个文件:', caCertPath, caKeyPath)\n    } catch (e) {\n      log.error('生成证书文件失败:', caCertPath, caKeyPath, ', error:', e)\n      throw e\n    }\n  }\n  return {\n    caCertPath,\n    caKeyPath,\n    create: true,\n  }\n}\n"
  },
  {
    "path": "packages/mitmproxy/src/lib/speed/SpeedTester.js",
    "content": "// const { exec } = require('node:child_process')\nconst net = require('node:net')\nconst _ = require('lodash')\nconst log = require('../../utils/util.log.server')\nconst config = require('./config.js')\n\n// const isWindows = process.platform === 'win32'\n\nconst DISABLE_TIMEOUT = 60 * 60 * 1000\n\nclass SpeedTester {\n  constructor ({ hostname, port }) {\n    this.dnsMap = config.getConfig().dnsMap\n\n    this.hostname = hostname\n    this.port = port || 443\n\n    this.ready = false\n    this.alive = []\n    this.backupList = []\n\n    this.testCount = 0\n    this.lastReadTime = Date.now()\n    this.keepCheckIntervalId = false\n\n    this.tryTestCount = 0\n\n    this.test() // 异步：初始化完成后先测速一次\n  }\n\n  pickFastAliveIpObj () {\n    this.touch()\n\n    if (this.alive.length === 0) {\n      if (this.backupList.length > 0 && this.tryTestCount % 10 > 0) {\n        this.testBackups() // 异步\n      } else if (this.tryTestCount % 10 === 0) {\n        this.test() // 异步\n      }\n      this.tryTestCount++\n\n      return null\n    }\n    return this.alive[0]\n  }\n\n  touch () {\n    this.lastReadTime = Date.now()\n    if (!this.keepCheckIntervalId) {\n      this.startChecker()\n    }\n  }\n\n  startChecker () {\n    if (this.keepCheckIntervalId) {\n      clearInterval(this.keepCheckIntervalId)\n    }\n    this.keepCheckIntervalId = setInterval(() => {\n      if (Date.now() - DISABLE_TIMEOUT > this.lastReadTime) {\n        // 超过很长时间没有访问，取消测试\n        clearInterval(this.keepCheckIntervalId)\n        this.keepCheckIntervalId = false\n        return\n      }\n      if (this.alive.length > 0) {\n        this.testBackups() // 异步\n      } else {\n        this.test() // 异步\n      }\n    }, config.getConfig().interval)\n  }\n\n  async getIpListFromDns (dnsMap) {\n    const ips = {}\n    const promiseList = []\n    for (const dnsKey in dnsMap) {\n      const dns = dnsMap[dnsKey]\n      const one = this.getFromOneDns(dns).then((ipList) => {\n        if (ipList && ipList.length > 0) {\n          for (const ip of ipList) {\n            ips[ip] = { dns: ipList.isPreSet === true ? '预设IP' : dnsKey }\n          }\n        }\n      })\n      promiseList.push(one)\n    }\n    await Promise.all(promiseList)\n\n    const items = []\n    for (const ip in ips) {\n      items.push({ host: ip, dns: ips[ip].dns })\n    }\n    return items\n  }\n\n  async getFromOneDns (dns) {\n    return await dns._lookupWithPreSetIpList(this.hostname)\n  }\n\n  async test () {\n    this.testCount++\n    log.debug(`[speed] test start: ${this.hostname}, testCount: ${this.testCount}`)\n\n    try {\n      const newList = await this.getIpListFromDns(this.dnsMap)\n      const newBackupList = [...newList, ...this.backupList]\n      this.backupList = _.unionBy(newBackupList, 'host')\n      await this.testBackups()\n      log.info(`[speed] test end: ${this.hostname} ➜ ip-list:`, this.backupList, `, testCount: ${this.testCount}`)\n      if (config.notify) {\n        config.notify({ key: 'test' })\n      }\n    } catch (e) {\n      log.error(`[speed] test failed: ${this.hostname}, testCount: ${this.testCount}, error:`, e)\n    }\n  }\n\n  async testBackups () {\n    if (this.backupList.length > 0) {\n      const aliveList = []\n\n      const testAll = []\n      for (const item of this.backupList) {\n        testAll.push(this.doTest(item, aliveList))\n      }\n      await Promise.all(testAll)\n      this.alive = aliveList\n    }\n\n    this.ready = true\n  }\n\n  async doTest (item, aliveList) {\n    try {\n      const ret = await this.testOne(item)\n      item.title = `${ret.by}测速成功：${ret.target}`\n      log.info(`[speed] test success: ${this.hostname} ➜ ${item.host}:${this.port} from DNS '${item.dns}'`)\n      _.merge(item, ret)\n      aliveList.push({ ...ret, ...item })\n      aliveList.sort((a, b) => a.time - b.time)\n      this.backupList.sort((a, b) => {\n        if (a.time === b.time) {\n          return 0\n        }\n        if (a.time == null) {\n          return 1\n        }\n        if (b.time == null) {\n          return -1\n        }\n        return a.time - b.time\n      })\n    } catch (e) {\n      if (item.time == null) {\n        item.title = e.message\n        item.status = 'failed'\n      }\n      if (!e.message.includes('timeout')) {\n        log.warn(`[speed] test error:   ${this.hostname} ➜ ${item.host}:${this.port} from DNS '${item.dns}', errorMsg: ${e.message}`)\n      }\n    }\n  }\n\n  testByTCP (item) {\n    return new Promise((resolve, reject) => {\n      const { host, dns } = item\n      const startTime = Date.now()\n\n      let isOver = false\n      const timeout = 5000\n      let timeoutId = null\n\n      const client = net.createConnection({ host, port: this.port }, () => {\n        isOver = true\n        clearTimeout(timeoutId)\n\n        const connectionTime = Date.now()\n        resolve({ status: 'success', by: 'TCP', target: `${host}:${this.port}`, time: connectionTime - startTime })\n        client.end()\n      })\n      client.on('error', (e) => {\n        isOver = true\n        clearTimeout(timeoutId)\n\n        log.warn('[speed] test by TCP error:  ', this.hostname, `➜ ${host}:${this.port} from DNS '${dns}', cost: ${Date.now() - startTime} ms, errorMsg:`, e.message)\n        reject(e)\n        client.end()\n      })\n\n      timeoutId = setTimeout(() => {\n        if (isOver) {\n          return\n        }\n\n        log.warn('[speed] test by TCP timeout:', this.hostname, `➜ ${host}:${this.port} from DNS '${dns}', cost: ${Date.now() - startTime} ms`)\n        reject(new Error('timeout'))\n        client.end()\n      }, timeout)\n    })\n  }\n\n  // 暂不使用\n  // testByPing (item) {\n  //   return new Promise((resolve, reject) => {\n  //     const { host, dns } = item\n  //     const startTime = Date.now()\n  //\n  //     // 设置超时程序\n  //     let isOver = false\n  //     const timeout = 5000\n  //     const timeoutId = setTimeout(() => {\n  //       if (!isOver) {\n  //         log.warn('[speed] test by PING timeout:', this.hostname, `➜ ${host} from DNS '${dns}', cost: ${Date.now() - startTime} ms`)\n  //         reject(new Error('timeout'))\n  //       }\n  //     }, timeout)\n  //\n  //     // 协议选择（如强制ping6）\n  //     const usePing6 = !isWindows && host.includes(':') // Windows无ping6命令\n  //     const cmd = usePing6\n  //       ? `ping6 -c 2 ${host}`\n  //       : isWindows\n  //         ? `ping -n 2 ${host}`\n  //         : `ping -c 2 ${host}`\n  //\n  //     log.debug('[speed] test by PING start:', this.hostname, `➜ ${host} from DNS '${dns}'`)\n  //     exec(cmd, (error, stdout, _stderr) => {\n  //       isOver = true\n  //       clearTimeout(timeoutId)\n  //\n  //       if (error) {\n  //         log.warn('[speed] test by PING error:', this.hostname, `➜ ${host} from DNS '${dns}', cost: ${Date.now() - startTime} ms, error: 目标不可达或超时`)\n  //         reject(new Error('目标不可达或超时'))\n  //         return\n  //       }\n  //\n  //       // 提取延迟数据（正则匹配）\n  //       const regex = /[=<](\\d+(?:\\.\\d*)?)ms/gi // 适配Linux/Windows\n  //       const times = []\n  //       let match\n  //       // eslint-disable-next-line no-cond-assign\n  //       while ((match = regex.exec(stdout)) !== null) {\n  //         times.push(Number.parseFloat(match[1]))\n  //       }\n  //\n  //       if (times.length === 0) {\n  //         log.warn('[speed] test by PING error:', this.hostname, `➜ ${host} from DNS '${dns}', cost: ${Date.now() - startTime} ms, error: 无法解析延迟`)\n  //         reject(new Error('无法解析延迟'))\n  //       } else {\n  //         // 计算平均延迟\n  //         const avg = times.reduce((a, b) => a + b, 0) / times.length\n  //         resolve({ status: 'success', by: 'PING', target: host, time: Math.round(avg) })\n  //       }\n  //     })\n  //   })\n  // }\n\n  testOne (item) {\n    return new Promise((resolve, reject) => {\n      const thenFun = (ret) => {\n        resolve(ret)\n      }\n\n      // 先用TCP测速\n      this.testByTCP(item)\n        .then(thenFun)\n        .catch((e) => {\n          // // TCP测速失败，再用 PING 测速\n          // this.testByPing(item)\n          //   .then(thenFun)\n          //   .catch((e2) => {\n          //     reject(new Error(`TCP测速失败：${e.message}；PING测速失败：${e2.message}；`))\n          //   })\n\n          reject(new Error(`TCP测速失败：${item.host}:${this.port} ${e.message}`))\n        })\n    })\n  }\n}\n\nmodule.exports = SpeedTester\n"
  },
  {
    "path": "packages/mitmproxy/src/lib/speed/config.js",
    "content": "const config = {\n  dnsMap: {},\n}\nmodule.exports = {\n  getConfig () {\n    return config\n  },\n  notify: null,\n}\n"
  },
  {
    "path": "packages/mitmproxy/src/lib/speed/index.js",
    "content": "const _ = require('lodash')\nconst log = require('../../utils/util.log.server')\nconst config = require('./config')\nconst SpeedTester = require('./SpeedTester.js')\n\nconst SpeedTestPool = {\n}\n\nfunction addSpeedTest (hostname, port) {\n  if (!port) {\n    const idx = hostname.indexOf(':')\n    if (idx > 0 && idx === hostname.lastIndexOf(':')) {\n      const arr = hostname.split(':')\n      hostname = arr[0]\n      port = Number.parseInt(arr[1]) || 443\n    } else {\n      port = 443\n    }\n  }\n\n  // 443端口不拼接在key上\n  const key = port === 443 ? hostname : `${hostname}:${port}`\n\n  if (SpeedTestPool[key] == null) {\n    return SpeedTestPool[key] = new SpeedTester({ hostname, port })\n  }\n\n  return SpeedTestPool[key]\n}\n\nfunction initSpeedTest (runtimeConfig) {\n  const { enabled, hostnameList } = runtimeConfig\n  const conf = config.getConfig()\n  _.merge(conf, runtimeConfig)\n  if (!enabled) {\n    return\n  }\n  _.forEach(hostnameList, (hostname) => {\n    addSpeedTest(hostname)\n  })\n  log.info('[speed] enabled，SpeedTestPool:', SpeedTestPool)\n}\n\nfunction getAllSpeedTester () {\n  const allSpeed = {}\n\n  if (config.getConfig().enabled) {\n    _.forEach(SpeedTestPool, (item, key) => {\n      allSpeed[key] = {\n        hostname: item.hostname,\n        port: item.port,\n        alive: item.alive,\n        backupList: item.backupList,\n      }\n    })\n  }\n\n  return allSpeed\n}\n\nfunction getSpeedTester (hostname, port) {\n  if (!config.getConfig().enabled) {\n    return null\n  }\n  return addSpeedTest(hostname, port)\n}\n\n// function registerNotify (notify) {\n//   config.notify = notify\n// }\n\nfunction reSpeedTest () {\n  _.forEach(SpeedTestPool, (item, _key) => {\n    item.test() // 异步\n  })\n}\n\n// action调用\nfunction action (event) {\n  if (event.key === 'reTest') {\n    reSpeedTest()\n  } else if (event.key === 'getList') {\n    process.send({ type: 'speed', event: { key: 'getList', value: getAllSpeedTester() } })\n  }\n}\nmodule.exports = {\n  SpeedTester,\n  initSpeedTest,\n  getSpeedTester,\n  // getAllSpeedTester,\n  // registerNotify,\n  reSpeedTest,\n  action,\n}\n"
  },
  {
    "path": "packages/mitmproxy/src/options.js",
    "content": "const fs = require('node:fs')\nconst path = require('node:path')\nconst lodash = require('lodash')\nconst dnsUtil = require('./lib/dns')\nconst interceptorImpls = require('./lib/interceptor')\nconst scriptInterceptor = require('./lib/interceptor/impl/res/script')\nconst { getTmpPacFilePath, downloadPacAsync, createOverwallMiddleware } = require('./lib/proxy/middleware/overwall')\nconst log = require('./utils/util.log.server')\nconst matchUtil = require('./utils/util.match')\n\n// 处理拦截配置\nfunction buildIntercepts (intercepts) {\n  // 自动生成script拦截器所需的辅助配置，降低使用`script拦截器`配置绝对地址和相对地址时的门槛\n  scriptInterceptor.handleScriptInterceptConfig(intercepts)\n\n  return intercepts\n}\n\n// 从拦截器配置中，获取exclusions字段，返回数组类型\nfunction getExclusionArray (exclusions) {\n  let ret = null\n  if (Array.isArray(exclusions)) {\n    if (exclusions.length > 0) {\n      ret = exclusions\n    }\n  } else if (lodash.isObject(exclusions)) {\n    ret = []\n    for (const exclusion in exclusions) {\n      ret.push(exclusion)\n    }\n    if (ret.length === 0) {\n      return null\n    }\n  }\n  return ret\n}\n\nmodule.exports = (serverConfig) => {\n  const intercepts = matchUtil.domainMapRegexply(buildIntercepts(serverConfig.intercepts))\n  const whiteList = matchUtil.domainMapRegexply(serverConfig.whiteList)\n  const timeoutMapping = matchUtil.domainMapRegexply(serverConfig.setting.timeoutMapping)\n\n  const dnsMapping = serverConfig.dns.mapping\n  const setting = serverConfig.setting\n\n  if (!setting.script.dirAbsolutePath) {\n    setting.script.dirAbsolutePath = path.join(setting.rootDir, setting.script.defaultDir)\n  }\n  if (setting.verifySsl !== false) {\n    setting.verifySsl = true\n  }\n  setting.timeoutMapping = timeoutMapping\n\n  const overWallConfig = serverConfig.plugin.overwall\n  if (overWallConfig.pac && overWallConfig.pac.enabled) {\n    const pacConfig = overWallConfig.pac\n\n    // 自动更新 pac.txt\n    if (!pacConfig.pacFileAbsolutePath && pacConfig.autoUpdate) {\n      // 异步下载远程 pac.txt 文件，并保存到本地；下载成功后，需要重启代理服务才会生效\n      downloadPacAsync(pacConfig)\n    }\n\n    // 优先使用本地已下载的 pac.txt 文件\n    if (!pacConfig.pacFileAbsolutePath && fs.existsSync(getTmpPacFilePath())) {\n      pacConfig.pacFileAbsolutePath = getTmpPacFilePath()\n      log.info('读取已下载的 pac.txt 文件:', pacConfig.pacFileAbsolutePath)\n    }\n\n    if (!pacConfig.pacFileAbsolutePath) {\n      log.info('setting.rootDir:', setting.rootDir)\n      pacConfig.pacFileAbsolutePath = path.join(setting.rootDir, pacConfig.pacFilePath)\n      log.info('读取内置的 pac.txt 文件:', pacConfig.pacFileAbsolutePath)\n      if (pacConfig.autoUpdate) {\n        log.warn('远程 pac.txt 文件下载失败或还在下载中，现使用内置 pac.txt 文件:', pacConfig.pacFileAbsolutePath)\n      }\n    }\n  }\n\n  // 插件列表\n  const middlewares = []\n\n  // 梯子插件：如果启用了，则添加到插件列表中\n  const overwallMiddleware = createOverwallMiddleware(overWallConfig)\n  if (overwallMiddleware) {\n    middlewares.push(overwallMiddleware)\n  }\n\n  const preSetIpList = matchUtil.domainMapRegexply(serverConfig.preSetIpList)\n\n  const options = {\n    host: serverConfig.host,\n    port: serverConfig.port,\n    dnsConfig: {\n      preSetIpList,\n      dnsMap: dnsUtil.initDNS(serverConfig.dns.providers, preSetIpList),\n      mapping: matchUtil.domainMapRegexply(dnsMapping),\n      speedTest: serverConfig.dns.speedTest,\n    },\n    setting,\n    compatibleConfig: {\n      connect: serverConfig.compatible ? matchUtil.domainMapRegexply(serverConfig.compatible.connect) : {},\n      request: serverConfig.compatible ? matchUtil.domainMapRegexply(serverConfig.compatible.request) : {},\n    },\n    middlewares,\n    sslConnectInterceptor: (req, cltSocket, head) => {\n      const hostname = req.url.split(':')[0]\n\n      // 配置了白名单的域名，将跳过代理\n      const inWhiteList = !!matchUtil.matchHostname(whiteList, hostname, 'in whiteList')\n      if (inWhiteList) {\n        log.info(`为白名单域名，不拦截: ${hostname}`)\n        return false // 不拦截\n      }\n\n      // 配置了拦截的域名，将会被代理\n      const matched = matchUtil.matchHostname(intercepts, hostname, 'matched intercepts')\n      if ((!!matched) === true) {\n        log.debug(`拦截器拦截：${req.url}, matched:`, matched)\n        return matched // 拦截\n      }\n\n      return null // 不在白名单中，也未配置在拦截功能中，跳过当前拦截器，由下一个拦截器判断\n    },\n    createIntercepts: (context) => {\n      const rOptions = context.rOptions\n      const interceptOpts = matchUtil.matchHostnameAll(intercepts, rOptions.hostname, 'get interceptOpts')\n      if (!interceptOpts) { // 该域名没有配置拦截器，直接过\n        return\n      }\n\n      const matchIntercepts = []\n      const matchInterceptsOpts = {}\n      for (const regexp in interceptOpts) { // 遍历拦截配置\n        // 判断是否匹配拦截器\n        const matched = matchUtil.isMatched(rOptions.path, regexp)\n        if (matched == null) { // 拦截器匹配失败\n          continue\n        }\n\n        // 获取拦截器\n        const interceptOpt = interceptOpts[regexp]\n        // interceptOpt.key = regexp\n\n        // 添加exclusions字段，用于排除某些路径\n        // @since 1.8.5\n        if (interceptOpt.exclusions) {\n          let isExcluded = false\n          try {\n            const exclusions = getExclusionArray(interceptOpt.exclusions)\n            if (exclusions) {\n              for (const exclusion of exclusions) {\n                if (matchUtil.isMatched(rOptions.path, exclusion)) {\n                  log.debug(`拦截器配置排除了path：${rOptions.protocol}//${rOptions.hostname}:${rOptions.port}${rOptions.path}, exclusion: '${exclusion}', interceptOpt:`, interceptOpt)\n                  isExcluded = true\n                }\n              }\n            }\n          } catch (e) {\n            log.error(`判断拦截器是否排除当前path时出现异常, path: ${rOptions.path}, interceptOpt:`, interceptOpt, ', error:', e)\n          }\n          if (isExcluded) {\n            continue\n          }\n        }\n\n        log.debug(`拦截器匹配path成功：${rOptions.protocol}//${rOptions.hostname}:${rOptions.port}${rOptions.path}, regexp: ${regexp}, interceptOpt:`, interceptOpt)\n\n        // log.info(`interceptor matched, regexp: '${regexp}' =>`, JSON.stringify(interceptOpt), ', url:', url)\n        for (const impl of interceptorImpls) {\n          // 根据拦截配置挑选合适的拦截器来处理\n          if (impl.is && impl.is(interceptOpt)) {\n            let action = 'add'\n\n            // 如果存在同名拦截器，则order值越大，优先级越高\n            const matchedInterceptOpt = matchInterceptsOpts[impl.name]\n            if (matchedInterceptOpt) {\n              if (matchedInterceptOpt.order >= (interceptOpt.order || 0)) {\n                log.warn(`duplicate interceptor: ${impl.name}, hostname: ${rOptions.hostname}`)\n                continue\n              }\n              action = 'replace'\n            }\n\n            const interceptor = { name: impl.name, priority: impl.priority }\n            if (impl.requestIntercept) {\n              // req拦截器\n              interceptor.requestIntercept = (context, req, res, ssl, next) => {\n                return impl.requestIntercept(context, interceptOpt, req, res, ssl, next, matched, interceptOpts.matched)\n              }\n            } else if (impl.responseIntercept) {\n              // res拦截器\n              interceptor.responseIntercept = (context, req, res, proxyReq, proxyRes, ssl, next) => {\n                return impl.responseIntercept(context, interceptOpt, req, res, proxyReq, proxyRes, ssl, next, matched, interceptOpts.matched)\n              }\n            }\n\n            // log.info(`${action} interceptor: ${impl.name}, hostname: ${rOptions.hostname}, regexp: ${regexp}`)\n            if (action === 'add') {\n              matchIntercepts.push(interceptor)\n            } else {\n              matchIntercepts[matchedInterceptOpt.index] = interceptor\n            }\n            matchInterceptsOpts[impl.name] = {\n              order: interceptOpt.order || 0,\n              index: matchIntercepts.length - 1,\n            }\n          }\n        }\n      }\n\n      matchIntercepts.sort((a, b) => {\n        return a.priority - b.priority\n      })\n      // for (const interceptor of matchIntercepts) {\n      //   log.info('interceptor:', interceptor.name, 'priority:', interceptor.priority)\n      // }\n\n      return matchIntercepts\n    },\n  }\n\n  if (setting.rootCaFile) {\n    options.caCertPath = setting.rootCaFile.certPath\n    options.caKeyPath = setting.rootCaFile.keyPath\n  }\n  return options\n}\n"
  },
  {
    "path": "packages/mitmproxy/src/utils/util.js",
    "content": "// const os = require('os')\nconst log = require('./util.log.server')\n\nconst util = {\n  getNodeVersion () {\n    const version = process.version\n    log.info(version)\n  },\n}\nutil.getNodeVersion()\nmodule.exports = util\n"
  },
  {
    "path": "packages/mitmproxy/src/utils/util.log.server.js",
    "content": "const loggerFactory = require('@docmirror/dev-sidecar/src/utils/util.logger')\n\nconst logger = loggerFactory.getLogger('server')\n\nmodule.exports = logger\n"
  },
  {
    "path": "packages/mitmproxy/src/utils/util.match.js",
    "content": "const lodash = require('lodash')\nconst log = require('./util.log.server')\nconst mergeApi = require('@docmirror/dev-sidecar/src/merge')\n\nfunction isMatched (url, regexp) {\n  if (regexp === '.*' || regexp === '*' || regexp === 'true' || regexp === true) {\n    return [url]\n  }\n\n  try {\n    let urlRegexp = regexp\n    if (regexp[0] === '*' || regexp[0] === '?' || regexp[0] === '+') {\n      urlRegexp = `.${regexp}`\n    }\n    return url.match(urlRegexp)\n  } catch {\n    log.error('匹配串有问题:', regexp)\n    return null\n  }\n}\n\nfunction domainRegexply (target) {\n  if (target === '.*' || target === '*' || target === 'true' || target === true) {\n    return '^.*$'\n  }\n  return `^${target.replace(/\\./g, '\\\\.').replace(/\\*/g, '.*')}$`\n}\n\nfunction domainMapRegexply (hostMap) {\n  if (hostMap == null) {\n    return { origin: {} }\n  }\n  const regexpMap = {}\n  const origin = {} // 用于快速匹配，见matchHostname、matchHostnameAll方法\n  lodash.each(hostMap, (value, domain) => {\n    try {\n      // 将域名匹配串格式如 `.xxx.com` 转换为 `*.xxx.com`\n      if (domain[0] === '.') {\n        if (hostMap[`*${domain}`] != null) {\n          return // 如果已经有匹配串 `*.xxx.com`，则忽略 `.xxx.com`\n        }\n        domain = `*${domain}`\n      }\n\n      if (domain.includes('*') || domain[0] === '^') {\n        const regDomain = domain[0] !== '^' ? domainRegexply(domain) : domain\n        regexpMap[regDomain] = value\n\n        if (domain.indexOf('*') === 0 && domain.lastIndexOf('*') === 0) {\n          origin[domain] = value\n        }\n      } else {\n        origin[domain] = value\n      }\n    } catch (e) {\n      log.error('匹配串有问题:', domain, e)\n    }\n  })\n  regexpMap.origin = origin\n  return regexpMap\n}\n\nfunction matchHostname (hostMap, hostname, action) {\n  // log.error('matchHostname:', action, hostMap)\n\n  if (hostMap == null) {\n    log.warn(`matchHostname: ${action}: '${hostname}' Not-Matched, hostMap is null`)\n    return null\n  }\n  if (hostMap.origin == null) {\n    log.warn(`matchHostname: ${action}: '${hostname}' Not-Matched, hostMap.origin is null`)\n    return null\n  }\n\n  // 域名快速匹配：直接匹配（优先级最高）\n  let value = hostMap.origin[hostname]\n  if (value != null) {\n    log.info(`matchHostname: ${action}: '${hostname}' -> { \"${hostname}\": ${JSON.stringify(value)} }`)\n    return value // 快速匹配成功\n  }\n  // 域名快速匹配：三种前缀通配符匹配\n  value = hostMap.origin[`*.${hostname}`]\n  if (value != null) {\n    log.info(`matchHostname: ${action}: '${hostname}' -> { \"*.${hostname}\": ${JSON.stringify(value)} }`)\n    return value // 快速匹配成功\n  }\n  value = hostMap.origin[`*${hostname}`]\n  if (value != null) {\n    log.info(`matchHostname: ${action}: '${hostname}' -> { \"*${hostname}\": ${JSON.stringify(value)} }`)\n    return value // 快速匹配成功\n  }\n\n  // 通配符匹配 或 正则表达式匹配\n  for (const regexp in hostMap) {\n    if (regexp === 'origin') {\n      continue\n    }\n\n    // 正则表达式匹配\n    if (hostname.match(regexp)) {\n      value = hostMap[regexp]\n      log.info(`matchHostname: ${action}: '${hostname}' -> { \"${regexp}\": ${JSON.stringify(value)} }`)\n      return value\n    }\n  }\n\n  log.debug(`matchHostname: ${action}: '${hostname}' Not-Matched`)\n}\n\nfunction merge (oldObj, newObj) {\n  return lodash.mergeWith(oldObj, newObj, (objValue, srcValue) => {\n    if (lodash.isArray(objValue)) {\n      return srcValue\n    }\n  })\n}\n\nfunction matchHostnameAll (hostMap, hostname, action) {\n  // log.debug('matchHostname-all:', action, hostMap)\n\n  if (hostMap == null) {\n    log.warn(`matchHostname-all: ${action}: '${hostname}', hostMap is null`)\n    return null\n  }\n  if (hostMap.origin == null) {\n    log.warn(`matchHostname-all: ${action}: '${hostname}', hostMap.origin is null`)\n    return null\n  }\n\n  let values = {}\n  let value\n\n  // 通配符匹配 或 正则表达式匹配（优先级：1，最低）\n  for (const regexp in hostMap) {\n    if (regexp === 'origin') {\n      continue\n    }\n\n    // if (target.indexOf('*') < 0 && target[0] !== '^') {\n    //   continue // 不是通配符匹配串，也不是正则表达式，跳过\n    // }\n\n    // 正则表达式匹配\n    const matched = hostname.match(regexp)\n    if (matched) {\n      value = hostMap[regexp]\n      log.debug(`matchHostname-one: ${action}: '${hostname}' -> { \"${regexp}\": ${JSON.stringify(value)} }`)\n      values = merge(values, value)\n\n      // 设置matched\n      if (matched.length > 1) {\n        if (values.matched) {\n          // 合并array\n          matched.shift()\n          values.matched = [...values.matched, ...matched] // 拼接上多个matched\n\n          // 合并groups\n          if (matched.groups) {\n            values.matched.groups = merge(values.matched.groups, matched.groups)\n          } else {\n            values.matched.groups = matched.groups\n          }\n        } else {\n          values.matched = matched\n        }\n      }\n    }\n  }\n\n  // 域名快速匹配：直接匹配 或者 两种前缀通配符匹配\n  // 优先级：2\n  value = hostMap.origin[`*${hostname}`]\n  if (value) {\n    log.debug(`matchHostname-one: ${action}: '${hostname}' -> { \"*${hostname}\": ${JSON.stringify(value)} }`)\n    values = merge(values, value)\n  }\n  // 优先级：3\n  value = hostMap.origin[`*.${hostname}`]\n  if (value) {\n    log.debug(`matchHostname-one: ${action}: '${hostname}' -> { \"*.${hostname}\": ${JSON.stringify(value)} }`)\n    values = merge(values, value)\n  }\n  // 优先级：4，最高（注：优先级高的配置，可以覆盖优先级低的配置，甚至有空配置时，可以移除已有配置）\n  value = hostMap.origin[hostname]\n  if (value) {\n    log.debug(`matchHostname-one: ${action}: '${hostname}' -> { \"${hostname}\": ${JSON.stringify(value)} }`)\n    values = merge(values, value)\n  }\n\n  if (!lodash.isEmpty(values)) {\n    mergeApi.deleteNullItems(values)\n    log.info(`matchHostname-all: ${action}: '${hostname}':`, JSON.stringify(values))\n    return values\n  } else {\n    log.debug(`matchHostname-all: ${action}: '${hostname}' Not-Matched`)\n  }\n}\n\nmodule.exports = {\n  isMatched,\n  domainRegexply,\n  domainMapRegexply,\n  matchHostname,\n  matchHostnameAll,\n}\n"
  },
  {
    "path": "packages/mitmproxy/src/utils/util.process.js",
    "content": "module.exports = {\n  fireError (e) {\n    if (process.send) {\n      process.send({ type: 'error', event: e, message: e.message })\n    }\n  },\n  fireStatus (status) {\n    if (process.send) {\n      process.send({ type: 'status', event: status })\n    }\n  },\n}\n"
  },
  {
    "path": "packages/mitmproxy/test/baiduOcrTest.js",
    "content": "const AipOcrClient = require('baidu-aip-sdk').ocr\n\n// 设置APPID/AK/SK\nconst APP_ID = '101474620'\nconst API_KEY = 'fqCvIHGisGwpsglzV2wdxZJ5'\nconst SECRET_KEY = 'RhTOXUA4V6CrGuCTJJvUQ7z6Nl4m0Lij'\n\n// 新建一个对象，建议只保存一个对象调用服务接口\nconst client = new AipOcrClient(APP_ID, API_KEY, SECRET_KEY)\n\n// 图片：6525\nconst imageBase64 = 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAFAAAAAeCAYAAAC7Q5mxAAAac0lEQVRoQ0VaCXScV3m9/+yrZtOMdsmbZMm2LNnybqdpnMQJjpsQMEsbEgItnFNoaFLasJ7Tc6A0kHIgoQRogabQkp4QSGJiCHZCbMdObMeOFVu7ZFm7RhpJs+9r7/eUgIPQNvr//33vfvfe777R+iZ7K+A/fUV9grb6id+X1WebzYZ4PA6TyQBNr0MymYRm0MNmt6NQzMFqMaJULiOdKgA6EzSdGZUSYDLoUM7lYDPqkYiHEaj2IplJIZHJwOasQrpYgdVqQyGZhs/jxfLysrqfwWiGw+HA/Pw86urqkMryHlYrorGE+r3FbkM6nYbT6US2kIdmN2JhYQEN7mrkeS0vr+mssmNobBTeRj9i2TRMbhvKugpOnTqF1EoYsaUVtARqUCzlkdHlMbO8gCp/ADAb4fLXYNu2bXDqbSilsrAVjfDbq2AqVBBeWYHbXYU4rxnPJOAN+KH1T6wWUIc/FZD3gobVAubzeeh0OhikIHxNiR9miwUVFjwWj6DEh5Ai6wxW6DQT/5BFrPD1OqCUzUIr5qHXlZHLZaDTa3BXV6NQriC4EuXfllHj9iIWiaKqqgp6vR6pTJrXMeD48eOqqJrBqO5//8cfgNPhQiafUxvq8XgwNTuDgr6Mnu4eBKemkI/FYTNbeP8SCwYYHWYspxOoWPWIJKJ468IFOPl7s6ZHldGEuYU5lAwVVMw6ZDVgmX9PpCCd5CYbzPjAnXej1uEFUjlYuCa+BI98/mE898Kv8PXHH8MDDz4IbfDGagEFeoI+KZ769t0C5ogi2e1iuYB0LguT2ayKKLseDM4B/HlDQwP8NY2qgKWiFFuKyEXwbysssMNmQrlc5FUryBOtOX5YbdxVoi20SPTU1mFpaUkV8dlnn0U4HMFnH3pIbR5fiiw34u8+9xC+850nFBoFgfX19bj/Ew/i0JHDOHToEPKJBOo81Sjm8rxPGQZ2zEoygunlRbgCHlx4+yIs3Pjp6+P46/s/LshQG9M7cBWTwVlk+XwFTYORCLZZ7UhFEigmsqir8mLHli50rFmPDO8bJWiKrICn1o9ILAZtaPxPBfxj8aSI2ioCBWnSrul8GvlCASarBaHlJfQPDGB5aRHsZrS1taGjfSvKJR3YVUQB4U/k8f9gY4snk1HVViV+n2QL601mXotoLpbRUF/Hdp3F8WPHia4M7jlyD+qbGtUGeYhO+VfgfY1mE1HM+xMh8i+VysBC5MvC06QVN7+udnkwPXFDtX6ukEOOC40XM8iUc7g6eA2TNyawcf16dLZvgtdRpfrJyU2b5zri3KQx/m3f0BDMZivcdheSyxHUuqpx867d0PFZvT43SqyHxWHB5atXsGVrF7SR9wrIy73Hf6sQXC1gmX+gcWeypZwqnvDfMPllYGgQBSJSENi+cSO2b9sNgg3ZTAlV5IwcecLM1xoNGubmp9DS0oRkOsmFp+En4tKZgkLEk09+lxRgwYc++GFUB3zIkHeEa6PRqFqINE6pVFKvFVqWjhAUrqxEEKitIWIs/HoFFp0BZGlEwyvwer2IJmOwCl/lkhgYG8Kbl85j7dq1uPvwXbwmEJycRnMjnymZgpW8usL2Les0IteMocERTBCpdpMF29q3sNhObN3UgemZSZI029/nwvMvvYi9+/ZDGxvrrVTY3Nq7HLjawETeuwXUGwyqdct6kj5vFE0mcK2/D5Mz0zBSIIpEzZrmFuzbeYC11NhCFTisTqIiDrvdingigppaH8KxMEKhRWzlrk3w4Z9++n8UNXzuc5/BSnSFaOX/WCjhN/loamoi8krkdROeeOIJPPLIIzh27CUcPXoUkUgEa1rWYm5ujm0URWvrRmTTGRTIn9VeHxJs5xx3U2c1Y3B8GIOjI8gUMujq6sTj3/oWfvrjn8DJTfjG1/4Ff//Qw3C7KHD8+yzbWmqRIw2UifY4uXlNXQOqybczMzNs+TJOnT2D7bt7sH5jK7uTdRtnAQVr74mIQh0vIgWUTjawdTIsoMFqVGgcnRhXD5RIJZWwlKjEtdV+3LT7ZiKA7VXQsYAOomKJRO/iBSgYi7Ooa6yj6hXwn//xYyLTjAce+KRC2XIkhN6rb+PBB0jIRPoAqcFMnrVzgYl4Ci6XC6+++iru/9jHEJxfhNNmV6KVzeZVa9uIDilkE3lYxEiQmqfQmB02JLMZnCfyliJhfp1EoKaGPM0W5GvkOk0NzWx7H4x60gOvVSbhCmX5fQEEZ+fw5HefwFe/+AUUyUuBQIAFTpFLL2FDeytcbOfZ2dnVAr6LuffApwooRCyf8+QtXhp2J+1MOoUrfVcxM0/xYJvJa1hCuGlLbt57C2yUfl3JoDhwcTEIT7WHViNN22LBUz/8PlwsaFdnF6YmpmHWWTA2fh3eGhf0RLIIw6b2DvT07EBoYRG1tbWYnZ6mPcqqIgqBi1iVcqU/OgOxO1LIR//xUTz22GO0GG4sceOqqfSpfBaDw8PoH+zj5pu5eLoAioYIk9PtVGgX1C8HV3DvPffSZtWQtSooF0vIEY3uKo+iMaG1KG2YFL1s4O9YDyvXk2AtRNy08ZErqzaGaJIbyC4UyiVaCo1kT49HHIYTMRaBPBGP4vjvX1aEvnv/Plx++y0UicBCJov7PnAf9EU9vDYvC7Ck/FKabfOTn7FdPHYcvP0glrm4wb4BWI1U0hjFxGigjWCrpGLc4VqloGJP9u/dxwe2IpVIQk9UhsNhlAplIteoWlXQIKrd1NTCDqiQSozsBoPqFKPFrLpDfOup189wIxdRz8IvrSxTXIhA3lPadHpuls9dIAD0bFEf7rjtDuRFmHgPp92B8FJYcbN4TZeHlERRKoAtTp5cXFnAjYkJTE1OQpu+0VchrpVCyu4IhFdvUmbLcYuItCK3wev34sRrryJEktbRcLZRON68+CaMJv6eZvfu2/4CVljUf8dfPI4wec3msuPofR+CwW7AufPnMDXFG3LBJp0RtZ5axV96m8YHK1B88khRTX0+H9aSU+tr68ltrQjOzCp7U8oWVSENOj2ef/55ZV0splVLY6eiSvEaW5oRJ0db6RqSqTiGqKjyN4loDO87cpe6TrFC1eZrZ1jAkbFxxbNZCtvGtRuwecNGWPVGRUEVAkn4X5xBibVIk34q+pJS7N5rvUoQa7zV0K6PXqno+MIyfZtwiphZgWuJNxIOKZEhzRSPqflpnDr3upL9fTffhByLffbCOU4OGpLxBO6+9QheOXYC+gyRSyP8wIP3K7dudlvx+9MnEFoJqXs0VNdix9btcOhsih7+cOE1blBZtYMgsMgWqqdKd27eTAtkhcNsR4bWx04DLC1b5gbkiHj5eoIo8Pv9ynzPBOfh81crkXN7PcruFPMF5Ficjo3tmKKF8XFyiHHhbp+odIJrmsWlty/znvSodA9Hj9yNx7/2DfzVhz6K3Tt20lhHYKtyUs05GS2H8PbAFYQpYPThtGG0SZkcfeDQpYrYBgM/dCyeXtqYKBTjLAs0sSVKhPzw+AhOv3EW7R0daKeki3144fiLSGQT8JADzQUjPnLkKPJhLs5ix8DwANq2tGMlG8OFqxeRzKXIK1U49Oe3QsuWcPbEGez/s5tg8dtw/vJFXLlyBR6XWxVQOGjvzl1Y27JGbUYjCzpPJCZiSaX4otLyfXd3N0UoTI6KwSDtViQALCbMkNyrqPCcYdR45+eoKJuRJG/VNjZgMbyMBX6YKDTHTv4WNTV+2PhqA2lif/cueGwu5S39dbWI8m+uz03hxuwkFih40sIFKnyaP/c63dAmpvorWaqWxqIJt1U4XqWJPBEI8X2ixTFyyjH6nrrmRphtVhy4+c84o2Zw7Le/gcFpQmg+iL2bd2HHxm6UE0UKr56vrcdUcAbDc+O4PHiF01Aah+96H9bXNyNG4vZZaEp57cGZUXR0bcbp06cxzJar9voVEtc2NmP3rl2IRzmK5Yt46cVjuP++B8g2RLiYLj6njh5TFpSjR3Ww9cOkhBQN/Pj4OKz0c34a62p60snxG/jKo1/GM888o4y3gwpqdTmJ/tfRNzXEwWARzaQULzf+g7ffRXiV8d//9TN88tOfwlvvvIOZpXlEOEjEORbK+vVGHR0CeZuTjzY1N1SRFily94SIRUhEoTQ+nJkFFLIO88XnyXdOrxtjN8ZRRVV0ENpRtmi6klXIqbNW49Deg6i2ehFfiXG3E7D5qnDq8jnEikkEObB7PG7csucAvGYnEgtR1UquFj/GZyYwOzWtFj43M0+P1wIrx8HfPP8Cfvb0z5GMxGjSS1gOLdG/2RTFBGidhMdE3Mx2M1ZSKc7XnJBGhhSP54i8TRva0NXWAZ/ThYGr/Uql5zg6JqnQgZZGzCdCGFgcJ2CoqMtRbG1pw4GuXfjmP38dn//8P3HNl3jdODKkGJ2dhp1i2rSmUQ0FIyMjWAkuro5yqmjcGeECeTjxYWlyxQrbQzjmmV8+q4ZkUeNHv/RFzAeD6neTRNj00iwVsoidrd2wlUzoad+GTDytKOAb3/kWdh26CZf6LqOlba2CfYCz5eGbb0clxiSFrn4hE4WvIUD3P4ix4RHOzU6M8rOOluJLj34BkaUIEvR3NSzYbQdvxaXzF1ULixrbnXbFrR4a9TjDiuFJetSRUQWE0PwC1tTU4+zJP+DJx7+NRDiubJDweo5cHGK4cPbaBcxXIrRrBbh0VlQbHdjT3g1dvoKrV95h1+RhclVhdHYKnsZaNG9Yh9aNG8jXabz+2ilUaLa1a4MXKk4iioMsYiRW4TwZlW5M3VC8lCAXGGhnLNyBPXv2qN2VWfPhf3gEB++8DQvJZYXYGtqXeygkWrqEhZkgkeGAnfJ/vv8KHzZMC7SsFlbF8WhNoBGd69qVmie0PEpGcjjJ/tyZc1haDKGluZnjoAON9U1oqm9QRlcmkiKFKxmJo4bRU3iZI5vPQ/QkeB29QtULv32J7cwggfcRpNZXB7Bn+w5GZjnUM74S42urcqkCRhJxnKCAJa15hht5GOgvO4nAXDiJBDvIzNEwSbVOUYicdCBdO3ugI78KijW6ktMsYJLjpDY92V9JUwElvRC78uRT30d1bYAcl6I9cKhxS2KljvY2bOMYJvLutjuVsX3z8gVMhRdUu1eZHejeuBlbWrdwkQlM0wSX6SULvObVob5VL0lDaybafWz5Fo5IJvJJkruf4oNGaTcmr08ofhPUyziW4cPnaTPEzmxgO5ro0eJ8aOG24PQcvvmvX6eY6fDUj76HqZk5ZcyTbGXZZBGfw4cP00um4KEpNrKoWc7RMnPlCZZJerirYwOIFiksTXWKx1vXrEMxnVdUkSfC5TkYcGLLli0KvcrSsUutVPiXf/c7hikrjLOuna+cfPUVzC7Mw+334b5PfFwtbHRiTLWpoFB29CAVU3hH44JijJscVLWxyeu4NNqvXL5MCBtobLdt6qZRJlqoaMJxDF0wfH0U1yeuq5vX0UqsX9MCFzcmIV5qfQue/80xZGiaZWNiHO8kMDBSBBxuD1GQYVFocC02tDQ1I8CEpopxk4mFiJDzKnox3y61yVIUNXaSP2XBDgqIjSCQf1YKhFifHLlekDg1NYOl6DKq6wMYGB1C56bN2Lt7DybIw6Ojo6QSO+Y5zvV0b0OtjIBch9TBqCe6+ey9rEtoiXnlzNRAZSEUQvO6FoUSGdc0ttZj3/4mdu7epcy1GNAD+/aqRUo6Mjo4BF+VG8tszUVmbpL2Zsl7oFpaOaJVyIk+qmlnVzfs5BDxZdlCVplaSWi8/JlwnPDRqbfeJI8tK+WVSUNQJhQSpvkVq8QnVoY+R3sjo9Z6bpJ8SChqYUs5adaHRgbR2NiIa9cYWbGIDhprcQox5nUydojZlulDJpaUiA05PE7KEKOcoEhIYdqI8q6uLnz1y19Be3s77ASIWJ+enh612RJgWOlF5fnEY84wwB0YGmELTw9WxIjKPFrhOGcmMa/QDswszKrUJU4OFFGpZ1vLsO4lh8jOyA3ylQLCjIscLgfHOKKdSqmXTDCdJfzrVDT+8isnsefAfpWu5IhUA1diYGsb2Ro2hxWLJPOf/98v0BCox+6dO1ChURbDKwb+yrWr9J9jsLCgYqmijOPrfDXYt2snXBahEY565D9blQN5orZAhJ88cUIVPBhcwO133oHuzq1qNBXhmeUML6mKbKSgXMZWCQ7mmSwdPnyEDmAamzd3Mrw4qZ4jwUjswL6bOB2xExhsSPokAa8cV8iMfubcWRZwdqgi8LfQiEpokKYndNJuvH7+LN39HEXFoqaTPJUnz5FNkpACUSmI0TjGUW85M9OIs3ANPE/ooDhIC1eYTMtYluV04yU1iNCkM0lYOCoViTzhGD19Z5Ajnwz+HRvaEeI00Uj/l2AnyBwus/fE3Ayu9fUp/rGTQ4UT2tetQ/emrQqpYp6/98On8NBn/lb5Shnbnv3Vc4zNupkCzSuHIDGcgECmp2Qq8ceJy8hnqfAs58D+/eRnBwPcKt5bEiAnfv3LX6tZ2OfzK6MtFFKh+KQJDjt/LvTw6plT0Hp7z1bWt27AT5/+KT78lx9VhJ7mgmR2Fc8kqiwtLNWXVEQcvvqeM6mV7fPciZfUaGMoalhb34guJtMSRKapfGKJNL5OBvIsi2YSdaS6ctJmIUpq5p4OBbFpayfGhoaVwn36bz7F5OYHyuK4qt0IRcM4c/Y0i7EIL38fXlyCn5nfnbfcqvioQoWmLCrREY6TzQ4tLeC1M6cVwqRwwtESlUnrZbk2aWMBQg3Rt30jU2WmzRqjpxgRJ/wpQ8Q8/ahEctVUcjMpbf36Vm4qz1boSeX3UtDXz52hkZ7oU0aaZyZKwQINdWrkEc7yMRaSqUQdJFmZC/J14hnlZE64IEvM/uKl51RcZCOtNzMA6GrbAicvztl79SyFtkJGQuEZdi5nU96LBZX0WNTQSLsjXGhmMaQgwjMyhwe5eTqbUan4cngJb/f28vf0q0R+DVFx5PY72FZJRKJJ1XZyBCCzPB8QfibbIhRyZjM5PaWeO8/CCYoNcqLI4kmi08AIq7VuHQauDbDVu7hO9icp5nv//iQT9h60tm3gwRkDZFJJWxuDBgbEUSJcwGDmcw6ODUObHOmtJMmBdZz7ZCbuZ1Qv8Y+Vvm9uIajicRmqvZxCRJXlxjJQS+YWSobx4qmXVdxfpv9bW9eIvT17UO1Y5Qx1MkAEqsMo7r5YFJlzq7gAKXKMXqxI+6SOJ8l5kr2JD5RQIMPRz+Ik93FeFa594YXnFYpE/RnL4Y5bD0Ijbeg0CVB54EWBC3ANwm+yQLEgMfKrIFO4Voq7wo0QhNbUBJiW81iWCZClZMY6jpciLEU+gwiNxGzSPRL4Cqp/8KMfocrtUmtQkRlfI4daeW60dr3vrYogJUa+EdGQAyLZRSF8I/9AbpQlnMQ8SjtLa8nhknBa0azhdzSjy4zti8kCmgN1TFp6UO+tUamJHBToyDOStKjpgEGpxFM7t3UrJGqkhQLnyitX3+EM7FFhZjt3eoKxl9FsYKpsQYScZeSp3vnzb6rfC3LFwuzZ0YNUNEXfxkMs8pdNTuvoXYUXE9yYkydP4uhHjqoi+BjsStvKeCl5o/wTUbEaLNDluCkMgGVzZGCQ14lgCX/3DfQrVd9EH1jPEOLll1/GR0hzIboWoZsgZ2htYWqoImORlcQoyiSLlN1yyM6ToGXxKpHhf8KPchMXRUZ2KKPjkH/+D0yCckw9+LqKgcO7i0LBZJAPV0XFrm9oQi1P3iTYlCQly0OcSIj+iwmJKOjbQ/3oY3KjsdXFe0nCIqpXkHySyKuwyJM8zBnkqJdhqCHtK35TpgxiBR5HgMKRUgGwWA05OZOWraet6X3nCtZRcESUBDmyJhFMdUhFr9tQ04DIwrLK/8bGxuCRmIsFl8N7qYFYH0GdXMvmsCt/2L19myq+FDzFMFkb7r9YEVg7eEHJz9489wZuu+Uggjx0lt2UF+oJ2bJWUhmatPEST8HC0QgtTAIJYw56cqDG+VFSDAuTGF2ZYsGZ1s9FdtBGCDH/7zO/wF3vO6wQJIZZWtnII89XLp5BhMeeEsrKOYWclG3fvp2zNLeMhzgSZLxBQRNkZFmYLa3tRN9OZPhOBY/DjXsPfwDPPvOsEgtZdB2RMsFzGwFDZ2enOoKVGVy4Ww65xHPKKCrTyuwURzvjKnAu8NBdqEQKJi0qVLWtZ7sqtpl1EDBJLSSxkp+l+CwG6oI2OHypIoLwMA+uv/34v6ngUqIiI3dU+EOKKzsqbeRgqy9QmXqvXsU8+bFk0SGukwvRXJZpgPm2Do+8DUIzq6hcDqj95EUd214QKNNBmqQsSW6YLt5sM+PiyDucNhI8BaMqkyZ8bh86OjYq4REEzPMkT45QpfUcbK3O1k3Y3NaKpdlFdXZrM9r4fOwAboCdgiSJTvPaNTwhzKvZXpRY0CZvH5HYTgonFLVIznvjjTeYapvVoZQ8o1DZvffeq0TSzq9lQ2RjZDNlipIESn4ndJSXEJYUpk0ujVUybKsshUSmC0lw7RzEey+9pY4IpVUFsuL1xB9KG4tCytzsDnhhcFMcyFc6+j5TmWemHJkMLKZE5TqxMOTCENumlamwCIS89SLC4vl5DrEYXcRSIcr3sQzwCJGxERfcXNek+EXyRmk7+SwBr4hCU109330Q4OGVHqU0D9tF2fl+HFmkCJSInpz9jvOAPJsmomm3VERHUXDx9G6ezy2cfpFrk/BCihkKLaCaPvX973+/AovM/2K2ZbKRAsokluNmqNM+bqqgT9As3RljnKed6T9TcTFRtss7DhgB+TgGFbibHu7mQ5/5rEKlnJkUGGHH+eYgMdoUP5Vey/tKNKumTuzkQEkGcbO0sKS7PPewso1NbEs55AlH46oIMo7JJgWIwmA4iLyjjP6xQSYgESxwoJcUpMzdpTVTeaQcEtVwFhWhqGMKY6ICp+jFPKQBOVVzEZk1bMnr168rtEmyJFxYU8PDe45wsgmyaEHOjfFJbCB6Rak93MBaTldWWqUIaUkKK60r+afQgAwKi2x/8ZxSOFFjNYpys9R9JGThad3/A6cC8EsBkFJyAAAAAElFTkSuQmCC'\n\nconst options = {}\noptions.detect_direction = 'false'\noptions.paragraph = 'false'\noptions.probability = 'false'\n\n// 调用通用文字识别（高精度版）（异步）\nclient.accurateBasic(imageBase64, options).then((result) => {\n  console.log(JSON.stringify(result))\n}).catch((err) => {\n  // 如果发生网络错误\n  console.log(err)\n})\n"
  },
  {
    "path": "packages/mitmproxy/test/dnsSpeedTest.js",
    "content": "const dns = require('../src/lib/dns/index.js')\nconst SpeedTest = require('../src/lib/speed/index.js')\nconst SpeedTester = require('../src/lib/speed/SpeedTester.js')\n\nconst dnsMap = dns.initDNS({\n  cloudflare: {\n    type: 'https',\n    server: 'https://1.1.1.1/dns-query',\n    cacheSize: 1000,\n  },\n  // py233: { //污染\n  //   type: 'https',\n  //   server: ' https://i.233py.com/dns-query',\n  //   cacheSize: 1000\n  // }\n  // google: { //不可用\n  //   type: 'https',\n  //   server: 'https://8.8.8.8/dns-query',\n  //   cacheSize: 1000\n  // },\n  // dnsSB: { //不可用\n  //   type: 'https',\n  //   server: 'https://doh.dns.sb/dns-query',\n  //   cacheSize: 1000\n  // }\n})\n\nSpeedTest.initSpeedTest({ hostnameList: {}, dnsMap })\n\nconst tester = new SpeedTester({ hostname: 'github.com' })\ntester.test().then(() => {\n  console.log('github.com  tester.alive = ', tester.alive)\n})\n"
  },
  {
    "path": "packages/mitmproxy/test/dnsTest-abroad-doh-sni.mjs",
    "content": "import DNSOverHTTPS from \"../src/lib/dns/https.js\";\n\n// 境外DNS的DoH配置sni测试\nconst servers = [\n\t'https://dns.quad9.net/dns-query',\n\t'https://max.rethinkdns.com/dns-query',\n\t'https://sky.rethinkdns.com/dns-query',\n\t'https://doh.opendns.com/dns-query',\n\t'https://cloudflare-dns.com/dns-query',\n\t'https://dns.google/dns-query',\n\t'https://dns.bebasid.com/unfiltered',\n\t'https://0ms.dev/dns-query',\n\t'https://dns.decloudus.com/dns-query',\n\t'https://wikimedia-dns.org/dns-query',\n\t'https://doh.applied-privacy.net/query',\n\t'https://private.canadianshield.cira.ca/dns-query',\n\t// 'https://dns.controld.com/comss', // 可直连，无需SNI\n\t'https://kaitain.restena.lu/dns-query',\n\t'https://doh.libredns.gr/dns-query',\n\t'https://doh.libredns.gr/ads',\n\t'https://dns.switch.ch/dns-query',\n\t'https://doh.nl.ahadns.net/dns-query',\n\t'https://doh.la.ahadns.net/dns-query',\n\t'https://dns.dnswarden.com/uncensored',\n\t'https://doh.ffmuc.net/dns-query',\n\t'https://dns.oszx.co/dns-query',\n\t'https://doh.tiarap.org/dns-query',\n\t'https://jp.tiarap.org/dns-query',\n\t'https://dns.adguard.com/dns-query',\n\t'https://rubyfish.cn/dns-query',\n\t'https://i.233py.com/dns-query',\n]\n\nconst hostnames = [\n\t'github.com',\n\t'mvnrepository.com',\n]\nconst sni = 'baidu.com'\n// const sni = ''\n\nconsole.log(`\\n--------------- 测试DoH的SNI功能：共 ${servers.length} 个服务，${hostnames.length} 个域名，SNI: ${sni || '无'} ---------------\\n`)\n\nlet n = 0\nlet success = 0\nlet error = 0\nconst arr = []\n\nfunction count (isSuccess, hostname, idx, dns, result, cost) {\n\tif (isSuccess) {\n\t\tsuccess++\n\t\tconst ipList = []\n\t\tfor (const answer of result.answers) {\n\t\t\tipList[ipList.length] = answer.data;\n\t\t}\n\t\tarr[idx] = `${dns.dnsServer} : ${hostname} -> [ ${ipList.join(', ')} ] , cost: ${cost} ms`;\n\t} else {\n\t\terror++\n\t}\n\n\tn++\n\n\tif (n === servers.length * hostnames.length) {\n\t\tconsole.info(`\\n\\n=============================================================================\\n全部测完：总计：${servers.length * hostnames.length}, 成功：${success}，失败：${error}`);\n\t\tfor (const item of arr) {\n\t\t\tif (item) {\n\t\t\t\tconsole.info(item);\n\t\t\t}\n\t\t}\n\t\tconsole.info('=============================================================================\\n\\n')\n\t}\n}\n\nlet x = 0;\nfor (let i = 0; i < servers.length; i++) {\n\tfor (const hostname of hostnames) {\n\t\tconst dns = new DNSOverHTTPS(`dns-${i}-${hostname}`, null, null, servers[i], sni)\n\t\tconst start = Date.now()\n\t\tconst idx = x;\n\t\tdns._doDnsQuery(hostname)\n\t\t\t.then((result) => {\n\t\t\t\tconsole.info(`===> ${dns.dnsServer}: ${hostname} ->`, result.answers, '\\n\\n')\n\t\t\t\tcount(true, hostname, idx, dns, result, Date.now() - start)\n\t\t\t})\n\t\t\t.catch((e) => {\n\t\t\t\tconsole.error(`===> ${dns.dnsServer}: ${hostname} 失败：`, e, '\\n\\n')\n\t\t\t\tcount(false, hostname)\n\t\t\t})\n\t\tx++;\n\t}\n}\n"
  },
  {
    "path": "packages/mitmproxy/test/dnsTest-abroad-dot-sni.mjs",
    "content": "import DNSOverTLS from \"../src/lib/dns/tls.js\";\n\n// 境外DNS的DoT配置sni测试\nconst servers = [\n\t// 'dot.360.cn',\n\n\t'1.1.1.1', // 可直连，无需SNI（有时候可以，有时候不行）\n\t'one.one.one.one',\n\t'cloudflare-dns.com',\n\t'security.cloudflare-dns.com',\n\t'family.cloudflare-dns.com',\n\t'1dot1dot1dot1.cloudflare-dns.com',\n\n\t'dot.sb',\n\t'185.222.222.222',\n\t'45.11.45.11',\n\n\t'dns.adguard.com',\n\t'dns.adguard-dns.com',\n\t'dns-family.adguard.com',\n\t'family.adguard-dns.com',\n\t'dns-unfiltered.adguard.com',\n\t'unfiltered.adguard-dns.com',\n\t'dns.bebasid.com',\n\t'unfiltered.dns.bebasid.com',\n\t'antivirus.bebasid.com',\n\t'internetsehat.bebasid.com',\n\t'family-adblock.bebasid.com',\n\t'oisd.dns.bebasid.com',\n\t'hagezi.dns.bebasid.com',\n\t'dns.cfiec.net',\n\t'dns.opendns.com',\n\t'familyshield.opendns.com',\n\t'sandbox.opendns.com',\n\t'family-filter-dns.cleanbrowsing.org',\n\t'adult-filter-dns.cleanbrowsing.org',\n\t'security-filter-dns.cleanbrowsing.org',\n\t'p0.freedns.controld.com',\n\t'p1.freedns.controld.com',\n\t'p2.freedns.controld.com',\n\t'p3.freedns.controld.com',\n\t'dns.decloudus.com',\n\t'getdnsapi.net',\n\t'dnsovertls.sinodun.com',\n\t'dnsovertls1.sinodun.com',\n\t'dns.de.futuredns.eu.org',\n\t'dns.us.futuredns.eu.org',\n\t'unicast.censurfridns.dk',\n]\n\nconst hostnames = [\n\t'github.com',\n\t'mvnrepository.com',\n]\nconst sni = 'baidu.com'\n// const sni = ''\n\nconsole.log(`\\n--------------- 测试DoT的SNI功能：共 ${servers.length} 个服务，${hostnames.length} 个域名，SNI: ${sni || '无'} ---------------\\n`)\n\nlet n = 0\nlet success = 0\nlet error = 0\nconst arr = []\n\nfunction count (isSuccess, hostname, idx, dns, result, cost) {\n\tif (isSuccess) {\n\t\tsuccess++\n\t\tconst ipList = []\n\t\tfor (const answer of result.answers) {\n\t\t\tipList[ipList.length] = answer.data;\n\t\t}\n\t\tarr[idx] = `${dns.dnsServer} : ${hostname} -> [ ${ipList.join(', ')} ] , cost: ${cost} ms`;\n\t} else {\n\t\terror++\n\t}\n\n\tn++\n\n\tif (n === servers.length * hostnames.length) {\n\t\tconsole.info(`\\n\\n=============================================================================\\n全部测完：总计：${servers.length * hostnames.length}, 成功：${success}，失败：${error}`);\n\t\tfor (const item of arr) {\n\t\t\tif (item) {\n\t\t\t\tconsole.info(item);\n\t\t\t}\n\t\t}\n\t\tconsole.info('=============================================================================\\n\\n')\n\t}\n}\n\nlet x = 0;\nfor (let i = 0; i < servers.length; i++) {\n\tfor (const hostname of hostnames) {\n\t\tconst dns = new DNSOverTLS(`dns-${i}-${hostname}`, null, null, servers[i], null, sni)\n\t\tconst start = Date.now()\n\t\tconst idx = x;\n\t\tdns._doDnsQuery(hostname)\n\t\t\t.then((result) => {\n\t\t\t\tconsole.info(`===> ${dns.dnsServer}: ${hostname} ->`, result.answers, '\\n\\n')\n\t\t\t\tcount(true, hostname, idx, dns, result, Date.now() - start)\n\t\t\t})\n\t\t\t.catch((e) => {\n\t\t\t\tconsole.error(`===> ${dns.dnsServer}: ${hostname} 失败：`, e, '\\n\\n')\n\t\t\t\tcount(false, hostname)\n\t\t\t})\n\t\tx++;\n\t}\n}\n"
  },
  {
    "path": "packages/mitmproxy/test/dnsTest-abroad.mjs",
    "content": "import assert from 'node:assert'\nimport dns from '../src/lib/dns/index.js'\nimport matchUtil from '../src/utils/util.match.js'\n\nconst presetIp = '100.100.100.100'\nconst preSetIpList = matchUtil.domainMapRegexply({\n  'xxx.com': [\n    presetIp\n  ]\n})\n\n// 境外DNS测试\nconst dnsProviders = dns.initDNS({\n  // udp\n  cloudflareUdp: {\n    server: 'udp://1.1.1.1',\n  },\n  quad9Udp: {\n    server: 'udp://9.9.9.9',\n  },\n\n  // tcp\n  cloudflareTcp: {\n    server: 'tcp://1.1.1.1',\n  },\n  quad9Tcp: {\n    server: 'tcp://9.9.9.9',\n  },\n\n  // https\n  cloudflare: {\n    server: 'https://1.1.1.1/dns-query',\n  },\n  quad9: {\n    server: 'https://9.9.9.9/dns-query',\n    forSNI: true,\n  },\n  rubyfish: {\n    server: 'https://rubyfish.cn/dns-query',\n  },\n  py233: {\n    server: ' https://i.233py.com/dns-query',\n  },\n\n  // tls\n  cloudflareTLS: {\n    type: 'tls',\n    server: '1.1.1.1',\n    servername: 'cloudflare-dns.com',\n  },\n  quad9TLS: {\n    server: 'tls://9.9.9.9',\n    servername: 'dns.quad9.net',\n  },\n}, preSetIpList)\n\n\nconst hasPresetHostname = 'xxx.com'\nconst noPresetHostname = 'yyy.com'\n\nconst hostname1 = 'github.com'\nconst hostname2 = 'api.github.com'\nconst hostname3 = 'hk.docmirror.cn'\nconst hostname4 = 'github.docmirror.cn'\nconst hostname5 = 'gh.docmirror.top'\nconst hostname6 = 'gh2.docmirror.top'\n\nlet ip\n\n\nconsole.log('\\n--------------- test ForSNI ---------------\\n')\nconsole.log(`===> test ForSNI: ${dnsProviders.ForSNI.dnsName}`, '\\n\\n')\nassert.strictEqual(dnsProviders.ForSNI, dnsProviders.quad9)\n\n\nconsole.log('\\n--------------- test PreSet ---------------\\n')\nip = await dnsProviders.PreSet.lookup(hasPresetHostname)\nconsole.log(`===> test PreSet: ${hasPresetHostname} ->`, ip, '\\n\\n')\nconsole.log('\\n\\n')\nassert.strictEqual(ip, presetIp) // 预设过IP，等于预设的IP\n\nip = await dnsProviders.PreSet.lookup(noPresetHostname)\nconsole.log(`===> test PreSet: ${noPresetHostname} ->`, ip, '\\n\\n')\nconsole.log('\\n\\n')\nassert.strictEqual(ip, noPresetHostname) // 未预设IP，等于域名自己\n\n\nconsole.log('\\n--------------- test udp ---------------\\n')\nip = await dnsProviders.cloudflareUdp.lookup(hasPresetHostname)\nassert.strictEqual(ip, presetIp) // test preset\nconsole.log('\\n\\n')\n\nassert.strictEqual(dnsProviders.cloudflareUdp.dnsType, 'UDP')\nip = await dnsProviders.cloudflareUdp.lookup(hostname1)\nconsole.log(`===> test cloudflare: ${hostname1} ->`, ip, '\\n\\n')\n\nassert.strictEqual(dnsProviders.quad9Udp.dnsType, 'UDP')\nip = await dnsProviders.quad9Udp.lookup(hostname1)\nconsole.log(`===> test quad9: ${hostname1} ->`, ip, '\\n\\n')\n\n\nconsole.log('\\n--------------- test tcp ---------------\\n')\nip = await dnsProviders.cloudflareTcp.lookup(hasPresetHostname)\nassert.strictEqual(ip, presetIp) // test preset\nconsole.log('\\n\\n')\n\nassert.strictEqual(dnsProviders.cloudflareTcp.dnsType, 'TCP')\nip = await dnsProviders.cloudflareTcp.lookup(hostname1)\nconsole.log(`===> test cloudflare: ${hostname1} ->`, ip, '\\n\\n')\n\nassert.strictEqual(dnsProviders.quad9Tcp.dnsType, 'TCP')\nip = await dnsProviders.quad9Tcp.lookup(hostname1)\nconsole.log(`===> test quad9: ${hostname1} ->`, ip, '\\n\\n')\n\n\nconsole.log('\\n--------------- test https ---------------\\n')\nip = await dnsProviders.cloudflare.lookup(hasPresetHostname)\nassert.strictEqual(ip, presetIp) // test preset\nconsole.log('\\n\\n')\n\nassert.strictEqual(dnsProviders.cloudflare.dnsType, 'HTTPS')\nip = await dnsProviders.cloudflare.lookup(hostname1)\nconsole.log(`===> test cloudflare: ${hostname1} ->`, ip, '\\n\\n')\n\nassert.strictEqual(dnsProviders.quad9.dnsType, 'HTTPS')\nip = await dnsProviders.quad9.lookup(hostname1)\nconsole.log(`===> test quad9: ${hostname1} ->`, ip, '\\n\\n')\n\nassert.strictEqual(dnsProviders.rubyfish.dnsType, 'HTTPS')\nip = await dnsProviders.rubyfish.lookup(hostname1)\nconsole.log(`===> test rubyfish: ${hostname1} ->`, ip, '\\n\\n')\n\nassert.strictEqual(dnsProviders.py233.dnsType, 'HTTPS')\nip = await dnsProviders.py233.lookup(hostname1)\nconsole.log(`===> test py233: ${hostname1} ->`, ip, '\\n\\n')\n\n\nconsole.log('\\n--------------- test TLS ---------------\\n')\nip = await dnsProviders.cloudflareTLS.lookup(hasPresetHostname)\nassert.strictEqual(ip, presetIp) // test preset\nconsole.log('\\n\\n')\n\nassert.strictEqual(dnsProviders.cloudflareTLS.dnsType, 'TLS')\nip = await dnsProviders.cloudflareTLS.lookup(hostname1)\nconsole.log(`===> test cloudflareTLS: ${hostname1} ->`, ip, '\\n\\n')\n\nassert.strictEqual(dnsProviders.quad9TLS.dnsType, 'TLS')\nip = await dnsProviders.quad9TLS.lookup(hostname1)\nconsole.log(`===> test quad9TLS: ${hostname1} ->`, ip, '\\n\\n')\n"
  },
  {
    "path": "packages/mitmproxy/test/dnsTest.mjs",
    "content": "import assert from 'node:assert'\nimport dns from '../src/lib/dns/index.js'\nimport matchUtil from '../src/utils/util.match.js'\n\nconst presetIp = '100.100.100.100'\nconst preSetIpList = matchUtil.domainMapRegexply({\n  'xxx.com': [\n    presetIp\n  ]\n})\n\n// 常用DNS测试\nconst dnsProviders = dns.initDNS({\n  // https\n  aliyun: {\n    type: 'https',\n    server: 'https://dns.alidns.com/dns-query',\n    cacheSize: 1000,\n  },\n  aliyun2: {\n    type: 'https',\n    server: 'dns.alidns.com', // 会自动补上 `https://` 和 `/dns-query`\n    cacheSize: 1000,\n  },\n  safe360: {\n    server: 'https://doh.360.cn/dns-query',\n    cacheSize: 1000,\n    forSNI: true,\n  },\n\n  // tls\n  aliyunTLS: {\n    server: 'tls://223.5.5.5:853',\n    cacheSize: 1000,\n  },\n  aliyunTLS2: {\n    server: 'tls://223.6.6.6',\n    cacheSize: 1000,\n  },\n  safe360TLS: {\n    server: 'tls://dot.360.cn',\n    cacheSize: 1000,\n  },\n\n  // tcp\n  googleTCP: {\n    type: 'tcp',\n    server: '8.8.8.8',\n    port: 53,\n    cacheSize: 1000,\n  },\n  aliyunTCP: {\n    server: 'tcp://223.5.5.5',\n    cacheSize: 1000,\n  },\n\n  // udp\n  googleUDP: {\n    // type: 'udp', // 默认是udp可以不用标\n    server: '8.8.8.8',\n    cacheSize: 1000,\n  },\n  aliyunUDP: {\n    server: 'udp://223.5.5.5',\n    cacheSize: 1000,\n  },\n}, preSetIpList)\n\n\nconst hasPresetHostname = 'xxx.com'\nconst noPresetHostname = 'yyy.com'\n\nconst hostname1 = 'github.com'\nconst hostname2 = 'api.github.com'\nconst hostname3 = 'hk.docmirror.cn'\nconst hostname4 = 'github.docmirror.cn'\nconst hostname5 = 'gh.docmirror.top'\nconst hostname6 = 'gh2.docmirror.top'\n\nlet ip\n\n\nconsole.log('\\n--------------- test ForSNI ---------------\\n')\nconsole.log(`===> test ForSNI: ${dnsProviders.ForSNI.dnsName}`, '\\n\\n')\nassert.strictEqual(dnsProviders.ForSNI, dnsProviders.safe360)\n\nconst dnsProviders2 = dns.initDNS({\n  aliyun: {\n    server: 'udp://223.5.5.5',\n  },\n}, {})\nconsole.log(`===> test ForSNI2: ${dnsProviders2.ForSNI.dnsName}`, '\\n\\n')\nassert.strictEqual(dnsProviders2.ForSNI, dnsProviders2.PreSet) // 未配置forSNI的DNS时，默认使用PreSet作为ForSNI\n\n\nconsole.log('\\n--------------- test PreSet ---------------\\n')\nip = await dnsProviders.PreSet.lookup(hasPresetHostname)\nconsole.log(`===> test PreSet: ${hasPresetHostname} ->`, ip, '\\n\\n')\nconsole.log('\\n\\n')\nassert.strictEqual(ip, presetIp) // 预设过IP，等于预设的IP\n\nip = await dnsProviders.PreSet.lookup(noPresetHostname)\nconsole.log(`===> test PreSet: ${noPresetHostname} ->`, ip, '\\n\\n')\nconsole.log('\\n\\n')\nassert.strictEqual(ip, noPresetHostname) // 未预设IP，等于域名自己\n\n\nconsole.log('\\n--------------- test https ---------------\\n')\nip = await dnsProviders.aliyun.lookup(hasPresetHostname)\nassert.strictEqual(ip, presetIp) // test preset\nconsole.log('\\n\\n')\n\nassert.strictEqual(dnsProviders.aliyun.dnsType, 'HTTPS')\nip = await dnsProviders.aliyun.lookup(hostname1)\nconsole.log(`===> test aliyun: ${hostname1} ->`, ip, '\\n\\n')\n\nassert.strictEqual(dnsProviders.aliyun2.dnsType, 'HTTPS')\nip = await dnsProviders.aliyun2.lookup(hostname1)\nconsole.log(`===> test aliyun2: ${hostname1} ->`, ip, '\\n\\n')\n\nassert.strictEqual(dnsProviders.safe360.dnsType, 'HTTPS')\nip = await dnsProviders.safe360.lookup(hostname1)\nconsole.log(`===> test safe360: ${hostname1} ->`, ip, '\\n\\n')\n\n\nconsole.log('\\n--------------- test TLS ---------------\\n')\nip = await dnsProviders.aliyunTLS.lookup(hasPresetHostname)\nassert.strictEqual(ip, presetIp) // test preset\nconsole.log('\\n\\n')\n\nassert.strictEqual(dnsProviders.aliyunTLS.dnsType, 'TLS')\nip = await dnsProviders.aliyunTLS.lookup(hostname1)\nconsole.log(`===> test aliyunTLS: ${hostname1} ->`, ip, '\\n\\n')\n\nassert.strictEqual(dnsProviders.aliyunTLS2.dnsType, 'TLS')\nip = await dnsProviders.aliyunTLS2.lookup(hostname1)\nconsole.log(`===> test aliyunTLS2: ${hostname1} ->`, ip, '\\n\\n')\n\nassert.strictEqual(dnsProviders.safe360TLS.dnsType, 'TLS')\nip = await dnsProviders.safe360TLS.lookup(hostname1)\nconsole.log(`===> test safe360TLS: ${hostname1} ->`, ip, '\\n\\n')\n\n\nconsole.log('\\n--------------- test TCP ---------------\\n')\nip = await dnsProviders.googleTCP.lookup(hasPresetHostname)\nassert.strictEqual(ip, presetIp) // test preset\nconsole.log('\\n\\n')\n\nassert.strictEqual(dnsProviders.googleTCP.dnsType, 'TCP')\nip = await dnsProviders.googleTCP.lookup(hostname1)\nconsole.log(`===> test googleTCP: ${hostname1} ->`, ip, '\\n\\n')\n\nassert.strictEqual(dnsProviders.aliyunTCP.dnsType, 'TCP')\nip = await dnsProviders.aliyunTCP.lookup(hostname1)\nconsole.log(`===> test aliyunTCP: ${hostname1} ->`, ip, '\\n\\n')\n\n\nconsole.log('\\n--------------- test UDP ---------------\\n')\nip = await dnsProviders.googleUDP.lookup(hasPresetHostname)\nassert.strictEqual(ip, presetIp) // test preset\nconsole.log('\\n\\n')\n\nassert.strictEqual(dnsProviders.googleUDP.dnsType, 'UDP')\nip = await dnsProviders.googleUDP.lookup(hostname1)\nconsole.log(`===> test googleUDP: ${hostname1} ->`, ip, '\\n\\n')\n\nassert.strictEqual(dnsProviders.aliyunUDP.dnsType, 'UDP')\nip = await dnsProviders.aliyunUDP.lookup(hostname1)\nconsole.log(`===> test aliyunUDP: ${hostname1} ->`, ip, '\\n\\n')\n\ndnsProviders.aliyunUDP.lookup(hostname1).then(ip0 => {\n  console.log(`===> test aliyunUDP: ${hostname1} ->`, ip0, '\\n\\n')\n})\ndnsProviders.aliyunUDP.lookup(hostname2).then(ip0 => {\n  console.log(`===> test aliyunUDP: ${hostname2} ->`, ip0, '\\n\\n')\n})\ndnsProviders.aliyunUDP.lookup('baidu.com').then(ip0 => {\n  console.log('===> test aliyunUDP: baidu.com ->', ip0, '\\n\\n')\n})\ndnsProviders.aliyunUDP.lookup('gitee.com').then(ip0 => {\n  console.log('===> test aliyunUDP: gitee.com ->', ip0, '\\n\\n')\n})\n"
  },
  {
    "path": "packages/mitmproxy/test/lodashTest.js",
    "content": "const assert = require('node:assert')\nconst lodash = require('lodash')\n\n// test lodash.isEqual\nconst arr1 = [1, 2, 3]\nconst arr2 = [1, 2, 3]\nconst arr3 = [3, 2, 1]\nassert.strictEqual(lodash.isEqual(arr1, arr2), true)\nassert.strictEqual(lodash.isEqual(arr1.sort(), arr3.sort()), true)\n\n// test lodash.isEmpty\n\nfunction isEmpty (obj) {\n  return obj == null || (lodash.isObject(obj) && lodash.isEmpty(obj))\n}\n\n// true\nassert.strictEqual(isEmpty(null), true)\nassert.strictEqual(isEmpty({}), true)\nassert.strictEqual(isEmpty([]), true)\n// false\nassert.strictEqual(isEmpty(true), false)\nassert.strictEqual(isEmpty(false), false)\nassert.strictEqual(isEmpty(1), false)\nassert.strictEqual(isEmpty(0), false)\nassert.strictEqual(isEmpty(-1), false)\nassert.strictEqual(isEmpty(''), false)\nassert.strictEqual(isEmpty('1'), false)\n\n// test lodash.unionBy\nconst list = [\n  { host: 1, port: 1, dns: 2 },\n  { host: 1, port: 1, dns: 3 },\n  { host: 1, port: 2, dns: 3 },\n  { host: 1, port: 2, dns: 3 },\n]\nconsole.info(lodash.unionBy(list, 'host', 'port'))\n"
  },
  {
    "path": "packages/mitmproxy/test/matchTest.js",
    "content": "const assert = require('node:assert')\n\nconst name = '/docmirror/dev-sidecar/raw/master/doc/index.png'\n// https://raw.fastgit.org/docmirror/dev-sidecar/master/doc/index.png\nconst ret = name.replace(/^(.+)\\/raw\\/(.+)$/, 'raw.fastgit.org$1/$2')\nconsole.log(ret)\nassert.strictEqual(ret, 'raw.fastgit.org/docmirror/dev-sidecar/master/doc/index.png')\n\nconst reg = /^\\/[^/]+\\/[^/]+$/\nconsole.log('/greper/d2-crud-plus/blob/master/.eslintignore'.match(reg))\nassert.strictEqual('/greper/d2-crud-plus/blob/master/.eslintignore'.match(reg), null)\n\nconst chunk = Buffer.from('<head></head>')\nconst script = '<script>a</script>'\nconst index = chunk.indexOf('</head>')\nconst scriptBuf = Buffer.from(script)\nconst chunkNew = Buffer.alloc(chunk.length + scriptBuf.length)\nchunk.copy(chunkNew, 0, 0, index)\nscriptBuf.copy(chunkNew, index, 0)\nchunk.copy(chunkNew, index + scriptBuf.length, index)\nconsole.log(chunkNew.toString())\nassert.strictEqual(chunkNew.toString(), '<head><script>a</script></head>')\n\nconst reg2 = /aaaa/i\nconsole.log(reg2.test('aaaa')) // true\nassert.strictEqual(reg2.test('aaaa'), true)\n\nconst reg3 = '/aaaa/i'\nconsole.log(new RegExp(reg3).test('aaaa')) // false\nassert.strictEqual(new RegExp(reg3).test('aaaa'), false)\n"
  },
  {
    "path": "packages/mitmproxy/test/matchUtilTest.js",
    "content": "const assert = require('node:assert')\nconst matchUtil = require('../src/utils/util.match')\n\nconst hostMap = matchUtil.domainMapRegexply({\n  'aaa.com': true,\n  '*bbb.com': true,\n  '*.ccc.com': true,\n  '^.{1,3}ddd.com$': true,\n  '*.cn': true,\n  '.github.com': true,\n\n  '*.eee.com': true,\n  '.eee.com': false, // 此配置将被忽略，因为有 '*.eee.com' 了，优先级更高\n})\n\nconsole.log(hostMap)\nassert.strictEqual(hostMap['^.*bbb\\\\.com$'], true)\nassert.strictEqual(hostMap['^.*\\\\.ccc\\\\.com$'], true)\nassert.strictEqual(hostMap['^.{1,3}ddd.com$'], true)\nassert.strictEqual(hostMap['^.*\\\\.cn$'], true)\nassert.strictEqual(hostMap['^.*\\\\.github\\\\.com$'], true)\nassert.strictEqual(hostMap['^.*\\\\.github\\\\.com$'], true)\nassert.strictEqual(hostMap['^.*\\\\.eee\\\\.com$'], true)\n\nconst origin = hostMap.origin\nassert.strictEqual(origin['aaa.com'], true)\nassert.strictEqual(origin['*bbb.com'], true)\nassert.strictEqual(origin['*.ccc.com'], true)\nassert.strictEqual(origin['*.cn'], true)\nassert.strictEqual(origin['*.github.com'], true)\nassert.strictEqual(origin['.eee.com'], undefined)\n\nconst value11 = matchUtil.matchHostname(hostMap, 'aaa.com', 'test1.1')\nconst value12 = matchUtil.matchHostname(hostMap, 'aaaa.com', 'test1.2')\nconst value13 = matchUtil.matchHostname(hostMap, 'aaaa.comx', 'test1.3')\nconsole.log('test1: aaa.com')\nassert.strictEqual(value11, true)\nassert.strictEqual(value12, undefined)\nassert.strictEqual(value13, undefined)\n\nconst value21 = matchUtil.matchHostname(hostMap, 'bbb.com', 'test2.1')\nconst value22 = matchUtil.matchHostname(hostMap, 'xbbb.com', 'test2.2')\nconst value23 = matchUtil.matchHostname(hostMap, 'bbb.comx', 'test2.3')\nconst value24 = matchUtil.matchHostname(hostMap, 'x.bbb.com', 'test2.4')\nconsole.log('test2: *bbb.com')\nassert.strictEqual(value21, true)\nassert.strictEqual(value22, true)\nassert.strictEqual(value23, undefined)\nassert.strictEqual(value24, true)\n\nconst value31 = matchUtil.matchHostname(hostMap, 'ccc.com', 'test3.1')\nconst value32 = matchUtil.matchHostname(hostMap, 'x.ccc.com', 'test3.2')\nconst value33 = matchUtil.matchHostname(hostMap, 'xccc.com', 'test3.3')\nconsole.log('test3: *.ccc.com')\nassert.strictEqual(value31, true)\nassert.strictEqual(value32, true)\nassert.strictEqual(value33, undefined)\n\nconst value41 = matchUtil.matchHostname(hostMap, 'ddd.com', 'test4.1')\nconst value42 = matchUtil.matchHostname(hostMap, 'x.ddd.com', 'test4.2')\nconst value43 = matchUtil.matchHostname(hostMap, 'xddd.com', 'test4.3')\nconsole.log('test4: ^.{1,3}ddd.com$')\nassert.strictEqual(value41, undefined)\nassert.strictEqual(value42, true)\nassert.strictEqual(value43, true)\n\nconst value51 = matchUtil.matchHostname(hostMap, 'zzz.cn', 'test5.1')\nconst value52 = matchUtil.matchHostname(hostMap, 'x.zzz.cn', 'test5.2')\nconst value53 = matchUtil.matchHostname(hostMap, 'zzz.cnet.com', 'test5.3')\nconsole.log('test5: *.cn')\nassert.strictEqual(value51, true)\nassert.strictEqual(value52, true)\nassert.strictEqual(value53, undefined)\n\nconst value61 = matchUtil.matchHostname(hostMap, 'github.com', 'test6.1')\nconst value62 = matchUtil.matchHostname(hostMap, 'api.github.com', 'test6.2')\nconst value63 = matchUtil.matchHostname(hostMap, 'aa.bb.github.com', 'test6.3')\nconst value64 = matchUtil.matchHostname(hostMap, 'aaagithub.com', 'test6.4')\nconsole.log('test6: .github.com')\nassert.strictEqual(value61, true)\nassert.strictEqual(value62, true)\nassert.strictEqual(value63, true)\nassert.strictEqual(value64, undefined)\n"
  },
  {
    "path": "packages/mitmproxy/test/monkeyTest.js",
    "content": "const assert = require('node:assert')\nconst monkey = require('../src/lib/monkey')\n\nlet scripts\ntry {\n  scripts = monkey.load('../gui/extra/scripts/') // 相对于 mitmproxy 目录的相对路径，而不是当前 test 目录的。\n} catch {\n  scripts = monkey.load('../../gui/extra/scripts/') // 相对于 当前 test 目录的相对路径\n}\n\n// console.log(scripts)\nassert.strictEqual(scripts.github != null, true)\nassert.strictEqual(scripts.google != null, true)\nassert.strictEqual(scripts.tampermonkey != null, true)\n"
  },
  {
    "path": "packages/mitmproxy/test/pacTest.js",
    "content": "const assert = require('node:assert')\nconst pac = require('../src/lib/proxy/middleware/source/pac')\n\nconst pacClient = pac.createPacClient('../gui/extra/pac/pac.txt') // 相对于 mitmproxy 目录的相对路径，而不是当前 test 目录的。\n\nconst string = pacClient.FindProxyForURL('https://www.facebook.com', 'www.facebook.com')\nconsole.log(`facebook: ${string}`)\nassert.strictEqual(string, pacClient.proxyUrl)\n\nconst string2 = pacClient.FindProxyForURL('https://http2.golang.org', 'http2.golang.org')\nconsole.log(`golang: ${string2}`)\nassert.strictEqual(string2, 'DIRECT;')\n"
  },
  {
    "path": "packages/mitmproxy/test/proxyTest.js",
    "content": "// const http = require('node:http')\n//\n// const options = {\n//   headers: {\n//     'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/69.0.3497.100 Safari/537.36',\n//   },\n//   lookup (hostname, options, callback) {\n//     const ip = '106.52.191.148'\n//     console.log('lookup')\n//     callback(null, ip, 4)\n//   },\n// }\n//\n// const request = http.get('http://test.target/', options, (response) => {\n//   response.on('data', (data) => {\n//     process.stdout.write(data)\n//   })\n// })\n//\n// request.on('error', (error) => {\n//   console.log(error)\n// })\n"
  },
  {
    "path": "packages/mitmproxy/test/responseReplaceTest.js",
    "content": "const assert = require('node:assert')\nconst responseReplace = require('../src/lib/interceptor/impl/res/responseReplace')\n\nconst headers = {}\nconst res = {\n  setHeader: (key, value) => {\n    headers[key] = value\n  },\n}\n\nconst proxyRes = {\n  rawHeaders: [\n    'Content-Type', 'application/json; charset=utf-8',\n    'Content-Length', '2',\n    'ETag', 'W/\"2\"',\n    'Date', 'Thu, 01 Jan 1970 00:00:00 GMT',\n    'Connection', 'keep-alive',\n  ],\n}\n\nconst newHeaders = {\n  'Content-Type': 'application/json; charset=utf-8',\n  'Content-Length': '3',\n  'xxx': 1,\n  'Date': '[remove]',\n  'yyy': '[remove]',\n}\n\nconst result = responseReplace.replaceResponseHeaders(newHeaders, res, proxyRes)\nconsole.log(proxyRes.rawHeaders)\nconsole.log(headers)\nconsole.log(result)\n\nassert.deepStrictEqual(proxyRes.rawHeaders, [\n  'Content-Type', 'application/json; charset=utf-8',\n  'Content-Length', '3',\n  'ETag', 'W/\"2\"',\n  'Date', '',\n  'Connection', 'keep-alive'\n])\nassert.deepStrictEqual(headers, {\n  xxx: 1,\n})\nassert.deepStrictEqual(result, {\n  'content-length': '2',\n  'date': 'Thu, 01 Jan 1970 00:00:00 GMT',\n  'xxx': null,\n})\n"
  },
  {
    "path": "packages/mitmproxy/test/sha256Test.js",
    "content": "// 需要时，在 package.json 中添加以下依赖:\n// \"devDependencies\": {\n//     \"crypto-js\": \"^4.2.0\"\n// }\n\n// const CryptoJs = require('crypto-js')\n//\n// const ret = CryptoJs.SHA256('111111111111')\n// console.log(ret.toString(CryptoJs.enc.Base64))\n// console.log(1 / 2)\n"
  },
  {
    "path": "packages/mitmproxy/test/utilTest.js",
    "content": "const assert = require('node:assert')\nconst util = require('../src/lib/proxy/common/util')\n\nlet arr\n\narr = util.parseHostnameAndPort('www.baidu.com')\nconsole.log('arr1:', arr)\nassert.strictEqual(arr.length === 1, true) // true\nassert.strictEqual(arr[0] === 'www.baidu.com', true) // true\n\narr = util.parseHostnameAndPort('www.baidu.com', 80)\nconsole.log('arr2:', arr)\nassert.strictEqual(arr.length === 2, true) // true\nassert.strictEqual(arr[0] === 'www.baidu.com', true) // true\nassert.strictEqual(arr[1] === 80, true) // true\n\narr = util.parseHostnameAndPort('www.baidu.com:8080')\nconsole.log('arr3:', arr)\nassert.strictEqual(arr.length === 2, true) // true\nassert.strictEqual(arr[0] === 'www.baidu.com', true) // true\nassert.strictEqual(arr[1] === 8080, true) // true\n\narr = util.parseHostnameAndPort('www.baidu.com:8080', 8080)\nconsole.log('arr4:', arr)\nassert.strictEqual(arr.length === 2, true) // true\nassert.strictEqual(arr[0] === 'www.baidu.com', true) // true\nassert.strictEqual(arr[1] === 8080, true) // true\n\narr = util.parseHostnameAndPort('[2001:abcd::1]')\nconsole.log('arr5:', arr)\nassert.strictEqual(arr.length === 1, true) // true\nassert.strictEqual(arr[0] === '[2001:abcd::1]', true) // ture\n\narr = util.parseHostnameAndPort('[2001:abcd::1]', 80)\nconsole.log('arr6:', arr)\nassert.strictEqual(arr.length === 2, true) // true\nassert.strictEqual(arr[0] === '[2001:abcd::1]', true) // ture\nassert.strictEqual(arr[1] === 80, true) // ture\n\narr = util.parseHostnameAndPort('[2001:abcd::1]:8080')\nconsole.log('arr7:', arr)\nassert.strictEqual(arr.length === 2, true) // true\nassert.strictEqual(arr[0] === '[2001:abcd::1]', true) // true\nassert.strictEqual(arr[1] === 8080, true) // ture\n\narr = util.parseHostnameAndPort('[2001:abcd::1]:8080', 8080)\nconsole.log('arr8:', arr)\nassert.strictEqual(arr.length === 2, true) // true\nassert.strictEqual(arr[0] === '[2001:abcd::1]', true) // true\nassert.strictEqual(arr[1] === 8080, true) // ture\n"
  },
  {
    "path": "pnpm-workspace.yaml",
    "content": "packages:\n  # all packages in subdirectories of packages/ and components/\n  - packages/*\n  # exclude packages that are inside test directories\n  - '!**/test/**'\n"
  },
  {
    "path": "test/test.js",
    "content": "// const cmd1 = require('node-cmd')\n// cmd1.get('set',\n//   function (err, data, stderr) {\n//     console.log('cmd complete:', err, data, stderr)\n//     if (err) {\n//       console.error('cmd 命令执行错误：', err, stderr)\n//     } else {\n//       console.log('cmd 命令执行结果：', data)\n//     }\n//   }\n// )\n\n// const process = require('child_process')\n//\n// const cmd = 'set'\n// process.exec(cmd, function (error, stdout, stderr) {\n//   console.log('error:' + error)\n//   console.log('stdout:' + stdout)\n//   console.log('stderr:' + stderr)\n// })\n\n// const fs = require('fs')\n// const content = fs.readFileSync('C:\\\\Users\\\\Administrator\\\\.dev-sidecar\\\\dev-sidecar.ca.crt')\n// console.log('content:',JSON.stringify(content.toString().replace(new RegExp('\\r\\n','g'),'\\n')))\n\n// function testCa () {\n//     const https = require('https')\n//     const fs = require('fs')\n//     process.env.NODE_EXTRA_CA_CERTS = 'C:\\\\Users\\\\Administrator\\\\.dev-sidecar\\\\dev-sidecar.ca.crt'\n//     process.env.GLOBAL_AGENT_HTTP_PROXY = \"http://127.0.0.1:31181\"\n//     process.env.GLOBAL_AGENT_HTTPS_PROXY = \"http://127.0.0.1:31181\"\n//     fs.readFileSync(process.env.NODE_EXTRA_CA_CERTS)\n//\n//     const options = {\n//         agent : new https.Agent({\n//             proxy: \"http://127.0.0.1:31181\"\n//         })\n//     }\n//     console.log('options', options)\n//\n//     https.get('https://ajax.googleapis.com/ajax/libs/jquery/1.12.4/jquery.min.js',options, (res) => {\n//         console.log('状态码:', res.statusCode)\n//         console.log('请求头:', res.headers)\n//\n//         res.on('data', (d) => {\n//             process.stdout.write(d)\n//         })\n//     }).on('error', (e) => {\n//         console.error(e)\n//     })\n// }\n\nfunction testRequest () {\n  // process.env.NODE_EXTRA_CA_CERTS='C:\\\\Users\\\\Administrator\\\\.dev-sidecar\\\\dev-sidecar.ca.crt'\n  console.log(process.env.NODE_EXTRA_CA_CERTS)\n  const request = require('request').defaults({\n    proxy: 'http://127.0.0.1:31181',\n  })\n  request('https://ajax.googleapis.com/ajax/libs/jquery/1.12.4/jquery.min.js', (error, response, body) => {\n    if (error) {\n      console.error(error)\n    } else {\n      console.log(body)\n    }\n  })\n}\n\n// testCa()\n\ntestRequest()\n"
  },
  {
    "path": "test/testDns.js",
    "content": "console.log('www.baidu.com'.match('.*.baidu.com'))\n"
  }
]