diff --git a/SurveyController.xcodeproj/project.pbxproj b/SurveyController.xcodeproj/project.pbxproj index 1c84a26..cfa5fc8 100644 --- a/SurveyController.xcodeproj/project.pbxproj +++ b/SurveyController.xcodeproj/project.pbxproj @@ -317,7 +317,7 @@ CODE_SIGN_IDENTITY = "-"; CODE_SIGN_STYLE = Automatic; COMBINE_HIDPI_IMAGES = YES; - CURRENT_PROJECT_VERSION = 10101; + CURRENT_PROJECT_VERSION = 10102; DEAD_CODE_STRIPPING = YES; ENABLE_APP_SANDBOX = NO; ENABLE_PREVIEWS = YES; @@ -331,7 +331,7 @@ "$(inherited)", "@executable_path/../Frameworks", ); - MARKETING_VERSION = 0.1.1; + MARKETING_VERSION = 0.1.2; PRODUCT_BUNDLE_IDENTIFIER = "com.surveycontroller.mac"; PRODUCT_NAME = "$(TARGET_NAME)"; SWIFT_EMIT_LOC_STRINGS = YES; @@ -346,7 +346,7 @@ CODE_SIGN_IDENTITY = "-"; CODE_SIGN_STYLE = Automatic; COMBINE_HIDPI_IMAGES = YES; - CURRENT_PROJECT_VERSION = 10101; + CURRENT_PROJECT_VERSION = 10102; DEAD_CODE_STRIPPING = YES; ENABLE_APP_SANDBOX = NO; ENABLE_PREVIEWS = NO; @@ -360,7 +360,7 @@ "$(inherited)", "@executable_path/../Frameworks", ); - MARKETING_VERSION = 0.1.1; + MARKETING_VERSION = 0.1.2; PRODUCT_BUNDLE_IDENTIFIER = "com.surveycontroller.mac"; PRODUCT_NAME = "$(TARGET_NAME)"; SWIFT_EMIT_LOC_STRINGS = YES; @@ -373,9 +373,9 @@ buildSettings = { BUNDLE_LOADER = "$(TEST_HOST)"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 10101; + CURRENT_PROJECT_VERSION = 10102; GENERATE_INFOPLIST_FILE = YES; - MARKETING_VERSION = 0.1.1; + MARKETING_VERSION = 0.1.2; PRODUCT_BUNDLE_IDENTIFIER = "com.surveycontroller.mac.tests"; PRODUCT_NAME = "$(TARGET_NAME)"; SWIFT_EMIT_LOC_STRINGS = NO; @@ -389,9 +389,9 @@ buildSettings = { BUNDLE_LOADER = "$(TEST_HOST)"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 10101; + CURRENT_PROJECT_VERSION = 10102; GENERATE_INFOPLIST_FILE = YES; - MARKETING_VERSION = 0.1.1; + MARKETING_VERSION = 0.1.2; PRODUCT_BUNDLE_IDENTIFIER = "com.surveycontroller.mac.tests"; PRODUCT_NAME = "$(TARGET_NAME)"; SWIFT_EMIT_LOC_STRINGS = NO; diff --git a/SurveyController/App/AppVersion.swift b/SurveyController/App/AppVersion.swift index 84dc84f..9b9970c 100644 --- a/SurveyController/App/AppVersion.swift +++ b/SurveyController/App/AppVersion.swift @@ -6,7 +6,7 @@ import Foundation /// 应用版本与 GitHub 仓库信息。 public enum AppVersion { /// 当前版本(与 MARKETING_VERSION 同步)。 - public static let version = "0.1.1" + public static let version = "0.1.2" /// 当前对齐的官方桌面端版本。 public static let alignedOfficialVersion = "4.0.6" diff --git a/SurveyController/Core/Proxy/ProxyPool.swift b/SurveyController/Core/Proxy/ProxyPool.swift index d37a9b3..1258f9b 100644 --- a/SurveyController/Core/Proxy/ProxyPool.swift +++ b/SurveyController/Core/Proxy/ProxyPool.swift @@ -47,7 +47,7 @@ public struct OfficialProxyProvider: ProxyProvider { leases.append(ProxyLease( address: proxy.address, expireAt: expireAt, - ttlSufficient: expireAt <= 0 || expireAt - Date().timeIntervalSince1970 > 60 + ttlSufficient: expireAt <= 0 || expireAt - Date().timeIntervalSince1970 >= ProxyPool.minTtlSeconds )) } return leases @@ -134,7 +134,9 @@ public struct CustomProxyProvider: ProxyProvider { /// 对标 session_policy 的代理池(actor 保证并发安全,替代 Python 锁)。 public actor ProxyPool { public static let cooldownSeconds: TimeInterval = 45 - public static let minTtlSeconds: TimeInterval = 60 + /// 纯 HTTP 链路不设置预留时长门槛;仍由 expireAt 判断代理是否已经过期。 + /// 对标 HTTP_PROXY_MIN_REMAINING_TTL_SECONDS(官方 aca7b90,#103)。 + public static let minTtlSeconds: TimeInterval = 0 private var available: [ProxyLease] = [] private var inUse: Set = [] @@ -203,8 +205,9 @@ public actor ProxyPool { if inUse.contains(lease.address) || blocked.contains(lease.address) { return false } if let until = cooldowns[lease.address], until > Date() { return false } if lease.expireAt > 0 { - if Date().timeIntervalSince1970 >= lease.expireAt { return false } - if lease.expireAt - Date().timeIntervalSince1970 < Self.minTtlSeconds { return false } + // 对标 proxy_lease_has_sufficient_ttl:剩余时长 >= 门槛才可用。门槛为 0 时,未过期即保留。 + let remaining = lease.expireAt - Date().timeIntervalSince1970 + if remaining < Self.minTtlSeconds { return false } } return true } diff --git a/SurveyControllerTests/RunEngineTests.swift b/SurveyControllerTests/RunEngineTests.swift index 01e2ce7..9f1fb34 100644 --- a/SurveyControllerTests/RunEngineTests.swift +++ b/SurveyControllerTests/RunEngineTests.swift @@ -63,6 +63,20 @@ final class ProxyPoolTests: XCTestCase { XCTAssertEqual(lease.address, "5.5.5.5:80") } + /// 对标 test_merge_prefetched_proxy_leases_keeps_future_proxy_without_ttl_floor: + /// 已提取、尚未过期的代理必须使用,不能因客户端预留时长被丢弃。 + func test_future_lease_is_kept_without_ttl_floor() async throws { + XCTAssertEqual(ProxyPool.minTtlSeconds, 0) + let now = Date().timeIntervalSince1970 + let provider = ScriptedProxyProvider(batches: [[ + ProxyLease(address: "1.1.1.1:8000", expireAt: now + 5), + ProxyLease(address: "2.2.2.2:8000", expireAt: now - 1), + ]]) + let pool = ProxyPool(provider: provider) + let lease = try await pool.acquire() + XCTAssertEqual(lease.address, "1.1.1.1:8000") + } + func test_empty_provider_raises_unavailable() async { let pool = ProxyPool(provider: ScriptedProxyProvider(batches: [])) do {