chore: prepare v1.0.5 release

This commit is contained in:
xweiba
2026-08-10 16:19:56 +08:00
parent 9510d9aa42
commit a6b9e8056b
18 changed files with 545 additions and 124 deletions
+38 -22
View File
@@ -52,6 +52,7 @@ struct FirstSetupView: View {
@ObservedObject private var runtimeMode = ProxyRuntimeModeStore.shared
@ObservedObject private var thirdPartyProxy = ThirdPartyProxyManager.shared
@ObservedObject private var thirdPartyClient = ThirdPartyProxyClientStore.shared
@ObservedObject private var motionSimulation = MotionSimulationStore.shared
@State private var copiedSubscriptionURL = false
@State private var copiedMITMHostname = false
@State private var screenshotPreview: SetupScreenshotPreview?
@@ -533,10 +534,17 @@ struct FirstSetupView: View {
shadowrocketHTTPSDecryptionGuide
} else {
GroupBox(label: Label("第 2 步:完成 \(client.name) 配置", systemImage: "slider.horizontal.3")) {
Text("请在 \(client.name) 中完成相应配置。")
.font(.caption)
.foregroundStyle(.secondary)
.frame(maxWidth: .infinity, alignment: .leading)
VStack(alignment: .leading, spacing: 12) {
Text("请在 \(client.name) 中完成相应配置。")
.font(.caption)
.foregroundStyle(.secondary)
.frame(maxWidth: .infinity, alignment: .leading)
Text("配置时请使用 gs-loc.apple.com 和 gs-loc-cn.apple.com 两个域名。")
.font(.caption)
.foregroundStyle(.secondary)
.frame(maxWidth: .infinity, alignment: .leading)
mitmHostnameCopyButton
}
}
}
@@ -556,21 +564,14 @@ struct FirstSetupView: View {
VStack(alignment: .leading, spacing: 12) {
instructionRow(1, "进入“配置 → 本地文件”,找到带黄点的配置,点击右侧 i 图标。")
instructionRow(2, "进入“HTTPS 解密”,开启解密开关。")
instructionRow(3, "在域名列表中添加 gs-loc.apple.com。")
instructionRow(3, "在域名列表中添加 gs-loc.apple.com 和 gs-loc-cn.apple.com")
setupScreenshot(
assetName: "ShadowrocketHTTPSDecryption",
title: "配置 HTTPS 解密",
caption: "1 开启 HTTPS 解密,2 添加 gs-loc.apple.com3 打开证书设置。"
caption: "1 开启 HTTPS 解密,2 添加 gs-loc.apple.com 和 gs-loc-cn.apple.com3 打开证书设置。"
)
Button {
UIPasteboard.general.string = ThirdPartyProxyManager.interceptionHostname
copiedMITMHostname = true
} label: {
Label(copiedMITMHostname ? "已复制 gs-loc.apple.com" : "复制解密域名", systemImage: "doc.on.doc")
.frame(maxWidth: .infinity)
}
.buttonStyle(.borderedProminent)
mitmHostnameCopyButton
instructionRow(4, "按 Shadowrocket 提示生成并完成证书授权。")
setupScreenshot(
@@ -595,6 +596,20 @@ struct FirstSetupView: View {
}
}
private var mitmHostnameCopyButton: some View {
Button {
UIPasteboard.general.string = ThirdPartyProxyManager.interceptionHostnamesText
copiedMITMHostname = true
} label: {
Label(
copiedMITMHostname ? "已复制两个解密域名" : "复制两个解密域名",
systemImage: "doc.on.doc"
)
.frame(maxWidth: .infinity)
}
.buttonStyle(.borderedProminent)
}
private func instructionRow(_ number: Int, _ text: String) -> some View {
HStack(alignment: .top, spacing: 8) {
Text("\(number)")
@@ -818,16 +833,17 @@ struct FirstSetupView: View {
Task { @MainActor in
defer { isVerifying = false }
do {
let version = try await thirdPartyProxy.validateVersion()
let response = try await thirdPartyProxy.query()
let response = try await thirdPartyProxy.validateConnection()
let advancedFeaturesAvailable = await thirdPartyProxy.refreshAdvancedFeatureAvailability()
if !advancedFeaturesAvailable {
motionSimulation.setEnabled(false)
}
let elapsedMilliseconds = Int(Date().timeIntervalSince(startedAt) * 1_000)
RuntimeLogger.info("APP", "ThirdPartyProxy", "第三方代理连接检测通过", details: [
"当前客户端": client.name,
"模块版本": version.moduleVersion,
"协议版本": String(version.protocolVersion),
"能力": version.capabilities.sorted().joined(separator: ","),
"请求动作": "WLOC query",
"连接状态": response.latitude == nil || response.longitude == nil ? "已连接,无保存坐标" : "已连接,有保存坐标",
"运动状态模拟": advancedFeaturesAvailable ? "支持" : "不支持,已关闭",
"耗时毫秒": String(elapsedMilliseconds)
])
onComplete()
@@ -846,14 +862,14 @@ struct FirstSetupView: View {
"连接状态": connectionState,
"耗时毫秒": String(elapsedMilliseconds),
"错误类型": errorType,
"处理建议": "检查模块、MITM、证书和代理/VPN连接"
"处理建议": ThirdPartyProxyError.recoverySuggestion(for: error)
]
)
thirdPartyTestFailure = ThirdPartyConnectionTestFailure(
message: """
======== 第三方代理连接检测 ========
当前客户端:\(client.name)
版本接口:/wloc-settings/version
配置接口:/wloc-settings/save
请求动作:WLOC query
检查范围:模块拦截、MITM、证书、代理/VPN 连接
连接状态:\(connectionState)
@@ -861,7 +877,7 @@ struct FirstSetupView: View {
耗时:\(elapsedMilliseconds) ms
错误类型:\(errorType)
错误详情:\(error.localizedDescription)
处理建议:确认模块已启用,并检查 MITM、证书和代理/VPN 连接后重试
处理建议:\(ThirdPartyProxyError.recoverySuggestion(for: error))
"""
)
showsThirdPartyFailureLog = true
+2 -2
View File
@@ -662,7 +662,7 @@ struct MapHomeView: View {
"当前客户端": thirdPartyClient.selectedClient.name,
"请求动作": "WLOC save",
"恢复状态": wasActive ? "保留原第三方坐标" : "保持未启用",
"处理建议": "检查模块、MITM、证书和代理/VPN连接"
"处理建议": ThirdPartyProxyError.recoverySuggestion(for: error)
]
)
setup.requestThirdPartySetup(message: error.localizedDescription)
@@ -744,7 +744,7 @@ struct MapHomeView: View {
"当前客户端": thirdPartyClient.selectedClient.name,
"请求动作": "WLOC clear",
"恢复状态": "保留已启用状态",
"处理建议": "检查模块、MITM、证书和代理/VPN连接"
"处理建议": ThirdPartyProxyError.recoverySuggestion(for: error)
]
)
setup.requestThirdPartySetup(message: error.localizedDescription)
+173 -10
View File
@@ -1,5 +1,22 @@
import SwiftUI
private enum UpdateCheckResult: Identifiable {
case current(currentVersion: String, latestVersion: String)
case available(AppUpdatePrompt)
case failed
var id: String {
switch self {
case .current(let currentVersion, let latestVersion):
return "current-\(currentVersion)-\(latestVersion)"
case .available(let prompt):
return "available-\(prompt.id)"
case .failed:
return "failed"
}
}
}
struct SettingsView: View {
@ObservedObject var setup: SetupCoordinator
@ObservedObject var actions: LocationActionCoordinator
@@ -15,8 +32,11 @@ struct SettingsView: View {
@State private var proxyOperationAlertTitle = "代理操作失败"
@State private var modeOperationRunning = false
@State private var copiedClient: ThirdPartyProxyClient?
@State private var copiedMITMHostnames = false
@State private var showCertificateResetConfirmation = false
@State private var githubDestination: SafariDestination?
@State private var isCheckingForUpdates = false
@State private var updateCheckResult: UpdateCheckResult?
var body: some View {
Form {
@@ -66,7 +86,11 @@ struct SettingsView: View {
Section("定位模拟") {
Toggle("运动状态模拟", isOn: motionSimulationBinding)
.disabled(modeOperationRunning || actions.state.isBusy || thirdPartyProxy.isRequesting)
.disabled(
modeOperationRunning ||
actions.state.isBusy ||
thirdPartyProxy.isRequesting
)
Text("实验性功能,默认关闭。开启后会同时模拟定位响应中的运动状态。")
.font(.footnote)
.foregroundStyle(.secondary)
@@ -109,6 +133,19 @@ struct SettingsView: View {
Label("进入引导页", systemImage: "arrow.clockwise.circle")
}
}
Button {
checkForUpdates()
} label: {
if isCheckingForUpdates {
HStack {
ProgressView()
Text("正在检查…")
}
} else {
Label("检查更新", systemImage: "arrow.triangle.2.circlepath")
}
}
.disabled(isCheckingForUpdates)
valueRow("版本", value: versionText)
}
@@ -205,6 +242,9 @@ struct SettingsView: View {
} message: {
Text(proxyOperationError)
}
.alert(item: $updateCheckResult) { result in
updateCheckAlert(for: result)
}
.confirmationDialog(
"重置证书?",
isPresented: $showCertificateResetConfirmation,
@@ -217,6 +257,16 @@ struct SettingsView: View {
} message: {
Text("当前虚拟定位和本地代理将停止。App 会删除钥匙串中的设备 CA、立即生成新证书,并打开安装与信任引导。你还需要前往 iOS「设置 → 通用 → VPN 与设备管理」手动删除旧证书,然后重新下载安装并完全信任新证书。")
}
.task(id: runtimeMode.mode) {
guard runtimeMode.mode == .thirdParty, !modeOperationRunning else { return }
if !(await thirdPartyProxy.refreshAdvancedFeatureAvailability()) {
disableUnsupportedThirdPartyMotionSimulation()
}
}
.onChange(of: thirdPartyProxy.moduleUpdateRecommended) { updateRecommended in
guard updateRecommended else { return }
disableUnsupportedThirdPartyMotionSimulation()
}
}
private func valueRow(_ title: String, value: String) -> some View {
@@ -229,6 +279,71 @@ struct SettingsView: View {
return "\(v) (\(b))"
}
private func checkForUpdates() {
guard !isCheckingForUpdates else { return }
isCheckingForUpdates = true
Task { @MainActor in
defer { isCheckingForUpdates = false }
guard let configuration = await AppRemoteConfigurationService.fetch() else {
updateCheckResult = .failed
return
}
AppRemoteConfigurationStore.shared.apply(configuration)
let currentVersion = Bundle.main.object(
forInfoDictionaryKey: "CFBundleShortVersionString"
) as? String ?? AppRemoteConfiguration.fallback.latestVersion
guard let pendingPrompt = configuration.updatePrompt(currentVersion: currentVersion) else {
updateCheckResult = .current(
currentVersion: currentVersion,
latestVersion: configuration.latestVersion
)
return
}
let releaseNotes = await AppRemoteConfigurationService.fetchReleaseNotes(
version: pendingPrompt.latestVersion
)
let prompt = configuration.updatePrompt(
currentVersion: currentVersion,
releaseNotes: releaseNotes
) ?? pendingPrompt
updateCheckResult = .available(prompt)
}
}
private func updateCheckAlert(for result: UpdateCheckResult) -> Alert {
switch result {
case .current(let currentVersion, let latestVersion):
return Alert(
title: Text("已是最新版本"),
message: Text("当前版本 \(currentVersion),远程最新版本 \(latestVersion)"),
dismissButton: .default(Text("知道了"))
)
case .available(let prompt):
let details = prompt.releaseNotes
?? "更新说明暂时无法加载,请前往最新 Release 页面查看。"
let message: String
if prompt.requirement == .required {
message = "当前版本 \(prompt.currentVersion) 已停止支持,请更新到 \(prompt.latestVersion) 后继续使用。\n\n\(details)"
} else {
message = "当前版本 \(prompt.currentVersion),最新版本 \(prompt.latestVersion)\n\n\(details)"
}
return Alert(
title: Text(prompt.requirement == .required ? "需要更新" : "发现新版本"),
message: Text(message),
primaryButton: .default(Text("前往更新")) {
UIApplication.shared.open(AppRemoteConfigurationService.releasesURL)
},
secondaryButton: .cancel(Text("稍后"))
)
case .failed:
return Alert(
title: Text("检查更新失败"),
message: Text("无法获取远程版本信息,请检查网络后重试。"),
dismissButton: .default(Text("知道了"))
)
}
}
private var proxyBinding: Binding<Bool> {
Binding(get: { proxy.isRunning }, set: { on in
Task {
@@ -267,7 +382,19 @@ struct SettingsView: View {
return
}
guard thirdPartyProxy.activeSettings?.success == true else {
motionSimulation.setEnabled(enabled)
guard enabled else {
motionSimulation.setEnabled(false)
return
}
modeOperationRunning = true
Task { @MainActor in
if await thirdPartyProxy.refreshAdvancedFeatureAvailability() {
motionSimulation.setEnabled(true)
} else {
presentMotionSimulationModuleUpdateAlert()
}
modeOperationRunning = false
}
return
}
modeOperationRunning = true
@@ -283,7 +410,11 @@ struct SettingsView: View {
error: error,
details: ["当前客户端": thirdPartyClient.selectedClient.name]
)
setup.requestThirdPartySetup(message: error.localizedDescription)
if error as? ThirdPartyProxyError == .moduleOutdated {
presentMotionSimulationModuleUpdateAlert()
} else {
setup.requestThirdPartySetup(message: error.localizedDescription)
}
}
modeOperationRunning = false
}
@@ -311,6 +442,12 @@ struct SettingsView: View {
.font(.footnote)
.foregroundStyle(.secondary)
if thirdPartyProxy.moduleUpdateRecommended {
Text("当前模块版本较旧,基础坐标功能仍可继续使用。重新导入最新模块后可使用版本检测和运动状态模拟。")
.font(.footnote)
.foregroundStyle(.orange)
}
if let verificationText = thirdPartyClient.selectedClient.verificationText {
HStack {
Text("验证状态")
@@ -328,6 +465,13 @@ struct SettingsView: View {
Label(copiedClient == thirdPartyClient.selectedClient ? "已复制模块订阅地址" : "复制模块订阅地址", systemImage: "doc.on.doc")
}
Button {
UIPasteboard.general.string = ThirdPartyProxyManager.interceptionHostnamesText
copiedMITMHostnames = true
} label: {
Label(copiedMITMHostnames ? "已复制两个解密域名" : "复制两个解密域名", systemImage: "doc.on.doc")
}
Button {
openThirdPartyClient(thirdPartyClient.selectedClient)
} label: {
@@ -349,7 +493,7 @@ struct SettingsView: View {
.font(.footnote).foregroundStyle(.secondary)
}
Text("复制模块订阅地址后,在对应代理客户端中添加模块/重写订阅,并启用 MITM。第三方客户端保存坐标后,即使关闭本 App,坐标仍由代理客户端持久化并继续生效。")
Text("复制模块订阅地址后,在对应代理客户端中添加模块/重写订阅,并为 gs-loc.apple.com 和 gs-loc-cn.apple.com 启用 MITM。第三方客户端保存坐标后,即使关闭本 App,坐标仍由代理客户端持久化并继续生效。")
.font(.footnote).foregroundStyle(.secondary)
}
}
@@ -387,7 +531,7 @@ struct SettingsView: View {
return """
App 在设备本地运行一个代理服务器(127.0.0.1:8888)。
通过 WiFi 手动代理配置,让系统的定位请求(gs-loc.apple.com/clls/wloc经过这个本地代理。代理使用已安装的 CA 证书对 HTTPS 流量做中间人解密,把 Apple 返回的定位坐标改写为你设置的虚拟坐标,再加密返回给系统,从而实现虚拟定位。
通过 WiFi 手动代理配置,让系统发往 gs-loc.apple.com 和 gs-loc-cn.apple.com 的定位请求经过这个本地代理。代理使用已安装的 CA 证书对 HTTPS 流量做中间人解密,把 Apple 返回的定位坐标改写为你设置的虚拟坐标,再加密返回给系统,从而实现虚拟定位。
"""
}
@@ -404,8 +548,8 @@ struct SettingsView: View {
runtimeMode.setMode(.thirdParty)
if runtimeMode.isInitialized(.thirdParty) {
do {
_ = try await thirdPartyProxy.validateVersion()
_ = try await thirdPartyProxy.query()
_ = try await thirdPartyProxy.validateConnection()
refreshThirdPartyAdvancedFeatures()
proxyOperationAlertTitle = "模式已切换"
proxyOperationError = "第三方代理模式检测通过。请关闭 Wi-Fi 中的 127.0.0.1:8888 手动代理,避免双重拦截。"
} catch {
@@ -447,8 +591,8 @@ struct SettingsView: View {
let startedAt = Date()
Task { @MainActor in
do {
_ = try await thirdPartyProxy.validateVersion()
_ = try await thirdPartyProxy.query()
_ = try await thirdPartyProxy.validateConnection()
refreshThirdPartyAdvancedFeatures()
RuntimeLogger.info("APP", "ThirdPartyProxy", "设置页第三方连接检测通过", details: [
"当前客户端": client.name,
"请求动作": "WLOC query",
@@ -466,7 +610,7 @@ struct SettingsView: View {
"请求动作": "WLOC query",
"连接状态": String(describing: thirdPartyProxy.connectionState),
"耗时毫秒": String(Int(Date().timeIntervalSince(startedAt) * 1_000)),
"处理建议": "检查模块、MITM、证书和代理/VPN连接"
"处理建议": ThirdPartyProxyError.recoverySuggestion(for: error)
]
)
openThirdPartySetup(for: error)
@@ -474,6 +618,25 @@ struct SettingsView: View {
}
}
private func refreshThirdPartyAdvancedFeatures() {
Task { @MainActor in
if !(await thirdPartyProxy.refreshAdvancedFeatureAvailability()) {
disableUnsupportedThirdPartyMotionSimulation()
}
}
}
private func presentMotionSimulationModuleUpdateAlert() {
disableUnsupportedThirdPartyMotionSimulation()
proxyOperationAlertTitle = "无法开启运动状态模拟"
proxyOperationError = "当前模块脚本不支持运动状态模拟,请重新导入最新模块脚本后再开启。基础坐标功能仍可继续使用。"
}
private func disableUnsupportedThirdPartyMotionSimulation() {
guard runtimeMode.mode == .thirdParty else { return }
motionSimulation.setEnabled(false)
}
private func openThirdPartySetup(for error: Error) {
setup.requestThirdPartySetup(message: error.localizedDescription)
dismiss()