V1.5.3: 远程日志不再静默失效(+本地日志文件);补 403 受限识别
现场两个问题分别定位: 一、"歌曲源全部失效" —— 已确认是**源站对该出口 IP 的授权/风控**,非客户端可修 - 实测(本机与设备同一出口 188.253.112.231): · 主源 http://w.w345.my/i.php → **HTTP 403 Forbidden**(nginx 拦截页); · 镜像源(desktok.top / mm.kk456.top / w.w345.my:66 / z.w345.my / b.sktv.top / w345.my) 返回 200,但对**每一首歌(含不存在的编号 1111111/9999999)都返回同一个广告/测试资源** https://wb66.cn/f/d/7b7c9/10188555.ts; · 换全新随机设备身份(mac/sn)结果完全相同 → 限制在**出口 IP** 层面,与设备无关; · 7 host × ls=1~3 × (720/1080 × h265 0/1) 全组合探测:0 个真源; · 旧的试用领取接口 api.php 已 **404**,App 无法自行领取窗口。 - 结论:源站侧无有效授权(i.php 的 ordertype=0、mwsLogin 的 license_box_count=0 / license_duration=0 均指向此)。**需更换公网 IP 或与歌曲源服务方确认续订**。 我们拒绝这些地址是正确行为(否则就是 1.4.10 修过的"广告被当成歌曲播放")。 二、"手动更新数据库后远程日志失效" —— 数据库没问题,日志客户端有隐患,两者都已处理 - 实测新曲库 day=261003(md5 300AFD957BF332F02C17ED94A9F4E4CF,431,530,367 字节, 解压 1.01GB、69.5 万首): 用 App 的**真实 SQL**(SONG_SELECT/推荐/中文搜索/拼音搜索/ 歌手检索/歌单/语种/歌手列表)逐条跑通 → **结构完全兼容,更新曲库不会破坏 App**。 - 日志侧修复(原来确实可能"静默失效"): · MiliLog.init 改为**可重复调用并自动重启发送线程**(旧实现 `if (started.getAndSet(true)) return`, 线程一旦退出就永久不再发送且无任何迹象); · sendLoop 整体加 catch(Throwable) 兜底,任何异常都不允许发送线程退出; · 新增**本地日志文件** <存储根>/logs/ktv.log(>1MB 滚动为 .prev), 远程日志不可用时现场可直接取文件;启动打 APP-INIT 行给出路径; · MiliLog.init 提前到 KtvApplication.onCreate,早于 MainActivity,早期崩溃也有日志。 - 补 403 识别: KtvJsBridge 的 403 判定从仅 /music/do.php 扩展到 /i.php 也认 (主源 i.php 就是 403);SongUrlResolver 记录本轮是否见过 403,全部失败且见过 403 时 同样走 Eac19WindowGuard 提示换 IP。 AdVideoFilter: 仅保留高区分度标记 wb66,去掉过于宽松的 demo(避免误伤真源 URL 里的随机 token)。 版本 1.5.2(34) → 1.5.3(35);签名证书不变。测试 9 例全通过。
This commit is contained in:
@@ -165,7 +165,7 @@ $BT/apksigner verify 目标名.apk
|
|||||||
|
|
||||||
### 版本号
|
### 版本号
|
||||||
|
|
||||||
- `app/build.gradle`:`versionName "1.5.2"` / `versionCode 34`(18~21 被本机临时诊断包占用过)
|
- `app/build.gradle`:`versionName "1.5.3"` / `versionCode 35`(18~21 被本机临时诊断包占用过)
|
||||||
- App 内显示统一用纯版本号(不加 `(versionCode)` 括号)
|
- App 内显示统一用纯版本号(不加 `(versionCode)` 括号)
|
||||||
- ⚠️ **发布构建已开启 NewApi 闸门**(`lint { checkReleaseBuilds true; fatal += ['NewApi'] }`):
|
- ⚠️ **发布构建已开启 NewApi 闸门**(`lint { checkReleaseBuilds true; fatal += ['NewApi'] }`):
|
||||||
任何「调用了高于 `minSdk 21` 且未做版本守卫的 API」都会让 `assembleRelease` 直接失败。
|
任何「调用了高于 `minSdk 21` 且未做版本守卫的 API」都会让 `assembleRelease` 直接失败。
|
||||||
@@ -357,6 +357,15 @@ $BT/apksigner verify 目标名.apk
|
|||||||
5. **状态样本 + 事件**缺一不可:事件(点歌/失败)说明"发生了什么",但排查"随时间变化"的
|
5. **状态样本 + 事件**缺一不可:事件(点歌/失败)说明"发生了什么",但排查"随时间变化"的
|
||||||
问题(空间变少、曲库丢失、长期离线)需要状态样本 —— 现由 6 小时一条 `HEALTH ...`
|
问题(空间变少、曲库丢失、长期离线)需要状态样本 —— 现由 6 小时一条 `HEALTH ...`
|
||||||
(版本/在线/可用空间/曲库大小/根目录)提供。
|
(版本/在线/可用空间/曲库大小/根目录)提供。
|
||||||
|
- ⚠️ **远程日志绝不能静默失效**(2026-10 现场反馈"手动更新曲库后远程日志失效了"):
|
||||||
|
1. `MiliLog.init()` **可以重复调用并会自动重启发送线程**(旧实现是
|
||||||
|
`if (started.getAndSet(true)) return`,一旦线程退出就永久不再发送,且无从察觉);
|
||||||
|
2. `sendLoop` 整体包了 `catch (Throwable)` 兜底 —— 任何异常都不允许让发送线程退出;
|
||||||
|
3. **本地留档**:每行同时写入 `<存储根>/logs/ktv.log`(超 1MB 滚动为 `.prev`)。
|
||||||
|
远程日志不可用时,让现场直接把该文件发出来即可取证;启动时会打一行
|
||||||
|
`APP-INIT ... logFile=<路径>` 指明位置;
|
||||||
|
4. `MiliLog.init` 已提到 **`KtvApplication.onCreate`**(早于 `MainActivity`),
|
||||||
|
这样即使更早的初始化抛异常,也已经有日志可查。
|
||||||
- 一段典型现场日志应为:`BOOT` → `STORAGE` → (点歌)`点歌编号=... 请求源=... 状态=成功 urlHost=...`
|
- 一段典型现场日志应为:`BOOT` → `STORAGE` → (点歌)`点歌编号=... 请求源=... 状态=成功 urlHost=...`
|
||||||
→ `PLAY ... src=local` → 每 6h `HEALTH ...`;异常时才有 `RAW-ERR`/`NET-ERR`/`NATIVE-SONG ...失败`。
|
→ `PLAY ... src=local` → 每 6h `HEALTH ...`;异常时才有 `RAW-ERR`/`NET-ERR`/`NATIVE-SONG ...失败`。
|
||||||
- **网络状态与断网恢复**:`KtvNetworkMonitor`(object,走 `ConnectivityManager` 回调、不轮询)
|
- **网络状态与断网恢复**:`KtvNetworkMonitor`(object,走 `ConnectivityManager` 回调、不轮询)
|
||||||
@@ -387,6 +396,14 @@ $BT/apksigner verify 目标名.apk
|
|||||||
并调用 `Eac19WindowGuard.noteRiskBlocked()`(弹「歌曲源临时受限」引导重启光猫/路由器换 IP),
|
并调用 `Eac19WindowGuard.noteRiskBlocked()`(弹「歌曲源临时受限」引导重启光猫/路由器换 IP),
|
||||||
同时进入 3 分钟冷却(`BLOCKED_COOLDOWN_MS`)避免继续冲击源站;拿到真源立即解除。
|
同时进入 3 分钟冷却(`BLOCKED_COOLDOWN_MS`)避免继续冲击源站;拿到真源立即解除。
|
||||||
- 现场处置:**更换公网出口 IP**(重启光猫/路由器重拨)即恢复;已下载的歌曲不受影响照常播。
|
- 现场处置:**更换公网出口 IP**(重启光猫/路由器重拨)即恢复;已下载的歌曲不受影响照常播。
|
||||||
|
- **403 也是受限信号**:2026-10 实测**主源 `w.w345.my/i.php` 直接返回 HTTP 403**、
|
||||||
|
镜像源返回 200 但只给固定广告文件 —— 两种形态会同时出现。因此
|
||||||
|
`KtvJsBridge.request` 的 403 判定已从"仅 `/music/do.php`"扩展到 **`/i.php` 也认**,
|
||||||
|
`SongUrlResolver` 也记录本轮是否见过 403(全部失败且见过 403 → 同样提示换 IP)。
|
||||||
|
- **旧的试用领取接口 `api.php` 已 404**(源站撤了),因此 App 无法自行"领取"试用窗口。
|
||||||
|
若换公网 IP 后仍只发广告,基本可判定为**服务/授权层面**问题(`i.php` 的
|
||||||
|
`ordertype=0`、`mwsLogin` 的 `license_box_count=0`/`license_duration=0` 都指向"无有效授权"),
|
||||||
|
需与歌曲源服务方确认续订,**不是客户端能修的**。
|
||||||
- ⚠️ 排查这类问题时**不要用生产身份/出口 IP 做密集探测**:源站按出口 IP 限流,
|
- ⚠️ 排查这类问题时**不要用生产身份/出口 IP 做密集探测**:源站按出口 IP 限流,
|
||||||
短时间内大量 `i.php`/`do.php`(多 host × 多参数组合)会加重受限,
|
短时间内大量 `i.php`/`do.php`(多 host × 多参数组合)会加重受限,
|
||||||
把"能用"变成"只发广告"。少量、间隔式验证即可。
|
把"能用"变成"只发广告"。少量、间隔式验证即可。
|
||||||
|
|||||||
+2
-2
@@ -11,8 +11,8 @@ android {
|
|||||||
applicationId "com.local.ktv"
|
applicationId "com.local.ktv"
|
||||||
minSdk 21
|
minSdk 21
|
||||||
targetSdk 25
|
targetSdk 25
|
||||||
versionCode 34
|
versionCode 35
|
||||||
versionName "1.5.2"
|
versionName "1.5.3"
|
||||||
ndk {
|
ndk {
|
||||||
// 双 ABI 打包:兼容 64 位(Exynos 1580 等 arm64-v8a)与 32 位设备(极米 Play 超越版
|
// 双 ABI 打包:兼容 64 位(Exynos 1580 等 arm64-v8a)与 32 位设备(极米 Play 超越版
|
||||||
// 的 Amlogic 950X2/S905X2 采用 32 位 Android 用户态,必须包含 armeabi-v7a,否则
|
// 的 Amlogic 950X2/S905X2 采用 32 位 Android 用户态,必须包含 armeabi-v7a,否则
|
||||||
|
|||||||
@@ -22,8 +22,8 @@ object AdVideoFilter {
|
|||||||
// 源站 2026-10 起"授权窗口失效"时会对每首歌返回同一个测试资源
|
// 源站 2026-10 起"授权窗口失效"时会对每首歌返回同一个测试资源
|
||||||
// (实测 https://wb66.cn/f/d/7b7c9/10188555.ts)。JS 的 isDemoUrl 早就把 wb66 当
|
// (实测 https://wb66.cn/f/d/7b7c9/10188555.ts)。JS 的 isDemoUrl 早就把 wb66 当
|
||||||
// 测试资源,这里对齐,保证两条取址路径判定一致。
|
// 测试资源,这里对齐,保证两条取址路径判定一致。
|
||||||
|
// ⚠️ 只收"wb66"这种高区分度标记:`demo` 之类太宽松的词会误伤真源 URL 里的随机 token。
|
||||||
"wb66",
|
"wb66",
|
||||||
"demo",
|
|
||||||
)
|
)
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -26,6 +26,13 @@ class KtvApplication : Application() {
|
|||||||
override fun onCreate() {
|
override fun onCreate() {
|
||||||
super.onCreate()
|
super.onCreate()
|
||||||
instanceRef = this
|
instanceRef = this
|
||||||
|
// 远程日志尽早启动:以前只在 MainActivity.onCreate 里 init,
|
||||||
|
// 一旦更早的初始化抛异常就直接崩溃、**一条日志都没有**,现场无从排查。
|
||||||
|
// init 现在可重复调用且会自动重启发送线程,这里是"最靠前"的启动点。
|
||||||
|
runCatching {
|
||||||
|
MiliLog.init(this)
|
||||||
|
MiliLog.info("APP-INIT 进程启动 ver=${BuildConfig.VERSION_NAME} logFile=${MiliLog.localLogPath()}")
|
||||||
|
}.onFailure { Log.e(TAG, "MiliLog.init failed", it) }
|
||||||
OkDownload.setSingletonInstance(OkDownload.Builder(this).build())
|
OkDownload.setSingletonInstance(OkDownload.Builder(this).build())
|
||||||
Log.i(TAG, "KTV application initialized without native services")
|
Log.i(TAG, "KTV application initialized without native services")
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -486,7 +486,9 @@ class KtvJsBridge(context: Context) {
|
|||||||
}
|
}
|
||||||
// 歌曲源试用窗口/风控到期: 对陌生出口 IP 可能返回 403"检测到异常行为",
|
// 歌曲源试用窗口/风控到期: 对陌生出口 IP 可能返回 403"检测到异常行为",
|
||||||
// 换公网 IP 恢复。通知 UI 弹一次提示引导换 IP。
|
// 换公网 IP 恢复。通知 UI 弹一次提示引导换 IP。
|
||||||
if (status == 403 && url.contains("/music/do.php")) {
|
// ⚠️ 取 token 的 `/i.php` 同样会 403(2026-10 实测主源 w.w345.my/i.php 就是 403)——
|
||||||
|
// 以前只认 `/music/do.php`,全都 403 时用户看不到任何提示。
|
||||||
|
if (status == 403 && (url.contains("/i.php") || url.contains("/music/do.php"))) {
|
||||||
Eac19WindowGuard.noteRiskBlocked()
|
Eac19WindowGuard.noteRiskBlocked()
|
||||||
}
|
}
|
||||||
return raw
|
return raw
|
||||||
|
|||||||
@@ -55,12 +55,20 @@ object MiliLog {
|
|||||||
private const val RETRY_SLEEP_MS = 1500L
|
private const val RETRY_SLEEP_MS = 1500L
|
||||||
private const val FAILURE_BACKOFF_MS = 10_000L
|
private const val FAILURE_BACKOFF_MS = 10_000L
|
||||||
|
|
||||||
|
/** 本地日志文件:远程日志一旦不可用(网络/服务端问题),现场仍能取证。 */
|
||||||
|
private const val LOCAL_LOG_DIR = "logs"
|
||||||
|
private const val LOCAL_LOG_NAME = "ktv.log"
|
||||||
|
private const val LOCAL_LOG_MAX_BYTES = 1024L * 1024L
|
||||||
|
|
||||||
private val started = AtomicBoolean(false)
|
private val started = AtomicBoolean(false)
|
||||||
private val running = AtomicBoolean(false)
|
private val running = AtomicBoolean(false)
|
||||||
private val publicIpFetching = AtomicBoolean(false)
|
private val publicIpFetching = AtomicBoolean(false)
|
||||||
private val queue = LinkedBlockingQueue<String>(MAX_QUEUE)
|
private val queue = LinkedBlockingQueue<String>(MAX_QUEUE)
|
||||||
private var senderThread: Thread? = null
|
private var senderThread: Thread? = null
|
||||||
|
|
||||||
|
/** 本地日志文件(后台线程写入,绝不占用主线程 I/O)。 */
|
||||||
|
@Volatile private var localLogFile: java.io.File? = null
|
||||||
|
|
||||||
@Volatile private var serverName: String = "android"
|
@Volatile private var serverName: String = "android"
|
||||||
|
|
||||||
/** 主机 IP:公网出口 IP([PUBLIC_IP_URL] 查询,5 分钟缓存)。 */
|
/** 主机 IP:公网出口 IP([PUBLIC_IP_URL] 查询,5 分钟缓存)。 */
|
||||||
@@ -74,13 +82,50 @@ object MiliLog {
|
|||||||
|
|
||||||
/** 用 app context 初始化并启动发送线程。可重复调用。 */
|
/** 用 app context 初始化并启动发送线程。可重复调用。 */
|
||||||
fun init(context: Context) {
|
fun init(context: Context) {
|
||||||
if (started.getAndSet(true)) return
|
// 首次初始化时准备本地日志文件。
|
||||||
|
if (localLogFile == null) {
|
||||||
|
runCatching {
|
||||||
|
val dir = java.io.File(AppPaths.root, LOCAL_LOG_DIR)
|
||||||
|
if (dir.exists() || dir.mkdirs()) localLogFile = java.io.File(dir, LOCAL_LOG_NAME)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!started.getAndSet(true)) {
|
||||||
serverName = sanitize(Build.MODEL).ifEmpty { "android" }
|
serverName = sanitize(Build.MODEL).ifEmpty { "android" }
|
||||||
|
}
|
||||||
|
// ⚠️ 只要**没有存活的发送线程**就(重新)启动它。
|
||||||
|
// 旧实现是 `if (started.getAndSet(true)) return`:一旦发送线程因任何原因退出
|
||||||
|
// (例如有人调用过 [shutdown],或线程意外终止),后续 init 全是空操作,
|
||||||
|
// 日志只入队、永远不再发送 —— 现场表现就是「远程日志失效」,而且无从察觉。
|
||||||
|
val alive = senderThread?.isAlive == true
|
||||||
|
if (running.get() && alive) {
|
||||||
|
ensureServerIp()
|
||||||
|
return
|
||||||
|
}
|
||||||
running.set(true)
|
running.set(true)
|
||||||
senderThread = Thread({ sendLoop() }, "mili-glog").also { it.isDaemon = true; it.start() }
|
senderThread = Thread({ sendLoop() }, "mili-glog").also { it.isDaemon = true; it.start() }
|
||||||
|
Log.i(TAG, "glog sender (re)started")
|
||||||
ensureServerIp()
|
ensureServerIp()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** 本地日志文件(供现场取证);未初始化时返回 null。 */
|
||||||
|
fun localLogPath(): String? = localLogFile?.absolutePath
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 追加一行到本地日志(**由发送线程调用**,不在调用方线程做磁盘 I/O)。
|
||||||
|
* 超过 [LOCAL_LOG_MAX_BYTES] 就滚动一次,保证文件不会无限增长。
|
||||||
|
*/
|
||||||
|
private fun appendLocal(line: String) {
|
||||||
|
val file = localLogFile ?: return
|
||||||
|
runCatching {
|
||||||
|
if (file.length() > LOCAL_LOG_MAX_BYTES) {
|
||||||
|
val prev = java.io.File(file.parentFile, "$LOCAL_LOG_NAME.prev")
|
||||||
|
prev.delete()
|
||||||
|
file.renameTo(prev)
|
||||||
|
}
|
||||||
|
file.appendText(line + "\n", Charsets.UTF_8)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 更新上报用的客户端网络/身份字段。
|
* 更新上报用的客户端网络/身份字段。
|
||||||
*
|
*
|
||||||
@@ -232,11 +277,14 @@ object MiliLog {
|
|||||||
private fun sendLoop() {
|
private fun sendLoop() {
|
||||||
var consecutiveFailures = 0
|
var consecutiveFailures = 0
|
||||||
while (running.get()) {
|
while (running.get()) {
|
||||||
|
try {
|
||||||
val body: String = try {
|
val body: String = try {
|
||||||
queue.take()
|
queue.take()
|
||||||
} catch (e: InterruptedException) {
|
} catch (e: InterruptedException) {
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
|
// 本地留一份(远程日志不可用时,现场仍可取证)。
|
||||||
|
appendLocal(body)
|
||||||
var sent = post(body)
|
var sent = post(body)
|
||||||
if (!sent) {
|
if (!sent) {
|
||||||
sleepQuietly(RETRY_SLEEP_MS)
|
sleepQuietly(RETRY_SLEEP_MS)
|
||||||
@@ -247,6 +295,7 @@ object MiliLog {
|
|||||||
// 顺手把已积压的日志发完。
|
// 顺手把已积压的日志发完。
|
||||||
var next: String? = queue.poll()
|
var next: String? = queue.poll()
|
||||||
while (next != null) {
|
while (next != null) {
|
||||||
|
appendLocal(next)
|
||||||
post(next)
|
post(next)
|
||||||
next = queue.poll()
|
next = queue.poll()
|
||||||
}
|
}
|
||||||
@@ -258,6 +307,11 @@ object MiliLog {
|
|||||||
sleepQuietly(FAILURE_BACKOFF_MS)
|
sleepQuietly(FAILURE_BACKOFF_MS)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
} catch (t: Throwable) {
|
||||||
|
// 兜底:发送线程**绝不能因为任何异常退出**,否则远程日志会静默失效。
|
||||||
|
Log.w(TAG, "glog loop error: ${t.javaClass.simpleName}: ${t.message}")
|
||||||
|
sleepQuietly(1000L)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -63,6 +63,9 @@ object SongUrlResolver {
|
|||||||
|
|
||||||
@Volatile private var sn: String = KtvJsBridge.DEVICE_SN
|
@Volatile private var sn: String = KtvJsBridge.DEVICE_SN
|
||||||
|
|
||||||
|
/** 本轮取址是否遇到过 HTTP 403(源站风控的直接信号)。 */
|
||||||
|
@Volatile private var sawForbidden = false
|
||||||
|
|
||||||
/** 被判定为"源站只发广告/测试资源"后的冷却截止时间(0 = 未受限)。 */
|
/** 被判定为"源站只发广告/测试资源"后的冷却截止时间(0 = 未受限)。 */
|
||||||
@Volatile private var blockedUntil: Long = 0L
|
@Volatile private var blockedUntil: Long = 0L
|
||||||
|
|
||||||
@@ -121,6 +124,7 @@ object SongUrlResolver {
|
|||||||
)
|
)
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
sawForbidden = false
|
||||||
val mac = runCatching { DeviceMac.effective() }.getOrDefault(sn)
|
val mac = runCatching { DeviceMac.effective() }.getOrDefault(sn)
|
||||||
val startedAt = System.currentTimeMillis()
|
val startedAt = System.currentTimeMillis()
|
||||||
val hosts = synchronized(hostOrder) { hostOrder.toList() }
|
val hosts = synchronized(hostOrder) { hostOrder.toList() }
|
||||||
@@ -183,7 +187,15 @@ object SongUrlResolver {
|
|||||||
)
|
)
|
||||||
Eac19WindowGuard.noteRiskBlocked()
|
Eac19WindowGuard.noteRiskBlocked()
|
||||||
} else {
|
} else {
|
||||||
MiliLog.error("NATIVE-SONG 全部源失败 编号=$musicno 详情=$lastDetail 用时=${elapsed}ms")
|
MiliLog.error(
|
||||||
|
"NATIVE-SONG 全部源失败 编号=$musicno 详情=$lastDetail 用时=${elapsed}ms saw403=$sawForbidden",
|
||||||
|
)
|
||||||
|
// 全都 403 = 源站明确风控(另一种受限形态,与"只发广告"并列),同样引导换公网 IP。
|
||||||
|
if (sawForbidden) {
|
||||||
|
blockedUntil = System.currentTimeMillis() + BLOCKED_COOLDOWN_MS
|
||||||
|
MiliLog.error("SOURCE-BLOCKED 源站返回 HTTP 403(风控/授权窗口失效)编号=$musicno")
|
||||||
|
Eac19WindowGuard.noteRiskBlocked()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
@@ -231,6 +243,9 @@ object SongUrlResolver {
|
|||||||
val envelope = runCatching { JSONObject(raw) }.getOrNull() ?: return null
|
val envelope = runCatching { JSONObject(raw) }.getOrNull() ?: return null
|
||||||
val status = envelope.optInt("status")
|
val status = envelope.optInt("status")
|
||||||
if (status !in 200..299) {
|
if (status !in 200..299) {
|
||||||
|
// 403 = 源站风控的直接信号(2026-10 实测主源 w.w345.my/i.php 就是 403)。
|
||||||
|
// 记下来,全部源都失败时据此走 Eac19WindowGuard 的"换公网 IP"提示。
|
||||||
|
if (status == 403) sawForbidden = true
|
||||||
MiliLog.warn(
|
MiliLog.warn(
|
||||||
"NATIVE-SONG HTTP $status ${runCatching { java.net.URI(url).host }.getOrNull()} " +
|
"NATIVE-SONG HTTP $status ${runCatching { java.net.URI(url).host }.getOrNull()} " +
|
||||||
"err=${envelope.optString("error").take(80)}",
|
"err=${envelope.optString("error").take(80)}",
|
||||||
|
|||||||
Reference in New Issue
Block a user