From 846bcbdccb0ec6623f27d711fca2570ffb4c986c Mon Sep 17 00:00:00 2001 From: ntbowen <59873000+ntbowen@users.noreply.github.com> Date: Fri, 28 Nov 2025 11:32:40 +0800 Subject: [PATCH 01/25] Enhance port selection and speedtest URL handling Added functionality to select supported Cloudflare HTTPS ports and custom speedtest URLs. Updated user input handling to include selected ports and speedtest URL in various modes. --- cloudflare_speedtest.py | 532 ++++++++++++++++++++++++++++++++++------ 1 file changed, 462 insertions(+), 70 deletions(-) diff --git a/cloudflare_speedtest.py b/cloudflare_speedtest.py index e85a09c..cf7407b 100644 --- a/cloudflare_speedtest.py +++ b/cloudflare_speedtest.py @@ -355,6 +355,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 地址列表文件""" @@ -705,6 +713,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 +1101,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 +1125,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 +1340,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 +1456,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 +1478,104 @@ 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}...") + + # 构建测速命令 + if sys.platform == "win32": + cmd = [exec_name] + else: + cmd = [f"./{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 格式 + # 构建测速命令 + if sys.platform == "win32": + cmd = [exec_name] + else: + cmd = [f"./{exec_name}"] - # 询问是否上报结果 - upload_info = upload_results_to_api("result.csv") + cmd.extend([ + "-f", actual_ip_file, + "-n", thread_count, + "-dn", dn_count, + "-sl", speed_limit, + "-tl", time_limit, + "-o", "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 +1589,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 +1746,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 +1773,118 @@ 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}...") + + if sys.platform == "win32": + cmd = [exec_name] + else: + cmd = [f"./{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 格式 + if sys.platform == "win32": + cmd = [exec_name] + else: + cmd = [f"./{exec_name}"] - # 询问是否上报结果 - upload_info = upload_results_to_api("result.csv") + cmd.extend([ + "-f", actual_ip_file, + "-n", thread_count, + "-dn", dn_count, + "-sl", speed_limit, + "-tl", time_limit, + "-o", "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 +1900,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: @@ -2147,6 +2527,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 +2543,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 +3057,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 +3082,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) From c9a21ef2e8950b85d2d399b00ac236764d7f069d Mon Sep 17 00:00:00 2001 From: ntbowen <59873000+ntbowen@users.noreply.github.com> Date: Fri, 28 Nov 2025 17:36:21 +0800 Subject: [PATCH 02/25] resources for GUI resources for GUI --- cloudflare_speedtest_gui.py | 1236 +++++++++++++++++++++++++++++++++++ icon/icon.png | Bin 0 -> 30661 bytes requirements-gui.txt | 2 + 3 files changed, 1238 insertions(+) create mode 100644 cloudflare_speedtest_gui.py create mode 100644 icon/icon.png create mode 100644 requirements-gui.txt diff --git a/cloudflare_speedtest_gui.py b/cloudflare_speedtest_gui.py new file mode 100644 index 0000000..ac57c4d --- /dev/null +++ b/cloudflare_speedtest_gui.py @@ -0,0 +1,1236 @@ +#!/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, + ) +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", 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() + + # 构建命令 + if sys.platform == "win32": + cmd = [exec_name] + else: + cmd = [f"./{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 0000000000000000000000000000000000000000..50e67a499ca68e317e39875e97d2a318b80fe475 GIT binary patch literal 30661 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 AFaQ7m literal 0 HcmV?d00001 diff --git a/requirements-gui.txt b/requirements-gui.txt new file mode 100644 index 0000000..c42b6fd --- /dev/null +++ b/requirements-gui.txt @@ -0,0 +1,2 @@ +# Cloudflare SpeedTest GUI 依赖 +flet>=0.21.0 From 76c2b311eb2d6c1e89754ba1b1276cb8252de093 Mon Sep 17 00:00:00 2001 From: ntbowen <59873000+ntbowen@users.noreply.github.com> Date: Fri, 28 Nov 2025 17:39:36 +0800 Subject: [PATCH 03/25] Add graphical mode instructions to README Added instructions for running the program in graphical mode. --- README.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/README.md b/README.md index 507a5a1..c7452d7 100644 --- a/README.md +++ b/README.md @@ -92,6 +92,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 ``` From 8ab1d1693be2ef11a03bd8acc5dabadf4f9c6979 Mon Sep 17 00:00:00 2001 From: ntbowen <59873000+ntbowen@users.noreply.github.com> Date: Fri, 28 Nov 2025 18:56:46 +0800 Subject: [PATCH 04/25] Update cloudflare_speedtest_gui.py --- cloudflare_speedtest_gui.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cloudflare_speedtest_gui.py b/cloudflare_speedtest_gui.py index ac57c4d..0cb7a1d 100644 --- a/cloudflare_speedtest_gui.py +++ b/cloudflare_speedtest_gui.py @@ -457,7 +457,7 @@ def param_item(label, field): 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", size=10, color=ft.Colors.GREY_500), + ft.Text("Made with ❤️ by Joey & Zag", size=10, color=ft.Colors.GREY_500), ], alignment=ft.MainAxisAlignment.CENTER, spacing=6, From 69da61229a961740ba528a1f8d21394c7f170770 Mon Sep 17 00:00:00 2001 From: ntbowen Date: Fri, 28 Nov 2025 19:07:43 +0800 Subject: [PATCH 05/25] =?UTF-8?q?feat:=20=E6=B7=BB=E5=8A=A0=20GUI=20?= =?UTF-8?q?=E5=9B=BE=E5=BD=A2=E7=95=8C=E9=9D=A2=E5=8F=8A=E5=A4=9A=E5=B9=B3?= =?UTF-8?q?=E5=8F=B0=E6=89=93=E5=8C=85=E5=B7=A5=E4=BD=9C=E6=B5=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 新增 cloudflare_speedtest_gui.py: 基于 Flet 的跨平台 GUI - 新增 requirements-gui.txt: GUI 依赖包 - 新增 icon/icon.png: 应用图标 - 新增 GitHub Actions 工作流: 支持 Linux/Windows/macOS 自动打包 - 支持输出格式: AppImage, DEB, EXE, DMG --- .github/workflows/build-gui.yml | 243 ++++++++++++++++++++++++++++++++ 1 file changed, 243 insertions(+) create mode 100644 .github/workflows/build-gui.yml diff --git a/.github/workflows/build-gui.yml b/.github/workflows/build-gui.yml new file mode 100644 index 0000000..3747854 --- /dev/null +++ b/.github/workflows/build-gui.yml @@ -0,0 +1,243 @@ +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: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version: '3.11' + + - name: Install dependencies + run: | + sudo apt-get update + sudo apt-get install -y libgtk-3-dev libgstreamer1.0-dev libgstreamer-plugins-base1.0-dev + pip install flet pyinstaller + pip install -r requirements-gui.txt + + - name: Build with Flet + run: | + flet pack -n yx-tools-gui \ + -i icon/icon.png \ + --add-data "cloudflare_speedtest.py:." \ + --product-name "yx-tools-gui" \ + cloudflare_speedtest_gui.py + + - name: Create AppImage + run: | + # 下载 appimagetool + wget -q https://github.com/AppImage/AppImageKit/releases/download/continuous/appimagetool-x86_64.AppImage -O appimagetool + chmod +x appimagetool + + # 创建 AppDir 结构 + mkdir -p AppDir/usr/bin + mkdir -p AppDir/usr/share/applications + mkdir -p AppDir/usr/share/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 + + cat > AppDir/yx-tools-gui.desktop << EOF + [Desktop Entry] + Name=yx-tools-gui + Name[zh_CN]=优选IP测速工具 + Comment=Cloudflare 优选 IP 测速工具 + Exec=yx-tools-gui + Icon=yx-tools-gui + Terminal=false + Type=Application + Categories=Network;Utility; + StartupWMClass=flet + EOF + + cp AppDir/yx-tools-gui.desktop AppDir/usr/share/applications/ + + cat > AppDir/AppRun << 'EOF' + #!/bin/bash + SELF=$(readlink -f "$0") + HERE=${SELF%/*} + exec "${HERE}/usr/bin/yx-tools-gui" "$@" + EOF + chmod +x AppDir/AppRun + + # 构建 AppImage + ARCH=x86_64 ./appimagetool AppDir dist/yx-tools-gui-x86_64.AppImage + + - name: Create DEB package + run: | + mkdir -p deb-pkg/DEBIAN + mkdir -p deb-pkg/opt/yx-tools-gui + mkdir -p deb-pkg/usr/bin + mkdir -p deb-pkg/usr/share/applications + mkdir -p deb-pkg/usr/share/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 + + cat > deb-pkg/usr/bin/yx-tools-gui << 'EOF' + #!/bin/bash + exec /opt/yx-tools-gui/yx-tools-gui "$@" + EOF + chmod +x deb-pkg/usr/bin/yx-tools-gui + + cat > deb-pkg/usr/share/applications/yx-tools-gui.desktop << EOF + [Desktop Entry] + Name=yx-tools-gui + Name[zh_CN]=优选IP测速工具 + Comment=Cloudflare 优选 IP 测速工具 + Exec=/usr/bin/yx-tools-gui + Icon=yx-tools-gui + Terminal=false + Type=Application + Categories=Network;Utility; + StartupWMClass=flet + EOF + + cat > deb-pkg/DEBIAN/control << EOF + Package: yx-tools-gui + Version: ${{ github.event.inputs.version || '1.0.0' }} + Section: net + Priority: optional + Architecture: amd64 + Maintainer: Joey & Zag + Description: Cloudflare 优选 IP 测速工具 + 基于 Flet 的跨平台图形界面工具 + EOF + + dpkg-deb --build deb-pkg dist/yx-tools-gui_${{ github.event.inputs.version || '1.0.0' }}_amd64.deb + + - name: Upload Linux artifacts + uses: actions/upload-artifact@v4 + with: + name: linux-packages + path: | + dist/yx-tools-gui + dist/yx-tools-gui-x86_64.AppImage + dist/yx-tools-gui_*.deb + + build-windows: + runs-on: windows-latest + steps: + - uses: actions/checkout@v4 + + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version: '3.11' + + - name: Install dependencies + run: | + pip install flet pyinstaller + pip install -r requirements-gui.txt + + - name: Build with Flet + run: | + flet pack -n yx-tools-gui ` + -i icon/icon.png ` + --add-data "cloudflare_speedtest.py;." ` + --product-name "yx-tools-gui" ` + --file-description "Cloudflare 优选 IP 测速工具" ` + --product-version "${{ github.event.inputs.version || '1.0.0' }}" ` + --company-name "Joey & Zag" ` + cloudflare_speedtest_gui.py + + - name: Upload Windows artifact + uses: actions/upload-artifact@v4 + with: + name: windows-exe + path: dist/yx-tools-gui.exe + + build-macos: + runs-on: macos-latest + steps: + - uses: actions/checkout@v4 + + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version: '3.11' + + - name: Install dependencies + run: | + pip install flet pyinstaller + pip install -r requirements-gui.txt + + - name: Build with Flet + run: | + flet pack -n yx-tools-gui \ + -i icon/icon.png \ + --add-data "cloudflare_speedtest.py:." \ + --product-name "yx-tools-gui" \ + --bundle-id "com.yxtools.gui" \ + cloudflare_speedtest_gui.py + + - name: Create DMG + run: | + # 安装 create-dmg + brew install create-dmg + + # 创建 DMG + create-dmg \ + --volname "yx-tools-gui" \ + --volicon "icon/icon.png" \ + --window-pos 200 120 \ + --window-size 600 400 \ + --icon-size 100 \ + --icon "yx-tools-gui.app" 150 185 \ + --app-drop-link 450 185 \ + "dist/yx-tools-gui-${{ github.event.inputs.version || '1.0.0' }}.dmg" \ + "dist/yx-tools-gui.app" || true + + # 如果 create-dmg 失败,使用 hdiutil + if [ ! -f "dist/yx-tools-gui-${{ github.event.inputs.version || '1.0.0' }}.dmg" ]; then + hdiutil create -volname "yx-tools-gui" -srcfolder dist/yx-tools-gui.app -ov -format UDZO "dist/yx-tools-gui-${{ github.event.inputs.version || '1.0.0' }}.dmg" + fi + + - name: Upload macOS artifacts + uses: actions/upload-artifact@v4 + with: + name: macos-dmg + path: | + dist/yx-tools-gui.app + dist/yx-tools-gui-*.dmg + + release: + needs: [build-linux, build-windows, build-macos] + runs-on: ubuntu-latest + if: startsWith(github.ref, 'refs/tags/') + steps: + - name: Download all artifacts + uses: actions/download-artifact@v4 + with: + path: artifacts + + - name: Display structure + run: ls -R artifacts + + - name: Create Release + uses: softprops/action-gh-release@v1 + with: + files: | + artifacts/linux-packages/* + artifacts/windows-exe/* + artifacts/macos-dmg/* + draft: false + prerelease: false + generate_release_notes: true + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} From 5fdadfea2c5a0f394807a792fd78a3b611727d8f Mon Sep 17 00:00:00 2001 From: ntbowen Date: Fri, 28 Nov 2025 19:31:10 +0800 Subject: [PATCH 06/25] =?UTF-8?q?fix:=20=E4=BF=AE=E5=A4=8D=20GUI=20?= =?UTF-8?q?=E6=89=93=E5=8C=85=E5=B7=A5=E4=BD=9C=E6=B5=81=E4=BE=9D=E8=B5=96?= =?UTF-8?q?=E9=97=AE=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 添加 libfuse2 用于 AppImage 构建 - 添加 pillow 用于图标格式转换 (PNG -> ICO/ICNS) --- .github/workflows/build-gui.yml | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/.github/workflows/build-gui.yml b/.github/workflows/build-gui.yml index 3747854..6d65846 100644 --- a/.github/workflows/build-gui.yml +++ b/.github/workflows/build-gui.yml @@ -25,8 +25,8 @@ jobs: - name: Install dependencies run: | sudo apt-get update - sudo apt-get install -y libgtk-3-dev libgstreamer1.0-dev libgstreamer-plugins-base1.0-dev - pip install flet pyinstaller + sudo apt-get install -y libgtk-3-dev libgstreamer1.0-dev libgstreamer-plugins-base1.0-dev libfuse2 + pip install flet pyinstaller pillow pip install -r requirements-gui.txt - name: Build with Flet @@ -142,7 +142,7 @@ jobs: - name: Install dependencies run: | - pip install flet pyinstaller + pip install flet pyinstaller pillow pip install -r requirements-gui.txt - name: Build with Flet @@ -151,9 +151,9 @@ jobs: -i icon/icon.png ` --add-data "cloudflare_speedtest.py;." ` --product-name "yx-tools-gui" ` - --file-description "Cloudflare 优选 IP 测速工具" ` + --file-description "Cloudflare IP Speed Test Tool" ` --product-version "${{ github.event.inputs.version || '1.0.0' }}" ` - --company-name "Joey & Zag" ` + --company-name "Joey and Zag" ` cloudflare_speedtest_gui.py - name: Upload Windows artifact @@ -174,7 +174,7 @@ jobs: - name: Install dependencies run: | - pip install flet pyinstaller + pip install flet pyinstaller pillow pip install -r requirements-gui.txt - name: Build with Flet From cb9d614e07f445f5de9f88e9ead06786e3c0bb82 Mon Sep 17 00:00:00 2001 From: ntbowen Date: Fri, 28 Nov 2025 19:48:21 +0800 Subject: [PATCH 07/25] =?UTF-8?q?feat:=20=E5=A4=9A=E6=9E=B6=E6=9E=84?= =?UTF-8?q?=E6=89=93=E5=8C=85=E5=B7=A5=E4=BD=9C=E6=B5=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 支持以下平台和格式: - RPM: x86_64, aarch64 - DEB: amd64, arm64 - AppImage: x86_64, aarch64 - DMG: Intel, Apple Silicon - EXE: x64, arm64 所有产物不压缩,直接输出原始文件 --- .github/workflows/build-gui.yml | 371 ++++++++++++++++++++------------ 1 file changed, 231 insertions(+), 140 deletions(-) diff --git a/.github/workflows/build-gui.yml b/.github/workflows/build-gui.yml index 6d65846..f9cdd25 100644 --- a/.github/workflows/build-gui.yml +++ b/.github/workflows/build-gui.yml @@ -12,7 +12,7 @@ on: default: '1.0.0' jobs: - build-linux: + build-linux-x64: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 @@ -25,219 +25,310 @@ jobs: - name: Install dependencies run: | sudo apt-get update - sudo apt-get install -y libgtk-3-dev libgstreamer1.0-dev libgstreamer-plugins-base1.0-dev libfuse2 + sudo apt-get install -y libgtk-3-dev libgstreamer1.0-dev libgstreamer-plugins-base1.0-dev libfuse2 rpm pip install flet pyinstaller pillow pip install -r requirements-gui.txt - name: Build with Flet run: | - flet pack -n yx-tools-gui \ - -i icon/icon.png \ - --add-data "cloudflare_speedtest.py:." \ - --product-name "yx-tools-gui" \ - cloudflare_speedtest_gui.py + flet pack -n yx-tools-gui -i icon/icon.png --add-data "cloudflare_speedtest.py:." --product-name "yx-tools-gui" cloudflare_speedtest_gui.py - - name: Create AppImage + - name: Create packages run: | - # 下载 appimagetool + 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 - - # 创建 AppDir 结构 - mkdir -p AppDir/usr/bin - mkdir -p AppDir/usr/share/applications - mkdir -p AppDir/usr/share/icons/hicolor/256x256/apps - + 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 - - cat > AppDir/yx-tools-gui.desktop << EOF + cat > AppDir/yx-tools-gui.desktop << 'EOF' [Desktop Entry] Name=yx-tools-gui - Name[zh_CN]=优选IP测速工具 - Comment=Cloudflare 优选 IP 测速工具 + Comment=Cloudflare IP Speed Test Tool Exec=yx-tools-gui Icon=yx-tools-gui Terminal=false Type=Application - Categories=Network;Utility; + Categories=Network; StartupWMClass=flet EOF - cp AppDir/yx-tools-gui.desktop AppDir/usr/share/applications/ - - cat > AppDir/AppRun << 'EOF' - #!/bin/bash - SELF=$(readlink -f "$0") - HERE=${SELF%/*} - exec "${HERE}/usr/bin/yx-tools-gui" "$@" - EOF + 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 - # 构建 AppImage - ARCH=x86_64 ./appimagetool AppDir dist/yx-tools-gui-x86_64.AppImage - - - name: Create DEB package - run: | - mkdir -p deb-pkg/DEBIAN - mkdir -p deb-pkg/opt/yx-tools-gui - mkdir -p deb-pkg/usr/bin - mkdir -p deb-pkg/usr/share/applications - mkdir -p deb-pkg/usr/share/icons/hicolor/256x256/apps - + # 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 - - cat > deb-pkg/usr/bin/yx-tools-gui << 'EOF' - #!/bin/bash - exec /opt/yx-tools-gui/yx-tools-gui "$@" - EOF + 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|' AppDir/yx-tools-gui.desktop > deb-pkg/usr/share/applications/yx-tools-gui.desktop + cat > deb-pkg/DEBIAN/control << EOF + Package: yx-tools-gui + Version: ${VERSION} + Architecture: amd64 + Maintainer: Joey and Zag + Description: Cloudflare IP Speed Test Tool + EOF + dpkg-deb --build deb-pkg yx-tools-gui_${VERSION}_amd64.deb - cat > deb-pkg/usr/share/applications/yx-tools-gui.desktop << EOF + # RPM + mkdir -p rpmbuild/{BUILD,RPMS,SPECS} + cat > rpmbuild/SPECS/yx-tools-gui.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 ${GITHUB_WORKSPACE}/dist/yx-tools-gui %{buildroot}/opt/yx-tools-gui/ + chmod +x %{buildroot}/opt/yx-tools-gui/yx-tools-gui + cp ${GITHUB_WORKSPACE}/icon/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 ${GITHUB_WORKSPACE}/AppDir/usr/share/applications/yx-tools-gui.desktop %{buildroot}/usr/share/applications/ + sed -i 's|Exec=yx-tools-gui|Exec=/usr/bin/yx-tools-gui|' %{buildroot}/usr/share/applications/yx-tools-gui.desktop + %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 + EOF + 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: rpm-x86_64 + path: yx-tools-gui-*.x86_64.rpm + compression-level: 0 + + - uses: actions/upload-artifact@v4 + with: + name: deb-amd64 + path: yx-tools-gui_*_amd64.deb + compression-level: 0 + + - uses: actions/upload-artifact@v4 + with: + name: appimage-x86_64 + path: 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 libgtk-3-dev libgstreamer1.0-dev libgstreamer-plugins-base1.0-dev libfuse2 rpm + pip install flet pyinstaller pillow + pip install -r requirements-gui.txt + - name: Build with Flet + run: flet pack -n yx-tools-gui -i icon/icon.png --add-data "cloudflare_speedtest.py:." --product-name "yx-tools-gui" cloudflare_speedtest_gui.py + - name: Create packages + run: | + VERSION=${{ github.event.inputs.version || '1.0.0' }} + wget -q https://github.com/AppImage/AppImageKit/releases/download/continuous/appimagetool-aarch64.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 + cat > AppDir/yx-tools-gui.desktop << 'EOF' [Desktop Entry] Name=yx-tools-gui - Name[zh_CN]=优选IP测速工具 - Comment=Cloudflare 优选 IP 测速工具 - Exec=/usr/bin/yx-tools-gui + Comment=Cloudflare IP Speed Test Tool + Exec=yx-tools-gui Icon=yx-tools-gui Terminal=false Type=Application - Categories=Network;Utility; + Categories=Network; StartupWMClass=flet EOF - + cp AppDir/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=aarch64 ./appimagetool AppDir yx-tools-gui-aarch64.AppImage + 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|' AppDir/yx-tools-gui.desktop > deb-pkg/usr/share/applications/yx-tools-gui.desktop cat > deb-pkg/DEBIAN/control << EOF Package: yx-tools-gui - Version: ${{ github.event.inputs.version || '1.0.0' }} - Section: net - Priority: optional - Architecture: amd64 - Maintainer: Joey & Zag - Description: Cloudflare 优选 IP 测速工具 - 基于 Flet 的跨平台图形界面工具 + Version: ${VERSION} + Architecture: arm64 + Maintainer: Joey and Zag + Description: Cloudflare IP Speed Test Tool EOF - - dpkg-deb --build deb-pkg dist/yx-tools-gui_${{ github.event.inputs.version || '1.0.0' }}_amd64.deb - - - name: Upload Linux artifacts - uses: actions/upload-artifact@v4 - with: - name: linux-packages - path: | - dist/yx-tools-gui - dist/yx-tools-gui-x86_64.AppImage - dist/yx-tools-gui_*.deb + dpkg-deb --build deb-pkg yx-tools-gui_${VERSION}_arm64.deb + mkdir -p rpmbuild/{BUILD,RPMS,SPECS} + cat > rpmbuild/SPECS/yx-tools-gui.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 ${GITHUB_WORKSPACE}/dist/yx-tools-gui %{buildroot}/opt/yx-tools-gui/ + chmod +x %{buildroot}/opt/yx-tools-gui/yx-tools-gui + cp ${GITHUB_WORKSPACE}/icon/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 ${GITHUB_WORKSPACE}/AppDir/usr/share/applications/yx-tools-gui.desktop %{buildroot}/usr/share/applications/ + sed -i 's|Exec=yx-tools-gui|Exec=/usr/bin/yx-tools-gui|' %{buildroot}/usr/share/applications/yx-tools-gui.desktop + %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 + EOF + 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: rpm-aarch64 + path: yx-tools-gui-*.aarch64.rpm + compression-level: 0 + - uses: actions/upload-artifact@v4 + with: + name: deb-arm64 + path: yx-tools-gui_*_arm64.deb + compression-level: 0 + - uses: actions/upload-artifact@v4 + with: + name: appimage-aarch64 + path: yx-tools-gui-aarch64.AppImage + compression-level: 0 - build-windows: + build-windows-x64: runs-on: windows-latest steps: - uses: actions/checkout@v4 - - - name: Set up Python - uses: actions/setup-python@v5 + - 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 run: | - flet pack -n yx-tools-gui ` - -i icon/icon.png ` - --add-data "cloudflare_speedtest.py;." ` - --product-name "yx-tools-gui" ` - --file-description "Cloudflare IP Speed Test Tool" ` - --product-version "${{ github.event.inputs.version || '1.0.0' }}" ` - --company-name "Joey and Zag" ` - cloudflare_speedtest_gui.py - - - name: Upload Windows artifact - uses: actions/upload-artifact@v4 + flet pack -n yx-tools-gui -i icon/icon.png --add-data "cloudflare_speedtest.py;." --product-name "yx-tools-gui" cloudflare_speedtest_gui.py + Rename-Item -Path "dist/yx-tools-gui.exe" -NewName "yx-tools-gui-x64.exe" + - uses: actions/upload-artifact@v4 with: - name: windows-exe - path: dist/yx-tools-gui.exe + name: exe-x64 + path: dist/yx-tools-gui-x64.exe + compression-level: 0 - build-macos: - runs-on: macos-latest + build-windows-arm64: + runs-on: windows-11-arm steps: - uses: actions/checkout@v4 - - - name: Set up Python - uses: actions/setup-python@v5 + - 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 run: | - flet pack -n yx-tools-gui \ - -i icon/icon.png \ - --add-data "cloudflare_speedtest.py:." \ - --product-name "yx-tools-gui" \ - --bundle-id "com.yxtools.gui" \ - cloudflare_speedtest_gui.py - + flet pack -n yx-tools-gui -i icon/icon.png --add-data "cloudflare_speedtest.py;." --product-name "yx-tools-gui" cloudflare_speedtest_gui.py + Rename-Item -Path "dist/yx-tools-gui.exe" -NewName "yx-tools-gui-arm64.exe" + - uses: actions/upload-artifact@v4 + with: + name: exe-arm64 + path: dist/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 dependencies + run: | + pip install flet pyinstaller pillow + pip install -r requirements-gui.txt + - name: Build with Flet + run: flet pack -n yx-tools-gui -i icon/icon.png --add-data "cloudflare_speedtest.py:." --product-name "yx-tools-gui" --bundle-id "com.yxtools.gui" cloudflare_speedtest_gui.py - name: Create DMG run: | - # 安装 create-dmg + VERSION=${{ github.event.inputs.version || '1.0.0' }} brew install create-dmg - - # 创建 DMG - create-dmg \ - --volname "yx-tools-gui" \ - --volicon "icon/icon.png" \ - --window-pos 200 120 \ - --window-size 600 400 \ - --icon-size 100 \ - --icon "yx-tools-gui.app" 150 185 \ - --app-drop-link 450 185 \ - "dist/yx-tools-gui-${{ github.event.inputs.version || '1.0.0' }}.dmg" \ - "dist/yx-tools-gui.app" || true - - # 如果 create-dmg 失败,使用 hdiutil - if [ ! -f "dist/yx-tools-gui-${{ github.event.inputs.version || '1.0.0' }}.dmg" ]; then - hdiutil create -volname "yx-tools-gui" -srcfolder dist/yx-tools-gui.app -ov -format UDZO "dist/yx-tools-gui-${{ github.event.inputs.version || '1.0.0' }}.dmg" - fi - - - name: Upload macOS artifacts - uses: actions/upload-artifact@v4 + create-dmg --volname "yx-tools-gui" --window-size 600 400 --icon-size 100 --icon "yx-tools-gui.app" 150 185 --app-drop-link 450 185 "yx-tools-gui-${VERSION}-intel.dmg" "dist/yx-tools-gui.app" || hdiutil create -volname "yx-tools-gui" -srcfolder dist/yx-tools-gui.app -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: - name: macos-dmg - path: | - dist/yx-tools-gui.app - dist/yx-tools-gui-*.dmg + python-version: '3.11' + - name: Install dependencies + run: | + pip install flet pyinstaller pillow + pip install -r requirements-gui.txt + - name: Build with Flet + run: flet pack -n yx-tools-gui -i icon/icon.png --add-data "cloudflare_speedtest.py:." --product-name "yx-tools-gui" --bundle-id "com.yxtools.gui" cloudflare_speedtest_gui.py + - name: Create DMG + run: | + VERSION=${{ github.event.inputs.version || '1.0.0' }} + brew install create-dmg + create-dmg --volname "yx-tools-gui" --window-size 600 400 --icon-size 100 --icon "yx-tools-gui.app" 150 185 --app-drop-link 450 185 "yx-tools-gui-${VERSION}-apple-silicon.dmg" "dist/yx-tools-gui.app" || hdiutil create -volname "yx-tools-gui" -srcfolder dist/yx-tools-gui.app -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, build-windows, build-macos] + 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: - - name: Download all artifacts - uses: actions/download-artifact@v4 + - uses: actions/download-artifact@v4 with: path: artifacts - - - name: Display structure - run: ls -R artifacts - - - name: Create Release - uses: softprops/action-gh-release@v1 + merge-multiple: true + - run: ls -la artifacts/ + - uses: softprops/action-gh-release@v1 with: - files: | - artifacts/linux-packages/* - artifacts/windows-exe/* - artifacts/macos-dmg/* + files: artifacts/* draft: false - prerelease: false generate_release_notes: true env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} From 95da3dad08dbbea2519e0c098d4ddbcb1cd504d9 Mon Sep 17 00:00:00 2001 From: ntbowen Date: Fri, 28 Nov 2025 19:56:28 +0800 Subject: [PATCH 08/25] =?UTF-8?q?fix:=20=E4=BF=AE=E5=A4=8D=20ARM64=20AppIm?= =?UTF-8?q?age=20=E6=9E=84=E5=BB=BA=E9=97=AE=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 使用 --appimage-extract 解压 appimagetool 后直接运行,避免 FUSE 和多架构检测问题 --- .github/workflows/build-gui.yml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.github/workflows/build-gui.yml b/.github/workflows/build-gui.yml index f9cdd25..1bca1c2 100644 --- a/.github/workflows/build-gui.yml +++ b/.github/workflows/build-gui.yml @@ -146,6 +146,7 @@ jobs: VERSION=${{ github.event.inputs.version || '1.0.0' }} wget -q https://github.com/AppImage/AppImageKit/releases/download/continuous/appimagetool-aarch64.AppImage -O appimagetool chmod +x appimagetool + ./appimagetool --appimage-extract 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 @@ -166,7 +167,7 @@ jobs: echo 'HERE=$(dirname "$(readlink -f "$0")")' >> AppDir/AppRun echo 'exec "${HERE}/usr/bin/yx-tools-gui" "$@"' >> AppDir/AppRun chmod +x AppDir/AppRun - ARCH=aarch64 ./appimagetool AppDir yx-tools-gui-aarch64.AppImage + ARCH=aarch64 ./squashfs-root/AppRun AppDir yx-tools-gui-aarch64.AppImage 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 From a73eb17fa5793689f79dcedb895708e46cf9185e Mon Sep 17 00:00:00 2001 From: ntbowen Date: Fri, 28 Nov 2025 20:03:53 +0800 Subject: [PATCH 09/25] =?UTF-8?q?fix:=20=E4=BF=AE=E5=A4=8D=20macOS=20Dock?= =?UTF-8?q?=20=E5=9B=BE=E6=A0=87=E6=98=BE=E7=A4=BA=E9=97=AE=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 使用 sips + iconutil 生成正确的 .icns 图标 - 将图标复制到 .app bundle 的 Resources 目录 - 确保 Dock 和程序窗口显示一致的图标 --- .github/workflows/build-gui.yml | 40 +++++++++++++++++++++++++++++++-- 1 file changed, 38 insertions(+), 2 deletions(-) diff --git a/.github/workflows/build-gui.yml b/.github/workflows/build-gui.yml index 1bca1c2..3d18f34 100644 --- a/.github/workflows/build-gui.yml +++ b/.github/workflows/build-gui.yml @@ -279,8 +279,26 @@ jobs: run: | pip install flet pyinstaller pillow pip install -r requirements-gui.txt + - name: Create ICNS icon + run: | + mkdir -p icon.iconset + sips -z 16 16 icon/icon.png --out icon.iconset/icon_16x16.png + sips -z 32 32 icon/icon.png --out icon.iconset/icon_16x16@2x.png + sips -z 32 32 icon/icon.png --out icon.iconset/icon_32x32.png + sips -z 64 64 icon/icon.png --out icon.iconset/icon_32x32@2x.png + sips -z 128 128 icon/icon.png --out icon.iconset/icon_128x128.png + sips -z 256 256 icon/icon.png --out icon.iconset/icon_128x128@2x.png + sips -z 256 256 icon/icon.png --out icon.iconset/icon_256x256.png + sips -z 512 512 icon/icon.png --out icon.iconset/icon_256x256@2x.png + sips -z 512 512 icon/icon.png --out icon.iconset/icon_512x512.png + cp icon/icon.png icon.iconset/icon_512x512@2x.png + iconutil -c icns icon.iconset -o icon/icon.icns - name: Build with Flet - run: flet pack -n yx-tools-gui -i icon/icon.png --add-data "cloudflare_speedtest.py:." --product-name "yx-tools-gui" --bundle-id "com.yxtools.gui" cloudflare_speedtest_gui.py + run: | + flet pack -n yx-tools-gui -i icon/icon.icns --add-data "cloudflare_speedtest.py:." --product-name "yx-tools-gui" --bundle-id "com.yxtools.gui" cloudflare_speedtest_gui.py + # 确保 .app 中的图标正确 + cp icon/icon.icns dist/yx-tools-gui.app/Contents/Resources/icon-windowed.icns 2>/dev/null || true + cp icon/icon.icns dist/yx-tools-gui.app/Contents/Resources/AppIcon.icns 2>/dev/null || true - name: Create DMG run: | VERSION=${{ github.event.inputs.version || '1.0.0' }} @@ -303,8 +321,26 @@ jobs: run: | pip install flet pyinstaller pillow pip install -r requirements-gui.txt + - name: Create ICNS icon + run: | + mkdir -p icon.iconset + sips -z 16 16 icon/icon.png --out icon.iconset/icon_16x16.png + sips -z 32 32 icon/icon.png --out icon.iconset/icon_16x16@2x.png + sips -z 32 32 icon/icon.png --out icon.iconset/icon_32x32.png + sips -z 64 64 icon/icon.png --out icon.iconset/icon_32x32@2x.png + sips -z 128 128 icon/icon.png --out icon.iconset/icon_128x128.png + sips -z 256 256 icon/icon.png --out icon.iconset/icon_128x128@2x.png + sips -z 256 256 icon/icon.png --out icon.iconset/icon_256x256.png + sips -z 512 512 icon/icon.png --out icon.iconset/icon_256x256@2x.png + sips -z 512 512 icon/icon.png --out icon.iconset/icon_512x512.png + cp icon/icon.png icon.iconset/icon_512x512@2x.png + iconutil -c icns icon.iconset -o icon/icon.icns - name: Build with Flet - run: flet pack -n yx-tools-gui -i icon/icon.png --add-data "cloudflare_speedtest.py:." --product-name "yx-tools-gui" --bundle-id "com.yxtools.gui" cloudflare_speedtest_gui.py + run: | + flet pack -n yx-tools-gui -i icon/icon.icns --add-data "cloudflare_speedtest.py:." --product-name "yx-tools-gui" --bundle-id "com.yxtools.gui" cloudflare_speedtest_gui.py + # 确保 .app 中的图标正确 + cp icon/icon.icns dist/yx-tools-gui.app/Contents/Resources/icon-windowed.icns 2>/dev/null || true + cp icon/icon.icns dist/yx-tools-gui.app/Contents/Resources/AppIcon.icns 2>/dev/null || true - name: Create DMG run: | VERSION=${{ github.event.inputs.version || '1.0.0' }} From a9f92c53fc5594d80df8ebf9a08e5c4b4c6c61cd Mon Sep 17 00:00:00 2001 From: ntbowen Date: Fri, 28 Nov 2025 20:15:32 +0800 Subject: [PATCH 10/25] =?UTF-8?q?fix:=20=E4=BF=AE=E5=A4=8D=20ARM64=20AppIm?= =?UTF-8?q?age=20=E5=92=8C=20macOS=20=E5=9B=BE=E6=A0=87/=E8=8F=9C=E5=8D=95?= =?UTF-8?q?=E9=97=AE=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - ARM64: 使用 export ARCH 和 --no-appstream 修复多架构检测 - macOS: 修改 Info.plist 设置正确的 CFBundleName 和 CFBundleDisplayName - macOS: 替换所有 .icns 图标文件确保 Dock 显示正确 --- .github/workflows/build-gui.yml | 28 +++++++++++++++++++++------- 1 file changed, 21 insertions(+), 7 deletions(-) diff --git a/.github/workflows/build-gui.yml b/.github/workflows/build-gui.yml index 3d18f34..81c9362 100644 --- a/.github/workflows/build-gui.yml +++ b/.github/workflows/build-gui.yml @@ -167,7 +167,7 @@ jobs: echo 'HERE=$(dirname "$(readlink -f "$0")")' >> AppDir/AppRun echo 'exec "${HERE}/usr/bin/yx-tools-gui" "$@"' >> AppDir/AppRun chmod +x AppDir/AppRun - ARCH=aarch64 ./squashfs-root/AppRun AppDir yx-tools-gui-aarch64.AppImage + export ARCH=aarch64 && ./squashfs-root/AppRun --no-appstream AppDir yx-tools-gui-aarch64.AppImage 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 @@ -296,9 +296,16 @@ jobs: - name: Build with Flet run: | flet pack -n yx-tools-gui -i icon/icon.icns --add-data "cloudflare_speedtest.py:." --product-name "yx-tools-gui" --bundle-id "com.yxtools.gui" cloudflare_speedtest_gui.py - # 确保 .app 中的图标正确 - cp icon/icon.icns dist/yx-tools-gui.app/Contents/Resources/icon-windowed.icns 2>/dev/null || true - cp icon/icon.icns dist/yx-tools-gui.app/Contents/Resources/AppIcon.icns 2>/dev/null || true + - name: Fix macOS app bundle + run: | + APP_PATH="dist/yx-tools-gui.app" + # 替换所有图标文件 + find "$APP_PATH/Contents/Resources" -name "*.icns" -exec cp icon/icon.icns {} \; + # 修改 Info.plist 确保应用名称正确 + /usr/libexec/PlistBuddy -c "Set :CFBundleName yx-tools-gui" "$APP_PATH/Contents/Info.plist" + /usr/libexec/PlistBuddy -c "Set :CFBundleDisplayName yx-tools-gui" "$APP_PATH/Contents/Info.plist" 2>/dev/null || /usr/libexec/PlistBuddy -c "Add :CFBundleDisplayName string yx-tools-gui" "$APP_PATH/Contents/Info.plist" + # 清除图标缓存 + touch "$APP_PATH" - name: Create DMG run: | VERSION=${{ github.event.inputs.version || '1.0.0' }} @@ -338,9 +345,16 @@ jobs: - name: Build with Flet run: | flet pack -n yx-tools-gui -i icon/icon.icns --add-data "cloudflare_speedtest.py:." --product-name "yx-tools-gui" --bundle-id "com.yxtools.gui" cloudflare_speedtest_gui.py - # 确保 .app 中的图标正确 - cp icon/icon.icns dist/yx-tools-gui.app/Contents/Resources/icon-windowed.icns 2>/dev/null || true - cp icon/icon.icns dist/yx-tools-gui.app/Contents/Resources/AppIcon.icns 2>/dev/null || true + - name: Fix macOS app bundle + run: | + APP_PATH="dist/yx-tools-gui.app" + # 替换所有图标文件 + find "$APP_PATH/Contents/Resources" -name "*.icns" -exec cp icon/icon.icns {} \; + # 修改 Info.plist 确保应用名称正确 + /usr/libexec/PlistBuddy -c "Set :CFBundleName yx-tools-gui" "$APP_PATH/Contents/Info.plist" + /usr/libexec/PlistBuddy -c "Set :CFBundleDisplayName yx-tools-gui" "$APP_PATH/Contents/Info.plist" 2>/dev/null || /usr/libexec/PlistBuddy -c "Add :CFBundleDisplayName string yx-tools-gui" "$APP_PATH/Contents/Info.plist" + # 清除图标缓存 + touch "$APP_PATH" - name: Create DMG run: | VERSION=${{ github.event.inputs.version || '1.0.0' }} From 0c01a404cad3dc1ad17bda7227d6b8dd0f4a137a Mon Sep 17 00:00:00 2001 From: ntbowen Date: Fri, 28 Nov 2025 20:26:13 +0800 Subject: [PATCH 11/25] =?UTF-8?q?fix:=20=E4=BD=BF=E7=94=A8=20linuxdeploy?= =?UTF-8?q?=20=E6=9B=BF=E4=BB=A3=20appimagetool=20=E8=A7=A3=E5=86=B3=20ARM?= =?UTF-8?q?64=20=E5=A4=9A=E6=9E=B6=E6=9E=84=E9=97=AE=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - ARM64 使用 linuxdeploy 构建 AppImage - 修复 desktop 文件路径引用 --- .github/workflows/build-gui.yml | 31 +++++++++++++++---------------- 1 file changed, 15 insertions(+), 16 deletions(-) diff --git a/.github/workflows/build-gui.yml b/.github/workflows/build-gui.yml index 81c9362..5636a3f 100644 --- a/.github/workflows/build-gui.yml +++ b/.github/workflows/build-gui.yml @@ -144,14 +144,8 @@ jobs: - name: Create packages run: | VERSION=${{ github.event.inputs.version || '1.0.0' }} - wget -q https://github.com/AppImage/AppImageKit/releases/download/continuous/appimagetool-aarch64.AppImage -O appimagetool - chmod +x appimagetool - ./appimagetool --appimage-extract - 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 - cat > AppDir/yx-tools-gui.desktop << 'EOF' + # 创建 desktop 文件 + cat > yx-tools-gui.desktop << 'EOF' [Desktop Entry] Name=yx-tools-gui Comment=Cloudflare IP Speed Test Tool @@ -162,19 +156,24 @@ jobs: Categories=Network; StartupWMClass=flet EOF - cp AppDir/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 - export ARCH=aarch64 && ./squashfs-root/AppRun --no-appstream AppDir yx-tools-gui-aarch64.AppImage + + # AppImage - 使用 linuxdeploy 替代 appimagetool + wget -q https://github.com/linuxdeploy/linuxdeploy/releases/download/continuous/linuxdeploy-aarch64.AppImage -O linuxdeploy + chmod +x linuxdeploy + ./linuxdeploy --appimage-extract + mkdir -p AppDir/usr/bin + cp dist/yx-tools-gui AppDir/usr/bin/ + cp icon/icon.png yx-tools-gui.png + ./squashfs-root/AppRun --appdir AppDir -e dist/yx-tools-gui -i yx-tools-gui.png -d yx-tools-gui.desktop --output appimage || echo "AppImage build skipped" + mv *.AppImage yx-tools-gui-aarch64.AppImage 2>/dev/null || touch yx-tools-gui-aarch64.AppImage.skip + 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|' AppDir/yx-tools-gui.desktop > deb-pkg/usr/share/applications/yx-tools-gui.desktop + 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 cat > deb-pkg/DEBIAN/control << EOF Package: yx-tools-gui Version: ${VERSION} @@ -200,7 +199,7 @@ jobs: 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 ${GITHUB_WORKSPACE}/AppDir/usr/share/applications/yx-tools-gui.desktop %{buildroot}/usr/share/applications/ + cp ${GITHUB_WORKSPACE}/yx-tools-gui.desktop %{buildroot}/usr/share/applications/ sed -i 's|Exec=yx-tools-gui|Exec=/usr/bin/yx-tools-gui|' %{buildroot}/usr/share/applications/yx-tools-gui.desktop %files /opt/yx-tools-gui/yx-tools-gui From 846652a248fba304c444584d3fe6262583740306 Mon Sep 17 00:00:00 2001 From: ntbowen Date: Fri, 28 Nov 2025 22:41:53 +0800 Subject: [PATCH 12/25] =?UTF-8?q?feat:=20=E4=BD=BF=E7=94=A8=20flet=20build?= =?UTF-8?q?=20=E6=9B=BF=E4=BB=A3=20flet=20pack=20=E4=BC=98=E5=8C=96=20macO?= =?UTF-8?q?S=20=E5=90=AF=E5=8A=A8=E9=80=9F=E5=BA=A6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 添加 pyproject.toml 配置文件 - macOS 构建使用 flet build macos 生成原生应用 - 添加 Flutter SDK 安装步骤 - 恢复 Flet 依赖 --- .github/workflows/build-gui.yml | 66 +++++++-------------------------- pyproject.toml | 33 +++++++++++++++++ requirements-gui.txt | 1 + 3 files changed, 48 insertions(+), 52 deletions(-) create mode 100644 pyproject.toml diff --git a/.github/workflows/build-gui.yml b/.github/workflows/build-gui.yml index 5636a3f..b248302 100644 --- a/.github/workflows/build-gui.yml +++ b/.github/workflows/build-gui.yml @@ -278,38 +278,19 @@ jobs: run: | pip install flet pyinstaller pillow pip install -r requirements-gui.txt - - name: Create ICNS icon - run: | - mkdir -p icon.iconset - sips -z 16 16 icon/icon.png --out icon.iconset/icon_16x16.png - sips -z 32 32 icon/icon.png --out icon.iconset/icon_16x16@2x.png - sips -z 32 32 icon/icon.png --out icon.iconset/icon_32x32.png - sips -z 64 64 icon/icon.png --out icon.iconset/icon_32x32@2x.png - sips -z 128 128 icon/icon.png --out icon.iconset/icon_128x128.png - sips -z 256 256 icon/icon.png --out icon.iconset/icon_128x128@2x.png - sips -z 256 256 icon/icon.png --out icon.iconset/icon_256x256.png - sips -z 512 512 icon/icon.png --out icon.iconset/icon_256x256@2x.png - sips -z 512 512 icon/icon.png --out icon.iconset/icon_512x512.png - cp icon/icon.png icon.iconset/icon_512x512@2x.png - iconutil -c icns icon.iconset -o icon/icon.icns + - name: Install Flutter + uses: subosito/flutter-action@v2 + with: + flutter-version: '3.24.0' + channel: 'stable' - name: Build with Flet run: | - flet pack -n yx-tools-gui -i icon/icon.icns --add-data "cloudflare_speedtest.py:." --product-name "yx-tools-gui" --bundle-id "com.yxtools.gui" cloudflare_speedtest_gui.py - - name: Fix macOS app bundle - run: | - APP_PATH="dist/yx-tools-gui.app" - # 替换所有图标文件 - find "$APP_PATH/Contents/Resources" -name "*.icns" -exec cp icon/icon.icns {} \; - # 修改 Info.plist 确保应用名称正确 - /usr/libexec/PlistBuddy -c "Set :CFBundleName yx-tools-gui" "$APP_PATH/Contents/Info.plist" - /usr/libexec/PlistBuddy -c "Set :CFBundleDisplayName yx-tools-gui" "$APP_PATH/Contents/Info.plist" 2>/dev/null || /usr/libexec/PlistBuddy -c "Add :CFBundleDisplayName string yx-tools-gui" "$APP_PATH/Contents/Info.plist" - # 清除图标缓存 - touch "$APP_PATH" + flet build macos --project "yx-tools-gui" --product "yx-tools-gui" --org "com.yxtools" --build-version "1.0.0" - name: Create DMG run: | VERSION=${{ github.event.inputs.version || '1.0.0' }} brew install create-dmg - create-dmg --volname "yx-tools-gui" --window-size 600 400 --icon-size 100 --icon "yx-tools-gui.app" 150 185 --app-drop-link 450 185 "yx-tools-gui-${VERSION}-intel.dmg" "dist/yx-tools-gui.app" || hdiutil create -volname "yx-tools-gui" -srcfolder dist/yx-tools-gui.app -ov -format UDZO "yx-tools-gui-${VERSION}-intel.dmg" + create-dmg --volname "yx-tools-gui" --window-size 600 400 --icon-size 100 --icon "yx-tools-gui.app" 150 185 --app-drop-link 450 185 "yx-tools-gui-${VERSION}-intel.dmg" "build/macos/yx-tools-gui.app" || hdiutil create -volname "yx-tools-gui" -srcfolder build/macos/yx-tools-gui.app -ov -format UDZO "yx-tools-gui-${VERSION}-intel.dmg" - uses: actions/upload-artifact@v4 with: name: dmg-intel @@ -327,38 +308,19 @@ jobs: run: | pip install flet pyinstaller pillow pip install -r requirements-gui.txt - - name: Create ICNS icon - run: | - mkdir -p icon.iconset - sips -z 16 16 icon/icon.png --out icon.iconset/icon_16x16.png - sips -z 32 32 icon/icon.png --out icon.iconset/icon_16x16@2x.png - sips -z 32 32 icon/icon.png --out icon.iconset/icon_32x32.png - sips -z 64 64 icon/icon.png --out icon.iconset/icon_32x32@2x.png - sips -z 128 128 icon/icon.png --out icon.iconset/icon_128x128.png - sips -z 256 256 icon/icon.png --out icon.iconset/icon_128x128@2x.png - sips -z 256 256 icon/icon.png --out icon.iconset/icon_256x256.png - sips -z 512 512 icon/icon.png --out icon.iconset/icon_256x256@2x.png - sips -z 512 512 icon/icon.png --out icon.iconset/icon_512x512.png - cp icon/icon.png icon.iconset/icon_512x512@2x.png - iconutil -c icns icon.iconset -o icon/icon.icns + - name: Install Flutter + uses: subosito/flutter-action@v2 + with: + flutter-version: '3.24.0' + channel: 'stable' - name: Build with Flet run: | - flet pack -n yx-tools-gui -i icon/icon.icns --add-data "cloudflare_speedtest.py:." --product-name "yx-tools-gui" --bundle-id "com.yxtools.gui" cloudflare_speedtest_gui.py - - name: Fix macOS app bundle - run: | - APP_PATH="dist/yx-tools-gui.app" - # 替换所有图标文件 - find "$APP_PATH/Contents/Resources" -name "*.icns" -exec cp icon/icon.icns {} \; - # 修改 Info.plist 确保应用名称正确 - /usr/libexec/PlistBuddy -c "Set :CFBundleName yx-tools-gui" "$APP_PATH/Contents/Info.plist" - /usr/libexec/PlistBuddy -c "Set :CFBundleDisplayName yx-tools-gui" "$APP_PATH/Contents/Info.plist" 2>/dev/null || /usr/libexec/PlistBuddy -c "Add :CFBundleDisplayName string yx-tools-gui" "$APP_PATH/Contents/Info.plist" - # 清除图标缓存 - touch "$APP_PATH" + flet build macos --project "yx-tools-gui" --product "yx-tools-gui" --org "com.yxtools" --build-version "1.0.0" - name: Create DMG run: | VERSION=${{ github.event.inputs.version || '1.0.0' }} brew install create-dmg - create-dmg --volname "yx-tools-gui" --window-size 600 400 --icon-size 100 --icon "yx-tools-gui.app" 150 185 --app-drop-link 450 185 "yx-tools-gui-${VERSION}-apple-silicon.dmg" "dist/yx-tools-gui.app" || hdiutil create -volname "yx-tools-gui" -srcfolder dist/yx-tools-gui.app -ov -format UDZO "yx-tools-gui-${VERSION}-apple-silicon.dmg" + create-dmg --volname "yx-tools-gui" --window-size 600 400 --icon-size 100 --icon "yx-tools-gui.app" 150 185 --app-drop-link 450 185 "yx-tools-gui-${VERSION}-apple-silicon.dmg" "build/macos/yx-tools-gui.app" || hdiutil create -volname "yx-tools-gui" -srcfolder build/macos/yx-tools-gui.app -ov -format UDZO "yx-tools-gui-${VERSION}-apple-silicon.dmg" - uses: actions/upload-artifact@v4 with: name: dmg-apple-silicon 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 index c42b6fd..f212de6 100644 --- a/requirements-gui.txt +++ b/requirements-gui.txt @@ -1,2 +1,3 @@ # Cloudflare SpeedTest GUI 依赖 flet>=0.21.0 +requests>=2.28.0 From f5ac98e8c9938463b68ea44fcd56cf01b5d00248 Mon Sep 17 00:00:00 2001 From: ntbowen Date: Fri, 28 Nov 2025 22:58:12 +0800 Subject: [PATCH 13/25] =?UTF-8?q?feat:=20=E6=89=80=E6=9C=89=E5=B9=B3?= =?UTF-8?q?=E5=8F=B0=E4=BD=BF=E7=94=A8=20flet=20build=20=E6=9B=BF=E4=BB=A3?= =?UTF-8?q?=20flet=20pack?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Linux x64/arm64: 使用 flet build linux - Windows x64/arm64: 使用 flet build windows - macOS Intel/ARM: 使用 flet build macos - 添加 Flutter SDK 安装步骤 - 预期体积减少 60-70%,启动速度提升 2-5 倍 --- .github/workflows/build-gui.yml | 287 ++++++++++++++++---------------- 1 file changed, 146 insertions(+), 141 deletions(-) diff --git a/.github/workflows/build-gui.yml b/.github/workflows/build-gui.yml index b248302..b61099d 100644 --- a/.github/workflows/build-gui.yml +++ b/.github/workflows/build-gui.yml @@ -16,23 +16,23 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - - - name: Set up Python - uses: actions/setup-python@v5 + - 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: | sudo apt-get update - sudo apt-get install -y libgtk-3-dev libgstreamer1.0-dev libgstreamer-plugins-base1.0-dev libfuse2 rpm - pip install flet pyinstaller pillow + sudo apt-get install -y libgtk-3-dev libgstreamer1.0-dev libgstreamer-plugins-base1.0-dev ninja-build libfuse2 rpm clang cmake + pip install flet pip install -r requirements-gui.txt - - name: Build with Flet run: | - flet pack -n yx-tools-gui -i icon/icon.png --add-data "cloudflare_speedtest.py:." --product-name "yx-tools-gui" cloudflare_speedtest_gui.py - + flet build linux --project "yx-tools-gui" --product "yx-tools-gui" --org "com.yxtools" --build-version "${{ github.event.inputs.version || '1.0.0' }}" - name: Create packages run: | VERSION=${{ github.event.inputs.version || '1.0.0' }} @@ -41,20 +41,19 @@ jobs: 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 build/linux/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 cat > AppDir/yx-tools-gui.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; - StartupWMClass=flet - 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; +EOF cp AppDir/yx-tools-gui.desktop AppDir/usr/share/applications/ echo '#!/bin/bash' > AppDir/AppRun echo 'HERE=$(dirname "$(readlink -f "$0")")' >> AppDir/AppRun @@ -64,66 +63,56 @@ jobs: # 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 build/linux/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|' AppDir/yx-tools-gui.desktop > deb-pkg/usr/share/applications/yx-tools-gui.desktop cat > deb-pkg/DEBIAN/control << EOF - Package: yx-tools-gui - Version: ${VERSION} - Architecture: amd64 - Maintainer: Joey and Zag - Description: Cloudflare IP Speed Test Tool - EOF +Package: yx-tools-gui +Version: ${VERSION} +Architecture: amd64 +Maintainer: Joey and Zag +Description: Cloudflare IP Speed Test Tool +EOF dpkg-deb --build deb-pkg yx-tools-gui_${VERSION}_amd64.deb # RPM mkdir -p rpmbuild/{BUILD,RPMS,SPECS} cat > rpmbuild/SPECS/yx-tools-gui.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 ${GITHUB_WORKSPACE}/dist/yx-tools-gui %{buildroot}/opt/yx-tools-gui/ - chmod +x %{buildroot}/opt/yx-tools-gui/yx-tools-gui - cp ${GITHUB_WORKSPACE}/icon/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 ${GITHUB_WORKSPACE}/AppDir/usr/share/applications/yx-tools-gui.desktop %{buildroot}/usr/share/applications/ - sed -i 's|Exec=yx-tools-gui|Exec=/usr/bin/yx-tools-gui|' %{buildroot}/usr/share/applications/yx-tools-gui.desktop - %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 - 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 ${GITHUB_WORKSPACE}/build/linux/yx-tools-gui %{buildroot}/opt/yx-tools-gui/ +chmod +x %{buildroot}/opt/yx-tools-gui/yx-tools-gui +cp ${GITHUB_WORKSPACE}/icon/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 ${GITHUB_WORKSPACE}/AppDir/usr/share/applications/yx-tools-gui.desktop %{buildroot}/usr/share/applications/ +sed -i 's|Exec=yx-tools-gui|Exec=/usr/bin/yx-tools-gui|' %{buildroot}/usr/share/applications/yx-tools-gui.desktop +%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 +EOF 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: rpm-x86_64 - path: yx-tools-gui-*.x86_64.rpm - compression-level: 0 - - uses: actions/upload-artifact@v4 with: - name: deb-amd64 - path: yx-tools-gui_*_amd64.deb - compression-level: 0 - - - uses: actions/upload-artifact@v4 - with: - name: appimage-x86_64 - path: yx-tools-gui-x86_64.AppImage + 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: @@ -133,96 +122,98 @@ jobs: - 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: | sudo apt-get update - sudo apt-get install -y libgtk-3-dev libgstreamer1.0-dev libgstreamer-plugins-base1.0-dev libfuse2 rpm - pip install flet pyinstaller pillow + sudo apt-get install -y libgtk-3-dev libgstreamer1.0-dev libgstreamer-plugins-base1.0-dev ninja-build libfuse2 rpm clang cmake + pip install flet pip install -r requirements-gui.txt - name: Build with Flet - run: flet pack -n yx-tools-gui -i icon/icon.png --add-data "cloudflare_speedtest.py:." --product-name "yx-tools-gui" cloudflare_speedtest_gui.py + run: | + flet build linux --project "yx-tools-gui" --product "yx-tools-gui" --org "com.yxtools" --build-version "${{ github.event.inputs.version || '1.0.0' }}" - name: Create packages run: | VERSION=${{ github.event.inputs.version || '1.0.0' }} + # 创建 desktop 文件 cat > yx-tools-gui.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; - StartupWMClass=flet - 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; +EOF - # AppImage - 使用 linuxdeploy 替代 appimagetool + # AppImage wget -q https://github.com/linuxdeploy/linuxdeploy/releases/download/continuous/linuxdeploy-aarch64.AppImage -O linuxdeploy chmod +x linuxdeploy ./linuxdeploy --appimage-extract mkdir -p AppDir/usr/bin - cp dist/yx-tools-gui AppDir/usr/bin/ + cp build/linux/yx-tools-gui AppDir/usr/bin/ cp icon/icon.png yx-tools-gui.png - ./squashfs-root/AppRun --appdir AppDir -e dist/yx-tools-gui -i yx-tools-gui.png -d yx-tools-gui.desktop --output appimage || echo "AppImage build skipped" + ./squashfs-root/AppRun --appdir AppDir -e build/linux/yx-tools-gui -i yx-tools-gui.png -d yx-tools-gui.desktop --output appimage || echo "AppImage build skipped" mv *.AppImage yx-tools-gui-aarch64.AppImage 2>/dev/null || touch yx-tools-gui-aarch64.AppImage.skip + # 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 build/linux/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 cat > deb-pkg/DEBIAN/control << EOF - Package: yx-tools-gui - Version: ${VERSION} - Architecture: arm64 - Maintainer: Joey and Zag - Description: Cloudflare IP Speed Test Tool - EOF +Package: yx-tools-gui +Version: ${VERSION} +Architecture: arm64 +Maintainer: Joey and Zag +Description: Cloudflare IP Speed Test Tool +EOF dpkg-deb --build deb-pkg yx-tools-gui_${VERSION}_arm64.deb + + # RPM mkdir -p rpmbuild/{BUILD,RPMS,SPECS} cat > rpmbuild/SPECS/yx-tools-gui.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 ${GITHUB_WORKSPACE}/dist/yx-tools-gui %{buildroot}/opt/yx-tools-gui/ - chmod +x %{buildroot}/opt/yx-tools-gui/yx-tools-gui - cp ${GITHUB_WORKSPACE}/icon/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 ${GITHUB_WORKSPACE}/yx-tools-gui.desktop %{buildroot}/usr/share/applications/ - sed -i 's|Exec=yx-tools-gui|Exec=/usr/bin/yx-tools-gui|' %{buildroot}/usr/share/applications/yx-tools-gui.desktop - %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 - 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 ${GITHUB_WORKSPACE}/build/linux/yx-tools-gui %{buildroot}/opt/yx-tools-gui/ +chmod +x %{buildroot}/opt/yx-tools-gui/yx-tools-gui +cp ${GITHUB_WORKSPACE}/icon/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 ${GITHUB_WORKSPACE}/yx-tools-gui.desktop %{buildroot}/usr/share/applications/ +sed -i 's|Exec=yx-tools-gui|Exec=/usr/bin/yx-tools-gui|' %{buildroot}/usr/share/applications/yx-tools-gui.desktop +%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 +EOF 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: rpm-aarch64 - path: yx-tools-gui-*.aarch64.rpm - compression-level: 0 - - uses: actions/upload-artifact@v4 - with: - name: deb-arm64 - path: yx-tools-gui_*_arm64.deb - compression-level: 0 - - uses: actions/upload-artifact@v4 - with: - name: appimage-aarch64 - path: yx-tools-gui-aarch64.AppImage + name: linux-arm64-packages + path: | + yx-tools-gui-*.aarch64.rpm + yx-tools-gui_*_arm64.deb + yx-tools-gui-aarch64.AppImage* compression-level: 0 build-windows-x64: @@ -232,18 +223,25 @@ jobs: - 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 pyinstaller pillow + pip install flet pip install -r requirements-gui.txt - name: Build with Flet run: | - flet pack -n yx-tools-gui -i icon/icon.png --add-data "cloudflare_speedtest.py;." --product-name "yx-tools-gui" cloudflare_speedtest_gui.py - Rename-Item -Path "dist/yx-tools-gui.exe" -NewName "yx-tools-gui-x64.exe" + flet build windows --project "yx-tools-gui" --product "yx-tools-gui" --org "com.yxtools" --build-version "${{ github.event.inputs.version || '1.0.0' }}" + - name: Package + run: | + Rename-Item -Path "build/windows/yx-tools-gui.exe" -NewName "yx-tools-gui-x64.exe" - uses: actions/upload-artifact@v4 with: name: exe-x64 - path: dist/yx-tools-gui-x64.exe + path: build/windows/yx-tools-gui-x64.exe compression-level: 0 build-windows-arm64: @@ -253,18 +251,25 @@ jobs: - 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 pyinstaller pillow + pip install flet pip install -r requirements-gui.txt - name: Build with Flet run: | - flet pack -n yx-tools-gui -i icon/icon.png --add-data "cloudflare_speedtest.py;." --product-name "yx-tools-gui" cloudflare_speedtest_gui.py - Rename-Item -Path "dist/yx-tools-gui.exe" -NewName "yx-tools-gui-arm64.exe" + flet build windows --project "yx-tools-gui" --product "yx-tools-gui" --org "com.yxtools" --build-version "${{ github.event.inputs.version || '1.0.0' }}" + - name: Package + run: | + Rename-Item -Path "build/windows/yx-tools-gui.exe" -NewName "yx-tools-gui-arm64.exe" - uses: actions/upload-artifact@v4 with: name: exe-arm64 - path: dist/yx-tools-gui-arm64.exe + path: build/windows/yx-tools-gui-arm64.exe compression-level: 0 build-macos-intel: @@ -274,18 +279,18 @@ jobs: - 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: 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: Build with Flet run: | - flet build macos --project "yx-tools-gui" --product "yx-tools-gui" --org "com.yxtools" --build-version "1.0.0" + flet build macos --project "yx-tools-gui" --product "yx-tools-gui" --org "com.yxtools" --build-version "${{ github.event.inputs.version || '1.0.0' }}" - name: Create DMG run: | VERSION=${{ github.event.inputs.version || '1.0.0' }} @@ -304,18 +309,18 @@ jobs: - 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: 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: Build with Flet run: | - flet build macos --project "yx-tools-gui" --product "yx-tools-gui" --org "com.yxtools" --build-version "1.0.0" + flet build macos --project "yx-tools-gui" --product "yx-tools-gui" --org "com.yxtools" --build-version "${{ github.event.inputs.version || '1.0.0' }}" - name: Create DMG run: | VERSION=${{ github.event.inputs.version || '1.0.0' }} From d7781532f7f83c5457ad4dd90e450fdc0146f9ef Mon Sep 17 00:00:00 2001 From: ntbowen Date: Fri, 28 Nov 2025 23:02:53 +0800 Subject: [PATCH 14/25] =?UTF-8?q?fix:=20=E4=BF=AE=E5=A4=8D=E5=B7=A5?= =?UTF-8?q?=E4=BD=9C=E6=B5=81=20YAML=20=E6=A0=BC=E5=BC=8F=E9=97=AE?= =?UTF-8?q?=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/build-gui.yml | 234 ++++++++++++++++---------------- 1 file changed, 117 insertions(+), 117 deletions(-) diff --git a/.github/workflows/build-gui.yml b/.github/workflows/build-gui.yml index b61099d..6b99e26 100644 --- a/.github/workflows/build-gui.yml +++ b/.github/workflows/build-gui.yml @@ -31,79 +31,84 @@ jobs: pip install flet pip install -r requirements-gui.txt - name: Build with Flet + run: flet build linux --project "yx-tools-gui" --product "yx-tools-gui" --org "com.yxtools" --build-version "${{ github.event.inputs.version || '1.0.0' }}" + - name: Create desktop file run: | - flet build linux --project "yx-tools-gui" --product "yx-tools-gui" --org "com.yxtools" --build-version "${{ github.event.inputs.version || '1.0.0' }}" - - name: Create packages + 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 AppImage 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 build/linux/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 - cat > AppDir/yx-tools-gui.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; -EOF - cp AppDir/yx-tools-gui.desktop AppDir/usr/share/applications/ + 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 + - name: Create DEB + run: | + VERSION=${{ github.event.inputs.version || '1.0.0' }} mkdir -p deb-pkg/{DEBIAN,opt/yx-tools-gui,usr/{bin,share/{applications,icons/hicolor/256x256/apps}}} cp build/linux/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|' AppDir/yx-tools-gui.desktop > deb-pkg/usr/share/applications/yx-tools-gui.desktop - cat > deb-pkg/DEBIAN/control << EOF -Package: yx-tools-gui -Version: ${VERSION} -Architecture: amd64 -Maintainer: Joey and Zag -Description: Cloudflare IP Speed Test Tool -EOF + 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 + - name: Create RPM + run: | + VERSION=${{ github.event.inputs.version || '1.0.0' }} mkdir -p rpmbuild/{BUILD,RPMS,SPECS} - cat > rpmbuild/SPECS/yx-tools-gui.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 ${GITHUB_WORKSPACE}/build/linux/yx-tools-gui %{buildroot}/opt/yx-tools-gui/ -chmod +x %{buildroot}/opt/yx-tools-gui/yx-tools-gui -cp ${GITHUB_WORKSPACE}/icon/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 ${GITHUB_WORKSPACE}/AppDir/usr/share/applications/yx-tools-gui.desktop %{buildroot}/usr/share/applications/ -sed -i 's|Exec=yx-tools-gui|Exec=/usr/bin/yx-tools-gui|' %{buildroot}/usr/share/applications/yx-tools-gui.desktop -%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 -EOF + cat > rpmbuild/SPECS/yx-tools-gui.spec << 'SPEC_EOF' + Name: yx-tools-gui + Version: VERSION_PLACEHOLDER + 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 + sed -i "s/VERSION_PLACEHOLDER/${VERSION}/" rpmbuild/SPECS/yx-tools-gui.spec + mkdir -p rpmbuild/SOURCES + cp build/linux/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 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 @@ -134,25 +139,22 @@ EOF pip install flet pip install -r requirements-gui.txt - name: Build with Flet + run: flet build linux --project "yx-tools-gui" --product "yx-tools-gui" --org "com.yxtools" --build-version "${{ github.event.inputs.version || '1.0.0' }}" + - name: Create desktop file run: | - flet build linux --project "yx-tools-gui" --product "yx-tools-gui" --org "com.yxtools" --build-version "${{ github.event.inputs.version || '1.0.0' }}" - - name: Create packages + 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 AppImage run: | - VERSION=${{ github.event.inputs.version || '1.0.0' }} - - # 创建 desktop 文件 - cat > yx-tools-gui.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; -EOF - - # AppImage wget -q https://github.com/linuxdeploy/linuxdeploy/releases/download/continuous/linuxdeploy-aarch64.AppImage -O linuxdeploy chmod +x linuxdeploy ./linuxdeploy --appimage-extract @@ -161,8 +163,9 @@ EOF cp icon/icon.png yx-tools-gui.png ./squashfs-root/AppRun --appdir AppDir -e build/linux/yx-tools-gui -i yx-tools-gui.png -d yx-tools-gui.desktop --output appimage || echo "AppImage build skipped" mv *.AppImage yx-tools-gui-aarch64.AppImage 2>/dev/null || touch yx-tools-gui-aarch64.AppImage.skip - - # DEB + - name: Create DEB + run: | + VERSION=${{ github.event.inputs.version || '1.0.0' }} mkdir -p deb-pkg/{DEBIAN,opt/yx-tools-gui,usr/{bin,share/{applications,icons/hicolor/256x256/apps}}} cp build/linux/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 @@ -170,41 +173,44 @@ EOF 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 - cat > deb-pkg/DEBIAN/control << EOF -Package: yx-tools-gui -Version: ${VERSION} -Architecture: arm64 -Maintainer: Joey and Zag -Description: Cloudflare IP Speed Test Tool -EOF + 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} - cat > rpmbuild/SPECS/yx-tools-gui.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 ${GITHUB_WORKSPACE}/build/linux/yx-tools-gui %{buildroot}/opt/yx-tools-gui/ -chmod +x %{buildroot}/opt/yx-tools-gui/yx-tools-gui -cp ${GITHUB_WORKSPACE}/icon/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 ${GITHUB_WORKSPACE}/yx-tools-gui.desktop %{buildroot}/usr/share/applications/ -sed -i 's|Exec=yx-tools-gui|Exec=/usr/bin/yx-tools-gui|' %{buildroot}/usr/share/applications/yx-tools-gui.desktop -%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 -EOF + - name: Create RPM + run: | + VERSION=${{ github.event.inputs.version || '1.0.0' }} + mkdir -p rpmbuild/{BUILD,RPMS,SPECS,SOURCES} + cat > rpmbuild/SPECS/yx-tools-gui.spec << 'SPEC_EOF' + Name: yx-tools-gui + Version: VERSION_PLACEHOLDER + 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 + sed -i "s/VERSION_PLACEHOLDER/${VERSION}/" rpmbuild/SPECS/yx-tools-gui.spec + cp build/linux/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 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 @@ -233,11 +239,9 @@ EOF pip install flet pip install -r requirements-gui.txt - name: Build with Flet - run: | - flet build windows --project "yx-tools-gui" --product "yx-tools-gui" --org "com.yxtools" --build-version "${{ github.event.inputs.version || '1.0.0' }}" + run: flet build windows --project "yx-tools-gui" --product "yx-tools-gui" --org "com.yxtools" --build-version "${{ github.event.inputs.version || '1.0.0' }}" - name: Package - run: | - Rename-Item -Path "build/windows/yx-tools-gui.exe" -NewName "yx-tools-gui-x64.exe" + run: Rename-Item -Path "build/windows/yx-tools-gui.exe" -NewName "yx-tools-gui-x64.exe" - uses: actions/upload-artifact@v4 with: name: exe-x64 @@ -261,11 +265,9 @@ EOF pip install flet pip install -r requirements-gui.txt - name: Build with Flet - run: | - flet build windows --project "yx-tools-gui" --product "yx-tools-gui" --org "com.yxtools" --build-version "${{ github.event.inputs.version || '1.0.0' }}" + run: flet build windows --project "yx-tools-gui" --product "yx-tools-gui" --org "com.yxtools" --build-version "${{ github.event.inputs.version || '1.0.0' }}" - name: Package - run: | - Rename-Item -Path "build/windows/yx-tools-gui.exe" -NewName "yx-tools-gui-arm64.exe" + run: Rename-Item -Path "build/windows/yx-tools-gui.exe" -NewName "yx-tools-gui-arm64.exe" - uses: actions/upload-artifact@v4 with: name: exe-arm64 @@ -289,8 +291,7 @@ EOF pip install flet pip install -r requirements-gui.txt - 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' }}" + run: flet build macos --project "yx-tools-gui" --product "yx-tools-gui" --org "com.yxtools" --build-version "${{ github.event.inputs.version || '1.0.0' }}" - name: Create DMG run: | VERSION=${{ github.event.inputs.version || '1.0.0' }} @@ -319,8 +320,7 @@ EOF pip install flet pip install -r requirements-gui.txt - 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' }}" + run: flet build macos --project "yx-tools-gui" --product "yx-tools-gui" --org "com.yxtools" --build-version "${{ github.event.inputs.version || '1.0.0' }}" - name: Create DMG run: | VERSION=${{ github.event.inputs.version || '1.0.0' }} From 71e4212f8a9a8b2e839964ac51dd127e877a16a6 Mon Sep 17 00:00:00 2001 From: ntbowen Date: Fri, 28 Nov 2025 23:29:47 +0800 Subject: [PATCH 15/25] =?UTF-8?q?fix:=20=E4=BF=AE=E5=A4=8D=20flet=20build?= =?UTF-8?q?=20=E5=B7=A5=E4=BD=9C=E6=B5=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 添加 assets/icon.png 用于自定义图标 - 添加详细日志输出 (-v) - 动态查找构建输出文件 - 修复 Windows 路径问题 --- .github/workflows/build-gui.yml | 175 ++++++++++++++++++++++---------- assets/icon.png | Bin 0 -> 30661 bytes 2 files changed, 124 insertions(+), 51 deletions(-) create mode 100644 assets/icon.png diff --git a/.github/workflows/build-gui.yml b/.github/workflows/build-gui.yml index 6b99e26..b12c6d9 100644 --- a/.github/workflows/build-gui.yml +++ b/.github/workflows/build-gui.yml @@ -27,11 +27,18 @@ jobs: - name: Install dependencies run: | sudo apt-get update - sudo apt-get install -y libgtk-3-dev libgstreamer1.0-dev libgstreamer-plugins-base1.0-dev ninja-build libfuse2 rpm clang cmake + sudo apt-get install -y libgtk-3-dev libgstreamer1.0-dev libgstreamer-plugins-base1.0-dev ninja-build libfuse2 rpm clang cmake pkg-config 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 linux --project "yx-tools-gui" --product "yx-tools-gui" --org "com.yxtools" --build-version "${{ github.event.inputs.version || '1.0.0' }}" + run: | + flet build linux --project "yx-tools-gui" --product "yx-tools-gui" --org "com.yxtools" --build-version "${{ github.event.inputs.version || '1.0.0' }}" -v + ls -la build/linux/ || true + find build -name "yx-tools-gui*" -type f || true - name: Create desktop file run: | cat > yx-tools-gui.desktop << 'DESKTOP_EOF' @@ -45,12 +52,23 @@ jobs: Categories=Network; DESKTOP_EOF sed -i 's/^ //' yx-tools-gui.desktop - - name: Create AppImage + - name: Find and package executable run: | + VERSION=${{ github.event.inputs.version || '1.0.0' }} + # 查找可执行文件 + EXEC_PATH=$(find build -name "yx-tools-gui" -type f -executable | head -1) + if [ -z "$EXEC_PATH" ]; then + echo "Error: Executable not found" + find build -type f + exit 1 + fi + echo "Found executable: $EXEC_PATH" + + # 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 build/linux/yx-tools-gui AppDir/usr/bin/ + cp "$EXEC_PATH" AppDir/usr/bin/yx-tools-gui 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/ @@ -60,11 +78,10 @@ jobs: 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 - - name: Create DEB - 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 build/linux/yx-tools-gui deb-pkg/opt/yx-tools-gui/ + cp "$EXEC_PATH" deb-pkg/opt/yx-tools-gui/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 @@ -76,13 +93,15 @@ jobs: 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 - - name: Create RPM - run: | - VERSION=${{ github.event.inputs.version || '1.0.0' }} - mkdir -p rpmbuild/{BUILD,RPMS,SPECS} - cat > rpmbuild/SPECS/yx-tools-gui.spec << 'SPEC_EOF' + + # RPM + mkdir -p rpmbuild/{BUILD,RPMS,SPECS,SOURCES} + cp "$EXEC_PATH" rpmbuild/SOURCES/yx-tools-gui + 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_PLACEHOLDER + Version: ${VERSION} Release: 1 Summary: Cloudflare IP Speed Test Tool License: MIT @@ -94,7 +113,7 @@ jobs: 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 + 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 @@ -104,11 +123,6 @@ jobs: /usr/share/icons/hicolor/256x256/apps/yx-tools-gui.png SPEC_EOF sed -i 's/^ //' rpmbuild/SPECS/yx-tools-gui.spec - sed -i "s/VERSION_PLACEHOLDER/${VERSION}/" rpmbuild/SPECS/yx-tools-gui.spec - mkdir -p rpmbuild/SOURCES - cp build/linux/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 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 @@ -135,11 +149,16 @@ jobs: - name: Install dependencies run: | sudo apt-get update - sudo apt-get install -y libgtk-3-dev libgstreamer1.0-dev libgstreamer-plugins-base1.0-dev ninja-build libfuse2 rpm clang cmake + sudo apt-get install -y libgtk-3-dev libgstreamer1.0-dev libgstreamer-plugins-base1.0-dev ninja-build libfuse2 rpm clang cmake pkg-config 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 linux --project "yx-tools-gui" --product "yx-tools-gui" --org "com.yxtools" --build-version "${{ github.event.inputs.version || '1.0.0' }}" + run: | + flet build linux --project "yx-tools-gui" --product "yx-tools-gui" --org "com.yxtools" --build-version "${{ github.event.inputs.version || '1.0.0' }}" -v - name: Create desktop file run: | cat > yx-tools-gui.desktop << 'DESKTOP_EOF' @@ -153,21 +172,29 @@ jobs: Categories=Network; DESKTOP_EOF sed -i 's/^ //' yx-tools-gui.desktop - - name: Create AppImage + - name: Find and package executable run: | + VERSION=${{ github.event.inputs.version || '1.0.0' }} + EXEC_PATH=$(find build -name "yx-tools-gui" -type f -executable | head -1) + if [ -z "$EXEC_PATH" ]; then + echo "Error: Executable not found" + find build -type f + exit 1 + fi + + # AppImage wget -q https://github.com/linuxdeploy/linuxdeploy/releases/download/continuous/linuxdeploy-aarch64.AppImage -O linuxdeploy chmod +x linuxdeploy ./linuxdeploy --appimage-extract mkdir -p AppDir/usr/bin - cp build/linux/yx-tools-gui AppDir/usr/bin/ + cp "$EXEC_PATH" AppDir/usr/bin/yx-tools-gui cp icon/icon.png yx-tools-gui.png - ./squashfs-root/AppRun --appdir AppDir -e build/linux/yx-tools-gui -i yx-tools-gui.png -d yx-tools-gui.desktop --output appimage || echo "AppImage build skipped" + ./squashfs-root/AppRun --appdir AppDir -e "$EXEC_PATH" -i yx-tools-gui.png -d yx-tools-gui.desktop --output appimage || echo "AppImage build skipped" mv *.AppImage yx-tools-gui-aarch64.AppImage 2>/dev/null || touch yx-tools-gui-aarch64.AppImage.skip - - name: Create DEB - 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 build/linux/yx-tools-gui deb-pkg/opt/yx-tools-gui/ + cp "$EXEC_PATH" deb-pkg/opt/yx-tools-gui/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 @@ -179,13 +206,15 @@ jobs: 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 - - name: Create RPM - run: | - VERSION=${{ github.event.inputs.version || '1.0.0' }} + + # RPM mkdir -p rpmbuild/{BUILD,RPMS,SPECS,SOURCES} - cat > rpmbuild/SPECS/yx-tools-gui.spec << 'SPEC_EOF' + cp "$EXEC_PATH" rpmbuild/SOURCES/yx-tools-gui + 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_PLACEHOLDER + Version: ${VERSION} Release: 1 Summary: Cloudflare IP Speed Test Tool License: MIT @@ -197,7 +226,7 @@ jobs: 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 + 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 @@ -207,10 +236,6 @@ jobs: /usr/share/icons/hicolor/256x256/apps/yx-tools-gui.png SPEC_EOF sed -i 's/^ //' rpmbuild/SPECS/yx-tools-gui.spec - sed -i "s/VERSION_PLACEHOLDER/${VERSION}/" rpmbuild/SPECS/yx-tools-gui.spec - cp build/linux/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 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 @@ -238,14 +263,27 @@ jobs: run: | pip install flet pip install -r requirements-gui.txt + - name: Prepare assets + run: | + mkdir -p assets + copy icon\icon.png assets\icon.png + shell: cmd - name: Build with Flet - run: flet build windows --project "yx-tools-gui" --product "yx-tools-gui" --org "com.yxtools" --build-version "${{ github.event.inputs.version || '1.0.0' }}" - - name: Package - run: Rename-Item -Path "build/windows/yx-tools-gui.exe" -NewName "yx-tools-gui-x64.exe" + run: flet build windows --project "yx-tools-gui" --product "yx-tools-gui" --org "com.yxtools" --build-version "${{ github.event.inputs.version || '1.0.0' }}" -v + - name: Find and rename executable + run: | + $exe = Get-ChildItem -Path build -Recurse -Filter "yx-tools-gui.exe" | Select-Object -First 1 + if ($exe) { + Copy-Item $exe.FullName -Destination "yx-tools-gui-x64.exe" + } else { + Get-ChildItem -Path build -Recurse + throw "Executable not found" + } + shell: pwsh - uses: actions/upload-artifact@v4 with: name: exe-x64 - path: build/windows/yx-tools-gui-x64.exe + path: yx-tools-gui-x64.exe compression-level: 0 build-windows-arm64: @@ -264,14 +302,27 @@ jobs: run: | pip install flet pip install -r requirements-gui.txt + - name: Prepare assets + run: | + mkdir -p assets + copy icon\icon.png assets\icon.png + shell: cmd - name: Build with Flet - run: flet build windows --project "yx-tools-gui" --product "yx-tools-gui" --org "com.yxtools" --build-version "${{ github.event.inputs.version || '1.0.0' }}" - - name: Package - run: Rename-Item -Path "build/windows/yx-tools-gui.exe" -NewName "yx-tools-gui-arm64.exe" + run: flet build windows --project "yx-tools-gui" --product "yx-tools-gui" --org "com.yxtools" --build-version "${{ github.event.inputs.version || '1.0.0' }}" -v + - name: Find and rename executable + run: | + $exe = Get-ChildItem -Path build -Recurse -Filter "yx-tools-gui.exe" | Select-Object -First 1 + if ($exe) { + Copy-Item $exe.FullName -Destination "yx-tools-gui-arm64.exe" + } else { + Get-ChildItem -Path build -Recurse + throw "Executable not found" + } + shell: pwsh - uses: actions/upload-artifact@v4 with: name: exe-arm64 - path: build/windows/yx-tools-gui-arm64.exe + path: yx-tools-gui-arm64.exe compression-level: 0 build-macos-intel: @@ -290,13 +341,24 @@ jobs: 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' }}" + 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 "yx-tools-gui.app" 150 185 --app-drop-link 450 185 "yx-tools-gui-${VERSION}-intel.dmg" "build/macos/yx-tools-gui.app" || hdiutil create -volname "yx-tools-gui" -srcfolder build/macos/yx-tools-gui.app -ov -format UDZO "yx-tools-gui-${VERSION}-intel.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 @@ -319,13 +381,24 @@ jobs: 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' }}" + 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 "yx-tools-gui.app" 150 185 --app-drop-link 450 185 "yx-tools-gui-${VERSION}-apple-silicon.dmg" "build/macos/yx-tools-gui.app" || hdiutil create -volname "yx-tools-gui" -srcfolder build/macos/yx-tools-gui.app -ov -format UDZO "yx-tools-gui-${VERSION}-apple-silicon.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 diff --git a/assets/icon.png b/assets/icon.png new file mode 100644 index 0000000000000000000000000000000000000000..50e67a499ca68e317e39875e97d2a318b80fe475 GIT binary patch literal 30661 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 AFaQ7m literal 0 HcmV?d00001 From a4924fa850863a3fbbd026903bc192307730654e Mon Sep 17 00:00:00 2001 From: ntbowen Date: Fri, 28 Nov 2025 23:50:08 +0800 Subject: [PATCH 16/25] =?UTF-8?q?fix:=20=E4=BF=AE=E5=A4=8D=E6=89=93?= =?UTF-8?q?=E5=8C=85=E5=90=8E=E6=9D=83=E9=99=90=E9=97=AE=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 添加 get_app_data_dir() 函数获取跨平台应用数据目录 - macOS: ~/Library/Application Support/yx-tools - Windows: %APPDATA%/yx-tools - Linux: ~/.local/share/yx-tools - 下载的可执行文件存放到用户可写目录 --- cloudflare_speedtest.py | 147 ++++++++++++++++++++++++++-------------- 1 file changed, 96 insertions(+), 51 deletions(-) diff --git a/cloudflare_speedtest.py b/cloudflare_speedtest.py index cf7407b..4883795 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): """ @@ -551,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}" - if os.path.exists(proxy_exec_name): - print(f"✓ 使用反代版本: {proxy_exec_name}") - return proxy_exec_name + # 完整路径 + 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(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": @@ -585,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) @@ -638,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": @@ -659,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) - print(f"✓ 反代版本设置完成: {final_name}") - return final_name + # 清理压缩包 + try: + os.remove(archive_path) + except Exception: + pass + + 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)}") @@ -675,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": @@ -686,10 +731,10 @@ 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 select_ip_version(): From 4b064bc173314a597cfcd81c9ba5e212c0ceaf7e Mon Sep 17 00:00:00 2001 From: ntbowen Date: Fri, 28 Nov 2025 23:56:46 +0800 Subject: [PATCH 17/25] =?UTF-8?q?fix:=20=E6=B7=B7=E5=90=88=E6=9E=84?= =?UTF-8?q?=E5=BB=BA=E7=AD=96=E7=95=A5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - macOS: 使用 flet build(启动快,原生体验) - Linux/Windows: 使用 flet pack(更稳定) - 修复打包后权限问题,下载文件到用户数据目录 --- .github/workflows/build-gui.yml | 136 +++++++++----------------------- 1 file changed, 38 insertions(+), 98 deletions(-) diff --git a/.github/workflows/build-gui.yml b/.github/workflows/build-gui.yml index b12c6d9..4bd0a19 100644 --- a/.github/workflows/build-gui.yml +++ b/.github/workflows/build-gui.yml @@ -19,26 +19,15 @@ jobs: - 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: | sudo apt-get update - sudo apt-get install -y libgtk-3-dev libgstreamer1.0-dev libgstreamer-plugins-base1.0-dev ninja-build libfuse2 rpm clang cmake pkg-config - pip install flet + sudo apt-get install -y libfuse2 rpm + pip install flet pyinstaller pillow pip install -r requirements-gui.txt - - name: Prepare assets - run: | - mkdir -p assets - cp icon/icon.png assets/icon.png - - name: Build with Flet + - name: Build with Flet Pack run: | - flet build linux --project "yx-tools-gui" --product "yx-tools-gui" --org "com.yxtools" --build-version "${{ github.event.inputs.version || '1.0.0' }}" -v - ls -la build/linux/ || true - find build -name "yx-tools-gui*" -type f || true + 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' @@ -52,23 +41,15 @@ jobs: Categories=Network; DESKTOP_EOF sed -i 's/^ //' yx-tools-gui.desktop - - name: Find and package executable + - name: Create packages run: | VERSION=${{ github.event.inputs.version || '1.0.0' }} - # 查找可执行文件 - EXEC_PATH=$(find build -name "yx-tools-gui" -type f -executable | head -1) - if [ -z "$EXEC_PATH" ]; then - echo "Error: Executable not found" - find build -type f - exit 1 - fi - echo "Found executable: $EXEC_PATH" # 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 "$EXEC_PATH" AppDir/usr/bin/yx-tools-gui + 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/ @@ -81,7 +62,7 @@ jobs: # DEB mkdir -p deb-pkg/{DEBIAN,opt/yx-tools-gui,usr/{bin,share/{applications,icons/hicolor/256x256/apps}}} - cp "$EXEC_PATH" deb-pkg/opt/yx-tools-gui/yx-tools-gui + 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 @@ -96,7 +77,7 @@ jobs: # RPM mkdir -p rpmbuild/{BUILD,RPMS,SPECS,SOURCES} - cp "$EXEC_PATH" rpmbuild/SOURCES/yx-tools-gui + 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 @@ -141,24 +122,15 @@ jobs: - 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: | sudo apt-get update - sudo apt-get install -y libgtk-3-dev libgstreamer1.0-dev libgstreamer-plugins-base1.0-dev ninja-build libfuse2 rpm clang cmake pkg-config - pip install flet + sudo apt-get install -y libfuse2 rpm + pip install flet pyinstaller pillow pip install -r requirements-gui.txt - - name: Prepare assets - run: | - mkdir -p assets - cp icon/icon.png assets/icon.png - - name: Build with Flet + - name: Build with Flet Pack run: | - flet build linux --project "yx-tools-gui" --product "yx-tools-gui" --org "com.yxtools" --build-version "${{ github.event.inputs.version || '1.0.0' }}" -v + 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' @@ -172,29 +144,27 @@ jobs: Categories=Network; DESKTOP_EOF sed -i 's/^ //' yx-tools-gui.desktop - - name: Find and package executable + - name: Create packages run: | VERSION=${{ github.event.inputs.version || '1.0.0' }} - EXEC_PATH=$(find build -name "yx-tools-gui" -type f -executable | head -1) - if [ -z "$EXEC_PATH" ]; then - echo "Error: Executable not found" - find build -type f - exit 1 - fi - # AppImage - wget -q https://github.com/linuxdeploy/linuxdeploy/releases/download/continuous/linuxdeploy-aarch64.AppImage -O linuxdeploy - chmod +x linuxdeploy - ./linuxdeploy --appimage-extract - mkdir -p AppDir/usr/bin - cp "$EXEC_PATH" AppDir/usr/bin/yx-tools-gui - cp icon/icon.png yx-tools-gui.png - ./squashfs-root/AppRun --appdir AppDir -e "$EXEC_PATH" -i yx-tools-gui.png -d yx-tools-gui.desktop --output appimage || echo "AppImage build skipped" - mv *.AppImage yx-tools-gui-aarch64.AppImage 2>/dev/null || touch yx-tools-gui-aarch64.AppImage.skip + # AppImage (may fail on arm64) + wget -q https://github.com/linuxdeploy/linuxdeploy/releases/download/continuous/linuxdeploy-aarch64.AppImage -O linuxdeploy || true + if [ -f linuxdeploy ]; then + chmod +x linuxdeploy + ./linuxdeploy --appimage-extract || true + mkdir -p AppDir/usr/bin + cp dist/yx-tools-gui AppDir/usr/bin/ + cp icon/icon.png yx-tools-gui.png + ./squashfs-root/AppRun --appdir AppDir -e dist/yx-tools-gui -i yx-tools-gui.png -d yx-tools-gui.desktop --output appimage || true + mv *.AppImage yx-tools-gui-aarch64.AppImage 2>/dev/null || touch yx-tools-gui-aarch64.AppImage.skip + else + touch yx-tools-gui-aarch64.AppImage.skip + fi # DEB mkdir -p deb-pkg/{DEBIAN,opt/yx-tools-gui,usr/{bin,share/{applications,icons/hicolor/256x256/apps}}} - cp "$EXEC_PATH" deb-pkg/opt/yx-tools-gui/yx-tools-gui + 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 @@ -209,7 +179,7 @@ jobs: # RPM mkdir -p rpmbuild/{BUILD,RPMS,SPECS,SOURCES} - cp "$EXEC_PATH" rpmbuild/SOURCES/yx-tools-gui + 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 @@ -254,31 +224,16 @@ jobs: - 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 flet pyinstaller pillow pip install -r requirements-gui.txt - - name: Prepare assets + - name: Build with Flet Pack run: | - mkdir -p assets - copy icon\icon.png assets\icon.png - shell: cmd - - name: Build with Flet - run: flet build windows --project "yx-tools-gui" --product "yx-tools-gui" --org "com.yxtools" --build-version "${{ github.event.inputs.version || '1.0.0' }}" -v - - name: Find and rename executable + 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: | - $exe = Get-ChildItem -Path build -Recurse -Filter "yx-tools-gui.exe" | Select-Object -First 1 - if ($exe) { - Copy-Item $exe.FullName -Destination "yx-tools-gui-x64.exe" - } else { - Get-ChildItem -Path build -Recurse - throw "Executable not found" - } + Copy-Item "dist/yx-tools-gui.exe" -Destination "yx-tools-gui-x64.exe" shell: pwsh - uses: actions/upload-artifact@v4 with: @@ -293,31 +248,16 @@ jobs: - 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 flet pyinstaller pillow pip install -r requirements-gui.txt - - name: Prepare assets + - name: Build with Flet Pack run: | - mkdir -p assets - copy icon\icon.png assets\icon.png - shell: cmd - - name: Build with Flet - run: flet build windows --project "yx-tools-gui" --product "yx-tools-gui" --org "com.yxtools" --build-version "${{ github.event.inputs.version || '1.0.0' }}" -v - - name: Find and rename executable + 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: | - $exe = Get-ChildItem -Path build -Recurse -Filter "yx-tools-gui.exe" | Select-Object -First 1 - if ($exe) { - Copy-Item $exe.FullName -Destination "yx-tools-gui-arm64.exe" - } else { - Get-ChildItem -Path build -Recurse - throw "Executable not found" - } + Copy-Item "dist/yx-tools-gui.exe" -Destination "yx-tools-gui-arm64.exe" shell: pwsh - uses: actions/upload-artifact@v4 with: From 8a31f1bb7dadaa4e3300861a689f1e58c71fd986 Mon Sep 17 00:00:00 2001 From: ntbowen Date: Sat, 29 Nov 2025 00:25:40 +0800 Subject: [PATCH 18/25] =?UTF-8?q?fix:=20=E4=BF=AE=E5=A4=8D=E7=BB=9D?= =?UTF-8?q?=E5=AF=B9=E8=B7=AF=E5=BE=84=E6=89=A7=E8=A1=8C=E9=97=AE=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 添加 get_exec_cmd() 辅助函数 - 当 exec_name 是绝对路径时不添加 ./ 前缀 - 修复 macOS 打包后 Permission denied 错误 --- cloudflare_speedtest.py | 55 +++++++++++++++---------------------- cloudflare_speedtest_gui.py | 6 ++-- 2 files changed, 24 insertions(+), 37 deletions(-) diff --git a/cloudflare_speedtest.py b/cloudflare_speedtest.py index 4883795..624eb0c 100644 --- a/cloudflare_speedtest.py +++ b/cloudflare_speedtest.py @@ -737,6 +737,19 @@ def download_cloudflare_speedtest(os_type, arch_type): 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(): """选择IP版本(IPv4或IPv6)""" print("\n" + "=" * 60) @@ -1532,10 +1545,7 @@ def handle_beginner_mode(ip_file=CLOUDFLARE_IP_FILE, ip_version="ipv4", selected print(f"\n🔍 正在测试端口 {port}...") # 构建测速命令 - if sys.platform == "win32": - cmd = [exec_name] - else: - cmd = [f"./{exec_name}"] + cmd = [get_exec_cmd(exec_name)] temp_result_file = f"result_port_{port}.csv" cmd.extend([ @@ -1581,10 +1591,7 @@ def handle_beginner_mode(ip_file=CLOUDFLARE_IP_FILE, ip_version="ipv4", selected else: # 单个端口或非 CIDR 格式 # 构建测速命令 - if sys.platform == "win32": - cmd = [exec_name] - else: - cmd = [f"./{exec_name}"] + cmd = [get_exec_cmd(exec_name)] cmd.extend([ "-f", actual_ip_file, @@ -1844,10 +1851,7 @@ def handle_normal_mode(ip_file=CLOUDFLARE_IP_FILE, ip_version="ipv4", selected_p for port in tp_ports: print(f"\n🔍 正在测试端口 {port}...") - if sys.platform == "win32": - cmd = [exec_name] - else: - cmd = [f"./{exec_name}"] + cmd = [get_exec_cmd(exec_name)] temp_result_file = f"result_port_{port}.csv" cmd.extend([ @@ -1892,10 +1896,7 @@ def handle_normal_mode(ip_file=CLOUDFLARE_IP_FILE, ip_version="ipv4", selected_p upload_info = None else: # 单个端口或非 CIDR 格式 - if sys.platform == "win32": - cmd = [exec_name] - else: - cmd = [f"./{exec_name}"] + cmd = [get_exec_cmd(exec_name)] cmd.extend([ "-f", actual_ip_file, @@ -2046,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, @@ -2090,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, @@ -2259,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, @@ -2347,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, @@ -4618,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 index 0cb7a1d..e50dfa4 100644 --- a/cloudflare_speedtest_gui.py +++ b/cloudflare_speedtest_gui.py @@ -28,6 +28,7 @@ load_config, save_config, generate_ipv6_file, + get_exec_cmd, ) except ImportError: print("错误: 请确保 cloudflare_speedtest.py 在同一目录下") @@ -1115,10 +1116,7 @@ def run_speedtest_thread(self): self.page.update() # 构建命令 - if sys.platform == "win32": - cmd = [exec_name] - else: - cmd = [f"./{exec_name}"] + cmd = [get_exec_cmd(exec_name)] cmd.extend([ "-f", actual_ip_file, From 85d2998eda76be07a7d66213d32150cc9bf4d32f Mon Sep 17 00:00:00 2001 From: ntbowen Date: Sat, 29 Nov 2025 01:28:50 +0800 Subject: [PATCH 19/25] =?UTF-8?q?fix:=20=E4=BF=AE=E5=A4=8D=20release=20job?= =?UTF-8?q?=20=E6=96=87=E4=BB=B6=E6=8F=90=E5=8F=96?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/build-gui.yml | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/.github/workflows/build-gui.yml b/.github/workflows/build-gui.yml index 4bd0a19..51cddff 100644 --- a/.github/workflows/build-gui.yml +++ b/.github/workflows/build-gui.yml @@ -353,11 +353,14 @@ jobs: - uses: actions/download-artifact@v4 with: path: artifacts - merge-multiple: true - - run: ls -la 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: artifacts/* + files: release/* draft: false generate_release_notes: true env: From 6c4828e4acc8006397720450911117448c9dd684 Mon Sep 17 00:00:00 2001 From: ntbowen Date: Sat, 29 Nov 2025 01:32:33 +0800 Subject: [PATCH 20/25] =?UTF-8?q?fix:=20=E7=A7=BB=E9=99=A4=E4=B8=8D?= =?UTF-8?q?=E7=A8=B3=E5=AE=9A=E7=9A=84=20arm64=20AppImage=EF=BC=8C?= =?UTF-8?q?=E6=9B=B4=E6=96=B0=20macOS=20runner?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/build-gui.yml | 17 +---------------- 1 file changed, 1 insertion(+), 16 deletions(-) diff --git a/.github/workflows/build-gui.yml b/.github/workflows/build-gui.yml index 51cddff..95af7c8 100644 --- a/.github/workflows/build-gui.yml +++ b/.github/workflows/build-gui.yml @@ -148,20 +148,6 @@ jobs: run: | VERSION=${{ github.event.inputs.version || '1.0.0' }} - # AppImage (may fail on arm64) - wget -q https://github.com/linuxdeploy/linuxdeploy/releases/download/continuous/linuxdeploy-aarch64.AppImage -O linuxdeploy || true - if [ -f linuxdeploy ]; then - chmod +x linuxdeploy - ./linuxdeploy --appimage-extract || true - mkdir -p AppDir/usr/bin - cp dist/yx-tools-gui AppDir/usr/bin/ - cp icon/icon.png yx-tools-gui.png - ./squashfs-root/AppRun --appdir AppDir -e dist/yx-tools-gui -i yx-tools-gui.png -d yx-tools-gui.desktop --output appimage || true - mv *.AppImage yx-tools-gui-aarch64.AppImage 2>/dev/null || touch yx-tools-gui-aarch64.AppImage.skip - else - touch yx-tools-gui-aarch64.AppImage.skip - fi - # 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/ @@ -214,7 +200,6 @@ jobs: path: | yx-tools-gui-*.aarch64.rpm yx-tools-gui_*_arm64.deb - yx-tools-gui-aarch64.AppImage* compression-level: 0 build-windows-x64: @@ -266,7 +251,7 @@ jobs: compression-level: 0 build-macos-intel: - runs-on: macos-13 + runs-on: macos-15-large steps: - uses: actions/checkout@v4 - uses: actions/setup-python@v5 From a19ccf83402a7df334c5b7d763947ac847b17d79 Mon Sep 17 00:00:00 2001 From: ntbowen Date: Sat, 29 Nov 2025 01:40:44 +0800 Subject: [PATCH 21/25] =?UTF-8?q?fix:=20=E6=94=B9=E5=9B=9E=E5=85=8D?= =?UTF-8?q?=E8=B4=B9=E7=9A=84=20macos-13=20runner?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/build-gui.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/build-gui.yml b/.github/workflows/build-gui.yml index 95af7c8..835be68 100644 --- a/.github/workflows/build-gui.yml +++ b/.github/workflows/build-gui.yml @@ -251,7 +251,7 @@ jobs: compression-level: 0 build-macos-intel: - runs-on: macos-15-large + runs-on: macos-13 steps: - uses: actions/checkout@v4 - uses: actions/setup-python@v5 From 0826b010eecf61ef7532bd24d1f45efdb043903e Mon Sep 17 00:00:00 2001 From: ntbowen <59873000+ntbowen@users.noreply.github.com> Date: Sat, 29 Nov 2025 08:52:16 +0800 Subject: [PATCH 22/25] Enhance README with graphical interface details Added graphical interface section with images and features. --- README.md | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/README.md b/README.md index c7452d7..a12ab96 100644 --- a/README.md +++ b/README.md @@ -52,6 +52,13 @@ - **多架构支持** - 支持amd64和arm64架构 - **环境隔离** - 容器化运行,环境干净整洁 +### 图形化界面 + image + image +- **现代化图形界面** - 操作更加直观友好 +- **主题切换** - 可根据个人喜好选择主题 +- **多架构支持** - 支持amd64和arm64架构 + ## 支持平台 | 平台 | 架构 | 状态 | @@ -108,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(推荐容器化部署) From 4187716f1a08e5f2a364fc6bdf1235e6f0539068 Mon Sep 17 00:00:00 2001 From: ntbowen <59873000+ntbowen@users.noreply.github.com> Date: Sat, 29 Nov 2025 09:02:07 +0800 Subject: [PATCH 23/25] Update README.md --- README.md | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/README.md b/README.md index a12ab96..5945e0b 100644 --- a/README.md +++ b/README.md @@ -55,6 +55,11 @@ ### 图形化界面 image image +- **Docker镜像** - 提供官方Docker镜像,开箱即用 +- **Docker Compose** - 支持Docker Compose一键部署 +- **数据持久化** - 支持挂载数据目录,保存测速结果 +- **多架构支持** - 支持amd64和arm64架构 +- **环境隔离** - 容器化运行,环境干净整洁 - **现代化图形界面** - 操作更加直观友好 - **主题切换** - 可根据个人喜好选择主题 - **多架构支持** - 支持amd64和arm64架构 From d3c47f51ec72565253a3c63bbaba255b39c42705 Mon Sep 17 00:00:00 2001 From: ntbowen <59873000+ntbowen@users.noreply.github.com> Date: Sat, 29 Nov 2025 09:03:39 +0800 Subject: [PATCH 24/25] Update README.md --- README.md | 9 ++------- 1 file changed, 2 insertions(+), 7 deletions(-) diff --git a/README.md b/README.md index 5945e0b..95ddb14 100644 --- a/README.md +++ b/README.md @@ -53,16 +53,11 @@ - **环境隔离** - 容器化运行,环境干净整洁 ### 图形化界面 - image - image -- **Docker镜像** - 提供官方Docker镜像,开箱即用 -- **Docker Compose** - 支持Docker Compose一键部署 -- **数据持久化** - 支持挂载数据目录,保存测速结果 -- **多架构支持** - 支持amd64和arm64架构 -- **环境隔离** - 容器化运行,环境干净整洁 - **现代化图形界面** - 操作更加直观友好 - **主题切换** - 可根据个人喜好选择主题 - **多架构支持** - 支持amd64和arm64架构 +- image +image ## 支持平台 From 32f1ce25b6916722a77f99d872e232c39facd29a Mon Sep 17 00:00:00 2001 From: ntbowen <59873000+ntbowen@users.noreply.github.com> Date: Sat, 29 Nov 2025 09:04:18 +0800 Subject: [PATCH 25/25] Fix image formatting in README.md --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 95ddb14..fb979eb 100644 --- a/README.md +++ b/README.md @@ -56,8 +56,8 @@ - **现代化图形界面** - 操作更加直观友好 - **主题切换** - 可根据个人喜好选择主题 - **多架构支持** - 支持amd64和arm64架构 -- image -image + image + image ## 支持平台