Просмотр исходного кода

Add need_confirmation response handling

Made-with: Cursor
wuzilong 6 месяцев назад
Родитель
Сommit
fc83959868

BIN
client-tauri/SteamSH_Portable.zip


+ 118 - 0
client-tauri/build-portable.ps1

@@ -0,0 +1,118 @@
+# 创建 SteamSH 便携式自解压包
+# 将 exe 打包成双击即可运行的便携版
+
+param(
+    [string]$SourceExe = "steamSH_standalone.exe",
+    [string]$OutputFile = "SteamSH_Portable.exe"
+)
+
+$ErrorActionPreference = "Stop"
+[Console]::OutputEncoding = [System.Text.Encoding]::UTF8
+
+Write-Host "========================================" -ForegroundColor Cyan
+Write-Host "SteamSH 便携版打包工具" -ForegroundColor Cyan
+Write-Host "========================================" -ForegroundColor Cyan
+
+# 检查输入文件
+Write-Host "`n[1/4] 检查输入文件..." -ForegroundColor Yellow
+if (-not (Test-Path $SourceExe)) {
+    Write-Host "错误: 找不到源文件 $SourceExe" -ForegroundColor Red
+    Write-Host "请先运行构建脚本生成 exe 文件" -ForegroundColor Yellow
+    exit 1
+}
+Write-Host "  ✓ 找到源文件: $SourceExe" -ForegroundColor Green
+
+# 创建临时目录
+Write-Host "`n[2/4] 准备便携包内容..." -ForegroundColor Yellow
+$tempDir = Join-Path $env:TEMP "SteamSH_Portable_$(Get-Date -Format 'yyyyMMdd_HHmmss')"
+New-Item -ItemType Directory -Path $tempDir -Force | Out-Null
+
+# 复制 exe
+Copy-Item $SourceExe (Join-Path $tempDir "steamSH.exe") -Force
+Write-Host "  ✓ 已复制主程序" -ForegroundColor Green
+
+# 创建启动脚本
+$startBat = @'
+@echo off
+chcp 65001 >nul
+echo 正在启动 SteamSH...
+cd /d "%~dp0"
+start "" "steamSH.exe"
+'@
+$startBat | Out-File (Join-Path $tempDir "启动SteamSH.bat") -Encoding UTF8
+Write-Host "  ✓ 已创建启动脚本" -ForegroundColor Green
+
+# 创建说明文件
+$readme = @'
+SteamSH 便携版
+=============
+
+使用说明:
+1. 双击 "启动SteamSH.bat" 即可启动程序
+2. 或直接运行 steamSH.exe
+
+注意:
+- 这是独立可执行文件,无需安装任何依赖
+- 所有数据会存储在用户目录下
+- 可以直接复制到U盘或移动设备使用
+'@
+$readme | Out-File (Join-Path $tempDir "说明.txt") -Encoding UTF8
+Write-Host "  ✓ 已创建说明文件" -ForegroundColor Green
+
+# 压缩为 zip
+Write-Host "`n[3/4] 创建压缩包..." -ForegroundColor Yellow
+$zipFile = "SteamSH_Portable.zip"
+if (Test-Path $zipFile) { Remove-Item $zipFile -Force }
+
+Compress-Archive -Path (Join-Path $tempDir "*") -DestinationPath $zipFile -Force
+Write-Host "  ✓ 已创建 $zipFile" -ForegroundColor Green
+
+# 查找 7z SFX 模块
+Write-Host "`n[4/4] 创建自解压 exe..." -ForegroundColor Yellow
+$sfxModule = $null
+$possiblePaths = @(
+    "C:\Program Files\7-Zip\7zSD.sfx",
+    "C:\Program Files\2345Soft\HaoZip\sfx\HaoZip7zSetup.sfx",
+    "C:\Program Files\2345Soft\HaoZip\sfx\HaoZip7zCon.sfx"
+)
+
+foreach ($path in $possiblePaths) {
+    if (Test-Path $path) {
+        $sfxModule = $path
+        Write-Host "  ✓ 找到 SFX 模块: $sfxModule" -ForegroundColor Green
+        break
+    }
+}
+
+if (-not $sfxModule) {
+    Write-Host "`n警告: 未找到 7-Zip SFX 模块" -ForegroundColor Yellow
+    Write-Host "将只生成 ZIP 压缩包" -ForegroundColor Yellow
+    Write-Host "`n提示: 安装 7-Zip 后可创建自解压 exe" -ForegroundColor Cyan
+    Write-Host "下载地址: https://www.7-zip.org/" -ForegroundColor Cyan
+    exit 0
+}
+
+# 拼接 SFX
+try {
+    $sfxBytes = [System.IO.File]::ReadAllBytes($sfxModule)
+    $zipBytes = [System.IO.File]::ReadAllBytes($zipFile)
+    $outputBytes = $sfxBytes + $zipBytes
+    [System.IO.File]::WriteAllBytes($OutputFile, $outputBytes)
+
+    $sizeMB = [math]::Round((Get-Item $OutputFile).Length / 1MB, 2)
+    Write-Host "`n========================================" -ForegroundColor Green
+    Write-Host "便携包构建完成!" -ForegroundColor Green
+    Write-Host "========================================" -ForegroundColor Green
+    Write-Host "`n输出文件:" -ForegroundColor White
+    Write-Host "  ZIP 包: $zipFile ($([math]::Round((Get-Item $zipFile).Length / 1MB, 2)) MB)" -ForegroundColor Cyan
+    Write-Host "  自解压 exe: $OutputFile ($sizeMB MB)" -ForegroundColor Green
+    Write-Host "`n分发方式:" -ForegroundColor White
+    Write-Host "  1. 直接分发 $OutputFile 给用户" -ForegroundColor White
+    Write-Host "  2. 用户双击即可自动解压并启动" -ForegroundColor White
+} catch {
+    Write-Host "错误: 创建 SFX 失败 - $_" -ForegroundColor Red
+    exit 1
+}
+
+# 清理临时文件
+Remove-Item $tempDir -Recurse -Force -ErrorAction SilentlyContinue

+ 27 - 0
client-tauri/src-tauri/src/commands/buff_sweep_runner.rs

@@ -1366,6 +1366,21 @@ async fn http_post_form_steam(
                     .unwrap_or_default();
                 let elapsed_ms = request_start.elapsed().as_millis() as u64;
 
+                // 检查是否是需要确认的响应(406 + need_confirmation=true)
+                // 这类响应不应重试,应交给上层处理确认逻辑
+                let is_confirmation_needed = status == reqwest::StatusCode::NOT_ACCEPTABLE
+                    && text.contains("need_confirmation")
+                    && text.contains("true");
+                if is_confirmation_needed {
+                    let confirm_msg = format!(
+                        "[sweep:{}] Steam POST 返回需要确认(406),不重试直接返回",
+                        label
+                    );
+                    info!("{}", confirm_msg);
+                    sweep_log_diag(sweep_file, rid, "info", &confirm_msg).await;
+                    return Ok((status, text));
+                }
+
                 // 只有失败的请求才记录日志
                 if !status.is_success() {
                     log_api_call_error(
@@ -1802,6 +1817,18 @@ async fn post_buylisting_with_retry(
                 // 解析 JSON
                 match serde_json::from_str::<Value>(&text) {
                     Ok(parsed) => {
+                        // 检查是否是需要确认的响应(406 + need_confirmation=true)
+                        // 这类响应不应重试,应直接返回
+                        if status == reqwest::StatusCode::NOT_ACCEPTABLE
+                            && parsed.get("need_confirmation") == Some(&json!(true))
+                        {
+                            info!(
+                                "[buy2] listing_id={} confirmation_id={} 返回需要确认(406) attempt={}/{}",
+                                listing_id, confirmation, attempt, max_retries
+                            );
+                            return Ok((status, text, parsed));
+                        }
+
                         // 检查是否购买成功
                         if let Some(w) = parsed.get("wallet_info").and_then(|x| x.as_object()) {
                             if w.get("success").and_then(|x| x.as_i64()) == Some(1) {

+ 3 - 0
client-tauri/src/views/HomeView.vue

@@ -65,6 +65,9 @@
                 <KeyOutlined /> 修改密码
               </a-menu-item>
               <a-menu-divider />
+              <a-menu-item key="version" disabled style="color: #999; cursor: default;">
+                <span style="font-size: 12px;">版本 V1.0.3.B</span>
+              </a-menu-item>
               <a-menu-item key="logout">
                 <LogoutOutlined /> 退出登录
               </a-menu-item>

+ 10 - 0
client-tauri/start.bat

@@ -0,0 +1,10 @@
+# SteamSH 便携版启动器
+# 此文件用于启动 SteamSH 主程序
+
+@echo off
+chcp 65001 >nul
+echo 正在启动 SteamSH...
+echo.
+
+cd /d "%~dp0"
+start "" "steamSH.exe"

+ 163 - 0
docs/test/debug_buy_502.py

@@ -0,0 +1,163 @@
+"""
+Steam Market Buy API 调试脚本
+复现 502 错误场景
+
+日志信息:
+- URL: https://steamcommunity.com/market/buylisting/634586007392692709
+- listing_id: 634586007392692709
+- goods_id: 35820
+- goods_name: 新星 | 毒蜥 (略有磨损)
+- steam_account: cea10388
+- target_price_usd: 5.5
+"""
+
+import requests
+import urllib.parse
+import json
+from datetime import datetime
+
+# 请求配置
+LISTING_ID = "689755100902207377"
+SESSION_ID = "C859D89325CCF1A3963504BBA006038D"
+STEAM_LOGIN_SECURE = (
+    "76561199354580595%7C%7CeyAidHlwIjogIkpXVCIsICJhbGciOiAiRWREU0EiIH0."
+    "eyAiaXNzIjogInI6MDAwRl8yODA5QzM3N182NERDMyIsICJzdWIiOiAiNzY1NjExOTkzNTQ1ODA1OTUiLCAiYXVkIjogWyAiY2xpZW50IiwgIndlYiIgXSwgImV4cCI6IDE3NzY2MTA2NDAsICJuYmYiOiAxNzY3ODgzNjc1LCAiaWF0IjogMTc3NjUyMzY3NSwgImp0aSI6ICIwMDEyXzI4MDlDMUQ0X0I0MEM0IiwgIm9hdCI6IDE3NzY1MjM2NzUsICJydF9leHAiOiAxNzc5MTMwMjMyLCAicGVyIjogMCwgImlwX3N1YmplY3QiOiAiMTAzLjEwNi4xODguMTU4IiwgImlwX2NvbmZpcm1lciI6ICIxMDMuMTA2LjE4OC4xNTgiIH0."
+    "UcD-w8xcSCRFQKS6CDLzAiRocQzW0UX54O8HwAXv_hm8ULiUvgdQYdZe2eZeqw_Hel66Zf9T5uNgJSbQNzogDw;"
+)
+ITEM_NAME = "Negev%20%7C%20Wall%20Bang%20%28Minimal%20Wear%29"
+
+# 价格信息(从日志中获取)
+SUBTOTAL = "21"
+FEE = "3"
+TOTAL = "24"
+
+
+def build_cookie() -> str:
+    """构建完整的 Cookie 字符串"""
+    return (
+        f"timezoneOffset=28800,0; "
+        f"sessionid={SESSION_ID}; "
+        f"steamLoginSecure={STEAM_LOGIN_SECURE}"
+    )
+
+
+def build_request_body() -> dict:
+    """构建请求体"""
+    return {
+        "sessionid": SESSION_ID,
+        "currency": "1",
+        "subtotal": SUBTOTAL,
+        "fee": FEE,
+        "total": TOTAL,
+        "quantity": "1",
+        "save_my_address": "0",
+        "tradefee_tax": "0",
+        "confirmation": "0",
+        "billing_state": "",
+    }
+
+
+def build_headers() -> dict:
+    """构建请求头"""
+    return {
+        "origin": "https://steamcommunity.com",
+        "cookie": build_cookie(),
+        "user-agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36",
+        "accept": "*/*",
+        "content-type": "application/x-www-form-urlencoded; charset=UTF-8",
+        "referer": f"https://steamcommunity.com/market/listings/730/{ITEM_NAME}",
+    }
+
+
+def send_buy_request(
+    listing_id: str,
+    use_proxy: bool = False,
+    proxy_url: str = None,
+    session: requests.Session = None,
+) -> tuple[requests.Response, float]:
+    """
+    发送购买请求
+
+    Returns:
+        (response, elapsed_ms)
+    """
+    url = f"https://steamcommunity.com/market/buylisting/{listing_id}"
+    data = build_request_body()
+    headers = build_headers()
+
+    if session is None:
+        session = requests.Session()
+
+    proxies = None
+    if use_proxy and proxy_url:
+        proxies = {"http": proxy_url, "https": proxy_url}
+
+    start_time = datetime.now()
+    try:
+        response = session.post(
+            url,
+            data=data,
+            headers=headers,
+            proxies=proxies,
+            timeout=30,
+            allow_redirects=False,
+        )
+    except requests.exceptions.RequestException as e:
+        elapsed = (datetime.now() - start_time).total_seconds() * 1000
+        print(f"[ERROR] 请求异常: {e}")
+        raise
+
+    elapsed_ms = (datetime.now() - start_time).total_seconds() * 1000
+    return response, elapsed_ms
+
+
+def print_response_info(response: requests.Response, elapsed_ms: float):
+    """打印响应信息"""
+    print("\n" + "=" * 60)
+    print(f"状态码: {response.status_code}")
+    print(f"耗时: {elapsed_ms:.0f}ms")
+    print(f"响应头: {json.dumps(dict(response.headers), indent=2, ensure_ascii=False)}")
+    print(f"\n响应体:")
+    try:
+        body_json = response.json()
+        print(json.dumps(body_json, indent=2, ensure_ascii=False))
+    except json.JSONDecodeError:
+        print(response.text)
+    print("=" * 60)
+
+
+def main():
+    print("Steam Market Buy API 调试脚本")
+    print(f"Listing ID: {LISTING_ID}")
+    print(f"Session ID: {SESSION_ID}")
+    print(f"价格: subtotal={SUBTOTAL}, fee={FEE}, total={TOTAL}")
+    print()
+
+    session = requests.Session()
+
+    # 测试 1: 直接请求(不使用代理)
+    print("[测试 1] 直接请求(不使用代理)...")
+    try:
+        response, elapsed_ms = send_buy_request(LISTING_ID, use_proxy=False, session=session)
+        print_response_info(response, elapsed_ms)
+
+        if response.status_code == 200:
+            print("✅ 购买成功!")
+        elif response.status_code == 502:
+            print("❌ 502 错误 - 他人已购买该物品")
+            try:
+                error_body = response.json()
+                print(f"错误信息: {error_body.get('message', 'Unknown')}")
+            except:
+                print(f"错误信息: {response.text}")
+        elif response.status_code == 429:
+            print("⚠️ 429 错误 - 请求过于频繁")
+        else:
+            print(f"❌ 其他错误: {response.status_code}")
+
+    except Exception as e:
+        print(f"[ERROR] {e}")
+
+
+if __name__ == "__main__":
+    main()

+ 1 - 0
docs/test/requirements.txt

@@ -0,0 +1 @@
+requests>=2.28.0