mirror of
https://github.com/xweiba/location-spoofer.git
synced 2026-09-27 00:51:55 +08:00
fix(i18n): 完善国际化并迁移 WLOC 脚本
This commit is contained in:
+32
-18
@@ -95,9 +95,10 @@ struct BugReportView: View {
|
|||||||
do {
|
do {
|
||||||
let response = try await thirdPartyProxy.query()
|
let response = try await thirdPartyProxy.query()
|
||||||
let active = response.success && response.latitude != nil && response.longitude != nil
|
let active = response.success && response.latitude != nil && response.longitude != nil
|
||||||
testLog = "第三方代理测试模式:模块连接成功;已保存坐标=\(active ? "是" : "否")"
|
let savedCoordinate = active ? String(localized: "是") : String(localized: "否")
|
||||||
|
testLog = String(localized: "第三方代理测试模式:模块连接成功;已保存坐标=\(savedCoordinate)")
|
||||||
} catch {
|
} catch {
|
||||||
testLog = "第三方代理测试模式:模块连接失败;\(error.localizedDescription)"
|
testLog = String(localized: "第三方代理测试模式:模块连接失败;\(error.localizedDescription)")
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
_ = await setup.runVerificationTest()
|
_ = await setup.runVerificationTest()
|
||||||
@@ -113,22 +114,11 @@ struct BugReportView: View {
|
|||||||
let systemVersion = UIDevice.current.systemVersion
|
let systemVersion = UIDevice.current.systemVersion
|
||||||
|
|
||||||
// 拼接报告
|
// 拼接报告
|
||||||
let report = """
|
let report = bugReport(
|
||||||
### 环境信息
|
appVersion: appVersion,
|
||||||
App 版本: \(appVersion)
|
systemVersion: systemVersion,
|
||||||
系统版本: iOS \(systemVersion)
|
testLog: testLog
|
||||||
运行模式: \(runtimeMode.mode.displayName)
|
)
|
||||||
第三方客户端: \(runtimeMode.mode == .thirdParty ? thirdPartyClient.selectedClient.name : "不适用")
|
|
||||||
可复现环境: \(isReproducible ? "是" : "否")
|
|
||||||
|
|
||||||
### 问题描述
|
|
||||||
\(description.trimmingCharacters(in: .whitespacesAndNewlines))
|
|
||||||
|
|
||||||
### 诊断日志
|
|
||||||
```
|
|
||||||
\(testLog.isEmpty ? "(无诊断数据)" : testLog)
|
|
||||||
```
|
|
||||||
"""
|
|
||||||
|
|
||||||
// 复制到剪切板
|
// 复制到剪切板
|
||||||
UIPasteboard.general.string = report
|
UIPasteboard.general.string = report
|
||||||
@@ -138,4 +128,28 @@ struct BugReportView: View {
|
|||||||
showCopiedAlert = true
|
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
@@ -23,7 +23,10 @@ struct RuntimeLogsView: View {
|
|||||||
private var filteredEntries: [RuntimeLogEntry] {
|
private var filteredEntries: [RuntimeLogEntry] {
|
||||||
let q = logFilter.trimmingCharacters(in: .whitespacesAndNewlines)
|
let q = logFilter.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||||
guard !q.isEmpty else { return entries }
|
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 {
|
var body: some View {
|
||||||
@@ -201,7 +204,7 @@ struct RuntimeLogsView: View {
|
|||||||
.foregroundStyle(entry.level == .error ? .red : entry.level == .warning ? .orange : .blue).frame(width: 18)
|
.foregroundStyle(entry.level == .error ? .red : entry.level == .warning ? .orange : .blue).frame(width: 18)
|
||||||
VStack(alignment: .leading, spacing: 4) {
|
VStack(alignment: .leading, spacing: 4) {
|
||||||
HStack {
|
HStack {
|
||||||
Text("\(entry.source) \(entry.category)").font(.caption.weight(.semibold))
|
Text("\(entry.source) \(entry.localizedCategory)").font(.caption.weight(.semibold))
|
||||||
Spacer()
|
Spacer()
|
||||||
Button {
|
Button {
|
||||||
UIPasteboard.general.string = entry.renderedText
|
UIPasteboard.general.string = entry.renderedText
|
||||||
@@ -225,9 +228,9 @@ struct RuntimeLogsView: View {
|
|||||||
}
|
}
|
||||||
.buttonStyle(.plain)
|
.buttonStyle(.plain)
|
||||||
}
|
}
|
||||||
Text(entry.message).font(.caption.monospaced()).textSelection(.enabled)
|
Text(entry.localizedMessage).font(.caption.monospaced()).textSelection(.enabled)
|
||||||
if !entry.details.isEmpty {
|
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)
|
.font(.caption2.monospaced()).foregroundStyle(.secondary).textSelection(.enabled)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -246,22 +249,26 @@ struct RuntimeLogsView: View {
|
|||||||
let active = response.success && response.latitude != nil && response.longitude != nil
|
let active = response.success && response.latitude != nil && response.longitude != nil
|
||||||
testSucceeded = true
|
testSucceeded = true
|
||||||
testResult = active ? String(localized: "第三方模块连接通过,已有坐标") : String(localized: "第三方模块连接通过,暂无坐标")
|
testResult = active ? String(localized: "第三方模块连接通过,已有坐标") : String(localized: "第三方模块连接通过,暂无坐标")
|
||||||
testMessage = """
|
testMessage = thirdPartyTestLog(active: active)
|
||||||
======== 第三方代理连接检测 ========
|
|
||||||
模式: 测试模式
|
|
||||||
请求: wloc-settings/save?action=query
|
|
||||||
拦截响应: 有效 JSON
|
|
||||||
已保存坐标: \(active ? "是" : "否")
|
|
||||||
"""
|
|
||||||
} catch {
|
} catch {
|
||||||
testSucceeded = false
|
testSucceeded = false
|
||||||
testResult = String(localized: "第三方模块连接失败")
|
testResult = String(localized: "第三方模块连接失败")
|
||||||
testMessage = """
|
testMessage = thirdPartyTestLog(error: error)
|
||||||
======== 第三方代理连接检测 ========
|
|
||||||
模式: 测试模式
|
|
||||||
请求: wloc-settings/save?action=query
|
|
||||||
结果: \(error.localizedDescription)
|
|
||||||
"""
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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
@@ -220,15 +220,15 @@ struct FirstSetupView: View {
|
|||||||
return thirdPartyTestFailure.message
|
return thirdPartyTestFailure.message
|
||||||
}
|
}
|
||||||
guard !setup.message.isEmpty else { return nil }
|
guard !setup.message.isEmpty else { return nil }
|
||||||
return """
|
return [
|
||||||
======== 第三方代理运行检测 ========
|
String(localized: "======== 第三方代理运行检测 ========"),
|
||||||
当前客户端:\(thirdPartyClient.selectedClient.name)
|
String(localized: "当前客户端:\(thirdPartyClient.selectedClient.name)"),
|
||||||
触发来源:地图或设置中的第三方代理操作
|
String(localized: "触发来源:地图或设置中的第三方代理操作"),
|
||||||
请求动作:WLOC 配置接口
|
String(localized: "请求动作:WLOC 配置接口"),
|
||||||
检测结果:失败
|
String(localized: "检测结果:失败"),
|
||||||
错误详情:\(setup.message)
|
String(localized: "错误详情:\(setup.message)"),
|
||||||
处理建议:确认模块已启用,并检查 MITM、证书和代理/VPN 连接。
|
String(localized: "处理建议:确认模块已启用,并检查 MITM、证书和代理/VPN 连接。")
|
||||||
"""
|
].joined(separator: "\n")
|
||||||
}
|
}
|
||||||
|
|
||||||
private var modeStep: some View {
|
private var modeStep: some View {
|
||||||
@@ -468,10 +468,10 @@ struct FirstSetupView: View {
|
|||||||
|
|
||||||
Text("返回格式")
|
Text("返回格式")
|
||||||
.font(.subheadline.bold())
|
.font(.subheadline.bold())
|
||||||
Text("""
|
Text(String(localized: """
|
||||||
成功:{"success":true,"longitude":113.0,"latitude":22.0,"accuracy":25}
|
成功:{"success":true,"longitude":113.0,"latitude":22.0,"accuracy":25}
|
||||||
失败:{"success":false,"error":"错误说明"}
|
失败:{"success":false,"error":"错误说明"}
|
||||||
""")
|
"""))
|
||||||
.font(.caption.monospaced())
|
.font(.caption.monospaced())
|
||||||
.textSelection(.enabled)
|
.textSelection(.enabled)
|
||||||
|
|
||||||
@@ -641,7 +641,7 @@ struct FirstSetupView: View {
|
|||||||
private func setupScreenshot(
|
private func setupScreenshot(
|
||||||
assetName: String,
|
assetName: String,
|
||||||
title: String,
|
title: String,
|
||||||
caption: LocalizedStringKey
|
caption: String
|
||||||
) -> some View {
|
) -> some View {
|
||||||
if let image = UIImage(named: assetName) {
|
if let image = UIImage(named: assetName) {
|
||||||
Button {
|
Button {
|
||||||
@@ -657,7 +657,7 @@ struct FirstSetupView: View {
|
|||||||
.clipShape(RoundedRectangle(cornerRadius: 6))
|
.clipShape(RoundedRectangle(cornerRadius: 6))
|
||||||
HStack(spacing: 6) {
|
HStack(spacing: 6) {
|
||||||
Image(systemName: "arrow.up.left.and.arrow.down.right")
|
Image(systemName: "arrow.up.left.and.arrow.down.right")
|
||||||
Text(caption)
|
Text(LocalizedStringKey(caption))
|
||||||
}
|
}
|
||||||
.font(.caption2)
|
.font(.caption2)
|
||||||
.foregroundStyle(.secondary)
|
.foregroundStyle(.secondary)
|
||||||
@@ -672,7 +672,11 @@ struct FirstSetupView: View {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
.buttonStyle(.plain)
|
.buttonStyle(.plain)
|
||||||
.accessibilityLabel(LocalizedStringKey(title))
|
.accessibilityLabel(
|
||||||
|
Text(LocalizedStringKey(title))
|
||||||
|
+ Text(": ")
|
||||||
|
+ Text(LocalizedStringKey(caption))
|
||||||
|
)
|
||||||
.accessibilityHint("轻点查看大图")
|
.accessibilityHint("轻点查看大图")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -872,21 +876,19 @@ struct FirstSetupView: View {
|
|||||||
"处理建议": ThirdPartyProxyError.recoverySuggestion(for: error)
|
"处理建议": ThirdPartyProxyError.recoverySuggestion(for: error)
|
||||||
]
|
]
|
||||||
)
|
)
|
||||||
thirdPartyTestFailure = ThirdPartyConnectionTestFailure(
|
thirdPartyTestFailure = ThirdPartyConnectionTestFailure(message: [
|
||||||
message: """
|
String(localized: "======== 第三方代理连接检测 ========"),
|
||||||
======== 第三方代理连接检测 ========
|
String(localized: "当前客户端:\(client.name)"),
|
||||||
当前客户端:\(client.name)
|
String(localized: "配置接口:/wloc-settings/save"),
|
||||||
配置接口:/wloc-settings/save
|
String(localized: "请求动作:WLOC query"),
|
||||||
请求动作:WLOC query
|
String(localized: "检查范围:模块拦截、MITM、证书、代理/VPN 连接"),
|
||||||
检查范围:模块拦截、MITM、证书、代理/VPN 连接
|
String(localized: "连接状态:\(connectionState)"),
|
||||||
连接状态:\(connectionState)
|
String(localized: "检测结果:失败"),
|
||||||
检测结果:失败
|
String(localized: "耗时:\(elapsedMilliseconds) ms"),
|
||||||
耗时:\(elapsedMilliseconds) ms
|
String(localized: "错误类型:\(errorType)"),
|
||||||
错误类型:\(errorType)
|
String(localized: "错误详情:\(error.localizedDescription)"),
|
||||||
错误详情:\(error.localizedDescription)
|
String(localized: "处理建议:\(ThirdPartyProxyError.recoverySuggestion(for: error))。")
|
||||||
处理建议:\(ThirdPartyProxyError.recoverySuggestion(for: error))。
|
].joined(separator: "\n"))
|
||||||
"""
|
|
||||||
)
|
|
||||||
showsThirdPartyFailureLog = true
|
showsThirdPartyFailureLog = true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -895,11 +897,11 @@ struct FirstSetupView: View {
|
|||||||
private var thirdPartyConnectionStateDescription: String {
|
private var thirdPartyConnectionStateDescription: String {
|
||||||
switch thirdPartyProxy.connectionState {
|
switch thirdPartyProxy.connectionState {
|
||||||
case .unknown:
|
case .unknown:
|
||||||
return "未检测"
|
return String(localized: "未检测")
|
||||||
case .connected(let active):
|
case .connected(let active):
|
||||||
return active ? "已连接,有保存坐标" : "已连接,无保存坐标"
|
return active ? String(localized: "已连接,有保存坐标") : String(localized: "已连接,无保存坐标")
|
||||||
case .failed(let message):
|
case .failed(let message):
|
||||||
return "连接失败(\(message))"
|
return String(localized: "连接失败(\(message))")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -766,7 +766,8 @@ struct MapHomeView: View {
|
|||||||
private func presentSuccessfulOperationTip(_ kind: VirtualLocationTipKind) {
|
private func presentSuccessfulOperationTip(_ kind: VirtualLocationTipKind) {
|
||||||
let count = tipPreferences.recordSuccessfulOperation(kind)
|
let count = tipPreferences.recordSuccessfulOperation(kind)
|
||||||
let operationName = kind == .activation ? "开启" : "关闭"
|
let operationName = kind == .activation ? "开启" : "关闭"
|
||||||
RuntimeLogger.info("APP", "提醒", "累计\(operationName)虚拟定位次数", details: [
|
RuntimeLogger.info("APP", "提醒", "累计虚拟定位操作次数", details: [
|
||||||
|
"操作": operationName,
|
||||||
"次数": String(count),
|
"次数": String(count),
|
||||||
"运行模式": runtimeMode.mode.displayName,
|
"运行模式": runtimeMode.mode.displayName,
|
||||||
"可显示不再提醒": String(tipPreferences.canSuppress(kind))
|
"可显示不再提醒": String(tipPreferences.canSuppress(kind))
|
||||||
@@ -1335,7 +1336,9 @@ struct MapHomeView: View {
|
|||||||
context.showFailureAlert,
|
context.showFailureAlert,
|
||||||
!Task.isCancelled,
|
!Task.isCancelled,
|
||||||
mapState.selection.revision == context.intent.selectionRevision else { return }
|
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
|
showLocationAlert = true
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -182,5 +182,5 @@ struct ProxyCoordinateSnapshot: Equatable {
|
|||||||
|
|
||||||
enum ProxyError: LocalizedError {
|
enum ProxyError: LocalizedError {
|
||||||
case startFailed
|
case startFailed
|
||||||
var errorDescription: String? { "Go proxy 启动失败" }
|
var errorDescription: String? { String(localized: "Go proxy 启动失败") }
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -222,11 +222,11 @@ struct SettingsView: View {
|
|||||||
|
|
||||||
Section("致谢") {
|
Section("致谢") {
|
||||||
Button {
|
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)
|
UIApplication.shared.open(url)
|
||||||
}
|
}
|
||||||
} label: {
|
} label: {
|
||||||
Label("核心定位改写逻辑移植自 Yu9191/wloc", systemImage: "heart.fill")
|
Label("定位改写脚本源自 Yu9191/wloc,现由本项目维护", systemImage: "heart.fill")
|
||||||
.foregroundStyle(.pink)
|
.foregroundStyle(.pink)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+20
-20
@@ -108,36 +108,36 @@ final class SetupCoordinator: ObservableObject {
|
|||||||
testLog = ""
|
testLog = ""
|
||||||
let log = { (msg: String) in self.testLog += msg + "\n" }
|
let log = { (msg: String) in self.testLog += msg + "\n" }
|
||||||
|
|
||||||
log("======== 代理验证测试 ========")
|
log(String(localized: "======== 代理验证测试 ========"))
|
||||||
log("App 版本: \(appVersion)")
|
log(String(localized: "App 版本: \(appVersion)"))
|
||||||
log("系统版本: iOS \(UIDevice.current.systemVersion)")
|
log(String(localized: "系统版本: iOS \(UIDevice.current.systemVersion)"))
|
||||||
log("")
|
log("")
|
||||||
|
|
||||||
// Step A: Proxy running
|
// Step A: Proxy running
|
||||||
log("[步骤 A] 检查代理是否运行…")
|
log(String(localized: "[步骤 A] 检查代理是否运行…"))
|
||||||
log(" 端口: 127.0.0.1:8888")
|
log(String(localized: " 端口: 127.0.0.1:8888"))
|
||||||
let stepAStart = Date()
|
let stepAStart = Date()
|
||||||
if !proxy.isRunning {
|
if !proxy.isRunning {
|
||||||
log(" ⚠ 代理未运行,尝试启动…")
|
log(String(localized: " ⚠ 代理未运行,尝试启动…"))
|
||||||
do { try await proxy.start() } catch {
|
do { try await proxy.start() } catch {
|
||||||
log(" ✗ 启动失败: \(error.localizedDescription)")
|
log(String(localized: " ✗ 启动失败: \(error.localizedDescription)"))
|
||||||
return .proxyNotRunning
|
return .proxyNotRunning
|
||||||
}
|
}
|
||||||
log(" ✓ 代理启动成功")
|
log(String(localized: " ✓ 代理启动成功"))
|
||||||
} else {
|
} else {
|
||||||
log(" ✓ 代理已在运行中")
|
log(String(localized: " ✓ 代理已在运行中"))
|
||||||
}
|
}
|
||||||
collectProxyLogs(since: stepAStart, to: log)
|
collectProxyLogs(since: stepAStart, to: log)
|
||||||
|
|
||||||
// Step B: Combined CA + WiFi proxy check (single request)
|
// Step B: Combined CA + WiFi proxy check (single request)
|
||||||
log("")
|
log("")
|
||||||
log("[步骤 B] 检测证书与 WiFi 代理…")
|
log(String(localized: "[步骤 B] 检测证书与 WiFi 代理…"))
|
||||||
log(" 方式: 请求 baidu.com/paopao-verify-<token>")
|
log(String(localized: " 方式: 请求 baidu.com/paopao-verify-<token>"))
|
||||||
log(" 结果判定: TLS 错误=证书问题 / 响应不匹配=代理未配置 / 匹配=通过")
|
log(String(localized: " 结果判定: TLS 错误=证书问题 / 响应不匹配=代理未配置 / 匹配=通过"))
|
||||||
let stepBStart = Date()
|
let stepBStart = Date()
|
||||||
let verifyToken = CoreBridge.refreshVerifyToken()
|
let verifyToken = CoreBridge.refreshVerifyToken()
|
||||||
guard !verifyToken.isEmpty else {
|
guard !verifyToken.isEmpty else {
|
||||||
log(" ✗ 无法生成验证 token")
|
log(String(localized: " ✗ 无法生成验证 token"))
|
||||||
return .certNotTrusted
|
return .certNotTrusted
|
||||||
}
|
}
|
||||||
do {
|
do {
|
||||||
@@ -150,17 +150,17 @@ final class SetupCoordinator: ObservableObject {
|
|||||||
let statusCode = (resp as? HTTPURLResponse)?.statusCode ?? 0
|
let statusCode = (resp as? HTTPURLResponse)?.statusCode ?? 0
|
||||||
let body = String(data: data, encoding: .utf8) ?? ""
|
let body = String(data: data, encoding: .utf8) ?? ""
|
||||||
if body == verifyToken {
|
if body == verifyToken {
|
||||||
log(" ✓ 证书已信任,WiFi 代理已配置")
|
log(String(localized: " ✓ 证书已信任,WiFi 代理已配置"))
|
||||||
} else {
|
} else {
|
||||||
log(" ✗ 响应不匹配: HTTP \(statusCode), \(data.count) bytes,WiFi 代理未配置")
|
log(String(localized: " ✗ 响应不匹配: HTTP \(statusCode), \(data.count) bytes,WiFi 代理未配置"))
|
||||||
return .wifiProxyNotConfigured
|
return .wifiProxyNotConfigured
|
||||||
}
|
}
|
||||||
} catch {
|
} catch {
|
||||||
let ns = error as NSError
|
let ns = error as NSError
|
||||||
let msg = error.localizedDescription
|
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) {
|
if isCertificateTrustError(nsError: ns, message: msg) {
|
||||||
log(" TLS/证书校验失败,CA 证书未信任")
|
log(String(localized: " TLS/证书校验失败,CA 证书未信任"))
|
||||||
return .certNotTrusted
|
return .certNotTrusted
|
||||||
}
|
}
|
||||||
return .wifiProxyNotConfigured
|
return .wifiProxyNotConfigured
|
||||||
@@ -168,7 +168,7 @@ final class SetupCoordinator: ObservableObject {
|
|||||||
collectProxyLogs(since: stepBStart, to: log)
|
collectProxyLogs(since: stepBStart, to: log)
|
||||||
|
|
||||||
log("")
|
log("")
|
||||||
log("======== 环境检测通过 ✓ ========")
|
log(String(localized: "======== 环境检测通过 ✓ ========"))
|
||||||
return .success
|
return .success
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -202,9 +202,9 @@ final class SetupCoordinator: ObservableObject {
|
|||||||
$0.source == "CORE" && $0.category == "Proxy" && $0.timestamp >= date
|
$0.source == "CORE" && $0.category == "Proxy" && $0.timestamp >= date
|
||||||
}
|
}
|
||||||
guard !proxyEntries.isEmpty else { return }
|
guard !proxyEntries.isEmpty else { return }
|
||||||
log(" --- 代理日志 ---")
|
log(String(localized: " --- 代理日志 ---"))
|
||||||
for e in proxyEntries {
|
for e in proxyEntries {
|
||||||
log(" " + e.message)
|
log(" " + e.localizedMessage)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+5
-4
@@ -229,10 +229,10 @@ Current client status:
|
|||||||
Community configurations are reviewed by client. Accepted submissions are linked in this table with attribution unless
|
Community configurations are reviewed by client. Accepted submissions are linked in this table with attribution unless
|
||||||
the contributor requests anonymous inclusion.
|
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)
|
- [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
|
The selected client owns its certificates, MITM configuration, and proxy switches. Review third-party modules and
|
||||||
scripts before importing them.
|
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;
|
- 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;
|
- Issue reports are copied by the user before being submitted to GitHub;
|
||||||
- App Mode accesses the local proxy and the environment-verification URL;
|
- 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;
|
- 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.
|
- 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
|
## 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)
|
- [Yu9191/wloc](https://github.com/Yu9191/wloc)
|
||||||
- [ios-location-spoofer](https://github.com/mekos2772/ios-location-spoofer)
|
- [ios-location-spoofer](https://github.com/mekos2772/ios-location-spoofer)
|
||||||
|
|||||||
@@ -219,10 +219,9 @@ App 只验证配置接口的 HTTP 状态、JSON 格式和坐标回读,不管
|
|||||||
|
|
||||||
社区配置按客户端分区审核;采纳后会在上表链接教程和投稿者,投稿者也可以选择匿名收录。
|
社区配置按客户端分区审核;采纳后会在上表链接教程和投稿者,投稿者也可以选择匿名收录。
|
||||||
|
|
||||||
相关模块快照和来源记录:
|
模块与脚本现由本仓库托管,原 `Yu9191/wloc` 仓库已删除,仅保留来源致谢,不再作为运行时依赖:
|
||||||
|
|
||||||
- [第三方模块说明](docs/THIRD_PARTY_MODULES.md)
|
- [第三方模块说明](docs/THIRD_PARTY_MODULES.md)
|
||||||
- [Yu9191/wloc](https://github.com/Yu9191/wloc)
|
|
||||||
|
|
||||||
第三方客户端、证书、MITM 和代理开关由客户端自身负责。导入任何第三方模块前,请先审查其配置和脚本内容。
|
第三方客户端、证书、MITM 和代理开关由客户端自身负责。导入任何第三方模块前,请先审查其配置和脚本内容。
|
||||||
|
|
||||||
@@ -351,7 +350,7 @@ dist/PaopaoLocationSpoofer-unsigned.ipa
|
|||||||
- 运行日志保存在设备 App Group 容器中,并自动保留近三天;
|
- 运行日志保存在设备 App Group 容器中,并自动保留近三天;
|
||||||
- 问题报告需要用户主动复制后提交到 GitHub;
|
- 问题报告需要用户主动复制后提交到 GitHub;
|
||||||
- APP 模式会访问本机代理和环境验证地址;
|
- APP 模式会访问本机代理和环境验证地址;
|
||||||
- 第三方代理模式可能访问上游模块地址和 WLOC 配置接口;
|
- 第三方代理模式可能访问本仓库托管的模块地址和 WLOC 配置接口;
|
||||||
- App 生成的 CA 私钥保存在设备 Keychain 中;
|
- App 生成的 CA 私钥保存在设备 Keychain 中;
|
||||||
- 第三方客户端模块、MITM 和证书链路由用户选择的客户端负责。
|
- 第三方客户端模块、MITM 和证书链路由用户选择的客户端负责。
|
||||||
|
|
||||||
@@ -430,7 +429,7 @@ GitHub Issue Form 中的“App 生成的诊断报告”字段与 App 复制内
|
|||||||
|
|
||||||
## 致谢与友链
|
## 致谢与友链
|
||||||
|
|
||||||
核心定位响应处理思路、Go 实现和第三方模块参考自:
|
核心定位响应处理思路、Go 实现和第三方模块参考自(`Yu9191/wloc` 原仓库已删除):
|
||||||
|
|
||||||
- [Yu9191/wloc](https://github.com/Yu9191/wloc)
|
- [Yu9191/wloc](https://github.com/Yu9191/wloc)
|
||||||
- [ios-location-spoofer](https://github.com/mekos2772/ios-location-spoofer)
|
- [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
|
||||||
@@ -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
|
||||||
@@ -142,6 +142,12 @@
|
|||||||
"第三方模块连接通过,已有坐标" = "Third-party module connected, coordinate present";
|
"第三方模块连接通过,已有坐标" = "Third-party module connected, coordinate present";
|
||||||
"第三方模块连接通过,暂无坐标" = "Third-party module connected, no coordinate yet";
|
"第三方模块连接通过,暂无坐标" = "Third-party module connected, no coordinate yet";
|
||||||
"第三方模块连接失败" = "Third-party module connection failed";
|
"第三方模块连接失败" = "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 */
|
/* MARK: - TipViews */
|
||||||
"生效说明" = "How to Activate";
|
"生效说明" = "How to Activate";
|
||||||
@@ -186,7 +192,7 @@
|
|||||||
"去提交" = "Submit";
|
"去提交" = "Submit";
|
||||||
"复制模板" = "Copy Template";
|
"复制模板" = "Copy Template";
|
||||||
"不再提示" = "Don't Show Again";
|
"不再提示" = "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.";
|
"你正在使用 %@。点击“去提交”会先复制投稿模板,并在 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";
|
"已复制投稿模板" = "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.";
|
"如果 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";
|
"第三方代理清除坐标失败" = "The third-party proxy failed to clear the coordinate";
|
||||||
"已有第三方代理请求正在执行" = "A third-party proxy request is already running";
|
"已有第三方代理请求正在执行" = "A third-party proxy request is already running";
|
||||||
"配置已提供,尚未验证" = "Configuration provided, not yet verified";
|
"配置已提供,尚未验证" = "Configuration provided, not yet verified";
|
||||||
|
"已连接,有保存坐标" = "Connected, coordinate saved";
|
||||||
|
"已连接,无保存坐标" = "Connected, no saved coordinate";
|
||||||
|
"连接失败(%@)" = "Connection failed (%@)";
|
||||||
|
"当前客户端:%@" = "Current client: %@";
|
||||||
|
"配置接口:/wloc-settings/save" = "Configuration endpoint: /wloc-settings/save";
|
||||||
|
"请求动作:WLOC query" = "Request action: WLOC query";
|
||||||
|
"检查范围:模块拦截、MITM、证书、代理/VPN 连接" = "Check scope: module interception, MITM, certificate, and proxy/VPN connection";
|
||||||
|
"连接状态:%@" = "Connection status: %@";
|
||||||
|
"检测结果:失败" = "Check result: Failed";
|
||||||
|
"耗时:%lld ms" = "Duration: %lld ms";
|
||||||
|
"错误类型:%@" = "Error type: %@";
|
||||||
|
"错误详情:%@" = "Error details: %@";
|
||||||
|
"处理建议:%@。" = "Suggested action: %@.";
|
||||||
|
"======== 第三方代理运行检测 ========" = "======== Third-party Proxy Runtime Check ========";
|
||||||
|
"触发来源:地图或设置中的第三方代理操作" = "Triggered by: third-party proxy action from the map or Settings";
|
||||||
|
"请求动作:WLOC 配置接口" = "Request action: WLOC configuration endpoint";
|
||||||
|
"处理建议:确认模块已启用,并检查 MITM、证书和代理/VPN 连接。" = "Suggested action: confirm the module is enabled, then check MITM, the certificate, and the proxy/VPN connection.";
|
||||||
|
|
||||||
|
/* MARK: - Exported diagnostics */
|
||||||
|
"环境信息" = "Environment";
|
||||||
|
"App 版本" = "App version";
|
||||||
|
"系统版本" = "System version";
|
||||||
|
"诊断日志" = "Diagnostic Logs";
|
||||||
|
"不适用" = "Not applicable";
|
||||||
|
"是" = "Yes";
|
||||||
|
"否" = "No";
|
||||||
|
"(无诊断数据)" = "(No diagnostic data)";
|
||||||
|
"第三方代理测试模式:模块连接成功;已保存坐标=%@" = "Third-party proxy test mode: module connected; saved coordinate=%@";
|
||||||
|
"第三方代理测试模式:模块连接失败;%@" = "Third-party proxy test mode: module connection failed; %@";
|
||||||
|
"======== 代理验证测试 ========" = "======== Proxy Verification Test ========";
|
||||||
|
"App 版本: %@" = "App version: %@";
|
||||||
|
"系统版本: iOS %@" = "System version: iOS %@";
|
||||||
|
"[步骤 A] 检查代理是否运行…" = "[Step A] Check whether the proxy is running…";
|
||||||
|
" 端口: 127.0.0.1:8888" = " Port: 127.0.0.1:8888";
|
||||||
|
" ⚠ 代理未运行,尝试启动…" = " ⚠ Proxy is not running; attempting to start it…";
|
||||||
|
" ✗ 启动失败: %@" = " ✗ Failed to start: %@";
|
||||||
|
" ✓ 代理启动成功" = " ✓ Proxy started";
|
||||||
|
" ✓ 代理已在运行中" = " ✓ Proxy is already running";
|
||||||
|
"[步骤 B] 检测证书与 WiFi 代理…" = "[Step B] Check certificate and Wi-Fi proxy…";
|
||||||
|
" 方式: 请求 baidu.com/paopao-verify-<token>" = " Method: request baidu.com/paopao-verify-<token>";
|
||||||
|
" 结果判定: TLS 错误=证书问题 / 响应不匹配=代理未配置 / 匹配=通过" = " Criteria: TLS error=certificate issue / response mismatch=proxy not configured / match=passed";
|
||||||
|
" ✗ 无法生成验证 token" = " ✗ Could not generate a verification token";
|
||||||
|
" ✓ 证书已信任,WiFi 代理已配置" = " ✓ Certificate is trusted and the Wi-Fi proxy is configured";
|
||||||
|
" ✗ 响应不匹配: HTTP %lld, %lld bytes,WiFi 代理未配置" = " ✗ Response mismatch: HTTP %1$lld, %2$lld bytes; Wi-Fi proxy is not configured";
|
||||||
|
" ✗ 请求失败 [%@ code=%lld]: %@" = " ✗ Request failed [%1$@ code=%2$lld]: %3$@";
|
||||||
|
" TLS/证书校验失败,CA 证书未信任" = " TLS/certificate validation failed; the CA certificate is not trusted";
|
||||||
|
"======== 环境检测通过 ✓ ========" = "======== Environment Check Passed ✓ ========";
|
||||||
|
" --- 代理日志 ---" = " --- Proxy Logs ---";
|
||||||
|
|
||||||
|
/* MARK: - Setup examples and acknowledgements */
|
||||||
|
"成功:{\"success\":true,\"longitude\":113.0,\"latitude\":22.0,\"accuracy\":25}\n失败:{\"success\":false,\"error\":\"错误说明\"}" = "Success: {\"success\":true,\"longitude\":113.0,\"latitude\":22.0,\"accuracy\":25}\nFailure: {\"success\":false,\"error\":\"Error details\"}";
|
||||||
|
"定位改写脚本源自 Yu9191/wloc,现由本项目维护" = "Location-rewrite scripts originated from Yu9191/wloc and are now maintained by this project";
|
||||||
|
|
||||||
|
/* MARK: - Runtime log categories and messages */
|
||||||
|
"地图" = "Map";
|
||||||
|
"坐标转换" = "Coordinate Conversion";
|
||||||
|
"定位" = "Location";
|
||||||
|
"实时定位" = "Live Location";
|
||||||
|
"提醒" = "Reminder";
|
||||||
|
"搜索" = "Search";
|
||||||
|
"收藏" = "Favorites";
|
||||||
|
"缩放" = "Zoom";
|
||||||
|
"========== App 启动 ==========" = "========== App Launch ==========";
|
||||||
|
"尚未选择运行模式,跳过本地 CA 和代理初始化" = "No runtime mode selected; skipping local CA and proxy initialization";
|
||||||
|
"第三方代理测试模式:跳过本地 CA、代理和环境检测" = "Third-party proxy test mode: skipping local CA, proxy, and environment checks";
|
||||||
|
"旧坐标数据迁移失败,将在下次启动重试" = "Legacy coordinate migration failed; it will be retried on the next launch";
|
||||||
|
"地图坐标标准初始化完成,开始后续启动流程" = "Map coordinate-system initialization completed; continuing app startup";
|
||||||
|
"没有持久化图钉,地图创建前请求实时定位" = "No persisted pin; requesting live location before creating the map";
|
||||||
|
"已使用实时定位准备唯一初始地图状态" = "Prepared the initial map state using live location";
|
||||||
|
"地图创建前取得的初始实时位置(WGS-84)" = "Initial live location obtained before map creation (WGS-84)";
|
||||||
|
"地图创建前无法取得实时定位,唯一初始位置使用深圳" = "Could not obtain live location before map creation; using Shenzhen as the initial location";
|
||||||
|
"已找到持久化图钉,直接准备唯一初始地图状态" = "Found a persisted pin and prepared the initial map state";
|
||||||
|
"启动门禁全部完成,现在创建 MapHomeView" = "All startup gates completed; creating MapHomeView";
|
||||||
|
"初始化" = "Initialize";
|
||||||
|
"启动代理 127.0.0.1:8888" = "Starting proxy at 127.0.0.1:8888";
|
||||||
|
"启动代理: 恢复上次 WGS-84 定位" = "Starting proxy: restoring the last WGS-84 location";
|
||||||
|
"启动成功" = "Started successfully";
|
||||||
|
"启动失败" = "Failed to start";
|
||||||
|
"写入坐标" = "Write coordinate";
|
||||||
|
"跳过过期坐标写入" = "Skipped stale coordinate write";
|
||||||
|
"跳过旧验证坐标恢复" = "Skipped restoring coordinates from an outdated verification";
|
||||||
|
"运动状态模拟设置已更新" = "Motion simulation setting updated";
|
||||||
|
"准备证书下载失败" = "Failed to prepare certificate download";
|
||||||
|
"设置虚拟定位坐标" = "Set simulated-location coordinate";
|
||||||
|
"apply失败" = "Apply failed";
|
||||||
|
"保存收藏失败" = "Failed to save favorite";
|
||||||
|
"本地服务初始化失败" = "Local service initialization failed";
|
||||||
|
"代理运行模式已切换" = "Proxy runtime mode changed";
|
||||||
|
"已迁移旧版模式初始化状态" = "Migrated legacy mode initialization state";
|
||||||
|
"地图坐标标准检测已有请求进行中" = "A map coordinate-system check is already running";
|
||||||
|
"地图坐标标准检测开始" = "Map coordinate-system check started";
|
||||||
|
"地图坐标标准检测获得明确结果" = "Map coordinate-system check returned a definitive result";
|
||||||
|
"地图坐标标准检测不可用,开始实时定位兜底" = "Map coordinate-system check unavailable; starting live-location fallback";
|
||||||
|
"地图坐标标准检测使用兜底结果" = "Map coordinate-system check used the fallback result";
|
||||||
|
"地图坐标标准检测被取消,保留默认国内标准" = "Map coordinate-system check was cancelled; keeping the default mainland-China system";
|
||||||
|
"地图坐标标准已确定,允许创建地图" = "Map coordinate system determined; map creation is now allowed";
|
||||||
|
"地图坐标标准运行期检测合并到进行中请求" = "Runtime map coordinate-system check joined the request already in progress";
|
||||||
|
"地图坐标标准运行期检测开始" = "Runtime map coordinate-system check started";
|
||||||
|
"地图坐标标准运行期检测结果已过期,取消写入" = "Runtime map coordinate-system result became stale; discarding it";
|
||||||
|
"地图坐标标准运行期检测完成,标准未变化" = "Runtime map coordinate-system check completed with no change";
|
||||||
|
"地图坐标标准运行期检测发现切换" = "Runtime map coordinate-system check detected a change";
|
||||||
|
"地图坐标标准运行期检测失败,保留当前标准" = "Runtime map coordinate-system check failed; keeping the current system";
|
||||||
|
"地图坐标标准运行期检测已取消" = "Runtime map coordinate-system check cancelled";
|
||||||
|
"实时定位不覆盖固定锚点的明确检测结果" = "Live location did not override the definitive fixed-anchor result";
|
||||||
|
"实时定位确认兜底地图坐标标准无需修正" = "Live location confirmed that the fallback map coordinate system needs no correction";
|
||||||
|
"实时定位修正启动兜底地图坐标标准" = "Live location corrected the startup fallback map coordinate system";
|
||||||
|
"第三方代理已保存 WGS-84 坐标" = "Third-party proxy saved the WGS-84 coordinate";
|
||||||
|
"第三方代理坐标已清除" = "Third-party proxy coordinate cleared";
|
||||||
|
"第三方代理请求失败" = "Third-party proxy request failed";
|
||||||
|
"第三方代理坐标同步成功" = "Third-party proxy coordinate synchronized";
|
||||||
|
"同步坐标到第三方客户端失败" = "Failed to synchronize the coordinate to the third-party client";
|
||||||
|
"验证结果" = "Verification result";
|
||||||
|
"验证失败" = "Verification failed";
|
||||||
|
"开启前检测失败,进入对应环境引导" = "Pre-start check failed; opening the relevant setup guide";
|
||||||
|
"清除第三方客户端坐标失败" = "Failed to clear the coordinate from the third-party client";
|
||||||
|
"累计虚拟定位操作次数" = "Counted simulated-location operations";
|
||||||
|
"已复制坐标" = "Coordinate copied";
|
||||||
|
"地图坐标标准切换时未找到当前选点缓存" = "No current-selection cache was found when the map coordinate system changed";
|
||||||
|
"地图坐标标准切换后已使用缓存坐标对回显当前选点" = "Restored the current selection from cached coordinates after the map coordinate system changed";
|
||||||
|
"地图坐标类型已变化" = "Map coordinate type changed";
|
||||||
|
"取消保存收藏:检测期间当前选点已变化" = "Cancelled saving favorite because the current selection changed during detection";
|
||||||
|
"保存当前选点为收藏" = "Saving the current selection as a favorite";
|
||||||
|
"第三方代理查询返回失败" = "Third-party proxy query returned a failure";
|
||||||
|
"启动后第三方代理状态查询失败" = "Failed to query third-party proxy status after startup";
|
||||||
|
"检测到 Wi-Fi 网络变化" = "Detected a Wi-Fi network change";
|
||||||
|
"网络仍在变化,重新计算环境检测等待时间" = "Network is still changing; recalculating the environment-check delay";
|
||||||
|
"等待 Wi-Fi 连接稳定后检测" = "Waiting for the Wi-Fi connection to stabilize before checking";
|
||||||
|
"延时检测已被更新的网络事件取消" = "Delayed check was cancelled by a newer network event";
|
||||||
|
"稳定等待结束后仍未连接 Wi-Fi,提示检查代理" = "Wi-Fi is still disconnected after the stability delay; prompting the user to check the proxy";
|
||||||
|
"开始后台环境检测" = "Starting background environment check";
|
||||||
|
"已有环境检测运行,1 秒后重试" = "An environment check is already running; retrying in 1 second";
|
||||||
|
"后台环境检测完成" = "Background environment check completed";
|
||||||
|
"环境检测连续被占用,本次不重复弹窗" = "Environment check remained busy; suppressing a duplicate alert";
|
||||||
|
"用户点击实时定位" = "User requested live location";
|
||||||
|
"MapKit 蓝点尚不可用,启动 CLLocationManager 兜底" = "MapKit user location is unavailable; starting the CLLocationManager fallback";
|
||||||
|
"MapKit 蓝点抢先完成请求,取消 CLLocationManager 兜底" = "MapKit completed the request first; cancelling the CLLocationManager fallback";
|
||||||
|
"虚拟定位开启后的 MapKit 蓝点标准判定" = "Determined the MapKit user-location coordinate system after simulated location started";
|
||||||
|
"登记实时定位请求上下文" = "Registered live-location request context";
|
||||||
|
"复用进行中的 CLLocationManager 请求并更新意图上下文" = "Reusing the active CLLocationManager request and updating its intent context";
|
||||||
|
"CLLocationManager 兜底未返回坐标" = "CLLocationManager fallback returned no coordinate";
|
||||||
|
"丢弃 CLLocationManager 结果:任务已取消或蓝点已抢先完成" = "Discarded CLLocationManager result because the task was cancelled or MapKit completed first";
|
||||||
|
"实时定位坐标完成标准判断并提交到地图" = "Live-location coordinate system determined and submitted to the map";
|
||||||
|
"MKLocalSearch 返回地点结果" = "MKLocalSearch returned a place result";
|
||||||
|
"反向地理编码网络失败,准备重试" = "Reverse geocoding network request failed; preparing to retry";
|
||||||
|
"反向地理编码失败" = "Reverse geocoding failed";
|
||||||
|
"获得搜索结果" = "Received search results";
|
||||||
|
"点击收藏点并回显到地图" = "Selected a favorite and displayed it on the map";
|
||||||
|
"忽略重复 CLLocationManager 请求" = "Ignored duplicate CLLocationManager request";
|
||||||
|
"CLLocationManager 请求入口" = "CLLocationManager request entry";
|
||||||
|
"授权状态不允许定位" = "Authorization status does not permit location access";
|
||||||
|
"创建 CLLocationManager 请求" = "Created CLLocationManager request";
|
||||||
|
"请求前台定位授权" = "Requesting foreground location authorization";
|
||||||
|
"定位授权状态变化" = "Location authorization status changed";
|
||||||
|
"CLLocationManager 返回样本批次" = "CLLocationManager returned a batch of samples";
|
||||||
|
"本批次没有可完成当前请求的样本" = "This batch contains no sample that can complete the current request";
|
||||||
|
"接受 CLLocationManager 样本并完成请求" = "Accepted CLLocationManager sample and completed the request";
|
||||||
|
"定位回调失败" = "Location callback failed";
|
||||||
|
"跳过 CLLocationManager 缓存样本" = "Skipped cached CLLocationManager sample";
|
||||||
|
"等待定位授权超时" = "Timed out waiting for location authorization";
|
||||||
|
"单次定位超时,切换持续定位" = "One-shot location timed out; switching to continuous updates";
|
||||||
|
"持续定位超时" = "Continuous location updates timed out";
|
||||||
|
"开始 CLLocationManager 单次定位" = "Starting one-shot CLLocationManager request";
|
||||||
|
"开始 CLLocationManager 持续定位兜底" = "Starting continuous CLLocationManager fallback";
|
||||||
|
"CLLocationManager 请求完成" = "CLLocationManager request completed";
|
||||||
|
"CLLocationManager 请求结束但没有坐标" = "CLLocationManager request ended without a coordinate";
|
||||||
|
"使用 CLLocationManager 新鲜缓存" = "Using a fresh CLLocationManager cache entry";
|
||||||
|
"实时定位按钮直接使用 MapKit 蓝点缓存" = "Live-location action used the MapKit user-location cache directly";
|
||||||
|
"主页收到待处理请求所需的 MapKit 蓝点回调" = "Home screen received the MapKit user-location callback for the pending request";
|
||||||
|
"主页收到 CLLocationManager 兜底坐标" = "Home screen received the CLLocationManager fallback coordinate";
|
||||||
|
"原始实时定位坐标" = "Original live-location coordinate";
|
||||||
|
"检查 CLLocationManager 样本" = "Checking CLLocationManager sample";
|
||||||
|
"拒绝 CLLocationManager 样本:坐标或精度无效" = "Rejected CLLocationManager sample because its coordinate or accuracy is invalid";
|
||||||
|
"MapKit 蓝点更新但 location 为空" = "MapKit user location updated but its location value is empty";
|
||||||
|
"拒绝 MapKit 蓝点样本:坐标无效" = "Rejected MapKit user-location sample because its coordinate is invalid";
|
||||||
|
"拒绝 MapKit 蓝点样本:水平精度无效" = "Rejected MapKit user-location sample because horizontal accuracy is invalid";
|
||||||
|
"音频中断恢复" = "Recovered from audio interruption";
|
||||||
|
"音频会话失败" = "Audio session failed";
|
||||||
|
"无法创建静音音频缓冲区" = "Could not create the silent-audio buffer";
|
||||||
|
"引擎启动失败" = "Audio engine failed to start";
|
||||||
|
"后台保活已启动(静音音频)" = "Background keep-alive started (silent audio)";
|
||||||
|
"后台保活已停止" = "Background keep-alive stopped";
|
||||||
|
"保存当前图钉失败" = "Failed to save the current pin";
|
||||||
|
"存储缩放" = "Stored zoom level";
|
||||||
|
"旧坐标数据迁移完成" = "Legacy coordinate migration completed";
|
||||||
|
"远程版本配置加载成功" = "Remote version configuration loaded";
|
||||||
|
"版本配置源不可用,尝试下一地址" = "Version configuration source unavailable; trying the next URL";
|
||||||
|
"版本配置加载失败,继续使用内置配置" = "Failed to load version configuration; continuing with built-in values";
|
||||||
|
"版本说明源不可用,尝试下一地址" = "Release-notes source unavailable; trying the next URL";
|
||||||
|
"版本说明加载失败,将使用最新 Release 页面" = "Failed to load release notes; using the latest Release page";
|
||||||
|
"复用设备钥匙串中的 CA" = "Reusing the CA from the device keychain";
|
||||||
|
"旧 CA 已迁移到设备钥匙串" = "Legacy CA migrated to the device keychain";
|
||||||
|
"已生成并保存设备专属 CA" = "Generated and saved a device-specific CA";
|
||||||
|
"已删除设备 CA,等待重新生成" = "Deleted the device CA; waiting to regenerate it";
|
||||||
|
"钥匙串中的 CA 无效,准备回退" = "CA in the keychain is invalid; preparing fallback";
|
||||||
|
"删除旧 CA 文件失败,将在下次启动重试" = "Failed to delete legacy CA files; it will be retried on the next launch";
|
||||||
|
"无法解析 CA 证书 PEM" = "Could not parse the CA certificate PEM";
|
||||||
|
"无法创建 SecTrust" = "Could not create SecTrust";
|
||||||
|
"SecTrust 评估返回错误" = "SecTrust evaluation returned an error";
|
||||||
|
"CA 证书已被系统信任" = "CA certificate is trusted by the system";
|
||||||
|
"CA 证书未被系统信任" = "CA certificate is not trusted by the system";
|
||||||
|
"调用 Go Core 生成 CA" = "Calling Go Core to generate the CA";
|
||||||
|
"Go Core CA 生成成功" = "Go Core generated the CA";
|
||||||
|
"本地证书服务已在运行" = "Local certificate server is already running";
|
||||||
|
"调用 Go Core 启动本地证书服务" = "Calling Go Core to start the local certificate server";
|
||||||
|
"本地证书服务启动成功" = "Local certificate server started";
|
||||||
|
"停止本地证书服务" = "Stopping local certificate server";
|
||||||
|
"开始第三方代理连接检测" = "Starting third-party proxy connection check";
|
||||||
|
"第三方代理连接检测通过" = "Third-party proxy connection check passed";
|
||||||
|
"第三方代理连接检测失败" = "Third-party proxy connection check failed";
|
||||||
|
"关闭代理前已同步关闭虚拟定位" = "Stopped simulated location before stopping the proxy";
|
||||||
|
"切换 APP 模式前无法清除第三方坐标" = "Could not clear the third-party coordinate before switching to App Mode";
|
||||||
|
"设置页第三方连接检测通过" = "Third-party connection check passed in Settings";
|
||||||
|
"设置页第三方连接检测失败" = "Third-party connection check failed in Settings";
|
||||||
|
"地图实际显示坐标" = "Coordinate displayed on map";
|
||||||
|
|
||||||
|
/* MARK: - Runtime log fields and values */
|
||||||
|
"App回到前台" = "App returned to foreground";
|
||||||
|
"App已确认地图标准" = "App confirmed the map coordinate system";
|
||||||
|
"CLLocationManager请求中" = "CLLocationManager request active";
|
||||||
|
"Go proxy 启动失败" = "Go proxy failed to start";
|
||||||
|
"MapKit地图蓝点" = "MapKit user location";
|
||||||
|
"MapKit蓝点新样本" = "New MapKit user-location sample";
|
||||||
|
"MapKit蓝点缓存" = "MapKit user-location cache";
|
||||||
|
"SSID可读取" = "SSID readable";
|
||||||
|
"WLOC写入标准" = "WLOC write coordinate system";
|
||||||
|
"WLOC目标标准" = "WLOC target coordinate system";
|
||||||
|
"Wi-Fi接口" = "Wi-Fi interface";
|
||||||
|
"intent选点revision" = "Intent selection revision";
|
||||||
|
"manager请求中" = "Manager request active";
|
||||||
|
"上下文存在" = "Context present";
|
||||||
|
"事件原因" = "Event reason";
|
||||||
|
"任务已取消" = "Task cancelled";
|
||||||
|
"任务已存在" = "Task already present";
|
||||||
|
"使用兜底" = "Used fallback";
|
||||||
|
"保存收藏" = "Save favorite";
|
||||||
|
"保存数据包含" = "Saved data contains";
|
||||||
|
"保持未启用" = "Kept disabled";
|
||||||
|
"保留原第三方坐标" = "Kept previous third-party coordinate";
|
||||||
|
"保留已启用状态" = "Kept enabled state";
|
||||||
|
"保留标准" = "Retained coordinate system";
|
||||||
|
"修正兜底标准" = "Corrected fallback coordinate system";
|
||||||
|
"内存缓存存在" = "In-memory cache present";
|
||||||
|
"初始坐标来源" = "Initial coordinate source";
|
||||||
|
"初始来源" = "Initial source";
|
||||||
|
"初始阶段" = "Initial phase";
|
||||||
|
"判定规则" = "Decision rule";
|
||||||
|
"原兜底来源" = "Original fallback source";
|
||||||
|
"原因" = "Reason";
|
||||||
|
"取值字段" = "Source field";
|
||||||
|
"可显示不再提醒" = "Can show suppress option";
|
||||||
|
"启动代理…" = "Starting proxy…";
|
||||||
|
"命中林士街" = "Matched Lin Shi Street";
|
||||||
|
"固定锚点" = "Fixed anchor";
|
||||||
|
"固定锚点查询超过5秒" = "Fixed-anchor query exceeded 5 seconds";
|
||||||
|
"固定锚点查询返回空结果" = "Fixed-anchor query returned no results";
|
||||||
|
"国内标准(GCJ-02)" = "Mainland China system (GCJ-02)";
|
||||||
|
"国内转换区域" = "Mainland conversion region";
|
||||||
|
"国外非转换区域" = "Non-conversion region outside mainland China";
|
||||||
|
"国际标准(WGS-84)" = "International system (WGS-84)";
|
||||||
|
"国际标准(WGS-84)+国内标准(GCJ-02)" = "International (WGS-84) + mainland China (GCJ-02)";
|
||||||
|
"图钉已按新类型重设" = "Pin reset for the new type";
|
||||||
|
"地图取值字段" = "Map source field";
|
||||||
|
"地图标准" = "Map coordinate system";
|
||||||
|
"坐标有效" = "Coordinate valid";
|
||||||
|
"坐标标准" = "Coordinate system";
|
||||||
|
"垂直精度米" = "Vertical accuracy (m)";
|
||||||
|
"基础校验通过" = "Basic validation passed";
|
||||||
|
"处理建议" = "Suggested action";
|
||||||
|
"失败时提示" = "Show alert on failure";
|
||||||
|
"实时定位存在" = "Live location present";
|
||||||
|
"实时定位服务区域" = "Live-location service region";
|
||||||
|
"客户端模式" = "Client mode";
|
||||||
|
"尝试" = "Attempt";
|
||||||
|
"已取消位置更新" = "Location updates cancelled";
|
||||||
|
"已恢复真实定位" = "Real location restored";
|
||||||
|
"已清理" = "Cleared";
|
||||||
|
"已重置" = "Reset";
|
||||||
|
"开启" = "Enable";
|
||||||
|
"异步地理编码" = "Asynchronous geocoding";
|
||||||
|
"当前地图标准" = "Current map coordinate system";
|
||||||
|
"当前客户端" = "Current client";
|
||||||
|
"当前标准" = "Current coordinate system";
|
||||||
|
"当前选点revision" = "Current selection revision";
|
||||||
|
"恢复状态" = "Restore state";
|
||||||
|
"批次索引" = "Batch index";
|
||||||
|
"持久化字段" = "Persisted field";
|
||||||
|
"授权状态" = "Authorization status";
|
||||||
|
"授权状态rawValue" = "Authorization status raw value";
|
||||||
|
"探测失败原因" = "Probe failure reason";
|
||||||
|
"搜索结果" = "Search results";
|
||||||
|
"操作" = "Operation";
|
||||||
|
"新类型" = "New type";
|
||||||
|
"无已保存" = "None saved";
|
||||||
|
"无法判定" = "Undetermined";
|
||||||
|
"无法启动本地证书服务" = "Could not start the local certificate server";
|
||||||
|
"无法生成本地证书" = "Could not generate the local certificate";
|
||||||
|
"日志策略" = "Logging policy";
|
||||||
|
"旧类型" = "Previous type";
|
||||||
|
"显示坐标字段" = "Displayed coordinate field";
|
||||||
|
"最低版本" = "Minimum version";
|
||||||
|
"最新版本" = "Latest version";
|
||||||
|
"最终标准" = "Final coordinate system";
|
||||||
|
"最终阶段" = "Final phase";
|
||||||
|
"有坐标" = "Coordinate present";
|
||||||
|
"有效样本数" = "Valid sample count";
|
||||||
|
"有缓存" = "Cache present";
|
||||||
|
"未知错误" = "Unknown error";
|
||||||
|
"本地 CA 证书或私钥无效" = "Local CA certificate or private key is invalid";
|
||||||
|
"无法写入设备钥匙串(%d)" = "Could not write to the device keychain (%d)";
|
||||||
|
"来源" = "Source";
|
||||||
|
"来源类型" = "Source type";
|
||||||
|
"林士街" = "Lin Shi Street";
|
||||||
|
"查看诊断日志" = "View diagnostic logs";
|
||||||
|
"样本年龄秒" = "Sample age (s)";
|
||||||
|
"样本数" = "Sample count";
|
||||||
|
"样本时间" = "Sample time";
|
||||||
|
"检查范围" = "Check scope";
|
||||||
|
"检测前标准" = "Coordinate system before check";
|
||||||
|
"模块拦截、MITM、代理/VPN连接" = "Module interception, MITM, and proxy/VPN connection";
|
||||||
|
"次数" = "Count";
|
||||||
|
"每次开启首次或判定变化" = "First sample after each start or classification change";
|
||||||
|
"水平精度米" = "Horizontal accuracy (m)";
|
||||||
|
"活动requestID" = "Active request ID";
|
||||||
|
"活动阶段" = "Active phase";
|
||||||
|
"海拔米" = "Altitude (m)";
|
||||||
|
"满足当前请求时间窗" = "Within current request time window";
|
||||||
|
"点击实时定位" = "Live-location action";
|
||||||
|
"目标所在区域" = "Target region";
|
||||||
|
"确认标准" = "Confirmed coordinate system";
|
||||||
|
"社区征集客户端数" = "Clients requesting community contributions";
|
||||||
|
"等待秒数" = "Wait duration (s)";
|
||||||
|
"系统缓存存在" = "System cache present";
|
||||||
|
"结果" = "Result";
|
||||||
|
"结果数" = "Result count";
|
||||||
|
"结果来源" = "Result source";
|
||||||
|
"缓存" = "Cache";
|
||||||
|
"缓存上限秒" = "Cache limit (s)";
|
||||||
|
"缓存时效通过" = "Cache freshness valid";
|
||||||
|
"缩放米" = "Zoom (m)";
|
||||||
|
"网络可用" = "Network available";
|
||||||
|
"耗时毫秒" = "Duration (ms)";
|
||||||
|
"蓝点回调更接近" = "User-location callback is closer";
|
||||||
|
"蓝点缓存" = "User-location cache";
|
||||||
|
"蓝点缓存存在" = "User-location cache present";
|
||||||
|
"蓝点距GCJ目标米" = "User-location distance to GCJ target (m)";
|
||||||
|
"蓝点距WGS目标米" = "User-location distance to WGS target (m)";
|
||||||
|
"触发原因" = "Trigger reason";
|
||||||
|
"诊断位置" = "Diagnostic location";
|
||||||
|
"请求动作" = "Request action";
|
||||||
|
"超时毫秒" = "Timeout (ms)";
|
||||||
|
"输入坐标标准" = "Input coordinate system";
|
||||||
|
"连接状态" = "Connection status";
|
||||||
|
"选点revision" = "Selection revision";
|
||||||
|
"选点期间发生变化" = "Selection changed during operation";
|
||||||
|
"错误" = "Error";
|
||||||
|
"错误类型" = "Error type";
|
||||||
|
"锚点" = "Anchor";
|
||||||
|
"阶段" = "Phase";
|
||||||
|
"首条名称" = "First result name";
|
||||||
|
"首条名称=林士街→GCJ-02,否则→WGS-84" = "First name=Lin Shi Street → GCJ-02; otherwise → WGS-84";
|
||||||
|
"默认国内标准" = "Default mainland-China coordinate system";
|
||||||
|
|
||||||
/* MARK: - ProxyManager */
|
/* MARK: - ProxyManager */
|
||||||
"证书下载地址无效" = "Invalid certificate download URL";
|
"证书下载地址无效" = "Invalid certificate download URL";
|
||||||
|
|||||||
@@ -13,8 +13,8 @@ enum CertificateAuthorityStoreError: LocalizedError {
|
|||||||
|
|
||||||
var errorDescription: String? {
|
var errorDescription: String? {
|
||||||
switch self {
|
switch self {
|
||||||
case .invalidAuthority: return "本地 CA 证书或私钥无效"
|
case .invalidAuthority: return String(localized: "本地 CA 证书或私钥无效")
|
||||||
case let .keychain(status): return "无法写入设备钥匙串(\(status))"
|
case let .keychain(status): return String(localized: "无法写入设备钥匙串(\(status))")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,8 +12,8 @@ enum CoreBridgeError: LocalizedError {
|
|||||||
|
|
||||||
var errorDescription: String? {
|
var errorDescription: String? {
|
||||||
switch self {
|
switch self {
|
||||||
case .generationFailed: return "无法生成本地证书"
|
case .generationFailed: return String(localized: "无法生成本地证书")
|
||||||
case .serverStartFailed: return "无法启动本地证书服务"
|
case .serverStartFailed: return String(localized: "无法启动本地证书服务")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+28
-3
@@ -38,10 +38,35 @@ struct RuntimeLogEntry: Codable, Identifiable, Equatable {
|
|||||||
var renderedText: String {
|
var renderedText: String {
|
||||||
let formatter = ISO8601DateFormatter()
|
let formatter = ISO8601DateFormatter()
|
||||||
formatter.formatOptions = [.withInternetDateTime, .withFractionalSeconds]
|
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: " ")
|
: " " + localizedDetails.sorted(by: { $0.0 < $1.0 }).map { "\($0.0)=\($0.1)" }.joined(separator: " ")
|
||||||
return "\(formatter.string(from: timestamp)) [\(source)] [\(level.rawValue.uppercased())] [\(category)] \(message)\(suffix)"
|
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)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -170,7 +170,10 @@ final class ThirdPartyProxyManager: ObservableObject {
|
|||||||
URLQueryItem(name: "lon", value: String(format: "%.8f", locale: Locale(identifier: "en_US_POSIX"), longitude)),
|
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: "lat", value: String(format: "%.8f", locale: Locale(identifier: "en_US_POSIX"), latitude)),
|
||||||
URLQueryItem(name: "acc", value: String(accuracy)),
|
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 }
|
guard let url = components.url else { throw ThirdPartyProxyError.invalidResponse }
|
||||||
@@ -203,6 +206,9 @@ final class ThirdPartyProxyManager: ObservableObject {
|
|||||||
}
|
}
|
||||||
|
|
||||||
enum ThirdPartyProxyClient: String, CaseIterable, Identifiable {
|
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 shadowrocket
|
||||||
case surge
|
case surge
|
||||||
case quantumultX
|
case quantumultX
|
||||||
@@ -239,21 +245,14 @@ enum ThirdPartyProxyClient: String, CaseIterable, Identifiable {
|
|||||||
|
|
||||||
@MainActor
|
@MainActor
|
||||||
var subscriptionURL: URL {
|
var subscriptionURL: URL {
|
||||||
// Third-party modules are served directly from the upstream Yu9191/wloc
|
let directory = ThirdPartyModuleSourceStore.shared.useMirror
|
||||||
// repository (mirror via gh-proxy when enabled). We no longer maintain
|
? "Resources/ThirdPartyProxyModules"
|
||||||
// project-owned module copies, so the URL tracks upstream releases.
|
: "ThirdParty/WlocScripts/modules/direct"
|
||||||
let fileName: String
|
let prefix = ThirdPartyModuleSourceStore.shared.useMirror
|
||||||
switch self {
|
? "https://gh-proxy.org/https://raw.githubusercontent.com/xweiba/location-spoofer/main/"
|
||||||
case .surge, .egern: fileName = "wloc.sgmodule"
|
: "https://raw.githubusercontent.com/xweiba/location-spoofer/main/"
|
||||||
case .quantumultX: fileName = "wloc.conf"
|
let base = "\(prefix)\(directory)/\(moduleFileName)"
|
||||||
case .loon: fileName = "wloc.lpx"
|
return URL(string: "\(base)?v=\(Self.moduleSubscriptionVersion)")!
|
||||||
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)!
|
|
||||||
}
|
}
|
||||||
|
|
||||||
var launchURL: URL? {
|
var launchURL: URL? {
|
||||||
|
|||||||
@@ -73,9 +73,13 @@ final class RealtimeLocationManagerTests: XCTestCase {
|
|||||||
func testOneShotTimeoutTransitionsToContinuousFallback() async {
|
func testOneShotTimeoutTransitionsToContinuousFallback() async {
|
||||||
let driver = FakeRealtimeLocationDriver()
|
let driver = FakeRealtimeLocationDriver()
|
||||||
let manager = RealtimeLocationManager(driver: driver, oneShotTimeoutNanoseconds: 5_000_000, fallbackTimeoutNanoseconds: 1_000_000_000)
|
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() }
|
let request = Task { await manager.requestLocation() }
|
||||||
try? await Task.sleep(nanoseconds: 20_000_000)
|
await fulfillment(of: [fallbackStarted], timeout: 1)
|
||||||
XCTAssertEqual(driver.startUpdatingCallCount, 1)
|
XCTAssertEqual(driver.startUpdatingCallCount, 1)
|
||||||
|
|
||||||
driver.emit(CLLocation(latitude: 39.90, longitude: 116.40))
|
driver.emit(CLLocation(latitude: 39.90, longitude: 116.40))
|
||||||
@@ -148,6 +152,7 @@ private final class FakeRealtimeLocationDriver: RealtimeLocationDriving {
|
|||||||
var authorizationStatus: CLAuthorizationStatus = .authorizedWhenInUse
|
var authorizationStatus: CLAuthorizationStatus = .authorizedWhenInUse
|
||||||
weak var delegate: CLLocationManagerDelegate?
|
weak var delegate: CLLocationManagerDelegate?
|
||||||
var onRequestLocation: (() -> Void)?
|
var onRequestLocation: (() -> Void)?
|
||||||
|
var onStartUpdating: (() -> Void)?
|
||||||
private(set) var requestAuthorizationCallCount = 0
|
private(set) var requestAuthorizationCallCount = 0
|
||||||
private(set) var requestLocationCallCount = 0
|
private(set) var requestLocationCallCount = 0
|
||||||
private(set) var startUpdatingCallCount = 0
|
private(set) var startUpdatingCallCount = 0
|
||||||
@@ -164,6 +169,7 @@ private final class FakeRealtimeLocationDriver: RealtimeLocationDriving {
|
|||||||
|
|
||||||
func startUpdatingLocation() {
|
func startUpdatingLocation() {
|
||||||
startUpdatingCallCount += 1
|
startUpdatingCallCount += 1
|
||||||
|
onStartUpdating?()
|
||||||
}
|
}
|
||||||
|
|
||||||
func stopUpdatingLocation() {
|
func stopUpdatingLocation() {
|
||||||
|
|||||||
@@ -125,16 +125,19 @@ final class ThirdPartyProxyManagerTests: XCTestCase {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func testClientLinksUseUpstreamModulesAndVerificationLabels() {
|
func testClientLinksUseProjectOwnedModulesAndVerificationLabels() {
|
||||||
XCTAssertEqual(
|
XCTAssertEqual(
|
||||||
ThirdPartyProxyManager.interceptionHostnamesText,
|
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"
|
"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)
|
XCTAssertNil(ThirdPartyProxyClient.shadowrocket.verificationText)
|
||||||
XCTAssertTrue(ThirdPartyProxyClient.surge.verificationText?.contains("尚未验证") == true)
|
XCTAssertEqual(
|
||||||
|
ThirdPartyProxyClient.surge.verificationText,
|
||||||
|
String(localized: "配置已提供,尚未验证")
|
||||||
|
)
|
||||||
XCTAssertEqual(ThirdPartyProxyClient.egern.subscriptionURL, ThirdPartyProxyClient.surge.subscriptionURL)
|
XCTAssertEqual(ThirdPartyProxyClient.egern.subscriptionURL, ThirdPartyProxyClient.surge.subscriptionURL)
|
||||||
XCTAssertTrue(ThirdPartyProxyClient.stash.subscriptionURL.absoluteString.hasPrefix(
|
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(
|
let stashComponents = URLComponents(
|
||||||
url: ThirdPartyProxyClient.stash.subscriptionURL,
|
url: ThirdPartyProxyClient.stash.subscriptionURL,
|
||||||
@@ -144,10 +147,10 @@ final class ThirdPartyProxyManagerTests: XCTestCase {
|
|||||||
url: ThirdPartyProxyClient.shadowrocket.subscriptionURL,
|
url: ThirdPartyProxyClient.shadowrocket.subscriptionURL,
|
||||||
resolvingAgainstBaseURL: false
|
resolvingAgainstBaseURL: false
|
||||||
)
|
)
|
||||||
XCTAssertEqual(stashComponents?.path, "/https://raw.githubusercontent.com/Yu9191/wloc/refs/heads/main/modules/wloc.stoverride")
|
XCTAssertEqual(stashComponents?.path, "/https://raw.githubusercontent.com/xweiba/location-spoofer/main/Resources/ThirdPartyProxyModules/wloc.stoverride")
|
||||||
XCTAssertEqual(shadowrocketComponents?.path, "/https://raw.githubusercontent.com/Yu9191/wloc/refs/heads/main/modules/wloc.module")
|
XCTAssertEqual(shadowrocketComponents?.path, "/https://raw.githubusercontent.com/xweiba/location-spoofer/main/Resources/ThirdPartyProxyModules/wloc.module")
|
||||||
XCTAssertTrue(stashComponents?.queryItems?.isEmpty ?? true)
|
XCTAssertEqual(stashComponents?.queryItems?.first?.value, ThirdPartyProxyClient.moduleSubscriptionVersion)
|
||||||
XCTAssertTrue(shadowrocketComponents?.queryItems?.isEmpty ?? true)
|
XCTAssertEqual(shadowrocketComponents?.queryItems?.first?.value, ThirdPartyProxyClient.moduleSubscriptionVersion)
|
||||||
XCTAssertEqual(ThirdPartyProxyClient.shadowrocket.launchURL?.scheme, "shadowrocket")
|
XCTAssertEqual(ThirdPartyProxyClient.shadowrocket.launchURL?.scheme, "shadowrocket")
|
||||||
XCTAssertEqual(ThirdPartyProxyClient.surge.launchURL?.scheme, "surge")
|
XCTAssertEqual(ThirdPartyProxyClient.surge.launchURL?.scheme, "surge")
|
||||||
XCTAssertEqual(ThirdPartyProxyClient.quantumultX.launchURL?.scheme, "quantumult-x")
|
XCTAssertEqual(ThirdPartyProxyClient.quantumultX.launchURL?.scheme, "quantumult-x")
|
||||||
|
|||||||
Executable
+58
@@ -0,0 +1,58 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
ROOT="$(cd "$(dirname "$0")/.." && pwd)"
|
||||||
|
STRINGS="$ROOT/Resources/en.lproj/Localizable.strings"
|
||||||
|
|
||||||
|
fail() {
|
||||||
|
echo "FAIL: $1" >&2
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
|
||||||
|
plutil -lint "$STRINGS" >/dev/null || fail "English localization file is invalid"
|
||||||
|
|
||||||
|
ruby - "$STRINGS" <<'RUBY' || exit 1
|
||||||
|
path = ARGV.fetch(0)
|
||||||
|
entries = File.readlines(path).map do |line|
|
||||||
|
match = line.match(/^"((?:\\.|[^"])*)"\s*=\s*"((?:\\.|[^"])*)";/)
|
||||||
|
[match[1], match[2]] if match
|
||||||
|
end.compact
|
||||||
|
|
||||||
|
duplicates = entries.group_by(&:first).select { |_key, values| values.length > 1 }.keys
|
||||||
|
abort "FAIL: duplicate English localization keys: #{duplicates.join(', ')}" unless duplicates.empty?
|
||||||
|
|
||||||
|
placeholder = /%(?:\d+\$)?(?:@|lld|ld|d|f)/
|
||||||
|
entries.each do |source, target|
|
||||||
|
source_count = source.scan(placeholder).length
|
||||||
|
target_count = target.scan(placeholder).length
|
||||||
|
abort "FAIL: placeholder mismatch for #{source.inspect}" unless source_count == target_count
|
||||||
|
end
|
||||||
|
RUBY
|
||||||
|
|
||||||
|
for key in \
|
||||||
|
'环境信息' \
|
||||||
|
'诊断日志' \
|
||||||
|
'======== 代理验证测试 ========' \
|
||||||
|
'======== 第三方代理连接检测 ========' \
|
||||||
|
'======== 第三方代理运行检测 ========' \
|
||||||
|
'第三方代理测试模式:模块连接成功;已保存坐标=%@' \
|
||||||
|
'成功:{\"success\":true,\"longitude\":113.0,\"latitude\":22.0,\"accuracy\":25}\n失败:{\"success\":false,\"error\":\"错误说明\"}'; do
|
||||||
|
grep -Fq "\"$key\" = " "$STRINGS" || fail "missing critical English localization: $key"
|
||||||
|
done
|
||||||
|
|
||||||
|
grep -q 'localizedCategory' "$ROOT/Shared/RuntimeLog.swift" \
|
||||||
|
|| fail "runtime log categories must be localized when rendered"
|
||||||
|
grep -q 'localizedDetailsText' "$ROOT/App/DiagnosticsView.swift" \
|
||||||
|
|| fail "runtime log details must be localized in the diagnostics UI"
|
||||||
|
grep -q 'String(localized: "诊断日志")' "$ROOT/App/BugReportView.swift" \
|
||||||
|
|| fail "generated bug reports must localize their diagnostic section"
|
||||||
|
grep -q 'log(" " + e.localizedMessage)' "$ROOT/App/SetupCoordinator.swift" \
|
||||||
|
|| fail "bug-report verification logs must render stored messages in the active language"
|
||||||
|
|
||||||
|
if grep -R -n 'raw.githubusercontent.com/Yu9191/wloc' \
|
||||||
|
"$ROOT/App" "$ROOT/Shared" "$ROOT/Resources/ThirdPartyProxyModules" \
|
||||||
|
"$ROOT/ThirdParty/WlocScripts/modules"; then
|
||||||
|
fail "deleted Yu9191/wloc repository must not remain a runtime dependency"
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "PASS: localization and diagnostic export contract"
|
||||||
@@ -10,8 +10,8 @@ CONTENT="$ROOT/App/ContentView.swift"
|
|||||||
SETUP="$ROOT/App/FirstSetupView.swift"
|
SETUP="$ROOT/App/FirstSetupView.swift"
|
||||||
SETTINGS="$ROOT/App/SettingsView.swift"
|
SETTINGS="$ROOT/App/SettingsView.swift"
|
||||||
|
|
||||||
grep -q 'return "APP模式"' "$MODE" || fail "APP mode display name is missing"
|
grep -q 'String(localized: "APP模式")' "$MODE" || fail "localized APP mode display name is missing"
|
||||||
grep -q 'return "第三方代理模式"' "$MODE" || fail "third-party 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 '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 '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"
|
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" \
|
grep -Fq '清除:GET ?action=clear' "$SETUP" \
|
||||||
|| fail "client integration guidance must document the clear action"
|
|| fail "client integration guidance must document the clear action"
|
||||||
|
|
||||||
grep -q 'Yu9191/wloc/refs/heads/main/modules' "$MANAGER" \
|
grep -q 'raw.githubusercontent.com/xweiba/location-spoofer/main' "$MANAGER" \
|
||||||
|| fail "third-party subscription must point at upstream Yu9191 modules"
|
|| 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.sgmodule' "$MANAGER" || fail "Surge/Egern module mapping is missing"
|
||||||
grep -q 'wloc.stoverride' "$MANAGER" || fail "Stash must use .stoverride directly"
|
grep -q 'wloc.stoverride' "$MANAGER" || fail "Stash must use .stoverride directly"
|
||||||
grep -q 'shadowrocket://' "$MANAGER" || fail "Shadowrocket launch URL is missing"
|
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"
|
|| fail "third-party coordinate sync failures must open the import guide"
|
||||||
grep -q '检测到第三方代理连接异常,请检查模块、MITM 和代理连接后重新检测' "$SETUP" \
|
grep -q '检测到第三方代理连接异常,请检查模块、MITM 和代理连接后重新检测' "$SETUP" \
|
||||||
|| fail "runtime repair must explain why the import guide opened"
|
|| 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"
|
|| fail "third-party failure details must render in one shared result area"
|
||||||
grep -Fq '当前客户端:\(client.name)' "$SETUP" \
|
grep -Fq '当前客户端:\(client.name)' "$SETUP" \
|
||||||
|| fail "third-party failure logs must identify the selected client"
|
|| 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:
|
with open(sys.argv[1], encoding="utf-8") as handle:
|
||||||
config = json.load(handle)
|
config = json.load(handle)
|
||||||
|
|
||||||
assert config["latestVersion"] == "1.0.5"
|
assert config["latestVersion"] == "1.0.6"
|
||||||
assert config["minimumSupportedVersion"] == "1.0.0"
|
assert config["minimumSupportedVersion"] == "1.0.0"
|
||||||
assert "shadowrocket" not in config["communityPromptClients"]
|
assert "shadowrocket" not in config["communityPromptClients"]
|
||||||
assert set(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"
|
|| fail "the App bug report must not be handed to an external GitHub client"
|
||||||
grep -Fq 'App 生成的诊断报告' "$BUG_REPORT" \
|
grep -Fq 'App 生成的诊断报告' "$BUG_REPORT" \
|
||||||
|| fail "the App must tell users where to paste the generated 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"
|
|| fail "the generated report must identify the selected third-party client"
|
||||||
grep -Fq 'Label("报告 Bug"' "$SETTINGS" \
|
grep -Fq 'Label("报告 Bug"' "$SETTINGS" \
|
||||||
|| fail "Settings must identify the support action as a bug report"
|
|| fail "Settings must identify the support action as a bug report"
|
||||||
|
|||||||
@@ -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.
|
||||||
Vendored
+19
@@ -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"
|
||||||
|
});
|
||||||
|
}
|
||||||
@@ -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"})}}})();
|
||||||
+6
File diff suppressed because one or more lines are too long
+11
@@ -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
@@ -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
|
||||||
@@ -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
|
||||||
@@ -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
|
||||||
@@ -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
@@ -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)"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Vendored
+16
@@ -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"
|
||||||
|
}
|
||||||
|
}
|
||||||
Vendored
+263
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -1,20 +1,26 @@
|
|||||||
# Third-party proxy modules
|
# Third-party proxy modules
|
||||||
|
|
||||||
Third-party proxy mode (Surge / Quantumult X / Loon / Shadowrocket / Stash /
|
The third-party proxy modules and WLOC scripts are maintained in this
|
||||||
Egern) now relies entirely on the upstream
|
repository. The original `Yu9191/wloc` repository was deleted, and its Raw
|
||||||
[Yu9191/wloc](https://github.com/Yu9191/wloc) modules. The App no longer
|
URLs now return HTTP 404, so it is retained only as provenance and is not a
|
||||||
maintains or ships project-owned module/script copies, so it never drifts
|
runtime dependency.
|
||||||
from the upstream protocol.
|
|
||||||
|
|
||||||
## Subscription addresses
|
## Hosted files
|
||||||
|
|
||||||
The App builds each client's module subscription URL directly from the
|
- Mirrored module subscriptions: `Resources/ThirdPartyProxyModules/`
|
||||||
upstream repository:
|
- 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):
|
The App appends `?v=1.0.7` to module subscription URLs, and every module uses
|
||||||
`https://gh-proxy.org/https://raw.githubusercontent.com/Yu9191/wloc/refs/heads/main/modules/<file>`
|
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:
|
- 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 |
|
| Module file | Client |
|
||||||
|---|---|
|
|---|---|
|
||||||
@@ -24,17 +30,36 @@ upstream repository:
|
|||||||
| `wloc.lpx` | Loon |
|
| `wloc.lpx` | Loon |
|
||||||
| `wloc.stoverride` | Stash |
|
| `wloc.stoverride` | Stash |
|
||||||
|
|
||||||
No `?v=` cache-bust is appended: the URL points at upstream's latest content,
|
The hosted files become downloadable from these URLs after this change is
|
||||||
and re-importing the subscription in the proxy client re-fetches it.
|
merged into `main`. Until then, validate them from the checked-out paths.
|
||||||
|
|
||||||
## Script protocol
|
## Script protocol
|
||||||
|
|
||||||
The upstream `wloc.js` patches Apple WLOC responses and reads coordinates from
|
`wloc.js` patches Apple WLOC responses and reads coordinates from the
|
||||||
the `wloc_settings` persistent key or the module `argument` config. The
|
`wloc_settings` persistent key or the module `argument` config.
|
||||||
upstream `wloc-settings.js` implements `wloc-settings/save` (query/clear/save)
|
`wloc-settings.js` implements `wloc-settings/save` (query/clear/save) using
|
||||||
using `lon`/`lat`/`acc`/`randomRadius` parameters.
|
`lon`/`lat`/`acc`/`randomRadius` parameters.
|
||||||
|
|
||||||
The App's third-party save sends `lon`/`lat`/`acc`, matching the upstream
|
The App's third-party save sends `lon`/`lat`/`acc`. Motion-state simulation
|
||||||
script. Motion-state simulation (fields 11/12) is **not** implemented by the
|
(fields 11/12) is unavailable in third-party mode; it remains available in
|
||||||
upstream scripts and is unavailable in third-party mode; it remains available
|
App Mode through the built-in proxy.
|
||||||
in APP mode (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.
|
||||||
|
|||||||
Reference in New Issue
Block a user