commit a2f0e9337933d113d4e3884b4e2933c4a685007f Author: hbxnlsy Date: Thu Apr 2 21:19:42 2026 +0800 init diff --git a/README.md b/README.md new file mode 100644 index 0000000..ec7ef90 --- /dev/null +++ b/README.md @@ -0,0 +1 @@ +# XboxAutoRegister \ No newline at end of file diff --git a/XboxAutoRegister-main/README.md b/XboxAutoRegister-main/README.md new file mode 100644 index 0000000..ec7ef90 --- /dev/null +++ b/XboxAutoRegister-main/README.md @@ -0,0 +1 @@ +# XboxAutoRegister \ No newline at end of file diff --git a/XboxAutoRegister-main/calc_us_price.js b/XboxAutoRegister-main/calc_us_price.js new file mode 100644 index 0000000..1f31c73 --- /dev/null +++ b/XboxAutoRegister-main/calc_us_price.js @@ -0,0 +1,209 @@ +const readline = require('readline'); + +const rl = readline.createInterface({ + input: process.stdin, + output: process.stdout +}); + +// 终极破甲器:手动追踪跳转,继承 Cookie,半路截取游戏 ID +async function resolveGameId(startUrl) { + let currentUrl = startUrl; + let cookies = {}; + + for (let i = 0; i < 7; i++) { // 最多追踪 7 层跳转 + try { + // 拼接继承的 Cookie + const cookieHeader = Object.entries(cookies).map(([k, v]) => `${k}=${v}`).join('; '); + + const response = await fetch(currentUrl, { + redirect: 'manual', // 关键:关闭自动跳转,改为我们手动一步步跟 + headers: { + 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/122.0.0.0 Safari/537.36', + 'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8', + 'Accept-Language': 'en-US,en;q=0.5', + 'Cookie': cookieHeader + } + }); + + // 继承服务器发下来的 Cookie,伪装得更像真人 + const setCookieHeader = response.headers.get('set-cookie'); + if (setCookieHeader) { + const parts = setCookieHeader.split(/,(?=\s*[a-zA-Z0-9_-]+\s*=)/); + for (const part of parts) { + const cookiePair = part.split(';')[0]; + const [key, ...val] = cookiePair.split('='); + if (key && val) cookies[key.trim()] = val.join('=').trim(); + } + } + + if (response.status >= 300 && response.status < 400) { + const location = response.headers.get('location'); + if (!location) break; + + const nextUrl = location.startsWith('http') ? location : new URL(location, currentUrl).href; + + // 解密 URL,剥开追踪网的层层包装 + let decodedUrl = nextUrl; + try { decodedUrl = decodeURIComponent(decodedUrl); } catch(e){} + try { decodedUrl = decodeURIComponent(decodedUrl); } catch(e){} + + // ⭐️ 半路截胡:只要在跳转链接里发现了 9 开头的 12 位代码,直接带走,不再往下跳! + const idMatch = decodedUrl.match(/(?:\/|id=|ProductId=|bigIds=)([9][A-Za-z0-9]{11})(?:[\/?#&'"]|$)/i); + if (idMatch) return idMatch[1].toUpperCase(); + + currentUrl = nextUrl; + } else if (response.status === 200) { + const htmlText = await response.text(); + + // 搜刮网页源码,防备 JS 动态跳转 + const htmlMatch = htmlText.match(/(?:\/|id=|ProductId=|bigIds=)([9][A-Za-z0-9]{11})(?:[\/?#&'"]|$)/i); + if (htmlMatch) return htmlMatch[1].toUpperCase(); + + // 检查 Meta Refresh 自动跳转 + const metaRefresh = htmlText.match(/]*http-equiv=["']refresh["'][^>]*content=["']\d+;\s*url=([^"']+)["']/i); + if (metaRefresh) { + currentUrl = metaRefresh[1].replace(/&/g, '&'); + if (!currentUrl.startsWith('http')) currentUrl = new URL(currentUrl, startUrl).href; + continue; + } + + // 检查 JS 自动跳转 + const jsRedirect = htmlText.match(/(?:window\.)?location(?:\.href)?\s*=\s*['"]([^'"]+)['"]/i); + if (jsRedirect) { + currentUrl = jsRedirect[1]; + if (!currentUrl.startsWith('http')) currentUrl = new URL(currentUrl, startUrl).href; + continue; + } + + break; + } else { + break; + } + } catch (e) { + break; + } + } + return null; +} + +// 核心查询逻辑 +async function getUSGameData(startUrl) { + try { + const urlObj = new URL(startUrl); + urlObj.searchParams.set('r', 'en-us'); + + let bigId = await resolveGameId(urlObj.toString()); + + if (!bigId) { + return { success: false, reason: "防爬虫拦截,未能从底层剥离出 12 位游戏代码" }; + } + + const apiUrl = `https://displaycatalog.mp.microsoft.com/v7.0/products?bigIds=${bigId}&market=US&languages=en-us&MS-CV=DUMMY.1`; + + const apiResponse = await fetch(apiUrl); + const data = await apiResponse.json(); + + if (!data.Products || data.Products.length === 0) { + return { success: false, reason: `成功获取 ID (${bigId}),但美区查无此游戏数据` }; + } + + const product = data.Products[0]; + const gameName = product.LocalizedProperties?.[0]?.ProductTitle || "未知游戏"; + + let finalPrice = null; + + if (!product.DisplaySkuAvailabilities || product.DisplaySkuAvailabilities.length === 0) { + return { success: false, name: gameName, reason: "该游戏没有销售规格 (无法购买)" }; + } + + // 智能找买断价 + for (const skuObj of product.DisplaySkuAvailabilities) { + if (skuObj.Sku && (skuObj.Sku.SkuType === 'full' || skuObj.Sku.SkuType === 'dlc' || skuObj.Sku.SkuType === 'consumable')) { + for (const avail of skuObj.Availabilities || []) { + if (avail.Actions && avail.Actions.includes('Purchase') && avail.OrderManagementData?.Price !== undefined) { + finalPrice = avail.OrderManagementData.Price.ListPrice; + break; + } + } + } + if (finalPrice !== null) break; + } + + if (finalPrice === null) { + for (const skuObj of product.DisplaySkuAvailabilities) { + for (const avail of skuObj.Availabilities || []) { + if (avail.Actions && avail.Actions.includes('Purchase') && avail.OrderManagementData?.Price !== undefined) { + finalPrice = avail.OrderManagementData.Price.ListPrice; + break; + } + } + if (finalPrice !== null) break; + } + } + + if (finalPrice === null) { + return { success: false, name: gameName, reason: "只有 XGP 订阅试玩或捆绑包专属,无单买价格" }; + } + + return { success: true, name: gameName, price: finalPrice }; + } catch (e) { + return { success: false, reason: `发生异常: ${e.message}` }; + } +} + +const inputUrls = []; + +console.log('🎮 请粘贴你的 Xbox 链接串 (支持包含回车的多行文本)。'); +console.log('💡 提示:粘贴完成后,请在【新的一行】按一次回车开始计算:\n'); + +rl.on('line', (line) => { + const trimmedLine = line.trim(); + if (trimmedLine === '') { + if (inputUrls.length > 0) { + rl.close(); + processUrls(inputUrls); + } + return; + } + const splitUrls = trimmedLine.split(/(?=https?:\/\/)/).filter(u => u.startsWith('http')); + inputUrls.push(...splitUrls); +}); + +async function processUrls(urls) { + console.log(`\n✅ 成功读取到 ${urls.length} 个链接,开始逐个查询美区价格...\n`); + + let totalPrice = 0; + let successCount = 0; + const failedDetails = []; + + for (let i = 0; i < urls.length; i++) { + const url = urls[i]; + process.stdout.write(`[${i + 1}/${urls.length}] 正在查询... `); + + const result = await getUSGameData(url); + + if (result.success) { + console.log(`✅ 成功 | ${result.name} | 现价: $${result.price}`); + totalPrice += result.price; + successCount++; + } else { + const namePart = result.name ? `[${result.name}] ` : ""; + console.log(`❌ 失败 | ${namePart}原因: ${result.reason}`); + failedDetails.push({ url, reason: result.reason, name: result.name }); + } + } + + console.log("\n================ 结算单 ================"); + console.log(`总计识别: ${urls.length} 个游戏`); + console.log(`成功查询: ${successCount} 个游戏`); + console.log(`美元总价: $${totalPrice.toFixed(2)}`); + console.log("========================================\n"); + + if (failedDetails.length > 0) { + console.log("⚠️ 以下链接需要手动核查:"); + failedDetails.forEach((f, idx) => { + const nameStr = f.name ? `游戏: ${f.name}\n ` : ""; + console.log(`${idx + 1}. ${nameStr}原因: ${f.reason}\n 链接: ${f.url}`); + }); + } +} \ No newline at end of file diff --git a/XboxAutoRegister-main/geckodriver.exe b/XboxAutoRegister-main/geckodriver.exe new file mode 100644 index 0000000..59b6a49 Binary files /dev/null and b/XboxAutoRegister-main/geckodriver.exe differ diff --git a/XboxAutoRegister-main/rotate.ps1 b/XboxAutoRegister-main/rotate.ps1 new file mode 100644 index 0000000..797c36c --- /dev/null +++ b/XboxAutoRegister-main/rotate.ps1 @@ -0,0 +1,60 @@ +# ================= CONFIG ================= +$API_URL = "http://127.0.0.1:9090" +$SECRET = "8130899" +$GROUP = "Rotate" +# ========================================== + +# 1. Force UTF-8 Output +[Console]::OutputEncoding = [System.Text.Encoding]::UTF8 + +Write-Host "1. Connecting to Clash..." -ForegroundColor Cyan + +# Use WebClient (Stable & Fixes Encoding) +$wc = New-Object System.Net.WebClient +$wc.Encoding = [System.Text.Encoding]::UTF8 + +try { + # URL + $ListUrl = "$API_URL/proxies/$GROUP" + + # === Step A: Get List === + # Add Auth Header + if ($SECRET -ne "") { $wc.Headers.Add("Authorization", "Bearer $SECRET") } + + $JsonContent = $wc.DownloadString($ListUrl) + + $JsonObj = $JsonContent | ConvertFrom-Json + $NodeList = $JsonObj.all + + if (!$NodeList -or $NodeList.Count -eq 0) { + throw "Error: Group [$GROUP] is empty!" + } + Write-Host " > Found $($NodeList.Count) nodes." -ForegroundColor Green + + # === Step B: Pick Random === + $Target = $NodeList | Get-Random + Write-Host "2. Switching to: $Target" -ForegroundColor Cyan + + # === Step C: Send Switch Command === + $Payload = @{ name = $Target } | ConvertTo-Json -Compress + + # 【Critical Fix】Clear headers to remove old Auth, preventing duplicate headers + $wc.Headers.Clear() + + # Re-add Headers correctly + $wc.Headers.Add("Content-Type", "application/json") + if ($SECRET -ne "") { $wc.Headers.Add("Authorization", "Bearer $SECRET") } + + # Send PUT + $Response = $wc.UploadString($ListUrl, "PUT", $Payload) + + Write-Host "3. [SUCCESS] Switched to: $Target" -ForegroundColor Green + +} catch { + Write-Host "==== ERROR ====" -ForegroundColor Red + Write-Host $_.Exception.Message -ForegroundColor Yellow + if ($_.Exception.InnerException) { + Write-Host "Detail: $($_.Exception.InnerException.Message)" -ForegroundColor Yellow + } + exit 1 +} \ No newline at end of file diff --git a/XboxAutoRegister-main/run_bot.py b/XboxAutoRegister-main/run_bot.py new file mode 100644 index 0000000..884f775 --- /dev/null +++ b/XboxAutoRegister-main/run_bot.py @@ -0,0 +1,382 @@ +from selenium import webdriver +from selenium.webdriver.firefox.service import Service +from selenium.webdriver.firefox.options import Options +from selenium.webdriver.common.by import By +from selenium.webdriver.support.ui import WebDriverWait +from selenium.webdriver.support import expected_conditions as EC +import subprocess +import time +import os + +# === 配置区域 === +INPUT_CSV = r'input\outlook账号_part_2.csv' # 原始输入文件 +TEMP_RETRY_CSV = r'output\temp_retry.csv' # 第一轮失败存放处(复活赛的输入) +FINAL_FAILED_CSV = r'output\failed.csv' # 最终失败文件 +SUCCESS_CSV = r'output\success.csv' # 成功文件 + +POWERSHELL_SCRIPT = r"E:\ClashScript\rotate.ps1" +GECKODRIVER_PATH = "geckodriver.exe" +FIREFOX_BINARY_PATH = r"C:\Program Files\Mozilla Firefox\firefox.exe" + + +# ================= 工具函数 ================= + +def rotate_ip(): + """切换IP""" + print(">>> [系统] 正在切换 IP (后台运行中)...") + try: + subprocess.run( + ["powershell.exe", "-ExecutionPolicy", "Bypass", "-File", POWERSHELL_SCRIPT], + check=True, + shell=True + ) + print(">>> [系统] IP 切换完成,等待网络恢复...") + time.sleep(2) + except subprocess.CalledProcessError as e: + print(f"!!! IP 切换失败: {e}") + + +def append_to_csv(file_path, email, password): + """追加写入一行 CSV""" + file_exists = os.path.exists(file_path) + try: + with open(file_path, 'a', encoding='utf-8') as f: + if not file_exists: + f.write("卡号\n") + f.write(f"{email}----{password}\n") + f.flush() + except Exception as e: + print(f"写入文件 {file_path} 失败: {e}") + + +def read_file_lines(file_path): + """读取文件所有行""" + if not os.path.exists(file_path): + return [] + try: + with open(file_path, 'r', encoding='utf-8') as f: + return f.readlines() + except UnicodeDecodeError: + try: + with open(file_path, 'r', encoding='gb18030') as f: + return f.readlines() + except: + return [] + + +def rewrite_source_file(file_path, lines): + """重写源文件(用于删除行)""" + try: + with open(file_path, 'w', encoding='utf-8') as f: + f.writelines(lines) + except Exception as e: + print(f"!!! 更新源文件失败: {e}") + + +def parse_account(line): + """解析账号密码""" + line = line.strip() + if not line or "卡号" in line: + return None, None + + email = "" + pwd = "" + # 支持 ---- 分割 + if "----" in line: + parts = line.split("----") + email = parts[0].strip() + if len(parts) > 1: + pwd = parts[1].strip() + # 支持 , 分割 + elif "," in line: + parts = line.split(",") + email = parts[0].strip() + if len(parts) > 1: + pwd = parts[1].strip() + + if email and pwd: + return email, pwd + return None, None + + +def count_valid_accounts(file_path): + """统计有效账号数""" + lines = read_file_lines(file_path) + count = 0 + for line in lines: + e, _ = parse_account(line) + if e: + count += 1 + return count + + +def login_process(driver, email, password): + """ + 业务逻辑:登录 Xbox + 返回: True(成功) / False(失败) + """ + print(f"=== 开始处理: {email} ===") + + try: + driver.get("https://www.xbox.com/en-us/auth/msa?action=logIn") + + # 1. 输入账号 + try: + WebDriverWait(driver, 30).until( + EC.visibility_of_element_located((By.ID, "usernameEntry")) + ).send_keys(email) + except: + pass + + time.sleep(1) + try: + driver.find_element(By.XPATH, "//button[@data-testid='primaryButton']").click() + except: + pass + + # 2. 输入密码 + WebDriverWait(driver, 30).until( + EC.visibility_of_element_located((By.NAME, "passwd")) + ).send_keys(password.strip()) + + time.sleep(1.5) + driver.find_element(By.XPATH, "//button[@data-testid='primaryButton']").click() + + # === 3. URL检测循环 === + print(">>> 进入 URL 监控模式...") + loop_start_time = time.time() + + while True: + if time.time() - loop_start_time > 60: + print(">>> URL 检测超时 (60s),强制下一步") + break + + try: + current_url = driver.current_url + + if "xbox.com" in current_url: + print(f"√√√ 直接跳转到了 Xbox 首页,成功!") + return True + + if "account.live.com" in current_url or "login.live.com" in current_url: + try: + # 处理跳过按钮 + skip_btns = driver.find_elements(By.ID, "iShowSkip") + if skip_btns and skip_btns[0].is_displayed(): + print(">>> 点击 '跳过'...") + skip_btns[0].click() + time.sleep(2) + continue + + # 处理常规确认按钮 + primary_btns = driver.find_elements(By.XPATH, "//button[@data-testid='primaryButton']") + if primary_btns and primary_btns[0].is_displayed(): + print(f">>> 检测到主按钮,点击确认...") + break + except: + pass + time.sleep(1) + else: + break + except: + break + + # === 4. 后续确认流程 === + clicked_yes = False + try: + yes_btn = WebDriverWait(driver, 10).until( + EC.element_to_be_clickable((By.XPATH, "//button[@data-testid='primaryButton']")) + ) + yes_btn.click() + clicked_yes = True + except: + pass + + if clicked_yes: + time.sleep(3) + + # 点击 "保存并继续" + print(" [关键] 等待 '保存并继续' (60s)...") + try: + save_btn = WebDriverWait(driver, 60).until( + EC.element_to_be_clickable((By.XPATH, "//button[contains(., '保存并继续')]")) + ) + save_btn.click() + time.sleep(3) + except: + print(f" [失败] 未找到 '保存并继续'") + return False + + # 检测成功标志 + print(" [关键] 等待 '可选诊断数据' (60s)...") + try: + WebDriverWait(driver, 60).until( + EC.presence_of_element_located((By.XPATH, "//h1[contains(., '可选诊断数据')]")) + ) + print(f"√√√√√√ 成功!账号 {email} 处理完毕!") + return True + + except: + print(f" [失败] 未检测到成功标志") + return False + + except Exception as e: + print(f"!!! 发生未知异常: {e}") + return False + + +def run_process_loop(source_file, success_output, fail_output, round_name): + """ + 通用处理循环: + 1. 读取 source_file + 2. 处理一个 -> 删一个 + 3. 成功 -> success_output + 4. 失败 -> fail_output + """ + print(f"\n========== 启动 {round_name} ==========") + print(f"输入: {source_file}") + print(f"失败将存入: {fail_output}") + + # 1. 统计总数 + total_count = count_valid_accounts(source_file) + if total_count == 0: + print(f"✨ {round_name} 无待处理账号,跳过。") + return 0 + + print(f"📊 {round_name} 待处理任务数: {total_count}") + + processed_count = 0 + fail_count = 0 + + while True: + # 2. 读取文件寻找下一个 + all_lines = read_file_lines(source_file) + + target_line_index = -1 + email = None + password = None + + for i, line in enumerate(all_lines): + e, p = parse_account(line) + if e and p: + target_line_index = i + email = e + password = p + break + + # 3. 如果找不到,说明本轮结束 + if target_line_index == -1: + print(f"\n🎉 {round_name} 结束!(进度: {processed_count}/{total_count})") + break + + processed_count += 1 + print(f"\n--------------------------------------------------") + print(f"🚀 [{round_name}] 进度: {processed_count}/{total_count} | 账号: {email}") + print(f"--------------------------------------------------") + + driver = None + try: + rotate_ip() # 换IP + + # 启动浏览器 + options = Options() + options.binary_location = FIREFOX_BINARY_PATH + options.add_argument("-headless") # 无头模式 + options.add_argument("--width=1920") + options.add_argument("--height=1080") + options.set_preference("general.useragent.override", + "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:121.0) Gecko/20100101 Firefox/121.0") + options.add_argument("-private") + + # 性能优化参数 + options.set_preference("security.webauth.webauthn", False) + options.set_preference("security.webauth.u2f", False) + options.set_preference("signon.rememberSignons", False) + + service = Service(GECKODRIVER_PATH) + driver = webdriver.Firefox(service=service, options=options) + + # 执行 + is_success = login_process(driver, email, password) + + # 结果分流 + if is_success: + print(f"OOO 成功 -> 写入 {success_output}") + append_to_csv(success_output, email, password) + else: + print(f"XXX 失败 -> 写入 {fail_output}") + append_to_csv(fail_output, email, password) + fail_count += 1 + + except Exception as e: + print(f"!!! 运行异常: {e}") + append_to_csv(fail_output, email, password) + fail_count += 1 + + finally: + if driver: + try: + driver.quit() + except: + pass + + # 4. 【核心】从源文件移除该行(即时保存进度) + if target_line_index != -1 and target_line_index < len(all_lines): + check_e, _ = parse_account(all_lines[target_line_index]) + if check_e == email: + del all_lines[target_line_index] + rewrite_source_file(source_file, all_lines) + + time.sleep(2) + + # 循环结束,尝试删除源文件(如果已空) + final_lines = read_file_lines(source_file) + has_valid = any(parse_account(x)[0] for x in final_lines) + if not has_valid: + print(f"🗑️ {source_file} 已处理完毕,删除文件。") + try: + os.remove(source_file) + except: + pass + + return fail_count + + +def main(): + if not os.path.exists(FIREFOX_BINARY_PATH): + print(f"❌ 错误: 找不到 Firefox,请检查路径: {FIREFOX_BINARY_PATH}") + return + + # === 第一轮:初赛 === + # 输入: outlook账号.csv + # 失败去向: temp_retry.csv (临时复活池) + fail_round_1 = run_process_loop(INPUT_CSV, SUCCESS_CSV, TEMP_RETRY_CSV, "第一轮(初赛)") + + if fail_round_1 == 0: + print("\n🎉🎉🎉 第一轮全胜!无需复活赛。") + if os.path.exists(TEMP_RETRY_CSV): os.remove(TEMP_RETRY_CSV) + return + + # === 第二轮:复活赛 === + print(f"\n⚠️ 第一轮产生了 {fail_round_1} 个失败账号,准备进入复活赛...") + print("⏳ 等待 5 秒...") + time.sleep(5) + + # 输入: temp_retry.csv (第一轮的失败者) + # 失败去向: failed.csv (最终失败记录) + fail_round_2 = run_process_loop(TEMP_RETRY_CSV, SUCCESS_CSV, FINAL_FAILED_CSV, "第二轮(复活赛)") + + print(f"\n================ 最终统计 ================") + print(f"第一轮失败: {fail_round_1}") + print(f"第二轮救回: {fail_round_1 - fail_round_2}") + print(f"最终失败数: {fail_round_2}") + + if fail_round_2 == 0: + print("🎉 复活赛全部成功!") + if os.path.exists(FINAL_FAILED_CSV): os.remove(FINAL_FAILED_CSV) + else: + print(f"😭 仍有账号失败,请查看: {FINAL_FAILED_CSV}") + + +if __name__ == "__main__": + main() \ No newline at end of file diff --git a/XboxAutoRegister-main/split_csv.py b/XboxAutoRegister-main/split_csv.py new file mode 100644 index 0000000..cb860c5 --- /dev/null +++ b/XboxAutoRegister-main/split_csv.py @@ -0,0 +1,78 @@ +import os +import shutil + + +def split_to_input_folder(input_file, rows_per_file=15): + """ + 将原始 CSV 切分并存入 ./input/ 文件夹,完成后删除原文件 + """ + target_dir = "input" + + # 1. 检查源文件 + if not os.path.exists(input_file): + print(f"❌ 错误:找不到源文件 '{input_file}'") + return + + # 2. 创建 input 文件夹(如果不存在) + if not os.path.exists(target_dir): + os.makedirs(target_dir) + print(f"📁 已创建目录: {target_dir}") + + # 3. 读取内容 + try: + try: + with open(input_file, 'r', encoding='utf-8') as f: + lines = f.readlines() + except UnicodeDecodeError: + with open(input_file, 'r', encoding='gb18030') as f: + lines = f.readlines() + except Exception as e: + print(f"❌ 读取文件失败: {e}") + return + + if len(lines) < 2: + print("⚠️ 文件中没有足够的账号数据。") + return + + # 提取表头和数据行 + header = lines[0] + data_lines = [l for l in lines[1:] if l.strip()] + total_accounts = len(data_lines) + + print(f"📂 发现 {total_accounts} 个账号,准备切分...") + + # 4. 开始切分并写入 input 文件夹 + file_count = 0 + success_count = 0 + + for i in range(0, total_accounts, rows_per_file): + file_count += 1 + chunk = data_lines[i: i + rows_per_file] + + # 生成输出路径,例如: input/outlook账号_part_1.csv + base_name = os.path.splitext(os.path.basename(input_file))[0] + output_filename = f"{base_name}_part_{file_count}.csv" + output_path = os.path.join(target_dir, output_filename) + + try: + with open(output_path, 'w', encoding='utf-8') as f: + f.write(header) + f.writelines(chunk) + success_count += 1 + print(f"✅ 已存入: {output_path} ({len(chunk)} 个账号)") + except Exception as e: + print(f"❌ 写入 {output_path} 失败: {e}") + return # 安全起见,失败则不删除原文件 + + # 5. 彻底删除原大文件 + if success_count > 0: + try: + os.remove(input_file) + print(f"\n🚀 分割完成!共生成 {success_count} 个文件并存入 '{target_dir}' 文件夹。") + print(f"🗑️ 原始文件 '{input_file}' 已删除。") + except Exception as e: + print(f"\n⚠️ 子文件已生成,但删除原文件失败: {e}") + + +if __name__ == "__main__": + split_to_input_folder('outlook账号.csv', rows_per_file=15) \ No newline at end of file diff --git a/calc_us_price.js b/calc_us_price.js new file mode 100644 index 0000000..1f31c73 --- /dev/null +++ b/calc_us_price.js @@ -0,0 +1,209 @@ +const readline = require('readline'); + +const rl = readline.createInterface({ + input: process.stdin, + output: process.stdout +}); + +// 终极破甲器:手动追踪跳转,继承 Cookie,半路截取游戏 ID +async function resolveGameId(startUrl) { + let currentUrl = startUrl; + let cookies = {}; + + for (let i = 0; i < 7; i++) { // 最多追踪 7 层跳转 + try { + // 拼接继承的 Cookie + const cookieHeader = Object.entries(cookies).map(([k, v]) => `${k}=${v}`).join('; '); + + const response = await fetch(currentUrl, { + redirect: 'manual', // 关键:关闭自动跳转,改为我们手动一步步跟 + headers: { + 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/122.0.0.0 Safari/537.36', + 'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8', + 'Accept-Language': 'en-US,en;q=0.5', + 'Cookie': cookieHeader + } + }); + + // 继承服务器发下来的 Cookie,伪装得更像真人 + const setCookieHeader = response.headers.get('set-cookie'); + if (setCookieHeader) { + const parts = setCookieHeader.split(/,(?=\s*[a-zA-Z0-9_-]+\s*=)/); + for (const part of parts) { + const cookiePair = part.split(';')[0]; + const [key, ...val] = cookiePair.split('='); + if (key && val) cookies[key.trim()] = val.join('=').trim(); + } + } + + if (response.status >= 300 && response.status < 400) { + const location = response.headers.get('location'); + if (!location) break; + + const nextUrl = location.startsWith('http') ? location : new URL(location, currentUrl).href; + + // 解密 URL,剥开追踪网的层层包装 + let decodedUrl = nextUrl; + try { decodedUrl = decodeURIComponent(decodedUrl); } catch(e){} + try { decodedUrl = decodeURIComponent(decodedUrl); } catch(e){} + + // ⭐️ 半路截胡:只要在跳转链接里发现了 9 开头的 12 位代码,直接带走,不再往下跳! + const idMatch = decodedUrl.match(/(?:\/|id=|ProductId=|bigIds=)([9][A-Za-z0-9]{11})(?:[\/?#&'"]|$)/i); + if (idMatch) return idMatch[1].toUpperCase(); + + currentUrl = nextUrl; + } else if (response.status === 200) { + const htmlText = await response.text(); + + // 搜刮网页源码,防备 JS 动态跳转 + const htmlMatch = htmlText.match(/(?:\/|id=|ProductId=|bigIds=)([9][A-Za-z0-9]{11})(?:[\/?#&'"]|$)/i); + if (htmlMatch) return htmlMatch[1].toUpperCase(); + + // 检查 Meta Refresh 自动跳转 + const metaRefresh = htmlText.match(/]*http-equiv=["']refresh["'][^>]*content=["']\d+;\s*url=([^"']+)["']/i); + if (metaRefresh) { + currentUrl = metaRefresh[1].replace(/&/g, '&'); + if (!currentUrl.startsWith('http')) currentUrl = new URL(currentUrl, startUrl).href; + continue; + } + + // 检查 JS 自动跳转 + const jsRedirect = htmlText.match(/(?:window\.)?location(?:\.href)?\s*=\s*['"]([^'"]+)['"]/i); + if (jsRedirect) { + currentUrl = jsRedirect[1]; + if (!currentUrl.startsWith('http')) currentUrl = new URL(currentUrl, startUrl).href; + continue; + } + + break; + } else { + break; + } + } catch (e) { + break; + } + } + return null; +} + +// 核心查询逻辑 +async function getUSGameData(startUrl) { + try { + const urlObj = new URL(startUrl); + urlObj.searchParams.set('r', 'en-us'); + + let bigId = await resolveGameId(urlObj.toString()); + + if (!bigId) { + return { success: false, reason: "防爬虫拦截,未能从底层剥离出 12 位游戏代码" }; + } + + const apiUrl = `https://displaycatalog.mp.microsoft.com/v7.0/products?bigIds=${bigId}&market=US&languages=en-us&MS-CV=DUMMY.1`; + + const apiResponse = await fetch(apiUrl); + const data = await apiResponse.json(); + + if (!data.Products || data.Products.length === 0) { + return { success: false, reason: `成功获取 ID (${bigId}),但美区查无此游戏数据` }; + } + + const product = data.Products[0]; + const gameName = product.LocalizedProperties?.[0]?.ProductTitle || "未知游戏"; + + let finalPrice = null; + + if (!product.DisplaySkuAvailabilities || product.DisplaySkuAvailabilities.length === 0) { + return { success: false, name: gameName, reason: "该游戏没有销售规格 (无法购买)" }; + } + + // 智能找买断价 + for (const skuObj of product.DisplaySkuAvailabilities) { + if (skuObj.Sku && (skuObj.Sku.SkuType === 'full' || skuObj.Sku.SkuType === 'dlc' || skuObj.Sku.SkuType === 'consumable')) { + for (const avail of skuObj.Availabilities || []) { + if (avail.Actions && avail.Actions.includes('Purchase') && avail.OrderManagementData?.Price !== undefined) { + finalPrice = avail.OrderManagementData.Price.ListPrice; + break; + } + } + } + if (finalPrice !== null) break; + } + + if (finalPrice === null) { + for (const skuObj of product.DisplaySkuAvailabilities) { + for (const avail of skuObj.Availabilities || []) { + if (avail.Actions && avail.Actions.includes('Purchase') && avail.OrderManagementData?.Price !== undefined) { + finalPrice = avail.OrderManagementData.Price.ListPrice; + break; + } + } + if (finalPrice !== null) break; + } + } + + if (finalPrice === null) { + return { success: false, name: gameName, reason: "只有 XGP 订阅试玩或捆绑包专属,无单买价格" }; + } + + return { success: true, name: gameName, price: finalPrice }; + } catch (e) { + return { success: false, reason: `发生异常: ${e.message}` }; + } +} + +const inputUrls = []; + +console.log('🎮 请粘贴你的 Xbox 链接串 (支持包含回车的多行文本)。'); +console.log('💡 提示:粘贴完成后,请在【新的一行】按一次回车开始计算:\n'); + +rl.on('line', (line) => { + const trimmedLine = line.trim(); + if (trimmedLine === '') { + if (inputUrls.length > 0) { + rl.close(); + processUrls(inputUrls); + } + return; + } + const splitUrls = trimmedLine.split(/(?=https?:\/\/)/).filter(u => u.startsWith('http')); + inputUrls.push(...splitUrls); +}); + +async function processUrls(urls) { + console.log(`\n✅ 成功读取到 ${urls.length} 个链接,开始逐个查询美区价格...\n`); + + let totalPrice = 0; + let successCount = 0; + const failedDetails = []; + + for (let i = 0; i < urls.length; i++) { + const url = urls[i]; + process.stdout.write(`[${i + 1}/${urls.length}] 正在查询... `); + + const result = await getUSGameData(url); + + if (result.success) { + console.log(`✅ 成功 | ${result.name} | 现价: $${result.price}`); + totalPrice += result.price; + successCount++; + } else { + const namePart = result.name ? `[${result.name}] ` : ""; + console.log(`❌ 失败 | ${namePart}原因: ${result.reason}`); + failedDetails.push({ url, reason: result.reason, name: result.name }); + } + } + + console.log("\n================ 结算单 ================"); + console.log(`总计识别: ${urls.length} 个游戏`); + console.log(`成功查询: ${successCount} 个游戏`); + console.log(`美元总价: $${totalPrice.toFixed(2)}`); + console.log("========================================\n"); + + if (failedDetails.length > 0) { + console.log("⚠️ 以下链接需要手动核查:"); + failedDetails.forEach((f, idx) => { + const nameStr = f.name ? `游戏: ${f.name}\n ` : ""; + console.log(`${idx + 1}. ${nameStr}原因: ${f.reason}\n 链接: ${f.url}`); + }); + } +} \ No newline at end of file diff --git a/geckodriver.exe b/geckodriver.exe new file mode 100644 index 0000000..59b6a49 Binary files /dev/null and b/geckodriver.exe differ diff --git a/rotate.ps1 b/rotate.ps1 new file mode 100644 index 0000000..797c36c --- /dev/null +++ b/rotate.ps1 @@ -0,0 +1,60 @@ +# ================= CONFIG ================= +$API_URL = "http://127.0.0.1:9090" +$SECRET = "8130899" +$GROUP = "Rotate" +# ========================================== + +# 1. Force UTF-8 Output +[Console]::OutputEncoding = [System.Text.Encoding]::UTF8 + +Write-Host "1. Connecting to Clash..." -ForegroundColor Cyan + +# Use WebClient (Stable & Fixes Encoding) +$wc = New-Object System.Net.WebClient +$wc.Encoding = [System.Text.Encoding]::UTF8 + +try { + # URL + $ListUrl = "$API_URL/proxies/$GROUP" + + # === Step A: Get List === + # Add Auth Header + if ($SECRET -ne "") { $wc.Headers.Add("Authorization", "Bearer $SECRET") } + + $JsonContent = $wc.DownloadString($ListUrl) + + $JsonObj = $JsonContent | ConvertFrom-Json + $NodeList = $JsonObj.all + + if (!$NodeList -or $NodeList.Count -eq 0) { + throw "Error: Group [$GROUP] is empty!" + } + Write-Host " > Found $($NodeList.Count) nodes." -ForegroundColor Green + + # === Step B: Pick Random === + $Target = $NodeList | Get-Random + Write-Host "2. Switching to: $Target" -ForegroundColor Cyan + + # === Step C: Send Switch Command === + $Payload = @{ name = $Target } | ConvertTo-Json -Compress + + # 【Critical Fix】Clear headers to remove old Auth, preventing duplicate headers + $wc.Headers.Clear() + + # Re-add Headers correctly + $wc.Headers.Add("Content-Type", "application/json") + if ($SECRET -ne "") { $wc.Headers.Add("Authorization", "Bearer $SECRET") } + + # Send PUT + $Response = $wc.UploadString($ListUrl, "PUT", $Payload) + + Write-Host "3. [SUCCESS] Switched to: $Target" -ForegroundColor Green + +} catch { + Write-Host "==== ERROR ====" -ForegroundColor Red + Write-Host $_.Exception.Message -ForegroundColor Yellow + if ($_.Exception.InnerException) { + Write-Host "Detail: $($_.Exception.InnerException.Message)" -ForegroundColor Yellow + } + exit 1 +} \ No newline at end of file diff --git a/run_bot.py b/run_bot.py new file mode 100644 index 0000000..884f775 --- /dev/null +++ b/run_bot.py @@ -0,0 +1,382 @@ +from selenium import webdriver +from selenium.webdriver.firefox.service import Service +from selenium.webdriver.firefox.options import Options +from selenium.webdriver.common.by import By +from selenium.webdriver.support.ui import WebDriverWait +from selenium.webdriver.support import expected_conditions as EC +import subprocess +import time +import os + +# === 配置区域 === +INPUT_CSV = r'input\outlook账号_part_2.csv' # 原始输入文件 +TEMP_RETRY_CSV = r'output\temp_retry.csv' # 第一轮失败存放处(复活赛的输入) +FINAL_FAILED_CSV = r'output\failed.csv' # 最终失败文件 +SUCCESS_CSV = r'output\success.csv' # 成功文件 + +POWERSHELL_SCRIPT = r"E:\ClashScript\rotate.ps1" +GECKODRIVER_PATH = "geckodriver.exe" +FIREFOX_BINARY_PATH = r"C:\Program Files\Mozilla Firefox\firefox.exe" + + +# ================= 工具函数 ================= + +def rotate_ip(): + """切换IP""" + print(">>> [系统] 正在切换 IP (后台运行中)...") + try: + subprocess.run( + ["powershell.exe", "-ExecutionPolicy", "Bypass", "-File", POWERSHELL_SCRIPT], + check=True, + shell=True + ) + print(">>> [系统] IP 切换完成,等待网络恢复...") + time.sleep(2) + except subprocess.CalledProcessError as e: + print(f"!!! IP 切换失败: {e}") + + +def append_to_csv(file_path, email, password): + """追加写入一行 CSV""" + file_exists = os.path.exists(file_path) + try: + with open(file_path, 'a', encoding='utf-8') as f: + if not file_exists: + f.write("卡号\n") + f.write(f"{email}----{password}\n") + f.flush() + except Exception as e: + print(f"写入文件 {file_path} 失败: {e}") + + +def read_file_lines(file_path): + """读取文件所有行""" + if not os.path.exists(file_path): + return [] + try: + with open(file_path, 'r', encoding='utf-8') as f: + return f.readlines() + except UnicodeDecodeError: + try: + with open(file_path, 'r', encoding='gb18030') as f: + return f.readlines() + except: + return [] + + +def rewrite_source_file(file_path, lines): + """重写源文件(用于删除行)""" + try: + with open(file_path, 'w', encoding='utf-8') as f: + f.writelines(lines) + except Exception as e: + print(f"!!! 更新源文件失败: {e}") + + +def parse_account(line): + """解析账号密码""" + line = line.strip() + if not line or "卡号" in line: + return None, None + + email = "" + pwd = "" + # 支持 ---- 分割 + if "----" in line: + parts = line.split("----") + email = parts[0].strip() + if len(parts) > 1: + pwd = parts[1].strip() + # 支持 , 分割 + elif "," in line: + parts = line.split(",") + email = parts[0].strip() + if len(parts) > 1: + pwd = parts[1].strip() + + if email and pwd: + return email, pwd + return None, None + + +def count_valid_accounts(file_path): + """统计有效账号数""" + lines = read_file_lines(file_path) + count = 0 + for line in lines: + e, _ = parse_account(line) + if e: + count += 1 + return count + + +def login_process(driver, email, password): + """ + 业务逻辑:登录 Xbox + 返回: True(成功) / False(失败) + """ + print(f"=== 开始处理: {email} ===") + + try: + driver.get("https://www.xbox.com/en-us/auth/msa?action=logIn") + + # 1. 输入账号 + try: + WebDriverWait(driver, 30).until( + EC.visibility_of_element_located((By.ID, "usernameEntry")) + ).send_keys(email) + except: + pass + + time.sleep(1) + try: + driver.find_element(By.XPATH, "//button[@data-testid='primaryButton']").click() + except: + pass + + # 2. 输入密码 + WebDriverWait(driver, 30).until( + EC.visibility_of_element_located((By.NAME, "passwd")) + ).send_keys(password.strip()) + + time.sleep(1.5) + driver.find_element(By.XPATH, "//button[@data-testid='primaryButton']").click() + + # === 3. URL检测循环 === + print(">>> 进入 URL 监控模式...") + loop_start_time = time.time() + + while True: + if time.time() - loop_start_time > 60: + print(">>> URL 检测超时 (60s),强制下一步") + break + + try: + current_url = driver.current_url + + if "xbox.com" in current_url: + print(f"√√√ 直接跳转到了 Xbox 首页,成功!") + return True + + if "account.live.com" in current_url or "login.live.com" in current_url: + try: + # 处理跳过按钮 + skip_btns = driver.find_elements(By.ID, "iShowSkip") + if skip_btns and skip_btns[0].is_displayed(): + print(">>> 点击 '跳过'...") + skip_btns[0].click() + time.sleep(2) + continue + + # 处理常规确认按钮 + primary_btns = driver.find_elements(By.XPATH, "//button[@data-testid='primaryButton']") + if primary_btns and primary_btns[0].is_displayed(): + print(f">>> 检测到主按钮,点击确认...") + break + except: + pass + time.sleep(1) + else: + break + except: + break + + # === 4. 后续确认流程 === + clicked_yes = False + try: + yes_btn = WebDriverWait(driver, 10).until( + EC.element_to_be_clickable((By.XPATH, "//button[@data-testid='primaryButton']")) + ) + yes_btn.click() + clicked_yes = True + except: + pass + + if clicked_yes: + time.sleep(3) + + # 点击 "保存并继续" + print(" [关键] 等待 '保存并继续' (60s)...") + try: + save_btn = WebDriverWait(driver, 60).until( + EC.element_to_be_clickable((By.XPATH, "//button[contains(., '保存并继续')]")) + ) + save_btn.click() + time.sleep(3) + except: + print(f" [失败] 未找到 '保存并继续'") + return False + + # 检测成功标志 + print(" [关键] 等待 '可选诊断数据' (60s)...") + try: + WebDriverWait(driver, 60).until( + EC.presence_of_element_located((By.XPATH, "//h1[contains(., '可选诊断数据')]")) + ) + print(f"√√√√√√ 成功!账号 {email} 处理完毕!") + return True + + except: + print(f" [失败] 未检测到成功标志") + return False + + except Exception as e: + print(f"!!! 发生未知异常: {e}") + return False + + +def run_process_loop(source_file, success_output, fail_output, round_name): + """ + 通用处理循环: + 1. 读取 source_file + 2. 处理一个 -> 删一个 + 3. 成功 -> success_output + 4. 失败 -> fail_output + """ + print(f"\n========== 启动 {round_name} ==========") + print(f"输入: {source_file}") + print(f"失败将存入: {fail_output}") + + # 1. 统计总数 + total_count = count_valid_accounts(source_file) + if total_count == 0: + print(f"✨ {round_name} 无待处理账号,跳过。") + return 0 + + print(f"📊 {round_name} 待处理任务数: {total_count}") + + processed_count = 0 + fail_count = 0 + + while True: + # 2. 读取文件寻找下一个 + all_lines = read_file_lines(source_file) + + target_line_index = -1 + email = None + password = None + + for i, line in enumerate(all_lines): + e, p = parse_account(line) + if e and p: + target_line_index = i + email = e + password = p + break + + # 3. 如果找不到,说明本轮结束 + if target_line_index == -1: + print(f"\n🎉 {round_name} 结束!(进度: {processed_count}/{total_count})") + break + + processed_count += 1 + print(f"\n--------------------------------------------------") + print(f"🚀 [{round_name}] 进度: {processed_count}/{total_count} | 账号: {email}") + print(f"--------------------------------------------------") + + driver = None + try: + rotate_ip() # 换IP + + # 启动浏览器 + options = Options() + options.binary_location = FIREFOX_BINARY_PATH + options.add_argument("-headless") # 无头模式 + options.add_argument("--width=1920") + options.add_argument("--height=1080") + options.set_preference("general.useragent.override", + "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:121.0) Gecko/20100101 Firefox/121.0") + options.add_argument("-private") + + # 性能优化参数 + options.set_preference("security.webauth.webauthn", False) + options.set_preference("security.webauth.u2f", False) + options.set_preference("signon.rememberSignons", False) + + service = Service(GECKODRIVER_PATH) + driver = webdriver.Firefox(service=service, options=options) + + # 执行 + is_success = login_process(driver, email, password) + + # 结果分流 + if is_success: + print(f"OOO 成功 -> 写入 {success_output}") + append_to_csv(success_output, email, password) + else: + print(f"XXX 失败 -> 写入 {fail_output}") + append_to_csv(fail_output, email, password) + fail_count += 1 + + except Exception as e: + print(f"!!! 运行异常: {e}") + append_to_csv(fail_output, email, password) + fail_count += 1 + + finally: + if driver: + try: + driver.quit() + except: + pass + + # 4. 【核心】从源文件移除该行(即时保存进度) + if target_line_index != -1 and target_line_index < len(all_lines): + check_e, _ = parse_account(all_lines[target_line_index]) + if check_e == email: + del all_lines[target_line_index] + rewrite_source_file(source_file, all_lines) + + time.sleep(2) + + # 循环结束,尝试删除源文件(如果已空) + final_lines = read_file_lines(source_file) + has_valid = any(parse_account(x)[0] for x in final_lines) + if not has_valid: + print(f"🗑️ {source_file} 已处理完毕,删除文件。") + try: + os.remove(source_file) + except: + pass + + return fail_count + + +def main(): + if not os.path.exists(FIREFOX_BINARY_PATH): + print(f"❌ 错误: 找不到 Firefox,请检查路径: {FIREFOX_BINARY_PATH}") + return + + # === 第一轮:初赛 === + # 输入: outlook账号.csv + # 失败去向: temp_retry.csv (临时复活池) + fail_round_1 = run_process_loop(INPUT_CSV, SUCCESS_CSV, TEMP_RETRY_CSV, "第一轮(初赛)") + + if fail_round_1 == 0: + print("\n🎉🎉🎉 第一轮全胜!无需复活赛。") + if os.path.exists(TEMP_RETRY_CSV): os.remove(TEMP_RETRY_CSV) + return + + # === 第二轮:复活赛 === + print(f"\n⚠️ 第一轮产生了 {fail_round_1} 个失败账号,准备进入复活赛...") + print("⏳ 等待 5 秒...") + time.sleep(5) + + # 输入: temp_retry.csv (第一轮的失败者) + # 失败去向: failed.csv (最终失败记录) + fail_round_2 = run_process_loop(TEMP_RETRY_CSV, SUCCESS_CSV, FINAL_FAILED_CSV, "第二轮(复活赛)") + + print(f"\n================ 最终统计 ================") + print(f"第一轮失败: {fail_round_1}") + print(f"第二轮救回: {fail_round_1 - fail_round_2}") + print(f"最终失败数: {fail_round_2}") + + if fail_round_2 == 0: + print("🎉 复活赛全部成功!") + if os.path.exists(FINAL_FAILED_CSV): os.remove(FINAL_FAILED_CSV) + else: + print(f"😭 仍有账号失败,请查看: {FINAL_FAILED_CSV}") + + +if __name__ == "__main__": + main() \ No newline at end of file diff --git a/split_csv.py b/split_csv.py new file mode 100644 index 0000000..cb860c5 --- /dev/null +++ b/split_csv.py @@ -0,0 +1,78 @@ +import os +import shutil + + +def split_to_input_folder(input_file, rows_per_file=15): + """ + 将原始 CSV 切分并存入 ./input/ 文件夹,完成后删除原文件 + """ + target_dir = "input" + + # 1. 检查源文件 + if not os.path.exists(input_file): + print(f"❌ 错误:找不到源文件 '{input_file}'") + return + + # 2. 创建 input 文件夹(如果不存在) + if not os.path.exists(target_dir): + os.makedirs(target_dir) + print(f"📁 已创建目录: {target_dir}") + + # 3. 读取内容 + try: + try: + with open(input_file, 'r', encoding='utf-8') as f: + lines = f.readlines() + except UnicodeDecodeError: + with open(input_file, 'r', encoding='gb18030') as f: + lines = f.readlines() + except Exception as e: + print(f"❌ 读取文件失败: {e}") + return + + if len(lines) < 2: + print("⚠️ 文件中没有足够的账号数据。") + return + + # 提取表头和数据行 + header = lines[0] + data_lines = [l for l in lines[1:] if l.strip()] + total_accounts = len(data_lines) + + print(f"📂 发现 {total_accounts} 个账号,准备切分...") + + # 4. 开始切分并写入 input 文件夹 + file_count = 0 + success_count = 0 + + for i in range(0, total_accounts, rows_per_file): + file_count += 1 + chunk = data_lines[i: i + rows_per_file] + + # 生成输出路径,例如: input/outlook账号_part_1.csv + base_name = os.path.splitext(os.path.basename(input_file))[0] + output_filename = f"{base_name}_part_{file_count}.csv" + output_path = os.path.join(target_dir, output_filename) + + try: + with open(output_path, 'w', encoding='utf-8') as f: + f.write(header) + f.writelines(chunk) + success_count += 1 + print(f"✅ 已存入: {output_path} ({len(chunk)} 个账号)") + except Exception as e: + print(f"❌ 写入 {output_path} 失败: {e}") + return # 安全起见,失败则不删除原文件 + + # 5. 彻底删除原大文件 + if success_count > 0: + try: + os.remove(input_file) + print(f"\n🚀 分割完成!共生成 {success_count} 个文件并存入 '{target_dir}' 文件夹。") + print(f"🗑️ 原始文件 '{input_file}' 已删除。") + except Exception as e: + print(f"\n⚠️ 子文件已生成,但删除原文件失败: {e}") + + +if __name__ == "__main__": + split_to_input_folder('outlook账号.csv', rows_per_file=15) \ No newline at end of file