diff --git a/.github/workflows/build-gui.yml b/.github/workflows/build-gui.yml
new file mode 100644
index 0000000..835be68
--- /dev/null
+++ b/.github/workflows/build-gui.yml
@@ -0,0 +1,352 @@
+name: Build GUI Packages
+
+on:
+ push:
+ tags:
+ - 'v*'
+ workflow_dispatch:
+ inputs:
+ version:
+ description: 'Version number (e.g., 1.0.0)'
+ required: false
+ default: '1.0.0'
+
+jobs:
+ build-linux-x64:
+ runs-on: ubuntu-latest
+ steps:
+ - uses: actions/checkout@v4
+ - uses: actions/setup-python@v5
+ with:
+ python-version: '3.11'
+ - name: Install dependencies
+ run: |
+ sudo apt-get update
+ sudo apt-get install -y libfuse2 rpm
+ pip install flet pyinstaller pillow
+ pip install -r requirements-gui.txt
+ - name: Build with Flet Pack
+ run: |
+ flet pack cloudflare_speedtest_gui.py --name yx-tools-gui --icon icon/icon.png --add-data "cloudflare_speedtest.py:." --add-data "icon:icon"
+ - name: Create desktop file
+ run: |
+ cat > yx-tools-gui.desktop << 'DESKTOP_EOF'
+ [Desktop Entry]
+ Name=yx-tools-gui
+ Comment=Cloudflare IP Speed Test Tool
+ Exec=yx-tools-gui
+ Icon=yx-tools-gui
+ Terminal=false
+ Type=Application
+ Categories=Network;
+ DESKTOP_EOF
+ sed -i 's/^ //' yx-tools-gui.desktop
+ - name: Create packages
+ run: |
+ VERSION=${{ github.event.inputs.version || '1.0.0' }}
+
+ # AppImage
+ wget -q https://github.com/AppImage/AppImageKit/releases/download/continuous/appimagetool-x86_64.AppImage -O appimagetool
+ chmod +x appimagetool
+ mkdir -p AppDir/usr/{bin,share/{applications,icons/hicolor/256x256/apps}}
+ cp dist/yx-tools-gui AppDir/usr/bin/
+ cp icon/icon.png AppDir/yx-tools-gui.png
+ cp icon/icon.png AppDir/usr/share/icons/hicolor/256x256/apps/yx-tools-gui.png
+ cp yx-tools-gui.desktop AppDir/
+ cp yx-tools-gui.desktop AppDir/usr/share/applications/
+ echo '#!/bin/bash' > AppDir/AppRun
+ echo 'HERE=$(dirname "$(readlink -f "$0")")' >> AppDir/AppRun
+ echo 'exec "${HERE}/usr/bin/yx-tools-gui" "$@"' >> AppDir/AppRun
+ chmod +x AppDir/AppRun
+ ARCH=x86_64 ./appimagetool AppDir yx-tools-gui-x86_64.AppImage
+
+ # DEB
+ mkdir -p deb-pkg/{DEBIAN,opt/yx-tools-gui,usr/{bin,share/{applications,icons/hicolor/256x256/apps}}}
+ cp dist/yx-tools-gui deb-pkg/opt/yx-tools-gui/
+ cp icon/icon.png deb-pkg/usr/share/icons/hicolor/256x256/apps/yx-tools-gui.png
+ echo '#!/bin/bash' > deb-pkg/usr/bin/yx-tools-gui
+ echo 'exec /opt/yx-tools-gui/yx-tools-gui "$@"' >> deb-pkg/usr/bin/yx-tools-gui
+ chmod +x deb-pkg/usr/bin/yx-tools-gui
+ sed 's|Exec=yx-tools-gui|Exec=/usr/bin/yx-tools-gui|' yx-tools-gui.desktop > deb-pkg/usr/share/applications/yx-tools-gui.desktop
+ echo "Package: yx-tools-gui" > deb-pkg/DEBIAN/control
+ echo "Version: ${VERSION}" >> deb-pkg/DEBIAN/control
+ echo "Architecture: amd64" >> deb-pkg/DEBIAN/control
+ echo "Maintainer: Joey and Zag" >> deb-pkg/DEBIAN/control
+ echo "Description: Cloudflare IP Speed Test Tool" >> deb-pkg/DEBIAN/control
+ dpkg-deb --build deb-pkg yx-tools-gui_${VERSION}_amd64.deb
+
+ # RPM
+ mkdir -p rpmbuild/{BUILD,RPMS,SPECS,SOURCES}
+ cp dist/yx-tools-gui rpmbuild/SOURCES/
+ cp icon/icon.png rpmbuild/SOURCES/
+ sed 's|Exec=yx-tools-gui|Exec=/usr/bin/yx-tools-gui|' yx-tools-gui.desktop > rpmbuild/SOURCES/yx-tools-gui.desktop
+ cat > rpmbuild/SPECS/yx-tools-gui.spec << SPEC_EOF
+ Name: yx-tools-gui
+ Version: ${VERSION}
+ Release: 1
+ Summary: Cloudflare IP Speed Test Tool
+ License: MIT
+ %description
+ Cloudflare IP Speed Test Tool
+ %install
+ mkdir -p %{buildroot}/opt/yx-tools-gui %{buildroot}/usr/bin %{buildroot}/usr/share/applications %{buildroot}/usr/share/icons/hicolor/256x256/apps
+ cp %{_sourcedir}/yx-tools-gui %{buildroot}/opt/yx-tools-gui/
+ chmod +x %{buildroot}/opt/yx-tools-gui/yx-tools-gui
+ cp %{_sourcedir}/icon.png %{buildroot}/usr/share/icons/hicolor/256x256/apps/yx-tools-gui.png
+ echo '#!/bin/bash' > %{buildroot}/usr/bin/yx-tools-gui
+ echo 'exec /opt/yx-tools-gui/yx-tools-gui "\$@"' >> %{buildroot}/usr/bin/yx-tools-gui
+ chmod +x %{buildroot}/usr/bin/yx-tools-gui
+ cp %{_sourcedir}/yx-tools-gui.desktop %{buildroot}/usr/share/applications/
+ %files
+ /opt/yx-tools-gui/yx-tools-gui
+ /usr/bin/yx-tools-gui
+ /usr/share/applications/yx-tools-gui.desktop
+ /usr/share/icons/hicolor/256x256/apps/yx-tools-gui.png
+ SPEC_EOF
+ sed -i 's/^ //' rpmbuild/SPECS/yx-tools-gui.spec
+ rpmbuild --define "_topdir ${GITHUB_WORKSPACE}/rpmbuild" -bb rpmbuild/SPECS/yx-tools-gui.spec
+ cp rpmbuild/RPMS/x86_64/*.rpm yx-tools-gui-${VERSION}.x86_64.rpm
+ - uses: actions/upload-artifact@v4
+ with:
+ name: linux-x64-packages
+ path: |
+ yx-tools-gui-*.x86_64.rpm
+ yx-tools-gui_*_amd64.deb
+ yx-tools-gui-x86_64.AppImage
+ compression-level: 0
+
+ build-linux-arm64:
+ runs-on: ubuntu-24.04-arm
+ steps:
+ - uses: actions/checkout@v4
+ - uses: actions/setup-python@v5
+ with:
+ python-version: '3.11'
+ - name: Install dependencies
+ run: |
+ sudo apt-get update
+ sudo apt-get install -y libfuse2 rpm
+ pip install flet pyinstaller pillow
+ pip install -r requirements-gui.txt
+ - name: Build with Flet Pack
+ run: |
+ flet pack cloudflare_speedtest_gui.py --name yx-tools-gui --icon icon/icon.png --add-data "cloudflare_speedtest.py:." --add-data "icon:icon"
+ - name: Create desktop file
+ run: |
+ cat > yx-tools-gui.desktop << 'DESKTOP_EOF'
+ [Desktop Entry]
+ Name=yx-tools-gui
+ Comment=Cloudflare IP Speed Test Tool
+ Exec=yx-tools-gui
+ Icon=yx-tools-gui
+ Terminal=false
+ Type=Application
+ Categories=Network;
+ DESKTOP_EOF
+ sed -i 's/^ //' yx-tools-gui.desktop
+ - name: Create packages
+ run: |
+ VERSION=${{ github.event.inputs.version || '1.0.0' }}
+
+ # DEB
+ mkdir -p deb-pkg/{DEBIAN,opt/yx-tools-gui,usr/{bin,share/{applications,icons/hicolor/256x256/apps}}}
+ cp dist/yx-tools-gui deb-pkg/opt/yx-tools-gui/
+ cp icon/icon.png deb-pkg/usr/share/icons/hicolor/256x256/apps/yx-tools-gui.png
+ echo '#!/bin/bash' > deb-pkg/usr/bin/yx-tools-gui
+ echo 'exec /opt/yx-tools-gui/yx-tools-gui "$@"' >> deb-pkg/usr/bin/yx-tools-gui
+ chmod +x deb-pkg/usr/bin/yx-tools-gui
+ sed 's|Exec=yx-tools-gui|Exec=/usr/bin/yx-tools-gui|' yx-tools-gui.desktop > deb-pkg/usr/share/applications/yx-tools-gui.desktop
+ echo "Package: yx-tools-gui" > deb-pkg/DEBIAN/control
+ echo "Version: ${VERSION}" >> deb-pkg/DEBIAN/control
+ echo "Architecture: arm64" >> deb-pkg/DEBIAN/control
+ echo "Maintainer: Joey and Zag" >> deb-pkg/DEBIAN/control
+ echo "Description: Cloudflare IP Speed Test Tool" >> deb-pkg/DEBIAN/control
+ dpkg-deb --build deb-pkg yx-tools-gui_${VERSION}_arm64.deb
+
+ # RPM
+ mkdir -p rpmbuild/{BUILD,RPMS,SPECS,SOURCES}
+ cp dist/yx-tools-gui rpmbuild/SOURCES/
+ cp icon/icon.png rpmbuild/SOURCES/
+ sed 's|Exec=yx-tools-gui|Exec=/usr/bin/yx-tools-gui|' yx-tools-gui.desktop > rpmbuild/SOURCES/yx-tools-gui.desktop
+ cat > rpmbuild/SPECS/yx-tools-gui.spec << SPEC_EOF
+ Name: yx-tools-gui
+ Version: ${VERSION}
+ Release: 1
+ Summary: Cloudflare IP Speed Test Tool
+ License: MIT
+ %description
+ Cloudflare IP Speed Test Tool
+ %install
+ mkdir -p %{buildroot}/opt/yx-tools-gui %{buildroot}/usr/bin %{buildroot}/usr/share/applications %{buildroot}/usr/share/icons/hicolor/256x256/apps
+ cp %{_sourcedir}/yx-tools-gui %{buildroot}/opt/yx-tools-gui/
+ chmod +x %{buildroot}/opt/yx-tools-gui/yx-tools-gui
+ cp %{_sourcedir}/icon.png %{buildroot}/usr/share/icons/hicolor/256x256/apps/yx-tools-gui.png
+ echo '#!/bin/bash' > %{buildroot}/usr/bin/yx-tools-gui
+ echo 'exec /opt/yx-tools-gui/yx-tools-gui "\$@"' >> %{buildroot}/usr/bin/yx-tools-gui
+ chmod +x %{buildroot}/usr/bin/yx-tools-gui
+ cp %{_sourcedir}/yx-tools-gui.desktop %{buildroot}/usr/share/applications/
+ %files
+ /opt/yx-tools-gui/yx-tools-gui
+ /usr/bin/yx-tools-gui
+ /usr/share/applications/yx-tools-gui.desktop
+ /usr/share/icons/hicolor/256x256/apps/yx-tools-gui.png
+ SPEC_EOF
+ sed -i 's/^ //' rpmbuild/SPECS/yx-tools-gui.spec
+ rpmbuild --define "_topdir ${GITHUB_WORKSPACE}/rpmbuild" -bb rpmbuild/SPECS/yx-tools-gui.spec
+ cp rpmbuild/RPMS/aarch64/*.rpm yx-tools-gui-${VERSION}.aarch64.rpm
+ - uses: actions/upload-artifact@v4
+ with:
+ name: linux-arm64-packages
+ path: |
+ yx-tools-gui-*.aarch64.rpm
+ yx-tools-gui_*_arm64.deb
+ compression-level: 0
+
+ build-windows-x64:
+ runs-on: windows-latest
+ steps:
+ - uses: actions/checkout@v4
+ - uses: actions/setup-python@v5
+ with:
+ python-version: '3.11'
+ - name: Install dependencies
+ run: |
+ pip install flet pyinstaller pillow
+ pip install -r requirements-gui.txt
+ - name: Build with Flet Pack
+ run: |
+ flet pack cloudflare_speedtest_gui.py --name yx-tools-gui --icon icon/icon.png --add-data "cloudflare_speedtest.py;." --add-data "icon;icon"
+ - name: Package
+ run: |
+ Copy-Item "dist/yx-tools-gui.exe" -Destination "yx-tools-gui-x64.exe"
+ shell: pwsh
+ - uses: actions/upload-artifact@v4
+ with:
+ name: exe-x64
+ path: yx-tools-gui-x64.exe
+ compression-level: 0
+
+ build-windows-arm64:
+ runs-on: windows-11-arm
+ steps:
+ - uses: actions/checkout@v4
+ - uses: actions/setup-python@v5
+ with:
+ python-version: '3.11'
+ - name: Install dependencies
+ run: |
+ pip install flet pyinstaller pillow
+ pip install -r requirements-gui.txt
+ - name: Build with Flet Pack
+ run: |
+ flet pack cloudflare_speedtest_gui.py --name yx-tools-gui --icon icon/icon.png --add-data "cloudflare_speedtest.py;." --add-data "icon;icon"
+ - name: Package
+ run: |
+ Copy-Item "dist/yx-tools-gui.exe" -Destination "yx-tools-gui-arm64.exe"
+ shell: pwsh
+ - uses: actions/upload-artifact@v4
+ with:
+ name: exe-arm64
+ path: yx-tools-gui-arm64.exe
+ compression-level: 0
+
+ build-macos-intel:
+ runs-on: macos-13
+ steps:
+ - uses: actions/checkout@v4
+ - uses: actions/setup-python@v5
+ with:
+ python-version: '3.11'
+ - name: Install Flutter
+ uses: subosito/flutter-action@v2
+ with:
+ flutter-version: '3.24.0'
+ channel: 'stable'
+ - name: Install dependencies
+ run: |
+ pip install flet
+ pip install -r requirements-gui.txt
+ - name: Prepare assets
+ run: |
+ mkdir -p assets
+ cp icon/icon.png assets/icon.png
+ - name: Build with Flet
+ run: flet build macos --project "yx-tools-gui" --product "yx-tools-gui" --org "com.yxtools" --build-version "${{ github.event.inputs.version || '1.0.0' }}" -v
+ - name: Create DMG
+ run: |
+ VERSION=${{ github.event.inputs.version || '1.0.0' }}
+ APP_PATH=$(find build -name "*.app" -type d | head -1)
+ if [ -z "$APP_PATH" ]; then
+ echo "Error: App bundle not found"
+ find build -type d
+ exit 1
+ fi
+ echo "Found app: $APP_PATH"
+ brew install create-dmg
+ create-dmg --volname "yx-tools-gui" --window-size 600 400 --icon-size 100 --icon "$(basename $APP_PATH)" 150 185 --app-drop-link 450 185 "yx-tools-gui-${VERSION}-intel.dmg" "$APP_PATH" || hdiutil create -volname "yx-tools-gui" -srcfolder "$APP_PATH" -ov -format UDZO "yx-tools-gui-${VERSION}-intel.dmg"
+ - uses: actions/upload-artifact@v4
+ with:
+ name: dmg-intel
+ path: yx-tools-gui-*-intel.dmg
+ compression-level: 0
+
+ build-macos-arm:
+ runs-on: macos-latest
+ steps:
+ - uses: actions/checkout@v4
+ - uses: actions/setup-python@v5
+ with:
+ python-version: '3.11'
+ - name: Install Flutter
+ uses: subosito/flutter-action@v2
+ with:
+ flutter-version: '3.24.0'
+ channel: 'stable'
+ - name: Install dependencies
+ run: |
+ pip install flet
+ pip install -r requirements-gui.txt
+ - name: Prepare assets
+ run: |
+ mkdir -p assets
+ cp icon/icon.png assets/icon.png
+ - name: Build with Flet
+ run: flet build macos --project "yx-tools-gui" --product "yx-tools-gui" --org "com.yxtools" --build-version "${{ github.event.inputs.version || '1.0.0' }}" -v
+ - name: Create DMG
+ run: |
+ VERSION=${{ github.event.inputs.version || '1.0.0' }}
+ APP_PATH=$(find build -name "*.app" -type d | head -1)
+ if [ -z "$APP_PATH" ]; then
+ echo "Error: App bundle not found"
+ find build -type d
+ exit 1
+ fi
+ echo "Found app: $APP_PATH"
+ brew install create-dmg
+ create-dmg --volname "yx-tools-gui" --window-size 600 400 --icon-size 100 --icon "$(basename $APP_PATH)" 150 185 --app-drop-link 450 185 "yx-tools-gui-${VERSION}-apple-silicon.dmg" "$APP_PATH" || hdiutil create -volname "yx-tools-gui" -srcfolder "$APP_PATH" -ov -format UDZO "yx-tools-gui-${VERSION}-apple-silicon.dmg"
+ - uses: actions/upload-artifact@v4
+ with:
+ name: dmg-apple-silicon
+ path: yx-tools-gui-*-apple-silicon.dmg
+ compression-level: 0
+
+ release:
+ needs: [build-linux-x64, build-linux-arm64, build-windows-x64, build-windows-arm64, build-macos-intel, build-macos-arm]
+ runs-on: ubuntu-latest
+ if: startsWith(github.ref, 'refs/tags/')
+ steps:
+ - uses: actions/download-artifact@v4
+ with:
+ path: artifacts
+ - name: Prepare release files
+ run: |
+ mkdir -p release
+ find artifacts -type f \( -name "*.dmg" -o -name "*.exe" -o -name "*.deb" -o -name "*.rpm" -o -name "*.AppImage" \) -exec cp {} release/ \;
+ ls -la release/
+ - uses: softprops/action-gh-release@v1
+ with:
+ files: release/*
+ draft: false
+ generate_release_notes: true
+ env:
+ GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
diff --git a/README.md b/README.md
index 507a5a1..fb979eb 100644
--- a/README.md
+++ b/README.md
@@ -52,6 +52,13 @@
- **多架构支持** - 支持amd64和arm64架构
- **环境隔离** - 容器化运行,环境干净整洁
+### 图形化界面
+- **现代化图形界面** - 操作更加直观友好
+- **主题切换** - 可根据个人喜好选择主题
+- **多架构支持** - 支持amd64和arm64架构
+
+
+
## 支持平台
| 平台 | 架构 | 状态 |
@@ -92,6 +99,9 @@ pip install -r requirements.txt
# 运行程序(命令行模式)
python3 cloudflare_speedtest.py --mode beginner --count 10 --speed 1 --delay 1000
+# 运行程序(图形模式)
+python3 cloudflare_speedtest_gui.py
+
# 查看帮助
python3 cloudflare_speedtest.py --help
```
@@ -105,6 +115,8 @@ python3 cloudflare_speedtest.py --help
- `CloudflareSpeedTest-macos-arm64` - macOS Apple Silicon
- `CloudflareSpeedTest-linux-amd64` - Linux x64
- `CloudflareSpeedTest-linux-arm64` - Linux ARM64
+- `yx-tools-gui-xxx.xxx` - 各平台的图形化版本
+
### 方法四:使用Docker(推荐容器化部署)
diff --git a/assets/icon.png b/assets/icon.png
new file mode 100644
index 0000000..50e67a4
Binary files /dev/null and b/assets/icon.png differ
diff --git a/cloudflare_speedtest.py b/cloudflare_speedtest.py
index e85a09c..624eb0c 100644
--- a/cloudflare_speedtest.py
+++ b/cloudflare_speedtest.py
@@ -18,6 +18,35 @@
from datetime import datetime
+def get_app_data_dir():
+ """
+ 获取应用数据目录(用于存放下载的可执行文件等)
+ 在打包后的应用中,当前目录可能是只读的,需要使用用户数据目录
+ """
+ app_name = "yx-tools"
+
+ if sys.platform == "darwin":
+ # macOS: ~/Library/Application Support/yx-tools
+ base = os.path.expanduser("~/Library/Application Support")
+ elif sys.platform == "win32":
+ # Windows: %APPDATA%/yx-tools
+ base = os.environ.get("APPDATA", os.path.expanduser("~"))
+ else:
+ # Linux: ~/.local/share/yx-tools
+ base = os.environ.get("XDG_DATA_HOME", os.path.expanduser("~/.local/share"))
+
+ app_dir = os.path.join(base, app_name)
+
+ # 确保目录存在
+ try:
+ os.makedirs(app_dir, exist_ok=True)
+ except Exception:
+ # 如果创建失败,回退到当前目录
+ app_dir = os.getcwd()
+
+ return app_dir
+
+
# 使用curl的备用HTTP请求函数(解决SSL模块不可用的问题)
def curl_request(url, method='GET', data=None, headers=None, timeout=30):
"""
@@ -355,6 +384,14 @@ def json(self):
# 保存交互模式下生成的命令(用于定时任务)
LAST_GENERATED_COMMAND = None
+# Cloudflare 支持的 HTTPS 端口列表
+# 参考: https://developers.cloudflare.com/fundamentals/reference/network-ports/
+CLOUDFLARE_HTTPS_PORTS = [443, 8443, 2053, 2083, 2087, 2096]
+
+# 测速 URL 配置
+# 默认 URL(可能不支持多端口下载测速)
+DEFAULT_SPEEDTEST_URL = "https://cf.xiu2.xyz/url"
+
def generate_ipv6_file():
"""生成 IPv6 地址列表文件"""
@@ -543,18 +580,41 @@ def download_file(url, filename):
def download_cloudflare_speedtest(os_type, arch_type):
"""下载 CloudflareSpeedTest 可执行文件(优先使用反代版本)"""
- # 优先检查反代版本
+ # 获取应用数据目录(用于存放下载的可执行文件)
+ app_data_dir = get_app_data_dir()
+
+ # 构建可执行文件名
if os_type == "win":
- proxy_exec_name = f"CloudflareST_proxy_{os_type}_{arch_type}.exe"
+ exec_basename = f"CloudflareST_proxy_{os_type}_{arch_type}.exe"
else:
- proxy_exec_name = f"CloudflareST_proxy_{os_type}_{arch_type}"
+ exec_basename = f"CloudflareST_proxy_{os_type}_{arch_type}"
+
+ # 完整路径
+ proxy_exec_path = os.path.join(app_data_dir, exec_basename)
+
+ # 优先检查应用数据目录中的反代版本
+ if os.path.exists(proxy_exec_path):
+ # 确保有执行权限
+ if os_type != "win":
+ try:
+ os.chmod(proxy_exec_path, 0o755)
+ except Exception:
+ pass
+ print(f"✓ 使用反代版本: {proxy_exec_path}")
+ return proxy_exec_path
- if os.path.exists(proxy_exec_name):
- print(f"✓ 使用反代版本: {proxy_exec_name}")
- return proxy_exec_name
+ # 也检查当前目录(兼容旧版本)
+ if os.path.exists(exec_basename):
+ if os_type != "win":
+ try:
+ os.chmod(exec_basename, 0o755)
+ except Exception:
+ pass
+ print(f"✓ 使用反代版本: {exec_basename}")
+ return os.path.abspath(exec_basename)
# 检查是否已下载反代版本
- print("反代版本不存在,开始下载反代版本...")
+ print(f"反代版本不存在,开始下载反代版本到 {app_data_dir}...")
# 构建下载URL - 使用您的GitHub仓库
if os_type == "win":
@@ -577,51 +637,49 @@ def download_cloudflare_speedtest(os_type, arch_type):
download_url = f"https://github.com/byJoey/CloudflareSpeedTest/releases/download/v1.0/{archive_name}"
- if not download_file(download_url, archive_name):
+ # 下载到应用数据目录
+ archive_path = os.path.join(app_data_dir, archive_name)
+
+ if not download_file(download_url, archive_path):
# 备用方案: 尝试 HTTP 下载
http_url = download_url.replace("https://", "http://")
- if not download_file(http_url, archive_name):
+ if not download_file(http_url, archive_path):
# 所有自动下载都失败,提供手动下载说明
print("\n" + "="*60)
print("自动下载失败,请手动下载反代版本:")
print(f"下载地址: {download_url}")
- print(f"解压后文件名应为: CloudflareST_proxy_{os_type}_{arch_type}{'.exe' if os_type == 'win' else ''}")
+ print(f"解压后放到: {app_data_dir}")
+ print(f"文件名应为: {exec_basename}")
print("="*60)
# 检查是否有手动下载的反代版本文件
- if os_type == "win":
- proxy_exec_name = f"CloudflareST_proxy_{os_type}_{arch_type}.exe"
- else:
- proxy_exec_name = f"CloudflareST_proxy_{os_type}_{arch_type}"
-
- if os.path.exists(proxy_exec_name):
- print(f"找到手动下载的反代版本: {proxy_exec_name}")
- # 手动下载的文件也需要赋予执行权限
+ if os.path.exists(proxy_exec_path):
+ print(f"找到手动下载的反代版本: {proxy_exec_path}")
if os_type != "win":
- os.chmod(proxy_exec_name, 0o755)
- print(f"已赋予执行权限: {proxy_exec_name}")
- return proxy_exec_name
+ os.chmod(proxy_exec_path, 0o755)
+ print(f"已赋予执行权限: {proxy_exec_path}")
+ return proxy_exec_path
else:
print("未找到反代版本文件,程序无法继续")
if sys.platform == "win32":
input("按 Enter 键退出...")
sys.exit(1)
else:
- # 解压文件
- print(f"正在解压: {archive_name}")
+ # 解压文件到应用数据目录
+ print(f"正在解压: {archive_path}")
try:
if archive_name.endswith('.zip'):
import zipfile
- with zipfile.ZipFile(archive_name, 'r') as zip_ref:
- zip_ref.extractall('.')
+ with zipfile.ZipFile(archive_path, 'r') as zip_ref:
+ zip_ref.extractall(app_data_dir)
elif archive_name.endswith('.tar.gz'):
import tarfile
- with tarfile.open(archive_name, 'r:gz') as tar_ref:
- tar_ref.extractall('.')
+ with tarfile.open(archive_path, 'r:gz') as tar_ref:
+ tar_ref.extractall(app_data_dir)
# 查找反代版本可执行文件
found_executable = None
- for root, dirs, files in os.walk('.'):
+ for root, dirs, files in os.walk(app_data_dir):
for file in files:
if file.startswith('CloudflareST_proxy_') and not file.endswith(('.zip', '.tar.gz')):
found_executable = os.path.join(root, file)
@@ -630,19 +688,12 @@ def download_cloudflare_speedtest(os_type, arch_type):
break
if found_executable:
- # 获取最终文件名 - 使用标准格式
- if os_type == "win":
- final_name = f"CloudflareST_proxy_{os_type}_{arch_type}.exe"
- else:
- final_name = f"CloudflareST_proxy_{os_type}_{arch_type}"
-
- # 如果文件不在当前目录或文件名不匹配,移动到当前目录并重命名
- if os.path.abspath(found_executable) != os.path.abspath(final_name):
- if os.path.exists(final_name):
- os.remove(final_name)
- # 确保源文件存在
+ # 如果文件名不匹配,重命名
+ if os.path.abspath(found_executable) != os.path.abspath(proxy_exec_path):
+ if os.path.exists(proxy_exec_path):
+ os.remove(proxy_exec_path)
if os.path.exists(found_executable):
- os.rename(found_executable, final_name)
+ os.rename(found_executable, proxy_exec_path)
else:
print(f"❌ 源文件不存在: {found_executable}")
if sys.platform == "win32":
@@ -651,15 +702,20 @@ def download_cloudflare_speedtest(os_type, arch_type):
# 设置执行权限
if os_type != "win":
- os.chmod(final_name, 0o755)
+ os.chmod(proxy_exec_path, 0o755)
+
+ # 清理压缩包
+ try:
+ os.remove(archive_path)
+ except Exception:
+ pass
- print(f"✓ 反代版本设置完成: {final_name}")
- return final_name
+ print(f"✓ 反代版本设置完成: {proxy_exec_path}")
+ return proxy_exec_path
else:
print("解压后未找到反代版本可执行文件")
- # 列出解压后的所有文件用于调试
print("解压后的文件:")
- for root, dirs, files in os.walk('.'):
+ for root, dirs, files in os.walk(app_data_dir):
for file in files:
if not file.endswith(('.zip', '.tar.gz', '.txt', '.md')):
print(f" - {os.path.join(root, file)}")
@@ -667,9 +723,6 @@ def download_cloudflare_speedtest(os_type, arch_type):
input("按 Enter 键退出...")
sys.exit(1)
- # 清理压缩包
- os.remove(archive_name)
-
except Exception as e:
print(f"解压失败: {e}")
if sys.platform == "win32":
@@ -678,10 +731,23 @@ def download_cloudflare_speedtest(os_type, arch_type):
# 在Unix系统上赋予执行权限
if os_type != "win":
- os.chmod(proxy_exec_name, 0o755)
- print(f"已赋予执行权限: {proxy_exec_name}")
+ os.chmod(proxy_exec_path, 0o755)
+ print(f"已赋予执行权限: {proxy_exec_path}")
- return proxy_exec_name
+ return proxy_exec_path
+
+
+def get_exec_cmd(exec_name):
+ """
+ 获取可执行文件的命令路径
+ 如果是绝对路径直接返回,否则在 Unix 系统上添加 ./ 前缀
+ """
+ if os.path.isabs(exec_name):
+ return exec_name
+ elif sys.platform == "win32":
+ return exec_name
+ else:
+ return f"./{exec_name}"
def select_ip_version():
@@ -705,6 +771,210 @@ def select_ip_version():
print("✗ 请输入 1 或 2")
+def select_ports():
+ """选择要测试的端口"""
+ print("\n" + "=" * 60)
+ print(" 端口选择")
+ print("=" * 60)
+ print(" Cloudflare 支持的 HTTPS 端口:")
+ for i, port in enumerate(CLOUDFLARE_HTTPS_PORTS, 1):
+ default_mark = " (默认)" if port == 443 else ""
+ print(f" {i}. {port}{default_mark}")
+ print(f" {len(CLOUDFLARE_HTTPS_PORTS) + 1}. 全部端口 - 测试所有支持的端口")
+ print(f" {len(CLOUDFLARE_HTTPS_PORTS) + 2}. 自定义 - 手动选择多个端口")
+ print("=" * 60)
+ print("💡 提示: 尽量使用单端口, 多端口会大大增加测试时间")
+
+ while True:
+ choice = input(f"\n请选择端口 [1-{len(CLOUDFLARE_HTTPS_PORTS) + 2}, 默认: 1]: ").strip()
+ if not choice or choice == "1":
+ print("✓ 已选择端口: 443")
+ return [443]
+
+ try:
+ choice_int = int(choice)
+ if 1 <= choice_int <= len(CLOUDFLARE_HTTPS_PORTS):
+ selected_port = CLOUDFLARE_HTTPS_PORTS[choice_int - 1]
+ print(f"✓ 已选择端口: {selected_port}")
+ return [selected_port]
+ elif choice_int == len(CLOUDFLARE_HTTPS_PORTS) + 1:
+ print(f"✓ 已选择全部端口: {', '.join(map(str, CLOUDFLARE_HTTPS_PORTS))}")
+ return CLOUDFLARE_HTTPS_PORTS.copy()
+ elif choice_int == len(CLOUDFLARE_HTTPS_PORTS) + 2:
+ # 自定义选择多个端口
+ # 使用单字节逗号显示,方便用户复制
+ ports_str = ','.join(map(str, CLOUDFLARE_HTTPS_PORTS))
+ print(f"\n请输入要测试的端口号, 用逗号分隔 (可直接复制下方端口)")
+ print(f"可选端口: {ports_str}")
+ custom_input = input("端口列表: ").strip()
+ if custom_input:
+ selected_ports = []
+ # 同时支持中英文逗号
+ for p in custom_input.replace(',', ',').split(','):
+ p = p.strip()
+ if p.isdigit():
+ port_int = int(p)
+ if port_int in CLOUDFLARE_HTTPS_PORTS:
+ if port_int not in selected_ports:
+ selected_ports.append(port_int)
+ else:
+ print(f"⚠️ 端口 {port_int} 不在 Cloudflare 支持列表中, 已跳过")
+ if selected_ports:
+ print(f"✓ 已选择端口: {', '.join(map(str, selected_ports))}")
+ return selected_ports
+ else:
+ print("✗ 未选择有效端口, 请重新选择")
+ else:
+ print("✗ 输入为空, 请重新选择")
+ else:
+ print(f"✗ 请输入 1-{len(CLOUDFLARE_HTTPS_PORTS) + 2} 之间的数字")
+ except ValueError:
+ print("✗ 请输入有效的数字")
+
+
+def select_speedtest_url():
+ """选择测速 URL"""
+ # 尝试读取上次保存的自定义 URL
+ saved_url = None
+ config = load_config()
+ if config and config.get("speedtest_url"):
+ saved_url = config.get("speedtest_url")
+
+ print("\n" + "=" * 60)
+ print(" 测速 URL 选择")
+ print("=" * 60)
+ print(" 1. 默认 URL - cf.xiu2.xyz (可能不支持多端口)")
+ if saved_url:
+ print(f" 2. 上次使用 - {saved_url}")
+ print(" 3. 自定义 URL - 输入新的测速地址")
+ else:
+ print(" 2. 自定义 URL - 输入自建测速地址 (可根据 URL 特性确认是否支持多端口)")
+ print("=" * 60)
+
+ while True:
+ if saved_url:
+ choice = input("\n请选择 [1/2/3, 默认: 2]: ").strip()
+ if not choice or choice == "2":
+ print(f"✓ 使用上次的测速 URL: {saved_url}")
+ return saved_url
+ elif choice == "1":
+ print(f"✓ 使用默认测速 URL: {DEFAULT_SPEEDTEST_URL}")
+ print("⚠️ 注意: 默认 URL 可能不支持多端口, 非 443 端口下载测速可能失败")
+ return DEFAULT_SPEEDTEST_URL
+ elif choice == "3":
+ custom_url = input("请输入自定义测速 URL (https://...): ").strip()
+ if custom_url:
+ if not custom_url.startswith("http"):
+ custom_url = "https://" + custom_url
+ print(f"✓ 使用自定义测速 URL: {custom_url}")
+ # 保存到配置文件
+ save_config(speedtest_url=custom_url)
+ return custom_url
+ else:
+ print("✗ URL 不能为空, 请重新输入")
+ else:
+ print("✗ 请输入 1, 2 或 3")
+ else:
+ choice = input("\n请选择 [1/2, 默认: 1]: ").strip()
+ if not choice or choice == "1":
+ print(f"✓ 使用默认测速 URL: {DEFAULT_SPEEDTEST_URL}")
+ print("⚠️ 注意: 默认 URL 可能不支持多端口, 非 443 端口下载测速可能失败")
+ return DEFAULT_SPEEDTEST_URL
+ elif choice == "2":
+ custom_url = input("请输入自定义测速 URL (https://...): ").strip()
+ if custom_url:
+ if not custom_url.startswith("http"):
+ custom_url = "https://" + custom_url
+ print(f"✓ 使用自定义测速 URL: {custom_url}")
+ # 保存到配置文件
+ save_config(speedtest_url=custom_url)
+ return custom_url
+ else:
+ print("✗ URL 不能为空, 请重新输入")
+ else:
+ print("✗ 请输入 1 或 2")
+
+
+def generate_ip_with_ports(ip_file, ports, output_file="ip_with_ports.txt"):
+ """根据 IP 文件和端口列表生成带端口的 IP 文件
+
+ CloudflareSpeedTest 支持的格式:
+ - 单个 IP: 1.2.3.4:443
+ - CIDR 格式需要使用 -tp 参数指定端口,不能在 IP 后面加端口
+
+ Args:
+ ip_file: 原始 IP 文件路径
+ ports: 端口列表
+ output_file: 输出文件路径
+
+ Returns:
+ tuple: (输出文件路径, 端口列表) 或 (None, None) 失败时
+ """
+ if not os.path.exists(ip_file):
+ print(f"❌ IP 文件不存在: {ip_file}")
+ return None, None
+
+ try:
+ # 读取原始 IP 列表
+ with open(ip_file, 'r', encoding='utf-8') as f:
+ ips = [line.strip() for line in f if line.strip()]
+
+ if not ips:
+ print("❌ IP 文件为空")
+ return None, None
+
+ # 检查是否包含 CIDR 格式
+ has_cidr = any('/' in ip for ip in ips)
+
+ if has_cidr:
+ # CIDR 格式不能在 IP 后面加端口,需要使用 -tp 参数
+ # 直接复制原文件,返回端口列表供命令行使用
+ print(f"📝 检测到 CIDR 格式,将使用 -tp 参数指定端口")
+
+ # 复制原文件到输出文件
+ with open(output_file, 'w', encoding='utf-8') as f:
+ for ip in ips:
+ f.write(ip + '\n')
+
+ print(f"✅ IP 文件已准备: {output_file}")
+ print(f" IP/CIDR 数量: {len(ips)}")
+ print(f" 测试端口: {', '.join(map(str, ports))}")
+
+ return output_file, ports
+ else:
+ # 单个 IP 格式,可以在 IP 后面加端口
+ ip_port_list = []
+ for ip in ips:
+ # 如果 IP 已经包含端口,跳过
+ if ':' in ip and not ip.startswith('['): # IPv4:port 格式
+ ip_port_list.append(ip)
+ elif ip.startswith('[') and ']:' in ip: # [IPv6]:port 格式
+ ip_port_list.append(ip)
+ else:
+ # 为每个端口生成一条记录
+ for port in ports:
+ if ':' in ip: # IPv6 地址
+ ip_port_list.append(f"[{ip}]:{port}")
+ else: # IPv4 地址
+ ip_port_list.append(f"{ip}:{port}")
+
+ # 写入输出文件
+ with open(output_file, 'w', encoding='utf-8') as f:
+ for ip_port in ip_port_list:
+ f.write(ip_port + '\n')
+
+ print(f"✅ 已生成带端口的 IP 文件: {output_file}")
+ print(f" 原始 IP 数量: {len(ips)}")
+ print(f" 端口数量: {len(ports)}")
+ print(f" 生成记录数: {len(ip_port_list)}")
+
+ return output_file, None # 端口已在文件中,不需要 -tp 参数
+
+ except Exception as e:
+ print(f"❌ 生成带端口 IP 文件失败: {e}")
+ return None, None
+
+
def download_cloudflare_ips(ip_version="ipv4", ip_file=CLOUDFLARE_IP_FILE):
"""下载或生成 Cloudflare IP 列表
@@ -889,18 +1159,20 @@ def display_preset_configs():
print("=" * 60)
-def get_user_input(ip_file=CLOUDFLARE_IP_FILE, ip_version="ipv4"):
+def get_user_input(ip_file=CLOUDFLARE_IP_FILE, ip_version="ipv4", selected_ports=None, speedtest_url=None):
"""获取用户输入参数
Args:
ip_file: 要使用的IP文件路径
ip_version: IP版本("ipv4" 或 "ipv6")
+ selected_ports: 已选择的端口列表
+ speedtest_url: 测速 URL
"""
# 询问功能选择
print("\n" + "=" * 60)
print(" 功能选择")
print("=" * 60)
- print(" 1. 小白快速测试 - 简单输入,适合新手")
+ print(" 1. 小白快速测试 - 简单输入, 适合新手")
print(" 2. 常规测速 - 测试指定机场码的IP速度")
print(" 3. 优选反代 - 从CSV文件生成反代IP列表")
print("=" * 60)
@@ -911,13 +1183,13 @@ def get_user_input(ip_file=CLOUDFLARE_IP_FILE, ip_version="ipv4"):
if choice == "1":
# 小白快速测试模式
- return handle_beginner_mode(ip_file, ip_version)
+ return handle_beginner_mode(ip_file, ip_version, selected_ports, speedtest_url)
elif choice == "3":
# 优选反代模式
return handle_proxy_mode()
else:
# 常规测速模式
- return handle_normal_mode(ip_file, ip_version)
+ return handle_normal_mode(ip_file, ip_version, selected_ports, speedtest_url)
def select_csv_file():
@@ -1126,20 +1398,26 @@ def handle_proxy_mode():
return None, None, None, None
-def handle_beginner_mode(ip_file=CLOUDFLARE_IP_FILE, ip_version="ipv4"):
+def handle_beginner_mode(ip_file=CLOUDFLARE_IP_FILE, ip_version="ipv4", selected_ports=None, speedtest_url=None):
"""处理小白快速测试模式
Args:
ip_file: 要使用的IP文件路径
ip_version: IP版本("ipv4" 或 "ipv6")
+ selected_ports: 已选择的端口列表
+ speedtest_url: 测速 URL
"""
print("\n" + "=" * 70)
print(" 小白快速测试模式")
print("=" * 70)
- print(" 此功能专为新手设计,只需要输入3个简单的数字即可开始测试")
+ print(" 此功能专为新手设计,只需要输入几个简单的数字即可开始测试")
print(" 无需了解复杂的参数设置,程序会引导您完成所有配置")
print("=" * 70)
+ # 如果没有传入端口,使用默认端口
+ if selected_ports is None:
+ selected_ports = [443]
+
# 获取测试IP数量
print("\n📊 第一步:设置测试IP数量")
print("说明:测试的IP数量越多,结果越准确,但耗时越长")
@@ -1236,8 +1514,21 @@ def handle_beginner_mode(ip_file=CLOUDFLARE_IP_FILE, ip_version="ipv4"):
print(f"\n🎯 开始测速...")
print(f"参数: 测试{dn_count}个IP, 速度下限{speed_limit}MB/s, 延迟上限{time_limit}ms")
+ print(f"端口: {', '.join(map(str, selected_ports))}")
print("模式: 小白快速测试(全自动,无需选择地区)")
+ # 如果选择了多个端口或非默认端口,生成带端口的 IP 文件
+ actual_ip_file = ip_file
+ tp_ports = None # 用于 -tp 参数的端口列表(CIDR 格式时使用)
+ if len(selected_ports) > 1 or selected_ports[0] != 443:
+ print(f"\n正在生成带端口的 IP 文件...")
+ generated_file, tp_ports = generate_ip_with_ports(ip_file, selected_ports, "ip_with_ports.txt")
+ if generated_file:
+ actual_ip_file = generated_file
+ else:
+ print("⚠️ 生成带端口文件失败,将使用默认端口 443")
+ tp_ports = None
+
# 直接使用 Cloudflare IP 列表进行测速
print(f"\n正在使用 Cloudflare IP 列表进行测速...")
@@ -1245,37 +1536,98 @@ def handle_beginner_mode(ip_file=CLOUDFLARE_IP_FILE, ip_version="ipv4"):
os_type, arch_type = get_system_info()
exec_name = download_cloudflare_speedtest(os_type, arch_type)
- # 构建测速命令
- if sys.platform == "win32":
- cmd = [exec_name]
+ # 如果是 CIDR 格式且有多个端口,需要分别测试每个端口
+ if tp_ports and len(tp_ports) > 1:
+ print(f"\n📝 CIDR 格式需要分别测试每个端口...")
+ all_results = []
+
+ for port in tp_ports:
+ print(f"\n🔍 正在测试端口 {port}...")
+
+ # 构建测速命令
+ cmd = [get_exec_cmd(exec_name)]
+
+ temp_result_file = f"result_port_{port}.csv"
+ cmd.extend([
+ "-f", actual_ip_file,
+ "-n", thread_count,
+ "-dn", dn_count,
+ "-sl", speed_limit,
+ "-tl", time_limit,
+ "-tp", str(port),
+ "-o", temp_result_file
+ ])
+
+ # 非 443 端口使用用户选择的测速 URL(默认 URL 不支持其他端口下载测速)
+ if port != 443 and speedtest_url:
+ cmd.extend(["-url", speedtest_url])
+
+ print(f"运行命令: {' '.join(cmd)}")
+ result = subprocess.run(cmd, encoding='utf-8', errors='replace')
+
+ if result.returncode == 0 and os.path.exists(temp_result_file):
+ # 读取结果并添加端口信息
+ with open(temp_result_file, 'r', encoding='utf-8') as f:
+ lines = f.readlines()
+ if lines:
+ if not all_results: # 第一个文件,保留表头
+ all_results.append(lines[0])
+ all_results.extend(lines[1:]) # 跳过表头
+ os.remove(temp_result_file)
+
+ # 合并所有结果到 result.csv
+ if all_results:
+ with open("result.csv", 'w', encoding='utf-8') as f:
+ f.writelines(all_results)
+ print(f"\n✅ 所有端口测速完成!结果已合并保存到 result.csv")
+ print("📊 您可以查看 result.csv 文件来了解详细的测试结果")
+ print("💡 提示:结果文件中的IP按速度从快到慢排序")
+
+ # 询问是否上报结果
+ upload_info = upload_results_to_api("result.csv")
+ else:
+ print("\n❌ 所有端口测速均失败")
+ upload_info = None
else:
- cmd = [f"./{exec_name}"]
-
- cmd.extend([
- "-f", ip_file,
- "-n", thread_count,
- "-dn", dn_count,
- "-sl", speed_limit,
- "-tl", time_limit,
- "-url", DEFAULT_SPEEDTEST_URL,
- "-o", "result.csv"
- ])
-
- print(f"\n运行命令: {' '.join(cmd)}")
- print("=" * 50)
-
- # 运行测速
- result = subprocess.run(cmd, encoding='utf-8', errors='replace')
-
- if result.returncode == 0:
- print("\n✅ 测速完成!结果已保存到 result.csv")
- print("📊 您可以查看 result.csv 文件来了解详细的测试结果")
- print("💡 提示:结果文件中的IP按速度从快到慢排序")
+ # 单个端口或非 CIDR 格式
+ # 构建测速命令
+ cmd = [get_exec_cmd(exec_name)]
+
+ cmd.extend([
+ "-f", actual_ip_file,
+ "-n", thread_count,
+ "-dn", dn_count,
+ "-sl", speed_limit,
+ "-tl", time_limit,
+ "-o", "result.csv"
+ ])
- # 询问是否上报结果
- upload_info = upload_results_to_api("result.csv")
+ # 如果需要使用 -tp 参数指定单个端口(CIDR 格式时)
+ if tp_ports and len(tp_ports) == 1:
+ cmd.extend(["-tp", str(tp_ports[0])])
+ # 非 443 端口使用用户选择的测速 URL
+ if tp_ports[0] != 443 and speedtest_url:
+ cmd.extend(["-url", speedtest_url])
- # 输出对应的命令行命令
+ print(f"\n运行命令: {' '.join(cmd)}")
+ print("=" * 50)
+
+ # 运行测速
+ result = subprocess.run(cmd, encoding='utf-8', errors='replace')
+
+ if result.returncode == 0:
+ print("\n✅ 测速完成!结果已保存到 result.csv")
+ print("📊 您可以查看 result.csv 文件来了解详细的测试结果")
+ print("💡 提示:结果文件中的IP按速度从快到慢排序")
+
+ # 询问是否上报结果
+ upload_info = upload_results_to_api("result.csv")
+ else:
+ print("\n❌ 测速失败")
+ upload_info = None
+
+ # 输出对应的命令行命令
+ if upload_info is not None or (tp_ports and len(tp_ports) > 1 and all_results):
print("\n" + "=" * 80)
print(" 💡 快速复用命令")
print("=" * 80)
@@ -1289,19 +1641,23 @@ def handle_beginner_mode(ip_file=CLOUDFLARE_IP_FILE, ip_version="ipv4"):
print("-" * 80)
print("💡 提示:您可以复制上面的命令,下次直接使用命令行模式运行")
print("=" * 80)
- else:
- print("\n❌ 测速失败")
return "ALL", dn_count, speed_limit, time_limit, thread_count
-def handle_normal_mode(ip_file=CLOUDFLARE_IP_FILE, ip_version="ipv4"):
+def handle_normal_mode(ip_file=CLOUDFLARE_IP_FILE, ip_version="ipv4", selected_ports=None, speedtest_url=None):
"""处理常规测速模式
Args:
ip_file: 要使用的IP文件路径
ip_version: IP版本("ipv4" 或 "ipv6")
+ selected_ports: 已选择的端口列表
+ speedtest_url: 测速 URL
"""
+ # 如果没有传入端口,使用默认端口
+ if selected_ports is None:
+ selected_ports = [443]
+
print("\n开始检测可用地区...")
print("正在使用HTTPing模式检测各地区可用性...")
@@ -1442,6 +1798,7 @@ def handle_normal_mode(ip_file=CLOUDFLARE_IP_FILE, ip_version="ipv4"):
print("✗ 请输入有效的数字")
print(f"\n测速参数: 地区={cfcolo}, 测试{dn_count}个IP, 速度下限{speed_limit}MB/s, 延迟上限{time_limit}ms, 线程数={thread_count}")
+ print(f"端口: {', '.join(map(str, selected_ports))}")
print("模式: 常规测速(指定地区)")
# 从地区扫描结果中提取该地区的IP进行测速
@@ -1468,41 +1825,112 @@ def handle_normal_mode(ip_file=CLOUDFLARE_IP_FILE, ip_version="ipv4"):
print(f"找到 {len(region_ips)} 个 {cfcolo} 地区的IP,开始测速...")
+ # 如果选择了多个端口或非默认端口,生成带端口的 IP 文件
+ actual_ip_file = region_ip_file
+ tp_ports = None
+ if len(selected_ports) > 1 or selected_ports[0] != 443:
+ print(f"\n正在生成带端口的 IP 文件...")
+ generated_file, tp_ports = generate_ip_with_ports(region_ip_file, selected_ports, "ip_with_ports.txt")
+ if generated_file:
+ actual_ip_file = generated_file
+ else:
+ print("⚠️ 生成带端口文件失败,将使用默认端口 443")
+ tp_ports = None
+
+ print(f"开始测速...")
+
# 使用该地区的IP文件进行测速
os_type, arch_type = get_system_info()
exec_name = download_cloudflare_speedtest(os_type, arch_type)
- # 构建测速命令
- if sys.platform == "win32":
- cmd = [exec_name]
+ # 如果是 CIDR 格式且有多个端口,需要分别测试每个端口
+ if tp_ports and len(tp_ports) > 1:
+ print(f"\n📝 CIDR 格式需要分别测试每个端口...")
+ all_results = []
+
+ for port in tp_ports:
+ print(f"\n🔍 正在测试端口 {port}...")
+
+ cmd = [get_exec_cmd(exec_name)]
+
+ temp_result_file = f"result_port_{port}.csv"
+ cmd.extend([
+ "-f", actual_ip_file,
+ "-n", thread_count,
+ "-dn", dn_count,
+ "-sl", speed_limit,
+ "-tl", time_limit,
+ "-tp", str(port),
+ "-o", temp_result_file
+ ])
+
+ # 非 443 端口使用用户选择的测速 URL(默认 URL 不支持其他端口下载测速)
+ if port != 443 and speedtest_url:
+ cmd.extend(["-url", speedtest_url])
+
+ print(f"运行命令: {' '.join(cmd)}")
+ result = subprocess.run(cmd, encoding='utf-8', errors='replace')
+
+ if result.returncode == 0 and os.path.exists(temp_result_file):
+ with open(temp_result_file, 'r', encoding='utf-8') as f:
+ lines = f.readlines()
+ if lines:
+ if not all_results:
+ all_results.append(lines[0])
+ all_results.extend(lines[1:])
+ os.remove(temp_result_file)
+
+ # 清理临时文件
+ if os.path.exists(region_ip_file):
+ os.remove(region_ip_file)
+ if actual_ip_file != region_ip_file and os.path.exists(actual_ip_file):
+ os.remove(actual_ip_file)
+
+ if all_results:
+ with open("result.csv", 'w', encoding='utf-8') as f:
+ f.writelines(all_results)
+ print(f"\n✅ 所有端口测速完成!结果已合并保存到 result.csv")
+ upload_info = upload_results_to_api("result.csv")
+ else:
+ print("\n❌ 所有端口测速均失败")
+ upload_info = None
else:
- cmd = [f"./{exec_name}"]
-
- cmd.extend([
- "-f", region_ip_file,
- "-n", thread_count,
- "-dn", dn_count,
- "-sl", speed_limit,
- "-tl", time_limit,
- "-url", DEFAULT_SPEEDTEST_URL,
- "-o", "result.csv"
- ])
-
- print(f"\n运行命令: {' '.join(cmd)}")
- print("=" * 50)
-
- # 运行测速
- result = subprocess.run(cmd, encoding='utf-8', errors='replace')
-
- # 清理临时文件
- if os.path.exists(region_ip_file):
- os.remove(region_ip_file)
-
- if result.returncode == 0:
- print("\n✅ 测速完成!结果已保存到 result.csv")
+ # 单个端口或非 CIDR 格式
+ cmd = [get_exec_cmd(exec_name)]
+
+ cmd.extend([
+ "-f", actual_ip_file,
+ "-n", thread_count,
+ "-dn", dn_count,
+ "-sl", speed_limit,
+ "-tl", time_limit,
+ "-o", "result.csv"
+ ])
- # 询问是否上报结果
- upload_info = upload_results_to_api("result.csv")
+ # 如果需要使用 -tp 参数指定单个端口(CIDR 格式时)
+ if tp_ports and len(tp_ports) == 1:
+ cmd.extend(["-tp", str(tp_ports[0])])
+ if tp_ports[0] != 443 and speedtest_url:
+ cmd.extend(["-url", speedtest_url])
+
+ print(f"\n运行命令: {' '.join(cmd)}")
+ print("=" * 50)
+
+ # 运行测速
+ result = subprocess.run(cmd, encoding='utf-8', errors='replace')
+
+ # 清理临时文件
+ if os.path.exists(region_ip_file):
+ os.remove(region_ip_file)
+ if actual_ip_file != region_ip_file and os.path.exists(actual_ip_file):
+ os.remove(actual_ip_file)
+
+ if result.returncode == 0:
+ print("\n✅ 测速完成!结果已保存到 result.csv")
+ upload_info = upload_results_to_api("result.csv")
+ else:
+ print("\n❌ 测速失败")
+ upload_info = None
# 输出对应的命令行命令
print("\n" + "=" * 80)
@@ -1518,8 +1946,6 @@ def handle_normal_mode(ip_file=CLOUDFLARE_IP_FILE, ip_version="ipv4"):
print("-" * 80)
print("💡 提示:您可以复制上面的命令,下次直接使用命令行模式运行")
print("=" * 80)
- else:
- print("\n❌ 测速失败")
else:
print(f"❌ 未找到 {cfcolo} 地区的IP")
else:
@@ -1621,7 +2047,7 @@ def run_speedtest_with_file(ip_file, dn_count, speed_limit, time_limit, thread_c
# 构建命令(反代模式使用TCPing,专注于端口信息)
cmd = [
- f"./{exec_name}",
+ get_exec_cmd(exec_name),
"-f", ip_file,
"-n", thread_count,
"-dn", dn_count,
@@ -1665,10 +2091,7 @@ def run_speedtest(exec_name, cfcolo, dn_count, speed_limit, time_limit, thread_c
print("-" * 50)
# 构建命令
- if sys.platform == "win32":
- cmd = [exec_name]
- else:
- cmd = [f"./{exec_name}"]
+ cmd = [get_exec_cmd(exec_name)]
cmd.extend([
"-n", thread_count,
@@ -1834,10 +2257,7 @@ def run_with_args(args):
return 1
# 构建测速命令
- if sys.platform == "win32":
- cmd = [exec_name]
- else:
- cmd = [f"./{exec_name}"]
+ cmd = [get_exec_cmd(exec_name)]
cmd.extend([
"-f", ip_file,
@@ -1922,10 +2342,7 @@ def run_with_args(args):
print(f"找到 {len(region_ips)} 个 {args.region} 地区的IP,开始测速...")
# 构建测速命令
- if sys.platform == "win32":
- cmd = [exec_name]
- else:
- cmd = [f"./{exec_name}"]
+ cmd = [get_exec_cmd(exec_name)]
cmd.extend([
"-f", region_ip_file,
@@ -2147,6 +2564,14 @@ def main():
print("❌ 准备IP列表失败")
return 1
+ # 选择测试端口
+ selected_ports = select_ports()
+
+ # 如果选择了非 443 端口,询问测速 URL
+ speedtest_url = None
+ if len(selected_ports) > 1 or (len(selected_ports) == 1 and selected_ports[0] != 443):
+ speedtest_url = select_speedtest_url()
+
# 获取用户输入
print(f"\n[参数配置]")
print("=" * 60)
@@ -2155,7 +2580,7 @@ def main():
print(" 博客 https://joeyblog.net")
print(" Telegram交流群: https://t.me/+ft-zI76oovgwNmRh")
print("=" * 60)
- result = get_user_input(ip_file, ip_version)
+ result = get_user_input(ip_file, ip_version, selected_ports, speedtest_url)
# 检查是否是优选反代模式
if result == (None, None, None, None):
@@ -2669,7 +3094,7 @@ def load_config():
return None
-def save_config(worker_domain=None, uuid=None, github_token=None, repo_info=None, file_path=None):
+def save_config(worker_domain=None, uuid=None, github_token=None, repo_info=None, file_path=None, speedtest_url=None):
"""保存配置到文件"""
try:
# 加载现有配置
@@ -2694,6 +3119,10 @@ def save_config(worker_domain=None, uuid=None, github_token=None, repo_info=None
existing_config["file_path"] = file_path
existing_config["github_last_used"] = datetime.now().strftime('%Y-%m-%d %H:%M:%S')
+ # 保存自定义测速 URL
+ if speedtest_url:
+ existing_config["speedtest_url"] = speedtest_url
+
# 保存配置
with open(CONFIG_FILE, 'w', encoding='utf-8') as f:
json.dump(existing_config, f, ensure_ascii=False, indent=2)
@@ -4181,10 +4610,7 @@ def detect_available_regions():
exec_name = download_cloudflare_speedtest(os_type, arch_type)
# 构建检测命令 - 使用HTTPing模式快速检测
- if sys.platform == "win32":
- cmd = [exec_name]
- else:
- cmd = [f"./{exec_name}"]
+ cmd = [get_exec_cmd(exec_name)]
cmd.extend([
"-dd", # 禁用下载测速,只做延迟测试
diff --git a/cloudflare_speedtest_gui.py b/cloudflare_speedtest_gui.py
new file mode 100644
index 0000000..e50dfa4
--- /dev/null
+++ b/cloudflare_speedtest_gui.py
@@ -0,0 +1,1234 @@
+#!/usr/bin/env python3
+# -*- coding: utf-8 -*-
+"""yx-tools-gui - 基于 Flet 的跨平台图形界面
+"""
+
+import flet as ft
+import subprocess
+import threading
+import os
+import sys
+import json
+import csv
+from datetime import datetime
+
+# 导入核心功能模块
+try:
+ from cloudflare_speedtest import (
+ CLOUDFLARE_HTTPS_PORTS,
+ DEFAULT_SPEEDTEST_URL,
+ CLOUDFLARE_IP_FILE,
+ CLOUDFLARE_IPV6_FILE,
+ CONFIG_FILE,
+ AIRPORT_CODES,
+ get_system_info,
+ download_cloudflare_speedtest,
+ download_cloudflare_ips,
+ generate_ip_with_ports,
+ load_config,
+ save_config,
+ generate_ipv6_file,
+ get_exec_cmd,
+ )
+except ImportError:
+ print("错误: 请确保 cloudflare_speedtest.py 在同一目录下")
+ sys.exit(1)
+
+
+# 主题颜色
+PRIMARY_COLOR = "#FF6B35" # 橙色
+SECONDARY_COLOR = "#004E89" # 深蓝色
+SUCCESS_COLOR = "#28A745"
+WARNING_COLOR = "#FFC107"
+ERROR_COLOR = "#DC3545"
+
+# 浅色主题
+LIGHT_BG_COLOR = "#F0F2F5"
+LIGHT_CARD_COLOR = "#FFFFFF"
+LIGHT_TEXT_COLOR = "#1A1A1A"
+
+# 深色主题
+DARK_BG_COLOR = "#1A1A2E"
+DARK_CARD_COLOR = "#2D2D44"
+DARK_TEXT_COLOR = "#E8E8E8"
+
+
+class CloudflareSpeedTestGUI:
+ def __init__(self, page: ft.Page):
+ self.page = page
+ self.setup_page()
+ self.create_ui()
+
+ def setup_page(self):
+ """设置页面属性"""
+ self.page.title = "yx-tools-gui - 优选 IP 测速工具"
+ self.page.window.width = 1000
+ self.page.window.height = 800
+ self.page.window.min_width = 900
+ self.page.window.min_height = 700
+ self.page.theme_mode = ft.ThemeMode.LIGHT
+ self.page.bgcolor = LIGHT_BG_COLOR
+ self.page.padding = 0
+
+ def create_section_title(self, icon, title, subtitle=None):
+ """创建区域标题"""
+ items = [
+ ft.Icon(icon, size=20, color=PRIMARY_COLOR),
+ ft.Text(title, size=16, weight=ft.FontWeight.BOLD, color=SECONDARY_COLOR),
+ ]
+ if subtitle:
+ items.append(ft.Text(subtitle, size=12, color=ft.Colors.GREY_500))
+ return ft.Row(items, spacing=8)
+
+ def create_ui(self):
+ """创建用户界面"""
+ # 加载配置
+ config = load_config() or {}
+ saved_url = config.get("speedtest_url", "")
+
+ # 统一卡片样式
+ CARD_PADDING = 12
+ CARD_RADIUS = 8
+ CARD_COLOR = LIGHT_CARD_COLOR
+ CARD_SHADOW = ft.BoxShadow(
+ spread_radius=0,
+ blur_radius=4,
+ color=ft.Colors.with_opacity(0.05, ft.Colors.BLACK),
+ offset=ft.Offset(0, 1),
+ )
+
+ # ===== 顶部标题栏 =====
+ header = ft.Container(
+ content=ft.Row(
+ [
+ ft.Row([
+ ft.Image(
+ src=os.path.join(os.path.dirname(__file__), "icon", "icon.png"),
+ width=32,
+ height=32,
+ fit=ft.ImageFit.CONTAIN,
+ ) if os.path.exists(os.path.join(os.path.dirname(__file__), "icon", "icon.png"))
+ else ft.Icon(ft.Icons.SPEED, size=32, color=PRIMARY_COLOR),
+ ft.Text("yx-tools-gui", size=18, weight=ft.FontWeight.BOLD, color=SECONDARY_COLOR),
+ ], spacing=10),
+ ft.Row([
+ ft.IconButton(icon=ft.Icons.BRIGHTNESS_6, tooltip="切换主题",
+ on_click=self.toggle_theme, icon_size=18),
+ ft.IconButton(icon=ft.Icons.HELP_OUTLINE, tooltip="帮助",
+ on_click=self.show_help, icon_size=18),
+ ], spacing=0),
+ ],
+ alignment=ft.MainAxisAlignment.SPACE_BETWEEN,
+ ),
+ padding=ft.padding.symmetric(horizontal=15, vertical=10),
+ bgcolor=CARD_COLOR,
+ shadow=CARD_SHADOW,
+ )
+
+ # ==================== 左侧:设置区域 ====================
+
+ # ===== IP 版本选择 =====
+ self.ip_version = ft.RadioGroup(
+ content=ft.Row([
+ ft.Radio(value="ipv4", label="IPv4", active_color=PRIMARY_COLOR),
+ ft.Radio(value="ipv6", label="IPv6", active_color=PRIMARY_COLOR),
+ ], spacing=20),
+ value="ipv4",
+ )
+
+ ip_section = ft.Container(
+ content=ft.Column([
+ ft.Row([
+ ft.Icon(ft.Icons.LANGUAGE, size=14, color=PRIMARY_COLOR),
+ ft.Text("IP 版本", size=12, weight=ft.FontWeight.BOLD),
+ ], spacing=5),
+ self.ip_version,
+ ], spacing=8),
+ padding=CARD_PADDING,
+ bgcolor=CARD_COLOR,
+ border_radius=CARD_RADIUS,
+ shadow=CARD_SHADOW,
+ )
+
+ # ===== 端口选择 =====
+ self.port_checkboxes = {}
+ port_chips = []
+ for port in CLOUDFLARE_HTTPS_PORTS:
+ chip = ft.Chip(
+ label=ft.Text(str(port), size=11, width=30, text_align=ft.TextAlign.CENTER),
+ selected=(port == 443),
+ on_select=lambda e, p=port: self.on_port_select(e, p),
+ selected_color=PRIMARY_COLOR,
+ show_checkmark=False,
+ )
+ self.port_checkboxes[port] = chip
+ port_chips.append(chip)
+
+ port_section = ft.Container(
+ content=ft.Column([
+ ft.Row([
+ ft.Row([
+ ft.Icon(ft.Icons.ROUTER, size=14, color=PRIMARY_COLOR),
+ ft.Text("测试端口", size=12, weight=ft.FontWeight.BOLD),
+ ], spacing=5),
+ ft.Row([
+ ft.TextButton("全选", on_click=self.select_all_ports,
+ style=ft.ButtonStyle(color=PRIMARY_COLOR, padding=3)),
+ ft.TextButton("仅443", on_click=self.deselect_all_ports,
+ style=ft.ButtonStyle(color=ft.Colors.GREY_600, padding=3)),
+ ], spacing=0),
+ ], alignment=ft.MainAxisAlignment.SPACE_BETWEEN),
+ ft.Row(port_chips, wrap=True, spacing=5, run_spacing=5),
+ ft.Text("💡 多端口会增加测试时间", size=10, color=ft.Colors.GREY_500),
+ ], spacing=6),
+ padding=CARD_PADDING,
+ bgcolor=CARD_COLOR,
+ border_radius=CARD_RADIUS,
+ shadow=CARD_SHADOW,
+ )
+
+ # ===== 测速 URL =====
+ self.url_type = ft.RadioGroup(
+ content=ft.Row([
+ ft.Radio(value="default", label="默认", active_color=PRIMARY_COLOR),
+ ft.Radio(value="custom", label="自定义", active_color=PRIMARY_COLOR),
+ ], spacing=15),
+ value="custom" if saved_url else "default",
+ on_change=self.on_url_type_change,
+ )
+
+ self.custom_url_field = ft.TextField(
+ value=saved_url,
+ visible=bool(saved_url),
+ hint_text="https://your-speedtest-url.com",
+ border_radius=5,
+ content_padding=ft.padding.symmetric(horizontal=8, vertical=6),
+ text_size=11,
+ dense=True,
+ )
+
+ url_section = ft.Container(
+ content=ft.Column([
+ ft.Row([
+ ft.Icon(ft.Icons.CLOUD_DOWNLOAD, size=14, color=PRIMARY_COLOR),
+ ft.Text("测速 URL", size=12, weight=ft.FontWeight.BOLD),
+ ], spacing=5),
+ self.url_type,
+ self.custom_url_field,
+ ft.Text("⚠️ 默认URL可能不支持非443端口", size=10, color=ft.Colors.ORANGE_700),
+ ], spacing=5),
+ padding=CARD_PADDING,
+ bgcolor=CARD_COLOR,
+ border_radius=CARD_RADIUS,
+ shadow=CARD_SHADOW,
+ )
+
+ # ===== 测速参数 =====
+ self.dn_count = ft.TextField(
+ value="10",
+ keyboard_type=ft.KeyboardType.NUMBER,
+ border_radius=5,
+ text_size=12,
+ height=36,
+ content_padding=ft.padding.symmetric(horizontal=10, vertical=8),
+ expand=True,
+ )
+ self.speed_limit = ft.TextField(
+ value="1",
+ keyboard_type=ft.KeyboardType.NUMBER,
+ border_radius=5,
+ text_size=12,
+ height=36,
+ content_padding=ft.padding.symmetric(horizontal=10, vertical=8),
+ expand=True,
+ )
+ self.time_limit = ft.TextField(
+ value="500",
+ keyboard_type=ft.KeyboardType.NUMBER,
+ border_radius=5,
+ text_size=12,
+ height=36,
+ content_padding=ft.padding.symmetric(horizontal=10, vertical=8),
+ expand=True,
+ )
+ self.thread_count = ft.TextField(
+ value="200",
+ keyboard_type=ft.KeyboardType.NUMBER,
+ border_radius=5,
+ text_size=12,
+ height=36,
+ content_padding=ft.padding.symmetric(horizontal=10, vertical=8),
+ expand=True,
+ )
+
+ # 参数项:标签 + 输入框
+ def param_item(label, field):
+ return ft.Column([
+ ft.Text(label, size=11, color=ft.Colors.GREY_600),
+ field,
+ ], spacing=3, expand=True)
+
+ params_section = ft.Container(
+ content=ft.Column([
+ ft.Row([
+ ft.Icon(ft.Icons.TUNE, size=14, color=PRIMARY_COLOR),
+ ft.Text("测速参数", size=12, weight=ft.FontWeight.BOLD),
+ ], spacing=5),
+ ft.Row([
+ param_item("IP 数量", self.dn_count),
+ param_item("速度下限 (MB/s)", self.speed_limit),
+ ], spacing=12),
+ ft.Row([
+ param_item("延迟上限 (ms)", self.time_limit),
+ param_item("线程数", self.thread_count),
+ ], spacing=12),
+ ], spacing=8),
+ padding=CARD_PADDING,
+ bgcolor=CARD_COLOR,
+ border_radius=CARD_RADIUS,
+ shadow=CARD_SHADOW,
+ )
+
+ # ===== 控制按钮 =====
+ self.start_btn = ft.ElevatedButton(
+ content=ft.Row([
+ ft.Icon(ft.Icons.PLAY_ARROW, color=ft.Colors.WHITE, size=16),
+ ft.Text("开始测速", size=13, weight=ft.FontWeight.BOLD, color=ft.Colors.WHITE),
+ ], spacing=5, alignment=ft.MainAxisAlignment.CENTER),
+ on_click=self.start_speedtest,
+ style=ft.ButtonStyle(bgcolor=PRIMARY_COLOR, shape=ft.RoundedRectangleBorder(radius=6)),
+ height=36, expand=True,
+ )
+
+ self.stop_btn = ft.ElevatedButton(
+ content=ft.Row([
+ ft.Icon(ft.Icons.STOP, color=ft.Colors.WHITE, size=16),
+ ft.Text("停止", size=13, color=ft.Colors.WHITE),
+ ], spacing=5, alignment=ft.MainAxisAlignment.CENTER),
+ on_click=self.stop_speedtest,
+ disabled=True,
+ style=ft.ButtonStyle(bgcolor=ERROR_COLOR, shape=ft.RoundedRectangleBorder(radius=6)),
+ height=36, expand=True,
+ )
+
+ self.progress_ring = ft.ProgressRing(width=16, height=16, stroke_width=2, color=PRIMARY_COLOR, visible=False)
+ self.progress_bar = ft.ProgressBar(color=PRIMARY_COLOR, bgcolor=ft.Colors.GREY_200, visible=False, expand=True)
+ self.status_text = ft.Text("就绪", size=11, color=ft.Colors.GREY_600)
+
+ control_section = ft.Container(
+ content=ft.Column([
+ ft.Row([self.start_btn, self.stop_btn], spacing=10),
+ ft.Row([self.progress_ring, self.status_text, self.progress_bar], spacing=6),
+ ], spacing=8),
+ padding=CARD_PADDING,
+ bgcolor=CARD_COLOR,
+ border_radius=CARD_RADIUS,
+ shadow=CARD_SHADOW,
+ )
+
+ # ===== 左侧面板 =====
+ left_panel = ft.Container(
+ content=ft.Column(
+ [
+ ip_section,
+ port_section,
+ url_section,
+ params_section,
+ ft.Container(expand=True), # 弹性空间
+ control_section,
+ ],
+ spacing=8,
+ expand=True,
+ scroll=ft.ScrollMode.AUTO,
+ ),
+ width=420,
+ expand=True,
+ )
+
+ # ==================== 右侧:日志和结果区域 ====================
+
+ # ===== 日志输出 =====
+ self.log_output = ft.TextField(
+ multiline=True,
+ read_only=True,
+ text_size=10,
+ value="🚀 准备就绪,点击「开始测速」按钮开始...\n",
+ border_radius=5,
+ border_color=ft.Colors.GREY_300,
+ content_padding=ft.padding.all(8),
+ expand=True,
+ )
+
+ log_section = ft.Container(
+ content=ft.Column([
+ ft.Row([
+ ft.Row([
+ ft.Icon(ft.Icons.TERMINAL, size=14, color=PRIMARY_COLOR),
+ ft.Text("运行日志", size=12, weight=ft.FontWeight.BOLD),
+ ], spacing=5),
+ ft.IconButton(icon=ft.Icons.CLEAR_ALL, tooltip="清空",
+ on_click=self.clear_log, icon_color=ft.Colors.GREY_500, icon_size=16),
+ ], alignment=ft.MainAxisAlignment.SPACE_BETWEEN),
+ self.log_output,
+ ], spacing=5, expand=True),
+ padding=CARD_PADDING,
+ bgcolor=CARD_COLOR,
+ border_radius=CARD_RADIUS,
+ shadow=CARD_SHADOW,
+ expand=1,
+ )
+
+ # ===== 结果表格 =====
+ self.result_table = ft.DataTable(
+ columns=[
+ ft.DataColumn(ft.Text("IP 地址", size=11, weight=ft.FontWeight.BOLD)),
+ ft.DataColumn(ft.Text("端口", size=11, weight=ft.FontWeight.BOLD)),
+ ft.DataColumn(ft.Text("延迟", size=11, weight=ft.FontWeight.BOLD), numeric=True),
+ ft.DataColumn(ft.Text("速度", size=11, weight=ft.FontWeight.BOLD), numeric=True),
+ ft.DataColumn(ft.Text("地区", size=11, weight=ft.FontWeight.BOLD)),
+ ],
+ rows=[],
+ border_radius=5,
+ heading_row_color=ft.Colors.GREY_100,
+ heading_row_height=32,
+ data_row_max_height=28,
+ data_row_min_height=24,
+ column_spacing=30,
+ horizontal_lines=ft.BorderSide(1, ft.Colors.GREY_200),
+ expand=True,
+ )
+
+ result_section = ft.Container(
+ content=ft.Column([
+ ft.Row([
+ ft.Row([
+ ft.Icon(ft.Icons.ANALYTICS, size=14, color=PRIMARY_COLOR),
+ ft.Text("测速结果", size=12, weight=ft.FontWeight.BOLD),
+ ], spacing=5),
+ ft.Row([
+ ft.IconButton(icon=ft.Icons.CLOUD_UPLOAD, tooltip="上传优选IP",
+ on_click=self.show_upload_dialog, icon_color=SUCCESS_COLOR, icon_size=16),
+ ft.IconButton(icon=ft.Icons.REFRESH, tooltip="刷新",
+ on_click=self.load_results, icon_color=PRIMARY_COLOR, icon_size=16),
+ ft.IconButton(icon=ft.Icons.FOLDER_OPEN, tooltip="打开文件夹",
+ on_click=self.export_results, icon_color=ft.Colors.GREY_600, icon_size=16),
+ ], spacing=0),
+ ], alignment=ft.MainAxisAlignment.SPACE_BETWEEN),
+ ft.Container(
+ content=ft.ListView(
+ controls=[self.result_table],
+ expand=True,
+ ),
+ border=ft.border.all(1, ft.Colors.GREY_200),
+ border_radius=5,
+ expand=True,
+ padding=0,
+ ),
+ ], spacing=5, expand=True),
+ padding=CARD_PADDING,
+ bgcolor=CARD_COLOR,
+ border_radius=CARD_RADIUS,
+ shadow=CARD_SHADOW,
+ expand=2,
+ )
+
+ # ===== 右侧面板 =====
+ right_panel = ft.Container(
+ content=ft.Column(
+ [
+ log_section,
+ result_section,
+ ],
+ spacing=10,
+ expand=True,
+ ),
+ expand=True,
+ )
+
+ # ===== 底部信息 =====
+ footer = ft.Container(
+ content=ft.Row(
+ [
+ ft.TextButton("GitHub", url="https://github.com/byJoey/yx-tools",
+ style=ft.ButtonStyle(padding=3)),
+ ft.Text("•", size=9, color=ft.Colors.GREY_400),
+ ft.TextButton("YouTube", url="https://www.youtube.com/@Joeyblog",
+ style=ft.ButtonStyle(padding=3)),
+ ft.Text("•", size=9, color=ft.Colors.GREY_400),
+ ft.TextButton("Telegram", url="https://t.me/+ft-zI76oovgwNmRh",
+ style=ft.ButtonStyle(padding=3)),
+ ft.Text("•", size=9, color=ft.Colors.GREY_400),
+ ft.Text("Made with ❤️ by Joey & Zag", size=10, color=ft.Colors.GREY_500),
+ ],
+ alignment=ft.MainAxisAlignment.CENTER,
+ spacing=6,
+ ),
+ padding=ft.padding.symmetric(vertical=8),
+ bgcolor=CARD_COLOR,
+ )
+
+ # ===== 主内容区域:左右两栏 =====
+ main_content = ft.Row(
+ [
+ ft.Container(content=left_panel, expand=2, padding=ft.padding.only(left=15, top=10, bottom=10, right=5)),
+ ft.Container(content=right_panel, expand=3, padding=ft.padding.only(left=5, top=10, bottom=10, right=15)),
+ ],
+ spacing=0,
+ expand=True,
+ )
+
+ # ===== 组装页面 =====
+ self.page.add(
+ ft.Column(
+ [
+ header,
+ main_content,
+ footer,
+ ],
+ spacing=0,
+ expand=True,
+ )
+ )
+
+ # 进程引用
+ self.process = None
+ self.running = False
+
+ def toggle_theme(self, e):
+ """切换主题"""
+ if self.page.theme_mode == ft.ThemeMode.LIGHT:
+ self.page.theme_mode = ft.ThemeMode.DARK
+ self.page.bgcolor = DARK_BG_COLOR
+ # 更新所有颜色
+ self.update_theme_colors(DARK_CARD_COLOR, DARK_TEXT_COLOR, is_dark=True)
+ else:
+ self.page.theme_mode = ft.ThemeMode.LIGHT
+ self.page.bgcolor = LIGHT_BG_COLOR
+ self.update_theme_colors(LIGHT_CARD_COLOR, LIGHT_TEXT_COLOR, is_dark=False)
+ self.page.update()
+
+ def update_theme_colors(self, card_color, text_color, is_dark=False):
+ """更新主题颜色"""
+ # 更新表格表头颜色
+ if hasattr(self, 'result_table'):
+ self.result_table.heading_row_color = "#3D3D5C" if is_dark else ft.Colors.GREY_100
+
+ # 遍历页面控件更新颜色
+ def update_control(control):
+ if isinstance(control, ft.Container):
+ if control.bgcolor in [LIGHT_CARD_COLOR, DARK_CARD_COLOR, "#FFFFFF", "#2D2D44"]:
+ control.bgcolor = card_color
+ if isinstance(control, ft.DataTable):
+ control.heading_row_color = "#3D3D5C" if is_dark else ft.Colors.GREY_100
+ if hasattr(control, 'controls'):
+ for c in control.controls:
+ update_control(c)
+ if hasattr(control, 'content') and control.content:
+ update_control(control.content)
+
+ for control in self.page.controls:
+ update_control(control)
+
+ def show_help(self, e):
+ """显示帮助"""
+ def close_dialog(e):
+ dialog.open = False
+ self.page.update()
+
+ dialog = ft.AlertDialog(
+ title=ft.Text("使用帮助"),
+ content=ft.Column([
+ ft.Text("1. 选择 IP 版本(IPv4 或 IPv6)", size=14),
+ ft.Text("2. 选择要测试的端口(可多选)", size=14),
+ ft.Text("3. 配置测速 URL(非 443 端口建议使用自定义 URL)", size=14),
+ ft.Text("4. 调整测速参数", size=14),
+ ft.Text("5. 点击「开始测速」按钮", size=14),
+ ft.Container(height=10),
+ ft.Text("💡 提示:", weight=ft.FontWeight.BOLD),
+ ft.Text("• 多端口测试会依次测试每个端口", size=12),
+ ft.Text("• 默认 URL 可能不支持非 443 端口", size=12),
+ ft.Text("• 自定义 URL 会自动保存", size=12),
+ ], tight=True, spacing=8),
+ actions=[
+ ft.TextButton("关闭", on_click=close_dialog),
+ ],
+ )
+ self.page.overlay.append(dialog)
+ dialog.open = True
+ self.page.update()
+
+ def on_port_select(self, e, port):
+ """端口选择变化"""
+ self.port_checkboxes[port].selected = e.control.selected
+ self.page.update()
+
+ def select_all_ports(self, e):
+ """全选端口"""
+ for chip in self.port_checkboxes.values():
+ chip.selected = True
+ self.page.update()
+
+ def deselect_all_ports(self, e):
+ """仅选择 443"""
+ for port, chip in self.port_checkboxes.items():
+ chip.selected = (port == 443)
+ self.page.update()
+
+ def on_url_type_change(self, e):
+ """URL 类型变化"""
+ self.custom_url_field.visible = (e.control.value == "custom")
+ self.page.update()
+
+ def on_url_change(self, e):
+ """URL 下拉框变化"""
+ self.custom_url_field.visible = (e.control.value == "custom")
+ self.page.update()
+
+ def log(self, message):
+ """添加日志"""
+ timestamp = datetime.now().strftime("%H:%M:%S")
+ self.log_output.value += f"[{timestamp}] {message}\n"
+ # 滚动到底部
+ self.log_output.value = self.log_output.value[-10000:] # 限制长度
+ self.page.update()
+
+ def clear_log(self, e):
+ """清空日志"""
+ self.log_output.value = ""
+ self.page.update()
+
+ def get_selected_ports(self):
+ """获取选中的端口"""
+ ports = []
+ for port, chip in self.port_checkboxes.items():
+ if chip.selected:
+ ports.append(port)
+ return ports if ports else [443]
+
+ def get_speedtest_url(self):
+ """获取测速 URL"""
+ if self.url_type.value == "custom" and self.custom_url_field.value:
+ url = self.custom_url_field.value.strip()
+ if not url.startswith("http"):
+ url = "https://" + url
+ # 保存到配置
+ save_config(speedtest_url=url)
+ return url
+ return DEFAULT_SPEEDTEST_URL
+
+ def export_results(self, e):
+ """用系统文件浏览器打开结果文件"""
+ result_file = "result.csv"
+ if not os.path.exists(result_file):
+ self.log("⚠️ 未找到结果文件")
+ return
+
+ abs_path = os.path.abspath(result_file)
+ self.log(f"📁 打开文件: {abs_path}")
+
+ # 用系统默认程序打开文件
+ try:
+ import subprocess
+ if sys.platform == "win32":
+ os.startfile(abs_path)
+ elif sys.platform == "darwin":
+ subprocess.run(["open", abs_path])
+ else:
+ # Linux - 尝试用文件管理器打开并选中文件
+ subprocess.run(["xdg-open", os.path.dirname(abs_path)])
+ except Exception as ex:
+ self.log(f"⚠️ 无法打开文件: {ex}")
+
+ def show_upload_dialog(self, e):
+ """显示上传对话框"""
+ result_file = "result.csv"
+ if not os.path.exists(result_file):
+ self.log("⚠️ 未找到结果文件,请先完成测速")
+ return
+
+ # 加载保存的配置
+ config = load_config() or {}
+ saved_domain = config.get('worker_domain', '')
+ saved_uuid = config.get('uuid', '')
+ saved_github_token = config.get('github_token', '')
+ saved_repo = config.get('repo_info', '')
+ saved_file_path = config.get('file_path', 'cloudflare_ips.txt')
+
+ # 获取当前主题的背景色
+ is_dark = self.page.theme_mode == ft.ThemeMode.DARK
+ dialog_bgcolor = DARK_CARD_COLOR if is_dark else LIGHT_CARD_COLOR
+
+ # ===== Cloudflare Workers API 字段 =====
+ self.upload_url_field = ft.TextField(
+ hint_text="https://你的域名/你的UUID或路径",
+ value=f"https://{saved_domain}/{saved_uuid}" if saved_domain and saved_uuid else "",
+ text_size=12,
+ dense=True,
+ height=38,
+ )
+ self.clear_existing_checkbox = ft.Checkbox(
+ label="清空现有IP后再上传",
+ value=True,
+ )
+
+ # ===== GitHub 字段 =====
+ self.github_token_field = ft.TextField(
+ hint_text="ghp_xxxxxxxxxxxx",
+ value=saved_github_token,
+ text_size=12,
+ dense=True,
+ password=True,
+ can_reveal_password=True,
+ height=38,
+ )
+ self.github_repo_field = ft.TextField(
+ hint_text="owner/repo",
+ value=saved_repo,
+ text_size=12,
+ dense=True,
+ height=38,
+ )
+ self.github_file_path_field = ft.TextField(
+ hint_text="cloudflare_ips.txt",
+ value=saved_file_path,
+ text_size=12,
+ dense=True,
+ height=38,
+ )
+
+ # ===== 通用字段 =====
+ # 读取结果文件获取最大数量
+ import csv
+ max_count = 0
+ try:
+ with open("result.csv", 'r', encoding='utf-8') as f:
+ reader = csv.DictReader(f)
+ max_count = sum(1 for row in reader if row.get('IP 地址'))
+ except:
+ max_count = 100
+
+ self.upload_max_count = max(1, max_count)
+ self.upload_count_value = min(10, self.upload_max_count)
+
+ self.upload_count_field = ft.TextField(
+ value=str(self.upload_count_value),
+ keyboard_type=ft.KeyboardType.NUMBER,
+ text_size=12,
+ width=60,
+ dense=True,
+ text_align=ft.TextAlign.CENTER,
+ height=32,
+ on_change=self.on_upload_count_change,
+ )
+
+ def decrease_count(e):
+ current = int(self.upload_count_field.value or "1")
+ if current > 1:
+ self.upload_count_field.value = str(current - 1)
+ self.page.update()
+
+ def increase_count(e):
+ current = int(self.upload_count_field.value or "1")
+ if current < self.upload_max_count:
+ self.upload_count_field.value = str(current + 1)
+ self.page.update()
+
+ upload_count_row = ft.Row([
+ ft.Text("上传数量", size=11, color=ft.Colors.GREY_500),
+ ft.IconButton(icon=ft.Icons.REMOVE, icon_size=16, on_click=decrease_count,
+ style=ft.ButtonStyle(padding=0)),
+ self.upload_count_field,
+ ft.IconButton(icon=ft.Icons.ADD, icon_size=16, on_click=increase_count,
+ style=ft.ButtonStyle(padding=0)),
+ ft.Text(f"/ {self.upload_max_count}", size=11, color=ft.Colors.GREY_500),
+ ], spacing=4, vertical_alignment=ft.CrossAxisAlignment.CENTER)
+
+ # API 上传内容
+ api_content = ft.Column([
+ ft.Text("管理页面 URL", size=11, color=ft.Colors.GREY_500),
+ self.upload_url_field,
+ self.clear_existing_checkbox,
+ ], spacing=6, tight=True)
+
+ # GitHub 上传内容
+ github_content = ft.Column([
+ ft.Text("GitHub Token", size=11, color=ft.Colors.GREY_500),
+ self.github_token_field,
+ ft.Text("仓库 (owner/repo)", size=11, color=ft.Colors.GREY_500),
+ self.github_repo_field,
+ ft.Text("文件路径", size=11, color=ft.Colors.GREY_500),
+ self.github_file_path_field,
+ ], spacing=6, tight=True)
+
+ # 标签页
+ self.upload_tabs = ft.Tabs(
+ selected_index=0,
+ tabs=[
+ ft.Tab(text="Cloudflare API", content=ft.Container(content=api_content, padding=10)),
+ ft.Tab(text="GitHub", content=ft.Container(content=github_content, padding=10)),
+ ],
+ height=180,
+ )
+
+ def close_dialog(e):
+ dialog.open = False
+ self.page.update()
+
+ def do_upload(e):
+ dialog.open = False
+ self.page.update()
+ # 根据选中的标签页执行不同的上传
+ if self.upload_tabs.selected_index == 0:
+ thread = threading.Thread(target=self.upload_to_api_thread)
+ else:
+ thread = threading.Thread(target=self.upload_to_github_thread)
+ thread.daemon = True
+ thread.start()
+
+ dialog = ft.AlertDialog(
+ title=ft.Text("上传优选IP", size=14, weight=ft.FontWeight.BOLD),
+ bgcolor=dialog_bgcolor,
+ content=ft.Container(
+ content=ft.Column([
+ self.upload_tabs,
+ ft.Divider(height=1),
+ upload_count_row,
+ ], spacing=6, tight=True),
+ width=400,
+ ),
+ actions=[
+ ft.TextButton("取消", on_click=close_dialog),
+ ft.ElevatedButton("上传", on_click=do_upload,
+ style=ft.ButtonStyle(bgcolor=SUCCESS_COLOR, color=ft.Colors.WHITE)),
+ ],
+ actions_alignment=ft.MainAxisAlignment.END,
+ )
+
+ self.page.overlay.append(dialog)
+ dialog.open = True
+ self.page.update()
+
+ def on_upload_count_change(self, e):
+ """验证上传数量输入"""
+ try:
+ value = int(e.control.value or "1")
+ if value < 1:
+ e.control.value = "1"
+ elif value > self.upload_max_count:
+ e.control.value = str(self.upload_max_count)
+ self.page.update()
+ except ValueError:
+ e.control.value = "10"
+ self.page.update()
+
+ def read_result_ips(self, upload_count):
+ """读取测速结果IP列表"""
+ import csv
+ best_ips = []
+ with open("result.csv", 'r', encoding='utf-8') as f:
+ reader = csv.DictReader(f)
+ for row in reader:
+ ip = (row.get('IP 地址') or '').strip()
+ port = (row.get('端口') or '443').strip()
+
+ # 获取速度
+ speed = ''
+ for key in ['下载速度(MB/s)', '下载速度 (MB/s)', '下载速度']:
+ if key in row and row[key]:
+ speed = str(row[key]).strip()
+ break
+
+ # 获取地区
+ region = (row.get('地区码') or 'N/A').strip()
+
+ if ip:
+ try:
+ speed_val = float(speed) if speed else 0
+ best_ips.append({
+ 'ip': ip,
+ 'port': int(port) if port else 443,
+ 'speed': speed_val,
+ 'region': region
+ })
+ except ValueError:
+ continue
+ return best_ips[:upload_count]
+
+ def upload_to_api_thread(self):
+ """上传到 Cloudflare Workers API"""
+ try:
+ url = self.upload_url_field.value.strip()
+ if not url:
+ self.log("❌ 请输入管理页面 URL")
+ return
+
+ # 解析 URL
+ from urllib.parse import urlparse
+ if not url.startswith(('http://', 'https://')):
+ url = 'https://' + url
+
+ parsed = urlparse(url)
+ worker_domain = parsed.netloc
+ path_parts = [p for p in parsed.path.strip('/').split('/') if p]
+
+ if not worker_domain or not path_parts:
+ self.log("❌ URL 格式错误,请检查")
+ return
+
+ uuid = path_parts[-1]
+
+ # 保存配置
+ save_config(worker_domain=worker_domain, uuid=uuid)
+
+ # 读取结果
+ upload_count = int(self.upload_count_field.value or "10")
+ clear_existing = self.clear_existing_checkbox.value
+
+ self.log(f"📤 开始上传优选IP到 {worker_domain}...")
+
+ best_ips = self.read_result_ips(upload_count)
+ if not best_ips:
+ self.log("❌ 未找到有效的测速结果")
+ return
+
+ # 构建 API URL
+ api_url = f"https://{worker_domain}/{uuid}/api/preferred-ips"
+
+ # 如果需要清空
+ if clear_existing:
+ self.log("🗑️ 清空现有数据...")
+ try:
+ import requests
+ resp = requests.delete(api_url, json={"all": True}, timeout=10)
+ if resp.status_code == 200:
+ self.log("✅ 现有数据已清空")
+ else:
+ self.log(f"⚠️ 清空失败: HTTP {resp.status_code}")
+ except Exception as ex:
+ self.log(f"⚠️ 清空失败: {ex}")
+
+ # 批量上传
+ batch_data = []
+ for ip_info in best_ips:
+ name = f"{ip_info['region']}-{ip_info['speed']:.2f}MB/s"
+ batch_data.append({
+ "ip": ip_info['ip'],
+ "port": ip_info['port'],
+ "name": name
+ })
+
+ self.log(f"🚀 上传 {len(batch_data)} 个优选IP...")
+
+ try:
+ import requests
+ resp = requests.post(
+ api_url,
+ json={"ips": batch_data},
+ headers={"Content-Type": "application/json"},
+ timeout=30
+ )
+
+ if resp.status_code == 200:
+ result = resp.json()
+ added = result.get('added', len(batch_data))
+ self.log(f"✅ 上传成功!已添加 {added} 个优选IP")
+ else:
+ self.log(f"❌ 上传失败: HTTP {resp.status_code}")
+ try:
+ error_msg = resp.json().get('error', resp.text)
+ self.log(f" 错误信息: {error_msg}")
+ except:
+ pass
+ except Exception as ex:
+ self.log(f"❌ 上传失败: {ex}")
+
+ except Exception as ex:
+ self.log(f"❌ 上传出错: {ex}")
+
+ def upload_to_github_thread(self):
+ """上传到 GitHub 仓库"""
+ try:
+ github_token = self.github_token_field.value.strip()
+ repo_info = self.github_repo_field.value.strip()
+ file_path = self.github_file_path_field.value.strip() or "cloudflare_ips.txt"
+
+ if not github_token:
+ self.log("❌ 请输入 GitHub Token")
+ return
+ if not repo_info or '/' not in repo_info:
+ self.log("❌ 仓库格式错误,应为 owner/repo")
+ return
+
+ # 保存配置
+ save_config(github_token=github_token, repo_info=repo_info, file_path=file_path)
+
+ upload_count = int(self.upload_count_field.value or "10")
+ best_ips = self.read_result_ips(upload_count)
+
+ if not best_ips:
+ self.log("❌ 未找到有效的测速结果")
+ return
+
+ self.log(f"📤 开始上传优选IP到 GitHub: {repo_info}...")
+
+ # 构建文件内容
+ content_lines = []
+ for ip_info in best_ips:
+ # 格式: IP:端口#地区-速度
+ line = f"{ip_info['ip']}:{ip_info['port']}#{ip_info['region']}-{ip_info['speed']:.2f}MB/s"
+ content_lines.append(line)
+
+ file_content = '\n'.join(content_lines)
+
+ # GitHub API
+ import requests
+ import base64
+
+ api_url = f"https://api.github.com/repos/{repo_info}/contents/{file_path}"
+ headers = {
+ "Authorization": f"token {github_token}",
+ "Accept": "application/vnd.github.v3+json",
+ }
+
+ # 检查文件是否存在(获取 SHA)
+ sha = None
+ try:
+ resp = requests.get(api_url, headers=headers, timeout=10)
+ if resp.status_code == 200:
+ sha = resp.json().get('sha')
+ self.log("📝 文件已存在,将更新内容")
+ except:
+ pass
+
+ # 上传/更新文件
+ data = {
+ "message": f"Update Cloudflare preferred IPs ({len(best_ips)} IPs)",
+ "content": base64.b64encode(file_content.encode()).decode(),
+ }
+ if sha:
+ data["sha"] = sha
+
+ self.log(f"🚀 上传 {len(best_ips)} 个优选IP...")
+
+ resp = requests.put(api_url, headers=headers, json=data, timeout=30)
+
+ if resp.status_code in [200, 201]:
+ self.log(f"✅ 上传成功!文件: {file_path}")
+ self.log(f" 仓库: https://github.com/{repo_info}")
+ else:
+ self.log(f"❌ 上传失败: HTTP {resp.status_code}")
+ try:
+ error_msg = resp.json().get('message', resp.text)
+ self.log(f" 错误信息: {error_msg}")
+ except:
+ pass
+
+ except Exception as ex:
+ self.log(f"❌ 上传出错: {ex}")
+
+ def start_speedtest(self, e):
+ """开始测速"""
+ self.running = True
+ self.start_btn.disabled = True
+ self.stop_btn.disabled = False
+ self.progress_bar.visible = True
+ self.progress_ring.visible = True
+ self.status_text.value = "正在初始化..."
+ self.page.update()
+
+ # 在新线程中运行测速
+ thread = threading.Thread(target=self.run_speedtest_thread)
+ thread.daemon = True
+ thread.start()
+
+ def stop_speedtest(self, e):
+ """停止测速"""
+ self.running = False
+ if self.process:
+ try:
+ self.process.terminate()
+ except:
+ pass
+ self.log("⏹️ 测速已停止")
+ self.reset_ui()
+
+ def reset_ui(self):
+ """重置 UI 状态"""
+ self.start_btn.disabled = False
+ self.stop_btn.disabled = True
+ self.progress_bar.visible = False
+ self.progress_ring.visible = False
+ self.status_text.value = "就绪"
+ self.page.update()
+
+ def run_speedtest_thread(self):
+ """测速线程"""
+ try:
+ # 获取参数
+ ip_version = self.ip_version.value
+ ip_file = CLOUDFLARE_IP_FILE if ip_version == "ipv4" else CLOUDFLARE_IPV6_FILE
+ selected_ports = self.get_selected_ports()
+ speedtest_url = self.get_speedtest_url()
+
+ self.log(f"📋 IP 版本: {ip_version}")
+ self.log(f"📋 测试端口: {', '.join(map(str, selected_ports))}")
+ self.log(f"📋 测速 URL: {speedtest_url}")
+
+ # 准备 IP 文件
+ self.status_text.value = "准备 IP 列表..."
+ self.page.update()
+
+ if ip_version == "ipv6":
+ generate_ipv6_file()
+
+ if not download_cloudflare_ips(ip_version, ip_file):
+ self.log("❌ 准备 IP 列表失败")
+ self.reset_ui()
+ return
+
+ self.log(f"✅ IP 列表已准备: {ip_file}")
+
+ # 下载测速工具
+ self.status_text.value = "下载测速工具..."
+ self.page.update()
+
+ os_type, arch_type = get_system_info()
+ exec_name = download_cloudflare_speedtest(os_type, arch_type)
+ self.log(f"✅ 测速工具已准备: {exec_name}")
+
+ # 生成带端口的 IP 文件
+ actual_ip_file = ip_file
+ tp_ports = None
+
+ if len(selected_ports) > 1 or selected_ports[0] != 443:
+ self.status_text.value = "生成带端口的 IP 文件..."
+ self.page.update()
+
+ generated_file, tp_ports = generate_ip_with_ports(
+ ip_file, selected_ports, "ip_with_ports.txt"
+ )
+ if generated_file:
+ actual_ip_file = generated_file
+ self.log(f"✅ 带端口 IP 文件已生成")
+
+ # 运行测速
+ self.status_text.value = "正在测速..."
+ self.page.update()
+
+ # 构建命令
+ cmd = [get_exec_cmd(exec_name)]
+
+ cmd.extend([
+ "-f", actual_ip_file,
+ "-n", self.thread_count.value,
+ "-dn", self.dn_count.value,
+ "-sl", self.speed_limit.value,
+ "-tl", self.time_limit.value,
+ "-o", "result.csv",
+ ])
+
+ # 如果是 CIDR 格式需要 -tp 参数
+ if tp_ports and len(tp_ports) == 1:
+ cmd.extend(["-tp", str(tp_ports[0])])
+ if tp_ports[0] != 443:
+ cmd.extend(["-url", speedtest_url])
+ elif not tp_ports and len(selected_ports) == 1 and selected_ports[0] != 443:
+ cmd.extend(["-url", speedtest_url])
+
+ self.log(f"🚀 运行命令: {' '.join(cmd)}")
+
+ # 执行测速
+ self.process = subprocess.Popen(
+ cmd,
+ stdout=subprocess.PIPE,
+ stderr=subprocess.STDOUT,
+ text=True,
+ bufsize=1,
+ encoding='utf-8',
+ errors='replace',
+ )
+
+ # 读取输出
+ for line in self.process.stdout:
+ if not self.running:
+ break
+ line = line.strip()
+ if line:
+ self.log(line)
+
+ self.process.wait()
+
+ if self.running:
+ if self.process.returncode == 0:
+ self.log("✅ 测速完成!")
+ self.status_text.value = "测速完成"
+ # 加载结果
+ self.load_results(None)
+ else:
+ self.log(f"❌ 测速失败,返回码: {self.process.returncode}")
+ self.status_text.value = "测速失败"
+
+ except Exception as e:
+ self.log(f"❌ 错误: {str(e)}")
+ self.status_text.value = "发生错误"
+ finally:
+ self.reset_ui()
+
+ def load_results(self, e):
+ """加载测速结果"""
+ result_file = "result.csv"
+ if not os.path.exists(result_file):
+ self.log("⚠️ 未找到结果文件")
+ return
+
+ try:
+ self.result_table.rows.clear()
+
+ with open(result_file, 'r', encoding='utf-8') as f:
+ reader = csv.DictReader(f)
+ count = 0
+ for row in reader:
+ if count >= 20: # 只显示前 20 条
+ break
+
+ ip = row.get('IP 地址', row.get('ip', ''))
+ port = row.get('端口', row.get('port', '443'))
+ latency = row.get('平均延迟', row.get('latency', ''))
+ speed = row.get('下载速度 (MB/s)', row.get('speed', ''))
+ region = row.get('地区码', row.get('colo', ''))
+
+ # 从 IP 中提取端口
+ if ':' in ip and not port:
+ parts = ip.rsplit(':', 1)
+ if len(parts) == 2 and parts[1].isdigit():
+ ip = parts[0]
+ port = parts[1]
+
+ self.result_table.rows.append(
+ ft.DataRow(
+ cells=[
+ ft.DataCell(ft.Text(ip)),
+ ft.DataCell(ft.Text(port)),
+ ft.DataCell(ft.Text(latency)),
+ ft.DataCell(ft.Text(speed)),
+ ft.DataCell(ft.Text(region)),
+ ]
+ )
+ )
+ count += 1
+
+ self.log(f"📊 已加载 {count} 条结果")
+ self.page.update()
+
+ except Exception as e:
+ self.log(f"❌ 加载结果失败: {str(e)}")
+
+
+def main(page: ft.Page):
+ CloudflareSpeedTestGUI(page)
+
+
+if __name__ == "__main__":
+ # 设置环境变量以确保任务栏图标正确显示
+ os.environ["SDL_VIDEO_X11_WMCLASS"] = "yx-tools-gui"
+ ft.app(target=main)
diff --git a/icon/icon.png b/icon/icon.png
new file mode 100644
index 0000000..50e67a4
Binary files /dev/null and b/icon/icon.png differ
diff --git a/pyproject.toml b/pyproject.toml
new file mode 100644
index 0000000..3d706b1
--- /dev/null
+++ b/pyproject.toml
@@ -0,0 +1,33 @@
+[project]
+name = "yx-tools-gui"
+version = "1.0.0"
+description = "Cloudflare IP 优选测速工具"
+authors = [
+ {name = "Joey & Zag"}
+]
+requires-python = ">=3.8"
+dependencies = [
+ "flet>=0.21.0",
+ "requests>=2.28.0",
+]
+
+[tool.flet]
+# 应用配置
+app.module = "cloudflare_speedtest_gui"
+app.name = "yx-tools-gui"
+app.description = "Cloudflare IP 优选测速工具"
+app.version = "1.0.0"
+
+# 产品信息
+product.name = "yx-tools-gui"
+product.org = "com.yxtools"
+product.bundle_id = "com.yxtools.gui"
+
+# 图标
+app.icon = "icon/icon.png"
+
+# 附加数据
+app.include = [
+ "cloudflare_speedtest.py",
+ "icon/",
+]
diff --git a/requirements-gui.txt b/requirements-gui.txt
new file mode 100644
index 0000000..f212de6
--- /dev/null
+++ b/requirements-gui.txt
@@ -0,0 +1,3 @@
+# Cloudflare SpeedTest GUI 依赖
+flet>=0.21.0
+requests>=2.28.0