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架构 + image + image + ## 支持平台 | 平台 | 架构 | 状态 | @@ -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