This commit is contained in:
2026-10-29 16:36:43 +08:00
parent 0cf352cabb
commit 2582f9ba3e
11 changed files with 428 additions and 49 deletions
@@ -15,6 +15,7 @@ data class CaptureProfileStep(
val path: String,
val fileName: String,
var postAction: suspend (CaptureResult) -> Unit = {},
val uploadType: String? = null,
)
data class CaptureResult(
@@ -1,7 +1,6 @@
package net.bluemangoo.indpjsk.data
import android.content.Context
import android.content.pm.ApplicationInfo
import android.graphics.drawable.Drawable
data class AppEntry(
@@ -16,10 +15,6 @@ object InstalledApps {
val pm = context.packageManager
return pm.getInstalledApplications(0)
.asSequence()
.filter { info ->
(info.flags and ApplicationInfo.FLAG_SYSTEM) == 0 ||
(info.flags and ApplicationInfo.FLAG_UPDATED_SYSTEM_APP) != 0
}
.mapNotNull { info ->
runCatching {
AppEntry(
@@ -0,0 +1,168 @@
package net.bluemangoo.indpjsk.data
import net.bluemangoo.indpjsk.upload.UploadMixin
import net.bluemangoo.indpjsk.upload.UploadProfileCustom
import net.bluemangoo.indpjsk.upload.UploadStore
import net.bluemangoo.indpjsk.upload.Uploader
object MixinProfiles {
fun merged(): List<CaptureProfile> {
val accs = collect()
if (accs.isEmpty()) return PredefinedProfiles.all
val builtIns = PredefinedProfiles.all
val builtInNames = builtIns.map { it.name }.toSet()
val usedProfileIds = builtIns.map { it.id }.toMutableSet()
val mergedBuiltIns = builtIns.map { base ->
val acc = accs[base.name] ?: return@map base
mergeInto(base, acc)
}
val mixinOnly = accs.values
.filter { it.name !in builtInNames }
.mapNotNull { acc ->
toProfile(acc, uniqueKey(usedProfileIds, acc.name, sanitize = true))
}
return mergedBuiltIns + mixinOnly
}
fun validate(mixin: UploadMixin): String? {
for ((name, steps) in mixin.profiles) {
if (steps.isEmpty()) {
return "配置「${name}」没有 steps"
}
for ((name1, hosts, target) in steps) {
if (target !in PredefinedProfiles.stepTargets &&
runCatching { Regex(target) }.isFailure
) {
return "步骤「${name1}」的 target 既不是内置类型,也不是合法的路径正则"
}
if (hosts.isEmpty()) {
return "步骤「${name1}」的 hosts 不能为空"
}
}
}
return null
}
private fun collect(): LinkedHashMap<String, ProfileAcc> {
val profiles = linkedMapOf<String, ProfileAcc>()
for ((_, profile) in UploadStore.load()) {
val custom = profile as? UploadProfileCustom ?: continue
val mixin = custom.mixin ?: continue
for ((name, steps) in mixin.profiles) {
val key = canonicalName(name)
if (key.isBlank()) continue
val profileAcc = profiles.getOrPut(key) { ProfileAcc(key) }
for ((name1, hosts, target1) in steps) {
val target = resolveTarget(target1) ?: continue
val stepKey = name1.trim().lowercase()
if (stepKey.isEmpty()) continue
val stepAcc = profileAcc.steps.getOrPut(stepKey) {
StepAcc(name1.trim(), target)
}
for (host in expandHosts(hosts)) {
if (stepAcc.hosts.none { it.equals(host, ignoreCase = true) }) {
stepAcc.hosts.add(host)
}
}
stepAcc.uploadProfileIds.add(custom.id)
}
}
}
return profiles
}
private fun mergeInto(base: CaptureProfile, acc: ProfileAcc): CaptureProfile {
val builtInStepKeys = base.steps.map { it.name.trim().lowercase() }.toSet()
val usedFileNames = base.steps.map { it.fileName }.toMutableSet()
val usedIds = base.steps.map { it.id }.toMutableSet()
val steps = base.steps.map { step ->
val extra = acc.steps[step.name.trim().lowercase()] ?: return@map step
step.copy(hosts = (step.hosts + extra.hosts).distinctBy { it.lowercase() })
} + acc.steps.values
.filter { it.name.trim().lowercase() !in builtInStepKeys }
.mapNotNull { buildStep(it, usedFileNames, usedIds) }
return base.copy(steps = steps)
}
private fun toProfile(acc: ProfileAcc, id: String): CaptureProfile? {
val usedFileNames = mutableSetOf<String>()
val usedIds = mutableSetOf<String>()
val steps = acc.steps.values.mapNotNull { buildStep(it, usedFileNames, usedIds) }
if (steps.isEmpty()) return null
return CaptureProfile(id = id, name = acc.name, steps = steps)
}
private fun buildStep(
step: StepAcc,
usedFileNames: MutableSet<String>,
usedIds: MutableSet<String>,
): CaptureProfileStep? {
if (step.hosts.isEmpty()) return null
val uploadIds = step.uploadProfileIds.toSet()
return CaptureProfileStep(
id = uniqueKey(usedIds, step.name, sanitize = false),
name = step.name,
hosts = step.hosts.toList(),
path = step.target.path,
fileName = uniqueKey(usedFileNames, step.name, sanitize = true),
postAction = { result -> Uploader.tryUpload(result, uploadIds) },
uploadType = step.target.uploadType,
)
}
private fun resolveTarget(raw: String): PredefinedProfiles.StepTarget? {
PredefinedProfiles.stepTargets[raw]?.let { return it }
if (raw.isBlank()) return null
return runCatching { Regex(raw) }
.map { PredefinedProfiles.StepTarget(path = raw, uploadType = raw) }
.getOrNull()
}
private fun expandHosts(hosts: List<String>): List<String> =
hosts.flatMap { host ->
PredefinedProfiles.hostAliases[host.trim().lowercase()] ?: listOf(host)
}.map { it.trim() }.filter { it.isNotEmpty() }
private fun canonicalName(name: String): String =
PredefinedProfiles.all
.firstOrNull {
it.name.equals(name, ignoreCase = true) || it.id.equals(name, ignoreCase = true)
}
?.name
?: name
private fun uniqueKey(used: MutableSet<String>, base: String, sanitize: Boolean): String {
val clean = if (sanitize) {
base.filter { it.isLetterOrDigit() || it == '_' || it == '-' }.ifEmpty { "step" }
} else {
base.ifBlank { "step" }
}
var candidate = clean
var index = 2
while (!used.add(candidate)) {
candidate = "${clean}_$index"
index++
}
return candidate
}
private class ProfileAcc(val name: String) {
val steps = linkedMapOf<String, StepAcc>()
}
private class StepAcc(
val name: String,
val target: PredefinedProfiles.StepTarget,
) {
val hosts = mutableListOf<String>()
val uploadProfileIds = linkedSetOf<String>()
}
}
@@ -6,6 +6,25 @@ object PredefinedProfiles {
private const val SUITE_PATH = "/api/suite/user/(\\d+)(\\?isLogin=true)?$"
private const val MYSEKAI_PATH =
"/api/user/(\\d+)/mysekai\\?isForceAllReloadOnlyMysekai=True"
private const val MYSEKAI_BIRTHDAY_PATH =
"/api/user/(\\d+)/mysekai/birthday-party/(\\d+)/delivery"
data class StepTarget(val path: String, val uploadType: String)
val stepTargets: Map<String, StepTarget> = mapOf(
"suite" to StepTarget(SUITE_PATH, "suite"),
"mysekai" to StepTarget(MYSEKAI_PATH, "mysekai"),
"birthday" to StepTarget(MYSEKAI_BIRTHDAY_PATH, "birthday"),
)
val hostAliases: Map<String, List<String>>
get() = mapOf(
"cn" to CN_HOSTS,
"jp" to JP_HOSTS,
"en" to EN_HOSTS,
"tw" to TW_HOSTS,
"kr" to KR_HOSTS
)
private fun suiteStep(hosts: List<String>) = CaptureProfileStep(
id = "suite",
@@ -33,7 +52,7 @@ object PredefinedProfiles {
id = "mysekai_birthday",
name = "烤森生日双叶",
hosts = hosts,
path = "/api/user/(\\d+)/mysekai/birthday-party/(\\d+)/delivery",
path = MYSEKAI_BIRTHDAY_PATH,
fileName = "mysekai_birthday",
postAction = { result ->
Uploader.tryUpload(result)
@@ -108,5 +127,6 @@ object PredefinedProfiles {
),
)
fun byId(id: String): CaptureProfile? = all.firstOrNull { it.id == id }
fun byId(id: String): CaptureProfile? =
MixinProfiles.merged().firstOrNull { it.id == id }
}