fix(i18n): 完善国际化并迁移 WLOC 脚本

This commit is contained in:
xweiba
2026-09-11 02:39:00 +08:00
parent 908ac8f734
commit 05949703af
42 changed files with 2006 additions and 160 deletions
+32 -18
View File
@@ -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)
```
"""
}
}
+24 -17
View File
@@ -23,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 {
@@ -201,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
@@ -225,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)
}
}
@@ -246,22 +249,26 @@ struct RuntimeLogsView: View {
let active = response.success && response.latitude != nil && response.longitude != nil
testSucceeded = true
testResult = active ? String(localized: "第三方模块连接通过,已有坐标") : String(localized: "第三方模块连接通过,暂无坐标")
testMessage = """
======== 第三方代理连接检测 ========
模式: 测试模式
请求: wloc-settings/save?action=query
拦截响应: 有效 JSON
已保存坐标: \(active ? "" : "")
"""
testMessage = thirdPartyTestLog(active: active)
} catch {
testSucceeded = false
testResult = String(localized: "第三方模块连接失败")
testMessage = """
======== 第三方代理连接检测 ========
模式: 测试模式
请求: wloc-settings/save?action=query
结果: \(error.localizedDescription)
"""
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")
}
}
+34 -32
View File
@@ -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 {
@@ -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)
@@ -641,7 +641,7 @@ struct FirstSetupView: View {
private func setupScreenshot(
assetName: String,
title: String,
caption: LocalizedStringKey
caption: String
) -> some View {
if let image = UIImage(named: assetName) {
Button {
@@ -657,7 +657,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)
@@ -672,7 +672,11 @@ struct FirstSetupView: View {
)
}
.buttonStyle(.plain)
.accessibilityLabel(LocalizedStringKey(title))
.accessibilityLabel(
Text(LocalizedStringKey(title))
+ Text(": ")
+ Text(LocalizedStringKey(caption))
)
.accessibilityHint("轻点查看大图")
}
}
@@ -872,21 +876,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
}
}
@@ -895,11 +897,11 @@ 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)")
}
}
+5 -2
View File
@@ -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))
@@ -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
}
+1 -1
View File
@@ -182,5 +182,5 @@ struct ProxyCoordinateSnapshot: Equatable {
enum ProxyError: LocalizedError {
case startFailed
var errorDescription: String? { "Go proxy 启动失败" }
var errorDescription: String? { String(localized: "Go proxy 启动失败") }
}
+2 -2
View File
@@ -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)
}
}
+20 -20
View File
@@ -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) bytesWiFi 代理未配置")
log(String(localized: " ✗ 响应不匹配: HTTP \(statusCode), \(data.count) bytesWiFi 代理未配置"))
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)
}
}
}
+5 -4
View File
@@ -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)
+3 -4
View File
@@ -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://www.icloud.com/shortcuts/a82717d8fdad4e6280866fcf911173f7 清理恢复位置 https://www.icloud.com/shortcuts/f42632d406504f24a2cd163af4fe012f | 选点页面: https://wloc-pages.pages.dev/
#!author=xweiba
#!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.7
^https?:\/\/gs-loc(-cn)?\.apple\.com\/wloc-settings\/(save|version) 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.7
[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
+19
View File
@@ -0,0 +1,19 @@
#!name=Apple WLOC 定位修改
#!desc=修改 Apple 网络定位返回坐标 | 快捷指令(推荐): 设置地理位置 https://www.icloud.com/shortcuts/a82717d8fdad4e6280866fcf911173f7 清理恢复位置 https://www.icloud.com/shortcuts/f42632d406504f24a2cd163af4fe012f
#!author=xweiba
#!homepage=https://github.com/xweiba/location-spoofer
#!openUrl=https://wloc-pages.pages.dev/
[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.7, 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|version) script-path=https://gh-proxy.org/https://raw.githubusercontent.com/xweiba/location-spoofer/main/ThirdParty/WlocScripts/dist/v1/wloc-settings.js?v=1.0.7, 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 网络定位返回坐标 (Shadowrocket 小火箭) | 快捷指令(推荐): 设置地理位置 https://www.icloud.com/shortcuts/a82717d8fdad4e6280866fcf911173f7 清理恢复位置 https://www.icloud.com/shortcuts/f42632d406504f24a2cd163af4fe012f | 选点页面: https://wloc-pages.pages.dev/
#!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.7,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|version),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.7
[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://www.icloud.com/shortcuts/a82717d8fdad4e6280866fcf911173f7 清理恢复位置 https://www.icloud.com/shortcuts/f42632d406504f24a2cd163af4fe012f | 选点页面: https://wloc-pages.pages.dev/
#!author=xweiba
#!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.7, argument=longitude={{{经度}}}&latitude={{{纬度}}}&accuracy={{{精度}}}&randomRadius={{{扰动半径}}}&logLevel={{{日志级别}}}
WLOC Settings = type=http-request, pattern="^https?:\/\/gs-loc(-cn)?\.apple\.com\/wloc-settings\/(save|version)", 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.7
[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,35 @@
name: Apple WLOC 定位修改
desc: "修改 Apple 网络定位返回坐标 | 快捷指令(推荐): 设置地理位置 https://www.icloud.com/shortcuts/a82717d8fdad4e6280866fcf911173f7 清理恢复位置 https://www.icloud.com/shortcuts/f42632d406504f24a2cd163af4fe012f | 选点页面: https://wloc-pages.pages.dev/"
author: xweiba
homepage: https://github.com/xweiba/location-spoofer
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|version)
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.7
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.7
interval: 86400
+368 -1
View File
@@ -142,6 +142,12 @@
"第三方模块连接通过,已有坐标" = "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";
@@ -186,7 +192,7 @@
"去提交" = "Submit";
"复制模板" = "Copy Template";
"不再提示" = "Don't Show Again";
"第三方客户端" = "third-party client";
"第三方客户端" = "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.";
@@ -330,6 +336,367 @@
"第三方代理清除坐标失败" = "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 bytesWiFi 代理未配置" = " ✗ 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";
+2 -2
View File
@@ -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)")
}
}
}
+2 -2
View File
@@ -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: "无法启动本地证书服务")
}
}
}
+28 -3
View File
@@ -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)
}
}
}
+15 -16
View File
@@ -170,7 +170,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 +206,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.7"
case shadowrocket
case surge
case quantumultX
@@ -239,21 +245,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? {
@@ -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,19 @@ 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"
)
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 +147,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")
+58
View File
@@ -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"
+20 -5
View File
@@ -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,8 +37,23 @@ 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.7' \
|| 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"
@@ -88,7 +103,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"
@@ -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.6"
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"
+6
View File
@@ -0,0 +1,6 @@
# Third-Party Notices
The generated proxy scripts bundle:
- `pako` 2.1.0, Copyright (C) 2014-2017 Vitaly Puzrin and contributors, MIT License.
- `esbuild` is used only as a development/build dependency and is distributed under the MIT License.
+19
View File
@@ -0,0 +1,19 @@
import { build } from "esbuild";
import { mkdir } from "node:fs/promises";
await mkdir(new URL("./dist/v1/", import.meta.url), { recursive: true });
for (const [entry, outfile] of [
["src/response-entry.js", "dist/v1/wloc.js"],
["src/settings-entry.js", "dist/v1/wloc-settings.js"]
]) {
await build({
entryPoints: [entry],
outfile,
bundle: true,
format: "iife",
target: ["es2017"],
minify: true,
legalComments: "eof"
});
}
+1
View File
@@ -0,0 +1 @@
(()=>{var y="1.0.0";var m=["wifi","cellTower","arpc","marker","synthetic","bare","motionSimulation"],c="locationSpoofer.settings.v1";function u(){return typeof $task!="undefined"?"quantumultX":typeof $loon!="undefined"?"loon":typeof $rocket!="undefined"?"shadowrocket":typeof Egern!="undefined"?"egern":typeof $environment!="undefined"&&$environment["stash-version"]?"stash":typeof $environment!="undefined"&&$environment["surge-version"]?"surge":"unknown"}function h(e){let t=u()==="quantumultX"?$prefs.valueForKey(e):$persistentStore.read(e);if(!t)return null;try{return JSON.parse(t)}catch(n){return null}}function l(e,t){let n=t==null?"":JSON.stringify(t);return u()==="quantumultX"?$prefs.setValueForKey(n,e):$persistentStore.write(n,e)}function i(e){let t={status:200,headers:{"Content-Type":"application/json; charset=utf-8"},body:JSON.stringify(e)};u()==="quantumultX"?$done(Object.assign({},t,{status:"HTTP/1.1 200 OK"})):u()==="stash"?$done(t):$done({response:t})}function g(e){let t=e.split("?")[1]||"",n={};return t.split("&").forEach(s=>{if(!s)return;let o=s.indexOf("="),f=o<0?s:s.slice(0,o),d=o<0?"":s.slice(o+1),a=f,p=d;try{a=decodeURIComponent(f.replace(/\+/g," "))}catch($){}try{p=decodeURIComponent(d.replace(/\+/g," "))}catch($){}Object.prototype.hasOwnProperty.call(n,a)||(n[a]=p)}),n}function O(e){let t=e.split("?")[0],n=t.indexOf("://");if(n<0)return t;let s=t.indexOf("/",n+3);return s<0?"/":t.slice(s)}var b=typeof $request=="undefined"?"":$request.url||"",E=O(b),r=g(b);if(E==="/wloc-settings/version")i({success:!0,moduleVersion:y,protocolVersion:1,capabilities:m});else if(r.action==="query"){let e=h(c);i(e&&e.enabled?{success:!0,longitude:e.longitude,latitude:e.latitude,accuracy:e.accuracy,motionSimulationEnabled:e.motionSimulationEnabled===!0}:{success:!1,error:"\u65E0\u5DF2\u4FDD\u5B58\u7684\u5750\u6807"})}else if(r.action==="clear")l(c,null),i({success:!0});else{let e=Number(r.lon!=null?r.lon:r.longitude),t=Number(r.lat!=null?r.lat:r.latitude),n=Number(r.acc!=null?r.acc:r.accuracy!=null?r.accuracy:25);if(!Number.isFinite(e)||!Number.isFinite(t))i({success:!1,error:"\u7F3A\u5C11 lon/lat \u53C2\u6570"});else{let s={enabled:!0,longitude:e,latitude:t,accuracy:n,motionSimulationEnabled:r.motion==="1"},o=l(c,s);i(o?{success:!0,longitude:e,latitude:t,accuracy:n,motionSimulationEnabled:s.motionSimulationEnabled}:{success:!1,error:"\u4FDD\u5B58\u914D\u7F6E\u5931\u8D25"})}}})();
File diff suppressed because one or more lines are too long
+11
View File
@@ -0,0 +1,11 @@
#!name=Apple WLOC 定位修改
#!desc=Location Spoofer 第三方代理模块
#!author=xweiba
#!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.7
^https?:\/\/gs-loc(-cn)?\.apple\.com\/wloc-settings\/(save|version) url script-echo-response https://raw.githubusercontent.com/xweiba/location-spoofer/main/ThirdParty/WlocScripts/dist/v1/wloc-settings.js?v=1.0.7
[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
+18
View File
@@ -0,0 +1,18 @@
#!name=Apple WLOC 定位修改
#!desc=Location Spoofer 第三方代理模块
#!author=xweiba
#!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.7, 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|version) script-path=https://raw.githubusercontent.com/xweiba/location-spoofer/main/ThirdParty/WlocScripts/dist/v1/wloc-settings.js?v=1.0.7, 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
+13
View File
@@ -0,0 +1,13 @@
#!name=Apple WLOC 定位修改
#!desc=修改 Apple 网络定位返回坐标 (Shadowrocket 小火箭) | 快捷指令(推荐): 设置地理位置 https://www.icloud.com/shortcuts/a82717d8fdad4e6280866fcf911173f7 清理恢复位置 https://www.icloud.com/shortcuts/f42632d406504f24a2cd163af4fe012f | 选点页面: https://wloc-pages.pages.dev/
#!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.7,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|version),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.7
[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
+14
View File
@@ -0,0 +1,14 @@
#!name=Apple WLOC 定位修改
#!desc=Location Spoofer 第三方代理模块
#!author=xweiba
#!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.7, argument=longitude={{{经度}}}&latitude={{{纬度}}}&accuracy={{{精度}}}&randomRadius={{{扰动半径}}}&logLevel={{{日志级别}}}
WLOC Settings = type=http-request, pattern="^https?:\/\/gs-loc(-cn)?\.apple\.com\/wloc-settings\/(save|version)", 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.7
[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
+35
View File
@@ -0,0 +1,35 @@
name: Apple WLOC 定位修改
desc: Location Spoofer 第三方代理模块
author: xweiba
homepage: https://github.com/xweiba/location-spoofer
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|version)
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.7
interval: 86400
WLOC.Settings:
url: https://raw.githubusercontent.com/xweiba/location-spoofer/main/ThirdParty/WlocScripts/dist/v1/wloc-settings.js?v=1.0.7
interval: 86400
+508
View File
@@ -0,0 +1,508 @@
{
"name": "@location-spoofer/wloc-scripts",
"version": "1.0.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "@location-spoofer/wloc-scripts",
"version": "1.0.0",
"dependencies": {
"pako": "2.1.0"
},
"devDependencies": {
"esbuild": "0.25.8"
}
},
"node_modules/@esbuild/aix-ppc64": {
"version": "0.25.8",
"resolved": "https://registry.npmmirror.com/@esbuild/aix-ppc64/-/aix-ppc64-0.25.8.tgz",
"integrity": "sha512-urAvrUedIqEiFR3FYSLTWQgLu5tb+m0qZw0NBEasUeo6wuqatkMDaRT+1uABiGXEu5vqgPd7FGE1BhsAIy9QVA==",
"cpu": [
"ppc64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"aix"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/android-arm": {
"version": "0.25.8",
"resolved": "https://registry.npmmirror.com/@esbuild/android-arm/-/android-arm-0.25.8.tgz",
"integrity": "sha512-RONsAvGCz5oWyePVnLdZY/HHwA++nxYWIX1atInlaW6SEkwq6XkP3+cb825EUcRs5Vss/lGh/2YxAb5xqc07Uw==",
"cpu": [
"arm"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"android"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/android-arm64": {
"version": "0.25.8",
"resolved": "https://registry.npmmirror.com/@esbuild/android-arm64/-/android-arm64-0.25.8.tgz",
"integrity": "sha512-OD3p7LYzWpLhZEyATcTSJ67qB5D+20vbtr6vHlHWSQYhKtzUYrETuWThmzFpZtFsBIxRvhO07+UgVA9m0i/O1w==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"android"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/android-x64": {
"version": "0.25.8",
"resolved": "https://registry.npmmirror.com/@esbuild/android-x64/-/android-x64-0.25.8.tgz",
"integrity": "sha512-yJAVPklM5+4+9dTeKwHOaA+LQkmrKFX96BM0A/2zQrbS6ENCmxc4OVoBs5dPkCCak2roAD+jKCdnmOqKszPkjA==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"android"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/darwin-arm64": {
"version": "0.25.8",
"resolved": "https://registry.npmmirror.com/@esbuild/darwin-arm64/-/darwin-arm64-0.25.8.tgz",
"integrity": "sha512-Jw0mxgIaYX6R8ODrdkLLPwBqHTtYHJSmzzd+QeytSugzQ0Vg4c5rDky5VgkoowbZQahCbsv1rT1KW72MPIkevw==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"darwin"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/darwin-x64": {
"version": "0.25.8",
"resolved": "https://registry.npmmirror.com/@esbuild/darwin-x64/-/darwin-x64-0.25.8.tgz",
"integrity": "sha512-Vh2gLxxHnuoQ+GjPNvDSDRpoBCUzY4Pu0kBqMBDlK4fuWbKgGtmDIeEC081xi26PPjn+1tct+Bh8FjyLlw1Zlg==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"darwin"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/freebsd-arm64": {
"version": "0.25.8",
"resolved": "https://registry.npmmirror.com/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.8.tgz",
"integrity": "sha512-YPJ7hDQ9DnNe5vxOm6jaie9QsTwcKedPvizTVlqWG9GBSq+BuyWEDazlGaDTC5NGU4QJd666V0yqCBL2oWKPfA==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"freebsd"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/freebsd-x64": {
"version": "0.25.8",
"resolved": "https://registry.npmmirror.com/@esbuild/freebsd-x64/-/freebsd-x64-0.25.8.tgz",
"integrity": "sha512-MmaEXxQRdXNFsRN/KcIimLnSJrk2r5H8v+WVafRWz5xdSVmWLoITZQXcgehI2ZE6gioE6HirAEToM/RvFBeuhw==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"freebsd"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/linux-arm": {
"version": "0.25.8",
"resolved": "https://registry.npmmirror.com/@esbuild/linux-arm/-/linux-arm-0.25.8.tgz",
"integrity": "sha512-FuzEP9BixzZohl1kLf76KEVOsxtIBFwCaLupVuk4eFVnOZfU+Wsn+x5Ryam7nILV2pkq2TqQM9EZPsOBuMC+kg==",
"cpu": [
"arm"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/linux-arm64": {
"version": "0.25.8",
"resolved": "https://registry.npmmirror.com/@esbuild/linux-arm64/-/linux-arm64-0.25.8.tgz",
"integrity": "sha512-WIgg00ARWv/uYLU7lsuDK00d/hHSfES5BzdWAdAig1ioV5kaFNrtK8EqGcUBJhYqotlUByUKz5Qo6u8tt7iD/w==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/linux-ia32": {
"version": "0.25.8",
"resolved": "https://registry.npmmirror.com/@esbuild/linux-ia32/-/linux-ia32-0.25.8.tgz",
"integrity": "sha512-A1D9YzRX1i+1AJZuFFUMP1E9fMaYY+GnSQil9Tlw05utlE86EKTUA7RjwHDkEitmLYiFsRd9HwKBPEftNdBfjg==",
"cpu": [
"ia32"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/linux-loong64": {
"version": "0.25.8",
"resolved": "https://registry.npmmirror.com/@esbuild/linux-loong64/-/linux-loong64-0.25.8.tgz",
"integrity": "sha512-O7k1J/dwHkY1RMVvglFHl1HzutGEFFZ3kNiDMSOyUrB7WcoHGf96Sh+64nTRT26l3GMbCW01Ekh/ThKM5iI7hQ==",
"cpu": [
"loong64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/linux-mips64el": {
"version": "0.25.8",
"resolved": "https://registry.npmmirror.com/@esbuild/linux-mips64el/-/linux-mips64el-0.25.8.tgz",
"integrity": "sha512-uv+dqfRazte3BzfMp8PAQXmdGHQt2oC/y2ovwpTteqrMx2lwaksiFZ/bdkXJC19ttTvNXBuWH53zy/aTj1FgGw==",
"cpu": [
"mips64el"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/linux-ppc64": {
"version": "0.25.8",
"resolved": "https://registry.npmmirror.com/@esbuild/linux-ppc64/-/linux-ppc64-0.25.8.tgz",
"integrity": "sha512-GyG0KcMi1GBavP5JgAkkstMGyMholMDybAf8wF5A70CALlDM2p/f7YFE7H92eDeH/VBtFJA5MT4nRPDGg4JuzQ==",
"cpu": [
"ppc64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/linux-riscv64": {
"version": "0.25.8",
"resolved": "https://registry.npmmirror.com/@esbuild/linux-riscv64/-/linux-riscv64-0.25.8.tgz",
"integrity": "sha512-rAqDYFv3yzMrq7GIcen3XP7TUEG/4LK86LUPMIz6RT8A6pRIDn0sDcvjudVZBiiTcZCY9y2SgYX2lgK3AF+1eg==",
"cpu": [
"riscv64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/linux-s390x": {
"version": "0.25.8",
"resolved": "https://registry.npmmirror.com/@esbuild/linux-s390x/-/linux-s390x-0.25.8.tgz",
"integrity": "sha512-Xutvh6VjlbcHpsIIbwY8GVRbwoviWT19tFhgdA7DlenLGC/mbc3lBoVb7jxj9Z+eyGqvcnSyIltYUrkKzWqSvg==",
"cpu": [
"s390x"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/linux-x64": {
"version": "0.25.8",
"resolved": "https://registry.npmmirror.com/@esbuild/linux-x64/-/linux-x64-0.25.8.tgz",
"integrity": "sha512-ASFQhgY4ElXh3nDcOMTkQero4b1lgubskNlhIfJrsH5OKZXDpUAKBlNS0Kx81jwOBp+HCeZqmoJuihTv57/jvQ==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/netbsd-arm64": {
"version": "0.25.8",
"resolved": "https://registry.npmmirror.com/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.8.tgz",
"integrity": "sha512-d1KfruIeohqAi6SA+gENMuObDbEjn22olAR7egqnkCD9DGBG0wsEARotkLgXDu6c4ncgWTZJtN5vcgxzWRMzcw==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"netbsd"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/netbsd-x64": {
"version": "0.25.8",
"resolved": "https://registry.npmmirror.com/@esbuild/netbsd-x64/-/netbsd-x64-0.25.8.tgz",
"integrity": "sha512-nVDCkrvx2ua+XQNyfrujIG38+YGyuy2Ru9kKVNyh5jAys6n+l44tTtToqHjino2My8VAY6Lw9H7RI73XFi66Cg==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"netbsd"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/openbsd-arm64": {
"version": "0.25.8",
"resolved": "https://registry.npmmirror.com/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.8.tgz",
"integrity": "sha512-j8HgrDuSJFAujkivSMSfPQSAa5Fxbvk4rgNAS5i3K+r8s1X0p1uOO2Hl2xNsGFppOeHOLAVgYwDVlmxhq5h+SQ==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"openbsd"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/openbsd-x64": {
"version": "0.25.8",
"resolved": "https://registry.npmmirror.com/@esbuild/openbsd-x64/-/openbsd-x64-0.25.8.tgz",
"integrity": "sha512-1h8MUAwa0VhNCDp6Af0HToI2TJFAn1uqT9Al6DJVzdIBAd21m/G0Yfc77KDM3uF3T/YaOgQq3qTJHPbTOInaIQ==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"openbsd"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/openharmony-arm64": {
"version": "0.25.8",
"resolved": "https://registry.npmmirror.com/@esbuild/openharmony-arm64/-/openharmony-arm64-0.25.8.tgz",
"integrity": "sha512-r2nVa5SIK9tSWd0kJd9HCffnDHKchTGikb//9c7HX+r+wHYCpQrSgxhlY6KWV1nFo1l4KFbsMlHk+L6fekLsUg==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"openharmony"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/sunos-x64": {
"version": "0.25.8",
"resolved": "https://registry.npmmirror.com/@esbuild/sunos-x64/-/sunos-x64-0.25.8.tgz",
"integrity": "sha512-zUlaP2S12YhQ2UzUfcCuMDHQFJyKABkAjvO5YSndMiIkMimPmxA+BYSBikWgsRpvyxuRnow4nS5NPnf9fpv41w==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"sunos"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/win32-arm64": {
"version": "0.25.8",
"resolved": "https://registry.npmmirror.com/@esbuild/win32-arm64/-/win32-arm64-0.25.8.tgz",
"integrity": "sha512-YEGFFWESlPva8hGL+zvj2z/SaK+pH0SwOM0Nc/d+rVnW7GSTFlLBGzZkuSU9kFIGIo8q9X3ucpZhu8PDN5A2sQ==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"win32"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/win32-ia32": {
"version": "0.25.8",
"resolved": "https://registry.npmmirror.com/@esbuild/win32-ia32/-/win32-ia32-0.25.8.tgz",
"integrity": "sha512-hiGgGC6KZ5LZz58OL/+qVVoZiuZlUYlYHNAmczOm7bs2oE1XriPFi5ZHHrS8ACpV5EjySrnoCKmcbQMN+ojnHg==",
"cpu": [
"ia32"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"win32"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/win32-x64": {
"version": "0.25.8",
"resolved": "https://registry.npmmirror.com/@esbuild/win32-x64/-/win32-x64-0.25.8.tgz",
"integrity": "sha512-cn3Yr7+OaaZq1c+2pe+8yxC8E144SReCQjN6/2ynubzYjvyqZjTXfQJpAcQpsdJq3My7XADANiYGHoFC69pLQw==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"win32"
],
"engines": {
"node": ">=18"
}
},
"node_modules/esbuild": {
"version": "0.25.8",
"resolved": "https://registry.npmmirror.com/esbuild/-/esbuild-0.25.8.tgz",
"integrity": "sha512-vVC0USHGtMi8+R4Kz8rt6JhEWLxsv9Rnu/lGYbPR8u47B+DCBksq9JarW0zOO7bs37hyOK1l2/oqtbciutL5+Q==",
"dev": true,
"hasInstallScript": true,
"license": "MIT",
"bin": {
"esbuild": "bin/esbuild"
},
"engines": {
"node": ">=18"
},
"optionalDependencies": {
"@esbuild/aix-ppc64": "0.25.8",
"@esbuild/android-arm": "0.25.8",
"@esbuild/android-arm64": "0.25.8",
"@esbuild/android-x64": "0.25.8",
"@esbuild/darwin-arm64": "0.25.8",
"@esbuild/darwin-x64": "0.25.8",
"@esbuild/freebsd-arm64": "0.25.8",
"@esbuild/freebsd-x64": "0.25.8",
"@esbuild/linux-arm": "0.25.8",
"@esbuild/linux-arm64": "0.25.8",
"@esbuild/linux-ia32": "0.25.8",
"@esbuild/linux-loong64": "0.25.8",
"@esbuild/linux-mips64el": "0.25.8",
"@esbuild/linux-ppc64": "0.25.8",
"@esbuild/linux-riscv64": "0.25.8",
"@esbuild/linux-s390x": "0.25.8",
"@esbuild/linux-x64": "0.25.8",
"@esbuild/netbsd-arm64": "0.25.8",
"@esbuild/netbsd-x64": "0.25.8",
"@esbuild/openbsd-arm64": "0.25.8",
"@esbuild/openbsd-x64": "0.25.8",
"@esbuild/openharmony-arm64": "0.25.8",
"@esbuild/sunos-x64": "0.25.8",
"@esbuild/win32-arm64": "0.25.8",
"@esbuild/win32-ia32": "0.25.8",
"@esbuild/win32-x64": "0.25.8"
}
},
"node_modules/pako": {
"version": "2.1.0",
"resolved": "https://registry.npmmirror.com/pako/-/pako-2.1.0.tgz",
"integrity": "sha512-w+eufiZ1WuJYgPXbV/PO3NCMEc3xqylkKHzp8bxp1uW4qaSNQUkwmLLEc3kKsfz8lpV1F8Ht3U1Cm+9Srog2ug==",
"license": "(MIT AND Zlib)"
}
}
}
+16
View File
@@ -0,0 +1,16 @@
{
"name": "@location-spoofer/wloc-scripts",
"version": "1.0.0",
"private": true,
"type": "module",
"scripts": {
"build": "node build.mjs",
"test": "node --test"
},
"dependencies": {
"pako": "2.1.0"
},
"devDependencies": {
"esbuild": "0.25.8"
}
}
+263
View File
@@ -0,0 +1,263 @@
export const MOTION_ACTIVITY_TYPE = 63;
export const MOTION_ACTIVITY_CONFIDENCE = 467;
export const WLOC_MARKER = Uint8Array.from([0, 0, 0, 1, 0, 0]);
const UINT32_RANGE = 0x100000000;
const concat = (...parts) => {
const length = parts.reduce((sum, part) => sum + part.length, 0);
const out = new Uint8Array(length);
let offset = 0;
for (const part of parts) {
out.set(part, offset);
offset += part.length;
}
return out;
};
const equal = (left, right) =>
left.length === right.length && left.every((value, index) => value === right[index]);
function readVarint(data, offset) {
let value = 0;
let multiplier = 1;
for (let index = 0; index < 10 && offset + index < data.length; index += 1) {
const byte = data[offset + index];
const chunk = byte & 0x7f;
if (value !== null && chunk <= Math.floor((Number.MAX_SAFE_INTEGER - value) / multiplier)) {
value += chunk * multiplier;
} else {
value = null;
}
if ((byte & 0x80) === 0) return { value, next: offset + index + 1 };
multiplier *= 0x80;
}
throw new Error("invalid varint");
}
function writeVarint(input) {
const value = Math.trunc(Number(input));
if (!Number.isSafeInteger(value)) throw new Error("varint value is not a safe integer");
let low = value >>> 0;
let high = Math.floor(value / UINT32_RANGE) >>> 0;
const bytes = [];
do {
const byte = low & 0x7f;
low = ((low >>> 7) | ((high & 0x7f) << 25)) >>> 0;
high >>>= 7;
const hasMore = high !== 0 || low !== 0;
bytes.push(byte | (hasMore ? 0x80 : 0));
} while (high !== 0 || low !== 0);
return Uint8Array.from(bytes);
}
const writeTag = (number, wireType) => writeVarint((number << 3) | wireType);
function writeLengthDelimited(number, value) {
return concat(writeTag(number, 2), writeVarint(value.length), value);
}
export function parseFields(data) {
const fields = [];
let offset = 0;
while (offset < data.length) {
const start = offset;
const tag = readVarint(data, offset);
offset = tag.next;
if (!Number.isSafeInteger(tag.value)) throw new Error("protobuf tag is too large");
const number = Math.floor(tag.value / 8);
const wireType = tag.value & 7;
if (number === 0) throw new Error("invalid protobuf field 0");
let value;
if (wireType === 0) {
const item = readVarint(data, offset);
value = data.slice(offset, item.next);
offset = item.next;
} else if (wireType === 1) {
if (offset + 8 > data.length) throw new Error("truncated fixed64");
value = data.slice(offset, offset + 8);
offset += 8;
} else if (wireType === 2) {
const length = readVarint(data, offset);
offset = length.next;
const size = length.value;
if (!Number.isSafeInteger(size) || offset + size > data.length) {
throw new Error("truncated length-delimited field");
}
value = data.slice(offset, offset + size);
offset += size;
} else if (wireType === 5) {
if (offset + 4 > data.length) throw new Error("truncated fixed32");
value = data.slice(offset, offset + 4);
offset += 4;
} else {
throw new Error(`unsupported wire type ${wireType}`);
}
fields.push({ number, wireType, value, raw: data.slice(start, offset) });
}
return fields;
}
function patchLocation(data, config, stats) {
const fields = parseFields(data);
if (!fields.some((field) => field.number === 1 && field.wireType === 0) ||
!fields.some((field) => field.number === 2 && field.wireType === 0)) {
return data;
}
let hasMotionType = false;
let hasMotionConfidence = false;
const parts = fields.map((field) => {
if (field.wireType !== 0) return field.raw;
if (field.number === 1) return concat(writeTag(1, 0), writeVarint(Math.round(config.latitude * 1e8)));
if (field.number === 2) return concat(writeTag(2, 0), writeVarint(Math.round(config.longitude * 1e8)));
if (field.number === 3) return concat(writeTag(3, 0), writeVarint(config.accuracy));
if (config.motionSimulationEnabled && field.number === 11) {
hasMotionType = true;
return concat(writeTag(11, 0), writeVarint(MOTION_ACTIVITY_TYPE));
}
if (config.motionSimulationEnabled && field.number === 12) {
hasMotionConfidence = true;
return concat(writeTag(12, 0), writeVarint(MOTION_ACTIVITY_CONFIDENCE));
}
return field.raw;
});
if (config.motionSimulationEnabled && !hasMotionType) {
parts.push(concat(writeTag(11, 0), writeVarint(MOTION_ACTIVITY_TYPE)));
}
if (config.motionSimulationEnabled && !hasMotionConfidence) {
parts.push(concat(writeTag(12, 0), writeVarint(MOTION_ACTIVITY_CONFIDENCE)));
}
const out = concat(...parts);
if (!equal(out, data)) stats.locations += 1;
return out;
}
function patchWifi(data, config, stats) {
const fields = parseFields(data);
const hasMac = fields.some((field) =>
field.number === 1 && field.wireType === 2 &&
/^[0-9a-fA-F]{1,2}(:[0-9a-fA-F]{1,2}){5}$/.test(
Array.from(field.value, (byte) => String.fromCharCode(byte)).join("")
)
);
if (!hasMac) return data;
let changed = false;
const parts = fields.map((field) => {
if (field.number !== 2 || field.wireType !== 2) return field.raw;
const value = patchLocation(field.value, config, stats);
changed ||= !equal(value, field.value);
return writeLengthDelimited(2, value);
});
if (changed) stats.wifi += 1;
return concat(...parts);
}
function patchCell(data, config, stats) {
let changed = false;
const parts = parseFields(data).map((field) => {
if (field.number !== 5 || field.wireType !== 2) return field.raw;
const value = patchLocation(field.value, config, stats);
changed ||= !equal(value, field.value);
return writeLengthDelimited(5, value);
});
if (changed) stats.cell += 1;
return concat(...parts);
}
export function patchPayload(data, config, stats = { wifi: 0, cell: 0, locations: 0 }) {
const parts = parseFields(data).map((field) => {
if (field.number === 2 && field.wireType === 2) {
return writeLengthDelimited(2, patchWifi(field.value, config, stats));
}
if ((field.number === 22 || field.number === 24) && field.wireType === 2) {
return writeLengthDelimited(field.number, patchCell(field.value, config, stats));
}
return field.raw;
});
return { data: concat(...parts), stats };
}
const uint16 = (data, offset) => (data[offset] << 8) | data[offset + 1];
const uint32 = (data, offset) =>
((data[offset] * 0x1000000) + (data[offset + 1] << 16) +
(data[offset + 2] << 8) + data[offset + 3]) >>> 0;
const writeUint16 = (value) => Uint8Array.from([(value >>> 8) & 0xff, value & 0xff]);
const writeUint32 = (value) => Uint8Array.from([
(value >>> 24) & 0xff, (value >>> 16) & 0xff, (value >>> 8) & 0xff, value & 0xff
]);
function findBytes(data, marker) {
outer: for (let offset = 0; offset <= data.length - marker.length; offset += 1) {
for (let index = 0; index < marker.length; index += 1) {
if (data[offset + index] !== marker[index]) continue outer;
}
return offset;
}
return -1;
}
function patchARPC(body, config) {
if (body.length < 2) throw new Error("short ARPC");
let offset = 2;
for (let index = 0; index < 3; index += 1) {
if (offset + 2 > body.length) throw new Error("truncated ARPC string");
offset += 2 + uint16(body, offset);
}
const lengthOffset = offset + 4;
const payloadOffset = lengthOffset + 4;
if (payloadOffset > body.length) throw new Error("truncated ARPC header");
const length = uint32(body, lengthOffset);
if (!length || payloadOffset + length > body.length) throw new Error("invalid ARPC length");
const patched = patchPayload(body.slice(payloadOffset, payloadOffset + length), config);
if (equal(patched.data, body.slice(payloadOffset, payloadOffset + length))) throw new Error("unchanged ARPC");
return { data: concat(body.slice(0, lengthOffset), writeUint32(patched.data.length),
patched.data, body.slice(payloadOffset + length)), stats: patched.stats };
}
function patchMarker(body, config) {
const markerOffset = findBytes(body, WLOC_MARKER);
if (markerOffset < 0) throw new Error("marker not found");
const lengthOffset = markerOffset + WLOC_MARKER.length;
const payloadOffset = lengthOffset + 2;
const length = uint16(body, lengthOffset);
if (!length || payloadOffset + length > body.length) throw new Error("invalid marker length");
const patched = patchPayload(body.slice(payloadOffset, payloadOffset + length), config);
if (patched.data.length > 65535 || equal(patched.data, body.slice(payloadOffset, payloadOffset + length))) {
throw new Error("unchanged marker");
}
return { data: concat(body.slice(0, lengthOffset), writeUint16(patched.data.length),
patched.data, body.slice(payloadOffset + length)), stats: patched.stats };
}
function patchSynthetic(body, offset, config) {
if (offset + 10 > body.length) throw new Error("short frame");
const length = uint16(body, offset + 8);
if (!length || offset + 10 + length > body.length) throw new Error("invalid frame");
const patched = patchPayload(body.slice(offset + 10, offset + 10 + length), config);
if (patched.data.length > 65535 || equal(patched.data, body.slice(offset + 10, offset + 10 + length))) {
throw new Error("unchanged frame");
}
return { data: concat(body.slice(0, offset + 8), writeUint16(patched.data.length),
patched.data, body.slice(offset + 10 + length)), stats: patched.stats };
}
export function patchWlocBody(body, config) {
for (const patcher of [patchARPC, patchMarker]) {
try { return patcher(body, config); } catch {}
}
const offsets = [...new Set([0, 2, 4, 6, 8, 10, 12, 14, 16,
...Array.from({ length: Math.min(96, Math.max(0, body.length - 10)) + 1 }, (_, index) => index)])];
for (const offset of offsets) {
try { return patchSynthetic(body, offset, config); } catch {}
}
for (let offset = 0; offset <= Math.min(256, body.length); offset += 1) {
try {
const patched = patchPayload(body.slice(offset), config);
if (!equal(patched.data, body.slice(offset))) {
return { data: concat(body.slice(0, offset), patched.data), stats: patched.stats };
}
} catch {}
}
throw new Error("no patchable wloc payload found");
}
export const internals = { concat, writeVarint, writeTag, writeLengthDelimited, equal };
+26
View File
@@ -0,0 +1,26 @@
import { ungzip } from "pako";
import { patchWlocBody } from "./core.js";
import {
STORAGE_KEY, finishBinary, finishPassthrough, readPersistent, responseBytes
} from "./runtime.js";
try {
const settings = readPersistent(STORAGE_KEY);
const input = responseBytes();
if (!settings || !settings.enabled || !input.length) {
finishPassthrough();
} else {
const isGzip = input.length >= 2 && input[0] === 0x1f && input[1] === 0x8b;
const body = isGzip ? ungzip(input) : input;
const patched = patchWlocBody(body, {
latitude: Number(settings.latitude),
longitude: Number(settings.longitude),
accuracy: Number(settings.accuracy != null ? settings.accuracy : 25),
motionSimulationEnabled: settings.motionSimulationEnabled === true
});
finishBinary(patched.data);
}
} catch (error) {
console.log(`[Location Spoofer] ${error && error.message ? error.message : error}`);
finishPassthrough();
}
+109
View File
@@ -0,0 +1,109 @@
export const MODULE_VERSION = "1.0.0";
export const PROTOCOL_VERSION = 1;
export const CAPABILITIES = [
"wifi", "cellTower", "arpc", "marker", "synthetic", "bare", "motionSimulation"
];
export const STORAGE_KEY = "locationSpoofer.settings.v1";
export function environment() {
if (typeof $task !== "undefined") return "quantumultX";
if (typeof $loon !== "undefined") return "loon";
if (typeof $rocket !== "undefined") return "shadowrocket";
if (typeof Egern !== "undefined") return "egern";
if (typeof $environment !== "undefined" && $environment["stash-version"]) return "stash";
if (typeof $environment !== "undefined" && $environment["surge-version"]) return "surge";
return "unknown";
}
export function readPersistent(key) {
const raw = environment() === "quantumultX"
? $prefs.valueForKey(key)
: $persistentStore.read(key);
if (!raw) return null;
try { return JSON.parse(raw); } catch { return null; }
}
export function writePersistent(key, value) {
const raw = value == null ? "" : JSON.stringify(value);
return environment() === "quantumultX"
? $prefs.setValueForKey(raw, key)
: $persistentStore.write(raw, key);
}
export function responseBytes() {
const response = typeof $response === "undefined" ? null : $response;
const value = response && response.bodyBytes != null ? response.bodyBytes : response && response.body;
if (value instanceof ArrayBuffer) return new Uint8Array(value);
if (ArrayBuffer.isView(value)) return new Uint8Array(value.buffer, value.byteOffset, value.byteLength);
if (typeof value === "string") {
return Uint8Array.from(value, (character) => character.charCodeAt(0) & 0xff);
}
return new Uint8Array();
}
function cleanHeaders(headers, length) {
const out = Object.assign({}, headers || {});
for (const name of ["Content-Encoding", "content-encoding", "Transfer-Encoding", "transfer-encoding"]) {
delete out[name];
}
out["Content-Length"] = String(length);
return out;
}
export function finishBinary(bytes) {
const response = typeof $response === "undefined" ? {} : $response;
const headers = cleanHeaders(response.headers, bytes.length);
const env = environment();
if (env === "quantumultX") {
delete headers["Content-Length"];
$done({ status: "HTTP/1.1 200 OK", headers, bodyBytes: bytes.buffer });
} else if (env === "stash") {
$done(Object.assign({}, response, { status: 200, headers, body: bytes }));
} else {
$done({ response: Object.assign({}, response, { status: 200, headers, body: bytes }) });
}
}
export function finishPassthrough() {
$done({});
}
export function finishJSON(value) {
const response = {
status: 200,
headers: { "Content-Type": "application/json; charset=utf-8" },
body: JSON.stringify(value)
};
if (environment() === "quantumultX") {
$done(Object.assign({}, response, { status: "HTTP/1.1 200 OK" }));
} else if (environment() === "stash") {
$done(response);
} else {
$done({ response });
}
}
export function queryParameters(url) {
const query = url.split("?")[1] || "";
const values = {};
query.split("&").forEach((item) => {
if (!item) return;
const separator = item.indexOf("=");
const rawKey = separator < 0 ? item : item.slice(0, separator);
const rawValue = separator < 0 ? "" : item.slice(separator + 1);
let key = rawKey;
let value = rawValue;
try { key = decodeURIComponent(rawKey.replace(/\+/g, " ")); } catch {}
try { value = decodeURIComponent(rawValue.replace(/\+/g, " ")); } catch {}
if (!Object.prototype.hasOwnProperty.call(values, key)) values[key] = value;
});
return values;
}
export function requestPath(url) {
const withoutQuery = url.split("?")[0];
const scheme = withoutQuery.indexOf("://");
if (scheme < 0) return withoutQuery;
const path = withoutQuery.indexOf("/", scheme + 3);
return path < 0 ? "/" : withoutQuery.slice(path);
}
+48
View File
@@ -0,0 +1,48 @@
import {
CAPABILITIES, MODULE_VERSION, PROTOCOL_VERSION, STORAGE_KEY,
finishJSON, queryParameters, readPersistent, requestPath, writePersistent
} from "./runtime.js";
const url = typeof $request === "undefined" ? "" : ($request.url || "");
const path = requestPath(url);
const parameters = queryParameters(url);
if (path === "/wloc-settings/version") {
finishJSON({
success: true,
moduleVersion: MODULE_VERSION,
protocolVersion: PROTOCOL_VERSION,
capabilities: CAPABILITIES
});
} else if (parameters.action === "query") {
const settings = readPersistent(STORAGE_KEY);
finishJSON(settings && settings.enabled
? { success: true, longitude: settings.longitude, latitude: settings.latitude,
accuracy: settings.accuracy, motionSimulationEnabled: settings.motionSimulationEnabled === true }
: { success: false, error: "无已保存的坐标" });
} else if (parameters.action === "clear") {
writePersistent(STORAGE_KEY, null);
finishJSON({ success: true });
} else {
const longitude = Number(parameters.lon != null ? parameters.lon : parameters.longitude);
const latitude = Number(parameters.lat != null ? parameters.lat : parameters.latitude);
const accuracy = Number(parameters.acc != null
? parameters.acc
: (parameters.accuracy != null ? parameters.accuracy : 25));
if (!Number.isFinite(longitude) || !Number.isFinite(latitude)) {
finishJSON({ success: false, error: "缺少 lon/lat 参数" });
} else {
const settings = {
enabled: true,
longitude,
latitude,
accuracy,
motionSimulationEnabled: parameters.motion === "1"
};
const success = writePersistent(STORAGE_KEY, settings);
finishJSON(success
? { success: true, longitude, latitude, accuracy,
motionSimulationEnabled: settings.motionSimulationEnabled }
: { success: false, error: "保存配置失败" });
}
}
@@ -0,0 +1,52 @@
import test from "node:test";
import assert from "node:assert/strict";
import { readFile } from "node:fs/promises";
import vm from "node:vm";
const bundles = [
new URL("../dist/v1/wloc.js", import.meta.url),
new URL("../dist/v1/wloc-settings.js", import.meta.url)
];
test("generated bundles avoid newer JavaScriptCore runtime requirements", async () => {
for (const bundle of bundles) {
const source = await readFile(bundle, "utf8");
for (const unsupported of [
/\bBigInt\b/,
/\bglobalThis\b/,
/\bURLSearchParams\b/,
/\bObject\.fromEntries\b/,
/\?\./,
/\?\?/
]) {
assert.equal(unsupported.test(source), false, `${bundle.pathname} contains ${unsupported}`);
}
}
});
test("settings bundle runs without modern URL and text globals", async () => {
const source = await readFile(bundles[1], "utf8");
let result;
const storage = new Map();
vm.runInNewContext(source, {
$rocket: {},
$request: {
url: "https://gs-loc.apple.com/wloc-settings/save?lon=121.1&lat=31.2&acc=25"
},
$persistentStore: {
read: (key) => storage.get(key) || null,
write: (value, key) => {
storage.set(key, value);
return true;
}
},
$done: (value) => { result = value; },
JSON,
Number,
Object,
String,
decodeURIComponent
});
assert.equal(JSON.parse(result.response.body).success, true);
});
+85
View File
@@ -0,0 +1,85 @@
import test from "node:test";
import assert from "node:assert/strict";
import {
MOTION_ACTIVITY_CONFIDENCE, MOTION_ACTIVITY_TYPE, internals, parseFields, patchWlocBody
} from "../src/core.js";
const { concat, writeVarint, writeTag, writeLengthDelimited } = internals;
function location(withMotion = false) {
const fields = [
concat(writeTag(1, 0), writeVarint(100)),
concat(writeTag(2, 0), writeVarint(200)),
concat(writeTag(3, 0), writeVarint(25))
];
if (withMotion) {
fields.push(concat(writeTag(11, 0), writeVarint(7)));
fields.push(concat(writeTag(12, 0), writeVarint(88)));
}
return concat(...fields);
}
function wifiPayload(value = location()) {
const device = concat(
writeLengthDelimited(1, new TextEncoder().encode("aa:bb:cc:dd:ee:ff")),
writeLengthDelimited(2, value)
);
return writeLengthDelimited(2, device);
}
function frame(payload) {
return concat(Uint8Array.from([0, 1, 0, 0, 0, 1, 0, 0]),
Uint8Array.from([payload.length >> 8, payload.length & 0xff]), payload);
}
const config = {
latitude: 31.230416,
longitude: 121.473701,
accuracy: 50,
motionSimulationEnabled: false
};
test("patches synthetic Wi-Fi response", () => {
const result = patchWlocBody(frame(wifiPayload()), config);
assert.equal(result.stats.wifi, 1);
assert.equal(result.stats.locations, 1);
});
test("preserves motion fields while disabled", () => {
const result = patchWlocBody(frame(wifiPayload(location(true))), config);
const payload = result.data.slice(10);
assert.ok(payload.includes(7));
assert.ok(payload.includes(88));
});
test("replaces motion fields while enabled", () => {
const result = patchWlocBody(frame(wifiPayload(location(true))), {
...config, motionSimulationEnabled: true
});
const root = parseFields(result.data.slice(10));
const device = parseFields(root[0].value);
const fields = parseFields(device.find((field) => field.number === 2).value);
const motionType = fields.find((field) => field.number === 11);
const motionConfidence = fields.find((field) => field.number === 12);
assert.deepEqual(motionType.value, writeVarint(MOTION_ACTIVITY_TYPE));
assert.deepEqual(motionConfidence.value, writeVarint(MOTION_ACTIVITY_CONFIDENCE));
});
test("patches CellTower fields 22 and 24", () => {
for (const number of [22, 24]) {
const cell = writeLengthDelimited(5, location());
const result = patchWlocBody(frame(writeLengthDelimited(number, cell)), config);
assert.equal(result.stats.cell, 1);
}
});
test("encodes signed int64 coordinates without BigInt", () => {
assert.deepEqual(
Array.from(writeVarint(-18_000_000_000)),
[128, 152, 247, 248, 188, 255, 255, 255, 255, 1]
);
assert.deepEqual(
Array.from(writeVarint(18_000_000_000)),
[128, 232, 136, 135, 67]
);
});
+46 -21
View File
@@ -1,20 +1,26 @@
# 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 WLOC scripts are maintained in this
repository. The original `Yu9191/wloc` repository was deleted, and its Raw
URLs now return HTTP 404, so it is retained only 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 generated scripts: `ThirdParty/WlocScripts/dist/v1/`
- Script source and tests: `ThirdParty/WlocScripts/src/` and
`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.7` to module subscription URLs, and every module uses
the same query parameter for its generated script URLs, to invalidate client
caches:
- default mirror:
`https://gh-proxy.org/https://raw.githubusercontent.com/xweiba/location-spoofer/main/Resources/ThirdPartyProxyModules/<file>?v=1.0.7`
- 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.7`
| Module file | Client |
|---|---|
@@ -24,17 +30,36 @@ 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.
The hosted files become downloadable from these URLs after this 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 the module `argument` config.
`wloc-settings.js` implements `wloc-settings/save` (query/clear/save) 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 `lon`/`lat`/`acc`. Motion-state simulation
(fields 11/12) is unavailable in third-party mode; it remains available in
App Mode through the built-in proxy.
## Provenance and maintenance
The response-rewrite approach and original module structure were derived from
`Yu9191/wloc`. The repository is no longer available, but its attribution is
preserved in module metadata and project acknowledgements. Bundled dependency
notices are recorded in `ThirdParty/WlocScripts/THIRD_PARTY_NOTICES.md`.
Build and test the scripts with:
```bash
cd ThirdParty/WlocScripts
npm ci
npm test
npm run build
```
If an authoritative, maintained upstream returns, compare its license,
protocol compatibility, tests, and release guarantees before considering a
switch away from the project-owned copies.