mirror of
https://github.com/xweiba/location-spoofer.git
synced 2026-09-26 16:41:54 +08:00
feat: improve runtime setup and guidance
This commit is contained in:
+337
-74
@@ -1,4 +1,20 @@
|
||||
import SwiftUI
|
||||
import UIKit
|
||||
|
||||
private struct SetupScreenshotPreview: Identifiable {
|
||||
let id = UUID()
|
||||
let image: UIImage
|
||||
let title: String
|
||||
}
|
||||
|
||||
private struct CertificateDownloadDestination: Identifiable {
|
||||
let id = UUID()
|
||||
let url: URL
|
||||
}
|
||||
|
||||
private struct ThirdPartyConnectionTestFailure {
|
||||
let message: String
|
||||
}
|
||||
|
||||
enum SetupStep: Int, CaseIterable {
|
||||
case mode
|
||||
@@ -6,7 +22,6 @@ enum SetupStep: Int, CaseIterable {
|
||||
case cert
|
||||
case thirdPartyClient
|
||||
case thirdPartyImport
|
||||
case thirdPartyTest
|
||||
|
||||
var title: String {
|
||||
switch self {
|
||||
@@ -14,8 +29,7 @@ enum SetupStep: Int, CaseIterable {
|
||||
case .proxy: return "配置 Wi-Fi 代理"
|
||||
case .cert: return "初始化 CA 证书"
|
||||
case .thirdPartyClient: return "选择客户端"
|
||||
case .thirdPartyImport: return "导入配置"
|
||||
case .thirdPartyTest: return "连接检测"
|
||||
case .thirdPartyImport: return "导入并检测"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -40,11 +54,27 @@ struct FirstSetupView: View {
|
||||
@ObservedObject private var thirdPartyClient = ThirdPartyProxyClientStore.shared
|
||||
@State private var copiedSubscriptionURL = false
|
||||
@State private var copiedMITMHostname = false
|
||||
@State private var screenshotPreview: SetupScreenshotPreview?
|
||||
@State private var certificateDownloadDestination: CertificateDownloadDestination?
|
||||
@State private var thirdPartyTestFailure: ThirdPartyConnectionTestFailure?
|
||||
@State private var showThirdPartyRepairReason: Bool
|
||||
@State private var showsVerificationResult: Bool
|
||||
@State private var showsThirdPartyFailureLog: Bool
|
||||
|
||||
init(setup: SetupCoordinator, onComplete: @escaping () -> Void) {
|
||||
self.setup = setup
|
||||
self.onComplete = onComplete
|
||||
_step = State(initialValue: setup.setupStep)
|
||||
_showThirdPartyRepairReason = State(
|
||||
initialValue: setup.setupStep == .thirdPartyImport && !setup.message.isEmpty
|
||||
)
|
||||
_showsVerificationResult = State(
|
||||
initialValue: [.proxy, .cert].contains(setup.setupStep)
|
||||
&& setup.lastVerificationResult != nil
|
||||
)
|
||||
_showsThirdPartyFailureLog = State(
|
||||
initialValue: setup.setupStep == .thirdPartyImport && !setup.message.isEmpty
|
||||
)
|
||||
}
|
||||
|
||||
private var certificateStepsComplete: Bool { downloadedDone && installedDone && trustedDone }
|
||||
@@ -56,30 +86,49 @@ struct FirstSetupView: View {
|
||||
NavigationView {
|
||||
VStack(spacing: 0) {
|
||||
progress
|
||||
ScrollView {
|
||||
VStack(alignment: .leading, spacing: 20) {
|
||||
switch step {
|
||||
case .mode: modeStep
|
||||
case .proxy: proxyStep
|
||||
case .cert: certificateStep
|
||||
case .thirdPartyClient: thirdPartyClientStep
|
||||
case .thirdPartyImport: thirdPartyImportStep
|
||||
case .thirdPartyTest: thirdPartyTestStep
|
||||
ScrollViewReader { scrollProxy in
|
||||
ScrollView {
|
||||
VStack(alignment: .leading, spacing: 20) {
|
||||
switch step {
|
||||
case .mode: modeStep
|
||||
case .proxy: proxyStep
|
||||
case .cert: certificateStep
|
||||
case .thirdPartyClient: thirdPartyClientStep
|
||||
case .thirdPartyImport: thirdPartyImportStep
|
||||
}
|
||||
if let displayedVerificationResult { resultView(displayedVerificationResult) }
|
||||
}
|
||||
if let result { resultView(result) }
|
||||
.padding(20)
|
||||
}
|
||||
.onChange(of: thirdPartyFailureLog) { failureLog in
|
||||
guard step == .thirdPartyImport, failureLog != nil else { return }
|
||||
DispatchQueue.main.async {
|
||||
withAnimation {
|
||||
scrollProxy.scrollTo("thirdPartyFailureLog", anchor: .bottom)
|
||||
}
|
||||
}
|
||||
}
|
||||
.onChange(of: step) { _ in
|
||||
showsVerificationResult = false
|
||||
showsThirdPartyFailureLog = false
|
||||
}
|
||||
.padding(20)
|
||||
}
|
||||
Divider()
|
||||
VStack(spacing: 10) {
|
||||
primaryAction
|
||||
if step != .mode {
|
||||
Button("上一步") { returnToPreviousStep() }
|
||||
.disabled(isVerifying || thirdPartyProxy.isRequesting)
|
||||
if step != .mode {
|
||||
HStack(spacing: 12) {
|
||||
Button {
|
||||
returnToPreviousStep()
|
||||
} label: {
|
||||
Label("上一步", systemImage: "chevron.left")
|
||||
}
|
||||
.buttonStyle(.bordered)
|
||||
.disabled(isVerifying || thirdPartyProxy.isRequesting)
|
||||
Spacer(minLength: 12)
|
||||
primaryAction
|
||||
}
|
||||
}
|
||||
.padding(.horizontal, 20)
|
||||
.padding(.vertical, 12)
|
||||
}
|
||||
}
|
||||
.navigationTitle("开始使用")
|
||||
.navigationBarTitleDisplayMode(.inline)
|
||||
@@ -92,6 +141,28 @@ struct FirstSetupView: View {
|
||||
)
|
||||
}
|
||||
}
|
||||
.sheet(item: $screenshotPreview) { preview in
|
||||
NavigationView {
|
||||
ScrollView {
|
||||
Image(uiImage: preview.image)
|
||||
.resizable()
|
||||
.scaledToFit()
|
||||
.clipShape(RoundedRectangle(cornerRadius: 8))
|
||||
.padding()
|
||||
}
|
||||
.navigationTitle(preview.title)
|
||||
.navigationBarTitleDisplayMode(.inline)
|
||||
.toolbar {
|
||||
ToolbarItem(placement: .confirmationAction) {
|
||||
Button("完成") { screenshotPreview = nil }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.sheet(item: $certificateDownloadDestination) { destination in
|
||||
SafariView(url: destination.url)
|
||||
.ignoresSafeArea()
|
||||
}
|
||||
.alert("无法直接跳转", isPresented: Binding(
|
||||
get: { !manualHint.isEmpty },
|
||||
set: { if !$0 { manualHint = "" } }
|
||||
@@ -133,11 +204,33 @@ struct FirstSetupView: View {
|
||||
return [.mode]
|
||||
case .proxy, .cert:
|
||||
return [.mode, .proxy, .cert]
|
||||
case .thirdPartyClient, .thirdPartyImport, .thirdPartyTest:
|
||||
return [.mode, .thirdPartyClient, .thirdPartyImport, .thirdPartyTest]
|
||||
case .thirdPartyClient, .thirdPartyImport:
|
||||
return [.mode, .thirdPartyClient, .thirdPartyImport]
|
||||
}
|
||||
}
|
||||
|
||||
private var displayedVerificationResult: VerificationResult? {
|
||||
guard showsVerificationResult else { return nil }
|
||||
return result ?? setup.lastVerificationResult
|
||||
}
|
||||
|
||||
private var thirdPartyFailureLog: String? {
|
||||
guard showsThirdPartyFailureLog else { return nil }
|
||||
if let thirdPartyTestFailure {
|
||||
return thirdPartyTestFailure.message
|
||||
}
|
||||
guard !setup.message.isEmpty else { return nil }
|
||||
return """
|
||||
======== 第三方代理运行检测 ========
|
||||
当前客户端:\(thirdPartyClient.selectedClient.name)
|
||||
触发来源:地图或设置中的第三方代理操作
|
||||
请求动作:WLOC 配置接口
|
||||
检测结果:失败
|
||||
错误详情:\(setup.message)
|
||||
处理建议:确认模块已启用,并检查 MITM、证书和代理/VPN 连接。
|
||||
"""
|
||||
}
|
||||
|
||||
private var modeStep: some View {
|
||||
VStack(alignment: .leading, spacing: 16) {
|
||||
Text("选择运行模式")
|
||||
@@ -217,12 +310,23 @@ struct FirstSetupView: View {
|
||||
|
||||
private var proxyStep: some View {
|
||||
VStack(alignment: .leading, spacing: 16) {
|
||||
if !setup.message.isEmpty {
|
||||
Label(setup.message, systemImage: "exclamationmark.triangle.fill")
|
||||
.font(.footnote)
|
||||
.foregroundStyle(.orange)
|
||||
.frame(maxWidth: .infinity, alignment: .leading)
|
||||
}
|
||||
GroupBox(label: Label("先配置 Wi-Fi 系统代理", systemImage: "wifi")) {
|
||||
Text("在当前 Wi-Fi 的详情页,将「HTTP 代理」设为「手动」:服务器填 127.0.0.1,端口填 8888。完成后点击下方「我已配置,开始检测」。检测会自动判断是 Wi-Fi 代理还是证书信任有问题。")
|
||||
Text("在当前 Wi-Fi 的详情页,将「HTTP 代理」设为「手动」:服务器填 127.0.0.1,端口填 8888。配置后点击下方「完成」。检测会自动判断是 Wi-Fi 代理还是证书信任有问题。")
|
||||
.font(.caption).foregroundStyle(.secondary)
|
||||
.frame(maxWidth: .infinity, alignment: .leading)
|
||||
.padding(.top, 4)
|
||||
}
|
||||
setupScreenshot(
|
||||
assetName: "AppModeWiFiProxy",
|
||||
title: "Wi-Fi 代理设置",
|
||||
caption: "1 选择手动,2 填写服务器 127.0.0.1,3 填写端口 8888。"
|
||||
)
|
||||
HStack(spacing: 12) {
|
||||
Button { UIPasteboard.general.string = "127.0.0.1:8888" } label: {
|
||||
Label("复制地址", systemImage: "doc.on.doc").frame(maxWidth: .infinity)
|
||||
@@ -241,15 +345,16 @@ struct FirstSetupView: View {
|
||||
certificateCard(
|
||||
title: "第 1 步:下载证书",
|
||||
icon: "arrow.down.circle",
|
||||
description: "下载本机随机生成的 CA 根证书。私钥仅保存在此设备的钥匙串中,不会随证书文件导出。Safari 出现配置描述文件下载提示时,选择「允许」。",
|
||||
actionTitle: "去下载",
|
||||
description: "下载本机随机生成的 CA 根证书。私钥仅保存在此设备的钥匙串中,不会随证书文件导出。App 会弹出 Safari 下载页;出现配置描述文件下载提示时,选择「允许」。",
|
||||
actionTitle: "打开下载页",
|
||||
actionIcon: "arrow.down.circle.fill",
|
||||
complete: downloadedDone,
|
||||
action: {
|
||||
Task {
|
||||
let opened = await setup.proxy.openCertificateDownload()
|
||||
if !opened {
|
||||
setupActionError = setup.proxy.error ?? "无法打开证书下载页面,请查看诊断日志"
|
||||
if let url = await setup.proxy.prepareCertificateDownloadURL() {
|
||||
certificateDownloadDestination = CertificateDownloadDestination(url: url)
|
||||
} else {
|
||||
setupActionError = setup.proxy.error ?? "无法准备证书下载页面,请查看诊断日志"
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -258,28 +363,43 @@ struct FirstSetupView: View {
|
||||
certificateCard(
|
||||
title: "第 2 步:安装证书",
|
||||
icon: "square.and.arrow.down",
|
||||
description: "下载完成后打开系统「设置」。如果顶部显示「已下载描述文件」,点进去安装;否则进入「通用 → VPN 与设备管理」,找到 WLOC CA 并完成安装。",
|
||||
description: "下载完成后打开系统「设置」。如果顶部显示「已下载描述文件」,点进去安装;否则进入「通用 → VPN 与设备管理」,找到 Location Spoofer CA 并完成安装。",
|
||||
actionTitle: "去安装",
|
||||
actionIcon: "gearshape",
|
||||
complete: installedDone,
|
||||
action: { openSettings(.general) },
|
||||
markComplete: { installedDone = true }
|
||||
)
|
||||
setupScreenshot(
|
||||
assetName: "AppModeCertificateInstall",
|
||||
title: "安装证书",
|
||||
caption: "1 在「VPN 与设备管理」中打开 Location Spoofer CA 描述文件并完成安装。"
|
||||
)
|
||||
certificateCard(
|
||||
title: "第 3 步:信任证书",
|
||||
icon: "shield.checkered",
|
||||
description: "安装后进入「设置 → 通用 → 关于本机 → 证书信任设置」,找到 WLOC CA 并开启完全信任。iOS 保留钥匙串数据时,重装 App 会继续复用同一证书。",
|
||||
description: "安装后进入「设置 → 通用 → 关于本机 → 证书信任设置」,找到 Location Spoofer CA 并开启完全信任。iOS 保留钥匙串数据时,重装 App 会继续复用同一证书。",
|
||||
actionTitle: "去信任",
|
||||
actionIcon: "shield.checkered",
|
||||
complete: trustedDone,
|
||||
action: { openSettings(.general) },
|
||||
markComplete: { trustedDone = true }
|
||||
)
|
||||
setupScreenshot(
|
||||
assetName: "AppModeCertificateTrust",
|
||||
title: "信任证书",
|
||||
caption: "1 在「证书信任设置」中为 Location Spoofer CA 开启完全信任。"
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private var thirdPartyClientStep: some View {
|
||||
VStack(alignment: .leading, spacing: 16) {
|
||||
if !setup.message.isEmpty {
|
||||
Label(setup.message, systemImage: "exclamationmark.triangle.fill")
|
||||
.font(.footnote)
|
||||
.foregroundStyle(.orange)
|
||||
}
|
||||
GroupBox(label: Label("选择第三方代理客户端", systemImage: "app.badge.checkmark")) {
|
||||
VStack(spacing: 0) {
|
||||
ForEach(ThirdPartyProxyClient.allCases) { client in
|
||||
@@ -289,9 +409,11 @@ struct FirstSetupView: View {
|
||||
HStack {
|
||||
VStack(alignment: .leading, spacing: 3) {
|
||||
Text(client.name).foregroundStyle(.primary)
|
||||
Text(client.verificationText)
|
||||
.font(.caption2)
|
||||
.foregroundStyle(client == .shadowrocket ? .green : .orange)
|
||||
if let verificationText = client.verificationText {
|
||||
Text(verificationText)
|
||||
.font(.caption2)
|
||||
.foregroundStyle(.orange)
|
||||
}
|
||||
}
|
||||
Spacer()
|
||||
Image(systemName: thirdPartyClient.selectedClient == client ? "checkmark.circle.fill" : "circle")
|
||||
@@ -305,7 +427,7 @@ struct FirstSetupView: View {
|
||||
}
|
||||
}
|
||||
|
||||
Text("Egern 直接使用 Surge 模块。Stash 直接订阅 .stoverride,不需要 Script Hub 转换。除 Shadowrocket 外,当前仅提供配置,尚未完成真机验证。")
|
||||
Text("除 Shadowrocket 外,当前客户端配置尚未完成真机验证,页面只提供模块导入入口和通用配置提醒。")
|
||||
.font(.footnote)
|
||||
.foregroundStyle(.secondary)
|
||||
}
|
||||
@@ -314,21 +436,31 @@ struct FirstSetupView: View {
|
||||
private var thirdPartyImportStep: some View {
|
||||
let client = thirdPartyClient.selectedClient
|
||||
return VStack(alignment: .leading, spacing: 16) {
|
||||
if showThirdPartyRepairReason {
|
||||
Label(
|
||||
"检测到第三方代理连接异常,请检查模块、MITM 和代理连接后重新检测。",
|
||||
systemImage: "exclamationmark.triangle.fill"
|
||||
)
|
||||
.font(.footnote)
|
||||
.foregroundStyle(.orange)
|
||||
.frame(maxWidth: .infinity, alignment: .leading)
|
||||
}
|
||||
|
||||
GroupBox(label: Label("第 1 步:导入 \(client.name) 模块", systemImage: "square.and.arrow.down")) {
|
||||
VStack(alignment: .leading, spacing: 12) {
|
||||
Text(importInstructions(for: client))
|
||||
.font(.caption)
|
||||
.foregroundStyle(.secondary)
|
||||
|
||||
instructionRow(1, "复制 \(client.name) 的模块订阅地址。")
|
||||
Button {
|
||||
UIPasteboard.general.string = client.subscriptionURL.absoluteString
|
||||
copiedSubscriptionURL = true
|
||||
} label: {
|
||||
Label(copiedSubscriptionURL ? "已复制订阅地址" : "复制订阅地址", systemImage: "doc.on.doc")
|
||||
Label(copiedSubscriptionURL ? "已复制模块订阅地址" : "复制模块订阅地址", systemImage: "doc.on.doc")
|
||||
.frame(maxWidth: .infinity)
|
||||
}
|
||||
.buttonStyle(.borderedProminent)
|
||||
|
||||
instructionRow(2, client == .shadowrocket
|
||||
? "打开 Shadowrocket,进入“配置 → 模块”。"
|
||||
: "打开 \(client.name)。")
|
||||
Button {
|
||||
openThirdPartyClient(client)
|
||||
} label: {
|
||||
@@ -336,15 +468,46 @@ struct FirstSetupView: View {
|
||||
.frame(maxWidth: .infinity)
|
||||
}
|
||||
.buttonStyle(.bordered)
|
||||
|
||||
if client == .shadowrocket {
|
||||
setupScreenshot(
|
||||
assetName: "ShadowrocketConfigDetails",
|
||||
title: "进入 Shadowrocket 配置",
|
||||
caption: "1 点击「模块」进入模块列表,2 可打开当前本地配置详情。"
|
||||
)
|
||||
}
|
||||
|
||||
if client == .shadowrocket {
|
||||
instructionRow(3, "点击右上角“+”,粘贴模块订阅地址并导入,然后确认模块已启用。")
|
||||
setupScreenshot(
|
||||
assetName: "ShadowrocketModuleImport",
|
||||
title: "导入 Shadowrocket 模块",
|
||||
caption: "1 点击右上角加号导入模块,2 确认模块已启用。"
|
||||
)
|
||||
} else {
|
||||
instructionRow(3, "在 \(client.name) 中导入刚才复制的模块订阅地址。")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if client == .shadowrocket {
|
||||
shadowrocketHTTPSDecryptionGuide
|
||||
} else {
|
||||
Text("请复制订阅地址,在客户端的模块、重写或覆写订阅入口中添加。证书、MITM、VPN 和代理连接请按第三方客户端自己的流程配置。")
|
||||
.font(.footnote)
|
||||
.foregroundStyle(.secondary)
|
||||
GroupBox(label: Label("第 2 步:完成 \(client.name) 配置", systemImage: "slider.horizontal.3")) {
|
||||
Text("请在 \(client.name) 中完成相应配置。")
|
||||
.font(.caption)
|
||||
.foregroundStyle(.secondary)
|
||||
.frame(maxWidth: .infinity, alignment: .leading)
|
||||
}
|
||||
}
|
||||
|
||||
if let thirdPartyFailureLog {
|
||||
testResultView(
|
||||
success: false,
|
||||
title: "接口连接失败",
|
||||
log: thirdPartyFailureLog
|
||||
)
|
||||
.id("thirdPartyFailureLog")
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -355,6 +518,11 @@ struct FirstSetupView: View {
|
||||
instructionRow(1, "进入“配置 → 本地文件”,找到带黄点的配置,点击右侧 i 图标。")
|
||||
instructionRow(2, "进入“HTTPS 解密”,开启解密开关。")
|
||||
instructionRow(3, "在域名列表中添加 gs-loc.apple.com。")
|
||||
setupScreenshot(
|
||||
assetName: "ShadowrocketHTTPSDecryption",
|
||||
title: "配置 HTTPS 解密",
|
||||
caption: "1 开启 HTTPS 解密,2 添加 gs-loc.apple.com,3 打开证书设置。"
|
||||
)
|
||||
|
||||
Button {
|
||||
UIPasteboard.general.string = ThirdPartyProxyManager.interceptionHostname
|
||||
@@ -366,6 +534,11 @@ struct FirstSetupView: View {
|
||||
.buttonStyle(.borderedProminent)
|
||||
|
||||
instructionRow(4, "按 Shadowrocket 提示生成并完成证书授权。")
|
||||
setupScreenshot(
|
||||
assetName: "ShadowrocketHTTPSCA",
|
||||
title: "授权 Shadowrocket 证书",
|
||||
caption: "1 打开 Shadowrocket 证书项并按提示安装、授权。"
|
||||
)
|
||||
instructionRow(5, "返回 HTTPS 解密页面,点击右上角勾号保存,然后开启代理。")
|
||||
|
||||
Button {
|
||||
@@ -397,11 +570,44 @@ struct FirstSetupView: View {
|
||||
}
|
||||
}
|
||||
|
||||
private func importInstructions(for client: ThirdPartyProxyClient) -> String {
|
||||
if client == .shadowrocket {
|
||||
return "先复制订阅地址。然后打开 Shadowrocket,进入“配置 → 模块”,点击右上角“+”,粘贴订阅地址并完成导入。导入完成后,模块配置由 Shadowrocket 保存,不依赖本 App 持续运行。"
|
||||
@ViewBuilder
|
||||
private func setupScreenshot(
|
||||
assetName: String,
|
||||
title: String,
|
||||
caption: String
|
||||
) -> some View {
|
||||
if let image = UIImage(named: assetName) {
|
||||
Button {
|
||||
screenshotPreview = SetupScreenshotPreview(
|
||||
image: image,
|
||||
title: title
|
||||
)
|
||||
} label: {
|
||||
VStack(alignment: .leading, spacing: 8) {
|
||||
Image(uiImage: image)
|
||||
.resizable()
|
||||
.scaledToFit()
|
||||
.clipShape(RoundedRectangle(cornerRadius: 6))
|
||||
HStack(spacing: 6) {
|
||||
Image(systemName: "arrow.up.left.and.arrow.down.right")
|
||||
Text(caption)
|
||||
}
|
||||
.font(.caption2)
|
||||
.foregroundStyle(.secondary)
|
||||
.multilineTextAlignment(.leading)
|
||||
}
|
||||
.padding(8)
|
||||
.background(Color(uiColor: .secondarySystemGroupedBackground))
|
||||
.clipShape(RoundedRectangle(cornerRadius: 8))
|
||||
.overlay(
|
||||
RoundedRectangle(cornerRadius: 8)
|
||||
.stroke(Color.secondary.opacity(0.18))
|
||||
)
|
||||
}
|
||||
.buttonStyle(.plain)
|
||||
.accessibilityLabel("\(title):\(caption)")
|
||||
.accessibilityHint("轻点查看大图")
|
||||
}
|
||||
return "先复制订阅地址,然后打开 \(client.name),在模块、重写或覆写订阅入口中粘贴并导入。配置由 \(client.name) 保存,不依赖本 App 持续运行。"
|
||||
}
|
||||
|
||||
private func openThirdPartyClient(_ client: ThirdPartyProxyClient) {
|
||||
@@ -414,18 +620,6 @@ struct FirstSetupView: View {
|
||||
}
|
||||
}
|
||||
|
||||
private var thirdPartyTestStep: some View {
|
||||
VStack(alignment: .leading, spacing: 16) {
|
||||
GroupBox(label: Label("检测配置接口", systemImage: "network")) {
|
||||
Text("请先在第三方客户端中启用刚导入的配置,并按客户端要求完成 MITM、证书和代理/VPN 连接。App 只调用 WLOC 查询接口确认模块能否正常响应,不检查或管理第三方证书。")
|
||||
.font(.caption)
|
||||
.foregroundStyle(.secondary)
|
||||
.frame(maxWidth: .infinity, alignment: .leading)
|
||||
.padding(.top, 4)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func returnToPreviousStep() {
|
||||
result = nil
|
||||
setupActionError = ""
|
||||
@@ -437,9 +631,8 @@ struct FirstSetupView: View {
|
||||
case .cert:
|
||||
step = .proxy
|
||||
case .thirdPartyImport:
|
||||
thirdPartyTestFailure = nil
|
||||
step = .thirdPartyClient
|
||||
case .thirdPartyTest:
|
||||
step = .thirdPartyImport
|
||||
}
|
||||
}
|
||||
|
||||
@@ -483,12 +676,20 @@ struct FirstSetupView: View {
|
||||
@ViewBuilder
|
||||
private func resultView(_ result: VerificationResult) -> some View {
|
||||
let success = result.isSuccess
|
||||
testResultView(
|
||||
success: success,
|
||||
title: success ? "环境检测通过" : failureSummary(result),
|
||||
log: setup.testLog
|
||||
)
|
||||
}
|
||||
|
||||
private func testResultView(success: Bool, title: String, log: String) -> some View {
|
||||
VStack(alignment: .leading, spacing: 10) {
|
||||
Label(success ? "环境检测通过" : failureSummary(result), systemImage: success ? "checkmark.circle.fill" : "xmark.circle.fill")
|
||||
Label(title, systemImage: success ? "checkmark.circle.fill" : "xmark.circle.fill")
|
||||
.foregroundStyle(success ? .green : .red)
|
||||
.font(.subheadline.weight(.semibold))
|
||||
if !success {
|
||||
Text(setup.testLog).font(.caption.monospaced()).textSelection(.enabled)
|
||||
Text(log).font(.caption.monospaced()).textSelection(.enabled)
|
||||
.frame(maxWidth: .infinity, alignment: .leading)
|
||||
.lineLimit(8)
|
||||
Button {
|
||||
@@ -512,7 +713,7 @@ struct FirstSetupView: View {
|
||||
Button {
|
||||
verifyAfterProxyConfirmation()
|
||||
} label: {
|
||||
actionLabel("我已配置,开始检测")
|
||||
actionLabel("完成")
|
||||
}
|
||||
.buttonStyle(.borderedProminent)
|
||||
.disabled(isVerifying)
|
||||
@@ -520,23 +721,22 @@ struct FirstSetupView: View {
|
||||
Button {
|
||||
verifyAfterCertificateConfirmation()
|
||||
} label: {
|
||||
actionLabel("确认完成,重新检测")
|
||||
actionLabel("完成")
|
||||
}
|
||||
.buttonStyle(.borderedProminent)
|
||||
.disabled(!certificateStepsComplete || isVerifying)
|
||||
} else if step == .thirdPartyClient {
|
||||
Button("下一步:导入配置") { step = .thirdPartyImport }
|
||||
.frame(maxWidth: .infinity)
|
||||
.buttonStyle(.borderedProminent)
|
||||
} else if step == .thirdPartyImport {
|
||||
Button("我已导入,下一步") { step = .thirdPartyTest }
|
||||
.frame(maxWidth: .infinity)
|
||||
Button {
|
||||
step = .thirdPartyImport
|
||||
} label: {
|
||||
actionLabel("完成")
|
||||
}
|
||||
.buttonStyle(.borderedProminent)
|
||||
} else {
|
||||
Button {
|
||||
verifyThirdPartyConnection()
|
||||
} label: {
|
||||
actionLabel("检测接口连接")
|
||||
actionLabel("完成")
|
||||
}
|
||||
.buttonStyle(.borderedProminent)
|
||||
.disabled(isVerifying || thirdPartyProxy.isRequesting)
|
||||
@@ -564,23 +764,84 @@ struct FirstSetupView: View {
|
||||
|
||||
private func verifyThirdPartyConnection() {
|
||||
guard !isVerifying else { return }
|
||||
let client = thirdPartyClient.selectedClient
|
||||
let startedAt = Date()
|
||||
isVerifying = true
|
||||
result = nil
|
||||
thirdPartyTestFailure = nil
|
||||
showsThirdPartyFailureLog = false
|
||||
setup.message = ""
|
||||
RuntimeLogger.info("APP", "ThirdPartyProxy", "开始第三方代理连接检测", details: [
|
||||
"当前客户端": client.name,
|
||||
"请求动作": "WLOC query",
|
||||
"检查范围": "模块拦截、MITM、代理/VPN连接"
|
||||
])
|
||||
Task { @MainActor in
|
||||
defer { isVerifying = false }
|
||||
do {
|
||||
_ = try await thirdPartyProxy.query()
|
||||
let response = try await thirdPartyProxy.query()
|
||||
let elapsedMilliseconds = Int(Date().timeIntervalSince(startedAt) * 1_000)
|
||||
RuntimeLogger.info("APP", "ThirdPartyProxy", "第三方代理连接检测通过", details: [
|
||||
"当前客户端": client.name,
|
||||
"请求动作": "WLOC query",
|
||||
"连接状态": response.latitude == nil || response.longitude == nil ? "已连接,无保存坐标" : "已连接,有保存坐标",
|
||||
"耗时毫秒": String(elapsedMilliseconds)
|
||||
])
|
||||
onComplete()
|
||||
} catch {
|
||||
setupActionError = error.localizedDescription
|
||||
let elapsedMilliseconds = Int(Date().timeIntervalSince(startedAt) * 1_000)
|
||||
let connectionState = thirdPartyConnectionStateDescription
|
||||
let errorType = String(describing: type(of: error))
|
||||
RuntimeLogger.error(
|
||||
"APP",
|
||||
"ThirdPartyProxy",
|
||||
"第三方代理连接检测失败",
|
||||
error: error,
|
||||
details: [
|
||||
"当前客户端": client.name,
|
||||
"请求动作": "WLOC query",
|
||||
"连接状态": connectionState,
|
||||
"耗时毫秒": String(elapsedMilliseconds),
|
||||
"错误类型": errorType,
|
||||
"处理建议": "检查模块、MITM、证书和代理/VPN连接"
|
||||
]
|
||||
)
|
||||
thirdPartyTestFailure = ThirdPartyConnectionTestFailure(
|
||||
message: """
|
||||
======== 第三方代理连接检测 ========
|
||||
当前客户端:\(client.name)
|
||||
请求动作:WLOC query
|
||||
检查范围:模块拦截、MITM、证书、代理/VPN 连接
|
||||
连接状态:\(connectionState)
|
||||
检测结果:失败
|
||||
耗时:\(elapsedMilliseconds) ms
|
||||
错误类型:\(errorType)
|
||||
错误详情:\(error.localizedDescription)
|
||||
处理建议:确认模块已启用,并检查 MITM、证书和代理/VPN 连接后重试。
|
||||
"""
|
||||
)
|
||||
showsThirdPartyFailureLog = true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private var thirdPartyConnectionStateDescription: String {
|
||||
switch thirdPartyProxy.connectionState {
|
||||
case .unknown:
|
||||
return "未检测"
|
||||
case .connected(let active):
|
||||
return active ? "已连接,有保存坐标" : "已连接,无保存坐标"
|
||||
case .failed(let message):
|
||||
return "连接失败(\(message))"
|
||||
}
|
||||
}
|
||||
|
||||
private func actionLabel(_ title: String) -> some View {
|
||||
HStack {
|
||||
if isVerifying { ProgressView().tint(.white).controlSize(.small) }
|
||||
Text(title).frame(maxWidth: .infinity)
|
||||
Text(title)
|
||||
.lineLimit(1)
|
||||
.minimumScaleFactor(0.78)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -610,11 +871,13 @@ struct FirstSetupView: View {
|
||||
guard !isVerifying else { return }
|
||||
isVerifying = true
|
||||
result = nil
|
||||
showsVerificationResult = false
|
||||
Task {
|
||||
let verification = await setup.runVerificationTest()
|
||||
setup.applyVerificationResult(verification)
|
||||
guard !Task.isCancelled else { return }
|
||||
result = verification
|
||||
showsVerificationResult = true
|
||||
isVerifying = false
|
||||
completion(verification)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user