mirror of
https://github.com/xweiba/location-spoofer.git
synced 2026-09-22 14:50:43 +08:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
46c623ad81 | ||
|
|
68cd5fd31c | ||
|
|
27f6029f9e | ||
|
|
05949703af | ||
|
|
908ac8f734 |
+33
-19
@@ -56,7 +56,7 @@ struct BugReportView: View {
|
||||
if isRunning {
|
||||
ProgressView().tint(.white)
|
||||
}
|
||||
Text(isRunning ? "正在生成报告…" : "生成 Bug 报告")
|
||||
(isRunning ? Text("正在生成报告…") : Text("生成 Bug 报告"))
|
||||
.font(.body.weight(.medium))
|
||||
}
|
||||
.frame(maxWidth: .infinity)
|
||||
@@ -95,9 +95,10 @@ struct BugReportView: View {
|
||||
do {
|
||||
let response = try await thirdPartyProxy.query()
|
||||
let active = response.success && response.latitude != nil && response.longitude != nil
|
||||
testLog = "第三方代理测试模式:模块连接成功;已保存坐标=\(active ? "是" : "否")"
|
||||
let savedCoordinate = active ? String(localized: "是") : String(localized: "否")
|
||||
testLog = String(localized: "第三方代理测试模式:模块连接成功;已保存坐标=\(savedCoordinate)")
|
||||
} catch {
|
||||
testLog = "第三方代理测试模式:模块连接失败;\(error.localizedDescription)"
|
||||
testLog = String(localized: "第三方代理测试模式:模块连接失败;\(error.localizedDescription)")
|
||||
}
|
||||
} else {
|
||||
_ = await setup.runVerificationTest()
|
||||
@@ -113,22 +114,11 @@ struct BugReportView: View {
|
||||
let systemVersion = UIDevice.current.systemVersion
|
||||
|
||||
// 拼接报告
|
||||
let report = """
|
||||
### 环境信息
|
||||
App 版本: \(appVersion)
|
||||
系统版本: iOS \(systemVersion)
|
||||
运行模式: \(runtimeMode.mode.displayName)
|
||||
第三方客户端: \(runtimeMode.mode == .thirdParty ? thirdPartyClient.selectedClient.name : "不适用")
|
||||
可复现环境: \(isReproducible ? "是" : "否")
|
||||
|
||||
### 问题描述
|
||||
\(description.trimmingCharacters(in: .whitespacesAndNewlines))
|
||||
|
||||
### 诊断日志
|
||||
```
|
||||
\(testLog.isEmpty ? "(无诊断数据)" : testLog)
|
||||
```
|
||||
"""
|
||||
let report = bugReport(
|
||||
appVersion: appVersion,
|
||||
systemVersion: systemVersion,
|
||||
testLog: testLog
|
||||
)
|
||||
|
||||
// 复制到剪切板
|
||||
UIPasteboard.general.string = report
|
||||
@@ -138,4 +128,28 @@ struct BugReportView: View {
|
||||
showCopiedAlert = true
|
||||
}
|
||||
}
|
||||
|
||||
private func bugReport(appVersion: String, systemVersion: String, testLog: String) -> String {
|
||||
let client = runtimeMode.mode == .thirdParty
|
||||
? thirdPartyClient.selectedClient.name
|
||||
: String(localized: "不适用")
|
||||
let reproducible = isReproducible ? String(localized: "是") : String(localized: "否")
|
||||
let diagnostics = testLog.isEmpty ? String(localized: "(无诊断数据)") : testLog
|
||||
return """
|
||||
### \(String(localized: "环境信息"))
|
||||
\(String(localized: "App 版本")): \(appVersion)
|
||||
\(String(localized: "系统版本")): iOS \(systemVersion)
|
||||
\(String(localized: "运行模式")): \(runtimeMode.mode.displayName)
|
||||
\(String(localized: "第三方客户端")): \(client)
|
||||
\(String(localized: "可复现环境")): \(reproducible)
|
||||
|
||||
### \(String(localized: "问题描述"))
|
||||
\(description.trimmingCharacters(in: .whitespacesAndNewlines))
|
||||
|
||||
### \(String(localized: "诊断日志"))
|
||||
```
|
||||
\(diagnostics)
|
||||
```
|
||||
"""
|
||||
}
|
||||
}
|
||||
|
||||
@@ -20,9 +20,9 @@ struct ContentView: View {
|
||||
Image(systemName: "location.fill")
|
||||
.font(.system(size: 48)).foregroundStyle(.blue)
|
||||
ProgressView()
|
||||
Text(runtimeMode.hasSelectedMode && runtimeMode.mode == .localWiFi
|
||||
? "正在初始化地图与本地代理…"
|
||||
: "正在初始化地图…")
|
||||
(runtimeMode.hasSelectedMode && runtimeMode.mode == .localWiFi
|
||||
? Text("正在初始化地图与本地代理…")
|
||||
: Text("正在初始化地图…"))
|
||||
.font(.subheadline).foregroundStyle(.secondary)
|
||||
}
|
||||
case .setup:
|
||||
@@ -177,7 +177,7 @@ struct ContentView: View {
|
||||
switch prompt.requirement {
|
||||
case .required:
|
||||
let details = prompt.releaseNotes
|
||||
?? "更新说明暂时无法加载,请前往最新 Release 页面查看。"
|
||||
?? String(localized: "更新说明暂时无法加载,请前往最新 Release 页面查看。")
|
||||
return Alert(
|
||||
title: Text("需要更新"),
|
||||
message: Text(
|
||||
@@ -189,7 +189,7 @@ struct ContentView: View {
|
||||
)
|
||||
case .recommended:
|
||||
let details = prompt.releaseNotes
|
||||
?? "更新说明暂时无法加载,请前往最新 Release 页面查看。"
|
||||
?? String(localized: "更新说明暂时无法加载,请前往最新 Release 页面查看。")
|
||||
return Alert(
|
||||
title: Text("发现新版本"),
|
||||
message: Text(
|
||||
|
||||
+42
-29
@@ -12,6 +12,7 @@ struct RuntimeLogsView: View {
|
||||
@State private var entries: [RuntimeLogEntry] = []
|
||||
@State private var isTesting = false
|
||||
@State private var testResult = ""
|
||||
@State private var testSucceeded = false
|
||||
@State private var testMessage = ""
|
||||
@State private var showClearConfirm = false
|
||||
@State private var copiedEntryID: UUID?
|
||||
@@ -22,7 +23,10 @@ struct RuntimeLogsView: View {
|
||||
private var filteredEntries: [RuntimeLogEntry] {
|
||||
let q = logFilter.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||
guard !q.isEmpty else { return entries }
|
||||
return entries.filter { $0.message.localizedCaseInsensitiveContains(q) }
|
||||
return entries.filter {
|
||||
$0.localizedMessage.localizedCaseInsensitiveContains(q)
|
||||
|| $0.localizedCategory.localizedCaseInsensitiveContains(q)
|
||||
}
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
@@ -42,7 +46,7 @@ struct RuntimeLogsView: View {
|
||||
if filteredEntries.isEmpty {
|
||||
VStack(spacing: 10) {
|
||||
Image(systemName: "doc.text.magnifyingglass").font(.largeTitle).foregroundStyle(.secondary)
|
||||
Text(entries.isEmpty ? "暂无运行日志" : "无匹配日志").foregroundStyle(.secondary)
|
||||
(entries.isEmpty ? Text("暂无运行日志") : Text("无匹配日志")).foregroundStyle(.secondary)
|
||||
}.frame(maxWidth: .infinity, maxHeight: .infinity)
|
||||
} else {
|
||||
ScrollView {
|
||||
@@ -103,8 +107,11 @@ struct RuntimeLogsView: View {
|
||||
await runThirdPartyConnectionTest()
|
||||
} else {
|
||||
let result = await setup.runVerificationTest()
|
||||
testResult = result.isSuccess ? "环境检测通过" : "环境检测失败: \(result.id)"
|
||||
if !result.isSuccess { testResult += ",查看下方日志" }
|
||||
testSucceeded = result.isSuccess
|
||||
testResult = result.isSuccess
|
||||
? String(localized: "环境检测通过")
|
||||
: String(localized: "环境检测失败: \(result.localizedTitle)")
|
||||
if !result.isSuccess { testResult += String(localized: ",查看下方日志") }
|
||||
testMessage = setup.testLog
|
||||
}
|
||||
isTesting = false; refresh()
|
||||
@@ -116,7 +123,7 @@ struct RuntimeLogsView: View {
|
||||
} else {
|
||||
Image(systemName: "play.fill").font(.system(size: 13, weight: .bold))
|
||||
}
|
||||
Text(isTesting ? "正在检测…" : "环境检测").font(.subheadline.weight(.semibold))
|
||||
(isTesting ? Text("正在检测…") : Text("环境检测")).font(.subheadline.weight(.semibold))
|
||||
Spacer()
|
||||
Image(systemName: "chevron.right").font(.system(size: 12, weight: .semibold)).opacity(0.5)
|
||||
}
|
||||
@@ -131,9 +138,9 @@ struct RuntimeLogsView: View {
|
||||
.fill(isTesting ? Color.gray : Color.blue)
|
||||
)
|
||||
.disabled(isTesting || actions.state.isBusy)
|
||||
Text(runtimeMode.mode == .thirdParty
|
||||
? "检查第三方模块能否拦截并响应 query 请求;不会写入测试坐标。"
|
||||
: "依次检查:本地代理 → CA 证书信任 → Wi-Fi 代理链路。")
|
||||
(runtimeMode.mode == .thirdParty
|
||||
? Text("检查第三方模块能否拦截并响应 query 请求;不会写入测试坐标。")
|
||||
: Text("依次检查:本地代理 → CA 证书信任 → Wi-Fi 代理链路。"))
|
||||
.font(.caption).foregroundStyle(.secondary)
|
||||
if !testMessage.isEmpty {
|
||||
VStack(alignment: .leading, spacing: 6) {
|
||||
@@ -180,13 +187,13 @@ struct RuntimeLogsView: View {
|
||||
}
|
||||
if runtimeMode.mode == .localWiFi {
|
||||
HStack(spacing: 14) {
|
||||
Label(proxy.isRunning ? "代理运行中" : "代理未运行", systemImage: proxy.isRunning ? "play.circle" : "stop.circle")
|
||||
Label(setup.canModify ? "可修改" : "不可修改", systemImage: setup.canModify ? "checkmark.shield.fill" : "xmark.shield")
|
||||
(proxy.isRunning ? Label("代理运行中", systemImage: "play.circle") : Label("代理未运行", systemImage: "stop.circle"))
|
||||
(setup.canModify ? Label("可修改", systemImage: "checkmark.shield.fill") : Label("不可修改", systemImage: "xmark.shield"))
|
||||
}.font(.caption).foregroundStyle(.secondary)
|
||||
}
|
||||
if !testResult.isEmpty {
|
||||
Text(testResult).font(.footnote.weight(.medium))
|
||||
.foregroundStyle(testResult.contains("通过") ? .green : .red)
|
||||
.foregroundStyle(testSucceeded ? .green : .red)
|
||||
}
|
||||
}.padding(14).background(Color(.secondarySystemBackground))
|
||||
}
|
||||
@@ -197,7 +204,7 @@ struct RuntimeLogsView: View {
|
||||
.foregroundStyle(entry.level == .error ? .red : entry.level == .warning ? .orange : .blue).frame(width: 18)
|
||||
VStack(alignment: .leading, spacing: 4) {
|
||||
HStack {
|
||||
Text("\(entry.source) \(entry.category)").font(.caption.weight(.semibold))
|
||||
Text("\(entry.source) \(entry.localizedCategory)").font(.caption.weight(.semibold))
|
||||
Spacer()
|
||||
Button {
|
||||
UIPasteboard.general.string = entry.renderedText
|
||||
@@ -221,9 +228,9 @@ struct RuntimeLogsView: View {
|
||||
}
|
||||
.buttonStyle(.plain)
|
||||
}
|
||||
Text(entry.message).font(.caption.monospaced()).textSelection(.enabled)
|
||||
Text(entry.localizedMessage).font(.caption.monospaced()).textSelection(.enabled)
|
||||
if !entry.details.isEmpty {
|
||||
Text(entry.details.sorted(by: { $0.key < $1.key }).map { "\($0.key): \($0.value)" }.joined(separator: "\n"))
|
||||
Text(entry.localizedDetailsText)
|
||||
.font(.caption2.monospaced()).foregroundStyle(.secondary).textSelection(.enabled)
|
||||
}
|
||||
}
|
||||
@@ -240,22 +247,28 @@ struct RuntimeLogsView: View {
|
||||
do {
|
||||
let response = try await thirdPartyProxy.query()
|
||||
let active = response.success && response.latitude != nil && response.longitude != nil
|
||||
testResult = active ? "第三方模块连接通过,已有坐标" : "第三方模块连接通过,暂无坐标"
|
||||
testMessage = """
|
||||
======== 第三方代理连接检测 ========
|
||||
模式: 测试模式
|
||||
请求: wloc-settings/save?action=query
|
||||
拦截响应: 有效 JSON
|
||||
已保存坐标: \(active ? "是" : "否")
|
||||
"""
|
||||
testSucceeded = true
|
||||
testResult = active ? String(localized: "第三方模块连接通过,已有坐标") : String(localized: "第三方模块连接通过,暂无坐标")
|
||||
testMessage = thirdPartyTestLog(active: active)
|
||||
} catch {
|
||||
testResult = "第三方模块连接失败"
|
||||
testMessage = """
|
||||
======== 第三方代理连接检测 ========
|
||||
模式: 测试模式
|
||||
请求: wloc-settings/save?action=query
|
||||
结果: \(error.localizedDescription)
|
||||
"""
|
||||
testSucceeded = false
|
||||
testResult = String(localized: "第三方模块连接失败")
|
||||
testMessage = thirdPartyTestLog(error: error)
|
||||
}
|
||||
}
|
||||
|
||||
private func thirdPartyTestLog(active: Bool? = nil, error: Error? = nil) -> String {
|
||||
var lines = [
|
||||
String(localized: "======== 第三方代理连接检测 ========"),
|
||||
String(localized: "模式: 测试模式"),
|
||||
String(localized: "请求: wloc-settings/save?action=query")
|
||||
]
|
||||
if let active {
|
||||
lines.append(String(localized: "拦截响应: 有效 JSON"))
|
||||
lines.append(String(localized: "已保存坐标: \(active ? String(localized: "是") : String(localized: "否"))"))
|
||||
} else if let error {
|
||||
lines.append(String(localized: "结果: \(error.localizedDescription)"))
|
||||
}
|
||||
return lines.joined(separator: "\n")
|
||||
}
|
||||
}
|
||||
|
||||
+92
-80
@@ -53,7 +53,7 @@ struct FirstSetupView: View {
|
||||
@ObservedObject private var thirdPartyProxy = ThirdPartyProxyManager.shared
|
||||
@ObservedObject private var thirdPartyClient = ThirdPartyProxyClientStore.shared
|
||||
@State private var copiedSubscriptionURL = false
|
||||
@State private var copiedMITMHostname = false
|
||||
@State private var copiedMITMHostname: String?
|
||||
@State private var screenshotPreview: SetupScreenshotPreview?
|
||||
@State private var certificateDownloadDestination: CertificateDownloadDestination?
|
||||
@State private var thirdPartyTestFailure: ThirdPartyConnectionTestFailure?
|
||||
@@ -150,7 +150,7 @@ struct FirstSetupView: View {
|
||||
.clipShape(RoundedRectangle(cornerRadius: 8))
|
||||
.padding()
|
||||
}
|
||||
.navigationTitle(preview.title)
|
||||
.navigationTitle(LocalizedStringKey(preview.title))
|
||||
.navigationBarTitleDisplayMode(.inline)
|
||||
.toolbar {
|
||||
ToolbarItem(placement: .confirmationAction) {
|
||||
@@ -188,7 +188,7 @@ struct FirstSetupView: View {
|
||||
Circle()
|
||||
.fill(value.rawValue <= step.rawValue ? Color.blue : Color.gray.opacity(0.3))
|
||||
.frame(width: 10, height: 10)
|
||||
Text(value.title).font(.caption).foregroundStyle(.secondary)
|
||||
Text(LocalizedStringKey(value.title)).font(.caption).foregroundStyle(.secondary)
|
||||
}
|
||||
if value != visibleSteps.last {
|
||||
Rectangle().fill(Color.gray.opacity(0.3)).frame(width: 28, height: 2)
|
||||
@@ -220,15 +220,15 @@ struct FirstSetupView: View {
|
||||
return thirdPartyTestFailure.message
|
||||
}
|
||||
guard !setup.message.isEmpty else { return nil }
|
||||
return """
|
||||
======== 第三方代理运行检测 ========
|
||||
当前客户端:\(thirdPartyClient.selectedClient.name)
|
||||
触发来源:地图或设置中的第三方代理操作
|
||||
请求动作:WLOC 配置接口
|
||||
检测结果:失败
|
||||
错误详情:\(setup.message)
|
||||
处理建议:确认模块已启用,并检查 MITM、证书和代理/VPN 连接。
|
||||
"""
|
||||
return [
|
||||
String(localized: "======== 第三方代理运行检测 ========"),
|
||||
String(localized: "当前客户端:\(thirdPartyClient.selectedClient.name)"),
|
||||
String(localized: "触发来源:地图或设置中的第三方代理操作"),
|
||||
String(localized: "请求动作:WLOC 配置接口"),
|
||||
String(localized: "检测结果:失败"),
|
||||
String(localized: "错误详情:\(setup.message)"),
|
||||
String(localized: "处理建议:确认模块已启用,并检查 MITM、证书和代理/VPN 连接。")
|
||||
].joined(separator: "\n")
|
||||
}
|
||||
|
||||
private var modeStep: some View {
|
||||
@@ -274,10 +274,10 @@ struct FirstSetupView: View {
|
||||
}
|
||||
|
||||
private func modeCard(
|
||||
title: String,
|
||||
title: LocalizedStringKey,
|
||||
icon: String,
|
||||
badges: [String],
|
||||
description: String,
|
||||
description: LocalizedStringKey,
|
||||
tint: Color,
|
||||
action: @escaping () -> Void
|
||||
) -> some View {
|
||||
@@ -288,7 +288,7 @@ struct FirstSetupView: View {
|
||||
.foregroundStyle(tint)
|
||||
HStack(spacing: 6) {
|
||||
ForEach(badges, id: \.self) { badge in
|
||||
Text(badge)
|
||||
Text(LocalizedStringKey(badge))
|
||||
.font(.caption2.weight(.semibold))
|
||||
.padding(.horizontal, 8)
|
||||
.padding(.vertical, 4)
|
||||
@@ -354,7 +354,7 @@ struct FirstSetupView: View {
|
||||
if let url = await setup.proxy.prepareCertificateDownloadURL() {
|
||||
certificateDownloadDestination = CertificateDownloadDestination(url: url)
|
||||
} else {
|
||||
setupActionError = setup.proxy.error ?? "无法准备证书下载页面,请查看诊断日志"
|
||||
setupActionError = setup.proxy.error ?? String(localized: "无法准备证书下载页面,请查看诊断日志")
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -468,10 +468,10 @@ struct FirstSetupView: View {
|
||||
|
||||
Text("返回格式")
|
||||
.font(.subheadline.bold())
|
||||
Text("""
|
||||
Text(String(localized: """
|
||||
成功:{"success":true,"longitude":113.0,"latitude":22.0,"accuracy":25}
|
||||
失败:{"success":false,"error":"错误说明"}
|
||||
""")
|
||||
"""))
|
||||
.font(.caption.monospaced())
|
||||
.textSelection(.enabled)
|
||||
|
||||
@@ -507,7 +507,7 @@ struct FirstSetupView: View {
|
||||
UIPasteboard.general.string = client.subscriptionURL.absoluteString
|
||||
copiedSubscriptionURL = true
|
||||
} label: {
|
||||
Label(copiedSubscriptionURL ? "已复制模块订阅地址" : "复制模块订阅地址", systemImage: "doc.on.doc")
|
||||
(copiedSubscriptionURL ? Label("已复制模块订阅地址", systemImage: "doc.on.doc") : Label("复制模块订阅地址", systemImage: "doc.on.doc"))
|
||||
.frame(maxWidth: .infinity)
|
||||
}
|
||||
.buttonStyle(.borderedProminent)
|
||||
@@ -557,7 +557,7 @@ struct FirstSetupView: View {
|
||||
.font(.caption)
|
||||
.foregroundStyle(.secondary)
|
||||
.frame(maxWidth: .infinity, alignment: .leading)
|
||||
mitmHostnameCopyButton
|
||||
mitmHostnameList(for: client)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -565,7 +565,7 @@ struct FirstSetupView: View {
|
||||
if let thirdPartyFailureLog {
|
||||
testResultView(
|
||||
success: false,
|
||||
title: "接口连接失败",
|
||||
title: String(localized: "接口连接失败"),
|
||||
log: thirdPartyFailureLog
|
||||
)
|
||||
.id("thirdPartyFailureLog")
|
||||
@@ -585,7 +585,7 @@ struct FirstSetupView: View {
|
||||
caption: "1 开启 HTTPS 解密,2 添加复制的全部解密域名,3 打开证书设置。"
|
||||
)
|
||||
|
||||
mitmHostnameCopyButton
|
||||
mitmHostnameList(for: .shadowrocket)
|
||||
|
||||
instructionRow(4, "按 Shadowrocket 提示生成并完成证书授权。")
|
||||
setupScreenshot(
|
||||
@@ -595,14 +595,6 @@ struct FirstSetupView: View {
|
||||
)
|
||||
instructionRow(5, "返回 HTTPS 解密页面,点击右上角勾号保存,然后开启代理。")
|
||||
|
||||
Button {
|
||||
openThirdPartyClient(.shadowrocket)
|
||||
} label: {
|
||||
Label("打开 Shadowrocket 继续配置", systemImage: "arrow.up.forward.app")
|
||||
.frame(maxWidth: .infinity)
|
||||
}
|
||||
.buttonStyle(.bordered)
|
||||
|
||||
Text("App 只能唤起 Shadowrocket,无法通过公开接口直接跳转到“模块”或“HTTPS 解密”页面。")
|
||||
.font(.caption2)
|
||||
.foregroundStyle(.secondary)
|
||||
@@ -610,21 +602,40 @@ 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)
|
||||
private func mitmHostnameList(for client: ThirdPartyProxyClient) -> some View {
|
||||
VStack(spacing: 8) {
|
||||
ForEach(ThirdPartyProxyManager.interceptionHostnames, id: \.self) { hostname in
|
||||
HStack(spacing: 8) {
|
||||
Text(hostname)
|
||||
.font(.caption.monospaced())
|
||||
.frame(maxWidth: .infinity, alignment: .leading)
|
||||
Button {
|
||||
UIPasteboard.general.string = hostname
|
||||
copiedMITMHostname = hostname
|
||||
} label: {
|
||||
Image(systemName: copiedMITMHostname == hostname ? "checkmark" : "doc.on.doc")
|
||||
.frame(width: 28, height: 28)
|
||||
}
|
||||
.buttonStyle(.bordered)
|
||||
.accessibilityLabel(
|
||||
copiedMITMHostname == hostname
|
||||
? Text("已复制 \(hostname)")
|
||||
: Text("复制 \(hostname)")
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
Button {
|
||||
openThirdPartyClient(client)
|
||||
} label: {
|
||||
Label("打开 \(client.name)", systemImage: "arrow.up.forward.app")
|
||||
.frame(maxWidth: .infinity)
|
||||
}
|
||||
.buttonStyle(.bordered)
|
||||
}
|
||||
.buttonStyle(.borderedProminent)
|
||||
}
|
||||
|
||||
private func instructionRow(_ number: Int, _ text: String) -> some View {
|
||||
private func instructionRow(_ number: Int, _ text: LocalizedStringKey) -> some View {
|
||||
HStack(alignment: .top, spacing: 8) {
|
||||
Text("\(number)")
|
||||
.font(.caption2.bold())
|
||||
@@ -658,7 +669,7 @@ struct FirstSetupView: View {
|
||||
.clipShape(RoundedRectangle(cornerRadius: 6))
|
||||
HStack(spacing: 6) {
|
||||
Image(systemName: "arrow.up.left.and.arrow.down.right")
|
||||
Text(caption)
|
||||
Text(LocalizedStringKey(caption))
|
||||
}
|
||||
.font(.caption2)
|
||||
.foregroundStyle(.secondary)
|
||||
@@ -673,7 +684,11 @@ struct FirstSetupView: View {
|
||||
)
|
||||
}
|
||||
.buttonStyle(.plain)
|
||||
.accessibilityLabel("\(title):\(caption)")
|
||||
.accessibilityLabel(
|
||||
Text(LocalizedStringKey(title))
|
||||
+ Text(": ")
|
||||
+ Text(LocalizedStringKey(caption))
|
||||
)
|
||||
.accessibilityHint("轻点查看大图")
|
||||
}
|
||||
}
|
||||
@@ -683,7 +698,7 @@ struct FirstSetupView: View {
|
||||
UIApplication.shared.open(url, options: [:]) { opened in
|
||||
guard !opened else { return }
|
||||
Task { @MainActor in
|
||||
manualHint = "无法打开 \(client.name),请确认客户端已安装后手动打开。"
|
||||
manualHint = String(localized: "无法打开 \(client.name),请确认客户端已安装后手动打开。")
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -705,10 +720,10 @@ struct FirstSetupView: View {
|
||||
}
|
||||
|
||||
private func certificateCard(
|
||||
title: String,
|
||||
title: LocalizedStringKey,
|
||||
icon: String,
|
||||
description: String,
|
||||
actionTitle: String,
|
||||
description: LocalizedStringKey,
|
||||
actionTitle: LocalizedStringKey,
|
||||
actionIcon: String,
|
||||
complete: Bool,
|
||||
action: @escaping () -> Void,
|
||||
@@ -728,10 +743,9 @@ struct FirstSetupView: View {
|
||||
.buttonStyle(.borderedProminent)
|
||||
.tint(.blue)
|
||||
Button(action: markComplete) {
|
||||
Label(
|
||||
complete ? "已完成 ✓" : "已完成",
|
||||
systemImage: complete ? "checkmark.circle.fill" : "circle"
|
||||
)
|
||||
(complete
|
||||
? Label("已完成 ✓", systemImage: "checkmark.circle.fill")
|
||||
: Label("已完成", systemImage: "circle"))
|
||||
.frame(maxWidth: .infinity)
|
||||
}
|
||||
.buttonStyle(.bordered)
|
||||
@@ -746,7 +760,7 @@ struct FirstSetupView: View {
|
||||
let success = result.isSuccess
|
||||
testResultView(
|
||||
success: success,
|
||||
title: success ? "环境检测通过" : failureSummary(result),
|
||||
title: success ? String(localized: "环境检测通过") : failureSummary(result),
|
||||
log: setup.testLog
|
||||
)
|
||||
}
|
||||
@@ -874,21 +888,19 @@ struct FirstSetupView: View {
|
||||
"处理建议": ThirdPartyProxyError.recoverySuggestion(for: error)
|
||||
]
|
||||
)
|
||||
thirdPartyTestFailure = ThirdPartyConnectionTestFailure(
|
||||
message: """
|
||||
======== 第三方代理连接检测 ========
|
||||
当前客户端:\(client.name)
|
||||
配置接口:/wloc-settings/save
|
||||
请求动作:WLOC query
|
||||
检查范围:模块拦截、MITM、证书、代理/VPN 连接
|
||||
连接状态:\(connectionState)
|
||||
检测结果:失败
|
||||
耗时:\(elapsedMilliseconds) ms
|
||||
错误类型:\(errorType)
|
||||
错误详情:\(error.localizedDescription)
|
||||
处理建议:\(ThirdPartyProxyError.recoverySuggestion(for: error))。
|
||||
"""
|
||||
)
|
||||
thirdPartyTestFailure = ThirdPartyConnectionTestFailure(message: [
|
||||
String(localized: "======== 第三方代理连接检测 ========"),
|
||||
String(localized: "当前客户端:\(client.name)"),
|
||||
String(localized: "配置接口:/wloc-settings/save"),
|
||||
String(localized: "请求动作:WLOC query"),
|
||||
String(localized: "检查范围:模块拦截、MITM、证书、代理/VPN 连接"),
|
||||
String(localized: "连接状态:\(connectionState)"),
|
||||
String(localized: "检测结果:失败"),
|
||||
String(localized: "耗时:\(elapsedMilliseconds) ms"),
|
||||
String(localized: "错误类型:\(errorType)"),
|
||||
String(localized: "错误详情:\(error.localizedDescription)"),
|
||||
String(localized: "处理建议:\(ThirdPartyProxyError.recoverySuggestion(for: error))。")
|
||||
].joined(separator: "\n"))
|
||||
showsThirdPartyFailureLog = true
|
||||
}
|
||||
}
|
||||
@@ -897,15 +909,15 @@ struct FirstSetupView: View {
|
||||
private var thirdPartyConnectionStateDescription: String {
|
||||
switch thirdPartyProxy.connectionState {
|
||||
case .unknown:
|
||||
return "未检测"
|
||||
return String(localized: "未检测")
|
||||
case .connected(let active):
|
||||
return active ? "已连接,有保存坐标" : "已连接,无保存坐标"
|
||||
return active ? String(localized: "已连接,有保存坐标") : String(localized: "已连接,无保存坐标")
|
||||
case .failed(let message):
|
||||
return "连接失败(\(message))"
|
||||
return String(localized: "连接失败(\(message))")
|
||||
}
|
||||
}
|
||||
|
||||
private func actionLabel(_ title: String) -> some View {
|
||||
private func actionLabel(_ title: LocalizedStringKey) -> some View {
|
||||
HStack {
|
||||
if isVerifying { ProgressView().tint(.white).controlSize(.small) }
|
||||
Text(title)
|
||||
@@ -954,14 +966,14 @@ struct FirstSetupView: View {
|
||||
|
||||
private func failureSummary(_ result: VerificationResult) -> String {
|
||||
switch result {
|
||||
case .certNotTrusted: return "证书尚未安装或信任"
|
||||
case .wifiProxyNotConfigured: return "Wi-Fi 代理未正确设置"
|
||||
case .proxyNotRunning: return "本地代理未能启动"
|
||||
case .verificationInProgress: return "检测仍在进行"
|
||||
case .verificationSuperseded: return "检测结果已过期"
|
||||
case .coordinateWriteFailed: return "坐标写入失败"
|
||||
case .patchFailed: return "定位改写检测失败"
|
||||
case .success: return "环境检测通过"
|
||||
case .certNotTrusted: return String(localized: "证书尚未安装或信任")
|
||||
case .wifiProxyNotConfigured: return String(localized: "Wi-Fi 代理未正确设置")
|
||||
case .proxyNotRunning: return String(localized: "本地代理未能启动")
|
||||
case .verificationInProgress: return String(localized: "检测仍在进行")
|
||||
case .verificationSuperseded: return String(localized: "检测结果已过期")
|
||||
case .coordinateWriteFailed: return String(localized: "坐标写入失败")
|
||||
case .patchFailed: return String(localized: "定位改写检测失败")
|
||||
case .success: return String(localized: "环境检测通过")
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+21
-18
@@ -301,7 +301,7 @@ struct MapHomeView: View {
|
||||
}
|
||||
} message: {
|
||||
Text(
|
||||
"你正在使用 \(communityContributionClient?.name ?? "第三方客户端")。点击“去提交”会先复制投稿模板,并在 App 内打开社区页面。采纳后将收录到 README,可选择是否匿名署名。"
|
||||
"你正在使用 \(communityContributionClient?.name ?? String(localized: "第三方客户端"))。点击“去提交”会先复制投稿模板,并在 App 内打开社区页面。采纳后将收录到 README,可选择是否匿名署名。"
|
||||
)
|
||||
}
|
||||
.alert("已复制投稿模板", isPresented: $showCommunityTemplateCopied) {
|
||||
@@ -483,7 +483,7 @@ struct MapHomeView: View {
|
||||
// 当前选点
|
||||
HStack {
|
||||
VStack(alignment: .leading, spacing: 3) {
|
||||
Text(mapState.displayName ?? "当前选点").font(.subheadline.weight(.semibold)).lineLimit(1)
|
||||
Text(mapState.displayName ?? String(localized: "当前选点")).font(.subheadline.weight(.semibold)).lineLimit(1)
|
||||
coordinateRow(label: "GCJ-02(国内)", system: .gcj02)
|
||||
coordinateRow(label: "WGS-84(国际)", system: .wgs84)
|
||||
}
|
||||
@@ -496,7 +496,7 @@ struct MapHomeView: View {
|
||||
activeTip = .deactivation
|
||||
}
|
||||
} label: {
|
||||
Text(spoofState == .active ? "无法生效?" : "无法取消?")
|
||||
(spoofState == .active ? Text("无法生效?") : Text("无法取消?"))
|
||||
.font(.system(size: 10, weight: .medium))
|
||||
.foregroundStyle(.secondary)
|
||||
.padding(.horizontal, 8)
|
||||
@@ -520,7 +520,7 @@ struct MapHomeView: View {
|
||||
.buttonStyle(.plain)
|
||||
.foregroundStyle(favorites.selectedFavoriteID != nil ? .orange : .gray)
|
||||
.disabled(favoriteSaveTask != nil)
|
||||
.accessibilityLabel(favorites.selectedFavoriteID != nil ? "已收藏,点击取消收藏" : "收藏当前选点")
|
||||
.accessibilityLabel(favorites.selectedFavoriteID != nil ? Text("已收藏,点击取消收藏") : Text("收藏当前选点"))
|
||||
}
|
||||
// 收藏
|
||||
if favorites.favorites.isEmpty {
|
||||
@@ -537,7 +537,7 @@ struct MapHomeView: View {
|
||||
if spoofState == .verifying {
|
||||
ProgressView().tint(.white)
|
||||
}
|
||||
Text(spoofState == .active && needsSwitchButton ? "关闭" : buttonTitle)
|
||||
(spoofState == .active && needsSwitchButton ? Text("关闭") : Text(buttonTitle))
|
||||
.font(.headline).lineLimit(1)
|
||||
}
|
||||
.frame(maxWidth: needsSwitchButton ? nil : .infinity)
|
||||
@@ -585,15 +585,15 @@ struct MapHomeView: View {
|
||||
private var buttonTitle: String {
|
||||
if runtimeMode.mode == .thirdParty {
|
||||
switch spoofState {
|
||||
case .idle: return "同步到第三方代理"
|
||||
case .verifying: return "检测并同步中…"
|
||||
case .active: return "停止第三方虚拟定位"
|
||||
case .idle: return String(localized: "同步到第三方代理")
|
||||
case .verifying: return String(localized: "检测并同步中…")
|
||||
case .active: return String(localized: "停止第三方虚拟定位")
|
||||
}
|
||||
}
|
||||
switch spoofState {
|
||||
case .idle: return "开始虚拟定位"
|
||||
case .verifying: return "验证环境中…"
|
||||
case .active: return "停止虚拟定位"
|
||||
case .idle: return String(localized: "开始虚拟定位")
|
||||
case .verifying: return String(localized: "验证环境中…")
|
||||
case .active: return String(localized: "停止虚拟定位")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -766,7 +766,8 @@ struct MapHomeView: View {
|
||||
private func presentSuccessfulOperationTip(_ kind: VirtualLocationTipKind) {
|
||||
let count = tipPreferences.recordSuccessfulOperation(kind)
|
||||
let operationName = kind == .activation ? "开启" : "关闭"
|
||||
RuntimeLogger.info("APP", "提醒", "累计\(operationName)虚拟定位次数", details: [
|
||||
RuntimeLogger.info("APP", "提醒", "累计虚拟定位操作次数", details: [
|
||||
"操作": operationName,
|
||||
"次数": String(count),
|
||||
"运行模式": runtimeMode.mode.displayName,
|
||||
"可显示不再提醒": String(tipPreferences.canSuppress(kind))
|
||||
@@ -856,7 +857,7 @@ struct MapHomeView: View {
|
||||
}
|
||||
|
||||
private func coordinateRow(
|
||||
label: String,
|
||||
label: LocalizedStringKey,
|
||||
system: CoordinateConverter.MapCoordinateSystem
|
||||
) -> some View {
|
||||
let coordinate = currentSelectionPair.coordinate(for: system)
|
||||
@@ -1091,7 +1092,7 @@ struct MapHomeView: View {
|
||||
"请求动作": "WLOC query",
|
||||
"错误": response.error ?? "未知错误"
|
||||
])
|
||||
setup.requestThirdPartySetup(message: response.error ?? "第三方代理查询失败")
|
||||
setup.requestThirdPartySetup(message: response.error ?? String(localized: "第三方代理查询失败"))
|
||||
}
|
||||
} catch {
|
||||
spoofState = .idle
|
||||
@@ -1144,7 +1145,7 @@ struct MapHomeView: View {
|
||||
"Wi-Fi接口": String(net.isWiFiEnabled)
|
||||
])
|
||||
activeTip = nil
|
||||
setup.requestSetup(message: "当前未连接可用的 Wi-Fi,请连接 Wi-Fi 后配置 127.0.0.1:8888 手动代理。")
|
||||
setup.requestSetup(message: String(localized: "当前未连接可用的 Wi-Fi,请连接 Wi-Fi 后配置 127.0.0.1:8888 手动代理。"))
|
||||
return
|
||||
}
|
||||
|
||||
@@ -1335,7 +1336,9 @@ struct MapHomeView: View {
|
||||
context.showFailureAlert,
|
||||
!Task.isCancelled,
|
||||
mapState.selection.revision == context.intent.selectionRevision else { return }
|
||||
RuntimeLogger.info("APP", "地图", "定位失败 status=\(realtime.authorizationStatus.rawValue)")
|
||||
RuntimeLogger.info("APP", "地图", "定位失败", details: [
|
||||
"授权状态rawValue": String(realtime.authorizationStatus.rawValue)
|
||||
])
|
||||
showLocationAlert = true
|
||||
return
|
||||
}
|
||||
@@ -1517,7 +1520,7 @@ struct MapHomeView: View {
|
||||
}
|
||||
searchResults = (response?.mapItems ?? []).prefix(6).map { item in
|
||||
let r = SearchLocationResult(
|
||||
name: item.name ?? "未命名",
|
||||
name: item.name ?? String(localized: "未命名"),
|
||||
subtitle: [item.placemark.locality, item.placemark.subLocality, item.placemark.thoroughfare]
|
||||
.compactMap { $0 }
|
||||
.filter { !$0.isEmpty }
|
||||
@@ -1529,7 +1532,7 @@ struct MapHomeView: View {
|
||||
])
|
||||
return r
|
||||
}
|
||||
if searchResults.isEmpty { searchError = "没有找到相关地点" }
|
||||
if searchResults.isEmpty { searchError = String(localized: "没有找到相关地点") }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -154,13 +154,13 @@ final class ProxyManager: ObservableObject {
|
||||
do {
|
||||
if !isRunning { try await start() }
|
||||
guard let url = URL(string: "http://127.0.0.1:8888/cert") else {
|
||||
error = "证书下载地址无效"
|
||||
error = String(localized: "证书下载地址无效")
|
||||
return nil
|
||||
}
|
||||
error = nil
|
||||
return url
|
||||
} catch {
|
||||
self.error = "启动代理失败: \(error.localizedDescription)"
|
||||
self.error = String(localized: "启动代理失败: \(error.localizedDescription)")
|
||||
RuntimeLogger.error("APP", "Certificate", "准备证书下载失败", error: error)
|
||||
return nil
|
||||
}
|
||||
@@ -182,5 +182,5 @@ struct ProxyCoordinateSnapshot: Equatable {
|
||||
|
||||
enum ProxyError: LocalizedError {
|
||||
case startFailed
|
||||
var errorDescription: String? { "Go proxy 启动失败" }
|
||||
var errorDescription: String? { String(localized: "Go proxy 启动失败") }
|
||||
}
|
||||
|
||||
+47
-33
@@ -30,10 +30,10 @@ struct SettingsView: View {
|
||||
@Environment(\.dismiss) private var dismiss
|
||||
@State private var activeTip: TipKind?
|
||||
@State private var proxyOperationError = ""
|
||||
@State private var proxyOperationAlertTitle = "代理操作失败"
|
||||
@State private var proxyOperationAlertTitle = String(localized: "代理操作失败")
|
||||
@State private var modeOperationRunning = false
|
||||
@State private var copiedClient: ThirdPartyProxyClient?
|
||||
@State private var copiedMITMHostnames = false
|
||||
@State private var copiedMITMHostname: String?
|
||||
@State private var showCertificateResetConfirmation = false
|
||||
@State private var githubDestination: SafariDestination?
|
||||
@State private var isCheckingForUpdates = false
|
||||
@@ -222,11 +222,11 @@ struct SettingsView: View {
|
||||
|
||||
Section("致谢") {
|
||||
Button {
|
||||
if let url = URL(string: "https://github.com/Yu9191/wloc") {
|
||||
if let url = URL(string: "https://github.com/xweiba/location-spoofer/blob/main/docs/THIRD_PARTY_MODULES.md") {
|
||||
UIApplication.shared.open(url)
|
||||
}
|
||||
} label: {
|
||||
Label("核心定位改写逻辑移植自 Yu9191/wloc", systemImage: "heart.fill")
|
||||
Label("定位改写脚本源自 Yu9191/wloc,现由本项目维护", systemImage: "heart.fill")
|
||||
.foregroundStyle(.pink)
|
||||
}
|
||||
}
|
||||
@@ -268,7 +268,7 @@ struct SettingsView: View {
|
||||
}
|
||||
}
|
||||
|
||||
private func valueRow(_ title: String, value: String) -> some View {
|
||||
private func valueRow(_ title: LocalizedStringKey, value: String) -> some View {
|
||||
HStack { Text(title); Spacer(); Text(value).font(.footnote.monospaced()).foregroundStyle(.secondary) }
|
||||
}
|
||||
|
||||
@@ -319,15 +319,15 @@ struct SettingsView: View {
|
||||
)
|
||||
case .available(let prompt):
|
||||
let details = prompt.releaseNotes
|
||||
?? "更新说明暂时无法加载,请前往最新 Release 页面查看。"
|
||||
?? String(localized: "更新说明暂时无法加载,请前往最新 Release 页面查看。")
|
||||
let message: String
|
||||
if prompt.requirement == .required {
|
||||
message = "当前版本 \(prompt.currentVersion) 已停止支持,请更新到 \(prompt.latestVersion) 后继续使用。\n\n\(details)"
|
||||
message = String(localized: "当前版本 \(prompt.currentVersion) 已停止支持,请更新到 \(prompt.latestVersion) 后继续使用。\n\n\(details)")
|
||||
} else {
|
||||
message = "当前版本 \(prompt.currentVersion),最新版本 \(prompt.latestVersion)。\n\n\(details)"
|
||||
message = String(localized: "当前版本 \(prompt.currentVersion),最新版本 \(prompt.latestVersion)。\n\n\(details)")
|
||||
}
|
||||
return Alert(
|
||||
title: Text(prompt.requirement == .required ? "需要更新" : "发现新版本"),
|
||||
title: prompt.requirement == .required ? Text("需要更新") : Text("发现新版本"),
|
||||
message: Text(message),
|
||||
primaryButton: .default(Text("前往更新")) {
|
||||
UIApplication.shared.open(AppRemoteConfigurationService.releasesURL)
|
||||
@@ -351,7 +351,7 @@ struct SettingsView: View {
|
||||
try await proxy.start()
|
||||
} catch {
|
||||
proxy.error = error.localizedDescription
|
||||
proxyOperationAlertTitle = "代理操作失败"
|
||||
proxyOperationAlertTitle = String(localized: "代理操作失败")
|
||||
proxyOperationError = error.localizedDescription
|
||||
}
|
||||
} else {
|
||||
@@ -422,14 +422,28 @@ struct SettingsView: View {
|
||||
UIPasteboard.general.string = thirdPartyClient.selectedClient.subscriptionURL.absoluteString
|
||||
copiedClient = thirdPartyClient.selectedClient
|
||||
} label: {
|
||||
Label(copiedClient == thirdPartyClient.selectedClient ? "已复制模块订阅地址" : "复制模块订阅地址", systemImage: "doc.on.doc")
|
||||
(copiedClient == thirdPartyClient.selectedClient ? Label("已复制模块订阅地址", systemImage: "doc.on.doc") : Label("复制模块订阅地址", systemImage: "doc.on.doc"))
|
||||
}
|
||||
|
||||
Button {
|
||||
UIPasteboard.general.string = ThirdPartyProxyManager.interceptionHostnamesText
|
||||
copiedMITMHostnames = true
|
||||
} label: {
|
||||
Label(copiedMITMHostnames ? "已复制解密域名" : "复制解密域名", systemImage: "doc.on.doc")
|
||||
ForEach(ThirdPartyProxyManager.interceptionHostnames, id: \.self) { hostname in
|
||||
HStack(spacing: 8) {
|
||||
Text(hostname)
|
||||
.font(.caption.monospaced())
|
||||
.frame(maxWidth: .infinity, alignment: .leading)
|
||||
Button {
|
||||
UIPasteboard.general.string = hostname
|
||||
copiedMITMHostname = hostname
|
||||
} label: {
|
||||
Image(systemName: copiedMITMHostname == hostname ? "checkmark" : "doc.on.doc")
|
||||
.frame(width: 28, height: 28)
|
||||
}
|
||||
.buttonStyle(.borderless)
|
||||
.accessibilityLabel(
|
||||
copiedMITMHostname == hostname
|
||||
? Text("已复制 \(hostname)")
|
||||
: Text("复制 \(hostname)")
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
Button {
|
||||
@@ -468,31 +482,31 @@ struct SettingsView: View {
|
||||
|
||||
private var thirdPartyStatusText: String {
|
||||
switch thirdPartyProxy.connectionState {
|
||||
case .unknown: return "未检测"
|
||||
case .connected(let active): return active ? "已连接,有坐标" : "已连接,无坐标"
|
||||
case .failed: return "连接失败"
|
||||
case .unknown: return String(localized: "未检测")
|
||||
case .connected(let active): return active ? String(localized: "已连接,有坐标") : String(localized: "已连接,无坐标")
|
||||
case .failed: return String(localized: "连接失败")
|
||||
}
|
||||
}
|
||||
|
||||
private var virtualLocationStatusText: String {
|
||||
if runtimeMode.mode == .localWiFi {
|
||||
return actions.virtualLocationEnabled ? "已开启" : "已关闭"
|
||||
return actions.virtualLocationEnabled ? String(localized: "已开启") : String(localized: "已关闭")
|
||||
}
|
||||
if case .connected(let active) = thirdPartyProxy.connectionState {
|
||||
return active ? "第三方已保存" : "未保存"
|
||||
return active ? String(localized: "第三方已保存") : String(localized: "未保存")
|
||||
}
|
||||
return "未知"
|
||||
return String(localized: "未知")
|
||||
}
|
||||
|
||||
private var workflowDescription: String {
|
||||
if runtimeMode.mode == .thirdParty {
|
||||
return "App 只负责地图选点、收藏和发送 WGS-84 坐标。第三方代理客户端通过模块拦截 Apple WLOC 请求并持久化当前坐标;本模式不启动本机代理,不使用 App 的 CA,也不需要配置 127.0.0.1:8888。"
|
||||
return String(localized: "App 只负责地图选点、收藏和发送 WGS-84 坐标。第三方代理客户端通过模块拦截 Apple WLOC 请求并持久化当前坐标;本模式不启动本机代理,不使用 App 的 CA,也不需要配置 127.0.0.1:8888。")
|
||||
}
|
||||
return """
|
||||
return String(localized: """
|
||||
App 在设备本地运行一个代理服务器(127.0.0.1:8888)。
|
||||
|
||||
通过 WiFi 手动代理配置,让系统发往 Apple 定位域名(gs-loc.apple.com、gsp-ssl.ls.apple.com、bluedot.is.autonavi.com 等)的定位请求经过这个本地代理。代理使用已安装的 CA 证书对 HTTPS 流量做中间人解密,把 Apple 返回的定位坐标改写为你设置的虚拟坐标,再加密返回给系统,从而实现虚拟定位。
|
||||
"""
|
||||
""")
|
||||
}
|
||||
|
||||
private func switchRuntimeMode(to newMode: ProxyRuntimeMode) {
|
||||
@@ -509,8 +523,8 @@ struct SettingsView: View {
|
||||
if runtimeMode.isInitialized(.thirdParty) {
|
||||
do {
|
||||
_ = try await thirdPartyProxy.query()
|
||||
proxyOperationAlertTitle = "模式已切换"
|
||||
proxyOperationError = "第三方代理模式检测通过。请关闭 Wi-Fi 中的 127.0.0.1:8888 手动代理,避免双重拦截。"
|
||||
proxyOperationAlertTitle = String(localized: "模式已切换")
|
||||
proxyOperationError = String(localized: "第三方代理模式检测通过。请关闭 Wi-Fi 中的 127.0.0.1:8888 手动代理,避免双重拦截。")
|
||||
} catch {
|
||||
openThirdPartySetup(for: error)
|
||||
}
|
||||
@@ -532,8 +546,8 @@ struct SettingsView: View {
|
||||
let result = await setup.runVerificationTest()
|
||||
setup.applyVerificationResult(result)
|
||||
if result.isSuccess {
|
||||
proxyOperationAlertTitle = "模式已切换"
|
||||
proxyOperationError = "APP 模式环境检测通过。请停用第三方 WLOC 模块或代理连接,避免双重拦截。"
|
||||
proxyOperationAlertTitle = String(localized: "模式已切换")
|
||||
proxyOperationError = String(localized: "APP 模式环境检测通过。请停用第三方 WLOC 模块或代理连接,避免双重拦截。")
|
||||
} else {
|
||||
dismiss()
|
||||
}
|
||||
@@ -594,14 +608,14 @@ struct SettingsView: View {
|
||||
try setup.certificateStore.reset()
|
||||
runtimeMode.resetInitialization(.localWiFi)
|
||||
guard await setup.prepareLocalServices() else {
|
||||
proxyOperationAlertTitle = "证书重置失败"
|
||||
proxyOperationAlertTitle = String(localized: "证书重置失败")
|
||||
proxyOperationError = setup.message
|
||||
return
|
||||
}
|
||||
setup.requestCertificateSetup()
|
||||
dismiss()
|
||||
} catch {
|
||||
proxyOperationAlertTitle = "证书重置失败"
|
||||
proxyOperationAlertTitle = String(localized: "证书重置失败")
|
||||
proxyOperationError = error.localizedDescription
|
||||
}
|
||||
}
|
||||
@@ -612,8 +626,8 @@ struct SettingsView: View {
|
||||
UIApplication.shared.open(url, options: [:]) { opened in
|
||||
guard !opened else { return }
|
||||
Task { @MainActor in
|
||||
proxyOperationAlertTitle = "无法打开客户端"
|
||||
proxyOperationError = "无法打开 \(client.name),请确认客户端已安装后手动打开。"
|
||||
proxyOperationAlertTitle = String(localized: "无法打开客户端")
|
||||
proxyOperationError = String(localized: "无法打开 \(client.name),请确认客户端已安装后手动打开。")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+24
-24
@@ -36,7 +36,7 @@ final class SetupCoordinator: ObservableObject {
|
||||
message = ""
|
||||
return true
|
||||
} catch {
|
||||
message = "本地代理初始化失败:\(error.localizedDescription)"
|
||||
message = String(localized: "本地代理初始化失败:\(error.localizedDescription)")
|
||||
RuntimeLogger.error("APP", "Startup", "本地服务初始化失败", error: error)
|
||||
return false
|
||||
}
|
||||
@@ -48,19 +48,19 @@ final class SetupCoordinator: ObservableObject {
|
||||
case .success:
|
||||
trustState = .trusted
|
||||
needsSetup = false
|
||||
message = "✓ 定位环境正常"
|
||||
message = String(localized: "✓ 定位环境正常")
|
||||
case .certNotTrusted:
|
||||
trustState = .unavailable
|
||||
setupStep = .cert
|
||||
needsSetup = true
|
||||
message = "CA 证书未安装或未信任"
|
||||
message = String(localized: "CA 证书未安装或未信任")
|
||||
case .verificationInProgress, .verificationSuperseded:
|
||||
break
|
||||
default:
|
||||
trustState = .unavailable
|
||||
setupStep = .proxy
|
||||
needsSetup = true
|
||||
message = "Wi-Fi 代理未正确设置,请检查 127.0.0.1:8888"
|
||||
message = String(localized: "Wi-Fi 代理未正确设置,请检查 127.0.0.1:8888")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -108,36 +108,36 @@ final class SetupCoordinator: ObservableObject {
|
||||
testLog = ""
|
||||
let log = { (msg: String) in self.testLog += msg + "\n" }
|
||||
|
||||
log("======== 代理验证测试 ========")
|
||||
log("App 版本: \(appVersion)")
|
||||
log("系统版本: iOS \(UIDevice.current.systemVersion)")
|
||||
log(String(localized: "======== 代理验证测试 ========"))
|
||||
log(String(localized: "App 版本: \(appVersion)"))
|
||||
log(String(localized: "系统版本: iOS \(UIDevice.current.systemVersion)"))
|
||||
log("")
|
||||
|
||||
// Step A: Proxy running
|
||||
log("[步骤 A] 检查代理是否运行…")
|
||||
log(" 端口: 127.0.0.1:8888")
|
||||
log(String(localized: "[步骤 A] 检查代理是否运行…"))
|
||||
log(String(localized: " 端口: 127.0.0.1:8888"))
|
||||
let stepAStart = Date()
|
||||
if !proxy.isRunning {
|
||||
log(" ⚠ 代理未运行,尝试启动…")
|
||||
log(String(localized: " ⚠ 代理未运行,尝试启动…"))
|
||||
do { try await proxy.start() } catch {
|
||||
log(" ✗ 启动失败: \(error.localizedDescription)")
|
||||
log(String(localized: " ✗ 启动失败: \(error.localizedDescription)"))
|
||||
return .proxyNotRunning
|
||||
}
|
||||
log(" ✓ 代理启动成功")
|
||||
log(String(localized: " ✓ 代理启动成功"))
|
||||
} else {
|
||||
log(" ✓ 代理已在运行中")
|
||||
log(String(localized: " ✓ 代理已在运行中"))
|
||||
}
|
||||
collectProxyLogs(since: stepAStart, to: log)
|
||||
|
||||
// Step B: Combined CA + WiFi proxy check (single request)
|
||||
log("")
|
||||
log("[步骤 B] 检测证书与 WiFi 代理…")
|
||||
log(" 方式: 请求 baidu.com/paopao-verify-<token>")
|
||||
log(" 结果判定: TLS 错误=证书问题 / 响应不匹配=代理未配置 / 匹配=通过")
|
||||
log(String(localized: "[步骤 B] 检测证书与 WiFi 代理…"))
|
||||
log(String(localized: " 方式: 请求 baidu.com/paopao-verify-<token>"))
|
||||
log(String(localized: " 结果判定: TLS 错误=证书问题 / 响应不匹配=代理未配置 / 匹配=通过"))
|
||||
let stepBStart = Date()
|
||||
let verifyToken = CoreBridge.refreshVerifyToken()
|
||||
guard !verifyToken.isEmpty else {
|
||||
log(" ✗ 无法生成验证 token")
|
||||
log(String(localized: " ✗ 无法生成验证 token"))
|
||||
return .certNotTrusted
|
||||
}
|
||||
do {
|
||||
@@ -150,17 +150,17 @@ final class SetupCoordinator: ObservableObject {
|
||||
let statusCode = (resp as? HTTPURLResponse)?.statusCode ?? 0
|
||||
let body = String(data: data, encoding: .utf8) ?? ""
|
||||
if body == verifyToken {
|
||||
log(" ✓ 证书已信任,WiFi 代理已配置")
|
||||
log(String(localized: " ✓ 证书已信任,WiFi 代理已配置"))
|
||||
} else {
|
||||
log(" ✗ 响应不匹配: HTTP \(statusCode), \(data.count) bytes,WiFi 代理未配置")
|
||||
log(String(localized: " ✗ 响应不匹配: HTTP \(statusCode), \(data.count) bytes,WiFi 代理未配置"))
|
||||
return .wifiProxyNotConfigured
|
||||
}
|
||||
} catch {
|
||||
let ns = error as NSError
|
||||
let msg = error.localizedDescription
|
||||
log(" ✗ 请求失败 [\(ns.domain) code=\(ns.code)]: \(msg)")
|
||||
log(String(localized: " ✗ 请求失败 [\(ns.domain) code=\(ns.code)]: \(msg)"))
|
||||
if isCertificateTrustError(nsError: ns, message: msg) {
|
||||
log(" TLS/证书校验失败,CA 证书未信任")
|
||||
log(String(localized: " TLS/证书校验失败,CA 证书未信任"))
|
||||
return .certNotTrusted
|
||||
}
|
||||
return .wifiProxyNotConfigured
|
||||
@@ -168,7 +168,7 @@ final class SetupCoordinator: ObservableObject {
|
||||
collectProxyLogs(since: stepBStart, to: log)
|
||||
|
||||
log("")
|
||||
log("======== 环境检测通过 ✓ ========")
|
||||
log(String(localized: "======== 环境检测通过 ✓ ========"))
|
||||
return .success
|
||||
}
|
||||
|
||||
@@ -202,9 +202,9 @@ final class SetupCoordinator: ObservableObject {
|
||||
$0.source == "CORE" && $0.category == "Proxy" && $0.timestamp >= date
|
||||
}
|
||||
guard !proxyEntries.isEmpty else { return }
|
||||
log(" --- 代理日志 ---")
|
||||
log(String(localized: " --- 代理日志 ---"))
|
||||
for e in proxyEntries {
|
||||
log(" " + e.message)
|
||||
log(" " + e.localizedMessage)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -25,13 +25,13 @@ enum SystemSettingsDestination {
|
||||
var manualPath: String {
|
||||
switch self {
|
||||
case .appPermissions:
|
||||
return "请手动打开「设置」,找到本 App 后检查定位权限。"
|
||||
return String(localized: "请手动打开「设置」,找到本 App 后检查定位权限。")
|
||||
case .general:
|
||||
return "请手动打开「设置 → 通用」。"
|
||||
return String(localized: "请手动打开「设置 → 通用」。")
|
||||
case .wifi:
|
||||
return "请手动打开「设置 → 无线局域网」,进入当前 Wi-Fi 的详情页。"
|
||||
return String(localized: "请手动打开「设置 → 无线局域网」,进入当前 Wi-Fi 的详情页。")
|
||||
case .locationServices:
|
||||
return "请手动打开「设置 → 隐私与安全性 → 定位服务」。"
|
||||
return String(localized: "请手动打开「设置 → 隐私与安全性 → 定位服务」。")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+5
-5
@@ -23,7 +23,7 @@ struct TipSheetView: View {
|
||||
}
|
||||
}.padding(16)
|
||||
}
|
||||
.navigationTitle(kind.rawValue).navigationBarTitleDisplayMode(.inline)
|
||||
.navigationTitle(LocalizedStringKey(kind.rawValue)).navigationBarTitleDisplayMode(.inline)
|
||||
.safeAreaInset(edge: .bottom) {
|
||||
Button { dismiss() } label: {
|
||||
Text("知道了").font(.body.weight(.medium)).frame(maxWidth: .infinity).padding(.vertical, 12)
|
||||
@@ -80,7 +80,7 @@ struct ActivationTipContent: View {
|
||||
|
||||
}
|
||||
|
||||
private func step(_ n: Int, _ title: String, _ detail: String) -> some View {
|
||||
private func step(_ n: Int, _ title: LocalizedStringKey, _ detail: LocalizedStringKey) -> some View {
|
||||
HStack(alignment: .top, spacing: 8) {
|
||||
Text("\(n)").font(.caption2.bold())
|
||||
.frame(width: 20, height: 20)
|
||||
@@ -92,7 +92,7 @@ struct ActivationTipContent: View {
|
||||
}
|
||||
}
|
||||
|
||||
private func systemStep(_ n: Int, _ title: String, _ detail: String) -> some View {
|
||||
private func systemStep(_ n: Int, _ title: LocalizedStringKey, _ detail: LocalizedStringKey) -> some View {
|
||||
HStack(alignment: .top, spacing: 8) {
|
||||
Text("\(n)").font(.caption2.bold())
|
||||
.frame(width: 20, height: 20)
|
||||
@@ -141,7 +141,7 @@ struct DeactivationTipContent: View {
|
||||
|
||||
}
|
||||
|
||||
private func step(_ n: Int, _ title: String, _ detail: String) -> some View {
|
||||
private func step(_ n: Int, _ title: LocalizedStringKey, _ detail: LocalizedStringKey) -> some View {
|
||||
HStack(alignment: .top, spacing: 8) {
|
||||
Text("\(n)").font(.caption2.bold())
|
||||
.frame(width: 20, height: 20)
|
||||
@@ -153,7 +153,7 @@ struct DeactivationTipContent: View {
|
||||
}
|
||||
}
|
||||
|
||||
private func systemStep(_ n: Int, _ title: String, _ detail: String) -> some View {
|
||||
private func systemStep(_ n: Int, _ title: LocalizedStringKey, _ detail: LocalizedStringKey) -> some View {
|
||||
HStack(alignment: .top, spacing: 8) {
|
||||
Text("\(n)").font(.caption2.bold())
|
||||
.frame(width: 20, height: 20)
|
||||
|
||||
+5
-4
@@ -229,10 +229,10 @@ Current client status:
|
||||
Community configurations are reviewed by client. Accepted submissions are linked in this table with attribution unless
|
||||
the contributor requests anonymous inclusion.
|
||||
|
||||
Module snapshots and provenance:
|
||||
Modules and scripts are now hosted by this repository. The original
|
||||
`Yu9191/wloc` repository was deleted and remains attribution only, not a runtime dependency:
|
||||
|
||||
- [Third-party module documentation](docs/THIRD_PARTY_MODULES.md)
|
||||
- [Yu9191/wloc](https://github.com/Yu9191/wloc)
|
||||
|
||||
The selected client owns its certificates, MITM configuration, and proxy switches. Review third-party modules and
|
||||
scripts before importing them.
|
||||
@@ -364,7 +364,7 @@ Deploy it to a test device using your own signing and installation process.
|
||||
- Runtime logs remain in the device App Group container and retain only the latest three days;
|
||||
- Issue reports are copied by the user before being submitted to GitHub;
|
||||
- App Mode accesses the local proxy and the environment-verification URL;
|
||||
- Third-party Proxy Mode may access the upstream module URL and the WLOC configuration endpoint;
|
||||
- Third-party Proxy Mode may access module URLs hosted by this repository and the WLOC configuration endpoint;
|
||||
- The CA private key generated by the app is stored in the device Keychain;
|
||||
- Third-party MITM, certificates, and proxy behavior are owned by the selected client.
|
||||
|
||||
@@ -443,7 +443,8 @@ guarantee for every app or release.
|
||||
|
||||
## Acknowledgements and Links
|
||||
|
||||
The core location-response handling approach, Go implementation, and third-party modules are based on:
|
||||
The core location-response handling approach, Go implementation, and third-party modules are based on
|
||||
the following projects (`Yu9191/wloc` has since been deleted):
|
||||
|
||||
- [Yu9191/wloc](https://github.com/Yu9191/wloc)
|
||||
- [ios-location-spoofer](https://github.com/mekos2772/ios-location-spoofer)
|
||||
|
||||
@@ -219,10 +219,9 @@ App 只验证配置接口的 HTTP 状态、JSON 格式和坐标回读,不管
|
||||
|
||||
社区配置按客户端分区审核;采纳后会在上表链接教程和投稿者,投稿者也可以选择匿名收录。
|
||||
|
||||
相关模块快照和来源记录:
|
||||
模块与脚本现由本仓库托管,原 `Yu9191/wloc` 仓库已删除,仅保留来源致谢,不再作为运行时依赖:
|
||||
|
||||
- [第三方模块说明](docs/THIRD_PARTY_MODULES.md)
|
||||
- [Yu9191/wloc](https://github.com/Yu9191/wloc)
|
||||
|
||||
第三方客户端、证书、MITM 和代理开关由客户端自身负责。导入任何第三方模块前,请先审查其配置和脚本内容。
|
||||
|
||||
@@ -351,7 +350,7 @@ dist/PaopaoLocationSpoofer-unsigned.ipa
|
||||
- 运行日志保存在设备 App Group 容器中,并自动保留近三天;
|
||||
- 问题报告需要用户主动复制后提交到 GitHub;
|
||||
- APP 模式会访问本机代理和环境验证地址;
|
||||
- 第三方代理模式可能访问上游模块地址和 WLOC 配置接口;
|
||||
- 第三方代理模式可能访问本仓库托管的模块地址和 WLOC 配置接口;
|
||||
- App 生成的 CA 私钥保存在设备 Keychain 中;
|
||||
- 第三方客户端模块、MITM 和证书链路由用户选择的客户端负责。
|
||||
|
||||
@@ -430,7 +429,7 @@ GitHub Issue Form 中的“App 生成的诊断报告”字段与 App 复制内
|
||||
|
||||
## 致谢与友链
|
||||
|
||||
核心定位响应处理思路、Go 实现和第三方模块参考自:
|
||||
核心定位响应处理思路、Go 实现和第三方模块参考自(`Yu9191/wloc` 原仓库已删除):
|
||||
|
||||
- [Yu9191/wloc](https://github.com/Yu9191/wloc)
|
||||
- [ios-location-spoofer](https://github.com/mekos2772/ios-location-spoofer)
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
#!name=Apple WLOC 定位修改
|
||||
#!desc=Apple 网络定位修改 | 使用说明: https://github.com/xweiba/location-spoofer/blob/main/docs/THIRD_PARTY_MODULES.md
|
||||
#!author=Yu9191 Rewrite
|
||||
#!homepage=https://github.com/xweiba/location-spoofer
|
||||
|
||||
[rewrite_local]
|
||||
^https?:\/\/(?:gs-loc(?:-cn)?\.apple\.com|gsp-ssl\.ls\.apple\.com|bluedot\.is\.autonavi\.com(?:\.gds\.alibabadns\.com)?)\/clls\/wloc url script-response-body https://gh-proxy.org/https://raw.githubusercontent.com/xweiba/location-spoofer/main/ThirdParty/WlocScripts/dist/v1/wloc.js?v=1.0.8
|
||||
^https?:\/\/gs-loc(-cn)?\.apple\.com\/wloc-settings\/save url script-echo-response https://gh-proxy.org/https://raw.githubusercontent.com/xweiba/location-spoofer/main/ThirdParty/WlocScripts/dist/v1/wloc-settings.js?v=1.0.8
|
||||
|
||||
[mitm]
|
||||
hostname = gs-loc.apple.com, gs-loc-cn.apple.com, gsp-ssl.ls.apple.com, bluedot.is.autonavi.com, bluedot.is.autonavi.com.gds.alibabadns.com
|
||||
@@ -0,0 +1,20 @@
|
||||
#!name=Apple WLOC 定位修改
|
||||
#!desc=Apple 网络定位修改 | 使用说明: https://github.com/xweiba/location-spoofer/blob/main/docs/THIRD_PARTY_MODULES.md
|
||||
#!icon=https://raw.githubusercontent.com/xweiba/location-spoofer/main/Resources/Assets.xcassets/AppIcon.appiconset/AppIcon.png
|
||||
#!author=Yu9191 Rewrite
|
||||
#!homepage=https://github.com/xweiba/location-spoofer
|
||||
|
||||
|
||||
[Argument]
|
||||
longitude = input, "113.94114", tag=经度(在线选点优先)
|
||||
latitude = input, "22.544577", tag=纬度(在线选点优先)
|
||||
accuracy = input, "25", tag=精度(米)
|
||||
randomRadius = input, "0", tag=扰动半径(米,0为关闭)
|
||||
logLevel = select, "info", "off", "error", "warn", "debug", "all", tag=日志级别
|
||||
|
||||
[Script]
|
||||
http-response ^https?:\/\/(?:gs-loc(?:-cn)?\.apple\.com|gsp-ssl\.ls\.apple\.com|bluedot\.is\.autonavi\.com(?:\.gds\.alibabadns\.com)?)\/clls\/wloc script-path=https://gh-proxy.org/https://raw.githubusercontent.com/xweiba/location-spoofer/main/ThirdParty/WlocScripts/dist/v1/wloc.js?v=1.0.8, requires-body=true, binary-body-mode=true, timeout=30, tag=Apple WLOC, argument=[{longitude},{latitude},{accuracy},{randomRadius},{logLevel}]
|
||||
http-request ^https?:\/\/gs-loc(-cn)?\.apple\.com\/wloc-settings\/save script-path=https://gh-proxy.org/https://raw.githubusercontent.com/xweiba/location-spoofer/main/ThirdParty/WlocScripts/dist/v1/wloc-settings.js?v=1.0.8, timeout=10, tag=WLOC Settings
|
||||
|
||||
[MITM]
|
||||
hostname = gs-loc.apple.com, gs-loc-cn.apple.com, gsp-ssl.ls.apple.com, bluedot.is.autonavi.com, bluedot.is.autonavi.com.gds.alibabadns.com
|
||||
@@ -0,0 +1,13 @@
|
||||
#!name=Apple WLOC 定位修改
|
||||
#!desc=Apple 网络定位修改 | 使用说明: https://github.com/xweiba/location-spoofer/blob/main/docs/THIRD_PARTY_MODULES.md
|
||||
#!author=Yu9191 Rewrite
|
||||
#!homepage=https://github.com/xweiba/location-spoofer
|
||||
#!icon=https://raw.githubusercontent.com/xweiba/location-spoofer/main/Resources/Assets.xcassets/AppIcon.appiconset/AppIcon.png
|
||||
#!category=Tools
|
||||
|
||||
[Script]
|
||||
Apple WLOC = type=http-response,pattern=^https?:\/\/(?:gs-loc(?:-cn)?\.apple\.com|gsp-ssl\.ls\.apple\.com|bluedot\.is\.autonavi\.com(?:\.gds\.alibabadns\.com)?)\/clls\/wloc,requires-body=1,binary-body-mode=1,max-size=0,timeout=30,script-path=https://gh-proxy.org/https://raw.githubusercontent.com/xweiba/location-spoofer/main/ThirdParty/WlocScripts/dist/v1/wloc.js?v=1.0.8,argument=longitude=113.94114&latitude=22.544577&accuracy=25&randomRadius=0&logLevel=info
|
||||
WLOC Settings = type=http-request,pattern=^https?:\/\/gs-loc(-cn)?\.apple\.com\/wloc-settings\/save,requires-body=0,max-size=0,timeout=10,script-path=https://gh-proxy.org/https://raw.githubusercontent.com/xweiba/location-spoofer/main/ThirdParty/WlocScripts/dist/v1/wloc-settings.js?v=1.0.8
|
||||
|
||||
[MITM]
|
||||
hostname = %APPEND% gs-loc.apple.com, gs-loc-cn.apple.com, gsp-ssl.ls.apple.com, bluedot.is.autonavi.com, bluedot.is.autonavi.com.gds.alibabadns.com
|
||||
@@ -0,0 +1,14 @@
|
||||
#!name=Apple WLOC 定位修改
|
||||
#!desc=Apple 网络定位修改 | 使用说明: https://github.com/xweiba/location-spoofer/blob/main/docs/THIRD_PARTY_MODULES.md
|
||||
#!author=Yu9191 Rewrite
|
||||
#!homepage=https://github.com/xweiba/location-spoofer
|
||||
#!category=Tools
|
||||
#!arguments=经度:113.94114, 纬度:22.544577, 精度:25, 扰动半径:0, 日志级别:info
|
||||
#!arguments-desc=经度/纬度: 默认坐标(在线选点储存后优先)\n精度: GPS精度(米)\n扰动半径: 每次响应在目标点周围随机偏移的最大距离(米),0为关闭\n日志级别: off/error/warn/info/debug/all\n\n使用方法: 打开选点页面 -> 选位置 -> 储存到设备
|
||||
|
||||
[Script]
|
||||
Apple WLOC = type=http-response, pattern="^https?:\/\/(?:gs-loc(?:-cn)?\.apple\.com|gsp-ssl\.ls\.apple\.com|bluedot\.is\.autonavi\.com(?:\.gds\.alibabadns\.com)?)\/clls\/wloc", requires-body=1, binary-body-mode=1, max-size=0, timeout=30, script-path=https://gh-proxy.org/https://raw.githubusercontent.com/xweiba/location-spoofer/main/ThirdParty/WlocScripts/dist/v1/wloc.js?v=1.0.8, argument=longitude={{{经度}}}&latitude={{{纬度}}}&accuracy={{{精度}}}&randomRadius={{{扰动半径}}}&logLevel={{{日志级别}}}
|
||||
WLOC Settings = type=http-request, pattern="^https?:\/\/gs-loc(-cn)?\.apple\.com\/wloc-settings\/save", requires-body=0, max-size=0, timeout=10, script-path=https://gh-proxy.org/https://raw.githubusercontent.com/xweiba/location-spoofer/main/ThirdParty/WlocScripts/dist/v1/wloc-settings.js?v=1.0.8
|
||||
|
||||
[MITM]
|
||||
hostname = %APPEND% gs-loc.apple.com, gs-loc-cn.apple.com, gsp-ssl.ls.apple.com, bluedot.is.autonavi.com, bluedot.is.autonavi.com.gds.alibabadns.com
|
||||
@@ -0,0 +1,36 @@
|
||||
name: Apple WLOC 定位修改
|
||||
desc: "Apple 网络定位修改 | 使用说明: https://github.com/xweiba/location-spoofer/blob/main/docs/THIRD_PARTY_MODULES.md"
|
||||
author: Yu9191 Rewrite
|
||||
homepage: https://github.com/xweiba/location-spoofer
|
||||
icon: https://raw.githubusercontent.com/xweiba/location-spoofer/main/Resources/Assets.xcassets/AppIcon.appiconset/AppIcon.png
|
||||
category: Tools
|
||||
|
||||
http:
|
||||
mitm:
|
||||
- "gs-loc.apple.com"
|
||||
- "gs-loc-cn.apple.com"
|
||||
- "gsp-ssl.ls.apple.com"
|
||||
- "bluedot.is.autonavi.com"
|
||||
- "bluedot.is.autonavi.com.gds.alibabadns.com"
|
||||
script:
|
||||
- match: ^https?:\/\/(?:gs-loc(?:-cn)?\.apple\.com|gsp-ssl\.ls\.apple\.com|bluedot\.is\.autonavi\.com(?:\.gds\.alibabadns\.com)?)\/clls\/wloc
|
||||
name: WLOC.Location
|
||||
type: response
|
||||
require-body: true
|
||||
binary-mode: true
|
||||
max-size: 0
|
||||
timeout: 30
|
||||
argument: longitude=113.94114&latitude=22.544577&accuracy=25&randomRadius=0&logLevel=info
|
||||
- match: ^https?:\/\/gs-loc(-cn)?\.apple\.com\/wloc-settings\/save
|
||||
name: WLOC.Settings
|
||||
type: request
|
||||
require-body: false
|
||||
timeout: 10
|
||||
|
||||
script-providers:
|
||||
WLOC.Location:
|
||||
url: https://gh-proxy.org/https://raw.githubusercontent.com/xweiba/location-spoofer/main/ThirdParty/WlocScripts/dist/v1/wloc.js?v=1.0.8
|
||||
interval: 86400
|
||||
WLOC.Settings:
|
||||
url: https://gh-proxy.org/https://raw.githubusercontent.com/xweiba/location-spoofer/main/ThirdParty/WlocScripts/dist/v1/wloc-settings.js?v=1.0.8
|
||||
interval: 86400
|
||||
@@ -0,0 +1,6 @@
|
||||
/* App display name */
|
||||
"CFBundleDisplayName" = "Location Spoofer";
|
||||
|
||||
/* Location permission prompts */
|
||||
"NSLocationWhenInUseUsageDescription" = "Used to display and verify whether the simulated location is active.";
|
||||
"NSLocationAlwaysAndWhenInUseUsageDescription" = "Used to display and verify whether the simulated location is active.";
|
||||
@@ -0,0 +1,703 @@
|
||||
/* ============================================================
|
||||
English localization overlay.
|
||||
Keys are the original Simplified-Chinese source strings
|
||||
(development language = zh-Hans). Any key not found here
|
||||
falls back to the Chinese key text automatically.
|
||||
============================================================ */
|
||||
|
||||
/* MARK: - Shared models (ProxyRuntimeMode / VerificationResult) */
|
||||
"APP模式" = "App Mode";
|
||||
"第三方代理模式" = "Third-party Proxy Mode";
|
||||
"成功" = "Success";
|
||||
"已有验证正在进行" = "A verification is already in progress";
|
||||
"验证已被新位置取代" = "Verification superseded by a new location";
|
||||
"证书未信任" = "Certificate not trusted";
|
||||
"WiFi代理未配置" = "Wi-Fi proxy not configured";
|
||||
"坐标写入失败" = "Failed to write coordinate";
|
||||
"改写验证失败" = "Rewrite verification failed";
|
||||
|
||||
/* MARK: - SettingsView */
|
||||
"运行模式" = "Runtime Mode";
|
||||
"模式" = "Mode";
|
||||
"状态" = "Status";
|
||||
"本机代理" = "Local Proxy";
|
||||
"第三方模块" = "Third-party Module";
|
||||
"检测连接" = "Test Connection";
|
||||
"虚拟定位" = "Simulated Location";
|
||||
"定位模拟" = "Location Simulation";
|
||||
"运动状态模拟" = "Motion State Simulation";
|
||||
"实验性功能,默认关闭。开启后会同时模拟定位响应中的运动状态。" = "Experimental, off by default. When enabled, the motion state in the location response is simulated too.";
|
||||
"随机扰动" = "Random Jitter";
|
||||
"开启后,下次同步坐标时会给目标点添加随机偏移(默认半径 50 米),避免位置固定在同一点。" = "When enabled, the next coordinate sync adds a random offset to the target point (default radius 50 m) so the location is not fixed at a single point.";
|
||||
"说明" = "Instructions";
|
||||
"工作原理" = "How It Works";
|
||||
"应用" = "App";
|
||||
"进入引导页" = "Open Setup Guide";
|
||||
"正在检查…" = "Checking…";
|
||||
"检查更新" = "Check for Updates";
|
||||
"版本" = "Version";
|
||||
"证书" = "Certificate";
|
||||
"重置证书" = "Reset Certificate";
|
||||
"仅删除 App 钥匙串中的设备 CA。iOS 中已经安装的旧证书需要在系统设置里手动移除。" = "Only removes the device CA from the app keychain. Any old certificate already installed in iOS must be removed manually in Settings.";
|
||||
"支持" = "Support";
|
||||
"使用帮助" = "Usage Help";
|
||||
"功能建议" = "Feature Request";
|
||||
"分享第三方配置" = "Share Third-party Config";
|
||||
"关于" = "About";
|
||||
"如果觉得好用,欢迎去 GitHub 给项目点个 Star" = "If you find it useful, please star the project on GitHub";
|
||||
"致谢" = "Acknowledgements";
|
||||
"核心定位改写逻辑移植自 Yu9191/wloc" = "Core location-rewrite logic ported from Yu9191/wloc";
|
||||
"设置" = "Settings";
|
||||
"完成" = "Done";
|
||||
"代理操作失败" = "Proxy Operation Failed";
|
||||
"重置证书?" = "Reset certificate?";
|
||||
"重置并生成新证书" = "Reset and Generate New Certificate";
|
||||
"当前虚拟定位和本地代理将停止。App 会删除钥匙串中的设备 CA、立即生成新证书,并打开安装与信任引导。你还需要前往 iOS「设置 → 通用 → VPN 与设备管理」手动删除旧证书,然后重新下载安装并完全信任新证书。" = "The current simulated location and local proxy will stop. The app deletes the device CA from the keychain, generates a new certificate immediately, and opens the install-and-trust guide. You must also go to iOS Settings → General → VPN & Device Management to manually delete the old certificate, then re-download, install, and fully trust the new one.";
|
||||
"第三方代理配置" = "Third-party Proxy Configuration";
|
||||
"客户端" = "Client";
|
||||
"使用国内镜像下载模块" = "Use domestic mirror to download modules";
|
||||
"仅影响之后复制和重新导入的模块地址;已安装模块需要重新导入后切换来源。" = "Only affects module URLs you copy or re-import afterward; installed modules must be re-imported to switch source.";
|
||||
"验证状态" = "Verification Status";
|
||||
"已复制模块订阅地址" = "Subscription URL copied";
|
||||
"复制模块订阅地址" = "Copy module subscription URL";
|
||||
"已复制解密域名" = "Decryption hostnames copied";
|
||||
"复制解密域名" = "Copy decryption hostnames";
|
||||
"打开 %@" = "Open %@";
|
||||
"重新打开配置引导" = "Reopen Setup Guide";
|
||||
"Egern 直接使用 Surge 的 .sgmodule 模块。" = "Egern uses Surge's .sgmodule module directly.";
|
||||
"Stash 直接订阅 .stoverride,不要通过 Script Hub 转换。" = "Stash subscribes to .stoverride directly; do not convert it via Script Hub.";
|
||||
"复制模块订阅地址后,在对应代理客户端中添加模块/重写订阅,并为复制的全部域名(含 gsp-ssl.ls.apple.com、bluedot.is.autonavi.com)启用 MITM。第三方客户端保存坐标后,即使关闭本 App,坐标仍由代理客户端持久化并继续生效。" = "After copying the subscription URL, add the module/rewrite subscription in your proxy client and enable MITM for all copied hostnames (including gsp-ssl.ls.apple.com and bluedot.is.autonavi.com). Once the third-party client saves the coordinate, it persists and stays in effect even after you close this app.";
|
||||
"已是最新版本" = "You are on the latest version";
|
||||
"当前版本 %@,远程最新版本 %@。" = "Current version %1$@, latest remote version %2$@.";
|
||||
"检查更新失败" = "Update Check Failed";
|
||||
"无法获取远程版本信息,请检查网络后重试。" = "Could not fetch remote version info. Check your network and try again.";
|
||||
"未检测" = "Not checked";
|
||||
"已连接,有坐标" = "Connected, coordinate present";
|
||||
"已连接,无坐标" = "Connected, no coordinate";
|
||||
"连接失败" = "Connection failed";
|
||||
"已开启" = "On";
|
||||
"已关闭" = "Off";
|
||||
"第三方已保存" = "Saved by third-party";
|
||||
"未保存" = "Not saved";
|
||||
"未知" = "Unknown";
|
||||
"App 只负责地图选点、收藏和发送 WGS-84 坐标。第三方代理客户端通过模块拦截 Apple WLOC 请求并持久化当前坐标;本模式不启动本机代理,不使用 App 的 CA,也不需要配置 127.0.0.1:8888。" = "The app only handles map selection, favorites, and sending WGS-84 coordinates. The third-party proxy client intercepts Apple WLOC requests through its module and persists the current coordinate. This mode does not start the local proxy, does not use the app's CA, and does not require configuring 127.0.0.1:8888.";
|
||||
"App 在设备本地运行一个代理服务器(127.0.0.1:8888)。\n\n通过 WiFi 手动代理配置,让系统发往 Apple 定位域名(gs-loc.apple.com、gsp-ssl.ls.apple.com、bluedot.is.autonavi.com 等)的定位请求经过这个本地代理。代理使用已安装的 CA 证书对 HTTPS 流量做中间人解密,把 Apple 返回的定位坐标改写为你设置的虚拟坐标,再加密返回给系统,从而实现虚拟定位。" = "The app runs a proxy server locally on the device (127.0.0.1:8888).\n\nA manual Wi-Fi proxy routes location requests sent to Apple location hostnames (gs-loc.apple.com, gsp-ssl.ls.apple.com, bluedot.is.autonavi.com, etc.) through this local proxy. Using the installed CA certificate, the proxy performs man-in-the-middle decryption of the HTTPS traffic, rewrites the location coordinate returned by Apple to the one you set, then re-encrypts and returns it to the system — producing the simulated location.";
|
||||
"模式已切换" = "Mode Switched";
|
||||
"第三方代理模式检测通过。请关闭 Wi-Fi 中的 127.0.0.1:8888 手动代理,避免双重拦截。" = "Third-party proxy mode check passed. Please turn off the 127.0.0.1:8888 manual proxy in Wi-Fi to avoid double interception.";
|
||||
"APP 模式环境检测通过。请停用第三方 WLOC 模块或代理连接,避免双重拦截。" = "App mode environment check passed. Please disable the third-party WLOC module or proxy connection to avoid double interception.";
|
||||
"证书重置失败" = "Certificate Reset Failed";
|
||||
"无法打开客户端" = "Could Not Open Client";
|
||||
"无法打开 %@,请确认客户端已安装后手动打开。" = "Could not open %@. Make sure the client is installed, then open it manually.";
|
||||
|
||||
/* MARK: - ContentView */
|
||||
"正在初始化地图与本地代理…" = "Initializing map and local proxy…";
|
||||
"正在初始化地图…" = "Initializing map…";
|
||||
"更新说明暂时无法加载,请前往最新 Release 页面查看。" = "Release notes could not be loaded. Please check the latest Release page.";
|
||||
"需要更新" = "Update Required";
|
||||
"当前版本 %@ 已停止支持,请更新到 %@ 后继续使用。\n\n%@" = "Version %1$@ is no longer supported. Please update to %2$@ to continue.\n\n%3$@";
|
||||
"立即更新" = "Update Now";
|
||||
"发现新版本" = "New Version Available";
|
||||
"当前版本 %@,最新版本 %@。\n\n%@" = "Current version %1$@, latest version %2$@.\n\n%3$@";
|
||||
"前往更新" = "Update";
|
||||
"稍后" = "Later";
|
||||
|
||||
/* MARK: - BugReportView */
|
||||
"遇到问题时,在这里生成 Bug 报告。系统会运行一次诊断测试,并将完整报告复制到剪切板。跳转到 GitHub 后,请粘贴到“App 生成的诊断报告”字段。" = "When you run into a problem, generate a bug report here. The app runs a diagnostic test and copies the full report to the clipboard. After opening GitHub, paste it into the “App-generated diagnostic report” field.";
|
||||
"可复现环境" = "Reproducible";
|
||||
"当前设备上问题稳定复现,非偶发性。" = "The issue reproduces reliably on this device and is not intermittent.";
|
||||
"问题描述" = "Problem Description";
|
||||
"正在生成报告…" = "Generating report…";
|
||||
"生成 Bug 报告" = "Generate Bug Report";
|
||||
"报告 Bug" = "Report a Bug";
|
||||
"已生成" = "Generated";
|
||||
"打开 GitHub 表单" = "Open GitHub Form";
|
||||
"稍后再说" = "Later";
|
||||
"Bug 报告已复制到剪切板。请在 GitHub 表单的“App 生成的诊断报告”字段中粘贴并提交。" = "The bug report has been copied to the clipboard. Paste it into the “App-generated diagnostic report” field of the GitHub form and submit.";
|
||||
|
||||
/* MARK: - DiagnosticsView */
|
||||
"过滤日志" = "Filter logs";
|
||||
"暂无运行日志" = "No runtime logs";
|
||||
"无匹配日志" = "No matching logs";
|
||||
"运行日志" = "Runtime Logs";
|
||||
"日志自动清理,仅保留近 3 天" = "Logs are cleared automatically; only the last 3 days are kept";
|
||||
"关闭" = "Close";
|
||||
"复制全部日志" = "Copy all logs";
|
||||
"清空全部日志" = "Clear all logs";
|
||||
"清空所有运行日志?" = "Clear all runtime logs?";
|
||||
"清空日志" = "Clear Logs";
|
||||
"取消" = "Cancel";
|
||||
"正在检测…" = "Checking…";
|
||||
"环境检测" = "Environment Check";
|
||||
"检查第三方模块能否拦截并响应 query 请求;不会写入测试坐标。" = "Checks whether the third-party module can intercept and respond to the query request; no test coordinates are written.";
|
||||
"依次检查:本地代理 → CA 证书信任 → Wi-Fi 代理链路。" = "Checks in order: local proxy → CA certificate trust → Wi-Fi proxy chain.";
|
||||
"测试日志" = "Test Log";
|
||||
"已复制" = "Copied";
|
||||
"代理运行中" = "Proxy running";
|
||||
"代理未运行" = "Proxy stopped";
|
||||
"可修改" = "Modifiable";
|
||||
"不可修改" = "Not modifiable";
|
||||
"环境检测通过" = "Environment check passed";
|
||||
"环境检测失败: %@" = "Environment check failed: %@";
|
||||
",查看下方日志" = ", see the log below";
|
||||
"第三方模块连接通过,已有坐标" = "Third-party module connected, coordinate present";
|
||||
"第三方模块连接通过,暂无坐标" = "Third-party module connected, no coordinate yet";
|
||||
"第三方模块连接失败" = "Third-party module connection failed";
|
||||
"======== 第三方代理连接检测 ========" = "======== Third-party Proxy Connection Check ========";
|
||||
"模式: 测试模式" = "Mode: Test mode";
|
||||
"请求: wloc-settings/save?action=query" = "Request: wloc-settings/save?action=query";
|
||||
"拦截响应: 有效 JSON" = "Intercepted response: Valid JSON";
|
||||
"已保存坐标: %@" = "Saved coordinate: %@";
|
||||
"结果: %@" = "Result: %@";
|
||||
|
||||
/* MARK: - TipViews */
|
||||
"生效说明" = "How to Activate";
|
||||
"失效说明" = "How to Deactivate";
|
||||
"关闭 WiFi 代理" = "Remove Wi-Fi Proxy";
|
||||
"知道了" = "Got It";
|
||||
"让虚拟定位生效" = "Make the simulated location take effect";
|
||||
"确认第三方代理已开启" = "Confirm the third-party proxy is on";
|
||||
"保持已导入的 WLOC 模块、HTTPS 解密和第三方代理/VPN 连接开启。" = "Keep the imported WLOC module, HTTPS decryption, and the third-party proxy/VPN connection enabled.";
|
||||
"开启飞行模式" = "Turn on Airplane Mode";
|
||||
"从控制中心打开飞行模式(点飞机图标),Wi‑Fi 会自动断开。这是为了清除 iOS 的定位缓存。等待 2 秒。" = "Open Control Center and enable Airplane Mode (tap the airplane icon); Wi-Fi disconnects automatically. This clears the iOS location cache. Wait 2 seconds.";
|
||||
"关闭 Wi‑Fi" = "Turn off Wi-Fi";
|
||||
"从控制中心再点一下 Wi‑Fi 图标,确认 Wi‑Fi 已关闭。等待 2 秒。" = "Tap the Wi-Fi icon in Control Center again and confirm Wi-Fi is off. Wait 2 seconds.";
|
||||
"关闭系统定位服务" = "Turn off Location Services";
|
||||
"打开系统「设置 → 隐私与安全性 → 定位服务」,关闭顶部的总开关。等待 2 秒。" = "Open Settings → Privacy & Security → Location Services and turn off the master switch at the top. Wait 2 seconds.";
|
||||
"打开 Wi‑Fi,启动虚拟定位" = "Turn on Wi-Fi and start the simulated location";
|
||||
"从控制中心打开 Wi‑Fi(飞行模式保持开启),确认第三方代理已连接。坐标已经同步到第三方代理。等待 2 秒。" = "Turn on Wi-Fi from Control Center (keep Airplane Mode on) and confirm the third-party proxy is connected. The coordinate is already synced to the third-party proxy. Wait 2 seconds.";
|
||||
"从控制中心打开 Wi‑Fi(飞行模式保持开启),进入 App 点底部「开始虚拟定位」。等待 2 秒。" = "Turn on Wi-Fi from Control Center (keep Airplane Mode on), then open the app and tap “Start Simulated Location” at the bottom. Wait 2 seconds.";
|
||||
"关闭飞行模式" = "Turn off Airplane Mode";
|
||||
"从控制中心关闭飞行模式。等待 2 秒。" = "Turn off Airplane Mode from Control Center. Wait 2 seconds.";
|
||||
"重新开启定位服务" = "Turn Location Services back on";
|
||||
"再次进入「设置 → 隐私与安全性 → 定位服务」,打开总开关。完成后打开地图验证定位是否已变化。" = "Open Settings → Privacy & Security → Location Services again and turn the master switch on. Then open a map app to verify the location has changed.";
|
||||
"还是无法生效?" = "Still not working?";
|
||||
"操作到第 3 步时关机重启,开机后从第 4 步继续。这样能彻底清除系统缓存的定位数据。" = "At step 3, power off and restart the device, then continue from step 4. This fully clears the system's cached location data.";
|
||||
"去设置" = "Open Settings";
|
||||
"取消虚拟定位" = "Cancel the simulated location";
|
||||
"从控制中心打开飞行模式,Wi‑Fi 会自动断开。等待 2 秒。" = "Enable Airplane Mode from Control Center; Wi-Fi disconnects automatically. Wait 2 seconds.";
|
||||
"从控制中心确认 Wi‑Fi 已关闭。等待 2 秒。" = "Confirm Wi-Fi is off from Control Center. Wait 2 seconds.";
|
||||
"打开「设置 → 隐私与安全性 → 定位服务」,关闭总开关。等待 2 秒。" = "Open Settings → Privacy & Security → Location Services and turn off the master switch. Wait 2 seconds.";
|
||||
"确认坐标已清除" = "Confirm the coordinate is cleared";
|
||||
"App 已通知第三方代理清除虚拟坐标。保持网络可用并等待 2 秒,让系统重新获取真实定位。" = "The app has told the third-party proxy to clear the simulated coordinate. Keep the network available and wait 2 seconds so the system can reacquire the real location.";
|
||||
"打开 Wi‑Fi,移除代理" = "Turn on Wi-Fi and remove the proxy";
|
||||
"从控制中心打开 Wi‑Fi。然后进入「设置 → 无线局域网 → 点 WiFi 右侧 (i) → HTTP 代理」,选择「关闭」后存储。等待 2 秒。" = "Turn on Wi-Fi from Control Center. Then go to Settings → Wi-Fi → tap the (i) next to your Wi-Fi → HTTP Proxy, choose Off, and save. Wait 2 seconds.";
|
||||
"再次进入「设置 → 隐私与安全性 → 定位服务」打开总开关。打开地图验证定位是否恢复。" = "Open Settings → Privacy & Security → Location Services again and turn the master switch on. Open a map app to verify the location is restored.";
|
||||
"还是无法取消?" = "Still can't cancel?";
|
||||
"操作到第 3 步时关机重启,开机后从第 4 步继续。" = "At step 3, power off and restart the device, then continue from step 4.";
|
||||
"移除代理配置" = "Remove the proxy configuration";
|
||||
"停止虚拟定位后,需要手动移除 WiFi 代理配置,否则可能无法上网。\n\n1. 打开「设置 → 无线局域网」\n2. 点击当前 WiFi 右侧 (i) 图标\n3. 找到「HTTP 代理」\n4. 选择「关闭」\n5. 点右上角「存储」" = "After stopping the simulated location, you must manually remove the Wi-Fi proxy configuration or you may lose internet access.\n\n1. Open Settings → Wi-Fi\n2. Tap the (i) icon next to your current Wi-Fi\n3. Find “HTTP Proxy”\n4. Choose “Off”\n5. Tap “Save” in the top-right corner";
|
||||
|
||||
/* MARK: - MapHomeView */
|
||||
"社区分享成功配置?" = "Share your working config with the community?";
|
||||
"去提交" = "Submit";
|
||||
"复制模板" = "Copy Template";
|
||||
"不再提示" = "Don't Show Again";
|
||||
"第三方客户端" = "Third-party client";
|
||||
"你正在使用 %@。点击“去提交”会先复制投稿模板,并在 App 内打开社区页面。采纳后将收录到 README,可选择是否匿名署名。" = "You are using %@. Tapping “Submit” first copies the submission template and opens the community page inside the app. Accepted submissions are added to the README; you can choose to stay anonymous.";
|
||||
"已复制投稿模板" = "Submission Template Copied";
|
||||
"如果 GitHub 登录或浏览器跳转后模板没有自动填充,可以直接粘贴。" = "If the template is not filled in automatically after you sign in to GitHub or the browser switches, just paste it manually.";
|
||||
"无法直接跳转" = "Cannot Open Directly";
|
||||
"定位失败" = "Location Failed";
|
||||
"打开设置" = "Open Settings";
|
||||
"无法获取当前定位,请检查定位服务是否已开启" = "Could not get the current location. Please check whether Location Services is enabled.";
|
||||
"编辑收藏名称" = "Edit Favorite Name";
|
||||
"名称" = "Name";
|
||||
"保存" = "Save";
|
||||
"修改收藏地点名称" = "Rename the favorite location";
|
||||
"搜索地点或坐标" = "Search a place or coordinates";
|
||||
"日志" = "Logs";
|
||||
"更多" = "More";
|
||||
"当前选点" = "Current Selection";
|
||||
"GCJ-02(国内)" = "GCJ-02 (Domestic)";
|
||||
"WGS-84(国际)" = "WGS-84 (Intl.)";
|
||||
"无法生效?" = "Not working?";
|
||||
"无法取消?" = "Can't cancel?";
|
||||
"已收藏,点击取消收藏" = "Favorited. Tap to remove.";
|
||||
"收藏当前选点" = "Favorite the current selection";
|
||||
"搜索或点击地图选点后,保存为收藏。" = "Search or tap the map to pick a point, then save it as a favorite.";
|
||||
"切换到此处" = "Switch Here";
|
||||
"同步到第三方代理" = "Sync to Third-party Proxy";
|
||||
"检测并同步中…" = "Checking and syncing…";
|
||||
"停止第三方虚拟定位" = "Stop Third-party Simulation";
|
||||
"开始虚拟定位" = "Start Simulated Location";
|
||||
"验证环境中…" = "Verifying environment…";
|
||||
"停止虚拟定位" = "Stop Simulated Location";
|
||||
"当前未连接可用的 Wi-Fi,请连接 Wi-Fi 后配置 127.0.0.1:8888 手动代理。" = "No usable Wi-Fi is connected. Connect to Wi-Fi, then configure the 127.0.0.1:8888 manual proxy.";
|
||||
"第三方代理查询失败" = "Third-party proxy query failed";
|
||||
"未命名" = "Unnamed";
|
||||
"没有找到相关地点" = "No matching places found";
|
||||
"虚拟定位已开启" = "Simulated Location On";
|
||||
"不再提醒" = "Don't Remind Me";
|
||||
"虚拟定位已关闭" = "Simulated Location Off";
|
||||
|
||||
/* MARK: - FirstSetupView */
|
||||
"选择模式" = "Choose Mode";
|
||||
"配置 Wi-Fi 代理" = "Configure Wi-Fi Proxy";
|
||||
"初始化 CA 证书" = "Initialize CA Certificate";
|
||||
"选择客户端" = "Choose Client";
|
||||
"导入并检测" = "Import & Test";
|
||||
"上一步" = "Back";
|
||||
"开始使用" = "Get Started";
|
||||
"操作失败" = "Operation Failed";
|
||||
"选择运行模式" = "Choose a Runtime Mode";
|
||||
"后续可在“设置 → 运行模式”中切换。两种模式不要同时拦截 WLOC 请求。" = "You can switch later in Settings → Runtime Mode. Do not intercept WLOC requests in both modes at the same time.";
|
||||
"仅 Wi-Fi" = "Wi-Fi only";
|
||||
"无外部依赖" = "No external deps";
|
||||
"App 在设备本地启动代理,通过当前 Wi-Fi 的手动 HTTP 代理改写定位响应。免费自签应用无法使用系统 VPN 的 Network Extension 能力,因此 APP模式不支持蜂窝网络,需要配置 Wi-Fi 代理并安装 App 生成的 CA。" = "The app starts a proxy locally on the device and rewrites location responses through a manual HTTP proxy on the current Wi-Fi. Free self-signed apps cannot use the system VPN's Network Extension, so App Mode does not support cellular; it requires configuring a Wi-Fi proxy and installing the CA the app generates.";
|
||||
"测试模式" = "Test mode";
|
||||
"App 负责选点,并通过 WLOC 配置接口查询和同步坐标;第三方代理客户端负责网络代理、模块拦截、MITM 和持久化。证书、VPN 与代理连接均由第三方客户端处理。" = "The app handles point selection and queries/syncs coordinates through the WLOC configuration endpoint; the third-party proxy client handles network proxying, module interception, MITM, and persistence. Certificates, VPN, and proxy connections are all handled by the third-party client.";
|
||||
"正在准备 APP模式本地服务…" = "Preparing App Mode local services…";
|
||||
"先配置 Wi-Fi 系统代理" = "First configure the Wi-Fi system proxy";
|
||||
"在当前 Wi-Fi 的详情页,将「HTTP 代理」设为「手动」:服务器填 127.0.0.1,端口填 8888。配置后点击下方「完成」。检测会自动判断是 Wi-Fi 代理还是证书信任有问题。" = "On the details page of your current Wi-Fi, set “HTTP Proxy” to “Manual”: server 127.0.0.1, port 8888. After configuring, tap “Done” below. The check automatically detects whether the Wi-Fi proxy or certificate trust is the problem.";
|
||||
"Wi-Fi 代理设置" = "Wi-Fi Proxy Settings";
|
||||
"1 选择手动,2 填写服务器 127.0.0.1,3 填写端口 8888。" = "1) Choose Manual, 2) enter server 127.0.0.1, 3) enter port 8888.";
|
||||
"复制地址" = "Copy Address";
|
||||
"打开 Wi-Fi 设置" = "Open Wi-Fi Settings";
|
||||
"第 1 步:下载证书" = "Step 1: Download the Certificate";
|
||||
"下载本机随机生成的 CA 根证书。私钥仅保存在此设备的钥匙串中,不会随证书文件导出。App 会弹出 Safari 下载页;出现配置描述文件下载提示时,选择「允许」。" = "Download the randomly generated CA root certificate for this device. The private key is stored only in this device's keychain and is never exported with the certificate file. The app opens a Safari download page; when prompted to download a configuration profile, choose “Allow”.";
|
||||
"打开下载页" = "Open Download Page";
|
||||
"无法准备证书下载页面,请查看诊断日志" = "Could not prepare the certificate download page. Please check the diagnostic log.";
|
||||
"第 2 步:安装证书" = "Step 2: Install the Certificate";
|
||||
"下载完成后打开系统「设置」。如果顶部显示「已下载描述文件」,点进去安装;否则进入「通用 → VPN 与设备管理」,找到 Location Spoofer CA 并完成安装。" = "After the download finishes, open Settings. If “Profile Downloaded” appears at the top, tap it to install; otherwise go to General → VPN & Device Management, find Location Spoofer CA, and install it.";
|
||||
"去安装" = "Install";
|
||||
"安装证书" = "Install Certificate";
|
||||
"1 在「VPN 与设备管理」中打开 Location Spoofer CA 描述文件并完成安装。" = "1) In VPN & Device Management, open the Location Spoofer CA profile and install it.";
|
||||
"第 3 步:信任证书" = "Step 3: Trust the Certificate";
|
||||
"安装后进入「设置 → 通用 → 关于本机 → 证书信任设置」,找到 Location Spoofer CA 并开启完全信任。iOS 保留钥匙串数据时,重装 App 会继续复用同一证书。" = "After installing, go to Settings → General → About → Certificate Trust Settings, find Location Spoofer CA, and enable full trust. As long as iOS keeps the keychain data, reinstalling the app reuses the same certificate.";
|
||||
"去信任" = "Trust";
|
||||
"信任证书" = "Trust Certificate";
|
||||
"1 在「证书信任设置」中为 Location Spoofer CA 开启完全信任。" = "1) In Certificate Trust Settings, enable full trust for Location Spoofer CA.";
|
||||
"已完成" = "Done";
|
||||
"已完成 ✓" = "Done ✓";
|
||||
"iOS 27 beta 6 起,系统已禁止对 gs-loc.apple.com 进行 MITM 拦截。该版本及之后的 beta 版本暂时无法使用本项目,等待后续适配方案。" = "Starting with iOS 27 beta 6, the system blocks MITM interception of gs-loc.apple.com. This project is temporarily unusable on that and later beta versions until a workaround is available.";
|
||||
"选择第三方代理客户端" = "Choose a Third-party Proxy Client";
|
||||
"除 Shadowrocket 外,当前客户端配置尚未完成真机验证,页面只提供模块导入入口和通用配置提醒。" = "Except for Shadowrocket, the current client configurations have not been fully verified on real devices; this page only provides a module import entry and general configuration reminders.";
|
||||
"第三方客户端适配说明" = "Third-party Client Integration Notes";
|
||||
"App 不连接远程坐标服务器,而是向 Apple 域名发起一个约定请求。第三方客户端需要在本机拦截该请求、保存 WGS-84 坐标并返回 JSON;定位模块再读取同一份数据,修改 Apple WLOC 响应。" = "The app does not connect to a remote coordinate server; instead it sends an agreed-upon request to an Apple hostname. The third-party client must intercept that request locally, store the WGS-84 coordinate, and return JSON; the location module then reads the same data and modifies the Apple WLOC response.";
|
||||
"配置接口" = "Configuration Endpoint";
|
||||
"查询:GET ?action=query\n保存:GET ?lon=<经度>&lat=<纬度>&acc=<精度>\n清除:GET ?action=clear" = "Query: GET ?action=query\nSave: GET ?lon=<lon>&lat=<lat>&acc=<accuracy>\nClear: GET ?action=clear";
|
||||
"返回格式" = "Response Format";
|
||||
"适配要求" = "Integration Requirements";
|
||||
"客户端需要支持请求脚本、持久化存储、HTTP 200 JSON 响应、Apple WLOC 响应脚本,以及 Apple 定位域名(gs-loc.apple.com、gsp-ssl.ls.apple.com、bluedot.is.autonavi.com 等)的 HTTPS 解密。保存接口和 WLOC 响应脚本必须读取同一份持久化数据。" = "The client must support request scripts, persistent storage, HTTP 200 JSON responses, an Apple WLOC response script, and HTTPS decryption for Apple location hostnames (gs-loc.apple.com, gsp-ssl.ls.apple.com, bluedot.is.autonavi.com, etc.). The save endpoint and the WLOC response script must read the same persistent data.";
|
||||
"检测到第三方代理连接异常,请检查模块、MITM 和代理连接后重新检测。" = "A third-party proxy connection problem was detected. Check the module, MITM, and proxy connection, then test again.";
|
||||
"第 1 步:导入 %@ 模块" = "Step 1: Import the %@ module";
|
||||
"复制 %@ 的模块订阅地址。" = "Copy the module subscription URL for %@.";
|
||||
"打开 Shadowrocket,进入“配置 → 模块”。" = "Open Shadowrocket and go to Config → Modules.";
|
||||
"打开 %@。" = "Open %@.";
|
||||
"进入 Shadowrocket 配置" = "Open Shadowrocket Config";
|
||||
"1 点击「模块」进入模块列表,2 可打开当前本地配置详情。" = "1) Tap “Modules” to open the module list, 2) open the current local config details.";
|
||||
"点击右上角“+”,粘贴模块订阅地址并导入,然后确认模块已启用。" = "Tap “+” in the top-right, paste the module subscription URL and import it, then confirm the module is enabled.";
|
||||
"导入 Shadowrocket 模块" = "Import Shadowrocket Module";
|
||||
"1 点击右上角加号导入模块,2 确认模块已启用。" = "1) Tap the plus in the top-right to import the module, 2) confirm the module is enabled.";
|
||||
"在 %@ 中导入刚才复制的模块订阅地址。" = "Import the module subscription URL you just copied into %@.";
|
||||
"第 2 步:完成 %@ 配置" = "Step 2: Finish configuring %@";
|
||||
"请在 %@ 中完成相应配置。" = "Complete the corresponding configuration in %@.";
|
||||
"配置时请复制下方全部解密域名(含 gsp-ssl.ls.apple.com、bluedot.is.autonavi.com)。" = "When configuring, copy all the decryption hostnames below (including gsp-ssl.ls.apple.com and bluedot.is.autonavi.com).";
|
||||
"第 2 步:配置 HTTPS 解密" = "Step 2: Configure HTTPS Decryption";
|
||||
"进入“配置 → 本地文件”,找到带黄点的配置,点击右侧 i 图标。" = "Go to Config → Local Files, find the config with the yellow dot, and tap the i icon on the right.";
|
||||
"进入“HTTPS 解密”,开启解密开关。" = "Open “HTTPS Decryption” and turn on the decryption switch.";
|
||||
"在域名列表中添加下方复制的全部解密域名。" = "Add all the decryption hostnames copied below to the hostname list.";
|
||||
"配置 HTTPS 解密" = "Configure HTTPS Decryption";
|
||||
"1 开启 HTTPS 解密,2 添加复制的全部解密域名,3 打开证书设置。" = "1) Turn on HTTPS Decryption, 2) add all copied decryption hostnames, 3) open certificate settings.";
|
||||
"按 Shadowrocket 提示生成并完成证书授权。" = "Follow Shadowrocket's prompts to generate and complete the certificate authorization.";
|
||||
"授权 Shadowrocket 证书" = "Authorize Shadowrocket Certificate";
|
||||
"1 打开 Shadowrocket 证书项并按提示安装、授权。" = "1) Open the Shadowrocket certificate item and install/authorize it as prompted.";
|
||||
"返回 HTTPS 解密页面,点击右上角勾号保存,然后开启代理。" = "Return to the HTTPS Decryption page, tap the checkmark in the top-right to save, then enable the proxy.";
|
||||
"打开 Shadowrocket 继续配置" = "Open Shadowrocket to Continue";
|
||||
"App 只能唤起 Shadowrocket,无法通过公开接口直接跳转到“模块”或“HTTPS 解密”页面。" = "The app can only launch Shadowrocket; it cannot jump directly to the “Modules” or “HTTPS Decryption” page through a public API.";
|
||||
"轻点查看大图" = "Tap to view full image";
|
||||
"证书尚未安装或信任" = "Certificate not installed or trusted";
|
||||
"Wi-Fi 代理未正确设置" = "Wi-Fi proxy is not set up correctly";
|
||||
"本地代理未能启动" = "The local proxy could not start";
|
||||
"检测仍在进行" = "The check is still running";
|
||||
"检测结果已过期" = "The check result is out of date";
|
||||
"定位改写检测失败" = "Location-rewrite check failed";
|
||||
"接口连接失败" = "Endpoint connection failed";
|
||||
|
||||
/* MARK: - SetupCoordinator */
|
||||
"本地代理初始化失败:%@" = "Local proxy initialization failed: %@";
|
||||
"✓ 定位环境正常" = "✓ Location environment OK";
|
||||
"CA 证书未安装或未信任" = "CA certificate not installed or not trusted";
|
||||
"Wi-Fi 代理未正确设置,请检查 127.0.0.1:8888" = "Wi-Fi proxy is not set up correctly. Please check 127.0.0.1:8888";
|
||||
|
||||
/* MARK: - SystemSettingsNavigator */
|
||||
"请手动打开「设置」,找到本 App 后检查定位权限。" = "Please open Settings manually, find this app, and check its location permission.";
|
||||
"请手动打开「设置 → 通用」。" = "Please open Settings → General manually.";
|
||||
"请手动打开「设置 → 无线局域网」,进入当前 Wi-Fi 的详情页。" = "Please open Settings → Wi-Fi manually and go to the details page of your current Wi-Fi.";
|
||||
"请手动打开「设置 → 隐私与安全性 → 定位服务」。" = "Please open Settings → Privacy & Security → Location Services manually.";
|
||||
|
||||
/* MARK: - ThirdPartyProxyManager */
|
||||
"第三方代理返回了无法识别的数据" = "The third-party proxy returned unrecognizable data";
|
||||
"请求未被第三方代理模块拦截,请检查模块、MITM 和代理连接" = "The request was not intercepted by the third-party proxy module. Check the module, MITM, and proxy connection.";
|
||||
"第三方代理保存的坐标与当前选点不一致" = "The coordinate saved by the third-party proxy does not match the current selection";
|
||||
"第三方代理请求失败:%@" = "Third-party proxy request failed: %@";
|
||||
"检查模块、MITM、证书和代理/VPN连接" = "Check the module, MITM, certificate, and proxy/VPN connection";
|
||||
"第三方代理拒绝保存坐标" = "The third-party proxy refused to save the coordinate";
|
||||
"第三方代理清除坐标失败" = "The third-party proxy failed to clear the coordinate";
|
||||
"已有第三方代理请求正在执行" = "A third-party proxy request is already running";
|
||||
"配置已提供,尚未验证" = "Configuration provided, not yet verified";
|
||||
"已连接,有保存坐标" = "Connected, coordinate saved";
|
||||
"已连接,无保存坐标" = "Connected, no saved coordinate";
|
||||
"连接失败(%@)" = "Connection failed (%@)";
|
||||
"当前客户端:%@" = "Current client: %@";
|
||||
"配置接口:/wloc-settings/save" = "Configuration endpoint: /wloc-settings/save";
|
||||
"请求动作:WLOC query" = "Request action: WLOC query";
|
||||
"检查范围:模块拦截、MITM、证书、代理/VPN 连接" = "Check scope: module interception, MITM, certificate, and proxy/VPN connection";
|
||||
"连接状态:%@" = "Connection status: %@";
|
||||
"检测结果:失败" = "Check result: Failed";
|
||||
"耗时:%lld ms" = "Duration: %lld ms";
|
||||
"错误类型:%@" = "Error type: %@";
|
||||
"错误详情:%@" = "Error details: %@";
|
||||
"处理建议:%@。" = "Suggested action: %@.";
|
||||
"======== 第三方代理运行检测 ========" = "======== Third-party Proxy Runtime Check ========";
|
||||
"触发来源:地图或设置中的第三方代理操作" = "Triggered by: third-party proxy action from the map or Settings";
|
||||
"请求动作:WLOC 配置接口" = "Request action: WLOC configuration endpoint";
|
||||
"处理建议:确认模块已启用,并检查 MITM、证书和代理/VPN 连接。" = "Suggested action: confirm the module is enabled, then check MITM, the certificate, and the proxy/VPN connection.";
|
||||
|
||||
/* MARK: - Exported diagnostics */
|
||||
"环境信息" = "Environment";
|
||||
"App 版本" = "App version";
|
||||
"系统版本" = "System version";
|
||||
"诊断日志" = "Diagnostic Logs";
|
||||
"不适用" = "Not applicable";
|
||||
"是" = "Yes";
|
||||
"否" = "No";
|
||||
"(无诊断数据)" = "(No diagnostic data)";
|
||||
"第三方代理测试模式:模块连接成功;已保存坐标=%@" = "Third-party proxy test mode: module connected; saved coordinate=%@";
|
||||
"第三方代理测试模式:模块连接失败;%@" = "Third-party proxy test mode: module connection failed; %@";
|
||||
"======== 代理验证测试 ========" = "======== Proxy Verification Test ========";
|
||||
"App 版本: %@" = "App version: %@";
|
||||
"系统版本: iOS %@" = "System version: iOS %@";
|
||||
"[步骤 A] 检查代理是否运行…" = "[Step A] Check whether the proxy is running…";
|
||||
" 端口: 127.0.0.1:8888" = " Port: 127.0.0.1:8888";
|
||||
" ⚠ 代理未运行,尝试启动…" = " ⚠ Proxy is not running; attempting to start it…";
|
||||
" ✗ 启动失败: %@" = " ✗ Failed to start: %@";
|
||||
" ✓ 代理启动成功" = " ✓ Proxy started";
|
||||
" ✓ 代理已在运行中" = " ✓ Proxy is already running";
|
||||
"[步骤 B] 检测证书与 WiFi 代理…" = "[Step B] Check certificate and Wi-Fi proxy…";
|
||||
" 方式: 请求 baidu.com/paopao-verify-<token>" = " Method: request baidu.com/paopao-verify-<token>";
|
||||
" 结果判定: TLS 错误=证书问题 / 响应不匹配=代理未配置 / 匹配=通过" = " Criteria: TLS error=certificate issue / response mismatch=proxy not configured / match=passed";
|
||||
" ✗ 无法生成验证 token" = " ✗ Could not generate a verification token";
|
||||
" ✓ 证书已信任,WiFi 代理已配置" = " ✓ Certificate is trusted and the Wi-Fi proxy is configured";
|
||||
" ✗ 响应不匹配: HTTP %lld, %lld bytes,WiFi 代理未配置" = " ✗ Response mismatch: HTTP %1$lld, %2$lld bytes; Wi-Fi proxy is not configured";
|
||||
" ✗ 请求失败 [%@ code=%lld]: %@" = " ✗ Request failed [%1$@ code=%2$lld]: %3$@";
|
||||
" TLS/证书校验失败,CA 证书未信任" = " TLS/certificate validation failed; the CA certificate is not trusted";
|
||||
"======== 环境检测通过 ✓ ========" = "======== Environment Check Passed ✓ ========";
|
||||
" --- 代理日志 ---" = " --- Proxy Logs ---";
|
||||
|
||||
/* MARK: - Setup examples and acknowledgements */
|
||||
"成功:{\"success\":true,\"longitude\":113.0,\"latitude\":22.0,\"accuracy\":25}\n失败:{\"success\":false,\"error\":\"错误说明\"}" = "Success: {\"success\":true,\"longitude\":113.0,\"latitude\":22.0,\"accuracy\":25}\nFailure: {\"success\":false,\"error\":\"Error details\"}";
|
||||
"定位改写脚本源自 Yu9191/wloc,现由本项目维护" = "Location-rewrite scripts originated from Yu9191/wloc and are now maintained by this project";
|
||||
|
||||
/* MARK: - Runtime log categories and messages */
|
||||
"地图" = "Map";
|
||||
"坐标转换" = "Coordinate Conversion";
|
||||
"定位" = "Location";
|
||||
"实时定位" = "Live Location";
|
||||
"提醒" = "Reminder";
|
||||
"搜索" = "Search";
|
||||
"收藏" = "Favorites";
|
||||
"缩放" = "Zoom";
|
||||
"========== App 启动 ==========" = "========== App Launch ==========";
|
||||
"尚未选择运行模式,跳过本地 CA 和代理初始化" = "No runtime mode selected; skipping local CA and proxy initialization";
|
||||
"第三方代理测试模式:跳过本地 CA、代理和环境检测" = "Third-party proxy test mode: skipping local CA, proxy, and environment checks";
|
||||
"旧坐标数据迁移失败,将在下次启动重试" = "Legacy coordinate migration failed; it will be retried on the next launch";
|
||||
"地图坐标标准初始化完成,开始后续启动流程" = "Map coordinate-system initialization completed; continuing app startup";
|
||||
"没有持久化图钉,地图创建前请求实时定位" = "No persisted pin; requesting live location before creating the map";
|
||||
"已使用实时定位准备唯一初始地图状态" = "Prepared the initial map state using live location";
|
||||
"地图创建前取得的初始实时位置(WGS-84)" = "Initial live location obtained before map creation (WGS-84)";
|
||||
"地图创建前无法取得实时定位,唯一初始位置使用深圳" = "Could not obtain live location before map creation; using Shenzhen as the initial location";
|
||||
"已找到持久化图钉,直接准备唯一初始地图状态" = "Found a persisted pin and prepared the initial map state";
|
||||
"启动门禁全部完成,现在创建 MapHomeView" = "All startup gates completed; creating MapHomeView";
|
||||
"初始化" = "Initialize";
|
||||
"启动代理 127.0.0.1:8888" = "Starting proxy at 127.0.0.1:8888";
|
||||
"启动代理: 恢复上次 WGS-84 定位" = "Starting proxy: restoring the last WGS-84 location";
|
||||
"启动成功" = "Started successfully";
|
||||
"启动失败" = "Failed to start";
|
||||
"写入坐标" = "Write coordinate";
|
||||
"跳过过期坐标写入" = "Skipped stale coordinate write";
|
||||
"跳过旧验证坐标恢复" = "Skipped restoring coordinates from an outdated verification";
|
||||
"运动状态模拟设置已更新" = "Motion simulation setting updated";
|
||||
"准备证书下载失败" = "Failed to prepare certificate download";
|
||||
"设置虚拟定位坐标" = "Set simulated-location coordinate";
|
||||
"apply失败" = "Apply failed";
|
||||
"保存收藏失败" = "Failed to save favorite";
|
||||
"本地服务初始化失败" = "Local service initialization failed";
|
||||
"代理运行模式已切换" = "Proxy runtime mode changed";
|
||||
"已迁移旧版模式初始化状态" = "Migrated legacy mode initialization state";
|
||||
"地图坐标标准检测已有请求进行中" = "A map coordinate-system check is already running";
|
||||
"地图坐标标准检测开始" = "Map coordinate-system check started";
|
||||
"地图坐标标准检测获得明确结果" = "Map coordinate-system check returned a definitive result";
|
||||
"地图坐标标准检测不可用,开始实时定位兜底" = "Map coordinate-system check unavailable; starting live-location fallback";
|
||||
"地图坐标标准检测使用兜底结果" = "Map coordinate-system check used the fallback result";
|
||||
"地图坐标标准检测被取消,保留默认国内标准" = "Map coordinate-system check was cancelled; keeping the default mainland-China system";
|
||||
"地图坐标标准已确定,允许创建地图" = "Map coordinate system determined; map creation is now allowed";
|
||||
"地图坐标标准运行期检测合并到进行中请求" = "Runtime map coordinate-system check joined the request already in progress";
|
||||
"地图坐标标准运行期检测开始" = "Runtime map coordinate-system check started";
|
||||
"地图坐标标准运行期检测结果已过期,取消写入" = "Runtime map coordinate-system result became stale; discarding it";
|
||||
"地图坐标标准运行期检测完成,标准未变化" = "Runtime map coordinate-system check completed with no change";
|
||||
"地图坐标标准运行期检测发现切换" = "Runtime map coordinate-system check detected a change";
|
||||
"地图坐标标准运行期检测失败,保留当前标准" = "Runtime map coordinate-system check failed; keeping the current system";
|
||||
"地图坐标标准运行期检测已取消" = "Runtime map coordinate-system check cancelled";
|
||||
"实时定位不覆盖固定锚点的明确检测结果" = "Live location did not override the definitive fixed-anchor result";
|
||||
"实时定位确认兜底地图坐标标准无需修正" = "Live location confirmed that the fallback map coordinate system needs no correction";
|
||||
"实时定位修正启动兜底地图坐标标准" = "Live location corrected the startup fallback map coordinate system";
|
||||
"第三方代理已保存 WGS-84 坐标" = "Third-party proxy saved the WGS-84 coordinate";
|
||||
"第三方代理坐标已清除" = "Third-party proxy coordinate cleared";
|
||||
"第三方代理请求失败" = "Third-party proxy request failed";
|
||||
"第三方代理坐标同步成功" = "Third-party proxy coordinate synchronized";
|
||||
"同步坐标到第三方客户端失败" = "Failed to synchronize the coordinate to the third-party client";
|
||||
"验证结果" = "Verification result";
|
||||
"验证失败" = "Verification failed";
|
||||
"开启前检测失败,进入对应环境引导" = "Pre-start check failed; opening the relevant setup guide";
|
||||
"清除第三方客户端坐标失败" = "Failed to clear the coordinate from the third-party client";
|
||||
"累计虚拟定位操作次数" = "Counted simulated-location operations";
|
||||
"已复制坐标" = "Coordinate copied";
|
||||
"地图坐标标准切换时未找到当前选点缓存" = "No current-selection cache was found when the map coordinate system changed";
|
||||
"地图坐标标准切换后已使用缓存坐标对回显当前选点" = "Restored the current selection from cached coordinates after the map coordinate system changed";
|
||||
"地图坐标类型已变化" = "Map coordinate type changed";
|
||||
"取消保存收藏:检测期间当前选点已变化" = "Cancelled saving favorite because the current selection changed during detection";
|
||||
"保存当前选点为收藏" = "Saving the current selection as a favorite";
|
||||
"第三方代理查询返回失败" = "Third-party proxy query returned a failure";
|
||||
"启动后第三方代理状态查询失败" = "Failed to query third-party proxy status after startup";
|
||||
"检测到 Wi-Fi 网络变化" = "Detected a Wi-Fi network change";
|
||||
"网络仍在变化,重新计算环境检测等待时间" = "Network is still changing; recalculating the environment-check delay";
|
||||
"等待 Wi-Fi 连接稳定后检测" = "Waiting for the Wi-Fi connection to stabilize before checking";
|
||||
"延时检测已被更新的网络事件取消" = "Delayed check was cancelled by a newer network event";
|
||||
"稳定等待结束后仍未连接 Wi-Fi,提示检查代理" = "Wi-Fi is still disconnected after the stability delay; prompting the user to check the proxy";
|
||||
"开始后台环境检测" = "Starting background environment check";
|
||||
"已有环境检测运行,1 秒后重试" = "An environment check is already running; retrying in 1 second";
|
||||
"后台环境检测完成" = "Background environment check completed";
|
||||
"环境检测连续被占用,本次不重复弹窗" = "Environment check remained busy; suppressing a duplicate alert";
|
||||
"用户点击实时定位" = "User requested live location";
|
||||
"MapKit 蓝点尚不可用,启动 CLLocationManager 兜底" = "MapKit user location is unavailable; starting the CLLocationManager fallback";
|
||||
"MapKit 蓝点抢先完成请求,取消 CLLocationManager 兜底" = "MapKit completed the request first; cancelling the CLLocationManager fallback";
|
||||
"虚拟定位开启后的 MapKit 蓝点标准判定" = "Determined the MapKit user-location coordinate system after simulated location started";
|
||||
"登记实时定位请求上下文" = "Registered live-location request context";
|
||||
"复用进行中的 CLLocationManager 请求并更新意图上下文" = "Reusing the active CLLocationManager request and updating its intent context";
|
||||
"CLLocationManager 兜底未返回坐标" = "CLLocationManager fallback returned no coordinate";
|
||||
"丢弃 CLLocationManager 结果:任务已取消或蓝点已抢先完成" = "Discarded CLLocationManager result because the task was cancelled or MapKit completed first";
|
||||
"实时定位坐标完成标准判断并提交到地图" = "Live-location coordinate system determined and submitted to the map";
|
||||
"MKLocalSearch 返回地点结果" = "MKLocalSearch returned a place result";
|
||||
"反向地理编码网络失败,准备重试" = "Reverse geocoding network request failed; preparing to retry";
|
||||
"反向地理编码失败" = "Reverse geocoding failed";
|
||||
"获得搜索结果" = "Received search results";
|
||||
"点击收藏点并回显到地图" = "Selected a favorite and displayed it on the map";
|
||||
"忽略重复 CLLocationManager 请求" = "Ignored duplicate CLLocationManager request";
|
||||
"CLLocationManager 请求入口" = "CLLocationManager request entry";
|
||||
"授权状态不允许定位" = "Authorization status does not permit location access";
|
||||
"创建 CLLocationManager 请求" = "Created CLLocationManager request";
|
||||
"请求前台定位授权" = "Requesting foreground location authorization";
|
||||
"定位授权状态变化" = "Location authorization status changed";
|
||||
"CLLocationManager 返回样本批次" = "CLLocationManager returned a batch of samples";
|
||||
"本批次没有可完成当前请求的样本" = "This batch contains no sample that can complete the current request";
|
||||
"接受 CLLocationManager 样本并完成请求" = "Accepted CLLocationManager sample and completed the request";
|
||||
"定位回调失败" = "Location callback failed";
|
||||
"跳过 CLLocationManager 缓存样本" = "Skipped cached CLLocationManager sample";
|
||||
"等待定位授权超时" = "Timed out waiting for location authorization";
|
||||
"单次定位超时,切换持续定位" = "One-shot location timed out; switching to continuous updates";
|
||||
"持续定位超时" = "Continuous location updates timed out";
|
||||
"开始 CLLocationManager 单次定位" = "Starting one-shot CLLocationManager request";
|
||||
"开始 CLLocationManager 持续定位兜底" = "Starting continuous CLLocationManager fallback";
|
||||
"CLLocationManager 请求完成" = "CLLocationManager request completed";
|
||||
"CLLocationManager 请求结束但没有坐标" = "CLLocationManager request ended without a coordinate";
|
||||
"使用 CLLocationManager 新鲜缓存" = "Using a fresh CLLocationManager cache entry";
|
||||
"实时定位按钮直接使用 MapKit 蓝点缓存" = "Live-location action used the MapKit user-location cache directly";
|
||||
"主页收到待处理请求所需的 MapKit 蓝点回调" = "Home screen received the MapKit user-location callback for the pending request";
|
||||
"主页收到 CLLocationManager 兜底坐标" = "Home screen received the CLLocationManager fallback coordinate";
|
||||
"原始实时定位坐标" = "Original live-location coordinate";
|
||||
"检查 CLLocationManager 样本" = "Checking CLLocationManager sample";
|
||||
"拒绝 CLLocationManager 样本:坐标或精度无效" = "Rejected CLLocationManager sample because its coordinate or accuracy is invalid";
|
||||
"MapKit 蓝点更新但 location 为空" = "MapKit user location updated but its location value is empty";
|
||||
"拒绝 MapKit 蓝点样本:坐标无效" = "Rejected MapKit user-location sample because its coordinate is invalid";
|
||||
"拒绝 MapKit 蓝点样本:水平精度无效" = "Rejected MapKit user-location sample because horizontal accuracy is invalid";
|
||||
"音频中断恢复" = "Recovered from audio interruption";
|
||||
"音频会话失败" = "Audio session failed";
|
||||
"无法创建静音音频缓冲区" = "Could not create the silent-audio buffer";
|
||||
"引擎启动失败" = "Audio engine failed to start";
|
||||
"后台保活已启动(静音音频)" = "Background keep-alive started (silent audio)";
|
||||
"后台保活已停止" = "Background keep-alive stopped";
|
||||
"保存当前图钉失败" = "Failed to save the current pin";
|
||||
"存储缩放" = "Stored zoom level";
|
||||
"旧坐标数据迁移完成" = "Legacy coordinate migration completed";
|
||||
"远程版本配置加载成功" = "Remote version configuration loaded";
|
||||
"版本配置源不可用,尝试下一地址" = "Version configuration source unavailable; trying the next URL";
|
||||
"版本配置加载失败,继续使用内置配置" = "Failed to load version configuration; continuing with built-in values";
|
||||
"版本说明源不可用,尝试下一地址" = "Release-notes source unavailable; trying the next URL";
|
||||
"版本说明加载失败,将使用最新 Release 页面" = "Failed to load release notes; using the latest Release page";
|
||||
"复用设备钥匙串中的 CA" = "Reusing the CA from the device keychain";
|
||||
"旧 CA 已迁移到设备钥匙串" = "Legacy CA migrated to the device keychain";
|
||||
"已生成并保存设备专属 CA" = "Generated and saved a device-specific CA";
|
||||
"已删除设备 CA,等待重新生成" = "Deleted the device CA; waiting to regenerate it";
|
||||
"钥匙串中的 CA 无效,准备回退" = "CA in the keychain is invalid; preparing fallback";
|
||||
"删除旧 CA 文件失败,将在下次启动重试" = "Failed to delete legacy CA files; it will be retried on the next launch";
|
||||
"无法解析 CA 证书 PEM" = "Could not parse the CA certificate PEM";
|
||||
"无法创建 SecTrust" = "Could not create SecTrust";
|
||||
"SecTrust 评估返回错误" = "SecTrust evaluation returned an error";
|
||||
"CA 证书已被系统信任" = "CA certificate is trusted by the system";
|
||||
"CA 证书未被系统信任" = "CA certificate is not trusted by the system";
|
||||
"调用 Go Core 生成 CA" = "Calling Go Core to generate the CA";
|
||||
"Go Core CA 生成成功" = "Go Core generated the CA";
|
||||
"本地证书服务已在运行" = "Local certificate server is already running";
|
||||
"调用 Go Core 启动本地证书服务" = "Calling Go Core to start the local certificate server";
|
||||
"本地证书服务启动成功" = "Local certificate server started";
|
||||
"停止本地证书服务" = "Stopping local certificate server";
|
||||
"开始第三方代理连接检测" = "Starting third-party proxy connection check";
|
||||
"第三方代理连接检测通过" = "Third-party proxy connection check passed";
|
||||
"第三方代理连接检测失败" = "Third-party proxy connection check failed";
|
||||
"关闭代理前已同步关闭虚拟定位" = "Stopped simulated location before stopping the proxy";
|
||||
"切换 APP 模式前无法清除第三方坐标" = "Could not clear the third-party coordinate before switching to App Mode";
|
||||
"设置页第三方连接检测通过" = "Third-party connection check passed in Settings";
|
||||
"设置页第三方连接检测失败" = "Third-party connection check failed in Settings";
|
||||
"地图实际显示坐标" = "Coordinate displayed on map";
|
||||
|
||||
/* MARK: - Runtime log fields and values */
|
||||
"App回到前台" = "App returned to foreground";
|
||||
"App已确认地图标准" = "App confirmed the map coordinate system";
|
||||
"CLLocationManager请求中" = "CLLocationManager request active";
|
||||
"Go proxy 启动失败" = "Go proxy failed to start";
|
||||
"MapKit地图蓝点" = "MapKit user location";
|
||||
"MapKit蓝点新样本" = "New MapKit user-location sample";
|
||||
"MapKit蓝点缓存" = "MapKit user-location cache";
|
||||
"SSID可读取" = "SSID readable";
|
||||
"WLOC写入标准" = "WLOC write coordinate system";
|
||||
"WLOC目标标准" = "WLOC target coordinate system";
|
||||
"Wi-Fi接口" = "Wi-Fi interface";
|
||||
"intent选点revision" = "Intent selection revision";
|
||||
"manager请求中" = "Manager request active";
|
||||
"上下文存在" = "Context present";
|
||||
"事件原因" = "Event reason";
|
||||
"任务已取消" = "Task cancelled";
|
||||
"任务已存在" = "Task already present";
|
||||
"使用兜底" = "Used fallback";
|
||||
"保存收藏" = "Save favorite";
|
||||
"保存数据包含" = "Saved data contains";
|
||||
"保持未启用" = "Kept disabled";
|
||||
"保留原第三方坐标" = "Kept previous third-party coordinate";
|
||||
"保留已启用状态" = "Kept enabled state";
|
||||
"保留标准" = "Retained coordinate system";
|
||||
"修正兜底标准" = "Corrected fallback coordinate system";
|
||||
"内存缓存存在" = "In-memory cache present";
|
||||
"初始坐标来源" = "Initial coordinate source";
|
||||
"初始来源" = "Initial source";
|
||||
"初始阶段" = "Initial phase";
|
||||
"判定规则" = "Decision rule";
|
||||
"原兜底来源" = "Original fallback source";
|
||||
"原因" = "Reason";
|
||||
"取值字段" = "Source field";
|
||||
"可显示不再提醒" = "Can show suppress option";
|
||||
"启动代理…" = "Starting proxy…";
|
||||
"命中林士街" = "Matched Lin Shi Street";
|
||||
"固定锚点" = "Fixed anchor";
|
||||
"固定锚点查询超过5秒" = "Fixed-anchor query exceeded 5 seconds";
|
||||
"固定锚点查询返回空结果" = "Fixed-anchor query returned no results";
|
||||
"国内标准(GCJ-02)" = "Mainland China system (GCJ-02)";
|
||||
"国内转换区域" = "Mainland conversion region";
|
||||
"国外非转换区域" = "Non-conversion region outside mainland China";
|
||||
"国际标准(WGS-84)" = "International system (WGS-84)";
|
||||
"国际标准(WGS-84)+国内标准(GCJ-02)" = "International (WGS-84) + mainland China (GCJ-02)";
|
||||
"图钉已按新类型重设" = "Pin reset for the new type";
|
||||
"地图取值字段" = "Map source field";
|
||||
"地图标准" = "Map coordinate system";
|
||||
"坐标有效" = "Coordinate valid";
|
||||
"坐标标准" = "Coordinate system";
|
||||
"垂直精度米" = "Vertical accuracy (m)";
|
||||
"基础校验通过" = "Basic validation passed";
|
||||
"处理建议" = "Suggested action";
|
||||
"失败时提示" = "Show alert on failure";
|
||||
"实时定位存在" = "Live location present";
|
||||
"实时定位服务区域" = "Live-location service region";
|
||||
"客户端模式" = "Client mode";
|
||||
"尝试" = "Attempt";
|
||||
"已取消位置更新" = "Location updates cancelled";
|
||||
"已恢复真实定位" = "Real location restored";
|
||||
"已清理" = "Cleared";
|
||||
"已重置" = "Reset";
|
||||
"开启" = "Enable";
|
||||
"异步地理编码" = "Asynchronous geocoding";
|
||||
"当前地图标准" = "Current map coordinate system";
|
||||
"当前客户端" = "Current client";
|
||||
"当前标准" = "Current coordinate system";
|
||||
"当前选点revision" = "Current selection revision";
|
||||
"恢复状态" = "Restore state";
|
||||
"批次索引" = "Batch index";
|
||||
"持久化字段" = "Persisted field";
|
||||
"授权状态" = "Authorization status";
|
||||
"授权状态rawValue" = "Authorization status raw value";
|
||||
"探测失败原因" = "Probe failure reason";
|
||||
"搜索结果" = "Search results";
|
||||
"操作" = "Operation";
|
||||
"新类型" = "New type";
|
||||
"无已保存" = "None saved";
|
||||
"无法判定" = "Undetermined";
|
||||
"无法启动本地证书服务" = "Could not start the local certificate server";
|
||||
"无法生成本地证书" = "Could not generate the local certificate";
|
||||
"日志策略" = "Logging policy";
|
||||
"旧类型" = "Previous type";
|
||||
"显示坐标字段" = "Displayed coordinate field";
|
||||
"最低版本" = "Minimum version";
|
||||
"最新版本" = "Latest version";
|
||||
"最终标准" = "Final coordinate system";
|
||||
"最终阶段" = "Final phase";
|
||||
"有坐标" = "Coordinate present";
|
||||
"有效样本数" = "Valid sample count";
|
||||
"有缓存" = "Cache present";
|
||||
"未知错误" = "Unknown error";
|
||||
"本地 CA 证书或私钥无效" = "Local CA certificate or private key is invalid";
|
||||
"无法写入设备钥匙串(%d)" = "Could not write to the device keychain (%d)";
|
||||
"来源" = "Source";
|
||||
"来源类型" = "Source type";
|
||||
"林士街" = "Lin Shi Street";
|
||||
"查看诊断日志" = "View diagnostic logs";
|
||||
"样本年龄秒" = "Sample age (s)";
|
||||
"样本数" = "Sample count";
|
||||
"样本时间" = "Sample time";
|
||||
"检查范围" = "Check scope";
|
||||
"检测前标准" = "Coordinate system before check";
|
||||
"模块拦截、MITM、代理/VPN连接" = "Module interception, MITM, and proxy/VPN connection";
|
||||
"次数" = "Count";
|
||||
"每次开启首次或判定变化" = "First sample after each start or classification change";
|
||||
"水平精度米" = "Horizontal accuracy (m)";
|
||||
"活动requestID" = "Active request ID";
|
||||
"活动阶段" = "Active phase";
|
||||
"海拔米" = "Altitude (m)";
|
||||
"满足当前请求时间窗" = "Within current request time window";
|
||||
"点击实时定位" = "Live-location action";
|
||||
"目标所在区域" = "Target region";
|
||||
"确认标准" = "Confirmed coordinate system";
|
||||
"社区征集客户端数" = "Clients requesting community contributions";
|
||||
"等待秒数" = "Wait duration (s)";
|
||||
"系统缓存存在" = "System cache present";
|
||||
"结果" = "Result";
|
||||
"结果数" = "Result count";
|
||||
"结果来源" = "Result source";
|
||||
"缓存" = "Cache";
|
||||
"缓存上限秒" = "Cache limit (s)";
|
||||
"缓存时效通过" = "Cache freshness valid";
|
||||
"缩放米" = "Zoom (m)";
|
||||
"网络可用" = "Network available";
|
||||
"耗时毫秒" = "Duration (ms)";
|
||||
"蓝点回调更接近" = "User-location callback is closer";
|
||||
"蓝点缓存" = "User-location cache";
|
||||
"蓝点缓存存在" = "User-location cache present";
|
||||
"蓝点距GCJ目标米" = "User-location distance to GCJ target (m)";
|
||||
"蓝点距WGS目标米" = "User-location distance to WGS target (m)";
|
||||
"触发原因" = "Trigger reason";
|
||||
"诊断位置" = "Diagnostic location";
|
||||
"请求动作" = "Request action";
|
||||
"超时毫秒" = "Timeout (ms)";
|
||||
"输入坐标标准" = "Input coordinate system";
|
||||
"连接状态" = "Connection status";
|
||||
"选点revision" = "Selection revision";
|
||||
"选点期间发生变化" = "Selection changed during operation";
|
||||
"错误" = "Error";
|
||||
"错误类型" = "Error type";
|
||||
"锚点" = "Anchor";
|
||||
"阶段" = "Phase";
|
||||
"首条名称" = "First result name";
|
||||
"首条名称=林士街→GCJ-02,否则→WGS-84" = "First name=Lin Shi Street → GCJ-02; otherwise → WGS-84";
|
||||
"默认国内标准" = "Default mainland-China coordinate system";
|
||||
|
||||
/* MARK: - ProxyManager */
|
||||
"证书下载地址无效" = "Invalid certificate download URL";
|
||||
"启动代理失败: %@" = "Failed to start proxy: %@";
|
||||
@@ -0,0 +1,6 @@
|
||||
/* App display name */
|
||||
"CFBundleDisplayName" = "Location Spoofer";
|
||||
|
||||
/* Location permission prompts */
|
||||
"NSLocationWhenInUseUsageDescription" = "用于显示和验证虚拟定位是否生效";
|
||||
"NSLocationAlwaysAndWhenInUseUsageDescription" = "用于显示和验证虚拟定位是否生效";
|
||||
@@ -27,6 +27,16 @@ fi
|
||||
|
||||
mkdir -p build/UnsignedIPA/Payload dist
|
||||
ditto "$APP" "build/UnsignedIPA/Payload/PaopaoLocationSpoofer.app"
|
||||
BUILD_TIMESTAMP="${BUILD_TIMESTAMP:-$(date '+%Y%m%d-%H%M%S')}"
|
||||
case "$BUILD_TIMESTAMP" in
|
||||
[0-9][0-9][0-9][0-9][0-9][0-9][0-9][0-9]-[0-9][0-9][0-9][0-9][0-9][0-9]) ;;
|
||||
*) echo "BUILD_TIMESTAMP must use yyyyMMdd-HHmmss format" >&2; exit 2 ;;
|
||||
esac
|
||||
IPA="$ROOT/dist/PaopaoLocationSpoofer-unsigned.ipa"
|
||||
TIMESTAMPED_IPA="$ROOT/dist/PaopaoLocationSpoofer-${BUILD_TIMESTAMP}-unsigned.ipa"
|
||||
rm -f "$IPA"
|
||||
cd build/UnsignedIPA
|
||||
zip -qry "$ROOT/dist/PaopaoLocationSpoofer-unsigned.ipa" Payload
|
||||
echo "Output: dist/PaopaoLocationSpoofer-unsigned.ipa"
|
||||
zip -qry "$IPA" Payload
|
||||
ditto "$IPA" "$TIMESTAMPED_IPA"
|
||||
echo "Output: $IPA"
|
||||
echo "Timestamped output: $TIMESTAMPED_IPA"
|
||||
|
||||
@@ -13,8 +13,8 @@ enum CertificateAuthorityStoreError: LocalizedError {
|
||||
|
||||
var errorDescription: String? {
|
||||
switch self {
|
||||
case .invalidAuthority: return "本地 CA 证书或私钥无效"
|
||||
case let .keychain(status): return "无法写入设备钥匙串(\(status))"
|
||||
case .invalidAuthority: return String(localized: "本地 CA 证书或私钥无效")
|
||||
case let .keychain(status): return String(localized: "无法写入设备钥匙串(\(status))")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,8 +12,8 @@ enum CoreBridgeError: LocalizedError {
|
||||
|
||||
var errorDescription: String? {
|
||||
switch self {
|
||||
case .generationFailed: return "无法生成本地证书"
|
||||
case .serverStartFailed: return "无法启动本地证书服务"
|
||||
case .generationFailed: return String(localized: "无法生成本地证书")
|
||||
case .serverStartFailed: return String(localized: "无法启动本地证书服务")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,8 +8,8 @@ enum ProxyRuntimeMode: String, CaseIterable, Codable, Identifiable {
|
||||
|
||||
var displayName: String {
|
||||
switch self {
|
||||
case .localWiFi: return "APP模式"
|
||||
case .thirdParty: return "第三方代理模式"
|
||||
case .localWiFi: return String(localized: "APP模式")
|
||||
case .thirdParty: return String(localized: "第三方代理模式")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+28
-3
@@ -38,10 +38,35 @@ struct RuntimeLogEntry: Codable, Identifiable, Equatable {
|
||||
var renderedText: String {
|
||||
let formatter = ISO8601DateFormatter()
|
||||
formatter.formatOptions = [.withInternetDateTime, .withFractionalSeconds]
|
||||
let suffix = details.isEmpty
|
||||
let localizedDetails = details.map {
|
||||
(localizedDiagnosticText($0.key), localizedDiagnosticValue($0.value))
|
||||
}
|
||||
let suffix = localizedDetails.isEmpty
|
||||
? ""
|
||||
: " " + details.sorted(by: { $0.key < $1.key }).map { "\($0.key)=\($0.value)" }.joined(separator: " ")
|
||||
return "\(formatter.string(from: timestamp)) [\(source)] [\(level.rawValue.uppercased())] [\(category)] \(message)\(suffix)"
|
||||
: " " + localizedDetails.sorted(by: { $0.0 < $1.0 }).map { "\($0.0)=\($0.1)" }.joined(separator: " ")
|
||||
return "\(formatter.string(from: timestamp)) [\(source)] [\(level.rawValue.uppercased())] [\(localizedCategory)] \(localizedMessage)\(suffix)"
|
||||
}
|
||||
|
||||
var localizedCategory: String { localizedDiagnosticText(category) }
|
||||
var localizedMessage: String { localizedDiagnosticText(message) }
|
||||
|
||||
var localizedDetailsText: String {
|
||||
details.map { (localizedDiagnosticText($0.key), localizedDiagnosticValue($0.value)) }
|
||||
.sorted(by: { $0.0 < $1.0 })
|
||||
.map { "\($0.0): \($0.1)" }
|
||||
.joined(separator: "\n")
|
||||
}
|
||||
|
||||
private func localizedDiagnosticText(_ text: String) -> String {
|
||||
String(localized: String.LocalizationValue(text))
|
||||
}
|
||||
|
||||
private func localizedDiagnosticValue(_ value: String) -> String {
|
||||
switch value {
|
||||
case "true": return String(localized: "是")
|
||||
case "false": return String(localized: "否")
|
||||
default: return localizedDiagnosticText(value)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -25,25 +25,25 @@ enum ThirdPartyProxyError: LocalizedError, Equatable {
|
||||
var errorDescription: String? {
|
||||
switch self {
|
||||
case .invalidResponse:
|
||||
return "第三方代理返回了无法识别的数据"
|
||||
return String(localized: "第三方代理返回了无法识别的数据")
|
||||
case .moduleNotIntercepted:
|
||||
return "请求未被第三方代理模块拦截,请检查模块、MITM 和代理连接"
|
||||
return String(localized: "请求未被第三方代理模块拦截,请检查模块、MITM 和代理连接")
|
||||
case .rejected(let message):
|
||||
return message
|
||||
case .coordinateMismatch:
|
||||
return "第三方代理保存的坐标与当前选点不一致"
|
||||
return String(localized: "第三方代理保存的坐标与当前选点不一致")
|
||||
case .network(let message):
|
||||
return "第三方代理请求失败:\(message)"
|
||||
return String(localized: "第三方代理请求失败:\(message)")
|
||||
}
|
||||
}
|
||||
|
||||
var recoverySuggestion: String {
|
||||
return "检查模块、MITM、证书和代理/VPN连接"
|
||||
return String(localized: "检查模块、MITM、证书和代理/VPN连接")
|
||||
}
|
||||
|
||||
static func recoverySuggestion(for error: Error) -> String {
|
||||
(error as? Self)?.recoverySuggestion
|
||||
?? "检查模块、MITM、证书和代理/VPN连接"
|
||||
?? String(localized: "检查模块、MITM、证书和代理/VPN连接")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -63,7 +63,6 @@ final class ThirdPartyProxyManager: ObservableObject {
|
||||
"bluedot.is.autonavi.com",
|
||||
"bluedot.is.autonavi.com.gds.alibabadns.com"
|
||||
]
|
||||
static let interceptionHostnamesText = interceptionHostnames.joined(separator: ", ")
|
||||
static let configurationEndpoint = URL(string: "https://gs-loc.apple.com/wloc-settings/save")!
|
||||
|
||||
@Published private(set) var connectionState: ThirdPartyProxyConnectionState = .unknown
|
||||
@@ -106,7 +105,7 @@ final class ThirdPartyProxyManager: ObservableObject {
|
||||
randomRadius: RandomRadiusStore.shared.isEnabled ? RandomRadiusStore.shared.radius : 0
|
||||
))
|
||||
guard response.success else {
|
||||
throw ThirdPartyProxyError.rejected(response.error ?? "第三方代理拒绝保存坐标")
|
||||
throw ThirdPartyProxyError.rejected(response.error ?? String(localized: "第三方代理拒绝保存坐标"))
|
||||
}
|
||||
guard let latitude = response.latitude,
|
||||
let longitude = response.longitude,
|
||||
@@ -127,7 +126,7 @@ final class ThirdPartyProxyManager: ObservableObject {
|
||||
func clear() async throws {
|
||||
let response = try await perform(action: .clear)
|
||||
guard response.success else {
|
||||
throw ThirdPartyProxyError.rejected(response.error ?? "第三方代理清除坐标失败")
|
||||
throw ThirdPartyProxyError.rejected(response.error ?? String(localized: "第三方代理清除坐标失败"))
|
||||
}
|
||||
activeSettings = nil
|
||||
connectionState = .connected(active: false)
|
||||
@@ -143,7 +142,7 @@ final class ThirdPartyProxyManager: ObservableObject {
|
||||
if response.error?.contains("无已保存") == true {
|
||||
return false
|
||||
}
|
||||
throw ThirdPartyProxyError.rejected(response.error ?? "第三方代理查询失败")
|
||||
throw ThirdPartyProxyError.rejected(response.error ?? String(localized: "第三方代理查询失败"))
|
||||
}
|
||||
|
||||
private enum Action {
|
||||
@@ -154,7 +153,7 @@ final class ThirdPartyProxyManager: ObservableObject {
|
||||
|
||||
private func perform(action: Action) async throws -> ThirdPartyProxySettingsResponse {
|
||||
guard !isRequesting else {
|
||||
throw ThirdPartyProxyError.rejected("已有第三方代理请求正在执行")
|
||||
throw ThirdPartyProxyError.rejected(String(localized: "已有第三方代理请求正在执行"))
|
||||
}
|
||||
isRequesting = true
|
||||
defer { isRequesting = false }
|
||||
@@ -170,7 +169,10 @@ final class ThirdPartyProxyManager: ObservableObject {
|
||||
URLQueryItem(name: "lon", value: String(format: "%.8f", locale: Locale(identifier: "en_US_POSIX"), longitude)),
|
||||
URLQueryItem(name: "lat", value: String(format: "%.8f", locale: Locale(identifier: "en_US_POSIX"), latitude)),
|
||||
URLQueryItem(name: "acc", value: String(accuracy)),
|
||||
URLQueryItem(name: "randomRadius", value: String(randomRadius))
|
||||
URLQueryItem(
|
||||
name: "randomRadius",
|
||||
value: String(format: "%g", locale: Locale(identifier: "en_US_POSIX"), randomRadius)
|
||||
)
|
||||
]
|
||||
}
|
||||
guard let url = components.url else { throw ThirdPartyProxyError.invalidResponse }
|
||||
@@ -203,6 +205,9 @@ final class ThirdPartyProxyManager: ObservableObject {
|
||||
}
|
||||
|
||||
enum ThirdPartyProxyClient: String, CaseIterable, Identifiable {
|
||||
/// Bump when a hosted module or script changes to invalidate proxy-client caches.
|
||||
static let moduleSubscriptionVersion = "1.0.8"
|
||||
|
||||
case shadowrocket
|
||||
case surge
|
||||
case quantumultX
|
||||
@@ -224,7 +229,7 @@ enum ThirdPartyProxyClient: String, CaseIterable, Identifiable {
|
||||
}
|
||||
|
||||
var verificationText: String? {
|
||||
self == .shadowrocket ? nil : "配置已提供,尚未验证"
|
||||
self == .shadowrocket ? nil : String(localized: "配置已提供,尚未验证")
|
||||
}
|
||||
|
||||
var moduleFileName: String {
|
||||
@@ -239,21 +244,14 @@ enum ThirdPartyProxyClient: String, CaseIterable, Identifiable {
|
||||
|
||||
@MainActor
|
||||
var subscriptionURL: URL {
|
||||
// Third-party modules are served directly from the upstream Yu9191/wloc
|
||||
// repository (mirror via gh-proxy when enabled). We no longer maintain
|
||||
// project-owned module copies, so the URL tracks upstream releases.
|
||||
let fileName: String
|
||||
switch self {
|
||||
case .surge, .egern: fileName = "wloc.sgmodule"
|
||||
case .quantumultX: fileName = "wloc.conf"
|
||||
case .loon: fileName = "wloc.lpx"
|
||||
case .stash: fileName = "wloc.stoverride"
|
||||
case .shadowrocket: fileName = "wloc.module"
|
||||
}
|
||||
let base = ThirdPartyModuleSourceStore.shared.useMirror
|
||||
? "https://gh-proxy.org/https://raw.githubusercontent.com/Yu9191/wloc/refs/heads/main/modules/\(fileName)"
|
||||
: "https://raw.githubusercontent.com/Yu9191/wloc/refs/heads/main/modules/\(fileName)"
|
||||
return URL(string: base)!
|
||||
let directory = ThirdPartyModuleSourceStore.shared.useMirror
|
||||
? "Resources/ThirdPartyProxyModules"
|
||||
: "ThirdParty/WlocScripts/modules/direct"
|
||||
let prefix = ThirdPartyModuleSourceStore.shared.useMirror
|
||||
? "https://gh-proxy.org/https://raw.githubusercontent.com/xweiba/location-spoofer/main/"
|
||||
: "https://raw.githubusercontent.com/xweiba/location-spoofer/main/"
|
||||
let base = "\(prefix)\(directory)/\(moduleFileName)"
|
||||
return URL(string: "\(base)?v=\(Self.moduleSubscriptionVersion)")!
|
||||
}
|
||||
|
||||
var launchURL: URL? {
|
||||
|
||||
@@ -26,4 +26,18 @@ enum VerificationResult: Equatable, Identifiable {
|
||||
|
||||
var isSuccess: Bool { self == .success }
|
||||
|
||||
/// Localized, user-facing title. `id` stays stable for routing/equality.
|
||||
var localizedTitle: String {
|
||||
switch self {
|
||||
case .success: return String(localized: "成功")
|
||||
case .proxyNotRunning: return String(localized: "代理未运行")
|
||||
case .verificationInProgress: return String(localized: "已有验证正在进行")
|
||||
case .verificationSuperseded: return String(localized: "验证已被新位置取代")
|
||||
case .certNotTrusted: return String(localized: "证书未信任")
|
||||
case .wifiProxyNotConfigured: return String(localized: "WiFi代理未配置")
|
||||
case .coordinateWriteFailed: return String(localized: "坐标写入失败")
|
||||
case .patchFailed: return String(localized: "改写验证失败")
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -73,9 +73,13 @@ final class RealtimeLocationManagerTests: XCTestCase {
|
||||
func testOneShotTimeoutTransitionsToContinuousFallback() async {
|
||||
let driver = FakeRealtimeLocationDriver()
|
||||
let manager = RealtimeLocationManager(driver: driver, oneShotTimeoutNanoseconds: 5_000_000, fallbackTimeoutNanoseconds: 1_000_000_000)
|
||||
let fallbackStarted = expectation(description: "Continuous location fallback started")
|
||||
driver.onStartUpdating = {
|
||||
fallbackStarted.fulfill()
|
||||
}
|
||||
|
||||
let request = Task { await manager.requestLocation() }
|
||||
try? await Task.sleep(nanoseconds: 20_000_000)
|
||||
await fulfillment(of: [fallbackStarted], timeout: 1)
|
||||
XCTAssertEqual(driver.startUpdatingCallCount, 1)
|
||||
|
||||
driver.emit(CLLocation(latitude: 39.90, longitude: 116.40))
|
||||
@@ -148,6 +152,7 @@ private final class FakeRealtimeLocationDriver: RealtimeLocationDriving {
|
||||
var authorizationStatus: CLAuthorizationStatus = .authorizedWhenInUse
|
||||
weak var delegate: CLLocationManagerDelegate?
|
||||
var onRequestLocation: (() -> Void)?
|
||||
var onStartUpdating: (() -> Void)?
|
||||
private(set) var requestAuthorizationCallCount = 0
|
||||
private(set) var requestLocationCallCount = 0
|
||||
private(set) var startUpdatingCallCount = 0
|
||||
@@ -164,6 +169,7 @@ private final class FakeRealtimeLocationDriver: RealtimeLocationDriving {
|
||||
|
||||
func startUpdatingLocation() {
|
||||
startUpdatingCallCount += 1
|
||||
onStartUpdating?()
|
||||
}
|
||||
|
||||
func stopUpdatingLocation() {
|
||||
|
||||
@@ -125,16 +125,25 @@ final class ThirdPartyProxyManagerTests: XCTestCase {
|
||||
}
|
||||
}
|
||||
|
||||
func testClientLinksUseUpstreamModulesAndVerificationLabels() {
|
||||
func testClientLinksUseProjectOwnedModulesAndVerificationLabels() {
|
||||
XCTAssertEqual(
|
||||
ThirdPartyProxyManager.interceptionHostnamesText,
|
||||
"gs-loc.apple.com, gs-loc-cn.apple.com, gsp-ssl.ls.apple.com, bluedot.is.autonavi.com, bluedot.is.autonavi.com.gds.alibabadns.com"
|
||||
ThirdPartyProxyManager.interceptionHostnames,
|
||||
[
|
||||
"gs-loc.apple.com",
|
||||
"gs-loc-cn.apple.com",
|
||||
"gsp-ssl.ls.apple.com",
|
||||
"bluedot.is.autonavi.com",
|
||||
"bluedot.is.autonavi.com.gds.alibabadns.com"
|
||||
]
|
||||
)
|
||||
XCTAssertNil(ThirdPartyProxyClient.shadowrocket.verificationText)
|
||||
XCTAssertTrue(ThirdPartyProxyClient.surge.verificationText?.contains("尚未验证") == true)
|
||||
XCTAssertEqual(
|
||||
ThirdPartyProxyClient.surge.verificationText,
|
||||
String(localized: "配置已提供,尚未验证")
|
||||
)
|
||||
XCTAssertEqual(ThirdPartyProxyClient.egern.subscriptionURL, ThirdPartyProxyClient.surge.subscriptionURL)
|
||||
XCTAssertTrue(ThirdPartyProxyClient.stash.subscriptionURL.absoluteString.hasPrefix(
|
||||
"https://gh-proxy.org/https://raw.githubusercontent.com/Yu9191/wloc/refs/heads/main/modules/"
|
||||
"https://gh-proxy.org/https://raw.githubusercontent.com/xweiba/location-spoofer/main/Resources/ThirdPartyProxyModules/"
|
||||
))
|
||||
let stashComponents = URLComponents(
|
||||
url: ThirdPartyProxyClient.stash.subscriptionURL,
|
||||
@@ -144,10 +153,10 @@ final class ThirdPartyProxyManagerTests: XCTestCase {
|
||||
url: ThirdPartyProxyClient.shadowrocket.subscriptionURL,
|
||||
resolvingAgainstBaseURL: false
|
||||
)
|
||||
XCTAssertEqual(stashComponents?.path, "/https://raw.githubusercontent.com/Yu9191/wloc/refs/heads/main/modules/wloc.stoverride")
|
||||
XCTAssertEqual(shadowrocketComponents?.path, "/https://raw.githubusercontent.com/Yu9191/wloc/refs/heads/main/modules/wloc.module")
|
||||
XCTAssertTrue(stashComponents?.queryItems?.isEmpty ?? true)
|
||||
XCTAssertTrue(shadowrocketComponents?.queryItems?.isEmpty ?? true)
|
||||
XCTAssertEqual(stashComponents?.path, "/https://raw.githubusercontent.com/xweiba/location-spoofer/main/Resources/ThirdPartyProxyModules/wloc.stoverride")
|
||||
XCTAssertEqual(shadowrocketComponents?.path, "/https://raw.githubusercontent.com/xweiba/location-spoofer/main/Resources/ThirdPartyProxyModules/wloc.module")
|
||||
XCTAssertEqual(stashComponents?.queryItems?.first?.value, ThirdPartyProxyClient.moduleSubscriptionVersion)
|
||||
XCTAssertEqual(shadowrocketComponents?.queryItems?.first?.value, ThirdPartyProxyClient.moduleSubscriptionVersion)
|
||||
XCTAssertEqual(ThirdPartyProxyClient.shadowrocket.launchURL?.scheme, "shadowrocket")
|
||||
XCTAssertEqual(ThirdPartyProxyClient.surge.launchURL?.scheme, "surge")
|
||||
XCTAssertEqual(ThirdPartyProxyClient.quantumultX.launchURL?.scheme, "quantumult-x")
|
||||
|
||||
@@ -8,6 +8,10 @@ test -x "$BUILD_SCRIPT" || fail "build.sh must be executable"
|
||||
grep -qF "build-unsigned-ipa.sh" "$BUILD_SCRIPT" || fail "build.sh must call build-unsigned-ipa.sh"
|
||||
|
||||
test -f "$ROOT/Scripts/build-unsigned-ipa.sh" || fail "build-unsigned-ipa.sh must exist"
|
||||
grep -qF 'BUILD_TIMESTAMP="${BUILD_TIMESTAMP:-$(date' "$ROOT/Scripts/build-unsigned-ipa.sh" \
|
||||
|| fail "build output must include its generation timestamp"
|
||||
grep -qF 'PaopaoLocationSpoofer-${BUILD_TIMESTAMP}-unsigned.ipa' "$ROOT/Scripts/build-unsigned-ipa.sh" \
|
||||
|| fail "timestamped IPA filename is missing"
|
||||
|
||||
# Should NOT contain Tunnel references
|
||||
! grep -qF "Tunnel" "$ROOT/Scripts/build-unsigned-ipa.sh" || fail "build-unsigned-ipa.sh must not reference Tunnel"
|
||||
|
||||
Executable
+58
@@ -0,0 +1,58 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
ROOT="$(cd "$(dirname "$0")/.." && pwd)"
|
||||
STRINGS="$ROOT/Resources/en.lproj/Localizable.strings"
|
||||
|
||||
fail() {
|
||||
echo "FAIL: $1" >&2
|
||||
exit 1
|
||||
}
|
||||
|
||||
plutil -lint "$STRINGS" >/dev/null || fail "English localization file is invalid"
|
||||
|
||||
ruby - "$STRINGS" <<'RUBY' || exit 1
|
||||
path = ARGV.fetch(0)
|
||||
entries = File.readlines(path).map do |line|
|
||||
match = line.match(/^"((?:\\.|[^"])*)"\s*=\s*"((?:\\.|[^"])*)";/)
|
||||
[match[1], match[2]] if match
|
||||
end.compact
|
||||
|
||||
duplicates = entries.group_by(&:first).select { |_key, values| values.length > 1 }.keys
|
||||
abort "FAIL: duplicate English localization keys: #{duplicates.join(', ')}" unless duplicates.empty?
|
||||
|
||||
placeholder = /%(?:\d+\$)?(?:@|lld|ld|d|f)/
|
||||
entries.each do |source, target|
|
||||
source_count = source.scan(placeholder).length
|
||||
target_count = target.scan(placeholder).length
|
||||
abort "FAIL: placeholder mismatch for #{source.inspect}" unless source_count == target_count
|
||||
end
|
||||
RUBY
|
||||
|
||||
for key in \
|
||||
'环境信息' \
|
||||
'诊断日志' \
|
||||
'======== 代理验证测试 ========' \
|
||||
'======== 第三方代理连接检测 ========' \
|
||||
'======== 第三方代理运行检测 ========' \
|
||||
'第三方代理测试模式:模块连接成功;已保存坐标=%@' \
|
||||
'成功:{\"success\":true,\"longitude\":113.0,\"latitude\":22.0,\"accuracy\":25}\n失败:{\"success\":false,\"error\":\"错误说明\"}'; do
|
||||
grep -Fq "\"$key\" = " "$STRINGS" || fail "missing critical English localization: $key"
|
||||
done
|
||||
|
||||
grep -q 'localizedCategory' "$ROOT/Shared/RuntimeLog.swift" \
|
||||
|| fail "runtime log categories must be localized when rendered"
|
||||
grep -q 'localizedDetailsText' "$ROOT/App/DiagnosticsView.swift" \
|
||||
|| fail "runtime log details must be localized in the diagnostics UI"
|
||||
grep -q 'String(localized: "诊断日志")' "$ROOT/App/BugReportView.swift" \
|
||||
|| fail "generated bug reports must localize their diagnostic section"
|
||||
grep -q 'log(" " + e.localizedMessage)' "$ROOT/App/SetupCoordinator.swift" \
|
||||
|| fail "bug-report verification logs must render stored messages in the active language"
|
||||
|
||||
if grep -R -n 'raw.githubusercontent.com/Yu9191/wloc' \
|
||||
"$ROOT/App" "$ROOT/Shared" "$ROOT/Resources/ThirdPartyProxyModules" \
|
||||
"$ROOT/ThirdParty/WlocScripts/modules"; then
|
||||
fail "deleted Yu9191/wloc repository must not remain a runtime dependency"
|
||||
fi
|
||||
|
||||
echo "PASS: localization and diagnostic export contract"
|
||||
@@ -10,8 +10,8 @@ CONTENT="$ROOT/App/ContentView.swift"
|
||||
SETUP="$ROOT/App/FirstSetupView.swift"
|
||||
SETTINGS="$ROOT/App/SettingsView.swift"
|
||||
|
||||
grep -q 'return "APP模式"' "$MODE" || fail "APP mode display name is missing"
|
||||
grep -q 'return "第三方代理模式"' "$MODE" || fail "third-party mode display name is missing"
|
||||
grep -q 'String(localized: "APP模式")' "$MODE" || fail "localized APP mode display name is missing"
|
||||
grep -q 'String(localized: "第三方代理模式")' "$MODE" || fail "localized third-party mode display name is missing"
|
||||
grep -q 'hasSelectedMode' "$MODE" || fail "first-launch mode selection must be persisted"
|
||||
grep -q 'guard runtimeMode.hasSelectedMode else' "$CONTENT" || fail "mode selection must gate startup"
|
||||
grep -q 'phase = .setup' "$CONTENT" || fail "first launch must enter setup before map construction"
|
||||
@@ -37,21 +37,39 @@ grep -Fq '保存:GET ?lon=<经度>&lat=<纬度>&acc=<精度>' "$SETUP" \
|
||||
grep -Fq '清除:GET ?action=clear' "$SETUP" \
|
||||
|| fail "client integration guidance must document the clear action"
|
||||
|
||||
grep -q 'Yu9191/wloc/refs/heads/main/modules' "$MANAGER" \
|
||||
|| fail "third-party subscription must point at upstream Yu9191 modules"
|
||||
grep -q 'raw.githubusercontent.com/xweiba/location-spoofer/main' "$MANAGER" \
|
||||
|| fail "third-party subscription must point at project-owned modules"
|
||||
! grep -q 'raw.githubusercontent.com/Yu9191/wloc' "$MANAGER" \
|
||||
|| fail "third-party subscription must not depend on the removed upstream repository"
|
||||
MODULES="$ROOT/Resources/ThirdPartyProxyModules"
|
||||
SCRIPTS="$ROOT/ThirdParty/WlocScripts"
|
||||
for file in wloc.module wloc.sgmodule wloc.conf wloc.lpx wloc.stoverride; do
|
||||
test -s "$MODULES/$file" || fail "missing project-owned mirrored module: $file"
|
||||
test -s "$SCRIPTS/modules/direct/$file" || fail "missing project-owned direct module: $file"
|
||||
! grep 'ThirdParty/WlocScripts/dist/v1/wloc' "$MODULES/$file" "$SCRIPTS/modules/direct/$file" \
|
||||
| grep -Fvq '?v=1.0.8' \
|
||||
|| fail "hosted script URLs must carry the current cache version: $file"
|
||||
done
|
||||
test -s "$SCRIPTS/dist/v1/wloc.js" || fail "missing hosted WLOC response script"
|
||||
test -s "$SCRIPTS/dist/v1/wloc-settings.js" || fail "missing hosted WLOC settings script"
|
||||
! grep -R -q 'raw.githubusercontent.com/Yu9191/wloc' "$MODULES" "$SCRIPTS/modules/direct" \
|
||||
|| fail "hosted modules must not reference the removed upstream repository"
|
||||
grep -q 'wloc.sgmodule' "$MANAGER" || fail "Surge/Egern module mapping is missing"
|
||||
grep -q 'wloc.stoverride' "$MANAGER" || fail "Stash must use .stoverride directly"
|
||||
grep -q 'shadowrocket://' "$MANAGER" || fail "Shadowrocket launch URL is missing"
|
||||
for scheme in surge quantumult-x loon stash egern; do
|
||||
grep -q "${scheme}://" "$MANAGER" || fail "$scheme launch URL is missing"
|
||||
done
|
||||
grep -q '复制解密域名' "$SETUP" || fail "all clients must expose the MITM hostname copy action"
|
||||
grep -Fq 'Text("复制 \(hostname)")' "$SETUP" \
|
||||
|| fail "all clients must expose a copy action for each MITM hostname"
|
||||
grep -q '配置时请复制下方全部解密域名' "$SETUP" \
|
||||
|| fail "setup guidance must direct users to copy all Apple location hostnames"
|
||||
grep -q 'ThirdPartyProxyManager.interceptionHostnamesText' "$SETUP" \
|
||||
|| fail "setup hostname copy actions must use the shared interception hostname value"
|
||||
grep -q 'ThirdPartyProxyManager.interceptionHostnamesText' "$SETTINGS" \
|
||||
|| fail "Settings must expose the shared interception hostname copy action"
|
||||
grep -q 'ForEach(ThirdPartyProxyManager.interceptionHostnames' "$SETUP" \
|
||||
|| fail "setup must render every interception hostname"
|
||||
grep -q 'ForEach(ThirdPartyProxyManager.interceptionHostnames' "$SETTINGS" \
|
||||
|| fail "Settings must render every interception hostname"
|
||||
test "$(grep -h 'UIPasteboard.general.string = hostname' "$SETUP" "$SETTINGS" | wc -l | tr -d ' ')" -eq 2 \
|
||||
|| fail "both hostname lists must copy one selected hostname"
|
||||
grep -q '配置 → 模块' "$SETUP" || fail "Shadowrocket module import guidance is missing"
|
||||
grep -q 'HTTPS 解密' "$SETUP" || fail "Shadowrocket HTTPS decryption guidance is missing"
|
||||
! grep -q '当前可测试' "$SETUP" || fail "Shadowrocket must not show the obsolete current-test label"
|
||||
@@ -88,7 +106,7 @@ grep -q 'setup.requestThirdPartySetup(message: error.localizedDescription)' "$RO
|
||||
|| fail "third-party coordinate sync failures must open the import guide"
|
||||
grep -q '检测到第三方代理连接异常,请检查模块、MITM 和代理连接后重新检测' "$SETUP" \
|
||||
|| fail "runtime repair must explain why the import guide opened"
|
||||
test "$(grep -c 'title: \"接口连接失败\"' "$SETUP")" -eq 1 \
|
||||
test "$(grep -c 'title: String(localized: \"接口连接失败\")' "$SETUP")" -eq 1 \
|
||||
|| fail "third-party failure details must render in one shared result area"
|
||||
grep -Fq '当前客户端:\(client.name)' "$SETUP" \
|
||||
|| fail "third-party failure logs must identify the selected client"
|
||||
@@ -124,7 +142,7 @@ test -s "$ROOT/docs/onboarding-screenshots/shadowrocket/shadowrocket-module-impo
|
||||
grep -q 'presentSuccessfulOperationTip(.activation)' "$ROOT/App/MapHomeView.swift" || fail "third-party save must present the activation tip"
|
||||
grep -q 'presentSuccessfulOperationTip(.deactivation)' "$ROOT/App/MapHomeView.swift" || fail "third-party clear must present the deactivation tip"
|
||||
grep -q 'if spoofState == .active' "$ROOT/App/MapHomeView.swift" || fail "manual help must follow the shared spoof state"
|
||||
grep -q 'MARKETING_VERSION: "1.0.6"' "$ROOT/project.yml" || fail "marketing version must be 1.0.6"
|
||||
grep -q 'CURRENT_PROJECT_VERSION: "7"' "$ROOT/project.yml" || fail "build version must be 7"
|
||||
grep -q 'MARKETING_VERSION: "1.0.7"' "$ROOT/project.yml" || fail "marketing version must be 1.0.7"
|
||||
grep -q 'CURRENT_PROJECT_VERSION: "8"' "$ROOT/project.yml" || fail "build version must be 8"
|
||||
|
||||
echo "PASS: third-party proxy mode contract"
|
||||
|
||||
@@ -25,7 +25,7 @@ import sys
|
||||
with open(sys.argv[1], encoding="utf-8") as handle:
|
||||
config = json.load(handle)
|
||||
|
||||
assert config["latestVersion"] == "1.0.5"
|
||||
assert config["latestVersion"] == "1.0.7"
|
||||
assert config["minimumSupportedVersion"] == "1.0.0"
|
||||
assert "shadowrocket" not in config["communityPromptClients"]
|
||||
assert set(config["communityPromptClients"]) == {
|
||||
@@ -121,7 +121,7 @@ grep -Fq 'SafariView(url: destination.url)' "$BUG_REPORT" \
|
||||
|| fail "the App bug report must not be handed to an external GitHub client"
|
||||
grep -Fq 'App 生成的诊断报告' "$BUG_REPORT" \
|
||||
|| fail "the App must tell users where to paste the generated report"
|
||||
grep -Fq '第三方客户端:' "$BUG_REPORT" \
|
||||
grep -Fq 'String(localized: "第三方客户端")' "$BUG_REPORT" \
|
||||
|| fail "the generated report must identify the selected third-party client"
|
||||
grep -Fq 'Label("报告 Bug"' "$SETTINGS" \
|
||||
|| fail "Settings must identify the support action as a bug report"
|
||||
|
||||
Vendored
+661
@@ -0,0 +1,661 @@
|
||||
GNU AFFERO GENERAL PUBLIC LICENSE
|
||||
Version 3, 19 November 2007
|
||||
|
||||
Copyright (C) 2007 Free Software Foundation, Inc. <https://fsf.org/>
|
||||
Everyone is permitted to copy and distribute verbatim copies
|
||||
of this license document, but changing it is not allowed.
|
||||
|
||||
Preamble
|
||||
|
||||
The GNU Affero General Public License is a free, copyleft license for
|
||||
software and other kinds of works, specifically designed to ensure
|
||||
cooperation with the community in the case of network server software.
|
||||
|
||||
The licenses for most software and other practical works are designed
|
||||
to take away your freedom to share and change the works. By contrast,
|
||||
our General Public Licenses are intended to guarantee your freedom to
|
||||
share and change all versions of a program--to make sure it remains free
|
||||
software for all its users.
|
||||
|
||||
When we speak of free software, we are referring to freedom, not
|
||||
price. Our General Public Licenses are designed to make sure that you
|
||||
have the freedom to distribute copies of free software (and charge for
|
||||
them if you wish), that you receive source code or can get it if you
|
||||
want it, that you can change the software or use pieces of it in new
|
||||
free programs, and that you know you can do these things.
|
||||
|
||||
Developers that use our General Public Licenses protect your rights
|
||||
with two steps: (1) assert copyright on the software, and (2) offer
|
||||
you this License which gives you legal permission to copy, distribute
|
||||
and/or modify the software.
|
||||
|
||||
A secondary benefit of defending all users' freedom is that
|
||||
improvements made in alternate versions of the program, if they
|
||||
receive widespread use, become available for other developers to
|
||||
incorporate. Many developers of free software are heartened and
|
||||
encouraged by the resulting cooperation. However, in the case of
|
||||
software used on network servers, this result may fail to come about.
|
||||
The GNU General Public License permits making a modified version and
|
||||
letting the public access it on a server without ever releasing its
|
||||
source code to the public.
|
||||
|
||||
The GNU Affero General Public License is designed specifically to
|
||||
ensure that, in such cases, the modified source code becomes available
|
||||
to the community. It requires the operator of a network server to
|
||||
provide the source code of the modified version running there to the
|
||||
users of that server. Therefore, public use of a modified version, on
|
||||
a publicly accessible server, gives the public access to the source
|
||||
code of the modified version.
|
||||
|
||||
An older license, called the Affero General Public License and
|
||||
published by Affero, was designed to accomplish similar goals. This is
|
||||
a different license, not a version of the Affero GPL, but Affero has
|
||||
released a new version of the Affero GPL which permits relicensing under
|
||||
this license.
|
||||
|
||||
The precise terms and conditions for copying, distribution and
|
||||
modification follow.
|
||||
|
||||
TERMS AND CONDITIONS
|
||||
|
||||
0. Definitions.
|
||||
|
||||
"This License" refers to version 3 of the GNU Affero General Public License.
|
||||
|
||||
"Copyright" also means copyright-like laws that apply to other kinds of
|
||||
works, such as semiconductor masks.
|
||||
|
||||
"The Program" refers to any copyrightable work licensed under this
|
||||
License. Each licensee is addressed as "you". "Licensees" and
|
||||
"recipients" may be individuals or organizations.
|
||||
|
||||
To "modify" a work means to copy from or adapt all or part of the work
|
||||
in a fashion requiring copyright permission, other than the making of an
|
||||
exact copy. The resulting work is called a "modified version" of the
|
||||
earlier work or a work "based on" the earlier work.
|
||||
|
||||
A "covered work" means either the unmodified Program or a work based
|
||||
on the Program.
|
||||
|
||||
To "propagate" a work means to do anything with it that, without
|
||||
permission, would make you directly or secondarily liable for
|
||||
infringement under applicable copyright law, except executing it on a
|
||||
computer or modifying a private copy. Propagation includes copying,
|
||||
distribution (with or without modification), making available to the
|
||||
public, and in some countries other activities as well.
|
||||
|
||||
To "convey" a work means any kind of propagation that enables other
|
||||
parties to make or receive copies. Mere interaction with a user through
|
||||
a computer network, with no transfer of a copy, is not conveying.
|
||||
|
||||
An interactive user interface displays "Appropriate Legal Notices"
|
||||
to the extent that it includes a convenient and prominently visible
|
||||
feature that (1) displays an appropriate copyright notice, and (2)
|
||||
tells the user that there is no warranty for the work (except to the
|
||||
extent that warranties are provided), that licensees may convey the
|
||||
work under this License, and how to view a copy of this License. If
|
||||
the interface presents a list of user commands or options, such as a
|
||||
menu, a prominent item in the list meets this criterion.
|
||||
|
||||
1. Source Code.
|
||||
|
||||
The "source code" for a work means the preferred form of the work
|
||||
for making modifications to it. "Object code" means any non-source
|
||||
form of a work.
|
||||
|
||||
A "Standard Interface" means an interface that either is an official
|
||||
standard defined by a recognized standards body, or, in the case of
|
||||
interfaces specified for a particular programming language, one that
|
||||
is widely used among developers working in that language.
|
||||
|
||||
The "System Libraries" of an executable work include anything, other
|
||||
than the work as a whole, that (a) is included in the normal form of
|
||||
packaging a Major Component, but which is not part of that Major
|
||||
Component, and (b) serves only to enable use of the work with that
|
||||
Major Component, or to implement a Standard Interface for which an
|
||||
implementation is available to the public in source code form. A
|
||||
"Major Component", in this context, means a major essential component
|
||||
(kernel, window system, and so on) of the specific operating system
|
||||
(if any) on which the executable work runs, or a compiler used to
|
||||
produce the work, or an object code interpreter used to run it.
|
||||
|
||||
The "Corresponding Source" for a work in object code form means all
|
||||
the source code needed to generate, install, and (for an executable
|
||||
work) run the object code and to modify the work, including scripts to
|
||||
control those activities. However, it does not include the work's
|
||||
System Libraries, or general-purpose tools or generally available free
|
||||
programs which are used unmodified in performing those activities but
|
||||
which are not part of the work. For example, Corresponding Source
|
||||
includes interface definition files associated with source files for
|
||||
the work, and the source code for shared libraries and dynamically
|
||||
linked subprograms that the work is specifically designed to require,
|
||||
such as by intimate data communication or control flow between those
|
||||
subprograms and other parts of the work.
|
||||
|
||||
The Corresponding Source need not include anything that users
|
||||
can regenerate automatically from other parts of the Corresponding
|
||||
Source.
|
||||
|
||||
The Corresponding Source for a work in source code form is that
|
||||
same work.
|
||||
|
||||
2. Basic Permissions.
|
||||
|
||||
All rights granted under this License are granted for the term of
|
||||
copyright on the Program, and are irrevocable provided the stated
|
||||
conditions are met. This License explicitly affirms your unlimited
|
||||
permission to run the unmodified Program. The output from running a
|
||||
covered work is covered by this License only if the output, given its
|
||||
content, constitutes a covered work. This License acknowledges your
|
||||
rights of fair use or other equivalent, as provided by copyright law.
|
||||
|
||||
You may make, run and propagate covered works that you do not
|
||||
convey, without conditions so long as your license otherwise remains
|
||||
in force. You may convey covered works to others for the sole purpose
|
||||
of having them make modifications exclusively for you, or provide you
|
||||
with facilities for running those works, provided that you comply with
|
||||
the terms of this License in conveying all material for which you do
|
||||
not control copyright. Those thus making or running the covered works
|
||||
for you must do so exclusively on your behalf, under your direction
|
||||
and control, on terms that prohibit them from making any copies of
|
||||
your copyrighted material outside their relationship with you.
|
||||
|
||||
Conveying under any other circumstances is permitted solely under
|
||||
the conditions stated below. Sublicensing is not allowed; section 10
|
||||
makes it unnecessary.
|
||||
|
||||
3. Protecting Users' Legal Rights From Anti-Circumvention Law.
|
||||
|
||||
No covered work shall be deemed part of an effective technological
|
||||
measure under any applicable law fulfilling obligations under article
|
||||
11 of the WIPO copyright treaty adopted on 20 December 1996, or
|
||||
similar laws prohibiting or restricting circumvention of such
|
||||
measures.
|
||||
|
||||
When you convey a covered work, you waive any legal power to forbid
|
||||
circumvention of technological measures to the extent such circumvention
|
||||
is effected by exercising rights under this License with respect to
|
||||
the covered work, and you disclaim any intention to limit operation or
|
||||
modification of the work as a means of enforcing, against the work's
|
||||
users, your or third parties' legal rights to forbid circumvention of
|
||||
technological measures.
|
||||
|
||||
4. Conveying Verbatim Copies.
|
||||
|
||||
You may convey verbatim copies of the Program's source code as you
|
||||
receive it, in any medium, provided that you conspicuously and
|
||||
appropriately publish on each copy an appropriate copyright notice;
|
||||
keep intact all notices stating that this License and any
|
||||
non-permissive terms added in accord with section 7 apply to the code;
|
||||
keep intact all notices of the absence of any warranty; and give all
|
||||
recipients a copy of this License along with the Program.
|
||||
|
||||
You may charge any price or no price for each copy that you convey,
|
||||
and you may offer support or warranty protection for a fee.
|
||||
|
||||
5. Conveying Modified Source Versions.
|
||||
|
||||
You may convey a work based on the Program, or the modifications to
|
||||
produce it from the Program, in the form of source code under the
|
||||
terms of section 4, provided that you also meet all of these conditions:
|
||||
|
||||
a) The work must carry prominent notices stating that you modified
|
||||
it, and giving a relevant date.
|
||||
|
||||
b) The work must carry prominent notices stating that it is
|
||||
released under this License and any conditions added under section
|
||||
7. This requirement modifies the requirement in section 4 to
|
||||
"keep intact all notices".
|
||||
|
||||
c) You must license the entire work, as a whole, under this
|
||||
License to anyone who comes into possession of a copy. This
|
||||
License will therefore apply, along with any applicable section 7
|
||||
additional terms, to the whole of the work, and all its parts,
|
||||
regardless of how they are packaged. This License gives no
|
||||
permission to license the work in any other way, but it does not
|
||||
invalidate such permission if you have separately received it.
|
||||
|
||||
d) If the work has interactive user interfaces, each must display
|
||||
Appropriate Legal Notices; however, if the Program has interactive
|
||||
interfaces that do not display Appropriate Legal Notices, your
|
||||
work need not make them do so.
|
||||
|
||||
A compilation of a covered work with other separate and independent
|
||||
works, which are not by their nature extensions of the covered work,
|
||||
and which are not combined with it such as to form a larger program,
|
||||
in or on a volume of a storage or distribution medium, is called an
|
||||
"aggregate" if the compilation and its resulting copyright are not
|
||||
used to limit the access or legal rights of the compilation's users
|
||||
beyond what the individual works permit. Inclusion of a covered work
|
||||
in an aggregate does not cause this License to apply to the other
|
||||
parts of the aggregate.
|
||||
|
||||
6. Conveying Non-Source Forms.
|
||||
|
||||
You may convey a covered work in object code form under the terms
|
||||
of sections 4 and 5, provided that you also convey the
|
||||
machine-readable Corresponding Source under the terms of this License,
|
||||
in one of these ways:
|
||||
|
||||
a) Convey the object code in, or embodied in, a physical product
|
||||
(including a physical distribution medium), accompanied by the
|
||||
Corresponding Source fixed on a durable physical medium
|
||||
customarily used for software interchange.
|
||||
|
||||
b) Convey the object code in, or embodied in, a physical product
|
||||
(including a physical distribution medium), accompanied by a
|
||||
written offer, valid for at least three years and valid for as
|
||||
long as you offer spare parts or customer support for that product
|
||||
model, to give anyone who possesses the object code either (1) a
|
||||
copy of the Corresponding Source for all the software in the
|
||||
product that is covered by this License, on a durable physical
|
||||
medium customarily used for software interchange, for a price no
|
||||
more than your reasonable cost of physically performing this
|
||||
conveying of source, or (2) access to copy the
|
||||
Corresponding Source from a network server at no charge.
|
||||
|
||||
c) Convey individual copies of the object code with a copy of the
|
||||
written offer to provide the Corresponding Source. This
|
||||
alternative is allowed only occasionally and noncommercially, and
|
||||
only if you received the object code with such an offer, in accord
|
||||
with subsection 6b.
|
||||
|
||||
d) Convey the object code by offering access from a designated
|
||||
place (gratis or for a charge), and offer equivalent access to the
|
||||
Corresponding Source in the same way through the same place at no
|
||||
further charge. You need not require recipients to copy the
|
||||
Corresponding Source along with the object code. If the place to
|
||||
copy the object code is a network server, the Corresponding Source
|
||||
may be on a different server (operated by you or a third party)
|
||||
that supports equivalent copying facilities, provided you maintain
|
||||
clear directions next to the object code saying where to find the
|
||||
Corresponding Source. Regardless of what server hosts the
|
||||
Corresponding Source, you remain obligated to ensure that it is
|
||||
available for as long as needed to satisfy these requirements.
|
||||
|
||||
e) Convey the object code using peer-to-peer transmission, provided
|
||||
you inform other peers where the object code and Corresponding
|
||||
Source of the work are being offered to the general public at no
|
||||
charge under subsection 6d.
|
||||
|
||||
A separable portion of the object code, whose source code is excluded
|
||||
from the Corresponding Source as a System Library, need not be
|
||||
included in conveying the object code work.
|
||||
|
||||
A "User Product" is either (1) a "consumer product", which means any
|
||||
tangible personal property which is normally used for personal, family,
|
||||
or household purposes, or (2) anything designed or sold for incorporation
|
||||
into a dwelling. In determining whether a product is a consumer product,
|
||||
doubtful cases shall be resolved in favor of coverage. For a particular
|
||||
product received by a particular user, "normally used" refers to a
|
||||
typical or common use of that class of product, regardless of the status
|
||||
of the particular user or of the way in which the particular user
|
||||
actually uses, or expects or is expected to use, the product. A product
|
||||
is a consumer product regardless of whether the product has substantial
|
||||
commercial, industrial or non-consumer uses, unless such uses represent
|
||||
the only significant mode of use of the product.
|
||||
|
||||
"Installation Information" for a User Product means any methods,
|
||||
procedures, authorization keys, or other information required to install
|
||||
and execute modified versions of a covered work in that User Product from
|
||||
a modified version of its Corresponding Source. The information must
|
||||
suffice to ensure that the continued functioning of the modified object
|
||||
code is in no case prevented or interfered with solely because
|
||||
modification has been made.
|
||||
|
||||
If you convey an object code work under this section in, or with, or
|
||||
specifically for use in, a User Product, and the conveying occurs as
|
||||
part of a transaction in which the right of possession and use of the
|
||||
User Product is transferred to the recipient in perpetuity or for a
|
||||
fixed term (regardless of how the transaction is characterized), the
|
||||
Corresponding Source conveyed under this section must be accompanied
|
||||
by the Installation Information. But this requirement does not apply
|
||||
if neither you nor any third party retains the ability to install
|
||||
modified object code on the User Product (for example, the work has
|
||||
been installed in ROM).
|
||||
|
||||
The requirement to provide Installation Information does not include a
|
||||
requirement to continue to provide support service, warranty, or updates
|
||||
for a work that has been modified or installed by the recipient, or for
|
||||
the User Product in which it has been modified or installed. Access to a
|
||||
network may be denied when the modification itself materially and
|
||||
adversely affects the operation of the network or violates the rules and
|
||||
protocols for communication across the network.
|
||||
|
||||
Corresponding Source conveyed, and Installation Information provided,
|
||||
in accord with this section must be in a format that is publicly
|
||||
documented (and with an implementation available to the public in
|
||||
source code form), and must require no special password or key for
|
||||
unpacking, reading or copying.
|
||||
|
||||
7. Additional Terms.
|
||||
|
||||
"Additional permissions" are terms that supplement the terms of this
|
||||
License by making exceptions from one or more of its conditions.
|
||||
Additional permissions that are applicable to the entire Program shall
|
||||
be treated as though they were included in this License, to the extent
|
||||
that they are valid under applicable law. If additional permissions
|
||||
apply only to part of the Program, that part may be used separately
|
||||
under those permissions, but the entire Program remains governed by
|
||||
this License without regard to the additional permissions.
|
||||
|
||||
When you convey a copy of a covered work, you may at your option
|
||||
remove any additional permissions from that copy, or from any part of
|
||||
it. (Additional permissions may be written to require their own
|
||||
removal in certain cases when you modify the work.) You may place
|
||||
additional permissions on material, added by you to a covered work,
|
||||
for which you have or can give appropriate copyright permission.
|
||||
|
||||
Notwithstanding any other provision of this License, for material you
|
||||
add to a covered work, you may (if authorized by the copyright holders of
|
||||
that material) supplement the terms of this License with terms:
|
||||
|
||||
a) Disclaiming warranty or limiting liability differently from the
|
||||
terms of sections 15 and 16 of this License; or
|
||||
|
||||
b) Requiring preservation of specified reasonable legal notices or
|
||||
author attributions in that material or in the Appropriate Legal
|
||||
Notices displayed by works containing it; or
|
||||
|
||||
c) Prohibiting misrepresentation of the origin of that material, or
|
||||
requiring that modified versions of such material be marked in
|
||||
reasonable ways as different from the original version; or
|
||||
|
||||
d) Limiting the use for publicity purposes of names of licensors or
|
||||
authors of the material; or
|
||||
|
||||
e) Declining to grant rights under trademark law for use of some
|
||||
trade names, trademarks, or service marks; or
|
||||
|
||||
f) Requiring indemnification of licensors and authors of that
|
||||
material by anyone who conveys the material (or modified versions of
|
||||
it) with contractual assumptions of liability to the recipient, for
|
||||
any liability that these contractual assumptions directly impose on
|
||||
those licensors and authors.
|
||||
|
||||
All other non-permissive additional terms are considered "further
|
||||
restrictions" within the meaning of section 10. If the Program as you
|
||||
received it, or any part of it, contains a notice stating that it is
|
||||
governed by this License along with a term that is a further
|
||||
restriction, you may remove that term. If a license document contains
|
||||
a further restriction but permits relicensing or conveying under this
|
||||
License, you may add to a covered work material governed by the terms
|
||||
of that license document, provided that the further restriction does
|
||||
not survive such relicensing or conveying.
|
||||
|
||||
If you add terms to a covered work in accord with this section, you
|
||||
must place, in the relevant source files, a statement of the
|
||||
additional terms that apply to those files, or a notice indicating
|
||||
where to find the applicable terms.
|
||||
|
||||
Additional terms, permissive or non-permissive, may be stated in the
|
||||
form of a separately written license, or stated as exceptions;
|
||||
the above requirements apply either way.
|
||||
|
||||
8. Termination.
|
||||
|
||||
You may not propagate or modify a covered work except as expressly
|
||||
provided under this License. Any attempt otherwise to propagate or
|
||||
modify it is void, and will automatically terminate your rights under
|
||||
this License (including any patent licenses granted under the third
|
||||
paragraph of section 11).
|
||||
|
||||
However, if you cease all violation of this License, then your
|
||||
license from a particular copyright holder is reinstated (a)
|
||||
provisionally, unless and until the copyright holder explicitly and
|
||||
finally terminates your license, and (b) permanently, if the copyright
|
||||
holder fails to notify you of the violation by some reasonable means
|
||||
prior to 60 days after the cessation.
|
||||
|
||||
Moreover, your license from a particular copyright holder is
|
||||
reinstated permanently if the copyright holder notifies you of the
|
||||
violation by some reasonable means, this is the first time you have
|
||||
received notice of violation of this License (for any work) from that
|
||||
copyright holder, and you cure the violation prior to 30 days after
|
||||
your receipt of the notice.
|
||||
|
||||
Termination of your rights under this section does not terminate the
|
||||
licenses of parties who have received copies or rights from you under
|
||||
this License. If your rights have been terminated and not permanently
|
||||
reinstated, you do not qualify to receive new licenses for the same
|
||||
material under section 10.
|
||||
|
||||
9. Acceptance Not Required for Having Copies.
|
||||
|
||||
You are not required to accept this License in order to receive or
|
||||
run a copy of the Program. Ancillary propagation of a covered work
|
||||
occurring solely as a consequence of using peer-to-peer transmission
|
||||
to receive a copy likewise does not require acceptance. However,
|
||||
nothing other than this License grants you permission to propagate or
|
||||
modify any covered work. These actions infringe copyright if you do
|
||||
not accept this License. Therefore, by modifying or propagating a
|
||||
covered work, you indicate your acceptance of this License to do so.
|
||||
|
||||
10. Automatic Licensing of Downstream Recipients.
|
||||
|
||||
Each time you convey a covered work, the recipient automatically
|
||||
receives a license from the original licensors, to run, modify and
|
||||
propagate that work, subject to this License. You are not responsible
|
||||
for enforcing compliance by third parties with this License.
|
||||
|
||||
An "entity transaction" is a transaction transferring control of an
|
||||
organization, or substantially all assets of one, or subdividing an
|
||||
organization, or merging organizations. If propagation of a covered
|
||||
work results from an entity transaction, each party to that
|
||||
transaction who receives a copy of the work also receives whatever
|
||||
licenses to the work the party's predecessor in interest had or could
|
||||
give under the previous paragraph, plus a right to possession of the
|
||||
Corresponding Source of the work from the predecessor in interest, if
|
||||
the predecessor has it or can get it with reasonable efforts.
|
||||
|
||||
You may not impose any further restrictions on the exercise of the
|
||||
rights granted or affirmed under this License. For example, you may
|
||||
not impose a license fee, royalty, or other charge for exercise of
|
||||
rights granted under this License, and you may not initiate litigation
|
||||
(including a cross-claim or counterclaim in a lawsuit) alleging that
|
||||
any patent claim is infringed by making, using, selling, offering for
|
||||
sale, or importing the Program or any portion of it.
|
||||
|
||||
11. Patents.
|
||||
|
||||
A "contributor" is a copyright holder who authorizes use under this
|
||||
License of the Program or a work on which the Program is based. The
|
||||
work thus licensed is called the contributor's "contributor version".
|
||||
|
||||
A contributor's "essential patent claims" are all patent claims
|
||||
owned or controlled by the contributor, whether already acquired or
|
||||
hereafter acquired, that would be infringed by some manner, permitted
|
||||
by this License, of making, using, or selling its contributor version,
|
||||
but do not include claims that would be infringed only as a
|
||||
consequence of further modification of the contributor version. For
|
||||
purposes of this definition, "control" includes the right to grant
|
||||
patent sublicenses in a manner consistent with the requirements of
|
||||
this License.
|
||||
|
||||
Each contributor grants you a non-exclusive, worldwide, royalty-free
|
||||
patent license under the contributor's essential patent claims, to
|
||||
make, use, sell, offer for sale, import and otherwise run, modify and
|
||||
propagate the contents of its contributor version.
|
||||
|
||||
In the following three paragraphs, a "patent license" is any express
|
||||
agreement or commitment, however denominated, not to enforce a patent
|
||||
(such as an express permission to practice a patent or covenant not to
|
||||
sue for patent infringement). To "grant" such a patent license to a
|
||||
party means to make such an agreement or commitment not to enforce a
|
||||
patent against the party.
|
||||
|
||||
If you convey a covered work, knowingly relying on a patent license,
|
||||
and the Corresponding Source of the work is not available for anyone
|
||||
to copy, free of charge and under the terms of this License, through a
|
||||
publicly available network server or other readily accessible means,
|
||||
then you must either (1) cause the Corresponding Source to be so
|
||||
available, or (2) arrange to deprive yourself of the benefit of the
|
||||
patent license for this particular work, or (3) arrange, in a manner
|
||||
consistent with the requirements of this License, to extend the patent
|
||||
license to downstream recipients. "Knowingly relying" means you have
|
||||
actual knowledge that, but for the patent license, your conveying the
|
||||
covered work in a country, or your recipient's use of the covered work
|
||||
in a country, would infringe one or more identifiable patents in that
|
||||
country that you have reason to believe are valid.
|
||||
|
||||
If, pursuant to or in connection with a single transaction or
|
||||
arrangement, you convey, or propagate by procuring conveyance of, a
|
||||
covered work, and grant a patent license to some of the parties
|
||||
receiving the covered work authorizing them to use, propagate, modify
|
||||
or convey a specific copy of the covered work, then the patent license
|
||||
you grant is automatically extended to all recipients of the covered
|
||||
work and works based on it.
|
||||
|
||||
A patent license is "discriminatory" if it does not include within
|
||||
the scope of its coverage, prohibits the exercise of, or is
|
||||
conditioned on the non-exercise of one or more of the rights that are
|
||||
specifically granted under this License. You may not convey a covered
|
||||
work if you are a party to an arrangement with a third party that is
|
||||
in the business of distributing software, under which you make payment
|
||||
to the third party based on the extent of your activity of conveying
|
||||
the work, and under which the third party grants, to any of the
|
||||
parties who would receive the covered work from you, a discriminatory
|
||||
patent license (a) in connection with copies of the covered work
|
||||
conveyed by you (or copies made from those copies), or (b) primarily
|
||||
for and in connection with specific products or compilations that
|
||||
contain the covered work, unless you entered into that arrangement,
|
||||
or that patent license was granted, prior to 28 March 2007.
|
||||
|
||||
Nothing in this License shall be construed as excluding or limiting
|
||||
any implied license or other defenses to infringement that may
|
||||
otherwise be available to you under applicable patent law.
|
||||
|
||||
12. No Surrender of Others' Freedom.
|
||||
|
||||
If conditions are imposed on you (whether by court order, agreement or
|
||||
otherwise) that contradict the conditions of this License, they do not
|
||||
excuse you from the conditions of this License. If you cannot convey a
|
||||
covered work so as to satisfy simultaneously your obligations under this
|
||||
License and any other pertinent obligations, then as a consequence you may
|
||||
not convey it at all. For example, if you agree to terms that obligate you
|
||||
to collect a royalty for further conveying from those to whom you convey
|
||||
the Program, the only way you could satisfy both those terms and this
|
||||
License would be to refrain entirely from conveying the Program.
|
||||
|
||||
13. Remote Network Interaction; Use with the GNU General Public License.
|
||||
|
||||
Notwithstanding any other provision of this License, if you modify the
|
||||
Program, your modified version must prominently offer all users
|
||||
interacting with it remotely through a computer network (if your version
|
||||
supports such interaction) an opportunity to receive the Corresponding
|
||||
Source of your version by providing access to the Corresponding Source
|
||||
from a network server at no charge, through some standard or customary
|
||||
means of facilitating copying of software. This Corresponding Source
|
||||
shall include the Corresponding Source for any work covered by version 3
|
||||
of the GNU General Public License that is incorporated pursuant to the
|
||||
following paragraph.
|
||||
|
||||
Notwithstanding any other provision of this License, you have
|
||||
permission to link or combine any covered work with a work licensed
|
||||
under version 3 of the GNU General Public License into a single
|
||||
combined work, and to convey the resulting work. The terms of this
|
||||
License will continue to apply to the part which is the covered work,
|
||||
but the work with which it is combined will remain governed by version
|
||||
3 of the GNU General Public License.
|
||||
|
||||
14. Revised Versions of this License.
|
||||
|
||||
The Free Software Foundation may publish revised and/or new versions of
|
||||
the GNU Affero General Public License from time to time. Such new versions
|
||||
will be similar in spirit to the present version, but may differ in detail to
|
||||
address new problems or concerns.
|
||||
|
||||
Each version is given a distinguishing version number. If the
|
||||
Program specifies that a certain numbered version of the GNU Affero General
|
||||
Public License "or any later version" applies to it, you have the
|
||||
option of following the terms and conditions either of that numbered
|
||||
version or of any later version published by the Free Software
|
||||
Foundation. If the Program does not specify a version number of the
|
||||
GNU Affero General Public License, you may choose any version ever published
|
||||
by the Free Software Foundation.
|
||||
|
||||
If the Program specifies that a proxy can decide which future
|
||||
versions of the GNU Affero General Public License can be used, that proxy's
|
||||
public statement of acceptance of a version permanently authorizes you
|
||||
to choose that version for the Program.
|
||||
|
||||
Later license versions may give you additional or different
|
||||
permissions. However, no additional obligations are imposed on any
|
||||
author or copyright holder as a result of your choosing to follow a
|
||||
later version.
|
||||
|
||||
15. Disclaimer of Warranty.
|
||||
|
||||
THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY
|
||||
APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT
|
||||
HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY
|
||||
OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO,
|
||||
THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
|
||||
PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM
|
||||
IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF
|
||||
ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
|
||||
|
||||
16. Limitation of Liability.
|
||||
|
||||
IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
|
||||
WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS
|
||||
THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY
|
||||
GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE
|
||||
USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF
|
||||
DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD
|
||||
PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS),
|
||||
EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF
|
||||
SUCH DAMAGES.
|
||||
|
||||
17. Interpretation of Sections 15 and 16.
|
||||
|
||||
If the disclaimer of warranty and limitation of liability provided
|
||||
above cannot be given local legal effect according to their terms,
|
||||
reviewing courts shall apply local law that most closely approximates
|
||||
an absolute waiver of all civil liability in connection with the
|
||||
Program, unless a warranty or assumption of liability accompanies a
|
||||
copy of the Program in return for a fee.
|
||||
|
||||
END OF TERMS AND CONDITIONS
|
||||
|
||||
How to Apply These Terms to Your New Programs
|
||||
|
||||
If you develop a new program, and you want it to be of the greatest
|
||||
possible use to the public, the best way to achieve this is to make it
|
||||
free software which everyone can redistribute and change under these terms.
|
||||
|
||||
To do so, attach the following notices to the program. It is safest
|
||||
to attach them to the start of each source file to most effectively
|
||||
state the exclusion of warranty; and each file should have at least
|
||||
the "copyright" line and a pointer to where the full notice is found.
|
||||
|
||||
<one line to give the program's name and a brief idea of what it does.>
|
||||
Copyright (C) <year> <name of author>
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU Affero General Public License as published
|
||||
by the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU Affero General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU Affero General Public License
|
||||
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
Also add information on how to contact you by electronic and paper mail.
|
||||
|
||||
If your software can interact with users remotely through a computer
|
||||
network, you should also make sure that it provides a way for users to
|
||||
get its source. For example, if your program is a web application, its
|
||||
interface could display a "Source" link that leads users to an archive
|
||||
of the code. There are many ways you could offer source, and different
|
||||
solutions will be better for different programs; see section 13 for the
|
||||
specific requirements.
|
||||
|
||||
You should also get your employer (if you work as a programmer) or school,
|
||||
if any, to sign a "copyright disclaimer" for the program, if necessary.
|
||||
For more information on this, and how to apply and follow the GNU AGPL, see
|
||||
<https://www.gnu.org/licenses/>.
|
||||
+14
@@ -0,0 +1,14 @@
|
||||
# Third-Party Notices
|
||||
|
||||
The files in `dist/v1/` are recovered prebuilt artifacts from `Yu9191/wloc`,
|
||||
obtained from the community restoration at `xepes0/wloc`. The original author
|
||||
and contributor attribution is preserved. These files are distributed under
|
||||
the included GNU Affero General Public License v3.0 (`LICENSE`).
|
||||
|
||||
The recovered upstream notice states that the prebuilt files may contain
|
||||
third-party components whose complete dependency and reproducible-build records
|
||||
were not recovered. This notice is not a complete software bill of materials or
|
||||
legal audit.
|
||||
|
||||
See `UPSTREAM.md` for the pinned source, hashes, local differences, regression
|
||||
checks, and maintenance exit condition.
|
||||
Vendored
+44
@@ -0,0 +1,44 @@
|
||||
# WLOC Upstream Provenance
|
||||
|
||||
## Pinned source
|
||||
|
||||
- Original repository: `https://github.com/Yu9191/wloc` (deleted)
|
||||
- Community restoration: `https://github.com/xepes0/wloc`
|
||||
- Pinned restoration commit: `7f28c5e4fb6b802862988735b92e5c800382f2a2`
|
||||
- Recovered original baseline: `529fcd841952571e79f40faf2d3e0ef90a7bdfa6`
|
||||
- License: GNU Affero General Public License v3.0; retained in `LICENSE`
|
||||
|
||||
The restoration preserves the original repository history and attributes the
|
||||
scripts to Yu9191 and the original contributors.
|
||||
|
||||
## Included file integrity
|
||||
|
||||
| File | SHA-256 |
|
||||
| --- | --- |
|
||||
| `dist/v1/wloc.js` | `a1b361e60f0b434585260fb59c65d1ddbe3bff89ace3639f592e7d8af432b3c1` |
|
||||
| `dist/v1/wloc-settings.js` | `cbff047a7f42055615ee19e208ddfc2ec66833e6f2b75555eac84f1c299a66cf` |
|
||||
| `LICENSE` | `8486a10c4393cee1c25392769ddd3b2d6c242d6ec7928e1414efff7dfb2f07ef` |
|
||||
|
||||
The response-script hash also matches the independent `pixian5/wloc`
|
||||
restoration at the time of import.
|
||||
|
||||
## Local differences
|
||||
|
||||
The two files under `dist/v1/` and `LICENSE` are byte-for-byte copies from the
|
||||
pinned restoration. Module files retain the recovered interception behavior but
|
||||
replace homepage, documentation, icon, and script URLs with project-owned URLs.
|
||||
The mirrored module variants use `gh-proxy.org`; direct variants use GitHub Raw.
|
||||
Both carry cache version `1.0.8`.
|
||||
|
||||
## Regression checks
|
||||
|
||||
Run `npm test` in this directory. Tests pin the exact artifact and license
|
||||
hashes and exercise Shadowrocket save/query behavior with the `wloc_settings`
|
||||
persistent key. Project contract and Xcode tests validate module URLs and app
|
||||
integration.
|
||||
|
||||
## Exit condition
|
||||
|
||||
Re-evaluate these copies when an authoritative maintained upstream becomes
|
||||
available. Return to an upstream dependency only after its license, provenance,
|
||||
protocol compatibility, cache behavior, and regression tests have been checked.
|
||||
File diff suppressed because one or more lines are too long
+2
File diff suppressed because one or more lines are too long
+11
@@ -0,0 +1,11 @@
|
||||
#!name=Apple WLOC 定位修改
|
||||
#!desc=Apple 网络定位修改 | 使用说明: https://github.com/xweiba/location-spoofer/blob/main/docs/THIRD_PARTY_MODULES.md
|
||||
#!author=Yu9191 Rewrite
|
||||
#!homepage=https://github.com/xweiba/location-spoofer
|
||||
|
||||
[rewrite_local]
|
||||
^https?:\/\/(?:gs-loc(?:-cn)?\.apple\.com|gsp-ssl\.ls\.apple\.com|bluedot\.is\.autonavi\.com(?:\.gds\.alibabadns\.com)?)\/clls\/wloc url script-response-body https://raw.githubusercontent.com/xweiba/location-spoofer/main/ThirdParty/WlocScripts/dist/v1/wloc.js?v=1.0.8
|
||||
^https?:\/\/gs-loc(-cn)?\.apple\.com\/wloc-settings\/save url script-echo-response https://raw.githubusercontent.com/xweiba/location-spoofer/main/ThirdParty/WlocScripts/dist/v1/wloc-settings.js?v=1.0.8
|
||||
|
||||
[mitm]
|
||||
hostname = gs-loc.apple.com, gs-loc-cn.apple.com, gsp-ssl.ls.apple.com, bluedot.is.autonavi.com, bluedot.is.autonavi.com.gds.alibabadns.com
|
||||
+20
@@ -0,0 +1,20 @@
|
||||
#!name=Apple WLOC 定位修改
|
||||
#!desc=Apple 网络定位修改 | 使用说明: https://github.com/xweiba/location-spoofer/blob/main/docs/THIRD_PARTY_MODULES.md
|
||||
#!icon=https://raw.githubusercontent.com/xweiba/location-spoofer/main/Resources/Assets.xcassets/AppIcon.appiconset/AppIcon.png
|
||||
#!author=Yu9191 Rewrite
|
||||
#!homepage=https://github.com/xweiba/location-spoofer
|
||||
|
||||
|
||||
[Argument]
|
||||
longitude = input, "113.94114", tag=经度(在线选点优先)
|
||||
latitude = input, "22.544577", tag=纬度(在线选点优先)
|
||||
accuracy = input, "25", tag=精度(米)
|
||||
randomRadius = input, "0", tag=扰动半径(米,0为关闭)
|
||||
logLevel = select, "info", "off", "error", "warn", "debug", "all", tag=日志级别
|
||||
|
||||
[Script]
|
||||
http-response ^https?:\/\/(?:gs-loc(?:-cn)?\.apple\.com|gsp-ssl\.ls\.apple\.com|bluedot\.is\.autonavi\.com(?:\.gds\.alibabadns\.com)?)\/clls\/wloc script-path=https://raw.githubusercontent.com/xweiba/location-spoofer/main/ThirdParty/WlocScripts/dist/v1/wloc.js?v=1.0.8, requires-body=true, binary-body-mode=true, timeout=30, tag=Apple WLOC, argument=[{longitude},{latitude},{accuracy},{randomRadius},{logLevel}]
|
||||
http-request ^https?:\/\/gs-loc(-cn)?\.apple\.com\/wloc-settings\/save script-path=https://raw.githubusercontent.com/xweiba/location-spoofer/main/ThirdParty/WlocScripts/dist/v1/wloc-settings.js?v=1.0.8, timeout=10, tag=WLOC Settings
|
||||
|
||||
[MITM]
|
||||
hostname = gs-loc.apple.com, gs-loc-cn.apple.com, gsp-ssl.ls.apple.com, bluedot.is.autonavi.com, bluedot.is.autonavi.com.gds.alibabadns.com
|
||||
@@ -0,0 +1,13 @@
|
||||
#!name=Apple WLOC 定位修改
|
||||
#!desc=Apple 网络定位修改 | 使用说明: https://github.com/xweiba/location-spoofer/blob/main/docs/THIRD_PARTY_MODULES.md
|
||||
#!author=Yu9191 Rewrite
|
||||
#!homepage=https://github.com/xweiba/location-spoofer
|
||||
#!icon=https://raw.githubusercontent.com/xweiba/location-spoofer/main/Resources/Assets.xcassets/AppIcon.appiconset/AppIcon.png
|
||||
#!category=Tools
|
||||
|
||||
[Script]
|
||||
Apple WLOC = type=http-response,pattern=^https?:\/\/(?:gs-loc(?:-cn)?\.apple\.com|gsp-ssl\.ls\.apple\.com|bluedot\.is\.autonavi\.com(?:\.gds\.alibabadns\.com)?)\/clls\/wloc,requires-body=1,binary-body-mode=1,max-size=0,timeout=30,script-path=https://raw.githubusercontent.com/xweiba/location-spoofer/main/ThirdParty/WlocScripts/dist/v1/wloc.js?v=1.0.8,argument=longitude=113.94114&latitude=22.544577&accuracy=25&randomRadius=0&logLevel=info
|
||||
WLOC Settings = type=http-request,pattern=^https?:\/\/gs-loc(-cn)?\.apple\.com\/wloc-settings\/save,requires-body=0,max-size=0,timeout=10,script-path=https://raw.githubusercontent.com/xweiba/location-spoofer/main/ThirdParty/WlocScripts/dist/v1/wloc-settings.js?v=1.0.8
|
||||
|
||||
[MITM]
|
||||
hostname = %APPEND% gs-loc.apple.com, gs-loc-cn.apple.com, gsp-ssl.ls.apple.com, bluedot.is.autonavi.com, bluedot.is.autonavi.com.gds.alibabadns.com
|
||||
@@ -0,0 +1,14 @@
|
||||
#!name=Apple WLOC 定位修改
|
||||
#!desc=Apple 网络定位修改 | 使用说明: https://github.com/xweiba/location-spoofer/blob/main/docs/THIRD_PARTY_MODULES.md
|
||||
#!author=Yu9191 Rewrite
|
||||
#!homepage=https://github.com/xweiba/location-spoofer
|
||||
#!category=Tools
|
||||
#!arguments=经度:113.94114, 纬度:22.544577, 精度:25, 扰动半径:0, 日志级别:info
|
||||
#!arguments-desc=经度/纬度: 默认坐标(在线选点储存后优先)\n精度: GPS精度(米)\n扰动半径: 每次响应在目标点周围随机偏移的最大距离(米),0为关闭\n日志级别: off/error/warn/info/debug/all\n\n使用方法: 打开选点页面 -> 选位置 -> 储存到设备
|
||||
|
||||
[Script]
|
||||
Apple WLOC = type=http-response, pattern="^https?:\/\/(?:gs-loc(?:-cn)?\.apple\.com|gsp-ssl\.ls\.apple\.com|bluedot\.is\.autonavi\.com(?:\.gds\.alibabadns\.com)?)\/clls\/wloc", requires-body=1, binary-body-mode=1, max-size=0, timeout=30, script-path=https://raw.githubusercontent.com/xweiba/location-spoofer/main/ThirdParty/WlocScripts/dist/v1/wloc.js?v=1.0.8, argument=longitude={{{经度}}}&latitude={{{纬度}}}&accuracy={{{精度}}}&randomRadius={{{扰动半径}}}&logLevel={{{日志级别}}}
|
||||
WLOC Settings = type=http-request, pattern="^https?:\/\/gs-loc(-cn)?\.apple\.com\/wloc-settings\/save", requires-body=0, max-size=0, timeout=10, script-path=https://raw.githubusercontent.com/xweiba/location-spoofer/main/ThirdParty/WlocScripts/dist/v1/wloc-settings.js?v=1.0.8
|
||||
|
||||
[MITM]
|
||||
hostname = %APPEND% gs-loc.apple.com, gs-loc-cn.apple.com, gsp-ssl.ls.apple.com, bluedot.is.autonavi.com, bluedot.is.autonavi.com.gds.alibabadns.com
|
||||
@@ -0,0 +1,36 @@
|
||||
name: Apple WLOC 定位修改
|
||||
desc: "Apple 网络定位修改 | 使用说明: https://github.com/xweiba/location-spoofer/blob/main/docs/THIRD_PARTY_MODULES.md"
|
||||
author: Yu9191 Rewrite
|
||||
homepage: https://github.com/xweiba/location-spoofer
|
||||
icon: https://raw.githubusercontent.com/xweiba/location-spoofer/main/Resources/Assets.xcassets/AppIcon.appiconset/AppIcon.png
|
||||
category: Tools
|
||||
|
||||
http:
|
||||
mitm:
|
||||
- "gs-loc.apple.com"
|
||||
- "gs-loc-cn.apple.com"
|
||||
- "gsp-ssl.ls.apple.com"
|
||||
- "bluedot.is.autonavi.com"
|
||||
- "bluedot.is.autonavi.com.gds.alibabadns.com"
|
||||
script:
|
||||
- match: ^https?:\/\/(?:gs-loc(?:-cn)?\.apple\.com|gsp-ssl\.ls\.apple\.com|bluedot\.is\.autonavi\.com(?:\.gds\.alibabadns\.com)?)\/clls\/wloc
|
||||
name: WLOC.Location
|
||||
type: response
|
||||
require-body: true
|
||||
binary-mode: true
|
||||
max-size: 0
|
||||
timeout: 30
|
||||
argument: longitude=113.94114&latitude=22.544577&accuracy=25&randomRadius=0&logLevel=info
|
||||
- match: ^https?:\/\/gs-loc(-cn)?\.apple\.com\/wloc-settings\/save
|
||||
name: WLOC.Settings
|
||||
type: request
|
||||
require-body: false
|
||||
timeout: 10
|
||||
|
||||
script-providers:
|
||||
WLOC.Location:
|
||||
url: https://raw.githubusercontent.com/xweiba/location-spoofer/main/ThirdParty/WlocScripts/dist/v1/wloc.js?v=1.0.8
|
||||
interval: 86400
|
||||
WLOC.Settings:
|
||||
url: https://raw.githubusercontent.com/xweiba/location-spoofer/main/ThirdParty/WlocScripts/dist/v1/wloc-settings.js?v=1.0.8
|
||||
interval: 86400
|
||||
Vendored
+10
@@ -0,0 +1,10 @@
|
||||
{
|
||||
"name": "@location-spoofer/wloc-scripts",
|
||||
"version": "1.0.8",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"license": "AGPL-3.0",
|
||||
"scripts": {
|
||||
"test": "node --test"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
import test from "node:test";
|
||||
import assert from "node:assert/strict";
|
||||
import { createHash } from "node:crypto";
|
||||
import { readFile } from "node:fs/promises";
|
||||
import vm from "node:vm";
|
||||
|
||||
const files = new Map([
|
||||
[new URL("../dist/v1/wloc.js", import.meta.url), "a1b361e60f0b434585260fb59c65d1ddbe3bff89ace3639f592e7d8af432b3c1"],
|
||||
[new URL("../dist/v1/wloc-settings.js", import.meta.url), "cbff047a7f42055615ee19e208ddfc2ec66833e6f2b75555eac84f1c299a66cf"],
|
||||
[new URL("../LICENSE", import.meta.url), "8486a10c4393cee1c25392769ddd3b2d6c242d6ec7928e1414efff7dfb2f07ef"]
|
||||
]);
|
||||
|
||||
test("vendored upstream files retain their pinned SHA-256 hashes", async () => {
|
||||
for (const [file, expectedHash] of files) {
|
||||
const contents = await readFile(file);
|
||||
const actualHash = createHash("sha256").update(contents).digest("hex");
|
||||
assert.equal(actualHash, expectedHash, file.pathname);
|
||||
}
|
||||
});
|
||||
|
||||
test("settings bundle saves and queries wloc_settings in Shadowrocket", async () => {
|
||||
const source = await readFile(new URL("../dist/v1/wloc-settings.js", import.meta.url), "utf8");
|
||||
const storage = new Map();
|
||||
|
||||
const run = (url) => {
|
||||
let result;
|
||||
vm.runInNewContext(source, {
|
||||
$rocket: {},
|
||||
$request: { url },
|
||||
$persistentStore: {
|
||||
read: (key) => storage.get(key) ?? null,
|
||||
write: (value, key) => {
|
||||
storage.set(key, value);
|
||||
return true;
|
||||
}
|
||||
},
|
||||
$done: (value) => { result = value; },
|
||||
console
|
||||
});
|
||||
return JSON.parse(result.response.body);
|
||||
};
|
||||
|
||||
const saved = run("https://gs-loc.apple.com/wloc-settings/save?lon=121.1&lat=31.2&acc=25");
|
||||
assert.equal(saved.success, true);
|
||||
assert.equal(storage.has("wloc_settings"), true);
|
||||
|
||||
const queried = run("https://gs-loc.apple.com/wloc-settings/save?action=query");
|
||||
assert.deepEqual(
|
||||
{ success: queried.success, longitude: queried.longitude, latitude: queried.latitude, accuracy: queried.accuracy },
|
||||
{ success: true, longitude: 121.1, latitude: 31.2, accuracy: 25 }
|
||||
);
|
||||
});
|
||||
@@ -8,7 +8,7 @@ usage() {
|
||||
cat <<'USAGE'
|
||||
Usage: ./build.sh [--test]
|
||||
|
||||
Builds dist/PaopaoLocationSpoofer-unsigned.ipa without signing it.
|
||||
Builds an unsigned IPA with both stable and timestamped filenames in dist/.
|
||||
|
||||
Options:
|
||||
--test After the unsigned IPA is created, run iOS Simulator unit tests.
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
|
||||
## 已发布
|
||||
|
||||
- [v1.0.7](https://github.com/xweiba/location-spoofer/releases/tag/v1.0.7) — 2026-09-11
|
||||
- [v1.0.6](https://github.com/xweiba/location-spoofer/releases/tag/v1.0.6) — 2026-09-01
|
||||
- [v1.0.5](https://github.com/xweiba/location-spoofer/releases/tag/v1.0.5) — 2026-08-10
|
||||
- [v1.0.4](https://github.com/xweiba/location-spoofer/releases/tag/v1.0.4) — 2026-08-09
|
||||
|
||||
+46
-21
@@ -1,20 +1,23 @@
|
||||
# Third-party proxy modules
|
||||
|
||||
Third-party proxy mode (Surge / Quantumult X / Loon / Shadowrocket / Stash /
|
||||
Egern) now relies entirely on the upstream
|
||||
[Yu9191/wloc](https://github.com/Yu9191/wloc) modules. The App no longer
|
||||
maintains or ships project-owned module/script copies, so it never drifts
|
||||
from the upstream protocol.
|
||||
The third-party proxy modules and recovered WLOC scripts are hosted in this
|
||||
repository. The original `Yu9191/wloc` repository was deleted, so it is retained
|
||||
as provenance and is not a runtime dependency.
|
||||
|
||||
## Subscription addresses
|
||||
## Hosted files
|
||||
|
||||
The App builds each client's module subscription URL directly from the
|
||||
upstream repository:
|
||||
- Mirrored module subscriptions: `Resources/ThirdPartyProxyModules/`
|
||||
- Direct module subscriptions: `ThirdParty/WlocScripts/modules/direct/`
|
||||
- Versioned recovered scripts: `ThirdParty/WlocScripts/dist/v1/`
|
||||
- Integrity and runtime tests: `ThirdParty/WlocScripts/test/`
|
||||
|
||||
- default mirror (gh-proxy):
|
||||
`https://gh-proxy.org/https://raw.githubusercontent.com/Yu9191/wloc/refs/heads/main/modules/<file>`
|
||||
The App appends `?v=1.0.8` to module subscription URLs, and every module uses
|
||||
the same cache version for its script URLs:
|
||||
|
||||
- default mirror:
|
||||
`https://gh-proxy.org/https://raw.githubusercontent.com/xweiba/location-spoofer/main/Resources/ThirdPartyProxyModules/<file>?v=1.0.8`
|
||||
- direct:
|
||||
`https://raw.githubusercontent.com/Yu9191/wloc/refs/heads/main/modules/<file>`
|
||||
`https://raw.githubusercontent.com/xweiba/location-spoofer/main/ThirdParty/WlocScripts/modules/direct/<file>?v=1.0.8`
|
||||
|
||||
| Module file | Client |
|
||||
|---|---|
|
||||
@@ -24,17 +27,39 @@ upstream repository:
|
||||
| `wloc.lpx` | Loon |
|
||||
| `wloc.stoverride` | Stash |
|
||||
|
||||
No `?v=` cache-bust is appended: the URL points at upstream's latest content,
|
||||
and re-importing the subscription in the proxy client re-fetches it.
|
||||
These URLs become downloadable after the change is merged into `main`. Until
|
||||
then, validate them from the checked-out paths.
|
||||
|
||||
## Script protocol
|
||||
|
||||
The upstream `wloc.js` patches Apple WLOC responses and reads coordinates from
|
||||
the `wloc_settings` persistent key or the module `argument` config. The
|
||||
upstream `wloc-settings.js` implements `wloc-settings/save` (query/clear/save)
|
||||
using `lon`/`lat`/`acc`/`randomRadius` parameters.
|
||||
`wloc.js` patches Apple WLOC responses and reads coordinates from the
|
||||
`wloc_settings` persistent key or module arguments. `wloc-settings.js`
|
||||
implements `wloc-settings/save` query, clear, and save actions using
|
||||
`lon`/`lat`/`acc`/`randomRadius` parameters.
|
||||
|
||||
The App's third-party save sends `lon`/`lat`/`acc`, matching the upstream
|
||||
script. Motion-state simulation (fields 11/12) is **not** implemented by the
|
||||
upstream scripts and is unavailable in third-party mode; it remains available
|
||||
in APP mode (built-in proxy).
|
||||
The App's third-party save sends WGS-84 `lon`/`lat`/`acc` values. Motion-state
|
||||
simulation is unavailable in third-party mode; it remains available in App Mode
|
||||
through the built-in proxy.
|
||||
|
||||
## Provenance and maintenance
|
||||
|
||||
The prebuilt scripts and license were copied byte-for-byte from the
|
||||
`xepes0/wloc` community restoration pinned at
|
||||
`7f28c5e4fb6b802862988735b92e5c800382f2a2`. The recovered original baseline is
|
||||
`529fcd841952571e79f40faf2d3e0ef90a7bdfa6`. Module metadata keeps the original
|
||||
author attribution while runtime URLs point to this project.
|
||||
|
||||
Full hashes, local differences, regression checks, licensing notes, and the
|
||||
maintenance exit condition are recorded in
|
||||
`ThirdParty/WlocScripts/UPSTREAM.md` and
|
||||
`ThirdParty/WlocScripts/THIRD_PARTY_NOTICES.md`.
|
||||
|
||||
Test the scripts with:
|
||||
|
||||
```bash
|
||||
cd ThirdParty/WlocScripts
|
||||
npm test
|
||||
```
|
||||
|
||||
There is no local rebuild command because the restoration did not recover a
|
||||
complete reproducible source tree for these prebuilt artifacts.
|
||||
|
||||
@@ -0,0 +1,26 @@
|
||||
# v1.0.7
|
||||
|
||||
发布日期:2026-09-11
|
||||
|
||||
## 主要更新
|
||||
|
||||
- 新增完整英文界面,并将诊断日志和 Bug 报告正文调整为英文,方便在 GitHub 提交和排查问题。
|
||||
- 从保留完整历史的 `xepes0/wloc` 恢复原 WLOC 脚本,固定上游提交与文件哈希,并改由本仓库长期托管。
|
||||
- 修复 Shadowrocket 等第三方客户端的脚本失效问题,模块和脚本统一使用项目自有地址及 `1.0.8` 缓存版本。
|
||||
- HTTPS 解密配置展示全部五个定位域名,每个域名提供独立复制按钮,并可直接打开当前第三方客户端。
|
||||
- 本地打包同时生成固定文件名和带生成时间的未签名 IPA,避免同名文件缓存造成误用。
|
||||
|
||||
## 提交变更总结
|
||||
|
||||
- `68cd5fd` fix(proxy): 恢复上游脚本与逐项域名复制
|
||||
- `0594970` fix(i18n): 完善国际化并迁移 WLOC 脚本
|
||||
- `908ac8f` Add English localization (device-language based)
|
||||
|
||||
## 自签安装
|
||||
|
||||
- Release 附件为未签名 IPA,安装前需要自行签名。
|
||||
- 可使用免费 Apple ID 和 Impactor 完成签名安装,无需付费开发者账号。
|
||||
- 签名时请保留 Bundle ID `com.paopaolabs.location-spoofer`、App Group `group.com.paopaolabs.location-spoofer` 及原有 entitlements。
|
||||
- 免费 Apple ID 签名通常只有 7 天有效期,到期后需要重新签名安装。
|
||||
|
||||
<!-- commit-range: v1.0.6..v1.0.7 -->
|
||||
+8
-2
@@ -4,12 +4,18 @@ options:
|
||||
deploymentTarget:
|
||||
iOS: "15.0"
|
||||
createIntermediateGroups: true
|
||||
developmentLanguage: zh-Hans
|
||||
|
||||
knownRegions:
|
||||
- zh-Hans
|
||||
- en
|
||||
- Base
|
||||
|
||||
settings:
|
||||
base:
|
||||
SWIFT_VERSION: "5.9"
|
||||
MARKETING_VERSION: "1.0.6"
|
||||
CURRENT_PROJECT_VERSION: "7"
|
||||
MARKETING_VERSION: "1.0.7"
|
||||
CURRENT_PROJECT_VERSION: "8"
|
||||
CODE_SIGN_STYLE: Manual
|
||||
CODE_SIGNING_ALLOWED: "NO"
|
||||
CODE_SIGNING_REQUIRED: "NO"
|
||||
|
||||
+1
-1
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"latestVersion": "1.0.6",
|
||||
"latestVersion": "1.0.7",
|
||||
"minimumSupportedVersion": "1.0.0",
|
||||
"communityPromptClients": ["surge", "quantumultX", "loon", "stash", "egern"]
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user