feat(partnerctl-module): load external modules from remote registry index

This commit is contained in:
2026-05-12 23:34:35 +08:00
parent 707fddda79
commit 2d45adf8c3
5 changed files with 144 additions and 12 deletions

View File

@@ -64,7 +64,8 @@ fun configureExternalGateway(home: Path, prompt: Prompt, manifest: ModuleManifes
text("configure.gateway.external.details.buildCommand") to manifest.source.buildCommand.joinToString(" "),
text("configure.gateway.external.details.artifact") to "${manifest.source.artifactDirectory}/${manifest.source.artifactPattern}",
text("configure.gateway.external.details.installTarget") to manifest.install.target,
text("configure.gateway.external.details.configTarget") to (manifest.config?.target ?: text("configure.gateway.external.details.noConfig")),
text("configure.gateway.external.details.configTarget") to (manifest.config?.target
?: text("configure.gateway.external.details.noConfig")),
),
)
@@ -138,18 +139,43 @@ private fun askField(prompt: Prompt, field: Field): JsonElement? {
}
}
@Suppress("KotlinConstantConditions")
private fun validateFieldValue(field: Field, value: String): String? {
if (value.isBlank() && !field.required) return null
return when (field.type) {
FieldType.STRING -> null
FieldType.INT -> value.toIntOrNull()?.let { null } ?: text("configure.field.error.int", field.label)
FieldType.NUMBER -> value.toDoubleOrNull()?.let { null } ?: text("configure.field.error.number", field.label)
FieldType.BOOLEAN -> value.toBooleanStrictOrNull()?.let { null } ?: text("configure.field.error.boolean", field.label)
FieldType.RAW_JSON -> runCatching { Json.parseToJsonElement(value) }
.exceptionOrNull()
?.let { text("configure.field.error.rawJson", field.label) }
FieldType.INT -> {
if (value.toIntOrNull() == null) {
text("configure.field.error.int", field.label)
} else {
null
}
}
FieldType.NUMBER -> {
if (value.toDoubleOrNull() == null) {
text("configure.field.error.number", field.label)
} else {
null
}
}
FieldType.BOOLEAN -> {
if (value.toBooleanStrictOrNull() == null) {
text("configure.field.error.boolean", field.label)
} else {
null
}
}
FieldType.RAW_JSON -> {
val result = runCatching { Json.parseToJsonElement(value) }.exceptionOrNull()
if (result == null) {
text("configure.field.error.rawJson", field.label)
} else {
null
}
}
}
}

View File

@@ -9,7 +9,7 @@ import java.nio.file.Paths
import kotlin.io.path.isDirectory
import kotlin.io.path.name
private const val PARTNER_REPO_URL = "https://gitea.slhaf.work/slhaf/Partner.git"
private const val PARTNER_REPO_URL = "https://github.com/slhaf/Partner.git"
fun buildFromSource(home: Path, prompt: Prompt) {
buildAndInstallFromSource(

View File

@@ -1,10 +1,20 @@
package work.slhaf.partner.ctl.support
import kotlinx.serialization.Serializable
import kotlinx.serialization.json.Json
private const val registryUrl = "https://raw.githubusercontent.com/slhaf/Partner/refs/heads/master/registry"
private const val indexUrl = "$registryUrl/index.json"
private val registryIndex = run {
Json.decodeFromString<RegistryIndex>(fetchText(indexUrl))
}
private fun loadModules(): Set<ModuleManifest> {
// TODO: 待实现具体加载逻辑
return emptySet()
return registryIndex.externalModules.map { indexItem ->
val manifestStr = fetchText("$registryUrl/${indexItem.registryRef}")
return@map Json.decodeFromString<ModuleManifest>(manifestStr)
}.toSet()
}
fun loadAvailableGateway(): Set<ModuleManifest> {
@@ -92,3 +102,35 @@ enum class FieldType {
BOOLEAN,
RAW_JSON,
}
@Serializable
data class RegistryIndex(
val partner: PartnerIndex,
val externalModules: List<ModulesIndexItem>
)
@Serializable
data class PartnerIndex(
val latestBuildable: Buildable,
val latestRelease: Release
) {
@Serializable
data class Buildable(
val url: String,
val ref: String
)
@Serializable
data class Release(
val url: String,
val version: String
)
}
@Serializable
data class ModulesIndexItem(
val name: String,
val version: String,
val withGateway: Boolean,
val registryRef: String
)

View File

@@ -0,0 +1,52 @@
package work.slhaf.partner.ctl.support
import java.io.IOException
import java.net.URI
import java.net.http.*
import java.time.Duration
private val httpClient: HttpClient = HttpClient.newBuilder()
.connectTimeout(Duration.ofSeconds(20))
.followRedirects(HttpClient.Redirect.NEVER)
.build()
fun fetchText(url: String): String {
var lastError: Exception? = null
repeat(3) { attempt ->
try {
val request = HttpRequest.newBuilder()
.uri(URI.create(url))
.timeout(Duration.ofSeconds(60))
.header("User-Agent", "partnerctl")
.GET()
.build()
val response = httpClient.send(
request,
HttpResponse.BodyHandlers.ofString()
)
if (response.statusCode() !in 200..299) {
throw IOException("Failed to fetch $url: HTTP ${response.statusCode()}")
}
return response.body()
} catch (e: HttpTimeoutException) {
lastError = e
} catch (e: HttpConnectTimeoutException) {
lastError = e
} catch (e: IOException) {
lastError = e
} catch (e: InterruptedException) {
Thread.currentThread().interrupt()
throw IOException("Interrupted while fetching $url", e)
}
if (attempt < 2) {
Thread.sleep(500L * (attempt + 1))
}
}
throw IOException("Failed to fetch $url after retries", lastError)
}

View File

@@ -0,0 +1,12 @@
package experimental
import kotlinx.serialization.json.Json
import work.slhaf.partner.ctl.support.RegistryIndex
import work.slhaf.partner.ctl.support.fetchText
fun main() {
val str = fetchText("https://raw.githubusercontent.com/slhaf/Partner/refs/heads/master/registry/index.json")
val index = Json.decodeFromString<RegistryIndex>(str)
println(index)
}